Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/scoring/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,13 +1199,19 @@ function constant(constants: Record<string, number>, key: string): number {
* Resolve a repo's time-decay curve: each parameter is the repo's per-repo override (from the registry's
* `scoring.time_decay`) when present, else the global default constant from the live scoring snapshot.
* Mirrors upstream's `resolve_time_decay` (RepoTimeDecayConfig overlaid on the module constants).
*
* Parity note (#1320): upstream coerces ONLY `grace_period_hours` to an integer
* (`grace_period_hours=int(pick(...))`) while the three curve params stay floats. A maintainer may legally
* configure a fractional grace (upstream validates `0 <= grace_period_hours <= 168`), so the resolved grace
* must be truncated toward zero to match the validator — otherwise a PR aged between `trunc(grace)` and
* `grace` is treated as fresh in the preview but already decaying upstream.
*/
export function resolveTimeDecay(
constants: Record<string, number>,
overrides?: RepoTimeDecayOverrides | null,
): { gracePeriodHours: number; sigmoidMidpointDays: number; sigmoidSteepness: number; minMultiplier: number } {
return {
gracePeriodHours: pickOverride(overrides?.gracePeriodHours, constant(constants, "TIME_DECAY_GRACE_PERIOD_HOURS")),
gracePeriodHours: Math.trunc(pickOverride(overrides?.gracePeriodHours, constant(constants, "TIME_DECAY_GRACE_PERIOD_HOURS"))),
sigmoidMidpointDays: pickOverride(overrides?.sigmoidMidpointDays, constant(constants, "TIME_DECAY_SIGMOID_MIDPOINT")),
sigmoidSteepness: pickOverride(overrides?.sigmoidSteepness, constant(constants, "TIME_DECAY_SIGMOID_STEEPNESS_SCALAR")),
minMultiplier: pickOverride(overrides?.minMultiplier, constant(constants, "TIME_DECAY_MIN_MULTIPLIER")),
Expand Down
17 changes: 17 additions & 0 deletions test/unit/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,23 @@ NOVELTY_BONUS_SCALAR = 3
expect(calculateTimeDecay(120, c, { sigmoidMidpointDays: 5 })).toBeCloseTo(0.5, 5);
});

it("truncates a fractional grace_period_hours override toward zero, mirroring upstream int() (#1320)", () => {
const c = DEFAULT_SCORING_CONSTANTS;
// Upstream resolve_time_decay does `grace_period_hours=int(pick(...))` — and only that field. A
// fractional override (legal under upstream's 0..168 range check) resolves to its truncated integer,
// while the float curve params are untouched.
expect(resolveTimeDecay(c, { gracePeriodHours: 13.9 }).gracePeriodHours).toBe(13);
expect(resolveTimeDecay(c, { gracePeriodHours: 13.9, sigmoidSteepness: 0.4 })).toEqual({
gracePeriodHours: 13,
sigmoidMidpointDays: 10,
sigmoidSteepness: 0.4,
minMultiplier: 0.05,
});
// The boundary case the bug hid: a PR aged between trunc(grace) and grace is already decaying
// upstream (13.5 >= 13), so the preview must decay it too rather than reporting it as fresh.
expect(calculateTimeDecay(13.5, c, { gracePeriodHours: 13.9 })).toBeLessThan(1);
});

it("applies each live repo's resolved curve in the preview (per-repo, not global)", () => {
const input: ScorePreviewInput = { repoFullName: repo.fullName, sourceTokenScore: 58, totalTokenScore: 600, sourceLines: 60, openPrCount: 0, credibility: 1, applyTimeDecay: true, prAgeHours: 18 };
// Repo with a 24h grace override (like JSONbored/gittensory) → an 18h-old PR is still fresh.
Expand Down
Loading