ccqa 1.49.0 → 1.50.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
@@ -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,
@@ -8595,11 +8603,11 @@ async function buildLiveTranscriptExcerpt(result, options = {}) {
8595
8603
  return combined.length > maxBytes ? `${combined.slice(0, maxBytes)}\n…[transcript excerpt truncated at ${maxBytes} bytes]` : combined;
8596
8604
  }
8597
8605
  function formatPreviousStep(step) {
8598
- const reason = oneLine$2(step.reasoning) || "(no reason given)";
8606
+ const reason = oneLine$3(step.reasoning) || "(no reason given)";
8599
8607
  return `[${step.stepId} ${step.status}: ${reason}]`;
8600
8608
  }
8601
8609
  async function formatFailingStep(step, headBytes, tailBytes) {
8602
- const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$2(step.instruction)}\nExpected: ${oneLine$2(step.expected)}\nReasoning (Claude's verdict): ${oneLine$2(step.reasoning) || "(none)"}`;
8610
+ const header = `\n>>> Failed step ${step.stepId}\nInstruction: ${oneLine$3(step.instruction)}\nExpected: ${oneLine$3(step.expected)}\nReasoning (Claude's verdict): ${oneLine$3(step.reasoning) || "(none)"}`;
8603
8611
  if (!step.logTxt) return `${header}\n(No assistant log file recorded for this step.)`;
8604
8612
  const raw = await readFile(step.logTxt, "utf-8").catch((err) => {
8605
8613
  return `[log file unreadable: ${err instanceof Error ? err.message : String(err)}]`;
@@ -8609,7 +8617,7 @@ async function formatFailingStep(step, headBytes, tailBytes) {
8609
8617
  const tail = raw.slice(raw.length - tailBytes);
8610
8618
  return `${header}\n--- assistant log (head ${headBytes}B) ---\n${head}\n…[${raw.length - headBytes - tailBytes} bytes omitted]…\n--- assistant log (tail ${tailBytes}B) ---\n${tail}`;
8611
8619
  }
8612
- function oneLine$2(s) {
8620
+ function oneLine$3(s) {
8613
8621
  return s.replace(/\s+/g, " ").trim();
8614
8622
  }
8615
8623
  //#endregion
@@ -10121,10 +10129,10 @@ function describeStep(step) {
10121
10129
  return describeStepBody(step);
10122
10130
  }
10123
10131
  function describeStepBody(step) {
10124
- if (isJudgeBody(step)) return `judge: ${oneLine$1(step.judgeByLlm)}`;
10125
- return `${oneLine$1(step.instruction)} → ${oneLine$1(step.expected)}`;
10132
+ if (isJudgeBody(step)) return `judge: ${oneLine$2(step.judgeByLlm)}`;
10133
+ return `${oneLine$2(step.instruction)} → ${oneLine$2(step.expected)}`;
10126
10134
  }
10127
- function oneLine$1(text) {
10135
+ function oneLine$2(text) {
10128
10136
  return text.trim().replace(/\s+/g, " ");
10129
10137
  }
10130
10138
  function emptyDeployLog() {
@@ -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,
@@ -12392,15 +12432,15 @@ function renderRunMarkdown(featureName, specName, result) {
12392
12432
  ].join("\n") + result.steps.map((s) => [
12393
12433
  `## ${s.stepId} — ${s.status}`,
12394
12434
  `- duration: ${(s.durationMs / 1e3).toFixed(1)}s`,
12395
- `- instruction: ${oneLine(s.instruction)}`,
12396
- `- expected: ${oneLine(s.expected)}`,
12397
- `- reasoning: ${oneLine(s.reasoning)}`,
12435
+ `- instruction: ${oneLine$1(s.instruction)}`,
12436
+ `- expected: ${oneLine$1(s.expected)}`,
12437
+ `- reasoning: ${oneLine$1(s.reasoning)}`,
12398
12438
  ...s.beforePng ? [`- before: ${s.beforePng}`] : [],
12399
12439
  ...s.afterPng ? [`- after: ${s.afterPng}`] : [],
12400
12440
  ""
12401
12441
  ].join("\n")).join("\n");
12402
12442
  }
12403
- function oneLine(s) {
12443
+ function oneLine$1(s) {
12404
12444
  return s.replace(/\s+/g, " ").trim();
12405
12445
  }
12406
12446
  //#endregion
@@ -13964,6 +14004,136 @@ async function waitForCdp(port) {
13964
14004
  }
13965
14005
  }
13966
14006
  //#endregion
14007
+ //#region src/prompts/verifies-spec.ts
14008
+ /**
14009
+ * Asks whether a generated test actually decides what its spec claims.
14010
+ *
14011
+ * The generation loop's only bar is "the test goes green", and a rewrite that
14012
+ * weakens an assertion clears that bar as easily as one that keeps it. Green
14013
+ * therefore does not mean checked, and nothing else looks. Observed cases: a
14014
+ * step whose expectation was "the linked page opens" asserting instead that
14015
+ * the *link* is still visible on the page it clicked from; another asserting
14016
+ * on a navigation element unrelated to the step.
14017
+ *
14018
+ * Deliberately narrow. It reads only what the step says and what the code
14019
+ * does, and reports the step as unchecked when the two do not line up. It
14020
+ * does not review style, coverage, or whether the expectation is a good one.
14021
+ */
14022
+ function verifiesSpecPrompt(input) {
14023
+ return [
14024
+ "You are reviewing whether a generated end-to-end test decides what its spec says.",
14025
+ "",
14026
+ "For each step below, the test must contain assertions that could FAIL if the",
14027
+ "step's `expected` stopped holding. Report a step when:",
14028
+ "",
14029
+ "- its assertions cannot fail while the product is broken in the way the",
14030
+ " expectation describes (e.g. the expectation says a page opens, and the",
14031
+ " code only re-checks the element it clicked);",
14032
+ "- what it asserts on is unrelated to what the step did (e.g. a navigation",
14033
+ " element that is present on every page);",
14034
+ "- it depends on something that varies between runs and is not part of the",
14035
+ " expectation (a count, an index, a position, wording that changes);",
14036
+ "- it has no assertion at all.",
14037
+ "",
14038
+ "Do NOT report: style, naming, structure, missing coverage the spec never",
14039
+ "asked for, or an expectation you merely disagree with. A step that checks",
14040
+ "less than you would have written, but still fails when the expectation",
14041
+ "breaks, is fine.",
14042
+ "",
14043
+ "## Steps",
14044
+ "",
14045
+ ...input.steps.map(stepLine),
14046
+ "",
14047
+ "## Generated test",
14048
+ "",
14049
+ "```",
14050
+ input.source,
14051
+ "```",
14052
+ "",
14053
+ "Answer with one json block and nothing else:",
14054
+ "",
14055
+ "```json",
14056
+ "{ \"findings\": [ { \"stepId\": \"step-05\", \"problem\": \"…\" } ] }",
14057
+ "```",
14058
+ "",
14059
+ "`problem` is one sentence naming what the step claims and what the code",
14060
+ "checks instead. Empty `findings` means every step is decided.",
14061
+ languageDirective(input.language)
14062
+ ].join("\n");
14063
+ }
14064
+ function stepLine(step) {
14065
+ if (!isExpandedActionStep(step)) return `- ${step.id}: judged by a model at run time — its claim is asserted by the injected call, so it needs no other assertion.`;
14066
+ return [
14067
+ `- ${step.id}`,
14068
+ ` does: ${oneLine(step.instruction)}`,
14069
+ ` expected: ${oneLine(step.expected)}`
14070
+ ].join("\n");
14071
+ }
14072
+ function oneLine(text) {
14073
+ return text.trim().split("\n").map((l) => l.trim()).join(" ");
14074
+ }
14075
+ //#endregion
14076
+ //#region src/targets/verifies-spec.ts
14077
+ const FindingsSchema = z.object({ findings: z.array(z.object({
14078
+ stepId: z.string(),
14079
+ problem: z.string()
14080
+ })) });
14081
+ /**
14082
+ * Findings in the model's answer, or null when it did not answer in the
14083
+ * agreed shape. Null is not "no findings": the caller says so rather than
14084
+ * reporting a clean review it never got.
14085
+ */
14086
+ function parseVerifiesSpecFindings(answer) {
14087
+ const json = extractJsonBlock(answer);
14088
+ if (!json) return null;
14089
+ try {
14090
+ return FindingsSchema.parse(JSON.parse(json)).findings;
14091
+ } catch {
14092
+ return null;
14093
+ }
14094
+ }
14095
+ /** The warning a finding becomes, phrased so the reader knows the test is green for nothing. */
14096
+ function formatFinding(finding) {
14097
+ return `step ${finding.stepId}: the generated test passes without deciding what this step claims — ${finding.problem}`;
14098
+ }
14099
+ /**
14100
+ * Read the generated test back and ask whether each step is actually decided
14101
+ * (see `verifiesSpecPrompt`). Returns warnings; an empty list means either a
14102
+ * clean review or one that could not be obtained, and the difference is
14103
+ * logged rather than encoded — a review that failed must not read as a pass,
14104
+ * but it must also not fail the generate that produced working files.
14105
+ */
14106
+ async function reviewGeneratedTest(input) {
14107
+ const source = (await Promise.all(input.result.files.filter((f) => f.kind === "test").map((f) => readFile(f.path, "utf8").catch(() => "")))).filter((s) => s.length > 0).join("\n\n");
14108
+ if (source.length === 0) {
14109
+ warn("could not check whether the generated test decides its spec (no test file to read)");
14110
+ return [];
14111
+ }
14112
+ const { result: answer, isError } = await (input.invoke ?? invokeClaudeStreaming)({
14113
+ prompt: verifiesSpecPrompt({
14114
+ steps: input.steps,
14115
+ source,
14116
+ language: input.language
14117
+ }),
14118
+ allowedTools: [],
14119
+ disableThinking: true,
14120
+ maxTurns: 1,
14121
+ silenceBashLog: true,
14122
+ ...input.model ? { model: input.model } : {},
14123
+ cwd: input.cwd
14124
+ }, () => {});
14125
+ if (isError) {
14126
+ warn("could not check whether the generated test decides its spec (Claude returned an error)");
14127
+ return [];
14128
+ }
14129
+ const findings = parseVerifiesSpecFindings(answer);
14130
+ if (findings === null) {
14131
+ warn("could not check whether the generated test decides its spec (no usable answer)");
14132
+ return [];
14133
+ }
14134
+ return findings.map(formatFinding);
14135
+ }
14136
+ //#endregion
13967
14137
  //#region src/targets/playwright/index.ts
13968
14138
  const PLAYWRIGHT_TARGET = "playwright";
13969
14139
  /**
@@ -14035,12 +14205,21 @@ async function generatePlaywrightTest(ctx) {
14035
14205
  });
14036
14206
  const missing = await missingInjectedCalls(result, stepMarkers, judgements);
14037
14207
  for (const w of missing) warn(w);
14208
+ const unchecked = await reviewGeneratedTest({
14209
+ result,
14210
+ steps: expanded,
14211
+ language: ctx.language,
14212
+ ...ctx.model ? { model: ctx.model } : {},
14213
+ cwd: ctx.cwd
14214
+ });
14215
+ for (const w of unchecked) warn(w);
14038
14216
  return {
14039
14217
  ...result,
14040
14218
  warnings: [
14041
14219
  ...result.warnings,
14042
14220
  ...judgeWarnings,
14043
- ...missing
14221
+ ...missing,
14222
+ ...unchecked
14044
14223
  ]
14045
14224
  };
14046
14225
  }
@@ -15198,7 +15377,7 @@ async function executeRun(targets, opts) {
15198
15377
  const declared = [...new Set(specs.flatMap(resources))];
15199
15378
  if (declared.length > 0) meta("serial groups", declared.join(", "));
15200
15379
  let waitingOnGroup = [];
15201
- if (hubCtx && rerunProfile !== null) {
15380
+ if (hubCtx && rerunProfile !== null && forExecution) {
15202
15381
  const held = await holdSpecs(hubCtx, rerunProfile, specs, resources, opts.teardown);
15203
15382
  waitingOnGroup = specs.flatMap((spec) => {
15204
15383
  const groups = resources(spec).filter((n) => held.deniedResources.includes(n));
@@ -15729,6 +15908,7 @@ async function runOneDeterministicSpec(spec, index, ctx) {
15729
15908
  CCQA_RUN_ID: runId
15730
15909
  };
15731
15910
  if (evidenceDir) specEnv[EVIDENCE_DIR_ENV] = evidenceDir;
15911
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15732
15912
  const proc = spawnVitestStreaming([
15733
15913
  "run",
15734
15914
  "--config",
@@ -15745,11 +15925,14 @@ async function runOneDeterministicSpec(spec, index, ctx) {
15745
15925
  await Promise.all([streamFiltered(proc.stdout, sink, tail), streamFiltered(proc.stderr, sink, tail)]);
15746
15926
  const specExitCode = await proc.exited;
15747
15927
  blank();
15928
+ const report = await readReport$1(reportFile);
15748
15929
  return {
15749
15930
  featureName,
15750
15931
  specName,
15751
15932
  scriptFile,
15752
- report: await readReport$1(reportFile),
15933
+ startedAt,
15934
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15935
+ report,
15753
15936
  exitCode: specExitCode,
15754
15937
  outputTail: tail ? tail.toString() : null,
15755
15938
  evidenceDir
@@ -15778,6 +15961,8 @@ async function analyzeDeterministicSummaries(summaries, cwd, reportDir, { pass,
15778
15961
  spec: s.specName,
15779
15962
  title: parsedSpec?.title ?? null,
15780
15963
  target: AGENT_BROWSER_TARGET,
15964
+ startedAt: s.startedAt,
15965
+ finishedAt: s.finishedAt,
15781
15966
  testCounts: s.report ? {
15782
15967
  total: s.report.numTotalTests,
15783
15968
  passed: s.report.numPassedTests,
@@ -20186,17 +20371,20 @@ function countSpecs(results) {
20186
20371
  async function updateSpecLedger(storage, run, results) {
20187
20372
  const { gitHead, branch } = run;
20188
20373
  if (run.kind !== "run" || !gitHead || !branch) return;
20189
- const entry = {
20374
+ const placeRow = await rowDeployPlacer(storage, run);
20375
+ const base = {
20190
20376
  gitHead,
20191
20377
  runId: run.id,
20192
- at: run.reportCreatedAt,
20193
- deployedSha: run.deployedSha ?? null,
20194
- deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
20378
+ at: run.reportCreatedAt
20195
20379
  };
20196
20380
  const ledger = emptyLedger();
20197
20381
  for (const row of results) {
20198
20382
  if (row.status === "skipped") continue;
20199
20383
  const key = `${row.feature}/${row.spec}`;
20384
+ const entry = {
20385
+ ...base,
20386
+ ...placeRow(row)
20387
+ };
20200
20388
  ledger.run[key] = entry;
20201
20389
  if (row.status === "passed") ledger.green[key] = entry;
20202
20390
  else ledger.red[key] = redEntry(entry, row);
@@ -20299,6 +20487,38 @@ async function resolveDeployedSha(storage, kind, project, profile, explicit) {
20299
20487
  };
20300
20488
  }
20301
20489
  /**
20490
+ * Place each row against the deploy log by its own execution window rather
20491
+ * than the run's (ADR-0027), so a run that outlives a deploy loses only the
20492
+ * specs that straddled it.
20493
+ *
20494
+ * Falls back to the run's placement where a finer answer isn't available: a
20495
+ * client-asserted sha is the caller's claim about its whole run, and a row
20496
+ * with no `startedAt` (an older client) has nothing finer to say. A row with
20497
+ * no end is measured to now, which only widens its window.
20498
+ */
20499
+ async function rowDeployPlacer(storage, run) {
20500
+ const runPlacement = {
20501
+ deployedSha: run.deployedSha ?? null,
20502
+ deployedShaAmbiguous: run.deployedShaAmbiguous ?? false
20503
+ };
20504
+ if (run.deployedShaSource !== "hub-deploy-log") return () => runPlacement;
20505
+ const log = await storage.deploys.getLog(run.project, run.profile ?? "default").catch((err) => {
20506
+ console.error(`hub: could not read the deploy log to place rows of run "${run.id}": ${errMsg(err)}`);
20507
+ return null;
20508
+ });
20509
+ if (!log) return () => runPlacement;
20510
+ const sealedAt = Date.now();
20511
+ return (row) => {
20512
+ const startMs = row.startedAt ? Date.parse(row.startedAt) : NaN;
20513
+ if (Number.isNaN(startMs)) return runPlacement;
20514
+ const endMs = row.finishedAt ? Date.parse(row.finishedAt) : sealedAt;
20515
+ return placeRowInDeployLog(log.entries, {
20516
+ startMs,
20517
+ endMs: Number.isNaN(endMs) ? sealedAt : endMs
20518
+ });
20519
+ };
20520
+ }
20521
+ /**
20302
20522
  * True when the deploy-log head moved while the run was open: the run
20303
20523
  * straddled a deploy, so which commit it exercised is not knowable and re-run
20304
20524
  * 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.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.49.0",
3
+ "version": "1.50.1",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {