feat(scoring): surface the time-decay multiplier in the score breakdown#1877
Conversation
explainScoreBreakdown explained every other scoring multiplier (density, contribution bonus, label, issue, credibility, review penalty, open-PR pressure, open-issue spam, merged-PR history floor in JSONbored#1801, and the issue-discovery validity floor now in JSONbored#1874) but silently omitted the timeDecayMultiplier, even though it multiplies into the final score (preview.ts:432) and is the only remaining multiplier on scoreEstimate without a breakdown component. Add a timeDecayBreakdown: neutral when the multiplier is >= 0.99 (the PR is fresh or upstream time-decay -- env SCORING_TIME_DECAY_ENABLED, default OFF -- is disabled for the preview), and reduced with an aged-PR lever when the multiplier has decayed. Purely additive explanation; no scoring behavior change.
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-06-30 19:12:12 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 7 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
…eakdown (#2356) * feat(scoring): surface the saturated base-score value in the score breakdown 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). * 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 * fix(scoring): derive base-score saturation from preview-estimate cap, not hardcoded threshold 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 * 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' * fix: remove dead locals from test, handle baseScoreCap undefined copy * test: cover fixedBaseScore and saturation model cap branches - fixedBaseScore override → baseScoreCap undefined, copy says 'fixed base score override' - pending_saturation_model → cap = MERGED_PR_BASE_SCORE + MAX_CONTRIBUTION_BONUS
Summary
explainScoreBreakdown(src/services/score-breakdown.ts) explained every other scoring multiplier -- density, contribution bonus, label, issue, credibility, review penalty, open-PR pressure, open-issue spam, the merged-PR history floor (added by #1801), and now the issue-discovery validity floor (added by #1874) -- but silently omitted thetimeDecayMultiplier, even though it multiplies into the final score (preview.ts:432) and is the only remaining multiplier onscoreEstimatewithout a breakdown component.What this adds
A
timeDecayBreakdowncomponent mirroring the siblingmergedHistoryBreakdown(#1801) andissueDiscoveryHistoryBreakdown(#1874):>= 0.99(the PR is fresh, or upstream time-decay -- envSCORING_TIME_DECAY_ENABLED, default OFF -- is disabled for the preview). The summary explicitly surfaces the env flag so a contributor understands why there is no age penalty on their preview.master_repositories.jsonoverride for this repo to slow the decay") when the multiplier has decayed -- purely upstream-sigmoid curve atpreview.ts:1265.Purely additive explanation; no scoring behavior change.
Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coverage--test/unit/score-breakdown.test.ts12/12 pass;src/services/score-breakdown.tsis 100% statements / 100% functions / 100% lines (the new component's two branches -- fresh / disabled and aged-with-decay -- are each covered).Targeted run:
Safety
sanitizePublicComment).UI Evidence
Not applicable -- backend score-breakdown projection with no visible UI, frontend, docs, or extension surface.