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,240 @@
1
+ // Pending-board liveness — the DATABASE half (target resolution + the narrow write-back).
2
+ //
3
+ // The GitHub half is github/pr-liveness.ts; the route that composes the two is
4
+ // `POST /api/attention/liveness` in api/routes/insights.ts. This module owns two things:
5
+ //
6
+ // 1. RESOLVING a client-supplied list of local PR ids to GitHub node ids, under the account
7
+ // AND the resolved workspace's membership. That resolve IS the isolation guarantee for a
8
+ // route whose ids arrive in a request body: an id belonging to another tenant, to another
9
+ // workspace, or to nothing at all simply does not come back, so it can never reach GitHub
10
+ // and can never be written to. (scripts/verify-isolation.ts exercises it directly.)
11
+ // 2. APPLYING what GitHub said, one narrow UPDATE per genuinely-moved row.
12
+ //
13
+ // It lives beside queries.ts rather than in it, on the db/repo-activity.ts precedent set earlier
14
+ // in this batch: queries.ts is already 9k lines and carries literal NUL bytes that make `rg`/
15
+ // `grep` under-report against it, so new folds go in a file a search can actually find.
16
+ import { and, eq, inArray } from 'drizzle-orm';
17
+ import { db, schema } from './client.js';
18
+ import { READY_MERGE_STATES } from './triage.js';
19
+ const { pullRequests } = schema;
20
+ /**
21
+ * Resolve the caller's PR ids to liveness targets, scoped to the account AND the workspace's
22
+ * repo membership.
23
+ *
24
+ * ⚠ BOTH PREDICATES, AND NEITHER IS REDUNDANT. `accountId` is the tenancy boundary; `repoIds` is
25
+ * the scope the route echoes back, and without it a caller could name their OWN PRs from another
26
+ * workspace and have this route refresh (and bill GitHub quota for) rows outside the board they
27
+ * claimed to be looking at. `repoIds` comes from `resolveWorkspaceScope`, so `[]` — an empty
28
+ * workspace — correctly resolves nothing rather than widening to the account.
29
+ */
30
+ export async function getPrLivenessTargets(accountId, repoIds, prIds) {
31
+ if (prIds.length === 0 || repoIds.length === 0)
32
+ return [];
33
+ const rows = await db
34
+ .select({
35
+ prId: pullRequests.id,
36
+ repoId: pullRequests.repoId,
37
+ githubNodeId: pullRequests.githubNodeId,
38
+ state: pullRequests.state,
39
+ isDraft: pullRequests.isDraft,
40
+ mergeable: pullRequests.mergeable,
41
+ mergeStateStatus: pullRequests.mergeStateStatus,
42
+ reviewDecision: pullRequests.reviewDecision,
43
+ inMergeQueue: pullRequests.inMergeQueue,
44
+ mergeQueueEntryState: pullRequests.mergeQueueEntryState,
45
+ updatedAt: pullRequests.updatedAt,
46
+ })
47
+ .from(pullRequests)
48
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, repoIds), inArray(pullRequests.id, prIds)))
49
+ .execute();
50
+ return rows.map((r) => ({
51
+ prId: r.prId,
52
+ repoId: r.repoId,
53
+ githubNodeId: r.githubNodeId,
54
+ state: r.state,
55
+ isDraft: r.isDraft,
56
+ mergeable: r.mergeable ?? null,
57
+ mergeStateStatus: r.mergeStateStatus ?? null,
58
+ reviewDecision: r.reviewDecision ?? null,
59
+ inMergeQueue: r.inMergeQueue ?? null,
60
+ mergeQueueEntryState: r.mergeQueueEntryState ?? null,
61
+ updatedAt: r.updatedAt,
62
+ }));
63
+ }
64
+ /**
65
+ * WHICH TARGETS EARN THE EXPENSIVE SECOND PASS, in priority order.
66
+ *
67
+ * The mergeability selection costs ~5s for 25 PRs and 502s the gateway at 50 (see the measured
68
+ * table in PR_LIVENESS_NODES_QUERY), so only a bounded subset can ever have it. The ordering is
69
+ * the whole design decision:
70
+ *
71
+ * 1. PRs whose STORED merge state already puts them on the board as a FORWARD card
72
+ * (READY_MERGE_STATES ∪ {'behind'}). These are the rows offering a Merge / Update-branch
73
+ * button, and a stale `mergeStateStatus` there is a button that 405s — the most expensive
74
+ * kind of wrong this board can be. The board caps forward cards at 15 per kind, so this
75
+ * group is at most 30 PRs and usually far fewer.
76
+ * 2. Everything else still open, most-recently-updated first — the rotation that lets a
77
+ * `blocked → clean` flip (somebody else's approval satisfying branch protection) be noticed
78
+ * at all. It is best-effort by construction: on a board with more than ~25 PRs the tail of
79
+ * this list simply waits for the next adaptive walk, which is exactly what it did before.
80
+ *
81
+ * ⚠ `'dirty'` IS DELIBERATELY NOT IN GROUP 1, even though the `conflicts` card kind is built off
82
+ * exactly this column. Group 1 exists because a stale merge state there is a BUTTON THAT 405s; a
83
+ * conflicts card carries no button, so a stale one costs the reader a wasted row, not a refused
84
+ * merge. It rides the group-2 rotation with everything else, and the SPA's own ranking (which
85
+ * decides which ids reach this function at all) puts conflicts cards first — so the question gets
86
+ * asked; how often it is answered stays best-effort, by design.
87
+ *
88
+ * Already-merged/closed targets are excluded outright: their card is leaving on this very refetch
89
+ * and GitHub will not be recomputing mergeability for them.
90
+ */
91
+ export function rankForMergeStatePass(targets, cap) {
92
+ const forward = (t) => t.mergeStateStatus != null &&
93
+ (t.mergeStateStatus === 'behind' || READY_MERGE_STATES.has(t.mergeStateStatus));
94
+ return targets
95
+ .filter((t) => t.state === 'open')
96
+ .sort((a, b) => Number(forward(b)) - Number(forward(a)) ||
97
+ b.updatedAt.getTime() - a.updatedAt.getTime() ||
98
+ a.prId - b.prId)
99
+ .slice(0, cap);
100
+ }
101
+ /**
102
+ * Write back ONE observation, and only the columns GitHub positively spoke about.
103
+ *
104
+ * ⚠ A COLUMN MAY BE CLEARED ONLY ON A POSITIVE STATEMENT FROM GITHUB. `graphqlTolerant` returns
105
+ * partial data with forbidden fields NULLED, so an absent `mergeable` is indistinguishable from
106
+ * "GitHub says unknown" at the JSON level. `foldLivenessNode` has already collapsed that: it
107
+ * refuses the whole node unless `state` arrived (proving the fragment landed), and it leaves
108
+ * `mergeable`/`mergeStateStatus` UNSET rather than null when they were not asked for. So here,
109
+ * `undefined` means omit the key and anything else is written — including `reviewDecision: null`,
110
+ * which on this path is GitHub saying "this repo requires no review", the same value the fat sync
111
+ * walk stores from the same field, and including `inMergeQueue: false`, which is what a landing or
112
+ * an ejection from the merge queue looks like.
113
+ *
114
+ * ⚠ IT DOES NOT TOUCH `mergedById`. That is the maintainer-inference input and it is not in this
115
+ * selection; inventing one (or clearing it) from a probe that never asked would corrupt a
116
+ * different feature. The next `persistPr` fills it in.
117
+ *
118
+ * Returns null when nothing in the row actually moved — the common case, and what lets the route
119
+ * report an honest `changed` count instead of inviting a refetch on every tick.
120
+ */
121
+ export async function applyPrLiveness(accountId, target, obs) {
122
+ const set = {};
123
+ // `dirty` = the row needs writing at all. `movedOnBoard` = a card can change because of it.
124
+ // Two flags, deliberately: see PrLivenessDiff.movedOnBoard.
125
+ let dirty = false;
126
+ let movedOnBoard = false;
127
+ const change = (onBoard) => {
128
+ dirty = true;
129
+ if (onBoard)
130
+ movedOnBoard = true;
131
+ };
132
+ if (obs.state !== target.state) {
133
+ set.state = obs.state;
134
+ change(true);
135
+ }
136
+ if (obs.isDraft != null && obs.isDraft !== target.isDraft) {
137
+ set.isDraft = obs.isDraft;
138
+ change(true);
139
+ }
140
+ // Timestamps that only ever ARRIVE (GitHub never un-merges), so they carry no comparison and
141
+ // no movement of their own — they ride along so a card leaving the board leaves with a real
142
+ // merged/closed clock instead of a null the next walk has to backfill.
143
+ if (obs.mergedAt != null && target.state !== 'merged') {
144
+ set.mergedAt = new Date(obs.mergedAt);
145
+ dirty = true;
146
+ }
147
+ if (obs.closedAt != null && target.state === 'open') {
148
+ set.closedAt = new Date(obs.closedAt);
149
+ dirty = true;
150
+ }
151
+ if (obs.updatedAt != null) {
152
+ const next = new Date(obs.updatedAt);
153
+ if (next.getTime() !== target.updatedAt.getTime()) {
154
+ set.updatedAt = next;
155
+ change(true);
156
+ }
157
+ }
158
+ // Written whenever it DIFFERS, null included — on this path a null is GitHub saying "this repo
159
+ // requires no review", the same value persistPr stores from the same field, and it reaches us
160
+ // only because `state` proved the fragment landed.
161
+ //
162
+ // ⚠ BOARD MOVEMENT, since the Pending card renders review standing. It was `change(false)` while
163
+ // no card showed the decision, on the argument that 92% of open PRs carry a null one restated
164
+ // every tick. That argument was always about the WRONG branch: the test above is a DIFF, so a
165
+ // restatement — null → null included — never gets here at all. What actually reaches this line is
166
+ // the approval that just landed, which is the single most useful thing the board can learn
167
+ // between walks and the reason someone leaves the tab open.
168
+ if (obs.reviewDecision !== target.reviewDecision) {
169
+ set.reviewDecision = obs.reviewDecision;
170
+ change(true);
171
+ }
172
+ // ── Merge queue — membership and entry state, written as ONE fact ─────────────────────────
173
+ //
174
+ // `undefined` = GitHub said nothing (see PrLivenessObservation): omit both keys, learn nothing.
175
+ // An observed value is written whenever it differs, `false` INCLUDED — a false is GitHub saying
176
+ // the PR is not in the queue, which is what a landing or an EJECTION looks like, and refusing to
177
+ // write it would leave a card claiming a queue place the PR no longer holds.
178
+ //
179
+ // Board movement both ways: the card shows queue membership by default, and `unmergeable` is the
180
+ // state worth interrupting someone for — GitHub is dropping the PR out of the queue.
181
+ //
182
+ // ⚠ THE ENTRY STATE IS WRITTEN WHENEVER MEMBERSHIP WAS OBSERVED, even to null. Not queued ⇒ no
183
+ // entry; queued in a state this codebase does not model ⇒ also null (see mergeQueueEntryStateFrom
184
+ // for why an unmodelled state is not stored raw). That is exactly what persistPr does with the
185
+ // same GitHub answer, and the two writers must agree or one column flip-flops between the walk
186
+ // and this sweep every minute. The unknown-never-demotes-known guard above does NOT apply here:
187
+ // it exists because GitHub answers `mergeable` with UNKNOWN while it computes one, and there is
188
+ // no such in-progress value on the queue fields — they are stored state.
189
+ if (obs.inMergeQueue !== undefined) {
190
+ const nextState = obs.mergeQueueEntryState ?? null;
191
+ if (obs.inMergeQueue !== target.inMergeQueue) {
192
+ set.inMergeQueue = obs.inMergeQueue;
193
+ change(true);
194
+ }
195
+ if (nextState !== target.mergeQueueEntryState) {
196
+ set.mergeQueueEntryState = nextState;
197
+ change(true);
198
+ }
199
+ }
200
+ // ── The two computed fields, and the one rule that keeps this sweep CONVERGENT ────────────
201
+ //
202
+ // `undefined` = the cheap pass never asked. Never conflated with GitHub's own 'unknown'.
203
+ //
204
+ // ⚠ AN OBSERVED 'unknown' DOES NOT OVERWRITE A KNOWN STORED VALUE, and this is a DELIBERATE
205
+ // divergence from `persistPr`, which writes it. GitHub does not store mergeability: asking for
206
+ // it STARTS a background trial merge, and the answer while that runs is UNKNOWN. `persistPr`
207
+ // runs on the adaptive cadence (2-15 min) so it rarely catches that window; this sweep runs
208
+ // every 60s and would sit in it — downgrading `clean` to `unknown`, taking the card's Merge
209
+ // button away, then restoring it a minute later. Every one of those transitions is a `changed`,
210
+ // so the board would refetch on a fixed timer forever with a GitHub call in front of it, which
211
+ // is precisely the churn `changed` exists to prevent.
212
+ //
213
+ // Cost of the guard: a PR whose merge state genuinely became unknowable (a force-push mid-
214
+ // computation) keeps its previous state until the next full walk. That is the same staleness
215
+ // every card had before this route existed, on a narrow slice of PRs, for a couple of minutes.
216
+ const authoritative = (next, stored, unknown) => next !== undefined && next !== stored && !(next === unknown && stored != null);
217
+ if (authoritative(obs.mergeStateStatus, target.mergeStateStatus, 'unknown')) {
218
+ set.mergeStateStatus = obs.mergeStateStatus;
219
+ change(true);
220
+ }
221
+ if (authoritative(obs.mergeable, target.mergeable, 'unknown')) {
222
+ set.mergeable = obs.mergeable;
223
+ // The board's merge GATE reads it (`card.mergeable ?? 'unknown'` → mergeVerdict), so a
224
+ // conflict appearing or clearing really can change a button.
225
+ change(true);
226
+ }
227
+ if (!dirty)
228
+ return null;
229
+ await db
230
+ .update(pullRequests)
231
+ .set(set)
232
+ .where(and(eq(pullRequests.id, target.prId), eq(pullRequests.accountId, accountId)))
233
+ .execute();
234
+ return {
235
+ prId: target.prId,
236
+ movedOnBoard,
237
+ leftOpenSet: target.state === 'open' && obs.state !== 'open',
238
+ };
239
+ }
240
+ //# sourceMappingURL=pr-liveness.js.map
@@ -0,0 +1,279 @@
1
+ // Query layer for "@you was mentioned on this PR" (CORE, free, no AI, no GitHub quota).
2
+ //
3
+ // WHAT THIS ANSWERS. My Turn's `mention` type (S6 of the ball rule, `getMyTurn`): a PERSON typed
4
+ // your login on an open PR, and you have not acted on that PR since. A mention is a summons EVEN
5
+ // IN A REPO YOU ONLY READ — that is the whole point of it, and why it is its own card type rather
6
+ // than a property of the repo. The card clears when you act AFTER the mention, so each row carries
7
+ // the moment of the NEWEST qualifying mention (`mentioned_at`) and who made it.
8
+ //
9
+ // TWO JOBS, and nothing else:
10
+ // 1. MATCH — the pure `@login` word-boundary predicate (exported and unit-pinned)
11
+ // 2. DERIVE — the full "which of this account's PRs mention the viewer, newest when and by whom"
12
+ // set (the scanner's worklist; see sync/mention-scan.ts), and the diff that makes
13
+ // the stored rows equal it. The READ is a join inside `getMyTurn`.
14
+ //
15
+ // WHO CAN SUMMON YOU. A mention by the viewer themself is not a summons, and neither is one by
16
+ // automation — the GLOBAL automation set (`globalAutomationUserIds`), the SAME set the ball rule
17
+ // resolves bot-ness with, so a mention this scanner stamps is always one `getMyTurn` would honour.
18
+ //
19
+ // WHY THE DERIVE IS A FULL RE-SCAN AND NOT AN INCREMENTAL CURSOR. A cursor over the three comment
20
+ // tables would have to be right about four different ways the corpus changes, and each wrong
21
+ // answer is silent: a 90-day BACKFILL inserts rows whose `created_at` predates any time-based
22
+ // watermark; a body EDIT changes neither a row's id nor its created_at; a deleted comment must
23
+ // REMOVE a mention; and Postgres hands out sequence values out of commit order, so an id-based
24
+ // watermark can skip a row permanently. Re-deriving the whole set and diffing it against what is
25
+ // stored is correct under all four with no state to keep, and it is affordable because the
26
+ // expensive half is bounded by the MATCHES, not by the corpus: on this repo's own dev database
27
+ // (65k comment/review bodies, 8.5k PRs) the three scans below run in ~0.19s and return 12 rows.
28
+ // The cost that does scale is the LIKE scan itself, which is why this is a background worker on a
29
+ // multi-minute cron and NEVER a per-request read — `getWorkspaceInsights` runs on every Feed
30
+ // landing.
31
+ import { and, desc, eq, inArray, isNotNull, sql } from 'drizzle-orm';
32
+ import { db, schema } from './client.js';
33
+ import { globalAutomationUserIds } from './automation-ids.js';
34
+ const { prMentions, prComments, pullRequests, reviewComments, reviews, users } = schema;
35
+ // A cap on how many MATCHING rows one account's scan will look at, per source table. Not a
36
+ // correctness bound — it is a guard against a pathological corpus (a bot that @-mentions the
37
+ // account owner on every PR) turning one tick into an unbounded allocation. Each scan orders
38
+ // NEWEST FIRST before the cap, so if it ever bites it drops the OLDEST matches — the ones least
39
+ // likely to be a PR's newest mention, which is the only one a row keeps.
40
+ const MENTION_SCAN_CAP = 20_000;
41
+ // Chunk size for the `IN (…)` lookups. SQLite's default bound-variable limit is 999.
42
+ const ID_CHUNK = 500;
43
+ /**
44
+ * THE MATCH RULE. `@login` as a whole word, case-insensitive.
45
+ *
46
+ * The two failure directions this exists to prevent, both silent:
47
+ * • `@alex` must NOT match "@alexwakeman" and `@alexwakeman` must not match "@alex" — the
48
+ * trailing class rejects a login that merely STARTS with ours (GitHub logins are
49
+ * `[A-Za-z0-9-]`; `_` is in the class as well because a reader cannot tell "@alex_wakeman"
50
+ * apart from a login we do not know, and under-notifying is the safe direction here).
51
+ * • "bob@alexwakeman.com" and "docs/@alexwakeman/notes.md" must not count — the leading class
52
+ * rejects an `@` glued to a word character, a path separator or a dot, which is what an
53
+ * email local part and a path segment look like.
54
+ *
55
+ * Everything else — start of string, whitespace, `(`, `[`, `>`, a backtick — is a real mention
56
+ * position and matches. (`deriveMentionedPrs` drops QUOTED lines before it asks: see
57
+ * `withoutQuotedLines`.)
58
+ *
59
+ * Deliberately NOT a `\b` boundary: `\b` is symmetric about word characters and `@` is not a word
60
+ * character, so `\b@login\b` would happily match the email case above.
61
+ */
62
+ export function mentionPattern(login) {
63
+ // The login is DATA (it comes from `accounts.github_login`, ultimately from GitHub). Escaping
64
+ // it keeps a hyphen or a dot from being read as regex syntax.
65
+ const escaped = login.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ return new RegExp(`(?<![A-Za-z0-9_@./-])@${escaped}(?![A-Za-z0-9_-])`, 'i');
67
+ }
68
+ /** The body without its markdown blockquote lines — what its author TYPED. A quote-reply repeats
69
+ * somebody else's words: "> @you can you look?" is not the quoter summoning you, and counting it
70
+ * would move the mention clock past an action you already took and credit the wrong person. */
71
+ export function withoutQuotedLines(body) {
72
+ return body.replace(/^[ \t]*>.*$/gm, '');
73
+ }
74
+ /** True when `text` @-mentions `login` as a whole word. Empty login never matches. */
75
+ export function mentionsLogin(text, login) {
76
+ if (!text || !login)
77
+ return false;
78
+ return mentionPattern(login).test(text);
79
+ }
80
+ // A `%@login%` LIKE pattern with the LIKE metacharacters escaped, paired with `ESCAPE '\'` below
81
+ // (the db/search.ts idiom). This is a PREFILTER ONLY — it is substring, not word-boundary, so
82
+ // `mentionsLogin` above is the authority on every row it returns.
83
+ function likePattern(login) {
84
+ return `%@${login.toLowerCase().replace(/([\\%_])/g, '\\$1')}%`;
85
+ }
86
+ function likeCol(col, pattern) {
87
+ return sql `lower(${col}) like ${pattern} escape '\\'`;
88
+ }
89
+ /**
90
+ * THE FULL DERIVATION: every PR of this account on which a PERSON other than the viewer
91
+ * @-mentions `login`, with that PR's NEWEST such mention.
92
+ *
93
+ * All three body-bearing tables, because a mention is a mention wherever it was typed: an
94
+ * issue-level PR comment, a review body, an inline review-thread comment. PR TITLES AND
95
+ * DESCRIPTIONS ARE DELIBERATELY OUT — descriptions are not persisted under lean storage
96
+ * (docs/BACKEND.md), so including them would make the answer depend on whether PERSIST_BODIES
97
+ * happened to be on, which is the one thing a derived fact must never do. Comment and review
98
+ * bodies are ALWAYS persisted, so this needs no GitHub fetch and no hydration.
99
+ *
100
+ * ⚠ A SKIPPED ROW IS SKIPPED BEFORE "NEWEST" IS DECIDED. A bot that echoes your login after a
101
+ * colleague's mention must not become the PR's mention (its time would move the card's clock, and
102
+ * the next bot echo would re-summon you after you acted); the colleague's mention stays the newest.
103
+ */
104
+ export async function deriveMentionedPrs(accountId, login) {
105
+ if (!login)
106
+ return [];
107
+ const pat = likePattern(login);
108
+ // Resolved once per scan: who "you" are (a self-mention is not a summons) and who is automation.
109
+ const [viewerRows, bots] = await Promise.all([
110
+ db
111
+ .select({ id: users.id })
112
+ .from(users)
113
+ .where(sql `lower(${users.githubLogin}) = ${login.toLowerCase()}`)
114
+ .limit(1)
115
+ .execute(),
116
+ globalAutomationUserIds(),
117
+ ]);
118
+ const viewerUserId = viewerRows[0]?.id ?? null;
119
+ // Three near-identical queries rather than one parameterised helper: drizzle's column types are
120
+ // per-table, and the generic version only type-checks by widening the columns to `any` — which
121
+ // is exactly the seam that would let a wrong `prId` through unnoticed.
122
+ const [pc, rc, rv] = await Promise.all([
123
+ db
124
+ .select({
125
+ prId: prComments.prId,
126
+ repoId: pullRequests.repoId,
127
+ body: prComments.body,
128
+ authorId: prComments.authorId,
129
+ at: prComments.createdAt,
130
+ })
131
+ .from(prComments)
132
+ .innerJoin(pullRequests, eq(pullRequests.id, prComments.prId))
133
+ .where(and(eq(pullRequests.accountId, accountId), isNotNull(prComments.body), likeCol(prComments.body, pat)))
134
+ .orderBy(desc(prComments.createdAt))
135
+ .limit(MENTION_SCAN_CAP)
136
+ .execute(),
137
+ db
138
+ .select({
139
+ prId: reviewComments.prId,
140
+ repoId: pullRequests.repoId,
141
+ body: reviewComments.body,
142
+ authorId: reviewComments.authorId,
143
+ at: reviewComments.createdAt,
144
+ })
145
+ .from(reviewComments)
146
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
147
+ .where(and(eq(pullRequests.accountId, accountId), isNotNull(reviewComments.body), likeCol(reviewComments.body, pat)))
148
+ .orderBy(desc(reviewComments.createdAt))
149
+ .limit(MENTION_SCAN_CAP)
150
+ .execute(),
151
+ db
152
+ .select({
153
+ prId: reviews.prId,
154
+ repoId: pullRequests.repoId,
155
+ body: reviews.body,
156
+ authorId: reviews.authorId,
157
+ at: reviews.submittedAt,
158
+ })
159
+ .from(reviews)
160
+ .innerJoin(pullRequests, eq(pullRequests.id, reviews.prId))
161
+ .where(and(eq(pullRequests.accountId, accountId), isNotNull(reviews.body), likeCol(reviews.body, pat)))
162
+ .orderBy(desc(reviews.submittedAt))
163
+ .limit(MENTION_SCAN_CAP)
164
+ .execute(),
165
+ ]);
166
+ const out = new Map();
167
+ for (const rows of [pc, rc, rv]) {
168
+ for (const r of rows) {
169
+ // Nobody we can name (a deleted account), yourself, or automation: not a summons.
170
+ if (r.authorId == null || r.authorId === viewerUserId || bots.has(r.authorId))
171
+ continue;
172
+ // ⚠ THE SQL IS A PREFILTER, THE REGEX IS THE ANSWER. `lower(body) LIKE '%@alex%'` matches
173
+ // "@alexwakeman" and "bob@alex.com" too; dropping this line is how "@alex" starts claiming
174
+ // every PR that mentions a colleague with a longer login. And only TYPED lines count — a
175
+ // quoted mention skipped here, before "newest" is decided, cannot restamp the clock.
176
+ if (r.body == null || !mentionsLogin(withoutQuotedLines(r.body), login))
177
+ continue;
178
+ const prev = out.get(r.prId);
179
+ if (prev != null && prev.mentionedAt.getTime() >= r.at.getTime())
180
+ continue;
181
+ out.set(r.prId, {
182
+ prId: r.prId,
183
+ repoId: r.repoId,
184
+ mentionedAt: r.at,
185
+ mentionedById: r.authorId,
186
+ });
187
+ }
188
+ }
189
+ return [...out.values()];
190
+ }
191
+ /** What is currently stored for this account: each PR, the login it was derived under, and the
192
+ * mention clock last stamped on it (NULL until the first scan after migration 0068). */
193
+ export async function listStoredMentions(accountId) {
194
+ return db
195
+ .select({
196
+ id: prMentions.id,
197
+ prId: prMentions.prId,
198
+ login: prMentions.login,
199
+ mentionedAt: prMentions.mentionedAt,
200
+ mentionedByUserId: prMentions.mentionedByUserId,
201
+ })
202
+ .from(prMentions)
203
+ .where(eq(prMentions.accountId, accountId))
204
+ .execute();
205
+ }
206
+ /**
207
+ * Make the stored set EQUAL the derived set for one account — the scanner's only writer.
208
+ *
209
+ * A diff rather than an upsert sweep, because the delete half is load-bearing: a mention edited
210
+ * out of a comment, a deleted comment, a PR that lost its mention when a review was dismissed,
211
+ * and every row derived under a login this account no longer has must all STOP summoning you.
212
+ * An insert-only writer would make the mention card a ratchet that only ever widens.
213
+ *
214
+ * ⚠ AND THE KEPT HALF IS RESTAMPED. A row is "unchanged" only when its login, its mention time and
215
+ * its author all match what was derived; a NEWER mention on the same PR must move `mentioned_at`,
216
+ * or you would act, the card would clear, and the colleague's second "@you?" would never return
217
+ * it.
218
+ *
219
+ * `login` is stored lowercased so the reader's equality test does not depend on how GitHub
220
+ * spelled the login on the day the row was written.
221
+ */
222
+ export async function syncAccountMentions(accountId, login, derived) {
223
+ const canonical = login.toLowerCase();
224
+ const stored = await listStoredMentions(accountId);
225
+ const storedByPr = new Map(stored.map((r) => [r.prId, r]));
226
+ const wanted = new Map(derived.map((d) => [d.prId, d]));
227
+ // Stale = no longer derived, OR derived under a different login (an account rename). Both are
228
+ // "this row no longer states a true fact", so both leave by the same path.
229
+ const staleIds = stored
230
+ .filter((r) => !wanted.has(r.prId) || r.login !== canonical)
231
+ .map((r) => r.id);
232
+ for (let i = 0; i < staleIds.length; i += ID_CHUNK) {
233
+ await db
234
+ .delete(prMentions)
235
+ .where(inArray(prMentions.id, staleIds.slice(i, i + ID_CHUNK)))
236
+ .execute();
237
+ }
238
+ let added = 0;
239
+ let updated = 0;
240
+ for (const d of derived) {
241
+ const existing = storedByPr.get(d.prId);
242
+ const kept = existing != null && existing.login === canonical;
243
+ if (kept &&
244
+ existing.mentionedAt?.getTime() === d.mentionedAt.getTime() &&
245
+ existing.mentionedByUserId === d.mentionedById) {
246
+ continue;
247
+ }
248
+ // An UPSERT on the table's one unique, `(account_id, pr_id)` — the `prm_account_pr` target
249
+ // every writer of this table uses — so a concurrent (or retried) tick cannot turn a duplicate
250
+ // into a thrown scan, and a kept row is restamped in the same statement.
251
+ const res = await db
252
+ .insert(prMentions)
253
+ .values({
254
+ accountId,
255
+ repoId: d.repoId,
256
+ prId: d.prId,
257
+ login: canonical,
258
+ mentionedAt: d.mentionedAt,
259
+ mentionedByUserId: d.mentionedById,
260
+ })
261
+ .onConflictDoUpdate({
262
+ target: [prMentions.accountId, prMentions.prId],
263
+ set: {
264
+ login: canonical,
265
+ repoId: d.repoId,
266
+ mentionedAt: d.mentionedAt,
267
+ mentionedByUserId: d.mentionedById,
268
+ },
269
+ })
270
+ .returning({ id: prMentions.id })
271
+ .execute();
272
+ if (kept)
273
+ updated += res.length;
274
+ else
275
+ added += res.length;
276
+ }
277
+ return { added, updated, removed: staleIds.length };
278
+ }
279
+ //# sourceMappingURL=pr-mentions.js.map
@@ -0,0 +1,91 @@
1
+ // A pull request's GitHub MERGE-QUEUE membership, stamped from a live observation.
2
+ //
3
+ // ⚠ NOT `db/merge-queue.ts`. That module is Limn's OWN per-repo landing order for armed intents
4
+ // (`queued_local`); this one is GitHub's native merge queue, as synced onto `pull_requests`
5
+ // (`in_merge_queue` + `merge_queue_entry_state`). Two queues, two files, never one name.
6
+ //
7
+ // WHY A STAMP AT ALL. A GitHub write is not done when GitHub 200s: the SPA re-reads from the local
8
+ // DB, so an enqueue nobody stamped is invisible until the next adaptive walk — up to fifteen
9
+ // minutes on a cold repo — and the Pending board is forbidden from fetching to find out. So EVERY
10
+ // path that holds a positive answer about membership writes it here:
11
+ //
12
+ // - POST / DELETE /api/prs/:id/merge-queue (the two verbs, and the DELETE's own probe)
13
+ // - GET /api/prs/:id/merge-options, when its queue probe answered
14
+ // - the arm route's AlreadyQueued 409 (it just proved the PR is queued)
15
+ // - the disarm route's dequeue of a watcher-made entry
16
+ // - the auto-merge runner's enqueue, and its per-tick live probe
17
+ //
18
+ // ⚠ POSITIVE ANSWERS ONLY — the partial-response rule ("a column may be CLEARED only on a positive
19
+ // statement from GitHub"). Callers pass a NON-NULL `fetchMergeQueueState` result (non-tolerant
20
+ // GraphQL with `isInMergeQueue: Boolean!`, so a result is a statement) or the outcome of a mutation
21
+ // GitHub accepted. Never the `.catch(() => null)` null of a best-effort probe: that is "we never
22
+ // received it", and writing `false` from it would tell the board a queued PR is not queued.
23
+ //
24
+ // ⚠ THE SYNC'S OWN THREE-STATE FOLD (sync/upsert.ts) DOES NOT COME THROUGH HERE, and must not: a
25
+ // walk that did not carry the selection OMITS the keys, and an unconditional setter cannot say
26
+ // "omit". This helper is correct precisely because every caller holds an answer.
27
+ //
28
+ // ⚠ A STAMP THAT CHANGES THE STORED VALUE RAISES THE CHANGE SIGNAL (`notePrChanged`). The board's
29
+ // liveness sweep compares GitHub against the ROW, so once a route has written the new value the
30
+ // sweep sees no difference and never repaints the card; without the signal a stamp made by the
31
+ // runner or by a GET would silence the one mechanism that used to catch it. A write that changed
32
+ // nothing raises nothing — a signal costs the SPA a cascade of refetches.
33
+ import { and, eq } from 'drizzle-orm';
34
+ import { db, schema } from './client.js';
35
+ import { mergeQueueEntryStateFrom } from '../sync/upsert.js';
36
+ import { notePrChanged } from '../sync/pr-change-signal.js';
37
+ /**
38
+ * Write what GitHub just said about this PR's merge-queue membership. Account-scoped (a foreign
39
+ * id matches no row and writes nothing). Returns true when the stored value CHANGED.
40
+ *
41
+ * `rawEntryState` is GitHub's `MergeQueueEntryState` as it arrived (`QUEUED`, `AWAITING_CHECKS`,
42
+ * …), normalised by the ONE normaliser the sync uses. Out of the queue means no entry — a stale
43
+ * `queued` is never carried past a dequeue. In the queue with no modelled state stores "queued,
44
+ * state unknown", never "not queued".
45
+ */
46
+ export async function stampPrMergeQueueState(prId, accountId, inQueue, rawEntryState) {
47
+ const { pullRequests } = schema;
48
+ const next = {
49
+ inMergeQueue: inQueue,
50
+ mergeQueueEntryState: inQueue ? mergeQueueEntryStateFrom(rawEntryState) : null,
51
+ };
52
+ const owned = and(eq(pullRequests.id, prId), eq(pullRequests.accountId, accountId));
53
+ const rows = await db
54
+ .select({
55
+ repoId: pullRequests.repoId,
56
+ inMergeQueue: pullRequests.inMergeQueue,
57
+ mergeQueueEntryState: pullRequests.mergeQueueEntryState,
58
+ })
59
+ .from(pullRequests)
60
+ .where(owned)
61
+ .execute();
62
+ const row = rows[0];
63
+ if (row == null)
64
+ return false;
65
+ if (row.inMergeQueue === next.inMergeQueue &&
66
+ (row.mergeQueueEntryState ?? null) === next.mergeQueueEntryState) {
67
+ return false;
68
+ }
69
+ await db.update(pullRequests).set(next).where(owned).execute();
70
+ notePrChanged(accountId, row.repoId);
71
+ return true;
72
+ }
73
+ /**
74
+ * The same write, NEVER FATAL — what every ROUTE calls.
75
+ *
76
+ * ⚠ A STAMP IS A LOCAL COPY OF SOMETHING GITHUB ALREADY SAID, so failing to write it is a stale
77
+ * screen, never a failed request. Most call sites sit inside a try whose catch answers
78
+ * `502 GitHubError`: a DB hiccup there would make GET merge-options read "Couldn't load merge
79
+ * status", refuse an arm, or — after GitHub has ACCEPTED an enqueue or a dequeue — tell the reader
80
+ * the write failed when it landed (and invite a retry). "Once GitHub has 201'd the route may not
81
+ * fail." So the failure is logged and swallowed here, once, rather than at each call site.
82
+ */
83
+ export async function stampPrMergeQueueStateNonFatal(prId, accountId, inQueue, rawEntryState, log) {
84
+ try {
85
+ await stampPrMergeQueueState(prId, accountId, inQueue, rawEntryState);
86
+ }
87
+ catch (err) {
88
+ log.warn({ err, prId }, 'merge queue: could not record the queue state on the PR row');
89
+ }
90
+ }
91
+ //# sourceMappingURL=pr-merge-queue-stamp.js.map