From 1653a31b16b91ac0fab5fbad7eab4d15db366895 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 9 Oct 2026 09:31:04 +0800 Subject: [PATCH] Avoid rounding near-perfect compare pass rates to 100%. With a few fails among tens of thousands of rows, 1-decimal rounding hid residual failures; keep extra precision so the UI never shows 100% when fails remain. Co-authored-by: Cursor --- netx_api/biz_state/compare_service.py | 31 ++++++++++++++++++++-- web/src/pages/network/BizComparePage.tsx | 33 ++++++++++++++++++++---- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/netx_api/biz_state/compare_service.py b/netx_api/biz_state/compare_service.py index 85472da..e11b551 100644 --- a/netx_api/biz_state/compare_service.py +++ b/netx_api/biz_state/compare_service.py @@ -204,6 +204,27 @@ def normalize_store_unchanged(raw: Any) -> str: return m if m in _STORE_UNCHANGED_MODES else "auto" +def compute_pass_rate(*, success: int, fail: int, empty_as: float = 100.0) -> float: + """Pass rate for cutover UI. + + With million-row sheets, 3 fails / 50k can round to 100.0% at 1 decimal — + never report 100 when any fail remains; add decimals until it stays < 100. + """ + s = max(0, int(success or 0)) + f = max(0, int(fail or 0)) + judged = s + f + if judged <= 0: + return float(empty_as) + rate = (s / judged) * 100.0 + if f <= 0: + return round(rate, 1) + for nd in (1, 2, 3, 4): + r = round(rate, nd) + if r < 100.0: + return r + return 99.9999 + + def resolve_unchanged_policy( mode: str, *, before_n: int, after_n: int ) -> dict[str, Any]: @@ -2885,7 +2906,9 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di matched = changed + unchanged diff_count = fail_count pass_rate = ( - round((success_count / judged) * 100, 1) if judged else (100.0 if total == 0 else 0.0) + compute_pass_rate(success=success_count, fail=fail_count, empty_as=100.0 if total == 0 else 0.0) + if judged + else (100.0 if total == 0 else 0.0) ) diff_rate = round((fail_count / judged) * 100, 1) if judged else 0.0 @@ -2920,7 +2943,11 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di # Pending sheets must not look like "100% pass" "pass_rate": None if pending - else (round((su / sj) * 100, 1) if sj else (100.0 if st == 0 else 0.0)), + else ( + compute_pass_rate(success=su, fail=sf, empty_as=100.0 if st == 0 else 0.0) + if sj + else (100.0 if st == 0 else 0.0) + ), } ) diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index aa3a018..38889e7 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -1376,7 +1376,22 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage const fail = sheetFailOf(c); const ok = sheetSuccessOf(c); const judged = fail + ok; - return judged ? Math.round((ok / judged) * 1000) / 10 : 100; + if (!judged) return 100; + const rate = (ok / judged) * 100; + if (fail <= 0) return Math.round(rate * 10) / 10; + // 3 fails / 50k ≈ 99.994% — 1-decimal round becomes 100%; keep digits until < 100 + for (const nd of [1, 2, 3, 4]) { + const r = Math.round(rate * 10 ** nd) / 10 ** nd; + if (r < 100) return r; + } + return 99.9999; + }; + + const formatPassRate = (rate: number | null | undefined) => { + if (rate === null || rate === undefined || Number.isNaN(rate)) return "…"; + // Trim trailing zeros but keep enough precision for near-100 rates + const s = Number(rate).toFixed(4).replace(/\.?0+$/, ""); + return `${s}%`; }; const summary = runDetail?.summary || {}; @@ -3591,7 +3606,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage pass_rate: pending ? null : judged - ? Math.round((unchanged / judged) * 1000) / 10 + ? (() => { + const rate = (unchanged / judged) * 100; + if (fail <= 0) return Math.round(rate * 10) / 10; + for (const nd of [1, 2, 3, 4]) { + const r = Math.round(rate * 10 ** nd) / 10 ** nd; + if (r < 100) return r; + } + return 99.9999; + })() : 100, }; }) @@ -3641,7 +3664,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage title={ pending ? `${label} · ${t("bizCompare.sheetPending")}` - : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate ?? "…"}%` + : `${label} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${formatPassRate(rate)}` } > @@ -3658,7 +3681,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage {pending ? "—" : ok} - {pending || rate === null ? "…" : `${rate}%`} + {pending || rate === null ? "…" : formatPassRate(rate)} @@ -3714,7 +3737,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage {activeSheetPending || activePassRate === null ? "…" - : `${activePassRate}%`} + : formatPassRate(activePassRate)}