ccqa 1.49.0 → 1.50.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
@@ -1502,6 +1502,8 @@ const ReportSpecResultSchema = z.object({
1502
1502
  failed: z.number()
1503
1503
  }).nullable(),
1504
1504
  durationMs: z.number().nullable(),
1505
+ startedAt: z.string().optional(),
1506
+ finishedAt: z.string().optional(),
1505
1507
  assertions: z.array(ReportAssertionSchema).nullable(),
1506
1508
  analysis: FailureAnalysisSchema.nullable(),
1507
1509
  analysisSkipped: z.string().nullable(),
@@ -6198,6 +6200,7 @@ async function runOneSpec$1(ref, opts, blocks) {
6198
6200
  meta("command", command);
6199
6201
  blank();
6200
6202
  const started = Date.now();
6203
+ const startedAt = new Date(started).toISOString();
6201
6204
  let outcome;
6202
6205
  let spawnFailure;
6203
6206
  let measured;
@@ -6218,6 +6221,7 @@ async function runOneSpec$1(ref, opts, blocks) {
6218
6221
  const coverageFields = coverageRowFields(opts, measured, attachError);
6219
6222
  if (spawnFailure !== void 0 || outcome === void 0) return {
6220
6223
  ...didNotExecute(`could not spawn runCommand: ${spawnFailure ?? "unknown error"}`, "the runCommand could not be spawned"),
6224
+ startedAt,
6221
6225
  ...coverageFields
6222
6226
  };
6223
6227
  const durationMs = Date.now() - started;
@@ -6244,6 +6248,8 @@ async function runOneSpec$1(ref, opts, blocks) {
6244
6248
  status: "passed"
6245
6249
  }),
6246
6250
  target: opts.targetId,
6251
+ startedAt,
6252
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
6247
6253
  durationMs,
6248
6254
  ...artifactFields,
6249
6255
  ...evidenceFields,
@@ -6251,6 +6257,8 @@ async function runOneSpec$1(ref, opts, blocks) {
6251
6257
  };
6252
6258
  return {
6253
6259
  ...failedRow([`command failed (exit ${outcome.exitCode}): ${command}`, outcome.tail.length > 0 ? `--- output (tail) ---\n${outcome.tail}` : null].filter((p) => p !== null).join("\n")),
6260
+ startedAt,
6261
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
6254
6262
  durationMs,
6255
6263
  ...artifactFields,
6256
6264
  ...evidenceFields,
@@ -10189,6 +10197,36 @@ function foldTouchIndex(current, entry, selection) {
10189
10197
  }
10190
10198
  return out;
10191
10199
  }
10200
+ /**
10201
+ * How much clock skew between the runner (which stamps a row's window) and
10202
+ * the hub (which stamps a deploy) the placement tolerates. The window is
10203
+ * widened by this on both ends, so a skewed clock costs a row its credit
10204
+ * rather than crediting a row that straddled.
10205
+ */
10206
+ const PLACEMENT_SKEW_MS = 1e4;
10207
+ /**
10208
+ * Place one row's execution window against the deploy log (ADR-0027).
10209
+ *
10210
+ * The window is widened by {@link PLACEMENT_SKEW_MS} and its start is
10211
+ * exclusive, so a deploy landing exactly as the spec began reads as a
10212
+ * straddle. Both choices err the same way: toward `ambiguous`, never toward
10213
+ * crediting a spec with a commit it may not have exercised.
10214
+ *
10215
+ * `entries` must be in append order, which the log guarantees and which is
10216
+ * time order.
10217
+ */
10218
+ function placeRowInDeployLog(entries, window) {
10219
+ const before = (ms, inclusive) => entries.findLast((e) => {
10220
+ const at = Date.parse(e.at);
10221
+ return inclusive ? at <= ms : at < ms;
10222
+ });
10223
+ const opened = before(window.startMs - PLACEMENT_SKEW_MS, false);
10224
+ const closed = before(Math.max(window.startMs, window.endMs) + PLACEMENT_SKEW_MS, true);
10225
+ return {
10226
+ deployedSha: opened?.sha ?? null,
10227
+ deployedShaAmbiguous: opened?.sha !== closed?.sha
10228
+ };
10229
+ }
10192
10230
  //#endregion
10193
10231
  //#region src/coverage/resolve-stream.ts
10194
10232
  /**
@@ -11774,6 +11812,8 @@ async function liveRunToReportResult(args) {
11774
11812
  target: AGENT_BROWSER_TARGET,
11775
11813
  status: result.status,
11776
11814
  testCounts: null,
11815
+ startedAt: result.startedAt,
11816
+ finishedAt: new Date(Date.parse(result.startedAt) + result.durationMs).toISOString(),
11777
11817
  durationMs: result.durationMs,
11778
11818
  assertions: null,
11779
11819
  analysis: null,
@@ -15729,6 +15769,7 @@ async function runOneDeterministicSpec(spec, index, ctx) {
15729
15769
  CCQA_RUN_ID: runId
15730
15770
  };
15731
15771
  if (evidenceDir) specEnv[EVIDENCE_DIR_ENV] = evidenceDir;
15772
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15732
15773
  const proc = spawnVitestStreaming([
15733
15774
  "run",
15734
15775
  "--config",
@@ -15745,11 +15786,14 @@ async function runOneDeterministicSpec(spec, index, ctx) {
15745
15786
  await Promise.all([streamFiltered(proc.stdout, sink, tail), streamFiltered(proc.stderr, sink, tail)]);
15746
15787
  const specExitCode = await proc.exited;
15747
15788
  blank();
15789
+ const report = await readReport$1(reportFile);
15748
15790
  return {
15749
15791
  featureName,
15750
15792
  specName,
15751
15793
  scriptFile,
15752
- report: await readReport$1(reportFile),
15794
+ startedAt,
15795
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15796
+ report,
15753
15797
  exitCode: specExitCode,
15754
15798
  outputTail: tail ? tail.toString() : null,
15755
15799
  evidenceDir
@@ -15778,6 +15822,8 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
15778
15822
  spec: s.specName,
15779
15823
  title: parsedSpec?.title ?? null,
15780
15824
  target: AGENT_BROWSER_TARGET,
15825
+ startedAt: s.startedAt,
15826
+ finishedAt: s.finishedAt,
15781
15827
  testCounts: s.report ? {
15782
15828
  total: s.report.numTotalTests,
15783
15829
  passed: s.report.numPassedTests,
@@ -20186,17 +20232,20 @@ function countSpecs(results) {
20186
20232
  async function updateSpecLedger(storage, run, results) {
20187
20233
  const { gitHead, branch } = run;
20188
20234
  if (run.kind !== "run" || !gitHead || !branch) return;
20189
- const entry = {
20235
+ const placeRow = await rowDeployPlacer(storage, run);
20236
+ const base = {
20190
20237
  gitHead,
20191
20238
  runId: run.id,
20192
- at: run.reportCreatedAt,
20193
- deployedSha: run.deployedSha ?? null,
20194
- deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
20239
+ at: run.reportCreatedAt
20195
20240
  };
20196
20241
  const ledger = emptyLedger();
20197
20242
  for (const row of results) {
20198
20243
  if (row.status === "skipped") continue;
20199
20244
  const key = `${row.feature}/${row.spec}`;
20245
+ const entry = {
20246
+ ...base,
20247
+ ...placeRow(row)
20248
+ };
20200
20249
  ledger.run[key] = entry;
20201
20250
  if (row.status === "passed") ledger.green[key] = entry;
20202
20251
  else ledger.red[key] = redEntry(entry, row);
@@ -20299,6 +20348,38 @@ async function resolveDeployedSha(storage, kind, project, profile, explicit) {
20299
20348
  };
20300
20349
  }
20301
20350
  /**
20351
+ * Place each row against the deploy log by its own execution window rather
20352
+ * than the run's (ADR-0027), so a run that outlives a deploy loses only the
20353
+ * specs that straddled it.
20354
+ *
20355
+ * Falls back to the run's placement where a finer answer isn't available: a
20356
+ * client-asserted sha is the caller's claim about its whole run, and a row
20357
+ * with no `startedAt` (an older client) has nothing finer to say. A row with
20358
+ * no end is measured to now, which only widens its window.
20359
+ */
20360
+ async function rowDeployPlacer(storage, run) {
20361
+ const runPlacement = {
20362
+ deployedSha: run.deployedSha ?? null,
20363
+ deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
20364
+ };
20365
+ if (run.deployedShaSource !== "hub-deploy-log") return () => runPlacement;
20366
+ const log = await storage.deploys.getLog(run.project, run.profile ?? "default").catch((err) => {
20367
+ console.error(`hub: could not read the deploy log to place rows of run "${run.id}": ${errMsg(err)}`);
20368
+ return null;
20369
+ });
20370
+ if (!log) return () => runPlacement;
20371
+ const sealedAt = Date.now();
20372
+ return (row) => {
20373
+ const startMs = row.startedAt ? Date.parse(row.startedAt) : NaN;
20374
+ if (Number.isNaN(startMs)) return runPlacement;
20375
+ const endMs = row.finishedAt ? Date.parse(row.finishedAt) : sealedAt;
20376
+ return placeRowInDeployLog(log.entries, {
20377
+ startMs,
20378
+ endMs: Number.isNaN(endMs) ? sealedAt : endMs
20379
+ });
20380
+ };
20381
+ }
20382
+ /**
20302
20383
  * True when the deploy-log head moved while the run was open: the run
20303
20384
  * straddled a deploy, so which commit it exercised is not knowable and re-run
20304
20385
  * selection must report `unknown` instead of picking one. Only meaningful for
@@ -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";
39
38
  run: "run";
40
39
  drift: "drift";
40
+ record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -640,8 +640,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
640
640
  title: z.ZodNullable<z.ZodString>;
641
641
  target: z.ZodOptional<z.ZodString>;
642
642
  mode: z.ZodOptional<z.ZodEnum<{
643
- deterministic: "deterministic";
644
643
  live: "live";
644
+ deterministic: "deterministic";
645
645
  }>>;
646
646
  status: z.ZodEnum<{
647
647
  passed: "passed";
@@ -655,6 +655,8 @@ declare const ReportSpecResultSchema: z.ZodObject<{
655
655
  failed: z.ZodNumber;
656
656
  }, z.core.$strip>>;
657
657
  durationMs: z.ZodNullable<z.ZodNumber>;
658
+ startedAt: z.ZodOptional<z.ZodString>;
659
+ finishedAt: z.ZodOptional<z.ZodString>;
658
660
  assertions: z.ZodNullable<z.ZodArray<z.ZodObject<{
659
661
  name: z.ZodString;
660
662
  status: z.ZodEnum<{
@@ -811,9 +813,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
811
813
  declare const RunReportDataSchema: z.ZodObject<{
812
814
  schemaVersion: z.ZodLiteral<1>;
813
815
  kind: z.ZodDefault<z.ZodEnum<{
814
- record: "record";
815
816
  run: "run";
816
817
  drift: "drift";
818
+ record: "record";
817
819
  }>>;
818
820
  createdAt: z.ZodString;
819
821
  runId: z.ZodNullable<z.ZodString>;
@@ -854,8 +856,8 @@ declare const RunReportDataSchema: z.ZodObject<{
854
856
  title: z.ZodNullable<z.ZodString>;
855
857
  target: z.ZodOptional<z.ZodString>;
856
858
  mode: z.ZodOptional<z.ZodEnum<{
857
- deterministic: "deterministic";
858
859
  live: "live";
860
+ deterministic: "deterministic";
859
861
  }>>;
860
862
  status: z.ZodEnum<{
861
863
  passed: "passed";
@@ -869,6 +871,8 @@ declare const RunReportDataSchema: z.ZodObject<{
869
871
  failed: z.ZodNumber;
870
872
  }, z.core.$strip>>;
871
873
  durationMs: z.ZodNullable<z.ZodNumber>;
874
+ startedAt: z.ZodOptional<z.ZodString>;
875
+ finishedAt: z.ZodOptional<z.ZodString>;
872
876
  assertions: z.ZodNullable<z.ZodArray<z.ZodObject<{
873
877
  name: z.ZodString;
874
878
  status: z.ZodEnum<{
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.49.0",
3
+ "version": "1.50.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.49.0",
3
+ "version": "1.50.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {