@jphutchins/code-review 0.1.0-alpha.63 → 0.1.0-alpha.65
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 +3 -3
- package/dist/index.js +635 -486
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +2 -1
- package/schema/findings.schema.json +9 -1
- package/templates/comment.eta +13 -1
- package/templates/inline.eta +5 -1
package/dist/index.js
CHANGED
|
@@ -109,7 +109,8 @@ var FindingIdRequired = t.type({ id: t.string });
|
|
|
109
109
|
var FindingOptional = t.partial({
|
|
110
110
|
side: SideCodec,
|
|
111
111
|
recommendation: t.string,
|
|
112
|
-
patch: t.string
|
|
112
|
+
patch: t.string,
|
|
113
|
+
rebuttal: t.string
|
|
113
114
|
});
|
|
114
115
|
var EndGeStart = (shape) => t.refinement(shape, (f) => f.end_line >= f.start_line, "EndGeStart");
|
|
115
116
|
var FindingShape = t.intersection([
|
|
@@ -135,7 +136,8 @@ var SystemicRequired = t.type({
|
|
|
135
136
|
var SystemicOptional = t.partial({
|
|
136
137
|
id: t.string,
|
|
137
138
|
finding_ids: t.array(t.string),
|
|
138
|
-
paths: t.array(t.string)
|
|
139
|
+
paths: t.array(t.string),
|
|
140
|
+
rebuttal: t.string
|
|
139
141
|
});
|
|
140
142
|
var SystemicProblemShape = t.intersection([SystemicRequired, RuleUrlCodec, SystemicOptional]);
|
|
141
143
|
var SystemicProblemCodec = strictExact("SystemicProblemStrict", SystemicProblemShape, [
|
|
@@ -245,6 +247,31 @@ var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
|
|
|
245
247
|
...PIPELINE_STAMPED_FIELDS,
|
|
246
248
|
"change_size"
|
|
247
249
|
]);
|
|
250
|
+
var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
251
|
+
var priorIdsFrom = (doc) => {
|
|
252
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
|
|
253
|
+
const rec = doc;
|
|
254
|
+
const idsOf = (field, systemic) => (Array.isArray(rec[field]) ? rec[field] : []).flatMap((raw) => {
|
|
255
|
+
const item = typeof raw === "object" && raw !== null ? asRecord(raw) : null;
|
|
256
|
+
if (item === null) return [];
|
|
257
|
+
const title = typeof item["title"] === "string" ? item["title"] : "";
|
|
258
|
+
const resolved = resolveRuleId({
|
|
259
|
+
...typeof item["id"] === "string" ? { id: item["id"] } : {},
|
|
260
|
+
...typeof item["code"] === "string" ? { code: item["code"] } : {},
|
|
261
|
+
...typeof item["path"] === "string" ? { path: item["path"] } : {},
|
|
262
|
+
title
|
|
263
|
+
});
|
|
264
|
+
const id = resolved ?? (systemic && title !== "" ? synthesizedSystemicId(title) : void 0);
|
|
265
|
+
return id !== void 0 ? [id] : [];
|
|
266
|
+
});
|
|
267
|
+
return [...idsOf("findings", false), ...idsOf("systemic_problems", true)];
|
|
268
|
+
};
|
|
269
|
+
var hasRebuttal = (item) => (item.rebuttal ?? "").trim() !== "";
|
|
270
|
+
var withoutRebuttals = (doc) => ({
|
|
271
|
+
...doc,
|
|
272
|
+
findings: doc.findings.map(({ rebuttal: _rebuttal, ...f }) => f),
|
|
273
|
+
...doc.systemic_problems !== void 0 ? { systemic_problems: doc.systemic_problems.map(({ rebuttal: _rebuttal, ...s }) => s) } : {}
|
|
274
|
+
});
|
|
248
275
|
var synthesizedId = (parts, prefix) => `${prefix}${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 12)}`;
|
|
249
276
|
var synthesizedFindingId = (path, title) => synthesizedId([path, title], "f-");
|
|
250
277
|
var synthesizedSystemicId = (title) => synthesizedId([title], "s-");
|
|
@@ -270,12 +297,18 @@ var LegacyRuleCodec = t.partial({
|
|
|
270
297
|
id: t.string,
|
|
271
298
|
code_url: UriString
|
|
272
299
|
});
|
|
273
|
-
var
|
|
300
|
+
var FindingOptionalV09 = t.partial({
|
|
301
|
+
side: SideCodec,
|
|
302
|
+
recommendation: t.string,
|
|
303
|
+
patch: t.string
|
|
304
|
+
});
|
|
305
|
+
var FindingShapeV09 = t.intersection([FindingCoreRequired, LegacyRuleCodec, FindingOptionalV09]);
|
|
274
306
|
var FindingCodecV09 = t.exact(EndGeStart(FindingShapeV09));
|
|
275
307
|
var SystemicV09Optional = t.partial({
|
|
276
308
|
finding_codes: t.array(t.string),
|
|
277
309
|
finding_ids: t.array(t.string),
|
|
278
|
-
paths: t.array(t.string)
|
|
310
|
+
paths: t.array(t.string),
|
|
311
|
+
rebuttal: t.unknown
|
|
279
312
|
});
|
|
280
313
|
var SystemicV09Shape = t.intersection([SystemicRequired, LegacyRuleCodec, SystemicV09Optional]);
|
|
281
314
|
var SystemicProblemCodecV09 = strictExact("SystemicV09Strict", SystemicV09Shape, [
|
|
@@ -343,7 +376,7 @@ var normalizeV09 = (doc) => {
|
|
|
343
376
|
id: resolveRuleId({ id, code, path: f.path, title: f.title }) ?? synthesizedFindingId(f.path, f.title)
|
|
344
377
|
}));
|
|
345
378
|
const systemic_problems = doc.systemic_problems?.map(
|
|
346
|
-
({ code, id, finding_codes, finding_ids, ...s }) => ({
|
|
379
|
+
({ code, id, finding_codes, finding_ids, rebuttal: _rebuttal, ...s }) => ({
|
|
347
380
|
...s,
|
|
348
381
|
id: resolveRuleId({ id, code, title: s.title }) ?? synthesizedSystemicId(s.title),
|
|
349
382
|
...finding_ids !== void 0 && finding_ids.length > 0 ? { finding_ids } : finding_codes !== void 0 ? { finding_ids: finding_codes } : {}
|
|
@@ -484,7 +517,7 @@ var TestSummaryCodec = t.intersection([
|
|
|
484
517
|
failures: t.array(TestFailureCodec)
|
|
485
518
|
})
|
|
486
519
|
]);
|
|
487
|
-
var DEFAULT_SCHEMA_VERSION = "0.
|
|
520
|
+
var DEFAULT_SCHEMA_VERSION = "0.11.0";
|
|
488
521
|
var incompleteFindings = (summary) => ({
|
|
489
522
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
490
523
|
summary,
|
|
@@ -494,7 +527,7 @@ var incompleteFindings = (summary) => ({
|
|
|
494
527
|
var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
|
|
495
528
|
|
|
496
529
|
// src/released.ts
|
|
497
|
-
var RELEASED = "2026-
|
|
530
|
+
var RELEASED = "2026-10-10";
|
|
498
531
|
|
|
499
532
|
// src/cost.ts
|
|
500
533
|
var defaultWarn = (message) => {
|
|
@@ -824,6 +857,8 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
|
|
|
824
857
|
var MAX_IDS_PER_ROUND = 8;
|
|
825
858
|
var hasId = (ids, code) => ids !== void 0 && Object.prototype.hasOwnProperty.call(ids, code);
|
|
826
859
|
var escapeCodeBackticks = (code) => code.replace(/`/g, "-").replace(/\r?\n/g, " ");
|
|
860
|
+
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
861
|
+
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
827
862
|
var normalizeIdCounts = (ids, priorCodes, preferredKeys, preValidated = false) => {
|
|
828
863
|
const entries = Object.entries((preValidated ? ids : usableCountsMap(ids)) ?? {});
|
|
829
864
|
if (entries.length === 0) return void 0;
|
|
@@ -1356,270 +1391,32 @@ var resolvePriorFindings = async (body, read) => {
|
|
|
1356
1391
|
}
|
|
1357
1392
|
};
|
|
1358
1393
|
|
|
1359
|
-
// src/comment-chain.ts
|
|
1360
|
-
function* ancestors(start, parentOf) {
|
|
1361
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1362
|
-
let current = start;
|
|
1363
|
-
while (current !== null && !seen.has(current)) {
|
|
1364
|
-
yield current;
|
|
1365
|
-
seen.add(current);
|
|
1366
|
-
current = parentOf(current);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
var numField = (rec, key2) => {
|
|
1370
|
-
const v = rec[key2];
|
|
1371
|
-
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
1372
|
-
};
|
|
1373
|
-
var messageUsage = (entry) => {
|
|
1374
|
-
const rec = asRecord(entry);
|
|
1375
|
-
if (rec === null || rec["type"] !== "assistant") return null;
|
|
1376
|
-
const msg = asRecord(rec["message"]);
|
|
1377
|
-
if (msg === null) return null;
|
|
1378
|
-
const model = msg["model"];
|
|
1379
|
-
const usage = asRecord(msg["usage"]);
|
|
1380
|
-
if (typeof model !== "string" || usage === null) return null;
|
|
1381
|
-
const id = msg["id"];
|
|
1382
|
-
return {
|
|
1383
|
-
id: typeof id === "string" ? id : null,
|
|
1384
|
-
model: modelIdentity(model),
|
|
1385
|
-
input: numField(usage, "input_tokens"),
|
|
1386
|
-
output: numField(usage, "output_tokens"),
|
|
1387
|
-
cacheRead: numField(usage, "cache_read_input_tokens"),
|
|
1388
|
-
cacheWrite: numField(usage, "cache_creation_input_tokens")
|
|
1389
|
-
};
|
|
1390
|
-
};
|
|
1391
|
-
var tsMsOf = (entry) => {
|
|
1392
|
-
const rec = asRecord(entry);
|
|
1393
|
-
const ts = rec?.["timestamp"];
|
|
1394
|
-
if (typeof ts !== "string") return null;
|
|
1395
|
-
const ms = Date.parse(ts);
|
|
1396
|
-
return Number.isNaN(ms) ? null : ms;
|
|
1397
|
-
};
|
|
1398
|
-
var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
1399
|
-
var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
1400
|
-
try {
|
|
1401
|
-
return [JSON.parse(line)];
|
|
1402
|
-
} catch {
|
|
1403
|
-
return [];
|
|
1404
|
-
}
|
|
1405
|
-
});
|
|
1406
|
-
var sumTranscriptUsage = (entries) => {
|
|
1407
|
-
const summed = entries.reduce(
|
|
1408
|
-
(acc, entry) => {
|
|
1409
|
-
const u = messageUsage(entry);
|
|
1410
|
-
if (u === null) return acc;
|
|
1411
|
-
if (u.id !== null && acc.seen.has(u.id)) return acc;
|
|
1412
|
-
if (u.id !== null) acc.seen.add(u.id);
|
|
1413
|
-
const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
|
|
1414
|
-
acc.totals.set(u.model, {
|
|
1415
|
-
input: prev.input + u.input,
|
|
1416
|
-
output: prev.output + u.output,
|
|
1417
|
-
cacheRead: prev.cacheRead + u.cacheRead,
|
|
1418
|
-
cacheWrite: prev.cacheWrite + u.cacheWrite
|
|
1419
|
-
});
|
|
1420
|
-
return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
|
|
1421
|
-
},
|
|
1422
|
-
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
1423
|
-
);
|
|
1424
|
-
const models = [...summed.totals].map(([model, t8]) => ({
|
|
1425
|
-
model,
|
|
1426
|
-
input_tokens: t8.input,
|
|
1427
|
-
output_tokens: t8.output,
|
|
1428
|
-
cache_read_tokens: t8.cacheRead,
|
|
1429
|
-
cache_write_tokens: t8.cacheWrite
|
|
1430
|
-
}));
|
|
1431
|
-
const bounds = entries.reduce(
|
|
1432
|
-
(acc, entry) => {
|
|
1433
|
-
const ms = tsMsOf(entry);
|
|
1434
|
-
if (ms === null) return acc;
|
|
1435
|
-
return {
|
|
1436
|
-
min: acc.min === null || ms < acc.min ? ms : acc.min,
|
|
1437
|
-
max: acc.max === null || ms > acc.max ? ms : acc.max
|
|
1438
|
-
};
|
|
1439
|
-
},
|
|
1440
|
-
{ min: null, max: null }
|
|
1441
|
-
);
|
|
1442
|
-
return {
|
|
1443
|
-
models,
|
|
1444
|
-
turns: summed.turns,
|
|
1445
|
-
durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
|
|
1446
|
-
firstTsMs: bounds.min,
|
|
1447
|
-
lastTsMs: bounds.max
|
|
1448
|
-
};
|
|
1449
|
-
};
|
|
1450
|
-
var subagentFiles = (mainPath) => {
|
|
1451
|
-
const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
|
|
1452
|
-
try {
|
|
1453
|
-
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
1454
|
-
} catch {
|
|
1455
|
-
return [];
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
|
-
var readTranscriptTree = (mainPath) => {
|
|
1459
|
-
const mainText = readFileOrNull(mainPath);
|
|
1460
|
-
const mainEntries = mainText === null ? [] : parseJsonl(mainText);
|
|
1461
|
-
const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
|
|
1462
|
-
const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
|
|
1463
|
-
const siblingReads = siblings.flatMap((path) => {
|
|
1464
|
-
const text = readFileOrNull(path);
|
|
1465
|
-
return text === null ? [] : [{ path, entries: parseJsonl(text) }];
|
|
1466
|
-
});
|
|
1467
|
-
return {
|
|
1468
|
-
entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
|
|
1469
|
-
files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
|
|
1470
|
-
missing: mainText === null
|
|
1471
|
-
};
|
|
1472
|
-
};
|
|
1473
|
-
|
|
1474
1394
|
// src/answered.ts
|
|
1475
|
-
var
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
description: t.string,
|
|
1491
|
-
reasoning: t.string,
|
|
1492
|
-
severity: t.union([
|
|
1493
|
-
t.literal("critical"),
|
|
1494
|
-
t.literal("major"),
|
|
1495
|
-
t.literal("minor"),
|
|
1496
|
-
t.literal("nit")
|
|
1497
|
-
]),
|
|
1498
|
-
path: t.string,
|
|
1499
|
-
patch: t.union([t.string, t.null]),
|
|
1500
|
-
replied_at: t.union([t.string, t.null]),
|
|
1501
|
-
reply_id: t.number,
|
|
1502
|
-
thread_url: t.string,
|
|
1503
|
-
reply_url: t.string,
|
|
1504
|
-
reply_author: t.string,
|
|
1505
|
-
reply_excerpt: t.string
|
|
1506
|
-
});
|
|
1507
|
-
var encodeAnsweredEntry = (e) => ({
|
|
1508
|
-
code: e.code,
|
|
1509
|
-
title: e.title,
|
|
1510
|
-
description: e.description,
|
|
1511
|
-
reasoning: e.reasoning,
|
|
1512
|
-
severity: e.severity,
|
|
1513
|
-
path: e.path,
|
|
1514
|
-
patch: e.patch,
|
|
1515
|
-
replied_at: e.repliedAt,
|
|
1516
|
-
reply_id: e.replyId,
|
|
1517
|
-
thread_url: e.threadUrl,
|
|
1518
|
-
reply_url: e.replyUrl,
|
|
1519
|
-
reply_author: e.replyAuthor,
|
|
1520
|
-
reply_excerpt: e.replyExcerpt
|
|
1395
|
+
var answeredRegistryFrom = (closures, prior) => closures.flatMap((response) => {
|
|
1396
|
+
const finding = prior?.findings.find((f) => resolveFindingId(f) === response.id);
|
|
1397
|
+
return finding === void 0 ? [] : [
|
|
1398
|
+
{
|
|
1399
|
+
code: response.id,
|
|
1400
|
+
title: finding.title,
|
|
1401
|
+
description: finding.description,
|
|
1402
|
+
reasoning: finding.reasoning,
|
|
1403
|
+
severity: finding.severity,
|
|
1404
|
+
path: finding.path,
|
|
1405
|
+
patch: finding.patch ?? null,
|
|
1406
|
+
answerUrl: response.source_url,
|
|
1407
|
+
answerAuthor: response.author
|
|
1408
|
+
}
|
|
1409
|
+
];
|
|
1521
1410
|
});
|
|
1522
|
-
var decodeAnsweredEntry = (raw) => {
|
|
1523
|
-
const decoded = AnsweredEntryCodec.decode(raw);
|
|
1524
|
-
return decoded._tag === "Right" ? decoded.right : null;
|
|
1525
|
-
};
|
|
1526
|
-
var isHuman = (login, type8, botLogin) => login !== botLogin && type8 === "User";
|
|
1527
|
-
var EXCERPT_LIMIT = 400;
|
|
1528
|
-
var answeredRegistryFrom = (comments, botLogin) => {
|
|
1529
|
-
const ordered = [...comments].sort(
|
|
1530
|
-
(a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? "") || (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)
|
|
1531
|
-
);
|
|
1532
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1533
|
-
for (const c of ordered) byId.set(c.id, c);
|
|
1534
|
-
const rootOf = (c) => {
|
|
1535
|
-
const chain = [
|
|
1536
|
-
...ancestors(
|
|
1537
|
-
c,
|
|
1538
|
-
(n) => n.in_reply_to_id === null ? null : byId.get(n.in_reply_to_id) ?? null
|
|
1539
|
-
)
|
|
1540
|
-
];
|
|
1541
|
-
const root = chain[chain.length - 1];
|
|
1542
|
-
return root !== void 0 && root.in_reply_to_id === null ? root : null;
|
|
1543
|
-
};
|
|
1544
|
-
const findingOf = (root) => {
|
|
1545
|
-
const decoded = parseFindingsMarker(root.body ?? "");
|
|
1546
|
-
const doc = typeof decoded === "object" && decoded !== null ? decoded.findings : void 0;
|
|
1547
|
-
const first = Array.isArray(doc) ? doc[0] : void 0;
|
|
1548
|
-
if (first === void 0) return null;
|
|
1549
|
-
const title = first["title"];
|
|
1550
|
-
const description = first["description"];
|
|
1551
|
-
const reasoning = first["reasoning"];
|
|
1552
|
-
const id = first["id"];
|
|
1553
|
-
const legacyCode = first["code"];
|
|
1554
|
-
const severity = first["severity"];
|
|
1555
|
-
const path = first["path"];
|
|
1556
|
-
const patch = first["patch"];
|
|
1557
|
-
return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
|
|
1558
|
-
// resolveRuleId: the ONE legacy-spelling precedence the upcast, this reader, and the
|
|
1559
|
-
// below-floor nit reader share — a pre-id marker (or one written before the migration)
|
|
1560
|
-
// carries `code`; a codeless one resolves to the same synthesized id the registry's
|
|
1561
|
-
// legacy upcast derives, so the entry keys to the identical claim on the next round.
|
|
1562
|
-
code: resolveRuleId({
|
|
1563
|
-
id: typeof id === "string" ? id : void 0,
|
|
1564
|
-
code: typeof legacyCode === "string" ? legacyCode : void 0,
|
|
1565
|
-
path,
|
|
1566
|
-
title
|
|
1567
|
-
}) ?? synthesizedFindingId(path, title),
|
|
1568
|
-
title,
|
|
1569
|
-
description,
|
|
1570
|
-
reasoning,
|
|
1571
|
-
severity,
|
|
1572
|
-
path,
|
|
1573
|
-
patch: typeof patch === "string" ? patch : null
|
|
1574
|
-
} : null;
|
|
1575
|
-
};
|
|
1576
|
-
const threads = /* @__PURE__ */ new Map();
|
|
1577
|
-
for (const c of ordered) {
|
|
1578
|
-
const root = rootOf(c);
|
|
1579
|
-
if (root === null) continue;
|
|
1580
|
-
const group = threads.get(root.id);
|
|
1581
|
-
if (group === void 0) threads.set(root.id, [c]);
|
|
1582
|
-
else group.push(c);
|
|
1583
|
-
}
|
|
1584
|
-
const entries = [];
|
|
1585
|
-
for (const [rootId, group] of threads) {
|
|
1586
|
-
const root = byId.get(rootId);
|
|
1587
|
-
if (root === void 0 || root.user_login !== botLogin) continue;
|
|
1588
|
-
const finding = findingOf(root);
|
|
1589
|
-
if (finding === null) continue;
|
|
1590
|
-
const humanReplies = group.filter(
|
|
1591
|
-
(c) => c.id !== root.id && isHuman(c.user_login, c.user_type, botLogin)
|
|
1592
|
-
);
|
|
1593
|
-
const reply = humanReplies[humanReplies.length - 1];
|
|
1594
|
-
if (reply === void 0) continue;
|
|
1595
|
-
entries.push({
|
|
1596
|
-
...finding,
|
|
1597
|
-
repliedAt: reply.created_at,
|
|
1598
|
-
replyId: reply.id,
|
|
1599
|
-
threadUrl: root.html_url,
|
|
1600
|
-
replyUrl: reply.html_url,
|
|
1601
|
-
replyAuthor: reply.user_login,
|
|
1602
|
-
replyExcerpt: clipText(reply.body ?? "", EXCERPT_LIMIT)
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
1606
|
-
for (const entry of [...entries].sort(
|
|
1607
|
-
(a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
|
|
1608
|
-
)) {
|
|
1609
|
-
const key2 = answeredNoteKey({ id: entry.code, title: entry.title });
|
|
1610
|
-
if (!byKey.has(key2)) byKey.set(key2, entry);
|
|
1611
|
-
}
|
|
1612
|
-
return [...byKey.values()];
|
|
1613
|
-
};
|
|
1614
1411
|
var isSynthesizedTitleMatch = (f, e) => isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1615
1412
|
var matches = (resolvedId, f, e) => e.code === resolvedId || isSynthesizedTitleMatch(f, e);
|
|
1413
|
+
var couldMatch = (code, findings) => isSynthesizedFindingId(code) || findings.some((f) => resolveFindingId(f) === code);
|
|
1616
1414
|
var VERBATIM_FIELDS = ["title", "description", "reasoning", "severity", "path", "patch"];
|
|
1617
1415
|
var verbatimFieldEqual = (f, e, field) => field === "patch" ? (f.patch ?? null) === e.patch : f[field] === e[field];
|
|
1618
|
-
var isVerbatimReRaise = (f, e) => VERBATIM_FIELDS.every((field) => verbatimFieldEqual(f, e, field));
|
|
1416
|
+
var isVerbatimReRaise = (f, e) => !hasRebuttal(f) && VERBATIM_FIELDS.every((field) => verbatimFieldEqual(f, e, field));
|
|
1619
1417
|
var isAnsweredDrop = (resolvedId, f, e) => matches(resolvedId, f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1620
1418
|
var verbatimMatchCount = (f, e) => VERBATIM_FIELDS.reduce((count, field) => count + (verbatimFieldEqual(f, e, field) ? 1 : 0), 0);
|
|
1621
1419
|
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1622
|
-
var NO_TITLE_MATCH = { entry: void 0, score: -1 };
|
|
1623
1420
|
var bestTitleMatch = (f, registry) => {
|
|
1624
1421
|
let best;
|
|
1625
1422
|
let bestScore = -1;
|
|
@@ -1632,9 +1429,10 @@ var bestTitleMatch = (f, registry) => {
|
|
|
1632
1429
|
if (score === VERBATIM_FIELDS.length) break;
|
|
1633
1430
|
}
|
|
1634
1431
|
}
|
|
1635
|
-
return
|
|
1432
|
+
return best;
|
|
1636
1433
|
};
|
|
1637
|
-
var
|
|
1434
|
+
var byAuthor = (e) => e.answerAuthor === null ? "" : ` by \`${escapeCodeBackticks(e.answerAuthor)}\``;
|
|
1435
|
+
var answeredNote = (e) => `Re-raised; prior answer at ${linkSafeUrl(e.answerUrl)}${byAuthor(e)} \u2014 cite the new evidence that invalidates it.`;
|
|
1638
1436
|
var applyAnswered = (findings, registry) => {
|
|
1639
1437
|
const kept = [];
|
|
1640
1438
|
const noteEntries = [];
|
|
@@ -1644,15 +1442,15 @@ var applyAnswered = (findings, registry) => {
|
|
|
1644
1442
|
for (const f of findings) {
|
|
1645
1443
|
const resolvedId = resolveFindingId(f);
|
|
1646
1444
|
const idMatch = registry.find((e) => e.code === resolvedId);
|
|
1647
|
-
const
|
|
1445
|
+
const titleMatch = idMatch === void 0 ? bestTitleMatch(f, registry) : void 0;
|
|
1648
1446
|
const entry = idMatch ?? titleMatch;
|
|
1649
1447
|
if (entry === void 0) {
|
|
1650
1448
|
kept.push(f);
|
|
1651
1449
|
continue;
|
|
1652
1450
|
}
|
|
1653
|
-
const dropped = (
|
|
1451
|
+
const dropped = isAnsweredDrop(resolvedId, f, entry);
|
|
1654
1452
|
if (dropped) {
|
|
1655
|
-
droppedByEntry.set(entry.
|
|
1453
|
+
droppedByEntry.set(entry.code, entry);
|
|
1656
1454
|
droppedFindingIds.push(resolvedId);
|
|
1657
1455
|
droppedCount += 1;
|
|
1658
1456
|
} else {
|
|
@@ -1672,42 +1470,33 @@ var answeredReRaiseNote = (entries, count) => {
|
|
|
1672
1470
|
if (entries.length === 0) return "";
|
|
1673
1471
|
const label = (e) => e.code !== "" ? `\`${escapeCodeBackticks(e.code)}\`` : `\u201C${escapeCodeBackticks(e.title)}\u201D`;
|
|
1674
1472
|
const lines = entries.map(
|
|
1675
|
-
(e) => `> - ${label(e)} \u2014 [prior answer](${e.
|
|
1473
|
+
(e) => `> - ${label(e)} \u2014 [prior answer](${linkSafeUrl(e.answerUrl)})${byAuthor(e)}`
|
|
1676
1474
|
);
|
|
1677
1475
|
return [
|
|
1678
|
-
`> \u21A9\uFE0F **${String(count)} finding(s) re-raised without new evidence \u2014 treated as answered** (
|
|
1476
|
+
`> \u21A9\uFE0F **${String(count)} finding(s) re-raised without new evidence \u2014 treated as answered** (a maintainer refuted or dismissed each):`,
|
|
1679
1477
|
...lines
|
|
1680
1478
|
].join("\n");
|
|
1681
1479
|
};
|
|
1682
|
-
var
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
process.stderr.write(
|
|
1700
|
-
`Warning: could not fetch review threads to detect answered findings (${errMsg(err)}) \u2014 no answered-finding state for this post
|
|
1701
|
-
`
|
|
1702
|
-
);
|
|
1703
|
-
return null;
|
|
1704
|
-
}
|
|
1705
|
-
};
|
|
1480
|
+
var DispositionCodec = t.keyof({ fixed: null, refuted: null, dismissed: null });
|
|
1481
|
+
var DISPOSITIONS = Object.keys(DispositionCodec.keys);
|
|
1482
|
+
var ResponseDispositionCodec = t.keyof({
|
|
1483
|
+
fixed: null,
|
|
1484
|
+
refuted: null,
|
|
1485
|
+
dismissed: null,
|
|
1486
|
+
unstated: null
|
|
1487
|
+
});
|
|
1488
|
+
var RESPONSE_FORM = `Review-Response: <id> ${DISPOSITIONS.join("|")} \u2014 <reason>`;
|
|
1489
|
+
var RESPONSE_TABLE_COLUMNS = ["id", "disposition", "reason"];
|
|
1490
|
+
`| ${RESPONSE_TABLE_COLUMNS.join(" | ")} |`;
|
|
1491
|
+
`| ${RESPONSE_TABLE_COLUMNS.map(() => "---").join(" | ")} |`;
|
|
1492
|
+
var listed = (words, conjunction) => {
|
|
1493
|
+
const quoted = words.map((word) => `\`${word}\``);
|
|
1494
|
+
return quoted.length < 2 ? quoted.join("") : `${quoted.slice(0, -1).join(", ")} ${conjunction} ${quoted.at(-1) ?? ""}`;
|
|
1495
|
+
};
|
|
1496
|
+
var RESPONSE_TEACHING = `To answer findings, post a PR conversation comment holding a rendered markdown table, not one inside a code block, whose columns are ${listed(RESPONSE_TABLE_COLUMNS, "and")}, one row per finding id, its disposition exactly one of ${listed(DISPOSITIONS, "or")}; or put \`${RESPONSE_FORM}\` lines in a commit message. Inline-thread replies are not read. A systemic problem's id, when shown, answers its whole class.`;
|
|
1706
1497
|
|
|
1707
1498
|
// src/render.ts
|
|
1708
1499
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1709
|
-
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1710
|
-
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
1711
1500
|
var shedNote = (count, summaryAvailable, runUrl, findingsDocLinked, thisRunArtifact) => count === 0 ? "" : `<sub>**${String(count)} finding${count === 1 ? "" : "s"} left out of this comment**${summaryAvailable && runUrl !== void 0 ? ` \u2014 the [run summary](<${runUrl}>) carries the whole review` : findingsDocLinked && thisRunArtifact ? " \u2014 the machine findings document carries them" : ""}.</sub>`;
|
|
1712
1501
|
var terminalNotice = (i) => [
|
|
1713
1502
|
`## ${i.verdictBadge}`,
|
|
@@ -1824,6 +1613,7 @@ var sanitizeSuppressedNit = (f, discussion, discussionTruncated) => ({
|
|
|
1824
1613
|
var carriedLines = (f) => [
|
|
1825
1614
|
`description: ${clipText(f.description, BODY_CLIP_CHARS)}`,
|
|
1826
1615
|
...f.recommendation !== void 0 ? [`recommendation: ${clipText(f.recommendation, BODY_CLIP_CHARS)}`] : [],
|
|
1616
|
+
...hasRebuttal(f) ? [`rebuttal: ${clipText(f.rebuttal ?? "", BODY_CLIP_CHARS)}`] : [],
|
|
1827
1617
|
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1828
1618
|
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1829
1619
|
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
@@ -1992,6 +1782,9 @@ var render = (input) => {
|
|
|
1992
1782
|
input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
|
|
1993
1783
|
),
|
|
1994
1784
|
summaryAvailable: input.summaryAvailable ?? false,
|
|
1785
|
+
responseForm: RESPONSE_FORM,
|
|
1786
|
+
responseTeaching: RESPONSE_TEACHING,
|
|
1787
|
+
teachesResponses: isConvergenceRound(route, incomplete),
|
|
1995
1788
|
shedNote: shedNote(
|
|
1996
1789
|
input.shedCount ?? 0,
|
|
1997
1790
|
input.summaryAvailable ?? false,
|
|
@@ -2083,7 +1876,8 @@ var render = (input) => {
|
|
|
2083
1876
|
},
|
|
2084
1877
|
verdictBadge,
|
|
2085
1878
|
severityEmoji,
|
|
2086
|
-
formatConfidence
|
|
1879
|
+
formatConfidence,
|
|
1880
|
+
hasRebuttal
|
|
2087
1881
|
});
|
|
2088
1882
|
};
|
|
2089
1883
|
var key = (path, line) => `${path}:${String(line)}`;
|
|
@@ -2146,9 +1940,11 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
|
|
|
2146
1940
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
2147
1941
|
eta.renderString(template, {
|
|
2148
1942
|
...f,
|
|
1943
|
+
idLabel: escapeCodeBackticks(f.id),
|
|
2149
1944
|
patchProjection: projectPatch(f.patch, "diff-anchored"),
|
|
2150
1945
|
severityEmoji,
|
|
2151
1946
|
formatConfidence,
|
|
1947
|
+
hasRebuttal,
|
|
2152
1948
|
modelsText,
|
|
2153
1949
|
jsonUrl: jsonUrl ?? null,
|
|
2154
1950
|
findingsPointer: pointer,
|
|
@@ -2170,6 +1966,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
2170
1966
|
title: clipText(f.title, BODY_CLIP_CHARS),
|
|
2171
1967
|
description: clipText(f.description, BODY_CLIP_CHARS),
|
|
2172
1968
|
...f.recommendation != null ? { recommendation: clipText(f.recommendation, BODY_CLIP_CHARS) } : {},
|
|
1969
|
+
...hasRebuttal(f) ? { rebuttal: clipText(f.rebuttal ?? "", BODY_CLIP_CHARS) } : {},
|
|
2173
1970
|
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
2174
1971
|
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
2175
1972
|
} : f;
|
|
@@ -2204,7 +2001,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
2204
2001
|
var renderStraysSection = (strays) => {
|
|
2205
2002
|
if (strays.length === 0) return "";
|
|
2206
2003
|
const items = strays.map(
|
|
2207
|
-
(f) => `- **${f.severity}** \xB7 \`${f.path}:${String(f.start_line)}\` \u2014 ${f.title}`
|
|
2004
|
+
(f) => `- **${f.severity}** \xB7 \`${f.path}:${String(f.start_line)}\` \u2014 ${f.title}${f.id === "" ? "" : ` \xB7 id \`${escapeCodeBackticks(f.id)}\``}`
|
|
2208
2005
|
);
|
|
2209
2006
|
return [
|
|
2210
2007
|
"",
|
|
@@ -2215,6 +2012,110 @@ var renderStraysSection = (strays) => {
|
|
|
2215
2012
|
...items
|
|
2216
2013
|
].join("\n");
|
|
2217
2014
|
};
|
|
2015
|
+
var numField = (rec, key2) => {
|
|
2016
|
+
const v = rec[key2];
|
|
2017
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
2018
|
+
};
|
|
2019
|
+
var messageUsage = (entry) => {
|
|
2020
|
+
const rec = asRecord(entry);
|
|
2021
|
+
if (rec === null || rec["type"] !== "assistant") return null;
|
|
2022
|
+
const msg = asRecord(rec["message"]);
|
|
2023
|
+
if (msg === null) return null;
|
|
2024
|
+
const model = msg["model"];
|
|
2025
|
+
const usage = asRecord(msg["usage"]);
|
|
2026
|
+
if (typeof model !== "string" || usage === null) return null;
|
|
2027
|
+
const id = msg["id"];
|
|
2028
|
+
return {
|
|
2029
|
+
id: typeof id === "string" ? id : null,
|
|
2030
|
+
model: modelIdentity(model),
|
|
2031
|
+
input: numField(usage, "input_tokens"),
|
|
2032
|
+
output: numField(usage, "output_tokens"),
|
|
2033
|
+
cacheRead: numField(usage, "cache_read_input_tokens"),
|
|
2034
|
+
cacheWrite: numField(usage, "cache_creation_input_tokens")
|
|
2035
|
+
};
|
|
2036
|
+
};
|
|
2037
|
+
var tsMsOf = (entry) => {
|
|
2038
|
+
const rec = asRecord(entry);
|
|
2039
|
+
const ts = rec?.["timestamp"];
|
|
2040
|
+
if (typeof ts !== "string") return null;
|
|
2041
|
+
const ms = Date.parse(ts);
|
|
2042
|
+
return Number.isNaN(ms) ? null : ms;
|
|
2043
|
+
};
|
|
2044
|
+
var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
2045
|
+
var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
2046
|
+
try {
|
|
2047
|
+
return [JSON.parse(line)];
|
|
2048
|
+
} catch {
|
|
2049
|
+
return [];
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
var sumTranscriptUsage = (entries) => {
|
|
2053
|
+
const summed = entries.reduce(
|
|
2054
|
+
(acc, entry) => {
|
|
2055
|
+
const u = messageUsage(entry);
|
|
2056
|
+
if (u === null) return acc;
|
|
2057
|
+
if (u.id !== null && acc.seen.has(u.id)) return acc;
|
|
2058
|
+
if (u.id !== null) acc.seen.add(u.id);
|
|
2059
|
+
const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
|
|
2060
|
+
acc.totals.set(u.model, {
|
|
2061
|
+
input: prev.input + u.input,
|
|
2062
|
+
output: prev.output + u.output,
|
|
2063
|
+
cacheRead: prev.cacheRead + u.cacheRead,
|
|
2064
|
+
cacheWrite: prev.cacheWrite + u.cacheWrite
|
|
2065
|
+
});
|
|
2066
|
+
return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
|
|
2067
|
+
},
|
|
2068
|
+
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
2069
|
+
);
|
|
2070
|
+
const models = [...summed.totals].map(([model, t9]) => ({
|
|
2071
|
+
model,
|
|
2072
|
+
input_tokens: t9.input,
|
|
2073
|
+
output_tokens: t9.output,
|
|
2074
|
+
cache_read_tokens: t9.cacheRead,
|
|
2075
|
+
cache_write_tokens: t9.cacheWrite
|
|
2076
|
+
}));
|
|
2077
|
+
const bounds = entries.reduce(
|
|
2078
|
+
(acc, entry) => {
|
|
2079
|
+
const ms = tsMsOf(entry);
|
|
2080
|
+
if (ms === null) return acc;
|
|
2081
|
+
return {
|
|
2082
|
+
min: acc.min === null || ms < acc.min ? ms : acc.min,
|
|
2083
|
+
max: acc.max === null || ms > acc.max ? ms : acc.max
|
|
2084
|
+
};
|
|
2085
|
+
},
|
|
2086
|
+
{ min: null, max: null }
|
|
2087
|
+
);
|
|
2088
|
+
return {
|
|
2089
|
+
models,
|
|
2090
|
+
turns: summed.turns,
|
|
2091
|
+
durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
|
|
2092
|
+
firstTsMs: bounds.min,
|
|
2093
|
+
lastTsMs: bounds.max
|
|
2094
|
+
};
|
|
2095
|
+
};
|
|
2096
|
+
var subagentFiles = (mainPath) => {
|
|
2097
|
+
const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
|
|
2098
|
+
try {
|
|
2099
|
+
return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
|
|
2100
|
+
} catch {
|
|
2101
|
+
return [];
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
var readTranscriptTree = (mainPath) => {
|
|
2105
|
+
const mainText = readFileOrNull(mainPath);
|
|
2106
|
+
const mainEntries = mainText === null ? [] : parseJsonl(mainText);
|
|
2107
|
+
const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
|
|
2108
|
+
const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
|
|
2109
|
+
const siblingReads = siblings.flatMap((path) => {
|
|
2110
|
+
const text = readFileOrNull(path);
|
|
2111
|
+
return text === null ? [] : [{ path, entries: parseJsonl(text) }];
|
|
2112
|
+
});
|
|
2113
|
+
return {
|
|
2114
|
+
entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
|
|
2115
|
+
files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
|
|
2116
|
+
missing: mainText === null
|
|
2117
|
+
};
|
|
2118
|
+
};
|
|
2218
2119
|
var addFormats = _addFormats;
|
|
2219
2120
|
var validatorCache = /* @__PURE__ */ new Map();
|
|
2220
2121
|
var compileSchema = (schemaPath) => {
|
|
@@ -2396,7 +2297,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
2396
2297
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
2397
2298
|
if (WRITE_TOOLS.has(toolName)) {
|
|
2398
2299
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
2399
|
-
if (typeof fp === "string" && targets.some((
|
|
2300
|
+
if (typeof fp === "string" && targets.some((t9) => fp === t9 || basename(fp) === basename(t9)))
|
|
2400
2301
|
return true;
|
|
2401
2302
|
}
|
|
2402
2303
|
if (toolName === "Bash") {
|
|
@@ -2423,7 +2324,6 @@ var sidecarPath = (draftPath, postfix) => {
|
|
|
2423
2324
|
return join(dirname(draftPath), `${basename(draftPath, ext)}${postfix}${ext}`);
|
|
2424
2325
|
};
|
|
2425
2326
|
var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
|
|
2426
|
-
var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
|
|
2427
2327
|
var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
|
|
2428
2328
|
var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
|
|
2429
2329
|
var costAxisDisengagedPath = (draftPath) => sidecarPath(draftPath, ".cost-axis-disengaged");
|
|
@@ -2502,9 +2402,9 @@ var parseWallMs = (raw) => {
|
|
|
2502
2402
|
};
|
|
2503
2403
|
var parseEpochSecMs = (raw) => {
|
|
2504
2404
|
if (raw === void 0) return null;
|
|
2505
|
-
const
|
|
2506
|
-
if (!/^\d+$/.test(
|
|
2507
|
-
const n = Number.parseInt(
|
|
2405
|
+
const t9 = raw.trim();
|
|
2406
|
+
if (!/^\d+$/.test(t9)) return null;
|
|
2407
|
+
const n = Number.parseInt(t9, 10);
|
|
2508
2408
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
2509
2409
|
};
|
|
2510
2410
|
var anchoredElapsedMs = (src) => {
|
|
@@ -2625,10 +2525,12 @@ var findingsTable = [
|
|
|
2625
2525
|
{
|
|
2626
2526
|
minor: "0.4",
|
|
2627
2527
|
defaultVersion: "0.4.0",
|
|
2628
|
-
// The frozen tolerant-in legacy schema, NOT the live
|
|
2528
|
+
// The frozen tolerant-in legacy schema, NOT the live file: every ajv-gated channel
|
|
2629
2529
|
// (validate --schema-version, the extraction ladder) dispatches the RAW doc's declared minor
|
|
2630
2530
|
// through schemaPathFor, so the ajv gate must accept exactly what the tolerant legacy codec
|
|
2631
2531
|
// accepts — the live file (id required) would reject the legacy docs the upcast promises to read.
|
|
2532
|
+
// One deliberate exception: the codec drops a draft-only `rebuttal` that the frozen file rejects.
|
|
2533
|
+
// A pre-0.10 document never carried one, so only a hybrid doc the rescue exists for meets it.
|
|
2632
2534
|
schemaFile: "v0.9/findings.schema.json",
|
|
2633
2535
|
codec: legacyFindingsCodec,
|
|
2634
2536
|
normalize: legacyFindingsNormalize,
|
|
@@ -2660,6 +2562,14 @@ var findingsTable = [
|
|
|
2660
2562
|
},
|
|
2661
2563
|
{
|
|
2662
2564
|
minor: "0.10",
|
|
2565
|
+
defaultVersion: "0.10.0",
|
|
2566
|
+
schemaFile: "findings.schema.json",
|
|
2567
|
+
codec: FindingsCodec,
|
|
2568
|
+
normalize: identity,
|
|
2569
|
+
latest: false
|
|
2570
|
+
},
|
|
2571
|
+
{
|
|
2572
|
+
minor: "0.11",
|
|
2663
2573
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
2664
2574
|
schemaFile: "findings.schema.json",
|
|
2665
2575
|
codec: FindingsCodec,
|
|
@@ -2714,21 +2624,28 @@ var describeValidationError = (e) => {
|
|
|
2714
2624
|
var formatErrors = (errors) => errors.map(describeValidationError);
|
|
2715
2625
|
var declaredVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
|
|
2716
2626
|
var supportedVersions = (kind) => tableFor(kind).map((entry) => entry.minor);
|
|
2717
|
-
var defaultVersion = (kind) => {
|
|
2718
|
-
const latest = tableFor(kind).find((entry) => entry.latest);
|
|
2719
|
-
if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
|
|
2720
|
-
return latest.defaultVersion;
|
|
2721
|
-
};
|
|
2722
2627
|
var bundledSchemaPath = (relativePath) => resolve(import.meta.dirname, "..", "schema", relativePath);
|
|
2723
|
-
var
|
|
2628
|
+
var entryFor = (kind, version) => {
|
|
2724
2629
|
const table = tableFor(kind);
|
|
2725
2630
|
const entry = version === void 0 ? table.find((v) => v.latest) : table.find((v) => v.minor === majorMinor(version));
|
|
2631
|
+
if (!entry && version === void 0) {
|
|
2632
|
+
throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
|
|
2633
|
+
}
|
|
2726
2634
|
if (!entry) {
|
|
2727
2635
|
throw new Error(
|
|
2728
2636
|
`Unsupported ${kind} schema version "${version ?? ""}" \u2014 supported: ${supportedVersions(kind).join(", ")}`
|
|
2729
2637
|
);
|
|
2730
2638
|
}
|
|
2731
|
-
return
|
|
2639
|
+
return entry;
|
|
2640
|
+
};
|
|
2641
|
+
var schemaPathFor = (kind, version) => bundledSchemaPath(entryFor(kind, version).schemaFile);
|
|
2642
|
+
var versionFor = (kind, version) => entryFor(kind, version).defaultVersion;
|
|
2643
|
+
var defaultVersion = (kind) => versionFor(kind);
|
|
2644
|
+
var livePinFor = (kind, version) => {
|
|
2645
|
+
const table = tableFor(kind);
|
|
2646
|
+
const latest = table.find((v) => v.latest);
|
|
2647
|
+
const entry = version === void 0 ? latest : table.find((v) => v.minor === majorMinor(version));
|
|
2648
|
+
return entry !== void 0 && entry.schemaFile === latest?.schemaFile ? entry.defaultVersion : void 0;
|
|
2732
2649
|
};
|
|
2733
2650
|
var resolveFindings = (raw) => {
|
|
2734
2651
|
const version = declaredVersion(raw);
|
|
@@ -2762,7 +2679,7 @@ var resolveTolerantFindings = (doc) => {
|
|
|
2762
2679
|
};
|
|
2763
2680
|
|
|
2764
2681
|
// src/pr.ts
|
|
2765
|
-
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
|
|
2682
|
+
var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha, headRepo: (.head.repo.full_name // null)}";
|
|
2766
2683
|
var parseCandidates = (stdout) => parseJsonl(stdout);
|
|
2767
2684
|
var fetchDirectCandidates = async (repo, headSha, ghApi) => {
|
|
2768
2685
|
try {
|
|
@@ -2791,7 +2708,7 @@ var resolvePr = (candidates, headBranch) => {
|
|
|
2791
2708
|
const scoped = candidates.length > 1 && headBranch ? candidates.filter((c) => c.headRef === headBranch) : candidates;
|
|
2792
2709
|
const chosen = scoped.find((c) => c.state === "open") ?? scoped[0] ?? candidates[0];
|
|
2793
2710
|
if (chosen === void 0) return { kind: "none" };
|
|
2794
|
-
return chosen.state === "open" ? { kind: "open", prNumber: chosen.number } : { kind: "not-open", prNumber: chosen.number, state: chosen.state };
|
|
2711
|
+
return chosen.state === "open" ? { kind: "open", prNumber: chosen.number, headRepo: chosen.headRepo ?? null } : { kind: "not-open", prNumber: chosen.number, state: chosen.state };
|
|
2795
2712
|
};
|
|
2796
2713
|
var fetchDiff = async (repo, prNumber, ghApi) => ghApi([
|
|
2797
2714
|
`repos/${repo}/pulls/${String(prNumber)}`,
|
|
@@ -2900,6 +2817,220 @@ var checkRun = async (input, ghApi = runGhApi) => {
|
|
|
2900
2817
|
];
|
|
2901
2818
|
await ghApi(endpoint, JSON.stringify(body));
|
|
2902
2819
|
};
|
|
2820
|
+
var RESPONSE_REASON_CLIP_CHARS = 300;
|
|
2821
|
+
var RESPONSES_PER_CHANNEL = 25;
|
|
2822
|
+
var ChannelCodec = t.keyof({ comment: null, commit: null });
|
|
2823
|
+
var MAINTAINER_ASSOCIATIONS = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
2824
|
+
var CLOSING_DISPOSITIONS = /* @__PURE__ */ new Set(["refuted", "dismissed"]);
|
|
2825
|
+
var ResponseShape = t.type({
|
|
2826
|
+
id: t.string,
|
|
2827
|
+
disposition: ResponseDispositionCodec,
|
|
2828
|
+
reason: t.string,
|
|
2829
|
+
channel: ChannelCodec,
|
|
2830
|
+
source_url: t.string,
|
|
2831
|
+
author: t.union([t.string, t.null]),
|
|
2832
|
+
author_association: t.union([t.string, t.null]),
|
|
2833
|
+
created_at: t.union([t.string, t.null])
|
|
2834
|
+
});
|
|
2835
|
+
var ResponseCodec = strictExact("Response", ResponseShape, [ResponseShape]);
|
|
2836
|
+
var UnmatchedResponseShape = t.type({
|
|
2837
|
+
id: t.string,
|
|
2838
|
+
channel: ChannelCodec,
|
|
2839
|
+
source_url: t.string
|
|
2840
|
+
});
|
|
2841
|
+
var UnmatchedResponseCodec = strictExact("UnmatchedResponse", UnmatchedResponseShape, [
|
|
2842
|
+
UnmatchedResponseShape
|
|
2843
|
+
]);
|
|
2844
|
+
var ResponsesFileShape = t.type({
|
|
2845
|
+
responses: t.array(ResponseCodec),
|
|
2846
|
+
unmatched: t.array(UnmatchedResponseCodec)
|
|
2847
|
+
});
|
|
2848
|
+
strictExact("ResponsesFile", ResponsesFileShape, [
|
|
2849
|
+
ResponsesFileShape
|
|
2850
|
+
]);
|
|
2851
|
+
var RESPONSE_LINE_RE = /^ {0,3}review-response:\s*(\S+)\s+([A-Za-z]+)(?![A-Za-z]|\s*\|)[\s—–:-]*(.*)$/i;
|
|
2852
|
+
var FENCE_OPEN_RE = /^ {0,3}(`{3,}(?=[^`]*$)|~{3,})/;
|
|
2853
|
+
var LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/;
|
|
2854
|
+
var unfencedLines = (text) => text.split(LINE_BREAK_RE).reduce(
|
|
2855
|
+
(state, line) => {
|
|
2856
|
+
const marker = FENCE_OPEN_RE.exec(line)?.[1];
|
|
2857
|
+
if (state.fence === null) {
|
|
2858
|
+
if (marker !== void 0) return { fence: marker, lines: state.lines };
|
|
2859
|
+
state.lines.push(line);
|
|
2860
|
+
return state;
|
|
2861
|
+
}
|
|
2862
|
+
const closes = marker !== void 0 && marker[0] === state.fence[0] && marker.length >= state.fence.length && line.trim() === marker;
|
|
2863
|
+
return closes ? { fence: null, lines: state.lines } : state;
|
|
2864
|
+
},
|
|
2865
|
+
{ fence: null, lines: [] }
|
|
2866
|
+
).lines;
|
|
2867
|
+
var WHOLE_LINE_CODE_SPAN_RE = /^ {0,3}`([^`]+)`\s*$/;
|
|
2868
|
+
var answerLines = (text) => unfencedLines(text).map((line) => WHOLE_LINE_CODE_SPAN_RE.exec(line)?.[1] ?? line);
|
|
2869
|
+
var unwrapId = (token) => token.replace(/^[`"']+|[`"'.,:;]+$/g, "");
|
|
2870
|
+
var lineAnswers = (lines) => lines.flatMap((line) => {
|
|
2871
|
+
const match = RESPONSE_LINE_RE.exec(line);
|
|
2872
|
+
const id = unwrapId(match?.[1] ?? "");
|
|
2873
|
+
const disposition = match?.[2]?.toLowerCase();
|
|
2874
|
+
return id !== "" && DispositionCodec.is(disposition) ? [
|
|
2875
|
+
{
|
|
2876
|
+
id,
|
|
2877
|
+
disposition,
|
|
2878
|
+
reason: clipText((match?.[3] ?? "").trim(), RESPONSE_REASON_CLIP_CHARS)
|
|
2879
|
+
}
|
|
2880
|
+
] : [];
|
|
2881
|
+
});
|
|
2882
|
+
var ID_IN_CELL_RE = /`([^`]+)`/g;
|
|
2883
|
+
var ID_HEADERS = /* @__PURE__ */ new Set(["id", "ids", "finding", "findings"]);
|
|
2884
|
+
var DISPOSITION_HEADERS = /* @__PURE__ */ new Set(["disposition", "verdict", "resolution"]);
|
|
2885
|
+
var CELL_RE = /((?:\\[\s\S]?|[^\\|])*)(\||$)/g;
|
|
2886
|
+
var splitCells = (row) => {
|
|
2887
|
+
const cells = [...row.matchAll(CELL_RE)];
|
|
2888
|
+
return cells.slice(0, cells.findIndex((match) => match[2] !== "|") + 1).map((match) => match[1] ?? "");
|
|
2889
|
+
};
|
|
2890
|
+
var BLOCK_START_RE = /^ {0,3}([-+*]\s|\d+[.)]\s|#{1,6}(\s|$)|>)/;
|
|
2891
|
+
var tableRow = (line) => {
|
|
2892
|
+
if (!/^ {0,3}\S/.test(line) || BLOCK_START_RE.test(line)) return null;
|
|
2893
|
+
const split = splitCells(line.trim());
|
|
2894
|
+
if (split.length < 2) return null;
|
|
2895
|
+
const inner = split.slice(line.trim().startsWith("|") ? 1 : 0);
|
|
2896
|
+
return (inner.length > 1 && inner[inner.length - 1]?.trim() === "" ? inner.slice(0, -1) : inner).map((cell) => cell.trim().replace(/\\\|/g, "|"));
|
|
2897
|
+
};
|
|
2898
|
+
var isDelimiter = (cells) => cells !== null && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
2899
|
+
var plainCell = (cell) => cell.replace(/[*_`]/g, "").trim().toLowerCase();
|
|
2900
|
+
var columnName = (cell) => plainCell(cell).split(/[\s/]+/)[0] ?? "";
|
|
2901
|
+
var idsInCell = (cell) => {
|
|
2902
|
+
const ids = cell.replace(/\([^()]*\)/g, "");
|
|
2903
|
+
const quoted = [...ids.matchAll(ID_IN_CELL_RE)].map((match) => unwrapId((match[1] ?? "").trim())).filter((id) => id !== "");
|
|
2904
|
+
const bare = ids.split(",").map((piece) => unwrapId(piece.trim())).filter((id) => id !== "");
|
|
2905
|
+
return quoted.length > 0 ? quoted : bare.every((id) => /^\S+$/.test(id)) ? bare : [];
|
|
2906
|
+
};
|
|
2907
|
+
var tableDisposition = (cell) => {
|
|
2908
|
+
const firstWord = plainCell(cell).replace(/^[^a-z]+/, "").split(/[^a-z]/)[0] ?? "";
|
|
2909
|
+
return DispositionCodec.is(firstWord) ? firstWord : "unstated";
|
|
2910
|
+
};
|
|
2911
|
+
var rowAnswers = (cells, columns) => {
|
|
2912
|
+
const dispositionCell = cells[columns.disposition] ?? "";
|
|
2913
|
+
const disposition = tableDisposition(dispositionCell);
|
|
2914
|
+
const verdictOnly = DispositionCodec.is(plainCell(dispositionCell));
|
|
2915
|
+
const reason = clipText(
|
|
2916
|
+
cells.filter(
|
|
2917
|
+
(cell, column) => cell !== "" && column !== columns.id && !(verdictOnly && column === columns.disposition)
|
|
2918
|
+
).join(" \u2014 "),
|
|
2919
|
+
RESPONSE_REASON_CLIP_CHARS
|
|
2920
|
+
);
|
|
2921
|
+
return idsInCell(cells[columns.id] ?? "").map((id) => ({ id, disposition, reason }));
|
|
2922
|
+
};
|
|
2923
|
+
var namesAColumn = (name) => ID_HEADERS.has(name) || DISPOSITION_HEADERS.has(name);
|
|
2924
|
+
var tableAnswers = (lines) => {
|
|
2925
|
+
const rows = lines.map(tableRow);
|
|
2926
|
+
return rows.reduce(
|
|
2927
|
+
(state, cells, index) => {
|
|
2928
|
+
if (state.skipDelimiter) return { ...state, skipDelimiter: false };
|
|
2929
|
+
if (cells === null) return { ...state, inTable: false, columns: null };
|
|
2930
|
+
const opensTable = !isDelimiter(cells) && isDelimiter(rows[index + 1] ?? null) && (!state.inTable || cells.map(columnName).some(namesAColumn));
|
|
2931
|
+
if (opensTable) {
|
|
2932
|
+
const header = cells.map(columnName);
|
|
2933
|
+
const id = header.findIndex((name) => ID_HEADERS.has(name));
|
|
2934
|
+
const disposition = header.findIndex((name) => DISPOSITION_HEADERS.has(name));
|
|
2935
|
+
return {
|
|
2936
|
+
...state,
|
|
2937
|
+
inTable: true,
|
|
2938
|
+
skipDelimiter: true,
|
|
2939
|
+
columns: id < 0 || disposition < 0 ? null : { id, disposition }
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
if (state.inTable && state.columns !== null && !isDelimiter(cells)) {
|
|
2943
|
+
state.answers.push(...rowAnswers(cells, state.columns));
|
|
2944
|
+
}
|
|
2945
|
+
return isDelimiter(rows[index + 1] ?? null) ? { ...state, skipDelimiter: true } : state;
|
|
2946
|
+
},
|
|
2947
|
+
{ columns: null, inTable: false, skipDelimiter: false, answers: [] }
|
|
2948
|
+
).answers;
|
|
2949
|
+
};
|
|
2950
|
+
var parseResponses = (text) => {
|
|
2951
|
+
const lines = answerLines(text);
|
|
2952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2953
|
+
return [...lineAnswers(lines), ...tableAnswers(lines)].filter((answer) => {
|
|
2954
|
+
const key2 = JSON.stringify([answer.id, answer.disposition, answer.reason]);
|
|
2955
|
+
if (seen.has(key2)) return false;
|
|
2956
|
+
seen.add(key2);
|
|
2957
|
+
return true;
|
|
2958
|
+
});
|
|
2959
|
+
};
|
|
2960
|
+
var isHuman = (login, type8, botLogin) => login !== botLogin && type8 === "User";
|
|
2961
|
+
var newestFirst = (a, b) => {
|
|
2962
|
+
const [left2, right2] = [a.created_at ?? "", b.created_at ?? ""];
|
|
2963
|
+
return left2 < right2 ? 1 : left2 > right2 ? -1 : b.id - a.id;
|
|
2964
|
+
};
|
|
2965
|
+
var answersFrom = (input) => {
|
|
2966
|
+
const comments = [...input.comments].filter((comment) => isHuman(comment.user.login, comment.user.type ?? null, input.botLogin)).sort(newestFirst).flatMap(
|
|
2967
|
+
(comment) => parseResponses(comment.body ?? "").map((line) => ({
|
|
2968
|
+
...line,
|
|
2969
|
+
channel: "comment",
|
|
2970
|
+
source_url: `https://github.com/${input.repo}/pull/${String(input.prNumber)}#issuecomment-${String(comment.id)}`,
|
|
2971
|
+
author: comment.user.login,
|
|
2972
|
+
author_association: comment.author_association ?? null,
|
|
2973
|
+
created_at: comment.created_at ?? null
|
|
2974
|
+
}))
|
|
2975
|
+
);
|
|
2976
|
+
const commits = [...input.commits].reverse().flatMap(
|
|
2977
|
+
(commit) => parseResponses(commit.message).map((line) => ({
|
|
2978
|
+
...line,
|
|
2979
|
+
channel: "commit",
|
|
2980
|
+
source_url: `https://github.com/${input.repo}/commit/${commit.sha}`,
|
|
2981
|
+
author: commit.author,
|
|
2982
|
+
author_association: null,
|
|
2983
|
+
created_at: commit.date ?? null
|
|
2984
|
+
}))
|
|
2985
|
+
);
|
|
2986
|
+
return { comments, commits };
|
|
2987
|
+
};
|
|
2988
|
+
var harvestResponses = (input) => {
|
|
2989
|
+
const { comments: fromComments, commits: fromCommits } = answersFrom(input);
|
|
2990
|
+
const matched = (response) => input.priorIds.has(response.id);
|
|
2991
|
+
const echoed = (response) => !matched(response) && ID_SHAPE_RE.test(response.id);
|
|
2992
|
+
const groups = [fromComments, fromCommits].flatMap((channel) => [
|
|
2993
|
+
channel.filter(matched),
|
|
2994
|
+
channel.filter(echoed)
|
|
2995
|
+
]);
|
|
2996
|
+
const [comments, commentEchoes, commits, commitEchoes] = groups.map(
|
|
2997
|
+
(group) => group.slice(0, RESPONSES_PER_CHANNEL)
|
|
2998
|
+
);
|
|
2999
|
+
return {
|
|
3000
|
+
file: {
|
|
3001
|
+
responses: [...comments ?? [], ...commits ?? []],
|
|
3002
|
+
unmatched: [...commentEchoes ?? [], ...commitEchoes ?? []].map(
|
|
3003
|
+
({ id, channel, source_url }) => ({ id, channel, source_url })
|
|
3004
|
+
)
|
|
3005
|
+
},
|
|
3006
|
+
dropped: groups.reduce(
|
|
3007
|
+
(total, group) => total + Math.max(0, group.length - RESPONSES_PER_CHANNEL),
|
|
3008
|
+
0
|
|
3009
|
+
)
|
|
3010
|
+
};
|
|
3011
|
+
};
|
|
3012
|
+
var isTrustedResponse = (response, headInBaseRepo) => {
|
|
3013
|
+
switch (response.channel) {
|
|
3014
|
+
case "comment":
|
|
3015
|
+
return MAINTAINER_ASSOCIATIONS.includes(response.author_association ?? "");
|
|
3016
|
+
case "commit":
|
|
3017
|
+
return headInBaseRepo;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
var answeredInstant = (response) => response.created_at === null ? Number.NaN : Date.parse(response.created_at);
|
|
3021
|
+
var closingResponses = (responses, isTrusted) => {
|
|
3022
|
+
const byId = responses.filter((response) => response.disposition !== "unstated" && isTrusted(response)).reduce((groups, response) => {
|
|
3023
|
+
const timed = { response, at: answeredInstant(response) };
|
|
3024
|
+
groups.set(response.id, [...groups.get(response.id) ?? [], timed]);
|
|
3025
|
+
return groups;
|
|
3026
|
+
}, /* @__PURE__ */ new Map());
|
|
3027
|
+
return [...byId.values()].flatMap((answers) => {
|
|
3028
|
+
if (answers.some(({ at }) => Number.isNaN(at))) return [];
|
|
3029
|
+
const latest = Math.max(...answers.map(({ at }) => at));
|
|
3030
|
+
const newest = answers.filter(({ at }) => at === latest).map(({ response }) => response);
|
|
3031
|
+
return newest.every((response) => CLOSING_DISPOSITIONS.has(response.disposition)) ? [...newest].sort((a, b) => a.source_url < b.source_url ? -1 : 1).slice(0, 1) : [];
|
|
3032
|
+
});
|
|
3033
|
+
};
|
|
2903
3034
|
|
|
2904
3035
|
// src/post.ts
|
|
2905
3036
|
var STICKY_CHAR_LIMIT = 65536;
|
|
@@ -3095,7 +3226,8 @@ var postInlineReview = async (pr, comments, inDiff, ghApi) => {
|
|
|
3095
3226
|
return { url, posted, inlinePosted, unposted };
|
|
3096
3227
|
}
|
|
3097
3228
|
};
|
|
3098
|
-
var ISSUE_COMMENTS_JQ = '.[] | {id, user: (.user.login // "(deleted)"), created_at, html_url, body: (.body // "")}';
|
|
3229
|
+
var ISSUE_COMMENTS_JQ = '.[] | {id, user: (.user.login // "(deleted)"), user_type: (.user.type // null), author_association: (.author_association // null), created_at, html_url, body: (.body // "")}';
|
|
3230
|
+
var nullableString = (value) => typeof value === "string" ? value : null;
|
|
3099
3231
|
var parseIssueCommentRows = (raw) => {
|
|
3100
3232
|
let malformed = 0;
|
|
3101
3233
|
const rows = [];
|
|
@@ -3114,7 +3246,15 @@ var parseIssueCommentRows = (raw) => {
|
|
|
3114
3246
|
const url = rec?.["html_url"];
|
|
3115
3247
|
const body = rec?.["body"];
|
|
3116
3248
|
if (typeof id === "number" && typeof author === "string" && typeof created === "string" && typeof url === "string" && typeof body === "string") {
|
|
3117
|
-
rows.push({
|
|
3249
|
+
rows.push({
|
|
3250
|
+
id,
|
|
3251
|
+
author,
|
|
3252
|
+
created,
|
|
3253
|
+
url,
|
|
3254
|
+
body,
|
|
3255
|
+
authorType: nullableString(rec?.["user_type"]),
|
|
3256
|
+
association: nullableString(rec?.["author_association"])
|
|
3257
|
+
});
|
|
3118
3258
|
} else {
|
|
3119
3259
|
malformed += 1;
|
|
3120
3260
|
}
|
|
@@ -3124,6 +3264,45 @@ var parseIssueCommentRows = (raw) => {
|
|
|
3124
3264
|
);
|
|
3125
3265
|
return { rows, malformed };
|
|
3126
3266
|
};
|
|
3267
|
+
var COMMITS_JQ = ".[] | {sha, message: .commit.message, author: (.author.login // null), date: (.commit.author.date // null)}";
|
|
3268
|
+
var fetchCommitAnswersSource = async (repo, prNumber, ghApi) => {
|
|
3269
|
+
try {
|
|
3270
|
+
const raw = await ghApi([
|
|
3271
|
+
`repos/${repo}/pulls/${String(prNumber)}/commits?per_page=100`,
|
|
3272
|
+
"--paginate",
|
|
3273
|
+
"--jq",
|
|
3274
|
+
COMMITS_JQ
|
|
3275
|
+
]);
|
|
3276
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
3277
|
+
const commits = lines.flatMap((line) => {
|
|
3278
|
+
const parsed = tryParseJson(line.trim());
|
|
3279
|
+
const rec = parsed.ok ? asRecord(parsed.value) : null;
|
|
3280
|
+
const sha = rec?.["sha"];
|
|
3281
|
+
const message = rec?.["message"];
|
|
3282
|
+
return typeof sha === "string" && typeof message === "string" ? [
|
|
3283
|
+
{
|
|
3284
|
+
sha,
|
|
3285
|
+
message,
|
|
3286
|
+
author: nullableString(rec?.["author"]),
|
|
3287
|
+
date: nullableString(rec?.["date"])
|
|
3288
|
+
}
|
|
3289
|
+
] : [];
|
|
3290
|
+
});
|
|
3291
|
+
if (commits.length < lines.length) {
|
|
3292
|
+
process.stderr.write(
|
|
3293
|
+
`Warning: ${String(lines.length - commits.length)} of the PR's commits did not decode \u2014 their answers close nothing
|
|
3294
|
+
`
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
return commits;
|
|
3298
|
+
} catch (err) {
|
|
3299
|
+
process.stderr.write(
|
|
3300
|
+
`Warning: could not read the PR's commits (${errMsg(err)}) \u2014 no commit answer closes a finding this round
|
|
3301
|
+
`
|
|
3302
|
+
);
|
|
3303
|
+
return [];
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3127
3306
|
var fetchIssueCommentRows = async (repo, prNumber, ghApi) => {
|
|
3128
3307
|
const raw = await ghApi([
|
|
3129
3308
|
// per_page rides the QUERY string — a `-f` field would flip `gh api` from GET to POST
|
|
@@ -3194,7 +3373,6 @@ var discussionRows = (rows, stickyId, botLogin) => {
|
|
|
3194
3373
|
return c.id !== stickyId && c.author !== botLogin;
|
|
3195
3374
|
});
|
|
3196
3375
|
};
|
|
3197
|
-
var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
3198
3376
|
var mentionsOutsideKnown = (reachable, currentIds) => {
|
|
3199
3377
|
const known = new Set(currentIds);
|
|
3200
3378
|
const escapedToRaw = escapedIdIndex(currentIds);
|
|
@@ -3279,24 +3457,6 @@ var buildStickyDiscussion = (reachable, currentIds, priorIds) => {
|
|
|
3279
3457
|
orphanedTruncated: Object.fromEntries(orphanedTruncated)
|
|
3280
3458
|
};
|
|
3281
3459
|
};
|
|
3282
|
-
var priorIdsFrom = (doc) => {
|
|
3283
|
-
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
|
|
3284
|
-
const rec = doc;
|
|
3285
|
-
const idsOf = (field, systemic) => (Array.isArray(rec[field]) ? rec[field] : []).flatMap((raw) => {
|
|
3286
|
-
const item = typeof raw === "object" && raw !== null ? asRecord(raw) : null;
|
|
3287
|
-
if (item === null) return [];
|
|
3288
|
-
const title = typeof item["title"] === "string" ? item["title"] : "";
|
|
3289
|
-
const resolved = resolveRuleId({
|
|
3290
|
-
...typeof item["id"] === "string" ? { id: item["id"] } : {},
|
|
3291
|
-
...typeof item["code"] === "string" ? { code: item["code"] } : {},
|
|
3292
|
-
...typeof item["path"] === "string" ? { path: item["path"] } : {},
|
|
3293
|
-
title
|
|
3294
|
-
});
|
|
3295
|
-
const id = resolved ?? (systemic && title !== "" ? synthesizedSystemicId(title) : void 0);
|
|
3296
|
-
return id !== void 0 ? [id] : [];
|
|
3297
|
-
});
|
|
3298
|
-
return [...idsOf("findings", false), ...idsOf("systemic_problems", true)];
|
|
3299
|
-
};
|
|
3300
3460
|
var appendBestEffort = (path, label, body) => {
|
|
3301
3461
|
if (path === void 0 || path === "") return;
|
|
3302
3462
|
try {
|
|
@@ -3391,8 +3551,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
3391
3551
|
const nodes = conn?.nodes;
|
|
3392
3552
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
3393
3553
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
3394
|
-
const ids = nodes.flatMap((
|
|
3395
|
-
const cnodes =
|
|
3554
|
+
const ids = nodes.flatMap((t9) => {
|
|
3555
|
+
const cnodes = t9.comments?.nodes;
|
|
3396
3556
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
3397
3557
|
});
|
|
3398
3558
|
return { ids, truncated };
|
|
@@ -3512,7 +3672,7 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3512
3672
|
const logAnsweredDrops = () => {
|
|
3513
3673
|
if (verbatimReRaised.length > 0) {
|
|
3514
3674
|
process.stderr.write(
|
|
3515
|
-
`${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each finding
|
|
3675
|
+
`${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each finding
|
|
3516
3676
|
`
|
|
3517
3677
|
);
|
|
3518
3678
|
}
|
|
@@ -3546,6 +3706,18 @@ ${dropNote}` : ""}`,
|
|
|
3546
3706
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
3547
3707
|
const inlineRequested = input.inline === true;
|
|
3548
3708
|
const envelope = loadEnvelope(input.envelopePath);
|
|
3709
|
+
const effectiveRoute = input.route ?? envelope?.route;
|
|
3710
|
+
const runRenderInput = {
|
|
3711
|
+
prices: decodedPrices.right,
|
|
3712
|
+
pricesProvided: input.pricesProvided,
|
|
3713
|
+
template,
|
|
3714
|
+
route: effectiveRoute,
|
|
3715
|
+
reviewedSha: input.headSha,
|
|
3716
|
+
effort: input.effort,
|
|
3717
|
+
runUrl: input.runUrl,
|
|
3718
|
+
jsonUrl: input.jsonUrl,
|
|
3719
|
+
postedAt: input.postedAt
|
|
3720
|
+
};
|
|
3549
3721
|
const reachable = existingSticky !== null ? discussionRows(commentRows, existingSticky.id, input.botLogin) : [];
|
|
3550
3722
|
const priorIsMechanic = existingSticky !== null && (parseReviewedRoute(existingSticky.body) === "mechanic" || parseMechanicAncestor(existingSticky.body));
|
|
3551
3723
|
const noticeDiscussion = async () => {
|
|
@@ -3569,7 +3741,7 @@ ${dropNote}` : ""}`,
|
|
|
3569
3741
|
};
|
|
3570
3742
|
};
|
|
3571
3743
|
const renderNotice = (message, discussion) => {
|
|
3572
|
-
const noticeConvergence =
|
|
3744
|
+
const noticeConvergence = effectiveRoute === "mechanic" ? mechanicConvergence(
|
|
3573
3745
|
priorConv,
|
|
3574
3746
|
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3575
3747
|
) : priorConv;
|
|
@@ -3582,22 +3754,14 @@ ${dropNote}` : ""}`,
|
|
|
3582
3754
|
};
|
|
3583
3755
|
return formatMarkdown(
|
|
3584
3756
|
render({
|
|
3757
|
+
...runRenderInput,
|
|
3585
3758
|
findings: findings2,
|
|
3586
3759
|
envelope: null,
|
|
3587
3760
|
incomplete: true,
|
|
3588
|
-
prices: decodedPrices.right,
|
|
3589
|
-
pricesProvided: input.pricesProvided,
|
|
3590
|
-
template,
|
|
3591
|
-
route: input.route,
|
|
3592
|
-
reviewedSha: input.headSha,
|
|
3593
|
-
effort: input.effort,
|
|
3594
3761
|
sameRootNotes: {},
|
|
3595
3762
|
roundCount: priorRoundCount,
|
|
3596
3763
|
convergenceRound: false,
|
|
3597
|
-
runUrl: input.runUrl,
|
|
3598
|
-
jsonUrl: input.jsonUrl,
|
|
3599
3764
|
findingsPointer: noticeFindingsPointer(findings2),
|
|
3600
|
-
postedAt: input.postedAt,
|
|
3601
3765
|
orphanedDiscussion: discussion.orphanedDiscussion,
|
|
3602
3766
|
orphanedTotal: discussion.orphanedTotal,
|
|
3603
3767
|
orphanedTruncated: discussion.orphanedTruncated,
|
|
@@ -3633,8 +3797,40 @@ ${dropNote}` : ""}`,
|
|
|
3633
3797
|
process.exit(0);
|
|
3634
3798
|
}
|
|
3635
3799
|
const loadedFindings = findingsResult.findings;
|
|
3636
|
-
const
|
|
3637
|
-
const
|
|
3800
|
+
const answerable = existingSticky !== null && !priorIsMechanic && loadedFindings.findings.length > 0;
|
|
3801
|
+
const headInBaseRepo = answerable && resolution.headRepo !== null && resolution.headRepo.toLowerCase() === input.repo.toLowerCase();
|
|
3802
|
+
const answers = answerable ? answersFrom({
|
|
3803
|
+
repo: input.repo,
|
|
3804
|
+
prNumber,
|
|
3805
|
+
botLogin: input.botLogin,
|
|
3806
|
+
comments: commentRows.map((row) => ({
|
|
3807
|
+
id: row.id,
|
|
3808
|
+
body: row.body,
|
|
3809
|
+
user: { login: row.author, type: row.authorType },
|
|
3810
|
+
created_at: row.created,
|
|
3811
|
+
author_association: row.association
|
|
3812
|
+
})),
|
|
3813
|
+
commits: headInBaseRepo ? await fetchCommitAnswersSource(input.repo, prNumber, ghApi) : []
|
|
3814
|
+
}) : { comments: [], commits: [] };
|
|
3815
|
+
const closures = closingResponses(
|
|
3816
|
+
[...answers.comments, ...answers.commits],
|
|
3817
|
+
(response) => isTrustedResponse(response, headInBaseRepo)
|
|
3818
|
+
).filter((closure) => couldMatch(closure.id, loadedFindings.findings));
|
|
3819
|
+
const priorForAnswers = closures.length > 0 && existingSticky !== null ? { value: await resolvePriorFindings(existingSticky.body, readArtifact) } : null;
|
|
3820
|
+
const answeredPrior = priorForAnswers === null ? null : resolveTolerantFindings(priorForAnswers.value);
|
|
3821
|
+
if (priorForAnswers !== null && answeredPrior === null) {
|
|
3822
|
+
process.stderr.write(
|
|
3823
|
+
`Warning: ${String(closures.length)} maintainer answer(s) close prior findings, but the prior review's findings did not resolve \u2014 no re-raise is treated as answered
|
|
3824
|
+
`
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
const answeredRegistry = answeredRegistryFrom(closures, answeredPrior);
|
|
3828
|
+
if (answeredPrior !== null && answeredRegistry.length < closures.length) {
|
|
3829
|
+
process.stderr.write(
|
|
3830
|
+
`Warning: ${String(closures.length - answeredRegistry.length)} maintainer answer(s) name no finding the prior review reported \u2014 they close nothing this round
|
|
3831
|
+
`
|
|
3832
|
+
);
|
|
3833
|
+
}
|
|
3638
3834
|
const answeredFilter = applyAnswered(loadedFindings.findings, answeredRegistry);
|
|
3639
3835
|
const reRaisedNotes = answeredFilter.reRaisedNotes;
|
|
3640
3836
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
@@ -3662,7 +3858,7 @@ ${dropNote}` : ""}`,
|
|
|
3662
3858
|
const roundHasNit = findings.findings.some((f) => f.severity === "nit");
|
|
3663
3859
|
const nitWantsPrior = roundHasNit && existingSticky !== null && (isFullReviewSticky(existingSticky.body) || isFullReviewAncestry(existingSticky.body));
|
|
3664
3860
|
const broadWantsPrior = existingSticky !== null && !priorIsMechanic && mentionsOutsideKnown(reachable, broadCurrentIds);
|
|
3665
|
-
const resolvedPrior = existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
3861
|
+
const resolvedPrior = priorForAnswers !== null ? priorForAnswers.value : existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
3666
3862
|
const priorDocForNits = nitWantsPrior ? resolvedPrior : null;
|
|
3667
3863
|
const priorSuppressedKeys = new Set(
|
|
3668
3864
|
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
@@ -3694,7 +3890,6 @@ ${dropNote}` : ""}`,
|
|
|
3694
3890
|
const orphanedTruncated = stickyDiscussion.orphanedTruncated;
|
|
3695
3891
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
3696
3892
|
const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
|
|
3697
|
-
const effectiveRoute = input.route ?? envelope?.route;
|
|
3698
3893
|
const shedPriority = (f) => SEVERITIES.length - 1 - SEVERITIES.indexOf(f.severity);
|
|
3699
3894
|
const shedOrderOf = (list) => list.map((f, index) => ({ f, index })).sort((a, b) => shedPriority(a.f) - shedPriority(b.f) || a.index - b.index).map((entry) => entry.f);
|
|
3700
3895
|
const sizeSticky = (renderAt, total, reserve = SHED_RESERVE) => {
|
|
@@ -3763,6 +3958,33 @@ ${findingsPointer2}`
|
|
|
3763
3958
|
${note}
|
|
3764
3959
|
`;
|
|
3765
3960
|
};
|
|
3961
|
+
const sharedRenderInput = {
|
|
3962
|
+
...runRenderInput,
|
|
3963
|
+
repo: input.headRepo || input.repo,
|
|
3964
|
+
testReport,
|
|
3965
|
+
clocDiff,
|
|
3966
|
+
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3967
|
+
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3968
|
+
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3969
|
+
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3970
|
+
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3971
|
+
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3972
|
+
// must hold on the surface that lists findings without an inline review.
|
|
3973
|
+
suppressedNits,
|
|
3974
|
+
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3975
|
+
answeredNotes: reRaisedNotes,
|
|
3976
|
+
answeredReRaiseNote: answeredDropNote,
|
|
3977
|
+
discussionByFinding,
|
|
3978
|
+
orphanedDiscussion,
|
|
3979
|
+
orphanedTotal,
|
|
3980
|
+
discussionTruncated,
|
|
3981
|
+
orphanedTruncated,
|
|
3982
|
+
orphanedUnresolvable: orphanResolveFailed,
|
|
3983
|
+
convergenceThreshold: input.convergenceThreshold,
|
|
3984
|
+
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3985
|
+
pricedAt: input.pricedAt,
|
|
3986
|
+
summaryAvailable
|
|
3987
|
+
};
|
|
3766
3988
|
if (envelope === null) {
|
|
3767
3989
|
const envelopelessIncomplete = isIncompleteFindings(findings);
|
|
3768
3990
|
if (wouldBuryCompleted(envelopelessIncomplete)) {
|
|
@@ -3779,44 +4001,14 @@ ${note}
|
|
|
3779
4001
|
) : priorConv
|
|
3780
4002
|
);
|
|
3781
4003
|
const lostInput = {
|
|
4004
|
+
...sharedRenderInput,
|
|
3782
4005
|
findings: stampedFindings2,
|
|
3783
4006
|
envelope: null,
|
|
3784
4007
|
incomplete: envelopelessIncomplete,
|
|
3785
|
-
prices: decodedPrices.right,
|
|
3786
|
-
pricesProvided: input.pricesProvided,
|
|
3787
|
-
template,
|
|
3788
|
-
route: effectiveRoute,
|
|
3789
|
-
reviewedSha: input.headSha,
|
|
3790
|
-
repo: input.headRepo || input.repo,
|
|
3791
|
-
effort: input.effort,
|
|
3792
4008
|
sameRootNotes: {},
|
|
3793
|
-
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
3794
|
-
// findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
|
|
3795
|
-
// carry them) so the kept re-raises' annotations actually render, and names the drops
|
|
3796
|
-
// exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
|
|
3797
|
-
// here too (issue #164): below-floor nits are hidden from the human list and shown only in the
|
|
3798
|
-
// collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
|
|
3799
|
-
// must hold on the surface that lists findings without an inline review.
|
|
3800
|
-
suppressedNits,
|
|
3801
|
-
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3802
|
-
answeredNotes: reRaisedNotes,
|
|
3803
|
-
answeredReRaiseNote: answeredDropNote,
|
|
3804
4009
|
roundCount: priorRoundCount,
|
|
3805
4010
|
convergenceRound: false,
|
|
3806
|
-
|
|
3807
|
-
clocDiff,
|
|
3808
|
-
runUrl: input.runUrl,
|
|
3809
|
-
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3810
|
-
jsonUrl: input.jsonUrl,
|
|
3811
|
-
findingsPointer: findingsBlob(stampedFindings2),
|
|
3812
|
-
postedAt: input.postedAt,
|
|
3813
|
-
summaryAvailable,
|
|
3814
|
-
discussionByFinding,
|
|
3815
|
-
orphanedDiscussion,
|
|
3816
|
-
orphanedTotal,
|
|
3817
|
-
discussionTruncated,
|
|
3818
|
-
orphanedTruncated,
|
|
3819
|
-
orphanedUnresolvable: orphanResolveFailed
|
|
4011
|
+
findingsPointer: findingsBlob(stampedFindings2)
|
|
3820
4012
|
};
|
|
3821
4013
|
const lostRender = (opts) => formatMarkdown(
|
|
3822
4014
|
render({
|
|
@@ -3894,7 +4086,6 @@ ${note}
|
|
|
3894
4086
|
}
|
|
3895
4087
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
3896
4088
|
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
3897
|
-
const currentCounts = computeSeverityCounts(findings.findings);
|
|
3898
4089
|
const currentCodes = computeIdCounts(findings.findings, findings.systemic_problems ?? []);
|
|
3899
4090
|
const roundNumber = priorRoundCount + 1;
|
|
3900
4091
|
const convergence = isRound ? buildConvergence(
|
|
@@ -3914,41 +4105,15 @@ ${note}
|
|
|
3914
4105
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
3915
4106
|
const findingsMarker = findingsBlob(stampedFindings);
|
|
3916
4107
|
const commonRenderInput = {
|
|
4108
|
+
...sharedRenderInput,
|
|
3917
4109
|
findings: stampedFindings,
|
|
3918
4110
|
envelope,
|
|
3919
4111
|
incomplete: thisIncomplete,
|
|
3920
|
-
prices: decodedPrices.right,
|
|
3921
|
-
pricesProvided: input.pricesProvided,
|
|
3922
|
-
template,
|
|
3923
|
-
route: effectiveRoute,
|
|
3924
|
-
reviewedSha: input.headSha,
|
|
3925
|
-
repo: input.headRepo || input.repo,
|
|
3926
|
-
effort: input.effort,
|
|
3927
|
-
testReport,
|
|
3928
|
-
clocDiff,
|
|
3929
|
-
severityCounts: currentCounts,
|
|
3930
4112
|
sameRootNotes,
|
|
3931
|
-
answeredNotes: reRaisedNotes,
|
|
3932
|
-
answeredReRaiseNote: answeredDropNote,
|
|
3933
|
-
discussionByFinding,
|
|
3934
|
-
orphanedDiscussion,
|
|
3935
|
-
orphanedTotal,
|
|
3936
|
-
discussionTruncated,
|
|
3937
|
-
orphanedTruncated,
|
|
3938
|
-
orphanedUnresolvable: orphanResolveFailed,
|
|
3939
4113
|
roundCount: currentRoundCount,
|
|
3940
|
-
convergenceThreshold: input.convergenceThreshold,
|
|
3941
|
-
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3942
4114
|
convergenceRound: isRound,
|
|
3943
4115
|
strays,
|
|
3944
|
-
|
|
3945
|
-
runUrl: input.runUrl,
|
|
3946
|
-
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
3947
|
-
jsonUrl: input.jsonUrl,
|
|
3948
|
-
findingsPointer: findingsMarker,
|
|
3949
|
-
postedAt: input.postedAt,
|
|
3950
|
-
pricedAt: input.pricedAt,
|
|
3951
|
-
summaryAvailable
|
|
4116
|
+
findingsPointer: findingsMarker
|
|
3952
4117
|
};
|
|
3953
4118
|
const longFilesNote = longFiles.length > 0 ? `
|
|
3954
4119
|
|
|
@@ -4526,6 +4691,13 @@ failing_jobs=${String(result.failingJobs)}
|
|
|
4526
4691
|
`;
|
|
4527
4692
|
}
|
|
4528
4693
|
};
|
|
4694
|
+
var FETCH_WITHOUT_DETACHED_CHILDREN = [
|
|
4695
|
+
"-c",
|
|
4696
|
+
"maintenance.auto=false",
|
|
4697
|
+
"-c",
|
|
4698
|
+
"gc.auto=0",
|
|
4699
|
+
"fetch"
|
|
4700
|
+
];
|
|
4529
4701
|
var runGit = (args) => execFileWithTimeout({
|
|
4530
4702
|
command: "git",
|
|
4531
4703
|
args,
|
|
@@ -4543,7 +4715,10 @@ var IssueCommentCodec = t.intersection([
|
|
|
4543
4715
|
t.type({
|
|
4544
4716
|
id: t.number,
|
|
4545
4717
|
body: t.union([t.string, t.null]),
|
|
4546
|
-
user: t.
|
|
4718
|
+
user: t.intersection([
|
|
4719
|
+
t.type({ login: t.string }),
|
|
4720
|
+
t.partial({ type: t.union([t.string, t.null]) })
|
|
4721
|
+
])
|
|
4547
4722
|
}),
|
|
4548
4723
|
t.partial({
|
|
4549
4724
|
created_at: t.union([t.string, t.null]),
|
|
@@ -4593,17 +4768,20 @@ var fetchFullDiff = async (repo, defaultBranch, headSha, ghApi, gitRun) => {
|
|
|
4593
4768
|
process.stderr.write(`compare diff fetch failed (${errMsg(err)}) \u2014 falling back to git diff
|
|
4594
4769
|
`);
|
|
4595
4770
|
}
|
|
4596
|
-
await gitRun([
|
|
4771
|
+
await gitRun([...FETCH_WITHOUT_DETACHED_CHILDREN, "origin", headSha]);
|
|
4597
4772
|
const base = (await gitRun(["rev-parse", "HEAD"])).trim();
|
|
4598
4773
|
return gitRun(["diff", `${base}...${headSha}`]);
|
|
4599
4774
|
};
|
|
4600
|
-
var CommitCodec = t.
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4775
|
+
var CommitCodec = t.intersection([
|
|
4776
|
+
t.type({
|
|
4777
|
+
sha: t.string,
|
|
4778
|
+
message: t.string,
|
|
4779
|
+
author: t.union([t.string, t.null]),
|
|
4780
|
+
email: t.union([t.string, t.null])
|
|
4781
|
+
}),
|
|
4782
|
+
t.partial({ date: t.union([t.string, t.null]), login: t.union([t.string, t.null]) })
|
|
4783
|
+
]);
|
|
4784
|
+
var COMMIT_JQ = ".commits[] | {sha: .sha, message: .commit.message, author: .commit.author.name, email: .commit.author.email, date: .commit.author.date, login: (.author.login // null)}";
|
|
4607
4785
|
var fetchCompareCommits = async (repo, defaultBranch, headSha, ghApi) => {
|
|
4608
4786
|
const rows = parseJsonl(
|
|
4609
4787
|
await ghApi([
|
|
@@ -4624,8 +4802,8 @@ var fetchCompareCommits = async (repo, defaultBranch, headSha, ghApi) => {
|
|
|
4624
4802
|
}
|
|
4625
4803
|
return commits;
|
|
4626
4804
|
};
|
|
4627
|
-
var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
|
|
4628
|
-
var REVIEW_COMMENT_JQ =
|
|
4805
|
+
var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login, type: .user.type}, created_at: .created_at, author_association: .author_association}";
|
|
4806
|
+
var REVIEW_COMMENT_JQ = ".[] | {body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association, path: .path, line: .line}";
|
|
4629
4807
|
var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
|
|
4630
4808
|
var ReviewCommentCodec = t.intersection([
|
|
4631
4809
|
t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
|
|
@@ -4785,7 +4963,7 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4785
4963
|
`PR diff fetch failed or was empty for ${String(meta.changed_files)} changed files \u2014 falling back to git diff
|
|
4786
4964
|
`
|
|
4787
4965
|
);
|
|
4788
|
-
await gitRun([
|
|
4966
|
+
await gitRun([...FETCH_WITHOUT_DETACHED_CHILDREN, "origin", input.headSha]);
|
|
4789
4967
|
return gitRun(["diff", meta.base_sha, input.headSha]);
|
|
4790
4968
|
})();
|
|
4791
4969
|
const fullDiff = stacked ? await fetchFullDiff(input.repo, input.defaultBranch, input.headSha, ghApi, gitRun) : prDiff;
|
|
@@ -4808,7 +4986,6 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4808
4986
|
]);
|
|
4809
4987
|
const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
|
|
4810
4988
|
const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
|
|
4811
|
-
const threadComments = decodeArrayOrNull(ThreadCommentCodec, reviewCommentRows);
|
|
4812
4989
|
const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
|
|
4813
4990
|
const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
|
|
4814
4991
|
writeFileSync(
|
|
@@ -4822,11 +4999,26 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4822
4999
|
join(input.outDir, "prior_findings.json"),
|
|
4823
5000
|
priorFindings === null ? "null" : JSON.stringify(priorFindings)
|
|
4824
5001
|
);
|
|
4825
|
-
const
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
5002
|
+
const harvest = seedsFromPrior ? harvestResponses({
|
|
5003
|
+
repo: input.repo,
|
|
5004
|
+
prNumber,
|
|
5005
|
+
botLogin: input.botLogin,
|
|
5006
|
+
priorIds: new Set(priorIdsFrom(priorFindings)),
|
|
5007
|
+
commits: commits.map((commit) => ({ ...commit, author: commit.login ?? null })),
|
|
5008
|
+
comments: issueComments ?? []
|
|
5009
|
+
}) : { file: { responses: [], unmatched: [] }, dropped: 0 };
|
|
5010
|
+
writeFileSync(join(input.outDir, "responses.json"), JSON.stringify(harvest.file));
|
|
5011
|
+
if (seedsFromPrior && priorFindings === null) {
|
|
5012
|
+
process.stderr.write(
|
|
5013
|
+
"Note: the prior review's findings did not resolve \u2014 every id-shaped answer is staged as unmatched\n"
|
|
5014
|
+
);
|
|
5015
|
+
}
|
|
5016
|
+
if (harvest.dropped > 0) {
|
|
5017
|
+
process.stderr.write(
|
|
5018
|
+
`Note: ${String(harvest.dropped)} answer(s) beyond the newest ${String(RESPONSES_PER_CHANNEL)} per channel were not staged
|
|
5019
|
+
`
|
|
5020
|
+
);
|
|
5021
|
+
}
|
|
4830
5022
|
writeFileSync(
|
|
4831
5023
|
join(input.outDir, "pr_conversation.json"),
|
|
4832
5024
|
JSON.stringify({
|
|
@@ -5153,7 +5345,6 @@ var composeReviewSettings = (opts) => {
|
|
|
5153
5345
|
};
|
|
5154
5346
|
|
|
5155
5347
|
// src/sandbox.ts
|
|
5156
|
-
var GITHUB_HOSTS = ["api.github.com", "github.com"];
|
|
5157
5348
|
var deriveModelHost = (apiBaseUrl) => {
|
|
5158
5349
|
const host = URL.canParse(apiBaseUrl) ? new URL(apiBaseUrl).hostname : "";
|
|
5159
5350
|
if (host === "") {
|
|
@@ -5173,11 +5364,7 @@ var parseExtraEndpoints = (extra) => extra.split(/\s+/).map((token) => token.rep
|
|
|
5173
5364
|
var buildSandboxConfig = (opts) => ({
|
|
5174
5365
|
network: {
|
|
5175
5366
|
allowedDomains: [
|
|
5176
|
-
.../* @__PURE__ */ new Set([
|
|
5177
|
-
deriveModelHost(opts.apiBaseUrl),
|
|
5178
|
-
...GITHUB_HOSTS,
|
|
5179
|
-
...parseExtraEndpoints(opts.extra ?? "")
|
|
5180
|
-
])
|
|
5367
|
+
.../* @__PURE__ */ new Set([deriveModelHost(opts.apiBaseUrl), ...parseExtraEndpoints(opts.extra ?? "")])
|
|
5181
5368
|
],
|
|
5182
5369
|
deniedDomains: []
|
|
5183
5370
|
},
|
|
@@ -5195,7 +5382,7 @@ var parseScope = (raw) => {
|
|
|
5195
5382
|
reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
|
|
5196
5383
|
};
|
|
5197
5384
|
}
|
|
5198
|
-
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((
|
|
5385
|
+
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t9) => t9 !== "")));
|
|
5199
5386
|
return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
|
|
5200
5387
|
};
|
|
5201
5388
|
|
|
@@ -5823,18 +6010,19 @@ var deadlineCmd = defineCommand({
|
|
|
5823
6010
|
}
|
|
5824
6011
|
});
|
|
5825
6012
|
var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredVersion(raw) : void 0;
|
|
6013
|
+
var liveSchemaPin = (kind, schemaPath, requestedVersion) => kind === "findings" && schemaPath === schemaPathFor(kind) ? livePinFor(kind, requestedVersion) : void 0;
|
|
5826
6014
|
var printableSchema = (schemaPath, pinVersion) => {
|
|
5827
6015
|
const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
|
|
5828
6016
|
const enforcementSchema = Object.fromEntries(
|
|
5829
6017
|
Object.entries(schema).filter(([key2]) => key2 !== "$schema")
|
|
5830
6018
|
);
|
|
5831
|
-
if (
|
|
6019
|
+
if (pinVersion === void 0) return JSON.stringify(enforcementSchema, null, 2);
|
|
5832
6020
|
const properties = enforcementSchema["properties"];
|
|
5833
6021
|
const schemaVersion = typeof properties === "object" && properties !== null ? properties["schema_version"] : void 0;
|
|
5834
6022
|
if (typeof schemaVersion === "object" && schemaVersion !== null) {
|
|
5835
6023
|
properties["schema_version"] = {
|
|
5836
6024
|
...schemaVersion,
|
|
5837
|
-
pattern: anchoredSchemaVersionPattern(
|
|
6025
|
+
pattern: anchoredSchemaVersionPattern(pinVersion)
|
|
5838
6026
|
};
|
|
5839
6027
|
}
|
|
5840
6028
|
return JSON.stringify(enforcementSchema, null, 2);
|
|
@@ -5870,7 +6058,8 @@ var validateCmd = defineCommand({
|
|
|
5870
6058
|
run: async ({ args }) => {
|
|
5871
6059
|
const kind = requireSchemaKind(args.kind || "findings");
|
|
5872
6060
|
const documentRaw = readJSON(args.document);
|
|
5873
|
-
const
|
|
6061
|
+
const requestedVersion = args["schema-version"] || derivedSchemaVersion(kind, documentRaw);
|
|
6062
|
+
const schemaPath = args.schema ? resolve(args.schema) : requireSchemaPath(kind, requestedVersion);
|
|
5874
6063
|
const { valid, errors } = validateAgainstSchema(documentRaw, schemaPath);
|
|
5875
6064
|
if (valid) {
|
|
5876
6065
|
process.stdout.write("\u2705 valid\n");
|
|
@@ -5882,7 +6071,7 @@ var validateCmd = defineCommand({
|
|
|
5882
6071
|
process.stderr.write(
|
|
5883
6072
|
`
|
|
5884
6073
|
The ${kind} document must conform to this schema (the field descriptions are the authoritative spec \u2014 match the property names exactly):
|
|
5885
|
-
${printableSchema(schemaPath, kind
|
|
6074
|
+
${printableSchema(schemaPath, liveSchemaPin(kind, schemaPath, requestedVersion))}
|
|
5886
6075
|
`
|
|
5887
6076
|
);
|
|
5888
6077
|
}
|
|
@@ -5906,10 +6095,6 @@ var seedDraftCmd = defineCommand({
|
|
|
5906
6095
|
type: "string",
|
|
5907
6096
|
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"
|
|
5908
6097
|
},
|
|
5909
|
-
"prior-answers": {
|
|
5910
|
-
type: "string",
|
|
5911
|
-
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"
|
|
5912
|
-
},
|
|
5913
6098
|
"nit-visibility-floor": {
|
|
5914
6099
|
type: "string",
|
|
5915
6100
|
description: "The nit visibility floor (issue #164), matched to the commenter's: the prior review's below-floor nits (confidence \xD7 likelihood below it) are re-derived from --prior and delivered to the .prior-suppressed sidecar as adjudicated context, so the next-round agent does not re-raise them as fresh nits; best-effort, never fails the seed. Empty \u21D2 the default"
|
|
@@ -5985,55 +6170,17 @@ var seedDraftCmd = defineCommand({
|
|
|
5985
6170
|
const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
|
|
5986
6171
|
isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
|
|
5987
6172
|
);
|
|
5988
|
-
const answeredRegistry = (() => {
|
|
5989
|
-
if (!args["prior-answers"]) return null;
|
|
5990
|
-
try {
|
|
5991
|
-
const raw = JSON.parse(readFileSync(resolve(args["prior-answers"]), "utf-8"));
|
|
5992
|
-
if (!Array.isArray(raw)) throw new Error("expected an array");
|
|
5993
|
-
const decoded = raw.flatMap((row) => {
|
|
5994
|
-
const entry = decodeAnsweredEntry(row);
|
|
5995
|
-
return entry === null ? [] : [entry];
|
|
5996
|
-
});
|
|
5997
|
-
if (decoded.length < raw.length) {
|
|
5998
|
-
process.stderr.write(
|
|
5999
|
-
`Warning: ${String(raw.length - decoded.length)} of ${String(raw.length)} answered-registry row(s) failed to decode \u2014 the seed's answered state is incomplete
|
|
6000
|
-
`
|
|
6001
|
-
);
|
|
6002
|
-
}
|
|
6003
|
-
return decoded;
|
|
6004
|
-
} catch (err) {
|
|
6005
|
-
process.stderr.write(
|
|
6006
|
-
`Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
|
|
6007
|
-
`
|
|
6008
|
-
);
|
|
6009
|
-
return null;
|
|
6010
|
-
}
|
|
6011
|
-
})();
|
|
6012
6173
|
const priorFindings = (() => {
|
|
6013
6174
|
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
6014
6175
|
return strippedPrior;
|
|
6015
6176
|
const resolved = resolvedPriorValue(strippedPrior);
|
|
6016
|
-
const doc = resolved === null ? strippedPrior :
|
|
6017
|
-
...resolved,
|
|
6018
|
-
findings: answeredRegistry !== null && answeredRegistry.length > 0 ? resolved.findings.filter((f) => {
|
|
6019
|
-
const resolvedId = resolveFindingId(f);
|
|
6020
|
-
return !answeredRegistry.some((e) => isAnsweredDrop(resolvedId, f, e));
|
|
6021
|
-
}) : resolved.findings
|
|
6022
|
-
};
|
|
6177
|
+
const doc = resolved === null ? strippedPrior : resolved;
|
|
6023
6178
|
const carried = doc["scope_metastasis"];
|
|
6024
6179
|
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
6025
6180
|
if (doc["verdict"] === "error") return doc;
|
|
6026
6181
|
const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
|
|
6027
6182
|
return computed === null ? doc : { ...doc, scope_metastasis: computed };
|
|
6028
6183
|
})();
|
|
6029
|
-
if (answeredRegistry !== null) {
|
|
6030
|
-
writeFileSync(priorAnswersPath(outPath), `${JSON.stringify(answeredRegistry, null, 2)}
|
|
6031
|
-
`);
|
|
6032
|
-
process.stderr.write(
|
|
6033
|
-
`Seeded ${priorAnswersPath(outPath)} with ${String(answeredRegistry.length)} answered finding(s) as context
|
|
6034
|
-
`
|
|
6035
|
-
);
|
|
6036
|
-
}
|
|
6037
6184
|
if (parsedPrior !== null && isFullReviewAncestry(priorBody ?? "")) {
|
|
6038
6185
|
try {
|
|
6039
6186
|
const belowFloor = priorBelowFloorNits(
|
|
@@ -6065,7 +6212,9 @@ var seedDraftCmd = defineCommand({
|
|
|
6065
6212
|
) : priorFindings;
|
|
6066
6213
|
const accepts = (doc) => {
|
|
6067
6214
|
const resolved = resolvedPriorValue(doc);
|
|
6068
|
-
|
|
6215
|
+
if (resolved === null) return null;
|
|
6216
|
+
const carried = withoutRebuttals(resolved);
|
|
6217
|
+
return validateAgainstSchema(carried, schemaPath).valid ? carried : null;
|
|
6069
6218
|
};
|
|
6070
6219
|
const seedDoc = accepts(priorFindings) ?? (() => {
|
|
6071
6220
|
const bare = accepts(barePrior);
|
|
@@ -6353,7 +6502,7 @@ var printSchemaCmd = defineCommand({
|
|
|
6353
6502
|
const schemaKind = requireSchemaKind(args.name);
|
|
6354
6503
|
const schemaPath = requireSchemaPath(schemaKind, args["schema-version"]);
|
|
6355
6504
|
process.stdout.write(
|
|
6356
|
-
`${printableSchema(schemaPath, schemaKind
|
|
6505
|
+
`${printableSchema(schemaPath, liveSchemaPin(schemaKind, schemaPath, args["schema-version"]))}
|
|
6357
6506
|
`
|
|
6358
6507
|
);
|
|
6359
6508
|
}
|
|
@@ -6969,7 +7118,7 @@ var checkScopeCmd = defineCommand({
|
|
|
6969
7118
|
var sandboxConfigCmd = defineCommand({
|
|
6970
7119
|
meta: {
|
|
6971
7120
|
name: "sandbox-config",
|
|
6972
|
-
description: "Emit the sandbox-runtime (srt) settings that jail the untrusted review agent's egress: allow the model host (derived from api_base_url)
|
|
7121
|
+
description: "Emit the sandbox-runtime (srt) settings that jail the untrusted review agent's egress: allow the model host (derived from api_base_url) and the consumer's extra_endpoints; deny all else; filesystem isolation off"
|
|
6973
7122
|
},
|
|
6974
7123
|
args: {
|
|
6975
7124
|
"api-base-url": {
|