executable-stories-formatters 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -191,6 +191,11 @@ var raw_run_schema_default = {
191
191
  },
192
192
  description: "Ticket/issue references. Each item is either a string ID or an object with id and optional url."
193
193
  },
194
+ covers: {
195
+ type: "array",
196
+ items: { type: "string" },
197
+ description: "Product-code paths/globs this scenario exercises (project-root-relative). Used to map code changes to at-risk scenarios."
198
+ },
194
199
  meta: {
195
200
  type: "object",
196
201
  description: "User-defined metadata for this story."
@@ -512,6 +517,10 @@ var raw_run_schema_default = {
512
517
  minimum: 0,
513
518
  description: "Step index (0-based)."
514
519
  },
520
+ stepId: {
521
+ type: "string",
522
+ description: "Stable step ID when the framework provides one (correlates with StoryStep.id)."
523
+ },
515
524
  title: {
516
525
  type: "string",
517
526
  description: "Step title/description."
@@ -1228,7 +1237,7 @@ var CucumberJsonFormatter = class {
1228
1237
  /**
1229
1238
  * Build a single step.
1230
1239
  */
1231
- buildStep(step, result, line, index, attachments, isLastStep, hasFailedStep) {
1240
+ buildStep(step, result, line2, index, attachments, isLastStep, hasFailedStep) {
1232
1241
  const keyword = this.options.keywordSpacing ? `${step.keyword} ` : step.keyword;
1233
1242
  const stepResult = this.buildStepResult(result);
1234
1243
  const embeddings = this.buildEmbeddings(attachments, result, isLastStep, hasFailedStep);
@@ -1236,7 +1245,7 @@ var CucumberJsonFormatter = class {
1236
1245
  embeddings.push(...screenshotEmbeddings);
1237
1246
  const jsonStep = {
1238
1247
  keyword,
1239
- line,
1248
+ line: line2,
1240
1249
  name: step.text,
1241
1250
  result: stepResult
1242
1251
  };
@@ -1290,14 +1299,14 @@ var CucumberJsonFormatter = class {
1290
1299
  duration: 0
1291
1300
  };
1292
1301
  }
1293
- const statusMap = {
1302
+ const statusMap2 = {
1294
1303
  passed: "passed",
1295
1304
  failed: "failed",
1296
1305
  skipped: "skipped",
1297
1306
  pending: "pending"
1298
1307
  };
1299
1308
  const stepResult = {
1300
- status: statusMap[result.status] ?? "undefined",
1309
+ status: statusMap2[result.status] ?? "undefined",
1301
1310
  // Duration in nanoseconds (Cucumber uses nanoseconds)
1302
1311
  duration: result.durationMs * 1e6
1303
1312
  };
@@ -15783,8 +15792,8 @@ var JUnitFormatter = class {
15783
15792
  case "section": {
15784
15793
  const lines = [];
15785
15794
  lines.push(`${indent}${entry.title}:`);
15786
- for (const line of entry.markdown.split("\n")) {
15787
- lines.push(`${indent} ${line}`);
15795
+ for (const line2 of entry.markdown.split("\n")) {
15796
+ lines.push(`${indent} ${line2}`);
15788
15797
  }
15789
15798
  return lines.join("\n");
15790
15799
  }
@@ -15793,8 +15802,8 @@ var JUnitFormatter = class {
15793
15802
  if (entry.title) {
15794
15803
  lines.push(`${indent}${entry.title}:`);
15795
15804
  }
15796
- for (const line of entry.code.split("\n")) {
15797
- lines.push(`${indent} ${line}`);
15805
+ for (const line2 of entry.code.split("\n")) {
15806
+ lines.push(`${indent} ${line2}`);
15798
15807
  }
15799
15808
  return lines.join("\n");
15800
15809
  }
@@ -15806,8 +15815,8 @@ var JUnitFormatter = class {
15806
15815
  const dataStr = JSON.stringify(entry.data, null, 2);
15807
15816
  const lines = [];
15808
15817
  lines.push(`${indent}[${entry.type}]:`);
15809
- for (const line of dataStr.split("\n")) {
15810
- lines.push(`${indent} ${line}`);
15818
+ for (const line2 of dataStr.split("\n")) {
15819
+ lines.push(`${indent} ${line2}`);
15811
15820
  }
15812
15821
  return lines.join("\n");
15813
15822
  }
@@ -15853,7 +15862,9 @@ var MarkdownFormatter = class {
15853
15862
  ticketUrlTemplate: options.ticketUrlTemplate,
15854
15863
  traceUrlTemplate: options.traceUrlTemplate,
15855
15864
  includeSourceLinks: options.includeSourceLinks ?? true,
15856
- customRenderers: options.customRenderers
15865
+ customRenderers: options.customRenderers,
15866
+ scenarioAnchor: options.scenarioAnchor,
15867
+ scenarioBadge: options.scenarioBadge
15857
15868
  };
15858
15869
  }
15859
15870
  /**
@@ -16040,6 +16051,11 @@ var MarkdownFormatter = class {
16040
16051
  * Render a single scenario.
16041
16052
  */
16042
16053
  renderScenario(lines, tc) {
16054
+ const anchorId = this.options.scenarioAnchor?.(tc);
16055
+ if (anchorId) {
16056
+ lines.push(`<a id="${anchorId}"></a>`);
16057
+ lines.push("");
16058
+ }
16043
16059
  if (this.options.customRenderers?.renderScenarioHeader) {
16044
16060
  const custom = this.options.customRenderers.renderScenarioHeader(tc);
16045
16061
  if (custom !== null) {
@@ -16055,6 +16071,10 @@ var MarkdownFormatter = class {
16055
16071
  icon = this.getStatusIcon(tc.status) + " ";
16056
16072
  }
16057
16073
  lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
16074
+ const badge2 = this.options.scenarioBadge?.(tc);
16075
+ if (badge2) {
16076
+ lines.push(badge2);
16077
+ }
16058
16078
  if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
16059
16079
  const permalink = this.buildPermalink(tc);
16060
16080
  lines.push(`Source: [${tc.sourceFile}](${permalink})`);
@@ -16129,8 +16149,8 @@ var MarkdownFormatter = class {
16129
16149
  buildPermalink(tc) {
16130
16150
  const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
16131
16151
  const file = tc.sourceFile;
16132
- const line = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16133
- return `${base}/${file}${line}`;
16152
+ const line2 = tc.sourceLine > 0 ? `#L${tc.sourceLine}` : "";
16153
+ return `${base}/${file}${line2}`;
16134
16154
  }
16135
16155
  /**
16136
16156
  * Render a step.
@@ -16198,8 +16218,8 @@ var MarkdownFormatter = class {
16198
16218
  lines.push(`${indent}`);
16199
16219
  }
16200
16220
  lines.push(`${indent}\`\`\`${entry.lang ?? ""}`);
16201
- for (const line of (entry.content ?? "").split("\n")) {
16202
- lines.push(`${indent}${line}`);
16221
+ for (const line2 of (entry.content ?? "").split("\n")) {
16222
+ lines.push(`${indent}${line2}`);
16203
16223
  }
16204
16224
  lines.push(`${indent}\`\`\``);
16205
16225
  lines.push(`${indent}`);
@@ -16222,8 +16242,8 @@ var MarkdownFormatter = class {
16222
16242
  case "section":
16223
16243
  lines.push(`${indent}**${entry.title}**`);
16224
16244
  lines.push(`${indent}`);
16225
- for (const line of (entry.markdown ?? "").split("\n")) {
16226
- lines.push(`${indent}${line}`);
16245
+ for (const line2 of (entry.markdown ?? "").split("\n")) {
16246
+ lines.push(`${indent}${line2}`);
16227
16247
  }
16228
16248
  lines.push(`${indent}`);
16229
16249
  break;
@@ -16232,8 +16252,8 @@ var MarkdownFormatter = class {
16232
16252
  lines.push(`${indent}**${entry.title}**`);
16233
16253
  }
16234
16254
  lines.push(`${indent}\`\`\`mermaid`);
16235
- for (const line of (entry.code ?? "").split("\n")) {
16236
- lines.push(`${indent}${line}`);
16255
+ for (const line2 of (entry.code ?? "").split("\n")) {
16256
+ lines.push(`${indent}${line2}`);
16237
16257
  }
16238
16258
  lines.push(`${indent}\`\`\``);
16239
16259
  break;
@@ -16263,8 +16283,8 @@ var MarkdownFormatter = class {
16263
16283
  lines.push(`${indent}<summary>${htmlLabel}</summary>`);
16264
16284
  lines.push(`${indent}`);
16265
16285
  lines.push(`${indent}\`\`\`html`);
16266
- for (const line of (entry.content ?? "").split("\n")) {
16267
- lines.push(`${indent}${line}`);
16286
+ for (const line2 of (entry.content ?? "").split("\n")) {
16287
+ lines.push(`${indent}${line2}`);
16268
16288
  }
16269
16289
  lines.push(`${indent}\`\`\``);
16270
16290
  lines.push(`${indent}`);
@@ -16286,8 +16306,8 @@ var MarkdownFormatter = class {
16286
16306
  lines.push(`${indent}**[${entry.type}]**`);
16287
16307
  lines.push(`${indent}`);
16288
16308
  lines.push(`${indent}\`\`\`json`);
16289
- for (const line of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16290
- lines.push(`${indent}${line}`);
16309
+ for (const line2 of JSON.stringify(entry.data ?? null, null, 2).split("\n")) {
16310
+ lines.push(`${indent}${line2}`);
16291
16311
  }
16292
16312
  lines.push(`${indent}\`\`\``);
16293
16313
  lines.push(`${indent}`);
@@ -16446,6 +16466,132 @@ function escapePipe(value) {
16446
16466
  return value.replace(/\|/g, "\\|");
16447
16467
  }
16448
16468
 
16469
+ // src/formatters/traceability-matrix.ts
16470
+ var TraceabilityMatrixFormatter = class {
16471
+ format(run) {
16472
+ const matrix = toTraceabilityMatrix(run);
16473
+ const lines = [];
16474
+ lines.push("# Traceability Matrix");
16475
+ lines.push("");
16476
+ lines.push(`Generated: ${matrix.generatedAt}`);
16477
+ lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
16478
+ if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
16479
+ if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
16480
+ lines.push("");
16481
+ lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
16482
+ lines.push("| ---: | ---: | ---: | ---: | ---: |");
16483
+ lines.push(
16484
+ `| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
16485
+ );
16486
+ lines.push("");
16487
+ for (const req of matrix.requirements) {
16488
+ const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
16489
+ lines.push(`## ${heading2}`);
16490
+ lines.push("");
16491
+ lines.push(`Status: ${renderRequirementStatus(req.status)}`);
16492
+ if (req.covers.length > 0) {
16493
+ lines.push(`Covers: ${req.covers.map((path16) => `\`${path16}\``).join(", ")}`);
16494
+ }
16495
+ lines.push("");
16496
+ lines.push("| Status | Scenario | Source | Covers |");
16497
+ lines.push("| --- | --- | --- | --- |");
16498
+ for (const scenario of req.scenarios) {
16499
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16500
+ const covers = scenario.covers.length > 0 ? scenario.covers.map((path16) => `\`${path16}\``).join(", ") : "";
16501
+ lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
16502
+ }
16503
+ lines.push("");
16504
+ }
16505
+ if (matrix.untraced.length > 0) {
16506
+ lines.push("## Untraced scenarios");
16507
+ lines.push("");
16508
+ lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
16509
+ lines.push("");
16510
+ lines.push("| Status | Scenario | Source |");
16511
+ lines.push("| --- | --- | --- |");
16512
+ for (const scenario of matrix.untraced) {
16513
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
16514
+ lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
16515
+ }
16516
+ lines.push("");
16517
+ }
16518
+ return lines.join("\n").trimEnd();
16519
+ }
16520
+ };
16521
+ function toTraceabilityMatrix(run) {
16522
+ const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
16523
+ const byTicket = /* @__PURE__ */ new Map();
16524
+ const untraced = [];
16525
+ for (const tc of sorted) {
16526
+ const tickets = tc.story.tickets ?? [];
16527
+ if (tickets.length === 0) {
16528
+ untraced.push({
16529
+ id: tc.id,
16530
+ title: tc.story.scenario,
16531
+ status: tc.status,
16532
+ sourceFile: tc.sourceFile,
16533
+ sourceLine: tc.sourceLine
16534
+ });
16535
+ continue;
16536
+ }
16537
+ for (const ticket of tickets) {
16538
+ const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
16539
+ if (!entry.url && ticket.url) entry.url = ticket.url;
16540
+ entry.cases.push(tc);
16541
+ byTicket.set(ticket.id, entry);
16542
+ }
16543
+ }
16544
+ const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
16545
+ const scenarios = entry.cases.map((tc) => ({
16546
+ id: tc.id,
16547
+ title: tc.story.scenario,
16548
+ status: tc.status,
16549
+ sourceFile: tc.sourceFile,
16550
+ sourceLine: tc.sourceLine,
16551
+ covers: tc.story.covers ?? []
16552
+ }));
16553
+ const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
16554
+ return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
16555
+ });
16556
+ return {
16557
+ schemaVersion: "1.0",
16558
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16559
+ run: {
16560
+ startedAt: new Date(run.startedAtMs).toISOString(),
16561
+ finishedAt: new Date(run.finishedAtMs).toISOString(),
16562
+ gitSha: run.gitSha,
16563
+ branch: run.ci?.branch
16564
+ },
16565
+ summary: {
16566
+ requirements: requirements.length,
16567
+ requirementsVerified: requirements.filter((r) => r.status === "verified").length,
16568
+ requirementsFailing: requirements.filter((r) => r.status === "failing").length,
16569
+ scenarios: run.testCases.length,
16570
+ untracedScenarios: untraced.length
16571
+ },
16572
+ requirements,
16573
+ untraced
16574
+ };
16575
+ }
16576
+ function requirementStatus(cases) {
16577
+ if (cases.some((tc) => tc.status === "failed")) return "failing";
16578
+ if (cases.some((tc) => tc.status === "passed")) return "verified";
16579
+ return "incomplete";
16580
+ }
16581
+ function renderRequirementStatus(status) {
16582
+ switch (status) {
16583
+ case "verified":
16584
+ return "verified (all scenarios passed)";
16585
+ case "failing":
16586
+ return "failing (a scenario failed)";
16587
+ default:
16588
+ return "incomplete (no scenario passed yet)";
16589
+ }
16590
+ }
16591
+ function escapePipe2(value) {
16592
+ return value.replace(/\|/g, "\\|");
16593
+ }
16594
+
16449
16595
  // src/formatters/cucumber-messages/synthesize-feature.ts
16450
16596
  function extractFeatureName(testCases, uri) {
16451
16597
  for (const tc of testCases) {
@@ -16672,60 +16818,60 @@ function buildStepArguments(step, stepLine) {
16672
16818
  }
16673
16819
  return {};
16674
16820
  }
16675
- function docEntryToDocString(doc, line) {
16821
+ function docEntryToDocString(doc, line2) {
16676
16822
  switch (doc.kind) {
16677
16823
  case "code":
16678
16824
  return {
16679
- location: { line },
16825
+ location: { line: line2 },
16680
16826
  mediaType: doc.lang,
16681
16827
  content: doc.content,
16682
16828
  delimiter: '"""'
16683
16829
  };
16684
16830
  case "note":
16685
16831
  return {
16686
- location: { line },
16832
+ location: { line: line2 },
16687
16833
  mediaType: "text/plain",
16688
16834
  content: doc.text,
16689
16835
  delimiter: '"""'
16690
16836
  };
16691
16837
  case "section":
16692
16838
  return {
16693
- location: { line },
16839
+ location: { line: line2 },
16694
16840
  mediaType: "text/markdown",
16695
16841
  content: doc.markdown,
16696
16842
  delimiter: '"""'
16697
16843
  };
16698
16844
  case "mermaid":
16699
16845
  return {
16700
- location: { line },
16846
+ location: { line: line2 },
16701
16847
  mediaType: "text/x-mermaid",
16702
16848
  content: doc.code,
16703
16849
  delimiter: '"""'
16704
16850
  };
16705
16851
  case "kv":
16706
16852
  return {
16707
- location: { line },
16853
+ location: { line: line2 },
16708
16854
  mediaType: "text/plain",
16709
16855
  content: `${doc.label}: ${typeof doc.value === "string" ? doc.value : JSON.stringify(doc.value)}`,
16710
16856
  delimiter: '"""'
16711
16857
  };
16712
16858
  case "link":
16713
16859
  return {
16714
- location: { line },
16860
+ location: { line: line2 },
16715
16861
  mediaType: "text/markdown",
16716
16862
  content: `[${doc.label}](${doc.url})`,
16717
16863
  delimiter: '"""'
16718
16864
  };
16719
16865
  case "custom":
16720
16866
  return {
16721
- location: { line },
16867
+ location: { line: line2 },
16722
16868
  mediaType: "application/json",
16723
16869
  content: JSON.stringify(doc.data, null, 2),
16724
16870
  delimiter: '"""'
16725
16871
  };
16726
16872
  case "tag":
16727
16873
  return {
16728
- location: { line },
16874
+ location: { line: line2 },
16729
16875
  mediaType: "text/plain",
16730
16876
  content: doc.names.map((n) => `@${n}`).join(" "),
16731
16877
  delimiter: '"""'
@@ -16735,18 +16881,18 @@ function docEntryToDocString(doc, line) {
16735
16881
  return void 0;
16736
16882
  }
16737
16883
  }
16738
- function buildDataTable(table2, line) {
16884
+ function buildDataTable(table2, line2) {
16739
16885
  const rows = [];
16740
16886
  rows.push({
16741
- location: { line },
16887
+ location: { line: line2 },
16742
16888
  cells: table2.columns.map((col) => ({
16743
- location: { line },
16889
+ location: { line: line2 },
16744
16890
  value: col
16745
16891
  })),
16746
16892
  id: ""
16747
16893
  });
16748
16894
  for (let r = 0; r < table2.rows.length; r++) {
16749
- const rowLine = line + 1 + r;
16895
+ const rowLine = line2 + 1 + r;
16750
16896
  rows.push({
16751
16897
  location: { line: rowLine },
16752
16898
  cells: table2.rows[r].map((cell) => ({
@@ -16757,7 +16903,7 @@ function buildDataTable(table2, line) {
16757
16903
  });
16758
16904
  }
16759
16905
  return {
16760
- location: { line },
16906
+ location: { line: line2 },
16761
16907
  rows
16762
16908
  };
16763
16909
  }
@@ -18309,17 +18455,17 @@ ${body}`;
18309
18455
  return humanizeSourceFile([...sourceFiles][0]) || this.title;
18310
18456
  }
18311
18457
  buildFrontmatter(run) {
18312
- const badge = _AstroFormatter.computeBadge(run.testCases);
18458
+ const badge2 = _AstroFormatter.computeBadge(run.testCases);
18313
18459
  const count2 = run.testCases.length;
18314
- const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
18460
+ const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge2.text.toLowerCase()}`;
18315
18461
  const lines = [
18316
18462
  "---",
18317
18463
  `title: ${yamlScalar(this.deriveTitle(run))}`,
18318
18464
  `description: ${description}`,
18319
18465
  "sidebar:",
18320
18466
  " badge:",
18321
- ` text: ${badge.text}`,
18322
- ` variant: ${badge.variant}`,
18467
+ ` text: ${badge2.text}`,
18468
+ ` variant: ${badge2.variant}`,
18323
18469
  "---"
18324
18470
  ];
18325
18471
  return lines.join("\n");
@@ -18775,12 +18921,16 @@ function groupBy7(items, keyFn) {
18775
18921
  import * as fs5 from "fs";
18776
18922
  import * as path6 from "path";
18777
18923
  var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
18778
- function isLocalPath(src) {
18924
+ function isRemoteRef(src) {
18779
18925
  const trimmed = src.trim();
18780
- if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
18781
- return false;
18782
- }
18783
- return !path6.posix.isAbsolute(trimmed) && !path6.win32.isAbsolute(trimmed);
18926
+ return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
18927
+ }
18928
+ function isAbsoluteRef(src) {
18929
+ const trimmed = src.trim();
18930
+ return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
18931
+ }
18932
+ function isRelativeLocalPath(src) {
18933
+ return !isRemoteRef(src) && !isAbsoluteRef(src);
18784
18934
  }
18785
18935
  function stripCodeContent(markdown) {
18786
18936
  let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
@@ -18796,21 +18946,21 @@ function scanMarkdownAssets(markdown) {
18796
18946
  let match;
18797
18947
  while ((match = mdImageRe.exec(stripped)) !== null) {
18798
18948
  const src = match[1].trim();
18799
- if (isLocalPath(src)) {
18949
+ if (!isRemoteRef(src)) {
18800
18950
  found.add(src);
18801
18951
  }
18802
18952
  }
18803
18953
  const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
18804
18954
  while ((match = htmlSrcRe.exec(stripped)) !== null) {
18805
18955
  const src = match[1].trim();
18806
- if (isLocalPath(src)) {
18956
+ if (!isRemoteRef(src)) {
18807
18957
  found.add(src);
18808
18958
  }
18809
18959
  }
18810
18960
  const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
18811
18961
  while ((match = posterRe.exec(stripped)) !== null) {
18812
18962
  const src = match[1].trim();
18813
- if (isLocalPath(src)) {
18963
+ if (!isRemoteRef(src)) {
18814
18964
  found.add(src);
18815
18965
  }
18816
18966
  }
@@ -18836,48 +18986,21 @@ function isCode(segment) {
18836
18986
  const trimmed = segment.trimStart();
18837
18987
  return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
18838
18988
  }
18989
+ function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
18990
+ if (isRemoteRef(trimmed)) return null;
18991
+ if (pathMap) {
18992
+ const mapped = pathMap.get(trimmed);
18993
+ return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
18994
+ }
18995
+ if (!isRelativeLocalPath(trimmed)) return null;
18996
+ return `${assetsBaseUrl}/${trimmed}`;
18997
+ }
18839
18998
  function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
18840
- let result = prose;
18841
- result = result.replace(
18842
- /(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
18843
- (full, pre, src, post) => {
18844
- const trimmed = src.trim();
18845
- if (!isLocalPath(trimmed)) return full;
18846
- if (pathMap) {
18847
- const mapped = pathMap.get(trimmed);
18848
- if (mapped === void 0) return full;
18849
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18850
- }
18851
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18852
- }
18853
- );
18854
- result = result.replace(
18855
- /(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
18856
- (full, pre, src, post) => {
18857
- const trimmed = src.trim();
18858
- if (!isLocalPath(trimmed)) return full;
18859
- if (pathMap) {
18860
- const mapped = pathMap.get(trimmed);
18861
- if (mapped === void 0) return full;
18862
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18863
- }
18864
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18865
- }
18866
- );
18867
- result = result.replace(
18868
- /(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
18869
- (full, pre, src, post) => {
18870
- const trimmed = src.trim();
18871
- if (!isLocalPath(trimmed)) return full;
18872
- if (pathMap) {
18873
- const mapped = pathMap.get(trimmed);
18874
- if (mapped === void 0) return full;
18875
- return `${pre}${assetsBaseUrl}/${mapped}${post}`;
18876
- }
18877
- return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
18878
- }
18879
- );
18880
- return result;
18999
+ const rewrite = (full, pre, src, post) => {
19000
+ const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
19001
+ return target === null ? full : `${pre}${target}${post}`;
19002
+ };
19003
+ return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
18881
19004
  }
18882
19005
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
18883
19006
  return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
@@ -18894,8 +19017,9 @@ function copyMarkdownAssets(options) {
18894
19017
  const pathMap = /* @__PURE__ */ new Map();
18895
19018
  const missing = [];
18896
19019
  for (const ref of refs) {
18897
- const absPath = path6.resolve(markdownDir, ref);
19020
+ const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
18898
19021
  if (!fs5.existsSync(absPath)) {
19022
+ if (isAbsoluteRef(ref)) continue;
18899
19023
  if (!allowMissing) {
18900
19024
  throw new Error(`Asset not found: ${absPath}`);
18901
19025
  }
@@ -18977,6 +19101,47 @@ function startWatch(options, deps = {}) {
18977
19101
  };
18978
19102
  }
18979
19103
 
19104
+ // src/behavior-diff.ts
19105
+ function classifyStatusChange(baseline, current) {
19106
+ if (baseline === void 0) return "added";
19107
+ if (current === void 0) return "removed";
19108
+ if (baseline === current) return "unchanged";
19109
+ if (baseline === "passed" && current === "failed") return "regressed";
19110
+ if (baseline === "failed" && current === "passed") return "fixed";
19111
+ return "changed";
19112
+ }
19113
+ function scenarioMap(report) {
19114
+ const map = /* @__PURE__ */ new Map();
19115
+ for (const feature of report.features) {
19116
+ for (const scenario of feature.scenarios) {
19117
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
19118
+ }
19119
+ }
19120
+ return map;
19121
+ }
19122
+ function diffStoryReports(baseline, current) {
19123
+ const base = scenarioMap(baseline);
19124
+ const curr = scenarioMap(current);
19125
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
19126
+ const scenarios = ids.map((id) => {
19127
+ const b = base.get(id);
19128
+ const c = curr.get(id);
19129
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
19130
+ const meta = c ?? b;
19131
+ return {
19132
+ id,
19133
+ title: meta.scenario.title,
19134
+ sourceFile: meta.sourceFile,
19135
+ kind,
19136
+ baselineStatus: b?.scenario.status,
19137
+ currentStatus: c?.scenario.status
19138
+ };
19139
+ });
19140
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19141
+ for (const s of scenarios) summary[s.kind] += 1;
19142
+ return { schemaVersion: "1.0", summary, scenarios };
19143
+ }
19144
+
18980
19145
  // src/publishers/confluence.ts
18981
19146
  function parseAdf(adf) {
18982
19147
  let parsed;
@@ -19209,7 +19374,7 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
19209
19374
  // src/converters/ndjson-parser.ts
19210
19375
  function parseNdjson(ndjson) {
19211
19376
  const lines = ndjson.trim().split("\n").filter(Boolean);
19212
- const envelopes = lines.map((line) => JSON.parse(line));
19377
+ const envelopes = lines.map((line2) => JSON.parse(line2));
19213
19378
  return parseEnvelopes(envelopes);
19214
19379
  }
19215
19380
  function parseEnvelopes(envelopes) {
@@ -20262,6 +20427,280 @@ function collectDocKinds(testCase) {
20262
20427
  return [...kinds].sort();
20263
20428
  }
20264
20429
 
20430
+ // src/scenario-failure.ts
20431
+ function failingScenarioMessage(tc) {
20432
+ const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
20433
+ return failingStep?.errorMessage ?? tc.errorMessage;
20434
+ }
20435
+
20436
+ // src/check.ts
20437
+ var ICON_PASS = "\u2713";
20438
+ var ICON_FAIL = "\u2717";
20439
+ var ICON_SKIP = "\u2298";
20440
+ var ICON_PENDING = "\u23F3";
20441
+ var ICON_WARN = "\u26A0";
20442
+ function buildCheck(args, _deps = {}) {
20443
+ const { testCases, baseline } = args;
20444
+ const summary = {
20445
+ total: testCases.length,
20446
+ passed: testCases.filter((tc) => tc.status === "passed").length,
20447
+ failed: testCases.filter((tc) => tc.status === "failed").length,
20448
+ skipped: testCases.filter((tc) => tc.status === "skipped").length,
20449
+ pending: testCases.filter((tc) => tc.status === "pending").length
20450
+ };
20451
+ let regressed = 0;
20452
+ let fixed = 0;
20453
+ if (baseline) {
20454
+ for (const tc of testCases) {
20455
+ const before = baseline.get(tc.id);
20456
+ if (before === "passed" && tc.status === "failed") regressed += 1;
20457
+ if (before === "failed" && tc.status === "passed") fixed += 1;
20458
+ }
20459
+ }
20460
+ const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
20461
+ if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
20462
+ return a.location.localeCompare(b.location);
20463
+ });
20464
+ return {
20465
+ summary,
20466
+ failures,
20467
+ regressed,
20468
+ fixed,
20469
+ comparedToBaseline: baseline !== void 0
20470
+ };
20471
+ }
20472
+ function toFailure(tc, baseline) {
20473
+ const failedIndexes = new Set(
20474
+ tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
20475
+ );
20476
+ const steps = tc.story.steps.map((step, index) => ({
20477
+ keyword: step.keyword,
20478
+ text: step.text,
20479
+ failed: failedIndexes.has(index)
20480
+ }));
20481
+ return {
20482
+ id: tc.id,
20483
+ scenario: tc.story.scenario,
20484
+ location: `${tc.sourceFile}:${tc.sourceLine}`,
20485
+ steps,
20486
+ errorMessage: failingScenarioMessage(tc),
20487
+ covers: tc.story.covers ?? [],
20488
+ tickets: (tc.story.tickets ?? []).map((t) => t.id),
20489
+ regressed: baseline?.get(tc.id) === "passed"
20490
+ };
20491
+ }
20492
+ function renderCheck(report, format) {
20493
+ return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
20494
+ }
20495
+ function renderCheckText(report) {
20496
+ const { summary, failures } = report;
20497
+ const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
20498
+ if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
20499
+ if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
20500
+ if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
20501
+ const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
20502
+ if (failures.length === 0) {
20503
+ const lines2 = [headline];
20504
+ if (report.comparedToBaseline && report.fixed > 0) {
20505
+ lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
20506
+ }
20507
+ lines2.push("All scenarios green.");
20508
+ return lines2.join("\n");
20509
+ }
20510
+ const lines = [headline, ""];
20511
+ for (const f of failures) {
20512
+ lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
20513
+ lines.push(` ${f.location}`);
20514
+ for (const step of f.steps) {
20515
+ const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
20516
+ lines.push(`${marker}${step.keyword} ${step.text}`);
20517
+ }
20518
+ if (f.errorMessage) {
20519
+ const firstLine = f.errorMessage.split("\n")[0];
20520
+ lines.push(` \u2192 ${firstLine}`);
20521
+ }
20522
+ if (f.covers.length > 0) {
20523
+ lines.push(` covers: ${f.covers.join(", ")}`);
20524
+ }
20525
+ if (f.tickets.length > 0) {
20526
+ lines.push(` ticket: ${f.tickets.join(", ")}`);
20527
+ }
20528
+ lines.push("");
20529
+ }
20530
+ if (report.comparedToBaseline) {
20531
+ if (report.regressed > 0) {
20532
+ lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
20533
+ }
20534
+ if (report.fixed > 0) {
20535
+ lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
20536
+ }
20537
+ if (report.regressed === 0 && report.fixed === 0) {
20538
+ lines.push("No status changes vs. baseline.");
20539
+ }
20540
+ }
20541
+ return lines.join("\n").trimEnd();
20542
+ }
20543
+
20544
+ // src/goal.ts
20545
+ var ACTIVE = ["passed", "failed"];
20546
+ function buildGoal(args, _deps = {}) {
20547
+ const { run, baseline } = args;
20548
+ const cases = run.testCases;
20549
+ const selectors = [
20550
+ ...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
20551
+ ...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
20552
+ ...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
20553
+ ];
20554
+ const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
20555
+ const regressions = [];
20556
+ if (baseline && args.enforceNoRegressions) {
20557
+ const before = statusMap(baseline);
20558
+ for (const tc of cases) {
20559
+ if (before.get(tc.id) === "passed" && tc.status === "failed") {
20560
+ regressions.push({ id: tc.id, title: tc.story.scenario });
20561
+ }
20562
+ }
20563
+ }
20564
+ const violations = [];
20565
+ if (baseline && args.enforceRatchet) {
20566
+ const current = new Map(cases.map((tc) => [tc.id, tc]));
20567
+ for (const base of baseline.testCases) {
20568
+ const now = current.get(base.id);
20569
+ if (!now) {
20570
+ violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
20571
+ continue;
20572
+ }
20573
+ if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
20574
+ violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
20575
+ }
20576
+ const baseSteps = base.story.steps.length;
20577
+ const nowSteps = now.story.steps.length;
20578
+ if (nowSteps < baseSteps) {
20579
+ violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
20580
+ }
20581
+ }
20582
+ }
20583
+ const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
20584
+ return {
20585
+ met,
20586
+ requirements,
20587
+ regressions,
20588
+ regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
20589
+ ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
20590
+ };
20591
+ }
20592
+ function evaluate(selector, matched) {
20593
+ const passed = matched.filter((tc) => tc.status === "passed").length;
20594
+ const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
20595
+ return {
20596
+ selector,
20597
+ matched: matched.length,
20598
+ passed,
20599
+ failing,
20600
+ met: matched.length > 0 && failing.length === 0
20601
+ };
20602
+ }
20603
+ function statusMap(run) {
20604
+ return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
20605
+ }
20606
+ function renderGoal(report, format) {
20607
+ if (format === "json") return JSON.stringify(report, null, 2);
20608
+ const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
20609
+ for (const req of report.requirements) {
20610
+ if (req.matched === 0) {
20611
+ lines.push(` ${req.selector}: no matching scenario (no proof)`);
20612
+ continue;
20613
+ }
20614
+ const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
20615
+ lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
20616
+ }
20617
+ if (report.regressionsEnforced) {
20618
+ if (report.regressions.length === 0) {
20619
+ lines.push(" regressions: 0");
20620
+ } else {
20621
+ lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
20622
+ }
20623
+ }
20624
+ if (report.ratchet.enforced) {
20625
+ if (report.ratchet.violations.length === 0) {
20626
+ lines.push(" ratchet: clean (0 scenarios removed/weakened)");
20627
+ } else {
20628
+ lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
20629
+ for (const v of report.ratchet.violations) {
20630
+ lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
20631
+ }
20632
+ }
20633
+ }
20634
+ return lines.join("\n");
20635
+ }
20636
+
20637
+ // src/triage.ts
20638
+ function buildTriage(args, _deps = {}) {
20639
+ const { testCases, baseline } = args;
20640
+ const failing = testCases.filter((tc) => tc.status === "failed");
20641
+ const ranked = failing.map((tc) => {
20642
+ const regressed = baseline?.get(tc.id) === "passed";
20643
+ return {
20644
+ tc,
20645
+ regressed,
20646
+ covers: tc.story.covers ?? []
20647
+ };
20648
+ }).sort((a, b) => {
20649
+ if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
20650
+ const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
20651
+ const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
20652
+ return la.localeCompare(lb);
20653
+ });
20654
+ const items = ranked.map((entry, index) => ({
20655
+ rank: index + 1,
20656
+ id: entry.tc.id,
20657
+ scenario: entry.tc.story.scenario,
20658
+ status: entry.tc.status,
20659
+ location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
20660
+ covers: entry.covers,
20661
+ tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
20662
+ errorMessage: failingScenarioMessage(entry.tc),
20663
+ regressed: entry.regressed,
20664
+ reason: entry.regressed ? "regression" : "failing"
20665
+ }));
20666
+ return {
20667
+ total: testCases.length,
20668
+ failing: failing.length,
20669
+ regressions: items.filter((i) => i.regressed).length,
20670
+ needsCovers: items.filter((i) => i.covers.length === 0).length,
20671
+ items
20672
+ };
20673
+ }
20674
+ function renderTriage(report, format) {
20675
+ if (format === "json") return JSON.stringify(report, null, 2);
20676
+ if (report.items.length === 0) {
20677
+ return "Nothing to triage. No failing scenarios.";
20678
+ }
20679
+ const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
20680
+ const lines = [header, ""];
20681
+ for (const item of report.items) {
20682
+ const tag = item.regressed ? "[regression] " : "";
20683
+ lines.push(`${item.rank}. ${tag}${item.scenario}`);
20684
+ lines.push(` ${item.location}`);
20685
+ if (item.errorMessage) {
20686
+ lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
20687
+ }
20688
+ if (item.covers.length > 0) {
20689
+ lines.push(` fix: ${item.covers.join(", ")}`);
20690
+ } else {
20691
+ lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
20692
+ }
20693
+ if (item.tickets.length > 0) {
20694
+ lines.push(` ticket: ${item.tickets.join(", ")}`);
20695
+ }
20696
+ lines.push("");
20697
+ }
20698
+ if (report.needsCovers > 0) {
20699
+ lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
20700
+ }
20701
+ return lines.join("\n").trimEnd();
20702
+ }
20703
+
20265
20704
  // src/review/conventions.ts
20266
20705
  var CHANGE_TAG_PREFIX = "change:";
20267
20706
  var AUDIENCE_TAG_PREFIX = "audience:";
@@ -21063,6 +21502,7 @@ var FORMAT_EXTENSIONS = {
21063
21502
  "behavior-manifest-json": ".behavior-manifest.json",
21064
21503
  markdown: ".md",
21065
21504
  "release-manifest": ".release-manifest.md",
21505
+ "traceability-matrix": ".traceability-matrix.md",
21066
21506
  html: ".html",
21067
21507
  "cucumber-html": ".cucumber.html",
21068
21508
  junit: ".junit.xml",
@@ -21285,7 +21725,9 @@ var ReportGenerator = class {
21285
21725
  permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
21286
21726
  ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
21287
21727
  traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
21288
- customRenderers: options.astro?.markdown?.customRenderers
21728
+ customRenderers: options.astro?.markdown?.customRenderers,
21729
+ scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
21730
+ scenarioBadge: options.astro?.markdown?.scenarioBadge
21289
21731
  }
21290
21732
  },
21291
21733
  assetMode: options.assetMode ?? "none",
@@ -21489,6 +21931,10 @@ var ReportGenerator = class {
21489
21931
  const formatter = new ReleaseManifestFormatter();
21490
21932
  return formatter.format(run);
21491
21933
  }
21934
+ case "traceability-matrix": {
21935
+ const formatter = new TraceabilityMatrixFormatter();
21936
+ return formatter.format(run);
21937
+ }
21492
21938
  case "story-report-json": {
21493
21939
  const formatter = new StoryReportJsonFormatter({
21494
21940
  pretty: this.options.storyReportJson.pretty
@@ -22144,6 +22590,189 @@ async function importOpenApi(options) {
22144
22590
  // src/build-docs.ts
22145
22591
  import * as fs13 from "fs";
22146
22592
  import * as path14 from "path";
22593
+
22594
+ // src/scenario-links.ts
22595
+ function scenarioAnchor(title) {
22596
+ return `scenario-${slugify(title)}`;
22597
+ }
22598
+ function buildScenarioLinks(report, options = {}) {
22599
+ const audienceSplit = options.audienceSplit ?? false;
22600
+ const baseUrl = (options.baseUrl ?? "/stories").replace(/\/$/, "");
22601
+ const scenarios = {};
22602
+ for (const feature of report.features) {
22603
+ const stem = slugify(cleanTestStem(feature.sourceFile));
22604
+ for (const scenario of feature.scenarios) {
22605
+ const audience = deriveAudience(feature.sourceFile, scenario.tags);
22606
+ const url = audienceSplit ? `${baseUrl}/${audience}/${stem}/` : `${baseUrl}/${stem}/`;
22607
+ const anchor = scenarioAnchor(scenario.title);
22608
+ scenarios[scenario.id] = {
22609
+ id: scenario.id,
22610
+ title: scenario.title,
22611
+ audience,
22612
+ status: scenario.status,
22613
+ sourceFile: feature.sourceFile,
22614
+ url,
22615
+ anchor,
22616
+ deepLink: `${url}#${anchor}`
22617
+ };
22618
+ }
22619
+ }
22620
+ return { schemaVersion: "1.0", runId: report.runId, baseUrl, scenarios };
22621
+ }
22622
+
22623
+ // src/changes-page.ts
22624
+ var GROUPS = [
22625
+ { kind: "regressed", heading: "Regressed", icon: "\u26A0\uFE0F" },
22626
+ { kind: "removed", heading: "Removed", icon: "\u{1F5D1}\uFE0F" },
22627
+ { kind: "added", heading: "Added", icon: "\u2728" },
22628
+ { kind: "fixed", heading: "Fixed", icon: "\u2705" },
22629
+ { kind: "changed", heading: "Changed", icon: "\u{1F501}" }
22630
+ ];
22631
+ function yamlScalar2(value) {
22632
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22633
+ return `'${value.replace(/'/g, "''")}'`;
22634
+ }
22635
+ return value;
22636
+ }
22637
+ function badge(summary) {
22638
+ if (summary.regressed > 0 || summary.removed > 0) return { text: "Regressed", variant: "danger" };
22639
+ if (summary.added > 0 || summary.fixed > 0 || summary.changed > 0)
22640
+ return { text: "Updated", variant: "tip" };
22641
+ return { text: "No changes", variant: "note" };
22642
+ }
22643
+ function line(entry, links) {
22644
+ const link2 = links.scenarios[entry.id];
22645
+ const label = link2 ? `[${entry.title}](${link2.deepLink})` : entry.title;
22646
+ const transition = entry.baselineStatus && entry.currentStatus && entry.baselineStatus !== entry.currentStatus ? ` \u2014 \`${entry.baselineStatus}\` \u2192 \`${entry.currentStatus}\`` : "";
22647
+ return `- ${label} \`${entry.sourceFile}\`${transition}`;
22648
+ }
22649
+ function renderChangesPage(diff, links) {
22650
+ const b = badge(diff.summary);
22651
+ const { added, removed, regressed, fixed, changed } = diff.summary;
22652
+ const totalChanged = added + removed + regressed + fixed + changed;
22653
+ const frontmatter = [
22654
+ "---",
22655
+ "title: What's changed",
22656
+ `description: ${yamlScalar2(
22657
+ totalChanged === 0 ? "No behavioural changes since the baseline" : `${totalChanged} scenario${totalChanged !== 1 ? "s" : ""} changed since the baseline`
22658
+ )}`,
22659
+ "sidebar:",
22660
+ " order: 0",
22661
+ " badge:",
22662
+ ` text: ${b.text}`,
22663
+ ` variant: ${b.variant}`,
22664
+ "---"
22665
+ ].join("\n");
22666
+ const body = [];
22667
+ if (totalChanged === 0) {
22668
+ body.push("No behavioural changes since the baseline run. \u2705");
22669
+ } else {
22670
+ body.push(
22671
+ `**${regressed}** regressed \xB7 **${removed}** removed \xB7 **${added}** added \xB7 **${fixed}** fixed \xB7 **${changed}** changed`,
22672
+ ""
22673
+ );
22674
+ for (const group of GROUPS) {
22675
+ const entries = diff.scenarios.filter((s) => s.kind === group.kind);
22676
+ if (entries.length === 0) continue;
22677
+ body.push(`## ${group.icon} ${group.heading} (${entries.length})`, "");
22678
+ for (const entry of entries) body.push(line(entry, links));
22679
+ body.push("");
22680
+ }
22681
+ }
22682
+ return `${frontmatter}
22683
+
22684
+ ${body.join("\n")}
22685
+ `;
22686
+ }
22687
+
22688
+ // src/overview-page.ts
22689
+ var AUDIENCE_CARDS = [
22690
+ {
22691
+ key: "engineer",
22692
+ label: "Engineer",
22693
+ icon: "\u{1F527}",
22694
+ blurb: "Unit & integration behaviour \u2014 how the system works under the hood."
22695
+ },
22696
+ {
22697
+ key: "stakeholder",
22698
+ label: "Stakeholder",
22699
+ icon: "\u{1F3AC}",
22700
+ blurb: "End-to-end journeys \u2014 what the product does, with video and traces."
22701
+ }
22702
+ ];
22703
+ var STATUS_ICON = {
22704
+ passed: "\u2705",
22705
+ failed: "\u274C",
22706
+ skipped: "\u23ED\uFE0F",
22707
+ pending: "\u{1F6A7}"
22708
+ };
22709
+ function yamlScalar3(value) {
22710
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
22711
+ return `'${value.replace(/'/g, "''")}'`;
22712
+ }
22713
+ return value;
22714
+ }
22715
+ function renderOverviewPage(links) {
22716
+ const all = Object.values(links.scenarios);
22717
+ const total = all.length;
22718
+ const passed = all.filter((s) => s.status === "passed").length;
22719
+ const failed = all.filter((s) => s.status === "failed").length;
22720
+ const frontmatter = [
22721
+ "---",
22722
+ "title: Stories",
22723
+ `description: ${yamlScalar3(
22724
+ `${total} scenario${total !== 1 ? "s" : ""} \u2014 ${passed} passed, ${failed} failed`
22725
+ )}`,
22726
+ "sidebar:",
22727
+ " order: 1",
22728
+ "---"
22729
+ ].join("\n");
22730
+ const body = [
22731
+ `**${total}** scenarios \xB7 **${passed}** passed \xB7 **${failed}** failed`,
22732
+ ""
22733
+ ];
22734
+ for (const card of AUDIENCE_CARDS) {
22735
+ const scenarios = all.filter((s) => s.audience === card.key);
22736
+ if (scenarios.length === 0) continue;
22737
+ const cardPassed = scenarios.filter((s) => s.status === "passed").length;
22738
+ const cardFailed = scenarios.filter((s) => s.status === "failed").length;
22739
+ const counts = cardFailed > 0 ? `${cardPassed} passed, ${cardFailed} failed` : `${cardPassed} passed`;
22740
+ body.push(`## ${card.icon} ${card.label} (${scenarios.length} \u2014 ${counts})`, "");
22741
+ body.push(`${card.blurb}`, "");
22742
+ for (const s of scenariosSorted(scenarios)) {
22743
+ body.push(`- ${STATUS_ICON[s.status] ?? "\u2022"} [${s.title}](${s.deepLink})`);
22744
+ }
22745
+ body.push("");
22746
+ }
22747
+ return `${frontmatter}
22748
+
22749
+ ${body.join("\n")}
22750
+ `;
22751
+ }
22752
+ function scenariosSorted(scenarios) {
22753
+ return [...scenarios].sort((a, b) => {
22754
+ const aFail = a.status === "failed" ? 0 : 1;
22755
+ const bFail = b.status === "failed" ? 0 : 1;
22756
+ if (aFail !== bFail) return aFail - bFail;
22757
+ return a.title.localeCompare(b.title);
22758
+ });
22759
+ }
22760
+
22761
+ // src/build-docs.ts
22762
+ var AUDIENCES = ["engineer", "stakeholder"];
22763
+ function partitionByAudience(run) {
22764
+ const buckets = {
22765
+ engineer: [],
22766
+ stakeholder: []
22767
+ };
22768
+ for (const tc of run.testCases) {
22769
+ buckets[deriveAudience(tc.sourceFile, tc.tags)].push(tc);
22770
+ }
22771
+ return {
22772
+ engineer: { ...run, testCases: buckets.engineer },
22773
+ stakeholder: { ...run, testCases: buckets.stakeholder }
22774
+ };
22775
+ }
22147
22776
  var BuildDocsError = class extends Error {
22148
22777
  constructor(message, kind) {
22149
22778
  super(message);
@@ -22185,6 +22814,40 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
22185
22814
  }
22186
22815
  return copied;
22187
22816
  }
22817
+ var CHANGE_BADGE = {
22818
+ added: "\u{1F195} **New** _since last run_",
22819
+ fixed: "\u2705 **Fixed** _since last run_",
22820
+ regressed: "\u26A0\uFE0F **Regressed** _since last run_"
22821
+ };
22822
+ function changeBadgeLookup(diff) {
22823
+ if (!diff) return void 0;
22824
+ const byKey = /* @__PURE__ */ new Map();
22825
+ for (const s of diff.scenarios) {
22826
+ const badge2 = CHANGE_BADGE[s.kind];
22827
+ if (badge2) byKey.set(`${s.sourceFile}\0${s.title}`, badge2);
22828
+ }
22829
+ if (byKey.size === 0) return void 0;
22830
+ return (tc) => byKey.get(`${tc.sourceFile}\0${tc.story.scenario}`);
22831
+ }
22832
+ function readStoryReport(reportPath) {
22833
+ if (!fs13.existsSync(reportPath)) return null;
22834
+ try {
22835
+ return JSON.parse(fs13.readFileSync(reportPath, "utf8"));
22836
+ } catch {
22837
+ return null;
22838
+ }
22839
+ }
22840
+ function writeScenarioLinks(reportPath, outDir, options = {}) {
22841
+ const report = readStoryReport(reportPath);
22842
+ if (!report) return null;
22843
+ const index = buildScenarioLinks(report, { audienceSplit: options.audienceSplit });
22844
+ fs13.writeFileSync(
22845
+ path14.join(outDir, "scenario-links.json"),
22846
+ JSON.stringify(index, null, 2),
22847
+ "utf8"
22848
+ );
22849
+ return index;
22850
+ }
22188
22851
  function clearGeneratedPages(dir) {
22189
22852
  if (!fs13.existsSync(dir)) return;
22190
22853
  for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
@@ -22235,16 +22898,72 @@ async function buildDocs(options) {
22235
22898
  outputDir: storiesPublicDir,
22236
22899
  outputName: "story-report"
22237
22900
  }).generate(canonical);
22901
+ let diff;
22902
+ if (options.baselinePath) {
22903
+ const baselineResolved = path14.resolve(options.baselinePath);
22904
+ const baseline = readStoryReport(baselineResolved);
22905
+ if (!baseline) {
22906
+ throw new BuildDocsError(
22907
+ `Baseline story-report not found or unreadable: ${baselineResolved}`,
22908
+ "input"
22909
+ );
22910
+ }
22911
+ const current = readStoryReport(reportPath);
22912
+ if (current) diff = diffStoryReports(baseline, current);
22913
+ }
22914
+ const scenarioBadge = changeBadgeLookup(diff);
22238
22915
  clearGeneratedPages(storyPagesDir);
22239
- await new ReportGenerator({
22916
+ const genPages = (run, outDir) => new ReportGenerator({
22240
22917
  formats: ["astro"],
22241
- outputDir: storyPagesDir,
22918
+ outputDir: outDir,
22242
22919
  outputName: "index",
22243
22920
  output: { mode: "colocated", colocatedStyle: "flat" },
22244
22921
  assetMode: "copy",
22245
- astro: { assetsDir, assetsBaseUrl: "/stories/assets" }
22246
- }).generate(canonical);
22922
+ astro: {
22923
+ assetsDir,
22924
+ assetsBaseUrl: "/stories/assets",
22925
+ markdown: {
22926
+ // Emit the same anchor scenario-links.json points at, so fragments resolve.
22927
+ scenarioAnchor: (tc) => scenarioAnchor(tc.story.scenario),
22928
+ scenarioBadge
22929
+ }
22930
+ }
22931
+ }).generate(run);
22932
+ const audiences = { engineer: 0, stakeholder: 0 };
22933
+ if (options.audienceSplit ?? false) {
22934
+ const partitioned = partitionByAudience(canonical);
22935
+ for (const audience of AUDIENCES) {
22936
+ const sub = partitioned[audience];
22937
+ audiences[audience] = sub.testCases.length;
22938
+ if (sub.testCases.length === 0) continue;
22939
+ await genPages(sub, path14.join(storyPagesDir, audience));
22940
+ }
22941
+ } else {
22942
+ await genPages(canonical, storyPagesDir);
22943
+ }
22247
22944
  const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
22945
+ const linksIndex = writeScenarioLinks(reportPath, storiesPublicDir, {
22946
+ audienceSplit: options.audienceSplit ?? false
22947
+ });
22948
+ const scenarioLinks = linksIndex ? Object.keys(linksIndex.scenarios).length : 0;
22949
+ if (linksIndex) {
22950
+ fs13.writeFileSync(
22951
+ path14.join(storyPagesDir, "index.md"),
22952
+ renderOverviewPage(linksIndex),
22953
+ "utf8"
22954
+ );
22955
+ }
22956
+ const changesJsonPath = path14.join(storiesPublicDir, "changes.json");
22957
+ const changesMdPath = path14.join(storyPagesDir, "changes.md");
22958
+ let changes;
22959
+ if (diff && linksIndex) {
22960
+ fs13.writeFileSync(changesJsonPath, JSON.stringify(diff, null, 2), "utf8");
22961
+ fs13.writeFileSync(changesMdPath, renderChangesPage(diff, linksIndex), "utf8");
22962
+ changes = diff.summary;
22963
+ } else {
22964
+ fs13.rmSync(changesJsonPath, { force: true });
22965
+ fs13.rmSync(changesMdPath, { force: true });
22966
+ }
22248
22967
  let apiPages = 0;
22249
22968
  if (options.openapiPath) {
22250
22969
  const res = await importOpenApi({
@@ -22255,7 +22974,7 @@ async function buildDocs(options) {
22255
22974
  });
22256
22975
  apiPages = res.pageCount;
22257
22976
  }
22258
- return { siteDir, bundledAssets, apiPages };
22977
+ return { siteDir, bundledAssets, apiPages, audiences, scenarioLinks, changes };
22259
22978
  } catch (err) {
22260
22979
  if (err instanceof BuildDocsError) throw err;
22261
22980
  throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
@@ -22286,6 +23005,7 @@ var EXIT_GENERATION = 3;
22286
23005
  var EXIT_USAGE = 4;
22287
23006
  var EXIT_COMPARE_GATE = 5;
22288
23007
  var EXIT_REVIEW_GATE = 5;
23008
+ var EXIT_AGENT_GATE = 5;
22289
23009
  var EXIT_RELEASE_GATE = 6;
22290
23010
  var HELP_TEXT = `
22291
23011
  executable-stories \u2014 Generate reports from test results JSON.
@@ -22297,6 +23017,9 @@ USAGE
22297
23017
  executable-stories gate-release <dev-run.json> <rc-run.json> [options]
22298
23018
  executable-stories review <file> --changed-files <path> [options]
22299
23019
  executable-stories list <file> [options]
23020
+ executable-stories check <file> [--baseline <path|auto>] [--check-format text|json] [--no-fail]
23021
+ executable-stories goal <file> [--require-tags <csv>] [--require-tickets <csv>] [--require-scenarios <csv>] [--baseline <path|auto>] [--no-regressions] [--goal-format text|json]
23022
+ executable-stories triage <file> [--baseline <path|auto>] [--triage-format text|json]
22300
23023
  executable-stories validate <file>
22301
23024
  executable-stories validate --stdin
22302
23025
  executable-stories init-astro [directory]
@@ -22316,6 +23039,9 @@ SUBCOMMANDS
22316
23039
  gate-release Verify a release candidate against the dev test baseline (RC gate)
22317
23040
  review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
22318
23041
  list List scenarios from a test run (text table or JSON)
23042
+ check Backpressure summary: compress passing, expand failing (GWT + error + covers); non-zero exit on failures
23043
+ goal Behavioral definition-of-done for agent loops: required scenarios pass, no regressions, no weakened scenarios (exit 0 = met, 5 = not)
23044
+ triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
22319
23045
  validate Validate a JSON file against the schema (no output generated)
22320
23046
  init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
22321
23047
  new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
@@ -22326,7 +23052,7 @@ SUBCOMMANDS
22326
23052
  deploy Record deployments, show environment status, detect drift
22327
23053
 
22328
23054
  OPTIONS
22329
- --format <formats> Comma-separated formats: html, markdown, release-manifest, 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)
23055
+ --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)
22330
23056
  astro Themed Markdown (for Astro docs sites with matching CSS)
22331
23057
  confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
22332
23058
  behavior-manifest-json Agent-readable behavior manifest and debugger warnings
@@ -22338,6 +23064,7 @@ OPTIONS
22338
23064
  cucumber-messages Raw NDJSON (Cucumber Messages)
22339
23065
  story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
22340
23066
  scenario-index-json Storybook-like scenario index for agents and explorers
23067
+ traceability-matrix Requirement-first matrix (ticket -> scenarios -> covered code -> status)
22341
23068
  --config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
22342
23069
  --input-type <type> Input type: raw, canonical, or ndjson (default: raw)
22343
23070
  --output-dir <dir> Output directory (default: reports)
@@ -22364,6 +23091,15 @@ OPTIONS
22364
23091
  --stdin Read JSON from stdin instead of file
22365
23092
  --list-format <format> list output format: text (default), json, csv, markdown-table
22366
23093
  --json-summary Deprecated alias for --list-format json
23094
+ --check-format <format> check output format: text (default) or json
23095
+ --no-fail (check) Report only \u2014 always exit 0 even when scenarios failed
23096
+ --require-tags <csv> (goal) Every scenario carrying any of these tags must pass
23097
+ --require-tickets <csv> (goal) Every scenario carrying any of these tickets must pass
23098
+ --require-scenarios <csv> (goal) These scenarios (by id or exact title) must pass
23099
+ --no-regressions (goal) Not met if any scenario regressed vs --baseline
23100
+ --no-ratchet (goal) Disable the removed/weakened-scenario guard (on by default with --baseline)
23101
+ --goal-format <format> goal output format: text (default) or json
23102
+ --triage-format <format> triage output format: text (default) or json
22367
23103
  --baseline <path|auto> Compare baseline file, or auto-pick a prior run for compare
22368
23104
  --baseline-dir <dir> Directory to scan when --baseline auto is used
22369
23105
  --pr-summary Print a PR-friendly markdown summary after compare
@@ -22388,6 +23124,31 @@ LIST
22388
23124
  list supports --include-tags, --exclude-tags for filtering
22389
23125
  list supports --input-type and --stdin
22390
23126
 
23127
+ CHECK
23128
+ check is the inner-loop "backpressure" view for coding agents: run it after tests.
23129
+ Passing scenarios collapse to a single count line; each failing scenario expands
23130
+ to its Given/When/Then steps, the step that broke, the error, and the product
23131
+ code it covers \u2014 so the agent gets an actionable signal, not a wall of green.
23132
+ check exits 5 when any scenario failed (so the agent loop pushes back); pass
23133
+ --no-fail to report only. --baseline <path|auto> adds "N regressed / N fixed"
23134
+ since the prior run. --check-format json emits the structured report.
23135
+
23136
+ GOAL
23137
+ goal is the behavioral stopping condition for an agent loop (the /goal pattern).
23138
+ It is "met" when the required scenarios pass, nothing regressed (with
23139
+ --no-regressions), and no scenario was removed, disabled, or had steps deleted
23140
+ versus --baseline (the ratchet, on by default when a baseline is given). Declare
23141
+ the target with --require-tags / --require-tickets / --require-scenarios; with
23142
+ none given, the goal is "every scenario passes". Exit 0 means met, 5 means not
23143
+ yet, so a loop can run until the verdict flips. --goal-format json for machines.
23144
+
23145
+ TRIAGE
23146
+ triage is the discovery-phase worklist for a loop. It lists failing scenarios,
23147
+ regressions first (with --baseline), each with the product code it covers, the
23148
+ error, and its tickets, so the loop can route each fix to a sub-agent. Failures
23149
+ with no covers are flagged. --triage-format json emits the work queue. triage
23150
+ always exits 0 \u2014 it reports work, it does not gate.
23151
+
22391
23152
  COMPARE
22392
23153
  compare supports --format html,markdown
22393
23154
  compare uses the same --input-type for both baseline and current files
@@ -22460,9 +23221,16 @@ EXIT CODES
22460
23221
  2 Canonical validation failure
22461
23222
  3 Formatter/generation failure
22462
23223
  4 Bad arguments / usage error
22463
- 5 Compare gate failed
23224
+ 5 Compare / review / check gate failed
22464
23225
  6 Release gate failed
22465
23226
  `.trim();
23227
+ function parseTextJsonFormat(flag, value) {
23228
+ if (value !== "text" && value !== "json") {
23229
+ console.error(`Error: ${flag} must be "text" or "json", got "${value}".`);
23230
+ process.exit(EXIT_USAGE);
23231
+ }
23232
+ return value;
23233
+ }
22466
23234
  async function parseCliArgs(argv) {
22467
23235
  const args = argv.slice(2);
22468
23236
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -22470,9 +23238,9 @@ async function parseCliArgs(argv) {
22470
23238
  process.exit(EXIT_SUCCESS);
22471
23239
  }
22472
23240
  const subcommand = args[0];
22473
- if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
23241
+ if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
22474
23242
  console.error(
22475
- `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
23243
+ `Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
22476
23244
  );
22477
23245
  process.exit(EXIT_USAGE);
22478
23246
  }
@@ -22560,6 +23328,15 @@ async function parseCliArgs(argv) {
22560
23328
  stdin: { type: "boolean", default: false },
22561
23329
  "json-summary": { type: "boolean", default: false },
22562
23330
  "list-format": { type: "string", default: "text" },
23331
+ "check-format": { type: "string", default: "text" },
23332
+ "no-fail": { type: "boolean", default: false },
23333
+ "require-tags": { type: "string" },
23334
+ "require-tickets": { type: "string" },
23335
+ "require-scenarios": { type: "string" },
23336
+ "no-regressions": { type: "boolean", default: false },
23337
+ "no-ratchet": { type: "boolean", default: false },
23338
+ "goal-format": { type: "string", default: "text" },
23339
+ "triage-format": { type: "string", default: "text" },
22563
23340
  "emit-canonical": { type: "string" },
22564
23341
  "slack-webhook": { type: "string" },
22565
23342
  "teams-webhook": { type: "string" },
@@ -22630,7 +23407,7 @@ async function parseCliArgs(argv) {
22630
23407
  }
22631
23408
  const pluginConfig = await loadConfig(values["config"]);
22632
23409
  const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
22633
- const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
23410
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "traceability-matrix", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
22634
23411
  const formatStr = values.format;
22635
23412
  const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
22636
23413
  const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
@@ -22638,7 +23415,7 @@ async function parseCliArgs(argv) {
22638
23415
  const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
22639
23416
  if (unknownFormats.length > 0) {
22640
23417
  const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
22641
- console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
23418
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
22642
23419
  process.exit(EXIT_USAGE);
22643
23420
  }
22644
23421
  const formats = builtInRequested;
@@ -22726,10 +23503,14 @@ async function parseCliArgs(argv) {
22726
23503
  console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
22727
23504
  process.exit(EXIT_USAGE);
22728
23505
  }
23506
+ const checkFormat = parseTextJsonFormat("--check-format", values["check-format"]);
23507
+ const goalFormat = parseTextJsonFormat("--goal-format", values["goal-format"]);
23508
+ const triageFormat = parseTextJsonFormat("--triage-format", values["triage-format"]);
22729
23509
  const cliArgs = {
22730
23510
  subcommand,
22731
23511
  inputFile,
22732
23512
  baselineFile,
23513
+ baselineArg: baselineValue,
22733
23514
  currentFile,
22734
23515
  baselineMode,
22735
23516
  baselineDir: values["baseline-dir"],
@@ -22756,6 +23537,15 @@ async function parseCliArgs(argv) {
22756
23537
  htmlThemePicker: values["html-theme-picker"],
22757
23538
  jsonSummary: values["json-summary"],
22758
23539
  listFormat: values["list-format"],
23540
+ checkFormat,
23541
+ noFail: values["no-fail"],
23542
+ requireTags: parseGlobs(values["require-tags"]),
23543
+ requireTickets: parseGlobs(values["require-tickets"]),
23544
+ requireScenarios: parseGlobs(values["require-scenarios"]),
23545
+ noRegressions: values["no-regressions"],
23546
+ noRatchet: values["no-ratchet"],
23547
+ goalFormat,
23548
+ triageFormat,
22759
23549
  emitCanonical: values["emit-canonical"],
22760
23550
  slackWebhook,
22761
23551
  teamsWebhook,
@@ -22972,6 +23762,24 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
22972
23762
  }
22973
23763
  return picked.file;
22974
23764
  }
23765
+ function resolveBaselineRun(args, currentRun) {
23766
+ if (!args.baselineArg) return void 0;
23767
+ let baselineFile;
23768
+ if (args.baselineArg === "auto") {
23769
+ if (!args.inputFile) {
23770
+ console.error("Error: --baseline auto requires a current input file (not --stdin).");
23771
+ process.exit(EXIT_USAGE);
23772
+ }
23773
+ baselineFile = resolveBaselineAuto(args.inputFile, currentRun, args);
23774
+ } else {
23775
+ baselineFile = args.baselineArg;
23776
+ }
23777
+ return applySelection(normalizeRunFromText(readFileInput(baselineFile), args).run, args);
23778
+ }
23779
+ function resolveBaselineStatusMap(args, currentRun) {
23780
+ const baselineRun = resolveBaselineRun(args, currentRun);
23781
+ return baselineRun ? new Map(baselineRun.testCases.map((tc) => [tc.id, tc.status])) : void 0;
23782
+ }
22975
23783
  async function main() {
22976
23784
  const { args, pluginConfig, customRequested } = await parseCliArgs(process.argv);
22977
23785
  const startMs = Date.now();
@@ -23082,6 +23890,51 @@ async function main() {
23082
23890
  console.log(output);
23083
23891
  process.exit(EXIT_SUCCESS);
23084
23892
  }
23893
+ if (args.subcommand === "check") {
23894
+ const text3 = await readInput(args);
23895
+ const run = applySelection(normalizeRunFromText(text3, args).run, args);
23896
+ const baseline = resolveBaselineStatusMap(args, run);
23897
+ const report = buildCheck(
23898
+ { testCases: run.testCases, baseline, format: args.checkFormat },
23899
+ {}
23900
+ );
23901
+ console.log(renderCheck(report, args.checkFormat));
23902
+ if (report.summary.failed > 0 && !args.noFail) {
23903
+ process.exit(EXIT_AGENT_GATE);
23904
+ }
23905
+ process.exit(EXIT_SUCCESS);
23906
+ }
23907
+ if (args.subcommand === "goal") {
23908
+ const text3 = await readInput(args);
23909
+ const run = applySelection(normalizeRunFromText(text3, args).run, args);
23910
+ const baseline = resolveBaselineRun(args, run);
23911
+ const report = buildGoal(
23912
+ {
23913
+ run,
23914
+ baseline,
23915
+ requireTags: args.requireTags,
23916
+ requireTickets: args.requireTickets,
23917
+ requireScenarios: args.requireScenarios,
23918
+ enforceNoRegressions: args.noRegressions,
23919
+ enforceRatchet: !args.noRatchet,
23920
+ format: args.goalFormat
23921
+ },
23922
+ {}
23923
+ );
23924
+ console.log(renderGoal(report, args.goalFormat));
23925
+ process.exit(report.met ? EXIT_SUCCESS : EXIT_AGENT_GATE);
23926
+ }
23927
+ if (args.subcommand === "triage") {
23928
+ const text3 = await readInput(args);
23929
+ const run = applySelection(normalizeRunFromText(text3, args).run, args);
23930
+ const baseline = resolveBaselineStatusMap(args, run);
23931
+ const report = buildTriage(
23932
+ { testCases: run.testCases, baseline, format: args.triageFormat },
23933
+ {}
23934
+ );
23935
+ console.log(renderTriage(report, args.triageFormat));
23936
+ process.exit(EXIT_SUCCESS);
23937
+ }
23085
23938
  if (args.subcommand === "watch") {
23086
23939
  if (!args.inputFile) {
23087
23940
  console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
@@ -23460,9 +24313,9 @@ function mapStatus(status) {
23460
24313
  function parseNameStatus(text2) {
23461
24314
  const files = [];
23462
24315
  for (const raw of text2.split("\n")) {
23463
- const line = raw.trim();
23464
- if (!line) continue;
23465
- const cols = line.includes(" ") ? line.split(" ") : line.split(/\s+/);
24316
+ const line2 = raw.trim();
24317
+ if (!line2) continue;
24318
+ const cols = line2.includes(" ") ? line2.split(" ") : line2.split(/\s+/);
23466
24319
  const status = cols[0];
23467
24320
  if (!status) continue;
23468
24321
  const filePath = /^[RC]/i.test(status) && cols.length >= 3 ? cols[cols.length - 1] : cols[1];
@@ -23969,7 +24822,9 @@ async function runBuildDocs(rawArgs) {
23969
24822
  options: {
23970
24823
  "site-dir": { type: "string" },
23971
24824
  openapi: { type: "string" },
23972
- "no-synthesize-stories": { type: "boolean", default: false }
24825
+ "no-synthesize-stories": { type: "boolean", default: false },
24826
+ "audience-split": { type: "boolean", default: false },
24827
+ baseline: { type: "string" }
23973
24828
  },
23974
24829
  allowPositionals: true,
23975
24830
  strict: true
@@ -23977,26 +24832,42 @@ async function runBuildDocs(rawArgs) {
23977
24832
  const rawRunPath = positionals[0];
23978
24833
  if (!rawRunPath) {
23979
24834
  console.error(
23980
- `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
24835
+ `Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>] [--baseline <prev-story-report.json>] [--audience-split]`
23981
24836
  );
23982
24837
  return EXIT_USAGE;
23983
24838
  }
24839
+ const audienceSplit = values["audience-split"];
23984
24840
  try {
23985
24841
  const result = await buildDocs({
23986
24842
  rawRunPath,
23987
24843
  siteDir: values["site-dir"] ?? ".",
23988
24844
  openapiPath: values.openapi,
23989
- synthesizeStories: !values["no-synthesize-stories"]
24845
+ synthesizeStories: !values["no-synthesize-stories"],
24846
+ audienceSplit,
24847
+ baselinePath: values.baseline
23990
24848
  });
23991
24849
  console.log(`\u2713 Living docs generated in ${result.siteDir}`);
23992
24850
  console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
23993
- console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
24851
+ console.log(` \u2022 Deep links \u2192 public/stories/scenario-links.json (${result.scenarioLinks})`);
24852
+ if (audienceSplit) {
24853
+ console.log(
24854
+ ` \u2022 Story pages \u2192 src/content/docs/stories/{engineer,stakeholder} (engineer: ${result.audiences.engineer}, stakeholder: ${result.audiences.stakeholder})`
24855
+ );
24856
+ } else {
24857
+ console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
24858
+ }
23994
24859
  if (result.bundledAssets > 0) {
23995
24860
  console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
23996
24861
  }
23997
24862
  if (result.apiPages > 0) {
23998
24863
  console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
23999
24864
  }
24865
+ if (result.changes) {
24866
+ const c = result.changes;
24867
+ console.log(
24868
+ ` \u2022 What's changed \u2192 src/content/docs/stories/changes.md (+${c.added} added, ${c.regressed} regressed, ${c.fixed} fixed, ${c.removed} removed)`
24869
+ );
24870
+ }
24000
24871
  const rel = path15.relative(process.cwd(), result.siteDir) || ".";
24001
24872
  console.log(`
24002
24873
  Preview: cd ${rel} && npm run dev`);
@@ -24200,6 +25071,15 @@ function createDefaultCliArgs() {
24200
25071
  htmlThemePicker: false,
24201
25072
  jsonSummary: false,
24202
25073
  listFormat: "text",
25074
+ checkFormat: "text",
25075
+ noFail: false,
25076
+ requireTags: [],
25077
+ requireTickets: [],
25078
+ requireScenarios: [],
25079
+ noRegressions: false,
25080
+ noRatchet: false,
25081
+ goalFormat: "text",
25082
+ triageFormat: "text",
24203
25083
  notify: "never",
24204
25084
  maxFailedTests: 5,
24205
25085
  maxHistoryRuns: 10,