executable-stories-formatters 0.9.0 → 0.10.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,
@@ -99,29 +103,34 @@ __export(src_exports, {
99
103
  readBranchName: () => readBranchName,
100
104
  readGitSha: () => readGitSha,
101
105
  readPackageVersion: () => readPackageVersion,
106
+ regenerateArtifacts: () => regenerateArtifacts,
102
107
  resolveAttachment: () => resolveAttachment,
103
108
  resolveAttachments: () => resolveAttachments,
104
109
  resolveTheme: () => resolveTheme,
105
110
  resolveTraceUrl: () => resolveTraceUrl,
106
111
  rewriteAssetPaths: () => rewriteAssetPaths,
107
112
  saveHistory: () => saveHistory,
113
+ scenariosCoveringPaths: () => scenariosCoveringPaths,
108
114
  sendNotifications: () => sendNotifications,
109
115
  sendSlackNotification: () => sendSlackNotification,
110
116
  sendTeamsNotification: () => sendTeamsNotification,
111
117
  sendWebhookNotification: () => sendWebhookNotification,
112
118
  signBody: () => signBody,
113
119
  slugify: () => slugify,
120
+ startWatch: () => startWatch,
114
121
  stripAnsi: () => stripAnsi,
122
+ toBehaviorManifest: () => toBehaviorManifest,
115
123
  toCIInfo: () => toCIInfo,
116
124
  toRawCIInfo: () => toRawCIInfo,
125
+ toScenarioIndex: () => toScenarioIndex,
117
126
  toStoryReport: () => toStoryReport,
118
127
  tryGetActiveOtelContext: () => tryGetActiveOtelContext,
119
128
  updateHistory: () => updateHistory,
120
129
  validateCanonicalRun: () => validateCanonicalRun
121
130
  });
122
131
  module.exports = __toCommonJS(src_exports);
123
- var fs8 = require("fs");
124
- var path9 = __toESM(require("path"), 1);
132
+ var fs9 = require("fs");
133
+ var path10 = __toESM(require("path"), 1);
125
134
  var fsPromises = __toESM(require("fs/promises"), 1);
126
135
 
127
136
  // src/converters/acl/status.ts
@@ -967,6 +976,9 @@ function buildScenario(tc, featureId) {
967
976
  if (tickets && tickets.length > 0) {
968
977
  scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
969
978
  }
979
+ if (tc.story.covers && tc.story.covers.length > 0) {
980
+ scenario.covers = [...tc.story.covers];
981
+ }
970
982
  return scenario;
971
983
  }
972
984
  function deriveFeatureTitle(group, relSourceFile) {
@@ -1090,6 +1102,181 @@ var StoryReportJsonFormatter = class {
1090
1102
  }
1091
1103
  };
1092
1104
 
1105
+ // src/formatters/scenario-index-json.ts
1106
+ var ScenarioIndexJsonFormatter = class {
1107
+ options;
1108
+ constructor(options = {}) {
1109
+ this.options = {
1110
+ pretty: options.pretty ?? true,
1111
+ filters: options.filters
1112
+ };
1113
+ }
1114
+ toIndex(run) {
1115
+ return toScenarioIndex(toStoryReport(run), this.options.filters);
1116
+ }
1117
+ format(run) {
1118
+ const index = this.toIndex(run);
1119
+ return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
1120
+ }
1121
+ };
1122
+ function toScenarioIndex(report, filters = {}) {
1123
+ const scenarios = report.features.flatMap(
1124
+ (feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
1125
+ ).filter((scenario) => matchesFilters(scenario, filters));
1126
+ return {
1127
+ schemaVersion: "1.0",
1128
+ runId: report.runId,
1129
+ generatedAtMs: report.finishedAtMs,
1130
+ summary: summarize(scenarios),
1131
+ scenarios
1132
+ };
1133
+ }
1134
+ function toScenarioIndexItem(feature, scenario) {
1135
+ return {
1136
+ id: scenario.id,
1137
+ title: scenario.title,
1138
+ status: scenario.status,
1139
+ feature: feature.title,
1140
+ sourceFile: feature.sourceFile,
1141
+ sourceLine: scenario.sourceLine,
1142
+ tags: scenario.tags,
1143
+ tickets: scenario.tickets ?? [],
1144
+ covers: scenario.covers ?? [],
1145
+ durationMs: scenario.durationMs,
1146
+ steps: scenario.steps.map((step) => ({
1147
+ id: step.id,
1148
+ index: step.index,
1149
+ keyword: step.keyword,
1150
+ text: step.text,
1151
+ status: step.status,
1152
+ durationMs: step.durationMs,
1153
+ errorMessage: step.errorMessage,
1154
+ docKinds: step.docEntries.map((entry) => entry.kind)
1155
+ })),
1156
+ docKinds: scenario.docEntries.map((entry) => entry.kind),
1157
+ error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
1158
+ };
1159
+ }
1160
+ function matchesFilters(scenario, filters) {
1161
+ if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
1162
+ return false;
1163
+ }
1164
+ if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
1165
+ return false;
1166
+ }
1167
+ if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
1168
+ return false;
1169
+ }
1170
+ return true;
1171
+ }
1172
+ function summarize(scenarios) {
1173
+ return {
1174
+ total: scenarios.length,
1175
+ passed: scenarios.filter((scenario) => scenario.status === "passed").length,
1176
+ failed: scenarios.filter((scenario) => scenario.status === "failed").length,
1177
+ skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
1178
+ pending: scenarios.filter((scenario) => scenario.status === "pending").length,
1179
+ durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
1180
+ };
1181
+ }
1182
+
1183
+ // src/formatters/behavior-manifest-json.ts
1184
+ var BehaviorManifestJsonFormatter = class {
1185
+ pretty;
1186
+ constructor(options = {}) {
1187
+ this.pretty = options.pretty ?? true;
1188
+ }
1189
+ toManifest(run) {
1190
+ return toBehaviorManifest(toStoryReport(run));
1191
+ }
1192
+ format(run) {
1193
+ const manifest = this.toManifest(run);
1194
+ return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
1195
+ }
1196
+ };
1197
+ function toBehaviorManifest(report) {
1198
+ const index = toScenarioIndex(report);
1199
+ const bySource = /* @__PURE__ */ new Map();
1200
+ const byTag = /* @__PURE__ */ new Map();
1201
+ const docKinds = /* @__PURE__ */ new Set();
1202
+ const debuggerIssues = [];
1203
+ for (const scenario of index.scenarios) {
1204
+ const source = bySource.get(scenario.sourceFile) ?? {
1205
+ path: scenario.sourceFile,
1206
+ scenarioCount: 0,
1207
+ failed: 0,
1208
+ tags: []
1209
+ };
1210
+ source.scenarioCount += 1;
1211
+ if (scenario.status === "failed") source.failed += 1;
1212
+ source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
1213
+ bySource.set(scenario.sourceFile, source);
1214
+ for (const tag of scenario.tags) {
1215
+ const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
1216
+ tagEntry.scenarioCount += 1;
1217
+ byTag.set(tag, tagEntry);
1218
+ }
1219
+ for (const kind of scenario.docKinds) docKinds.add(kind);
1220
+ for (const step of scenario.steps) {
1221
+ for (const kind of step.docKinds) docKinds.add(kind);
1222
+ }
1223
+ if (!scenarioHasDocs(scenario)) {
1224
+ debuggerIssues.push({
1225
+ severity: "warning",
1226
+ code: "missing-docs",
1227
+ scenarioId: scenario.id,
1228
+ title: scenario.title,
1229
+ message: "Scenario has no doc entries."
1230
+ });
1231
+ }
1232
+ if (scenario.tags.length === 0) {
1233
+ debuggerIssues.push({
1234
+ severity: "warning",
1235
+ code: "missing-tags",
1236
+ scenarioId: scenario.id,
1237
+ title: scenario.title,
1238
+ message: "Scenario has no tags."
1239
+ });
1240
+ }
1241
+ if (scenario.covers.length === 0) {
1242
+ debuggerIssues.push({
1243
+ severity: "warning",
1244
+ code: "missing-covers",
1245
+ scenarioId: scenario.id,
1246
+ title: scenario.title,
1247
+ message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
1248
+ });
1249
+ }
1250
+ if (scenario.sourceLine === void 0) {
1251
+ debuggerIssues.push({
1252
+ severity: "warning",
1253
+ code: "missing-source-line",
1254
+ scenarioId: scenario.id,
1255
+ title: scenario.title,
1256
+ message: "Scenario has no source line."
1257
+ });
1258
+ }
1259
+ }
1260
+ const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
1261
+ return {
1262
+ schemaVersion: "1.0",
1263
+ runId: report.runId,
1264
+ generatedAtMs: report.finishedAtMs,
1265
+ summary: index.summary,
1266
+ sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
1267
+ tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
1268
+ docCoverage: {
1269
+ scenariosWithDocs,
1270
+ scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
1271
+ docKinds: [...docKinds].sort()
1272
+ },
1273
+ debugger: debuggerIssues
1274
+ };
1275
+ }
1276
+ function scenarioHasDocs(scenario) {
1277
+ return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
1278
+ }
1279
+
1093
1280
  // src/formatters/html/renderers/index.ts
1094
1281
  var fs2 = __toESM(require("fs"), 1);
1095
1282
  var path3 = __toESM(require("path"), 1);
@@ -15958,8 +16145,8 @@ function extractDocAttachments(step) {
15958
16145
  }
15959
16146
  return attachments;
15960
16147
  }
15961
- function guessMediaType(path10) {
15962
- const lower = path10.toLowerCase();
16148
+ function guessMediaType(path11) {
16149
+ const lower = path11.toLowerCase();
15963
16150
  if (lower.endsWith(".png")) return "image/png";
15964
16151
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15965
16152
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16100,11 +16287,11 @@ var CucumberHtmlFormatter = class {
16100
16287
  for (const envelope of envelopes) {
16101
16288
  const accepted = htmlStream.write(envelope);
16102
16289
  if (!accepted) {
16103
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
16290
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
16104
16291
  }
16105
16292
  }
16106
- await new Promise((resolve7, reject) => {
16107
- collector.on("finish", resolve7);
16293
+ await new Promise((resolve8, reject) => {
16294
+ collector.on("finish", resolve8);
16108
16295
  collector.on("error", reject);
16109
16296
  htmlStream.end();
16110
16297
  });
@@ -18093,6 +18280,184 @@ ${result.errors.join("\n")}`);
18093
18280
  }
18094
18281
  }
18095
18282
 
18283
+ // src/coverage-index.ts
18284
+ function normalizePath(path11) {
18285
+ return path11.replace(/^\.\//, "");
18286
+ }
18287
+ function scenariosCoveringPaths(index, paths) {
18288
+ const queries = paths.map(normalizePath);
18289
+ return index.scenarios.filter(
18290
+ (scenario) => scenario.covers.some(
18291
+ (glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
18292
+ )
18293
+ );
18294
+ }
18295
+
18296
+ // src/watch.ts
18297
+ var fs6 = __toESM(require("fs"), 1);
18298
+ var path7 = __toESM(require("path"), 1);
18299
+
18300
+ // src/converters/synthesize.ts
18301
+ var KEYWORD_MAP = {
18302
+ given: "Given",
18303
+ when: "When",
18304
+ then: "Then",
18305
+ and: "And",
18306
+ but: "But"
18307
+ };
18308
+ function normalizeKeyword(keyword) {
18309
+ return KEYWORD_MAP[keyword.toLowerCase()] ?? keyword;
18310
+ }
18311
+ function normalizeStepKeywords(steps) {
18312
+ return steps.map((step) => ({
18313
+ ...step,
18314
+ keyword: normalizeKeyword(step.keyword)
18315
+ }));
18316
+ }
18317
+ function deriveScenarioName(tc) {
18318
+ if (tc.title) return tc.title;
18319
+ if (tc.titlePath && tc.titlePath.length > 0) {
18320
+ return tc.titlePath[tc.titlePath.length - 1];
18321
+ }
18322
+ return "Untitled";
18323
+ }
18324
+ function synthesizeStories(raw) {
18325
+ return {
18326
+ ...raw,
18327
+ testCases: raw.testCases.map(synthesizeTestCase)
18328
+ };
18329
+ }
18330
+ function synthesizeTestCase(tc) {
18331
+ if (tc.story == null) {
18332
+ const scenario = deriveScenarioName(tc);
18333
+ return {
18334
+ ...tc,
18335
+ story: {
18336
+ scenario,
18337
+ steps: [{ keyword: "Then", text: scenario }]
18338
+ }
18339
+ };
18340
+ }
18341
+ const steps = tc.story.steps;
18342
+ if (!steps || steps.length === 0) {
18343
+ return {
18344
+ ...tc,
18345
+ story: {
18346
+ ...tc.story,
18347
+ steps: [{ keyword: "Then", text: tc.story.scenario }]
18348
+ }
18349
+ };
18350
+ }
18351
+ return {
18352
+ ...tc,
18353
+ story: {
18354
+ ...tc.story,
18355
+ steps: normalizeStepKeywords(steps)
18356
+ }
18357
+ };
18358
+ }
18359
+
18360
+ // src/watch.ts
18361
+ function toRun(data, inputType, synthesize) {
18362
+ if (inputType === "canonical") return data;
18363
+ let raw = data;
18364
+ if (synthesize) raw = synthesizeStories(raw);
18365
+ return canonicalizeRun(raw);
18366
+ }
18367
+ async function regenerateArtifacts(options, deps = {}) {
18368
+ const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
18369
+ const data = JSON.parse(read(path7.resolve(options.input)));
18370
+ const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
18371
+ const generator = new ReportGenerator({
18372
+ formats: options.formats,
18373
+ outputDir: options.outputDir,
18374
+ outputName: options.outputName
18375
+ });
18376
+ const result = await generator.generate(run);
18377
+ return [...result.values()].flat();
18378
+ }
18379
+ function startWatch(options, deps = {}) {
18380
+ const log = deps.log ?? ((message) => console.log(message));
18381
+ const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
18382
+ const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
18383
+ const debounceMs = options.debounceMs ?? 150;
18384
+ let timer;
18385
+ let running = false;
18386
+ let pending = false;
18387
+ const run = async () => {
18388
+ if (running) {
18389
+ pending = true;
18390
+ return;
18391
+ }
18392
+ running = true;
18393
+ try {
18394
+ const files = await regenerate(options.input);
18395
+ log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
18396
+ } catch (error) {
18397
+ log(`Watch regeneration failed: ${error.message}`);
18398
+ } finally {
18399
+ running = false;
18400
+ if (pending) {
18401
+ pending = false;
18402
+ trigger();
18403
+ }
18404
+ }
18405
+ };
18406
+ const trigger = () => {
18407
+ if (timer) clearTimeout(timer);
18408
+ timer = setTimeout(() => void run(), debounceMs);
18409
+ };
18410
+ trigger();
18411
+ const watcher = watchFn(path7.resolve(options.input), trigger);
18412
+ return {
18413
+ close: () => {
18414
+ if (timer) clearTimeout(timer);
18415
+ watcher.close();
18416
+ }
18417
+ };
18418
+ }
18419
+
18420
+ // src/behavior-diff.ts
18421
+ function classifyStatusChange(baseline, current) {
18422
+ if (baseline === void 0) return "added";
18423
+ if (current === void 0) return "removed";
18424
+ if (baseline === current) return "unchanged";
18425
+ if (baseline === "passed" && current === "failed") return "regressed";
18426
+ if (baseline === "failed" && current === "passed") return "fixed";
18427
+ return "changed";
18428
+ }
18429
+ function scenarioMap(report) {
18430
+ const map = /* @__PURE__ */ new Map();
18431
+ for (const feature of report.features) {
18432
+ for (const scenario of feature.scenarios) {
18433
+ map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
18434
+ }
18435
+ }
18436
+ return map;
18437
+ }
18438
+ function diffStoryReports(baseline, current) {
18439
+ const base = scenarioMap(baseline);
18440
+ const curr = scenarioMap(current);
18441
+ const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
18442
+ const scenarios = ids.map((id) => {
18443
+ const b = base.get(id);
18444
+ const c = curr.get(id);
18445
+ const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
18446
+ const meta = c ?? b;
18447
+ return {
18448
+ id,
18449
+ title: meta.scenario.title,
18450
+ sourceFile: meta.sourceFile,
18451
+ kind,
18452
+ baselineStatus: b?.scenario.status,
18453
+ currentStatus: c?.scenario.status
18454
+ };
18455
+ });
18456
+ const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
18457
+ for (const s of scenarios) summary[s.kind] += 1;
18458
+ return { schemaVersion: "1.0", summary, scenarios };
18459
+ }
18460
+
18096
18461
  // src/publishers/confluence.ts
18097
18462
  function parseAdf(adf) {
18098
18463
  let parsed;
@@ -18688,27 +19053,27 @@ function pickleStepArgumentToDocs(ps) {
18688
19053
  }
18689
19054
 
18690
19055
  // src/utils/git-info.ts
18691
- var fs6 = __toESM(require("fs"), 1);
18692
- var path7 = __toESM(require("path"), 1);
19056
+ var fs7 = __toESM(require("fs"), 1);
19057
+ var path8 = __toESM(require("path"), 1);
18693
19058
  function readGitSha(cwd = process.cwd()) {
18694
19059
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
18695
19060
  if (envSha) return envSha;
18696
19061
  const gitDir = findGitDir(cwd);
18697
19062
  if (!gitDir) return void 0;
18698
19063
  try {
18699
- const headPath = path7.join(gitDir, "HEAD");
18700
- const head = fs6.readFileSync(headPath, "utf8").trim();
19064
+ const headPath = path8.join(gitDir, "HEAD");
19065
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18701
19066
  if (!head.startsWith("ref:")) {
18702
19067
  return head;
18703
19068
  }
18704
19069
  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();
19070
+ const refFile = path8.join(gitDir, refPath);
19071
+ if (fs7.existsSync(refFile)) {
19072
+ return fs7.readFileSync(refFile, "utf8").trim();
18708
19073
  }
18709
- const packedRefs = path7.join(gitDir, "packed-refs");
18710
- if (fs6.existsSync(packedRefs)) {
18711
- const content = fs6.readFileSync(packedRefs, "utf8");
19074
+ const packedRefs = path8.join(gitDir, "packed-refs");
19075
+ if (fs7.existsSync(packedRefs)) {
19076
+ const content = fs7.readFileSync(packedRefs, "utf8");
18712
19077
  for (const line of content.split("\n")) {
18713
19078
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
18714
19079
  const [sha, ref] = line.split(" ");
@@ -18723,19 +19088,19 @@ function readGitSha(cwd = process.cwd()) {
18723
19088
  function findGitDir(start) {
18724
19089
  let current = start;
18725
19090
  while (true) {
18726
- const candidate = path7.join(current, ".git");
18727
- if (fs6.existsSync(candidate)) {
18728
- const stat = fs6.statSync(candidate);
19091
+ const candidate = path8.join(current, ".git");
19092
+ if (fs7.existsSync(candidate)) {
19093
+ const stat = fs7.statSync(candidate);
18729
19094
  if (stat.isFile()) {
18730
- const content = fs6.readFileSync(candidate, "utf8").trim();
19095
+ const content = fs7.readFileSync(candidate, "utf8").trim();
18731
19096
  const match = content.match(/^gitdir: (.+)$/);
18732
19097
  if (match) {
18733
- return path7.resolve(current, match[1]);
19098
+ return path8.resolve(current, match[1]);
18734
19099
  }
18735
19100
  }
18736
19101
  return candidate;
18737
19102
  }
18738
- const parent = path7.dirname(current);
19103
+ const parent = path8.dirname(current);
18739
19104
  if (parent === current) return void 0;
18740
19105
  current = parent;
18741
19106
  }
@@ -18746,8 +19111,8 @@ function readBranchName(cwd = process.cwd()) {
18746
19111
  const gitDir = findGitDir(cwd);
18747
19112
  if (!gitDir) return void 0;
18748
19113
  try {
18749
- const headPath = path7.join(gitDir, "HEAD");
18750
- const head = fs6.readFileSync(headPath, "utf8").trim();
19114
+ const headPath = path8.join(gitDir, "HEAD");
19115
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18751
19116
  if (head.startsWith("ref:")) {
18752
19117
  const refPath = head.replace("ref:", "").trim();
18753
19118
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -18784,8 +19149,8 @@ function nanosecondsToMs(ns) {
18784
19149
  }
18785
19150
 
18786
19151
  // src/utils/metadata.ts
18787
- var fs7 = __toESM(require("fs"), 1);
18788
- var path8 = __toESM(require("path"), 1);
19152
+ var fs8 = __toESM(require("fs"), 1);
19153
+ var path9 = __toESM(require("path"), 1);
18789
19154
  var versionCache = /* @__PURE__ */ new Map();
18790
19155
  function readPackageVersion(root) {
18791
19156
  if (versionCache.has(root)) {
@@ -18796,18 +19161,18 @@ function readPackageVersion(root) {
18796
19161
  return version;
18797
19162
  }
18798
19163
  function findPackageVersion(startDir) {
18799
- let current = path8.resolve(startDir);
19164
+ let current = path9.resolve(startDir);
18800
19165
  while (true) {
18801
- const pkgPath = path8.join(current, "package.json");
19166
+ const pkgPath = path9.join(current, "package.json");
18802
19167
  try {
18803
- if (fs7.existsSync(pkgPath)) {
18804
- const raw = fs7.readFileSync(pkgPath, "utf8");
19168
+ if (fs8.existsSync(pkgPath)) {
19169
+ const raw = fs8.readFileSync(pkgPath, "utf8");
18805
19170
  const parsed = JSON.parse(raw);
18806
19171
  return parsed.version;
18807
19172
  }
18808
19173
  } catch {
18809
19174
  }
18810
- const parent = path8.dirname(current);
19175
+ const parent = path9.dirname(current);
18811
19176
  if (parent === current) {
18812
19177
  return void 0;
18813
19178
  }
@@ -19678,12 +20043,22 @@ function listScenarios(args, _deps) {
19678
20043
  const { testCases, format } = args;
19679
20044
  if (format === "json") {
19680
20045
  const items = testCases.map((tc) => ({
20046
+ id: tc.id,
19681
20047
  scenario: tc.story.scenario,
19682
20048
  status: tc.status,
19683
20049
  sourceFile: tc.sourceFile,
19684
20050
  sourceLine: tc.sourceLine,
20051
+ suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
19685
20052
  tags: tc.tags,
19686
- id: tc.id
20053
+ tickets: tc.story.tickets ?? [],
20054
+ covers: tc.story.covers ?? [],
20055
+ durationMs: tc.durationMs,
20056
+ error: tc.errorMessage ? {
20057
+ message: tc.errorMessage,
20058
+ stack: tc.errorStack
20059
+ } : void 0,
20060
+ steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
20061
+ docKinds: collectDocKinds(tc)
19687
20062
  }));
19688
20063
  return JSON.stringify(items, null, 2);
19689
20064
  }
@@ -19756,6 +20131,34 @@ function listScenarios(args, _deps) {
19756
20131
  ];
19757
20132
  return lines.join("\n");
19758
20133
  }
20134
+ function toScenarioStep(step, index, testCase) {
20135
+ const result = testCase.stepResults.find(
20136
+ (candidate) => candidate.index === index || candidate.stepId === step.id
20137
+ );
20138
+ return {
20139
+ id: step.id,
20140
+ index,
20141
+ keyword: step.keyword,
20142
+ text: step.text,
20143
+ status: result?.status ?? testCase.status,
20144
+ durationMs: result?.durationMs ?? step.durationMs ?? 0,
20145
+ errorMessage: result?.errorMessage,
20146
+ mode: step.mode,
20147
+ docKinds: (step.docs ?? []).map((doc) => doc.kind)
20148
+ };
20149
+ }
20150
+ function collectDocKinds(testCase) {
20151
+ const kinds = /* @__PURE__ */ new Set();
20152
+ for (const doc of testCase.story.docs ?? []) {
20153
+ kinds.add(doc.kind);
20154
+ }
20155
+ for (const step of testCase.story.steps) {
20156
+ for (const doc of step.docs ?? []) {
20157
+ kinds.add(doc.kind);
20158
+ }
20159
+ }
20160
+ return [...kinds].sort();
20161
+ }
19759
20162
 
19760
20163
  // src/review/conventions.ts
19761
20164
  var CHANGE_TAG_PREFIX = "change:";
@@ -19803,18 +20206,18 @@ function deriveChangeType(tags) {
19803
20206
  }
19804
20207
  return "unknown";
19805
20208
  }
19806
- function extensionOf(path10) {
19807
- const base = path10.split("/").pop() ?? path10;
20209
+ function extensionOf(path11) {
20210
+ const base = path11.split("/").pop() ?? path11;
19808
20211
  const dot = base.lastIndexOf(".");
19809
20212
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
19810
20213
  }
19811
- function isTestFile(path10) {
19812
- return TEST_INFIX.test(path10);
20214
+ function isTestFile(path11) {
20215
+ return TEST_INFIX.test(path11);
19813
20216
  }
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));
20217
+ function isReviewableSource(path11) {
20218
+ if (isTestFile(path11)) return false;
20219
+ if (path11.endsWith(".d.ts")) return false;
20220
+ return CODE_EXTENSIONS.has(extensionOf(path11));
19818
20221
  }
19819
20222
  function testBaseKey(testFile) {
19820
20223
  return testFile.replace(TEST_INFIX, "");
@@ -19918,7 +20321,7 @@ function toClaim(testCase, changedSourcePaths) {
19918
20321
  const { strength, reasons } = gradeEvidence(testCase, audience);
19919
20322
  const key = testBaseKey(testCase.sourceFile);
19920
20323
  const coversFiles = changedSourcePaths.filter(
19921
- (path10) => sourceBaseKey(path10) === key
20324
+ (path11) => sourceBaseKey(path11) === key
19922
20325
  );
19923
20326
  return {
19924
20327
  id: testCase.id,
@@ -20451,6 +20854,7 @@ applyTheme(getEffectiveTheme());` : "";
20451
20854
  // src/index.ts
20452
20855
  var FORMAT_EXTENSIONS = {
20453
20856
  astro: ".md",
20857
+ "behavior-manifest-json": ".behavior-manifest.json",
20454
20858
  markdown: ".md",
20455
20859
  html: ".html",
20456
20860
  "cucumber-html": ".cucumber.html",
@@ -20458,6 +20862,7 @@ var FORMAT_EXTENSIONS = {
20458
20862
  "cucumber-json": ".cucumber.json",
20459
20863
  "cucumber-messages": ".ndjson",
20460
20864
  confluence: ".adf.json",
20865
+ "scenario-index-json": ".scenarios-index.json",
20461
20866
  "story-report-json": ".story-report.json"
20462
20867
  };
20463
20868
  var TEST_EXTENSIONS = [
@@ -20485,11 +20890,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20485
20890
  const ext = FORMAT_EXTENSIONS[format];
20486
20891
  const effectiveName = outputName + (outputNameSuffix ?? "");
20487
20892
  if (mode === "aggregated") {
20488
- return toPosix(path9.join(baseOutputDir, `${effectiveName}${ext}`));
20893
+ return toPosix(path10.join(baseOutputDir, `${effectiveName}${ext}`));
20489
20894
  }
20490
20895
  const normalizedSource = toPosix(sourceFile);
20491
- const dirOfSource = path9.posix.dirname(normalizedSource);
20492
- let baseName = path9.posix.basename(normalizedSource);
20896
+ const dirOfSource = path10.posix.dirname(normalizedSource);
20897
+ let baseName = path10.posix.basename(normalizedSource);
20493
20898
  for (const testExt of TEST_EXTENSIONS) {
20494
20899
  if (baseName.endsWith(testExt)) {
20495
20900
  baseName = baseName.slice(0, -testExt.length);
@@ -20498,9 +20903,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20498
20903
  }
20499
20904
  const fileName = `${baseName}.${effectiveName}${ext}`;
20500
20905
  if (colocatedStyle === "adjacent") {
20501
- return toPosix(path9.posix.join(dirOfSource, fileName));
20906
+ return toPosix(path10.posix.join(dirOfSource, fileName));
20502
20907
  }
20503
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
20908
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
20504
20909
  }
20505
20910
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
20506
20911
  const groups = /* @__PURE__ */ new Map();
@@ -20586,6 +20991,12 @@ var ReportGenerator = class {
20586
20991
  storyReportJson: {
20587
20992
  pretty: options.storyReportJson?.pretty ?? true
20588
20993
  },
20994
+ scenarioIndexJson: {
20995
+ pretty: options.scenarioIndexJson?.pretty ?? true
20996
+ },
20997
+ behaviorManifestJson: {
20998
+ pretty: options.behaviorManifestJson?.pretty ?? true
20999
+ },
20589
21000
  cucumberMessages: {
20590
21001
  uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
20591
21002
  includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
@@ -20701,8 +21112,8 @@ var ReportGenerator = class {
20701
21112
  if (astroPaths) {
20702
21113
  for (const mdPath of astroPaths) {
20703
21114
  const content = await fsPromises.readFile(mdPath, "utf8");
20704
- const mdDir = path9.dirname(mdPath);
20705
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
21115
+ const mdDir = path10.dirname(mdPath);
21116
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
20706
21117
  const result = copyMarkdownAssets({
20707
21118
  markdown: content,
20708
21119
  markdownDir: mdDir,
@@ -20733,9 +21144,9 @@ var ReportGenerator = class {
20733
21144
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
20734
21145
  const ext = FORMAT_EXTENSIONS[format];
20735
21146
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
20736
- const outputPath = toPosix(path9.join(this.options.outputDir, `${effectiveName}${ext}`));
21147
+ const outputPath = toPosix(path10.join(this.options.outputDir, `${effectiveName}${ext}`));
20737
21148
  const content = await this.formatContent(run, format);
20738
- const dir = path9.dirname(outputPath);
21149
+ const dir = path10.dirname(outputPath);
20739
21150
  await fsPromises.mkdir(dir, { recursive: true });
20740
21151
  await this.deps.writeFile(outputPath, content);
20741
21152
  return [outputPath];
@@ -20747,7 +21158,7 @@ var ReportGenerator = class {
20747
21158
  testCases
20748
21159
  };
20749
21160
  const content = await this.formatContent(groupRun, format);
20750
- const dir = path9.dirname(outputPath);
21161
+ const dir = path10.dirname(outputPath);
20751
21162
  await fsPromises.mkdir(dir, { recursive: true });
20752
21163
  await this.deps.writeFile(outputPath, content);
20753
21164
  writtenPaths.push(outputPath);
@@ -20860,6 +21271,18 @@ var ReportGenerator = class {
20860
21271
  });
20861
21272
  return formatter.format(run);
20862
21273
  }
21274
+ case "scenario-index-json": {
21275
+ const formatter = new ScenarioIndexJsonFormatter({
21276
+ pretty: this.options.scenarioIndexJson.pretty
21277
+ });
21278
+ return formatter.format(run);
21279
+ }
21280
+ case "behavior-manifest-json": {
21281
+ const formatter = new BehaviorManifestJsonFormatter({
21282
+ pretty: this.options.behaviorManifestJson.pretty
21283
+ });
21284
+ return formatter.format(run);
21285
+ }
20863
21286
  default:
20864
21287
  throw new Error(`Unknown format: ${format}`);
20865
21288
  }
@@ -20876,7 +21299,7 @@ async function generateRunComparison(args) {
20876
21299
  await fsPromises.mkdir(outputDir, { recursive: true });
20877
21300
  for (const format of args.formats) {
20878
21301
  const ext = format === "html" ? ".html" : ".md";
20879
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
21302
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
20880
21303
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
20881
21304
  await fsPromises.writeFile(outputPath, content, "utf8");
20882
21305
  files.push(outputPath);
@@ -20898,6 +21321,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20898
21321
  // Annotate the CommonJS export names for ESM import in node:
20899
21322
  0 && (module.exports = {
20900
21323
  AstroFormatter,
21324
+ BehaviorManifestJsonFormatter,
20901
21325
  ConfluenceFormatter,
20902
21326
  CucumberHtmlFormatter,
20903
21327
  CucumberJsonFormatter,
@@ -20918,6 +21342,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20918
21342
  STORY_META_KEY,
20919
21343
  STORY_REPORT_SCHEMA_MAJOR,
20920
21344
  STORY_REPORT_SCHEMA_VERSION,
21345
+ ScenarioIndexJsonFormatter,
20921
21346
  StoryReportJsonFormatter,
20922
21347
  adaptJestRun,
20923
21348
  adaptPlaywrightRun,
@@ -20928,6 +21353,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20928
21353
  calculateFlakiness,
20929
21354
  calculateStability,
20930
21355
  canonicalizeRun,
21356
+ classifyStatusChange,
20931
21357
  clearVersionCache,
20932
21358
  computeTestMetrics,
20933
21359
  copyMarkdownAssets,
@@ -20939,6 +21365,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20939
21365
  detectCI,
20940
21366
  detectPerformanceTrend,
20941
21367
  diffRuns,
21368
+ diffStoryReports,
20942
21369
  findGitDir,
20943
21370
  formatDuration,
20944
21371
  generateRunComparison,
@@ -20966,21 +21393,26 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20966
21393
  readBranchName,
20967
21394
  readGitSha,
20968
21395
  readPackageVersion,
21396
+ regenerateArtifacts,
20969
21397
  resolveAttachment,
20970
21398
  resolveAttachments,
20971
21399
  resolveTheme,
20972
21400
  resolveTraceUrl,
20973
21401
  rewriteAssetPaths,
20974
21402
  saveHistory,
21403
+ scenariosCoveringPaths,
20975
21404
  sendNotifications,
20976
21405
  sendSlackNotification,
20977
21406
  sendTeamsNotification,
20978
21407
  sendWebhookNotification,
20979
21408
  signBody,
20980
21409
  slugify,
21410
+ startWatch,
20981
21411
  stripAnsi,
21412
+ toBehaviorManifest,
20982
21413
  toCIInfo,
20983
21414
  toRawCIInfo,
21415
+ toScenarioIndex,
20984
21416
  toStoryReport,
20985
21417
  tryGetActiveOtelContext,
20986
21418
  updateHistory,