Skip to content

opportunityFreshnessFactor still hand-duplicates its arithmetic and reads live Date.now(), unlike opportunityCompetitionFactor which #7529 converted to delegate to the pure mirror #8011

Description

@JSONbored

Context

packages/loopover-engine/src/reward-risk.ts's opportunityCompetitionFactor (line 900) was fixed by #7529 (after a NaN-propagation bug) to delegate to the pure mirror computeOpportunityCompetition (./opportunity-competition.js) instead of hand-duplicating its arithmetic — its own comment says: "Delegating -- rather than duplicating the guards -- is what stops the two drifting apart again."

The very next function in the same file, opportunityFreshnessFactor (line 911), was never given the same treatment. It hand-reimplements issueAgeDays/pickIssueTimestamp/isParseableIssueTimestamp instead of importing and delegating to computeOpportunityFreshness (./opportunity-freshness.js), which already exists in the same package with the identical round4/clamp/exp(-age/20) formula.

Worse: computeOpportunityFreshness's signature takes an injected nowMs clock (this codebase's pervasive "no bare Date.now()" purity discipline, used for testability everywhere else), but reward-risk.ts's hand-duplicated issueAgeDays calls Date.now() directly — so buildRepoRewardRisk (which calls opportunityFreshnessFactor(args.issues) with no clock argument) is not actually deterministic the way the rest of the module claims to be. Any future drift between the two copies — like the exact NaN bug #7529 just fixed for its sibling — has no guard against it today.

⚠️ Required pattern — read opportunityCompetitionFactor's post-#7529 delegation shape in the same file before starting. Convert opportunityFreshnessFactor to delegate to computeOpportunityFreshness from ./opportunity-freshness.js, threading through an injectable nowMs clock the same way the freshness module's own signature already expects — do not hand-duplicate the arithmetic a second time.

Requirements

  • opportunityFreshnessFactor must delegate to computeOpportunityFreshness instead of hand-reimplementing issueAgeDays/pickIssueTimestamp/isParseableIssueTimestamp.
  • No more bare Date.now() call in this path — the clock must be injectable, mirroring the rest of this module's purity discipline.
  • buildRepoRewardRisk's call site updates accordingly (threading a clock through, or defaulting to Date.now() only at the call boundary, not inside the pure calculator).
  • Output must remain arithmetically identical for finite inputs versus the current implementation (this is a refactor for correctness/consistency, not a formula change).

Deliverables

  • opportunityFreshnessFactor delegates to computeOpportunityFreshness.
  • No bare Date.now() remains inside the pure calculator path; the clock is injected.
  • Existing reward-risk.ts tests continue to pass (values unchanged for finite inputs).

Test Coverage Requirements

packages/loopover-engine/** — 99%+ Codecov patch target, both branches. Add a test asserting opportunityFreshnessFactor produces identical output to a direct computeOpportunityFreshness call for the same inputs, and a determinism test (same inputs + injected clock → same output, no flakiness from real time).

Expected Outcome

opportunityFreshnessFactor is deterministic and can't silently drift from computeOpportunityFreshness's formula the way opportunityCompetitionFactor briefly did before #7529.

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