From f9d4b50758243d9a7ffe220401730777fb52f194 Mon Sep 17 00:00:00 2001 From: kai392 Date: Wed, 29 Jul 2026 09:20:49 +0800 Subject: [PATCH] fix(engine): correct computeOpportunityFreshness's contract and cover the non-finite clock (#9618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its JSDoc claimed a [0.05, 1] range and said it mirrors `opportunityFreshnessFactor` in `src/signals/reward-risk.ts` — both wrong: two paths return the 0 no-signal sentinel (no open issues, or a non-finite clock), and #8011 made `opportunityFreshnessFactor` in `packages/loopover-engine/src/reward-risk.ts` delegate TO this module, so this is the canonical implementation, not a mirror. The JSDoc now states the real 0-sentinel cases and the [0.05, 1] measured range (and that 0 differs 20x from 0.05 in the ranker score). The `/* v8 ignore next */` on the non-finite-`nowMs` guard was also false — that guard is reachable from reward-risk.ts:289's `args.nowMs ?? Date.now()` — so it's removed and the branch is now covered by a real test. No behaviour change: the two 0 returns, the clamp(exp(-age/20), 0.05, 1) formula, round4, and pickTimestamp order are untouched. Tests (engine node:test suite): non-finite clock -> 0, no open issues -> 0, and the exact measured floor/ceiling (0.05 / 1). The root vitest suite already covers the non-finite branch, so both the engine and backend Codecov flags union to cover it. Closes #9618 Co-Authored-By: Claude Opus 4.8 --- .../loopover-engine/src/opportunity-freshness.ts | 11 +++++++---- .../test/opportunity-freshness.test.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/loopover-engine/src/opportunity-freshness.ts b/packages/loopover-engine/src/opportunity-freshness.ts index 0c4743f250..8816c2f366 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 6e0172391e..09d78ced21 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); +});