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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path11 from "path";
3
+ import * as path12 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -51,7 +51,7 @@ function tokenize(text2) {
51
51
  }
52
52
  function behaviourFingerprint(input) {
53
53
  const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
54
- const covers = [...input.covers ?? []].map((path12) => path12.trim()).filter(Boolean).sort().join(",");
54
+ const covers = [...input.covers ?? []].map((path13) => path13.trim()).filter(Boolean).sort().join(",");
55
55
  if (steps.length === 0 && covers.length === 0) return "";
56
56
  return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
57
57
  }
@@ -14274,12 +14274,12 @@ function hasSufficientHistory(entries, min) {
14274
14274
  }
14275
14275
 
14276
14276
  // src/formatters/html/renderers/scenario.ts
14277
- function renderTicket(ticket, template, escapeHtml4) {
14277
+ function renderTicket(ticket, template, escapeHtml5) {
14278
14278
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
14279
14279
  if (url) {
14280
- return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
14280
+ return `<a class="tag ticket-tag" href="${escapeHtml5(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml5(ticket.id)}</a>`;
14281
14281
  }
14282
- return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
14282
+ return `<span class="tag ticket-tag">${escapeHtml5(ticket.id)}</span>`;
14283
14283
  }
14284
14284
  function renderScenario(args, deps) {
14285
14285
  const { tc } = args;
@@ -14479,7 +14479,7 @@ function flattenTree(roots) {
14479
14479
  }
14480
14480
  return result;
14481
14481
  }
14482
- function buildTooltip(span, escapeHtml4) {
14482
+ function buildTooltip(span, escapeHtml5) {
14483
14483
  const parts = [];
14484
14484
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
14485
14485
  if (span.statusMessage) {
@@ -14497,7 +14497,7 @@ function buildTooltip(span, escapeHtml4) {
14497
14497
  if (text2.length > TOOLTIP_MAX_LENGTH) {
14498
14498
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
14499
14499
  }
14500
- return escapeHtml4(text2);
14500
+ return escapeHtml5(text2);
14501
14501
  }
14502
14502
  function renderTraceView(args, deps) {
14503
14503
  if (!args.spans || args.spans.length === 0) return "";
@@ -15792,14 +15792,14 @@ var TraceabilityMatrixFormatter = class {
15792
15792
  lines.push("");
15793
15793
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
15794
15794
  if (req.covers.length > 0) {
15795
- lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
15795
+ lines.push(`Covers: ${req.covers.map((path13) => `\`${path13}\``).join(", ")}`);
15796
15796
  }
15797
15797
  lines.push("");
15798
15798
  lines.push("| Status | Scenario | Source | Covers |");
15799
15799
  lines.push("| --- | --- | --- | --- |");
15800
15800
  for (const scenario of req.scenarios) {
15801
15801
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15802
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
15802
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path13) => `\`${path13}\``).join(", ") : "";
15803
15803
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
15804
15804
  }
15805
15805
  lines.push("");
@@ -15901,8 +15901,8 @@ function extractFeatureName(testCases, uri) {
15901
15901
  return tc.titlePath[0];
15902
15902
  }
15903
15903
  }
15904
- const basename3 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
15905
- return basename3.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
15904
+ const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
15905
+ return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
15906
15906
  }
15907
15907
  function synthesizeFeature(uri, testCases) {
15908
15908
  const featureName = extractFeatureName(testCases, uri);
@@ -16514,8 +16514,8 @@ function extractDocAttachments(step) {
16514
16514
  }
16515
16515
  return attachments;
16516
16516
  }
16517
- function guessMediaType(path12) {
16518
- const lower = path12.toLowerCase();
16517
+ function guessMediaType(path13) {
16518
+ const lower = path13.toLowerCase();
16519
16519
  if (lower.endsWith(".png")) return "image/png";
16520
16520
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16521
16521
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16656,11 +16656,11 @@ var CucumberHtmlFormatter = class {
16656
16656
  for (const envelope of envelopes) {
16657
16657
  const accepted = htmlStream.write(envelope);
16658
16658
  if (!accepted) {
16659
- await new Promise((resolve9) => htmlStream.once("drain", resolve9));
16659
+ await new Promise((resolve10) => htmlStream.once("drain", resolve10));
16660
16660
  }
16661
16661
  }
16662
- await new Promise((resolve9, reject) => {
16663
- collector.on("finish", resolve9);
16662
+ await new Promise((resolve10, reject) => {
16663
+ collector.on("finish", resolve10);
16664
16664
  collector.on("error", reject);
16665
16665
  htmlStream.end();
16666
16666
  });
@@ -18824,14 +18824,14 @@ ${result.errors.join("\n")}`);
18824
18824
  }
18825
18825
 
18826
18826
  // src/coverage-index.ts
18827
- function normalizePath(path12) {
18828
- return path12.replace(/^\.\//, "");
18827
+ function normalizePath(path13) {
18828
+ return path13.replace(/^\.\//, "");
18829
18829
  }
18830
18830
  function scenariosCoveringPaths(index, paths) {
18831
18831
  const queries = paths.map(normalizePath);
18832
18832
  return index.scenarios.filter(
18833
18833
  (scenario) => scenario.covers.some(
18834
- (glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
18834
+ (glob) => queries.some((path13) => matchesPattern(normalizePath(glob), path13))
18835
18835
  )
18836
18836
  );
18837
18837
  }
@@ -18907,7 +18907,7 @@ function toRun(data, inputType, synthesize) {
18907
18907
  if (synthesize) raw = synthesizeStories(raw);
18908
18908
  return canonicalizeRun(raw);
18909
18909
  }
18910
- async function regenerateArtifacts(options, deps = {}) {
18910
+ async function regenerateRun(options, deps = {}) {
18911
18911
  const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18912
18912
  const data = JSON.parse(read(path7.resolve(options.input)));
18913
18913
  const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
@@ -18917,7 +18917,10 @@ async function regenerateArtifacts(options, deps = {}) {
18917
18917
  outputName: options.outputName
18918
18918
  });
18919
18919
  const result = await generator.generate(run);
18920
- return [...result.values()].flat();
18920
+ return { files: [...result.values()].flat(), run };
18921
+ }
18922
+ async function regenerateArtifacts(options, deps = {}) {
18923
+ return (await regenerateRun(options, deps)).files;
18921
18924
  }
18922
18925
  function startWatch(options, deps = {}) {
18923
18926
  const log = deps.log ?? ((message) => console.log(message));
@@ -18960,6 +18963,203 @@ function startWatch(options, deps = {}) {
18960
18963
  };
18961
18964
  }
18962
18965
 
18966
+ // src/serve.ts
18967
+ import * as fs7 from "fs";
18968
+ import * as http from "http";
18969
+ import * as path8 from "path";
18970
+ function advanceState(prev, run) {
18971
+ if (prev.sessionBaseline === null) {
18972
+ return { sessionBaseline: run, previous: null, current: run, runCount: 1 };
18973
+ }
18974
+ return {
18975
+ sessionBaseline: prev.sessionBaseline,
18976
+ previous: prev.current,
18977
+ current: run,
18978
+ runCount: prev.runCount + 1
18979
+ };
18980
+ }
18981
+ function computeDeltas(state) {
18982
+ if (state.current === null || state.sessionBaseline === null || state.runCount <= 1) {
18983
+ return { session: null, iteration: null };
18984
+ }
18985
+ return {
18986
+ session: diffRuns(state.sessionBaseline, state.current),
18987
+ iteration: state.previous ? diffRuns(state.previous, state.current) : null
18988
+ };
18989
+ }
18990
+ function pluralize(n, word) {
18991
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
18992
+ }
18993
+ function summarizeDiff(diff) {
18994
+ const s = diff.summary;
18995
+ const parts = [];
18996
+ if (s.fixed > 0) parts.push(`+${pluralize(s.fixed, "passing")}`);
18997
+ if (s.regressed > 0) parts.push(`${pluralize(s.regressed, "regressed")}`);
18998
+ if (s.added > 0) parts.push(`${pluralize(s.added, "new behaviour")}`);
18999
+ if (s.removed > 0) parts.push(`${pluralize(s.removed, "removed")}`);
19000
+ const moved = s.renamed + s.moved;
19001
+ if (moved > 0) parts.push(`${pluralize(moved, "renamed")}`);
19002
+ if (s.changed > 0) parts.push(`${pluralize(s.changed, "changed")}`);
19003
+ return parts.length > 0 ? parts.join(", ") : null;
19004
+ }
19005
+ function escapeHtml3(text2) {
19006
+ return text2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19007
+ }
19008
+ function renderDeltaStrip(state) {
19009
+ if (state.current === null) return "";
19010
+ const { session, iteration } = computeDeltas(state);
19011
+ if (session === null) {
19012
+ const label = `Run #${state.runCount} captured \u2014 baseline pinned. Watching for changes\u2026`;
19013
+ return `<div data-es-live="strip"><strong>Live</strong> \xB7 ${escapeHtml3(label)}</div>`;
19014
+ }
19015
+ const sessionLine = summarizeDiff(session) ?? "no change yet";
19016
+ let detail = "";
19017
+ if (iteration) {
19018
+ const iterationLine = summarizeDiff(iteration);
19019
+ if (iterationLine) detail = ` \xB7 <span data-es-live="iteration">this iteration: ${escapeHtml3(iterationLine)}</span>`;
19020
+ }
19021
+ return [
19022
+ `<div data-es-live="strip">`,
19023
+ `<strong>Live</strong> \xB7 run #${state.runCount} \xB7 `,
19024
+ `<span data-es-live="session">since you started: ${escapeHtml3(sessionLine)}</span>`,
19025
+ detail,
19026
+ `</div>`
19027
+ ].join("");
19028
+ }
19029
+ var RELOAD_CLIENT = `<script data-es-live="client">
19030
+ (function () {
19031
+ try {
19032
+ var es = new EventSource("/__es_reload");
19033
+ es.onmessage = function (e) { if (e.data === "reload") location.reload(); };
19034
+ } catch (err) { /* SSE unavailable: stay static */ }
19035
+ })();
19036
+ </script>`;
19037
+ var STRIP_STYLE = `<style data-es-live="style">
19038
+ [data-es-live="strip"]{position:sticky;top:0;z-index:9999;font:14px/1.5 system-ui,sans-serif;
19039
+ padding:8px 16px;background:#0b1021;color:#e6e9f5;border-bottom:1px solid #2a3052}
19040
+ [data-es-live="strip"] strong{color:#7dd3fc}
19041
+ </style>`;
19042
+ function injectLiveBits(html, stripHtml) {
19043
+ let out = html;
19044
+ const bodyOpen = out.match(/<body[^>]*>/i);
19045
+ if (bodyOpen) {
19046
+ const at = bodyOpen.index + bodyOpen[0].length;
19047
+ out = out.slice(0, at) + stripHtml + out.slice(at);
19048
+ } else {
19049
+ out = stripHtml + out;
19050
+ }
19051
+ const tail = STRIP_STYLE + RELOAD_CLIENT;
19052
+ if (/<\/body>/i.test(out)) {
19053
+ out = out.replace(/<\/body>/i, tail + "</body>");
19054
+ } else {
19055
+ out += tail;
19056
+ }
19057
+ return out;
19058
+ }
19059
+ var CONTENT_TYPES = {
19060
+ ".html": "text/html; charset=utf-8",
19061
+ ".css": "text/css; charset=utf-8",
19062
+ ".js": "text/javascript; charset=utf-8",
19063
+ ".json": "application/json; charset=utf-8",
19064
+ ".svg": "image/svg+xml",
19065
+ ".png": "image/png"
19066
+ };
19067
+ function watchInputDir(filePath, listener) {
19068
+ const dir = path8.dirname(filePath);
19069
+ const base = path8.basename(filePath);
19070
+ fs7.mkdirSync(dir, { recursive: true });
19071
+ const watcher = fs7.watch(dir, (_event, changed) => {
19072
+ if (!changed || changed === base) listener();
19073
+ });
19074
+ return { close: () => watcher.close() };
19075
+ }
19076
+ function plainText(html) {
19077
+ return html.replace(/<[^>]+>/g, "").trim();
19078
+ }
19079
+ function startServe(options, deps = {}) {
19080
+ const log = deps.log ?? ((message) => console.log(message));
19081
+ const read = deps.readFile ?? ((filePath) => fs7.readFileSync(filePath, "utf8"));
19082
+ const port = options.port ?? 4321;
19083
+ const host = options.host ?? "127.0.0.1";
19084
+ let state = { sessionBaseline: null, previous: null, current: null, runCount: 0 };
19085
+ let htmlPath = null;
19086
+ let stripHtml = renderDeltaStrip(state);
19087
+ const clients = /* @__PURE__ */ new Set();
19088
+ const pushReload = () => {
19089
+ for (const res of clients) res.write("data: reload\n\n");
19090
+ };
19091
+ const handler = (req, res) => {
19092
+ const url = (req.url ?? "/").split("?")[0];
19093
+ if (url === "/__es_reload") {
19094
+ res.writeHead(200, {
19095
+ "Content-Type": "text/event-stream",
19096
+ "Cache-Control": "no-cache",
19097
+ Connection: "keep-alive"
19098
+ });
19099
+ res.write("retry: 1000\n\n");
19100
+ clients.add(res);
19101
+ req.on("close", () => clients.delete(res));
19102
+ return;
19103
+ }
19104
+ if (url === "/" || url === "/index.html") {
19105
+ const html = htmlPath ? read(htmlPath) : "<!doctype html><html><body><h1>executable-stories</h1></body></html>";
19106
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
19107
+ res.end(injectLiveBits(html, stripHtml));
19108
+ return;
19109
+ }
19110
+ const safe = path8.normalize(url).replace(/^(\.\.[/\\])+/, "");
19111
+ const filePath = path8.join(path8.resolve(options.outputDir), safe);
19112
+ if (filePath.startsWith(path8.resolve(options.outputDir)) && fs7.existsSync(filePath)) {
19113
+ const ext = path8.extname(filePath).toLowerCase();
19114
+ res.writeHead(200, { "Content-Type": CONTENT_TYPES[ext] ?? "application/octet-stream" });
19115
+ res.end(read(filePath));
19116
+ return;
19117
+ }
19118
+ res.writeHead(404, { "Content-Type": "text/plain" });
19119
+ res.end("Not found");
19120
+ };
19121
+ const server = deps.createServer ? deps.createServer(handler) : http.createServer(handler);
19122
+ const watchOptions = {
19123
+ input: options.input,
19124
+ outputDir: options.outputDir,
19125
+ outputName: options.outputName,
19126
+ formats: options.formats,
19127
+ inputType: options.inputType ?? "raw",
19128
+ synthesize: options.synthesize !== false,
19129
+ debounceMs: options.debounceMs
19130
+ };
19131
+ const watchHandle = startWatch(watchOptions, {
19132
+ readFile: read,
19133
+ watch: deps.watch ?? watchInputDir,
19134
+ log: () => {
19135
+ },
19136
+ // serve emits its own per-run line below
19137
+ regenerate: async (input) => {
19138
+ if (!fs7.existsSync(path8.resolve(input))) return [];
19139
+ const { files, run } = await regenerateRun({ ...watchOptions, input }, { readFile: read });
19140
+ htmlPath = files.find((f) => f.endsWith(".html")) ?? htmlPath;
19141
+ state = advanceState(state, run);
19142
+ stripHtml = renderDeltaStrip(state);
19143
+ log(`Run #${state.runCount}: ${plainText(stripHtml)}`);
19144
+ pushReload();
19145
+ return files;
19146
+ }
19147
+ });
19148
+ server.listen(port, host);
19149
+ const address = server.address();
19150
+ const boundPort = typeof address === "object" && address ? address.port : port;
19151
+ log(`Live docs: http://${host}:${boundPort} (Ctrl+C to stop)`);
19152
+ return {
19153
+ port: boundPort,
19154
+ close: () => {
19155
+ watchHandle.close();
19156
+ for (const res of clients) res.end();
19157
+ clients.clear();
19158
+ server.close();
19159
+ }
19160
+ };
19161
+ }
19162
+
18963
19163
  // src/behavior-diff.ts
18964
19164
  function classifyStatusChange(baseline, current) {
18965
19165
  if (baseline === void 0) return "added";
@@ -19599,27 +19799,27 @@ function pickleStepArgumentToDocs(ps) {
19599
19799
  }
19600
19800
 
19601
19801
  // src/utils/git-info.ts
19602
- import * as fs7 from "fs";
19603
- import * as path8 from "path";
19802
+ import * as fs8 from "fs";
19803
+ import * as path9 from "path";
19604
19804
  function readGitSha(cwd = process.cwd()) {
19605
19805
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
19606
19806
  if (envSha) return envSha;
19607
19807
  const gitDir = findGitDir(cwd);
19608
19808
  if (!gitDir) return void 0;
19609
19809
  try {
19610
- const headPath = path8.join(gitDir, "HEAD");
19611
- const head = fs7.readFileSync(headPath, "utf8").trim();
19810
+ const headPath = path9.join(gitDir, "HEAD");
19811
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19612
19812
  if (!head.startsWith("ref:")) {
19613
19813
  return head;
19614
19814
  }
19615
19815
  const refPath = head.replace("ref:", "").trim();
19616
- const refFile = path8.join(gitDir, refPath);
19617
- if (fs7.existsSync(refFile)) {
19618
- return fs7.readFileSync(refFile, "utf8").trim();
19816
+ const refFile = path9.join(gitDir, refPath);
19817
+ if (fs8.existsSync(refFile)) {
19818
+ return fs8.readFileSync(refFile, "utf8").trim();
19619
19819
  }
19620
- const packedRefs = path8.join(gitDir, "packed-refs");
19621
- if (fs7.existsSync(packedRefs)) {
19622
- const content = fs7.readFileSync(packedRefs, "utf8");
19820
+ const packedRefs = path9.join(gitDir, "packed-refs");
19821
+ if (fs8.existsSync(packedRefs)) {
19822
+ const content = fs8.readFileSync(packedRefs, "utf8");
19623
19823
  for (const line of content.split("\n")) {
19624
19824
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
19625
19825
  const [sha, ref] = line.split(" ");
@@ -19634,19 +19834,19 @@ function readGitSha(cwd = process.cwd()) {
19634
19834
  function findGitDir(start) {
19635
19835
  let current = start;
19636
19836
  while (true) {
19637
- const candidate = path8.join(current, ".git");
19638
- if (fs7.existsSync(candidate)) {
19639
- const stat = fs7.statSync(candidate);
19837
+ const candidate = path9.join(current, ".git");
19838
+ if (fs8.existsSync(candidate)) {
19839
+ const stat = fs8.statSync(candidate);
19640
19840
  if (stat.isFile()) {
19641
- const content = fs7.readFileSync(candidate, "utf8").trim();
19841
+ const content = fs8.readFileSync(candidate, "utf8").trim();
19642
19842
  const match = content.match(/^gitdir: (.+)$/);
19643
19843
  if (match) {
19644
- return path8.resolve(current, match[1]);
19844
+ return path9.resolve(current, match[1]);
19645
19845
  }
19646
19846
  }
19647
19847
  return candidate;
19648
19848
  }
19649
- const parent = path8.dirname(current);
19849
+ const parent = path9.dirname(current);
19650
19850
  if (parent === current) return void 0;
19651
19851
  current = parent;
19652
19852
  }
@@ -19657,8 +19857,8 @@ function readBranchName(cwd = process.cwd()) {
19657
19857
  const gitDir = findGitDir(cwd);
19658
19858
  if (!gitDir) return void 0;
19659
19859
  try {
19660
- const headPath = path8.join(gitDir, "HEAD");
19661
- const head = fs7.readFileSync(headPath, "utf8").trim();
19860
+ const headPath = path9.join(gitDir, "HEAD");
19861
+ const head = fs8.readFileSync(headPath, "utf8").trim();
19662
19862
  if (head.startsWith("ref:")) {
19663
19863
  const refPath = head.replace("ref:", "").trim();
19664
19864
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -19695,8 +19895,8 @@ function nanosecondsToMs(ns) {
19695
19895
  }
19696
19896
 
19697
19897
  // src/utils/metadata.ts
19698
- import * as fs8 from "fs";
19699
- import * as path9 from "path";
19898
+ import * as fs9 from "fs";
19899
+ import * as path10 from "path";
19700
19900
  var versionCache = /* @__PURE__ */ new Map();
19701
19901
  function readPackageVersion(root) {
19702
19902
  if (versionCache.has(root)) {
@@ -19707,18 +19907,18 @@ function readPackageVersion(root) {
19707
19907
  return version;
19708
19908
  }
19709
19909
  function findPackageVersion(startDir) {
19710
- let current = path9.resolve(startDir);
19910
+ let current = path10.resolve(startDir);
19711
19911
  while (true) {
19712
- const pkgPath = path9.join(current, "package.json");
19912
+ const pkgPath = path10.join(current, "package.json");
19713
19913
  try {
19714
- if (fs8.existsSync(pkgPath)) {
19715
- const raw = fs8.readFileSync(pkgPath, "utf8");
19914
+ if (fs9.existsSync(pkgPath)) {
19915
+ const raw = fs9.readFileSync(pkgPath, "utf8");
19716
19916
  const parsed = JSON.parse(raw);
19717
19917
  return parsed.version;
19718
19918
  }
19719
19919
  } catch {
19720
19920
  }
19721
- const parent = path9.dirname(current);
19921
+ const parent = path10.dirname(current);
19722
19922
  if (parent === current) {
19723
19923
  return void 0;
19724
19924
  }
@@ -21046,18 +21246,18 @@ function deriveChangeType(tags) {
21046
21246
  }
21047
21247
  return "unknown";
21048
21248
  }
21049
- function extensionOf(path12) {
21050
- const base = path12.split("/").pop() ?? path12;
21249
+ function extensionOf(path13) {
21250
+ const base = path13.split("/").pop() ?? path13;
21051
21251
  const dot = base.lastIndexOf(".");
21052
21252
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
21053
21253
  }
21054
- function isTestFile(path12) {
21055
- return TEST_INFIX.test(path12);
21254
+ function isTestFile(path13) {
21255
+ return TEST_INFIX.test(path13);
21056
21256
  }
21057
- function isReviewableSource(path12) {
21058
- if (isTestFile(path12)) return false;
21059
- if (path12.endsWith(".d.ts")) return false;
21060
- return CODE_EXTENSIONS.has(extensionOf(path12));
21257
+ function isReviewableSource(path13) {
21258
+ if (isTestFile(path13)) return false;
21259
+ if (path13.endsWith(".d.ts")) return false;
21260
+ return CODE_EXTENSIONS.has(extensionOf(path13));
21061
21261
  }
21062
21262
  function testBaseKey(testFile) {
21063
21263
  return testFile.replace(TEST_INFIX, "");
@@ -21161,7 +21361,7 @@ function toClaim(testCase, changedSourcePaths) {
21161
21361
  const { strength, reasons } = gradeEvidence(testCase, audience);
21162
21362
  const key = testBaseKey(testCase.sourceFile);
21163
21363
  const coversFiles = changedSourcePaths.filter(
21164
- (path12) => sourceBaseKey(path12) === key
21364
+ (path13) => sourceBaseKey(path13) === key
21165
21365
  );
21166
21366
  return {
21167
21367
  id: testCase.id,
@@ -21410,7 +21610,7 @@ var ReviewMarkdownFormatter = class {
21410
21610
  };
21411
21611
 
21412
21612
  // src/formatters/review-html.ts
21413
- function escapeHtml3(value) {
21613
+ function escapeHtml4(value) {
21414
21614
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21415
21615
  }
21416
21616
  var STRENGTH_LABEL = {
@@ -21432,22 +21632,22 @@ function statusIcon3(status) {
21432
21632
  }
21433
21633
  }
21434
21634
  function formatStep3(step) {
21435
- return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
21635
+ return `<li><strong>${escapeHtml4(step.keyword)}</strong> ${escapeHtml4(step.text)}</li>`;
21436
21636
  }
21437
21637
  function inlineDoc(doc) {
21438
21638
  switch (doc.kind) {
21439
21639
  case "note":
21440
- return escapeHtml3(doc.text);
21640
+ return escapeHtml4(doc.text);
21441
21641
  case "section":
21442
- return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
21642
+ return `<strong>${escapeHtml4(doc.title)}</strong>: ${escapeHtml4(doc.markdown)}`;
21443
21643
  case "kv":
21444
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
21644
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(String(doc.value))}`;
21445
21645
  case "code":
21446
- return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
21646
+ return `${escapeHtml4(doc.label)}: <code>${escapeHtml4(doc.content)}</code>`;
21447
21647
  case "link":
21448
- return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
21648
+ return `${escapeHtml4(doc.label)}: ${escapeHtml4(doc.url)}`;
21449
21649
  default:
21450
- return escapeHtml3(doc.kind);
21650
+ return escapeHtml4(doc.kind);
21451
21651
  }
21452
21652
  }
21453
21653
  function renderEvidenceArtifacts(testCase) {
@@ -21455,7 +21655,7 @@ function renderEvidenceArtifacts(testCase) {
21455
21655
  for (const att of testCase.attachments) {
21456
21656
  if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
21457
21657
  parts.push(
21458
- `<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
21658
+ `<img class="shot" alt="${escapeHtml4(att.name)}" src="data:${escapeHtml4(att.mediaType)};base64,${att.body}" />`
21459
21659
  );
21460
21660
  }
21461
21661
  }
@@ -21470,22 +21670,22 @@ function renderTicketPills(claim) {
21470
21670
  const tickets = claim.testCase.story.tickets ?? [];
21471
21671
  if (tickets.length === 0) return "";
21472
21672
  return `<div class="ticket-row">${tickets.map((ticket) => {
21473
- const label = escapeHtml3(ticket.id);
21673
+ const label = escapeHtml4(ticket.id);
21474
21674
  if (ticket.url) {
21475
- return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21675
+ return `<a class="ticket-pill" href="${escapeHtml4(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
21476
21676
  }
21477
21677
  return `<span class="ticket-pill">${label}</span>`;
21478
21678
  }).join("")}</div>`;
21479
21679
  }
21480
21680
  function renderClaimCard(claim) {
21481
21681
  const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
21482
- const search = escapeHtml3(
21682
+ const search = escapeHtml4(
21483
21683
  `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
21484
21684
  ).toLowerCase();
21485
21685
  const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
21486
- const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
21487
- const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
21488
- const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
21686
+ const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml4(r)}</li>`).join("")}</ul>`;
21687
+ const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml4(claim.intent)}</div>` : "";
21688
+ const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml4(f)}</code>`).join(", ")}</p>` : "";
21489
21689
  const docs = (claim.testCase.story.docs ?? []).filter(
21490
21690
  (d) => d.kind === "section" || d.kind === "note"
21491
21691
  );
@@ -21495,9 +21695,9 @@ function renderClaimCard(claim) {
21495
21695
  <header class="claim-header">
21496
21696
  <div>
21497
21697
  <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
21498
- ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
21499
- <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
21500
- <p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21698
+ ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml4(claim.changeType)}</span>` : ""}
21699
+ <h3>${statusIcon3(claim.status)} ${escapeHtml4(claim.scenario)}</h3>
21700
+ <p class="source">${escapeHtml4(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
21501
21701
  ${renderTicketPills(claim)}
21502
21702
  </div>
21503
21703
  </header>
@@ -21512,18 +21712,18 @@ function renderClaimCard(claim) {
21512
21712
  </article>`;
21513
21713
  }
21514
21714
  function renderChangedFileRow(file) {
21515
- const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21715
+ const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml4(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
21516
21716
  return `<tr data-band="${file.band}">
21517
21717
  <td><span class="band-dot band-${file.band}"></span></td>
21518
- <td><code>${escapeHtml3(file.path)}</code></td>
21519
- <td>${escapeHtml3(file.changeKind)}</td>
21718
+ <td><code>${escapeHtml4(file.path)}</code></td>
21719
+ <td>${escapeHtml4(file.changeKind)}</td>
21520
21720
  <td>${claims}</td>
21521
21721
  </tr>`;
21522
21722
  }
21523
21723
  function renderAudienceSection2(title, claims) {
21524
21724
  if (claims.length === 0) return "";
21525
21725
  return `<section class="audience-section">
21526
- <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
21726
+ <h2>${escapeHtml4(title)} <span class="count">${claims.length}</span></h2>
21527
21727
  <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
21528
21728
  </section>`;
21529
21729
  }
@@ -21613,13 +21813,13 @@ var ReviewHtmlFormatter = class {
21613
21813
  const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
21614
21814
  applyTheme(getEffectiveTheme());` : "";
21615
21815
  const themeAttr = this.darkMode ? ' data-theme="light"' : "";
21616
- const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
21816
+ const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml4(context.baseRef ?? "base")} \u2192 ${escapeHtml4(context.headRef ?? "head")}</p>` : "";
21617
21817
  return `<!doctype html>
21618
21818
  <html lang="en"${themeAttr}>
21619
21819
  <head>
21620
21820
  <meta charset="utf-8" />
21621
21821
  <meta name="viewport" content="width=device-width, initial-scale=1" />
21622
- <title>${escapeHtml3(this.title)}</title>
21822
+ <title>${escapeHtml4(this.title)}</title>
21623
21823
  <style>
21624
21824
  ${this.theme.css}
21625
21825
  ${REVIEW_CSS}
@@ -21629,7 +21829,7 @@ applyTheme(getEffectiveTheme());` : "";
21629
21829
  <main>
21630
21830
  <div class="hero-card card">
21631
21831
  <div class="review-header">
21632
- <h1>${escapeHtml3(this.title)}</h1>
21832
+ <h1>${escapeHtml4(this.title)}</h1>
21633
21833
  ${themeToggleHtml}
21634
21834
  </div>
21635
21835
  ${refsLine}
@@ -21644,7 +21844,7 @@ applyTheme(getEffectiveTheme());` : "";
21644
21844
  </section>
21645
21845
  <section class="card priority-banner">
21646
21846
  <h2>Review priority</h2>
21647
- <p class="subtle">${escapeHtml3(priority)}</p>
21847
+ <p class="subtle">${escapeHtml4(priority)}</p>
21648
21848
  </section>
21649
21849
  ${changedFilesPanel}
21650
21850
  <section class="toolbar">
@@ -21692,8 +21892,8 @@ applyTheme(getEffectiveTheme());` : "";
21692
21892
  };
21693
21893
 
21694
21894
  // src/deploy/ledger.ts
21695
- import * as fs9 from "fs";
21696
- import * as path10 from "path";
21895
+ import * as fs10 from "fs";
21896
+ import * as path11 from "path";
21697
21897
  function createEmptyLedger() {
21698
21898
  return {
21699
21899
  deployments: [],
@@ -21701,12 +21901,12 @@ function createEmptyLedger() {
21701
21901
  };
21702
21902
  }
21703
21903
  function loadLedger(ledgerPath) {
21704
- const resolved = path10.resolve(ledgerPath);
21705
- if (!fs9.existsSync(resolved)) {
21904
+ const resolved = path11.resolve(ledgerPath);
21905
+ if (!fs10.existsSync(resolved)) {
21706
21906
  return createEmptyLedger();
21707
21907
  }
21708
21908
  try {
21709
- const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
21909
+ const raw = JSON.parse(fs10.readFileSync(resolved, "utf8"));
21710
21910
  if (raw.schemaVersion !== 1) {
21711
21911
  throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
21712
21912
  }
@@ -21717,10 +21917,10 @@ function loadLedger(ledgerPath) {
21717
21917
  }
21718
21918
  }
21719
21919
  function saveLedger(ledger, ledgerPath) {
21720
- const resolved = path10.resolve(ledgerPath);
21721
- const dir = path10.dirname(resolved);
21722
- fs9.mkdirSync(dir, { recursive: true });
21723
- fs9.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21920
+ const resolved = path11.resolve(ledgerPath);
21921
+ const dir = path11.dirname(resolved);
21922
+ fs10.mkdirSync(dir, { recursive: true });
21923
+ fs10.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
21724
21924
  }
21725
21925
  function getLatestDeployment(ledger, environment) {
21726
21926
  return [...ledger.deployments].reverse().find((d) => d.environment === environment);
@@ -21840,11 +22040,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21840
22040
  const ext = FORMAT_EXTENSIONS[format];
21841
22041
  const effectiveName = outputName + (outputNameSuffix ?? "");
21842
22042
  if (mode === "aggregated") {
21843
- return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
22043
+ return toPosix(path12.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21844
22044
  }
21845
22045
  const normalizedSource = toPosix(sourceFile);
21846
- const dirOfSource = path11.posix.dirname(normalizedSource);
21847
- let baseName = path11.posix.basename(normalizedSource);
22046
+ const dirOfSource = path12.posix.dirname(normalizedSource);
22047
+ let baseName = path12.posix.basename(normalizedSource);
21848
22048
  for (const testExt of TEST_EXTENSIONS) {
21849
22049
  if (baseName.endsWith(testExt)) {
21850
22050
  baseName = baseName.slice(0, -testExt.length);
@@ -21853,12 +22053,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
21853
22053
  }
21854
22054
  const fileName = `${baseName}.${effectiveName}${ext}`;
21855
22055
  if (colocatedStyle === "adjacent") {
21856
- return toPosix(path11.posix.join(dirOfSource, fileName));
22056
+ return toPosix(path12.posix.join(dirOfSource, fileName));
21857
22057
  }
21858
22058
  if (colocatedStyle === "flat") {
21859
- return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
22059
+ return toPosix(path12.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21860
22060
  }
21861
- return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
22061
+ return toPosix(path12.posix.join(baseOutputDir, dirOfSource, fileName));
21862
22062
  }
21863
22063
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
21864
22064
  const groups = /* @__PURE__ */ new Map();
@@ -22071,8 +22271,8 @@ var ReportGenerator = class {
22071
22271
  if (astroPaths) {
22072
22272
  for (const mdPath of astroPaths) {
22073
22273
  const content = await fsPromises.readFile(mdPath, "utf8");
22074
- const mdDir = path11.dirname(mdPath);
22075
- const assetsDir = path11.resolve(this.options.astro.assetsDir);
22274
+ const mdDir = path12.dirname(mdPath);
22275
+ const assetsDir = path12.resolve(this.options.astro.assetsDir);
22076
22276
  const result = copyMarkdownAssets({
22077
22277
  markdown: content,
22078
22278
  markdownDir: mdDir,
@@ -22103,9 +22303,9 @@ var ReportGenerator = class {
22103
22303
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
22104
22304
  const ext = FORMAT_EXTENSIONS[format];
22105
22305
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
22106
- const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22306
+ const outputPath = toPosix(path12.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
22107
22307
  const content = await this.formatContent(run, format);
22108
- const dir = path11.dirname(outputPath);
22308
+ const dir = path12.dirname(outputPath);
22109
22309
  await fsPromises.mkdir(dir, { recursive: true });
22110
22310
  await this.deps.writeFile(outputPath, content);
22111
22311
  return [outputPath];
@@ -22117,7 +22317,7 @@ var ReportGenerator = class {
22117
22317
  testCases
22118
22318
  };
22119
22319
  const content = await this.formatContent(groupRun, format);
22120
- const dir = path11.dirname(outputPath);
22320
+ const dir = path12.dirname(outputPath);
22121
22321
  await fsPromises.mkdir(dir, { recursive: true });
22122
22322
  await this.deps.writeFile(outputPath, content);
22123
22323
  writtenPaths.push(outputPath);
@@ -22269,7 +22469,7 @@ async function generateRunComparison(args) {
22269
22469
  await fsPromises.mkdir(outputDir, { recursive: true });
22270
22470
  for (const format of args.formats) {
22271
22471
  const ext = format === "html" ? ".html" : ".md";
22272
- const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
22472
+ const outputPath = toPosix(path12.join(outputDir, `${outputName}${ext}`));
22273
22473
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
22274
22474
  await fsPromises.writeFile(outputPath, content, "utf8");
22275
22475
  files.push(outputPath);
@@ -22318,6 +22518,7 @@ export {
22318
22518
  adaptJestRun,
22319
22519
  adaptPlaywrightRun,
22320
22520
  adaptVitestRun,
22521
+ advanceState,
22321
22522
  assertValidRun,
22322
22523
  buildCheck,
22323
22524
  buildGoal,
@@ -22330,6 +22531,7 @@ export {
22330
22531
  canonicalizeRun,
22331
22532
  classifyStatusChange,
22332
22533
  clearVersionCache,
22534
+ computeDeltas,
22333
22535
  computeTestMetrics,
22334
22536
  copyMarkdownAssets,
22335
22537
  createPrCommentSummary,
@@ -22352,6 +22554,7 @@ export {
22352
22554
  getEnvironmentDrift,
22353
22555
  gradeEvidence,
22354
22556
  hasSufficientHistory,
22557
+ injectLiveBits,
22355
22558
  isReviewableSource,
22356
22559
  isTestFile,
22357
22560
  joinNameAndExt,
@@ -22373,7 +22576,9 @@ export {
22373
22576
  readPackageVersion,
22374
22577
  recordDeployment,
22375
22578
  regenerateArtifacts,
22579
+ regenerateRun,
22376
22580
  renderCheck,
22581
+ renderDeltaStrip,
22377
22582
  renderGoal,
22378
22583
  renderTriage,
22379
22584
  resolveAttachment,
@@ -22389,6 +22594,7 @@ export {
22389
22594
  sendWebhookNotification,
22390
22595
  signBody,
22391
22596
  slugify,
22597
+ startServe,
22392
22598
  startWatch,
22393
22599
  stripAnsi,
22394
22600
  toBehaviorManifest,