@jphutchins/code-review 0.1.0-alpha.52 → 0.1.0-alpha.53
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 +286 -98
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/comment.eta +16 -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,34 @@ 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 findingPointer = (finding, schemaVersion, jsonUrl) => {
|
|
572
|
+
const payload = findingPayload(finding, schemaVersion);
|
|
573
|
+
if (payload.length > INLINE_EMBED_LIMIT_CHARS) {
|
|
574
|
+
if (jsonUrl !== void 0) {
|
|
575
|
+
return `${AGENTS_STOP_DIRECTIVE}
|
|
576
|
+
<!-- code-review:findings-json ${jsonUrl} -->`;
|
|
577
|
+
}
|
|
578
|
+
const identity2 = `${finding.path}:${String(finding.start_line)}`;
|
|
579
|
+
if (!warnedValveFindings.has(identity2)) {
|
|
580
|
+
warnedValveFindings.add(identity2);
|
|
581
|
+
process.stderr.write(
|
|
582
|
+
`::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
|
|
583
|
+
`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
return `${AGENTS_STOP_DIRECTIVE}
|
|
589
|
+
<!-- code-review:findings-json;base64 ${payload} -->`;
|
|
544
590
|
};
|
|
545
|
-
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
546
591
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
547
592
|
var parseReviewedSha = (body) => {
|
|
548
593
|
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
@@ -819,6 +864,13 @@ var CONVERGENCE_RE = /<!-- code-review:convergence;base64 ([A-Za-z0-9+/=]+) -->/
|
|
|
819
864
|
var convergenceMarker = (convergence) => `<!-- code-review:convergence;base64 ${Buffer.from(JSON.stringify(convergence), "utf-8").toString(
|
|
820
865
|
"base64"
|
|
821
866
|
)} -->`;
|
|
867
|
+
var findingsMarkerPair = (jsonUrl, convergence) => {
|
|
868
|
+
const marker = jsonUrl !== void 0 ? findingsPointer(jsonUrl) : "";
|
|
869
|
+
if (convergence === void 0) return marker;
|
|
870
|
+
const conv = convergenceMarker(convergence);
|
|
871
|
+
return marker === "" ? conv : `${marker}
|
|
872
|
+
${conv}`;
|
|
873
|
+
};
|
|
822
874
|
var parseConvergenceMarker = (body) => {
|
|
823
875
|
const b64 = CONVERGENCE_RE.exec(body)?.[1];
|
|
824
876
|
return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
|
|
@@ -872,36 +924,9 @@ var formatConfidence = (n) => n.toFixed(2);
|
|
|
872
924
|
var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
873
925
|
const sha7 = headSha.slice(0, 7);
|
|
874
926
|
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.${
|
|
927
|
+
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.` : "";
|
|
928
|
+
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run2}`;
|
|
877
929
|
};
|
|
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]`;
|
|
888
|
-
};
|
|
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
930
|
var numField = (rec, key2) => {
|
|
906
931
|
const v = rec[key2];
|
|
907
932
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -1139,6 +1164,8 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1139
1164
|
return [...byKey.values()];
|
|
1140
1165
|
};
|
|
1141
1166
|
var matches = (f, e) => f.code !== void 0 && f.code !== "" ? e.code === f.code : e.code === "" && e.title === f.title;
|
|
1167
|
+
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;
|
|
1168
|
+
var isAnsweredDrop = (f, e) => matches(f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1142
1169
|
var answeredNoteKey = (f) => f.code !== void 0 && f.code !== "" ? f.code : `title:${f.title}`;
|
|
1143
1170
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1144
1171
|
var applyAnswered = (findings, registry) => {
|
|
@@ -1152,8 +1179,7 @@ var applyAnswered = (findings, registry) => {
|
|
|
1152
1179
|
kept.push(f);
|
|
1153
1180
|
continue;
|
|
1154
1181
|
}
|
|
1155
|
-
|
|
1156
|
-
if (verbatim && f.severity !== "critical") {
|
|
1182
|
+
if (isAnsweredDrop(f, entry)) {
|
|
1157
1183
|
droppedByKey.set(answeredNoteKey(f), entry);
|
|
1158
1184
|
droppedCount += 1;
|
|
1159
1185
|
} else {
|
|
@@ -1206,26 +1232,47 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1206
1232
|
|
|
1207
1233
|
// src/render.ts
|
|
1208
1234
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1235
|
+
var linkSafeUrl = (url) => escapeCodeBackticks(url).replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1236
|
+
var CARRIED_TOTAL_CHARS = 4e4;
|
|
1237
|
+
var SUPPRESSED_NIT_BLOCK_OVERHEAD = 280;
|
|
1209
1238
|
var sanitizeFinding = (f, answeredNotes) => {
|
|
1210
1239
|
const key2 = answeredNoteKey(f);
|
|
1211
1240
|
return {
|
|
1212
1241
|
...f,
|
|
1213
1242
|
title: escapePipes(f.title),
|
|
1214
1243
|
path: escapeCodeBackticks(f.path),
|
|
1244
|
+
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code), codeKey: f.code } : {},
|
|
1245
|
+
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1215
1246
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1216
1247
|
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1217
1248
|
};
|
|
1218
1249
|
};
|
|
1250
|
+
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1219
1251
|
var sanitizeSuppressedNit = (f) => ({
|
|
1220
1252
|
title: escapeCodeBackticks(f.title),
|
|
1221
1253
|
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code) } : {},
|
|
1222
|
-
|
|
1254
|
+
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1255
|
+
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1223
1256
|
startLine: f.start_line,
|
|
1224
|
-
|
|
1257
|
+
endLine: f.end_line,
|
|
1258
|
+
...f.side !== void 0 ? { side: f.side } : {},
|
|
1259
|
+
severity: f.severity,
|
|
1260
|
+
confidence: formatConfidence(f.confidence),
|
|
1261
|
+
likelihood: formatConfidence(f.likelihood),
|
|
1262
|
+
m: formatConfidence(f.confidence * f.likelihood),
|
|
1263
|
+
carried: carriedLines(f)
|
|
1225
1264
|
});
|
|
1265
|
+
var carriedLines = (f) => [
|
|
1266
|
+
`description: ${clipText(f.description, BODY_CLIP_CHARS)}`,
|
|
1267
|
+
...f.recommendation !== void 0 ? [`recommendation: ${clipText(f.recommendation, BODY_CLIP_CHARS)}`] : [],
|
|
1268
|
+
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1269
|
+
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1270
|
+
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
1226
1271
|
var sanitizeSystemic = (s) => ({
|
|
1227
1272
|
...s,
|
|
1228
1273
|
title: escapePipes(s.title),
|
|
1274
|
+
...s.code !== void 0 ? { code: escapeCodeBackticks(s.code) } : {},
|
|
1275
|
+
...s.code_url !== void 0 ? { code_url: linkSafeUrl(s.code_url) } : {},
|
|
1229
1276
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1230
1277
|
...s.finding_codes !== void 0 ? { finding_codes: s.finding_codes.map(escapeCodeBackticks) } : {}
|
|
1231
1278
|
});
|
|
@@ -1258,6 +1305,14 @@ var render = (input) => {
|
|
|
1258
1305
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1259
1306
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
1260
1307
|
const advisoryAllowed = isFullReviewRound;
|
|
1308
|
+
const suppressedBudget = (input.suppressedNits ?? []).map(sanitizeSuppressedNit).reduce(
|
|
1309
|
+
(acc, n) => {
|
|
1310
|
+
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.
|
|
1311
|
+
(n.code !== void 0 ? n.code.length + 2 : 0) + (n.codeUrl !== void 0 ? n.codeUrl.length + 4 : 0);
|
|
1312
|
+
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 };
|
|
1313
|
+
},
|
|
1314
|
+
{ list: [], used: 0, dropped: 0 }
|
|
1315
|
+
);
|
|
1261
1316
|
return eta.renderString(input.template, {
|
|
1262
1317
|
findings: input.findings,
|
|
1263
1318
|
envelope: input.envelope,
|
|
@@ -1279,7 +1334,8 @@ var render = (input) => {
|
|
|
1279
1334
|
severityCounts,
|
|
1280
1335
|
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
1281
1336
|
strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
|
|
1282
|
-
suppressedNits:
|
|
1337
|
+
suppressedNits: suppressedBudget.list,
|
|
1338
|
+
carriedDroppedNits: suppressedBudget.dropped,
|
|
1283
1339
|
nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
|
|
1284
1340
|
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
1285
1341
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
@@ -1287,10 +1343,11 @@ var render = (input) => {
|
|
|
1287
1343
|
unverifiedNoLogs: input.unverifiedNoLogs === true,
|
|
1288
1344
|
runUrl: input.runUrl ?? null,
|
|
1289
1345
|
jsonUrl: input.jsonUrl ?? null,
|
|
1290
|
-
// The
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1346
|
+
// The marker names the findings artifact, so the convergence no longer rides inside the comment —
|
|
1347
|
+
// it needs its own compact marker beside the link or a trajectory would require a fetch to read
|
|
1348
|
+
// (issue #217). post precomputes both together in findingsBlob; the standalone `render` command
|
|
1349
|
+
// builds the same pair here, so neither path can emit a link with the convergence missing.
|
|
1350
|
+
findingsPointer: input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence),
|
|
1294
1351
|
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
1295
1352
|
metastasisNote: advisoryAllowed ? metastasisNote(trajectory) : "",
|
|
1296
1353
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
@@ -1406,6 +1463,15 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1406
1463
|
};
|
|
1407
1464
|
const comments = inDiff.map((f) => {
|
|
1408
1465
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1466
|
+
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
1467
|
+
const view = clipProse ? {
|
|
1468
|
+
...f,
|
|
1469
|
+
title: clipText(f.title, BODY_CLIP_CHARS),
|
|
1470
|
+
description: clipText(f.description, BODY_CLIP_CHARS),
|
|
1471
|
+
...f.recommendation != null ? { recommendation: clipText(f.recommendation, BODY_CLIP_CHARS) } : {},
|
|
1472
|
+
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
1473
|
+
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
1474
|
+
} : f;
|
|
1409
1475
|
const sameRootNote = noteFor(f, context.sameRootNotes);
|
|
1410
1476
|
const answeredNote2 = noteFor(f, context.answeredNotes);
|
|
1411
1477
|
const comment = {
|
|
@@ -1413,7 +1479,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1413
1479
|
line: f.end_line,
|
|
1414
1480
|
side: defaultSide(f.side),
|
|
1415
1481
|
body: renderCommentBody(
|
|
1416
|
-
|
|
1482
|
+
view,
|
|
1417
1483
|
eta,
|
|
1418
1484
|
inlineTemplate,
|
|
1419
1485
|
modelsText,
|
|
@@ -1990,7 +2056,7 @@ var classifyExecError = (err, stderr, timeoutMs) => {
|
|
|
1990
2056
|
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
1991
2057
|
return stderrStr || errMsg(err);
|
|
1992
2058
|
};
|
|
1993
|
-
var execFileWithTimeout = (spec) => new Promise((
|
|
2059
|
+
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
1994
2060
|
const child = execFile(
|
|
1995
2061
|
spec.command,
|
|
1996
2062
|
[...spec.args],
|
|
@@ -2006,7 +2072,7 @@ var execFileWithTimeout = (spec) => new Promise((resolve3, reject) => {
|
|
|
2006
2072
|
reject(
|
|
2007
2073
|
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
2008
2074
|
);
|
|
2009
|
-
else
|
|
2075
|
+
else resolve4(stdout);
|
|
2010
2076
|
}
|
|
2011
2077
|
);
|
|
2012
2078
|
if (spec.stdin !== void 0) {
|
|
@@ -2025,6 +2091,90 @@ var runGhApi = (args, stdin, env) => execFileWithTimeout({
|
|
|
2025
2091
|
env,
|
|
2026
2092
|
stdin
|
|
2027
2093
|
});
|
|
2094
|
+
var run = promisify(execFile);
|
|
2095
|
+
var STEP_TIMEOUT_MS = 6e4;
|
|
2096
|
+
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
2097
|
+
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
2098
|
+
var containedPath = (dir, member) => {
|
|
2099
|
+
const target = resolve$1(dir, member);
|
|
2100
|
+
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
2101
|
+
};
|
|
2102
|
+
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
2103
|
+
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
2104
|
+
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
2105
|
+
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
2106
|
+
var readArtifactFindings = (ghApiPath) => {
|
|
2107
|
+
return async (zipUrl) => {
|
|
2108
|
+
try {
|
|
2109
|
+
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
2110
|
+
const zip = join(dir, "findings.zip");
|
|
2111
|
+
try {
|
|
2112
|
+
await ghApiPath(zipUrl, zip);
|
|
2113
|
+
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
2114
|
+
encoding: "utf-8",
|
|
2115
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
2116
|
+
timeout: STEP_TIMEOUT_MS
|
|
2117
|
+
});
|
|
2118
|
+
const member = locateFindingsMember(listing);
|
|
2119
|
+
if (member === null) return null;
|
|
2120
|
+
await run("unzip", [zip, "-d", dir], {
|
|
2121
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
2122
|
+
timeout: STEP_TIMEOUT_MS
|
|
2123
|
+
});
|
|
2124
|
+
const target = containedPath(dir, member);
|
|
2125
|
+
if (target === null) {
|
|
2126
|
+
process.stderr.write(
|
|
2127
|
+
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
2128
|
+
);
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
return await readFile(target, "utf-8");
|
|
2132
|
+
} finally {
|
|
2133
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2134
|
+
}
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
process.stderr.write(
|
|
2137
|
+
annotationSafe(
|
|
2138
|
+
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2139
|
+
)
|
|
2140
|
+
);
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
};
|
|
2145
|
+
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
2146
|
+
const { stdout } = await run("gh", ["api", url], {
|
|
2147
|
+
encoding: "buffer",
|
|
2148
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
2149
|
+
timeout: STEP_TIMEOUT_MS
|
|
2150
|
+
});
|
|
2151
|
+
await writeFile(outPath, stdout);
|
|
2152
|
+
});
|
|
2153
|
+
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
2154
|
+
var withStampedConvergence = (doc, body) => {
|
|
2155
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
2156
|
+
const stripped = withoutKey(doc, "convergence");
|
|
2157
|
+
const stamped = parseConvergenceMarker(body);
|
|
2158
|
+
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
2159
|
+
};
|
|
2160
|
+
var resolvePriorFindings = async (body, read) => {
|
|
2161
|
+
const embedded = parseFindingsMarker(body);
|
|
2162
|
+
if (embedded !== null) return embedded;
|
|
2163
|
+
const url = findingsArtifactUrl(body);
|
|
2164
|
+
if (url === null) return null;
|
|
2165
|
+
const text = await read(url);
|
|
2166
|
+
if (text === null) return null;
|
|
2167
|
+
try {
|
|
2168
|
+
return withStampedConvergence(JSON.parse(text), body);
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
process.stderr.write(
|
|
2171
|
+
annotationSafe(
|
|
2172
|
+
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2173
|
+
)
|
|
2174
|
+
);
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2028
2178
|
|
|
2029
2179
|
// src/pr.ts
|
|
2030
2180
|
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
@@ -2521,7 +2671,7 @@ var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
|
2521
2671
|
);
|
|
2522
2672
|
}
|
|
2523
2673
|
};
|
|
2524
|
-
var post = async (input, ghApi = runGhApi) => {
|
|
2674
|
+
var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
2525
2675
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
2526
2676
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
2527
2677
|
if (resolution.kind === "none") {
|
|
@@ -2554,14 +2704,20 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
2554
2704
|
...doc,
|
|
2555
2705
|
convergence: conv ?? void 0
|
|
2556
2706
|
});
|
|
2707
|
+
const carriedFindingsLink = !input.jsonUrl && existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
2708
|
+
let warnedNoJsonUrl = false;
|
|
2557
2709
|
const findingsBlob = (doc) => {
|
|
2558
|
-
|
|
2559
|
-
if (
|
|
2560
|
-
return
|
|
2710
|
+
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
2711
|
+
if (carriedFindingsLink !== null) {
|
|
2712
|
+
return findingsMarkerPair(carriedFindingsLink, doc.convergence);
|
|
2561
2713
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2714
|
+
if (!warnedNoJsonUrl) {
|
|
2715
|
+
warnedNoJsonUrl = true;
|
|
2716
|
+
process.stderr.write(
|
|
2717
|
+
"::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"
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
return findingsMarkerPair(void 0, doc.convergence);
|
|
2565
2721
|
};
|
|
2566
2722
|
const leaveInPlace = (message) => {
|
|
2567
2723
|
process.stderr.write(
|
|
@@ -2672,8 +2828,12 @@ ${dropNote}` : ""}`,
|
|
|
2672
2828
|
findings: [...answeredFilter.findings],
|
|
2673
2829
|
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
2674
2830
|
};
|
|
2831
|
+
const roundHasNit = findings.findings.some((f) => f.severity === "nit");
|
|
2832
|
+
const priorDocForNits = roundHasNit && existingSticky !== null && isFullReviewSticky(existingSticky.body) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
2675
2833
|
const priorSuppressedKeys = new Set(
|
|
2676
|
-
|
|
2834
|
+
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2835
|
+
(n) => answeredNoteKey({ code: n.code, title: n.title })
|
|
2836
|
+
)
|
|
2677
2837
|
);
|
|
2678
2838
|
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || priorSuppressedKeys.has(answeredNoteKey(f)));
|
|
2679
2839
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
@@ -2785,16 +2945,6 @@ ${dropNote}` : ""}`,
|
|
|
2785
2945
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
2786
2946
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
2787
2947
|
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
2948
|
const commonRenderInput = {
|
|
2799
2949
|
findings: stampedFindings,
|
|
2800
2950
|
envelope,
|
|
@@ -2839,6 +2989,11 @@ ${dropNote}` : ""}`,
|
|
|
2839
2989
|
reviewUrl: reviewUrl2
|
|
2840
2990
|
}) + longFilesNote
|
|
2841
2991
|
);
|
|
2992
|
+
if (!input.jsonUrl && existingSticky !== null && parseReviewComplete(existingSticky.body) && parseReviewedRoute(existingSticky.body) === "full review" && hasFindingsMarker(existingSticky.body)) {
|
|
2993
|
+
leaveInPlace(
|
|
2994
|
+
"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"
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2842
2997
|
const stickyRef = await upsertSticky(
|
|
2843
2998
|
input.repo,
|
|
2844
2999
|
prNumber,
|
|
@@ -3247,10 +3402,10 @@ var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi,
|
|
|
3247
3402
|
}
|
|
3248
3403
|
};
|
|
3249
3404
|
const poll = async (lastSeenNames, lastRunId) => {
|
|
3250
|
-
const { run, seenNames } = await safeResolve();
|
|
3251
|
-
if (
|
|
3252
|
-
return { kind: "concluded", conclusion:
|
|
3253
|
-
const runId =
|
|
3405
|
+
const { run: run2, seenNames } = await safeResolve();
|
|
3406
|
+
if (run2 !== null && run2.status === "completed" && run2.conclusion !== null)
|
|
3407
|
+
return { kind: "concluded", conclusion: run2.conclusion, runId: run2.id };
|
|
3408
|
+
const runId = run2 === null ? lastRunId : run2.id;
|
|
3254
3409
|
const names = seenNames.length > 0 ? seenNames : lastSeenNames;
|
|
3255
3410
|
if (deps.elapsedMs() >= options.timeoutMs)
|
|
3256
3411
|
return { kind: "timed-out", runId, seenNames: names };
|
|
@@ -3421,8 +3576,7 @@ var priorReviewFrom = (comments, botLogin) => {
|
|
|
3421
3576
|
return last ? { id: last.id, body: last.body } : null;
|
|
3422
3577
|
};
|
|
3423
3578
|
var MAX_CONVERSATION_COMMENTS = 50;
|
|
3424
|
-
var
|
|
3425
|
-
var clip = (body) => clipText(body, MAX_CONVERSATION_BODY_CHARS);
|
|
3579
|
+
var clip = (body) => clipText(body, BODY_CLIP_CHARS);
|
|
3426
3580
|
var boundedHuman = (items, botLogin, label, project) => {
|
|
3427
3581
|
const human = items.filter(
|
|
3428
3582
|
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
@@ -3495,7 +3649,7 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
|
3495
3649
|
}
|
|
3496
3650
|
return { staged, failing: failing.length };
|
|
3497
3651
|
};
|
|
3498
|
-
var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
3652
|
+
var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghArtifactReader) => {
|
|
3499
3653
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
3500
3654
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
3501
3655
|
if (resolution.kind === "none") {
|
|
@@ -3550,6 +3704,13 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
3550
3704
|
join(input.outDir, "prior_review.json"),
|
|
3551
3705
|
prior === null ? "null" : JSON.stringify(prior)
|
|
3552
3706
|
);
|
|
3707
|
+
const seedsFromPrior = input.conclusion === "success" && prior !== null && prior.body !== null && parseReviewedRoute(prior.body) === "full review";
|
|
3708
|
+
const resolvedPrior = seedsFromPrior ? await resolvePriorFindings(prior.body, readArtifact) : null;
|
|
3709
|
+
const priorFindings = resolvedPrior !== null && typeof resolvedPrior === "object" ? resolvedPrior : null;
|
|
3710
|
+
writeFileSync(
|
|
3711
|
+
join(input.outDir, "prior_findings.json"),
|
|
3712
|
+
priorFindings === null ? "null" : JSON.stringify(priorFindings)
|
|
3713
|
+
);
|
|
3553
3714
|
const answered = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
|
|
3554
3715
|
writeFileSync(
|
|
3555
3716
|
join(input.outDir, "answered.json"),
|
|
@@ -4132,6 +4293,9 @@ var renderCmd = defineCommand({
|
|
|
4132
4293
|
pricedAt: /* @__PURE__ */ new Date()
|
|
4133
4294
|
});
|
|
4134
4295
|
process.stdout.write(output2);
|
|
4296
|
+
process.stderr.write(
|
|
4297
|
+
"::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"
|
|
4298
|
+
);
|
|
4135
4299
|
}
|
|
4136
4300
|
});
|
|
4137
4301
|
var inlineCmd = defineCommand({
|
|
@@ -4599,6 +4763,10 @@ var seedDraftCmd = defineCommand({
|
|
|
4599
4763
|
type: "string",
|
|
4600
4764
|
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
4765
|
},
|
|
4766
|
+
"prior-findings": {
|
|
4767
|
+
type: "string",
|
|
4768
|
+
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"
|
|
4769
|
+
},
|
|
4602
4770
|
"prior-answers": {
|
|
4603
4771
|
type: "string",
|
|
4604
4772
|
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 +4834,20 @@ var seedDraftCmd = defineCommand({
|
|
|
4666
4834
|
})();
|
|
4667
4835
|
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
4668
4836
|
})();
|
|
4669
|
-
const
|
|
4837
|
+
const stagedPrior = (() => {
|
|
4838
|
+
if (!args["prior-findings"]) return null;
|
|
4839
|
+
try {
|
|
4840
|
+
return JSON.parse(readFileSync(resolve$1(args["prior-findings"]), "utf-8"));
|
|
4841
|
+
} catch {
|
|
4842
|
+
return null;
|
|
4843
|
+
}
|
|
4844
|
+
})();
|
|
4845
|
+
const parsedPrior = stagedPrior !== null ? stagedPrior : priorBody === null ? null : parseFindingsMarker(priorBody);
|
|
4670
4846
|
const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
|
|
4671
4847
|
isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
|
|
4672
4848
|
);
|
|
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"]) {
|
|
4849
|
+
const answeredRegistry = (() => {
|
|
4850
|
+
if (!args["prior-answers"]) return null;
|
|
4683
4851
|
try {
|
|
4684
4852
|
const raw = JSON.parse(readFileSync(resolve$1(args["prior-answers"]), "utf-8"));
|
|
4685
4853
|
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
@@ -4693,18 +4861,38 @@ var seedDraftCmd = defineCommand({
|
|
|
4693
4861
|
`
|
|
4694
4862
|
);
|
|
4695
4863
|
}
|
|
4696
|
-
|
|
4697
|
-
`);
|
|
4698
|
-
process.stderr.write(
|
|
4699
|
-
`Seeded ${priorAnswersPath(outPath)} with ${String(decoded.length)} answered finding(s) as context
|
|
4700
|
-
`
|
|
4701
|
-
);
|
|
4864
|
+
return decoded;
|
|
4702
4865
|
} catch (err) {
|
|
4703
4866
|
process.stderr.write(
|
|
4704
4867
|
`Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
|
|
4705
4868
|
`
|
|
4706
4869
|
);
|
|
4870
|
+
return null;
|
|
4707
4871
|
}
|
|
4872
|
+
})();
|
|
4873
|
+
const priorFindings = (() => {
|
|
4874
|
+
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4875
|
+
return strippedPrior;
|
|
4876
|
+
const raw = strippedPrior;
|
|
4877
|
+
const doc = answeredRegistry !== null && answeredRegistry.length > 0 && Array.isArray(raw["findings"]) ? {
|
|
4878
|
+
...raw,
|
|
4879
|
+
findings: raw["findings"].filter(
|
|
4880
|
+
(f) => !answeredRegistry.some((e) => isAnsweredDrop(f, e))
|
|
4881
|
+
)
|
|
4882
|
+
} : raw;
|
|
4883
|
+
const carried = doc["scope_metastasis"];
|
|
4884
|
+
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
4885
|
+
if (doc["verdict"] === "error") return doc;
|
|
4886
|
+
const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
|
|
4887
|
+
return computed === null ? doc : { ...doc, scope_metastasis: computed };
|
|
4888
|
+
})();
|
|
4889
|
+
if (answeredRegistry !== null) {
|
|
4890
|
+
writeFileSync(priorAnswersPath(outPath), `${JSON.stringify(answeredRegistry, null, 2)}
|
|
4891
|
+
`);
|
|
4892
|
+
process.stderr.write(
|
|
4893
|
+
`Seeded ${priorAnswersPath(outPath)} with ${String(answeredRegistry.length)} answered finding(s) as context
|
|
4894
|
+
`
|
|
4895
|
+
);
|
|
4708
4896
|
}
|
|
4709
4897
|
if (parsedPrior !== null && parseReviewedRoute(priorBody ?? "") === "full review") {
|
|
4710
4898
|
try {
|