Simplify compare verdict to fail, success, and pass rate.

Treat missing+mismatch as fail, match as success, and added as a special filter; show three right-aligned sheet badges.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-18 10:41:24 +08:00
parent 357b831f2d
commit bb00ec91f4
6 changed files with 189 additions and 148 deletions

View file

@ -205,7 +205,8 @@ def _filter_inline_diffs(
for d in diffs: for d in diffs:
dk = str(d.get("kind") or "") dk = str(d.get("kind") or "")
if kind_n == "diff": if kind_n == "diff":
if dk == "unchanged": # Fail = missing + mismatch; added is a special bucket
if dk not in ("removed", "changed"):
continue continue
elif kind_n != "all" and dk != kind_n: elif kind_n != "all" and dk != kind_n:
continue continue
@ -1328,10 +1329,16 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di
before_count = int(summary.get("before_count") or 0) before_count = int(summary.get("before_count") or 0)
after_count = int(summary.get("after_count") or 0) after_count = int(summary.get("after_count") or 0)
total = added + removed + changed + unchanged total = added + removed + changed + unchanged
# Compare verdict: fail = missing + mismatch; success = match; added is special
fail_count = removed + changed
success_count = unchanged
judged = fail_count + success_count
matched = changed + unchanged matched = changed + unchanged
diff_count = added + removed + changed diff_count = fail_count
pass_rate = round((unchanged / matched) * 100, 1) if matched else (100.0 if total == 0 else 0.0) pass_rate = (
diff_rate = round((diff_count / total) * 100, 1) if total else 0.0 round((success_count / judged) * 100, 1) if judged else (100.0 if total == 0 else 0.0)
)
diff_rate = round((fail_count / judged) * 100, 1) if judged else 0.0
sheet_cards: list[dict[str, Any]] = [] sheet_cards: list[dict[str, Any]] = []
for sh in sheets: for sh in sheets:
@ -1341,7 +1348,8 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di
sc = int(ss.get("changed") or 0) sc = int(ss.get("changed") or 0)
su = int(ss.get("unchanged") or 0) su = int(ss.get("unchanged") or 0)
st = sa + sr + sc + su st = sa + sr + sc + su
sm = sc + su sf = sr + sc
sj = sf + su
sheet_cards.append( sheet_cards.append(
{ {
"metric_id": sh.get("metric_id") or "", "metric_id": sh.get("metric_id") or "",
@ -1352,8 +1360,10 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di
"unchanged": su, "unchanged": su,
"before_count": int(ss.get("before_count") or 0), "before_count": int(ss.get("before_count") or 0),
"after_count": int(ss.get("after_count") or 0), "after_count": int(ss.get("after_count") or 0),
"diff_count": sa + sr + sc, "fail_count": sf,
"pass_rate": round((su / sm) * 100, 1) if sm else (100.0 if st == 0 else 0.0), "success_count": su,
"diff_count": sf,
"pass_rate": round((su / sj) * 100, 1) if sj else (100.0 if st == 0 else 0.0),
} }
) )
@ -1382,10 +1392,12 @@ def _enrich_summary(summary: dict[str, Any], sheets: list[dict[str, Any]]) -> di
"sheet_count": int(summary.get("sheet_count") or len(sheets) or 0), "sheet_count": int(summary.get("sheet_count") or len(sheets) or 0),
"total_rows": total, "total_rows": total,
"matched_rows": matched, "matched_rows": matched,
"fail_count": fail_count,
"success_count": success_count,
"diff_count": diff_count, "diff_count": diff_count,
"pass_rate": pass_rate, "pass_rate": pass_rate,
"diff_rate": diff_rate, "diff_rate": diff_rate,
"ok": diff_count == 0, "ok": fail_count == 0,
"sheet_cards": sheet_cards, "sheet_cards": sheet_cards,
"top_changed_fields": top_fields, "top_changed_fields": top_fields,
} }
@ -1476,7 +1488,7 @@ def list_run_diffs(
BizCompareDiff.metric_id == mid, BizCompareDiff.metric_id == mid,
) )
if kind_n == "diff": if kind_n == "diff":
q = q.filter(BizCompareDiff.kind.in_(("added", "removed", "changed"))) q = q.filter(BizCompareDiff.kind.in_(("removed", "changed")))
elif kind_n != "all": elif kind_n != "all":
q = q.filter(BizCompareDiff.kind == kind_n) q = q.filter(BizCompareDiff.kind == kind_n)
if kw_n: if kw_n:

View file

@ -303,7 +303,7 @@ class CompareDiffPagingTests(unittest.TestCase):
}, },
] ]
only_diff = _filter_inline_diffs(diffs, kind="diff", kw="") only_diff = _filter_inline_diffs(diffs, kind="diff", kw="")
self.assertEqual({d["kind"] for d in only_diff}, {"added", "removed", "changed"}) self.assertEqual({d["kind"] for d in only_diff}, {"removed", "changed"})
only_added = _filter_inline_diffs(diffs, kind="added", kw="") only_added = _filter_inline_diffs(diffs, kind="added", kw="")
self.assertEqual(len(only_added), 1) self.assertEqual(len(only_added), 1)
hit = _filter_inline_diffs(diffs, kind="all", kw='"p":"4"') hit = _filter_inline_diffs(diffs, kind="all", kw='"p":"4"')

View file

@ -364,17 +364,20 @@ const en = {
resultEmpty: "No matching diff rows", resultEmpty: "No matching diff rows",
resultFilterPh: "Filter key / values…", resultFilterPh: "Filter key / values…",
kindAll: "All", kindAll: "All",
kindDiff: "Failures only", kindDiff: "Fail",
kindAdded: "Extra only", kindAdded: "Added",
kindRemoved: "Missing only", kindRemoved: "Missing only",
kindChanged: "Mismatch only", kindChanged: "Mismatch only",
kindUnchanged: "Match only", kindUnchanged: "Pass",
kindAddedShort: "Extra", kindAddedShort: "Added",
kindRemovedShort: "Missing", kindRemovedShort: "Missing",
kindChangedShort: "Mismatch", kindChangedShort: "Mismatch",
kindUnchangedShort: "Match", kindUnchangedShort: "Pass",
kindFail: "Fail", kindFail: "Fail",
kindPass: "Pass", kindPass: "Pass",
kindSuccess: "Pass",
successCount: "Pass",
passRateShort: "Pass %",
verdictPass: "Pass", verdictPass: "Pass",
verdictFail: "Fail", verdictFail: "Fail",
failCount: "Failed rows", failCount: "Failed rows",

View file

@ -364,17 +364,20 @@ const zh = {
resultEmpty: "无匹配失败/结果行", resultEmpty: "无匹配失败/结果行",
resultFilterPh: "筛选身份 / 字段值…", resultFilterPh: "筛选身份 / 字段值…",
kindAll: "全部", kindAll: "全部",
kindDiff: "仅失败", kindDiff: "失败",
kindAdded: "仅多余", kindAdded: "新增",
kindRemoved: "仅缺失", kindRemoved: "仅缺失",
kindChanged: "仅不一致", kindChanged: "仅不一致",
kindUnchanged: "仅一致", kindUnchanged: "成功",
kindAddedShort: "多余", kindAddedShort: "新增",
kindRemovedShort: "缺失", kindRemovedShort: "缺失",
kindChangedShort: "不一致", kindChangedShort: "不一致",
kindUnchangedShort: "一致", kindUnchangedShort: "成功",
kindFail: "失败", kindFail: "失败",
kindPass: "通过", kindPass: "成功",
kindSuccess: "成功",
successCount: "成功",
passRateShort: "通过率",
verdictPass: "通过", verdictPass: "通过",
verdictFail: "不通过", verdictFail: "不通过",
failCount: "失败条目", failCount: "失败条目",

View file

@ -10263,8 +10263,8 @@ html.login-page--paused .login-page__flare {
.bs-cmp-nav__legend { .bs-cmp-nav__legend {
display: grid; display: grid;
grid-template-columns: repeat(4, 1.6em); grid-template-columns: repeat(3, minmax(1.6em, auto));
gap: 2px; gap: 4px 6px;
justify-items: end; justify-items: end;
margin-top: 6px; margin-top: 6px;
margin-left: auto; margin-left: auto;
@ -10273,6 +10273,40 @@ html.login-page--paused .login-page__flare {
font-weight: 700; font-weight: 700;
} }
.bs-cmp-nav__stats {
display: inline-grid;
grid-template-columns: repeat(3, minmax(1.6em, auto));
gap: 2px 6px;
justify-items: end;
align-items: center;
flex: 0 0 auto;
font-variant-numeric: tabular-nums;
}
.bs-cmp-nav__num {
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #475569;
opacity: 0.55;
}
.bs-cmp-nav__num--fail.is-hot {
color: #f87171;
opacity: 1;
}
.bs-cmp-nav__num--ok.is-hot {
color: #4ade80;
opacity: 1;
}
.bs-cmp-nav__num--rate {
color: #93c5fd;
opacity: 0.95;
min-width: 2.6em;
}
.bs-cmp-nav__list { .bs-cmp-nav__list {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
@ -10336,44 +10370,6 @@ html.login-page--paused .login-page__flare {
min-width: 0; min-width: 0;
} }
.bs-cmp-nav__stats {
display: inline-grid;
grid-template-columns: repeat(4, 1.6em);
gap: 2px;
justify-items: end;
align-items: center;
flex: 0 0 auto;
font-variant-numeric: tabular-nums;
}
.bs-cmp-nav__num {
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #475569;
opacity: 0.55;
}
.bs-cmp-nav__num--removed.is-hot {
color: #f87171;
opacity: 1;
}
.bs-cmp-nav__num--added.is-hot {
color: #4ade80;
opacity: 1;
}
.bs-cmp-nav__num--changed.is-hot {
color: #fbbf24;
opacity: 1;
}
.bs-cmp-nav__num--unchanged {
color: #94a3b8;
opacity: 0.85;
}
.bs-cmp-main { .bs-cmp-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -795,17 +795,51 @@ export function BizComparePage() {
return kind; return kind;
}; };
const verdictLabel = (kind: string) => { /** Table verdict: fail = missing+mismatch; success = match; added is special. */
if (kind === "unchanged") return t("bizCompare.kindPass"); const rowVerdict = (kind: string): { label: string; tone: "fail" | "pass" | "added" } => {
return t("bizCompare.kindFail"); if (kind === "added") return { label: t("bizCompare.kindAddedShort"), tone: "added" };
if (kind === "unchanged") return { label: t("bizCompare.kindSuccess"), tone: "pass" };
return { label: t("bizCompare.kindFail"), tone: "fail" };
}; };
const failFieldsLabel = (d: DiffRow) => { const failFieldsLabel = (d: DiffRow) => {
if (d.kind === "added" || d.kind === "removed") return t("bizCompare.failWholeRow"); if (d.kind === "added") return t("bizCompare.kindAddedShort");
if (d.kind === "removed") return t("bizCompare.failWholeRow");
const names = failFieldNames(d); const names = failFieldNames(d);
return names.length ? names.join(" · ") : t("bizCompare.failFieldsEmpty"); return names.length ? names.join(" · ") : t("bizCompare.failFieldsEmpty");
}; };
const sheetFailOf = (c: {
fail_count?: number;
diff_count?: number;
removed?: number;
changed?: number;
}) =>
Number(
c.fail_count ??
c.diff_count ??
Number(c.removed || 0) + Number(c.changed || 0),
);
const sheetSuccessOf = (c: { success_count?: number; unchanged?: number }) =>
Number(c.success_count ?? c.unchanged ?? 0);
const sheetPassRateOf = (c: {
pass_rate?: number;
fail_count?: number;
diff_count?: number;
removed?: number;
changed?: number;
success_count?: number;
unchanged?: number;
}) => {
if (c.pass_rate !== undefined && c.pass_rate !== null) return Number(c.pass_rate);
const fail = sheetFailOf(c);
const ok = sheetSuccessOf(c);
const judged = fail + ok;
return judged ? Math.round((ok / judged) * 1000) / 10 : 100;
};
const summary = runDetail?.summary || {}; const summary = runDetail?.summary || {};
const resultPages = Math.max(1, Math.ceil(resultTotal / Math.max(1, resultPageSize))); const resultPages = Math.max(1, Math.ceil(resultTotal / Math.max(1, resultPageSize)));
const sheetCards = useMemo(() => { const sheetCards = useMemo(() => {
@ -818,20 +852,25 @@ export function BizComparePage() {
unchanged?: number; unchanged?: number;
before_count?: number; before_count?: number;
after_count?: number; after_count?: number;
fail_count?: number;
success_count?: number;
diff_count?: number; diff_count?: number;
pass_rate?: number; pass_rate?: number;
}>; }>;
// Diffs first so ops can scan quickly when many sheets // Failures first so ops can scan quickly when many sheets
return [...raw].sort((a, b) => { return [...raw].sort((a, b) => {
const da = Number(a.diff_count || 0); const da = sheetFailOf(a);
const db = Number(b.diff_count || 0); const db = sheetFailOf(b);
if (da !== db) return db - da; if (da !== db) return db - da;
return String(a.metric_id).localeCompare(String(b.metric_id)); return String(a.metric_id).localeCompare(String(b.metric_id));
}); });
}, [summary.sheet_cards]); }, [summary.sheet_cards]);
const sheetFailCount = sheetCards.filter((c) => Number(c.diff_count || 0) > 0).length; const sheetFailCount = sheetCards.filter((c) => sheetFailOf(c) > 0).length;
const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0]; const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0];
const activeFail = sheetFailOf(activeSheetCard || {});
const activeSuccess = sheetSuccessOf(activeSheetCard || {});
const activePassRate = sheetPassRateOf(activeSheetCard || {});
const activeAdded = Number(activeSheetCard?.added || 0);
useEffect(() => { useEffect(() => {
const syncFs = () => { const syncFs = () => {
const el = boardRef.current; const el = boardRef.current;
@ -2247,51 +2286,55 @@ export function BizComparePage() {
</div> </div>
<div className="bs-cmp-nav__legend" aria-hidden> <div className="bs-cmp-nav__legend" aria-hidden>
<span <span
className="bs-cmp-nav__num bs-cmp-nav__num--removed is-hot" className="bs-cmp-nav__num bs-cmp-nav__num--fail is-hot"
title={t("bizCompare.missCount")} title={t("bizCompare.kindFail")}
> >
- {t("bizCompare.kindFail")}
</span> </span>
<span <span
className="bs-cmp-nav__num bs-cmp-nav__num--added is-hot" className="bs-cmp-nav__num bs-cmp-nav__num--ok is-hot"
title={t("bizCompare.extraCount")} title={t("bizCompare.kindSuccess")}
> >
+ {t("bizCompare.kindSuccess")}
</span> </span>
<span <span
className="bs-cmp-nav__num bs-cmp-nav__num--changed is-hot" className="bs-cmp-nav__num bs-cmp-nav__num--rate"
title={t("bizCompare.mismatchCount")} title={t("bizCompare.passRateShort")}
> >
~ %
</span>
<span
className="bs-cmp-nav__num bs-cmp-nav__num--unchanged"
title={t("bizCompare.matchCount")}
>
=
</span> </span>
</div> </div>
</div> </div>
<div className="bs-cmp-nav__list" role="tablist"> <div className="bs-cmp-nav__list" role="tablist">
{(sheetCards.length {(sheetCards.length
? sheetCards ? sheetCards
: runSheets.map((s) => ({ : runSheets.map((s) => {
metric_id: s.metric_id, const removed = Number(s.summary?.removed || 0);
mode: s.mode, const changed = Number(s.summary?.changed || 0);
added: s.summary?.added, const unchanged = Number(s.summary?.unchanged || 0);
removed: s.summary?.removed, const fail = removed + changed;
changed: s.summary?.changed, const judged = fail + unchanged;
unchanged: s.summary?.unchanged, return {
before_count: s.summary?.before_count, metric_id: s.metric_id,
after_count: s.summary?.after_count, mode: s.mode,
diff_count: added: s.summary?.added,
Number(s.summary?.added || 0) + removed,
Number(s.summary?.removed || 0) + changed,
Number(s.summary?.changed || 0), unchanged,
pass_rate: s.summary?.pass_rate, before_count: s.summary?.before_count,
})) after_count: s.summary?.after_count,
fail_count: fail,
success_count: unchanged,
diff_count: fail,
pass_rate: judged
? Math.round((unchanged / judged) * 1000) / 10
: 100,
};
})
).map((c) => { ).map((c) => {
const dirty = Number(c.diff_count || 0); const fail = sheetFailOf(c);
const ok = sheetSuccessOf(c);
const rate = sheetPassRateOf(c);
const active = resultSheetId === c.metric_id; const active = resultSheetId === c.metric_id;
return ( return (
<button <button
@ -2300,40 +2343,26 @@ export function BizComparePage() {
role="tab" role="tab"
aria-selected={active} aria-selected={active}
className={`bs-cmp-nav__item${active ? " is-active" : ""}${ className={`bs-cmp-nav__item${active ? " is-active" : ""}${
dirty > 0 ? " has-diff" : " is-clean" fail > 0 ? " has-diff" : " is-clean"
}`} }`}
onClick={() => setResultSheetId(c.metric_id)} onClick={() => setResultSheetId(c.metric_id)}
title={c.metric_id} title={`${c.metric_id} · ${t("bizCompare.kindFail")} ${fail} · ${t("bizCompare.kindSuccess")} ${ok} · ${t("bizCompare.passRateShort")} ${rate}%`}
> >
<span className="bs-cmp-nav__dot" aria-hidden /> <span className="bs-cmp-nav__dot" aria-hidden />
<span className="bs-cmp-nav__name">{c.metric_id}</span> <span className="bs-cmp-nav__name">{c.metric_id}</span>
<span <span className="bs-cmp-nav__stats">
className="bs-cmp-nav__stats"
title={`${t("bizCompare.missCount")} ${Number(c.removed || 0)} · ${t("bizCompare.extraCount")} ${Number(c.added || 0)} · ${t("bizCompare.mismatchCount")} ${Number(c.changed || 0)} · ${t("bizCompare.matchCount")} ${Number(c.unchanged || 0)}`}
>
<span <span
className={`bs-cmp-nav__num bs-cmp-nav__num--removed${ className={`bs-cmp-nav__num bs-cmp-nav__num--fail${
Number(c.removed || 0) > 0 ? " is-hot" : "" fail > 0 ? " is-hot" : ""
}`} }`}
> >
{Number(c.removed || 0)} {fail}
</span> </span>
<span <span className="bs-cmp-nav__num bs-cmp-nav__num--ok is-hot">
className={`bs-cmp-nav__num bs-cmp-nav__num--added${ {ok}
Number(c.added || 0) > 0 ? " is-hot" : ""
}`}
>
{Number(c.added || 0)}
</span> </span>
<span <span className="bs-cmp-nav__num bs-cmp-nav__num--rate">
className={`bs-cmp-nav__num bs-cmp-nav__num--changed${ {rate}%
Number(c.changed || 0) > 0 ? " is-hot" : ""
}`}
>
{Number(c.changed || 0)}
</span>
<span className="bs-cmp-nav__num bs-cmp-nav__num--unchanged">
{Number(c.unchanged || 0)}
</span> </span>
</span> </span>
</button> </button>
@ -2344,9 +2373,7 @@ export function BizComparePage() {
<div className="bs-cmp-main"> <div className="bs-cmp-main">
<div <div
className={`bs-cmp-strip${ className={`bs-cmp-strip${activeFail > 0 ? " is-warn" : " is-ok"}`}
Number(activeSheetCard?.diff_count || 0) > 0 ? " is-warn" : " is-ok"
}`}
> >
<div className="bs-cmp-strip__sheet"> <div className="bs-cmp-strip__sheet">
<span className="bs-cmp-strip__sheet-tag"> <span className="bs-cmp-strip__sheet-tag">
@ -2359,9 +2386,8 @@ export function BizComparePage() {
{activeSheetCard?.metric_id || "—"} {activeSheetCard?.metric_id || "—"}
</strong> </strong>
<span className="muted bs-cmp-strip__sheet-mode"> <span className="muted bs-cmp-strip__sheet-mode">
{Number(activeSheetCard?.diff_count || 0) > 0 {activeFail > 0 ? t("bizCompare.kindFail") : t("bizCompare.kindPass")}
? t("bizCompare.kindFail") {` · ${t("bizCompare.passRateShort")} ${activePassRate}%`}
: t("bizCompare.kindPass")}
{activeSheetCard?.mode === "presence" {activeSheetCard?.mode === "presence"
? ` · ${t("bizCompare.presenceShort")}` ? ` · ${t("bizCompare.presenceShort")}`
: ""} : ""}
@ -2370,11 +2396,9 @@ export function BizComparePage() {
<div className="bs-cmp-strip__kinds" role="group"> <div className="bs-cmp-strip__kinds" role="group">
{( {(
[ [
["diff", Number(activeSheetCard?.diff_count || 0), "diff"], ["diff", activeFail, "diff"],
["removed", activeSheetCard?.removed ?? 0, "removed"], ["added", activeAdded, "added"],
["added", activeSheetCard?.added ?? 0, "added"], ["unchanged", activeSuccess, "unchanged"],
["changed", activeSheetCard?.changed ?? 0, "changed"],
["unchanged", activeSheetCard?.unchanged ?? 0, "unchanged"],
["all", null, "all"], ["all", null, "all"],
] as const ] as const
).map(([id, n, cls]) => ( ).map(([id, n, cls]) => (
@ -2390,7 +2414,9 @@ export function BizComparePage() {
? t("bizCompare.kindDiff") ? t("bizCompare.kindDiff")
: id === "all" : id === "all"
? t("bizCompare.kindAll") ? t("bizCompare.kindAll")
: kindLabel(id)} : id === "added"
? t("bizCompare.kindAddedShort")
: t("bizCompare.kindSuccess")}
{n !== null ? ( {n !== null ? (
<> <>
{" "} {" "}
@ -2473,23 +2499,24 @@ export function BizComparePage() {
const post = pickSideRow( const post = pickSideRow(
d.after as Record<string, unknown> | null | undefined, d.after as Record<string, unknown> | null | undefined,
); );
const isFail = d.kind !== "unchanged"; const verdict = rowVerdict(d.kind);
const isFail = verdict.tone === "fail";
return ( return (
<tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}> <tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}>
<td className="bs-cmp-col-kind"> <td className="bs-cmp-col-kind">
<span <span className={`bs-cmp-badge bs-cmp-badge--${verdict.tone}`}>
className={`bs-cmp-badge bs-cmp-badge--${ {verdict.label}
isFail ? "fail" : "pass"
}`}
>
{verdictLabel(d.kind)}
</span>
<span className={`bs-cmp-badge bs-cmp-badge--${d.kind}`}>
{kindLabel(d.kind)}
</span> </span>
{isFail ? (
<span className={`bs-cmp-badge bs-cmp-badge--${d.kind}`}>
{kindLabel(d.kind)}
</span>
) : null}
</td> </td>
<td className="bs-cmp-fail-cell"> <td className="bs-cmp-fail-cell">
{isFail ? failFieldsLabel(d) : t("bizCompare.failFieldsEmpty")} {verdict.tone === "pass"
? t("bizCompare.failFieldsEmpty")
: failFieldsLabel(d)}
</td> </td>
{resultColumns.keys.map((k) => ( {resultColumns.keys.map((k) => (
<td key={k} className="bs-cmp-key-cell"> <td key={k} className="bs-cmp-key-cell">