ccqa 1.34.0 → 1.35.1

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/README.md CHANGED
@@ -197,6 +197,26 @@ tenant, an account, a role — not an environment: ccqa tracks one
197
197
  verification environment
198
198
  ([ADR-0013](./docs/adr/0013-one-verification-environment.md)).
199
199
 
200
+ ## Agent skills
201
+
202
+ `skills/` ships guides that let a coding agent (e.g. Claude Code) drive ccqa
203
+ end-to-end on its own:
204
+
205
+ - **ccqa-record** — create a new test case: pin down the behavior, write the
206
+ spec, choose deterministic or live mode, record, run to green.
207
+ - **ccqa-rerecord** — bring a flagged or failing test case back to green:
208
+ read the hub's finding, decide what went stale, repair that, re-record.
209
+ - **ccqa-resolve** — clear everything the hub is holding for a person: read
210
+ the verdict, order the rows, route each to a product fix, a re-recording or
211
+ an environment repair.
212
+
213
+ Install them with the [skills CLI](https://github.com/vercel-labs/skills)
214
+ into a consuming project (or `-g` for all projects):
215
+
216
+ ```bash
217
+ npx skills add <this-repo> --skill ccqa-record --skill ccqa-rerecord --skill ccqa-resolve
218
+ ```
219
+
200
220
  ## Documentation
201
221
 
202
222
  | I want to… | Read |
package/dist/bin/ccqa.mjs CHANGED
@@ -89,17 +89,19 @@ function* iterEnvRefNames(value) {
89
89
  }
90
90
  }
91
91
  /**
92
- * Resolve every `$VAR` / `${VAR}` reference against the current process env.
92
+ * Resolve every `$VAR` / `${VAR}` reference against `overrides`, then the
93
+ * current process env. `overrides` carries values an invoker injects into a
94
+ * child process (e.g. CCQA_RUN_ID), which beat the parent env there.
93
95
  *
94
96
  * Missing variables expand to the empty string, mirroring `sh` behaviour.
95
97
  * Throwing would force ccqa to be invoked with every var set even for
96
98
  * unused blocks, which is more user-hostile than letting the test fail
97
99
  * downstream with a clearer message ("login form rejected: empty password").
98
100
  */
99
- function resolveEnvRefs(value) {
101
+ function resolveEnvRefs(value, overrides = {}) {
100
102
  return value.replace(ENV_VAR_RE, (_, braced, plain) => {
101
103
  const name = braced ?? plain ?? "";
102
- return process.env[name] ?? "";
104
+ return overrides[name] ?? process.env[name] ?? "";
103
105
  });
104
106
  }
105
107
  /**
@@ -408,6 +410,7 @@ function collectIncludedBlockNames(spec) {
408
410
  const CCQA_DIR = ".ccqa";
409
411
  const SPEC_FILE = "spec.yaml";
410
412
  const RECORDING_FILE = "ir.json";
413
+ const FAILED_RECORDING_FILE = "ir.failed.json";
411
414
  const PERSPECTIVES_FILE = "perspectives.yaml";
412
415
  const PERSPECTIVES_MD_FILE = "perspectives.md";
413
416
  function getCcqaDir(cwd = process.cwd()) {
@@ -476,9 +479,22 @@ async function saveRecording(featureName, specName, actions, cwd) {
476
479
  await mkdir(specDir, { recursive: true });
477
480
  const recordingPath = join(specDir, RECORDING_FILE);
478
481
  await writeFile(recordingPath, JSON.stringify(actions, null, 2), "utf-8");
479
- await Promise.all(LEGACY_RECORDING_FILES.map((f) => unlink(join(specDir, f)).catch(() => {})));
482
+ await Promise.all([...LEGACY_RECORDING_FILES, FAILED_RECORDING_FILE].map((f) => unlink(join(specDir, f)).catch(() => {})));
480
483
  return recordingPath;
481
484
  }
485
+ /**
486
+ * Persist the actions of a trace that FAILED. They go to a side file so a
487
+ * recording that did not demonstrate the spec can never replace one that
488
+ * did — `ir.json` and the generated code stay whatever they were. The next
489
+ * successful {@link saveRecording} deletes the file.
490
+ */
491
+ async function saveFailedRecording(featureName, specName, actions, cwd) {
492
+ const specDir = getSpecDir(featureName, specName, cwd);
493
+ await mkdir(specDir, { recursive: true });
494
+ const path = join(specDir, FAILED_RECORDING_FILE);
495
+ await writeFile(path, JSON.stringify(actions, null, 2), "utf-8");
496
+ return path;
497
+ }
482
498
  function getBlocksDir(cwd) {
483
499
  return join(getCcqaDir(cwd), "blocks");
484
500
  }
@@ -1326,10 +1342,12 @@ function opt(key, value) {
1326
1342
  * `instruction` / `expected` may *also* contain `${ENV}` refs that
1327
1343
  * don't go through include params.
1328
1344
  *
1329
- * Only refs whose env value is currently non-empty land in the map
1330
- * scrubbing against an empty string would corrupt unrelated empty strings
1331
- * in the action stream. Names whose env is unset are returned via
1332
- * `unresolved` so the caller can warn the user.
1345
+ * Each ref resolves against `overrides` first, then `process.env`
1346
+ * `overrides` carries values the invoker injects into the child process,
1347
+ * which beat the parent env there. Only refs that resolve non-empty land in
1348
+ * the map scrubbing against an empty string would corrupt unrelated empty
1349
+ * strings in the action stream; the rest are returned via `unresolved` so
1350
+ * the caller can warn the user.
1333
1351
  *
1334
1352
  * Longer values sort first so a `${SHORT}` whose value is a substring of a
1335
1353
  * `${LONG}` value doesn't clobber the longer one.
@@ -1337,7 +1355,7 @@ function opt(key, value) {
1337
1355
  * `title` is deliberately NOT scanned — it never reaches the recorded action
1338
1356
  * stream.
1339
1357
  */
1340
- function buildSpecEnvScrub(spec, expanded) {
1358
+ function buildSpecEnvScrub(spec, expanded, overrides = {}) {
1341
1359
  const refNames = /* @__PURE__ */ new Set();
1342
1360
  for (const step of spec.steps) if (isIncludeStep(step)) for (const v of Object.values(step.params ?? {})) collect(v, refNames);
1343
1361
  else {
@@ -1351,7 +1369,7 @@ function buildSpecEnvScrub(spec, expanded) {
1351
1369
  const map = [];
1352
1370
  const unresolved = [];
1353
1371
  for (const name of refNames) {
1354
- const value = process.env[name];
1372
+ const value = overrides[name] ?? process.env[name];
1355
1373
  if (typeof value === "string" && value.length > 0) map.push([value, "${" + name + "}"]);
1356
1374
  else unresolved.push(name);
1357
1375
  }
@@ -1383,8 +1401,8 @@ const COMMON_PROSE_VALUES = new Set([
1383
1401
  * command log too, trading that short-value coverage for not building a
1384
1402
  * second map.
1385
1403
  */
1386
- function buildProseEnvScrubMap(spec, expanded) {
1387
- return buildSpecEnvScrub(spec, expanded).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
1404
+ function buildProseEnvScrubMap(spec, expanded, overrides = {}) {
1405
+ return buildSpecEnvScrub(spec, expanded, overrides).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
1388
1406
  }
1389
1407
  /**
1390
1408
  * Replace every occurrence of an env value with its `${VAR}` placeholder in
@@ -10570,7 +10588,6 @@ async function runOneSpec(args) {
10570
10588
  }
10571
10589
  const spec = parseTestSpec(specContent);
10572
10590
  const expanded = expandSpec(spec, { blocks: await loadAllBlocks(cwd) });
10573
- const envScrubMap = buildProseEnvScrubMap(spec, expanded);
10574
10591
  meta("spec", spec.title);
10575
10592
  meta("steps", expanded.length);
10576
10593
  const includes = collectIncludedBlockNames(spec);
@@ -10600,6 +10617,7 @@ async function runOneSpec(args) {
10600
10617
  }
10601
10618
  try {
10602
10619
  const runId = buildRunId();
10620
+ const envScrubMap = buildProseEnvScrubMap(spec, expanded, { CCQA_RUN_ID: runId });
10603
10621
  const runDir = opts.out ?? join(specDir, "runs", runId);
10604
10622
  await mkdir(runDir, { recursive: true });
10605
10623
  meta("runDir", runDir);
@@ -11835,10 +11853,11 @@ async function runVitest(scriptPath, agentBrowserSession) {
11835
11853
  "--config",
11836
11854
  bundledVitestConfigPath(),
11837
11855
  scriptPath
11838
- ], agentBrowserSession ? { env: {
11856
+ ], { env: {
11839
11857
  ...process.env,
11840
- AGENT_BROWSER_SESSION: agentBrowserSession
11841
- } } : {});
11858
+ CCQA_RUN_ID: buildRunId(),
11859
+ ...agentBrowserSession ? { AGENT_BROWSER_SESSION: agentBrowserSession } : {}
11860
+ } });
11842
11861
  const currentScript = await readFile(scriptPath, "utf8");
11843
11862
  return {
11844
11863
  exitCode,
@@ -14030,6 +14049,10 @@ function createRunTeardown() {
14030
14049
  untrackSession(name) {
14031
14050
  sessions.delete(name);
14032
14051
  },
14052
+ async closeTracked(name) {
14053
+ await closeSession(name);
14054
+ sessions.delete(name);
14055
+ },
14033
14056
  onFinalize(fn) {
14034
14057
  finalizers.push(fn);
14035
14058
  },
@@ -14304,6 +14327,7 @@ CCQA_STEP=<step-id> agent-browser --session SESSION upload "<input[type=file] se
14304
14327
  - \`@ref\` / \`@e1\` / \`e14\` — reference IDs are session-specific and change every run.
14305
14328
  - **Bare tag selectors**: \`button\`, \`a\`, \`div\`, \`td\`, \`tr\`, \`main a\`, \`table tbody tr:nth-child(N)\`. These match every element of that tag and are non-deterministic on replay. **This includes the inner selector inside \`find first/last/nth\`** — see the \`find\` rules below.
14306
14329
  - \`[role='button']\` or \`[type='checkbox']\` alone — matches too many elements.
14330
+ - **Playwright-only pseudo-classes**: \`:has-text()\`, \`:text-is()\`, \`:text-matches()\`, \`:visible\`. agent-browser's CSS engine does not implement them — they match nothing and every command using them fails. Use \`text=...\` or plain CSS instead.
14307
14331
  - JavaScript execution (\`eval\`, \`js\`) — blocked by the hook layer.
14308
14332
 
14309
14333
  ### \`find\` subset (fallback when no ALLOWED CSS uniquely targets the element)
@@ -14685,9 +14709,9 @@ const ASSERT_TIMEOUT_MS = 1e4;
14685
14709
  * has no side effect; assert types whose codegen forms aren't directly
14686
14710
  * verifiable here fall through to the caller's `unverifiable` fallback).
14687
14711
  */
14688
- function actionToAbArgs(action, sessionName) {
14712
+ function actionToAbArgs(action, sessionName, envOverrides = {}) {
14689
14713
  const base = ["--session", sessionName];
14690
- const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s);
14714
+ const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s, envOverrides);
14691
14715
  switch (action.action) {
14692
14716
  case "snapshot": return null;
14693
14717
  case "assert": return assertToAbArgs(action, sub, sessionName);
@@ -14772,8 +14796,8 @@ const NO_STEP_ID = "__no_step__";
14772
14796
  * `wait <selector>`); everything else spawns the agent-browser argv. A single
14773
14797
  * hard-timeout (SIGTERM) retry covers the daemon's occasional under-load drop.
14774
14798
  */
14775
- function runValidationAction(action, sessionName) {
14776
- const built = actionToAbArgs(action, sessionName);
14799
+ function runValidationAction(action, sessionName, envOverrides = {}) {
14800
+ const built = actionToAbArgs(action, sessionName, envOverrides);
14777
14801
  if (built === null) return {
14778
14802
  skipped: true,
14779
14803
  ok: false,
@@ -14817,7 +14841,7 @@ function validateActions(actions, opts) {
14817
14841
  });
14818
14842
  continue;
14819
14843
  }
14820
- const outcome = runValidationAction(action, opts.sessionName);
14844
+ const outcome = runValidationAction(action, opts.sessionName, opts.envOverrides);
14821
14845
  if (outcome.skipped) {
14822
14846
  kept.push(action);
14823
14847
  continue;
@@ -14894,7 +14918,7 @@ function rescueLostSteps(actions, kept, dropped, opts) {
14894
14918
  for (const [stepId, drops] of lostStepDrops.entries()) {
14895
14919
  let anyForThisStep = false;
14896
14920
  for (const d of drops) {
14897
- const outcome = runValidationAction(d.action, opts.sessionName);
14921
+ const outcome = runValidationAction(d.action, opts.sessionName, opts.envOverrides);
14898
14922
  if (outcome.skipped) continue;
14899
14923
  if (outcome.ok) {
14900
14924
  rescuedIndices.add(d.index);
@@ -15076,12 +15100,22 @@ function formatUnstableDrop(drop) {
15076
15100
  }
15077
15101
  //#endregion
15078
15102
  //#region src/cli/trace.ts
15103
+ /**
15104
+ * Step ids (in spec order) whose kept actions include no assertion. A step
15105
+ * with none produced a test that performs the step but verifies nothing
15106
+ * about its `expected` — surfaced as a per-step warning after validation.
15107
+ */
15108
+ function stepsWithoutAsserts(stepIds, actions) {
15109
+ const withAssert = new Set(actions.filter((a) => a.action === "assert" && a.stepId !== void 0).map((a) => a.stepId));
15110
+ return stepIds.filter((id) => !withAssert.has(id));
15111
+ }
15079
15112
  async function runTrace(featureName, specName, model, validationMode = "lenient", language, opts = {}) {
15080
15113
  header("trace", `${featureName}/${specName}`);
15081
15114
  await preflightAgentBrowserCommand();
15082
15115
  const spec = parseTestSpec(await readSpecFile(featureName, specName, opts.cwd));
15083
15116
  const expanded = expandSpec(spec, { blocks: await loadAllBlocks(opts.cwd) });
15084
- const envScrub = buildSpecEnvScrub(spec, expanded);
15117
+ const sessionName = generateSessionName();
15118
+ const envScrub = buildSpecEnvScrub(spec, expanded, { CCQA_RUN_ID: sessionName });
15085
15119
  const envScrubMap = envScrub.map;
15086
15120
  if (envScrub.unresolved.length > 0) {
15087
15121
  warn(`spec references env var(s) that are unset at record time: ${envScrub.unresolved.join(", ")}`);
@@ -15093,7 +15127,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
15093
15127
  const includes = collectIncludedBlockNames(spec);
15094
15128
  if (includes.length > 0) meta("blocks", includes.join(", "));
15095
15129
  blank();
15096
- const sessionName = generateSessionName();
15130
+ opts.teardown?.trackSession(sessionName);
15097
15131
  const baseSystemPrompt = buildTraceSystemPrompt({
15098
15132
  title: spec.title,
15099
15133
  steps: expanded,
@@ -15176,13 +15210,18 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
15176
15210
  }
15177
15211
  });
15178
15212
  if (isError) overallStatus = "failed";
15179
- const validatedActions = validateAndReport(dedupAndReport(scrubAndReport(traceActions)), validationMode);
15180
- const recordingPath = await saveRecording(featureName, specName, validatedActions, opts.cwd);
15213
+ const dedupedActions = dedupAndReport(scrubAndReport(traceActions));
15214
+ const validatedActions = overallStatus === "passed" || opts.validateFailedTrace === true ? validateAndReport(dedupedActions, validationMode, { CCQA_RUN_ID: sessionName }, opts.teardown) : dedupedActions;
15215
+ opts.teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
15216
+ const recordingPath = overallStatus === "passed" ? await saveRecording(featureName, specName, validatedActions, opts.cwd) : await saveFailedRecording(featureName, specName, validatedActions, opts.cwd);
15181
15217
  blank();
15182
15218
  meta("saved", recordingPath);
15183
15219
  meta("actions", validatedActions.length);
15184
15220
  meta("status", overallStatus.toUpperCase());
15185
- hint(`run 'ccqa generate ${featureName}/${specName}' to generate a test script`);
15221
+ if (overallStatus === "passed") {
15222
+ for (const stepId of stepsWithoutAsserts(expanded.map((s) => s.id), validatedActions)) warn(`${stepId} recorded no assertion — nothing in the generated test verifies its 'expected'`);
15223
+ hint(`run 'ccqa generate ${featureName}/${specName}' to generate a test script`);
15224
+ } else warn("trace FAILED — the recorded actions were saved beside the spec for diagnosis; the previous ir.json and generated code are left untouched");
15186
15225
  return {
15187
15226
  status: overallStatus,
15188
15227
  statusLines,
@@ -15323,26 +15362,30 @@ function isAdjacentDuplicate(a, b) {
15323
15362
  }
15324
15363
  /**
15325
15364
  * Run the post-trace replay validation and emit user-visible drop reports.
15326
- * Splitting this out keeps `runTrace` readable; the function is pure aside
15327
- * from `log.*` and the agent-browser invocations inside `validateActions`.
15365
+ * Splitting this out keeps `runTrace` readable; side effects are `log.*`,
15366
+ * the agent-browser invocations inside `validateActions`, and the replay
15367
+ * session's registration/close on `teardown`.
15328
15368
  *
15329
15369
  * In lenient mode (the default) failing actions are NOT removed — they're
15330
15370
  * tagged with `replayUnstable: true` and merged back into the output stream
15331
15371
  * in their original order so codegen can still emit them (with a `// [warn]`
15332
15372
  * comment) and let the auto-fix loop decide what to do.
15333
15373
  */
15334
- function validateAndReport(actions, mode) {
15374
+ function validateAndReport(actions, mode, envOverrides, teardown) {
15335
15375
  if (actions.length === 0) return actions;
15336
15376
  const sessionName = `${generateSessionName()}-validate`;
15377
+ teardown?.trackSession(sessionName);
15337
15378
  blank();
15338
15379
  info(`post-trace validation in ${mode} mode (replaying ${actions.length} recorded action(s))...`);
15339
15380
  const { kept, unstable, dropped, rescuedSteps = [] } = validateActions(actions, {
15340
15381
  sessionName,
15341
15382
  mode,
15383
+ envOverrides,
15342
15384
  onProgress: (i, total, action) => {
15343
15385
  progress(i, total, validationProgressLabel(action));
15344
15386
  }
15345
15387
  });
15388
+ teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
15346
15389
  progressEnd();
15347
15390
  if (rescuedSteps.length > 0) info(`rescued ${rescuedSteps.length} step(s) that had lost every action: ${rescuedSteps.join(", ")}`);
15348
15391
  if (mode === "lenient") {
@@ -15565,6 +15608,10 @@ function resolveTargetOrExit(resolve) {
15565
15608
  async function runGenerateLocked(featureName, specName, opts, cwd) {
15566
15609
  const specYaml = await readSpecFile(featureName, specName, cwd);
15567
15610
  const spec = parseTestSpec(specYaml);
15611
+ if (spec.mode === "live") {
15612
+ error(`this spec is 'mode: live' — a live spec runs without generated code. Run 'ccqa run ${featureName}/${specName}' instead`);
15613
+ process.exit(2);
15614
+ }
15568
15615
  const config = await loadProjectConfig(cwd);
15569
15616
  const target = resolveTargetOrExit(() => opts.targetOverride !== void 0 ? resolveTargetOverride(spec, opts.targetOverride) : resolveTarget(spec, config));
15570
15617
  meta("target", target.id + (opts.targetOverride !== void 0 ? " (--target override)" : ""));
@@ -15737,6 +15784,10 @@ async function runRecord(specPath, opts) {
15737
15784
  error(`target "${target.id}" does not use a browser recording — run 'ccqa generate ${featureName}/${specName}' instead`);
15738
15785
  process.exit(2);
15739
15786
  }
15787
+ if (spec.mode === "live") {
15788
+ error(`this spec is 'mode: live' — a live spec runs without a recording. Run 'ccqa run ${featureName}/${specName}' instead`);
15789
+ process.exit(2);
15790
+ }
15740
15791
  const project = opts.hubProfile !== void 0 ? resolveProject(opts) : void 0;
15741
15792
  if (opts.hubProfile !== void 0) await applyProfileFromOption({
15742
15793
  profile: opts.hubProfile,
@@ -15804,6 +15855,8 @@ async function runRecord(specPath, opts) {
15804
15855
  const traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
15805
15856
  cwd: cwdForProfile,
15806
15857
  hubContext,
15858
+ teardown,
15859
+ validateFailedTrace: opts.learnHubTracePrompt === true,
15807
15860
  ...opts.instruction ? { instruction: opts.instruction } : {},
15808
15861
  onStep: (stepId) => {
15809
15862
  tracingStep = stepId;
@@ -15821,7 +15874,7 @@ async function runRecord(specPath, opts) {
15821
15874
  ...opts.model ? { model: opts.model } : {},
15822
15875
  ...language ? { language } : {}
15823
15876
  });
15824
- if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
15877
+ if (!opts.traceOnly && traceResult.status === "passed") generated = (await runGenerate(featureName, specName, {
15825
15878
  maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
15826
15879
  fixMode: toFixMode(opts.autoFix ?? "interactive"),
15827
15880
  force: opts.overwrite ?? false,
@@ -16089,7 +16142,9 @@ Ask whether the **intent** the step describes still exists in the product:
16089
16142
  - The intent exists, but the string or selector the spec names is gone or renamed → **TEST_DRIFT**. Cite where the replacement lives.
16090
16143
  - The intent itself is gone, or deliberately different → **SPEC_CHANGE**. Cite the source that shows the new shape.
16091
16144
 
16092
- A renamed button is TEST_DRIFT. A button that no longer exists because the flow was replaced is SPEC_CHANGE. If the source shows a rename you can point at, prefer TEST_DRIFT; if you cannot find where the intent went, SPEC_CHANGE is the most you may claim, and UNKNOWN when even that is a guess.
16145
+ A renamed button is TEST_DRIFT. A button that no longer exists because the flow was replaced is SPEC_CHANGE. If the source shows a rename you can point at, prefer TEST_DRIFT.
16146
+
16147
+ SPEC_CHANGE is the more expensive answer — it sends a human to rewrite or retire the spec — so it takes the *stronger* evidence, not the weaker. Failing to find where the intent went is not a finding; that is UNKNOWN. Claim SPEC_CHANGE only when you can point at the source that shows the new shape, or at where the implementation would sit if it still existed.
16093
16148
 
16094
16149
  ## Which surface drifted
16095
16150
 
@@ -16122,6 +16177,8 @@ sit nearby.
16122
16177
 
16123
16178
  - **No drift is a claim, not a default.** Make it after picking the concrete strings from *every* surface you were given — the spec's \`expected\` and the generated code's selectors alike — and finding each of them in the source. Clearing the test case because one surface checked out is the most common way to miss a real finding. If you never looked, the honest answer is UNKNOWN.
16124
16179
  - **A finding needs a citation.** Every TEST_DRIFT and SPEC_CHANGE must carry at least one \`evidence\` entry with a real \`file\`, and a line where you can give one. A label with no citation is a guess wearing a verdict's clothes — answer UNKNOWN instead.
16180
+ - **A citation must apply to the case at hand.** Finding the string is not the end of it — read what encloses the line before you cite it. A line inside a guard, behind an early \`return\`, or in a branch this spec's steps never enter says nothing about this spec. Name the conditions that must hold for that line to run, and check the spec puts the product in them. A citation that only proves the line exists is not evidence.
16181
+ - **A comment is not the code.** Comments in the product's source say what someone intended, and they rarely restate the conditions they sit under. A line reading "this is not supported", sitting inside a guarded branch, is true only inside that branch. Cite the control flow you traced, not the sentence you found.
16125
16182
  - **Do not report style.** Wording you would have phrased differently is not drift. Report only what would make a replay fail, or what asks about something the product no longer does.
16126
16183
  - \`confidence\` is about the label: how sure you are it is the right one, not how bad the finding is.
16127
16184
 
@@ -16131,6 +16188,7 @@ sit nearby.
16131
16188
  2. \`Grep\` the source for them, at the page, component or handler the step is about.
16132
16189
  3. For \`include\` steps, confirm the block exists under \`.ccqa/blocks/<name>/spec.yaml\` and that every \`params\` key is declared on it.
16133
16190
  4. When a string is missing, look for what replaced it before concluding. Where it went is what decides the label.
16191
+ 5. Before citing any line, read the block that encloses it. Which conditions must hold for it to run, and does the spec put the product in those conditions? A line that only runs in a case the spec never enters proves nothing about the spec.
16134
16192
 
16135
16193
  ${guidance.userPromptBlock ?? ""}${guidance.customPromptBlock ?? ""}## Output (STRICT)
16136
16194
 
@@ -16488,7 +16546,7 @@ function determineExitCode(results, threshold) {
16488
16546
  //#endregion
16489
16547
  //#region src/drift/to-report.ts
16490
16548
  /** Tracks the drift prompt's own version — the two must never drift apart. */
16491
- const DRIFT_REPORT_PROMPT_VERSION = "6";
16549
+ const DRIFT_REPORT_PROMPT_VERSION = "7";
16492
16550
  /**
16493
16551
  * Spec-level status under the given threshold, mirroring determineExitCode's
16494
16552
  * per-spec logic (exit-code.ts) but scoped to a single SpecResult.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
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.34.0",
3
+ "version": "1.35.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {