ccqa 1.31.1 → 1.31.3

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
@@ -15579,10 +15579,11 @@ async function runRecord(specPath, opts) {
15579
15579
  let sealed = true;
15580
15580
  let tracingStep;
15581
15581
  let abortCause;
15582
+ let traceFailureNote;
15582
15583
  const teardown = createRunTeardown();
15583
15584
  teardown.onFinalize(async () => {
15584
15585
  if (!push) return;
15585
- sealed = await sealRecordPush(push, featureName, specName, recorded, abortCause !== void 0 ? abortNote(abortCause, tracingStep) : void 0);
15586
+ sealed = await sealRecordPush(push, featureName, specName, recorded, abortCause !== void 0 ? abortNote(abortCause, tracingStep) : traceFailureNote);
15586
15587
  });
15587
15588
  const disposeSignalHandlers = installTeardownSignalHandlers(teardown, (sig) => {
15588
15589
  abortCause = `terminated by signal (${sig})`;
@@ -15609,6 +15610,7 @@ async function runRecord(specPath, opts) {
15609
15610
  }
15610
15611
  });
15611
15612
  tracingStep = void 0;
15613
+ if (traceResult.status !== "passed") traceFailureNote = "trace finished FAILED — a step did not complete, so the recording does not demonstrate the spec";
15612
15614
  blank();
15613
15615
  await learnFromTrace({
15614
15616
  enabled: opts.learnHubTracePrompt === true,
@@ -15633,14 +15635,17 @@ async function runRecord(specPath, opts) {
15633
15635
  } finally {
15634
15636
  await releaseLock();
15635
15637
  }
15636
- recorded = generated;
15638
+ recorded = generated && traceFailureNote === void 0;
15637
15639
  } finally {
15638
15640
  if (deadline !== void 0) clearTimeout(deadline);
15639
15641
  await teardown.run();
15640
15642
  disposeSignalHandlers();
15641
15643
  }
15642
15644
  if (!sealed) process.exit(2);
15643
- if (!recorded) process.exit(1);
15645
+ if (!recorded) {
15646
+ if (traceFailureNote !== void 0) error(traceFailureNote);
15647
+ process.exit(1);
15648
+ }
15644
15649
  }
15645
15650
  /** `--timeout <seconds>`: a positive whole number of seconds. */
15646
15651
  function parseTimeoutSeconds(raw) {
@@ -16487,19 +16492,42 @@ async function fetchAuditNeed(ctx, profile) {
16487
16492
  throw new RunUsageError(`${FLAG}: could not ask the hub which specs need auditing: ${errMessage(err)}`);
16488
16493
  }
16489
16494
  }
16495
+ /**
16496
+ * Specs whose drift-ledger entry is still open. The hub's audit-need answer is
16497
+ * deploy-based, and a merged fix changes only the spec tree — no deploy lands
16498
+ * on the spec, so the hub would never call it due again and the entry would
16499
+ * stay open forever. A drifted spec is due until the audit itself clears it.
16500
+ * Unreadable ledger degrades to the deploy-based answer alone: the sweep must
16501
+ * not die over the supplementary question.
16502
+ */
16503
+ async function fetchStillDrifted(ctx) {
16504
+ try {
16505
+ const ledger = await ctx.hub.getDriftLedger(ctx.project);
16506
+ return new Set(Object.entries(ledger.specs ?? {}).filter(([, entry]) => entry.label != null).map(([key]) => key));
16507
+ } catch {
16508
+ return /* @__PURE__ */ new Set();
16509
+ }
16510
+ }
16490
16511
  /** Worst-known-first, so the line leads with what has never been looked at. */
16491
16512
  const SUMMARY_ORDER = rankedOrder({
16492
16513
  neverAudited: 0,
16493
- cannotTell: 1,
16494
- deployReached: 2,
16495
- held: 3,
16496
- current: 4
16514
+ stillDrifted: 1,
16515
+ cannotTell: 2,
16516
+ deployReached: 3,
16517
+ held: 4,
16518
+ current: 5
16497
16519
  });
16498
- function selectSpecsNeedingAudit(targets, report) {
16520
+ function selectSpecsNeedingAudit(targets, report, stillDrifted = /* @__PURE__ */ new Set()) {
16499
16521
  const counts = /* @__PURE__ */ new Map();
16500
16522
  const selected = [];
16501
16523
  for (const target of targets) {
16502
- const need = report.specs[specKey(target)] ?? { because: "neverAudited" };
16524
+ const key = specKey(target);
16525
+ if (stillDrifted.has(key)) {
16526
+ counts.set("stillDrifted", (counts.get("stillDrifted") ?? 0) + 1);
16527
+ selected.push(target);
16528
+ continue;
16529
+ }
16530
+ const need = report.specs[key] ?? { because: "neverAudited" };
16503
16531
  counts.set(need.because, (counts.get(need.because) ?? 0) + 1);
16504
16532
  if (needsAudit(need)) selected.push(target);
16505
16533
  }
@@ -16511,7 +16539,7 @@ function selectSpecsNeedingAudit(targets, report) {
16511
16539
  //#endregion
16512
16540
  //#region src/cli/audit.ts
16513
16541
  const DEFAULT_CONCURRENCY = 3;
16514
- const auditCommand = addProfileOption(addLanguageOption(new Command("audit").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Read each spec against the code it describes and report where the two have drifted. Static: no browser is run, so this is the cheap check to put in front of `ccqa run`.").optionsGroup("Which specs to audit:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Costs one model call; specs it cannot decide are audited rather than skipped.").option("--only-hub-audit-needed", "Only specs the hub says a deploy has landed on since the audit last read them. A spec that was never audited is always included, and one the hub cannot answer for is audited rather than skipped. No git diff involved. Requires a hub connection and --hub-profile.").optionsGroup("How to run it:").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").optionsGroup("What to do with the results:").option("--report-format <fmt>", "Output format: text | json | github", "text").option("--report-to-hub", "Push the result to a ccqa hub as a run (kind: drift), which is what updates the drift ledger. A spec it finds drifted answers `needsRepair` to `ccqa run --only-hub-rerun-needed`, and is not run until a person repairs it.").option("--exit-on <level>", "Exit non-zero on this severity or higher: warn | error", "error").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption))).action(withUsageErrors(async (specPath, opts) => {
16542
+ const auditCommand = addProfileOption(addLanguageOption(new Command("audit").argument("[feature/spec]", "Optional spec id. If omitted, every spec under .ccqa/features/ is checked.").description("Read each spec against the code it describes and report where the two have drifted. Static: no browser is run, so this is the cheap check to put in front of `ccqa run`.").optionsGroup("Which specs to audit:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Costs one model call; specs it cannot decide are audited rather than skipped.").option("--only-hub-audit-needed", "Only specs the hub says a deploy has landed on since the audit last read them. A spec that was never audited is always included, one the hub cannot answer for is audited rather than skipped, and one whose drift entry is still open is always re-audited — a merged fix changes only the spec tree, which no deploy answer covers. No git diff involved. Requires a hub connection and --hub-profile.").optionsGroup("How to run it:").option("--concurrency <n>", `Parallel spec checks (default: ${DEFAULT_CONCURRENCY})`).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").optionsGroup("What to do with the results:").option("--report-format <fmt>", "Output format: text | json | github", "text").option("--report-to-hub", "Push the result to a ccqa hub as a run (kind: drift), which is what updates the drift ledger. A spec it finds drifted answers `needsRepair` to `ccqa run --only-hub-rerun-needed`, and is not run until a person repairs it.").option("--exit-on <level>", "Exit non-zero on this severity or higher: warn | error", "error").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory used as both the .ccqa root and the codebase Claude reads. Useful for monorepos. Defaults to process.cwd().").option("--project <name>", "Logical project name for the pushed run. Defaults to the current directory's name.").option(...hubUrlOption).option(...hubTokenOption).option(...hubHeaderOption))).action(withUsageErrors(async (specPath, opts) => {
16515
16543
  await withCostReporting("audit", () => runAudit(specPath, opts));
16516
16544
  }));
16517
16545
  async function runAudit(specPath, opts) {
@@ -16555,11 +16583,12 @@ async function runAudit(specPath, opts) {
16555
16583
  }
16556
16584
  if (opts.onlyHubAuditNeeded) {
16557
16585
  const total = targets.length;
16558
- const report = await fetchAuditNeed({
16586
+ const ctx = {
16559
16587
  hub,
16560
16588
  project: hubProject
16561
- }, opts.hubProfile);
16562
- const selection = selectSpecsNeedingAudit(targets, report);
16589
+ };
16590
+ const [report, stillDrifted] = await Promise.all([fetchAuditNeed(ctx, opts.hubProfile), fetchStillDrifted(ctx)]);
16591
+ const selection = selectSpecsNeedingAudit(targets, report, stillDrifted);
16563
16592
  targets = selection.selected;
16564
16593
  if (format === "text") {
16565
16594
  meta("hub", selection.summary);
@@ -481,8 +481,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
481
481
  title: z.ZodNullable<z.ZodString>;
482
482
  target: z.ZodOptional<z.ZodString>;
483
483
  mode: z.ZodOptional<z.ZodEnum<{
484
- deterministic: "deterministic";
485
484
  live: "live";
485
+ deterministic: "deterministic";
486
486
  }>>;
487
487
  status: z.ZodEnum<{
488
488
  passed: "passed";
@@ -665,8 +665,8 @@ declare const RunReportDataSchema: z.ZodObject<{
665
665
  title: z.ZodNullable<z.ZodString>;
666
666
  target: z.ZodOptional<z.ZodString>;
667
667
  mode: z.ZodOptional<z.ZodEnum<{
668
- deterministic: "deterministic";
669
668
  live: "live";
669
+ deterministic: "deterministic";
670
670
  }>>;
671
671
  status: z.ZodEnum<{
672
672
  passed: "passed";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.31.1",
3
+ "version": "1.31.3",
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.31.1",
3
+ "version": "1.31.3",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {