limn-review 0.1.123

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 (474) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +133 -0
  3. package/dist/ai/runtime.js +535 -0
  4. package/dist/api/plugins/auth.js +169 -0
  5. package/dist/api/plugins/error-handler.js +114 -0
  6. package/dist/api/plugins/rate-limit.js +855 -0
  7. package/dist/api/plugins/security.js +321 -0
  8. package/dist/api/routes/activity.js +103 -0
  9. package/dist/api/routes/ai-runtime.js +67 -0
  10. package/dist/api/routes/auth.js +194 -0
  11. package/dist/api/routes/billing.js +209 -0
  12. package/dist/api/routes/bot-triage.js +1289 -0
  13. package/dist/api/routes/conflicts.js +730 -0
  14. package/dist/api/routes/contact.js +318 -0
  15. package/dist/api/routes/daily-brief.js +58 -0
  16. package/dist/api/routes/flow.js +71 -0
  17. package/dist/api/routes/health.js +7 -0
  18. package/dist/api/routes/insights.js +200 -0
  19. package/dist/api/routes/me.js +458 -0
  20. package/dist/api/routes/mentions.js +22 -0
  21. package/dist/api/routes/mergers.js +8 -0
  22. package/dist/api/routes/ml-labels.js +109 -0
  23. package/dist/api/routes/open-prs.js +35 -0
  24. package/dist/api/routes/prs.js +1633 -0
  25. package/dist/api/routes/reactions.js +217 -0
  26. package/dist/api/routes/repos.js +563 -0
  27. package/dist/api/routes/search.js +41 -0
  28. package/dist/api/routes/threads.js +118 -0
  29. package/dist/api/routes/timeline.js +150 -0
  30. package/dist/api/routes/users.js +67 -0
  31. package/dist/api/routes/webhooks.js +181 -0
  32. package/dist/api/routes/workspaces.js +322 -0
  33. package/dist/app.js +274 -0
  34. package/dist/ascii.js +19 -0
  35. package/dist/auth/account.js +482 -0
  36. package/dist/auth/crypto.js +46 -0
  37. package/dist/bot-triage/resolve.js +59 -0
  38. package/dist/cli.js +388 -0
  39. package/dist/coding/activity.js +80 -0
  40. package/dist/coding/agent.js +288 -0
  41. package/dist/coding/ai-fix/auto-fix.js +212 -0
  42. package/dist/coding/ai-fix/manager.js +370 -0
  43. package/dist/coding/ai-fix/persist.js +213 -0
  44. package/dist/coding/ai-fix/pr-context.js +58 -0
  45. package/dist/coding/ai-fix/prompts.js +138 -0
  46. package/dist/coding/ai-fix/review-seed.js +357 -0
  47. package/dist/coding/ai-fix/routes.js +425 -0
  48. package/dist/coding/git-ops.js +321 -0
  49. package/dist/coding/git.js +184 -0
  50. package/dist/coding/merge.js +262 -0
  51. package/dist/coding/safe-path.js +75 -0
  52. package/dist/coding/schema.js +43 -0
  53. package/dist/config.js +648 -0
  54. package/dist/conflict/diff.js +304 -0
  55. package/dist/conflict/git.js +118 -0
  56. package/dist/conflict/hash.js +73 -0
  57. package/dist/conflict/janitor.js +218 -0
  58. package/dist/conflict/land.js +675 -0
  59. package/dist/conflict/model-types.js +2 -0
  60. package/dist/conflict/model.js +1081 -0
  61. package/dist/conflict/parse.js +142 -0
  62. package/dist/conflict/seam.js +136 -0
  63. package/dist/conflict/session.js +532 -0
  64. package/dist/conflict/suggestion.js +398 -0
  65. package/dist/conflict/tokens.js +72 -0
  66. package/dist/data-dir.js +112 -0
  67. package/dist/db/actor-lanes.js +213 -0
  68. package/dist/db/automation-ids.js +97 -0
  69. package/dist/db/automation-output.js +349 -0
  70. package/dist/db/blast-radius-query.js +36 -0
  71. package/dist/db/blast-radius.js +226 -0
  72. package/dist/db/bot-dormancy.js +115 -0
  73. package/dist/db/bot-overlap.js +579 -0
  74. package/dist/db/bot-volume.js +722 -0
  75. package/dist/db/bot-window.js +35 -0
  76. package/dist/db/branch-queries.js +310 -0
  77. package/dist/db/change-shape.js +243 -0
  78. package/dist/db/changepoint.js +75 -0
  79. package/dist/db/cleanup.js +36 -0
  80. package/dist/db/client.js +102 -0
  81. package/dist/db/code-loc.js +388 -0
  82. package/dist/db/credits.js +79 -0
  83. package/dist/db/daily-brief.js +406 -0
  84. package/dist/db/dependency-cards.js +145 -0
  85. package/dist/db/erase-account.js +255 -0
  86. package/dist/db/export-account.js +189 -0
  87. package/dist/db/failing-checks.js +124 -0
  88. package/dist/db/file-coupling.js +197 -0
  89. package/dist/db/flow-detail.js +661 -0
  90. package/dist/db/flow-pointers.js +197 -0
  91. package/dist/db/flow-settings.js +67 -0
  92. package/dist/db/forecast.js +114 -0
  93. package/dist/db/line-overlap.js +81 -0
  94. package/dist/db/merge-queue.js +189 -0
  95. package/dist/db/migrate.js +17 -0
  96. package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
  97. package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
  98. package/dist/db/migrations/0002_famous_sersi.sql +9 -0
  99. package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
  100. package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
  101. package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
  102. package/dist/db/migrations/0006_cute_violations.sql +45 -0
  103. package/dist/db/migrations/0007_dark_anthem.sql +2 -0
  104. package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
  105. package/dist/db/migrations/0009_lean_bodies.sql +13 -0
  106. package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
  107. package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
  108. package/dist/db/migrations/0012_findings_included_default.sql +7 -0
  109. package/dist/db/migrations/0013_review_routing.sql +9 -0
  110. package/dist/db/migrations/0014_account_last_active.sql +6 -0
  111. package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
  112. package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
  113. package/dist/db/migrations/0017_account_display_name.sql +7 -0
  114. package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
  115. package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
  116. package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
  117. package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
  118. package/dist/db/migrations/0022_ci_status_events.sql +21 -0
  119. package/dist/db/migrations/0023_ai_usage.sql +21 -0
  120. package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
  121. package/dist/db/migrations/0025_account_billing.sql +7 -0
  122. package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
  123. package/dist/db/migrations/0027_user_github_type.sql +6 -0
  124. package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
  125. package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
  126. package/dist/db/migrations/0030_teams.sql +32 -0
  127. package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
  128. package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
  129. package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
  130. package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
  131. package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
  132. package/dist/db/migrations/0036_search_index.sql +25 -0
  133. package/dist/db/migrations/0037_author_indexes.sql +15 -0
  134. package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
  135. package/dist/db/migrations/0039_branch_status.sql +44 -0
  136. package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
  137. package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
  138. package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
  139. package/dist/db/migrations/0043_account_reviewers.sql +131 -0
  140. package/dist/db/migrations/0044_workspaces.sql +138 -0
  141. package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
  142. package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
  143. package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
  144. package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
  145. package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
  146. package/dist/db/migrations/0050_users_app_slug.sql +12 -0
  147. package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
  148. package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
  149. package/dist/db/migrations/0053_automation_roles.sql +79 -0
  150. package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
  151. package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
  152. package/dist/db/migrations/0056_pr_mentions.sql +22 -0
  153. package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
  154. package/dist/db/migrations/0058_pending_mute.sql +53 -0
  155. package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
  156. package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
  157. package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
  158. package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
  159. package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
  160. package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
  161. package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
  162. package/dist/db/migrations/0066_review_request_events.sql +31 -0
  163. package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
  164. package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
  165. package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
  166. package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
  167. package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
  168. package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
  169. package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
  170. package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
  171. package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
  172. package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
  173. package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
  174. package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
  175. package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
  176. package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
  177. package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
  178. package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
  179. package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
  180. package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
  181. package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
  182. package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
  183. package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
  184. package/dist/db/migrations/meta/_journal.json +566 -0
  185. package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
  186. package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
  187. package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
  188. package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
  189. package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
  190. package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
  191. package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
  192. package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
  193. package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
  194. package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
  195. package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
  196. package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
  197. package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
  198. package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
  199. package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
  200. package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
  201. package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
  202. package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
  203. package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
  204. package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
  205. package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
  206. package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
  207. package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
  208. package/dist/db/migrations-pg/0023_search_index.sql +19 -0
  209. package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
  210. package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
  211. package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
  212. package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
  213. package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
  214. package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
  215. package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
  216. package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
  217. package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
  218. package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
  219. package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
  220. package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
  221. package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
  222. package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
  223. package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
  224. package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
  225. package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
  226. package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
  227. package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
  228. package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
  229. package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
  230. package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
  231. package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
  232. package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
  233. package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
  234. package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
  235. package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
  236. package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
  237. package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
  238. package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
  239. package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
  240. package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
  241. package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
  242. package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
  243. package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
  244. package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
  245. package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
  246. package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
  247. package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
  248. package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
  249. package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
  250. package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
  251. package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
  252. package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
  253. package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
  254. package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
  255. package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
  256. package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
  257. package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
  258. package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
  259. package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
  260. package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
  261. package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
  262. package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
  263. package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
  264. package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
  265. package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
  266. package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
  267. package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
  268. package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
  269. package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
  270. package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
  271. package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
  272. package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
  273. package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
  274. package/dist/db/migrations-pg/meta/_journal.json +475 -0
  275. package/dist/db/ml-labels.js +1666 -0
  276. package/dist/db/my-turn-dismissals.js +343 -0
  277. package/dist/db/my-turn-settings.js +34 -0
  278. package/dist/db/pending-classify.js +46 -0
  279. package/dist/db/pending-mute.js +171 -0
  280. package/dist/db/pending-tabs.js +238 -0
  281. package/dist/db/period-metrics.js +820 -0
  282. package/dist/db/person-period.js +763 -0
  283. package/dist/db/pr-intervals.js +930 -0
  284. package/dist/db/pr-liveness.js +240 -0
  285. package/dist/db/pr-mentions.js +279 -0
  286. package/dist/db/pr-merge-queue-stamp.js +91 -0
  287. package/dist/db/queries.js +14631 -0
  288. package/dist/db/reaction-targets.js +94 -0
  289. package/dist/db/repo-activity.js +171 -0
  290. package/dist/db/retention.js +138 -0
  291. package/dist/db/review-threads-for-review.js +240 -0
  292. package/dist/db/run-migrations.js +24 -0
  293. package/dist/db/schema.pg.js +1220 -0
  294. package/dist/db/schema.sqlite.js +1801 -0
  295. package/dist/db/search.js +317 -0
  296. package/dist/db/security-alerts.js +218 -0
  297. package/dist/db/synthesis-input.js +565 -0
  298. package/dist/db/triage.js +320 -0
  299. package/dist/db/usage.js +63 -0
  300. package/dist/db/work-plan.js +1086 -0
  301. package/dist/db/working-hours.js +161 -0
  302. package/dist/feed/my-turn.js +122 -0
  303. package/dist/github/actions-logs.js +397 -0
  304. package/dist/github/auth.js +86 -0
  305. package/dist/github/branch-queries.js +229 -0
  306. package/dist/github/client.js +454 -0
  307. package/dist/github/codeowners.js +221 -0
  308. package/dist/github/commit-checks.js +105 -0
  309. package/dist/github/compare.js +110 -0
  310. package/dist/github/diff-anchor.js +227 -0
  311. package/dist/github/mutations.js +706 -0
  312. package/dist/github/pr-file-content.js +254 -0
  313. package/dist/github/pr-liveness.js +161 -0
  314. package/dist/github/queries.js +875 -0
  315. package/dist/github/rate-budget.js +129 -0
  316. package/dist/github/reactions.js +161 -0
  317. package/dist/github/reviewer-suggest.js +104 -0
  318. package/dist/github/team-reviewers.js +96 -0
  319. package/dist/index.js +184 -0
  320. package/dist/merge/auto-merge-runner.js +960 -0
  321. package/dist/ml/severity-client.js +218 -0
  322. package/dist/pr/detail-enricher.js +26 -0
  323. package/dist/pro/bind.js +302 -0
  324. package/dist/pro/contract.js +37 -0
  325. package/dist/pro/migrate.js +64 -0
  326. package/dist/review/agent-context.js +127 -0
  327. package/dist/review/agent.js +385 -0
  328. package/dist/review/agentic.js +59 -0
  329. package/dist/review/auth.js +94 -0
  330. package/dist/review/chat-agent.js +179 -0
  331. package/dist/review/claude-review/ai-ready.js +16 -0
  332. package/dist/review/claude-review/auto-settings.js +139 -0
  333. package/dist/review/claude-review/auto.js +244 -0
  334. package/dist/review/claude-review/chat.js +489 -0
  335. package/dist/review/claude-review/ci-failures.js +350 -0
  336. package/dist/review/claude-review/follow-up.js +310 -0
  337. package/dist/review/claude-review/manager.js +686 -0
  338. package/dist/review/claude-review/persist.js +996 -0
  339. package/dist/review/claude-review/prompts.js +357 -0
  340. package/dist/review/claude-review/routes.js +518 -0
  341. package/dist/review/claude-review/routing.js +61 -0
  342. package/dist/review/claude-review/specialists.js +190 -0
  343. package/dist/review/claude-review/threads.js +265 -0
  344. package/dist/review/claude-review/ticket.js +282 -0
  345. package/dist/review/clone-hygiene.js +380 -0
  346. package/dist/review/clone-manager.js +367 -0
  347. package/dist/review/llm.js +113 -0
  348. package/dist/review/model-options.js +48 -0
  349. package/dist/review/plugin-providers.js +12 -0
  350. package/dist/review/post-review.js +216 -0
  351. package/dist/review/post-seam.js +161 -0
  352. package/dist/review/prepare.js +168 -0
  353. package/dist/review/pricing.js +17 -0
  354. package/dist/review/schema.js +156 -0
  355. package/dist/review/submit-map.js +41 -0
  356. package/dist/review/usage.js +90 -0
  357. package/dist/shared/claude-review.js +335 -0
  358. package/dist/shared/conflict-fold.js +151 -0
  359. package/dist/shared/conflicts.js +30 -0
  360. package/dist/shared/flow-settings.js +191 -0
  361. package/dist/shared/index.js +8 -0
  362. package/dist/shared/my-turn-settings.js +238 -0
  363. package/dist/shared/pending-rules.js +281 -0
  364. package/dist/shared/product.js +54 -0
  365. package/dist/shared/types.js +896 -0
  366. package/dist/status.js +386 -0
  367. package/dist/sync/adaptive.js +185 -0
  368. package/dist/sync/app-attribution.js +66 -0
  369. package/dist/sync/auth-notices.js +36 -0
  370. package/dist/sync/backfill-ci-history.js +315 -0
  371. package/dist/sync/backfill-commit-headlines.js +136 -0
  372. package/dist/sync/backfill-pr-security.js +175 -0
  373. package/dist/sync/backfill-review-requests.js +97 -0
  374. package/dist/sync/benchmark-rollup.js +50 -0
  375. package/dist/sync/bot-detection.js +309 -0
  376. package/dist/sync/bot-resolution-markers.js +30 -0
  377. package/dist/sync/branch-status.js +634 -0
  378. package/dist/sync/classify-change-shape.js +150 -0
  379. package/dist/sync/commit-files.js +74 -0
  380. package/dist/sync/derive-thread-state.js +99 -0
  381. package/dist/sync/drain-review-threads.js +212 -0
  382. package/dist/sync/hydrate-detail.js +482 -0
  383. package/dist/sync/mention-scan.js +89 -0
  384. package/dist/sync/ml-enrichment.js +478 -0
  385. package/dist/sync/pr-change-signal.js +96 -0
  386. package/dist/sync/pr-liveness-sweep.js +139 -0
  387. package/dist/sync/pr-settle.js +381 -0
  388. package/dist/sync/refresh-pr.js +163 -0
  389. package/dist/sync/reparse-vendor-badges.js +495 -0
  390. package/dist/sync/resync-after-write.js +280 -0
  391. package/dist/sync/review-fingerprint.js +117 -0
  392. package/dist/sync/reviewer-behavior.js +103 -0
  393. package/dist/sync/reviewer-classify.js +503 -0
  394. package/dist/sync/routing-files.js +97 -0
  395. package/dist/sync/scheduled-jobs.js +12 -0
  396. package/dist/sync/scheduler.js +159 -0
  397. package/dist/sync/security-detect.js +521 -0
  398. package/dist/sync/sync-manager.js +750 -0
  399. package/dist/sync/sync-one-pr.js +205 -0
  400. package/dist/sync/sync-repo.js +400 -0
  401. package/dist/sync/unsettled-prs.js +506 -0
  402. package/dist/sync/upsert.js +1190 -0
  403. package/package.json +56 -0
  404. package/public/apple-touch-icon.png +0 -0
  405. package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
  406. package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
  407. package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
  408. package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  409. package/public/assets/highlight-BEHUn5zE.css +10 -0
  410. package/public/assets/highlight-CIIZLa6Z.js +12 -0
  411. package/public/assets/index-BDnYs6zg.js +73 -0
  412. package/public/assets/index-mCuI--RC.css +1 -0
  413. package/public/assets/markdown-I_JLLVAZ.js +32 -0
  414. package/public/assets/perfProbe-Da13EhTE.js +1 -0
  415. package/public/assets/tanstack-5qsxxaLe.js +17 -0
  416. package/public/assets/vis-C3BCkP5U.js +1280 -0
  417. package/public/assets/vis-ziCVehMy.css +1 -0
  418. package/public/favicon-16.png +0 -0
  419. package/public/favicon-32.png +0 -0
  420. package/public/favicon-48.png +0 -0
  421. package/public/favicon.svg +5 -0
  422. package/public/index.html +41 -0
  423. package/public-landing/apple-touch-icon.png +0 -0
  424. package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
  425. package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
  426. package/public-landing/assets/index-CA3eEGXO.js +40 -0
  427. package/public-landing/assets/index-naQrQ6Fe.css +1 -0
  428. package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
  429. package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
  430. package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
  431. package/public-landing/bots/index.html +156 -0
  432. package/public-landing/contact/index.html +156 -0
  433. package/public-landing/cookies/index.html +156 -0
  434. package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
  435. package/public-landing/demo/limn-walkthrough.cues.json +44 -0
  436. package/public-landing/demo/limn-walkthrough.mp4 +0 -0
  437. package/public-landing/favicon-16.png +0 -0
  438. package/public-landing/favicon-32.png +0 -0
  439. package/public-landing/favicon-48.png +0 -0
  440. package/public-landing/favicon.svg +5 -0
  441. package/public-landing/features/index.html +156 -0
  442. package/public-landing/for-developers/index.html +156 -0
  443. package/public-landing/for-managers/index.html +156 -0
  444. package/public-landing/how-it-works/index.html +156 -0
  445. package/public-landing/how-we-measure/index.html +156 -0
  446. package/public-landing/icon-192.png +0 -0
  447. package/public-landing/icon-512.png +0 -0
  448. package/public-landing/index.html +156 -0
  449. package/public-landing/insights/index.html +156 -0
  450. package/public-landing/og-image.png +0 -0
  451. package/public-landing/pricing/index.html +156 -0
  452. package/public-landing/privacy/index.html +156 -0
  453. package/public-landing/pro/index.html +156 -0
  454. package/public-landing/reviews/index.html +156 -0
  455. package/public-landing/robots.txt +8 -0
  456. package/public-landing/shots/benchmark.png +0 -0
  457. package/public-landing/shots/bot-roi.png +0 -0
  458. package/public-landing/shots/bot-settings.png +0 -0
  459. package/public-landing/shots/chronology.png +0 -0
  460. package/public-landing/shots/feed.png +0 -0
  461. package/public-landing/shots/flow-metrics.png +0 -0
  462. package/public-landing/shots/free-reports.png +0 -0
  463. package/public-landing/shots/pending-board.png +0 -0
  464. package/public-landing/shots/pending-card.png +0 -0
  465. package/public-landing/shots/period-report.png +0 -0
  466. package/public-landing/shots/pr-detail.png +0 -0
  467. package/public-landing/shots/pr-threads.png +0 -0
  468. package/public-landing/shots/reach.png +0 -0
  469. package/public-landing/shots/repo-rows.png +0 -0
  470. package/public-landing/shots/severity-findings.png +0 -0
  471. package/public-landing/shots/severity-strip.png +0 -0
  472. package/public-landing/site.webmanifest +14 -0
  473. package/public-landing/sitemap.xml +51 -0
  474. package/public-landing/terms/index.html +156 -0
@@ -0,0 +1,996 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { and, asc, desc, eq, gte, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm';
3
+ import { CLAUDE_FINDING_LENSES, threadAssessmentCounts } from '../../shared/index.js';
4
+ import { storedList, stripStoredStoryLead } from '../../shared/index.js';
5
+ import { ticketEntriesOf } from './ticket.js';
6
+ import { isFollowUpEligible, resolveFindingBody, } from './follow-up.js';
7
+ // The Claude Review persistence layer. It reads + writes the claudeReviews /
8
+ // claudeReviewFindings tables via ctx.db + ctx.schema (review/agent-context.ts). Only the
9
+ // FEATURE-only reads live here; the cross-surface reads (getClaudeReviewFeedItems /
10
+ // getUnactionedClaudeReviews / listClaudeReviewsByRepo) live in db/queries.ts.
11
+ // ctx.schema is typed loosely (Record<string, any>) — this module addresses tables by name and
12
+ // casts result rows, so it compiles identically against either dialect's schema.
13
+ /* eslint-disable @typescript-eslint/no-explicit-any */
14
+ function tables(ctx) {
15
+ const s = ctx.schema;
16
+ return { cr: s.claudeReviews, crf: s.claudeReviewFindings, prs: s.pullRequests, repos: s.repos };
17
+ }
18
+ const iso = (d) => d instanceof Date ? d.toISOString() : d == null ? null : new Date(d).toISOString();
19
+ const isoReq = (d) => d instanceof Date ? d.toISOString() : new Date(d).toISOString();
20
+ // GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its path.
21
+ function diffAnchorId(path) {
22
+ return createHash('sha256').update(path, 'utf8').digest('hex');
23
+ }
24
+ // A story finding's origin off its row; null unless BOTH columns hold a value.
25
+ export function storyOf(r) {
26
+ return r.storyIndex != null && Number.isInteger(r.storyIndex) && r.storyIndex >= 0 && r.storyRef
27
+ ? { index: r.storyIndex, ref: r.storyRef }
28
+ : null;
29
+ }
30
+ // A stored CI record, or null for an older row / anything malformed (never a guessed shape).
31
+ export function ciRecordOf(v) {
32
+ if (v == null || typeof v !== 'object')
33
+ return null;
34
+ const r = v;
35
+ if (!Array.isArray(r.failures) || typeof r.state !== 'string')
36
+ return null;
37
+ return {
38
+ state: r.state,
39
+ checkCount: typeof r.checkCount === 'number' ? r.checkCount : 0,
40
+ failures: r.failures,
41
+ };
42
+ }
43
+ function mapFinding(r) {
44
+ return {
45
+ id: r.id,
46
+ reviewId: r.reviewId,
47
+ path: r.path,
48
+ line: r.line,
49
+ side: r.side,
50
+ diffAnchorId: diffAnchorId(r.path),
51
+ severity: r.severity,
52
+ title: r.title,
53
+ // A story finding stored before its lead moved to post time began with that lead, which only
54
+ // repeated the title on screen (shared `stripStoredStoryLead`).
55
+ body: stripStoredStoryLead(r.body, storyOf(r)),
56
+ editedBody: r.editedBody,
57
+ suggestion: r.suggestion,
58
+ diffHunk: r.diffHunk,
59
+ anchored: r.anchored,
60
+ fileInDiff: r.fileInDiff,
61
+ included: r.included,
62
+ postedAt: iso(r.postedAt),
63
+ githubCommentId: r.githubCommentId,
64
+ postedCommentKind: r.postedCommentKind,
65
+ createdAt: isoReq(r.createdAt),
66
+ priorFindingId: r.priorFindingId ?? null,
67
+ lens: asLens(r.lens),
68
+ story: storyOf(r),
69
+ };
70
+ }
71
+ // A stored lens outside today's list reads as a general finding, never a made-up label.
72
+ function asLens(v) {
73
+ return v != null && CLAUDE_FINDING_LENSES.includes(v)
74
+ ? v
75
+ : null;
76
+ }
77
+ // The stored follow-up + each item's DERIVED `reraisedFindingId` (the first of this run's
78
+ // findings whose priorFindingId points at the item's earlier finding).
79
+ function mapFollowUp(fu, findings) {
80
+ if (!fu)
81
+ return null;
82
+ const firstByPrior = new Map();
83
+ for (const f of findings) {
84
+ if (f.priorFindingId != null && !firstByPrior.has(f.priorFindingId)) {
85
+ firstByPrior.set(f.priorFindingId, f.id);
86
+ }
87
+ }
88
+ return {
89
+ ...fu,
90
+ items: (fu.items ?? []).map((it) => ({
91
+ ...it,
92
+ reraisedFindingId: firstByPrior.get(it.priorFindingId) ?? null,
93
+ })),
94
+ };
95
+ }
96
+ function mapReview(r, findings, head = null) {
97
+ const tickets = ticketEntriesOf(r.ticket, r.ticketAssessment);
98
+ return {
99
+ id: r.id,
100
+ prId: r.prId,
101
+ headSha: r.headSha,
102
+ status: r.status,
103
+ model: r.model,
104
+ scope: r.scope,
105
+ reviewMode: r.reviewMode,
106
+ routeReason: r.routeReason,
107
+ summary: r.summary,
108
+ verdict: r.verdict,
109
+ userBody: r.userBody,
110
+ userVerdict: r.userVerdict,
111
+ costUsd: r.costUsd,
112
+ inputTokens: r.inputTokens,
113
+ outputTokens: r.outputTokens,
114
+ cacheReadTokens: r.cacheReadTokens,
115
+ cacheCreationTokens: r.cacheCreationTokens,
116
+ numTurns: r.numTurns,
117
+ diffBytes: r.diffBytes,
118
+ diffCapped: r.diffCapped,
119
+ error: r.error,
120
+ excludedFiles: r.excludedFiles ?? [],
121
+ postedReviewId: r.postedReviewId,
122
+ postedAt: iso(r.postedAt),
123
+ createdAt: isoReq(r.createdAt),
124
+ finishedAt: iso(r.finishedAt),
125
+ findings: findings.map(mapFinding),
126
+ tickets,
127
+ ticket: tickets[0]?.ticket ?? null,
128
+ ticketAssessment: tickets[0]?.assessment ?? null,
129
+ head,
130
+ followUp: mapFollowUp(r.followUp, findings),
131
+ trigger: r.trigger === 'auto' ? 'auto' : 'manual',
132
+ // null = this run did not assess threads (older row, skip, not succeeded) — never [].
133
+ threadAssessments: Array.isArray(r.threadAssessments) ? r.threadAssessments : null,
134
+ threadAssessmentCounts: Array.isArray(r.threadAssessments)
135
+ ? threadAssessmentCounts(r.threadAssessments)
136
+ : null,
137
+ // null = this run did not look at CI (older row, skip, not succeeded, checks unreadable).
138
+ ...ciWireOf(r.ciFailures),
139
+ };
140
+ }
141
+ function ciWireOf(v) {
142
+ const rec = ciRecordOf(v);
143
+ return rec
144
+ ? { ciFailures: rec.failures, ciState: { state: rec.state, checkCount: rec.checkCount } }
145
+ : { ciFailures: null, ciState: null };
146
+ }
147
+ const summaryOf = (r) => ({
148
+ id: r.id,
149
+ headSha: r.headSha,
150
+ status: r.status,
151
+ model: r.model,
152
+ scope: r.scope,
153
+ reviewMode: r.reviewMode,
154
+ verdict: r.verdict,
155
+ userVerdict: r.userVerdict,
156
+ costUsd: r.costUsd,
157
+ postedAt: iso(r.postedAt),
158
+ createdAt: isoReq(r.createdAt),
159
+ finishedAt: iso(r.finishedAt),
160
+ trigger: r.trigger === 'auto' ? 'auto' : 'manual',
161
+ });
162
+ // ---- Reads (account-scoped via the join up to repos.accountId — IDOR guard) ----
163
+ export async function getClaudeReviewById(ctx, reviewId, accountId) {
164
+ const { cr, crf, prs, repos } = tables(ctx);
165
+ const rows = (await ctx.db
166
+ .select({ review: cr, prHeadSha: prs.headSha })
167
+ .from(cr)
168
+ .innerJoin(prs, eq(prs.id, cr.prId))
169
+ .innerJoin(repos, eq(repos.id, prs.repoId))
170
+ .where(and(eq(cr.id, reviewId), eq(repos.accountId, accountId)))
171
+ .limit(1)
172
+ .execute());
173
+ const row = rows[0]?.review ?? null;
174
+ if (!row)
175
+ return null;
176
+ const findings = (await ctx.db
177
+ .select()
178
+ .from(crf)
179
+ .where(eq(crf.reviewId, reviewId))
180
+ .orderBy(asc(crf.id))
181
+ .execute());
182
+ const head = await reviewHeadState(ctx, row.prId, row.headSha, rows[0].prHeadSha);
183
+ return mapReview(row, findings, head);
184
+ }
185
+ export async function getLatestClaudeReview(ctx, prId, accountId) {
186
+ const { cr, prs, repos } = tables(ctx);
187
+ const rows = (await ctx.db
188
+ .select({ id: cr.id })
189
+ .from(cr)
190
+ .innerJoin(prs, eq(prs.id, cr.prId))
191
+ .innerJoin(repos, eq(repos.id, prs.repoId))
192
+ .where(and(eq(cr.prId, prId), eq(repos.accountId, accountId)))
193
+ .orderBy(desc(cr.id))
194
+ .limit(1)
195
+ .execute());
196
+ const row = rows[0] ?? null;
197
+ if (!row)
198
+ return null;
199
+ return getClaudeReviewById(ctx, row.id, accountId);
200
+ }
201
+ export async function listClaudeReviewHistory(ctx, prId, accountId) {
202
+ const { cr, prs, repos } = tables(ctx);
203
+ const rows = (await ctx.db
204
+ .select({ review: cr })
205
+ .from(cr)
206
+ .innerJoin(prs, eq(prs.id, cr.prId))
207
+ .innerJoin(repos, eq(repos.id, prs.repoId))
208
+ .where(and(eq(cr.prId, prId), eq(repos.accountId, accountId)))
209
+ .orderBy(desc(cr.id))
210
+ .execute());
211
+ return rows.map(({ review }) => summaryOf(review));
212
+ }
213
+ // The window that scopes the cross-PR list (mirrors the core backfill default of 90d).
214
+ const LIST_WINDOW_DAYS = Number.parseInt(process.env.PRO_REVIEW_LIST_WINDOW_DAYS ?? '', 10) || 90;
215
+ export async function listAllClaudeReviews(ctx, accountId) {
216
+ const { cr, prs, repos } = tables(ctx);
217
+ const cutoff = new Date(Date.now() - LIST_WINDOW_DAYS * 24 * 60 * 60 * 1000);
218
+ // sqlite timestamps are unix SECONDS; pg is a Date. Bind the dialect-appropriate value.
219
+ const tsBound = ctx.isPg ? cutoff : Math.floor(cutoff.getTime() / 1000);
220
+ const rows = (await ctx.db
221
+ .select({
222
+ reviewId: cr.id,
223
+ prId: cr.prId,
224
+ owner: repos.owner,
225
+ name: repos.name,
226
+ prNumber: prs.number,
227
+ prTitle: prs.title,
228
+ prState: prs.state,
229
+ summary: cr.summary,
230
+ verdict: cr.verdict,
231
+ headSha: cr.headSha,
232
+ status: cr.status,
233
+ createdAt: cr.createdAt,
234
+ finishedAt: cr.finishedAt,
235
+ })
236
+ .from(cr)
237
+ .innerJoin(prs, eq(prs.id, cr.prId))
238
+ .innerJoin(repos, eq(repos.id, prs.repoId))
239
+ .where(and(eq(repos.accountId, accountId), eq(cr.status, 'succeeded'), or(eq(prs.state, 'open'), gte(sql `coalesce(${prs.mergedAt}, ${prs.closedAt}, ${prs.openedAt})`, tsBound))))
240
+ .orderBy(desc(cr.finishedAt), desc(cr.createdAt))
241
+ .execute());
242
+ const seen = new Set();
243
+ const items = [];
244
+ for (const r of rows) {
245
+ if (seen.has(r.prId))
246
+ continue;
247
+ seen.add(r.prId);
248
+ items.push({
249
+ reviewId: r.reviewId,
250
+ prId: r.prId,
251
+ repoFullName: `${r.owner}/${r.name}`,
252
+ prNumber: r.prNumber,
253
+ prTitle: r.prTitle,
254
+ prState: r.prState,
255
+ summary: r.summary,
256
+ verdict: r.verdict,
257
+ headSha: r.headSha,
258
+ status: r.status,
259
+ createdAt: isoReq(r.createdAt),
260
+ finishedAt: iso(r.finishedAt),
261
+ });
262
+ }
263
+ return items;
264
+ }
265
+ /**
266
+ * The LATEST run per PR for a batch of PR ids — the Open PRs table's "Claude review" column, ONE
267
+ * query for the whole list. Account-scoped through `repos.accountId` (a foreign or unknown id is
268
+ * simply absent). `headMoved` compares the run's head with the PR's SYNCED head; an unknown synced
269
+ * head is never "moved". DB-only: no GitHub call, no model.
270
+ */
271
+ export async function getLatestReviewStates(ctx, prIds, accountId,
272
+ // TRAILING: the AI Fix manager's in-memory claim (`isFixRunning`). When given, each state also
273
+ // carries `fix` ('running' / 'ready') from ONE batched ai_fixes read. Absent ⇒ no `fix` field.
274
+ fixRunning) {
275
+ if (prIds.length === 0)
276
+ return [];
277
+ const { cr, prs, repos } = tables(ctx);
278
+ const rows = (await ctx.db
279
+ .select({
280
+ reviewId: cr.id,
281
+ prId: cr.prId,
282
+ status: cr.status,
283
+ verdict: cr.verdict,
284
+ headSha: cr.headSha,
285
+ finishedAt: cr.finishedAt,
286
+ ticket: cr.ticket,
287
+ prHeadSha: prs.headSha,
288
+ trigger: cr.trigger,
289
+ })
290
+ .from(cr)
291
+ .innerJoin(prs, eq(prs.id, cr.prId))
292
+ .innerJoin(repos, eq(repos.id, prs.repoId))
293
+ .where(and(inArray(cr.prId, [...prIds]), eq(repos.accountId, accountId)))
294
+ .orderBy(desc(cr.id))
295
+ .execute());
296
+ const out = [];
297
+ const latestByPr = new Map();
298
+ for (const r of rows) {
299
+ if (latestByPr.has(r.prId))
300
+ continue; // newest first — the first row per PR is its latest run
301
+ latestByPr.set(r.prId, r);
302
+ out.push({
303
+ prId: r.prId,
304
+ reviewId: r.reviewId,
305
+ status: r.status,
306
+ verdict: r.verdict,
307
+ reviewedHeadSha: r.headSha,
308
+ finishedAt: iso(r.finishedAt),
309
+ ticket: storedList(r.ticket)[0] ?? null,
310
+ tickets: storedList(r.ticket),
311
+ headMoved: r.prHeadSha != null && r.prHeadSha !== r.headSha,
312
+ currentHeadSha: r.prHeadSha ?? null,
313
+ trigger: r.trigger === 'auto' ? 'auto' : 'manual',
314
+ });
315
+ }
316
+ await foldStateSummaries(ctx, out, latestByPr);
317
+ if (fixRunning)
318
+ await foldFixStates(ctx, out, latestByPr, accountId, fixRunning);
319
+ return out;
320
+ }
321
+ /**
322
+ * The Open PRs strip's AI Fix pill, for the listed PRs in ONE read of `ai_fixes`: `'running'` while
323
+ * the fixer holds a queued / running fix for the PR (the manager's claim is the authority — a row
324
+ * left `running` by a crash is not), else `'ready'` when a SUCCEEDED fix with a non-empty patch,
325
+ * never pushed, was built on the PR's CURRENT synced head. Mutates `states` in place.
326
+ */
327
+ async function foldFixStates(ctx, states, latestByPr, accountId, fixRunning) {
328
+ if (states.length === 0)
329
+ return;
330
+ const af = ctx.schema.aiFixes;
331
+ const rows = (await ctx.db
332
+ .select({ prId: af.prId, status: af.status, baseSha: af.baseSha, patch: af.patch, pushedAt: af.pushedAt })
333
+ .from(af)
334
+ .where(and(eq(af.accountId, accountId), inArray(af.prId, states.map((s) => s.prId))))
335
+ .execute());
336
+ const running = new Set();
337
+ const ready = new Set();
338
+ for (const r of rows) {
339
+ if ((r.status === 'queued' || r.status === 'running') && fixRunning(r.prId))
340
+ running.add(r.prId);
341
+ const head = latestByPr.get(r.prId)?.prHeadSha ?? null;
342
+ if (r.status === 'succeeded' &&
343
+ r.pushedAt == null &&
344
+ (r.patch ?? '').trim() !== '' &&
345
+ head != null &&
346
+ r.baseSha === head)
347
+ ready.add(r.prId);
348
+ }
349
+ for (const s of states) {
350
+ if (running.has(s.prId))
351
+ s.fix = 'running';
352
+ else if (ready.has(s.prId))
353
+ s.fix = 'ready';
354
+ }
355
+ }
356
+ /**
357
+ * The Open PRs strip's figures for the latest runs: severity / lens / posted counts, story
358
+ * alignments, follow-up statuses and the outdated commit count. A FIXED number of batched reads
359
+ * (findings, the run's JSON columns, the moved PRs' commits) whatever the list length — never one
360
+ * per PR. Mutates `states` in place. A run that did not succeed gets no `summary`.
361
+ */
362
+ async function foldStateSummaries(ctx, states, latestByPr) {
363
+ const { cr, crf } = tables(ctx);
364
+ const done = states.filter((s) => s.status === 'succeeded' && s.reviewId != null);
365
+ const doneIds = done.map((s) => s.reviewId);
366
+ // 1. Every finding of the finished runs — the columns the fold needs, nothing else.
367
+ const findings = doneIds.length
368
+ ? (await ctx.db
369
+ .select({ reviewId: crf.reviewId, severity: crf.severity, lens: crf.lens, postedAt: crf.postedAt })
370
+ .from(crf)
371
+ .where(inArray(crf.reviewId, doneIds))
372
+ .execute())
373
+ : [];
374
+ // 2. The finished runs' stored assessments (JSON) — read for the LATEST runs only, never history.
375
+ const extras = doneIds.length
376
+ ? (await ctx.db
377
+ .select({
378
+ id: cr.id,
379
+ ticketAssessment: cr.ticketAssessment,
380
+ followUp: cr.followUp,
381
+ postedAt: cr.postedAt,
382
+ threadAssessments: cr.threadAssessments,
383
+ ciFailures: cr.ciFailures,
384
+ })
385
+ .from(cr)
386
+ .where(inArray(cr.id, doneIds))
387
+ .execute())
388
+ : [];
389
+ const extrasById = new Map(extras.map((e) => [e.id, e]));
390
+ const findingsById = new Map();
391
+ for (const f of findings) {
392
+ const list = findingsById.get(f.reviewId) ?? [];
393
+ list.push(f);
394
+ findingsById.set(f.reviewId, list);
395
+ }
396
+ for (const s of done) {
397
+ const id = s.reviewId;
398
+ const counts = { blocker: 0, warning: 0, nit: 0, question: 0, praise: 0 };
399
+ const lenses = {};
400
+ let postedFindings = 0;
401
+ for (const f of findingsById.get(id) ?? []) {
402
+ if (f.severity in counts)
403
+ counts[f.severity] += 1;
404
+ const lens = CLAUDE_FINDING_LENSES.includes(f.lens ?? '') ? f.lens : null;
405
+ if (lens)
406
+ lenses[lens] = (lenses[lens] ?? 0) + 1;
407
+ if (f.postedAt != null)
408
+ postedFindings += 1;
409
+ }
410
+ const extra = extrasById.get(id);
411
+ const latest = latestByPr.get(s.prId);
412
+ const tickets = ticketEntriesOf((latest?.ticket ?? null), extra?.ticketAssessment ?? null).map((e) => ({
413
+ key: e.ticket.key ?? null,
414
+ title: e.ticket.title ?? null,
415
+ alignment: e.assessment?.alignment ?? null,
416
+ }));
417
+ let followUp = null;
418
+ const items = extra?.followUp?.items;
419
+ if (Array.isArray(items) && items.length > 0) {
420
+ followUp = { addressed: 0, partly_addressed: 0, not_addressed: 0, no_longer_applies: 0, not_checked: 0 };
421
+ for (const it of items)
422
+ if (it.status in followUp)
423
+ followUp[it.status] += 1;
424
+ }
425
+ const summary = {
426
+ findings: counts,
427
+ lenses,
428
+ postedFindings,
429
+ reviewPosted: extra?.postedAt != null,
430
+ tickets,
431
+ followUp,
432
+ };
433
+ // null = the run did not assess threads (an older row): no figure, never zeros.
434
+ if (Array.isArray(extra?.threadAssessments)) {
435
+ summary.threadAssessments = threadAssessmentCounts(extra.threadAssessments);
436
+ }
437
+ // null = the run did not look at CI: no figure, never zeros.
438
+ const ci = ciRecordOf(extra?.ciFailures);
439
+ if (ci) {
440
+ summary.ci = {
441
+ failing: ci.failures.length,
442
+ diagnosed: ci.failures.filter((f) => f.status === 'diagnosed').length,
443
+ };
444
+ }
445
+ s.summary = summary;
446
+ }
447
+ // 3. The outdated count, for every moved PR — ONE commits read for all of them.
448
+ const moved = states.filter((s) => s.headMoved && s.reviewedHeadSha != null);
449
+ const c = ctx.schema.commits;
450
+ if (moved.length === 0 || !c)
451
+ return;
452
+ const commitRows = (await ctx.db
453
+ .select({ prId: c.prId, sha: c.sha, committedAt: c.committedAt, headline: c.messageHeadline })
454
+ .from(c)
455
+ .where(inArray(c.prId, moved.map((s) => s.prId)))
456
+ .execute());
457
+ const commitsByPr = new Map();
458
+ for (const r of commitRows) {
459
+ const list = commitsByPr.get(r.prId) ?? [];
460
+ list.push(r);
461
+ commitsByPr.set(r.prId, list);
462
+ }
463
+ for (const s of moved) {
464
+ const head = latestByPr.get(s.prId)?.prHeadSha;
465
+ if (!head)
466
+ continue;
467
+ s.commitsSince = countCommitsSince(commitsByPr.get(s.prId) ?? [], s.reviewedHeadSha, head);
468
+ }
469
+ }
470
+ export async function getReviewPostContext(ctx, reviewId, accountId) {
471
+ const { cr, prs, repos } = tables(ctx);
472
+ const rows = (await ctx.db
473
+ .select({
474
+ headSha: cr.headSha,
475
+ userBody: cr.userBody,
476
+ owner: repos.owner,
477
+ name: repos.name,
478
+ prNumber: prs.number,
479
+ })
480
+ .from(cr)
481
+ .innerJoin(prs, eq(prs.id, cr.prId))
482
+ .innerJoin(repos, eq(repos.id, prs.repoId))
483
+ .where(and(eq(cr.id, reviewId), eq(repos.accountId, accountId)))
484
+ .limit(1)
485
+ .execute());
486
+ const row = rows[0];
487
+ if (!row)
488
+ return null;
489
+ return {
490
+ reviewHeadSha: row.headSha,
491
+ userBody: row.userBody,
492
+ owner: row.owner,
493
+ name: row.name,
494
+ prNumber: row.prNumber,
495
+ };
496
+ }
497
+ export async function getFindingPostContext(ctx, findingId, accountId) {
498
+ const { cr, crf, prs, repos } = tables(ctx);
499
+ const rows = (await ctx.db
500
+ .select({
501
+ finding: crf,
502
+ reviewHeadSha: cr.headSha,
503
+ owner: repos.owner,
504
+ name: repos.name,
505
+ prNumber: prs.number,
506
+ })
507
+ .from(crf)
508
+ .innerJoin(cr, eq(cr.id, crf.reviewId))
509
+ .innerJoin(prs, eq(prs.id, cr.prId))
510
+ .innerJoin(repos, eq(repos.id, prs.repoId))
511
+ .where(and(eq(crf.id, findingId), eq(repos.accountId, accountId)))
512
+ .limit(1)
513
+ .execute());
514
+ const row = rows[0];
515
+ if (!row)
516
+ return null;
517
+ return { finding: row.finding, reviewHeadSha: row.reviewHeadSha, owner: row.owner, name: row.name, prNumber: row.prNumber };
518
+ }
519
+ export async function getReviewPrContext(ctx, prId, accountId) {
520
+ const { prs, repos } = tables(ctx);
521
+ const rows = (await ctx.db
522
+ .select({
523
+ prId: prs.id,
524
+ owner: repos.owner,
525
+ name: repos.name,
526
+ number: prs.number,
527
+ title: prs.title,
528
+ body: prs.body,
529
+ baseRefName: prs.baseRefName,
530
+ headSha: prs.headSha,
531
+ })
532
+ .from(prs)
533
+ .innerJoin(repos, eq(repos.id, prs.repoId))
534
+ .where(and(eq(prs.id, prId), eq(repos.accountId, accountId)))
535
+ .limit(1)
536
+ .execute());
537
+ const row = rows[0];
538
+ if (!row)
539
+ return null;
540
+ return {
541
+ prId: row.prId,
542
+ owner: row.owner,
543
+ name: row.name,
544
+ repoFullName: `${row.owner}/${row.name}`,
545
+ number: row.number,
546
+ title: row.title,
547
+ body: row.body,
548
+ baseRefName: row.baseRefName,
549
+ headSha: row.headSha,
550
+ };
551
+ }
552
+ // ---- The previous review, for the follow-up ----
553
+ /**
554
+ * The PREVIOUS review a new run follows up on, with its eligible findings — or null.
555
+ *
556
+ * The previous review is the newest SUCCEEDED run for the same PR (account-scoped through the
557
+ * repos join — the getLatestClaudeReview IDOR pattern) with an id below the current run's, that
558
+ * actually READ code: `review_mode IS NULL OR review_mode <> 'skip'`. ⚠ The NULL arm is
559
+ * load-bearing — pre-routing rows have no mode, and a bare `<>` drops NULL rows in SQL.
560
+ *
561
+ * Its findings are kept when eligible (`isFollowUpEligible`: POSTED to GitHub and not praise),
562
+ * with the body the user saw. A finding that was ignored, left unposted or only copied never enters
563
+ * the follow-up — not the prompt, not the stored record, not the counts. CARRY-FORWARD re-loads two kinds of OLDER finding named by
564
+ * that run's follow-up items — the ids come only from our own stored JSON and are re-scoped to this
565
+ * PR + account by the query — and marks them `carried`, so nothing drops out of the chain:
566
+ * - every 'not_checked' item (never shown to the model, or not reported on), when eligible;
567
+ * - every still-open item (not / partly addressed) that no ELIGIBLE (posted) finding of that
568
+ * run raises again, when the earlier finding is itself eligible. That keeps a posted comment in
569
+ * the chain when its re-raise was never posted — the re-raise follow-up.ts saved left out
570
+ * because the comment was already on the same commit (`isAlreadyOnThisCommit`), or one the
571
+ * reader simply did not post — so it is followed up against the comment that IS on the PR.
572
+ *
573
+ * Every finding carries the head of the review that RAISED it (`cr.headSha` via the join), because
574
+ * "has the code moved since?" is asked per finding, never against the previous review's head.
575
+ */
576
+ export async function loadPriorReviewForFollowUp(ctx, prId, accountId, beforeReviewId) {
577
+ const { cr, crf, prs, repos } = tables(ctx);
578
+ const rows = (await ctx.db
579
+ .select({ id: cr.id, headSha: cr.headSha, followUp: cr.followUp })
580
+ .from(cr)
581
+ .innerJoin(prs, eq(prs.id, cr.prId))
582
+ .innerJoin(repos, eq(repos.id, prs.repoId))
583
+ .where(and(eq(cr.prId, prId), eq(repos.accountId, accountId), eq(cr.status, 'succeeded'), lt(cr.id, beforeReviewId), or(isNull(cr.reviewMode), ne(cr.reviewMode, 'skip'))))
584
+ .orderBy(desc(cr.id))
585
+ .limit(1)
586
+ .execute());
587
+ const row = rows[0];
588
+ if (!row)
589
+ return null;
590
+ const toPrior = (f, headSha, carried) => ({
591
+ id: f.id,
592
+ headSha,
593
+ path: f.path,
594
+ line: f.line,
595
+ side: f.side,
596
+ severity: f.severity,
597
+ title: f.title,
598
+ body: resolveFindingBody(f),
599
+ suggestion: f.suggestion,
600
+ diffHunk: f.diffHunk,
601
+ anchored: f.anchored,
602
+ fileInDiff: f.fileInDiff,
603
+ posted: f.postedAt != null,
604
+ carried,
605
+ story: storyOf(f),
606
+ });
607
+ const own = (await ctx.db
608
+ .select()
609
+ .from(crf)
610
+ .where(eq(crf.reviewId, row.id))
611
+ .orderBy(asc(crf.id))
612
+ .execute());
613
+ const eligibleOwn = own.filter(isFollowUpEligible);
614
+ const findings = eligibleOwn.map((f) => toPrior(f, row.headSha, false));
615
+ const seen = new Set(own.map((f) => f.id));
616
+ // An earlier finding an eligible finding of that run already raises again is followed up
617
+ // THROUGH that re-raise, never twice.
618
+ const reraisedByEligible = new Set(eligibleOwn.map((f) => f.priorFindingId).filter((id) => id != null));
619
+ const notChecked = new Set();
620
+ const openNotReraised = new Map();
621
+ for (const it of row.followUp?.items ?? []) {
622
+ if (!Number.isInteger(it.priorFindingId) || seen.has(it.priorFindingId))
623
+ continue;
624
+ if (it.status === 'not_checked')
625
+ notChecked.add(it.priorFindingId);
626
+ else if ((it.status === 'not_addressed' || it.status === 'partly_addressed') &&
627
+ !reraisedByEligible.has(it.priorFindingId)) {
628
+ // Kept with the answer that run gave: on a same-head re-run it stands (follow-up.ts).
629
+ openNotReraised.set(it.priorFindingId, { status: it.status, explanation: it.explanation ?? null });
630
+ }
631
+ }
632
+ const carriedIds = [...new Set([...notChecked, ...openNotReraised.keys()])];
633
+ if (carriedIds.length > 0) {
634
+ const carriedRows = (await ctx.db
635
+ .select({ finding: crf, headSha: cr.headSha })
636
+ .from(crf)
637
+ .innerJoin(cr, eq(cr.id, crf.reviewId))
638
+ .innerJoin(prs, eq(prs.id, cr.prId))
639
+ .innerJoin(repos, eq(repos.id, prs.repoId))
640
+ .where(and(inArray(crf.id, carriedIds), eq(cr.prId, prId), eq(repos.accountId, accountId)))
641
+ .orderBy(asc(crf.id))
642
+ .execute());
643
+ for (const { finding, headSha } of carriedRows) {
644
+ // The ONE rule (posted, not praise) applies to carried findings exactly as to own ones.
645
+ if (seen.has(finding.id) || !isFollowUpEligible(finding))
646
+ continue;
647
+ seen.add(finding.id);
648
+ findings.push({ ...toPrior(finding, headSha, true), priorStatus: openNotReraised.get(finding.id) ?? null });
649
+ }
650
+ }
651
+ return { reviewId: row.id, headSha: row.headSha, findings };
652
+ }
653
+ // ---- Writers ----
654
+ // The tickets are stored at QUEUE time (not on success), so a failed or cancelled run still
655
+ // prefills the SPA's panel for the re-run. Stored as an ARRAY (null when none).
656
+ export async function insertQueuedReview(ctx, prId, headSha, model, accountId, tickets = [],
657
+ // TRAILING: who started it. 'auto' = the per-workspace sweeper (auto.ts). A label only: core's
658
+ // My Turn read keeps an auto run exactly like a manual one, and the chip says "Auto review".
659
+ trigger = 'manual') {
660
+ const { cr } = tables(ctx);
661
+ const rows = (await ctx.db
662
+ .insert(cr)
663
+ .values({
664
+ accountId,
665
+ prId,
666
+ headSha,
667
+ status: 'queued',
668
+ model,
669
+ ticket: tickets.length > 0 ? [...tickets] : null,
670
+ trigger,
671
+ })
672
+ .returning({ id: cr.id })
673
+ .execute());
674
+ return rows[0].id;
675
+ }
676
+ export async function markReviewRunning(ctx, id) {
677
+ const { cr } = tables(ctx);
678
+ await ctx.db.update(cr).set({ status: 'running' }).where(eq(cr.id, id)).execute();
679
+ }
680
+ export async function markReviewRouted(ctx, id, reviewMode, routeReason) {
681
+ const { cr } = tables(ctx);
682
+ await ctx.db.update(cr).set({ reviewMode, routeReason }).where(eq(cr.id, id)).execute();
683
+ }
684
+ export async function saveReviewSuccess(ctx, id, data) {
685
+ const { cr, crf } = tables(ctx);
686
+ await ctx.runTransaction(async (tx) => {
687
+ await tx
688
+ .update(cr)
689
+ .set({
690
+ status: 'succeeded',
691
+ scope: data.scope,
692
+ summary: data.summary,
693
+ verdict: data.verdict,
694
+ costUsd: data.costUsd,
695
+ inputTokens: data.inputTokens,
696
+ outputTokens: data.outputTokens,
697
+ cacheReadTokens: data.cacheReadTokens ?? null,
698
+ cacheCreationTokens: data.cacheCreationTokens ?? null,
699
+ numTurns: data.numTurns,
700
+ diffBytes: data.diffBytes ?? null,
701
+ diffCapped: data.diffCapped ?? null,
702
+ excludedFiles: data.excludedFiles,
703
+ followUp: data.followUp ?? null,
704
+ ticketAssessment: data.ticketAssessment ?? null,
705
+ threadAssessments: data.threadAssessments ?? null,
706
+ ciFailures: data.ciFailures ?? null,
707
+ finishedAt: new Date(),
708
+ })
709
+ .where(eq(cr.id, id))
710
+ .execute();
711
+ for (const f of data.findings) {
712
+ await tx
713
+ .insert(crf)
714
+ .values({
715
+ reviewId: id,
716
+ path: f.path,
717
+ line: f.line,
718
+ side: f.side,
719
+ severity: f.severity,
720
+ title: f.title,
721
+ body: f.body,
722
+ suggestion: f.suggestion,
723
+ diffHunk: f.diffHunk,
724
+ anchored: f.anchored,
725
+ fileInDiff: f.fileInDiff,
726
+ // Findings are INCLUDED by default (the UI is opt-OUT); the column default is
727
+ // false for back-compat, so set it here. The one exception arrives as `included:
728
+ // false`: a re-raise of a comment already posted on this same commit (follow-up.ts
729
+ // `isAlreadyOnThisCommit`), so Post review does not put it on GitHub twice.
730
+ included: f.included ?? true,
731
+ priorFindingId: f.priorFindingId ?? null,
732
+ lens: f.lens ?? null,
733
+ // Set only on a story finding (ticket.ts `storyFindingsFrom`, or a re-raise of one).
734
+ storyIndex: f.story?.index ?? null,
735
+ storyRef: f.story?.ref ?? null,
736
+ })
737
+ .execute();
738
+ }
739
+ });
740
+ await recordReviewUsage(ctx, id, data.costUsd, data.inputTokens, data.outputTokens).catch(() => { });
741
+ }
742
+ export async function markReviewFailed(ctx, id, error, telemetry = {}) {
743
+ const { cr } = tables(ctx);
744
+ await ctx.db
745
+ .update(cr)
746
+ .set({
747
+ status: 'failed',
748
+ error: error.slice(0, 4000),
749
+ costUsd: telemetry.costUsd ?? null,
750
+ inputTokens: telemetry.inputTokens ?? null,
751
+ outputTokens: telemetry.outputTokens ?? null,
752
+ cacheReadTokens: telemetry.cacheReadTokens ?? null,
753
+ cacheCreationTokens: telemetry.cacheCreationTokens ?? null,
754
+ numTurns: telemetry.numTurns ?? null,
755
+ diffBytes: telemetry.diffBytes ?? null,
756
+ diffCapped: telemetry.diffCapped ?? null,
757
+ scope: telemetry.scope ?? null,
758
+ excludedFiles: telemetry.excludedFiles ?? null,
759
+ finishedAt: new Date(),
760
+ })
761
+ .where(eq(cr.id, id))
762
+ .execute();
763
+ await recordReviewUsage(ctx, id, telemetry.costUsd, telemetry.inputTokens, telemetry.outputTokens).catch(() => { });
764
+ }
765
+ export async function markReviewCancelled(ctx, id) {
766
+ const { cr } = tables(ctx);
767
+ await ctx.db
768
+ .update(cr)
769
+ .set({ status: 'cancelled', finishedAt: new Date() })
770
+ .where(eq(cr.id, id))
771
+ .execute();
772
+ }
773
+ export async function updateReviewDraft(ctx, id, fields) {
774
+ const set = {
775
+ ...(fields.userBody !== undefined ? { userBody: fields.userBody } : {}),
776
+ ...(fields.userVerdict !== undefined ? { userVerdict: fields.userVerdict } : {}),
777
+ };
778
+ if (Object.keys(set).length === 0)
779
+ return true;
780
+ const { cr } = tables(ctx);
781
+ const changed = (await ctx.db
782
+ .update(cr)
783
+ .set(set)
784
+ .where(eq(cr.id, id))
785
+ .returning({ id: cr.id })
786
+ .execute());
787
+ return changed.length > 0;
788
+ }
789
+ export async function updateFinding(ctx, findingId, fields) {
790
+ const set = {
791
+ ...(fields.included !== undefined ? { included: fields.included } : {}),
792
+ ...(fields.editedBody !== undefined
793
+ ? { editedBody: fields.editedBody === '' ? null : fields.editedBody }
794
+ : {}),
795
+ };
796
+ if (Object.keys(set).length === 0)
797
+ return true;
798
+ const { crf } = tables(ctx);
799
+ const changed = (await ctx.db
800
+ .update(crf)
801
+ .set(set)
802
+ .where(eq(crf.id, findingId))
803
+ .returning({ id: crf.id })
804
+ .execute());
805
+ return changed.length > 0;
806
+ }
807
+ export async function markReviewPosted(ctx, id, postedReviewId, inlineFindingIds, prComments = []) {
808
+ const { cr, crf } = tables(ctx);
809
+ const now = new Date();
810
+ await ctx.runTransaction(async (tx) => {
811
+ await tx.update(cr).set({ postedReviewId, postedAt: now }).where(eq(cr.id, id)).execute();
812
+ if (inlineFindingIds.length > 0) {
813
+ await tx
814
+ .update(crf)
815
+ .set({ postedAt: now, postedCommentKind: 'inline' })
816
+ .where(inArray(crf.id, inlineFindingIds))
817
+ .execute();
818
+ }
819
+ for (const pc of prComments) {
820
+ await tx
821
+ .update(crf)
822
+ .set({ postedAt: now, githubCommentId: pc.commentId, postedCommentKind: 'pr_comment' })
823
+ .where(eq(crf.id, pc.findingId))
824
+ .execute();
825
+ }
826
+ });
827
+ }
828
+ export async function markFindingPosted(ctx, findingId, githubCommentId, kind = 'inline') {
829
+ const { crf } = tables(ctx);
830
+ await ctx.db
831
+ .update(crf)
832
+ .set({ postedAt: new Date(), githubCommentId, postedCommentKind: kind })
833
+ .where(eq(crf.id, findingId))
834
+ .execute();
835
+ }
836
+ // On startup, heal runs left mid-flight by a crash/restart.
837
+ export async function reconcileOrphanedReviews(ctx) {
838
+ const { cr } = tables(ctx);
839
+ const changed = (await ctx.db
840
+ .update(cr)
841
+ .set({ status: 'failed', error: 'interrupted by restart', finishedAt: new Date() })
842
+ .where(inArray(cr.status, ['queued', 'running']))
843
+ .returning({ id: cr.id })
844
+ .execute());
845
+ return changed.length;
846
+ }
847
+ // Record a run's cost on the shared AI-usage ledger (agent seam). Reads accountId/model/prId
848
+ // back off the row. Best-effort — a $0 / non-billing run records nothing.
849
+ async function recordReviewUsage(ctx, id, costUsd, inputTokens, outputTokens) {
850
+ if (costUsd == null || !Number.isFinite(costUsd) || costUsd <= 0)
851
+ return;
852
+ const { cr } = tables(ctx);
853
+ const row = (await ctx.db
854
+ .select({ accountId: cr.accountId, model: cr.model, prId: cr.prId })
855
+ .from(cr)
856
+ .where(eq(cr.id, id))
857
+ .limit(1)
858
+ .execute())[0];
859
+ if (!row)
860
+ return;
861
+ await ctx.recordAiUsage({
862
+ accountId: row.accountId,
863
+ seam: 'agent',
864
+ feature: 'claude_review',
865
+ model: row.model,
866
+ costUsd,
867
+ inputTokens: inputTokens ?? null,
868
+ outputTokens: outputTokens ?? null,
869
+ prId: row.prId,
870
+ });
871
+ }
872
+ /**
873
+ * The pure half of `reviewHeadState` (also folded per PR by the batched Open PRs states read):
874
+ * how many of a PR's synced commits are newer than the reviewed one. null when either commit is
875
+ * not synced, or the history looks rewritten (a newer copy with the reviewed commit's headline).
876
+ */
877
+ export function countCommitsSince(rows, reviewedSha, currentHeadSha) {
878
+ const ms = (v) => (v instanceof Date ? v.getTime() : Number(v));
879
+ const reviewed = rows.find((r) => r.sha === reviewedSha);
880
+ const head = rows.find((r) => r.sha === currentHeadSha);
881
+ if (!reviewed || !head)
882
+ return null;
883
+ const at = ms(reviewed.committedAt);
884
+ const newer = rows.filter((r) => r.sha !== reviewedSha && ms(r.committedAt) > at);
885
+ // Sync never prunes commits a force-push dropped, so a rebase or amend leaves the reviewed
886
+ // commit's own rewritten copy among the "newer" rows (same headline, later time). Counting
887
+ // those would call a rewrite "N newer commits"; say only that the branch changed (null).
888
+ const rewritten = reviewed.headline != null && newer.some((r) => r.headline === reviewed.headline);
889
+ return rewritten ? null : newer.length;
890
+ }
891
+ /**
892
+ * Is the review behind the PR's head? DB-only. `commitsSince` counts the PR's synced commits newer
893
+ * than the reviewed one (by commit time); null when the reviewed commit is not among them, or when
894
+ * it looks rewritten (a newer copy with its headline) — never a guess. null overall when the PR's head is unknown.
895
+ */
896
+ export async function reviewHeadState(ctx, prId, reviewedSha, currentHeadSha) {
897
+ if (!currentHeadSha)
898
+ return null;
899
+ if (currentHeadSha === reviewedSha) {
900
+ return { currentHeadSha, outdated: false, commitsSince: 0 };
901
+ }
902
+ const c = ctx.schema.commits;
903
+ let commitsSince = null;
904
+ if (c) {
905
+ const rows = (await ctx.db
906
+ .select({ sha: c.sha, committedAt: c.committedAt, headline: c.messageHeadline })
907
+ .from(c)
908
+ .where(eq(c.prId, prId))
909
+ .execute());
910
+ commitsSince = countCommitsSince(rows, reviewedSha, currentHeadSha);
911
+ }
912
+ return { currentHeadSha, outdated: true, commitsSince };
913
+ }
914
+ // ---- Auto re-review helpers ----
915
+ /** Is there ANY run (any status) of this PR at `headSha`? One run per head. */
916
+ export async function hasReviewAtHead(ctx, prId, accountId, headSha) {
917
+ const { cr } = tables(ctx);
918
+ const rows = (await ctx.db
919
+ .select({ id: cr.id })
920
+ .from(cr)
921
+ .where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.headSha, headSha)))
922
+ .limit(1)
923
+ .execute());
924
+ return rows.length > 0;
925
+ }
926
+ /**
927
+ * Is the auto re-review keyed (headSha, commentsAtMs) already SETTLED? Any run of this PR at the
928
+ * head settles a moved head (one run per head). For new comments on an unchanged head
929
+ * (`commentsAtMs` set) a run at the head settles it only when it covered that comment: its
930
+ * `comments_through` (else its start, `created_at`) is at or after it. The sweeper's candidate read
931
+ * (db/queries.ts `getAutoReviewCandidates`) applies the same rule; this is the re-check a waiting
932
+ * item makes before its row is written.
933
+ */
934
+ export async function isAutoReReviewSettled(ctx, prId, accountId, headSha, commentsAtMs) {
935
+ const { cr } = tables(ctx);
936
+ const rows = (await ctx.db
937
+ .select({ createdAt: cr.createdAt, commentsThrough: cr.commentsThrough })
938
+ .from(cr)
939
+ .where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.headSha, headSha)))
940
+ .execute());
941
+ if (rows.length === 0)
942
+ return false;
943
+ if (commentsAtMs == null)
944
+ return true;
945
+ return rows.some((r) => (r.commentsThrough ?? r.createdAt).getTime() >= commentsAtMs);
946
+ }
947
+ /** Record the newest qualifying review comment the run saw (the comment half of the re-review key). */
948
+ export async function markReviewCommentsSeen(ctx, id, commentsThrough) {
949
+ if (!commentsThrough)
950
+ return;
951
+ const { cr } = tables(ctx);
952
+ await ctx.db.update(cr).set({ commentsThrough }).where(eq(cr.id, id)).execute();
953
+ }
954
+ /**
955
+ * The previous SUCCEEDED run (non-skip, id below this one) — what a same-head run carries forward:
956
+ * its head, its stories + their assessments, and its thread assessments. null when none.
957
+ */
958
+ export async function loadPriorRunForCarry(ctx, prId, accountId, beforeReviewId) {
959
+ const { cr } = tables(ctx);
960
+ const rows = (await ctx.db
961
+ .select({
962
+ headSha: cr.headSha,
963
+ ticket: cr.ticket,
964
+ ticketAssessment: cr.ticketAssessment,
965
+ threadAssessments: cr.threadAssessments,
966
+ ciFailures: cr.ciFailures,
967
+ })
968
+ .from(cr)
969
+ .where(and(eq(cr.prId, prId), eq(cr.accountId, accountId), eq(cr.status, 'succeeded'), lt(cr.id, beforeReviewId), or(isNull(cr.reviewMode), ne(cr.reviewMode, 'skip'))))
970
+ .orderBy(desc(cr.id))
971
+ .limit(1)
972
+ .execute());
973
+ const r = rows[0];
974
+ if (!r)
975
+ return null;
976
+ return {
977
+ headSha: r.headSha,
978
+ tickets: storedList(r.ticket),
979
+ ticketAssessments: storedList(r.ticketAssessment),
980
+ threadAssessments: Array.isArray(r.threadAssessments) ? r.threadAssessments : null,
981
+ ciFailures: ciRecordOf(r.ciFailures)?.failures ?? null,
982
+ };
983
+ }
984
+ /** The stories stored on the PR's latest run ([] when none) — what a re-review carries forward. */
985
+ export async function getLatestStoredTickets(ctx, prId, accountId) {
986
+ const { cr } = tables(ctx);
987
+ const rows = (await ctx.db
988
+ .select({ ticket: cr.ticket })
989
+ .from(cr)
990
+ .where(and(eq(cr.prId, prId), eq(cr.accountId, accountId)))
991
+ .orderBy(desc(cr.id))
992
+ .limit(1)
993
+ .execute());
994
+ return storedList(rows[0]?.ticket ?? null);
995
+ }
996
+ //# sourceMappingURL=persist.js.map