ccqa 1.16.0 → 1.16.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/dist/bin/ccqa.mjs CHANGED
@@ -8828,7 +8828,11 @@ const deployRecord = new Command("record").description("Tell the hub what a depl
8828
8828
  meta("previous", previous ? previous.slice(0, 12) : "(none)");
8829
8829
  meta("changed paths", changedPaths === null ? "(not reported)" : String(changedPaths.length));
8830
8830
  if (opts.select) meta("selection", describeSelection(selection, diff !== null));
8831
- if (entry.gapBefore) warn("this deploy does not chain onto the log head, so a gap is recorded — specs whose baseline sits behind it report 'unknown' rather than 'not needed'");
8831
+ if (entry.gapBefore) warn("this deploy does not chain onto the log head, so a gap is recorded — specs whose baseline sits behind it report 'unanswerable' rather than 'verified'");
8832
+ if (selection && !entry.hasSelection) {
8833
+ error("the hub recorded this deploy but could not store its spec selection — the range answers 'unanswerable' from here on, and nothing fills it in later. Re-record this deploy.");
8834
+ process.exit(1);
8835
+ }
8832
8836
  info(`recorded deploy #${entry.index}`);
8833
8837
  }));
8834
8838
  /**
@@ -15429,6 +15433,10 @@ async function runAudit(specPath, opts) {
15429
15433
  error("--only-hub-audit-needed and an explicit spec id cannot be combined; it only applies to a full sweep");
15430
15434
  process.exit(2);
15431
15435
  }
15436
+ if (opts.onlyHubAuditNeeded && opts.onlyAffectedBy) {
15437
+ error("--only-hub-audit-needed and --only-affected-by cannot be combined: a spec the hub says needs auditing that the diff drops is never audited, so it stays due forever. Pick the one that matches the job — the hub answer after a deploy, the diff on a pull request.");
15438
+ process.exit(2);
15439
+ }
15432
15440
  let hub = null;
15433
15441
  let hubProject = null;
15434
15442
  let holder = null;
@@ -15445,7 +15453,7 @@ async function runAudit(specPath, opts) {
15445
15453
  });
15446
15454
  }
15447
15455
  let targets = await collectTargets(specPath, cwd);
15448
- if (targets.length === 0) exitWithNoSpecs(format, "no test specs found under .ccqa/features/");
15456
+ if (targets.length === 0) exitWithNoSpecs(format, "noSpecsFound", "no test specs found under .ccqa/features/");
15449
15457
  if (format === "text") {
15450
15458
  header("audit", specPath ?? `${targets.length} spec${targets.length > 1 ? "s" : ""}`);
15451
15459
  if (opts.cwd) meta("cwd", cwd);
@@ -15462,12 +15470,12 @@ async function runAudit(specPath, opts) {
15462
15470
  meta("hub", selection.summary);
15463
15471
  meta("scoped", `${targets.length} of ${total} spec${total > 1 ? "s" : ""}`);
15464
15472
  }
15465
- if (targets.length === 0) exitWithNoSpecs(format, "every spec has been audited since the last deploy that reached it");
15473
+ if (targets.length === 0) exitWithNoSpecs(format, "allCurrent", "every spec has been audited since the last deploy that reached it");
15466
15474
  holder = randomUUID();
15467
15475
  const claimed = await claimSpecs(hub, hubProject, opts.hubProfile, targets, holder);
15468
15476
  if (claimed.length < targets.length && format === "text") meta("held-elsewhere", `${targets.length - claimed.length} spec(s) another job is auditing`);
15469
15477
  targets = claimed;
15470
- if (targets.length === 0) exitWithNoSpecs(format, "every spec that needs auditing is already being audited by another job");
15478
+ if (targets.length === 0) exitWithNoSpecs(format, "allHeld", "every spec that needs auditing is already being audited by another job");
15471
15479
  }
15472
15480
  let baseRef = null;
15473
15481
  if (opts.onlyAffectedBy) {
@@ -15481,7 +15489,7 @@ async function runAudit(specPath, opts) {
15481
15489
  targets = selection.specs;
15482
15490
  baseRef = selection.base.ref;
15483
15491
  if (format === "text") meta("scoped", `${targets.length} of ${total} spec${total > 1 ? "s" : ""}`);
15484
- if (targets.length === 0) exitWithNoSpecs(format, "no specs intersect the changed file set; nothing to check");
15492
+ if (targets.length === 0) exitWithNoSpecs(format, "noDiffIntersection", "no specs intersect the changed file set; nothing to check");
15485
15493
  }
15486
15494
  const blocks = await loadAvailableBlocks(cwd);
15487
15495
  try {
@@ -15592,10 +15600,14 @@ async function pushDriftResults(args, resolveHub = resolveHubClient) {
15592
15600
  throw err;
15593
15601
  }
15594
15602
  }
15595
- function exitWithNoSpecs(format, message) {
15603
+ function exitWithNoSpecs(format, reason, message) {
15596
15604
  reportCost();
15597
- if (format === "json") process.stdout.write(`${JSON.stringify({ specs: [] }, null, 2)}\n`);
15605
+ if (format === "json") process.stdout.write(`${JSON.stringify({
15606
+ specs: [],
15607
+ skipped: reason
15608
+ }, null, 2)}\n`);
15598
15609
  else if (format === "text") info(message);
15610
+ else if (format === "github" && reason === "noSpecsFound") process.stdout.write(`::warning::${message}\n`);
15599
15611
  process.exit(0);
15600
15612
  }
15601
15613
  async function collectTargets(specPath, cwd) {
@@ -17296,27 +17308,33 @@ function createRecordDeployHandler(storage) {
17296
17308
  ...ref ? { ref } : {},
17297
17309
  ...runUrl ? { runUrl } : {},
17298
17310
  changedPaths: changedPaths ?? null,
17299
- hasSelection: selection !== void 0
17311
+ hasSelection: false
17312
+ });
17313
+ const folded = selection !== void 0 && await foldIntoTouchIndex(storage, project, profile, entry, selection);
17314
+ if (folded) await storage.deploys.confirmSelection(project, profile, entry.index);
17315
+ sendJson(ctx.res, 201, {
17316
+ ...entry,
17317
+ hasSelection: folded === true
17300
17318
  });
17301
- if (selection !== void 0) await foldIntoTouchIndex(storage, project, profile, entry, selection);
17302
- sendJson(ctx.res, 201, entry);
17303
17319
  };
17304
17320
  }
17305
17321
  /**
17306
17322
  * Record what this deploy's selection decided, so a later read can answer each
17307
17323
  * spec's own range with two integer comparisons.
17308
17324
  *
17309
- * Deliberately after the append, and it does not fail the request: the log is
17310
- * the record of what shipped and has to land even if the fold cannot. A lost
17311
- * fold shows up as specs reporting `unknown` rather than as a wrong `notNeeded`
17312
- * the entry is already marked `hasSelection`, so the read side knows the
17313
- * range is unresolved either way.
17325
+ * It does not fail the request: the log is the record of what shipped and has
17326
+ * to land even if the fold cannot. What a lost fold must not do is *look* like
17327
+ * a recorded one so the caller only marks the entry `hasSelection` when this
17328
+ * returns true, and the range reads as unresolved otherwise. The client is told
17329
+ * by the entry it gets back.
17314
17330
  */
17315
17331
  async function foldIntoTouchIndex(storage, project, profile, entry, selection) {
17316
17332
  try {
17317
17333
  await storage.deploys.updateTouchIndex(project, profile, (current) => foldTouchIndex(current, entry, selection));
17334
+ return true;
17318
17335
  } catch (err) {
17319
17336
  console.error(`hub: touch-index fold failed for deploy "${entry.sha}" of "${project}/${profile}": ${errMsg(err)}`);
17337
+ return false;
17320
17338
  }
17321
17339
  }
17322
17340
  /** GET /api/v1/projects/:project/deploys?profile=&limit= — the retained log, oldest first. */
@@ -23434,6 +23452,18 @@ function createFileDeployStore(root) {
23434
23452
  const log = await updateJson(deployLogPath(root, project, profile), (current) => appendDeploy(current, input));
23435
23453
  return log.entries[log.entries.length - 1];
23436
23454
  },
23455
+ async confirmSelection(project, profile, index) {
23456
+ await updateJson(deployLogPath(root, project, profile), (current) => {
23457
+ const log = current ?? emptyDeployLog();
23458
+ return {
23459
+ ...log,
23460
+ entries: log.entries.map((e) => e.index === index ? {
23461
+ ...e,
23462
+ hasSelection: true
23463
+ } : e)
23464
+ };
23465
+ });
23466
+ },
23437
23467
  getLog: readLog,
23438
23468
  async head(project, profile) {
23439
23469
  const { entries } = await readLog(project, profile);
@@ -388,8 +388,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
388
388
  title: z.ZodNullable<z.ZodString>;
389
389
  target: z.ZodOptional<z.ZodString>;
390
390
  mode: z.ZodOptional<z.ZodEnum<{
391
- live: "live";
392
391
  deterministic: "deterministic";
392
+ live: "live";
393
393
  }>>;
394
394
  status: z.ZodEnum<{
395
395
  passed: "passed";
@@ -574,8 +574,8 @@ declare const RunReportDataSchema: z.ZodObject<{
574
574
  title: z.ZodNullable<z.ZodString>;
575
575
  target: z.ZodOptional<z.ZodString>;
576
576
  mode: z.ZodOptional<z.ZodEnum<{
577
- live: "live";
578
577
  deterministic: "deterministic";
578
+ live: "live";
579
579
  }>>;
580
580
  status: z.ZodEnum<{
581
581
  passed: "passed";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.16.0",
3
+ "version": "1.16.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.16.0",
3
+ "version": "1.16.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {