co-maintainer 0.4.0-beta.1

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.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,582 @@
1
+ import { OpenRouterProvider } from "../ai/openrouter.js";
2
+ import { completeWithMermaidTools, } from "../ai/mermaid_loop.js";
3
+ import { loadGuides } from "../review/guides.js";
4
+ import { computeScope } from "./scope.js";
5
+ import { prepareCodegraphTools } from "./codegraph_tools.js";
6
+ import { needsSummary, READ_FULL_DIFF_TOOL, readFullDiff, summarizeDiff, } from "./diff_summary.js";
7
+ import { numberPatch } from "./hunks.js";
8
+ const MAX_REVIEW_DIFF_CHARS = 240_000;
9
+ export const MERMAID_GUIDANCE = `Mermaid selection and minimal syntax:
10
+ flowchart = decisions, branches, pipelines, and fallback paths;
11
+ swimlane-beta = work crossing owners, actors, services, teams, or layers;
12
+ sequenceDiagram = ordered calls, webhooks, retries, responses, and timing;
13
+ classDiagram = class, interface, type, inheritance, or composition relationships;
14
+ stateDiagram-v2 = lifecycle and state transitions;
15
+ erDiagram = database entities, keys, and cardinality;
16
+ requirementDiagram = requirements linked to tests or implementation;
17
+ usecase-beta = actors and system capabilities;
18
+ C4Context = users, systems, boundaries, and integrations;
19
+ zenuml = compact nested call sequences;
20
+ packet = binary fields, bit ranges, and protocol layout;
21
+ architecture-beta = services, containers, storage, and deployment topology;
22
+ eventmodeling = commands, events, processors, read models, and timelines;
23
+ treeView-beta = directory, file, module, or dependency hierarchy.
24
+ If a diagram is useful, call read-mermaid-syntaxes before writing any Mermaid.
25
+ Request every type you need in a single call, because repeated tool rounds are
26
+ capped. Use the smallest type that matches the evidence.`;
27
+ const REVIEW_ROLE = `You are a precise open-source code reviewer.
28
+ Evidence must come from the supplied diff and review guide. Keep findings
29
+ concise by default.`;
30
+ const REVIEW_DIAGRAM_RULES = `People generally find it easier to understand the
31
+ problem you've identified when it's presented in diagrams. When a multi-step
32
+ flow, lifecycle, dependency, data model, protocol, or architecture change is
33
+ part of the finding, you are expected to draw it — do not skip the diagram
34
+ just to avoid the extra tool call. During the initial review, use at most one
35
+ diagram, so spend it on the finding that benefits most. If a user later asks
36
+ for detailed reasoning in a reply, that reply may use up to five diagrams, but
37
+ only when each adds a distinct useful view.
38
+ ${MERMAID_GUIDANCE}
39
+ Do not invent nodes, actors, states, services, tables, or events. Omit the
40
+ diagram only when no finding actually fits one of the categories above, or
41
+ when the syntax is genuinely uncertain.`;
42
+ /** Without tool support the model cannot read the Mermaid syntax docs, so
43
+ * asking for a diagram only invites invented syntax. */
44
+ export const NO_DIAGRAM_RULES = `Do not use Mermaid or any other diagram. Explain with prose only.`;
45
+ const DIAGRAM_PROMPT_RULES = `People generally find it easier to understand the
46
+ problem you've identified when it's presented in diagrams. When a finding
47
+ involves a multi-step flow, lifecycle, dependency, data model, protocol, or
48
+ architecture change, you are expected to include a Mermaid fenced code block
49
+ for it — reading the syntax with read-mermaid-syntaxes first is a small cost,
50
+ not a reason to skip the diagram. This review may contain at most one diagram
51
+ in total, so if more than one finding qualifies, pick the one the diagram
52
+ clarifies most. Its type must be one of the types named in the system
53
+ instructions. Keep labels short and grounded in the supplied evidence. A
54
+ one-line fix or an obvious, single-step cause and effect genuinely needs no
55
+ diagram — that is the only reason to omit one. Close every fenced block.`;
56
+ export function reviewSystemPrompt(diagrams) {
57
+ return `${REVIEW_ROLE}
58
+ ${diagrams ? REVIEW_DIAGRAM_RULES : NO_DIAGRAM_RULES}`;
59
+ }
60
+ /** Shared PR and local/remote workspace review instructions: confirm claims
61
+ * against the indexed graph, not only the diff slice. */
62
+ export const CODEGRAPH_DIFF_VERIFICATION = `Examine the changes line by line, not just file by file — a single file can
63
+ contain more than one independent defect, and a change that looks fine in
64
+ isolation can be wrong once you trace what calls it or what else it affects.
65
+ When a finding depends on behavior outside the changed lines, confirm it with
66
+ codegraph-node, codegraph-callers, codegraph-callees, codegraph-impact, and
67
+ codegraph-affected before reporting it. Drop or correct findings that only seem
68
+ plausible from the diff but contradict unchanged callers, callees, or the same
69
+ pattern elsewhere in the repo. Use those tools to check blast radius and whether
70
+ a test reaches the path — do not guess coverage or impact from the diff alone
71
+ when a tool can answer. A missing regression test is not a substitute for
72
+ identifying the concrete input or code path that misbehaves when you can.`;
73
+ function text(value, limit = 20_000) {
74
+ const result = String(value ?? "");
75
+ return result.length > limit
76
+ ? `${result.slice(0, limit)}\n[truncated]`
77
+ : result;
78
+ }
79
+ const MAX_FILE_PATCH_CHARS = 12_000;
80
+ /** GitHub omits `patch` entirely for files it considers too large, and the file
81
+ * still appears in the compare response with only its counts. Rendering that as
82
+ * an empty body reads as "this file did not change", and a reviewer then
83
+ * reports the absence as a finding (a lockfile that was in fact regenerated,
84
+ * say). Say plainly that the hunks are missing, and mark a per-file truncation
85
+ * for the same reason. */
86
+ export function filePatch(file) {
87
+ const patch = String(file.patch ?? "");
88
+ const status = String(file.status ?? "modified");
89
+ const changes = Number(file.changes ?? 0);
90
+ const additions = Number(file.additions ?? 0);
91
+ const deletions = Number(file.deletions ?? 0);
92
+ if (patch === "") {
93
+ const counts = Number.isFinite(changes) && changes > 0
94
+ ? `${changes} changed lines (+${additions} -${deletions})`
95
+ : "an unreported number of changed lines";
96
+ return (`[${status}; ${counts}; diff withheld by GitHub, not shown here. ` +
97
+ `Do not treat this file as unchanged and do not report its contents.]`);
98
+ }
99
+ if (patch.length > MAX_FILE_PATCH_CHARS) {
100
+ return (`${numberPatch(patch.slice(0, MAX_FILE_PATCH_CHARS))}\n[${status}; ${changes} ` +
101
+ `changed lines total; this file's diff is cut off here, later hunks are ` +
102
+ `not shown.]`);
103
+ }
104
+ return numberPatch(patch);
105
+ }
106
+ // Uncapped, a 100,000-line diff would ask for 1,000+ tool-loop rounds, each
107
+ // able to spend multiple external calls.
108
+ export function clampToolRounds(totalDiffLines) {
109
+ return Math.min(Math.round(4 + totalDiffLines / 100), 8);
110
+ }
111
+ // Uncapped, --improve-matrix scales maxTokens past what most providers'
112
+ // 128k-token context can hold on its own, before the prompt even counts.
113
+ export function clampImproveMatrix(matrix) {
114
+ return Math.min(Math.max(1, matrix), 4);
115
+ }
116
+ export async function readGuide(repo, name) {
117
+ const guides = await loadGuides(repo);
118
+ switch (name) {
119
+ case "PR_REVIEW_GUIDE.md":
120
+ return guides.shortGuide;
121
+ case "PR_REVIEW_DETAILED_GUIDE.md":
122
+ return guides.detailed;
123
+ case "CODEBASE.md":
124
+ return guides.codebase;
125
+ case "SKILL.md":
126
+ return guides.skill;
127
+ default:
128
+ return "";
129
+ }
130
+ }
131
+ export async function reviewPullRequest(client, options, usage, snapshot, ai, progress, extras) {
132
+ if (!options.prNumber)
133
+ throw new Error("review requires a PR number");
134
+ const number = options.prNumber;
135
+ const report = progress ?? (() => { });
136
+ report(`loading PR context for ${options.repo}#${number}`);
137
+ const [pr, allComments, allReviews, guides] = await Promise.all([
138
+ client.request(`repos/${options.repo}/pulls/${number}`),
139
+ client.pages(`repos/${options.repo}/issues/${number}/comments`),
140
+ client.pages(`repos/${options.repo}/pulls/${number}/reviews`),
141
+ loadGuides(options.repo),
142
+ ]);
143
+ const { shortGuide, detailed, codebase, skill } = guides;
144
+ report(`context loaded · comments=${allComments.length} · reviews=${allReviews.length} · ` +
145
+ `guide=${shortGuide.length || skill.length} chars · codebase=${codebase.length} chars`);
146
+ const guide = shortGuide || skill;
147
+ const before = (item) => !snapshot || String(item.created_at ?? "") < snapshot.before;
148
+ const comments = allComments.filter(before);
149
+ const reviews = allReviews.filter(before);
150
+ const files = snapshot
151
+ ? ((await client.request(`repos/${options.repo}/compare/${snapshot.base ??
152
+ String(pr.base?.ref ?? "main")}...${snapshot.commit}`)).files ?? [])
153
+ : await client.pages(`repos/${options.repo}/pulls/${number}/files`);
154
+ report(`diff files loaded · ${files.length} files`);
155
+ if (!guide) {
156
+ throw new Error(`repos/${options.repo}/PR_REVIEW_GUIDE.md was not found; run init first`);
157
+ }
158
+ if (options.debug) {
159
+ console.log(`[debug] guides · short=${guide.length} chars · detailed=${detailed.length} chars · codebase=${codebase.length} chars`);
160
+ console.log(`[debug] PR #${number} · comments=${comments.length} · reviews=${reviews.length} · files=${files.length}`);
161
+ }
162
+ // What the author actually wrote this round, as opposed to code that
163
+ // arrived by merging the default branch in — see scope.ts. A re-review
164
+ // round's diff regularly carries a `merge main` that dwarfs the PR's own
165
+ // change and that no human reviewer reads either; --review-upstream turns
166
+ // this off and reviews everything, matching the pre-scope behavior.
167
+ const headSha = snapshot?.commit ?? String(pr.head?.sha ?? "");
168
+ const baseRevision = snapshot?.base ?? String(pr.base?.ref ?? "main");
169
+ if (options.reviewUpstream) {
170
+ report("scope skipped · --review-upstream");
171
+ }
172
+ else if (!headSha) {
173
+ report("scope skipped · no head commit for this pull request");
174
+ }
175
+ const scope = options.reviewUpstream || !headSha
176
+ ? undefined
177
+ : await computeScope(options.repo, baseRevision, headSha);
178
+ if (!options.reviewUpstream && headSha && !scope) {
179
+ report("scope unavailable · reviewing every changed file");
180
+ }
181
+ const named = files.map((file) => {
182
+ const value = file;
183
+ return { file: value, path: String(value.filename ?? "") };
184
+ });
185
+ const unchanged = new Set(extras?.unchangedPaths ?? []);
186
+ const ownFiles = (scope ? named.filter(({ path }) => !scope.upstreamFiles.has(path)) : named).filter(({ path }) => !unchanged.has(path));
187
+ const upstreamFiles = scope
188
+ ? named.filter(({ path }) => scope.upstreamFiles.has(path))
189
+ : [];
190
+ if (scope) {
191
+ report(`scope resolved · own=${ownFiles.length} files · upstream=${upstreamFiles.length} files`);
192
+ }
193
+ // Built only if needed: OpenRouterProvider's constructor requires a real
194
+ // API key, which a fake-AI test run never has.
195
+ const filesNeedSummary = ownFiles.some(({ file }) => needsSummary(Number(file.changes ?? 0), String(file.patch ?? "")));
196
+ const lowProvider = filesNeedSummary
197
+ ? (ai ??
198
+ new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
199
+ : undefined;
200
+ const patchByPath = new Map();
201
+ const [ownSections, codegraphTools] = await Promise.all([
202
+ Promise.all(ownFiles.map(async ({ file, path }) => {
203
+ const changes = Number(file.changes ?? 0);
204
+ const patch = String(file.patch ?? "");
205
+ if (!needsSummary(changes, patch)) {
206
+ return `FILE: ${path}\n${filePatch(file)}`;
207
+ }
208
+ patchByPath.set(path, patch);
209
+ const description = await summarizeDiff(path, patch, lowProvider, usage);
210
+ report(`summarized large diff · ${path} · ${changes} changed lines`);
211
+ return (`FILE: ${path}\n[${changes} changed lines — summarized below; ` +
212
+ `call read-full-diff("${path}") for the complete diff if this is not ` +
213
+ `enough]\n${description}`);
214
+ })),
215
+ options.useCodegraph && headSha
216
+ ? prepareCodegraphTools(options.repo, number, headSha)
217
+ : Promise.resolve([]),
218
+ ]);
219
+ if (options.useCodegraph) {
220
+ report(`codegraph tools · ${codegraphTools.length > 0 ? "ready" : "unavailable"}`);
221
+ }
222
+ const extraTools = [
223
+ ...codegraphTools,
224
+ ...(patchByPath.size > 0
225
+ ? [
226
+ {
227
+ name: "read-full-diff",
228
+ tool: READ_FULL_DIFF_TOOL,
229
+ run: (args) => readFullDiff(patchByPath, args),
230
+ },
231
+ ]
232
+ : []),
233
+ ];
234
+ // Bigger own-scope diffs need more codegraph round-trips to trace — fixed
235
+ // at 3 rounds regardless of size caused the model to run out mid-review on
236
+ // large PRs (verified via a tokio benchmark run: it hallucinated fake
237
+ // tool-call text instead of a finding once tools were dropped).
238
+ const totalDiffLines = ownFiles.reduce((sum, { file }) => sum + Number(file.changes ?? 0), 0);
239
+ const maxToolRounds = clampToolRounds(totalDiffLines);
240
+ report(`tool rounds · ${maxToolRounds} (own diff ${totalDiffLines} changed lines)`);
241
+ const ownPatch = ownSections.join("\n\n");
242
+ const diffWasTruncated = ownPatch.length > MAX_REVIEW_DIFF_CHARS;
243
+ const ownDiff = text(ownPatch, MAX_REVIEW_DIFF_CHARS);
244
+ const unchangedListing = extras?.unchangedPaths?.length
245
+ ? `\nUNCHANGED SINCE LAST REVIEW (paths only — do not re-report findings here):\n${extras.unchangedPaths
246
+ .map((path) => `- ${path}`)
247
+ .join("\n")}`
248
+ : "";
249
+ const upstreamListing = upstreamFiles
250
+ .map(({ file, path }) => `- ${path} (+${Number(file.additions ?? 0)} -${Number(file.deletions ?? 0)})`)
251
+ .join("\n");
252
+ const diff = upstreamFiles.length === 0
253
+ ? `${ownDiff}${unchangedListing}`
254
+ : `${ownDiff}
255
+
256
+ UPSTREAM CONTEXT — arrived via a merge this round, not authored by this pull
257
+ request. Do not raise a finding located only in this code; only note an
258
+ interaction if the pull request's own change above relies on or conflicts with
259
+ one of these files, and never mark that finding blocking:
260
+ ${upstreamListing}${unchangedListing}`;
261
+ report(`diff prepared · ${ownPatch.length} chars${diffWasTruncated ? " · truncated for model context" : ""}`);
262
+ const provider = ai ??
263
+ new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
264
+ const diagrams = provider.supportsTools !== false;
265
+ const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
266
+ const prompt = `Review this pull request against the repository's review guide and
267
+ codebase conventions. Find only actionable code-level violations supported by
268
+ the diff and either the guide or the codebase conventions — a pull request
269
+ that departs from how this repository's own code is actually written is a
270
+ valid finding even when the review guide has no matching rule.
271
+ Do not repeat existing review comments unless the diff still contains the issue.
272
+ Do not invent requirements. Ignore bot noise and historical PR identities.
273
+ Reason thoroughly, then return concise Markdown only with either:
274
+ "## Findings" followed by findings, or "## Findings\\n\\nNo actionable findings."
275
+ There is no fixed number of findings. Return every independently actionable
276
+ finding supported by the diff and guide, including zero findings when appropriate.
277
+ Do not stop early; inspect all supplied diff text first and return the natural
278
+ count. If the diff contains a truncation marker, limit claims to the supplied
279
+ text and do not imply that omitted files were reviewed.
280
+ Do not invent low-value findings.
281
+ When the DIFF section below has an UPSTREAM CONTEXT part, that code arrived
282
+ through a merge and was not authored by this pull request; do not raise a
283
+ finding located only there, and never mark blocking a finding whose only
284
+ support is upstream context.
285
+ ${CODEGRAPH_DIFF_VERIFICATION}
286
+ Each finding must use this exact structure, keeping the default finding under
287
+ 120 words excluding an optional diagram and an optional suggestion:
288
+
289
+ ### [P1 · blocking] \`path/to/file.ts\` — \`symbol()\`
290
+ Location: \`path/to/file.ts:42\`
291
+
292
+ One sentence describing what is wrong and its impact.
293
+
294
+ Add one short evidence paragraph explaining the mechanism or reproduction.
295
+ Do not add labels such as Mechanism, Symptom, Scenario, Verified, Repro,
296
+ Options, or Scope unless that detail is necessary to understand a complex
297
+ finding. End every finding with this exact sentence on its own line:
298
+ "If you'd like me to explain it in more detail, please ask." No finding may
299
+ omit it and nothing may follow it.
300
+
301
+ Use P0-P3 severity and exactly either "blocking" or "non-blocking".
302
+ Keep the Location line machine-readable; it is removed from user-facing
303
+ review copies. Use Markdown backticks around paths and symbols.
304
+ Every diff line in the DIFF section starts with its line number in the new
305
+ file. Copy Location numbers from that column instead of counting from the @@
306
+ header, and use \`path:from-to\` when the finding spans several lines. Removed
307
+ lines have no number, so anchor a finding about removed code to the nearest
308
+ numbered line.
309
+
310
+ Include one GitHub suggestion when the fix is a direct replacement of
311
+ consecutive numbered lines from a single hunk of the same file and you are
312
+ confident in the exact replacement text — this is the common case for
313
+ single-line and small multi-line fixes. Keep Location as the full span of
314
+ the problem, and put the suggestion right before the closing sentence:
315
+
316
+ Suggestion: \`path/to/file.ts:42-43\`
317
+ \`\`\`suggestion
318
+ every line of 42-43 as it should read, with its original indentation
319
+ \`\`\`
320
+
321
+ The Suggestion range must sit inside the Location range and cover only the
322
+ lines the fix changes. The block replaces that whole range, so write every
323
+ line of it, not just the edited part, and write nothing else inside the block.
324
+ When the replacement itself contains three backticks, open and close the block
325
+ with four. Leave the suggestion out when the fix needs removed lines, another
326
+ file, or more than one hunk, or when you are not sure of the exact code.
327
+ ${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
328
+
329
+ REVIEW GUIDE:
330
+ ${guide}
331
+
332
+ DETAILED GUIDE:
333
+ ${detailed}
334
+
335
+ CODEBASE CONVENTIONS:
336
+ ${codebase || "None recorded."}
337
+
338
+ PULL REQUEST:
339
+ ${JSON.stringify({
340
+ number,
341
+ title: String(pr.title ?? ""),
342
+ body: String(pr.body ?? ""),
343
+ state: pr.state,
344
+ changedFiles: files.map((file) => String(file.filename ?? "")),
345
+ existingComments: comments.map((comment) => String(comment.body ?? "")),
346
+ existingReviews: reviews.map((review) => String(review.body ?? "")),
347
+ })}
348
+
349
+ ${carryBlock}DIFF:
350
+ ${diff}`;
351
+ const matrix = clampImproveMatrix(options.improveMatrix);
352
+ const request = {
353
+ job: "review_pull_request",
354
+ system: reviewSystemPrompt(diagrams),
355
+ prompt,
356
+ maxTokens: 24_000 * matrix,
357
+ reasoningEffort: "high",
358
+ };
359
+ report(`AI request · model=${options.highModel ?? "openrouter default"} · ` +
360
+ `prompt=${prompt.length} chars · maxTokens=${request.maxTokens}`);
361
+ if (options.debug) {
362
+ console.log(`[debug] review prompt · ${prompt.length} chars · diff=${diff.length} chars`);
363
+ console.log(`[debug] openrouter request · model=${options.highModel ?? "openai/gpt-5.6-luna"} · maxTokens=${request.maxTokens}`);
364
+ }
365
+ let response = await completeWithMermaidTools(provider, request, 1, extraTools, maxToolRounds);
366
+ if (usage)
367
+ await usage(response);
368
+ report(`AI response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
369
+ if (options.debug) {
370
+ console.log(`[debug] initial review response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
371
+ console.log(`\n----- INITIAL REVIEW -----\n${response.text}\n`);
372
+ }
373
+ if (!response.text.trim()) {
374
+ throw new Error("OpenRouter returned an empty review; the reasoning budget may have been exhausted");
375
+ }
376
+ let reviewText = response.text.trim();
377
+ for (let pass = 2; pass <= matrix; pass++) {
378
+ const improvementRequest = {
379
+ ...request,
380
+ job: "improve_review",
381
+ prompt: `Audit the draft review below against the complete pull-request diff
382
+ and the supplied review guides. Preserve valid findings, correct inaccurate ones,
383
+ remove duplicate or unsupported ones, and add every missing actionable finding.
384
+ Do not stop early and do not invent requirements. Keep the existing finding
385
+ structure unchanged. Return only the complete revised review in the same format.
386
+
387
+ ORIGINAL REVIEW CONTEXT:
388
+ ${prompt}
389
+
390
+ DRAFT REVIEW:
391
+ ${reviewText}`,
392
+ };
393
+ if (options.debug) {
394
+ console.log(`[debug] improvement ${pass - 1} request · prompt=${improvementRequest.prompt.length} chars · maxTokens=${improvementRequest.maxTokens}`);
395
+ }
396
+ report(`AI improvement pass ${pass - 1} of ${matrix - 1}`);
397
+ response = await completeWithMermaidTools(provider, improvementRequest, 1, extraTools, maxToolRounds);
398
+ if (usage)
399
+ await usage(response);
400
+ report(`AI improvement response · input=${response.tokensIn} tokens · ` +
401
+ `output=${response.tokensOut} tokens`);
402
+ if (!response.text.trim()) {
403
+ throw new Error(`OpenRouter returned an empty review improvement at pass ${pass - 1}`);
404
+ }
405
+ reviewText = response.text.trim();
406
+ if (options.debug) {
407
+ console.log(`[debug] improvement ${pass - 1} response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
408
+ console.log(`\n----- IMPROVED REVIEW ${pass - 1} -----\n${reviewText}\n`);
409
+ }
410
+ }
411
+ const visiblePaths = ownFiles.map(({ path }) => path);
412
+ return {
413
+ ...response,
414
+ text: `## Severity
415
+
416
+ - P0 — Critical: production outage, data loss, or security issue.
417
+ - P1 — High: major behavior is broken and should be fixed before merge.
418
+ - P2 — Medium: important correctness or maintainability issue.
419
+ - P3 — Low: minor, non-blocking improvement or edge case.
420
+
421
+ ${reviewText}`,
422
+ visiblePaths,
423
+ guideBuiltAt: guides.guideBuiltAt,
424
+ };
425
+ }
426
+ function revisionToGithubFiles(revision) {
427
+ return revision.files.map((file) => ({
428
+ filename: file.path,
429
+ status: file.status === "renamed" ? "renamed" : file.status,
430
+ previous_filename: file.previousPath,
431
+ additions: file.additions,
432
+ deletions: file.deletions,
433
+ changes: file.additions + file.deletions,
434
+ patch: file.patch,
435
+ }));
436
+ }
437
+ /** Local workspace review — same prompt loop as PR review without GitHub. */
438
+ export async function reviewWorkspaceRevision(revision, options, headSha, usage, ai, progress, extras) {
439
+ const report = progress ?? (() => { });
440
+ const guides = await loadGuides(options.repo);
441
+ const { shortGuide, detailed, codebase, skill } = guides;
442
+ const guide = shortGuide || skill;
443
+ if (!guide) {
444
+ throw new Error(`repos/${options.repo}/PR_REVIEW_GUIDE.md was not found; run init first`);
445
+ }
446
+ const files = revisionToGithubFiles(revision);
447
+ const unchanged = new Set(extras?.unchangedPaths ?? []);
448
+ const ownFiles = files
449
+ .map((file) => ({
450
+ file,
451
+ path: String(file.filename ?? ""),
452
+ }))
453
+ .filter(({ path }) => !unchanged.has(path));
454
+ const patchByPath = new Map();
455
+ const ownSections = await Promise.all(ownFiles.map(async ({ file, path }) => {
456
+ const changes = Number(file.changes ?? 0);
457
+ const patch = String(file.patch ?? "");
458
+ const lowProvider = needsSummary(changes, patch)
459
+ ? (ai ??
460
+ new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
461
+ : undefined;
462
+ if (!lowProvider)
463
+ return `FILE: ${path}\n${filePatch(file)}`;
464
+ patchByPath.set(path, patch);
465
+ const description = await summarizeDiff(path, patch, lowProvider, usage);
466
+ return `FILE: ${path}\n[${changes} changed lines — summarized below]\n${description}`;
467
+ }));
468
+ const codegraphTools = options.useCodegraph
469
+ ? extras?.prepareCodegraphTools
470
+ ? await extras.prepareCodegraphTools()
471
+ : await prepareCodegraphTools(options.repo, 0, headSha)
472
+ : [];
473
+ const extraTools = [
474
+ ...codegraphTools,
475
+ ...(patchByPath.size > 0
476
+ ? [
477
+ {
478
+ name: "read-full-diff",
479
+ tool: READ_FULL_DIFF_TOOL,
480
+ run: (args) => readFullDiff(patchByPath, args),
481
+ },
482
+ ]
483
+ : []),
484
+ ];
485
+ const totalDiffLines = ownFiles.reduce((sum, { file }) => sum + Number(file.changes ?? 0), 0);
486
+ const maxToolRounds = clampToolRounds(totalDiffLines);
487
+ const ownDiff = text(ownSections.join("\n\n"), MAX_REVIEW_DIFF_CHARS);
488
+ const unchangedListing = extras?.unchangedPaths?.length
489
+ ? `\nUNCHANGED SINCE LAST REVIEW (paths only):\n${extras.unchangedPaths
490
+ .map((path) => `- ${path}`)
491
+ .join("\n")}`
492
+ : "";
493
+ const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
494
+ const provider = ai ??
495
+ new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
496
+ const diagrams = provider.supportsTools !== false;
497
+ const prompt = `Review these local changes against the repository's review guide and
498
+ codebase conventions. Find only actionable code-level violations supported by
499
+ the diff and either the guide or the codebase conventions.
500
+ Return concise Markdown with either "## Findings" and findings, or
501
+ "## Findings\\n\\nNo actionable findings."
502
+ Do not invent low-value findings. If the diff contains a truncation marker or a
503
+ file is summarized, limit claims to the supplied text and use read-full-diff or
504
+ codegraph tools before asserting behavior outside what was shown.
505
+ ${CODEGRAPH_DIFF_VERIFICATION}
506
+ ${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
507
+
508
+ REVIEW GUIDE:
509
+ ${guide}
510
+
511
+ DETAILED GUIDE:
512
+ ${detailed}
513
+
514
+ CODEBASE CONVENTIONS:
515
+ ${codebase || "None recorded."}
516
+
517
+ WORKSPACE:
518
+ ${JSON.stringify({
519
+ branch: revision.baseLabel,
520
+ changedFiles: ownFiles.map(({ path }) => path),
521
+ })}
522
+
523
+ ${carryBlock}DIFF:
524
+ ${ownDiff}${unchangedListing}`;
525
+ const matrix = clampImproveMatrix(options.improveMatrix);
526
+ const request = {
527
+ job: "review_local",
528
+ system: reviewSystemPrompt(diagrams),
529
+ prompt,
530
+ maxTokens: 24_000 * matrix,
531
+ reasoningEffort: "high",
532
+ };
533
+ report(`AI request · prompt=${prompt.length} chars`);
534
+ let response = await completeWithMermaidTools(provider, request, 1, extraTools, maxToolRounds);
535
+ if (usage)
536
+ await usage(response);
537
+ if (!response.text.trim()) {
538
+ throw new Error("OpenRouter returned an empty review");
539
+ }
540
+ let reviewText = response.text.trim();
541
+ for (let pass = 2; pass <= matrix; pass++) {
542
+ const improvementRequest = {
543
+ ...request,
544
+ job: "improve_review",
545
+ prompt: `Audit the draft review below against the complete diff and guides.
546
+ Preserve valid findings, correct inaccurate ones, remove duplicate or
547
+ unsupported ones, and add every missing actionable finding. Re-check each
548
+ finding with codegraph when it depends on behavior outside the diff; remove
549
+ findings that only looked plausible from the diff slice.
550
+ Return only the complete revised review in the same format.
551
+
552
+ ORIGINAL REVIEW CONTEXT:
553
+ ${prompt}
554
+
555
+ DRAFT REVIEW:
556
+ ${reviewText}`,
557
+ };
558
+ response = await completeWithMermaidTools(provider, improvementRequest, 1, extraTools, maxToolRounds);
559
+ if (usage)
560
+ await usage(response);
561
+ reviewText = response.text.trim();
562
+ }
563
+ const codegraphState = !options.useCodegraph
564
+ ? "disabled"
565
+ : codegraphTools.length > 0
566
+ ? "used"
567
+ : "unavailable";
568
+ return {
569
+ ...response,
570
+ text: `## Severity
571
+
572
+ - P0 — Critical: production outage, data loss, or security issue.
573
+ - P1 — High: major behavior is broken and should be fixed before merge.
574
+ - P2 — Medium: important correctness or maintainability issue.
575
+ - P3 — Low: minor, non-blocking improvement or edge case.
576
+
577
+ ${reviewText}`,
578
+ visiblePaths: ownFiles.map(({ path }) => path),
579
+ guideBuiltAt: guides.guideBuiltAt,
580
+ codegraphState,
581
+ };
582
+ }
@@ -0,0 +1,17 @@
1
+ /** Round detection and repeat matching for P11 re-reviews.
2
+ * Rounds are clusters of review comments that share `original_commit_id`,
3
+ * oldest first, the same grouping benchmark/core_v2/README.md describes. */
4
+ import type { FindingRow } from "../store/rows.ts";
5
+ import type { Span } from "./findings.ts";
6
+ export type RoundComment = {
7
+ original_commit_id?: string;
8
+ commit_id?: string;
9
+ created_at?: string;
10
+ };
11
+ export declare function reviewRounds(comments: RoundComment[]): string[];
12
+ /** Previous round's commit: the last clustered original_commit_id that is
13
+ * not the current head. Undefined when there is no earlier round. */
14
+ export declare function incrementalBase(rounds: string[], headSha: string): string | undefined;
15
+ export declare function spansOverlap(a: Span, b: Span): boolean;
16
+ /** First previous finding that covers the same path and line. */
17
+ export declare function matchRepeat(finding: Span, previous: FindingRow[]): FindingRow | undefined;
@@ -0,0 +1,43 @@
1
+ export function reviewRounds(comments) {
2
+ const sorted = [...comments].sort((a, b) => String(a.created_at ?? "").localeCompare(String(b.created_at ?? "")));
3
+ const rounds = [];
4
+ const seen = new Set();
5
+ for (const comment of sorted) {
6
+ const id = comment.original_commit_id || comment.commit_id;
7
+ if (!id || seen.has(id))
8
+ continue;
9
+ seen.add(id);
10
+ rounds.push(id);
11
+ }
12
+ return rounds;
13
+ }
14
+ /** Previous round's commit: the last clustered original_commit_id that is
15
+ * not the current head. Undefined when there is no earlier round. */
16
+ export function incrementalBase(rounds, headSha) {
17
+ const previous = rounds.filter((sha) => sha !== headSha);
18
+ return previous.at(-1);
19
+ }
20
+ function normalizePath(path) {
21
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
22
+ }
23
+ function asSpan(row) {
24
+ if (!row.path || row.line_from == null)
25
+ return undefined;
26
+ return {
27
+ path: row.path,
28
+ from: row.line_from,
29
+ to: row.line_to ?? row.line_from,
30
+ };
31
+ }
32
+ export function spansOverlap(a, b) {
33
+ if (normalizePath(a.path) !== normalizePath(b.path))
34
+ return false;
35
+ return a.from <= b.to && b.from <= a.to;
36
+ }
37
+ /** First previous finding that covers the same path and line. */
38
+ export function matchRepeat(finding, previous) {
39
+ return previous.find((row) => {
40
+ const span = asSpan(row);
41
+ return span !== undefined && spansOverlap(finding, span);
42
+ });
43
+ }
@@ -0,0 +1,35 @@
1
+ import { type Run } from "./checkout.ts";
2
+ /** What a pull request's author actually wrote this round, as opposed to code
3
+ * that arrived by merging the default branch in. A re-review round's diff
4
+ * regularly includes a `merge main` a human reviewer never reads and a bot
5
+ * should not either — one measured case in this repo's own benchmark had a
6
+ * round diff of 48 files where the author's own commits touched 5. Findings
7
+ * belong in the own set; upstream code is context, shown but not policed,
8
+ * because the author's own change can still interact badly with it. */
9
+ export type ScopeResult = {
10
+ /** Files touched by a commit the author made this round: their own
11
+ * non-merge commits, plus the conflict-resolution hunks of any merge they
12
+ * made. A clean "merge main in" contributes nothing here — `--cc` only
13
+ * surfaces lines that differ from every parent, which is exactly the part
14
+ * a human resolved by hand. */
15
+ ownFiles: Set<string>;
16
+ /** Everything else touched between the round's boundary and its head —
17
+ * code that arrived via a merge and was not authored this round. */
18
+ upstreamFiles: Set<string>;
19
+ defaultBranch: string;
20
+ ownCommits: number;
21
+ mergeCommits: number;
22
+ };
23
+ /** The scope algorithm against a clone that is already on disk. Split out from
24
+ * `computeScope` so a caller that already manages its own clone — the
25
+ * benchmark's OCR runner has its own, separate from co-maintainer's — can run
26
+ * the identical algorithm without a second clone of the same repository. */
27
+ export declare function scopeInClone(clone: string, base: string, head: string, run?: Run, remote?: string): Promise<ScopeResult | undefined>;
28
+ /** Clones (or reuses) `repo` under co-maintainer's own cache and runs the
29
+ * scope algorithm against it. Returns `undefined` on any failure — no clone,
30
+ * an unresolvable commit, no default branch to compare against — so the
31
+ * caller's fallback is always "review everything, unscoped", never a broken
32
+ * review. */
33
+ export declare function computeScope(repo: string, base: string, head: string, options?: {
34
+ run?: Run;
35
+ }): Promise<ScopeResult | undefined>;