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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path11 from "path";
3
+ import * as path12 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -42,6 +42,41 @@ function generateFeatureId(uri) {
42
42
  function generateScenarioId(featureId, scenarioName) {
43
43
  return `${featureId};${slugify(scenarioName)}`;
44
44
  }
45
+ function normalizeText(text2) {
46
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
47
+ }
48
+ function tokenize(text2) {
49
+ const normalized = normalizeText(text2);
50
+ return normalized.length === 0 ? [] : normalized.split(" ");
51
+ }
52
+ function behaviourFingerprint(input) {
53
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
54
+ const covers = [...input.covers ?? []].map((path13) => path13.trim()).filter(Boolean).sort().join(",");
55
+ if (steps.length === 0 && covers.length === 0) return "";
56
+ return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
57
+ }
58
+ function jaccard(a, b) {
59
+ if (a.size === 0 && b.size === 0) return 1;
60
+ if (a.size === 0 || b.size === 0) return 0;
61
+ let intersection = 0;
62
+ for (const value of a) if (b.has(value)) intersection += 1;
63
+ return intersection / (a.size + b.size - intersection);
64
+ }
65
+ function stepTokens(steps) {
66
+ const tokens = /* @__PURE__ */ new Set();
67
+ for (const step of steps) {
68
+ for (const token of tokenize(step.text)) tokens.add(token);
69
+ }
70
+ return tokens;
71
+ }
72
+ function behaviourSimilarity(a, b) {
73
+ const aSteps = stepTokens(a.steps);
74
+ const bSteps = stepTokens(b.steps);
75
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
76
+ const stepScore = jaccard(aSteps, bSteps);
77
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
78
+ return stepScore * 0.7 + titleScore * 0.3;
79
+ }
45
80
 
46
81
  // src/converters/acl/steps.ts
47
82
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -14239,12 +14274,12 @@ function hasSufficientHistory(entries, min) {
14239
14274
  }
14240
14275
 
14241
14276
  // src/formatters/html/renderers/scenario.ts
14242
- function renderTicket(ticket, template, escapeHtml4) {
14277
+ function renderTicket(ticket, template, escapeHtml5) {
14243
14278
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
14244
14279
  if (url) {
14245
- return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
14280
+ return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
14246
14281
  }
14247
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
14282
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
14248
14283
  }
14249
14284
  function renderScenario(args, deps) {
14250
14285
  const { tc } = args;
@@ -14444,7 +14479,7 @@ function flattenTree(roots) {
14444
14479
  }
14445
14480
  return result;
14446
14481
  }
14447
- function buildTooltip(span, escapeHtml4) {
14482
+ function buildTooltip(span, escapeHtml5) {
14448
14483
  const parts = [];
14449
14484
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
14450
14485
  if (span.statusMessage) {
@@ -14462,7 +14497,7 @@ function buildTooltip(span, escapeHtml4) {
14462
14497
  if (text2.length > TOOLTIP_MAX_LENGTH) {
14463
14498
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
14464
14499
  }
14465
- return escapeHtml4(text2);
14500
+ return escapeHtml5(text2);
14466
14501
  }
14467
14502
  function renderTraceView(args, deps) {
14468
14503
  if (!args.spans || args.spans.length === 0) return "";
@@ -15757,14 +15792,14 @@ var TraceabilityMatrixFormatter = class {
15757
15792
  lines.push("");
15758
15793
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
15759
15794
  if (req.covers.length > 0) {
15760
- lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
15795
+ lines.push(`Covers: ${req.covers.map((path13) => `\`${path13}\``).join(", ")}`);
15761
15796
  }
15762
15797
  lines.push("");
15763
15798
  lines.push("| Status | Scenario | Source | Covers |");
15764
15799
  lines.push("| --- | --- | --- | --- |");
15765
15800
  for (const scenario of req.scenarios) {
15766
15801
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15767
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
15802
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path13) => `\`${path13}\``).join(", ") : "";
15768
15803
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
15769
15804
  }
15770
15805
  lines.push("");
@@ -15866,8 +15901,8 @@ function extractFeatureName(testCases, uri) {
15866
15901
  return tc.titlePath[0];
15867
15902
  }
15868
15903
  }
15869
- const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
15870
- return basename3.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
15904
+ const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
15905
+ return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
15871
15906
  }
15872
15907
  function synthesizeFeature(uri, testCases) {
15873
15908
  const featureName = extractFeatureName(testCases, uri);
@@ -16479,8 +16514,8 @@ function extractDocAttachments(step) {
16479
16514
  }
16480
16515
  return attachments;
16481
16516
  }
16482
- function guessMediaType(path12) {
16483
- const lower = path12.toLowerCase();
16517
+ function guessMediaType(path13) {
16518
+ const lower = path13.toLowerCase();
16484
16519
  if (lower.endsWith(".png")) return "image/png";
16485
16520
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16486
16521
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16621,11 +16656,11 @@ var CucumberHtmlFormatter = class {
16621
16656
  for (const envelope of envelopes) {
16622
16657
  const accepted = htmlStream.write(envelope);
16623
16658
  if (!accepted) {
16624
- await new Promise((resolve9) => htmlStream.once("drain", resolve9));
16659
+ await new Promise((resolve10) => htmlStream.once("drain", resolve10));
16625
16660
  }
16626
16661
  }
16627
- await new Promise((resolve9, reject) => {
16628
- collector.on("finish", resolve9);
16662
+ await new Promise((resolve10, reject) => {
16663
+ collector.on("finish", resolve10);
16629
16664
  collector.on("error", reject);
16630
16665
  htmlStream.end();
16631
16666
  });
@@ -16663,6 +16698,10 @@ function titleFor(kind) {
16663
16698
  return "Added";
16664
16699
  case "removed":
16665
16700
  return "Removed";
16701
+ case "renamed":
16702
+ return "Renamed";
16703
+ case "moved":
16704
+ return "Moved";
16666
16705
  case "changed":
16667
16706
  return "Changed";
16668
16707
  default:
@@ -16711,6 +16750,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
16711
16750
  addSection(lines, diff, "fixed", maxScenarios);
16712
16751
  addSection(lines, diff, "added", maxScenarios);
16713
16752
  addSection(lines, diff, "removed", maxScenarios);
16753
+ addSection(lines, diff, "renamed", maxScenarios);
16754
+ addSection(lines, diff, "moved", maxScenarios);
16714
16755
  addSection(lines, diff, "changed", maxScenarios);
16715
16756
  return lines.join("\n").trimEnd();
16716
16757
  }
@@ -16757,8 +16798,10 @@ function sortDiffs(scenarios) {
16757
16798
  fixed: 1,
16758
16799
  added: 2,
16759
16800
  removed: 3,
16760
- changed: 4,
16761
- unchanged: 5
16801
+ renamed: 4,
16802
+ moved: 5,
16803
+ changed: 6,
16804
+ unchanged: 7
16762
16805
  };
16763
16806
  return [...scenarios].sort((a, b) => {
16764
16807
  if (rank[a.kind] !== rank[b.kind]) {
@@ -16773,69 +16816,108 @@ function sortDiffs(scenarios) {
16773
16816
  return a.scenario.localeCompare(b.scenario);
16774
16817
  });
16775
16818
  }
16819
+ var SIMILARITY_THRESHOLD = 0.75;
16820
+ function identityInput(tc) {
16821
+ return {
16822
+ scenario: tc.story.scenario,
16823
+ sourceFile: tc.sourceFile,
16824
+ steps: tc.story.steps,
16825
+ covers: tc.story.covers
16826
+ };
16827
+ }
16828
+ function changedFieldsOf(flags) {
16829
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16830
+ }
16831
+ function allChangedFlags(errorMessage) {
16832
+ return {
16833
+ status: true,
16834
+ steps: true,
16835
+ docs: true,
16836
+ tags: true,
16837
+ tickets: true,
16838
+ source: true,
16839
+ duration: true,
16840
+ attachments: true,
16841
+ error: Boolean(errorMessage),
16842
+ titlePath: true
16843
+ };
16844
+ }
16845
+ function matchIdentities(removed, added) {
16846
+ const pairs = [];
16847
+ const remainingRemoved = new Set(removed);
16848
+ const remainingAdded = new Set(added);
16849
+ const removedByFp = /* @__PURE__ */ new Map();
16850
+ const addedByFp = /* @__PURE__ */ new Map();
16851
+ for (const tc of remainingRemoved) {
16852
+ const fp = behaviourFingerprint(identityInput(tc));
16853
+ if (fp === "") continue;
16854
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
16855
+ }
16856
+ for (const tc of remainingAdded) {
16857
+ const fp = behaviourFingerprint(identityInput(tc));
16858
+ if (fp === "") continue;
16859
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
16860
+ }
16861
+ for (const [fp, removedGroup] of removedByFp) {
16862
+ const addedGroup = addedByFp.get(fp);
16863
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
16864
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
16865
+ remainingRemoved.delete(removedGroup[0]);
16866
+ remainingAdded.delete(addedGroup[0]);
16867
+ }
16868
+ }
16869
+ for (const before of [...remainingRemoved]) {
16870
+ let best;
16871
+ let bestScore = 0;
16872
+ let tied = false;
16873
+ for (const after of remainingAdded) {
16874
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
16875
+ if (score > bestScore) {
16876
+ bestScore = score;
16877
+ best = after;
16878
+ tied = false;
16879
+ } else if (score === bestScore) {
16880
+ tied = true;
16881
+ }
16882
+ }
16883
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
16884
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
16885
+ remainingRemoved.delete(before);
16886
+ remainingAdded.delete(best);
16887
+ }
16888
+ }
16889
+ return {
16890
+ pairs,
16891
+ unmatchedRemoved: [...remainingRemoved],
16892
+ unmatchedAdded: [...remainingAdded]
16893
+ };
16894
+ }
16895
+ function identityKind(before, after) {
16896
+ const titleChanged = before.story.scenario !== after.story.scenario;
16897
+ const fileChanged = before.sourceFile !== after.sourceFile;
16898
+ return fileChanged && !titleChanged ? "moved" : "renamed";
16899
+ }
16776
16900
  function diffRuns(baseline, current) {
16777
16901
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
16778
16902
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
16779
16903
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
16780
16904
  const scenarios = [];
16905
+ const removedCases = [];
16906
+ const addedCases = [];
16781
16907
  for (const id of ids) {
16782
16908
  const before = baselineById.get(id);
16783
16909
  const after = currentById.get(id);
16784
16910
  if (!before && after) {
16785
- const flags2 = {
16786
- status: true,
16787
- steps: true,
16788
- docs: true,
16789
- tags: true,
16790
- tickets: true,
16791
- source: true,
16792
- duration: true,
16793
- attachments: true,
16794
- error: Boolean(after.errorMessage),
16795
- titlePath: true
16796
- };
16797
- scenarios.push({
16798
- kind: "added",
16799
- id,
16800
- scenario: after.story.scenario,
16801
- sourceFile: after.sourceFile,
16802
- sourceLine: after.sourceLine,
16803
- current: toScenarioSnapshot(after),
16804
- flags: flags2,
16805
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16806
- });
16911
+ addedCases.push(after);
16807
16912
  continue;
16808
16913
  }
16809
16914
  if (before && !after) {
16810
- const flags2 = {
16811
- status: true,
16812
- steps: true,
16813
- docs: true,
16814
- tags: true,
16815
- tickets: true,
16816
- source: true,
16817
- duration: true,
16818
- attachments: true,
16819
- error: Boolean(before.errorMessage),
16820
- titlePath: true
16821
- };
16822
- scenarios.push({
16823
- kind: "removed",
16824
- id,
16825
- scenario: before.story.scenario,
16826
- sourceFile: before.sourceFile,
16827
- sourceLine: before.sourceLine,
16828
- baseline: toScenarioSnapshot(before),
16829
- flags: flags2,
16830
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16831
- });
16832
- continue;
16833
- }
16834
- if (!before || !after) {
16915
+ removedCases.push(before);
16835
16916
  continue;
16836
16917
  }
16918
+ if (!before || !after) continue;
16837
16919
  const flags = buildFlags(before, after);
16838
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16920
+ const changedFields = changedFieldsOf(flags);
16839
16921
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
16840
16922
  scenarios.push({
16841
16923
  kind,
@@ -16850,12 +16932,59 @@ function diffRuns(baseline, current) {
16850
16932
  durationDeltaMs: after.durationMs - before.durationMs
16851
16933
  });
16852
16934
  }
16935
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
16936
+ for (const { before, after, confidence, matchedBy } of pairs) {
16937
+ const flags = buildFlags(before, after);
16938
+ scenarios.push({
16939
+ kind: identityKind(before, after),
16940
+ id: after.id,
16941
+ previousId: before.id,
16942
+ scenario: after.story.scenario,
16943
+ sourceFile: after.sourceFile,
16944
+ sourceLine: after.sourceLine,
16945
+ baseline: toScenarioSnapshot(before),
16946
+ current: toScenarioSnapshot(after),
16947
+ flags,
16948
+ changedFields: changedFieldsOf(flags),
16949
+ durationDeltaMs: after.durationMs - before.durationMs,
16950
+ matchConfidence: confidence,
16951
+ matchedBy
16952
+ });
16953
+ }
16954
+ for (const after of unmatchedAdded) {
16955
+ const flags = allChangedFlags(after.errorMessage);
16956
+ scenarios.push({
16957
+ kind: "added",
16958
+ id: after.id,
16959
+ scenario: after.story.scenario,
16960
+ sourceFile: after.sourceFile,
16961
+ sourceLine: after.sourceLine,
16962
+ current: toScenarioSnapshot(after),
16963
+ flags,
16964
+ changedFields: changedFieldsOf(flags)
16965
+ });
16966
+ }
16967
+ for (const before of unmatchedRemoved) {
16968
+ const flags = allChangedFlags(before.errorMessage);
16969
+ scenarios.push({
16970
+ kind: "removed",
16971
+ id: before.id,
16972
+ scenario: before.story.scenario,
16973
+ sourceFile: before.sourceFile,
16974
+ sourceLine: before.sourceLine,
16975
+ baseline: toScenarioSnapshot(before),
16976
+ flags,
16977
+ changedFields: changedFieldsOf(flags)
16978
+ });
16979
+ }
16853
16980
  const sorted = sortDiffs(scenarios);
16854
16981
  const summary = {
16855
16982
  totalBaseline: baseline.testCases.length,
16856
16983
  totalCurrent: current.testCases.length,
16857
16984
  added: sorted.filter((s) => s.kind === "added").length,
16858
16985
  removed: sorted.filter((s) => s.kind === "removed").length,
16986
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
16987
+ moved: sorted.filter((s) => s.kind === "moved").length,
16859
16988
  changed: sorted.filter((s) => s.kind === "changed").length,
16860
16989
  regressed: sorted.filter((s) => s.kind === "regressed").length,
16861
16990
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -16883,6 +17012,10 @@ function statusLabel(kind) {
16883
17012
  return "Added";
16884
17013
  case "removed":
16885
17014
  return "Removed";
17015
+ case "renamed":
17016
+ return "Renamed";
17017
+ case "moved":
17018
+ return "Moved";
16886
17019
  case "changed":
16887
17020
  return "Changed";
16888
17021
  default:
@@ -17208,6 +17341,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17208
17341
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17209
17342
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17210
17343
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
17344
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
17345
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17211
17346
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17212
17347
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17213
17348
  </section>
@@ -17268,6 +17403,10 @@ function formatStatus(kind) {
17268
17403
  return "Added";
17269
17404
  case "removed":
17270
17405
  return "Removed";
17406
+ case "renamed":
17407
+ return "Renamed";
17408
+ case "moved":
17409
+ return "Moved";
17271
17410
  case "changed":
17272
17411
  return "Changed";
17273
17412
  default:
@@ -17420,13 +17559,13 @@ var RunDiffMarkdownFormatter = class {
17420
17559
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
17421
17560
  }
17422
17561
  lines.push("");
17423
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
17424
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
17562
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
17563
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
17425
17564
  lines.push(
17426
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17565
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17427
17566
  );
17428
17567
  lines.push("");
17429
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
17568
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
17430
17569
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
17431
17570
  if (scenarios.length === 0) continue;
17432
17571
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -18685,14 +18824,14 @@ ${result.errors.join("\n")}`);
18685
18824
  }
18686
18825
 
18687
18826
  // src/coverage-index.ts
18688
- function normalizePath(path12) {
18689
- return path12.replace(/^\.\//, "");
18827
+ function normalizePath(path13) {
18828
+ return path13.replace(/^\.\//, "");
18690
18829
  }
18691
18830
  function scenariosCoveringPaths(index, paths) {
18692
18831
  const queries = paths.map(normalizePath);
18693
18832
  return index.scenarios.filter(
18694
18833
  (scenario) => scenario.covers.some(
18695
- (glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
18834
+ (glob) => queries.some((path13) => matchesPattern(normalizePath(glob), path13))
18696
18835
  )
18697
18836
  );
18698
18837
  }
@@ -18768,7 +18907,7 @@ function toRun(data, inputType, synthesize) {
18768
18907
  if (synthesize) raw = synthesizeStories(raw);
18769
18908
  return canonicalizeRun(raw);
18770
18909
  }
18771
- async function regenerateArtifacts(options, deps = {}) {
18910
+ async function regenerateRun(options, deps = {}) {
18772
18911
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18773
18912
  const data = JSON.parse(read(path7.resolve(options.input)));
18774
18913
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -18778,7 +18917,10 @@ async function regenerateArtifacts(options, deps = {}) {
18778
18917
  outputName: options.outputName
18779
18918
  });
18780
18919
  const result = await generator.generate(run);
18781
- return [...result.values()].flat();
18920
+ return { files: [...result.values()].flat(), run };
18921
+ }
18922
+ async function regenerateArtifacts(options, deps = {}) {
18923
+ return (await regenerateRun(options, deps)).files;
18782
18924
  }
18783
18925
  function startWatch(options, deps = {}) {
18784
18926
  const log = deps.log ?? ((message) => console.log(message));
@@ -18821,6 +18963,203 @@ function startWatch(options, deps = {}) {
18821
18963
  };
18822
18964
  }
18823
18965
 
18966
+ // src/serve.ts
18967
+ import * as fs7 from "fs";
18968
+ import * as http from "http";
18969
+ import * as path8 from "path";
18970
+ function advanceState(prev, run) {
18971
+ if (prev.sessionBaseline === null) {
18972
+ return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
18973
+ }
18974
+ return {
18975
+ sessionBaseline: prev.sessionBaseline,
18976
+ previous: prev.current,
18977
+ current: run,
18978
+ runCount: prev.runCount + 1
18979
+ };
18980
+ }
18981
+ function computeDeltas(state) {
18982
+ if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
18983
+ return { session: null, iteration: null };
18984
+ }
18985
+ return {
18986
+ session: diffRuns(state.sessionBaseline, state.current),
18987
+ iteration: state.previous ? diffRuns(state.previous, state.current) : null
18988
+ };
18989
+ }
18990
+ function pluralize(n, word) {
18991
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
18992
+ }
18993
+ function summarizeDiff(diff) {
18994
+ const s = diff.summary;
18995
+ const parts = [];
18996
+ if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
18997
+ if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
18998
+ if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
18999
+ if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
19000
+ const moved = s.renamed + s.moved;
19001
+ if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
19002
+ if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
19003
+ return parts.length > 0 ? parts.join(", ") : null;
19004
+ }
19005
+ function escapeHtml3(text2) {
19006
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19007
+ }
19008
+ function renderDeltaStrip(state) {
19009
+ if (state.current === null) return "";
19010
+ const { session, iteration } = computeDeltas(state);
19011
+ if (session === null) {
19012
+ const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
19013
+ return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
19014
+ }
19015
+ const sessionLine = summarizeDiff(session) ?? "no change yet";
19016
+ let detail = "";
19017
+ if (iteration) {
19018
+ const iterationLine = summarizeDiff(iteration);
19019
+ if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
19020
+ }
19021
+ return [
19022
+ `<div data-es-live="strip">`,
19023
+ `<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
19024
+ `<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
19025
+ detail,
19026
+ `</div>`
19027
+ ].join("");
19028
+ }
19029
+ var RELOAD_CLIENT = `<script data-es-live="client">
19030
+ (function () {
19031
+ try {
19032
+ var es = new EventSource("/__es_reload");
19033
+ es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
19034
+ } catch (err) { /* SSE unavailable: stay static */ }
19035
+ })();
19036
+ </script>`;
19037
+ var STRIP_STYLE = `<style data-es-live="style">
19038
+ [data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
19039
+ padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
19040
+ [data-es-live="strip"] strong{color:#7dd3fc}
19041
+ </style>`;
19042
+ function injectLiveBits(html, stripHtml) {
19043
+ let out = html;
19044
+ const bodyOpen = out.match(/<body[^>]*>/i);
19045
+ if (bodyOpen) {
19046
+ const at = bodyOpen.index + bodyOpen[0].length;
19047
+ out = out.slice(0, at) + stripHtml + out.slice(at);
19048
+ } else {
19049
+ out = stripHtml + out;
19050
+ }
19051
+ const tail = STRIP_STYLE + RELOAD_CLIENT;
19052
+ if (/<\/body>/i.test(out)) {
19053
+ out = out.replace(/<\/body>/i, tail + "</body>");
19054
+ } else {
19055
+ out += tail;
19056
+ }
19057
+ return out;
19058
+ }
19059
+ var CONTENT_TYPES = {
19060
+ ".html": "text/html; charset=utf-8",
19061
+ ".css": "text/css; charset=utf-8",
19062
+ ".js": "text/javascript; charset=utf-8",
19063
+ ".json": "application/json; charset=utf-8",
19064
+ ".svg": "image/svg+xml",
19065
+ ".png": "image/png"
19066
+ };
19067
+ function watchInputDir(filePath, listener) {
19068
+ const dir = path8.dirname(filePath);
19069
+ const base = path8.basename(filePath);
19070
+ fs7.mkdirSync(dir, { recursive: true });
19071
+ const watcher = fs7.watch(dir, (_event, changed) => {
19072
+ if (!changed || changed === base) listener();
19073
+ });
19074
+ return { close: () => watcher.close() };
19075
+ }
19076
+ function plainText(html) {
19077
+ return html.replace(/<[^>]+>/g, "").trim();
19078
+ }
19079
+ function startServe(options, deps = {}) {
19080
+ const log = deps.log ?? ((message) => console.log(message));
19081
+ const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
19082
+ const port = options.port ?? 4321;
19083
+ const host = options.host ?? "127.0.0.1";
19084
+ let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
19085
+ let htmlPath = null;
19086
+ let stripHtml = renderDeltaStrip(state);
19087
+ const clients = /* @__PURE__ */ new Set();
19088
+ const pushReload = () => {
19089
+ for (const res of clients) res.write("data: reload\n\n");
19090
+ };
19091
+ const handler = (req, res) => {
19092
+ const url = (req.url ?? "/").split("?")[0];
19093
+ if (url === "/__es_reload") {
19094
+ res.writeHead(200, {
19095
+ "Content-Type": "text/event-stream",
19096
+ "Cache-Control": "no-cache",
19097
+ Connection: "keep-alive"
19098
+ });
19099
+ res.write("retry: 1000\n\n");
19100
+ clients.add(res);
19101
+ req.on("close", () => clients.delete(res));
19102
+ return;
19103
+ }
19104
+ if (url === "/" || url === "/index.html") {
19105
+ const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
19106
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19107
+ res.end(injectLiveBits(html, stripHtml));
19108
+ return;
19109
+ }
19110
+ const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
19111
+ const filePath = path8.join(path8.resolve(options.outputDir), safe);
19112
+ if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
19113
+ const ext = path8.extname(filePath).toLowerCase();
19114
+ res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
19115
+ res.end(read(filePath));
19116
+ return;
19117
+ }
19118
+ res.writeHead(404, { "Content-Type": "text/plain" });
19119
+ res.end("Not found");
19120
+ };
19121
+ const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
19122
+ const watchOptions = {
19123
+ input: options.input,
19124
+ outputDir: options.outputDir,
19125
+ outputName: options.outputName,
19126
+ formats: options.formats,
19127
+ inputType: options.inputType ?? "raw",
19128
+ synthesize: options.synthesize !== false,
19129
+ debounceMs: options.debounceMs
19130
+ };
19131
+ const watchHandle = startWatch(watchOptions, {
19132
+ readFile: read,
19133
+ watch: deps.watch ?? watchInputDir,
19134
+ log: () => {
19135
+ },
19136
+ // serve emits its own per-run line below
19137
+ regenerate: async (input) => {
19138
+ if (!fs7.existsSync(path8.resolve(input))) return [];
19139
+ const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
19140
+ htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
19141
+ state = advanceState(state, run);
19142
+ stripHtml = renderDeltaStrip(state);
19143
+ log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
19144
+ pushReload();
19145
+ return files;
19146
+ }
19147
+ });
19148
+ server.listen(port, host);
19149
+ const address = server.address();
19150
+ const boundPort = typeof address === "object" && address ? address.port : port;
19151
+ log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
19152
+ return {
19153
+ port: boundPort,
19154
+ close: () => {
19155
+ watchHandle.close();
19156
+ for (const res of clients) res.end();
19157
+ clients.clear();
19158
+ server.close();
19159
+ }
19160
+ };
19161
+ }
19162
+
18824
19163
  // src/behavior-diff.ts
18825
19164
  function classifyStatusChange(baseline, current) {
18826
19165
  if (baseline === void 0) return "added";
@@ -18858,7 +19197,10 @@ function diffStoryReports(baseline, current) {
18858
19197
  };
18859
19198
  });
18860
19199
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
18861
- for (const s of scenarios) summary[s.kind] += 1;
19200
+ for (const s of scenarios) {
19201
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19202
+ summary[s.kind] += 1;
19203
+ }
18862
19204
  return { schemaVersion: "1.0", summary, scenarios };
18863
19205
  }
18864
19206
 
@@ -19457,27 +19799,27 @@ function pickleStepArgumentToDocs(ps) {
19457
19799
  }
19458
19800
 
19459
19801
  // src/utils/git-info.ts
19460
- import * as fs7 from "fs";
19461
- import * as path8 from "path";
19802
+ import * as fs8 from "fs";
19803
+ import * as path9 from "path";
19462
19804
  function readGitSha(cwd = process.cwd()) {
19463
19805
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
19464
19806
  if (envSha) return envSha;
19465
19807
  const gitDir = findGitDir(cwd);
19466
19808
  if (!gitDir) return void 0;
19467
19809
  try {
19468
- const headPath = path8.join(gitDir, "HEAD");
19469
- const head = fs7.readFileSync(headPath, "utf8").trim();
19810
+ const headPath = path9.join(gitDir, "HEAD");
19811
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19470
19812
  if (!head.startsWith("ref:")) {
19471
19813
  return head;
19472
19814
  }
19473
19815
  const refPath = head.replace("ref:", "").trim();
19474
- const refFile = path8.join(gitDir, refPath);
19475
- if (fs7.existsSync(refFile)) {
19476
- return fs7.readFileSync(refFile, "utf8").trim();
19816
+ const refFile = path9.join(gitDir, refPath);
19817
+ if (fs8.existsSync(refFile)) {
19818
+ return fs8.readFileSync(refFile, "utf8").trim();
19477
19819
  }
19478
- const packedRefs = path8.join(gitDir, "packed-refs");
19479
- if (fs7.existsSync(packedRefs)) {
19480
- const content = fs7.readFileSync(packedRefs, "utf8");
19820
+ const packedRefs = path9.join(gitDir, "packed-refs");
19821
+ if (fs8.existsSync(packedRefs)) {
19822
+ const content = fs8.readFileSync(packedRefs, "utf8");
19481
19823
  for (const line of content.split("\n")) {
19482
19824
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
19483
19825
  const [sha, ref] = line.split(" ");
@@ -19492,19 +19834,19 @@ function readGitSha(cwd = process.cwd()) {
19492
19834
  function findGitDir(start) {
19493
19835
  let current = start;
19494
19836
  while (true) {
19495
- const candidate = path8.join(current, ".git");
19496
- if (fs7.existsSync(candidate)) {
19497
- const stat = fs7.statSync(candidate);
19837
+ const candidate = path9.join(current, ".git");
19838
+ if (fs8.existsSync(candidate)) {
19839
+ const stat = fs8.statSync(candidate);
19498
19840
  if (stat.isFile()) {
19499
- const content = fs7.readFileSync(candidate, "utf8").trim();
19841
+ const content = fs8.readFileSync(candidate, "utf8").trim();
19500
19842
  const match = content.match(/^gitdir: (.+)$/);
19501
19843
  if (match) {
19502
- return path8.resolve(current, match[1]);
19844
+ return path9.resolve(current, match[1]);
19503
19845
  }
19504
19846
  }
19505
19847
  return candidate;
19506
19848
  }
19507
- const parent = path8.dirname(current);
19849
+ const parent = path9.dirname(current);
19508
19850
  if (parent === current) return void 0;
19509
19851
  current = parent;
19510
19852
  }
@@ -19515,8 +19857,8 @@ function readBranchName(cwd = process.cwd()) {
19515
19857
  const gitDir = findGitDir(cwd);
19516
19858
  if (!gitDir) return void 0;
19517
19859
  try {
19518
- const headPath = path8.join(gitDir, "HEAD");
19519
- const head = fs7.readFileSync(headPath, "utf8").trim();
19860
+ const headPath = path9.join(gitDir, "HEAD");
19861
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19520
19862
  if (head.startsWith("ref:")) {
19521
19863
  const refPath = head.replace("ref:", "").trim();
19522
19864
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -19553,8 +19895,8 @@ function nanosecondsToMs(ns) {
19553
19895
  }
19554
19896
 
19555
19897
  // src/utils/metadata.ts
19556
- import * as fs8 from "fs";
19557
- import * as path9 from "path";
19898
+ import * as fs9 from "fs";
19899
+ import * as path10 from "path";
19558
19900
  var versionCache = /* @__PURE__ */ new Map();
19559
19901
  function readPackageVersion(root) {
19560
19902
  if (versionCache.has(root)) {
@@ -19565,18 +19907,18 @@ function readPackageVersion(root) {
19565
19907
  return version;
19566
19908
  }
19567
19909
  function findPackageVersion(startDir) {
19568
- let current = path9.resolve(startDir);
19910
+ let current = path10.resolve(startDir);
19569
19911
  while (true) {
19570
- const pkgPath = path9.join(current, "package.json");
19912
+ const pkgPath = path10.join(current, "package.json");
19571
19913
  try {
19572
- if (fs8.existsSync(pkgPath)) {
19573
- const raw = fs8.readFileSync(pkgPath, "utf8");
19914
+ if (fs9.existsSync(pkgPath)) {
19915
+ const raw = fs9.readFileSync(pkgPath, "utf8");
19574
19916
  const parsed = JSON.parse(raw);
19575
19917
  return parsed.version;
19576
19918
  }
19577
19919
  } catch {
19578
19920
  }
19579
- const parent = path9.dirname(current);
19921
+ const parent = path10.dirname(current);
19580
19922
  if (parent === current) {
19581
19923
  return void 0;
19582
19924
  }
@@ -20904,18 +21246,18 @@ function deriveChangeType(tags) {
20904
21246
  }
20905
21247
  return "unknown";
20906
21248
  }
20907
- function extensionOf(path12) {
20908
- const base = path12.split("/").pop() ?? path12;
21249
+ function extensionOf(path13) {
21250
+ const base = path13.split("/").pop() ?? path13;
20909
21251
  const dot = base.lastIndexOf(".");
20910
21252
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20911
21253
  }
20912
- function isTestFile(path12) {
20913
- return TEST_INFIX.test(path12);
21254
+ function isTestFile(path13) {
21255
+ return TEST_INFIX.test(path13);
20914
21256
  }
20915
- function isReviewableSource(path12) {
20916
- if (isTestFile(path12)) return false;
20917
- if (path12.endsWith(".d.ts")) return false;
20918
- return CODE_EXTENSIONS.has(extensionOf(path12));
21257
+ function isReviewableSource(path13) {
21258
+ if (isTestFile(path13)) return false;
21259
+ if (path13.endsWith(".d.ts")) return false;
21260
+ return CODE_EXTENSIONS.has(extensionOf(path13));
20919
21261
  }
20920
21262
  function testBaseKey(testFile) {
20921
21263
  return testFile.replace(TEST_INFIX, "");
@@ -21019,7 +21361,7 @@ function toClaim(testCase, changedSourcePaths) {
21019
21361
  const { strength, reasons } = gradeEvidence(testCase, audience);
21020
21362
  const key = testBaseKey(testCase.sourceFile);
21021
21363
  const coversFiles = changedSourcePaths.filter(
21022
- (path12) => sourceBaseKey(path12) === key
21364
+ (path13) => sourceBaseKey(path13) === key
21023
21365
  );
21024
21366
  return {
21025
21367
  id: testCase.id,
@@ -21268,7 +21610,7 @@ var ReviewMarkdownFormatter = class {
21268
21610
  };
21269
21611
 
21270
21612
  // src/formatters/review-html.ts
21271
- function escapeHtml3(value) {
21613
+ function escapeHtml4(value) {
21272
21614
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21273
21615
  }
21274
21616
  var STRENGTH_LABEL = {
@@ -21290,22 +21632,22 @@ function statusIcon3(status) {
21290
21632
  }
21291
21633
  }
21292
21634
  function formatStep3(step) {
21293
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21635
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21294
21636
  }
21295
21637
  function inlineDoc(doc) {
21296
21638
  switch (doc.kind) {
21297
21639
  case "note":
21298
- return escapeHtml3(doc.text);
21640
+ return escapeHtml4(doc.text);
21299
21641
  case "section":
21300
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21642
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21301
21643
  case "kv":
21302
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21644
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21303
21645
  case "code":
21304
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21646
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21305
21647
  case "link":
21306
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21648
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21307
21649
  default:
21308
- return escapeHtml3(doc.kind);
21650
+ return escapeHtml4(doc.kind);
21309
21651
  }
21310
21652
  }
21311
21653
  function renderEvidenceArtifacts(testCase) {
@@ -21313,7 +21655,7 @@ function renderEvidenceArtifacts(testCase) {
21313
21655
  for (const att of testCase.attachments) {
21314
21656
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21315
21657
  parts.push(
21316
- `<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
21658
+ `<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
21317
21659
  );
21318
21660
  }
21319
21661
  }
@@ -21328,22 +21670,22 @@ function renderTicketPills(claim) {
21328
21670
  const tickets = claim.testCase.story.tickets ?? [];
21329
21671
  if (tickets.length === 0) return "";
21330
21672
  return `<div class="ticket-row">${tickets.map((ticket) => {
21331
- const label = escapeHtml3(ticket.id);
21673
+ const label = escapeHtml4(ticket.id);
21332
21674
  if (ticket.url) {
21333
- return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21675
+ return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21334
21676
  }
21335
21677
  return `<span class="ticket-pill">${label}</span>`;
21336
21678
  }).join("")}</div>`;
21337
21679
  }
21338
21680
  function renderClaimCard(claim) {
21339
21681
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21340
- const search = escapeHtml3(
21682
+ const search = escapeHtml4(
21341
21683
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21342
21684
  ).toLowerCase();
21343
21685
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21344
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21345
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21346
- const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
21686
+ const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
21687
+ const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
21688
+ const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
21347
21689
  const docs = (claim.testCase.story.docs ?? []).filter(
21348
21690
  (d) => d.kind === "section" || d.kind === "note"
21349
21691
  );
@@ -21353,9 +21695,9 @@ function renderClaimCard(claim) {
21353
21695
  <header class="claim-header">
21354
21696
  <div>
21355
21697
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21356
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21357
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21358
- <p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21698
+ ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
21699
+ <h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
21700
+ <p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21359
21701
  ${renderTicketPills(claim)}
21360
21702
  </div>
21361
21703
  </header>
@@ -21370,18 +21712,18 @@ function renderClaimCard(claim) {
21370
21712
  </article>`;
21371
21713
  }
21372
21714
  function renderChangedFileRow(file) {
21373
- const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21715
+ const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21374
21716
  return `<tr data-band="${file.band}">
21375
21717
  <td><span class="band-dot band-${file.band}"></span></td>
21376
- <td><code>${escapeHtml3(file.path)}</code></td>
21377
- <td>${escapeHtml3(file.changeKind)}</td>
21718
+ <td><code>${escapeHtml4(file.path)}</code></td>
21719
+ <td>${escapeHtml4(file.changeKind)}</td>
21378
21720
  <td>${claims}</td>
21379
21721
  </tr>`;
21380
21722
  }
21381
21723
  function renderAudienceSection2(title, claims) {
21382
21724
  if (claims.length === 0) return "";
21383
21725
  return `<section class="audience-section">
21384
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21726
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21385
21727
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21386
21728
  </section>`;
21387
21729
  }
@@ -21471,13 +21813,13 @@ var ReviewHtmlFormatter = class {
21471
21813
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21472
21814
  applyTheme(getEffectiveTheme());` : "";
21473
21815
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21474
- const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
21816
+ const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
21475
21817
  return `<!doctype html>
21476
21818
  <html lang="en"${themeAttr}>
21477
21819
  <head>
21478
21820
  <meta charset="utf-8" />
21479
21821
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21480
- <title>${escapeHtml3(this.title)}</title>
21822
+ <title>${escapeHtml4(this.title)}</title>
21481
21823
  <style>
21482
21824
  ${this.theme.css}
21483
21825
  ${REVIEW_CSS}
@@ -21487,7 +21829,7 @@ applyTheme(getEffectiveTheme());` : "";
21487
21829
  <main>
21488
21830
  <div class="hero-card card">
21489
21831
  <div class="review-header">
21490
- <h1>${escapeHtml3(this.title)}</h1>
21832
+ <h1>${escapeHtml4(this.title)}</h1>
21491
21833
  ${themeToggleHtml}
21492
21834
  </div>
21493
21835
  ${refsLine}
@@ -21502,7 +21844,7 @@ applyTheme(getEffectiveTheme());` : "";
21502
21844
  </section>
21503
21845
  <section class="card priority-banner">
21504
21846
  <h2>Review priority</h2>
21505
- <p class="subtle">${escapeHtml3(priority)}</p>
21847
+ <p class="subtle">${escapeHtml4(priority)}</p>
21506
21848
  </section>
21507
21849
  ${changedFilesPanel}
21508
21850
  <section class="toolbar">
@@ -21550,8 +21892,8 @@ applyTheme(getEffectiveTheme());` : "";
21550
21892
  };
21551
21893
 
21552
21894
  // src/deploy/ledger.ts
21553
- import * as fs9 from "fs";
21554
- import * as path10 from "path";
21895
+ import * as fs10 from "fs";
21896
+ import * as path11 from "path";
21555
21897
  function createEmptyLedger() {
21556
21898
  return {
21557
21899
  deployments: [],
@@ -21559,12 +21901,12 @@ function createEmptyLedger() {
21559
21901
  };
21560
21902
  }
21561
21903
  function loadLedger(ledgerPath) {
21562
- const resolved = path10.resolve(ledgerPath);
21563
- if (!fs9.existsSync(resolved)) {
21904
+ const resolved = path11.resolve(ledgerPath);
21905
+ if (!fs10.existsSync(resolved)) {
21564
21906
  return createEmptyLedger();
21565
21907
  }
21566
21908
  try {
21567
- const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
21909
+ const raw = JSON.parse(fs10.readFileSync(resolved, "utf8"));
21568
21910
  if (raw.schemaVersion !== 1) {
21569
21911
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21570
21912
  }
@@ -21575,10 +21917,10 @@ function loadLedger(ledgerPath) {
21575
21917
  }
21576
21918
  }
21577
21919
  function saveLedger(ledger, ledgerPath) {
21578
- const resolved = path10.resolve(ledgerPath);
21579
- const dir = path10.dirname(resolved);
21580
- fs9.mkdirSync(dir, { recursive: true });
21581
- fs9.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21920
+ const resolved = path11.resolve(ledgerPath);
21921
+ const dir = path11.dirname(resolved);
21922
+ fs10.mkdirSync(dir, { recursive: true });
21923
+ fs10.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21582
21924
  }
21583
21925
  function getLatestDeployment(ledger, environment) {
21584
21926
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21698,11 +22040,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21698
22040
  const ext = FORMAT_EXTENSIONS[format];
21699
22041
  const effectiveName = outputName + (outputNameSuffix ?? "");
21700
22042
  if (mode === "aggregated") {
21701
- return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
22043
+ return toPosix(path12.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21702
22044
  }
21703
22045
  const normalizedSource = toPosix(sourceFile);
21704
- const dirOfSource = path11.posix.dirname(normalizedSource);
21705
- let baseName = path11.posix.basename(normalizedSource);
22046
+ const dirOfSource = path12.posix.dirname(normalizedSource);
22047
+ let baseName = path12.posix.basename(normalizedSource);
21706
22048
  for (const testExt of TEST_EXTENSIONS) {
21707
22049
  if (baseName.endsWith(testExt)) {
21708
22050
  baseName = baseName.slice(0, -testExt.length);
@@ -21711,12 +22053,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21711
22053
  }
21712
22054
  const fileName = `${baseName}.${effectiveName}${ext}`;
21713
22055
  if (colocatedStyle === "adjacent") {
21714
- return toPosix(path11.posix.join(dirOfSource, fileName));
22056
+ return toPosix(path12.posix.join(dirOfSource, fileName));
21715
22057
  }
21716
22058
  if (colocatedStyle === "flat") {
21717
- return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
22059
+ return toPosix(path12.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21718
22060
  }
21719
- return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
22061
+ return toPosix(path12.posix.join(baseOutputDir, dirOfSource, fileName));
21720
22062
  }
21721
22063
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
21722
22064
  const groups = /* @__PURE__ */ new Map();
@@ -21929,8 +22271,8 @@ var ReportGenerator = class {
21929
22271
  if (astroPaths) {
21930
22272
  for (const mdPath of astroPaths) {
21931
22273
  const content = await fsPromises.readFile(mdPath, "utf8");
21932
- const mdDir = path11.dirname(mdPath);
21933
- const assetsDir = path11.resolve(this.options.astro.assetsDir);
22274
+ const mdDir = path12.dirname(mdPath);
22275
+ const assetsDir = path12.resolve(this.options.astro.assetsDir);
21934
22276
  const result = copyMarkdownAssets({
21935
22277
  markdown: content,
21936
22278
  markdownDir: mdDir,
@@ -21961,9 +22303,9 @@ var ReportGenerator = class {
21961
22303
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
21962
22304
  const ext = FORMAT_EXTENSIONS[format];
21963
22305
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
21964
- const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22306
+ const outputPath = toPosix(path12.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21965
22307
  const content = await this.formatContent(run, format);
21966
- const dir = path11.dirname(outputPath);
22308
+ const dir = path12.dirname(outputPath);
21967
22309
  await fsPromises.mkdir(dir, { recursive: true });
21968
22310
  await this.deps.writeFile(outputPath, content);
21969
22311
  return [outputPath];
@@ -21975,7 +22317,7 @@ var ReportGenerator = class {
21975
22317
  testCases
21976
22318
  };
21977
22319
  const content = await this.formatContent(groupRun, format);
21978
- const dir = path11.dirname(outputPath);
22320
+ const dir = path12.dirname(outputPath);
21979
22321
  await fsPromises.mkdir(dir, { recursive: true });
21980
22322
  await this.deps.writeFile(outputPath, content);
21981
22323
  writtenPaths.push(outputPath);
@@ -22127,7 +22469,7 @@ async function generateRunComparison(args) {
22127
22469
  await fsPromises.mkdir(outputDir, { recursive: true });
22128
22470
  for (const format of args.formats) {
22129
22471
  const ext = format === "html" ? ".html" : ".md";
22130
- const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
22472
+ const outputPath = toPosix(path12.join(outputDir, `${outputName}${ext}`));
22131
22473
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
22132
22474
  await fsPromises.writeFile(outputPath, content, "utf8");
22133
22475
  files.push(outputPath);
@@ -22176,6 +22518,7 @@ export {
22176
22518
  adaptJestRun,
22177
22519
  adaptPlaywrightRun,
22178
22520
  adaptVitestRun,
22521
+ advanceState,
22179
22522
  assertValidRun,
22180
22523
  buildCheck,
22181
22524
  buildGoal,
@@ -22188,6 +22531,7 @@ export {
22188
22531
  canonicalizeRun,
22189
22532
  classifyStatusChange,
22190
22533
  clearVersionCache,
22534
+ computeDeltas,
22191
22535
  computeTestMetrics,
22192
22536
  copyMarkdownAssets,
22193
22537
  createPrCommentSummary,
@@ -22210,6 +22554,7 @@ export {
22210
22554
  getEnvironmentDrift,
22211
22555
  gradeEvidence,
22212
22556
  hasSufficientHistory,
22557
+ injectLiveBits,
22213
22558
  isReviewableSource,
22214
22559
  isTestFile,
22215
22560
  joinNameAndExt,
@@ -22231,7 +22576,9 @@ export {
22231
22576
  readPackageVersion,
22232
22577
  recordDeployment,
22233
22578
  regenerateArtifacts,
22579
+ regenerateRun,
22234
22580
  renderCheck,
22581
+ renderDeltaStrip,
22235
22582
  renderGoal,
22236
22583
  renderTriage,
22237
22584
  resolveAttachment,
@@ -22247,6 +22594,7 @@ export {
22247
22594
  sendWebhookNotification,
22248
22595
  signBody,
22249
22596
  slugify,
22597
+ startServe,
22250
22598
  startWatch,
22251
22599
  stripAnsi,
22252
22600
  toBehaviorManifest,