executable-stories-formatters 0.14.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
  }
@@ -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",
@@ -22064,7 +22070,13 @@ function copyDirRecursive(src, dest, onFile, baseSrc = src) {
22064
22070
  // src/scaffold-doc.ts
22065
22071
  import * as fs10 from "fs";
22066
22072
  import * as path11 from "path";
22067
- var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
22073
+ var TEMPLATES = [
22074
+ "adr",
22075
+ "runbook",
22076
+ "decision-log",
22077
+ "incident",
22078
+ "scenario-note"
22079
+ ];
22068
22080
  function slugify3(input) {
22069
22081
  return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
22070
22082
  }
@@ -22193,6 +22205,30 @@ _How it was fixed._
22193
22205
 
22194
22206
  - [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
22195
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_
22196
22232
  `
22197
22233
  }
22198
22234
  };
@@ -22211,10 +22247,15 @@ function scaffoldDoc(options) {
22211
22247
  const today = options.today ?? /* @__PURE__ */ new Date();
22212
22248
  const name = (options.name ?? "").trim() || defaultName(template);
22213
22249
  const slug2 = slugify3(name);
22250
+ const scenarioId = normalizeScenarioId(options.scenarioId);
22214
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
+ }
22215
22255
  const ctx = {
22216
22256
  name,
22217
22257
  slug: slug2,
22258
+ scenarioId,
22218
22259
  isoDate: isoDate(today),
22219
22260
  seq: nextSeq(dir)
22220
22261
  };
@@ -22239,6 +22280,8 @@ function defaultName(template) {
22239
22280
  return "Decisions";
22240
22281
  case "incident":
22241
22282
  return "Untitled incident";
22283
+ case "scenario-note":
22284
+ return "Untitled scenario note";
22242
22285
  }
22243
22286
  }
22244
22287
  function titleFor2(template, ctx) {
@@ -22251,12 +22294,43 @@ function titleFor2(template, ctx) {
22251
22294
  return `Decision log \u2014 ${ctx.name}`;
22252
22295
  case "incident":
22253
22296
  return `Incident \u2014 ${ctx.name}`;
22297
+ case "scenario-note":
22298
+ return `Business context \u2014 ${ctx.name}`;
22299
+ }
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.`);
22254
22306
  }
22307
+ return value;
22255
22308
  }
22256
22309
 
22257
22310
  // src/check-links.ts
22311
+ import * as fs12 from "fs";
22312
+ import * as path13 from "path";
22313
+
22314
+ // src/utils/markdown-files.ts
22258
22315
  import * as fs11 from "fs";
22259
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
22260
22334
  function stripCode(markdown) {
22261
22335
  let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
22262
22336
  out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
@@ -22286,34 +22360,19 @@ function classifyLink(link2) {
22286
22360
  function resolutionCandidates(fromFile, link2) {
22287
22361
  const withoutAnchor = link2.split("#")[0];
22288
22362
  if (!withoutAnchor) return [];
22289
- const base = path12.resolve(path12.dirname(fromFile), withoutAnchor);
22363
+ const base = path13.resolve(path13.dirname(fromFile), withoutAnchor);
22290
22364
  const candidates = [base];
22291
- if (!path12.extname(base)) {
22365
+ if (!path13.extname(base)) {
22292
22366
  candidates.push(`${base}.md`, `${base}.mdx`);
22293
- 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"));
22294
22368
  }
22295
22369
  return candidates;
22296
22370
  }
22297
22371
  function resolvesOnDisk(fromFile, link2) {
22298
22372
  return resolutionCandidates(fromFile, link2).some(
22299
- (candidate) => fs11.existsSync(candidate) && fs11.statSync(candidate).isFile()
22373
+ (candidate) => fs12.existsSync(candidate) && fs12.statSync(candidate).isFile()
22300
22374
  );
22301
22375
  }
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
22376
  async function isExternalAlive(url, timeoutMs) {
22318
22377
  const attempt = async (method) => {
22319
22378
  const controller = new AbortController();
@@ -22337,17 +22396,17 @@ async function isExternalAlive(url, timeoutMs) {
22337
22396
  }
22338
22397
  async function checkLinks(options) {
22339
22398
  const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
22340
- if (!fs11.existsSync(target)) {
22399
+ if (!fs12.existsSync(target)) {
22341
22400
  throw new Error(`Path not found: ${target}`);
22342
22401
  }
22343
- const files = collectDocFiles(target);
22402
+ const files = collectMarkdownFiles(target);
22344
22403
  const broken = [];
22345
22404
  let linksChecked = 0;
22346
22405
  let externalChecked = 0;
22347
22406
  let skipped = 0;
22348
22407
  const externalCache = /* @__PURE__ */ new Map();
22349
22408
  for (const file of files) {
22350
- const content = fs11.readFileSync(file, "utf8");
22409
+ const content = fs12.readFileSync(file, "utf8");
22351
22410
  for (const link2 of extractLinks(content)) {
22352
22411
  const kind = classifyLink(link2);
22353
22412
  if (kind === "anchor" || kind === "mail" || kind === "root") {
@@ -22403,8 +22462,8 @@ function formatLinkReport(report) {
22403
22462
  }
22404
22463
 
22405
22464
  // src/import-openapi.ts
22406
- import * as fs12 from "fs";
22407
- import * as path13 from "path";
22465
+ import * as fs13 from "fs";
22466
+ import * as path14 from "path";
22408
22467
  import { parse as parseYamlString } from "yaml";
22409
22468
  var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
22410
22469
  function parseYaml(raw, specPath) {
@@ -22417,9 +22476,9 @@ function parseYaml(raw, specPath) {
22417
22476
  }
22418
22477
  }
22419
22478
  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();
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();
22423
22482
  if (ext === ".json") return JSON.parse(raw);
22424
22483
  if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
22425
22484
  try {
@@ -22450,8 +22509,8 @@ function extractEndpoints(spec) {
22450
22509
  }
22451
22510
  function loadScenarios(runFile) {
22452
22511
  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"));
22512
+ if (!fs13.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
22513
+ const report = JSON.parse(fs13.readFileSync(runFile, "utf8"));
22455
22514
  return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
22456
22515
  }
22457
22516
  function endpointRefs(endpoint) {
@@ -22558,25 +22617,25 @@ async function importOpenApi(options) {
22558
22617
  list.push(item);
22559
22618
  groups.set(item.endpoint.tag, list);
22560
22619
  }
22561
- const outputDir = options.outputDir ?? path13.join("src", "content", "docs", "api");
22562
- if (fs12.existsSync(outputDir) && !options.force) {
22563
- 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);
22564
22623
  if (entries.length > 0) {
22565
22624
  throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
22566
22625
  }
22567
22626
  }
22568
- fs12.mkdirSync(outputDir, { recursive: true });
22627
+ fs13.mkdirSync(outputDir, { recursive: true });
22569
22628
  const coveredCount = coverage.filter((c) => c.status === "covered").length;
22570
22629
  const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
22571
- fs12.writeFileSync(
22572
- path13.join(outputDir, "index.mdx"),
22630
+ fs13.writeFileSync(
22631
+ path14.join(outputDir, "index.mdx"),
22573
22632
  renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
22574
22633
  "utf8"
22575
22634
  );
22576
22635
  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");
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");
22580
22639
  }
22581
22640
  return {
22582
22641
  outputDir,
@@ -22588,8 +22647,8 @@ async function importOpenApi(options) {
22588
22647
  }
22589
22648
 
22590
22649
  // src/build-docs.ts
22591
- import * as fs13 from "fs";
22592
- import * as path14 from "path";
22650
+ import * as fs15 from "fs";
22651
+ import * as path16 from "path";
22593
22652
 
22594
22653
  // src/scenario-links.ts
22595
22654
  function scenarioAnchor(title) {
@@ -22685,6 +22744,58 @@ ${body.join("\n")}
22685
22744
  `;
22686
22745
  }
22687
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
+
22688
22799
  // src/overview-page.ts
22689
22800
  var AUDIENCE_CARDS = [
22690
22801
  {
@@ -22712,11 +22823,12 @@ function yamlScalar3(value) {
22712
22823
  }
22713
22824
  return value;
22714
22825
  }
22715
- function renderOverviewPage(links) {
22826
+ function renderOverviewPage(links, notesIndex) {
22716
22827
  const all = Object.values(links.scenarios);
22717
22828
  const total = all.length;
22718
22829
  const passed = all.filter((s) => s.status === "passed").length;
22719
22830
  const failed = all.filter((s) => s.status === "failed").length;
22831
+ const notesById = notesByScenarioId(notesIndex ?? { schemaVersion: "1.0", notes: [] });
22720
22832
  const frontmatter = [
22721
22833
  "---",
22722
22834
  "title: Stories",
@@ -22740,7 +22852,9 @@ function renderOverviewPage(links) {
22740
22852
  body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
22741
22853
  body.push(`${card.blurb}`, "");
22742
22854
  for (const s of scenariosSorted(scenarios)) {
22743
- body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})`);
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}`);
22744
22858
  }
22745
22859
  body.push("");
22746
22860
  }
@@ -22782,22 +22896,22 @@ var BuildDocsError = class extends Error {
22782
22896
  };
22783
22897
  var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
22784
22898
  function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
22785
- if (!fs13.existsSync(reportPath)) return 0;
22786
- const report = JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22899
+ if (!fs15.existsSync(reportPath)) return 0;
22900
+ const report = JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22787
22901
  let copied = 0;
22788
22902
  const bundle = (value) => {
22789
- const rel = copyAsset(path14.resolve(value), assetsDir);
22903
+ const rel = copyAsset(path16.resolve(value), assetsDir);
22790
22904
  copied++;
22791
- return `${baseUrl}/${path14.basename(rel)}`;
22905
+ return `${baseUrl}/${path16.basename(rel)}`;
22792
22906
  };
22793
22907
  const visit = (entries) => {
22794
22908
  for (const entry of entries ?? []) {
22795
22909
  const e = entry;
22796
22910
  if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
22797
- 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)) {
22798
22912
  e.path = bundle(e.path);
22799
22913
  }
22800
- 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)) {
22801
22915
  e.poster = bundle(e.poster);
22802
22916
  }
22803
22917
  }
@@ -22810,7 +22924,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22810
22924
  }
22811
22925
  }
22812
22926
  if (copied > 0) {
22813
- fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22927
+ fs15.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
22814
22928
  }
22815
22929
  return copied;
22816
22930
  }
@@ -22819,50 +22933,69 @@ var CHANGE_BADGE = {
22819
22933
  fixed: "\u2705 **Fixed** _since last run_",
22820
22934
  regressed: "\u26A0\uFE0F **Regressed** _since last run_"
22821
22935
  };
22936
+ function scenarioKey(sourceFile, title) {
22937
+ return `${sourceFile}\0${title}`;
22938
+ }
22822
22939
  function changeBadgeLookup(diff) {
22823
22940
  if (!diff) return void 0;
22824
22941
  const byKey = /* @__PURE__ */ new Map();
22825
22942
  for (const s of diff.scenarios) {
22826
22943
  const badge2 = CHANGE_BADGE[s.kind];
22827
- if (badge2) byKey.set(`${s.sourceFile}\0${s.title}`, badge2);
22944
+ if (badge2) byKey.set(scenarioKey(s.sourceFile, s.title), badge2);
22828
22945
  }
22829
22946
  if (byKey.size === 0) return void 0;
22830
- return (tc) => byKey.get(`${tc.sourceFile}\0${tc.story.scenario}`);
22947
+ return (tc) => byKey.get(scenarioKey(tc.sourceFile, tc.story.scenario));
22831
22948
  }
22832
22949
  function readStoryReport(reportPath) {
22833
- if (!fs13.existsSync(reportPath)) return null;
22950
+ if (!fs15.existsSync(reportPath)) return null;
22834
22951
  try {
22835
- return JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22952
+ return JSON.parse(fs15.readFileSync(reportPath, "utf8"));
22836
22953
  } catch {
22837
22954
  return null;
22838
22955
  }
22839
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
+ }
22840
22973
  function writeScenarioLinks(reportPath, outDir, options = {}) {
22841
22974
  const report = readStoryReport(reportPath);
22842
22975
  if (!report) return null;
22843
22976
  const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22844
- fs13.writeFileSync(
22845
- path14.join(outDir, "scenario-links.json"),
22977
+ fs15.writeFileSync(
22978
+ path16.join(outDir, "scenario-links.json"),
22846
22979
  JSON.stringify(index, null, 2),
22847
22980
  "utf8"
22848
22981
  );
22849
22982
  return index;
22850
22983
  }
22851
22984
  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);
22985
+ if (!fs15.existsSync(dir)) return;
22986
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
22987
+ const full = path16.join(dir, entry.name);
22855
22988
  if (entry.isDirectory()) {
22856
22989
  clearGeneratedPages(full);
22857
- if (fs13.readdirSync(full).length === 0) fs13.rmdirSync(full);
22990
+ if (fs15.readdirSync(full).length === 0) fs15.rmdirSync(full);
22858
22991
  } else if (/\.mdx?$/.test(entry.name)) {
22859
- fs13.rmSync(full);
22992
+ fs15.rmSync(full);
22860
22993
  }
22861
22994
  }
22862
22995
  }
22863
22996
  function loadCanonicalRun(rawRunPath, synthesize) {
22864
22997
  try {
22865
- const data = JSON.parse(fs13.readFileSync(path14.resolve(rawRunPath), "utf8"));
22998
+ const data = JSON.parse(fs15.readFileSync(path16.resolve(rawRunPath), "utf8"));
22866
22999
  if (data.schemaVersion !== 1) {
22867
23000
  throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
22868
23001
  }
@@ -22885,12 +23018,13 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
22885
23018
  }
22886
23019
  }
22887
23020
  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");
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");
22894
23028
  const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
22895
23029
  try {
22896
23030
  await new ReportGenerator({
@@ -22898,9 +23032,10 @@ async function buildDocs(options) {
22898
23032
  outputDir: storiesPublicDir,
22899
23033
  outputName: "story-report"
22900
23034
  }).generate(canonical);
23035
+ const currentReport = readStoryReport(reportPath);
22901
23036
  let diff;
22902
23037
  if (options.baselinePath) {
22903
- const baselineResolved = path14.resolve(options.baselinePath);
23038
+ const baselineResolved = path16.resolve(options.baselinePath);
22904
23039
  const baseline = readStoryReport(baselineResolved);
22905
23040
  if (!baseline) {
22906
23041
  throw new BuildDocsError(
@@ -22908,10 +23043,11 @@ async function buildDocs(options) {
22908
23043
  "input"
22909
23044
  );
22910
23045
  }
22911
- const current = readStoryReport(reportPath);
22912
- if (current) diff = diffStoryReports(baseline, current);
23046
+ if (currentReport) diff = diffStoryReports(baseline, currentReport);
22913
23047
  }
22914
23048
  const scenarioBadge = changeBadgeLookup(diff);
23049
+ const notesIndex = buildScenarioNotesIndex(notesDir);
23050
+ const scenarioNoteLink = noteLinkLookup(currentReport, notesIndex);
22915
23051
  clearGeneratedPages(storyPagesDir);
22916
23052
  const genPages = (run, outDir) => new ReportGenerator({
22917
23053
  formats: ["astro"],
@@ -22925,7 +23061,8 @@ async function buildDocs(options) {
22925
23061
  markdown: {
22926
23062
  // Emit the same anchor scenario-links.json points at, so fragments resolve.
22927
23063
  scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
22928
- scenarioBadge
23064
+ scenarioBadge,
23065
+ scenarioNoteLink
22929
23066
  }
22930
23067
  }
22931
23068
  }).generate(run);
@@ -22936,7 +23073,7 @@ async function buildDocs(options) {
22936
23073
  const sub = partitioned[audience];
22937
23074
  audiences[audience] = sub.testCases.length;
22938
23075
  if (sub.testCases.length === 0) continue;
22939
- await genPages(sub, path14.join(storyPagesDir, audience));
23076
+ await genPages(sub, path16.join(storyPagesDir, audience));
22940
23077
  }
22941
23078
  } else {
22942
23079
  await genPages(canonical, storyPagesDir);
@@ -22946,35 +23083,37 @@ async function buildDocs(options) {
22946
23083
  audienceSplit: options.audienceSplit ?? false
22947
23084
  });
22948
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;
22949
23088
  if (linksIndex) {
22950
- fs13.writeFileSync(
22951
- path14.join(storyPagesDir, "index.md"),
22952
- renderOverviewPage(linksIndex),
23089
+ fs15.writeFileSync(
23090
+ path16.join(storyPagesDir, "index.md"),
23091
+ renderOverviewPage(linksIndex, notesIndex),
22953
23092
  "utf8"
22954
23093
  );
22955
23094
  }
22956
- const changesJsonPath = path14.join(storiesPublicDir, "changes.json");
22957
- const changesMdPath = path14.join(storyPagesDir, "changes.md");
23095
+ const changesJsonPath = path16.join(storiesPublicDir, "changes.json");
23096
+ const changesMdPath = path16.join(storyPagesDir, "changes.md");
22958
23097
  let changes;
22959
23098
  if (diff && linksIndex) {
22960
- fs13.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
22961
- fs13.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
23099
+ fs15.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
23100
+ fs15.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
22962
23101
  changes = diff.summary;
22963
23102
  } else {
22964
- fs13.rmSync(changesJsonPath, { force: true });
22965
- fs13.rmSync(changesMdPath, { force: true });
23103
+ fs15.rmSync(changesJsonPath, { force: true });
23104
+ fs15.rmSync(changesMdPath, { force: true });
22966
23105
  }
22967
23106
  let apiPages = 0;
22968
23107
  if (options.openapiPath) {
22969
23108
  const res = await importOpenApi({
22970
- specPath: path14.resolve(options.openapiPath),
23109
+ specPath: path16.resolve(options.openapiPath),
22971
23110
  outputDir: apiDir,
22972
23111
  runFile: reportPath,
22973
23112
  force: true
22974
23113
  });
22975
23114
  apiPages = res.pageCount;
22976
23115
  }
22977
- return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
23116
+ return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, notesIndexed, changes };
22978
23117
  } catch (err) {
22979
23118
  if (err instanceof BuildDocsError) throw err;
22980
23119
  throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
@@ -22982,11 +23121,11 @@ async function buildDocs(options) {
22982
23121
  }
22983
23122
 
22984
23123
  // src/config.ts
22985
- import { existsSync as existsSync12 } from "fs";
23124
+ import { existsSync as existsSync13 } from "fs";
22986
23125
  import { resolve as resolve10 } from "path";
22987
23126
  async function loadConfig(configPath) {
22988
23127
  const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
22989
- if (!existsSync12(resolved)) return {};
23128
+ if (!existsSync13(resolved)) return {};
22990
23129
  const mod = await import(resolved);
22991
23130
  const config = mod.default;
22992
23131
  if (!config || typeof config !== "object" || Array.isArray(config)) {
@@ -23044,7 +23183,7 @@ SUBCOMMANDS
23044
23183
  triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
23045
23184
  validate Validate a JSON file against the schema (no output generated)
23046
23185
  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)
23186
+ new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
23048
23187
  check-links Scan docs for broken internal/external links (CI-friendly exit code)
23049
23188
  import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
23050
23189
  publish-confluence Publish an ADF JSON file to a Confluence page via REST API
@@ -23583,20 +23722,20 @@ async function readInput(args) {
23583
23722
  if (args.stdin) {
23584
23723
  return readStdin();
23585
23724
  }
23586
- const filePath = path15.resolve(args.inputFile);
23587
- if (!fs14.existsSync(filePath)) {
23725
+ const filePath = path17.resolve(args.inputFile);
23726
+ if (!fs16.existsSync(filePath)) {
23588
23727
  console.error(`Error: File not found: ${filePath}`);
23589
23728
  process.exit(EXIT_USAGE);
23590
23729
  }
23591
- return fs14.readFileSync(filePath, "utf8");
23730
+ return fs16.readFileSync(filePath, "utf8");
23592
23731
  }
23593
23732
  function readFileInput(filePath) {
23594
- const resolved = path15.resolve(filePath);
23595
- if (!fs14.existsSync(resolved)) {
23733
+ const resolved = path17.resolve(filePath);
23734
+ if (!fs16.existsSync(resolved)) {
23596
23735
  console.error(`Error: File not found: ${resolved}`);
23597
23736
  process.exit(EXIT_USAGE);
23598
23737
  }
23599
- return fs14.readFileSync(resolved, "utf8");
23738
+ return fs16.readFileSync(resolved, "utf8");
23600
23739
  }
23601
23740
  function readStdin() {
23602
23741
  return new Promise((resolve12, reject) => {
@@ -23729,14 +23868,14 @@ function tryNormalizeRunFromText(text2, args) {
23729
23868
  }
23730
23869
  }
23731
23870
  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)) {
23871
+ const baselineDir = path17.resolve(args.baselineDir ?? path17.dirname(currentFile));
23872
+ const currentResolved = path17.resolve(currentFile);
23873
+ if (!fs16.existsSync(baselineDir)) {
23735
23874
  console.error(`Error: baseline directory not found: ${baselineDir}`);
23736
23875
  process.exit(EXIT_USAGE);
23737
23876
  }
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(
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(
23740
23879
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
23741
23880
  );
23742
23881
  }
@@ -23744,14 +23883,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
23744
23883
  const candidates = listBaselineCandidates(currentFile, args);
23745
23884
  const comparable = [];
23746
23885
  for (const candidate of candidates) {
23747
- const run = tryNormalizeRunFromText(fs14.readFileSync(candidate, "utf8"), args);
23886
+ const run = tryNormalizeRunFromText(fs16.readFileSync(candidate, "utf8"), args);
23748
23887
  if (run) {
23749
23888
  comparable.push({ file: candidate, run });
23750
23889
  }
23751
23890
  }
23752
23891
  if (comparable.length === 0) {
23753
23892
  console.error(
23754
- `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))}.`
23755
23894
  );
23756
23895
  process.exit(EXIT_USAGE);
23757
23896
  }
@@ -23996,9 +24135,9 @@ async function main() {
23996
24135
  process.exit(EXIT_SCHEMA_VALIDATION);
23997
24136
  }
23998
24137
  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");
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");
24002
24141
  }
24003
24142
  try {
24004
24143
  const result = await generateReports(run, args);
@@ -24055,9 +24194,9 @@ ${msg}`);
24055
24194
  }
24056
24195
  const run = data;
24057
24196
  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");
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");
24061
24200
  }
24062
24201
  try {
24063
24202
  const result = await generateReports(run, args);
@@ -24113,9 +24252,9 @@ ${msg}`);
24113
24252
  process.exit(EXIT_CANONICAL_VALIDATION);
24114
24253
  }
24115
24254
  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");
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");
24119
24258
  }
24120
24259
  try {
24121
24260
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -24140,9 +24279,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
24140
24279
  const ext = formatter.fileExtension ?? formatName;
24141
24280
  const baseName = args.outputName ?? "report";
24142
24281
  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");
24282
+ const filepath = path17.join(outputDir, filename);
24283
+ fs16.mkdirSync(outputDir, { recursive: true });
24284
+ fs16.writeFileSync(filepath, content, "utf8");
24146
24285
  console.log(`Generated: ${filepath}`);
24147
24286
  } catch (err) {
24148
24287
  console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
@@ -24192,13 +24331,13 @@ async function dispatchNotifications(run, args) {
24192
24331
  }
24193
24332
  function runHistoryPipeline(run, args) {
24194
24333
  if (!args.historyFile) return;
24195
- const historyPath = path15.resolve(args.historyFile);
24334
+ const historyPath = path17.resolve(args.historyFile);
24196
24335
  const store = loadHistory(
24197
24336
  { filePath: historyPath },
24198
24337
  {
24199
24338
  readFile: (p) => {
24200
24339
  try {
24201
- return fs14.readFileSync(p, "utf8");
24340
+ return fs16.readFileSync(p, "utf8");
24202
24341
  } catch {
24203
24342
  return void 0;
24204
24343
  }
@@ -24211,11 +24350,11 @@ function runHistoryPipeline(run, args) {
24211
24350
  run,
24212
24351
  maxRuns: args.maxHistoryRuns
24213
24352
  });
24214
- const dir = path15.dirname(historyPath);
24215
- fs14.mkdirSync(dir, { recursive: true });
24353
+ const dir = path17.dirname(historyPath);
24354
+ fs16.mkdirSync(dir, { recursive: true });
24216
24355
  saveHistory(
24217
24356
  { filePath: historyPath, store: updated },
24218
- { writeFile: (p, content) => fs14.writeFileSync(p, content, "utf8") }
24357
+ { writeFile: (p, content) => fs16.writeFileSync(p, content, "utf8") }
24219
24358
  );
24220
24359
  let metricsCount = 0;
24221
24360
  for (const testId of Object.keys(updated.tests)) {
@@ -24363,11 +24502,11 @@ function writeReviewReport(review, args) {
24363
24502
  const outputDir = args.outputDir ?? "reports";
24364
24503
  const baseName = args.outputName ?? "evidence-review";
24365
24504
  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");
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");
24371
24510
  return [mdPath, htmlPath];
24372
24511
  }
24373
24512
  function evaluateReviewGate(review, args) {
@@ -24413,9 +24552,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
24413
24552
  function printCompareResult(result, args, startMs) {
24414
24553
  const durationMs = Date.now() - startMs;
24415
24554
  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");
24555
+ const outputPath = path17.resolve(args.prSummaryFile);
24556
+ fs16.mkdirSync(path17.dirname(outputPath), { recursive: true });
24557
+ fs16.writeFileSync(outputPath, result.prSummary, "utf8");
24419
24558
  }
24420
24559
  if (args.jsonSummary) {
24421
24560
  console.log(
@@ -24444,13 +24583,13 @@ function printCompareResult(result, args, startMs) {
24444
24583
  }
24445
24584
  }
24446
24585
  function loadReleasePolicy(policyPath) {
24447
- const resolved = path15.resolve(policyPath);
24448
- if (!fs14.existsSync(resolved)) {
24586
+ const resolved = path17.resolve(policyPath);
24587
+ if (!fs16.existsSync(resolved)) {
24449
24588
  console.error(`Error: release policy file not found: ${resolved}`);
24450
24589
  process.exit(EXIT_USAGE);
24451
24590
  }
24452
24591
  try {
24453
- const raw = JSON.parse(fs14.readFileSync(resolved, "utf8"));
24592
+ const raw = JSON.parse(fs16.readFileSync(resolved, "utf8"));
24454
24593
  return {
24455
24594
  allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
24456
24595
  allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
@@ -24554,7 +24693,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24554
24693
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24555
24694
  process.exit(EXIT_USAGE);
24556
24695
  }
24557
- if (!fs14.existsSync(inputFile)) {
24696
+ if (!fs16.existsSync(inputFile)) {
24558
24697
  console.error(`Error: file not found: ${inputFile}`);
24559
24698
  process.exit(EXIT_USAGE);
24560
24699
  }
@@ -24582,7 +24721,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24582
24721
  console.error("Error: --title is required when creating a new page");
24583
24722
  process.exit(EXIT_USAGE);
24584
24723
  }
24585
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24724
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24586
24725
  if (dryRun) {
24587
24726
  console.log(
24588
24727
  JSON.stringify(
@@ -24661,7 +24800,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24661
24800
  console.error("Error: missing ADF file argument. Run with --help for usage.");
24662
24801
  process.exit(EXIT_USAGE);
24663
24802
  }
24664
- if (!fs14.existsSync(inputFile)) {
24803
+ if (!fs16.existsSync(inputFile)) {
24665
24804
  console.error(`Error: file not found: ${inputFile}`);
24666
24805
  process.exit(EXIT_USAGE);
24667
24806
  }
@@ -24688,7 +24827,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24688
24827
  process.exit(EXIT_USAGE);
24689
24828
  }
24690
24829
  const mode = modeRaw;
24691
- const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
24830
+ const adf = fs16.readFileSync(path17.resolve(inputFile), "utf8");
24692
24831
  if (dryRun) {
24693
24832
  console.log(
24694
24833
  JSON.stringify(
@@ -24732,14 +24871,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
24732
24871
  function runNew(rawArgs) {
24733
24872
  const { values, positionals } = parseArgs({
24734
24873
  args: rawArgs,
24735
- 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
+ },
24736
24879
  allowPositionals: true,
24737
24880
  strict: true
24738
24881
  });
24739
24882
  const template = positionals[0];
24740
24883
  const name = positionals.slice(1).join(" ");
24741
24884
  if (!template) {
24742
- 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
+ );
24743
24888
  console.error(`Templates: ${TEMPLATES.join(", ")}`);
24744
24889
  return EXIT_USAGE;
24745
24890
  }
@@ -24747,6 +24892,7 @@ function runNew(rawArgs) {
24747
24892
  const result = scaffoldDoc({
24748
24893
  template,
24749
24894
  name,
24895
+ scenarioId: values["scenario-id"],
24750
24896
  baseDir: values.dir,
24751
24897
  force: values.force
24752
24898
  });
@@ -24849,6 +24995,7 @@ async function runBuildDocs(rawArgs) {
24849
24995
  console.log(`\u2713 Living docs generated in ${result.siteDir}`);
24850
24996
  console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
24851
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})`);
24852
24999
  if (audienceSplit) {
24853
25000
  console.log(
24854
25001
  ` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
@@ -24868,7 +25015,7 @@ async function runBuildDocs(rawArgs) {
24868
25015
  ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
24869
25016
  );
24870
25017
  }
24871
- const rel = path15.relative(process.cwd(), result.siteDir) || ".";
25018
+ const rel = path17.relative(process.cwd(), result.siteDir) || ".";
24872
25019
  console.log(`
24873
25020
  Preview: cd ${rel} && npm run dev`);
24874
25021
  return EXIT_SUCCESS;