executable-stories-formatters 0.14.0 → 0.15.1

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.
Files changed (42) hide show
  1. package/dist/cli.js +346 -167
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +8 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +9 -0
  6. package/dist/index.d.ts +9 -0
  7. package/dist/index.js +8 -2
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -2
  10. package/schemas/README.md +1 -1
  11. package/templates/astro-starlight/astro.config.mjs +57 -0
  12. package/templates/astro-starlight/gitignore +14 -0
  13. package/templates/astro-starlight/package.json +20 -0
  14. package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
  15. package/templates/astro-starlight/public/stories/notes-index.json +4 -0
  16. package/templates/astro-starlight/public/stories/story-report.json +17 -0
  17. package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
  18. package/templates/astro-starlight/src/components/Checklist.astro +15 -0
  19. package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
  20. package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
  21. package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
  22. package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
  23. package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
  24. package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
  25. package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
  26. package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
  27. package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
  28. package/templates/astro-starlight/src/content.config.ts +18 -0
  29. package/templates/astro-starlight/src/lib/config.ts +50 -0
  30. package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
  31. package/templates/astro-starlight/src/lib/report-health.ts +61 -0
  32. package/templates/astro-starlight/src/lib/verification.ts +247 -0
  33. package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
  34. package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
  35. package/templates/astro-starlight/src/styles/global.css +293 -0
  36. package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
  37. package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
  38. package/templates/astro-starlight/src/styles/themes/default.css +86 -0
  39. package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
  40. package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
  41. package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
  42. package/templates/astro-starlight/tsconfig.json +13 -0
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
  }
@@ -15864,7 +15864,8 @@ var MarkdownFormatter = class {
15864
15864
  includeSourceLinks: options.includeSourceLinks ?? true,
15865
15865
  customRenderers: options.customRenderers,
15866
15866
  scenarioAnchor: options.scenarioAnchor,
15867
- scenarioBadge: options.scenarioBadge
15867
+ scenarioBadge: options.scenarioBadge,
15868
+ scenarioNoteLink: options.scenarioNoteLink
15868
15869
  };
15869
15870
  }
15870
15871
  /**
@@ -16075,6 +16076,10 @@ var MarkdownFormatter = class {
16075
16076
  if (badge2) {
16076
16077
  lines.push(badge2);
16077
16078
  }
16079
+ const noteLink = this.options.scenarioNoteLink?.(tc);
16080
+ if (noteLink) {
16081
+ lines.push(noteLink);
16082
+ }
16078
16083
  if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
16079
16084
  const permalink = this.buildPermalink(tc);
16080
16085
  lines.push(`Source: [${tc.sourceFile}](${permalink})`);
@@ -16490,14 +16495,14 @@ var TraceabilityMatrixFormatter = class {
16490
16495
  lines.push("");
16491
16496
  lines.push(`Status: ${renderRequirementStatus(req.status)}`);
16492
16497
  if (req.covers.length > 0) {
16493
- lines.push(`Covers: ${req.covers.map((path16) => `\`${path16}\``).join(", ")}`);
16498
+ lines.push(`Covers: ${req.covers.map((path18) => `\`${path18}\``).join(", ")}`);
16494
16499
  }
16495
16500
  lines.push("");
16496
16501
  lines.push("| Status | Scenario | Source | Covers |");
16497
16502
  lines.push("| --- | --- | --- | --- |");
16498
16503
  for (const scenario of req.scenarios) {
16499
16504
  const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16500
- 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(", ") : "";
16501
16506
  lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
16502
16507
  }
16503
16508
  lines.push("");
@@ -16599,8 +16604,8 @@ function extractFeatureName(testCases, uri) {
16599
16604
  return tc.titlePath[0];
16600
16605
  }
16601
16606
  }
16602
- const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
16603
- 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());
16604
16609
  }
16605
16610
  function synthesizeFeature(uri, testCases) {
16606
16611
  const featureName = extractFeatureName(testCases, uri);
@@ -17212,8 +17217,8 @@ function extractDocAttachments(step) {
17212
17217
  }
17213
17218
  return attachments;
17214
17219
  }
17215
- function guessMediaType(path16) {
17216
- const lower = path16.toLowerCase();
17220
+ function guessMediaType(path18) {
17221
+ const lower = path18.toLowerCase();
17217
17222
  if (lower.endsWith(".png")) return "image/png";
17218
17223
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
17219
17224
  if (lower.endsWith(".gif")) return "image/gif";
@@ -20747,18 +20752,18 @@ function deriveChangeType(tags) {
20747
20752
  }
20748
20753
  return "unknown";
20749
20754
  }
20750
- function extensionOf(path16) {
20751
- const base = path16.split("/").pop() ?? path16;
20755
+ function extensionOf(path18) {
20756
+ const base = path18.split("/").pop() ?? path18;
20752
20757
  const dot = base.lastIndexOf(".");
20753
20758
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20754
20759
  }
20755
- function isTestFile(path16) {
20756
- return TEST_INFIX.test(path16);
20760
+ function isTestFile(path18) {
20761
+ return TEST_INFIX.test(path18);
20757
20762
  }
20758
- function isReviewableSource(path16) {
20759
- if (isTestFile(path16)) return false;
20760
- if (path16.endsWith(".d.ts")) return false;
20761
- 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));
20762
20767
  }
20763
20768
  function testBaseKey(testFile) {
20764
20769
  return testFile.replace(TEST_INFIX, "");
@@ -20862,7 +20867,7 @@ function toClaim(testCase, changedSourcePaths) {
20862
20867
  const { strength, reasons } = gradeEvidence(testCase, audience);
20863
20868
  const key = testBaseKey(testCase.sourceFile);
20864
20869
  const coversFiles = changedSourcePaths.filter(
20865
- (path16) => sourceBaseKey(path16) === key
20870
+ (path18) => sourceBaseKey(path18) === key
20866
20871
  );
20867
20872
  return {
20868
20873
  id: testCase.id,
@@ -21727,7 +21732,8 @@ var ReportGenerator = class {
21727
21732
  traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
21728
21733
  customRenderers: options.astro?.markdown?.customRenderers,
21729
21734
  scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
21730
- scenarioBadge: options.astro?.markdown?.scenarioBadge
21735
+ scenarioBadge: options.astro?.markdown?.scenarioBadge,
21736
+ scenarioNoteLink: options.astro?.markdown?.scenarioNoteLink
21731
21737
  }
21732
21738
  },
21733
21739
  assetMode: options.assetMode ?? "none",
@@ -21981,6 +21987,9 @@ import { fileURLToPath } from "url";
21981
21987
  var __dirname = path10.dirname(fileURLToPath(import.meta.url));
21982
21988
  var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
21983
21989
  var FRAMEWORK_FILES = ["tsconfig.json"];
21990
+ function isScaffoldedAstroSite(dir) {
21991
+ return fs9.existsSync(path10.join(dir, "astro.config.mjs"));
21992
+ }
21984
21993
  function initAstro(options = {}) {
21985
21994
  const targetDir = options.targetDir ?? "./story-docs";
21986
21995
  const force = options.force ?? false;
@@ -22006,7 +22015,7 @@ function initAstro(options = {}) {
22006
22015
  return { targetDir };
22007
22016
  }
22008
22017
  function updateFrameworkFiles(templateDir, targetDir) {
22009
- if (!fs9.existsSync(targetDir) || !fs9.existsSync(path10.join(targetDir, "astro.config.mjs"))) {
22018
+ if (!isScaffoldedAstroSite(targetDir)) {
22010
22019
  throw new Error(
22011
22020
  `"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
22012
22021
  );
@@ -22051,7 +22060,8 @@ function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22051
22060
  const entries = fs9.readdirSync(src, { withFileTypes: true });
22052
22061
  for (const entry of entries) {
22053
22062
  const srcPath = path10.join(src, entry.name);
22054
- const destPath = path10.join(dest, entry.name);
22063
+ const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
22064
+ const destPath = path10.join(dest, destName);
22055
22065
  if (entry.isDirectory()) {
22056
22066
  copyDirRecursive(srcPath, destPath, onFile, baseSrc);
22057
22067
  } else {
@@ -22064,7 +22074,13 @@ function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22064
22074
  // src/scaffold-doc.ts
22065
22075
  import * as fs10 from "fs";
22066
22076
  import * as path11 from "path";
22067
- var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
22077
+ var TEMPLATES = [
22078
+ "adr",
22079
+ "runbook",
22080
+ "decision-log",
22081
+ "incident",
22082
+ "scenario-note"
22083
+ ];
22068
22084
  function slugify3(input) {
22069
22085
  return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
22070
22086
  }
@@ -22193,6 +22209,30 @@ _How it was fixed._
22193
22209
 
22194
22210
  - [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
22195
22211
  becomes a failing badge.
22212
+ `
22213
+ },
22214
+ "scenario-note": {
22215
+ subdir: "notes",
22216
+ filename: (_slug, ctx) => ctx.scenarioId ?? ctx.slug,
22217
+ content: (ctx) => `---
22218
+ title: 'Business context \u2014 ${ctx.name}'
22219
+ description: 'Stakeholder context for ${ctx.name}'
22220
+ scenarioId: ${ctx.scenarioId}
22221
+ # Link this note back to the scenario it explains so the badge and explorer stay aligned.
22222
+ verifiedBy: [${ctx.scenarioId}]
22223
+ ---
22224
+
22225
+ This page is hand-written commentary for a generated scenario. It is never
22226
+ overwritten by \`build-docs\`.
22227
+
22228
+ ## Why this behavior matters
22229
+
22230
+ _Describe the business rule, policy, customer promise, or operational nuance._
22231
+
22232
+ ## Caveats
22233
+
22234
+ - _What readers should know when this scenario passes_
22235
+ - _Any assumptions, exclusions, or follow-up links_
22196
22236
  `
22197
22237
  }
22198
22238
  };
@@ -22211,10 +22251,15 @@ function scaffoldDoc(options) {
22211
22251
  const today = options.today ?? /* @__PURE__ */ new Date();
22212
22252
  const name = (options.name ?? "").trim() || defaultName(template);
22213
22253
  const slug2 = slugify3(name);
22254
+ const scenarioId = normalizeScenarioId(options.scenarioId);
22214
22255
  const dir = path11.join(baseDir, spec.subdir);
22256
+ if (template === "scenario-note" && !scenarioId) {
22257
+ throw new Error(`Template "scenario-note" requires --scenario-id.`);
22258
+ }
22215
22259
  const ctx = {
22216
22260
  name,
22217
22261
  slug: slug2,
22262
+ scenarioId,
22218
22263
  isoDate: isoDate(today),
22219
22264
  seq: nextSeq(dir)
22220
22265
  };
@@ -22239,6 +22284,8 @@ function defaultName(template) {
22239
22284
  return "Decisions";
22240
22285
  case "incident":
22241
22286
  return "Untitled incident";
22287
+ case "scenario-note":
22288
+ return "Untitled scenario note";
22242
22289
  }
22243
22290
  }
22244
22291
  function titleFor2(template, ctx) {
@@ -22251,12 +22298,43 @@ function titleFor2(template, ctx) {
22251
22298
  return `Decision log \u2014 ${ctx.name}`;
22252
22299
  case "incident":
22253
22300
  return `Incident \u2014 ${ctx.name}`;
22301
+ case "scenario-note":
22302
+ return `Business context \u2014 ${ctx.name}`;
22254
22303
  }
22255
22304
  }
22305
+ function normalizeScenarioId(input) {
22306
+ const value = input?.trim();
22307
+ if (!value) return void 0;
22308
+ if (value.includes("/") || value.includes("\\")) {
22309
+ throw new Error(`scenarioId must not contain path separators.`);
22310
+ }
22311
+ return value;
22312
+ }
22256
22313
 
22257
22314
  // src/check-links.ts
22315
+ import * as fs12 from "fs";
22316
+ import * as path13 from "path";
22317
+
22318
+ // src/utils/markdown-files.ts
22258
22319
  import * as fs11 from "fs";
22259
22320
  import * as path12 from "path";
22321
+ function collectMarkdownFiles(target) {
22322
+ if (!fs11.existsSync(target)) return [];
22323
+ if (fs11.statSync(target).isFile()) return [target];
22324
+ const out = [];
22325
+ const walk = (dir) => {
22326
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22327
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22328
+ const full = path12.join(dir, entry.name);
22329
+ if (entry.isDirectory()) walk(full);
22330
+ else if (/\.mdx?$/u.test(entry.name)) out.push(full);
22331
+ }
22332
+ };
22333
+ walk(target);
22334
+ return out;
22335
+ }
22336
+
22337
+ // src/check-links.ts
22260
22338
  function stripCode(markdown) {
22261
22339
  let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
22262
22340
  out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
@@ -22286,34 +22364,19 @@ function classifyLink(link2) {
22286
22364
  function resolutionCandidates(fromFile, link2) {
22287
22365
  const withoutAnchor = link2.split("#")[0];
22288
22366
  if (!withoutAnchor) return [];
22289
- const base = path12.resolve(path12.dirname(fromFile), withoutAnchor);
22367
+ const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
22290
22368
  const candidates = [base];
22291
- if (!path12.extname(base)) {
22369
+ if (!path13.extname(base)) {
22292
22370
  candidates.push(`${base}.md`, `${base}.mdx`);
22293
- candidates.push(path12.join(base, "index.md"), path12.join(base, "index.mdx"));
22371
+ candidates.push(path13.join(base, "index.md"), path13.join(base, "index.mdx"));
22294
22372
  }
22295
22373
  return candidates;
22296
22374
  }
22297
22375
  function resolvesOnDisk(fromFile, link2) {
22298
22376
  return resolutionCandidates(fromFile, link2).some(
22299
- (candidate) => fs11.existsSync(candidate) && fs11.statSync(candidate).isFile()
22377
+ (candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
22300
22378
  );
22301
22379
  }
22302
- function collectDocFiles(target) {
22303
- const stat = fs11.statSync(target);
22304
- if (stat.isFile()) return [target];
22305
- const out = [];
22306
- const walk = (dir) => {
22307
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
22308
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
22309
- const full = path12.join(dir, entry.name);
22310
- if (entry.isDirectory()) walk(full);
22311
- else if (/\.mdx?$/.test(entry.name)) out.push(full);
22312
- }
22313
- };
22314
- walk(target);
22315
- return out;
22316
- }
22317
22380
  async function isExternalAlive(url, timeoutMs) {
22318
22381
  const attempt = async (method) => {
22319
22382
  const controller = new AbortController();
@@ -22337,17 +22400,17 @@ async function isExternalAlive(url, timeoutMs) {
22337
22400
  }
22338
22401
  async function checkLinks(options) {
22339
22402
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
22340
- if (!fs11.existsSync(target)) {
22403
+ if (!fs12.existsSync(target)) {
22341
22404
  throw new Error(`Path not found: ${target}`);
22342
22405
  }
22343
- const files = collectDocFiles(target);
22406
+ const files = collectMarkdownFiles(target);
22344
22407
  const broken = [];
22345
22408
  let linksChecked = 0;
22346
22409
  let externalChecked = 0;
22347
22410
  let skipped = 0;
22348
22411
  const externalCache = /* @__PURE__ */ new Map();
22349
22412
  for (const file of files) {
22350
- const content = fs11.readFileSync(file, "utf8");
22413
+ const content = fs12.readFileSync(file, "utf8");
22351
22414
  for (const link2 of extractLinks(content)) {
22352
22415
  const kind = classifyLink(link2);
22353
22416
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -22403,8 +22466,8 @@ function formatLinkReport(report) {
22403
22466
  }
22404
22467
 
22405
22468
  // src/import-openapi.ts
22406
- import * as fs12 from "fs";
22407
- import * as path13 from "path";
22469
+ import * as fs13 from "fs";
22470
+ import * as path14 from "path";
22408
22471
  import { parse as parseYamlString } from "yaml";
22409
22472
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
22410
22473
  function parseYaml(raw, specPath) {
@@ -22417,9 +22480,9 @@ function parseYaml(raw, specPath) {
22417
22480
  }
22418
22481
  }
22419
22482
  function parseSpec(specPath) {
22420
- if (!fs12.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22421
- const raw = fs12.readFileSync(specPath, "utf8");
22422
- const ext = path13.extname(specPath).toLowerCase();
22483
+ if (!fs13.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
22484
+ const raw = fs13.readFileSync(specPath, "utf8");
22485
+ const ext = path14.extname(specPath).toLowerCase();
22423
22486
  if (ext === ".json") return JSON.parse(raw);
22424
22487
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
22425
22488
  try {
@@ -22450,8 +22513,8 @@ function extractEndpoints(spec) {
22450
22513
  }
22451
22514
  function loadScenarios(runFile) {
22452
22515
  if (!runFile) return [];
22453
- if (!fs12.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22454
- const report = JSON.parse(fs12.readFileSync(runFile, "utf8"));
22516
+ if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22517
+ const report = JSON.parse(fs13.readFileSync(runFile, "utf8"));
22455
22518
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
22456
22519
  }
22457
22520
  function endpointRefs(endpoint) {
@@ -22558,25 +22621,25 @@ async function importOpenApi(options) {
22558
22621
  list.push(item);
22559
22622
  groups.set(item.endpoint.tag, list);
22560
22623
  }
22561
- const outputDir = options.outputDir ?? path13.join("src", "content", "docs", "api");
22562
- if (fs12.existsSync(outputDir) && !options.force) {
22563
- const entries = fs12.readdirSync(outputDir);
22624
+ const outputDir = options.outputDir ?? path14.join("src", "content", "docs", "api");
22625
+ if (fs13.existsSync(outputDir) && !options.force) {
22626
+ const entries = fs13.readdirSync(outputDir);
22564
22627
  if (entries.length > 0) {
22565
22628
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
22566
22629
  }
22567
22630
  }
22568
- fs12.mkdirSync(outputDir, { recursive: true });
22631
+ fs13.mkdirSync(outputDir, { recursive: true });
22569
22632
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
22570
22633
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
22571
- fs12.writeFileSync(
22572
- path13.join(outputDir, "index.mdx"),
22634
+ fs13.writeFileSync(
22635
+ path14.join(outputDir, "index.mdx"),
22573
22636
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
22574
22637
  "utf8"
22575
22638
  );
22576
22639
  for (const [tag, rows] of groups) {
22577
- const dir = path13.join(outputDir, slug(tag));
22578
- fs12.mkdirSync(dir, { recursive: true });
22579
- fs12.writeFileSync(path13.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22640
+ const dir = path14.join(outputDir, slug(tag));
22641
+ fs13.mkdirSync(dir, { recursive: true });
22642
+ fs13.writeFileSync(path14.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
22580
22643
  }
22581
22644
  return {
22582
22645
  outputDir,
@@ -22588,8 +22651,8 @@ async function importOpenApi(options) {
22588
22651
  }
22589
22652
 
22590
22653
  // src/build-docs.ts
22591
- import * as fs13 from "fs";
22592
- import * as path14 from "path";
22654
+ import * as fs15 from "fs";
22655
+ import * as path16 from "path";
22593
22656
 
22594
22657
  // src/scenario-links.ts
22595
22658
  function scenarioAnchor(title) {
@@ -22685,6 +22748,58 @@ ${body.join("\n")}
22685
22748
  `;
22686
22749
  }
22687
22750
 
22751
+ // src/notes-index.ts
22752
+ import * as fs14 from "fs";
22753
+ import * as path15 from "path";
22754
+ import { slug as githubSlug } from "github-slugger";
22755
+ import { parse as parseYaml2 } from "yaml";
22756
+ function buildScenarioNotesIndex(notesDir) {
22757
+ const entries = collectMarkdownFiles(notesDir).map((filePath) => readScenarioNote(filePath, notesDir)).filter((entry) => entry !== null).sort((a, b) => a.slug.localeCompare(b.slug));
22758
+ return {
22759
+ schemaVersion: "1.0",
22760
+ notes: entries
22761
+ };
22762
+ }
22763
+ function writeNotesIndex(index, outPath) {
22764
+ fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
22765
+ fs14.writeFileSync(outPath, JSON.stringify(index, null, 2), "utf8");
22766
+ return index;
22767
+ }
22768
+ function notesByScenarioId(index) {
22769
+ const map = /* @__PURE__ */ new Map();
22770
+ for (const note of index.notes) {
22771
+ if (!map.has(note.scenarioId)) map.set(note.scenarioId, note);
22772
+ }
22773
+ return map;
22774
+ }
22775
+ function noteHref(note) {
22776
+ return `/notes/${note.slug}/`;
22777
+ }
22778
+ function noteLinkMarkdown(note) {
22779
+ return `[Business context \u2192](${noteHref(note)})`;
22780
+ }
22781
+ function readScenarioNote(filePath, notesDir) {
22782
+ const relative5 = path15.relative(notesDir, filePath);
22783
+ const stem = relative5.replace(/\.(?:md|mdx)$/u, "");
22784
+ const frontmatter = parseFrontmatter(fs14.readFileSync(filePath, "utf8"));
22785
+ const scenarioId = typeof frontmatter.scenarioId === "string" && frontmatter.scenarioId.trim().length > 0 ? frontmatter.scenarioId.trim() : path15.basename(stem);
22786
+ const title = typeof frontmatter.title === "string" && frontmatter.title.trim().length > 0 ? frontmatter.title.trim() : `Business context \u2014 ${scenarioId}`;
22787
+ return {
22788
+ scenarioId,
22789
+ slug: toRouteSlug(stem),
22790
+ title
22791
+ };
22792
+ }
22793
+ function parseFrontmatter(source) {
22794
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
22795
+ if (!match) return {};
22796
+ const parsed = parseYaml2(match[1]);
22797
+ return parsed && typeof parsed === "object" ? parsed : {};
22798
+ }
22799
+ function toRouteSlug(stem) {
22800
+ return stem.split(path15.sep).map((segment) => githubSlug(segment)).join("/").replace(/\/index$/u, "");
22801
+ }
22802
+
22688
22803
  // src/overview-page.ts
22689
22804
  var AUDIENCE_CARDS = [
22690
22805
  {
@@ -22712,11 +22827,12 @@ function yamlScalar3(value) {
22712
22827
  }
22713
22828
  return value;
22714
22829
  }
22715
- function renderOverviewPage(links) {
22830
+ function renderOverviewPage(links, notesIndex) {
22716
22831
  const all = Object.values(links.scenarios);
22717
22832
  const total = all.length;
22718
22833
  const passed = all.filter((s) => s.status === "passed").length;
22719
22834
  const failed = all.filter((s) => s.status === "failed").length;
22835
+ const notesById = notesByScenarioId(notesIndex ?? { schemaVersion: "1.0", notes: [] });
22720
22836
  const frontmatter = [
22721
22837
  "---",
22722
22838
  "title: Stories",
@@ -22740,7 +22856,9 @@ function renderOverviewPage(links) {
22740
22856
  body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
22741
22857
  body.push(`${card.blurb}`, "");
22742
22858
  for (const s of scenariosSorted(scenarios)) {
22743
- body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})`);
22859
+ const note = notesById.get(s.id);
22860
+ const noteSuffix = note ? ` \xB7 ${noteLinkMarkdown(note)}` : "";
22861
+ body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})${noteSuffix}`);
22744
22862
  }
22745
22863
  body.push("");
22746
22864
  }
@@ -22782,22 +22900,22 @@ var BuildDocsError = class extends Error {
22782
22900
  };
22783
22901
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
22784
22902
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
22785
- if (!fs13.existsSync(reportPath)) return 0;
22786
- const report = JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22903
+ if (!fs15.existsSync(reportPath)) return 0;
22904
+ const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22787
22905
  let copied = 0;
22788
22906
  const bundle = (value) => {
22789
- const rel = copyAsset(path14.resolve(value), assetsDir);
22907
+ const rel = copyAsset(path16.resolve(value), assetsDir);
22790
22908
  copied++;
22791
- return `${baseUrl}/${path14.basename(rel)}`;
22909
+ return `${baseUrl}/${path16.basename(rel)}`;
22792
22910
  };
22793
22911
  const visit = (entries) => {
22794
22912
  for (const entry of entries ?? []) {
22795
22913
  const e = entry;
22796
22914
  if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
22797
- if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
22915
+ if (typeof e.path === "string" && !isRemote(e.path) && fs15.existsSync(e.path)) {
22798
22916
  e.path = bundle(e.path);
22799
22917
  }
22800
- if (typeof e.poster === "string" && !isRemote(e.poster) && fs13.existsSync(e.poster)) {
22918
+ if (typeof e.poster === "string" && !isRemote(e.poster) && fs15.existsSync(e.poster)) {
22801
22919
  e.poster = bundle(e.poster);
22802
22920
  }
22803
22921
  }
@@ -22810,7 +22928,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22810
22928
  }
22811
22929
  }
22812
22930
  if (copied > 0) {
22813
- fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22931
+ fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22814
22932
  }
22815
22933
  return copied;
22816
22934
  }
@@ -22819,50 +22937,69 @@ var CHANGE_BADGE = {
22819
22937
  fixed: "\u2705 **Fixed** _since last run_",
22820
22938
  regressed: "\u26A0\uFE0F **Regressed** _since last run_"
22821
22939
  };
22940
+ function scenarioKey(sourceFile, title) {
22941
+ return `${sourceFile}\0${title}`;
22942
+ }
22822
22943
  function changeBadgeLookup(diff) {
22823
22944
  if (!diff) return void 0;
22824
22945
  const byKey = /* @__PURE__ */ new Map();
22825
22946
  for (const s of diff.scenarios) {
22826
22947
  const badge2 = CHANGE_BADGE[s.kind];
22827
- if (badge2) byKey.set(`${s.sourceFile}\0${s.title}`, badge2);
22948
+ if (badge2) byKey.set(scenarioKey(s.sourceFile, s.title), badge2);
22828
22949
  }
22829
22950
  if (byKey.size === 0) return void 0;
22830
- return (tc) => byKey.get(`${tc.sourceFile}\0${tc.story.scenario}`);
22951
+ return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22831
22952
  }
22832
22953
  function readStoryReport(reportPath) {
22833
- if (!fs13.existsSync(reportPath)) return null;
22954
+ if (!fs15.existsSync(reportPath)) return null;
22834
22955
  try {
22835
- return JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22956
+ return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22836
22957
  } catch {
22837
22958
  return null;
22838
22959
  }
22839
22960
  }
22961
+ function noteLinkLookup(report, notesIndex) {
22962
+ if (!report) return void 0;
22963
+ const noteById = notesByScenarioId(notesIndex);
22964
+ if (noteById.size === 0) return void 0;
22965
+ const idByKey = /* @__PURE__ */ new Map();
22966
+ for (const feature of report.features) {
22967
+ for (const scenario of feature.scenarios) {
22968
+ idByKey.set(scenarioKey(feature.sourceFile, scenario.title), scenario.id);
22969
+ }
22970
+ }
22971
+ return (tc) => {
22972
+ const id = idByKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22973
+ const note = id ? noteById.get(id) : void 0;
22974
+ return note ? noteLinkMarkdown(note) : void 0;
22975
+ };
22976
+ }
22840
22977
  function writeScenarioLinks(reportPath, outDir, options = {}) {
22841
22978
  const report = readStoryReport(reportPath);
22842
22979
  if (!report) return null;
22843
22980
  const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22844
- fs13.writeFileSync(
22845
- path14.join(outDir, "scenario-links.json"),
22981
+ fs15.writeFileSync(
22982
+ path16.join(outDir, "scenario-links.json"),
22846
22983
  JSON.stringify(index, null, 2),
22847
22984
  "utf8"
22848
22985
  );
22849
22986
  return index;
22850
22987
  }
22851
22988
  function clearGeneratedPages(dir) {
22852
- if (!fs13.existsSync(dir)) return;
22853
- for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
22854
- const full = path14.join(dir, entry.name);
22989
+ if (!fs15.existsSync(dir)) return;
22990
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
22991
+ const full = path16.join(dir, entry.name);
22855
22992
  if (entry.isDirectory()) {
22856
22993
  clearGeneratedPages(full);
22857
- if (fs13.readdirSync(full).length === 0) fs13.rmdirSync(full);
22994
+ if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
22858
22995
  } else if (/\.mdx?$/.test(entry.name)) {
22859
- fs13.rmSync(full);
22996
+ fs15.rmSync(full);
22860
22997
  }
22861
22998
  }
22862
22999
  }
22863
23000
  function loadCanonicalRun(rawRunPath, synthesize) {
22864
23001
  try {
22865
- const data = JSON.parse(fs13.readFileSync(path14.resolve(rawRunPath), "utf8"));
23002
+ const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
22866
23003
  if (data.schemaVersion !== 1) {
22867
23004
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
22868
23005
  }
@@ -22885,12 +23022,19 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
22885
23022
  }
22886
23023
  }
22887
23024
  async function buildDocs(options) {
22888
- const siteDir = path14.resolve(options.siteDir);
22889
- const storiesPublicDir = path14.join(siteDir, "public", "stories");
22890
- const assetsDir = path14.join(storiesPublicDir, "assets");
22891
- const storyPagesDir = path14.join(siteDir, "src", "content", "docs", "stories");
22892
- const apiDir = path14.join(siteDir, "src", "content", "docs", "api");
22893
- const reportPath = path14.join(storiesPublicDir, "story-report.json");
23025
+ const siteDir = path16.resolve(options.siteDir);
23026
+ if (!isScaffoldedAstroSite(siteDir)) {
23027
+ throw new BuildDocsError(
23028
+ `"${siteDir}" is not a scaffolded Astro docs site (no astro.config.mjs). Run "executable-stories init-astro <dir>" first, then pass it with --site-dir <dir>.`,
23029
+ "usage"
23030
+ );
23031
+ }
23032
+ const storiesPublicDir = path16.join(siteDir, "public", "stories");
23033
+ const assetsDir = path16.join(storiesPublicDir, "assets");
23034
+ const storyPagesDir = path16.join(siteDir, "src", "content", "docs", "stories");
23035
+ const notesDir = path16.join(siteDir, "src", "content", "docs", "notes");
23036
+ const apiDir = path16.join(siteDir, "src", "content", "docs", "api");
23037
+ const reportPath = path16.join(storiesPublicDir, "story-report.json");
22894
23038
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
22895
23039
  try {
22896
23040
  await new ReportGenerator({
@@ -22898,9 +23042,10 @@ async function buildDocs(options) {
22898
23042
  outputDir: storiesPublicDir,
22899
23043
  outputName: "story-report"
22900
23044
  }).generate(canonical);
23045
+ const currentReport = readStoryReport(reportPath);
22901
23046
  let diff;
22902
23047
  if (options.baselinePath) {
22903
- const baselineResolved = path14.resolve(options.baselinePath);
23048
+ const baselineResolved = path16.resolve(options.baselinePath);
22904
23049
  const baseline = readStoryReport(baselineResolved);
22905
23050
  if (!baseline) {
22906
23051
  throw new BuildDocsError(
@@ -22908,10 +23053,11 @@ async function buildDocs(options) {
22908
23053
  "input"
22909
23054
  );
22910
23055
  }
22911
- const current = readStoryReport(reportPath);
22912
- if (current) diff = diffStoryReports(baseline, current);
23056
+ if (currentReport) diff = diffStoryReports(baseline, currentReport);
22913
23057
  }
22914
23058
  const scenarioBadge = changeBadgeLookup(diff);
23059
+ const notesIndex = buildScenarioNotesIndex(notesDir);
23060
+ const scenarioNoteLink = noteLinkLookup(currentReport, notesIndex);
22915
23061
  clearGeneratedPages(storyPagesDir);
22916
23062
  const genPages = (run, outDir) => new ReportGenerator({
22917
23063
  formats: ["astro"],
@@ -22925,7 +23071,8 @@ async function buildDocs(options) {
22925
23071
  markdown: {
22926
23072
  // Emit the same anchor scenario-links.json points at, so fragments resolve.
22927
23073
  scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
22928
- scenarioBadge
23074
+ scenarioBadge,
23075
+ scenarioNoteLink
22929
23076
  }
22930
23077
  }
22931
23078
  }).generate(run);
@@ -22936,7 +23083,7 @@ async function buildDocs(options) {
22936
23083
  const sub = partitioned[audience];
22937
23084
  audiences[audience] = sub.testCases.length;
22938
23085
  if (sub.testCases.length === 0) continue;
22939
- await genPages(sub, path14.join(storyPagesDir, audience));
23086
+ await genPages(sub, path16.join(storyPagesDir, audience));
22940
23087
  }
22941
23088
  } else {
22942
23089
  await genPages(canonical, storyPagesDir);
@@ -22946,35 +23093,37 @@ async function buildDocs(options) {
22946
23093
  audienceSplit: options.audienceSplit ?? false
22947
23094
  });
22948
23095
  const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
23096
+ writeNotesIndex(notesIndex, path16.join(storiesPublicDir, "notes-index.json"));
23097
+ const notesIndexed = notesIndex.notes.length;
22949
23098
  if (linksIndex) {
22950
- fs13.writeFileSync(
22951
- path14.join(storyPagesDir, "index.md"),
22952
- renderOverviewPage(linksIndex),
23099
+ fs15.writeFileSync(
23100
+ path16.join(storyPagesDir, "index.md"),
23101
+ renderOverviewPage(linksIndex, notesIndex),
22953
23102
  "utf8"
22954
23103
  );
22955
23104
  }
22956
- const changesJsonPath = path14.join(storiesPublicDir, "changes.json");
22957
- const changesMdPath = path14.join(storyPagesDir, "changes.md");
23105
+ const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
23106
+ const changesMdPath = path16.join(storyPagesDir, "changes.md");
22958
23107
  let changes;
22959
23108
  if (diff && linksIndex) {
22960
- fs13.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
22961
- fs13.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
23109
+ fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23110
+ fs15.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
22962
23111
  changes = diff.summary;
22963
23112
  } else {
22964
- fs13.rmSync(changesJsonPath, { force: true });
22965
- fs13.rmSync(changesMdPath, { force: true });
23113
+ fs15.rmSync(changesJsonPath, { force: true });
23114
+ fs15.rmSync(changesMdPath, { force: true });
22966
23115
  }
22967
23116
  let apiPages = 0;
22968
23117
  if (options.openapiPath) {
22969
23118
  const res = await importOpenApi({
22970
- specPath: path14.resolve(options.openapiPath),
23119
+ specPath: path16.resolve(options.openapiPath),
22971
23120
  outputDir: apiDir,
22972
23121
  runFile: reportPath,
22973
23122
  force: true
22974
23123
  });
22975
23124
  apiPages = res.pageCount;
22976
23125
  }
22977
- return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
23126
+ return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, notesIndexed, changes };
22978
23127
  } catch (err) {
22979
23128
  if (err instanceof BuildDocsError) throw err;
22980
23129
  throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
@@ -22982,11 +23131,11 @@ async function buildDocs(options) {
22982
23131
  }
22983
23132
 
22984
23133
  // src/config.ts
22985
- import { existsSync as existsSync12 } from "fs";
23134
+ import { existsSync as existsSync13 } from "fs";
22986
23135
  import { resolve as resolve10 } from "path";
22987
23136
  async function loadConfig(configPath) {
22988
23137
  const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
22989
- if (!existsSync12(resolved)) return {};
23138
+ if (!existsSync13(resolved)) return {};
22990
23139
  const mod = await import(resolved);
22991
23140
  const config = mod.default;
22992
23141
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -23013,6 +23162,7 @@ executable-stories \u2014 Generate reports from test results JSON.
23013
23162
  USAGE
23014
23163
  executable-stories format <file> [options]
23015
23164
  executable-stories format --stdin [options]
23165
+ executable-stories watch <raw-run.json> [options]
23016
23166
  executable-stories compare <baseline-file> <current-file> [options]
23017
23167
  executable-stories gate-release <dev-run.json> <rc-run.json> [options]
23018
23168
  executable-stories review <file> --changed-files <path> [options]
@@ -23023,6 +23173,7 @@ USAGE
23023
23173
  executable-stories validate <file>
23024
23174
  executable-stories validate --stdin
23025
23175
  executable-stories init-astro [directory]
23176
+ executable-stories build-docs <raw-run.json> [--site-dir <dir>] [options]
23026
23177
  executable-stories new <template> "<name>" [options]
23027
23178
  executable-stories check-links <dir> [options]
23028
23179
  executable-stories import-openapi <spec> [options]
@@ -23044,7 +23195,8 @@ SUBCOMMANDS
23044
23195
  triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
23045
23196
  validate Validate a JSON file against the schema (no output generated)
23046
23197
  init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
23047
- new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
23198
+ build-docs Build the living-docs site: one page per story file + Explorer data (auto-pickup, prunes deleted stories)
23199
+ new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
23048
23200
  check-links Scan docs for broken internal/external links (CI-friendly exit code)
23049
23201
  import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
23050
23202
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
@@ -23053,7 +23205,7 @@ SUBCOMMANDS
23053
23205
 
23054
23206
  OPTIONS
23055
23207
  --format <formats> Comma-separated formats: html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
23056
- astro Themed Markdown (for Astro docs sites with matching CSS)
23208
+ astro Themed Markdown primitive (single aggregated page; for a full site use "build-docs")
23057
23209
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
23058
23210
  behavior-manifest-json Agent-readable behavior manifest and debugger warnings
23059
23211
  html Custom HTML report (accessible, dark mode, mermaid)
@@ -23174,6 +23326,20 @@ DEPLOY
23174
23326
  INIT-ASTRO
23175
23327
  executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
23176
23328
  --force Overwrite existing directory
23329
+ --update Refresh framework files only (keeps your content + config)
23330
+
23331
+ BUILD-DOCS
23332
+ Build the multi-page living-docs site from a raw run: one Astro page per story
23333
+ file plus Explorer data (scenario-links.json, story-report.json). Auto-pickup \u2014
23334
+ a new *.story.test.ts becomes a new page on the next run; deleting a story
23335
+ prunes its page. This is the headline living-docs flow; "format --format astro"
23336
+ is a low-level primitive that emits a single aggregated page, not a site.
23337
+
23338
+ executable-stories build-docs <raw-run.json> [--site-dir <dir>]
23339
+ --site-dir <dir> Target site dir (default: a scaffolded init-astro site)
23340
+ --openapi <spec> Link generated API pages to verifying stories
23341
+ --baseline <prev-report> Diff against a prior story-report.json for change markers
23342
+ --audience-split Split pages by audience (business vs technical)
23177
23343
 
23178
23344
  PUBLISH-CONFLUENCE
23179
23345
  executable-stories publish-confluence <file.adf.json> [options]
@@ -23280,12 +23446,17 @@ async function parseCliArgs(argv) {
23280
23446
  console.log("To change theme, edit astro.config.mjs customCss array.");
23281
23447
  console.log("");
23282
23448
  console.log("Next steps:");
23283
- console.log(` cd ${result.targetDir}`);
23284
- console.log(" pnpm install # or npm install");
23285
- console.log(" pnpm dev # start the dev server");
23286
- console.log("");
23287
- console.log("Generate everything (story pages, explorer data, API pages) in one step:");
23288
- console.log(` executable-stories build-docs run.json --site-dir ${result.targetDir} [--openapi spec.json]`);
23449
+ console.log(` 1. cd ${result.targetDir} && pnpm install # or npm install`);
23450
+ console.log(" 2. In your TEST project, add the StoryReporter with a rawRunPath, e.g.");
23451
+ console.log(" StoryReporter({ rawRunPath: 'reports/raw-run.json' })");
23452
+ console.log(" (this is what writes the raw run that build-docs reads)");
23453
+ console.log(" 3. Run your tests to produce reports/raw-run.json:");
23454
+ console.log(" pnpm test");
23455
+ console.log(" 4. Build the living-docs site (story pages, explorer data, API pages):");
23456
+ console.log(
23457
+ ` executable-stories build-docs reports/raw-run.json --site-dir ${result.targetDir} [--openapi spec.json]`
23458
+ );
23459
+ console.log(` 5. Preview it: cd ${result.targetDir} && pnpm dev`);
23289
23460
  console.log("");
23290
23461
  console.log("Later, pull template/design improvements without losing your content:");
23291
23462
  console.log(` executable-stories init-astro ${result.targetDir} --update`);
@@ -23583,20 +23754,20 @@ async function readInput(args) {
23583
23754
  if (args.stdin) {
23584
23755
  return readStdin();
23585
23756
  }
23586
- const filePath = path15.resolve(args.inputFile);
23587
- if (!fs14.existsSync(filePath)) {
23757
+ const filePath = path17.resolve(args.inputFile);
23758
+ if (!fs16.existsSync(filePath)) {
23588
23759
  console.error(`Error: File not found: ${filePath}`);
23589
23760
  process.exit(EXIT_USAGE);
23590
23761
  }
23591
- return fs14.readFileSync(filePath, "utf8");
23762
+ return fs16.readFileSync(filePath, "utf8");
23592
23763
  }
23593
23764
  function readFileInput(filePath) {
23594
- const resolved = path15.resolve(filePath);
23595
- if (!fs14.existsSync(resolved)) {
23765
+ const resolved = path17.resolve(filePath);
23766
+ if (!fs16.existsSync(resolved)) {
23596
23767
  console.error(`Error: File not found: ${resolved}`);
23597
23768
  process.exit(EXIT_USAGE);
23598
23769
  }
23599
- return fs14.readFileSync(resolved, "utf8");
23770
+ return fs16.readFileSync(resolved, "utf8");
23600
23771
  }
23601
23772
  function readStdin() {
23602
23773
  return new Promise((resolve12, reject) => {
@@ -23729,14 +23900,14 @@ function tryNormalizeRunFromText(text2, args) {
23729
23900
  }
23730
23901
  }
23731
23902
  function listBaselineCandidates(currentFile, args) {
23732
- const baselineDir = path15.resolve(args.baselineDir ?? path15.dirname(currentFile));
23733
- const currentResolved = path15.resolve(currentFile);
23734
- if (!fs14.existsSync(baselineDir)) {
23903
+ const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
23904
+ const currentResolved = path17.resolve(currentFile);
23905
+ if (!fs16.existsSync(baselineDir)) {
23735
23906
  console.error(`Error: baseline directory not found: ${baselineDir}`);
23736
23907
  process.exit(EXIT_USAGE);
23737
23908
  }
23738
- const entries = fs14.readdirSync(baselineDir, { withFileTypes: true });
23739
- return entries.filter((entry) => entry.isFile()).map((entry) => path15.join(baselineDir, entry.name)).filter((candidate) => path15.resolve(candidate) !== currentResolved).filter(
23909
+ const entries = fs16.readdirSync(baselineDir, { withFileTypes: true });
23910
+ return entries.filter((entry) => entry.isFile()).map((entry) => path17.join(baselineDir, entry.name)).filter((candidate) => path17.resolve(candidate) !== currentResolved).filter(
23740
23911
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
23741
23912
  );
23742
23913
  }
@@ -23744,14 +23915,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
23744
23915
  const candidates = listBaselineCandidates(currentFile, args);
23745
23916
  const comparable = [];
23746
23917
  for (const candidate of candidates) {
23747
- const run = tryNormalizeRunFromText(fs14.readFileSync(candidate, "utf8"), args);
23918
+ const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
23748
23919
  if (run) {
23749
23920
  comparable.push({ file: candidate, run });
23750
23921
  }
23751
23922
  }
23752
23923
  if (comparable.length === 0) {
23753
23924
  console.error(
23754
- `Error: no compatible baseline files found in ${path15.resolve(args.baselineDir ?? path15.dirname(currentFile))}.`
23925
+ `Error: no compatible baseline files found in ${path17.resolve(args.baselineDir ?? path17.dirname(currentFile))}.`
23755
23926
  );
23756
23927
  process.exit(EXIT_USAGE);
23757
23928
  }
@@ -23996,9 +24167,9 @@ async function main() {
23996
24167
  process.exit(EXIT_SCHEMA_VALIDATION);
23997
24168
  }
23998
24169
  if (args.emitCanonical) {
23999
- const outPath = path15.resolve(args.emitCanonical);
24000
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
24001
- fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24170
+ const outPath = path17.resolve(args.emitCanonical);
24171
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24172
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24002
24173
  }
24003
24174
  try {
24004
24175
  const result = await generateReports(run, args);
@@ -24055,9 +24226,9 @@ ${msg}`);
24055
24226
  }
24056
24227
  const run = data;
24057
24228
  if (args.emitCanonical) {
24058
- const outPath = path15.resolve(args.emitCanonical);
24059
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
24060
- fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24229
+ const outPath = path17.resolve(args.emitCanonical);
24230
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24231
+ fs16.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
24061
24232
  }
24062
24233
  try {
24063
24234
  const result = await generateReports(run, args);
@@ -24113,9 +24284,9 @@ ${msg}`);
24113
24284
  process.exit(EXIT_CANONICAL_VALIDATION);
24114
24285
  }
24115
24286
  if (args.emitCanonical) {
24116
- const outPath = path15.resolve(args.emitCanonical);
24117
- fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
24118
- fs14.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
24287
+ const outPath = path17.resolve(args.emitCanonical);
24288
+ fs16.mkdirSync(path17.dirname(outPath), { recursive: true });
24289
+ fs16.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
24119
24290
  }
24120
24291
  try {
24121
24292
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -24140,9 +24311,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
24140
24311
  const ext = formatter.fileExtension ?? formatName;
24141
24312
  const baseName = args.outputName ?? "report";
24142
24313
  const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
24143
- const filepath = path15.join(outputDir, filename);
24144
- fs14.mkdirSync(outputDir, { recursive: true });
24145
- fs14.writeFileSync(filepath, content, "utf8");
24314
+ const filepath = path17.join(outputDir, filename);
24315
+ fs16.mkdirSync(outputDir, { recursive: true });
24316
+ fs16.writeFileSync(filepath, content, "utf8");
24146
24317
  console.log(`Generated: ${filepath}`);
24147
24318
  } catch (err) {
24148
24319
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -24192,13 +24363,13 @@ async function dispatchNotifications(run, args) {
24192
24363
  }
24193
24364
  function runHistoryPipeline(run, args) {
24194
24365
  if (!args.historyFile) return;
24195
- const historyPath = path15.resolve(args.historyFile);
24366
+ const historyPath = path17.resolve(args.historyFile);
24196
24367
  const store = loadHistory(
24197
24368
  { filePath: historyPath },
24198
24369
  {
24199
24370
  readFile: (p) => {
24200
24371
  try {
24201
- return fs14.readFileSync(p, "utf8");
24372
+ return fs16.readFileSync(p, "utf8");
24202
24373
  } catch {
24203
24374
  return void 0;
24204
24375
  }
@@ -24211,11 +24382,11 @@ function runHistoryPipeline(run, args) {
24211
24382
  run,
24212
24383
  maxRuns: args.maxHistoryRuns
24213
24384
  });
24214
- const dir = path15.dirname(historyPath);
24215
- fs14.mkdirSync(dir, { recursive: true });
24385
+ const dir = path17.dirname(historyPath);
24386
+ fs16.mkdirSync(dir, { recursive: true });
24216
24387
  saveHistory(
24217
24388
  { filePath: historyPath, store: updated },
24218
- { writeFile: (p, content) => fs14.writeFileSync(p, content, "utf8") }
24389
+ { writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
24219
24390
  );
24220
24391
  let metricsCount = 0;
24221
24392
  for (const testId of Object.keys(updated.tests)) {
@@ -24363,11 +24534,11 @@ function writeReviewReport(review, args) {
24363
24534
  const outputDir = args.outputDir ?? "reports";
24364
24535
  const baseName = args.outputName ?? "evidence-review";
24365
24536
  const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
24366
- fs14.mkdirSync(outputDir, { recursive: true });
24367
- const mdPath = path15.join(outputDir, `${baseName}${suffix}.md`);
24368
- const htmlPath = path15.join(outputDir, `${baseName}${suffix}.html`);
24369
- fs14.writeFileSync(mdPath, markdown, "utf8");
24370
- fs14.writeFileSync(htmlPath, html, "utf8");
24537
+ fs16.mkdirSync(outputDir, { recursive: true });
24538
+ const mdPath = path17.join(outputDir, `${baseName}${suffix}.md`);
24539
+ const htmlPath = path17.join(outputDir, `${baseName}${suffix}.html`);
24540
+ fs16.writeFileSync(mdPath, markdown, "utf8");
24541
+ fs16.writeFileSync(htmlPath, html, "utf8");
24371
24542
  return [mdPath, htmlPath];
24372
24543
  }
24373
24544
  function evaluateReviewGate(review, args) {
@@ -24413,9 +24584,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
24413
24584
  function printCompareResult(result, args, startMs) {
24414
24585
  const durationMs = Date.now() - startMs;
24415
24586
  if (result.prSummary && args.prSummaryFile) {
24416
- const outputPath = path15.resolve(args.prSummaryFile);
24417
- fs14.mkdirSync(path15.dirname(outputPath), { recursive: true });
24418
- fs14.writeFileSync(outputPath, result.prSummary, "utf8");
24587
+ const outputPath = path17.resolve(args.prSummaryFile);
24588
+ fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
24589
+ fs16.writeFileSync(outputPath, result.prSummary, "utf8");
24419
24590
  }
24420
24591
  if (args.jsonSummary) {
24421
24592
  console.log(
@@ -24444,13 +24615,13 @@ function printCompareResult(result, args, startMs) {
24444
24615
  }
24445
24616
  }
24446
24617
  function loadReleasePolicy(policyPath) {
24447
- const resolved = path15.resolve(policyPath);
24448
- if (!fs14.existsSync(resolved)) {
24618
+ const resolved = path17.resolve(policyPath);
24619
+ if (!fs16.existsSync(resolved)) {
24449
24620
  console.error(`Error: release policy file not found: ${resolved}`);
24450
24621
  process.exit(EXIT_USAGE);
24451
24622
  }
24452
24623
  try {
24453
- const raw = JSON.parse(fs14.readFileSync(resolved, "utf8"));
24624
+ const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
24454
24625
  return {
24455
24626
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
24456
24627
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -24554,7 +24725,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24554
24725
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24555
24726
  process.exit(EXIT_USAGE);
24556
24727
  }
24557
- if (!fs14.existsSync(inputFile)) {
24728
+ if (!fs16.existsSync(inputFile)) {
24558
24729
  console.error(`Error: file not found: ${inputFile}`);
24559
24730
  process.exit(EXIT_USAGE);
24560
24731
  }
@@ -24582,7 +24753,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24582
24753
  console.error("Error: --title is required when creating a new page");
24583
24754
  process.exit(EXIT_USAGE);
24584
24755
  }
24585
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24756
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24586
24757
  if (dryRun) {
24587
24758
  console.log(
24588
24759
  JSON.stringify(
@@ -24661,7 +24832,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24661
24832
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24662
24833
  process.exit(EXIT_USAGE);
24663
24834
  }
24664
- if (!fs14.existsSync(inputFile)) {
24835
+ if (!fs16.existsSync(inputFile)) {
24665
24836
  console.error(`Error: file not found: ${inputFile}`);
24666
24837
  process.exit(EXIT_USAGE);
24667
24838
  }
@@ -24688,7 +24859,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24688
24859
  process.exit(EXIT_USAGE);
24689
24860
  }
24690
24861
  const mode = modeRaw;
24691
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24862
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24692
24863
  if (dryRun) {
24693
24864
  console.log(
24694
24865
  JSON.stringify(
@@ -24732,14 +24903,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24732
24903
  function runNew(rawArgs) {
24733
24904
  const { values, positionals } = parseArgs({
24734
24905
  args: rawArgs,
24735
- options: { dir: { type: "string" }, force: { type: "boolean", default: false } },
24906
+ options: {
24907
+ dir: { type: "string" },
24908
+ force: { type: "boolean", default: false },
24909
+ "scenario-id": { type: "string" }
24910
+ },
24736
24911
  allowPositionals: true,
24737
24912
  strict: true
24738
24913
  });
24739
24914
  const template = positionals[0];
24740
24915
  const name = positionals.slice(1).join(" ");
24741
24916
  if (!template) {
24742
- console.error(`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--force]`);
24917
+ console.error(
24918
+ `Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
24919
+ );
24743
24920
  console.error(`Templates: ${TEMPLATES.join(", ")}`);
24744
24921
  return EXIT_USAGE;
24745
24922
  }
@@ -24747,6 +24924,7 @@ function runNew(rawArgs) {
24747
24924
  const result = scaffoldDoc({
24748
24925
  template,
24749
24926
  name,
24927
+ scenarioId: values["scenario-id"],
24750
24928
  baseDir: values.dir,
24751
24929
  force: values.force
24752
24930
  });
@@ -24849,6 +25027,7 @@ async function runBuildDocs(rawArgs) {
24849
25027
  console.log(`\u2713 Living docs generated in ${result.siteDir}`);
24850
25028
  console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
24851
25029
  console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
25030
+ console.log(` \u2022 Note links \u2192 public/stories/notes-index.json (${result.notesIndexed})`);
24852
25031
  if (audienceSplit) {
24853
25032
  console.log(
24854
25033
  ` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
@@ -24868,7 +25047,7 @@ async function runBuildDocs(rawArgs) {
24868
25047
  ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
24869
25048
  );
24870
25049
  }
24871
- const rel = path15.relative(process.cwd(), result.siteDir) || ".";
25050
+ const rel = path17.relative(process.cwd(), result.siteDir) || ".";
24872
25051
  console.log(`
24873
25052
  Preview: cd ${rel} && npm run dev`);
24874
25053
  return EXIT_SUCCESS;