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/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1306 -96
- package/dist/cli.js.map +1 -1
- package/dist/{index-BiAYcEiz.d.cts → index-CbWFyoTx.d.cts} +161 -4
- package/dist/{index-BiAYcEiz.d.ts → index-CbWFyoTx.d.ts} +161 -4
- package/dist/index.cjs +1196 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +413 -126
- package/dist/index.d.ts +413 -126
- package/dist/index.js +1178 -55
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/schemas/behavior-manifest-v1.json +65 -0
- package/schemas/examples/dotnet.json +84 -20
- package/schemas/examples/go.json +77 -20
- package/schemas/examples/junit5.json +84 -20
- package/schemas/examples/pytest.json +92 -20
- package/schemas/examples/rust.json +84 -20
- package/schemas/raw-run.schema.json +49 -2
- package/schemas/scenario-index-v1.json +88 -0
- package/schemas/story-report-v1.json +5 -0
- package/bin/intent.js +0 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import "fs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as path10 from "path";
|
|
4
4
|
import * as fsPromises from "fs/promises";
|
|
5
5
|
|
|
6
6
|
// src/converters/acl/status.ts
|
|
@@ -281,7 +281,8 @@ function canonicalizeTestCase(raw, options, projectRoot) {
|
|
|
281
281
|
projectName: raw.projectName,
|
|
282
282
|
retry: raw.retry ?? 0,
|
|
283
283
|
retries: raw.retries ?? 0,
|
|
284
|
-
tags
|
|
284
|
+
tags,
|
|
285
|
+
...raw.evidence ? { evidence: raw.evidence } : {}
|
|
285
286
|
};
|
|
286
287
|
}
|
|
287
288
|
function normalizeTags(story) {
|
|
@@ -845,6 +846,9 @@ function buildScenario(tc, featureId) {
|
|
|
845
846
|
if (tickets && tickets.length > 0) {
|
|
846
847
|
scenario.tickets = tickets.map((t) => t.url ? { id: t.id, url: t.url } : { id: t.id });
|
|
847
848
|
}
|
|
849
|
+
if (tc.story.covers && tc.story.covers.length > 0) {
|
|
850
|
+
scenario.covers = [...tc.story.covers];
|
|
851
|
+
}
|
|
848
852
|
return scenario;
|
|
849
853
|
}
|
|
850
854
|
function deriveFeatureTitle(group, relSourceFile) {
|
|
@@ -968,6 +972,181 @@ var StoryReportJsonFormatter = class {
|
|
|
968
972
|
}
|
|
969
973
|
};
|
|
970
974
|
|
|
975
|
+
// src/formatters/scenario-index-json.ts
|
|
976
|
+
var ScenarioIndexJsonFormatter = class {
|
|
977
|
+
options;
|
|
978
|
+
constructor(options = {}) {
|
|
979
|
+
this.options = {
|
|
980
|
+
pretty: options.pretty ?? true,
|
|
981
|
+
filters: options.filters
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
toIndex(run) {
|
|
985
|
+
return toScenarioIndex(toStoryReport(run), this.options.filters);
|
|
986
|
+
}
|
|
987
|
+
format(run) {
|
|
988
|
+
const index = this.toIndex(run);
|
|
989
|
+
return this.options.pretty ? JSON.stringify(index, null, 2) : JSON.stringify(index);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
function toScenarioIndex(report, filters = {}) {
|
|
993
|
+
const scenarios = report.features.flatMap(
|
|
994
|
+
(feature) => feature.scenarios.map((scenario) => toScenarioIndexItem(feature, scenario))
|
|
995
|
+
).filter((scenario) => matchesFilters(scenario, filters));
|
|
996
|
+
return {
|
|
997
|
+
schemaVersion: "1.0",
|
|
998
|
+
runId: report.runId,
|
|
999
|
+
generatedAtMs: report.finishedAtMs,
|
|
1000
|
+
summary: summarize(scenarios),
|
|
1001
|
+
scenarios
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function toScenarioIndexItem(feature, scenario) {
|
|
1005
|
+
return {
|
|
1006
|
+
id: scenario.id,
|
|
1007
|
+
title: scenario.title,
|
|
1008
|
+
status: scenario.status,
|
|
1009
|
+
feature: feature.title,
|
|
1010
|
+
sourceFile: feature.sourceFile,
|
|
1011
|
+
sourceLine: scenario.sourceLine,
|
|
1012
|
+
tags: scenario.tags,
|
|
1013
|
+
tickets: scenario.tickets ?? [],
|
|
1014
|
+
covers: scenario.covers ?? [],
|
|
1015
|
+
durationMs: scenario.durationMs,
|
|
1016
|
+
steps: scenario.steps.map((step) => ({
|
|
1017
|
+
id: step.id,
|
|
1018
|
+
index: step.index,
|
|
1019
|
+
keyword: step.keyword,
|
|
1020
|
+
text: step.text,
|
|
1021
|
+
status: step.status,
|
|
1022
|
+
durationMs: step.durationMs,
|
|
1023
|
+
errorMessage: step.errorMessage,
|
|
1024
|
+
docKinds: step.docEntries.map((entry) => entry.kind)
|
|
1025
|
+
})),
|
|
1026
|
+
docKinds: scenario.docEntries.map((entry) => entry.kind),
|
|
1027
|
+
error: scenario.errorMessage ? { message: scenario.errorMessage, stack: scenario.errorStack } : void 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function matchesFilters(scenario, filters) {
|
|
1031
|
+
if (filters.statuses?.length && !filters.statuses.includes(scenario.status)) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (filters.tags?.length && !filters.tags.some((tag) => scenario.tags.includes(tag))) {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
if (filters.sourceFiles?.length && !filters.sourceFiles.some((sourceFile) => scenario.sourceFile.includes(sourceFile))) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
function summarize(scenarios) {
|
|
1043
|
+
return {
|
|
1044
|
+
total: scenarios.length,
|
|
1045
|
+
passed: scenarios.filter((scenario) => scenario.status === "passed").length,
|
|
1046
|
+
failed: scenarios.filter((scenario) => scenario.status === "failed").length,
|
|
1047
|
+
skipped: scenarios.filter((scenario) => scenario.status === "skipped").length,
|
|
1048
|
+
pending: scenarios.filter((scenario) => scenario.status === "pending").length,
|
|
1049
|
+
durationMs: scenarios.reduce((total, scenario) => total + scenario.durationMs, 0)
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/formatters/behavior-manifest-json.ts
|
|
1054
|
+
var BehaviorManifestJsonFormatter = class {
|
|
1055
|
+
pretty;
|
|
1056
|
+
constructor(options = {}) {
|
|
1057
|
+
this.pretty = options.pretty ?? true;
|
|
1058
|
+
}
|
|
1059
|
+
toManifest(run) {
|
|
1060
|
+
return toBehaviorManifest(toStoryReport(run));
|
|
1061
|
+
}
|
|
1062
|
+
format(run) {
|
|
1063
|
+
const manifest = this.toManifest(run);
|
|
1064
|
+
return this.pretty ? JSON.stringify(manifest, null, 2) : JSON.stringify(manifest);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
function toBehaviorManifest(report) {
|
|
1068
|
+
const index = toScenarioIndex(report);
|
|
1069
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1070
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
1071
|
+
const docKinds = /* @__PURE__ */ new Set();
|
|
1072
|
+
const debuggerIssues = [];
|
|
1073
|
+
for (const scenario of index.scenarios) {
|
|
1074
|
+
const source = bySource.get(scenario.sourceFile) ?? {
|
|
1075
|
+
path: scenario.sourceFile,
|
|
1076
|
+
scenarioCount: 0,
|
|
1077
|
+
failed: 0,
|
|
1078
|
+
tags: []
|
|
1079
|
+
};
|
|
1080
|
+
source.scenarioCount += 1;
|
|
1081
|
+
if (scenario.status === "failed") source.failed += 1;
|
|
1082
|
+
source.tags = [.../* @__PURE__ */ new Set([...source.tags, ...scenario.tags])].sort();
|
|
1083
|
+
bySource.set(scenario.sourceFile, source);
|
|
1084
|
+
for (const tag of scenario.tags) {
|
|
1085
|
+
const tagEntry = byTag.get(tag) ?? { name: tag, scenarioCount: 0 };
|
|
1086
|
+
tagEntry.scenarioCount += 1;
|
|
1087
|
+
byTag.set(tag, tagEntry);
|
|
1088
|
+
}
|
|
1089
|
+
for (const kind of scenario.docKinds) docKinds.add(kind);
|
|
1090
|
+
for (const step of scenario.steps) {
|
|
1091
|
+
for (const kind of step.docKinds) docKinds.add(kind);
|
|
1092
|
+
}
|
|
1093
|
+
if (!scenarioHasDocs(scenario)) {
|
|
1094
|
+
debuggerIssues.push({
|
|
1095
|
+
severity: "warning",
|
|
1096
|
+
code: "missing-docs",
|
|
1097
|
+
scenarioId: scenario.id,
|
|
1098
|
+
title: scenario.title,
|
|
1099
|
+
message: "Scenario has no doc entries."
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
if (scenario.tags.length === 0) {
|
|
1103
|
+
debuggerIssues.push({
|
|
1104
|
+
severity: "warning",
|
|
1105
|
+
code: "missing-tags",
|
|
1106
|
+
scenarioId: scenario.id,
|
|
1107
|
+
title: scenario.title,
|
|
1108
|
+
message: "Scenario has no tags."
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
if (scenario.covers.length === 0) {
|
|
1112
|
+
debuggerIssues.push({
|
|
1113
|
+
severity: "warning",
|
|
1114
|
+
code: "missing-covers",
|
|
1115
|
+
scenarioId: scenario.id,
|
|
1116
|
+
title: scenario.title,
|
|
1117
|
+
message: "Scenario declares no covers (product-code paths), so code\u2192scenario lookup cannot find it."
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
if (scenario.sourceLine === void 0) {
|
|
1121
|
+
debuggerIssues.push({
|
|
1122
|
+
severity: "warning",
|
|
1123
|
+
code: "missing-source-line",
|
|
1124
|
+
scenarioId: scenario.id,
|
|
1125
|
+
title: scenario.title,
|
|
1126
|
+
message: "Scenario has no source line."
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const scenariosWithDocs = index.scenarios.filter(scenarioHasDocs).length;
|
|
1131
|
+
return {
|
|
1132
|
+
schemaVersion: "1.0",
|
|
1133
|
+
runId: report.runId,
|
|
1134
|
+
generatedAtMs: report.finishedAtMs,
|
|
1135
|
+
summary: index.summary,
|
|
1136
|
+
sourceFiles: [...bySource.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
1137
|
+
tags: [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
1138
|
+
docCoverage: {
|
|
1139
|
+
scenariosWithDocs,
|
|
1140
|
+
scenariosWithoutDocs: index.scenarios.length - scenariosWithDocs,
|
|
1141
|
+
docKinds: [...docKinds].sort()
|
|
1142
|
+
},
|
|
1143
|
+
debugger: debuggerIssues
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function scenarioHasDocs(scenario) {
|
|
1147
|
+
return scenario.docKinds.length > 0 || scenario.steps.some((step) => step.docKinds.length > 0);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
971
1150
|
// src/formatters/html/renderers/index.ts
|
|
972
1151
|
import * as fs2 from "fs";
|
|
973
1152
|
import * as path3 from "path";
|
|
@@ -13814,7 +13993,7 @@ function renderDocEntry(entry, deps) {
|
|
|
13814
13993
|
// src/formatters/html/renderers/steps.ts
|
|
13815
13994
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
13816
13995
|
function renderStep(step, stepResult, index, deps) {
|
|
13817
|
-
const
|
|
13996
|
+
const statusIcon4 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
13818
13997
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
13819
13998
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
13820
13999
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -13823,7 +14002,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
13823
14002
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
13824
14003
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
13825
14004
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
13826
|
-
<span class="step-status ${statusClass}">${
|
|
14005
|
+
<span class="step-status ${statusClass}">${statusIcon4}</span>
|
|
13827
14006
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
13828
14007
|
<span class="step-text">${textHtml}</span>
|
|
13829
14008
|
<span class="step-duration">${duration}</span>
|
|
@@ -13873,16 +14052,16 @@ function hasSufficientHistory(entries, min) {
|
|
|
13873
14052
|
}
|
|
13874
14053
|
|
|
13875
14054
|
// src/formatters/html/renderers/scenario.ts
|
|
13876
|
-
function renderTicket(ticket, template,
|
|
14055
|
+
function renderTicket(ticket, template, escapeHtml4) {
|
|
13877
14056
|
const url = ticket.url ?? (template ? template.replace("{ticket}", ticket.id) : void 0);
|
|
13878
14057
|
if (url) {
|
|
13879
|
-
return `<a class="tag ticket-tag" href="${
|
|
14058
|
+
return `<a class="tag ticket-tag" href="${escapeHtml4(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml4(ticket.id)}</a>`;
|
|
13880
14059
|
}
|
|
13881
|
-
return `<span class="tag ticket-tag">${
|
|
14060
|
+
return `<span class="tag ticket-tag">${escapeHtml4(ticket.id)}</span>`;
|
|
13882
14061
|
}
|
|
13883
14062
|
function renderScenario(args, deps) {
|
|
13884
14063
|
const { tc } = args;
|
|
13885
|
-
const
|
|
14064
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
13886
14065
|
const statusClass = `status-${tc.status}`;
|
|
13887
14066
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
13888
14067
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -13952,7 +14131,7 @@ function renderScenario(args, deps) {
|
|
|
13952
14131
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
13953
14132
|
<div class="scenario-info">
|
|
13954
14133
|
<div class="scenario-title">
|
|
13955
|
-
<span class="status-icon ${statusClass}">${
|
|
14134
|
+
<span class="status-icon ${statusClass}">${statusIcon4}</span>
|
|
13956
14135
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
13957
14136
|
</div>
|
|
13958
14137
|
<div class="scenario-meta">${tags}${tickets}${outcomeBadge}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
@@ -14078,7 +14257,7 @@ function flattenTree(roots) {
|
|
|
14078
14257
|
}
|
|
14079
14258
|
return result;
|
|
14080
14259
|
}
|
|
14081
|
-
function buildTooltip(span,
|
|
14260
|
+
function buildTooltip(span, escapeHtml4) {
|
|
14082
14261
|
const parts = [];
|
|
14083
14262
|
parts.push(`${span.name} (${formatDuration(span.durationMs)})`);
|
|
14084
14263
|
if (span.statusMessage) {
|
|
@@ -14096,7 +14275,7 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
14096
14275
|
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
14097
14276
|
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
14098
14277
|
}
|
|
14099
|
-
return
|
|
14278
|
+
return escapeHtml4(text2);
|
|
14100
14279
|
}
|
|
14101
14280
|
function renderTraceView(args, deps) {
|
|
14102
14281
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14319,11 +14498,11 @@ function renderToc(args, deps) {
|
|
|
14319
14498
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14320
14499
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14321
14500
|
const scenarios = testCases.map((tc) => {
|
|
14322
|
-
const
|
|
14501
|
+
const statusIcon4 = deps.getStatusIcon(tc.status);
|
|
14323
14502
|
const statusClass = `status-${tc.status}`;
|
|
14324
14503
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14325
14504
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14326
|
-
<span class="toc-status ${statusClass}">${
|
|
14505
|
+
<span class="toc-status ${statusClass}">${statusIcon4}</span>
|
|
14327
14506
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14328
14507
|
</a>`;
|
|
14329
14508
|
}).join("\n");
|
|
@@ -15836,8 +16015,8 @@ function extractDocAttachments(step) {
|
|
|
15836
16015
|
}
|
|
15837
16016
|
return attachments;
|
|
15838
16017
|
}
|
|
15839
|
-
function guessMediaType(
|
|
15840
|
-
const lower =
|
|
16018
|
+
function guessMediaType(path11) {
|
|
16019
|
+
const lower = path11.toLowerCase();
|
|
15841
16020
|
if (lower.endsWith(".png")) return "image/png";
|
|
15842
16021
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
15843
16022
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -15978,11 +16157,11 @@ var CucumberHtmlFormatter = class {
|
|
|
15978
16157
|
for (const envelope of envelopes) {
|
|
15979
16158
|
const accepted = htmlStream.write(envelope);
|
|
15980
16159
|
if (!accepted) {
|
|
15981
|
-
await new Promise((
|
|
16160
|
+
await new Promise((resolve8) => htmlStream.once("drain", resolve8));
|
|
15982
16161
|
}
|
|
15983
16162
|
}
|
|
15984
|
-
await new Promise((
|
|
15985
|
-
collector.on("finish",
|
|
16163
|
+
await new Promise((resolve8, reject) => {
|
|
16164
|
+
collector.on("finish", resolve8);
|
|
15986
16165
|
collector.on("error", reject);
|
|
15987
16166
|
htmlStream.end();
|
|
15988
16167
|
});
|
|
@@ -17971,6 +18150,184 @@ ${result.errors.join("\n")}`);
|
|
|
17971
18150
|
}
|
|
17972
18151
|
}
|
|
17973
18152
|
|
|
18153
|
+
// src/coverage-index.ts
|
|
18154
|
+
function normalizePath(path11) {
|
|
18155
|
+
return path11.replace(/^\.\//, "");
|
|
18156
|
+
}
|
|
18157
|
+
function scenariosCoveringPaths(index, paths) {
|
|
18158
|
+
const queries = paths.map(normalizePath);
|
|
18159
|
+
return index.scenarios.filter(
|
|
18160
|
+
(scenario) => scenario.covers.some(
|
|
18161
|
+
(glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
|
|
18162
|
+
)
|
|
18163
|
+
);
|
|
18164
|
+
}
|
|
18165
|
+
|
|
18166
|
+
// src/watch.ts
|
|
18167
|
+
import * as fs6 from "fs";
|
|
18168
|
+
import * as path7 from "path";
|
|
18169
|
+
|
|
18170
|
+
// src/converters/synthesize.ts
|
|
18171
|
+
var KEYWORD_MAP = {
|
|
18172
|
+
given: "Given",
|
|
18173
|
+
when: "When",
|
|
18174
|
+
then: "Then",
|
|
18175
|
+
and: "And",
|
|
18176
|
+
but: "But"
|
|
18177
|
+
};
|
|
18178
|
+
function normalizeKeyword(keyword) {
|
|
18179
|
+
return KEYWORD_MAP[keyword.toLowerCase()] ?? keyword;
|
|
18180
|
+
}
|
|
18181
|
+
function normalizeStepKeywords(steps) {
|
|
18182
|
+
return steps.map((step) => ({
|
|
18183
|
+
...step,
|
|
18184
|
+
keyword: normalizeKeyword(step.keyword)
|
|
18185
|
+
}));
|
|
18186
|
+
}
|
|
18187
|
+
function deriveScenarioName(tc) {
|
|
18188
|
+
if (tc.title) return tc.title;
|
|
18189
|
+
if (tc.titlePath && tc.titlePath.length > 0) {
|
|
18190
|
+
return tc.titlePath[tc.titlePath.length - 1];
|
|
18191
|
+
}
|
|
18192
|
+
return "Untitled";
|
|
18193
|
+
}
|
|
18194
|
+
function synthesizeStories(raw) {
|
|
18195
|
+
return {
|
|
18196
|
+
...raw,
|
|
18197
|
+
testCases: raw.testCases.map(synthesizeTestCase)
|
|
18198
|
+
};
|
|
18199
|
+
}
|
|
18200
|
+
function synthesizeTestCase(tc) {
|
|
18201
|
+
if (tc.story == null) {
|
|
18202
|
+
const scenario = deriveScenarioName(tc);
|
|
18203
|
+
return {
|
|
18204
|
+
...tc,
|
|
18205
|
+
story: {
|
|
18206
|
+
scenario,
|
|
18207
|
+
steps: [{ keyword: "Then", text: scenario }]
|
|
18208
|
+
}
|
|
18209
|
+
};
|
|
18210
|
+
}
|
|
18211
|
+
const steps = tc.story.steps;
|
|
18212
|
+
if (!steps || steps.length === 0) {
|
|
18213
|
+
return {
|
|
18214
|
+
...tc,
|
|
18215
|
+
story: {
|
|
18216
|
+
...tc.story,
|
|
18217
|
+
steps: [{ keyword: "Then", text: tc.story.scenario }]
|
|
18218
|
+
}
|
|
18219
|
+
};
|
|
18220
|
+
}
|
|
18221
|
+
return {
|
|
18222
|
+
...tc,
|
|
18223
|
+
story: {
|
|
18224
|
+
...tc.story,
|
|
18225
|
+
steps: normalizeStepKeywords(steps)
|
|
18226
|
+
}
|
|
18227
|
+
};
|
|
18228
|
+
}
|
|
18229
|
+
|
|
18230
|
+
// src/watch.ts
|
|
18231
|
+
function toRun(data, inputType, synthesize) {
|
|
18232
|
+
if (inputType === "canonical") return data;
|
|
18233
|
+
let raw = data;
|
|
18234
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
18235
|
+
return canonicalizeRun(raw);
|
|
18236
|
+
}
|
|
18237
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
18238
|
+
const read = deps.readFile ?? ((filePath) => fs6.readFileSync(filePath, "utf8"));
|
|
18239
|
+
const data = JSON.parse(read(path7.resolve(options.input)));
|
|
18240
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
18241
|
+
const generator = new ReportGenerator({
|
|
18242
|
+
formats: options.formats,
|
|
18243
|
+
outputDir: options.outputDir,
|
|
18244
|
+
outputName: options.outputName
|
|
18245
|
+
});
|
|
18246
|
+
const result = await generator.generate(run);
|
|
18247
|
+
return [...result.values()].flat();
|
|
18248
|
+
}
|
|
18249
|
+
function startWatch(options, deps = {}) {
|
|
18250
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
18251
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
18252
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs6.watch(filePath, listener));
|
|
18253
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
18254
|
+
let timer;
|
|
18255
|
+
let running = false;
|
|
18256
|
+
let pending = false;
|
|
18257
|
+
const run = async () => {
|
|
18258
|
+
if (running) {
|
|
18259
|
+
pending = true;
|
|
18260
|
+
return;
|
|
18261
|
+
}
|
|
18262
|
+
running = true;
|
|
18263
|
+
try {
|
|
18264
|
+
const files = await regenerate(options.input);
|
|
18265
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
18266
|
+
} catch (error) {
|
|
18267
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
18268
|
+
} finally {
|
|
18269
|
+
running = false;
|
|
18270
|
+
if (pending) {
|
|
18271
|
+
pending = false;
|
|
18272
|
+
trigger();
|
|
18273
|
+
}
|
|
18274
|
+
}
|
|
18275
|
+
};
|
|
18276
|
+
const trigger = () => {
|
|
18277
|
+
if (timer) clearTimeout(timer);
|
|
18278
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
18279
|
+
};
|
|
18280
|
+
trigger();
|
|
18281
|
+
const watcher = watchFn(path7.resolve(options.input), trigger);
|
|
18282
|
+
return {
|
|
18283
|
+
close: () => {
|
|
18284
|
+
if (timer) clearTimeout(timer);
|
|
18285
|
+
watcher.close();
|
|
18286
|
+
}
|
|
18287
|
+
};
|
|
18288
|
+
}
|
|
18289
|
+
|
|
18290
|
+
// src/behavior-diff.ts
|
|
18291
|
+
function classifyStatusChange(baseline, current) {
|
|
18292
|
+
if (baseline === void 0) return "added";
|
|
18293
|
+
if (current === void 0) return "removed";
|
|
18294
|
+
if (baseline === current) return "unchanged";
|
|
18295
|
+
if (baseline === "passed" && current === "failed") return "regressed";
|
|
18296
|
+
if (baseline === "failed" && current === "passed") return "fixed";
|
|
18297
|
+
return "changed";
|
|
18298
|
+
}
|
|
18299
|
+
function scenarioMap(report) {
|
|
18300
|
+
const map = /* @__PURE__ */ new Map();
|
|
18301
|
+
for (const feature of report.features) {
|
|
18302
|
+
for (const scenario of feature.scenarios) {
|
|
18303
|
+
map.set(scenario.id, { scenario, sourceFile: feature.sourceFile });
|
|
18304
|
+
}
|
|
18305
|
+
}
|
|
18306
|
+
return map;
|
|
18307
|
+
}
|
|
18308
|
+
function diffStoryReports(baseline, current) {
|
|
18309
|
+
const base = scenarioMap(baseline);
|
|
18310
|
+
const curr = scenarioMap(current);
|
|
18311
|
+
const ids = [.../* @__PURE__ */ new Set([...base.keys(), ...curr.keys()])];
|
|
18312
|
+
const scenarios = ids.map((id) => {
|
|
18313
|
+
const b = base.get(id);
|
|
18314
|
+
const c = curr.get(id);
|
|
18315
|
+
const kind = classifyStatusChange(b?.scenario.status, c?.scenario.status);
|
|
18316
|
+
const meta = c ?? b;
|
|
18317
|
+
return {
|
|
18318
|
+
id,
|
|
18319
|
+
title: meta.scenario.title,
|
|
18320
|
+
sourceFile: meta.sourceFile,
|
|
18321
|
+
kind,
|
|
18322
|
+
baselineStatus: b?.scenario.status,
|
|
18323
|
+
currentStatus: c?.scenario.status
|
|
18324
|
+
};
|
|
18325
|
+
});
|
|
18326
|
+
const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
|
|
18327
|
+
for (const s of scenarios) summary[s.kind] += 1;
|
|
18328
|
+
return { schemaVersion: "1.0", summary, scenarios };
|
|
18329
|
+
}
|
|
18330
|
+
|
|
17974
18331
|
// src/publishers/confluence.ts
|
|
17975
18332
|
function parseAdf(adf) {
|
|
17976
18333
|
let parsed;
|
|
@@ -18566,27 +18923,27 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
18566
18923
|
}
|
|
18567
18924
|
|
|
18568
18925
|
// src/utils/git-info.ts
|
|
18569
|
-
import * as
|
|
18570
|
-
import * as
|
|
18926
|
+
import * as fs7 from "fs";
|
|
18927
|
+
import * as path8 from "path";
|
|
18571
18928
|
function readGitSha(cwd = process.cwd()) {
|
|
18572
18929
|
const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
|
|
18573
18930
|
if (envSha) return envSha;
|
|
18574
18931
|
const gitDir = findGitDir(cwd);
|
|
18575
18932
|
if (!gitDir) return void 0;
|
|
18576
18933
|
try {
|
|
18577
|
-
const headPath =
|
|
18578
|
-
const head =
|
|
18934
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
18935
|
+
const head = fs7.readFileSync(headPath, "utf8").trim();
|
|
18579
18936
|
if (!head.startsWith("ref:")) {
|
|
18580
18937
|
return head;
|
|
18581
18938
|
}
|
|
18582
18939
|
const refPath = head.replace("ref:", "").trim();
|
|
18583
|
-
const refFile =
|
|
18584
|
-
if (
|
|
18585
|
-
return
|
|
18940
|
+
const refFile = path8.join(gitDir, refPath);
|
|
18941
|
+
if (fs7.existsSync(refFile)) {
|
|
18942
|
+
return fs7.readFileSync(refFile, "utf8").trim();
|
|
18586
18943
|
}
|
|
18587
|
-
const packedRefs =
|
|
18588
|
-
if (
|
|
18589
|
-
const content =
|
|
18944
|
+
const packedRefs = path8.join(gitDir, "packed-refs");
|
|
18945
|
+
if (fs7.existsSync(packedRefs)) {
|
|
18946
|
+
const content = fs7.readFileSync(packedRefs, "utf8");
|
|
18590
18947
|
for (const line of content.split("\n")) {
|
|
18591
18948
|
if (!line || line.startsWith("#") || line.startsWith("^")) continue;
|
|
18592
18949
|
const [sha, ref] = line.split(" ");
|
|
@@ -18601,19 +18958,19 @@ function readGitSha(cwd = process.cwd()) {
|
|
|
18601
18958
|
function findGitDir(start) {
|
|
18602
18959
|
let current = start;
|
|
18603
18960
|
while (true) {
|
|
18604
|
-
const candidate =
|
|
18605
|
-
if (
|
|
18606
|
-
const stat =
|
|
18961
|
+
const candidate = path8.join(current, ".git");
|
|
18962
|
+
if (fs7.existsSync(candidate)) {
|
|
18963
|
+
const stat = fs7.statSync(candidate);
|
|
18607
18964
|
if (stat.isFile()) {
|
|
18608
|
-
const content =
|
|
18965
|
+
const content = fs7.readFileSync(candidate, "utf8").trim();
|
|
18609
18966
|
const match = content.match(/^gitdir: (.+)$/);
|
|
18610
18967
|
if (match) {
|
|
18611
|
-
return
|
|
18968
|
+
return path8.resolve(current, match[1]);
|
|
18612
18969
|
}
|
|
18613
18970
|
}
|
|
18614
18971
|
return candidate;
|
|
18615
18972
|
}
|
|
18616
|
-
const parent =
|
|
18973
|
+
const parent = path8.dirname(current);
|
|
18617
18974
|
if (parent === current) return void 0;
|
|
18618
18975
|
current = parent;
|
|
18619
18976
|
}
|
|
@@ -18624,8 +18981,8 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
18624
18981
|
const gitDir = findGitDir(cwd);
|
|
18625
18982
|
if (!gitDir) return void 0;
|
|
18626
18983
|
try {
|
|
18627
|
-
const headPath =
|
|
18628
|
-
const head =
|
|
18984
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
18985
|
+
const head = fs7.readFileSync(headPath, "utf8").trim();
|
|
18629
18986
|
if (head.startsWith("ref:")) {
|
|
18630
18987
|
const refPath = head.replace("ref:", "").trim();
|
|
18631
18988
|
const match = refPath.match(/^refs\/heads\/(.+)$/);
|
|
@@ -18662,8 +19019,8 @@ function nanosecondsToMs(ns) {
|
|
|
18662
19019
|
}
|
|
18663
19020
|
|
|
18664
19021
|
// src/utils/metadata.ts
|
|
18665
|
-
import * as
|
|
18666
|
-
import * as
|
|
19022
|
+
import * as fs8 from "fs";
|
|
19023
|
+
import * as path9 from "path";
|
|
18667
19024
|
var versionCache = /* @__PURE__ */ new Map();
|
|
18668
19025
|
function readPackageVersion(root) {
|
|
18669
19026
|
if (versionCache.has(root)) {
|
|
@@ -18674,18 +19031,18 @@ function readPackageVersion(root) {
|
|
|
18674
19031
|
return version;
|
|
18675
19032
|
}
|
|
18676
19033
|
function findPackageVersion(startDir) {
|
|
18677
|
-
let current =
|
|
19034
|
+
let current = path9.resolve(startDir);
|
|
18678
19035
|
while (true) {
|
|
18679
|
-
const pkgPath =
|
|
19036
|
+
const pkgPath = path9.join(current, "package.json");
|
|
18680
19037
|
try {
|
|
18681
|
-
if (
|
|
18682
|
-
const raw =
|
|
19038
|
+
if (fs8.existsSync(pkgPath)) {
|
|
19039
|
+
const raw = fs8.readFileSync(pkgPath, "utf8");
|
|
18683
19040
|
const parsed = JSON.parse(raw);
|
|
18684
19041
|
return parsed.version;
|
|
18685
19042
|
}
|
|
18686
19043
|
} catch {
|
|
18687
19044
|
}
|
|
18688
|
-
const parent =
|
|
19045
|
+
const parent = path9.dirname(current);
|
|
18689
19046
|
if (parent === current) {
|
|
18690
19047
|
return void 0;
|
|
18691
19048
|
}
|
|
@@ -19555,12 +19912,22 @@ function listScenarios(args, _deps) {
|
|
|
19555
19912
|
const { testCases, format } = args;
|
|
19556
19913
|
if (format === "json") {
|
|
19557
19914
|
const items = testCases.map((tc) => ({
|
|
19915
|
+
id: tc.id,
|
|
19558
19916
|
scenario: tc.story.scenario,
|
|
19559
19917
|
status: tc.status,
|
|
19560
19918
|
sourceFile: tc.sourceFile,
|
|
19561
19919
|
sourceLine: tc.sourceLine,
|
|
19920
|
+
suitePath: tc.story.suitePath ?? tc.titlePath.slice(0, -1),
|
|
19562
19921
|
tags: tc.tags,
|
|
19563
|
-
|
|
19922
|
+
tickets: tc.story.tickets ?? [],
|
|
19923
|
+
covers: tc.story.covers ?? [],
|
|
19924
|
+
durationMs: tc.durationMs,
|
|
19925
|
+
error: tc.errorMessage ? {
|
|
19926
|
+
message: tc.errorMessage,
|
|
19927
|
+
stack: tc.errorStack
|
|
19928
|
+
} : void 0,
|
|
19929
|
+
steps: tc.story.steps.map((step, index) => toScenarioStep(step, index, tc)),
|
|
19930
|
+
docKinds: collectDocKinds(tc)
|
|
19564
19931
|
}));
|
|
19565
19932
|
return JSON.stringify(items, null, 2);
|
|
19566
19933
|
}
|
|
@@ -19633,10 +20000,730 @@ function listScenarios(args, _deps) {
|
|
|
19633
20000
|
];
|
|
19634
20001
|
return lines.join("\n");
|
|
19635
20002
|
}
|
|
20003
|
+
function toScenarioStep(step, index, testCase) {
|
|
20004
|
+
const result = testCase.stepResults.find(
|
|
20005
|
+
(candidate) => candidate.index === index || candidate.stepId === step.id
|
|
20006
|
+
);
|
|
20007
|
+
return {
|
|
20008
|
+
id: step.id,
|
|
20009
|
+
index,
|
|
20010
|
+
keyword: step.keyword,
|
|
20011
|
+
text: step.text,
|
|
20012
|
+
status: result?.status ?? testCase.status,
|
|
20013
|
+
durationMs: result?.durationMs ?? step.durationMs ?? 0,
|
|
20014
|
+
errorMessage: result?.errorMessage,
|
|
20015
|
+
mode: step.mode,
|
|
20016
|
+
docKinds: (step.docs ?? []).map((doc) => doc.kind)
|
|
20017
|
+
};
|
|
20018
|
+
}
|
|
20019
|
+
function collectDocKinds(testCase) {
|
|
20020
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
20021
|
+
for (const doc of testCase.story.docs ?? []) {
|
|
20022
|
+
kinds.add(doc.kind);
|
|
20023
|
+
}
|
|
20024
|
+
for (const step of testCase.story.steps) {
|
|
20025
|
+
for (const doc of step.docs ?? []) {
|
|
20026
|
+
kinds.add(doc.kind);
|
|
20027
|
+
}
|
|
20028
|
+
}
|
|
20029
|
+
return [...kinds].sort();
|
|
20030
|
+
}
|
|
20031
|
+
|
|
20032
|
+
// src/review/conventions.ts
|
|
20033
|
+
var CHANGE_TAG_PREFIX = "change:";
|
|
20034
|
+
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
20035
|
+
var VALID_CHANGE_TYPES = /* @__PURE__ */ new Set([
|
|
20036
|
+
"feature",
|
|
20037
|
+
"bugfix",
|
|
20038
|
+
"refactor",
|
|
20039
|
+
"perf",
|
|
20040
|
+
"deps"
|
|
20041
|
+
]);
|
|
20042
|
+
var STAKEHOLDER_FILE = /(?:\.e2e\.)|(?:^|\/)e2e\/|(?:\.spec\.)/i;
|
|
20043
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
20044
|
+
"ts",
|
|
20045
|
+
"tsx",
|
|
20046
|
+
"js",
|
|
20047
|
+
"jsx",
|
|
20048
|
+
"mjs",
|
|
20049
|
+
"cjs",
|
|
20050
|
+
"py",
|
|
20051
|
+
"go",
|
|
20052
|
+
"rs",
|
|
20053
|
+
"kt",
|
|
20054
|
+
"kts",
|
|
20055
|
+
"java",
|
|
20056
|
+
"cs",
|
|
20057
|
+
"rb"
|
|
20058
|
+
]);
|
|
20059
|
+
var TEST_INFIX = /\.(?:story\.)?(?:int\.|e2e\.|unit\.)?(?:test|spec|cy)\.[a-z]+$/i;
|
|
20060
|
+
function deriveAudience(sourceFile, tags) {
|
|
20061
|
+
const override = tags.map((t) => t.toLowerCase()).find((t) => t.startsWith(AUDIENCE_TAG_PREFIX));
|
|
20062
|
+
if (override) {
|
|
20063
|
+
const value = override.slice(AUDIENCE_TAG_PREFIX.length);
|
|
20064
|
+
if (value === "stakeholder" || value === "engineer") return value;
|
|
20065
|
+
}
|
|
20066
|
+
return STAKEHOLDER_FILE.test(sourceFile) ? "stakeholder" : "engineer";
|
|
20067
|
+
}
|
|
20068
|
+
function deriveChangeType(tags) {
|
|
20069
|
+
for (const tag of tags) {
|
|
20070
|
+
const lower = tag.toLowerCase();
|
|
20071
|
+
if (lower.startsWith(CHANGE_TAG_PREFIX)) {
|
|
20072
|
+
const value = lower.slice(CHANGE_TAG_PREFIX.length);
|
|
20073
|
+
if (VALID_CHANGE_TYPES.has(value)) return value;
|
|
20074
|
+
}
|
|
20075
|
+
}
|
|
20076
|
+
return "unknown";
|
|
20077
|
+
}
|
|
20078
|
+
function extensionOf(path11) {
|
|
20079
|
+
const base = path11.split("/").pop() ?? path11;
|
|
20080
|
+
const dot = base.lastIndexOf(".");
|
|
20081
|
+
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
20082
|
+
}
|
|
20083
|
+
function isTestFile(path11) {
|
|
20084
|
+
return TEST_INFIX.test(path11);
|
|
20085
|
+
}
|
|
20086
|
+
function isReviewableSource(path11) {
|
|
20087
|
+
if (isTestFile(path11)) return false;
|
|
20088
|
+
if (path11.endsWith(".d.ts")) return false;
|
|
20089
|
+
return CODE_EXTENSIONS.has(extensionOf(path11));
|
|
20090
|
+
}
|
|
20091
|
+
function testBaseKey(testFile) {
|
|
20092
|
+
return testFile.replace(TEST_INFIX, "");
|
|
20093
|
+
}
|
|
20094
|
+
function sourceBaseKey(sourceFile) {
|
|
20095
|
+
const dot = sourceFile.lastIndexOf(".");
|
|
20096
|
+
const slash = sourceFile.lastIndexOf("/");
|
|
20097
|
+
return dot > slash ? sourceFile.slice(0, dot) : sourceFile;
|
|
20098
|
+
}
|
|
20099
|
+
|
|
20100
|
+
// src/review/build-review.ts
|
|
20101
|
+
var STRENGTH_RANK = {
|
|
20102
|
+
none: 0,
|
|
20103
|
+
weak: 1,
|
|
20104
|
+
moderate: 2,
|
|
20105
|
+
strong: 3
|
|
20106
|
+
};
|
|
20107
|
+
var INTENT_SECTION_TITLE = /\b(why|intent|approach|rationale|reasoning)\b/i;
|
|
20108
|
+
function findDoc(docs, predicate) {
|
|
20109
|
+
if (!docs) return void 0;
|
|
20110
|
+
for (const doc of docs) {
|
|
20111
|
+
if (predicate(doc)) return doc;
|
|
20112
|
+
const nested = findDoc(doc.children, predicate);
|
|
20113
|
+
if (nested) return nested;
|
|
20114
|
+
}
|
|
20115
|
+
return void 0;
|
|
20116
|
+
}
|
|
20117
|
+
function anyDoc(docs, predicate) {
|
|
20118
|
+
return findDoc(docs, predicate) !== void 0;
|
|
20119
|
+
}
|
|
20120
|
+
function extractIntent(testCase) {
|
|
20121
|
+
const docs = testCase.story.docs;
|
|
20122
|
+
const section = findDoc(
|
|
20123
|
+
docs,
|
|
20124
|
+
(d) => d.kind === "section" && INTENT_SECTION_TITLE.test(d.title)
|
|
20125
|
+
);
|
|
20126
|
+
if (section && section.kind === "section") return section.markdown;
|
|
20127
|
+
const note = findDoc(docs, (d) => d.kind === "note");
|
|
20128
|
+
if (note && note.kind === "note") return note.text;
|
|
20129
|
+
return void 0;
|
|
20130
|
+
}
|
|
20131
|
+
function hasScreenshot(testCase) {
|
|
20132
|
+
if (testCase.attachments.some((a) => a.mediaType.startsWith("image/"))) {
|
|
20133
|
+
return true;
|
|
20134
|
+
}
|
|
20135
|
+
if (anyDoc(testCase.story.docs, (d) => d.kind === "screenshot")) return true;
|
|
20136
|
+
return testCase.story.steps.some(
|
|
20137
|
+
(step) => anyDoc(step.docs, (d) => d.kind === "screenshot")
|
|
20138
|
+
);
|
|
20139
|
+
}
|
|
20140
|
+
function hasOtelTrace(testCase) {
|
|
20141
|
+
return (testCase.story.otelSpans?.length ?? 0) > 0;
|
|
20142
|
+
}
|
|
20143
|
+
function gradeEvidence(testCase, audience) {
|
|
20144
|
+
if (testCase.status !== "passed") {
|
|
20145
|
+
return {
|
|
20146
|
+
strength: "none",
|
|
20147
|
+
reasons: [`test is ${testCase.status} \u2014 the proof does not hold`]
|
|
20148
|
+
};
|
|
20149
|
+
}
|
|
20150
|
+
const ev = testCase.evidence;
|
|
20151
|
+
const screenshot = hasScreenshot(testCase);
|
|
20152
|
+
const otel = hasOtelTrace(testCase);
|
|
20153
|
+
const isIntegration = /\.int\.test\./i.test(testCase.sourceFile);
|
|
20154
|
+
const mutation = ev?.mutationScorePct;
|
|
20155
|
+
const changedCov = ev?.changedLineCoveragePct;
|
|
20156
|
+
const strong2 = [];
|
|
20157
|
+
if (ev?.failingFirstVerified) {
|
|
20158
|
+
strong2.push("failing-first verified (red on base ref, green on head)");
|
|
20159
|
+
}
|
|
20160
|
+
if (typeof mutation === "number" && mutation >= 80) {
|
|
20161
|
+
strong2.push(`mutation score ${mutation}% (\u226580%)`);
|
|
20162
|
+
}
|
|
20163
|
+
if (screenshot && otel) {
|
|
20164
|
+
strong2.push("backed by screenshot + OTEL trace");
|
|
20165
|
+
} else if (audience === "stakeholder" && (screenshot || otel)) {
|
|
20166
|
+
strong2.push(`stakeholder proof: ${screenshot ? "screenshot" : "OTEL trace"}`);
|
|
20167
|
+
}
|
|
20168
|
+
if (strong2.length > 0) return { strength: "strong", reasons: strong2 };
|
|
20169
|
+
const moderate = [];
|
|
20170
|
+
if (screenshot) moderate.push("screenshot attached");
|
|
20171
|
+
if (otel) moderate.push("OTEL trace attached");
|
|
20172
|
+
if (typeof mutation === "number" && mutation >= 50) {
|
|
20173
|
+
moderate.push(`mutation score ${mutation}%`);
|
|
20174
|
+
}
|
|
20175
|
+
if (typeof changedCov === "number" && changedCov >= 80) {
|
|
20176
|
+
moderate.push(`changed-line coverage ${changedCov}%`);
|
|
20177
|
+
}
|
|
20178
|
+
if (isIntegration) moderate.push("integration-level test");
|
|
20179
|
+
if (moderate.length > 0) return { strength: "moderate", reasons: moderate };
|
|
20180
|
+
return {
|
|
20181
|
+
strength: "weak",
|
|
20182
|
+
reasons: [
|
|
20183
|
+
"passing test only \u2014 no corroborating evidence (add e2e proof, mutation score, or failing-first)"
|
|
20184
|
+
]
|
|
20185
|
+
};
|
|
20186
|
+
}
|
|
20187
|
+
function toClaim(testCase, changedSourcePaths) {
|
|
20188
|
+
const audience = deriveAudience(testCase.sourceFile, testCase.tags);
|
|
20189
|
+
const changeType = deriveChangeType(testCase.tags);
|
|
20190
|
+
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20191
|
+
const key = testBaseKey(testCase.sourceFile);
|
|
20192
|
+
const coversFiles = changedSourcePaths.filter(
|
|
20193
|
+
(path11) => sourceBaseKey(path11) === key
|
|
20194
|
+
);
|
|
20195
|
+
return {
|
|
20196
|
+
id: testCase.id,
|
|
20197
|
+
scenario: testCase.story.scenario,
|
|
20198
|
+
sourceFile: testCase.sourceFile,
|
|
20199
|
+
sourceLine: testCase.sourceLine,
|
|
20200
|
+
status: testCase.status,
|
|
20201
|
+
audience,
|
|
20202
|
+
changeType,
|
|
20203
|
+
strength,
|
|
20204
|
+
strengthReasons: reasons,
|
|
20205
|
+
intent: extractIntent(testCase),
|
|
20206
|
+
coversFiles,
|
|
20207
|
+
testCase
|
|
20208
|
+
};
|
|
20209
|
+
}
|
|
20210
|
+
function bandFor(claims) {
|
|
20211
|
+
if (claims.length === 0) return "uncovered";
|
|
20212
|
+
const maxRank = Math.max(...claims.map((c) => STRENGTH_RANK[c.strength]));
|
|
20213
|
+
return maxRank >= STRENGTH_RANK.moderate ? "covered" : "weak";
|
|
20214
|
+
}
|
|
20215
|
+
var AUDIENCE_ORDER = {
|
|
20216
|
+
stakeholder: 0,
|
|
20217
|
+
engineer: 1
|
|
20218
|
+
};
|
|
20219
|
+
function buildReview(run, context = { changedFiles: [] }) {
|
|
20220
|
+
const changedSource = context.changedFiles.filter(
|
|
20221
|
+
(f) => isReviewableSource(f.path)
|
|
20222
|
+
);
|
|
20223
|
+
const changedSourcePaths = changedSource.map((f) => f.path);
|
|
20224
|
+
const claims = run.testCases.map((tc) => toClaim(tc, changedSourcePaths));
|
|
20225
|
+
const changedFiles = changedSource.map((file) => {
|
|
20226
|
+
const covering = claims.filter((c) => c.coversFiles.includes(file.path));
|
|
20227
|
+
return {
|
|
20228
|
+
path: file.path,
|
|
20229
|
+
changeKind: file.changeKind,
|
|
20230
|
+
band: bandFor(covering),
|
|
20231
|
+
claims: covering.map((c) => ({
|
|
20232
|
+
id: c.id,
|
|
20233
|
+
scenario: c.scenario,
|
|
20234
|
+
strength: c.strength
|
|
20235
|
+
}))
|
|
20236
|
+
};
|
|
20237
|
+
});
|
|
20238
|
+
const sortedClaims = [...claims].sort((a, b) => {
|
|
20239
|
+
if (AUDIENCE_ORDER[a.audience] !== AUDIENCE_ORDER[b.audience]) {
|
|
20240
|
+
return AUDIENCE_ORDER[a.audience] - AUDIENCE_ORDER[b.audience];
|
|
20241
|
+
}
|
|
20242
|
+
if (STRENGTH_RANK[a.strength] !== STRENGTH_RANK[b.strength]) {
|
|
20243
|
+
return STRENGTH_RANK[a.strength] - STRENGTH_RANK[b.strength];
|
|
20244
|
+
}
|
|
20245
|
+
if (a.sourceFile !== b.sourceFile) {
|
|
20246
|
+
return a.sourceFile.localeCompare(b.sourceFile);
|
|
20247
|
+
}
|
|
20248
|
+
return a.scenario.localeCompare(b.scenario);
|
|
20249
|
+
});
|
|
20250
|
+
const bandRank = { uncovered: 0, weak: 1, covered: 2 };
|
|
20251
|
+
const sortedFiles = [...changedFiles].sort((a, b) => {
|
|
20252
|
+
if (bandRank[a.band] !== bandRank[b.band]) {
|
|
20253
|
+
return bandRank[a.band] - bandRank[b.band];
|
|
20254
|
+
}
|
|
20255
|
+
return a.path.localeCompare(b.path);
|
|
20256
|
+
});
|
|
20257
|
+
const summary = buildSummary2(sortedClaims, sortedFiles);
|
|
20258
|
+
return {
|
|
20259
|
+
run,
|
|
20260
|
+
context,
|
|
20261
|
+
summary,
|
|
20262
|
+
claims: sortedClaims,
|
|
20263
|
+
changedFiles: sortedFiles
|
|
20264
|
+
};
|
|
20265
|
+
}
|
|
20266
|
+
function buildSummary2(claims, changedFiles) {
|
|
20267
|
+
const byAudience = {
|
|
20268
|
+
stakeholder: 0,
|
|
20269
|
+
engineer: 0
|
|
20270
|
+
};
|
|
20271
|
+
const byStrength = {
|
|
20272
|
+
none: 0,
|
|
20273
|
+
weak: 0,
|
|
20274
|
+
moderate: 0,
|
|
20275
|
+
strong: 0
|
|
20276
|
+
};
|
|
20277
|
+
for (const claim of claims) {
|
|
20278
|
+
byAudience[claim.audience] += 1;
|
|
20279
|
+
byStrength[claim.strength] += 1;
|
|
20280
|
+
}
|
|
20281
|
+
return {
|
|
20282
|
+
totalClaims: claims.length,
|
|
20283
|
+
byAudience,
|
|
20284
|
+
byStrength,
|
|
20285
|
+
changedSourceFiles: changedFiles.length,
|
|
20286
|
+
uncovered: changedFiles.filter((f) => f.band === "uncovered").length,
|
|
20287
|
+
weaklyCovered: changedFiles.filter((f) => f.band === "weak").length,
|
|
20288
|
+
covered: changedFiles.filter((f) => f.band === "covered").length
|
|
20289
|
+
};
|
|
20290
|
+
}
|
|
20291
|
+
|
|
20292
|
+
// src/formatters/review-markdown.ts
|
|
20293
|
+
var STRENGTH_BADGE = {
|
|
20294
|
+
strong: "\u{1F7E2} strong",
|
|
20295
|
+
moderate: "\u{1F7E1} moderate",
|
|
20296
|
+
weak: "\u{1F7E0} weak",
|
|
20297
|
+
none: "\u{1F534} none"
|
|
20298
|
+
};
|
|
20299
|
+
function statusIcon2(status) {
|
|
20300
|
+
switch (status) {
|
|
20301
|
+
case "passed":
|
|
20302
|
+
return "\u2705";
|
|
20303
|
+
case "failed":
|
|
20304
|
+
return "\u274C";
|
|
20305
|
+
case "skipped":
|
|
20306
|
+
return "\u2298";
|
|
20307
|
+
default:
|
|
20308
|
+
return "\u2022";
|
|
20309
|
+
}
|
|
20310
|
+
}
|
|
20311
|
+
function escapeCell2(value) {
|
|
20312
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
20313
|
+
}
|
|
20314
|
+
function intentSummary(intent) {
|
|
20315
|
+
const firstLine = intent.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
20316
|
+
const trimmed = firstLine.trim();
|
|
20317
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}\u2026` : trimmed;
|
|
20318
|
+
}
|
|
20319
|
+
function renderTicket2(ticket) {
|
|
20320
|
+
return ticket.url ? `[${ticket.id}](${ticket.url})` : `\`${ticket.id}\``;
|
|
20321
|
+
}
|
|
20322
|
+
function renderUncoveredBand(lines, files) {
|
|
20323
|
+
const uncovered = files.filter((f) => f.band === "uncovered");
|
|
20324
|
+
if (uncovered.length === 0) return;
|
|
20325
|
+
lines.push(`## \u{1F534} Changed code with no evidence (${uncovered.length})`);
|
|
20326
|
+
lines.push("");
|
|
20327
|
+
lines.push("Start here \u2014 these changed source files have no claim or test behind them.");
|
|
20328
|
+
lines.push("");
|
|
20329
|
+
for (const file of uncovered) {
|
|
20330
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_`);
|
|
20331
|
+
}
|
|
20332
|
+
lines.push("");
|
|
20333
|
+
}
|
|
20334
|
+
function renderWeakBand(lines, files) {
|
|
20335
|
+
const weak = files.filter((f) => f.band === "weak");
|
|
20336
|
+
if (weak.length === 0) return;
|
|
20337
|
+
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
20338
|
+
lines.push("");
|
|
20339
|
+
for (const file of weak) {
|
|
20340
|
+
const covered = file.claims.map((c) => `${escapeCell2(c.scenario)} (${c.strength})`).join(", ");
|
|
20341
|
+
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
20342
|
+
}
|
|
20343
|
+
lines.push("");
|
|
20344
|
+
}
|
|
20345
|
+
function renderClaim(lines, claim) {
|
|
20346
|
+
lines.push(`### ${statusIcon2(claim.status)} ${claim.scenario}`);
|
|
20347
|
+
lines.push("");
|
|
20348
|
+
lines.push(`- File: \`${claim.sourceFile}:${claim.sourceLine}\``);
|
|
20349
|
+
if (claim.changeType !== "unknown") {
|
|
20350
|
+
lines.push(`- Change: \`${claim.changeType}\``);
|
|
20351
|
+
}
|
|
20352
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20353
|
+
if (tickets.length > 0) {
|
|
20354
|
+
lines.push(`- Tickets: ${tickets.map(renderTicket2).join(", ")}`);
|
|
20355
|
+
}
|
|
20356
|
+
lines.push(
|
|
20357
|
+
`- Evidence: ${STRENGTH_BADGE[claim.strength]} \u2014 ${claim.strengthReasons.join("; ")}`
|
|
20358
|
+
);
|
|
20359
|
+
if (claim.coversFiles.length > 0) {
|
|
20360
|
+
lines.push(
|
|
20361
|
+
`- Covers: ${claim.coversFiles.map((f) => `\`${f}\``).join(", ")}`
|
|
20362
|
+
);
|
|
20363
|
+
}
|
|
20364
|
+
if (claim.intent) {
|
|
20365
|
+
lines.push(`- Why: ${escapeCell2(intentSummary(claim.intent))}`);
|
|
20366
|
+
}
|
|
20367
|
+
lines.push("");
|
|
20368
|
+
}
|
|
20369
|
+
function renderAudienceSection(lines, title, claims) {
|
|
20370
|
+
if (claims.length === 0) return;
|
|
20371
|
+
lines.push(`## ${title} (${claims.length})`);
|
|
20372
|
+
lines.push("");
|
|
20373
|
+
for (const claim of claims) {
|
|
20374
|
+
renderClaim(lines, claim);
|
|
20375
|
+
}
|
|
20376
|
+
}
|
|
20377
|
+
var ReviewMarkdownFormatter = class {
|
|
20378
|
+
title;
|
|
20379
|
+
constructor(options = {}) {
|
|
20380
|
+
this.title = options.title ?? "Evidence Review";
|
|
20381
|
+
}
|
|
20382
|
+
format(review) {
|
|
20383
|
+
const lines = [];
|
|
20384
|
+
const { summary, context } = review;
|
|
20385
|
+
lines.push(`# ${this.title}`);
|
|
20386
|
+
lines.push("");
|
|
20387
|
+
if (context.baseRef || context.headRef) {
|
|
20388
|
+
lines.push(
|
|
20389
|
+
`Comparing \`${context.baseRef ?? "base"}\` \u2192 \`${context.headRef ?? "head"}\`.`
|
|
20390
|
+
);
|
|
20391
|
+
lines.push("");
|
|
20392
|
+
}
|
|
20393
|
+
lines.push("## Review priority");
|
|
20394
|
+
lines.push("");
|
|
20395
|
+
if (summary.changedSourceFiles === 0) {
|
|
20396
|
+
lines.push(
|
|
20397
|
+
"No changed source files supplied \u2014 showing claims and evidence only."
|
|
20398
|
+
);
|
|
20399
|
+
} else if (summary.uncovered > 0) {
|
|
20400
|
+
lines.push(
|
|
20401
|
+
`Review the ${summary.uncovered} unaccounted-for file(s) first: changed code with no evidence behind it.`
|
|
20402
|
+
);
|
|
20403
|
+
} else if (summary.weaklyCovered > 0) {
|
|
20404
|
+
lines.push(
|
|
20405
|
+
`No unaccounted-for changes. Review ${summary.weaklyCovered} weakly-covered file(s) next.`
|
|
20406
|
+
);
|
|
20407
|
+
} else {
|
|
20408
|
+
lines.push("Every changed source file is backed by at least moderate evidence.");
|
|
20409
|
+
}
|
|
20410
|
+
lines.push("");
|
|
20411
|
+
if (summary.changedSourceFiles > 0) {
|
|
20412
|
+
lines.push("| \u{1F534} Uncovered | \u{1F7E1} Weak | \u{1F7E2} Covered | Changed files |");
|
|
20413
|
+
lines.push("| ---: | ---: | ---: | ---: |");
|
|
20414
|
+
lines.push(
|
|
20415
|
+
`| ${summary.uncovered} | ${summary.weaklyCovered} | ${summary.covered} | ${summary.changedSourceFiles} |`
|
|
20416
|
+
);
|
|
20417
|
+
lines.push("");
|
|
20418
|
+
}
|
|
20419
|
+
lines.push("| Claims | Stakeholder | Engineer | Strong | Moderate | Weak | None |");
|
|
20420
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
|
|
20421
|
+
lines.push(
|
|
20422
|
+
`| ${summary.totalClaims} | ${summary.byAudience.stakeholder} | ${summary.byAudience.engineer} | ${summary.byStrength.strong} | ${summary.byStrength.moderate} | ${summary.byStrength.weak} | ${summary.byStrength.none} |`
|
|
20423
|
+
);
|
|
20424
|
+
lines.push("");
|
|
20425
|
+
renderUncoveredBand(lines, review.changedFiles);
|
|
20426
|
+
renderWeakBand(lines, review.changedFiles);
|
|
20427
|
+
renderAudienceSection(
|
|
20428
|
+
lines,
|
|
20429
|
+
"Stakeholder behaviour",
|
|
20430
|
+
review.claims.filter((c) => c.audience === "stakeholder")
|
|
20431
|
+
);
|
|
20432
|
+
renderAudienceSection(
|
|
20433
|
+
lines,
|
|
20434
|
+
"Engineer changes",
|
|
20435
|
+
review.claims.filter((c) => c.audience === "engineer")
|
|
20436
|
+
);
|
|
20437
|
+
return lines.join("\n").trimEnd();
|
|
20438
|
+
}
|
|
20439
|
+
};
|
|
20440
|
+
|
|
20441
|
+
// src/formatters/review-html.ts
|
|
20442
|
+
function escapeHtml3(value) {
|
|
20443
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
20444
|
+
}
|
|
20445
|
+
var STRENGTH_LABEL = {
|
|
20446
|
+
strong: "Strong",
|
|
20447
|
+
moderate: "Moderate",
|
|
20448
|
+
weak: "Weak",
|
|
20449
|
+
none: "None"
|
|
20450
|
+
};
|
|
20451
|
+
function statusIcon3(status) {
|
|
20452
|
+
switch (status) {
|
|
20453
|
+
case "passed":
|
|
20454
|
+
return "\u2705";
|
|
20455
|
+
case "failed":
|
|
20456
|
+
return "\u274C";
|
|
20457
|
+
case "skipped":
|
|
20458
|
+
return "\u2298";
|
|
20459
|
+
default:
|
|
20460
|
+
return "\u2022";
|
|
20461
|
+
}
|
|
20462
|
+
}
|
|
20463
|
+
function formatStep3(step) {
|
|
20464
|
+
return `<li><strong>${escapeHtml3(step.keyword)}</strong> ${escapeHtml3(step.text)}</li>`;
|
|
20465
|
+
}
|
|
20466
|
+
function inlineDoc(doc) {
|
|
20467
|
+
switch (doc.kind) {
|
|
20468
|
+
case "note":
|
|
20469
|
+
return escapeHtml3(doc.text);
|
|
20470
|
+
case "section":
|
|
20471
|
+
return `<strong>${escapeHtml3(doc.title)}</strong>: ${escapeHtml3(doc.markdown)}`;
|
|
20472
|
+
case "kv":
|
|
20473
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(String(doc.value))}`;
|
|
20474
|
+
case "code":
|
|
20475
|
+
return `${escapeHtml3(doc.label)}: <code>${escapeHtml3(doc.content)}</code>`;
|
|
20476
|
+
case "link":
|
|
20477
|
+
return `${escapeHtml3(doc.label)}: ${escapeHtml3(doc.url)}`;
|
|
20478
|
+
default:
|
|
20479
|
+
return escapeHtml3(doc.kind);
|
|
20480
|
+
}
|
|
20481
|
+
}
|
|
20482
|
+
function renderEvidenceArtifacts(testCase) {
|
|
20483
|
+
const parts = [];
|
|
20484
|
+
for (const att of testCase.attachments) {
|
|
20485
|
+
if (att.mediaType.startsWith("image/") && att.contentEncoding === "BASE64") {
|
|
20486
|
+
parts.push(
|
|
20487
|
+
`<img class="shot" alt="${escapeHtml3(att.name)}" src="data:${escapeHtml3(att.mediaType)};base64,${att.body}" />`
|
|
20488
|
+
);
|
|
20489
|
+
}
|
|
20490
|
+
}
|
|
20491
|
+
if ((testCase.story.otelSpans?.length ?? 0) > 0) {
|
|
20492
|
+
parts.push(
|
|
20493
|
+
`<p class="trace-note">\u{1F4E1} ${testCase.story.otelSpans.length} OTEL span(s) captured</p>`
|
|
20494
|
+
);
|
|
20495
|
+
}
|
|
20496
|
+
return parts.length > 0 ? `<div class="artifacts">${parts.join("")}</div>` : "";
|
|
20497
|
+
}
|
|
20498
|
+
function renderTicketPills(claim) {
|
|
20499
|
+
const tickets = claim.testCase.story.tickets ?? [];
|
|
20500
|
+
if (tickets.length === 0) return "";
|
|
20501
|
+
return `<div class="ticket-row">${tickets.map((ticket) => {
|
|
20502
|
+
const label = escapeHtml3(ticket.id);
|
|
20503
|
+
if (ticket.url) {
|
|
20504
|
+
return `<a class="ticket-pill" href="${escapeHtml3(ticket.url)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
|
20505
|
+
}
|
|
20506
|
+
return `<span class="ticket-pill">${label}</span>`;
|
|
20507
|
+
}).join("")}</div>`;
|
|
20508
|
+
}
|
|
20509
|
+
function renderClaimCard(claim) {
|
|
20510
|
+
const ticketSearch = (claim.testCase.story.tickets ?? []).map((ticket) => ticket.id).join(" ");
|
|
20511
|
+
const search = escapeHtml3(
|
|
20512
|
+
`${claim.scenario} ${claim.sourceFile} ${claim.changeType} ${claim.audience} ${claim.strength} ${ticketSearch}`
|
|
20513
|
+
).toLowerCase();
|
|
20514
|
+
const steps = claim.testCase.story.steps.length > 0 ? `<ul class="step-list">${claim.testCase.story.steps.map(formatStep3).join("")}</ul>` : "";
|
|
20515
|
+
const reasons = `<ul class="reasons">${claim.strengthReasons.map((r) => `<li>${escapeHtml3(r)}</li>`).join("")}</ul>`;
|
|
20516
|
+
const intent = claim.intent !== void 0 ? `<div class="intent"><span class="intent-label">Why</span> ${escapeHtml3(claim.intent)}</div>` : "";
|
|
20517
|
+
const covers = claim.coversFiles.length > 0 ? `<p class="covers">Covers ${claim.coversFiles.map((f) => `<code>${escapeHtml3(f)}</code>`).join(", ")}</p>` : "";
|
|
20518
|
+
const docs = (claim.testCase.story.docs ?? []).filter(
|
|
20519
|
+
(d) => d.kind === "section" || d.kind === "note"
|
|
20520
|
+
);
|
|
20521
|
+
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
20522
|
+
return `
|
|
20523
|
+
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
20524
|
+
<header class="claim-header">
|
|
20525
|
+
<div>
|
|
20526
|
+
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
20527
|
+
${claim.changeType !== "unknown" ? `<span class="change-pill">${escapeHtml3(claim.changeType)}</span>` : ""}
|
|
20528
|
+
<h3>${statusIcon3(claim.status)} ${escapeHtml3(claim.scenario)}</h3>
|
|
20529
|
+
<p class="source">${escapeHtml3(`${claim.sourceFile}:${claim.sourceLine}`)}</p>
|
|
20530
|
+
${renderTicketPills(claim)}
|
|
20531
|
+
</div>
|
|
20532
|
+
</header>
|
|
20533
|
+
${intent}${extraDocs}
|
|
20534
|
+
<div class="evidence-block">
|
|
20535
|
+
<span class="evidence-label">Evidence</span>
|
|
20536
|
+
${reasons}
|
|
20537
|
+
</div>
|
|
20538
|
+
${covers}
|
|
20539
|
+
${renderEvidenceArtifacts(claim.testCase)}
|
|
20540
|
+
${steps}
|
|
20541
|
+
</article>`;
|
|
20542
|
+
}
|
|
20543
|
+
function renderChangedFileRow(file) {
|
|
20544
|
+
const claims = file.claims.length > 0 ? file.claims.map((c) => `${escapeHtml3(c.scenario)} <em>(${c.strength})</em>`).join(", ") : "\u2014";
|
|
20545
|
+
return `<tr data-band="${file.band}">
|
|
20546
|
+
<td><span class="band-dot band-${file.band}"></span></td>
|
|
20547
|
+
<td><code>${escapeHtml3(file.path)}</code></td>
|
|
20548
|
+
<td>${escapeHtml3(file.changeKind)}</td>
|
|
20549
|
+
<td>${claims}</td>
|
|
20550
|
+
</tr>`;
|
|
20551
|
+
}
|
|
20552
|
+
function renderAudienceSection2(title, claims) {
|
|
20553
|
+
if (claims.length === 0) return "";
|
|
20554
|
+
return `<section class="audience-section">
|
|
20555
|
+
<h2>${escapeHtml3(title)} <span class="count">${claims.length}</span></h2>
|
|
20556
|
+
<div class="claim-list">${claims.map(renderClaimCard).join("\n")}</div>
|
|
20557
|
+
</section>`;
|
|
20558
|
+
}
|
|
20559
|
+
var REVIEW_CSS = `
|
|
20560
|
+
* { box-sizing: border-box; }
|
|
20561
|
+
body { margin: 0; font-family: var(--font-sans, system-ui, sans-serif); background: var(--background); color: var(--foreground); }
|
|
20562
|
+
main { max-width: 1100px; margin: 0 auto; padding: 32px 20px 80px; }
|
|
20563
|
+
h1, h2, h3, p { margin: 0; }
|
|
20564
|
+
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
20565
|
+
.subtle { color: var(--muted-foreground); margin-top: 6px; }
|
|
20566
|
+
.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); }
|
|
20567
|
+
.card, .claim-card, .summary-card, .panel { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius, 16px); }
|
|
20568
|
+
.hero-card { padding: 24px; margin-bottom: 20px; }
|
|
20569
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
20570
|
+
.summary-card { padding: 14px 16px; }
|
|
20571
|
+
.summary-card strong { display: block; font-size: 1.8rem; }
|
|
20572
|
+
.priority-banner { padding: 18px 20px; margin-bottom: 20px; background: linear-gradient(135deg, color-mix(in srgb, var(--destructive) 10%, transparent), var(--card)); }
|
|
20573
|
+
.panel { padding: 18px; margin-bottom: 24px; }
|
|
20574
|
+
table { width: 100%; border-collapse: collapse; }
|
|
20575
|
+
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
20576
|
+
th { color: var(--muted-foreground); font-weight: 600; }
|
|
20577
|
+
.band-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
|
|
20578
|
+
.band-uncovered { background: var(--destructive); }
|
|
20579
|
+
.band-weak { background: var(--warning, #b58900); }
|
|
20580
|
+
.band-covered { background: var(--success, #2e7d32); }
|
|
20581
|
+
.toolbar { position: sticky; top: 12px; z-index: 2; display: flex; flex-wrap: wrap; gap: 10px; padding: 14px; margin-bottom: 20px; }
|
|
20582
|
+
.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); }
|
|
20583
|
+
.toolbar button { border: 1px solid var(--border); background: var(--secondary); border-radius: 999px; padding: 10px 14px; font: inherit; cursor: pointer; color: var(--foreground); }
|
|
20584
|
+
.toolbar button.active { background: var(--foreground); color: var(--background); }
|
|
20585
|
+
.audience-section { margin-bottom: 28px; }
|
|
20586
|
+
.audience-section h2 { margin-bottom: 12px; }
|
|
20587
|
+
.count { color: var(--muted-foreground); font-weight: 400; }
|
|
20588
|
+
.claim-list { display: grid; gap: 14px; }
|
|
20589
|
+
.claim-card { padding: 18px; }
|
|
20590
|
+
.claim-header h3 { margin-top: 8px; }
|
|
20591
|
+
.source { color: var(--muted-foreground); font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.85rem; margin-top: 4px; }
|
|
20592
|
+
.ticket-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
20593
|
+
.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; }
|
|
20594
|
+
.ticket-pill:hover { color: var(--foreground); border-color: var(--muted-foreground); }
|
|
20595
|
+
.strength-badge, .change-pill { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 0.8rem; margin-right: 6px; }
|
|
20596
|
+
.change-pill { background: var(--secondary); }
|
|
20597
|
+
.strength-strong { background: color-mix(in srgb, var(--success, #2e7d32) 18%, transparent); color: var(--success, #2e7d32); }
|
|
20598
|
+
.strength-moderate { background: color-mix(in srgb, var(--warning, #b58900) 20%, transparent); color: var(--warning, #b58900); }
|
|
20599
|
+
.strength-weak { background: color-mix(in srgb, #d2691e 20%, transparent); color: #b5530a; }
|
|
20600
|
+
.strength-none { background: color-mix(in srgb, var(--destructive) 16%, transparent); color: var(--destructive); }
|
|
20601
|
+
.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; }
|
|
20602
|
+
.intent-label { font-weight: 700; margin-right: 6px; }
|
|
20603
|
+
.evidence-block { margin-top: 10px; }
|
|
20604
|
+
.evidence-label { font-weight: 600; color: var(--muted-foreground); }
|
|
20605
|
+
.reasons { margin: 6px 0 0; padding-left: 18px; }
|
|
20606
|
+
.covers { color: var(--muted-foreground); margin-top: 8px; font-size: 0.9rem; }
|
|
20607
|
+
.artifacts { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
|
|
20608
|
+
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
20609
|
+
.trace-note { color: var(--muted-foreground); }
|
|
20610
|
+
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
20611
|
+
`;
|
|
20612
|
+
var JS_THEME_TOGGLE2 = `
|
|
20613
|
+
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
20614
|
+
function getEffectiveTheme() { var s = localStorage.getItem('review-theme'); return (s === 'dark' || s === 'light') ? s : getSystemTheme(); }
|
|
20615
|
+
function toggleTheme() { var n = getEffectiveTheme() === 'dark' ? 'light' : 'dark'; localStorage.setItem('review-theme', n); applyTheme(n); }
|
|
20616
|
+
function applyTheme(t) {
|
|
20617
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
20618
|
+
var b = document.querySelector('.theme-toggle');
|
|
20619
|
+
if (b) { b.textContent = t === 'dark' ? '\\u2600\\ufe0f' : '\\ud83c\\udf19'; }
|
|
20620
|
+
}
|
|
20621
|
+
`;
|
|
20622
|
+
var ReviewHtmlFormatter = class {
|
|
20623
|
+
title;
|
|
20624
|
+
theme;
|
|
20625
|
+
darkMode;
|
|
20626
|
+
constructor(options = {}) {
|
|
20627
|
+
this.title = options.title ?? "Evidence Review";
|
|
20628
|
+
this.theme = resolveTheme(options.theme ?? "default");
|
|
20629
|
+
this.darkMode = options.darkMode ?? true;
|
|
20630
|
+
}
|
|
20631
|
+
format(review) {
|
|
20632
|
+
const { summary, context } = review;
|
|
20633
|
+
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.";
|
|
20634
|
+
const changedFilesPanel = summary.changedSourceFiles > 0 ? `<section class="panel">
|
|
20635
|
+
<h2>Changed files</h2>
|
|
20636
|
+
<table>
|
|
20637
|
+
<thead><tr><th></th><th>File</th><th>Change</th><th>Evidence</th></tr></thead>
|
|
20638
|
+
<tbody>${review.changedFiles.map(renderChangedFileRow).join("")}</tbody>
|
|
20639
|
+
</table>
|
|
20640
|
+
</section>` : "";
|
|
20641
|
+
const themeToggleHtml = this.darkMode ? `<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme"></button>` : "";
|
|
20642
|
+
const themeInitJs = this.darkMode ? `${JS_THEME_TOGGLE2}
|
|
20643
|
+
applyTheme(getEffectiveTheme());` : "";
|
|
20644
|
+
const themeAttr = this.darkMode ? ' data-theme="light"' : "";
|
|
20645
|
+
const refsLine = context.baseRef || context.headRef ? `<p class="subtle">Comparing ${escapeHtml3(context.baseRef ?? "base")} \u2192 ${escapeHtml3(context.headRef ?? "head")}</p>` : "";
|
|
20646
|
+
return `<!doctype html>
|
|
20647
|
+
<html lang="en"${themeAttr}>
|
|
20648
|
+
<head>
|
|
20649
|
+
<meta charset="utf-8" />
|
|
20650
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20651
|
+
<title>${escapeHtml3(this.title)}</title>
|
|
20652
|
+
<style>
|
|
20653
|
+
${this.theme.css}
|
|
20654
|
+
${REVIEW_CSS}
|
|
20655
|
+
</style>
|
|
20656
|
+
</head>
|
|
20657
|
+
<body>
|
|
20658
|
+
<main>
|
|
20659
|
+
<div class="hero-card card">
|
|
20660
|
+
<div class="review-header">
|
|
20661
|
+
<h1>${escapeHtml3(this.title)}</h1>
|
|
20662
|
+
${themeToggleHtml}
|
|
20663
|
+
</div>
|
|
20664
|
+
${refsLine}
|
|
20665
|
+
</div>
|
|
20666
|
+
<section class="summary-grid">
|
|
20667
|
+
<div class="summary-card"><strong>${summary.uncovered}</strong><span>\u{1F534} Uncovered</span></div>
|
|
20668
|
+
<div class="summary-card"><strong>${summary.weaklyCovered}</strong><span>\u{1F7E1} Weak</span></div>
|
|
20669
|
+
<div class="summary-card"><strong>${summary.covered}</strong><span>\u{1F7E2} Covered</span></div>
|
|
20670
|
+
<div class="summary-card"><strong>${summary.totalClaims}</strong><span>Claims</span></div>
|
|
20671
|
+
<div class="summary-card"><strong>${summary.byStrength.strong}</strong><span>Strong</span></div>
|
|
20672
|
+
<div class="summary-card"><strong>${summary.byStrength.weak + summary.byStrength.none}</strong><span>Weak/None</span></div>
|
|
20673
|
+
</section>
|
|
20674
|
+
<section class="card priority-banner">
|
|
20675
|
+
<h2>Review priority</h2>
|
|
20676
|
+
<p class="subtle">${escapeHtml3(priority)}</p>
|
|
20677
|
+
</section>
|
|
20678
|
+
${changedFilesPanel}
|
|
20679
|
+
<section class="toolbar">
|
|
20680
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
20681
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
20682
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
20683
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
20684
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
20685
|
+
</section>
|
|
20686
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
20687
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
20688
|
+
</main>
|
|
20689
|
+
<script>
|
|
20690
|
+
${themeInitJs}
|
|
20691
|
+
const input = document.querySelector('input[type="search"]');
|
|
20692
|
+
const buttons = Array.from(document.querySelectorAll('[data-filter]'));
|
|
20693
|
+
const cards = Array.from(document.querySelectorAll('.claim-card'));
|
|
20694
|
+
let activeFilter = 'all';
|
|
20695
|
+
function applyFilters() {
|
|
20696
|
+
const query = (input.value || '').trim().toLowerCase();
|
|
20697
|
+
cards.forEach((card) => {
|
|
20698
|
+
const audience = card.getAttribute('data-audience');
|
|
20699
|
+
const strength = card.getAttribute('data-strength');
|
|
20700
|
+
const haystack = card.getAttribute('data-search') || '';
|
|
20701
|
+
let matchesFilter = activeFilter === 'all'
|
|
20702
|
+
|| audience === activeFilter
|
|
20703
|
+
|| (activeFilter === 'weak' && (strength === 'weak' || strength === 'none'));
|
|
20704
|
+
const matchesSearch = !query || haystack.includes(query);
|
|
20705
|
+
card.style.display = matchesFilter && matchesSearch ? '' : 'none';
|
|
20706
|
+
});
|
|
20707
|
+
}
|
|
20708
|
+
input.addEventListener('input', applyFilters);
|
|
20709
|
+
buttons.forEach((button) => {
|
|
20710
|
+
button.addEventListener('click', () => {
|
|
20711
|
+
activeFilter = button.getAttribute('data-filter');
|
|
20712
|
+
buttons.forEach((b) => b.classList.toggle('active', b === button));
|
|
20713
|
+
applyFilters();
|
|
20714
|
+
});
|
|
20715
|
+
});
|
|
20716
|
+
applyFilters();
|
|
20717
|
+
</script>
|
|
20718
|
+
</body>
|
|
20719
|
+
</html>`;
|
|
20720
|
+
}
|
|
20721
|
+
};
|
|
19636
20722
|
|
|
19637
20723
|
// src/index.ts
|
|
19638
20724
|
var FORMAT_EXTENSIONS = {
|
|
19639
20725
|
astro: ".md",
|
|
20726
|
+
"behavior-manifest-json": ".behavior-manifest.json",
|
|
19640
20727
|
markdown: ".md",
|
|
19641
20728
|
html: ".html",
|
|
19642
20729
|
"cucumber-html": ".cucumber.html",
|
|
@@ -19644,6 +20731,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
19644
20731
|
"cucumber-json": ".cucumber.json",
|
|
19645
20732
|
"cucumber-messages": ".ndjson",
|
|
19646
20733
|
confluence: ".adf.json",
|
|
20734
|
+
"scenario-index-json": ".scenarios-index.json",
|
|
19647
20735
|
"story-report-json": ".story-report.json"
|
|
19648
20736
|
};
|
|
19649
20737
|
var TEST_EXTENSIONS = [
|
|
@@ -19671,11 +20759,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19671
20759
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19672
20760
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
19673
20761
|
if (mode === "aggregated") {
|
|
19674
|
-
return toPosix(
|
|
20762
|
+
return toPosix(path10.join(baseOutputDir, `${effectiveName}${ext}`));
|
|
19675
20763
|
}
|
|
19676
20764
|
const normalizedSource = toPosix(sourceFile);
|
|
19677
|
-
const dirOfSource =
|
|
19678
|
-
let baseName =
|
|
20765
|
+
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
20766
|
+
let baseName = path10.posix.basename(normalizedSource);
|
|
19679
20767
|
for (const testExt of TEST_EXTENSIONS) {
|
|
19680
20768
|
if (baseName.endsWith(testExt)) {
|
|
19681
20769
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -19684,9 +20772,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
19684
20772
|
}
|
|
19685
20773
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
19686
20774
|
if (colocatedStyle === "adjacent") {
|
|
19687
|
-
return toPosix(
|
|
20775
|
+
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
19688
20776
|
}
|
|
19689
|
-
return toPosix(
|
|
20777
|
+
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
19690
20778
|
}
|
|
19691
20779
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
19692
20780
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -19772,6 +20860,12 @@ var ReportGenerator = class {
|
|
|
19772
20860
|
storyReportJson: {
|
|
19773
20861
|
pretty: options.storyReportJson?.pretty ?? true
|
|
19774
20862
|
},
|
|
20863
|
+
scenarioIndexJson: {
|
|
20864
|
+
pretty: options.scenarioIndexJson?.pretty ?? true
|
|
20865
|
+
},
|
|
20866
|
+
behaviorManifestJson: {
|
|
20867
|
+
pretty: options.behaviorManifestJson?.pretty ?? true
|
|
20868
|
+
},
|
|
19775
20869
|
cucumberMessages: {
|
|
19776
20870
|
uriStrategy: options.cucumberMessages?.uriStrategy ?? "sourceFile",
|
|
19777
20871
|
includeSynthetics: options.cucumberMessages?.includeSynthetics ?? true,
|
|
@@ -19887,8 +20981,8 @@ var ReportGenerator = class {
|
|
|
19887
20981
|
if (astroPaths) {
|
|
19888
20982
|
for (const mdPath of astroPaths) {
|
|
19889
20983
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
19890
|
-
const mdDir =
|
|
19891
|
-
const assetsDir =
|
|
20984
|
+
const mdDir = path10.dirname(mdPath);
|
|
20985
|
+
const assetsDir = path10.resolve(this.options.astro.assetsDir);
|
|
19892
20986
|
const result = copyMarkdownAssets({
|
|
19893
20987
|
markdown: content,
|
|
19894
20988
|
markdownDir: mdDir,
|
|
@@ -19919,9 +21013,9 @@ var ReportGenerator = class {
|
|
|
19919
21013
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
19920
21014
|
const ext = FORMAT_EXTENSIONS[format];
|
|
19921
21015
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
19922
|
-
const outputPath = toPosix(
|
|
21016
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, `${effectiveName}${ext}`));
|
|
19923
21017
|
const content = await this.formatContent(run, format);
|
|
19924
|
-
const dir =
|
|
21018
|
+
const dir = path10.dirname(outputPath);
|
|
19925
21019
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19926
21020
|
await this.deps.writeFile(outputPath, content);
|
|
19927
21021
|
return [outputPath];
|
|
@@ -19933,7 +21027,7 @@ var ReportGenerator = class {
|
|
|
19933
21027
|
testCases
|
|
19934
21028
|
};
|
|
19935
21029
|
const content = await this.formatContent(groupRun, format);
|
|
19936
|
-
const dir =
|
|
21030
|
+
const dir = path10.dirname(outputPath);
|
|
19937
21031
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
19938
21032
|
await this.deps.writeFile(outputPath, content);
|
|
19939
21033
|
writtenPaths.push(outputPath);
|
|
@@ -20046,6 +21140,18 @@ var ReportGenerator = class {
|
|
|
20046
21140
|
});
|
|
20047
21141
|
return formatter.format(run);
|
|
20048
21142
|
}
|
|
21143
|
+
case "scenario-index-json": {
|
|
21144
|
+
const formatter = new ScenarioIndexJsonFormatter({
|
|
21145
|
+
pretty: this.options.scenarioIndexJson.pretty
|
|
21146
|
+
});
|
|
21147
|
+
return formatter.format(run);
|
|
21148
|
+
}
|
|
21149
|
+
case "behavior-manifest-json": {
|
|
21150
|
+
const formatter = new BehaviorManifestJsonFormatter({
|
|
21151
|
+
pretty: this.options.behaviorManifestJson.pretty
|
|
21152
|
+
});
|
|
21153
|
+
return formatter.format(run);
|
|
21154
|
+
}
|
|
20049
21155
|
default:
|
|
20050
21156
|
throw new Error(`Unknown format: ${format}`);
|
|
20051
21157
|
}
|
|
@@ -20062,7 +21168,7 @@ async function generateRunComparison(args) {
|
|
|
20062
21168
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
20063
21169
|
for (const format of args.formats) {
|
|
20064
21170
|
const ext = format === "html" ? ".html" : ".md";
|
|
20065
|
-
const outputPath = toPosix(
|
|
21171
|
+
const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
|
|
20066
21172
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
20067
21173
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
20068
21174
|
files.push(outputPath);
|
|
@@ -20083,6 +21189,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
20083
21189
|
}
|
|
20084
21190
|
export {
|
|
20085
21191
|
AstroFormatter,
|
|
21192
|
+
BehaviorManifestJsonFormatter,
|
|
20086
21193
|
ConfluenceFormatter,
|
|
20087
21194
|
CucumberHtmlFormatter,
|
|
20088
21195
|
CucumberJsonFormatter,
|
|
@@ -20096,29 +21203,37 @@ export {
|
|
|
20096
21203
|
MIN_PERF_SAMPLES,
|
|
20097
21204
|
MarkdownFormatter,
|
|
20098
21205
|
ReportGenerator,
|
|
21206
|
+
ReviewHtmlFormatter,
|
|
21207
|
+
ReviewMarkdownFormatter,
|
|
20099
21208
|
RunDiffHtmlFormatter,
|
|
20100
21209
|
RunDiffMarkdownFormatter,
|
|
20101
21210
|
STORY_META_KEY,
|
|
20102
21211
|
STORY_REPORT_SCHEMA_MAJOR,
|
|
20103
21212
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21213
|
+
ScenarioIndexJsonFormatter,
|
|
20104
21214
|
StoryReportJsonFormatter,
|
|
20105
21215
|
adaptJestRun,
|
|
20106
21216
|
adaptPlaywrightRun,
|
|
20107
21217
|
adaptVitestRun,
|
|
20108
21218
|
assertValidRun,
|
|
21219
|
+
buildReview,
|
|
20109
21220
|
bundleAssets,
|
|
20110
21221
|
calculateFlakiness,
|
|
20111
21222
|
calculateStability,
|
|
20112
21223
|
canonicalizeRun,
|
|
21224
|
+
classifyStatusChange,
|
|
20113
21225
|
clearVersionCache,
|
|
20114
21226
|
computeTestMetrics,
|
|
20115
21227
|
copyMarkdownAssets,
|
|
20116
21228
|
createPrCommentSummary,
|
|
20117
21229
|
createReportGenerator,
|
|
21230
|
+
deriveAudience,
|
|
21231
|
+
deriveChangeType,
|
|
20118
21232
|
deriveStepResults,
|
|
20119
21233
|
detectCI4 as detectCI,
|
|
20120
21234
|
detectPerformanceTrend,
|
|
20121
21235
|
diffRuns,
|
|
21236
|
+
diffStoryReports,
|
|
20122
21237
|
findGitDir,
|
|
20123
21238
|
formatDuration3 as formatDuration,
|
|
20124
21239
|
generateRunComparison,
|
|
@@ -20126,7 +21241,10 @@ export {
|
|
|
20126
21241
|
generateTestCaseId,
|
|
20127
21242
|
getAvailableThemes,
|
|
20128
21243
|
getCssOnlyThemes,
|
|
21244
|
+
gradeEvidence,
|
|
20129
21245
|
hasSufficientHistory,
|
|
21246
|
+
isReviewableSource,
|
|
21247
|
+
isTestFile,
|
|
20130
21248
|
listScenarios,
|
|
20131
21249
|
loadHistory,
|
|
20132
21250
|
mergeStepResults,
|
|
@@ -20143,21 +21261,26 @@ export {
|
|
|
20143
21261
|
readBranchName,
|
|
20144
21262
|
readGitSha,
|
|
20145
21263
|
readPackageVersion,
|
|
21264
|
+
regenerateArtifacts,
|
|
20146
21265
|
resolveAttachment,
|
|
20147
21266
|
resolveAttachments,
|
|
20148
21267
|
resolveTheme,
|
|
20149
21268
|
resolveTraceUrl,
|
|
20150
21269
|
rewriteAssetPaths,
|
|
20151
21270
|
saveHistory,
|
|
21271
|
+
scenariosCoveringPaths,
|
|
20152
21272
|
sendNotifications,
|
|
20153
21273
|
sendSlackNotification,
|
|
20154
21274
|
sendTeamsNotification,
|
|
20155
21275
|
sendWebhookNotification,
|
|
20156
21276
|
signBody,
|
|
20157
21277
|
slugify,
|
|
21278
|
+
startWatch,
|
|
20158
21279
|
stripAnsi,
|
|
21280
|
+
toBehaviorManifest,
|
|
20159
21281
|
toCIInfo,
|
|
20160
21282
|
toRawCIInfo,
|
|
21283
|
+
toScenarioIndex,
|
|
20161
21284
|
toStoryReport,
|
|
20162
21285
|
tryGetActiveOtelContext,
|
|
20163
21286
|
updateHistory,
|