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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path9 from "path";
3
+ import * as path10 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -763,6 +763,15 @@ function copyDocEntry(entry) {
763
763
  phase: entry.phase,
764
764
  ...children
765
765
  };
766
+ case "video":
767
+ return {
768
+ kind: "video",
769
+ path: entry.path,
770
+ ...entry.caption ? { caption: entry.caption } : {},
771
+ ...entry.poster ? { poster: entry.poster } : {},
772
+ phase: entry.phase,
773
+ ...children
774
+ };
766
775
  case "custom":
767
776
  return {
768
777
  kind: "custom",
@@ -846,6 +855,9 @@ function buildScenario(tc, featureId) {
846
855
  if (tickets && tickets.length > 0) {
847
856
  scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
848
857
  }
858
+ if (tc.story.covers && tc.story.covers.length > 0) {
859
+ scenario.covers = [...tc.story.covers];
860
+ }
849
861
  return scenario;
850
862
  }
851
863
  function deriveFeatureTitle(group, relSourceFile) {
@@ -969,6 +981,181 @@ var StoryReportJsonFormatter = class {
969
981
  }
970
982
  };
971
983
 
984
+ // src/formatters/scenario-index-json.ts
985
+ var ScenarioIndexJsonFormatter = class {
986
+ options;
987
+ constructor(options = {}) {
988
+ this.options = {
989
+ pretty: options.pretty ?? true,
990
+ filters: options.filters
991
+ };
992
+ }
993
+ toIndex(run) {
994
+ return toScenarioIndex(toStoryReport(run), this.options.filters);
995
+ }
996
+ format(run) {
997
+ const index = this.toIndex(run);
998
+ return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
999
+ }
1000
+ };
1001
+ function toScenarioIndex(report, filters = {}) {
1002
+ const scenarios = report.features.flatMap(
1003
+ (feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
1004
+ ).filter((scenario) => matchesFilters(scenario, filters));
1005
+ return {
1006
+ schemaVersion: "1.0",
1007
+ runId: report.runId,
1008
+ generatedAtMs: report.finishedAtMs,
1009
+ summary: summarize(scenarios),
1010
+ scenarios
1011
+ };
1012
+ }
1013
+ function toScenarioIndexItem(feature, scenario) {
1014
+ return {
1015
+ id: scenario.id,
1016
+ title: scenario.title,
1017
+ status: scenario.status,
1018
+ feature: feature.title,
1019
+ sourceFile: feature.sourceFile,
1020
+ sourceLine: scenario.sourceLine,
1021
+ tags: scenario.tags,
1022
+ tickets: scenario.tickets ?? [],
1023
+ covers: scenario.covers ?? [],
1024
+ durationMs: scenario.durationMs,
1025
+ steps: scenario.steps.map((step) => ({
1026
+ id: step.id,
1027
+ index: step.index,
1028
+ keyword: step.keyword,
1029
+ text: step.text,
1030
+ status: step.status,
1031
+ durationMs: step.durationMs,
1032
+ errorMessage: step.errorMessage,
1033
+ docKinds: step.docEntries.map((entry) => entry.kind)
1034
+ })),
1035
+ docKinds: scenario.docEntries.map((entry) => entry.kind),
1036
+ error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
1037
+ };
1038
+ }
1039
+ function matchesFilters(scenario, filters) {
1040
+ if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
1041
+ return false;
1042
+ }
1043
+ if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
1044
+ return false;
1045
+ }
1046
+ if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
1047
+ return false;
1048
+ }
1049
+ return true;
1050
+ }
1051
+ function summarize(scenarios) {
1052
+ return {
1053
+ total: scenarios.length,
1054
+ passed: scenarios.filter((scenario) => scenario.status === "passed").length,
1055
+ failed: scenarios.filter((scenario) => scenario.status === "failed").length,
1056
+ skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
1057
+ pending: scenarios.filter((scenario) => scenario.status === "pending").length,
1058
+ durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
1059
+ };
1060
+ }
1061
+
1062
+ // src/formatters/behavior-manifest-json.ts
1063
+ var BehaviorManifestJsonFormatter = class {
1064
+ pretty;
1065
+ constructor(options = {}) {
1066
+ this.pretty = options.pretty ?? true;
1067
+ }
1068
+ toManifest(run) {
1069
+ return toBehaviorManifest(toStoryReport(run));
1070
+ }
1071
+ format(run) {
1072
+ const manifest = this.toManifest(run);
1073
+ return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
1074
+ }
1075
+ };
1076
+ function toBehaviorManifest(report) {
1077
+ const index = toScenarioIndex(report);
1078
+ const bySource = /* @__PURE__ */ new Map();
1079
+ const byTag = /* @__PURE__ */ new Map();
1080
+ const docKinds = /* @__PURE__ */ new Set();
1081
+ const debuggerIssues = [];
1082
+ for (const scenario of index.scenarios) {
1083
+ const source = bySource.get(scenario.sourceFile) ?? {
1084
+ path: scenario.sourceFile,
1085
+ scenarioCount: 0,
1086
+ failed: 0,
1087
+ tags: []
1088
+ };
1089
+ source.scenarioCount += 1;
1090
+ if (scenario.status === "failed") source.failed += 1;
1091
+ source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
1092
+ bySource.set(scenario.sourceFile, source);
1093
+ for (const tag of scenario.tags) {
1094
+ const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
1095
+ tagEntry.scenarioCount += 1;
1096
+ byTag.set(tag, tagEntry);
1097
+ }
1098
+ for (const kind of scenario.docKinds) docKinds.add(kind);
1099
+ for (const step of scenario.steps) {
1100
+ for (const kind of step.docKinds) docKinds.add(kind);
1101
+ }
1102
+ if (!scenarioHasDocs(scenario)) {
1103
+ debuggerIssues.push({
1104
+ severity: "warning",
1105
+ code: "missing-docs",
1106
+ scenarioId: scenario.id,
1107
+ title: scenario.title,
1108
+ message: "Scenario has no doc entries."
1109
+ });
1110
+ }
1111
+ if (scenario.tags.length === 0) {
1112
+ debuggerIssues.push({
1113
+ severity: "warning",
1114
+ code: "missing-tags",
1115
+ scenarioId: scenario.id,
1116
+ title: scenario.title,
1117
+ message: "Scenario has no tags."
1118
+ });
1119
+ }
1120
+ if (scenario.covers.length === 0) {
1121
+ debuggerIssues.push({
1122
+ severity: "warning",
1123
+ code: "missing-covers",
1124
+ scenarioId: scenario.id,
1125
+ title: scenario.title,
1126
+ message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
1127
+ });
1128
+ }
1129
+ if (scenario.sourceLine === void 0) {
1130
+ debuggerIssues.push({
1131
+ severity: "warning",
1132
+ code: "missing-source-line",
1133
+ scenarioId: scenario.id,
1134
+ title: scenario.title,
1135
+ message: "Scenario has no source line."
1136
+ });
1137
+ }
1138
+ }
1139
+ const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
1140
+ return {
1141
+ schemaVersion: "1.0",
1142
+ runId: report.runId,
1143
+ generatedAtMs: report.finishedAtMs,
1144
+ summary: index.summary,
1145
+ sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
1146
+ tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
1147
+ docCoverage: {
1148
+ scenariosWithDocs,
1149
+ scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
1150
+ docKinds: [...docKinds].sort()
1151
+ },
1152
+ debugger: debuggerIssues
1153
+ };
1154
+ }
1155
+ function scenarioHasDocs(scenario) {
1156
+ return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
1157
+ }
1158
+
972
1159
  // src/formatters/html/renderers/index.ts
973
1160
  import * as fs2 from "fs";
974
1161
  import * as path3 from "path";
@@ -13746,6 +13933,23 @@ function renderDocScreenshot(entry, deps) {
13746
13933
  ${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
13747
13934
  </div>`;
13748
13935
  }
13936
+ function renderDocVideo(entry, deps) {
13937
+ const isRemote = /^(?:https?:|data:)/i.test(entry.path);
13938
+ const isAbsoluteFsPath = !isRemote && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
13939
+ const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
13940
+ if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
13941
+ return `<div class="doc-video doc-video-missing">
13942
+ <div class="doc-video-missing-label">Video unavailable</div>
13943
+ <div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
13944
+ ${captionHtml}
13945
+ </div>`;
13946
+ }
13947
+ const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
13948
+ return `<div class="doc-video">
13949
+ <video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
13950
+ ${captionHtml}
13951
+ </div>`;
13952
+ }
13749
13953
  function renderDocCustom(entry, deps) {
13750
13954
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
13751
13955
  const data = entry.data;
@@ -13799,6 +14003,9 @@ function renderDocEntry(entry, deps) {
13799
14003
  case "screenshot":
13800
14004
  html = renderDocScreenshot(entry, deps);
13801
14005
  break;
14006
+ case "video":
14007
+ html = renderDocVideo(entry, deps);
14008
+ break;
13802
14009
  case "custom":
13803
14010
  html = renderDocCustom(entry, deps);
13804
14011
  break;
@@ -15125,6 +15332,19 @@ var MarkdownFormatter = class {
15125
15332
  case "screenshot":
15126
15333
  lines.push(`${indent}![${entry.alt ?? "Screenshot"}](${entry.path})`);
15127
15334
  break;
15335
+ case "video": {
15336
+ const poster = entry.poster ? ` poster="${entry.poster}"` : "";
15337
+ lines.push(`${indent}`);
15338
+ lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
15339
+ lines.push(`${indent} <source src="${entry.path}" />`);
15340
+ lines.push(`${indent}</video>`);
15341
+ if (entry.caption) {
15342
+ lines.push(`${indent}`);
15343
+ lines.push(`${indent}*${entry.caption}*`);
15344
+ }
15345
+ lines.push(`${indent}`);
15346
+ break;
15347
+ }
15128
15348
  case "custom":
15129
15349
  if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
15130
15350
  const data = entry.data;
@@ -15837,8 +16057,8 @@ function extractDocAttachments(step) {
15837
16057
  }
15838
16058
  return attachments;
15839
16059
  }
15840
- function guessMediaType(path10) {
15841
- const lower = path10.toLowerCase();
16060
+ function guessMediaType(path11) {
16061
+ const lower = path11.toLowerCase();
15842
16062
  if (lower.endsWith(".png")) return "image/png";
15843
16063
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15844
16064
  if (lower.endsWith(".gif")) return "image/gif";
@@ -15979,11 +16199,11 @@ var CucumberHtmlFormatter = class {
15979
16199
  for (const envelope of envelopes) {
15980
16200
  const accepted = htmlStream.write(envelope);
15981
16201
  if (!accepted) {
15982
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
16202
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
15983
16203
  }
15984
16204
  }
15985
- await new Promise((resolve7, reject) => {
15986
- collector.on("finish", resolve7);
16205
+ await new Promise((resolve8, reject) => {
16206
+ collector.on("finish", resolve8);
15987
16207
  collector.on("error", reject);
15988
16208
  htmlStream.end();
15989
16209
  });
@@ -16284,6 +16504,8 @@ function formatDocEntry(doc) {
16284
16504
  return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
16285
16505
  case "screenshot":
16286
16506
  return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
16507
+ case "video":
16508
+ return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
16287
16509
  case "custom":
16288
16510
  return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
16289
16511
  }
@@ -16740,6 +16962,8 @@ function formatDocEntry2(doc) {
16740
16962
  return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
16741
16963
  case "screenshot":
16742
16964
  return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
16965
+ case "video":
16966
+ return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
16743
16967
  case "custom":
16744
16968
  return `${doc.type}: ${JSON.stringify(doc.data)}`;
16745
16969
  }
@@ -16985,19 +17209,35 @@ function replaceAssetRef(html, original, replacement) {
16985
17209
  return html;
16986
17210
  }
16987
17211
 
17212
+ // src/utils/source-file.ts
17213
+ function cleanTestStem(fileName) {
17214
+ const base = fileName.split(/[\\/]/).pop() ?? fileName;
17215
+ const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
17216
+ if (stripped !== base) return stripped;
17217
+ return base.replace(/\.[^.]+$/, "");
17218
+ }
17219
+ function humanizeSourceFile(fileName) {
17220
+ return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
17221
+ }
17222
+
16988
17223
  // src/formatters/astro.ts
16989
17224
  var AstroFormatter = class _AstroFormatter {
16990
17225
  markdownFormatter;
16991
17226
  title;
17227
+ perFileTitle;
16992
17228
  constructor(options = {}) {
16993
17229
  this.title = options.markdown?.title ?? "User Stories";
17230
+ this.perFileTitle = options.perFileTitle ?? false;
16994
17231
  this.markdownFormatter = new MarkdownFormatter({
16995
17232
  ...options.markdown,
16996
17233
  title: this.title,
16997
17234
  stepStyle: "gherkin",
16998
17235
  includeFrontMatter: false,
16999
17236
  includeSummaryTable: false,
17000
- includeMetadata: false
17237
+ includeMetadata: false,
17238
+ // A per-file page is one file already — group by suite/describe so the
17239
+ // body shows clean section headings, not the redundant source path.
17240
+ groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
17001
17241
  });
17002
17242
  }
17003
17243
  format(run) {
@@ -17007,13 +17247,31 @@ var AstroFormatter = class _AstroFormatter {
17007
17247
  return `${frontmatter}
17008
17248
  ${body}`;
17009
17249
  }
17250
+ /**
17251
+ * Title for the page. A per-file page (one source file — i.e. colocated mode)
17252
+ * is titled by its suite/describe name, falling back to a humanized filename,
17253
+ * so the docs nav reads "Convert Currency" not "User Stories" six times over.
17254
+ * Multi-file (aggregated) pages keep the configured title.
17255
+ */
17256
+ deriveTitle(run) {
17257
+ if (!this.perFileTitle) return this.title;
17258
+ const sourceFiles = new Set(
17259
+ run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
17260
+ );
17261
+ if (sourceFiles.size !== 1) return this.title;
17262
+ const suites = new Set(
17263
+ run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
17264
+ );
17265
+ if (suites.size === 1) return [...suites][0];
17266
+ return humanizeSourceFile([...sourceFiles][0]) || this.title;
17267
+ }
17010
17268
  buildFrontmatter(run) {
17011
17269
  const badge = _AstroFormatter.computeBadge(run.testCases);
17012
17270
  const count = run.testCases.length;
17013
17271
  const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
17014
17272
  const lines = [
17015
17273
  "---",
17016
- `title: ${this.title}`,
17274
+ `title: ${yamlScalar(this.deriveTitle(run))}`,
17017
17275
  `description: ${description}`,
17018
17276
  "sidebar:",
17019
17277
  " badge:",
@@ -17031,6 +17289,12 @@ ${body}`;
17031
17289
  return { text: "Passed", variant: "success" };
17032
17290
  }
17033
17291
  };
17292
+ function yamlScalar(value) {
17293
+ if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
17294
+ return `'${value.replace(/'/g, "''")}'`;
17295
+ }
17296
+ return value;
17297
+ }
17034
17298
 
17035
17299
  // src/formatters/confluence.ts
17036
17300
  var ConfluenceFormatter = class {
@@ -17307,6 +17571,15 @@ ${tc.errorStack}` : "");
17307
17571
  ])
17308
17572
  );
17309
17573
  break;
17574
+ case "video":
17575
+ content.push(
17576
+ paragraph([
17577
+ text(entry.caption ?? "Video", strong()),
17578
+ text(": "),
17579
+ link(entry.path, entry.path)
17580
+ ])
17581
+ );
17582
+ break;
17310
17583
  case "custom":
17311
17584
  content.push(paragraph([text(`[${entry.type}]`, strong())]));
17312
17585
  content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
@@ -17476,6 +17749,13 @@ function scanMarkdownAssets(markdown) {
17476
17749
  found.add(src);
17477
17750
  }
17478
17751
  }
17752
+ const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
17753
+ while ((match = posterRe.exec(stripped)) !== null) {
17754
+ const src = match[1].trim();
17755
+ if (isLocalPath(src)) {
17756
+ found.add(src);
17757
+ }
17758
+ }
17479
17759
  return Array.from(found);
17480
17760
  }
17481
17761
  function splitByCode(markdown) {
@@ -17526,6 +17806,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
17526
17806
  return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
17527
17807
  }
17528
17808
  );
17809
+ result = result.replace(
17810
+ /(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
17811
+ (full, pre, src, post) => {
17812
+ const trimmed = src.trim();
17813
+ if (!isLocalPath(trimmed)) return full;
17814
+ if (pathMap) {
17815
+ const mapped = pathMap.get(trimmed);
17816
+ if (mapped === void 0) return full;
17817
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
17818
+ }
17819
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
17820
+ }
17821
+ );
17529
17822
  return result;
17530
17823
  }
17531
17824
  function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
@@ -17972,6 +18265,184 @@ ${result.errors.join("\n")}`);
17972
18265
  }
17973
18266
  }
17974
18267
 
18268
+ // src/coverage-index.ts
18269
+ function normalizePath(path11) {
18270
+ return path11.replace(/^\.\//, "");
18271
+ }
18272
+ function scenariosCoveringPaths(index, paths) {
18273
+ const queries = paths.map(normalizePath);
18274
+ return index.scenarios.filter(
18275
+ (scenario) => scenario.covers.some(
18276
+ (glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
18277
+ )
18278
+ );
18279
+ }
18280
+
18281
+ // src/watch.ts
18282
+ import * as fs6 from "fs";
18283
+ import * as path7 from "path";
18284
+
18285
+ // src/converters/synthesize.ts
18286
+ var KEYWORD_MAP = {
18287
+ given: "Given",
18288
+ when: "When",
18289
+ then: "Then",
18290
+ and: "And",
18291
+ but: "But"
18292
+ };
18293
+ function normalizeKeyword(keyword) {
18294
+ return KEYWORD_MAP[keyword.toLowerCase()] ?? keyword;
18295
+ }
18296
+ function normalizeStepKeywords(steps) {
18297
+ return steps.map((step) => ({
18298
+ ...step,
18299
+ keyword: normalizeKeyword(step.keyword)
18300
+ }));
18301
+ }
18302
+ function deriveScenarioName(tc) {
18303
+ if (tc.title) return tc.title;
18304
+ if (tc.titlePath && tc.titlePath.length > 0) {
18305
+ return tc.titlePath[tc.titlePath.length - 1];
18306
+ }
18307
+ return "Untitled";
18308
+ }
18309
+ function synthesizeStories(raw) {
18310
+ return {
18311
+ ...raw,
18312
+ testCases: raw.testCases.map(synthesizeTestCase)
18313
+ };
18314
+ }
18315
+ function synthesizeTestCase(tc) {
18316
+ if (tc.story == null) {
18317
+ const scenario = deriveScenarioName(tc);
18318
+ return {
18319
+ ...tc,
18320
+ story: {
18321
+ scenario,
18322
+ steps: [{ keyword: "Then", text: scenario }]
18323
+ }
18324
+ };
18325
+ }
18326
+ const steps = tc.story.steps;
18327
+ if (!steps || steps.length === 0) {
18328
+ return {
18329
+ ...tc,
18330
+ story: {
18331
+ ...tc.story,
18332
+ steps: [{ keyword: "Then", text: tc.story.scenario }]
18333
+ }
18334
+ };
18335
+ }
18336
+ return {
18337
+ ...tc,
18338
+ story: {
18339
+ ...tc.story,
18340
+ steps: normalizeStepKeywords(steps)
18341
+ }
18342
+ };
18343
+ }
18344
+
18345
+ // src/watch.ts
18346
+ function toRun(data, inputType, synthesize) {
18347
+ if (inputType === "canonical") return data;
18348
+ let raw = data;
18349
+ if (synthesize) raw = synthesizeStories(raw);
18350
+ return canonicalizeRun(raw);
18351
+ }
18352
+ async function regenerateArtifacts(options, deps = {}) {
18353
+ const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18354
+ const data = JSON.parse(read(path7.resolve(options.input)));
18355
+ const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
18356
+ const generator = new ReportGenerator({
18357
+ formats: options.formats,
18358
+ outputDir: options.outputDir,
18359
+ outputName: options.outputName
18360
+ });
18361
+ const result = await generator.generate(run);
18362
+ return [...result.values()].flat();
18363
+ }
18364
+ function startWatch(options, deps = {}) {
18365
+ const log = deps.log ?? ((message) => console.log(message));
18366
+ const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
18367
+ const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
18368
+ const debounceMs = options.debounceMs ?? 150;
18369
+ let timer;
18370
+ let running = false;
18371
+ let pending = false;
18372
+ const run = async () => {
18373
+ if (running) {
18374
+ pending = true;
18375
+ return;
18376
+ }
18377
+ running = true;
18378
+ try {
18379
+ const files = await regenerate(options.input);
18380
+ log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
18381
+ } catch (error) {
18382
+ log(`Watch regeneration failed: ${error.message}`);
18383
+ } finally {
18384
+ running = false;
18385
+ if (pending) {
18386
+ pending = false;
18387
+ trigger();
18388
+ }
18389
+ }
18390
+ };
18391
+ const trigger = () => {
18392
+ if (timer) clearTimeout(timer);
18393
+ timer = setTimeout(() => void run(), debounceMs);
18394
+ };
18395
+ trigger();
18396
+ const watcher = watchFn(path7.resolve(options.input), trigger);
18397
+ return {
18398
+ close: () => {
18399
+ if (timer) clearTimeout(timer);
18400
+ watcher.close();
18401
+ }
18402
+ };
18403
+ }
18404
+
18405
+ // src/behavior-diff.ts
18406
+ function classifyStatusChange(baseline, current) {
18407
+ if (baseline === void 0) return "added";
18408
+ if (current === void 0) return "removed";
18409
+ if (baseline === current) return "unchanged";
18410
+ if (baseline === "passed" && current === "failed") return "regressed";
18411
+ if (baseline === "failed" && current === "passed") return "fixed";
18412
+ return "changed";
18413
+ }
18414
+ function scenarioMap(report) {
18415
+ const map = /* @__PURE__ */ new Map();
18416
+ for (const feature of report.features) {
18417
+ for (const scenario of feature.scenarios) {
18418
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
18419
+ }
18420
+ }
18421
+ return map;
18422
+ }
18423
+ function diffStoryReports(baseline, current) {
18424
+ const base = scenarioMap(baseline);
18425
+ const curr = scenarioMap(current);
18426
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
18427
+ const scenarios = ids.map((id) => {
18428
+ const b = base.get(id);
18429
+ const c = curr.get(id);
18430
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
18431
+ const meta = c ?? b;
18432
+ return {
18433
+ id,
18434
+ title: meta.scenario.title,
18435
+ sourceFile: meta.sourceFile,
18436
+ kind,
18437
+ baselineStatus: b?.scenario.status,
18438
+ currentStatus: c?.scenario.status
18439
+ };
18440
+ });
18441
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
18442
+ for (const s of scenarios) summary[s.kind] += 1;
18443
+ return { schemaVersion: "1.0", summary, scenarios };
18444
+ }
18445
+
17975
18446
  // src/publishers/confluence.ts
17976
18447
  function parseAdf(adf) {
17977
18448
  let parsed;
@@ -18567,27 +19038,27 @@ function pickleStepArgumentToDocs(ps) {
18567
19038
  }
18568
19039
 
18569
19040
  // src/utils/git-info.ts
18570
- import * as fs6 from "fs";
18571
- import * as path7 from "path";
19041
+ import * as fs7 from "fs";
19042
+ import * as path8 from "path";
18572
19043
  function readGitSha(cwd = process.cwd()) {
18573
19044
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
18574
19045
  if (envSha) return envSha;
18575
19046
  const gitDir = findGitDir(cwd);
18576
19047
  if (!gitDir) return void 0;
18577
19048
  try {
18578
- const headPath = path7.join(gitDir, "HEAD");
18579
- const head = fs6.readFileSync(headPath, "utf8").trim();
19049
+ const headPath = path8.join(gitDir, "HEAD");
19050
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18580
19051
  if (!head.startsWith("ref:")) {
18581
19052
  return head;
18582
19053
  }
18583
19054
  const refPath = head.replace("ref:", "").trim();
18584
- const refFile = path7.join(gitDir, refPath);
18585
- if (fs6.existsSync(refFile)) {
18586
- return fs6.readFileSync(refFile, "utf8").trim();
19055
+ const refFile = path8.join(gitDir, refPath);
19056
+ if (fs7.existsSync(refFile)) {
19057
+ return fs7.readFileSync(refFile, "utf8").trim();
18587
19058
  }
18588
- const packedRefs = path7.join(gitDir, "packed-refs");
18589
- if (fs6.existsSync(packedRefs)) {
18590
- const content = fs6.readFileSync(packedRefs, "utf8");
19059
+ const packedRefs = path8.join(gitDir, "packed-refs");
19060
+ if (fs7.existsSync(packedRefs)) {
19061
+ const content = fs7.readFileSync(packedRefs, "utf8");
18591
19062
  for (const line of content.split("\n")) {
18592
19063
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
18593
19064
  const [sha, ref] = line.split(" ");
@@ -18602,19 +19073,19 @@ function readGitSha(cwd = process.cwd()) {
18602
19073
  function findGitDir(start) {
18603
19074
  let current = start;
18604
19075
  while (true) {
18605
- const candidate = path7.join(current, ".git");
18606
- if (fs6.existsSync(candidate)) {
18607
- const stat = fs6.statSync(candidate);
19076
+ const candidate = path8.join(current, ".git");
19077
+ if (fs7.existsSync(candidate)) {
19078
+ const stat = fs7.statSync(candidate);
18608
19079
  if (stat.isFile()) {
18609
- const content = fs6.readFileSync(candidate, "utf8").trim();
19080
+ const content = fs7.readFileSync(candidate, "utf8").trim();
18610
19081
  const match = content.match(/^gitdir: (.+)$/);
18611
19082
  if (match) {
18612
- return path7.resolve(current, match[1]);
19083
+ return path8.resolve(current, match[1]);
18613
19084
  }
18614
19085
  }
18615
19086
  return candidate;
18616
19087
  }
18617
- const parent = path7.dirname(current);
19088
+ const parent = path8.dirname(current);
18618
19089
  if (parent === current) return void 0;
18619
19090
  current = parent;
18620
19091
  }
@@ -18625,8 +19096,8 @@ function readBranchName(cwd = process.cwd()) {
18625
19096
  const gitDir = findGitDir(cwd);
18626
19097
  if (!gitDir) return void 0;
18627
19098
  try {
18628
- const headPath = path7.join(gitDir, "HEAD");
18629
- const head = fs6.readFileSync(headPath, "utf8").trim();
19099
+ const headPath = path8.join(gitDir, "HEAD");
19100
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18630
19101
  if (head.startsWith("ref:")) {
18631
19102
  const refPath = head.replace("ref:", "").trim();
18632
19103
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -18663,8 +19134,8 @@ function nanosecondsToMs(ns) {
18663
19134
  }
18664
19135
 
18665
19136
  // src/utils/metadata.ts
18666
- import * as fs7 from "fs";
18667
- import * as path8 from "path";
19137
+ import * as fs8 from "fs";
19138
+ import * as path9 from "path";
18668
19139
  var versionCache = /* @__PURE__ */ new Map();
18669
19140
  function readPackageVersion(root) {
18670
19141
  if (versionCache.has(root)) {
@@ -18675,18 +19146,18 @@ function readPackageVersion(root) {
18675
19146
  return version;
18676
19147
  }
18677
19148
  function findPackageVersion(startDir) {
18678
- let current = path8.resolve(startDir);
19149
+ let current = path9.resolve(startDir);
18679
19150
  while (true) {
18680
- const pkgPath = path8.join(current, "package.json");
19151
+ const pkgPath = path9.join(current, "package.json");
18681
19152
  try {
18682
- if (fs7.existsSync(pkgPath)) {
18683
- const raw = fs7.readFileSync(pkgPath, "utf8");
19153
+ if (fs8.existsSync(pkgPath)) {
19154
+ const raw = fs8.readFileSync(pkgPath, "utf8");
18684
19155
  const parsed = JSON.parse(raw);
18685
19156
  return parsed.version;
18686
19157
  }
18687
19158
  } catch {
18688
19159
  }
18689
- const parent = path8.dirname(current);
19160
+ const parent = path9.dirname(current);
18690
19161
  if (parent === current) {
18691
19162
  return void 0;
18692
19163
  }
@@ -19556,12 +20027,22 @@ function listScenarios(args, _deps) {
19556
20027
  const { testCases, format } = args;
19557
20028
  if (format === "json") {
19558
20029
  const items = testCases.map((tc) => ({
20030
+ id: tc.id,
19559
20031
  scenario: tc.story.scenario,
19560
20032
  status: tc.status,
19561
20033
  sourceFile: tc.sourceFile,
19562
20034
  sourceLine: tc.sourceLine,
20035
+ suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
19563
20036
  tags: tc.tags,
19564
- id: tc.id
20037
+ tickets: tc.story.tickets ?? [],
20038
+ covers: tc.story.covers ?? [],
20039
+ durationMs: tc.durationMs,
20040
+ error: tc.errorMessage ? {
20041
+ message: tc.errorMessage,
20042
+ stack: tc.errorStack
20043
+ } : void 0,
20044
+ steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
20045
+ docKinds: collectDocKinds(tc)
19565
20046
  }));
19566
20047
  return JSON.stringify(items, null, 2);
19567
20048
  }
@@ -19634,6 +20115,34 @@ function listScenarios(args, _deps) {
19634
20115
  ];
19635
20116
  return lines.join("\n");
19636
20117
  }
20118
+ function toScenarioStep(step, index, testCase) {
20119
+ const result = testCase.stepResults.find(
20120
+ (candidate) => candidate.index === index || candidate.stepId === step.id
20121
+ );
20122
+ return {
20123
+ id: step.id,
20124
+ index,
20125
+ keyword: step.keyword,
20126
+ text: step.text,
20127
+ status: result?.status ?? testCase.status,
20128
+ durationMs: result?.durationMs ?? step.durationMs ?? 0,
20129
+ errorMessage: result?.errorMessage,
20130
+ mode: step.mode,
20131
+ docKinds: (step.docs ?? []).map((doc) => doc.kind)
20132
+ };
20133
+ }
20134
+ function collectDocKinds(testCase) {
20135
+ const kinds = /* @__PURE__ */ new Set();
20136
+ for (const doc of testCase.story.docs ?? []) {
20137
+ kinds.add(doc.kind);
20138
+ }
20139
+ for (const step of testCase.story.steps) {
20140
+ for (const doc of step.docs ?? []) {
20141
+ kinds.add(doc.kind);
20142
+ }
20143
+ }
20144
+ return [...kinds].sort();
20145
+ }
19637
20146
 
19638
20147
  // src/review/conventions.ts
19639
20148
  var CHANGE_TAG_PREFIX = "change:";
@@ -19681,18 +20190,18 @@ function deriveChangeType(tags) {
19681
20190
  }
19682
20191
  return "unknown";
19683
20192
  }
19684
- function extensionOf(path10) {
19685
- const base = path10.split("/").pop() ?? path10;
20193
+ function extensionOf(path11) {
20194
+ const base = path11.split("/").pop() ?? path11;
19686
20195
  const dot = base.lastIndexOf(".");
19687
20196
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
19688
20197
  }
19689
- function isTestFile(path10) {
19690
- return TEST_INFIX.test(path10);
20198
+ function isTestFile(path11) {
20199
+ return TEST_INFIX.test(path11);
19691
20200
  }
19692
- function isReviewableSource(path10) {
19693
- if (isTestFile(path10)) return false;
19694
- if (path10.endsWith(".d.ts")) return false;
19695
- return CODE_EXTENSIONS.has(extensionOf(path10));
20201
+ function isReviewableSource(path11) {
20202
+ if (isTestFile(path11)) return false;
20203
+ if (path11.endsWith(".d.ts")) return false;
20204
+ return CODE_EXTENSIONS.has(extensionOf(path11));
19696
20205
  }
19697
20206
  function testBaseKey(testFile) {
19698
20207
  return testFile.replace(TEST_INFIX, "");
@@ -19796,7 +20305,7 @@ function toClaim(testCase, changedSourcePaths) {
19796
20305
  const { strength, reasons } = gradeEvidence(testCase, audience);
19797
20306
  const key = testBaseKey(testCase.sourceFile);
19798
20307
  const coversFiles = changedSourcePaths.filter(
19799
- (path10) => sourceBaseKey(path10) === key
20308
+ (path11) => sourceBaseKey(path11) === key
19800
20309
  );
19801
20310
  return {
19802
20311
  id: testCase.id,
@@ -20329,6 +20838,7 @@ applyTheme(getEffectiveTheme());` : "";
20329
20838
  // src/index.ts
20330
20839
  var FORMAT_EXTENSIONS = {
20331
20840
  astro: ".md",
20841
+ "behavior-manifest-json": ".behavior-manifest.json",
20332
20842
  markdown: ".md",
20333
20843
  html: ".html",
20334
20844
  "cucumber-html": ".cucumber.html",
@@ -20336,8 +20846,13 @@ var FORMAT_EXTENSIONS = {
20336
20846
  "cucumber-json": ".cucumber.json",
20337
20847
  "cucumber-messages": ".ndjson",
20338
20848
  confluence: ".adf.json",
20849
+ "scenario-index-json": ".scenarios-index.json",
20339
20850
  "story-report-json": ".story-report.json"
20340
20851
  };
20852
+ function joinNameAndExt(name, ext) {
20853
+ const stutter = `.${name}.`;
20854
+ return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
20855
+ }
20341
20856
  var TEST_EXTENSIONS = [
20342
20857
  ".test.ts",
20343
20858
  ".test.tsx",
@@ -20363,11 +20878,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20363
20878
  const ext = FORMAT_EXTENSIONS[format];
20364
20879
  const effectiveName = outputName + (outputNameSuffix ?? "");
20365
20880
  if (mode === "aggregated") {
20366
- return toPosix(path9.join(baseOutputDir, `${effectiveName}${ext}`));
20881
+ return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
20367
20882
  }
20368
20883
  const normalizedSource = toPosix(sourceFile);
20369
- const dirOfSource = path9.posix.dirname(normalizedSource);
20370
- let baseName = path9.posix.basename(normalizedSource);
20884
+ const dirOfSource = path10.posix.dirname(normalizedSource);
20885
+ let baseName = path10.posix.basename(normalizedSource);
20371
20886
  for (const testExt of TEST_EXTENSIONS) {
20372
20887
  if (baseName.endsWith(testExt)) {
20373
20888
  baseName = baseName.slice(0, -testExt.length);
@@ -20376,9 +20891,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20376
20891
  }
20377
20892
  const fileName = `${baseName}.${effectiveName}${ext}`;
20378
20893
  if (colocatedStyle === "adjacent") {
20379
- return toPosix(path9.posix.join(dirOfSource, fileName));
20894
+ return toPosix(path10.posix.join(dirOfSource, fileName));
20380
20895
  }
20381
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
20896
+ if (colocatedStyle === "flat") {
20897
+ return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
20898
+ }
20899
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
20382
20900
  }
20383
20901
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
20384
20902
  const groups = /* @__PURE__ */ new Map();
@@ -20464,6 +20982,12 @@ var ReportGenerator = class {
20464
20982
  storyReportJson: {
20465
20983
  pretty: options.storyReportJson?.pretty ?? true
20466
20984
  },
20985
+ scenarioIndexJson: {
20986
+ pretty: options.scenarioIndexJson?.pretty ?? true
20987
+ },
20988
+ behaviorManifestJson: {
20989
+ pretty: options.behaviorManifestJson?.pretty ?? true
20990
+ },
20467
20991
  cucumberMessages: {
20468
20992
  uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
20469
20993
  includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
@@ -20579,8 +21103,8 @@ var ReportGenerator = class {
20579
21103
  if (astroPaths) {
20580
21104
  for (const mdPath of astroPaths) {
20581
21105
  const content = await fsPromises.readFile(mdPath, "utf8");
20582
- const mdDir = path9.dirname(mdPath);
20583
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
21106
+ const mdDir = path10.dirname(mdPath);
21107
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
20584
21108
  const result = copyMarkdownAssets({
20585
21109
  markdown: content,
20586
21110
  markdownDir: mdDir,
@@ -20611,9 +21135,9 @@ var ReportGenerator = class {
20611
21135
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
20612
21136
  const ext = FORMAT_EXTENSIONS[format];
20613
21137
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
20614
- const outputPath = toPosix(path9.join(this.options.outputDir, `${effectiveName}${ext}`));
21138
+ const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
20615
21139
  const content = await this.formatContent(run, format);
20616
- const dir = path9.dirname(outputPath);
21140
+ const dir = path10.dirname(outputPath);
20617
21141
  await fsPromises.mkdir(dir, { recursive: true });
20618
21142
  await this.deps.writeFile(outputPath, content);
20619
21143
  return [outputPath];
@@ -20625,7 +21149,7 @@ var ReportGenerator = class {
20625
21149
  testCases
20626
21150
  };
20627
21151
  const content = await this.formatContent(groupRun, format);
20628
- const dir = path9.dirname(outputPath);
21152
+ const dir = path10.dirname(outputPath);
20629
21153
  await fsPromises.mkdir(dir, { recursive: true });
20630
21154
  await this.deps.writeFile(outputPath, content);
20631
21155
  writtenPaths.push(outputPath);
@@ -20691,6 +21215,8 @@ var ReportGenerator = class {
20691
21215
  case "astro": {
20692
21216
  const formatter = new AstroFormatter({
20693
21217
  assetsBaseUrl: this.options.astro.assetsBaseUrl,
21218
+ // Colocated = one page per file, so title each by its own suite/file.
21219
+ perFileTitle: this.options.output.mode === "colocated",
20694
21220
  markdown: this.options.astro.markdown
20695
21221
  });
20696
21222
  return formatter.format(run);
@@ -20738,6 +21264,18 @@ var ReportGenerator = class {
20738
21264
  });
20739
21265
  return formatter.format(run);
20740
21266
  }
21267
+ case "scenario-index-json": {
21268
+ const formatter = new ScenarioIndexJsonFormatter({
21269
+ pretty: this.options.scenarioIndexJson.pretty
21270
+ });
21271
+ return formatter.format(run);
21272
+ }
21273
+ case "behavior-manifest-json": {
21274
+ const formatter = new BehaviorManifestJsonFormatter({
21275
+ pretty: this.options.behaviorManifestJson.pretty
21276
+ });
21277
+ return formatter.format(run);
21278
+ }
20741
21279
  default:
20742
21280
  throw new Error(`Unknown format: ${format}`);
20743
21281
  }
@@ -20754,7 +21292,7 @@ async function generateRunComparison(args) {
20754
21292
  await fsPromises.mkdir(outputDir, { recursive: true });
20755
21293
  for (const format of args.formats) {
20756
21294
  const ext = format === "html" ? ".html" : ".md";
20757
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
21295
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
20758
21296
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
20759
21297
  await fsPromises.writeFile(outputPath, content, "utf8");
20760
21298
  files.push(outputPath);
@@ -20775,6 +21313,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20775
21313
  }
20776
21314
  export {
20777
21315
  AstroFormatter,
21316
+ BehaviorManifestJsonFormatter,
20778
21317
  ConfluenceFormatter,
20779
21318
  CucumberHtmlFormatter,
20780
21319
  CucumberJsonFormatter,
@@ -20795,6 +21334,7 @@ export {
20795
21334
  STORY_META_KEY,
20796
21335
  STORY_REPORT_SCHEMA_MAJOR,
20797
21336
  STORY_REPORT_SCHEMA_VERSION,
21337
+ ScenarioIndexJsonFormatter,
20798
21338
  StoryReportJsonFormatter,
20799
21339
  adaptJestRun,
20800
21340
  adaptPlaywrightRun,
@@ -20805,6 +21345,7 @@ export {
20805
21345
  calculateFlakiness,
20806
21346
  calculateStability,
20807
21347
  canonicalizeRun,
21348
+ classifyStatusChange,
20808
21349
  clearVersionCache,
20809
21350
  computeTestMetrics,
20810
21351
  copyMarkdownAssets,
@@ -20816,6 +21357,7 @@ export {
20816
21357
  detectCI4 as detectCI,
20817
21358
  detectPerformanceTrend,
20818
21359
  diffRuns,
21360
+ diffStoryReports,
20819
21361
  findGitDir,
20820
21362
  formatDuration3 as formatDuration,
20821
21363
  generateRunComparison,
@@ -20827,6 +21369,7 @@ export {
20827
21369
  hasSufficientHistory,
20828
21370
  isReviewableSource,
20829
21371
  isTestFile,
21372
+ joinNameAndExt,
20830
21373
  listScenarios,
20831
21374
  loadHistory,
20832
21375
  mergeStepResults,
@@ -20843,21 +21386,26 @@ export {
20843
21386
  readBranchName,
20844
21387
  readGitSha,
20845
21388
  readPackageVersion,
21389
+ regenerateArtifacts,
20846
21390
  resolveAttachment,
20847
21391
  resolveAttachments,
20848
21392
  resolveTheme,
20849
21393
  resolveTraceUrl,
20850
21394
  rewriteAssetPaths,
20851
21395
  saveHistory,
21396
+ scenariosCoveringPaths,
20852
21397
  sendNotifications,
20853
21398
  sendSlackNotification,
20854
21399
  sendTeamsNotification,
20855
21400
  sendWebhookNotification,
20856
21401
  signBody,
20857
21402
  slugify,
21403
+ startWatch,
20858
21404
  stripAnsi,
21405
+ toBehaviorManifest,
20859
21406
  toCIInfo,
20860
21407
  toRawCIInfo,
21408
+ toScenarioIndex,
20861
21409
  toStoryReport,
20862
21410
  tryGetActiveOtelContext,
20863
21411
  updateHistory,