@jphutchins/code-review 0.1.0-alpha.65 → 0.1.0-alpha.67
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/dist/index.js +798 -330
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/VERSIONING.md +1 -1
- package/schema/findings.schema.json +5 -0
- package/templates/comment.eta +76 -16
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { execFile } from 'child_process';
|
|
|
9
9
|
import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
|
|
10
10
|
import { tmpdir } from 'os';
|
|
11
11
|
import { promisify } from 'util';
|
|
12
|
+
import { isRight } from 'fp-ts/lib/Either.js';
|
|
12
13
|
import parseDiff from 'parse-diff';
|
|
13
14
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
14
15
|
import _addFormats from 'ajv-formats';
|
|
@@ -69,6 +70,11 @@ var LineNumber = t.refinement(
|
|
|
69
70
|
"LineNumber"
|
|
70
71
|
);
|
|
71
72
|
var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
|
|
73
|
+
var NonEmptyString = t.refinement(
|
|
74
|
+
t.string,
|
|
75
|
+
(s) => s.trim().length > 0,
|
|
76
|
+
"NonEmptyString"
|
|
77
|
+
);
|
|
72
78
|
var Likelihood = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Likelihood");
|
|
73
79
|
var FiniteNumber = t.refinement(t.number, (n) => Number.isFinite(n), "FiniteNumber");
|
|
74
80
|
var NonNegativePrice = t.refinement(t.number, (n) => n >= 0, "NonNegativePrice");
|
|
@@ -235,7 +241,8 @@ var FindingsCodec = t.exact(
|
|
|
235
241
|
// Pipeline-stamped only (issue #174); see ConvergenceCodec.
|
|
236
242
|
convergence: ConvergenceCodec,
|
|
237
243
|
// Agent-written best-effort (issue #182); see ChangeSizeCodec.
|
|
238
|
-
change_size: ChangeSizeCodec
|
|
244
|
+
change_size: ChangeSizeCodec,
|
|
245
|
+
unfinished: t.array(NonEmptyString)
|
|
239
246
|
})
|
|
240
247
|
])
|
|
241
248
|
);
|
|
@@ -245,8 +252,31 @@ var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set([
|
|
|
245
252
|
]);
|
|
246
253
|
var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
|
|
247
254
|
...PIPELINE_STAMPED_FIELDS,
|
|
248
|
-
"change_size"
|
|
255
|
+
"change_size",
|
|
256
|
+
"unfinished"
|
|
249
257
|
]);
|
|
258
|
+
var withoutBrokenOptionalFields = (doc, accepts) => {
|
|
259
|
+
const bare = Object.fromEntries(
|
|
260
|
+
Object.entries(doc).filter(([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2))
|
|
261
|
+
);
|
|
262
|
+
if (!accepts(bare)) return null;
|
|
263
|
+
const usable = (kept, key2, value) => {
|
|
264
|
+
if (accepts({ ...kept, [key2]: value })) return value;
|
|
265
|
+
if (!Array.isArray(value)) return void 0;
|
|
266
|
+
const entries = value.filter((entry) => accepts({ ...kept, [key2]: [entry] }));
|
|
267
|
+
return entries.length > 0 && accepts({ ...kept, [key2]: entries }) ? entries : void 0;
|
|
268
|
+
};
|
|
269
|
+
const restored = Object.entries(doc).filter(([key2]) => RECOVERABLE_OPTIONAL_FIELDS.has(key2)).reduce((kept, [key2, value]) => {
|
|
270
|
+
const restoredValue = usable(kept, key2, value);
|
|
271
|
+
return restoredValue === void 0 ? kept : { ...kept, [key2]: restoredValue };
|
|
272
|
+
}, bare);
|
|
273
|
+
return {
|
|
274
|
+
doc: restored,
|
|
275
|
+
stripped: Object.keys(doc).filter(
|
|
276
|
+
(key2) => !Object.hasOwn(restored, key2) || restored[key2] !== doc[key2]
|
|
277
|
+
)
|
|
278
|
+
};
|
|
279
|
+
};
|
|
250
280
|
var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
251
281
|
var priorIdsFrom = (doc) => {
|
|
252
282
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
|
|
@@ -272,11 +302,28 @@ var withoutRebuttals = (doc) => ({
|
|
|
272
302
|
findings: doc.findings.map(({ rebuttal: _rebuttal, ...f }) => f),
|
|
273
303
|
...doc.systemic_problems !== void 0 ? { systemic_problems: doc.systemic_problems.map(({ rebuttal: _rebuttal, ...s }) => s) } : {}
|
|
274
304
|
});
|
|
305
|
+
var withSystemicProblems = ({ systemic_problems: _replaced, ...doc }, systemic) => ({
|
|
306
|
+
...doc,
|
|
307
|
+
...systemic.length > 0 ? { systemic_problems: [...systemic] } : {}
|
|
308
|
+
});
|
|
275
309
|
var synthesizedId = (parts, prefix) => `${prefix}${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 12)}`;
|
|
276
310
|
var synthesizedFindingId = (path, title) => synthesizedId([path, title], "f-");
|
|
277
311
|
var synthesizedSystemicId = (title) => synthesizedId([title], "s-");
|
|
278
312
|
var isSynthesizedFindingId = (id) => /^f-[A-Za-z0-9_-]{12}$/.test(id);
|
|
279
313
|
var resolveFindingId = (f) => f.id !== "" ? f.id : synthesizedFindingId(f.path, f.title);
|
|
314
|
+
var resolveSystemicId = (s) => s.id !== void 0 && s.id !== "" ? s.id : s.title !== "" ? synthesizedSystemicId(s.title) : void 0;
|
|
315
|
+
var findingIdIn = (ids) => (f) => ids.has(resolveFindingId(f));
|
|
316
|
+
var systemicIdIn = (ids) => (s) => {
|
|
317
|
+
const id = resolveSystemicId(s);
|
|
318
|
+
return id !== void 0 && ids.has(id);
|
|
319
|
+
};
|
|
320
|
+
var withoutIds = (doc, ids) => ids.size === 0 ? doc : {
|
|
321
|
+
...withSystemicProblems(
|
|
322
|
+
doc,
|
|
323
|
+
(doc.systemic_problems ?? []).filter((s) => !systemicIdIn(ids)(s))
|
|
324
|
+
),
|
|
325
|
+
findings: doc.findings.filter((f) => !findingIdIn(ids)(f))
|
|
326
|
+
};
|
|
280
327
|
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;
|
|
281
328
|
var usableCountsMap = (v) => {
|
|
282
329
|
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
@@ -664,6 +711,369 @@ var warnStalePrices = (prices, warn = defaultWarn) => {
|
|
|
664
711
|
`::warning:: code-review cost: the price map was last verified ${prices._updated}, before this CLI's release (${RELEASED}) \u2014 the map may miss pricing this CLI ships; re-verify against the provider's pricing page`
|
|
665
712
|
);
|
|
666
713
|
};
|
|
714
|
+
var DispositionCodec = t.keyof({ fixed: null, refuted: null, dismissed: null });
|
|
715
|
+
var DISPOSITIONS = Object.keys(DispositionCodec.keys);
|
|
716
|
+
var RulingCodec = t.keyof({ upheld: null, overruled: null });
|
|
717
|
+
var RULINGS = Object.keys(RulingCodec.keys);
|
|
718
|
+
var ResponseDispositionCodec = t.keyof({
|
|
719
|
+
fixed: null,
|
|
720
|
+
refuted: null,
|
|
721
|
+
dismissed: null,
|
|
722
|
+
upheld: null,
|
|
723
|
+
overruled: null,
|
|
724
|
+
unstated: null
|
|
725
|
+
});
|
|
726
|
+
var isAnswerWord = (word) => DispositionCodec.is(word) || RulingCodec.is(word);
|
|
727
|
+
var RESPONSE_FORM = `Review-Response: <id> ${DISPOSITIONS.join("|")} \u2014 <reason>`;
|
|
728
|
+
var RESPONSE_TABLE_COLUMNS = ["id", "disposition", "reason"];
|
|
729
|
+
`| ${RESPONSE_TABLE_COLUMNS.join(" | ")} |`;
|
|
730
|
+
`| ${RESPONSE_TABLE_COLUMNS.map(() => "---").join(" | ")} |`;
|
|
731
|
+
var listed = (words, conjunction) => {
|
|
732
|
+
const quoted = words.map((word) => `\`${word}\``);
|
|
733
|
+
return quoted.length < 2 ? quoted.join("") : `${quoted.slice(0, -1).join(", ")} ${conjunction} ${quoted.at(-1) ?? ""}`;
|
|
734
|
+
};
|
|
735
|
+
var RULING_FORM = `Review-Response: <id> ${RULINGS.join("|")} \u2014 <reason>`;
|
|
736
|
+
var RULING_TEACHING = `A maintainer rules on a contested finding by answering its id, in a table row or a \`${RULING_FORM}\` commit line, with \`upheld\` (the finding stands at full weight until it is fixed) or \`overruled\` (it is closed for good).`;
|
|
737
|
+
var RESPONSE_TEACHING = `To answer findings, post a PR conversation comment holding a rendered markdown table, not one inside a code block, whose columns are ${listed(RESPONSE_TABLE_COLUMNS, "and")}, one row per finding id, its disposition exactly one of ${listed(DISPOSITIONS, "or")}; or put \`${RESPONSE_FORM}\` lines in a commit message. Inline-thread replies are not read. A systemic problem's id, when shown, answers its whole class.`;
|
|
738
|
+
|
|
739
|
+
// src/responses.ts
|
|
740
|
+
var RESPONSE_REASON_CLIP_CHARS = 300;
|
|
741
|
+
var RESPONSES_PER_CHANNEL = 25;
|
|
742
|
+
var ChannelCodec = t.keyof({ comment: null, commit: null });
|
|
743
|
+
var MAINTAINER_ASSOCIATIONS = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
744
|
+
var CLOSING_DISPOSITIONS = /* @__PURE__ */ new Set(["refuted", "dismissed"]);
|
|
745
|
+
var ResponseShape = t.type({
|
|
746
|
+
id: t.string,
|
|
747
|
+
disposition: ResponseDispositionCodec,
|
|
748
|
+
reason: t.string,
|
|
749
|
+
channel: ChannelCodec,
|
|
750
|
+
source_url: t.string,
|
|
751
|
+
author: t.union([t.string, t.null]),
|
|
752
|
+
author_association: t.union([t.string, t.null]),
|
|
753
|
+
created_at: t.union([t.string, t.null])
|
|
754
|
+
});
|
|
755
|
+
var ResponseCodec = strictExact("Response", ResponseShape, [ResponseShape]);
|
|
756
|
+
var UnmatchedResponseShape = t.type({
|
|
757
|
+
id: t.string,
|
|
758
|
+
channel: ChannelCodec,
|
|
759
|
+
source_url: t.string
|
|
760
|
+
});
|
|
761
|
+
var UnmatchedResponseCodec = strictExact("UnmatchedResponse", UnmatchedResponseShape, [
|
|
762
|
+
UnmatchedResponseShape
|
|
763
|
+
]);
|
|
764
|
+
var ResponsesFileAnswers = t.type({
|
|
765
|
+
responses: t.array(ResponseCodec),
|
|
766
|
+
unmatched: t.array(UnmatchedResponseCodec)
|
|
767
|
+
});
|
|
768
|
+
var HeldClaimShape = t.type({
|
|
769
|
+
id: t.string,
|
|
770
|
+
title: t.string,
|
|
771
|
+
severity: SeverityCodec,
|
|
772
|
+
since: t.string
|
|
773
|
+
});
|
|
774
|
+
var HeldClaimCodec = strictExact("HeldClaim", HeldClaimShape, [HeldClaimShape]);
|
|
775
|
+
var ResponsesFileDialogue = t.partial({
|
|
776
|
+
contested: t.array(HeldClaimCodec),
|
|
777
|
+
upheld: t.array(HeldClaimCodec),
|
|
778
|
+
overruled: t.array(HeldClaimCodec)
|
|
779
|
+
});
|
|
780
|
+
var ResponsesFileShape = t.intersection([ResponsesFileAnswers, ResponsesFileDialogue]);
|
|
781
|
+
strictExact("ResponsesFile", ResponsesFileShape, [
|
|
782
|
+
ResponsesFileAnswers,
|
|
783
|
+
ResponsesFileDialogue
|
|
784
|
+
]);
|
|
785
|
+
var RESPONSE_LINE_RE = /^ {0,3}review-response:\s*(\S+)\s+([A-Za-z]+)(?![A-Za-z]|\s*\|)[\s—–:-]*(.*)$/i;
|
|
786
|
+
var FENCE_OPEN_RE = /^ {0,3}(`{3,}(?=[^`]*$)|~{3,})/;
|
|
787
|
+
var LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/;
|
|
788
|
+
var unfencedLines = (text) => text.split(LINE_BREAK_RE).reduce(
|
|
789
|
+
(state, line) => {
|
|
790
|
+
const marker = FENCE_OPEN_RE.exec(line)?.[1];
|
|
791
|
+
if (state.fence === null) {
|
|
792
|
+
if (marker !== void 0) return { fence: marker, lines: state.lines };
|
|
793
|
+
state.lines.push(line);
|
|
794
|
+
return state;
|
|
795
|
+
}
|
|
796
|
+
const closes = marker !== void 0 && marker[0] === state.fence[0] && marker.length >= state.fence.length && line.trim() === marker;
|
|
797
|
+
return closes ? { fence: null, lines: state.lines } : state;
|
|
798
|
+
},
|
|
799
|
+
{ fence: null, lines: [] }
|
|
800
|
+
).lines;
|
|
801
|
+
var WHOLE_LINE_CODE_SPAN_RE = /^ {0,3}`([^`]+)`\s*$/;
|
|
802
|
+
var answerLines = (text) => unfencedLines(text).map((line) => WHOLE_LINE_CODE_SPAN_RE.exec(line)?.[1] ?? line);
|
|
803
|
+
var unwrapId = (token) => token.replace(/^[`"']+|[`"'.,:;]+$/g, "");
|
|
804
|
+
var lineAnswers = (lines) => lines.flatMap((line) => {
|
|
805
|
+
const match = RESPONSE_LINE_RE.exec(line);
|
|
806
|
+
const id = unwrapId(match?.[1] ?? "");
|
|
807
|
+
const disposition = match?.[2]?.toLowerCase();
|
|
808
|
+
return id !== "" && isAnswerWord(disposition) ? [
|
|
809
|
+
{
|
|
810
|
+
id,
|
|
811
|
+
disposition,
|
|
812
|
+
reason: clipText((match?.[3] ?? "").trim(), RESPONSE_REASON_CLIP_CHARS)
|
|
813
|
+
}
|
|
814
|
+
] : [];
|
|
815
|
+
});
|
|
816
|
+
var ID_IN_CELL_RE = /`([^`]+)`/g;
|
|
817
|
+
var ID_HEADERS = /* @__PURE__ */ new Set(["id", "ids", "finding", "findings"]);
|
|
818
|
+
var DISPOSITION_HEADERS = /* @__PURE__ */ new Set(["disposition", "verdict", "resolution"]);
|
|
819
|
+
var CELL_RE = /((?:\\[\s\S]?|[^\\|])*)(\||$)/g;
|
|
820
|
+
var splitCells = (row) => {
|
|
821
|
+
const cells = [...row.matchAll(CELL_RE)];
|
|
822
|
+
return cells.slice(0, cells.findIndex((match) => match[2] !== "|") + 1).map((match) => match[1] ?? "");
|
|
823
|
+
};
|
|
824
|
+
var BLOCK_START_RE = /^ {0,3}([-+*]\s|\d+[.)]\s|#{1,6}(\s|$)|>)/;
|
|
825
|
+
var tableRow = (line) => {
|
|
826
|
+
if (!/^ {0,3}\S/.test(line) || BLOCK_START_RE.test(line)) return null;
|
|
827
|
+
const split = splitCells(line.trim());
|
|
828
|
+
if (split.length < 2) return null;
|
|
829
|
+
const inner = split.slice(line.trim().startsWith("|") ? 1 : 0);
|
|
830
|
+
return (inner.length > 1 && inner[inner.length - 1]?.trim() === "" ? inner.slice(0, -1) : inner).map((cell) => cell.trim().replace(/\\\|/g, "|"));
|
|
831
|
+
};
|
|
832
|
+
var isDelimiter = (cells) => cells !== null && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
833
|
+
var plainCell = (cell) => cell.replace(/[*_`]/g, "").trim().toLowerCase();
|
|
834
|
+
var columnName = (cell) => plainCell(cell).split(/[\s/]+/)[0] ?? "";
|
|
835
|
+
var idsInCell = (cell) => {
|
|
836
|
+
const ids = cell.replace(/\([^()]*\)/g, "");
|
|
837
|
+
const quoted = [...ids.matchAll(ID_IN_CELL_RE)].map((match) => unwrapId((match[1] ?? "").trim())).filter((id) => id !== "");
|
|
838
|
+
const bare = ids.split(",").map((piece) => unwrapId(piece.trim())).filter((id) => id !== "");
|
|
839
|
+
return quoted.length > 0 ? quoted : bare.every((id) => /^\S+$/.test(id)) ? bare : [];
|
|
840
|
+
};
|
|
841
|
+
var tableDisposition = (cell) => {
|
|
842
|
+
const firstWord = plainCell(cell).replace(/^[^a-z]+/, "").split(/[^a-z]/)[0] ?? "";
|
|
843
|
+
return isAnswerWord(firstWord) ? firstWord : "unstated";
|
|
844
|
+
};
|
|
845
|
+
var rowAnswers = (cells, columns) => {
|
|
846
|
+
const dispositionCell = cells[columns.disposition] ?? "";
|
|
847
|
+
const disposition = tableDisposition(dispositionCell);
|
|
848
|
+
const verdictOnly = isAnswerWord(plainCell(dispositionCell));
|
|
849
|
+
const reason = clipText(
|
|
850
|
+
cells.filter(
|
|
851
|
+
(cell, column) => cell !== "" && column !== columns.id && !(verdictOnly && column === columns.disposition)
|
|
852
|
+
).join(" \u2014 "),
|
|
853
|
+
RESPONSE_REASON_CLIP_CHARS
|
|
854
|
+
);
|
|
855
|
+
return idsInCell(cells[columns.id] ?? "").map((id) => ({ id, disposition, reason }));
|
|
856
|
+
};
|
|
857
|
+
var namesAColumn = (name) => ID_HEADERS.has(name) || DISPOSITION_HEADERS.has(name);
|
|
858
|
+
var tableAnswers = (lines) => {
|
|
859
|
+
const rows = lines.map(tableRow);
|
|
860
|
+
return rows.reduce(
|
|
861
|
+
(state, cells, index) => {
|
|
862
|
+
if (state.skipDelimiter) return { ...state, skipDelimiter: false };
|
|
863
|
+
if (cells === null) return { ...state, inTable: false, columns: null };
|
|
864
|
+
const opensTable = !isDelimiter(cells) && isDelimiter(rows[index + 1] ?? null) && (!state.inTable || cells.map(columnName).some(namesAColumn));
|
|
865
|
+
if (opensTable) {
|
|
866
|
+
const header = cells.map(columnName);
|
|
867
|
+
const id = header.findIndex((name) => ID_HEADERS.has(name));
|
|
868
|
+
const disposition = header.findIndex((name) => DISPOSITION_HEADERS.has(name));
|
|
869
|
+
return {
|
|
870
|
+
...state,
|
|
871
|
+
inTable: true,
|
|
872
|
+
skipDelimiter: true,
|
|
873
|
+
columns: id < 0 || disposition < 0 ? null : { id, disposition }
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (state.inTable && state.columns !== null && !isDelimiter(cells)) {
|
|
877
|
+
state.answers.push(...rowAnswers(cells, state.columns));
|
|
878
|
+
}
|
|
879
|
+
return isDelimiter(rows[index + 1] ?? null) ? { ...state, skipDelimiter: true } : state;
|
|
880
|
+
},
|
|
881
|
+
{ columns: null, inTable: false, skipDelimiter: false, answers: [] }
|
|
882
|
+
).answers;
|
|
883
|
+
};
|
|
884
|
+
var parseResponses = (text) => {
|
|
885
|
+
const lines = answerLines(text);
|
|
886
|
+
const seen = /* @__PURE__ */ new Set();
|
|
887
|
+
return [...lineAnswers(lines), ...tableAnswers(lines)].filter((answer) => {
|
|
888
|
+
const key2 = JSON.stringify([answer.id, answer.disposition, answer.reason]);
|
|
889
|
+
if (seen.has(key2)) return false;
|
|
890
|
+
seen.add(key2);
|
|
891
|
+
return true;
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
var isHuman = (login, type9, botLogin) => login !== botLogin && type9 === "User";
|
|
895
|
+
var newestFirst = (a, b) => {
|
|
896
|
+
const [left2, right2] = [a.created_at ?? "", b.created_at ?? ""];
|
|
897
|
+
return left2 < right2 ? 1 : left2 > right2 ? -1 : b.id - a.id;
|
|
898
|
+
};
|
|
899
|
+
var answersFrom = (input) => {
|
|
900
|
+
const comments = [...input.comments].filter((comment) => isHuman(comment.user.login, comment.user.type ?? null, input.botLogin)).sort(newestFirst).flatMap(
|
|
901
|
+
(comment) => parseResponses(comment.body ?? "").map((line) => ({
|
|
902
|
+
...line,
|
|
903
|
+
channel: "comment",
|
|
904
|
+
source_url: `https://github.com/${input.repo}/pull/${String(input.prNumber)}#issuecomment-${String(comment.id)}`,
|
|
905
|
+
author: comment.user.login,
|
|
906
|
+
author_association: comment.author_association ?? null,
|
|
907
|
+
created_at: comment.created_at ?? null
|
|
908
|
+
}))
|
|
909
|
+
);
|
|
910
|
+
const commits = [...input.commits].reverse().flatMap(
|
|
911
|
+
(commit) => parseResponses(commit.message).map((line) => ({
|
|
912
|
+
...line,
|
|
913
|
+
channel: "commit",
|
|
914
|
+
source_url: `https://github.com/${input.repo}/commit/${commit.sha}`,
|
|
915
|
+
author: commit.author,
|
|
916
|
+
author_association: null,
|
|
917
|
+
created_at: commit.date ?? null
|
|
918
|
+
}))
|
|
919
|
+
);
|
|
920
|
+
return { comments, commits };
|
|
921
|
+
};
|
|
922
|
+
var harvestResponses = (input) => {
|
|
923
|
+
const { comments: fromComments, commits: fromCommits } = answersFrom(input);
|
|
924
|
+
const matched = (response) => input.priorIds.has(response.id);
|
|
925
|
+
const echoed = (response) => !matched(response) && ID_SHAPE_RE.test(response.id);
|
|
926
|
+
const groups = [fromComments, fromCommits].flatMap((channel) => [
|
|
927
|
+
channel.filter(matched),
|
|
928
|
+
channel.filter(echoed)
|
|
929
|
+
]);
|
|
930
|
+
const [comments, commentEchoes, commits, commitEchoes] = groups.map(
|
|
931
|
+
(group) => group.slice(0, RESPONSES_PER_CHANNEL)
|
|
932
|
+
);
|
|
933
|
+
return {
|
|
934
|
+
file: {
|
|
935
|
+
responses: [...comments ?? [], ...commits ?? []],
|
|
936
|
+
unmatched: [...commentEchoes ?? [], ...commitEchoes ?? []].map(
|
|
937
|
+
({ id, channel, source_url }) => ({ id, channel, source_url })
|
|
938
|
+
)
|
|
939
|
+
},
|
|
940
|
+
dropped: groups.reduce(
|
|
941
|
+
(total, group) => total + Math.max(0, group.length - RESPONSES_PER_CHANNEL),
|
|
942
|
+
0
|
|
943
|
+
)
|
|
944
|
+
};
|
|
945
|
+
};
|
|
946
|
+
var isTrustedResponse = (response, headInBaseRepo) => {
|
|
947
|
+
switch (response.channel) {
|
|
948
|
+
case "comment":
|
|
949
|
+
return MAINTAINER_ASSOCIATIONS.includes(response.author_association ?? "");
|
|
950
|
+
case "commit":
|
|
951
|
+
return headInBaseRepo;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
var answeredInstant = (response) => response.created_at === null ? Number.NaN : Date.parse(response.created_at);
|
|
955
|
+
var closingResponses = (responses, isTrusted) => newestTrustedAnswersIn(responses, isTrusted, CLOSING_DISPOSITIONS);
|
|
956
|
+
var withRulingsOn = (responses, ruledIds, isTrusted) => responses.map(
|
|
957
|
+
(response) => RulingCodec.is(response.disposition) && !(ruledIds.has(response.id) && isTrusted(response)) ? { ...response, disposition: "unstated" } : response
|
|
958
|
+
);
|
|
959
|
+
var newestTrustedAnswersIn = (responses, isTrusted, dispositions) => {
|
|
960
|
+
const byId = responses.filter((response) => response.disposition !== "unstated" && isTrusted(response)).reduce((groups, response) => {
|
|
961
|
+
const timed = { response, at: answeredInstant(response) };
|
|
962
|
+
groups.set(response.id, [...groups.get(response.id) ?? [], timed]);
|
|
963
|
+
return groups;
|
|
964
|
+
}, /* @__PURE__ */ new Map());
|
|
965
|
+
return [...byId.values()].flatMap((answers) => {
|
|
966
|
+
if (answers.some(({ at }) => Number.isNaN(at))) return [];
|
|
967
|
+
const latest = Math.max(...answers.map(({ at }) => at));
|
|
968
|
+
const newest = answers.filter(({ at }) => at === latest).map(({ response }) => response);
|
|
969
|
+
return newest.every((response) => dispositions.has(response.disposition)) ? [...newest].sort((a, b) => a.source_url < b.source_url ? -1 : 1).slice(0, 1) : [];
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// src/dialogue.ts
|
|
974
|
+
var DialogueStateCodec = t.keyof({
|
|
975
|
+
rebutted: null,
|
|
976
|
+
contested: null,
|
|
977
|
+
upheld: null,
|
|
978
|
+
overruled: null
|
|
979
|
+
});
|
|
980
|
+
var DialogueEntryCodec = t.type({
|
|
981
|
+
id: t.string,
|
|
982
|
+
state: DialogueStateCodec,
|
|
983
|
+
answer: t.string,
|
|
984
|
+
at: t.string,
|
|
985
|
+
title: t.string,
|
|
986
|
+
severity: SeverityCodec
|
|
987
|
+
});
|
|
988
|
+
var DIALOGUE_STATES = Object.keys(DialogueStateCodec.keys);
|
|
989
|
+
var DIALOGUE_ENTRY_CAP = 32;
|
|
990
|
+
var decodeDialogue = (raw) => {
|
|
991
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
992
|
+
const decoded = items.map((item) => ({ item, entry: DialogueEntryCodec.decode(item) }));
|
|
993
|
+
const entries = decoded.flatMap(({ entry }) => isRight(entry) ? [entry.right] : []);
|
|
994
|
+
const undecoded = decoded.flatMap(({ item, entry }) => isRight(entry) ? [] : [item]);
|
|
995
|
+
return { entries, undecoded, skipped: Array.isArray(raw) ? undecoded.length : 1 };
|
|
996
|
+
};
|
|
997
|
+
var dialogueAnswers = (responses, isTrusted) => {
|
|
998
|
+
const newest = (dispositions) => new Map(
|
|
999
|
+
newestTrustedAnswersIn(responses, isTrusted, new Set(dispositions)).map((response) => [
|
|
1000
|
+
response.id,
|
|
1001
|
+
response
|
|
1002
|
+
])
|
|
1003
|
+
);
|
|
1004
|
+
return {
|
|
1005
|
+
closures: newest(["refuted", "dismissed"]),
|
|
1006
|
+
overrulings: newest(["overruled"]),
|
|
1007
|
+
upholdings: newest(["upheld"]),
|
|
1008
|
+
fixed: new Set(newest(["fixed"]).keys())
|
|
1009
|
+
};
|
|
1010
|
+
};
|
|
1011
|
+
var linked = (entry, answer, state) => answer.created_at === null ? [entry] : [{ ...entry, state, answer: answer.source_url, at: answer.created_at }];
|
|
1012
|
+
var isNewer = (answer, entry) => answer.created_at !== null && Date.parse(answer.created_at) > Date.parse(entry.at);
|
|
1013
|
+
var isOlder = (answer, entry) => answer.created_at !== null && Date.parse(answer.created_at) < Date.parse(entry.at);
|
|
1014
|
+
var advanceEntry = (entry, answers, item) => {
|
|
1015
|
+
if (answers.fixed.has(entry.id)) return [];
|
|
1016
|
+
const refreshed = item !== void 0 && (entry.state === "contested" || entry.state === "upheld" || item.rebutted) ? { ...entry, title: item.title, severity: item.severity } : entry;
|
|
1017
|
+
const closure = answers.closures.get(entry.id);
|
|
1018
|
+
switch (entry.state) {
|
|
1019
|
+
case "rebutted":
|
|
1020
|
+
return closure === void 0 ? [refreshed] : linked(refreshed, closure, isNewer(closure, entry) ? "contested" : "rebutted");
|
|
1021
|
+
case "contested": {
|
|
1022
|
+
const overruling = answers.overrulings.get(entry.id);
|
|
1023
|
+
const upholding = answers.upholdings.get(entry.id);
|
|
1024
|
+
return upholding !== void 0 ? linked(refreshed, upholding, "upheld") : overruling !== void 0 ? linked(refreshed, overruling, "overruled") : closure === void 0 ? [refreshed] : linked(refreshed, closure, isOlder(closure, entry) ? "rebutted" : "contested");
|
|
1025
|
+
}
|
|
1026
|
+
case "upheld": {
|
|
1027
|
+
const overruling = answers.overrulings.get(entry.id);
|
|
1028
|
+
return overruling !== void 0 && isNewer(overruling, entry) ? linked(refreshed, overruling, "overruled") : [refreshed];
|
|
1029
|
+
}
|
|
1030
|
+
case "overruled": {
|
|
1031
|
+
const upholding = answers.upholdings.get(entry.id);
|
|
1032
|
+
return upholding !== void 0 && isNewer(upholding, entry) ? linked(refreshed, upholding, "upheld") : [refreshed];
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
var advanceDialogue = (prior, answers, current) => {
|
|
1037
|
+
const currentById = new Map(current.map((item) => [item.id, item]));
|
|
1038
|
+
const priorIds = new Set(prior.map((entry) => entry.id));
|
|
1039
|
+
const opened = [...currentById.values()].flatMap((item) => {
|
|
1040
|
+
const closure = answers.closures.get(item.id);
|
|
1041
|
+
const at = closure?.created_at ?? null;
|
|
1042
|
+
return priorIds.has(item.id) || !item.rebutted || closure === void 0 || at === null ? [] : [
|
|
1043
|
+
{
|
|
1044
|
+
id: item.id,
|
|
1045
|
+
state: "rebutted",
|
|
1046
|
+
answer: closure.source_url,
|
|
1047
|
+
at,
|
|
1048
|
+
title: item.title,
|
|
1049
|
+
severity: item.severity
|
|
1050
|
+
}
|
|
1051
|
+
];
|
|
1052
|
+
});
|
|
1053
|
+
const entries = [
|
|
1054
|
+
...prior.flatMap((entry) => advanceEntry(entry, answers, currentById.get(entry.id))),
|
|
1055
|
+
...opened
|
|
1056
|
+
];
|
|
1057
|
+
const held = entries.filter((entry) => entry.state !== "rebutted");
|
|
1058
|
+
return [
|
|
1059
|
+
...held,
|
|
1060
|
+
...entries.filter((entry) => entry.state === "rebutted").slice(0, Math.max(0, DIALOGUE_ENTRY_CAP - held.length))
|
|
1061
|
+
];
|
|
1062
|
+
};
|
|
1063
|
+
var dialogueMetricsTable = (metrics) => [
|
|
1064
|
+
"### \u{1F4AC} Review dialogue",
|
|
1065
|
+
"",
|
|
1066
|
+
"| | |",
|
|
1067
|
+
"|---|--:|",
|
|
1068
|
+
`| answers read (comments \xB7 commits) | ${String(metrics.commentAnswers)} \xB7 ${String(metrics.commitAnswers)} |`,
|
|
1069
|
+
`| trusted closing answers | ${String(metrics.closures)} |`,
|
|
1070
|
+
`| re-raises dropped (answered \xB7 overruled) | ${String(metrics.droppedAnswered)} \xB7 ${String(metrics.droppedOverruled)} |`,
|
|
1071
|
+
`| re-raises kept and annotated | ${String(metrics.annotated)} |`,
|
|
1072
|
+
`| ${DIALOGUE_STATES.join(" \xB7 ")} | ${DIALOGUE_STATES.map(
|
|
1073
|
+
(state) => String(metrics.entries.filter((entry) => entry.state === state).length)
|
|
1074
|
+
).join(" \xB7 ")} |`,
|
|
1075
|
+
`| systemic problems no trusted answer names | ${String(metrics.systemicUnanswered)} |`
|
|
1076
|
+
].join("\n");
|
|
667
1077
|
|
|
668
1078
|
// src/patch.ts
|
|
669
1079
|
var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
@@ -791,6 +1201,7 @@ var decodeBase64Json = (b64) => {
|
|
|
791
1201
|
return void 0;
|
|
792
1202
|
}
|
|
793
1203
|
};
|
|
1204
|
+
var noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
794
1205
|
var findingsPointer = (jsonUrl) => encodeMarker(jsonUrl);
|
|
795
1206
|
var warnedValveFindings = /* @__PURE__ */ new Set();
|
|
796
1207
|
var INLINE_PROSE_CLIP_THRESHOLD_CHARS = 3e4;
|
|
@@ -1022,8 +1433,15 @@ var convergenceScore = (doc, threshold) => round2(
|
|
|
1022
1433
|
0
|
|
1023
1434
|
)
|
|
1024
1435
|
);
|
|
1025
|
-
var
|
|
1026
|
-
|
|
1436
|
+
var CONTESTED_MAJOR = 0.51;
|
|
1437
|
+
var contestedWeight = (severity, threshold) => severity === "major" ? CONTESTED_MAJOR : convergenceFloor(severity, threshold);
|
|
1438
|
+
var dialogueScore = (doc, contested, threshold) => {
|
|
1439
|
+
return round2(
|
|
1440
|
+
convergenceScore(withoutIds(doc, new Set(contested.map((entry) => entry.id))), threshold) + contested.reduce((sum, entry) => sum + contestedWeight(entry.severity, threshold), 0)
|
|
1441
|
+
);
|
|
1442
|
+
};
|
|
1443
|
+
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha, contested = []) => {
|
|
1444
|
+
const score = dialogueScore(doc, contested, threshold);
|
|
1027
1445
|
const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
|
|
1028
1446
|
const current = {
|
|
1029
1447
|
round,
|
|
@@ -1154,6 +1572,13 @@ var findingsMarkerPair = (jsonUrl, convergence) => {
|
|
|
1154
1572
|
return marker === "" ? conv : `${marker}
|
|
1155
1573
|
${conv}`;
|
|
1156
1574
|
};
|
|
1575
|
+
var DIALOGUE_RE = /<!-- code-review:dialogue;base64 ([A-Za-z0-9+/=]+) -->/;
|
|
1576
|
+
var dialogueMarker = (entries, undecoded = []) => entries.length + undecoded.length === 0 ? "" : `<!-- code-review:dialogue;base64 ${Buffer.from(JSON.stringify([...entries, ...undecoded]), "utf-8").toString("base64")} -->`;
|
|
1577
|
+
var carriedDialogueMarker = (body) => DIALOGUE_RE.exec(body)?.[0] ?? "";
|
|
1578
|
+
var parseDialogueMarker = (body) => {
|
|
1579
|
+
const b64 = DIALOGUE_RE.exec(body)?.[1];
|
|
1580
|
+
return b64 === void 0 ? { entries: [], undecoded: [], skipped: 0 } : decodeDialogue(decodeBase64Json(b64));
|
|
1581
|
+
};
|
|
1157
1582
|
var parseConvergenceMarker = (body) => {
|
|
1158
1583
|
const b64 = CONVERGENCE_RE.exec(body)?.[1];
|
|
1159
1584
|
return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
|
|
@@ -1210,11 +1635,20 @@ var carryForwardMarkers = (body) => {
|
|
|
1210
1635
|
const findings = carriedFindingsMarker(body);
|
|
1211
1636
|
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
1212
1637
|
const convergence = CONVERGENCE_RE.exec(body)?.[0];
|
|
1638
|
+
const dialogue = DIALOGUE_RE.exec(body)?.[0];
|
|
1213
1639
|
const rounds = ROUNDS_RE.exec(body)?.[0];
|
|
1214
1640
|
const signal = SIGNAL_RE.exec(body)?.[0];
|
|
1215
1641
|
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
1216
1642
|
${findings}` : void 0;
|
|
1217
|
-
return [
|
|
1643
|
+
return [
|
|
1644
|
+
findingsBlock,
|
|
1645
|
+
reviewedSha,
|
|
1646
|
+
carriedProvenanceMarkers(body),
|
|
1647
|
+
convergence,
|
|
1648
|
+
dialogue,
|
|
1649
|
+
rounds,
|
|
1650
|
+
signal
|
|
1651
|
+
].filter((m) => m !== void 0 && m !== "").join("\n\n");
|
|
1218
1652
|
};
|
|
1219
1653
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
1220
1654
|
var projectPatch = (patch, surface) => {
|
|
@@ -1392,29 +1826,38 @@ var resolvePriorFindings = async (body, read) => {
|
|
|
1392
1826
|
};
|
|
1393
1827
|
|
|
1394
1828
|
// src/answered.ts
|
|
1395
|
-
var
|
|
1396
|
-
const
|
|
1397
|
-
return
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
answerAuthor: response.author
|
|
1408
|
-
}
|
|
1409
|
-
];
|
|
1829
|
+
var closedPriorItems = (closures, items, idOf) => closures.flatMap((response) => {
|
|
1830
|
+
const item = items.find((candidate) => idOf(candidate) === response.id);
|
|
1831
|
+
return item === void 0 ? [] : [[item, response]];
|
|
1832
|
+
});
|
|
1833
|
+
var closedClaim = (item, response) => ({
|
|
1834
|
+
code: response.id,
|
|
1835
|
+
title: item.title,
|
|
1836
|
+
description: item.description,
|
|
1837
|
+
reasoning: item.reasoning,
|
|
1838
|
+
severity: item.severity,
|
|
1839
|
+
answerUrl: response.source_url,
|
|
1840
|
+
answerAuthor: response.author
|
|
1410
1841
|
});
|
|
1842
|
+
var answeredRegistryFrom = (closures, prior) => closedPriorItems(closures, prior?.findings ?? [], resolveFindingId).map(
|
|
1843
|
+
([finding, response]) => ({
|
|
1844
|
+
...closedClaim(finding, response),
|
|
1845
|
+
path: finding.path,
|
|
1846
|
+
patch: finding.patch ?? null
|
|
1847
|
+
})
|
|
1848
|
+
);
|
|
1849
|
+
var answeredSystemicRegistryFrom = (closures, prior) => closedPriorItems(closures, prior?.systemic_problems ?? [], resolveSystemicId).map(
|
|
1850
|
+
([systemic, response]) => closedClaim(systemic, response)
|
|
1851
|
+
);
|
|
1411
1852
|
var isSynthesizedTitleMatch = (f, e) => isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1412
1853
|
var matches = (resolvedId, f, e) => e.code === resolvedId || isSynthesizedTitleMatch(f, e);
|
|
1413
|
-
var couldMatch = (code,
|
|
1414
|
-
var
|
|
1854
|
+
var couldMatch = (code, doc) => isSynthesizedFindingId(code) || doc.findings.some((f) => resolveFindingId(f) === code) || (doc.systemic_problems ?? []).some((s) => resolveSystemicId(s) === code);
|
|
1855
|
+
var CLAIM_FIELDS = ["title", "description", "reasoning", "severity"];
|
|
1856
|
+
var VERBATIM_FIELDS = [...CLAIM_FIELDS, "path", "patch"];
|
|
1415
1857
|
var verbatimFieldEqual = (f, e, field) => field === "patch" ? (f.patch ?? null) === e.patch : f[field] === e[field];
|
|
1416
|
-
var isVerbatimReRaise = (
|
|
1417
|
-
var
|
|
1858
|
+
var isVerbatimReRaise = (item, fields, fieldEqual) => !hasRebuttal(item) && fields.every(fieldEqual);
|
|
1859
|
+
var isVerbatimNonCritical = (item, fields, fieldEqual) => isVerbatimReRaise(item, fields, fieldEqual) && item.severity !== "critical";
|
|
1860
|
+
var isAnsweredDrop = (resolvedId, f, e) => matches(resolvedId, f, e) && isVerbatimNonCritical(f, VERBATIM_FIELDS, (field) => verbatimFieldEqual(f, e, field));
|
|
1418
1861
|
var verbatimMatchCount = (f, e) => VERBATIM_FIELDS.reduce((count, field) => count + (verbatimFieldEqual(f, e, field) ? 1 : 0), 0);
|
|
1419
1862
|
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1420
1863
|
var bestTitleMatch = (f, registry) => {
|
|
@@ -1466,38 +1909,78 @@ var applyAnswered = (findings, registry) => {
|
|
|
1466
1909
|
droppedCount
|
|
1467
1910
|
};
|
|
1468
1911
|
};
|
|
1469
|
-
var
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1912
|
+
var answeredSystemicNoteKey = (s) => `systemic:${resolveSystemicId(s) ?? ""}`;
|
|
1913
|
+
var systemicVerdict = (systemic, registry) => {
|
|
1914
|
+
const resolvedId = resolveSystemicId(systemic);
|
|
1915
|
+
const entry = registry.find((e) => e.code === resolvedId);
|
|
1916
|
+
return entry === void 0 ? { kind: "unanswered", systemic } : {
|
|
1917
|
+
kind: isVerbatimNonCritical(
|
|
1918
|
+
systemic,
|
|
1919
|
+
CLAIM_FIELDS,
|
|
1920
|
+
(field) => systemic[field] === entry[field]
|
|
1921
|
+
) ? "dropped" : "annotated",
|
|
1922
|
+
systemic,
|
|
1923
|
+
entry
|
|
1924
|
+
};
|
|
1479
1925
|
};
|
|
1480
|
-
var
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const quoted = words.map((word) => `\`${word}\``);
|
|
1494
|
-
return quoted.length < 2 ? quoted.join("") : `${quoted.slice(0, -1).join(", ")} ${conjunction} ${quoted.at(-1) ?? ""}`;
|
|
1926
|
+
var applyAnsweredSystemic = (systemic, registry) => {
|
|
1927
|
+
const verdicts = systemic.map((s) => systemicVerdict(s, registry));
|
|
1928
|
+
const dropped = verdicts.flatMap((v) => v.kind === "dropped" ? [v.entry] : []);
|
|
1929
|
+
return {
|
|
1930
|
+
systemic: verdicts.flatMap((v) => v.kind === "dropped" ? [] : [v.systemic]),
|
|
1931
|
+
reRaisedNotes: Object.fromEntries(
|
|
1932
|
+
verdicts.flatMap(
|
|
1933
|
+
(v) => v.kind === "annotated" ? [[answeredSystemicNoteKey(v.systemic), answeredNote(v.entry)]] : []
|
|
1934
|
+
)
|
|
1935
|
+
),
|
|
1936
|
+
verbatimReRaised: [...new Map(dropped.map((e) => [e.code, e])).values()],
|
|
1937
|
+
droppedCount: dropped.length
|
|
1938
|
+
};
|
|
1495
1939
|
};
|
|
1496
|
-
var
|
|
1940
|
+
var applyOverruled = (items, rulings, idOf, noteKey) => {
|
|
1941
|
+
const ruled = items.map((item) => ({ item, ruling: rulings.get(idOf(item) ?? "") }));
|
|
1942
|
+
const drops = (entry) => entry.ruling !== void 0 && entry.item.severity !== "critical";
|
|
1943
|
+
const dropped = ruled.filter(drops).flatMap(({ ruling }) => ruling === void 0 ? [] : [ruling]);
|
|
1944
|
+
return {
|
|
1945
|
+
kept: ruled.filter((entry) => !drops(entry)).map(({ item }) => item),
|
|
1946
|
+
reRaisedNotes: Object.fromEntries(
|
|
1947
|
+
ruled.flatMap(
|
|
1948
|
+
({ item, ruling }) => ruling !== void 0 && !drops({ item, ruling }) ? [[noteKey(item), answeredNote(ruling)]] : []
|
|
1949
|
+
)
|
|
1950
|
+
),
|
|
1951
|
+
verbatimReRaised: [...new Map(dropped.map((ruling) => [ruling.code, ruling])).values()],
|
|
1952
|
+
droppedCount: dropped.length
|
|
1953
|
+
};
|
|
1954
|
+
};
|
|
1955
|
+
var closingAnswerLines = (entries, linkText) => entries.map((e) => {
|
|
1956
|
+
const label = e.code !== "" ? `\`${escapeCodeBackticks(e.code)}\`` : `\u201C${escapeCodeBackticks(e.title)}\u201D`;
|
|
1957
|
+
return `> - ${label} \u2014 [${linkText}](${linkSafeUrl(e.answerUrl)})${byAuthor(e)}`;
|
|
1958
|
+
});
|
|
1959
|
+
var answeredReRaiseNote = (entries, count) => entries.length === 0 ? "" : [
|
|
1960
|
+
`> \u21A9\uFE0F **${String(count)} re-raise(s) without new evidence \u2014 treated as answered** (a maintainer's answer closed each):`,
|
|
1961
|
+
...closingAnswerLines(entries, "prior answer")
|
|
1962
|
+
].join("\n");
|
|
1963
|
+
var overruledReRaiseNote = (entries, count) => entries.length === 0 ? "" : [
|
|
1964
|
+
`> \u2696\uFE0F **${String(count)} re-raise(s) of an id a maintainer overruled \u2014 closed for good**:`,
|
|
1965
|
+
...closingAnswerLines(entries, "the ruling")
|
|
1966
|
+
].join("\n");
|
|
1497
1967
|
|
|
1498
1968
|
// src/render.ts
|
|
1499
1969
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
1500
1970
|
var shedNote = (count, summaryAvailable, runUrl, findingsDocLinked, thisRunArtifact) => count === 0 ? "" : `<sub>**${String(count)} finding${count === 1 ? "" : "s"} left out of this comment**${summaryAvailable && runUrl !== void 0 ? ` \u2014 the [run summary](<${runUrl}>) carries the whole review` : findingsDocLinked && thisRunArtifact ? " \u2014 the machine findings document carries them" : ""}.</sub>`;
|
|
1971
|
+
var UNFINISHED_SHOWN = 8;
|
|
1972
|
+
var UNFINISHED_CLIP_CHARS = 240;
|
|
1973
|
+
var unfinishedLine = (check) => {
|
|
1974
|
+
const escaped = escapeHtml(check.replace(/\s+/g, " ").trim());
|
|
1975
|
+
return escaped.length > UNFINISHED_CLIP_CHARS ? `${escaped.slice(0, UNFINISHED_CLIP_CHARS).replace(/&[#a-z0-9]*$/i, "").replace(/[\uD800-\uDBFF]$/, "")}\u2026` : escaped;
|
|
1976
|
+
};
|
|
1977
|
+
var shownUnfinished = (doc, fullReviewRound) => {
|
|
1978
|
+
const checks = fullReviewRound ? doc.unfinished ?? [] : [];
|
|
1979
|
+
return [
|
|
1980
|
+
...checks.slice(0, UNFINISHED_SHOWN).map(unfinishedLine),
|
|
1981
|
+
...checks.length > UNFINISHED_SHOWN ? [`\u2026 and ${String(checks.length - UNFINISHED_SHOWN)} more`] : []
|
|
1982
|
+
];
|
|
1983
|
+
};
|
|
1501
1984
|
var terminalNotice = (i) => [
|
|
1502
1985
|
`## ${i.verdictBadge}`,
|
|
1503
1986
|
"",
|
|
@@ -1509,6 +1992,7 @@ var terminalNotice = (i) => [
|
|
|
1509
1992
|
] : [],
|
|
1510
1993
|
"",
|
|
1511
1994
|
`> **The review for \`${escapeCodeBackticks(i.reviewedSha.slice(0, 7))}\` exceeds GitHub's comment-size limit.**` + (i.summaryAvailable ? i.runUrl ? ` The whole review lives in the [run summary](<${i.runUrl}>).` : " The whole review lives in the workflow run's summary." : i.findingsDocLinked && i.thisRunArtifact ? " The whole review lives in the machine findings document." : "") + (i.findingsDocLinked && i.thisRunArtifact ? " The machine findings document is linked above." : ""),
|
|
1995
|
+
...i.unfinished.length > 0 ? ["", `> **Unfinished checks:** ${i.unfinished.join(" \xB7 ")}`] : [],
|
|
1512
1996
|
"",
|
|
1513
1997
|
"> [!WARNING]",
|
|
1514
1998
|
`> **LLM Disclosure** \u2014 this review was produced by ${i.modelNames || "unknown model"}.`,
|
|
@@ -1587,7 +2071,7 @@ var sanitizeFinding = (f, answeredNotes, discussionByFinding, permalinkBase, una
|
|
|
1587
2071
|
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1588
2072
|
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
1589
2073
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1590
|
-
answeredNote:
|
|
2074
|
+
answeredNote: noteFor(answeredNotes, key2),
|
|
1591
2075
|
discussion: discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(discussionByFinding, f.id) ? discussionByFinding[f.id] ?? [] : [],
|
|
1592
2076
|
...discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(discussionTruncated, f.id) ? { discussionTruncated: discussionTruncated[f.id] } : {}
|
|
1593
2077
|
};
|
|
@@ -1617,9 +2101,10 @@ var carriedLines = (f) => [
|
|
|
1617
2101
|
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1618
2102
|
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1619
2103
|
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
1620
|
-
var sanitizeSystemic = (s, discussion, discussionTruncated) => ({
|
|
2104
|
+
var sanitizeSystemic = (s, answeredNotes, discussion, discussionTruncated) => ({
|
|
1621
2105
|
...s,
|
|
1622
2106
|
title: escapePipes(s.title),
|
|
2107
|
+
answeredNote: noteFor(answeredNotes, answeredSystemicNoteKey(s)),
|
|
1623
2108
|
discussion,
|
|
1624
2109
|
...discussionTruncated !== void 0 ? { discussionTruncated } : {},
|
|
1625
2110
|
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncated, "") : "",
|
|
@@ -1671,6 +2156,9 @@ var render = (input) => {
|
|
|
1671
2156
|
const trajectory = convergence?.rounds ?? input.rounds ?? [];
|
|
1672
2157
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1673
2158
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
2159
|
+
const unfinished = input.findings.unfinished ?? [];
|
|
2160
|
+
const unfinishedSuffix = unfinished.length > 0 ? ` \xB7 ${String(unfinished.length)} unfinished check${unfinished.length === 1 ? "" : "s"}` : "";
|
|
2161
|
+
const contestedSuffix = (input.contested ?? []).length > 0 ? ` \xB7 ${String((input.contested ?? []).length)} contested` : "";
|
|
1674
2162
|
const advisoryAllowed = isFullReviewRound;
|
|
1675
2163
|
const discussionLinksFor = (id) => input.discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionByFinding, id) ? input.discussionByFinding[id] ?? [] : [];
|
|
1676
2164
|
const discussionTruncatedFor = (id) => input.discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionTruncated, id) ? input.discussionTruncated[id] : void 0;
|
|
@@ -1750,6 +2238,7 @@ var render = (input) => {
|
|
|
1750
2238
|
(input.findings.systemic_problems ?? []).map(
|
|
1751
2239
|
(s) => sanitizeSystemic(
|
|
1752
2240
|
s,
|
|
2241
|
+
input.answeredNotes,
|
|
1753
2242
|
s.id !== void 0 ? discussionLinksFor(s.id) : [],
|
|
1754
2243
|
s.id !== void 0 ? discussionTruncatedFor(s.id) : void 0
|
|
1755
2244
|
)
|
|
@@ -1758,6 +2247,23 @@ var render = (input) => {
|
|
|
1758
2247
|
(s) => s.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + s.discussionHtml.length : 0,
|
|
1759
2248
|
(s) => ({ ...s, discussion: [], discussionHtml: "" })
|
|
1760
2249
|
);
|
|
2250
|
+
const contestedBudget = budgetBySize(
|
|
2251
|
+
(input.contested ?? []).map((view) => {
|
|
2252
|
+
const discussion = discussionLinksFor(view.id);
|
|
2253
|
+
return {
|
|
2254
|
+
...view,
|
|
2255
|
+
...view.reRaise?.location !== void 0 ? { reRaise: { ...view.reRaise, location: escapeCodeBackticks(view.reRaise.location) } } : {},
|
|
2256
|
+
id: escapeCodeBackticks(view.id),
|
|
2257
|
+
title: escapePipes(view.title),
|
|
2258
|
+
answerUrl: linkSafeUrl(view.answerUrl),
|
|
2259
|
+
discussion,
|
|
2260
|
+
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncatedFor(view.id), "") : ""
|
|
2261
|
+
};
|
|
2262
|
+
}),
|
|
2263
|
+
DISCUSSION_TOTAL_CHARS - orphanedBudget.used - discussionBudget.used - systemicBudget.used,
|
|
2264
|
+
(view) => view.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + view.discussionHtml.length : 0,
|
|
2265
|
+
(view) => ({ ...view, discussion: [], discussionHtml: "" })
|
|
2266
|
+
);
|
|
1761
2267
|
const orphanedRender = {
|
|
1762
2268
|
lines: orphanedBudget.kept.map(([, entry]) => entry.html),
|
|
1763
2269
|
// The "(showing N of M)" total counts DISTINCT DISPLAYED entries — the pre-budget fold above
|
|
@@ -1784,6 +2290,7 @@ var render = (input) => {
|
|
|
1784
2290
|
summaryAvailable: input.summaryAvailable ?? false,
|
|
1785
2291
|
responseForm: RESPONSE_FORM,
|
|
1786
2292
|
responseTeaching: RESPONSE_TEACHING,
|
|
2293
|
+
rulingTeaching: RULING_TEACHING,
|
|
1787
2294
|
teachesResponses: isConvergenceRound(route, incomplete),
|
|
1788
2295
|
shedNote: shedNote(
|
|
1789
2296
|
input.shedCount ?? 0,
|
|
@@ -1795,12 +2302,13 @@ var render = (input) => {
|
|
|
1795
2302
|
input.jsonUrl !== void 0
|
|
1796
2303
|
),
|
|
1797
2304
|
terminalNotice: terminalNotice({
|
|
2305
|
+
unfinished: shownUnfinished(input.findings, isFullReviewRound),
|
|
1798
2306
|
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
1799
2307
|
verdictBadge: verdictBadge(input.findings.verdict),
|
|
1800
2308
|
route: route ?? void 0,
|
|
1801
2309
|
postedAt: input.postedAt ?? "",
|
|
1802
2310
|
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
1803
|
-
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : "",
|
|
2311
|
+
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) + contestedSuffix + unfinishedSuffix : "",
|
|
1804
2312
|
inlinePosted: input.inlineDisposition?.kind === "posted" ? input.inlineDisposition.count : 0,
|
|
1805
2313
|
runUrl: input.runUrl,
|
|
1806
2314
|
summaryAvailable: input.summaryAvailable ?? false,
|
|
@@ -1827,15 +2335,21 @@ var render = (input) => {
|
|
|
1827
2335
|
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
1828
2336
|
postedAt: input.postedAt ?? "",
|
|
1829
2337
|
severityCounts,
|
|
1830
|
-
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
2338
|
+
convergenceSummary: !isFullReviewRound ? "" : (convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold)) + contestedSuffix + unfinishedSuffix,
|
|
1831
2339
|
strays: discussionBudget.kept,
|
|
1832
2340
|
orphanedLines: orphanedRender.lines,
|
|
1833
2341
|
orphanedUnresolvable: input.orphanedUnresolvable === true,
|
|
1834
|
-
discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + orphanedBudget.droppedItems.length,
|
|
1835
|
-
discussionDroppedIds:
|
|
2342
|
+
discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + contestedBudget.droppedItems.length + orphanedBudget.droppedItems.length,
|
|
2343
|
+
discussionDroppedIds: [
|
|
2344
|
+
...discussionBudget.droppedItems.map((v) => clipText(escapeCodeBackticks(v.idKey), 64)),
|
|
2345
|
+
...contestedBudget.droppedItems.map((v) => clipText(v.id, 64))
|
|
2346
|
+
].slice(0, DROPPED_IDS_SHOWN),
|
|
1836
2347
|
discussionDroppedSystemicIds: systemicBudget.droppedItems.flatMap((s) => s.id !== void 0 ? [clipText(s.id, 64)] : []).slice(0, DROPPED_IDS_SHOWN),
|
|
1837
2348
|
discussionDroppedOrphanedIds: orphanedBudget.droppedItems.map(([token]) => clipText(token, 64)).slice(0, DROPPED_IDS_SHOWN),
|
|
1838
|
-
discussionDroppedExtra: Math.max(
|
|
2349
|
+
discussionDroppedExtra: Math.max(
|
|
2350
|
+
0,
|
|
2351
|
+
discussionBudget.droppedItems.length + contestedBudget.droppedItems.length - DROPPED_IDS_SHOWN
|
|
2352
|
+
) + Math.max(0, systemicBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, orphanedBudget.droppedItems.length - DROPPED_IDS_SHOWN),
|
|
1839
2353
|
discussionCap: PER_FINDING_LINKS,
|
|
1840
2354
|
orphanedTotal: orphanedRender.total,
|
|
1841
2355
|
suppressedNits: suppressedBudget.kept,
|
|
@@ -1857,6 +2371,8 @@ var render = (input) => {
|
|
|
1857
2371
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
1858
2372
|
answeredNotes: input.answeredNotes ?? {},
|
|
1859
2373
|
answeredReRaiseNote: input.answeredReRaiseNote ?? "",
|
|
2374
|
+
contested: contestedBudget.kept,
|
|
2375
|
+
unfinished: shownUnfinished(input.findings, isFullReviewRound),
|
|
1860
2376
|
reviewUrl: input.reviewUrl ?? null,
|
|
1861
2377
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
1862
2378
|
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
@@ -1957,7 +2473,6 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1957
2473
|
const { inDiff, strays } = context.partition ?? partitionFindings(findings, indexDiff(diff));
|
|
1958
2474
|
const eta = new Eta({ autoTrim: false });
|
|
1959
2475
|
const modelsText = formatModels(models);
|
|
1960
|
-
const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1961
2476
|
const comments = inDiff.map((f) => {
|
|
1962
2477
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1963
2478
|
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
@@ -2067,12 +2582,12 @@ var sumTranscriptUsage = (entries) => {
|
|
|
2067
2582
|
},
|
|
2068
2583
|
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
2069
2584
|
);
|
|
2070
|
-
const models = [...summed.totals].map(([model,
|
|
2585
|
+
const models = [...summed.totals].map(([model, t10]) => ({
|
|
2071
2586
|
model,
|
|
2072
|
-
input_tokens:
|
|
2073
|
-
output_tokens:
|
|
2074
|
-
cache_read_tokens:
|
|
2075
|
-
cache_write_tokens:
|
|
2587
|
+
input_tokens: t10.input,
|
|
2588
|
+
output_tokens: t10.output,
|
|
2589
|
+
cache_read_tokens: t10.cacheRead,
|
|
2590
|
+
cache_write_tokens: t10.cacheWrite
|
|
2076
2591
|
}));
|
|
2077
2592
|
const bounds = entries.reduce(
|
|
2078
2593
|
(acc, entry) => {
|
|
@@ -2272,8 +2787,8 @@ var spendClause = (i) => i.spentUsd === null ? null : i.budgetUsd !== null && i.
|
|
|
2272
2787
|
var timeClause = (i) => i.elapsedMs === null ? null : i.wallMs !== null && i.wallMs > 0 ? `${mins(i.elapsedMs)}/${mins(i.wallMs)} elapsed (${pct(i.elapsedMs / i.wallMs)})` : `${mins(i.elapsedMs)} elapsed`;
|
|
2273
2788
|
var directive = (phase, draftPath, isSubagent) => {
|
|
2274
2789
|
if (isSubagent)
|
|
2275
|
-
return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now and report the findings you have back to the main agent in your reply. Do not write ${draftPath} yourself; only the main agent writes it.` : `Wind down investigation and report the findings you have back to the main agent in your reply \u2014 do not write ${draftPath} yourself; the main agent writes it, and you may run out of budget otherwise.`;
|
|
2276
|
-
return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Do not wait for subagents still running in the background. Write your COMPLETE findings from what you already have to ${draftPath} and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 fold in the subagent reports you already have rather than waiting on stragglers; you may run out of budget before you finish otherwise.`;
|
|
2790
|
+
return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now and report the findings you have back to the main agent in your reply, naming each check you planned but did not finish. Do not write ${draftPath} yourself; only the main agent writes it.` : `Wind down investigation and report the findings you have back to the main agent in your reply, naming each check you planned but did not finish \u2014 do not write ${draftPath} yourself; the main agent writes it, and you may run out of budget otherwise.`;
|
|
2791
|
+
return phase.kind === "hard" ? `Budget nearly exhausted \u2014 STOP all new investigation now. Do not wait for subagents still running in the background. Write your COMPLETE findings from what you already have to ${draftPath}, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish, and run \`code-review validate ${draftPath} --explain\` until it passes (--explain prints the exact schema when the shape is wrong). Other tools are blocked until that draft is written.` : `Wind down investigation and write your COMPLETE findings to ${draftPath} now, listing in its \`unfinished\` field, when its schema has one, every check you or a subagent planned but did not finish, then run \`code-review validate ${draftPath} --explain\` (it prints the exact schema if the shape is wrong) \u2014 fold in the subagent reports you already have rather than waiting on stragglers; you may run out of budget before you finish otherwise.`;
|
|
2277
2792
|
};
|
|
2278
2793
|
var budgetMessage = (i, phase, draftPath, isSubagent) => {
|
|
2279
2794
|
const status = [spendClause(i), timeClause(i)].filter((c) => c !== null).join(" \xB7 ");
|
|
@@ -2297,7 +2812,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
2297
2812
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
2298
2813
|
if (WRITE_TOOLS.has(toolName)) {
|
|
2299
2814
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
2300
|
-
if (typeof fp === "string" && targets.some((
|
|
2815
|
+
if (typeof fp === "string" && targets.some((t10) => fp === t10 || basename(fp) === basename(t10)))
|
|
2301
2816
|
return true;
|
|
2302
2817
|
}
|
|
2303
2818
|
if (toolName === "Bash") {
|
|
@@ -2402,9 +2917,9 @@ var parseWallMs = (raw) => {
|
|
|
2402
2917
|
};
|
|
2403
2918
|
var parseEpochSecMs = (raw) => {
|
|
2404
2919
|
if (raw === void 0) return null;
|
|
2405
|
-
const
|
|
2406
|
-
if (!/^\d+$/.test(
|
|
2407
|
-
const n = Number.parseInt(
|
|
2920
|
+
const t10 = raw.trim();
|
|
2921
|
+
if (!/^\d+$/.test(t10)) return null;
|
|
2922
|
+
const n = Number.parseInt(t10, 10);
|
|
2408
2923
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
2409
2924
|
};
|
|
2410
2925
|
var anchoredElapsedMs = (src) => {
|
|
@@ -2817,220 +3332,6 @@ var checkRun = async (input, ghApi = runGhApi) => {
|
|
|
2817
3332
|
];
|
|
2818
3333
|
await ghApi(endpoint, JSON.stringify(body));
|
|
2819
3334
|
};
|
|
2820
|
-
var RESPONSE_REASON_CLIP_CHARS = 300;
|
|
2821
|
-
var RESPONSES_PER_CHANNEL = 25;
|
|
2822
|
-
var ChannelCodec = t.keyof({ comment: null, commit: null });
|
|
2823
|
-
var MAINTAINER_ASSOCIATIONS = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
2824
|
-
var CLOSING_DISPOSITIONS = /* @__PURE__ */ new Set(["refuted", "dismissed"]);
|
|
2825
|
-
var ResponseShape = t.type({
|
|
2826
|
-
id: t.string,
|
|
2827
|
-
disposition: ResponseDispositionCodec,
|
|
2828
|
-
reason: t.string,
|
|
2829
|
-
channel: ChannelCodec,
|
|
2830
|
-
source_url: t.string,
|
|
2831
|
-
author: t.union([t.string, t.null]),
|
|
2832
|
-
author_association: t.union([t.string, t.null]),
|
|
2833
|
-
created_at: t.union([t.string, t.null])
|
|
2834
|
-
});
|
|
2835
|
-
var ResponseCodec = strictExact("Response", ResponseShape, [ResponseShape]);
|
|
2836
|
-
var UnmatchedResponseShape = t.type({
|
|
2837
|
-
id: t.string,
|
|
2838
|
-
channel: ChannelCodec,
|
|
2839
|
-
source_url: t.string
|
|
2840
|
-
});
|
|
2841
|
-
var UnmatchedResponseCodec = strictExact("UnmatchedResponse", UnmatchedResponseShape, [
|
|
2842
|
-
UnmatchedResponseShape
|
|
2843
|
-
]);
|
|
2844
|
-
var ResponsesFileShape = t.type({
|
|
2845
|
-
responses: t.array(ResponseCodec),
|
|
2846
|
-
unmatched: t.array(UnmatchedResponseCodec)
|
|
2847
|
-
});
|
|
2848
|
-
strictExact("ResponsesFile", ResponsesFileShape, [
|
|
2849
|
-
ResponsesFileShape
|
|
2850
|
-
]);
|
|
2851
|
-
var RESPONSE_LINE_RE = /^ {0,3}review-response:\s*(\S+)\s+([A-Za-z]+)(?![A-Za-z]|\s*\|)[\s—–:-]*(.*)$/i;
|
|
2852
|
-
var FENCE_OPEN_RE = /^ {0,3}(`{3,}(?=[^`]*$)|~{3,})/;
|
|
2853
|
-
var LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/;
|
|
2854
|
-
var unfencedLines = (text) => text.split(LINE_BREAK_RE).reduce(
|
|
2855
|
-
(state, line) => {
|
|
2856
|
-
const marker = FENCE_OPEN_RE.exec(line)?.[1];
|
|
2857
|
-
if (state.fence === null) {
|
|
2858
|
-
if (marker !== void 0) return { fence: marker, lines: state.lines };
|
|
2859
|
-
state.lines.push(line);
|
|
2860
|
-
return state;
|
|
2861
|
-
}
|
|
2862
|
-
const closes = marker !== void 0 && marker[0] === state.fence[0] && marker.length >= state.fence.length && line.trim() === marker;
|
|
2863
|
-
return closes ? { fence: null, lines: state.lines } : state;
|
|
2864
|
-
},
|
|
2865
|
-
{ fence: null, lines: [] }
|
|
2866
|
-
).lines;
|
|
2867
|
-
var WHOLE_LINE_CODE_SPAN_RE = /^ {0,3}`([^`]+)`\s*$/;
|
|
2868
|
-
var answerLines = (text) => unfencedLines(text).map((line) => WHOLE_LINE_CODE_SPAN_RE.exec(line)?.[1] ?? line);
|
|
2869
|
-
var unwrapId = (token) => token.replace(/^[`"']+|[`"'.,:;]+$/g, "");
|
|
2870
|
-
var lineAnswers = (lines) => lines.flatMap((line) => {
|
|
2871
|
-
const match = RESPONSE_LINE_RE.exec(line);
|
|
2872
|
-
const id = unwrapId(match?.[1] ?? "");
|
|
2873
|
-
const disposition = match?.[2]?.toLowerCase();
|
|
2874
|
-
return id !== "" && DispositionCodec.is(disposition) ? [
|
|
2875
|
-
{
|
|
2876
|
-
id,
|
|
2877
|
-
disposition,
|
|
2878
|
-
reason: clipText((match?.[3] ?? "").trim(), RESPONSE_REASON_CLIP_CHARS)
|
|
2879
|
-
}
|
|
2880
|
-
] : [];
|
|
2881
|
-
});
|
|
2882
|
-
var ID_IN_CELL_RE = /`([^`]+)`/g;
|
|
2883
|
-
var ID_HEADERS = /* @__PURE__ */ new Set(["id", "ids", "finding", "findings"]);
|
|
2884
|
-
var DISPOSITION_HEADERS = /* @__PURE__ */ new Set(["disposition", "verdict", "resolution"]);
|
|
2885
|
-
var CELL_RE = /((?:\\[\s\S]?|[^\\|])*)(\||$)/g;
|
|
2886
|
-
var splitCells = (row) => {
|
|
2887
|
-
const cells = [...row.matchAll(CELL_RE)];
|
|
2888
|
-
return cells.slice(0, cells.findIndex((match) => match[2] !== "|") + 1).map((match) => match[1] ?? "");
|
|
2889
|
-
};
|
|
2890
|
-
var BLOCK_START_RE = /^ {0,3}([-+*]\s|\d+[.)]\s|#{1,6}(\s|$)|>)/;
|
|
2891
|
-
var tableRow = (line) => {
|
|
2892
|
-
if (!/^ {0,3}\S/.test(line) || BLOCK_START_RE.test(line)) return null;
|
|
2893
|
-
const split = splitCells(line.trim());
|
|
2894
|
-
if (split.length < 2) return null;
|
|
2895
|
-
const inner = split.slice(line.trim().startsWith("|") ? 1 : 0);
|
|
2896
|
-
return (inner.length > 1 && inner[inner.length - 1]?.trim() === "" ? inner.slice(0, -1) : inner).map((cell) => cell.trim().replace(/\\\|/g, "|"));
|
|
2897
|
-
};
|
|
2898
|
-
var isDelimiter = (cells) => cells !== null && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
2899
|
-
var plainCell = (cell) => cell.replace(/[*_`]/g, "").trim().toLowerCase();
|
|
2900
|
-
var columnName = (cell) => plainCell(cell).split(/[\s/]+/)[0] ?? "";
|
|
2901
|
-
var idsInCell = (cell) => {
|
|
2902
|
-
const ids = cell.replace(/\([^()]*\)/g, "");
|
|
2903
|
-
const quoted = [...ids.matchAll(ID_IN_CELL_RE)].map((match) => unwrapId((match[1] ?? "").trim())).filter((id) => id !== "");
|
|
2904
|
-
const bare = ids.split(",").map((piece) => unwrapId(piece.trim())).filter((id) => id !== "");
|
|
2905
|
-
return quoted.length > 0 ? quoted : bare.every((id) => /^\S+$/.test(id)) ? bare : [];
|
|
2906
|
-
};
|
|
2907
|
-
var tableDisposition = (cell) => {
|
|
2908
|
-
const firstWord = plainCell(cell).replace(/^[^a-z]+/, "").split(/[^a-z]/)[0] ?? "";
|
|
2909
|
-
return DispositionCodec.is(firstWord) ? firstWord : "unstated";
|
|
2910
|
-
};
|
|
2911
|
-
var rowAnswers = (cells, columns) => {
|
|
2912
|
-
const dispositionCell = cells[columns.disposition] ?? "";
|
|
2913
|
-
const disposition = tableDisposition(dispositionCell);
|
|
2914
|
-
const verdictOnly = DispositionCodec.is(plainCell(dispositionCell));
|
|
2915
|
-
const reason = clipText(
|
|
2916
|
-
cells.filter(
|
|
2917
|
-
(cell, column) => cell !== "" && column !== columns.id && !(verdictOnly && column === columns.disposition)
|
|
2918
|
-
).join(" \u2014 "),
|
|
2919
|
-
RESPONSE_REASON_CLIP_CHARS
|
|
2920
|
-
);
|
|
2921
|
-
return idsInCell(cells[columns.id] ?? "").map((id) => ({ id, disposition, reason }));
|
|
2922
|
-
};
|
|
2923
|
-
var namesAColumn = (name) => ID_HEADERS.has(name) || DISPOSITION_HEADERS.has(name);
|
|
2924
|
-
var tableAnswers = (lines) => {
|
|
2925
|
-
const rows = lines.map(tableRow);
|
|
2926
|
-
return rows.reduce(
|
|
2927
|
-
(state, cells, index) => {
|
|
2928
|
-
if (state.skipDelimiter) return { ...state, skipDelimiter: false };
|
|
2929
|
-
if (cells === null) return { ...state, inTable: false, columns: null };
|
|
2930
|
-
const opensTable = !isDelimiter(cells) && isDelimiter(rows[index + 1] ?? null) && (!state.inTable || cells.map(columnName).some(namesAColumn));
|
|
2931
|
-
if (opensTable) {
|
|
2932
|
-
const header = cells.map(columnName);
|
|
2933
|
-
const id = header.findIndex((name) => ID_HEADERS.has(name));
|
|
2934
|
-
const disposition = header.findIndex((name) => DISPOSITION_HEADERS.has(name));
|
|
2935
|
-
return {
|
|
2936
|
-
...state,
|
|
2937
|
-
inTable: true,
|
|
2938
|
-
skipDelimiter: true,
|
|
2939
|
-
columns: id < 0 || disposition < 0 ? null : { id, disposition }
|
|
2940
|
-
};
|
|
2941
|
-
}
|
|
2942
|
-
if (state.inTable && state.columns !== null && !isDelimiter(cells)) {
|
|
2943
|
-
state.answers.push(...rowAnswers(cells, state.columns));
|
|
2944
|
-
}
|
|
2945
|
-
return isDelimiter(rows[index + 1] ?? null) ? { ...state, skipDelimiter: true } : state;
|
|
2946
|
-
},
|
|
2947
|
-
{ columns: null, inTable: false, skipDelimiter: false, answers: [] }
|
|
2948
|
-
).answers;
|
|
2949
|
-
};
|
|
2950
|
-
var parseResponses = (text) => {
|
|
2951
|
-
const lines = answerLines(text);
|
|
2952
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2953
|
-
return [...lineAnswers(lines), ...tableAnswers(lines)].filter((answer) => {
|
|
2954
|
-
const key2 = JSON.stringify([answer.id, answer.disposition, answer.reason]);
|
|
2955
|
-
if (seen.has(key2)) return false;
|
|
2956
|
-
seen.add(key2);
|
|
2957
|
-
return true;
|
|
2958
|
-
});
|
|
2959
|
-
};
|
|
2960
|
-
var isHuman = (login, type8, botLogin) => login !== botLogin && type8 === "User";
|
|
2961
|
-
var newestFirst = (a, b) => {
|
|
2962
|
-
const [left2, right2] = [a.created_at ?? "", b.created_at ?? ""];
|
|
2963
|
-
return left2 < right2 ? 1 : left2 > right2 ? -1 : b.id - a.id;
|
|
2964
|
-
};
|
|
2965
|
-
var answersFrom = (input) => {
|
|
2966
|
-
const comments = [...input.comments].filter((comment) => isHuman(comment.user.login, comment.user.type ?? null, input.botLogin)).sort(newestFirst).flatMap(
|
|
2967
|
-
(comment) => parseResponses(comment.body ?? "").map((line) => ({
|
|
2968
|
-
...line,
|
|
2969
|
-
channel: "comment",
|
|
2970
|
-
source_url: `https://github.com/${input.repo}/pull/${String(input.prNumber)}#issuecomment-${String(comment.id)}`,
|
|
2971
|
-
author: comment.user.login,
|
|
2972
|
-
author_association: comment.author_association ?? null,
|
|
2973
|
-
created_at: comment.created_at ?? null
|
|
2974
|
-
}))
|
|
2975
|
-
);
|
|
2976
|
-
const commits = [...input.commits].reverse().flatMap(
|
|
2977
|
-
(commit) => parseResponses(commit.message).map((line) => ({
|
|
2978
|
-
...line,
|
|
2979
|
-
channel: "commit",
|
|
2980
|
-
source_url: `https://github.com/${input.repo}/commit/${commit.sha}`,
|
|
2981
|
-
author: commit.author,
|
|
2982
|
-
author_association: null,
|
|
2983
|
-
created_at: commit.date ?? null
|
|
2984
|
-
}))
|
|
2985
|
-
);
|
|
2986
|
-
return { comments, commits };
|
|
2987
|
-
};
|
|
2988
|
-
var harvestResponses = (input) => {
|
|
2989
|
-
const { comments: fromComments, commits: fromCommits } = answersFrom(input);
|
|
2990
|
-
const matched = (response) => input.priorIds.has(response.id);
|
|
2991
|
-
const echoed = (response) => !matched(response) && ID_SHAPE_RE.test(response.id);
|
|
2992
|
-
const groups = [fromComments, fromCommits].flatMap((channel) => [
|
|
2993
|
-
channel.filter(matched),
|
|
2994
|
-
channel.filter(echoed)
|
|
2995
|
-
]);
|
|
2996
|
-
const [comments, commentEchoes, commits, commitEchoes] = groups.map(
|
|
2997
|
-
(group) => group.slice(0, RESPONSES_PER_CHANNEL)
|
|
2998
|
-
);
|
|
2999
|
-
return {
|
|
3000
|
-
file: {
|
|
3001
|
-
responses: [...comments ?? [], ...commits ?? []],
|
|
3002
|
-
unmatched: [...commentEchoes ?? [], ...commitEchoes ?? []].map(
|
|
3003
|
-
({ id, channel, source_url }) => ({ id, channel, source_url })
|
|
3004
|
-
)
|
|
3005
|
-
},
|
|
3006
|
-
dropped: groups.reduce(
|
|
3007
|
-
(total, group) => total + Math.max(0, group.length - RESPONSES_PER_CHANNEL),
|
|
3008
|
-
0
|
|
3009
|
-
)
|
|
3010
|
-
};
|
|
3011
|
-
};
|
|
3012
|
-
var isTrustedResponse = (response, headInBaseRepo) => {
|
|
3013
|
-
switch (response.channel) {
|
|
3014
|
-
case "comment":
|
|
3015
|
-
return MAINTAINER_ASSOCIATIONS.includes(response.author_association ?? "");
|
|
3016
|
-
case "commit":
|
|
3017
|
-
return headInBaseRepo;
|
|
3018
|
-
}
|
|
3019
|
-
};
|
|
3020
|
-
var answeredInstant = (response) => response.created_at === null ? Number.NaN : Date.parse(response.created_at);
|
|
3021
|
-
var closingResponses = (responses, isTrusted) => {
|
|
3022
|
-
const byId = responses.filter((response) => response.disposition !== "unstated" && isTrusted(response)).reduce((groups, response) => {
|
|
3023
|
-
const timed = { response, at: answeredInstant(response) };
|
|
3024
|
-
groups.set(response.id, [...groups.get(response.id) ?? [], timed]);
|
|
3025
|
-
return groups;
|
|
3026
|
-
}, /* @__PURE__ */ new Map());
|
|
3027
|
-
return [...byId.values()].flatMap((answers) => {
|
|
3028
|
-
if (answers.some(({ at }) => Number.isNaN(at))) return [];
|
|
3029
|
-
const latest = Math.max(...answers.map(({ at }) => at));
|
|
3030
|
-
const newest = answers.filter(({ at }) => at === latest).map(({ response }) => response);
|
|
3031
|
-
return newest.every((response) => CLOSING_DISPOSITIONS.has(response.disposition)) ? [...newest].sort((a, b) => a.source_url < b.source_url ? -1 : 1).slice(0, 1) : [];
|
|
3032
|
-
});
|
|
3033
|
-
};
|
|
3034
3335
|
|
|
3035
3336
|
// src/post.ts
|
|
3036
3337
|
var STICKY_CHAR_LIMIT = 65536;
|
|
@@ -3079,15 +3380,16 @@ var loadFindings = (path) => {
|
|
|
3079
3380
|
}
|
|
3080
3381
|
const first = decodeFindings(raw);
|
|
3081
3382
|
if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => RECOVERABLE_OPTIONAL_FIELDS.has(k))) {
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3383
|
+
const recovered = withoutBrokenOptionalFields(
|
|
3384
|
+
raw,
|
|
3385
|
+
(candidate) => decodeFindings(candidate).kind === "ok"
|
|
3084
3386
|
);
|
|
3085
|
-
|
|
3086
|
-
if (retry.kind === "ok") {
|
|
3387
|
+
if (recovered !== null) {
|
|
3087
3388
|
process.stderr.write(
|
|
3088
|
-
|
|
3389
|
+
`Warning: the review draft carried an invalid best-effort field (${recovered.stripped.join("/")}) \u2014 stripped it and used the rest; the pipeline re-stamps convergence
|
|
3390
|
+
`
|
|
3089
3391
|
);
|
|
3090
|
-
return
|
|
3392
|
+
return decodeFindings(recovered.doc);
|
|
3091
3393
|
}
|
|
3092
3394
|
}
|
|
3093
3395
|
return first;
|
|
@@ -3551,8 +3853,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
3551
3853
|
const nodes = conn?.nodes;
|
|
3552
3854
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
3553
3855
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
3554
|
-
const ids = nodes.flatMap((
|
|
3555
|
-
const cnodes =
|
|
3856
|
+
const ids = nodes.flatMap((t10) => {
|
|
3857
|
+
const cnodes = t10.comments?.nodes;
|
|
3556
3858
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
3557
3859
|
});
|
|
3558
3860
|
return { ids, truncated };
|
|
@@ -3648,7 +3950,7 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3648
3950
|
convergence: conv ?? void 0
|
|
3649
3951
|
});
|
|
3650
3952
|
let warnedNoJsonUrl = false;
|
|
3651
|
-
const
|
|
3953
|
+
const findingsPair = (doc) => {
|
|
3652
3954
|
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
3653
3955
|
const carriedLink = existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
3654
3956
|
if (carriedLink !== null) {
|
|
@@ -3662,6 +3964,8 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3662
3964
|
}
|
|
3663
3965
|
return findingsMarkerPair(void 0, doc.convergence);
|
|
3664
3966
|
};
|
|
3967
|
+
const carriedDialogue = existingSticky !== null ? carriedDialogueMarker(existingSticky.body) : "";
|
|
3968
|
+
const findingsBlob = (doc, dialogue2 = carriedDialogue) => [findingsPair(doc), dialogue2].filter((marker) => marker !== "").join("\n");
|
|
3665
3969
|
const leaveInPlace = (message) => {
|
|
3666
3970
|
process.stderr.write(
|
|
3667
3971
|
message ?? "Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place\n"
|
|
@@ -3670,9 +3974,9 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3670
3974
|
process.exit(0);
|
|
3671
3975
|
};
|
|
3672
3976
|
const logAnsweredDrops = () => {
|
|
3673
|
-
if (
|
|
3977
|
+
if (droppedCount > 0) {
|
|
3674
3978
|
process.stderr.write(
|
|
3675
|
-
`${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each
|
|
3979
|
+
`${String(droppedCount)} verbatim re-raise(s) of answered findings or systemic problems were treated as answered \u2014 the preserved sticky shows each one
|
|
3676
3980
|
`
|
|
3677
3981
|
);
|
|
3678
3982
|
}
|
|
@@ -3750,7 +4054,7 @@ ${dropNote}` : ""}`,
|
|
|
3750
4054
|
const carried = existingSticky !== null ? carriedFindingsMarker(existingSticky.body) : null;
|
|
3751
4055
|
if (carried === null) return findingsBlob(doc);
|
|
3752
4056
|
const provenance = existingSticky !== null ? carriedAncestryMarkers(existingSticky.body) : "";
|
|
3753
|
-
return [carriedMarkerPointer(carried, doc.convergence), provenance].filter((p) => p !== "").join("\n\n");
|
|
4057
|
+
return [carriedMarkerPointer(carried, doc.convergence), carriedDialogue, provenance].filter((p) => p !== "").join("\n\n");
|
|
3754
4058
|
};
|
|
3755
4059
|
return formatMarkdown(
|
|
3756
4060
|
render({
|
|
@@ -3797,7 +4101,18 @@ ${dropNote}` : ""}`,
|
|
|
3797
4101
|
process.exit(0);
|
|
3798
4102
|
}
|
|
3799
4103
|
const loadedFindings = findingsResult.findings;
|
|
3800
|
-
const
|
|
4104
|
+
const priorDialogue = existingSticky !== null && input.withoutDialogue !== true ? parseDialogueMarker(existingSticky.body) : { entries: [], undecoded: [], skipped: 0 };
|
|
4105
|
+
if (priorDialogue.skipped > 0) {
|
|
4106
|
+
process.stderr.write(
|
|
4107
|
+
`Warning: ${String(priorDialogue.skipped)} entry(ies) of the prior sticky's dialogue state did not decode \u2014 carried unread
|
|
4108
|
+
`
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
const dialogueRound = input.withoutDialogue !== true && envelope !== null && isConvergenceRound(
|
|
4112
|
+
effectiveRoute,
|
|
4113
|
+
envelope.incomplete === true || isIncompleteFindings(loadedFindings)
|
|
4114
|
+
) && isReviewVerdict(loadedFindings.verdict);
|
|
4115
|
+
const answerable = input.withoutDialogue !== true && existingSticky !== null && !priorIsMechanic && (loadedFindings.findings.length > 0 || (loadedFindings.systemic_problems ?? []).length > 0 || dialogueRound && priorDialogue.entries.length > 0);
|
|
3801
4116
|
const headInBaseRepo = answerable && resolution.headRepo !== null && resolution.headRepo.toLowerCase() === input.repo.toLowerCase();
|
|
3802
4117
|
const answers = answerable ? answersFrom({
|
|
3803
4118
|
repo: input.repo,
|
|
@@ -3812,10 +4127,46 @@ ${dropNote}` : ""}`,
|
|
|
3812
4127
|
})),
|
|
3813
4128
|
commits: headInBaseRepo ? await fetchCommitAnswersSource(input.repo, prNumber, ghApi) : []
|
|
3814
4129
|
}) : { comments: [], commits: [] };
|
|
3815
|
-
const
|
|
4130
|
+
const isTrusted = (response) => isTrustedResponse(response, headInBaseRepo);
|
|
4131
|
+
const allAnswers = withRulingsOn(
|
|
3816
4132
|
[...answers.comments, ...answers.commits],
|
|
3817
|
-
|
|
3818
|
-
|
|
4133
|
+
new Set(
|
|
4134
|
+
priorDialogue.entries.filter((entry) => entry.state !== "rebutted").map((entry) => entry.id)
|
|
4135
|
+
),
|
|
4136
|
+
isTrusted
|
|
4137
|
+
);
|
|
4138
|
+
const unorderedRulings = allAnswers.filter(
|
|
4139
|
+
(answer) => RulingCodec.is(answer.disposition) && Number.isNaN(Date.parse(answer.created_at ?? ""))
|
|
4140
|
+
).length;
|
|
4141
|
+
if (unorderedRulings > 0) {
|
|
4142
|
+
process.stderr.write(
|
|
4143
|
+
`Warning: ${String(unorderedRulings)} maintainer ruling(s) carry no readable time, so they cannot be ordered against the other answers \u2014 not applied
|
|
4144
|
+
`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
const allClosures = closingResponses(allAnswers, isTrusted);
|
|
4148
|
+
const dialogue = answerable && dialogueRound ? advanceDialogue(priorDialogue.entries, dialogueAnswers(allAnswers, isTrusted), [
|
|
4149
|
+
...loadedFindings.findings.map((f) => ({
|
|
4150
|
+
id: resolveFindingId(f),
|
|
4151
|
+
title: f.title,
|
|
4152
|
+
severity: f.severity,
|
|
4153
|
+
rebutted: hasRebuttal(f)
|
|
4154
|
+
})),
|
|
4155
|
+
...(loadedFindings.systemic_problems ?? []).flatMap((s) => {
|
|
4156
|
+
const id = resolveSystemicId(s);
|
|
4157
|
+
return id === void 0 ? [] : [{ id, title: s.title, severity: s.severity, rebutted: hasRebuttal(s) }];
|
|
4158
|
+
})
|
|
4159
|
+
]) : priorDialogue.entries;
|
|
4160
|
+
const upheldIds = new Set(
|
|
4161
|
+
dialogue.filter((entry) => entry.state === "upheld").map((entry) => entry.id)
|
|
4162
|
+
);
|
|
4163
|
+
const closures = allClosures.filter(
|
|
4164
|
+
(closure) => couldMatch(closure.id, loadedFindings) && !upheldIds.has(closure.id)
|
|
4165
|
+
);
|
|
4166
|
+
const contested = dialogueRound ? dialogue.filter((entry) => entry.state === "contested") : [];
|
|
4167
|
+
const contestedIds = new Set(contested.map((entry) => entry.id));
|
|
4168
|
+
const isContestedFinding = findingIdIn(contestedIds);
|
|
4169
|
+
const isContestedSystemic = systemicIdIn(contestedIds);
|
|
3819
4170
|
const priorForAnswers = closures.length > 0 && existingSticky !== null ? { value: await resolvePriorFindings(existingSticky.body, readArtifact) } : null;
|
|
3820
4171
|
const answeredPrior = priorForAnswers === null ? null : resolveTolerantFindings(priorForAnswers.value);
|
|
3821
4172
|
if (priorForAnswers !== null && answeredPrior === null) {
|
|
@@ -3825,37 +4176,101 @@ ${dropNote}` : ""}`,
|
|
|
3825
4176
|
);
|
|
3826
4177
|
}
|
|
3827
4178
|
const answeredRegistry = answeredRegistryFrom(closures, answeredPrior);
|
|
3828
|
-
|
|
4179
|
+
const answeredSystemicRegistry = answeredSystemicRegistryFrom(closures, answeredPrior);
|
|
4180
|
+
const boundIds = new Set([...answeredRegistry, ...answeredSystemicRegistry].map((e) => e.code));
|
|
4181
|
+
const unboundClosureCount = answeredPrior === null ? 0 : closures.filter((closure) => !boundIds.has(closure.id)).length;
|
|
4182
|
+
if (unboundClosureCount > 0) {
|
|
3829
4183
|
process.stderr.write(
|
|
3830
|
-
`Warning: ${String(
|
|
4184
|
+
`Warning: ${String(unboundClosureCount)} maintainer answer(s) name nothing the prior review reported \u2014 they close nothing this round
|
|
3831
4185
|
`
|
|
3832
4186
|
);
|
|
3833
4187
|
}
|
|
3834
|
-
const
|
|
3835
|
-
|
|
4188
|
+
const rulings = new Map(
|
|
4189
|
+
dialogue.filter((entry) => entry.state === "overruled").map((entry) => [
|
|
4190
|
+
entry.id,
|
|
4191
|
+
{
|
|
4192
|
+
code: entry.id,
|
|
4193
|
+
title: entry.title,
|
|
4194
|
+
answerUrl: entry.answer,
|
|
4195
|
+
answerAuthor: allAnswers.find((answer) => answer.source_url === entry.answer)?.author ?? null
|
|
4196
|
+
}
|
|
4197
|
+
])
|
|
4198
|
+
);
|
|
4199
|
+
const overruledFilter = applyOverruled(
|
|
4200
|
+
loadedFindings.findings.filter((f) => !isContestedFinding(f)),
|
|
4201
|
+
rulings,
|
|
4202
|
+
resolveFindingId,
|
|
4203
|
+
answeredNoteKey
|
|
4204
|
+
);
|
|
4205
|
+
const overruledSystemicFilter = applyOverruled(
|
|
4206
|
+
(loadedFindings.systemic_problems ?? []).filter((s) => !isContestedSystemic(s)),
|
|
4207
|
+
rulings,
|
|
4208
|
+
resolveSystemicId,
|
|
4209
|
+
answeredSystemicNoteKey
|
|
4210
|
+
);
|
|
4211
|
+
const answeredFilter = applyAnswered(overruledFilter.kept, answeredRegistry);
|
|
4212
|
+
const answeredSystemicFilter = applyAnsweredSystemic(
|
|
4213
|
+
overruledSystemicFilter.kept,
|
|
4214
|
+
answeredSystemicRegistry
|
|
4215
|
+
);
|
|
4216
|
+
const contestedFindings = loadedFindings.findings.filter(isContestedFinding);
|
|
4217
|
+
const contestedSystemic = (loadedFindings.systemic_problems ?? []).filter(isContestedSystemic);
|
|
4218
|
+
const contestedReRaises = new Map([
|
|
4219
|
+
...contestedSystemic.map((s) => [resolveSystemicId(s) ?? "", s]),
|
|
4220
|
+
...contestedFindings.map((f) => [resolveFindingId(f), f])
|
|
4221
|
+
]);
|
|
4222
|
+
const contestedViews = contested.map((entry) => {
|
|
4223
|
+
const reRaise = contestedReRaises.get(entry.id);
|
|
4224
|
+
return {
|
|
4225
|
+
id: entry.id,
|
|
4226
|
+
title: entry.title,
|
|
4227
|
+
severity: entry.severity,
|
|
4228
|
+
answerUrl: entry.answer,
|
|
4229
|
+
...reRaise === void 0 ? {} : {
|
|
4230
|
+
reRaise: {
|
|
4231
|
+
..."path" in reRaise ? {
|
|
4232
|
+
location: `${reRaise.path}:${lineRange(reRaise.start_line, reRaise.end_line, "\u2013")}`
|
|
4233
|
+
} : {},
|
|
4234
|
+
description: reRaise.description,
|
|
4235
|
+
reasoning: reRaise.reasoning,
|
|
4236
|
+
...hasRebuttal(reRaise) ? { rebuttal: reRaise.rebuttal ?? "" } : {}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
});
|
|
4241
|
+
const reRaisedNotes = {
|
|
4242
|
+
...answeredFilter.reRaisedNotes,
|
|
4243
|
+
...answeredSystemicFilter.reRaisedNotes,
|
|
4244
|
+
...overruledFilter.reRaisedNotes,
|
|
4245
|
+
...overruledSystemicFilter.reRaisedNotes
|
|
4246
|
+
};
|
|
3836
4247
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
3837
|
-
const
|
|
4248
|
+
const answeredDropCount = answeredFilter.droppedCount + answeredSystemicFilter.droppedCount;
|
|
4249
|
+
const overruledDropCount = overruledFilter.droppedCount + overruledSystemicFilter.droppedCount;
|
|
4250
|
+
const droppedCount = answeredDropCount + overruledDropCount;
|
|
3838
4251
|
const droppedIds = /* @__PURE__ */ new Set([
|
|
3839
|
-
...verbatimReRaised.flatMap(
|
|
4252
|
+
...[...verbatimReRaised, ...overruledFilter.verbatimReRaised].flatMap(
|
|
4253
|
+
(e) => e.code !== "" ? [e.code] : []
|
|
4254
|
+
),
|
|
3840
4255
|
...answeredFilter.droppedFindingIds.filter((id) => id !== "")
|
|
3841
4256
|
]);
|
|
3842
|
-
const keptCodes = new Set(answeredFilter.findings.map((f) => f.id));
|
|
4257
|
+
const keptCodes = new Set([...answeredFilter.findings, ...contestedFindings].map((f) => f.id));
|
|
3843
4258
|
const trulyDropped = new Set([...droppedIds].filter((c) => !keptCodes.has(c)));
|
|
3844
|
-
const systemic = trulyDropped.size === 0 ?
|
|
4259
|
+
const systemic = trulyDropped.size === 0 ? answeredSystemicFilter.systemic : answeredSystemicFilter.systemic.map((s) => {
|
|
3845
4260
|
if (s.finding_ids === void 0) return s;
|
|
3846
4261
|
const codes = s.finding_ids.filter((c) => !trulyDropped.has(c));
|
|
3847
4262
|
return codes.length === s.finding_ids.length ? s : { ...s, finding_ids: codes };
|
|
3848
4263
|
});
|
|
3849
4264
|
const findings = {
|
|
3850
|
-
...loadedFindings,
|
|
3851
|
-
findings: [...answeredFilter.findings]
|
|
3852
|
-
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
4265
|
+
...withSystemicProblems(loadedFindings, [...systemic, ...contestedSystemic]),
|
|
4266
|
+
findings: [...answeredFilter.findings, ...contestedFindings]
|
|
3853
4267
|
};
|
|
3854
4268
|
const broadCurrentIds = [
|
|
3855
4269
|
...findings.findings.map((f) => f.id).filter((id) => id !== ""),
|
|
3856
4270
|
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
3857
4271
|
];
|
|
3858
|
-
const
|
|
4272
|
+
const argued = findings.findings.filter((f) => !isContestedFinding(f));
|
|
4273
|
+
const roundHasNit = argued.some((f) => f.severity === "nit");
|
|
3859
4274
|
const nitWantsPrior = roundHasNit && existingSticky !== null && (isFullReviewSticky(existingSticky.body) || isFullReviewAncestry(existingSticky.body));
|
|
3860
4275
|
const broadWantsPrior = existingSticky !== null && !priorIsMechanic && mentionsOutsideKnown(reachable, broadCurrentIds);
|
|
3861
4276
|
const resolvedPrior = priorForAnswers !== null ? priorForAnswers.value : existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
@@ -3869,15 +4284,25 @@ ${dropNote}` : ""}`,
|
|
|
3869
4284
|
// synthesized prior), the answeredNoteKey form (an empty-id prior), and the bare title: key
|
|
3870
4285
|
// (a pathless codeless prior).
|
|
3871
4286
|
priorSuppressedKeys.has(resolveFindingId(f)) || priorSuppressedKeys.has(answeredNoteKey(f)) || priorSuppressedKeys.has(`title:${f.title}`));
|
|
3872
|
-
const suppressedNits =
|
|
3873
|
-
const visibleFindings =
|
|
3874
|
-
const answeredDropNote =
|
|
4287
|
+
const suppressedNits = argued.filter(isSuppressedNit);
|
|
4288
|
+
const visibleFindings = argued.filter((f) => !isSuppressedNit(f));
|
|
4289
|
+
const answeredDropNote = [
|
|
4290
|
+
answeredReRaiseNote(
|
|
4291
|
+
[...verbatimReRaised, ...answeredSystemicFilter.verbatimReRaised],
|
|
4292
|
+
answeredDropCount
|
|
4293
|
+
),
|
|
4294
|
+
overruledReRaiseNote(
|
|
4295
|
+
[...overruledFilter.verbatimReRaised, ...overruledSystemicFilter.verbatimReRaised],
|
|
4296
|
+
overruledDropCount
|
|
4297
|
+
)
|
|
4298
|
+
].filter((note) => note !== "").join("\n>\n") + (droppedCount > 0 && findings.findings.length === 0 && (findings.systemic_problems ?? []).length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
|
|
3875
4299
|
const inlinePartition = input.inline === true && envelope !== null ? partitionFindings(visibleFindings, indexDiff(diff)) : null;
|
|
3876
4300
|
const straysForDiscussion = inlinePartition?.strays ?? visibleFindings;
|
|
3877
4301
|
const currentIds = [
|
|
3878
4302
|
...straysForDiscussion.map((f) => f.id).filter((id) => id !== ""),
|
|
3879
4303
|
...suppressedNits.map((f) => f.id).filter((id) => id !== ""),
|
|
3880
|
-
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
4304
|
+
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== ""),
|
|
4305
|
+
...contested.map((entry) => entry.id)
|
|
3881
4306
|
];
|
|
3882
4307
|
const orphanResolveFailed = existingSticky !== null && broadWantsPrior && carriedFindingsMarker(existingSticky.body) !== null && resolvedPrior === null;
|
|
3883
4308
|
const broadLive = new Set(broadCurrentIds);
|
|
@@ -3927,6 +4352,7 @@ ${findingsPointer2}`
|
|
|
3927
4352
|
] : [],
|
|
3928
4353
|
"",
|
|
3929
4354
|
terminalNotice({
|
|
4355
|
+
unfinished: [],
|
|
3930
4356
|
reviewedSha: input.headSha,
|
|
3931
4357
|
verdictBadge: verdictBadge(loadedFindings.verdict),
|
|
3932
4358
|
route: route ?? void 0,
|
|
@@ -3974,6 +4400,7 @@ ${note}
|
|
|
3974
4400
|
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3975
4401
|
answeredNotes: reRaisedNotes,
|
|
3976
4402
|
answeredReRaiseNote: answeredDropNote,
|
|
4403
|
+
contested: contestedViews,
|
|
3977
4404
|
discussionByFinding,
|
|
3978
4405
|
orphanedDiscussion,
|
|
3979
4406
|
orphanedTotal,
|
|
@@ -4094,7 +4521,8 @@ ${note}
|
|
|
4094
4521
|
priorTraj,
|
|
4095
4522
|
roundNumber,
|
|
4096
4523
|
currentCodes,
|
|
4097
|
-
input.headSha.slice(0, 12)
|
|
4524
|
+
input.headSha.slice(0, 12),
|
|
4525
|
+
contested
|
|
4098
4526
|
) : effectiveRoute === "mechanic" ? (
|
|
4099
4527
|
// A mechanic pass means CI failed — whatever the carried prior says, its own stamp must
|
|
4100
4528
|
// never read "converged". The threshold-relative critical floor keeps the carried triple
|
|
@@ -4103,10 +4531,13 @@ ${note}
|
|
|
4103
4531
|
) : priorConv;
|
|
4104
4532
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
4105
4533
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
4106
|
-
const findingsMarker = findingsBlob(
|
|
4534
|
+
const findingsMarker = findingsBlob(
|
|
4535
|
+
stampedFindings,
|
|
4536
|
+
answerable && dialogueRound ? dialogueMarker(dialogue, priorDialogue.undecoded) : carriedDialogue
|
|
4537
|
+
);
|
|
4107
4538
|
const commonRenderInput = {
|
|
4108
4539
|
...sharedRenderInput,
|
|
4109
|
-
findings: stampedFindings,
|
|
4540
|
+
findings: withoutIds(stampedFindings, contestedIds),
|
|
4110
4541
|
envelope,
|
|
4111
4542
|
incomplete: thisIncomplete,
|
|
4112
4543
|
sameRootNotes,
|
|
@@ -4314,6 +4745,24 @@ ${note}
|
|
|
4314
4745
|
finalDiscussionOverride
|
|
4315
4746
|
)
|
|
4316
4747
|
);
|
|
4748
|
+
if (answerable && dialogueRound) {
|
|
4749
|
+
appendRunSummary(process.env["GITHUB_STEP_SUMMARY"], () => {
|
|
4750
|
+
const trustedAnswerIds = new Set(allAnswers.filter(isTrusted).map((answer) => answer.id));
|
|
4751
|
+
return dialogueMetricsTable({
|
|
4752
|
+
commentAnswers: answers.comments.length,
|
|
4753
|
+
commitAnswers: answers.commits.length,
|
|
4754
|
+
closures: allClosures.length,
|
|
4755
|
+
droppedAnswered: answeredDropCount,
|
|
4756
|
+
droppedOverruled: overruledDropCount,
|
|
4757
|
+
annotated: Object.keys(reRaisedNotes).length,
|
|
4758
|
+
entries: dialogue,
|
|
4759
|
+
systemicUnanswered: (findings.systemic_problems ?? []).filter((s) => {
|
|
4760
|
+
const id = resolveSystemicId(s);
|
|
4761
|
+
return id === void 0 || !trustedAnswerIds.has(id);
|
|
4762
|
+
}).length
|
|
4763
|
+
});
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4317
4766
|
};
|
|
4318
4767
|
var noticeBody = (lead, existingBody) => {
|
|
4319
4768
|
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
@@ -4999,15 +5448,29 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4999
5448
|
join(input.outDir, "prior_findings.json"),
|
|
5000
5449
|
priorFindings === null ? "null" : JSON.stringify(priorFindings)
|
|
5001
5450
|
);
|
|
5451
|
+
const held = (seedsFromPrior ? parseDialogueMarker(prior.body).entries : []).filter(
|
|
5452
|
+
(entry) => entry.state !== "rebutted"
|
|
5453
|
+
);
|
|
5002
5454
|
const harvest = seedsFromPrior ? harvestResponses({
|
|
5003
5455
|
repo: input.repo,
|
|
5004
5456
|
prNumber,
|
|
5005
5457
|
botLogin: input.botLogin,
|
|
5006
|
-
priorIds: new Set(priorIdsFrom(priorFindings)),
|
|
5458
|
+
priorIds: /* @__PURE__ */ new Set([...priorIdsFrom(priorFindings), ...held.map((entry) => entry.id)]),
|
|
5007
5459
|
commits: commits.map((commit) => ({ ...commit, author: commit.login ?? null })),
|
|
5008
5460
|
comments: issueComments ?? []
|
|
5009
5461
|
}) : { file: { responses: [], unmatched: [] }, dropped: 0 };
|
|
5010
|
-
writeFileSync(
|
|
5462
|
+
writeFileSync(
|
|
5463
|
+
join(input.outDir, "responses.json"),
|
|
5464
|
+
JSON.stringify({
|
|
5465
|
+
...harvest.file,
|
|
5466
|
+
...Object.fromEntries(
|
|
5467
|
+
["contested", "upheld", "overruled"].flatMap((state) => {
|
|
5468
|
+
const claims = held.filter((entry) => entry.state === state).map(({ id, title, severity, at }) => ({ id, title, severity, since: at }));
|
|
5469
|
+
return claims.length > 0 ? [[state, claims]] : [];
|
|
5470
|
+
})
|
|
5471
|
+
)
|
|
5472
|
+
})
|
|
5473
|
+
);
|
|
5011
5474
|
if (seedsFromPrior && priorFindings === null) {
|
|
5012
5475
|
process.stderr.write(
|
|
5013
5476
|
"Note: the prior review's findings did not resolve \u2014 every id-shaped answer is staged as unmatched\n"
|
|
@@ -5382,7 +5845,7 @@ var parseScope = (raw) => {
|
|
|
5382
5845
|
reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
|
|
5383
5846
|
};
|
|
5384
5847
|
}
|
|
5385
|
-
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((
|
|
5848
|
+
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t10) => t10 !== "")));
|
|
5386
5849
|
return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
|
|
5387
5850
|
};
|
|
5388
5851
|
|
|
@@ -6205,11 +6668,6 @@ var seedDraftCmd = defineCommand({
|
|
|
6205
6668
|
const seededFromPrior = priorFindings === null ? false : (() => {
|
|
6206
6669
|
try {
|
|
6207
6670
|
const schemaPath = args.schema ? resolve(args.schema) : schemaPathFor(kind, args["schema-version"]);
|
|
6208
|
-
const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
|
|
6209
|
-
Object.entries(priorFindings).filter(
|
|
6210
|
-
([key2]) => !RECOVERABLE_OPTIONAL_FIELDS.has(key2)
|
|
6211
|
-
)
|
|
6212
|
-
) : priorFindings;
|
|
6213
6671
|
const accepts = (doc) => {
|
|
6214
6672
|
const resolved = resolvedPriorValue(doc);
|
|
6215
6673
|
if (resolved === null) return null;
|
|
@@ -6217,13 +6675,18 @@ var seedDraftCmd = defineCommand({
|
|
|
6217
6675
|
return validateAgainstSchema(carried, schemaPath).valid ? carried : null;
|
|
6218
6676
|
};
|
|
6219
6677
|
const seedDoc = accepts(priorFindings) ?? (() => {
|
|
6220
|
-
|
|
6221
|
-
|
|
6678
|
+
if (typeof priorFindings !== "object" || Array.isArray(priorFindings))
|
|
6679
|
+
return null;
|
|
6680
|
+
const recovered = withoutBrokenOptionalFields(
|
|
6681
|
+
priorFindings,
|
|
6682
|
+
(candidate) => accepts(candidate) !== null
|
|
6683
|
+
);
|
|
6684
|
+
if (recovered === null) return null;
|
|
6222
6685
|
process.stderr.write(
|
|
6223
|
-
`Note: the in-force schema rejects a carried recoverable field (
|
|
6686
|
+
`Note: the in-force schema rejects a carried recoverable field (${recovered.stripped.join("/")}) \u2014 seeding the prior without it (issue #150 review r2 / #182 review r2)
|
|
6224
6687
|
`
|
|
6225
6688
|
);
|
|
6226
|
-
return
|
|
6689
|
+
return accepts(recovered.doc);
|
|
6227
6690
|
})();
|
|
6228
6691
|
if (seedDoc === null) return false;
|
|
6229
6692
|
if (isIncompleteFindings(seedDoc)) return false;
|
|
@@ -6754,6 +7217,10 @@ var postCmd = defineCommand({
|
|
|
6754
7217
|
"unverified-no-logs": {
|
|
6755
7218
|
type: "boolean",
|
|
6756
7219
|
description: "Mark the review unverified: the fast-fix route ran with no failing-job logs staged, so its findings came from the diff alone. The caller decides this \u2014 the logs are staged in the review job, not here"
|
|
7220
|
+
},
|
|
7221
|
+
"without-dialogue": {
|
|
7222
|
+
type: "boolean",
|
|
7223
|
+
description: "Turn the review dialogue off: read no maintainer answers, so none closes, contests or rules on a finding, and carry the sticky's dialogue state untouched"
|
|
6757
7224
|
}
|
|
6758
7225
|
},
|
|
6759
7226
|
run: async ({ args }) => {
|
|
@@ -6788,6 +7255,7 @@ var postCmd = defineCommand({
|
|
|
6788
7255
|
nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
|
|
6789
7256
|
inline: args.inline,
|
|
6790
7257
|
unverifiedNoLogs: args["unverified-no-logs"],
|
|
7258
|
+
withoutDialogue: args["without-dialogue"],
|
|
6791
7259
|
postedAt: formatUtc(/* @__PURE__ */ new Date()),
|
|
6792
7260
|
pricedAt: /* @__PURE__ */ new Date()
|
|
6793
7261
|
});
|