@jphutchins/code-review 0.1.0-alpha.65 → 0.1.0-alpha.66
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 +730 -311
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/comment.eta +68 -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';
|
|
@@ -272,11 +273,28 @@ var withoutRebuttals = (doc) => ({
|
|
|
272
273
|
findings: doc.findings.map(({ rebuttal: _rebuttal, ...f }) => f),
|
|
273
274
|
...doc.systemic_problems !== void 0 ? { systemic_problems: doc.systemic_problems.map(({ rebuttal: _rebuttal, ...s }) => s) } : {}
|
|
274
275
|
});
|
|
276
|
+
var withSystemicProblems = ({ systemic_problems: _replaced, ...doc }, systemic) => ({
|
|
277
|
+
...doc,
|
|
278
|
+
...systemic.length > 0 ? { systemic_problems: [...systemic] } : {}
|
|
279
|
+
});
|
|
275
280
|
var synthesizedId = (parts, prefix) => `${prefix}${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 12)}`;
|
|
276
281
|
var synthesizedFindingId = (path, title) => synthesizedId([path, title], "f-");
|
|
277
282
|
var synthesizedSystemicId = (title) => synthesizedId([title], "s-");
|
|
278
283
|
var isSynthesizedFindingId = (id) => /^f-[A-Za-z0-9_-]{12}$/.test(id);
|
|
279
284
|
var resolveFindingId = (f) => f.id !== "" ? f.id : synthesizedFindingId(f.path, f.title);
|
|
285
|
+
var resolveSystemicId = (s) => s.id !== void 0 && s.id !== "" ? s.id : s.title !== "" ? synthesizedSystemicId(s.title) : void 0;
|
|
286
|
+
var findingIdIn = (ids) => (f) => ids.has(resolveFindingId(f));
|
|
287
|
+
var systemicIdIn = (ids) => (s) => {
|
|
288
|
+
const id = resolveSystemicId(s);
|
|
289
|
+
return id !== void 0 && ids.has(id);
|
|
290
|
+
};
|
|
291
|
+
var withoutIds = (doc, ids) => ids.size === 0 ? doc : {
|
|
292
|
+
...withSystemicProblems(
|
|
293
|
+
doc,
|
|
294
|
+
(doc.systemic_problems ?? []).filter((s) => !systemicIdIn(ids)(s))
|
|
295
|
+
),
|
|
296
|
+
findings: doc.findings.filter((f) => !findingIdIn(ids)(f))
|
|
297
|
+
};
|
|
280
298
|
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
299
|
var usableCountsMap = (v) => {
|
|
282
300
|
if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
|
|
@@ -664,6 +682,369 @@ var warnStalePrices = (prices, warn = defaultWarn) => {
|
|
|
664
682
|
`::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
683
|
);
|
|
666
684
|
};
|
|
685
|
+
var DispositionCodec = t.keyof({ fixed: null, refuted: null, dismissed: null });
|
|
686
|
+
var DISPOSITIONS = Object.keys(DispositionCodec.keys);
|
|
687
|
+
var RulingCodec = t.keyof({ upheld: null, overruled: null });
|
|
688
|
+
var RULINGS = Object.keys(RulingCodec.keys);
|
|
689
|
+
var ResponseDispositionCodec = t.keyof({
|
|
690
|
+
fixed: null,
|
|
691
|
+
refuted: null,
|
|
692
|
+
dismissed: null,
|
|
693
|
+
upheld: null,
|
|
694
|
+
overruled: null,
|
|
695
|
+
unstated: null
|
|
696
|
+
});
|
|
697
|
+
var isAnswerWord = (word) => DispositionCodec.is(word) || RulingCodec.is(word);
|
|
698
|
+
var RESPONSE_FORM = `Review-Response: <id> ${DISPOSITIONS.join("|")} \u2014 <reason>`;
|
|
699
|
+
var RESPONSE_TABLE_COLUMNS = ["id", "disposition", "reason"];
|
|
700
|
+
`| ${RESPONSE_TABLE_COLUMNS.join(" | ")} |`;
|
|
701
|
+
`| ${RESPONSE_TABLE_COLUMNS.map(() => "---").join(" | ")} |`;
|
|
702
|
+
var listed = (words, conjunction) => {
|
|
703
|
+
const quoted = words.map((word) => `\`${word}\``);
|
|
704
|
+
return quoted.length < 2 ? quoted.join("") : `${quoted.slice(0, -1).join(", ")} ${conjunction} ${quoted.at(-1) ?? ""}`;
|
|
705
|
+
};
|
|
706
|
+
var RULING_FORM = `Review-Response: <id> ${RULINGS.join("|")} \u2014 <reason>`;
|
|
707
|
+
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).`;
|
|
708
|
+
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.`;
|
|
709
|
+
|
|
710
|
+
// src/responses.ts
|
|
711
|
+
var RESPONSE_REASON_CLIP_CHARS = 300;
|
|
712
|
+
var RESPONSES_PER_CHANNEL = 25;
|
|
713
|
+
var ChannelCodec = t.keyof({ comment: null, commit: null });
|
|
714
|
+
var MAINTAINER_ASSOCIATIONS = ["OWNER", "MEMBER", "COLLABORATOR"];
|
|
715
|
+
var CLOSING_DISPOSITIONS = /* @__PURE__ */ new Set(["refuted", "dismissed"]);
|
|
716
|
+
var ResponseShape = t.type({
|
|
717
|
+
id: t.string,
|
|
718
|
+
disposition: ResponseDispositionCodec,
|
|
719
|
+
reason: t.string,
|
|
720
|
+
channel: ChannelCodec,
|
|
721
|
+
source_url: t.string,
|
|
722
|
+
author: t.union([t.string, t.null]),
|
|
723
|
+
author_association: t.union([t.string, t.null]),
|
|
724
|
+
created_at: t.union([t.string, t.null])
|
|
725
|
+
});
|
|
726
|
+
var ResponseCodec = strictExact("Response", ResponseShape, [ResponseShape]);
|
|
727
|
+
var UnmatchedResponseShape = t.type({
|
|
728
|
+
id: t.string,
|
|
729
|
+
channel: ChannelCodec,
|
|
730
|
+
source_url: t.string
|
|
731
|
+
});
|
|
732
|
+
var UnmatchedResponseCodec = strictExact("UnmatchedResponse", UnmatchedResponseShape, [
|
|
733
|
+
UnmatchedResponseShape
|
|
734
|
+
]);
|
|
735
|
+
var ResponsesFileAnswers = t.type({
|
|
736
|
+
responses: t.array(ResponseCodec),
|
|
737
|
+
unmatched: t.array(UnmatchedResponseCodec)
|
|
738
|
+
});
|
|
739
|
+
var HeldClaimShape = t.type({
|
|
740
|
+
id: t.string,
|
|
741
|
+
title: t.string,
|
|
742
|
+
severity: SeverityCodec,
|
|
743
|
+
since: t.string
|
|
744
|
+
});
|
|
745
|
+
var HeldClaimCodec = strictExact("HeldClaim", HeldClaimShape, [HeldClaimShape]);
|
|
746
|
+
var ResponsesFileDialogue = t.partial({
|
|
747
|
+
contested: t.array(HeldClaimCodec),
|
|
748
|
+
upheld: t.array(HeldClaimCodec),
|
|
749
|
+
overruled: t.array(HeldClaimCodec)
|
|
750
|
+
});
|
|
751
|
+
var ResponsesFileShape = t.intersection([ResponsesFileAnswers, ResponsesFileDialogue]);
|
|
752
|
+
strictExact("ResponsesFile", ResponsesFileShape, [
|
|
753
|
+
ResponsesFileAnswers,
|
|
754
|
+
ResponsesFileDialogue
|
|
755
|
+
]);
|
|
756
|
+
var RESPONSE_LINE_RE = /^ {0,3}review-response:\s*(\S+)\s+([A-Za-z]+)(?![A-Za-z]|\s*\|)[\s—–:-]*(.*)$/i;
|
|
757
|
+
var FENCE_OPEN_RE = /^ {0,3}(`{3,}(?=[^`]*$)|~{3,})/;
|
|
758
|
+
var LINE_BREAK_RE = /\r\n?|\n|\u2028|\u2029/;
|
|
759
|
+
var unfencedLines = (text) => text.split(LINE_BREAK_RE).reduce(
|
|
760
|
+
(state, line) => {
|
|
761
|
+
const marker = FENCE_OPEN_RE.exec(line)?.[1];
|
|
762
|
+
if (state.fence === null) {
|
|
763
|
+
if (marker !== void 0) return { fence: marker, lines: state.lines };
|
|
764
|
+
state.lines.push(line);
|
|
765
|
+
return state;
|
|
766
|
+
}
|
|
767
|
+
const closes = marker !== void 0 && marker[0] === state.fence[0] && marker.length >= state.fence.length && line.trim() === marker;
|
|
768
|
+
return closes ? { fence: null, lines: state.lines } : state;
|
|
769
|
+
},
|
|
770
|
+
{ fence: null, lines: [] }
|
|
771
|
+
).lines;
|
|
772
|
+
var WHOLE_LINE_CODE_SPAN_RE = /^ {0,3}`([^`]+)`\s*$/;
|
|
773
|
+
var answerLines = (text) => unfencedLines(text).map((line) => WHOLE_LINE_CODE_SPAN_RE.exec(line)?.[1] ?? line);
|
|
774
|
+
var unwrapId = (token) => token.replace(/^[`"']+|[`"'.,:;]+$/g, "");
|
|
775
|
+
var lineAnswers = (lines) => lines.flatMap((line) => {
|
|
776
|
+
const match = RESPONSE_LINE_RE.exec(line);
|
|
777
|
+
const id = unwrapId(match?.[1] ?? "");
|
|
778
|
+
const disposition = match?.[2]?.toLowerCase();
|
|
779
|
+
return id !== "" && isAnswerWord(disposition) ? [
|
|
780
|
+
{
|
|
781
|
+
id,
|
|
782
|
+
disposition,
|
|
783
|
+
reason: clipText((match?.[3] ?? "").trim(), RESPONSE_REASON_CLIP_CHARS)
|
|
784
|
+
}
|
|
785
|
+
] : [];
|
|
786
|
+
});
|
|
787
|
+
var ID_IN_CELL_RE = /`([^`]+)`/g;
|
|
788
|
+
var ID_HEADERS = /* @__PURE__ */ new Set(["id", "ids", "finding", "findings"]);
|
|
789
|
+
var DISPOSITION_HEADERS = /* @__PURE__ */ new Set(["disposition", "verdict", "resolution"]);
|
|
790
|
+
var CELL_RE = /((?:\\[\s\S]?|[^\\|])*)(\||$)/g;
|
|
791
|
+
var splitCells = (row) => {
|
|
792
|
+
const cells = [...row.matchAll(CELL_RE)];
|
|
793
|
+
return cells.slice(0, cells.findIndex((match) => match[2] !== "|") + 1).map((match) => match[1] ?? "");
|
|
794
|
+
};
|
|
795
|
+
var BLOCK_START_RE = /^ {0,3}([-+*]\s|\d+[.)]\s|#{1,6}(\s|$)|>)/;
|
|
796
|
+
var tableRow = (line) => {
|
|
797
|
+
if (!/^ {0,3}\S/.test(line) || BLOCK_START_RE.test(line)) return null;
|
|
798
|
+
const split = splitCells(line.trim());
|
|
799
|
+
if (split.length < 2) return null;
|
|
800
|
+
const inner = split.slice(line.trim().startsWith("|") ? 1 : 0);
|
|
801
|
+
return (inner.length > 1 && inner[inner.length - 1]?.trim() === "" ? inner.slice(0, -1) : inner).map((cell) => cell.trim().replace(/\\\|/g, "|"));
|
|
802
|
+
};
|
|
803
|
+
var isDelimiter = (cells) => cells !== null && cells.every((cell) => /^:?-+:?$/.test(cell));
|
|
804
|
+
var plainCell = (cell) => cell.replace(/[*_`]/g, "").trim().toLowerCase();
|
|
805
|
+
var columnName = (cell) => plainCell(cell).split(/[\s/]+/)[0] ?? "";
|
|
806
|
+
var idsInCell = (cell) => {
|
|
807
|
+
const ids = cell.replace(/\([^()]*\)/g, "");
|
|
808
|
+
const quoted = [...ids.matchAll(ID_IN_CELL_RE)].map((match) => unwrapId((match[1] ?? "").trim())).filter((id) => id !== "");
|
|
809
|
+
const bare = ids.split(",").map((piece) => unwrapId(piece.trim())).filter((id) => id !== "");
|
|
810
|
+
return quoted.length > 0 ? quoted : bare.every((id) => /^\S+$/.test(id)) ? bare : [];
|
|
811
|
+
};
|
|
812
|
+
var tableDisposition = (cell) => {
|
|
813
|
+
const firstWord = plainCell(cell).replace(/^[^a-z]+/, "").split(/[^a-z]/)[0] ?? "";
|
|
814
|
+
return isAnswerWord(firstWord) ? firstWord : "unstated";
|
|
815
|
+
};
|
|
816
|
+
var rowAnswers = (cells, columns) => {
|
|
817
|
+
const dispositionCell = cells[columns.disposition] ?? "";
|
|
818
|
+
const disposition = tableDisposition(dispositionCell);
|
|
819
|
+
const verdictOnly = isAnswerWord(plainCell(dispositionCell));
|
|
820
|
+
const reason = clipText(
|
|
821
|
+
cells.filter(
|
|
822
|
+
(cell, column) => cell !== "" && column !== columns.id && !(verdictOnly && column === columns.disposition)
|
|
823
|
+
).join(" \u2014 "),
|
|
824
|
+
RESPONSE_REASON_CLIP_CHARS
|
|
825
|
+
);
|
|
826
|
+
return idsInCell(cells[columns.id] ?? "").map((id) => ({ id, disposition, reason }));
|
|
827
|
+
};
|
|
828
|
+
var namesAColumn = (name) => ID_HEADERS.has(name) || DISPOSITION_HEADERS.has(name);
|
|
829
|
+
var tableAnswers = (lines) => {
|
|
830
|
+
const rows = lines.map(tableRow);
|
|
831
|
+
return rows.reduce(
|
|
832
|
+
(state, cells, index) => {
|
|
833
|
+
if (state.skipDelimiter) return { ...state, skipDelimiter: false };
|
|
834
|
+
if (cells === null) return { ...state, inTable: false, columns: null };
|
|
835
|
+
const opensTable = !isDelimiter(cells) && isDelimiter(rows[index + 1] ?? null) && (!state.inTable || cells.map(columnName).some(namesAColumn));
|
|
836
|
+
if (opensTable) {
|
|
837
|
+
const header = cells.map(columnName);
|
|
838
|
+
const id = header.findIndex((name) => ID_HEADERS.has(name));
|
|
839
|
+
const disposition = header.findIndex((name) => DISPOSITION_HEADERS.has(name));
|
|
840
|
+
return {
|
|
841
|
+
...state,
|
|
842
|
+
inTable: true,
|
|
843
|
+
skipDelimiter: true,
|
|
844
|
+
columns: id < 0 || disposition < 0 ? null : { id, disposition }
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (state.inTable && state.columns !== null && !isDelimiter(cells)) {
|
|
848
|
+
state.answers.push(...rowAnswers(cells, state.columns));
|
|
849
|
+
}
|
|
850
|
+
return isDelimiter(rows[index + 1] ?? null) ? { ...state, skipDelimiter: true } : state;
|
|
851
|
+
},
|
|
852
|
+
{ columns: null, inTable: false, skipDelimiter: false, answers: [] }
|
|
853
|
+
).answers;
|
|
854
|
+
};
|
|
855
|
+
var parseResponses = (text) => {
|
|
856
|
+
const lines = answerLines(text);
|
|
857
|
+
const seen = /* @__PURE__ */ new Set();
|
|
858
|
+
return [...lineAnswers(lines), ...tableAnswers(lines)].filter((answer) => {
|
|
859
|
+
const key2 = JSON.stringify([answer.id, answer.disposition, answer.reason]);
|
|
860
|
+
if (seen.has(key2)) return false;
|
|
861
|
+
seen.add(key2);
|
|
862
|
+
return true;
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
var isHuman = (login, type9, botLogin) => login !== botLogin && type9 === "User";
|
|
866
|
+
var newestFirst = (a, b) => {
|
|
867
|
+
const [left2, right2] = [a.created_at ?? "", b.created_at ?? ""];
|
|
868
|
+
return left2 < right2 ? 1 : left2 > right2 ? -1 : b.id - a.id;
|
|
869
|
+
};
|
|
870
|
+
var answersFrom = (input) => {
|
|
871
|
+
const comments = [...input.comments].filter((comment) => isHuman(comment.user.login, comment.user.type ?? null, input.botLogin)).sort(newestFirst).flatMap(
|
|
872
|
+
(comment) => parseResponses(comment.body ?? "").map((line) => ({
|
|
873
|
+
...line,
|
|
874
|
+
channel: "comment",
|
|
875
|
+
source_url: `https://github.com/${input.repo}/pull/${String(input.prNumber)}#issuecomment-${String(comment.id)}`,
|
|
876
|
+
author: comment.user.login,
|
|
877
|
+
author_association: comment.author_association ?? null,
|
|
878
|
+
created_at: comment.created_at ?? null
|
|
879
|
+
}))
|
|
880
|
+
);
|
|
881
|
+
const commits = [...input.commits].reverse().flatMap(
|
|
882
|
+
(commit) => parseResponses(commit.message).map((line) => ({
|
|
883
|
+
...line,
|
|
884
|
+
channel: "commit",
|
|
885
|
+
source_url: `https://github.com/${input.repo}/commit/${commit.sha}`,
|
|
886
|
+
author: commit.author,
|
|
887
|
+
author_association: null,
|
|
888
|
+
created_at: commit.date ?? null
|
|
889
|
+
}))
|
|
890
|
+
);
|
|
891
|
+
return { comments, commits };
|
|
892
|
+
};
|
|
893
|
+
var harvestResponses = (input) => {
|
|
894
|
+
const { comments: fromComments, commits: fromCommits } = answersFrom(input);
|
|
895
|
+
const matched = (response) => input.priorIds.has(response.id);
|
|
896
|
+
const echoed = (response) => !matched(response) && ID_SHAPE_RE.test(response.id);
|
|
897
|
+
const groups = [fromComments, fromCommits].flatMap((channel) => [
|
|
898
|
+
channel.filter(matched),
|
|
899
|
+
channel.filter(echoed)
|
|
900
|
+
]);
|
|
901
|
+
const [comments, commentEchoes, commits, commitEchoes] = groups.map(
|
|
902
|
+
(group) => group.slice(0, RESPONSES_PER_CHANNEL)
|
|
903
|
+
);
|
|
904
|
+
return {
|
|
905
|
+
file: {
|
|
906
|
+
responses: [...comments ?? [], ...commits ?? []],
|
|
907
|
+
unmatched: [...commentEchoes ?? [], ...commitEchoes ?? []].map(
|
|
908
|
+
({ id, channel, source_url }) => ({ id, channel, source_url })
|
|
909
|
+
)
|
|
910
|
+
},
|
|
911
|
+
dropped: groups.reduce(
|
|
912
|
+
(total, group) => total + Math.max(0, group.length - RESPONSES_PER_CHANNEL),
|
|
913
|
+
0
|
|
914
|
+
)
|
|
915
|
+
};
|
|
916
|
+
};
|
|
917
|
+
var isTrustedResponse = (response, headInBaseRepo) => {
|
|
918
|
+
switch (response.channel) {
|
|
919
|
+
case "comment":
|
|
920
|
+
return MAINTAINER_ASSOCIATIONS.includes(response.author_association ?? "");
|
|
921
|
+
case "commit":
|
|
922
|
+
return headInBaseRepo;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
var answeredInstant = (response) => response.created_at === null ? Number.NaN : Date.parse(response.created_at);
|
|
926
|
+
var closingResponses = (responses, isTrusted) => newestTrustedAnswersIn(responses, isTrusted, CLOSING_DISPOSITIONS);
|
|
927
|
+
var withRulingsOn = (responses, ruledIds, isTrusted) => responses.map(
|
|
928
|
+
(response) => RulingCodec.is(response.disposition) && !(ruledIds.has(response.id) && isTrusted(response)) ? { ...response, disposition: "unstated" } : response
|
|
929
|
+
);
|
|
930
|
+
var newestTrustedAnswersIn = (responses, isTrusted, dispositions) => {
|
|
931
|
+
const byId = responses.filter((response) => response.disposition !== "unstated" && isTrusted(response)).reduce((groups, response) => {
|
|
932
|
+
const timed = { response, at: answeredInstant(response) };
|
|
933
|
+
groups.set(response.id, [...groups.get(response.id) ?? [], timed]);
|
|
934
|
+
return groups;
|
|
935
|
+
}, /* @__PURE__ */ new Map());
|
|
936
|
+
return [...byId.values()].flatMap((answers) => {
|
|
937
|
+
if (answers.some(({ at }) => Number.isNaN(at))) return [];
|
|
938
|
+
const latest = Math.max(...answers.map(({ at }) => at));
|
|
939
|
+
const newest = answers.filter(({ at }) => at === latest).map(({ response }) => response);
|
|
940
|
+
return newest.every((response) => dispositions.has(response.disposition)) ? [...newest].sort((a, b) => a.source_url < b.source_url ? -1 : 1).slice(0, 1) : [];
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// src/dialogue.ts
|
|
945
|
+
var DialogueStateCodec = t.keyof({
|
|
946
|
+
rebutted: null,
|
|
947
|
+
contested: null,
|
|
948
|
+
upheld: null,
|
|
949
|
+
overruled: null
|
|
950
|
+
});
|
|
951
|
+
var DialogueEntryCodec = t.type({
|
|
952
|
+
id: t.string,
|
|
953
|
+
state: DialogueStateCodec,
|
|
954
|
+
answer: t.string,
|
|
955
|
+
at: t.string,
|
|
956
|
+
title: t.string,
|
|
957
|
+
severity: SeverityCodec
|
|
958
|
+
});
|
|
959
|
+
var DIALOGUE_STATES = Object.keys(DialogueStateCodec.keys);
|
|
960
|
+
var DIALOGUE_ENTRY_CAP = 32;
|
|
961
|
+
var decodeDialogue = (raw) => {
|
|
962
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
963
|
+
const decoded = items.map((item) => ({ item, entry: DialogueEntryCodec.decode(item) }));
|
|
964
|
+
const entries = decoded.flatMap(({ entry }) => isRight(entry) ? [entry.right] : []);
|
|
965
|
+
const undecoded = decoded.flatMap(({ item, entry }) => isRight(entry) ? [] : [item]);
|
|
966
|
+
return { entries, undecoded, skipped: Array.isArray(raw) ? undecoded.length : 1 };
|
|
967
|
+
};
|
|
968
|
+
var dialogueAnswers = (responses, isTrusted) => {
|
|
969
|
+
const newest = (dispositions) => new Map(
|
|
970
|
+
newestTrustedAnswersIn(responses, isTrusted, new Set(dispositions)).map((response) => [
|
|
971
|
+
response.id,
|
|
972
|
+
response
|
|
973
|
+
])
|
|
974
|
+
);
|
|
975
|
+
return {
|
|
976
|
+
closures: newest(["refuted", "dismissed"]),
|
|
977
|
+
overrulings: newest(["overruled"]),
|
|
978
|
+
upholdings: newest(["upheld"]),
|
|
979
|
+
fixed: new Set(newest(["fixed"]).keys())
|
|
980
|
+
};
|
|
981
|
+
};
|
|
982
|
+
var linked = (entry, answer, state) => answer.created_at === null ? [entry] : [{ ...entry, state, answer: answer.source_url, at: answer.created_at }];
|
|
983
|
+
var isNewer = (answer, entry) => answer.created_at !== null && Date.parse(answer.created_at) > Date.parse(entry.at);
|
|
984
|
+
var isOlder = (answer, entry) => answer.created_at !== null && Date.parse(answer.created_at) < Date.parse(entry.at);
|
|
985
|
+
var advanceEntry = (entry, answers, item) => {
|
|
986
|
+
if (answers.fixed.has(entry.id)) return [];
|
|
987
|
+
const refreshed = item !== void 0 && (entry.state === "contested" || entry.state === "upheld" || item.rebutted) ? { ...entry, title: item.title, severity: item.severity } : entry;
|
|
988
|
+
const closure = answers.closures.get(entry.id);
|
|
989
|
+
switch (entry.state) {
|
|
990
|
+
case "rebutted":
|
|
991
|
+
return closure === void 0 ? [refreshed] : linked(refreshed, closure, isNewer(closure, entry) ? "contested" : "rebutted");
|
|
992
|
+
case "contested": {
|
|
993
|
+
const overruling = answers.overrulings.get(entry.id);
|
|
994
|
+
const upholding = answers.upholdings.get(entry.id);
|
|
995
|
+
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");
|
|
996
|
+
}
|
|
997
|
+
case "upheld": {
|
|
998
|
+
const overruling = answers.overrulings.get(entry.id);
|
|
999
|
+
return overruling !== void 0 && isNewer(overruling, entry) ? linked(refreshed, overruling, "overruled") : [refreshed];
|
|
1000
|
+
}
|
|
1001
|
+
case "overruled": {
|
|
1002
|
+
const upholding = answers.upholdings.get(entry.id);
|
|
1003
|
+
return upholding !== void 0 && isNewer(upholding, entry) ? linked(refreshed, upholding, "upheld") : [refreshed];
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
var advanceDialogue = (prior, answers, current) => {
|
|
1008
|
+
const currentById = new Map(current.map((item) => [item.id, item]));
|
|
1009
|
+
const priorIds = new Set(prior.map((entry) => entry.id));
|
|
1010
|
+
const opened = [...currentById.values()].flatMap((item) => {
|
|
1011
|
+
const closure = answers.closures.get(item.id);
|
|
1012
|
+
const at = closure?.created_at ?? null;
|
|
1013
|
+
return priorIds.has(item.id) || !item.rebutted || closure === void 0 || at === null ? [] : [
|
|
1014
|
+
{
|
|
1015
|
+
id: item.id,
|
|
1016
|
+
state: "rebutted",
|
|
1017
|
+
answer: closure.source_url,
|
|
1018
|
+
at,
|
|
1019
|
+
title: item.title,
|
|
1020
|
+
severity: item.severity
|
|
1021
|
+
}
|
|
1022
|
+
];
|
|
1023
|
+
});
|
|
1024
|
+
const entries = [
|
|
1025
|
+
...prior.flatMap((entry) => advanceEntry(entry, answers, currentById.get(entry.id))),
|
|
1026
|
+
...opened
|
|
1027
|
+
];
|
|
1028
|
+
const held = entries.filter((entry) => entry.state !== "rebutted");
|
|
1029
|
+
return [
|
|
1030
|
+
...held,
|
|
1031
|
+
...entries.filter((entry) => entry.state === "rebutted").slice(0, Math.max(0, DIALOGUE_ENTRY_CAP - held.length))
|
|
1032
|
+
];
|
|
1033
|
+
};
|
|
1034
|
+
var dialogueMetricsTable = (metrics) => [
|
|
1035
|
+
"### \u{1F4AC} Review dialogue",
|
|
1036
|
+
"",
|
|
1037
|
+
"| | |",
|
|
1038
|
+
"|---|--:|",
|
|
1039
|
+
`| answers read (comments \xB7 commits) | ${String(metrics.commentAnswers)} \xB7 ${String(metrics.commitAnswers)} |`,
|
|
1040
|
+
`| trusted closing answers | ${String(metrics.closures)} |`,
|
|
1041
|
+
`| re-raises dropped (answered \xB7 overruled) | ${String(metrics.droppedAnswered)} \xB7 ${String(metrics.droppedOverruled)} |`,
|
|
1042
|
+
`| re-raises kept and annotated | ${String(metrics.annotated)} |`,
|
|
1043
|
+
`| ${DIALOGUE_STATES.join(" \xB7 ")} | ${DIALOGUE_STATES.map(
|
|
1044
|
+
(state) => String(metrics.entries.filter((entry) => entry.state === state).length)
|
|
1045
|
+
).join(" \xB7 ")} |`,
|
|
1046
|
+
`| systemic problems no trusted answer names | ${String(metrics.systemicUnanswered)} |`
|
|
1047
|
+
].join("\n");
|
|
667
1048
|
|
|
668
1049
|
// src/patch.ts
|
|
669
1050
|
var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
@@ -791,6 +1172,7 @@ var decodeBase64Json = (b64) => {
|
|
|
791
1172
|
return void 0;
|
|
792
1173
|
}
|
|
793
1174
|
};
|
|
1175
|
+
var noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
794
1176
|
var findingsPointer = (jsonUrl) => encodeMarker(jsonUrl);
|
|
795
1177
|
var warnedValveFindings = /* @__PURE__ */ new Set();
|
|
796
1178
|
var INLINE_PROSE_CLIP_THRESHOLD_CHARS = 3e4;
|
|
@@ -1022,8 +1404,15 @@ var convergenceScore = (doc, threshold) => round2(
|
|
|
1022
1404
|
0
|
|
1023
1405
|
)
|
|
1024
1406
|
);
|
|
1025
|
-
var
|
|
1026
|
-
|
|
1407
|
+
var CONTESTED_MAJOR = 0.51;
|
|
1408
|
+
var contestedWeight = (severity, threshold) => severity === "major" ? CONTESTED_MAJOR : convergenceFloor(severity, threshold);
|
|
1409
|
+
var dialogueScore = (doc, contested, threshold) => {
|
|
1410
|
+
return round2(
|
|
1411
|
+
convergenceScore(withoutIds(doc, new Set(contested.map((entry) => entry.id))), threshold) + contested.reduce((sum, entry) => sum + contestedWeight(entry.severity, threshold), 0)
|
|
1412
|
+
);
|
|
1413
|
+
};
|
|
1414
|
+
var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha, contested = []) => {
|
|
1415
|
+
const score = dialogueScore(doc, contested, threshold);
|
|
1027
1416
|
const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
|
|
1028
1417
|
const current = {
|
|
1029
1418
|
round,
|
|
@@ -1154,6 +1543,13 @@ var findingsMarkerPair = (jsonUrl, convergence) => {
|
|
|
1154
1543
|
return marker === "" ? conv : `${marker}
|
|
1155
1544
|
${conv}`;
|
|
1156
1545
|
};
|
|
1546
|
+
var DIALOGUE_RE = /<!-- code-review:dialogue;base64 ([A-Za-z0-9+/=]+) -->/;
|
|
1547
|
+
var dialogueMarker = (entries, undecoded = []) => entries.length + undecoded.length === 0 ? "" : `<!-- code-review:dialogue;base64 ${Buffer.from(JSON.stringify([...entries, ...undecoded]), "utf-8").toString("base64")} -->`;
|
|
1548
|
+
var carriedDialogueMarker = (body) => DIALOGUE_RE.exec(body)?.[0] ?? "";
|
|
1549
|
+
var parseDialogueMarker = (body) => {
|
|
1550
|
+
const b64 = DIALOGUE_RE.exec(body)?.[1];
|
|
1551
|
+
return b64 === void 0 ? { entries: [], undecoded: [], skipped: 0 } : decodeDialogue(decodeBase64Json(b64));
|
|
1552
|
+
};
|
|
1157
1553
|
var parseConvergenceMarker = (body) => {
|
|
1158
1554
|
const b64 = CONVERGENCE_RE.exec(body)?.[1];
|
|
1159
1555
|
return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
|
|
@@ -1210,11 +1606,20 @@ var carryForwardMarkers = (body) => {
|
|
|
1210
1606
|
const findings = carriedFindingsMarker(body);
|
|
1211
1607
|
const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
|
|
1212
1608
|
const convergence = CONVERGENCE_RE.exec(body)?.[0];
|
|
1609
|
+
const dialogue = DIALOGUE_RE.exec(body)?.[0];
|
|
1213
1610
|
const rounds = ROUNDS_RE.exec(body)?.[0];
|
|
1214
1611
|
const signal = SIGNAL_RE.exec(body)?.[0];
|
|
1215
1612
|
const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
|
|
1216
1613
|
${findings}` : void 0;
|
|
1217
|
-
return [
|
|
1614
|
+
return [
|
|
1615
|
+
findingsBlock,
|
|
1616
|
+
reviewedSha,
|
|
1617
|
+
carriedProvenanceMarkers(body),
|
|
1618
|
+
convergence,
|
|
1619
|
+
dialogue,
|
|
1620
|
+
rounds,
|
|
1621
|
+
signal
|
|
1622
|
+
].filter((m) => m !== void 0 && m !== "").join("\n\n");
|
|
1218
1623
|
};
|
|
1219
1624
|
var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
|
|
1220
1625
|
var projectPatch = (patch, surface) => {
|
|
@@ -1392,29 +1797,38 @@ var resolvePriorFindings = async (body, read) => {
|
|
|
1392
1797
|
};
|
|
1393
1798
|
|
|
1394
1799
|
// src/answered.ts
|
|
1395
|
-
var
|
|
1396
|
-
const
|
|
1397
|
-
return
|
|
1398
|
-
{
|
|
1399
|
-
code: response.id,
|
|
1400
|
-
title: finding.title,
|
|
1401
|
-
description: finding.description,
|
|
1402
|
-
reasoning: finding.reasoning,
|
|
1403
|
-
severity: finding.severity,
|
|
1404
|
-
path: finding.path,
|
|
1405
|
-
patch: finding.patch ?? null,
|
|
1406
|
-
answerUrl: response.source_url,
|
|
1407
|
-
answerAuthor: response.author
|
|
1408
|
-
}
|
|
1409
|
-
];
|
|
1800
|
+
var closedPriorItems = (closures, items, idOf) => closures.flatMap((response) => {
|
|
1801
|
+
const item = items.find((candidate) => idOf(candidate) === response.id);
|
|
1802
|
+
return item === void 0 ? [] : [[item, response]];
|
|
1410
1803
|
});
|
|
1804
|
+
var closedClaim = (item, response) => ({
|
|
1805
|
+
code: response.id,
|
|
1806
|
+
title: item.title,
|
|
1807
|
+
description: item.description,
|
|
1808
|
+
reasoning: item.reasoning,
|
|
1809
|
+
severity: item.severity,
|
|
1810
|
+
answerUrl: response.source_url,
|
|
1811
|
+
answerAuthor: response.author
|
|
1812
|
+
});
|
|
1813
|
+
var answeredRegistryFrom = (closures, prior) => closedPriorItems(closures, prior?.findings ?? [], resolveFindingId).map(
|
|
1814
|
+
([finding, response]) => ({
|
|
1815
|
+
...closedClaim(finding, response),
|
|
1816
|
+
path: finding.path,
|
|
1817
|
+
patch: finding.patch ?? null
|
|
1818
|
+
})
|
|
1819
|
+
);
|
|
1820
|
+
var answeredSystemicRegistryFrom = (closures, prior) => closedPriorItems(closures, prior?.systemic_problems ?? [], resolveSystemicId).map(
|
|
1821
|
+
([systemic, response]) => closedClaim(systemic, response)
|
|
1822
|
+
);
|
|
1411
1823
|
var isSynthesizedTitleMatch = (f, e) => isSynthesizedFindingId(e.code) && e.title === f.title;
|
|
1412
1824
|
var matches = (resolvedId, f, e) => e.code === resolvedId || isSynthesizedTitleMatch(f, e);
|
|
1413
|
-
var couldMatch = (code,
|
|
1414
|
-
var
|
|
1825
|
+
var couldMatch = (code, doc) => isSynthesizedFindingId(code) || doc.findings.some((f) => resolveFindingId(f) === code) || (doc.systemic_problems ?? []).some((s) => resolveSystemicId(s) === code);
|
|
1826
|
+
var CLAIM_FIELDS = ["title", "description", "reasoning", "severity"];
|
|
1827
|
+
var VERBATIM_FIELDS = [...CLAIM_FIELDS, "path", "patch"];
|
|
1415
1828
|
var verbatimFieldEqual = (f, e, field) => field === "patch" ? (f.patch ?? null) === e.patch : f[field] === e[field];
|
|
1416
|
-
var isVerbatimReRaise = (
|
|
1417
|
-
var
|
|
1829
|
+
var isVerbatimReRaise = (item, fields, fieldEqual) => !hasRebuttal(item) && fields.every(fieldEqual);
|
|
1830
|
+
var isVerbatimNonCritical = (item, fields, fieldEqual) => isVerbatimReRaise(item, fields, fieldEqual) && item.severity !== "critical";
|
|
1831
|
+
var isAnsweredDrop = (resolvedId, f, e) => matches(resolvedId, f, e) && isVerbatimNonCritical(f, VERBATIM_FIELDS, (field) => verbatimFieldEqual(f, e, field));
|
|
1418
1832
|
var verbatimMatchCount = (f, e) => VERBATIM_FIELDS.reduce((count, field) => count + (verbatimFieldEqual(f, e, field) ? 1 : 0), 0);
|
|
1419
1833
|
var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
|
|
1420
1834
|
var bestTitleMatch = (f, registry) => {
|
|
@@ -1466,34 +1880,61 @@ var applyAnswered = (findings, registry) => {
|
|
|
1466
1880
|
droppedCount
|
|
1467
1881
|
};
|
|
1468
1882
|
};
|
|
1469
|
-
var
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1883
|
+
var answeredSystemicNoteKey = (s) => `systemic:${resolveSystemicId(s) ?? ""}`;
|
|
1884
|
+
var systemicVerdict = (systemic, registry) => {
|
|
1885
|
+
const resolvedId = resolveSystemicId(systemic);
|
|
1886
|
+
const entry = registry.find((e) => e.code === resolvedId);
|
|
1887
|
+
return entry === void 0 ? { kind: "unanswered", systemic } : {
|
|
1888
|
+
kind: isVerbatimNonCritical(
|
|
1889
|
+
systemic,
|
|
1890
|
+
CLAIM_FIELDS,
|
|
1891
|
+
(field) => systemic[field] === entry[field]
|
|
1892
|
+
) ? "dropped" : "annotated",
|
|
1893
|
+
systemic,
|
|
1894
|
+
entry
|
|
1895
|
+
};
|
|
1479
1896
|
};
|
|
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) ?? ""}`;
|
|
1897
|
+
var applyAnsweredSystemic = (systemic, registry) => {
|
|
1898
|
+
const verdicts = systemic.map((s) => systemicVerdict(s, registry));
|
|
1899
|
+
const dropped = verdicts.flatMap((v) => v.kind === "dropped" ? [v.entry] : []);
|
|
1900
|
+
return {
|
|
1901
|
+
systemic: verdicts.flatMap((v) => v.kind === "dropped" ? [] : [v.systemic]),
|
|
1902
|
+
reRaisedNotes: Object.fromEntries(
|
|
1903
|
+
verdicts.flatMap(
|
|
1904
|
+
(v) => v.kind === "annotated" ? [[answeredSystemicNoteKey(v.systemic), answeredNote(v.entry)]] : []
|
|
1905
|
+
)
|
|
1906
|
+
),
|
|
1907
|
+
verbatimReRaised: [...new Map(dropped.map((e) => [e.code, e])).values()],
|
|
1908
|
+
droppedCount: dropped.length
|
|
1909
|
+
};
|
|
1495
1910
|
};
|
|
1496
|
-
var
|
|
1911
|
+
var applyOverruled = (items, rulings, idOf, noteKey) => {
|
|
1912
|
+
const ruled = items.map((item) => ({ item, ruling: rulings.get(idOf(item) ?? "") }));
|
|
1913
|
+
const drops = (entry) => entry.ruling !== void 0 && entry.item.severity !== "critical";
|
|
1914
|
+
const dropped = ruled.filter(drops).flatMap(({ ruling }) => ruling === void 0 ? [] : [ruling]);
|
|
1915
|
+
return {
|
|
1916
|
+
kept: ruled.filter((entry) => !drops(entry)).map(({ item }) => item),
|
|
1917
|
+
reRaisedNotes: Object.fromEntries(
|
|
1918
|
+
ruled.flatMap(
|
|
1919
|
+
({ item, ruling }) => ruling !== void 0 && !drops({ item, ruling }) ? [[noteKey(item), answeredNote(ruling)]] : []
|
|
1920
|
+
)
|
|
1921
|
+
),
|
|
1922
|
+
verbatimReRaised: [...new Map(dropped.map((ruling) => [ruling.code, ruling])).values()],
|
|
1923
|
+
droppedCount: dropped.length
|
|
1924
|
+
};
|
|
1925
|
+
};
|
|
1926
|
+
var closingAnswerLines = (entries, linkText) => entries.map((e) => {
|
|
1927
|
+
const label = e.code !== "" ? `\`${escapeCodeBackticks(e.code)}\`` : `\u201C${escapeCodeBackticks(e.title)}\u201D`;
|
|
1928
|
+
return `> - ${label} \u2014 [${linkText}](${linkSafeUrl(e.answerUrl)})${byAuthor(e)}`;
|
|
1929
|
+
});
|
|
1930
|
+
var answeredReRaiseNote = (entries, count) => entries.length === 0 ? "" : [
|
|
1931
|
+
`> \u21A9\uFE0F **${String(count)} re-raise(s) without new evidence \u2014 treated as answered** (a maintainer's answer closed each):`,
|
|
1932
|
+
...closingAnswerLines(entries, "prior answer")
|
|
1933
|
+
].join("\n");
|
|
1934
|
+
var overruledReRaiseNote = (entries, count) => entries.length === 0 ? "" : [
|
|
1935
|
+
`> \u2696\uFE0F **${String(count)} re-raise(s) of an id a maintainer overruled \u2014 closed for good**:`,
|
|
1936
|
+
...closingAnswerLines(entries, "the ruling")
|
|
1937
|
+
].join("\n");
|
|
1497
1938
|
|
|
1498
1939
|
// src/render.ts
|
|
1499
1940
|
var escapePipes = (text) => text.replace(/\|/g, "\\|");
|
|
@@ -1587,7 +2028,7 @@ var sanitizeFinding = (f, answeredNotes, discussionByFinding, permalinkBase, una
|
|
|
1587
2028
|
rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
|
|
1588
2029
|
...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
|
|
1589
2030
|
patchProjection: projectPatch(f.patch, "comment-body"),
|
|
1590
|
-
answeredNote:
|
|
2031
|
+
answeredNote: noteFor(answeredNotes, key2),
|
|
1591
2032
|
discussion: discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(discussionByFinding, f.id) ? discussionByFinding[f.id] ?? [] : [],
|
|
1592
2033
|
...discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(discussionTruncated, f.id) ? { discussionTruncated: discussionTruncated[f.id] } : {}
|
|
1593
2034
|
};
|
|
@@ -1617,9 +2058,10 @@ var carriedLines = (f) => [
|
|
|
1617
2058
|
`reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
|
|
1618
2059
|
...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
|
|
1619
2060
|
].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
|
|
1620
|
-
var sanitizeSystemic = (s, discussion, discussionTruncated) => ({
|
|
2061
|
+
var sanitizeSystemic = (s, answeredNotes, discussion, discussionTruncated) => ({
|
|
1621
2062
|
...s,
|
|
1622
2063
|
title: escapePipes(s.title),
|
|
2064
|
+
answeredNote: noteFor(answeredNotes, answeredSystemicNoteKey(s)),
|
|
1623
2065
|
discussion,
|
|
1624
2066
|
...discussionTruncated !== void 0 ? { discussionTruncated } : {},
|
|
1625
2067
|
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncated, "") : "",
|
|
@@ -1671,6 +2113,7 @@ var render = (input) => {
|
|
|
1671
2113
|
const trajectory = convergence?.rounds ?? input.rounds ?? [];
|
|
1672
2114
|
const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
|
|
1673
2115
|
const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
|
|
2116
|
+
const contestedSuffix = (input.contested ?? []).length > 0 ? ` \xB7 ${String((input.contested ?? []).length)} contested` : "";
|
|
1674
2117
|
const advisoryAllowed = isFullReviewRound;
|
|
1675
2118
|
const discussionLinksFor = (id) => input.discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionByFinding, id) ? input.discussionByFinding[id] ?? [] : [];
|
|
1676
2119
|
const discussionTruncatedFor = (id) => input.discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionTruncated, id) ? input.discussionTruncated[id] : void 0;
|
|
@@ -1750,6 +2193,7 @@ var render = (input) => {
|
|
|
1750
2193
|
(input.findings.systemic_problems ?? []).map(
|
|
1751
2194
|
(s) => sanitizeSystemic(
|
|
1752
2195
|
s,
|
|
2196
|
+
input.answeredNotes,
|
|
1753
2197
|
s.id !== void 0 ? discussionLinksFor(s.id) : [],
|
|
1754
2198
|
s.id !== void 0 ? discussionTruncatedFor(s.id) : void 0
|
|
1755
2199
|
)
|
|
@@ -1758,6 +2202,23 @@ var render = (input) => {
|
|
|
1758
2202
|
(s) => s.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + s.discussionHtml.length : 0,
|
|
1759
2203
|
(s) => ({ ...s, discussion: [], discussionHtml: "" })
|
|
1760
2204
|
);
|
|
2205
|
+
const contestedBudget = budgetBySize(
|
|
2206
|
+
(input.contested ?? []).map((view) => {
|
|
2207
|
+
const discussion = discussionLinksFor(view.id);
|
|
2208
|
+
return {
|
|
2209
|
+
...view,
|
|
2210
|
+
...view.reRaise?.location !== void 0 ? { reRaise: { ...view.reRaise, location: escapeCodeBackticks(view.reRaise.location) } } : {},
|
|
2211
|
+
id: escapeCodeBackticks(view.id),
|
|
2212
|
+
title: escapePipes(view.title),
|
|
2213
|
+
answerUrl: linkSafeUrl(view.answerUrl),
|
|
2214
|
+
discussion,
|
|
2215
|
+
discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncatedFor(view.id), "") : ""
|
|
2216
|
+
};
|
|
2217
|
+
}),
|
|
2218
|
+
DISCUSSION_TOTAL_CHARS - orphanedBudget.used - discussionBudget.used - systemicBudget.used,
|
|
2219
|
+
(view) => view.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + view.discussionHtml.length : 0,
|
|
2220
|
+
(view) => ({ ...view, discussion: [], discussionHtml: "" })
|
|
2221
|
+
);
|
|
1761
2222
|
const orphanedRender = {
|
|
1762
2223
|
lines: orphanedBudget.kept.map(([, entry]) => entry.html),
|
|
1763
2224
|
// The "(showing N of M)" total counts DISTINCT DISPLAYED entries — the pre-budget fold above
|
|
@@ -1784,6 +2245,7 @@ var render = (input) => {
|
|
|
1784
2245
|
summaryAvailable: input.summaryAvailable ?? false,
|
|
1785
2246
|
responseForm: RESPONSE_FORM,
|
|
1786
2247
|
responseTeaching: RESPONSE_TEACHING,
|
|
2248
|
+
rulingTeaching: RULING_TEACHING,
|
|
1787
2249
|
teachesResponses: isConvergenceRound(route, incomplete),
|
|
1788
2250
|
shedNote: shedNote(
|
|
1789
2251
|
input.shedCount ?? 0,
|
|
@@ -1800,7 +2262,7 @@ var render = (input) => {
|
|
|
1800
2262
|
route: route ?? void 0,
|
|
1801
2263
|
postedAt: input.postedAt ?? "",
|
|
1802
2264
|
roundsSummary: roundsSummary(trajectory, input.roundCount),
|
|
1803
|
-
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : "",
|
|
2265
|
+
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) + contestedSuffix : "",
|
|
1804
2266
|
inlinePosted: input.inlineDisposition?.kind === "posted" ? input.inlineDisposition.count : 0,
|
|
1805
2267
|
runUrl: input.runUrl,
|
|
1806
2268
|
summaryAvailable: input.summaryAvailable ?? false,
|
|
@@ -1827,15 +2289,21 @@ var render = (input) => {
|
|
|
1827
2289
|
reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
|
|
1828
2290
|
postedAt: input.postedAt ?? "",
|
|
1829
2291
|
severityCounts,
|
|
1830
|
-
convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
|
|
2292
|
+
convergenceSummary: !isFullReviewRound ? "" : (convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold)) + contestedSuffix,
|
|
1831
2293
|
strays: discussionBudget.kept,
|
|
1832
2294
|
orphanedLines: orphanedRender.lines,
|
|
1833
2295
|
orphanedUnresolvable: input.orphanedUnresolvable === true,
|
|
1834
|
-
discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + orphanedBudget.droppedItems.length,
|
|
1835
|
-
discussionDroppedIds:
|
|
2296
|
+
discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + contestedBudget.droppedItems.length + orphanedBudget.droppedItems.length,
|
|
2297
|
+
discussionDroppedIds: [
|
|
2298
|
+
...discussionBudget.droppedItems.map((v) => clipText(escapeCodeBackticks(v.idKey), 64)),
|
|
2299
|
+
...contestedBudget.droppedItems.map((v) => clipText(v.id, 64))
|
|
2300
|
+
].slice(0, DROPPED_IDS_SHOWN),
|
|
1836
2301
|
discussionDroppedSystemicIds: systemicBudget.droppedItems.flatMap((s) => s.id !== void 0 ? [clipText(s.id, 64)] : []).slice(0, DROPPED_IDS_SHOWN),
|
|
1837
2302
|
discussionDroppedOrphanedIds: orphanedBudget.droppedItems.map(([token]) => clipText(token, 64)).slice(0, DROPPED_IDS_SHOWN),
|
|
1838
|
-
discussionDroppedExtra: Math.max(
|
|
2303
|
+
discussionDroppedExtra: Math.max(
|
|
2304
|
+
0,
|
|
2305
|
+
discussionBudget.droppedItems.length + contestedBudget.droppedItems.length - DROPPED_IDS_SHOWN
|
|
2306
|
+
) + Math.max(0, systemicBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, orphanedBudget.droppedItems.length - DROPPED_IDS_SHOWN),
|
|
1839
2307
|
discussionCap: PER_FINDING_LINKS,
|
|
1840
2308
|
orphanedTotal: orphanedRender.total,
|
|
1841
2309
|
suppressedNits: suppressedBudget.kept,
|
|
@@ -1857,6 +2325,7 @@ var render = (input) => {
|
|
|
1857
2325
|
sameRootNotes: advisoryAllowed ? sameRootNotes : {},
|
|
1858
2326
|
answeredNotes: input.answeredNotes ?? {},
|
|
1859
2327
|
answeredReRaiseNote: input.answeredReRaiseNote ?? "",
|
|
2328
|
+
contested: contestedBudget.kept,
|
|
1860
2329
|
reviewUrl: input.reviewUrl ?? null,
|
|
1861
2330
|
formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
|
|
1862
2331
|
// N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
|
|
@@ -1957,7 +2426,6 @@ var buildInlineComments = (findings, diff, context) => {
|
|
|
1957
2426
|
const { inDiff, strays } = context.partition ?? partitionFindings(findings, indexDiff(diff));
|
|
1958
2427
|
const eta = new Eta({ autoTrim: false });
|
|
1959
2428
|
const modelsText = formatModels(models);
|
|
1960
|
-
const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
|
|
1961
2429
|
const comments = inDiff.map((f) => {
|
|
1962
2430
|
const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
|
|
1963
2431
|
const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
|
|
@@ -2067,12 +2535,12 @@ var sumTranscriptUsage = (entries) => {
|
|
|
2067
2535
|
},
|
|
2068
2536
|
{ totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
|
|
2069
2537
|
);
|
|
2070
|
-
const models = [...summed.totals].map(([model,
|
|
2538
|
+
const models = [...summed.totals].map(([model, t10]) => ({
|
|
2071
2539
|
model,
|
|
2072
|
-
input_tokens:
|
|
2073
|
-
output_tokens:
|
|
2074
|
-
cache_read_tokens:
|
|
2075
|
-
cache_write_tokens:
|
|
2540
|
+
input_tokens: t10.input,
|
|
2541
|
+
output_tokens: t10.output,
|
|
2542
|
+
cache_read_tokens: t10.cacheRead,
|
|
2543
|
+
cache_write_tokens: t10.cacheWrite
|
|
2076
2544
|
}));
|
|
2077
2545
|
const bounds = entries.reduce(
|
|
2078
2546
|
(acc, entry) => {
|
|
@@ -2297,7 +2765,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
|
|
|
2297
2765
|
const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
|
|
2298
2766
|
if (WRITE_TOOLS.has(toolName)) {
|
|
2299
2767
|
const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
|
|
2300
|
-
if (typeof fp === "string" && targets.some((
|
|
2768
|
+
if (typeof fp === "string" && targets.some((t10) => fp === t10 || basename(fp) === basename(t10)))
|
|
2301
2769
|
return true;
|
|
2302
2770
|
}
|
|
2303
2771
|
if (toolName === "Bash") {
|
|
@@ -2402,9 +2870,9 @@ var parseWallMs = (raw) => {
|
|
|
2402
2870
|
};
|
|
2403
2871
|
var parseEpochSecMs = (raw) => {
|
|
2404
2872
|
if (raw === void 0) return null;
|
|
2405
|
-
const
|
|
2406
|
-
if (!/^\d+$/.test(
|
|
2407
|
-
const n = Number.parseInt(
|
|
2873
|
+
const t10 = raw.trim();
|
|
2874
|
+
if (!/^\d+$/.test(t10)) return null;
|
|
2875
|
+
const n = Number.parseInt(t10, 10);
|
|
2408
2876
|
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
2409
2877
|
};
|
|
2410
2878
|
var anchoredElapsedMs = (src) => {
|
|
@@ -2817,220 +3285,6 @@ var checkRun = async (input, ghApi = runGhApi) => {
|
|
|
2817
3285
|
];
|
|
2818
3286
|
await ghApi(endpoint, JSON.stringify(body));
|
|
2819
3287
|
};
|
|
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
3288
|
|
|
3035
3289
|
// src/post.ts
|
|
3036
3290
|
var STICKY_CHAR_LIMIT = 65536;
|
|
@@ -3551,8 +3805,8 @@ var priorBotCommentIds = (raw, botLogin) => {
|
|
|
3551
3805
|
const nodes = conn?.nodes;
|
|
3552
3806
|
if (!Array.isArray(nodes)) return { ids: [], truncated };
|
|
3553
3807
|
const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
|
|
3554
|
-
const ids = nodes.flatMap((
|
|
3555
|
-
const cnodes =
|
|
3808
|
+
const ids = nodes.flatMap((t10) => {
|
|
3809
|
+
const cnodes = t10.comments?.nodes;
|
|
3556
3810
|
return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
|
|
3557
3811
|
});
|
|
3558
3812
|
return { ids, truncated };
|
|
@@ -3648,7 +3902,7 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3648
3902
|
convergence: conv ?? void 0
|
|
3649
3903
|
});
|
|
3650
3904
|
let warnedNoJsonUrl = false;
|
|
3651
|
-
const
|
|
3905
|
+
const findingsPair = (doc) => {
|
|
3652
3906
|
if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
|
|
3653
3907
|
const carriedLink = existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
|
|
3654
3908
|
if (carriedLink !== null) {
|
|
@@ -3662,6 +3916,8 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3662
3916
|
}
|
|
3663
3917
|
return findingsMarkerPair(void 0, doc.convergence);
|
|
3664
3918
|
};
|
|
3919
|
+
const carriedDialogue = existingSticky !== null ? carriedDialogueMarker(existingSticky.body) : "";
|
|
3920
|
+
const findingsBlob = (doc, dialogue2 = carriedDialogue) => [findingsPair(doc), dialogue2].filter((marker) => marker !== "").join("\n");
|
|
3665
3921
|
const leaveInPlace = (message) => {
|
|
3666
3922
|
process.stderr.write(
|
|
3667
3923
|
message ?? "Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place\n"
|
|
@@ -3670,9 +3926,9 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
|
|
|
3670
3926
|
process.exit(0);
|
|
3671
3927
|
};
|
|
3672
3928
|
const logAnsweredDrops = () => {
|
|
3673
|
-
if (
|
|
3929
|
+
if (droppedCount > 0) {
|
|
3674
3930
|
process.stderr.write(
|
|
3675
|
-
`${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each
|
|
3931
|
+
`${String(droppedCount)} verbatim re-raise(s) of answered findings or systemic problems were treated as answered \u2014 the preserved sticky shows each one
|
|
3676
3932
|
`
|
|
3677
3933
|
);
|
|
3678
3934
|
}
|
|
@@ -3750,7 +4006,7 @@ ${dropNote}` : ""}`,
|
|
|
3750
4006
|
const carried = existingSticky !== null ? carriedFindingsMarker(existingSticky.body) : null;
|
|
3751
4007
|
if (carried === null) return findingsBlob(doc);
|
|
3752
4008
|
const provenance = existingSticky !== null ? carriedAncestryMarkers(existingSticky.body) : "";
|
|
3753
|
-
return [carriedMarkerPointer(carried, doc.convergence), provenance].filter((p) => p !== "").join("\n\n");
|
|
4009
|
+
return [carriedMarkerPointer(carried, doc.convergence), carriedDialogue, provenance].filter((p) => p !== "").join("\n\n");
|
|
3754
4010
|
};
|
|
3755
4011
|
return formatMarkdown(
|
|
3756
4012
|
render({
|
|
@@ -3797,7 +4053,18 @@ ${dropNote}` : ""}`,
|
|
|
3797
4053
|
process.exit(0);
|
|
3798
4054
|
}
|
|
3799
4055
|
const loadedFindings = findingsResult.findings;
|
|
3800
|
-
const
|
|
4056
|
+
const priorDialogue = existingSticky !== null && input.withoutDialogue !== true ? parseDialogueMarker(existingSticky.body) : { entries: [], undecoded: [], skipped: 0 };
|
|
4057
|
+
if (priorDialogue.skipped > 0) {
|
|
4058
|
+
process.stderr.write(
|
|
4059
|
+
`Warning: ${String(priorDialogue.skipped)} entry(ies) of the prior sticky's dialogue state did not decode \u2014 carried unread
|
|
4060
|
+
`
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
const dialogueRound = input.withoutDialogue !== true && envelope !== null && isConvergenceRound(
|
|
4064
|
+
effectiveRoute,
|
|
4065
|
+
envelope.incomplete === true || isIncompleteFindings(loadedFindings)
|
|
4066
|
+
) && isReviewVerdict(loadedFindings.verdict);
|
|
4067
|
+
const answerable = input.withoutDialogue !== true && existingSticky !== null && !priorIsMechanic && (loadedFindings.findings.length > 0 || (loadedFindings.systemic_problems ?? []).length > 0 || dialogueRound && priorDialogue.entries.length > 0);
|
|
3801
4068
|
const headInBaseRepo = answerable && resolution.headRepo !== null && resolution.headRepo.toLowerCase() === input.repo.toLowerCase();
|
|
3802
4069
|
const answers = answerable ? answersFrom({
|
|
3803
4070
|
repo: input.repo,
|
|
@@ -3812,10 +4079,46 @@ ${dropNote}` : ""}`,
|
|
|
3812
4079
|
})),
|
|
3813
4080
|
commits: headInBaseRepo ? await fetchCommitAnswersSource(input.repo, prNumber, ghApi) : []
|
|
3814
4081
|
}) : { comments: [], commits: [] };
|
|
3815
|
-
const
|
|
4082
|
+
const isTrusted = (response) => isTrustedResponse(response, headInBaseRepo);
|
|
4083
|
+
const allAnswers = withRulingsOn(
|
|
3816
4084
|
[...answers.comments, ...answers.commits],
|
|
3817
|
-
|
|
3818
|
-
|
|
4085
|
+
new Set(
|
|
4086
|
+
priorDialogue.entries.filter((entry) => entry.state !== "rebutted").map((entry) => entry.id)
|
|
4087
|
+
),
|
|
4088
|
+
isTrusted
|
|
4089
|
+
);
|
|
4090
|
+
const unorderedRulings = allAnswers.filter(
|
|
4091
|
+
(answer) => RulingCodec.is(answer.disposition) && Number.isNaN(Date.parse(answer.created_at ?? ""))
|
|
4092
|
+
).length;
|
|
4093
|
+
if (unorderedRulings > 0) {
|
|
4094
|
+
process.stderr.write(
|
|
4095
|
+
`Warning: ${String(unorderedRulings)} maintainer ruling(s) carry no readable time, so they cannot be ordered against the other answers \u2014 not applied
|
|
4096
|
+
`
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
const allClosures = closingResponses(allAnswers, isTrusted);
|
|
4100
|
+
const dialogue = answerable && dialogueRound ? advanceDialogue(priorDialogue.entries, dialogueAnswers(allAnswers, isTrusted), [
|
|
4101
|
+
...loadedFindings.findings.map((f) => ({
|
|
4102
|
+
id: resolveFindingId(f),
|
|
4103
|
+
title: f.title,
|
|
4104
|
+
severity: f.severity,
|
|
4105
|
+
rebutted: hasRebuttal(f)
|
|
4106
|
+
})),
|
|
4107
|
+
...(loadedFindings.systemic_problems ?? []).flatMap((s) => {
|
|
4108
|
+
const id = resolveSystemicId(s);
|
|
4109
|
+
return id === void 0 ? [] : [{ id, title: s.title, severity: s.severity, rebutted: hasRebuttal(s) }];
|
|
4110
|
+
})
|
|
4111
|
+
]) : priorDialogue.entries;
|
|
4112
|
+
const upheldIds = new Set(
|
|
4113
|
+
dialogue.filter((entry) => entry.state === "upheld").map((entry) => entry.id)
|
|
4114
|
+
);
|
|
4115
|
+
const closures = allClosures.filter(
|
|
4116
|
+
(closure) => couldMatch(closure.id, loadedFindings) && !upheldIds.has(closure.id)
|
|
4117
|
+
);
|
|
4118
|
+
const contested = dialogueRound ? dialogue.filter((entry) => entry.state === "contested") : [];
|
|
4119
|
+
const contestedIds = new Set(contested.map((entry) => entry.id));
|
|
4120
|
+
const isContestedFinding = findingIdIn(contestedIds);
|
|
4121
|
+
const isContestedSystemic = systemicIdIn(contestedIds);
|
|
3819
4122
|
const priorForAnswers = closures.length > 0 && existingSticky !== null ? { value: await resolvePriorFindings(existingSticky.body, readArtifact) } : null;
|
|
3820
4123
|
const answeredPrior = priorForAnswers === null ? null : resolveTolerantFindings(priorForAnswers.value);
|
|
3821
4124
|
if (priorForAnswers !== null && answeredPrior === null) {
|
|
@@ -3825,37 +4128,101 @@ ${dropNote}` : ""}`,
|
|
|
3825
4128
|
);
|
|
3826
4129
|
}
|
|
3827
4130
|
const answeredRegistry = answeredRegistryFrom(closures, answeredPrior);
|
|
3828
|
-
|
|
4131
|
+
const answeredSystemicRegistry = answeredSystemicRegistryFrom(closures, answeredPrior);
|
|
4132
|
+
const boundIds = new Set([...answeredRegistry, ...answeredSystemicRegistry].map((e) => e.code));
|
|
4133
|
+
const unboundClosureCount = answeredPrior === null ? 0 : closures.filter((closure) => !boundIds.has(closure.id)).length;
|
|
4134
|
+
if (unboundClosureCount > 0) {
|
|
3829
4135
|
process.stderr.write(
|
|
3830
|
-
`Warning: ${String(
|
|
4136
|
+
`Warning: ${String(unboundClosureCount)} maintainer answer(s) name nothing the prior review reported \u2014 they close nothing this round
|
|
3831
4137
|
`
|
|
3832
4138
|
);
|
|
3833
4139
|
}
|
|
3834
|
-
const
|
|
3835
|
-
|
|
4140
|
+
const rulings = new Map(
|
|
4141
|
+
dialogue.filter((entry) => entry.state === "overruled").map((entry) => [
|
|
4142
|
+
entry.id,
|
|
4143
|
+
{
|
|
4144
|
+
code: entry.id,
|
|
4145
|
+
title: entry.title,
|
|
4146
|
+
answerUrl: entry.answer,
|
|
4147
|
+
answerAuthor: allAnswers.find((answer) => answer.source_url === entry.answer)?.author ?? null
|
|
4148
|
+
}
|
|
4149
|
+
])
|
|
4150
|
+
);
|
|
4151
|
+
const overruledFilter = applyOverruled(
|
|
4152
|
+
loadedFindings.findings.filter((f) => !isContestedFinding(f)),
|
|
4153
|
+
rulings,
|
|
4154
|
+
resolveFindingId,
|
|
4155
|
+
answeredNoteKey
|
|
4156
|
+
);
|
|
4157
|
+
const overruledSystemicFilter = applyOverruled(
|
|
4158
|
+
(loadedFindings.systemic_problems ?? []).filter((s) => !isContestedSystemic(s)),
|
|
4159
|
+
rulings,
|
|
4160
|
+
resolveSystemicId,
|
|
4161
|
+
answeredSystemicNoteKey
|
|
4162
|
+
);
|
|
4163
|
+
const answeredFilter = applyAnswered(overruledFilter.kept, answeredRegistry);
|
|
4164
|
+
const answeredSystemicFilter = applyAnsweredSystemic(
|
|
4165
|
+
overruledSystemicFilter.kept,
|
|
4166
|
+
answeredSystemicRegistry
|
|
4167
|
+
);
|
|
4168
|
+
const contestedFindings = loadedFindings.findings.filter(isContestedFinding);
|
|
4169
|
+
const contestedSystemic = (loadedFindings.systemic_problems ?? []).filter(isContestedSystemic);
|
|
4170
|
+
const contestedReRaises = new Map([
|
|
4171
|
+
...contestedSystemic.map((s) => [resolveSystemicId(s) ?? "", s]),
|
|
4172
|
+
...contestedFindings.map((f) => [resolveFindingId(f), f])
|
|
4173
|
+
]);
|
|
4174
|
+
const contestedViews = contested.map((entry) => {
|
|
4175
|
+
const reRaise = contestedReRaises.get(entry.id);
|
|
4176
|
+
return {
|
|
4177
|
+
id: entry.id,
|
|
4178
|
+
title: entry.title,
|
|
4179
|
+
severity: entry.severity,
|
|
4180
|
+
answerUrl: entry.answer,
|
|
4181
|
+
...reRaise === void 0 ? {} : {
|
|
4182
|
+
reRaise: {
|
|
4183
|
+
..."path" in reRaise ? {
|
|
4184
|
+
location: `${reRaise.path}:${lineRange(reRaise.start_line, reRaise.end_line, "\u2013")}`
|
|
4185
|
+
} : {},
|
|
4186
|
+
description: reRaise.description,
|
|
4187
|
+
reasoning: reRaise.reasoning,
|
|
4188
|
+
...hasRebuttal(reRaise) ? { rebuttal: reRaise.rebuttal ?? "" } : {}
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
};
|
|
4192
|
+
});
|
|
4193
|
+
const reRaisedNotes = {
|
|
4194
|
+
...answeredFilter.reRaisedNotes,
|
|
4195
|
+
...answeredSystemicFilter.reRaisedNotes,
|
|
4196
|
+
...overruledFilter.reRaisedNotes,
|
|
4197
|
+
...overruledSystemicFilter.reRaisedNotes
|
|
4198
|
+
};
|
|
3836
4199
|
const verbatimReRaised = answeredFilter.verbatimReRaised;
|
|
3837
|
-
const
|
|
4200
|
+
const answeredDropCount = answeredFilter.droppedCount + answeredSystemicFilter.droppedCount;
|
|
4201
|
+
const overruledDropCount = overruledFilter.droppedCount + overruledSystemicFilter.droppedCount;
|
|
4202
|
+
const droppedCount = answeredDropCount + overruledDropCount;
|
|
3838
4203
|
const droppedIds = /* @__PURE__ */ new Set([
|
|
3839
|
-
...verbatimReRaised.flatMap(
|
|
4204
|
+
...[...verbatimReRaised, ...overruledFilter.verbatimReRaised].flatMap(
|
|
4205
|
+
(e) => e.code !== "" ? [e.code] : []
|
|
4206
|
+
),
|
|
3840
4207
|
...answeredFilter.droppedFindingIds.filter((id) => id !== "")
|
|
3841
4208
|
]);
|
|
3842
|
-
const keptCodes = new Set(answeredFilter.findings.map((f) => f.id));
|
|
4209
|
+
const keptCodes = new Set([...answeredFilter.findings, ...contestedFindings].map((f) => f.id));
|
|
3843
4210
|
const trulyDropped = new Set([...droppedIds].filter((c) => !keptCodes.has(c)));
|
|
3844
|
-
const systemic = trulyDropped.size === 0 ?
|
|
4211
|
+
const systemic = trulyDropped.size === 0 ? answeredSystemicFilter.systemic : answeredSystemicFilter.systemic.map((s) => {
|
|
3845
4212
|
if (s.finding_ids === void 0) return s;
|
|
3846
4213
|
const codes = s.finding_ids.filter((c) => !trulyDropped.has(c));
|
|
3847
4214
|
return codes.length === s.finding_ids.length ? s : { ...s, finding_ids: codes };
|
|
3848
4215
|
});
|
|
3849
4216
|
const findings = {
|
|
3850
|
-
...loadedFindings,
|
|
3851
|
-
findings: [...answeredFilter.findings]
|
|
3852
|
-
...systemic.length > 0 ? { systemic_problems: systemic } : {}
|
|
4217
|
+
...withSystemicProblems(loadedFindings, [...systemic, ...contestedSystemic]),
|
|
4218
|
+
findings: [...answeredFilter.findings, ...contestedFindings]
|
|
3853
4219
|
};
|
|
3854
4220
|
const broadCurrentIds = [
|
|
3855
4221
|
...findings.findings.map((f) => f.id).filter((id) => id !== ""),
|
|
3856
4222
|
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
3857
4223
|
];
|
|
3858
|
-
const
|
|
4224
|
+
const argued = findings.findings.filter((f) => !isContestedFinding(f));
|
|
4225
|
+
const roundHasNit = argued.some((f) => f.severity === "nit");
|
|
3859
4226
|
const nitWantsPrior = roundHasNit && existingSticky !== null && (isFullReviewSticky(existingSticky.body) || isFullReviewAncestry(existingSticky.body));
|
|
3860
4227
|
const broadWantsPrior = existingSticky !== null && !priorIsMechanic && mentionsOutsideKnown(reachable, broadCurrentIds);
|
|
3861
4228
|
const resolvedPrior = priorForAnswers !== null ? priorForAnswers.value : existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
|
|
@@ -3869,15 +4236,25 @@ ${dropNote}` : ""}`,
|
|
|
3869
4236
|
// synthesized prior), the answeredNoteKey form (an empty-id prior), and the bare title: key
|
|
3870
4237
|
// (a pathless codeless prior).
|
|
3871
4238
|
priorSuppressedKeys.has(resolveFindingId(f)) || priorSuppressedKeys.has(answeredNoteKey(f)) || priorSuppressedKeys.has(`title:${f.title}`));
|
|
3872
|
-
const suppressedNits =
|
|
3873
|
-
const visibleFindings =
|
|
3874
|
-
const answeredDropNote =
|
|
4239
|
+
const suppressedNits = argued.filter(isSuppressedNit);
|
|
4240
|
+
const visibleFindings = argued.filter((f) => !isSuppressedNit(f));
|
|
4241
|
+
const answeredDropNote = [
|
|
4242
|
+
answeredReRaiseNote(
|
|
4243
|
+
[...verbatimReRaised, ...answeredSystemicFilter.verbatimReRaised],
|
|
4244
|
+
answeredDropCount
|
|
4245
|
+
),
|
|
4246
|
+
overruledReRaiseNote(
|
|
4247
|
+
[...overruledFilter.verbatimReRaised, ...overruledSystemicFilter.verbatimReRaised],
|
|
4248
|
+
overruledDropCount
|
|
4249
|
+
)
|
|
4250
|
+
].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
4251
|
const inlinePartition = input.inline === true && envelope !== null ? partitionFindings(visibleFindings, indexDiff(diff)) : null;
|
|
3876
4252
|
const straysForDiscussion = inlinePartition?.strays ?? visibleFindings;
|
|
3877
4253
|
const currentIds = [
|
|
3878
4254
|
...straysForDiscussion.map((f) => f.id).filter((id) => id !== ""),
|
|
3879
4255
|
...suppressedNits.map((f) => f.id).filter((id) => id !== ""),
|
|
3880
|
-
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
|
|
4256
|
+
...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== ""),
|
|
4257
|
+
...contested.map((entry) => entry.id)
|
|
3881
4258
|
];
|
|
3882
4259
|
const orphanResolveFailed = existingSticky !== null && broadWantsPrior && carriedFindingsMarker(existingSticky.body) !== null && resolvedPrior === null;
|
|
3883
4260
|
const broadLive = new Set(broadCurrentIds);
|
|
@@ -3974,6 +4351,7 @@ ${note}
|
|
|
3974
4351
|
nitVisibilityFloor: input.nitVisibilityFloor,
|
|
3975
4352
|
answeredNotes: reRaisedNotes,
|
|
3976
4353
|
answeredReRaiseNote: answeredDropNote,
|
|
4354
|
+
contested: contestedViews,
|
|
3977
4355
|
discussionByFinding,
|
|
3978
4356
|
orphanedDiscussion,
|
|
3979
4357
|
orphanedTotal,
|
|
@@ -4094,7 +4472,8 @@ ${note}
|
|
|
4094
4472
|
priorTraj,
|
|
4095
4473
|
roundNumber,
|
|
4096
4474
|
currentCodes,
|
|
4097
|
-
input.headSha.slice(0, 12)
|
|
4475
|
+
input.headSha.slice(0, 12),
|
|
4476
|
+
contested
|
|
4098
4477
|
) : effectiveRoute === "mechanic" ? (
|
|
4099
4478
|
// A mechanic pass means CI failed — whatever the carried prior says, its own stamp must
|
|
4100
4479
|
// never read "converged". The threshold-relative critical floor keeps the carried triple
|
|
@@ -4103,10 +4482,13 @@ ${note}
|
|
|
4103
4482
|
) : priorConv;
|
|
4104
4483
|
const stampedFindings = stampConvergence(findings, convergence);
|
|
4105
4484
|
const currentRoundCount = isRound ? roundNumber : priorRoundCount;
|
|
4106
|
-
const findingsMarker = findingsBlob(
|
|
4485
|
+
const findingsMarker = findingsBlob(
|
|
4486
|
+
stampedFindings,
|
|
4487
|
+
answerable && dialogueRound ? dialogueMarker(dialogue, priorDialogue.undecoded) : carriedDialogue
|
|
4488
|
+
);
|
|
4107
4489
|
const commonRenderInput = {
|
|
4108
4490
|
...sharedRenderInput,
|
|
4109
|
-
findings: stampedFindings,
|
|
4491
|
+
findings: withoutIds(stampedFindings, contestedIds),
|
|
4110
4492
|
envelope,
|
|
4111
4493
|
incomplete: thisIncomplete,
|
|
4112
4494
|
sameRootNotes,
|
|
@@ -4314,6 +4696,24 @@ ${note}
|
|
|
4314
4696
|
finalDiscussionOverride
|
|
4315
4697
|
)
|
|
4316
4698
|
);
|
|
4699
|
+
if (answerable && dialogueRound) {
|
|
4700
|
+
appendRunSummary(process.env["GITHUB_STEP_SUMMARY"], () => {
|
|
4701
|
+
const trustedAnswerIds = new Set(allAnswers.filter(isTrusted).map((answer) => answer.id));
|
|
4702
|
+
return dialogueMetricsTable({
|
|
4703
|
+
commentAnswers: answers.comments.length,
|
|
4704
|
+
commitAnswers: answers.commits.length,
|
|
4705
|
+
closures: allClosures.length,
|
|
4706
|
+
droppedAnswered: answeredDropCount,
|
|
4707
|
+
droppedOverruled: overruledDropCount,
|
|
4708
|
+
annotated: Object.keys(reRaisedNotes).length,
|
|
4709
|
+
entries: dialogue,
|
|
4710
|
+
systemicUnanswered: (findings.systemic_problems ?? []).filter((s) => {
|
|
4711
|
+
const id = resolveSystemicId(s);
|
|
4712
|
+
return id === void 0 || !trustedAnswerIds.has(id);
|
|
4713
|
+
}).length
|
|
4714
|
+
});
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
4317
4717
|
};
|
|
4318
4718
|
var noticeBody = (lead, existingBody) => {
|
|
4319
4719
|
const carried = existingBody ? carryForwardMarkers(existingBody) : "";
|
|
@@ -4999,15 +5399,29 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
|
|
|
4999
5399
|
join(input.outDir, "prior_findings.json"),
|
|
5000
5400
|
priorFindings === null ? "null" : JSON.stringify(priorFindings)
|
|
5001
5401
|
);
|
|
5402
|
+
const held = (seedsFromPrior ? parseDialogueMarker(prior.body).entries : []).filter(
|
|
5403
|
+
(entry) => entry.state !== "rebutted"
|
|
5404
|
+
);
|
|
5002
5405
|
const harvest = seedsFromPrior ? harvestResponses({
|
|
5003
5406
|
repo: input.repo,
|
|
5004
5407
|
prNumber,
|
|
5005
5408
|
botLogin: input.botLogin,
|
|
5006
|
-
priorIds: new Set(priorIdsFrom(priorFindings)),
|
|
5409
|
+
priorIds: /* @__PURE__ */ new Set([...priorIdsFrom(priorFindings), ...held.map((entry) => entry.id)]),
|
|
5007
5410
|
commits: commits.map((commit) => ({ ...commit, author: commit.login ?? null })),
|
|
5008
5411
|
comments: issueComments ?? []
|
|
5009
5412
|
}) : { file: { responses: [], unmatched: [] }, dropped: 0 };
|
|
5010
|
-
writeFileSync(
|
|
5413
|
+
writeFileSync(
|
|
5414
|
+
join(input.outDir, "responses.json"),
|
|
5415
|
+
JSON.stringify({
|
|
5416
|
+
...harvest.file,
|
|
5417
|
+
...Object.fromEntries(
|
|
5418
|
+
["contested", "upheld", "overruled"].flatMap((state) => {
|
|
5419
|
+
const claims = held.filter((entry) => entry.state === state).map(({ id, title, severity, at }) => ({ id, title, severity, since: at }));
|
|
5420
|
+
return claims.length > 0 ? [[state, claims]] : [];
|
|
5421
|
+
})
|
|
5422
|
+
)
|
|
5423
|
+
})
|
|
5424
|
+
);
|
|
5011
5425
|
if (seedsFromPrior && priorFindings === null) {
|
|
5012
5426
|
process.stderr.write(
|
|
5013
5427
|
"Note: the prior review's findings did not resolve \u2014 every id-shaped answer is staged as unmatched\n"
|
|
@@ -5382,7 +5796,7 @@ var parseScope = (raw) => {
|
|
|
5382
5796
|
reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
|
|
5383
5797
|
};
|
|
5384
5798
|
}
|
|
5385
|
-
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((
|
|
5799
|
+
const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t10) => t10 !== "")));
|
|
5386
5800
|
return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
|
|
5387
5801
|
};
|
|
5388
5802
|
|
|
@@ -6754,6 +7168,10 @@ var postCmd = defineCommand({
|
|
|
6754
7168
|
"unverified-no-logs": {
|
|
6755
7169
|
type: "boolean",
|
|
6756
7170
|
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"
|
|
7171
|
+
},
|
|
7172
|
+
"without-dialogue": {
|
|
7173
|
+
type: "boolean",
|
|
7174
|
+
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
7175
|
}
|
|
6758
7176
|
},
|
|
6759
7177
|
run: async ({ args }) => {
|
|
@@ -6788,6 +7206,7 @@ var postCmd = defineCommand({
|
|
|
6788
7206
|
nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
|
|
6789
7207
|
inline: args.inline,
|
|
6790
7208
|
unverifiedNoLogs: args["unverified-no-logs"],
|
|
7209
|
+
withoutDialogue: args["without-dialogue"],
|
|
6791
7210
|
postedAt: formatUtc(/* @__PURE__ */ new Date()),
|
|
6792
7211
|
pricedAt: /* @__PURE__ */ new Date()
|
|
6793
7212
|
});
|