adaptReputation computes completionRate/onTimeDeliveryRate without clamping, despite an unused coerceFraction helper built for exactly this
Los mantenedores suelen responder en 1 día
@rudra496 ya está trabajando en esto.
Desde el 18/8/2026.
Evaluación
Este issue todavía no se ha evaluado.
Descripción
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 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). - Audit the rest of
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. - Add a regression test proving a corrupted/out-of-range backend value can no longer produce an impossible displayed percentage.
Acceptance Criteria
-
adaptReputationusescoerceFraction(or an equivalent, newly-clamped helper) for bothcompletionRateandonTimeDeliveryRate, rather than the current unclamped inline division. - A raw
completionRate/onTimeDeliveryPercentagevalue of"150"produces aReputationProfile.completionRate/onTimeDeliveryRateof exactly1(clamped), not1.5. - A raw negative value produces
0, not a negative fraction. - A normal, in-range value (e.g.
"94") continues to produce0.94exactly as before — no regression to the common case. - The PR documents the outcome of the broader adapter-layer audit for the same unclamped pattern, even if no other instance is found.
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 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.
- Lenguaje dominante
- TypeScript
- Estrellas
- 1
- Forks
- 94
- Merge medio
- 1 h 2 min
- PR fusionados (30 d)
- 25
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de MergeFi/frontend
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
bug testing
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
accessibility bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
MergeFi/frontend#835 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de MergeFi/frontend
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
zapcooking/frontend#946 ·
Los mantenedores suelen responder en 1 día
-
enhancement good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
liberation-data/drivine#733 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
makecindy/cindy#10062 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día