@jphutchins/code-review 0.1.0-alpha.62 → 0.1.0-alpha.64
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 +575 -335
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/comment.eta +13 -1
package/dist/index.js
CHANGED
|
@@ -2,23 +2,32 @@
|
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync, writeFileSync, unlinkSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
4
|
import { randomBytes, createHash } from 'crypto';
|
|
5
|
-
import { resolve
|
|
5
|
+
import { resolve, join, dirname, basename, extname, sep } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
7
7
|
import * as t from 'io-ts';
|
|
8
|
-
import parseDiff from 'parse-diff';
|
|
9
|
-
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
10
|
-
import _addFormats from 'ajv-formats';
|
|
11
8
|
import { execFile } from 'child_process';
|
|
12
9
|
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
13
10
|
import { tmpdir } from 'os';
|
|
14
11
|
import { promisify } from 'util';
|
|
12
|
+
import parseDiff from 'parse-diff';
|
|
13
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
14
|
+
import _addFormats from 'ajv-formats';
|
|
15
15
|
import { performance } from 'perf_hooks';
|
|
16
16
|
import { PathReporter } from 'io-ts/lib/PathReporter.js';
|
|
17
17
|
|
|
18
18
|
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
19
19
|
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
20
20
|
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
21
|
-
var
|
|
21
|
+
var CONTEXT_SUFFIX_RE = /\[[12]m\]/i;
|
|
22
|
+
var MODEL_IDENTITY_SUFFIX_RE = new RegExp(CONTEXT_SUFFIX_RE.source, "gi");
|
|
23
|
+
var modelIdentity = (configuredModelId) => configuredModelId.replace(MODEL_IDENTITY_SUFFIX_RE, "");
|
|
24
|
+
var addModelUsage = (prior, entry) => ({
|
|
25
|
+
...prior,
|
|
26
|
+
input_tokens: prior.input_tokens + entry.input_tokens,
|
|
27
|
+
output_tokens: prior.output_tokens + entry.output_tokens,
|
|
28
|
+
cache_read_tokens: (prior.cache_read_tokens ?? 0) + (entry.cache_read_tokens ?? 0),
|
|
29
|
+
cache_write_tokens: (prior.cache_write_tokens ?? 0) + (entry.cache_write_tokens ?? 0)
|
|
30
|
+
});
|
|
22
31
|
var clipText = (body, max) => {
|
|
23
32
|
if (body.length <= max) return body;
|
|
24
33
|
const cut = body.slice(0, max);
|
|
@@ -485,7 +494,7 @@ var incompleteFindings = (summary) => ({
|
|
|
485
494
|
var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
|
|
486
495
|
|
|
487
496
|
// src/released.ts
|
|
488
|
-
var RELEASED = "2026-
|
|
497
|
+
var RELEASED = "2026-10-02";
|
|
489
498
|
|
|
490
499
|
// src/cost.ts
|
|
491
500
|
var defaultWarn = (message) => {
|
|
@@ -533,8 +542,7 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
533
542
|
);
|
|
534
543
|
return null;
|
|
535
544
|
};
|
|
536
|
-
var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
537
|
-
const p = Object.hasOwn(prices.models, entry.model) ? prices.models[entry.model] : void 0;
|
|
545
|
+
var computeModelCost = (entry, prices, pricedAt, warn, colliding = /* @__PURE__ */ new Set()) => {
|
|
538
546
|
const cacheRead = entry.cache_read_tokens ?? 0;
|
|
539
547
|
const cacheWrite = entry.cache_write_tokens ?? 0;
|
|
540
548
|
const zero = {
|
|
@@ -546,6 +554,14 @@ var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
|
546
554
|
costUSD: 0,
|
|
547
555
|
known: true
|
|
548
556
|
};
|
|
557
|
+
const canonicalModel = modelIdentity(entry.model);
|
|
558
|
+
if (colliding.has(canonicalModel)) {
|
|
559
|
+
warn(
|
|
560
|
+
`code-review cost: model "${annotationSafe(entry.model)}" is unpriced \u2014 its price map key collides with another key after canonicalization`
|
|
561
|
+
);
|
|
562
|
+
return { ...zero, known: false };
|
|
563
|
+
}
|
|
564
|
+
const p = Object.hasOwn(prices.models, canonicalModel) ? prices.models[canonicalModel] : void 0;
|
|
549
565
|
if (!p) {
|
|
550
566
|
warn(
|
|
551
567
|
// annotationSafe: a model id is an unvalidated t.string — a CR/LF in it must not emit a
|
|
@@ -560,7 +576,26 @@ var computeModelCost = (entry, prices, pricedAt, warn) => {
|
|
|
560
576
|
return { ...zero, costUSD };
|
|
561
577
|
};
|
|
562
578
|
var computeCost = (models, prices, pricedAt, warn = defaultWarn) => {
|
|
563
|
-
const
|
|
579
|
+
const colliding = /* @__PURE__ */ new Set();
|
|
580
|
+
const seenCanonical = /* @__PURE__ */ new Map();
|
|
581
|
+
const canonicalModels = Object.fromEntries(
|
|
582
|
+
Object.entries(prices.models).map(([key2, rate]) => {
|
|
583
|
+
const canonical = modelIdentity(key2);
|
|
584
|
+
const prior = seenCanonical.get(canonical);
|
|
585
|
+
if (prior !== void 0) {
|
|
586
|
+
colliding.add(canonical);
|
|
587
|
+
warn(
|
|
588
|
+
`code-review cost: the price map keys "${annotationSafe(prior)}" and "${annotationSafe(key2)}" both canonicalize to "${annotationSafe(canonical)}" \u2014 the model is unpriced; fix the map`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
seenCanonical.set(canonical, key2);
|
|
592
|
+
return [canonical, rate];
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
const canonicalPrices = { ...prices, models: canonicalModels };
|
|
596
|
+
const lines = models.map(
|
|
597
|
+
(entry) => computeModelCost(entry, canonicalPrices, pricedAt, warn, colliding)
|
|
598
|
+
);
|
|
564
599
|
return {
|
|
565
600
|
lines,
|
|
566
601
|
totalInputTokens: lines.reduce((s, l) => s + l.inputTokens, 0),
|
|
@@ -1159,6 +1194,167 @@ var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
|
1159
1194
|
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.` : "";
|
|
1160
1195
|
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run2}`;
|
|
1161
1196
|
};
|
|
1197
|
+
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
1198
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
1199
|
+
var parseTimeoutMs = (raw, fallback) => {
|
|
1200
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return fallback;
|
|
1201
|
+
const parsed = Number(raw);
|
|
1202
|
+
return parsed > 0 && parsed <= MAX_TIMEOUT_MS ? parsed : fallback;
|
|
1203
|
+
};
|
|
1204
|
+
var SUBPROCESS_TIMEOUT_ENV = "CODE_REVIEW_SUBPROCESS_TIMEOUT_MS";
|
|
1205
|
+
var DEFAULT_SUBPROCESS_TIMEOUT_MS = 12e4;
|
|
1206
|
+
var subprocessTimeoutMs = () => parseTimeoutMs(process.env[SUBPROCESS_TIMEOUT_ENV], DEFAULT_SUBPROCESS_TIMEOUT_MS);
|
|
1207
|
+
var classifyExecError = (err, stderr, timeoutMs) => {
|
|
1208
|
+
const e = err;
|
|
1209
|
+
const stderrStr = stderr.trim();
|
|
1210
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
1211
|
+
return `output exceeded ${String(MAX_BUFFER)} bytes (killed)`;
|
|
1212
|
+
if (e.killed === true)
|
|
1213
|
+
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
1214
|
+
return stderrStr || errMsg(err);
|
|
1215
|
+
};
|
|
1216
|
+
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
1217
|
+
const child = execFile(
|
|
1218
|
+
spec.command,
|
|
1219
|
+
[...spec.args],
|
|
1220
|
+
{
|
|
1221
|
+
...spec.env ? { env: { ...process.env, ...spec.env } } : {},
|
|
1222
|
+
encoding: "utf-8",
|
|
1223
|
+
maxBuffer: MAX_BUFFER,
|
|
1224
|
+
timeout: spec.timeoutMs,
|
|
1225
|
+
killSignal: "SIGKILL"
|
|
1226
|
+
},
|
|
1227
|
+
(err, stdout, stderr) => {
|
|
1228
|
+
if (err)
|
|
1229
|
+
reject(
|
|
1230
|
+
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
1231
|
+
);
|
|
1232
|
+
else resolve4(stdout);
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
if (spec.stdin !== void 0) {
|
|
1236
|
+
child.stdin?.on("error", () => void 0);
|
|
1237
|
+
child.stdin?.end(spec.stdin);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
// src/gh.ts
|
|
1242
|
+
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
1243
|
+
var flagFirst;
|
|
1244
|
+
var withEscapeRetry = async (run2, idempotent) => {
|
|
1245
|
+
if (flagFirst) return run2(true);
|
|
1246
|
+
try {
|
|
1247
|
+
return await run2(false);
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
1250
|
+
if (!idempotent) throw err;
|
|
1251
|
+
flagFirst = true;
|
|
1252
|
+
return run2(true);
|
|
1253
|
+
}
|
|
1254
|
+
throw err;
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
1258
|
+
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
1259
|
+
(withFlag) => execFileWithTimeout({
|
|
1260
|
+
command: "gh",
|
|
1261
|
+
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
1262
|
+
label: `gh api ${describeEndpoint(args)}`,
|
|
1263
|
+
timeoutMs: subprocessTimeoutMs(),
|
|
1264
|
+
env,
|
|
1265
|
+
stdin
|
|
1266
|
+
}),
|
|
1267
|
+
isIdempotentCall(args)
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// src/artifact.ts
|
|
1271
|
+
var run = promisify(execFile);
|
|
1272
|
+
var STEP_TIMEOUT_MS = 6e4;
|
|
1273
|
+
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
1274
|
+
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
1275
|
+
var containedPath = (dir, member) => {
|
|
1276
|
+
const target = resolve(dir, member);
|
|
1277
|
+
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
1278
|
+
};
|
|
1279
|
+
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
1280
|
+
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
1281
|
+
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
1282
|
+
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
1283
|
+
var readArtifactFindings = (ghApiPath) => {
|
|
1284
|
+
return async (zipUrl) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
1287
|
+
const zip = join(dir, "findings.zip");
|
|
1288
|
+
try {
|
|
1289
|
+
await ghApiPath(zipUrl, zip);
|
|
1290
|
+
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
1291
|
+
encoding: "utf-8",
|
|
1292
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1293
|
+
timeout: STEP_TIMEOUT_MS
|
|
1294
|
+
});
|
|
1295
|
+
const member = locateFindingsMember(listing);
|
|
1296
|
+
if (member === null) return null;
|
|
1297
|
+
await run("unzip", [zip, "-d", dir], {
|
|
1298
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1299
|
+
timeout: STEP_TIMEOUT_MS
|
|
1300
|
+
});
|
|
1301
|
+
const target = containedPath(dir, member);
|
|
1302
|
+
if (target === null) {
|
|
1303
|
+
process.stderr.write(
|
|
1304
|
+
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
1305
|
+
);
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
return await readFile(target, "utf-8");
|
|
1309
|
+
} finally {
|
|
1310
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
1311
|
+
}
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
process.stderr.write(
|
|
1314
|
+
annotationSafe(
|
|
1315
|
+
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
1316
|
+
)
|
|
1317
|
+
);
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
};
|
|
1322
|
+
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
1323
|
+
const { stdout } = await withEscapeRetry(
|
|
1324
|
+
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
1325
|
+
encoding: "buffer",
|
|
1326
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
1327
|
+
timeout: STEP_TIMEOUT_MS
|
|
1328
|
+
}),
|
|
1329
|
+
true
|
|
1330
|
+
);
|
|
1331
|
+
await writeFile(outPath, stdout);
|
|
1332
|
+
});
|
|
1333
|
+
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
1334
|
+
var withStampedConvergence = (doc, body) => {
|
|
1335
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
1336
|
+
const stripped = withoutKey(doc, "convergence");
|
|
1337
|
+
const stamped = parseConvergenceMarker(body);
|
|
1338
|
+
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
1339
|
+
};
|
|
1340
|
+
var resolvePriorFindings = async (body, read) => {
|
|
1341
|
+
const embedded = parseFindingsMarker(body);
|
|
1342
|
+
if (embedded !== null) return embedded;
|
|
1343
|
+
const url = findingsArtifactUrl(body);
|
|
1344
|
+
if (url === null) return null;
|
|
1345
|
+
const text = await read(url);
|
|
1346
|
+
if (text === null) return null;
|
|
1347
|
+
try {
|
|
1348
|
+
return withStampedConvergence(JSON.parse(text), body);
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
process.stderr.write(
|
|
1351
|
+
annotationSafe(
|
|
1352
|
+
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1162
1358
|
|
|
1163
1359
|
// src/comment-chain.ts
|
|
1164
1360
|
function* ancestors(start, parentOf) {
|
|
@@ -1512,6 +1708,24 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1512
1708
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1513
1709
|
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1514
1710
|
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
1711
|
+
var shedNote = (count, summaryAvailable, runUrl, findingsDocLinked, thisRunArtifact) => count === 0 ? "" : `<sub>**${String(count)} finding${count === 1 ? "" : "s"} left out of this comment**${summaryAvailable && runUrl !== void 0 ? ` \u2014 the [run summary](<${runUrl}>) carries the whole review` : findingsDocLinked && thisRunArtifact ? " \u2014 the machine findings document carries them" : ""}.</sub>`;
|
|
1712
|
+
var terminalNotice = (i) => [
|
|
1713
|
+
`## ${i.verdictBadge}`,
|
|
1714
|
+
"",
|
|
1715
|
+
`<sub>**Reviewed** \`${escapeCodeBackticks(i.reviewedSha.slice(0, 7))}\`${i.postedAt ? ` at ${i.postedAt}` : ""}${i.route ? ` \xB7 **route:** ${i.route}` : ""}</sub>`,
|
|
1716
|
+
...i.roundsSummary !== "" ? [`<sub>${i.roundsSummary}</sub>`] : [],
|
|
1717
|
+
...i.convergenceSummary !== "" ? [`<sub>${i.convergenceSummary}</sub>`] : [],
|
|
1718
|
+
...i.inlinePosted > 0 ? [
|
|
1719
|
+
`<sub>\u2192 ${String(i.inlinePosted)} inline comment${i.inlinePosted === 1 ? "" : "s"} posted on the diff \u2014 the disposition line the terminal round otherwise skips.</sub>`
|
|
1720
|
+
] : [],
|
|
1721
|
+
"",
|
|
1722
|
+
`> **The review for \`${escapeCodeBackticks(i.reviewedSha.slice(0, 7))}\` exceeds GitHub's comment-size limit.**` + (i.summaryAvailable ? i.runUrl ? ` The whole review lives in the [run summary](<${i.runUrl}>).` : " The whole review lives in the workflow run's summary." : i.findingsDocLinked && i.thisRunArtifact ? " The whole review lives in the machine findings document." : "") + (i.findingsDocLinked && i.thisRunArtifact ? " The machine findings document is linked above." : ""),
|
|
1723
|
+
"",
|
|
1724
|
+
"> [!WARNING]",
|
|
1725
|
+
`> **LLM Disclosure** \u2014 this review was produced by ${i.modelNames || "unknown model"}.`,
|
|
1726
|
+
">",
|
|
1727
|
+
`> _Generated by [code-review](https://github.com/JPHutchins/code-review)${i.runUrl ? ` \xB7 [view the run & traces](<${i.runUrl}>)` : ""}._`
|
|
1728
|
+
].join("\n");
|
|
1515
1729
|
var permalinkFor = (base, f, anchor) => {
|
|
1516
1730
|
if (f.path === "" || f.side === "LEFT") return void 0;
|
|
1517
1731
|
const path = f.path.split("/").map(
|
|
@@ -1636,6 +1850,20 @@ var computeSeverityCounts = (findings) => findings.reduce(
|
|
|
1636
1850
|
);
|
|
1637
1851
|
var isReviewVerdict = (verdict) => verdict !== "error";
|
|
1638
1852
|
var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
|
|
1853
|
+
var verdictBadge = (v) => {
|
|
1854
|
+
switch (v) {
|
|
1855
|
+
case "approve":
|
|
1856
|
+
return "\u2705 approved";
|
|
1857
|
+
case "comment":
|
|
1858
|
+
return "\u{1F4AC} comment";
|
|
1859
|
+
case "changes":
|
|
1860
|
+
return "\u{1F527} changes requested";
|
|
1861
|
+
case "error":
|
|
1862
|
+
return "\u{1F6E0}\uFE0F no review verdict";
|
|
1863
|
+
default:
|
|
1864
|
+
return `\u2753 ${v}`;
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1639
1867
|
var render = (input) => {
|
|
1640
1868
|
const eta = new Eta({ autoTrim: false });
|
|
1641
1869
|
const usageAvailable = input.envelope !== null;
|
|
@@ -1748,6 +1976,8 @@ var render = (input) => {
|
|
|
1748
1976
|
};
|
|
1749
1977
|
return eta.renderString(input.template, {
|
|
1750
1978
|
findings: input.findings,
|
|
1979
|
+
shedCount: input.shedCount ?? 0,
|
|
1980
|
+
terminal: input.terminal ?? false,
|
|
1751
1981
|
envelope: input.envelope,
|
|
1752
1982
|
usageAvailable,
|
|
1753
1983
|
hasUsage,
|
|
@@ -1755,6 +1985,38 @@ var render = (input) => {
|
|
|
1755
1985
|
costReport,
|
|
1756
1986
|
pricesProvided,
|
|
1757
1987
|
costProvenance,
|
|
1988
|
+
// The terminal branch's "machine findings document is linked above" claim must only fire when
|
|
1989
|
+
// the pointer actually carries a findings-json marker (the convergence-only marker does not).
|
|
1990
|
+
// Derived from the SAME value the template renders (the jsonUrl fallback pair included).
|
|
1991
|
+
findingsDocLinked: hasFindingsMarker(
|
|
1992
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
1993
|
+
),
|
|
1994
|
+
summaryAvailable: input.summaryAvailable ?? false,
|
|
1995
|
+
shedNote: shedNote(
|
|
1996
|
+
input.shedCount ?? 0,
|
|
1997
|
+
input.summaryAvailable ?? false,
|
|
1998
|
+
input.runUrl,
|
|
1999
|
+
hasFindingsMarker(
|
|
2000
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
2001
|
+
),
|
|
2002
|
+
input.jsonUrl !== void 0
|
|
2003
|
+
),
|
|
2004
|
+
terminalNotice: terminalNotice({
|
|
2005
|
+
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
2006
|
+
verdictBadge: verdictBadge(input.findings.verdict),
|
|
2007
|
+
route: route ?? void 0,
|
|
2008
|
+
postedAt: input.postedAt ?? "",
|
|
2009
|
+
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
2010
|
+
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : "",
|
|
2011
|
+
inlinePosted: input.inlineDisposition?.kind === "posted" ? input.inlineDisposition.count : 0,
|
|
2012
|
+
runUrl: input.runUrl,
|
|
2013
|
+
summaryAvailable: input.summaryAvailable ?? false,
|
|
2014
|
+
findingsDocLinked: hasFindingsMarker(
|
|
2015
|
+
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
2016
|
+
),
|
|
2017
|
+
thisRunArtifact: input.jsonUrl !== void 0,
|
|
2018
|
+
modelNames
|
|
2019
|
+
}),
|
|
1758
2020
|
// The house sanitizer for untrusted template fields — collapses line breaks AND escapes
|
|
1759
2021
|
// backticks, so a hand-edited stamp cannot break the code span or the blockquote.
|
|
1760
2022
|
// A nonconforming stamp must not be presented as validated: the template shows a note
|
|
@@ -1819,20 +2081,7 @@ var render = (input) => {
|
|
|
1819
2081
|
const s = Math.round(ms / 1e3);
|
|
1820
2082
|
return s >= 60 ? `${String(Math.floor(s / 60))}m ${String(s % 60)}s` : `${String(s)}s`;
|
|
1821
2083
|
},
|
|
1822
|
-
verdictBadge
|
|
1823
|
-
switch (v) {
|
|
1824
|
-
case "approve":
|
|
1825
|
-
return "\u2705 approved";
|
|
1826
|
-
case "comment":
|
|
1827
|
-
return "\u{1F4AC} comment";
|
|
1828
|
-
case "changes":
|
|
1829
|
-
return "\u{1F527} changes requested";
|
|
1830
|
-
case "error":
|
|
1831
|
-
return "\u{1F6E0}\uFE0F no review verdict";
|
|
1832
|
-
default:
|
|
1833
|
-
return `\u2753 ${v}`;
|
|
1834
|
-
}
|
|
1835
|
-
},
|
|
2084
|
+
verdictBadge,
|
|
1836
2085
|
severityEmoji,
|
|
1837
2086
|
formatConfidence
|
|
1838
2087
|
});
|
|
@@ -2470,7 +2719,7 @@ var defaultVersion = (kind) => {
|
|
|
2470
2719
|
if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
|
|
2471
2720
|
return latest.defaultVersion;
|
|
2472
2721
|
};
|
|
2473
|
-
var bundledSchemaPath = (relativePath) => resolve
|
|
2722
|
+
var bundledSchemaPath = (relativePath) => resolve(import.meta.dirname, "..", "schema", relativePath);
|
|
2474
2723
|
var schemaPathFor = (kind, version) => {
|
|
2475
2724
|
const table = tableFor(kind);
|
|
2476
2725
|
const entry = version === void 0 ? table.find((v) => v.latest) : table.find((v) => v.minor === majorMinor(version));
|
|
@@ -2503,7 +2752,7 @@ var resolvers = {
|
|
|
2503
2752
|
triage: (raw) => resolveSingleVersion("triage", raw),
|
|
2504
2753
|
prices: (raw) => resolveSingleVersion("prices", raw)
|
|
2505
2754
|
};
|
|
2506
|
-
var
|
|
2755
|
+
var resolve2 = (kind, raw) => resolvers[kind](raw);
|
|
2507
2756
|
var resolveTolerantFindings = (doc) => {
|
|
2508
2757
|
const r = resolveFindings(doc);
|
|
2509
2758
|
if (r.kind === "ok") return r.value;
|
|
@@ -2511,165 +2760,6 @@ var resolveTolerantFindings = (doc) => {
|
|
|
2511
2760
|
const legacy = FindingsCodecV09.decode(doc);
|
|
2512
2761
|
return legacy._tag === "Right" ? normalizeV09(legacy.right) : null;
|
|
2513
2762
|
};
|
|
2514
|
-
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
2515
|
-
var MAX_TIMEOUT_MS = 2147483647;
|
|
2516
|
-
var parseTimeoutMs = (raw, fallback) => {
|
|
2517
|
-
if (raw === void 0 || !/^\d+$/.test(raw)) return fallback;
|
|
2518
|
-
const parsed = Number(raw);
|
|
2519
|
-
return parsed > 0 && parsed <= MAX_TIMEOUT_MS ? parsed : fallback;
|
|
2520
|
-
};
|
|
2521
|
-
var SUBPROCESS_TIMEOUT_ENV = "CODE_REVIEW_SUBPROCESS_TIMEOUT_MS";
|
|
2522
|
-
var DEFAULT_SUBPROCESS_TIMEOUT_MS = 12e4;
|
|
2523
|
-
var subprocessTimeoutMs = () => parseTimeoutMs(process.env[SUBPROCESS_TIMEOUT_ENV], DEFAULT_SUBPROCESS_TIMEOUT_MS);
|
|
2524
|
-
var classifyExecError = (err, stderr, timeoutMs) => {
|
|
2525
|
-
const e = err;
|
|
2526
|
-
const stderrStr = stderr.trim();
|
|
2527
|
-
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
2528
|
-
return `output exceeded ${String(MAX_BUFFER)} bytes (killed)`;
|
|
2529
|
-
if (e.killed === true)
|
|
2530
|
-
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
2531
|
-
return stderrStr || errMsg(err);
|
|
2532
|
-
};
|
|
2533
|
-
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
2534
|
-
const child = execFile(
|
|
2535
|
-
spec.command,
|
|
2536
|
-
[...spec.args],
|
|
2537
|
-
{
|
|
2538
|
-
...spec.env ? { env: { ...process.env, ...spec.env } } : {},
|
|
2539
|
-
encoding: "utf-8",
|
|
2540
|
-
maxBuffer: MAX_BUFFER,
|
|
2541
|
-
timeout: spec.timeoutMs,
|
|
2542
|
-
killSignal: "SIGKILL"
|
|
2543
|
-
},
|
|
2544
|
-
(err, stdout, stderr) => {
|
|
2545
|
-
if (err)
|
|
2546
|
-
reject(
|
|
2547
|
-
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
2548
|
-
);
|
|
2549
|
-
else resolve4(stdout);
|
|
2550
|
-
}
|
|
2551
|
-
);
|
|
2552
|
-
if (spec.stdin !== void 0) {
|
|
2553
|
-
child.stdin?.on("error", () => void 0);
|
|
2554
|
-
child.stdin?.end(spec.stdin);
|
|
2555
|
-
}
|
|
2556
|
-
});
|
|
2557
|
-
|
|
2558
|
-
// src/gh.ts
|
|
2559
|
-
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
2560
|
-
var flagFirst;
|
|
2561
|
-
var withEscapeRetry = async (run2, idempotent) => {
|
|
2562
|
-
if (flagFirst) return run2(true);
|
|
2563
|
-
try {
|
|
2564
|
-
return await run2(false);
|
|
2565
|
-
} catch (err) {
|
|
2566
|
-
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
2567
|
-
if (!idempotent) throw err;
|
|
2568
|
-
flagFirst = true;
|
|
2569
|
-
return run2(true);
|
|
2570
|
-
}
|
|
2571
|
-
throw err;
|
|
2572
|
-
}
|
|
2573
|
-
};
|
|
2574
|
-
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
2575
|
-
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
2576
|
-
(withFlag) => execFileWithTimeout({
|
|
2577
|
-
command: "gh",
|
|
2578
|
-
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
2579
|
-
label: `gh api ${describeEndpoint(args)}`,
|
|
2580
|
-
timeoutMs: subprocessTimeoutMs(),
|
|
2581
|
-
env,
|
|
2582
|
-
stdin
|
|
2583
|
-
}),
|
|
2584
|
-
isIdempotentCall(args)
|
|
2585
|
-
);
|
|
2586
|
-
var run = promisify(execFile);
|
|
2587
|
-
var STEP_TIMEOUT_MS = 6e4;
|
|
2588
|
-
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
2589
|
-
var findingsArtifactUrl = (body) => MARKER_URL.exec(body)?.[1] ?? null;
|
|
2590
|
-
var containedPath = (dir, member) => {
|
|
2591
|
-
const target = resolve$1(dir, member);
|
|
2592
|
-
return target.startsWith(`${dir}${sep}`) || target === dir ? target : null;
|
|
2593
|
-
};
|
|
2594
|
-
var hasFindingsMarker = (body) => parseFindingsMarker(body) !== null || findingsArtifactUrl(body) !== null;
|
|
2595
|
-
var locateFindingsMember = (listing) => listing.split("\n").map((l) => l.trim()).filter(
|
|
2596
|
-
(l) => l.toLowerCase() === "findings.json" || l.toLowerCase().endsWith("/findings.json")
|
|
2597
|
-
).sort((a, b) => a.length - b.length)[0] ?? null;
|
|
2598
|
-
var readArtifactFindings = (ghApiPath) => {
|
|
2599
|
-
return async (zipUrl) => {
|
|
2600
|
-
try {
|
|
2601
|
-
const dir = await mkdtemp(join(tmpdir(), "code-review-artifact-"));
|
|
2602
|
-
const zip = join(dir, "findings.zip");
|
|
2603
|
-
try {
|
|
2604
|
-
await ghApiPath(zipUrl, zip);
|
|
2605
|
-
const { stdout: listing } = await run("unzip", ["-Z1", zip], {
|
|
2606
|
-
encoding: "utf-8",
|
|
2607
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
2608
|
-
timeout: STEP_TIMEOUT_MS
|
|
2609
|
-
});
|
|
2610
|
-
const member = locateFindingsMember(listing);
|
|
2611
|
-
if (member === null) return null;
|
|
2612
|
-
await run("unzip", [zip, "-d", dir], {
|
|
2613
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
2614
|
-
timeout: STEP_TIMEOUT_MS
|
|
2615
|
-
});
|
|
2616
|
-
const target = containedPath(dir, member);
|
|
2617
|
-
if (target === null) {
|
|
2618
|
-
process.stderr.write(
|
|
2619
|
-
`::warning::the findings artifact names a member outside the archive directory (${member}) \u2014 the re-review seeds without the prior document`
|
|
2620
|
-
);
|
|
2621
|
-
return null;
|
|
2622
|
-
}
|
|
2623
|
-
return await readFile(target, "utf-8");
|
|
2624
|
-
} finally {
|
|
2625
|
-
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2626
|
-
}
|
|
2627
|
-
} catch (err) {
|
|
2628
|
-
process.stderr.write(
|
|
2629
|
-
annotationSafe(
|
|
2630
|
-
`::warning::could not resolve the prior findings artifact ${zipUrl} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2631
|
-
)
|
|
2632
|
-
);
|
|
2633
|
-
return null;
|
|
2634
|
-
}
|
|
2635
|
-
};
|
|
2636
|
-
};
|
|
2637
|
-
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
2638
|
-
const { stdout } = await withEscapeRetry(
|
|
2639
|
-
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
2640
|
-
encoding: "buffer",
|
|
2641
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
2642
|
-
timeout: STEP_TIMEOUT_MS
|
|
2643
|
-
}),
|
|
2644
|
-
true
|
|
2645
|
-
);
|
|
2646
|
-
await writeFile(outPath, stdout);
|
|
2647
|
-
});
|
|
2648
|
-
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
2649
|
-
var withStampedConvergence = (doc, body) => {
|
|
2650
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
2651
|
-
const stripped = withoutKey(doc, "convergence");
|
|
2652
|
-
const stamped = parseConvergenceMarker(body);
|
|
2653
|
-
return stamped === null ? stripped : { ...stripped, convergence: stamped };
|
|
2654
|
-
};
|
|
2655
|
-
var resolvePriorFindings = async (body, read) => {
|
|
2656
|
-
const embedded = parseFindingsMarker(body);
|
|
2657
|
-
if (embedded !== null) return embedded;
|
|
2658
|
-
const url = findingsArtifactUrl(body);
|
|
2659
|
-
if (url === null) return null;
|
|
2660
|
-
const text = await read(url);
|
|
2661
|
-
if (text === null) return null;
|
|
2662
|
-
try {
|
|
2663
|
-
return withStampedConvergence(JSON.parse(text), body);
|
|
2664
|
-
} catch (err) {
|
|
2665
|
-
process.stderr.write(
|
|
2666
|
-
annotationSafe(
|
|
2667
|
-
`::warning::could not parse the findings document fetched from ${url} (${errMsg(err)}) \u2014 the re-review seeds without the prior document`
|
|
2668
|
-
)
|
|
2669
|
-
);
|
|
2670
|
-
return null;
|
|
2671
|
-
}
|
|
2672
|
-
};
|
|
2673
2763
|
|
|
2674
2764
|
// src/pr.ts
|
|
2675
2765
|
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
@@ -2812,6 +2902,9 @@ var checkRun = async (input, ghApi = runGhApi) => {
|
|
|
2812
2902
|
};
|
|
2813
2903
|
|
|
2814
2904
|
// src/post.ts
|
|
2905
|
+
var STICKY_CHAR_LIMIT = 65536;
|
|
2906
|
+
var SHED_RESERVE = 4e3;
|
|
2907
|
+
var SHED_NOTE_BUDGET = 512;
|
|
2815
2908
|
var DEFAULT_MARKER = "<!-- code-review -->";
|
|
2816
2909
|
var EMPTY_MECHANIC_LEAVE_MESSAGE = "The CI-fix pass found no issues and the sticky already reflects a completed full review \u2014 leaving it in place\n";
|
|
2817
2910
|
var MAX_SUGGESTION_LINES = 10;
|
|
@@ -2835,7 +2928,7 @@ var checkLongSuggestions = (comments) => {
|
|
|
2835
2928
|
return { comments: adjusted, longFiles };
|
|
2836
2929
|
};
|
|
2837
2930
|
var decodeFindings = (doc) => {
|
|
2838
|
-
const resolution =
|
|
2931
|
+
const resolution = resolve2("findings", doc);
|
|
2839
2932
|
switch (resolution.kind) {
|
|
2840
2933
|
case "ok":
|
|
2841
2934
|
return { kind: "ok", findings: resolution.value };
|
|
@@ -3453,6 +3546,18 @@ ${dropNote}` : ""}`,
|
|
|
3453
3546
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
3454
3547
|
const inlineRequested = input.inline === true;
|
|
3455
3548
|
const envelope = loadEnvelope(input.envelopePath);
|
|
3549
|
+
const effectiveRoute = input.route ?? envelope?.route;
|
|
3550
|
+
const runRenderInput = {
|
|
3551
|
+
prices: decodedPrices.right,
|
|
3552
|
+
pricesProvided: input.pricesProvided,
|
|
3553
|
+
template,
|
|
3554
|
+
route: effectiveRoute,
|
|
3555
|
+
reviewedSha: input.headSha,
|
|
3556
|
+
effort: input.effort,
|
|
3557
|
+
runUrl: input.runUrl,
|
|
3558
|
+
jsonUrl: input.jsonUrl,
|
|
3559
|
+
postedAt: input.postedAt
|
|
3560
|
+
};
|
|
3456
3561
|
const reachable = existingSticky !== null ? discussionRows(commentRows, existingSticky.id, input.botLogin) : [];
|
|
3457
3562
|
const priorIsMechanic = existingSticky !== null && (parseReviewedRoute(existingSticky.body) === "mechanic" || parseMechanicAncestor(existingSticky.body));
|
|
3458
3563
|
const noticeDiscussion = async () => {
|
|
@@ -3476,7 +3581,7 @@ ${dropNote}` : ""}`,
|
|
|
3476
3581
|
};
|
|
3477
3582
|
};
|
|
3478
3583
|
const renderNotice = (message, discussion) => {
|
|
3479
|
-
const noticeConvergence =
|
|
3584
|
+
const noticeConvergence = effectiveRoute === "mechanic" ? mechanicConvergence(
|
|
3480
3585
|
priorConv,
|
|
3481
3586
|
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3482
3587
|
) : priorConv;
|
|
@@ -3489,22 +3594,14 @@ ${dropNote}` : ""}`,
|
|
|
3489
3594
|
};
|
|
3490
3595
|
return formatMarkdown(
|
|
3491
3596
|
render({
|
|
3597
|
+
...runRenderInput,
|
|
3492
3598
|
findings: findings2,
|
|
3493
3599
|
envelope: null,
|
|
3494
3600
|
incomplete: true,
|
|
3495
|
-
prices: decodedPrices.right,
|
|
3496
|
-
pricesProvided: input.pricesProvided,
|
|
3497
|
-
template,
|
|
3498
|
-
route: input.route,
|
|
3499
|
-
reviewedSha: input.headSha,
|
|
3500
|
-
effort: input.effort,
|
|
3501
3601
|
sameRootNotes: {},
|
|
3502
3602
|
roundCount: priorRoundCount,
|
|
3503
3603
|
convergenceRound: false,
|
|
3504
|
-
runUrl: input.runUrl,
|
|
3505
|
-
jsonUrl: input.jsonUrl,
|
|
3506
3604
|
findingsPointer: noticeFindingsPointer(findings2),
|
|
3507
|
-
postedAt: input.postedAt,
|
|
3508
3605
|
orphanedDiscussion: discussion.orphanedDiscussion,
|
|
3509
3606
|
orphanedTotal: discussion.orphanedTotal,
|
|
3510
3607
|
orphanedTruncated: discussion.orphanedTruncated,
|
|
@@ -3601,7 +3698,101 @@ ${dropNote}` : ""}`,
|
|
|
3601
3698
|
const orphanedTruncated = stickyDiscussion.orphanedTruncated;
|
|
3602
3699
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
3603
3700
|
const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
|
|
3604
|
-
const
|
|
3701
|
+
const shedPriority = (f) => SEVERITIES.length - 1 - SEVERITIES.indexOf(f.severity);
|
|
3702
|
+
const shedOrderOf = (list) => list.map((f, index) => ({ f, index })).sort((a, b) => shedPriority(a.f) - shedPriority(b.f) || a.index - b.index).map((entry) => entry.f);
|
|
3703
|
+
const sizeSticky = (renderAt, total, reserve = SHED_RESERVE) => {
|
|
3704
|
+
const target = STICKY_CHAR_LIMIT - reserve - SHED_NOTE_BUDGET;
|
|
3705
|
+
const base = renderAt(0);
|
|
3706
|
+
if (base.length <= target) return { body: base, shedCount: 0, terminal: false };
|
|
3707
|
+
const allShed = renderAt(total);
|
|
3708
|
+
if (allShed.length > STICKY_CHAR_LIMIT - SHED_NOTE_BUDGET)
|
|
3709
|
+
return { body: "", shedCount: total, terminal: true };
|
|
3710
|
+
let low = 0;
|
|
3711
|
+
let high = total;
|
|
3712
|
+
let fitted = "";
|
|
3713
|
+
while (low < high) {
|
|
3714
|
+
const mid = low + high >> 1;
|
|
3715
|
+
const body = renderAt(mid);
|
|
3716
|
+
if (body.length <= target) {
|
|
3717
|
+
high = mid;
|
|
3718
|
+
fitted = body;
|
|
3719
|
+
} else {
|
|
3720
|
+
low = mid + 1;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
return { body: fitted !== "" ? fitted : allShed, shedCount: low, terminal: false };
|
|
3724
|
+
};
|
|
3725
|
+
const minimalTerminalBody = (findingsPointer2, route, incomplete, summaryAvailable2, modelNames2) => [
|
|
3726
|
+
DEFAULT_MARKER,
|
|
3727
|
+
`<!-- reviewed-sha: ${input.headSha} -->`,
|
|
3728
|
+
...!incomplete && route !== void 0 ? [`<!-- reviewed-route: ${route.replace(/--/g, "~~").replace(/\s+/g, " ")} -->`] : [],
|
|
3729
|
+
...!incomplete ? [REVIEW_COMPLETE_MARKER] : [],
|
|
3730
|
+
// The pointer's link form already begins with the directive; the convergence-only marker
|
|
3731
|
+
// does not, and a terminal surface must carry the directive either way.
|
|
3732
|
+
...findingsPointer2 !== "" ? [
|
|
3733
|
+
findingsPointer2.startsWith(AGENTS_STOP_DIRECTIVE) ? findingsPointer2 : `${AGENTS_STOP_DIRECTIVE}
|
|
3734
|
+
${findingsPointer2}`
|
|
3735
|
+
] : [],
|
|
3736
|
+
"",
|
|
3737
|
+
terminalNotice({
|
|
3738
|
+
reviewedSha: input.headSha,
|
|
3739
|
+
verdictBadge: verdictBadge(loadedFindings.verdict),
|
|
3740
|
+
route: route ?? void 0,
|
|
3741
|
+
postedAt: input.postedAt,
|
|
3742
|
+
roundsSummary: "",
|
|
3743
|
+
convergenceSummary: "",
|
|
3744
|
+
inlinePosted: 0,
|
|
3745
|
+
runUrl: input.runUrl,
|
|
3746
|
+
summaryAvailable: summaryAvailable2,
|
|
3747
|
+
findingsDocLinked: hasFindingsMarker(findingsPointer2),
|
|
3748
|
+
thisRunArtifact: input.jsonUrl !== void 0,
|
|
3749
|
+
modelNames: modelNames2
|
|
3750
|
+
})
|
|
3751
|
+
].join("\n");
|
|
3752
|
+
const terminalOrMinimal = (terminalRender, findingsPointer2, route, incomplete, summaryAvailable2, modelNames2) => terminalRender.length <= STICKY_CHAR_LIMIT ? terminalRender : minimalTerminalBody(findingsPointer2, route, incomplete, summaryAvailable2, modelNames2);
|
|
3753
|
+
const summaryAvailable = (process.env["GITHUB_STEP_SUMMARY"] ?? "") !== "";
|
|
3754
|
+
const modelNames = envelope !== null && envelope.models.length > 0 ? envelope.models.map((m) => m.model).join(", ") : "";
|
|
3755
|
+
const withShedNote = (body, shedCount, findingsPointer2) => {
|
|
3756
|
+
if (shedCount === 0) return body;
|
|
3757
|
+
const note = shedNote(
|
|
3758
|
+
shedCount,
|
|
3759
|
+
summaryAvailable,
|
|
3760
|
+
input.runUrl,
|
|
3761
|
+
hasFindingsMarker(findingsPointer2),
|
|
3762
|
+
input.jsonUrl !== void 0
|
|
3763
|
+
);
|
|
3764
|
+
return body.includes(note) ? body : `${body}
|
|
3765
|
+
|
|
3766
|
+
${note}
|
|
3767
|
+
`;
|
|
3768
|
+
};
|
|
3769
|
+
const sharedRenderInput = {
|
|
3770
|
+
...runRenderInput,
|
|
3771
|
+
repo: input.headRepo || input.repo,
|
|
3772
|
+
testReport,
|
|
3773
|
+
clocDiff,
|
|
3774
|
+
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3775
|
+
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3776
|
+
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3777
|
+
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3778
|
+
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3779
|
+
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3780
|
+
// must hold on the surface that lists findings without an inline review.
|
|
3781
|
+
suppressedNits,
|
|
3782
|
+
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3783
|
+
answeredNotes: reRaisedNotes,
|
|
3784
|
+
answeredReRaiseNote: answeredDropNote,
|
|
3785
|
+
discussionByFinding,
|
|
3786
|
+
orphanedDiscussion,
|
|
3787
|
+
orphanedTotal,
|
|
3788
|
+
discussionTruncated,
|
|
3789
|
+
orphanedTruncated,
|
|
3790
|
+
orphanedUnresolvable: orphanResolveFailed,
|
|
3791
|
+
convergenceThreshold: input.convergenceThreshold,
|
|
3792
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3793
|
+
pricedAt: input.pricedAt,
|
|
3794
|
+
summaryAvailable
|
|
3795
|
+
};
|
|
3605
3796
|
if (envelope === null) {
|
|
3606
3797
|
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
3607
3798
|
if (wouldBuryCompleted(envelopelessIncomplete)) {
|
|
@@ -3617,51 +3808,45 @@ ${dropNote}` : ""}`,
|
|
|
3617
3808
|
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3618
3809
|
) : priorConv
|
|
3619
3810
|
);
|
|
3620
|
-
const
|
|
3811
|
+
const lostInput = {
|
|
3812
|
+
...sharedRenderInput,
|
|
3813
|
+
findings: stampedFindings2,
|
|
3814
|
+
envelope: null,
|
|
3815
|
+
incomplete: envelopelessIncomplete,
|
|
3816
|
+
sameRootNotes: {},
|
|
3817
|
+
roundCount: priorRoundCount,
|
|
3818
|
+
convergenceRound: false,
|
|
3819
|
+
findingsPointer: findingsBlob(stampedFindings2)
|
|
3820
|
+
};
|
|
3821
|
+
const lostRender = (opts) => formatMarkdown(
|
|
3621
3822
|
render({
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
prices: decodedPrices.right,
|
|
3626
|
-
pricesProvided: input.pricesProvided,
|
|
3627
|
-
template,
|
|
3628
|
-
route: effectiveRoute,
|
|
3629
|
-
reviewedSha: input.headSha,
|
|
3630
|
-
repo: input.headRepo || input.repo,
|
|
3631
|
-
effort: input.effort,
|
|
3632
|
-
sameRootNotes: {},
|
|
3633
|
-
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3634
|
-
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3635
|
-
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3636
|
-
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3637
|
-
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3638
|
-
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3639
|
-
// must hold on the surface that lists findings without an inline review.
|
|
3640
|
-
strays: visibleFindings,
|
|
3641
|
-
suppressedNits,
|
|
3642
|
-
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3643
|
-
answeredNotes: reRaisedNotes,
|
|
3644
|
-
answeredReRaiseNote: answeredDropNote,
|
|
3645
|
-
roundCount: priorRoundCount,
|
|
3646
|
-
convergenceRound: false,
|
|
3647
|
-
testReport,
|
|
3648
|
-
clocDiff,
|
|
3649
|
-
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" },
|
|
3650
|
-
runUrl: input.runUrl,
|
|
3651
|
-
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3652
|
-
jsonUrl: input.jsonUrl,
|
|
3653
|
-
findingsPointer: findingsBlob(stampedFindings2),
|
|
3654
|
-
postedAt: input.postedAt,
|
|
3655
|
-
discussionByFinding,
|
|
3656
|
-
orphanedDiscussion,
|
|
3657
|
-
orphanedTotal,
|
|
3658
|
-
discussionTruncated,
|
|
3659
|
-
orphanedTruncated,
|
|
3660
|
-
orphanedUnresolvable: orphanResolveFailed
|
|
3823
|
+
...lostInput,
|
|
3824
|
+
...opts,
|
|
3825
|
+
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" }
|
|
3661
3826
|
})
|
|
3662
3827
|
);
|
|
3828
|
+
const lostShedOrder = shedOrderOf(visibleFindings);
|
|
3829
|
+
const lostRenderAt = (shedCount) => shedCount === 0 ? lostRender({ strays: visibleFindings }) : (() => {
|
|
3830
|
+
const dropped = new Set(lostShedOrder.slice(0, shedCount));
|
|
3831
|
+
return lostRender({
|
|
3832
|
+
strays: visibleFindings.filter((f) => !dropped.has(f)),
|
|
3833
|
+
shedCount
|
|
3834
|
+
});
|
|
3835
|
+
})();
|
|
3836
|
+
const lostSized = sizeSticky(lostRenderAt, visibleFindings.length, 0);
|
|
3837
|
+
const body = lostSized.terminal ? terminalOrMinimal(
|
|
3838
|
+
lostRender({ strays: visibleFindings, terminal: true }),
|
|
3839
|
+
findingsBlob(stampedFindings2),
|
|
3840
|
+
effectiveRoute,
|
|
3841
|
+
envelopelessIncomplete,
|
|
3842
|
+
summaryAvailable,
|
|
3843
|
+
""
|
|
3844
|
+
) : withShedNote(lostSized.body, lostSized.shedCount, findingsBlob(stampedFindings2));
|
|
3663
3845
|
await upsertSticky(input.repo, prNumber, existingSticky, body, ghApi, true);
|
|
3664
|
-
appendRunSummary(
|
|
3846
|
+
appendRunSummary(
|
|
3847
|
+
process.env["GITHUB_STEP_SUMMARY"],
|
|
3848
|
+
() => lostSized.shedCount === 0 ? lostSized.body : lostRender({ strays: visibleFindings })
|
|
3849
|
+
);
|
|
3665
3850
|
if (inlineRequested) {
|
|
3666
3851
|
process.stderr.write(
|
|
3667
3852
|
"Warning: inline: true was requested, but the result envelope is missing \u2014 inline comments cannot be built; the findings are in the sticky and the run summary instead\n"
|
|
@@ -3680,6 +3865,11 @@ ${dropNote}` : ""}`,
|
|
|
3680
3865
|
}
|
|
3681
3866
|
if (emptyMechanicWouldBury(effectiveRoute, thisIncomplete) && existingSticky !== null)
|
|
3682
3867
|
await emptyMechanicLeaveOrNote(existingSticky);
|
|
3868
|
+
if (!input.jsonUrl && existingSticky !== null && parseReviewComplete(existingSticky.body) && parseReviewedRoute(existingSticky.body) === "full review" && hasFindingsMarker(existingSticky.body)) {
|
|
3869
|
+
leaveInPlace(
|
|
3870
|
+
"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"
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3683
3873
|
const isRound = isConvergenceRound(effectiveRoute, thisIncomplete) && isReviewVerdict(findings.verdict);
|
|
3684
3874
|
const sameRootNotes = isRound ? computeSameRootNotes(priorTraj, findings.findings, input.headSha.slice(0, 12)) : {};
|
|
3685
3875
|
const {
|
|
@@ -3704,7 +3894,6 @@ ${dropNote}` : ""}`,
|
|
|
3704
3894
|
}
|
|
3705
3895
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
3706
3896
|
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
3707
|
-
const currentCounts = computeSeverityCounts(findings.findings);
|
|
3708
3897
|
const currentCodes = computeIdCounts(findings.findings, findings.systemic_problems ?? []);
|
|
3709
3898
|
const roundNumber = priorRoundCount + 1;
|
|
3710
3899
|
const convergence = isRound ? buildConvergence(
|
|
@@ -3724,40 +3913,15 @@ ${dropNote}` : ""}`,
|
|
|
3724
3913
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
3725
3914
|
const findingsMarker = findingsBlob(stampedFindings);
|
|
3726
3915
|
const commonRenderInput = {
|
|
3916
|
+
...sharedRenderInput,
|
|
3727
3917
|
findings: stampedFindings,
|
|
3728
3918
|
envelope,
|
|
3729
3919
|
incomplete: thisIncomplete,
|
|
3730
|
-
prices: decodedPrices.right,
|
|
3731
|
-
pricesProvided: input.pricesProvided,
|
|
3732
|
-
template,
|
|
3733
|
-
route: effectiveRoute,
|
|
3734
|
-
reviewedSha: input.headSha,
|
|
3735
|
-
repo: input.headRepo || input.repo,
|
|
3736
|
-
effort: input.effort,
|
|
3737
|
-
testReport,
|
|
3738
|
-
clocDiff,
|
|
3739
|
-
severityCounts: currentCounts,
|
|
3740
3920
|
sameRootNotes,
|
|
3741
|
-
answeredNotes: reRaisedNotes,
|
|
3742
|
-
answeredReRaiseNote: answeredDropNote,
|
|
3743
|
-
discussionByFinding,
|
|
3744
|
-
orphanedDiscussion,
|
|
3745
|
-
orphanedTotal,
|
|
3746
|
-
discussionTruncated,
|
|
3747
|
-
orphanedTruncated,
|
|
3748
|
-
orphanedUnresolvable: orphanResolveFailed,
|
|
3749
3921
|
roundCount: currentRoundCount,
|
|
3750
|
-
convergenceThreshold: input.convergenceThreshold,
|
|
3751
|
-
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3752
3922
|
convergenceRound: isRound,
|
|
3753
3923
|
strays,
|
|
3754
|
-
|
|
3755
|
-
runUrl: input.runUrl,
|
|
3756
|
-
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3757
|
-
jsonUrl: input.jsonUrl,
|
|
3758
|
-
findingsPointer: findingsMarker,
|
|
3759
|
-
postedAt: input.postedAt,
|
|
3760
|
-
pricedAt: input.pricedAt
|
|
3924
|
+
findingsPointer: findingsMarker
|
|
3761
3925
|
};
|
|
3762
3926
|
const longFilesNote = longFiles.length > 0 ? `
|
|
3763
3927
|
|
|
@@ -3765,27 +3929,56 @@ ${dropNote}` : ""}`,
|
|
|
3765
3929
|
|
|
3766
3930
|
> **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.
|
|
3767
3931
|
` : "";
|
|
3768
|
-
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays, discussionOverride) => formatMarkdown(
|
|
3932
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays, discussionOverride, terminal = false, shedCount = 0) => formatMarkdown(
|
|
3769
3933
|
render({
|
|
3770
3934
|
...commonRenderInput,
|
|
3771
3935
|
...straysOverride ? { strays: straysOverride } : {},
|
|
3772
3936
|
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
3773
3937
|
...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
|
|
3774
3938
|
...discussionOverride ?? {},
|
|
3939
|
+
...terminal ? { terminal } : {},
|
|
3940
|
+
...shedCount > 0 ? { shedCount } : {},
|
|
3775
3941
|
inlineDisposition,
|
|
3776
3942
|
reviewUrl: reviewUrl2
|
|
3777
3943
|
}) + longFilesNote
|
|
3778
3944
|
);
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3945
|
+
const mainShedOrder = shedOrderOf(strays);
|
|
3946
|
+
const mainRenderAt = (shedCount) => shedCount === 0 ? renderBody(initialDisposition) : renderBody(
|
|
3947
|
+
initialDisposition,
|
|
3948
|
+
void 0,
|
|
3949
|
+
strays.filter((f) => !new Set(mainShedOrder.slice(0, shedCount)).has(f)),
|
|
3950
|
+
void 0,
|
|
3951
|
+
void 0,
|
|
3952
|
+
void 0,
|
|
3953
|
+
false,
|
|
3954
|
+
shedCount
|
|
3955
|
+
);
|
|
3956
|
+
const mainSized = sizeSticky(
|
|
3957
|
+
mainRenderAt,
|
|
3958
|
+
strays.length,
|
|
3959
|
+
inlineRequested && rawComments.length > 0 ? SHED_RESERVE : 0
|
|
3960
|
+
);
|
|
3961
|
+
const fitStickyBody = () => mainSized.terminal ? terminalOrMinimal(
|
|
3962
|
+
renderBody(
|
|
3963
|
+
initialDisposition,
|
|
3964
|
+
void 0,
|
|
3965
|
+
void 0,
|
|
3966
|
+
void 0,
|
|
3967
|
+
void 0,
|
|
3968
|
+
void 0,
|
|
3969
|
+
true
|
|
3970
|
+
),
|
|
3971
|
+
findingsMarker,
|
|
3972
|
+
effectiveRoute,
|
|
3973
|
+
thisIncomplete,
|
|
3974
|
+
summaryAvailable,
|
|
3975
|
+
modelNames
|
|
3976
|
+
) : withShedNote(mainSized.body, mainSized.shedCount, findingsMarker);
|
|
3784
3977
|
const stickyRef = await upsertSticky(
|
|
3785
3978
|
input.repo,
|
|
3786
3979
|
prNumber,
|
|
3787
3980
|
existingSticky,
|
|
3788
|
-
|
|
3981
|
+
fitStickyBody(),
|
|
3789
3982
|
ghApi,
|
|
3790
3983
|
true
|
|
3791
3984
|
);
|
|
@@ -3827,7 +4020,6 @@ ${dropNote}` : ""}`,
|
|
|
3827
4020
|
await minimizeComments(prNumber, priorInlineComments, ghApi);
|
|
3828
4021
|
}
|
|
3829
4022
|
const unanchoredCount = unposted.length;
|
|
3830
|
-
const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
|
|
3831
4023
|
const finalDiscussion = unanchoredCount > 0 ? buildStickyDiscussion(
|
|
3832
4024
|
reachable,
|
|
3833
4025
|
[...currentIds, ...unposted.map((f) => f.id).filter((id) => id !== "")],
|
|
@@ -3842,29 +4034,75 @@ ${dropNote}` : ""}`,
|
|
|
3842
4034
|
} : void 0;
|
|
3843
4035
|
if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
|
|
3844
4036
|
const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
stickyRef.id,
|
|
3849
|
-
renderBody(
|
|
4037
|
+
if (mainSized.terminal) {
|
|
4038
|
+
try {
|
|
4039
|
+
const terminalPatch = renderBody(
|
|
3850
4040
|
finalDisposition,
|
|
3851
4041
|
reviewUrl,
|
|
3852
|
-
|
|
4042
|
+
void 0,
|
|
3853
4043
|
unanchoredCount,
|
|
3854
4044
|
unposted,
|
|
3855
|
-
finalDiscussionOverride
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
4045
|
+
finalDiscussionOverride,
|
|
4046
|
+
true
|
|
4047
|
+
);
|
|
4048
|
+
if (terminalPatch.length <= STICKY_CHAR_LIMIT) {
|
|
4049
|
+
await patchComment(input.repo, stickyRef.id, terminalPatch, ghApi);
|
|
4050
|
+
process.stderr.write(
|
|
4051
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
3861
4052
|
`
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
4053
|
+
);
|
|
4054
|
+
} else {
|
|
4055
|
+
process.stderr.write(
|
|
4056
|
+
`Warning: the terminal sticky patch exceeds GitHub's comment limit \u2014 the sticky stands; the run summary carries the review
|
|
3866
4057
|
`
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
} catch (err) {
|
|
4061
|
+
process.stderr.write(
|
|
4062
|
+
`Warning: failed to update the terminal sticky after the review: ${errMsg(err)}
|
|
4063
|
+
`
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
} else {
|
|
4067
|
+
const patchShedOrder = shedOrderOf(strays);
|
|
4068
|
+
const patchRenderAt = (shedCount) => renderBody(
|
|
4069
|
+
finalDisposition,
|
|
4070
|
+
reviewUrl,
|
|
4071
|
+
[
|
|
4072
|
+
...unposted,
|
|
4073
|
+
...shedCount === 0 ? strays : strays.filter((f) => !new Set(patchShedOrder.slice(0, shedCount)).has(f))
|
|
4074
|
+
],
|
|
4075
|
+
unanchoredCount,
|
|
4076
|
+
unposted,
|
|
4077
|
+
finalDiscussionOverride,
|
|
4078
|
+
false,
|
|
4079
|
+
shedCount
|
|
3867
4080
|
);
|
|
4081
|
+
const patchSized = sizeSticky(patchRenderAt, strays.length, 0);
|
|
4082
|
+
try {
|
|
4083
|
+
if (patchSized.terminal) {
|
|
4084
|
+
process.stderr.write(
|
|
4085
|
+
`Warning: the final sticky patch still exceeds GitHub's comment limit \u2014 the sticky stands as posted${summaryAvailable ? "; the whole review (rejected findings included) is in the run summary" : ""}
|
|
4086
|
+
`
|
|
4087
|
+
);
|
|
4088
|
+
} else {
|
|
4089
|
+
await patchComment(
|
|
4090
|
+
input.repo,
|
|
4091
|
+
stickyRef.id,
|
|
4092
|
+
withShedNote(patchSized.body, patchSized.shedCount, findingsMarker),
|
|
4093
|
+
ghApi
|
|
4094
|
+
);
|
|
4095
|
+
process.stderr.write(
|
|
4096
|
+
`Updated sticky comment #${String(stickyRef.id)} to reflect the review
|
|
4097
|
+
`
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
process.stderr.write(
|
|
4102
|
+
`Warning: failed to update the sticky summary after the review: ${errMsg(err)}
|
|
4103
|
+
`
|
|
4104
|
+
);
|
|
4105
|
+
}
|
|
3868
4106
|
}
|
|
3869
4107
|
}
|
|
3870
4108
|
appendRunSummary(
|
|
@@ -4261,6 +4499,13 @@ failing_jobs=${String(result.failingJobs)}
|
|
|
4261
4499
|
`;
|
|
4262
4500
|
}
|
|
4263
4501
|
};
|
|
4502
|
+
var FETCH_WITHOUT_DETACHED_CHILDREN = [
|
|
4503
|
+
"-c",
|
|
4504
|
+
"maintenance.auto=false",
|
|
4505
|
+
"-c",
|
|
4506
|
+
"gc.auto=0",
|
|
4507
|
+
"fetch"
|
|
4508
|
+
];
|
|
4264
4509
|
var runGit = (args) => execFileWithTimeout({
|
|
4265
4510
|
command: "git",
|
|
4266
4511
|
args,
|
|
@@ -4328,7 +4573,7 @@ var fetchFullDiff = async (repo, defaultBranch, headSha, ghApi, gitRun) => {
|
|
|
4328
4573
|
process.stderr.write(`compare diff fetch failed (${errMsg(err)}) \u2014 falling back to git diff
|
|
4329
4574
|
`);
|
|
4330
4575
|
}
|
|
4331
|
-
await gitRun([
|
|
4576
|
+
await gitRun([...FETCH_WITHOUT_DETACHED_CHILDREN, "origin", headSha]);
|
|
4332
4577
|
const base = (await gitRun(["rev-parse", "HEAD"])).trim();
|
|
4333
4578
|
return gitRun(["diff", `${base}...${headSha}`]);
|
|
4334
4579
|
};
|
|
@@ -4520,7 +4765,7 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4520
4765
|
`PR diff fetch failed or was empty for ${String(meta.changed_files)} changed files \u2014 falling back to git diff
|
|
4521
4766
|
`
|
|
4522
4767
|
);
|
|
4523
|
-
await gitRun([
|
|
4768
|
+
await gitRun([...FETCH_WITHOUT_DETACHED_CHILDREN, "origin", input.headSha]);
|
|
4524
4769
|
return gitRun(["diff", meta.base_sha, input.headSha]);
|
|
4525
4770
|
})();
|
|
4526
4771
|
const fullDiff = stacked ? await fetchFullDiff(input.repo, input.defaultBranch, input.headSha, ghApi, gitRun) : prDiff;
|
|
@@ -4633,7 +4878,7 @@ var gateCandidate = (kind, rawCandidate) => {
|
|
|
4633
4878
|
const schemaPath = safeSchemaPathFor(kind, candidateVersion(kind, candidate));
|
|
4634
4879
|
if (schemaPath === null) return null;
|
|
4635
4880
|
if (!validateAgainstSchema(candidate, schemaPath).valid) return null;
|
|
4636
|
-
const resolution =
|
|
4881
|
+
const resolution = resolve2(kind, candidate);
|
|
4637
4882
|
return resolution.kind === "ok" ? { version: resolution.version, candidate } : null;
|
|
4638
4883
|
};
|
|
4639
4884
|
var candidateFromJsonText = (kind, text) => {
|
|
@@ -4756,13 +5001,6 @@ var ClaudeCodeEnvelopeCodec = t.intersection([
|
|
|
4756
5001
|
structured_output: t.unknown
|
|
4757
5002
|
})
|
|
4758
5003
|
]);
|
|
4759
|
-
var sumUsage = (into, entry) => ({
|
|
4760
|
-
...into,
|
|
4761
|
-
input_tokens: into.input_tokens + entry.input_tokens,
|
|
4762
|
-
output_tokens: into.output_tokens + entry.output_tokens,
|
|
4763
|
-
...into.cache_read_tokens !== void 0 || entry.cache_read_tokens !== void 0 ? { cache_read_tokens: (into.cache_read_tokens ?? 0) + (entry.cache_read_tokens ?? 0) } : {},
|
|
4764
|
-
...into.cache_write_tokens !== void 0 || entry.cache_write_tokens !== void 0 ? { cache_write_tokens: (into.cache_write_tokens ?? 0) + (entry.cache_write_tokens ?? 0) } : {}
|
|
4765
|
-
});
|
|
4766
5004
|
var mapModelUsage = (modelUsage) => [
|
|
4767
5005
|
...Object.entries(modelUsage).reduce((byModel, [configuredModelId, entry]) => {
|
|
4768
5006
|
const model = modelIdentity(configuredModelId);
|
|
@@ -4774,7 +5012,7 @@ var mapModelUsage = (modelUsage) => [
|
|
|
4774
5012
|
...entry.cacheCreationInputTokens !== void 0 ? { cache_write_tokens: entry.cacheCreationInputTokens } : {}
|
|
4775
5013
|
};
|
|
4776
5014
|
const prior = byModel.get(model);
|
|
4777
|
-
return byModel.set(model, prior === void 0 ? mapped :
|
|
5015
|
+
return byModel.set(model, prior === void 0 ? mapped : addModelUsage(prior, mapped));
|
|
4778
5016
|
}, /* @__PURE__ */ new Map()).values()
|
|
4779
5017
|
];
|
|
4780
5018
|
var findingsOutcome = (native, agentFilePath, agentFileFallbackPath) => {
|
|
@@ -4786,7 +5024,7 @@ var findingsOutcome = (native, agentFilePath, agentFileFallbackPath) => {
|
|
|
4786
5024
|
});
|
|
4787
5025
|
if (ladder.kind !== "ok")
|
|
4788
5026
|
return { kind: "telemetry-only", reason: describeLadderFailure(ladder) };
|
|
4789
|
-
const resolution =
|
|
5027
|
+
const resolution = resolve2("findings", ladder.candidate);
|
|
4790
5028
|
return resolution.kind === "ok" ? { kind: "ok", version: resolution.version, findings: resolution.value } : {
|
|
4791
5029
|
kind: "telemetry-only",
|
|
4792
5030
|
reason: "internal error: the extraction ladder validated a candidate the registry then rejected"
|
|
@@ -4895,7 +5133,6 @@ var composeReviewSettings = (opts) => {
|
|
|
4895
5133
|
};
|
|
4896
5134
|
|
|
4897
5135
|
// src/sandbox.ts
|
|
4898
|
-
var GITHUB_HOSTS = ["api.github.com", "github.com"];
|
|
4899
5136
|
var deriveModelHost = (apiBaseUrl) => {
|
|
4900
5137
|
const host = URL.canParse(apiBaseUrl) ? new URL(apiBaseUrl).hostname : "";
|
|
4901
5138
|
if (host === "") {
|
|
@@ -4915,11 +5152,7 @@ var parseExtraEndpoints = (extra) => extra.split(/\s+/).map((token) => token.rep
|
|
|
4915
5152
|
var buildSandboxConfig = (opts) => ({
|
|
4916
5153
|
network: {
|
|
4917
5154
|
allowedDomains: [
|
|
4918
|
-
.../* @__PURE__ */ new Set([
|
|
4919
|
-
deriveModelHost(opts.apiBaseUrl),
|
|
4920
|
-
...GITHUB_HOSTS,
|
|
4921
|
-
...parseExtraEndpoints(opts.extra ?? "")
|
|
4922
|
-
])
|
|
5155
|
+
.../* @__PURE__ */ new Set([deriveModelHost(opts.apiBaseUrl), ...parseExtraEndpoints(opts.extra ?? "")])
|
|
4923
5156
|
],
|
|
4924
5157
|
deniedDomains: []
|
|
4925
5158
|
},
|
|
@@ -4945,7 +5178,7 @@ var parseScope = (raw) => {
|
|
|
4945
5178
|
var resolvedPriorValue = resolveTolerantFindings;
|
|
4946
5179
|
var readJSON = (path) => {
|
|
4947
5180
|
try {
|
|
4948
|
-
return JSON.parse(readFileSync(resolve
|
|
5181
|
+
return JSON.parse(readFileSync(resolve(path), "utf-8"));
|
|
4949
5182
|
} catch (err) {
|
|
4950
5183
|
return fail(`Cannot read ${path}: ${errMsg(err)}`);
|
|
4951
5184
|
}
|
|
@@ -4958,7 +5191,7 @@ var fail = (msg) => {
|
|
|
4958
5191
|
var readJSONOrAbsent = (path) => {
|
|
4959
5192
|
const read = (() => {
|
|
4960
5193
|
try {
|
|
4961
|
-
return { text: readFileSync(resolve
|
|
5194
|
+
return { text: readFileSync(resolve(path), "utf-8") };
|
|
4962
5195
|
} catch (err) {
|
|
4963
5196
|
return { error: errMsg(err) };
|
|
4964
5197
|
}
|
|
@@ -4988,7 +5221,7 @@ var readJSONOrAbsent = (path) => {
|
|
|
4988
5221
|
}
|
|
4989
5222
|
};
|
|
4990
5223
|
var readSandboxConfigForNotice = (path) => {
|
|
4991
|
-
const text = readFileOrNull(resolve
|
|
5224
|
+
const text = readFileOrNull(resolve(path));
|
|
4992
5225
|
if (text === null) return void 0;
|
|
4993
5226
|
const parsed = tryParseJson(text);
|
|
4994
5227
|
if (!parsed.ok) {
|
|
@@ -5029,7 +5262,7 @@ var unwrapAdapt = (either) => {
|
|
|
5029
5262
|
}
|
|
5030
5263
|
};
|
|
5031
5264
|
var transcriptFallbackFrom = (path) => {
|
|
5032
|
-
const tree = readTranscriptTree(resolve
|
|
5265
|
+
const tree = readTranscriptTree(resolve(path));
|
|
5033
5266
|
if (tree.missing)
|
|
5034
5267
|
process.stderr.write(
|
|
5035
5268
|
`code-review adapt: transcript ${path} is unreadable \u2014 no telemetry fallback
|
|
@@ -5038,13 +5271,13 @@ var transcriptFallbackFrom = (path) => {
|
|
|
5038
5271
|
const usage = sumTranscriptUsage(tree.entries);
|
|
5039
5272
|
return { models: usage.models, turns: usage.turns, durationMs: usage.durationMs };
|
|
5040
5273
|
};
|
|
5041
|
-
var bundledPath = (...segments) => resolve
|
|
5274
|
+
var bundledPath = (...segments) => resolve(import.meta.dirname, "..", ...segments);
|
|
5042
5275
|
var readPackageVersion = () => JSON.parse(readFileSync(bundledPath("package.json"), "utf-8")).version;
|
|
5043
5276
|
var packageVersion = readPackageVersion();
|
|
5044
|
-
var resolveTemplatePath = (templateArg) => templateArg ? resolve
|
|
5045
|
-
var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve
|
|
5277
|
+
var resolveTemplatePath = (templateArg) => templateArg ? resolve(templateArg) : bundledPath("templates", "comment.eta");
|
|
5278
|
+
var resolveInlineTemplatePath = (templateArg) => templateArg ? resolve(templateArg) : bundledPath("templates", "inline.eta");
|
|
5046
5279
|
var resolvePrices = (pricesArg) => {
|
|
5047
|
-
if (pricesArg) return { kind: "provided", path: resolve
|
|
5280
|
+
if (pricesArg) return { kind: "provided", path: resolve(pricesArg) };
|
|
5048
5281
|
process.stderr.write(
|
|
5049
5282
|
"code-review: no --prices given \u2014 cost will be reported as N/A (no price map to recompute from)\n"
|
|
5050
5283
|
);
|
|
@@ -5174,7 +5407,7 @@ var inlineCmd = defineCommand({
|
|
|
5174
5407
|
},
|
|
5175
5408
|
run: async ({ args }) => {
|
|
5176
5409
|
const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
|
|
5177
|
-
const diff = readFileSync(resolve
|
|
5410
|
+
const diff = readFileSync(resolve(args.diff), "utf-8");
|
|
5178
5411
|
const inlineTemplate = readFileSync(resolveInlineTemplatePath(args.template), "utf-8");
|
|
5179
5412
|
const floor = parseNitVisibilityFloor(args["nit-visibility-floor"]);
|
|
5180
5413
|
const visibleFindings = findings.findings.filter((f) => !isBelowVisibilityFloor(f, floor));
|
|
@@ -5211,6 +5444,8 @@ var costCmd = defineCommand({
|
|
|
5211
5444
|
const report = computeCost(
|
|
5212
5445
|
envelope.models,
|
|
5213
5446
|
prices,
|
|
5447
|
+
// Price a saved envelope at the RUN's own instant (issue #170), so re-running `cost` later prices
|
|
5448
|
+
// the same envelope to the same slot deterministically — not at whatever wall clock it is re-run at.
|
|
5214
5449
|
parseInstant(envelope.generated_at) ?? /* @__PURE__ */ new Date()
|
|
5215
5450
|
);
|
|
5216
5451
|
process.stdout.write(JSON.stringify(report, null, 2));
|
|
@@ -5233,7 +5468,7 @@ var checkCostCmd = defineCommand({
|
|
|
5233
5468
|
}
|
|
5234
5469
|
},
|
|
5235
5470
|
run: async ({ args }) => {
|
|
5236
|
-
const tree = readTranscriptTree(resolve
|
|
5471
|
+
const tree = readTranscriptTree(resolve(args.transcript));
|
|
5237
5472
|
if (tree.missing) {
|
|
5238
5473
|
process.stderr.write(
|
|
5239
5474
|
`code-review check-cost: transcript ${args.transcript} is unreadable \u2014 reporting zero spend
|
|
@@ -5267,7 +5502,7 @@ var checkCostCmd = defineCommand({
|
|
|
5267
5502
|
});
|
|
5268
5503
|
var tryReadPrices = (path) => {
|
|
5269
5504
|
try {
|
|
5270
|
-
const decoded = PriceMapCodec.decode(JSON.parse(readFileSync(resolve
|
|
5505
|
+
const decoded = PriceMapCodec.decode(JSON.parse(readFileSync(resolve(path), "utf-8")));
|
|
5271
5506
|
return decoded._tag === "Right" ? decoded.right : null;
|
|
5272
5507
|
} catch {
|
|
5273
5508
|
return null;
|
|
@@ -5386,10 +5621,10 @@ var budgetHookCmd = defineCommand({
|
|
|
5386
5621
|
},
|
|
5387
5622
|
run: async ({ args }) => {
|
|
5388
5623
|
try {
|
|
5389
|
-
const draftPath = resolve
|
|
5624
|
+
const draftPath = resolve(args.draft);
|
|
5390
5625
|
const input = readStdinJSON();
|
|
5391
5626
|
const transcriptPath = transcriptPathOf(input);
|
|
5392
|
-
const tree = transcriptPath ? readTranscriptTree(resolve
|
|
5627
|
+
const tree = transcriptPath ? readTranscriptTree(resolve(transcriptPath)) : void 0;
|
|
5393
5628
|
const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
|
|
5394
5629
|
const prices = args.prices ? tryReadPrices(args.prices) : null;
|
|
5395
5630
|
const costReport = prices !== null && usage ? (
|
|
@@ -5518,7 +5753,7 @@ var printSettingsCmd = defineCommand({
|
|
|
5518
5753
|
if (args.kind && !["findings", "triage", "prices"].includes(args.kind))
|
|
5519
5754
|
fail(`--kind must be one of findings|triage|prices (got '${args.kind}')`);
|
|
5520
5755
|
const settings = composeReviewSettings({
|
|
5521
|
-
draftPath: resolve
|
|
5756
|
+
draftPath: resolve(args.draft),
|
|
5522
5757
|
stop: {
|
|
5523
5758
|
kind: args.kind,
|
|
5524
5759
|
schema: args.schema,
|
|
@@ -5610,7 +5845,7 @@ var validateCmd = defineCommand({
|
|
|
5610
5845
|
run: async ({ args }) => {
|
|
5611
5846
|
const kind = requireSchemaKind(args.kind || "findings");
|
|
5612
5847
|
const documentRaw = readJSON(args.document);
|
|
5613
|
-
const schemaPath = args.schema ? resolve
|
|
5848
|
+
const schemaPath = args.schema ? resolve(args.schema) : requireSchemaPath(kind, args["schema-version"] || derivedSchemaVersion(kind, documentRaw));
|
|
5614
5849
|
const { valid, errors } = validateAgainstSchema(documentRaw, schemaPath);
|
|
5615
5850
|
if (valid) {
|
|
5616
5851
|
process.stdout.write("\u2705 valid\n");
|
|
@@ -5677,7 +5912,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5677
5912
|
}
|
|
5678
5913
|
},
|
|
5679
5914
|
run: async ({ args }) => {
|
|
5680
|
-
const outPath = resolve
|
|
5915
|
+
const outPath = resolve(args.out);
|
|
5681
5916
|
const kindArg = args.kind || "findings";
|
|
5682
5917
|
const kind = isSchemaKind(kindArg) ? kindArg : "findings";
|
|
5683
5918
|
if (kind !== kindArg) {
|
|
@@ -5706,7 +5941,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5706
5941
|
if (!args.prior) return null;
|
|
5707
5942
|
const raw = (() => {
|
|
5708
5943
|
try {
|
|
5709
|
-
return JSON.parse(readFileSync(resolve
|
|
5944
|
+
return JSON.parse(readFileSync(resolve(args.prior), "utf-8"));
|
|
5710
5945
|
} catch {
|
|
5711
5946
|
return null;
|
|
5712
5947
|
}
|
|
@@ -5716,7 +5951,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5716
5951
|
const stagedPrior = (() => {
|
|
5717
5952
|
if (!args["prior-findings"]) return null;
|
|
5718
5953
|
try {
|
|
5719
|
-
return JSON.parse(readFileSync(resolve
|
|
5954
|
+
return JSON.parse(readFileSync(resolve(args["prior-findings"]), "utf-8"));
|
|
5720
5955
|
} catch {
|
|
5721
5956
|
return null;
|
|
5722
5957
|
}
|
|
@@ -5728,7 +5963,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5728
5963
|
const answeredRegistry = (() => {
|
|
5729
5964
|
if (!args["prior-answers"]) return null;
|
|
5730
5965
|
try {
|
|
5731
|
-
const raw = JSON.parse(readFileSync(resolve
|
|
5966
|
+
const raw = JSON.parse(readFileSync(resolve(args["prior-answers"]), "utf-8"));
|
|
5732
5967
|
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
5733
5968
|
const decoded = raw.flatMap((row) => {
|
|
5734
5969
|
const entry = decodeAnsweredEntry(row);
|
|
@@ -5797,7 +6032,7 @@ var seedDraftCmd = defineCommand({
|
|
|
5797
6032
|
}
|
|
5798
6033
|
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
5799
6034
|
try {
|
|
5800
|
-
const schemaPath = args.schema ? resolve
|
|
6035
|
+
const schemaPath = args.schema ? resolve(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
5801
6036
|
const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
|
|
5802
6037
|
Object.entries(priorFindings).filter(
|
|
5803
6038
|
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
@@ -6014,7 +6249,7 @@ var readFileLines = (path) => {
|
|
|
6014
6249
|
};
|
|
6015
6250
|
var validateFinding = (finding, repoRoot) => {
|
|
6016
6251
|
if (finding.patch === void 0) return finding;
|
|
6017
|
-
const lines = readFileLines(resolve
|
|
6252
|
+
const lines = readFileLines(resolve(repoRoot, finding.path));
|
|
6018
6253
|
if (lines === null) {
|
|
6019
6254
|
process.stderr.write(
|
|
6020
6255
|
`validate-patches: ${finding.path}: could not read file at "${repoRoot}" \u2014 dropping patch
|
|
@@ -6054,7 +6289,7 @@ var validatePatchesCmd = defineCommand({
|
|
|
6054
6289
|
},
|
|
6055
6290
|
run: async ({ args }) => {
|
|
6056
6291
|
const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
|
|
6057
|
-
const repoRoot = args["repo-root"] ? resolve
|
|
6292
|
+
const repoRoot = args["repo-root"] ? resolve(args["repo-root"]) : process.cwd();
|
|
6058
6293
|
const validated = {
|
|
6059
6294
|
...findings,
|
|
6060
6295
|
findings: findings.findings.map((f) => validateFinding(f, repoRoot))
|
|
@@ -6168,7 +6403,7 @@ var stopGateCmd = defineCommand({
|
|
|
6168
6403
|
}
|
|
6169
6404
|
},
|
|
6170
6405
|
run: async ({ args }) => {
|
|
6171
|
-
const draftPath = resolve
|
|
6406
|
+
const draftPath = resolve(args.draft);
|
|
6172
6407
|
if (args["print-settings"]) {
|
|
6173
6408
|
const command = defaultHookCommand(draftPath, {
|
|
6174
6409
|
kind: args.kind,
|
|
@@ -6184,10 +6419,10 @@ var stopGateCmd = defineCommand({
|
|
|
6184
6419
|
drainStdin();
|
|
6185
6420
|
const kind = requireSchemaKind(args.kind || "findings");
|
|
6186
6421
|
const maxNudges = requireMaxNudges(args["max-nudges"]);
|
|
6187
|
-
const counterPath = args.counter ? resolve
|
|
6422
|
+
const counterPath = args.counter ? resolve(args.counter) : `${draftPath}.nudges`;
|
|
6188
6423
|
const state = draftState(
|
|
6189
6424
|
draftPath,
|
|
6190
|
-
(parsed) => args.schema ? resolve
|
|
6425
|
+
(parsed) => args.schema ? resolve(args.schema) : schemaPathFor(kind, args["schema-version"] || derivedSchemaVersion(kind, parsed))
|
|
6191
6426
|
);
|
|
6192
6427
|
const nudges = readNudges(counterPath);
|
|
6193
6428
|
const decision = decideGate(state, nudges, maxNudges, draftPath, kind);
|
|
@@ -6255,7 +6490,7 @@ var gatherCmd = defineCommand({
|
|
|
6255
6490
|
runId: args["run-id"],
|
|
6256
6491
|
conclusion: args.conclusion,
|
|
6257
6492
|
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
6258
|
-
outDir: args["out-dir"] ? resolve
|
|
6493
|
+
outDir: args["out-dir"] ? resolve(args["out-dir"]) : process.cwd()
|
|
6259
6494
|
});
|
|
6260
6495
|
process.stdout.write(renderOutputs(result));
|
|
6261
6496
|
}
|
|
@@ -6349,6 +6584,11 @@ var postCmd = defineCommand({
|
|
|
6349
6584
|
},
|
|
6350
6585
|
run: async ({ args }) => {
|
|
6351
6586
|
const priceResolution = resolvePrices(args.prices);
|
|
6587
|
+
if (/--|\s/.test(args["head-sha"])) {
|
|
6588
|
+
throw new Error(
|
|
6589
|
+
'--head-sha must be a commit SHA (no "--" or whitespace): ' + args["head-sha"]
|
|
6590
|
+
);
|
|
6591
|
+
}
|
|
6352
6592
|
await post({
|
|
6353
6593
|
repo: args.repo,
|
|
6354
6594
|
// The workflow's post step threads HEAD_REPO env (the fork's owner/name) — a finding
|
|
@@ -6704,7 +6944,7 @@ var checkScopeCmd = defineCommand({
|
|
|
6704
6944
|
var sandboxConfigCmd = defineCommand({
|
|
6705
6945
|
meta: {
|
|
6706
6946
|
name: "sandbox-config",
|
|
6707
|
-
description: "Emit the sandbox-runtime (srt) settings that jail the untrusted review agent's egress: allow the model host (derived from api_base_url)
|
|
6947
|
+
description: "Emit the sandbox-runtime (srt) settings that jail the untrusted review agent's egress: allow the model host (derived from api_base_url) and the consumer's extra_endpoints; deny all else; filesystem isolation off"
|
|
6708
6948
|
},
|
|
6709
6949
|
args: {
|
|
6710
6950
|
"api-base-url": {
|
|
@@ -6749,7 +6989,7 @@ var sandboxConfigCmd = defineCommand({
|
|
|
6749
6989
|
const json = `${JSON.stringify(config, null, 2)}
|
|
6750
6990
|
`;
|
|
6751
6991
|
if (args.out) {
|
|
6752
|
-
writeFileSync(resolve
|
|
6992
|
+
writeFileSync(resolve(args.out), json);
|
|
6753
6993
|
} else {
|
|
6754
6994
|
process.stdout.write(json);
|
|
6755
6995
|
}
|