executable-stories-formatters 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1237,7 +1237,7 @@ var CucumberJsonFormatter = class {
1237
1237
  /**
1238
1238
  * Build a single step.
1239
1239
  */
1240
- buildStep(step, result, line, index, attachments, isLastStep, hasFailedStep) {
1240
+ buildStep(step, result, line2, index, attachments, isLastStep, hasFailedStep) {
1241
1241
  const keyword = this.options.keywordSpacing ? `${step.keyword} ` : step.keyword;
1242
1242
  const stepResult = this.buildStepResult(result);
1243
1243
  const embeddings = this.buildEmbeddings(attachments, result, isLastStep, hasFailedStep);
@@ -1245,7 +1245,7 @@ var CucumberJsonFormatter = class {
1245
1245
  embeddings.push(...screenshotEmbeddings);
1246
1246
  const jsonStep = {
1247
1247
  keyword,
1248
- line,
1248
+ line: line2,
1249
1249
  name: step.text,
1250
1250
  result: stepResult
1251
1251
  };
@@ -15792,8 +15792,8 @@ var JUnitFormatter = class {
15792
15792
  case "section": {
15793
15793
  const lines = [];
15794
15794
  lines.push(`${indent}${entry.title}:`);
15795
- for (const line of entry.markdown.split("\n")) {
15796
- lines.push(`${indent} ${line}`);
15795
+ for (const line2 of entry.markdown.split("\n")) {
15796
+ lines.push(`${indent} ${line2}`);
15797
15797
  }
15798
15798
  return lines.join("\n");
15799
15799
  }
@@ -15802,8 +15802,8 @@ var JUnitFormatter = class {
15802
15802
  if (entry.title) {
15803
15803
  lines.push(`${indent}${entry.title}:`);
15804
15804
  }
15805
- for (const line of entry.code.split("\n")) {
15806
- lines.push(`${indent} ${line}`);
15805
+ for (const line2 of entry.code.split("\n")) {
15806
+ lines.push(`${indent} ${line2}`);
15807
15807
  }
15808
15808
  return lines.join("\n");
15809
15809
  }
@@ -15815,8 +15815,8 @@ var JUnitFormatter = class {
15815
15815
  const dataStr = JSON.stringify(entry.data, null, 2);
15816
15816
  const lines = [];
15817
15817
  lines.push(`${indent}[${entry.type}]:`);
15818
- for (const line of dataStr.split("\n")) {
15819
- lines.push(`${indent} ${line}`);
15818
+ for (const line2 of dataStr.split("\n")) {
15819
+ lines.push(`${indent} ${line2}`);
15820
15820
  }
15821
15821
  return lines.join("\n");
15822
15822
  }
@@ -15862,7 +15862,9 @@ var MarkdownFormatter = class {
15862
15862
  ticketUrlTemplate: options.ticketUrlTemplate,
15863
15863
  traceUrlTemplate: options.traceUrlTemplate,
15864
15864
  includeSourceLinks: options.includeSourceLinks ?? true,
15865
- customRenderers: options.customRenderers
15865
+ customRenderers: options.customRenderers,
15866
+ scenarioAnchor: options.scenarioAnchor,
15867
+ scenarioBadge: options.scenarioBadge
15866
15868
  };
15867
15869
  }
15868
15870
  /**
@@ -16049,6 +16051,11 @@ var MarkdownFormatter = class {
16049
16051
  * Render a single scenario.
16050
16052
  */
16051
16053
  renderScenario(lines, tc) {
16054
+ const anchorId = this.options.scenarioAnchor?.(tc);
16055
+ if (anchorId) {
16056
+ lines.push(`<a id="${anchorId}"></a>`);
16057
+ lines.push("");
16058
+ }
16052
16059
  if (this.options.customRenderers?.renderScenarioHeader) {
16053
16060
  const custom = this.options.customRenderers.renderScenarioHeader(tc);
16054
16061
  if (custom !== null) {
@@ -16064,6 +16071,10 @@ var MarkdownFormatter = class {
16064
16071
  icon = this.getStatusIcon(tc.status) + " ";
16065
16072
  }
16066
16073
  lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
16074
+ const badge2 = this.options.scenarioBadge?.(tc);
16075
+ if (badge2) {
16076
+ lines.push(badge2);
16077
+ }
16067
16078
  if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
16068
16079
  const permalink = this.buildPermalink(tc);
16069
16080
  lines.push(`Source: [${tc.sourceFile}](${permalink})`);
@@ -16138,8 +16149,8 @@ var MarkdownFormatter = class {
16138
16149
  buildPermalink(tc) {
16139
16150
  const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
16140
16151
  const file = tc.sourceFile;
16141
- const line = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16142
- return `${base}/${file}${line}`;
16152
+ const line2 = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16153
+ return `${base}/${file}${line2}`;
16143
16154
  }
16144
16155
  /**
16145
16156
  * Render a step.
@@ -16207,8 +16218,8 @@ var MarkdownFormatter = class {
16207
16218
  lines.push(`${indent}`);
16208
16219
  }
16209
16220
  lines.push(`${indent}\`\`\`${entry.lang ?? ""}`);
16210
- for (const line of (entry.content ?? "").split("\n")) {
16211
- lines.push(`${indent}${line}`);
16221
+ for (const line2 of (entry.content ?? "").split("\n")) {
16222
+ lines.push(`${indent}${line2}`);
16212
16223
  }
16213
16224
  lines.push(`${indent}\`\`\``);
16214
16225
  lines.push(`${indent}`);
@@ -16231,8 +16242,8 @@ var MarkdownFormatter = class {
16231
16242
  case "section":
16232
16243
  lines.push(`${indent}**${entry.title}**`);
16233
16244
  lines.push(`${indent}`);
16234
- for (const line of (entry.markdown ?? "").split("\n")) {
16235
- lines.push(`${indent}${line}`);
16245
+ for (const line2 of (entry.markdown ?? "").split("\n")) {
16246
+ lines.push(`${indent}${line2}`);
16236
16247
  }
16237
16248
  lines.push(`${indent}`);
16238
16249
  break;
@@ -16241,8 +16252,8 @@ var MarkdownFormatter = class {
16241
16252
  lines.push(`${indent}**${entry.title}**`);
16242
16253
  }
16243
16254
  lines.push(`${indent}\`\`\`mermaid`);
16244
- for (const line of (entry.code ?? "").split("\n")) {
16245
- lines.push(`${indent}${line}`);
16255
+ for (const line2 of (entry.code ?? "").split("\n")) {
16256
+ lines.push(`${indent}${line2}`);
16246
16257
  }
16247
16258
  lines.push(`${indent}\`\`\``);
16248
16259
  break;
@@ -16272,8 +16283,8 @@ var MarkdownFormatter = class {
16272
16283
  lines.push(`${indent}<summary>${htmlLabel}</summary>`);
16273
16284
  lines.push(`${indent}`);
16274
16285
  lines.push(`${indent}\`\`\`html`);
16275
- for (const line of (entry.content ?? "").split("\n")) {
16276
- lines.push(`${indent}${line}`);
16286
+ for (const line2 of (entry.content ?? "").split("\n")) {
16287
+ lines.push(`${indent}${line2}`);
16277
16288
  }
16278
16289
  lines.push(`${indent}\`\`\``);
16279
16290
  lines.push(`${indent}`);
@@ -16295,8 +16306,8 @@ var MarkdownFormatter = class {
16295
16306
  lines.push(`${indent}**[${entry.type}]**`);
16296
16307
  lines.push(`${indent}`);
16297
16308
  lines.push(`${indent}\`\`\`json`);
16298
- for (const line of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16299
- lines.push(`${indent}${line}`);
16309
+ for (const line2 of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16310
+ lines.push(`${indent}${line2}`);
16300
16311
  }
16301
16312
  lines.push(`${indent}\`\`\``);
16302
16313
  lines.push(`${indent}`);
@@ -16807,60 +16818,60 @@ function buildStepArguments(step, stepLine) {
16807
16818
  }
16808
16819
  return {};
16809
16820
  }
16810
- function docEntryToDocString(doc, line) {
16821
+ function docEntryToDocString(doc, line2) {
16811
16822
  switch (doc.kind) {
16812
16823
  case "code":
16813
16824
  return {
16814
- location: { line },
16825
+ location: { line: line2 },
16815
16826
  mediaType: doc.lang,
16816
16827
  content: doc.content,
16817
16828
  delimiter: '"""'
16818
16829
  };
16819
16830
  case "note":
16820
16831
  return {
16821
- location: { line },
16832
+ location: { line: line2 },
16822
16833
  mediaType: "text/plain",
16823
16834
  content: doc.text,
16824
16835
  delimiter: '"""'
16825
16836
  };
16826
16837
  case "section":
16827
16838
  return {
16828
- location: { line },
16839
+ location: { line: line2 },
16829
16840
  mediaType: "text/markdown",
16830
16841
  content: doc.markdown,
16831
16842
  delimiter: '"""'
16832
16843
  };
16833
16844
  case "mermaid":
16834
16845
  return {
16835
- location: { line },
16846
+ location: { line: line2 },
16836
16847
  mediaType: "text/x-mermaid",
16837
16848
  content: doc.code,
16838
16849
  delimiter: '"""'
16839
16850
  };
16840
16851
  case "kv":
16841
16852
  return {
16842
- location: { line },
16853
+ location: { line: line2 },
16843
16854
  mediaType: "text/plain",
16844
16855
  content: `${doc.label}: ${typeof doc.value === "string" ? doc.value : JSON.stringify(doc.value)}`,
16845
16856
  delimiter: '"""'
16846
16857
  };
16847
16858
  case "link":
16848
16859
  return {
16849
- location: { line },
16860
+ location: { line: line2 },
16850
16861
  mediaType: "text/markdown",
16851
16862
  content: `[${doc.label}](${doc.url})`,
16852
16863
  delimiter: '"""'
16853
16864
  };
16854
16865
  case "custom":
16855
16866
  return {
16856
- location: { line },
16867
+ location: { line: line2 },
16857
16868
  mediaType: "application/json",
16858
16869
  content: JSON.stringify(doc.data, null, 2),
16859
16870
  delimiter: '"""'
16860
16871
  };
16861
16872
  case "tag":
16862
16873
  return {
16863
- location: { line },
16874
+ location: { line: line2 },
16864
16875
  mediaType: "text/plain",
16865
16876
  content: doc.names.map((n) => `@${n}`).join(" "),
16866
16877
  delimiter: '"""'
@@ -16870,18 +16881,18 @@ function docEntryToDocString(doc, line) {
16870
16881
  return void 0;
16871
16882
  }
16872
16883
  }
16873
- function buildDataTable(table2, line) {
16884
+ function buildDataTable(table2, line2) {
16874
16885
  const rows = [];
16875
16886
  rows.push({
16876
- location: { line },
16887
+ location: { line: line2 },
16877
16888
  cells: table2.columns.map((col) => ({
16878
- location: { line },
16889
+ location: { line: line2 },
16879
16890
  value: col
16880
16891
  })),
16881
16892
  id: ""
16882
16893
  });
16883
16894
  for (let r = 0; r < table2.rows.length; r++) {
16884
- const rowLine = line + 1 + r;
16895
+ const rowLine = line2 + 1 + r;
16885
16896
  rows.push({
16886
16897
  location: { line: rowLine },
16887
16898
  cells: table2.rows[r].map((cell) => ({
@@ -16892,7 +16903,7 @@ function buildDataTable(table2, line) {
16892
16903
  });
16893
16904
  }
16894
16905
  return {
16895
- location: { line },
16906
+ location: { line: line2 },
16896
16907
  rows
16897
16908
  };
16898
16909
  }
@@ -18444,17 +18455,17 @@ ${body}`;
18444
18455
  return humanizeSourceFile([...sourceFiles][0]) || this.title;
18445
18456
  }
18446
18457
  buildFrontmatter(run) {
18447
- const badge = _AstroFormatter.computeBadge(run.testCases);
18458
+ const badge2 = _AstroFormatter.computeBadge(run.testCases);
18448
18459
  const count2 = run.testCases.length;
18449
- const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
18460
+ const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge2.text.toLowerCase()}`;
18450
18461
  const lines = [
18451
18462
  "---",
18452
18463
  `title: ${yamlScalar(this.deriveTitle(run))}`,
18453
18464
  `description: ${description}`,
18454
18465
  "sidebar:",
18455
18466
  " badge:",
18456
- ` text: ${badge.text}`,
18457
- ` variant: ${badge.variant}`,
18467
+ ` text: ${badge2.text}`,
18468
+ ` variant: ${badge2.variant}`,
18458
18469
  "---"
18459
18470
  ];
18460
18471
  return lines.join("\n");
@@ -18910,12 +18921,16 @@ function groupBy7(items, keyFn) {
18910
18921
  import * as fs5 from "fs";
18911
18922
  import * as path6 from "path";
18912
18923
  var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
18913
- function isLocalPath(src) {
18924
+ function isRemoteRef(src) {
18914
18925
  const trimmed = src.trim();
18915
- if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
18916
- return false;
18917
- }
18918
- return !path6.posix.isAbsolute(trimmed) && !path6.win32.isAbsolute(trimmed);
18926
+ return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
18927
+ }
18928
+ function isAbsoluteRef(src) {
18929
+ const trimmed = src.trim();
18930
+ return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
18931
+ }
18932
+ function isRelativeLocalPath(src) {
18933
+ return !isRemoteRef(src) && !isAbsoluteRef(src);
18919
18934
  }
18920
18935
  function stripCodeContent(markdown) {
18921
18936
  let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
@@ -18931,21 +18946,21 @@ function scanMarkdownAssets(markdown) {
18931
18946
  let match;
18932
18947
  while ((match = mdImageRe.exec(stripped)) !== null) {
18933
18948
  const src = match[1].trim();
18934
- if (isLocalPath(src)) {
18949
+ if (!isRemoteRef(src)) {
18935
18950
  found.add(src);
18936
18951
  }
18937
18952
  }
18938
18953
  const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
18939
18954
  while ((match = htmlSrcRe.exec(stripped)) !== null) {
18940
18955
  const src = match[1].trim();
18941
- if (isLocalPath(src)) {
18956
+ if (!isRemoteRef(src)) {
18942
18957
  found.add(src);
18943
18958
  }
18944
18959
  }
18945
18960
  const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
18946
18961
  while ((match = posterRe.exec(stripped)) !== null) {
18947
18962
  const src = match[1].trim();
18948
- if (isLocalPath(src)) {
18963
+ if (!isRemoteRef(src)) {
18949
18964
  found.add(src);
18950
18965
  }
18951
18966
  }
@@ -18971,48 +18986,21 @@ function isCode(segment) {
18971
18986
  const trimmed = segment.trimStart();
18972
18987
  return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
18973
18988
  }
18989
+ function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
18990
+ if (isRemoteRef(trimmed)) return null;
18991
+ if (pathMap) {
18992
+ const mapped = pathMap.get(trimmed);
18993
+ return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
18994
+ }
18995
+ if (!isRelativeLocalPath(trimmed)) return null;
18996
+ return `${assetsBaseUrl}/${trimmed}`;
18997
+ }
18974
18998
  function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
18975
- let result = prose;
18976
- result = result.replace(
18977
- /(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
18978
- (full, pre, src, post) => {
18979
- const trimmed = src.trim();
18980
- if (!isLocalPath(trimmed)) return full;
18981
- if (pathMap) {
18982
- const mapped = pathMap.get(trimmed);
18983
- if (mapped === void 0) return full;
18984
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18985
- }
18986
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18987
- }
18988
- );
18989
- result = result.replace(
18990
- /(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
18991
- (full, pre, src, post) => {
18992
- const trimmed = src.trim();
18993
- if (!isLocalPath(trimmed)) return full;
18994
- if (pathMap) {
18995
- const mapped = pathMap.get(trimmed);
18996
- if (mapped === void 0) return full;
18997
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18998
- }
18999
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
19000
- }
19001
- );
19002
- result = result.replace(
19003
- /(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
19004
- (full, pre, src, post) => {
19005
- const trimmed = src.trim();
19006
- if (!isLocalPath(trimmed)) return full;
19007
- if (pathMap) {
19008
- const mapped = pathMap.get(trimmed);
19009
- if (mapped === void 0) return full;
19010
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
19011
- }
19012
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
19013
- }
19014
- );
19015
- return result;
18999
+ const rewrite = (full, pre, src, post) => {
19000
+ const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
19001
+ return target === null ? full : `${pre}${target}${post}`;
19002
+ };
19003
+ return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
19016
19004
  }
19017
19005
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
19018
19006
  return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
@@ -19029,8 +19017,9 @@ function copyMarkdownAssets(options) {
19029
19017
  const pathMap = /* @__PURE__ */ new Map();
19030
19018
  const missing = [];
19031
19019
  for (const ref of refs) {
19032
- const absPath = path6.resolve(markdownDir, ref);
19020
+ const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
19033
19021
  if (!fs5.existsSync(absPath)) {
19022
+ if (isAbsoluteRef(ref)) continue;
19034
19023
  if (!allowMissing) {
19035
19024
  throw new Error(`Asset not found: ${absPath}`);
19036
19025
  }
@@ -19112,6 +19101,47 @@ function startWatch(options, deps = {}) {
19112
19101
  };
19113
19102
  }
19114
19103
 
19104
+ // src/behavior-diff.ts
19105
+ function classifyStatusChange(baseline, current) {
19106
+ if (baseline === void 0) return "added";
19107
+ if (current === void 0) return "removed";
19108
+ if (baseline === current) return "unchanged";
19109
+ if (baseline === "passed" && current === "failed") return "regressed";
19110
+ if (baseline === "failed" && current === "passed") return "fixed";
19111
+ return "changed";
19112
+ }
19113
+ function scenarioMap(report) {
19114
+ const map = /* @__PURE__ */ new Map();
19115
+ for (const feature of report.features) {
19116
+ for (const scenario of feature.scenarios) {
19117
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
19118
+ }
19119
+ }
19120
+ return map;
19121
+ }
19122
+ function diffStoryReports(baseline, current) {
19123
+ const base = scenarioMap(baseline);
19124
+ const curr = scenarioMap(current);
19125
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
19126
+ const scenarios = ids.map((id) => {
19127
+ const b = base.get(id);
19128
+ const c = curr.get(id);
19129
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
19130
+ const meta = c ?? b;
19131
+ return {
19132
+ id,
19133
+ title: meta.scenario.title,
19134
+ sourceFile: meta.sourceFile,
19135
+ kind,
19136
+ baselineStatus: b?.scenario.status,
19137
+ currentStatus: c?.scenario.status
19138
+ };
19139
+ });
19140
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19141
+ for (const s of scenarios) summary[s.kind] += 1;
19142
+ return { schemaVersion: "1.0", summary, scenarios };
19143
+ }
19144
+
19115
19145
  // src/publishers/confluence.ts
19116
19146
  function parseAdf(adf) {
19117
19147
  let parsed;
@@ -19344,7 +19374,7 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
19344
19374
  // src/converters/ndjson-parser.ts
19345
19375
  function parseNdjson(ndjson) {
19346
19376
  const lines = ndjson.trim().split("\n").filter(Boolean);
19347
- const envelopes = lines.map((line) => JSON.parse(line));
19377
+ const envelopes = lines.map((line2) => JSON.parse(line2));
19348
19378
  return parseEnvelopes(envelopes);
19349
19379
  }
19350
19380
  function parseEnvelopes(envelopes) {
@@ -21695,7 +21725,9 @@ var ReportGenerator = class {
21695
21725
  permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
21696
21726
  ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
21697
21727
  traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
21698
- customRenderers: options.astro?.markdown?.customRenderers
21728
+ customRenderers: options.astro?.markdown?.customRenderers,
21729
+ scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
21730
+ scenarioBadge: options.astro?.markdown?.scenarioBadge
21699
21731
  }
21700
21732
  },
21701
21733
  assetMode: options.assetMode ?? "none",
@@ -22558,6 +22590,189 @@ async function importOpenApi(options) {
22558
22590
  // src/build-docs.ts
22559
22591
  import * as fs13 from "fs";
22560
22592
  import * as path14 from "path";
22593
+
22594
+ // src/scenario-links.ts
22595
+ function scenarioAnchor(title) {
22596
+ return `scenario-${slugify(title)}`;
22597
+ }
22598
+ function buildScenarioLinks(report, options = {}) {
22599
+ const audienceSplit = options.audienceSplit ?? false;
22600
+ const baseUrl = (options.baseUrl ?? "/stories").replace(/\/$/, "");
22601
+ const scenarios = {};
22602
+ for (const feature of report.features) {
22603
+ const stem = slugify(cleanTestStem(feature.sourceFile));
22604
+ for (const scenario of feature.scenarios) {
22605
+ const audience = deriveAudience(feature.sourceFile, scenario.tags);
22606
+ const url = audienceSplit ? `${baseUrl}/${audience}/${stem}/` : `${baseUrl}/${stem}/`;
22607
+ const anchor = scenarioAnchor(scenario.title);
22608
+ scenarios[scenario.id] = {
22609
+ id: scenario.id,
22610
+ title: scenario.title,
22611
+ audience,
22612
+ status: scenario.status,
22613
+ sourceFile: feature.sourceFile,
22614
+ url,
22615
+ anchor,
22616
+ deepLink: `${url}#${anchor}`
22617
+ };
22618
+ }
22619
+ }
22620
+ return { schemaVersion: "1.0", runId: report.runId, baseUrl, scenarios };
22621
+ }
22622
+
22623
+ // src/changes-page.ts
22624
+ var GROUPS = [
22625
+ { kind: "regressed", heading: "Regressed", icon: "\u26A0\uFE0F" },
22626
+ { kind: "removed", heading: "Removed", icon: "\u{1F5D1}\uFE0F" },
22627
+ { kind: "added", heading: "Added", icon: "\u2728" },
22628
+ { kind: "fixed", heading: "Fixed", icon: "\u2705" },
22629
+ { kind: "changed", heading: "Changed", icon: "\u{1F501}" }
22630
+ ];
22631
+ function yamlScalar2(value) {
22632
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22633
+ return `'${value.replace(/'/g, "''")}'`;
22634
+ }
22635
+ return value;
22636
+ }
22637
+ function badge(summary) {
22638
+ if (summary.regressed > 0 || summary.removed > 0) return { text: "Regressed", variant: "danger" };
22639
+ if (summary.added > 0 || summary.fixed > 0 || summary.changed > 0)
22640
+ return { text: "Updated", variant: "tip" };
22641
+ return { text: "No changes", variant: "note" };
22642
+ }
22643
+ function line(entry, links) {
22644
+ const link2 = links.scenarios[entry.id];
22645
+ const label = link2 ? `[${entry.title}](${link2.deepLink})` : entry.title;
22646
+ const transition = entry.baselineStatus && entry.currentStatus && entry.baselineStatus !== entry.currentStatus ? ` \u2014 \`${entry.baselineStatus}\` \u2192 \`${entry.currentStatus}\`` : "";
22647
+ return `- ${label} \`${entry.sourceFile}\`${transition}`;
22648
+ }
22649
+ function renderChangesPage(diff, links) {
22650
+ const b = badge(diff.summary);
22651
+ const { added, removed, regressed, fixed, changed } = diff.summary;
22652
+ const totalChanged = added + removed + regressed + fixed + changed;
22653
+ const frontmatter = [
22654
+ "---",
22655
+ "title: What's changed",
22656
+ `description: ${yamlScalar2(
22657
+ totalChanged === 0 ? "No behavioural changes since the baseline" : `${totalChanged} scenario${totalChanged !== 1 ? "s" : ""} changed since the baseline`
22658
+ )}`,
22659
+ "sidebar:",
22660
+ " order: 0",
22661
+ " badge:",
22662
+ ` text: ${b.text}`,
22663
+ ` variant: ${b.variant}`,
22664
+ "---"
22665
+ ].join("\n");
22666
+ const body = [];
22667
+ if (totalChanged === 0) {
22668
+ body.push("No behavioural changes since the baseline run. \u2705");
22669
+ } else {
22670
+ body.push(
22671
+ `**${regressed}** regressed \xB7 **${removed}** removed \xB7 **${added}** added \xB7 **${fixed}** fixed \xB7 **${changed}** changed`,
22672
+ ""
22673
+ );
22674
+ for (const group of GROUPS) {
22675
+ const entries = diff.scenarios.filter((s) => s.kind === group.kind);
22676
+ if (entries.length === 0) continue;
22677
+ body.push(`## ${group.icon} ${group.heading} (${entries.length})`, "");
22678
+ for (const entry of entries) body.push(line(entry, links));
22679
+ body.push("");
22680
+ }
22681
+ }
22682
+ return `${frontmatter}
22683
+
22684
+ ${body.join("\n")}
22685
+ `;
22686
+ }
22687
+
22688
+ // src/overview-page.ts
22689
+ var AUDIENCE_CARDS = [
22690
+ {
22691
+ key: "engineer",
22692
+ label: "Engineer",
22693
+ icon: "\u{1F527}",
22694
+ blurb: "Unit & integration behaviour \u2014 how the system works under the hood."
22695
+ },
22696
+ {
22697
+ key: "stakeholder",
22698
+ label: "Stakeholder",
22699
+ icon: "\u{1F3AC}",
22700
+ blurb: "End-to-end journeys \u2014 what the product does, with video and traces."
22701
+ }
22702
+ ];
22703
+ var STATUS_ICON = {
22704
+ passed: "\u2705",
22705
+ failed: "\u274C",
22706
+ skipped: "\u23ED\uFE0F",
22707
+ pending: "\u{1F6A7}"
22708
+ };
22709
+ function yamlScalar3(value) {
22710
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22711
+ return `'${value.replace(/'/g, "''")}'`;
22712
+ }
22713
+ return value;
22714
+ }
22715
+ function renderOverviewPage(links) {
22716
+ const all = Object.values(links.scenarios);
22717
+ const total = all.length;
22718
+ const passed = all.filter((s) => s.status === "passed").length;
22719
+ const failed = all.filter((s) => s.status === "failed").length;
22720
+ const frontmatter = [
22721
+ "---",
22722
+ "title: Stories",
22723
+ `description: ${yamlScalar3(
22724
+ `${total} scenario${total !== 1 ? "s" : ""} \u2014 ${passed} passed, ${failed} failed`
22725
+ )}`,
22726
+ "sidebar:",
22727
+ " order: 1",
22728
+ "---"
22729
+ ].join("\n");
22730
+ const body = [
22731
+ `**${total}** scenarios \xB7 **${passed}** passed \xB7 **${failed}** failed`,
22732
+ ""
22733
+ ];
22734
+ for (const card of AUDIENCE_CARDS) {
22735
+ const scenarios = all.filter((s) => s.audience === card.key);
22736
+ if (scenarios.length === 0) continue;
22737
+ const cardPassed = scenarios.filter((s) => s.status === "passed").length;
22738
+ const cardFailed = scenarios.filter((s) => s.status === "failed").length;
22739
+ const counts = cardFailed > 0 ? `${cardPassed} passed, ${cardFailed} failed` : `${cardPassed} passed`;
22740
+ body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
22741
+ body.push(`${card.blurb}`, "");
22742
+ for (const s of scenariosSorted(scenarios)) {
22743
+ body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})`);
22744
+ }
22745
+ body.push("");
22746
+ }
22747
+ return `${frontmatter}
22748
+
22749
+ ${body.join("\n")}
22750
+ `;
22751
+ }
22752
+ function scenariosSorted(scenarios) {
22753
+ return [...scenarios].sort((a, b) => {
22754
+ const aFail = a.status === "failed" ? 0 : 1;
22755
+ const bFail = b.status === "failed" ? 0 : 1;
22756
+ if (aFail !== bFail) return aFail - bFail;
22757
+ return a.title.localeCompare(b.title);
22758
+ });
22759
+ }
22760
+
22761
+ // src/build-docs.ts
22762
+ var AUDIENCES = ["engineer", "stakeholder"];
22763
+ function partitionByAudience(run) {
22764
+ const buckets = {
22765
+ engineer: [],
22766
+ stakeholder: []
22767
+ };
22768
+ for (const tc of run.testCases) {
22769
+ buckets[deriveAudience(tc.sourceFile, tc.tags)].push(tc);
22770
+ }
22771
+ return {
22772
+ engineer: { ...run, testCases: buckets.engineer },
22773
+ stakeholder: { ...run, testCases: buckets.stakeholder }
22774
+ };
22775
+ }
22561
22776
  var BuildDocsError = class extends Error {
22562
22777
  constructor(message, kind) {
22563
22778
  super(message);
@@ -22599,6 +22814,40 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22599
22814
  }
22600
22815
  return copied;
22601
22816
  }
22817
+ var CHANGE_BADGE = {
22818
+ added: "\u{1F195} **New** _since last run_",
22819
+ fixed: "\u2705 **Fixed** _since last run_",
22820
+ regressed: "\u26A0\uFE0F **Regressed** _since last run_"
22821
+ };
22822
+ function changeBadgeLookup(diff) {
22823
+ if (!diff) return void 0;
22824
+ const byKey = /* @__PURE__ */ new Map();
22825
+ for (const s of diff.scenarios) {
22826
+ const badge2 = CHANGE_BADGE[s.kind];
22827
+ if (badge2) byKey.set(`${s.sourceFile}\0${s.title}`, badge2);
22828
+ }
22829
+ if (byKey.size === 0) return void 0;
22830
+ return (tc) => byKey.get(`${tc.sourceFile}\0${tc.story.scenario}`);
22831
+ }
22832
+ function readStoryReport(reportPath) {
22833
+ if (!fs13.existsSync(reportPath)) return null;
22834
+ try {
22835
+ return JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22836
+ } catch {
22837
+ return null;
22838
+ }
22839
+ }
22840
+ function writeScenarioLinks(reportPath, outDir, options = {}) {
22841
+ const report = readStoryReport(reportPath);
22842
+ if (!report) return null;
22843
+ const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22844
+ fs13.writeFileSync(
22845
+ path14.join(outDir, "scenario-links.json"),
22846
+ JSON.stringify(index, null, 2),
22847
+ "utf8"
22848
+ );
22849
+ return index;
22850
+ }
22602
22851
  function clearGeneratedPages(dir) {
22603
22852
  if (!fs13.existsSync(dir)) return;
22604
22853
  for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
@@ -22649,16 +22898,72 @@ async function buildDocs(options) {
22649
22898
  outputDir: storiesPublicDir,
22650
22899
  outputName: "story-report"
22651
22900
  }).generate(canonical);
22901
+ let diff;
22902
+ if (options.baselinePath) {
22903
+ const baselineResolved = path14.resolve(options.baselinePath);
22904
+ const baseline = readStoryReport(baselineResolved);
22905
+ if (!baseline) {
22906
+ throw new BuildDocsError(
22907
+ `Baseline story-report not found or unreadable: ${baselineResolved}`,
22908
+ "input"
22909
+ );
22910
+ }
22911
+ const current = readStoryReport(reportPath);
22912
+ if (current) diff = diffStoryReports(baseline, current);
22913
+ }
22914
+ const scenarioBadge = changeBadgeLookup(diff);
22652
22915
  clearGeneratedPages(storyPagesDir);
22653
- await new ReportGenerator({
22916
+ const genPages = (run, outDir) => new ReportGenerator({
22654
22917
  formats: ["astro"],
22655
- outputDir: storyPagesDir,
22918
+ outputDir: outDir,
22656
22919
  outputName: "index",
22657
22920
  output: { mode: "colocated", colocatedStyle: "flat" },
22658
22921
  assetMode: "copy",
22659
- astro: { assetsDir, assetsBaseUrl: "/stories/assets" }
22660
- }).generate(canonical);
22922
+ astro: {
22923
+ assetsDir,
22924
+ assetsBaseUrl: "/stories/assets",
22925
+ markdown: {
22926
+ // Emit the same anchor scenario-links.json points at, so fragments resolve.
22927
+ scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
22928
+ scenarioBadge
22929
+ }
22930
+ }
22931
+ }).generate(run);
22932
+ const audiences = { engineer: 0, stakeholder: 0 };
22933
+ if (options.audienceSplit ?? false) {
22934
+ const partitioned = partitionByAudience(canonical);
22935
+ for (const audience of AUDIENCES) {
22936
+ const sub = partitioned[audience];
22937
+ audiences[audience] = sub.testCases.length;
22938
+ if (sub.testCases.length === 0) continue;
22939
+ await genPages(sub, path14.join(storyPagesDir, audience));
22940
+ }
22941
+ } else {
22942
+ await genPages(canonical, storyPagesDir);
22943
+ }
22661
22944
  const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
22945
+ const linksIndex = writeScenarioLinks(reportPath, storiesPublicDir, {
22946
+ audienceSplit: options.audienceSplit ?? false
22947
+ });
22948
+ const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
22949
+ if (linksIndex) {
22950
+ fs13.writeFileSync(
22951
+ path14.join(storyPagesDir, "index.md"),
22952
+ renderOverviewPage(linksIndex),
22953
+ "utf8"
22954
+ );
22955
+ }
22956
+ const changesJsonPath = path14.join(storiesPublicDir, "changes.json");
22957
+ const changesMdPath = path14.join(storyPagesDir, "changes.md");
22958
+ let changes;
22959
+ if (diff && linksIndex) {
22960
+ fs13.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
22961
+ fs13.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
22962
+ changes = diff.summary;
22963
+ } else {
22964
+ fs13.rmSync(changesJsonPath, { force: true });
22965
+ fs13.rmSync(changesMdPath, { force: true });
22966
+ }
22662
22967
  let apiPages = 0;
22663
22968
  if (options.openapiPath) {
22664
22969
  const res = await importOpenApi({
@@ -22669,7 +22974,7 @@ async function buildDocs(options) {
22669
22974
  });
22670
22975
  apiPages = res.pageCount;
22671
22976
  }
22672
- return { siteDir, bundledAssets, apiPages };
22977
+ return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
22673
22978
  } catch (err) {
22674
22979
  if (err instanceof BuildDocsError) throw err;
22675
22980
  throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
@@ -24008,9 +24313,9 @@ function mapStatus(status) {
24008
24313
  function parseNameStatus(text2) {
24009
24314
  const files = [];
24010
24315
  for (const raw of text2.split("\n")) {
24011
- const line = raw.trim();
24012
- if (!line) continue;
24013
- const cols = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
24316
+ const line2 = raw.trim();
24317
+ if (!line2) continue;
24318
+ const cols = line2.includes(" ") ? line2.split(" ") : line2.split(/\s+/);
24014
24319
  const status = cols[0];
24015
24320
  if (!status) continue;
24016
24321
  const filePath = /^[RC]/i.test(status) && cols.length >= 3 ? cols[cols.length - 1] : cols[1];
@@ -24517,7 +24822,9 @@ async function runBuildDocs(rawArgs) {
24517
24822
  options: {
24518
24823
  "site-dir": { type: "string" },
24519
24824
  openapi: { type: "string" },
24520
- "no-synthesize-stories": { type: "boolean", default: false }
24825
+ "no-synthesize-stories": { type: "boolean", default: false },
24826
+ "audience-split": { type: "boolean", default: false },
24827
+ baseline: { type: "string" }
24521
24828
  },
24522
24829
  allowPositionals: true,
24523
24830
  strict: true
@@ -24525,26 +24832,42 @@ async function runBuildDocs(rawArgs) {
24525
24832
  const rawRunPath = positionals[0];
24526
24833
  if (!rawRunPath) {
24527
24834
  console.error(
24528
- `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
24835
+ `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>] [--baseline <prev-story-report.json>] [--audience-split]`
24529
24836
  );
24530
24837
  return EXIT_USAGE;
24531
24838
  }
24839
+ const audienceSplit = values["audience-split"];
24532
24840
  try {
24533
24841
  const result = await buildDocs({
24534
24842
  rawRunPath,
24535
24843
  siteDir: values["site-dir"] ?? ".",
24536
24844
  openapiPath: values.openapi,
24537
- synthesizeStories: !values["no-synthesize-stories"]
24845
+ synthesizeStories: !values["no-synthesize-stories"],
24846
+ audienceSplit,
24847
+ baselinePath: values.baseline
24538
24848
  });
24539
24849
  console.log(`\u2713 Living docs generated in ${result.siteDir}`);
24540
24850
  console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
24541
- console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
24851
+ console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
24852
+ if (audienceSplit) {
24853
+ console.log(
24854
+ ` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
24855
+ );
24856
+ } else {
24857
+ console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
24858
+ }
24542
24859
  if (result.bundledAssets > 0) {
24543
24860
  console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
24544
24861
  }
24545
24862
  if (result.apiPages > 0) {
24546
24863
  console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
24547
24864
  }
24865
+ if (result.changes) {
24866
+ const c = result.changes;
24867
+ console.log(
24868
+ ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
24869
+ );
24870
+ }
24548
24871
  const rel = path15.relative(process.cwd(), result.siteDir) || ".";
24549
24872
  console.log(`
24550
24873
  Preview: cd ${rel} && npm run dev`);