diff --git a/packages/loopover-engine/src/opportunity-freshness.ts b/packages/loopover-engine/src/opportunity-freshness.ts index 0c4743f25..8816c2f36 100644 --- a/packages/loopover-engine/src/opportunity-freshness.ts +++ b/packages/loopover-engine/src/opportunity-freshness.ts @@ -45,15 +45,18 @@ export const opportunityFreshnessInternals = { /* v8 ignore stop */ /** - * Compute a [0.05, 1] freshness factor from open issue timestamps, mirroring - * `opportunityFreshnessFactor` in `src/signals/reward-risk.ts` with an injected clock so the miner engine - * stays pure and testable. + * Compute an open-issue freshness factor from issue timestamps, with an injected clock so the miner engine + * stays pure and testable. Returns `0` as a sentinel for "no measurable freshness signal" — either there are + * no open issues, or `nowMs` is non-finite — and otherwise a value in `[0.05, 1]`. `0` is meaningfully distinct + * from `0.05` (measured but maximally stale): in the multiplicative ranker score they differ 20x, so a caller + * must not treat a `0` as merely "very stale". `opportunityFreshnessFactor` in + * `packages/loopover-engine/src/reward-risk.ts` delegates to this function (#8011) — this module is the + * canonical implementation, not a mirror of it. */ export function computeOpportunityFreshness( issues: readonly FreshnessIssue[], nowMs: number, ): number { - /* v8 ignore next -- Caller supplies a finite epoch; non-finite clocks degrade to zero freshness. */ if (!Number.isFinite(nowMs)) return 0; const openIssues = issues.filter(isOpenIssue); if (openIssues.length === 0) return 0; diff --git a/packages/loopover-engine/test/opportunity-freshness.test.ts b/packages/loopover-engine/test/opportunity-freshness.test.ts index 6e0172391..09d78ced2 100644 --- a/packages/loopover-engine/test/opportunity-freshness.test.ts +++ b/packages/loopover-engine/test/opportunity-freshness.test.ts @@ -78,3 +78,19 @@ test("computeOpportunityFreshness handles large open-issue lists without spreadi const score = computeOpportunityFreshness(issues, nowMs); assert.ok(score > 0.7); }); + +test("#9618: a non-finite clock returns the 0 no-signal sentinel", () => { + assert.equal(computeOpportunityFreshness([{ state: "open", updatedAt: "2026-07-01T00:00:00.000Z" }], Number.NaN), 0); + assert.equal(computeOpportunityFreshness([{ state: "open", updatedAt: "2026-07-01T00:00:00.000Z" }], Number.POSITIVE_INFINITY), 0); +}); + +test("#9618: no open issues returns the 0 no-signal sentinel", () => { + assert.equal(computeOpportunityFreshness([], 1_700_000_000_000), 0); + assert.equal(computeOpportunityFreshness([{ state: "closed", updatedAt: "2023-11-14T00:00:00.000Z" }], 1_700_000_000_000), 0); +}); + +test("#9618: the measured range floor and ceiling are exactly 0.05 and 1", () => { + // ~5 years old -> exp(-age/20) underflows the clamp to the 0.05 floor; same-day (age 0) -> exp(0) = 1. + assert.equal(computeOpportunityFreshness([{ state: "open", updatedAt: "2018-11-14T00:00:00.000Z" }], 1_700_000_000_000), 0.05); + assert.equal(computeOpportunityFreshness([{ state: "open", updatedAt: "2023-11-14T00:00:00.000Z" }], 1_700_000_000_000), 1); +});