@jphutchins/code-review 0.1.0-alpha.54 → 0.1.0-alpha.55
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 +400 -126
- 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 +6 -6
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,
|
|
@@ -617,24 +819,21 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
|
|
|
617
819
|
const v = u[k];
|
|
618
820
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
619
821
|
});
|
|
620
|
-
var
|
|
621
|
-
var
|
|
822
|
+
var MAX_IDS_PER_ROUND = 8;
|
|
823
|
+
var hasId = (ids, code) => ids !== void 0 && Object.prototype.hasOwnProperty.call(ids, code);
|
|
622
824
|
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
|
-
);
|
|
825
|
+
var normalizeIdCounts = (ids, priorCodes) => {
|
|
826
|
+
const entries = Object.entries(usableCountsMap(ids) ?? {});
|
|
628
827
|
if (entries.length === 0) return void 0;
|
|
629
828
|
const sorted = entries.sort((a, b) => {
|
|
630
829
|
if (b[1] !== a[1]) return b[1] - a[1];
|
|
631
|
-
const aPrior =
|
|
632
|
-
const bPrior =
|
|
830
|
+
const aPrior = hasId(priorCodes, a[0]) ? 1 : 0;
|
|
831
|
+
const bPrior = hasId(priorCodes, b[0]) ? 1 : 0;
|
|
633
832
|
if (aPrior !== bPrior) return bPrior - aPrior;
|
|
634
833
|
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
635
834
|
});
|
|
636
|
-
const base = sorted.slice(0,
|
|
637
|
-
const priorKept = sorted.slice(
|
|
835
|
+
const base = sorted.slice(0, MAX_IDS_PER_ROUND);
|
|
836
|
+
const priorKept = sorted.slice(MAX_IDS_PER_ROUND).filter(([code]) => hasId(priorCodes, code)).slice(0, MAX_IDS_PER_ROUND);
|
|
638
837
|
return Object.fromEntries([...base, ...priorKept]);
|
|
639
838
|
};
|
|
640
839
|
var parseRounds = (body) => {
|
|
@@ -646,13 +845,13 @@ var parseRounds = (body) => {
|
|
|
646
845
|
let priorCodes;
|
|
647
846
|
for (const u of decoded.filter(isSeverityCounts)) {
|
|
648
847
|
const rec = u;
|
|
649
|
-
const codes =
|
|
848
|
+
const codes = normalizeIdCounts(rec["ids"], priorCodes) ?? normalizeIdCounts(rec["codes"], priorCodes);
|
|
650
849
|
priorCodes = codes;
|
|
651
850
|
const sha = rec["sha"];
|
|
652
851
|
const shaStr = typeof sha === "string" && sha !== "" ? sha : void 0;
|
|
653
852
|
const round = rec["round"];
|
|
654
853
|
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 };
|
|
854
|
+
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
855
|
const record3 = shaStr === void 0 ? base : { ...base, sha: shaStr };
|
|
657
856
|
kept.push(roundNum === void 0 ? record3 : { ...record3, round: roundNum });
|
|
658
857
|
}
|
|
@@ -666,29 +865,31 @@ var roundsSummary = (rounds, count = rounds.length) => {
|
|
|
666
865
|
const trajectory = cells.length === 0 ? "" : rounds.length > TRAJECTORY_SCORES ? `\u2026 \u2192 ${cells.join(" \u2192 ")}` : cells.join(" \u2192 ");
|
|
667
866
|
return trajectory === "" ? `**Round ${String(count)}**` : `**Round ${String(count)}** \xB7 ${trajectory}`;
|
|
668
867
|
};
|
|
669
|
-
var
|
|
868
|
+
var computeIdCounts = (findings, systemic = []) => {
|
|
670
869
|
const counts = /* @__PURE__ */ new Map();
|
|
671
870
|
for (const code of [
|
|
672
|
-
|
|
673
|
-
|
|
871
|
+
// resolveFindingId: an EMPTY id counts under the same synthesized key the answered match uses —
|
|
872
|
+
// one finding can never be simultaneously tracked (dropped-by-synthesis) and uncounted.
|
|
873
|
+
...findings.map((f) => resolveFindingId(f)),
|
|
874
|
+
...systemic.flatMap((s) => [s.id, ...s.finding_ids ?? []])
|
|
674
875
|
]) {
|
|
675
876
|
if (code === void 0 || code === "") continue;
|
|
676
877
|
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
677
878
|
}
|
|
678
879
|
return Object.fromEntries(counts);
|
|
679
880
|
};
|
|
680
|
-
var
|
|
881
|
+
var consecutiveIdStreaks = (rounds) => {
|
|
681
882
|
const entries = [];
|
|
682
883
|
if (rounds.length === 0) return {};
|
|
683
|
-
const lastCodes = rounds[rounds.length - 1]?.
|
|
884
|
+
const lastCodes = rounds[rounds.length - 1]?.ids;
|
|
684
885
|
if (lastCodes === void 0) return {};
|
|
685
886
|
for (const code of Object.keys(lastCodes)) {
|
|
686
887
|
let streak = 0;
|
|
687
888
|
let startIndex = rounds.length;
|
|
688
889
|
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 &&
|
|
890
|
+
const codes = rounds[i]?.ids;
|
|
891
|
+
if (codes === void 0 || !hasId(codes, code)) break;
|
|
892
|
+
if (i > 0 && rounds[i]?.sha !== void 0 && rounds[i]?.sha === rounds[i - 1]?.sha && hasId(rounds[i - 1]?.ids, code)) {
|
|
692
893
|
continue;
|
|
693
894
|
}
|
|
694
895
|
streak += 1;
|
|
@@ -702,12 +903,12 @@ var consecutiveCodeStreaks = (rounds) => {
|
|
|
702
903
|
};
|
|
703
904
|
var DEFAULT_METASTASIS_STREAK = 3;
|
|
704
905
|
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
|
|
906
|
+
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
907
|
var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
707
|
-
const flagged =
|
|
908
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
708
909
|
if (flagged.length === 0) return "";
|
|
709
910
|
const lines = flagged.map(
|
|
710
|
-
({
|
|
911
|
+
({ id, streak }) => `> **\`${escapeCodeBackticks(id)}\`** \u2014 findings in ${String(streak.streak)} consecutive rounds.`
|
|
711
912
|
);
|
|
712
913
|
return [
|
|
713
914
|
"> [!WARNING]",
|
|
@@ -716,27 +917,27 @@ var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
|
716
917
|
].join("\n");
|
|
717
918
|
};
|
|
718
919
|
var computeScopeMetastasis = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
719
|
-
const flagged =
|
|
920
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
720
921
|
if (flagged.length === 0) return null;
|
|
721
922
|
return {
|
|
722
923
|
decision_prompt: SCOPE_METASTASIS_DECISION_PROMPT,
|
|
723
|
-
recurring: flagged.map(({
|
|
724
|
-
|
|
924
|
+
recurring: flagged.map(({ id, streak }) => ({
|
|
925
|
+
id,
|
|
725
926
|
consecutive_rounds: streak.streak,
|
|
726
927
|
start_round: streak.startRound
|
|
727
928
|
}))
|
|
728
929
|
};
|
|
729
930
|
};
|
|
730
931
|
var computeSameRootNotes = (priorRounds, findings, currentSha) => {
|
|
731
|
-
const codes = findings.map((f) => f
|
|
932
|
+
const codes = findings.map((f) => resolveFindingId(f));
|
|
732
933
|
const entries = [];
|
|
733
934
|
for (const code of codes) {
|
|
734
935
|
let lastRound = 0;
|
|
735
936
|
for (let i = priorRounds.length - 1; i >= 0; i--) {
|
|
736
937
|
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 &&
|
|
938
|
+
if (i > 0 && priorRounds[i]?.sha !== void 0 && priorRounds[i]?.sha === priorRounds[i - 1]?.sha && hasId(priorRounds[i - 1]?.ids, code))
|
|
738
939
|
continue;
|
|
739
|
-
const count = priorRounds[i]?.
|
|
940
|
+
const count = priorRounds[i]?.ids?.[code];
|
|
740
941
|
if (count !== void 0 && count > 0) {
|
|
741
942
|
lastRound = priorRounds[i]?.round ?? i + 1;
|
|
742
943
|
break;
|
|
@@ -770,13 +971,13 @@ var convergenceScore = (doc, threshold) => round2(
|
|
|
770
971
|
0
|
|
771
972
|
)
|
|
772
973
|
);
|
|
773
|
-
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1,
|
|
974
|
+
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha) => {
|
|
774
975
|
const score = convergenceScore(doc, threshold);
|
|
775
|
-
const normalized =
|
|
976
|
+
const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
|
|
776
977
|
const current = {
|
|
777
978
|
round,
|
|
778
979
|
score,
|
|
779
|
-
...normalized !== void 0 ? {
|
|
980
|
+
...normalized !== void 0 ? { ids: { ...normalized } } : {},
|
|
780
981
|
...sha !== void 0 ? { sha } : {}
|
|
781
982
|
};
|
|
782
983
|
const rounds = [...priorRounds, current].slice(-64);
|
|
@@ -795,11 +996,16 @@ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
|
|
|
795
996
|
if (!isBelowVisibilityFloor(rec, floor)) continue;
|
|
796
997
|
const title = rec["title"];
|
|
797
998
|
if (typeof title !== "string") continue;
|
|
798
|
-
const code = typeof rec["code"] === "string" && rec["code"] !== "" ? rec["code"] : void 0;
|
|
799
999
|
const path = typeof rec["path"] === "string" ? rec["path"] : void 0;
|
|
1000
|
+
const id = resolveRuleId({
|
|
1001
|
+
id: typeof rec["id"] === "string" ? rec["id"] : void 0,
|
|
1002
|
+
code: typeof rec["code"] === "string" ? rec["code"] : void 0,
|
|
1003
|
+
path,
|
|
1004
|
+
title
|
|
1005
|
+
});
|
|
800
1006
|
nits.push({
|
|
801
1007
|
title,
|
|
802
|
-
...
|
|
1008
|
+
...id !== void 0 ? { id } : {},
|
|
803
1009
|
...path !== void 0 ? { path } : {}
|
|
804
1010
|
});
|
|
805
1011
|
}
|
|
@@ -852,8 +1058,33 @@ var parseSurfaceSignal = (doc) => {
|
|
|
852
1058
|
convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
|
|
853
1059
|
};
|
|
854
1060
|
};
|
|
1061
|
+
var withLegacyConvergenceIds = (raw) => {
|
|
1062
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
1063
|
+
const rec = raw;
|
|
1064
|
+
const rounds = rec["rounds"];
|
|
1065
|
+
if (!Array.isArray(rounds)) return raw;
|
|
1066
|
+
const needsMigration = (r) => {
|
|
1067
|
+
if (typeof r !== "object" || r === null) return false;
|
|
1068
|
+
const round = r;
|
|
1069
|
+
return round["codes"] !== void 0 || usableCountsMap(round["ids"]) === void 0;
|
|
1070
|
+
};
|
|
1071
|
+
if (!rounds.some(needsMigration)) return raw;
|
|
1072
|
+
const mapped = rounds.map((r) => {
|
|
1073
|
+
if (typeof r !== "object" || r === null) return r;
|
|
1074
|
+
const round = r;
|
|
1075
|
+
const ids = usableCountsMap(round["ids"]) ?? usableCountsMap(round["codes"]);
|
|
1076
|
+
if (ids === void 0) {
|
|
1077
|
+
return round["ids"] !== void 0 || round["codes"] !== void 0 ? Object.fromEntries(Object.entries(round).filter(([k]) => k !== "ids" && k !== "codes")) : round;
|
|
1078
|
+
}
|
|
1079
|
+
const rest = Object.fromEntries(
|
|
1080
|
+
Object.entries(round).filter(([k]) => k !== "ids" && k !== "codes")
|
|
1081
|
+
);
|
|
1082
|
+
return { ...rest, ids };
|
|
1083
|
+
});
|
|
1084
|
+
return { ...rec, rounds: mapped };
|
|
1085
|
+
};
|
|
855
1086
|
var validStampedConvergence = (raw) => {
|
|
856
|
-
const decoded = ConvergenceCodec.decode(raw);
|
|
1087
|
+
const decoded = ConvergenceCodec.decode(withLegacyConvergenceIds(raw));
|
|
857
1088
|
return decoded._tag === "Right" && decoded.right.rounds !== void 0 && decoded.right.rounds.length > 0 ? decoded.right : null;
|
|
858
1089
|
};
|
|
859
1090
|
var parseConvergence = (priorDoc) => {
|
|
@@ -878,7 +1109,7 @@ var parseConvergenceMarker = (body) => {
|
|
|
878
1109
|
};
|
|
879
1110
|
var roundRecordsToConvergenceRounds = (records) => records.map((r, i) => ({
|
|
880
1111
|
round: r.round ?? i + 1,
|
|
881
|
-
...r.
|
|
1112
|
+
...r.ids !== void 0 ? { ids: { ...r.ids } } : {},
|
|
882
1113
|
...r.sha !== void 0 ? { sha: r.sha } : {}
|
|
883
1114
|
}));
|
|
884
1115
|
var priorTrajectory = (priorDoc, priorBody) => parseConvergence(priorDoc)?.rounds ?? parseConvergenceMarker(priorBody)?.rounds ?? roundRecordsToConvergenceRounds(parseRounds(priorBody));
|
|
@@ -1112,12 +1343,22 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1112
1343
|
const title = first["title"];
|
|
1113
1344
|
const description = first["description"];
|
|
1114
1345
|
const reasoning = first["reasoning"];
|
|
1115
|
-
const
|
|
1346
|
+
const id = first["id"];
|
|
1347
|
+
const legacyCode = first["code"];
|
|
1116
1348
|
const severity = first["severity"];
|
|
1117
1349
|
const path = first["path"];
|
|
1118
1350
|
const patch = first["patch"];
|
|
1119
1351
|
return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
|
|
1120
|
-
|
|
1352
|
+
// resolveRuleId: the ONE legacy-spelling precedence the upcast, this reader, and the
|
|
1353
|
+
// below-floor nit reader share — a pre-id marker (or one written before the migration)
|
|
1354
|
+
// carries `code`; a codeless one resolves to the same synthesized id the registry's
|
|
1355
|
+
// legacy upcast derives, so the entry keys to the identical claim on the next round.
|
|
1356
|
+
code: resolveRuleId({
|
|
1357
|
+
id: typeof id === "string" ? id : void 0,
|
|
1358
|
+
code: typeof legacyCode === "string" ? legacyCode : void 0,
|
|
1359
|
+
path,
|
|
1360
|
+
title
|
|
1361
|
+
}) ?? synthesizedFindingId(path, title),
|
|
1121
1362
|
title,
|
|
1122
1363
|
description,
|
|
1123
1364
|
reasoning,
|
|
@@ -1159,29 +1400,31 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1159
1400
|
for (const entry of [...entries].sort(
|
|
1160
1401
|
(a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
|
|
1161
1402
|
)) {
|
|
1162
|
-
const key2 = answeredNoteKey(entry);
|
|
1403
|
+
const key2 = answeredNoteKey({ id: entry.code, title: entry.title });
|
|
1163
1404
|
if (!byKey.has(key2)) byKey.set(key2, entry);
|
|
1164
1405
|
}
|
|
1165
1406
|
return [...byKey.values()];
|
|
1166
1407
|
};
|
|
1167
|
-
var matches = (f, e) =>
|
|
1408
|
+
var matches = (f, e) => e.code === resolveFindingId(f) || isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1168
1409
|
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
1410
|
var isAnsweredDrop = (f, e) => matches(f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1170
|
-
var answeredNoteKey = (f) => f.
|
|
1411
|
+
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1171
1412
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1172
1413
|
var applyAnswered = (findings, registry) => {
|
|
1173
1414
|
const kept = [];
|
|
1174
1415
|
const noteEntries = [];
|
|
1175
|
-
const
|
|
1416
|
+
const droppedByEntry = /* @__PURE__ */ new Map();
|
|
1417
|
+
const droppedFindingIds = [];
|
|
1176
1418
|
let droppedCount = 0;
|
|
1177
1419
|
for (const f of findings) {
|
|
1178
|
-
const entry = registry.find((e) =>
|
|
1420
|
+
const entry = registry.find((e) => e.code === resolveFindingId(f)) ?? registry.find((e) => isSynthesizedFindingId(e.code) && e.title === f.title);
|
|
1179
1421
|
if (entry === void 0) {
|
|
1180
1422
|
kept.push(f);
|
|
1181
1423
|
continue;
|
|
1182
1424
|
}
|
|
1183
1425
|
if (isAnsweredDrop(f, entry)) {
|
|
1184
|
-
|
|
1426
|
+
droppedByEntry.set(entry.replyId, entry);
|
|
1427
|
+
droppedFindingIds.push(resolveFindingId(f));
|
|
1185
1428
|
droppedCount += 1;
|
|
1186
1429
|
} else {
|
|
1187
1430
|
kept.push(f);
|
|
@@ -1191,7 +1434,8 @@ var applyAnswered = (findings, registry) => {
|
|
|
1191
1434
|
return {
|
|
1192
1435
|
findings: kept,
|
|
1193
1436
|
reRaisedNotes: Object.fromEntries(noteEntries),
|
|
1194
|
-
verbatimReRaised: [...
|
|
1437
|
+
verbatimReRaised: [...droppedByEntry.values()],
|
|
1438
|
+
droppedFindingIds,
|
|
1195
1439
|
droppedCount
|
|
1196
1440
|
};
|
|
1197
1441
|
};
|
|
@@ -1262,7 +1506,10 @@ var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
|
|
|
1262
1506
|
...f,
|
|
1263
1507
|
title: escapePipes(f.title),
|
|
1264
1508
|
path: escapeCodeBackticks(f.path),
|
|
1265
|
-
|
|
1509
|
+
id: escapeCodeBackticks(f.id),
|
|
1510
|
+
// The RESOLVED id: the same-root notes are keyed on resolveFindingId (an empty id resolves to
|
|
1511
|
+
// its synthesized key), so the lookup must not miss the note on the raw id.
|
|
1512
|
+
idKey: resolveFindingId(f),
|
|
1266
1513
|
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1267
1514
|
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1268
1515
|
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
@@ -1273,7 +1520,7 @@ var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
|
|
|
1273
1520
|
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1274
1521
|
var sanitizeSuppressedNit = (f) => ({
|
|
1275
1522
|
title: escapeCodeBackticks(f.title),
|
|
1276
|
-
|
|
1523
|
+
id: escapeCodeBackticks(f.id),
|
|
1277
1524
|
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1278
1525
|
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1279
1526
|
startLine: f.start_line,
|
|
@@ -1294,10 +1541,10 @@ var carriedLines = (f) => [
|
|
|
1294
1541
|
var sanitizeSystemic = (s) => ({
|
|
1295
1542
|
...s,
|
|
1296
1543
|
title: escapePipes(s.title),
|
|
1297
|
-
...s.
|
|
1544
|
+
...s.id !== void 0 ? { id: escapeCodeBackticks(s.id) } : {},
|
|
1298
1545
|
...s.code_url !== void 0 ? { code_url: linkSafeUrl(s.code_url) } : {},
|
|
1299
1546
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1300
|
-
...s.
|
|
1547
|
+
...s.finding_ids !== void 0 ? { finding_ids: s.finding_ids.map(escapeCodeBackticks) } : {}
|
|
1301
1548
|
});
|
|
1302
1549
|
var emptySeverityCounts = () => ({
|
|
1303
1550
|
critical: 0,
|
|
@@ -1330,8 +1577,8 @@ var render = (input) => {
|
|
|
1330
1577
|
const advisoryAllowed = isFullReviewRound;
|
|
1331
1578
|
const suppressedBudget = (input.suppressedNits ?? []).map(sanitizeSuppressedNit).reduce(
|
|
1332
1579
|
(acc, n) => {
|
|
1333
|
-
const size = n.carried.reduce((sum, line) => sum + line.length + 3, 0) + SUPPRESSED_NIT_BLOCK_OVERHEAD + n.title.length + n.path.length * 2 +
|
|
1334
|
-
(n.
|
|
1580
|
+
const size = 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.
|
|
1581
|
+
n.id.length * 2 + 2 + (n.codeUrl !== void 0 ? n.codeUrl.length + 4 : 0) + String(n.startLine).length * 2 + String(n.endLine).length + (n.side !== void 0 ? n.side.length + 2 : 0);
|
|
1335
1582
|
return acc.used + size > CARRIED_TOTAL_CHARS ? { list: acc.list, used: acc.used, dropped: acc.dropped + 1 } : { list: [...acc.list, n], used: acc.used + size, dropped: acc.dropped };
|
|
1336
1583
|
},
|
|
1337
1584
|
{ list: [], used: 0, dropped: 0 }
|
|
@@ -1483,11 +1730,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1483
1730
|
const { inDiff, strays } = partitionFindings(findings, index);
|
|
1484
1731
|
const eta = new Eta({ autoTrim: false });
|
|
1485
1732
|
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
|
-
};
|
|
1733
|
+
const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1491
1734
|
const comments = inDiff.map((f) => {
|
|
1492
1735
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1493
1736
|
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
@@ -1499,8 +1742,8 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1499
1742
|
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
1500
1743
|
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
1501
1744
|
} : f;
|
|
1502
|
-
const sameRootNote = noteFor(
|
|
1503
|
-
const answeredNote2 = noteFor(
|
|
1745
|
+
const sameRootNote = noteFor(context.sameRootNotes, resolveFindingId(f));
|
|
1746
|
+
const answeredNote2 = noteFor(context.answeredNotes, answeredNoteKey(f));
|
|
1504
1747
|
const comment = {
|
|
1505
1748
|
path: f.path,
|
|
1506
1749
|
line: f.end_line,
|
|
@@ -1944,33 +2187,47 @@ var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
|
|
|
1944
2187
|
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
2188
|
);
|
|
1946
2189
|
var identity = (decoded) => decoded;
|
|
2190
|
+
var legacyFindingsCodec = FindingsCodecV09;
|
|
2191
|
+
var legacyFindingsNormalize = (doc) => normalizeV09(doc);
|
|
1947
2192
|
var findingsTable = [
|
|
1948
2193
|
{
|
|
1949
2194
|
minor: "0.4",
|
|
1950
2195
|
defaultVersion: "0.4.0",
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2196
|
+
// The frozen tolerant-in legacy schema, NOT the live 0.10 file: every ajv-gated channel
|
|
2197
|
+
// (validate --schema-version, the extraction ladder) dispatches the RAW doc's declared minor
|
|
2198
|
+
// through schemaPathFor, so the ajv gate must accept exactly what the tolerant legacy codec
|
|
2199
|
+
// accepts — the live file (id required) would reject the legacy docs the upcast promises to read.
|
|
2200
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2201
|
+
codec: legacyFindingsCodec,
|
|
2202
|
+
normalize: legacyFindingsNormalize,
|
|
1954
2203
|
latest: false
|
|
1955
2204
|
},
|
|
1956
2205
|
{
|
|
1957
2206
|
minor: "0.5",
|
|
1958
2207
|
defaultVersion: "0.5.0",
|
|
1959
|
-
schemaFile: "findings.schema.json",
|
|
1960
|
-
codec:
|
|
1961
|
-
normalize:
|
|
2208
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2209
|
+
codec: legacyFindingsCodec,
|
|
2210
|
+
normalize: legacyFindingsNormalize,
|
|
1962
2211
|
latest: false
|
|
1963
2212
|
},
|
|
1964
2213
|
{
|
|
1965
2214
|
minor: "0.6",
|
|
1966
2215
|
defaultVersion: "0.6.0",
|
|
1967
|
-
schemaFile: "findings.schema.json",
|
|
1968
|
-
codec:
|
|
1969
|
-
normalize:
|
|
2216
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2217
|
+
codec: legacyFindingsCodec,
|
|
2218
|
+
normalize: legacyFindingsNormalize,
|
|
1970
2219
|
latest: false
|
|
1971
2220
|
},
|
|
1972
2221
|
{
|
|
1973
2222
|
minor: "0.9",
|
|
2223
|
+
defaultVersion: "0.9.0",
|
|
2224
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2225
|
+
codec: legacyFindingsCodec,
|
|
2226
|
+
normalize: legacyFindingsNormalize,
|
|
2227
|
+
latest: false
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
minor: "0.10",
|
|
1974
2231
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
1975
2232
|
schemaFile: "findings.schema.json",
|
|
1976
2233
|
codec: FindingsCodec,
|
|
@@ -2064,6 +2321,13 @@ var resolvers = {
|
|
|
2064
2321
|
prices: (raw) => resolveSingleVersion("prices", raw)
|
|
2065
2322
|
};
|
|
2066
2323
|
var resolve = (kind, raw) => resolvers[kind](raw);
|
|
2324
|
+
var resolveTolerantFindings = (doc) => {
|
|
2325
|
+
const r = resolveFindings(doc);
|
|
2326
|
+
if (r.kind === "ok") return r.value;
|
|
2327
|
+
if (r.kind === "unsupported-version") return null;
|
|
2328
|
+
const legacy = FindingsCodecV09.decode(doc);
|
|
2329
|
+
return legacy._tag === "Right" ? normalizeV09(legacy.right) : null;
|
|
2330
|
+
};
|
|
2067
2331
|
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
2068
2332
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
2069
2333
|
var parseTimeoutMs = (raw, fallback) => {
|
|
@@ -2861,15 +3125,16 @@ ${dropNote}` : ""}`,
|
|
|
2861
3125
|
const reRaisedNotes = answeredFilter.reRaisedNotes;
|
|
2862
3126
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
2863
3127
|
const droppedCount = answeredFilter.droppedCount;
|
|
2864
|
-
const
|
|
2865
|
-
|
|
2866
|
-
answeredFilter.
|
|
2867
|
-
);
|
|
2868
|
-
const
|
|
3128
|
+
const droppedIds = /* @__PURE__ */ new Set([
|
|
3129
|
+
...verbatimReRaised.flatMap((e) => e.code !== "" ? [e.code] : []),
|
|
3130
|
+
...answeredFilter.droppedFindingIds.filter((id) => id !== "")
|
|
3131
|
+
]);
|
|
3132
|
+
const keptCodes = new Set(answeredFilter.findings.map((f) => f.id));
|
|
3133
|
+
const trulyDropped = new Set([...droppedIds].filter((c) => !keptCodes.has(c)));
|
|
2869
3134
|
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.
|
|
3135
|
+
if (s.finding_ids === void 0) return s;
|
|
3136
|
+
const codes = s.finding_ids.filter((c) => !trulyDropped.has(c));
|
|
3137
|
+
return codes.length === s.finding_ids.length ? s : { ...s, finding_ids: codes };
|
|
2873
3138
|
});
|
|
2874
3139
|
const findings = {
|
|
2875
3140
|
...loadedFindings,
|
|
@@ -2880,10 +3145,13 @@ ${dropNote}` : ""}`,
|
|
|
2880
3145
|
const priorDocForNits = roundHasNit && existingSticky !== null && isFullReviewSticky(existingSticky.body) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
2881
3146
|
const priorSuppressedKeys = new Set(
|
|
2882
3147
|
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2883
|
-
(n) => answeredNoteKey({
|
|
3148
|
+
(n) => answeredNoteKey({ id: n.id ?? "", title: n.title })
|
|
2884
3149
|
)
|
|
2885
3150
|
);
|
|
2886
|
-
const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) ||
|
|
3151
|
+
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
|
|
3152
|
+
// synthesized prior), the answeredNoteKey form (an empty-id prior), and the bare title: key
|
|
3153
|
+
// (a pathless codeless prior).
|
|
3154
|
+
priorSuppressedKeys.has(resolveFindingId(f)) || priorSuppressedKeys.has(answeredNoteKey(f)) || priorSuppressedKeys.has(`title:${f.title}`));
|
|
2887
3155
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
2888
3156
|
const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
|
|
2889
3157
|
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._" : "");
|
|
@@ -2981,7 +3249,7 @@ ${dropNote}` : ""}`,
|
|
|
2981
3249
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
2982
3250
|
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2983
3251
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
2984
|
-
const currentCodes =
|
|
3252
|
+
const currentCodes = computeIdCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2985
3253
|
const roundNumber = priorRoundCount + 1;
|
|
2986
3254
|
const convergence = isRound ? buildConvergence(
|
|
2987
3255
|
findings,
|
|
@@ -4153,6 +4421,7 @@ var parseScope = (raw) => {
|
|
|
4153
4421
|
};
|
|
4154
4422
|
|
|
4155
4423
|
// src/index.ts
|
|
4424
|
+
var resolvedPriorValue = resolveTolerantFindings;
|
|
4156
4425
|
var readJSON = (path) => {
|
|
4157
4426
|
try {
|
|
4158
4427
|
return JSON.parse(readFileSync(resolve$1(path), "utf-8"));
|
|
@@ -4930,13 +5199,13 @@ var seedDraftCmd = defineCommand({
|
|
|
4930
5199
|
const priorFindings = (() => {
|
|
4931
5200
|
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4932
5201
|
return strippedPrior;
|
|
4933
|
-
const
|
|
4934
|
-
const doc =
|
|
4935
|
-
...
|
|
4936
|
-
findings:
|
|
5202
|
+
const resolved = resolvedPriorValue(strippedPrior);
|
|
5203
|
+
const doc = resolved === null ? strippedPrior : {
|
|
5204
|
+
...resolved,
|
|
5205
|
+
findings: answeredRegistry !== null && answeredRegistry.length > 0 ? resolved.findings.filter(
|
|
4937
5206
|
(f) => !answeredRegistry.some((e) => isAnsweredDrop(f, e))
|
|
4938
|
-
)
|
|
4939
|
-
}
|
|
5207
|
+
) : resolved.findings
|
|
5208
|
+
};
|
|
4940
5209
|
const carried = doc["scope_metastasis"];
|
|
4941
5210
|
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
4942
5211
|
if (doc["verdict"] === "error") return doc;
|
|
@@ -4980,22 +5249,27 @@ var seedDraftCmd = defineCommand({
|
|
|
4980
5249
|
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
4981
5250
|
)
|
|
4982
5251
|
) : priorFindings;
|
|
4983
|
-
const accepts = (doc) =>
|
|
4984
|
-
|
|
4985
|
-
|
|
5252
|
+
const accepts = (doc) => {
|
|
5253
|
+
const resolved = resolvedPriorValue(doc);
|
|
5254
|
+
return resolved !== null && validateAgainstSchema(resolved, schemaPath).valid ? resolved : null;
|
|
5255
|
+
};
|
|
5256
|
+
const seedDoc = accepts(priorFindings) ?? (() => {
|
|
5257
|
+
const bare = accepts(barePrior);
|
|
5258
|
+
if (bare === null) return null;
|
|
5259
|
+
process.stderr.write(
|
|
5260
|
+
`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
5261
|
`
|
|
4987
|
-
|
|
5262
|
+
);
|
|
5263
|
+
return bare;
|
|
5264
|
+
})();
|
|
4988
5265
|
if (seedDoc === null) return false;
|
|
4989
|
-
|
|
4990
|
-
if (resolution.kind !== "ok") return false;
|
|
4991
|
-
if (isIncompleteFindings(resolution.value)) return false;
|
|
5266
|
+
if (isIncompleteFindings(seedDoc)) return false;
|
|
4992
5267
|
if (parseReviewedRoute(priorBody ?? "") !== "full review") return false;
|
|
4993
5268
|
writeFileSync(outPath, SEED_SENTINEL);
|
|
4994
5269
|
writeFileSync(priorContextPath(outPath), `${JSON.stringify(seedDoc, null, 2)}
|
|
4995
5270
|
`);
|
|
4996
|
-
const count = resolution.value.findings.length;
|
|
4997
5271
|
process.stderr.write(
|
|
4998
|
-
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(
|
|
5272
|
+
`Seeded ${outPath} with the sentinel and wrote the prior review (${String(seedDoc.findings.length)} finding(s)) to ${priorContextPath(outPath)} as context
|
|
4999
5273
|
`
|
|
5000
5274
|
);
|
|
5001
5275
|
return true;
|