executable-stories-formatters 0.16.0 → 0.17.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/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
- import * as fs16 from "fs";
6
- import * as path17 from "path";
5
+ import * as fs17 from "fs";
6
+ import * as path18 from "path";
7
7
 
8
8
  // src/validation/schema-validator.ts
9
9
  import Ajv from "ajv/dist/2020.js";
@@ -588,17 +588,17 @@ function validateRawRun(data) {
588
588
  return { valid: true, errors: [] };
589
589
  }
590
590
  const errors = (validate.errors ?? []).map((err) => {
591
- const path18 = err.instancePath || "/";
591
+ const path19 = err.instancePath || "/";
592
592
  const message = err.message ?? "unknown error";
593
593
  if (err.keyword === "additionalProperties") {
594
594
  const extra = err.params.additionalProperty;
595
- return `${path18}: ${message} \u2014 '${extra}'`;
595
+ return `${path19}: ${message} \u2014 '${extra}'`;
596
596
  }
597
597
  if (err.keyword === "enum") {
598
598
  const allowed = err.params.allowedValues;
599
- return `${path18}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
599
+ return `${path19}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
600
600
  }
601
- return `${path18}: ${message}`;
601
+ return `${path19}: ${message}`;
602
602
  });
603
603
  return { valid: false, errors };
604
604
  }
@@ -711,7 +711,7 @@ function tokenize(text2) {
711
711
  }
712
712
  function behaviourFingerprint(input) {
713
713
  const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
714
- const covers = [...input.covers ?? []].map((path18) => path18.trim()).filter(Boolean).sort().join(",");
714
+ const covers = [...input.covers ?? []].map((path19) => path19.trim()).filter(Boolean).sort().join(",");
715
715
  if (steps.length === 0 && covers.length === 0) return "";
716
716
  return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
717
717
  }
@@ -1108,7 +1108,7 @@ ${result.errors.join("\n")}`);
1108
1108
 
1109
1109
  // src/index.ts
1110
1110
  import "fs";
1111
- import * as path9 from "path";
1111
+ import * as path10 from "path";
1112
1112
  import * as fsPromises from "fs/promises";
1113
1113
 
1114
1114
  // src/converters/acl/lines.ts
@@ -15012,12 +15012,12 @@ function highlightStepParams(text2, deps) {
15012
15012
  var MIN_METRIC_SAMPLES = 5;
15013
15013
 
15014
15014
  // src/formatters/html/renderers/scenario.ts
15015
- function renderTicket(ticket, template, escapeHtml4) {
15015
+ function renderTicket(ticket, template, escapeHtml5) {
15016
15016
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
15017
15017
  if (url) {
15018
- return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
15018
+ return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
15019
15019
  }
15020
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
15020
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
15021
15021
  }
15022
15022
  function renderScenario(args, deps) {
15023
15023
  const { tc } = args;
@@ -15217,7 +15217,7 @@ function flattenTree(roots) {
15217
15217
  }
15218
15218
  return result;
15219
15219
  }
15220
- function buildTooltip(span, escapeHtml4) {
15220
+ function buildTooltip(span, escapeHtml5) {
15221
15221
  const parts = [];
15222
15222
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
15223
15223
  if (span.statusMessage) {
@@ -15235,7 +15235,7 @@ function buildTooltip(span, escapeHtml4) {
15235
15235
  if (text2.length > TOOLTIP_MAX_LENGTH) {
15236
15236
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
15237
15237
  }
15238
- return escapeHtml4(text2);
15238
+ return escapeHtml5(text2);
15239
15239
  }
15240
15240
  function renderTraceView(args, deps) {
15241
15241
  if (!args.spans || args.spans.length === 0) return "";
@@ -16530,14 +16530,14 @@ var TraceabilityMatrixFormatter = class {
16530
16530
  lines.push("");
16531
16531
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
16532
16532
  if (req.covers.length > 0) {
16533
- lines.push(`Covers: ${req.covers.map((path18) => `\`${path18}\``).join(", ")}`);
16533
+ lines.push(`Covers: ${req.covers.map((path19) => `\`${path19}\``).join(", ")}`);
16534
16534
  }
16535
16535
  lines.push("");
16536
16536
  lines.push("| Status | Scenario | Source | Covers |");
16537
16537
  lines.push("| --- | --- | --- | --- |");
16538
16538
  for (const scenario of req.scenarios) {
16539
16539
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16540
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path18) => `\`${path18}\``).join(", ") : "";
16540
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path19) => `\`${path19}\``).join(", ") : "";
16541
16541
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
16542
16542
  }
16543
16543
  lines.push("");
@@ -16639,8 +16639,8 @@ function extractFeatureName(testCases, uri) {
16639
16639
  return tc.titlePath[0];
16640
16640
  }
16641
16641
  }
16642
- const basename5 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16643
- return basename5.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16642
+ const basename6 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16643
+ return basename6.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16644
16644
  }
16645
16645
  function synthesizeFeature(uri, testCases) {
16646
16646
  const featureName = extractFeatureName(testCases, uri);
@@ -17252,8 +17252,8 @@ function extractDocAttachments(step) {
17252
17252
  }
17253
17253
  return attachments;
17254
17254
  }
17255
- function guessMediaType(path18) {
17256
- const lower = path18.toLowerCase();
17255
+ function guessMediaType(path19) {
17256
+ const lower = path19.toLowerCase();
17257
17257
  if (lower.endsWith(".png")) return "image/png";
17258
17258
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
17259
17259
  if (lower.endsWith(".gif")) return "image/gif";
@@ -17394,11 +17394,11 @@ var CucumberHtmlFormatter = class {
17394
17394
  for (const envelope of envelopes) {
17395
17395
  const accepted = htmlStream.write(envelope);
17396
17396
  if (!accepted) {
17397
- await new Promise((resolve12) => htmlStream.once("drain", resolve12));
17397
+ await new Promise((resolve13) => htmlStream.once("drain", resolve13));
17398
17398
  }
17399
17399
  }
17400
- await new Promise((resolve12, reject) => {
17401
- collector.on("finish", resolve12);
17400
+ await new Promise((resolve13, reject) => {
17401
+ collector.on("finish", resolve13);
17402
17402
  collector.on("error", reject);
17403
17403
  htmlStream.end();
17404
17404
  });
@@ -19192,7 +19192,7 @@ function toRun(data, inputType, synthesize) {
19192
19192
  if (synthesize) raw = synthesizeStories(raw);
19193
19193
  return canonicalizeRun(raw);
19194
19194
  }
19195
- async function regenerateArtifacts(options, deps = {}) {
19195
+ async function regenerateRun(options, deps = {}) {
19196
19196
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
19197
19197
  const data = JSON.parse(read(path7.resolve(options.input)));
19198
19198
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -19202,7 +19202,10 @@ async function regenerateArtifacts(options, deps = {}) {
19202
19202
  outputName: options.outputName
19203
19203
  });
19204
19204
  const result = await generator.generate(run);
19205
- return [...result.values()].flat();
19205
+ return { files: [...result.values()].flat(), run };
19206
+ }
19207
+ async function regenerateArtifacts(options, deps = {}) {
19208
+ return (await regenerateRun(options, deps)).files;
19206
19209
  }
19207
19210
  function startWatch(options, deps = {}) {
19208
19211
  const log = deps.log ?? ((message) => console.log(message));
@@ -19245,6 +19248,203 @@ function startWatch(options, deps = {}) {
19245
19248
  };
19246
19249
  }
19247
19250
 
19251
+ // src/serve.ts
19252
+ import * as fs7 from "fs";
19253
+ import * as http from "http";
19254
+ import * as path8 from "path";
19255
+ function advanceState(prev, run) {
19256
+ if (prev.sessionBaseline === null) {
19257
+ return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
19258
+ }
19259
+ return {
19260
+ sessionBaseline: prev.sessionBaseline,
19261
+ previous: prev.current,
19262
+ current: run,
19263
+ runCount: prev.runCount + 1
19264
+ };
19265
+ }
19266
+ function computeDeltas(state) {
19267
+ if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
19268
+ return { session: null, iteration: null };
19269
+ }
19270
+ return {
19271
+ session: diffRuns(state.sessionBaseline, state.current),
19272
+ iteration: state.previous ? diffRuns(state.previous, state.current) : null
19273
+ };
19274
+ }
19275
+ function pluralize(n, word) {
19276
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
19277
+ }
19278
+ function summarizeDiff(diff) {
19279
+ const s = diff.summary;
19280
+ const parts = [];
19281
+ if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
19282
+ if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
19283
+ if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
19284
+ if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
19285
+ const moved = s.renamed + s.moved;
19286
+ if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
19287
+ if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
19288
+ return parts.length > 0 ? parts.join(", ") : null;
19289
+ }
19290
+ function escapeHtml3(text2) {
19291
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19292
+ }
19293
+ function renderDeltaStrip(state) {
19294
+ if (state.current === null) return "";
19295
+ const { session, iteration } = computeDeltas(state);
19296
+ if (session === null) {
19297
+ const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
19298
+ return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
19299
+ }
19300
+ const sessionLine = summarizeDiff(session) ?? "no change yet";
19301
+ let detail = "";
19302
+ if (iteration) {
19303
+ const iterationLine = summarizeDiff(iteration);
19304
+ if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
19305
+ }
19306
+ return [
19307
+ `<div data-es-live="strip">`,
19308
+ `<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
19309
+ `<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
19310
+ detail,
19311
+ `</div>`
19312
+ ].join("");
19313
+ }
19314
+ var RELOAD_CLIENT = `<script data-es-live="client">
19315
+ (function () {
19316
+ try {
19317
+ var es = new EventSource("/__es_reload");
19318
+ es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
19319
+ } catch (err) { /* SSE unavailable: stay static */ }
19320
+ })();
19321
+ </script>`;
19322
+ var STRIP_STYLE = `<style data-es-live="style">
19323
+ [data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
19324
+ padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
19325
+ [data-es-live="strip"] strong{color:#7dd3fc}
19326
+ </style>`;
19327
+ function injectLiveBits(html, stripHtml) {
19328
+ let out = html;
19329
+ const bodyOpen = out.match(/<body[^>]*>/i);
19330
+ if (bodyOpen) {
19331
+ const at = bodyOpen.index + bodyOpen[0].length;
19332
+ out = out.slice(0, at) + stripHtml + out.slice(at);
19333
+ } else {
19334
+ out = stripHtml + out;
19335
+ }
19336
+ const tail = STRIP_STYLE + RELOAD_CLIENT;
19337
+ if (/<\/body>/i.test(out)) {
19338
+ out = out.replace(/<\/body>/i, tail + "</body>");
19339
+ } else {
19340
+ out += tail;
19341
+ }
19342
+ return out;
19343
+ }
19344
+ var CONTENT_TYPES = {
19345
+ ".html": "text/html; charset=utf-8",
19346
+ ".css": "text/css; charset=utf-8",
19347
+ ".js": "text/javascript; charset=utf-8",
19348
+ ".json": "application/json; charset=utf-8",
19349
+ ".svg": "image/svg+xml",
19350
+ ".png": "image/png"
19351
+ };
19352
+ function watchInputDir(filePath, listener) {
19353
+ const dir = path8.dirname(filePath);
19354
+ const base = path8.basename(filePath);
19355
+ fs7.mkdirSync(dir, { recursive: true });
19356
+ const watcher = fs7.watch(dir, (_event, changed) => {
19357
+ if (!changed || changed === base) listener();
19358
+ });
19359
+ return { close: () => watcher.close() };
19360
+ }
19361
+ function plainText(html) {
19362
+ return html.replace(/<[^>]+>/g, "").trim();
19363
+ }
19364
+ function startServe(options, deps = {}) {
19365
+ const log = deps.log ?? ((message) => console.log(message));
19366
+ const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
19367
+ const port = options.port ?? 4321;
19368
+ const host = options.host ?? "127.0.0.1";
19369
+ let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
19370
+ let htmlPath = null;
19371
+ let stripHtml = renderDeltaStrip(state);
19372
+ const clients = /* @__PURE__ */ new Set();
19373
+ const pushReload = () => {
19374
+ for (const res of clients) res.write("data: reload\n\n");
19375
+ };
19376
+ const handler = (req, res) => {
19377
+ const url = (req.url ?? "/").split("?")[0];
19378
+ if (url === "/__es_reload") {
19379
+ res.writeHead(200, {
19380
+ "Content-Type": "text/event-stream",
19381
+ "Cache-Control": "no-cache",
19382
+ Connection: "keep-alive"
19383
+ });
19384
+ res.write("retry: 1000\n\n");
19385
+ clients.add(res);
19386
+ req.on("close", () => clients.delete(res));
19387
+ return;
19388
+ }
19389
+ if (url === "/" || url === "/index.html") {
19390
+ const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
19391
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19392
+ res.end(injectLiveBits(html, stripHtml));
19393
+ return;
19394
+ }
19395
+ const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
19396
+ const filePath = path8.join(path8.resolve(options.outputDir), safe);
19397
+ if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
19398
+ const ext = path8.extname(filePath).toLowerCase();
19399
+ res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
19400
+ res.end(read(filePath));
19401
+ return;
19402
+ }
19403
+ res.writeHead(404, { "Content-Type": "text/plain" });
19404
+ res.end("Not found");
19405
+ };
19406
+ const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
19407
+ const watchOptions = {
19408
+ input: options.input,
19409
+ outputDir: options.outputDir,
19410
+ outputName: options.outputName,
19411
+ formats: options.formats,
19412
+ inputType: options.inputType ?? "raw",
19413
+ synthesize: options.synthesize !== false,
19414
+ debounceMs: options.debounceMs
19415
+ };
19416
+ const watchHandle = startWatch(watchOptions, {
19417
+ readFile: read,
19418
+ watch: deps.watch ?? watchInputDir,
19419
+ log: () => {
19420
+ },
19421
+ // serve emits its own per-run line below
19422
+ regenerate: async (input) => {
19423
+ if (!fs7.existsSync(path8.resolve(input))) return [];
19424
+ const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
19425
+ htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
19426
+ state = advanceState(state, run);
19427
+ stripHtml = renderDeltaStrip(state);
19428
+ log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
19429
+ pushReload();
19430
+ return files;
19431
+ }
19432
+ });
19433
+ server.listen(port, host);
19434
+ const address = server.address();
19435
+ const boundPort = typeof address === "object" && address ? address.port : port;
19436
+ log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
19437
+ return {
19438
+ port: boundPort,
19439
+ close: () => {
19440
+ watchHandle.close();
19441
+ for (const res of clients) res.end();
19442
+ clients.clear();
19443
+ server.close();
19444
+ }
19445
+ };
19446
+ }
19447
+
19248
19448
  // src/behavior-diff.ts
19249
19449
  function classifyStatusChange(baseline, current) {
19250
19450
  if (baseline === void 0) return "added";
@@ -20894,18 +21094,18 @@ function deriveChangeType(tags) {
20894
21094
  }
20895
21095
  return "unknown";
20896
21096
  }
20897
- function extensionOf(path18) {
20898
- const base = path18.split("/").pop() ?? path18;
21097
+ function extensionOf(path19) {
21098
+ const base = path19.split("/").pop() ?? path19;
20899
21099
  const dot = base.lastIndexOf(".");
20900
21100
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20901
21101
  }
20902
- function isTestFile(path18) {
20903
- return TEST_INFIX.test(path18);
21102
+ function isTestFile(path19) {
21103
+ return TEST_INFIX.test(path19);
20904
21104
  }
20905
- function isReviewableSource(path18) {
20906
- if (isTestFile(path18)) return false;
20907
- if (path18.endsWith(".d.ts")) return false;
20908
- return CODE_EXTENSIONS.has(extensionOf(path18));
21105
+ function isReviewableSource(path19) {
21106
+ if (isTestFile(path19)) return false;
21107
+ if (path19.endsWith(".d.ts")) return false;
21108
+ return CODE_EXTENSIONS.has(extensionOf(path19));
20909
21109
  }
20910
21110
  function testBaseKey(testFile) {
20911
21111
  return testFile.replace(TEST_INFIX, "");
@@ -21009,7 +21209,7 @@ function toClaim(testCase, changedSourcePaths) {
21009
21209
  const { strength, reasons } = gradeEvidence(testCase, audience);
21010
21210
  const key = testBaseKey(testCase.sourceFile);
21011
21211
  const coversFiles = changedSourcePaths.filter(
21012
- (path18) => sourceBaseKey(path18) === key
21212
+ (path19) => sourceBaseKey(path19) === key
21013
21213
  );
21014
21214
  return {
21015
21215
  id: testCase.id,
@@ -21258,7 +21458,7 @@ var ReviewMarkdownFormatter = class {
21258
21458
  };
21259
21459
 
21260
21460
  // src/formatters/review-html.ts
21261
- function escapeHtml3(value) {
21461
+ function escapeHtml4(value) {
21262
21462
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21263
21463
  }
21264
21464
  var STRENGTH_LABEL = {
@@ -21280,22 +21480,22 @@ function statusIcon3(status) {
21280
21480
  }
21281
21481
  }
21282
21482
  function formatStep3(step) {
21283
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21483
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21284
21484
  }
21285
21485
  function inlineDoc(doc) {
21286
21486
  switch (doc.kind) {
21287
21487
  case "note":
21288
- return escapeHtml3(doc.text);
21488
+ return escapeHtml4(doc.text);
21289
21489
  case "section":
21290
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21490
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21291
21491
  case "kv":
21292
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21492
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21293
21493
  case "code":
21294
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21494
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21295
21495
  case "link":
21296
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21496
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21297
21497
  default:
21298
- return escapeHtml3(doc.kind);
21498
+ return escapeHtml4(doc.kind);
21299
21499
  }
21300
21500
  }
21301
21501
  function renderEvidenceArtifacts(testCase) {
@@ -21303,7 +21503,7 @@ function renderEvidenceArtifacts(testCase) {
21303
21503
  for (const att of testCase.attachments) {
21304
21504
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21305
21505
  parts.push(
21306
- `<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
21506
+ `<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
21307
21507
  );
21308
21508
  }
21309
21509
  }
@@ -21318,22 +21518,22 @@ function renderTicketPills(claim) {
21318
21518
  const tickets = claim.testCase.story.tickets ?? [];
21319
21519
  if (tickets.length === 0) return "";
21320
21520
  return `<div class="ticket-row">${tickets.map((ticket) => {
21321
- const label = escapeHtml3(ticket.id);
21521
+ const label = escapeHtml4(ticket.id);
21322
21522
  if (ticket.url) {
21323
- return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21523
+ return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21324
21524
  }
21325
21525
  return `<span class="ticket-pill">${label}</span>`;
21326
21526
  }).join("")}</div>`;
21327
21527
  }
21328
21528
  function renderClaimCard(claim) {
21329
21529
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21330
- const search = escapeHtml3(
21530
+ const search = escapeHtml4(
21331
21531
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21332
21532
  ).toLowerCase();
21333
21533
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21334
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21335
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21336
- const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
21534
+ const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
21535
+ const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
21536
+ const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
21337
21537
  const docs = (claim.testCase.story.docs ?? []).filter(
21338
21538
  (d) => d.kind === "section" || d.kind === "note"
21339
21539
  );
@@ -21343,9 +21543,9 @@ function renderClaimCard(claim) {
21343
21543
  <header class="claim-header">
21344
21544
  <div>
21345
21545
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21346
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21347
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21348
- <p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21546
+ ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
21547
+ <h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
21548
+ <p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21349
21549
  ${renderTicketPills(claim)}
21350
21550
  </div>
21351
21551
  </header>
@@ -21360,18 +21560,18 @@ function renderClaimCard(claim) {
21360
21560
  </article>`;
21361
21561
  }
21362
21562
  function renderChangedFileRow(file) {
21363
- const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21563
+ const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21364
21564
  return `<tr data-band="${file.band}">
21365
21565
  <td><span class="band-dot band-${file.band}"></span></td>
21366
- <td><code>${escapeHtml3(file.path)}</code></td>
21367
- <td>${escapeHtml3(file.changeKind)}</td>
21566
+ <td><code>${escapeHtml4(file.path)}</code></td>
21567
+ <td>${escapeHtml4(file.changeKind)}</td>
21368
21568
  <td>${claims}</td>
21369
21569
  </tr>`;
21370
21570
  }
21371
21571
  function renderAudienceSection2(title, claims) {
21372
21572
  if (claims.length === 0) return "";
21373
21573
  return `<section class="audience-section">
21374
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21574
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21375
21575
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21376
21576
  </section>`;
21377
21577
  }
@@ -21461,13 +21661,13 @@ var ReviewHtmlFormatter = class {
21461
21661
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21462
21662
  applyTheme(getEffectiveTheme());` : "";
21463
21663
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21464
- const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
21664
+ const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
21465
21665
  return `<!doctype html>
21466
21666
  <html lang="en"${themeAttr}>
21467
21667
  <head>
21468
21668
  <meta charset="utf-8" />
21469
21669
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21470
- <title>${escapeHtml3(this.title)}</title>
21670
+ <title>${escapeHtml4(this.title)}</title>
21471
21671
  <style>
21472
21672
  ${this.theme.css}
21473
21673
  ${REVIEW_CSS}
@@ -21477,7 +21677,7 @@ applyTheme(getEffectiveTheme());` : "";
21477
21677
  <main>
21478
21678
  <div class="hero-card card">
21479
21679
  <div class="review-header">
21480
- <h1>${escapeHtml3(this.title)}</h1>
21680
+ <h1>${escapeHtml4(this.title)}</h1>
21481
21681
  ${themeToggleHtml}
21482
21682
  </div>
21483
21683
  ${refsLine}
@@ -21492,7 +21692,7 @@ applyTheme(getEffectiveTheme());` : "";
21492
21692
  </section>
21493
21693
  <section class="card priority-banner">
21494
21694
  <h2>Review priority</h2>
21495
- <p class="subtle">${escapeHtml3(priority)}</p>
21695
+ <p class="subtle">${escapeHtml4(priority)}</p>
21496
21696
  </section>
21497
21697
  ${changedFilesPanel}
21498
21698
  <section class="toolbar">
@@ -21540,8 +21740,8 @@ applyTheme(getEffectiveTheme());` : "";
21540
21740
  };
21541
21741
 
21542
21742
  // src/deploy/ledger.ts
21543
- import * as fs7 from "fs";
21544
- import * as path8 from "path";
21743
+ import * as fs8 from "fs";
21744
+ import * as path9 from "path";
21545
21745
  function createEmptyLedger() {
21546
21746
  return {
21547
21747
  deployments: [],
@@ -21549,12 +21749,12 @@ function createEmptyLedger() {
21549
21749
  };
21550
21750
  }
21551
21751
  function loadLedger(ledgerPath) {
21552
- const resolved = path8.resolve(ledgerPath);
21553
- if (!fs7.existsSync(resolved)) {
21752
+ const resolved = path9.resolve(ledgerPath);
21753
+ if (!fs8.existsSync(resolved)) {
21554
21754
  return createEmptyLedger();
21555
21755
  }
21556
21756
  try {
21557
- const raw = JSON.parse(fs7.readFileSync(resolved, "utf8"));
21757
+ const raw = JSON.parse(fs8.readFileSync(resolved, "utf8"));
21558
21758
  if (raw.schemaVersion !== 1) {
21559
21759
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21560
21760
  }
@@ -21565,10 +21765,10 @@ function loadLedger(ledgerPath) {
21565
21765
  }
21566
21766
  }
21567
21767
  function saveLedger(ledger, ledgerPath) {
21568
- const resolved = path8.resolve(ledgerPath);
21569
- const dir = path8.dirname(resolved);
21570
- fs7.mkdirSync(dir, { recursive: true });
21571
- fs7.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21768
+ const resolved = path9.resolve(ledgerPath);
21769
+ const dir = path9.dirname(resolved);
21770
+ fs8.mkdirSync(dir, { recursive: true });
21771
+ fs8.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21572
21772
  }
21573
21773
  function getLatestDeployment(ledger, environment) {
21574
21774
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21688,11 +21888,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21688
21888
  const ext = FORMAT_EXTENSIONS[format];
21689
21889
  const effectiveName = outputName + (outputNameSuffix ?? "");
21690
21890
  if (mode === "aggregated") {
21691
- return toPosix(path9.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21891
+ return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21692
21892
  }
21693
21893
  const normalizedSource = toPosix(sourceFile);
21694
- const dirOfSource = path9.posix.dirname(normalizedSource);
21695
- let baseName = path9.posix.basename(normalizedSource);
21894
+ const dirOfSource = path10.posix.dirname(normalizedSource);
21895
+ let baseName = path10.posix.basename(normalizedSource);
21696
21896
  for (const testExt of TEST_EXTENSIONS) {
21697
21897
  if (baseName.endsWith(testExt)) {
21698
21898
  baseName = baseName.slice(0, -testExt.length);
@@ -21701,12 +21901,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21701
21901
  }
21702
21902
  const fileName = `${baseName}.${effectiveName}${ext}`;
21703
21903
  if (colocatedStyle === "adjacent") {
21704
- return toPosix(path9.posix.join(dirOfSource, fileName));
21904
+ return toPosix(path10.posix.join(dirOfSource, fileName));
21705
21905
  }
21706
21906
  if (colocatedStyle === "flat") {
21707
- return toPosix(path9.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21907
+ return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21708
21908
  }
21709
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
21909
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
21710
21910
  }
21711
21911
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
21712
21912
  const groups = /* @__PURE__ */ new Map();
@@ -21919,8 +22119,8 @@ var ReportGenerator = class {
21919
22119
  if (astroPaths) {
21920
22120
  for (const mdPath of astroPaths) {
21921
22121
  const content = await fsPromises.readFile(mdPath, "utf8");
21922
- const mdDir = path9.dirname(mdPath);
21923
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
22122
+ const mdDir = path10.dirname(mdPath);
22123
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
21924
22124
  const result = copyMarkdownAssets({
21925
22125
  markdown: content,
21926
22126
  markdownDir: mdDir,
@@ -21951,9 +22151,9 @@ var ReportGenerator = class {
21951
22151
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
21952
22152
  const ext = FORMAT_EXTENSIONS[format];
21953
22153
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
21954
- const outputPath = toPosix(path9.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22154
+ const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21955
22155
  const content = await this.formatContent(run, format);
21956
- const dir = path9.dirname(outputPath);
22156
+ const dir = path10.dirname(outputPath);
21957
22157
  await fsPromises.mkdir(dir, { recursive: true });
21958
22158
  await this.deps.writeFile(outputPath, content);
21959
22159
  return [outputPath];
@@ -21965,7 +22165,7 @@ var ReportGenerator = class {
21965
22165
  testCases
21966
22166
  };
21967
22167
  const content = await this.formatContent(groupRun, format);
21968
- const dir = path9.dirname(outputPath);
22168
+ const dir = path10.dirname(outputPath);
21969
22169
  await fsPromises.mkdir(dir, { recursive: true });
21970
22170
  await this.deps.writeFile(outputPath, content);
21971
22171
  writtenPaths.push(outputPath);
@@ -22114,7 +22314,7 @@ async function generateRunComparison(args) {
22114
22314
  await fsPromises.mkdir(outputDir, { recursive: true });
22115
22315
  for (const format of args.formats) {
22116
22316
  const ext = format === "html" ? ".html" : ".md";
22117
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
22317
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
22118
22318
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
22119
22319
  await fsPromises.writeFile(outputPath, content, "utf8");
22120
22320
  files.push(outputPath);
@@ -22123,21 +22323,21 @@ async function generateRunComparison(args) {
22123
22323
  }
22124
22324
 
22125
22325
  // src/init-astro.ts
22126
- import * as fs9 from "fs";
22127
- import * as path10 from "path";
22326
+ import * as fs10 from "fs";
22327
+ import * as path11 from "path";
22128
22328
  import { fileURLToPath } from "url";
22129
- var __dirname = path10.dirname(fileURLToPath(import.meta.url));
22329
+ var __dirname = path11.dirname(fileURLToPath(import.meta.url));
22130
22330
  var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
22131
22331
  var FRAMEWORK_FILES = ["tsconfig.json"];
22132
22332
  function isScaffoldedAstroSite(dir) {
22133
- return fs9.existsSync(path10.join(dir, "astro.config.mjs"));
22333
+ return fs10.existsSync(path11.join(dir, "astro.config.mjs"));
22134
22334
  }
22135
22335
  function initAstro(options = {}) {
22136
22336
  const targetDir = options.targetDir ?? "./story-docs";
22137
22337
  const force = options.force ?? false;
22138
22338
  const update = options.update ?? false;
22139
- const templateDir = path10.resolve(__dirname, "..", "templates", "astro-starlight");
22140
- if (!fs9.existsSync(templateDir)) {
22339
+ const templateDir = path11.resolve(__dirname, "..", "templates", "astro-starlight");
22340
+ if (!fs10.existsSync(templateDir)) {
22141
22341
  throw new Error(
22142
22342
  `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
22143
22343
  );
@@ -22145,8 +22345,8 @@ function initAstro(options = {}) {
22145
22345
  if (update) {
22146
22346
  return updateFrameworkFiles(templateDir, targetDir);
22147
22347
  }
22148
- if (fs9.existsSync(targetDir)) {
22149
- const entries = fs9.readdirSync(targetDir);
22348
+ if (fs10.existsSync(targetDir)) {
22349
+ const entries = fs10.readdirSync(targetDir);
22150
22350
  if (entries.length > 0 && !force) {
22151
22351
  throw new Error(
22152
22352
  `Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
@@ -22164,25 +22364,25 @@ function updateFrameworkFiles(templateDir, targetDir) {
22164
22364
  }
22165
22365
  const updated = [];
22166
22366
  for (const dir of FRAMEWORK_DIRS) {
22167
- const src = path10.join(templateDir, dir);
22168
- if (!fs9.existsSync(src)) continue;
22169
- copyDirRecursive(src, path10.join(targetDir, dir), (rel) => updated.push(path10.join(dir, rel)));
22367
+ const src = path11.join(templateDir, dir);
22368
+ if (!fs10.existsSync(src)) continue;
22369
+ copyDirRecursive(src, path11.join(targetDir, dir), (rel) => updated.push(path11.join(dir, rel)));
22170
22370
  }
22171
22371
  for (const file of FRAMEWORK_FILES) {
22172
- const src = path10.join(templateDir, file);
22173
- if (!fs9.existsSync(src)) continue;
22174
- fs9.copyFileSync(src, path10.join(targetDir, file));
22372
+ const src = path11.join(templateDir, file);
22373
+ if (!fs10.existsSync(src)) continue;
22374
+ fs10.copyFileSync(src, path11.join(targetDir, file));
22175
22375
  updated.push(file);
22176
22376
  }
22177
22377
  if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
22178
22378
  return { targetDir, updatedFiles: updated };
22179
22379
  }
22180
22380
  function mergeDependencies(templateDir, targetDir) {
22181
- const tmplPkgPath = path10.join(templateDir, "package.json");
22182
- const userPkgPath = path10.join(targetDir, "package.json");
22183
- if (!fs9.existsSync(tmplPkgPath) || !fs9.existsSync(userPkgPath)) return false;
22184
- const tmpl = JSON.parse(fs9.readFileSync(tmplPkgPath, "utf8"));
22185
- const user = JSON.parse(fs9.readFileSync(userPkgPath, "utf8"));
22381
+ const tmplPkgPath = path11.join(templateDir, "package.json");
22382
+ const userPkgPath = path11.join(targetDir, "package.json");
22383
+ if (!fs10.existsSync(tmplPkgPath) || !fs10.existsSync(userPkgPath)) return false;
22384
+ const tmpl = JSON.parse(fs10.readFileSync(tmplPkgPath, "utf8"));
22385
+ const user = JSON.parse(fs10.readFileSync(userPkgPath, "utf8"));
22186
22386
  user.dependencies = user.dependencies ?? {};
22187
22387
  let changed = false;
22188
22388
  for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
@@ -22192,30 +22392,30 @@ function mergeDependencies(templateDir, targetDir) {
22192
22392
  }
22193
22393
  }
22194
22394
  if (changed) {
22195
- fs9.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
22395
+ fs10.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
22196
22396
  `, "utf8");
22197
22397
  }
22198
22398
  return changed;
22199
22399
  }
22200
22400
  function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22201
- fs9.mkdirSync(dest, { recursive: true });
22202
- const entries = fs9.readdirSync(src, { withFileTypes: true });
22401
+ fs10.mkdirSync(dest, { recursive: true });
22402
+ const entries = fs10.readdirSync(src, { withFileTypes: true });
22203
22403
  for (const entry of entries) {
22204
- const srcPath = path10.join(src, entry.name);
22404
+ const srcPath = path11.join(src, entry.name);
22205
22405
  const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
22206
- const destPath = path10.join(dest, destName);
22406
+ const destPath = path11.join(dest, destName);
22207
22407
  if (entry.isDirectory()) {
22208
22408
  copyDirRecursive(srcPath, destPath, onFile, baseSrc);
22209
22409
  } else {
22210
- fs9.copyFileSync(srcPath, destPath);
22211
- onFile?.(path10.relative(baseSrc, srcPath));
22410
+ fs10.copyFileSync(srcPath, destPath);
22411
+ onFile?.(path11.relative(baseSrc, srcPath));
22212
22412
  }
22213
22413
  }
22214
22414
  }
22215
22415
 
22216
22416
  // src/scaffold-doc.ts
22217
- import * as fs10 from "fs";
22218
- import * as path11 from "path";
22417
+ import * as fs11 from "fs";
22418
+ import * as path12 from "path";
22219
22419
  var TEMPLATES = [
22220
22420
  "adr",
22221
22421
  "runbook",
@@ -22232,7 +22432,7 @@ function isoDate(today) {
22232
22432
  function nextSeq(dir) {
22233
22433
  let max = 0;
22234
22434
  try {
22235
- for (const entry of fs10.readdirSync(dir)) {
22435
+ for (const entry of fs11.readdirSync(dir)) {
22236
22436
  const match = /^(\d{1,4})-/.exec(entry);
22237
22437
  if (match) max = Math.max(max, Number.parseInt(match[1], 10));
22238
22438
  }
@@ -22389,12 +22589,12 @@ function scaffoldDoc(options) {
22389
22589
  );
22390
22590
  }
22391
22591
  const spec = TEMPLATE_SPECS[template];
22392
- const baseDir = options.baseDir ?? path11.join("src", "content", "docs");
22592
+ const baseDir = options.baseDir ?? path12.join("src", "content", "docs");
22393
22593
  const today = options.today ?? /* @__PURE__ */ new Date();
22394
22594
  const name = (options.name ?? "").trim() || defaultName(template);
22395
22595
  const slug2 = slugify3(name);
22396
22596
  const scenarioId = normalizeScenarioId(options.scenarioId);
22397
- const dir = path11.join(baseDir, spec.subdir);
22597
+ const dir = path12.join(baseDir, spec.subdir);
22398
22598
  if (template === "scenario-note" && !scenarioId) {
22399
22599
  throw new Error(`Template "scenario-note" requires --scenario-id.`);
22400
22600
  }
@@ -22406,14 +22606,14 @@ function scaffoldDoc(options) {
22406
22606
  seq: nextSeq(dir)
22407
22607
  };
22408
22608
  const filename = `${spec.filename(slug2, ctx)}.mdx`;
22409
- const filePath = path11.join(dir, filename);
22410
- if (fs10.existsSync(filePath) && !options.force) {
22609
+ const filePath = path12.join(dir, filename);
22610
+ if (fs11.existsSync(filePath) && !options.force) {
22411
22611
  throw new Error(
22412
22612
  `File "${filePath}" already exists. Use --force to overwrite.`
22413
22613
  );
22414
22614
  }
22415
- fs10.mkdirSync(dir, { recursive: true });
22416
- fs10.writeFileSync(filePath, spec.content(ctx), "utf8");
22615
+ fs11.mkdirSync(dir, { recursive: true });
22616
+ fs11.writeFileSync(filePath, spec.content(ctx), "utf8");
22417
22617
  return { template, path: filePath, title: titleFor2(template, ctx) };
22418
22618
  }
22419
22619
  function defaultName(template) {
@@ -22454,20 +22654,20 @@ function normalizeScenarioId(input) {
22454
22654
  }
22455
22655
 
22456
22656
  // src/check-links.ts
22457
- import * as fs12 from "fs";
22458
- import * as path13 from "path";
22657
+ import * as fs13 from "fs";
22658
+ import * as path14 from "path";
22459
22659
 
22460
22660
  // src/utils/markdown-files.ts
22461
- import * as fs11 from "fs";
22462
- import * as path12 from "path";
22661
+ import * as fs12 from "fs";
22662
+ import * as path13 from "path";
22463
22663
  function collectMarkdownFiles(target) {
22464
- if (!fs11.existsSync(target)) return [];
22465
- if (fs11.statSync(target).isFile()) return [target];
22664
+ if (!fs12.existsSync(target)) return [];
22665
+ if (fs12.statSync(target).isFile()) return [target];
22466
22666
  const out = [];
22467
22667
  const walk = (dir) => {
22468
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22668
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
22469
22669
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22470
- const full = path12.join(dir, entry.name);
22670
+ const full = path13.join(dir, entry.name);
22471
22671
  if (entry.isDirectory()) walk(full);
22472
22672
  else if (/\.mdx?$/u.test(entry.name)) out.push(full);
22473
22673
  }
@@ -22506,17 +22706,17 @@ function classifyLink(link2) {
22506
22706
  function resolutionCandidates(fromFile, link2) {
22507
22707
  const withoutAnchor = link2.split("#")[0];
22508
22708
  if (!withoutAnchor) return [];
22509
- const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
22709
+ const base = path14.resolve(path14.dirname(fromFile), withoutAnchor);
22510
22710
  const candidates = [base];
22511
- if (!path13.extname(base)) {
22711
+ if (!path14.extname(base)) {
22512
22712
  candidates.push(`${base}.md`, `${base}.mdx`);
22513
- candidates.push(path13.join(base, "index.md"), path13.join(base, "index.mdx"));
22713
+ candidates.push(path14.join(base, "index.md"), path14.join(base, "index.mdx"));
22514
22714
  }
22515
22715
  return candidates;
22516
22716
  }
22517
22717
  function resolvesOnDisk(fromFile, link2) {
22518
22718
  return resolutionCandidates(fromFile, link2).some(
22519
- (candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
22719
+ (candidate) => fs13.existsSync(candidate) && fs13.statSync(candidate).isFile()
22520
22720
  );
22521
22721
  }
22522
22722
  async function isExternalAlive(url, timeoutMs) {
@@ -22542,7 +22742,7 @@ async function isExternalAlive(url, timeoutMs) {
22542
22742
  }
22543
22743
  async function checkLinks(options) {
22544
22744
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
22545
- if (!fs12.existsSync(target)) {
22745
+ if (!fs13.existsSync(target)) {
22546
22746
  throw new Error(`Path not found: ${target}`);
22547
22747
  }
22548
22748
  const files = collectMarkdownFiles(target);
@@ -22552,7 +22752,7 @@ async function checkLinks(options) {
22552
22752
  let skipped = 0;
22553
22753
  const externalCache = /* @__PURE__ */ new Map();
22554
22754
  for (const file of files) {
22555
- const content = fs12.readFileSync(file, "utf8");
22755
+ const content = fs13.readFileSync(file, "utf8");
22556
22756
  for (const link2 of extractLinks(content)) {
22557
22757
  const kind = classifyLink(link2);
22558
22758
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -22608,8 +22808,8 @@ function formatLinkReport(report) {
22608
22808
  }
22609
22809
 
22610
22810
  // src/import-openapi.ts
22611
- import * as fs13 from "fs";
22612
- import * as path14 from "path";
22811
+ import * as fs14 from "fs";
22812
+ import * as path15 from "path";
22613
22813
  import { parse as parseYamlString } from "yaml";
22614
22814
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
22615
22815
  function parseYaml(raw, specPath) {
@@ -22622,9 +22822,9 @@ function parseYaml(raw, specPath) {
22622
22822
  }
22623
22823
  }
22624
22824
  function parseSpec(specPath) {
22625
- if (!fs13.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22626
- const raw = fs13.readFileSync(specPath, "utf8");
22627
- const ext = path14.extname(specPath).toLowerCase();
22825
+ if (!fs14.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22826
+ const raw = fs14.readFileSync(specPath, "utf8");
22827
+ const ext = path15.extname(specPath).toLowerCase();
22628
22828
  if (ext === ".json") return JSON.parse(raw);
22629
22829
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
22630
22830
  try {
@@ -22655,8 +22855,8 @@ function extractEndpoints(spec) {
22655
22855
  }
22656
22856
  function loadScenarios(runFile) {
22657
22857
  if (!runFile) return [];
22658
- if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22659
- const report = JSON.parse(fs13.readFileSync(runFile, "utf8"));
22858
+ if (!fs14.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22859
+ const report = JSON.parse(fs14.readFileSync(runFile, "utf8"));
22660
22860
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
22661
22861
  }
22662
22862
  function endpointRefs(endpoint) {
@@ -22763,25 +22963,25 @@ async function importOpenApi(options) {
22763
22963
  list.push(item);
22764
22964
  groups.set(item.endpoint.tag, list);
22765
22965
  }
22766
- const outputDir = options.outputDir ?? path14.join("src", "content", "docs", "api");
22767
- if (fs13.existsSync(outputDir) && !options.force) {
22768
- const entries = fs13.readdirSync(outputDir);
22966
+ const outputDir = options.outputDir ?? path15.join("src", "content", "docs", "api");
22967
+ if (fs14.existsSync(outputDir) && !options.force) {
22968
+ const entries = fs14.readdirSync(outputDir);
22769
22969
  if (entries.length > 0) {
22770
22970
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
22771
22971
  }
22772
22972
  }
22773
- fs13.mkdirSync(outputDir, { recursive: true });
22973
+ fs14.mkdirSync(outputDir, { recursive: true });
22774
22974
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
22775
22975
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
22776
- fs13.writeFileSync(
22777
- path14.join(outputDir, "index.mdx"),
22976
+ fs14.writeFileSync(
22977
+ path15.join(outputDir, "index.mdx"),
22778
22978
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
22779
22979
  "utf8"
22780
22980
  );
22781
22981
  for (const [tag, rows] of groups) {
22782
- const dir = path14.join(outputDir, slug(tag));
22783
- fs13.mkdirSync(dir, { recursive: true });
22784
- fs13.writeFileSync(path14.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22982
+ const dir = path15.join(outputDir, slug(tag));
22983
+ fs14.mkdirSync(dir, { recursive: true });
22984
+ fs14.writeFileSync(path15.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22785
22985
  }
22786
22986
  return {
22787
22987
  outputDir,
@@ -22793,8 +22993,8 @@ async function importOpenApi(options) {
22793
22993
  }
22794
22994
 
22795
22995
  // src/build-docs.ts
22796
- import * as fs15 from "fs";
22797
- import * as path16 from "path";
22996
+ import * as fs16 from "fs";
22997
+ import * as path17 from "path";
22798
22998
 
22799
22999
  // src/scenario-links.ts
22800
23000
  function scenarioAnchor(title) {
@@ -22891,8 +23091,8 @@ ${body.join("\n")}
22891
23091
  }
22892
23092
 
22893
23093
  // src/notes-index.ts
22894
- import * as fs14 from "fs";
22895
- import * as path15 from "path";
23094
+ import * as fs15 from "fs";
23095
+ import * as path16 from "path";
22896
23096
  import { slug as githubSlug } from "github-slugger";
22897
23097
  import { parse as parseYaml2 } from "yaml";
22898
23098
  function buildScenarioNotesIndex(notesDir) {
@@ -22903,8 +23103,8 @@ function buildScenarioNotesIndex(notesDir) {
22903
23103
  };
22904
23104
  }
22905
23105
  function writeNotesIndex(index, outPath) {
22906
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22907
- fs14.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
23106
+ fs15.mkdirSync(path16.dirname(outPath), { recursive: true });
23107
+ fs15.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
22908
23108
  return index;
22909
23109
  }
22910
23110
  function notesByScenarioId(index) {
@@ -22921,10 +23121,10 @@ function noteLinkMarkdown(note) {
22921
23121
  return `[Business context \u2192](${noteHref(note)})`;
22922
23122
  }
22923
23123
  function readScenarioNote(filePath, notesDir) {
22924
- const relative5 = path15.relative(notesDir, filePath);
23124
+ const relative5 = path16.relative(notesDir, filePath);
22925
23125
  const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
22926
- const frontmatter = parseFrontmatter(fs14.readFileSync(filePath, "utf8"));
22927
- const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path15.basename(stem);
23126
+ const frontmatter = parseFrontmatter(fs15.readFileSync(filePath, "utf8"));
23127
+ const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path16.basename(stem);
22928
23128
  const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
22929
23129
  return {
22930
23130
  scenarioId,
@@ -22939,7 +23139,7 @@ function parseFrontmatter(source) {
22939
23139
  return parsed && typeof parsed === "object" ? parsed : {};
22940
23140
  }
22941
23141
  function toRouteSlug(stem) {
22942
- return stem.split(path15.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
23142
+ return stem.split(path16.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
22943
23143
  }
22944
23144
 
22945
23145
  // src/overview-page.ts
@@ -23042,22 +23242,22 @@ var BuildDocsError = class extends Error {
23042
23242
  };
23043
23243
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
23044
23244
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
23045
- if (!fs15.existsSync(reportPath)) return 0;
23046
- const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
23245
+ if (!fs16.existsSync(reportPath)) return 0;
23246
+ const report = JSON.parse(fs16.readFileSync(reportPath, "utf8"));
23047
23247
  let copied = 0;
23048
23248
  const bundle = (value) => {
23049
- const rel = copyAsset(path16.resolve(value), assetsDir);
23249
+ const rel = copyAsset(path17.resolve(value), assetsDir);
23050
23250
  copied++;
23051
- return `${baseUrl}/${path16.basename(rel)}`;
23251
+ return `${baseUrl}/${path17.basename(rel)}`;
23052
23252
  };
23053
23253
  const visit = (entries) => {
23054
23254
  for (const entry of entries ?? []) {
23055
23255
  const e = entry;
23056
23256
  if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
23057
- if (typeof e.path === "string" && !isRemote(e.path) && fs15.existsSync(e.path)) {
23257
+ if (typeof e.path === "string" && !isRemote(e.path) && fs16.existsSync(e.path)) {
23058
23258
  e.path = bundle(e.path);
23059
23259
  }
23060
- if (typeof e.poster === "string" && !isRemote(e.poster) && fs15.existsSync(e.poster)) {
23260
+ if (typeof e.poster === "string" && !isRemote(e.poster) && fs16.existsSync(e.poster)) {
23061
23261
  e.poster = bundle(e.poster);
23062
23262
  }
23063
23263
  }
@@ -23070,7 +23270,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
23070
23270
  }
23071
23271
  }
23072
23272
  if (copied > 0) {
23073
- fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
23273
+ fs16.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
23074
23274
  }
23075
23275
  return copied;
23076
23276
  }
@@ -23093,9 +23293,9 @@ function changeBadgeLookup(diff) {
23093
23293
  return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
23094
23294
  }
23095
23295
  function readStoryReport(reportPath) {
23096
- if (!fs15.existsSync(reportPath)) return null;
23296
+ if (!fs16.existsSync(reportPath)) return null;
23097
23297
  try {
23098
- return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
23298
+ return JSON.parse(fs16.readFileSync(reportPath, "utf8"));
23099
23299
  } catch {
23100
23300
  return null;
23101
23301
  }
@@ -23120,28 +23320,28 @@ function writeScenarioLinks(reportPath, outDir, options = {}) {
23120
23320
  const report = readStoryReport(reportPath);
23121
23321
  if (!report) return null;
23122
23322
  const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
23123
- fs15.writeFileSync(
23124
- path16.join(outDir, "scenario-links.json"),
23323
+ fs16.writeFileSync(
23324
+ path17.join(outDir, "scenario-links.json"),
23125
23325
  JSON.stringify(index, null, 2),
23126
23326
  "utf8"
23127
23327
  );
23128
23328
  return index;
23129
23329
  }
23130
23330
  function clearGeneratedPages(dir) {
23131
- if (!fs15.existsSync(dir)) return;
23132
- for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
23133
- const full = path16.join(dir, entry.name);
23331
+ if (!fs16.existsSync(dir)) return;
23332
+ for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
23333
+ const full = path17.join(dir, entry.name);
23134
23334
  if (entry.isDirectory()) {
23135
23335
  clearGeneratedPages(full);
23136
- if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
23336
+ if (fs16.readdirSync(full).length === 0) fs16.rmdirSync(full);
23137
23337
  } else if (/\.mdx?$/.test(entry.name)) {
23138
- fs15.rmSync(full);
23338
+ fs16.rmSync(full);
23139
23339
  }
23140
23340
  }
23141
23341
  }
23142
23342
  function loadCanonicalRun(rawRunPath, synthesize) {
23143
23343
  try {
23144
- const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
23344
+ const data = JSON.parse(fs16.readFileSync(path17.resolve(rawRunPath), "utf8"));
23145
23345
  if (data.schemaVersion !== 1) {
23146
23346
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
23147
23347
  }
@@ -23164,19 +23364,19 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
23164
23364
  }
23165
23365
  }
23166
23366
  async function buildDocs(options) {
23167
- const siteDir = path16.resolve(options.siteDir);
23367
+ const siteDir = path17.resolve(options.siteDir);
23168
23368
  if (!isScaffoldedAstroSite(siteDir)) {
23169
23369
  throw new BuildDocsError(
23170
23370
  `"${siteDir}" is not a scaffolded Astro docs site (no astro.config.mjs). Run "executable-stories init-astro <dir>" first, then pass it with --site-dir <dir>.`,
23171
23371
  "usage"
23172
23372
  );
23173
23373
  }
23174
- const storiesPublicDir = path16.join(siteDir, "public", "stories");
23175
- const assetsDir = path16.join(storiesPublicDir, "assets");
23176
- const storyPagesDir = path16.join(siteDir, "src", "content", "docs", "stories");
23177
- const notesDir = path16.join(siteDir, "src", "content", "docs", "notes");
23178
- const apiDir = path16.join(siteDir, "src", "content", "docs", "api");
23179
- const reportPath = path16.join(storiesPublicDir, "story-report.json");
23374
+ const storiesPublicDir = path17.join(siteDir, "public", "stories");
23375
+ const assetsDir = path17.join(storiesPublicDir, "assets");
23376
+ const storyPagesDir = path17.join(siteDir, "src", "content", "docs", "stories");
23377
+ const notesDir = path17.join(siteDir, "src", "content", "docs", "notes");
23378
+ const apiDir = path17.join(siteDir, "src", "content", "docs", "api");
23379
+ const reportPath = path17.join(storiesPublicDir, "story-report.json");
23180
23380
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
23181
23381
  try {
23182
23382
  await new ReportGenerator({
@@ -23187,7 +23387,7 @@ async function buildDocs(options) {
23187
23387
  const currentReport = readStoryReport(reportPath);
23188
23388
  let diff;
23189
23389
  if (options.baselinePath) {
23190
- const baselineResolved = path16.resolve(options.baselinePath);
23390
+ const baselineResolved = path17.resolve(options.baselinePath);
23191
23391
  const baseline = readStoryReport(baselineResolved);
23192
23392
  if (!baseline) {
23193
23393
  throw new BuildDocsError(
@@ -23225,7 +23425,7 @@ async function buildDocs(options) {
23225
23425
  const sub = partitioned[audience];
23226
23426
  audiences[audience] = sub.testCases.length;
23227
23427
  if (sub.testCases.length === 0) continue;
23228
- await genPages(sub, path16.join(storyPagesDir, audience));
23428
+ await genPages(sub, path17.join(storyPagesDir, audience));
23229
23429
  }
23230
23430
  } else {
23231
23431
  await genPages(canonical, storyPagesDir);
@@ -23235,30 +23435,30 @@ async function buildDocs(options) {
23235
23435
  audienceSplit: options.audienceSplit ?? false
23236
23436
  });
23237
23437
  const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
23238
- writeNotesIndex(notesIndex, path16.join(storiesPublicDir, "notes-index.json"));
23438
+ writeNotesIndex(notesIndex, path17.join(storiesPublicDir, "notes-index.json"));
23239
23439
  const notesIndexed = notesIndex.notes.length;
23240
23440
  if (linksIndex) {
23241
- fs15.writeFileSync(
23242
- path16.join(storyPagesDir, "index.md"),
23441
+ fs16.writeFileSync(
23442
+ path17.join(storyPagesDir, "index.md"),
23243
23443
  renderOverviewPage(linksIndex, notesIndex),
23244
23444
  "utf8"
23245
23445
  );
23246
23446
  }
23247
- const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
23248
- const changesMdPath = path16.join(storyPagesDir, "changes.md");
23447
+ const changesJsonPath = path17.join(storiesPublicDir, "changes.json");
23448
+ const changesMdPath = path17.join(storyPagesDir, "changes.md");
23249
23449
  let changes;
23250
23450
  if (diff && linksIndex) {
23251
- fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23252
- fs15.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
23451
+ fs16.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23452
+ fs16.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
23253
23453
  changes = diff.summary;
23254
23454
  } else {
23255
- fs15.rmSync(changesJsonPath, { force: true });
23256
- fs15.rmSync(changesMdPath, { force: true });
23455
+ fs16.rmSync(changesJsonPath, { force: true });
23456
+ fs16.rmSync(changesMdPath, { force: true });
23257
23457
  }
23258
23458
  let apiPages = 0;
23259
23459
  if (options.openapiPath) {
23260
23460
  const res = await importOpenApi({
23261
- specPath: path16.resolve(options.openapiPath),
23461
+ specPath: path17.resolve(options.openapiPath),
23262
23462
  outputDir: apiDir,
23263
23463
  runFile: reportPath,
23264
23464
  force: true
@@ -23273,11 +23473,11 @@ async function buildDocs(options) {
23273
23473
  }
23274
23474
 
23275
23475
  // src/config.ts
23276
- import { existsSync as existsSync13 } from "fs";
23277
- import { resolve as resolve10 } from "path";
23476
+ import { existsSync as existsSync14 } from "fs";
23477
+ import { resolve as resolve11 } from "path";
23278
23478
  async function loadConfig(configPath) {
23279
- const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
23280
- if (!existsSync13(resolved)) return {};
23479
+ const resolved = configPath ? resolve11(configPath) : resolve11(process.cwd(), "executable-stories.config.js");
23480
+ if (!existsSync14(resolved)) return {};
23281
23481
  const mod = await import(resolved);
23282
23482
  const config = mod.default;
23283
23483
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -23305,6 +23505,7 @@ USAGE
23305
23505
  executable-stories format <file> [options]
23306
23506
  executable-stories format --stdin [options]
23307
23507
  executable-stories watch <raw-run.json> [options]
23508
+ executable-stories serve <raw-run.json> [--port <n>] [--host <host>] [options]
23308
23509
  executable-stories compare <baseline-file> <current-file> [options]
23309
23510
  executable-stories gate-release <dev-run.json> <rc-run.json> [options]
23310
23511
  executable-stories review <file> --changed-files <path> [options]
@@ -23328,6 +23529,7 @@ USAGE
23328
23529
  SUBCOMMANDS
23329
23530
  format Read raw test results and generate reports
23330
23531
  watch Regenerate reports whenever the raw-run file changes (live agent index)
23532
+ serve Live docs URL: regenerate + browser reload + "what changed since you started" (for agent loops)
23331
23533
  compare Compare two runs and generate a diff report
23332
23534
  gate-release Verify a release candidate against the dev test baseline (RC gate)
23333
23535
  review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
@@ -23546,9 +23748,9 @@ async function parseCliArgs(argv) {
23546
23748
  process.exit(EXIT_SUCCESS);
23547
23749
  }
23548
23750
  const subcommand = args[0];
23549
- if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
23751
+ if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "serve" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
23550
23752
  console.error(
23551
- `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
23753
+ `Unknown subcommand: "${subcommand}". Use "format", "watch", "serve", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
23552
23754
  );
23553
23755
  process.exit(EXIT_USAGE);
23554
23756
  }
@@ -23666,6 +23868,8 @@ async function parseCliArgs(argv) {
23666
23868
  "webhook-hmac-timestamp": { type: "boolean", default: false },
23667
23869
  "asset-mode": { type: "string", default: "none" },
23668
23870
  "allow-missing-assets": { type: "boolean", default: false },
23871
+ port: { type: "string" },
23872
+ host: { type: "string" },
23669
23873
  "pr-summary": { type: "boolean", default: false },
23670
23874
  "pr-summary-file": { type: "string" },
23671
23875
  "fail-on-regression": { type: "boolean", default: false },
@@ -23888,7 +24092,9 @@ async function parseCliArgs(argv) {
23888
24092
  headRef: values["head-ref"],
23889
24093
  failOn: failOnRaw,
23890
24094
  minEvidence: minEvidenceRaw,
23891
- config: values["config"]
24095
+ config: values["config"],
24096
+ servePort: values["port"] ? Number.parseInt(values["port"], 10) : 4321,
24097
+ serveHost: values["host"] ?? "127.0.0.1"
23892
24098
  };
23893
24099
  return { args: cliArgs, pluginConfig, customRequested };
23894
24100
  }
@@ -23896,27 +24102,27 @@ async function readInput(args) {
23896
24102
  if (args.stdin) {
23897
24103
  return readStdin();
23898
24104
  }
23899
- const filePath = path17.resolve(args.inputFile);
23900
- if (!fs16.existsSync(filePath)) {
24105
+ const filePath = path18.resolve(args.inputFile);
24106
+ if (!fs17.existsSync(filePath)) {
23901
24107
  console.error(`Error: File not found: ${filePath}`);
23902
24108
  process.exit(EXIT_USAGE);
23903
24109
  }
23904
- return fs16.readFileSync(filePath, "utf8");
24110
+ return fs17.readFileSync(filePath, "utf8");
23905
24111
  }
23906
24112
  function readFileInput(filePath) {
23907
- const resolved = path17.resolve(filePath);
23908
- if (!fs16.existsSync(resolved)) {
24113
+ const resolved = path18.resolve(filePath);
24114
+ if (!fs17.existsSync(resolved)) {
23909
24115
  console.error(`Error: File not found: ${resolved}`);
23910
24116
  process.exit(EXIT_USAGE);
23911
24117
  }
23912
- return fs16.readFileSync(resolved, "utf8");
24118
+ return fs17.readFileSync(resolved, "utf8");
23913
24119
  }
23914
24120
  function readStdin() {
23915
- return new Promise((resolve12, reject) => {
24121
+ return new Promise((resolve13, reject) => {
23916
24122
  const chunks = [];
23917
24123
  process.stdin.setEncoding("utf8");
23918
24124
  process.stdin.on("data", (chunk) => chunks.push(chunk));
23919
- process.stdin.on("end", () => resolve12(chunks.join("")));
24125
+ process.stdin.on("end", () => resolve13(chunks.join("")));
23920
24126
  process.stdin.on("error", reject);
23921
24127
  });
23922
24128
  }
@@ -24042,14 +24248,14 @@ function tryNormalizeRunFromText(text2, args) {
24042
24248
  }
24043
24249
  }
24044
24250
  function listBaselineCandidates(currentFile, args) {
24045
- const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
24046
- const currentResolved = path17.resolve(currentFile);
24047
- if (!fs16.existsSync(baselineDir)) {
24251
+ const baselineDir = path18.resolve(args.baselineDir ?? path18.dirname(currentFile));
24252
+ const currentResolved = path18.resolve(currentFile);
24253
+ if (!fs17.existsSync(baselineDir)) {
24048
24254
  console.error(`Error: baseline directory not found: ${baselineDir}`);
24049
24255
  process.exit(EXIT_USAGE);
24050
24256
  }
24051
- const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
24052
- return entries.filter((entry) => entry.isFile()).map((entry) => path17.join(baselineDir, entry.name)).filter((candidate) => path17.resolve(candidate) !== currentResolved).filter(
24257
+ const entries = fs17.readdirSync(baselineDir, { withFileTypes: true });
24258
+ return entries.filter((entry) => entry.isFile()).map((entry) => path18.join(baselineDir, entry.name)).filter((candidate) => path18.resolve(candidate) !== currentResolved).filter(
24053
24259
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
24054
24260
  );
24055
24261
  }
@@ -24057,14 +24263,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
24057
24263
  const candidates = listBaselineCandidates(currentFile, args);
24058
24264
  const comparable = [];
24059
24265
  for (const candidate of candidates) {
24060
- const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
24266
+ const run = tryNormalizeRunFromText(fs17.readFileSync(candidate, "utf8"), args);
24061
24267
  if (run) {
24062
24268
  comparable.push({ file: candidate, run });
24063
24269
  }
24064
24270
  }
24065
24271
  if (comparable.length === 0) {
24066
24272
  console.error(
24067
- `Error: no compatible baseline files found in ${path17.resolve(args.baselineDir ?? path17.dirname(currentFile))}.`
24273
+ `Error: no compatible baseline files found in ${path18.resolve(args.baselineDir ?? path18.dirname(currentFile))}.`
24068
24274
  );
24069
24275
  process.exit(EXIT_USAGE);
24070
24276
  }
@@ -24266,6 +24472,24 @@ async function main() {
24266
24472
  });
24267
24473
  return;
24268
24474
  }
24475
+ if (args.subcommand === "serve") {
24476
+ if (!args.inputFile) {
24477
+ console.error("Error: serve requires an input file (the raw-run JSON the framework writes).");
24478
+ process.exit(EXIT_USAGE);
24479
+ }
24480
+ const serveFormats = args.formats.includes("html") ? args.formats : [...args.formats, "html"];
24481
+ startServe({
24482
+ input: args.inputFile,
24483
+ outputDir: args.outputDir,
24484
+ outputName: args.outputName,
24485
+ formats: serveFormats,
24486
+ inputType: args.inputType === "canonical" ? "canonical" : "raw",
24487
+ synthesize: args.synthesizeStories,
24488
+ port: args.servePort,
24489
+ host: args.serveHost
24490
+ });
24491
+ return;
24492
+ }
24269
24493
  const text2 = await readInput(args);
24270
24494
  if (args.inputType === "ndjson") {
24271
24495
  if (args.subcommand === "validate") {
@@ -24309,9 +24533,9 @@ async function main() {
24309
24533
  process.exit(EXIT_SCHEMA_VALIDATION);
24310
24534
  }
24311
24535
  if (args.emitCanonical) {
24312
- const outPath = path17.resolve(args.emitCanonical);
24313
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24314
- fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24536
+ const outPath = path18.resolve(args.emitCanonical);
24537
+ fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
24538
+ fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24315
24539
  }
24316
24540
  try {
24317
24541
  const result = await generateReports(run, args);
@@ -24368,9 +24592,9 @@ ${msg}`);
24368
24592
  }
24369
24593
  const run = data;
24370
24594
  if (args.emitCanonical) {
24371
- const outPath = path17.resolve(args.emitCanonical);
24372
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24373
- fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24595
+ const outPath = path18.resolve(args.emitCanonical);
24596
+ fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
24597
+ fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24374
24598
  }
24375
24599
  try {
24376
24600
  const result = await generateReports(run, args);
@@ -24426,9 +24650,9 @@ ${msg}`);
24426
24650
  process.exit(EXIT_CANONICAL_VALIDATION);
24427
24651
  }
24428
24652
  if (args.emitCanonical) {
24429
- const outPath = path17.resolve(args.emitCanonical);
24430
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24431
- fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
24653
+ const outPath = path18.resolve(args.emitCanonical);
24654
+ fs17.mkdirSync(path18.dirname(outPath), { recursive: true });
24655
+ fs17.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
24432
24656
  }
24433
24657
  try {
24434
24658
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -24453,9 +24677,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
24453
24677
  const ext = formatter.fileExtension ?? formatName;
24454
24678
  const baseName = args.outputName ?? "report";
24455
24679
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
24456
- const filepath = path17.join(outputDir, filename);
24457
- fs16.mkdirSync(outputDir, { recursive: true });
24458
- fs16.writeFileSync(filepath, content, "utf8");
24680
+ const filepath = path18.join(outputDir, filename);
24681
+ fs17.mkdirSync(outputDir, { recursive: true });
24682
+ fs17.writeFileSync(filepath, content, "utf8");
24459
24683
  console.log(`Generated: ${filepath}`);
24460
24684
  } catch (err) {
24461
24685
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -24505,13 +24729,13 @@ async function dispatchNotifications(run, args) {
24505
24729
  }
24506
24730
  function runHistoryPipeline(run, args) {
24507
24731
  if (!args.historyFile) return;
24508
- const historyPath = path17.resolve(args.historyFile);
24732
+ const historyPath = path18.resolve(args.historyFile);
24509
24733
  const store = loadHistory(
24510
24734
  { filePath: historyPath },
24511
24735
  {
24512
24736
  readFile: (p) => {
24513
24737
  try {
24514
- return fs16.readFileSync(p, "utf8");
24738
+ return fs17.readFileSync(p, "utf8");
24515
24739
  } catch {
24516
24740
  return void 0;
24517
24741
  }
@@ -24524,11 +24748,11 @@ function runHistoryPipeline(run, args) {
24524
24748
  run,
24525
24749
  maxRuns: args.maxHistoryRuns
24526
24750
  });
24527
- const dir = path17.dirname(historyPath);
24528
- fs16.mkdirSync(dir, { recursive: true });
24751
+ const dir = path18.dirname(historyPath);
24752
+ fs17.mkdirSync(dir, { recursive: true });
24529
24753
  saveHistory(
24530
24754
  { filePath: historyPath, store: updated },
24531
- { writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
24755
+ { writeFile: (p, content) => fs17.writeFileSync(p, content, "utf8") }
24532
24756
  );
24533
24757
  let metricsCount = 0;
24534
24758
  for (const testId of Object.keys(updated.tests)) {
@@ -24676,11 +24900,11 @@ function writeReviewReport(review, args) {
24676
24900
  const outputDir = args.outputDir ?? "reports";
24677
24901
  const baseName = args.outputName ?? "evidence-review";
24678
24902
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
24679
- fs16.mkdirSync(outputDir, { recursive: true });
24680
- const mdPath = path17.join(outputDir, `${baseName}${suffix}.md`);
24681
- const htmlPath = path17.join(outputDir, `${baseName}${suffix}.html`);
24682
- fs16.writeFileSync(mdPath, markdown, "utf8");
24683
- fs16.writeFileSync(htmlPath, html, "utf8");
24903
+ fs17.mkdirSync(outputDir, { recursive: true });
24904
+ const mdPath = path18.join(outputDir, `${baseName}${suffix}.md`);
24905
+ const htmlPath = path18.join(outputDir, `${baseName}${suffix}.html`);
24906
+ fs17.writeFileSync(mdPath, markdown, "utf8");
24907
+ fs17.writeFileSync(htmlPath, html, "utf8");
24684
24908
  return [mdPath, htmlPath];
24685
24909
  }
24686
24910
  function evaluateReviewGate(review, args) {
@@ -24726,9 +24950,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
24726
24950
  function printCompareResult(result, args, startMs) {
24727
24951
  const durationMs = Date.now() - startMs;
24728
24952
  if (result.prSummary && args.prSummaryFile) {
24729
- const outputPath = path17.resolve(args.prSummaryFile);
24730
- fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
24731
- fs16.writeFileSync(outputPath, result.prSummary, "utf8");
24953
+ const outputPath = path18.resolve(args.prSummaryFile);
24954
+ fs17.mkdirSync(path18.dirname(outputPath), { recursive: true });
24955
+ fs17.writeFileSync(outputPath, result.prSummary, "utf8");
24732
24956
  }
24733
24957
  if (args.jsonSummary) {
24734
24958
  console.log(
@@ -24757,13 +24981,13 @@ function printCompareResult(result, args, startMs) {
24757
24981
  }
24758
24982
  }
24759
24983
  function loadReleasePolicy(policyPath) {
24760
- const resolved = path17.resolve(policyPath);
24761
- if (!fs16.existsSync(resolved)) {
24984
+ const resolved = path18.resolve(policyPath);
24985
+ if (!fs17.existsSync(resolved)) {
24762
24986
  console.error(`Error: release policy file not found: ${resolved}`);
24763
24987
  process.exit(EXIT_USAGE);
24764
24988
  }
24765
24989
  try {
24766
- const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
24990
+ const raw = JSON.parse(fs17.readFileSync(resolved, "utf8"));
24767
24991
  return {
24768
24992
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
24769
24993
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -24867,7 +25091,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24867
25091
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24868
25092
  process.exit(EXIT_USAGE);
24869
25093
  }
24870
- if (!fs16.existsSync(inputFile)) {
25094
+ if (!fs17.existsSync(inputFile)) {
24871
25095
  console.error(`Error: file not found: ${inputFile}`);
24872
25096
  process.exit(EXIT_USAGE);
24873
25097
  }
@@ -24895,7 +25119,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24895
25119
  console.error("Error: --title is required when creating a new page");
24896
25120
  process.exit(EXIT_USAGE);
24897
25121
  }
24898
- const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
25122
+ const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
24899
25123
  if (dryRun) {
24900
25124
  console.log(
24901
25125
  JSON.stringify(
@@ -24974,7 +25198,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24974
25198
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24975
25199
  process.exit(EXIT_USAGE);
24976
25200
  }
24977
- if (!fs16.existsSync(inputFile)) {
25201
+ if (!fs17.existsSync(inputFile)) {
24978
25202
  console.error(`Error: file not found: ${inputFile}`);
24979
25203
  process.exit(EXIT_USAGE);
24980
25204
  }
@@ -25001,7 +25225,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
25001
25225
  process.exit(EXIT_USAGE);
25002
25226
  }
25003
25227
  const mode = modeRaw;
25004
- const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
25228
+ const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
25005
25229
  if (dryRun) {
25006
25230
  console.log(
25007
25231
  JSON.stringify(
@@ -25189,7 +25413,7 @@ async function runBuildDocs(rawArgs) {
25189
25413
  ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
25190
25414
  );
25191
25415
  }
25192
- const rel = path17.relative(process.cwd(), result.siteDir) || ".";
25416
+ const rel = path18.relative(process.cwd(), result.siteDir) || ".";
25193
25417
  console.log(`
25194
25418
  Preview: cd ${rel} && npm run dev`);
25195
25419
  return EXIT_SUCCESS;
@@ -25416,7 +25640,9 @@ function createDefaultCliArgs() {
25416
25640
  failOnAddedFailures: false,
25417
25641
  failOnRemoval: false,
25418
25642
  failOnNew: false,
25419
- baselineMode: "explicit"
25643
+ baselineMode: "explicit",
25644
+ servePort: 4321,
25645
+ serveHost: "127.0.0.1"
25420
25646
  };
25421
25647
  }
25422
25648
  main().catch((err) => {