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 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.driftAuditEnabled && r.result.status === "failed" ? await runDriftAuditOne(r, opts, cwd) : null;
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 (driftAuditEnabled && auth.ok && failed.length > 0) {
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("--no-drift-audit", "With --failure-analysis: skip the spec↔code drift audit shown in the report.").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) => {
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="matrix-card"></div>
15113
- <p class="muted" id="triage-progress" style="font-size:12.5px;margin-top:8px"></p>
15114
-
15115
- <div class="learn-cta" id="learn-cta" hidden>
15116
- <div class="learn-cta-text">
15117
- <div class="t" data-i18n="learn.cta.title">Learn from these grades</div>
15118
- <div class="d" data-i18n="learn.cta.desc">Turn the graded cases into a custom prompt that calibrates future failure classification.</div>
15119
- </div>
15120
- <div class="learn-cta-actions">
15121
- <button class="btn primary sm" id="learn-run" data-i18n="learn.cta.run">Learn</button>
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
- .analysis-box { display: flex; flex-direction: column; gap: 12px; padding-bottom: 4px; }
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-headline { font-size: 14px; font-weight: 600; color: var(--fg); line-height: 1.5; }
15492
- .analysis-rec { font-size: 13px; color: var(--fg-dim); background: var(--surface-2); border: 1px solid var(--border); border-left: 2px solid var(--muted); border-radius: var(--radius-sm); padding: 10px 12px; line-height: 1.55; }
15493
- .analysis-rec .rec-k { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 600; margin-bottom: 4px; }
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
- .grade { margin-top: 4px; padding: 14px; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface-2); }
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; margin-top: 18px; padding: 16px 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-2); }
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
- "rec.title": "Recommendation",
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
- "spec.driftWarn": "Spec drift", "det.steps": "Steps",
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?", "grade.predicted": "predicted",
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 graded cases yet. Grade a failed spec above to populate the confusion matrix.",
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
- "rec.title": "推奨対応",
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
- "spec.driftWarn": "仕様ドリフト", "det.steps": "ステップ",
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": "実際の原因は何でしたか?", "grade.predicted": "予測",
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
- // Tier2 verdict block: label + confidence, headline, and the recommendation
16532
- // callout. Reasoning is NOT here renderSpecCard places it as a Tier3
16533
- // accordion (or inline when it's too short to be worth folding).
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
- if (a.headline) wrap.appendChild(el("div", "analysis-headline", a.headline));
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
- var rec = el("div", "analysis-rec");
16545
- rec.appendChild(el("span", "rec-k", t("rec.title")));
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?") with
16784
- // the model's guess as muted context, a segmented single-select over the
16785
- // failure labels, and a status chip (ungraded / saved·matches / saved·
16786
- // corrected). One tap grades it. Optimistic PUT with rollback; on success it
16787
- // refreshes the confusion matrix. The English label value is what's sent and
16788
- // stored; the segment just shows its localized name.
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
- if (r.status === "failed" && r.analysis) {
16903
- // Tier2: the verdict block (analysis) + the grading action, always shown.
16904
- body.appendChild(analysisSection(runId, r));
16905
- body.appendChild(triageGradeControl(runId, r, triageState));
16906
- // Reasoning: fold it as a Tier3 accordion, but only when it carries real
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
- body.appendChild(detailsBlock(t("acc.reasoning"), null, el("div", "analysis-reasoning", reasoning)));
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
- if (r.driftIssues && r.driftIssues.length > 0) {
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
- // Recompute the progress line here so it stays in sync after each grade,
16980
- // not just on initial load.
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
- document.getElementById("triage-progress").textContent = "Error loading triage: " + err.message;
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("matrix-card").hidden = isDrift;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {