executable-stories-formatters 0.9.0 → 0.11.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/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AstroFormatter: () => AstroFormatter,
34
+ BehaviorManifestJsonFormatter: () => BehaviorManifestJsonFormatter,
34
35
  ConfluenceFormatter: () => ConfluenceFormatter,
35
36
  CucumberHtmlFormatter: () => CucumberHtmlFormatter,
36
37
  CucumberJsonFormatter: () => CucumberJsonFormatter,
@@ -51,6 +52,7 @@ __export(src_exports, {
51
52
  STORY_META_KEY: () => STORY_META_KEY,
52
53
  STORY_REPORT_SCHEMA_MAJOR: () => STORY_REPORT_SCHEMA_MAJOR,
53
54
  STORY_REPORT_SCHEMA_VERSION: () => STORY_REPORT_SCHEMA_VERSION,
55
+ ScenarioIndexJsonFormatter: () => ScenarioIndexJsonFormatter,
54
56
  StoryReportJsonFormatter: () => StoryReportJsonFormatter,
55
57
  adaptJestRun: () => adaptJestRun,
56
58
  adaptPlaywrightRun: () => adaptPlaywrightRun,
@@ -61,6 +63,7 @@ __export(src_exports, {
61
63
  calculateFlakiness: () => calculateFlakiness,
62
64
  calculateStability: () => calculateStability,
63
65
  canonicalizeRun: () => canonicalizeRun,
66
+ classifyStatusChange: () => classifyStatusChange,
64
67
  clearVersionCache: () => clearVersionCache,
65
68
  computeTestMetrics: () => computeTestMetrics,
66
69
  copyMarkdownAssets: () => copyMarkdownAssets,
@@ -72,6 +75,7 @@ __export(src_exports, {
72
75
  detectCI: () => detectCI4,
73
76
  detectPerformanceTrend: () => detectPerformanceTrend,
74
77
  diffRuns: () => diffRuns,
78
+ diffStoryReports: () => diffStoryReports,
75
79
  findGitDir: () => findGitDir,
76
80
  formatDuration: () => formatDuration3,
77
81
  generateRunComparison: () => generateRunComparison,
@@ -83,6 +87,7 @@ __export(src_exports, {
83
87
  hasSufficientHistory: () => hasSufficientHistory,
84
88
  isReviewableSource: () => isReviewableSource,
85
89
  isTestFile: () => isTestFile,
90
+ joinNameAndExt: () => joinNameAndExt,
86
91
  listScenarios: () => listScenarios,
87
92
  loadHistory: () => loadHistory,
88
93
  mergeStepResults: () => mergeStepResults,
@@ -99,29 +104,34 @@ __export(src_exports, {
99
104
  readBranchName: () => readBranchName,
100
105
  readGitSha: () => readGitSha,
101
106
  readPackageVersion: () => readPackageVersion,
107
+ regenerateArtifacts: () => regenerateArtifacts,
102
108
  resolveAttachment: () => resolveAttachment,
103
109
  resolveAttachments: () => resolveAttachments,
104
110
  resolveTheme: () => resolveTheme,
105
111
  resolveTraceUrl: () => resolveTraceUrl,
106
112
  rewriteAssetPaths: () => rewriteAssetPaths,
107
113
  saveHistory: () => saveHistory,
114
+ scenariosCoveringPaths: () => scenariosCoveringPaths,
108
115
  sendNotifications: () => sendNotifications,
109
116
  sendSlackNotification: () => sendSlackNotification,
110
117
  sendTeamsNotification: () => sendTeamsNotification,
111
118
  sendWebhookNotification: () => sendWebhookNotification,
112
119
  signBody: () => signBody,
113
120
  slugify: () => slugify,
121
+ startWatch: () => startWatch,
114
122
  stripAnsi: () => stripAnsi,
123
+ toBehaviorManifest: () => toBehaviorManifest,
115
124
  toCIInfo: () => toCIInfo,
116
125
  toRawCIInfo: () => toRawCIInfo,
126
+ toScenarioIndex: () => toScenarioIndex,
117
127
  toStoryReport: () => toStoryReport,
118
128
  tryGetActiveOtelContext: () => tryGetActiveOtelContext,
119
129
  updateHistory: () => updateHistory,
120
130
  validateCanonicalRun: () => validateCanonicalRun
121
131
  });
122
132
  module.exports = __toCommonJS(src_exports);
123
- var fs8 = require("fs");
124
- var path9 = __toESM(require("path"), 1);
133
+ var fs9 = require("fs");
134
+ var path10 = __toESM(require("path"), 1);
125
135
  var fsPromises = __toESM(require("fs/promises"), 1);
126
136
 
127
137
  // src/converters/acl/status.ts
@@ -884,6 +894,15 @@ function copyDocEntry(entry) {
884
894
  phase: entry.phase,
885
895
  ...children
886
896
  };
897
+ case "video":
898
+ return {
899
+ kind: "video",
900
+ path: entry.path,
901
+ ...entry.caption ? { caption: entry.caption } : {},
902
+ ...entry.poster ? { poster: entry.poster } : {},
903
+ phase: entry.phase,
904
+ ...children
905
+ };
887
906
  case "custom":
888
907
  return {
889
908
  kind: "custom",
@@ -967,6 +986,9 @@ function buildScenario(tc, featureId) {
967
986
  if (tickets && tickets.length > 0) {
968
987
  scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
969
988
  }
989
+ if (tc.story.covers && tc.story.covers.length > 0) {
990
+ scenario.covers = [...tc.story.covers];
991
+ }
970
992
  return scenario;
971
993
  }
972
994
  function deriveFeatureTitle(group, relSourceFile) {
@@ -1090,6 +1112,181 @@ var StoryReportJsonFormatter = class {
1090
1112
  }
1091
1113
  };
1092
1114
 
1115
+ // src/formatters/scenario-index-json.ts
1116
+ var ScenarioIndexJsonFormatter = class {
1117
+ options;
1118
+ constructor(options = {}) {
1119
+ this.options = {
1120
+ pretty: options.pretty ?? true,
1121
+ filters: options.filters
1122
+ };
1123
+ }
1124
+ toIndex(run) {
1125
+ return toScenarioIndex(toStoryReport(run), this.options.filters);
1126
+ }
1127
+ format(run) {
1128
+ const index = this.toIndex(run);
1129
+ return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
1130
+ }
1131
+ };
1132
+ function toScenarioIndex(report, filters = {}) {
1133
+ const scenarios = report.features.flatMap(
1134
+ (feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
1135
+ ).filter((scenario) => matchesFilters(scenario, filters));
1136
+ return {
1137
+ schemaVersion: "1.0",
1138
+ runId: report.runId,
1139
+ generatedAtMs: report.finishedAtMs,
1140
+ summary: summarize(scenarios),
1141
+ scenarios
1142
+ };
1143
+ }
1144
+ function toScenarioIndexItem(feature, scenario) {
1145
+ return {
1146
+ id: scenario.id,
1147
+ title: scenario.title,
1148
+ status: scenario.status,
1149
+ feature: feature.title,
1150
+ sourceFile: feature.sourceFile,
1151
+ sourceLine: scenario.sourceLine,
1152
+ tags: scenario.tags,
1153
+ tickets: scenario.tickets ?? [],
1154
+ covers: scenario.covers ?? [],
1155
+ durationMs: scenario.durationMs,
1156
+ steps: scenario.steps.map((step) => ({
1157
+ id: step.id,
1158
+ index: step.index,
1159
+ keyword: step.keyword,
1160
+ text: step.text,
1161
+ status: step.status,
1162
+ durationMs: step.durationMs,
1163
+ errorMessage: step.errorMessage,
1164
+ docKinds: step.docEntries.map((entry) => entry.kind)
1165
+ })),
1166
+ docKinds: scenario.docEntries.map((entry) => entry.kind),
1167
+ error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
1168
+ };
1169
+ }
1170
+ function matchesFilters(scenario, filters) {
1171
+ if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
1172
+ return false;
1173
+ }
1174
+ if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
1175
+ return false;
1176
+ }
1177
+ if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
1178
+ return false;
1179
+ }
1180
+ return true;
1181
+ }
1182
+ function summarize(scenarios) {
1183
+ return {
1184
+ total: scenarios.length,
1185
+ passed: scenarios.filter((scenario) => scenario.status === "passed").length,
1186
+ failed: scenarios.filter((scenario) => scenario.status === "failed").length,
1187
+ skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
1188
+ pending: scenarios.filter((scenario) => scenario.status === "pending").length,
1189
+ durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
1190
+ };
1191
+ }
1192
+
1193
+ // src/formatters/behavior-manifest-json.ts
1194
+ var BehaviorManifestJsonFormatter = class {
1195
+ pretty;
1196
+ constructor(options = {}) {
1197
+ this.pretty = options.pretty ?? true;
1198
+ }
1199
+ toManifest(run) {
1200
+ return toBehaviorManifest(toStoryReport(run));
1201
+ }
1202
+ format(run) {
1203
+ const manifest = this.toManifest(run);
1204
+ return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
1205
+ }
1206
+ };
1207
+ function toBehaviorManifest(report) {
1208
+ const index = toScenarioIndex(report);
1209
+ const bySource = /* @__PURE__ */ new Map();
1210
+ const byTag = /* @__PURE__ */ new Map();
1211
+ const docKinds = /* @__PURE__ */ new Set();
1212
+ const debuggerIssues = [];
1213
+ for (const scenario of index.scenarios) {
1214
+ const source = bySource.get(scenario.sourceFile) ?? {
1215
+ path: scenario.sourceFile,
1216
+ scenarioCount: 0,
1217
+ failed: 0,
1218
+ tags: []
1219
+ };
1220
+ source.scenarioCount += 1;
1221
+ if (scenario.status === "failed") source.failed += 1;
1222
+ source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
1223
+ bySource.set(scenario.sourceFile, source);
1224
+ for (const tag of scenario.tags) {
1225
+ const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
1226
+ tagEntry.scenarioCount += 1;
1227
+ byTag.set(tag, tagEntry);
1228
+ }
1229
+ for (const kind of scenario.docKinds) docKinds.add(kind);
1230
+ for (const step of scenario.steps) {
1231
+ for (const kind of step.docKinds) docKinds.add(kind);
1232
+ }
1233
+ if (!scenarioHasDocs(scenario)) {
1234
+ debuggerIssues.push({
1235
+ severity: "warning",
1236
+ code: "missing-docs",
1237
+ scenarioId: scenario.id,
1238
+ title: scenario.title,
1239
+ message: "Scenario has no doc entries."
1240
+ });
1241
+ }
1242
+ if (scenario.tags.length === 0) {
1243
+ debuggerIssues.push({
1244
+ severity: "warning",
1245
+ code: "missing-tags",
1246
+ scenarioId: scenario.id,
1247
+ title: scenario.title,
1248
+ message: "Scenario has no tags."
1249
+ });
1250
+ }
1251
+ if (scenario.covers.length === 0) {
1252
+ debuggerIssues.push({
1253
+ severity: "warning",
1254
+ code: "missing-covers",
1255
+ scenarioId: scenario.id,
1256
+ title: scenario.title,
1257
+ message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
1258
+ });
1259
+ }
1260
+ if (scenario.sourceLine === void 0) {
1261
+ debuggerIssues.push({
1262
+ severity: "warning",
1263
+ code: "missing-source-line",
1264
+ scenarioId: scenario.id,
1265
+ title: scenario.title,
1266
+ message: "Scenario has no source line."
1267
+ });
1268
+ }
1269
+ }
1270
+ const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
1271
+ return {
1272
+ schemaVersion: "1.0",
1273
+ runId: report.runId,
1274
+ generatedAtMs: report.finishedAtMs,
1275
+ summary: index.summary,
1276
+ sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
1277
+ tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
1278
+ docCoverage: {
1279
+ scenariosWithDocs,
1280
+ scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
1281
+ docKinds: [...docKinds].sort()
1282
+ },
1283
+ debugger: debuggerIssues
1284
+ };
1285
+ }
1286
+ function scenarioHasDocs(scenario) {
1287
+ return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
1288
+ }
1289
+
1093
1290
  // src/formatters/html/renderers/index.ts
1094
1291
  var fs2 = __toESM(require("fs"), 1);
1095
1292
  var path3 = __toESM(require("path"), 1);
@@ -13867,6 +14064,23 @@ function renderDocScreenshot(entry, deps) {
13867
14064
  ${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
13868
14065
  </div>`;
13869
14066
  }
14067
+ function renderDocVideo(entry, deps) {
14068
+ const isRemote = /^(?:https?:|data:)/i.test(entry.path);
14069
+ const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
14070
+ const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
14071
+ if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
14072
+ return `<div class="doc-video doc-video-missing">
14073
+ <div class="doc-video-missing-label">Video unavailable</div>
14074
+ <div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
14075
+ ${captionHtml}
14076
+ </div>`;
14077
+ }
14078
+ const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
14079
+ return `<div class="doc-video">
14080
+ <video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
14081
+ ${captionHtml}
14082
+ </div>`;
14083
+ }
13870
14084
  function renderDocCustom(entry, deps) {
13871
14085
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
13872
14086
  const data = entry.data;
@@ -13920,6 +14134,9 @@ function renderDocEntry(entry, deps) {
13920
14134
  case "screenshot":
13921
14135
  html = renderDocScreenshot(entry, deps);
13922
14136
  break;
14137
+ case "video":
14138
+ html = renderDocVideo(entry, deps);
14139
+ break;
13923
14140
  case "custom":
13924
14141
  html = renderDocCustom(entry, deps);
13925
14142
  break;
@@ -15246,6 +15463,19 @@ var MarkdownFormatter = class {
15246
15463
  case "screenshot":
15247
15464
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
15248
15465
  break;
15466
+ case "video": {
15467
+ const poster = entry.poster ? ` poster="${entry.poster}"` : "";
15468
+ lines.push(`${indent}`);
15469
+ lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
15470
+ lines.push(`${indent} <source src="${entry.path}" />`);
15471
+ lines.push(`${indent}</video>`);
15472
+ if (entry.caption) {
15473
+ lines.push(`${indent}`);
15474
+ lines.push(`${indent}*${entry.caption}*`);
15475
+ }
15476
+ lines.push(`${indent}`);
15477
+ break;
15478
+ }
15249
15479
  case "custom":
15250
15480
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
15251
15481
  const data = entry.data;
@@ -15958,8 +16188,8 @@ function extractDocAttachments(step) {
15958
16188
  }
15959
16189
  return attachments;
15960
16190
  }
15961
- function guessMediaType(path10) {
15962
- const lower = path10.toLowerCase();
16191
+ function guessMediaType(path11) {
16192
+ const lower = path11.toLowerCase();
15963
16193
  if (lower.endsWith(".png")) return "image/png";
15964
16194
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15965
16195
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16100,11 +16330,11 @@ var CucumberHtmlFormatter = class {
16100
16330
  for (const envelope of envelopes) {
16101
16331
  const accepted = htmlStream.write(envelope);
16102
16332
  if (!accepted) {
16103
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
16333
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
16104
16334
  }
16105
16335
  }
16106
- await new Promise((resolve7, reject) => {
16107
- collector.on("finish", resolve7);
16336
+ await new Promise((resolve8, reject) => {
16337
+ collector.on("finish", resolve8);
16108
16338
  collector.on("error", reject);
16109
16339
  htmlStream.end();
16110
16340
  });
@@ -16405,6 +16635,8 @@ function formatDocEntry(doc) {
16405
16635
  return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
16406
16636
  case "screenshot":
16407
16637
  return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
16638
+ case "video":
16639
+ return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
16408
16640
  case "custom":
16409
16641
  return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
16410
16642
  }
@@ -16861,6 +17093,8 @@ function formatDocEntry2(doc) {
16861
17093
  return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
16862
17094
  case "screenshot":
16863
17095
  return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
17096
+ case "video":
17097
+ return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
16864
17098
  case "custom":
16865
17099
  return `${doc.type}: ${JSON.stringify(doc.data)}`;
16866
17100
  }
@@ -17106,19 +17340,35 @@ function replaceAssetRef(html, original, replacement) {
17106
17340
  return html;
17107
17341
  }
17108
17342
 
17343
+ // src/utils/source-file.ts
17344
+ function cleanTestStem(fileName) {
17345
+ const base = fileName.split(/[\\/]/).pop() ?? fileName;
17346
+ const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
17347
+ if (stripped !== base) return stripped;
17348
+ return base.replace(/\.[^.]+$/, "");
17349
+ }
17350
+ function humanizeSourceFile(fileName) {
17351
+ return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
17352
+ }
17353
+
17109
17354
  // src/formatters/astro.ts
17110
17355
  var AstroFormatter = class _AstroFormatter {
17111
17356
  markdownFormatter;
17112
17357
  title;
17358
+ perFileTitle;
17113
17359
  constructor(options = {}) {
17114
17360
  this.title = options.markdown?.title ?? "User Stories";
17361
+ this.perFileTitle = options.perFileTitle ?? false;
17115
17362
  this.markdownFormatter = new MarkdownFormatter({
17116
17363
  ...options.markdown,
17117
17364
  title: this.title,
17118
17365
  stepStyle: "gherkin",
17119
17366
  includeFrontMatter: false,
17120
17367
  includeSummaryTable: false,
17121
- includeMetadata: false
17368
+ includeMetadata: false,
17369
+ // A per-file page is one file already — group by suite/describe so the
17370
+ // body shows clean section headings, not the redundant source path.
17371
+ groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
17122
17372
  });
17123
17373
  }
17124
17374
  format(run) {
@@ -17128,13 +17378,31 @@ var AstroFormatter = class _AstroFormatter {
17128
17378
  return `${frontmatter}
17129
17379
  ${body}`;
17130
17380
  }
17381
+ /**
17382
+ * Title for the page. A per-file page (one source file — i.e. colocated mode)
17383
+ * is titled by its suite/describe name, falling back to a humanized filename,
17384
+ * so the docs nav reads "Convert Currency" not "User Stories" six times over.
17385
+ * Multi-file (aggregated) pages keep the configured title.
17386
+ */
17387
+ deriveTitle(run) {
17388
+ if (!this.perFileTitle) return this.title;
17389
+ const sourceFiles = new Set(
17390
+ run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
17391
+ );
17392
+ if (sourceFiles.size !== 1) return this.title;
17393
+ const suites = new Set(
17394
+ run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
17395
+ );
17396
+ if (suites.size === 1) return [...suites][0];
17397
+ return humanizeSourceFile([...sourceFiles][0]) || this.title;
17398
+ }
17131
17399
  buildFrontmatter(run) {
17132
17400
  const badge = _AstroFormatter.computeBadge(run.testCases);
17133
17401
  const count = run.testCases.length;
17134
17402
  const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
17135
17403
  const lines = [
17136
17404
  "---",
17137
- `title: ${this.title}`,
17405
+ `title: ${yamlScalar(this.deriveTitle(run))}`,
17138
17406
  `description: ${description}`,
17139
17407
  "sidebar:",
17140
17408
  " badge:",
@@ -17152,6 +17420,12 @@ ${body}`;
17152
17420
  return { text: "Passed", variant: "success" };
17153
17421
  }
17154
17422
  };
17423
+ function yamlScalar(value) {
17424
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
17425
+ return `'${value.replace(/'/g, "''")}'`;
17426
+ }
17427
+ return value;
17428
+ }
17155
17429
 
17156
17430
  // src/formatters/confluence.ts
17157
17431
  var ConfluenceFormatter = class {
@@ -17428,6 +17702,15 @@ ${tc.errorStack}` : "");
17428
17702
  ])
17429
17703
  );
17430
17704
  break;
17705
+ case "video":
17706
+ content.push(
17707
+ paragraph([
17708
+ text(entry.caption ?? "Video", strong()),
17709
+ text(": "),
17710
+ link(entry.path, entry.path)
17711
+ ])
17712
+ );
17713
+ break;
17431
17714
  case "custom":
17432
17715
  content.push(paragraph([text(`[${entry.type}]`, strong())]));
17433
17716
  content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
@@ -17597,6 +17880,13 @@ function scanMarkdownAssets(markdown) {
17597
17880
  found.add(src);
17598
17881
  }
17599
17882
  }
17883
+ const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
17884
+ while ((match = posterRe.exec(stripped)) !== null) {
17885
+ const src = match[1].trim();
17886
+ if (isLocalPath(src)) {
17887
+ found.add(src);
17888
+ }
17889
+ }
17600
17890
  return Array.from(found);
17601
17891
  }
17602
17892
  function splitByCode(markdown) {
@@ -17647,6 +17937,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
17647
17937
  return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
17648
17938
  }
17649
17939
  );
17940
+ result = result.replace(
17941
+ /(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
17942
+ (full, pre, src, post) => {
17943
+ const trimmed = src.trim();
17944
+ if (!isLocalPath(trimmed)) return full;
17945
+ if (pathMap) {
17946
+ const mapped = pathMap.get(trimmed);
17947
+ if (mapped === void 0) return full;
17948
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
17949
+ }
17950
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
17951
+ }
17952
+ );
17650
17953
  return result;
17651
17954
  }
17652
17955
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
@@ -18093,6 +18396,184 @@ ${result.errors.join("\n")}`);
18093
18396
  }
18094
18397
  }
18095
18398
 
18399
+ // src/coverage-index.ts
18400
+ function normalizePath(path11) {
18401
+ return path11.replace(/^\.\//, "");
18402
+ }
18403
+ function scenariosCoveringPaths(index, paths) {
18404
+ const queries = paths.map(normalizePath);
18405
+ return index.scenarios.filter(
18406
+ (scenario) => scenario.covers.some(
18407
+ (glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
18408
+ )
18409
+ );
18410
+ }
18411
+
18412
+ // src/watch.ts
18413
+ var fs6 = __toESM(require("fs"), 1);
18414
+ var path7 = __toESM(require("path"), 1);
18415
+
18416
+ // src/converters/synthesize.ts
18417
+ var KEYWORD_MAP = {
18418
+ given: "Given",
18419
+ when: "When",
18420
+ then: "Then",
18421
+ and: "And",
18422
+ but: "But"
18423
+ };
18424
+ function normalizeKeyword(keyword) {
18425
+ return KEYWORD_MAP[keyword.toLowerCase()] ?? keyword;
18426
+ }
18427
+ function normalizeStepKeywords(steps) {
18428
+ return steps.map((step) => ({
18429
+ ...step,
18430
+ keyword: normalizeKeyword(step.keyword)
18431
+ }));
18432
+ }
18433
+ function deriveScenarioName(tc) {
18434
+ if (tc.title) return tc.title;
18435
+ if (tc.titlePath && tc.titlePath.length > 0) {
18436
+ return tc.titlePath[tc.titlePath.length - 1];
18437
+ }
18438
+ return "Untitled";
18439
+ }
18440
+ function synthesizeStories(raw) {
18441
+ return {
18442
+ ...raw,
18443
+ testCases: raw.testCases.map(synthesizeTestCase)
18444
+ };
18445
+ }
18446
+ function synthesizeTestCase(tc) {
18447
+ if (tc.story == null) {
18448
+ const scenario = deriveScenarioName(tc);
18449
+ return {
18450
+ ...tc,
18451
+ story: {
18452
+ scenario,
18453
+ steps: [{ keyword: "Then", text: scenario }]
18454
+ }
18455
+ };
18456
+ }
18457
+ const steps = tc.story.steps;
18458
+ if (!steps || steps.length === 0) {
18459
+ return {
18460
+ ...tc,
18461
+ story: {
18462
+ ...tc.story,
18463
+ steps: [{ keyword: "Then", text: tc.story.scenario }]
18464
+ }
18465
+ };
18466
+ }
18467
+ return {
18468
+ ...tc,
18469
+ story: {
18470
+ ...tc.story,
18471
+ steps: normalizeStepKeywords(steps)
18472
+ }
18473
+ };
18474
+ }
18475
+
18476
+ // src/watch.ts
18477
+ function toRun(data, inputType, synthesize) {
18478
+ if (inputType === "canonical") return data;
18479
+ let raw = data;
18480
+ if (synthesize) raw = synthesizeStories(raw);
18481
+ return canonicalizeRun(raw);
18482
+ }
18483
+ async function regenerateArtifacts(options, deps = {}) {
18484
+ const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18485
+ const data = JSON.parse(read(path7.resolve(options.input)));
18486
+ const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
18487
+ const generator = new ReportGenerator({
18488
+ formats: options.formats,
18489
+ outputDir: options.outputDir,
18490
+ outputName: options.outputName
18491
+ });
18492
+ const result = await generator.generate(run);
18493
+ return [...result.values()].flat();
18494
+ }
18495
+ function startWatch(options, deps = {}) {
18496
+ const log = deps.log ?? ((message) => console.log(message));
18497
+ const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
18498
+ const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
18499
+ const debounceMs = options.debounceMs ?? 150;
18500
+ let timer;
18501
+ let running = false;
18502
+ let pending = false;
18503
+ const run = async () => {
18504
+ if (running) {
18505
+ pending = true;
18506
+ return;
18507
+ }
18508
+ running = true;
18509
+ try {
18510
+ const files = await regenerate(options.input);
18511
+ log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
18512
+ } catch (error) {
18513
+ log(`Watch regeneration failed: ${error.message}`);
18514
+ } finally {
18515
+ running = false;
18516
+ if (pending) {
18517
+ pending = false;
18518
+ trigger();
18519
+ }
18520
+ }
18521
+ };
18522
+ const trigger = () => {
18523
+ if (timer) clearTimeout(timer);
18524
+ timer = setTimeout(() => void run(), debounceMs);
18525
+ };
18526
+ trigger();
18527
+ const watcher = watchFn(path7.resolve(options.input), trigger);
18528
+ return {
18529
+ close: () => {
18530
+ if (timer) clearTimeout(timer);
18531
+ watcher.close();
18532
+ }
18533
+ };
18534
+ }
18535
+
18536
+ // src/behavior-diff.ts
18537
+ function classifyStatusChange(baseline, current) {
18538
+ if (baseline === void 0) return "added";
18539
+ if (current === void 0) return "removed";
18540
+ if (baseline === current) return "unchanged";
18541
+ if (baseline === "passed" && current === "failed") return "regressed";
18542
+ if (baseline === "failed" && current === "passed") return "fixed";
18543
+ return "changed";
18544
+ }
18545
+ function scenarioMap(report) {
18546
+ const map = /* @__PURE__ */ new Map();
18547
+ for (const feature of report.features) {
18548
+ for (const scenario of feature.scenarios) {
18549
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
18550
+ }
18551
+ }
18552
+ return map;
18553
+ }
18554
+ function diffStoryReports(baseline, current) {
18555
+ const base = scenarioMap(baseline);
18556
+ const curr = scenarioMap(current);
18557
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
18558
+ const scenarios = ids.map((id) => {
18559
+ const b = base.get(id);
18560
+ const c = curr.get(id);
18561
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
18562
+ const meta = c ?? b;
18563
+ return {
18564
+ id,
18565
+ title: meta.scenario.title,
18566
+ sourceFile: meta.sourceFile,
18567
+ kind,
18568
+ baselineStatus: b?.scenario.status,
18569
+ currentStatus: c?.scenario.status
18570
+ };
18571
+ });
18572
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
18573
+ for (const s of scenarios) summary[s.kind] += 1;
18574
+ return { schemaVersion: "1.0", summary, scenarios };
18575
+ }
18576
+
18096
18577
  // src/publishers/confluence.ts
18097
18578
  function parseAdf(adf) {
18098
18579
  let parsed;
@@ -18688,27 +19169,27 @@ function pickleStepArgumentToDocs(ps) {
18688
19169
  }
18689
19170
 
18690
19171
  // src/utils/git-info.ts
18691
- var fs6 = __toESM(require("fs"), 1);
18692
- var path7 = __toESM(require("path"), 1);
19172
+ var fs7 = __toESM(require("fs"), 1);
19173
+ var path8 = __toESM(require("path"), 1);
18693
19174
  function readGitSha(cwd = process.cwd()) {
18694
19175
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
18695
19176
  if (envSha) return envSha;
18696
19177
  const gitDir = findGitDir(cwd);
18697
19178
  if (!gitDir) return void 0;
18698
19179
  try {
18699
- const headPath = path7.join(gitDir, "HEAD");
18700
- const head = fs6.readFileSync(headPath, "utf8").trim();
19180
+ const headPath = path8.join(gitDir, "HEAD");
19181
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18701
19182
  if (!head.startsWith("ref:")) {
18702
19183
  return head;
18703
19184
  }
18704
19185
  const refPath = head.replace("ref:", "").trim();
18705
- const refFile = path7.join(gitDir, refPath);
18706
- if (fs6.existsSync(refFile)) {
18707
- return fs6.readFileSync(refFile, "utf8").trim();
19186
+ const refFile = path8.join(gitDir, refPath);
19187
+ if (fs7.existsSync(refFile)) {
19188
+ return fs7.readFileSync(refFile, "utf8").trim();
18708
19189
  }
18709
- const packedRefs = path7.join(gitDir, "packed-refs");
18710
- if (fs6.existsSync(packedRefs)) {
18711
- const content = fs6.readFileSync(packedRefs, "utf8");
19190
+ const packedRefs = path8.join(gitDir, "packed-refs");
19191
+ if (fs7.existsSync(packedRefs)) {
19192
+ const content = fs7.readFileSync(packedRefs, "utf8");
18712
19193
  for (const line of content.split("\n")) {
18713
19194
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
18714
19195
  const [sha, ref] = line.split(" ");
@@ -18723,19 +19204,19 @@ function readGitSha(cwd = process.cwd()) {
18723
19204
  function findGitDir(start) {
18724
19205
  let current = start;
18725
19206
  while (true) {
18726
- const candidate = path7.join(current, ".git");
18727
- if (fs6.existsSync(candidate)) {
18728
- const stat = fs6.statSync(candidate);
19207
+ const candidate = path8.join(current, ".git");
19208
+ if (fs7.existsSync(candidate)) {
19209
+ const stat = fs7.statSync(candidate);
18729
19210
  if (stat.isFile()) {
18730
- const content = fs6.readFileSync(candidate, "utf8").trim();
19211
+ const content = fs7.readFileSync(candidate, "utf8").trim();
18731
19212
  const match = content.match(/^gitdir: (.+)$/);
18732
19213
  if (match) {
18733
- return path7.resolve(current, match[1]);
19214
+ return path8.resolve(current, match[1]);
18734
19215
  }
18735
19216
  }
18736
19217
  return candidate;
18737
19218
  }
18738
- const parent = path7.dirname(current);
19219
+ const parent = path8.dirname(current);
18739
19220
  if (parent === current) return void 0;
18740
19221
  current = parent;
18741
19222
  }
@@ -18746,8 +19227,8 @@ function readBranchName(cwd = process.cwd()) {
18746
19227
  const gitDir = findGitDir(cwd);
18747
19228
  if (!gitDir) return void 0;
18748
19229
  try {
18749
- const headPath = path7.join(gitDir, "HEAD");
18750
- const head = fs6.readFileSync(headPath, "utf8").trim();
19230
+ const headPath = path8.join(gitDir, "HEAD");
19231
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18751
19232
  if (head.startsWith("ref:")) {
18752
19233
  const refPath = head.replace("ref:", "").trim();
18753
19234
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -18784,8 +19265,8 @@ function nanosecondsToMs(ns) {
18784
19265
  }
18785
19266
 
18786
19267
  // src/utils/metadata.ts
18787
- var fs7 = __toESM(require("fs"), 1);
18788
- var path8 = __toESM(require("path"), 1);
19268
+ var fs8 = __toESM(require("fs"), 1);
19269
+ var path9 = __toESM(require("path"), 1);
18789
19270
  var versionCache = /* @__PURE__ */ new Map();
18790
19271
  function readPackageVersion(root) {
18791
19272
  if (versionCache.has(root)) {
@@ -18796,18 +19277,18 @@ function readPackageVersion(root) {
18796
19277
  return version;
18797
19278
  }
18798
19279
  function findPackageVersion(startDir) {
18799
- let current = path8.resolve(startDir);
19280
+ let current = path9.resolve(startDir);
18800
19281
  while (true) {
18801
- const pkgPath = path8.join(current, "package.json");
19282
+ const pkgPath = path9.join(current, "package.json");
18802
19283
  try {
18803
- if (fs7.existsSync(pkgPath)) {
18804
- const raw = fs7.readFileSync(pkgPath, "utf8");
19284
+ if (fs8.existsSync(pkgPath)) {
19285
+ const raw = fs8.readFileSync(pkgPath, "utf8");
18805
19286
  const parsed = JSON.parse(raw);
18806
19287
  return parsed.version;
18807
19288
  }
18808
19289
  } catch {
18809
19290
  }
18810
- const parent = path8.dirname(current);
19291
+ const parent = path9.dirname(current);
18811
19292
  if (parent === current) {
18812
19293
  return void 0;
18813
19294
  }
@@ -19678,12 +20159,22 @@ function listScenarios(args, _deps) {
19678
20159
  const { testCases, format } = args;
19679
20160
  if (format === "json") {
19680
20161
  const items = testCases.map((tc) => ({
20162
+ id: tc.id,
19681
20163
  scenario: tc.story.scenario,
19682
20164
  status: tc.status,
19683
20165
  sourceFile: tc.sourceFile,
19684
20166
  sourceLine: tc.sourceLine,
20167
+ suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
19685
20168
  tags: tc.tags,
19686
- id: tc.id
20169
+ tickets: tc.story.tickets ?? [],
20170
+ covers: tc.story.covers ?? [],
20171
+ durationMs: tc.durationMs,
20172
+ error: tc.errorMessage ? {
20173
+ message: tc.errorMessage,
20174
+ stack: tc.errorStack
20175
+ } : void 0,
20176
+ steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
20177
+ docKinds: collectDocKinds(tc)
19687
20178
  }));
19688
20179
  return JSON.stringify(items, null, 2);
19689
20180
  }
@@ -19756,6 +20247,34 @@ function listScenarios(args, _deps) {
19756
20247
  ];
19757
20248
  return lines.join("\n");
19758
20249
  }
20250
+ function toScenarioStep(step, index, testCase) {
20251
+ const result = testCase.stepResults.find(
20252
+ (candidate) => candidate.index === index || candidate.stepId === step.id
20253
+ );
20254
+ return {
20255
+ id: step.id,
20256
+ index,
20257
+ keyword: step.keyword,
20258
+ text: step.text,
20259
+ status: result?.status ?? testCase.status,
20260
+ durationMs: result?.durationMs ?? step.durationMs ?? 0,
20261
+ errorMessage: result?.errorMessage,
20262
+ mode: step.mode,
20263
+ docKinds: (step.docs ?? []).map((doc) => doc.kind)
20264
+ };
20265
+ }
20266
+ function collectDocKinds(testCase) {
20267
+ const kinds = /* @__PURE__ */ new Set();
20268
+ for (const doc of testCase.story.docs ?? []) {
20269
+ kinds.add(doc.kind);
20270
+ }
20271
+ for (const step of testCase.story.steps) {
20272
+ for (const doc of step.docs ?? []) {
20273
+ kinds.add(doc.kind);
20274
+ }
20275
+ }
20276
+ return [...kinds].sort();
20277
+ }
19759
20278
 
19760
20279
  // src/review/conventions.ts
19761
20280
  var CHANGE_TAG_PREFIX = "change:";
@@ -19803,18 +20322,18 @@ function deriveChangeType(tags) {
19803
20322
  }
19804
20323
  return "unknown";
19805
20324
  }
19806
- function extensionOf(path10) {
19807
- const base = path10.split("/").pop() ?? path10;
20325
+ function extensionOf(path11) {
20326
+ const base = path11.split("/").pop() ?? path11;
19808
20327
  const dot = base.lastIndexOf(".");
19809
20328
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
19810
20329
  }
19811
- function isTestFile(path10) {
19812
- return TEST_INFIX.test(path10);
20330
+ function isTestFile(path11) {
20331
+ return TEST_INFIX.test(path11);
19813
20332
  }
19814
- function isReviewableSource(path10) {
19815
- if (isTestFile(path10)) return false;
19816
- if (path10.endsWith(".d.ts")) return false;
19817
- return CODE_EXTENSIONS.has(extensionOf(path10));
20333
+ function isReviewableSource(path11) {
20334
+ if (isTestFile(path11)) return false;
20335
+ if (path11.endsWith(".d.ts")) return false;
20336
+ return CODE_EXTENSIONS.has(extensionOf(path11));
19818
20337
  }
19819
20338
  function testBaseKey(testFile) {
19820
20339
  return testFile.replace(TEST_INFIX, "");
@@ -19918,7 +20437,7 @@ function toClaim(testCase, changedSourcePaths) {
19918
20437
  const { strength, reasons } = gradeEvidence(testCase, audience);
19919
20438
  const key = testBaseKey(testCase.sourceFile);
19920
20439
  const coversFiles = changedSourcePaths.filter(
19921
- (path10) => sourceBaseKey(path10) === key
20440
+ (path11) => sourceBaseKey(path11) === key
19922
20441
  );
19923
20442
  return {
19924
20443
  id: testCase.id,
@@ -20451,6 +20970,7 @@ applyTheme(getEffectiveTheme());` : "";
20451
20970
  // src/index.ts
20452
20971
  var FORMAT_EXTENSIONS = {
20453
20972
  astro: ".md",
20973
+ "behavior-manifest-json": ".behavior-manifest.json",
20454
20974
  markdown: ".md",
20455
20975
  html: ".html",
20456
20976
  "cucumber-html": ".cucumber.html",
@@ -20458,8 +20978,13 @@ var FORMAT_EXTENSIONS = {
20458
20978
  "cucumber-json": ".cucumber.json",
20459
20979
  "cucumber-messages": ".ndjson",
20460
20980
  confluence: ".adf.json",
20981
+ "scenario-index-json": ".scenarios-index.json",
20461
20982
  "story-report-json": ".story-report.json"
20462
20983
  };
20984
+ function joinNameAndExt(name, ext) {
20985
+ const stutter = `.${name}.`;
20986
+ return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
20987
+ }
20463
20988
  var TEST_EXTENSIONS = [
20464
20989
  ".test.ts",
20465
20990
  ".test.tsx",
@@ -20485,11 +21010,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20485
21010
  const ext = FORMAT_EXTENSIONS[format];
20486
21011
  const effectiveName = outputName + (outputNameSuffix ?? "");
20487
21012
  if (mode === "aggregated") {
20488
- return toPosix(path9.join(baseOutputDir, `${effectiveName}${ext}`));
21013
+ return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
20489
21014
  }
20490
21015
  const normalizedSource = toPosix(sourceFile);
20491
- const dirOfSource = path9.posix.dirname(normalizedSource);
20492
- let baseName = path9.posix.basename(normalizedSource);
21016
+ const dirOfSource = path10.posix.dirname(normalizedSource);
21017
+ let baseName = path10.posix.basename(normalizedSource);
20493
21018
  for (const testExt of TEST_EXTENSIONS) {
20494
21019
  if (baseName.endsWith(testExt)) {
20495
21020
  baseName = baseName.slice(0, -testExt.length);
@@ -20498,9 +21023,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20498
21023
  }
20499
21024
  const fileName = `${baseName}.${effectiveName}${ext}`;
20500
21025
  if (colocatedStyle === "adjacent") {
20501
- return toPosix(path9.posix.join(dirOfSource, fileName));
21026
+ return toPosix(path10.posix.join(dirOfSource, fileName));
20502
21027
  }
20503
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
21028
+ if (colocatedStyle === "flat") {
21029
+ return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21030
+ }
21031
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
20504
21032
  }
20505
21033
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
20506
21034
  const groups = /* @__PURE__ */ new Map();
@@ -20586,6 +21114,12 @@ var ReportGenerator = class {
20586
21114
  storyReportJson: {
20587
21115
  pretty: options.storyReportJson?.pretty ?? true
20588
21116
  },
21117
+ scenarioIndexJson: {
21118
+ pretty: options.scenarioIndexJson?.pretty ?? true
21119
+ },
21120
+ behaviorManifestJson: {
21121
+ pretty: options.behaviorManifestJson?.pretty ?? true
21122
+ },
20589
21123
  cucumberMessages: {
20590
21124
  uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
20591
21125
  includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
@@ -20701,8 +21235,8 @@ var ReportGenerator = class {
20701
21235
  if (astroPaths) {
20702
21236
  for (const mdPath of astroPaths) {
20703
21237
  const content = await fsPromises.readFile(mdPath, "utf8");
20704
- const mdDir = path9.dirname(mdPath);
20705
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
21238
+ const mdDir = path10.dirname(mdPath);
21239
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
20706
21240
  const result = copyMarkdownAssets({
20707
21241
  markdown: content,
20708
21242
  markdownDir: mdDir,
@@ -20733,9 +21267,9 @@ var ReportGenerator = class {
20733
21267
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
20734
21268
  const ext = FORMAT_EXTENSIONS[format];
20735
21269
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
20736
- const outputPath = toPosix(path9.join(this.options.outputDir, `${effectiveName}${ext}`));
21270
+ const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
20737
21271
  const content = await this.formatContent(run, format);
20738
- const dir = path9.dirname(outputPath);
21272
+ const dir = path10.dirname(outputPath);
20739
21273
  await fsPromises.mkdir(dir, { recursive: true });
20740
21274
  await this.deps.writeFile(outputPath, content);
20741
21275
  return [outputPath];
@@ -20747,7 +21281,7 @@ var ReportGenerator = class {
20747
21281
  testCases
20748
21282
  };
20749
21283
  const content = await this.formatContent(groupRun, format);
20750
- const dir = path9.dirname(outputPath);
21284
+ const dir = path10.dirname(outputPath);
20751
21285
  await fsPromises.mkdir(dir, { recursive: true });
20752
21286
  await this.deps.writeFile(outputPath, content);
20753
21287
  writtenPaths.push(outputPath);
@@ -20813,6 +21347,8 @@ var ReportGenerator = class {
20813
21347
  case "astro": {
20814
21348
  const formatter = new AstroFormatter({
20815
21349
  assetsBaseUrl: this.options.astro.assetsBaseUrl,
21350
+ // Colocated = one page per file, so title each by its own suite/file.
21351
+ perFileTitle: this.options.output.mode === "colocated",
20816
21352
  markdown: this.options.astro.markdown
20817
21353
  });
20818
21354
  return formatter.format(run);
@@ -20860,6 +21396,18 @@ var ReportGenerator = class {
20860
21396
  });
20861
21397
  return formatter.format(run);
20862
21398
  }
21399
+ case "scenario-index-json": {
21400
+ const formatter = new ScenarioIndexJsonFormatter({
21401
+ pretty: this.options.scenarioIndexJson.pretty
21402
+ });
21403
+ return formatter.format(run);
21404
+ }
21405
+ case "behavior-manifest-json": {
21406
+ const formatter = new BehaviorManifestJsonFormatter({
21407
+ pretty: this.options.behaviorManifestJson.pretty
21408
+ });
21409
+ return formatter.format(run);
21410
+ }
20863
21411
  default:
20864
21412
  throw new Error(`Unknown format: ${format}`);
20865
21413
  }
@@ -20876,7 +21424,7 @@ async function generateRunComparison(args) {
20876
21424
  await fsPromises.mkdir(outputDir, { recursive: true });
20877
21425
  for (const format of args.formats) {
20878
21426
  const ext = format === "html" ? ".html" : ".md";
20879
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
21427
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
20880
21428
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
20881
21429
  await fsPromises.writeFile(outputPath, content, "utf8");
20882
21430
  files.push(outputPath);
@@ -20898,6 +21446,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20898
21446
  // Annotate the CommonJS export names for ESM import in node:
20899
21447
  0 && (module.exports = {
20900
21448
  AstroFormatter,
21449
+ BehaviorManifestJsonFormatter,
20901
21450
  ConfluenceFormatter,
20902
21451
  CucumberHtmlFormatter,
20903
21452
  CucumberJsonFormatter,
@@ -20918,6 +21467,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20918
21467
  STORY_META_KEY,
20919
21468
  STORY_REPORT_SCHEMA_MAJOR,
20920
21469
  STORY_REPORT_SCHEMA_VERSION,
21470
+ ScenarioIndexJsonFormatter,
20921
21471
  StoryReportJsonFormatter,
20922
21472
  adaptJestRun,
20923
21473
  adaptPlaywrightRun,
@@ -20928,6 +21478,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20928
21478
  calculateFlakiness,
20929
21479
  calculateStability,
20930
21480
  canonicalizeRun,
21481
+ classifyStatusChange,
20931
21482
  clearVersionCache,
20932
21483
  computeTestMetrics,
20933
21484
  copyMarkdownAssets,
@@ -20939,6 +21490,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20939
21490
  detectCI,
20940
21491
  detectPerformanceTrend,
20941
21492
  diffRuns,
21493
+ diffStoryReports,
20942
21494
  findGitDir,
20943
21495
  formatDuration,
20944
21496
  generateRunComparison,
@@ -20950,6 +21502,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20950
21502
  hasSufficientHistory,
20951
21503
  isReviewableSource,
20952
21504
  isTestFile,
21505
+ joinNameAndExt,
20953
21506
  listScenarios,
20954
21507
  loadHistory,
20955
21508
  mergeStepResults,
@@ -20966,21 +21519,26 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20966
21519
  readBranchName,
20967
21520
  readGitSha,
20968
21521
  readPackageVersion,
21522
+ regenerateArtifacts,
20969
21523
  resolveAttachment,
20970
21524
  resolveAttachments,
20971
21525
  resolveTheme,
20972
21526
  resolveTraceUrl,
20973
21527
  rewriteAssetPaths,
20974
21528
  saveHistory,
21529
+ scenariosCoveringPaths,
20975
21530
  sendNotifications,
20976
21531
  sendSlackNotification,
20977
21532
  sendTeamsNotification,
20978
21533
  sendWebhookNotification,
20979
21534
  signBody,
20980
21535
  slugify,
21536
+ startWatch,
20981
21537
  stripAnsi,
21538
+ toBehaviorManifest,
20982
21539
  toCIInfo,
20983
21540
  toRawCIInfo,
21541
+ toScenarioIndex,
20984
21542
  toStoryReport,
20985
21543
  tryGetActiveOtelContext,
20986
21544
  updateHistory,