ccqa 1.4.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
@@ -3161,12 +3161,45 @@ function hashTriageUserPrompt(text) {
3161
3161
  */
3162
3162
  const CHANGED_FILE_DIFF_TOOL = "mcp__diff__changed_file_diff";
3163
3163
  function buildFailureAnalysisPrompt(input) {
3164
- const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, driftIssues, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
3164
+ const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
3165
+ const lastGreen = baseSource === "last-green";
3165
3166
  const triageUserPromptBlock = buildTriageUserPromptBlock(triageUserPrompt);
3166
3167
  const customPromptBlock = buildCustomPromptBlock(customPrompt);
3167
- return `You are analyzing a failing E2E regression test right after a source change landed. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure, using the source diff as your primary context.
3168
+ const languageBlock = outputLanguageBlock(outputLanguage, "`reasoning`, `detail`", "label names (TEST_DRIFT, etc.)");
3169
+ const executionBlock = buildExecutionEvidenceBlock(script, failureLog, liveTranscriptExcerpt);
3170
+ const baseLabel = lastGreen ? `this spec's last passing commit${baseRef && baseRef !== "last-green" ? ` (${baseRef})` : ""}` : baseRef ?? "base";
3171
+ const rangeNote = range ? ` — spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}` : "";
3172
+ let diffBlock;
3173
+ if (diffPatch === null) diffBlock = `## Source changes
3168
3174
 
3169
- ${outputLanguageBlock(outputLanguage, "`reasoning`, `detail`", "label names (TEST_DRIFT, etc.)")}## The three categories
3175
+ No diff context is available (the base ref could not be resolved, or there are no changes). Classify from the failure log, the spec, and what you can read in the repository — and be correspondingly more conservative: prefer UNKNOWN over a confident SPEC_CHANGE/PRODUCT_BUG call without diff evidence.
3176
+ `;
3177
+ else if (diffPatch.length === 0) diffBlock = `## Source changes since ${baseLabel}${rangeNote}
3178
+
3179
+ ### Changed files (name-status)
3180
+ ${changedFiles && changedFiles.length > 0 ? changedFiles : "(no changes in range)"}
3181
+
3182
+ No changed file matches this spec's relatedPaths, so no hunks are inlined. "No related change" is a real signal — but before concluding, scan the name-status list for anything that could plausibly reach this spec and fetch its hunk with \`${CHANGED_FILE_DIFF_TOOL}\`.
3183
+ `;
3184
+ else diffBlock = `## Source changes since ${baseLabel}${rangeNote} (git diff, scoped to this spec's relatedPaths, may be truncated)
3185
+
3186
+ ### Changed files (name-status)
3187
+ ${changedFiles ?? "(unavailable)"}
3188
+
3189
+ ### Patch
3190
+ \`\`\`diff
3191
+ ${diffPatch}
3192
+ \`\`\`
3193
+ `;
3194
+ const driftBlock = driftIssues && driftIssues.length > 0 ? `## Spec↔code drift audit findings
3195
+
3196
+ A separate read-only audit compared the spec against the current source. Treat these as hints, not verdicts:
3197
+
3198
+ ${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
3199
+ ` : "";
3200
+ return `You are analyzing a failing E2E regression test against the source changes since a known-good baseline. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure, using the source diff as your primary context.
3201
+
3202
+ ${languageBlock}## The three categories
3170
3203
 
3171
3204
  The question that separates them: **is the behavior the spec describes still what the product intends?**
3172
3205
 
@@ -3189,10 +3222,13 @@ You have **up to 12 tool turns**. Do NOT write, edit, run shell commands, or hit
3189
3222
 
3190
3223
  ## Decision guidance
3191
3224
 
3225
+ ${lastGreen ? `The baseline is the commit where THIS spec last passed, so the range strictly covers the window in which it broke: the cause is either inside these changes or outside the code entirely (flaky timing, environment, an external service, test data). The range may mix several unrelated merges — most of the diff is noise; what matters is the specific change you can tie to the failing step.` : `The baseline is a fixed ref (typically the PR base): the spec is NOT guaranteed to have passed there, so the range is not guaranteed to contain the cause.`}
3226
+
3192
3227
  - Diff touches only attributes/identifiers the test selects on (labels, testids, class names, timing) while the user-visible flow is intact → TEST_DRIFT.
3193
3228
  - Diff intentionally removes/reworks the UI or flow that a spec step verifies (component deleted, page restructured, copy redefined, feature flag flipped) → SPEC_CHANGE.
3194
3229
  - Diff UNINTENTIONALLY breaks behavior the spec still intends — e.g. a refactor that drops a side effect, an inverted condition, a regression hiding inside a cleanup commit — → PRODUCT_BUG, citing the diff hunk as evidence. A product bug is often introduced BY the diff; what separates it from SPEC_CHANGE is intent: does the change read as a deliberate redesign of what the spec verifies, or as collateral damage?
3195
- - Diff is unrelated to the failing step (or there is no relevant diff) and the test was passing before → lean PRODUCT_BUG; first rule out timing/data flakiness and infrastructure errors (daemon not running, network down, missing credentials) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.
3230
+ ${lastGreen ? `- No change in the range explains the failing step (after checking the inline patch, the name-status list, and any hunks you fetched) → the cause is outside the code: answer UNKNOWN with low confidence and name the suspected external cause (flaky timing, environment, external service, test data). Do NOT default to PRODUCT_BUG here — under this baseline a product regression must be tied to an in-range change.` : `- Diff is unrelated to the failing step (or there is no relevant diff) and the test was passing before → lean PRODUCT_BUG; first rule out timing/data flakiness and infrastructure errors (daemon not running, network down, missing credentials) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.`}${range ? `
3231
+ - This range spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}. The wider the range, the more unrelated changes are mixed in: SPEC_CHANGE and TEST_DRIFT still require citing the specific hunk — do not infer intent from the bulk of a large diff, and lower confidence when the evidence is spread thin.` : ""}
3196
3232
  - The drift audit findings (when present) flag spec↔code mismatches; an ERROR there usually supports TEST_DRIFT or SPEC_CHANGE over PRODUCT_BUG.
3197
3233
 
3198
3234
  ## Sub-diagnosis vocabulary
@@ -3232,32 +3268,15 @@ Your **final** assistant message must start with \`{\` and end with \`}\` — a
3232
3268
  - 0.4-0.7: plausible but another category could explain it
3233
3269
  - < 0.4: answer UNKNOWN instead of guessing
3234
3270
 
3235
- Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). PRODUCT_BUG should explain why the diff does NOT account for the failure.
3271
+ Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). PRODUCT_BUG should cite the in-range change that unintentionally broke the behavior when one exists; ${lastGreen ? "under this last-green baseline, if no in-range change explains the failure, that is UNKNOWN (external cause), not PRODUCT_BUG" : "when no such change exists, explain why the diff does NOT account for the failure"}.
3236
3272
 
3237
3273
  ## Test Spec (spec.yaml)
3238
3274
  ${specYaml}
3239
3275
 
3240
- ${buildExecutionEvidenceBlock(script, failureLog, liveTranscriptExcerpt)}
3241
-
3242
- ${diffPatch ? `## Source changes since ${baseRef ?? "base"} (git diff, may be truncated)
3243
-
3244
- ### Changed files (name-status)
3245
- ${changedFiles ?? "(unavailable)"}
3246
-
3247
- ### Patch
3248
- \`\`\`diff
3249
- ${diffPatch}
3250
- \`\`\`
3251
- ` : `## Source changes
3252
-
3253
- No diff context is available (the base ref could not be resolved, or there are no changes). Classify from the failure log, the spec, and what you can read in the repository — and be correspondingly more conservative: prefer UNKNOWN over a confident SPEC_CHANGE/PRODUCT_BUG call without diff evidence.
3254
- `}
3255
- ${driftIssues && driftIssues.length > 0 ? `## Spec↔code drift audit findings
3256
-
3257
- A separate read-only audit compared the spec against the current source. Treat these as hints, not verdicts:
3276
+ ${executionBlock}
3258
3277
 
3259
- ${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
3260
- ` : ""}`;
3278
+ ${diffBlock}
3279
+ ${driftBlock}`;
3261
3280
  }
3262
3281
  /**
3263
3282
  * Render the execution-evidence section the model needs to classify the
@@ -3671,17 +3690,20 @@ function splitPatchByFile(patch) {
3671
3690
  /**
3672
3691
  * Scope a full patch down to the files a spec depends on, then truncate so
3673
3692
  * the analysis prompt stays bounded. `relatedPaths` null/empty means the
3674
- * spec is unscoped — keep the whole patch (still truncated). Callers scoping
3675
- * the same patch for many specs can pass pre-split sections instead.
3693
+ * spec is unscoped — keep the whole patch (still truncated). When
3694
+ * relatedPaths are declared but nothing in the diff matches, the result is
3695
+ * the empty string: "no related change" is itself a signal the prompt
3696
+ * renders explicitly, and the model can inspect any unmatched file's hunk
3697
+ * via the on-demand diff tool — inlining the full unrelated diff (the old
3698
+ * fallback) just burned the prompt budget, especially under wide last-green
3699
+ * baselines. Callers scoping the same patch for many specs can pass
3700
+ * pre-split sections instead.
3676
3701
  */
3677
3702
  function scopePatchForSpec(patch, relatedPaths, caps = {}) {
3678
3703
  const perFile = caps.perFile ?? 8192;
3679
3704
  const total = caps.total ?? 49152;
3680
3705
  let sections = typeof patch === "string" ? splitPatchByFile(patch) : patch;
3681
- if (relatedPaths && relatedPaths.length > 0) {
3682
- const scoped = sections.filter((s) => isPathAffectedBy(s.path, relatedPaths));
3683
- if (scoped.length > 0) sections = scoped;
3684
- }
3706
+ if (relatedPaths && relatedPaths.length > 0) sections = sections.filter((s) => isPathAffectedBy(s.path, relatedPaths));
3685
3707
  const parts = [];
3686
3708
  let used = 0;
3687
3709
  let droppedFiles = 0;
@@ -3716,6 +3738,40 @@ function lookupFileDiff(sections, path) {
3716
3738
  if (section.body.length <= 16384) return section.body;
3717
3739
  return `${section.body.slice(0, FILE_DIFF_RESPONSE_CAP)}\n[truncated: ${section.body.length - FILE_DIFF_RESPONSE_CAP} more chars — Read the file for its full current state]`;
3718
3740
  }
3741
+ /**
3742
+ * Best-effort width of the base..HEAD range. Two-dot rev-list matches what
3743
+ * the three-dot diff shows: commits on the HEAD side since the merge base.
3744
+ */
3745
+ async function measureRange(sha, cwd) {
3746
+ try {
3747
+ const [{ stdout: count }, { stdout: baseTime }, { stdout: headTime }] = await Promise.all([
3748
+ execFileP("git", [
3749
+ "rev-list",
3750
+ "--count",
3751
+ `${sha}..HEAD`
3752
+ ], { cwd }),
3753
+ execFileP("git", [
3754
+ "log",
3755
+ "-1",
3756
+ "--format=%ct",
3757
+ sha
3758
+ ], { cwd }),
3759
+ execFileP("git", [
3760
+ "log",
3761
+ "-1",
3762
+ "--format=%ct",
3763
+ "HEAD"
3764
+ ], { cwd })
3765
+ ]);
3766
+ const seconds = Number(headTime.trim()) - Number(baseTime.trim());
3767
+ return {
3768
+ commitCount: Number(count.trim()),
3769
+ days: Math.max(0, Math.round(seconds / 86400))
3770
+ };
3771
+ } catch {
3772
+ return null;
3773
+ }
3774
+ }
3719
3775
  function createDiffProvider(args) {
3720
3776
  const { resolveBase, cwd } = args;
3721
3777
  const captures = /* @__PURE__ */ new Map();
@@ -3724,17 +3780,19 @@ function createDiffProvider(args) {
3724
3780
  const cached = captures.get(sha);
3725
3781
  if (cached) return cached;
3726
3782
  const pending = (async () => {
3727
- const result = await capturePrDiff(sha, cwd);
3783
+ const [result, range] = await Promise.all([capturePrDiff(sha, cwd), measureRange(sha, cwd)]);
3728
3784
  if (!result.ok) return {
3729
3785
  sections: null,
3730
3786
  nameStatus: null,
3731
- error: result.error
3787
+ error: result.error,
3788
+ range
3732
3789
  };
3733
3790
  const { patch, nameStatus } = result.diff;
3734
3791
  return {
3735
3792
  sections: patch.length > 0 ? splitPatchByFile(patch) : [],
3736
3793
  nameStatus,
3737
- error: null
3794
+ error: null,
3795
+ range
3738
3796
  };
3739
3797
  })();
3740
3798
  captures.set(sha, pending);
@@ -3760,6 +3818,7 @@ function createDiffProvider(args) {
3760
3818
  patch: sections ? scopePatchForSpec(sections, scope) : null,
3761
3819
  nameStatus: captured.nameStatus,
3762
3820
  error: captured.error,
3821
+ range: captured.range,
3763
3822
  fileDiff: (path) => sections ? lookupFileDiff(sections, path) : null
3764
3823
  };
3765
3824
  } };
@@ -5831,7 +5890,6 @@ async function runLiveSpecs(specs, opts) {
5831
5890
  const userPromptSuffix = userPromptBundle?.text ?? null;
5832
5891
  const diffProvider = opts.diffProvider ?? null;
5833
5892
  const failureAnalysisEnabled = diffProvider != null;
5834
- const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
5835
5893
  const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
5836
5894
  ok: false,
5837
5895
  reason: "disabled"
@@ -5859,8 +5917,7 @@ async function runLiveSpecs(specs, opts) {
5859
5917
  const row = await buildLiveReportRow(outcome, {
5860
5918
  auth,
5861
5919
  diffProvider,
5862
- reportDir,
5863
- driftAuditEnabled
5920
+ reportDir
5864
5921
  }, opts, cwd);
5865
5922
  await opts.report?.upsert(row);
5866
5923
  return {
@@ -5894,7 +5951,7 @@ async function buildLiveReportRow(r, ctx, opts, cwd) {
5894
5951
  result: r.result,
5895
5952
  reportDir: ctx.reportDir
5896
5953
  });
5897
- 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;
5898
5955
  const analysis = ctx.diffProvider && r.result.status === "failed" ? await analyzeOneLiveFailure(r, ctx.diffProvider, driftForSpec, ctx.auth, opts, cwd) : void 0;
5899
5956
  return {
5900
5957
  ...base,
@@ -6146,6 +6203,8 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
6146
6203
  diffPatch: specDiff.patch,
6147
6204
  changedFiles: specDiff.nameStatus,
6148
6205
  baseRef: specDiff.base.ref,
6206
+ baseSource: specDiff.base.source,
6207
+ range: specDiff.range,
6149
6208
  driftIssues: driftForSpec,
6150
6209
  ...opts.language ? { outputLanguage: opts.language } : {},
6151
6210
  ...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
@@ -9494,7 +9553,6 @@ async function executeRun(targets, opts) {
9494
9553
  ...typeof opts.retry === "number" ? { retry: opts.retry } : {},
9495
9554
  concurrency: opts.concurrency ?? 1,
9496
9555
  ...opts.profile ? { profile: opts.profile } : {},
9497
- ...opts.driftAudit !== false ? { driftAudit: true } : {},
9498
9556
  diffProvider,
9499
9557
  hubContext: hubCtx,
9500
9558
  customPrompt,
@@ -9724,7 +9782,6 @@ function failedSpec(s) {
9724
9782
  */
9725
9783
  async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, customPrompt, triageUserPrompt, diffProvider) {
9726
9784
  const failureAnalysisEnabled = diffProvider != null;
9727
- const driftAuditEnabled = failureAnalysisEnabled && opts.driftAudit !== false;
9728
9785
  const auth = failureAnalysisEnabled ? driftAuthAvailable() : {
9729
9786
  ok: false,
9730
9787
  reason: "skipped by flags"
@@ -9735,7 +9792,7 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
9735
9792
  const specInfoByKey = new Map(tree.flatMap((f) => f.specs.map((sp) => [`${f.featureName}/${sp.specName}`, sp])));
9736
9793
  const findSpecInfo = (s) => specInfoByKey.get(`${s.featureName}/${s.specName}`) ?? null;
9737
9794
  let driftResults = [];
9738
- if (driftAuditEnabled && auth.ok && failed.length > 0) {
9795
+ if (failureAnalysisEnabled && auth.ok && failed.length > 0) {
9739
9796
  const targets = failed.map((s) => {
9740
9797
  const spec = findSpecInfo(s);
9741
9798
  if (!spec) return null;
@@ -9823,6 +9880,8 @@ async function analyzeDeterministicSummaries(summaries, opts, cwd, reportDir, cu
9823
9880
  diffPatch: diffExcerpt,
9824
9881
  changedFiles: specDiffResult.nameStatus,
9825
9882
  baseRef: specDiffResult.base.ref,
9883
+ baseSource: specDiffResult.base.source,
9884
+ range: specDiffResult.range,
9826
9885
  driftIssues,
9827
9886
  ...opts.language ? { outputLanguage: opts.language } : {},
9828
9887
  ...triageUserPrompt ? { triageUserPrompt } : {},
@@ -9888,7 +9947,7 @@ function buildReportEnvelope(args) {
9888
9947
  },
9889
9948
  model: opts.model ?? null,
9890
9949
  language: opts.language ?? null,
9891
- promptVersion: "5",
9950
+ promptVersion: "6",
9892
9951
  customPromptVersion,
9893
9952
  ...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
9894
9953
  };
@@ -10212,7 +10271,7 @@ function installTeardownSignalHandlers(teardown) {
10212
10271
  }
10213
10272
  //#endregion
10214
10273
  //#region src/cli/run.ts
10215
- 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) => {
10216
10275
  if (REPORT_FORMATS.includes(raw)) return raw;
10217
10276
  throw new Error(`--format must be one of ${REPORT_FORMATS.join(" | ")}`);
10218
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) => {
@@ -15041,21 +15100,25 @@ const HTML_BODY = `
15041
15100
  <div class="rd-head" id="rd-head"></div>
15042
15101
 
15043
15102
  <!-- Triage first: grading + learning is the most important action, so it
15044
- 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. -->
15045
15108
  <div class="triage-head" id="triage-head">
15046
15109
  <h3 style="font-size:14px" data-i18n="detail.triage">Triage</h3>
15047
15110
  <span class="triage-summary" id="triage-summary"></span>
15048
15111
  </div>
15049
- <div class="card" id="matrix-card"></div>
15050
- <p class="muted" id="triage-progress" style="font-size:12.5px;margin-top:8px"></p>
15051
-
15052
- <div class="learn-cta" id="learn-cta" hidden>
15053
- <div class="learn-cta-text">
15054
- <div class="t" data-i18n="learn.cta.title">Learn from these grades</div>
15055
- <div class="d" data-i18n="learn.cta.desc">Turn the graded cases into a custom prompt that calibrates future failure classification.</div>
15056
- </div>
15057
- <div class="learn-cta-actions">
15058
- <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>
15059
15122
  </div>
15060
15123
  </div>
15061
15124
 
@@ -15379,8 +15442,6 @@ const CSS = `
15379
15442
  .badge-det { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
15380
15443
  /* which generation target ran the spec (agent-browser / playwright / runn) */
15381
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); }
15382
- .badge.drift-warn { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
15383
- .badge.drift-warn .d { background: var(--amber); }
15384
15445
  .badge-drift { background: var(--violet-bg); color: var(--violet); border-color: var(--violet-border); }
15385
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); }
15386
15447
  /* Below .chip in source order so these override its background/border/color
@@ -15423,11 +15484,18 @@ const CSS = `
15423
15484
  .spec-card-head .spacer { flex: 1; }
15424
15485
  .spec-card-body { padding: 0 20px 16px; }
15425
15486
  /* Tier2 verdict block */
15426
- .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); }
15427
15492
  .analysis-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
15428
- .analysis-headline { font-size: 14px; font-weight: 600; color: var(--fg); line-height: 1.5; }
15429
- .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; }
15430
- .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); }
15431
15499
  .analysis-reasoning { font-size: 13px; color: var(--fg-dim); white-space: pre-wrap; line-height: 1.6; }
15432
15500
  .analysis-inline-reason { font-size: 13px; color: var(--fg-dim); line-height: 1.55; }
15433
15501
  /* Tier3 accordion (real header bar + rotating chevron, replaces the tiny ▸) */
@@ -15493,11 +15561,12 @@ const CSS = `
15493
15561
 
15494
15562
  /* triage grading — an explicit question + a segmented single-select, framed
15495
15563
  as an action ("tell us the real cause"), not a data readout. */
15496
- .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); }
15497
15568
  .grade-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
15498
15569
  .grade-q { font-size: 13px; font-weight: 600; color: var(--fg); }
15499
- .grade-pred { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; font-size: 12px; color: var(--muted); }
15500
- .grade-pred .grade-arrow { color: var(--muted-2); }
15501
15570
  .grade-bottom { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
15502
15571
  .grade-seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--radius-md); overflow: hidden; background: var(--surface); }
15503
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; }
@@ -15635,7 +15704,7 @@ const CSS = `
15635
15704
  .ro-tag { font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
15636
15705
  color: var(--muted-2); background: var(--surface-3); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; margin-left: 4px; }
15637
15706
 
15638
- .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); }
15639
15708
  .learn-cta-text { flex: 1; min-width: 0; }
15640
15709
  .learn-cta-text .t { font-size: 13.5px; font-weight: 600; color: var(--fg); }
15641
15710
  .learn-cta-text .d { font-size: 12px; color: var(--muted); margin-top: 3px; }
@@ -15756,26 +15825,25 @@ const CLIENT_JS = `
15756
15825
  "meta.branch": "Branch", "meta.specs": "Specs", "meta.prompt": "Prompt",
15757
15826
  "meta.created": "Created", "meta.passed": "passed", "meta.profile": "Profile",
15758
15827
  "meta.drift": "Drift",
15759
- "rec.title": "Recommendation",
15828
+ "diag.cause": "Cause", "diag.fix": "Fix",
15760
15829
  "acc.reasoning": "Reasoning", "acc.evidence": "Evidence", "acc.steps": "Live run steps",
15761
15830
  "acc.assertions": "Assertions", "acc.drift": "Drift audit",
15762
15831
  "acc.artifacts": "Artifacts",
15763
15832
  "art.open": "Open", "art.loadFailed": "could not load (it may have been omitted from the push)",
15764
15833
  "acc.assertions.hint": "Test cases from the recorded spec run",
15765
15834
  "spec.kind.live": "Live", "spec.kind.det": "Deterministic",
15766
- "spec.driftWarn": "Spec drift", "det.steps": "Steps",
15835
+ "det.steps": "Steps",
15767
15836
  "kind.run": "Test run", "kind.drift": "Drift audit",
15768
15837
  "drift.summary.issues": "Issues", "drift.summary.errors": "Errors",
15769
15838
  "drift.summary.warnings": "Warnings", "drift.summary.specsWithIssues": "Specs with issues",
15770
15839
  "drift.clean": "No drift issues",
15771
- "grade.question": "What was the real cause?", "grade.predicted": "predicted",
15840
+ "grade.question": "What was the real cause?",
15772
15841
  "grade.ungraded": "ungraded", "grade.matches": "saved · matches",
15773
15842
  "grade.corrected": "saved · corrected", "grade.saving": "saving…",
15774
15843
  "grade.error": "couldn't save — retry",
15775
- "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.",
15776
15845
  "matrix.predicted": "predicted \\\\ actual", "matrix.accuracy": "Accuracy",
15777
15846
  "matrix.accSuffix": "of graded cases match the prediction", "matrix.graded": "graded",
15778
- "matrix.progress": "Recorded actual cause: {n} / {total} failing specs",
15779
15847
  "learn.cta.title": "Learn from these grades",
15780
15848
  "learn.cta.desc": "Learn from what you graded so ccqa classifies failure causes the same way next time.",
15781
15849
  "learn.cta.run": "Learn",
@@ -15846,26 +15914,25 @@ const CLIENT_JS = `
15846
15914
  "meta.branch": "ブランチ", "meta.specs": "スペック", "meta.prompt": "プロンプト",
15847
15915
  "meta.created": "作成", "meta.passed": "合格", "meta.profile": "プロファイル",
15848
15916
  "meta.drift": "ドリフト",
15849
- "rec.title": "推奨対応",
15917
+ "diag.cause": "原因", "diag.fix": "対処",
15850
15918
  "acc.reasoning": "推論", "acc.evidence": "根拠", "acc.steps": "実行ステップ",
15851
15919
  "acc.assertions": "アサーション", "acc.drift": "ドリフト監査",
15852
15920
  "acc.artifacts": "成果物",
15853
15921
  "art.open": "開く", "art.loadFailed": "読み込めませんでした(push時に省略された可能性があります)",
15854
15922
  "acc.assertions.hint": "記録したスペック実行のテストケース",
15855
15923
  "spec.kind.live": "ライブ", "spec.kind.det": "決定的",
15856
- "spec.driftWarn": "仕様ドリフト", "det.steps": "ステップ",
15924
+ "det.steps": "ステップ",
15857
15925
  "kind.run": "テスト実行", "kind.drift": "ドリフト監査",
15858
15926
  "drift.summary.issues": "問題数", "drift.summary.errors": "エラー",
15859
15927
  "drift.summary.warnings": "警告", "drift.summary.specsWithIssues": "問題のあるスペック",
15860
15928
  "drift.clean": "ドリフトの問題なし",
15861
- "grade.question": "実際の原因は何でしたか?", "grade.predicted": "予測",
15929
+ "grade.question": "実際の原因は何でしたか?",
15862
15930
  "grade.ungraded": "未評価", "grade.matches": "保存済み · 一致",
15863
15931
  "grade.corrected": "保存済み · 修正", "grade.saving": "保存中…",
15864
15932
  "grade.error": "保存に失敗 — 再試行",
15865
- "matrix.empty": "まだ評価がありません。上の失敗スペックを評価すると混同行列に反映されます。",
15933
+ "matrix.empty": "まだ採点がありません。下の失敗スペックの診断カードで実際の原因を選ぶと、ここに集計されます。",
15866
15934
  "matrix.predicted": "予測 \\\\ 実際", "matrix.accuracy": "正解率",
15867
15935
  "matrix.accSuffix": "件の採点が予測と一致", "matrix.graded": "採点済み",
15868
- "matrix.progress": "実際の原因を記録: {total} 件中 {n} 件の失敗スペック",
15869
15936
  "learn.cta.title": "この採点から学習",
15870
15937
  "learn.cta.desc": "採点した内容をもとに、ccqaが次回から同じように失敗の原因を分類できるよう学習します。",
15871
15938
  "learn.cta.run": "学習",
@@ -16465,27 +16532,61 @@ const CLIENT_JS = `
16465
16532
 
16466
16533
  // ── run detail: spec cards ──────────────────────────────────────────
16467
16534
 
16468
- // Tier2 verdict block: label + confidence, headline, and the recommendation
16469
- // callout. Reasoning is NOT here renderSpecCard places it as a Tier3
16470
- // 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.
16471
16541
  function analysisSection(runId, r) {
16472
16542
  var wrap = el("div", "analysis-box");
16473
16543
  var a = r.analysis;
16474
16544
  var head = el("div", "analysis-head");
16475
16545
  head.appendChild(labelChip(a.label));
16476
16546
  head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
16477
- if (a.subDiagnosis && a.subDiagnosis !== "NONE") head.appendChild(el("span", "muted", a.subDiagnosis));
16478
16547
  wrap.appendChild(head);
16479
- 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
+ }
16480
16553
  if (a.recommendation) {
16481
- var rec = el("div", "analysis-rec");
16482
- rec.appendChild(el("span", "rec-k", t("rec.title")));
16483
- rec.appendChild(document.createTextNode(a.recommendation));
16484
- wrap.appendChild(rec);
16554
+ kv.appendChild(el("div", "k", t("diag.fix")));
16555
+ kv.appendChild(el("div", "v", a.recommendation));
16485
16556
  }
16557
+ if (kv.childNodes.length > 0) wrap.appendChild(kv);
16486
16558
  return wrap;
16487
16559
  }
16488
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
+
16489
16590
  function evidenceSection(runId, evidence) {
16490
16591
  var grid = el("div", "evidence-grid");
16491
16592
  evidence.forEach(function (e) {
@@ -16717,25 +16818,22 @@ const CLIENT_JS = `
16717
16818
  return det;
16718
16819
  }
16719
16820
 
16720
- // The grading action: an explicit question ("What was the real cause?") with
16721
- // the model's guess as muted context, a segmented single-select over the
16722
- // failure labels, and a status chip (ungraded / saved·matches / saved·
16723
- // corrected). One tap grades it. Optimistic PUT with rollback; on success it
16724
- // refreshes the confusion matrix. The English label value is what's sent and
16725
- // 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.
16726
16827
  function triageGradeControl(runId, r, triageState) {
16727
16828
  var key = r.feature + "/" + r.spec;
16728
16829
  var predicted = r.analysis ? r.analysis.label : "UNKNOWN";
16729
16830
 
16730
16831
  var wrap = el("div", "grade");
16731
16832
 
16833
+ // No "predicted →" chip here: the control lives inside the diagnosis
16834
+ // card, directly under the prediction it grades — repeating it is noise.
16732
16835
  var top = el("div", "grade-top");
16733
16836
  top.appendChild(el("span", "grade-q", t("grade.question")));
16734
- var pred = el("span", "grade-pred");
16735
- pred.appendChild(document.createTextNode(t("grade.predicted")));
16736
- pred.appendChild(el("span", "grade-arrow", "→"));
16737
- pred.appendChild(labelChip(predicted));
16738
- top.appendChild(pred);
16739
16837
  wrap.appendChild(top);
16740
16838
 
16741
16839
  var bottom = el("div", "grade-bottom");
@@ -16819,13 +16917,6 @@ const CLIENT_JS = `
16819
16917
  else if (r.liveRun) head.appendChild(el("span", "badge-live", t("spec.kind.live")));
16820
16918
  else if (!external) head.appendChild(el("span", "badge-det", t("spec.kind.det")));
16821
16919
  head.appendChild(statusBadge(r.status));
16822
- var hasDriftError = r.driftIssues && r.driftIssues.some(function (d) { return d.severity === "ERROR"; });
16823
- if (hasDriftError) {
16824
- var w = el("span", "badge drift-warn");
16825
- w.appendChild(el("span", "d"));
16826
- w.appendChild(document.createTextNode(" " + t("spec.driftWarn")));
16827
- head.appendChild(w);
16828
- }
16829
16920
  card.appendChild(head);
16830
16921
 
16831
16922
  var body = el("div", "spec-card-body");
@@ -16836,17 +16927,23 @@ const CLIENT_JS = `
16836
16927
  any = true;
16837
16928
  }
16838
16929
 
16839
- if (r.status === "failed" && r.analysis) {
16840
- // Tier2: the verdict block (analysis) + the grading action, always shown.
16841
- body.appendChild(analysisSection(runId, r));
16842
- body.appendChild(triageGradeControl(runId, r, triageState));
16843
- // 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
16844
16939
  // content. A one-char/empty reasoning behind a disclosure reads as broken
16845
16940
  // (the old "▸ r"), so drop it entirely below the threshold.
16846
16941
  var reasoning = r.analysis.reasoning ? String(r.analysis.reasoning).trim() : "";
16847
16942
  if (reasoning.length > 2) {
16848
- 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)));
16849
16944
  }
16945
+ box.appendChild(triageGradeControl(runId, r, triageState));
16946
+ body.appendChild(box);
16850
16947
  any = true;
16851
16948
  } else if (r.status === "failed" && r.analysisSkipped) {
16852
16949
  body.appendChild(el("div", "muted", "Analysis skipped: " + r.analysisSkipped));
@@ -16878,7 +16975,10 @@ const CLIENT_JS = `
16878
16975
  any = true;
16879
16976
  }
16880
16977
 
16881
- 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) {
16882
16982
  body.appendChild(detailsBlock(t("acc.drift"), r.driftIssues.length, driftSection(r.driftIssues)));
16883
16983
  any = true;
16884
16984
  }
@@ -16913,12 +17013,9 @@ const CLIENT_JS = `
16913
17013
  var cases = Object.keys(triageState.byKey).map(function (k) { return triageState.byKey[k]; })
16914
17014
  .filter(function (c) { return c.predicted && c.actual; });
16915
17015
 
16916
- // Recompute the progress line here so it stays in sync after each grade,
16917
- // 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.
16918
17018
  var total = typeof triageState.total === "number" ? triageState.total : Object.keys(triageState.byKey).length;
16919
- document.getElementById("triage-progress").textContent =
16920
- t("matrix.progress").replace("{n}", cases.length).replace("{total}", total);
16921
-
16922
17019
  var summary = document.getElementById("triage-summary");
16923
17020
 
16924
17021
  if (cases.length === 0) {
@@ -16989,7 +17086,12 @@ const CLIENT_JS = `
16989
17086
  renderMatrix(triageState);
16990
17087
  onLoaded(triageState);
16991
17088
  }).catch(function (err) {
16992
- 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);
16993
17095
  onLoaded({ byKey: {} });
16994
17096
  });
16995
17097
  }
@@ -17015,7 +17117,6 @@ const CLIENT_JS = `
17015
17117
  clear(document.getElementById("rd-head"));
17016
17118
  clear(document.getElementById("matrix-card"));
17017
17119
  document.getElementById("detail-spec-count").textContent = "";
17018
- document.getElementById("triage-progress").textContent = "";
17019
17120
  document.getElementById("triage-summary").textContent = "";
17020
17121
 
17021
17122
  apiFetch("/api/v1/runs/" + encodeURIComponent(runId)).then(function (run) {
@@ -17029,8 +17130,7 @@ const CLIENT_JS = `
17029
17130
  // as the other, and neither escapes its own catch.
17030
17131
  var isDrift = report.kind === "drift";
17031
17132
  document.getElementById("triage-head").hidden = isDrift;
17032
- document.getElementById("matrix-card").hidden = isDrift;
17033
- document.getElementById("triage-progress").hidden = isDrift;
17133
+ document.getElementById("triage-card").hidden = isDrift;
17034
17134
  renderSpecCards(runId, report.results, { byKey: {} }, isDrift);
17035
17135
  if (isDrift) return; // drift runs have no triage: skip loadTriage entirely
17036
17136
  loadTriage(runId, function (triageState) {
@@ -18409,7 +18509,7 @@ function createLearningWorker(deps) {
18409
18509
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
18410
18510
  const customPrompt = {
18411
18511
  schemaVersion: 1,
18412
- basePromptVersion: "5",
18512
+ basePromptVersion: "6",
18413
18513
  customPromptVersion: `${generatedAt}-c${cases.length}`,
18414
18514
  generatedAt,
18415
18515
  guidance
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.4.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.4.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": {