@jphutchins/code-review 0.1.0-alpha.52 → 0.1.0-alpha.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +359 -110
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/comment.eta +20 -5
package/dist/index.js
CHANGED
|
@@ -2,16 +2,45 @@
|
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
|
-
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
5
|
+
import { resolve as resolve$1, join, dirname, basename, extname, sep } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
7
7
|
import * as t from 'io-ts';
|
|
8
8
|
import parseDiff from 'parse-diff';
|
|
9
9
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
10
10
|
import _addFormats from 'ajv-formats';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
|
+
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
13
|
+
import { tmpdir } from 'os';
|
|
14
|
+
import { promisify } from 'util';
|
|
12
15
|
import { performance } from 'perf_hooks';
|
|
13
16
|
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
14
17
|
|
|
18
|
+
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
19
|
+
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
20
|
+
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
21
|
+
var modelIdentity = (configuredModelId) => configuredModelId.replace(/\[[12]m\]$/i, "");
|
|
22
|
+
var clipText = (body, max) => {
|
|
23
|
+
if (body.length <= max) return body;
|
|
24
|
+
const cut = body.slice(0, max);
|
|
25
|
+
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
26
|
+
return `${safe}
|
|
27
|
+
\u2026 [truncated]`;
|
|
28
|
+
};
|
|
29
|
+
var BODY_CLIP_CHARS = 4e3;
|
|
30
|
+
var tryParseJson = (text) => {
|
|
31
|
+
try {
|
|
32
|
+
return { ok: true, value: JSON.parse(text) };
|
|
33
|
+
} catch {
|
|
34
|
+
return { ok: false };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var readFileOrNull = (path) => {
|
|
38
|
+
try {
|
|
39
|
+
return readFileSync(path, "utf-8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
15
44
|
var SeverityCodec = t.union([
|
|
16
45
|
t.literal("critical"),
|
|
17
46
|
t.literal("major"),
|
|
@@ -520,16 +549,10 @@ var severityEmoji = (s) => {
|
|
|
520
549
|
return "\u2753";
|
|
521
550
|
}
|
|
522
551
|
};
|
|
523
|
-
var EMBED_LIMIT = 42700;
|
|
524
552
|
var FINDINGS_SCHEMA_URL = "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json";
|
|
525
|
-
var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
|
|
529
|
-
const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
|
|
530
|
-
return marker ? `${AGENTS_STOP_DIRECTIVE}
|
|
531
|
-
${marker}` : "";
|
|
532
|
-
};
|
|
553
|
+
var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment names a code-review findings document in the marker, and usually renders that same review as prose. Read the prose when it is there: it is the cheaper read. Fetch the document when the prose is not the review (this comment may be a status notice), when the prose is only part of it (findings anchored to diff lines, and those threads carry their own finding rather than a link \u2014 the workflow run's summary renders the review whole), or when you need a field the prose does not render \u2014 decode the marker where the comment carries it instead (an inline thread's finding, or a pre-#217 sticky). Read the document's schema_version and fetch the schema for THAT version before acting \u2014 a schema's own $id is its canonical URL, and the URL below is the current version, not a pinned one: ${FINDINGS_SCHEMA_URL} \u2014 then parse the WHOLE findings document, not only the fields you recognize. -->`;
|
|
554
|
+
var encodeMarker = (jsonUrl) => `${AGENTS_STOP_DIRECTIVE}
|
|
555
|
+
<!-- code-review:findings-json ${jsonUrl} -->`;
|
|
533
556
|
var decodeBase64Json = (b64) => {
|
|
534
557
|
try {
|
|
535
558
|
return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
|
|
@@ -537,12 +560,35 @@ var decodeBase64Json = (b64) => {
|
|
|
537
560
|
return void 0;
|
|
538
561
|
}
|
|
539
562
|
};
|
|
540
|
-
var findingsPointer = (
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
563
|
+
var findingsPointer = (jsonUrl) => encodeMarker(jsonUrl);
|
|
564
|
+
var warnedValveFindings = /* @__PURE__ */ new Set();
|
|
565
|
+
var INLINE_PROSE_CLIP_THRESHOLD_CHARS = 3e4;
|
|
566
|
+
var INLINE_EMBED_LIMIT_CHARS = 38e3;
|
|
567
|
+
var findingPayload = (finding, schemaVersion) => Buffer.from(
|
|
568
|
+
JSON.stringify({ schema_version: schemaVersion, findings: [finding] }),
|
|
569
|
+
"utf-8"
|
|
570
|
+
).toString("base64");
|
|
571
|
+
var lineRange = (startLine, endLine, separator) => startLine === endLine ? String(startLine) : `${String(startLine)}${separator}${String(endLine)}`;
|
|
572
|
+
var findingPointer = (finding, schemaVersion, jsonUrl) => {
|
|
573
|
+
const payload = findingPayload(finding, schemaVersion);
|
|
574
|
+
if (payload.length > INLINE_EMBED_LIMIT_CHARS) {
|
|
575
|
+
if (jsonUrl !== void 0) {
|
|
576
|
+
return `${AGENTS_STOP_DIRECTIVE}
|
|
577
|
+
<!-- code-review:findings-json ${jsonUrl} -->`;
|
|
578
|
+
}
|
|
579
|
+
const identity2 = `${finding.path}:${String(finding.start_line)}`;
|
|
580
|
+
if (!warnedValveFindings.has(identity2)) {
|
|
581
|
+
warnedValveFindings.add(identity2);
|
|
582
|
+
process.stderr.write(
|
|
583
|
+
`::warning::the finding at ${identity2} has a payload past the inline comment limit and no --json-url was supplied to name the artifact \u2014 the comment carries no findings marker
|
|
584
|
+
`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return "";
|
|
588
|
+
}
|
|
589
|
+
return `${AGENTS_STOP_DIRECTIVE}
|
|
590
|
+
<!-- code-review:findings-json;base64 ${payload} -->`;
|
|
544
591
|
};
|
|
545
|
-
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
546
592
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
547
593
|
var parseReviewedSha = (body) => {
|
|
548
594
|
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
@@ -819,6 +865,13 @@ var CONVERGENCE_RE = /<!-- code-review:convergence;base64 ([A-Za-z0-9+/=]+) -->/
|
|
|
819
865
|
var convergenceMarker = (convergence) => `<!-- code-review:convergence;base64 ${Buffer.from(JSON.stringify(convergence), "utf-8").toString(
|
|
820
866
|
"base64"
|
|
821
867
|
)} -->`;
|
|
868
|
+
var findingsMarkerPair = (jsonUrl, convergence) => {
|
|
869
|
+
const marker = jsonUrl !== void 0 ? findingsPointer(jsonUrl) : "";
|
|
870
|
+
if (convergence === void 0) return marker;
|
|
871
|
+
const conv = convergenceMarker(convergence);
|
|
872
|
+
return marker === "" ? conv : `${marker}
|
|
873
|
+
${conv}`;
|
|
874
|
+
};
|
|
822
875
|
var parseConvergenceMarker = (body) => {
|
|
823
876
|
const b64 = CONVERGENCE_RE.exec(body)?.[1];
|
|
824
877
|
return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
|
|
@@ -872,36 +925,9 @@ var formatConfidence = (n) => n.toFixed(2);
|
|
|
872
925
|
var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
873
926
|
const sha7 = headSha.slice(0, 7);
|
|
874
927
|
const summary = stickyUrl ? `the [summary comment](${stickyUrl})` : "the summary comment";
|
|
875
|
-
const
|
|
876
|
-
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${
|
|
877
|
-
};
|
|
878
|
-
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
879
|
-
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
880
|
-
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
881
|
-
var modelIdentity = (configuredModelId) => configuredModelId.replace(/\[[12]m\]$/i, "");
|
|
882
|
-
var clipText = (body, max) => {
|
|
883
|
-
if (body.length <= max) return body;
|
|
884
|
-
const cut = body.slice(0, max);
|
|
885
|
-
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
886
|
-
return `${safe}
|
|
887
|
-
\u2026 [truncated]`;
|
|
928
|
+
const run2 = runUrl ? ` See the [workflow run](${runUrl}) for ${runHasSummary ? "this round's review in full, its job log," : "the job log"} and the findings artifact.` : "";
|
|
929
|
+
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run2}`;
|
|
888
930
|
};
|
|
889
|
-
var tryParseJson = (text) => {
|
|
890
|
-
try {
|
|
891
|
-
return { ok: true, value: JSON.parse(text) };
|
|
892
|
-
} catch {
|
|
893
|
-
return { ok: false };
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
var readFileOrNull = (path) => {
|
|
897
|
-
try {
|
|
898
|
-
return readFileSync(path, "utf-8");
|
|
899
|
-
} catch {
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
// src/transcript.ts
|
|
905
931
|
var numField = (rec, key2) => {
|
|
906
932
|
const v = rec[key2];
|
|
907
933
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -1139,6 +1165,8 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1139
1165
|
return [...byKey.values()];
|
|
1140
1166
|
};
|
|
1141
1167
|
var matches = (f, e) => f.code !== void 0 && f.code !== "" ? e.code === f.code : e.code === "" && e.title === f.title;
|
|
1168
|
+
var isVerbatimReRaise = (f, e) => f.title === e.title && f.description === e.description && f.reasoning === e.reasoning && f.severity === e.severity && f.path === e.path && (f.patch ?? null) === e.patch;
|
|
1169
|
+
var isAnsweredDrop = (f, e) => matches(f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1142
1170
|
var answeredNoteKey = (f) => f.code !== void 0 && f.code !== "" ? f.code : `title:${f.title}`;
|
|
1143
1171
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1144
1172
|
var applyAnswered = (findings, registry) => {
|
|
@@ -1152,8 +1180,7 @@ var applyAnswered = (findings, registry) => {
|
|
|
1152
1180
|
kept.push(f);
|
|
1153
1181
|
continue;
|
|
1154
1182
|
}
|
|
1155
|
-
|
|
1156
|
-
if (verbatim && f.severity !== "critical") {
|
|
1183
|
+
if (isAnsweredDrop(f, entry)) {
|
|
1157
1184
|
droppedByKey.set(answeredNoteKey(f), entry);
|
|
1158
1185
|
droppedCount += 1;
|
|
1159
1186
|
} else {
|
|
@@ -1206,26 +1233,69 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1206
1233
|
|
|
1207
1234
|
// src/render.ts
|
|
1208
1235
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1209
|
-
var
|
|
1236
|
+
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1237
|
+
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
1238
|
+
var permalinkFor = (base, f, anchor) => {
|
|
1239
|
+
if (f.path === "" || f.side === "LEFT") return void 0;
|
|
1240
|
+
const path = f.path.split("/").map(
|
|
1241
|
+
(segment) => encodeAutolinkParens(
|
|
1242
|
+
encodeURIComponent(
|
|
1243
|
+
segment.replace(
|
|
1244
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
1245
|
+
"\uFFFD"
|
|
1246
|
+
)
|
|
1247
|
+
)
|
|
1248
|
+
// Markdown emphasis/syntax characters: `__` runs split the bare URL across CommonMark
|
|
1249
|
+
// nodes and truncate the autolink, and a path-only link ending in one of these loses its
|
|
1250
|
+
// tail to the autolinker's trailing-punctuation trim (issue #231 r4).
|
|
1251
|
+
).replace(/_/g, "%5F").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/!/g, "%21")
|
|
1252
|
+
).join("/");
|
|
1253
|
+
return anchor ? `${base}${path}#L${lineRange(f.start_line, f.end_line, "-L")}` : `${base}${path}`;
|
|
1254
|
+
};
|
|
1255
|
+
var CARRIED_TOTAL_CHARS = 4e4;
|
|
1256
|
+
var SUPPRESSED_NIT_BLOCK_OVERHEAD = 280;
|
|
1257
|
+
var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
|
|
1210
1258
|
const key2 = answeredNoteKey(f);
|
|
1259
|
+
const anchored = permalinkBase !== void 0 && !(unanchored?.has(f) ?? false);
|
|
1260
|
+
const permalink = permalinkBase === void 0 ? void 0 : permalinkFor(permalinkBase, f, anchored);
|
|
1211
1261
|
return {
|
|
1212
1262
|
...f,
|
|
1213
1263
|
title: escapePipes(f.title),
|
|
1214
1264
|
path: escapeCodeBackticks(f.path),
|
|
1265
|
+
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code), codeKey: f.code } : {},
|
|
1266
|
+
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1267
|
+
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1268
|
+
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
1215
1269
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1216
1270
|
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1217
1271
|
};
|
|
1218
1272
|
};
|
|
1273
|
+
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1219
1274
|
var sanitizeSuppressedNit = (f) => ({
|
|
1220
1275
|
title: escapeCodeBackticks(f.title),
|
|
1221
1276
|
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code) } : {},
|
|
1222
|
-
|
|
1277
|
+
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1278
|
+
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1223
1279
|
startLine: f.start_line,
|
|
1224
|
-
|
|
1280
|
+
endLine: f.end_line,
|
|
1281
|
+
...f.side !== void 0 ? { side: f.side } : {},
|
|
1282
|
+
severity: f.severity,
|
|
1283
|
+
confidence: formatConfidence(f.confidence),
|
|
1284
|
+
likelihood: formatConfidence(f.likelihood),
|
|
1285
|
+
m: formatConfidence(f.confidence * f.likelihood),
|
|
1286
|
+
carried: carriedLines(f)
|
|
1225
1287
|
});
|
|
1288
|
+
var carriedLines = (f) => [
|
|
1289
|
+
`description: ${clipText(f.description, BODY_CLIP_CHARS)}`,
|
|
1290
|
+
...f.recommendation !== void 0 ? [`recommendation: ${clipText(f.recommendation, BODY_CLIP_CHARS)}`] : [],
|
|
1291
|
+
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1292
|
+
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1293
|
+
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
1226
1294
|
var sanitizeSystemic = (s) => ({
|
|
1227
1295
|
...s,
|
|
1228
1296
|
title: escapePipes(s.title),
|
|
1297
|
+
...s.code !== void 0 ? { code: escapeCodeBackticks(s.code) } : {},
|
|
1298
|
+
...s.code_url !== void 0 ? { code_url: linkSafeUrl(s.code_url) } : {},
|
|
1229
1299
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1230
1300
|
...s.finding_codes !== void 0 ? { finding_codes: s.finding_codes.map(escapeCodeBackticks) } : {}
|
|
1231
1301
|
});
|
|
@@ -1258,6 +1328,16 @@ var render = (input) => {
|
|
|
1258
1328
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1259
1329
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
1260
1330
|
const advisoryAllowed = isFullReviewRound;
|
|
1331
|
+
const suppressedBudget = (input.suppressedNits ?? []).map(sanitizeSuppressedNit).reduce(
|
|
1332
|
+
(acc, n) => {
|
|
1333
|
+
const size = n.carried.reduce((sum, line) => sum + line.length + 3, 0) + SUPPRESSED_NIT_BLOCK_OVERHEAD + n.title.length + n.path.length * 2 + (n.code?.length ?? 0) + (n.codeUrl?.length ?? 0) + String(n.startLine).length * 2 + String(n.endLine).length + (n.side !== void 0 ? n.side.length + 2 : 0) + // The summary line's wrappers: backticks around the code and the [](...) around the link.
|
|
1334
|
+
(n.code !== void 0 ? n.code.length + 2 : 0) + (n.codeUrl !== void 0 ? n.codeUrl.length + 4 : 0);
|
|
1335
|
+
return acc.used + size > CARRIED_TOTAL_CHARS ? { list: acc.list, used: acc.used, dropped: acc.dropped + 1 } : { list: [...acc.list, n], used: acc.used + size, dropped: acc.dropped };
|
|
1336
|
+
},
|
|
1337
|
+
{ list: [], used: 0, dropped: 0 }
|
|
1338
|
+
);
|
|
1339
|
+
const permalinkBase = input.repo !== void 0 && input.repo !== "" && input.reviewedSha !== void 0 && input.reviewedSha !== "" ? `https://github.com/${input.repo}/blob/${input.reviewedSha}/` : void 0;
|
|
1340
|
+
const unanchored = new Set(input.unanchoredStrays ?? []);
|
|
1261
1341
|
return eta.renderString(input.template, {
|
|
1262
1342
|
findings: input.findings,
|
|
1263
1343
|
envelope: input.envelope,
|
|
@@ -1278,8 +1358,11 @@ var render = (input) => {
|
|
|
1278
1358
|
postedAt: input.postedAt ?? "",
|
|
1279
1359
|
severityCounts,
|
|
1280
1360
|
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
1281
|
-
strays: (input.strays ?? []).map(
|
|
1282
|
-
|
|
1361
|
+
strays: (input.strays ?? []).map(
|
|
1362
|
+
(f) => sanitizeFinding(f, input.answeredNotes, permalinkBase, unanchored)
|
|
1363
|
+
),
|
|
1364
|
+
suppressedNits: suppressedBudget.list,
|
|
1365
|
+
carriedDroppedNits: suppressedBudget.dropped,
|
|
1283
1366
|
nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
|
|
1284
1367
|
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
1285
1368
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
@@ -1287,10 +1370,11 @@ var render = (input) => {
|
|
|
1287
1370
|
unverifiedNoLogs: input.unverifiedNoLogs === true,
|
|
1288
1371
|
runUrl: input.runUrl ?? null,
|
|
1289
1372
|
jsonUrl: input.jsonUrl ?? null,
|
|
1290
|
-
// The
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1373
|
+
// The marker names the findings artifact, so the convergence no longer rides inside the comment —
|
|
1374
|
+
// it needs its own compact marker beside the link or a trajectory would require a fetch to read
|
|
1375
|
+
// (issue #217). post precomputes both together in findingsBlob; the standalone `render` command
|
|
1376
|
+
// builds the same pair here, so neither path can emit a link with the convergence missing.
|
|
1377
|
+
findingsPointer: input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence),
|
|
1294
1378
|
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
1295
1379
|
metastasisNote: advisoryAllowed ? metastasisNote(trajectory) : "",
|
|
1296
1380
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
@@ -1406,6 +1490,15 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1406
1490
|
};
|
|
1407
1491
|
const comments = inDiff.map((f) => {
|
|
1408
1492
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1493
|
+
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
1494
|
+
const view = clipProse ? {
|
|
1495
|
+
...f,
|
|
1496
|
+
title: clipText(f.title, BODY_CLIP_CHARS),
|
|
1497
|
+
description: clipText(f.description, BODY_CLIP_CHARS),
|
|
1498
|
+
...f.recommendation != null ? { recommendation: clipText(f.recommendation, BODY_CLIP_CHARS) } : {},
|
|
1499
|
+
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
1500
|
+
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
1501
|
+
} : f;
|
|
1409
1502
|
const sameRootNote = noteFor(f, context.sameRootNotes);
|
|
1410
1503
|
const answeredNote2 = noteFor(f, context.answeredNotes);
|
|
1411
1504
|
const comment = {
|
|
@@ -1413,7 +1506,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1413
1506
|
line: f.end_line,
|
|
1414
1507
|
side: defaultSide(f.side),
|
|
1415
1508
|
body: renderCommentBody(
|
|
1416
|
-
|
|
1509
|
+
view,
|
|
1417
1510
|
eta,
|
|
1418
1511
|
inlineTemplate,
|
|
1419
1512
|
modelsText,
|
|
@@ -1990,7 +2083,7 @@ var classifyExecError = (err, stderr, timeoutMs) => {
|
|
|
1990
2083
|
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
1991
2084
|
return stderrStr || errMsg(err);
|
|
1992
2085
|
};
|
|
1993
|
-
var execFileWithTimeout = (spec) => new Promise((
|
|
2086
|
+
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
1994
2087
|
const child = execFile(
|
|
1995
2088
|
spec.command,
|
|
1996
2089
|
[...spec.args],
|
|
@@ -2006,7 +2099,7 @@ var execFileWithTimeout = (spec) => new Promise((resolve3, reject) => {
|
|
|
2006
2099
|
reject(
|
|
2007
2100
|
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
2008
2101
|
);
|
|
2009
|
-
else
|
|
2102
|
+
else resolve4(stdout);
|
|
2010
2103
|
}
|
|
2011
2104
|
);
|
|
2012
2105
|
if (spec.stdin !== void 0) {
|
|
@@ -2017,14 +2110,119 @@ var execFileWithTimeout = (spec) => new Promise((resolve3, reject) => {
|
|
|
2017
2110
|
|
|
2018
2111
|
// src/gh.ts
|
|
2019
2112
|
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
2020
|
-
var
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2113
|
+
var flagFirst;
|
|
2114
|
+
var withEscapeRetry = async (run2, idempotent) => {
|
|
2115
|
+
if (flagFirst) return run2(true);
|
|
2116
|
+
try {
|
|
2117
|
+
return await run2(false);
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
2120
|
+
if (!idempotent) throw err;
|
|
2121
|
+
flagFirst = true;
|
|
2122
|
+
return run2(true);
|
|
2123
|
+
}
|
|
2124
|
+
throw err;
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
2128
|
+
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
2129
|
+
(withFlag) => execFileWithTimeout({
|
|
2130
|
+
command: "gh",
|
|
2131
|
+
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
2132
|
+
label: `gh api ${describeEndpoint(args)}`,
|
|
2133
|
+
timeoutMs: subprocessTimeoutMs(),
|
|
2134
|
+
env,
|
|
2135
|
+
stdin
|
|
2136
|
+
}),
|
|
2137
|
+
isIdempotentCall(args)
|
|
2138
|
+
);
|
|
2139
|
+
var run = promisify(execFile);
|
|
2140
|
+
var STEP_TIMEOUT_MS = 6e4;
|
|
2141
|
+
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
2142
|
+
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
2143
|
+
var containedPath = (dir, member) => {
|
|
2144
|
+
const target = resolve$1(dir, member);
|
|
2145
|
+
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
2146
|
+
};
|
|
2147
|
+
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
2148
|
+
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
2149
|
+
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
2150
|
+
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
2151
|
+
var readArtifactFindings = (ghApiPath) => {
|
|
2152
|
+
return async (zipUrl) => {
|
|
2153
|
+
try {
|
|
2154
|
+
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
2155
|
+
const zip = join(dir, "findings.zip");
|
|
2156
|
+
try {
|
|
2157
|
+
await ghApiPath(zipUrl, zip);
|
|
2158
|
+
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
2159
|
+
encoding: "utf-8",
|
|
2160
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
2161
|
+
timeout: STEP_TIMEOUT_MS
|
|
2162
|
+
});
|
|
2163
|
+
const member = locateFindingsMember(listing);
|
|
2164
|
+
if (member === null) return null;
|
|
2165
|
+
await run("unzip", [zip, "-d", dir], {
|
|
2166
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
2167
|
+
timeout: STEP_TIMEOUT_MS
|
|
2168
|
+
});
|
|
2169
|
+
const target = containedPath(dir, member);
|
|
2170
|
+
if (target === null) {
|
|
2171
|
+
process.stderr.write(
|
|
2172
|
+
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
2173
|
+
);
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
return await readFile(target, "utf-8");
|
|
2177
|
+
} finally {
|
|
2178
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2179
|
+
}
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
process.stderr.write(
|
|
2182
|
+
annotationSafe(
|
|
2183
|
+
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2184
|
+
)
|
|
2185
|
+
);
|
|
2186
|
+
return null;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
};
|
|
2190
|
+
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
2191
|
+
const { stdout } = await withEscapeRetry(
|
|
2192
|
+
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
2193
|
+
encoding: "buffer",
|
|
2194
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
2195
|
+
timeout: STEP_TIMEOUT_MS
|
|
2196
|
+
}),
|
|
2197
|
+
true
|
|
2198
|
+
);
|
|
2199
|
+
await writeFile(outPath, stdout);
|
|
2027
2200
|
});
|
|
2201
|
+
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
2202
|
+
var withStampedConvergence = (doc, body) => {
|
|
2203
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
2204
|
+
const stripped = withoutKey(doc, "convergence");
|
|
2205
|
+
const stamped = parseConvergenceMarker(body);
|
|
2206
|
+
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
2207
|
+
};
|
|
2208
|
+
var resolvePriorFindings = async (body, read) => {
|
|
2209
|
+
const embedded = parseFindingsMarker(body);
|
|
2210
|
+
if (embedded !== null) return embedded;
|
|
2211
|
+
const url = findingsArtifactUrl(body);
|
|
2212
|
+
if (url === null) return null;
|
|
2213
|
+
const text = await read(url);
|
|
2214
|
+
if (text === null) return null;
|
|
2215
|
+
try {
|
|
2216
|
+
return withStampedConvergence(JSON.parse(text), body);
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
process.stderr.write(
|
|
2219
|
+
annotationSafe(
|
|
2220
|
+
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2221
|
+
)
|
|
2222
|
+
);
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2028
2226
|
|
|
2029
2227
|
// src/pr.ts
|
|
2030
2228
|
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
@@ -2521,7 +2719,7 @@ var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
|
2521
2719
|
);
|
|
2522
2720
|
}
|
|
2523
2721
|
};
|
|
2524
|
-
var post = async (input, ghApi = runGhApi) => {
|
|
2722
|
+
var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
2525
2723
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
2526
2724
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
2527
2725
|
if (resolution.kind === "none") {
|
|
@@ -2554,14 +2752,20 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
2554
2752
|
...doc,
|
|
2555
2753
|
convergence: conv ?? void 0
|
|
2556
2754
|
});
|
|
2755
|
+
const carriedFindingsLink = !input.jsonUrl && existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
2756
|
+
let warnedNoJsonUrl = false;
|
|
2557
2757
|
const findingsBlob = (doc) => {
|
|
2558
|
-
|
|
2559
|
-
if (
|
|
2560
|
-
return
|
|
2758
|
+
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
2759
|
+
if (carriedFindingsLink !== null) {
|
|
2760
|
+
return findingsMarkerPair(carriedFindingsLink, doc.convergence);
|
|
2561
2761
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2762
|
+
if (!warnedNoJsonUrl) {
|
|
2763
|
+
warnedNoJsonUrl = true;
|
|
2764
|
+
process.stderr.write(
|
|
2765
|
+
"::error::no --json-url was supplied and the existing sticky carries no findings marker to carry forward, so this comment names no findings artifact \u2014 the review's prose is intact but its machine channel is gone, and the next round cannot seed from it\n"
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
return findingsMarkerPair(void 0, doc.convergence);
|
|
2565
2769
|
};
|
|
2566
2770
|
const leaveInPlace = (message) => {
|
|
2567
2771
|
process.stderr.write(
|
|
@@ -2672,8 +2876,12 @@ ${dropNote}` : ""}`,
|
|
|
2672
2876
|
findings: [...answeredFilter.findings],
|
|
2673
2877
|
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
2674
2878
|
};
|
|
2879
|
+
const roundHasNit = findings.findings.some((f) => f.severity === "nit");
|
|
2880
|
+
const priorDocForNits = roundHasNit && existingSticky !== null && isFullReviewSticky(existingSticky.body) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
2675
2881
|
const priorSuppressedKeys = new Set(
|
|
2676
|
-
|
|
2882
|
+
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2883
|
+
(n) => answeredNoteKey({ code: n.code, title: n.title })
|
|
2884
|
+
)
|
|
2677
2885
|
);
|
|
2678
2886
|
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || priorSuppressedKeys.has(answeredNoteKey(f)));
|
|
2679
2887
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
@@ -2702,6 +2910,7 @@ ${dropNote}` : ""}`,
|
|
|
2702
2910
|
template,
|
|
2703
2911
|
route: effectiveRoute,
|
|
2704
2912
|
reviewedSha: input.headSha,
|
|
2913
|
+
repo: input.headRepo || input.repo,
|
|
2705
2914
|
effort: input.effort,
|
|
2706
2915
|
sameRootNotes: {},
|
|
2707
2916
|
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
@@ -2785,16 +2994,6 @@ ${dropNote}` : ""}`,
|
|
|
2785
2994
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
2786
2995
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
2787
2996
|
const findingsMarker = findingsBlob(stampedFindings);
|
|
2788
|
-
const markerForm = findingsMarkerForm(stampedFindings, input.jsonUrl);
|
|
2789
|
-
if (markerForm === "link") {
|
|
2790
|
-
process.stderr.write(
|
|
2791
|
-
"Warning: the findings-json blob exceeds the embed limit \u2014 degraded to the jsonUrl-link form; the convergence rides a compact marker beside it, but a decoding agent (and the next-round seed) must fetch the artifact for the FINDINGS\n"
|
|
2792
|
-
);
|
|
2793
|
-
} else if (markerForm === "omitted") {
|
|
2794
|
-
process.stderr.write(
|
|
2795
|
-
"Warning: the findings-json blob exceeds the embed limit and no --json-url was given \u2014 the convergence rides a compact marker but the embedded findings seed is dropped from the posted surfaces\n"
|
|
2796
|
-
);
|
|
2797
|
-
}
|
|
2798
2997
|
const commonRenderInput = {
|
|
2799
2998
|
findings: stampedFindings,
|
|
2800
2999
|
envelope,
|
|
@@ -2804,6 +3003,7 @@ ${dropNote}` : ""}`,
|
|
|
2804
3003
|
template,
|
|
2805
3004
|
route: effectiveRoute,
|
|
2806
3005
|
reviewedSha: input.headSha,
|
|
3006
|
+
repo: input.headRepo || input.repo,
|
|
2807
3007
|
effort: input.effort,
|
|
2808
3008
|
testReport,
|
|
2809
3009
|
clocDiff,
|
|
@@ -2830,15 +3030,21 @@ ${dropNote}` : ""}`,
|
|
|
2830
3030
|
|
|
2831
3031
|
> **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
|
|
2832
3032
|
` : "";
|
|
2833
|
-
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
|
|
3033
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays) => formatMarkdown(
|
|
2834
3034
|
render({
|
|
2835
3035
|
...commonRenderInput,
|
|
2836
3036
|
...straysOverride ? { strays: straysOverride } : {},
|
|
2837
3037
|
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
3038
|
+
...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
|
|
2838
3039
|
inlineDisposition,
|
|
2839
3040
|
reviewUrl: reviewUrl2
|
|
2840
3041
|
}) + longFilesNote
|
|
2841
3042
|
);
|
|
3043
|
+
if (!input.jsonUrl && existingSticky !== null && parseReviewComplete(existingSticky.body) && parseReviewedRoute(existingSticky.body) === "full review" && hasFindingsMarker(existingSticky.body)) {
|
|
3044
|
+
leaveInPlace(
|
|
3045
|
+
"no --json-url was supplied and the existing sticky still carries the prior findings document's marker (embedded or link) \u2014 leaving it in place rather than severing the seed chain\n"
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
2842
3048
|
const stickyRef = await upsertSticky(
|
|
2843
3049
|
input.repo,
|
|
2844
3050
|
prNumber,
|
|
@@ -2886,7 +3092,7 @@ ${dropNote}` : ""}`,
|
|
|
2886
3092
|
await patchComment(
|
|
2887
3093
|
input.repo,
|
|
2888
3094
|
stickyRef.id,
|
|
2889
|
-
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
|
|
3095
|
+
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount, unposted),
|
|
2890
3096
|
ghApi
|
|
2891
3097
|
);
|
|
2892
3098
|
process.stderr.write(
|
|
@@ -2905,7 +3111,13 @@ ${dropNote}` : ""}`,
|
|
|
2905
3111
|
() => renderBody(
|
|
2906
3112
|
{ kind: "whole-document", inlineCount: inlinePosted, rejectedCount: unanchoredCount },
|
|
2907
3113
|
reviewUrl,
|
|
2908
|
-
visibleFindings
|
|
3114
|
+
visibleFindings,
|
|
3115
|
+
void 0,
|
|
3116
|
+
// The rejected-anchor invariant holds on EVERY surface, the run summary included — this
|
|
3117
|
+
// document deliberately carries every finding, so the GitHub-rejected ones must keep their
|
|
3118
|
+
// path-only links here too (issue #231 r2). Unconditional: renderBody's own gate treats an
|
|
3119
|
+
// empty array exactly like absence, so the ternary was a duplicated decision (issue #231 r3).
|
|
3120
|
+
unposted
|
|
2909
3121
|
)
|
|
2910
3122
|
);
|
|
2911
3123
|
};
|
|
@@ -3247,10 +3459,10 @@ var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi,
|
|
|
3247
3459
|
}
|
|
3248
3460
|
};
|
|
3249
3461
|
const poll = async (lastSeenNames, lastRunId) => {
|
|
3250
|
-
const { run, seenNames } = await safeResolve();
|
|
3251
|
-
if (
|
|
3252
|
-
return { kind: "concluded", conclusion:
|
|
3253
|
-
const runId =
|
|
3462
|
+
const { run: run2, seenNames } = await safeResolve();
|
|
3463
|
+
if (run2 !== null && run2.status === "completed" && run2.conclusion !== null)
|
|
3464
|
+
return { kind: "concluded", conclusion: run2.conclusion, runId: run2.id };
|
|
3465
|
+
const runId = run2 === null ? lastRunId : run2.id;
|
|
3254
3466
|
const names = seenNames.length > 0 ? seenNames : lastSeenNames;
|
|
3255
3467
|
if (deps.elapsedMs() >= options.timeoutMs)
|
|
3256
3468
|
return { kind: "timed-out", runId, seenNames: names };
|
|
@@ -3421,8 +3633,7 @@ var priorReviewFrom = (comments, botLogin) => {
|
|
|
3421
3633
|
return last ? { id: last.id, body: last.body } : null;
|
|
3422
3634
|
};
|
|
3423
3635
|
var MAX_CONVERSATION_COMMENTS = 50;
|
|
3424
|
-
var
|
|
3425
|
-
var clip = (body) => clipText(body, MAX_CONVERSATION_BODY_CHARS);
|
|
3636
|
+
var clip = (body) => clipText(body, BODY_CLIP_CHARS);
|
|
3426
3637
|
var boundedHuman = (items, botLogin, label, project) => {
|
|
3427
3638
|
const human = items.filter(
|
|
3428
3639
|
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
@@ -3495,7 +3706,7 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
|
3495
3706
|
}
|
|
3496
3707
|
return { staged, failing: failing.length };
|
|
3497
3708
|
};
|
|
3498
|
-
var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
3709
|
+
var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghArtifactReader) => {
|
|
3499
3710
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
3500
3711
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
3501
3712
|
if (resolution.kind === "none") {
|
|
@@ -3550,6 +3761,13 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
3550
3761
|
join(input.outDir, "prior_review.json"),
|
|
3551
3762
|
prior === null ? "null" : JSON.stringify(prior)
|
|
3552
3763
|
);
|
|
3764
|
+
const seedsFromPrior = input.conclusion === "success" && prior !== null && prior.body !== null && parseReviewedRoute(prior.body) === "full review";
|
|
3765
|
+
const resolvedPrior = seedsFromPrior ? await resolvePriorFindings(prior.body, readArtifact) : null;
|
|
3766
|
+
const priorFindings = resolvedPrior !== null && typeof resolvedPrior === "object" ? resolvedPrior : null;
|
|
3767
|
+
writeFileSync(
|
|
3768
|
+
join(input.outDir, "prior_findings.json"),
|
|
3769
|
+
priorFindings === null ? "null" : JSON.stringify(priorFindings)
|
|
3770
|
+
);
|
|
3553
3771
|
const answered = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
|
|
3554
3772
|
writeFileSync(
|
|
3555
3773
|
join(input.outDir, "answered.json"),
|
|
@@ -4132,6 +4350,9 @@ var renderCmd = defineCommand({
|
|
|
4132
4350
|
pricedAt: /* @__PURE__ */ new Date()
|
|
4133
4351
|
});
|
|
4134
4352
|
process.stdout.write(output2);
|
|
4353
|
+
process.stderr.write(
|
|
4354
|
+
"::error::no --json-url was supplied, so this comment names no findings artifact \u2014 the review's prose is intact but its machine channel is gone, and the next round cannot seed from it\n"
|
|
4355
|
+
);
|
|
4135
4356
|
}
|
|
4136
4357
|
});
|
|
4137
4358
|
var inlineCmd = defineCommand({
|
|
@@ -4599,6 +4820,10 @@ var seedDraftCmd = defineCommand({
|
|
|
4599
4820
|
type: "string",
|
|
4600
4821
|
description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and delivered as re-review context when it validates against the schema"
|
|
4601
4822
|
},
|
|
4823
|
+
"prior-findings": {
|
|
4824
|
+
type: "string",
|
|
4825
|
+
description: "Path to the gather-staged prior findings document (prior_findings.json). gather resolves it \u2014 from the sticky's embedded blob, or by fetching the artifact the marker names \u2014 because gather holds the repo token and this step deliberately does not: it runs the jailed agent over untrusted PR code. Absent or null falls back to decoding an embedded blob out of --prior, which needs no token"
|
|
4826
|
+
},
|
|
4602
4827
|
"prior-answers": {
|
|
4603
4828
|
type: "string",
|
|
4604
4829
|
description: "Path to the gather-staged answered-findings registry (answered.json) \u2014 the prior inline findings whose threads a human reply answered (issue #151). Delivered out-of-band to the .prior-answers sidecar beside the prior context so the next-round agent sees the already-answered state; best-effort, never fails the seed"
|
|
@@ -4666,20 +4891,20 @@ var seedDraftCmd = defineCommand({
|
|
|
4666
4891
|
})();
|
|
4667
4892
|
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
4668
4893
|
})();
|
|
4669
|
-
const
|
|
4894
|
+
const stagedPrior = (() => {
|
|
4895
|
+
if (!args["prior-findings"]) return null;
|
|
4896
|
+
try {
|
|
4897
|
+
return JSON.parse(readFileSync(resolve$1(args["prior-findings"]), "utf-8"));
|
|
4898
|
+
} catch {
|
|
4899
|
+
return null;
|
|
4900
|
+
}
|
|
4901
|
+
})();
|
|
4902
|
+
const parsedPrior = stagedPrior !== null ? stagedPrior : priorBody === null ? null : parseFindingsMarker(priorBody);
|
|
4670
4903
|
const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
|
|
4671
4904
|
isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
|
|
4672
4905
|
);
|
|
4673
|
-
const
|
|
4674
|
-
if (
|
|
4675
|
-
return strippedPrior;
|
|
4676
|
-
const carried = strippedPrior["scope_metastasis"];
|
|
4677
|
-
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return strippedPrior;
|
|
4678
|
-
if (strippedPrior["verdict"] === "error") return strippedPrior;
|
|
4679
|
-
const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
|
|
4680
|
-
return computed === null ? strippedPrior : { ...strippedPrior, scope_metastasis: computed };
|
|
4681
|
-
})();
|
|
4682
|
-
if (args["prior-answers"]) {
|
|
4906
|
+
const answeredRegistry = (() => {
|
|
4907
|
+
if (!args["prior-answers"]) return null;
|
|
4683
4908
|
try {
|
|
4684
4909
|
const raw = JSON.parse(readFileSync(resolve$1(args["prior-answers"]), "utf-8"));
|
|
4685
4910
|
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
@@ -4693,18 +4918,38 @@ var seedDraftCmd = defineCommand({
|
|
|
4693
4918
|
`
|
|
4694
4919
|
);
|
|
4695
4920
|
}
|
|
4696
|
-
|
|
4697
|
-
`);
|
|
4698
|
-
process.stderr.write(
|
|
4699
|
-
`Seeded ${priorAnswersPath(outPath)} with ${String(decoded.length)} answered finding(s) as context
|
|
4700
|
-
`
|
|
4701
|
-
);
|
|
4921
|
+
return decoded;
|
|
4702
4922
|
} catch (err) {
|
|
4703
4923
|
process.stderr.write(
|
|
4704
4924
|
`Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
|
|
4705
4925
|
`
|
|
4706
4926
|
);
|
|
4927
|
+
return null;
|
|
4707
4928
|
}
|
|
4929
|
+
})();
|
|
4930
|
+
const priorFindings = (() => {
|
|
4931
|
+
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4932
|
+
return strippedPrior;
|
|
4933
|
+
const raw = strippedPrior;
|
|
4934
|
+
const doc = answeredRegistry !== null && answeredRegistry.length > 0 && Array.isArray(raw["findings"]) ? {
|
|
4935
|
+
...raw,
|
|
4936
|
+
findings: raw["findings"].filter(
|
|
4937
|
+
(f) => !answeredRegistry.some((e) => isAnsweredDrop(f, e))
|
|
4938
|
+
)
|
|
4939
|
+
} : raw;
|
|
4940
|
+
const carried = doc["scope_metastasis"];
|
|
4941
|
+
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
4942
|
+
if (doc["verdict"] === "error") return doc;
|
|
4943
|
+
const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
|
|
4944
|
+
return computed === null ? doc : { ...doc, scope_metastasis: computed };
|
|
4945
|
+
})();
|
|
4946
|
+
if (answeredRegistry !== null) {
|
|
4947
|
+
writeFileSync(priorAnswersPath(outPath), `${JSON.stringify(answeredRegistry, null, 2)}
|
|
4948
|
+
`);
|
|
4949
|
+
process.stderr.write(
|
|
4950
|
+
`Seeded ${priorAnswersPath(outPath)} with ${String(answeredRegistry.length)} answered finding(s) as context
|
|
4951
|
+
`
|
|
4952
|
+
);
|
|
4708
4953
|
}
|
|
4709
4954
|
if (parsedPrior !== null && parseReviewedRoute(priorBody ?? "") === "full review") {
|
|
4710
4955
|
try {
|
|
@@ -5259,6 +5504,10 @@ var postCmd = defineCommand({
|
|
|
5259
5504
|
const priceResolution = resolvePrices(args.prices);
|
|
5260
5505
|
await post({
|
|
5261
5506
|
repo: args.repo,
|
|
5507
|
+
// The workflow's post step threads HEAD_REPO env (the fork's owner/name) — a finding
|
|
5508
|
+
// permalink targets the tree the reviewed SHA lives in (issue #231 r1). Absent/empty ⇒ the
|
|
5509
|
+
// base repo. Env rather than a flag: an older pinned CLI simply ignores it.
|
|
5510
|
+
headRepo: process.env["HEAD_REPO"] || void 0,
|
|
5262
5511
|
headSha: args["head-sha"],
|
|
5263
5512
|
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
5264
5513
|
findingsPath: args.findings,
|