Skip to content

engine(freshness): computeOpportunityFreshness's documented range and mirror ref are wrong #9618

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

packages/loopover-engine/src/opportunity-freshness.ts documents computeOpportunityFreshness as:

"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."

Three things in that sentence are wrong or misleading:

  1. The range is not [0.05, 1]. Two paths return 0, outside the stated interval: a non-finite
    nowMs, and an issue list with no open issues. The clamp on the main path really is [0.05, 1],
    which makes 0 a meaningful sentinel — but a caller reading the JSDoc has no way to know a 0 is
    possible, and 0 vs 0.05 is a 20x difference in the multiplicative ranker score
    (potential x feasibility x laneFit x freshness x (1 - dupRisk)).
  2. The named function is not at that path. src/signals/reward-risk.ts contains no
    opportunityFreshnessFactor (grep returns zero hits for freshness in that file). The real one is
    at packages/loopover-engine/src/reward-risk.ts:919.
  3. The relationship is inverted. opportunityFreshnessFactor still hand-duplicates its arithmetic and reads live Date.now(), unlike opportunityCompetitionFactor which #7529 converted to delegate to the pure mirror #8011 rewrote reward-risk.ts's opportunityFreshnessFactor to
    delegate to this module (return computeOpportunityFreshness(issues, nowMs);, with a comment
    saying so). This module is now the canonical implementation, not a mirror of one — the sibling
    opportunityCompetitionFactor got the identical treatment in fix(engine): opportunityCompetitionFactor doesn't fail closed on non-finite inputs like its own engine-mirror sibling does #7529 and its call site says so
    explicitly.

Separately, the non-finite-nowMs guard is suppressed from coverage with
/* v8 ignore next -- Caller supplies a finite epoch; non-finite clocks degrade to zero freshness. */
(line 56). That claim does not hold: the guard is reachable from
packages/loopover-engine/src/reward-risk.ts:289, which passes args.nowMs ?? Date.now() — a
caller-supplied value with no finiteness check of its own. The file is inside coverage.include, so
this is a live branch hidden from the gate.

Requirements

  • The JSDoc on computeOpportunityFreshness MUST state the real contract: 0 when there are no open
    issues or the clock is non-finite, otherwise a value in [0.05, 1]. It MUST name what 0 means
    (no measurable freshness signal), so a caller can distinguish it from 0.05 (measured, maximally
    stale).
  • The stale cross-reference MUST be corrected to opportunityFreshnessFactor in
    packages/loopover-engine/src/reward-risk.ts, and reworded to state that that function delegates
    to this one
    (opportunityFreshnessFactor still hand-duplicates its arithmetic and reads live Date.now(), unlike opportunityCompetitionFactor which #7529 converted to delegate to the pure mirror #8011), not that this one mirrors it — matching the wording style of the
    opportunityCompetitionFactor delegation comment at
    packages/loopover-engine/src/reward-risk.ts:908-917.
  • The /* v8 ignore next */ on the non-finite-nowMs guard MUST be deleted and the branch covered by
    a real test.
  • No behaviour change: the two 0 returns, the clamp(exp(-age/20), 0.05, 1) formula, the round4
    rounding, and the pickTimestamp order (updatedAt then createdAt, each only if parseable) MUST be
    byte-for-byte unchanged.

⚠️ Required pattern: mirror the delegation-comment wording already used at
packages/loopover-engine/src/reward-risk.ts:908-917 (opportunityCompetitionFactor, #7529) and
:919-929 (opportunityFreshnessFactor, #8011). What does NOT satisfy this issue: changing the
0 returns to 0.05 (that silently reweights every ranked candidate); deleting the sentinel;
adding a new exported constant for it; or moving the function.

Deliverables

  • The JSDoc on computeOpportunityFreshness in
    packages/loopover-engine/src/opportunity-freshness.ts states the 0-sentinel cases and the
    [0.05, 1] range for the measured case, and cites
    packages/loopover-engine/src/reward-risk.ts with the delegation direction stated correctly.
  • The /* v8 ignore next */ above the !Number.isFinite(nowMs) guard is removed.
  • A new named test asserting computeOpportunityFreshness([{ state: "open" }], Number.NaN) === 0
    and the same for Number.POSITIVE_INFINITY.
  • A new named test asserting computeOpportunityFreshness([], 1_700_000_000_000) === 0 and
    computeOpportunityFreshness([{ state: "closed", updatedAt: <now> }], 1_700_000_000_000) === 0
    (no open issues).
  • A new named test asserting the measured floor: an issue whose updatedAt is ~5 years old returns
    exactly 0.05, and a same-day issue returns 1.
  • git diff shows no change to any expression inside computeOpportunityFreshness,
    issueAgeDays, pickTimestamp, round4, or clamp.

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example
correcting the JSDoc without removing the coverage suppression and testing the non-finite-clock branch
— does not resolve this issue.

Test Coverage Requirements

packages/loopover-engine/src/**/*.ts is inside coverage.include in vitest.config.ts and
carries its own engine Codecov flag; the 99%+ branch-counted codecov/patch gate applies to every
line this PR touches, including the guard whose suppression is being removed. Both arms of that guard
need a test (a finite clock and a non-finite one), as do both arms of the open-issue filter (at least
one open issue, and none). The non-finite-clock test is the required named regression test for the
removed suppression.

Expected Outcome

A caller reading computeOpportunityFreshness's JSDoc learns that 0 is a real return value and what
it means, is pointed at a function that actually exists and told which way the delegation runs, and the
non-finite-clock branch is graded by the coverage gate instead of suppressed from it.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions