Overview
src/lib/utils.ts already has a fraction-clamping helper built and exported specifically for this purpose:
export function coerceFraction(value: string | null | undefined, fallback = 0): number {
const n = coerceDecimal(value, fallback);
if (n < 0) return 0;
if (n > 1) return 1;
return n;
}
I grepped the entire src/ tree for coerceFraction and the only match is its own declaration — it is never imported or called anywhere in the codebase. Meanwhile, adaptReputation — the function responsible for turning a raw reputation snapshot into the completionRate/onTimeDeliveryRate fractions that ReputationPage and the contributor dashboard display — reimplements the same "backend percentage string → 0–1 fraction" conversion inline, without the clamp:
export function adaptReputation(
user: RawUserProfile,
snapshot: RawReputationSnapshot | null,
): ReputationProfile {
return {
handle: user.username,
avatarUrl: user.avatarUrl ?? `https://api.dicebear.com/9.x/identicon/svg?seed=${user.username}`,
lifetimeEarnings: snapshot ? coerceNonNegative(snapshot.totalEarnings) : 0,
mergedPRs: snapshot?.mergedPrCount ?? 0,
completionRate: snapshot ? coerceDecimal(snapshot.completionRate) / 100 : 0,
avgReviewTimeHours: snapshot ? coerceNonNegative(snapshot.avgReviewTimeHours) : 0,
onTimeDeliveryRate: snapshot ? coerceDecimal(snapshot.onTimeDeliveryPercentage) / 100 : 0,
languages: snapshot ? Object.keys(snapshot.languages) : [],
organizations: snapshot?.orgsContributedTo ?? [],
topClients: [],
};
}
coerceDecimal(snapshot.completionRate) / 100 and coerceDecimal(snapshot.onTimeDeliveryPercentage) / 100 both run the raw string through coerceDecimal (which only guards against non-numeric/non-finite input, falling back to 0 — it does not clamp to any range) and then divide by 100, with nothing after that division ever constraining the result back into [0, 1]. If the backend ever sends a completionRate string like "150" (a corrupted computation, a units mismatch, a backend bug computing a rate as a raw count instead of a percentage), the result is 1.5, not clamped — flowing straight through to ReputationProfile.completionRate: 1.5, and from there into formatPercent(1.5) → "150%", displayed with full visual confidence on a public contributor reputation page. The same applies symmetrically to a negative input producing a negative displayed percentage.
This is exactly the shape of value coerceFraction exists to guard — a 0–1 fraction derived from untrusted backend input — and it sits unused three lines above the function that actually needed it.
Requirements
- Replace
adaptReputation's inline coerceDecimal(...) / 100 pattern for both completionRate and onTimeDeliveryRate with coerceFraction, adjusting coerceFraction's signature/call convention as needed to handle the "divide by 100 first, then clamp to [0, 1]" step these two fields require (note coerceFraction as currently written clamps an already-in-[0,1]-range input; it doesn't itself divide by 100, so either extend it to accept a divisor parameter, or clamp after the existing manual division — either is fine as long as the end result is genuinely bounded).
- Audit the rest of
adaptReputation (and adaptBounty/adaptMilestone/adaptMaintenancePool) for any other place a fraction/percentage is derived without a clamp, now that this specific pattern has been found once — confirm whether this is an isolated miss or a broader gap across the adapter layer.
- Add a regression test proving a corrupted/out-of-range backend value can no longer produce an impossible displayed percentage.
Acceptance Criteria
Additional Notes
Precise references:
src/lib/utils.ts:20-25 — coerceFraction, confirmed unused anywhere else via grep -rn "coerceFraction" src/ (one match: the declaration itself).
src/lib/adapters.ts:141-159 — adaptReputation, the unclamped completionRate/onTimeDeliveryRate computation at lines 152 and 154.
src/lib/utils.ts:8-13 — coerceDecimal, confirmed to only guard non-finite/non-numeric input (falling back to 0), with no upper/lower bound clamp of its own — confirming the clamp genuinely needs to happen at the adaptReputation call site (or inside a fixed coerceFraction), not somewhere further upstream that might already be covering this.
src/lib/utils.ts:40-43 — formatPercent, confirmed to have no clamp either (Math.round(value * 100)}% with no Math.min/Math.max), meaning nothing downstream of adaptReputation would catch an out-of-range fraction before it reaches the screen — the clamp genuinely has to happen at the source.
src/app/reputation/[handle]/page.tsx:49-56 — the two StatCards that would display the corrupted value: "Completion rate" and "On-time delivery," both on a page with no authentication gate (public profile).
Relationship to other issues: distinct from the separate HomePage/ReputationPage StatCard-bypass issue in this batch — that issue is about these same two stat cards not using StatCard's format/numeric-value system at all (they're passed pre-formatted strings via formatPercent). This issue is about the correctness of the number going into formatPercent in the first place, upstream in the adapter layer, independent of how StatCard ends up displaying it. Both should be fixed; neither fix makes the other unnecessary.
Test/reproduction plan: unit-test adaptReputation with a fixture RawReputationSnapshot where completionRate: "150" and assert the returned completionRate is 1, not 1.5; repeat with completionRate: "-20" and assert 0; repeat with completionRate: "94" and assert 0.94 (no regression). Mirror the same three cases for onTimeDeliveryPercentage/onTimeDeliveryRate.
Overview
src/lib/utils.tsalready has a fraction-clamping helper built and exported specifically for this purpose:I grepped the entire
src/tree forcoerceFractionand the only match is its own declaration — it is never imported or called anywhere in the codebase. Meanwhile,adaptReputation— the function responsible for turning a raw reputation snapshot into thecompletionRate/onTimeDeliveryRatefractions thatReputationPageand the contributor dashboard display — reimplements the same "backend percentage string → 0–1 fraction" conversion inline, without the clamp:coerceDecimal(snapshot.completionRate) / 100andcoerceDecimal(snapshot.onTimeDeliveryPercentage) / 100both run the raw string throughcoerceDecimal(which only guards against non-numeric/non-finite input, falling back to0— it does not clamp to any range) and then divide by 100, with nothing after that division ever constraining the result back into[0, 1]. If the backend ever sends acompletionRatestring like"150"(a corrupted computation, a units mismatch, a backend bug computing a rate as a raw count instead of a percentage), the result is1.5, not clamped — flowing straight through toReputationProfile.completionRate: 1.5, and from there intoformatPercent(1.5)→"150%", displayed with full visual confidence on a public contributor reputation page. The same applies symmetrically to a negative input producing a negative displayed percentage.This is exactly the shape of value
coerceFractionexists to guard — a 0–1 fraction derived from untrusted backend input — and it sits unused three lines above the function that actually needed it.Requirements
adaptReputation's inlinecoerceDecimal(...) / 100pattern for bothcompletionRateandonTimeDeliveryRatewithcoerceFraction, adjustingcoerceFraction's signature/call convention as needed to handle the "divide by 100 first, then clamp to[0, 1]" step these two fields require (notecoerceFractionas currently written clamps an already-in-[0,1]-range input; it doesn't itself divide by 100, so either extend it to accept adivisorparameter, or clamp after the existing manual division — either is fine as long as the end result is genuinely bounded).adaptReputation(andadaptBounty/adaptMilestone/adaptMaintenancePool) for any other place a fraction/percentage is derived without a clamp, now that this specific pattern has been found once — confirm whether this is an isolated miss or a broader gap across the adapter layer.Acceptance Criteria
adaptReputationusescoerceFraction(or an equivalent, newly-clamped helper) for bothcompletionRateandonTimeDeliveryRate, rather than the current unclamped inline division.completionRate/onTimeDeliveryPercentagevalue of"150"produces aReputationProfile.completionRate/onTimeDeliveryRateof exactly1(clamped), not1.5.0, not a negative fraction."94") continues to produce0.94exactly as before — no regression to the common case.Additional Notes
Precise references:
src/lib/utils.ts:20-25—coerceFraction, confirmed unused anywhere else viagrep -rn "coerceFraction" src/(one match: the declaration itself).src/lib/adapters.ts:141-159—adaptReputation, the unclampedcompletionRate/onTimeDeliveryRatecomputation at lines 152 and 154.src/lib/utils.ts:8-13—coerceDecimal, confirmed to only guard non-finite/non-numeric input (falling back to0), with no upper/lower bound clamp of its own — confirming the clamp genuinely needs to happen at theadaptReputationcall site (or inside a fixedcoerceFraction), not somewhere further upstream that might already be covering this.src/lib/utils.ts:40-43—formatPercent, confirmed to have no clamp either (Math.round(value * 100)}%with noMath.min/Math.max), meaning nothing downstream ofadaptReputationwould catch an out-of-range fraction before it reaches the screen — the clamp genuinely has to happen at the source.src/app/reputation/[handle]/page.tsx:49-56— the twoStatCards that would display the corrupted value: "Completion rate" and "On-time delivery," both on a page with no authentication gate (public profile).Relationship to other issues: distinct from the separate HomePage/ReputationPage
StatCard-bypass issue in this batch — that issue is about these same two stat cards not usingStatCard'sformat/numeric-valuesystem at all (they're passed pre-formatted strings viaformatPercent). This issue is about the correctness of the number going intoformatPercentin the first place, upstream in the adapter layer, independent of howStatCardends up displaying it. Both should be fixed; neither fix makes the other unnecessary.Test/reproduction plan: unit-test
adaptReputationwith a fixtureRawReputationSnapshotwherecompletionRate: "150"and assert the returnedcompletionRateis1, not1.5; repeat withcompletionRate: "-20"and assert0; repeat withcompletionRate: "94"and assert0.94(no regression). Mirror the same three cases foronTimeDeliveryPercentage/onTimeDeliveryRate.