executable-stories-formatters 0.8.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,
@@ -44,29 +45,37 @@ __export(src_exports, {
44
45
  MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
45
46
  MarkdownFormatter: () => MarkdownFormatter,
46
47
  ReportGenerator: () => ReportGenerator,
48
+ ReviewHtmlFormatter: () => ReviewHtmlFormatter,
49
+ ReviewMarkdownFormatter: () => ReviewMarkdownFormatter,
47
50
  RunDiffHtmlFormatter: () => RunDiffHtmlFormatter,
48
51
  RunDiffMarkdownFormatter: () => RunDiffMarkdownFormatter,
49
52
  STORY_META_KEY: () => STORY_META_KEY,
50
53
  STORY_REPORT_SCHEMA_MAJOR: () => STORY_REPORT_SCHEMA_MAJOR,
51
54
  STORY_REPORT_SCHEMA_VERSION: () => STORY_REPORT_SCHEMA_VERSION,
55
+ ScenarioIndexJsonFormatter: () => ScenarioIndexJsonFormatter,
52
56
  StoryReportJsonFormatter: () => StoryReportJsonFormatter,
53
57
  adaptJestRun: () => adaptJestRun,
54
58
  adaptPlaywrightRun: () => adaptPlaywrightRun,
55
59
  adaptVitestRun: () => adaptVitestRun,
56
60
  assertValidRun: () => assertValidRun,
61
+ buildReview: () => buildReview,
57
62
  bundleAssets: () => bundleAssets,
58
63
  calculateFlakiness: () => calculateFlakiness,
59
64
  calculateStability: () => calculateStability,
60
65
  canonicalizeRun: () => canonicalizeRun,
66
+ classifyStatusChange: () => classifyStatusChange,
61
67
  clearVersionCache: () => clearVersionCache,
62
68
  computeTestMetrics: () => computeTestMetrics,
63
69
  copyMarkdownAssets: () => copyMarkdownAssets,
64
70
  createPrCommentSummary: () => createPrCommentSummary,
65
71
  createReportGenerator: () => createReportGenerator,
72
+ deriveAudience: () => deriveAudience,
73
+ deriveChangeType: () => deriveChangeType,
66
74
  deriveStepResults: () => deriveStepResults,
67
75
  detectCI: () => detectCI4,
68
76
  detectPerformanceTrend: () => detectPerformanceTrend,
69
77
  diffRuns: () => diffRuns,
78
+ diffStoryReports: () => diffStoryReports,
70
79
  findGitDir: () => findGitDir,
71
80
  formatDuration: () => formatDuration3,
72
81
  generateRunComparison: () => generateRunComparison,
@@ -74,7 +83,10 @@ __export(src_exports, {
74
83
  generateTestCaseId: () => generateTestCaseId,
75
84
  getAvailableThemes: () => getAvailableThemes,
76
85
  getCssOnlyThemes: () => getCssOnlyThemes,
86
+ gradeEvidence: () => gradeEvidence,
77
87
  hasSufficientHistory: () => hasSufficientHistory,
88
+ isReviewableSource: () => isReviewableSource,
89
+ isTestFile: () => isTestFile,
78
90
  listScenarios: () => listScenarios,
79
91
  loadHistory: () => loadHistory,
80
92
  mergeStepResults: () => mergeStepResults,
@@ -91,29 +103,34 @@ __export(src_exports, {
91
103
  readBranchName: () => readBranchName,
92
104
  readGitSha: () => readGitSha,
93
105
  readPackageVersion: () => readPackageVersion,
106
+ regenerateArtifacts: () => regenerateArtifacts,
94
107
  resolveAttachment: () => resolveAttachment,
95
108
  resolveAttachments: () => resolveAttachments,
96
109
  resolveTheme: () => resolveTheme,
97
110
  resolveTraceUrl: () => resolveTraceUrl,
98
111
  rewriteAssetPaths: () => rewriteAssetPaths,
99
112
  saveHistory: () => saveHistory,
113
+ scenariosCoveringPaths: () => scenariosCoveringPaths,
100
114
  sendNotifications: () => sendNotifications,
101
115
  sendSlackNotification: () => sendSlackNotification,
102
116
  sendTeamsNotification: () => sendTeamsNotification,
103
117
  sendWebhookNotification: () => sendWebhookNotification,
104
118
  signBody: () => signBody,
105
119
  slugify: () => slugify,
120
+ startWatch: () => startWatch,
106
121
  stripAnsi: () => stripAnsi,
122
+ toBehaviorManifest: () => toBehaviorManifest,
107
123
  toCIInfo: () => toCIInfo,
108
124
  toRawCIInfo: () => toRawCIInfo,
125
+ toScenarioIndex: () => toScenarioIndex,
109
126
  toStoryReport: () => toStoryReport,
110
127
  tryGetActiveOtelContext: () => tryGetActiveOtelContext,
111
128
  updateHistory: () => updateHistory,
112
129
  validateCanonicalRun: () => validateCanonicalRun
113
130
  });
114
131
  module.exports = __toCommonJS(src_exports);
115
- var fs8 = require("fs");
116
- var path9 = __toESM(require("path"), 1);
132
+ var fs9 = require("fs");
133
+ var path10 = __toESM(require("path"), 1);
117
134
  var fsPromises = __toESM(require("fs/promises"), 1);
118
135
 
119
136
  // src/converters/acl/status.ts
@@ -394,7 +411,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
394
411
  projectName: raw.projectName,
395
412
  retry: raw.retry ?? 0,
396
413
  retries: raw.retries ?? 0,
397
- tags
414
+ tags,
415
+ ...raw.evidence ? { evidence: raw.evidence } : {}
398
416
  };
399
417
  }
400
418
  function normalizeTags(story) {
@@ -958,6 +976,9 @@ function buildScenario(tc, featureId) {
958
976
  if (tickets && tickets.length > 0) {
959
977
  scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
960
978
  }
979
+ if (tc.story.covers && tc.story.covers.length > 0) {
980
+ scenario.covers = [...tc.story.covers];
981
+ }
961
982
  return scenario;
962
983
  }
963
984
  function deriveFeatureTitle(group, relSourceFile) {
@@ -1081,6 +1102,181 @@ var StoryReportJsonFormatter = class {
1081
1102
  }
1082
1103
  };
1083
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
+
1084
1280
  // src/formatters/html/renderers/index.ts
1085
1281
  var fs2 = __toESM(require("fs"), 1);
1086
1282
  var path3 = __toESM(require("path"), 1);
@@ -13927,7 +14123,7 @@ function renderDocEntry(entry, deps) {
13927
14123
  // src/formatters/html/renderers/steps.ts
13928
14124
  var CONTINUATION_KEYWORDS = ["And", "But", "*"];
13929
14125
  function renderStep(step, stepResult, index, deps) {
13930
- const statusIcon2 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
14126
+ const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
13931
14127
  const statusClass = stepResult ? `status-${stepResult.status}` : "";
13932
14128
  const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
13933
14129
  const keywordTrimmed = step.keyword.trim();
@@ -13936,7 +14132,7 @@ function renderStep(step, stepResult, index, deps) {
13936
14132
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
13937
14133
  const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
13938
14134
  return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
13939
- <span class="step-status ${statusClass}">${statusIcon2}</span>
14135
+ <span class="step-status ${statusClass}">${statusIcon4}</span>
13940
14136
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
13941
14137
  <span class="step-text">${textHtml}</span>
13942
14138
  <span class="step-duration">${duration}</span>
@@ -13986,16 +14182,16 @@ function hasSufficientHistory(entries, min) {
13986
14182
  }
13987
14183
 
13988
14184
  // src/formatters/html/renderers/scenario.ts
13989
- function renderTicket(ticket, template, escapeHtml3) {
14185
+ function renderTicket(ticket, template, escapeHtml4) {
13990
14186
  const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
13991
14187
  if (url) {
13992
- return `<a class="tag ticket-tag" href="${escapeHtml3(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml3(ticket.id)}</a>`;
14188
+ return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
13993
14189
  }
13994
- return `<span class="tag ticket-tag">${escapeHtml3(ticket.id)}</span>`;
14190
+ return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
13995
14191
  }
13996
14192
  function renderScenario(args, deps) {
13997
14193
  const { tc } = args;
13998
- const statusIcon2 = deps.getStatusIcon(tc.status);
14194
+ const statusIcon4 = deps.getStatusIcon(tc.status);
13999
14195
  const statusClass = `status-${tc.status}`;
14000
14196
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
14001
14197
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
@@ -14065,7 +14261,7 @@ function renderScenario(args, deps) {
14065
14261
  <div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
14066
14262
  <div class="scenario-info">
14067
14263
  <div class="scenario-title">
14068
- <span class="status-icon ${statusClass}">${statusIcon2}</span>
14264
+ <span class="status-icon ${statusClass}">${statusIcon4}</span>
14069
14265
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
14070
14266
  </div>
14071
14267
  <div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
@@ -14191,7 +14387,7 @@ function flattenTree(roots) {
14191
14387
  }
14192
14388
  return result;
14193
14389
  }
14194
- function buildTooltip(span, escapeHtml3) {
14390
+ function buildTooltip(span, escapeHtml4) {
14195
14391
  const parts = [];
14196
14392
  parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
14197
14393
  if (span.statusMessage) {
@@ -14209,7 +14405,7 @@ function buildTooltip(span, escapeHtml3) {
14209
14405
  if (text2.length > TOOLTIP_MAX_LENGTH) {
14210
14406
  text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
14211
14407
  }
14212
- return escapeHtml3(text2);
14408
+ return escapeHtml4(text2);
14213
14409
  }
14214
14410
  function renderTraceView(args, deps) {
14215
14411
  if (!args.spans || args.spans.length === 0) return "";
@@ -14432,11 +14628,11 @@ function renderToc(args, deps) {
14432
14628
  const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
14433
14629
  const featureSlug = `feature-${slugify(file)}`;
14434
14630
  const scenarios = testCases.map((tc) => {
14435
- const statusIcon2 = deps.getStatusIcon(tc.status);
14631
+ const statusIcon4 = deps.getStatusIcon(tc.status);
14436
14632
  const statusClass = `status-${tc.status}`;
14437
14633
  const failedClass = tc.status === "failed" ? " toc-failed" : "";
14438
14634
  return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
14439
- <span class="toc-status ${statusClass}">${statusIcon2}</span>
14635
+ <span class="toc-status ${statusClass}">${statusIcon4}</span>
14440
14636
  ${deps.escapeHtml(tc.story.scenario)}
14441
14637
  </a>`;
14442
14638
  }).join("\n");
@@ -15949,8 +16145,8 @@ function extractDocAttachments(step) {
15949
16145
  }
15950
16146
  return attachments;
15951
16147
  }
15952
- function guessMediaType(path10) {
15953
- const lower = path10.toLowerCase();
16148
+ function guessMediaType(path11) {
16149
+ const lower = path11.toLowerCase();
15954
16150
  if (lower.endsWith(".png")) return "image/png";
15955
16151
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15956
16152
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16091,11 +16287,11 @@ var CucumberHtmlFormatter = class {
16091
16287
  for (const envelope of envelopes) {
16092
16288
  const accepted = htmlStream.write(envelope);
16093
16289
  if (!accepted) {
16094
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
16290
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
16095
16291
  }
16096
16292
  }
16097
- await new Promise((resolve7, reject) => {
16098
- collector.on("finish", resolve7);
16293
+ await new Promise((resolve8, reject) => {
16294
+ collector.on("finish", resolve8);
16099
16295
  collector.on("error", reject);
16100
16296
  htmlStream.end();
16101
16297
  });
@@ -18084,6 +18280,184 @@ ${result.errors.join("\n")}`);
18084
18280
  }
18085
18281
  }
18086
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
+
18087
18461
  // src/publishers/confluence.ts
18088
18462
  function parseAdf(adf) {
18089
18463
  let parsed;
@@ -18679,27 +19053,27 @@ function pickleStepArgumentToDocs(ps) {
18679
19053
  }
18680
19054
 
18681
19055
  // src/utils/git-info.ts
18682
- var fs6 = __toESM(require("fs"), 1);
18683
- var path7 = __toESM(require("path"), 1);
19056
+ var fs7 = __toESM(require("fs"), 1);
19057
+ var path8 = __toESM(require("path"), 1);
18684
19058
  function readGitSha(cwd = process.cwd()) {
18685
19059
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
18686
19060
  if (envSha) return envSha;
18687
19061
  const gitDir = findGitDir(cwd);
18688
19062
  if (!gitDir) return void 0;
18689
19063
  try {
18690
- const headPath = path7.join(gitDir, "HEAD");
18691
- const head = fs6.readFileSync(headPath, "utf8").trim();
19064
+ const headPath = path8.join(gitDir, "HEAD");
19065
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18692
19066
  if (!head.startsWith("ref:")) {
18693
19067
  return head;
18694
19068
  }
18695
19069
  const refPath = head.replace("ref:", "").trim();
18696
- const refFile = path7.join(gitDir, refPath);
18697
- if (fs6.existsSync(refFile)) {
18698
- 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();
18699
19073
  }
18700
- const packedRefs = path7.join(gitDir, "packed-refs");
18701
- if (fs6.existsSync(packedRefs)) {
18702
- 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");
18703
19077
  for (const line of content.split("\n")) {
18704
19078
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
18705
19079
  const [sha, ref] = line.split(" ");
@@ -18714,19 +19088,19 @@ function readGitSha(cwd = process.cwd()) {
18714
19088
  function findGitDir(start) {
18715
19089
  let current = start;
18716
19090
  while (true) {
18717
- const candidate = path7.join(current, ".git");
18718
- if (fs6.existsSync(candidate)) {
18719
- const stat = fs6.statSync(candidate);
19091
+ const candidate = path8.join(current, ".git");
19092
+ if (fs7.existsSync(candidate)) {
19093
+ const stat = fs7.statSync(candidate);
18720
19094
  if (stat.isFile()) {
18721
- const content = fs6.readFileSync(candidate, "utf8").trim();
19095
+ const content = fs7.readFileSync(candidate, "utf8").trim();
18722
19096
  const match = content.match(/^gitdir: (.+)$/);
18723
19097
  if (match) {
18724
- return path7.resolve(current, match[1]);
19098
+ return path8.resolve(current, match[1]);
18725
19099
  }
18726
19100
  }
18727
19101
  return candidate;
18728
19102
  }
18729
- const parent = path7.dirname(current);
19103
+ const parent = path8.dirname(current);
18730
19104
  if (parent === current) return void 0;
18731
19105
  current = parent;
18732
19106
  }
@@ -18737,8 +19111,8 @@ function readBranchName(cwd = process.cwd()) {
18737
19111
  const gitDir = findGitDir(cwd);
18738
19112
  if (!gitDir) return void 0;
18739
19113
  try {
18740
- const headPath = path7.join(gitDir, "HEAD");
18741
- const head = fs6.readFileSync(headPath, "utf8").trim();
19114
+ const headPath = path8.join(gitDir, "HEAD");
19115
+ const head = fs7.readFileSync(headPath, "utf8").trim();
18742
19116
  if (head.startsWith("ref:")) {
18743
19117
  const refPath = head.replace("ref:", "").trim();
18744
19118
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -18775,8 +19149,8 @@ function nanosecondsToMs(ns) {
18775
19149
  }
18776
19150
 
18777
19151
  // src/utils/metadata.ts
18778
- var fs7 = __toESM(require("fs"), 1);
18779
- var path8 = __toESM(require("path"), 1);
19152
+ var fs8 = __toESM(require("fs"), 1);
19153
+ var path9 = __toESM(require("path"), 1);
18780
19154
  var versionCache = /* @__PURE__ */ new Map();
18781
19155
  function readPackageVersion(root) {
18782
19156
  if (versionCache.has(root)) {
@@ -18787,18 +19161,18 @@ function readPackageVersion(root) {
18787
19161
  return version;
18788
19162
  }
18789
19163
  function findPackageVersion(startDir) {
18790
- let current = path8.resolve(startDir);
19164
+ let current = path9.resolve(startDir);
18791
19165
  while (true) {
18792
- const pkgPath = path8.join(current, "package.json");
19166
+ const pkgPath = path9.join(current, "package.json");
18793
19167
  try {
18794
- if (fs7.existsSync(pkgPath)) {
18795
- const raw = fs7.readFileSync(pkgPath, "utf8");
19168
+ if (fs8.existsSync(pkgPath)) {
19169
+ const raw = fs8.readFileSync(pkgPath, "utf8");
18796
19170
  const parsed = JSON.parse(raw);
18797
19171
  return parsed.version;
18798
19172
  }
18799
19173
  } catch {
18800
19174
  }
18801
- const parent = path8.dirname(current);
19175
+ const parent = path9.dirname(current);
18802
19176
  if (parent === current) {
18803
19177
  return void 0;
18804
19178
  }
@@ -19669,12 +20043,22 @@ function listScenarios(args, _deps) {
19669
20043
  const { testCases, format } = args;
19670
20044
  if (format === "json") {
19671
20045
  const items = testCases.map((tc) => ({
20046
+ id: tc.id,
19672
20047
  scenario: tc.story.scenario,
19673
20048
  status: tc.status,
19674
20049
  sourceFile: tc.sourceFile,
19675
20050
  sourceLine: tc.sourceLine,
20051
+ suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
19676
20052
  tags: tc.tags,
19677
- 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)
19678
20062
  }));
19679
20063
  return JSON.stringify(items, null, 2);
19680
20064
  }
@@ -19747,10 +20131,730 @@ function listScenarios(args, _deps) {
19747
20131
  ];
19748
20132
  return lines.join("\n");
19749
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
+ }
20162
+
20163
+ // src/review/conventions.ts
20164
+ var CHANGE_TAG_PREFIX = "change:";
20165
+ var AUDIENCE_TAG_PREFIX = "audience:";
20166
+ var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
20167
+ "feature",
20168
+ "bugfix",
20169
+ "refactor",
20170
+ "perf",
20171
+ "deps"
20172
+ ]);
20173
+ var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
20174
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
20175
+ "ts",
20176
+ "tsx",
20177
+ "js",
20178
+ "jsx",
20179
+ "mjs",
20180
+ "cjs",
20181
+ "py",
20182
+ "go",
20183
+ "rs",
20184
+ "kt",
20185
+ "kts",
20186
+ "java",
20187
+ "cs",
20188
+ "rb"
20189
+ ]);
20190
+ var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
20191
+ function deriveAudience(sourceFile, tags) {
20192
+ const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
20193
+ if (override) {
20194
+ const value = override.slice(AUDIENCE_TAG_PREFIX.length);
20195
+ if (value === "stakeholder" || value === "engineer") return value;
20196
+ }
20197
+ return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
20198
+ }
20199
+ function deriveChangeType(tags) {
20200
+ for (const tag of tags) {
20201
+ const lower = tag.toLowerCase();
20202
+ if (lower.startsWith(CHANGE_TAG_PREFIX)) {
20203
+ const value = lower.slice(CHANGE_TAG_PREFIX.length);
20204
+ if (VALID_CHANGE_TYPES.has(value)) return value;
20205
+ }
20206
+ }
20207
+ return "unknown";
20208
+ }
20209
+ function extensionOf(path11) {
20210
+ const base = path11.split("/").pop() ?? path11;
20211
+ const dot = base.lastIndexOf(".");
20212
+ return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20213
+ }
20214
+ function isTestFile(path11) {
20215
+ return TEST_INFIX.test(path11);
20216
+ }
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));
20221
+ }
20222
+ function testBaseKey(testFile) {
20223
+ return testFile.replace(TEST_INFIX, "");
20224
+ }
20225
+ function sourceBaseKey(sourceFile) {
20226
+ const dot = sourceFile.lastIndexOf(".");
20227
+ const slash = sourceFile.lastIndexOf("/");
20228
+ return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
20229
+ }
20230
+
20231
+ // src/review/build-review.ts
20232
+ var STRENGTH_RANK = {
20233
+ none: 0,
20234
+ weak: 1,
20235
+ moderate: 2,
20236
+ strong: 3
20237
+ };
20238
+ var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
20239
+ function findDoc(docs, predicate) {
20240
+ if (!docs) return void 0;
20241
+ for (const doc of docs) {
20242
+ if (predicate(doc)) return doc;
20243
+ const nested = findDoc(doc.children, predicate);
20244
+ if (nested) return nested;
20245
+ }
20246
+ return void 0;
20247
+ }
20248
+ function anyDoc(docs, predicate) {
20249
+ return findDoc(docs, predicate) !== void 0;
20250
+ }
20251
+ function extractIntent(testCase) {
20252
+ const docs = testCase.story.docs;
20253
+ const section = findDoc(
20254
+ docs,
20255
+ (d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
20256
+ );
20257
+ if (section && section.kind === "section") return section.markdown;
20258
+ const note = findDoc(docs, (d) => d.kind === "note");
20259
+ if (note && note.kind === "note") return note.text;
20260
+ return void 0;
20261
+ }
20262
+ function hasScreenshot(testCase) {
20263
+ if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
20264
+ return true;
20265
+ }
20266
+ if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
20267
+ return testCase.story.steps.some(
20268
+ (step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
20269
+ );
20270
+ }
20271
+ function hasOtelTrace(testCase) {
20272
+ return (testCase.story.otelSpans?.length ?? 0) > 0;
20273
+ }
20274
+ function gradeEvidence(testCase, audience) {
20275
+ if (testCase.status !== "passed") {
20276
+ return {
20277
+ strength: "none",
20278
+ reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
20279
+ };
20280
+ }
20281
+ const ev = testCase.evidence;
20282
+ const screenshot = hasScreenshot(testCase);
20283
+ const otel = hasOtelTrace(testCase);
20284
+ const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
20285
+ const mutation = ev?.mutationScorePct;
20286
+ const changedCov = ev?.changedLineCoveragePct;
20287
+ const strong2 = [];
20288
+ if (ev?.failingFirstVerified) {
20289
+ strong2.push("failing-first verified (red on base ref, green on head)");
20290
+ }
20291
+ if (typeof mutation === "number" && mutation >= 80) {
20292
+ strong2.push(`mutation score ${mutation}% (\u226580%)`);
20293
+ }
20294
+ if (screenshot && otel) {
20295
+ strong2.push("backed by screenshot + OTEL trace");
20296
+ } else if (audience === "stakeholder" && (screenshot || otel)) {
20297
+ strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
20298
+ }
20299
+ if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
20300
+ const moderate = [];
20301
+ if (screenshot) moderate.push("screenshot attached");
20302
+ if (otel) moderate.push("OTEL trace attached");
20303
+ if (typeof mutation === "number" && mutation >= 50) {
20304
+ moderate.push(`mutation score ${mutation}%`);
20305
+ }
20306
+ if (typeof changedCov === "number" && changedCov >= 80) {
20307
+ moderate.push(`changed-line coverage ${changedCov}%`);
20308
+ }
20309
+ if (isIntegration) moderate.push("integration-level test");
20310
+ if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
20311
+ return {
20312
+ strength: "weak",
20313
+ reasons: [
20314
+ "passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
20315
+ ]
20316
+ };
20317
+ }
20318
+ function toClaim(testCase, changedSourcePaths) {
20319
+ const audience = deriveAudience(testCase.sourceFile, testCase.tags);
20320
+ const changeType = deriveChangeType(testCase.tags);
20321
+ const { strength, reasons } = gradeEvidence(testCase, audience);
20322
+ const key = testBaseKey(testCase.sourceFile);
20323
+ const coversFiles = changedSourcePaths.filter(
20324
+ (path11) => sourceBaseKey(path11) === key
20325
+ );
20326
+ return {
20327
+ id: testCase.id,
20328
+ scenario: testCase.story.scenario,
20329
+ sourceFile: testCase.sourceFile,
20330
+ sourceLine: testCase.sourceLine,
20331
+ status: testCase.status,
20332
+ audience,
20333
+ changeType,
20334
+ strength,
20335
+ strengthReasons: reasons,
20336
+ intent: extractIntent(testCase),
20337
+ coversFiles,
20338
+ testCase
20339
+ };
20340
+ }
20341
+ function bandFor(claims) {
20342
+ if (claims.length === 0) return "uncovered";
20343
+ const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
20344
+ return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
20345
+ }
20346
+ var AUDIENCE_ORDER = {
20347
+ stakeholder: 0,
20348
+ engineer: 1
20349
+ };
20350
+ function buildReview(run, context = { changedFiles: [] }) {
20351
+ const changedSource = context.changedFiles.filter(
20352
+ (f) => isReviewableSource(f.path)
20353
+ );
20354
+ const changedSourcePaths = changedSource.map((f) => f.path);
20355
+ const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
20356
+ const changedFiles = changedSource.map((file) => {
20357
+ const covering = claims.filter((c) => c.coversFiles.includes(file.path));
20358
+ return {
20359
+ path: file.path,
20360
+ changeKind: file.changeKind,
20361
+ band: bandFor(covering),
20362
+ claims: covering.map((c) => ({
20363
+ id: c.id,
20364
+ scenario: c.scenario,
20365
+ strength: c.strength
20366
+ }))
20367
+ };
20368
+ });
20369
+ const sortedClaims = [...claims].sort((a, b) => {
20370
+ if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
20371
+ return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
20372
+ }
20373
+ if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
20374
+ return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
20375
+ }
20376
+ if (a.sourceFile !== b.sourceFile) {
20377
+ return a.sourceFile.localeCompare(b.sourceFile);
20378
+ }
20379
+ return a.scenario.localeCompare(b.scenario);
20380
+ });
20381
+ const bandRank = { uncovered: 0, weak: 1, covered: 2 };
20382
+ const sortedFiles = [...changedFiles].sort((a, b) => {
20383
+ if (bandRank[a.band] !== bandRank[b.band]) {
20384
+ return bandRank[a.band] - bandRank[b.band];
20385
+ }
20386
+ return a.path.localeCompare(b.path);
20387
+ });
20388
+ const summary = buildSummary2(sortedClaims, sortedFiles);
20389
+ return {
20390
+ run,
20391
+ context,
20392
+ summary,
20393
+ claims: sortedClaims,
20394
+ changedFiles: sortedFiles
20395
+ };
20396
+ }
20397
+ function buildSummary2(claims, changedFiles) {
20398
+ const byAudience = {
20399
+ stakeholder: 0,
20400
+ engineer: 0
20401
+ };
20402
+ const byStrength = {
20403
+ none: 0,
20404
+ weak: 0,
20405
+ moderate: 0,
20406
+ strong: 0
20407
+ };
20408
+ for (const claim of claims) {
20409
+ byAudience[claim.audience] += 1;
20410
+ byStrength[claim.strength] += 1;
20411
+ }
20412
+ return {
20413
+ totalClaims: claims.length,
20414
+ byAudience,
20415
+ byStrength,
20416
+ changedSourceFiles: changedFiles.length,
20417
+ uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
20418
+ weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
20419
+ covered: changedFiles.filter((f) => f.band === "covered").length
20420
+ };
20421
+ }
20422
+
20423
+ // src/formatters/review-markdown.ts
20424
+ var STRENGTH_BADGE = {
20425
+ strong: "\u{1F7E2} strong",
20426
+ moderate: "\u{1F7E1} moderate",
20427
+ weak: "\u{1F7E0} weak",
20428
+ none: "\u{1F534} none"
20429
+ };
20430
+ function statusIcon2(status) {
20431
+ switch (status) {
20432
+ case "passed":
20433
+ return "\u2705";
20434
+ case "failed":
20435
+ return "\u274C";
20436
+ case "skipped":
20437
+ return "\u2298";
20438
+ default:
20439
+ return "\u2022";
20440
+ }
20441
+ }
20442
+ function escapeCell2(value) {
20443
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
20444
+ }
20445
+ function intentSummary(intent) {
20446
+ const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
20447
+ const trimmed = firstLine.trim();
20448
+ return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
20449
+ }
20450
+ function renderTicket2(ticket) {
20451
+ return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
20452
+ }
20453
+ function renderUncoveredBand(lines, files) {
20454
+ const uncovered = files.filter((f) => f.band === "uncovered");
20455
+ if (uncovered.length === 0) return;
20456
+ lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
20457
+ lines.push("");
20458
+ lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
20459
+ lines.push("");
20460
+ for (const file of uncovered) {
20461
+ lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
20462
+ }
20463
+ lines.push("");
20464
+ }
20465
+ function renderWeakBand(lines, files) {
20466
+ const weak = files.filter((f) => f.band === "weak");
20467
+ if (weak.length === 0) return;
20468
+ lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
20469
+ lines.push("");
20470
+ for (const file of weak) {
20471
+ const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
20472
+ lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
20473
+ }
20474
+ lines.push("");
20475
+ }
20476
+ function renderClaim(lines, claim) {
20477
+ lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
20478
+ lines.push("");
20479
+ lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
20480
+ if (claim.changeType !== "unknown") {
20481
+ lines.push(`- Change: \`${claim.changeType}\``);
20482
+ }
20483
+ const tickets = claim.testCase.story.tickets ?? [];
20484
+ if (tickets.length > 0) {
20485
+ lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
20486
+ }
20487
+ lines.push(
20488
+ `- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
20489
+ );
20490
+ if (claim.coversFiles.length > 0) {
20491
+ lines.push(
20492
+ `- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
20493
+ );
20494
+ }
20495
+ if (claim.intent) {
20496
+ lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
20497
+ }
20498
+ lines.push("");
20499
+ }
20500
+ function renderAudienceSection(lines, title, claims) {
20501
+ if (claims.length === 0) return;
20502
+ lines.push(`## ${title} (${claims.length})`);
20503
+ lines.push("");
20504
+ for (const claim of claims) {
20505
+ renderClaim(lines, claim);
20506
+ }
20507
+ }
20508
+ var ReviewMarkdownFormatter = class {
20509
+ title;
20510
+ constructor(options = {}) {
20511
+ this.title = options.title ?? "Evidence Review";
20512
+ }
20513
+ format(review) {
20514
+ const lines = [];
20515
+ const { summary, context } = review;
20516
+ lines.push(`# ${this.title}`);
20517
+ lines.push("");
20518
+ if (context.baseRef || context.headRef) {
20519
+ lines.push(
20520
+ `Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
20521
+ );
20522
+ lines.push("");
20523
+ }
20524
+ lines.push("## Review priority");
20525
+ lines.push("");
20526
+ if (summary.changedSourceFiles === 0) {
20527
+ lines.push(
20528
+ "No changed source files supplied \u2014 showing claims and evidence only."
20529
+ );
20530
+ } else if (summary.uncovered > 0) {
20531
+ lines.push(
20532
+ `Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
20533
+ );
20534
+ } else if (summary.weaklyCovered > 0) {
20535
+ lines.push(
20536
+ `No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
20537
+ );
20538
+ } else {
20539
+ lines.push("Every changed source file is backed by at least moderate evidence.");
20540
+ }
20541
+ lines.push("");
20542
+ if (summary.changedSourceFiles > 0) {
20543
+ lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
20544
+ lines.push("| ---: | ---: | ---: | ---: |");
20545
+ lines.push(
20546
+ `| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
20547
+ );
20548
+ lines.push("");
20549
+ }
20550
+ lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
20551
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
20552
+ lines.push(
20553
+ `| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
20554
+ );
20555
+ lines.push("");
20556
+ renderUncoveredBand(lines, review.changedFiles);
20557
+ renderWeakBand(lines, review.changedFiles);
20558
+ renderAudienceSection(
20559
+ lines,
20560
+ "Stakeholder behaviour",
20561
+ review.claims.filter((c) => c.audience === "stakeholder")
20562
+ );
20563
+ renderAudienceSection(
20564
+ lines,
20565
+ "Engineer changes",
20566
+ review.claims.filter((c) => c.audience === "engineer")
20567
+ );
20568
+ return lines.join("\n").trimEnd();
20569
+ }
20570
+ };
20571
+
20572
+ // src/formatters/review-html.ts
20573
+ function escapeHtml3(value) {
20574
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20575
+ }
20576
+ var STRENGTH_LABEL = {
20577
+ strong: "Strong",
20578
+ moderate: "Moderate",
20579
+ weak: "Weak",
20580
+ none: "None"
20581
+ };
20582
+ function statusIcon3(status) {
20583
+ switch (status) {
20584
+ case "passed":
20585
+ return "\u2705";
20586
+ case "failed":
20587
+ return "\u274C";
20588
+ case "skipped":
20589
+ return "\u2298";
20590
+ default:
20591
+ return "\u2022";
20592
+ }
20593
+ }
20594
+ function formatStep3(step) {
20595
+ return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
20596
+ }
20597
+ function inlineDoc(doc) {
20598
+ switch (doc.kind) {
20599
+ case "note":
20600
+ return escapeHtml3(doc.text);
20601
+ case "section":
20602
+ return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
20603
+ case "kv":
20604
+ return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
20605
+ case "code":
20606
+ return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
20607
+ case "link":
20608
+ return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
20609
+ default:
20610
+ return escapeHtml3(doc.kind);
20611
+ }
20612
+ }
20613
+ function renderEvidenceArtifacts(testCase) {
20614
+ const parts = [];
20615
+ for (const att of testCase.attachments) {
20616
+ if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
20617
+ parts.push(
20618
+ `<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
20619
+ );
20620
+ }
20621
+ }
20622
+ if ((testCase.story.otelSpans?.length ?? 0) > 0) {
20623
+ parts.push(
20624
+ `<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
20625
+ );
20626
+ }
20627
+ return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
20628
+ }
20629
+ function renderTicketPills(claim) {
20630
+ const tickets = claim.testCase.story.tickets ?? [];
20631
+ if (tickets.length === 0) return "";
20632
+ return `<div class="ticket-row">${tickets.map((ticket) => {
20633
+ const label = escapeHtml3(ticket.id);
20634
+ if (ticket.url) {
20635
+ return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
20636
+ }
20637
+ return `<span class="ticket-pill">${label}</span>`;
20638
+ }).join("")}</div>`;
20639
+ }
20640
+ function renderClaimCard(claim) {
20641
+ const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
20642
+ const search = escapeHtml3(
20643
+ `${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
20644
+ ).toLowerCase();
20645
+ const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
20646
+ const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
20647
+ const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
20648
+ const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
20649
+ const docs = (claim.testCase.story.docs ?? []).filter(
20650
+ (d) => d.kind === "section" || d.kind === "note"
20651
+ );
20652
+ const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
20653
+ return `
20654
+ <article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
20655
+ <header class="claim-header">
20656
+ <div>
20657
+ <span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
20658
+ ${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
20659
+ <h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
20660
+ <p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
20661
+ ${renderTicketPills(claim)}
20662
+ </div>
20663
+ </header>
20664
+ ${intent}${extraDocs}
20665
+ <div class="evidence-block">
20666
+ <span class="evidence-label">Evidence</span>
20667
+ ${reasons}
20668
+ </div>
20669
+ ${covers}
20670
+ ${renderEvidenceArtifacts(claim.testCase)}
20671
+ ${steps}
20672
+ </article>`;
20673
+ }
20674
+ function renderChangedFileRow(file) {
20675
+ const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
20676
+ return `<tr data-band="${file.band}">
20677
+ <td><span class="band-dot band-${file.band}"></span></td>
20678
+ <td><code>${escapeHtml3(file.path)}</code></td>
20679
+ <td>${escapeHtml3(file.changeKind)}</td>
20680
+ <td>${claims}</td>
20681
+ </tr>`;
20682
+ }
20683
+ function renderAudienceSection2(title, claims) {
20684
+ if (claims.length === 0) return "";
20685
+ return `<section class="audience-section">
20686
+ <h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
20687
+ <div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
20688
+ </section>`;
20689
+ }
20690
+ var REVIEW_CSS = `
20691
+ * { box-sizing: border-box; }
20692
+ body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
20693
+ main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
20694
+ h1, h2, h3, p { margin: 0; }
20695
+ .review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
20696
+ .subtle { color: var(--muted-foreground); margin-top: 6px; }
20697
+ .theme-toggle { background: var(--secondary); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; cursor: pointer; font-size: 1.1rem; color: var(--foreground); }
20698
+ .card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
20699
+ .hero-card { padding: 24px; margin-bottom: 20px; }
20700
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
20701
+ .summary-card { padding: 14px 16px; }
20702
+ .summary-card strong { display: block; font-size: 1.8rem; }
20703
+ .priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
20704
+ .panel { padding: 18px; margin-bottom: 24px; }
20705
+ table { width: 100%; border-collapse: collapse; }
20706
+ th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
20707
+ th { color: var(--muted-foreground); font-weight: 600; }
20708
+ .band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
20709
+ .band-uncovered { background: var(--destructive); }
20710
+ .band-weak { background: var(--warning, #b58900); }
20711
+ .band-covered { background: var(--success, #2e7d32); }
20712
+ .toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
20713
+ .toolbar input { flex: 1 1 240px; border: 1px solid var(--border); border-radius: 999px; padding: 10px 14px; font: inherit; background: var(--background); color: var(--foreground); }
20714
+ .toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
20715
+ .toolbar button.active { background: var(--foreground); color: var(--background); }
20716
+ .audience-section { margin-bottom: 28px; }
20717
+ .audience-section h2 { margin-bottom: 12px; }
20718
+ .count { color: var(--muted-foreground); font-weight: 400; }
20719
+ .claim-list { display: grid; gap: 14px; }
20720
+ .claim-card { padding: 18px; }
20721
+ .claim-header h3 { margin-top: 8px; }
20722
+ .source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
20723
+ .ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
20724
+ .ticket-pill { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 999px; padding: 3px 9px; color: var(--muted-foreground); background: var(--background); font-size: 0.78rem; text-decoration: none; }
20725
+ .ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
20726
+ .strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
20727
+ .change-pill { background: var(--secondary); }
20728
+ .strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
20729
+ .strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
20730
+ .strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
20731
+ .strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
20732
+ .intent { margin: 12px 0; padding: 10px 12px; border-left: 3px solid var(--border); background: color-mix(in srgb, var(--card) 60%, var(--background)); border-radius: 6px; }
20733
+ .intent-label { font-weight: 700; margin-right: 6px; }
20734
+ .evidence-block { margin-top: 10px; }
20735
+ .evidence-label { font-weight: 600; color: var(--muted-foreground); }
20736
+ .reasons { margin: 6px 0 0; padding-left: 18px; }
20737
+ .covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
20738
+ .artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
20739
+ .shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
20740
+ .trace-note { color: var(--muted-foreground); }
20741
+ .step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
20742
+ `;
20743
+ var JS_THEME_TOGGLE2 = `
20744
+ function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
20745
+ function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
20746
+ function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
20747
+ function applyTheme(t) {
20748
+ document.documentElement.setAttribute('data-theme', t);
20749
+ var b = document.querySelector('.theme-toggle');
20750
+ if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
20751
+ }
20752
+ `;
20753
+ var ReviewHtmlFormatter = class {
20754
+ title;
20755
+ theme;
20756
+ darkMode;
20757
+ constructor(options = {}) {
20758
+ this.title = options.title ?? "Evidence Review";
20759
+ this.theme = resolveTheme(options.theme ?? "default");
20760
+ this.darkMode = options.darkMode ?? true;
20761
+ }
20762
+ format(review) {
20763
+ const { summary, context } = review;
20764
+ const priority = summary.changedSourceFiles === 0 ? "No changed source files supplied \u2014 showing claims and evidence only." : summary.uncovered > 0 ? `${summary.uncovered} changed file(s) have no evidence. Review them first.` : summary.weaklyCovered > 0 ? `No unaccounted-for changes. ${summary.weaklyCovered} file(s) are weakly covered.` : "Every changed source file is backed by at least moderate evidence.";
20765
+ const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
20766
+ <h2>Changed files</h2>
20767
+ <table>
20768
+ <thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
20769
+ <tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
20770
+ </table>
20771
+ </section>` : "";
20772
+ const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
20773
+ const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
20774
+ applyTheme(getEffectiveTheme());` : "";
20775
+ const themeAttr = this.darkMode ? ' data-theme="light"' : "";
20776
+ const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
20777
+ return `<!doctype html>
20778
+ <html lang="en"${themeAttr}>
20779
+ <head>
20780
+ <meta charset="utf-8" />
20781
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
20782
+ <title>${escapeHtml3(this.title)}</title>
20783
+ <style>
20784
+ ${this.theme.css}
20785
+ ${REVIEW_CSS}
20786
+ </style>
20787
+ </head>
20788
+ <body>
20789
+ <main>
20790
+ <div class="hero-card card">
20791
+ <div class="review-header">
20792
+ <h1>${escapeHtml3(this.title)}</h1>
20793
+ ${themeToggleHtml}
20794
+ </div>
20795
+ ${refsLine}
20796
+ </div>
20797
+ <section class="summary-grid">
20798
+ <div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
20799
+ <div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
20800
+ <div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
20801
+ <div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
20802
+ <div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
20803
+ <div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
20804
+ </section>
20805
+ <section class="card priority-banner">
20806
+ <h2>Review priority</h2>
20807
+ <p class="subtle">${escapeHtml3(priority)}</p>
20808
+ </section>
20809
+ ${changedFilesPanel}
20810
+ <section class="toolbar">
20811
+ <input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
20812
+ <button type="button" class="active" data-filter="all">All</button>
20813
+ <button type="button" data-filter="stakeholder">Stakeholder</button>
20814
+ <button type="button" data-filter="engineer">Engineer</button>
20815
+ <button type="button" data-filter="weak">Weak/None</button>
20816
+ </section>
20817
+ ${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
20818
+ ${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
20819
+ </main>
20820
+ <script>
20821
+ ${themeInitJs}
20822
+ const input = document.querySelector('input[type="search"]');
20823
+ const buttons = Array.from(document.querySelectorAll('[data-filter]'));
20824
+ const cards = Array.from(document.querySelectorAll('.claim-card'));
20825
+ let activeFilter = 'all';
20826
+ function applyFilters() {
20827
+ const query = (input.value || '').trim().toLowerCase();
20828
+ cards.forEach((card) => {
20829
+ const audience = card.getAttribute('data-audience');
20830
+ const strength = card.getAttribute('data-strength');
20831
+ const haystack = card.getAttribute('data-search') || '';
20832
+ let matchesFilter = activeFilter === 'all'
20833
+ || audience === activeFilter
20834
+ || (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
20835
+ const matchesSearch = !query || haystack.includes(query);
20836
+ card.style.display = matchesFilter && matchesSearch ? '' : 'none';
20837
+ });
20838
+ }
20839
+ input.addEventListener('input', applyFilters);
20840
+ buttons.forEach((button) => {
20841
+ button.addEventListener('click', () => {
20842
+ activeFilter = button.getAttribute('data-filter');
20843
+ buttons.forEach((b) => b.classList.toggle('active', b === button));
20844
+ applyFilters();
20845
+ });
20846
+ });
20847
+ applyFilters();
20848
+ </script>
20849
+ </body>
20850
+ </html>`;
20851
+ }
20852
+ };
19750
20853
 
19751
20854
  // src/index.ts
19752
20855
  var FORMAT_EXTENSIONS = {
19753
20856
  astro: ".md",
20857
+ "behavior-manifest-json": ".behavior-manifest.json",
19754
20858
  markdown: ".md",
19755
20859
  html: ".html",
19756
20860
  "cucumber-html": ".cucumber.html",
@@ -19758,6 +20862,7 @@ var FORMAT_EXTENSIONS = {
19758
20862
  "cucumber-json": ".cucumber.json",
19759
20863
  "cucumber-messages": ".ndjson",
19760
20864
  confluence: ".adf.json",
20865
+ "scenario-index-json": ".scenarios-index.json",
19761
20866
  "story-report-json": ".story-report.json"
19762
20867
  };
19763
20868
  var TEST_EXTENSIONS = [
@@ -19785,11 +20890,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
19785
20890
  const ext = FORMAT_EXTENSIONS[format];
19786
20891
  const effectiveName = outputName + (outputNameSuffix ?? "");
19787
20892
  if (mode === "aggregated") {
19788
- return toPosix(path9.join(baseOutputDir, `${effectiveName}${ext}`));
20893
+ return toPosix(path10.join(baseOutputDir, `${effectiveName}${ext}`));
19789
20894
  }
19790
20895
  const normalizedSource = toPosix(sourceFile);
19791
- const dirOfSource = path9.posix.dirname(normalizedSource);
19792
- let baseName = path9.posix.basename(normalizedSource);
20896
+ const dirOfSource = path10.posix.dirname(normalizedSource);
20897
+ let baseName = path10.posix.basename(normalizedSource);
19793
20898
  for (const testExt of TEST_EXTENSIONS) {
19794
20899
  if (baseName.endsWith(testExt)) {
19795
20900
  baseName = baseName.slice(0, -testExt.length);
@@ -19798,9 +20903,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
19798
20903
  }
19799
20904
  const fileName = `${baseName}.${effectiveName}${ext}`;
19800
20905
  if (colocatedStyle === "adjacent") {
19801
- return toPosix(path9.posix.join(dirOfSource, fileName));
20906
+ return toPosix(path10.posix.join(dirOfSource, fileName));
19802
20907
  }
19803
- return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
20908
+ return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
19804
20909
  }
19805
20910
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
19806
20911
  const groups = /* @__PURE__ */ new Map();
@@ -19886,6 +20991,12 @@ var ReportGenerator = class {
19886
20991
  storyReportJson: {
19887
20992
  pretty: options.storyReportJson?.pretty ?? true
19888
20993
  },
20994
+ scenarioIndexJson: {
20995
+ pretty: options.scenarioIndexJson?.pretty ?? true
20996
+ },
20997
+ behaviorManifestJson: {
20998
+ pretty: options.behaviorManifestJson?.pretty ?? true
20999
+ },
19889
21000
  cucumberMessages: {
19890
21001
  uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
19891
21002
  includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
@@ -20001,8 +21112,8 @@ var ReportGenerator = class {
20001
21112
  if (astroPaths) {
20002
21113
  for (const mdPath of astroPaths) {
20003
21114
  const content = await fsPromises.readFile(mdPath, "utf8");
20004
- const mdDir = path9.dirname(mdPath);
20005
- const assetsDir = path9.resolve(this.options.astro.assetsDir);
21115
+ const mdDir = path10.dirname(mdPath);
21116
+ const assetsDir = path10.resolve(this.options.astro.assetsDir);
20006
21117
  const result = copyMarkdownAssets({
20007
21118
  markdown: content,
20008
21119
  markdownDir: mdDir,
@@ -20033,9 +21144,9 @@ var ReportGenerator = class {
20033
21144
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
20034
21145
  const ext = FORMAT_EXTENSIONS[format];
20035
21146
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
20036
- const outputPath = toPosix(path9.join(this.options.outputDir, `${effectiveName}${ext}`));
21147
+ const outputPath = toPosix(path10.join(this.options.outputDir, `${effectiveName}${ext}`));
20037
21148
  const content = await this.formatContent(run, format);
20038
- const dir = path9.dirname(outputPath);
21149
+ const dir = path10.dirname(outputPath);
20039
21150
  await fsPromises.mkdir(dir, { recursive: true });
20040
21151
  await this.deps.writeFile(outputPath, content);
20041
21152
  return [outputPath];
@@ -20047,7 +21158,7 @@ var ReportGenerator = class {
20047
21158
  testCases
20048
21159
  };
20049
21160
  const content = await this.formatContent(groupRun, format);
20050
- const dir = path9.dirname(outputPath);
21161
+ const dir = path10.dirname(outputPath);
20051
21162
  await fsPromises.mkdir(dir, { recursive: true });
20052
21163
  await this.deps.writeFile(outputPath, content);
20053
21164
  writtenPaths.push(outputPath);
@@ -20160,6 +21271,18 @@ var ReportGenerator = class {
20160
21271
  });
20161
21272
  return formatter.format(run);
20162
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
+ }
20163
21286
  default:
20164
21287
  throw new Error(`Unknown format: ${format}`);
20165
21288
  }
@@ -20176,7 +21299,7 @@ async function generateRunComparison(args) {
20176
21299
  await fsPromises.mkdir(outputDir, { recursive: true });
20177
21300
  for (const format of args.formats) {
20178
21301
  const ext = format === "html" ? ".html" : ".md";
20179
- const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
21302
+ const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
20180
21303
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
20181
21304
  await fsPromises.writeFile(outputPath, content, "utf8");
20182
21305
  files.push(outputPath);
@@ -20198,6 +21321,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20198
21321
  // Annotate the CommonJS export names for ESM import in node:
20199
21322
  0 && (module.exports = {
20200
21323
  AstroFormatter,
21324
+ BehaviorManifestJsonFormatter,
20201
21325
  ConfluenceFormatter,
20202
21326
  CucumberHtmlFormatter,
20203
21327
  CucumberJsonFormatter,
@@ -20211,29 +21335,37 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20211
21335
  MIN_PERF_SAMPLES,
20212
21336
  MarkdownFormatter,
20213
21337
  ReportGenerator,
21338
+ ReviewHtmlFormatter,
21339
+ ReviewMarkdownFormatter,
20214
21340
  RunDiffHtmlFormatter,
20215
21341
  RunDiffMarkdownFormatter,
20216
21342
  STORY_META_KEY,
20217
21343
  STORY_REPORT_SCHEMA_MAJOR,
20218
21344
  STORY_REPORT_SCHEMA_VERSION,
21345
+ ScenarioIndexJsonFormatter,
20219
21346
  StoryReportJsonFormatter,
20220
21347
  adaptJestRun,
20221
21348
  adaptPlaywrightRun,
20222
21349
  adaptVitestRun,
20223
21350
  assertValidRun,
21351
+ buildReview,
20224
21352
  bundleAssets,
20225
21353
  calculateFlakiness,
20226
21354
  calculateStability,
20227
21355
  canonicalizeRun,
21356
+ classifyStatusChange,
20228
21357
  clearVersionCache,
20229
21358
  computeTestMetrics,
20230
21359
  copyMarkdownAssets,
20231
21360
  createPrCommentSummary,
20232
21361
  createReportGenerator,
21362
+ deriveAudience,
21363
+ deriveChangeType,
20233
21364
  deriveStepResults,
20234
21365
  detectCI,
20235
21366
  detectPerformanceTrend,
20236
21367
  diffRuns,
21368
+ diffStoryReports,
20237
21369
  findGitDir,
20238
21370
  formatDuration,
20239
21371
  generateRunComparison,
@@ -20241,7 +21373,10 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20241
21373
  generateTestCaseId,
20242
21374
  getAvailableThemes,
20243
21375
  getCssOnlyThemes,
21376
+ gradeEvidence,
20244
21377
  hasSufficientHistory,
21378
+ isReviewableSource,
21379
+ isTestFile,
20245
21380
  listScenarios,
20246
21381
  loadHistory,
20247
21382
  mergeStepResults,
@@ -20258,21 +21393,26 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
20258
21393
  readBranchName,
20259
21394
  readGitSha,
20260
21395
  readPackageVersion,
21396
+ regenerateArtifacts,
20261
21397
  resolveAttachment,
20262
21398
  resolveAttachments,
20263
21399
  resolveTheme,
20264
21400
  resolveTraceUrl,
20265
21401
  rewriteAssetPaths,
20266
21402
  saveHistory,
21403
+ scenariosCoveringPaths,
20267
21404
  sendNotifications,
20268
21405
  sendSlackNotification,
20269
21406
  sendTeamsNotification,
20270
21407
  sendWebhookNotification,
20271
21408
  signBody,
20272
21409
  slugify,
21410
+ startWatch,
20273
21411
  stripAnsi,
21412
+ toBehaviorManifest,
20274
21413
  toCIInfo,
20275
21414
  toRawCIInfo,
21415
+ toScenarioIndex,
20276
21416
  toStoryReport,
20277
21417
  tryGetActiveOtelContext,
20278
21418
  updateHistory,