executable-stories-formatters 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -196,7 +202,7 @@ function tokenize(text2) {
196
202
  }
197
203
  function behaviourFingerprint(input) {
198
204
  const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
199
- const covers = [...input.covers ?? []].map((path12) => path12.trim()).filter(Boolean).sort().join(",");
205
+ const covers = [...input.covers ?? []].map((path13) => path13.trim()).filter(Boolean).sort().join(",");
200
206
  if (steps.length === 0 && covers.length === 0) return "";
201
207
  return (0, import_node_crypto.createHash)("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
202
208
  }
@@ -14419,12 +14425,12 @@ function hasSufficientHistory(entries, min) {
14419
14425
  }
14420
14426
 
14421
14427
  // src/formatters/html/renderers/scenario.ts
14422
- function renderTicket(ticket, template, escapeHtml4) {
14428
+ function renderTicket(ticket, template, escapeHtml5) {
14423
14429
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
14424
14430
  if (url) {
14425
- 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>`;
14426
14432
  }
14427
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
14433
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
14428
14434
  }
14429
14435
  function renderScenario(args, deps) {
14430
14436
  const { tc } = args;
@@ -14624,7 +14630,7 @@ function flattenTree(roots) {
14624
14630
  }
14625
14631
  return result;
14626
14632
  }
14627
- function buildTooltip(span, escapeHtml4) {
14633
+ function buildTooltip(span, escapeHtml5) {
14628
14634
  const parts = [];
14629
14635
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
14630
14636
  if (span.statusMessage) {
@@ -14642,7 +14648,7 @@ function buildTooltip(span, escapeHtml4) {
14642
14648
  if (text2.length > TOOLTIP_MAX_LENGTH) {
14643
14649
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
14644
14650
  }
14645
- return escapeHtml4(text2);
14651
+ return escapeHtml5(text2);
14646
14652
  }
14647
14653
  function renderTraceView(args, deps) {
14648
14654
  if (!args.spans || args.spans.length === 0) return "";
@@ -15937,14 +15943,14 @@ var TraceabilityMatrixFormatter = class {
15937
15943
  lines.push("");
15938
15944
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
15939
15945
  if (req.covers.length > 0) {
15940
- lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
15946
+ lines.push(`Covers: ${req.covers.map((path13) => `\`${path13}\``).join(", ")}`);
15941
15947
  }
15942
15948
  lines.push("");
15943
15949
  lines.push("| Status | Scenario | Source | Covers |");
15944
15950
  lines.push("| --- | --- | --- | --- |");
15945
15951
  for (const scenario of req.scenarios) {
15946
15952
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15947
- 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(", ") : "";
15948
15954
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
15949
15955
  }
15950
15956
  lines.push("");
@@ -16046,8 +16052,8 @@ function extractFeatureName(testCases, uri) {
16046
16052
  return tc.titlePath[0];
16047
16053
  }
16048
16054
  }
16049
- const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16050
- 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());
16051
16057
  }
16052
16058
  function synthesizeFeature(uri, testCases) {
16053
16059
  const featureName = extractFeatureName(testCases, uri);
@@ -16659,8 +16665,8 @@ function extractDocAttachments(step) {
16659
16665
  }
16660
16666
  return attachments;
16661
16667
  }
16662
- function guessMediaType(path12) {
16663
- const lower = path12.toLowerCase();
16668
+ function guessMediaType(path13) {
16669
+ const lower = path13.toLowerCase();
16664
16670
  if (lower.endsWith(".png")) return "image/png";
16665
16671
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16666
16672
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16801,11 +16807,11 @@ var CucumberHtmlFormatter = class {
16801
16807
  for (const envelope of envelopes) {
16802
16808
  const accepted = htmlStream.write(envelope);
16803
16809
  if (!accepted) {
16804
- await new Promise((resolve9) => htmlStream.once("drain", resolve9));
16810
+ await new Promise((resolve10) => htmlStream.once("drain", resolve10));
16805
16811
  }
16806
16812
  }
16807
- await new Promise((resolve9, reject) => {
16808
- collector.on("finish", resolve9);
16813
+ await new Promise((resolve10, reject) => {
16814
+ collector.on("finish", resolve10);
16809
16815
  collector.on("error", reject);
16810
16816
  htmlStream.end();
16811
16817
  });
@@ -18969,14 +18975,14 @@ ${result.errors.join("\n")}`);
18969
18975
  }
18970
18976
 
18971
18977
  // src/coverage-index.ts
18972
- function normalizePath(path12) {
18973
- return path12.replace(/^\.\//, "");
18978
+ function normalizePath(path13) {
18979
+ return path13.replace(/^\.\//, "");
18974
18980
  }
18975
18981
  function scenariosCoveringPaths(index, paths) {
18976
18982
  const queries = paths.map(normalizePath);
18977
18983
  return index.scenarios.filter(
18978
18984
  (scenario) => scenario.covers.some(
18979
- (glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
18985
+ (glob) => queries.some((path13) => matchesPattern(normalizePath(glob), path13))
18980
18986
  )
18981
18987
  );
18982
18988
  }
@@ -19052,7 +19058,7 @@ function toRun(data, inputType, synthesize) {
19052
19058
  if (synthesize) raw = synthesizeStories(raw);
19053
19059
  return canonicalizeRun(raw);
19054
19060
  }
19055
- async function regenerateArtifacts(options, deps = {}) {
19061
+ async function regenerateRun(options, deps = {}) {
19056
19062
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
19057
19063
  const data = JSON.parse(read(path7.resolve(options.input)));
19058
19064
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -19062,7 +19068,10 @@ async function regenerateArtifacts(options, deps = {}) {
19062
19068
  outputName: options.outputName
19063
19069
  });
19064
19070
  const result = await generator.generate(run);
19065
- 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;
19066
19075
  }
19067
19076
  function startWatch(options, deps = {}) {
19068
19077
  const log = deps.log ?? ((message) => console.log(message));
@@ -19105,6 +19114,203 @@ function startWatch(options, deps = {}) {
19105
19114
  };
19106
19115
  }
19107
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
+
19108
19314
  // src/behavior-diff.ts
19109
19315
  function classifyStatusChange(baseline, current) {
19110
19316
  if (baseline === void 0) return "added";
@@ -19744,27 +19950,27 @@ function pickleStepArgumentToDocs(ps) {
19744
19950
  }
19745
19951
 
19746
19952
  // src/utils/git-info.ts
19747
- var fs7 = __toESM(require("fs"), 1);
19748
- var path8 = __toESM(require("path"), 1);
19953
+ var fs8 = __toESM(require("fs"), 1);
19954
+ var path9 = __toESM(require("path"), 1);
19749
19955
  function readGitSha(cwd = process.cwd()) {
19750
19956
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
19751
19957
  if (envSha) return envSha;
19752
19958
  const gitDir = findGitDir(cwd);
19753
19959
  if (!gitDir) return void 0;
19754
19960
  try {
19755
- const headPath = path8.join(gitDir, "HEAD");
19756
- const head = fs7.readFileSync(headPath, "utf8").trim();
19961
+ const headPath = path9.join(gitDir, "HEAD");
19962
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19757
19963
  if (!head.startsWith("ref:")) {
19758
19964
  return head;
19759
19965
  }
19760
19966
  const refPath = head.replace("ref:", "").trim();
19761
- const refFile = path8.join(gitDir, refPath);
19762
- if (fs7.existsSync(refFile)) {
19763
- 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();
19764
19970
  }
19765
- const packedRefs = path8.join(gitDir, "packed-refs");
19766
- if (fs7.existsSync(packedRefs)) {
19767
- 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");
19768
19974
  for (const line of content.split("\n")) {
19769
19975
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
19770
19976
  const [sha, ref] = line.split(" ");
@@ -19779,19 +19985,19 @@ function readGitSha(cwd = process.cwd()) {
19779
19985
  function findGitDir(start) {
19780
19986
  let current = start;
19781
19987
  while (true) {
19782
- const candidate = path8.join(current, ".git");
19783
- if (fs7.existsSync(candidate)) {
19784
- const stat = fs7.statSync(candidate);
19988
+ const candidate = path9.join(current, ".git");
19989
+ if (fs8.existsSync(candidate)) {
19990
+ const stat = fs8.statSync(candidate);
19785
19991
  if (stat.isFile()) {
19786
- const content = fs7.readFileSync(candidate, "utf8").trim();
19992
+ const content = fs8.readFileSync(candidate, "utf8").trim();
19787
19993
  const match = content.match(/^gitdir: (.+)$/);
19788
19994
  if (match) {
19789
- return path8.resolve(current, match[1]);
19995
+ return path9.resolve(current, match[1]);
19790
19996
  }
19791
19997
  }
19792
19998
  return candidate;
19793
19999
  }
19794
- const parent = path8.dirname(current);
20000
+ const parent = path9.dirname(current);
19795
20001
  if (parent === current) return void 0;
19796
20002
  current = parent;
19797
20003
  }
@@ -19802,8 +20008,8 @@ function readBranchName(cwd = process.cwd()) {
19802
20008
  const gitDir = findGitDir(cwd);
19803
20009
  if (!gitDir) return void 0;
19804
20010
  try {
19805
- const headPath = path8.join(gitDir, "HEAD");
19806
- const head = fs7.readFileSync(headPath, "utf8").trim();
20011
+ const headPath = path9.join(gitDir, "HEAD");
20012
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19807
20013
  if (head.startsWith("ref:")) {
19808
20014
  const refPath = head.replace("ref:", "").trim();
19809
20015
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -19840,8 +20046,8 @@ function nanosecondsToMs(ns) {
19840
20046
  }
19841
20047
 
19842
20048
  // src/utils/metadata.ts
19843
- var fs8 = __toESM(require("fs"), 1);
19844
- var path9 = __toESM(require("path"), 1);
20049
+ var fs9 = __toESM(require("fs"), 1);
20050
+ var path10 = __toESM(require("path"), 1);
19845
20051
  var versionCache = /* @__PURE__ */ new Map();
19846
20052
  function readPackageVersion(root) {
19847
20053
  if (versionCache.has(root)) {
@@ -19852,18 +20058,18 @@ function readPackageVersion(root) {
19852
20058
  return version;
19853
20059
  }
19854
20060
  function findPackageVersion(startDir) {
19855
- let current = path9.resolve(startDir);
20061
+ let current = path10.resolve(startDir);
19856
20062
  while (true) {
19857
- const pkgPath = path9.join(current, "package.json");
20063
+ const pkgPath = path10.join(current, "package.json");
19858
20064
  try {
19859
- if (fs8.existsSync(pkgPath)) {
19860
- const raw = fs8.readFileSync(pkgPath, "utf8");
20065
+ if (fs9.existsSync(pkgPath)) {
20066
+ const raw = fs9.readFileSync(pkgPath, "utf8");
19861
20067
  const parsed = JSON.parse(raw);
19862
20068
  return parsed.version;
19863
20069
  }
19864
20070
  } catch {
19865
20071
  }
19866
- const parent = path9.dirname(current);
20072
+ const parent = path10.dirname(current);
19867
20073
  if (parent === current) {
19868
20074
  return void 0;
19869
20075
  }
@@ -21192,18 +21398,18 @@ function deriveChangeType(tags) {
21192
21398
  }
21193
21399
  return "unknown";
21194
21400
  }
21195
- function extensionOf(path12) {
21196
- const base = path12.split("/").pop() ?? path12;
21401
+ function extensionOf(path13) {
21402
+ const base = path13.split("/").pop() ?? path13;
21197
21403
  const dot = base.lastIndexOf(".");
21198
21404
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
21199
21405
  }
21200
- function isTestFile(path12) {
21201
- return TEST_INFIX.test(path12);
21406
+ function isTestFile(path13) {
21407
+ return TEST_INFIX.test(path13);
21202
21408
  }
21203
- function isReviewableSource(path12) {
21204
- if (isTestFile(path12)) return false;
21205
- if (path12.endsWith(".d.ts")) return false;
21206
- 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));
21207
21413
  }
21208
21414
  function testBaseKey(testFile) {
21209
21415
  return testFile.replace(TEST_INFIX, "");
@@ -21307,7 +21513,7 @@ function toClaim(testCase, changedSourcePaths) {
21307
21513
  const { strength, reasons } = gradeEvidence(testCase, audience);
21308
21514
  const key = testBaseKey(testCase.sourceFile);
21309
21515
  const coversFiles = changedSourcePaths.filter(
21310
- (path12) => sourceBaseKey(path12) === key
21516
+ (path13) => sourceBaseKey(path13) === key
21311
21517
  );
21312
21518
  return {
21313
21519
  id: testCase.id,
@@ -21556,7 +21762,7 @@ var ReviewMarkdownFormatter = class {
21556
21762
  };
21557
21763
 
21558
21764
  // src/formatters/review-html.ts
21559
- function escapeHtml3(value) {
21765
+ function escapeHtml4(value) {
21560
21766
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21561
21767
  }
21562
21768
  var STRENGTH_LABEL = {
@@ -21578,22 +21784,22 @@ function statusIcon3(status) {
21578
21784
  }
21579
21785
  }
21580
21786
  function formatStep3(step) {
21581
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21787
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21582
21788
  }
21583
21789
  function inlineDoc(doc) {
21584
21790
  switch (doc.kind) {
21585
21791
  case "note":
21586
- return escapeHtml3(doc.text);
21792
+ return escapeHtml4(doc.text);
21587
21793
  case "section":
21588
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21794
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21589
21795
  case "kv":
21590
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21796
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21591
21797
  case "code":
21592
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21798
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21593
21799
  case "link":
21594
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21800
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21595
21801
  default:
21596
- return escapeHtml3(doc.kind);
21802
+ return escapeHtml4(doc.kind);
21597
21803
  }
21598
21804
  }
21599
21805
  function renderEvidenceArtifacts(testCase) {
@@ -21601,7 +21807,7 @@ function renderEvidenceArtifacts(testCase) {
21601
21807
  for (const att of testCase.attachments) {
21602
21808
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21603
21809
  parts.push(
21604
- `<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}" />`
21605
21811
  );
21606
21812
  }
21607
21813
  }
@@ -21616,22 +21822,22 @@ function renderTicketPills(claim) {
21616
21822
  const tickets = claim.testCase.story.tickets ?? [];
21617
21823
  if (tickets.length === 0) return "";
21618
21824
  return `<div class="ticket-row">${tickets.map((ticket) => {
21619
- const label = escapeHtml3(ticket.id);
21825
+ const label = escapeHtml4(ticket.id);
21620
21826
  if (ticket.url) {
21621
- 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>`;
21622
21828
  }
21623
21829
  return `<span class="ticket-pill">${label}</span>`;
21624
21830
  }).join("")}</div>`;
21625
21831
  }
21626
21832
  function renderClaimCard(claim) {
21627
21833
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21628
- const search = escapeHtml3(
21834
+ const search = escapeHtml4(
21629
21835
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21630
21836
  ).toLowerCase();
21631
21837
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21632
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21633
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21634
- 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>` : "";
21635
21841
  const docs = (claim.testCase.story.docs ?? []).filter(
21636
21842
  (d) => d.kind === "section" || d.kind === "note"
21637
21843
  );
@@ -21641,9 +21847,9 @@ function renderClaimCard(claim) {
21641
21847
  <header class="claim-header">
21642
21848
  <div>
21643
21849
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21644
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21645
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21646
- <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>
21647
21853
  ${renderTicketPills(claim)}
21648
21854
  </div>
21649
21855
  </header>
@@ -21658,18 +21864,18 @@ function renderClaimCard(claim) {
21658
21864
  </article>`;
21659
21865
  }
21660
21866
  function renderChangedFileRow(file) {
21661
- 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";
21662
21868
  return `<tr data-band="${file.band}">
21663
21869
  <td><span class="band-dot band-${file.band}"></span></td>
21664
- <td><code>${escapeHtml3(file.path)}</code></td>
21665
- <td>${escapeHtml3(file.changeKind)}</td>
21870
+ <td><code>${escapeHtml4(file.path)}</code></td>
21871
+ <td>${escapeHtml4(file.changeKind)}</td>
21666
21872
  <td>${claims}</td>
21667
21873
  </tr>`;
21668
21874
  }
21669
21875
  function renderAudienceSection2(title, claims) {
21670
21876
  if (claims.length === 0) return "";
21671
21877
  return `<section class="audience-section">
21672
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21878
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21673
21879
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21674
21880
  </section>`;
21675
21881
  }
@@ -21759,13 +21965,13 @@ var ReviewHtmlFormatter = class {
21759
21965
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21760
21966
  applyTheme(getEffectiveTheme());` : "";
21761
21967
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21762
- 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>` : "";
21763
21969
  return `<!doctype html>
21764
21970
  <html lang="en"${themeAttr}>
21765
21971
  <head>
21766
21972
  <meta charset="utf-8" />
21767
21973
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21768
- <title>${escapeHtml3(this.title)}</title>
21974
+ <title>${escapeHtml4(this.title)}</title>
21769
21975
  <style>
21770
21976
  ${this.theme.css}
21771
21977
  ${REVIEW_CSS}
@@ -21775,7 +21981,7 @@ applyTheme(getEffectiveTheme());` : "";
21775
21981
  <main>
21776
21982
  <div class="hero-card card">
21777
21983
  <div class="review-header">
21778
- <h1>${escapeHtml3(this.title)}</h1>
21984
+ <h1>${escapeHtml4(this.title)}</h1>
21779
21985
  ${themeToggleHtml}
21780
21986
  </div>
21781
21987
  ${refsLine}
@@ -21790,7 +21996,7 @@ applyTheme(getEffectiveTheme());` : "";
21790
21996
  </section>
21791
21997
  <section class="card priority-banner">
21792
21998
  <h2>Review priority</h2>
21793
- <p class="subtle">${escapeHtml3(priority)}</p>
21999
+ <p class="subtle">${escapeHtml4(priority)}</p>
21794
22000
  </section>
21795
22001
  ${changedFilesPanel}
21796
22002
  <section class="toolbar">
@@ -21838,8 +22044,8 @@ applyTheme(getEffectiveTheme());` : "";
21838
22044
  };
21839
22045
 
21840
22046
  // src/deploy/ledger.ts
21841
- var fs9 = __toESM(require("fs"), 1);
21842
- var path10 = __toESM(require("path"), 1);
22047
+ var fs10 = __toESM(require("fs"), 1);
22048
+ var path11 = __toESM(require("path"), 1);
21843
22049
  function createEmptyLedger() {
21844
22050
  return {
21845
22051
  deployments: [],
@@ -21847,12 +22053,12 @@ function createEmptyLedger() {
21847
22053
  };
21848
22054
  }
21849
22055
  function loadLedger(ledgerPath) {
21850
- const resolved = path10.resolve(ledgerPath);
21851
- if (!fs9.existsSync(resolved)) {
22056
+ const resolved = path11.resolve(ledgerPath);
22057
+ if (!fs10.existsSync(resolved)) {
21852
22058
  return createEmptyLedger();
21853
22059
  }
21854
22060
  try {
21855
- const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
22061
+ const raw = JSON.parse(fs10.readFileSync(resolved, "utf8"));
21856
22062
  if (raw.schemaVersion !== 1) {
21857
22063
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21858
22064
  }
@@ -21863,10 +22069,10 @@ function loadLedger(ledgerPath) {
21863
22069
  }
21864
22070
  }
21865
22071
  function saveLedger(ledger, ledgerPath) {
21866
- const resolved = path10.resolve(ledgerPath);
21867
- const dir = path10.dirname(resolved);
21868
- fs9.mkdirSync(dir, { recursive: true });
21869
- 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");
21870
22076
  }
21871
22077
  function getLatestDeployment(ledger, environment) {
21872
22078
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21986,11 +22192,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21986
22192
  const ext = FORMAT_EXTENSIONS[format];
21987
22193
  const effectiveName = outputName + (outputNameSuffix ?? "");
21988
22194
  if (mode === "aggregated") {
21989
- return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
22195
+ return toPosix(path12.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21990
22196
  }
21991
22197
  const normalizedSource = toPosix(sourceFile);
21992
- const dirOfSource = path11.posix.dirname(normalizedSource);
21993
- let baseName = path11.posix.basename(normalizedSource);
22198
+ const dirOfSource = path12.posix.dirname(normalizedSource);
22199
+ let baseName = path12.posix.basename(normalizedSource);
21994
22200
  for (const testExt of TEST_EXTENSIONS) {
21995
22201
  if (baseName.endsWith(testExt)) {
21996
22202
  baseName = baseName.slice(0, -testExt.length);
@@ -21999,12 +22205,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21999
22205
  }
22000
22206
  const fileName = `${baseName}.${effectiveName}${ext}`;
22001
22207
  if (colocatedStyle === "adjacent") {
22002
- return toPosix(path11.posix.join(dirOfSource, fileName));
22208
+ return toPosix(path12.posix.join(dirOfSource, fileName));
22003
22209
  }
22004
22210
  if (colocatedStyle === "flat") {
22005
- return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
22211
+ return toPosix(path12.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
22006
22212
  }
22007
- return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
22213
+ return toPosix(path12.posix.join(baseOutputDir, dirOfSource, fileName));
22008
22214
  }
22009
22215
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
22010
22216
  const groups = /* @__PURE__ */ new Map();
@@ -22217,8 +22423,8 @@ var ReportGenerator = class {
22217
22423
  if (astroPaths) {
22218
22424
  for (const mdPath of astroPaths) {
22219
22425
  const content = await fsPromises.readFile(mdPath, "utf8");
22220
- const mdDir = path11.dirname(mdPath);
22221
- const assetsDir = path11.resolve(this.options.astro.assetsDir);
22426
+ const mdDir = path12.dirname(mdPath);
22427
+ const assetsDir = path12.resolve(this.options.astro.assetsDir);
22222
22428
  const result = copyMarkdownAssets({
22223
22429
  markdown: content,
22224
22430
  markdownDir: mdDir,
@@ -22249,9 +22455,9 @@ var ReportGenerator = class {
22249
22455
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
22250
22456
  const ext = FORMAT_EXTENSIONS[format];
22251
22457
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
22252
- const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22458
+ const outputPath = toPosix(path12.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22253
22459
  const content = await this.formatContent(run, format);
22254
- const dir = path11.dirname(outputPath);
22460
+ const dir = path12.dirname(outputPath);
22255
22461
  await fsPromises.mkdir(dir, { recursive: true });
22256
22462
  await this.deps.writeFile(outputPath, content);
22257
22463
  return [outputPath];
@@ -22263,7 +22469,7 @@ var ReportGenerator = class {
22263
22469
  testCases
22264
22470
  };
22265
22471
  const content = await this.formatContent(groupRun, format);
22266
- const dir = path11.dirname(outputPath);
22472
+ const dir = path12.dirname(outputPath);
22267
22473
  await fsPromises.mkdir(dir, { recursive: true });
22268
22474
  await this.deps.writeFile(outputPath, content);
22269
22475
  writtenPaths.push(outputPath);
@@ -22415,7 +22621,7 @@ async function generateRunComparison(args) {
22415
22621
  await fsPromises.mkdir(outputDir, { recursive: true });
22416
22622
  for (const format of args.formats) {
22417
22623
  const ext = format === "html" ? ".html" : ".md";
22418
- const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
22624
+ const outputPath = toPosix(path12.join(outputDir, `${outputName}${ext}`));
22419
22625
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
22420
22626
  await fsPromises.writeFile(outputPath, content, "utf8");
22421
22627
  files.push(outputPath);
@@ -22465,6 +22671,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22465
22671
  adaptJestRun,
22466
22672
  adaptPlaywrightRun,
22467
22673
  adaptVitestRun,
22674
+ advanceState,
22468
22675
  assertValidRun,
22469
22676
  buildCheck,
22470
22677
  buildGoal,
@@ -22477,6 +22684,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22477
22684
  canonicalizeRun,
22478
22685
  classifyStatusChange,
22479
22686
  clearVersionCache,
22687
+ computeDeltas,
22480
22688
  computeTestMetrics,
22481
22689
  copyMarkdownAssets,
22482
22690
  createPrCommentSummary,
@@ -22499,6 +22707,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22499
22707
  getEnvironmentDrift,
22500
22708
  gradeEvidence,
22501
22709
  hasSufficientHistory,
22710
+ injectLiveBits,
22502
22711
  isReviewableSource,
22503
22712
  isTestFile,
22504
22713
  joinNameAndExt,
@@ -22520,7 +22729,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22520
22729
  readPackageVersion,
22521
22730
  recordDeployment,
22522
22731
  regenerateArtifacts,
22732
+ regenerateRun,
22523
22733
  renderCheck,
22734
+ renderDeltaStrip,
22524
22735
  renderGoal,
22525
22736
  renderTriage,
22526
22737
  resolveAttachment,
@@ -22536,6 +22747,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
22536
22747
  sendWebhookNotification,
22537
22748
  signBody,
22538
22749
  slugify,
22750
+ startServe,
22539
22751
  startWatch,
22540
22752
  stripAnsi,
22541
22753
  toBehaviorManifest,