ccqa 1.40.7 → 1.40.8

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
@@ -16360,6 +16360,53 @@ async function collectChangedSpecs(specs, opts) {
16360
16360
  };
16361
16361
  }
16362
16362
  //#endregion
16363
+ //#region src/run/measure-backfill.ts
16364
+ /**
16365
+ * `ccqa run --measure-backfill <n>`: keep the measured-reach edges alive.
16366
+ *
16367
+ * Selection (ADR-0024) consumes each spec's most recent measured reach, and
16368
+ * an edge expires after `EDGE_MAX_AGE_MS`. Nothing else re-measures: an
16369
+ * unmeasured spec answers `unknown`, `unknown` marks nothing due (ADR-0023),
16370
+ * and a suite can settle into a state where no run ever fires — the seed
16371
+ * deadlock. Appending a few unmeasured-or-aging specs to every selected run
16372
+ * breaks that loop and keeps the whole suite inside the freshness window
16373
+ * without a scheduled full sweep.
16374
+ */
16375
+ /**
16376
+ * Re-measure once an edge has spent half its lifetime. Half, not "expired":
16377
+ * a spec re-measured only after expiry answers `unknown` for the gap between
16378
+ * expiry and the next run, which is exactly the window this flag exists to
16379
+ * close.
16380
+ */
16381
+ const REMEASURE_AFTER_MS = EDGE_MAX_AGE_MS / 2;
16382
+ /**
16383
+ * Picks up to `limit` specs from `inventory` worth re-measuring: ones with no
16384
+ * edge at all first (they cost an `unknown` verdict today), then the oldest
16385
+ * measured ones. Specs already selected for this run are never doubled.
16386
+ */
16387
+ function chooseMeasureBackfill(inventory, selected, edges, limit, now) {
16388
+ const alreadyRunning = new Set(selected.map(specKey));
16389
+ const missing = [];
16390
+ const aging = [];
16391
+ for (const spec of inventory) {
16392
+ if (alreadyRunning.has(specKey(spec))) continue;
16393
+ const edge = edges.get(specKey(spec));
16394
+ if (edge === void 0) missing.push(spec);
16395
+ else if (now - edge.measuredAt > REMEASURE_AFTER_MS) aging.push({
16396
+ spec,
16397
+ measuredAt: edge.measuredAt
16398
+ });
16399
+ }
16400
+ aging.sort((a, b) => a.measuredAt - b.measuredAt);
16401
+ const specs = [...missing, ...aging.map((entry) => entry.spec)].slice(0, limit);
16402
+ const missingTaken = Math.min(missing.length, specs.length);
16403
+ return {
16404
+ specs,
16405
+ missing: missingTaken,
16406
+ aging: specs.length - missingTaken
16407
+ };
16408
+ }
16409
+ //#endregion
16363
16410
  //#region src/run/pipeline.ts
16364
16411
  async function resolveVitestConfig(cwd) {
16365
16412
  const userConfig = resolve(cwd, ".ccqa/vitest.config.ts");
@@ -16538,6 +16585,10 @@ async function executeRun(targets, opts) {
16538
16585
  if (rerunProfile !== null && hubCtx == null) throw new RunUsageError(needsHubConnection("--only-hub-rerun-needed"));
16539
16586
  if (opts.reportToHub && hubCtx == null) throw new RunUsageError(REPORT_TO_HUB_NEEDS_CONNECTION);
16540
16587
  if (opts.learnHubLivePrompt && hubCtx == null) throw new RunUsageError(needsHubConnection("--learn-hub-live-prompt"));
16588
+ if ((opts.measureBackfill ?? 0) > 0) {
16589
+ if (opts.coverage !== true) throw new RunUsageError("--measure-backfill does nothing without --coverage — there is no measurement to keep fresh");
16590
+ if (!filtering) throw new RunUsageError("--measure-backfill needs a selection flag (--only-hub-rerun-needed / --only-affected-by); an explicit spec list runs exactly what was asked");
16591
+ }
16541
16592
  let coverageInbox;
16542
16593
  if (opts.coverageInbox === "hub") {
16543
16594
  if (opts.coverage !== true) throw new RunUsageError("--coverage-inbox hub does nothing without --coverage — there is no measurement to stream");
@@ -16586,6 +16637,7 @@ async function executeRun(targets, opts) {
16586
16637
  };
16587
16638
  const enumerateAll = () => listAllSpecsWithSpecFile(cwd);
16588
16639
  let specs = dedupeSpecs((await Promise.all((targets.length ? targets : [void 0]).map((t) => resolveSpecTargets(t, enumerateAll, cwd)))).flat());
16640
+ const inventory = specs;
16589
16641
  if (filtering) {
16590
16642
  const before = specs.length;
16591
16643
  let inProgress = 0;
@@ -16616,6 +16668,14 @@ async function executeRun(targets, opts) {
16616
16668
  throw new RunUsageError("nothing was selected and no spec was cleared to run: exiting non-zero rather than reporting a green run that verified nothing");
16617
16669
  }
16618
16670
  }
16671
+ if (filtering && (opts.measureBackfill ?? 0) > 0 && hubCtx != null) {
16672
+ const edges = await loadCoverageEdges(hubCtx);
16673
+ const pick = chooseMeasureBackfill(inventory, specs, edges, opts.measureBackfill ?? 0, Date.now());
16674
+ if (pick.specs.length > 0) {
16675
+ specs = [...specs, ...pick.specs];
16676
+ meta("measure-backfill", `${pick.specs.length} spec(s) appended (${pick.missing} unmeasured / ${pick.aging} aging)`);
16677
+ }
16678
+ }
16619
16679
  if (specs.length === 0) {
16620
16680
  warn("no specs to run");
16621
16681
  return {
@@ -17570,7 +17630,11 @@ const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command(
17570
17630
  }, "text").option("--report-to-hub", "Incrementally push the run report to the hub as the run progresses (open → patch per spec → finalize). Requires --hub-url/--hub-token (or CCQA_HUB_URL/CCQA_HUB_TOKEN). Without it, hub credentials are used only to fetch variables/sessions/prompts, not to push.").option("--coverage", "Measure what each spec actually reached in the application under test, and record it on the spec's report row. Needs a `coverage:` block in .ccqa/config.yaml naming the instrumented origins the spec cookie may go to. The browser half attaches to the target's browser from outside (nothing is emitted into generated tests; needs node 22+); the server half needs the application running with ccqa-tools.").option("--coverage-inbox <where>", "With --coverage: where the measurement's two sides meet. 'local' (default) binds a loopback inbox on this machine for the run's duration; 'hub' appends every event to the hub's durable coverage inbox instead — nothing listens on the runner, report.json carries no coverage, and the hub resolves per-spec results on read (requires a hub connection).", (raw) => {
17571
17631
  if (COVERAGE_INBOX_MODES.includes(raw)) return raw;
17572
17632
  throw new Error(`--coverage-inbox must be one of ${COVERAGE_INBOX_MODES.join(" | ")}`);
17573
- }, "local").optionsGroup("Learning:").option("--learn-hub-live-prompt", "(live only) After the run finishes, ask Claude to refresh the \"live.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(async (targets, opts) => {
17633
+ }, "local").option("--measure-backfill <n>", "With --coverage and a selection flag (--only-hub-rerun-needed / --only-affected-by): also run up to <n> specs whose measured reach is missing or past half its freshness window, unmeasured-first then oldest-first. This is what keeps selection's edges alive without it an unmeasured spec answers `unknown` forever, because `unknown` marks nothing due and nothing else re-measures.", (raw) => {
17634
+ const n = Number(raw);
17635
+ if (!Number.isInteger(n) || n < 0) throw new Error("--measure-backfill must be a non-negative integer");
17636
+ return n;
17637
+ }).optionsGroup("Learning:").option("--learn-hub-live-prompt", "(live only) After the run finishes, ask Claude to refresh the \"live.agent\" prompt on the hub from a summary of the run. Requires a hub connection.").optionsGroup("Environment and connection:").option("--cwd <path>", "Working directory containing the .ccqa/ tree (monorepo support). Defaults to the current directory.").option("--project <name>", "Project name for the hub. Defaults to the current directory's name.")))).action(async (targets, opts) => {
17574
17638
  await runCliAction(targets, opts);
17575
17639
  });
17576
17640
  /** Parse --concurrency: a positive integer. Rejects 0, negatives, non-integers. */
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.40.7",
3
+ "version": "1.40.8",
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.40.7",
3
+ "version": "1.40.8",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {