ccqa 1.8.0 → 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
@@ -789,10 +789,26 @@ async function runPool(items, concurrency, fn) {
789
789
  //#endregion
790
790
  //#region src/drift/auth.ts
791
791
  /**
792
+ * Claude Code can also run against AWS Bedrock / Google Vertex AI, selected by
793
+ * these env toggles. Credentials then come from the cloud SDK's own chain
794
+ * (instance/task roles, gcloud auth, …), so none of the Anthropic-side probes
795
+ * below apply — a set toggle counts as auth being available. ccqa forwards the
796
+ * toggle verbatim; only "0"/"false" (any case) is treated as explicitly off.
797
+ */
798
+ const CLOUD_PROVIDER_ENV_KEYS = ["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX"];
799
+ function cloudProviderEnabled() {
800
+ return CLOUD_PROVIDER_ENV_KEYS.some((key) => {
801
+ const value = process.env[key]?.trim().toLowerCase();
802
+ return value !== void 0 && value !== "" && value !== "0" && value !== "false";
803
+ });
804
+ }
805
+ /**
792
806
  * Probe whether the host has any credential the Anthropic SDK can pick up:
793
807
  * 1. ANTHROPIC_API_KEY env var (CI / scripted use)
794
- * 2. ~/.claude/.credentials.json (Claude Code login, file-based platforms)
795
- * 3. macOS Keychain item "Claude Code-credentials" (Claude Code login on
808
+ * 2. CLAUDE_CODE_USE_BEDROCK / CLAUDE_CODE_USE_VERTEX (cloud-provider
809
+ * endpoints authenticated by the cloud SDK's credential chain)
810
+ * 3. ~/.claude/.credentials.json (Claude Code login, file-based platforms)
811
+ * 4. macOS Keychain item "Claude Code-credentials" (Claude Code login on
796
812
  * darwin stores the OAuth credentials in the Keychain, not on disk)
797
813
  *
798
814
  * Claude-driven hooks are opt-in, so the caller only consults this after the
@@ -802,11 +818,12 @@ async function runPool(items, concurrency, fn) {
802
818
  function driftAuthAvailable() {
803
819
  const key = process.env["ANTHROPIC_API_KEY"];
804
820
  if (typeof key === "string" && key.length > 0) return { ok: true };
821
+ if (cloudProviderEnabled()) return { ok: true };
805
822
  if (existsSync(join(homedir(), ".claude", ".credentials.json"))) return { ok: true };
806
823
  if (process.platform === "darwin" && keychainHasClaudeCredentials()) return { ok: true };
807
824
  return {
808
825
  ok: false,
809
- reason: "no ANTHROPIC_API_KEY / claude login"
826
+ reason: "no ANTHROPIC_API_KEY / Bedrock or Vertex env / claude login"
810
827
  };
811
828
  }
812
829
  /**
@@ -3231,7 +3248,7 @@ function hashTriageUserPrompt(text) {
3231
3248
  */
3232
3249
  const CHANGED_FILE_DIFF_TOOL = "mcp__diff__changed_file_diff";
3233
3250
  function buildFailureAnalysisPrompt(input) {
3234
- 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;
3235
3252
  const lastGreen = baseSource === "last-green";
3236
3253
  const triageUserPromptBlock = buildTriageUserPromptBlock(triageUserPrompt);
3237
3254
  const customPromptBlock = buildCustomPromptBlock(customPrompt);
@@ -3240,7 +3257,13 @@ function buildFailureAnalysisPrompt(input) {
3240
3257
  const baseLabel = lastGreen ? `this spec's last passing commit${baseRef && baseRef !== "last-green" ? ` (${baseRef})` : ""}` : baseRef ?? "base";
3241
3258
  const rangeNote = range ? ` — spans ${range.commitCount} commit${range.commitCount === 1 ? "" : "s"} over ${range.days} day${range.days === 1 ? "" : "s"}` : "";
3242
3259
  let diffBlock;
3243
- 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
3244
3267
 
3245
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.
3246
3269
  `;
@@ -3267,14 +3290,14 @@ A separate read-only audit compared the spec against the current source. Treat t
3267
3290
 
3268
3291
  ${driftIssues.map((i) => `- [${i.severity}] (${DRAFT_CATEGORY_LABEL[i.category]}${i.stepId ? `, step ${i.stepId}` : ""}) ${i.message}${i.detail ? ` — ${i.detail}` : ""}`).join("\n")}
3269
3292
  ` : "";
3270
- 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.`}
3271
3294
 
3272
3295
  ${languageBlock}## The three categories
3273
3296
 
3274
3297
  The question that separates them: **is the behavior the spec describes still what the product intends?**
3275
3298
 
3276
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.
3277
- 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."}
3278
3301
  3. PRODUCT_BUG — neither of the above: the failure is not explained by the diff nor by test staleness. The product regressed.
3279
3302
 
3280
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.
@@ -3286,19 +3309,24 @@ You can call \`Grep\`, \`Glob\`, and \`Read\` against the current repository (po
3286
3309
  - read the changed files in full when the truncated patch is not enough,
3287
3310
  - check whether the element/flow the spec describes still exists in the source.
3288
3311
 
3289
- 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.`}
3290
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` : ""}
3291
3314
  You have **up to 12 tool turns**. Do NOT write, edit, run shell commands, or hit the network.
3292
3315
 
3293
3316
  ## Decision guidance
3294
3317
 
3295
- ${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.`}
3296
3324
 
3297
3325
  - Diff touches only attributes/identifiers the test selects on (labels, testids, class names, timing) while the user-visible flow is intact → TEST_DRIFT.
3298
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.
3299
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?
3300
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 ? `
3301
- - 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.` : ""}`}
3302
3330
  - The drift audit findings (when present) flag spec↔code mismatches; an ERROR there usually supports TEST_DRIFT or SPEC_CHANGE over PRODUCT_BUG.
3303
3331
 
3304
3332
  ## Sub-diagnosis vocabulary
@@ -3338,7 +3366,7 @@ Your **final** assistant message must start with \`{\` and end with \`}\` — a
3338
3366
  - 0.4-0.7: plausible but another category could explain it
3339
3367
  - < 0.4: answer UNKNOWN instead of guessing
3340
3368
 
3341
- 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"}.`}
3342
3370
 
3343
3371
  ## Test Spec (spec.yaml)
3344
3372
  ${specYaml}
@@ -5750,10 +5778,6 @@ function createFailureAnalysisPass(deps) {
5750
5778
  ...fields,
5751
5779
  analysisSkipped: ANALYSIS_DISABLED
5752
5780
  };
5753
- if (!specDiffResult.ok) return {
5754
- ...fields,
5755
- analysisSkipped: specDiffResult.skip
5756
- };
5757
5781
  if (!deps.auth.ok) return {
5758
5782
  ...fields,
5759
5783
  analysisSkipped: deps.auth.reason
@@ -5762,16 +5786,18 @@ function createFailureAnalysisPass(deps) {
5762
5786
  ...fields,
5763
5787
  analysisSkipped: "no spec.yaml found for this spec"
5764
5788
  };
5765
- 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)" : ""}`);
5766
5791
  const outcome = await analyzeFailure({
5767
5792
  script: await input.readScript(),
5768
5793
  specYaml: input.specYaml,
5769
5794
  failureLog: input.failureLog,
5770
- diffPatch: specDiffResult.patch,
5771
- changedFiles: specDiffResult.nameStatus,
5772
- baseRef: specDiffResult.base.ref,
5773
- baseSource: specDiffResult.base.source,
5774
- 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 } : {},
5775
5801
  driftIssues: input.driftIssues,
5776
5802
  ...input.artifactsDir ? { artifactsDir: input.artifactsDir } : {},
5777
5803
  ...deps.language ? { outputLanguage: deps.language } : {},
@@ -5780,7 +5806,7 @@ function createFailureAnalysisPass(deps) {
5780
5806
  }, {
5781
5807
  ...deps.model ? { model: deps.model } : {},
5782
5808
  cwd: deps.cwd,
5783
- getFileDiff: specDiffResult.fileDiff
5809
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
5784
5810
  });
5785
5811
  if (!printedHeader) {
5786
5812
  printedHeader = true;
@@ -8227,26 +8253,24 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8227
8253
  failureLogExcerpt: null,
8228
8254
  diffExcerpt: null
8229
8255
  };
8230
- const specDiff = await diffProvider.forSpec({
8256
+ const specDiffResult = await diffProvider.forSpec({
8231
8257
  featureName: r.featureName,
8232
8258
  specName: r.specName
8233
8259
  });
8234
- if (!specDiff.ok) return {
8235
- analysis: null,
8236
- analysisSkipped: specDiff.skip,
8237
- failureLogExcerpt: excerpt,
8238
- diffExcerpt: null
8239
- };
8240
- 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`);
8241
8264
  const customPrompt = resolveCustomPromptForTarget(opts.customPrompt, AGENT_BROWSER_TARGET);
8242
8265
  const outcome = await analyzeFailure({
8243
8266
  liveTranscriptExcerpt: excerpt,
8244
8267
  specYaml: r.specYaml,
8245
- diffPatch: specDiff.patch,
8246
- changedFiles: specDiff.nameStatus,
8247
- baseRef: specDiff.base.ref,
8248
- baseSource: specDiff.base.source,
8249
- 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 } : {},
8250
8274
  driftIssues: driftForSpec,
8251
8275
  ...opts.language ? { outputLanguage: opts.language } : {},
8252
8276
  ...opts.triageUserPrompt ? { triageUserPrompt: opts.triageUserPrompt } : {},
@@ -8254,7 +8278,7 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8254
8278
  }, {
8255
8279
  ...opts.model ? { model: opts.model } : {},
8256
8280
  cwd,
8257
- getFileDiff: specDiff.fileDiff
8281
+ getFileDiff: specDiff?.fileDiff ?? (() => null)
8258
8282
  });
8259
8283
  const pct = Math.round(outcome.analysis.confidence * 100);
8260
8284
  const headline = outcome.analysis.headline.trim() || (outcome.analysis.reasoning.split("\n")[0] ?? "").trim();
@@ -8263,11 +8287,11 @@ async function analyzeOneLiveFailure(r, diffProvider, driftForSpec, auth, opts,
8263
8287
  analysis: outcome.analysis,
8264
8288
  analysisSkipped: null,
8265
8289
  failureLogExcerpt: excerpt,
8266
- diffExcerpt: specDiff.patch,
8267
- analysisBase: {
8290
+ diffExcerpt: specDiff?.patch ?? null,
8291
+ ...specDiff ? { analysisBase: {
8268
8292
  ref: specDiff.base.ref,
8269
8293
  sha: specDiff.base.sha
8270
- },
8294
+ } } : {},
8271
8295
  ...customPrompt ? { customPromptVersion: customPrompt.customPromptVersion } : {}
8272
8296
  };
8273
8297
  }
@@ -11052,7 +11076,7 @@ function buildReportEnvelope(args) {
11052
11076
  },
11053
11077
  model: opts.model ?? null,
11054
11078
  language: opts.language ?? null,
11055
- promptVersion: "7",
11079
+ promptVersion: "8",
11056
11080
  customPromptVersion,
11057
11081
  ...triageUserPromptHash !== null ? { triageUserPromptHash } : {}
11058
11082
  };
@@ -19455,7 +19479,7 @@ function createLearningWorker(deps) {
19455
19479
  const prevCustomPrompt = await loadStoredCustomPrompt(storage, job.project);
19456
19480
  const customPrompt = {
19457
19481
  schemaVersion: 1,
19458
- basePromptVersion: "7",
19482
+ basePromptVersion: "8",
19459
19483
  customPromptVersion: `${generatedAt}-c${fallbackCases.length}`,
19460
19484
  generatedAt,
19461
19485
  guidance: fallbackGuidance ?? "",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.8.0",
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.0",
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": {