Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/loopover-engine/src/opportunity-freshness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions packages/loopover-engine/test/opportunity-freshness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});