@jphutchins/code-review 0.1.0-alpha.64 → 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 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';
@@ -109,7 +110,8 @@ var FindingIdRequired = t.type({ id: t.string });
109
110
  var FindingOptional = t.partial({
110
111
  side: SideCodec,
111
112
  recommendation: t.string,
112
- patch: t.string
113
+ patch: t.string,
114
+ rebuttal: t.string
113
115
  });
114
116
  var EndGeStart = (shape) => t.refinement(shape, (f) => f.end_line >= f.start_line, "EndGeStart");
115
117
  var FindingShape = t.intersection([
@@ -135,7 +137,8 @@ var SystemicRequired = t.type({
135
137
  var SystemicOptional = t.partial({
136
138
  id: t.string,
137
139
  finding_ids: t.array(t.string),
138
- paths: t.array(t.string)
140
+ paths: t.array(t.string),
141
+ rebuttal: t.string
139
142
  });
140
143
  var SystemicProblemShape = t.intersection([SystemicRequired, RuleUrlCodec, SystemicOptional]);
141
144
  var SystemicProblemCodec = strictExact("SystemicProblemStrict", SystemicProblemShape, [
@@ -245,11 +248,53 @@ var RECOVERABLE_OPTIONAL_FIELDS = /* @__PURE__ */ new Set([
245
248
  ...PIPELINE_STAMPED_FIELDS,
246
249
  "change_size"
247
250
  ]);
251
+ var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
252
+ var priorIdsFrom = (doc) => {
253
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
254
+ const rec = doc;
255
+ const idsOf = (field, systemic) => (Array.isArray(rec[field]) ? rec[field] : []).flatMap((raw) => {
256
+ const item = typeof raw === "object" && raw !== null ? asRecord(raw) : null;
257
+ if (item === null) return [];
258
+ const title = typeof item["title"] === "string" ? item["title"] : "";
259
+ const resolved = resolveRuleId({
260
+ ...typeof item["id"] === "string" ? { id: item["id"] } : {},
261
+ ...typeof item["code"] === "string" ? { code: item["code"] } : {},
262
+ ...typeof item["path"] === "string" ? { path: item["path"] } : {},
263
+ title
264
+ });
265
+ const id = resolved ?? (systemic && title !== "" ? synthesizedSystemicId(title) : void 0);
266
+ return id !== void 0 ? [id] : [];
267
+ });
268
+ return [...idsOf("findings", false), ...idsOf("systemic_problems", true)];
269
+ };
270
+ var hasRebuttal = (item) => (item.rebuttal ?? "").trim() !== "";
271
+ var withoutRebuttals = (doc) => ({
272
+ ...doc,
273
+ findings: doc.findings.map(({ rebuttal: _rebuttal, ...f }) => f),
274
+ ...doc.systemic_problems !== void 0 ? { systemic_problems: doc.systemic_problems.map(({ rebuttal: _rebuttal, ...s }) => s) } : {}
275
+ });
276
+ var withSystemicProblems = ({ systemic_problems: _replaced, ...doc }, systemic) => ({
277
+ ...doc,
278
+ ...systemic.length > 0 ? { systemic_problems: [...systemic] } : {}
279
+ });
248
280
  var synthesizedId = (parts, prefix) => `${prefix}${createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 12)}`;
249
281
  var synthesizedFindingId = (path, title) => synthesizedId([path, title], "f-");
250
282
  var synthesizedSystemicId = (title) => synthesizedId([title], "s-");
251
283
  var isSynthesizedFindingId = (id) => /^f-[A-Za-z0-9_-]{12}$/.test(id);
252
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
+ };
253
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;
254
299
  var usableCountsMap = (v) => {
255
300
  if (typeof v !== "object" || v === null || Array.isArray(v)) return void 0;
@@ -270,12 +315,18 @@ var LegacyRuleCodec = t.partial({
270
315
  id: t.string,
271
316
  code_url: UriString
272
317
  });
273
- var FindingShapeV09 = t.intersection([FindingCoreRequired, LegacyRuleCodec, FindingOptional]);
318
+ var FindingOptionalV09 = t.partial({
319
+ side: SideCodec,
320
+ recommendation: t.string,
321
+ patch: t.string
322
+ });
323
+ var FindingShapeV09 = t.intersection([FindingCoreRequired, LegacyRuleCodec, FindingOptionalV09]);
274
324
  var FindingCodecV09 = t.exact(EndGeStart(FindingShapeV09));
275
325
  var SystemicV09Optional = t.partial({
276
326
  finding_codes: t.array(t.string),
277
327
  finding_ids: t.array(t.string),
278
- paths: t.array(t.string)
328
+ paths: t.array(t.string),
329
+ rebuttal: t.unknown
279
330
  });
280
331
  var SystemicV09Shape = t.intersection([SystemicRequired, LegacyRuleCodec, SystemicV09Optional]);
281
332
  var SystemicProblemCodecV09 = strictExact("SystemicV09Strict", SystemicV09Shape, [
@@ -343,7 +394,7 @@ var normalizeV09 = (doc) => {
343
394
  id: resolveRuleId({ id, code, path: f.path, title: f.title }) ?? synthesizedFindingId(f.path, f.title)
344
395
  }));
345
396
  const systemic_problems = doc.systemic_problems?.map(
346
- ({ code, id, finding_codes, finding_ids, ...s }) => ({
397
+ ({ code, id, finding_codes, finding_ids, rebuttal: _rebuttal, ...s }) => ({
347
398
  ...s,
348
399
  id: resolveRuleId({ id, code, title: s.title }) ?? synthesizedSystemicId(s.title),
349
400
  ...finding_ids !== void 0 && finding_ids.length > 0 ? { finding_ids } : finding_codes !== void 0 ? { finding_ids: finding_codes } : {}
@@ -484,7 +535,7 @@ var TestSummaryCodec = t.intersection([
484
535
  failures: t.array(TestFailureCodec)
485
536
  })
486
537
  ]);
487
- var DEFAULT_SCHEMA_VERSION = "0.10.0";
538
+ var DEFAULT_SCHEMA_VERSION = "0.11.0";
488
539
  var incompleteFindings = (summary) => ({
489
540
  schema_version: DEFAULT_SCHEMA_VERSION,
490
541
  summary,
@@ -494,7 +545,7 @@ var incompleteFindings = (summary) => ({
494
545
  var isIncompleteFindings = (findings) => findings.verdict === "error" && findings.findings.length === 0;
495
546
 
496
547
  // src/released.ts
497
- var RELEASED = "2026-10-02";
548
+ var RELEASED = "2026-10-10";
498
549
 
499
550
  // src/cost.ts
500
551
  var defaultWarn = (message) => {
@@ -631,6 +682,369 @@ var warnStalePrices = (prices, warn = defaultWarn) => {
631
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`
632
683
  );
633
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");
634
1048
 
635
1049
  // src/patch.ts
636
1050
  var HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/;
@@ -758,6 +1172,7 @@ var decodeBase64Json = (b64) => {
758
1172
  return void 0;
759
1173
  }
760
1174
  };
1175
+ var noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
761
1176
  var findingsPointer = (jsonUrl) => encodeMarker(jsonUrl);
762
1177
  var warnedValveFindings = /* @__PURE__ */ new Set();
763
1178
  var INLINE_PROSE_CLIP_THRESHOLD_CHARS = 3e4;
@@ -824,6 +1239,8 @@ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.
824
1239
  var MAX_IDS_PER_ROUND = 8;
825
1240
  var hasId = (ids, code) => ids !== void 0 && Object.prototype.hasOwnProperty.call(ids, code);
826
1241
  var escapeCodeBackticks = (code) => code.replace(/`/g, "-").replace(/\r?\n/g, " ");
1242
+ var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
1243
+ var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
827
1244
  var normalizeIdCounts = (ids, priorCodes, preferredKeys, preValidated = false) => {
828
1245
  const entries = Object.entries((preValidated ? ids : usableCountsMap(ids)) ?? {});
829
1246
  if (entries.length === 0) return void 0;
@@ -987,8 +1404,15 @@ var convergenceScore = (doc, threshold) => round2(
987
1404
  0
988
1405
  )
989
1406
  );
990
- var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, ids = {}, sha) => {
991
- const score = convergenceScore(doc, threshold);
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);
992
1416
  const normalized = normalizeIdCounts(ids, priorRounds[priorRounds.length - 1]?.ids);
993
1417
  const current = {
994
1418
  round,
@@ -1119,6 +1543,13 @@ var findingsMarkerPair = (jsonUrl, convergence) => {
1119
1543
  return marker === "" ? conv : `${marker}
1120
1544
  ${conv}`;
1121
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
+ };
1122
1553
  var parseConvergenceMarker = (body) => {
1123
1554
  const b64 = CONVERGENCE_RE.exec(body)?.[1];
1124
1555
  return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
@@ -1175,11 +1606,20 @@ var carryForwardMarkers = (body) => {
1175
1606
  const findings = carriedFindingsMarker(body);
1176
1607
  const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
1177
1608
  const convergence = CONVERGENCE_RE.exec(body)?.[0];
1609
+ const dialogue = DIALOGUE_RE.exec(body)?.[0];
1178
1610
  const rounds = ROUNDS_RE.exec(body)?.[0];
1179
1611
  const signal = SIGNAL_RE.exec(body)?.[0];
1180
1612
  const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
1181
1613
  ${findings}` : void 0;
1182
- return [findingsBlock, reviewedSha, carriedProvenanceMarkers(body), convergence, rounds, signal].filter((m) => m !== void 0 && m !== "").join("\n\n");
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");
1183
1623
  };
1184
1624
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
1185
1625
  var projectPatch = (patch, surface) => {
@@ -1356,270 +1796,41 @@ var resolvePriorFindings = async (body, read) => {
1356
1796
  }
1357
1797
  };
1358
1798
 
1359
- // src/comment-chain.ts
1360
- function* ancestors(start, parentOf) {
1361
- const seen = /* @__PURE__ */ new Set();
1362
- let current = start;
1363
- while (current !== null && !seen.has(current)) {
1364
- yield current;
1365
- seen.add(current);
1366
- current = parentOf(current);
1367
- }
1368
- }
1369
- var numField = (rec, key2) => {
1370
- const v = rec[key2];
1371
- return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
1372
- };
1373
- var messageUsage = (entry) => {
1374
- const rec = asRecord(entry);
1375
- if (rec === null || rec["type"] !== "assistant") return null;
1376
- const msg = asRecord(rec["message"]);
1377
- if (msg === null) return null;
1378
- const model = msg["model"];
1379
- const usage = asRecord(msg["usage"]);
1380
- if (typeof model !== "string" || usage === null) return null;
1381
- const id = msg["id"];
1382
- return {
1383
- id: typeof id === "string" ? id : null,
1384
- model: modelIdentity(model),
1385
- input: numField(usage, "input_tokens"),
1386
- output: numField(usage, "output_tokens"),
1387
- cacheRead: numField(usage, "cache_read_input_tokens"),
1388
- cacheWrite: numField(usage, "cache_creation_input_tokens")
1389
- };
1390
- };
1391
- var tsMsOf = (entry) => {
1392
- const rec = asRecord(entry);
1393
- const ts = rec?.["timestamp"];
1394
- if (typeof ts !== "string") return null;
1395
- const ms = Date.parse(ts);
1396
- return Number.isNaN(ms) ? null : ms;
1397
- };
1398
- var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
1399
- var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
1400
- try {
1401
- return [JSON.parse(line)];
1402
- } catch {
1403
- return [];
1404
- }
1405
- });
1406
- var sumTranscriptUsage = (entries) => {
1407
- const summed = entries.reduce(
1408
- (acc, entry) => {
1409
- const u = messageUsage(entry);
1410
- if (u === null) return acc;
1411
- if (u.id !== null && acc.seen.has(u.id)) return acc;
1412
- if (u.id !== null) acc.seen.add(u.id);
1413
- const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
1414
- acc.totals.set(u.model, {
1415
- input: prev.input + u.input,
1416
- output: prev.output + u.output,
1417
- cacheRead: prev.cacheRead + u.cacheRead,
1418
- cacheWrite: prev.cacheWrite + u.cacheWrite
1419
- });
1420
- return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
1421
- },
1422
- { totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
1423
- );
1424
- const models = [...summed.totals].map(([model, t8]) => ({
1425
- model,
1426
- input_tokens: t8.input,
1427
- output_tokens: t8.output,
1428
- cache_read_tokens: t8.cacheRead,
1429
- cache_write_tokens: t8.cacheWrite
1430
- }));
1431
- const bounds = entries.reduce(
1432
- (acc, entry) => {
1433
- const ms = tsMsOf(entry);
1434
- if (ms === null) return acc;
1435
- return {
1436
- min: acc.min === null || ms < acc.min ? ms : acc.min,
1437
- max: acc.max === null || ms > acc.max ? ms : acc.max
1438
- };
1439
- },
1440
- { min: null, max: null }
1441
- );
1442
- return {
1443
- models,
1444
- turns: summed.turns,
1445
- durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
1446
- firstTsMs: bounds.min,
1447
- lastTsMs: bounds.max
1448
- };
1449
- };
1450
- var subagentFiles = (mainPath) => {
1451
- const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
1452
- try {
1453
- return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
1454
- } catch {
1455
- return [];
1456
- }
1457
- };
1458
- var readTranscriptTree = (mainPath) => {
1459
- const mainText = readFileOrNull(mainPath);
1460
- const mainEntries = mainText === null ? [] : parseJsonl(mainText);
1461
- const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
1462
- const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
1463
- const siblingReads = siblings.flatMap((path) => {
1464
- const text = readFileOrNull(path);
1465
- return text === null ? [] : [{ path, entries: parseJsonl(text) }];
1466
- });
1467
- return {
1468
- entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
1469
- files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
1470
- missing: mainText === null
1471
- };
1472
- };
1473
-
1474
1799
  // src/answered.ts
1475
- var THREAD_COMMENT_JQ = ".[] | {id, in_reply_to_id, user: {login: .user.login}, user_login: .user.login, user_type: .user.type, body, html_url, path, line, created_at, author_association}";
1476
- var ThreadCommentCodec = t.type({
1477
- id: t.number,
1478
- in_reply_to_id: t.union([t.number, t.null]),
1479
- user_login: t.string,
1480
- user_type: t.union([t.string, t.null]),
1481
- body: t.union([t.string, t.null]),
1482
- html_url: t.string,
1483
- path: t.union([t.string, t.null]),
1484
- line: t.union([t.number, t.null]),
1485
- created_at: t.union([t.string, t.null])
1486
- });
1487
- var AnsweredEntryCodec = t.type({
1488
- code: t.string,
1489
- title: t.string,
1490
- description: t.string,
1491
- reasoning: t.string,
1492
- severity: t.union([
1493
- t.literal("critical"),
1494
- t.literal("major"),
1495
- t.literal("minor"),
1496
- t.literal("nit")
1497
- ]),
1498
- path: t.string,
1499
- patch: t.union([t.string, t.null]),
1500
- replied_at: t.union([t.string, t.null]),
1501
- reply_id: t.number,
1502
- thread_url: t.string,
1503
- reply_url: t.string,
1504
- reply_author: t.string,
1505
- reply_excerpt: t.string
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]];
1506
1803
  });
1507
- var encodeAnsweredEntry = (e) => ({
1508
- code: e.code,
1509
- title: e.title,
1510
- description: e.description,
1511
- reasoning: e.reasoning,
1512
- severity: e.severity,
1513
- path: e.path,
1514
- patch: e.patch,
1515
- replied_at: e.repliedAt,
1516
- reply_id: e.replyId,
1517
- thread_url: e.threadUrl,
1518
- reply_url: e.replyUrl,
1519
- reply_author: e.replyAuthor,
1520
- reply_excerpt: e.replyExcerpt
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
1521
1812
  });
1522
- var decodeAnsweredEntry = (raw) => {
1523
- const decoded = AnsweredEntryCodec.decode(raw);
1524
- return decoded._tag === "Right" ? decoded.right : null;
1525
- };
1526
- var isHuman = (login, type8, botLogin) => login !== botLogin && type8 === "User";
1527
- var EXCERPT_LIMIT = 400;
1528
- var answeredRegistryFrom = (comments, botLogin) => {
1529
- const ordered = [...comments].sort(
1530
- (a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? "") || (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)
1531
- );
1532
- const byId = /* @__PURE__ */ new Map();
1533
- for (const c of ordered) byId.set(c.id, c);
1534
- const rootOf = (c) => {
1535
- const chain = [
1536
- ...ancestors(
1537
- c,
1538
- (n) => n.in_reply_to_id === null ? null : byId.get(n.in_reply_to_id) ?? null
1539
- )
1540
- ];
1541
- const root = chain[chain.length - 1];
1542
- return root !== void 0 && root.in_reply_to_id === null ? root : null;
1543
- };
1544
- const findingOf = (root) => {
1545
- const decoded = parseFindingsMarker(root.body ?? "");
1546
- const doc = typeof decoded === "object" && decoded !== null ? decoded.findings : void 0;
1547
- const first = Array.isArray(doc) ? doc[0] : void 0;
1548
- if (first === void 0) return null;
1549
- const title = first["title"];
1550
- const description = first["description"];
1551
- const reasoning = first["reasoning"];
1552
- const id = first["id"];
1553
- const legacyCode = first["code"];
1554
- const severity = first["severity"];
1555
- const path = first["path"];
1556
- const patch = first["patch"];
1557
- return typeof title === "string" && typeof description === "string" && typeof reasoning === "string" && typeof path === "string" && (severity === "critical" || severity === "major" || severity === "minor" || severity === "nit") ? {
1558
- // resolveRuleId: the ONE legacy-spelling precedence the upcast, this reader, and the
1559
- // below-floor nit reader share — a pre-id marker (or one written before the migration)
1560
- // carries `code`; a codeless one resolves to the same synthesized id the registry's
1561
- // legacy upcast derives, so the entry keys to the identical claim on the next round.
1562
- code: resolveRuleId({
1563
- id: typeof id === "string" ? id : void 0,
1564
- code: typeof legacyCode === "string" ? legacyCode : void 0,
1565
- path,
1566
- title
1567
- }) ?? synthesizedFindingId(path, title),
1568
- title,
1569
- description,
1570
- reasoning,
1571
- severity,
1572
- path,
1573
- patch: typeof patch === "string" ? patch : null
1574
- } : null;
1575
- };
1576
- const threads = /* @__PURE__ */ new Map();
1577
- for (const c of ordered) {
1578
- const root = rootOf(c);
1579
- if (root === null) continue;
1580
- const group = threads.get(root.id);
1581
- if (group === void 0) threads.set(root.id, [c]);
1582
- else group.push(c);
1583
- }
1584
- const entries = [];
1585
- for (const [rootId, group] of threads) {
1586
- const root = byId.get(rootId);
1587
- if (root === void 0 || root.user_login !== botLogin) continue;
1588
- const finding = findingOf(root);
1589
- if (finding === null) continue;
1590
- const humanReplies = group.filter(
1591
- (c) => c.id !== root.id && isHuman(c.user_login, c.user_type, botLogin)
1592
- );
1593
- const reply = humanReplies[humanReplies.length - 1];
1594
- if (reply === void 0) continue;
1595
- entries.push({
1596
- ...finding,
1597
- repliedAt: reply.created_at,
1598
- replyId: reply.id,
1599
- threadUrl: root.html_url,
1600
- replyUrl: reply.html_url,
1601
- replyAuthor: reply.user_login,
1602
- replyExcerpt: clipText(reply.body ?? "", EXCERPT_LIMIT)
1603
- });
1604
- }
1605
- const byKey = /* @__PURE__ */ new Map();
1606
- for (const entry of [...entries].sort(
1607
- (a, b) => (b.repliedAt ?? "").localeCompare(a.repliedAt ?? "") || b.replyId - a.replyId
1608
- )) {
1609
- const key2 = answeredNoteKey({ id: entry.code, title: entry.title });
1610
- if (!byKey.has(key2)) byKey.set(key2, entry);
1611
- }
1612
- return [...byKey.values()];
1613
- };
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
+ );
1614
1823
  var isSynthesizedTitleMatch = (f, e) => isSynthesizedFindingId(e.code) && e.title === f.title;
1615
1824
  var matches = (resolvedId, f, e) => e.code === resolvedId || isSynthesizedTitleMatch(f, e);
1616
- var VERBATIM_FIELDS = ["title", "description", "reasoning", "severity", "path", "patch"];
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"];
1617
1828
  var verbatimFieldEqual = (f, e, field) => field === "patch" ? (f.patch ?? null) === e.patch : f[field] === e[field];
1618
- var isVerbatimReRaise = (f, e) => VERBATIM_FIELDS.every((field) => verbatimFieldEqual(f, e, field));
1619
- var isAnsweredDrop = (resolvedId, f, e) => matches(resolvedId, f, e) && isVerbatimReRaise(f, e) && f.severity !== "critical";
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));
1620
1832
  var verbatimMatchCount = (f, e) => VERBATIM_FIELDS.reduce((count, field) => count + (verbatimFieldEqual(f, e, field) ? 1 : 0), 0);
1621
1833
  var answeredNoteKey = (f) => f.id !== "" ? f.id : `title:${f.title}`;
1622
- var NO_TITLE_MATCH = { entry: void 0, score: -1 };
1623
1834
  var bestTitleMatch = (f, registry) => {
1624
1835
  let best;
1625
1836
  let bestScore = -1;
@@ -1632,9 +1843,10 @@ var bestTitleMatch = (f, registry) => {
1632
1843
  if (score === VERBATIM_FIELDS.length) break;
1633
1844
  }
1634
1845
  }
1635
- return { entry: best, score: bestScore };
1846
+ return best;
1636
1847
  };
1637
- var answeredNote = (e) => `Re-raised; prior answer at ${e.replyUrl} by ${e.replyAuthor} \u2014 cite the new evidence that invalidates it.`;
1848
+ var byAuthor = (e) => e.answerAuthor === null ? "" : ` by \`${escapeCodeBackticks(e.answerAuthor)}\``;
1849
+ var answeredNote = (e) => `Re-raised; prior answer at ${linkSafeUrl(e.answerUrl)}${byAuthor(e)} \u2014 cite the new evidence that invalidates it.`;
1638
1850
  var applyAnswered = (findings, registry) => {
1639
1851
  const kept = [];
1640
1852
  const noteEntries = [];
@@ -1644,15 +1856,15 @@ var applyAnswered = (findings, registry) => {
1644
1856
  for (const f of findings) {
1645
1857
  const resolvedId = resolveFindingId(f);
1646
1858
  const idMatch = registry.find((e) => e.code === resolvedId);
1647
- const { entry: titleMatch, score: titleScore } = idMatch === void 0 ? bestTitleMatch(f, registry) : NO_TITLE_MATCH;
1859
+ const titleMatch = idMatch === void 0 ? bestTitleMatch(f, registry) : void 0;
1648
1860
  const entry = idMatch ?? titleMatch;
1649
1861
  if (entry === void 0) {
1650
1862
  kept.push(f);
1651
1863
  continue;
1652
1864
  }
1653
- const dropped = (idMatch !== void 0 ? isVerbatimReRaise(f, entry) : titleScore === VERBATIM_FIELDS.length) && f.severity !== "critical";
1865
+ const dropped = isAnsweredDrop(resolvedId, f, entry);
1654
1866
  if (dropped) {
1655
- droppedByEntry.set(entry.replyId, entry);
1867
+ droppedByEntry.set(entry.code, entry);
1656
1868
  droppedFindingIds.push(resolvedId);
1657
1869
  droppedCount += 1;
1658
1870
  } else {
@@ -1668,46 +1880,64 @@ var applyAnswered = (findings, registry) => {
1668
1880
  droppedCount
1669
1881
  };
1670
1882
  };
1671
- var answeredReRaiseNote = (entries, count) => {
1672
- if (entries.length === 0) return "";
1673
- const label = (e) => e.code !== "" ? `\`${escapeCodeBackticks(e.code)}\`` : `\u201C${escapeCodeBackticks(e.title)}\u201D`;
1674
- const lines = entries.map(
1675
- (e) => `> - ${label(e)} \u2014 [prior answer](${e.replyUrl}) by ${e.replyAuthor}`
1676
- );
1677
- return [
1678
- `> \u21A9\uFE0F **${String(count)} finding(s) re-raised without new evidence \u2014 treated as answered** (each has a human reply on its prior inline thread):`,
1679
- ...lines
1680
- ].join("\n");
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
+ };
1681
1896
  };
1682
- var fetchThreadComments = async (ghApi, repo, prNumber) => {
1683
- try {
1684
- const rows = parseJsonl(
1685
- await ghApi([
1686
- // per_page rides the QUERY string — a `-f` field would flip `gh api` from GET to POST
1687
- // and 422 every answered-thread fetch.
1688
- `repos/${repo}/pulls/${String(prNumber)}/comments?per_page=100`,
1689
- "--paginate",
1690
- "--jq",
1691
- THREAD_COMMENT_JQ
1692
- ])
1693
- );
1694
- return rows.flatMap((row) => {
1695
- const decoded = ThreadCommentCodec.decode(row);
1696
- return decoded._tag === "Right" ? [decoded.right] : [];
1697
- });
1698
- } catch (err) {
1699
- process.stderr.write(
1700
- `Warning: could not fetch review threads to detect answered findings (${errMsg(err)}) \u2014 no answered-finding state for this post
1701
- `
1702
- );
1703
- return null;
1704
- }
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
+ };
1910
+ };
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
+ };
1705
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");
1706
1938
 
1707
1939
  // src/render.ts
1708
1940
  var escapePipes = (text) => text.replace(/\|/g, "\\|");
1709
- var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
1710
- var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
1711
1941
  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>`;
1712
1942
  var terminalNotice = (i) => [
1713
1943
  `## ${i.verdictBadge}`,
@@ -1798,7 +2028,7 @@ var sanitizeFinding = (f, answeredNotes, discussionByFinding, permalinkBase, una
1798
2028
  rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
1799
2029
  ...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
1800
2030
  patchProjection: projectPatch(f.patch, "comment-body"),
1801
- answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : "",
2031
+ answeredNote: noteFor(answeredNotes, key2),
1802
2032
  discussion: discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(discussionByFinding, f.id) ? discussionByFinding[f.id] ?? [] : [],
1803
2033
  ...discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(discussionTruncated, f.id) ? { discussionTruncated: discussionTruncated[f.id] } : {}
1804
2034
  };
@@ -1824,12 +2054,14 @@ var sanitizeSuppressedNit = (f, discussion, discussionTruncated) => ({
1824
2054
  var carriedLines = (f) => [
1825
2055
  `description: ${clipText(f.description, BODY_CLIP_CHARS)}`,
1826
2056
  ...f.recommendation !== void 0 ? [`recommendation: ${clipText(f.recommendation, BODY_CLIP_CHARS)}`] : [],
2057
+ ...hasRebuttal(f) ? [`rebuttal: ${clipText(f.rebuttal ?? "", BODY_CLIP_CHARS)}`] : [],
1827
2058
  `reasoning: ${clipText(f.reasoning, BODY_CLIP_CHARS)}`,
1828
2059
  ...f.patch !== void 0 ? ["patch:", clipText(f.patch, BODY_CLIP_CHARS)] : []
1829
2060
  ].flatMap((block) => commentSafe(block).split("\n")).map((line) => line.trimEnd());
1830
- var sanitizeSystemic = (s, discussion, discussionTruncated) => ({
2061
+ var sanitizeSystemic = (s, answeredNotes, discussion, discussionTruncated) => ({
1831
2062
  ...s,
1832
2063
  title: escapePipes(s.title),
2064
+ answeredNote: noteFor(answeredNotes, answeredSystemicNoteKey(s)),
1833
2065
  discussion,
1834
2066
  ...discussionTruncated !== void 0 ? { discussionTruncated } : {},
1835
2067
  discussionHtml: discussion.length > 0 ? discussionAsideHtml(discussion, discussionTruncated, "") : "",
@@ -1881,6 +2113,7 @@ var render = (input) => {
1881
2113
  const trajectory = convergence?.rounds ?? input.rounds ?? [];
1882
2114
  const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
1883
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` : "";
1884
2117
  const advisoryAllowed = isFullReviewRound;
1885
2118
  const discussionLinksFor = (id) => input.discussionByFinding !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionByFinding, id) ? input.discussionByFinding[id] ?? [] : [];
1886
2119
  const discussionTruncatedFor = (id) => input.discussionTruncated !== void 0 && Object.prototype.hasOwnProperty.call(input.discussionTruncated, id) ? input.discussionTruncated[id] : void 0;
@@ -1960,6 +2193,7 @@ var render = (input) => {
1960
2193
  (input.findings.systemic_problems ?? []).map(
1961
2194
  (s) => sanitizeSystemic(
1962
2195
  s,
2196
+ input.answeredNotes,
1963
2197
  s.id !== void 0 ? discussionLinksFor(s.id) : [],
1964
2198
  s.id !== void 0 ? discussionTruncatedFor(s.id) : void 0
1965
2199
  )
@@ -1968,6 +2202,23 @@ var render = (input) => {
1968
2202
  (s) => s.discussion.length > 0 ? DISCUSSION_BLOCK_OVERHEAD + s.discussionHtml.length : 0,
1969
2203
  (s) => ({ ...s, discussion: [], discussionHtml: "" })
1970
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
+ );
1971
2222
  const orphanedRender = {
1972
2223
  lines: orphanedBudget.kept.map(([, entry]) => entry.html),
1973
2224
  // The "(showing N of M)" total counts DISTINCT DISPLAYED entries — the pre-budget fold above
@@ -1992,6 +2243,10 @@ var render = (input) => {
1992
2243
  input.findingsPointer ?? findingsMarkerPair(input.jsonUrl, input.findings.convergence)
1993
2244
  ),
1994
2245
  summaryAvailable: input.summaryAvailable ?? false,
2246
+ responseForm: RESPONSE_FORM,
2247
+ responseTeaching: RESPONSE_TEACHING,
2248
+ rulingTeaching: RULING_TEACHING,
2249
+ teachesResponses: isConvergenceRound(route, incomplete),
1995
2250
  shedNote: shedNote(
1996
2251
  input.shedCount ?? 0,
1997
2252
  input.summaryAvailable ?? false,
@@ -2007,7 +2262,7 @@ var render = (input) => {
2007
2262
  route: route ?? void 0,
2008
2263
  postedAt: input.postedAt ?? "",
2009
2264
  roundsSummary: roundsSummary(trajectory, input.roundCount),
2010
- convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : "",
2265
+ convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) + contestedSuffix : "",
2011
2266
  inlinePosted: input.inlineDisposition?.kind === "posted" ? input.inlineDisposition.count : 0,
2012
2267
  runUrl: input.runUrl,
2013
2268
  summaryAvailable: input.summaryAvailable ?? false,
@@ -2034,15 +2289,21 @@ var render = (input) => {
2034
2289
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
2035
2290
  postedAt: input.postedAt ?? "",
2036
2291
  severityCounts,
2037
- convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
2292
+ convergenceSummary: !isFullReviewRound ? "" : (convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold)) + contestedSuffix,
2038
2293
  strays: discussionBudget.kept,
2039
2294
  orphanedLines: orphanedRender.lines,
2040
2295
  orphanedUnresolvable: input.orphanedUnresolvable === true,
2041
- discussionDropped: discussionBudget.droppedItems.length + systemicBudget.droppedItems.length + orphanedBudget.droppedItems.length,
2042
- discussionDroppedIds: discussionBudget.droppedItems.map((v) => clipText(escapeCodeBackticks(v.idKey), 64)).slice(0, DROPPED_IDS_SHOWN),
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),
2043
2301
  discussionDroppedSystemicIds: systemicBudget.droppedItems.flatMap((s) => s.id !== void 0 ? [clipText(s.id, 64)] : []).slice(0, DROPPED_IDS_SHOWN),
2044
2302
  discussionDroppedOrphanedIds: orphanedBudget.droppedItems.map(([token]) => clipText(token, 64)).slice(0, DROPPED_IDS_SHOWN),
2045
- discussionDroppedExtra: Math.max(0, discussionBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, systemicBudget.droppedItems.length - DROPPED_IDS_SHOWN) + Math.max(0, orphanedBudget.droppedItems.length - DROPPED_IDS_SHOWN),
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),
2046
2307
  discussionCap: PER_FINDING_LINKS,
2047
2308
  orphanedTotal: orphanedRender.total,
2048
2309
  suppressedNits: suppressedBudget.kept,
@@ -2064,6 +2325,7 @@ var render = (input) => {
2064
2325
  sameRootNotes: advisoryAllowed ? sameRootNotes : {},
2065
2326
  answeredNotes: input.answeredNotes ?? {},
2066
2327
  answeredReRaiseNote: input.answeredReRaiseNote ?? "",
2328
+ contested: contestedBudget.kept,
2067
2329
  reviewUrl: input.reviewUrl ?? null,
2068
2330
  formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
2069
2331
  // N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to
@@ -2083,7 +2345,8 @@ var render = (input) => {
2083
2345
  },
2084
2346
  verdictBadge,
2085
2347
  severityEmoji,
2086
- formatConfidence
2348
+ formatConfidence,
2349
+ hasRebuttal
2087
2350
  });
2088
2351
  };
2089
2352
  var key = (path, line) => `${path}:${String(line)}`;
@@ -2146,9 +2409,11 @@ var renderCommentBody = (f, eta, template, modelsText, jsonUrl, pointer, sameRoo
2146
2409
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
2147
2410
  eta.renderString(template, {
2148
2411
  ...f,
2412
+ idLabel: escapeCodeBackticks(f.id),
2149
2413
  patchProjection: projectPatch(f.patch, "diff-anchored"),
2150
2414
  severityEmoji,
2151
2415
  formatConfidence,
2416
+ hasRebuttal,
2152
2417
  modelsText,
2153
2418
  jsonUrl: jsonUrl ?? null,
2154
2419
  findingsPointer: pointer,
@@ -2161,7 +2426,6 @@ var buildInlineComments = (findings, diff, context) => {
2161
2426
  const { inDiff, strays } = context.partition ?? partitionFindings(findings, indexDiff(diff));
2162
2427
  const eta = new Eta({ autoTrim: false });
2163
2428
  const modelsText = formatModels(models);
2164
- const noteFor = (notes, key2) => notes !== void 0 && Object.prototype.hasOwnProperty.call(notes, key2) ? notes[key2] ?? "" : "";
2165
2429
  const comments = inDiff.map((f) => {
2166
2430
  const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
2167
2431
  const clipProse = fullFindings !== void 0 && findingPayload(f, fullFindings.schema_version).length > INLINE_PROSE_CLIP_THRESHOLD_CHARS;
@@ -2170,6 +2434,7 @@ var buildInlineComments = (findings, diff, context) => {
2170
2434
  title: clipText(f.title, BODY_CLIP_CHARS),
2171
2435
  description: clipText(f.description, BODY_CLIP_CHARS),
2172
2436
  ...f.recommendation != null ? { recommendation: clipText(f.recommendation, BODY_CLIP_CHARS) } : {},
2437
+ ...hasRebuttal(f) ? { rebuttal: clipText(f.rebuttal ?? "", BODY_CLIP_CHARS) } : {},
2173
2438
  reasoning: clipText(f.reasoning, BODY_CLIP_CHARS),
2174
2439
  ...f.patch != null ? { patch: clipText(f.patch, BODY_CLIP_CHARS) } : {}
2175
2440
  } : f;
@@ -2204,7 +2469,7 @@ var buildInlineComments = (findings, diff, context) => {
2204
2469
  var renderStraysSection = (strays) => {
2205
2470
  if (strays.length === 0) return "";
2206
2471
  const items = strays.map(
2207
- (f) => `- **${f.severity}** \xB7 \`${f.path}:${String(f.start_line)}\` \u2014 ${f.title}`
2472
+ (f) => `- **${f.severity}** \xB7 \`${f.path}:${String(f.start_line)}\` \u2014 ${f.title}${f.id === "" ? "" : ` \xB7 id \`${escapeCodeBackticks(f.id)}\``}`
2208
2473
  );
2209
2474
  return [
2210
2475
  "",
@@ -2215,6 +2480,110 @@ var renderStraysSection = (strays) => {
2215
2480
  ...items
2216
2481
  ].join("\n");
2217
2482
  };
2483
+ var numField = (rec, key2) => {
2484
+ const v = rec[key2];
2485
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0;
2486
+ };
2487
+ var messageUsage = (entry) => {
2488
+ const rec = asRecord(entry);
2489
+ if (rec === null || rec["type"] !== "assistant") return null;
2490
+ const msg = asRecord(rec["message"]);
2491
+ if (msg === null) return null;
2492
+ const model = msg["model"];
2493
+ const usage = asRecord(msg["usage"]);
2494
+ if (typeof model !== "string" || usage === null) return null;
2495
+ const id = msg["id"];
2496
+ return {
2497
+ id: typeof id === "string" ? id : null,
2498
+ model: modelIdentity(model),
2499
+ input: numField(usage, "input_tokens"),
2500
+ output: numField(usage, "output_tokens"),
2501
+ cacheRead: numField(usage, "cache_read_input_tokens"),
2502
+ cacheWrite: numField(usage, "cache_creation_input_tokens")
2503
+ };
2504
+ };
2505
+ var tsMsOf = (entry) => {
2506
+ const rec = asRecord(entry);
2507
+ const ts = rec?.["timestamp"];
2508
+ if (typeof ts !== "string") return null;
2509
+ const ms = Date.parse(ts);
2510
+ return Number.isNaN(ms) ? null : ms;
2511
+ };
2512
+ var EMPTY_TOTALS = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
2513
+ var parseJsonl = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
2514
+ try {
2515
+ return [JSON.parse(line)];
2516
+ } catch {
2517
+ return [];
2518
+ }
2519
+ });
2520
+ var sumTranscriptUsage = (entries) => {
2521
+ const summed = entries.reduce(
2522
+ (acc, entry) => {
2523
+ const u = messageUsage(entry);
2524
+ if (u === null) return acc;
2525
+ if (u.id !== null && acc.seen.has(u.id)) return acc;
2526
+ if (u.id !== null) acc.seen.add(u.id);
2527
+ const prev = acc.totals.get(u.model) ?? EMPTY_TOTALS;
2528
+ acc.totals.set(u.model, {
2529
+ input: prev.input + u.input,
2530
+ output: prev.output + u.output,
2531
+ cacheRead: prev.cacheRead + u.cacheRead,
2532
+ cacheWrite: prev.cacheWrite + u.cacheWrite
2533
+ });
2534
+ return { totals: acc.totals, turns: acc.turns + 1, seen: acc.seen };
2535
+ },
2536
+ { totals: /* @__PURE__ */ new Map(), turns: 0, seen: /* @__PURE__ */ new Set() }
2537
+ );
2538
+ const models = [...summed.totals].map(([model, t10]) => ({
2539
+ model,
2540
+ input_tokens: t10.input,
2541
+ output_tokens: t10.output,
2542
+ cache_read_tokens: t10.cacheRead,
2543
+ cache_write_tokens: t10.cacheWrite
2544
+ }));
2545
+ const bounds = entries.reduce(
2546
+ (acc, entry) => {
2547
+ const ms = tsMsOf(entry);
2548
+ if (ms === null) return acc;
2549
+ return {
2550
+ min: acc.min === null || ms < acc.min ? ms : acc.min,
2551
+ max: acc.max === null || ms > acc.max ? ms : acc.max
2552
+ };
2553
+ },
2554
+ { min: null, max: null }
2555
+ );
2556
+ return {
2557
+ models,
2558
+ turns: summed.turns,
2559
+ durationMs: bounds.min !== null && bounds.max !== null ? bounds.max - bounds.min : 0,
2560
+ firstTsMs: bounds.min,
2561
+ lastTsMs: bounds.max
2562
+ };
2563
+ };
2564
+ var subagentFiles = (mainPath) => {
2565
+ const dir = join(dirname(mainPath), basename(mainPath, ".jsonl"), "subagents");
2566
+ try {
2567
+ return readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => join(dir, name));
2568
+ } catch {
2569
+ return [];
2570
+ }
2571
+ };
2572
+ var readTranscriptTree = (mainPath) => {
2573
+ const mainText = readFileOrNull(mainPath);
2574
+ const mainEntries = mainText === null ? [] : parseJsonl(mainText);
2575
+ const inlineSidechains = mainEntries.some((e) => asRecord(e)?.["isSidechain"] === true);
2576
+ const siblings = inlineSidechains ? [] : subagentFiles(mainPath);
2577
+ const siblingReads = siblings.flatMap((path) => {
2578
+ const text = readFileOrNull(path);
2579
+ return text === null ? [] : [{ path, entries: parseJsonl(text) }];
2580
+ });
2581
+ return {
2582
+ entries: [...mainEntries, ...siblingReads.flatMap((r) => r.entries)],
2583
+ files: [...mainText === null ? [] : [mainPath], ...siblingReads.map((r) => r.path)],
2584
+ missing: mainText === null
2585
+ };
2586
+ };
2218
2587
  var addFormats = _addFormats;
2219
2588
  var validatorCache = /* @__PURE__ */ new Map();
2220
2589
  var compileSchema = (schemaPath) => {
@@ -2396,7 +2765,7 @@ var writesToDraft = (toolName, toolInput, draftPath) => {
2396
2765
  const targets = [draftPath, basename(draftPath), "$DRAFT", "${DRAFT}"];
2397
2766
  if (WRITE_TOOLS.has(toolName)) {
2398
2767
  const fp = rec?.["file_path"] ?? rec?.["notebook_path"];
2399
- if (typeof fp === "string" && targets.some((t8) => fp === t8 || basename(fp) === basename(t8)))
2768
+ if (typeof fp === "string" && targets.some((t10) => fp === t10 || basename(fp) === basename(t10)))
2400
2769
  return true;
2401
2770
  }
2402
2771
  if (toolName === "Bash") {
@@ -2423,7 +2792,6 @@ var sidecarPath = (draftPath, postfix) => {
2423
2792
  return join(dirname(draftPath), `${basename(draftPath, ext)}${postfix}${ext}`);
2424
2793
  };
2425
2794
  var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
2426
- var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
2427
2795
  var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
2428
2796
  var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
2429
2797
  var costAxisDisengagedPath = (draftPath) => sidecarPath(draftPath, ".cost-axis-disengaged");
@@ -2502,9 +2870,9 @@ var parseWallMs = (raw) => {
2502
2870
  };
2503
2871
  var parseEpochSecMs = (raw) => {
2504
2872
  if (raw === void 0) return null;
2505
- const t8 = raw.trim();
2506
- if (!/^\d+$/.test(t8)) return null;
2507
- const n = Number.parseInt(t8, 10);
2873
+ const t10 = raw.trim();
2874
+ if (!/^\d+$/.test(t10)) return null;
2875
+ const n = Number.parseInt(t10, 10);
2508
2876
  return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
2509
2877
  };
2510
2878
  var anchoredElapsedMs = (src) => {
@@ -2625,10 +2993,12 @@ var findingsTable = [
2625
2993
  {
2626
2994
  minor: "0.4",
2627
2995
  defaultVersion: "0.4.0",
2628
- // The frozen tolerant-in legacy schema, NOT the live 0.10 file: every ajv-gated channel
2996
+ // The frozen tolerant-in legacy schema, NOT the live file: every ajv-gated channel
2629
2997
  // (validate --schema-version, the extraction ladder) dispatches the RAW doc's declared minor
2630
2998
  // through schemaPathFor, so the ajv gate must accept exactly what the tolerant legacy codec
2631
2999
  // accepts — the live file (id required) would reject the legacy docs the upcast promises to read.
3000
+ // One deliberate exception: the codec drops a draft-only `rebuttal` that the frozen file rejects.
3001
+ // A pre-0.10 document never carried one, so only a hybrid doc the rescue exists for meets it.
2632
3002
  schemaFile: "v0.9/findings.schema.json",
2633
3003
  codec: legacyFindingsCodec,
2634
3004
  normalize: legacyFindingsNormalize,
@@ -2660,6 +3030,14 @@ var findingsTable = [
2660
3030
  },
2661
3031
  {
2662
3032
  minor: "0.10",
3033
+ defaultVersion: "0.10.0",
3034
+ schemaFile: "findings.schema.json",
3035
+ codec: FindingsCodec,
3036
+ normalize: identity,
3037
+ latest: false
3038
+ },
3039
+ {
3040
+ minor: "0.11",
2663
3041
  defaultVersion: DEFAULT_SCHEMA_VERSION,
2664
3042
  schemaFile: "findings.schema.json",
2665
3043
  codec: FindingsCodec,
@@ -2714,21 +3092,28 @@ var describeValidationError = (e) => {
2714
3092
  var formatErrors = (errors) => errors.map(describeValidationError);
2715
3093
  var declaredVersion = (raw) => typeof raw === "object" && raw !== null && "schema_version" in raw ? typeof raw.schema_version === "string" ? raw.schema_version : void 0 : void 0;
2716
3094
  var supportedVersions = (kind) => tableFor(kind).map((entry) => entry.minor);
2717
- var defaultVersion = (kind) => {
2718
- const latest = tableFor(kind).find((entry) => entry.latest);
2719
- if (!latest) throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
2720
- return latest.defaultVersion;
2721
- };
2722
3095
  var bundledSchemaPath = (relativePath) => resolve(import.meta.dirname, "..", "schema", relativePath);
2723
- var schemaPathFor = (kind, version) => {
3096
+ var entryFor = (kind, version) => {
2724
3097
  const table = tableFor(kind);
2725
3098
  const entry = version === void 0 ? table.find((v) => v.latest) : table.find((v) => v.minor === majorMinor(version));
3099
+ if (!entry && version === void 0) {
3100
+ throw new Error(`Registry invariant violated \u2014 no latest entry for "${kind}"`);
3101
+ }
2726
3102
  if (!entry) {
2727
3103
  throw new Error(
2728
3104
  `Unsupported ${kind} schema version "${version ?? ""}" \u2014 supported: ${supportedVersions(kind).join(", ")}`
2729
3105
  );
2730
3106
  }
2731
- return bundledSchemaPath(entry.schemaFile);
3107
+ return entry;
3108
+ };
3109
+ var schemaPathFor = (kind, version) => bundledSchemaPath(entryFor(kind, version).schemaFile);
3110
+ var versionFor = (kind, version) => entryFor(kind, version).defaultVersion;
3111
+ var defaultVersion = (kind) => versionFor(kind);
3112
+ var livePinFor = (kind, version) => {
3113
+ const table = tableFor(kind);
3114
+ const latest = table.find((v) => v.latest);
3115
+ const entry = version === void 0 ? latest : table.find((v) => v.minor === majorMinor(version));
3116
+ return entry !== void 0 && entry.schemaFile === latest?.schemaFile ? entry.defaultVersion : void 0;
2732
3117
  };
2733
3118
  var resolveFindings = (raw) => {
2734
3119
  const version = declaredVersion(raw);
@@ -2762,7 +3147,7 @@ var resolveTolerantFindings = (doc) => {
2762
3147
  };
2763
3148
 
2764
3149
  // src/pr.ts
2765
- var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha}";
3150
+ var CANDIDATE_JQ = ".[] | {number: .number, state: .state, headRef: .head.ref, headSha: .head.sha, headRepo: (.head.repo.full_name // null)}";
2766
3151
  var parseCandidates = (stdout) => parseJsonl(stdout);
2767
3152
  var fetchDirectCandidates = async (repo, headSha, ghApi) => {
2768
3153
  try {
@@ -2791,7 +3176,7 @@ var resolvePr = (candidates, headBranch) => {
2791
3176
  const scoped = candidates.length > 1 && headBranch ? candidates.filter((c) => c.headRef === headBranch) : candidates;
2792
3177
  const chosen = scoped.find((c) => c.state === "open") ?? scoped[0] ?? candidates[0];
2793
3178
  if (chosen === void 0) return { kind: "none" };
2794
- return chosen.state === "open" ? { kind: "open", prNumber: chosen.number } : { kind: "not-open", prNumber: chosen.number, state: chosen.state };
3179
+ return chosen.state === "open" ? { kind: "open", prNumber: chosen.number, headRepo: chosen.headRepo ?? null } : { kind: "not-open", prNumber: chosen.number, state: chosen.state };
2795
3180
  };
2796
3181
  var fetchDiff = async (repo, prNumber, ghApi) => ghApi([
2797
3182
  `repos/${repo}/pulls/${String(prNumber)}`,
@@ -3095,7 +3480,8 @@ var postInlineReview = async (pr, comments, inDiff, ghApi) => {
3095
3480
  return { url, posted, inlinePosted, unposted };
3096
3481
  }
3097
3482
  };
3098
- var ISSUE_COMMENTS_JQ = '.[] | {id, user: (.user.login // "(deleted)"), created_at, html_url, body: (.body // "")}';
3483
+ var ISSUE_COMMENTS_JQ = '.[] | {id, user: (.user.login // "(deleted)"), user_type: (.user.type // null), author_association: (.author_association // null), created_at, html_url, body: (.body // "")}';
3484
+ var nullableString = (value) => typeof value === "string" ? value : null;
3099
3485
  var parseIssueCommentRows = (raw) => {
3100
3486
  let malformed = 0;
3101
3487
  const rows = [];
@@ -3114,7 +3500,15 @@ var parseIssueCommentRows = (raw) => {
3114
3500
  const url = rec?.["html_url"];
3115
3501
  const body = rec?.["body"];
3116
3502
  if (typeof id === "number" && typeof author === "string" && typeof created === "string" && typeof url === "string" && typeof body === "string") {
3117
- rows.push({ id, author, created, url, body });
3503
+ rows.push({
3504
+ id,
3505
+ author,
3506
+ created,
3507
+ url,
3508
+ body,
3509
+ authorType: nullableString(rec?.["user_type"]),
3510
+ association: nullableString(rec?.["author_association"])
3511
+ });
3118
3512
  } else {
3119
3513
  malformed += 1;
3120
3514
  }
@@ -3124,6 +3518,45 @@ var parseIssueCommentRows = (raw) => {
3124
3518
  );
3125
3519
  return { rows, malformed };
3126
3520
  };
3521
+ var COMMITS_JQ = ".[] | {sha, message: .commit.message, author: (.author.login // null), date: (.commit.author.date // null)}";
3522
+ var fetchCommitAnswersSource = async (repo, prNumber, ghApi) => {
3523
+ try {
3524
+ const raw = await ghApi([
3525
+ `repos/${repo}/pulls/${String(prNumber)}/commits?per_page=100`,
3526
+ "--paginate",
3527
+ "--jq",
3528
+ COMMITS_JQ
3529
+ ]);
3530
+ const lines = raw.split("\n").filter((line) => line.trim() !== "");
3531
+ const commits = lines.flatMap((line) => {
3532
+ const parsed = tryParseJson(line.trim());
3533
+ const rec = parsed.ok ? asRecord(parsed.value) : null;
3534
+ const sha = rec?.["sha"];
3535
+ const message = rec?.["message"];
3536
+ return typeof sha === "string" && typeof message === "string" ? [
3537
+ {
3538
+ sha,
3539
+ message,
3540
+ author: nullableString(rec?.["author"]),
3541
+ date: nullableString(rec?.["date"])
3542
+ }
3543
+ ] : [];
3544
+ });
3545
+ if (commits.length < lines.length) {
3546
+ process.stderr.write(
3547
+ `Warning: ${String(lines.length - commits.length)} of the PR's commits did not decode \u2014 their answers close nothing
3548
+ `
3549
+ );
3550
+ }
3551
+ return commits;
3552
+ } catch (err) {
3553
+ process.stderr.write(
3554
+ `Warning: could not read the PR's commits (${errMsg(err)}) \u2014 no commit answer closes a finding this round
3555
+ `
3556
+ );
3557
+ return [];
3558
+ }
3559
+ };
3127
3560
  var fetchIssueCommentRows = async (repo, prNumber, ghApi) => {
3128
3561
  const raw = await ghApi([
3129
3562
  // per_page rides the QUERY string — a `-f` field would flip `gh api` from GET to POST
@@ -3194,7 +3627,6 @@ var discussionRows = (rows, stickyId, botLogin) => {
3194
3627
  return c.id !== stickyId && c.author !== botLogin;
3195
3628
  });
3196
3629
  };
3197
- var ID_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
3198
3630
  var mentionsOutsideKnown = (reachable, currentIds) => {
3199
3631
  const known = new Set(currentIds);
3200
3632
  const escapedToRaw = escapedIdIndex(currentIds);
@@ -3279,24 +3711,6 @@ var buildStickyDiscussion = (reachable, currentIds, priorIds) => {
3279
3711
  orphanedTruncated: Object.fromEntries(orphanedTruncated)
3280
3712
  };
3281
3713
  };
3282
- var priorIdsFrom = (doc) => {
3283
- if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [];
3284
- const rec = doc;
3285
- const idsOf = (field, systemic) => (Array.isArray(rec[field]) ? rec[field] : []).flatMap((raw) => {
3286
- const item = typeof raw === "object" && raw !== null ? asRecord(raw) : null;
3287
- if (item === null) return [];
3288
- const title = typeof item["title"] === "string" ? item["title"] : "";
3289
- const resolved = resolveRuleId({
3290
- ...typeof item["id"] === "string" ? { id: item["id"] } : {},
3291
- ...typeof item["code"] === "string" ? { code: item["code"] } : {},
3292
- ...typeof item["path"] === "string" ? { path: item["path"] } : {},
3293
- title
3294
- });
3295
- const id = resolved ?? (systemic && title !== "" ? synthesizedSystemicId(title) : void 0);
3296
- return id !== void 0 ? [id] : [];
3297
- });
3298
- return [...idsOf("findings", false), ...idsOf("systemic_problems", true)];
3299
- };
3300
3714
  var appendBestEffort = (path, label, body) => {
3301
3715
  if (path === void 0 || path === "") return;
3302
3716
  try {
@@ -3391,8 +3805,8 @@ var priorBotCommentIds = (raw, botLogin) => {
3391
3805
  const nodes = conn?.nodes;
3392
3806
  if (!Array.isArray(nodes)) return { ids: [], truncated };
3393
3807
  const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
3394
- const ids = nodes.flatMap((t8) => {
3395
- const cnodes = t8.comments?.nodes;
3808
+ const ids = nodes.flatMap((t10) => {
3809
+ const cnodes = t10.comments?.nodes;
3396
3810
  return Array.isArray(cnodes) ? cnodes.map((c) => priorBotCommentId(c, logins)).filter((id) => id !== null) : [];
3397
3811
  });
3398
3812
  return { ids, truncated };
@@ -3488,7 +3902,7 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
3488
3902
  convergence: conv ?? void 0
3489
3903
  });
3490
3904
  let warnedNoJsonUrl = false;
3491
- const findingsBlob = (doc) => {
3905
+ const findingsPair = (doc) => {
3492
3906
  if (input.jsonUrl) return findingsMarkerPair(input.jsonUrl, doc.convergence);
3493
3907
  const carriedLink = existingSticky !== null ? findingsArtifactUrl(existingSticky.body) : null;
3494
3908
  if (carriedLink !== null) {
@@ -3502,6 +3916,8 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
3502
3916
  }
3503
3917
  return findingsMarkerPair(void 0, doc.convergence);
3504
3918
  };
3919
+ const carriedDialogue = existingSticky !== null ? carriedDialogueMarker(existingSticky.body) : "";
3920
+ const findingsBlob = (doc, dialogue2 = carriedDialogue) => [findingsPair(doc), dialogue2].filter((marker) => marker !== "").join("\n");
3505
3921
  const leaveInPlace = (message) => {
3506
3922
  process.stderr.write(
3507
3923
  message ?? "Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place\n"
@@ -3510,9 +3926,9 @@ var post = async (input, ghApi = runGhApi, readArtifact = ghArtifactReader) => {
3510
3926
  process.exit(0);
3511
3927
  };
3512
3928
  const logAnsweredDrops = () => {
3513
- if (verbatimReRaised.length > 0) {
3929
+ if (droppedCount > 0) {
3514
3930
  process.stderr.write(
3515
- `${String(droppedCount)} verbatim re-raise(s) of answered findings were treated as answered \u2014 the preserved sticky shows each finding and its prior reply
3931
+ `${String(droppedCount)} verbatim re-raise(s) of answered findings or systemic problems were treated as answered \u2014 the preserved sticky shows each one
3516
3932
  `
3517
3933
  );
3518
3934
  }
@@ -3590,7 +4006,7 @@ ${dropNote}` : ""}`,
3590
4006
  const carried = existingSticky !== null ? carriedFindingsMarker(existingSticky.body) : null;
3591
4007
  if (carried === null) return findingsBlob(doc);
3592
4008
  const provenance = existingSticky !== null ? carriedAncestryMarkers(existingSticky.body) : "";
3593
- 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");
3594
4010
  };
3595
4011
  return formatMarkdown(
3596
4012
  render({
@@ -3637,36 +4053,179 @@ ${dropNote}` : ""}`,
3637
4053
  process.exit(0);
3638
4054
  }
3639
4055
  const loadedFindings = findingsResult.findings;
3640
- const threadComments = await fetchThreadComments(ghApi, input.repo, prNumber);
3641
- const answeredRegistry = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
3642
- const answeredFilter = applyAnswered(loadedFindings.findings, answeredRegistry);
3643
- const reRaisedNotes = answeredFilter.reRaisedNotes;
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);
4068
+ const headInBaseRepo = answerable && resolution.headRepo !== null && resolution.headRepo.toLowerCase() === input.repo.toLowerCase();
4069
+ const answers = answerable ? answersFrom({
4070
+ repo: input.repo,
4071
+ prNumber,
4072
+ botLogin: input.botLogin,
4073
+ comments: commentRows.map((row) => ({
4074
+ id: row.id,
4075
+ body: row.body,
4076
+ user: { login: row.author, type: row.authorType },
4077
+ created_at: row.created,
4078
+ author_association: row.association
4079
+ })),
4080
+ commits: headInBaseRepo ? await fetchCommitAnswersSource(input.repo, prNumber, ghApi) : []
4081
+ }) : { comments: [], commits: [] };
4082
+ const isTrusted = (response) => isTrustedResponse(response, headInBaseRepo);
4083
+ const allAnswers = withRulingsOn(
4084
+ [...answers.comments, ...answers.commits],
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);
4122
+ const priorForAnswers = closures.length > 0 && existingSticky !== null ? { value: await resolvePriorFindings(existingSticky.body, readArtifact) } : null;
4123
+ const answeredPrior = priorForAnswers === null ? null : resolveTolerantFindings(priorForAnswers.value);
4124
+ if (priorForAnswers !== null && answeredPrior === null) {
4125
+ process.stderr.write(
4126
+ `Warning: ${String(closures.length)} maintainer answer(s) close prior findings, but the prior review's findings did not resolve \u2014 no re-raise is treated as answered
4127
+ `
4128
+ );
4129
+ }
4130
+ const answeredRegistry = answeredRegistryFrom(closures, answeredPrior);
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) {
4135
+ process.stderr.write(
4136
+ `Warning: ${String(unboundClosureCount)} maintainer answer(s) name nothing the prior review reported \u2014 they close nothing this round
4137
+ `
4138
+ );
4139
+ }
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
+ };
3644
4199
  const verbatimReRaised = answeredFilter.verbatimReRaised;
3645
- const droppedCount = answeredFilter.droppedCount;
4200
+ const answeredDropCount = answeredFilter.droppedCount + answeredSystemicFilter.droppedCount;
4201
+ const overruledDropCount = overruledFilter.droppedCount + overruledSystemicFilter.droppedCount;
4202
+ const droppedCount = answeredDropCount + overruledDropCount;
3646
4203
  const droppedIds = /* @__PURE__ */ new Set([
3647
- ...verbatimReRaised.flatMap((e) => e.code !== "" ? [e.code] : []),
4204
+ ...[...verbatimReRaised, ...overruledFilter.verbatimReRaised].flatMap(
4205
+ (e) => e.code !== "" ? [e.code] : []
4206
+ ),
3648
4207
  ...answeredFilter.droppedFindingIds.filter((id) => id !== "")
3649
4208
  ]);
3650
- const keptCodes = new Set(answeredFilter.findings.map((f) => f.id));
4209
+ const keptCodes = new Set([...answeredFilter.findings, ...contestedFindings].map((f) => f.id));
3651
4210
  const trulyDropped = new Set([...droppedIds].filter((c) => !keptCodes.has(c)));
3652
- const systemic = trulyDropped.size === 0 ? loadedFindings.systemic_problems ?? [] : (loadedFindings.systemic_problems ?? []).map((s) => {
4211
+ const systemic = trulyDropped.size === 0 ? answeredSystemicFilter.systemic : answeredSystemicFilter.systemic.map((s) => {
3653
4212
  if (s.finding_ids === void 0) return s;
3654
4213
  const codes = s.finding_ids.filter((c) => !trulyDropped.has(c));
3655
4214
  return codes.length === s.finding_ids.length ? s : { ...s, finding_ids: codes };
3656
4215
  });
3657
4216
  const findings = {
3658
- ...loadedFindings,
3659
- findings: [...answeredFilter.findings],
3660
- ...systemic.length > 0 ? { systemic_problems: systemic } : {}
4217
+ ...withSystemicProblems(loadedFindings, [...systemic, ...contestedSystemic]),
4218
+ findings: [...answeredFilter.findings, ...contestedFindings]
3661
4219
  };
3662
4220
  const broadCurrentIds = [
3663
4221
  ...findings.findings.map((f) => f.id).filter((id) => id !== ""),
3664
4222
  ...(findings.systemic_problems ?? []).map((s) => s.id).filter((id) => id !== void 0 && id !== "")
3665
4223
  ];
3666
- const roundHasNit = findings.findings.some((f) => f.severity === "nit");
4224
+ const argued = findings.findings.filter((f) => !isContestedFinding(f));
4225
+ const roundHasNit = argued.some((f) => f.severity === "nit");
3667
4226
  const nitWantsPrior = roundHasNit && existingSticky !== null && (isFullReviewSticky(existingSticky.body) || isFullReviewAncestry(existingSticky.body));
3668
4227
  const broadWantsPrior = existingSticky !== null && !priorIsMechanic && mentionsOutsideKnown(reachable, broadCurrentIds);
3669
- const resolvedPrior = existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
4228
+ const resolvedPrior = priorForAnswers !== null ? priorForAnswers.value : existingSticky !== null && (nitWantsPrior || broadWantsPrior) ? await resolvePriorFindings(existingSticky.body, readArtifact) : null;
3670
4229
  const priorDocForNits = nitWantsPrior ? resolvedPrior : null;
3671
4230
  const priorSuppressedKeys = new Set(
3672
4231
  priorBelowFloorNits(priorDocForNits, input.nitVisibilityFloor).map(
@@ -3677,15 +4236,25 @@ ${dropNote}` : ""}`,
3677
4236
  // synthesized prior), the answeredNoteKey form (an empty-id prior), and the bare title: key
3678
4237
  // (a pathless codeless prior).
3679
4238
  priorSuppressedKeys.has(resolveFindingId(f)) || priorSuppressedKeys.has(answeredNoteKey(f)) || priorSuppressedKeys.has(`title:${f.title}`));
3680
- const suppressedNits = findings.findings.filter(isSuppressedNit);
3681
- const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
3682
- const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
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._" : "");
3683
4251
  const inlinePartition = input.inline === true && envelope !== null ? partitionFindings(visibleFindings, indexDiff(diff)) : null;
3684
4252
  const straysForDiscussion = inlinePartition?.strays ?? visibleFindings;
3685
4253
  const currentIds = [
3686
4254
  ...straysForDiscussion.map((f) => f.id).filter((id) => id !== ""),
3687
4255
  ...suppressedNits.map((f) => f.id).filter((id) => id !== ""),
3688
- ...(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)
3689
4258
  ];
3690
4259
  const orphanResolveFailed = existingSticky !== null && broadWantsPrior && carriedFindingsMarker(existingSticky.body) !== null && resolvedPrior === null;
3691
4260
  const broadLive = new Set(broadCurrentIds);
@@ -3782,6 +4351,7 @@ ${note}
3782
4351
  nitVisibilityFloor: input.nitVisibilityFloor,
3783
4352
  answeredNotes: reRaisedNotes,
3784
4353
  answeredReRaiseNote: answeredDropNote,
4354
+ contested: contestedViews,
3785
4355
  discussionByFinding,
3786
4356
  orphanedDiscussion,
3787
4357
  orphanedTotal,
@@ -3902,7 +4472,8 @@ ${note}
3902
4472
  priorTraj,
3903
4473
  roundNumber,
3904
4474
  currentCodes,
3905
- input.headSha.slice(0, 12)
4475
+ input.headSha.slice(0, 12),
4476
+ contested
3906
4477
  ) : effectiveRoute === "mechanic" ? (
3907
4478
  // A mechanic pass means CI failed — whatever the carried prior says, its own stamp must
3908
4479
  // never read "converged". The threshold-relative critical floor keeps the carried triple
@@ -3911,10 +4482,13 @@ ${note}
3911
4482
  ) : priorConv;
3912
4483
  const stampedFindings = stampConvergence(findings, convergence);
3913
4484
  const currentRoundCount = isRound ? roundNumber : priorRoundCount;
3914
- const findingsMarker = findingsBlob(stampedFindings);
4485
+ const findingsMarker = findingsBlob(
4486
+ stampedFindings,
4487
+ answerable && dialogueRound ? dialogueMarker(dialogue, priorDialogue.undecoded) : carriedDialogue
4488
+ );
3915
4489
  const commonRenderInput = {
3916
4490
  ...sharedRenderInput,
3917
- findings: stampedFindings,
4491
+ findings: withoutIds(stampedFindings, contestedIds),
3918
4492
  envelope,
3919
4493
  incomplete: thisIncomplete,
3920
4494
  sameRootNotes,
@@ -4122,6 +4696,24 @@ ${note}
4122
4696
  finalDiscussionOverride
4123
4697
  )
4124
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
+ }
4125
4717
  };
4126
4718
  var noticeBody = (lead, existingBody) => {
4127
4719
  const carried = existingBody ? carryForwardMarkers(existingBody) : "";
@@ -4523,7 +5115,10 @@ var IssueCommentCodec = t.intersection([
4523
5115
  t.type({
4524
5116
  id: t.number,
4525
5117
  body: t.union([t.string, t.null]),
4526
- user: t.type({ login: t.string })
5118
+ user: t.intersection([
5119
+ t.type({ login: t.string }),
5120
+ t.partial({ type: t.union([t.string, t.null]) })
5121
+ ])
4527
5122
  }),
4528
5123
  t.partial({
4529
5124
  created_at: t.union([t.string, t.null]),
@@ -4577,13 +5172,16 @@ var fetchFullDiff = async (repo, defaultBranch, headSha, ghApi, gitRun) => {
4577
5172
  const base = (await gitRun(["rev-parse", "HEAD"])).trim();
4578
5173
  return gitRun(["diff", `${base}...${headSha}`]);
4579
5174
  };
4580
- var CommitCodec = t.type({
4581
- sha: t.string,
4582
- message: t.string,
4583
- author: t.union([t.string, t.null]),
4584
- email: t.union([t.string, t.null])
4585
- });
4586
- var COMMIT_JQ = ".commits[] | {sha: .sha, message: .commit.message, author: .commit.author.name, email: .commit.author.email}";
5175
+ var CommitCodec = t.intersection([
5176
+ t.type({
5177
+ sha: t.string,
5178
+ message: t.string,
5179
+ author: t.union([t.string, t.null]),
5180
+ email: t.union([t.string, t.null])
5181
+ }),
5182
+ t.partial({ date: t.union([t.string, t.null]), login: t.union([t.string, t.null]) })
5183
+ ]);
5184
+ var COMMIT_JQ = ".commits[] | {sha: .sha, message: .commit.message, author: .commit.author.name, email: .commit.author.email, date: .commit.author.date, login: (.author.login // null)}";
4587
5185
  var fetchCompareCommits = async (repo, defaultBranch, headSha, ghApi) => {
4588
5186
  const rows = parseJsonl(
4589
5187
  await ghApi([
@@ -4604,8 +5202,8 @@ var fetchCompareCommits = async (repo, defaultBranch, headSha, ghApi) => {
4604
5202
  }
4605
5203
  return commits;
4606
5204
  };
4607
- var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
4608
- var REVIEW_COMMENT_JQ = THREAD_COMMENT_JQ;
5205
+ var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login, type: .user.type}, created_at: .created_at, author_association: .author_association}";
5206
+ var REVIEW_COMMENT_JQ = ".[] | {body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association, path: .path, line: .line}";
4609
5207
  var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
4610
5208
  var ReviewCommentCodec = t.intersection([
4611
5209
  t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
@@ -4788,7 +5386,6 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
4788
5386
  ]);
4789
5387
  const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
4790
5388
  const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
4791
- const threadComments = decodeArrayOrNull(ThreadCommentCodec, reviewCommentRows);
4792
5389
  const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
4793
5390
  const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
4794
5391
  writeFileSync(
@@ -4802,11 +5399,40 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit, readArtifact = ghA
4802
5399
  join(input.outDir, "prior_findings.json"),
4803
5400
  priorFindings === null ? "null" : JSON.stringify(priorFindings)
4804
5401
  );
4805
- const answered = threadComments === null ? [] : answeredRegistryFrom(threadComments, input.botLogin);
5402
+ const held = (seedsFromPrior ? parseDialogueMarker(prior.body).entries : []).filter(
5403
+ (entry) => entry.state !== "rebutted"
5404
+ );
5405
+ const harvest = seedsFromPrior ? harvestResponses({
5406
+ repo: input.repo,
5407
+ prNumber,
5408
+ botLogin: input.botLogin,
5409
+ priorIds: /* @__PURE__ */ new Set([...priorIdsFrom(priorFindings), ...held.map((entry) => entry.id)]),
5410
+ commits: commits.map((commit) => ({ ...commit, author: commit.login ?? null })),
5411
+ comments: issueComments ?? []
5412
+ }) : { file: { responses: [], unmatched: [] }, dropped: 0 };
4806
5413
  writeFileSync(
4807
- join(input.outDir, "answered.json"),
4808
- JSON.stringify(answered.map(encodeAnsweredEntry))
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
+ })
4809
5424
  );
5425
+ if (seedsFromPrior && priorFindings === null) {
5426
+ process.stderr.write(
5427
+ "Note: the prior review's findings did not resolve \u2014 every id-shaped answer is staged as unmatched\n"
5428
+ );
5429
+ }
5430
+ if (harvest.dropped > 0) {
5431
+ process.stderr.write(
5432
+ `Note: ${String(harvest.dropped)} answer(s) beyond the newest ${String(RESPONSES_PER_CHANNEL)} per channel were not staged
5433
+ `
5434
+ );
5435
+ }
4810
5436
  writeFileSync(
4811
5437
  join(input.outDir, "pr_conversation.json"),
4812
5438
  JSON.stringify({
@@ -5170,7 +5796,7 @@ var parseScope = (raw) => {
5170
5796
  reason: 'scope contains a character (newline, carriage return, backtick, "<", ">", or "|") that would corrupt the review prompt \u2014 use plain language names/tags'
5171
5797
  };
5172
5798
  }
5173
- const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t8) => t8 !== "")));
5799
+ const languages = Array.from(new Set(trimmed.split(SCOPE_SEPARATOR_RE).filter((t10) => t10 !== "")));
5174
5800
  return languages.length === 0 ? { kind: "absent" } : { kind: "ok", languages };
5175
5801
  };
5176
5802
 
@@ -5798,18 +6424,19 @@ var deadlineCmd = defineCommand({
5798
6424
  }
5799
6425
  });
5800
6426
  var derivedSchemaVersion = (kind, raw) => kind === "findings" ? declaredVersion(raw) : void 0;
6427
+ var liveSchemaPin = (kind, schemaPath, requestedVersion) => kind === "findings" && schemaPath === schemaPathFor(kind) ? livePinFor(kind, requestedVersion) : void 0;
5801
6428
  var printableSchema = (schemaPath, pinVersion) => {
5802
6429
  const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
5803
6430
  const enforcementSchema = Object.fromEntries(
5804
6431
  Object.entries(schema).filter(([key2]) => key2 !== "$schema")
5805
6432
  );
5806
- if (!pinVersion) return JSON.stringify(enforcementSchema, null, 2);
6433
+ if (pinVersion === void 0) return JSON.stringify(enforcementSchema, null, 2);
5807
6434
  const properties = enforcementSchema["properties"];
5808
6435
  const schemaVersion = typeof properties === "object" && properties !== null ? properties["schema_version"] : void 0;
5809
6436
  if (typeof schemaVersion === "object" && schemaVersion !== null) {
5810
6437
  properties["schema_version"] = {
5811
6438
  ...schemaVersion,
5812
- pattern: anchoredSchemaVersionPattern(DEFAULT_SCHEMA_VERSION)
6439
+ pattern: anchoredSchemaVersionPattern(pinVersion)
5813
6440
  };
5814
6441
  }
5815
6442
  return JSON.stringify(enforcementSchema, null, 2);
@@ -5845,7 +6472,8 @@ var validateCmd = defineCommand({
5845
6472
  run: async ({ args }) => {
5846
6473
  const kind = requireSchemaKind(args.kind || "findings");
5847
6474
  const documentRaw = readJSON(args.document);
5848
- const schemaPath = args.schema ? resolve(args.schema) : requireSchemaPath(kind, args["schema-version"] || derivedSchemaVersion(kind, documentRaw));
6475
+ const requestedVersion = args["schema-version"] || derivedSchemaVersion(kind, documentRaw);
6476
+ const schemaPath = args.schema ? resolve(args.schema) : requireSchemaPath(kind, requestedVersion);
5849
6477
  const { valid, errors } = validateAgainstSchema(documentRaw, schemaPath);
5850
6478
  if (valid) {
5851
6479
  process.stdout.write("\u2705 valid\n");
@@ -5857,7 +6485,7 @@ var validateCmd = defineCommand({
5857
6485
  process.stderr.write(
5858
6486
  `
5859
6487
  The ${kind} document must conform to this schema (the field descriptions are the authoritative spec \u2014 match the property names exactly):
5860
- ${printableSchema(schemaPath, kind === "findings" && schemaPath === schemaPathFor(kind))}
6488
+ ${printableSchema(schemaPath, liveSchemaPin(kind, schemaPath, requestedVersion))}
5861
6489
  `
5862
6490
  );
5863
6491
  }
@@ -5881,10 +6509,6 @@ var seedDraftCmd = defineCommand({
5881
6509
  type: "string",
5882
6510
  description: "Path to the gather-staged prior findings document (prior_findings.json). gather resolves it \u2014 from the sticky's embedded blob, or by fetching the artifact the marker names \u2014 because gather holds the repo token and this step deliberately does not: it runs the jailed agent over untrusted PR code. Absent or null falls back to decoding an embedded blob out of --prior, which needs no token"
5883
6511
  },
5884
- "prior-answers": {
5885
- type: "string",
5886
- description: "Path to the gather-staged answered-findings registry (answered.json) \u2014 the prior inline findings whose threads a human reply answered (issue #151). Delivered out-of-band to the .prior-answers sidecar beside the prior context so the next-round agent sees the already-answered state; best-effort, never fails the seed"
5887
- },
5888
6512
  "nit-visibility-floor": {
5889
6513
  type: "string",
5890
6514
  description: "The nit visibility floor (issue #164), matched to the commenter's: the prior review's below-floor nits (confidence \xD7 likelihood below it) are re-derived from --prior and delivered to the .prior-suppressed sidecar as adjudicated context, so the next-round agent does not re-raise them as fresh nits; best-effort, never fails the seed. Empty \u21D2 the default"
@@ -5960,55 +6584,17 @@ var seedDraftCmd = defineCommand({
5960
6584
  const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
5961
6585
  isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
5962
6586
  );
5963
- const answeredRegistry = (() => {
5964
- if (!args["prior-answers"]) return null;
5965
- try {
5966
- const raw = JSON.parse(readFileSync(resolve(args["prior-answers"]), "utf-8"));
5967
- if (!Array.isArray(raw)) throw new Error("expected an array");
5968
- const decoded = raw.flatMap((row) => {
5969
- const entry = decodeAnsweredEntry(row);
5970
- return entry === null ? [] : [entry];
5971
- });
5972
- if (decoded.length < raw.length) {
5973
- process.stderr.write(
5974
- `Warning: ${String(raw.length - decoded.length)} of ${String(raw.length)} answered-registry row(s) failed to decode \u2014 the seed's answered state is incomplete
5975
- `
5976
- );
5977
- }
5978
- return decoded;
5979
- } catch (err) {
5980
- process.stderr.write(
5981
- `Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
5982
- `
5983
- );
5984
- return null;
5985
- }
5986
- })();
5987
6587
  const priorFindings = (() => {
5988
6588
  if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
5989
6589
  return strippedPrior;
5990
6590
  const resolved = resolvedPriorValue(strippedPrior);
5991
- const doc = resolved === null ? strippedPrior : {
5992
- ...resolved,
5993
- findings: answeredRegistry !== null && answeredRegistry.length > 0 ? resolved.findings.filter((f) => {
5994
- const resolvedId = resolveFindingId(f);
5995
- return !answeredRegistry.some((e) => isAnsweredDrop(resolvedId, f, e));
5996
- }) : resolved.findings
5997
- };
6591
+ const doc = resolved === null ? strippedPrior : resolved;
5998
6592
  const carried = doc["scope_metastasis"];
5999
6593
  if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return doc;
6000
6594
  if (doc["verdict"] === "error") return doc;
6001
6595
  const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
6002
6596
  return computed === null ? doc : { ...doc, scope_metastasis: computed };
6003
6597
  })();
6004
- if (answeredRegistry !== null) {
6005
- writeFileSync(priorAnswersPath(outPath), `${JSON.stringify(answeredRegistry, null, 2)}
6006
- `);
6007
- process.stderr.write(
6008
- `Seeded ${priorAnswersPath(outPath)} with ${String(answeredRegistry.length)} answered finding(s) as context
6009
- `
6010
- );
6011
- }
6012
6598
  if (parsedPrior !== null && isFullReviewAncestry(priorBody ?? "")) {
6013
6599
  try {
6014
6600
  const belowFloor = priorBelowFloorNits(
@@ -6040,7 +6626,9 @@ var seedDraftCmd = defineCommand({
6040
6626
  ) : priorFindings;
6041
6627
  const accepts = (doc) => {
6042
6628
  const resolved = resolvedPriorValue(doc);
6043
- return resolved !== null && validateAgainstSchema(resolved, schemaPath).valid ? resolved : null;
6629
+ if (resolved === null) return null;
6630
+ const carried = withoutRebuttals(resolved);
6631
+ return validateAgainstSchema(carried, schemaPath).valid ? carried : null;
6044
6632
  };
6045
6633
  const seedDoc = accepts(priorFindings) ?? (() => {
6046
6634
  const bare = accepts(barePrior);
@@ -6328,7 +6916,7 @@ var printSchemaCmd = defineCommand({
6328
6916
  const schemaKind = requireSchemaKind(args.name);
6329
6917
  const schemaPath = requireSchemaPath(schemaKind, args["schema-version"]);
6330
6918
  process.stdout.write(
6331
- `${printableSchema(schemaPath, schemaKind === "findings" && schemaPath === schemaPathFor(schemaKind))}
6919
+ `${printableSchema(schemaPath, liveSchemaPin(schemaKind, schemaPath, args["schema-version"]))}
6332
6920
  `
6333
6921
  );
6334
6922
  }
@@ -6580,6 +7168,10 @@ var postCmd = defineCommand({
6580
7168
  "unverified-no-logs": {
6581
7169
  type: "boolean",
6582
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"
6583
7175
  }
6584
7176
  },
6585
7177
  run: async ({ args }) => {
@@ -6614,6 +7206,7 @@ var postCmd = defineCommand({
6614
7206
  nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
6615
7207
  inline: args.inline,
6616
7208
  unverifiedNoLogs: args["unverified-no-logs"],
7209
+ withoutDialogue: args["without-dialogue"],
6617
7210
  postedAt: formatUtc(/* @__PURE__ */ new Date()),
6618
7211
  pricedAt: /* @__PURE__ */ new Date()
6619
7212
  });