ccqa 1.59.0 → 1.60.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
@@ -14836,6 +14836,12 @@ function tail(output) {
14836
14836
  const trimmed = output.trim();
14837
14837
  return trimmed.length <= OUTPUT_TAIL_CHARS ? trimmed : trimmed.slice(-OUTPUT_TAIL_CHARS);
14838
14838
  }
14839
+ const LOGGED_TAIL_LINES = 40;
14840
+ /** The end of an output tail that is already scrubbed, one log line per line. */
14841
+ function logOutputTail(scrubbed, write) {
14842
+ if (scrubbed === "") return;
14843
+ for (const line of scrubbed.split("\n").slice(-LOGGED_TAIL_LINES)) write(` | ${line}`);
14844
+ }
14839
14845
  /**
14840
14846
  * Where new support files may be created.
14841
14847
  *
@@ -15027,16 +15033,19 @@ async function runVerificationLoop(p, state) {
15027
15033
  failing = run;
15028
15034
  }
15029
15035
  }
15036
+ const outputTail = scrubEnvValues(tail(failing.output), outputScrub);
15030
15037
  if (attempt >= maxRetries) {
15031
15038
  warn(`verification still failing after ${maxRetries} fix attempt(s) — generated files kept`);
15039
+ logOutputTail(outputTail, warn);
15032
15040
  await clearCaseRun(p.ctx.ref);
15033
15041
  return finish(false);
15034
15042
  }
15035
15043
  fix(`verification failed (exit ${failing.exitCode}) — requesting a fix (${attempt + 1}/${maxRetries})`);
15044
+ logOutputTail(outputTail, fix);
15036
15045
  const fixPrompt = buildLlmFixPrompt({
15037
15046
  targetId: p.target,
15038
15047
  command: failing.command,
15039
- outputTail: scrubEnvValues(tail(failing.output), outputScrub),
15048
+ outputTail,
15040
15049
  files: [...state.entries()].map(([path, f]) => ({
15041
15050
  path,
15042
15051
  contents: f.contents,
@@ -29038,12 +29047,12 @@ function releaseAll(locks, holder) {
29038
29047
  }
29039
29048
  //#endregion
29040
29049
  //#region src/hub/core/audit-need.ts
29041
- function auditNeed(drift, key, range) {
29042
- const entry = drift.specs[key];
29050
+ function auditNeed(drift, spec, range, deployTimes) {
29051
+ const entry = drift.specs[spec.key];
29043
29052
  if (!entry) return { because: "neverAudited" };
29044
- const since = freshness(entry.gitHead, key, range);
29053
+ const since = freshness(entry.gitHead, spec.key, range);
29045
29054
  switch (since.kind) {
29046
- case "current": return { because: "current" };
29055
+ case "current": return specMovedSince(spec.changedAt, entry.gitHead, entry.at, deployTimes) ? { because: "deployReached" } : { because: "current" };
29047
29056
  case "touched": return { because: "deployReached" };
29048
29057
  case "unanswerable": return {
29049
29058
  because: "cannotTell",
@@ -29052,13 +29061,52 @@ function auditNeed(drift, key, range) {
29052
29061
  default: throw new Error(`unhandled freshness: ${String(since)}`);
29053
29062
  }
29054
29063
  }
29064
+ /**
29065
+ * When each deployed commit reached the environment. A baseline read at that
29066
+ * commit cannot have seen anything committed after it was deployed.
29067
+ */
29068
+ function deployedAt(log) {
29069
+ const out = /* @__PURE__ */ new Map();
29070
+ for (const entry of log.entries) out.set(entry.sha, entry.at);
29071
+ return out;
29072
+ }
29073
+ /**
29074
+ * Has the spec moved since the baseline was taken?
29075
+ *
29076
+ * A verdict is a claim about a (spec, product) pair, so either side moving
29077
+ * invalidates it. The deploy log covers the product side; this covers the
29078
+ * other one. Without it a spec repaired and merged stays `needsRepair` until
29079
+ * a deploy happens to reach it, and a run that passed against the previous
29080
+ * spec keeps answering `verified` for the new one.
29081
+ *
29082
+ * Compared against when the baseline commit was *deployed*, not when the audit
29083
+ * or run happened: the tree read at that commit predates its deployment, so an
29084
+ * edit after it is definitely not in it. Falls back to the baseline's own
29085
+ * timestamp when the log cannot place the commit.
29086
+ *
29087
+ * One-directional. A later edit time proves the baseline is stale; an earlier
29088
+ * one proves nothing, and this answers false rather than guessing.
29089
+ */
29090
+ function specMovedSince(changedAt, baselineSha, baselineAt, deployTimes) {
29091
+ if (!changedAt) return null;
29092
+ return isLater(changedAt, baselineSha && deployTimes.get(baselineSha) || baselineAt) ? changedAt : null;
29093
+ }
29094
+ /**
29095
+ * Compared as instants, never as strings: a spec's edit time carries its
29096
+ * committer's offset, and `+09:00` sorts after `Z` for an earlier moment.
29097
+ * An unparseable side answers false, the same as a missing one.
29098
+ */
29099
+ function isLater(a, b) {
29100
+ return Date.parse(a) > Date.parse(b);
29101
+ }
29055
29102
  /** True for every answer but `current`. */
29056
29103
  function needsAudit(need) {
29057
29104
  return need.because !== "current";
29058
29105
  }
29059
29106
  function computeAuditNeed(input) {
29060
29107
  const range = buildRange(input.log, input.touchIndex);
29061
- return Object.fromEntries(input.specs.map((spec) => [spec.key, heldBy(input.locks, spec.key, input.now) ? { because: "held" } : auditNeed(input.drift, spec.key, range)]));
29108
+ const deployTimes = deployedAt(input.log);
29109
+ return Object.fromEntries(input.specs.map((spec) => [spec.key, heldBy(input.locks, spec.key, input.now) ? { because: "held" } : auditNeed(input.drift, spec, range, deployTimes)]));
29062
29110
  }
29063
29111
  //#endregion
29064
29112
  //#region src/drift/audit-selection.ts
@@ -29620,7 +29668,8 @@ async function readChunk(cwd, paths, byRelPath, out) {
29620
29668
  let when = "";
29621
29669
  for (const line of stdout.split("\n")) {
29622
29670
  if (line.startsWith("\0")) {
29623
- when = line.slice(1).trim();
29671
+ const ms = Date.parse(line.slice(1).trim());
29672
+ when = Number.isNaN(ms) ? "" : new Date(ms).toISOString();
29624
29673
  continue;
29625
29674
  }
29626
29675
  const abs = byRelPath.get(line.trim());
@@ -32244,36 +32293,6 @@ function createGetCoverageEdgesHandler(config) {
32244
32293
  }
32245
32294
  //#endregion
32246
32295
  //#region src/hub/core/rerun.ts
32247
- /**
32248
- * When each deployed commit reached the environment. A baseline read at that
32249
- * commit cannot have seen anything committed after it was deployed.
32250
- */
32251
- function deployedAt(log) {
32252
- const out = /* @__PURE__ */ new Map();
32253
- for (const entry of log.entries) out.set(entry.sha, entry.at);
32254
- return out;
32255
- }
32256
- /**
32257
- * Has the spec moved since the baseline was taken?
32258
- *
32259
- * A verdict is a claim about a (spec, product) pair, so either side moving
32260
- * invalidates it. The deploy log covers the product side; this covers the
32261
- * other one. Without it a spec repaired and merged stays `needsRepair` until
32262
- * a deploy happens to reach it, and a run that passed against the previous
32263
- * spec keeps answering `verified` for the new one.
32264
- *
32265
- * Compared against when the baseline commit was *deployed*, not when the audit
32266
- * or run happened: the tree read at that commit predates its deployment, so an
32267
- * edit after it is definitely not in it. Falls back to the baseline's own
32268
- * timestamp when the log cannot place the commit.
32269
- *
32270
- * One-directional. A later edit time proves the baseline is stale; an earlier
32271
- * one proves nothing, and this answers false rather than guessing.
32272
- */
32273
- function specMovedSince(changedAt, baselineSha, baselineAt, deployTimes) {
32274
- if (!changedAt) return null;
32275
- return changedAt > (baselineSha && deployTimes.get(baselineSha) || baselineAt) ? changedAt : null;
32276
- }
32277
32296
  function computeRerun(input) {
32278
32297
  const { specs, ledger, log, touchIndex, drift, locks, attestations, dismissals, now } = input;
32279
32298
  const range = buildRange(log, touchIndex);
@@ -32285,7 +32304,7 @@ function computeRerun(input) {
32285
32304
  lastGreen: ledger.green[spec.key] ?? null,
32286
32305
  lastRed: ledger.red[spec.key] ?? null
32287
32306
  };
32288
- let audit = auditState(drift, spec.key, range);
32307
+ let audit = auditState(drift, spec, range, deployTimes);
32289
32308
  let execution = executionState(coords, (sha) => freshness(sha, spec.key, range));
32290
32309
  const driftEntry = drift.specs[spec.key];
32291
32310
  const dismissal = dismissals.specs[spec.key];
@@ -32293,7 +32312,6 @@ function computeRerun(input) {
32293
32312
  if (dismissed) audit = { audit: "clean" };
32294
32313
  const auditMoved = specMovedSince(spec.changedAt, driftEntry?.gitHead ?? null, driftEntry?.at ?? "", deployTimes);
32295
32314
  const runMoved = specMovedSince(spec.changedAt, coords.lastRun?.deployedSha ?? null, coords.lastRun?.at ?? "", deployTimes);
32296
- if (auditMoved && audit.audit !== "due") audit = { audit: "due" };
32297
32315
  if (runMoved && execution.execution === "passed") execution = { execution: "stale" };
32298
32316
  const manualState = readAttestation(attestations.specs[spec.key], spec, coords, range, log, deployTimes);
32299
32317
  const lapsedOverRed = manualState?.kind === "lapsed" && manualState.because !== "newerRed";
@@ -32338,7 +32356,7 @@ function computeRerun(input) {
32338
32356
  */
32339
32357
  function readAttestation(attest, spec, { lastRun, lastRed }, range, log, deployTimes) {
32340
32358
  if (!attest) return null;
32341
- const newerRed = lastRed !== null && lastRed.at > attest.at;
32359
+ const newerRed = lastRed !== null && isLater(lastRed.at, attest.at);
32342
32360
  if (newerRed && lastRed.runId === lastRun?.runId) return {
32343
32361
  kind: "lapsed",
32344
32362
  attest,
@@ -32384,8 +32402,8 @@ function readAttestation(attest, spec, { lastRun, lastRed }, range, log, deployT
32384
32402
  * rule out is treated as having landed (ADR-0014). The hole is kept as an
32385
32403
  * annotation so the answer stays explicable.
32386
32404
  */
32387
- function auditState(drift, key, range) {
32388
- const need = auditNeed(drift, key, range);
32405
+ function auditState(drift, spec, range, deployTimes) {
32406
+ const need = auditNeed(drift, spec, range, deployTimes);
32389
32407
  switch (need.because) {
32390
32408
  case "neverAudited":
32391
32409
  case "deployReached": return { audit: "due" };
@@ -32394,7 +32412,7 @@ function auditState(drift, key, range) {
32394
32412
  ...need.reason ? { auditAssumedReached: need.reason } : {}
32395
32413
  };
32396
32414
  case "current": {
32397
- const label = drift.specs[key].label;
32415
+ const label = drift.specs[spec.key].label;
32398
32416
  if (label === null) return { audit: "clean" };
32399
32417
  if (label === "UNKNOWN") return { audit: "undecided" };
32400
32418
  if (label === "PRODUCT_BUG" || label === "ENVIRONMENT") return { audit: "clean" };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.59.0",
3
+ "version": "1.60.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.59.0",
3
+ "version": "1.60.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {