ccqa 1.8.1 → 1.8.2

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
@@ -3248,7 +3248,7 @@ function hashTriageUserPrompt(text) {
3248
3248
  */
3249
3249
  const CHANGED_FILE_DIFF_TOOL = "mcp__diff__changed_file_diff";
3250
3250
  function buildFailureAnalysisPrompt(input) {
3251
- const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, artifactsDir = null, outputLanguage = "auto", triageUserPrompt, customPrompt } = input;
3251
+ const { script, specYaml, failureLog, liveTranscriptExcerpt, diffPatch, changedFiles, baseRef, baseSource = null, range = null, driftIssues, artifactsDir = null, outputLanguage = "auto", triageUserPrompt, customPrompt, baselineMissing = null } = input;
3252
3252
  const lastGreen = baseSource === "last-green";
3253
3253
  const triageUserPromptBlock = buildTriageUserPromptBlock(triageUserPrompt);
3254
3254
  const customPromptBlock = buildCustomPromptBlock(customPrompt);
@@ -3257,7 +3257,13 @@ function buildFailureAnalysisPrompt(input) {
3257
3257
  const baseLabel = lastGreen ? `this spec's last passing commit${baseRef && baseRef !== "last-green" ? ` (${baseRef})` : ""}` : baseRef ?? "base";
3258
3258
  const rangeNote = range ? ` — spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}` : "";
3259
3259
  let diffBlock;
3260
- if (diffPatch === null) diffBlock = `## Source changes
3260
+ if (baselineMissing) diffBlock = `## Source changes
3261
+
3262
+ No baseline exists for this spec (${baselineMissing}), so there is no source diff. Work from the current repository state instead:
3263
+ - Grep for the exact selector / text / aria-label the failing step targets. Absent or renamed while the user-visible flow the spec describes still exists → the test is stale. The flow itself no longer implemented → the spec is stale.
3264
+ - Without a change window you cannot attribute the failure to a specific change — do not claim a change "introduced" it. State what the current source shows.
3265
+ `;
3266
+ else if (diffPatch === null) diffBlock = `## Source changes
3261
3267
 
3262
3268
  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.
3263
3269
  `;
@@ -3284,14 +3290,14 @@ A separate read-only audit compared the spec against the current source. Treat t
3284
3290
 
3285
3291
  ${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
3286
3292
  ` : "";
3287
- 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.
3293
+ return `${baselineMissing ? `You are analyzing a failing E2E regression test. No known-good baseline exists for this spec yet, so there is no source diff: your primary context is the failure evidence plus the CURRENT state of the repository, which you can inspect with the read-only tools. Your job is a root-cause CALL, not a fix: decide which of three categories explains the failure.` : `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.`}
3288
3294
 
3289
3295
  ${languageBlock}## The three categories
3290
3296
 
3291
3297
  The question that separates them: **is the behavior the spec describes still what the product intends?**
3292
3298
 
3293
3299
  1. TEST_DRIFT — what the spec verifies is unchanged; only the test code drifted from the source. Typical: a selector/aria-label/placeholder rename, a timing change, an over-tight assertion. The diff shows a change that is invisible to the user's intent but visible to the test.
3294
- 2. SPEC_CHANGE — the thing being verified itself changed: the UI flow, the layout, the feature's intended behavior. The diff deliberately changes what the spec asserts. You MUST cite the diff hunk (file + what changed) as evidence for this label.
3300
+ 2. SPEC_CHANGE — the thing being verified itself changed: the UI flow, the layout, the feature's intended behavior. ${baselineMissing ? "The current source deliberately implements something other than what the spec asserts. You MUST cite the source file you read as evidence for this label." : "The diff deliberately changes what the spec asserts. You MUST cite the diff hunk (file + what changed) as evidence for this label."}
3295
3301
  3. PRODUCT_BUG — neither of the above: the failure is not explained by the diff nor by test staleness. The product regressed.
3296
3302
 
3297
3303
  If the evidence is too weak to choose, answer UNKNOWN — a wrong confident call is worse than an honest UNKNOWN, because humans grade these predictions to measure accuracy.
@@ -3303,19 +3309,24 @@ You can call \`Grep\`, \`Glob\`, and \`Read\` against the current repository (po
3303
3309
  - read the changed files in full when the truncated patch is not enough,
3304
3310
  - check whether the element/flow the spec describes still exists in the source.
3305
3311
 
3306
- You can also call \`${CHANGED_FILE_DIFF_TOOL}\` with a file path to fetch that file's diff hunk for this run's base...HEAD range. The inline patch below is scoped to this spec's relatedPaths — files OUTSIDE that scope still appear in "Changed files (name-status)" but their hunks are not inlined. Before blaming (or ruling out) such a file, fetch its diff with this tool; Read only shows you its post-change state, not what changed.
3312
+ ${baselineMissing ? `There is no diff range for this run, so the \`${CHANGED_FILE_DIFF_TOOL}\` tool has nothing to return — every conclusion must come from the current source state plus the failure evidence.` : `You can also call \`${CHANGED_FILE_DIFF_TOOL}\` with a file path to fetch that file's diff hunk for this run's base...HEAD range. The inline patch below is scoped to this spec's relatedPaths — files OUTSIDE that scope still appear in "Changed files (name-status)" but their hunks are not inlined. Before blaming (or ruling out) such a file, fetch its diff with this tool; Read only shows you its post-change state, not what changed.`}
3307
3313
  ${artifactsDir ? `\nThe test runner wrote this run's artifacts under \`${artifactsDir}\` (relative to the working directory). Read them for failure context the log tail above may not carry — e.g. a Playwright \`error-context.md\` holds the page's accessibility snapshot at the moment of failure, which often shows directly whether the awaited element was present. Do NOT open image/trace binaries.\n` : ""}
3308
3314
  You have **up to 12 tool turns**. Do NOT write, edit, run shell commands, or hit the network.
3309
3315
 
3310
3316
  ## Decision guidance
3311
3317
 
3312
- ${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.`}
3318
+ ${baselineMissing ? `There is no baseline, so there is no "what changed" evidence at all. Classify from the failure signature checked against the current source:
3319
+
3320
+ - The selector / text / attribute the failing step targets is absent or renamed in the current source, while the user-visible flow the spec describes still exists → TEST_DRIFT (cite the file:line where the renamed/replacement element lives).
3321
+ - The flow or feature the spec describes is no longer implemented — page gone, component removed, copy redefined, feature reworked → SPEC_CHANGE (cite the file you read that shows the new shape).
3322
+ - The flow exists and the test's selectors still match the source, but the observed behavior is wrong (error response, missing side effect, wrong data) → lean PRODUCT_BUG; FIRST rule out environment/data/timing causes (daemon not running, network down, missing credentials, stale test data) — those read as UNKNOWN with low confidence, not PRODUCT_BUG.
3323
+ - Without diff evidence, treat 0.7 as a practical confidence ceiling unless the current source alone is conclusive (e.g. the targeted selector is verifiably gone).` : `${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.`}
3313
3324
 
3314
3325
  - Diff touches only attributes/identifiers the test selects on (labels, testids, class names, timing) while the user-visible flow is intact → TEST_DRIFT.
3315
3326
  - Diff intentionally removes/reworks the UI or flow that a spec step verifies (component deleted, page restructured, copy redefined, feature flag flipped) → SPEC_CHANGE.
3316
3327
  - 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?
3317
3328
  ${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 ? `
3318
- - 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.` : ""}
3329
+ - 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.` : ""}`}
3319
3330
  - The drift audit findings (when present) flag spec↔code mismatches; an ERROR there usually supports TEST_DRIFT or SPEC_CHANGE over PRODUCT_BUG.
3320
3331
 
3321
3332
  ## Sub-diagnosis vocabulary
@@ -3355,7 +3366,7 @@ Your **final** assistant message must start with \`{\` and end with \`}\` — a
3355
3366
  - 0.4-0.7: plausible but another category could explain it
3356
3367
  - < 0.4: answer UNKNOWN instead of guessing
3357
3368
 
3358
- 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"}.
3369
+ Evidence rules: TEST_DRIFT and SPEC_CHANGE require at least one concrete \`file\` reference (diff hunk or file:line you actually read). ${baselineMissing ? "With no baseline there is no in-range change to cite: PRODUCT_BUG must instead explain why current-state inspection rules out test staleness and spec change." : `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"}.`}
3359
3370
 
3360
3371
  ## Test Spec (spec.yaml)
3361
3372
  ${specYaml}
@@ -5767,10 +5778,6 @@ function createFailureAnalysisPass(deps) {
5767
5778
  ...fields,
5768
5779
  analysisSkipped: ANALYSIS_DISABLED
5769
5780
  };
5770
- if (!specDiffResult.ok) return {
5771
- ...fields,
5772
- analysisSkipped: specDiffResult.skip
5773
- };
5774
5781
  if (!deps.auth.ok) return {
5775
5782
  ...fields,
5776
5783
  analysisSkipped: deps.auth.reason
@@ -5779,16 +5786,18 @@ function createFailureAnalysisPass(deps) {
5779
5786
  ...fields,
5780
5787
  analysisSkipped: "no spec.yaml found for this spec"
5781
5788
  };
5782
- info(`failure analysis: ${featureName}/${specName}`);
5789
+ const baselineMissing = specDiffResult.ok ? null : specDiffResult.skip;
5790
+ info(`failure analysis: ${featureName}/${specName}${baselineMissing ? " (no baseline — classifying from current source)" : ""}`);
5783
5791
  const outcome = await analyzeFailure({
5784
5792
  script: await input.readScript(),
5785
5793
  specYaml: input.specYaml,
5786
5794
  failureLog: input.failureLog,
5787
- diffPatch: specDiffResult.patch,
5788
- changedFiles: specDiffResult.nameStatus,
5789
- baseRef: specDiffResult.base.ref,
5790
- baseSource: specDiffResult.base.source,
5791
- range: specDiffResult.range,
5795
+ diffPatch: specDiff?.patch ?? null,
5796
+ changedFiles: specDiff?.nameStatus ?? null,
5797
+ baseRef: specDiff?.base.ref ?? null,
5798
+ baseSource: specDiff?.base.source ?? null,
5799
+ range: specDiff?.range ?? null,
5800
+ ...baselineMissing ? { baselineMissing } : {},
5792
5801
  driftIssues: input.driftIssues,
5793
5802
  ...input.artifactsDir ? { artifactsDir: input.artifactsDir } : {},
5794
5803
  ...deps.language ? { outputLanguage: deps.language } : {},
@@ -5797,7 +5806,7 @@ function createFailureAnalysisPass(deps) {
5797
5806
  }, {
5798
5807
  ...deps.model ? { model: deps.model } : {},
5799
5808
  cwd: deps.cwd,
5800
- getFileDiff: specDiffResult.fileDiff
5809
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
5801
5810
  });
5802
5811
  if (!printedHeader) {
5803
5812
  printedHeader = true;
@@ -8244,26 +8253,24 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8244
8253
  failureLogExcerpt: null,
8245
8254
  diffExcerpt: null
8246
8255
  };
8247
- const specDiff = await diffProvider.forSpec({
8256
+ const specDiffResult = await diffProvider.forSpec({
8248
8257
  featureName: r.featureName,
8249
8258
  specName: r.specName
8250
8259
  });
8251
- if (!specDiff.ok) return {
8252
- analysis: null,
8253
- analysisSkipped: specDiff.skip,
8254
- failureLogExcerpt: excerpt,
8255
- diffExcerpt: null
8256
- };
8257
- if (specDiff.error) info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
8260
+ const specDiff = specDiffResult.ok ? specDiffResult : null;
8261
+ const baselineMissing = specDiffResult.ok ? null : specDiffResult.skip;
8262
+ if (baselineMissing) info(`failure analysis: no baseline (${baselineMissing}) — classifying from current source`);
8263
+ else if (specDiff?.error) info(`failure analysis: source diff unavailable (${specDiff.error}) — analyzing without diff context`);
8258
8264
  const customPrompt = resolveCustomPromptForTarget(opts.customPrompt, AGENT_BROWSER_TARGET);
8259
8265
  const outcome = await analyzeFailure({
8260
8266
  liveTranscriptExcerpt: excerpt,
8261
8267
  specYaml: r.specYaml,
8262
- diffPatch: specDiff.patch,
8263
- changedFiles: specDiff.nameStatus,
8264
- baseRef: specDiff.base.ref,
8265
- baseSource: specDiff.base.source,
8266
- range: specDiff.range,
8268
+ diffPatch: specDiff?.patch ?? null,
8269
+ changedFiles: specDiff?.nameStatus ?? null,
8270
+ baseRef: specDiff?.base.ref ?? null,
8271
+ baseSource: specDiff?.base.source ?? null,
8272
+ range: specDiff?.range ?? null,
8273
+ ...baselineMissing ? { baselineMissing } : {},
8267
8274
  driftIssues: driftForSpec,
8268
8275
  ...opts.language ? { outputLanguage: opts.language } : {},
8269
8276
  ...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
@@ -8271,7 +8278,7 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8271
8278
  }, {
8272
8279
  ...opts.model ? { model: opts.model } : {},
8273
8280
  cwd,
8274
- getFileDiff: specDiff.fileDiff
8281
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
8275
8282
  });
8276
8283
  const pct = Math.round(outcome.analysis.confidence * 100);
8277
8284
  const headline = outcome.analysis.headline.trim() || (outcome.analysis.reasoning.split("\n")[0] ?? "").trim();
@@ -8280,11 +8287,11 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8280
8287
  analysis: outcome.analysis,
8281
8288
  analysisSkipped: null,
8282
8289
  failureLogExcerpt: excerpt,
8283
- diffExcerpt: specDiff.patch,
8284
- analysisBase: {
8290
+ diffExcerpt: specDiff?.patch ?? null,
8291
+ ...specDiff ? { analysisBase: {
8285
8292
  ref: specDiff.base.ref,
8286
8293
  sha: specDiff.base.sha
8287
- },
8294
+ } } : {},
8288
8295
  ...customPrompt ? { customPromptVersion: customPrompt.customPromptVersion } : {}
8289
8296
  };
8290
8297
  }
@@ -11069,7 +11076,7 @@ function buildReportEnvelope(args) {
11069
11076
  },
11070
11077
  model: opts.model ?? null,
11071
11078
  language: opts.language ?? null,
11072
- promptVersion: "7",
11079
+ promptVersion: "8",
11073
11080
  customPromptVersion,
11074
11081
  ...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
11075
11082
  };
@@ -19472,7 +19479,7 @@ function createLearningWorker(deps) {
19472
19479
  const prevCustomPrompt = await loadStoredCustomPrompt(storage, job.project);
19473
19480
  const customPrompt = {
19474
19481
  schemaVersion: 1,
19475
- basePromptVersion: "7",
19482
+ basePromptVersion: "8",
19476
19483
  customPromptVersion: `${generatedAt}-c${fallbackCases.length}`,
19477
19484
  generatedAt,
19478
19485
  guidance: fallbackGuidance ?? "",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
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.8.1",
3
+ "version": "1.8.2",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {