Skip to content

[Bug]: Decision-pack rewardUpside lane shares omit the OSS_EMISSION_SHARE factor, so direct-PR / issue-discovery upside (and the repo priority score) are inflated by ~11% #391

Description

@philluiz2323

Summary

buildRepoDecision (src/services/decision-pack.ts) computes a repo's rewardUpside lane shares straight from the raw registry emissionShare, without the OSS_EMISSION_SHARE (0.9) factor that every other path applies:

// src/services/decision-pack.ts:679-684
const rewardUpside = {
  emissionShare: round(config?.emissionShare ?? 0),
  directPrShare: round((config?.emissionShare ?? 0) * (1 - (config?.issueDiscoveryShare ?? 0))),     // <- no * ossEmissionShare
  issueDiscoveryShare: round((config?.emissionShare ?? 0) * (config?.issueDiscoveryShare ?? 0)),     // <- no * ossEmissionShare
  maintainerCut: round(config?.maintainerCut ?? 0),
};

The canonical lane math in buildScorePreview (src/scoring/preview.ts:270-322) applies the factor and keeps the raw emission share distinct from the OSS-adjusted slices:

// src/scoring/preview.ts:270-275
const emissionShare = clamp(config?.emissionShare ?? 0, 0, 1);
const ossEmissionShare = constant(constants, "OSS_EMISSION_SHARE", 0.9);
const repoSlice = emissionShare * ossEmissionShare;             // actual OSS mining pool for the repo
const directPrSlice = repoSlice * (1 - issueDiscoveryShare);    // split of the *mining* pool
const issueDiscoverySlice = repoSlice * issueDiscoveryShare;
// ...exposed as laneMath: { repoEmissionShare: emissionShare (raw), repoSlice, directPrSlice, issueDiscoverySlice }

So directPrShare/issueDiscoveryShare in the decision pack are the analogs of preview's directPrSlice/issueDiscoverySlice, but they are computed off the raw emission share and are therefore inflated by 1 / 0.9 ≈ 11.1%.

Why this is wrong

OSS_EMISSION_SHARE is the fraction of a repo's emission that is actually available to OSS mining (the rest is allocated elsewhere). directPrShare/issueDiscoveryShare are meant to represent the tokens a miner can earn in each lane — i.e. a split of the mining pool, exactly like directPrSlice/issueDiscoverySlice in preview.ts.

A direct proof the value is impossible: with issueDiscoveryShare = 0, the current code yields directPrShare = emissionShare — equal to the repo's entire raw emission. But the whole OSS mining pool for the repo is only emissionShare * 0.9 (preview.repoSlice). A single lane's share thus exceeds the total mining pool it is drawn from, which cannot be correct.

The two paths also disagree with each other:

  • reward-risk.ts (buildRepoRewardRisk, line 283-288) builds its rewardUpside from currentPreview.laneMath.repoSlice / directPrSlice / issueDiscoverySlice — the correctly OSS-adjusted values.
  • decision-pack.ts re-derives the lane shares itself from the raw emissionShare and drops the factor.

So for the same repo, the reward-risk surface and the decision-pack surface report different lane upside, and the decision-pack one is too high.

Downstream impact

rewardUpside is not cosmetic — it drives the repo ranking. priorityFor (decision-pack.ts:1094-1109) uses:

const upside = Math.max(rewardUpside.directPrShare, rewardUpside.issueDiscoveryShare, rewardUpside.emissionShare * 0.35) * 1000;
// ...priorityScore = clamp(base + upside + history - queuePenalty - blockerPenalty, 0, 100)

Because directPrShare/issueDiscoveryShare are inflated, upside is inflated, so priorityScore is too high. priorityScore is what sorts pursueRepos, cleanupFirst, topActions, etc. (decision-pack.ts:589-590, 642-645). The bias is not uniform: repos whose registry split makes the Math.max resolve to a lane share (rather than the emissionShare * 0.35 floor term, which uses the raw value and is unchanged) get a bigger boost, so the relative ranking of repos is distorted, not just shifted by a constant.

The inflated directPrShare/issueDiscoveryShare are also rendered verbatim in the miner-facing reason strings (decision-pack.ts:1274, 1592, 1594), e.g. "direct PR lane share <X>", overstating the reward a contributor should expect.

Failure mode (concrete example)

Repo with emissionShare = 0.02, issueDiscoveryShare = 1 (a pure issue-discovery repo):

  • Current: rewardUpside.issueDiscoveryShare = 0.02 * 1 = 0.02.
  • Correct: 0.02 * 0.9 * 1 = 0.018, matching preview.laneMath.issueDiscoverySlice for the same repo.

The decision pack tells the miner the issue-discovery lane upside is 0.02 and ranks the repo accordingly, while the scoring preview / reward-risk surface says 0.018. The contributor sees an ~11% inflated upside and a correspondingly inflated priority.

Steps to reproduce

  1. Call buildRepoDecision (or build a full contributor decision pack) for a registered repo with a non-zero emissionShare.
  2. Compare decision.rewardUpside.directPrShare / issueDiscoveryShare against the same repo's buildScorePreview(...).laneMath.directPrSlice / issueDiscoverySlice.
  3. Observe the decision-pack values are larger by a factor of 1 / OSS_EMISSION_SHARE (≈1.111), and that directPrShare can equal the full raw emissionShare (exceeding laneMath.repoSlice).

Expected behavior

Decision-pack lane shares match the canonical lane math: directPrShare = emissionShare * OSS_EMISSION_SHARE * (1 - issueDiscoveryShare) and issueDiscoveryShare = emissionShare * OSS_EMISSION_SHARE * issueDiscoveryShare, consistent with preview.laneMath and reward-risk.rewardUpside. The raw emissionShare field stays raw (it mirrors laneMath.repoEmissionShare).

Actual behavior

The lane shares are computed off the raw emissionShare with no OSS_EMISSION_SHARE factor, so they are inflated by ~11.1% and a single lane share can exceed the repo's entire mining pool, inflating priorityFor's upside and the repo's priorityScore.

Suggested fix

  • Thread the OSS_EMISSION_SHARE constant into buildRepoDecision from the scoring snapshot that the decision-pack builder already loads (scoringSnapshot is in scope at the buildContributorDecisionPack call site, decision-pack.ts:462), defaulting to 0.9 for direct callers — mirroring constant(constants, "OSS_EMISSION_SHARE", 0.9) in preview.ts.
  • Multiply the two lane shares by that factor:
    const ossEmissionShare = args.ossEmissionShare ?? 0.9;
    const baseEmission = config?.emissionShare ?? 0;
    const rewardUpside = {
      emissionShare: round(baseEmission),                                                    // raw, unchanged
      directPrShare: round(baseEmission * ossEmissionShare * (1 - (config?.issueDiscoveryShare ?? 0))),
      issueDiscoveryShare: round(baseEmission * ossEmissionShare * (config?.issueDiscoveryShare ?? 0)),
      maintainerCut: round(config?.maintainerCut ?? 0),
    };
  • Add fail-on-revert coverage: a buildRepoDecision whose rewardUpside.issueDiscoveryShare / directPrShare equals the OSS-adjusted value (and matches buildScorePreview(...).laneMath), not the raw split.

Metadata

Metadata

Assignees

No one assigned

    Labels

    slopAI slop and/or attempts to game additional points via manipulation or alt profiles.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions