executable-stories-formatters 0.13.0 → 0.15.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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
- import * as fs14 from "fs";
6
- import * as path15 from "path";
5
+ import * as fs16 from "fs";
6
+ import * as path17 from "path";
7
7
 
8
8
  // src/validation/schema-validator.ts
9
9
  import Ajv from "ajv/dist/2020.js";
@@ -588,17 +588,17 @@ function validateRawRun(data) {
588
588
  return { valid: true, errors: [] };
589
589
  }
590
590
  const errors = (validate.errors ?? []).map((err) => {
591
- const path16 = err.instancePath || "/";
591
+ const path18 = err.instancePath || "/";
592
592
  const message = err.message ?? "unknown error";
593
593
  if (err.keyword === "additionalProperties") {
594
594
  const extra = err.params.additionalProperty;
595
- return `${path16}: ${message} \u2014 '${extra}'`;
595
+ return `${path18}: ${message} \u2014 '${extra}'`;
596
596
  }
597
597
  if (err.keyword === "enum") {
598
598
  const allowed = err.params.allowedValues;
599
- return `${path16}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
599
+ return `${path18}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
600
600
  }
601
- return `${path16}: ${message}`;
601
+ return `${path18}: ${message}`;
602
602
  });
603
603
  return { valid: false, errors };
604
604
  }
@@ -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,10 @@ 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,
15868
+ scenarioNoteLink: options.scenarioNoteLink
15866
15869
  };
15867
15870
  }
15868
15871
  /**
@@ -16049,6 +16052,11 @@ var MarkdownFormatter = class {
16049
16052
  * Render a single scenario.
16050
16053
  */
16051
16054
  renderScenario(lines, tc) {
16055
+ const anchorId = this.options.scenarioAnchor?.(tc);
16056
+ if (anchorId) {
16057
+ lines.push(`<a id="${anchorId}"></a>`);
16058
+ lines.push("");
16059
+ }
16052
16060
  if (this.options.customRenderers?.renderScenarioHeader) {
16053
16061
  const custom = this.options.customRenderers.renderScenarioHeader(tc);
16054
16062
  if (custom !== null) {
@@ -16064,6 +16072,14 @@ var MarkdownFormatter = class {
16064
16072
  icon = this.getStatusIcon(tc.status) + " ";
16065
16073
  }
16066
16074
  lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
16075
+ const badge2 = this.options.scenarioBadge?.(tc);
16076
+ if (badge2) {
16077
+ lines.push(badge2);
16078
+ }
16079
+ const noteLink = this.options.scenarioNoteLink?.(tc);
16080
+ if (noteLink) {
16081
+ lines.push(noteLink);
16082
+ }
16067
16083
  if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
16068
16084
  const permalink = this.buildPermalink(tc);
16069
16085
  lines.push(`Source: [${tc.sourceFile}](${permalink})`);
@@ -16138,8 +16154,8 @@ var MarkdownFormatter = class {
16138
16154
  buildPermalink(tc) {
16139
16155
  const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
16140
16156
  const file = tc.sourceFile;
16141
- const line = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16142
- return `${base}/${file}${line}`;
16157
+ const line2 = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16158
+ return `${base}/${file}${line2}`;
16143
16159
  }
16144
16160
  /**
16145
16161
  * Render a step.
@@ -16207,8 +16223,8 @@ var MarkdownFormatter = class {
16207
16223
  lines.push(`${indent}`);
16208
16224
  }
16209
16225
  lines.push(`${indent}\`\`\`${entry.lang ?? ""}`);
16210
- for (const line of (entry.content ?? "").split("\n")) {
16211
- lines.push(`${indent}${line}`);
16226
+ for (const line2 of (entry.content ?? "").split("\n")) {
16227
+ lines.push(`${indent}${line2}`);
16212
16228
  }
16213
16229
  lines.push(`${indent}\`\`\``);
16214
16230
  lines.push(`${indent}`);
@@ -16231,8 +16247,8 @@ var MarkdownFormatter = class {
16231
16247
  case "section":
16232
16248
  lines.push(`${indent}**${entry.title}**`);
16233
16249
  lines.push(`${indent}`);
16234
- for (const line of (entry.markdown ?? "").split("\n")) {
16235
- lines.push(`${indent}${line}`);
16250
+ for (const line2 of (entry.markdown ?? "").split("\n")) {
16251
+ lines.push(`${indent}${line2}`);
16236
16252
  }
16237
16253
  lines.push(`${indent}`);
16238
16254
  break;
@@ -16241,8 +16257,8 @@ var MarkdownFormatter = class {
16241
16257
  lines.push(`${indent}**${entry.title}**`);
16242
16258
  }
16243
16259
  lines.push(`${indent}\`\`\`mermaid`);
16244
- for (const line of (entry.code ?? "").split("\n")) {
16245
- lines.push(`${indent}${line}`);
16260
+ for (const line2 of (entry.code ?? "").split("\n")) {
16261
+ lines.push(`${indent}${line2}`);
16246
16262
  }
16247
16263
  lines.push(`${indent}\`\`\``);
16248
16264
  break;
@@ -16272,8 +16288,8 @@ var MarkdownFormatter = class {
16272
16288
  lines.push(`${indent}<summary>${htmlLabel}</summary>`);
16273
16289
  lines.push(`${indent}`);
16274
16290
  lines.push(`${indent}\`\`\`html`);
16275
- for (const line of (entry.content ?? "").split("\n")) {
16276
- lines.push(`${indent}${line}`);
16291
+ for (const line2 of (entry.content ?? "").split("\n")) {
16292
+ lines.push(`${indent}${line2}`);
16277
16293
  }
16278
16294
  lines.push(`${indent}\`\`\``);
16279
16295
  lines.push(`${indent}`);
@@ -16295,8 +16311,8 @@ var MarkdownFormatter = class {
16295
16311
  lines.push(`${indent}**[${entry.type}]**`);
16296
16312
  lines.push(`${indent}`);
16297
16313
  lines.push(`${indent}\`\`\`json`);
16298
- for (const line of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16299
- lines.push(`${indent}${line}`);
16314
+ for (const line2 of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16315
+ lines.push(`${indent}${line2}`);
16300
16316
  }
16301
16317
  lines.push(`${indent}\`\`\``);
16302
16318
  lines.push(`${indent}`);
@@ -16479,14 +16495,14 @@ var TraceabilityMatrixFormatter = class {
16479
16495
  lines.push("");
16480
16496
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
16481
16497
  if (req.covers.length > 0) {
16482
- lines.push(`Covers: ${req.covers.map((path16) => `\`${path16}\``).join(", ")}`);
16498
+ lines.push(`Covers: ${req.covers.map((path18) => `\`${path18}\``).join(", ")}`);
16483
16499
  }
16484
16500
  lines.push("");
16485
16501
  lines.push("| Status | Scenario | Source | Covers |");
16486
16502
  lines.push("| --- | --- | --- | --- |");
16487
16503
  for (const scenario of req.scenarios) {
16488
16504
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16489
- const covers = scenario.covers.length > 0 ? scenario.covers.map((path16) => `\`${path16}\``).join(", ") : "";
16505
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path18) => `\`${path18}\``).join(", ") : "";
16490
16506
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
16491
16507
  }
16492
16508
  lines.push("");
@@ -16588,8 +16604,8 @@ function extractFeatureName(testCases, uri) {
16588
16604
  return tc.titlePath[0];
16589
16605
  }
16590
16606
  }
16591
- const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16592
- return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16607
+ const basename5 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16608
+ return basename5.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
16593
16609
  }
16594
16610
  function synthesizeFeature(uri, testCases) {
16595
16611
  const featureName = extractFeatureName(testCases, uri);
@@ -16807,60 +16823,60 @@ function buildStepArguments(step, stepLine) {
16807
16823
  }
16808
16824
  return {};
16809
16825
  }
16810
- function docEntryToDocString(doc, line) {
16826
+ function docEntryToDocString(doc, line2) {
16811
16827
  switch (doc.kind) {
16812
16828
  case "code":
16813
16829
  return {
16814
- location: { line },
16830
+ location: { line: line2 },
16815
16831
  mediaType: doc.lang,
16816
16832
  content: doc.content,
16817
16833
  delimiter: '"""'
16818
16834
  };
16819
16835
  case "note":
16820
16836
  return {
16821
- location: { line },
16837
+ location: { line: line2 },
16822
16838
  mediaType: "text/plain",
16823
16839
  content: doc.text,
16824
16840
  delimiter: '"""'
16825
16841
  };
16826
16842
  case "section":
16827
16843
  return {
16828
- location: { line },
16844
+ location: { line: line2 },
16829
16845
  mediaType: "text/markdown",
16830
16846
  content: doc.markdown,
16831
16847
  delimiter: '"""'
16832
16848
  };
16833
16849
  case "mermaid":
16834
16850
  return {
16835
- location: { line },
16851
+ location: { line: line2 },
16836
16852
  mediaType: "text/x-mermaid",
16837
16853
  content: doc.code,
16838
16854
  delimiter: '"""'
16839
16855
  };
16840
16856
  case "kv":
16841
16857
  return {
16842
- location: { line },
16858
+ location: { line: line2 },
16843
16859
  mediaType: "text/plain",
16844
16860
  content: `${doc.label}: ${typeof doc.value === "string" ? doc.value : JSON.stringify(doc.value)}`,
16845
16861
  delimiter: '"""'
16846
16862
  };
16847
16863
  case "link":
16848
16864
  return {
16849
- location: { line },
16865
+ location: { line: line2 },
16850
16866
  mediaType: "text/markdown",
16851
16867
  content: `[${doc.label}](${doc.url})`,
16852
16868
  delimiter: '"""'
16853
16869
  };
16854
16870
  case "custom":
16855
16871
  return {
16856
- location: { line },
16872
+ location: { line: line2 },
16857
16873
  mediaType: "application/json",
16858
16874
  content: JSON.stringify(doc.data, null, 2),
16859
16875
  delimiter: '"""'
16860
16876
  };
16861
16877
  case "tag":
16862
16878
  return {
16863
- location: { line },
16879
+ location: { line: line2 },
16864
16880
  mediaType: "text/plain",
16865
16881
  content: doc.names.map((n) => `@${n}`).join(" "),
16866
16882
  delimiter: '"""'
@@ -16870,18 +16886,18 @@ function docEntryToDocString(doc, line) {
16870
16886
  return void 0;
16871
16887
  }
16872
16888
  }
16873
- function buildDataTable(table2, line) {
16889
+ function buildDataTable(table2, line2) {
16874
16890
  const rows = [];
16875
16891
  rows.push({
16876
- location: { line },
16892
+ location: { line: line2 },
16877
16893
  cells: table2.columns.map((col) => ({
16878
- location: { line },
16894
+ location: { line: line2 },
16879
16895
  value: col
16880
16896
  })),
16881
16897
  id: ""
16882
16898
  });
16883
16899
  for (let r = 0; r < table2.rows.length; r++) {
16884
- const rowLine = line + 1 + r;
16900
+ const rowLine = line2 + 1 + r;
16885
16901
  rows.push({
16886
16902
  location: { line: rowLine },
16887
16903
  cells: table2.rows[r].map((cell) => ({
@@ -16892,7 +16908,7 @@ function buildDataTable(table2, line) {
16892
16908
  });
16893
16909
  }
16894
16910
  return {
16895
- location: { line },
16911
+ location: { line: line2 },
16896
16912
  rows
16897
16913
  };
16898
16914
  }
@@ -17201,8 +17217,8 @@ function extractDocAttachments(step) {
17201
17217
  }
17202
17218
  return attachments;
17203
17219
  }
17204
- function guessMediaType(path16) {
17205
- const lower = path16.toLowerCase();
17220
+ function guessMediaType(path18) {
17221
+ const lower = path18.toLowerCase();
17206
17222
  if (lower.endsWith(".png")) return "image/png";
17207
17223
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
17208
17224
  if (lower.endsWith(".gif")) return "image/gif";
@@ -18444,17 +18460,17 @@ ${body}`;
18444
18460
  return humanizeSourceFile([...sourceFiles][0]) || this.title;
18445
18461
  }
18446
18462
  buildFrontmatter(run) {
18447
- const badge = _AstroFormatter.computeBadge(run.testCases);
18463
+ const badge2 = _AstroFormatter.computeBadge(run.testCases);
18448
18464
  const count2 = run.testCases.length;
18449
- const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
18465
+ const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge2.text.toLowerCase()}`;
18450
18466
  const lines = [
18451
18467
  "---",
18452
18468
  `title: ${yamlScalar(this.deriveTitle(run))}`,
18453
18469
  `description: ${description}`,
18454
18470
  "sidebar:",
18455
18471
  " badge:",
18456
- ` text: ${badge.text}`,
18457
- ` variant: ${badge.variant}`,
18472
+ ` text: ${badge2.text}`,
18473
+ ` variant: ${badge2.variant}`,
18458
18474
  "---"
18459
18475
  ];
18460
18476
  return lines.join("\n");
@@ -18910,12 +18926,16 @@ function groupBy7(items, keyFn) {
18910
18926
  import * as fs5 from "fs";
18911
18927
  import * as path6 from "path";
18912
18928
  var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
18913
- function isLocalPath(src) {
18929
+ function isRemoteRef(src) {
18914
18930
  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);
18931
+ return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
18932
+ }
18933
+ function isAbsoluteRef(src) {
18934
+ const trimmed = src.trim();
18935
+ return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
18936
+ }
18937
+ function isRelativeLocalPath(src) {
18938
+ return !isRemoteRef(src) && !isAbsoluteRef(src);
18919
18939
  }
18920
18940
  function stripCodeContent(markdown) {
18921
18941
  let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
@@ -18931,21 +18951,21 @@ function scanMarkdownAssets(markdown) {
18931
18951
  let match;
18932
18952
  while ((match = mdImageRe.exec(stripped)) !== null) {
18933
18953
  const src = match[1].trim();
18934
- if (isLocalPath(src)) {
18954
+ if (!isRemoteRef(src)) {
18935
18955
  found.add(src);
18936
18956
  }
18937
18957
  }
18938
18958
  const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
18939
18959
  while ((match = htmlSrcRe.exec(stripped)) !== null) {
18940
18960
  const src = match[1].trim();
18941
- if (isLocalPath(src)) {
18961
+ if (!isRemoteRef(src)) {
18942
18962
  found.add(src);
18943
18963
  }
18944
18964
  }
18945
18965
  const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
18946
18966
  while ((match = posterRe.exec(stripped)) !== null) {
18947
18967
  const src = match[1].trim();
18948
- if (isLocalPath(src)) {
18968
+ if (!isRemoteRef(src)) {
18949
18969
  found.add(src);
18950
18970
  }
18951
18971
  }
@@ -18971,48 +18991,21 @@ function isCode(segment) {
18971
18991
  const trimmed = segment.trimStart();
18972
18992
  return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
18973
18993
  }
18994
+ function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
18995
+ if (isRemoteRef(trimmed)) return null;
18996
+ if (pathMap) {
18997
+ const mapped = pathMap.get(trimmed);
18998
+ return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
18999
+ }
19000
+ if (!isRelativeLocalPath(trimmed)) return null;
19001
+ return `${assetsBaseUrl}/${trimmed}`;
19002
+ }
18974
19003
  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;
19004
+ const rewrite = (full, pre, src, post) => {
19005
+ const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
19006
+ return target === null ? full : `${pre}${target}${post}`;
19007
+ };
19008
+ return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
19016
19009
  }
19017
19010
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
19018
19011
  return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
@@ -19029,8 +19022,9 @@ function copyMarkdownAssets(options) {
19029
19022
  const pathMap = /* @__PURE__ */ new Map();
19030
19023
  const missing = [];
19031
19024
  for (const ref of refs) {
19032
- const absPath = path6.resolve(markdownDir, ref);
19025
+ const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
19033
19026
  if (!fs5.existsSync(absPath)) {
19027
+ if (isAbsoluteRef(ref)) continue;
19034
19028
  if (!allowMissing) {
19035
19029
  throw new Error(`Asset not found: ${absPath}`);
19036
19030
  }
@@ -19112,6 +19106,47 @@ function startWatch(options, deps = {}) {
19112
19106
  };
19113
19107
  }
19114
19108
 
19109
+ // src/behavior-diff.ts
19110
+ function classifyStatusChange(baseline, current) {
19111
+ if (baseline === void 0) return "added";
19112
+ if (current === void 0) return "removed";
19113
+ if (baseline === current) return "unchanged";
19114
+ if (baseline === "passed" && current === "failed") return "regressed";
19115
+ if (baseline === "failed" && current === "passed") return "fixed";
19116
+ return "changed";
19117
+ }
19118
+ function scenarioMap(report) {
19119
+ const map = /* @__PURE__ */ new Map();
19120
+ for (const feature of report.features) {
19121
+ for (const scenario of feature.scenarios) {
19122
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
19123
+ }
19124
+ }
19125
+ return map;
19126
+ }
19127
+ function diffStoryReports(baseline, current) {
19128
+ const base = scenarioMap(baseline);
19129
+ const curr = scenarioMap(current);
19130
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
19131
+ const scenarios = ids.map((id) => {
19132
+ const b = base.get(id);
19133
+ const c = curr.get(id);
19134
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
19135
+ const meta = c ?? b;
19136
+ return {
19137
+ id,
19138
+ title: meta.scenario.title,
19139
+ sourceFile: meta.sourceFile,
19140
+ kind,
19141
+ baselineStatus: b?.scenario.status,
19142
+ currentStatus: c?.scenario.status
19143
+ };
19144
+ });
19145
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19146
+ for (const s of scenarios) summary[s.kind] += 1;
19147
+ return { schemaVersion: "1.0", summary, scenarios };
19148
+ }
19149
+
19115
19150
  // src/publishers/confluence.ts
19116
19151
  function parseAdf(adf) {
19117
19152
  let parsed;
@@ -19344,7 +19379,7 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
19344
19379
  // src/converters/ndjson-parser.ts
19345
19380
  function parseNdjson(ndjson) {
19346
19381
  const lines = ndjson.trim().split("\n").filter(Boolean);
19347
- const envelopes = lines.map((line) => JSON.parse(line));
19382
+ const envelopes = lines.map((line2) => JSON.parse(line2));
19348
19383
  return parseEnvelopes(envelopes);
19349
19384
  }
19350
19385
  function parseEnvelopes(envelopes) {
@@ -20717,18 +20752,18 @@ function deriveChangeType(tags) {
20717
20752
  }
20718
20753
  return "unknown";
20719
20754
  }
20720
- function extensionOf(path16) {
20721
- const base = path16.split("/").pop() ?? path16;
20755
+ function extensionOf(path18) {
20756
+ const base = path18.split("/").pop() ?? path18;
20722
20757
  const dot = base.lastIndexOf(".");
20723
20758
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20724
20759
  }
20725
- function isTestFile(path16) {
20726
- return TEST_INFIX.test(path16);
20760
+ function isTestFile(path18) {
20761
+ return TEST_INFIX.test(path18);
20727
20762
  }
20728
- function isReviewableSource(path16) {
20729
- if (isTestFile(path16)) return false;
20730
- if (path16.endsWith(".d.ts")) return false;
20731
- return CODE_EXTENSIONS.has(extensionOf(path16));
20763
+ function isReviewableSource(path18) {
20764
+ if (isTestFile(path18)) return false;
20765
+ if (path18.endsWith(".d.ts")) return false;
20766
+ return CODE_EXTENSIONS.has(extensionOf(path18));
20732
20767
  }
20733
20768
  function testBaseKey(testFile) {
20734
20769
  return testFile.replace(TEST_INFIX, "");
@@ -20832,7 +20867,7 @@ function toClaim(testCase, changedSourcePaths) {
20832
20867
  const { strength, reasons } = gradeEvidence(testCase, audience);
20833
20868
  const key = testBaseKey(testCase.sourceFile);
20834
20869
  const coversFiles = changedSourcePaths.filter(
20835
- (path16) => sourceBaseKey(path16) === key
20870
+ (path18) => sourceBaseKey(path18) === key
20836
20871
  );
20837
20872
  return {
20838
20873
  id: testCase.id,
@@ -21695,7 +21730,10 @@ var ReportGenerator = class {
21695
21730
  permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
21696
21731
  ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
21697
21732
  traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
21698
- customRenderers: options.astro?.markdown?.customRenderers
21733
+ customRenderers: options.astro?.markdown?.customRenderers,
21734
+ scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
21735
+ scenarioBadge: options.astro?.markdown?.scenarioBadge,
21736
+ scenarioNoteLink: options.astro?.markdown?.scenarioNoteLink
21699
21737
  }
21700
21738
  },
21701
21739
  assetMode: options.assetMode ?? "none",
@@ -22032,7 +22070,13 @@ function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22032
22070
  // src/scaffold-doc.ts
22033
22071
  import * as fs10 from "fs";
22034
22072
  import * as path11 from "path";
22035
- var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
22073
+ var TEMPLATES = [
22074
+ "adr",
22075
+ "runbook",
22076
+ "decision-log",
22077
+ "incident",
22078
+ "scenario-note"
22079
+ ];
22036
22080
  function slugify3(input) {
22037
22081
  return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
22038
22082
  }
@@ -22161,6 +22205,30 @@ _How it was fixed._
22161
22205
 
22162
22206
  - [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
22163
22207
  becomes a failing badge.
22208
+ `
22209
+ },
22210
+ "scenario-note": {
22211
+ subdir: "notes",
22212
+ filename: (_slug, ctx) => ctx.scenarioId ?? ctx.slug,
22213
+ content: (ctx) => `---
22214
+ title: 'Business context \u2014 ${ctx.name}'
22215
+ description: 'Stakeholder context for ${ctx.name}'
22216
+ scenarioId: ${ctx.scenarioId}
22217
+ # Link this note back to the scenario it explains so the badge and explorer stay aligned.
22218
+ verifiedBy: [${ctx.scenarioId}]
22219
+ ---
22220
+
22221
+ This page is hand-written commentary for a generated scenario. It is never
22222
+ overwritten by \`build-docs\`.
22223
+
22224
+ ## Why this behavior matters
22225
+
22226
+ _Describe the business rule, policy, customer promise, or operational nuance._
22227
+
22228
+ ## Caveats
22229
+
22230
+ - _What readers should know when this scenario passes_
22231
+ - _Any assumptions, exclusions, or follow-up links_
22164
22232
  `
22165
22233
  }
22166
22234
  };
@@ -22179,10 +22247,15 @@ function scaffoldDoc(options) {
22179
22247
  const today = options.today ?? /* @__PURE__ */ new Date();
22180
22248
  const name = (options.name ?? "").trim() || defaultName(template);
22181
22249
  const slug2 = slugify3(name);
22250
+ const scenarioId = normalizeScenarioId(options.scenarioId);
22182
22251
  const dir = path11.join(baseDir, spec.subdir);
22252
+ if (template === "scenario-note" && !scenarioId) {
22253
+ throw new Error(`Template "scenario-note" requires --scenario-id.`);
22254
+ }
22183
22255
  const ctx = {
22184
22256
  name,
22185
22257
  slug: slug2,
22258
+ scenarioId,
22186
22259
  isoDate: isoDate(today),
22187
22260
  seq: nextSeq(dir)
22188
22261
  };
@@ -22207,6 +22280,8 @@ function defaultName(template) {
22207
22280
  return "Decisions";
22208
22281
  case "incident":
22209
22282
  return "Untitled incident";
22283
+ case "scenario-note":
22284
+ return "Untitled scenario note";
22210
22285
  }
22211
22286
  }
22212
22287
  function titleFor2(template, ctx) {
@@ -22219,12 +22294,43 @@ function titleFor2(template, ctx) {
22219
22294
  return `Decision log \u2014 ${ctx.name}`;
22220
22295
  case "incident":
22221
22296
  return `Incident \u2014 ${ctx.name}`;
22297
+ case "scenario-note":
22298
+ return `Business context \u2014 ${ctx.name}`;
22222
22299
  }
22223
22300
  }
22301
+ function normalizeScenarioId(input) {
22302
+ const value = input?.trim();
22303
+ if (!value) return void 0;
22304
+ if (value.includes("/") || value.includes("\\")) {
22305
+ throw new Error(`scenarioId must not contain path separators.`);
22306
+ }
22307
+ return value;
22308
+ }
22224
22309
 
22225
22310
  // src/check-links.ts
22311
+ import * as fs12 from "fs";
22312
+ import * as path13 from "path";
22313
+
22314
+ // src/utils/markdown-files.ts
22226
22315
  import * as fs11 from "fs";
22227
22316
  import * as path12 from "path";
22317
+ function collectMarkdownFiles(target) {
22318
+ if (!fs11.existsSync(target)) return [];
22319
+ if (fs11.statSync(target).isFile()) return [target];
22320
+ const out = [];
22321
+ const walk = (dir) => {
22322
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22323
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22324
+ const full = path12.join(dir, entry.name);
22325
+ if (entry.isDirectory()) walk(full);
22326
+ else if (/\.mdx?$/u.test(entry.name)) out.push(full);
22327
+ }
22328
+ };
22329
+ walk(target);
22330
+ return out;
22331
+ }
22332
+
22333
+ // src/check-links.ts
22228
22334
  function stripCode(markdown) {
22229
22335
  let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
22230
22336
  out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
@@ -22254,34 +22360,19 @@ function classifyLink(link2) {
22254
22360
  function resolutionCandidates(fromFile, link2) {
22255
22361
  const withoutAnchor = link2.split("#")[0];
22256
22362
  if (!withoutAnchor) return [];
22257
- const base = path12.resolve(path12.dirname(fromFile), withoutAnchor);
22363
+ const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
22258
22364
  const candidates = [base];
22259
- if (!path12.extname(base)) {
22365
+ if (!path13.extname(base)) {
22260
22366
  candidates.push(`${base}.md`, `${base}.mdx`);
22261
- candidates.push(path12.join(base, "index.md"), path12.join(base, "index.mdx"));
22367
+ candidates.push(path13.join(base, "index.md"), path13.join(base, "index.mdx"));
22262
22368
  }
22263
22369
  return candidates;
22264
22370
  }
22265
22371
  function resolvesOnDisk(fromFile, link2) {
22266
22372
  return resolutionCandidates(fromFile, link2).some(
22267
- (candidate) => fs11.existsSync(candidate) && fs11.statSync(candidate).isFile()
22373
+ (candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
22268
22374
  );
22269
22375
  }
22270
- function collectDocFiles(target) {
22271
- const stat = fs11.statSync(target);
22272
- if (stat.isFile()) return [target];
22273
- const out = [];
22274
- const walk = (dir) => {
22275
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22276
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22277
- const full = path12.join(dir, entry.name);
22278
- if (entry.isDirectory()) walk(full);
22279
- else if (/\.mdx?$/.test(entry.name)) out.push(full);
22280
- }
22281
- };
22282
- walk(target);
22283
- return out;
22284
- }
22285
22376
  async function isExternalAlive(url, timeoutMs) {
22286
22377
  const attempt = async (method) => {
22287
22378
  const controller = new AbortController();
@@ -22305,17 +22396,17 @@ async function isExternalAlive(url, timeoutMs) {
22305
22396
  }
22306
22397
  async function checkLinks(options) {
22307
22398
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
22308
- if (!fs11.existsSync(target)) {
22399
+ if (!fs12.existsSync(target)) {
22309
22400
  throw new Error(`Path not found: ${target}`);
22310
22401
  }
22311
- const files = collectDocFiles(target);
22402
+ const files = collectMarkdownFiles(target);
22312
22403
  const broken = [];
22313
22404
  let linksChecked = 0;
22314
22405
  let externalChecked = 0;
22315
22406
  let skipped = 0;
22316
22407
  const externalCache = /* @__PURE__ */ new Map();
22317
22408
  for (const file of files) {
22318
- const content = fs11.readFileSync(file, "utf8");
22409
+ const content = fs12.readFileSync(file, "utf8");
22319
22410
  for (const link2 of extractLinks(content)) {
22320
22411
  const kind = classifyLink(link2);
22321
22412
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -22371,8 +22462,8 @@ function formatLinkReport(report) {
22371
22462
  }
22372
22463
 
22373
22464
  // src/import-openapi.ts
22374
- import * as fs12 from "fs";
22375
- import * as path13 from "path";
22465
+ import * as fs13 from "fs";
22466
+ import * as path14 from "path";
22376
22467
  import { parse as parseYamlString } from "yaml";
22377
22468
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
22378
22469
  function parseYaml(raw, specPath) {
@@ -22385,9 +22476,9 @@ function parseYaml(raw, specPath) {
22385
22476
  }
22386
22477
  }
22387
22478
  function parseSpec(specPath) {
22388
- if (!fs12.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22389
- const raw = fs12.readFileSync(specPath, "utf8");
22390
- const ext = path13.extname(specPath).toLowerCase();
22479
+ if (!fs13.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22480
+ const raw = fs13.readFileSync(specPath, "utf8");
22481
+ const ext = path14.extname(specPath).toLowerCase();
22391
22482
  if (ext === ".json") return JSON.parse(raw);
22392
22483
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
22393
22484
  try {
@@ -22418,8 +22509,8 @@ function extractEndpoints(spec) {
22418
22509
  }
22419
22510
  function loadScenarios(runFile) {
22420
22511
  if (!runFile) return [];
22421
- if (!fs12.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22422
- const report = JSON.parse(fs12.readFileSync(runFile, "utf8"));
22512
+ if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22513
+ const report = JSON.parse(fs13.readFileSync(runFile, "utf8"));
22423
22514
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
22424
22515
  }
22425
22516
  function endpointRefs(endpoint) {
@@ -22526,25 +22617,25 @@ async function importOpenApi(options) {
22526
22617
  list.push(item);
22527
22618
  groups.set(item.endpoint.tag, list);
22528
22619
  }
22529
- const outputDir = options.outputDir ?? path13.join("src", "content", "docs", "api");
22530
- if (fs12.existsSync(outputDir) && !options.force) {
22531
- const entries = fs12.readdirSync(outputDir);
22620
+ const outputDir = options.outputDir ?? path14.join("src", "content", "docs", "api");
22621
+ if (fs13.existsSync(outputDir) && !options.force) {
22622
+ const entries = fs13.readdirSync(outputDir);
22532
22623
  if (entries.length > 0) {
22533
22624
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
22534
22625
  }
22535
22626
  }
22536
- fs12.mkdirSync(outputDir, { recursive: true });
22627
+ fs13.mkdirSync(outputDir, { recursive: true });
22537
22628
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
22538
22629
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
22539
- fs12.writeFileSync(
22540
- path13.join(outputDir, "index.mdx"),
22630
+ fs13.writeFileSync(
22631
+ path14.join(outputDir, "index.mdx"),
22541
22632
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
22542
22633
  "utf8"
22543
22634
  );
22544
22635
  for (const [tag, rows] of groups) {
22545
- const dir = path13.join(outputDir, slug(tag));
22546
- fs12.mkdirSync(dir, { recursive: true });
22547
- fs12.writeFileSync(path13.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22636
+ const dir = path14.join(outputDir, slug(tag));
22637
+ fs13.mkdirSync(dir, { recursive: true });
22638
+ fs13.writeFileSync(path14.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22548
22639
  }
22549
22640
  return {
22550
22641
  outputDir,
@@ -22556,8 +22647,246 @@ async function importOpenApi(options) {
22556
22647
  }
22557
22648
 
22558
22649
  // src/build-docs.ts
22559
- import * as fs13 from "fs";
22560
- import * as path14 from "path";
22650
+ import * as fs15 from "fs";
22651
+ import * as path16 from "path";
22652
+
22653
+ // src/scenario-links.ts
22654
+ function scenarioAnchor(title) {
22655
+ return `scenario-${slugify(title)}`;
22656
+ }
22657
+ function buildScenarioLinks(report, options = {}) {
22658
+ const audienceSplit = options.audienceSplit ?? false;
22659
+ const baseUrl = (options.baseUrl ?? "/stories").replace(/\/$/, "");
22660
+ const scenarios = {};
22661
+ for (const feature of report.features) {
22662
+ const stem = slugify(cleanTestStem(feature.sourceFile));
22663
+ for (const scenario of feature.scenarios) {
22664
+ const audience = deriveAudience(feature.sourceFile, scenario.tags);
22665
+ const url = audienceSplit ? `${baseUrl}/${audience}/${stem}/` : `${baseUrl}/${stem}/`;
22666
+ const anchor = scenarioAnchor(scenario.title);
22667
+ scenarios[scenario.id] = {
22668
+ id: scenario.id,
22669
+ title: scenario.title,
22670
+ audience,
22671
+ status: scenario.status,
22672
+ sourceFile: feature.sourceFile,
22673
+ url,
22674
+ anchor,
22675
+ deepLink: `${url}#${anchor}`
22676
+ };
22677
+ }
22678
+ }
22679
+ return { schemaVersion: "1.0", runId: report.runId, baseUrl, scenarios };
22680
+ }
22681
+
22682
+ // src/changes-page.ts
22683
+ var GROUPS = [
22684
+ { kind: "regressed", heading: "Regressed", icon: "\u26A0\uFE0F" },
22685
+ { kind: "removed", heading: "Removed", icon: "\u{1F5D1}\uFE0F" },
22686
+ { kind: "added", heading: "Added", icon: "\u2728" },
22687
+ { kind: "fixed", heading: "Fixed", icon: "\u2705" },
22688
+ { kind: "changed", heading: "Changed", icon: "\u{1F501}" }
22689
+ ];
22690
+ function yamlScalar2(value) {
22691
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22692
+ return `'${value.replace(/'/g, "''")}'`;
22693
+ }
22694
+ return value;
22695
+ }
22696
+ function badge(summary) {
22697
+ if (summary.regressed > 0 || summary.removed > 0) return { text: "Regressed", variant: "danger" };
22698
+ if (summary.added > 0 || summary.fixed > 0 || summary.changed > 0)
22699
+ return { text: "Updated", variant: "tip" };
22700
+ return { text: "No changes", variant: "note" };
22701
+ }
22702
+ function line(entry, links) {
22703
+ const link2 = links.scenarios[entry.id];
22704
+ const label = link2 ? `[${entry.title}](${link2.deepLink})` : entry.title;
22705
+ const transition = entry.baselineStatus && entry.currentStatus && entry.baselineStatus !== entry.currentStatus ? ` \u2014 \`${entry.baselineStatus}\` \u2192 \`${entry.currentStatus}\`` : "";
22706
+ return `- ${label} \`${entry.sourceFile}\`${transition}`;
22707
+ }
22708
+ function renderChangesPage(diff, links) {
22709
+ const b = badge(diff.summary);
22710
+ const { added, removed, regressed, fixed, changed } = diff.summary;
22711
+ const totalChanged = added + removed + regressed + fixed + changed;
22712
+ const frontmatter = [
22713
+ "---",
22714
+ "title: What's changed",
22715
+ `description: ${yamlScalar2(
22716
+ totalChanged === 0 ? "No behavioural changes since the baseline" : `${totalChanged} scenario${totalChanged !== 1 ? "s" : ""} changed since the baseline`
22717
+ )}`,
22718
+ "sidebar:",
22719
+ " order: 0",
22720
+ " badge:",
22721
+ ` text: ${b.text}`,
22722
+ ` variant: ${b.variant}`,
22723
+ "---"
22724
+ ].join("\n");
22725
+ const body = [];
22726
+ if (totalChanged === 0) {
22727
+ body.push("No behavioural changes since the baseline run. \u2705");
22728
+ } else {
22729
+ body.push(
22730
+ `**${regressed}** regressed \xB7 **${removed}** removed \xB7 **${added}** added \xB7 **${fixed}** fixed \xB7 **${changed}** changed`,
22731
+ ""
22732
+ );
22733
+ for (const group of GROUPS) {
22734
+ const entries = diff.scenarios.filter((s) => s.kind === group.kind);
22735
+ if (entries.length === 0) continue;
22736
+ body.push(`## ${group.icon} ${group.heading} (${entries.length})`, "");
22737
+ for (const entry of entries) body.push(line(entry, links));
22738
+ body.push("");
22739
+ }
22740
+ }
22741
+ return `${frontmatter}
22742
+
22743
+ ${body.join("\n")}
22744
+ `;
22745
+ }
22746
+
22747
+ // src/notes-index.ts
22748
+ import * as fs14 from "fs";
22749
+ import * as path15 from "path";
22750
+ import { slug as githubSlug } from "github-slugger";
22751
+ import { parse as parseYaml2 } from "yaml";
22752
+ function buildScenarioNotesIndex(notesDir) {
22753
+ const entries = collectMarkdownFiles(notesDir).map((filePath) => readScenarioNote(filePath, notesDir)).filter((entry) => entry !== null).sort((a, b) => a.slug.localeCompare(b.slug));
22754
+ return {
22755
+ schemaVersion: "1.0",
22756
+ notes: entries
22757
+ };
22758
+ }
22759
+ function writeNotesIndex(index, outPath) {
22760
+ fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22761
+ fs14.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
22762
+ return index;
22763
+ }
22764
+ function notesByScenarioId(index) {
22765
+ const map = /* @__PURE__ */ new Map();
22766
+ for (const note of index.notes) {
22767
+ if (!map.has(note.scenarioId)) map.set(note.scenarioId, note);
22768
+ }
22769
+ return map;
22770
+ }
22771
+ function noteHref(note) {
22772
+ return `/notes/${note.slug}/`;
22773
+ }
22774
+ function noteLinkMarkdown(note) {
22775
+ return `[Business context \u2192](${noteHref(note)})`;
22776
+ }
22777
+ function readScenarioNote(filePath, notesDir) {
22778
+ const relative5 = path15.relative(notesDir, filePath);
22779
+ const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
22780
+ const frontmatter = parseFrontmatter(fs14.readFileSync(filePath, "utf8"));
22781
+ const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path15.basename(stem);
22782
+ const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
22783
+ return {
22784
+ scenarioId,
22785
+ slug: toRouteSlug(stem),
22786
+ title
22787
+ };
22788
+ }
22789
+ function parseFrontmatter(source) {
22790
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
22791
+ if (!match) return {};
22792
+ const parsed = parseYaml2(match[1]);
22793
+ return parsed && typeof parsed === "object" ? parsed : {};
22794
+ }
22795
+ function toRouteSlug(stem) {
22796
+ return stem.split(path15.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
22797
+ }
22798
+
22799
+ // src/overview-page.ts
22800
+ var AUDIENCE_CARDS = [
22801
+ {
22802
+ key: "engineer",
22803
+ label: "Engineer",
22804
+ icon: "\u{1F527}",
22805
+ blurb: "Unit & integration behaviour \u2014 how the system works under the hood."
22806
+ },
22807
+ {
22808
+ key: "stakeholder",
22809
+ label: "Stakeholder",
22810
+ icon: "\u{1F3AC}",
22811
+ blurb: "End-to-end journeys \u2014 what the product does, with video and traces."
22812
+ }
22813
+ ];
22814
+ var STATUS_ICON = {
22815
+ passed: "\u2705",
22816
+ failed: "\u274C",
22817
+ skipped: "\u23ED\uFE0F",
22818
+ pending: "\u{1F6A7}"
22819
+ };
22820
+ function yamlScalar3(value) {
22821
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22822
+ return `'${value.replace(/'/g, "''")}'`;
22823
+ }
22824
+ return value;
22825
+ }
22826
+ function renderOverviewPage(links, notesIndex) {
22827
+ const all = Object.values(links.scenarios);
22828
+ const total = all.length;
22829
+ const passed = all.filter((s) => s.status === "passed").length;
22830
+ const failed = all.filter((s) => s.status === "failed").length;
22831
+ const notesById = notesByScenarioId(notesIndex ?? { schemaVersion: "1.0", notes: [] });
22832
+ const frontmatter = [
22833
+ "---",
22834
+ "title: Stories",
22835
+ `description: ${yamlScalar3(
22836
+ `${total} scenario${total !== 1 ? "s" : ""} \u2014 ${passed} passed, ${failed} failed`
22837
+ )}`,
22838
+ "sidebar:",
22839
+ " order: 1",
22840
+ "---"
22841
+ ].join("\n");
22842
+ const body = [
22843
+ `**${total}** scenarios \xB7 **${passed}** passed \xB7 **${failed}** failed`,
22844
+ ""
22845
+ ];
22846
+ for (const card of AUDIENCE_CARDS) {
22847
+ const scenarios = all.filter((s) => s.audience === card.key);
22848
+ if (scenarios.length === 0) continue;
22849
+ const cardPassed = scenarios.filter((s) => s.status === "passed").length;
22850
+ const cardFailed = scenarios.filter((s) => s.status === "failed").length;
22851
+ const counts = cardFailed > 0 ? `${cardPassed} passed, ${cardFailed} failed` : `${cardPassed} passed`;
22852
+ body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
22853
+ body.push(`${card.blurb}`, "");
22854
+ for (const s of scenariosSorted(scenarios)) {
22855
+ const note = notesById.get(s.id);
22856
+ const noteSuffix = note ? ` \xB7 ${noteLinkMarkdown(note)}` : "";
22857
+ body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})${noteSuffix}`);
22858
+ }
22859
+ body.push("");
22860
+ }
22861
+ return `${frontmatter}
22862
+
22863
+ ${body.join("\n")}
22864
+ `;
22865
+ }
22866
+ function scenariosSorted(scenarios) {
22867
+ return [...scenarios].sort((a, b) => {
22868
+ const aFail = a.status === "failed" ? 0 : 1;
22869
+ const bFail = b.status === "failed" ? 0 : 1;
22870
+ if (aFail !== bFail) return aFail - bFail;
22871
+ return a.title.localeCompare(b.title);
22872
+ });
22873
+ }
22874
+
22875
+ // src/build-docs.ts
22876
+ var AUDIENCES = ["engineer", "stakeholder"];
22877
+ function partitionByAudience(run) {
22878
+ const buckets = {
22879
+ engineer: [],
22880
+ stakeholder: []
22881
+ };
22882
+ for (const tc of run.testCases) {
22883
+ buckets[deriveAudience(tc.sourceFile, tc.tags)].push(tc);
22884
+ }
22885
+ return {
22886
+ engineer: { ...run, testCases: buckets.engineer },
22887
+ stakeholder: { ...run, testCases: buckets.stakeholder }
22888
+ };
22889
+ }
22561
22890
  var BuildDocsError = class extends Error {
22562
22891
  constructor(message, kind) {
22563
22892
  super(message);
@@ -22567,22 +22896,22 @@ var BuildDocsError = class extends Error {
22567
22896
  };
22568
22897
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
22569
22898
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
22570
- if (!fs13.existsSync(reportPath)) return 0;
22571
- const report = JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22899
+ if (!fs15.existsSync(reportPath)) return 0;
22900
+ const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22572
22901
  let copied = 0;
22573
22902
  const bundle = (value) => {
22574
- const rel = copyAsset(path14.resolve(value), assetsDir);
22903
+ const rel = copyAsset(path16.resolve(value), assetsDir);
22575
22904
  copied++;
22576
- return `${baseUrl}/${path14.basename(rel)}`;
22905
+ return `${baseUrl}/${path16.basename(rel)}`;
22577
22906
  };
22578
22907
  const visit = (entries) => {
22579
22908
  for (const entry of entries ?? []) {
22580
22909
  const e = entry;
22581
22910
  if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
22582
- if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
22911
+ if (typeof e.path === "string" && !isRemote(e.path) && fs15.existsSync(e.path)) {
22583
22912
  e.path = bundle(e.path);
22584
22913
  }
22585
- if (typeof e.poster === "string" && !isRemote(e.poster) && fs13.existsSync(e.poster)) {
22914
+ if (typeof e.poster === "string" && !isRemote(e.poster) && fs15.existsSync(e.poster)) {
22586
22915
  e.poster = bundle(e.poster);
22587
22916
  }
22588
22917
  }
@@ -22595,25 +22924,78 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22595
22924
  }
22596
22925
  }
22597
22926
  if (copied > 0) {
22598
- fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22927
+ fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22599
22928
  }
22600
22929
  return copied;
22601
22930
  }
22931
+ var CHANGE_BADGE = {
22932
+ added: "\u{1F195} **New** _since last run_",
22933
+ fixed: "\u2705 **Fixed** _since last run_",
22934
+ regressed: "\u26A0\uFE0F **Regressed** _since last run_"
22935
+ };
22936
+ function scenarioKey(sourceFile, title) {
22937
+ return `${sourceFile}\0${title}`;
22938
+ }
22939
+ function changeBadgeLookup(diff) {
22940
+ if (!diff) return void 0;
22941
+ const byKey = /* @__PURE__ */ new Map();
22942
+ for (const s of diff.scenarios) {
22943
+ const badge2 = CHANGE_BADGE[s.kind];
22944
+ if (badge2) byKey.set(scenarioKey(s.sourceFile, s.title), badge2);
22945
+ }
22946
+ if (byKey.size === 0) return void 0;
22947
+ return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22948
+ }
22949
+ function readStoryReport(reportPath) {
22950
+ if (!fs15.existsSync(reportPath)) return null;
22951
+ try {
22952
+ return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22953
+ } catch {
22954
+ return null;
22955
+ }
22956
+ }
22957
+ function noteLinkLookup(report, notesIndex) {
22958
+ if (!report) return void 0;
22959
+ const noteById = notesByScenarioId(notesIndex);
22960
+ if (noteById.size === 0) return void 0;
22961
+ const idByKey = /* @__PURE__ */ new Map();
22962
+ for (const feature of report.features) {
22963
+ for (const scenario of feature.scenarios) {
22964
+ idByKey.set(scenarioKey(feature.sourceFile, scenario.title), scenario.id);
22965
+ }
22966
+ }
22967
+ return (tc) => {
22968
+ const id = idByKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22969
+ const note = id ? noteById.get(id) : void 0;
22970
+ return note ? noteLinkMarkdown(note) : void 0;
22971
+ };
22972
+ }
22973
+ function writeScenarioLinks(reportPath, outDir, options = {}) {
22974
+ const report = readStoryReport(reportPath);
22975
+ if (!report) return null;
22976
+ const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22977
+ fs15.writeFileSync(
22978
+ path16.join(outDir, "scenario-links.json"),
22979
+ JSON.stringify(index, null, 2),
22980
+ "utf8"
22981
+ );
22982
+ return index;
22983
+ }
22602
22984
  function clearGeneratedPages(dir) {
22603
- if (!fs13.existsSync(dir)) return;
22604
- for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
22605
- const full = path14.join(dir, entry.name);
22985
+ if (!fs15.existsSync(dir)) return;
22986
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
22987
+ const full = path16.join(dir, entry.name);
22606
22988
  if (entry.isDirectory()) {
22607
22989
  clearGeneratedPages(full);
22608
- if (fs13.readdirSync(full).length === 0) fs13.rmdirSync(full);
22990
+ if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
22609
22991
  } else if (/\.mdx?$/.test(entry.name)) {
22610
- fs13.rmSync(full);
22992
+ fs15.rmSync(full);
22611
22993
  }
22612
22994
  }
22613
22995
  }
22614
22996
  function loadCanonicalRun(rawRunPath, synthesize) {
22615
22997
  try {
22616
- const data = JSON.parse(fs13.readFileSync(path14.resolve(rawRunPath), "utf8"));
22998
+ const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
22617
22999
  if (data.schemaVersion !== 1) {
22618
23000
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
22619
23001
  }
@@ -22636,12 +23018,13 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
22636
23018
  }
22637
23019
  }
22638
23020
  async function buildDocs(options) {
22639
- const siteDir = path14.resolve(options.siteDir);
22640
- const storiesPublicDir = path14.join(siteDir, "public", "stories");
22641
- const assetsDir = path14.join(storiesPublicDir, "assets");
22642
- const storyPagesDir = path14.join(siteDir, "src", "content", "docs", "stories");
22643
- const apiDir = path14.join(siteDir, "src", "content", "docs", "api");
22644
- const reportPath = path14.join(storiesPublicDir, "story-report.json");
23021
+ const siteDir = path16.resolve(options.siteDir);
23022
+ const storiesPublicDir = path16.join(siteDir, "public", "stories");
23023
+ const assetsDir = path16.join(storiesPublicDir, "assets");
23024
+ const storyPagesDir = path16.join(siteDir, "src", "content", "docs", "stories");
23025
+ const notesDir = path16.join(siteDir, "src", "content", "docs", "notes");
23026
+ const apiDir = path16.join(siteDir, "src", "content", "docs", "api");
23027
+ const reportPath = path16.join(storiesPublicDir, "story-report.json");
22645
23028
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
22646
23029
  try {
22647
23030
  await new ReportGenerator({
@@ -22649,27 +23032,88 @@ async function buildDocs(options) {
22649
23032
  outputDir: storiesPublicDir,
22650
23033
  outputName: "story-report"
22651
23034
  }).generate(canonical);
23035
+ const currentReport = readStoryReport(reportPath);
23036
+ let diff;
23037
+ if (options.baselinePath) {
23038
+ const baselineResolved = path16.resolve(options.baselinePath);
23039
+ const baseline = readStoryReport(baselineResolved);
23040
+ if (!baseline) {
23041
+ throw new BuildDocsError(
23042
+ `Baseline story-report not found or unreadable: ${baselineResolved}`,
23043
+ "input"
23044
+ );
23045
+ }
23046
+ if (currentReport) diff = diffStoryReports(baseline, currentReport);
23047
+ }
23048
+ const scenarioBadge = changeBadgeLookup(diff);
23049
+ const notesIndex = buildScenarioNotesIndex(notesDir);
23050
+ const scenarioNoteLink = noteLinkLookup(currentReport, notesIndex);
22652
23051
  clearGeneratedPages(storyPagesDir);
22653
- await new ReportGenerator({
23052
+ const genPages = (run, outDir) => new ReportGenerator({
22654
23053
  formats: ["astro"],
22655
- outputDir: storyPagesDir,
23054
+ outputDir: outDir,
22656
23055
  outputName: "index",
22657
23056
  output: { mode: "colocated", colocatedStyle: "flat" },
22658
23057
  assetMode: "copy",
22659
- astro: { assetsDir, assetsBaseUrl: "/stories/assets" }
22660
- }).generate(canonical);
23058
+ astro: {
23059
+ assetsDir,
23060
+ assetsBaseUrl: "/stories/assets",
23061
+ markdown: {
23062
+ // Emit the same anchor scenario-links.json points at, so fragments resolve.
23063
+ scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
23064
+ scenarioBadge,
23065
+ scenarioNoteLink
23066
+ }
23067
+ }
23068
+ }).generate(run);
23069
+ const audiences = { engineer: 0, stakeholder: 0 };
23070
+ if (options.audienceSplit ?? false) {
23071
+ const partitioned = partitionByAudience(canonical);
23072
+ for (const audience of AUDIENCES) {
23073
+ const sub = partitioned[audience];
23074
+ audiences[audience] = sub.testCases.length;
23075
+ if (sub.testCases.length === 0) continue;
23076
+ await genPages(sub, path16.join(storyPagesDir, audience));
23077
+ }
23078
+ } else {
23079
+ await genPages(canonical, storyPagesDir);
23080
+ }
22661
23081
  const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
23082
+ const linksIndex = writeScenarioLinks(reportPath, storiesPublicDir, {
23083
+ audienceSplit: options.audienceSplit ?? false
23084
+ });
23085
+ const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
23086
+ writeNotesIndex(notesIndex, path16.join(storiesPublicDir, "notes-index.json"));
23087
+ const notesIndexed = notesIndex.notes.length;
23088
+ if (linksIndex) {
23089
+ fs15.writeFileSync(
23090
+ path16.join(storyPagesDir, "index.md"),
23091
+ renderOverviewPage(linksIndex, notesIndex),
23092
+ "utf8"
23093
+ );
23094
+ }
23095
+ const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
23096
+ const changesMdPath = path16.join(storyPagesDir, "changes.md");
23097
+ let changes;
23098
+ if (diff && linksIndex) {
23099
+ fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23100
+ fs15.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
23101
+ changes = diff.summary;
23102
+ } else {
23103
+ fs15.rmSync(changesJsonPath, { force: true });
23104
+ fs15.rmSync(changesMdPath, { force: true });
23105
+ }
22662
23106
  let apiPages = 0;
22663
23107
  if (options.openapiPath) {
22664
23108
  const res = await importOpenApi({
22665
- specPath: path14.resolve(options.openapiPath),
23109
+ specPath: path16.resolve(options.openapiPath),
22666
23110
  outputDir: apiDir,
22667
23111
  runFile: reportPath,
22668
23112
  force: true
22669
23113
  });
22670
23114
  apiPages = res.pageCount;
22671
23115
  }
22672
- return { siteDir, bundledAssets, apiPages };
23116
+ return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, notesIndexed, changes };
22673
23117
  } catch (err) {
22674
23118
  if (err instanceof BuildDocsError) throw err;
22675
23119
  throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
@@ -22677,11 +23121,11 @@ async function buildDocs(options) {
22677
23121
  }
22678
23122
 
22679
23123
  // src/config.ts
22680
- import { existsSync as existsSync12 } from "fs";
23124
+ import { existsSync as existsSync13 } from "fs";
22681
23125
  import { resolve as resolve10 } from "path";
22682
23126
  async function loadConfig(configPath) {
22683
23127
  const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
22684
- if (!existsSync12(resolved)) return {};
23128
+ if (!existsSync13(resolved)) return {};
22685
23129
  const mod = await import(resolved);
22686
23130
  const config = mod.default;
22687
23131
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -22739,7 +23183,7 @@ SUBCOMMANDS
22739
23183
  triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
22740
23184
  validate Validate a JSON file against the schema (no output generated)
22741
23185
  init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
22742
- new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
23186
+ new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
22743
23187
  check-links Scan docs for broken internal/external links (CI-friendly exit code)
22744
23188
  import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
22745
23189
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
@@ -23278,20 +23722,20 @@ async function readInput(args) {
23278
23722
  if (args.stdin) {
23279
23723
  return readStdin();
23280
23724
  }
23281
- const filePath = path15.resolve(args.inputFile);
23282
- if (!fs14.existsSync(filePath)) {
23725
+ const filePath = path17.resolve(args.inputFile);
23726
+ if (!fs16.existsSync(filePath)) {
23283
23727
  console.error(`Error: File not found: ${filePath}`);
23284
23728
  process.exit(EXIT_USAGE);
23285
23729
  }
23286
- return fs14.readFileSync(filePath, "utf8");
23730
+ return fs16.readFileSync(filePath, "utf8");
23287
23731
  }
23288
23732
  function readFileInput(filePath) {
23289
- const resolved = path15.resolve(filePath);
23290
- if (!fs14.existsSync(resolved)) {
23733
+ const resolved = path17.resolve(filePath);
23734
+ if (!fs16.existsSync(resolved)) {
23291
23735
  console.error(`Error: File not found: ${resolved}`);
23292
23736
  process.exit(EXIT_USAGE);
23293
23737
  }
23294
- return fs14.readFileSync(resolved, "utf8");
23738
+ return fs16.readFileSync(resolved, "utf8");
23295
23739
  }
23296
23740
  function readStdin() {
23297
23741
  return new Promise((resolve12, reject) => {
@@ -23424,14 +23868,14 @@ function tryNormalizeRunFromText(text2, args) {
23424
23868
  }
23425
23869
  }
23426
23870
  function listBaselineCandidates(currentFile, args) {
23427
- const baselineDir = path15.resolve(args.baselineDir ?? path15.dirname(currentFile));
23428
- const currentResolved = path15.resolve(currentFile);
23429
- if (!fs14.existsSync(baselineDir)) {
23871
+ const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
23872
+ const currentResolved = path17.resolve(currentFile);
23873
+ if (!fs16.existsSync(baselineDir)) {
23430
23874
  console.error(`Error: baseline directory not found: ${baselineDir}`);
23431
23875
  process.exit(EXIT_USAGE);
23432
23876
  }
23433
- const entries = fs14.readdirSync(baselineDir, { withFileTypes: true });
23434
- return entries.filter((entry) => entry.isFile()).map((entry) => path15.join(baselineDir, entry.name)).filter((candidate) => path15.resolve(candidate) !== currentResolved).filter(
23877
+ const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
23878
+ return entries.filter((entry) => entry.isFile()).map((entry) => path17.join(baselineDir, entry.name)).filter((candidate) => path17.resolve(candidate) !== currentResolved).filter(
23435
23879
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
23436
23880
  );
23437
23881
  }
@@ -23439,14 +23883,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
23439
23883
  const candidates = listBaselineCandidates(currentFile, args);
23440
23884
  const comparable = [];
23441
23885
  for (const candidate of candidates) {
23442
- const run = tryNormalizeRunFromText(fs14.readFileSync(candidate, "utf8"), args);
23886
+ const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
23443
23887
  if (run) {
23444
23888
  comparable.push({ file: candidate, run });
23445
23889
  }
23446
23890
  }
23447
23891
  if (comparable.length === 0) {
23448
23892
  console.error(
23449
- `Error: no compatible baseline files found in ${path15.resolve(args.baselineDir ?? path15.dirname(currentFile))}.`
23893
+ `Error: no compatible baseline files found in ${path17.resolve(args.baselineDir ?? path17.dirname(currentFile))}.`
23450
23894
  );
23451
23895
  process.exit(EXIT_USAGE);
23452
23896
  }
@@ -23691,9 +24135,9 @@ async function main() {
23691
24135
  process.exit(EXIT_SCHEMA_VALIDATION);
23692
24136
  }
23693
24137
  if (args.emitCanonical) {
23694
- const outPath = path15.resolve(args.emitCanonical);
23695
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
23696
- fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24138
+ const outPath = path17.resolve(args.emitCanonical);
24139
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24140
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
23697
24141
  }
23698
24142
  try {
23699
24143
  const result = await generateReports(run, args);
@@ -23750,9 +24194,9 @@ ${msg}`);
23750
24194
  }
23751
24195
  const run = data;
23752
24196
  if (args.emitCanonical) {
23753
- const outPath = path15.resolve(args.emitCanonical);
23754
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
23755
- fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24197
+ const outPath = path17.resolve(args.emitCanonical);
24198
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24199
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
23756
24200
  }
23757
24201
  try {
23758
24202
  const result = await generateReports(run, args);
@@ -23808,9 +24252,9 @@ ${msg}`);
23808
24252
  process.exit(EXIT_CANONICAL_VALIDATION);
23809
24253
  }
23810
24254
  if (args.emitCanonical) {
23811
- const outPath = path15.resolve(args.emitCanonical);
23812
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
23813
- fs14.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
24255
+ const outPath = path17.resolve(args.emitCanonical);
24256
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24257
+ fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
23814
24258
  }
23815
24259
  try {
23816
24260
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -23835,9 +24279,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
23835
24279
  const ext = formatter.fileExtension ?? formatName;
23836
24280
  const baseName = args.outputName ?? "report";
23837
24281
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
23838
- const filepath = path15.join(outputDir, filename);
23839
- fs14.mkdirSync(outputDir, { recursive: true });
23840
- fs14.writeFileSync(filepath, content, "utf8");
24282
+ const filepath = path17.join(outputDir, filename);
24283
+ fs16.mkdirSync(outputDir, { recursive: true });
24284
+ fs16.writeFileSync(filepath, content, "utf8");
23841
24285
  console.log(`Generated: ${filepath}`);
23842
24286
  } catch (err) {
23843
24287
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -23887,13 +24331,13 @@ async function dispatchNotifications(run, args) {
23887
24331
  }
23888
24332
  function runHistoryPipeline(run, args) {
23889
24333
  if (!args.historyFile) return;
23890
- const historyPath = path15.resolve(args.historyFile);
24334
+ const historyPath = path17.resolve(args.historyFile);
23891
24335
  const store = loadHistory(
23892
24336
  { filePath: historyPath },
23893
24337
  {
23894
24338
  readFile: (p) => {
23895
24339
  try {
23896
- return fs14.readFileSync(p, "utf8");
24340
+ return fs16.readFileSync(p, "utf8");
23897
24341
  } catch {
23898
24342
  return void 0;
23899
24343
  }
@@ -23906,11 +24350,11 @@ function runHistoryPipeline(run, args) {
23906
24350
  run,
23907
24351
  maxRuns: args.maxHistoryRuns
23908
24352
  });
23909
- const dir = path15.dirname(historyPath);
23910
- fs14.mkdirSync(dir, { recursive: true });
24353
+ const dir = path17.dirname(historyPath);
24354
+ fs16.mkdirSync(dir, { recursive: true });
23911
24355
  saveHistory(
23912
24356
  { filePath: historyPath, store: updated },
23913
- { writeFile: (p, content) => fs14.writeFileSync(p, content, "utf8") }
24357
+ { writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
23914
24358
  );
23915
24359
  let metricsCount = 0;
23916
24360
  for (const testId of Object.keys(updated.tests)) {
@@ -24008,9 +24452,9 @@ function mapStatus(status) {
24008
24452
  function parseNameStatus(text2) {
24009
24453
  const files = [];
24010
24454
  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+/);
24455
+ const line2 = raw.trim();
24456
+ if (!line2) continue;
24457
+ const cols = line2.includes(" ") ? line2.split(" ") : line2.split(/\s+/);
24014
24458
  const status = cols[0];
24015
24459
  if (!status) continue;
24016
24460
  const filePath = /^[RC]/i.test(status) && cols.length >= 3 ? cols[cols.length - 1] : cols[1];
@@ -24058,11 +24502,11 @@ function writeReviewReport(review, args) {
24058
24502
  const outputDir = args.outputDir ?? "reports";
24059
24503
  const baseName = args.outputName ?? "evidence-review";
24060
24504
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
24061
- fs14.mkdirSync(outputDir, { recursive: true });
24062
- const mdPath = path15.join(outputDir, `${baseName}${suffix}.md`);
24063
- const htmlPath = path15.join(outputDir, `${baseName}${suffix}.html`);
24064
- fs14.writeFileSync(mdPath, markdown, "utf8");
24065
- fs14.writeFileSync(htmlPath, html, "utf8");
24505
+ fs16.mkdirSync(outputDir, { recursive: true });
24506
+ const mdPath = path17.join(outputDir, `${baseName}${suffix}.md`);
24507
+ const htmlPath = path17.join(outputDir, `${baseName}${suffix}.html`);
24508
+ fs16.writeFileSync(mdPath, markdown, "utf8");
24509
+ fs16.writeFileSync(htmlPath, html, "utf8");
24066
24510
  return [mdPath, htmlPath];
24067
24511
  }
24068
24512
  function evaluateReviewGate(review, args) {
@@ -24108,9 +24552,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
24108
24552
  function printCompareResult(result, args, startMs) {
24109
24553
  const durationMs = Date.now() - startMs;
24110
24554
  if (result.prSummary && args.prSummaryFile) {
24111
- const outputPath = path15.resolve(args.prSummaryFile);
24112
- fs14.mkdirSync(path15.dirname(outputPath), { recursive: true });
24113
- fs14.writeFileSync(outputPath, result.prSummary, "utf8");
24555
+ const outputPath = path17.resolve(args.prSummaryFile);
24556
+ fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
24557
+ fs16.writeFileSync(outputPath, result.prSummary, "utf8");
24114
24558
  }
24115
24559
  if (args.jsonSummary) {
24116
24560
  console.log(
@@ -24139,13 +24583,13 @@ function printCompareResult(result, args, startMs) {
24139
24583
  }
24140
24584
  }
24141
24585
  function loadReleasePolicy(policyPath) {
24142
- const resolved = path15.resolve(policyPath);
24143
- if (!fs14.existsSync(resolved)) {
24586
+ const resolved = path17.resolve(policyPath);
24587
+ if (!fs16.existsSync(resolved)) {
24144
24588
  console.error(`Error: release policy file not found: ${resolved}`);
24145
24589
  process.exit(EXIT_USAGE);
24146
24590
  }
24147
24591
  try {
24148
- const raw = JSON.parse(fs14.readFileSync(resolved, "utf8"));
24592
+ const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
24149
24593
  return {
24150
24594
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
24151
24595
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -24249,7 +24693,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24249
24693
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24250
24694
  process.exit(EXIT_USAGE);
24251
24695
  }
24252
- if (!fs14.existsSync(inputFile)) {
24696
+ if (!fs16.existsSync(inputFile)) {
24253
24697
  console.error(`Error: file not found: ${inputFile}`);
24254
24698
  process.exit(EXIT_USAGE);
24255
24699
  }
@@ -24277,7 +24721,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24277
24721
  console.error("Error: --title is required when creating a new page");
24278
24722
  process.exit(EXIT_USAGE);
24279
24723
  }
24280
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24724
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24281
24725
  if (dryRun) {
24282
24726
  console.log(
24283
24727
  JSON.stringify(
@@ -24356,7 +24800,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24356
24800
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24357
24801
  process.exit(EXIT_USAGE);
24358
24802
  }
24359
- if (!fs14.existsSync(inputFile)) {
24803
+ if (!fs16.existsSync(inputFile)) {
24360
24804
  console.error(`Error: file not found: ${inputFile}`);
24361
24805
  process.exit(EXIT_USAGE);
24362
24806
  }
@@ -24383,7 +24827,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24383
24827
  process.exit(EXIT_USAGE);
24384
24828
  }
24385
24829
  const mode = modeRaw;
24386
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24830
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24387
24831
  if (dryRun) {
24388
24832
  console.log(
24389
24833
  JSON.stringify(
@@ -24427,14 +24871,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24427
24871
  function runNew(rawArgs) {
24428
24872
  const { values, positionals } = parseArgs({
24429
24873
  args: rawArgs,
24430
- options: { dir: { type: "string" }, force: { type: "boolean", default: false } },
24874
+ options: {
24875
+ dir: { type: "string" },
24876
+ force: { type: "boolean", default: false },
24877
+ "scenario-id": { type: "string" }
24878
+ },
24431
24879
  allowPositionals: true,
24432
24880
  strict: true
24433
24881
  });
24434
24882
  const template = positionals[0];
24435
24883
  const name = positionals.slice(1).join(" ");
24436
24884
  if (!template) {
24437
- console.error(`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--force]`);
24885
+ console.error(
24886
+ `Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
24887
+ );
24438
24888
  console.error(`Templates: ${TEMPLATES.join(", ")}`);
24439
24889
  return EXIT_USAGE;
24440
24890
  }
@@ -24442,6 +24892,7 @@ function runNew(rawArgs) {
24442
24892
  const result = scaffoldDoc({
24443
24893
  template,
24444
24894
  name,
24895
+ scenarioId: values["scenario-id"],
24445
24896
  baseDir: values.dir,
24446
24897
  force: values.force
24447
24898
  });
@@ -24517,7 +24968,9 @@ async function runBuildDocs(rawArgs) {
24517
24968
  options: {
24518
24969
  "site-dir": { type: "string" },
24519
24970
  openapi: { type: "string" },
24520
- "no-synthesize-stories": { type: "boolean", default: false }
24971
+ "no-synthesize-stories": { type: "boolean", default: false },
24972
+ "audience-split": { type: "boolean", default: false },
24973
+ baseline: { type: "string" }
24521
24974
  },
24522
24975
  allowPositionals: true,
24523
24976
  strict: true
@@ -24525,27 +24978,44 @@ async function runBuildDocs(rawArgs) {
24525
24978
  const rawRunPath = positionals[0];
24526
24979
  if (!rawRunPath) {
24527
24980
  console.error(
24528
- `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
24981
+ `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>] [--baseline <prev-story-report.json>] [--audience-split]`
24529
24982
  );
24530
24983
  return EXIT_USAGE;
24531
24984
  }
24985
+ const audienceSplit = values["audience-split"];
24532
24986
  try {
24533
24987
  const result = await buildDocs({
24534
24988
  rawRunPath,
24535
24989
  siteDir: values["site-dir"] ?? ".",
24536
24990
  openapiPath: values.openapi,
24537
- synthesizeStories: !values["no-synthesize-stories"]
24991
+ synthesizeStories: !values["no-synthesize-stories"],
24992
+ audienceSplit,
24993
+ baselinePath: values.baseline
24538
24994
  });
24539
24995
  console.log(`\u2713 Living docs generated in ${result.siteDir}`);
24540
24996
  console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
24541
- console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
24997
+ console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
24998
+ console.log(` \u2022 Note links \u2192 public/stories/notes-index.json (${result.notesIndexed})`);
24999
+ if (audienceSplit) {
25000
+ console.log(
25001
+ ` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
25002
+ );
25003
+ } else {
25004
+ console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
25005
+ }
24542
25006
  if (result.bundledAssets > 0) {
24543
25007
  console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
24544
25008
  }
24545
25009
  if (result.apiPages > 0) {
24546
25010
  console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
24547
25011
  }
24548
- const rel = path15.relative(process.cwd(), result.siteDir) || ".";
25012
+ if (result.changes) {
25013
+ const c = result.changes;
25014
+ console.log(
25015
+ ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
25016
+ );
25017
+ }
25018
+ const rel = path17.relative(process.cwd(), result.siteDir) || ".";
24549
25019
  console.log(`
24550
25020
  Preview: cd ${rel} && npm run dev`);
24551
25021
  return EXIT_SUCCESS;