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,94 @@
1
+ // Emoji reactions — the ONLY database work the feature does.
2
+ //
3
+ // Reactions are never stored. This module exists for exactly one reason: turn the
4
+ // (kind, LOCAL id) pairs a client sends into GitHub node ids, and do it through a join that
5
+ // is scoped by accountId — so tenancy is STRUCTURAL rather than a check some handler has to
6
+ // remember.
7
+ //
8
+ // Why local ids on the wire at all, when GitHub's mutation wants a node id?
9
+ // • An unvalidated node-id list would be BOTH an existence oracle over other tenants' GitHub
10
+ // content AND a way to spend this account's GitHub quota on arbitrary nodes. Resolving
11
+ // local ids removes the class: a client cannot name an id it does not own, because the
12
+ // join simply returns nothing.
13
+ // • The local ids are already on CommentDetail / PrCommentDetail / ReviewDetail, so no read
14
+ // payload had to grow a field and no getter had to change.
15
+ //
16
+ // The three kinds live in THREE SEPARATE ID SPACES (`review_comments`, `pr_comments`,
17
+ // `reviews`) — the same hazard `ml_comment_labels` documents — so every lookup carries the
18
+ // kind and the three are queried separately. A bare numeric id means nothing on its own.
19
+ import { and, eq, inArray } from 'drizzle-orm';
20
+ import { db, schema } from './client.js';
21
+ const { pullRequests, prComments, reviewComments, reviews } = schema;
22
+ /**
23
+ * Resolve a batch of (kind, local id) pairs to GitHub node ids, scoped to `accountId`.
24
+ *
25
+ * A pair that does not resolve — unknown id, wrong kind, another tenant's row — is simply
26
+ * ABSENT from the result. It is never an error and never a 404: distinguishing "does not
27
+ * exist" from "is not yours" is the oracle this design avoids, and the client's honest
28
+ * rendering for both is the same (no reaction bar).
29
+ *
30
+ * Three indexed `IN` lookups at most, and only for the kinds actually present.
31
+ */
32
+ export async function resolveReactionTargets(accountId, targets) {
33
+ const byKind = new Map();
34
+ for (const t of targets) {
35
+ if (!Number.isInteger(t.id) || t.id <= 0)
36
+ continue;
37
+ let set = byKind.get(t.kind);
38
+ if (!set) {
39
+ set = new Set();
40
+ byKind.set(t.kind, set);
41
+ }
42
+ set.add(t.id);
43
+ }
44
+ const out = [];
45
+ const reviewCommentIds = byKind.get('review_comment');
46
+ if (reviewCommentIds && reviewCommentIds.size > 0) {
47
+ const rows = await db
48
+ .select({
49
+ id: reviewComments.id,
50
+ nodeId: reviewComments.githubNodeId,
51
+ prId: reviewComments.prId,
52
+ })
53
+ .from(reviewComments)
54
+ // The comment tables carry no accountId of their own — they reach their tenant through
55
+ // pr_id, exactly as ml_comment_labels' cleanup does. The join IS the isolation.
56
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
57
+ .where(and(eq(pullRequests.accountId, accountId), inArray(reviewComments.id, [...reviewCommentIds])))
58
+ .execute();
59
+ for (const r of rows) {
60
+ out.push({ kind: 'review_comment', id: r.id, nodeId: r.nodeId, prId: r.prId });
61
+ }
62
+ }
63
+ const prCommentIds = byKind.get('pr_comment');
64
+ if (prCommentIds && prCommentIds.size > 0) {
65
+ const rows = await db
66
+ .select({ id: prComments.id, nodeId: prComments.githubNodeId, prId: prComments.prId })
67
+ .from(prComments)
68
+ .innerJoin(pullRequests, eq(pullRequests.id, prComments.prId))
69
+ .where(and(eq(pullRequests.accountId, accountId), inArray(prComments.id, [...prCommentIds])))
70
+ .execute();
71
+ for (const r of rows) {
72
+ out.push({ kind: 'pr_comment', id: r.id, nodeId: r.nodeId, prId: r.prId });
73
+ }
74
+ }
75
+ const reviewIds = byKind.get('review');
76
+ if (reviewIds && reviewIds.size > 0) {
77
+ const rows = await db
78
+ .select({ id: reviews.id, nodeId: reviews.githubNodeId, prId: reviews.prId })
79
+ .from(reviews)
80
+ .innerJoin(pullRequests, eq(pullRequests.id, reviews.prId))
81
+ .where(and(eq(pullRequests.accountId, accountId), inArray(reviews.id, [...reviewIds])))
82
+ .execute();
83
+ for (const r of rows) {
84
+ out.push({ kind: 'review', id: r.id, nodeId: r.nodeId, prId: r.prId });
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ /** Single-target convenience for the write route. `null` ⇒ unknown or not this account's. */
90
+ export async function resolveReactionTarget(accountId, target) {
91
+ const rows = await resolveReactionTargets(accountId, [target]);
92
+ return rows[0] ?? null;
93
+ }
94
+ //# sourceMappingURL=reaction-targets.js.map
@@ -0,0 +1,171 @@
1
+ // WHERE IS THE WORK HAPPENING — per-repository activity over a rolling 14 days.
2
+ //
3
+ // The flow-metric tiles beside this on Reports → Overview answer "how much" and "how fast" for the
4
+ // whole workspace. They cannot answer "which repository", because a workspace-wide figure is one
5
+ // number over a population whose members routinely differ by two orders of magnitude: on the dev
6
+ // corpus one fortnight held 147 PRs / 1.4k lines in a config repo and 71 PRs / 141.7k lines in an
7
+ // application repo. Both are "activity"; they are not the same activity.
8
+ //
9
+ // ── WHY THIS IS A SEPARATE MODULE AND NOT A FIELD ON getWorkspaceMetrics ─────────────────────
10
+ //
11
+ // Two reasons, and the second is the load-bearing one:
12
+ //
13
+ // 1. `getWorkspaceMetrics` is a 400-line fold whose PR SELECT carries none of the five columns
14
+ // this needs (repoId, authorId, additions, deletions, changedFiles).
15
+ // 2. It honours an ARBITRARY comparison window handed in by the Pro layer (the workspace sprint
16
+ // cadence). This surface is FREE and cannot read that cadence — `resolveComparisonWindow` /
17
+ // `getComparisonWindow` live in the private plugin over `pro_workspace_settings`, and CORE has
18
+ // zero references to them. So the window here is the same trailing 14 days
19
+ // (`INSIGHT_SPRINT_DAYS`) the free tiles already use, which is what makes the chart and the
20
+ // tiles beside it agree BY CONSTRUCTION rather than by coincidence. Folding this into
21
+ // `getWorkspaceMetrics` would silently hand it the plugin's cadence window on the one call
22
+ // path that has one, and the panel would then carry two 14-day-labelled things measuring
23
+ // different fortnights.
24
+ //
25
+ // It also cannot live in `queries.ts`: it needs `resolveActorLanes`, and `actor-lanes.ts` imports
26
+ // FROM `queries.ts`. Every other lane-consuming fold (`period-metrics.ts`, `pr-intervals.ts`,
27
+ // `automation-output.ts`, `person-period.ts`) is a module beside them for exactly that reason.
28
+ //
29
+ // COST: two indexed scans (one PR window, one repo list) plus `resolveActorLanes`' fixed handful.
30
+ // Nothing multiplies by repo count, which is what keeps `/api/workspace-metrics` honest on the
31
+ // `read` fall-through — the tier comment in api/plugins/rate-limit.ts turns on that word.
32
+ import { and, eq, gte, inArray, lt } from 'drizzle-orm';
33
+ import { db, schema } from './client.js';
34
+ import { resolveActorLanes } from './actor-lanes.js';
35
+ const { pullRequests, repos } = schema;
36
+ /** The window. MIRRORS `INSIGHT_SPRINT_DAYS` in queries.ts, which is what the free flow-metric
37
+ * tiles use — the two are read side by side and a divergence would be invisible. Deliberately a
38
+ * constant and not a parameter: this is a FREE surface and the sprint cadence is plugin-owned, so
39
+ * there is no setting that could legitimately move it. The label on screen must say "rolling 14
40
+ * days", never "this sprint". */
41
+ export const REPO_ACTIVITY_WINDOW_DAYS = 14;
42
+ /** Top-N by PRs opened. Beyond about a dozen bands the bars are unreadable even rotated (the dev
43
+ * workspace has 19 active repositories in a typical fortnight). NEVER a silent truncation — the
44
+ * response carries `activeRepos` and an `omitted` aggregate so the cards can state both what was
45
+ * cut and what it was worth. */
46
+ export const REPO_ACTIVITY_MAX_REPOS = 12;
47
+ /**
48
+ * Per-repository PR and line counts over the trailing `REPO_ACTIVITY_WINDOW_DAYS`.
49
+ *
50
+ * ⚠ `scope.repoIds` IS REQUIRED AND CONCRETE, and `[]` returns null — the same contract
51
+ * `getWorkspaceMetricsForScope` states at length. A nullable "means every repo" parameter would
52
+ * widen an empty workspace to the whole account, which is the opposite of what a scope is.
53
+ * `scope.workspaceId` is not decoration either: it is the JUDGEMENT grain, the thing that decides
54
+ * who counts as automation, and it is why this takes a `BotScope` rather than a bare id list.
55
+ */
56
+ export async function getWorkspaceRepoActivity(accountId, scope, nowMs) {
57
+ if (scope.repoIds.length === 0)
58
+ return null;
59
+ // Ownership narrowing, mirroring getWorkspaceMetricsForScope: `resolveWorkspaceScope` already
60
+ // guarantees `repoIds ⊆ this workspace's membership`, and this second predicate makes the tenancy
61
+ // structural in the query rather than inherited from a caller's memory.
62
+ const ownedRepos = await db
63
+ .select({ id: repos.id, owner: repos.owner, name: repos.name, createdAt: repos.createdAt })
64
+ .from(repos)
65
+ .where(and(eq(repos.accountId, accountId), inArray(repos.id, scope.repoIds)))
66
+ .execute();
67
+ if (ownedRepos.length === 0)
68
+ return null;
69
+ const ownedIds = ownedRepos.map((r) => r.id);
70
+ const toMs = nowMs;
71
+ const fromMs = toMs - REPO_ACTIVITY_WINDOW_DAYS * 86_400_000;
72
+ // ⚠ TWO-SIDED AND HALF-OPEN, `[fromMs, toMs)`. `gte`/`lt`, never `lte` on the upper bound: a PR
73
+ // opened at exactly `toMs` belongs to the next read of this window, not to both.
74
+ const prs = await db
75
+ .select({
76
+ repoId: pullRequests.repoId,
77
+ authorId: pullRequests.authorId,
78
+ additions: pullRequests.additions,
79
+ deletions: pullRequests.deletions,
80
+ changedFiles: pullRequests.changedFiles,
81
+ })
82
+ .from(pullRequests)
83
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, ownedIds), gte(pullRequests.openedAt, new Date(fromMs)), lt(pullRequests.openedAt, new Date(toMs))))
84
+ .execute();
85
+ // THE AUTOMATION SET. `automatedIds`, never `automatedReviewerUserIds` alone. Real accounts carry
86
+ // the same actor as two rows with conflicting flags (`dependabot` and `dependabot[bot]`), and on
87
+ // the measured account one of each pair sat at `automated: 0`. Trusting one signal splits one bot
88
+ // across both series and under-counts each.
89
+ //
90
+ // ⚠ ITS CANDIDATE SET IS (workspace verdict ∪ globally `users.isBot`), and the login vocabularies
91
+ // refine a lane INSIDE that set rather than widening it — a vocabulary hit counts on its own for
92
+ // a candidate, but admits nobody. So an actor with a vendor login, `isBot: false` and no
93
+ // `workspace_reviewers` row lands in the HUMAN series. That is deliberate: it is the answer every
94
+ // other lane consumer gives, and re-deriving "is this a bot" here to be cleverer would put a
95
+ // second classifier on screen that can disagree with the Timeline's own bot-hiding.
96
+ const lanes = await resolveActorLanes(accountId, scope);
97
+ const byRepo = new Map();
98
+ for (const p of prs) {
99
+ let acc = byRepo.get(p.repoId);
100
+ if (!acc) {
101
+ acc = { human: 0, automation: 0, lines: 0, sized: 0, unsized: 0 };
102
+ byRepo.set(p.repoId, acc);
103
+ }
104
+ // A null author is neither evidence of a bot nor of a person; it counts as human for the same
105
+ // reason `resolveActorLanes` rule 2 does — the absence of an automation signal is not one.
106
+ if (p.authorId != null && lanes.automatedIds.has(p.authorId))
107
+ acc.automation += 1;
108
+ else
109
+ acc.human += 1;
110
+ // ⚠ THE SANCTIONED UNSIZED GUARD, copied verbatim from period-metrics.ts. The three size
111
+ // columns are NOT NULL DEFAULT 0, so a PR whose detail never hydrated is byte-identical to one
112
+ // that changed nothing. Summing the fabricated zero is invisible; summing 80% of a repository's
113
+ // PRs and calling it that repository's line count is a wrong number with no way to notice.
114
+ const observed = p.changedFiles > 0 || p.additions > 0 || p.deletions > 0;
115
+ if (observed) {
116
+ acc.sized += 1;
117
+ acc.lines += p.additions + p.deletions;
118
+ }
119
+ else {
120
+ acc.unsized += 1;
121
+ }
122
+ }
123
+ const rows = [];
124
+ for (const r of ownedRepos) {
125
+ const acc = byRepo.get(r.id);
126
+ if (!acc)
127
+ continue; // a repository with nothing opened in the window draws no band at all
128
+ rows.push({
129
+ repoId: r.id,
130
+ repoFullName: `${r.owner}/${r.name}`,
131
+ prsOpenedHuman: acc.human,
132
+ prsOpenedAutomation: acc.automation,
133
+ linesChanged: acc.sized > 0 ? acc.lines : null,
134
+ sizedPrs: acc.sized,
135
+ unsizedPrs: acc.unsized,
136
+ // ⚠ `repos.createdAt` is when the repo was added to THIS ACCOUNT, not when it was created on
137
+ // GitHub — which is exactly the axis wanted: it bounds how much of the window we could
138
+ // possibly have observed. A real `>=` against the window start; sqlite stores these as epoch
139
+ // SECONDS, so no `+1ms` fudging.
140
+ addedDuringWindow: r.createdAt.getTime() >= fromMs,
141
+ });
142
+ }
143
+ // Rank by TOTAL PRs opened — the primary metric, and the one both charts are ordered by so a
144
+ // reader can scan the same repository across the pair without re-finding it. Ties break on the
145
+ // repo name so the order is stable between requests rather than heap order.
146
+ rows.sort((a, b) => b.prsOpenedHuman +
147
+ b.prsOpenedAutomation -
148
+ (a.prsOpenedHuman + a.prsOpenedAutomation) ||
149
+ a.repoFullName.localeCompare(b.repoFullName));
150
+ const shown = rows.slice(0, REPO_ACTIVITY_MAX_REPOS);
151
+ const cut = rows.slice(REPO_ACTIVITY_MAX_REPOS);
152
+ // What the cap cut, so the cards can state it. The lines chart is ranked by the PR count, so the
153
+ // repository that leads on lines CAN be below the fold — `omitted.linesChanged` is the only thing
154
+ // that lets the reader see that happened. Null when nothing cut was ever sized, for the same
155
+ // reason a row's own figure is: unknown is not zero.
156
+ const cutSized = cut.reduce((n, r) => n + r.sizedPrs, 0);
157
+ return {
158
+ windowDays: REPO_ACTIVITY_WINDOW_DAYS,
159
+ from: new Date(fromMs).toISOString(),
160
+ to: new Date(toMs).toISOString(),
161
+ repos: shown,
162
+ activeRepos: rows.length,
163
+ workspaceRepos: ownedRepos.length,
164
+ omitted: {
165
+ repos: cut.length,
166
+ prsOpened: cut.reduce((n, r) => n + r.prsOpenedHuman + r.prsOpenedAutomation, 0),
167
+ linesChanged: cutSized > 0 ? cut.reduce((n, r) => n + (r.linesChanged ?? 0), 0) : null,
168
+ },
169
+ };
170
+ }
171
+ //# sourceMappingURL=repo-activity.js.map
@@ -0,0 +1,138 @@
1
+ import { inArray, lt } from 'drizzle-orm';
2
+ import { db, runTransaction, schema } from './client.js';
3
+ import { config } from '../config.js';
4
+ // Time-based retention sweep. Per-account server data (events, PRs + their whole subtree)
5
+ // accumulates forever otherwise; this prunes anything past the retention window so the DB
6
+ // stays bounded (the pre-launch, tight-budget scalability lever). Runs periodically from
7
+ // the scheduler in BOTH deployment modes.
8
+ //
9
+ // THE ONE SAFE TTL KEY is `pullRequests.updatedAt < cutoff`, and we delete the WHOLE PR
10
+ // subtree anchored to that. Two load-bearing reasons:
11
+ // 1. Idempotency: the incremental sync re-walks a window ending at `lastIncremental −
12
+ // overlap` (~20 min) and a forced full sync re-walks `now − backfillDays`. A PR whose
13
+ // updatedAt is beyond the (clamped ≥ backfillDays) cutoff is outside BOTH windows, so
14
+ // it is never re-fetched → stays deleted. Keying on a CHILD's own timestamp instead
15
+ // (an old commit/event on a still-recent PR) would let the next sync re-create it →
16
+ // churn. So we anchor everything to the parent PR's recency.
17
+ // 2. FK integrity: there is NO `ON DELETE CASCADE` anywhere (FKs ARE enforced), so the
18
+ // subtree is deleted manually, parent-last, in a transaction — mirroring deleteRepo.
19
+ const { events, ciStatusEvents, reviewComments, reviewThreads, prComments, reviews, commits, reviewRequests, prViews, claudeReviews, claudeReviewFindings, pullRequests, prMentions, } = schema;
20
+ const retentionHandlers = [];
21
+ export function registerRetentionHandler(h) {
22
+ retentionHandlers.push(h);
23
+ }
24
+ // Batch size for the delete — bounds each transaction + the IN-clause size on both dialects.
25
+ const BATCH = 500;
26
+ function chunk(arr, n) {
27
+ const out = [];
28
+ for (let i = 0; i < arr.length; i += n)
29
+ out.push(arr.slice(i, i + n));
30
+ return out;
31
+ }
32
+ // Delete a batch of PRs and everything hanging off them, FK-safe order (children before
33
+ // parents — the exact order deleteRepo uses). `tx` is the dialect-aware transaction executor.
34
+ async function deletePrSubtree(tx, prIds) {
35
+ await tx.delete(events).where(inArray(events.prId, prIds)).execute();
36
+ // CI status history FKs pull_requests with ON DELETE no action (migrations 0022 / pg
37
+ // 0011) — it MUST go before the pullRequests delete below or the sweep FK-fails.
38
+ await tx.delete(ciStatusEvents).where(inArray(ciStatusEvents.prId, prIds)).execute();
39
+ await tx.delete(reviewComments).where(inArray(reviewComments.prId, prIds)).execute();
40
+ await tx.delete(reviewThreads).where(inArray(reviewThreads.prId, prIds)).execute();
41
+ await tx.delete(prComments).where(inArray(prComments.prId, prIds)).execute();
42
+ await tx.delete(reviews).where(inArray(reviews.prId, prIds)).execute();
43
+ await tx.delete(commits).where(inArray(commits.prId, prIds)).execute();
44
+ await tx.delete(reviewRequests).where(inArray(reviewRequests.prId, prIds)).execute();
45
+ // Review-request HISTORY (migration 0066 / pg 0053) — a PR child with no cascade; it must go
46
+ // before the pullRequests delete below or the sweep FK-fails.
47
+ await tx
48
+ .delete(schema.reviewRequestEvents)
49
+ .where(inArray(schema.reviewRequestEvents.prId, prIds))
50
+ .execute();
51
+ await tx.delete(prViews).where(inArray(prViews.prId, prIds)).execute();
52
+ // "@you" mention rows (migration 0056 / pg 0043). The pr_id FK cascades, but this sweep runs
53
+ // parent-last by hand on both dialects and a mention row outliving its PR would go on claiming
54
+ // a deleted PR is personally relevant.
55
+ await tx.delete(prMentions).where(inArray(prMentions.prId, prIds)).execute();
56
+ // My Turn dismissals of these PRs (migration 0069 / pg 0056) — the composite FK cascades, but this
57
+ // sweep runs parent-last by hand on both dialects.
58
+ await tx
59
+ .delete(schema.myTurnDismissals)
60
+ .where(inArray(schema.myTurnDismissals.prId, prIds))
61
+ .execute();
62
+ // Claude review runs + findings FK these PRs — clear findings (via reviewId) then runs.
63
+ const reviewIdRows = await tx
64
+ .select({ id: claudeReviews.id })
65
+ .from(claudeReviews)
66
+ .where(inArray(claudeReviews.prId, prIds))
67
+ .execute();
68
+ const reviewIds = reviewIdRows.map((r) => r.id);
69
+ if (reviewIds.length > 0) {
70
+ // Claude Review chat messages (migration 0073 / pg 0060) FK both the runs and the findings —
71
+ // they go first.
72
+ await tx
73
+ .delete(schema.claudeReviewChatMessages)
74
+ .where(inArray(schema.claudeReviewChatMessages.reviewId, reviewIds))
75
+ .execute();
76
+ await tx
77
+ .delete(claudeReviewFindings)
78
+ .where(inArray(claudeReviewFindings.reviewId, reviewIds))
79
+ .execute();
80
+ }
81
+ await tx.delete(claudeReviews).where(inArray(claudeReviews.prId, prIds)).execute();
82
+ // AI Fix runs (core since migration 0074 / pg 0061; the plugin's retention hook used to prune
83
+ // them). No FKs — explicit, pr-keyed.
84
+ await tx.delete(schema.aiFixes).where(inArray(schema.aiFixes.prId, prIds)).execute();
85
+ await tx.delete(pullRequests).where(inArray(pullRequests.id, prIds)).execute();
86
+ // NB: commitFiles is GLOBAL (sha-keyed, shared across PRs/tenants) — deliberately NOT
87
+ // pruned here. users + syncState are likewise out of scope.
88
+ //
89
+ // NB2: `trunk_ci_status_events` (migration 0052 / pg 0039) is deliberately absent, and its
90
+ // absence is STRUCTURAL, not an oversight. This whole sweep is anchored to a parent PR's
91
+ // `updatedAt` (see the header) and a trunk CI observation has no PR — there is nothing here to
92
+ // key it on, and inventing a second, differently-keyed sweep would give the file two retention
93
+ // definitions. It is bounded instead by a HYBRID trim in its own writer
94
+ // (`sync/branch-status.ts` — `staleTrunkCiEventIds`: the newest `TRUNK_CI_EVENT_WINDOW` rows per
95
+ // repo ∪ everything inside `FEED_WINDOW_DAYS`), which runs on every branch snapshot, and it is
96
+ // cleared outright by `deleteRepo` + `eraseAccountData`. ⚠ It is NOT a plain count trim: a
97
+ // pure newest-N bound evicted the failure rows the Feed reads on repos that sync faster than the
98
+ // read window elapses. `branch_commits` is out for the same reason (its hybrid trim lives in the
99
+ // same writer).
100
+ }
101
+ // Prune every PR (and subtree) older than the retention window. `retentionDays` defaults
102
+ // to config; a test can pass a value. Returns the number of PRs pruned. No-op when
103
+ // retention is disabled (days <= 0).
104
+ export async function pruneOldData(log, retentionDays = config.retentionDays) {
105
+ if (!retentionDays || retentionDays <= 0)
106
+ return 0;
107
+ // Clamp to the backfill window so a forced full sync can't resurrect a deleted PR.
108
+ const effDays = Math.max(retentionDays, config.backfillDays);
109
+ const cutoff = new Date(Date.now() - effDays * 24 * 60 * 60 * 1000);
110
+ const oldRows = await db
111
+ .select({ id: pullRequests.id })
112
+ .from(pullRequests)
113
+ .where(lt(pullRequests.updatedAt, cutoff))
114
+ .execute();
115
+ const prIds = oldRows.map((r) => r.id);
116
+ if (prIds.length === 0)
117
+ return 0;
118
+ let pruned = 0;
119
+ for (const batch of chunk(prIds, BATCH)) {
120
+ await runTransaction(async (tx) => {
121
+ await deletePrSubtree(tx, batch);
122
+ });
123
+ // Prune plugin-owned tables for these PRs (outside the core tx — the plugin owns its
124
+ // own transactions; a plugin failure must not roll back the core delete). Best-effort.
125
+ for (const h of retentionHandlers) {
126
+ try {
127
+ await h({ prIds: batch });
128
+ }
129
+ catch (err) {
130
+ log.warn({ err }, 'retention: pro handler failed for a batch');
131
+ }
132
+ }
133
+ pruned += batch.length;
134
+ }
135
+ log.info(`retention: pruned ${pruned} PR(s) older than ${effDays}d`);
136
+ return pruned;
137
+ }
138
+ //# sourceMappingURL=retention.js.map
@@ -0,0 +1,240 @@
1
+ // CLAUDE REVIEW — the OTHER reviewers' open threads on a PR (CORE, DB-only, no GitHub call).
2
+ //
3
+ // Two readers, ONE predicate:
4
+ // • the review run loads every unresolved review thread that is NOT rooted on one of Limn's own
5
+ // posted findings (`loadReviewThreadsForReview`) and asks Claude, per thread, whether the
6
+ // comment is right and whether the code has dealt with it (review/claude-review/threads.ts);
7
+ // • the auto-review sweeper re-reviews the SAME head when a newer qualifying comment arrives
8
+ // (`newestReviewCommentAt`, read by `getAutoReviewCandidates`).
9
+ //
10
+ // ⚠ A QUALIFYING COMMENT is a review-thread comment in an UNRESOLVED thread that Limn did NOT post,
11
+ // by a person or a review bot (the `countsAuthor` filter below). Both readers go through `isLimnPostedComment`, so the comment
12
+ // the run records as "seen" (`claude_reviews.comments_through`) and the comment the sweeper treats
13
+ // as "new" can never disagree; if they did, a review could re-trigger itself forever.
14
+ //
15
+ // ⚠ HOW WE KNOW LIMN POSTED IT. Limn posts AS THE READER (their own GitHub token), so TWO facts
16
+ // must hold together:
17
+ // A. the comment's author is the ACCOUNT'S OWN GitHub login (accounts.github_login, compared
18
+ // case-insensitively) — a marker someone else pasted, or a teammate's own Limn post, is
19
+ // another reviewer's comment; and
20
+ // B. it carries Limn's provenance — any one of:
21
+ // 1. the hidden `<!-- pierre:claude-review` marker. Every finding comment carries
22
+ // `<!-- pierre:claude-review-finding v=1 -->` (post-review.ts FINDING_COMMENT_MARKER), review
23
+ // bodies their own spelling of it. It survives an edit on GitHub. ⚠ The retired per-ticket
24
+ // "Post as comment" stamped `<!-- pierre:claude-review-ticket v=1 -->`; nothing produces it
25
+ // now (a story's results post as findings), but comments already on GitHub carry it, so
26
+ // the match stays a PREFIX — never narrow it to the finding spelling;
27
+ // 2. LEGACY, for comments posted before that marker existed: the comment's GitHub id is a
28
+ // posted finding's `github_comment_id`, or its text STARTS WITH a posted finding's resolved
29
+ // body (an edit on GitHub defeats this one — hence the marker).
30
+ // An unknown author (a deleted account, an unsynced user) is never "own".
31
+ //
32
+ // ⚠ ONLY REAL REVIEWERS MOVE THE TRIGGER. The comment-time half (`newestReviewCommentAt`, and the
33
+ // loader's `newestCommentAt` that becomes `comments_through`) also takes a `countsAuthor` filter —
34
+ // built ONCE per workspace by db/queries.ts `reReviewCommentAuthorFilter`: a person, or a bot whose
35
+ // role in this workspace is `'review'`. CI, coverage, deploy and dependency bots never re-trigger a
36
+ // review. Both readers get the SAME filter, so what a run records as seen and what the sweeper
37
+ // calls new still agree. ⚠ The THREADS themselves are not filtered: the review still assesses
38
+ // every open thread, whoever wrote it.
39
+ import { and, eq, gt, inArray, isNotNull } from 'drizzle-orm';
40
+ import { db, schema } from './client.js';
41
+ import { globalAutomationUserIds } from './automation-ids.js';
42
+ const LIMN_MARKER = /<!--\s*pierre:claude-review/i;
43
+ const norm = (s) => s.replace(/\r\n?/g, '\n').trim();
44
+ /** The account's own GitHub login — the author half of `isLimnPostedComment`. */
45
+ export async function accountGithubLogin(accountId) {
46
+ const row = (await db
47
+ .select({ login: schema.accounts.githubLogin })
48
+ .from(schema.accounts)
49
+ .where(eq(schema.accounts.id, accountId))
50
+ .limit(1)
51
+ .execute())[0];
52
+ return row?.login ?? null;
53
+ }
54
+ /** Is this review-thread comment one Limn posted? (The ONE predicate — see the header.) */
55
+ export function isLimnPostedComment(c, own, accountLogin) {
56
+ if (!accountLogin || !c.authorLogin)
57
+ return false;
58
+ if (c.authorLogin.toLowerCase() !== accountLogin.toLowerCase())
59
+ return false;
60
+ const body = c.body ?? '';
61
+ if (LIMN_MARKER.test(body))
62
+ return true;
63
+ if (!own)
64
+ return false;
65
+ if (c.databaseId != null && own.commentIds.has(c.databaseId))
66
+ return true;
67
+ const text = norm(body);
68
+ if (!text)
69
+ return false;
70
+ return own.bodies.some((b) => b.length > 0 && text.startsWith(b));
71
+ }
72
+ /** Every POSTED finding's resolved body + stored comment id, per PR (account-scoped). */
73
+ export async function ownPostedCommentsForPrs(accountId, prIds) {
74
+ const out = new Map();
75
+ if (prIds.length === 0)
76
+ return out;
77
+ const { claudeReviews: cr, claudeReviewFindings: crf } = schema;
78
+ const rows = await db
79
+ .select({
80
+ prId: cr.prId,
81
+ body: crf.body,
82
+ editedBody: crf.editedBody,
83
+ githubCommentId: crf.githubCommentId,
84
+ })
85
+ .from(crf)
86
+ .innerJoin(cr, eq(cr.id, crf.reviewId))
87
+ .where(and(eq(cr.accountId, accountId), inArray(cr.prId, [...prIds]), isNotNull(crf.postedAt)))
88
+ .execute();
89
+ for (const r of rows) {
90
+ let o = out.get(r.prId);
91
+ if (!o) {
92
+ o = { bodies: [], commentIds: new Set() };
93
+ out.set(r.prId, o);
94
+ }
95
+ const resolved = r.editedBody && r.editedBody.trim() ? r.editedBody : r.body;
96
+ o.bodies.push(norm(resolved));
97
+ if (r.githubCommentId)
98
+ o.commentIds.add(r.githubCommentId);
99
+ }
100
+ return out;
101
+ }
102
+ /**
103
+ * The newest QUALIFYING review-thread comment per PR, created strictly after `since` (a lower bound
104
+ * that keeps the scan small; pass the oldest cutoff you care about). PRs with none are absent.
105
+ */
106
+ export async function newestReviewCommentAt(accountId, prIds, since,
107
+ // TRAILING: only these authors' comments count (see the header). Absent ⇒ every author.
108
+ countsAuthor) {
109
+ const out = new Map();
110
+ if (prIds.length === 0)
111
+ return out;
112
+ const { reviewComments: rc, reviewThreads: rt, pullRequests: prs, users } = schema;
113
+ const rows = await db
114
+ .select({
115
+ prId: rc.prId,
116
+ createdAt: rc.createdAt,
117
+ body: rc.body,
118
+ databaseId: rc.databaseId,
119
+ authorId: rc.authorId,
120
+ authorLogin: users.githubLogin,
121
+ })
122
+ .from(rc)
123
+ .innerJoin(rt, eq(rt.id, rc.threadId))
124
+ .innerJoin(prs, eq(prs.id, rc.prId))
125
+ .leftJoin(users, eq(users.id, rc.authorId))
126
+ .where(and(eq(prs.accountId, accountId), inArray(rc.prId, [...prIds]), eq(rt.isResolved, false), gt(rc.createdAt, since)))
127
+ .execute();
128
+ if (rows.length === 0)
129
+ return out;
130
+ const [own, login] = await Promise.all([
131
+ ownPostedCommentsForPrs(accountId, [...new Set(rows.map((r) => r.prId))]),
132
+ accountGithubLogin(accountId),
133
+ ]);
134
+ for (const r of rows) {
135
+ if (isLimnPostedComment(r, own.get(r.prId), login))
136
+ continue;
137
+ if (countsAuthor && !countsAuthor(r.authorId ?? null))
138
+ continue;
139
+ const prev = out.get(r.prId);
140
+ if (!prev || r.createdAt.getTime() > prev.getTime())
141
+ out.set(r.prId, r.createdAt);
142
+ }
143
+ return out;
144
+ }
145
+ /**
146
+ * Every unresolved review thread on the PR that is NOT rooted on one of Limn's own posted findings,
147
+ * with its comments. Account-scoped (a foreign PR reads as no threads). `authorIsBot` is the GLOBAL
148
+ * automation set (`globalAutomationUserIds`) — the ball rule's set, which needs no workspace.
149
+ */
150
+ export async function loadReviewThreadsForReview(accountId, prId,
151
+ // TRAILING: which authors move `newestCommentAt` (never which threads are sent). Absent ⇒ all.
152
+ countsAuthor) {
153
+ const { reviewComments: rc, reviewThreads: rt, pullRequests: prs, repos, users, commits } = schema;
154
+ const pr = (await db
155
+ .select({ number: prs.number, owner: repos.owner, name: repos.name })
156
+ .from(prs)
157
+ .innerJoin(repos, eq(repos.id, prs.repoId))
158
+ .where(and(eq(prs.id, prId), eq(prs.accountId, accountId)))
159
+ .limit(1)
160
+ .execute())[0];
161
+ if (!pr)
162
+ return { threads: [], newestCommentAt: null };
163
+ const rows = await db
164
+ .select({
165
+ threadId: rt.id,
166
+ path: rt.path,
167
+ line: rt.line,
168
+ isOutdated: rt.isOutdated,
169
+ derivedState: rt.derivedState,
170
+ commentId: rc.id,
171
+ body: rc.body,
172
+ excerpt: rc.excerpt,
173
+ databaseId: rc.databaseId,
174
+ createdAt: rc.createdAt,
175
+ authorId: rc.authorId,
176
+ authorLogin: users.githubLogin,
177
+ })
178
+ .from(rt)
179
+ .innerJoin(rc, eq(rc.threadId, rt.id))
180
+ .leftJoin(users, eq(users.id, rc.authorId))
181
+ .where(and(eq(rt.prId, prId), eq(rt.isResolved, false)))
182
+ .execute();
183
+ if (rows.length === 0)
184
+ return { threads: [], newestCommentAt: null };
185
+ const [ownMap, bots, commitRows, login] = await Promise.all([
186
+ ownPostedCommentsForPrs(accountId, [prId]),
187
+ globalAutomationUserIds(),
188
+ db.select({ committedAt: commits.committedAt }).from(commits).where(eq(commits.prId, prId)).execute(),
189
+ accountGithubLogin(accountId),
190
+ ]);
191
+ const own = ownMap.get(prId);
192
+ const commitTimes = commitRows.map((c) => c.committedAt.getTime());
193
+ const after = (t) => commitTimes.filter((c) => c > t.getTime()).length;
194
+ const byThread = new Map();
195
+ for (const r of rows) {
196
+ const list = byThread.get(r.threadId) ?? [];
197
+ list.push(r);
198
+ byThread.set(r.threadId, list);
199
+ }
200
+ let newest = null;
201
+ const threads = [];
202
+ for (const list of byThread.values()) {
203
+ list.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.commentId - b.commentId);
204
+ const root = list[0];
205
+ const rootIsOwn = isLimnPostedComment(root, own, login);
206
+ const kept = list.filter((c) => !isLimnPostedComment(c, own, login));
207
+ for (const c of kept) {
208
+ if (countsAuthor && !countsAuthor(c.authorId ?? null))
209
+ continue;
210
+ if (!newest || c.createdAt > newest)
211
+ newest = c.createdAt;
212
+ }
213
+ // A thread rooted on Limn's own posted finding belongs to the follow-up, not here.
214
+ if (rootIsOwn || kept.length === 0)
215
+ continue;
216
+ const first = kept[0];
217
+ const last = kept[kept.length - 1];
218
+ threads.push({
219
+ threadId: root.threadId,
220
+ path: root.path,
221
+ line: root.line,
222
+ isOutdated: root.isOutdated,
223
+ derivedState: root.derivedState,
224
+ url: root.databaseId
225
+ ? `https://github.com/${pr.owner}/${pr.name}/pull/${pr.number}#discussion_r${root.databaseId}`
226
+ : null,
227
+ comments: kept.map((c) => ({
228
+ authorLogin: c.authorLogin ?? null,
229
+ authorIsBot: c.authorId != null && bots.has(c.authorId),
230
+ createdAt: c.createdAt,
231
+ body: (c.body ?? c.excerpt ?? '').trim(),
232
+ })),
233
+ commitsAfterFirstComment: after(first.createdAt),
234
+ commitsAfterLastComment: after(last.createdAt),
235
+ });
236
+ }
237
+ threads.sort((a, b) => a.threadId - b.threadId);
238
+ return { threads, newestCommentAt: newest };
239
+ }
240
+ //# sourceMappingURL=review-threads-for-review.js.map
@@ -0,0 +1,24 @@
1
+ import { resolve } from 'node:path';
2
+ import { db, isPg } from './client.js';
3
+ // Apply pending migrations against the shared connection. Dialect-aware: the
4
+ // sqlite migrator is synchronous, the node-postgres migrator is async — so this
5
+ // is `async` either way and the caller (index.ts / migrate.ts) awaits it. Each
6
+ // dialect resolves its own migrations folder next to the compiled output
7
+ // (dist/db/migrations or dist/db/migrations-pg). Safe at server startup — does
8
+ // not close the connection.
9
+ export async function runMigrations() {
10
+ const migrationsFolder = resolve(import.meta.dirname, isPg ? 'migrations-pg' : 'migrations');
11
+ if (isPg) {
12
+ const { migrate } = await import('drizzle-orm/node-postgres/migrator');
13
+ await migrate(db, {
14
+ migrationsFolder,
15
+ });
16
+ }
17
+ else {
18
+ const { migrate } = await import('drizzle-orm/better-sqlite3/migrator');
19
+ // `db` is typed as the pg instance; at runtime here it's the better-sqlite3
20
+ // one, which is what the sqlite migrator expects.
21
+ migrate(db, { migrationsFolder });
22
+ }
23
+ }
24
+ //# sourceMappingURL=run-migrations.js.map