@jphutchins/code-review 0.1.0-alpha.39 → 0.1.0-alpha.40
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 +44 -1
- package/dist/index.js +660 -229
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +21 -1
- package/schema/findings.schema.json +58 -1
- package/templates/comment.eta +32 -1
- package/templates/inline.eta +4 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import { readFileSync, writeFileSync,
|
|
3
|
+
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync } from 'fs';
|
|
4
4
|
import { randomBytes } from 'crypto';
|
|
5
5
|
import { resolve as resolve$1, join, dirname, basename, extname } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
@@ -37,6 +37,15 @@ var SchemaVersion = t.refinement(
|
|
|
37
37
|
(s) => SCHEMA_VERSION_RE.test(s),
|
|
38
38
|
"SchemaVersion"
|
|
39
39
|
);
|
|
40
|
+
var UriString = t.refinement(
|
|
41
|
+
t.string,
|
|
42
|
+
(s) => !/\s/.test(s) && URL.canParse(s),
|
|
43
|
+
"UriString"
|
|
44
|
+
);
|
|
45
|
+
var FindingRuleCodec = t.partial({
|
|
46
|
+
code: t.string,
|
|
47
|
+
code_url: UriString
|
|
48
|
+
});
|
|
40
49
|
var FindingShape = t.intersection([
|
|
41
50
|
t.type({
|
|
42
51
|
path: t.string,
|
|
@@ -48,10 +57,9 @@ var FindingShape = t.intersection([
|
|
|
48
57
|
reasoning: t.string,
|
|
49
58
|
confidence: Confidence
|
|
50
59
|
}),
|
|
60
|
+
FindingRuleCodec,
|
|
51
61
|
t.partial({
|
|
52
62
|
side: SideCodec,
|
|
53
|
-
code: t.string,
|
|
54
|
-
code_url: t.string,
|
|
55
63
|
recommendation: t.string,
|
|
56
64
|
patch: t.string
|
|
57
65
|
})
|
|
@@ -62,13 +70,41 @@ var EndGeStart = t.refinement(
|
|
|
62
70
|
"EndGeStart"
|
|
63
71
|
);
|
|
64
72
|
var FindingCodec = t.exact(EndGeStart);
|
|
73
|
+
var SystemicRequired = t.type({
|
|
74
|
+
title: t.string,
|
|
75
|
+
description: t.string,
|
|
76
|
+
severity: SeverityCodec,
|
|
77
|
+
reasoning: t.string,
|
|
78
|
+
confidence: Confidence
|
|
79
|
+
});
|
|
80
|
+
var SystemicOptional = t.partial({
|
|
81
|
+
finding_codes: t.array(t.string),
|
|
82
|
+
paths: t.array(t.string)
|
|
83
|
+
});
|
|
84
|
+
var SystemicProblemShape = t.intersection([SystemicRequired, FindingRuleCodec, SystemicOptional]);
|
|
85
|
+
var SYSTEMIC_KEYS = /* @__PURE__ */ new Set([
|
|
86
|
+
...Object.keys(SystemicRequired.props),
|
|
87
|
+
...Object.keys(FindingRuleCodec.props),
|
|
88
|
+
...Object.keys(SystemicOptional.props)
|
|
89
|
+
]);
|
|
90
|
+
var SystemicProblemStrict = t.refinement(
|
|
91
|
+
SystemicProblemShape,
|
|
92
|
+
(s) => Object.keys(s).every((k) => SYSTEMIC_KEYS.has(k)),
|
|
93
|
+
"SystemicProblemStrict"
|
|
94
|
+
);
|
|
95
|
+
var SystemicProblemCodec = t.exact(SystemicProblemStrict);
|
|
65
96
|
var FindingsCodec = t.exact(
|
|
66
|
-
t.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
t.intersection([
|
|
98
|
+
t.type({
|
|
99
|
+
schema_version: SchemaVersion,
|
|
100
|
+
summary: t.string,
|
|
101
|
+
verdict: VerdictCodec,
|
|
102
|
+
findings: t.array(FindingCodec)
|
|
103
|
+
}),
|
|
104
|
+
t.partial({
|
|
105
|
+
systemic_problems: t.array(SystemicProblemCodec)
|
|
106
|
+
})
|
|
107
|
+
])
|
|
72
108
|
);
|
|
73
109
|
var TriageCodec = t.type({
|
|
74
110
|
safe: t.boolean,
|
|
@@ -134,13 +170,7 @@ var TestSummaryCodec = t.intersection([
|
|
|
134
170
|
failures: t.array(TestFailureCodec)
|
|
135
171
|
})
|
|
136
172
|
]);
|
|
137
|
-
var DEFAULT_SCHEMA_VERSION = "0.
|
|
138
|
-
var emptyFindings = (summary) => ({
|
|
139
|
-
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
140
|
-
summary,
|
|
141
|
-
verdict: "comment",
|
|
142
|
-
findings: []
|
|
143
|
-
});
|
|
173
|
+
var DEFAULT_SCHEMA_VERSION = "0.6.0";
|
|
144
174
|
var incompleteFindings = (summary) => ({
|
|
145
175
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
146
176
|
summary,
|
|
@@ -308,8 +338,9 @@ var severityEmoji = (s) => {
|
|
|
308
338
|
return "\u2753";
|
|
309
339
|
}
|
|
310
340
|
};
|
|
311
|
-
var EMBED_LIMIT =
|
|
341
|
+
var EMBED_LIMIT = 40200;
|
|
312
342
|
var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
|
|
343
|
+
var b64LengthOf = (document) => Buffer.from(JSON.stringify(document), "utf-8").toString("base64").length;
|
|
313
344
|
var encodeMarker = (document, jsonUrl, limit) => {
|
|
314
345
|
const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
|
|
315
346
|
const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
|
|
@@ -324,12 +355,24 @@ var decodeBase64Json = (b64) => {
|
|
|
324
355
|
}
|
|
325
356
|
};
|
|
326
357
|
var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
|
|
358
|
+
var findingsMarkerForm = (findings, jsonUrl, limit = EMBED_LIMIT) => {
|
|
359
|
+
if (b64LengthOf(findings) <= limit) return "embedded";
|
|
360
|
+
return jsonUrl ? "link" : "omitted";
|
|
361
|
+
};
|
|
327
362
|
var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
|
|
328
363
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
329
364
|
var parseReviewedSha = (body) => {
|
|
330
365
|
const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
|
|
331
366
|
return sha && sha !== ZERO_SHA ? sha : null;
|
|
332
367
|
};
|
|
368
|
+
var ROUTE_RE = /<!-- reviewed-route: ([^>]*) -->/;
|
|
369
|
+
var parseReviewedRoute = (body) => ROUTE_RE.exec(body)?.[1] || null;
|
|
370
|
+
var isFullReviewSticky = (body) => {
|
|
371
|
+
const route = parseReviewedRoute(body);
|
|
372
|
+
return route === "full review" || route !== "mechanic" && parseRounds(body).length > 0;
|
|
373
|
+
};
|
|
374
|
+
var COMPLETED_ANCESTOR_MARKER = "<!-- review-complete-ancestor -->";
|
|
375
|
+
var parseCompletedAncestor = (body) => body.includes(COMPLETED_ANCESTOR_MARKER);
|
|
333
376
|
var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
|
|
334
377
|
var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
|
|
335
378
|
var parseFindingsMarker = (body) => {
|
|
@@ -343,38 +386,233 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
|
|
|
343
386
|
const v = u[k];
|
|
344
387
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
345
388
|
});
|
|
389
|
+
var MAX_CODES_PER_ROUND = 8;
|
|
390
|
+
var hasCode = (codes, code) => codes !== void 0 && Object.prototype.hasOwnProperty.call(codes, code);
|
|
391
|
+
var escapeCodeBackticks = (code) => code.replace(/`/g, "-").replace(/\r?\n/g, " ");
|
|
392
|
+
var normalizeCodeCounts = (codes, priorCodes) => {
|
|
393
|
+
if (typeof codes !== "object" || codes === null || Array.isArray(codes)) return void 0;
|
|
394
|
+
const entries = Object.entries(codes).filter(
|
|
395
|
+
(e) => typeof e[1] === "number" && Number.isSafeInteger(e[1]) && e[1] > 0
|
|
396
|
+
).sort((a, b) => {
|
|
397
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
398
|
+
const aPrior = hasCode(priorCodes, a[0]) ? 1 : 0;
|
|
399
|
+
const bPrior = hasCode(priorCodes, b[0]) ? 1 : 0;
|
|
400
|
+
if (aPrior !== bPrior) return bPrior - aPrior;
|
|
401
|
+
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
402
|
+
});
|
|
403
|
+
if (entries.length === 0) return void 0;
|
|
404
|
+
const sorted = entries.sort((a, b) => {
|
|
405
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
406
|
+
const aPrior = hasCode(priorCodes, a[0]) ? 1 : 0;
|
|
407
|
+
const bPrior = hasCode(priorCodes, b[0]) ? 1 : 0;
|
|
408
|
+
if (aPrior !== bPrior) return bPrior - aPrior;
|
|
409
|
+
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
410
|
+
});
|
|
411
|
+
const base = sorted.slice(0, MAX_CODES_PER_ROUND);
|
|
412
|
+
const priorKept = sorted.slice(MAX_CODES_PER_ROUND).filter(([code]) => hasCode(priorCodes, code)).slice(0, MAX_CODES_PER_ROUND);
|
|
413
|
+
return Object.fromEntries([...base, ...priorKept]);
|
|
414
|
+
};
|
|
346
415
|
var parseRounds = (body) => {
|
|
347
416
|
const b64 = ROUNDS_RE.exec(body)?.[1];
|
|
348
417
|
if (b64 === void 0) return [];
|
|
349
418
|
const decoded = decodeBase64Json(b64);
|
|
350
|
-
|
|
419
|
+
if (!Array.isArray(decoded)) return [];
|
|
420
|
+
return decoded.filter(isSeverityCounts).map((u) => {
|
|
421
|
+
const rec = u;
|
|
422
|
+
const codes = normalizeCodeCounts(rec["codes"]);
|
|
423
|
+
const sha = rec["sha"];
|
|
424
|
+
const shaStr = typeof sha === "string" && sha !== "" ? sha : void 0;
|
|
425
|
+
const round = rec["round"];
|
|
426
|
+
const roundNum = typeof round === "number" && Number.isSafeInteger(round) && round >= 1 ? round : void 0;
|
|
427
|
+
const base = codes === void 0 ? { critical: u.critical, major: u.major, minor: u.minor, nit: u.nit } : { critical: u.critical, major: u.major, minor: u.minor, nit: u.nit, codes };
|
|
428
|
+
const kept = shaStr === void 0 ? base : { ...base, sha: shaStr };
|
|
429
|
+
return roundNum === void 0 ? kept : { ...kept, round: roundNum };
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
var ROUNDS_MARKER_LIMIT = 8e3;
|
|
433
|
+
var roundsMarker = (rounds) => {
|
|
434
|
+
if (rounds.length === 0) return "";
|
|
435
|
+
const serialize = (kept2) => `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(kept2), "utf-8").toString("base64")} -->`;
|
|
436
|
+
const stripCodes = (n) => rounds.map(
|
|
437
|
+
(r, i) => i < n ? { critical: r.critical, major: r.major, minor: r.minor, nit: r.nit } : r
|
|
438
|
+
);
|
|
439
|
+
let stripped = 0;
|
|
440
|
+
while (stripped < rounds.length && serialize(stripCodes(stripped)).length > ROUNDS_MARKER_LIMIT) {
|
|
441
|
+
stripped += 1;
|
|
442
|
+
}
|
|
443
|
+
const kept = stripCodes(stripped);
|
|
444
|
+
const bounded = serialize(kept).length > ROUNDS_MARKER_LIMIT ? kept.slice(-8) : kept;
|
|
445
|
+
return serialize(bounded);
|
|
351
446
|
};
|
|
352
|
-
var roundsMarker = (rounds) => rounds.length === 0 ? "" : `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(rounds), "utf-8").toString("base64")} -->`;
|
|
353
447
|
var roundChip = (c) => {
|
|
354
448
|
const parts = SEVERITIES.filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
|
|
355
449
|
return parts.length === 0 ? "clean" : parts.join(" ");
|
|
356
450
|
};
|
|
357
451
|
var TRAJECTORY_CHIPS = 8;
|
|
358
|
-
var roundsSummary = (rounds) => {
|
|
359
|
-
if (
|
|
452
|
+
var roundsSummary = (rounds, count = rounds.length) => {
|
|
453
|
+
if (count === 0) return "";
|
|
360
454
|
const chips = rounds.slice(-TRAJECTORY_CHIPS).map(roundChip);
|
|
361
|
-
const trajectory = rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
|
|
362
|
-
return `**Round ${String(
|
|
455
|
+
const trajectory = chips.length === 0 ? "" : rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
|
|
456
|
+
return trajectory === "" ? `**Round ${String(count)}**` : `**Round ${String(count)}** \xB7 ${trajectory}`;
|
|
457
|
+
};
|
|
458
|
+
var computeCodeCounts = (findings, systemic = []) => {
|
|
459
|
+
const counts = /* @__PURE__ */ new Map();
|
|
460
|
+
for (const code of [
|
|
461
|
+
...findings.map((f) => f.code),
|
|
462
|
+
...systemic.flatMap((s) => [s.code, ...s.finding_codes ?? []])
|
|
463
|
+
]) {
|
|
464
|
+
if (code === void 0 || code === "") continue;
|
|
465
|
+
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
466
|
+
}
|
|
467
|
+
return Object.fromEntries(counts);
|
|
468
|
+
};
|
|
469
|
+
var roundRecord = (counts, codes, priorCodes, sha, round) => {
|
|
470
|
+
const normalized = normalizeCodeCounts(codes, priorCodes);
|
|
471
|
+
const record3 = normalized === void 0 ? { ...counts } : { ...counts, codes: normalized };
|
|
472
|
+
return {
|
|
473
|
+
...record3,
|
|
474
|
+
...sha !== void 0 ? { sha } : {},
|
|
475
|
+
...round !== void 0 ? { round } : {}
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
var consecutiveCodeStreaks = (rounds) => {
|
|
479
|
+
const entries = [];
|
|
480
|
+
if (rounds.length === 0) return {};
|
|
481
|
+
const lastCodes = rounds[rounds.length - 1]?.codes;
|
|
482
|
+
if (lastCodes === void 0) return {};
|
|
483
|
+
for (const code of Object.keys(lastCodes)) {
|
|
484
|
+
let streak = 0;
|
|
485
|
+
let startIndex = rounds.length;
|
|
486
|
+
for (let i = rounds.length - 1; i >= 0; i--) {
|
|
487
|
+
const codes = rounds[i]?.codes;
|
|
488
|
+
if (codes === void 0 || !hasCode(codes, code)) break;
|
|
489
|
+
if (i > 0 && rounds[i]?.sha !== void 0 && rounds[i]?.sha === rounds[i - 1]?.sha && hasCode(rounds[i - 1]?.codes, code)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
streak += 1;
|
|
493
|
+
startIndex = i;
|
|
494
|
+
}
|
|
495
|
+
if (streak > 0) {
|
|
496
|
+
entries.push([code, { streak, startRound: rounds[startIndex]?.round ?? startIndex + 1 }]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return Object.fromEntries(entries);
|
|
500
|
+
};
|
|
501
|
+
var DEFAULT_METASTASIS_STREAK = 3;
|
|
502
|
+
var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
503
|
+
const flagged = Object.entries(consecutiveCodeStreaks(rounds)).filter(([, s]) => s.streak >= minStreak).sort((a, b) => b[1].streak - a[1].streak);
|
|
504
|
+
if (flagged.length === 0) return "";
|
|
505
|
+
const lines = flagged.map(
|
|
506
|
+
([code, s]) => `> **\`${escapeCodeBackticks(code)}\`** \u2014 findings in ${String(s.streak)} consecutive rounds.`
|
|
507
|
+
);
|
|
508
|
+
return [
|
|
509
|
+
"> [!WARNING]",
|
|
510
|
+
"> **Scope metastasis** \u2014 findings keep recurring in the same mechanism across consecutive rounds; each fix keeps enabling the next finding in that machinery. Consider whether a structural fix (change the shape, not the edge case) or a scope narrowing would converge this faster.",
|
|
511
|
+
...lines
|
|
512
|
+
].join("\n");
|
|
513
|
+
};
|
|
514
|
+
var computeSameRootNotes = (priorRounds, findings, currentSha) => {
|
|
515
|
+
const codes = findings.map((f) => f.code).filter((c) => c !== void 0 && c !== "");
|
|
516
|
+
const entries = [];
|
|
517
|
+
for (const code of codes) {
|
|
518
|
+
let lastRound = 0;
|
|
519
|
+
for (let i = priorRounds.length - 1; i >= 0; i--) {
|
|
520
|
+
if (currentSha !== void 0 && priorRounds[i]?.sha === currentSha) continue;
|
|
521
|
+
if (i > 0 && priorRounds[i]?.sha !== void 0 && priorRounds[i]?.sha === priorRounds[i - 1]?.sha && hasCode(priorRounds[i - 1]?.codes, code))
|
|
522
|
+
continue;
|
|
523
|
+
const count = priorRounds[i]?.codes?.[code];
|
|
524
|
+
if (count !== void 0 && count > 0) {
|
|
525
|
+
lastRound = priorRounds[i]?.round ?? i + 1;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (lastRound > 0) {
|
|
530
|
+
entries.push([
|
|
531
|
+
code,
|
|
532
|
+
`Same mechanism as round ${String(lastRound)} (\`${escapeCodeBackticks(code)}\`) \u2014 the prior fix in this area re-opened it; consider a structural fix or a scope narrowing.`
|
|
533
|
+
]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return Object.fromEntries(entries);
|
|
363
537
|
};
|
|
364
538
|
var CONVERGENCE_WEIGHTS = { critical: 4, major: 2, minor: 1, nit: 0 };
|
|
365
539
|
var DEFAULT_CONVERGENCE_THRESHOLD = 1;
|
|
366
540
|
var convergenceScore = (counts) => SEVERITIES.reduce((sum, k) => sum + counts[k] * CONVERGENCE_WEIGHTS[k], 0);
|
|
367
541
|
var convergenceSummary = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
|
|
542
|
+
const { score, converged } = convergenceSignal(counts, threshold);
|
|
543
|
+
return converged ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
|
|
544
|
+
};
|
|
545
|
+
var SURFACE_SCHEMA_VERSION = "0.7.0";
|
|
546
|
+
var convergenceSignal = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
|
|
368
547
|
const score = convergenceScore(counts);
|
|
369
|
-
return
|
|
548
|
+
return { score, threshold, converged: score <= threshold };
|
|
549
|
+
};
|
|
550
|
+
var signalForRound = (round, counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(counts, threshold) });
|
|
551
|
+
var surfaceFindings = (findings, signal) => {
|
|
552
|
+
const agentDoc = Object.fromEntries(
|
|
553
|
+
Object.entries(findings).filter(([key2]) => key2 !== "round" && key2 !== "convergence")
|
|
554
|
+
);
|
|
555
|
+
return {
|
|
556
|
+
...agentDoc,
|
|
557
|
+
schema_version: SURFACE_SCHEMA_VERSION,
|
|
558
|
+
...signal === null ? {} : signal
|
|
559
|
+
};
|
|
560
|
+
};
|
|
561
|
+
var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
|
|
562
|
+
JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
|
|
563
|
+
"utf-8"
|
|
564
|
+
).toString("base64")} -->`;
|
|
565
|
+
var surfacedFindingsPointer = (findings, signal, jsonUrl) => {
|
|
566
|
+
const marker = findingsPointer(surfaceFindings(findings, signal), jsonUrl);
|
|
567
|
+
if (signal === null || marker.includes("<!-- code-review:findings-json;base64 ")) return marker;
|
|
568
|
+
return marker === "" ? signalMarker(signal) : `${marker}
|
|
569
|
+
${signalMarker(signal)}`;
|
|
570
|
+
};
|
|
571
|
+
var SIGNAL_RE = /<!-- code-review:signal;base64 ([A-Za-z0-9+/=]+) -->/;
|
|
572
|
+
var parseSignalMarker = (body) => {
|
|
573
|
+
const b64 = SIGNAL_RE.exec(body)?.[1];
|
|
574
|
+
if (b64 === void 0) return null;
|
|
575
|
+
return parseSurfaceSignal(decodeBase64Json(b64));
|
|
576
|
+
};
|
|
577
|
+
var parseSurfaceSignal = (doc) => {
|
|
578
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return null;
|
|
579
|
+
const o = doc;
|
|
580
|
+
const declared = o["schema_version"];
|
|
581
|
+
if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return null;
|
|
582
|
+
const round = o["round"];
|
|
583
|
+
const convergence = o["convergence"];
|
|
584
|
+
if (typeof round !== "number" || !Number.isSafeInteger(round) || round < 1) return null;
|
|
585
|
+
if (typeof convergence !== "object" || convergence === null) return null;
|
|
586
|
+
const c = convergence;
|
|
587
|
+
if (typeof c["score"] !== "number" || !Number.isFinite(c["score"]) || typeof c["threshold"] !== "number" || !Number.isFinite(c["threshold"]) || typeof c["converged"] !== "boolean") {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
round,
|
|
592
|
+
convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
|
|
593
|
+
};
|
|
594
|
+
};
|
|
595
|
+
var SURFACE_SCHEMA_VERSIONS = [SURFACE_SCHEMA_VERSION];
|
|
596
|
+
var stripSurfaceFields = (doc) => {
|
|
597
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
|
|
598
|
+
const o = doc;
|
|
599
|
+
const declared = o["schema_version"];
|
|
600
|
+
if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return doc;
|
|
601
|
+
const rest = Object.fromEntries(
|
|
602
|
+
Object.entries(o).filter(([key2]) => key2 !== "convergence" && key2 !== "round")
|
|
603
|
+
);
|
|
604
|
+
return { ...rest, schema_version: DEFAULT_SCHEMA_VERSION };
|
|
370
605
|
};
|
|
371
606
|
var carryForwardMarkers = (body) => {
|
|
372
607
|
const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
|
|
373
608
|
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
609
|
+
const reviewedRoute = ROUTE_RE.exec(body)?.[0];
|
|
374
610
|
const rounds = ROUNDS_RE.exec(body)?.[0];
|
|
611
|
+
const signal = SIGNAL_RE.exec(body)?.[0];
|
|
612
|
+
const completedAncestor = parseReviewComplete(body) || parseCompletedAncestor(body) ? COMPLETED_ANCESTOR_MARKER : void 0;
|
|
375
613
|
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
376
614
|
${findings}` : void 0;
|
|
377
|
-
return [findingsBlock, reviewedSha, rounds].filter((m) => m !== void 0).join("\n\n");
|
|
615
|
+
return [findingsBlock, reviewedSha, reviewedRoute, completedAncestor, rounds, signal].filter((m) => m !== void 0).join("\n\n");
|
|
378
616
|
};
|
|
379
617
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
380
618
|
var projectPatch = (patch) => {
|
|
@@ -393,13 +631,18 @@ ${linkLine}` : linkLine;
|
|
|
393
631
|
|
|
394
632
|
// src/render.ts
|
|
395
633
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
396
|
-
var escapeCodeBackticks = (text) => text.replace(/`/g, "-");
|
|
397
634
|
var sanitizeFinding = (f) => ({
|
|
398
635
|
...f,
|
|
399
636
|
title: escapePipes(f.title),
|
|
400
637
|
path: escapeCodeBackticks(f.path),
|
|
401
638
|
patchProjection: projectPatch(f.patch)
|
|
402
639
|
});
|
|
640
|
+
var sanitizeSystemic = (s) => ({
|
|
641
|
+
...s,
|
|
642
|
+
title: escapePipes(s.title),
|
|
643
|
+
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
644
|
+
...s.finding_codes !== void 0 ? { finding_codes: s.finding_codes.map(escapeCodeBackticks) } : {}
|
|
645
|
+
});
|
|
403
646
|
var emptySeverityCounts = () => ({
|
|
404
647
|
critical: 0,
|
|
405
648
|
major: 0,
|
|
@@ -410,6 +653,11 @@ var computeSeverityCounts = (findings) => findings.reduce(
|
|
|
410
653
|
(acc, f) => f.severity in acc ? { ...acc, [f.severity]: acc[f.severity] + 1 } : acc,
|
|
411
654
|
emptySeverityCounts()
|
|
412
655
|
);
|
|
656
|
+
var isReviewVerdict = (verdict) => verdict !== "error";
|
|
657
|
+
var computeRoundCounts = (findings) => (findings.systemic_problems ?? []).reduce(
|
|
658
|
+
(acc, s) => s.severity in acc ? { ...acc, [s.severity]: acc[s.severity] + 1 } : acc,
|
|
659
|
+
computeSeverityCounts(findings.findings)
|
|
660
|
+
);
|
|
413
661
|
var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
|
|
414
662
|
var render = (input) => {
|
|
415
663
|
const eta = new Eta({ autoTrim: false });
|
|
@@ -423,7 +671,10 @@ var render = (input) => {
|
|
|
423
671
|
const modelNames = input.envelope ? input.envelope.models.map((m) => m.model).join(", ") : "";
|
|
424
672
|
const severityCounts = input.severityCounts ?? computeSeverityCounts(input.findings.findings);
|
|
425
673
|
const rounds = input.rounds ?? [];
|
|
426
|
-
const
|
|
674
|
+
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(rounds.slice(0, -1), input.findings.findings);
|
|
675
|
+
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
676
|
+
const convergenceCounts = rounds[rounds.length - 1] ?? computeRoundCounts(input.findings);
|
|
677
|
+
const advisoryAllowed = isFullReviewRound;
|
|
427
678
|
return eta.renderString(input.template, {
|
|
428
679
|
findings: input.findings,
|
|
429
680
|
envelope: input.envelope,
|
|
@@ -439,15 +690,27 @@ var render = (input) => {
|
|
|
439
690
|
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
440
691
|
postedAt: input.postedAt ?? "",
|
|
441
692
|
severityCounts,
|
|
442
|
-
convergenceSummary: isFullReviewRound ? convergenceSummary(
|
|
693
|
+
convergenceSummary: isFullReviewRound ? convergenceSummary(convergenceCounts, input.convergenceThreshold) : "",
|
|
443
694
|
strays: (input.strays ?? []).map(sanitizeFinding),
|
|
695
|
+
systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
|
|
444
696
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
445
697
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
446
698
|
runUrl: input.runUrl ?? null,
|
|
447
699
|
jsonUrl: input.jsonUrl ?? null,
|
|
448
|
-
findingsPointer: input.findingsPointer ??
|
|
700
|
+
findingsPointer: input.findingsPointer ?? surfacedFindingsPointer(
|
|
701
|
+
input.findings,
|
|
702
|
+
// The fallback embeds a signal exactly when the badge renders — never beside a suppressed
|
|
703
|
+
// badge, and from the same counts the badge reads. It assumes a post-style history (the
|
|
704
|
+
// caller appends this run's counts last), numbering the round exactly as the trajectory
|
|
705
|
+
// label does; post always supplies the marker, so this path cannot disagree with it in
|
|
706
|
+
// production (issue #141 review r4).
|
|
707
|
+
isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, convergenceCounts, input.convergenceThreshold) : null,
|
|
708
|
+
input.jsonUrl
|
|
709
|
+
),
|
|
449
710
|
roundsMarker: roundsMarker(rounds),
|
|
450
|
-
roundsSummary: roundsSummary(rounds),
|
|
711
|
+
roundsSummary: roundsSummary(rounds, input.roundCount),
|
|
712
|
+
metastasisNote: advisoryAllowed ? metastasisNote(rounds) : "",
|
|
713
|
+
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
451
714
|
reviewUrl: input.reviewUrl ?? null,
|
|
452
715
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
453
716
|
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
|
|
@@ -530,7 +793,7 @@ var partitionFindings = (findings, index) => {
|
|
|
530
793
|
|
|
531
794
|
// src/inline.ts
|
|
532
795
|
var formatModels = (models) => models.length > 0 ? models.map((m) => `\`${m}\``).join("/") : "an AI model";
|
|
533
|
-
var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer) => (
|
|
796
|
+
var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRootNote) => (
|
|
534
797
|
// Eta.renderString returns string | Promise<string>; with autoTrim:false it's always sync.
|
|
535
798
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
536
799
|
eta.renderString(template, {
|
|
@@ -540,7 +803,8 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer) => (
|
|
|
540
803
|
formatConfidence,
|
|
541
804
|
modelsText,
|
|
542
805
|
jsonUrl: jsonUrl ?? null,
|
|
543
|
-
findingsPointer: pointer
|
|
806
|
+
findingsPointer: pointer,
|
|
807
|
+
sameRootNote
|
|
544
808
|
})
|
|
545
809
|
);
|
|
546
810
|
var buildInlineComments = (findings, diff, context) => {
|
|
@@ -551,11 +815,12 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
551
815
|
const modelsText = formatModels(models);
|
|
552
816
|
const comments = inDiff.map((f) => {
|
|
553
817
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
818
|
+
const sameRootNote = f.code !== void 0 && f.code !== "" && context.sameRootNotes !== void 0 && Object.prototype.hasOwnProperty.call(context.sameRootNotes, f.code) ? context.sameRootNotes[f.code] ?? "" : "";
|
|
554
819
|
const comment = {
|
|
555
820
|
path: f.path,
|
|
556
821
|
line: f.end_line,
|
|
557
822
|
side: defaultSide(f.side),
|
|
558
|
-
body: renderCommentBody(f, eta, inlineTemplate, modelsText, jsonUrl, pointer)
|
|
823
|
+
body: renderCommentBody(f, eta, inlineTemplate, modelsText, jsonUrl, pointer, sameRootNote)
|
|
559
824
|
};
|
|
560
825
|
if (f.start_line < f.end_line) {
|
|
561
826
|
return {
|
|
@@ -901,13 +1166,20 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
901
1166
|
return false;
|
|
902
1167
|
};
|
|
903
1168
|
var singleWriterMessage = (draftPath) => `Only the main agent may write ${draftPath}. When a subagent writes it too, the concurrent writers clobber each other and the review comes out empty. Do NOT write, edit, or redirect into ${draftPath} \u2014 instead, return the findings you discovered in your reply (the field names are in the schema); the main agent collects every subagent's reported findings and writes the draft itself.`;
|
|
904
|
-
var
|
|
905
|
-
var
|
|
1169
|
+
var SEED_SENTINEL = "code-review seed sentinel \u2014 not a review; replace this file with your findings review.";
|
|
1170
|
+
var normalizedDraft = (draftText) => typeof draftText === "string" ? draftText.replace(/^\uFEFF/, "").trim() : "";
|
|
1171
|
+
var isSeedSentinel = (draftText) => normalizedDraft(draftText) === SEED_SENTINEL;
|
|
1172
|
+
var mainHasWrittenDraft = (draftText) => {
|
|
1173
|
+
const normalized = normalizedDraft(draftText);
|
|
1174
|
+
return normalized !== "" && normalized !== SEED_SENTINEL;
|
|
1175
|
+
};
|
|
1176
|
+
var sidecarPath = (draftPath, postfix) => {
|
|
906
1177
|
const ext = extname(draftPath);
|
|
907
|
-
return join(dirname(draftPath), `${basename(draftPath, ext)}
|
|
1178
|
+
return join(dirname(draftPath), `${basename(draftPath, ext)}${postfix}${ext}`);
|
|
908
1179
|
};
|
|
909
|
-
var
|
|
910
|
-
var
|
|
1180
|
+
var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
|
|
1181
|
+
var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
|
|
1182
|
+
var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and the pre-seeded $DRAFT is a non-review sentinel: it does not count until you have replaced it with your own review. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
|
|
911
1183
|
var forceBackgroundSpawn = (toolInput) => ({
|
|
912
1184
|
hookSpecificOutput: {
|
|
913
1185
|
hookEventName: "PreToolUse",
|
|
@@ -953,7 +1225,7 @@ var evaluateBudgetHook = (input, params) => {
|
|
|
953
1225
|
if (phase.kind === "hard" && blockedDuringConvergence(toolName, rec["tool_input"]))
|
|
954
1226
|
return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
|
|
955
1227
|
if (SPAWN_TOOLS.has(toolName)) {
|
|
956
|
-
if (!isSubagent && !params.mainDraftWritten)
|
|
1228
|
+
if (!isSubagent && !params.mainDraftWritten())
|
|
957
1229
|
return denyPreTool(spawnFloorMessage(params.draftPath));
|
|
958
1230
|
return forceBackgroundSpawn(rec["tool_input"]);
|
|
959
1231
|
}
|
|
@@ -1110,6 +1382,14 @@ var findingsTable = [
|
|
|
1110
1382
|
},
|
|
1111
1383
|
{
|
|
1112
1384
|
minor: "0.5",
|
|
1385
|
+
defaultVersion: "0.5.0",
|
|
1386
|
+
schemaFile: "findings.schema.json",
|
|
1387
|
+
codec: FindingsCodec,
|
|
1388
|
+
normalize: identity,
|
|
1389
|
+
latest: false
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
minor: "0.6",
|
|
1113
1393
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
1114
1394
|
schemaFile: "findings.schema.json",
|
|
1115
1395
|
codec: FindingsCodec,
|
|
@@ -1152,9 +1432,9 @@ var formatErrors = (errors) => errors.map(describeValidationError);
|
|
|
1152
1432
|
var declaredVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
|
|
1153
1433
|
var supportedVersions = (kind) => tableFor(kind).map((entry) => entry.minor);
|
|
1154
1434
|
var defaultVersion = (kind) => {
|
|
1155
|
-
const
|
|
1156
|
-
if (!
|
|
1157
|
-
return
|
|
1435
|
+
const latest = tableFor(kind).find((entry) => entry.latest);
|
|
1436
|
+
if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
|
|
1437
|
+
return latest.defaultVersion;
|
|
1158
1438
|
};
|
|
1159
1439
|
var bundledSchemaPath = (relativePath) => resolve$1(import.meta.dirname, "..", "schema", relativePath);
|
|
1160
1440
|
var schemaPathFor = (kind, version) => {
|
|
@@ -1282,8 +1562,111 @@ var fetchDiff = async (repo, prNumber, ghApi) => ghApi([
|
|
|
1282
1562
|
"Accept: application/vnd.github.v3.diff"
|
|
1283
1563
|
]);
|
|
1284
1564
|
|
|
1565
|
+
// src/checkrun.ts
|
|
1566
|
+
var CHECK_RUN_NAME = "Code review";
|
|
1567
|
+
var settled = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
1568
|
+
var runIdFromUrl = (runUrl) => {
|
|
1569
|
+
const m = /\/actions\/runs\/(\d+)\/?$/.exec(runUrl);
|
|
1570
|
+
return m?.[1] ?? null;
|
|
1571
|
+
};
|
|
1572
|
+
var ownedCheck = (checks, runUrl) => {
|
|
1573
|
+
const runId = runIdFromUrl(runUrl);
|
|
1574
|
+
if (runId === null) return null;
|
|
1575
|
+
return checks.find((c) => c.detailsUrl !== null && c.detailsUrl.endsWith(`/runs/${runId}`)) ?? null;
|
|
1576
|
+
};
|
|
1577
|
+
var decideCheckAction = (checks, intent, runUrl) => {
|
|
1578
|
+
const owned = ownedCheck(checks, runUrl);
|
|
1579
|
+
switch (intent) {
|
|
1580
|
+
case "in_progress":
|
|
1581
|
+
return owned !== null ? { kind: "noop", reason: "this run's check is already in progress" } : { kind: "create", status: "in_progress" };
|
|
1582
|
+
case "neutral":
|
|
1583
|
+
if (owned === null) return { kind: "create", status: "completed", conclusion: "neutral" };
|
|
1584
|
+
return owned.status === "completed" && owned.conclusion === "neutral" ? { kind: "noop", reason: "the check already records this completed review" } : { kind: "patch", id: owned.id, status: "completed", conclusion: "neutral" };
|
|
1585
|
+
case "failure":
|
|
1586
|
+
if (owned === null) return { kind: "create", status: "completed", conclusion: "failure" };
|
|
1587
|
+
if (owned.status === "completed" && owned.conclusion !== null && settled.has(owned.conclusion))
|
|
1588
|
+
return { kind: "noop", reason: "a completed review already recorded this head" };
|
|
1589
|
+
return owned.status === "completed" && owned.conclusion === "failure" ? { kind: "noop", reason: "the check already records this failure" } : { kind: "patch", id: owned.id, status: "completed", conclusion: "failure" };
|
|
1590
|
+
case "cancelled":
|
|
1591
|
+
return { kind: "noop", reason: "cancelled is handled via decideCancelledAction" };
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
var decideCancelledAction = (checks, runUrl) => {
|
|
1595
|
+
const owned = ownedCheck(checks, runUrl);
|
|
1596
|
+
if (owned === null) return { kind: "noop", reason: "no check was created by this run to settle" };
|
|
1597
|
+
if (owned.status === "completed" && owned.conclusion === "cancelled")
|
|
1598
|
+
return { kind: "noop", reason: "the check already records this cancelled run" };
|
|
1599
|
+
if (owned.status === "completed" && owned.conclusion !== null && settled.has(owned.conclusion))
|
|
1600
|
+
return { kind: "noop", reason: "a completed review already recorded this head" };
|
|
1601
|
+
return { kind: "patch", id: owned.id, status: "completed", conclusion: "cancelled" };
|
|
1602
|
+
};
|
|
1603
|
+
var CHECK_JQ = ".check_runs[] | {id: .id, status: .status, conclusion: .conclusion, detailsUrl: .details_url}";
|
|
1604
|
+
var fetchChecks = async (repo, headSha, ghApi) => parseJsonl(
|
|
1605
|
+
await ghApi([
|
|
1606
|
+
`repos/${repo}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(CHECK_RUN_NAME)}&per_page=100`,
|
|
1607
|
+
"--paginate",
|
|
1608
|
+
"--jq",
|
|
1609
|
+
CHECK_JQ
|
|
1610
|
+
])
|
|
1611
|
+
);
|
|
1612
|
+
var output = (intent, runUrl) => {
|
|
1613
|
+
switch (intent) {
|
|
1614
|
+
case "in_progress":
|
|
1615
|
+
return {
|
|
1616
|
+
title: "Code review in progress",
|
|
1617
|
+
summary: `The review is running \u2014 [see the run](${runUrl}).`
|
|
1618
|
+
};
|
|
1619
|
+
case "neutral":
|
|
1620
|
+
return {
|
|
1621
|
+
title: "Code review complete",
|
|
1622
|
+
summary: `The review was posted \u2014 [see the run](${runUrl}).`
|
|
1623
|
+
};
|
|
1624
|
+
case "failure":
|
|
1625
|
+
return {
|
|
1626
|
+
title: "Code review did not complete",
|
|
1627
|
+
summary: `The review job failed \u2014 [see the run](${runUrl}). Re-request the review; do not treat this round as spent.`
|
|
1628
|
+
};
|
|
1629
|
+
case "cancelled":
|
|
1630
|
+
return {
|
|
1631
|
+
title: "Code review superseded",
|
|
1632
|
+
summary: `This review run was cancelled \u2014 [see the run](${runUrl}). No action needed.`
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
var checkRun = async (input, ghApi = runGhApi) => {
|
|
1637
|
+
const checks = await fetchChecks(input.repo, input.headSha, ghApi);
|
|
1638
|
+
const action = input.intent === "cancelled" ? decideCancelledAction(checks, input.runUrl) : decideCheckAction(checks, input.intent, input.runUrl);
|
|
1639
|
+
if (action.kind === "noop") {
|
|
1640
|
+
process.stderr.write(`code-review check-run: ${action.reason} \u2014 leaving it
|
|
1641
|
+
`);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const body = action.kind === "create" ? {
|
|
1645
|
+
name: CHECK_RUN_NAME,
|
|
1646
|
+
head_sha: input.headSha,
|
|
1647
|
+
status: action.status,
|
|
1648
|
+
details_url: input.runUrl,
|
|
1649
|
+
...action.conclusion ? { conclusion: action.conclusion } : {},
|
|
1650
|
+
output: output(input.intent, input.runUrl)
|
|
1651
|
+
} : {
|
|
1652
|
+
status: action.status,
|
|
1653
|
+
conclusion: action.conclusion,
|
|
1654
|
+
details_url: input.runUrl,
|
|
1655
|
+
output: output(input.intent, input.runUrl)
|
|
1656
|
+
};
|
|
1657
|
+
const endpoint = action.kind === "create" ? [`--method`, `POST`, `repos/${input.repo}/check-runs`, `--input`, `-`] : [
|
|
1658
|
+
`--method`,
|
|
1659
|
+
`PATCH`,
|
|
1660
|
+
`repos/${input.repo}/check-runs/${String(action.id)}`,
|
|
1661
|
+
`--input`,
|
|
1662
|
+
`-`
|
|
1663
|
+
];
|
|
1664
|
+
await ghApi(endpoint, JSON.stringify(body));
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1285
1667
|
// src/post.ts
|
|
1286
1668
|
var DEFAULT_MARKER = "<!-- code-review -->";
|
|
1669
|
+
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";
|
|
1287
1670
|
var MAX_SUGGESTION_LINES = 10;
|
|
1288
1671
|
var countSuggestionLines = (text) => text.split("\n").length;
|
|
1289
1672
|
var checkLongSuggestions = (comments) => {
|
|
@@ -1608,12 +1991,33 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1608
1991
|
);
|
|
1609
1992
|
const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
|
|
1610
1993
|
const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
|
|
1994
|
+
const priorIsFullReview = (body) => isFullReviewSticky(body) || parseReviewedRoute(body) === null && (parseReviewComplete(body) || parseCompletedAncestor(body));
|
|
1995
|
+
const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
|
|
1611
1996
|
const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
|
|
1612
|
-
const
|
|
1997
|
+
const priorSignal = existingSticky === null ? null : parseSignalMarker(existingSticky.body) ?? parseSurfaceSignal(parseFindingsMarker(existingSticky.body));
|
|
1998
|
+
const findingsMarkerFor = (findings2, signal2) => {
|
|
1999
|
+
const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl);
|
|
2000
|
+
return signal2 !== null || priorSignal === null ? pointer : `${pointer}
|
|
2001
|
+
${signalMarker(priorSignal)}`;
|
|
2002
|
+
};
|
|
2003
|
+
const leaveInPlace = (message) => {
|
|
1613
2004
|
process.stderr.write(
|
|
1614
|
-
|
|
1615
|
-
|
|
2005
|
+
message ?? "Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place\n"
|
|
2006
|
+
);
|
|
2007
|
+
process.exit(0);
|
|
2008
|
+
};
|
|
2009
|
+
const emptyMechanicLeaveOrNote = async (sticky) => {
|
|
2010
|
+
if (existingComplete) leaveInPlace(EMPTY_MECHANIC_LEAVE_MESSAGE);
|
|
2011
|
+
const priorSha = parseReviewedSha(sticky.body);
|
|
2012
|
+
const body = formatMarkdown(
|
|
2013
|
+
noticeBody(
|
|
2014
|
+
`${DEFAULT_MARKER}
|
|
2015
|
+
|
|
2016
|
+
\u26A0\uFE0F **CI-fix pass completed with no findings** for \`${input.headSha.slice(0, 7)}\` \u2014 the completed full review of \`${priorSha ? priorSha.slice(0, 7) : "an earlier commit"}\` is preserved below.`,
|
|
2017
|
+
sticky.body
|
|
2018
|
+
)
|
|
1616
2019
|
);
|
|
2020
|
+
await upsertSticky(input.repo, prNumber, sticky, body, ghApi);
|
|
1617
2021
|
process.exit(0);
|
|
1618
2022
|
};
|
|
1619
2023
|
const prices = JSON.parse(readFileSync(input.pricesPath, "utf-8"));
|
|
@@ -1623,24 +2027,34 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1623
2027
|
}
|
|
1624
2028
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
1625
2029
|
const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
|
|
1626
|
-
const renderNotice = (message) =>
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2030
|
+
const renderNotice = (message) => {
|
|
2031
|
+
const findings2 = incompleteFindings(`### \u26A0\uFE0F ${message}`);
|
|
2032
|
+
return formatMarkdown(
|
|
2033
|
+
render({
|
|
2034
|
+
findings: findings2,
|
|
2035
|
+
envelope: null,
|
|
2036
|
+
incomplete: true,
|
|
2037
|
+
prices: decodedPrices.right,
|
|
2038
|
+
pricesProvided: input.pricesProvided,
|
|
2039
|
+
template,
|
|
2040
|
+
route: input.route,
|
|
2041
|
+
reviewedSha: input.headSha,
|
|
2042
|
+
effort: input.effort,
|
|
2043
|
+
rounds: priorRounds,
|
|
2044
|
+
sameRootNotes: {},
|
|
2045
|
+
roundCount: priorSignal?.round ?? priorRounds.length,
|
|
2046
|
+
convergenceRound: false,
|
|
2047
|
+
runUrl: input.runUrl,
|
|
2048
|
+
jsonUrl: input.jsonUrl,
|
|
2049
|
+
// A notice's own blob stays clean: verdict "error" + a carried "converged" would read as a
|
|
2050
|
+
// stop signal for a run that produced no verdict (issue #141 review r2). The prior signal
|
|
2051
|
+
// survives on the sticky in the compact marker (findingsMarkerFor), and the carried-forward
|
|
2052
|
+
// trajectory (rounds marker) remains the historical record.
|
|
2053
|
+
findingsPointer: findingsMarkerFor(findings2, null),
|
|
2054
|
+
postedAt: input.postedAt
|
|
2055
|
+
})
|
|
2056
|
+
);
|
|
2057
|
+
};
|
|
1644
2058
|
if (isEmptyDiff(diff)) {
|
|
1645
2059
|
if (wouldBuryCompleted(true)) leaveInPlace();
|
|
1646
2060
|
await upsertSticky(
|
|
@@ -1667,9 +2081,12 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1667
2081
|
const findings = findingsResult.findings;
|
|
1668
2082
|
const envelope = loadEnvelope(input.envelopePath);
|
|
1669
2083
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
2084
|
+
const effectiveRoute = input.route ?? envelope?.route;
|
|
1670
2085
|
if (envelope === null) {
|
|
1671
2086
|
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
1672
2087
|
if (wouldBuryCompleted(envelopelessIncomplete)) leaveInPlace();
|
|
2088
|
+
if (emptyMechanicWouldBury(effectiveRoute, envelopelessIncomplete) && existingSticky !== null)
|
|
2089
|
+
await emptyMechanicLeaveOrNote(existingSticky);
|
|
1673
2090
|
const body = formatMarkdown(
|
|
1674
2091
|
render({
|
|
1675
2092
|
findings,
|
|
@@ -1678,15 +2095,23 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1678
2095
|
prices: decodedPrices.right,
|
|
1679
2096
|
pricesProvided: input.pricesProvided,
|
|
1680
2097
|
template,
|
|
1681
|
-
route:
|
|
2098
|
+
route: effectiveRoute,
|
|
1682
2099
|
reviewedSha: input.headSha,
|
|
1683
2100
|
effort: input.effort,
|
|
1684
2101
|
rounds: priorRounds,
|
|
2102
|
+
sameRootNotes: {},
|
|
2103
|
+
roundCount: priorSignal?.round ?? priorRounds.length,
|
|
1685
2104
|
convergenceRound: false,
|
|
1686
2105
|
testReport,
|
|
1687
2106
|
inlineDisposition: { kind: "no-envelope" },
|
|
1688
2107
|
runUrl: input.runUrl,
|
|
1689
2108
|
jsonUrl: input.jsonUrl,
|
|
2109
|
+
// Same signal rule as the main path: only a completed-review doc carries the prior signal
|
|
2110
|
+
// in its blob; an error-verdict doc preserves it in the compact marker instead.
|
|
2111
|
+
findingsPointer: findingsMarkerFor(
|
|
2112
|
+
findings,
|
|
2113
|
+
isReviewVerdict(findings.verdict) ? priorSignal : null
|
|
2114
|
+
),
|
|
1690
2115
|
postedAt: input.postedAt
|
|
1691
2116
|
})
|
|
1692
2117
|
);
|
|
@@ -1698,7 +2123,10 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1698
2123
|
}
|
|
1699
2124
|
const thisIncomplete = envelope.incomplete === true || isIncompleteFindings(findings);
|
|
1700
2125
|
if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
|
|
1701
|
-
|
|
2126
|
+
if (emptyMechanicWouldBury(effectiveRoute, thisIncomplete) && existingSticky !== null)
|
|
2127
|
+
await emptyMechanicLeaveOrNote(existingSticky);
|
|
2128
|
+
const isRound = isConvergenceRound(effectiveRoute, thisIncomplete) && isReviewVerdict(findings.verdict);
|
|
2129
|
+
const sameRootNotes = isRound ? computeSameRootNotes(priorRounds, findings.findings, input.headSha.slice(0, 12)) : {};
|
|
1702
2130
|
const {
|
|
1703
2131
|
comments: rawComments,
|
|
1704
2132
|
strays,
|
|
@@ -1707,7 +2135,8 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1707
2135
|
inlineTemplate,
|
|
1708
2136
|
models: envelope.models.map((m) => m.model),
|
|
1709
2137
|
findings,
|
|
1710
|
-
jsonUrl: input.jsonUrl
|
|
2138
|
+
jsonUrl: input.jsonUrl,
|
|
2139
|
+
sameRootNotes
|
|
1711
2140
|
});
|
|
1712
2141
|
const { comments, longFiles } = checkLongSuggestions(rawComments);
|
|
1713
2142
|
for (const wf of longFiles) {
|
|
@@ -1719,9 +2148,31 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1719
2148
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
1720
2149
|
const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
1721
2150
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
1722
|
-
const
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
2151
|
+
const currentCodes = computeCodeCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2152
|
+
const priorLastCodes = priorRounds.length > 0 ? priorRounds[priorRounds.length - 1]?.codes : void 0;
|
|
2153
|
+
const roundNumber = Math.max(priorSignal?.round ?? priorRounds.length, priorRounds.length) + 1;
|
|
2154
|
+
const rounds = isRound ? [
|
|
2155
|
+
...priorRounds,
|
|
2156
|
+
roundRecord(
|
|
2157
|
+
computeRoundCounts(findings),
|
|
2158
|
+
currentCodes,
|
|
2159
|
+
priorLastCodes,
|
|
2160
|
+
input.headSha.slice(0, 12),
|
|
2161
|
+
roundNumber
|
|
2162
|
+
)
|
|
2163
|
+
] : priorRounds;
|
|
2164
|
+
const signal = isRound ? signalForRound(roundNumber, computeRoundCounts(findings), input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
|
|
2165
|
+
const findingsMarker = findingsMarkerFor(findings, signal);
|
|
2166
|
+
const markerForm = findingsMarkerForm(surfaceFindings(findings, signal), input.jsonUrl);
|
|
2167
|
+
if (markerForm === "link") {
|
|
2168
|
+
process.stderr.write(
|
|
2169
|
+
"Warning: the findings-json marker exceeds the embed limit \u2014 degraded to the jsonUrl-link form; a decoding agent must fetch the artifact instead of the embedded JSON\n"
|
|
2170
|
+
);
|
|
2171
|
+
} else if (markerForm === "omitted") {
|
|
2172
|
+
process.stderr.write(
|
|
2173
|
+
"Warning: the findings-json marker exceeds the embed limit and no --json-url was given \u2014 the machine-readable channel is omitted from the posted surfaces\n"
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
1725
2176
|
const commonRenderInput = {
|
|
1726
2177
|
findings,
|
|
1727
2178
|
envelope,
|
|
@@ -1729,12 +2180,14 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1729
2180
|
prices: decodedPrices.right,
|
|
1730
2181
|
pricesProvided: input.pricesProvided,
|
|
1731
2182
|
template,
|
|
1732
|
-
route:
|
|
2183
|
+
route: effectiveRoute,
|
|
1733
2184
|
reviewedSha: input.headSha,
|
|
1734
2185
|
effort: input.effort,
|
|
1735
2186
|
testReport,
|
|
1736
2187
|
severityCounts: currentCounts,
|
|
1737
2188
|
rounds,
|
|
2189
|
+
sameRootNotes,
|
|
2190
|
+
roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
|
|
1738
2191
|
convergenceThreshold: input.convergenceThreshold,
|
|
1739
2192
|
convergenceRound: isRound,
|
|
1740
2193
|
strays,
|
|
@@ -1817,15 +2270,22 @@ var post = async (input, ghApi = runGhApi) => {
|
|
|
1817
2270
|
}
|
|
1818
2271
|
}
|
|
1819
2272
|
};
|
|
1820
|
-
var
|
|
1821
|
-
const notice = `${DEFAULT_MARKER}
|
|
1822
|
-
|
|
1823
|
-
\u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.`;
|
|
2273
|
+
var noticeBody = (lead, existingBody) => {
|
|
1824
2274
|
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
1825
|
-
return carried ? `${
|
|
2275
|
+
return carried ? `${lead}
|
|
1826
2276
|
|
|
1827
|
-
${carried}` :
|
|
2277
|
+
${carried}` : lead;
|
|
2278
|
+
};
|
|
2279
|
+
var bodyRefsRun = (body, runUrl) => {
|
|
2280
|
+
const runId = runIdFromUrl(runUrl);
|
|
2281
|
+
return runId === null ? body.includes(runUrl) : new RegExp(`/actions/runs/${runId}(?!\\d)`).test(body);
|
|
1828
2282
|
};
|
|
2283
|
+
var announceBody = (headSha, runUrl, existingBody) => noticeBody(
|
|
2284
|
+
`${DEFAULT_MARKER}
|
|
2285
|
+
|
|
2286
|
+
\u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.`,
|
|
2287
|
+
existingBody
|
|
2288
|
+
);
|
|
1829
2289
|
var announce = async (input, ghApi = runGhApi) => {
|
|
1830
2290
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
1831
2291
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
@@ -1858,15 +2318,18 @@ var announce = async (input, ghApi = runGhApi) => {
|
|
|
1858
2318
|
ghApi
|
|
1859
2319
|
);
|
|
1860
2320
|
};
|
|
1861
|
-
var incompleteBody = (headSha, runUrl, existingBody) =>
|
|
1862
|
-
|
|
2321
|
+
var incompleteBody = (headSha, runUrl, existingBody) => noticeBody(
|
|
2322
|
+
`${DEFAULT_MARKER}
|
|
1863
2323
|
|
|
1864
|
-
\u26A0\uFE0F **Code review did not complete** for \`${headSha.slice(0, 7)}\` \u2014 the review job failed ([run](${runUrl})). Re-request the review; do not treat this round as spent
|
|
1865
|
-
|
|
1866
|
-
|
|
2324
|
+
\u26A0\uFE0F **Code review did not complete** for \`${headSha.slice(0, 7)}\` \u2014 the review job failed ([run](${runUrl})). Re-request the review; do not treat this round as spent.`,
|
|
2325
|
+
existingBody
|
|
2326
|
+
);
|
|
2327
|
+
var cancelledBody = (headSha, runUrl, existingBody) => noticeBody(
|
|
2328
|
+
`${DEFAULT_MARKER}
|
|
1867
2329
|
|
|
1868
|
-
${
|
|
1869
|
-
|
|
2330
|
+
\u21A9\uFE0F **Code review superseded** for \`${headSha.slice(0, 7)}\` \u2014 this run was cancelled before completing, typically because a newer review run started on this branch. No action needed. [View the cancelled run](${runUrl}) for the record.`,
|
|
2331
|
+
existingBody
|
|
2332
|
+
);
|
|
1870
2333
|
var reportIncomplete = async (input, ghApi = runGhApi) => {
|
|
1871
2334
|
const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
|
|
1872
2335
|
const resolution = resolvePr(candidates, input.headBranch);
|
|
@@ -1889,8 +2352,13 @@ var reportIncomplete = async (input, ghApi = runGhApi) => {
|
|
|
1889
2352
|
`);
|
|
1890
2353
|
return;
|
|
1891
2354
|
}
|
|
1892
|
-
if (existing !== null && !existing.body
|
|
2355
|
+
if (existing !== null && !bodyRefsRun(existing.body, input.runUrl)) {
|
|
1893
2356
|
process.stderr.write(`Sticky belongs to another run \u2014 leaving it in place
|
|
2357
|
+
`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
if (input.cancelled && existing === null) {
|
|
2361
|
+
process.stderr.write(`Cancelled review has no sticky to supersede \u2014 leaving it absent
|
|
1894
2362
|
`);
|
|
1895
2363
|
return;
|
|
1896
2364
|
}
|
|
@@ -1898,94 +2366,10 @@ var reportIncomplete = async (input, ghApi = runGhApi) => {
|
|
|
1898
2366
|
input.repo,
|
|
1899
2367
|
resolution.prNumber,
|
|
1900
2368
|
existing,
|
|
1901
|
-
incompleteBody(input.headSha, input.runUrl, existing?.body),
|
|
2369
|
+
input.cancelled ? cancelledBody(input.headSha, input.runUrl, existing?.body) : incompleteBody(input.headSha, input.runUrl, existing?.body),
|
|
1902
2370
|
ghApi
|
|
1903
2371
|
);
|
|
1904
2372
|
};
|
|
1905
|
-
|
|
1906
|
-
// src/checkrun.ts
|
|
1907
|
-
var CHECK_RUN_NAME = "Code review";
|
|
1908
|
-
var latest = (checks) => checks.reduce(
|
|
1909
|
-
(best, c) => best === null || c.id > best.id ? c : best,
|
|
1910
|
-
null
|
|
1911
|
-
);
|
|
1912
|
-
var isOpen = (status) => status === "in_progress" || status === "queued";
|
|
1913
|
-
var settled = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
1914
|
-
var decideCheckAction = (checks, intent) => {
|
|
1915
|
-
const head = latest(checks);
|
|
1916
|
-
switch (intent) {
|
|
1917
|
-
case "in_progress":
|
|
1918
|
-
return head !== null && isOpen(head.status) ? { kind: "noop", reason: "a check is already in progress for this head" } : { kind: "create", status: "in_progress" };
|
|
1919
|
-
case "neutral":
|
|
1920
|
-
if (head === null) return { kind: "create", status: "completed", conclusion: "neutral" };
|
|
1921
|
-
return head.status === "completed" && head.conclusion === "neutral" ? { kind: "noop", reason: "the check already records this completed review" } : { kind: "patch", id: head.id, status: "completed", conclusion: "neutral" };
|
|
1922
|
-
case "failure":
|
|
1923
|
-
if (head === null) return { kind: "create", status: "completed", conclusion: "failure" };
|
|
1924
|
-
if (head.status === "completed" && head.conclusion !== null && settled.has(head.conclusion))
|
|
1925
|
-
return { kind: "noop", reason: "a completed review already recorded this head" };
|
|
1926
|
-
return head.status === "completed" && head.conclusion === "failure" ? { kind: "noop", reason: "the check already records this failure" } : { kind: "patch", id: head.id, status: "completed", conclusion: "failure" };
|
|
1927
|
-
}
|
|
1928
|
-
};
|
|
1929
|
-
var CHECK_JQ = ".check_runs[] | {id: .id, status: .status, conclusion: .conclusion}";
|
|
1930
|
-
var fetchChecks = async (repo, headSha, ghApi) => parseJsonl(
|
|
1931
|
-
await ghApi([
|
|
1932
|
-
`repos/${repo}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(CHECK_RUN_NAME)}&per_page=100`,
|
|
1933
|
-
"--paginate",
|
|
1934
|
-
"--jq",
|
|
1935
|
-
CHECK_JQ
|
|
1936
|
-
])
|
|
1937
|
-
);
|
|
1938
|
-
var output = (intent, runUrl) => {
|
|
1939
|
-
switch (intent) {
|
|
1940
|
-
case "in_progress":
|
|
1941
|
-
return {
|
|
1942
|
-
title: "Code review in progress",
|
|
1943
|
-
summary: `The review is running \u2014 [see the run](${runUrl}).`
|
|
1944
|
-
};
|
|
1945
|
-
case "neutral":
|
|
1946
|
-
return {
|
|
1947
|
-
title: "Code review complete",
|
|
1948
|
-
summary: `The review was posted \u2014 [see the run](${runUrl}).`
|
|
1949
|
-
};
|
|
1950
|
-
case "failure":
|
|
1951
|
-
return {
|
|
1952
|
-
title: "Code review did not complete",
|
|
1953
|
-
summary: `The review job failed \u2014 [see the run](${runUrl}). Re-request the review; do not treat this round as spent.`
|
|
1954
|
-
};
|
|
1955
|
-
}
|
|
1956
|
-
};
|
|
1957
|
-
var checkRun = async (input, ghApi = runGhApi) => {
|
|
1958
|
-
const action = decideCheckAction(
|
|
1959
|
-
await fetchChecks(input.repo, input.headSha, ghApi),
|
|
1960
|
-
input.intent
|
|
1961
|
-
);
|
|
1962
|
-
if (action.kind === "noop") {
|
|
1963
|
-
process.stderr.write(`code-review check-run: ${action.reason} \u2014 leaving it
|
|
1964
|
-
`);
|
|
1965
|
-
return;
|
|
1966
|
-
}
|
|
1967
|
-
const body = action.kind === "create" ? {
|
|
1968
|
-
name: CHECK_RUN_NAME,
|
|
1969
|
-
head_sha: input.headSha,
|
|
1970
|
-
status: action.status,
|
|
1971
|
-
details_url: input.runUrl,
|
|
1972
|
-
...action.conclusion ? { conclusion: action.conclusion } : {},
|
|
1973
|
-
output: output(input.intent, input.runUrl)
|
|
1974
|
-
} : {
|
|
1975
|
-
status: action.status,
|
|
1976
|
-
conclusion: action.conclusion,
|
|
1977
|
-
details_url: input.runUrl,
|
|
1978
|
-
output: output(input.intent, input.runUrl)
|
|
1979
|
-
};
|
|
1980
|
-
const endpoint = action.kind === "create" ? [`--method`, `POST`, `repos/${input.repo}/check-runs`, `--input`, `-`] : [
|
|
1981
|
-
`--method`,
|
|
1982
|
-
`PATCH`,
|
|
1983
|
-
`repos/${input.repo}/check-runs/${String(action.id)}`,
|
|
1984
|
-
`--input`,
|
|
1985
|
-
`-`
|
|
1986
|
-
];
|
|
1987
|
-
await ghApi(endpoint, JSON.stringify(body));
|
|
1988
|
-
};
|
|
1989
2373
|
var DURATION_RE = /^(\d+)(h|m|s)$/;
|
|
1990
2374
|
var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
|
|
1991
2375
|
var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
|
|
@@ -2192,12 +2576,12 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
|
|
|
2192
2576
|
`
|
|
2193
2577
|
);
|
|
2194
2578
|
}
|
|
2195
|
-
const
|
|
2579
|
+
const latest = runs.filter((r) => r.name === workflowName).reduce(
|
|
2196
2580
|
(best, r) => best === null || r.run_number > best.run_number ? r : best,
|
|
2197
2581
|
null
|
|
2198
2582
|
);
|
|
2199
2583
|
return {
|
|
2200
|
-
run:
|
|
2584
|
+
run: latest === null ? null : { id: latest.id, status: latest.status ?? "unknown", conclusion: latest.conclusion },
|
|
2201
2585
|
seenNames: [...new Set(runs.flatMap((r) => r.name === null ? [] : [r.name]))]
|
|
2202
2586
|
};
|
|
2203
2587
|
};
|
|
@@ -2748,28 +3132,21 @@ var nativeTelemetry = (native, meta) => resolveTelemetry(
|
|
|
2748
3132
|
);
|
|
2749
3133
|
var absentTelemetry = (meta) => resolveTelemetry({ models: [], turns: 0, durationMs: 0, vendorCostUsd: null }, meta);
|
|
2750
3134
|
var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath, seedUnrevised) => {
|
|
2751
|
-
if (seedUnrevised)
|
|
2752
|
-
return {
|
|
2753
|
-
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
2754
|
-
findings: incompleteFindings(
|
|
2755
|
-
"### \u26A0\uFE0F Review did not complete\n\nThe review agent did not write a review (its draft is the untouched pre-seed). See the workflow logs."
|
|
2756
|
-
),
|
|
2757
|
-
incomplete: true,
|
|
2758
|
-
...telemetry
|
|
2759
|
-
};
|
|
2760
3135
|
const outcome = findingsOutcome(native, agentFilePath, agentFileFallbackPath);
|
|
2761
3136
|
switch (outcome.kind) {
|
|
2762
3137
|
case "ok":
|
|
2763
3138
|
return { schema_version: outcome.version, findings: outcome.findings, ...telemetry };
|
|
2764
|
-
case "telemetry-only":
|
|
3139
|
+
case "telemetry-only": {
|
|
3140
|
+
const reason = seedUnrevised ? "the review agent did not write a review (its draft is still the pre-seeded sentinel)" : outcome.reason;
|
|
2765
3141
|
return {
|
|
2766
3142
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
2767
3143
|
findings: incompleteFindings(`### \u26A0\uFE0F Review did not complete
|
|
2768
3144
|
|
|
2769
|
-
${
|
|
3145
|
+
${reason}`),
|
|
2770
3146
|
incomplete: true,
|
|
2771
3147
|
...telemetry
|
|
2772
3148
|
};
|
|
3149
|
+
}
|
|
2773
3150
|
}
|
|
2774
3151
|
};
|
|
2775
3152
|
var adapt = (adapterName, native, agentFilePath, meta = {}) => {
|
|
@@ -2848,6 +3225,21 @@ var buildSandboxConfig = (opts) => ({
|
|
|
2848
3225
|
filesystem: { allowRead: [], denyRead: [], allowWrite: ["/"], denyWrite: [] }
|
|
2849
3226
|
});
|
|
2850
3227
|
|
|
3228
|
+
// src/scope.ts
|
|
3229
|
+
var SCOPE_SEPARATOR_RE = /[\s,;]+/;
|
|
3230
|
+
var parseScope = (raw) => {
|
|
3231
|
+
const trimmed = raw?.trim();
|
|
3232
|
+
if (trimmed === void 0 || trimmed === "") return { kind: "absent" };
|
|
3233
|
+
if (UNSAFE_IN_SUMMARY.test(trimmed)) {
|
|
3234
|
+
return {
|
|
3235
|
+
kind: "invalid",
|
|
3236
|
+
reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t7) => t7 !== "")));
|
|
3240
|
+
return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
|
|
3241
|
+
};
|
|
3242
|
+
|
|
2851
3243
|
// src/index.ts
|
|
2852
3244
|
var readJSON = (path) => {
|
|
2853
3245
|
try {
|
|
@@ -3010,6 +3402,9 @@ var renderCmd = defineCommand({
|
|
|
3010
3402
|
const prices = decode(PriceMapCodec.decode(readJSON(priceResolution.path)), "prices");
|
|
3011
3403
|
const template = readFileSync(templatePath, "utf-8");
|
|
3012
3404
|
const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
|
|
3405
|
+
const route = args.route || envelope.route || null;
|
|
3406
|
+
const isRound = isConvergenceRound(route, envelope.incomplete === true || isIncompleteFindings(findings)) && isReviewVerdict(findings.verdict);
|
|
3407
|
+
const counts = computeRoundCounts(findings);
|
|
3013
3408
|
const output2 = render({
|
|
3014
3409
|
findings,
|
|
3015
3410
|
envelope,
|
|
@@ -3020,6 +3415,7 @@ var renderCmd = defineCommand({
|
|
|
3020
3415
|
route: args.route,
|
|
3021
3416
|
effort: args.effort,
|
|
3022
3417
|
testReport,
|
|
3418
|
+
rounds: isRound ? [counts] : [],
|
|
3023
3419
|
convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
|
|
3024
3420
|
postedAt: formatUtc(/* @__PURE__ */ new Date())
|
|
3025
3421
|
});
|
|
@@ -3153,21 +3549,33 @@ var parseConvergenceThreshold = (raw) => {
|
|
|
3153
3549
|
}
|
|
3154
3550
|
return n;
|
|
3155
3551
|
};
|
|
3156
|
-
var
|
|
3552
|
+
var transcriptPathOf = (input) => {
|
|
3553
|
+
const tp = (typeof input === "object" && input !== null ? input : {})["transcript_path"];
|
|
3554
|
+
return typeof tp === "string" ? tp : void 0;
|
|
3555
|
+
};
|
|
3556
|
+
var statMtimeMsOrNull = (path) => {
|
|
3157
3557
|
try {
|
|
3158
3558
|
return statSync(path).mtimeMs;
|
|
3159
3559
|
} catch {
|
|
3160
3560
|
return null;
|
|
3161
3561
|
}
|
|
3162
3562
|
};
|
|
3163
|
-
var
|
|
3164
|
-
|
|
3165
|
-
|
|
3563
|
+
var statSizeOrNull = (path) => {
|
|
3564
|
+
try {
|
|
3565
|
+
return statSync(path).size;
|
|
3566
|
+
} catch {
|
|
3567
|
+
return null;
|
|
3568
|
+
}
|
|
3166
3569
|
};
|
|
3167
3570
|
var snapshotIfValid = (draftPath) => {
|
|
3168
3571
|
try {
|
|
3572
|
+
const snapPath = lastValidPath(draftPath);
|
|
3573
|
+
const draftMtime = statMtimeMsOrNull(draftPath);
|
|
3574
|
+
if (draftMtime === null) return;
|
|
3575
|
+
const snapMtime = statMtimeMsOrNull(snapPath);
|
|
3576
|
+
if (snapMtime !== null && draftMtime <= snapMtime) return;
|
|
3169
3577
|
if (extractStructured({ kind: "findings", native: void 0, agentFilePath: draftPath }).kind === "ok")
|
|
3170
|
-
copyFileSync(draftPath,
|
|
3578
|
+
copyFileSync(draftPath, snapPath);
|
|
3171
3579
|
} catch (err) {
|
|
3172
3580
|
process.stderr.write(
|
|
3173
3581
|
`code-review: could not snapshot the last-valid draft (${errMsg(err)}) \u2014 any prior snapshot is unchanged
|
|
@@ -3242,10 +3650,8 @@ var budgetHookCmd = defineCommand({
|
|
|
3242
3650
|
flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
|
|
3243
3651
|
},
|
|
3244
3652
|
draftPath,
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
mtimeMsOf(seedMarkerPath(draftPath))
|
|
3248
|
-
)
|
|
3653
|
+
// Lazy: the spawn gate is the only consumer, and the hook fires on every tool event.
|
|
3654
|
+
mainDraftWritten: () => mainHasWrittenDraft(readFileOrNull(draftPath))
|
|
3249
3655
|
});
|
|
3250
3656
|
if (asRecord(input)?.["hook_event_name"] === "PostToolBatch" && !isSubagentHookInput(input))
|
|
3251
3657
|
snapshotIfValid(draftPath);
|
|
@@ -3428,12 +3834,12 @@ ${printableSchema(schemaPath)}
|
|
|
3428
3834
|
var seedDraftCmd = defineCommand({
|
|
3429
3835
|
meta: {
|
|
3430
3836
|
name: "seed-draft",
|
|
3431
|
-
description: "
|
|
3837
|
+
description: "Initialize $DRAFT before the review runs: write a NON-REVIEW SENTINEL (no recovery path can validate it as a review), and deliver the decoded findings of a prior review OUT-OF-BAND to a read-only context file beside the draft when one exists and still validates (incremental re-review). Skips a prior that never completed (an error-verdict notice) or that a CI-fix mechanic pass produced \u2014 the seed chain is route-aware. Prints the mode to stdout (prior-same|prior-new when prior context was delivered, by whether the prior review examined this same commit; empty-had-prior when a prior review exists but its findings could not be loaded; empty on a first review; none when even the sentinel write failed) and always exits 0"
|
|
3432
3838
|
},
|
|
3433
3839
|
args: {
|
|
3434
3840
|
prior: {
|
|
3435
3841
|
type: "string",
|
|
3436
|
-
description: "Path to the prior-review JSON gather staged ({ id, body }, or the literal null); its embedded base64 findings marker is decoded and
|
|
3842
|
+
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"
|
|
3437
3843
|
},
|
|
3438
3844
|
"head-sha": {
|
|
3439
3845
|
type: "string",
|
|
@@ -3441,7 +3847,7 @@ var seedDraftCmd = defineCommand({
|
|
|
3441
3847
|
},
|
|
3442
3848
|
out: {
|
|
3443
3849
|
type: "string",
|
|
3444
|
-
description: "Path to write the
|
|
3850
|
+
description: "Path to write the sentinel $DRAFT to (an absolute path outside the worktree)",
|
|
3445
3851
|
required: true
|
|
3446
3852
|
},
|
|
3447
3853
|
kind: {
|
|
@@ -3454,7 +3860,7 @@ var seedDraftCmd = defineCommand({
|
|
|
3454
3860
|
},
|
|
3455
3861
|
"schema-version": {
|
|
3456
3862
|
type: "string",
|
|
3457
|
-
description: "Schema major.minor to validate the prior findings against (default: the kind's latest \u2014 an older-shaped prior review then
|
|
3863
|
+
description: "Schema major.minor to validate the prior findings against (default: the kind's latest \u2014 an older-shaped prior review then delivers no context)"
|
|
3458
3864
|
}
|
|
3459
3865
|
},
|
|
3460
3866
|
run: async ({ args }) => {
|
|
@@ -3467,29 +3873,17 @@ var seedDraftCmd = defineCommand({
|
|
|
3467
3873
|
`
|
|
3468
3874
|
);
|
|
3469
3875
|
}
|
|
3470
|
-
const
|
|
3876
|
+
const writeSentinel = () => {
|
|
3471
3877
|
try {
|
|
3472
|
-
writeFileSync(
|
|
3473
|
-
} catch (err) {
|
|
3878
|
+
writeFileSync(outPath, SEED_SENTINEL);
|
|
3474
3879
|
process.stderr.write(
|
|
3475
|
-
`
|
|
3476
|
-
`
|
|
3477
|
-
);
|
|
3478
|
-
}
|
|
3479
|
-
};
|
|
3480
|
-
const writeScaffold = () => {
|
|
3481
|
-
try {
|
|
3482
|
-
writeFileSync(outPath, `${JSON.stringify(emptyFindings(""), null, 2)}
|
|
3483
|
-
`);
|
|
3484
|
-
writeSeedMarker();
|
|
3485
|
-
process.stderr.write(
|
|
3486
|
-
`Seeded ${outPath} with an empty valid scaffold \u2014 no decodable prior findings to build on
|
|
3880
|
+
`Seeded ${outPath} with the non-review sentinel \u2014 the agent must replace it with its review
|
|
3487
3881
|
`
|
|
3488
3882
|
);
|
|
3489
3883
|
return true;
|
|
3490
3884
|
} catch (err) {
|
|
3491
3885
|
process.stderr.write(
|
|
3492
|
-
`Warning: could not write the seed
|
|
3886
|
+
`Warning: could not write the seed sentinel to ${outPath} (${errMsg(err)}) \u2014 the agent will create $DRAFT itself
|
|
3493
3887
|
`
|
|
3494
3888
|
);
|
|
3495
3889
|
return false;
|
|
@@ -3506,24 +3900,30 @@ var seedDraftCmd = defineCommand({
|
|
|
3506
3900
|
})();
|
|
3507
3901
|
return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
|
|
3508
3902
|
})();
|
|
3509
|
-
const priorFindings = priorBody === null ? null : parseFindingsMarker(priorBody);
|
|
3903
|
+
const priorFindings = priorBody === null ? null : stripSurfaceFields(parseFindingsMarker(priorBody));
|
|
3510
3904
|
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
3511
3905
|
try {
|
|
3512
3906
|
const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
3513
3907
|
if (!validateAgainstSchema(priorFindings, schemaPath).valid) return false;
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3908
|
+
const resolution = resolve("findings", priorFindings);
|
|
3909
|
+
if (resolution.kind !== "ok") return false;
|
|
3910
|
+
if (isIncompleteFindings(resolution.value)) return false;
|
|
3911
|
+
if (parseReviewedRoute(priorBody ?? "") !== "full review") return false;
|
|
3912
|
+
writeFileSync(outPath, SEED_SENTINEL);
|
|
3913
|
+
writeFileSync(
|
|
3914
|
+
priorContextPath(outPath),
|
|
3915
|
+
`${JSON.stringify(priorFindings, null, 2)}
|
|
3916
|
+
`
|
|
3917
|
+
);
|
|
3918
|
+
const count = resolution.value.findings.length;
|
|
3519
3919
|
process.stderr.write(
|
|
3520
|
-
`Seeded ${outPath}
|
|
3920
|
+
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(count)} finding(s)) to ${priorContextPath(outPath)} as context
|
|
3521
3921
|
`
|
|
3522
3922
|
);
|
|
3523
3923
|
return true;
|
|
3524
3924
|
} catch (err) {
|
|
3525
3925
|
process.stderr.write(
|
|
3526
|
-
`Warning: could not seed from the prior review (${errMsg(err)}) \u2014
|
|
3926
|
+
`Warning: could not seed from the prior review (${errMsg(err)}) \u2014 seeding the sentinel only
|
|
3527
3927
|
`
|
|
3528
3928
|
);
|
|
3529
3929
|
return false;
|
|
@@ -3534,7 +3934,7 @@ var seedDraftCmd = defineCommand({
|
|
|
3534
3934
|
const priorSha = priorBody === null ? null : parseReviewedSha(priorBody);
|
|
3535
3935
|
return args["head-sha"] && priorSha && priorSha === args["head-sha"].toLowerCase() ? "prior-same" : "prior-new";
|
|
3536
3936
|
}
|
|
3537
|
-
if (!
|
|
3937
|
+
if (!writeSentinel()) return "none";
|
|
3538
3938
|
return priorBody === null ? "empty" : "empty-had-prior";
|
|
3539
3939
|
})();
|
|
3540
3940
|
process.stdout.write(`${mode}
|
|
@@ -3580,7 +3980,8 @@ var adaptCmd = defineCommand({
|
|
|
3580
3980
|
},
|
|
3581
3981
|
run: async ({ args }) => {
|
|
3582
3982
|
const agentFile = args["agent-file"];
|
|
3583
|
-
const
|
|
3983
|
+
const agentFileSize = agentFile ? statSizeOrNull(agentFile) : null;
|
|
3984
|
+
const seedUnrevised = agentFileSize !== null && agentFileSize <= Buffer.byteLength(SEED_SENTINEL) + 2 && isSeedSentinel(readFileOrNull(agentFile));
|
|
3584
3985
|
const envelope = unwrapAdapt(
|
|
3585
3986
|
adapt(requireAdapterName(args.adapter), readJSONOrAbsent(args.native), agentFile, {
|
|
3586
3987
|
route: args.route,
|
|
@@ -4077,11 +4478,11 @@ var announceCmd = defineCommand({
|
|
|
4077
4478
|
);
|
|
4078
4479
|
}
|
|
4079
4480
|
});
|
|
4080
|
-
var isCheckIntent = (s) => s === "in_progress" || s === "neutral" || s === "failure";
|
|
4481
|
+
var isCheckIntent = (s) => s === "in_progress" || s === "neutral" || s === "failure" || s === "cancelled";
|
|
4081
4482
|
var checkRunCmd = defineCommand({
|
|
4082
4483
|
meta: {
|
|
4083
4484
|
name: "check-run",
|
|
4084
|
-
description: "Upsert the native 'Code review' check-run on the head SHA \u2014 the attribution surface that appears in the PR's own checks list and (writing to the base repo) works for fork PRs too. `in_progress` at review start, `neutral` when the review completes, `failure` when it didn't. Forward-only: `failure` never
|
|
4485
|
+
description: "Upsert the native 'Code review' check-run on the head SHA \u2014 the attribution surface that appears in the PR's own checks list and (writing to the base repo) works for fork PRs too. `in_progress` at review start, `neutral` when the review completes, `failure` when it didn't, `cancelled` when a cancelled review settles its own check (matched by details_url, so it never touches a superseding run's check). Forward-only: `failure`/`cancelled` never overwrite a completed review."
|
|
4085
4486
|
},
|
|
4086
4487
|
args: {
|
|
4087
4488
|
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
@@ -4092,19 +4493,19 @@ var checkRunCmd = defineCommand({
|
|
|
4092
4493
|
},
|
|
4093
4494
|
status: {
|
|
4094
4495
|
type: "positional",
|
|
4095
|
-
description: "One of: in_progress, neutral, failure",
|
|
4496
|
+
description: "One of: in_progress, neutral, failure, cancelled",
|
|
4096
4497
|
required: true
|
|
4097
4498
|
},
|
|
4098
4499
|
"run-url": {
|
|
4099
4500
|
type: "string",
|
|
4100
|
-
description: "Workflow run URL the check-run's details link to",
|
|
4501
|
+
description: "Workflow run URL the check-run's details link to (also the ownership key for `cancelled`)",
|
|
4101
4502
|
required: true
|
|
4102
4503
|
}
|
|
4103
4504
|
},
|
|
4104
4505
|
run: async ({ args }) => {
|
|
4105
4506
|
if (!isCheckIntent(args.status)) {
|
|
4106
4507
|
process.stderr.write(
|
|
4107
|
-
`::warning::code-review check-run: unrecognized status "${annotationSafe(args.status)}" \u2014 expected in_progress, neutral, or
|
|
4508
|
+
`::warning::code-review check-run: unrecognized status "${annotationSafe(args.status)}" \u2014 expected in_progress, neutral, failure, or cancelled; skipping
|
|
4108
4509
|
`
|
|
4109
4510
|
);
|
|
4110
4511
|
return;
|
|
@@ -4125,7 +4526,7 @@ var checkRunCmd = defineCommand({
|
|
|
4125
4526
|
var reportIncompleteCmd = defineCommand({
|
|
4126
4527
|
meta: {
|
|
4127
4528
|
name: "report-incomplete",
|
|
4128
|
-
description: "Post (or update) the sticky when a review job hard-failed and posted nothing \u2014 an attributed 'did not complete' notice linking the run, telling the reader to re-request. Never buries a completed review, and never overwrites a superseding run's live in-progress placeholder.
|
|
4529
|
+
description: "Post (or update) the sticky when a review job hard-failed and posted nothing \u2014 an attributed 'did not complete' notice linking the run, telling the reader to re-request; with --cancelled, the informational 'superseded \u2014 no action needed' notice instead (issue #139). Never buries a completed review, and never overwrites a superseding run's live in-progress placeholder."
|
|
4129
4530
|
},
|
|
4130
4531
|
args: {
|
|
4131
4532
|
repo: { type: "string", description: "Repository (owner/name)", required: true },
|
|
@@ -4146,6 +4547,10 @@ var reportIncompleteCmd = defineCommand({
|
|
|
4146
4547
|
"head-branch": {
|
|
4147
4548
|
type: "string",
|
|
4148
4549
|
description: "Head branch to disambiguate the PR when multiple share a commit"
|
|
4550
|
+
},
|
|
4551
|
+
cancelled: {
|
|
4552
|
+
type: "boolean",
|
|
4553
|
+
description: "This run was CANCELLED before completing (typically superseded by a newer run on the same branch) \u2014 post the informational 'superseded' notice, not the failure notice (issue #139)"
|
|
4149
4554
|
}
|
|
4150
4555
|
},
|
|
4151
4556
|
run: async ({ args }) => {
|
|
@@ -4154,10 +4559,11 @@ var reportIncompleteCmd = defineCommand({
|
|
|
4154
4559
|
headSha: args["head-sha"],
|
|
4155
4560
|
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
4156
4561
|
runUrl: args["run-url"],
|
|
4157
|
-
headBranch: args["head-branch"]
|
|
4562
|
+
headBranch: args["head-branch"],
|
|
4563
|
+
cancelled: args.cancelled
|
|
4158
4564
|
}).catch(
|
|
4159
4565
|
(err) => process.stderr.write(
|
|
4160
|
-
`::warning::code-review report-incomplete: could not post the
|
|
4566
|
+
`::warning::code-review report-incomplete: could not post the notice (${annotationSafe(errMsg(err))}) \u2014 continuing
|
|
4161
4567
|
`
|
|
4162
4568
|
)
|
|
4163
4569
|
);
|
|
@@ -4321,6 +4727,30 @@ var awaitCiCmd = defineCommand({
|
|
|
4321
4727
|
process.stdout.write(renderCiOutputs(outcome));
|
|
4322
4728
|
}
|
|
4323
4729
|
});
|
|
4730
|
+
var checkScopeCmd = defineCommand({
|
|
4731
|
+
meta: {
|
|
4732
|
+
name: "check-scope",
|
|
4733
|
+
description: "Validate + normalize the workflow's `scope` input \u2014 the languages/inputs the project accepts (issue #139). Prints the normalized space-separated language list for splicing into the review prompt, or nothing when the scope is empty (the reviewer then infers it from the README's first paragraph). Fails loudly on a malformed value, so a config typo never silently corrupts the prompt it is spliced into."
|
|
4734
|
+
},
|
|
4735
|
+
args: {
|
|
4736
|
+
scope: {
|
|
4737
|
+
type: "string",
|
|
4738
|
+
description: 'The raw scope value \u2014 whitespace/comma/semicolon-separated language names (e.g. "C C++"); empty \u21D2 absent'
|
|
4739
|
+
}
|
|
4740
|
+
},
|
|
4741
|
+
run: ({ args }) => {
|
|
4742
|
+
const parsed = parseScope(args.scope);
|
|
4743
|
+
switch (parsed.kind) {
|
|
4744
|
+
case "absent":
|
|
4745
|
+
return;
|
|
4746
|
+
case "invalid":
|
|
4747
|
+
return fail(`check-scope: ${parsed.reason}`);
|
|
4748
|
+
case "ok":
|
|
4749
|
+
process.stdout.write(`${parsed.languages.join(" ")}
|
|
4750
|
+
`);
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
});
|
|
4324
4754
|
var sandboxConfigCmd = defineCommand({
|
|
4325
4755
|
meta: {
|
|
4326
4756
|
name: "sandbox-config",
|
|
@@ -4391,6 +4821,7 @@ var main = defineCommand({
|
|
|
4391
4821
|
post: postCmd,
|
|
4392
4822
|
announce: announceCmd,
|
|
4393
4823
|
"check-run": checkRunCmd,
|
|
4824
|
+
"check-scope": checkScopeCmd,
|
|
4394
4825
|
"report-incomplete": reportIncompleteCmd,
|
|
4395
4826
|
cost: costCmd,
|
|
4396
4827
|
"check-cost": checkCostCmd,
|