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.cjs CHANGED
@@ -59,6 +59,7 @@ __export(src_exports, {
59
59
  adaptJestRun: () => adaptJestRun,
60
60
  adaptPlaywrightRun: () => adaptPlaywrightRun,
61
61
  adaptVitestRun: () => adaptVitestRun,
62
+ advanceState: () => advanceState,
62
63
  assertValidRun: () => assertValidRun,
63
64
  buildCheck: () => buildCheck,
64
65
  buildGoal: () => buildGoal,
@@ -71,6 +72,7 @@ __export(src_exports, {
71
72
  canonicalizeRun: () => canonicalizeRun,
72
73
  classifyStatusChange: () => classifyStatusChange,
73
74
  clearVersionCache: () => clearVersionCache,
75
+ computeDeltas: () => computeDeltas,
74
76
  computeTestMetrics: () => computeTestMetrics,
75
77
  copyMarkdownAssets: () => copyMarkdownAssets,
76
78
  createPrCommentSummary: () => createPrCommentSummary,
@@ -93,6 +95,7 @@ __export(src_exports, {
93
95
  getEnvironmentDrift: () => getEnvironmentDrift,
94
96
  gradeEvidence: () => gradeEvidence,
95
97
  hasSufficientHistory: () => hasSufficientHistory,
98
+ injectLiveBits: () => injectLiveBits,
96
99
  isReviewableSource: () => isReviewableSource,
97
100
  isTestFile: () => isTestFile,
98
101
  joinNameAndExt: () => joinNameAndExt,
@@ -114,7 +117,9 @@ __export(src_exports, {
114
117
  readPackageVersion: () => readPackageVersion,
115
118
  recordDeployment: () => recordDeployment,
116
119
  regenerateArtifacts: () => regenerateArtifacts,
120
+ regenerateRun: () => regenerateRun,
117
121
  renderCheck: () => renderCheck,
122
+ renderDeltaStrip: () => renderDeltaStrip,
118
123
  renderGoal: () => renderGoal,
119
124
  renderTriage: () => renderTriage,
120
125
  resolveAttachment: () => resolveAttachment,
@@ -130,6 +135,7 @@ __export(src_exports, {
130
135
  sendWebhookNotification: () => sendWebhookNotification,
131
136
  signBody: () => signBody,
132
137
  slugify: () => slugify,
138
+ startServe: () => startServe,
133
139
  startWatch: () => startWatch,
134
140
  stripAnsi: () => stripAnsi,
135
141
  toBehaviorManifest: () => toBehaviorManifest,
@@ -144,8 +150,8 @@ __export(src_exports, {
144
150
  validateCanonicalRun: () => validateCanonicalRun
145
151
  });
146
152
  module.exports = __toCommonJS(src_exports);
147
- var fs10 = require("fs");
148
- var path11 = __toESM(require("path"), 1);
153
+ var fs11 = require("fs");
154
+ var path12 = __toESM(require("path"), 1);
149
155
  var fsPromises = __toESM(require("fs/promises"), 1);
150
156
 
151
157
  // src/converters/acl/status.ts
@@ -187,6 +193,41 @@ function generateFeatureId(uri) {
187
193
  function generateScenarioId(featureId, scenarioName) {
188
194
  return `${featureId};${slugify(scenarioName)}`;
189
195
  }
196
+ function normalizeText(text2) {
197
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
198
+ }
199
+ function tokenize(text2) {
200
+ const normalized = normalizeText(text2);
201
+ return normalized.length === 0 ? [] : normalized.split(" ");
202
+ }
203
+ function behaviourFingerprint(input) {
204
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
205
+ const covers = [...input.covers ?? []].map((path13) => path13.trim()).filter(Boolean).sort().join(",");
206
+ if (steps.length === 0 && covers.length === 0) return "";
207
+ return (0, import_node_crypto.createHash)("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
208
+ }
209
+ function jaccard(a, b) {
210
+ if (a.size === 0 && b.size === 0) return 1;
211
+ if (a.size === 0 || b.size === 0) return 0;
212
+ let intersection = 0;
213
+ for (const value of a) if (b.has(value)) intersection += 1;
214
+ return intersection / (a.size + b.size - intersection);
215
+ }
216
+ function stepTokens(steps) {
217
+ const tokens = /* @__PURE__ */ new Set();
218
+ for (const step of steps) {
219
+ for (const token of tokenize(step.text)) tokens.add(token);
220
+ }
221
+ return tokens;
222
+ }
223
+ function behaviourSimilarity(a, b) {
224
+ const aSteps = stepTokens(a.steps);
225
+ const bSteps = stepTokens(b.steps);
226
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
227
+ const stepScore = jaccard(aSteps, bSteps);
228
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
229
+ return stepScore * 0.7 + titleScore * 0.3;
230
+ }
190
231
 
191
232
  // src/converters/acl/steps.ts
192
233
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -14384,12 +14425,12 @@ function hasSufficientHistory(entries, min) {
14384
14425
  }
14385
14426
 
14386
14427
  // src/formatters/html/renderers/scenario.ts
14387
- function renderTicket(ticket, template, escapeHtml4) {
14428
+ function renderTicket(ticket, template, escapeHtml5) {
14388
14429
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
14389
14430
  if (url) {
14390
- return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
14431
+ return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
14391
14432
  }
14392
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
14433
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
14393
14434
  }
14394
14435
  function renderScenario(args, deps) {
14395
14436
  const { tc } = args;
@@ -14589,7 +14630,7 @@ function flattenTree(roots) {
14589
14630
  }
14590
14631
  return result;
14591
14632
  }
14592
- function buildTooltip(span, escapeHtml4) {
14633
+ function buildTooltip(span, escapeHtml5) {
14593
14634
  const parts = [];
14594
14635
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
14595
14636
  if (span.statusMessage) {
@@ -14607,7 +14648,7 @@ function buildTooltip(span, escapeHtml4) {
14607
14648
  if (text2.length > TOOLTIP_MAX_LENGTH) {
14608
14649
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
14609
14650
  }
14610
- return escapeHtml4(text2);
14651
+ return escapeHtml5(text2);
14611
14652
  }
14612
14653
  function renderTraceView(args, deps) {
14613
14654
  if (!args.spans || args.spans.length === 0) return "";
@@ -15902,14 +15943,14 @@ var TraceabilityMatrixFormatter = class {
15902
15943
  lines.push("");
15903
15944
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
15904
15945
  if (req.covers.length > 0) {
15905
- lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
15946
+ lines.push(`Covers: ${req.covers.map((path13) => `\`${path13}\``).join(", ")}`);
15906
15947
  }
15907
15948
  lines.push("");
15908
15949
  lines.push("| Status | Scenario | Source | Covers |");
15909
15950
  lines.push("| --- | --- | --- | --- |");
15910
15951
  for (const scenario of req.scenarios) {
15911
15952
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15912
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
15953
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path13) => `\`${path13}\``).join(", ") : "";
15913
15954
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
15914
15955
  }
15915
15956
  lines.push("");
@@ -16011,8 +16052,8 @@ function extractFeatureName(testCases, uri) {
16011
16052
  return tc.titlePath[0];
16012
16053
  }
16013
16054
  }
16014
- const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16015
- return basename3.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16055
+ const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16056
+ return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16016
16057
  }
16017
16058
  function synthesizeFeature(uri, testCases) {
16018
16059
  const featureName = extractFeatureName(testCases, uri);
@@ -16624,8 +16665,8 @@ function extractDocAttachments(step) {
16624
16665
  }
16625
16666
  return attachments;
16626
16667
  }
16627
- function guessMediaType(path12) {
16628
- const lower = path12.toLowerCase();
16668
+ function guessMediaType(path13) {
16669
+ const lower = path13.toLowerCase();
16629
16670
  if (lower.endsWith(".png")) return "image/png";
16630
16671
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16631
16672
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16766,11 +16807,11 @@ var CucumberHtmlFormatter = class {
16766
16807
  for (const envelope of envelopes) {
16767
16808
  const accepted = htmlStream.write(envelope);
16768
16809
  if (!accepted) {
16769
- await new Promise((resolve9) => htmlStream.once("drain", resolve9));
16810
+ await new Promise((resolve10) => htmlStream.once("drain", resolve10));
16770
16811
  }
16771
16812
  }
16772
- await new Promise((resolve9, reject) => {
16773
- collector.on("finish", resolve9);
16813
+ await new Promise((resolve10, reject) => {
16814
+ collector.on("finish", resolve10);
16774
16815
  collector.on("error", reject);
16775
16816
  htmlStream.end();
16776
16817
  });
@@ -16808,6 +16849,10 @@ function titleFor(kind) {
16808
16849
  return "Added";
16809
16850
  case "removed":
16810
16851
  return "Removed";
16852
+ case "renamed":
16853
+ return "Renamed";
16854
+ case "moved":
16855
+ return "Moved";
16811
16856
  case "changed":
16812
16857
  return "Changed";
16813
16858
  default:
@@ -16856,6 +16901,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
16856
16901
  addSection(lines, diff, "fixed", maxScenarios);
16857
16902
  addSection(lines, diff, "added", maxScenarios);
16858
16903
  addSection(lines, diff, "removed", maxScenarios);
16904
+ addSection(lines, diff, "renamed", maxScenarios);
16905
+ addSection(lines, diff, "moved", maxScenarios);
16859
16906
  addSection(lines, diff, "changed", maxScenarios);
16860
16907
  return lines.join("\n").trimEnd();
16861
16908
  }
@@ -16902,8 +16949,10 @@ function sortDiffs(scenarios) {
16902
16949
  fixed: 1,
16903
16950
  added: 2,
16904
16951
  removed: 3,
16905
- changed: 4,
16906
- unchanged: 5
16952
+ renamed: 4,
16953
+ moved: 5,
16954
+ changed: 6,
16955
+ unchanged: 7
16907
16956
  };
16908
16957
  return [...scenarios].sort((a, b) => {
16909
16958
  if (rank[a.kind] !== rank[b.kind]) {
@@ -16918,69 +16967,108 @@ function sortDiffs(scenarios) {
16918
16967
  return a.scenario.localeCompare(b.scenario);
16919
16968
  });
16920
16969
  }
16970
+ var SIMILARITY_THRESHOLD = 0.75;
16971
+ function identityInput(tc) {
16972
+ return {
16973
+ scenario: tc.story.scenario,
16974
+ sourceFile: tc.sourceFile,
16975
+ steps: tc.story.steps,
16976
+ covers: tc.story.covers
16977
+ };
16978
+ }
16979
+ function changedFieldsOf(flags) {
16980
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16981
+ }
16982
+ function allChangedFlags(errorMessage) {
16983
+ return {
16984
+ status: true,
16985
+ steps: true,
16986
+ docs: true,
16987
+ tags: true,
16988
+ tickets: true,
16989
+ source: true,
16990
+ duration: true,
16991
+ attachments: true,
16992
+ error: Boolean(errorMessage),
16993
+ titlePath: true
16994
+ };
16995
+ }
16996
+ function matchIdentities(removed, added) {
16997
+ const pairs = [];
16998
+ const remainingRemoved = new Set(removed);
16999
+ const remainingAdded = new Set(added);
17000
+ const removedByFp = /* @__PURE__ */ new Map();
17001
+ const addedByFp = /* @__PURE__ */ new Map();
17002
+ for (const tc of remainingRemoved) {
17003
+ const fp = behaviourFingerprint(identityInput(tc));
17004
+ if (fp === "") continue;
17005
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
17006
+ }
17007
+ for (const tc of remainingAdded) {
17008
+ const fp = behaviourFingerprint(identityInput(tc));
17009
+ if (fp === "") continue;
17010
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
17011
+ }
17012
+ for (const [fp, removedGroup] of removedByFp) {
17013
+ const addedGroup = addedByFp.get(fp);
17014
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
17015
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
17016
+ remainingRemoved.delete(removedGroup[0]);
17017
+ remainingAdded.delete(addedGroup[0]);
17018
+ }
17019
+ }
17020
+ for (const before of [...remainingRemoved]) {
17021
+ let best;
17022
+ let bestScore = 0;
17023
+ let tied = false;
17024
+ for (const after of remainingAdded) {
17025
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
17026
+ if (score > bestScore) {
17027
+ bestScore = score;
17028
+ best = after;
17029
+ tied = false;
17030
+ } else if (score === bestScore) {
17031
+ tied = true;
17032
+ }
17033
+ }
17034
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
17035
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
17036
+ remainingRemoved.delete(before);
17037
+ remainingAdded.delete(best);
17038
+ }
17039
+ }
17040
+ return {
17041
+ pairs,
17042
+ unmatchedRemoved: [...remainingRemoved],
17043
+ unmatchedAdded: [...remainingAdded]
17044
+ };
17045
+ }
17046
+ function identityKind(before, after) {
17047
+ const titleChanged = before.story.scenario !== after.story.scenario;
17048
+ const fileChanged = before.sourceFile !== after.sourceFile;
17049
+ return fileChanged && !titleChanged ? "moved" : "renamed";
17050
+ }
16921
17051
  function diffRuns(baseline, current) {
16922
17052
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
16923
17053
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
16924
17054
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
16925
17055
  const scenarios = [];
17056
+ const removedCases = [];
17057
+ const addedCases = [];
16926
17058
  for (const id of ids) {
16927
17059
  const before = baselineById.get(id);
16928
17060
  const after = currentById.get(id);
16929
17061
  if (!before && after) {
16930
- const flags2 = {
16931
- status: true,
16932
- steps: true,
16933
- docs: true,
16934
- tags: true,
16935
- tickets: true,
16936
- source: true,
16937
- duration: true,
16938
- attachments: true,
16939
- error: Boolean(after.errorMessage),
16940
- titlePath: true
16941
- };
16942
- scenarios.push({
16943
- kind: "added",
16944
- id,
16945
- scenario: after.story.scenario,
16946
- sourceFile: after.sourceFile,
16947
- sourceLine: after.sourceLine,
16948
- current: toScenarioSnapshot(after),
16949
- flags: flags2,
16950
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16951
- });
17062
+ addedCases.push(after);
16952
17063
  continue;
16953
17064
  }
16954
17065
  if (before && !after) {
16955
- const flags2 = {
16956
- status: true,
16957
- steps: true,
16958
- docs: true,
16959
- tags: true,
16960
- tickets: true,
16961
- source: true,
16962
- duration: true,
16963
- attachments: true,
16964
- error: Boolean(before.errorMessage),
16965
- titlePath: true
16966
- };
16967
- scenarios.push({
16968
- kind: "removed",
16969
- id,
16970
- scenario: before.story.scenario,
16971
- sourceFile: before.sourceFile,
16972
- sourceLine: before.sourceLine,
16973
- baseline: toScenarioSnapshot(before),
16974
- flags: flags2,
16975
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16976
- });
16977
- continue;
16978
- }
16979
- if (!before || !after) {
17066
+ removedCases.push(before);
16980
17067
  continue;
16981
17068
  }
17069
+ if (!before || !after) continue;
16982
17070
  const flags = buildFlags(before, after);
16983
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17071
+ const changedFields = changedFieldsOf(flags);
16984
17072
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
16985
17073
  scenarios.push({
16986
17074
  kind,
@@ -16995,12 +17083,59 @@ function diffRuns(baseline, current) {
16995
17083
  durationDeltaMs: after.durationMs - before.durationMs
16996
17084
  });
16997
17085
  }
17086
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
17087
+ for (const { before, after, confidence, matchedBy } of pairs) {
17088
+ const flags = buildFlags(before, after);
17089
+ scenarios.push({
17090
+ kind: identityKind(before, after),
17091
+ id: after.id,
17092
+ previousId: before.id,
17093
+ scenario: after.story.scenario,
17094
+ sourceFile: after.sourceFile,
17095
+ sourceLine: after.sourceLine,
17096
+ baseline: toScenarioSnapshot(before),
17097
+ current: toScenarioSnapshot(after),
17098
+ flags,
17099
+ changedFields: changedFieldsOf(flags),
17100
+ durationDeltaMs: after.durationMs - before.durationMs,
17101
+ matchConfidence: confidence,
17102
+ matchedBy
17103
+ });
17104
+ }
17105
+ for (const after of unmatchedAdded) {
17106
+ const flags = allChangedFlags(after.errorMessage);
17107
+ scenarios.push({
17108
+ kind: "added",
17109
+ id: after.id,
17110
+ scenario: after.story.scenario,
17111
+ sourceFile: after.sourceFile,
17112
+ sourceLine: after.sourceLine,
17113
+ current: toScenarioSnapshot(after),
17114
+ flags,
17115
+ changedFields: changedFieldsOf(flags)
17116
+ });
17117
+ }
17118
+ for (const before of unmatchedRemoved) {
17119
+ const flags = allChangedFlags(before.errorMessage);
17120
+ scenarios.push({
17121
+ kind: "removed",
17122
+ id: before.id,
17123
+ scenario: before.story.scenario,
17124
+ sourceFile: before.sourceFile,
17125
+ sourceLine: before.sourceLine,
17126
+ baseline: toScenarioSnapshot(before),
17127
+ flags,
17128
+ changedFields: changedFieldsOf(flags)
17129
+ });
17130
+ }
16998
17131
  const sorted = sortDiffs(scenarios);
16999
17132
  const summary = {
17000
17133
  totalBaseline: baseline.testCases.length,
17001
17134
  totalCurrent: current.testCases.length,
17002
17135
  added: sorted.filter((s) => s.kind === "added").length,
17003
17136
  removed: sorted.filter((s) => s.kind === "removed").length,
17137
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
17138
+ moved: sorted.filter((s) => s.kind === "moved").length,
17004
17139
  changed: sorted.filter((s) => s.kind === "changed").length,
17005
17140
  regressed: sorted.filter((s) => s.kind === "regressed").length,
17006
17141
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -17028,6 +17163,10 @@ function statusLabel(kind) {
17028
17163
  return "Added";
17029
17164
  case "removed":
17030
17165
  return "Removed";
17166
+ case "renamed":
17167
+ return "Renamed";
17168
+ case "moved":
17169
+ return "Moved";
17031
17170
  case "changed":
17032
17171
  return "Changed";
17033
17172
  default:
@@ -17353,6 +17492,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17353
17492
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17354
17493
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17355
17494
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
17495
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
17496
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17356
17497
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17357
17498
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17358
17499
  </section>
@@ -17413,6 +17554,10 @@ function formatStatus(kind) {
17413
17554
  return "Added";
17414
17555
  case "removed":
17415
17556
  return "Removed";
17557
+ case "renamed":
17558
+ return "Renamed";
17559
+ case "moved":
17560
+ return "Moved";
17416
17561
  case "changed":
17417
17562
  return "Changed";
17418
17563
  default:
@@ -17565,13 +17710,13 @@ var RunDiffMarkdownFormatter = class {
17565
17710
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
17566
17711
  }
17567
17712
  lines.push("");
17568
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
17569
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
17713
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
17714
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
17570
17715
  lines.push(
17571
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17716
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17572
17717
  );
17573
17718
  lines.push("");
17574
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
17719
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
17575
17720
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
17576
17721
  if (scenarios.length === 0) continue;
17577
17722
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -18830,14 +18975,14 @@ ${result.errors.join("\n")}`);
18830
18975
  }
18831
18976
 
18832
18977
  // src/coverage-index.ts
18833
- function normalizePath(path12) {
18834
- return path12.replace(/^\.\//, "");
18978
+ function normalizePath(path13) {
18979
+ return path13.replace(/^\.\//, "");
18835
18980
  }
18836
18981
  function scenariosCoveringPaths(index, paths) {
18837
18982
  const queries = paths.map(normalizePath);
18838
18983
  return index.scenarios.filter(
18839
18984
  (scenario) => scenario.covers.some(
18840
- (glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
18985
+ (glob) => queries.some((path13) => matchesPattern(normalizePath(glob), path13))
18841
18986
  )
18842
18987
  );
18843
18988
  }
@@ -18913,7 +19058,7 @@ function toRun(data, inputType, synthesize) {
18913
19058
  if (synthesize) raw = synthesizeStories(raw);
18914
19059
  return canonicalizeRun(raw);
18915
19060
  }
18916
- async function regenerateArtifacts(options, deps = {}) {
19061
+ async function regenerateRun(options, deps = {}) {
18917
19062
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18918
19063
  const data = JSON.parse(read(path7.resolve(options.input)));
18919
19064
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -18923,7 +19068,10 @@ async function regenerateArtifacts(options, deps = {}) {
18923
19068
  outputName: options.outputName
18924
19069
  });
18925
19070
  const result = await generator.generate(run);
18926
- return [...result.values()].flat();
19071
+ return { files: [...result.values()].flat(), run };
19072
+ }
19073
+ async function regenerateArtifacts(options, deps = {}) {
19074
+ return (await regenerateRun(options, deps)).files;
18927
19075
  }
18928
19076
  function startWatch(options, deps = {}) {
18929
19077
  const log = deps.log ?? ((message) => console.log(message));
@@ -18966,6 +19114,203 @@ function startWatch(options, deps = {}) {
18966
19114
  };
18967
19115
  }
18968
19116
 
19117
+ // src/serve.ts
19118
+ var fs7 = __toESM(require("fs"), 1);
19119
+ var http = __toESM(require("http"), 1);
19120
+ var path8 = __toESM(require("path"), 1);
19121
+ function advanceState(prev, run) {
19122
+ if (prev.sessionBaseline === null) {
19123
+ return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
19124
+ }
19125
+ return {
19126
+ sessionBaseline: prev.sessionBaseline,
19127
+ previous: prev.current,
19128
+ current: run,
19129
+ runCount: prev.runCount + 1
19130
+ };
19131
+ }
19132
+ function computeDeltas(state) {
19133
+ if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
19134
+ return { session: null, iteration: null };
19135
+ }
19136
+ return {
19137
+ session: diffRuns(state.sessionBaseline, state.current),
19138
+ iteration: state.previous ? diffRuns(state.previous, state.current) : null
19139
+ };
19140
+ }
19141
+ function pluralize(n, word) {
19142
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
19143
+ }
19144
+ function summarizeDiff(diff) {
19145
+ const s = diff.summary;
19146
+ const parts = [];
19147
+ if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
19148
+ if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
19149
+ if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
19150
+ if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
19151
+ const moved = s.renamed + s.moved;
19152
+ if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
19153
+ if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
19154
+ return parts.length > 0 ? parts.join(", ") : null;
19155
+ }
19156
+ function escapeHtml3(text2) {
19157
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19158
+ }
19159
+ function renderDeltaStrip(state) {
19160
+ if (state.current === null) return "";
19161
+ const { session, iteration } = computeDeltas(state);
19162
+ if (session === null) {
19163
+ const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
19164
+ return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
19165
+ }
19166
+ const sessionLine = summarizeDiff(session) ?? "no change yet";
19167
+ let detail = "";
19168
+ if (iteration) {
19169
+ const iterationLine = summarizeDiff(iteration);
19170
+ if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
19171
+ }
19172
+ return [
19173
+ `<div data-es-live="strip">`,
19174
+ `<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
19175
+ `<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
19176
+ detail,
19177
+ `</div>`
19178
+ ].join("");
19179
+ }
19180
+ var RELOAD_CLIENT = `<script data-es-live="client">
19181
+ (function () {
19182
+ try {
19183
+ var es = new EventSource("/__es_reload");
19184
+ es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
19185
+ } catch (err) { /* SSE unavailable: stay static */ }
19186
+ })();
19187
+ </script>`;
19188
+ var STRIP_STYLE = `<style data-es-live="style">
19189
+ [data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
19190
+ padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
19191
+ [data-es-live="strip"] strong{color:#7dd3fc}
19192
+ </style>`;
19193
+ function injectLiveBits(html, stripHtml) {
19194
+ let out = html;
19195
+ const bodyOpen = out.match(/<body[^>]*>/i);
19196
+ if (bodyOpen) {
19197
+ const at = bodyOpen.index + bodyOpen[0].length;
19198
+ out = out.slice(0, at) + stripHtml + out.slice(at);
19199
+ } else {
19200
+ out = stripHtml + out;
19201
+ }
19202
+ const tail = STRIP_STYLE + RELOAD_CLIENT;
19203
+ if (/<\/body>/i.test(out)) {
19204
+ out = out.replace(/<\/body>/i, tail + "</body>");
19205
+ } else {
19206
+ out += tail;
19207
+ }
19208
+ return out;
19209
+ }
19210
+ var CONTENT_TYPES = {
19211
+ ".html": "text/html; charset=utf-8",
19212
+ ".css": "text/css; charset=utf-8",
19213
+ ".js": "text/javascript; charset=utf-8",
19214
+ ".json": "application/json; charset=utf-8",
19215
+ ".svg": "image/svg+xml",
19216
+ ".png": "image/png"
19217
+ };
19218
+ function watchInputDir(filePath, listener) {
19219
+ const dir = path8.dirname(filePath);
19220
+ const base = path8.basename(filePath);
19221
+ fs7.mkdirSync(dir, { recursive: true });
19222
+ const watcher = fs7.watch(dir, (_event, changed) => {
19223
+ if (!changed || changed === base) listener();
19224
+ });
19225
+ return { close: () => watcher.close() };
19226
+ }
19227
+ function plainText(html) {
19228
+ return html.replace(/<[^>]+>/g, "").trim();
19229
+ }
19230
+ function startServe(options, deps = {}) {
19231
+ const log = deps.log ?? ((message) => console.log(message));
19232
+ const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
19233
+ const port = options.port ?? 4321;
19234
+ const host = options.host ?? "127.0.0.1";
19235
+ let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
19236
+ let htmlPath = null;
19237
+ let stripHtml = renderDeltaStrip(state);
19238
+ const clients = /* @__PURE__ */ new Set();
19239
+ const pushReload = () => {
19240
+ for (const res of clients) res.write("data: reload\n\n");
19241
+ };
19242
+ const handler = (req, res) => {
19243
+ const url = (req.url ?? "/").split("?")[0];
19244
+ if (url === "/__es_reload") {
19245
+ res.writeHead(200, {
19246
+ "Content-Type": "text/event-stream",
19247
+ "Cache-Control": "no-cache",
19248
+ Connection: "keep-alive"
19249
+ });
19250
+ res.write("retry: 1000\n\n");
19251
+ clients.add(res);
19252
+ req.on("close", () => clients.delete(res));
19253
+ return;
19254
+ }
19255
+ if (url === "/" || url === "/index.html") {
19256
+ const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
19257
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19258
+ res.end(injectLiveBits(html, stripHtml));
19259
+ return;
19260
+ }
19261
+ const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
19262
+ const filePath = path8.join(path8.resolve(options.outputDir), safe);
19263
+ if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
19264
+ const ext = path8.extname(filePath).toLowerCase();
19265
+ res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
19266
+ res.end(read(filePath));
19267
+ return;
19268
+ }
19269
+ res.writeHead(404, { "Content-Type": "text/plain" });
19270
+ res.end("Not found");
19271
+ };
19272
+ const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
19273
+ const watchOptions = {
19274
+ input: options.input,
19275
+ outputDir: options.outputDir,
19276
+ outputName: options.outputName,
19277
+ formats: options.formats,
19278
+ inputType: options.inputType ?? "raw",
19279
+ synthesize: options.synthesize !== false,
19280
+ debounceMs: options.debounceMs
19281
+ };
19282
+ const watchHandle = startWatch(watchOptions, {
19283
+ readFile: read,
19284
+ watch: deps.watch ?? watchInputDir,
19285
+ log: () => {
19286
+ },
19287
+ // serve emits its own per-run line below
19288
+ regenerate: async (input) => {
19289
+ if (!fs7.existsSync(path8.resolve(input))) return [];
19290
+ const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
19291
+ htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
19292
+ state = advanceState(state, run);
19293
+ stripHtml = renderDeltaStrip(state);
19294
+ log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
19295
+ pushReload();
19296
+ return files;
19297
+ }
19298
+ });
19299
+ server.listen(port, host);
19300
+ const address = server.address();
19301
+ const boundPort = typeof address === "object" && address ? address.port : port;
19302
+ log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
19303
+ return {
19304
+ port: boundPort,
19305
+ close: () => {
19306
+ watchHandle.close();
19307
+ for (const res of clients) res.end();
19308
+ clients.clear();
19309
+ server.close();
19310
+ }
19311
+ };
19312
+ }
19313
+
18969
19314
  // src/behavior-diff.ts
18970
19315
  function classifyStatusChange(baseline, current) {
18971
19316
  if (baseline === void 0) return "added";
@@ -19003,7 +19348,10 @@ function diffStoryReports(baseline, current) {
19003
19348
  };
19004
19349
  });
19005
19350
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19006
- for (const s of scenarios) summary[s.kind] += 1;
19351
+ for (const s of scenarios) {
19352
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19353
+ summary[s.kind] += 1;
19354
+ }
19007
19355
  return { schemaVersion: "1.0", summary, scenarios };
19008
19356
  }
19009
19357
 
@@ -19602,27 +19950,27 @@ function pickleStepArgumentToDocs(ps) {
19602
19950
  }
19603
19951
 
19604
19952
  // src/utils/git-info.ts
19605
- var fs7 = __toESM(require("fs"), 1);
19606
- var path8 = __toESM(require("path"), 1);
19953
+ var fs8 = __toESM(require("fs"), 1);
19954
+ var path9 = __toESM(require("path"), 1);
19607
19955
  function readGitSha(cwd = process.cwd()) {
19608
19956
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
19609
19957
  if (envSha) return envSha;
19610
19958
  const gitDir = findGitDir(cwd);
19611
19959
  if (!gitDir) return void 0;
19612
19960
  try {
19613
- const headPath = path8.join(gitDir, "HEAD");
19614
- const head = fs7.readFileSync(headPath, "utf8").trim();
19961
+ const headPath = path9.join(gitDir, "HEAD");
19962
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19615
19963
  if (!head.startsWith("ref:")) {
19616
19964
  return head;
19617
19965
  }
19618
19966
  const refPath = head.replace("ref:", "").trim();
19619
- const refFile = path8.join(gitDir, refPath);
19620
- if (fs7.existsSync(refFile)) {
19621
- return fs7.readFileSync(refFile, "utf8").trim();
19967
+ const refFile = path9.join(gitDir, refPath);
19968
+ if (fs8.existsSync(refFile)) {
19969
+ return fs8.readFileSync(refFile, "utf8").trim();
19622
19970
  }
19623
- const packedRefs = path8.join(gitDir, "packed-refs");
19624
- if (fs7.existsSync(packedRefs)) {
19625
- const content = fs7.readFileSync(packedRefs, "utf8");
19971
+ const packedRefs = path9.join(gitDir, "packed-refs");
19972
+ if (fs8.existsSync(packedRefs)) {
19973
+ const content = fs8.readFileSync(packedRefs, "utf8");
19626
19974
  for (const line of content.split("\n")) {
19627
19975
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
19628
19976
  const [sha, ref] = line.split(" ");
@@ -19637,19 +19985,19 @@ function readGitSha(cwd = process.cwd()) {
19637
19985
  function findGitDir(start) {
19638
19986
  let current = start;
19639
19987
  while (true) {
19640
- const candidate = path8.join(current, ".git");
19641
- if (fs7.existsSync(candidate)) {
19642
- const stat = fs7.statSync(candidate);
19988
+ const candidate = path9.join(current, ".git");
19989
+ if (fs8.existsSync(candidate)) {
19990
+ const stat = fs8.statSync(candidate);
19643
19991
  if (stat.isFile()) {
19644
- const content = fs7.readFileSync(candidate, "utf8").trim();
19992
+ const content = fs8.readFileSync(candidate, "utf8").trim();
19645
19993
  const match = content.match(/^gitdir: (.+)$/);
19646
19994
  if (match) {
19647
- return path8.resolve(current, match[1]);
19995
+ return path9.resolve(current, match[1]);
19648
19996
  }
19649
19997
  }
19650
19998
  return candidate;
19651
19999
  }
19652
- const parent = path8.dirname(current);
20000
+ const parent = path9.dirname(current);
19653
20001
  if (parent === current) return void 0;
19654
20002
  current = parent;
19655
20003
  }
@@ -19660,8 +20008,8 @@ function readBranchName(cwd = process.cwd()) {
19660
20008
  const gitDir = findGitDir(cwd);
19661
20009
  if (!gitDir) return void 0;
19662
20010
  try {
19663
- const headPath = path8.join(gitDir, "HEAD");
19664
- const head = fs7.readFileSync(headPath, "utf8").trim();
20011
+ const headPath = path9.join(gitDir, "HEAD");
20012
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19665
20013
  if (head.startsWith("ref:")) {
19666
20014
  const refPath = head.replace("ref:", "").trim();
19667
20015
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -19698,8 +20046,8 @@ function nanosecondsToMs(ns) {
19698
20046
  }
19699
20047
 
19700
20048
  // src/utils/metadata.ts
19701
- var fs8 = __toESM(require("fs"), 1);
19702
- var path9 = __toESM(require("path"), 1);
20049
+ var fs9 = __toESM(require("fs"), 1);
20050
+ var path10 = __toESM(require("path"), 1);
19703
20051
  var versionCache = /* @__PURE__ */ new Map();
19704
20052
  function readPackageVersion(root) {
19705
20053
  if (versionCache.has(root)) {
@@ -19710,18 +20058,18 @@ function readPackageVersion(root) {
19710
20058
  return version;
19711
20059
  }
19712
20060
  function findPackageVersion(startDir) {
19713
- let current = path9.resolve(startDir);
20061
+ let current = path10.resolve(startDir);
19714
20062
  while (true) {
19715
- const pkgPath = path9.join(current, "package.json");
20063
+ const pkgPath = path10.join(current, "package.json");
19716
20064
  try {
19717
- if (fs8.existsSync(pkgPath)) {
19718
- const raw = fs8.readFileSync(pkgPath, "utf8");
20065
+ if (fs9.existsSync(pkgPath)) {
20066
+ const raw = fs9.readFileSync(pkgPath, "utf8");
19719
20067
  const parsed = JSON.parse(raw);
19720
20068
  return parsed.version;
19721
20069
  }
19722
20070
  } catch {
19723
20071
  }
19724
- const parent = path9.dirname(current);
20072
+ const parent = path10.dirname(current);
19725
20073
  if (parent === current) {
19726
20074
  return void 0;
19727
20075
  }
@@ -21050,18 +21398,18 @@ function deriveChangeType(tags) {
21050
21398
  }
21051
21399
  return "unknown";
21052
21400
  }
21053
- function extensionOf(path12) {
21054
- const base = path12.split("/").pop() ?? path12;
21401
+ function extensionOf(path13) {
21402
+ const base = path13.split("/").pop() ?? path13;
21055
21403
  const dot = base.lastIndexOf(".");
21056
21404
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
21057
21405
  }
21058
- function isTestFile(path12) {
21059
- return TEST_INFIX.test(path12);
21406
+ function isTestFile(path13) {
21407
+ return TEST_INFIX.test(path13);
21060
21408
  }
21061
- function isReviewableSource(path12) {
21062
- if (isTestFile(path12)) return false;
21063
- if (path12.endsWith(".d.ts")) return false;
21064
- return CODE_EXTENSIONS.has(extensionOf(path12));
21409
+ function isReviewableSource(path13) {
21410
+ if (isTestFile(path13)) return false;
21411
+ if (path13.endsWith(".d.ts")) return false;
21412
+ return CODE_EXTENSIONS.has(extensionOf(path13));
21065
21413
  }
21066
21414
  function testBaseKey(testFile) {
21067
21415
  return testFile.replace(TEST_INFIX, "");
@@ -21165,7 +21513,7 @@ function toClaim(testCase, changedSourcePaths) {
21165
21513
  const { strength, reasons } = gradeEvidence(testCase, audience);
21166
21514
  const key = testBaseKey(testCase.sourceFile);
21167
21515
  const coversFiles = changedSourcePaths.filter(
21168
- (path12) => sourceBaseKey(path12) === key
21516
+ (path13) => sourceBaseKey(path13) === key
21169
21517
  );
21170
21518
  return {
21171
21519
  id: testCase.id,
@@ -21414,7 +21762,7 @@ var ReviewMarkdownFormatter = class {
21414
21762
  };
21415
21763
 
21416
21764
  // src/formatters/review-html.ts
21417
- function escapeHtml3(value) {
21765
+ function escapeHtml4(value) {
21418
21766
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21419
21767
  }
21420
21768
  var STRENGTH_LABEL = {
@@ -21436,22 +21784,22 @@ function statusIcon3(status) {
21436
21784
  }
21437
21785
  }
21438
21786
  function formatStep3(step) {
21439
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21787
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21440
21788
  }
21441
21789
  function inlineDoc(doc) {
21442
21790
  switch (doc.kind) {
21443
21791
  case "note":
21444
- return escapeHtml3(doc.text);
21792
+ return escapeHtml4(doc.text);
21445
21793
  case "section":
21446
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21794
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21447
21795
  case "kv":
21448
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21796
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21449
21797
  case "code":
21450
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21798
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21451
21799
  case "link":
21452
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21800
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21453
21801
  default:
21454
- return escapeHtml3(doc.kind);
21802
+ return escapeHtml4(doc.kind);
21455
21803
  }
21456
21804
  }
21457
21805
  function renderEvidenceArtifacts(testCase) {
@@ -21459,7 +21807,7 @@ function renderEvidenceArtifacts(testCase) {
21459
21807
  for (const att of testCase.attachments) {
21460
21808
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21461
21809
  parts.push(
21462
- `<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
21810
+ `<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
21463
21811
  );
21464
21812
  }
21465
21813
  }
@@ -21474,22 +21822,22 @@ function renderTicketPills(claim) {
21474
21822
  const tickets = claim.testCase.story.tickets ?? [];
21475
21823
  if (tickets.length === 0) return "";
21476
21824
  return `<div class="ticket-row">${tickets.map((ticket) => {
21477
- const label = escapeHtml3(ticket.id);
21825
+ const label = escapeHtml4(ticket.id);
21478
21826
  if (ticket.url) {
21479
- return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21827
+ return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21480
21828
  }
21481
21829
  return `<span class="ticket-pill">${label}</span>`;
21482
21830
  }).join("")}</div>`;
21483
21831
  }
21484
21832
  function renderClaimCard(claim) {
21485
21833
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21486
- const search = escapeHtml3(
21834
+ const search = escapeHtml4(
21487
21835
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21488
21836
  ).toLowerCase();
21489
21837
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21490
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21491
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21492
- const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
21838
+ const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
21839
+ const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
21840
+ const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
21493
21841
  const docs = (claim.testCase.story.docs ?? []).filter(
21494
21842
  (d) => d.kind === "section" || d.kind === "note"
21495
21843
  );
@@ -21499,9 +21847,9 @@ function renderClaimCard(claim) {
21499
21847
  <header class="claim-header">
21500
21848
  <div>
21501
21849
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21502
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21503
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21504
- <p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21850
+ ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
21851
+ <h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
21852
+ <p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21505
21853
  ${renderTicketPills(claim)}
21506
21854
  </div>
21507
21855
  </header>
@@ -21516,18 +21864,18 @@ function renderClaimCard(claim) {
21516
21864
  </article>`;
21517
21865
  }
21518
21866
  function renderChangedFileRow(file) {
21519
- const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21867
+ const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21520
21868
  return `<tr data-band="${file.band}">
21521
21869
  <td><span class="band-dot band-${file.band}"></span></td>
21522
- <td><code>${escapeHtml3(file.path)}</code></td>
21523
- <td>${escapeHtml3(file.changeKind)}</td>
21870
+ <td><code>${escapeHtml4(file.path)}</code></td>
21871
+ <td>${escapeHtml4(file.changeKind)}</td>
21524
21872
  <td>${claims}</td>
21525
21873
  </tr>`;
21526
21874
  }
21527
21875
  function renderAudienceSection2(title, claims) {
21528
21876
  if (claims.length === 0) return "";
21529
21877
  return `<section class="audience-section">
21530
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21878
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21531
21879
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21532
21880
  </section>`;
21533
21881
  }
@@ -21617,13 +21965,13 @@ var ReviewHtmlFormatter = class {
21617
21965
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21618
21966
  applyTheme(getEffectiveTheme());` : "";
21619
21967
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21620
- const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
21968
+ const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
21621
21969
  return `<!doctype html>
21622
21970
  <html lang="en"${themeAttr}>
21623
21971
  <head>
21624
21972
  <meta charset="utf-8" />
21625
21973
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21626
- <title>${escapeHtml3(this.title)}</title>
21974
+ <title>${escapeHtml4(this.title)}</title>
21627
21975
  <style>
21628
21976
  ${this.theme.css}
21629
21977
  ${REVIEW_CSS}
@@ -21633,7 +21981,7 @@ applyTheme(getEffectiveTheme());` : "";
21633
21981
  <main>
21634
21982
  <div class="hero-card card">
21635
21983
  <div class="review-header">
21636
- <h1>${escapeHtml3(this.title)}</h1>
21984
+ <h1>${escapeHtml4(this.title)}</h1>
21637
21985
  ${themeToggleHtml}
21638
21986
  </div>
21639
21987
  ${refsLine}
@@ -21648,7 +21996,7 @@ applyTheme(getEffectiveTheme());` : "";
21648
21996
  </section>
21649
21997
  <section class="card priority-banner">
21650
21998
  <h2>Review priority</h2>
21651
- <p class="subtle">${escapeHtml3(priority)}</p>
21999
+ <p class="subtle">${escapeHtml4(priority)}</p>
21652
22000
  </section>
21653
22001
  ${changedFilesPanel}
21654
22002
  <section class="toolbar">
@@ -21696,8 +22044,8 @@ applyTheme(getEffectiveTheme());` : "";
21696
22044
  };
21697
22045
 
21698
22046
  // src/deploy/ledger.ts
21699
- var fs9 = __toESM(require("fs"), 1);
21700
- var path10 = __toESM(require("path"), 1);
22047
+ var fs10 = __toESM(require("fs"), 1);
22048
+ var path11 = __toESM(require("path"), 1);
21701
22049
  function createEmptyLedger() {
21702
22050
  return {
21703
22051
  deployments: [],
@@ -21705,12 +22053,12 @@ function createEmptyLedger() {
21705
22053
  };
21706
22054
  }
21707
22055
  function loadLedger(ledgerPath) {
21708
- const resolved = path10.resolve(ledgerPath);
21709
- if (!fs9.existsSync(resolved)) {
22056
+ const resolved = path11.resolve(ledgerPath);
22057
+ if (!fs10.existsSync(resolved)) {
21710
22058
  return createEmptyLedger();
21711
22059
  }
21712
22060
  try {
21713
- const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
22061
+ const raw = JSON.parse(fs10.readFileSync(resolved, "utf8"));
21714
22062
  if (raw.schemaVersion !== 1) {
21715
22063
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21716
22064
  }
@@ -21721,10 +22069,10 @@ function loadLedger(ledgerPath) {
21721
22069
  }
21722
22070
  }
21723
22071
  function saveLedger(ledger, ledgerPath) {
21724
- const resolved = path10.resolve(ledgerPath);
21725
- const dir = path10.dirname(resolved);
21726
- fs9.mkdirSync(dir, { recursive: true });
21727
- fs9.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
22072
+ const resolved = path11.resolve(ledgerPath);
22073
+ const dir = path11.dirname(resolved);
22074
+ fs10.mkdirSync(dir, { recursive: true });
22075
+ fs10.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21728
22076
  }
21729
22077
  function getLatestDeployment(ledger, environment) {
21730
22078
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21844,11 +22192,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21844
22192
  const ext = FORMAT_EXTENSIONS[format];
21845
22193
  const effectiveName = outputName + (outputNameSuffix ?? "");
21846
22194
  if (mode === "aggregated") {
21847
- return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
22195
+ return toPosix(path12.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21848
22196
  }
21849
22197
  const normalizedSource = toPosix(sourceFile);
21850
- const dirOfSource = path11.posix.dirname(normalizedSource);
21851
- let baseName = path11.posix.basename(normalizedSource);
22198
+ const dirOfSource = path12.posix.dirname(normalizedSource);
22199
+ let baseName = path12.posix.basename(normalizedSource);
21852
22200
  for (const testExt of TEST_EXTENSIONS) {
21853
22201
  if (baseName.endsWith(testExt)) {
21854
22202
  baseName = baseName.slice(0, -testExt.length);
@@ -21857,12 +22205,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21857
22205
  }
21858
22206
  const fileName = `${baseName}.${effectiveName}${ext}`;
21859
22207
  if (colocatedStyle === "adjacent") {
21860
- return toPosix(path11.posix.join(dirOfSource, fileName));
22208
+ return toPosix(path12.posix.join(dirOfSource, fileName));
21861
22209
  }
21862
22210
  if (colocatedStyle === "flat") {
21863
- return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
22211
+ return toPosix(path12.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21864
22212
  }
21865
- return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
22213
+ return toPosix(path12.posix.join(baseOutputDir, dirOfSource, fileName));
21866
22214
  }
21867
22215
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
21868
22216
  const groups = /* @__PURE__ */ new Map();
@@ -22075,8 +22423,8 @@ var ReportGenerator = class {
22075
22423
  if (astroPaths) {
22076
22424
  for (const mdPath of astroPaths) {
22077
22425
  const content = await fsPromises.readFile(mdPath, "utf8");
22078
- const mdDir = path11.dirname(mdPath);
22079
- const assetsDir = path11.resolve(this.options.astro.assetsDir);
22426
+ const mdDir = path12.dirname(mdPath);
22427
+ const assetsDir = path12.resolve(this.options.astro.assetsDir);
22080
22428
  const result = copyMarkdownAssets({
22081
22429
  markdown: content,
22082
22430
  markdownDir: mdDir,
@@ -22107,9 +22455,9 @@ var ReportGenerator = class {
22107
22455
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
22108
22456
  const ext = FORMAT_EXTENSIONS[format];
22109
22457
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
22110
- const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22458
+ const outputPath = toPosix(path12.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22111
22459
  const content = await this.formatContent(run, format);
22112
- const dir = path11.dirname(outputPath);
22460
+ const dir = path12.dirname(outputPath);
22113
22461
  await fsPromises.mkdir(dir, { recursive: true });
22114
22462
  await this.deps.writeFile(outputPath, content);
22115
22463
  return [outputPath];
@@ -22121,7 +22469,7 @@ var ReportGenerator = class {
22121
22469
  testCases
22122
22470
  };
22123
22471
  const content = await this.formatContent(groupRun, format);
22124
- const dir = path11.dirname(outputPath);
22472
+ const dir = path12.dirname(outputPath);
22125
22473
  await fsPromises.mkdir(dir, { recursive: true });
22126
22474
  await this.deps.writeFile(outputPath, content);
22127
22475
  writtenPaths.push(outputPath);
@@ -22273,7 +22621,7 @@ async function generateRunComparison(args) {
22273
22621
  await fsPromises.mkdir(outputDir, { recursive: true });
22274
22622
  for (const format of args.formats) {
22275
22623
  const ext = format === "html" ? ".html" : ".md";
22276
- const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
22624
+ const outputPath = toPosix(path12.join(outputDir, `${outputName}${ext}`));
22277
22625
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
22278
22626
  await fsPromises.writeFile(outputPath, content, "utf8");
22279
22627
  files.push(outputPath);
@@ -22323,6 +22671,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22323
22671
  adaptJestRun,
22324
22672
  adaptPlaywrightRun,
22325
22673
  adaptVitestRun,
22674
+ advanceState,
22326
22675
  assertValidRun,
22327
22676
  buildCheck,
22328
22677
  buildGoal,
@@ -22335,6 +22684,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22335
22684
  canonicalizeRun,
22336
22685
  classifyStatusChange,
22337
22686
  clearVersionCache,
22687
+ computeDeltas,
22338
22688
  computeTestMetrics,
22339
22689
  copyMarkdownAssets,
22340
22690
  createPrCommentSummary,
@@ -22357,6 +22707,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22357
22707
  getEnvironmentDrift,
22358
22708
  gradeEvidence,
22359
22709
  hasSufficientHistory,
22710
+ injectLiveBits,
22360
22711
  isReviewableSource,
22361
22712
  isTestFile,
22362
22713
  joinNameAndExt,
@@ -22378,7 +22729,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22378
22729
  readPackageVersion,
22379
22730
  recordDeployment,
22380
22731
  regenerateArtifacts,
22732
+ regenerateRun,
22381
22733
  renderCheck,
22734
+ renderDeltaStrip,
22382
22735
  renderGoal,
22383
22736
  renderTriage,
22384
22737
  resolveAttachment,
@@ -22394,6 +22747,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22394
22747
  sendWebhookNotification,
22395
22748
  signBody,
22396
22749
  slugify,
22750
+ startServe,
22397
22751
  startWatch,
22398
22752
  stripAnsi,
22399
22753
  toBehaviorManifest,