@jphutchins/code-review 0.1.0-alpha.8 → 0.1.0-alpha.9

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
@@ -89,6 +89,10 @@ var drop = (reason) => ({
89
89
  kind: "drop",
90
90
  reason
91
91
  });
92
+ var keep = (reason) => ({
93
+ kind: "keep",
94
+ reason
95
+ });
92
96
  var parseHunk = (patch) => {
93
97
  const rawLines = patch.split("\n");
94
98
  const lines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
@@ -128,10 +132,12 @@ var validatePatch = (patch, fileLines) => {
128
132
  const removedCount = body.filter((l) => l.kind === "removed").length;
129
133
  const addedCount = body.filter((l) => l.kind === "added").length;
130
134
  if (removedCount === 0 && addedCount === 0) return drop("hunk contains no changes");
131
- if (removedCount === 0) return drop("pure insertion has no range to anchor a suggestion");
135
+ if (removedCount === 0) {
136
+ return keep("pure insertion applies cleanly but has no removed range to anchor a suggestion");
137
+ }
132
138
  const { firstRemoved, lastRemoved } = removedRange(body, oldStart);
133
139
  if (firstRemoved === null || lastRemoved === null) return drop("malformed hunk body");
134
- return { startLine: firstRemoved, endLine: lastRemoved };
140
+ return { kind: "anchored", startLine: firstRemoved, endLine: lastRemoved };
135
141
  };
136
142
  var patchToSuggestion = (patch) => {
137
143
  const parsed = parseHunk(patch);
@@ -164,12 +170,14 @@ var severityEmoji = (s) => {
164
170
  };
165
171
  var EMBED_LIMIT = 4e4;
166
172
  var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
167
- var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => {
168
- const b64 = Buffer.from(JSON.stringify(findings), "utf-8").toString("base64");
173
+ var encodeMarker = (document, jsonUrl, limit) => {
174
+ const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
169
175
  const marker = b64.length <= limit ? `<!-- code-review:findings-json;base64 ${b64} -->` : jsonUrl ? `<!-- code-review:findings-json ${jsonUrl} -->` : "";
170
176
  return marker ? `${AGENTS_STOP_DIRECTIVE}
171
177
  ${marker}` : "";
172
178
  };
179
+ var findingsPointer = (findings, jsonUrl, limit = EMBED_LIMIT) => encodeMarker(findings, jsonUrl, limit);
180
+ var findingPointer = (finding, schemaVersion, jsonUrl, limit = EMBED_LIMIT) => encodeMarker({ schema_version: schemaVersion, findings: [finding] }, jsonUrl, limit);
173
181
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
174
182
  var projectPatch = (patch) => {
175
183
  if (patch === void 0) return { kind: "none" };
@@ -330,8 +338,8 @@ var buildInlineComments = (findings, diff, context) => {
330
338
  const { inDiff, strays } = partitionFindings(findings, index);
331
339
  const eta = new Eta({ autoTrim: false });
332
340
  const modelsText = formatModels(models);
333
- const pointer = context.findingsPointer ?? (fullFindings ? findingsPointer(fullFindings, jsonUrl) : "");
334
341
  const comments = inDiff.map((f) => {
342
+ const pointer = fullFindings ? findingPointer(f, fullFindings.schema_version, jsonUrl) : "";
335
343
  const comment = {
336
344
  path: f.path,
337
345
  line: f.end_line,
@@ -868,6 +876,84 @@ var dismissReviews = async (repo, prNumber, ids, ghApi) => {
868
876
  }
869
877
  }
870
878
  };
879
+ var REVIEW_THREAD_COMMENTS_QUERY = "query($owner:String!,$name:String!,$pr:Int!){repository(owner:$owner,name:$name){pullRequest(number:$pr){reviewThreads(first:100){pageInfo{hasNextPage}nodes{comments(first:100){nodes{id isMinimized author{login} originalCommit{oid}}}}}}}}";
880
+ var MINIMIZE_COMMENT_MUTATION = "mutation($id:ID!){minimizeComment(input:{subjectId:$id,classifier:OUTDATED}){minimizedComment{isMinimized}}}";
881
+ var supersededCommentId = (c, headSha, logins) => {
882
+ if (typeof c !== "object" || c === null) return null;
883
+ const o = c;
884
+ const login = o.author?.login;
885
+ const oid = o.originalCommit?.oid;
886
+ return typeof o.id === "string" && o.isMinimized !== true && typeof login === "string" && logins.includes(login) && typeof oid === "string" && oid !== headSha ? o.id : null;
887
+ };
888
+ var supersededBotCommentIds = (raw, headSha, botLogin) => {
889
+ let parsed;
890
+ try {
891
+ parsed = JSON.parse(raw);
892
+ } catch {
893
+ return { ids: [], truncated: false };
894
+ }
895
+ const conn = parsed.data?.repository?.pullRequest?.reviewThreads;
896
+ const truncated = conn?.pageInfo?.hasNextPage === true;
897
+ const nodes = conn?.nodes;
898
+ if (!Array.isArray(nodes)) return { ids: [], truncated };
899
+ const logins = [botLogin.replace(/\[bot\]$/, ""), botLogin];
900
+ const ids = nodes.flatMap((t4) => {
901
+ const cnodes = t4.comments?.nodes;
902
+ return Array.isArray(cnodes) ? cnodes.map((c) => supersededCommentId(c, headSha, logins)).filter((id) => id !== null) : [];
903
+ });
904
+ return { ids, truncated };
905
+ };
906
+ var minimizeSupersededComments = async (repo, prNumber, headSha, botLogin, ghApi) => {
907
+ const slash = repo.indexOf("/");
908
+ if (slash <= 0) return;
909
+ const owner = repo.slice(0, slash);
910
+ const name = repo.slice(slash + 1);
911
+ let raw;
912
+ try {
913
+ raw = await ghApi([
914
+ "graphql",
915
+ "-f",
916
+ `query=${REVIEW_THREAD_COMMENTS_QUERY}`,
917
+ "-f",
918
+ `owner=${owner}`,
919
+ "-f",
920
+ `name=${name}`,
921
+ "-F",
922
+ `pr=${String(prNumber)}`
923
+ ]);
924
+ } catch (err) {
925
+ process.stderr.write(
926
+ `Warning: could not list review threads to minimize stale comments on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
927
+ `
928
+ );
929
+ return;
930
+ }
931
+ const { ids, truncated } = supersededBotCommentIds(raw, headSha, botLogin);
932
+ if (truncated) {
933
+ process.stderr.write(
934
+ `Note: PR #${String(prNumber)} has more than 100 review threads \u2014 only the first 100 were scanned for stale bot comments
935
+ `
936
+ );
937
+ }
938
+ let minimized = 0;
939
+ for (const id of ids) {
940
+ try {
941
+ await ghApi(["graphql", "-f", `query=${MINIMIZE_COMMENT_MUTATION}`, "-f", `id=${id}`]);
942
+ minimized += 1;
943
+ } catch (err) {
944
+ process.stderr.write(
945
+ `Warning: failed to minimize a stale review comment on PR #${String(prNumber)}: ${err instanceof Error ? err.message : String(err)}
946
+ `
947
+ );
948
+ }
949
+ }
950
+ if (minimized > 0) {
951
+ process.stderr.write(
952
+ `Minimized ${String(minimized)} stale inline comment(s) from superseded reviews on PR #${String(prNumber)}
953
+ `
954
+ );
955
+ }
956
+ };
871
957
  var post = async (input, ghApi = runGhApi) => {
872
958
  const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
873
959
  const resolution = resolvePr(candidates, input.headBranch);
@@ -966,7 +1052,8 @@ var post = async (input, ghApi = runGhApi) => {
966
1052
  const { comments: rawComments, strays } = buildInlineComments(findings.findings, diff, {
967
1053
  inlineTemplate,
968
1054
  models: envelope.models.map((m) => m.model),
969
- findingsPointer: findingsMarker
1055
+ findings,
1056
+ jsonUrl: input.jsonUrl
970
1057
  });
971
1058
  const { comments, longFiles } = checkLongSuggestions(rawComments);
972
1059
  for (const wf of longFiles) {
@@ -1034,6 +1121,7 @@ var post = async (input, ghApi = runGhApi) => {
1034
1121
  `Posted ${String(comments.length)} inline comments on PR #${String(prNumber)}
1035
1122
  `
1036
1123
  );
1124
+ await minimizeSupersededComments(input.repo, prNumber, input.headSha, input.botLogin, ghApi);
1037
1125
  if (stickyRef !== null) {
1038
1126
  const confirmedDisposition = {
1039
1127
  kind: "posted",
@@ -1836,19 +1924,23 @@ var validateFinding = (finding, repoRoot) => {
1836
1924
  return withoutPatch(finding);
1837
1925
  }
1838
1926
  const result = validatePatch(finding.patch, lines);
1839
- if ("kind" in result) {
1840
- process.stderr.write(
1841
- `validate-patches: ${finding.path}:${String(finding.start_line)}: ${result.reason} \u2014 dropping patch
1927
+ switch (result.kind) {
1928
+ case "anchored":
1929
+ return { ...finding, start_line: result.startLine, end_line: result.endLine };
1930
+ case "keep":
1931
+ return finding;
1932
+ case "drop":
1933
+ process.stderr.write(
1934
+ `validate-patches: ${finding.path}:${String(finding.start_line)}: ${result.reason} \u2014 dropping patch
1842
1935
  `
1843
- );
1844
- return withoutPatch(finding);
1936
+ );
1937
+ return withoutPatch(finding);
1845
1938
  }
1846
- return { ...finding, start_line: result.startLine, end_line: result.endLine };
1847
1939
  };
1848
1940
  var validatePatchesCmd = defineCommand({
1849
1941
  meta: {
1850
1942
  name: "validate-patches",
1851
- description: "Validate each finding's patch against the real PR-head tree, aligning the finding's range to it and keeping the patch, or dropping the patch (issue #10)"
1943
+ description: "Validate each finding's patch against the real PR-head tree, aligning the finding's range and keeping the patch when it anchors, keeping it unaligned when it's a pure insertion, or dropping it when it doesn't apply (issue #10)"
1852
1944
  },
1853
1945
  args: {
1854
1946
  findings: {