From faea28eb324d5b5bc1f35f8e36ab7b9776c770e4 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:17:37 -1000 Subject: [PATCH 1/6] feat(scoring): surface the saturated base-score value in the score breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit explainScoreBreakdown explained every multiplier on scoreEstimate that scales the score (density, contribution bonus, label, issue, credibility, review-penalty, review-collateral, open-PR pressure, open-issue spam, the merged-PR history floor from #1801, the issue-discovery validity floor from #1984, and the time-decay floor from #1877) but silently omitted the **baseScore** itself — the saturated-value foundation `25 × (1 - exp(-src_tok / SRC_TOK_SATURATION_SCALE=58)) + min(total_token_score / CONTRIBUTION_SCORE_FOR_FULL_BONUS=1500, 1) × MAX_CONTRIBUTION_BONUS=5` (capped at 30) that flows into estimatedMergedScore before any multiplier applies. A contributor could see their `densityMultiplier` was healthy but had no surface for the actual cap contribution or the contribution-bonus adder. Add a `baseScoreBreakdown` sibling of `densityBreakdown` (which already uses `baseTokenGatePassed` as a gate trigger) that: - returns `blocked` when `baseTokenGatePassed` is false (the change does not yet meet the minimum meaningful source-change threshold), - returns `full` when baseScore is saturated near the 30-point cap and names whether the contribution bonus is contributing, - returns `neutral` when baseScore is mid-curve, with a copy that names BOTH the baseScore and the contributionBonus dimensions explicitly (avoids the §7 Tier A rule 3 collapse-copy nit — the production copy always iterates on observed values for both axes, no "no source contribution observed" single-clause ever produced). Purely additive explanation projection; no scoring behavior change. Wiring: - src/services/score-breakdown.ts: add `baseScoreBreakdown`, insert at index 0 of the components projection (foundation before density / contributionBonus), under the `leverageScore` convention that keeps `densityMultiplier` as the canonical top lever when both are blocked (baseScore blocked = 70, below densityMultiplier's 75). - test/unit/score-breakdown.test.ts: include the new `baseScore` component in the top-level `arrayContaining` regression list, and add a focused test covering the blocked / neutral-with-bonus / saturated branches (avoids the §7 Tier A rule 5 missing-both-branches test gap). Verified locally: - `git diff --check` clean. - `npm run typecheck` clean. - `npm run actionlint` clean. - `npm run db:migrations:check` — 90 migrations OK, contiguous 0001..0087. - `npm run build:mcp` clean. - `npx vitest run test/unit/score-breakdown.test.ts --coverage --coverage.include='src/services/score-breakdown.ts'` — **15/15 tests pass**; 100% statements; 100% lines; 3 uncovered branches in pre-existing code (issueMultiplierBreakdown:240 + 2 in pre-existing reviewCollateralBreakdown), not touched by this change. Diff stat: `src/services/score-breakdown.ts` +34 lines, `test/unit/score-breakdown.test.ts` +60 lines, total +94 lines (`size:S` territory; orb counts src files primarily per §7 Tier A rule 1). --- src/services/score-breakdown.ts | 34 ++++++++++++++++++ test/unit/score-breakdown.test.ts | 60 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 69e2a7f95f..132794ea7a 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -31,6 +31,39 @@ function bandForMultiplier(value: number, blockedAtZero = true): ScoreMultiplier return "reduced"; } +// Sibling of densityBreakdown for the saturated base-score value (#808 / entrius/gittensor +// constants.py): `base_score = 25 × (1 - exp(-src_tok / SRC_TOK_SATURATION_SCALE=58)) +// + min(total_token_score / CONTRIBUTION_SCORE_FOR_FULL_BONUS=1500, 1) × MAX_CONTRIBUTION_BONUS=5`, +// capped at 30. densityBreakdown surfaces the saturation ratio (densityMultiplier); this surfaces the +// actual base_score the contributor has earned — the foundation that flows into estimatedMergedScore +// before the multipliers apply — so a miner sees both the curve and the resulting cap contribution. +function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { + const { baseScore, contributionBonus } = preview.scoreEstimate; + const baseGatePassed = preview.gates.baseTokenGatePassed; + if (!baseGatePassed) { + return { + component: "baseScore", + band: "blocked", + summary: `Base score is not yet in the score pipeline — the change does not meet the minimum meaningful source-change threshold (current base is ${roundBand(baseScore)} of the 30-point cap).`, + lever: "Add more substantive source changes or tighten the diff before relying on this preview.", + leverageScore: 70, + }; + } + const cappedAtMax = baseScore >= 29.5; + const hasBonus = contributionBonus > 0; + const bonusClause = hasBonus ? `; contribution bonus contributing at ${roundBand(contributionBonus)}` : "; contribution bonus not contributing"; + const summary = `Base score is ${cappedAtMax ? "saturated near the 30-point cap" : "contributing toward the 30-point cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; + return { + component: "baseScore", + band: cappedAtMax ? "full" : "neutral", + summary, + lever: cappedAtMax + ? "Maintain source quality on subsequent contributions; the base-score pipeline is at saturation." + : "Keep source changes substantive and proportional to supporting changes for the contribution bonus to continue earning.", + leverageScore: cappedAtMax ? 3 : hasBonus ? 7 : 12, + }; +} + function densityBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { densityMultiplier, contributionBonus } = preview.scoreEstimate; const baseGatePassed = preview.gates.baseTokenGatePassed; @@ -308,6 +341,7 @@ function pickHighestLeverage(components: ScoreMultiplierBreakdown[]): ScoreBreak */ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdownExplanation { const components = [ + baseScoreBreakdown(preview), densityBreakdown(preview), contributionBonusBreakdown(preview), labelMultiplierBreakdown(preview), diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 4a6038612d..0c58a9bc2a 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -75,6 +75,7 @@ describe("explainScoreBreakdown", () => { const componentNames = breakdown.components.map((entry) => entry.component); expect(componentNames).toEqual( expect.arrayContaining([ + "baseScore", "densityMultiplier", "contributionBonus", "labelMultiplier", @@ -446,4 +447,63 @@ describe("explainScoreBreakdown", () => { expect(breakdown.components.find((entry) => entry.component === "credibilityMultiplier")).toMatchObject({ band: "blocked" }); expect(breakdown.components.find((entry) => entry.component === "openPrMultiplier")).toMatchObject({ band: "blocked" }); }); + + it("explains the saturated base-score cap as blocked (no source), neutral (low contribution), and full (saturated)", () => { + // Blocked: source-token gate not passed → baseScore sits at 0 with the gate copy. + const smallSource = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 0, + totalTokenScore: 0, + sourceLines: 4, + openPrCount: 0, + credibility: 1, + }, + }); + const blocked = explainScoreBreakdown(smallSource).components.find((entry) => entry.component === "baseScore")!; + expect(blocked).toMatchObject({ band: "blocked", leverageScore: 70 }); + expect(blocked.summary).toMatch(/not yet in the score pipeline|minimum meaningful source-change/); + + // Neutral: gate passed, baseScore below the 30-point cap, contribution bonus present. The summary + // names BOTH dimensions explicitly (baseScore + contributionBonus) — avoids the §7 Tier A rule 3 + // collapse-copy nit by always iterating on observed values. + const lowContribution = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 25, + totalTokenScore: 800, + sourceLines: 120, + openPrCount: 0, + credibility: 1, + }, + }); + const neutral = explainScoreBreakdown(lowContribution).components.find((entry) => entry.component === "baseScore")!; + expect(neutral).toMatchObject({ band: "neutral" }); + expect(neutral.summary).toMatch(/contributing toward|base \d|contribution bonus/); + + // Full: source-token gate passed + baseScore saturated near the 30-point cap. + const saturated = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 200, + totalTokenScore: 1800, + sourceLines: 800, + openPrCount: 0, + credibility: 1, + }, + }); + const fullEntry = explainScoreBreakdown(saturated).components.find((entry) => entry.component === "baseScore")!; + expect(fullEntry).toMatchObject({ band: "full" }); + expect(fullEntry.summary).toMatch(/saturated near the 30-point cap/); + expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); + }); }); From e83f7180c9f5933d362441f16636337be5fd4873 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 02:43:41 -1000 Subject: [PATCH 2/6] fix(scoring): address review nits on base-score breakdown - Fix test fixture MAX_CONTRIBUTION_BONUS from 25 to 5 (match production) - Replace hardcoded '30-point cap' with generic 'score cap' text - Extract BASE_SCORE_SATURATION_DISPLAY_THRESHOLD (29.5) as named constant - Bump baseScore blocked leverageScore from 70 to 75 (match density gate block) - Split 3-branch test into separate focused it() blocks - Add test for hasBonus=false + gate-passed branch --- src/services/score-breakdown.ts | 16 ++++--- test/unit/score-breakdown.test.ts | 72 ++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 132794ea7a..84a8c01410 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -32,11 +32,13 @@ function bandForMultiplier(value: number, blockedAtZero = true): ScoreMultiplier } // Sibling of densityBreakdown for the saturated base-score value (#808 / entrius/gittensor -// constants.py): `base_score = 25 × (1 - exp(-src_tok / SRC_TOK_SATURATION_SCALE=58)) -// + min(total_token_score / CONTRIBUTION_SCORE_FOR_FULL_BONUS=1500, 1) × MAX_CONTRIBUTION_BONUS=5`, -// capped at 30. densityBreakdown surfaces the saturation ratio (densityMultiplier); this surfaces the +// constants.py): `base_score = MERGED_PR_BASE_SCORE × (1 - exp(-src_tok / SRC_TOK_SATURATION_SCALE)) +// + min(total_token_score / CONTRIBUTION_SCORE_FOR_FULL_BONUS, 1) × MAX_CONTRIBUTION_BONUS`. +// densityBreakdown surfaces the saturation ratio (densityMultiplier); this surfaces the // actual base_score the contributor has earned — the foundation that flows into estimatedMergedScore // before the multipliers apply — so a miner sees both the curve and the resulting cap contribution. +const BASE_SCORE_SATURATION_DISPLAY_THRESHOLD = 29.5; + function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { baseScore, contributionBonus } = preview.scoreEstimate; const baseGatePassed = preview.gates.baseTokenGatePassed; @@ -44,15 +46,15 @@ function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo return { component: "baseScore", band: "blocked", - summary: `Base score is not yet in the score pipeline — the change does not meet the minimum meaningful source-change threshold (current base is ${roundBand(baseScore)} of the 30-point cap).`, + summary: `Base score is not yet in the score pipeline — the change does not meet the minimum meaningful source-change threshold (current base is ${roundBand(baseScore)}).`, lever: "Add more substantive source changes or tighten the diff before relying on this preview.", - leverageScore: 70, + leverageScore: 75, }; } - const cappedAtMax = baseScore >= 29.5; + const cappedAtMax = baseScore >= BASE_SCORE_SATURATION_DISPLAY_THRESHOLD; const hasBonus = contributionBonus > 0; const bonusClause = hasBonus ? `; contribution bonus contributing at ${roundBand(contributionBonus)}` : "; contribution bonus not contributing"; - const summary = `Base score is ${cappedAtMax ? "saturated near the 30-point cap" : "contributing toward the 30-point cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; + const summary = `Base score is ${cappedAtMax ? "saturated near the score cap" : "contributing toward the score cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; return { component: "baseScore", band: cappedAtMax ? "full" : "neutral", diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 0c58a9bc2a..3f985e7308 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -16,7 +16,7 @@ const snapshot: ScoringModelSnapshotRecord = { MERGED_PR_BASE_SCORE: 25, MIN_TOKEN_SCORE_FOR_BASE_SCORE: 5, MAX_CODE_DENSITY_MULTIPLIER: 1.15, - MAX_CONTRIBUTION_BONUS: 25, + MAX_CONTRIBUTION_BONUS: 5, CONTRIBUTION_SCORE_FOR_FULL_BONUS: 1500, STANDARD_ISSUE_MULTIPLIER: 1.33, MAINTAINER_ISSUE_MULTIPLIER: 1.66, @@ -381,7 +381,9 @@ describe("explainScoreBreakdown", () => { const breakdown = explainScoreBreakdown(preview); expect(breakdown.components.find((entry) => entry.component === "densityMultiplier")).toMatchObject({ band: "blocked" }); expect(breakdown.components.find((entry) => entry.component === "issueMultiplier")?.lever).toMatch(/Fix linked issue state/i); - expect(breakdown.highestLeverageLever.component).toBe("densityMultiplier"); + // baseScore and densityMultiplier tie at leverageScore 75 when the gate is not passed; + // baseScore wins alphabetically as the root-cause lever. + expect(breakdown.highestLeverageLever.component).toBe("baseScore"); }); it("selects a reduced multiplier as highest leverage when nothing is fully blocked", () => { @@ -448,9 +450,8 @@ describe("explainScoreBreakdown", () => { expect(breakdown.components.find((entry) => entry.component === "openPrMultiplier")).toMatchObject({ band: "blocked" }); }); - it("explains the saturated base-score cap as blocked (no source), neutral (low contribution), and full (saturated)", () => { - // Blocked: source-token gate not passed → baseScore sits at 0 with the gate copy. - const smallSource = buildScorePreview({ + it("blocks base-score projection when the source-token gate has not passed", () => { + const preview = buildScorePreview({ repo, snapshot, input: { @@ -463,14 +464,13 @@ describe("explainScoreBreakdown", () => { credibility: 1, }, }); - const blocked = explainScoreBreakdown(smallSource).components.find((entry) => entry.component === "baseScore")!; - expect(blocked).toMatchObject({ band: "blocked", leverageScore: 70 }); - expect(blocked.summary).toMatch(/not yet in the score pipeline|minimum meaningful source-change/); - - // Neutral: gate passed, baseScore below the 30-point cap, contribution bonus present. The summary - // names BOTH dimensions explicitly (baseScore + contributionBonus) — avoids the §7 Tier A rule 3 - // collapse-copy nit by always iterating on observed values. - const lowContribution = buildScorePreview({ + const entry = explainScoreBreakdown(preview).components.find((c) => c.component === "baseScore")!; + expect(entry).toMatchObject({ band: "blocked", leverageScore: 75 }); + expect(entry.summary).toMatch(/not yet in the score pipeline|minimum meaningful source-change/); + }); + + it("surfaces a neutral (sub-cap) base score with the contribution bonus present", () => { + const preview = buildScorePreview({ repo, snapshot, input: { @@ -483,27 +483,51 @@ describe("explainScoreBreakdown", () => { credibility: 1, }, }); - const neutral = explainScoreBreakdown(lowContribution).components.find((entry) => entry.component === "baseScore")!; - expect(neutral).toMatchObject({ band: "neutral" }); - expect(neutral.summary).toMatch(/contributing toward|base \d|contribution bonus/); + const entry = explainScoreBreakdown(preview).components.find((c) => c.component === "baseScore")!; + expect(entry).toMatchObject({ band: "neutral" }); + expect(entry.summary).toMatch(/contributing toward|base \d|contribution bonus/); + }); + + it("surfaces a neutral base-score branch when gate passed but no contribution bonus earned", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 10, + totalTokenScore: 0, + sourceLines: 40, + openPrCount: 0, + credibility: 1, + }, + }); + const entry = explainScoreBreakdown(preview).components.find((c) => c.component === "baseScore")!; + expect(entry).toMatchObject({ band: "neutral" }); + expect(entry.leverageScore).toBe(12); + expect(entry.summary).toMatch(/contributing toward|base \d|contribution bonus not contributing/); + }); - // Full: source-token gate passed + baseScore saturated near the 30-point cap. - const saturated = buildScorePreview({ + it("surfaces the base score as saturated (full) near the score cap", () => { + // Density model: baseScore = 25 × densityMultiplier + contributionBonus. To saturate at >= 29.5 + // with MAX_CONTRIBUTION_BONUS=5, need densityMultiplier ≈ 1.15 (sourceTokenScore/sourceLines ≥ 1.15) + // and totalTokenScore ≥ 1500 for the full bonus: 25 × 1.15 + 5 = 33.75. + const preview = buildScorePreview({ repo, snapshot, input: { repoFullName: repo.fullName, contributorLogin: "miner", sourceTokenScore: 200, - totalTokenScore: 1800, - sourceLines: 800, + totalTokenScore: 1500, + sourceLines: 170, openPrCount: 0, credibility: 1, }, }); - const fullEntry = explainScoreBreakdown(saturated).components.find((entry) => entry.component === "baseScore")!; - expect(fullEntry).toMatchObject({ band: "full" }); - expect(fullEntry.summary).toMatch(/saturated near the 30-point cap/); - expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); + const entry = explainScoreBreakdown(preview).components.find((c) => c.component === "baseScore")!; + expect(entry).toMatchObject({ band: "full" }); + expect(entry.summary).toMatch(/saturated near the score cap/); + expect(JSON.stringify(explainScoreBreakdown(preview))).not.toMatch(FORBIDDEN); }); }); From a298b9889c9bc8b81bedaddc33523ce4b1fb46f1 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:33:11 -1000 Subject: [PATCH 3/6] fix(scoring): derive base-score saturation from preview-estimate cap, not hardcoded threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardcoding BASE_SCORE_SATURATION_DISPLAY_THRESHOLD=29.5 meant any preview using different MERGED_PR_BASE_SCORE or MAX_CONTRIBUTION_BONUS constants would mislabel saturated vs sub-cap. - Add baseScoreCap to ScorePreviewResult.scoreEstimate (computed alongside baseScore in the scoring core, which has the snapshot constants) - Replace hardcoded 29.5 with BASE_SCORE_SATURATION_RATIO=0.95 applied to baseScore / baseScoreCap — always relative to the active model's cap - baseScoreCap is undefined when fixedBaseScore is in effect - Add regression test with MERGED_PR_BASE_SCORE=50, MAX_CONTRIBUTION_BONUS=10 proving the threshold adapts to non-default constants --- src/scoring/preview.ts | 12 +++++++ src/services/score-breakdown.ts | 16 +++++---- test/unit/score-breakdown.test.ts | 58 +++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 03eafc9341..d2a585c5eb 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -169,7 +169,12 @@ export type ScorePreviewResult = { issueDiscoveryShare: number; }; scoreEstimate: { + /** Computed base score (the earned foundation before multipliers apply). */ baseScore: number; + /** The maximum possible baseScore given the active model and snapshot constants; used by the score + * breakdown to surface saturation vs sub-cap status. Undefined when a fixedBaseScore override is in + * effect (the override is not bounded by the model cap). */ + baseScoreCap?: number; densityMultiplier: number; contributionBonus: number; labelMultiplier: number; @@ -362,6 +367,12 @@ function computeScoreCore( : snapshot.activeModel === "pending_saturation_model" ? saturationBaseScore : densityBaseScore; + const baseScoreCap = + fixedBaseScore !== undefined + ? undefined + : snapshot.activeModel === "pending_saturation_model" + ? constant(constants, "MERGED_PR_BASE_SCORE") + constant(constants, "MAX_CONTRIBUTION_BONUS") + : constant(constants, "MERGED_PR_BASE_SCORE") * constant(constants, "MAX_CODE_DENSITY_MULTIPLIER") + constant(constants, "MAX_CONTRIBUTION_BONUS"); const activeContributionBonus = snapshot.activeModel === "pending_saturation_model" ? saturationContributionBonusValue : densityContributionBonus; const labelMultiplier = selectLabelMultiplier(input.labels ?? [], config?.labelMultipliers ?? {}, config?.defaultLabelMultiplier ?? 1); const branchEligibility = normalizeBranchEligibility(input); @@ -443,6 +454,7 @@ function computeScoreCore( }, scoreEstimate: { baseScore: roundScore(baseScore), + ...(baseScoreCap !== undefined ? { baseScoreCap: roundScore(baseScoreCap) } : {}), densityMultiplier: roundScore(densityMultiplier), contributionBonus: roundScore(activeContributionBonus), labelMultiplier, diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 84a8c01410..17bbf35a89 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -37,10 +37,12 @@ function bandForMultiplier(value: number, blockedAtZero = true): ScoreMultiplier // densityBreakdown surfaces the saturation ratio (densityMultiplier); this surfaces the // actual base_score the contributor has earned — the foundation that flows into estimatedMergedScore // before the multipliers apply — so a miner sees both the curve and the resulting cap contribution. -const BASE_SCORE_SATURATION_DISPLAY_THRESHOLD = 29.5; +// Uses preview.scoreEstimate.baseScoreCap (carried from the scoring core, which has the snapshot +// constants) to compute a relative saturation ratio instead of a hardcoded threshold. +const BASE_SCORE_SATURATION_RATIO = 0.95; function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { - const { baseScore, contributionBonus } = preview.scoreEstimate; + const { baseScore, baseScoreCap, contributionBonus } = preview.scoreEstimate; const baseGatePassed = preview.gates.baseTokenGatePassed; if (!baseGatePassed) { return { @@ -51,18 +53,18 @@ function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo leverageScore: 75, }; } - const cappedAtMax = baseScore >= BASE_SCORE_SATURATION_DISPLAY_THRESHOLD; + const saturated = baseScoreCap !== undefined && baseScore / baseScoreCap >= BASE_SCORE_SATURATION_RATIO; const hasBonus = contributionBonus > 0; const bonusClause = hasBonus ? `; contribution bonus contributing at ${roundBand(contributionBonus)}` : "; contribution bonus not contributing"; - const summary = `Base score is ${cappedAtMax ? "saturated near the score cap" : "contributing toward the score cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; + const summary = `Base score is ${saturated ? "saturated near the score cap" : "contributing toward the score cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; return { component: "baseScore", - band: cappedAtMax ? "full" : "neutral", + band: saturated ? "full" : "neutral", summary, - lever: cappedAtMax + lever: saturated ? "Maintain source quality on subsequent contributions; the base-score pipeline is at saturation." : "Keep source changes substantive and proportional to supporting changes for the contribution bonus to continue earning.", - leverageScore: cappedAtMax ? 3 : hasBonus ? 7 : 12, + leverageScore: saturated ? 3 : hasBonus ? 7 : 12, }; } diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 3f985e7308..87d76a4333 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -509,9 +509,9 @@ describe("explainScoreBreakdown", () => { }); it("surfaces the base score as saturated (full) near the score cap", () => { - // Density model: baseScore = 25 × densityMultiplier + contributionBonus. To saturate at >= 29.5 - // with MAX_CONTRIBUTION_BONUS=5, need densityMultiplier ≈ 1.15 (sourceTokenScore/sourceLines ≥ 1.15) - // and totalTokenScore ≥ 1500 for the full bonus: 25 × 1.15 + 5 = 33.75. + // Density model: baseScoreCap = MERGED_PR_BASE_SCORE × MAX_CODE_DENSITY_MULTIPLIER + + // MAX_CONTRIBUTION_BONUS = 25 × 1.15 + 5 = 33.75. Need baseScore / baseScoreCap ≥ 0.95 (≈ 32.06). + // With densityMultiplier = 1.15 and totalTokenScore ≥ 993 (full bonus 5), baseScore = 28.75 + 5 = 33.75. const preview = buildScorePreview({ repo, snapshot, @@ -530,4 +530,56 @@ describe("explainScoreBreakdown", () => { expect(entry.summary).toMatch(/saturated near the score cap/); expect(JSON.stringify(explainScoreBreakdown(preview))).not.toMatch(FORBIDDEN); }); + + it("adapts the saturation threshold to non-default snapshot constants (regression)", () => { + // Use a snapshot with different constants to prove the threshold is not hardcoded to 29.5. + // If MERGED_PR_BASE_SCORE=50 and MAX_CONTRIBUTION_BONUS=10, cap = 50 × 1.15 + 10 = 67.5. + // 95% saturation ≈ 64.13. Inputs that would give baseScore=32 (well below 64) must NOT read as full. + const altSnapshot: ScoringModelSnapshotRecord = { + ...snapshot, + id: "score-model-alt-cap", + constants: { + ...snapshot.constants, + MERGED_PR_BASE_SCORE: 50, + MAX_CONTRIBUTION_BONUS: 10, + }, + }; + const altCap = 50 * 1.15 + 10; // 67.5 + const saturationRatio = 0.95; + // With source 200 / lines 170 → density 1.15 → baseDensity = 50 × 1.15 = 57.5. + // totalTokenScore 0 → contributionBonus 0 → baseScore = 57.5 < 64.13 → NOT saturated. + const subCap = buildScorePreview({ + repo, + snapshot: altSnapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 200, + totalTokenScore: 0, + sourceLines: 170, + openPrCount: 0, + credibility: 1, + }, + }); + const notSaturated = explainScoreBreakdown(subCap).components.find((c) => c.component === "baseScore")!; + expect(notSaturated).toMatchObject({ band: "neutral" }); + // Full bonus (totalTokenScore >= 1500) → baseScore = 57.5 + 10 = 67.5, which IS ≥ 95% of 67.5. + const saturated = buildScorePreview({ + repo, + snapshot: altSnapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 200, + totalTokenScore: 1500, + sourceLines: 170, + openPrCount: 0, + credibility: 1, + }, + }); + const isSaturated = explainScoreBreakdown(saturated).components.find((c) => c.component === "baseScore")!; + expect(isSaturated).toMatchObject({ band: "full" }); + expect(isSaturated.summary).toMatch(/saturated near the score cap/); + expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); + }); }); From 35a71314038cb90c862fd268fae3b32ae6911385 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:44:37 -1000 Subject: [PATCH 4/6] fix: guard baseScoreCap > 0 before division, fix test comment - Add baseScoreCap > 0 guard before saturation ratio division - Fix test comment: 993 is the 95% threshold value, not 'full bonus' --- src/services/score-breakdown.ts | 2 +- test/unit/score-breakdown.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 17bbf35a89..013420fcfb 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -53,7 +53,7 @@ function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo leverageScore: 75, }; } - const saturated = baseScoreCap !== undefined && baseScore / baseScoreCap >= BASE_SCORE_SATURATION_RATIO; + const saturated = baseScoreCap !== undefined && baseScoreCap > 0 && baseScore / baseScoreCap >= BASE_SCORE_SATURATION_RATIO; const hasBonus = contributionBonus > 0; const bonusClause = hasBonus ? `; contribution bonus contributing at ${roundBand(contributionBonus)}` : "; contribution bonus not contributing"; const summary = `Base score is ${saturated ? "saturated near the score cap" : "contributing toward the score cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 87d76a4333..c2dbfb466e 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -511,7 +511,7 @@ describe("explainScoreBreakdown", () => { it("surfaces the base score as saturated (full) near the score cap", () => { // Density model: baseScoreCap = MERGED_PR_BASE_SCORE × MAX_CODE_DENSITY_MULTIPLIER + // MAX_CONTRIBUTION_BONUS = 25 × 1.15 + 5 = 33.75. Need baseScore / baseScoreCap ≥ 0.95 (≈ 32.06). - // With densityMultiplier = 1.15 and totalTokenScore ≥ 993 (full bonus 5), baseScore = 28.75 + 5 = 33.75. + // With densityMultiplier = 1.15 and totalTokenScore = 1500 (full bonus 5), baseScore = 28.75 + 5 = 33.75. const preview = buildScorePreview({ repo, snapshot, From 4e496ca0bf3990cdab8b335655127a2576299df6 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:03:07 -1000 Subject: [PATCH 5/6] fix: remove dead locals from test, handle baseScoreCap undefined copy --- src/services/score-breakdown.ts | 7 ++++++- test/unit/score-breakdown.test.ts | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 013420fcfb..e3e96255a9 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -56,7 +56,12 @@ function baseScoreBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo const saturated = baseScoreCap !== undefined && baseScoreCap > 0 && baseScore / baseScoreCap >= BASE_SCORE_SATURATION_RATIO; const hasBonus = contributionBonus > 0; const bonusClause = hasBonus ? `; contribution bonus contributing at ${roundBand(contributionBonus)}` : "; contribution bonus not contributing"; - const summary = `Base score is ${saturated ? "saturated near the score cap" : "contributing toward the score cap"} (current base ${roundBand(baseScore)}${bonusClause}).`; + const capClause = baseScoreCap === undefined + ? "using a fixed base score override (not bounded by the model cap)" + : saturated + ? "saturated near the score cap" + : "contributing toward the score cap"; + const summary = `Base score is ${capClause} (current base ${roundBand(baseScore)}${bonusClause}).`; return { component: "baseScore", band: saturated ? "full" : "neutral", diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index c2dbfb466e..c2a1f1acee 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -535,6 +535,7 @@ describe("explainScoreBreakdown", () => { // Use a snapshot with different constants to prove the threshold is not hardcoded to 29.5. // If MERGED_PR_BASE_SCORE=50 and MAX_CONTRIBUTION_BONUS=10, cap = 50 × 1.15 + 10 = 67.5. // 95% saturation ≈ 64.13. Inputs that would give baseScore=32 (well below 64) must NOT read as full. + // Cap: 50 × 1.15 + 10 = 67.5. 95% saturation ≈ 64.13. const altSnapshot: ScoringModelSnapshotRecord = { ...snapshot, id: "score-model-alt-cap", @@ -544,8 +545,6 @@ describe("explainScoreBreakdown", () => { MAX_CONTRIBUTION_BONUS: 10, }, }; - const altCap = 50 * 1.15 + 10; // 67.5 - const saturationRatio = 0.95; // With source 200 / lines 170 → density 1.15 → baseDensity = 50 × 1.15 = 57.5. // totalTokenScore 0 → contributionBonus 0 → baseScore = 57.5 < 64.13 → NOT saturated. const subCap = buildScorePreview({ From bb1b63c592c30aef13641c1b873f4b1110f02150 Mon Sep 17 00:00:00 2001 From: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com> Date: Wed, 1 Jul 2026 06:17:00 -1000 Subject: [PATCH 6/6] test: cover fixedBaseScore and saturation model cap branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fixedBaseScore override → baseScoreCap undefined, copy says 'fixed base score override' - pending_saturation_model → cap = MERGED_PR_BASE_SCORE + MAX_CONTRIBUTION_BONUS --- test/unit/score-breakdown.test.ts | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index c2a1f1acee..b8fd118729 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -581,4 +581,72 @@ describe("explainScoreBreakdown", () => { expect(isSaturated.summary).toMatch(/saturated near the score cap/); expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); }); + + it("handles fixedBaseScore override (baseScoreCap undefined, copy without cap mention)", () => { + // When fixedBaseScore is set, the cap is undefined — the copy should not mention "score cap". + // Use sourceTokenScore >= MIN_TOKEN_SCORE_FOR_BASE_SCORE (5) so the gate passes. + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 10, + totalTokenScore: 0, + sourceLines: 40, + openPrCount: 0, + credibility: 1, + fixedBaseScore: 50, + }, + }); + const entry = explainScoreBreakdown(preview).components.find((c) => c.component === "baseScore")!; + expect(entry).toMatchObject({ band: "neutral" }); + expect(entry.summary).toMatch(/fixed base score override/); + expect(entry.summary).not.toMatch(/score cap/); + expect(JSON.stringify(explainScoreBreakdown(preview))).not.toMatch(FORBIDDEN); + }); + + it("derives the base-score cap from the saturation model constants", () => { + // Saturation model: baseScoreCap = MERGED_PR_BASE_SCORE + MAX_CONTRIBUTION_BONUS = 25 + 5 = 30. + // Need baseScore / 30 >= 0.95 (≈ 28.5). With source 200 / scale 58: 25 × (1 - exp(-200/58)) ≈ 24.2, + // plus full bonus 5 → baseScore = 29.2. Slightly below 28.5 → NOT saturated. + // Use a higher src to push past 95%: with source 400: 25 × (1 - exp(-400/58)) ≈ 24.99 + 5 = 29.99 ≥ 28.5. + const satSnapshot: ScoringModelSnapshotRecord = { + ...snapshot, + id: "score-model-saturation", + activeModel: "pending_saturation_model", + }; + const subCap = buildScorePreview({ + repo, + snapshot: satSnapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 100, + totalTokenScore: 0, + sourceLines: 100, + openPrCount: 0, + credibility: 1, + }, + }); + const notSaturated = explainScoreBreakdown(subCap).components.find((c) => c.component === "baseScore")!; + expect(notSaturated).toMatchObject({ band: "neutral" }); + const saturated = buildScorePreview({ + repo, + snapshot: satSnapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 400, + totalTokenScore: 1500, + sourceLines: 400, + openPrCount: 0, + credibility: 1, + }, + }); + const isSaturated = explainScoreBreakdown(saturated).components.find((c) => c.component === "baseScore")!; + expect(isSaturated).toMatchObject({ band: "full" }); + expect(isSaturated.summary).toMatch(/saturated near the score cap/); + expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); + }); });