@jphutchins/code-review 0.1.0-alpha.53 → 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 +480 -145
- 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 +10 -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,
|
|
@@ -568,6 +770,7 @@ var findingPayload = (finding, schemaVersion) => Buffer.from(
|
|
|
568
770
|
JSON.stringify({ schema_version: schemaVersion, findings: [finding] }),
|
|
569
771
|
"utf-8"
|
|
570
772
|
).toString("base64");
|
|
773
|
+
var lineRange = (startLine, endLine, separator) => startLine === endLine ? String(startLine) : `${String(startLine)}${separator}${String(endLine)}`;
|
|
571
774
|
var findingPointer = (finding, schemaVersion, jsonUrl) => {
|
|
572
775
|
const payload = findingPayload(finding, schemaVersion);
|
|
573
776
|
if (payload.length > INLINE_EMBED_LIMIT_CHARS) {
|
|
@@ -616,24 +819,21 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
|
|
|
616
819
|
const v = u[k];
|
|
617
820
|
return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
618
821
|
});
|
|
619
|
-
var
|
|
620
|
-
var
|
|
822
|
+
var MAX_IDS_PER_ROUND = 8;
|
|
823
|
+
var hasId = (ids, code) => ids !== void 0 && Object.prototype.hasOwnProperty.call(ids, code);
|
|
621
824
|
var escapeCodeBackticks = (code) => code.replace(/`/g, "-").replace(/\r?\n/g, " ");
|
|
622
|
-
var
|
|
623
|
-
|
|
624
|
-
const entries = Object.entries(codes).filter(
|
|
625
|
-
(e) => typeof e[1] === "number" && Number.isSafeInteger(e[1]) && e[1] > 0
|
|
626
|
-
);
|
|
825
|
+
var normalizeIdCounts = (ids, priorCodes) => {
|
|
826
|
+
const entries = Object.entries(usableCountsMap(ids) ?? {});
|
|
627
827
|
if (entries.length === 0) return void 0;
|
|
628
828
|
const sorted = entries.sort((a, b) => {
|
|
629
829
|
if (b[1] !== a[1]) return b[1] - a[1];
|
|
630
|
-
const aPrior =
|
|
631
|
-
const bPrior =
|
|
830
|
+
const aPrior = hasId(priorCodes, a[0]) ? 1 : 0;
|
|
831
|
+
const bPrior = hasId(priorCodes, b[0]) ? 1 : 0;
|
|
632
832
|
if (aPrior !== bPrior) return bPrior - aPrior;
|
|
633
833
|
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
634
834
|
});
|
|
635
|
-
const base = sorted.slice(0,
|
|
636
|
-
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);
|
|
637
837
|
return Object.fromEntries([...base, ...priorKept]);
|
|
638
838
|
};
|
|
639
839
|
var parseRounds = (body) => {
|
|
@@ -645,13 +845,13 @@ var parseRounds = (body) => {
|
|
|
645
845
|
let priorCodes;
|
|
646
846
|
for (const u of decoded.filter(isSeverityCounts)) {
|
|
647
847
|
const rec = u;
|
|
648
|
-
const codes =
|
|
848
|
+
const codes = normalizeIdCounts(rec["ids"], priorCodes) ?? normalizeIdCounts(rec["codes"], priorCodes);
|
|
649
849
|
priorCodes = codes;
|
|
650
850
|
const sha = rec["sha"];
|
|
651
851
|
const shaStr = typeof sha === "string" && sha !== "" ? sha : void 0;
|
|
652
852
|
const round = rec["round"];
|
|
653
853
|
const roundNum = typeof round === "number" && Number.isSafeInteger(round) && round >= 1 ? round : void 0;
|
|
654
|
-
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 };
|
|
655
855
|
const record3 = shaStr === void 0 ? base : { ...base, sha: shaStr };
|
|
656
856
|
kept.push(roundNum === void 0 ? record3 : { ...record3, round: roundNum });
|
|
657
857
|
}
|
|
@@ -665,29 +865,31 @@ var roundsSummary = (rounds, count = rounds.length) => {
|
|
|
665
865
|
const trajectory = cells.length === 0 ? "" : rounds.length > TRAJECTORY_SCORES ? `\u2026 \u2192 ${cells.join(" \u2192 ")}` : cells.join(" \u2192 ");
|
|
666
866
|
return trajectory === "" ? `**Round ${String(count)}**` : `**Round ${String(count)}** \xB7 ${trajectory}`;
|
|
667
867
|
};
|
|
668
|
-
var
|
|
868
|
+
var computeIdCounts = (findings, systemic = []) => {
|
|
669
869
|
const counts = /* @__PURE__ */ new Map();
|
|
670
870
|
for (const code of [
|
|
671
|
-
|
|
672
|
-
|
|
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 ?? []])
|
|
673
875
|
]) {
|
|
674
876
|
if (code === void 0 || code === "") continue;
|
|
675
877
|
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
676
878
|
}
|
|
677
879
|
return Object.fromEntries(counts);
|
|
678
880
|
};
|
|
679
|
-
var
|
|
881
|
+
var consecutiveIdStreaks = (rounds) => {
|
|
680
882
|
const entries = [];
|
|
681
883
|
if (rounds.length === 0) return {};
|
|
682
|
-
const lastCodes = rounds[rounds.length - 1]?.
|
|
884
|
+
const lastCodes = rounds[rounds.length - 1]?.ids;
|
|
683
885
|
if (lastCodes === void 0) return {};
|
|
684
886
|
for (const code of Object.keys(lastCodes)) {
|
|
685
887
|
let streak = 0;
|
|
686
888
|
let startIndex = rounds.length;
|
|
687
889
|
for (let i = rounds.length - 1; i >= 0; i--) {
|
|
688
|
-
const codes = rounds[i]?.
|
|
689
|
-
if (codes === void 0 || !
|
|
690
|
-
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)) {
|
|
691
893
|
continue;
|
|
692
894
|
}
|
|
693
895
|
streak += 1;
|
|
@@ -701,12 +903,12 @@ var consecutiveCodeStreaks = (rounds) => {
|
|
|
701
903
|
};
|
|
702
904
|
var DEFAULT_METASTASIS_STREAK = 3;
|
|
703
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.";
|
|
704
|
-
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 }));
|
|
705
907
|
var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
706
|
-
const flagged =
|
|
908
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
707
909
|
if (flagged.length === 0) return "";
|
|
708
910
|
const lines = flagged.map(
|
|
709
|
-
({
|
|
911
|
+
({ id, streak }) => `> **\`${escapeCodeBackticks(id)}\`** \u2014 findings in ${String(streak.streak)} consecutive rounds.`
|
|
710
912
|
);
|
|
711
913
|
return [
|
|
712
914
|
"> [!WARNING]",
|
|
@@ -715,27 +917,27 @@ var metastasisNote = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
|
715
917
|
].join("\n");
|
|
716
918
|
};
|
|
717
919
|
var computeScopeMetastasis = (rounds, minStreak = DEFAULT_METASTASIS_STREAK) => {
|
|
718
|
-
const flagged =
|
|
920
|
+
const flagged = flaggedIdStreaks(rounds, minStreak);
|
|
719
921
|
if (flagged.length === 0) return null;
|
|
720
922
|
return {
|
|
721
923
|
decision_prompt: SCOPE_METASTASIS_DECISION_PROMPT,
|
|
722
|
-
recurring: flagged.map(({
|
|
723
|
-
|
|
924
|
+
recurring: flagged.map(({ id, streak }) => ({
|
|
925
|
+
id,
|
|
724
926
|
consecutive_rounds: streak.streak,
|
|
725
927
|
start_round: streak.startRound
|
|
726
928
|
}))
|
|
727
929
|
};
|
|
728
930
|
};
|
|
729
931
|
var computeSameRootNotes = (priorRounds, findings, currentSha) => {
|
|
730
|
-
const codes = findings.map((f) => f
|
|
932
|
+
const codes = findings.map((f) => resolveFindingId(f));
|
|
731
933
|
const entries = [];
|
|
732
934
|
for (const code of codes) {
|
|
733
935
|
let lastRound = 0;
|
|
734
936
|
for (let i = priorRounds.length - 1; i >= 0; i--) {
|
|
735
937
|
if (currentSha !== void 0 && priorRounds[i]?.sha === currentSha) continue;
|
|
736
|
-
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))
|
|
737
939
|
continue;
|
|
738
|
-
const count = priorRounds[i]?.
|
|
940
|
+
const count = priorRounds[i]?.ids?.[code];
|
|
739
941
|
if (count !== void 0 && count > 0) {
|
|
740
942
|
lastRound = priorRounds[i]?.round ?? i + 1;
|
|
741
943
|
break;
|
|
@@ -769,13 +971,13 @@ var convergenceScore = (doc, threshold) => round2(
|
|
|
769
971
|
0
|
|
770
972
|
)
|
|
771
973
|
);
|
|
772
|
-
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1,
|
|
974
|
+
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha) => {
|
|
773
975
|
const score = convergenceScore(doc, threshold);
|
|
774
|
-
const normalized =
|
|
976
|
+
const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
|
|
775
977
|
const current = {
|
|
776
978
|
round,
|
|
777
979
|
score,
|
|
778
|
-
...normalized !== void 0 ? {
|
|
980
|
+
...normalized !== void 0 ? { ids: { ...normalized } } : {},
|
|
779
981
|
...sha !== void 0 ? { sha } : {}
|
|
780
982
|
};
|
|
781
983
|
const rounds = [...priorRounds, current].slice(-64);
|
|
@@ -794,11 +996,16 @@ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
|
|
|
794
996
|
if (!isBelowVisibilityFloor(rec, floor)) continue;
|
|
795
997
|
const title = rec["title"];
|
|
796
998
|
if (typeof title !== "string") continue;
|
|
797
|
-
const code = typeof rec["code"] === "string" && rec["code"] !== "" ? rec["code"] : void 0;
|
|
798
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
|
+
});
|
|
799
1006
|
nits.push({
|
|
800
1007
|
title,
|
|
801
|
-
...
|
|
1008
|
+
...id !== void 0 ? { id } : {},
|
|
802
1009
|
...path !== void 0 ? { path } : {}
|
|
803
1010
|
});
|
|
804
1011
|
}
|
|
@@ -851,8 +1058,33 @@ var parseSurfaceSignal = (doc) => {
|
|
|
851
1058
|
convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
|
|
852
1059
|
};
|
|
853
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
|
+
};
|
|
854
1086
|
var validStampedConvergence = (raw) => {
|
|
855
|
-
const decoded = ConvergenceCodec.decode(raw);
|
|
1087
|
+
const decoded = ConvergenceCodec.decode(withLegacyConvergenceIds(raw));
|
|
856
1088
|
return decoded._tag === "Right" && decoded.right.rounds !== void 0 && decoded.right.rounds.length > 0 ? decoded.right : null;
|
|
857
1089
|
};
|
|
858
1090
|
var parseConvergence = (priorDoc) => {
|
|
@@ -877,7 +1109,7 @@ var parseConvergenceMarker = (body) => {
|
|
|
877
1109
|
};
|
|
878
1110
|
var roundRecordsToConvergenceRounds = (records) => records.map((r, i) => ({
|
|
879
1111
|
round: r.round ?? i + 1,
|
|
880
|
-
...r.
|
|
1112
|
+
...r.ids !== void 0 ? { ids: { ...r.ids } } : {},
|
|
881
1113
|
...r.sha !== void 0 ? { sha: r.sha } : {}
|
|
882
1114
|
}));
|
|
883
1115
|
var priorTrajectory = (priorDoc, priorBody) => parseConvergence(priorDoc)?.rounds ?? parseConvergenceMarker(priorBody)?.rounds ?? roundRecordsToConvergenceRounds(parseRounds(priorBody));
|
|
@@ -1111,12 +1343,22 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1111
1343
|
const title = first["title"];
|
|
1112
1344
|
const description = first["description"];
|
|
1113
1345
|
const reasoning = first["reasoning"];
|
|
1114
|
-
const
|
|
1346
|
+
const id = first["id"];
|
|
1347
|
+
const legacyCode = first["code"];
|
|
1115
1348
|
const severity = first["severity"];
|
|
1116
1349
|
const path = first["path"];
|
|
1117
1350
|
const patch = first["patch"];
|
|
1118
1351
|
return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
|
|
1119
|
-
|
|
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),
|
|
1120
1362
|
title,
|
|
1121
1363
|
description,
|
|
1122
1364
|
reasoning,
|
|
@@ -1158,29 +1400,31 @@ var answeredRegistryFrom = (comments, botLogin) => {
|
|
|
1158
1400
|
for (const entry of [...entries].sort(
|
|
1159
1401
|
(a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
|
|
1160
1402
|
)) {
|
|
1161
|
-
const key2 = answeredNoteKey(entry);
|
|
1403
|
+
const key2 = answeredNoteKey({ id: entry.code, title: entry.title });
|
|
1162
1404
|
if (!byKey.has(key2)) byKey.set(key2, entry);
|
|
1163
1405
|
}
|
|
1164
1406
|
return [...byKey.values()];
|
|
1165
1407
|
};
|
|
1166
|
-
var matches = (f, e) =>
|
|
1408
|
+
var matches = (f, e) => e.code === resolveFindingId(f) || isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1167
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;
|
|
1168
1410
|
var isAnsweredDrop = (f, e) => matches(f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
|
|
1169
|
-
var answeredNoteKey = (f) => f.
|
|
1411
|
+
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1170
1412
|
var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
|
|
1171
1413
|
var applyAnswered = (findings, registry) => {
|
|
1172
1414
|
const kept = [];
|
|
1173
1415
|
const noteEntries = [];
|
|
1174
|
-
const
|
|
1416
|
+
const droppedByEntry = /* @__PURE__ */ new Map();
|
|
1417
|
+
const droppedFindingIds = [];
|
|
1175
1418
|
let droppedCount = 0;
|
|
1176
1419
|
for (const f of findings) {
|
|
1177
|
-
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);
|
|
1178
1421
|
if (entry === void 0) {
|
|
1179
1422
|
kept.push(f);
|
|
1180
1423
|
continue;
|
|
1181
1424
|
}
|
|
1182
1425
|
if (isAnsweredDrop(f, entry)) {
|
|
1183
|
-
|
|
1426
|
+
droppedByEntry.set(entry.replyId, entry);
|
|
1427
|
+
droppedFindingIds.push(resolveFindingId(f));
|
|
1184
1428
|
droppedCount += 1;
|
|
1185
1429
|
} else {
|
|
1186
1430
|
kept.push(f);
|
|
@@ -1190,7 +1434,8 @@ var applyAnswered = (findings, registry) => {
|
|
|
1190
1434
|
return {
|
|
1191
1435
|
findings: kept,
|
|
1192
1436
|
reRaisedNotes: Object.fromEntries(noteEntries),
|
|
1193
|
-
verbatimReRaised: [...
|
|
1437
|
+
verbatimReRaised: [...droppedByEntry.values()],
|
|
1438
|
+
droppedFindingIds,
|
|
1194
1439
|
droppedCount
|
|
1195
1440
|
};
|
|
1196
1441
|
};
|
|
@@ -1232,17 +1477,42 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
|
|
|
1232
1477
|
|
|
1233
1478
|
// src/render.ts
|
|
1234
1479
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1235
|
-
var
|
|
1480
|
+
var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
1481
|
+
var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
|
|
1482
|
+
var permalinkFor = (base, f, anchor) => {
|
|
1483
|
+
if (f.path === "" || f.side === "LEFT") return void 0;
|
|
1484
|
+
const path = f.path.split("/").map(
|
|
1485
|
+
(segment) => encodeAutolinkParens(
|
|
1486
|
+
encodeURIComponent(
|
|
1487
|
+
segment.replace(
|
|
1488
|
+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
|
|
1489
|
+
"\uFFFD"
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
// Markdown emphasis/syntax characters: `__` runs split the bare URL across CommonMark
|
|
1493
|
+
// nodes and truncate the autolink, and a path-only link ending in one of these loses its
|
|
1494
|
+
// tail to the autolinker's trailing-punctuation trim (issue #231 r4).
|
|
1495
|
+
).replace(/_/g, "%5F").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/!/g, "%21")
|
|
1496
|
+
).join("/");
|
|
1497
|
+
return anchor ? `${base}${path}#L${lineRange(f.start_line, f.end_line, "-L")}` : `${base}${path}`;
|
|
1498
|
+
};
|
|
1236
1499
|
var CARRIED_TOTAL_CHARS = 4e4;
|
|
1237
1500
|
var SUPPRESSED_NIT_BLOCK_OVERHEAD = 280;
|
|
1238
|
-
var sanitizeFinding = (f, answeredNotes) => {
|
|
1501
|
+
var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
|
|
1239
1502
|
const key2 = answeredNoteKey(f);
|
|
1503
|
+
const anchored = permalinkBase !== void 0 && !(unanchored?.has(f) ?? false);
|
|
1504
|
+
const permalink = permalinkBase === void 0 ? void 0 : permalinkFor(permalinkBase, f, anchored);
|
|
1240
1505
|
return {
|
|
1241
1506
|
...f,
|
|
1242
1507
|
title: escapePipes(f.title),
|
|
1243
1508
|
path: escapeCodeBackticks(f.path),
|
|
1244
|
-
|
|
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),
|
|
1245
1513
|
...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
|
|
1514
|
+
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1515
|
+
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
1246
1516
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1247
1517
|
answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
|
|
1248
1518
|
};
|
|
@@ -1250,7 +1520,7 @@ var sanitizeFinding = (f, answeredNotes) => {
|
|
|
1250
1520
|
var commentSafe = (text) => text.replace(/--+(?=>)/g, (dashes) => `${dashes}\u200B`);
|
|
1251
1521
|
var sanitizeSuppressedNit = (f) => ({
|
|
1252
1522
|
title: escapeCodeBackticks(f.title),
|
|
1253
|
-
|
|
1523
|
+
id: escapeCodeBackticks(f.id),
|
|
1254
1524
|
...f.code_url !== void 0 ? { codeUrl: linkSafeUrl(f.code_url) } : {},
|
|
1255
1525
|
path: commentSafe(escapeCodeBackticks(f.path)),
|
|
1256
1526
|
startLine: f.start_line,
|
|
@@ -1271,10 +1541,10 @@ var carriedLines = (f) => [
|
|
|
1271
1541
|
var sanitizeSystemic = (s) => ({
|
|
1272
1542
|
...s,
|
|
1273
1543
|
title: escapePipes(s.title),
|
|
1274
|
-
...s.
|
|
1544
|
+
...s.id !== void 0 ? { id: escapeCodeBackticks(s.id) } : {},
|
|
1275
1545
|
...s.code_url !== void 0 ? { code_url: linkSafeUrl(s.code_url) } : {},
|
|
1276
1546
|
...s.paths !== void 0 ? { paths: s.paths.map(escapeCodeBackticks) } : {},
|
|
1277
|
-
...s.
|
|
1547
|
+
...s.finding_ids !== void 0 ? { finding_ids: s.finding_ids.map(escapeCodeBackticks) } : {}
|
|
1278
1548
|
});
|
|
1279
1549
|
var emptySeverityCounts = () => ({
|
|
1280
1550
|
critical: 0,
|
|
@@ -1307,12 +1577,14 @@ var render = (input) => {
|
|
|
1307
1577
|
const advisoryAllowed = isFullReviewRound;
|
|
1308
1578
|
const suppressedBudget = (input.suppressedNits ?? []).map(sanitizeSuppressedNit).reduce(
|
|
1309
1579
|
(acc, n) => {
|
|
1310
|
-
const size = n.carried.reduce((sum, line) => sum + line.length + 3, 0) + SUPPRESSED_NIT_BLOCK_OVERHEAD + n.title.length + n.path.length * 2 +
|
|
1311
|
-
(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);
|
|
1312
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 };
|
|
1313
1583
|
},
|
|
1314
1584
|
{ list: [], used: 0, dropped: 0 }
|
|
1315
1585
|
);
|
|
1586
|
+
const permalinkBase = input.repo !== void 0 && input.repo !== "" && input.reviewedSha !== void 0 && input.reviewedSha !== "" ? `https://github.com/${input.repo}/blob/${input.reviewedSha}/` : void 0;
|
|
1587
|
+
const unanchored = new Set(input.unanchoredStrays ?? []);
|
|
1316
1588
|
return eta.renderString(input.template, {
|
|
1317
1589
|
findings: input.findings,
|
|
1318
1590
|
envelope: input.envelope,
|
|
@@ -1333,7 +1605,9 @@ var render = (input) => {
|
|
|
1333
1605
|
postedAt: input.postedAt ?? "",
|
|
1334
1606
|
severityCounts,
|
|
1335
1607
|
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
1336
|
-
strays: (input.strays ?? []).map(
|
|
1608
|
+
strays: (input.strays ?? []).map(
|
|
1609
|
+
(f) => sanitizeFinding(f, input.answeredNotes, permalinkBase, unanchored)
|
|
1610
|
+
),
|
|
1337
1611
|
suppressedNits: suppressedBudget.list,
|
|
1338
1612
|
carriedDroppedNits: suppressedBudget.dropped,
|
|
1339
1613
|
nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
|
|
@@ -1456,11 +1730,7 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1456
1730
|
const { inDiff, strays } = partitionFindings(findings, index);
|
|
1457
1731
|
const eta = new Eta({ autoTrim: false });
|
|
1458
1732
|
const modelsText = formatModels(models);
|
|
1459
|
-
const noteFor = (
|
|
1460
|
-
if (notes === void 0) return "";
|
|
1461
|
-
const key2 = answeredNoteKey(f);
|
|
1462
|
-
return Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1463
|
-
};
|
|
1733
|
+
const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1464
1734
|
const comments = inDiff.map((f) => {
|
|
1465
1735
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1466
1736
|
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
@@ -1472,8 +1742,8 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1472
1742
|
reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
|
|
1473
1743
|
...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
|
|
1474
1744
|
} : f;
|
|
1475
|
-
const sameRootNote = noteFor(
|
|
1476
|
-
const answeredNote2 = noteFor(
|
|
1745
|
+
const sameRootNote = noteFor(context.sameRootNotes, resolveFindingId(f));
|
|
1746
|
+
const answeredNote2 = noteFor(context.answeredNotes, answeredNoteKey(f));
|
|
1477
1747
|
const comment = {
|
|
1478
1748
|
path: f.path,
|
|
1479
1749
|
line: f.end_line,
|
|
@@ -1917,33 +2187,47 @@ var buildUnknownNoticeEnvelope = (kind) => noticeEnvelope(
|
|
|
1917
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.`
|
|
1918
2188
|
);
|
|
1919
2189
|
var identity = (decoded) => decoded;
|
|
2190
|
+
var legacyFindingsCodec = FindingsCodecV09;
|
|
2191
|
+
var legacyFindingsNormalize = (doc) => normalizeV09(doc);
|
|
1920
2192
|
var findingsTable = [
|
|
1921
2193
|
{
|
|
1922
2194
|
minor: "0.4",
|
|
1923
2195
|
defaultVersion: "0.4.0",
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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,
|
|
1927
2203
|
latest: false
|
|
1928
2204
|
},
|
|
1929
2205
|
{
|
|
1930
2206
|
minor: "0.5",
|
|
1931
2207
|
defaultVersion: "0.5.0",
|
|
1932
|
-
schemaFile: "findings.schema.json",
|
|
1933
|
-
codec:
|
|
1934
|
-
normalize:
|
|
2208
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2209
|
+
codec: legacyFindingsCodec,
|
|
2210
|
+
normalize: legacyFindingsNormalize,
|
|
1935
2211
|
latest: false
|
|
1936
2212
|
},
|
|
1937
2213
|
{
|
|
1938
2214
|
minor: "0.6",
|
|
1939
2215
|
defaultVersion: "0.6.0",
|
|
1940
|
-
schemaFile: "findings.schema.json",
|
|
1941
|
-
codec:
|
|
1942
|
-
normalize:
|
|
2216
|
+
schemaFile: "v0.9/findings.schema.json",
|
|
2217
|
+
codec: legacyFindingsCodec,
|
|
2218
|
+
normalize: legacyFindingsNormalize,
|
|
1943
2219
|
latest: false
|
|
1944
2220
|
},
|
|
1945
2221
|
{
|
|
1946
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",
|
|
1947
2231
|
defaultVersion: DEFAULT_SCHEMA_VERSION,
|
|
1948
2232
|
schemaFile: "findings.schema.json",
|
|
1949
2233
|
codec: FindingsCodec,
|
|
@@ -2037,6 +2321,13 @@ var resolvers = {
|
|
|
2037
2321
|
prices: (raw) => resolveSingleVersion("prices", raw)
|
|
2038
2322
|
};
|
|
2039
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
|
+
};
|
|
2040
2331
|
var MAX_BUFFER = 100 * 1024 * 1024;
|
|
2041
2332
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
2042
2333
|
var parseTimeoutMs = (raw, fallback) => {
|
|
@@ -2083,14 +2374,32 @@ var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
|
|
|
2083
2374
|
|
|
2084
2375
|
// src/gh.ts
|
|
2085
2376
|
var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
|
|
2086
|
-
var
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2377
|
+
var flagFirst;
|
|
2378
|
+
var withEscapeRetry = async (run2, idempotent) => {
|
|
2379
|
+
if (flagFirst) return run2(true);
|
|
2380
|
+
try {
|
|
2381
|
+
return await run2(false);
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
if (errMsg(err).includes("--allow-escape-sequences")) {
|
|
2384
|
+
if (!idempotent) throw err;
|
|
2385
|
+
flagFirst = true;
|
|
2386
|
+
return run2(true);
|
|
2387
|
+
}
|
|
2388
|
+
throw err;
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
|
|
2392
|
+
var runGhApi = (args, stdin, env) => withEscapeRetry(
|
|
2393
|
+
(withFlag) => execFileWithTimeout({
|
|
2394
|
+
command: "gh",
|
|
2395
|
+
args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
|
|
2396
|
+
label: `gh api ${describeEndpoint(args)}`,
|
|
2397
|
+
timeoutMs: subprocessTimeoutMs(),
|
|
2398
|
+
env,
|
|
2399
|
+
stdin
|
|
2400
|
+
}),
|
|
2401
|
+
isIdempotentCall(args)
|
|
2402
|
+
);
|
|
2094
2403
|
var run = promisify(execFile);
|
|
2095
2404
|
var STEP_TIMEOUT_MS = 6e4;
|
|
2096
2405
|
var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
|
|
@@ -2143,11 +2452,14 @@ var readArtifactFindings = (ghApiPath) => {
|
|
|
2143
2452
|
};
|
|
2144
2453
|
};
|
|
2145
2454
|
var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
|
|
2146
|
-
const { stdout } = await
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2455
|
+
const { stdout } = await withEscapeRetry(
|
|
2456
|
+
(withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
|
|
2457
|
+
encoding: "buffer",
|
|
2458
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
2459
|
+
timeout: STEP_TIMEOUT_MS
|
|
2460
|
+
}),
|
|
2461
|
+
true
|
|
2462
|
+
);
|
|
2151
2463
|
await writeFile(outPath, stdout);
|
|
2152
2464
|
});
|
|
2153
2465
|
var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
|
|
@@ -2813,15 +3125,16 @@ ${dropNote}` : ""}`,
|
|
|
2813
3125
|
const reRaisedNotes = answeredFilter.reRaisedNotes;
|
|
2814
3126
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
2815
3127
|
const droppedCount = answeredFilter.droppedCount;
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
answeredFilter.
|
|
2819
|
-
);
|
|
2820
|
-
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)));
|
|
2821
3134
|
const systemic = trulyDropped.size === 0 ? loadedFindings.systemic_problems ?? [] : (loadedFindings.systemic_problems ?? []).map((s) => {
|
|
2822
|
-
if (s.
|
|
2823
|
-
const codes = s.
|
|
2824
|
-
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 };
|
|
2825
3138
|
});
|
|
2826
3139
|
const findings = {
|
|
2827
3140
|
...loadedFindings,
|
|
@@ -2832,10 +3145,13 @@ ${dropNote}` : ""}`,
|
|
|
2832
3145
|
const priorDocForNits = roundHasNit && existingSticky !== null && isFullReviewSticky(existingSticky.body) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
2833
3146
|
const priorSuppressedKeys = new Set(
|
|
2834
3147
|
priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
|
|
2835
|
-
(n) => answeredNoteKey({
|
|
3148
|
+
(n) => answeredNoteKey({ id: n.id ?? "", title: n.title })
|
|
2836
3149
|
)
|
|
2837
3150
|
);
|
|
2838
|
-
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}`));
|
|
2839
3155
|
const suppressedNits = findings.findings.filter(isSuppressedNit);
|
|
2840
3156
|
const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
|
|
2841
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._" : "");
|
|
@@ -2862,6 +3178,7 @@ ${dropNote}` : ""}`,
|
|
|
2862
3178
|
template,
|
|
2863
3179
|
route: effectiveRoute,
|
|
2864
3180
|
reviewedSha: input.headSha,
|
|
3181
|
+
repo: input.headRepo || input.repo,
|
|
2865
3182
|
effort: input.effort,
|
|
2866
3183
|
sameRootNotes: {},
|
|
2867
3184
|
// The answered-state honesty rules apply on EVERY surface that renders the filtered
|
|
@@ -2932,7 +3249,7 @@ ${dropNote}` : ""}`,
|
|
|
2932
3249
|
const botReviews = await fetchBotReviews(input.repo, prNumber, input.botLogin, ghApi);
|
|
2933
3250
|
const initialDisposition = !inlineRequested ? { kind: "disabled" } : comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
|
|
2934
3251
|
const currentCounts = computeSeverityCounts(findings.findings);
|
|
2935
|
-
const currentCodes =
|
|
3252
|
+
const currentCodes = computeIdCounts(findings.findings, findings.systemic_problems ?? []);
|
|
2936
3253
|
const roundNumber = priorRoundCount + 1;
|
|
2937
3254
|
const convergence = isRound ? buildConvergence(
|
|
2938
3255
|
findings,
|
|
@@ -2954,6 +3271,7 @@ ${dropNote}` : ""}`,
|
|
|
2954
3271
|
template,
|
|
2955
3272
|
route: effectiveRoute,
|
|
2956
3273
|
reviewedSha: input.headSha,
|
|
3274
|
+
repo: input.headRepo || input.repo,
|
|
2957
3275
|
effort: input.effort,
|
|
2958
3276
|
testReport,
|
|
2959
3277
|
clocDiff,
|
|
@@ -2980,11 +3298,12 @@ ${dropNote}` : ""}`,
|
|
|
2980
3298
|
|
|
2981
3299
|
> **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.
|
|
2982
3300
|
` : "";
|
|
2983
|
-
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
|
|
3301
|
+
const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays) => formatMarkdown(
|
|
2984
3302
|
render({
|
|
2985
3303
|
...commonRenderInput,
|
|
2986
3304
|
...straysOverride ? { strays: straysOverride } : {},
|
|
2987
3305
|
...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
|
|
3306
|
+
...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
|
|
2988
3307
|
inlineDisposition,
|
|
2989
3308
|
reviewUrl: reviewUrl2
|
|
2990
3309
|
}) + longFilesNote
|
|
@@ -3041,7 +3360,7 @@ ${dropNote}` : ""}`,
|
|
|
3041
3360
|
await patchComment(
|
|
3042
3361
|
input.repo,
|
|
3043
3362
|
stickyRef.id,
|
|
3044
|
-
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
|
|
3363
|
+
renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount, unposted),
|
|
3045
3364
|
ghApi
|
|
3046
3365
|
);
|
|
3047
3366
|
process.stderr.write(
|
|
@@ -3060,7 +3379,13 @@ ${dropNote}` : ""}`,
|
|
|
3060
3379
|
() => renderBody(
|
|
3061
3380
|
{ kind: "whole-document", inlineCount: inlinePosted, rejectedCount: unanchoredCount },
|
|
3062
3381
|
reviewUrl,
|
|
3063
|
-
visibleFindings
|
|
3382
|
+
visibleFindings,
|
|
3383
|
+
void 0,
|
|
3384
|
+
// The rejected-anchor invariant holds on EVERY surface, the run summary included — this
|
|
3385
|
+
// document deliberately carries every finding, so the GitHub-rejected ones must keep their
|
|
3386
|
+
// path-only links here too (issue #231 r2). Unconditional: renderBody's own gate treats an
|
|
3387
|
+
// empty array exactly like absence, so the ternary was a duplicated decision (issue #231 r3).
|
|
3388
|
+
unposted
|
|
3064
3389
|
)
|
|
3065
3390
|
);
|
|
3066
3391
|
};
|
|
@@ -4096,6 +4421,7 @@ var parseScope = (raw) => {
|
|
|
4096
4421
|
};
|
|
4097
4422
|
|
|
4098
4423
|
// src/index.ts
|
|
4424
|
+
var resolvedPriorValue = resolveTolerantFindings;
|
|
4099
4425
|
var readJSON = (path) => {
|
|
4100
4426
|
try {
|
|
4101
4427
|
return JSON.parse(readFileSync(resolve$1(path), "utf-8"));
|
|
@@ -4873,13 +5199,13 @@ var seedDraftCmd = defineCommand({
|
|
|
4873
5199
|
const priorFindings = (() => {
|
|
4874
5200
|
if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
|
|
4875
5201
|
return strippedPrior;
|
|
4876
|
-
const
|
|
4877
|
-
const doc =
|
|
4878
|
-
...
|
|
4879
|
-
findings:
|
|
5202
|
+
const resolved = resolvedPriorValue(strippedPrior);
|
|
5203
|
+
const doc = resolved === null ? strippedPrior : {
|
|
5204
|
+
...resolved,
|
|
5205
|
+
findings: answeredRegistry !== null && answeredRegistry.length > 0 ? resolved.findings.filter(
|
|
4880
5206
|
(f) => !answeredRegistry.some((e) => isAnsweredDrop(f, e))
|
|
4881
|
-
)
|
|
4882
|
-
}
|
|
5207
|
+
) : resolved.findings
|
|
5208
|
+
};
|
|
4883
5209
|
const carried = doc["scope_metastasis"];
|
|
4884
5210
|
if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
|
|
4885
5211
|
if (doc["verdict"] === "error") return doc;
|
|
@@ -4923,22 +5249,27 @@ var seedDraftCmd = defineCommand({
|
|
|
4923
5249
|
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
4924
5250
|
)
|
|
4925
5251
|
) : priorFindings;
|
|
4926
|
-
const accepts = (doc) =>
|
|
4927
|
-
|
|
4928
|
-
|
|
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)
|
|
4929
5261
|
`
|
|
4930
|
-
|
|
5262
|
+
);
|
|
5263
|
+
return bare;
|
|
5264
|
+
})();
|
|
4931
5265
|
if (seedDoc === null) return false;
|
|
4932
|
-
|
|
4933
|
-
if (resolution.kind !== "ok") return false;
|
|
4934
|
-
if (isIncompleteFindings(resolution.value)) return false;
|
|
5266
|
+
if (isIncompleteFindings(seedDoc)) return false;
|
|
4935
5267
|
if (parseReviewedRoute(priorBody ?? "") !== "full review") return false;
|
|
4936
5268
|
writeFileSync(outPath, SEED_SENTINEL);
|
|
4937
5269
|
writeFileSync(priorContextPath(outPath), `${JSON.stringify(seedDoc, null, 2)}
|
|
4938
5270
|
`);
|
|
4939
|
-
const count = resolution.value.findings.length;
|
|
4940
5271
|
process.stderr.write(
|
|
4941
|
-
`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
|
|
4942
5273
|
`
|
|
4943
5274
|
);
|
|
4944
5275
|
return true;
|
|
@@ -5447,6 +5778,10 @@ var postCmd = defineCommand({
|
|
|
5447
5778
|
const priceResolution = resolvePrices(args.prices);
|
|
5448
5779
|
await post({
|
|
5449
5780
|
repo: args.repo,
|
|
5781
|
+
// The workflow's post step threads HEAD_REPO env (the fork's owner/name) — a finding
|
|
5782
|
+
// permalink targets the tree the reviewed SHA lives in (issue #231 r1). Absent/empty ⇒ the
|
|
5783
|
+
// base repo. Env rather than a flag: an older pinned CLI simply ignores it.
|
|
5784
|
+
headRepo: process.env["HEAD_REPO"] || void 0,
|
|
5450
5785
|
headSha: args["head-sha"],
|
|
5451
5786
|
botLogin: args["bot-login"] || "github-actions[bot]",
|
|
5452
5787
|
findingsPath: args.findings,
|