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

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

View file

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

View file

@ -10263,8 +10263,8 @@ html.login-page--paused .login-page__flare {
.bs-cmp-nav__legend {
display: grid;
grid-template-columns: repeat(4, 1.6em);
gap: 2px;
grid-template-columns: repeat(3, minmax(1.6em, auto));
gap: 4px 6px;
justify-items: end;
margin-top: 6px;
margin-left: auto;
@ -10273,6 +10273,40 @@ html.login-page--paused .login-page__flare {
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 {
flex: 1 1 auto;
min-height: 0;
@ -10336,44 +10370,6 @@ html.login-page--paused .login-page__flare {
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 {
display: flex;
flex-direction: column;

View file

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