ccqa 1.28.0 → 1.29.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
@@ -237,17 +237,11 @@ const DEFAULT_SPEC_MODE = "deterministic";
237
237
  * `required: false` makes it optional. `secret: true` flags the value as
238
238
  * sensitive — codegen renders such values as `process.env.<NAME> ?? ""`
239
239
  * template literals so the secret never ends up baked into test.spec.ts.
240
- * `dummy` is a placeholder value surfaced by the draft / drift prompts
241
- * (which see the block in isolation, before any include site exists);
242
- * `description` is the param's semantic role, also consumed by those
243
- * prompts and by spec authors browsing the block.
244
240
  */
245
241
  const BlockParamSchema = z.object({
246
242
  name: z.string().min(1),
247
243
  required: z.boolean().optional(),
248
- secret: z.boolean().optional(),
249
- dummy: z.string().optional(),
250
- description: z.string().optional()
244
+ secret: z.boolean().optional()
251
245
  }).strict();
252
246
  /**
253
247
  * Block schema. Block steps are restricted to ActionStep — nested blocks are
@@ -270,15 +264,25 @@ function isParamRequired(param) {
270
264
  }
271
265
  //#endregion
272
266
  //#region src/spec/parser.ts
273
- /**
274
- * Fields the schema used to accept, and what to do now. The schema is
275
- * `.strict()`, so a spec still carrying one fails on an "unrecognized key"
276
- * that says nothing about why it stopped being recognised. Folded into
277
- * `humanizeIssue`'s `unrecognized_keys` branch (below) rather than checked
278
- * ahead of validation, so the migration note reaches both `parseTestSpec` and
279
- * `parseBlockSpec` through the one place that already rewrites that error.
280
- */
281
- const REMOVED_FIELDS = { relatedPaths: "which specs a change affects is now decided by `ccqa select-specs`, which reads the diff instead of a declared path list. Delete the field." };
267
+ /** The spec/block root (an `unrecognized_keys` issue there has an empty path). */
268
+ const atRoot = (path) => path.length === 0;
269
+ /** A block param entry the issue path is `params.<index>`. */
270
+ const atBlockParam = (path) => path.length === 2 && path[0] === "params" && typeof path[1] === "number";
271
+ const UNREAD_PARAM_FIELD = "nothing reads it (a block param reaches the prompts as its name, required and secret only). Delete the line.";
272
+ const REMOVED_FIELDS = {
273
+ relatedPaths: {
274
+ at: atRoot,
275
+ message: "which specs a change affects is now decided by `ccqa select-specs`, which reads the diff instead of a declared path list. Delete the field."
276
+ },
277
+ dummy: {
278
+ at: atBlockParam,
279
+ message: UNREAD_PARAM_FIELD
280
+ },
281
+ description: {
282
+ at: atBlockParam,
283
+ message: UNREAD_PARAM_FIELD
284
+ }
285
+ };
282
286
  /** Parse a spec.yaml. Schema rejections are rewritten with actionable messages. */
283
287
  function parseTestSpec(content, source = "spec.yaml") {
284
288
  const raw = parseYamlOrThrow(content, source);
@@ -336,9 +340,9 @@ function humanizeIssue(issue, isBlock) {
336
340
  if (issue.code === "unrecognized_keys") {
337
341
  const keys = Array.isArray(issue.keys) ? issue.keys : [];
338
342
  if (isBlock && keys.includes("include")) return `Nested blocks are not supported — flatten by inlining the included block's steps into this block.`;
339
- const removed = keys.filter((k) => k in REMOVED_FIELDS);
340
- const stillUnknown = keys.filter((k) => !(k in REMOVED_FIELDS));
341
- const parts = removed.map((k) => `\`${k}\` is no longer part of the spec schema — ${REMOVED_FIELDS[k]}`);
343
+ const removed = keys.filter((k) => REMOVED_FIELDS[k]?.at(issue.path));
344
+ const stillUnknown = keys.filter((k) => !REMOVED_FIELDS[k]?.at(issue.path));
345
+ const parts = removed.map((k) => `\`${k}\` is no longer part of the spec schema — ${REMOVED_FIELDS[k].message}`);
342
346
  if (stillUnknown.length > 0) parts.push(`Unknown keys: ${stillUnknown.join(", ")}`);
343
347
  return parts.join(" ");
344
348
  }
@@ -512,8 +516,8 @@ async function loadAllBlocks(cwd) {
512
516
  * Co-located with `loadAllBlocks` so callers don't have to remember the
513
517
  * isParamRequired / secret-default mapping.
514
518
  */
515
- async function loadAvailableBlocks(cwd) {
516
- return [...(await loadAllBlocks(cwd)).entries()].map(([name, block]) => ({
519
+ function projectAvailableBlocks(blocks) {
520
+ return [...blocks.entries()].map(([name, block]) => ({
517
521
  name,
518
522
  title: block.title,
519
523
  params: (block.params ?? []).map((p) => ({
@@ -523,6 +527,10 @@ async function loadAvailableBlocks(cwd) {
523
527
  }))
524
528
  }));
525
529
  }
530
+ /** `loadAllBlocks` + `projectAvailableBlocks`, for callers that need only the projection. */
531
+ async function loadAvailableBlocks(cwd) {
532
+ return projectAvailableBlocks(await loadAllBlocks(cwd));
533
+ }
526
534
  const USER_PROMPT_MAX_BYTES = 32768;
527
535
  /**
528
536
  * Load the prompt bundle from the hub for one guidance kind ("record" /
@@ -1298,6 +1306,95 @@ function opt(key, value) {
1298
1306
  return value ? { [key]: value } : {};
1299
1307
  }
1300
1308
  //#endregion
1309
+ //#region src/runtime/env-scrub.ts
1310
+ /**
1311
+ * Build a list of `[envValue, "${VAR}"]` pairs for every `${VAR}` reference
1312
+ * mentioned in the spec OR in any of its expanded (block-inlined) steps.
1313
+ * Used at trace time to scrub recorded Claude-text outputs so a value the
1314
+ * spec author intentionally threaded through `process.env` is preserved as
1315
+ * `${VAR}` in `ir.json` rather than baked in as the concrete
1316
+ * trace-time value.
1317
+ *
1318
+ * Why we walk `spec.steps` AND `expanded`:
1319
+ * - `spec.steps` carries the spec's own `instruction` / `expected` + each
1320
+ * include's raw `params` (which may themselves be `${ENV}` refs).
1321
+ * - `expanded` carries the inlined block-internal steps, whose
1322
+ * `instruction` / `expected` may *also* contain `${ENV}` refs that
1323
+ * don't go through include params.
1324
+ *
1325
+ * Only refs whose env value is currently non-empty land in the map —
1326
+ * scrubbing against an empty string would corrupt unrelated empty strings
1327
+ * in the action stream. Names whose env is unset are returned via
1328
+ * `unresolved` so the caller can warn the user.
1329
+ *
1330
+ * Longer values sort first so a `${SHORT}` whose value is a substring of a
1331
+ * `${LONG}` value doesn't clobber the longer one.
1332
+ *
1333
+ * `title` is deliberately NOT scanned — it never reaches the recorded action
1334
+ * stream.
1335
+ */
1336
+ function buildSpecEnvScrub(spec, expanded) {
1337
+ const refNames = /* @__PURE__ */ new Set();
1338
+ for (const step of spec.steps) if (isIncludeStep(step)) for (const v of Object.values(step.params ?? {})) collect(v, refNames);
1339
+ else {
1340
+ collect(step.instruction, refNames);
1341
+ collect(step.expected, refNames);
1342
+ }
1343
+ for (const step of expanded) {
1344
+ collect(step.instruction, refNames);
1345
+ collect(step.expected, refNames);
1346
+ }
1347
+ const map = [];
1348
+ const unresolved = [];
1349
+ for (const name of refNames) {
1350
+ const value = process.env[name];
1351
+ if (typeof value === "string" && value.length > 0) map.push([value, "${" + name + "}"]);
1352
+ else unresolved.push(name);
1353
+ }
1354
+ map.sort((a, b) => b[0].length - a[0].length);
1355
+ return {
1356
+ map,
1357
+ unresolved
1358
+ };
1359
+ }
1360
+ function collect(value, into) {
1361
+ for (const name of iterEnvRefNames(value)) into.add(name);
1362
+ }
1363
+ /** Shorter than this, a value is no secret and matches inside ordinary words. */
1364
+ const MIN_PROSE_SCRUB_LENGTH = 4;
1365
+ /** Long enough to clear the length bar, still ordinary prose / JSON. */
1366
+ const COMMON_PROSE_VALUES = new Set([
1367
+ "true",
1368
+ "false",
1369
+ "null",
1370
+ "none",
1371
+ "undefined"
1372
+ ]);
1373
+ /**
1374
+ * Scrub map for model output, built like {@link buildSpecEnvScrub} but
1375
+ * without the values that read as ordinary text (`"1"`, `"true"`): prose
1376
+ * runs to paragraphs, where replacing every occurrence of such a value
1377
+ * costs more meaning than it protects. Record's own scrub keeps them for
1378
+ * its single command lines; the live path reuses this one map for its Bash
1379
+ * command log too, trading that short-value coverage for not building a
1380
+ * second map.
1381
+ */
1382
+ function buildProseEnvScrubMap(spec, expanded) {
1383
+ return buildSpecEnvScrub(spec, expanded).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
1384
+ }
1385
+ /**
1386
+ * Replace every occurrence of an env value with its `${VAR}` placeholder in
1387
+ * `text`. **Caller invariant**: the map must be sorted longest-value-first
1388
+ * so a shorter value doesn't shadow a longer one that contains it as a
1389
+ * substring. `buildSpecEnvScrub` upholds this; hand-built maps should too.
1390
+ */
1391
+ function scrubEnvValues(text, scrubMap) {
1392
+ if (scrubMap.length === 0) return text;
1393
+ let out = text;
1394
+ for (const [value, placeholder] of scrubMap) if (out.includes(value)) out = out.replaceAll(value, placeholder);
1395
+ return out;
1396
+ }
1397
+ //#endregion
1301
1398
  //#region src/claude/native-binary.ts
1302
1399
  const require$1 = createRequire(import.meta.url);
1303
1400
  /**
@@ -1457,7 +1554,7 @@ function warnOnceIfNativeBinaryMissing() {
1457
1554
  if (missing) warn(missingNativeBinaryMessage(missing));
1458
1555
  }
1459
1556
  async function invokeClaudeStreaming(options, onEvent) {
1460
- const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, relaxAbConstraints = false } = options;
1557
+ const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
1461
1558
  const resolvedModel = resolveModel(model);
1462
1559
  const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
1463
1560
  const mergedEnv = env || hasEndpointEnv ? withoutEmptyEndpointVars({
@@ -1561,7 +1658,7 @@ async function invokeClaudeStreaming(options, onEvent) {
1561
1658
  if (msg.type === "assistant" && !silenceBashLog) {
1562
1659
  for (const block of msg.message.content ?? []) if (block.type === "tool_use" && block.name === "Bash") {
1563
1660
  const cmd = block.input?.["command"];
1564
- if (typeof cmd === "string") bash(cmd);
1661
+ if (typeof cmd === "string") bash(scrubEnvValues(cmd, envScrubMap));
1565
1662
  }
1566
1663
  }
1567
1664
  if (msg.type === "result") {
@@ -3486,6 +3583,36 @@ function buildDiffMcpServer(getFileDiff) {
3486
3583
  * with confidence 0 rather than throwing — the report must always render.
3487
3584
  */
3488
3585
  async function analyzeFailure(input, options) {
3586
+ return scrubOutcome(await classifyFailure(input, options), options.envScrubMap ?? []);
3587
+ }
3588
+ /**
3589
+ * Mask the profile values the classifier may have quoted: its Read/Grep reach
3590
+ * the repository, so a local `.env` is in reach of its prose even when the
3591
+ * evidence it was handed is clean. A literal match — a value the model
3592
+ * paraphrases still gets through. Only what the classifier authored is
3593
+ * covered: the row's other evidence (`failureLogExcerpt`, `diffExcerpt`)
3594
+ * passes to the report and the hub as its producer wrote it.
3595
+ */
3596
+ function scrubOutcome(outcome, scrubMap) {
3597
+ if (scrubMap.length === 0) return outcome;
3598
+ const scrub = (text) => scrubEnvValues(text, scrubMap);
3599
+ const { analysis } = outcome;
3600
+ return {
3601
+ ...outcome,
3602
+ raw: scrub(outcome.raw),
3603
+ analysis: {
3604
+ ...analysis,
3605
+ headline: scrub(analysis.headline),
3606
+ recommendation: scrub(analysis.recommendation),
3607
+ reasoning: scrub(analysis.reasoning),
3608
+ evidence: analysis.evidence.map((item) => ({
3609
+ ...item,
3610
+ detail: scrub(item.detail)
3611
+ }))
3612
+ }
3613
+ };
3614
+ }
3615
+ async function classifyFailure(input, options) {
3489
3616
  const { result: raw, isError } = await invokeClaudeStreaming({
3490
3617
  prompt: buildFailureAnalysisPrompt(input),
3491
3618
  allowedTools: [
@@ -6264,6 +6391,7 @@ function createFailureAnalysisPass(deps) {
6264
6391
  };
6265
6392
  const baselineMissing = specDiffResult.ok ? null : specDiffResult.skip;
6266
6393
  info(`failure analysis: ${featureName}/${specName}${baselineMissing ? " (no baseline — classifying from current source)" : ""}`);
6394
+ const envScrubMap = specEnvScrubMap(input.parsedSpec, deps.parsedBlocks);
6267
6395
  const outcome = await analyzeFailure({
6268
6396
  script: await input.readScript(),
6269
6397
  hasGeneratedSurface: true,
@@ -6283,7 +6411,8 @@ function createFailureAnalysisPass(deps) {
6283
6411
  }, {
6284
6412
  ...deps.model ? { model: deps.model } : {},
6285
6413
  cwd: deps.cwd,
6286
- getFileDiff: specDiff?.fileDiff ?? (() => null)
6414
+ getFileDiff: specDiff?.fileDiff ?? (() => null),
6415
+ envScrubMap
6287
6416
  });
6288
6417
  if (!printedHeader) {
6289
6418
  printedHeader = true;
@@ -6297,6 +6426,20 @@ function createFailureAnalysisPass(deps) {
6297
6426
  };
6298
6427
  } };
6299
6428
  }
6429
+ /**
6430
+ * The `${VAR}` values this spec resolved, for masking them out of the
6431
+ * classifier's prose. Read from `process.env` here rather than at run start
6432
+ * (where the live path builds its own): a profile is applied once per
6433
+ * invocation, so this is still what the spec ran against.
6434
+ */
6435
+ function specEnvScrubMap(spec, blocks) {
6436
+ if (spec === null) return [];
6437
+ try {
6438
+ return buildProseEnvScrubMap(spec, expandSpec(spec, { blocks }));
6439
+ } catch {
6440
+ return buildProseEnvScrubMap(spec, []);
6441
+ }
6442
+ }
6300
6443
  /** One classified spec's line in the failure-analysis block. */
6301
6444
  function printAnalysis(featureName, specName, analysis) {
6302
6445
  const pct = Math.round(analysis.confidence * 100);
@@ -6349,6 +6492,7 @@ async function analyzeExternalRows(rows, run) {
6349
6492
  readScript: () => readGeneratedTestSources(ref, deps.cwd),
6350
6493
  failureLog: row.failureLogExcerpt ?? "",
6351
6494
  specYaml: row.specYaml,
6495
+ parsedSpec: tryParseTestSpec(row.specYaml),
6352
6496
  target: row.target ?? "agent-browser",
6353
6497
  artifactsDir: readableArtifactsDir(ref, deps)
6354
6498
  });
@@ -9365,93 +9509,6 @@ function isStorageStateShape(state) {
9365
9509
  return typeof state === "object" && state !== null && Array.isArray(state.cookies) && Array.isArray(state.origins);
9366
9510
  }
9367
9511
  //#endregion
9368
- //#region src/runtime/env-scrub.ts
9369
- /**
9370
- * Build a list of `[envValue, "${VAR}"]` pairs for every `${VAR}` reference
9371
- * mentioned in the spec OR in any of its expanded (block-inlined) steps.
9372
- * Used at trace time to scrub recorded Claude-text outputs so a value the
9373
- * spec author intentionally threaded through `process.env` is preserved as
9374
- * `${VAR}` in `ir.json` rather than baked in as the concrete
9375
- * trace-time value.
9376
- *
9377
- * Why we walk `spec.steps` AND `expanded`:
9378
- * - `spec.steps` carries the spec's own `instruction` / `expected` + each
9379
- * include's raw `params` (which may themselves be `${ENV}` refs).
9380
- * - `expanded` carries the inlined block-internal steps, whose
9381
- * `instruction` / `expected` may *also* contain `${ENV}` refs that
9382
- * don't go through include params.
9383
- *
9384
- * Only refs whose env value is currently non-empty land in the map —
9385
- * scrubbing against an empty string would corrupt unrelated empty strings
9386
- * in the action stream. Names whose env is unset are returned via
9387
- * `unresolved` so the caller can warn the user.
9388
- *
9389
- * Longer values sort first so a `${SHORT}` whose value is a substring of a
9390
- * `${LONG}` value doesn't clobber the longer one.
9391
- *
9392
- * `title` is deliberately NOT scanned — it never reaches the recorded action
9393
- * stream.
9394
- */
9395
- function buildSpecEnvScrub(spec, expanded) {
9396
- const refNames = /* @__PURE__ */ new Set();
9397
- for (const step of spec.steps) if (isIncludeStep(step)) for (const v of Object.values(step.params ?? {})) collect(v, refNames);
9398
- else {
9399
- collect(step.instruction, refNames);
9400
- collect(step.expected, refNames);
9401
- }
9402
- for (const step of expanded) {
9403
- collect(step.instruction, refNames);
9404
- collect(step.expected, refNames);
9405
- }
9406
- const map = [];
9407
- const unresolved = [];
9408
- for (const name of refNames) {
9409
- const value = process.env[name];
9410
- if (typeof value === "string" && value.length > 0) map.push([value, "${" + name + "}"]);
9411
- else unresolved.push(name);
9412
- }
9413
- map.sort((a, b) => b[0].length - a[0].length);
9414
- return {
9415
- map,
9416
- unresolved
9417
- };
9418
- }
9419
- function collect(value, into) {
9420
- for (const name of iterEnvRefNames(value)) into.add(name);
9421
- }
9422
- /** Shorter than this, a value is no secret and matches inside ordinary words. */
9423
- const MIN_PROSE_SCRUB_LENGTH = 4;
9424
- /** Long enough to clear the length bar, still ordinary prose / JSON. */
9425
- const COMMON_PROSE_VALUES = new Set([
9426
- "true",
9427
- "false",
9428
- "null",
9429
- "none",
9430
- "undefined"
9431
- ]);
9432
- /**
9433
- * Scrub map for a live run, built like {@link buildSpecEnvScrub} but without
9434
- * the values that read as ordinary text (`"1"`, `"true"`). A live step records
9435
- * paragraphs of model prose, so replacing every occurrence of such a value
9436
- * would cost more meaning than it protects; record scrubs single command
9437
- * lines, where the same trade favours keeping them.
9438
- */
9439
- function buildLiveEnvScrubMap(spec, expanded) {
9440
- return buildSpecEnvScrub(spec, expanded).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
9441
- }
9442
- /**
9443
- * Replace every occurrence of an env value with its `${VAR}` placeholder in
9444
- * `text`. **Caller invariant**: the map must be sorted longest-value-first
9445
- * so a shorter value doesn't shadow a longer one that contains it as a
9446
- * substring. `buildSpecEnvScrub` upholds this; hand-built maps should too.
9447
- */
9448
- function scrubEnvValues(text, scrubMap) {
9449
- if (scrubMap.length === 0) return text;
9450
- let out = text;
9451
- for (const [value, placeholder] of scrubMap) if (out.includes(value)) out = out.replaceAll(value, placeholder);
9452
- return out;
9453
- }
9454
- //#endregion
9455
9512
  //#region src/claude/agent-browser-invoke.ts
9456
9513
  function agentBrowserInvokeBase(input) {
9457
9514
  return {
@@ -9749,6 +9806,7 @@ async function runLiveExecutor(input) {
9749
9806
  prompt: userPrompt,
9750
9807
  systemPrompt,
9751
9808
  model: input.model,
9809
+ envScrubMap: input.envScrubMap,
9752
9810
  relaxAbConstraints: true
9753
9811
  }, (msg) => {
9754
9812
  if (msg.type !== "assistant") return;
@@ -10297,7 +10355,7 @@ async function runOneSpec(args) {
10297
10355
  }
10298
10356
  const spec = parseTestSpec(specContent);
10299
10357
  const expanded = expandSpec(spec, { blocks: await loadAllBlocks(cwd) });
10300
- const envScrubMap = buildLiveEnvScrubMap(spec, expanded);
10358
+ const envScrubMap = buildProseEnvScrubMap(spec, expanded);
10301
10359
  meta("spec", spec.title);
10302
10360
  meta("steps", expanded.length);
10303
10361
  const includes = collectIncludedBlockNames(spec);
@@ -10359,6 +10417,7 @@ async function runOneSpec(args) {
10359
10417
  specName,
10360
10418
  runDir,
10361
10419
  specYaml: specContent,
10420
+ envScrubMap,
10362
10421
  result
10363
10422
  };
10364
10423
  } finally {
@@ -10417,7 +10476,8 @@ async function analyzeOneLiveFailure(r, diffProvider, auth, blocks, opts, cwd) {
10417
10476
  }, {
10418
10477
  ...opts.model ? { model: opts.model } : {},
10419
10478
  cwd,
10420
- getFileDiff: specDiff?.fileDiff ?? (() => null)
10479
+ getFileDiff: specDiff?.fileDiff ?? (() => null),
10480
+ envScrubMap: r.envScrubMap
10421
10481
  });
10422
10482
  const pct = Math.round(outcome.analysis.confidence * 100);
10423
10483
  const headline = outcome.analysis.headline.trim() || (outcome.analysis.reasoning.split("\n")[0] ?? "").trim();
@@ -12916,6 +12976,7 @@ async function executeRun(targets, opts) {
12916
12976
  });
12917
12977
  const triageUserPromptHash = triageUserPrompt ? hashTriageUserPrompt(triageUserPrompt) : null;
12918
12978
  const reportDir = resolveReportDir(opts.reportDir, cwd);
12979
+ const parsedBlocks = await loadAllBlocks(cwd);
12919
12980
  const analysisDeps = {
12920
12981
  diffProvider,
12921
12982
  auth: diffProvider ? driftAuthAvailable() : {
@@ -12924,7 +12985,8 @@ async function executeRun(targets, opts) {
12924
12985
  },
12925
12986
  cwd,
12926
12987
  reportDir,
12927
- blocks: await loadAvailableBlocks(cwd),
12988
+ blocks: projectAvailableBlocks(parsedBlocks),
12989
+ parsedBlocks,
12928
12990
  ...opts.model ? { model: opts.model } : {},
12929
12991
  ...opts.language ? { language: opts.language } : {},
12930
12992
  customPrompt,
@@ -13373,14 +13435,13 @@ function failedSpec(s) {
13373
13435
  * specs share). Degrades — never throws — when Claude auth or the git diff
13374
13436
  * aren't available. Caller writes report.json.
13375
13437
  */
13376
- async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass }) {
13377
- const allBlocks = await loadAllBlocks(cwd);
13438
+ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass, deps }) {
13378
13439
  const results = [];
13379
13440
  for (const s of summaries) {
13380
13441
  const assertions = collectAssertions(s);
13381
13442
  const specYaml = await tryReadSpecFile(s.featureName, s.specName, cwd);
13382
13443
  const parsedSpec = tryParseTestSpec(specYaml);
13383
- const stepDescriptions = buildStepDescriptions(parsedSpec, allBlocks);
13444
+ const stepDescriptions = buildStepDescriptions(parsedSpec, deps.parsedBlocks);
13384
13445
  const evidence = await loadEvidenceForSpec(s.evidenceDir, reportDir, stepDescriptions);
13385
13446
  const base = {
13386
13447
  feature: s.featureName,
@@ -13416,6 +13477,7 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass }
13416
13477
  readScript: () => readScriptSafe(s.scriptFile),
13417
13478
  failureLog,
13418
13479
  specYaml,
13480
+ parsedSpec,
13419
13481
  target: AGENT_BROWSER_TARGET
13420
13482
  });
13421
13483
  results.push({
@@ -14833,6 +14895,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
14833
14895
  runId: sessionName
14834
14896
  }),
14835
14897
  model,
14898
+ envScrubMap,
14836
14899
  onAbAction: ({ abAction, stepId, assertMarker }) => {
14837
14900
  const stepForCommand = stepTracker.fromCommand(stepId);
14838
14901
  const line = abAction === void 0 ? null : scrubEnvValues(abAction, envScrubMap);
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
+ record: "record";
38
39
  run: "run";
39
40
  drift: "drift";
40
- record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -621,9 +621,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
621
621
  declare const RunReportDataSchema: z.ZodObject<{
622
622
  schemaVersion: z.ZodLiteral<1>;
623
623
  kind: z.ZodDefault<z.ZodEnum<{
624
+ record: "record";
624
625
  run: "run";
625
626
  drift: "drift";
626
- record: "record";
627
627
  }>>;
628
628
  createdAt: z.ZodString;
629
629
  runId: z.ZodNullable<z.ZodString>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.28.0",
3
+ "version": "1.29.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.28.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {