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.js
CHANGED
|
@@ -7614,6 +7614,157 @@ function renderTriage(report, format) {
|
|
|
7614
7614
|
return lines.join("\n").trimEnd();
|
|
7615
7615
|
}
|
|
7616
7616
|
|
|
7617
|
+
// src/review/diff-anchor.ts
|
|
7618
|
+
import { createHash as createHash6 } from "crypto";
|
|
7619
|
+
var CONTEXT_WINDOW = 3;
|
|
7620
|
+
var MAX_FUZZ = 2;
|
|
7621
|
+
var normalize = (line) => line.trim();
|
|
7622
|
+
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
7623
|
+
function stripPathPrefix(raw) {
|
|
7624
|
+
const path11 = raw.split(" ")[0].trim();
|
|
7625
|
+
if (path11 === "/dev/null") return void 0;
|
|
7626
|
+
return path11.replace(/^[ab]\//, "");
|
|
7627
|
+
}
|
|
7628
|
+
function parseUnifiedDiff(patch) {
|
|
7629
|
+
const files = [];
|
|
7630
|
+
let current;
|
|
7631
|
+
let hunk;
|
|
7632
|
+
let oldRemaining = 0;
|
|
7633
|
+
let newRemaining = 0;
|
|
7634
|
+
for (const line of patch.split("\n")) {
|
|
7635
|
+
if (hunk && (oldRemaining > 0 || newRemaining > 0)) {
|
|
7636
|
+
if (line.startsWith("\\")) continue;
|
|
7637
|
+
const kind = line.startsWith("+") ? "add" : line.startsWith("-") ? "del" : "context";
|
|
7638
|
+
hunk.lines.push({ kind, text: line.slice(1) });
|
|
7639
|
+
if (kind !== "add") oldRemaining--;
|
|
7640
|
+
if (kind !== "del") newRemaining--;
|
|
7641
|
+
continue;
|
|
7642
|
+
}
|
|
7643
|
+
hunk = void 0;
|
|
7644
|
+
if (line.startsWith("diff --git ")) {
|
|
7645
|
+
current = { hunks: [] };
|
|
7646
|
+
files.push(current);
|
|
7647
|
+
continue;
|
|
7648
|
+
}
|
|
7649
|
+
if (line.startsWith("--- ")) {
|
|
7650
|
+
if (!current || current.oldPath !== void 0 || current.hunks.length > 0) {
|
|
7651
|
+
current = { hunks: [] };
|
|
7652
|
+
files.push(current);
|
|
7653
|
+
}
|
|
7654
|
+
current.oldPath = stripPathPrefix(line.slice(4));
|
|
7655
|
+
continue;
|
|
7656
|
+
}
|
|
7657
|
+
if (line.startsWith("+++ ") && current) {
|
|
7658
|
+
current.newPath = stripPathPrefix(line.slice(4));
|
|
7659
|
+
continue;
|
|
7660
|
+
}
|
|
7661
|
+
const match = HUNK_HEADER.exec(line);
|
|
7662
|
+
if (match && current) {
|
|
7663
|
+
hunk = {
|
|
7664
|
+
oldStart: Number(match[1]),
|
|
7665
|
+
newStart: Number(match[3]),
|
|
7666
|
+
header: match[5] ?? "",
|
|
7667
|
+
lines: []
|
|
7668
|
+
};
|
|
7669
|
+
oldRemaining = match[2] === void 0 ? 1 : Number(match[2]);
|
|
7670
|
+
newRemaining = match[4] === void 0 ? 1 : Number(match[4]);
|
|
7671
|
+
current.hunks.push(hunk);
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
return files;
|
|
7675
|
+
}
|
|
7676
|
+
function createAnchor(args) {
|
|
7677
|
+
const { file, hunkIndex, lineIndex } = args;
|
|
7678
|
+
const lines = file.hunks[hunkIndex].lines;
|
|
7679
|
+
if (lines[lineIndex].kind === "context") {
|
|
7680
|
+
throw new Error(`Line ${lineIndex} is a context line; anchors target changed lines`);
|
|
7681
|
+
}
|
|
7682
|
+
const changed = [];
|
|
7683
|
+
let end = lineIndex;
|
|
7684
|
+
while (end < lines.length && lines[end].kind !== "context") {
|
|
7685
|
+
changed.push({ kind: lines[end].kind, text: lines[end].text });
|
|
7686
|
+
end++;
|
|
7687
|
+
}
|
|
7688
|
+
const contextBefore = lines.slice(Math.max(0, lineIndex - CONTEXT_WINDOW), lineIndex).map((l) => l.text);
|
|
7689
|
+
const contextAfter = lines.slice(end, end + CONTEXT_WINDOW).map((l) => l.text);
|
|
7690
|
+
const hash = createHash6("sha256").update(
|
|
7691
|
+
JSON.stringify({
|
|
7692
|
+
changed: changed.map((l) => `${l.kind}:${normalize(l.text)}`),
|
|
7693
|
+
before: contextBefore.map(normalize),
|
|
7694
|
+
after: contextAfter.map(normalize)
|
|
7695
|
+
})
|
|
7696
|
+
).digest("hex");
|
|
7697
|
+
return {
|
|
7698
|
+
hash,
|
|
7699
|
+
file: file.newPath ?? file.oldPath ?? "",
|
|
7700
|
+
changed,
|
|
7701
|
+
contextBefore,
|
|
7702
|
+
contextAfter
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
function changedRunCandidates(anchor, file, fileIndex) {
|
|
7706
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
7707
|
+
const out = [];
|
|
7708
|
+
file.hunks.forEach((hunk, hunkIndex) => {
|
|
7709
|
+
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
7710
|
+
for (let j = 0; j < anchor.changed.length; j++) {
|
|
7711
|
+
const line = hunk.lines[i + j];
|
|
7712
|
+
const want = anchor.changed[j];
|
|
7713
|
+
if (line.kind !== want.kind || normalize(line.text) !== normalize(want.text)) {
|
|
7714
|
+
continue outer;
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
out.push({ fileIndex, file: path11, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
7718
|
+
}
|
|
7719
|
+
});
|
|
7720
|
+
return out;
|
|
7721
|
+
}
|
|
7722
|
+
function contextMatches(anchor, candidate, fuzz) {
|
|
7723
|
+
const before = anchor.contextBefore.slice(Math.min(fuzz, anchor.contextBefore.length));
|
|
7724
|
+
const after = anchor.contextAfter.slice(
|
|
7725
|
+
0,
|
|
7726
|
+
Math.max(0, anchor.contextAfter.length - fuzz)
|
|
7727
|
+
);
|
|
7728
|
+
for (let i = 0; i < before.length; i++) {
|
|
7729
|
+
const line = candidate.lines[candidate.lineIndex - before.length + i];
|
|
7730
|
+
if (line === void 0 || normalize(line.text) !== normalize(before[i])) return false;
|
|
7731
|
+
}
|
|
7732
|
+
const end = candidate.lineIndex + anchor.changed.length;
|
|
7733
|
+
for (let i = 0; i < after.length; i++) {
|
|
7734
|
+
const line = candidate.lines[end + i];
|
|
7735
|
+
if (line === void 0 || normalize(line.text) !== normalize(after[i])) return false;
|
|
7736
|
+
}
|
|
7737
|
+
return true;
|
|
7738
|
+
}
|
|
7739
|
+
function resolveCandidates(anchor, candidates) {
|
|
7740
|
+
for (let fuzz = 0; fuzz <= MAX_FUZZ; fuzz++) {
|
|
7741
|
+
const matches = candidates.filter((c) => contextMatches(anchor, c, fuzz));
|
|
7742
|
+
if (matches.length === 1) {
|
|
7743
|
+
const m = matches[0];
|
|
7744
|
+
return {
|
|
7745
|
+
state: "anchored",
|
|
7746
|
+
fileIndex: m.fileIndex,
|
|
7747
|
+
file: m.file,
|
|
7748
|
+
hunkIndex: m.hunkIndex,
|
|
7749
|
+
lineIndex: m.lineIndex,
|
|
7750
|
+
lineCount: anchor.changed.length,
|
|
7751
|
+
fuzz
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
if (matches.length > 1) return { state: "ambiguous" };
|
|
7755
|
+
}
|
|
7756
|
+
return void 0;
|
|
7757
|
+
}
|
|
7758
|
+
function relocateAnchor(anchor, files) {
|
|
7759
|
+
const own = [];
|
|
7760
|
+
const others = [];
|
|
7761
|
+
files.forEach((file, fileIndex) => {
|
|
7762
|
+
const isOwn = file.newPath === anchor.file || file.oldPath === anchor.file;
|
|
7763
|
+
(isOwn ? own : others).push(...changedRunCandidates(anchor, file, fileIndex));
|
|
7764
|
+
});
|
|
7765
|
+
return resolveCandidates(anchor, own) ?? resolveCandidates(anchor, others) ?? { state: "orphaned" };
|
|
7766
|
+
}
|
|
7767
|
+
|
|
7617
7768
|
// src/review/conventions.ts
|
|
7618
7769
|
var CHANGE_TAG_PREFIX = "change:";
|
|
7619
7770
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -7801,6 +7952,33 @@ var AUDIENCE_ORDER = {
|
|
|
7801
7952
|
stakeholder: 0,
|
|
7802
7953
|
engineer: 1
|
|
7803
7954
|
};
|
|
7955
|
+
function buildCodeDiff(input, run, context) {
|
|
7956
|
+
const files = parseUnifiedDiff(input.patch);
|
|
7957
|
+
const byId = new Map(run.testCases.map((tc) => [tc.id, tc]));
|
|
7958
|
+
return {
|
|
7959
|
+
title: input.title,
|
|
7960
|
+
patch: input.patch,
|
|
7961
|
+
patchUrl: input.patchUrl,
|
|
7962
|
+
baseLabel: input.baseLabel ?? context.baseRef,
|
|
7963
|
+
headLabel: input.headLabel ?? context.headRef,
|
|
7964
|
+
files,
|
|
7965
|
+
annotations: input.annotations.map((annotation) => ({
|
|
7966
|
+
anchorHash: annotation.anchor?.hash,
|
|
7967
|
+
text: annotation.text,
|
|
7968
|
+
label: annotation.label,
|
|
7969
|
+
resolution: annotation.anchor ? relocateAnchor(annotation.anchor, files) : { state: annotation.unresolved ?? "orphaned" },
|
|
7970
|
+
scenarios: (annotation.scenarioIds ?? []).map((id) => {
|
|
7971
|
+
const testCase = byId.get(id);
|
|
7972
|
+
return testCase ? {
|
|
7973
|
+
id,
|
|
7974
|
+
resolved: true,
|
|
7975
|
+
scenario: testCase.story.scenario,
|
|
7976
|
+
status: testCase.status
|
|
7977
|
+
} : { id, resolved: false };
|
|
7978
|
+
})
|
|
7979
|
+
}))
|
|
7980
|
+
};
|
|
7981
|
+
}
|
|
7804
7982
|
function buildReview(run, context = { changedFiles: [] }) {
|
|
7805
7983
|
const changedSource = context.changedFiles.filter(
|
|
7806
7984
|
(f) => isReviewableSource(f.path)
|
|
@@ -7845,9 +8023,31 @@ function buildReview(run, context = { changedFiles: [] }) {
|
|
|
7845
8023
|
context,
|
|
7846
8024
|
summary,
|
|
7847
8025
|
claims: sortedClaims,
|
|
7848
|
-
changedFiles: sortedFiles
|
|
8026
|
+
changedFiles: sortedFiles,
|
|
8027
|
+
codeDiffs: (context.codeDiffs ?? []).map((d) => buildCodeDiff(d, run, context))
|
|
7849
8028
|
};
|
|
7850
8029
|
}
|
|
8030
|
+
function codeDiffDiagnostics(review) {
|
|
8031
|
+
const issues = [];
|
|
8032
|
+
for (const evidence of review.codeDiffs) {
|
|
8033
|
+
evidence.annotations.forEach((annotation, i) => {
|
|
8034
|
+
const name = annotation.label ?? `annotation ${i + 1}`;
|
|
8035
|
+
if (annotation.resolution.state !== "anchored") {
|
|
8036
|
+
issues.push(
|
|
8037
|
+
`"${evidence.title}" ${name}: anchor is ${annotation.resolution.state} in the current patch`
|
|
8038
|
+
);
|
|
8039
|
+
}
|
|
8040
|
+
for (const ref of annotation.scenarios) {
|
|
8041
|
+
if (!ref.resolved) {
|
|
8042
|
+
issues.push(
|
|
8043
|
+
`"${evidence.title}" ${name}: cites scenario "${ref.id}" which is not in this run`
|
|
8044
|
+
);
|
|
8045
|
+
}
|
|
8046
|
+
}
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
return issues;
|
|
8050
|
+
}
|
|
7851
8051
|
function buildSummary2(claims, changedFiles) {
|
|
7852
8052
|
const byAudience = {
|
|
7853
8053
|
stakeholder: 0,
|
|
@@ -7874,6 +8074,75 @@ function buildSummary2(claims, changedFiles) {
|
|
|
7874
8074
|
};
|
|
7875
8075
|
}
|
|
7876
8076
|
|
|
8077
|
+
// src/review/code-diff-sidecar.ts
|
|
8078
|
+
function locateMatches(file, match) {
|
|
8079
|
+
const out = [];
|
|
8080
|
+
file.hunks.forEach((hunk, hunkIndex) => {
|
|
8081
|
+
hunk.lines.forEach((line, lineIndex) => {
|
|
8082
|
+
if (line.kind !== "context" && line.text.includes(match)) {
|
|
8083
|
+
out.push({ hunkIndex, lineIndex });
|
|
8084
|
+
}
|
|
8085
|
+
});
|
|
8086
|
+
});
|
|
8087
|
+
return out;
|
|
8088
|
+
}
|
|
8089
|
+
function runStart(file, hunkIndex, lineIndex) {
|
|
8090
|
+
const lines = file.hunks[hunkIndex].lines;
|
|
8091
|
+
let start = lineIndex;
|
|
8092
|
+
while (start > 0 && lines[start - 1].kind !== "context") start--;
|
|
8093
|
+
return start;
|
|
8094
|
+
}
|
|
8095
|
+
function assembleCodeDiff(args) {
|
|
8096
|
+
const { sidecar, patch } = args;
|
|
8097
|
+
const files = parseUnifiedDiff(patch);
|
|
8098
|
+
const warnings = [];
|
|
8099
|
+
if (sidecar.patchUrl !== void 0 && !sidecar.patchUrl.startsWith("https://")) {
|
|
8100
|
+
warnings.push(
|
|
8101
|
+
`patchUrl "${sidecar.patchUrl}" is not https: \u2014 it will render as inert text, not a link`
|
|
8102
|
+
);
|
|
8103
|
+
}
|
|
8104
|
+
const annotations = sidecar.annotations.map(
|
|
8105
|
+
(entry) => {
|
|
8106
|
+
const base = {
|
|
8107
|
+
text: entry.text,
|
|
8108
|
+
label: entry.label,
|
|
8109
|
+
scenarioIds: entry.scenarioIds
|
|
8110
|
+
};
|
|
8111
|
+
const file = files.find(
|
|
8112
|
+
(f) => f.newPath === entry.file || f.oldPath === entry.file
|
|
8113
|
+
);
|
|
8114
|
+
const matches = file ? locateMatches(file, entry.match) : [];
|
|
8115
|
+
if (file && matches.length === 1) {
|
|
8116
|
+
const { hunkIndex, lineIndex } = matches[0];
|
|
8117
|
+
const anchor = createAnchor({
|
|
8118
|
+
file,
|
|
8119
|
+
hunkIndex,
|
|
8120
|
+
lineIndex: runStart(file, hunkIndex, lineIndex)
|
|
8121
|
+
});
|
|
8122
|
+
return { ...base, anchor };
|
|
8123
|
+
}
|
|
8124
|
+
warnings.push(
|
|
8125
|
+
!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`
|
|
8126
|
+
);
|
|
8127
|
+
return {
|
|
8128
|
+
...base,
|
|
8129
|
+
unresolved: file && matches.length > 1 ? "ambiguous" : "orphaned"
|
|
8130
|
+
};
|
|
8131
|
+
}
|
|
8132
|
+
);
|
|
8133
|
+
return {
|
|
8134
|
+
input: {
|
|
8135
|
+
title: sidecar.title,
|
|
8136
|
+
patch,
|
|
8137
|
+
patchUrl: sidecar.patchUrl,
|
|
8138
|
+
baseLabel: sidecar.baseLabel,
|
|
8139
|
+
headLabel: sidecar.headLabel,
|
|
8140
|
+
annotations
|
|
8141
|
+
},
|
|
8142
|
+
warnings
|
|
8143
|
+
};
|
|
8144
|
+
}
|
|
8145
|
+
|
|
7877
8146
|
// src/formatters/review-markdown.ts
|
|
7878
8147
|
var STRENGTH_BADGE = {
|
|
7879
8148
|
strong: "\u{1F7E2} strong",
|
|
@@ -7959,6 +8228,73 @@ function renderAudienceSection(lines, title, claims) {
|
|
|
7959
8228
|
renderClaim(lines, claim);
|
|
7960
8229
|
}
|
|
7961
8230
|
}
|
|
8231
|
+
var MAX_PATCH_EMBED_BYTES = 64 * 1024;
|
|
8232
|
+
function fenceFor(content) {
|
|
8233
|
+
const runs = content.match(/`{3,}/g);
|
|
8234
|
+
const length = runs ? Math.max(...runs.map((r) => r.length)) + 1 : 3;
|
|
8235
|
+
return "`".repeat(length);
|
|
8236
|
+
}
|
|
8237
|
+
function renderCodeDiff(lines, evidence) {
|
|
8238
|
+
lines.push(`## Code diff evidence: ${evidence.title}`);
|
|
8239
|
+
lines.push("");
|
|
8240
|
+
if (evidence.baseLabel || evidence.headLabel) {
|
|
8241
|
+
lines.push(
|
|
8242
|
+
`Comparing \`${evidence.baseLabel ?? "base"}\` \u2192 \`${evidence.headLabel ?? "head"}\`.`
|
|
8243
|
+
);
|
|
8244
|
+
lines.push("");
|
|
8245
|
+
}
|
|
8246
|
+
if (evidence.patchUrl !== void 0) {
|
|
8247
|
+
lines.push(
|
|
8248
|
+
evidence.patchUrl.startsWith("https://") ? `Canonical patch: ${evidence.patchUrl}` : `Canonical patch: \`${evidence.patchUrl.replace(/`/g, "")}\``
|
|
8249
|
+
);
|
|
8250
|
+
lines.push("");
|
|
8251
|
+
}
|
|
8252
|
+
evidence.annotations.forEach((annotation, i) => {
|
|
8253
|
+
lines.push(`### ${annotation.label ?? `Annotation ${i + 1}`}`);
|
|
8254
|
+
lines.push("");
|
|
8255
|
+
lines.push(annotation.text);
|
|
8256
|
+
lines.push("");
|
|
8257
|
+
if (annotation.resolution.state === "orphaned") {
|
|
8258
|
+
lines.push(
|
|
8259
|
+
"> \u26A0\uFE0F Orphaned annotation \u2014 could not locate these lines in the current patch."
|
|
8260
|
+
);
|
|
8261
|
+
lines.push("");
|
|
8262
|
+
} else if (annotation.resolution.state === "ambiguous") {
|
|
8263
|
+
lines.push(
|
|
8264
|
+
"> \u26A0\uFE0F Ambiguous anchor \u2014 these lines appear in more than one place in the current patch."
|
|
8265
|
+
);
|
|
8266
|
+
lines.push("");
|
|
8267
|
+
} else if (annotation.resolution.file !== void 0) {
|
|
8268
|
+
lines.push(`Location: \`${annotation.resolution.file}\``);
|
|
8269
|
+
lines.push("");
|
|
8270
|
+
}
|
|
8271
|
+
if (annotation.scenarios.length === 0) {
|
|
8272
|
+
lines.push("_Not covered by a scenario._");
|
|
8273
|
+
} else {
|
|
8274
|
+
for (const ref of annotation.scenarios) {
|
|
8275
|
+
lines.push(
|
|
8276
|
+
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)`
|
|
8277
|
+
);
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
lines.push("");
|
|
8281
|
+
});
|
|
8282
|
+
if (Buffer.byteLength(evidence.patch, "utf8") <= MAX_PATCH_EMBED_BYTES) {
|
|
8283
|
+
const fence = fenceFor(evidence.patch);
|
|
8284
|
+
lines.push("<details><summary>Raw patch (audit)</summary>");
|
|
8285
|
+
lines.push("");
|
|
8286
|
+
lines.push(`${fence}diff`);
|
|
8287
|
+
lines.push(evidence.patch.trimEnd());
|
|
8288
|
+
lines.push(fence);
|
|
8289
|
+
lines.push("");
|
|
8290
|
+
lines.push("</details>");
|
|
8291
|
+
} else {
|
|
8292
|
+
lines.push(
|
|
8293
|
+
`_Patch too large to embed${evidence.patchUrl ? " \u2014 see the canonical patch link above" : ""}._`
|
|
8294
|
+
);
|
|
8295
|
+
}
|
|
8296
|
+
lines.push("");
|
|
8297
|
+
}
|
|
7962
8298
|
var ReviewMarkdownFormatter = class {
|
|
7963
8299
|
title;
|
|
7964
8300
|
constructor(options = {}) {
|
|
@@ -8019,6 +8355,9 @@ var ReviewMarkdownFormatter = class {
|
|
|
8019
8355
|
"Engineer changes",
|
|
8020
8356
|
review.claims.filter((c) => c.audience === "engineer")
|
|
8021
8357
|
);
|
|
8358
|
+
for (const evidence of review.codeDiffs) {
|
|
8359
|
+
renderCodeDiff(lines, evidence);
|
|
8360
|
+
}
|
|
8022
8361
|
return lines.join("\n").trimEnd();
|
|
8023
8362
|
}
|
|
8024
8363
|
};
|
|
@@ -8105,7 +8444,7 @@ function renderClaimCard(claim) {
|
|
|
8105
8444
|
);
|
|
8106
8445
|
const extraDocs = docs.length > 0 && claim.intent === void 0 ? `<div class="intent">${docs.map(inlineDoc).join("<br>")}</div>` : "";
|
|
8107
8446
|
return `
|
|
8108
|
-
<article class="claim-card" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
8447
|
+
<article class="claim-card" id="claim-${escapeHtml2(claim.id)}" data-audience="${claim.audience}" data-strength="${claim.strength}" data-search="${search}">
|
|
8109
8448
|
<header class="claim-header">
|
|
8110
8449
|
<div>
|
|
8111
8450
|
<span class="strength-badge strength-${claim.strength}">${STRENGTH_LABEL[claim.strength]}</span>
|
|
@@ -8134,6 +8473,66 @@ function renderChangedFileRow(file) {
|
|
|
8134
8473
|
<td>${claims}</td>
|
|
8135
8474
|
</tr>`;
|
|
8136
8475
|
}
|
|
8476
|
+
var MAX_PATCH_EMBED_BYTES2 = 256 * 1024;
|
|
8477
|
+
function renderScenarioRef(ref) {
|
|
8478
|
+
if (!ref.resolved) {
|
|
8479
|
+
return `<span class="scenario-ref unverified">\u26A0\uFE0F ${escapeHtml2(ref.id)} (unverified reference)</span>`;
|
|
8480
|
+
}
|
|
8481
|
+
const icon = ref.status === "passed" ? "\u2705" : ref.status === "failed" ? "\u274C" : "\u2298";
|
|
8482
|
+
return `<a class="scenario-ref" href="#claim-${escapeHtml2(ref.id)}">${icon} ${escapeHtml2(ref.scenario ?? ref.id)}</a>`;
|
|
8483
|
+
}
|
|
8484
|
+
function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
8485
|
+
let oldLn = hunk.oldStart;
|
|
8486
|
+
let newLn = hunk.newStart;
|
|
8487
|
+
const rows = hunk.lines.map((line, i) => {
|
|
8488
|
+
const anchored = anchoredStart !== void 0 && anchoredCount !== void 0 && i >= anchoredStart && i < anchoredStart + anchoredCount;
|
|
8489
|
+
const cls = `diff-${line.kind}${anchored ? " diff-anchored" : ""}`;
|
|
8490
|
+
const o = line.kind === "add" ? "" : String(oldLn++);
|
|
8491
|
+
const n = line.kind === "del" ? "" : String(newLn++);
|
|
8492
|
+
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
8493
|
+
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>`;
|
|
8494
|
+
});
|
|
8495
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
8496
|
+
return `<div class="diff-hunk">
|
|
8497
|
+
<div class="diff-file-header"><code>${escapeHtml2(path11)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml2(hunk.header)}</span></div>
|
|
8498
|
+
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
8499
|
+
</div>`;
|
|
8500
|
+
}
|
|
8501
|
+
function renderAnnotation(evidence, annotation, diffIndex, index) {
|
|
8502
|
+
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>`;
|
|
8503
|
+
const res = annotation.resolution;
|
|
8504
|
+
let body;
|
|
8505
|
+
if (res.state === "anchored" && res.fileIndex !== void 0 && res.hunkIndex !== void 0) {
|
|
8506
|
+
const file = evidence.files[res.fileIndex];
|
|
8507
|
+
body = renderDiffHunk(file, file.hunks[res.hunkIndex], res.lineIndex, res.lineCount);
|
|
8508
|
+
} else if (res.state === "ambiguous") {
|
|
8509
|
+
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>`;
|
|
8510
|
+
} else {
|
|
8511
|
+
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>`;
|
|
8512
|
+
}
|
|
8513
|
+
return `<article class="annotation" id="code-diff-${diffIndex}-a${index}" data-anchor-state="${res.state}">
|
|
8514
|
+
<h4>${escapeHtml2(annotation.label ?? `Annotation ${index + 1}`)}</h4>
|
|
8515
|
+
<p class="annotation-text">${escapeHtml2(annotation.text)}</p>
|
|
8516
|
+
${scenarios}
|
|
8517
|
+
${body}
|
|
8518
|
+
</article>`;
|
|
8519
|
+
}
|
|
8520
|
+
function renderCodeDiffSection(evidence, diffIndex) {
|
|
8521
|
+
const comparing = evidence.baseLabel || evidence.headLabel ? `<p class="subtle">Comparing ${escapeHtml2(evidence.baseLabel ?? "base")} \u2192 ${escapeHtml2(evidence.headLabel ?? "head")}</p>` : "";
|
|
8522
|
+
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>`;
|
|
8523
|
+
const outline = evidence.annotations.length > 1 ? `<ol class="diff-outline">${evidence.annotations.map(
|
|
8524
|
+
(a, i) => `<li><a href="#code-diff-${diffIndex}-a${i}">${escapeHtml2(a.label ?? `Annotation ${i + 1}`)}</a></li>`
|
|
8525
|
+
).join("")}</ol>` : "";
|
|
8526
|
+
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>`;
|
|
8527
|
+
return `<section class="panel code-diff" id="code-diff-${diffIndex}">
|
|
8528
|
+
<h2>Code diff: ${escapeHtml2(evidence.title)}</h2>
|
|
8529
|
+
${comparing}
|
|
8530
|
+
${audit}
|
|
8531
|
+
${outline}
|
|
8532
|
+
${evidence.annotations.map((a, i) => renderAnnotation(evidence, a, diffIndex, i)).join("\n")}
|
|
8533
|
+
${rawPatch}
|
|
8534
|
+
</section>`;
|
|
8535
|
+
}
|
|
8137
8536
|
function renderAudienceSection2(title, claims) {
|
|
8138
8537
|
if (claims.length === 0) return "";
|
|
8139
8538
|
return `<section class="audience-section">
|
|
@@ -8193,6 +8592,29 @@ var REVIEW_CSS = `
|
|
|
8193
8592
|
.shot { max-width: 280px; max-height: 200px; border: 1px solid var(--border); border-radius: 8px; }
|
|
8194
8593
|
.trace-note { color: var(--muted-foreground); }
|
|
8195
8594
|
.step-list { margin: 12px 0 0; padding-left: 18px; color: var(--muted-foreground); }
|
|
8595
|
+
.code-diff h2 { margin-bottom: 6px; }
|
|
8596
|
+
.code-diff a { color: var(--foreground); }
|
|
8597
|
+
.diff-outline { margin: 12px 0; padding-left: 20px; }
|
|
8598
|
+
.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)); }
|
|
8599
|
+
.annotation h4 { margin: 0 0 6px; }
|
|
8600
|
+
.annotation-text { margin: 0 0 10px; }
|
|
8601
|
+
.scenario-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
|
|
8602
|
+
.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); }
|
|
8603
|
+
.scenario-ref.unverified { color: var(--warning, #b58900); border-color: var(--warning, #b58900); }
|
|
8604
|
+
.no-scenario { color: var(--warning, #b58900); font-size: 0.9rem; margin: 0 0 10px; }
|
|
8605
|
+
.anchor-notice { color: var(--warning, #b58900); background: color-mix(in srgb, var(--warning, #b58900) 10%, transparent); border-radius: 6px; padding: 8px 10px; margin: 0; }
|
|
8606
|
+
.diff-hunk { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
8607
|
+
.diff-file-header { padding: 6px 10px; background: var(--secondary); border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
8608
|
+
.diff-table { font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.82rem; }
|
|
8609
|
+
.diff-table td { padding: 1px 8px; border-bottom: none; }
|
|
8610
|
+
.diff-code { white-space: pre-wrap; word-break: break-all; }
|
|
8611
|
+
.diff-ln { color: var(--muted-foreground); text-align: right; user-select: none; width: 1%; white-space: nowrap; }
|
|
8612
|
+
.diff-sign { user-select: none; width: 1%; }
|
|
8613
|
+
.diff-add { background: color-mix(in srgb, var(--success, #2e7d32) 12%, transparent); }
|
|
8614
|
+
.diff-del { background: color-mix(in srgb, var(--destructive) 10%, transparent); }
|
|
8615
|
+
.diff-anchored .diff-code { box-shadow: inset 3px 0 0 var(--primary, #2e7d32); }
|
|
8616
|
+
.raw-patch { margin-top: 16px; }
|
|
8617
|
+
.raw-patch pre { overflow-x: auto; background: var(--secondary); border-radius: 8px; padding: 12px; }
|
|
8196
8618
|
`;
|
|
8197
8619
|
var JS_THEME_TOGGLE2 = `
|
|
8198
8620
|
function getSystemTheme() { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
|
|
@@ -8259,15 +8681,18 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
8259
8681
|
<p class="subtle">${escapeHtml2(priority)}</p>
|
|
8260
8682
|
</section>
|
|
8261
8683
|
${changedFilesPanel}
|
|
8262
|
-
<
|
|
8263
|
-
<
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8684
|
+
<div class="claims-region">
|
|
8685
|
+
<section class="toolbar">
|
|
8686
|
+
<input type="search" placeholder="Filter claims by scenario, file, change-type" aria-label="Filter claims" />
|
|
8687
|
+
<button type="button" class="active" data-filter="all">All</button>
|
|
8688
|
+
<button type="button" data-filter="stakeholder">Stakeholder</button>
|
|
8689
|
+
<button type="button" data-filter="engineer">Engineer</button>
|
|
8690
|
+
<button type="button" data-filter="weak">Weak/None</button>
|
|
8691
|
+
</section>
|
|
8692
|
+
${renderAudienceSection2("Stakeholder behaviour", review.claims.filter((c) => c.audience === "stakeholder"))}
|
|
8693
|
+
${renderAudienceSection2("Engineer changes", review.claims.filter((c) => c.audience === "engineer"))}
|
|
8694
|
+
</div>
|
|
8695
|
+
${review.codeDiffs.map((d, i) => renderCodeDiffSection(d, i)).join("\n")}
|
|
8271
8696
|
</main>
|
|
8272
8697
|
<script>
|
|
8273
8698
|
${themeInitJs}
|
|
@@ -8962,6 +9387,7 @@ export {
|
|
|
8962
9387
|
adaptPlaywrightRun,
|
|
8963
9388
|
adaptVitestRun,
|
|
8964
9389
|
advanceState,
|
|
9390
|
+
assembleCodeDiff,
|
|
8965
9391
|
assertValidRun,
|
|
8966
9392
|
buildCheck,
|
|
8967
9393
|
buildGoal,
|
|
@@ -8974,8 +9400,10 @@ export {
|
|
|
8974
9400
|
canonicalizeRun,
|
|
8975
9401
|
classifyStatusChange,
|
|
8976
9402
|
clearVersionCache,
|
|
9403
|
+
codeDiffDiagnostics,
|
|
8977
9404
|
computeTestMetrics,
|
|
8978
9405
|
copyMarkdownAssets,
|
|
9406
|
+
createAnchor,
|
|
8979
9407
|
createPrCommentSummary,
|
|
8980
9408
|
createReportGenerator,
|
|
8981
9409
|
deriveAudience,
|
|
@@ -9010,6 +9438,7 @@ export {
|
|
|
9010
9438
|
normalizeVitestResults,
|
|
9011
9439
|
parseEnvelopes,
|
|
9012
9440
|
parseNdjson,
|
|
9441
|
+
parseUnifiedDiff,
|
|
9013
9442
|
publishConfluencePage,
|
|
9014
9443
|
publishJiraIssue,
|
|
9015
9444
|
readBranchName,
|
|
@@ -9018,6 +9447,7 @@ export {
|
|
|
9018
9447
|
recordDeployment,
|
|
9019
9448
|
regenerateArtifacts,
|
|
9020
9449
|
regenerateRun,
|
|
9450
|
+
relocateAnchor,
|
|
9021
9451
|
renderCheck,
|
|
9022
9452
|
renderGoal,
|
|
9023
9453
|
renderTriage,
|