executable-stories-formatters 1.3.0 → 1.4.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/cli.js +470 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +446 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -1
- package/dist/index.d.ts +233 -1
- package/dist/index.js +441 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,7 @@ __export(src_exports, {
|
|
|
60
60
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
61
61
|
adaptVitestRun: () => adaptVitestRun,
|
|
62
62
|
advanceState: () => advanceState,
|
|
63
|
+
assembleCodeDiff: () => assembleCodeDiff,
|
|
63
64
|
assertValidRun: () => assertValidRun,
|
|
64
65
|
buildCheck: () => buildCheck,
|
|
65
66
|
buildGoal: () => buildGoal,
|
|
@@ -72,8 +73,10 @@ __export(src_exports, {
|
|
|
72
73
|
canonicalizeRun: () => canonicalizeRun,
|
|
73
74
|
classifyStatusChange: () => classifyStatusChange,
|
|
74
75
|
clearVersionCache: () => clearVersionCache,
|
|
76
|
+
codeDiffDiagnostics: () => codeDiffDiagnostics,
|
|
75
77
|
computeTestMetrics: () => computeTestMetrics,
|
|
76
78
|
copyMarkdownAssets: () => copyMarkdownAssets,
|
|
79
|
+
createAnchor: () => createAnchor,
|
|
77
80
|
createPrCommentSummary: () => createPrCommentSummary,
|
|
78
81
|
createReportGenerator: () => createReportGenerator,
|
|
79
82
|
deriveAudience: () => deriveAudience,
|
|
@@ -108,6 +111,7 @@ __export(src_exports, {
|
|
|
108
111
|
normalizeVitestResults: () => normalizeVitestResults,
|
|
109
112
|
parseEnvelopes: () => parseEnvelopes,
|
|
110
113
|
parseNdjson: () => parseNdjson,
|
|
114
|
+
parseUnifiedDiff: () => parseUnifiedDiff,
|
|
111
115
|
publishConfluencePage: () => publishConfluencePage,
|
|
112
116
|
publishJiraIssue: () => publishJiraIssue,
|
|
113
117
|
readBranchName: () => readBranchName,
|
|
@@ -116,6 +120,7 @@ __export(src_exports, {
|
|
|
116
120
|
recordDeployment: () => recordDeployment,
|
|
117
121
|
regenerateArtifacts: () => regenerateArtifacts,
|
|
118
122
|
regenerateRun: () => regenerateRun,
|
|
123
|
+
relocateAnchor: () => relocateAnchor,
|
|
119
124
|
renderCheck: () => renderCheck,
|
|
120
125
|
renderGoal: () => renderGoal,
|
|
121
126
|
renderTriage: () => renderTriage,
|
|
@@ -7761,6 +7766,157 @@ function renderTriage(report, format) {
|
|
|
7761
7766
|
return lines.join("\n").trimEnd();
|
|
7762
7767
|
}
|
|
7763
7768
|
|
|
7769
|
+
// src/review/diff-anchor.ts
|
|
7770
|
+
var import_node_crypto6 = require("crypto");
|
|
7771
|
+
var CONTEXT_WINDOW = 3;
|
|
7772
|
+
var MAX_FUZZ = 2;
|
|
7773
|
+
var normalize = (line) => line.trim();
|
|
7774
|
+
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
7775
|
+
function stripPathPrefix(raw) {
|
|
7776
|
+
const path11 = raw.split(" ")[0].trim();
|
|
7777
|
+
if (path11 === "/dev/null") return void 0;
|
|
7778
|
+
return path11.replace(/^[ab]\//, "");
|
|
7779
|
+
}
|
|
7780
|
+
function parseUnifiedDiff(patch) {
|
|
7781
|
+
const files = [];
|
|
7782
|
+
let current;
|
|
7783
|
+
let hunk;
|
|
7784
|
+
let oldRemaining = 0;
|
|
7785
|
+
let newRemaining = 0;
|
|
7786
|
+
for (const line of patch.split("\n")) {
|
|
7787
|
+
if (hunk && (oldRemaining > 0 || newRemaining > 0)) {
|
|
7788
|
+
if (line.startsWith("\\")) continue;
|
|
7789
|
+
const kind = line.startsWith("+") ? "add" : line.startsWith("-") ? "del" : "context";
|
|
7790
|
+
hunk.lines.push({ kind, text: line.slice(1) });
|
|
7791
|
+
if (kind !== "add") oldRemaining--;
|
|
7792
|
+
if (kind !== "del") newRemaining--;
|
|
7793
|
+
continue;
|
|
7794
|
+
}
|
|
7795
|
+
hunk = void 0;
|
|
7796
|
+
if (line.startsWith("diff --git ")) {
|
|
7797
|
+
current = { hunks: [] };
|
|
7798
|
+
files.push(current);
|
|
7799
|
+
continue;
|
|
7800
|
+
}
|
|
7801
|
+
if (line.startsWith("--- ")) {
|
|
7802
|
+
if (!current || current.oldPath !== void 0 || current.hunks.length > 0) {
|
|
7803
|
+
current = { hunks: [] };
|
|
7804
|
+
files.push(current);
|
|
7805
|
+
}
|
|
7806
|
+
current.oldPath = stripPathPrefix(line.slice(4));
|
|
7807
|
+
continue;
|
|
7808
|
+
}
|
|
7809
|
+
if (line.startsWith("+++ ") && current) {
|
|
7810
|
+
current.newPath = stripPathPrefix(line.slice(4));
|
|
7811
|
+
continue;
|
|
7812
|
+
}
|
|
7813
|
+
const match = HUNK_HEADER.exec(line);
|
|
7814
|
+
if (match && current) {
|
|
7815
|
+
hunk = {
|
|
7816
|
+
oldStart: Number(match[1]),
|
|
7817
|
+
newStart: Number(match[3]),
|
|
7818
|
+
header: match[5] ?? "",
|
|
7819
|
+
lines: []
|
|
7820
|
+
};
|
|
7821
|
+
oldRemaining = match[2] === void 0 ? 1 : Number(match[2]);
|
|
7822
|
+
newRemaining = match[4] === void 0 ? 1 : Number(match[4]);
|
|
7823
|
+
current.hunks.push(hunk);
|
|
7824
|
+
}
|
|
7825
|
+
}
|
|
7826
|
+
return files;
|
|
7827
|
+
}
|
|
7828
|
+
function createAnchor(args) {
|
|
7829
|
+
const { file, hunkIndex, lineIndex } = args;
|
|
7830
|
+
const lines = file.hunks[hunkIndex].lines;
|
|
7831
|
+
if (lines[lineIndex].kind === "context") {
|
|
7832
|
+
throw new Error(`Line ${lineIndex} is a context line; anchors target changed lines`);
|
|
7833
|
+
}
|
|
7834
|
+
const changed = [];
|
|
7835
|
+
let end = lineIndex;
|
|
7836
|
+
while (end < lines.length && lines[end].kind !== "context") {
|
|
7837
|
+
changed.push({ kind: lines[end].kind, text: lines[end].text });
|
|
7838
|
+
end++;
|
|
7839
|
+
}
|
|
7840
|
+
const contextBefore = lines.slice(Math.max(0, lineIndex - CONTEXT_WINDOW), lineIndex).map((l) => l.text);
|
|
7841
|
+
const contextAfter = lines.slice(end, end + CONTEXT_WINDOW).map((l) => l.text);
|
|
7842
|
+
const hash = (0, import_node_crypto6.createHash)("sha256").update(
|
|
7843
|
+
JSON.stringify({
|
|
7844
|
+
changed: changed.map((l) => `${l.kind}:${normalize(l.text)}`),
|
|
7845
|
+
before: contextBefore.map(normalize),
|
|
7846
|
+
after: contextAfter.map(normalize)
|
|
7847
|
+
})
|
|
7848
|
+
).digest("hex");
|
|
7849
|
+
return {
|
|
7850
|
+
hash,
|
|
7851
|
+
file: file.newPath ?? file.oldPath ?? "",
|
|
7852
|
+
changed,
|
|
7853
|
+
contextBefore,
|
|
7854
|
+
contextAfter
|
|
7855
|
+
};
|
|
7856
|
+
}
|
|
7857
|
+
function changedRunCandidates(anchor, file, fileIndex) {
|
|
7858
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
7859
|
+
const out = [];
|
|
7860
|
+
file.hunks.forEach((hunk, hunkIndex) => {
|
|
7861
|
+
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
7862
|
+
for (let j = 0; j < anchor.changed.length; j++) {
|
|
7863
|
+
const line = hunk.lines[i + j];
|
|
7864
|
+
const want = anchor.changed[j];
|
|
7865
|
+
if (line.kind !== want.kind || normalize(line.text) !== normalize(want.text)) {
|
|
7866
|
+
continue outer;
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
out.push({ fileIndex, file: path11, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
7870
|
+
}
|
|
7871
|
+
});
|
|
7872
|
+
return out;
|
|
7873
|
+
}
|
|
7874
|
+
function contextMatches(anchor, candidate, fuzz) {
|
|
7875
|
+
const before = anchor.contextBefore.slice(Math.min(fuzz, anchor.contextBefore.length));
|
|
7876
|
+
const after = anchor.contextAfter.slice(
|
|
7877
|
+
0,
|
|
7878
|
+
Math.max(0, anchor.contextAfter.length - fuzz)
|
|
7879
|
+
);
|
|
7880
|
+
for (let i = 0; i < before.length; i++) {
|
|
7881
|
+
const line = candidate.lines[candidate.lineIndex - before.length + i];
|
|
7882
|
+
if (line === void 0 || normalize(line.text) !== normalize(before[i])) return false;
|
|
7883
|
+
}
|
|
7884
|
+
const end = candidate.lineIndex + anchor.changed.length;
|
|
7885
|
+
for (let i = 0; i < after.length; i++) {
|
|
7886
|
+
const line = candidate.lines[end + i];
|
|
7887
|
+
if (line === void 0 || normalize(line.text) !== normalize(after[i])) return false;
|
|
7888
|
+
}
|
|
7889
|
+
return true;
|
|
7890
|
+
}
|
|
7891
|
+
function resolveCandidates(anchor, candidates) {
|
|
7892
|
+
for (let fuzz = 0; fuzz <= MAX_FUZZ; fuzz++) {
|
|
7893
|
+
const matches = candidates.filter((c) => contextMatches(anchor, c, fuzz));
|
|
7894
|
+
if (matches.length === 1) {
|
|
7895
|
+
const m = matches[0];
|
|
7896
|
+
return {
|
|
7897
|
+
state: "anchored",
|
|
7898
|
+
fileIndex: m.fileIndex,
|
|
7899
|
+
file: m.file,
|
|
7900
|
+
hunkIndex: m.hunkIndex,
|
|
7901
|
+
lineIndex: m.lineIndex,
|
|
7902
|
+
lineCount: anchor.changed.length,
|
|
7903
|
+
fuzz
|
|
7904
|
+
};
|
|
7905
|
+
}
|
|
7906
|
+
if (matches.length > 1) return { state: "ambiguous" };
|
|
7907
|
+
}
|
|
7908
|
+
return void 0;
|
|
7909
|
+
}
|
|
7910
|
+
function relocateAnchor(anchor, files) {
|
|
7911
|
+
const own = [];
|
|
7912
|
+
const others = [];
|
|
7913
|
+
files.forEach((file, fileIndex) => {
|
|
7914
|
+
const isOwn = file.newPath === anchor.file || file.oldPath === anchor.file;
|
|
7915
|
+
(isOwn ? own : others).push(...changedRunCandidates(anchor, file, fileIndex));
|
|
7916
|
+
});
|
|
7917
|
+
return resolveCandidates(anchor, own) ?? resolveCandidates(anchor, others) ?? { state: "orphaned" };
|
|
7918
|
+
}
|
|
7919
|
+
|
|
7764
7920
|
// src/review/conventions.ts
|
|
7765
7921
|
var CHANGE_TAG_PREFIX = "change:";
|
|
7766
7922
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -7948,6 +8104,33 @@ var AUDIENCE_ORDER = {
|
|
|
7948
8104
|
stakeholder: 0,
|
|
7949
8105
|
engineer: 1
|
|
7950
8106
|
};
|
|
8107
|
+
function buildCodeDiff(input, run, context) {
|
|
8108
|
+
const files = parseUnifiedDiff(input.patch);
|
|
8109
|
+
const byId = new Map(run.testCases.map((tc) => [tc.id, tc]));
|
|
8110
|
+
return {
|
|
8111
|
+
title: input.title,
|
|
8112
|
+
patch: input.patch,
|
|
8113
|
+
patchUrl: input.patchUrl,
|
|
8114
|
+
baseLabel: input.baseLabel ?? context.baseRef,
|
|
8115
|
+
headLabel: input.headLabel ?? context.headRef,
|
|
8116
|
+
files,
|
|
8117
|
+
annotations: input.annotations.map((annotation) => ({
|
|
8118
|
+
anchorHash: annotation.anchor?.hash,
|
|
8119
|
+
text: annotation.text,
|
|
8120
|
+
label: annotation.label,
|
|
8121
|
+
resolution: annotation.anchor ? relocateAnchor(annotation.anchor, files) : { state: annotation.unresolved ?? "orphaned" },
|
|
8122
|
+
scenarios: (annotation.scenarioIds ?? []).map((id) => {
|
|
8123
|
+
const testCase = byId.get(id);
|
|
8124
|
+
return testCase ? {
|
|
8125
|
+
id,
|
|
8126
|
+
resolved: true,
|
|
8127
|
+
scenario: testCase.story.scenario,
|
|
8128
|
+
status: testCase.status
|
|
8129
|
+
} : { id, resolved: false };
|
|
8130
|
+
})
|
|
8131
|
+
}))
|
|
8132
|
+
};
|
|
8133
|
+
}
|
|
7951
8134
|
function buildReview(run, context = { changedFiles: [] }) {
|
|
7952
8135
|
const changedSource = context.changedFiles.filter(
|
|
7953
8136
|
(f) => isReviewableSource(f.path)
|
|
@@ -7992,9 +8175,31 @@ function buildReview(run, context = { changedFiles: [] }) {
|
|
|
7992
8175
|
context,
|
|
7993
8176
|
summary,
|
|
7994
8177
|
claims: sortedClaims,
|
|
7995
|
-
changedFiles: sortedFiles
|
|
8178
|
+
changedFiles: sortedFiles,
|
|
8179
|
+
codeDiffs: (context.codeDiffs ?? []).map((d) => buildCodeDiff(d, run, context))
|
|
7996
8180
|
};
|
|
7997
8181
|
}
|
|
8182
|
+
function codeDiffDiagnostics(review) {
|
|
8183
|
+
const issues = [];
|
|
8184
|
+
for (const evidence of review.codeDiffs) {
|
|
8185
|
+
evidence.annotations.forEach((annotation, i) => {
|
|
8186
|
+
const name = annotation.label ?? `annotation ${i + 1}`;
|
|
8187
|
+
if (annotation.resolution.state !== "anchored") {
|
|
8188
|
+
issues.push(
|
|
8189
|
+
`"${evidence.title}" ${name}: anchor is ${annotation.resolution.state} in the current patch`
|
|
8190
|
+
);
|
|
8191
|
+
}
|
|
8192
|
+
for (const ref of annotation.scenarios) {
|
|
8193
|
+
if (!ref.resolved) {
|
|
8194
|
+
issues.push(
|
|
8195
|
+
`"${evidence.title}" ${name}: cites scenario "${ref.id}" which is not in this run`
|
|
8196
|
+
);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
});
|
|
8200
|
+
}
|
|
8201
|
+
return issues;
|
|
8202
|
+
}
|
|
7998
8203
|
function buildSummary2(claims, changedFiles) {
|
|
7999
8204
|
const byAudience = {
|
|
8000
8205
|
stakeholder: 0,
|
|
@@ -8021,6 +8226,75 @@ function buildSummary2(claims, changedFiles) {
|
|
|
8021
8226
|
};
|
|
8022
8227
|
}
|
|
8023
8228
|
|
|
8229
|
+
// src/review/code-diff-sidecar.ts
|
|
8230
|
+
function locateMatches(file, match) {
|
|
8231
|
+
const out = [];
|
|
8232
|
+
file.hunks.forEach((hunk, hunkIndex) => {
|
|
8233
|
+
hunk.lines.forEach((line, lineIndex) => {
|
|
8234
|
+
if (line.kind !== "context" && line.text.includes(match)) {
|
|
8235
|
+
out.push({ hunkIndex, lineIndex });
|
|
8236
|
+
}
|
|
8237
|
+
});
|
|
8238
|
+
});
|
|
8239
|
+
return out;
|
|
8240
|
+
}
|
|
8241
|
+
function runStart(file, hunkIndex, lineIndex) {
|
|
8242
|
+
const lines = file.hunks[hunkIndex].lines;
|
|
8243
|
+
let start = lineIndex;
|
|
8244
|
+
while (start > 0 && lines[start - 1].kind !== "context") start--;
|
|
8245
|
+
return start;
|
|
8246
|
+
}
|
|
8247
|
+
function assembleCodeDiff(args) {
|
|
8248
|
+
const { sidecar, patch } = args;
|
|
8249
|
+
const files = parseUnifiedDiff(patch);
|
|
8250
|
+
const warnings = [];
|
|
8251
|
+
if (sidecar.patchUrl !== void 0 && !sidecar.patchUrl.startsWith("https://")) {
|
|
8252
|
+
warnings.push(
|
|
8253
|
+
`patchUrl "${sidecar.patchUrl}" is not https: \u2014 it will render as inert text, not a link`
|
|
8254
|
+
);
|
|
8255
|
+
}
|
|
8256
|
+
const annotations = sidecar.annotations.map(
|
|
8257
|
+
(entry) => {
|
|
8258
|
+
const base = {
|
|
8259
|
+
text: entry.text,
|
|
8260
|
+
label: entry.label,
|
|
8261
|
+
scenarioIds: entry.scenarioIds
|
|
8262
|
+
};
|
|
8263
|
+
const file = files.find(
|
|
8264
|
+
(f) => f.newPath === entry.file || f.oldPath === entry.file
|
|
8265
|
+
);
|
|
8266
|
+
const matches = file ? locateMatches(file, entry.match) : [];
|
|
8267
|
+
if (file && matches.length === 1) {
|
|
8268
|
+
const { hunkIndex, lineIndex } = matches[0];
|
|
8269
|
+
const anchor = createAnchor({
|
|
8270
|
+
file,
|
|
8271
|
+
hunkIndex,
|
|
8272
|
+
lineIndex: runStart(file, hunkIndex, lineIndex)
|
|
8273
|
+
});
|
|
8274
|
+
return { ...base, anchor };
|
|
8275
|
+
}
|
|
8276
|
+
warnings.push(
|
|
8277
|
+
!file ? `annotation "${entry.label ?? entry.match}": file "${entry.file}" is not in the patch` : matches.length === 0 ? `annotation "${entry.label ?? entry.match}": no changed line in "${entry.file}" contains "${entry.match}"` : `annotation "${entry.label ?? entry.match}": "${entry.match}" matches ${matches.length} changed lines in "${entry.file}" \u2014 make it unique`
|
|
8278
|
+
);
|
|
8279
|
+
return {
|
|
8280
|
+
...base,
|
|
8281
|
+
unresolved: file && matches.length > 1 ? "ambiguous" : "orphaned"
|
|
8282
|
+
};
|
|
8283
|
+
}
|
|
8284
|
+
);
|
|
8285
|
+
return {
|
|
8286
|
+
input: {
|
|
8287
|
+
title: sidecar.title,
|
|
8288
|
+
patch,
|
|
8289
|
+
patchUrl: sidecar.patchUrl,
|
|
8290
|
+
baseLabel: sidecar.baseLabel,
|
|
8291
|
+
headLabel: sidecar.headLabel,
|
|
8292
|
+
annotations
|
|
8293
|
+
},
|
|
8294
|
+
warnings
|
|
8295
|
+
};
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8024
8298
|
// src/formatters/review-markdown.ts
|
|
8025
8299
|
var STRENGTH_BADGE = {
|
|
8026
8300
|
strong: "\u{1F7E2} strong",
|
|
@@ -8106,6 +8380,73 @@ function renderAudienceSection(lines, title, claims) {
|
|
|
8106
8380
|
renderClaim(lines, claim);
|
|
8107
8381
|
}
|
|
8108
8382
|
}
|
|
8383
|
+
var MAX_PATCH_EMBED_BYTES = 64 * 1024;
|
|
8384
|
+
function fenceFor(content) {
|
|
8385
|
+
const runs = content.match(/`{3,}/g);
|
|
8386
|
+
const length = runs ? Math.max(...runs.map((r) => r.length)) + 1 : 3;
|
|
8387
|
+
return "`".repeat(length);
|
|
8388
|
+
}
|
|
8389
|
+
function renderCodeDiff(lines, evidence) {
|
|
8390
|
+
lines.push(`## Code diff evidence: ${evidence.title}`);
|
|
8391
|
+
lines.push("");
|
|
8392
|
+
if (evidence.baseLabel || evidence.headLabel) {
|
|
8393
|
+
lines.push(
|
|
8394
|
+
`Comparing \`${evidence.baseLabel ?? "base"}\` \u2192 \`${evidence.headLabel ?? "head"}\`.`
|
|
8395
|
+
);
|
|
8396
|
+
lines.push("");
|
|
8397
|
+
}
|
|
8398
|
+
if (evidence.patchUrl !== void 0) {
|
|
8399
|
+
lines.push(
|
|
8400
|
+
evidence.patchUrl.startsWith("https://") ? `Canonical patch: ${evidence.patchUrl}` : `Canonical patch: \`${evidence.patchUrl.replace(/`/g, "")}\``
|
|
8401
|
+
);
|
|
8402
|
+
lines.push("");
|
|
8403
|
+
}
|
|
8404
|
+
evidence.annotations.forEach((annotation, i) => {
|
|
8405
|
+
lines.push(`### ${annotation.label ?? `Annotation ${i + 1}`}`);
|
|
8406
|
+
lines.push("");
|
|
8407
|
+
lines.push(annotation.text);
|
|
8408
|
+
lines.push("");
|
|
8409
|
+
if (annotation.resolution.state === "orphaned") {
|
|
8410
|
+
lines.push(
|
|
8411
|
+
"> \u26A0\uFE0F Orphaned annotation \u2014 could not locate these lines in the current patch."
|
|
8412
|
+
);
|
|
8413
|
+
lines.push("");
|
|
8414
|
+
} else if (annotation.resolution.state === "ambiguous") {
|
|
8415
|
+
lines.push(
|
|
8416
|
+
"> \u26A0\uFE0F Ambiguous anchor \u2014 these lines appear in more than one place in the current patch."
|
|
8417
|
+
);
|
|
8418
|
+
lines.push("");
|
|
8419
|
+
} else if (annotation.resolution.file !== void 0) {
|
|
8420
|
+
lines.push(`Location: \`${annotation.resolution.file}\``);
|
|
8421
|
+
lines.push("");
|
|
8422
|
+
}
|
|
8423
|
+
if (annotation.scenarios.length === 0) {
|
|
8424
|
+
lines.push("_Not covered by a scenario._");
|
|
8425
|
+
} else {
|
|
8426
|
+
for (const ref of annotation.scenarios) {
|
|
8427
|
+
lines.push(
|
|
8428
|
+
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${escapeCell2(ref.scenario ?? ref.id)} (\`${ref.id}\`)` : `- \u26A0\uFE0F \`${ref.id}\` \u2014 unverified reference (scenario not in this run)`
|
|
8429
|
+
);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
lines.push("");
|
|
8433
|
+
});
|
|
8434
|
+
if (Buffer.byteLength(evidence.patch, "utf8") <= MAX_PATCH_EMBED_BYTES) {
|
|
8435
|
+
const fence = fenceFor(evidence.patch);
|
|
8436
|
+
lines.push("<details><summary>Raw patch (audit)</summary>");
|
|
8437
|
+
lines.push("");
|
|
8438
|
+
lines.push(`${fence}diff`);
|
|
8439
|
+
lines.push(evidence.patch.trimEnd());
|
|
8440
|
+
lines.push(fence);
|
|
8441
|
+
lines.push("");
|
|
8442
|
+
lines.push("</details>");
|
|
8443
|
+
} else {
|
|
8444
|
+
lines.push(
|
|
8445
|
+
`_Patch too large to embed${evidence.patchUrl ? " \u2014 see the canonical patch link above" : ""}._`
|
|
8446
|
+
);
|
|
8447
|
+
}
|
|
8448
|
+
lines.push("");
|
|
8449
|
+
}
|
|
8109
8450
|
var ReviewMarkdownFormatter = class {
|
|
8110
8451
|
title;
|
|
8111
8452
|
constructor(options = {}) {
|
|
@@ -8166,6 +8507,9 @@ var ReviewMarkdownFormatter = class {
|
|
|
8166
8507
|
"Engineer changes",
|
|
8167
8508
|
review.claims.filter((c) => c.audience === "engineer")
|
|
8168
8509
|
);
|
|
8510
|
+
for (const evidence of review.codeDiffs) {
|
|
8511
|
+
renderCodeDiff(lines, evidence);
|
|
8512
|
+
}
|
|
8169
8513
|
return lines.join("\n").trimEnd();
|
|
8170
8514
|
}
|
|
8171
8515
|
};
|
|
@@ -8252,7 +8596,7 @@ function renderClaimCard(claim) {
|
|
|
8252
8596
|
);
|
|
8253
8597
|
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
8254
8598
|
return `
|
|
8255
|
-
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
8599
|
+
<article class="claim-card" id="claim-${escapeHtml2(claim.id)}" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
8256
8600
|
<header class="claim-header">
|
|
8257
8601
|
<div>
|
|
8258
8602
|
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
@@ -8281,6 +8625,66 @@ function renderChangedFileRow(file) {
|
|
|
8281
8625
|
<td>${claims}</td>
|
|
8282
8626
|
</tr>`;
|
|
8283
8627
|
}
|
|
8628
|
+
var MAX_PATCH_EMBED_BYTES2 = 256 * 1024;
|
|
8629
|
+
function renderScenarioRef(ref) {
|
|
8630
|
+
if (!ref.resolved) {
|
|
8631
|
+
return `<span class="scenario-ref unverified">\u26A0\uFE0F ${escapeHtml2(ref.id)} (unverified reference)</span>`;
|
|
8632
|
+
}
|
|
8633
|
+
const icon = ref.status === "passed" ? "\u2705" : ref.status === "failed" ? "\u274C" : "\u2298";
|
|
8634
|
+
return `<a class="scenario-ref" href="#claim-${escapeHtml2(ref.id)}">${icon} ${escapeHtml2(ref.scenario ?? ref.id)}</a>`;
|
|
8635
|
+
}
|
|
8636
|
+
function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
8637
|
+
let oldLn = hunk.oldStart;
|
|
8638
|
+
let newLn = hunk.newStart;
|
|
8639
|
+
const rows = hunk.lines.map((line, i) => {
|
|
8640
|
+
const anchored = anchoredStart !== void 0 && anchoredCount !== void 0 && i >= anchoredStart && i < anchoredStart + anchoredCount;
|
|
8641
|
+
const cls = `diff-${line.kind}${anchored ? " diff-anchored" : ""}`;
|
|
8642
|
+
const o = line.kind === "add" ? "" : String(oldLn++);
|
|
8643
|
+
const n = line.kind === "del" ? "" : String(newLn++);
|
|
8644
|
+
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
8645
|
+
return `<tr class="${cls}"><td class="diff-ln">${o}</td><td class="diff-ln">${n}</td><td class="diff-sign">${sign}</td><td class="diff-code">${escapeHtml2(line.text)}</td></tr>`;
|
|
8646
|
+
});
|
|
8647
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
8648
|
+
return `<div class="diff-hunk">
|
|
8649
|
+
<div class="diff-file-header"><code>${escapeHtml2(path11)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml2(hunk.header)}</span></div>
|
|
8650
|
+
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
8651
|
+
</div>`;
|
|
8652
|
+
}
|
|
8653
|
+
function renderAnnotation(evidence, annotation, diffIndex, index) {
|
|
8654
|
+
const scenarios = annotation.scenarios.length > 0 ? `<div class="scenario-row">${annotation.scenarios.map(renderScenarioRef).join("")}</div>` : `<p class="no-scenario">Not covered by a scenario \u2014 implementation claim without behavioural evidence.</p>`;
|
|
8655
|
+
const res = annotation.resolution;
|
|
8656
|
+
let body;
|
|
8657
|
+
if (res.state === "anchored" && res.fileIndex !== void 0 && res.hunkIndex !== void 0) {
|
|
8658
|
+
const file = evidence.files[res.fileIndex];
|
|
8659
|
+
body = renderDiffHunk(file, file.hunks[res.hunkIndex], res.lineIndex, res.lineCount);
|
|
8660
|
+
} else if (res.state === "ambiguous") {
|
|
8661
|
+
body = `<p class="anchor-notice">\u26A0\uFE0F Ambiguous anchor \u2014 these lines appear in more than one place in the current patch, so the annotation is not attached to any of them.</p>`;
|
|
8662
|
+
} else {
|
|
8663
|
+
body = `<p class="anchor-notice">\u26A0\uFE0F Orphaned annotation \u2014 could not locate these lines in the current patch. The prose below may describe code that has since changed.</p>`;
|
|
8664
|
+
}
|
|
8665
|
+
return `<article class="annotation" id="code-diff-${diffIndex}-a${index}" data-anchor-state="${res.state}">
|
|
8666
|
+
<h4>${escapeHtml2(annotation.label ?? `Annotation ${index + 1}`)}</h4>
|
|
8667
|
+
<p class="annotation-text">${escapeHtml2(annotation.text)}</p>
|
|
8668
|
+
${scenarios}
|
|
8669
|
+
${body}
|
|
8670
|
+
</article>`;
|
|
8671
|
+
}
|
|
8672
|
+
function renderCodeDiffSection(evidence, diffIndex) {
|
|
8673
|
+
const comparing = evidence.baseLabel || evidence.headLabel ? `<p class="subtle">Comparing ${escapeHtml2(evidence.baseLabel ?? "base")} \u2192 ${escapeHtml2(evidence.headLabel ?? "head")}</p>` : "";
|
|
8674
|
+
const audit = evidence.patchUrl === void 0 ? "" : evidence.patchUrl.startsWith("https://") ? `<p class="subtle">Canonical patch: <a href="${escapeHtml2(evidence.patchUrl)}" rel="noopener noreferrer">${escapeHtml2(evidence.patchUrl)}</a></p>` : `<p class="subtle">Canonical patch: <code>${escapeHtml2(evidence.patchUrl)}</code></p>`;
|
|
8675
|
+
const outline = evidence.annotations.length > 1 ? `<ol class="diff-outline">${evidence.annotations.map(
|
|
8676
|
+
(a, i) => `<li><a href="#code-diff-${diffIndex}-a${i}">${escapeHtml2(a.label ?? `Annotation ${i + 1}`)}</a></li>`
|
|
8677
|
+
).join("")}</ol>` : "";
|
|
8678
|
+
const rawPatch = Buffer.byteLength(evidence.patch, "utf8") <= MAX_PATCH_EMBED_BYTES2 ? `<details class="raw-patch"><summary>Raw patch (audit)</summary><pre><code>${escapeHtml2(evidence.patch)}</code></pre></details>` : `<p class="anchor-notice">Patch too large to embed${evidence.patchUrl ? " \u2014 use the canonical patch link above" : ""}.</p>`;
|
|
8679
|
+
return `<section class="panel code-diff" id="code-diff-${diffIndex}">
|
|
8680
|
+
<h2>Code diff: ${escapeHtml2(evidence.title)}</h2>
|
|
8681
|
+
${comparing}
|
|
8682
|
+
${audit}
|
|
8683
|
+
${outline}
|
|
8684
|
+
${evidence.annotations.map((a, i) => renderAnnotation(evidence, a, diffIndex, i)).join("\n")}
|
|
8685
|
+
${rawPatch}
|
|
8686
|
+
</section>`;
|
|
8687
|
+
}
|
|
8284
8688
|
function renderAudienceSection2(title, claims) {
|
|
8285
8689
|
if (claims.length === 0) return "";
|
|
8286
8690
|
return `<section class="audience-section">
|
|
@@ -8340,6 +8744,29 @@ var REVIEW_CSS = `
|
|
|
8340
8744
|
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
8341
8745
|
.trace-note { color: var(--muted-foreground); }
|
|
8342
8746
|
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
8747
|
+
.code-diff h2 { margin-bottom: 6px; }
|
|
8748
|
+
.code-diff a { color: var(--foreground); }
|
|
8749
|
+
.diff-outline { margin: 12px 0; padding-left: 20px; }
|
|
8750
|
+
.annotation { margin: 18px 0; padding: 14px 16px; border: 1px solid var(--border); border-radius: 10px; background: color-mix(in srgb, var(--card) 60%, var(--background)); }
|
|
8751
|
+
.annotation h4 { margin: 0 0 6px; }
|
|
8752
|
+
.annotation-text { margin: 0 0 10px; }
|
|
8753
|
+
.scenario-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
|
8754
|
+
.scenario-ref { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px; font-size: 0.82rem; text-decoration: none; color: var(--foreground); background: var(--background); }
|
|
8755
|
+
.scenario-ref.unverified { color: var(--warning, #b58900); border-color: var(--warning, #b58900); }
|
|
8756
|
+
.no-scenario { color: var(--warning, #b58900); font-size: 0.9rem; margin: 0 0 10px; }
|
|
8757
|
+
.anchor-notice { color: var(--warning, #b58900); background: color-mix(in srgb, var(--warning, #b58900) 10%, transparent); border-radius: 6px; padding: 8px 10px; margin: 0; }
|
|
8758
|
+
.diff-hunk { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
8759
|
+
.diff-file-header { padding: 6px 10px; background: var(--secondary); border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
8760
|
+
.diff-table { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.82rem; }
|
|
8761
|
+
.diff-table td { padding: 1px 8px; border-bottom: none; }
|
|
8762
|
+
.diff-code { white-space: pre-wrap; word-break: break-all; }
|
|
8763
|
+
.diff-ln { color: var(--muted-foreground); text-align: right; user-select: none; width: 1%; white-space: nowrap; }
|
|
8764
|
+
.diff-sign { user-select: none; width: 1%; }
|
|
8765
|
+
.diff-add { background: color-mix(in srgb, var(--success, #2e7d32) 12%, transparent); }
|
|
8766
|
+
.diff-del { background: color-mix(in srgb, var(--destructive) 10%, transparent); }
|
|
8767
|
+
.diff-anchored .diff-code { box-shadow: inset 3px 0 0 var(--primary, #2e7d32); }
|
|
8768
|
+
.raw-patch { margin-top: 16px; }
|
|
8769
|
+
.raw-patch pre { overflow-x: auto; background: var(--secondary); border-radius: 8px; padding: 12px; }
|
|
8343
8770
|
`;
|
|
8344
8771
|
var JS_THEME_TOGGLE2 = `
|
|
8345
8772
|
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
@@ -8406,15 +8833,18 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
8406
8833
|
<p class="subtle">${escapeHtml2(priority)}</p>
|
|
8407
8834
|
</section>
|
|
8408
8835
|
${changedFilesPanel}
|
|
8409
|
-
<
|
|
8410
|
-
<
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8836
|
+
<div class="claims-region">
|
|
8837
|
+
<section class="toolbar">
|
|
8838
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
8839
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
8840
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
8841
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
8842
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
8843
|
+
</section>
|
|
8844
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
8845
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
8846
|
+
</div>
|
|
8847
|
+
${review.codeDiffs.map((d, i) => renderCodeDiffSection(d, i)).join("\n")}
|
|
8418
8848
|
</main>
|
|
8419
8849
|
<script>
|
|
8420
8850
|
${themeInitJs}
|
|
@@ -9110,6 +9540,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
9110
9540
|
adaptPlaywrightRun,
|
|
9111
9541
|
adaptVitestRun,
|
|
9112
9542
|
advanceState,
|
|
9543
|
+
assembleCodeDiff,
|
|
9113
9544
|
assertValidRun,
|
|
9114
9545
|
buildCheck,
|
|
9115
9546
|
buildGoal,
|
|
@@ -9122,8 +9553,10 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
9122
9553
|
canonicalizeRun,
|
|
9123
9554
|
classifyStatusChange,
|
|
9124
9555
|
clearVersionCache,
|
|
9556
|
+
codeDiffDiagnostics,
|
|
9125
9557
|
computeTestMetrics,
|
|
9126
9558
|
copyMarkdownAssets,
|
|
9559
|
+
createAnchor,
|
|
9127
9560
|
createPrCommentSummary,
|
|
9128
9561
|
createReportGenerator,
|
|
9129
9562
|
deriveAudience,
|
|
@@ -9158,6 +9591,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
9158
9591
|
normalizeVitestResults,
|
|
9159
9592
|
parseEnvelopes,
|
|
9160
9593
|
parseNdjson,
|
|
9594
|
+
parseUnifiedDiff,
|
|
9161
9595
|
publishConfluencePage,
|
|
9162
9596
|
publishJiraIssue,
|
|
9163
9597
|
readBranchName,
|
|
@@ -9166,6 +9600,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
9166
9600
|
recordDeployment,
|
|
9167
9601
|
regenerateArtifacts,
|
|
9168
9602
|
regenerateRun,
|
|
9603
|
+
relocateAnchor,
|
|
9169
9604
|
renderCheck,
|
|
9170
9605
|
renderGoal,
|
|
9171
9606
|
renderTriage,
|