ccqa 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/ccqa.mjs +119 -82
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -5890,7 +5890,6 @@ async function runLiveSpecs(specs, opts) {
|
|
|
5890
5890
|
const userPromptSuffix = userPromptBundle?.text ?? null;
|
|
5891
5891
|
const diffProvider = opts.diffProvider ?? null;
|
|
5892
5892
|
const failureAnalysisEnabled = diffProvider != null;
|
|
5893
|
-
const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
|
|
5894
5893
|
const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
|
|
5895
5894
|
ok: false,
|
|
5896
5895
|
reason: "disabled"
|
|
@@ -5918,8 +5917,7 @@ async function runLiveSpecs(specs, opts) {
|
|
|
5918
5917
|
const row = await buildLiveReportRow(outcome, {
|
|
5919
5918
|
auth,
|
|
5920
5919
|
diffProvider,
|
|
5921
|
-
reportDir
|
|
5922
|
-
driftAuditEnabled
|
|
5920
|
+
reportDir
|
|
5923
5921
|
}, opts, cwd);
|
|
5924
5922
|
await opts.report?.upsert(row);
|
|
5925
5923
|
return {
|
|
@@ -5953,7 +5951,7 @@ async function buildLiveReportRow(r, ctx, opts, cwd) {
|
|
|
5953
5951
|
result: r.result,
|
|
5954
5952
|
reportDir: ctx.reportDir
|
|
5955
5953
|
});
|
|
5956
|
-
const driftForSpec = ctx.
|
|
5954
|
+
const driftForSpec = ctx.diffProvider && r.result.status === "failed" ? await runDriftAuditOne(r, opts, cwd) : null;
|
|
5957
5955
|
const analysis = ctx.diffProvider && r.result.status === "failed" ? await analyzeOneLiveFailure(r, ctx.diffProvider, driftForSpec, ctx.auth, opts, cwd) : void 0;
|
|
5958
5956
|
return {
|
|
5959
5957
|
...base,
|
|
@@ -9555,7 +9553,6 @@ async function executeRun(targets, opts) {
|
|
|
9555
9553
|
...typeof opts.retry === "number" ? { retry: opts.retry } : {},
|
|
9556
9554
|
concurrency: opts.concurrency ?? 1,
|
|
9557
9555
|
...opts.profile ? { profile: opts.profile } : {},
|
|
9558
|
-
...opts.driftAudit !== false ? { driftAudit: true } : {},
|
|
9559
9556
|
diffProvider,
|
|
9560
9557
|
hubContext: hubCtx,
|
|
9561
9558
|
customPrompt,
|
|
@@ -9785,7 +9782,6 @@ function failedSpec(s) {
|
|
|
9785
9782
|
*/
|
|
9786
9783
|
async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, customPrompt, triageUserPrompt, diffProvider) {
|
|
9787
9784
|
const failureAnalysisEnabled = diffProvider != null;
|
|
9788
|
-
const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
|
|
9789
9785
|
const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
|
|
9790
9786
|
ok: false,
|
|
9791
9787
|
reason: "skipped by flags"
|
|
@@ -9796,7 +9792,7 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
|
|
|
9796
9792
|
const specInfoByKey = new Map(tree.flatMap((f) => f.specs.map((sp) => [`${f.featureName}/${sp.specName}`, sp])));
|
|
9797
9793
|
const findSpecInfo = (s) => specInfoByKey.get(`${s.featureName}/${s.specName}`) ?? null;
|
|
9798
9794
|
let driftResults = [];
|
|
9799
|
-
if (
|
|
9795
|
+
if (failureAnalysisEnabled && auth.ok && failed.length > 0) {
|
|
9800
9796
|
const targets = failed.map((s) => {
|
|
9801
9797
|
const spec = findSpecInfo(s);
|
|
9802
9798
|
if (!spec) return null;
|
|
@@ -10275,7 +10271,7 @@ function installTeardownSignalHandlers(teardown) {
|
|
|
10275
10271
|
}
|
|
10276
10272
|
//#endregion
|
|
10277
10273
|
//#region src/cli/run.ts
|
|
10278
|
-
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs. Each spec's execution mode comes from its spec.yaml `mode:` field (default deterministic; set `mode: live` to have Claude drive agent-browser live per step). Deterministic specs replay the recorded test.spec.ts under vitest. A structured report (report.json + evidence) is always written; use --push-report to also stream it to a hub.").option("--report [dir]", `Directory for the structured run results (report.json + evidence PNGs) that are always written. Default: ${DEFAULT_REPORT_DIR}/. Pass this only to change the location.`).option("--push-report", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--changed [base]", "Restrict execution to specs whose relatedPaths intersect the git diff against [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --changed=origin/main). Cannot be combined with an explicit spec id.").option("--failure-analysis [base]", "Classify each failure (TEST_DRIFT / SPEC_CHANGE / PRODUCT_BUG) against the source diff since [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --failure-analysis=origin/main), or pass 'last-green' to diff each spec against the commit where it last passed (per-spec baselines from the hub; requires a hub connection). Off by default — no Claude calls without it.").option("--
|
|
10274
|
+
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs. Each spec's execution mode comes from its spec.yaml `mode:` field (default deterministic; set `mode: live` to have Claude drive agent-browser live per step). Deterministic specs replay the recorded test.spec.ts under vitest. A structured report (report.json + evidence) is always written; use --push-report to also stream it to a hub.").option("--report [dir]", `Directory for the structured run results (report.json + evidence PNGs) that are always written. Default: ${DEFAULT_REPORT_DIR}/. Pass this only to change the location.`).option("--push-report", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--changed [base]", "Restrict execution to specs whose relatedPaths intersect the git diff against [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --changed=origin/main). Cannot be combined with an explicit spec id.").option("--failure-analysis [base]", "Classify each failure (TEST_DRIFT / SPEC_CHANGE / PRODUCT_BUG) against the source diff since [base]. Without a value the base comes from $GITHUB_BASE_REF (pull_request CI); elsewhere pass it explicitly (e.g. --failure-analysis=origin/main), or pass 'last-green' to diff each spec against the commit where it last passed (per-spec baselines from the hub; requires a hub connection). Off by default — no Claude calls without it.").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--format <fmt>", "Additional output format alongside HTML when --report is set: 'text' (default), 'json' (writes report.json), 'github' (GitHub Actions annotations on stdout).", (raw) => {
|
|
10279
10275
|
if (REPORT_FORMATS.includes(raw)) return raw;
|
|
10280
10276
|
throw new Error(`--format must be one of ${REPORT_FORMATS.join(" | ")}`);
|
|
10281
10277
|
}, "text").option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--no-evidence", `(deterministic only) Skip step-boundary evidence capture (PNG + meta JSON written to ${DEFAULT_REPORT_DIR}/${EVIDENCE_SUBDIR}/ by default).`).option("--retry <n>", "(live only) Retry each failed step up to N more times before recording failure. Default 0.", (raw) => {
|
|
@@ -15104,21 +15100,25 @@ const HTML_BODY = `
|
|
|
15104
15100
|
<div class="rd-head" id="rd-head"></div>
|
|
15105
15101
|
|
|
15106
15102
|
<!-- Triage first: grading + learning is the most important action, so it
|
|
15107
|
-
sits above the spec list rather than being buried below it.
|
|
15103
|
+
sits above the spec list rather than being buried below it. The
|
|
15104
|
+
heading row carries the graded counter; one card below it holds
|
|
15105
|
+
the confusion matrix (or its empty state) with the learn CTA as
|
|
15106
|
+
the card footer — grades are the learning job's input, so
|
|
15107
|
+
grade → tally → learn reads top to bottom. -->
|
|
15108
15108
|
<div class="triage-head" id="triage-head">
|
|
15109
15109
|
<h3 style="font-size:14px" data-i18n="detail.triage">Triage</h3>
|
|
15110
15110
|
<span class="triage-summary" id="triage-summary"></span>
|
|
15111
15111
|
</div>
|
|
15112
|
-
<div class="card" id="
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15112
|
+
<div class="card triage-card" id="triage-card">
|
|
15113
|
+
<div id="matrix-card"></div>
|
|
15114
|
+
<div class="learn-cta" id="learn-cta" hidden>
|
|
15115
|
+
<div class="learn-cta-text">
|
|
15116
|
+
<div class="t" data-i18n="learn.cta.title">Learn from these grades</div>
|
|
15117
|
+
<div class="d" data-i18n="learn.cta.desc">Turn the graded cases into a custom prompt that calibrates future failure classification.</div>
|
|
15118
|
+
</div>
|
|
15119
|
+
<div class="learn-cta-actions">
|
|
15120
|
+
<button class="btn primary sm" id="learn-run" data-i18n="learn.cta.run">Learn</button>
|
|
15121
|
+
</div>
|
|
15122
15122
|
</div>
|
|
15123
15123
|
</div>
|
|
15124
15124
|
|
|
@@ -15442,8 +15442,6 @@ const CSS = `
|
|
|
15442
15442
|
.badge-det { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
15443
15443
|
/* which generation target ran the spec (agent-browser / playwright / runn) */
|
|
15444
15444
|
.badge-target { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: var(--radius-sm); font-size: 11px; font-family: var(--mono); background: var(--surface-3); color: var(--muted); border: 1px solid var(--border); }
|
|
15445
|
-
.badge.drift-warn { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
15446
|
-
.badge.drift-warn .d { background: var(--amber); }
|
|
15447
15445
|
.badge-drift { background: var(--violet-bg); color: var(--violet); border-color: var(--violet-border); }
|
|
15448
15446
|
.chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 6px; background: var(--surface-3); border: 1px solid var(--border); color: var(--fg-dim); font-size: 12px; font-family: var(--mono); }
|
|
15449
15447
|
/* Below .chip in source order so these override its background/border/color
|
|
@@ -15486,11 +15484,18 @@ const CSS = `
|
|
|
15486
15484
|
.spec-card-head .spacer { flex: 1; }
|
|
15487
15485
|
.spec-card-body { padding: 0 20px 16px; }
|
|
15488
15486
|
/* Tier2 verdict block */
|
|
15489
|
-
|
|
15487
|
+
/* The diagnosis card: a bordered sub-surface so the model's verdict + the
|
|
15488
|
+
grading zone read as one unit, distinct from the execution details
|
|
15489
|
+
(steps/assertions) below it. */
|
|
15490
|
+
.analysis-box { display: flex; flex-direction: column; gap: 12px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); padding: 14px 16px; }
|
|
15491
|
+
.analysis-box .acc > summary:hover { background: var(--surface-3); }
|
|
15490
15492
|
.analysis-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
15491
|
-
.analysis-
|
|
15492
|
-
.analysis-
|
|
15493
|
-
.analysis-
|
|
15493
|
+
.analysis-kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: 13.5px; }
|
|
15494
|
+
.analysis-kv .k { font-size: 11px; font-weight: 600; color: var(--muted); padding-top: 3px; white-space: nowrap; }
|
|
15495
|
+
.analysis-kv .v { color: var(--fg-dim); line-height: 1.55; }
|
|
15496
|
+
.analysis-kv .v.headline { color: var(--fg); font-weight: 600; }
|
|
15497
|
+
/* Model-evidence rows reuse the drift-row list shape; only the file ref needs its own style. */
|
|
15498
|
+
.ev-file { font-size: 12px; color: var(--fg-dim); }
|
|
15494
15499
|
.analysis-reasoning { font-size: 13px; color: var(--fg-dim); white-space: pre-wrap; line-height: 1.6; }
|
|
15495
15500
|
.analysis-inline-reason { font-size: 13px; color: var(--fg-dim); line-height: 1.55; }
|
|
15496
15501
|
/* Tier3 accordion (real header bar + rotating chevron, replaces the tiny ▸) */
|
|
@@ -15556,11 +15561,12 @@ const CSS = `
|
|
|
15556
15561
|
|
|
15557
15562
|
/* triage grading — an explicit question + a segmented single-select, framed
|
|
15558
15563
|
as an action ("tell us the real cause"), not a data readout. */
|
|
15559
|
-
|
|
15564
|
+
/* Embedded at the bottom of the diagnosis card: a divider separates the
|
|
15565
|
+
human's grading zone from the model's output above it, without breaking
|
|
15566
|
+
the two out of the shared context. */
|
|
15567
|
+
.grade { margin-top: 2px; padding: 12px 0 0; border-top: 1px solid var(--border); }
|
|
15560
15568
|
.grade-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
15561
15569
|
.grade-q { font-size: 13px; font-weight: 600; color: var(--fg); }
|
|
15562
|
-
.grade-pred { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; font-size: 12px; color: var(--muted); }
|
|
15563
|
-
.grade-pred .grade-arrow { color: var(--muted-2); }
|
|
15564
15570
|
.grade-bottom { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
15565
15571
|
.grade-seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-md); overflow: hidden; background: var(--surface); }
|
|
15566
15572
|
.grade-seg .seg { height: 34px; padding: 0 14px; border: 0; background: transparent; color: var(--muted); font-size: 13px; font-weight: 500; border-right: 1px solid var(--border); display: inline-flex; align-items: center; gap: 5px; }
|
|
@@ -15698,7 +15704,7 @@ const CSS = `
|
|
|
15698
15704
|
.ro-tag { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
|
|
15699
15705
|
color: var(--muted-2); background: var(--surface-3); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; margin-left: 4px; }
|
|
15700
15706
|
|
|
15701
|
-
.learn-cta { display: flex; align-items: center; gap: 16px;
|
|
15707
|
+
.learn-cta { display: flex; align-items: center; gap: 16px; padding: 14px 20px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--radius-md) var(--radius-md); }
|
|
15702
15708
|
.learn-cta-text { flex: 1; min-width: 0; }
|
|
15703
15709
|
.learn-cta-text .t { font-size: 13.5px; font-weight: 600; color: var(--fg); }
|
|
15704
15710
|
.learn-cta-text .d { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
|
@@ -15819,26 +15825,25 @@ const CLIENT_JS = `
|
|
|
15819
15825
|
"meta.branch": "Branch", "meta.specs": "Specs", "meta.prompt": "Prompt",
|
|
15820
15826
|
"meta.created": "Created", "meta.passed": "passed", "meta.profile": "Profile",
|
|
15821
15827
|
"meta.drift": "Drift",
|
|
15822
|
-
"
|
|
15828
|
+
"diag.cause": "Cause", "diag.fix": "Fix",
|
|
15823
15829
|
"acc.reasoning": "Reasoning", "acc.evidence": "Evidence", "acc.steps": "Live run steps",
|
|
15824
15830
|
"acc.assertions": "Assertions", "acc.drift": "Drift audit",
|
|
15825
15831
|
"acc.artifacts": "Artifacts",
|
|
15826
15832
|
"art.open": "Open", "art.loadFailed": "could not load (it may have been omitted from the push)",
|
|
15827
15833
|
"acc.assertions.hint": "Test cases from the recorded spec run",
|
|
15828
15834
|
"spec.kind.live": "Live", "spec.kind.det": "Deterministic",
|
|
15829
|
-
"
|
|
15835
|
+
"det.steps": "Steps",
|
|
15830
15836
|
"kind.run": "Test run", "kind.drift": "Drift audit",
|
|
15831
15837
|
"drift.summary.issues": "Issues", "drift.summary.errors": "Errors",
|
|
15832
15838
|
"drift.summary.warnings": "Warnings", "drift.summary.specsWithIssues": "Specs with issues",
|
|
15833
15839
|
"drift.clean": "No drift issues",
|
|
15834
|
-
"grade.question": "What was the real cause?",
|
|
15840
|
+
"grade.question": "What was the real cause?",
|
|
15835
15841
|
"grade.ungraded": "ungraded", "grade.matches": "saved · matches",
|
|
15836
15842
|
"grade.corrected": "saved · corrected", "grade.saving": "saving…",
|
|
15837
15843
|
"grade.error": "couldn't save — retry",
|
|
15838
|
-
"matrix.empty": "No
|
|
15844
|
+
"matrix.empty": "No grades yet. Pick the real cause on a failed spec's diagnosis card below and it is tallied here.",
|
|
15839
15845
|
"matrix.predicted": "predicted \\\\ actual", "matrix.accuracy": "Accuracy",
|
|
15840
15846
|
"matrix.accSuffix": "of graded cases match the prediction", "matrix.graded": "graded",
|
|
15841
|
-
"matrix.progress": "Recorded actual cause: {n} / {total} failing specs",
|
|
15842
15847
|
"learn.cta.title": "Learn from these grades",
|
|
15843
15848
|
"learn.cta.desc": "Learn from what you graded so ccqa classifies failure causes the same way next time.",
|
|
15844
15849
|
"learn.cta.run": "Learn",
|
|
@@ -15909,26 +15914,25 @@ const CLIENT_JS = `
|
|
|
15909
15914
|
"meta.branch": "ブランチ", "meta.specs": "スペック", "meta.prompt": "プロンプト",
|
|
15910
15915
|
"meta.created": "作成", "meta.passed": "合格", "meta.profile": "プロファイル",
|
|
15911
15916
|
"meta.drift": "ドリフト",
|
|
15912
|
-
"
|
|
15917
|
+
"diag.cause": "原因", "diag.fix": "対処",
|
|
15913
15918
|
"acc.reasoning": "推論", "acc.evidence": "根拠", "acc.steps": "実行ステップ",
|
|
15914
15919
|
"acc.assertions": "アサーション", "acc.drift": "ドリフト監査",
|
|
15915
15920
|
"acc.artifacts": "成果物",
|
|
15916
15921
|
"art.open": "開く", "art.loadFailed": "読み込めませんでした(push時に省略された可能性があります)",
|
|
15917
15922
|
"acc.assertions.hint": "記録したスペック実行のテストケース",
|
|
15918
15923
|
"spec.kind.live": "ライブ", "spec.kind.det": "決定的",
|
|
15919
|
-
"
|
|
15924
|
+
"det.steps": "ステップ",
|
|
15920
15925
|
"kind.run": "テスト実行", "kind.drift": "ドリフト監査",
|
|
15921
15926
|
"drift.summary.issues": "問題数", "drift.summary.errors": "エラー",
|
|
15922
15927
|
"drift.summary.warnings": "警告", "drift.summary.specsWithIssues": "問題のあるスペック",
|
|
15923
15928
|
"drift.clean": "ドリフトの問題なし",
|
|
15924
|
-
"grade.question": "実際の原因は何でしたか?",
|
|
15929
|
+
"grade.question": "実際の原因は何でしたか?",
|
|
15925
15930
|
"grade.ungraded": "未評価", "grade.matches": "保存済み · 一致",
|
|
15926
15931
|
"grade.corrected": "保存済み · 修正", "grade.saving": "保存中…",
|
|
15927
15932
|
"grade.error": "保存に失敗 — 再試行",
|
|
15928
|
-
"matrix.empty": "
|
|
15933
|
+
"matrix.empty": "まだ採点がありません。下の失敗スペックの診断カードで実際の原因を選ぶと、ここに集計されます。",
|
|
15929
15934
|
"matrix.predicted": "予測 \\\\ 実際", "matrix.accuracy": "正解率",
|
|
15930
15935
|
"matrix.accSuffix": "件の採点が予測と一致", "matrix.graded": "採点済み",
|
|
15931
|
-
"matrix.progress": "実際の原因を記録: {total} 件中 {n} 件の失敗スペック",
|
|
15932
15936
|
"learn.cta.title": "この採点から学習",
|
|
15933
15937
|
"learn.cta.desc": "採点した内容をもとに、ccqaが次回から同じように失敗の原因を分類できるよう学習します。",
|
|
15934
15938
|
"learn.cta.run": "学習",
|
|
@@ -16528,27 +16532,61 @@ const CLIENT_JS = `
|
|
|
16528
16532
|
|
|
16529
16533
|
// ── run detail: spec cards ──────────────────────────────────────────
|
|
16530
16534
|
|
|
16531
|
-
//
|
|
16532
|
-
//
|
|
16533
|
-
//
|
|
16535
|
+
// The diagnosis card: one surface for everything about a failure's cause.
|
|
16536
|
+
// Verdict (label + confidence), then the cause→fix pair as labelled rows —
|
|
16537
|
+
// headline and recommendation are one causal unit, so they read as one.
|
|
16538
|
+
// subDiagnosis is deliberately NOT shown: it is a machine vocabulary for
|
|
16539
|
+
// accuracy stratification and learning, not for humans. The caller appends
|
|
16540
|
+
// the evidence/reasoning accordions and the grading zone into this box.
|
|
16534
16541
|
function analysisSection(runId, r) {
|
|
16535
16542
|
var wrap = el("div", "analysis-box");
|
|
16536
16543
|
var a = r.analysis;
|
|
16537
16544
|
var head = el("div", "analysis-head");
|
|
16538
16545
|
head.appendChild(labelChip(a.label));
|
|
16539
16546
|
head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
|
|
16540
|
-
if (a.subDiagnosis && a.subDiagnosis !== "NONE") head.appendChild(el("span", "muted", a.subDiagnosis));
|
|
16541
16547
|
wrap.appendChild(head);
|
|
16542
|
-
|
|
16548
|
+
var kv = el("div", "analysis-kv");
|
|
16549
|
+
if (a.headline) {
|
|
16550
|
+
kv.appendChild(el("div", "k", t("diag.cause")));
|
|
16551
|
+
kv.appendChild(el("div", "v headline", a.headline));
|
|
16552
|
+
}
|
|
16543
16553
|
if (a.recommendation) {
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
rec.appendChild(document.createTextNode(a.recommendation));
|
|
16547
|
-
wrap.appendChild(rec);
|
|
16554
|
+
kv.appendChild(el("div", "k", t("diag.fix")));
|
|
16555
|
+
kv.appendChild(el("div", "v", a.recommendation));
|
|
16548
16556
|
}
|
|
16557
|
+
if (kv.childNodes.length > 0) wrap.appendChild(kv);
|
|
16549
16558
|
return wrap;
|
|
16550
16559
|
}
|
|
16551
16560
|
|
|
16561
|
+
// 根拠: the model's evidence items (file + what it proves) merged with the
|
|
16562
|
+
// drift-audit findings. The audit is an input hint TO the classifier, so
|
|
16563
|
+
// its findings belong here as supporting evidence — not as a sibling
|
|
16564
|
+
// section that reads like an independent feature.
|
|
16565
|
+
function analysisEvidenceSection(r) {
|
|
16566
|
+
var wrap = el("div");
|
|
16567
|
+
var count = 0;
|
|
16568
|
+
var items = r.analysis && r.analysis.evidence ? r.analysis.evidence : [];
|
|
16569
|
+
items.forEach(function (e) {
|
|
16570
|
+
// Same list shape as the drift findings below, so the merged 根拠 list
|
|
16571
|
+
// reads as one.
|
|
16572
|
+
var row = el("div", "drift-row");
|
|
16573
|
+
if (e.file) {
|
|
16574
|
+
var head = el("div", "drift-head");
|
|
16575
|
+
head.appendChild(el("code", "ev-file", e.file));
|
|
16576
|
+
row.appendChild(head);
|
|
16577
|
+
}
|
|
16578
|
+
row.appendChild(el("div", "drift-msg", e.detail));
|
|
16579
|
+
wrap.appendChild(row);
|
|
16580
|
+
count++;
|
|
16581
|
+
});
|
|
16582
|
+
if (r.driftIssues && r.driftIssues.length > 0) {
|
|
16583
|
+
wrap.appendChild(el("div", "section-label", t("acc.drift")));
|
|
16584
|
+
wrap.appendChild(driftSection(r.driftIssues));
|
|
16585
|
+
count += r.driftIssues.length;
|
|
16586
|
+
}
|
|
16587
|
+
return { node: wrap, count: count };
|
|
16588
|
+
}
|
|
16589
|
+
|
|
16552
16590
|
function evidenceSection(runId, evidence) {
|
|
16553
16591
|
var grid = el("div", "evidence-grid");
|
|
16554
16592
|
evidence.forEach(function (e) {
|
|
@@ -16780,25 +16818,22 @@ const CLIENT_JS = `
|
|
|
16780
16818
|
return det;
|
|
16781
16819
|
}
|
|
16782
16820
|
|
|
16783
|
-
// The grading action: an explicit question ("What was the real cause?")
|
|
16784
|
-
//
|
|
16785
|
-
//
|
|
16786
|
-
//
|
|
16787
|
-
//
|
|
16788
|
-
//
|
|
16821
|
+
// The grading action: an explicit question ("What was the real cause?"), a
|
|
16822
|
+
// segmented single-select over the failure labels, and a status chip
|
|
16823
|
+
// (ungraded / saved·matches / saved·corrected). One tap grades it.
|
|
16824
|
+
// Optimistic PUT with rollback; on success it refreshes the confusion
|
|
16825
|
+
// matrix. The English label value is what's sent and stored; the segment
|
|
16826
|
+
// just shows its localized name.
|
|
16789
16827
|
function triageGradeControl(runId, r, triageState) {
|
|
16790
16828
|
var key = r.feature + "/" + r.spec;
|
|
16791
16829
|
var predicted = r.analysis ? r.analysis.label : "UNKNOWN";
|
|
16792
16830
|
|
|
16793
16831
|
var wrap = el("div", "grade");
|
|
16794
16832
|
|
|
16833
|
+
// No "predicted →" chip here: the control lives inside the diagnosis
|
|
16834
|
+
// card, directly under the prediction it grades — repeating it is noise.
|
|
16795
16835
|
var top = el("div", "grade-top");
|
|
16796
16836
|
top.appendChild(el("span", "grade-q", t("grade.question")));
|
|
16797
|
-
var pred = el("span", "grade-pred");
|
|
16798
|
-
pred.appendChild(document.createTextNode(t("grade.predicted")));
|
|
16799
|
-
pred.appendChild(el("span", "grade-arrow", "→"));
|
|
16800
|
-
pred.appendChild(labelChip(predicted));
|
|
16801
|
-
top.appendChild(pred);
|
|
16802
16837
|
wrap.appendChild(top);
|
|
16803
16838
|
|
|
16804
16839
|
var bottom = el("div", "grade-bottom");
|
|
@@ -16882,13 +16917,6 @@ const CLIENT_JS = `
|
|
|
16882
16917
|
else if (r.liveRun) head.appendChild(el("span", "badge-live", t("spec.kind.live")));
|
|
16883
16918
|
else if (!external) head.appendChild(el("span", "badge-det", t("spec.kind.det")));
|
|
16884
16919
|
head.appendChild(statusBadge(r.status));
|
|
16885
|
-
var hasDriftError = r.driftIssues && r.driftIssues.some(function (d) { return d.severity === "ERROR"; });
|
|
16886
|
-
if (hasDriftError) {
|
|
16887
|
-
var w = el("span", "badge drift-warn");
|
|
16888
|
-
w.appendChild(el("span", "d"));
|
|
16889
|
-
w.appendChild(document.createTextNode(" " + t("spec.driftWarn")));
|
|
16890
|
-
head.appendChild(w);
|
|
16891
|
-
}
|
|
16892
16920
|
card.appendChild(head);
|
|
16893
16921
|
|
|
16894
16922
|
var body = el("div", "spec-card-body");
|
|
@@ -16899,17 +16927,23 @@ const CLIENT_JS = `
|
|
|
16899
16927
|
any = true;
|
|
16900
16928
|
}
|
|
16901
16929
|
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
//
|
|
16930
|
+
var hasAnalysis = r.status === "failed" && r.analysis;
|
|
16931
|
+
if (hasAnalysis) {
|
|
16932
|
+
// The diagnosis card: verdict + cause/fix, then evidence and reasoning
|
|
16933
|
+
// as accordions, then the grading zone — one surface for the whole
|
|
16934
|
+
// "why did this fail and was the call right" story.
|
|
16935
|
+
var box = analysisSection(runId, r);
|
|
16936
|
+
var ev = analysisEvidenceSection(r);
|
|
16937
|
+
if (ev.count > 0) box.appendChild(detailsBlock(t("acc.evidence"), ev.count, ev.node));
|
|
16938
|
+
// Reasoning: fold it as an accordion, but only when it carries real
|
|
16907
16939
|
// content. A one-char/empty reasoning behind a disclosure reads as broken
|
|
16908
16940
|
// (the old "▸ r"), so drop it entirely below the threshold.
|
|
16909
16941
|
var reasoning = r.analysis.reasoning ? String(r.analysis.reasoning).trim() : "";
|
|
16910
16942
|
if (reasoning.length > 2) {
|
|
16911
|
-
|
|
16943
|
+
box.appendChild(detailsBlock(t("acc.reasoning"), null, el("div", "analysis-reasoning", reasoning)));
|
|
16912
16944
|
}
|
|
16945
|
+
box.appendChild(triageGradeControl(runId, r, triageState));
|
|
16946
|
+
body.appendChild(box);
|
|
16913
16947
|
any = true;
|
|
16914
16948
|
} else if (r.status === "failed" && r.analysisSkipped) {
|
|
16915
16949
|
body.appendChild(el("div", "muted", "Analysis skipped: " + r.analysisSkipped));
|
|
@@ -16941,7 +16975,10 @@ const CLIENT_JS = `
|
|
|
16941
16975
|
any = true;
|
|
16942
16976
|
}
|
|
16943
16977
|
|
|
16944
|
-
|
|
16978
|
+
// Drift findings render standalone only when they aren't already folded
|
|
16979
|
+
// into the diagnosis card's evidence: drift-kind runs (the audit IS the
|
|
16980
|
+
// content) and failed rows whose analysis was skipped.
|
|
16981
|
+
if (!hasAnalysis && r.driftIssues && r.driftIssues.length > 0) {
|
|
16945
16982
|
body.appendChild(detailsBlock(t("acc.drift"), r.driftIssues.length, driftSection(r.driftIssues)));
|
|
16946
16983
|
any = true;
|
|
16947
16984
|
}
|
|
@@ -16976,12 +17013,9 @@ const CLIENT_JS = `
|
|
|
16976
17013
|
var cases = Object.keys(triageState.byKey).map(function (k) { return triageState.byKey[k]; })
|
|
16977
17014
|
.filter(function (c) { return c.predicted && c.actual; });
|
|
16978
17015
|
|
|
16979
|
-
//
|
|
16980
|
-
//
|
|
17016
|
+
// The "graded m / n" counter in the header is the single progress
|
|
17017
|
+
// readout — recomputed here so it stays in sync after each grade.
|
|
16981
17018
|
var total = typeof triageState.total === "number" ? triageState.total : Object.keys(triageState.byKey).length;
|
|
16982
|
-
document.getElementById("triage-progress").textContent =
|
|
16983
|
-
t("matrix.progress").replace("{n}", cases.length).replace("{total}", total);
|
|
16984
|
-
|
|
16985
17019
|
var summary = document.getElementById("triage-summary");
|
|
16986
17020
|
|
|
16987
17021
|
if (cases.length === 0) {
|
|
@@ -17052,7 +17086,12 @@ const CLIENT_JS = `
|
|
|
17052
17086
|
renderMatrix(triageState);
|
|
17053
17087
|
onLoaded(triageState);
|
|
17054
17088
|
}).catch(function (err) {
|
|
17055
|
-
|
|
17089
|
+
// Surface the load failure inside the triage card so it isn't silent.
|
|
17090
|
+
var card = document.getElementById("matrix-card");
|
|
17091
|
+
clear(card);
|
|
17092
|
+
var wrap = el("div", "matrix-wrap");
|
|
17093
|
+
wrap.appendChild(el("div", "muted", "Error loading triage: " + err.message));
|
|
17094
|
+
card.appendChild(wrap);
|
|
17056
17095
|
onLoaded({ byKey: {} });
|
|
17057
17096
|
});
|
|
17058
17097
|
}
|
|
@@ -17078,7 +17117,6 @@ const CLIENT_JS = `
|
|
|
17078
17117
|
clear(document.getElementById("rd-head"));
|
|
17079
17118
|
clear(document.getElementById("matrix-card"));
|
|
17080
17119
|
document.getElementById("detail-spec-count").textContent = "";
|
|
17081
|
-
document.getElementById("triage-progress").textContent = "";
|
|
17082
17120
|
document.getElementById("triage-summary").textContent = "";
|
|
17083
17121
|
|
|
17084
17122
|
apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
|
|
@@ -17092,8 +17130,7 @@ const CLIENT_JS = `
|
|
|
17092
17130
|
// as the other, and neither escapes its own catch.
|
|
17093
17131
|
var isDrift = report.kind === "drift";
|
|
17094
17132
|
document.getElementById("triage-head").hidden = isDrift;
|
|
17095
|
-
document.getElementById("
|
|
17096
|
-
document.getElementById("triage-progress").hidden = isDrift;
|
|
17133
|
+
document.getElementById("triage-card").hidden = isDrift;
|
|
17097
17134
|
renderSpecCards(runId, report.results, { byKey: {} }, isDrift);
|
|
17098
17135
|
if (isDrift) return; // drift runs have no triage: skip loadTriage entirely
|
|
17099
17136
|
loadTriage(runId, function (triageState) {
|
package/dist/package.json
CHANGED