Context
// packages/loopover-engine/src/reward-risk.ts:899-901
function opportunityCompetitionFactor(highRiskDuplicateClusters: number, openPullRequests: number): number {
return round(clamp(highRiskDuplicateClusters / Math.max(1, openPullRequests), 0, 1));
}
This is the original, hosted implementation — packages/loopover-engine/src/opportunity-competition.ts's
computeOpportunityCompetition is explicitly documented as its pure, injectable-clock mirror for the
miner engine ("mirroring opportunityCompetitionFactor in src/signals/reward-risk.ts so the miner
engine can derive dupRisk inputs without importing hosted signal code"). But the mirror was hardened
in a way the original never was:
// packages/loopover-engine/src/opportunity-competition.ts
function finiteNonNegative(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.max(0, value);
}
function failClosedClusterPressure(value: number): number {
// Non-finite input (NaN/±Infinity) means the duplicate-cluster signal is broken, not absent, so
// treat it as maximal pressure instead of `finiteNonNegative`'s fail-open 0 — dividing by
// `Math.max(1, openPrs)` and clamping below then yields the maximum competition factor of 1.
if (!Number.isFinite(value)) return Number.POSITIVE_INFINITY;
return Math.max(0, value);
}
export function computeOpportunityCompetition(highRiskDuplicateClusters: number, openPullRequests: number): number {
const clusters = failClosedClusterPressure(highRiskDuplicateClusters);
const openPrs = finiteNonNegative(openPullRequests);
return round4(clamp(clusters / Math.max(1, openPrs), 0, 1));
}
The original opportunityCompetitionFactor has neither guard: it passes highRiskDuplicateClusters and
openPullRequests straight into the arithmetic. Concretely, if openPullRequests is NaN (e.g. an
upstream aggregation bug, or a caller propagating a bad reading), Math.max(1, NaN) evaluates to NaN
(one NaN operand makes Math.max/Math.min return NaN unconditionally), so the whole expression —
clamp(x / NaN, 0, 1) — becomes NaN (since Math.min(1, NaN) and Math.max(0, NaN) both propagate
NaN through clamp), and round(NaN) is NaN. That NaN then flows into
competitionFactor at reward-risk.ts:283 and downstream into the reward/risk score the maintainer
gate's own ranking depends on — silently, since nothing here throws or logs. The comment on the mirror's
failClosedClusterPressure makes clear this was a deliberate, reasoned hardening (fail closed to
maximum pressure, not fail open to zero) — it just never made it back into the original it mirrors.
Requirements
- Add the same two guards to
opportunityCompetitionFactor in reward-risk.ts: a non-finite
highRiskDuplicateClusters must fail closed to maximal pressure (Number.POSITIVE_INFINITY before the
Math.max(1, openPullRequests) division, exactly mirroring failClosedClusterPressure's documented
rationale), and a non-finite/negative openPullRequests must normalize to a finite non-negative number
(mirroring finiteNonNegative) before Math.max(1, ...).
- Either import the two mirror's helper functions directly (
opportunity-competition.ts is already a
sibling module in the same package) or duplicate the identical two guard functions locally with a
comment cross-referencing the mirror — do not silently diverge the two implementations again.
- Do not change
opportunityFreshnessFactor or any other function in this file — this issue is scoped to
opportunityCompetitionFactor only (its opportunityFreshnessFactor sibling was checked and already
matches its own mirror in opportunity-freshness.ts correctly).
Deliverables
Test Coverage Requirements
99%+ Codecov patch gate, branch-counted, on src/**. Add regression tests asserting
opportunityCompetitionFactor(NaN, 5) returns 1 (maximal pressure, not NaN), and
opportunityCompetitionFactor(2, NaN) returns a finite, clamped value (not NaN) — mirroring
computeOpportunityCompetition's own existing test cases for the identical inputs.
Expected Outcome
A non-finite duplicate-cluster count or open-PR count fed into the hosted opportunityCompetitionFactor
can no longer silently poison the reward/risk score with NaN; it fails closed to maximal competition
pressure exactly like its already-hardened engine-mirror sibling does, so the two can never disagree on
this edge case again.
Links & Resources
packages/loopover-engine/src/opportunity-competition.ts — the already-hardened mirror to converge onto.
packages/loopover-engine/src/opportunity-freshness.ts — confirmed-clean sibling (checked, not part of this issue).
Context
This is the original, hosted implementation —
packages/loopover-engine/src/opportunity-competition.ts'scomputeOpportunityCompetitionis explicitly documented as its pure, injectable-clock mirror for theminer engine ("mirroring
opportunityCompetitionFactorinsrc/signals/reward-risk.tsso the minerengine can derive
dupRiskinputs without importing hosted signal code"). But the mirror was hardenedin a way the original never was:
The original
opportunityCompetitionFactorhas neither guard: it passeshighRiskDuplicateClustersandopenPullRequestsstraight into the arithmetic. Concretely, ifopenPullRequestsisNaN(e.g. anupstream aggregation bug, or a caller propagating a bad reading),
Math.max(1, NaN)evaluates toNaN(one
NaNoperand makesMath.max/Math.minreturnNaNunconditionally), so the whole expression —clamp(x / NaN, 0, 1)— becomesNaN(sinceMath.min(1, NaN)andMath.max(0, NaN)both propagateNaNthroughclamp), andround(NaN)isNaN. ThatNaNthen flows intocompetitionFactoratreward-risk.ts:283and downstream into the reward/risk score the maintainergate's own ranking depends on — silently, since nothing here throws or logs. The comment on the mirror's
failClosedClusterPressuremakes clear this was a deliberate, reasoned hardening (fail closed tomaximum pressure, not fail open to zero) — it just never made it back into the original it mirrors.
Requirements
opportunityCompetitionFactorinreward-risk.ts: a non-finitehighRiskDuplicateClustersmust fail closed to maximal pressure (Number.POSITIVE_INFINITYbefore theMath.max(1, openPullRequests)division, exactly mirroringfailClosedClusterPressure's documentedrationale), and a non-finite/negative
openPullRequestsmust normalize to a finite non-negative number(mirroring
finiteNonNegative) beforeMath.max(1, ...).opportunity-competition.tsis already asibling module in the same package) or duplicate the identical two guard functions locally with a
comment cross-referencing the mirror — do not silently diverge the two implementations again.
opportunityFreshnessFactoror any other function in this file — this issue is scoped toopportunityCompetitionFactoronly (itsopportunityFreshnessFactorsibling was checked and alreadymatches its own mirror in
opportunity-freshness.tscorrectly).Deliverables
opportunityCompetitionFactorinreward-risk.tsfails closed to maximal competition pressure on a non-finitehighRiskDuplicateClusters, and normalizes a non-finite/negativeopenPullRequeststo a safe finite value, matchingcomputeOpportunityCompetition's documented behavior exactly.opportunity-competition.tsas the sibling this must stay in sync with.Test Coverage Requirements
99%+ Codecov patch gate, branch-counted, on
src/**. Add regression tests assertingopportunityCompetitionFactor(NaN, 5)returns1(maximal pressure, notNaN), andopportunityCompetitionFactor(2, NaN)returns a finite, clamped value (notNaN) — mirroringcomputeOpportunityCompetition's own existing test cases for the identical inputs.Expected Outcome
A non-finite duplicate-cluster count or open-PR count fed into the hosted
opportunityCompetitionFactorcan no longer silently poison the reward/risk score with
NaN; it fails closed to maximal competitionpressure exactly like its already-hardened engine-mirror sibling does, so the two can never disagree on
this edge case again.
Links & Resources
packages/loopover-engine/src/opportunity-competition.ts— the already-hardened mirror to converge onto.packages/loopover-engine/src/opportunity-freshness.ts— confirmed-clean sibling (checked, not part of this issue).