adaptReputation computes completionRate/onTimeDeliveryRate without clamping, despite an unused coerceFraction helper built for exactly this
メンテナーはふだん 1 日以内に返信
@rudra496 がすでに取り組んでいます。
2026年8月18日 から。
評価
この issue はまだ評価されていません。
説明
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.
- 主要言語
- TypeScript
- スター
- 1
- フォーク
- 94
- 平均マージ
- 1時間 2分
- マージ済み PR(30日)
- 25
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MergeFi/frontend のほかの issue
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
bug testing
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 1 日以内に返信
-
accessibility bug
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
MergeFi/frontend#835 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
MergeFi/frontend の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
zapcooking/frontend#946 ·
メンテナーはふだん 1 日以内に返信
-
enhancement good first issue
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
liberation-data/drivine#733 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
makecindy/cindy#10062 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信