@jphutchins/code-review 0.1.0-alpha.54 → 0.1.0-alpha.56
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 +2 -2
- package/dist/index.js +919 -200
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +6 -2
- package/schema/findings.schema.json +17 -16
- package/schema/v0.9/findings.schema.json +418 -0
- package/templates/comment.eta +47 -12
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from 'citty';
|
|
3
3
|
import { readFileSync, writeFileSync, copyFileSync, statSync, readdirSync, appendFileSync } from 'fs';
|
|
4
|
-
import { randomBytes } from 'crypto';
|
|
4
|
+
import { randomBytes, createHash } from 'crypto';
|
|
5
5
|
import { resolve as resolve$1, join, dirname, basename, extname, sep } from 'path';
|
|
6
6
|
import { Eta } from 'eta';
|
|
7
7
|
import * as t from 'io-ts';
|
|
@@ -74,35 +74,43 @@ var UriString = t.refinement(
|
|
|
74
74
|
(s) => !/\s/.test(s) && URL.canParse(s),
|
|
75
75
|
"UriString"
|
|
76
76
|
);
|
|
77
|
-
var
|
|
78
|
-
code: t.string,
|
|
77
|
+
var RuleUrlCodec = t.partial({
|
|
79
78
|
code_url: UriString
|
|
80
79
|
});
|
|
81
|
-
var
|
|
82
|
-
t.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
]);
|
|
80
|
+
var FindingRequired = t.type({
|
|
81
|
+
id: t.string,
|
|
82
|
+
path: t.string,
|
|
83
|
+
start_line: LineNumber,
|
|
84
|
+
end_line: LineNumber,
|
|
85
|
+
severity: SeverityCodec,
|
|
86
|
+
title: t.string,
|
|
87
|
+
description: t.string,
|
|
88
|
+
reasoning: t.string,
|
|
89
|
+
confidence: Confidence,
|
|
90
|
+
likelihood: Likelihood
|
|
91
|
+
});
|
|
92
|
+
var FindingOptional = t.partial({
|
|
93
|
+
side: SideCodec,
|
|
94
|
+
recommendation: t.string,
|
|
95
|
+
patch: t.string
|
|
96
|
+
});
|
|
97
|
+
var FindingShape = t.intersection([FindingRequired, RuleUrlCodec, FindingOptional]);
|
|
100
98
|
var EndGeStart = t.refinement(
|
|
101
99
|
FindingShape,
|
|
102
100
|
(f) => f.end_line >= f.start_line,
|
|
103
101
|
"EndGeStart"
|
|
104
102
|
);
|
|
105
|
-
var
|
|
103
|
+
var FINDING_KEYS = /* @__PURE__ */ new Set([
|
|
104
|
+
...Object.keys(FindingRequired.props),
|
|
105
|
+
...Object.keys(RuleUrlCodec.props),
|
|
106
|
+
...Object.keys(FindingOptional.props)
|
|
107
|
+
]);
|
|
108
|
+
var FindingStrict = t.refinement(
|
|
109
|
+
EndGeStart,
|
|
110
|
+
(f) => Object.keys(f).every((k) => FINDING_KEYS.has(k)),
|
|
111
|
+
"FindingStrict"
|
|
112
|
+
);
|
|
113
|
+
var FindingCodec = t.exact(FindingStrict);
|
|
106
114
|
var SystemicRequired = t.type({
|
|
107
115
|
title: t.string,
|
|
108
116
|
description: t.string,
|
|
@@ -112,13 +120,14 @@ var SystemicRequired = t.type({
|
|
|
112
120
|
likelihood: Likelihood
|
|
113
121
|
});
|
|
114
122
|
var SystemicOptional = t.partial({
|
|
115
|
-
|
|
123
|
+
id: t.string,
|
|
124
|
+
finding_ids: t.array(t.string),
|
|
116
125
|
paths: t.array(t.string)
|
|
117
126
|
});
|
|
118
|
-
var SystemicProblemShape = t.intersection([SystemicRequired,
|
|
127
|
+
var SystemicProblemShape = t.intersection([SystemicRequired, RuleUrlCodec, SystemicOptional]);
|
|
119
128
|
var SYSTEMIC_KEYS = /* @__PURE__ */ new Set([
|
|
120
129
|
...Object.keys(SystemicRequired.props),
|
|
121
|
-
...Object.keys(
|
|
130
|
+
...Object.keys(RuleUrlCodec.props),
|
|
122
131
|
...Object.keys(SystemicOptional.props)
|
|
123
132
|
]);
|
|
124
133
|
var SystemicProblemStrict = t.refinement(
|
|
@@ -128,7 +137,7 @@ var SystemicProblemStrict = t.refinement(
|
|
|
128
137
|
);
|
|
129
138
|
var SystemicProblemCodec = t.exact(SystemicProblemStrict);
|
|
130
139
|
var RecurringShape = t.type({
|
|
131
|
-
|
|
140
|
+
id: t.string,
|
|
132
141
|
consecutive_rounds: t.refinement(
|
|
133
142
|
t.number,
|
|
134
143
|
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
@@ -162,14 +171,25 @@ var RoundNumber = t.refinement(
|
|
|
162
171
|
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
163
172
|
"RoundNumber"
|
|
164
173
|
);
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
var idFrequencyCodec = (name) => new t.Type(
|
|
175
|
+
name,
|
|
176
|
+
(u) => typeof u === "object" && u !== null && !Array.isArray(u),
|
|
177
|
+
(u, c) => {
|
|
178
|
+
if (typeof u !== "object" || u === null || Array.isArray(u)) return t.failure(u, c);
|
|
179
|
+
const entries = [];
|
|
180
|
+
for (const [k, v] of Object.entries(u)) {
|
|
181
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0) return t.failure(v, c);
|
|
182
|
+
entries.push([k, v]);
|
|
183
|
+
}
|
|
184
|
+
return t.success(Object.fromEntries(entries));
|
|
185
|
+
},
|
|
186
|
+
(a) => a
|
|
168
187
|
);
|
|
188
|
+
var IdFrequency = idFrequencyCodec("IdFrequency");
|
|
169
189
|
var ConvergenceRoundRequired = t.type({ round: RoundNumber });
|
|
170
190
|
var ConvergenceRoundOptional = t.partial({
|
|
171
191
|
score: FiniteNumber,
|
|
172
|
-
|
|
192
|
+
ids: IdFrequency,
|
|
173
193
|
sha: t.string
|
|
174
194
|
});
|
|
175
195
|
var ConvergenceRoundShape = t.intersection([ConvergenceRoundRequired, ConvergenceRoundOptional]);
|
|
@@ -236,6 +256,188 @@ var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
|
|
|
236
256
|
...PIPELINE_STAMPED_FIELDS,
|
|
237
257
|
"change_size"
|
|
238
258
|
]);
|
|
259
|
+
var synthesizedId = (parts, prefix) => `${prefix}${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 12)}`;
|
|
260
|
+
var synthesizedFindingId = (path, title) => synthesizedId([path, title], "f-");
|
|
261
|
+
var synthesizedSystemicId = (title) => synthesizedId([title], "s-");
|
|
262
|
+
var isSynthesizedFindingId = (id) => /^f-[A-Za-z0-9_-]{12}$/.test(id);
|
|
263
|
+
var resolveFindingId = (f) => f.id !== "" ? f.id : synthesizedFindingId(f.path, f.title);
|
|
264
|
+
var resolveRuleId = (rec) => rec.id !== void 0 && rec.id !== "" ? rec.id : rec.code !== void 0 && rec.code !== "" ? rec.code : rec.path !== void 0 ? synthesizedFindingId(rec.path, rec.title) : void 0;
|
|
265
|
+
var usableCountsMap = (v) => {
|
|
266
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
267
|
+
const entries = Object.entries(v).filter(
|
|
268
|
+
(e) => typeof e[1] === "number" && Number.isSafeInteger(e[1]) && e[1] > 0
|
|
269
|
+
);
|
|
270
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
271
|
+
};
|
|
272
|
+
var LegacyRuleCodec = t.partial({
|
|
273
|
+
code: t.string,
|
|
274
|
+
id: t.string,
|
|
275
|
+
code_url: UriString
|
|
276
|
+
});
|
|
277
|
+
var FindingShapeV09 = t.intersection([
|
|
278
|
+
t.type({
|
|
279
|
+
path: t.string,
|
|
280
|
+
start_line: LineNumber,
|
|
281
|
+
end_line: LineNumber,
|
|
282
|
+
severity: SeverityCodec,
|
|
283
|
+
title: t.string,
|
|
284
|
+
description: t.string,
|
|
285
|
+
reasoning: t.string,
|
|
286
|
+
confidence: Confidence,
|
|
287
|
+
likelihood: Likelihood
|
|
288
|
+
}),
|
|
289
|
+
LegacyRuleCodec,
|
|
290
|
+
t.partial({
|
|
291
|
+
side: SideCodec,
|
|
292
|
+
recommendation: t.string,
|
|
293
|
+
patch: t.string
|
|
294
|
+
})
|
|
295
|
+
]);
|
|
296
|
+
var EndGeStartV09 = t.refinement(
|
|
297
|
+
FindingShapeV09,
|
|
298
|
+
(f) => f.end_line >= f.start_line,
|
|
299
|
+
"EndGeStartV09"
|
|
300
|
+
);
|
|
301
|
+
var FindingCodecV09 = t.exact(EndGeStartV09);
|
|
302
|
+
var SystemicV09Optional = t.partial({
|
|
303
|
+
finding_codes: t.array(t.string),
|
|
304
|
+
finding_ids: t.array(t.string),
|
|
305
|
+
paths: t.array(t.string)
|
|
306
|
+
});
|
|
307
|
+
var SystemicV09Shape = t.intersection([SystemicRequired, LegacyRuleCodec, SystemicV09Optional]);
|
|
308
|
+
var SYSTEMIC_V09_KEYS = /* @__PURE__ */ new Set([
|
|
309
|
+
...Object.keys(SystemicRequired.props),
|
|
310
|
+
...Object.keys(LegacyRuleCodec.props),
|
|
311
|
+
...Object.keys(SystemicV09Optional.props)
|
|
312
|
+
]);
|
|
313
|
+
var SystemicV09Strict = t.refinement(
|
|
314
|
+
SystemicV09Shape,
|
|
315
|
+
(s) => Object.keys(s).every((k) => SYSTEMIC_V09_KEYS.has(k)),
|
|
316
|
+
"SystemicV09Strict"
|
|
317
|
+
);
|
|
318
|
+
var SystemicProblemCodecV09 = t.exact(SystemicV09Strict);
|
|
319
|
+
var RecurringV09Shape = t.intersection([
|
|
320
|
+
t.partial({ code: t.string, id: t.string }),
|
|
321
|
+
t.type({
|
|
322
|
+
consecutive_rounds: t.refinement(
|
|
323
|
+
t.number,
|
|
324
|
+
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
325
|
+
"ConsecutiveRoundsV09"
|
|
326
|
+
),
|
|
327
|
+
start_round: t.refinement(
|
|
328
|
+
t.number,
|
|
329
|
+
(n) => Number.isSafeInteger(n) && n >= 1,
|
|
330
|
+
"StartRoundV09"
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
]);
|
|
334
|
+
var ScopeMetastasisV09Shape = t.type({
|
|
335
|
+
decision_prompt: t.string,
|
|
336
|
+
recurring: t.array(RecurringV09Shape)
|
|
337
|
+
});
|
|
338
|
+
var SCOPE_METASTASIS_V09_KEYS = new Set(Object.keys(ScopeMetastasisV09Shape.props));
|
|
339
|
+
var ScopeMetastasisV09Strict = t.refinement(
|
|
340
|
+
ScopeMetastasisV09Shape,
|
|
341
|
+
(s) => Object.keys(s).every((k) => SCOPE_METASTASIS_V09_KEYS.has(k)),
|
|
342
|
+
"ScopeMetastasisV09Strict"
|
|
343
|
+
);
|
|
344
|
+
var ScopeMetastasisCodecV09 = t.exact(ScopeMetastasisV09Strict);
|
|
345
|
+
var CodeFrequencyV09 = idFrequencyCodec("CodeFrequencyV09");
|
|
346
|
+
var ConvergenceRoundV09Shape = t.intersection([
|
|
347
|
+
t.type({ round: RoundNumber }),
|
|
348
|
+
t.partial({ score: FiniteNumber, codes: CodeFrequencyV09, ids: CodeFrequencyV09, sha: t.string })
|
|
349
|
+
]);
|
|
350
|
+
var CONVERGENCE_ROUND_V09_KEYS = /* @__PURE__ */ new Set(["round", "score", "codes", "ids", "sha"]);
|
|
351
|
+
var ConvergenceRoundV09Strict = t.refinement(
|
|
352
|
+
ConvergenceRoundV09Shape,
|
|
353
|
+
(r) => Object.keys(r).every((k) => CONVERGENCE_ROUND_V09_KEYS.has(k)),
|
|
354
|
+
"ConvergenceRoundV09Strict"
|
|
355
|
+
);
|
|
356
|
+
var ConvergenceRoundCodecV09 = t.exact(ConvergenceRoundV09Strict);
|
|
357
|
+
var ConvergenceV09Shape = t.intersection([
|
|
358
|
+
ConvergenceCoreShape,
|
|
359
|
+
t.partial({ rounds: t.array(ConvergenceRoundCodecV09) })
|
|
360
|
+
]);
|
|
361
|
+
var CONVERGENCE_V09_KEYS = /* @__PURE__ */ new Set([...Object.keys(ConvergenceCoreShape.props), "rounds"]);
|
|
362
|
+
var ConvergenceV09Strict = t.refinement(
|
|
363
|
+
ConvergenceV09Shape,
|
|
364
|
+
(c) => Object.keys(c).every((k) => CONVERGENCE_V09_KEYS.has(k)),
|
|
365
|
+
"ConvergenceV09Strict"
|
|
366
|
+
);
|
|
367
|
+
var ConvergenceCodecV09 = t.exact(ConvergenceV09Strict);
|
|
368
|
+
var FindingsV09Shape = t.intersection([
|
|
369
|
+
t.type({
|
|
370
|
+
// Any pre-0.10 minor — the registry dispatches on major.minor before decoding, so the codec
|
|
371
|
+
// accepts every legacy patch version (0.4.x through 0.9.x) through the one tolerant shape.
|
|
372
|
+
// SchemaVersion keeps the F3 strictness: a patch-less "0.4" or over-long "0.4.0.0" still fails
|
|
373
|
+
// the codec gate exactly as the ajv gate rejects it.
|
|
374
|
+
schema_version: SchemaVersion,
|
|
375
|
+
summary: t.string,
|
|
376
|
+
verdict: VerdictCodec,
|
|
377
|
+
findings: t.array(FindingCodecV09)
|
|
378
|
+
}),
|
|
379
|
+
t.partial({
|
|
380
|
+
systemic_problems: t.array(SystemicProblemCodecV09),
|
|
381
|
+
scope_metastasis: ScopeMetastasisCodecV09,
|
|
382
|
+
convergence: ConvergenceCodecV09,
|
|
383
|
+
change_size: ChangeSizeCodec
|
|
384
|
+
})
|
|
385
|
+
]);
|
|
386
|
+
var FindingsCodecV09 = t.exact(FindingsV09Shape);
|
|
387
|
+
var normalizeV09 = (doc) => {
|
|
388
|
+
const findings = doc.findings.map(({ code, id, ...f }) => ({
|
|
389
|
+
...f,
|
|
390
|
+
id: resolveRuleId({ id, code, path: f.path, title: f.title }) ?? synthesizedFindingId(f.path, f.title)
|
|
391
|
+
}));
|
|
392
|
+
const systemic_problems = doc.systemic_problems?.map(
|
|
393
|
+
({ code, id, finding_codes, finding_ids, ...s }) => ({
|
|
394
|
+
...s,
|
|
395
|
+
id: resolveRuleId({ id, code, title: s.title }) ?? synthesizedSystemicId(s.title),
|
|
396
|
+
...finding_ids !== void 0 && finding_ids.length > 0 ? { finding_ids } : finding_codes !== void 0 ? { finding_ids: finding_codes } : {}
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
const scope_metastasis = doc.scope_metastasis === void 0 ? void 0 : {
|
|
400
|
+
decision_prompt: doc.scope_metastasis.decision_prompt,
|
|
401
|
+
// A legacy recurring item carrying neither code nor id names nothing — drop it rather than
|
|
402
|
+
// synthesize an id with nothing to key it on.
|
|
403
|
+
recurring: doc.scope_metastasis.recurring.flatMap((r) => {
|
|
404
|
+
const carried = r.id !== void 0 && r.id !== "" ? r.id : r.code;
|
|
405
|
+
return carried === void 0 || carried === "" ? [] : [
|
|
406
|
+
{
|
|
407
|
+
id: carried,
|
|
408
|
+
consecutive_rounds: r.consecutive_rounds,
|
|
409
|
+
start_round: r.start_round
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
})
|
|
413
|
+
};
|
|
414
|
+
const convergence = doc.convergence === void 0 ? void 0 : {
|
|
415
|
+
score: doc.convergence.score,
|
|
416
|
+
threshold: doc.convergence.threshold,
|
|
417
|
+
converged: doc.convergence.converged,
|
|
418
|
+
...doc.convergence.rounds !== void 0 ? {
|
|
419
|
+
rounds: doc.convergence.rounds.map((r) => {
|
|
420
|
+
const ids = usableCountsMap(r.ids) ?? usableCountsMap(r.codes);
|
|
421
|
+
return {
|
|
422
|
+
round: r.round,
|
|
423
|
+
...r.score !== void 0 ? { score: r.score } : {},
|
|
424
|
+
...ids !== void 0 ? { ids } : {},
|
|
425
|
+
...r.sha !== void 0 ? { sha: r.sha } : {}
|
|
426
|
+
};
|
|
427
|
+
})
|
|
428
|
+
} : {}
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
432
|
+
summary: doc.summary,
|
|
433
|
+
verdict: doc.verdict,
|
|
434
|
+
findings,
|
|
435
|
+
...systemic_problems !== void 0 ? { systemic_problems } : {},
|
|
436
|
+
...scope_metastasis !== void 0 ? { scope_metastasis } : {},
|
|
437
|
+
...convergence !== void 0 ? { convergence } : {},
|
|
438
|
+
...doc.change_size !== void 0 ? { change_size: doc.change_size } : {}
|
|
439
|
+
};
|
|
440
|
+
};
|
|
239
441
|
var TriageCodec = t.type({
|
|
240
442
|
safe: t.boolean,
|
|
241
443
|
reasons: t.string
|
|
@@ -344,7 +546,7 @@ var TestSummaryCodec = t.intersection([
|
|
|
344
546
|
failures: t.array(TestFailureCodec)
|
|
345
547
|
})
|
|
346
548
|
]);
|
|
347
|
-
var DEFAULT_SCHEMA_VERSION = "0.
|
|
549
|
+
var DEFAULT_SCHEMA_VERSION = "0.10.0";
|
|
348
550
|
var incompleteFindings = (summary) => ({
|
|
349
551
|
schema_version: DEFAULT_SCHEMA_VERSION,
|
|
350
552
|
summary,
|
|
@@ -599,11 +801,17 @@ var parseReviewedRoute = (body) => ROUTE_RE.exec(body)?.[1] || null;
|
|
|
599
801
|
var isFullReviewSticky = (body) => {
|
|
600
802
|
const route = parseReviewedRoute(body);
|
|
601
803
|
if (route === "full review") return true;
|
|
602
|
-
if (route === "mechanic") return false;
|
|
804
|
+
if (route === "mechanic" || parseMechanicAncestor(body)) return false;
|
|
603
805
|
return priorTrajectory(parseFindingsMarker(body), body).length > 0;
|
|
604
806
|
};
|
|
807
|
+
var MECHANIC_ANCESTOR_MARKER = "<!-- review-mechanic-ancestor -->";
|
|
808
|
+
var parseMechanicAncestor = (body) => new RegExp(`(^|
|
|
809
|
+
)${MECHANIC_ANCESTOR_MARKER}(
|
|
810
|
+
|$)`).test(body);
|
|
605
811
|
var COMPLETED_ANCESTOR_MARKER = "<!-- review-complete-ancestor -->";
|
|
606
|
-
var parseCompletedAncestor = (body) =>
|
|
812
|
+
var parseCompletedAncestor = (body) => new RegExp(`(^|
|
|
813
|
+
)${COMPLETED_ANCESTOR_MARKER}(
|
|
814
|
+
|$)`).test(body);
|
|
607
815
|
var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
|
|
608
816
|
var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
|
|
609
817
|
var parseFindingsMarker = (body) => {
|
|
@@ -617,24 +825,21 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
|
|
|
617
825
|
const v = u[k];
|
|
618
826
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
619
827
|
});
|
|
620
|
-
var
|
|
621
|
-
var
|
|
828
|
+
var MAX_IDS_PER_ROUND = 8;
|
|
829
|
+
var hasId = (ids, code) => ids !== void 0 && Object.prototype.hasOwnProperty.call(ids, code);
|
|
622
830
|
var escapeCodeBackticks = (code) => code.replace(/`/g, "-").replace(/\r?\n/g, " ");
|
|
623
|
-
var
|
|
624
|
-
|
|
625
|
-
const entries = Object.entries(codes).filter(
|
|
626
|
-
(e) => typeof e[1] === "number" && Number.isSafeInteger(e[1]) && e[1] > 0
|
|
627
|
-
);
|
|
831
|
+
var normalizeIdCounts = (ids, priorCodes) => {
|
|
832
|
+
const entries = Object.entries(usableCountsMap(ids) ?? {});
|
|
628
833
|
if (entries.length === 0) return void 0;
|
|
629
834
|
const sorted = entries.sort((a, b) => {
|
|
630
835
|
if (b[1] !== a[1]) return b[1] - a[1];
|
|
631
|
-
const aPrior =
|
|
632
|
-
const bPrior =
|
|
836
|
+
const aPrior = hasId(priorCodes, a[0]) ? 1 : 0;
|
|
837
|
+
const bPrior = hasId(priorCodes, b[0]) ? 1 : 0;
|
|
633
838
|
if (aPrior !== bPrior) return bPrior - aPrior;
|
|
634
839
|
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
635
840
|
});
|
|
636
|
-
const base = sorted.slice(0,
|
|
637
|
-
const priorKept = sorted.slice(
|
|
841
|
+
const base = sorted.slice(0, MAX_IDS_PER_ROUND);
|
|
842
|
+
const priorKept = sorted.slice(MAX_IDS_PER_ROUND).filter(([code]) => hasId(priorCodes, code)).slice(0, MAX_IDS_PER_ROUND);
|
|
638
843
|
return Object.fromEntries([...base, ...priorKept]);
|
|
639
844
|
};
|
|
640
845
|
var parseRounds = (body) => {
|
|
@@ -646,13 +851,13 @@ var parseRounds = (body) => {
|
|
|
646
851
|
let priorCodes;
|
|
647
852
|
for (const u of decoded.filter(isSeverityCounts)) {
|
|
648
853
|
const rec = u;
|
|
649
|
-
const codes =
|
|
854
|
+
const codes = normalizeIdCounts(rec["ids"], priorCodes) ?? normalizeIdCounts(rec["codes"], priorCodes);
|
|
650
855
|
priorCodes = codes;
|
|
651
856
|
const sha = rec["sha"];
|
|
652
857
|
const shaStr = typeof sha === "string" && sha !== "" ? sha : void 0;
|
|
653
858
|
const round = rec["round"];
|
|
654
859
|
const roundNum = typeof round === "number" && Number.isSafeInteger(round) && round >= 1 ? round : void 0;
|
|
655
|
-
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 };
|
|
860
|
+
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, ids: codes };
|
|
656
861
|
const record3 = shaStr === void 0 ? base : { ...base, sha: shaStr };
|
|
657
862
|
kept.push(roundNum === void 0 ? record3 : { ...record3, round: roundNum });
|
|
658
863
|
}
|
|
@@ -666,29 +871,31 @@ var roundsSummary = (rounds, count = rounds.length) => {
|
|
|
666
871
|
const trajectory = cells.length === 0 ? "" : rounds.length > TRAJECTORY_SCORES ? `\u2026 \u2192 ${cells.join(" \u2192 ")}` : cells.join(" \u2192 ");
|
|
667
872
|
return trajectory === "" ? `**Round ${String(count)}**` : `**Round ${String(count)}** \xB7 ${trajectory}`;
|
|
668
873
|
};
|
|
669
|
-
var
|
|
874
|
+
var computeIdCounts = (findings, systemic = []) => {
|
|
670
875
|
const counts = /* @__PURE__ */ new Map();
|
|
671
876
|
for (const code of [
|
|
672
|
-
|
|
673
|
-
|
|
877
|
+
// resolveFindingId: an EMPTY id counts under the same synthesized key the answered match uses —
|
|
878
|
+
// one finding can never be simultaneously tracked (dropped-by-synthesis) and uncounted.
|
|
879
|
+
...findings.map((f) => resolveFindingId(f)),
|
|
880
|
+
...systemic.flatMap((s) => [s.id, ...s.finding_ids ?? []])
|
|
674
881
|
]) {
|
|
675
882
|
if (code === void 0 || code === "") continue;
|
|
676
883
|
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
677
884
|
}
|
|
678
885
|
return Object.fromEntries(counts);
|
|
679
886
|
};
|
|
680
|
-
var
|
|
887
|
+
var consecutiveIdStreaks = (rounds) => {
|
|
681
888
|
const entries = [];
|
|
682
889
|
if (rounds.length === 0) return {};
|
|
683
|
-
const lastCodes = rounds[rounds.length - 1]?.
|
|
890
|
+
const lastCodes = rounds[rounds.length - 1]?.ids;
|
|
684
891
|
if (lastCodes === void 0) return {};
|
|
685
892
|
for (const code of Object.keys(lastCodes)) {
|
|
686
893
|
let streak = 0;
|
|
687
894
|
let startIndex = rounds.length;
|
|
688
895
|
for (let i = rounds.length - 1; i >= 0; i--) {
|
|
689
|
-
const codes = rounds[i]?.
|
|
690
|
-
if (codes === void 0 || !
|
|
691
|
-
if (i > 0 && rounds[i]?.sha !== void 0 && rounds[i]?.sha === rounds[i - 1]?.sha &&
|
|
896
|
+
const codes = rounds[i]?.ids;
|
|
897
|
+
if (codes === void 0 || !hasId(codes, code)) break;
|
|
898
|
+
if (i > 0 && rounds[i]?.sha !== void 0 && rounds[i]?.sha === rounds[i - 1]?.sha && hasId(rounds[i - 1]?.ids, code)) {
|
|
692
899
|
continue;
|
|
693
900
|
}
|
|
694
901
|
streak += 1;
|
|
@@ -702,12 +909,12 @@ var consecutiveCodeStreaks = (rounds) => {
|
|
|
702
909
|
};
|
|
703
910
|
var DEFAULT_METASTASIS_STREAK = 3;
|
|
704
911
|
var SCOPE_METASTASIS_DECISION_PROMPT = "Findings keep recurring in the same mechanism across consecutive rounds \u2014 each fix keeps enabling the next finding in that machinery. This is a decision, not a directive: state in your summary whether you are committing to the expanding scope (plan the remaining facets of the recurring mechanism(s) above as one unit) or narrowing the scope so the recurrence stops.";
|
|
705
|
-
var
|
|
912
|
+
var flaggedIdStreaks = (rounds, minStreak) => Object.entries(consecutiveIdStreaks(rounds)).filter(([, s]) => s.streak >= minStreak).sort((a, b) => b[1].streak - a[1].streak).map(([id, streak]) => ({ id, streak }));
|
|
706
913
|
var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
707
|
-
const flagged =
|
|
914
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
708
915
|
if (flagged.length === 0) return "";
|
|
709
916
|
const lines = flagged.map(
|
|
710
|
-
({
|
|
917
|
+
({ id, streak }) => `> **\`${escapeCodeBackticks(id)}\`** \u2014 findings in ${String(streak.streak)} consecutive rounds.`
|
|
711
918
|
);
|
|
712
919
|
return [
|
|
713
920
|
"> [!WARNING]",
|
|
@@ -716,27 +923,27 @@ var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
|
716
923
|
].join("\n");
|
|
717
924
|
};
|
|
718
925
|
var computeScopeMetastasis = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
719
|
-
const flagged =
|
|
926
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
720
927
|
if (flagged.length === 0) return null;
|
|
721
928
|
return {
|
|
722
929
|
decision_prompt: SCOPE_METASTASIS_DECISION_PROMPT,
|
|
723
|
-
recurring: flagged.map(({
|
|
724
|
-
|
|
930
|
+
recurring: flagged.map(({ id, streak }) => ({
|
|
931
|
+
id,
|
|
725
932
|
consecutive_rounds: streak.streak,
|
|
726
933
|
start_round: streak.startRound
|
|
727
934
|
}))
|
|
728
935
|
};
|
|
729
936
|
};
|
|
730
937
|
var computeSameRootNotes = (priorRounds, findings, currentSha) => {
|
|
731
|
-
const codes = findings.map((f) => f
|
|
938
|
+
const codes = findings.map((f) => resolveFindingId(f));
|
|
732
939
|
const entries = [];
|
|
733
940
|
for (const code of codes) {
|
|
734
941
|
let lastRound = 0;
|
|
735
942
|
for (let i = priorRounds.length - 1; i >= 0; i--) {
|
|
736
943
|
if (currentSha !== void 0 && priorRounds[i]?.sha === currentSha) continue;
|
|
737
|
-
if (i > 0 && priorRounds[i]?.sha !== void 0 && priorRounds[i]?.sha === priorRounds[i - 1]?.sha &&
|
|
944
|
+
if (i > 0 && priorRounds[i]?.sha !== void 0 && priorRounds[i]?.sha === priorRounds[i - 1]?.sha && hasId(priorRounds[i - 1]?.ids, code))
|
|
738
945
|
continue;
|
|
739
|
-
const count = priorRounds[i]?.
|
|
946
|
+
const count = priorRounds[i]?.ids?.[code];
|
|
740
947
|
if (count !== void 0 && count > 0) {
|
|
741
948
|
lastRound = priorRounds[i]?.round ?? i + 1;
|
|
742
949
|
break;
|
|
@@ -756,6 +963,12 @@ var CRITICAL_FLOOR_MARGIN = 0.01;
|
|
|
756
963
|
var MINOR_FLOOR = 0.1;
|
|
757
964
|
var DEFAULT_CONVERGENCE_THRESHOLD = 1;
|
|
758
965
|
var convergenceFloor = (severity, threshold) => severity === "critical" ? threshold + CRITICAL_FLOOR_MARGIN : severity === "major" ? 0.5 : severity === "minor" ? MINOR_FLOOR : 0;
|
|
966
|
+
var mechanicConvergence = (prior, threshold) => prior === null ? null : {
|
|
967
|
+
score: convergenceFloor("critical", threshold),
|
|
968
|
+
threshold,
|
|
969
|
+
converged: false,
|
|
970
|
+
rounds: prior.rounds
|
|
971
|
+
};
|
|
759
972
|
var round2 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
|
|
760
973
|
var contribution = (severity, confidence, likelihood, threshold) => {
|
|
761
974
|
const floor = convergenceFloor(severity, threshold);
|
|
@@ -770,13 +983,13 @@ var convergenceScore = (doc, threshold) => round2(
|
|
|
770
983
|
0
|
|
771
984
|
)
|
|
772
985
|
);
|
|
773
|
-
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1,
|
|
986
|
+
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha) => {
|
|
774
987
|
const score = convergenceScore(doc, threshold);
|
|
775
|
-
const normalized =
|
|
988
|
+
const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
|
|
776
989
|
const current = {
|
|
777
990
|
round,
|
|
778
991
|
score,
|
|
779
|
-
...normalized !== void 0 ? {
|
|
992
|
+
...normalized !== void 0 ? { ids: { ...normalized } } : {},
|
|
780
993
|
...sha !== void 0 ? { sha } : {}
|
|
781
994
|
};
|
|
782
995
|
const rounds = [...priorRounds, current].slice(-64);
|
|
@@ -795,11 +1008,16 @@ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
|
|
|
795
1008
|
if (!isBelowVisibilityFloor(rec, floor)) continue;
|
|
796
1009
|
const title = rec["title"];
|
|
797
1010
|
if (typeof title !== "string") continue;
|
|
798
|
-
const code = typeof rec["code"] === "string" && rec["code"] !== "" ? rec["code"] : void 0;
|
|
799
1011
|
const path = typeof rec["path"] === "string" ? rec["path"] : void 0;
|
|
1012
|
+
const id = resolveRuleId({
|
|
1013
|
+
id: typeof rec["id"] === "string" ? rec["id"] : void 0,
|
|
1014
|
+
code: typeof rec["code"] === "string" ? rec["code"] : void 0,
|
|
1015
|
+
path,
|
|
1016
|
+
title
|
|
1017
|
+
});
|
|
800
1018
|
nits.push({
|
|
801
1019
|
title,
|
|
802
|
-
...
|
|
1020
|
+
...id !== void 0 ? { id } : {},
|
|
803
1021
|
...path !== void 0 ? { path } : {}
|
|
804
1022
|
});
|
|
805
1023
|
}
|
|
@@ -852,8 +1070,33 @@ var parseSurfaceSignal = (doc) => {
|
|
|
852
1070
|
convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
|
|
853
1071
|
};
|
|
854
1072
|
};
|
|
1073
|
+
var withLegacyConvergenceIds = (raw) => {
|
|
1074
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
1075
|
+
const rec = raw;
|
|
1076
|
+
const rounds = rec["rounds"];
|
|
1077
|
+
if (!Array.isArray(rounds)) return raw;
|
|
1078
|
+
const needsMigration = (r) => {
|
|
1079
|
+
if (typeof r !== "object" || r === null) return false;
|
|
1080
|
+
const round = r;
|
|
1081
|
+
return round["codes"] !== void 0 || usableCountsMap(round["ids"]) === void 0;
|
|
1082
|
+
};
|
|
1083
|
+
if (!rounds.some(needsMigration)) return raw;
|
|
1084
|
+
const mapped = rounds.map((r) => {
|
|
1085
|
+
if (typeof r !== "object" || r === null) return r;
|
|
1086
|
+
const round = r;
|
|
1087
|
+
const ids = usableCountsMap(round["ids"]) ?? usableCountsMap(round["codes"]);
|
|
1088
|
+
if (ids === void 0) {
|
|
1089
|
+
return round["ids"] !== void 0 || round["codes"] !== void 0 ? Object.fromEntries(Object.entries(round).filter(([k]) => k !== "ids" && k !== "codes")) : round;
|
|
1090
|
+
}
|
|
1091
|
+
const rest = Object.fromEntries(
|
|
1092
|
+
Object.entries(round).filter(([k]) => k !== "ids" && k !== "codes")
|
|
1093
|
+
);
|
|
1094
|
+
return { ...rest, ids };
|
|
1095
|
+
});
|
|
1096
|
+
return { ...rec, rounds: mapped };
|
|
1097
|
+
};
|
|
855
1098
|
var validStampedConvergence = (raw) => {
|
|
856
|
-
const decoded = ConvergenceCodec.decode(raw);
|
|
1099
|
+
const decoded = ConvergenceCodec.decode(withLegacyConvergenceIds(raw));
|
|
857
1100
|
return decoded._tag === "Right" && decoded.right.rounds !== void 0 && decoded.right.rounds.length > 0 ? decoded.right : null;
|
|
858
1101
|
};
|
|
859
1102
|
var parseConvergence = (priorDoc) => {
|
|
@@ -878,7 +1121,7 @@ var parseConvergenceMarker = (body) => {
|
|
|
878
1121
|
};
|
|
879
1122
|
var roundRecordsToConvergenceRounds = (records) => records.map((r, i) => ({
|
|
880
1123
|
round: r.round ?? i + 1,
|
|
881
|
-
...r.
|
|
1124
|
+
...r.ids !== void 0 ? { ids: { ...r.ids } } : {},
|
|
882
1125
|
...r.sha !== void 0 ? { sha: r.sha } : {}
|
|
883
1126
|
}));
|
|
884
1127
|
var priorTrajectory = (priorDoc, priorBody) => parseConvergence(priorDoc)?.rounds ?? parseConvergenceMarker(priorBody)?.rounds ?? roundRecordsToConvergenceRounds(parseRounds(priorBody));
|
|
@@ -903,17 +1146,36 @@ var stripSurfaceFields = (doc) => {
|
|
|
903
1146
|
);
|
|
904
1147
|
return { ...rest, schema_version: DEFAULT_SCHEMA_VERSION };
|
|
905
1148
|
};
|
|
1149
|
+
var carriedFindingsMarker = (body) => /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0] ?? null;
|
|
1150
|
+
var carriedMarkerPointer = (carried, convergence) => [
|
|
1151
|
+
`${AGENTS_STOP_DIRECTIVE}
|
|
1152
|
+
${carried}`,
|
|
1153
|
+
...convergence !== void 0 ? [convergenceMarker(convergence)] : []
|
|
1154
|
+
].join("\n");
|
|
1155
|
+
var completedAncestryMarker = (body) => parseReviewComplete(body) || parseCompletedAncestor(body) ? COMPLETED_ANCESTOR_MARKER : void 0;
|
|
1156
|
+
var carriedProvenanceMarkers = (body) => [
|
|
1157
|
+
// The placeholder keeps the REAL route marker: the seed chain requires it outright to stay
|
|
1158
|
+
// route-aware across the prose swap.
|
|
1159
|
+
ROUTE_RE.exec(body)?.[0],
|
|
1160
|
+
parseMechanicAncestor(body) ? MECHANIC_ANCESTOR_MARKER : void 0,
|
|
1161
|
+
completedAncestryMarker(body)
|
|
1162
|
+
].filter((m) => m !== void 0).join("\n\n");
|
|
1163
|
+
var isFullReviewAncestry = (body) => parseReviewedRoute(body) === "full review" || parseReviewedRoute(body) === null && parseCompletedAncestor(body) && !parseMechanicAncestor(body);
|
|
1164
|
+
var carriedAncestryMarkers = (body) => [
|
|
1165
|
+
// A notice body carries no route marker, so the second hop reads the ancestor marker itself:
|
|
1166
|
+
// mechanic ancestry survives notice-over-notice, not just notice-over-sticky.
|
|
1167
|
+
parseReviewedRoute(body) === "mechanic" || parseMechanicAncestor(body) ? MECHANIC_ANCESTOR_MARKER : void 0,
|
|
1168
|
+
completedAncestryMarker(body)
|
|
1169
|
+
].filter((m) => m !== void 0).join("\n\n");
|
|
906
1170
|
var carryForwardMarkers = (body) => {
|
|
907
|
-
const findings =
|
|
1171
|
+
const findings = carriedFindingsMarker(body);
|
|
908
1172
|
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
909
|
-
const reviewedRoute = ROUTE_RE.exec(body)?.[0];
|
|
910
1173
|
const convergence = CONVERGENCE_RE.exec(body)?.[0];
|
|
911
1174
|
const rounds = ROUNDS_RE.exec(body)?.[0];
|
|
912
1175
|
const signal = SIGNAL_RE.exec(body)?.[0];
|
|
913
|
-
const completedAncestor = parseReviewComplete(body) || parseCompletedAncestor(body) ? COMPLETED_ANCESTOR_MARKER : void 0;
|
|
914
1176
|
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
915
1177
|
${findings}` : void 0;
|
|
916
|
-
return [findingsBlock, reviewedSha,
|
|
1178
|
+
return [findingsBlock, reviewedSha, carriedProvenanceMarkers(body), convergence, rounds, signal].filter((m) => m !== void 0 && m !== "").join("\n\n");
|
|
917
1179
|
};
|
|
918
1180
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
919
1181
|
var projectPatch = (patch, surface) => {
|
|
@@ -928,6 +1190,17 @@ var reviewBodyPointer = (headSha, stickyUrl, runUrl, runHasSummary) => {
|
|
|
928
1190
|
const run2 = runUrl ? ` See the [workflow run](${runUrl}) for ${runHasSummary ? "this round's review in full, its job log," : "the job log"} and the findings artifact.` : "";
|
|
929
1191
|
return `\u{1F916} Automated code review for \`${sha7}\` \u2014 see ${summary} for the verdict, walkthrough, and cost.${run2}`;
|
|
930
1192
|
};
|
|
1193
|
+
|
|
1194
|
+
// src/comment-chain.ts
|
|
1195
|
+
function* ancestors(start, parentOf) {
|
|
1196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1197
|
+
let current = start;
|
|
1198
|
+
while (current !== null && !seen.has(current)) {
|
|
1199
|
+
yield current;
|
|
1200
|
+
seen.add(current);
|
|
1201
|
+
current = parentOf(current);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
931
1204
|
var numField = (rec, key2) => {
|
|
932
1205
|
const v = rec[key2];
|
|
933
1206
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
|
|
@@ -1094,15 +1367,14 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1094
1367
|
const byId = /* @__PURE__ */ new Map();
|
|
1095
1368
|
for (const c of ordered) byId.set(c.id, c);
|
|
1096
1369
|
const rootOf = (c) => {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return current.in_reply_to_id === null ? current : null;
|
|
1370
|
+
const chain = [
|
|
1371
|
+
...ancestors(
|
|
1372
|
+
c,
|
|
1373
|
+
(n) => n.in_reply_to_id === null ? null : byId.get(n.in_reply_to_id) ?? null
|
|
1374
|
+
)
|
|
1375
|
+
];
|
|
1376
|
+
const root = chain[chain.length - 1];
|
|
1377
|
+
return root !== void 0 && root.in_reply_to_id === null ? root : null;
|
|
1106
1378
|
};
|
|
1107
1379
|
const findingOf = (root) => {
|
|
1108
1380
|
const decoded = parseFindingsMarker(root.body ?? "");
|
|
@@ -1112,12 +1384,22 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1112
1384
|
const title = first["title"];
|
|
1113
1385
|
const description = first["description"];
|
|
1114
1386
|
const reasoning = first["reasoning"];
|
|
1115
|
-
const
|
|
1387
|
+
const id = first["id"];
|
|
1388
|
+
const legacyCode = first["code"];
|
|
1116
1389
|
const severity = first["severity"];
|
|
1117
1390
|
const path = first["path"];
|
|
1118
1391
|
const patch = first["patch"];
|
|
1119
1392
|
return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
|
|
1120
|
-
|
|
1393
|
+
// resolveRuleId: the ONE legacy-spelling precedence the upcast, this reader, and the
|
|
1394
|
+
// below-floor nit reader share — a pre-id marker (or one written before the migration)
|
|
1395
|
+
// carries `code`; a codeless one resolves to the same synthesized id the registry's
|
|
1396
|
+
// legacy upcast derives, so the entry keys to the identical claim on the next round.
|
|
1397
|
+
code: resolveRuleId({
|
|
1398
|
+
id: typeof id === "string" ? id : void 0,
|
|
1399
|
+
code: typeof legacyCode === "string" ? legacyCode : void 0,
|
|
1400
|
+
path,
|
|
1401
|
+
title
|
|
1402
|
+
}) ?? synthesizedFindingId(path, title),
|
|
1121
1403
|
title,
|
|
1122
1404
|
description,
|
|
1123
1405
|
reasoning,
|
|
@@ -1159,29 +1441,31 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1159
1441
|
for (const entry of [...entries].sort(
|
|
1160
1442
|
(a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
|
|
1161
1443
|
)) {
|
|
1162
|
-
const key2 = answeredNoteKey(entry);
|
|
1444
|
+
const key2 = answeredNoteKey({ id: entry.code, title: entry.title });
|
|
1163
1445
|
if (!byKey.has(key2)) byKey.set(key2, entry);
|
|
1164
1446
|
}
|
|
1165
1447
|
return [...byKey.values()];
|
|
1166
1448
|
};
|
|
1167
|
-
var matches = (f, e) =>
|
|
1449
|
+
var matches = (f, e) => e.code === resolveFindingId(f) || isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1168
1450
|
var isVerbatimReRaise = (f, e) => f.title === e.title && f.description === e.description && f.reasoning === e.reasoning && f.severity === e.severity && f.path === e.path && (f.patch ?? null) === e.patch;
|
|
1169
1451
|
var isAnsweredDrop = (f, e) => matches(f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1170
|
-
var answeredNoteKey = (f) => f.
|
|
1452
|
+
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1171
1453
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1172
1454
|
var applyAnswered = (findings, registry) => {
|
|
1173
1455
|
const kept = [];
|
|
1174
1456
|
const noteEntries = [];
|
|
1175
|
-
const
|
|
1457
|
+
const droppedByEntry = /* @__PURE__ */ new Map();
|
|
1458
|
+
const droppedFindingIds = [];
|
|
1176
1459
|
let droppedCount = 0;
|
|
1177
1460
|
for (const f of findings) {
|
|
1178
|
-
const entry = registry.find((e) =>
|
|
1461
|
+
const entry = registry.find((e) => e.code === resolveFindingId(f)) ?? registry.find((e) => isSynthesizedFindingId(e.code) && e.title === f.title);
|
|
1179
1462
|
if (entry === void 0) {
|
|
1180
1463
|
kept.push(f);
|
|
1181
1464
|
continue;
|
|
1182
1465
|
}
|
|
1183
1466
|
if (isAnsweredDrop(f, entry)) {
|
|
1184
|
-
|
|
1467
|
+
droppedByEntry.set(entry.replyId, entry);
|
|
1468
|
+
droppedFindingIds.push(resolveFindingId(f));
|
|
1185
1469
|
droppedCount += 1;
|
|
1186
1470
|
} else {
|
|
1187
1471
|
kept.push(f);
|
|
@@ -1191,7 +1475,8 @@ var applyAnswered = (findings, registry) => {
|
|
|
1191
1475
|
return {
|
|
1192
1476
|
findings: kept,
|
|
1193
1477
|
reRaisedNotes: Object.fromEntries(noteEntries),
|
|
1194
|
-
verbatimReRaised: [...
|
|
1478
|
+
verbatimReRaised: [...droppedByEntry.values()],
|
|
1479
|
+
droppedFindingIds,
|
|
1195
1480
|
droppedCount
|
|
1196
1481
|
};
|
|
1197
1482
|
};
|
|
@@ -1210,9 +1495,9 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1210
1495
|
try {
|
|
1211
1496
|
const rows = parseJsonl(
|
|
1212
1497
|
await ghApi([
|
|
1213
|
-
`
|
|
1214
|
-
|
|
1215
|
-
|
|
1498
|
+
// per_page rides the QUERY string — a `-f` field would flip `gh api` from GET to POST
|
|
1499
|
+
// and 422 every answered-thread fetch.
|
|
1500
|
+
`repos/${repo}/pulls/${String(prNumber)}/comments?per_page=100`,
|
|
1216
1501
|
"--paginate",
|
|
1217
1502
|
"--jq",
|
|
1218
1503
|
THREAD_COMMENT_JQ
|
|
@@ -1254,7 +1539,44 @@ var permalinkFor = (base, f, anchor) => {
|
|
|
1254
1539
|
};
|
|
1255
1540
|
var CARRIED_TOTAL_CHARS = 4e4;
|
|
1256
1541
|
var SUPPRESSED_NIT_BLOCK_OVERHEAD = 280;
|
|
1257
|
-
var
|
|
1542
|
+
var DISCUSSION_TOTAL_CHARS = 8e3;
|
|
1543
|
+
var DISCUSSION_BLOCK_OVERHEAD = 120;
|
|
1544
|
+
var orphanedEntryHtml = (token, links, truncated) => `- **\`${token}\`**: ${links.map((d) => `[${escapeHtml(d.author)} \xB7 ${d.when}](${linkSafeUrl(d.url)})`).join(" ")}${truncated !== void 0 ? ` _(showing the ${String(PER_FINDING_LINKS)} newest of ${String(truncated)})_` : ""}`;
|
|
1545
|
+
var PER_FINDING_LINKS = 6;
|
|
1546
|
+
var DROPPED_IDS_SHOWN = 6;
|
|
1547
|
+
var budgetBySize = (items, cap, sizeOf, onDrop) => {
|
|
1548
|
+
const kept = [];
|
|
1549
|
+
const droppedItems = [];
|
|
1550
|
+
let used = 0;
|
|
1551
|
+
for (const item of items) {
|
|
1552
|
+
const size = sizeOf(item);
|
|
1553
|
+
if (used + size > cap) {
|
|
1554
|
+
droppedItems.push(item);
|
|
1555
|
+
const replaced = onDrop(item);
|
|
1556
|
+
if (replaced !== null) kept.push(replaced);
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
kept.push(item);
|
|
1560
|
+
used += size;
|
|
1561
|
+
}
|
|
1562
|
+
return { kept, droppedItems, used };
|
|
1563
|
+
};
|
|
1564
|
+
var escapeHtml = (text) => text.replace(
|
|
1565
|
+
/[&<>"']/g,
|
|
1566
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c
|
|
1567
|
+
);
|
|
1568
|
+
var discussionAsideHtml = (links, truncated, prefix) => [
|
|
1569
|
+
...links.map((d) => `${prefix}- [${escapeHtml(d.author)} \xB7 ${d.when}](${linkSafeUrl(d.url)})`),
|
|
1570
|
+
...truncated !== void 0 ? [
|
|
1571
|
+
// A blank line closes the list: the note stands apart as its own paragraph instead of
|
|
1572
|
+
// reading as a continuation of the newest bullet.
|
|
1573
|
+
prefix,
|
|
1574
|
+
`${prefix}_(showing the ${String(PER_FINDING_LINKS)} newest of ${String(
|
|
1575
|
+
truncated
|
|
1576
|
+
)} replies \u2014 the rest live on the comment thread.)_`
|
|
1577
|
+
] : []
|
|
1578
|
+
].join("\n");
|
|
1579
|
+
var sanitizeFinding = (f, answeredNotes, discussionByFinding, permalinkBase, unanchored, discussionTruncated) => {
|
|
1258
1580
|
const key2 = answeredNoteKey(f);
|
|
1259
1581
|
const anchored = permalinkBase !== void 0 && !(unanchored?.has(f) ?? false);
|
|
1260
1582
|
const permalink = permalinkBase === void 0 ? void 0 : permalinkFor(permalinkBase, f, anchored);
|
|
@@ -1262,18 +1584,23 @@ var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
|
|
|
1262
1584
|
...f,
|
|
1263
1585
|
title: escapePipes(f.title),
|
|
1264
1586
|
path: escapeCodeBackticks(f.path),
|
|
1265
|
-
|
|
1587
|
+
id: escapeCodeBackticks(f.id),
|
|
1588
|
+
// The RESOLVED id: the same-root notes are keyed on resolveFindingId (an empty id resolves to
|
|
1589
|
+
// its synthesized key), so the lookup must not miss the note on the raw id.
|
|
1590
|
+
idKey: resolveFindingId(f),
|
|
1266
1591
|
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1267
1592
|
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1268
1593
|
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
1269
1594
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1270
|
-
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1595
|
+
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : "",
|
|
1596
|
+
discussion: discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(discussionByFinding, f.id) ? discussionByFinding[f.id] ?? [] : [],
|
|
1597
|
+
...discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(discussionTruncated, f.id) ? { discussionTruncated: discussionTruncated[f.id] } : {}
|
|
1271
1598
|
};
|
|
1272
1599
|
};
|
|
1273
1600
|
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1274
|
-
var sanitizeSuppressedNit = (f) => ({
|
|
1601
|
+
var sanitizeSuppressedNit = (f, discussion, discussionTruncated) => ({
|
|
1275
1602
|
title: escapeCodeBackticks(f.title),
|
|
1276
|
-
|
|
1603
|
+
id: escapeCodeBackticks(f.id),
|
|
1277
1604
|
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1278
1605
|
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1279
1606
|
startLine: f.start_line,
|
|
@@ -1283,7 +1610,10 @@ var sanitizeSuppressedNit = (f) => ({
|
|
|
1283
1610
|
confidence: formatConfidence(f.confidence),
|
|
1284
1611
|
likelihood: formatConfidence(f.likelihood),
|
|
1285
1612
|
m: formatConfidence(f.confidence * f.likelihood),
|
|
1286
|
-
carried: carriedLines(f)
|
|
1613
|
+
carried: carriedLines(f),
|
|
1614
|
+
discussion,
|
|
1615
|
+
...discussionTruncated !== void 0 ? { discussionTruncated } : {},
|
|
1616
|
+
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncated, "> ") : ""
|
|
1287
1617
|
});
|
|
1288
1618
|
var carriedLines = (f) => [
|
|
1289
1619
|
`description: ${clipText(f.description, BODY_CLIP_CHARS)}`,
|
|
@@ -1291,13 +1621,16 @@ var carriedLines = (f) => [
|
|
|
1291
1621
|
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1292
1622
|
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1293
1623
|
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
1294
|
-
var sanitizeSystemic = (s) => ({
|
|
1624
|
+
var sanitizeSystemic = (s, discussion, discussionTruncated) => ({
|
|
1295
1625
|
...s,
|
|
1296
1626
|
title: escapePipes(s.title),
|
|
1297
|
-
|
|
1627
|
+
discussion,
|
|
1628
|
+
...discussionTruncated !== void 0 ? { discussionTruncated } : {},
|
|
1629
|
+
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncated, "") : "",
|
|
1630
|
+
...s.id !== void 0 ? { id: escapeCodeBackticks(s.id) } : {},
|
|
1298
1631
|
...s.code_url !== void 0 ? { code_url: linkSafeUrl(s.code_url) } : {},
|
|
1299
1632
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1300
|
-
...s.
|
|
1633
|
+
...s.finding_ids !== void 0 ? { finding_ids: s.finding_ids.map(escapeCodeBackticks) } : {}
|
|
1301
1634
|
});
|
|
1302
1635
|
var emptySeverityCounts = () => ({
|
|
1303
1636
|
critical: 0,
|
|
@@ -1328,16 +1661,98 @@ var render = (input) => {
|
|
|
1328
1661
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1329
1662
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
1330
1663
|
const advisoryAllowed = isFullReviewRound;
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1664
|
+
const discussionLinksFor = (id) => input.discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionByFinding, id) ? input.discussionByFinding[id] ?? [] : [];
|
|
1665
|
+
const discussionTruncatedFor = (id) => input.discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionTruncated, id) ? input.discussionTruncated[id] : void 0;
|
|
1666
|
+
const suppressedBudget = budgetBySize(
|
|
1667
|
+
(input.suppressedNits ?? []).map(
|
|
1668
|
+
(f) => sanitizeSuppressedNit(f, discussionLinksFor(f.id), discussionTruncatedFor(f.id))
|
|
1669
|
+
),
|
|
1670
|
+
CARRIED_TOTAL_CHARS,
|
|
1671
|
+
(n) => (
|
|
1672
|
+
// The fixed overhead covers the summary line's structure; the reviewer-controlled variable
|
|
1673
|
+
// parts are budgeted at their true RENDERED size — the title, the path TWICE (summary +
|
|
1674
|
+
// location line), the code/code_url the summary line renders, each carried line's `> `
|
|
1675
|
+
// blockquote prefix (length + 3: prefix + newline), the location line's line numbers and
|
|
1676
|
+
// side label, and the discussion slot's link list.
|
|
1677
|
+
n.carried.reduce((sum, line) => sum + line.length + 3, 0) + SUPPRESSED_NIT_BLOCK_OVERHEAD + n.title.length + n.path.length * 2 + // The id renders with its two wrapper backticks; the code_url adds the [](...) link form.
|
|
1678
|
+
// The id renders ONCE: a backtick span inside parens, or the []() link form.
|
|
1679
|
+
n.id.length + (n.codeUrl !== void 0 ? n.codeUrl.length + 8 : 4) + String(n.startLine).length * 2 + String(n.endLine).length + (n.side !== void 0 ? n.side.length + 2 : 0) + // The composed slot's EXACT rendered size — its `> ` blockquote prefixes included.
|
|
1680
|
+
n.discussionHtml.length
|
|
1681
|
+
),
|
|
1682
|
+
() => null
|
|
1338
1683
|
);
|
|
1339
1684
|
const permalinkBase = input.repo !== void 0 && input.repo !== "" && input.reviewedSha !== void 0 && input.reviewedSha !== "" ? `https://github.com/${input.repo}/blob/${input.reviewedSha}/` : void 0;
|
|
1340
1685
|
const unanchored = new Set(input.unanchoredStrays ?? []);
|
|
1686
|
+
const strayViews = (input.strays ?? []).map((f) => {
|
|
1687
|
+
const view = sanitizeFinding(
|
|
1688
|
+
f,
|
|
1689
|
+
input.answeredNotes,
|
|
1690
|
+
input.discussionByFinding,
|
|
1691
|
+
permalinkBase,
|
|
1692
|
+
unanchored,
|
|
1693
|
+
input.discussionTruncated
|
|
1694
|
+
);
|
|
1695
|
+
return {
|
|
1696
|
+
...view,
|
|
1697
|
+
discussionHtml: view.discussion.length > 0 ? discussionAsideHtml(view.discussion, view.discussionTruncated, "") : ""
|
|
1698
|
+
};
|
|
1699
|
+
});
|
|
1700
|
+
const orphanedPreBudget = /* @__PURE__ */ new Map();
|
|
1701
|
+
for (const [token, links] of Object.entries(input.orphanedDiscussion ?? {})) {
|
|
1702
|
+
const key2 = escapeCodeBackticks(token);
|
|
1703
|
+
const truncatedFor = input.orphanedTruncated !== void 0 && Object.prototype.hasOwnProperty.call(input.orphanedTruncated, token) ? input.orphanedTruncated[token] : void 0;
|
|
1704
|
+
const existing = orphanedPreBudget.get(key2);
|
|
1705
|
+
const htmlFor = (entryLinks, entryTruncated) => orphanedEntryHtml(key2, entryLinks, entryTruncated);
|
|
1706
|
+
if (existing === void 0) {
|
|
1707
|
+
orphanedPreBudget.set(key2, {
|
|
1708
|
+
links,
|
|
1709
|
+
...truncatedFor !== void 0 ? { truncated: truncatedFor } : {},
|
|
1710
|
+
html: htmlFor(links, truncatedFor)
|
|
1711
|
+
});
|
|
1712
|
+
} else {
|
|
1713
|
+
const mergedTotal = (existing.truncated ?? existing.links.length) + (truncatedFor ?? links.length);
|
|
1714
|
+
const merged = [...existing.links, ...links].sort(
|
|
1715
|
+
(a, b) => (b.at ?? b.when).localeCompare(a.at ?? a.when)
|
|
1716
|
+
);
|
|
1717
|
+
const mergedLinks = merged.slice(0, PER_FINDING_LINKS);
|
|
1718
|
+
const mergedTruncated = mergedTotal > PER_FINDING_LINKS ? mergedTotal : void 0;
|
|
1719
|
+
orphanedPreBudget.set(key2, {
|
|
1720
|
+
links: mergedLinks,
|
|
1721
|
+
...mergedTruncated !== void 0 ? { truncated: mergedTruncated } : {},
|
|
1722
|
+
html: htmlFor(mergedLinks, mergedTruncated)
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const orphanedBudget = budgetBySize(
|
|
1727
|
+
[...orphanedPreBudget.entries()],
|
|
1728
|
+
DISCUSSION_TOTAL_CHARS,
|
|
1729
|
+
([, entry]) => entry.html.length,
|
|
1730
|
+
() => null
|
|
1731
|
+
);
|
|
1732
|
+
const discussionBudget = budgetBySize(
|
|
1733
|
+
strayViews,
|
|
1734
|
+
DISCUSSION_TOTAL_CHARS - orphanedBudget.used,
|
|
1735
|
+
(view) => view.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + view.discussionHtml.length : 0,
|
|
1736
|
+
(view) => ({ ...view, discussion: [], discussionHtml: "" })
|
|
1737
|
+
);
|
|
1738
|
+
const systemicBudget = budgetBySize(
|
|
1739
|
+
(input.findings.systemic_problems ?? []).map(
|
|
1740
|
+
(s) => sanitizeSystemic(
|
|
1741
|
+
s,
|
|
1742
|
+
s.id !== void 0 ? discussionLinksFor(s.id) : [],
|
|
1743
|
+
s.id !== void 0 ? discussionTruncatedFor(s.id) : void 0
|
|
1744
|
+
)
|
|
1745
|
+
),
|
|
1746
|
+
DISCUSSION_TOTAL_CHARS - orphanedBudget.used - discussionBudget.used,
|
|
1747
|
+
(s) => s.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + s.discussionHtml.length : 0,
|
|
1748
|
+
(s) => ({ ...s, discussion: [], discussionHtml: "" })
|
|
1749
|
+
);
|
|
1750
|
+
const orphanedRender = {
|
|
1751
|
+
lines: orphanedBudget.kept.map(([, entry]) => entry.html),
|
|
1752
|
+
// The "(showing N of M)" total counts DISTINCT DISPLAYED entries — the pre-budget fold above
|
|
1753
|
+
// already merged twins, and the budget's own drops are named in the dropped-threads marker.
|
|
1754
|
+
total: input.orphanedTotal !== void 0 ? input.orphanedTotal - (Object.keys(input.orphanedDiscussion ?? {}).length - orphanedPreBudget.size) : orphanedPreBudget.size - orphanedBudget.droppedItems.length
|
|
1755
|
+
};
|
|
1341
1756
|
return eta.renderString(input.template, {
|
|
1342
1757
|
findings: input.findings,
|
|
1343
1758
|
envelope: input.envelope,
|
|
@@ -1358,13 +1773,20 @@ var render = (input) => {
|
|
|
1358
1773
|
postedAt: input.postedAt ?? "",
|
|
1359
1774
|
severityCounts,
|
|
1360
1775
|
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
1361
|
-
strays:
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1776
|
+
strays: discussionBudget.kept,
|
|
1777
|
+
orphanedLines: orphanedRender.lines,
|
|
1778
|
+
orphanedUnresolvable: input.orphanedUnresolvable === true,
|
|
1779
|
+
discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + orphanedBudget.droppedItems.length,
|
|
1780
|
+
discussionDroppedIds: discussionBudget.droppedItems.map((v) => clipText(escapeCodeBackticks(v.idKey), 64)).slice(0, DROPPED_IDS_SHOWN),
|
|
1781
|
+
discussionDroppedSystemicIds: systemicBudget.droppedItems.flatMap((s) => s.id !== void 0 ? [clipText(s.id, 64)] : []).slice(0, DROPPED_IDS_SHOWN),
|
|
1782
|
+
discussionDroppedOrphanedIds: orphanedBudget.droppedItems.map(([token]) => clipText(token, 64)).slice(0, DROPPED_IDS_SHOWN),
|
|
1783
|
+
discussionDroppedExtra: Math.max(0, discussionBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, systemicBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, orphanedBudget.droppedItems.length - DROPPED_IDS_SHOWN),
|
|
1784
|
+
discussionCap: PER_FINDING_LINKS,
|
|
1785
|
+
orphanedTotal: orphanedRender.total,
|
|
1786
|
+
suppressedNits: suppressedBudget.kept,
|
|
1787
|
+
carriedDroppedNits: suppressedBudget.droppedItems.length,
|
|
1366
1788
|
nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
|
|
1367
|
-
systemic:
|
|
1789
|
+
systemic: systemicBudget.kept,
|
|
1368
1790
|
unanchoredCount: input.unanchoredCount ?? 0,
|
|
1369
1791
|
inlineDisposition: input.inlineDisposition ?? null,
|
|
1370
1792
|
unverifiedNoLogs: input.unverifiedNoLogs === true,
|
|
@@ -1479,15 +1901,10 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
|
|
|
1479
1901
|
);
|
|
1480
1902
|
var buildInlineComments = (findings, diff, context) => {
|
|
1481
1903
|
const { inlineTemplate, models = [], jsonUrl, findings: fullFindings } = context;
|
|
1482
|
-
const
|
|
1483
|
-
const { inDiff, strays } = partitionFindings(findings, index);
|
|
1904
|
+
const { inDiff, strays } = context.partition ?? partitionFindings(findings, indexDiff(diff));
|
|
1484
1905
|
const eta = new Eta({ autoTrim: false });
|
|
1485
1906
|
const modelsText = formatModels(models);
|
|
1486
|
-
const noteFor = (
|
|
1487
|
-
if (notes === void 0) return "";
|
|
1488
|
-
const key2 = answeredNoteKey(f);
|
|
1489
|
-
return Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1490
|
-
};
|
|
1907
|
+
const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1491
1908
|
const comments = inDiff.map((f) => {
|
|
1492
1909
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1493
1910
|
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
@@ -1499,8 +1916,8 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1499
1916
|
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
1500
1917
|
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
1501
1918
|
} : f;
|
|
1502
|
-
const sameRootNote = noteFor(
|
|
1503
|
-
const answeredNote2 = noteFor(
|
|
1919
|
+
const sameRootNote = noteFor(context.sameRootNotes, resolveFindingId(f));
|
|
1920
|
+
const answeredNote2 = noteFor(context.answeredNotes, answeredNoteKey(f));
|
|
1504
1921
|
const comment = {
|
|
1505
1922
|
path: f.path,
|
|
1506
1923
|
line: f.end_line,
|
|
@@ -1944,33 +2361,47 @@ var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
|
|
|
1944
2361
|
The workflow asked for an unrecognized notice kind (\`${kind}\`) \u2014 the pinned code-review CLI is older than the workflow calling it (check that its version matches). Failing closed. See the workflow logs.`
|
|
1945
2362
|
);
|
|
1946
2363
|
var identity = (decoded) => decoded;
|
|
2364
|
+
var legacyFindingsCodec = FindingsCodecV09;
|
|
2365
|
+
var legacyFindingsNormalize = (doc) => normalizeV09(doc);
|
|
1947
2366
|
var findingsTable = [
|
|
1948
2367
|
{
|
|
1949
2368
|
minor: "0.4",
|
|
1950
2369
|
defaultVersion: "0.4.0",
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2370
|
+
// The frozen tolerant-in legacy schema, NOT the live 0.10 file: every ajv-gated channel
|
|
2371
|
+
// (validate --schema-version, the extraction ladder) dispatches the RAW doc's declared minor
|
|
2372
|
+
// through schemaPathFor, so the ajv gate must accept exactly what the tolerant legacy codec
|
|
2373
|
+
// accepts — the live file (id required) would reject the legacy docs the upcast promises to read.
|
|
2374
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2375
|
+
codec: legacyFindingsCodec,
|
|
2376
|
+
normalize: legacyFindingsNormalize,
|
|
1954
2377
|
latest: false
|
|
1955
2378
|
},
|
|
1956
2379
|
{
|
|
1957
2380
|
minor: "0.5",
|
|
1958
2381
|
defaultVersion: "0.5.0",
|
|
1959
|
-
schemaFile: "findings.schema.json",
|
|
1960
|
-
codec:
|
|
1961
|
-
normalize:
|
|
2382
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2383
|
+
codec: legacyFindingsCodec,
|
|
2384
|
+
normalize: legacyFindingsNormalize,
|
|
1962
2385
|
latest: false
|
|
1963
2386
|
},
|
|
1964
2387
|
{
|
|
1965
2388
|
minor: "0.6",
|
|
1966
2389
|
defaultVersion: "0.6.0",
|
|
1967
|
-
schemaFile: "findings.schema.json",
|
|
1968
|
-
codec:
|
|
1969
|
-
normalize:
|
|
2390
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2391
|
+
codec: legacyFindingsCodec,
|
|
2392
|
+
normalize: legacyFindingsNormalize,
|
|
1970
2393
|
latest: false
|
|
1971
2394
|
},
|
|
1972
2395
|
{
|
|
1973
2396
|
minor: "0.9",
|
|
2397
|
+
defaultVersion: "0.9.0",
|
|
2398
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2399
|
+
codec: legacyFindingsCodec,
|
|
2400
|
+
normalize: legacyFindingsNormalize,
|
|
2401
|
+
latest: false
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
minor: "0.10",
|
|
1974
2405
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
1975
2406
|
schemaFile: "findings.schema.json",
|
|
1976
2407
|
codec: FindingsCodec,
|
|
@@ -2064,6 +2495,13 @@ var resolvers = {
|
|
|
2064
2495
|
prices: (raw) => resolveSingleVersion("prices", raw)
|
|
2065
2496
|
};
|
|
2066
2497
|
var resolve = (kind, raw) => resolvers[kind](raw);
|
|
2498
|
+
var resolveTolerantFindings = (doc) => {
|
|
2499
|
+
const r = resolveFindings(doc);
|
|
2500
|
+
if (r.kind === "ok") return r.value;
|
|
2501
|
+
if (r.kind === "unsupported-version") return null;
|
|
2502
|
+
const legacy = FindingsCodecV09.decode(doc);
|
|
2503
|
+
return legacy._tag === "Right" ? normalizeV09(legacy.right) : null;
|
|
2504
|
+
};
|
|
2067
2505
|
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
2068
2506
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
2069
2507
|
var parseTimeoutMs = (raw, fallback) => {
|
|
@@ -2539,24 +2977,58 @@ var postInlineReview = async (pr, comments, inDiff, ghApi) => {
|
|
|
2539
2977
|
return { url, inlinePosted, unposted };
|
|
2540
2978
|
}
|
|
2541
2979
|
};
|
|
2542
|
-
var
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2980
|
+
var ISSUE_COMMENTS_JQ = '.[] | {id, user: (.user.login // "(deleted)"), created_at, html_url, body: (.body // "")}';
|
|
2981
|
+
var parseIssueCommentRows = (raw) => {
|
|
2982
|
+
let malformed = 0;
|
|
2983
|
+
const rows = [];
|
|
2984
|
+
for (const line of raw.split("\n")) {
|
|
2985
|
+
const trimmed = line.trim();
|
|
2986
|
+
if (trimmed === "") continue;
|
|
2987
|
+
const parsed = tryParseJson(trimmed);
|
|
2988
|
+
if (!parsed.ok) {
|
|
2989
|
+
malformed += 1;
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
const rec = asRecord(parsed.value);
|
|
2993
|
+
const id = rec?.["id"];
|
|
2994
|
+
const author = rec?.["user"];
|
|
2995
|
+
const created = rec?.["created_at"];
|
|
2996
|
+
const url = rec?.["html_url"];
|
|
2997
|
+
const body = rec?.["body"];
|
|
2998
|
+
if (typeof id === "number" && typeof author === "string" && typeof created === "string" && typeof url === "string" && typeof body === "string") {
|
|
2999
|
+
rows.push({ id, author, created, url, body });
|
|
3000
|
+
} else {
|
|
3001
|
+
malformed += 1;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
rows.sort(
|
|
3005
|
+
(a, b) => a.created.localeCompare(b.created) || (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)
|
|
2552
3006
|
);
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
3007
|
+
return { rows, malformed };
|
|
3008
|
+
};
|
|
3009
|
+
var fetchIssueCommentRows = async (repo, prNumber, ghApi) => {
|
|
3010
|
+
const raw = await ghApi([
|
|
3011
|
+
// per_page rides the QUERY string — a `-f` field would flip `gh api` from GET to POST
|
|
3012
|
+
// (create-comment) and 422 every sticky lookup.
|
|
3013
|
+
`repos/${repo}/issues/${String(prNumber)}/comments?per_page=100`,
|
|
3014
|
+
"--paginate",
|
|
3015
|
+
"--jq",
|
|
3016
|
+
ISSUE_COMMENTS_JQ
|
|
3017
|
+
]);
|
|
3018
|
+
const { rows, malformed } = parseIssueCommentRows(raw);
|
|
3019
|
+
if (malformed > 0) {
|
|
3020
|
+
throw new Error(
|
|
3021
|
+
`${String(malformed)} issue-comment row(s) failed to decode \u2014 refusing to treat a corrupted history as an absent sticky`
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
return rows;
|
|
3025
|
+
};
|
|
3026
|
+
var selectBotComment = (rows, botLogin, marker) => {
|
|
3027
|
+
const bot = rows.filter((r) => r.author === botLogin && r.body.startsWith(marker));
|
|
3028
|
+
const last = bot[bot.length - 1];
|
|
3029
|
+
return last === void 0 ? null : { id: last.id, body: last.body };
|
|
2559
3030
|
};
|
|
3031
|
+
var findBotComment = async (repo, prNumber, botLogin, marker, ghApi) => selectBotComment(await fetchIssueCommentRows(repo, prNumber, ghApi), botLogin, marker);
|
|
2560
3032
|
var parseCommentRef = (raw) => {
|
|
2561
3033
|
const parsed = tryParseJson(raw);
|
|
2562
3034
|
const rec = parsed.ok ? asRecord(parsed.value) : null;
|
|
@@ -2579,6 +3051,134 @@ var postComment = async (repo, prNumber, body, ghApi) => {
|
|
|
2579
3051
|
);
|
|
2580
3052
|
return parseCommentRef(stdout);
|
|
2581
3053
|
};
|
|
3054
|
+
var ID_TOKEN_RE = /`([^`\n]+)`/g;
|
|
3055
|
+
var ORPHAN_TOKEN_CAP = 8;
|
|
3056
|
+
var escapedIdIndex = (ids) => {
|
|
3057
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3058
|
+
for (const id of ids) {
|
|
3059
|
+
const escaped = escapeCodeBackticks(id);
|
|
3060
|
+
if (escaped === id) continue;
|
|
3061
|
+
const list = owners.get(escaped);
|
|
3062
|
+
if (list === void 0) owners.set(escaped, [id]);
|
|
3063
|
+
else if (!list.includes(id)) list.push(id);
|
|
3064
|
+
}
|
|
3065
|
+
return new Map(
|
|
3066
|
+
[...owners.entries()].flatMap(
|
|
3067
|
+
([escaped, raws]) => raws.length === 1 && raws[0] !== void 0 ? [[escaped, raws[0]]] : []
|
|
3068
|
+
)
|
|
3069
|
+
);
|
|
3070
|
+
};
|
|
3071
|
+
var discussionRows = (rows, stickyId, botLogin) => {
|
|
3072
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3073
|
+
return rows.filter((c) => {
|
|
3074
|
+
if (seen.has(c.id)) return false;
|
|
3075
|
+
seen.add(c.id);
|
|
3076
|
+
return c.id !== stickyId && c.author !== botLogin;
|
|
3077
|
+
});
|
|
3078
|
+
};
|
|
3079
|
+
var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
3080
|
+
var mentionsOutsideKnown = (reachable, currentIds) => {
|
|
3081
|
+
const known = new Set(currentIds);
|
|
3082
|
+
const escapedToRaw = escapedIdIndex(currentIds);
|
|
3083
|
+
for (const c of reachable) {
|
|
3084
|
+
for (const m of c.body.matchAll(ID_TOKEN_RE)) {
|
|
3085
|
+
const token = m[1];
|
|
3086
|
+
if (token === void 0) continue;
|
|
3087
|
+
const raw = known.has(token) ? token : escapedToRaw.get(token);
|
|
3088
|
+
if ((raw === void 0 || !known.has(raw)) && ID_SHAPE_RE.test(token)) return true;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
return false;
|
|
3092
|
+
};
|
|
3093
|
+
var buildStickyDiscussion = (reachable, currentIds, priorIds) => {
|
|
3094
|
+
const known = new Set(currentIds);
|
|
3095
|
+
const departed = new Set(priorIds);
|
|
3096
|
+
const escapedToRaw = escapedIdIndex([...currentIds, ...priorIds]);
|
|
3097
|
+
const byFinding = Object.fromEntries(
|
|
3098
|
+
currentIds.map((id) => [id, []])
|
|
3099
|
+
);
|
|
3100
|
+
const orphaned = /* @__PURE__ */ new Map();
|
|
3101
|
+
const latestAt = /* @__PURE__ */ new Map();
|
|
3102
|
+
for (const c of reachable) {
|
|
3103
|
+
const link = { author: c.author, when: c.created.slice(0, 10), url: c.url, at: c.created };
|
|
3104
|
+
const pushed = /* @__PURE__ */ new Set();
|
|
3105
|
+
for (const m of c.body.matchAll(ID_TOKEN_RE)) {
|
|
3106
|
+
const token = m[1];
|
|
3107
|
+
if (token === void 0 || pushed.has(token)) continue;
|
|
3108
|
+
pushed.add(token);
|
|
3109
|
+
const rawKnown = known.has(token);
|
|
3110
|
+
const alias = rawKnown ? void 0 : escapedToRaw.get(token);
|
|
3111
|
+
const aliasIsKnown = alias !== void 0 && known.has(alias);
|
|
3112
|
+
const rawDeparted = departed.has(token);
|
|
3113
|
+
if (rawKnown) {
|
|
3114
|
+
byFinding[token]?.push(link);
|
|
3115
|
+
} else if (aliasIsKnown) {
|
|
3116
|
+
if (!rawDeparted) byFinding[alias]?.push(link);
|
|
3117
|
+
} else if (rawDeparted) {
|
|
3118
|
+
const key2 = token;
|
|
3119
|
+
const links = orphaned.get(key2);
|
|
3120
|
+
if (links === void 0) orphaned.set(key2, [link]);
|
|
3121
|
+
else links.push(link);
|
|
3122
|
+
const latest = latestAt.get(key2);
|
|
3123
|
+
if (latest === void 0 || c.created.localeCompare(latest) > 0)
|
|
3124
|
+
latestAt.set(key2, c.created);
|
|
3125
|
+
} else if (alias !== void 0 && departed.has(alias)) {
|
|
3126
|
+
const key2 = alias;
|
|
3127
|
+
const links = orphaned.get(key2);
|
|
3128
|
+
if (links === void 0) orphaned.set(key2, [link]);
|
|
3129
|
+
else links.push(link);
|
|
3130
|
+
const latest = latestAt.get(key2);
|
|
3131
|
+
if (latest === void 0 || c.created.localeCompare(latest) > 0)
|
|
3132
|
+
latestAt.set(key2, c.created);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
const truncated = /* @__PURE__ */ new Map();
|
|
3137
|
+
for (const [id, links] of Object.entries(byFinding)) {
|
|
3138
|
+
links.reverse();
|
|
3139
|
+
if (links.length > PER_FINDING_LINKS) {
|
|
3140
|
+
truncated.set(id, links.length);
|
|
3141
|
+
links.splice(PER_FINDING_LINKS);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
const orphanedTruncated = /* @__PURE__ */ new Map();
|
|
3145
|
+
for (const [token, links] of orphaned) {
|
|
3146
|
+
links.reverse();
|
|
3147
|
+
if (links.length > PER_FINDING_LINKS) {
|
|
3148
|
+
orphanedTruncated.set(token, links.length);
|
|
3149
|
+
links.splice(PER_FINDING_LINKS);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
const orphanedTotal = orphaned.size;
|
|
3153
|
+
const byLatestReply = [...orphaned.entries()].sort(
|
|
3154
|
+
(a, b) => (latestAt.get(b[0]) ?? "").localeCompare(latestAt.get(a[0]) ?? "")
|
|
3155
|
+
);
|
|
3156
|
+
return {
|
|
3157
|
+
byFinding,
|
|
3158
|
+
orphaned: Object.fromEntries(byLatestReply.slice(0, ORPHAN_TOKEN_CAP)),
|
|
3159
|
+
orphanedTotal,
|
|
3160
|
+
truncated: Object.fromEntries(truncated),
|
|
3161
|
+
orphanedTruncated: Object.fromEntries(orphanedTruncated)
|
|
3162
|
+
};
|
|
3163
|
+
};
|
|
3164
|
+
var priorIdsFrom = (doc) => {
|
|
3165
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
|
|
3166
|
+
const rec = doc;
|
|
3167
|
+
const idsOf = (field, systemic) => (Array.isArray(rec[field]) ? rec[field] : []).flatMap((raw) => {
|
|
3168
|
+
const item = typeof raw === "object" && raw !== null ? asRecord(raw) : null;
|
|
3169
|
+
if (item === null) return [];
|
|
3170
|
+
const title = typeof item["title"] === "string" ? item["title"] : "";
|
|
3171
|
+
const resolved = resolveRuleId({
|
|
3172
|
+
...typeof item["id"] === "string" ? { id: item["id"] } : {},
|
|
3173
|
+
...typeof item["code"] === "string" ? { code: item["code"] } : {},
|
|
3174
|
+
...typeof item["path"] === "string" ? { path: item["path"] } : {},
|
|
3175
|
+
title
|
|
3176
|
+
});
|
|
3177
|
+
const id = resolved ?? (systemic && title !== "" ? synthesizedSystemicId(title) : void 0);
|
|
3178
|
+
return id !== void 0 ? [id] : [];
|
|
3179
|
+
});
|
|
3180
|
+
return [...idsOf("findings", false), ...idsOf("systemic_problems", true)];
|
|
3181
|
+
};
|
|
2582
3182
|
var appendRunSummary = (summaryPath, body) => {
|
|
2583
3183
|
if (summaryPath === void 0 || summaryPath === "") return;
|
|
2584
3184
|
const rendered = body();
|
|
@@ -2735,13 +3335,14 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
2735
3335
|
process.exit(0);
|
|
2736
3336
|
}
|
|
2737
3337
|
const prNumber = resolution.prNumber;
|
|
2738
|
-
const [diff,
|
|
3338
|
+
const [diff, commentRows] = await Promise.all([
|
|
2739
3339
|
fetchDiff(input.repo, prNumber, ghApi),
|
|
2740
|
-
|
|
3340
|
+
fetchIssueCommentRows(input.repo, prNumber, ghApi)
|
|
2741
3341
|
]);
|
|
3342
|
+
const existingSticky = selectBotComment(commentRows, input.botLogin, DEFAULT_MARKER);
|
|
2742
3343
|
const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
|
|
2743
3344
|
const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
|
|
2744
|
-
const priorIsFullReview = (body) => isFullReviewSticky(body) || parseReviewedRoute(body) === null && (parseReviewComplete(body) || parseCompletedAncestor(body));
|
|
3345
|
+
const priorIsFullReview = (body) => isFullReviewSticky(body) || parseReviewedRoute(body) === null && (parseReviewComplete(body) || parseCompletedAncestor(body) && !parseMechanicAncestor(body));
|
|
2745
3346
|
const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
|
|
2746
3347
|
const priorDoc = existingSticky !== null ? parseFindingsMarker(existingSticky.body) : null;
|
|
2747
3348
|
const priorBody = existingSticky?.body ?? "";
|
|
@@ -2752,12 +3353,12 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
2752
3353
|
...doc,
|
|
2753
3354
|
convergence: conv ?? void 0
|
|
2754
3355
|
});
|
|
2755
|
-
const carriedFindingsLink = !input.jsonUrl && existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
2756
3356
|
let warnedNoJsonUrl = false;
|
|
2757
3357
|
const findingsBlob = (doc) => {
|
|
2758
3358
|
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
2759
|
-
|
|
2760
|
-
|
|
3359
|
+
const carriedLink = existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
3360
|
+
if (carriedLink !== null) {
|
|
3361
|
+
return findingsMarkerPair(carriedLink, doc.convergence);
|
|
2761
3362
|
}
|
|
2762
3363
|
if (!warnedNoJsonUrl) {
|
|
2763
3364
|
warnedNoJsonUrl = true;
|
|
@@ -2808,8 +3409,41 @@ ${dropNote}` : ""}`,
|
|
|
2808
3409
|
}
|
|
2809
3410
|
const template = readFileSync(input.templatePath, "utf-8");
|
|
2810
3411
|
const inlineRequested = input.inline === true;
|
|
2811
|
-
const
|
|
2812
|
-
|
|
3412
|
+
const envelope = loadEnvelope(input.envelopePath);
|
|
3413
|
+
const reachable = existingSticky !== null ? discussionRows(commentRows, existingSticky.id, input.botLogin) : [];
|
|
3414
|
+
const priorIsMechanic = existingSticky !== null && (parseReviewedRoute(existingSticky.body) === "mechanic" || parseMechanicAncestor(existingSticky.body));
|
|
3415
|
+
const noticeDiscussion = async () => {
|
|
3416
|
+
if (existingSticky === null) {
|
|
3417
|
+
return {
|
|
3418
|
+
orphanedDiscussion: {},
|
|
3419
|
+
orphanedTotal: 0,
|
|
3420
|
+
orphanedTruncated: {},
|
|
3421
|
+
orphanedUnresolvable: false
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
const wantsPrior = !priorIsMechanic && mentionsOutsideKnown(reachable, []);
|
|
3425
|
+
const resolved = wantsPrior ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
3426
|
+
const built = buildStickyDiscussion(reachable, [], priorIdsFrom(resolved));
|
|
3427
|
+
const hadMarker = carriedFindingsMarker(existingSticky.body) !== null;
|
|
3428
|
+
return {
|
|
3429
|
+
orphanedDiscussion: built.orphaned,
|
|
3430
|
+
orphanedTotal: built.orphanedTotal,
|
|
3431
|
+
orphanedTruncated: built.orphanedTruncated,
|
|
3432
|
+
orphanedUnresolvable: wantsPrior && hadMarker && resolved === null
|
|
3433
|
+
};
|
|
3434
|
+
};
|
|
3435
|
+
const renderNotice = (message, discussion) => {
|
|
3436
|
+
const noticeConvergence = (input.route ?? envelope?.route) === "mechanic" ? mechanicConvergence(
|
|
3437
|
+
priorConv,
|
|
3438
|
+
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3439
|
+
) : priorConv;
|
|
3440
|
+
const findings2 = stampConvergence(incompleteFindings(`### \u26A0\uFE0F ${message}`), noticeConvergence);
|
|
3441
|
+
const noticeFindingsPointer = (doc) => {
|
|
3442
|
+
const carried = existingSticky !== null ? carriedFindingsMarker(existingSticky.body) : null;
|
|
3443
|
+
if (carried === null) return findingsBlob(doc);
|
|
3444
|
+
const provenance = existingSticky !== null ? carriedAncestryMarkers(existingSticky.body) : "";
|
|
3445
|
+
return [carriedMarkerPointer(carried, doc.convergence), provenance].filter((p) => p !== "").join("\n\n");
|
|
3446
|
+
};
|
|
2813
3447
|
return formatMarkdown(
|
|
2814
3448
|
render({
|
|
2815
3449
|
findings: findings2,
|
|
@@ -2826,18 +3460,23 @@ ${dropNote}` : ""}`,
|
|
|
2826
3460
|
convergenceRound: false,
|
|
2827
3461
|
runUrl: input.runUrl,
|
|
2828
3462
|
jsonUrl: input.jsonUrl,
|
|
2829
|
-
findingsPointer:
|
|
2830
|
-
postedAt: input.postedAt
|
|
3463
|
+
findingsPointer: noticeFindingsPointer(findings2),
|
|
3464
|
+
postedAt: input.postedAt,
|
|
3465
|
+
orphanedDiscussion: discussion.orphanedDiscussion,
|
|
3466
|
+
orphanedTotal: discussion.orphanedTotal,
|
|
3467
|
+
orphanedTruncated: discussion.orphanedTruncated,
|
|
3468
|
+
orphanedUnresolvable: discussion.orphanedUnresolvable
|
|
2831
3469
|
})
|
|
2832
3470
|
);
|
|
2833
3471
|
};
|
|
2834
3472
|
if (isEmptyDiff(diff)) {
|
|
2835
3473
|
if (wouldBuryCompleted(true)) leaveInPlace();
|
|
3474
|
+
const discussion = await noticeDiscussion();
|
|
2836
3475
|
await upsertSticky(
|
|
2837
3476
|
input.repo,
|
|
2838
3477
|
prNumber,
|
|
2839
3478
|
existingSticky,
|
|
2840
|
-
renderNotice("The diff for this PR is empty \u2014 nothing to review."),
|
|
3479
|
+
renderNotice("The diff for this PR is empty \u2014 nothing to review.", discussion),
|
|
2841
3480
|
ghApi
|
|
2842
3481
|
);
|
|
2843
3482
|
process.exit(0);
|
|
@@ -2845,11 +3484,12 @@ ${dropNote}` : ""}`,
|
|
|
2845
3484
|
const findingsResult = loadFindings(input.findingsPath);
|
|
2846
3485
|
if (findingsResult.kind !== "ok") {
|
|
2847
3486
|
if (wouldBuryCompleted(true)) leaveInPlace();
|
|
3487
|
+
const discussion = await noticeDiscussion();
|
|
2848
3488
|
await upsertSticky(
|
|
2849
3489
|
input.repo,
|
|
2850
3490
|
prNumber,
|
|
2851
3491
|
existingSticky,
|
|
2852
|
-
renderNotice(noticeMessageFor(findingsResult)),
|
|
3492
|
+
renderNotice(noticeMessageFor(findingsResult), discussion),
|
|
2853
3493
|
ghApi
|
|
2854
3494
|
);
|
|
2855
3495
|
process.exit(0);
|
|
@@ -2861,33 +3501,59 @@ ${dropNote}` : ""}`,
|
|
|
2861
3501
|
const reRaisedNotes = answeredFilter.reRaisedNotes;
|
|
2862
3502
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
2863
3503
|
const droppedCount = answeredFilter.droppedCount;
|
|
2864
|
-
const
|
|
2865
|
-
|
|
2866
|
-
answeredFilter.
|
|
2867
|
-
);
|
|
2868
|
-
const
|
|
3504
|
+
const droppedIds = /* @__PURE__ */ new Set([
|
|
3505
|
+
...verbatimReRaised.flatMap((e) => e.code !== "" ? [e.code] : []),
|
|
3506
|
+
...answeredFilter.droppedFindingIds.filter((id) => id !== "")
|
|
3507
|
+
]);
|
|
3508
|
+
const keptCodes = new Set(answeredFilter.findings.map((f) => f.id));
|
|
3509
|
+
const trulyDropped = new Set([...droppedIds].filter((c) => !keptCodes.has(c)));
|
|
2869
3510
|
const systemic = trulyDropped.size === 0 ? loadedFindings.systemic_problems ?? [] : (loadedFindings.systemic_problems ?? []).map((s) => {
|
|
2870
|
-
if (s.
|
|
2871
|
-
const codes = s.
|
|
2872
|
-
return codes.length === s.
|
|
3511
|
+
if (s.finding_ids === void 0) return s;
|
|
3512
|
+
const codes = s.finding_ids.filter((c) => !trulyDropped.has(c));
|
|
3513
|
+
return codes.length === s.finding_ids.length ? s : { ...s, finding_ids: codes };
|
|
2873
3514
|
});
|
|
2874
3515
|
const findings = {
|
|
2875
3516
|
...loadedFindings,
|
|
2876
3517
|
findings: [...answeredFilter.findings],
|
|
2877
3518
|
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
2878
3519
|
};
|
|
3520
|
+
const broadCurrentIds = [
|
|
3521
|
+
...findings.findings.map((f) => f.id).filter((id) => id !== ""),
|
|
3522
|
+
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
3523
|
+
];
|
|
2879
3524
|
const roundHasNit = findings.findings.some((f) => f.severity === "nit");
|
|
2880
|
-
const
|
|
3525
|
+
const nitWantsPrior = roundHasNit && existingSticky !== null && (isFullReviewSticky(existingSticky.body) || isFullReviewAncestry(existingSticky.body));
|
|
3526
|
+
const broadWantsPrior = existingSticky !== null && !priorIsMechanic && mentionsOutsideKnown(reachable, broadCurrentIds);
|
|
3527
|
+
const resolvedPrior = existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
3528
|
+
const priorDocForNits = nitWantsPrior ? resolvedPrior : null;
|
|
2881
3529
|
const priorSuppressedKeys = new Set(
|
|
2882
3530
|
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2883
|
-
(n) => answeredNoteKey({
|
|
3531
|
+
(n) => answeredNoteKey({ id: n.id ?? "", title: n.title })
|
|
2884
3532
|
)
|
|
2885
3533
|
);
|
|
2886
|
-
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) ||
|
|
3534
|
+
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || // ALL THREE key forms the prior side can emit: the resolved id (a coded or same-path
|
|
3535
|
+
// synthesized prior), the answeredNoteKey form (an empty-id prior), and the bare title: key
|
|
3536
|
+
// (a pathless codeless prior).
|
|
3537
|
+
priorSuppressedKeys.has(resolveFindingId(f)) || priorSuppressedKeys.has(answeredNoteKey(f)) || priorSuppressedKeys.has(`title:${f.title}`));
|
|
2887
3538
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
2888
3539
|
const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
|
|
2889
3540
|
const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
|
|
2890
|
-
const
|
|
3541
|
+
const inlinePartition = input.inline === true && envelope !== null ? partitionFindings(visibleFindings, indexDiff(diff)) : null;
|
|
3542
|
+
const straysForDiscussion = inlinePartition?.strays ?? visibleFindings;
|
|
3543
|
+
const currentIds = [
|
|
3544
|
+
...straysForDiscussion.map((f) => f.id).filter((id) => id !== ""),
|
|
3545
|
+
...suppressedNits.map((f) => f.id).filter((id) => id !== ""),
|
|
3546
|
+
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
3547
|
+
];
|
|
3548
|
+
const orphanResolveFailed = existingSticky !== null && broadWantsPrior && carriedFindingsMarker(existingSticky.body) !== null && resolvedPrior === null;
|
|
3549
|
+
const broadLive = new Set(broadCurrentIds);
|
|
3550
|
+
const departedIds = priorIdsFrom(resolvedPrior).filter((id) => !broadLive.has(id));
|
|
3551
|
+
const stickyDiscussion = existingSticky !== null ? buildStickyDiscussion(reachable, currentIds, departedIds) : { byFinding: {}, orphaned: {}, orphanedTotal: 0, truncated: {}, orphanedTruncated: {} };
|
|
3552
|
+
const discussionByFinding = stickyDiscussion.byFinding;
|
|
3553
|
+
const orphanedDiscussion = stickyDiscussion.orphaned;
|
|
3554
|
+
const orphanedTotal = stickyDiscussion.orphanedTotal;
|
|
3555
|
+
const discussionTruncated = stickyDiscussion.truncated;
|
|
3556
|
+
const orphanedTruncated = stickyDiscussion.orphanedTruncated;
|
|
2891
3557
|
const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
|
|
2892
3558
|
const clocDiff = input.clocDiffPath ? loadClocDiff(input.clocDiffPath) : void 0;
|
|
2893
3559
|
const effectiveRoute = input.route ?? envelope?.route;
|
|
@@ -2899,7 +3565,13 @@ ${dropNote}` : ""}`,
|
|
|
2899
3565
|
}
|
|
2900
3566
|
if (emptyMechanicWouldBury(effectiveRoute, envelopelessIncomplete) && existingSticky !== null)
|
|
2901
3567
|
await emptyMechanicLeaveOrNote(existingSticky);
|
|
2902
|
-
const stampedFindings2 = stampConvergence(
|
|
3568
|
+
const stampedFindings2 = stampConvergence(
|
|
3569
|
+
findings,
|
|
3570
|
+
effectiveRoute === "mechanic" ? mechanicConvergence(
|
|
3571
|
+
priorConv,
|
|
3572
|
+
input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD
|
|
3573
|
+
) : priorConv
|
|
3574
|
+
);
|
|
2903
3575
|
const body = formatMarkdown(
|
|
2904
3576
|
render({
|
|
2905
3577
|
findings: stampedFindings2,
|
|
@@ -2934,7 +3606,13 @@ ${dropNote}` : ""}`,
|
|
|
2934
3606
|
unverifiedNoLogs: input.unverifiedNoLogs,
|
|
2935
3607
|
jsonUrl: input.jsonUrl,
|
|
2936
3608
|
findingsPointer: findingsBlob(stampedFindings2),
|
|
2937
|
-
postedAt: input.postedAt
|
|
3609
|
+
postedAt: input.postedAt,
|
|
3610
|
+
discussionByFinding,
|
|
3611
|
+
orphanedDiscussion,
|
|
3612
|
+
orphanedTotal,
|
|
3613
|
+
discussionTruncated,
|
|
3614
|
+
orphanedTruncated,
|
|
3615
|
+
orphanedUnresolvable: orphanResolveFailed
|
|
2938
3616
|
})
|
|
2939
3617
|
);
|
|
2940
3618
|
await upsertSticky(input.repo, prNumber, existingSticky, body, ghApi);
|
|
@@ -2964,6 +3642,7 @@ ${dropNote}` : ""}`,
|
|
|
2964
3642
|
strays,
|
|
2965
3643
|
inDiff
|
|
2966
3644
|
} = inlineRequested ? buildInlineComments(visibleFindings, diff, {
|
|
3645
|
+
...inlinePartition !== null ? { partition: inlinePartition } : {},
|
|
2967
3646
|
inlineTemplate,
|
|
2968
3647
|
models: envelope.models.map((m) => m.model),
|
|
2969
3648
|
findings,
|
|
@@ -2981,7 +3660,7 @@ ${dropNote}` : ""}`,
|
|
|
2981
3660
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
2982
3661
|
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2983
3662
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
2984
|
-
const currentCodes =
|
|
3663
|
+
const currentCodes = computeIdCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2985
3664
|
const roundNumber = priorRoundCount + 1;
|
|
2986
3665
|
const convergence = isRound ? buildConvergence(
|
|
2987
3666
|
findings,
|
|
@@ -2990,6 +3669,11 @@ ${dropNote}` : ""}`,
|
|
|
2990
3669
|
roundNumber,
|
|
2991
3670
|
currentCodes,
|
|
2992
3671
|
input.headSha.slice(0, 12)
|
|
3672
|
+
) : effectiveRoute === "mechanic" ? (
|
|
3673
|
+
// A mechanic pass means CI failed — whatever the carried prior says, its own stamp must
|
|
3674
|
+
// never read "converged". The threshold-relative critical floor keeps the carried triple
|
|
3675
|
+
// consistent at every threshold; null (no marker) when no prior round exists (issue #224).
|
|
3676
|
+
mechanicConvergence(priorConv, input.convergenceThreshold ?? DEFAULT_CONVERGENCE_THRESHOLD)
|
|
2993
3677
|
) : priorConv;
|
|
2994
3678
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
2995
3679
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
@@ -3011,6 +3695,12 @@ ${dropNote}` : ""}`,
|
|
|
3011
3695
|
sameRootNotes,
|
|
3012
3696
|
answeredNotes: reRaisedNotes,
|
|
3013
3697
|
answeredReRaiseNote: answeredDropNote,
|
|
3698
|
+
discussionByFinding,
|
|
3699
|
+
orphanedDiscussion,
|
|
3700
|
+
orphanedTotal,
|
|
3701
|
+
discussionTruncated,
|
|
3702
|
+
orphanedTruncated,
|
|
3703
|
+
orphanedUnresolvable: orphanResolveFailed,
|
|
3014
3704
|
roundCount: currentRoundCount,
|
|
3015
3705
|
convergenceThreshold: input.convergenceThreshold,
|
|
3016
3706
|
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
@@ -3030,12 +3720,13 @@ ${dropNote}` : ""}`,
|
|
|
3030
3720
|
|
|
3031
3721
|
> **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
|
|
3032
3722
|
` : "";
|
|
3033
|
-
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays) => formatMarkdown(
|
|
3723
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays, discussionOverride) => formatMarkdown(
|
|
3034
3724
|
render({
|
|
3035
3725
|
...commonRenderInput,
|
|
3036
3726
|
...straysOverride ? { strays: straysOverride } : {},
|
|
3037
3727
|
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
3038
3728
|
...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
|
|
3729
|
+
...discussionOverride ?? {},
|
|
3039
3730
|
inlineDisposition,
|
|
3040
3731
|
reviewUrl: reviewUrl2
|
|
3041
3732
|
}) + longFilesNote
|
|
@@ -3086,13 +3777,32 @@ ${dropNote}` : ""}`,
|
|
|
3086
3777
|
await minimizeComments(prNumber, priorInlineComments, ghApi);
|
|
3087
3778
|
const unanchoredCount = unposted.length;
|
|
3088
3779
|
const finalStrays = unanchoredCount > 0 ? [...unposted, ...strays] : strays;
|
|
3780
|
+
const finalDiscussion = unanchoredCount > 0 ? buildStickyDiscussion(
|
|
3781
|
+
reachable,
|
|
3782
|
+
[...currentIds, ...unposted.map((f) => f.id).filter((id) => id !== "")],
|
|
3783
|
+
departedIds
|
|
3784
|
+
) : null;
|
|
3785
|
+
const finalDiscussionOverride = finalDiscussion !== null ? {
|
|
3786
|
+
discussionByFinding: finalDiscussion.byFinding,
|
|
3787
|
+
orphanedDiscussion: finalDiscussion.orphaned,
|
|
3788
|
+
orphanedTotal: finalDiscussion.orphanedTotal,
|
|
3789
|
+
discussionTruncated: finalDiscussion.truncated,
|
|
3790
|
+
orphanedTruncated: finalDiscussion.orphanedTruncated
|
|
3791
|
+
} : void 0;
|
|
3089
3792
|
if (stickyRef !== null && (inlinePosted > 0 || unanchoredCount > 0)) {
|
|
3090
3793
|
const finalDisposition = inlinePosted > 0 ? { kind: "posted", count: inlinePosted, sha: input.headSha } : { kind: "inline-unavailable" };
|
|
3091
3794
|
try {
|
|
3092
3795
|
await patchComment(
|
|
3093
3796
|
input.repo,
|
|
3094
3797
|
stickyRef.id,
|
|
3095
|
-
renderBody(
|
|
3798
|
+
renderBody(
|
|
3799
|
+
finalDisposition,
|
|
3800
|
+
reviewUrl,
|
|
3801
|
+
finalStrays,
|
|
3802
|
+
unanchoredCount,
|
|
3803
|
+
unposted,
|
|
3804
|
+
finalDiscussionOverride
|
|
3805
|
+
),
|
|
3096
3806
|
ghApi
|
|
3097
3807
|
);
|
|
3098
3808
|
process.stderr.write(
|
|
@@ -3117,7 +3827,10 @@ ${dropNote}` : ""}`,
|
|
|
3117
3827
|
// document deliberately carries every finding, so the GitHub-rejected ones must keep their
|
|
3118
3828
|
// path-only links here too (issue #231 r2). Unconditional: renderBody's own gate treats an
|
|
3119
3829
|
// empty array exactly like absence, so the ternary was a duplicated decision (issue #231 r3).
|
|
3120
|
-
unposted
|
|
3830
|
+
unposted,
|
|
3831
|
+
// The summary is the sticky's long-lived twin — a rejected finding's rebuilt discussion
|
|
3832
|
+
// trail renders here exactly as on the final patch.
|
|
3833
|
+
finalDiscussionOverride
|
|
3121
3834
|
)
|
|
3122
3835
|
);
|
|
3123
3836
|
};
|
|
@@ -3761,7 +4474,7 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
3761
4474
|
join(input.outDir, "prior_review.json"),
|
|
3762
4475
|
prior === null ? "null" : JSON.stringify(prior)
|
|
3763
4476
|
);
|
|
3764
|
-
const seedsFromPrior = input.conclusion === "success" && prior !== null && prior.body !== null &&
|
|
4477
|
+
const seedsFromPrior = input.conclusion === "success" && prior !== null && prior.body !== null && isFullReviewAncestry(prior.body);
|
|
3765
4478
|
const resolvedPrior = seedsFromPrior ? await resolvePriorFindings(prior.body, readArtifact) : null;
|
|
3766
4479
|
const priorFindings = resolvedPrior !== null && typeof resolvedPrior === "object" ? resolvedPrior : null;
|
|
3767
4480
|
writeFileSync(
|
|
@@ -4153,6 +4866,7 @@ var parseScope = (raw) => {
|
|
|
4153
4866
|
};
|
|
4154
4867
|
|
|
4155
4868
|
// src/index.ts
|
|
4869
|
+
var resolvedPriorValue = resolveTolerantFindings;
|
|
4156
4870
|
var readJSON = (path) => {
|
|
4157
4871
|
try {
|
|
4158
4872
|
return JSON.parse(readFileSync(resolve$1(path), "utf-8"));
|
|
@@ -4930,13 +5644,13 @@ var seedDraftCmd = defineCommand({
|
|
|
4930
5644
|
const priorFindings = (() => {
|
|
4931
5645
|
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4932
5646
|
return strippedPrior;
|
|
4933
|
-
const
|
|
4934
|
-
const doc =
|
|
4935
|
-
...
|
|
4936
|
-
findings:
|
|
5647
|
+
const resolved = resolvedPriorValue(strippedPrior);
|
|
5648
|
+
const doc = resolved === null ? strippedPrior : {
|
|
5649
|
+
...resolved,
|
|
5650
|
+
findings: answeredRegistry !== null && answeredRegistry.length > 0 ? resolved.findings.filter(
|
|
4937
5651
|
(f) => !answeredRegistry.some((e) => isAnsweredDrop(f, e))
|
|
4938
|
-
)
|
|
4939
|
-
}
|
|
5652
|
+
) : resolved.findings
|
|
5653
|
+
};
|
|
4940
5654
|
const carried = doc["scope_metastasis"];
|
|
4941
5655
|
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
4942
5656
|
if (doc["verdict"] === "error") return doc;
|
|
@@ -4951,7 +5665,7 @@ var seedDraftCmd = defineCommand({
|
|
|
4951
5665
|
`
|
|
4952
5666
|
);
|
|
4953
5667
|
}
|
|
4954
|
-
if (parsedPrior !== null &&
|
|
5668
|
+
if (parsedPrior !== null && isFullReviewAncestry(priorBody ?? "")) {
|
|
4955
5669
|
try {
|
|
4956
5670
|
const belowFloor = priorBelowFloorNits(
|
|
4957
5671
|
parsedPrior,
|
|
@@ -4980,22 +5694,27 @@ var seedDraftCmd = defineCommand({
|
|
|
4980
5694
|
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
4981
5695
|
)
|
|
4982
5696
|
) : priorFindings;
|
|
4983
|
-
const accepts = (doc) =>
|
|
4984
|
-
|
|
4985
|
-
|
|
5697
|
+
const accepts = (doc) => {
|
|
5698
|
+
const resolved = resolvedPriorValue(doc);
|
|
5699
|
+
return resolved !== null && validateAgainstSchema(resolved, schemaPath).valid ? resolved : null;
|
|
5700
|
+
};
|
|
5701
|
+
const seedDoc = accepts(priorFindings) ?? (() => {
|
|
5702
|
+
const bare = accepts(barePrior);
|
|
5703
|
+
if (bare === null) return null;
|
|
5704
|
+
process.stderr.write(
|
|
5705
|
+
`Note: the in-force schema rejects a carried recoverable field (scope_metastasis/convergence/change_size) \u2014 seeding the prior without it (issue #150 review r2 / #182 review r2)
|
|
4986
5706
|
`
|
|
4987
|
-
|
|
5707
|
+
);
|
|
5708
|
+
return bare;
|
|
5709
|
+
})();
|
|
4988
5710
|
if (seedDoc === null) return false;
|
|
4989
|
-
|
|
4990
|
-
if (
|
|
4991
|
-
if (isIncompleteFindings(resolution.value)) return false;
|
|
4992
|
-
if (parseReviewedRoute(priorBody ?? "") !== "full review") return false;
|
|
5711
|
+
if (isIncompleteFindings(seedDoc)) return false;
|
|
5712
|
+
if (!isFullReviewAncestry(priorBody ?? "")) return false;
|
|
4993
5713
|
writeFileSync(outPath, SEED_SENTINEL);
|
|
4994
5714
|
writeFileSync(priorContextPath(outPath), `${JSON.stringify(seedDoc, null, 2)}
|
|
4995
5715
|
`);
|
|
4996
|
-
const count = resolution.value.findings.length;
|
|
4997
5716
|
process.stderr.write(
|
|
4998
|
-
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(
|
|
5717
|
+
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(seedDoc.findings.length)} finding(s)) to ${priorContextPath(outPath)} as context
|
|
4999
5718
|
`
|
|
5000
5719
|
);
|
|
5001
5720
|
return true;
|