Skip to content

feat(scoring): surface the time-decay multiplier in the score breakdown#1877

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
RenzoMXD:feat/scoring-time-decay-breakdown
Jun 30, 2026
Merged

feat(scoring): surface the time-decay multiplier in the score breakdown#1877
JSONbored merged 1 commit into
JSONbored:mainfrom
RenzoMXD:feat/scoring-time-decay-breakdown

Conversation

@RenzoMXD

Copy link
Copy Markdown
Contributor

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 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.

What this adds

A timeDecayBreakdown component mirroring the sibling mergedHistoryBreakdown (#1801) and issueDiscoveryHistoryBreakdown (#1874):

  • 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). The summary explicitly surfaces the env flag so a contributor understands why there is no age penalty on their preview.
  • reduced (or further) with an aged-PR lever ("land the work while it is fresh, or extend the upstream time-decay curve in the repo's master_repositories.json override for this repo to slow the decay") when the multiplier has decayed -- purely upstream-sigmoid curve at preview.ts:1265.

Purely additive explanation; no scoring behavior change.

Scope

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage -- test/unit/score-breakdown.test.ts 12/12 pass; src/services/score-breakdown.ts is 100% statements / 100% functions / 100% lines (the new component's two branches -- fresh / disabled and aged-with-decay -- are each covered).

Targeted run:

npx vitest run test/unit/score-breakdown.test.ts --coverage --coverage.include='src/services/score-breakdown.ts'
# 12/12 passed; 100% statements/functions/lines

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed (a forbidden-term assertion is included).
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics (every summary/lever runs through the existing sanitizePublicComment).
  • No auth, cookie, CORS, GitHub App, Cloudflare, or session changes -- a purely additive explanation projection over an already-computed multiplier; no scoring logic change.
  • No UI changes.
  • No docs/changelog changes.

UI Evidence

Not applicable -- backend score-breakdown projection with no visible UI, frontend, docs, or extension surface.

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.
@RenzoMXD RenzoMXD requested a review from JSONbored as a code owner June 30, 2026 13:14
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Jun 30, 2026
@gittensory-orb

gittensory-orb Bot commented Jun 30, 2026

Copy link
Copy Markdown

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-06-30 19:12:12 UTC

2 files · 1 AI reviewer · no blockers · readiness 48/100 · CI green · unknown

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
The diff cleanly adds `timeDecayMultiplier` to the score-breakdown component list and sanitizes it through the same path as the existing multiplier explanations. The new helper mirrors the surrounding breakdown functions, covers both neutral and decayed cases, and the test asserts the component is present plus exercises the reduced branch through `buildScorePreview`. I do not see a reachable correctness break in the provided changed files.

Nits — 7 non-blocking
  • nit: `src/services/score-breakdown.ts:144` uses one neutral summary for two materially different states, so a fresh PR and disabled time decay are indistinguishable to the reader; consider using available preview/input state if exposed, or soften the wording further.
  • nit: `src/services/score-breakdown.ts:153` hard-codes curve details in the public explanation, which can drift if the upstream sigmoid constants change; consider deriving those values from the scoring constants or keeping the summary less parameter-specific.
  • nit: `test/unit/score-breakdown.test.ts:165` only asserts the neutral band for the fresh/disabled case and does not verify the new explanatory text is sanitized/non-empty in that branch specifically.
  • In `src/services/score-breakdown.ts:144`, split the neutral explanation if the preview model exposes whether decay was disabled versus merely still inside the grace period.
  • In `src/services/score-breakdown.ts:153`, avoid embedding exact curve constants unless they are sourced from the same scoring configuration that calculates `timeDecayMultiplier`.
  • Readiness score is below the configured threshold — Use the readiness panel as advisory maintainer context; the score does not block this PR.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ⚠️ 3 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Change scope ❌ 8/20 High review scope from cached public metadata (size label size:S; no linked issue context).
Validation posture ❌ 5/25 Preflight is holding this PR; address the blocker before review.
Contributor workload ✅ 10/10 Author activity: 53 registered-repo PR(s), 25 merged, 7 issue(s).
Contributor context ✅ Confirmed Gittensor contributor RenzoMXD; Gittensor profile; 53 PR(s), 7 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
Contributor next steps
  • Explain no-issue PR.
  • Review top overlaps.
  • Add a concise scope and risk note.
  • Fix the blocker.
  • Triage stale or unlinked PRs.
  • Refresh registry data or choose a registered active repo.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
  • Check active issues and PRs before submitting.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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.

  • Re-run Gittensory review

@dosubot dosubot Bot added the lgtm Approved by a maintainer. label Jun 30, 2026
@JSONbored JSONbored merged commit 5f95a8c into JSONbored:main Jun 30, 2026
7 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jun 30, 2026
JSONbored pushed a commit that referenced this pull request Jul 1, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 1.25x multiplier. gittensor Gittensor contributor context lgtm Approved by a maintainer. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants