executable-stories-formatters 0.15.1 → 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
  }
@@ -702,6 +702,41 @@ function generateFeatureId(uri) {
702
702
  function generateScenarioId(featureId, scenarioName) {
703
703
  return `${featureId};${slugify(scenarioName)}`;
704
704
  }
705
+ function normalizeText(text2) {
706
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
707
+ }
708
+ function tokenize(text2) {
709
+ const normalized = normalizeText(text2);
710
+ return normalized.length === 0 ? [] : normalized.split(" ");
711
+ }
712
+ function behaviourFingerprint(input) {
713
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
714
+ const covers = [...input.covers ?? []].map((path19) => path19.trim()).filter(Boolean).sort().join(",");
715
+ if (steps.length === 0 && covers.length === 0) return "";
716
+ return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
717
+ }
718
+ function jaccard(a, b) {
719
+ if (a.size === 0 && b.size === 0) return 1;
720
+ if (a.size === 0 || b.size === 0) return 0;
721
+ let intersection = 0;
722
+ for (const value of a) if (b.has(value)) intersection += 1;
723
+ return intersection / (a.size + b.size - intersection);
724
+ }
725
+ function stepTokens(steps) {
726
+ const tokens = /* @__PURE__ */ new Set();
727
+ for (const step of steps) {
728
+ for (const token of tokenize(step.text)) tokens.add(token);
729
+ }
730
+ return tokens;
731
+ }
732
+ function behaviourSimilarity(a, b) {
733
+ const aSteps = stepTokens(a.steps);
734
+ const bSteps = stepTokens(b.steps);
735
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
736
+ const stepScore = jaccard(aSteps, bSteps);
737
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
738
+ return stepScore * 0.7 + titleScore * 0.3;
739
+ }
705
740
 
706
741
  // src/converters/acl/steps.ts
707
742
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -1073,7 +1108,7 @@ ${result.errors.join("\n")}`);
1073
1108
 
1074
1109
  // src/index.ts
1075
1110
  import "fs";
1076
- import * as path9 from "path";
1111
+ import * as path10 from "path";
1077
1112
  import * as fsPromises from "fs/promises";
1078
1113
 
1079
1114
  // src/converters/acl/lines.ts
@@ -14977,12 +15012,12 @@ function highlightStepParams(text2, deps) {
14977
15012
  var MIN_METRIC_SAMPLES = 5;
14978
15013
 
14979
15014
  // src/formatters/html/renderers/scenario.ts
14980
- function renderTicket(ticket, template, escapeHtml4) {
15015
+ function renderTicket(ticket, template, escapeHtml5) {
14981
15016
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
14982
15017
  if (url) {
14983
- 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>`;
14984
15019
  }
14985
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
15020
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
14986
15021
  }
14987
15022
  function renderScenario(args, deps) {
14988
15023
  const { tc } = args;
@@ -15182,7 +15217,7 @@ function flattenTree(roots) {
15182
15217
  }
15183
15218
  return result;
15184
15219
  }
15185
- function buildTooltip(span, escapeHtml4) {
15220
+ function buildTooltip(span, escapeHtml5) {
15186
15221
  const parts = [];
15187
15222
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
15188
15223
  if (span.statusMessage) {
@@ -15200,7 +15235,7 @@ function buildTooltip(span, escapeHtml4) {
15200
15235
  if (text2.length > TOOLTIP_MAX_LENGTH) {
15201
15236
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
15202
15237
  }
15203
- return escapeHtml4(text2);
15238
+ return escapeHtml5(text2);
15204
15239
  }
15205
15240
  function renderTraceView(args, deps) {
15206
15241
  if (!args.spans || args.spans.length === 0) return "";
@@ -16495,14 +16530,14 @@ var TraceabilityMatrixFormatter = class {
16495
16530
  lines.push("");
16496
16531
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
16497
16532
  if (req.covers.length > 0) {
16498
- lines.push(`Covers: ${req.covers.map((path18) => `\`${path18}\``).join(", ")}`);
16533
+ lines.push(`Covers: ${req.covers.map((path19) => `\`${path19}\``).join(", ")}`);
16499
16534
  }
16500
16535
  lines.push("");
16501
16536
  lines.push("| Status | Scenario | Source | Covers |");
16502
16537
  lines.push("| --- | --- | --- | --- |");
16503
16538
  for (const scenario of req.scenarios) {
16504
16539
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16505
- 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(", ") : "";
16506
16541
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
16507
16542
  }
16508
16543
  lines.push("");
@@ -16604,8 +16639,8 @@ function extractFeatureName(testCases, uri) {
16604
16639
  return tc.titlePath[0];
16605
16640
  }
16606
16641
  }
16607
- const basename5 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16608
- 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());
16609
16644
  }
16610
16645
  function synthesizeFeature(uri, testCases) {
16611
16646
  const featureName = extractFeatureName(testCases, uri);
@@ -17217,8 +17252,8 @@ function extractDocAttachments(step) {
17217
17252
  }
17218
17253
  return attachments;
17219
17254
  }
17220
- function guessMediaType(path18) {
17221
- const lower = path18.toLowerCase();
17255
+ function guessMediaType(path19) {
17256
+ const lower = path19.toLowerCase();
17222
17257
  if (lower.endsWith(".png")) return "image/png";
17223
17258
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
17224
17259
  if (lower.endsWith(".gif")) return "image/gif";
@@ -17359,11 +17394,11 @@ var CucumberHtmlFormatter = class {
17359
17394
  for (const envelope of envelopes) {
17360
17395
  const accepted = htmlStream.write(envelope);
17361
17396
  if (!accepted) {
17362
- await new Promise((resolve12) => htmlStream.once("drain", resolve12));
17397
+ await new Promise((resolve13) => htmlStream.once("drain", resolve13));
17363
17398
  }
17364
17399
  }
17365
- await new Promise((resolve12, reject) => {
17366
- collector.on("finish", resolve12);
17400
+ await new Promise((resolve13, reject) => {
17401
+ collector.on("finish", resolve13);
17367
17402
  collector.on("error", reject);
17368
17403
  htmlStream.end();
17369
17404
  });
@@ -17401,6 +17436,10 @@ function titleFor(kind) {
17401
17436
  return "Added";
17402
17437
  case "removed":
17403
17438
  return "Removed";
17439
+ case "renamed":
17440
+ return "Renamed";
17441
+ case "moved":
17442
+ return "Moved";
17404
17443
  case "changed":
17405
17444
  return "Changed";
17406
17445
  default:
@@ -17449,6 +17488,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
17449
17488
  addSection(lines, diff, "fixed", maxScenarios);
17450
17489
  addSection(lines, diff, "added", maxScenarios);
17451
17490
  addSection(lines, diff, "removed", maxScenarios);
17491
+ addSection(lines, diff, "renamed", maxScenarios);
17492
+ addSection(lines, diff, "moved", maxScenarios);
17452
17493
  addSection(lines, diff, "changed", maxScenarios);
17453
17494
  return lines.join("\n").trimEnd();
17454
17495
  }
@@ -17525,8 +17566,10 @@ function sortDiffs(scenarios) {
17525
17566
  fixed: 1,
17526
17567
  added: 2,
17527
17568
  removed: 3,
17528
- changed: 4,
17529
- unchanged: 5
17569
+ renamed: 4,
17570
+ moved: 5,
17571
+ changed: 6,
17572
+ unchanged: 7
17530
17573
  };
17531
17574
  return [...scenarios].sort((a, b) => {
17532
17575
  if (rank[a.kind] !== rank[b.kind]) {
@@ -17541,69 +17584,108 @@ function sortDiffs(scenarios) {
17541
17584
  return a.scenario.localeCompare(b.scenario);
17542
17585
  });
17543
17586
  }
17587
+ var SIMILARITY_THRESHOLD = 0.75;
17588
+ function identityInput(tc) {
17589
+ return {
17590
+ scenario: tc.story.scenario,
17591
+ sourceFile: tc.sourceFile,
17592
+ steps: tc.story.steps,
17593
+ covers: tc.story.covers
17594
+ };
17595
+ }
17596
+ function changedFieldsOf(flags) {
17597
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17598
+ }
17599
+ function allChangedFlags(errorMessage) {
17600
+ return {
17601
+ status: true,
17602
+ steps: true,
17603
+ docs: true,
17604
+ tags: true,
17605
+ tickets: true,
17606
+ source: true,
17607
+ duration: true,
17608
+ attachments: true,
17609
+ error: Boolean(errorMessage),
17610
+ titlePath: true
17611
+ };
17612
+ }
17613
+ function matchIdentities(removed, added) {
17614
+ const pairs = [];
17615
+ const remainingRemoved = new Set(removed);
17616
+ const remainingAdded = new Set(added);
17617
+ const removedByFp = /* @__PURE__ */ new Map();
17618
+ const addedByFp = /* @__PURE__ */ new Map();
17619
+ for (const tc of remainingRemoved) {
17620
+ const fp = behaviourFingerprint(identityInput(tc));
17621
+ if (fp === "") continue;
17622
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
17623
+ }
17624
+ for (const tc of remainingAdded) {
17625
+ const fp = behaviourFingerprint(identityInput(tc));
17626
+ if (fp === "") continue;
17627
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
17628
+ }
17629
+ for (const [fp, removedGroup] of removedByFp) {
17630
+ const addedGroup = addedByFp.get(fp);
17631
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
17632
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
17633
+ remainingRemoved.delete(removedGroup[0]);
17634
+ remainingAdded.delete(addedGroup[0]);
17635
+ }
17636
+ }
17637
+ for (const before of [...remainingRemoved]) {
17638
+ let best;
17639
+ let bestScore = 0;
17640
+ let tied = false;
17641
+ for (const after of remainingAdded) {
17642
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
17643
+ if (score > bestScore) {
17644
+ bestScore = score;
17645
+ best = after;
17646
+ tied = false;
17647
+ } else if (score === bestScore) {
17648
+ tied = true;
17649
+ }
17650
+ }
17651
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
17652
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
17653
+ remainingRemoved.delete(before);
17654
+ remainingAdded.delete(best);
17655
+ }
17656
+ }
17657
+ return {
17658
+ pairs,
17659
+ unmatchedRemoved: [...remainingRemoved],
17660
+ unmatchedAdded: [...remainingAdded]
17661
+ };
17662
+ }
17663
+ function identityKind(before, after) {
17664
+ const titleChanged = before.story.scenario !== after.story.scenario;
17665
+ const fileChanged = before.sourceFile !== after.sourceFile;
17666
+ return fileChanged && !titleChanged ? "moved" : "renamed";
17667
+ }
17544
17668
  function diffRuns(baseline, current) {
17545
17669
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
17546
17670
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
17547
17671
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
17548
17672
  const scenarios = [];
17673
+ const removedCases = [];
17674
+ const addedCases = [];
17549
17675
  for (const id of ids) {
17550
17676
  const before = baselineById.get(id);
17551
17677
  const after = currentById.get(id);
17552
17678
  if (!before && after) {
17553
- const flags2 = {
17554
- status: true,
17555
- steps: true,
17556
- docs: true,
17557
- tags: true,
17558
- tickets: true,
17559
- source: true,
17560
- duration: true,
17561
- attachments: true,
17562
- error: Boolean(after.errorMessage),
17563
- titlePath: true
17564
- };
17565
- scenarios.push({
17566
- kind: "added",
17567
- id,
17568
- scenario: after.story.scenario,
17569
- sourceFile: after.sourceFile,
17570
- sourceLine: after.sourceLine,
17571
- current: toScenarioSnapshot(after),
17572
- flags: flags2,
17573
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
17574
- });
17679
+ addedCases.push(after);
17575
17680
  continue;
17576
17681
  }
17577
17682
  if (before && !after) {
17578
- const flags2 = {
17579
- status: true,
17580
- steps: true,
17581
- docs: true,
17582
- tags: true,
17583
- tickets: true,
17584
- source: true,
17585
- duration: true,
17586
- attachments: true,
17587
- error: Boolean(before.errorMessage),
17588
- titlePath: true
17589
- };
17590
- scenarios.push({
17591
- kind: "removed",
17592
- id,
17593
- scenario: before.story.scenario,
17594
- sourceFile: before.sourceFile,
17595
- sourceLine: before.sourceLine,
17596
- baseline: toScenarioSnapshot(before),
17597
- flags: flags2,
17598
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
17599
- });
17600
- continue;
17601
- }
17602
- if (!before || !after) {
17683
+ removedCases.push(before);
17603
17684
  continue;
17604
17685
  }
17686
+ if (!before || !after) continue;
17605
17687
  const flags = buildFlags(before, after);
17606
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17688
+ const changedFields = changedFieldsOf(flags);
17607
17689
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
17608
17690
  scenarios.push({
17609
17691
  kind,
@@ -17618,12 +17700,59 @@ function diffRuns(baseline, current) {
17618
17700
  durationDeltaMs: after.durationMs - before.durationMs
17619
17701
  });
17620
17702
  }
17703
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
17704
+ for (const { before, after, confidence, matchedBy } of pairs) {
17705
+ const flags = buildFlags(before, after);
17706
+ scenarios.push({
17707
+ kind: identityKind(before, after),
17708
+ id: after.id,
17709
+ previousId: before.id,
17710
+ scenario: after.story.scenario,
17711
+ sourceFile: after.sourceFile,
17712
+ sourceLine: after.sourceLine,
17713
+ baseline: toScenarioSnapshot(before),
17714
+ current: toScenarioSnapshot(after),
17715
+ flags,
17716
+ changedFields: changedFieldsOf(flags),
17717
+ durationDeltaMs: after.durationMs - before.durationMs,
17718
+ matchConfidence: confidence,
17719
+ matchedBy
17720
+ });
17721
+ }
17722
+ for (const after of unmatchedAdded) {
17723
+ const flags = allChangedFlags(after.errorMessage);
17724
+ scenarios.push({
17725
+ kind: "added",
17726
+ id: after.id,
17727
+ scenario: after.story.scenario,
17728
+ sourceFile: after.sourceFile,
17729
+ sourceLine: after.sourceLine,
17730
+ current: toScenarioSnapshot(after),
17731
+ flags,
17732
+ changedFields: changedFieldsOf(flags)
17733
+ });
17734
+ }
17735
+ for (const before of unmatchedRemoved) {
17736
+ const flags = allChangedFlags(before.errorMessage);
17737
+ scenarios.push({
17738
+ kind: "removed",
17739
+ id: before.id,
17740
+ scenario: before.story.scenario,
17741
+ sourceFile: before.sourceFile,
17742
+ sourceLine: before.sourceLine,
17743
+ baseline: toScenarioSnapshot(before),
17744
+ flags,
17745
+ changedFields: changedFieldsOf(flags)
17746
+ });
17747
+ }
17621
17748
  const sorted = sortDiffs(scenarios);
17622
17749
  const summary = {
17623
17750
  totalBaseline: baseline.testCases.length,
17624
17751
  totalCurrent: current.testCases.length,
17625
17752
  added: sorted.filter((s) => s.kind === "added").length,
17626
17753
  removed: sorted.filter((s) => s.kind === "removed").length,
17754
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
17755
+ moved: sorted.filter((s) => s.kind === "moved").length,
17627
17756
  changed: sorted.filter((s) => s.kind === "changed").length,
17628
17757
  regressed: sorted.filter((s) => s.kind === "regressed").length,
17629
17758
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -17651,6 +17780,10 @@ function statusLabel(kind) {
17651
17780
  return "Added";
17652
17781
  case "removed":
17653
17782
  return "Removed";
17783
+ case "renamed":
17784
+ return "Renamed";
17785
+ case "moved":
17786
+ return "Moved";
17654
17787
  case "changed":
17655
17788
  return "Changed";
17656
17789
  default:
@@ -17976,6 +18109,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17976
18109
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17977
18110
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17978
18111
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
18112
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
18113
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17979
18114
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17980
18115
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17981
18116
  </section>
@@ -18036,6 +18171,10 @@ function formatStatus(kind) {
18036
18171
  return "Added";
18037
18172
  case "removed":
18038
18173
  return "Removed";
18174
+ case "renamed":
18175
+ return "Renamed";
18176
+ case "moved":
18177
+ return "Moved";
18039
18178
  case "changed":
18040
18179
  return "Changed";
18041
18180
  default:
@@ -18188,13 +18327,13 @@ var RunDiffMarkdownFormatter = class {
18188
18327
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
18189
18328
  }
18190
18329
  lines.push("");
18191
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
18192
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
18330
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
18331
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
18193
18332
  lines.push(
18194
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
18333
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
18195
18334
  );
18196
18335
  lines.push("");
18197
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
18336
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
18198
18337
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
18199
18338
  if (scenarios.length === 0) continue;
18200
18339
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -19053,7 +19192,7 @@ function toRun(data, inputType, synthesize) {
19053
19192
  if (synthesize) raw = synthesizeStories(raw);
19054
19193
  return canonicalizeRun(raw);
19055
19194
  }
19056
- async function regenerateArtifacts(options, deps = {}) {
19195
+ async function regenerateRun(options, deps = {}) {
19057
19196
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
19058
19197
  const data = JSON.parse(read(path7.resolve(options.input)));
19059
19198
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -19063,7 +19202,10 @@ async function regenerateArtifacts(options, deps = {}) {
19063
19202
  outputName: options.outputName
19064
19203
  });
19065
19204
  const result = await generator.generate(run);
19066
- 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;
19067
19209
  }
19068
19210
  function startWatch(options, deps = {}) {
19069
19211
  const log = deps.log ?? ((message) => console.log(message));
@@ -19106,6 +19248,203 @@ function startWatch(options, deps = {}) {
19106
19248
  };
19107
19249
  }
19108
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
+
19109
19448
  // src/behavior-diff.ts
19110
19449
  function classifyStatusChange(baseline, current) {
19111
19450
  if (baseline === void 0) return "added";
@@ -19143,7 +19482,10 @@ function diffStoryReports(baseline, current) {
19143
19482
  };
19144
19483
  });
19145
19484
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19146
- for (const s of scenarios) summary[s.kind] += 1;
19485
+ for (const s of scenarios) {
19486
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19487
+ summary[s.kind] += 1;
19488
+ }
19147
19489
  return { schemaVersion: "1.0", summary, scenarios };
19148
19490
  }
19149
19491
 
@@ -20752,18 +21094,18 @@ function deriveChangeType(tags) {
20752
21094
  }
20753
21095
  return "unknown";
20754
21096
  }
20755
- function extensionOf(path18) {
20756
- const base = path18.split("/").pop() ?? path18;
21097
+ function extensionOf(path19) {
21098
+ const base = path19.split("/").pop() ?? path19;
20757
21099
  const dot = base.lastIndexOf(".");
20758
21100
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20759
21101
  }
20760
- function isTestFile(path18) {
20761
- return TEST_INFIX.test(path18);
21102
+ function isTestFile(path19) {
21103
+ return TEST_INFIX.test(path19);
20762
21104
  }
20763
- function isReviewableSource(path18) {
20764
- if (isTestFile(path18)) return false;
20765
- if (path18.endsWith(".d.ts")) return false;
20766
- 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));
20767
21109
  }
20768
21110
  function testBaseKey(testFile) {
20769
21111
  return testFile.replace(TEST_INFIX, "");
@@ -20867,7 +21209,7 @@ function toClaim(testCase, changedSourcePaths) {
20867
21209
  const { strength, reasons } = gradeEvidence(testCase, audience);
20868
21210
  const key = testBaseKey(testCase.sourceFile);
20869
21211
  const coversFiles = changedSourcePaths.filter(
20870
- (path18) => sourceBaseKey(path18) === key
21212
+ (path19) => sourceBaseKey(path19) === key
20871
21213
  );
20872
21214
  return {
20873
21215
  id: testCase.id,
@@ -21116,7 +21458,7 @@ var ReviewMarkdownFormatter = class {
21116
21458
  };
21117
21459
 
21118
21460
  // src/formatters/review-html.ts
21119
- function escapeHtml3(value) {
21461
+ function escapeHtml4(value) {
21120
21462
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21121
21463
  }
21122
21464
  var STRENGTH_LABEL = {
@@ -21138,22 +21480,22 @@ function statusIcon3(status) {
21138
21480
  }
21139
21481
  }
21140
21482
  function formatStep3(step) {
21141
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21483
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21142
21484
  }
21143
21485
  function inlineDoc(doc) {
21144
21486
  switch (doc.kind) {
21145
21487
  case "note":
21146
- return escapeHtml3(doc.text);
21488
+ return escapeHtml4(doc.text);
21147
21489
  case "section":
21148
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21490
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21149
21491
  case "kv":
21150
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21492
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21151
21493
  case "code":
21152
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21494
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21153
21495
  case "link":
21154
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21496
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21155
21497
  default:
21156
- return escapeHtml3(doc.kind);
21498
+ return escapeHtml4(doc.kind);
21157
21499
  }
21158
21500
  }
21159
21501
  function renderEvidenceArtifacts(testCase) {
@@ -21161,7 +21503,7 @@ function renderEvidenceArtifacts(testCase) {
21161
21503
  for (const att of testCase.attachments) {
21162
21504
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21163
21505
  parts.push(
21164
- `<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}" />`
21165
21507
  );
21166
21508
  }
21167
21509
  }
@@ -21176,22 +21518,22 @@ function renderTicketPills(claim) {
21176
21518
  const tickets = claim.testCase.story.tickets ?? [];
21177
21519
  if (tickets.length === 0) return "";
21178
21520
  return `<div class="ticket-row">${tickets.map((ticket) => {
21179
- const label = escapeHtml3(ticket.id);
21521
+ const label = escapeHtml4(ticket.id);
21180
21522
  if (ticket.url) {
21181
- 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>`;
21182
21524
  }
21183
21525
  return `<span class="ticket-pill">${label}</span>`;
21184
21526
  }).join("")}</div>`;
21185
21527
  }
21186
21528
  function renderClaimCard(claim) {
21187
21529
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21188
- const search = escapeHtml3(
21530
+ const search = escapeHtml4(
21189
21531
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21190
21532
  ).toLowerCase();
21191
21533
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21192
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21193
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21194
- 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>` : "";
21195
21537
  const docs = (claim.testCase.story.docs ?? []).filter(
21196
21538
  (d) => d.kind === "section" || d.kind === "note"
21197
21539
  );
@@ -21201,9 +21543,9 @@ function renderClaimCard(claim) {
21201
21543
  <header class="claim-header">
21202
21544
  <div>
21203
21545
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21204
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21205
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21206
- <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>
21207
21549
  ${renderTicketPills(claim)}
21208
21550
  </div>
21209
21551
  </header>
@@ -21218,18 +21560,18 @@ function renderClaimCard(claim) {
21218
21560
  </article>`;
21219
21561
  }
21220
21562
  function renderChangedFileRow(file) {
21221
- 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";
21222
21564
  return `<tr data-band="${file.band}">
21223
21565
  <td><span class="band-dot band-${file.band}"></span></td>
21224
- <td><code>${escapeHtml3(file.path)}</code></td>
21225
- <td>${escapeHtml3(file.changeKind)}</td>
21566
+ <td><code>${escapeHtml4(file.path)}</code></td>
21567
+ <td>${escapeHtml4(file.changeKind)}</td>
21226
21568
  <td>${claims}</td>
21227
21569
  </tr>`;
21228
21570
  }
21229
21571
  function renderAudienceSection2(title, claims) {
21230
21572
  if (claims.length === 0) return "";
21231
21573
  return `<section class="audience-section">
21232
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21574
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21233
21575
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21234
21576
  </section>`;
21235
21577
  }
@@ -21319,13 +21661,13 @@ var ReviewHtmlFormatter = class {
21319
21661
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21320
21662
  applyTheme(getEffectiveTheme());` : "";
21321
21663
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21322
- 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>` : "";
21323
21665
  return `<!doctype html>
21324
21666
  <html lang="en"${themeAttr}>
21325
21667
  <head>
21326
21668
  <meta charset="utf-8" />
21327
21669
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21328
- <title>${escapeHtml3(this.title)}</title>
21670
+ <title>${escapeHtml4(this.title)}</title>
21329
21671
  <style>
21330
21672
  ${this.theme.css}
21331
21673
  ${REVIEW_CSS}
@@ -21335,7 +21677,7 @@ applyTheme(getEffectiveTheme());` : "";
21335
21677
  <main>
21336
21678
  <div class="hero-card card">
21337
21679
  <div class="review-header">
21338
- <h1>${escapeHtml3(this.title)}</h1>
21680
+ <h1>${escapeHtml4(this.title)}</h1>
21339
21681
  ${themeToggleHtml}
21340
21682
  </div>
21341
21683
  ${refsLine}
@@ -21350,7 +21692,7 @@ applyTheme(getEffectiveTheme());` : "";
21350
21692
  </section>
21351
21693
  <section class="card priority-banner">
21352
21694
  <h2>Review priority</h2>
21353
- <p class="subtle">${escapeHtml3(priority)}</p>
21695
+ <p class="subtle">${escapeHtml4(priority)}</p>
21354
21696
  </section>
21355
21697
  ${changedFilesPanel}
21356
21698
  <section class="toolbar">
@@ -21398,8 +21740,8 @@ applyTheme(getEffectiveTheme());` : "";
21398
21740
  };
21399
21741
 
21400
21742
  // src/deploy/ledger.ts
21401
- import * as fs7 from "fs";
21402
- import * as path8 from "path";
21743
+ import * as fs8 from "fs";
21744
+ import * as path9 from "path";
21403
21745
  function createEmptyLedger() {
21404
21746
  return {
21405
21747
  deployments: [],
@@ -21407,12 +21749,12 @@ function createEmptyLedger() {
21407
21749
  };
21408
21750
  }
21409
21751
  function loadLedger(ledgerPath) {
21410
- const resolved = path8.resolve(ledgerPath);
21411
- if (!fs7.existsSync(resolved)) {
21752
+ const resolved = path9.resolve(ledgerPath);
21753
+ if (!fs8.existsSync(resolved)) {
21412
21754
  return createEmptyLedger();
21413
21755
  }
21414
21756
  try {
21415
- const raw = JSON.parse(fs7.readFileSync(resolved, "utf8"));
21757
+ const raw = JSON.parse(fs8.readFileSync(resolved, "utf8"));
21416
21758
  if (raw.schemaVersion !== 1) {
21417
21759
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21418
21760
  }
@@ -21423,10 +21765,10 @@ function loadLedger(ledgerPath) {
21423
21765
  }
21424
21766
  }
21425
21767
  function saveLedger(ledger, ledgerPath) {
21426
- const resolved = path8.resolve(ledgerPath);
21427
- const dir = path8.dirname(resolved);
21428
- fs7.mkdirSync(dir, { recursive: true });
21429
- 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");
21430
21772
  }
21431
21773
  function getLatestDeployment(ledger, environment) {
21432
21774
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21546,11 +21888,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21546
21888
  const ext = FORMAT_EXTENSIONS[format];
21547
21889
  const effectiveName = outputName + (outputNameSuffix ?? "");
21548
21890
  if (mode === "aggregated") {
21549
- return toPosix(path9.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21891
+ return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21550
21892
  }
21551
21893
  const normalizedSource = toPosix(sourceFile);
21552
- const dirOfSource = path9.posix.dirname(normalizedSource);
21553
- let baseName = path9.posix.basename(normalizedSource);
21894
+ const dirOfSource = path10.posix.dirname(normalizedSource);
21895
+ let baseName = path10.posix.basename(normalizedSource);
21554
21896
  for (const testExt of TEST_EXTENSIONS) {
21555
21897
  if (baseName.endsWith(testExt)) {
21556
21898
  baseName = baseName.slice(0, -testExt.length);
@@ -21559,12 +21901,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21559
21901
  }
21560
21902
  const fileName = `${baseName}.${effectiveName}${ext}`;
21561
21903
  if (colocatedStyle === "adjacent") {
21562
- return toPosix(path9.posix.join(dirOfSource, fileName));
21904
+ return toPosix(path10.posix.join(dirOfSource, fileName));
21563
21905
  }
21564
21906
  if (colocatedStyle === "flat") {
21565
- return toPosix(path9.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21907
+ return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21566
21908
  }
21567
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
21909
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
21568
21910
  }
21569
21911
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
21570
21912
  const groups = /* @__PURE__ */ new Map();
@@ -21777,8 +22119,8 @@ var ReportGenerator = class {
21777
22119
  if (astroPaths) {
21778
22120
  for (const mdPath of astroPaths) {
21779
22121
  const content = await fsPromises.readFile(mdPath, "utf8");
21780
- const mdDir = path9.dirname(mdPath);
21781
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
22122
+ const mdDir = path10.dirname(mdPath);
22123
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
21782
22124
  const result = copyMarkdownAssets({
21783
22125
  markdown: content,
21784
22126
  markdownDir: mdDir,
@@ -21809,9 +22151,9 @@ var ReportGenerator = class {
21809
22151
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
21810
22152
  const ext = FORMAT_EXTENSIONS[format];
21811
22153
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
21812
- const outputPath = toPosix(path9.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22154
+ const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21813
22155
  const content = await this.formatContent(run, format);
21814
- const dir = path9.dirname(outputPath);
22156
+ const dir = path10.dirname(outputPath);
21815
22157
  await fsPromises.mkdir(dir, { recursive: true });
21816
22158
  await this.deps.writeFile(outputPath, content);
21817
22159
  return [outputPath];
@@ -21823,7 +22165,7 @@ var ReportGenerator = class {
21823
22165
  testCases
21824
22166
  };
21825
22167
  const content = await this.formatContent(groupRun, format);
21826
- const dir = path9.dirname(outputPath);
22168
+ const dir = path10.dirname(outputPath);
21827
22169
  await fsPromises.mkdir(dir, { recursive: true });
21828
22170
  await this.deps.writeFile(outputPath, content);
21829
22171
  writtenPaths.push(outputPath);
@@ -21972,7 +22314,7 @@ async function generateRunComparison(args) {
21972
22314
  await fsPromises.mkdir(outputDir, { recursive: true });
21973
22315
  for (const format of args.formats) {
21974
22316
  const ext = format === "html" ? ".html" : ".md";
21975
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
22317
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
21976
22318
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
21977
22319
  await fsPromises.writeFile(outputPath, content, "utf8");
21978
22320
  files.push(outputPath);
@@ -21981,21 +22323,21 @@ async function generateRunComparison(args) {
21981
22323
  }
21982
22324
 
21983
22325
  // src/init-astro.ts
21984
- import * as fs9 from "fs";
21985
- import * as path10 from "path";
22326
+ import * as fs10 from "fs";
22327
+ import * as path11 from "path";
21986
22328
  import { fileURLToPath } from "url";
21987
- var __dirname = path10.dirname(fileURLToPath(import.meta.url));
22329
+ var __dirname = path11.dirname(fileURLToPath(import.meta.url));
21988
22330
  var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
21989
22331
  var FRAMEWORK_FILES = ["tsconfig.json"];
21990
22332
  function isScaffoldedAstroSite(dir) {
21991
- return fs9.existsSync(path10.join(dir, "astro.config.mjs"));
22333
+ return fs10.existsSync(path11.join(dir, "astro.config.mjs"));
21992
22334
  }
21993
22335
  function initAstro(options = {}) {
21994
22336
  const targetDir = options.targetDir ?? "./story-docs";
21995
22337
  const force = options.force ?? false;
21996
22338
  const update = options.update ?? false;
21997
- const templateDir = path10.resolve(__dirname, "..", "templates", "astro-starlight");
21998
- if (!fs9.existsSync(templateDir)) {
22339
+ const templateDir = path11.resolve(__dirname, "..", "templates", "astro-starlight");
22340
+ if (!fs10.existsSync(templateDir)) {
21999
22341
  throw new Error(
22000
22342
  `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
22001
22343
  );
@@ -22003,8 +22345,8 @@ function initAstro(options = {}) {
22003
22345
  if (update) {
22004
22346
  return updateFrameworkFiles(templateDir, targetDir);
22005
22347
  }
22006
- if (fs9.existsSync(targetDir)) {
22007
- const entries = fs9.readdirSync(targetDir);
22348
+ if (fs10.existsSync(targetDir)) {
22349
+ const entries = fs10.readdirSync(targetDir);
22008
22350
  if (entries.length > 0 && !force) {
22009
22351
  throw new Error(
22010
22352
  `Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
@@ -22022,25 +22364,25 @@ function updateFrameworkFiles(templateDir, targetDir) {
22022
22364
  }
22023
22365
  const updated = [];
22024
22366
  for (const dir of FRAMEWORK_DIRS) {
22025
- const src = path10.join(templateDir, dir);
22026
- if (!fs9.existsSync(src)) continue;
22027
- 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)));
22028
22370
  }
22029
22371
  for (const file of FRAMEWORK_FILES) {
22030
- const src = path10.join(templateDir, file);
22031
- if (!fs9.existsSync(src)) continue;
22032
- 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));
22033
22375
  updated.push(file);
22034
22376
  }
22035
22377
  if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
22036
22378
  return { targetDir, updatedFiles: updated };
22037
22379
  }
22038
22380
  function mergeDependencies(templateDir, targetDir) {
22039
- const tmplPkgPath = path10.join(templateDir, "package.json");
22040
- const userPkgPath = path10.join(targetDir, "package.json");
22041
- if (!fs9.existsSync(tmplPkgPath) || !fs9.existsSync(userPkgPath)) return false;
22042
- const tmpl = JSON.parse(fs9.readFileSync(tmplPkgPath, "utf8"));
22043
- 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"));
22044
22386
  user.dependencies = user.dependencies ?? {};
22045
22387
  let changed = false;
22046
22388
  for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
@@ -22050,30 +22392,30 @@ function mergeDependencies(templateDir, targetDir) {
22050
22392
  }
22051
22393
  }
22052
22394
  if (changed) {
22053
- fs9.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
22395
+ fs10.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
22054
22396
  `, "utf8");
22055
22397
  }
22056
22398
  return changed;
22057
22399
  }
22058
22400
  function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22059
- fs9.mkdirSync(dest, { recursive: true });
22060
- const entries = fs9.readdirSync(src, { withFileTypes: true });
22401
+ fs10.mkdirSync(dest, { recursive: true });
22402
+ const entries = fs10.readdirSync(src, { withFileTypes: true });
22061
22403
  for (const entry of entries) {
22062
- const srcPath = path10.join(src, entry.name);
22404
+ const srcPath = path11.join(src, entry.name);
22063
22405
  const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
22064
- const destPath = path10.join(dest, destName);
22406
+ const destPath = path11.join(dest, destName);
22065
22407
  if (entry.isDirectory()) {
22066
22408
  copyDirRecursive(srcPath, destPath, onFile, baseSrc);
22067
22409
  } else {
22068
- fs9.copyFileSync(srcPath, destPath);
22069
- onFile?.(path10.relative(baseSrc, srcPath));
22410
+ fs10.copyFileSync(srcPath, destPath);
22411
+ onFile?.(path11.relative(baseSrc, srcPath));
22070
22412
  }
22071
22413
  }
22072
22414
  }
22073
22415
 
22074
22416
  // src/scaffold-doc.ts
22075
- import * as fs10 from "fs";
22076
- import * as path11 from "path";
22417
+ import * as fs11 from "fs";
22418
+ import * as path12 from "path";
22077
22419
  var TEMPLATES = [
22078
22420
  "adr",
22079
22421
  "runbook",
@@ -22090,7 +22432,7 @@ function isoDate(today) {
22090
22432
  function nextSeq(dir) {
22091
22433
  let max = 0;
22092
22434
  try {
22093
- for (const entry of fs10.readdirSync(dir)) {
22435
+ for (const entry of fs11.readdirSync(dir)) {
22094
22436
  const match = /^(\d{1,4})-/.exec(entry);
22095
22437
  if (match) max = Math.max(max, Number.parseInt(match[1], 10));
22096
22438
  }
@@ -22247,12 +22589,12 @@ function scaffoldDoc(options) {
22247
22589
  );
22248
22590
  }
22249
22591
  const spec = TEMPLATE_SPECS[template];
22250
- const baseDir = options.baseDir ?? path11.join("src", "content", "docs");
22592
+ const baseDir = options.baseDir ?? path12.join("src", "content", "docs");
22251
22593
  const today = options.today ?? /* @__PURE__ */ new Date();
22252
22594
  const name = (options.name ?? "").trim() || defaultName(template);
22253
22595
  const slug2 = slugify3(name);
22254
22596
  const scenarioId = normalizeScenarioId(options.scenarioId);
22255
- const dir = path11.join(baseDir, spec.subdir);
22597
+ const dir = path12.join(baseDir, spec.subdir);
22256
22598
  if (template === "scenario-note" && !scenarioId) {
22257
22599
  throw new Error(`Template "scenario-note" requires --scenario-id.`);
22258
22600
  }
@@ -22264,14 +22606,14 @@ function scaffoldDoc(options) {
22264
22606
  seq: nextSeq(dir)
22265
22607
  };
22266
22608
  const filename = `${spec.filename(slug2, ctx)}.mdx`;
22267
- const filePath = path11.join(dir, filename);
22268
- if (fs10.existsSync(filePath) && !options.force) {
22609
+ const filePath = path12.join(dir, filename);
22610
+ if (fs11.existsSync(filePath) && !options.force) {
22269
22611
  throw new Error(
22270
22612
  `File "${filePath}" already exists. Use --force to overwrite.`
22271
22613
  );
22272
22614
  }
22273
- fs10.mkdirSync(dir, { recursive: true });
22274
- fs10.writeFileSync(filePath, spec.content(ctx), "utf8");
22615
+ fs11.mkdirSync(dir, { recursive: true });
22616
+ fs11.writeFileSync(filePath, spec.content(ctx), "utf8");
22275
22617
  return { template, path: filePath, title: titleFor2(template, ctx) };
22276
22618
  }
22277
22619
  function defaultName(template) {
@@ -22312,20 +22654,20 @@ function normalizeScenarioId(input) {
22312
22654
  }
22313
22655
 
22314
22656
  // src/check-links.ts
22315
- import * as fs12 from "fs";
22316
- import * as path13 from "path";
22657
+ import * as fs13 from "fs";
22658
+ import * as path14 from "path";
22317
22659
 
22318
22660
  // src/utils/markdown-files.ts
22319
- import * as fs11 from "fs";
22320
- import * as path12 from "path";
22661
+ import * as fs12 from "fs";
22662
+ import * as path13 from "path";
22321
22663
  function collectMarkdownFiles(target) {
22322
- if (!fs11.existsSync(target)) return [];
22323
- if (fs11.statSync(target).isFile()) return [target];
22664
+ if (!fs12.existsSync(target)) return [];
22665
+ if (fs12.statSync(target).isFile()) return [target];
22324
22666
  const out = [];
22325
22667
  const walk = (dir) => {
22326
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22668
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
22327
22669
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22328
- const full = path12.join(dir, entry.name);
22670
+ const full = path13.join(dir, entry.name);
22329
22671
  if (entry.isDirectory()) walk(full);
22330
22672
  else if (/\.mdx?$/u.test(entry.name)) out.push(full);
22331
22673
  }
@@ -22364,17 +22706,17 @@ function classifyLink(link2) {
22364
22706
  function resolutionCandidates(fromFile, link2) {
22365
22707
  const withoutAnchor = link2.split("#")[0];
22366
22708
  if (!withoutAnchor) return [];
22367
- const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
22709
+ const base = path14.resolve(path14.dirname(fromFile), withoutAnchor);
22368
22710
  const candidates = [base];
22369
- if (!path13.extname(base)) {
22711
+ if (!path14.extname(base)) {
22370
22712
  candidates.push(`${base}.md`, `${base}.mdx`);
22371
- 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"));
22372
22714
  }
22373
22715
  return candidates;
22374
22716
  }
22375
22717
  function resolvesOnDisk(fromFile, link2) {
22376
22718
  return resolutionCandidates(fromFile, link2).some(
22377
- (candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
22719
+ (candidate) => fs13.existsSync(candidate) && fs13.statSync(candidate).isFile()
22378
22720
  );
22379
22721
  }
22380
22722
  async function isExternalAlive(url, timeoutMs) {
@@ -22400,7 +22742,7 @@ async function isExternalAlive(url, timeoutMs) {
22400
22742
  }
22401
22743
  async function checkLinks(options) {
22402
22744
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
22403
- if (!fs12.existsSync(target)) {
22745
+ if (!fs13.existsSync(target)) {
22404
22746
  throw new Error(`Path not found: ${target}`);
22405
22747
  }
22406
22748
  const files = collectMarkdownFiles(target);
@@ -22410,7 +22752,7 @@ async function checkLinks(options) {
22410
22752
  let skipped = 0;
22411
22753
  const externalCache = /* @__PURE__ */ new Map();
22412
22754
  for (const file of files) {
22413
- const content = fs12.readFileSync(file, "utf8");
22755
+ const content = fs13.readFileSync(file, "utf8");
22414
22756
  for (const link2 of extractLinks(content)) {
22415
22757
  const kind = classifyLink(link2);
22416
22758
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -22466,8 +22808,8 @@ function formatLinkReport(report) {
22466
22808
  }
22467
22809
 
22468
22810
  // src/import-openapi.ts
22469
- import * as fs13 from "fs";
22470
- import * as path14 from "path";
22811
+ import * as fs14 from "fs";
22812
+ import * as path15 from "path";
22471
22813
  import { parse as parseYamlString } from "yaml";
22472
22814
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
22473
22815
  function parseYaml(raw, specPath) {
@@ -22480,9 +22822,9 @@ function parseYaml(raw, specPath) {
22480
22822
  }
22481
22823
  }
22482
22824
  function parseSpec(specPath) {
22483
- if (!fs13.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22484
- const raw = fs13.readFileSync(specPath, "utf8");
22485
- 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();
22486
22828
  if (ext === ".json") return JSON.parse(raw);
22487
22829
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
22488
22830
  try {
@@ -22513,8 +22855,8 @@ function extractEndpoints(spec) {
22513
22855
  }
22514
22856
  function loadScenarios(runFile) {
22515
22857
  if (!runFile) return [];
22516
- if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22517
- 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"));
22518
22860
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
22519
22861
  }
22520
22862
  function endpointRefs(endpoint) {
@@ -22621,25 +22963,25 @@ async function importOpenApi(options) {
22621
22963
  list.push(item);
22622
22964
  groups.set(item.endpoint.tag, list);
22623
22965
  }
22624
- const outputDir = options.outputDir ?? path14.join("src", "content", "docs", "api");
22625
- if (fs13.existsSync(outputDir) && !options.force) {
22626
- 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);
22627
22969
  if (entries.length > 0) {
22628
22970
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
22629
22971
  }
22630
22972
  }
22631
- fs13.mkdirSync(outputDir, { recursive: true });
22973
+ fs14.mkdirSync(outputDir, { recursive: true });
22632
22974
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
22633
22975
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
22634
- fs13.writeFileSync(
22635
- path14.join(outputDir, "index.mdx"),
22976
+ fs14.writeFileSync(
22977
+ path15.join(outputDir, "index.mdx"),
22636
22978
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
22637
22979
  "utf8"
22638
22980
  );
22639
22981
  for (const [tag, rows] of groups) {
22640
- const dir = path14.join(outputDir, slug(tag));
22641
- fs13.mkdirSync(dir, { recursive: true });
22642
- 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");
22643
22985
  }
22644
22986
  return {
22645
22987
  outputDir,
@@ -22651,8 +22993,8 @@ async function importOpenApi(options) {
22651
22993
  }
22652
22994
 
22653
22995
  // src/build-docs.ts
22654
- import * as fs15 from "fs";
22655
- import * as path16 from "path";
22996
+ import * as fs16 from "fs";
22997
+ import * as path17 from "path";
22656
22998
 
22657
22999
  // src/scenario-links.ts
22658
23000
  function scenarioAnchor(title) {
@@ -22749,8 +23091,8 @@ ${body.join("\n")}
22749
23091
  }
22750
23092
 
22751
23093
  // src/notes-index.ts
22752
- import * as fs14 from "fs";
22753
- import * as path15 from "path";
23094
+ import * as fs15 from "fs";
23095
+ import * as path16 from "path";
22754
23096
  import { slug as githubSlug } from "github-slugger";
22755
23097
  import { parse as parseYaml2 } from "yaml";
22756
23098
  function buildScenarioNotesIndex(notesDir) {
@@ -22761,8 +23103,8 @@ function buildScenarioNotesIndex(notesDir) {
22761
23103
  };
22762
23104
  }
22763
23105
  function writeNotesIndex(index, outPath) {
22764
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22765
- 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");
22766
23108
  return index;
22767
23109
  }
22768
23110
  function notesByScenarioId(index) {
@@ -22779,10 +23121,10 @@ function noteLinkMarkdown(note) {
22779
23121
  return `[Business context \u2192](${noteHref(note)})`;
22780
23122
  }
22781
23123
  function readScenarioNote(filePath, notesDir) {
22782
- const relative5 = path15.relative(notesDir, filePath);
23124
+ const relative5 = path16.relative(notesDir, filePath);
22783
23125
  const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
22784
- const frontmatter = parseFrontmatter(fs14.readFileSync(filePath, "utf8"));
22785
- 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);
22786
23128
  const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
22787
23129
  return {
22788
23130
  scenarioId,
@@ -22797,7 +23139,7 @@ function parseFrontmatter(source) {
22797
23139
  return parsed && typeof parsed === "object" ? parsed : {};
22798
23140
  }
22799
23141
  function toRouteSlug(stem) {
22800
- 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, "");
22801
23143
  }
22802
23144
 
22803
23145
  // src/overview-page.ts
@@ -22900,22 +23242,22 @@ var BuildDocsError = class extends Error {
22900
23242
  };
22901
23243
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
22902
23244
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
22903
- if (!fs15.existsSync(reportPath)) return 0;
22904
- const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
23245
+ if (!fs16.existsSync(reportPath)) return 0;
23246
+ const report = JSON.parse(fs16.readFileSync(reportPath, "utf8"));
22905
23247
  let copied = 0;
22906
23248
  const bundle = (value) => {
22907
- const rel = copyAsset(path16.resolve(value), assetsDir);
23249
+ const rel = copyAsset(path17.resolve(value), assetsDir);
22908
23250
  copied++;
22909
- return `${baseUrl}/${path16.basename(rel)}`;
23251
+ return `${baseUrl}/${path17.basename(rel)}`;
22910
23252
  };
22911
23253
  const visit = (entries) => {
22912
23254
  for (const entry of entries ?? []) {
22913
23255
  const e = entry;
22914
23256
  if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
22915
- 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)) {
22916
23258
  e.path = bundle(e.path);
22917
23259
  }
22918
- 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)) {
22919
23261
  e.poster = bundle(e.poster);
22920
23262
  }
22921
23263
  }
@@ -22928,7 +23270,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22928
23270
  }
22929
23271
  }
22930
23272
  if (copied > 0) {
22931
- fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
23273
+ fs16.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22932
23274
  }
22933
23275
  return copied;
22934
23276
  }
@@ -22951,9 +23293,9 @@ function changeBadgeLookup(diff) {
22951
23293
  return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22952
23294
  }
22953
23295
  function readStoryReport(reportPath) {
22954
- if (!fs15.existsSync(reportPath)) return null;
23296
+ if (!fs16.existsSync(reportPath)) return null;
22955
23297
  try {
22956
- return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
23298
+ return JSON.parse(fs16.readFileSync(reportPath, "utf8"));
22957
23299
  } catch {
22958
23300
  return null;
22959
23301
  }
@@ -22978,28 +23320,28 @@ function writeScenarioLinks(reportPath, outDir, options = {}) {
22978
23320
  const report = readStoryReport(reportPath);
22979
23321
  if (!report) return null;
22980
23322
  const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22981
- fs15.writeFileSync(
22982
- path16.join(outDir, "scenario-links.json"),
23323
+ fs16.writeFileSync(
23324
+ path17.join(outDir, "scenario-links.json"),
22983
23325
  JSON.stringify(index, null, 2),
22984
23326
  "utf8"
22985
23327
  );
22986
23328
  return index;
22987
23329
  }
22988
23330
  function clearGeneratedPages(dir) {
22989
- if (!fs15.existsSync(dir)) return;
22990
- for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
22991
- 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);
22992
23334
  if (entry.isDirectory()) {
22993
23335
  clearGeneratedPages(full);
22994
- if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
23336
+ if (fs16.readdirSync(full).length === 0) fs16.rmdirSync(full);
22995
23337
  } else if (/\.mdx?$/.test(entry.name)) {
22996
- fs15.rmSync(full);
23338
+ fs16.rmSync(full);
22997
23339
  }
22998
23340
  }
22999
23341
  }
23000
23342
  function loadCanonicalRun(rawRunPath, synthesize) {
23001
23343
  try {
23002
- const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
23344
+ const data = JSON.parse(fs16.readFileSync(path17.resolve(rawRunPath), "utf8"));
23003
23345
  if (data.schemaVersion !== 1) {
23004
23346
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
23005
23347
  }
@@ -23022,19 +23364,19 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
23022
23364
  }
23023
23365
  }
23024
23366
  async function buildDocs(options) {
23025
- const siteDir = path16.resolve(options.siteDir);
23367
+ const siteDir = path17.resolve(options.siteDir);
23026
23368
  if (!isScaffoldedAstroSite(siteDir)) {
23027
23369
  throw new BuildDocsError(
23028
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>.`,
23029
23371
  "usage"
23030
23372
  );
23031
23373
  }
23032
- const storiesPublicDir = path16.join(siteDir, "public", "stories");
23033
- const assetsDir = path16.join(storiesPublicDir, "assets");
23034
- const storyPagesDir = path16.join(siteDir, "src", "content", "docs", "stories");
23035
- const notesDir = path16.join(siteDir, "src", "content", "docs", "notes");
23036
- const apiDir = path16.join(siteDir, "src", "content", "docs", "api");
23037
- 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");
23038
23380
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
23039
23381
  try {
23040
23382
  await new ReportGenerator({
@@ -23045,7 +23387,7 @@ async function buildDocs(options) {
23045
23387
  const currentReport = readStoryReport(reportPath);
23046
23388
  let diff;
23047
23389
  if (options.baselinePath) {
23048
- const baselineResolved = path16.resolve(options.baselinePath);
23390
+ const baselineResolved = path17.resolve(options.baselinePath);
23049
23391
  const baseline = readStoryReport(baselineResolved);
23050
23392
  if (!baseline) {
23051
23393
  throw new BuildDocsError(
@@ -23083,7 +23425,7 @@ async function buildDocs(options) {
23083
23425
  const sub = partitioned[audience];
23084
23426
  audiences[audience] = sub.testCases.length;
23085
23427
  if (sub.testCases.length === 0) continue;
23086
- await genPages(sub, path16.join(storyPagesDir, audience));
23428
+ await genPages(sub, path17.join(storyPagesDir, audience));
23087
23429
  }
23088
23430
  } else {
23089
23431
  await genPages(canonical, storyPagesDir);
@@ -23093,30 +23435,30 @@ async function buildDocs(options) {
23093
23435
  audienceSplit: options.audienceSplit ?? false
23094
23436
  });
23095
23437
  const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
23096
- writeNotesIndex(notesIndex, path16.join(storiesPublicDir, "notes-index.json"));
23438
+ writeNotesIndex(notesIndex, path17.join(storiesPublicDir, "notes-index.json"));
23097
23439
  const notesIndexed = notesIndex.notes.length;
23098
23440
  if (linksIndex) {
23099
- fs15.writeFileSync(
23100
- path16.join(storyPagesDir, "index.md"),
23441
+ fs16.writeFileSync(
23442
+ path17.join(storyPagesDir, "index.md"),
23101
23443
  renderOverviewPage(linksIndex, notesIndex),
23102
23444
  "utf8"
23103
23445
  );
23104
23446
  }
23105
- const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
23106
- const changesMdPath = path16.join(storyPagesDir, "changes.md");
23447
+ const changesJsonPath = path17.join(storiesPublicDir, "changes.json");
23448
+ const changesMdPath = path17.join(storyPagesDir, "changes.md");
23107
23449
  let changes;
23108
23450
  if (diff && linksIndex) {
23109
- fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23110
- 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");
23111
23453
  changes = diff.summary;
23112
23454
  } else {
23113
- fs15.rmSync(changesJsonPath, { force: true });
23114
- fs15.rmSync(changesMdPath, { force: true });
23455
+ fs16.rmSync(changesJsonPath, { force: true });
23456
+ fs16.rmSync(changesMdPath, { force: true });
23115
23457
  }
23116
23458
  let apiPages = 0;
23117
23459
  if (options.openapiPath) {
23118
23460
  const res = await importOpenApi({
23119
- specPath: path16.resolve(options.openapiPath),
23461
+ specPath: path17.resolve(options.openapiPath),
23120
23462
  outputDir: apiDir,
23121
23463
  runFile: reportPath,
23122
23464
  force: true
@@ -23131,11 +23473,11 @@ async function buildDocs(options) {
23131
23473
  }
23132
23474
 
23133
23475
  // src/config.ts
23134
- import { existsSync as existsSync13 } from "fs";
23135
- import { resolve as resolve10 } from "path";
23476
+ import { existsSync as existsSync14 } from "fs";
23477
+ import { resolve as resolve11 } from "path";
23136
23478
  async function loadConfig(configPath) {
23137
- const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
23138
- if (!existsSync13(resolved)) return {};
23479
+ const resolved = configPath ? resolve11(configPath) : resolve11(process.cwd(), "executable-stories.config.js");
23480
+ if (!existsSync14(resolved)) return {};
23139
23481
  const mod = await import(resolved);
23140
23482
  const config = mod.default;
23141
23483
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -23163,6 +23505,7 @@ USAGE
23163
23505
  executable-stories format <file> [options]
23164
23506
  executable-stories format --stdin [options]
23165
23507
  executable-stories watch <raw-run.json> [options]
23508
+ executable-stories serve <raw-run.json> [--port <n>] [--host <host>] [options]
23166
23509
  executable-stories compare <baseline-file> <current-file> [options]
23167
23510
  executable-stories gate-release <dev-run.json> <rc-run.json> [options]
23168
23511
  executable-stories review <file> --changed-files <path> [options]
@@ -23186,6 +23529,7 @@ USAGE
23186
23529
  SUBCOMMANDS
23187
23530
  format Read raw test results and generate reports
23188
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)
23189
23533
  compare Compare two runs and generate a diff report
23190
23534
  gate-release Verify a release candidate against the dev test baseline (RC gate)
23191
23535
  review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
@@ -23404,9 +23748,9 @@ async function parseCliArgs(argv) {
23404
23748
  process.exit(EXIT_SUCCESS);
23405
23749
  }
23406
23750
  const subcommand = args[0];
23407
- 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") {
23408
23752
  console.error(
23409
- `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".`
23410
23754
  );
23411
23755
  process.exit(EXIT_USAGE);
23412
23756
  }
@@ -23524,6 +23868,8 @@ async function parseCliArgs(argv) {
23524
23868
  "webhook-hmac-timestamp": { type: "boolean", default: false },
23525
23869
  "asset-mode": { type: "string", default: "none" },
23526
23870
  "allow-missing-assets": { type: "boolean", default: false },
23871
+ port: { type: "string" },
23872
+ host: { type: "string" },
23527
23873
  "pr-summary": { type: "boolean", default: false },
23528
23874
  "pr-summary-file": { type: "string" },
23529
23875
  "fail-on-regression": { type: "boolean", default: false },
@@ -23746,7 +24092,9 @@ async function parseCliArgs(argv) {
23746
24092
  headRef: values["head-ref"],
23747
24093
  failOn: failOnRaw,
23748
24094
  minEvidence: minEvidenceRaw,
23749
- 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"
23750
24098
  };
23751
24099
  return { args: cliArgs, pluginConfig, customRequested };
23752
24100
  }
@@ -23754,27 +24102,27 @@ async function readInput(args) {
23754
24102
  if (args.stdin) {
23755
24103
  return readStdin();
23756
24104
  }
23757
- const filePath = path17.resolve(args.inputFile);
23758
- if (!fs16.existsSync(filePath)) {
24105
+ const filePath = path18.resolve(args.inputFile);
24106
+ if (!fs17.existsSync(filePath)) {
23759
24107
  console.error(`Error: File not found: ${filePath}`);
23760
24108
  process.exit(EXIT_USAGE);
23761
24109
  }
23762
- return fs16.readFileSync(filePath, "utf8");
24110
+ return fs17.readFileSync(filePath, "utf8");
23763
24111
  }
23764
24112
  function readFileInput(filePath) {
23765
- const resolved = path17.resolve(filePath);
23766
- if (!fs16.existsSync(resolved)) {
24113
+ const resolved = path18.resolve(filePath);
24114
+ if (!fs17.existsSync(resolved)) {
23767
24115
  console.error(`Error: File not found: ${resolved}`);
23768
24116
  process.exit(EXIT_USAGE);
23769
24117
  }
23770
- return fs16.readFileSync(resolved, "utf8");
24118
+ return fs17.readFileSync(resolved, "utf8");
23771
24119
  }
23772
24120
  function readStdin() {
23773
- return new Promise((resolve12, reject) => {
24121
+ return new Promise((resolve13, reject) => {
23774
24122
  const chunks = [];
23775
24123
  process.stdin.setEncoding("utf8");
23776
24124
  process.stdin.on("data", (chunk) => chunks.push(chunk));
23777
- process.stdin.on("end", () => resolve12(chunks.join("")));
24125
+ process.stdin.on("end", () => resolve13(chunks.join("")));
23778
24126
  process.stdin.on("error", reject);
23779
24127
  });
23780
24128
  }
@@ -23900,14 +24248,14 @@ function tryNormalizeRunFromText(text2, args) {
23900
24248
  }
23901
24249
  }
23902
24250
  function listBaselineCandidates(currentFile, args) {
23903
- const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
23904
- const currentResolved = path17.resolve(currentFile);
23905
- 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)) {
23906
24254
  console.error(`Error: baseline directory not found: ${baselineDir}`);
23907
24255
  process.exit(EXIT_USAGE);
23908
24256
  }
23909
- const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
23910
- 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(
23911
24259
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
23912
24260
  );
23913
24261
  }
@@ -23915,14 +24263,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
23915
24263
  const candidates = listBaselineCandidates(currentFile, args);
23916
24264
  const comparable = [];
23917
24265
  for (const candidate of candidates) {
23918
- const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
24266
+ const run = tryNormalizeRunFromText(fs17.readFileSync(candidate, "utf8"), args);
23919
24267
  if (run) {
23920
24268
  comparable.push({ file: candidate, run });
23921
24269
  }
23922
24270
  }
23923
24271
  if (comparable.length === 0) {
23924
24272
  console.error(
23925
- `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))}.`
23926
24274
  );
23927
24275
  process.exit(EXIT_USAGE);
23928
24276
  }
@@ -24124,6 +24472,24 @@ async function main() {
24124
24472
  });
24125
24473
  return;
24126
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
+ }
24127
24493
  const text2 = await readInput(args);
24128
24494
  if (args.inputType === "ndjson") {
24129
24495
  if (args.subcommand === "validate") {
@@ -24167,9 +24533,9 @@ async function main() {
24167
24533
  process.exit(EXIT_SCHEMA_VALIDATION);
24168
24534
  }
24169
24535
  if (args.emitCanonical) {
24170
- const outPath = path17.resolve(args.emitCanonical);
24171
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24172
- 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");
24173
24539
  }
24174
24540
  try {
24175
24541
  const result = await generateReports(run, args);
@@ -24226,9 +24592,9 @@ ${msg}`);
24226
24592
  }
24227
24593
  const run = data;
24228
24594
  if (args.emitCanonical) {
24229
- const outPath = path17.resolve(args.emitCanonical);
24230
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24231
- 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");
24232
24598
  }
24233
24599
  try {
24234
24600
  const result = await generateReports(run, args);
@@ -24284,9 +24650,9 @@ ${msg}`);
24284
24650
  process.exit(EXIT_CANONICAL_VALIDATION);
24285
24651
  }
24286
24652
  if (args.emitCanonical) {
24287
- const outPath = path17.resolve(args.emitCanonical);
24288
- fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24289
- 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");
24290
24656
  }
24291
24657
  try {
24292
24658
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -24311,9 +24677,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
24311
24677
  const ext = formatter.fileExtension ?? formatName;
24312
24678
  const baseName = args.outputName ?? "report";
24313
24679
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
24314
- const filepath = path17.join(outputDir, filename);
24315
- fs16.mkdirSync(outputDir, { recursive: true });
24316
- fs16.writeFileSync(filepath, content, "utf8");
24680
+ const filepath = path18.join(outputDir, filename);
24681
+ fs17.mkdirSync(outputDir, { recursive: true });
24682
+ fs17.writeFileSync(filepath, content, "utf8");
24317
24683
  console.log(`Generated: ${filepath}`);
24318
24684
  } catch (err) {
24319
24685
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -24363,13 +24729,13 @@ async function dispatchNotifications(run, args) {
24363
24729
  }
24364
24730
  function runHistoryPipeline(run, args) {
24365
24731
  if (!args.historyFile) return;
24366
- const historyPath = path17.resolve(args.historyFile);
24732
+ const historyPath = path18.resolve(args.historyFile);
24367
24733
  const store = loadHistory(
24368
24734
  { filePath: historyPath },
24369
24735
  {
24370
24736
  readFile: (p) => {
24371
24737
  try {
24372
- return fs16.readFileSync(p, "utf8");
24738
+ return fs17.readFileSync(p, "utf8");
24373
24739
  } catch {
24374
24740
  return void 0;
24375
24741
  }
@@ -24382,11 +24748,11 @@ function runHistoryPipeline(run, args) {
24382
24748
  run,
24383
24749
  maxRuns: args.maxHistoryRuns
24384
24750
  });
24385
- const dir = path17.dirname(historyPath);
24386
- fs16.mkdirSync(dir, { recursive: true });
24751
+ const dir = path18.dirname(historyPath);
24752
+ fs17.mkdirSync(dir, { recursive: true });
24387
24753
  saveHistory(
24388
24754
  { filePath: historyPath, store: updated },
24389
- { writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
24755
+ { writeFile: (p, content) => fs17.writeFileSync(p, content, "utf8") }
24390
24756
  );
24391
24757
  let metricsCount = 0;
24392
24758
  for (const testId of Object.keys(updated.tests)) {
@@ -24534,11 +24900,11 @@ function writeReviewReport(review, args) {
24534
24900
  const outputDir = args.outputDir ?? "reports";
24535
24901
  const baseName = args.outputName ?? "evidence-review";
24536
24902
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
24537
- fs16.mkdirSync(outputDir, { recursive: true });
24538
- const mdPath = path17.join(outputDir, `${baseName}${suffix}.md`);
24539
- const htmlPath = path17.join(outputDir, `${baseName}${suffix}.html`);
24540
- fs16.writeFileSync(mdPath, markdown, "utf8");
24541
- 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");
24542
24908
  return [mdPath, htmlPath];
24543
24909
  }
24544
24910
  function evaluateReviewGate(review, args) {
@@ -24584,9 +24950,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
24584
24950
  function printCompareResult(result, args, startMs) {
24585
24951
  const durationMs = Date.now() - startMs;
24586
24952
  if (result.prSummary && args.prSummaryFile) {
24587
- const outputPath = path17.resolve(args.prSummaryFile);
24588
- fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
24589
- 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");
24590
24956
  }
24591
24957
  if (args.jsonSummary) {
24592
24958
  console.log(
@@ -24615,13 +24981,13 @@ function printCompareResult(result, args, startMs) {
24615
24981
  }
24616
24982
  }
24617
24983
  function loadReleasePolicy(policyPath) {
24618
- const resolved = path17.resolve(policyPath);
24619
- if (!fs16.existsSync(resolved)) {
24984
+ const resolved = path18.resolve(policyPath);
24985
+ if (!fs17.existsSync(resolved)) {
24620
24986
  console.error(`Error: release policy file not found: ${resolved}`);
24621
24987
  process.exit(EXIT_USAGE);
24622
24988
  }
24623
24989
  try {
24624
- const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
24990
+ const raw = JSON.parse(fs17.readFileSync(resolved, "utf8"));
24625
24991
  return {
24626
24992
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
24627
24993
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -24725,7 +25091,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24725
25091
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24726
25092
  process.exit(EXIT_USAGE);
24727
25093
  }
24728
- if (!fs16.existsSync(inputFile)) {
25094
+ if (!fs17.existsSync(inputFile)) {
24729
25095
  console.error(`Error: file not found: ${inputFile}`);
24730
25096
  process.exit(EXIT_USAGE);
24731
25097
  }
@@ -24753,7 +25119,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24753
25119
  console.error("Error: --title is required when creating a new page");
24754
25120
  process.exit(EXIT_USAGE);
24755
25121
  }
24756
- const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
25122
+ const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
24757
25123
  if (dryRun) {
24758
25124
  console.log(
24759
25125
  JSON.stringify(
@@ -24832,7 +25198,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24832
25198
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24833
25199
  process.exit(EXIT_USAGE);
24834
25200
  }
24835
- if (!fs16.existsSync(inputFile)) {
25201
+ if (!fs17.existsSync(inputFile)) {
24836
25202
  console.error(`Error: file not found: ${inputFile}`);
24837
25203
  process.exit(EXIT_USAGE);
24838
25204
  }
@@ -24859,7 +25225,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24859
25225
  process.exit(EXIT_USAGE);
24860
25226
  }
24861
25227
  const mode = modeRaw;
24862
- const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
25228
+ const adf = fs17.readFileSync(path18.resolve(inputFile), "utf8");
24863
25229
  if (dryRun) {
24864
25230
  console.log(
24865
25231
  JSON.stringify(
@@ -25047,7 +25413,7 @@ async function runBuildDocs(rawArgs) {
25047
25413
  ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
25048
25414
  );
25049
25415
  }
25050
- const rel = path17.relative(process.cwd(), result.siteDir) || ".";
25416
+ const rel = path18.relative(process.cwd(), result.siteDir) || ".";
25051
25417
  console.log(`
25052
25418
  Preview: cd ${rel} && npm run dev`);
25053
25419
  return EXIT_SUCCESS;
@@ -25274,7 +25640,9 @@ function createDefaultCliArgs() {
25274
25640
  failOnAddedFailures: false,
25275
25641
  failOnRemoval: false,
25276
25642
  failOnNew: false,
25277
- baselineMode: "explicit"
25643
+ baselineMode: "explicit",
25644
+ servePort: 4321,
25645
+ serveHost: "127.0.0.1"
25278
25646
  };
25279
25647
  }
25280
25648
  main().catch((err) => {