@jphutchins/code-review 0.1.0-alpha.51 → 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/README.md +6 -4
- package/dist/index.js +419 -135
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +28 -1
- package/schema/prices.example.json +29 -5
- package/schema/prices.schema.json +73 -6
- package/templates/comment.eta +31 -7
package/dist/index.js
CHANGED
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync } from 'fs';
|
|
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"),
|
|
@@ -282,9 +311,16 @@ var NonEmptyPriceSlots = t.refinement(
|
|
|
282
311
|
(a) => a.length >= 1,
|
|
283
312
|
"NonEmptyPriceSlots"
|
|
284
313
|
);
|
|
314
|
+
var SlottedRequired = t.type({ slots: NonEmptyPriceSlots });
|
|
315
|
+
var SlottedOptional = t.partial({ weekend_slots: NonEmptyPriceSlots });
|
|
316
|
+
var SlottedShape = t.intersection([SlottedRequired, SlottedOptional]);
|
|
317
|
+
var SLOTTED_KEYS = /* @__PURE__ */ new Set([
|
|
318
|
+
...Object.keys(SlottedRequired.props),
|
|
319
|
+
...Object.keys(SlottedOptional.props)
|
|
320
|
+
]);
|
|
285
321
|
var SlottedModelPricesStrict = t.refinement(
|
|
286
|
-
|
|
287
|
-
(s) => Object.keys(s).every((k) => k
|
|
322
|
+
SlottedShape,
|
|
323
|
+
(s) => Object.keys(s).every((k) => SLOTTED_KEYS.has(k)),
|
|
288
324
|
"SlottedModelPricesStrict"
|
|
289
325
|
);
|
|
290
326
|
var SlottedModelPricesCodec = t.exact(SlottedModelPricesStrict);
|
|
@@ -339,6 +375,11 @@ var slotCovers = (slot, minute) => {
|
|
|
339
375
|
const to = hhmmToMinutes(slot.utc_to);
|
|
340
376
|
return from < to ? minute >= from && minute < to : minute >= from || minute < to;
|
|
341
377
|
};
|
|
378
|
+
var BEIJING_OFFSET_MINUTES = 8 * 60;
|
|
379
|
+
var isBeijingWeekend = (instant) => {
|
|
380
|
+
const day = new Date(instant.getTime() + BEIJING_OFFSET_MINUTES * 6e4).getUTCDay();
|
|
381
|
+
return day === 0 || day === 6;
|
|
382
|
+
};
|
|
342
383
|
var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
343
384
|
if (!("slots" in p)) return p;
|
|
344
385
|
if (pricedAt === void 0) {
|
|
@@ -348,10 +389,13 @@ var resolveFlatPrices = (model, p, pricedAt, warn) => {
|
|
|
348
389
|
return null;
|
|
349
390
|
}
|
|
350
391
|
const minute = utcMinuteOfDay(pricedAt);
|
|
351
|
-
const
|
|
392
|
+
const weekendSlots = p.weekend_slots;
|
|
393
|
+
const useWeekend = weekendSlots != null && isBeijingWeekend(pricedAt);
|
|
394
|
+
const slots = useWeekend ? weekendSlots : p.slots;
|
|
395
|
+
const covering = slots.filter((s) => slotCovers(s, minute));
|
|
352
396
|
if (covering.length === 1) return covering[0] ?? null;
|
|
353
397
|
warn(
|
|
354
|
-
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots cover ${hhmmOf(minute)} UTC (expected exactly 1);
|
|
398
|
+
`code-review cost: model "${model}" \u2014 ${String(covering.length)} price slots in \`${useWeekend ? "weekend_slots" : "slots"}\` cover ${hhmmOf(minute)} UTC (expected exactly 1); that array must partition the 24h day with no gap or overlap; cost for this model set to $0`
|
|
355
399
|
);
|
|
356
400
|
return null;
|
|
357
401
|
};
|
|
@@ -505,16 +549,10 @@ var severityEmoji = (s) => {
|
|
|
505
549
|
return "\u2753";
|
|
506
550
|
}
|
|
507
551
|
};
|
|
508
|
-
var EMBED_LIMIT = 42700;
|
|
509
552
|
var FINDINGS_SCHEMA_URL = "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json";
|
|
510
|
-
var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment
|
|
511
|
-
var
|
|
512
|
-
|
|
513
|
-
const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
|
|
514
|
-
const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
|
|
515
|
-
return marker ? `${AGENTS_STOP_DIRECTIVE}
|
|
516
|
-
${marker}` : "";
|
|
517
|
-
};
|
|
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} -->`;
|
|
518
556
|
var decodeBase64Json = (b64) => {
|
|
519
557
|
try {
|
|
520
558
|
return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
|
|
@@ -522,12 +560,34 @@ var decodeBase64Json = (b64) => {
|
|
|
522
560
|
return void 0;
|
|
523
561
|
}
|
|
524
562
|
};
|
|
525
|
-
var findingsPointer = (
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
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} -->`;
|
|
529
590
|
};
|
|
530
|
-
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
531
591
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
532
592
|
var parseReviewedSha = (body) => {
|
|
533
593
|
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
@@ -804,6 +864,13 @@ var CONVERGENCE_RE = /<!-- code-review:convergence;base64 ([A-Za-z0-9+/=]+) -->/
|
|
|
804
864
|
var convergenceMarker = (convergence) => `<!-- code-review:convergence;base64 ${Buffer.from(JSON.stringify(convergence), "utf-8").toString(
|
|
805
865
|
"base64"
|
|
806
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
|
+
};
|
|
807
874
|
var parseConvergenceMarker = (body) => {
|
|
808
875
|
const b64 = CONVERGENCE_RE.exec(body)?.[1];
|
|
809
876
|
return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
|
|
@@ -848,43 +915,18 @@ ${findings}` : void 0;
|
|
|
848
915
|
return [findingsBlock, reviewedSha, reviewedRoute, completedAncestor, convergence, rounds, signal].filter((m) => m !== void 0).join("\n\n");
|
|
849
916
|
};
|
|
850
917
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
851
|
-
var projectPatch = (patch) => {
|
|
918
|
+
var projectPatch = (patch, surface) => {
|
|
852
919
|
if (patch === void 0) return { kind: "none" };
|
|
853
|
-
const lowered = patchToSuggestion(patch);
|
|
920
|
+
const lowered = surface === "diff-anchored" ? patchToSuggestion(patch) : void 0;
|
|
854
921
|
return typeof lowered === "string" ? { kind: "suggestion", text: escapeFence(lowered) } : { kind: "patch", raw: escapeFence(patch) };
|
|
855
922
|
};
|
|
856
923
|
var formatConfidence = (n) => n.toFixed(2);
|
|
857
|
-
var reviewBodyPointer = (headSha, stickyUrl) => {
|
|
924
|
+
var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
858
925
|
const sha7 = headSha.slice(0, 7);
|
|
859
|
-
|
|
926
|
+
const summary = stickyUrl ? `the [summary comment](${stickyUrl})` : "the summary comment";
|
|
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}`;
|
|
860
929
|
};
|
|
861
|
-
var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
|
|
862
|
-
var errMsg = (e) => e instanceof Error ? e.message : String(e);
|
|
863
|
-
var annotationSafe = (msg) => msg.replaceAll(/[\r\n]+/g, " ");
|
|
864
|
-
var modelIdentity = (configuredModelId) => configuredModelId.replace(/\[[12]m\]$/i, "");
|
|
865
|
-
var clipText = (body, max) => {
|
|
866
|
-
if (body.length <= max) return body;
|
|
867
|
-
const cut = body.slice(0, max);
|
|
868
|
-
const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
869
|
-
return `${safe}
|
|
870
|
-
\u2026 [truncated]`;
|
|
871
|
-
};
|
|
872
|
-
var tryParseJson = (text) => {
|
|
873
|
-
try {
|
|
874
|
-
return { ok: true, value: JSON.parse(text) };
|
|
875
|
-
} catch {
|
|
876
|
-
return { ok: false };
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
var readFileOrNull = (path) => {
|
|
880
|
-
try {
|
|
881
|
-
return readFileSync(path, "utf-8");
|
|
882
|
-
} catch {
|
|
883
|
-
return null;
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
// src/transcript.ts
|
|
888
930
|
var numField = (rec, key2) => {
|
|
889
931
|
const v = rec[key2];
|
|
890
932
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -1122,6 +1164,8 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1122
1164
|
return [...byKey.values()];
|
|
1123
1165
|
};
|
|
1124
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";
|
|
1125
1169
|
var answeredNoteKey = (f) => f.code !== void 0 && f.code !== "" ? f.code : `title:${f.title}`;
|
|
1126
1170
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1127
1171
|
var applyAnswered = (findings, registry) => {
|
|
@@ -1135,8 +1179,7 @@ var applyAnswered = (findings, registry) => {
|
|
|
1135
1179
|
kept.push(f);
|
|
1136
1180
|
continue;
|
|
1137
1181
|
}
|
|
1138
|
-
|
|
1139
|
-
if (verbatim && f.severity !== "critical") {
|
|
1182
|
+
if (isAnsweredDrop(f, entry)) {
|
|
1140
1183
|
droppedByKey.set(answeredNoteKey(f), entry);
|
|
1141
1184
|
droppedCount += 1;
|
|
1142
1185
|
} else {
|
|
@@ -1189,26 +1232,47 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1189
1232
|
|
|
1190
1233
|
// src/render.ts
|
|
1191
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;
|
|
1192
1238
|
var sanitizeFinding = (f, answeredNotes) => {
|
|
1193
1239
|
const key2 = answeredNoteKey(f);
|
|
1194
1240
|
return {
|
|
1195
1241
|
...f,
|
|
1196
1242
|
title: escapePipes(f.title),
|
|
1197
1243
|
path: escapeCodeBackticks(f.path),
|
|
1198
|
-
|
|
1244
|
+
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code), codeKey: f.code } : {},
|
|
1245
|
+
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1246
|
+
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1199
1247
|
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1200
1248
|
};
|
|
1201
1249
|
};
|
|
1250
|
+
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1202
1251
|
var sanitizeSuppressedNit = (f) => ({
|
|
1203
1252
|
title: escapeCodeBackticks(f.title),
|
|
1204
1253
|
...f.code !== void 0 ? { code: escapeCodeBackticks(f.code) } : {},
|
|
1205
|
-
|
|
1254
|
+
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1255
|
+
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1206
1256
|
startLine: f.start_line,
|
|
1207
|
-
|
|
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)
|
|
1208
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());
|
|
1209
1271
|
var sanitizeSystemic = (s) => ({
|
|
1210
1272
|
...s,
|
|
1211
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) } : {},
|
|
1212
1276
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1213
1277
|
...s.finding_codes !== void 0 ? { finding_codes: s.finding_codes.map(escapeCodeBackticks) } : {}
|
|
1214
1278
|
});
|
|
@@ -1241,6 +1305,14 @@ var render = (input) => {
|
|
|
1241
1305
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1242
1306
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
1243
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
|
+
);
|
|
1244
1316
|
return eta.renderString(input.template, {
|
|
1245
1317
|
findings: input.findings,
|
|
1246
1318
|
envelope: input.envelope,
|
|
@@ -1262,17 +1334,20 @@ var render = (input) => {
|
|
|
1262
1334
|
severityCounts,
|
|
1263
1335
|
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
1264
1336
|
strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
|
|
1265
|
-
suppressedNits:
|
|
1337
|
+
suppressedNits: suppressedBudget.list,
|
|
1338
|
+
carriedDroppedNits: suppressedBudget.dropped,
|
|
1266
1339
|
nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
|
|
1267
1340
|
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
1268
1341
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
1269
1342
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
1343
|
+
unverifiedNoLogs: input.unverifiedNoLogs === true,
|
|
1270
1344
|
runUrl: input.runUrl ?? null,
|
|
1271
1345
|
jsonUrl: input.jsonUrl ?? null,
|
|
1272
|
-
// The
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
|
|
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),
|
|
1276
1351
|
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
1277
1352
|
metastasisNote: advisoryAllowed ? metastasisNote(trajectory) : "",
|
|
1278
1353
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
@@ -1365,7 +1440,7 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
|
|
|
1365
1440
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
1366
1441
|
eta.renderString(template, {
|
|
1367
1442
|
...f,
|
|
1368
|
-
patchProjection: projectPatch(f.patch),
|
|
1443
|
+
patchProjection: projectPatch(f.patch, "diff-anchored"),
|
|
1369
1444
|
severityEmoji,
|
|
1370
1445
|
formatConfidence,
|
|
1371
1446
|
modelsText,
|
|
@@ -1388,6 +1463,15 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1388
1463
|
};
|
|
1389
1464
|
const comments = inDiff.map((f) => {
|
|
1390
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;
|
|
1391
1475
|
const sameRootNote = noteFor(f, context.sameRootNotes);
|
|
1392
1476
|
const answeredNote2 = noteFor(f, context.answeredNotes);
|
|
1393
1477
|
const comment = {
|
|
@@ -1395,7 +1479,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1395
1479
|
line: f.end_line,
|
|
1396
1480
|
side: defaultSide(f.side),
|
|
1397
1481
|
body: renderCommentBody(
|
|
1398
|
-
|
|
1482
|
+
view,
|
|
1399
1483
|
eta,
|
|
1400
1484
|
inlineTemplate,
|
|
1401
1485
|
modelsText,
|
|
@@ -1972,7 +2056,7 @@ var classifyExecError = (err, stderr, timeoutMs) => {
|
|
|
1972
2056
|
return `no response within ${String(timeoutMs)}ms (killed a hung child)${stderrStr ? `: ${stderrStr}` : ""}`;
|
|
1973
2057
|
return stderrStr || errMsg(err);
|
|
1974
2058
|
};
|
|
1975
|
-
var execFileWithTimeout = (spec) => new Promise((
|
|
2059
|
+
var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
1976
2060
|
const child = execFile(
|
|
1977
2061
|
spec.command,
|
|
1978
2062
|
[...spec.args],
|
|
@@ -1988,7 +2072,7 @@ var execFileWithTimeout = (spec) => new Promise((resolve3, reject) => {
|
|
|
1988
2072
|
reject(
|
|
1989
2073
|
new Error(`${spec.label} failed: ${classifyExecError(err, stderr, spec.timeoutMs)}`)
|
|
1990
2074
|
);
|
|
1991
|
-
else
|
|
2075
|
+
else resolve4(stdout);
|
|
1992
2076
|
}
|
|
1993
2077
|
);
|
|
1994
2078
|
if (spec.stdin !== void 0) {
|
|
@@ -2007,6 +2091,90 @@ var runGhApi = (args, stdin, env) => execFileWithTimeout({
|
|
|
2007
2091
|
env,
|
|
2008
2092
|
stdin
|
|
2009
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
|
+
};
|
|
2010
2178
|
|
|
2011
2179
|
// src/pr.ts
|
|
2012
2180
|
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
@@ -2273,31 +2441,43 @@ var commentPayload = (c) => ({
|
|
|
2273
2441
|
...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
|
|
2274
2442
|
body: formatMarkdown(c.body)
|
|
2275
2443
|
});
|
|
2276
|
-
var postInlineReview = async (
|
|
2277
|
-
const pointer = reviewBodyPointer(
|
|
2444
|
+
var postInlineReview = async (pr, comments, inDiff, ghApi) => {
|
|
2445
|
+
const pointer = reviewBodyPointer(
|
|
2446
|
+
pr.headSha,
|
|
2447
|
+
pr.stickyUrl,
|
|
2448
|
+
pr.runUrl,
|
|
2449
|
+
(process.env["GITHUB_STEP_SUMMARY"] ?? "") !== ""
|
|
2450
|
+
);
|
|
2278
2451
|
const reviewBody = (withComments) => JSON.stringify({
|
|
2279
2452
|
body: pointer,
|
|
2280
|
-
commit_id: headSha,
|
|
2453
|
+
commit_id: pr.headSha,
|
|
2281
2454
|
event: "COMMENT",
|
|
2282
2455
|
comments: withComments ? comments.map(commentPayload) : []
|
|
2283
2456
|
});
|
|
2284
|
-
const reviewsEndpoint = [`repos/${repo}/pulls/${String(prNumber)}/reviews`, "--input", "-"];
|
|
2457
|
+
const reviewsEndpoint = [`repos/${pr.repo}/pulls/${String(pr.prNumber)}/reviews`, "--input", "-"];
|
|
2285
2458
|
try {
|
|
2286
2459
|
const stdout = await ghApi(reviewsEndpoint, reviewBody(true));
|
|
2287
2460
|
return { url: parseHtmlUrl(stdout), inlinePosted: comments.length, unposted: [] };
|
|
2288
2461
|
} catch (err) {
|
|
2289
2462
|
if (comments.length === 0) throw err;
|
|
2290
2463
|
process.stderr.write(
|
|
2291
|
-
`Warning: the batched inline review on PR #${String(prNumber)} was rejected (${errMsg(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
2464
|
+
`Warning: the batched inline review on PR #${String(pr.prNumber)} was rejected (${errMsg(err)}) \u2014 posting the review body-only, then each comment individually to keep the ones GitHub accepts (issue #57)
|
|
2292
2465
|
`
|
|
2293
2466
|
);
|
|
2294
2467
|
const url = parseHtmlUrl(await ghApi(reviewsEndpoint, reviewBody(false)));
|
|
2295
|
-
const commentsEndpoint = [
|
|
2468
|
+
const commentsEndpoint = [
|
|
2469
|
+
`repos/${pr.repo}/pulls/${String(pr.prNumber)}/comments`,
|
|
2470
|
+
"--input",
|
|
2471
|
+
"-"
|
|
2472
|
+
];
|
|
2296
2473
|
const unposted = [];
|
|
2297
2474
|
let inlinePosted = 0;
|
|
2298
2475
|
for (const [i, c] of comments.entries()) {
|
|
2299
2476
|
try {
|
|
2300
|
-
await ghApi(
|
|
2477
|
+
await ghApi(
|
|
2478
|
+
commentsEndpoint,
|
|
2479
|
+
JSON.stringify({ commit_id: pr.headSha, ...commentPayload(c) })
|
|
2480
|
+
);
|
|
2301
2481
|
inlinePosted += 1;
|
|
2302
2482
|
} catch (e) {
|
|
2303
2483
|
const finding = inDiff[i];
|
|
@@ -2351,6 +2531,18 @@ var postComment = async (repo, prNumber, body, ghApi) => {
|
|
|
2351
2531
|
);
|
|
2352
2532
|
return parseCommentRef(stdout);
|
|
2353
2533
|
};
|
|
2534
|
+
var appendRunSummary = (summaryPath, body) => {
|
|
2535
|
+
if (summaryPath === void 0 || summaryPath === "") return;
|
|
2536
|
+
const rendered = body();
|
|
2537
|
+
try {
|
|
2538
|
+
appendFileSync(summaryPath, `
|
|
2539
|
+
${rendered}
|
|
2540
|
+
`);
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
process.stderr.write(`Warning: could not write the run summary: ${errMsg(err)}
|
|
2543
|
+
`);
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2354
2546
|
var upsertSticky = async (repo, prNumber, existing, body, ghApi) => {
|
|
2355
2547
|
if (existing !== null) {
|
|
2356
2548
|
const patched = await patchComment(repo, existing.id, body, ghApi);
|
|
@@ -2388,6 +2580,7 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
|
|
|
2388
2580
|
"--input",
|
|
2389
2581
|
"-"
|
|
2390
2582
|
],
|
|
2583
|
+
// GitHub caps a dismissal message at 140 chars.
|
|
2391
2584
|
JSON.stringify({ message: "Superseded by a new review for an updated commit." })
|
|
2392
2585
|
);
|
|
2393
2586
|
} catch (err) {
|
|
@@ -2478,7 +2671,7 @@ var minimizeComments = async (prNumber, ids, ghApi) => {
|
|
|
2478
2671
|
);
|
|
2479
2672
|
}
|
|
2480
2673
|
};
|
|
2481
|
-
var post = async (input, ghApi = runGhApi) => {
|
|
2674
|
+
var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
2482
2675
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
2483
2676
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
2484
2677
|
if (resolution.kind === "none") {
|
|
@@ -2511,14 +2704,20 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
2511
2704
|
...doc,
|
|
2512
2705
|
convergence: conv ?? void 0
|
|
2513
2706
|
});
|
|
2707
|
+
const carriedFindingsLink = !input.jsonUrl && existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
2708
|
+
let warnedNoJsonUrl = false;
|
|
2514
2709
|
const findingsBlob = (doc) => {
|
|
2515
|
-
|
|
2516
|
-
if (
|
|
2517
|
-
return
|
|
2710
|
+
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
2711
|
+
if (carriedFindingsLink !== null) {
|
|
2712
|
+
return findingsMarkerPair(carriedFindingsLink, doc.convergence);
|
|
2518
2713
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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);
|
|
2522
2721
|
};
|
|
2523
2722
|
const leaveInPlace = (message) => {
|
|
2524
2723
|
process.stderr.write(
|
|
@@ -2545,7 +2744,7 @@ ${conv}`;
|
|
|
2545
2744
|
noticeBody(
|
|
2546
2745
|
`${DEFAULT_MARKER}
|
|
2547
2746
|
|
|
2548
|
-
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}
|
|
2747
|
+
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}\`${input.unverifiedNoLogs === true ? ' \u2014 **but no failing-job logs were available**, so it had only the diff to work from and "no findings" is not evidence of none' : ""} \u2014 the completed full review of \`${priorSha ? priorSha.slice(0, 7) : "an earlier commit"}\` is preserved below.${dropNote ? `
|
|
2549
2748
|
|
|
2550
2749
|
${dropNote}` : ""}`,
|
|
2551
2750
|
sticky.body
|
|
@@ -2560,7 +2759,7 @@ ${dropNote}` : ""}`,
|
|
|
2560
2759
|
throw new Error(`Price map at ${input.pricesPath} does not match the expected shape`);
|
|
2561
2760
|
}
|
|
2562
2761
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
2563
|
-
const
|
|
2762
|
+
const inlineRequested = input.inline === true;
|
|
2564
2763
|
const renderNotice = (message) => {
|
|
2565
2764
|
const findings2 = stampConvergence(incompleteFindings(`### \u26A0\uFE0F ${message}`), priorConv);
|
|
2566
2765
|
return formatMarkdown(
|
|
@@ -2629,8 +2828,12 @@ ${dropNote}` : ""}`,
|
|
|
2629
2828
|
findings: [...answeredFilter.findings],
|
|
2630
2829
|
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
2631
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;
|
|
2632
2833
|
const priorSuppressedKeys = new Set(
|
|
2633
|
-
|
|
2834
|
+
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2835
|
+
(n) => answeredNoteKey({ code: n.code, title: n.title })
|
|
2836
|
+
)
|
|
2634
2837
|
);
|
|
2635
2838
|
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || priorSuppressedKeys.has(answeredNoteKey(f)));
|
|
2636
2839
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
@@ -2677,19 +2880,27 @@ ${dropNote}` : ""}`,
|
|
|
2677
2880
|
convergenceRound: false,
|
|
2678
2881
|
testReport,
|
|
2679
2882
|
clocDiff,
|
|
2680
|
-
inlineDisposition: { kind: "no-envelope" },
|
|
2883
|
+
inlineDisposition: inlineRequested ? { kind: "no-envelope" } : { kind: "disabled" },
|
|
2681
2884
|
runUrl: input.runUrl,
|
|
2885
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
2682
2886
|
jsonUrl: input.jsonUrl,
|
|
2683
2887
|
findingsPointer: findingsBlob(stampedFindings2),
|
|
2684
2888
|
postedAt: input.postedAt
|
|
2685
2889
|
})
|
|
2686
2890
|
);
|
|
2687
2891
|
await upsertSticky(input.repo, prNumber, existingSticky, body, ghApi);
|
|
2892
|
+
appendRunSummary(process.env["GITHUB_STEP_SUMMARY"], () => body);
|
|
2893
|
+
if (inlineRequested) {
|
|
2894
|
+
process.stderr.write(
|
|
2895
|
+
"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"
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2688
2898
|
process.stderr.write(
|
|
2689
|
-
"Result envelope missing or malformed \u2014 posted sticky summary without usage/cost data
|
|
2899
|
+
"Result envelope missing or malformed \u2014 posted sticky summary without usage/cost data\n"
|
|
2690
2900
|
);
|
|
2691
2901
|
process.exit(0);
|
|
2692
2902
|
}
|
|
2903
|
+
const inlineTemplate = inlineRequested ? readFileSync(input.inlineTemplatePath, "utf-8") : "";
|
|
2693
2904
|
const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
|
|
2694
2905
|
if (wouldBuryCompleted(thisIncomplete)) {
|
|
2695
2906
|
logAnsweredDrops();
|
|
@@ -2703,14 +2914,14 @@ ${dropNote}` : ""}`,
|
|
|
2703
2914
|
comments: rawComments,
|
|
2704
2915
|
strays,
|
|
2705
2916
|
inDiff
|
|
2706
|
-
} = buildInlineComments(visibleFindings, diff, {
|
|
2917
|
+
} = inlineRequested ? buildInlineComments(visibleFindings, diff, {
|
|
2707
2918
|
inlineTemplate,
|
|
2708
2919
|
models: envelope.models.map((m) => m.model),
|
|
2709
2920
|
findings,
|
|
2710
2921
|
jsonUrl: input.jsonUrl,
|
|
2711
2922
|
sameRootNotes,
|
|
2712
2923
|
answeredNotes: reRaisedNotes
|
|
2713
|
-
});
|
|
2924
|
+
}) : { comments: [], strays: visibleFindings, inDiff: [] };
|
|
2714
2925
|
const { comments, longFiles } = checkLongSuggestions(rawComments);
|
|
2715
2926
|
for (const wf of longFiles) {
|
|
2716
2927
|
process.stderr.write(
|
|
@@ -2719,7 +2930,7 @@ ${dropNote}` : ""}`,
|
|
|
2719
2930
|
);
|
|
2720
2931
|
}
|
|
2721
2932
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
2722
|
-
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2933
|
+
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2723
2934
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
2724
2935
|
const currentCodes = computeCodeCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2725
2936
|
const roundNumber = priorRoundCount + 1;
|
|
@@ -2734,16 +2945,6 @@ ${dropNote}` : ""}`,
|
|
|
2734
2945
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
2735
2946
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
2736
2947
|
const findingsMarker = findingsBlob(stampedFindings);
|
|
2737
|
-
const markerForm = findingsMarkerForm(stampedFindings, input.jsonUrl);
|
|
2738
|
-
if (markerForm === "link") {
|
|
2739
|
-
process.stderr.write(
|
|
2740
|
-
"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"
|
|
2741
|
-
);
|
|
2742
|
-
} else if (markerForm === "omitted") {
|
|
2743
|
-
process.stderr.write(
|
|
2744
|
-
"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"
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
2948
|
const commonRenderInput = {
|
|
2748
2949
|
findings: stampedFindings,
|
|
2749
2950
|
envelope,
|
|
@@ -2767,6 +2968,7 @@ ${dropNote}` : ""}`,
|
|
|
2767
2968
|
strays,
|
|
2768
2969
|
suppressedNits,
|
|
2769
2970
|
runUrl: input.runUrl,
|
|
2971
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
2770
2972
|
jsonUrl: input.jsonUrl,
|
|
2771
2973
|
findingsPointer: findingsMarker,
|
|
2772
2974
|
postedAt: input.postedAt,
|
|
@@ -2787,6 +2989,11 @@ ${dropNote}` : ""}`,
|
|
|
2787
2989
|
reviewUrl: reviewUrl2
|
|
2788
2990
|
}) + longFilesNote
|
|
2789
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
|
+
}
|
|
2790
2997
|
const stickyRef = await upsertSticky(
|
|
2791
2998
|
input.repo,
|
|
2792
2999
|
prNumber,
|
|
@@ -2805,16 +3012,20 @@ ${dropNote}` : ""}`,
|
|
|
2805
3012
|
inlinePosted,
|
|
2806
3013
|
unposted
|
|
2807
3014
|
} = await postInlineReview(
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
3015
|
+
{
|
|
3016
|
+
repo: input.repo,
|
|
3017
|
+
prNumber,
|
|
3018
|
+
headSha: input.headSha,
|
|
3019
|
+
stickyUrl: stickyRef?.url,
|
|
3020
|
+
runUrl: input.runUrl
|
|
3021
|
+
},
|
|
2811
3022
|
comments,
|
|
2812
3023
|
inDiff,
|
|
2813
|
-
stickyRef?.url,
|
|
2814
3024
|
ghApi
|
|
2815
3025
|
);
|
|
2816
3026
|
process.stderr.write(
|
|
2817
|
-
`Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
3027
|
+
inlineRequested ? `Posted a review with ${String(inlinePosted)} inline comment(s) on PR #${String(prNumber)}
|
|
3028
|
+
` : `Posted a body-only review on PR #${String(prNumber)}; the findings are in the sticky
|
|
2818
3029
|
`
|
|
2819
3030
|
);
|
|
2820
3031
|
const priorReviewIds = botReviews.map((r) => r.id);
|
|
@@ -2844,6 +3055,14 @@ ${dropNote}` : ""}`,
|
|
|
2844
3055
|
);
|
|
2845
3056
|
}
|
|
2846
3057
|
}
|
|
3058
|
+
appendRunSummary(
|
|
3059
|
+
process.env["GITHUB_STEP_SUMMARY"],
|
|
3060
|
+
() => renderBody(
|
|
3061
|
+
{ kind: "whole-document", inlineCount: inlinePosted, rejectedCount: unanchoredCount },
|
|
3062
|
+
reviewUrl,
|
|
3063
|
+
visibleFindings
|
|
3064
|
+
)
|
|
3065
|
+
);
|
|
2847
3066
|
};
|
|
2848
3067
|
var noticeBody = (lead, existingBody) => {
|
|
2849
3068
|
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
@@ -3183,10 +3402,10 @@ var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi,
|
|
|
3183
3402
|
}
|
|
3184
3403
|
};
|
|
3185
3404
|
const poll = async (lastSeenNames, lastRunId) => {
|
|
3186
|
-
const { run, seenNames } = await safeResolve();
|
|
3187
|
-
if (
|
|
3188
|
-
return { kind: "concluded", conclusion:
|
|
3189
|
-
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;
|
|
3190
3409
|
const names = seenNames.length > 0 ? seenNames : lastSeenNames;
|
|
3191
3410
|
if (deps.elapsedMs() >= options.timeoutMs)
|
|
3192
3411
|
return { kind: "timed-out", runId, seenNames: names };
|
|
@@ -3216,6 +3435,8 @@ conclusion=${result.conclusion}
|
|
|
3216
3435
|
diff_size=${String(result.diffSize)}
|
|
3217
3436
|
stacked=${String(result.stacked)}
|
|
3218
3437
|
base_sha=${result.baseSha}
|
|
3438
|
+
staged_job_logs=${String(result.stagedJobLogs)}
|
|
3439
|
+
failing_jobs=${String(result.failingJobs)}
|
|
3219
3440
|
`;
|
|
3220
3441
|
}
|
|
3221
3442
|
};
|
|
@@ -3244,7 +3465,7 @@ var IssueCommentCodec = t.intersection([
|
|
|
3244
3465
|
})
|
|
3245
3466
|
]);
|
|
3246
3467
|
var JobCodec = t.type({ id: t.number, conclusion: t.union([t.string, t.null]) });
|
|
3247
|
-
var
|
|
3468
|
+
var JOBS_JQ = ".jobs[] | {id: .id, conclusion: .conclusion}";
|
|
3248
3469
|
var fetchPrMeta = async (repo, prNumber, ghApi) => {
|
|
3249
3470
|
const stdout = await ghApi([
|
|
3250
3471
|
`repos/${repo}/pulls/${String(prNumber)}`,
|
|
@@ -3355,8 +3576,7 @@ var priorReviewFrom = (comments, botLogin) => {
|
|
|
3355
3576
|
return last ? { id: last.id, body: last.body } : null;
|
|
3356
3577
|
};
|
|
3357
3578
|
var MAX_CONVERSATION_COMMENTS = 50;
|
|
3358
|
-
var
|
|
3359
|
-
var clip = (body) => clipText(body, MAX_CONVERSATION_BODY_CHARS);
|
|
3579
|
+
var clip = (body) => clipText(body, BODY_CLIP_CHARS);
|
|
3360
3580
|
var boundedHuman = (items, botLogin, label, project) => {
|
|
3361
3581
|
const human = items.filter(
|
|
3362
3582
|
(a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
|
|
@@ -3391,16 +3611,23 @@ var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review
|
|
|
3391
3611
|
state: r.state ?? null,
|
|
3392
3612
|
body: clip(r.body)
|
|
3393
3613
|
}));
|
|
3614
|
+
var MAX_STAGED_JOB_LOGS = 20;
|
|
3394
3615
|
var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3616
|
+
const rows = parseJsonl(
|
|
3617
|
+
await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`, "--paginate", "--jq", JOBS_JQ])
|
|
3618
|
+
);
|
|
3619
|
+
const decoded = t.array(JobCodec).decode(rows);
|
|
3397
3620
|
if (decoded._tag === "Left") {
|
|
3398
3621
|
throw new Error(`Jobs list for run ${runId} did not match the expected shape`);
|
|
3399
3622
|
}
|
|
3400
|
-
|
|
3623
|
+
const failing = decoded.right.filter((j) => j.conclusion === "failure");
|
|
3624
|
+
const selected = failing.slice(0, MAX_STAGED_JOB_LOGS);
|
|
3625
|
+
let staged = 0;
|
|
3626
|
+
for (const job of selected) {
|
|
3401
3627
|
try {
|
|
3402
3628
|
const log = await ghApi([`repos/${repo}/actions/jobs/${String(job.id)}/logs`]);
|
|
3403
3629
|
writeFileSync(join(outDir, `job_${String(job.id)}.log`), log);
|
|
3630
|
+
staged += 1;
|
|
3404
3631
|
} catch (err) {
|
|
3405
3632
|
process.stderr.write(
|
|
3406
3633
|
`Warning: failed to download logs for job ${String(job.id)}: ${errMsg(err)} \u2014 continuing with the logs retrieved so far
|
|
@@ -3408,8 +3635,21 @@ var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
|
|
|
3408
3635
|
);
|
|
3409
3636
|
}
|
|
3410
3637
|
}
|
|
3638
|
+
if (failing.length > selected.length) {
|
|
3639
|
+
process.stderr.write(
|
|
3640
|
+
`::warning::${annotationSafe(`${String(failing.length)} failing job(s) in run ${runId}; staged ${String(staged)} of the first ${String(selected.length)} log(s) \u2014 the review does not see the rest`)}
|
|
3641
|
+
`
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
if (staged === 0) {
|
|
3645
|
+
process.stderr.write(
|
|
3646
|
+
`::warning::${annotationSafe(`No failing-job logs could be staged for run ${runId} (${String(failing.length)} failing job(s) reported) \u2014 the review has only the diff to work from`)}
|
|
3647
|
+
`
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
return { staged, failing: failing.length };
|
|
3411
3651
|
};
|
|
3412
|
-
var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
3652
|
+
var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghArtifactReader) => {
|
|
3413
3653
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
3414
3654
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
3415
3655
|
if (resolution.kind === "none") {
|
|
@@ -3464,6 +3704,13 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
3464
3704
|
join(input.outDir, "prior_review.json"),
|
|
3465
3705
|
prior === null ? "null" : JSON.stringify(prior)
|
|
3466
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
|
+
);
|
|
3467
3714
|
const answered = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
|
|
3468
3715
|
writeFileSync(
|
|
3469
3716
|
join(input.outDir, "answered.json"),
|
|
@@ -3477,16 +3724,16 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
|
|
|
3477
3724
|
reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
|
|
3478
3725
|
})
|
|
3479
3726
|
);
|
|
3480
|
-
|
|
3481
|
-
await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi);
|
|
3482
|
-
}
|
|
3727
|
+
const jobLogs = input.conclusion === "failure" ? await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi) : { staged: 0, failing: 0 };
|
|
3483
3728
|
return {
|
|
3484
3729
|
kind: "gathered",
|
|
3485
3730
|
pr: prNumber,
|
|
3486
3731
|
conclusion: input.conclusion,
|
|
3487
3732
|
diffSize: Buffer.byteLength(prDiff, "utf8"),
|
|
3488
3733
|
stacked,
|
|
3489
|
-
baseSha: meta.base_sha
|
|
3734
|
+
baseSha: meta.base_sha,
|
|
3735
|
+
stagedJobLogs: jobLogs.staged,
|
|
3736
|
+
failingJobs: jobLogs.failing
|
|
3490
3737
|
};
|
|
3491
3738
|
};
|
|
3492
3739
|
|
|
@@ -4046,6 +4293,9 @@ var renderCmd = defineCommand({
|
|
|
4046
4293
|
pricedAt: /* @__PURE__ */ new Date()
|
|
4047
4294
|
});
|
|
4048
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
|
+
);
|
|
4049
4299
|
}
|
|
4050
4300
|
});
|
|
4051
4301
|
var inlineCmd = defineCommand({
|
|
@@ -4513,6 +4763,10 @@ var seedDraftCmd = defineCommand({
|
|
|
4513
4763
|
type: "string",
|
|
4514
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"
|
|
4515
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
|
+
},
|
|
4516
4770
|
"prior-answers": {
|
|
4517
4771
|
type: "string",
|
|
4518
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"
|
|
@@ -4580,20 +4834,20 @@ var seedDraftCmd = defineCommand({
|
|
|
4580
4834
|
})();
|
|
4581
4835
|
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
4582
4836
|
})();
|
|
4583
|
-
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);
|
|
4584
4846
|
const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
|
|
4585
4847
|
isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
|
|
4586
4848
|
);
|
|
4587
|
-
const
|
|
4588
|
-
if (
|
|
4589
|
-
return strippedPrior;
|
|
4590
|
-
const carried = strippedPrior["scope_metastasis"];
|
|
4591
|
-
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return strippedPrior;
|
|
4592
|
-
if (strippedPrior["verdict"] === "error") return strippedPrior;
|
|
4593
|
-
const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
|
|
4594
|
-
return computed === null ? strippedPrior : { ...strippedPrior, scope_metastasis: computed };
|
|
4595
|
-
})();
|
|
4596
|
-
if (args["prior-answers"]) {
|
|
4849
|
+
const answeredRegistry = (() => {
|
|
4850
|
+
if (!args["prior-answers"]) return null;
|
|
4597
4851
|
try {
|
|
4598
4852
|
const raw = JSON.parse(readFileSync(resolve$1(args["prior-answers"]), "utf-8"));
|
|
4599
4853
|
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
@@ -4607,18 +4861,38 @@ var seedDraftCmd = defineCommand({
|
|
|
4607
4861
|
`
|
|
4608
4862
|
);
|
|
4609
4863
|
}
|
|
4610
|
-
|
|
4611
|
-
`);
|
|
4612
|
-
process.stderr.write(
|
|
4613
|
-
`Seeded ${priorAnswersPath(outPath)} with ${String(decoded.length)} answered finding(s) as context
|
|
4614
|
-
`
|
|
4615
|
-
);
|
|
4864
|
+
return decoded;
|
|
4616
4865
|
} catch (err) {
|
|
4617
4866
|
process.stderr.write(
|
|
4618
4867
|
`Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
|
|
4619
4868
|
`
|
|
4620
4869
|
);
|
|
4870
|
+
return null;
|
|
4621
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
|
+
);
|
|
4622
4896
|
}
|
|
4623
4897
|
if (parsedPrior !== null && parseReviewedRoute(priorBody ?? "") === "full review") {
|
|
4624
4898
|
try {
|
|
@@ -5146,7 +5420,7 @@ var postCmd = defineCommand({
|
|
|
5146
5420
|
},
|
|
5147
5421
|
"run-url": {
|
|
5148
5422
|
type: "string",
|
|
5149
|
-
description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside"
|
|
5423
|
+
description: "Workflow run URL (transcript/traces), rendered as a link in the LLM Disclosure aside and in the review-object body"
|
|
5150
5424
|
},
|
|
5151
5425
|
"json-url": {
|
|
5152
5426
|
type: "string",
|
|
@@ -5159,6 +5433,14 @@ var postCmd = defineCommand({
|
|
|
5159
5433
|
"nit-visibility-floor": {
|
|
5160
5434
|
type: "string",
|
|
5161
5435
|
description: NIT_VISIBILITY_FLOOR_DESCRIPTION
|
|
5436
|
+
},
|
|
5437
|
+
inline: {
|
|
5438
|
+
type: "boolean",
|
|
5439
|
+
description: "Also render findings as inline review comments on the diff. Off by default: an inline thread cannot be revised by a later round, so stale threads accumulate. The review object is posted either way; with this off the sticky lists the findings instead"
|
|
5440
|
+
},
|
|
5441
|
+
"unverified-no-logs": {
|
|
5442
|
+
type: "boolean",
|
|
5443
|
+
description: "Mark the review unverified: the fast-fix route ran with no failing-job logs staged, so its findings came from the diff alone. The caller decides this \u2014 the logs are staged in the review job, not here"
|
|
5162
5444
|
}
|
|
5163
5445
|
},
|
|
5164
5446
|
run: async ({ args }) => {
|
|
@@ -5182,6 +5464,8 @@ var postCmd = defineCommand({
|
|
|
5182
5464
|
jsonUrl: args["json-url"],
|
|
5183
5465
|
convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
|
|
5184
5466
|
nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
|
|
5467
|
+
inline: args.inline,
|
|
5468
|
+
unverifiedNoLogs: args["unverified-no-logs"],
|
|
5185
5469
|
postedAt: formatUtc(/* @__PURE__ */ new Date()),
|
|
5186
5470
|
pricedAt: /* @__PURE__ */ new Date()
|
|
5187
5471
|
});
|