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,506 @@
1
+ // The UNSETTLED-PR BACKSTOP and the TRUNK-MOVED RECHECK — re-reading merge state that GitHub
2
+ // changes WITHOUT bumping a pull request's `updatedAt`.
3
+ //
4
+ // THE BLIND SPOT. The adaptive walk re-reads only PRs whose `updatedAt` falls inside
5
+ // `since = lastIncrementalSyncAt − SYNC_OVERLAP_MINUTES`. GitHub finishing a mergeability
6
+ // computation does not bump `updatedAt`, and neither does CI finishing — so a PR the walk stored
7
+ // as `unknown` (GitHub was mid-computation when the walk asked) or as CI `pending` can stay that
8
+ // way for hours. Reproduced on real data: open PRs stored `unknown` (and CI `pending` for over two
9
+ // hours) that GitHub reported as known, with the same `updatedAt`. (A raw count of stored
10
+ // `unknown` rows overstates it: about half of the 132 first counted sat in one repo whose walks
11
+ // were FAILING, which is a different fault, and GitHub itself answers UNKNOWN on a first ask.)
12
+ // The 30-minute re-walk floor does not help — it re-walks, but with the same `since` window,
13
+ // which is exactly what excludes those PRs.
14
+ // The same blindness hits EVERY other open PR in a repo when a merge moves its trunk: each one's
15
+ // verdict (`behind`, `dirty`, `clean`) is now about a base that no longer exists, and nothing
16
+ // about THEM changed on GitHub's clock.
17
+ //
18
+ // SO, TWO CHEAP RE-READS through the Pending board's EXISTING batched liveness path
19
+ // (`fetchPrLivenessForNodes` with merge state — 1 GraphQL point, ≤25 PRs, ~5s — and
20
+ // `applyPrLiveness`, whose guards stay exactly as they are):
21
+ // • runUnsettledPrBackstop — after every successful repo walk (scheduled AND manual): up to 25
22
+ // open non-draft PRs whose stored `mergeable` / `merge_state_status` is unknown or NULL, most
23
+ // recently updated first; plus at most 5 CI-`pending`-for-over-20-minutes PRs handed to the
24
+ // settle ladder (the liveness selection carries no check rollup, so those need the full
25
+ // single-PR read). It costs nothing — no GitHub call at all — on a repo with no such PRs.
26
+ // • scheduleTrunkMovedRecheck — ~30s and ~90s after a merge LANDS (the merge route, the
27
+ // auto-merge runner's direct and queue landings): up to 25 of the repo's open non-draft PRs,
28
+ // forward cards first (a stale `clean` there is a Merge button that 405s).
29
+ //
30
+ // ⚠ GITHUB COMPUTES MERGEABILITY LAZILY: the first ask for a PR often answers UNKNOWN and STARTS
31
+ // the computation (measured: UNKNOWN, then MERGEABLE/CLEAN ~8s later, same `updatedAt`). One read
32
+ // is therefore not a backstop — PRs still unknown after the first pass get ONE second read ~10s
33
+ // later, and the trunk recheck's 90s pass is the 30s pass's second read. A PR still unsettled
34
+ // after two reads running — answered UNKNOWN, answered WITHOUT the merge fields, or not answered
35
+ // at all — is then left alone by the backstop for 30 minutes (`stuckUnknown`, excluded in the
36
+ // query), so a PR GitHub will not compute cannot cost a point on every hot-bucket walk.
37
+ // ⚠ NOT HEAD-AWARE, deliberately. The liveness selection carries no `headRefOid` (documented in
38
+ // github/queries.ts), and `applyPrLiveness`'s "an observed unknown never demotes a known state"
39
+ // rule stays as it is — this module only ever turns unknown INTO known, or lets a later read
40
+ // replace one known value with another.
41
+ // ⚠ NON-FATAL AND BUDGET-AWARE. Nothing here throws into its caller; a limited token is not asked
42
+ // (`isLimited`, and `fetchPrLivenessForNodes` degrades to empty on a limit) — a pause, never red.
43
+ // A read that FAILS (a gateway 502/timeout, a token that cannot be minted) puts the repo's
44
+ // backstop into a 15-minute back-off, so a repo GitHub cannot answer for is not re-asked on every
45
+ // walk.
46
+ // ⚠ IT NEVER WRITES `updatedAt`. `applyPrLiveness` advances it from the observation, but this
47
+ // module exists for updatedAt-SILENT changes: an observed newer `updatedAt` means real activity
48
+ // the walk has not persisted yet, and stamping it here would hide that activity from the SPA's
49
+ // "newer updatedAt → refetch the PR detail" rule once the walk does persist it.
50
+ import { and, desc, eq, inArray, isNull, notInArray, or } from 'drizzle-orm';
51
+ import { getAccessToken } from '../auth/account.js';
52
+ import { db, schema } from '../db/client.js';
53
+ import { applyPrLiveness, rankForMergeStatePass, } from '../db/pr-liveness.js';
54
+ import { fetchPrLivenessForNodes, PR_MERGE_STATE_NODE_BATCH, } from '../github/pr-liveness.js';
55
+ import { isLimited } from '../github/rate-budget.js';
56
+ import { notePrChanged } from './pr-change-signal.js';
57
+ import { getPrSettleFacts, schedulePrSettle } from './pr-settle.js';
58
+ const { pullRequests, ciStatusEvents } = schema;
59
+ const errMsg = (err) => (err instanceof Error ? err.message : String(err));
60
+ /** The merge-state pass's measured batch (25 answers in ~5s; 50 502s the gateway). */
61
+ export const UNSETTLED_BATCH = PR_MERGE_STATE_NODE_BATCH;
62
+ /** How many open PRs the trunk-moved recheck ranks from (forward first, then most recent). */
63
+ const TRUNK_CANDIDATE_CAP = 500;
64
+ /** How many unsettled candidates the backstop selects. The cooling-down PRs are excluded IN THE
65
+ * QUERY (up to COOLING_EXCLUDE_MAX of them), so a repo with many stuck PRs cannot starve the
66
+ * rest; the in-memory filter after it is only the backstop for an over-long exclusion list. */
67
+ const UNSETTLED_CANDIDATE_CAP = 100;
68
+ const COOLING_EXCLUDE_MAX = 1_000;
69
+ /** After a FAILED backstop read (not a rate limit — that pauses), the repo is left alone this long. */
70
+ const FAILED_READ_BACKOFF_MS = 15 * 60_000;
71
+ const FAILED_READ_MAX = 1_000;
72
+ /**
73
+ * A PR still unsettled after two reads running is left alone this long by the backstop.
74
+ * GitHub normally finishes within seconds of being asked, so a PR still unknown after a second
75
+ * read is one it is not computing (or cannot) — and re-asking on every hot-bucket walk (every
76
+ * 2 min, 2 points each with its second read) would spend ~60 points/hour on a single repo for
77
+ * nothing. The trunk-moved recheck ignores this: a moved base is a new question.
78
+ */
79
+ const STUCK_UNKNOWN_COOLDOWN_MS = 30 * 60_000;
80
+ const STUCK_UNKNOWN_MAX = 5_000;
81
+ /** GitHub's lazy computation: the second read of a still-unknown PR. */
82
+ const SECOND_READ_DELAY_MS = 10_000;
83
+ /** CI `pending` for longer than this, with no newer observation, is presumed stale. */
84
+ const CI_STALE_MS = 20 * 60_000;
85
+ /** At most this many CI nudges (one single-PR sync each) per walk. */
86
+ const CI_NUDGES_PER_WALK = 5;
87
+ /** And never the same PR twice inside this window, so a genuinely long CI run costs ~3/hour. */
88
+ const CI_NUDGE_COOLDOWN_MS = 20 * 60_000;
89
+ const CI_NUDGE_MAX = 2_000;
90
+ /** After a merge lands: two re-reads, the second doubling as the first's lazy-computation read. */
91
+ const TRUNK_RECHECK_OFFSETS_MS = [30_000, 90_000];
92
+ const TRUNK_TIMERS_MAX = 1_000;
93
+ function toTarget(r) {
94
+ return {
95
+ prId: r.prId,
96
+ repoId: r.repoId,
97
+ githubNodeId: r.githubNodeId,
98
+ state: r.state,
99
+ isDraft: r.isDraft,
100
+ mergeable: r.mergeable ?? null,
101
+ mergeStateStatus: r.mergeStateStatus ?? null,
102
+ reviewDecision: r.reviewDecision ?? null,
103
+ inMergeQueue: r.inMergeQueue ?? null,
104
+ mergeQueueEntryState: r.mergeQueueEntryState ?? null,
105
+ updatedAt: r.updatedAt,
106
+ };
107
+ }
108
+ /**
109
+ * The repo's PRs worth a merge-state re-read, as liveness targets.
110
+ *
111
+ * ⚠ REPO-ADDRESSED, so ACCOUNT-SCOPED: `(accountId, repoId)` both bind — a repo id belonging to
112
+ * another tenant resolves nothing and can never reach GitHub or a write. Named in
113
+ * scripts/verify-isolation.ts.
114
+ *
115
+ * 'unsettled' — open, non-draft, `mergeable` OR `merge_state_status` unknown/NULL, minus
116
+ * `excludePrIds` (the ones cooling down — see `stuckUnknown`); most recently
117
+ * updated first; up to 100 candidates, of which the caller asks GitHub about 25.
118
+ * 'open' — every open non-draft PR (capped), ranked by `rankForMergeStatePass` (forward
119
+ * cards first); at most 25.
120
+ */
121
+ export async function getRepoMergeStateTargets(accountId, repoId, mode, excludePrIds = []) {
122
+ const base = [
123
+ eq(pullRequests.accountId, accountId),
124
+ eq(pullRequests.repoId, repoId),
125
+ eq(pullRequests.state, 'open'),
126
+ eq(pullRequests.isDraft, false),
127
+ ];
128
+ const exclude = excludePrIds.slice(0, COOLING_EXCLUDE_MAX);
129
+ if (mode === 'unsettled' && exclude.length > 0) {
130
+ base.push(notInArray(pullRequests.id, exclude));
131
+ }
132
+ const where = mode === 'unsettled'
133
+ ? and(...base, or(isNull(pullRequests.mergeable), eq(pullRequests.mergeable, 'unknown'), isNull(pullRequests.mergeStateStatus), eq(pullRequests.mergeStateStatus, 'unknown')))
134
+ : and(...base);
135
+ const rows = await db
136
+ .select({
137
+ prId: pullRequests.id,
138
+ repoId: pullRequests.repoId,
139
+ githubNodeId: pullRequests.githubNodeId,
140
+ state: pullRequests.state,
141
+ isDraft: pullRequests.isDraft,
142
+ mergeable: pullRequests.mergeable,
143
+ mergeStateStatus: pullRequests.mergeStateStatus,
144
+ reviewDecision: pullRequests.reviewDecision,
145
+ inMergeQueue: pullRequests.inMergeQueue,
146
+ mergeQueueEntryState: pullRequests.mergeQueueEntryState,
147
+ updatedAt: pullRequests.updatedAt,
148
+ })
149
+ .from(pullRequests)
150
+ .where(where)
151
+ .orderBy(desc(pullRequests.updatedAt), desc(pullRequests.id))
152
+ .limit(mode === 'unsettled' ? UNSETTLED_CANDIDATE_CAP : TRUNK_CANDIDATE_CAP)
153
+ .execute();
154
+ const targets = rows.map(toTarget);
155
+ return mode === 'unsettled' ? targets : rankForMergeStatePass(targets, UNSETTLED_BATCH);
156
+ }
157
+ // One re-read per (account, repo) at a time — the backstop, its second read and the trunk
158
+ // rechecks can all land on one repo within seconds and would otherwise pay twice for one answer.
159
+ // ⚠ SYNCHRONOUS CLAIM, released in a `finally` that covers every bail path incl. a thrown lookup.
160
+ const recheckInFlight = new Set();
161
+ const repoKey = (accountId, repoId) => `${accountId}:${repoId}`;
162
+ const stuckUnknown = new Map();
163
+ function noteUnknownAnswer(accountId, repoId, prId, stillUnknown) {
164
+ const key = `${accountId}:${prId}`;
165
+ const prev = stuckUnknown.get(key);
166
+ if (!stillUnknown) {
167
+ if (prev)
168
+ stuckUnknown.delete(key);
169
+ return;
170
+ }
171
+ const streak = (prev?.streak ?? 0) + 1;
172
+ if (prev)
173
+ stuckUnknown.delete(key);
174
+ else if (stuckUnknown.size >= STUCK_UNKNOWN_MAX) {
175
+ const oldest = stuckUnknown.keys().next().value;
176
+ if (oldest !== undefined)
177
+ stuckUnknown.delete(oldest);
178
+ }
179
+ stuckUnknown.set(key, {
180
+ accountId,
181
+ repoId,
182
+ prId,
183
+ streak,
184
+ until: streak >= 2 ? Date.now() + STUCK_UNKNOWN_COOLDOWN_MS : 0,
185
+ });
186
+ }
187
+ function coolingDown(accountId, prId, now) {
188
+ const e = stuckUnknown.get(`${accountId}:${prId}`);
189
+ return e != null && e.until > now;
190
+ }
191
+ /** This repo's PRs the backstop is leaving alone right now — excluded in the query. */
192
+ function coolingPrIds(accountId, repoId, now) {
193
+ const out = [];
194
+ for (const e of stuckUnknown.values()) {
195
+ if (e.accountId === accountId && e.repoId === repoId && e.until > now)
196
+ out.push(e.prId);
197
+ }
198
+ return out;
199
+ }
200
+ // `account:repo` → until when the backstop leaves this repo alone after a FAILED read. Bounded FIFO.
201
+ const failedReadUntil = new Map();
202
+ function noteFailedRead(key) {
203
+ if (failedReadUntil.has(key))
204
+ failedReadUntil.delete(key);
205
+ else if (failedReadUntil.size >= FAILED_READ_MAX) {
206
+ const oldest = failedReadUntil.keys().next().value;
207
+ if (oldest !== undefined)
208
+ failedReadUntil.delete(oldest);
209
+ }
210
+ failedReadUntil.set(key, Date.now() + FAILED_READ_BACKOFF_MS);
211
+ }
212
+ const knownValue = (v) => v != null && v !== 'unknown';
213
+ /**
214
+ * Is this PR still UNSETTLED after the read? Judged on what is now STORED — the observed value
215
+ * when GitHub stated a known one, else the stored one (`applyPrLiveness` never lets an observed
216
+ * UNKNOWN demote a known value, and writes nothing for an ABSENT field).
217
+ * ⚠ ABSENT COUNTS AS UNSETTLED, not just a literal UNKNOWN: a token that cannot read the merge
218
+ * fields, or a node GitHub nulled after a partial error, leaves the row unknown for ever, and a
219
+ * streak that reset on every such answer re-asked about that PR on every walk with no back-off.
220
+ */
221
+ function unsettledAfterRead(target, obs) {
222
+ if (obs.state !== 'open')
223
+ return false;
224
+ const mergeable = knownValue(obs.mergeable) ? obs.mergeable : target.mergeable;
225
+ const mergeState = knownValue(obs.mergeStateStatus)
226
+ ? obs.mergeStateStatus
227
+ : target.mergeStateStatus;
228
+ const draft = obs.isDraft ?? target.isDraft;
229
+ return !knownValue(mergeable) || (!draft && !knownValue(mergeState));
230
+ }
231
+ /**
232
+ * Re-read up to 25 of one repo's open PRs' merge state from GitHub and write back what moved.
233
+ * NEVER THROWS.
234
+ */
235
+ export async function recheckRepoMergeStates(args) {
236
+ const { accountId, repoId, log, mode } = args;
237
+ const out = {
238
+ checked: 0,
239
+ changed: 0,
240
+ stillUnknown: 0,
241
+ paused: false,
242
+ skipped: false,
243
+ };
244
+ const key = repoKey(accountId, repoId);
245
+ if (recheckInFlight.has(key))
246
+ return { ...out, skipped: true };
247
+ const now = Date.now();
248
+ if (mode === 'unsettled' && (failedReadUntil.get(key) ?? 0) > now) {
249
+ return { ...out, skipped: true };
250
+ }
251
+ recheckInFlight.add(key);
252
+ try {
253
+ if (isLimited(accountId))
254
+ return { ...out, paused: true };
255
+ const candidates = await getRepoMergeStateTargets(accountId, repoId, mode, mode === 'unsettled' ? coolingPrIds(accountId, repoId, now) : []);
256
+ const targets = mode === 'unsettled'
257
+ ? candidates.filter((t) => !coolingDown(accountId, t.prId, now)).slice(0, UNSETTLED_BATCH)
258
+ : candidates;
259
+ // The cheap case, and the common one: nothing unsettled → no token, no GitHub call.
260
+ if (targets.length === 0)
261
+ return out;
262
+ const token = await getAccessToken(accountId);
263
+ const observed = await fetchPrLivenessForNodes(token, targets.map((t) => t.githubNodeId), {
264
+ accountId,
265
+ withMergeState: true,
266
+ onPartial: () => log.warn(`merge-state recheck repo ${repoId}: partial GraphQL response (continuing)`),
267
+ onRateLimited: () => {
268
+ out.paused = true;
269
+ },
270
+ });
271
+ out.checked = targets.length;
272
+ const byNode = new Map(targets.map((t) => [t.githubNodeId, t]));
273
+ const answered = new Set();
274
+ // Sequential, not Promise.all: on sqlite these share one write lock, and the loop is ≤25.
275
+ for (const obs of observed) {
276
+ const target = byNode.get(obs.nodeId);
277
+ if (!target)
278
+ continue;
279
+ answered.add(obs.nodeId);
280
+ // `updatedAt` stripped — see the header: this pass never advances it.
281
+ const diff = await applyPrLiveness(accountId, target, { ...obs, updatedAt: null });
282
+ if (diff?.movedOnBoard)
283
+ out.changed += 1;
284
+ const unsettled = unsettledAfterRead(target, obs);
285
+ // A literal UNKNOWN is GitHub computing: worth the one quick second read.
286
+ if (obs.state === 'open' &&
287
+ (obs.mergeable === 'unknown' || obs.mergeStateStatus === 'unknown')) {
288
+ out.stillUnknown += 1;
289
+ }
290
+ noteUnknownAnswer(accountId, repoId, target.prId, unsettled);
291
+ }
292
+ // A PR asked about but not ANSWERED (a node the token cannot see, or one refused after a
293
+ // partial error) is still unsettled — it builds the same streak, so it cannot be re-asked on
294
+ // every walk. Not when the read was abandoned for a rate limit: nothing was answered then.
295
+ if (!out.paused) {
296
+ for (const t of targets) {
297
+ if (!answered.has(t.githubNodeId))
298
+ noteUnknownAnswer(accountId, repoId, t.prId, true);
299
+ }
300
+ }
301
+ if (out.changed > 0)
302
+ notePrChanged(accountId, repoId);
303
+ return out;
304
+ }
305
+ catch (err) {
306
+ log.warn(`merge-state recheck repo ${repoId} failed (non-fatal): ${errMsg(err)}`);
307
+ // Back the BACKSTOP off this repo (the trunk-moved recheck is per merge and bounded already):
308
+ // a gateway that 502s the merge-state batch will 502 it again on the next walk.
309
+ if (mode === 'unsettled')
310
+ noteFailedRead(key);
311
+ return out;
312
+ }
313
+ finally {
314
+ recheckInFlight.delete(key);
315
+ }
316
+ }
317
+ // Background re-reads this module started off a timer — tracked only so a test can await them
318
+ // deterministically (`__unsettledPrsDrain`). Every tracked promise is one that never rejects.
319
+ const backgroundWork = new Set();
320
+ function track(p) {
321
+ backgroundWork.add(p);
322
+ void p.finally(() => backgroundWork.delete(p));
323
+ }
324
+ // ── The per-walk backstop ────────────────────────────────────────────────────────────────────
325
+ // One pending second read per repo; a later walk's backstop simply replaces it.
326
+ const secondReads = new Map();
327
+ function scheduleSecondRead(accountId, repoId, log) {
328
+ const key = repoKey(accountId, repoId);
329
+ const existing = secondReads.get(key);
330
+ if (existing)
331
+ clearTimeout(existing);
332
+ const timer = setTimeout(() => {
333
+ secondReads.delete(key);
334
+ track(recheckRepoMergeStates({ accountId, repoId, log, mode: 'unsettled' }));
335
+ }, SECOND_READ_DELAY_MS);
336
+ if (typeof timer.unref === 'function')
337
+ timer.unref();
338
+ secondReads.set(key, timer);
339
+ }
340
+ // prId-keyed (`account:pr`) → when this PR was last nudged for stale CI. Bounded FIFO.
341
+ const ciNudgedAt = new Map();
342
+ /**
343
+ * Hand at most 5 of the repo's stale CI-`pending` PRs to the settle ladder. "Stale" = the PR's
344
+ * latest `ci_status_events` observation (a transition log: written only when the status or head
345
+ * CHANGES) is older than 20 minutes, i.e. the walk has seen nothing new since. Returns how many
346
+ * were nudged. Cheap when there are none: one indexed select, no GitHub call.
347
+ *
348
+ * ⚠ THE LADDER, NOT THE LIVENESS PASS: the liveness selection carries no check rollup, and adding
349
+ * `statusCheckRollup` to it would change its measured point cost. One single-PR sync (~1 point)
350
+ * per nudge, at most 5 per walk, and never the same PR twice inside 20 minutes.
351
+ */
352
+ export async function nudgeStaleCiPending(accountId, repoId, log, now = Date.now()) {
353
+ const candidates = await db
354
+ .select({ prId: pullRequests.id })
355
+ .from(pullRequests)
356
+ .where(and(eq(pullRequests.accountId, accountId), eq(pullRequests.repoId, repoId), eq(pullRequests.state, 'open'), eq(pullRequests.isDraft, false), eq(pullRequests.ciStatus, 'pending')))
357
+ .orderBy(desc(pullRequests.updatedAt), desc(pullRequests.id))
358
+ .limit(50)
359
+ .execute();
360
+ if (candidates.length === 0)
361
+ return 0;
362
+ const ids = candidates.map((c) => c.prId);
363
+ const observations = await db
364
+ .select({ prId: ciStatusEvents.prId, observedAt: ciStatusEvents.observedAt })
365
+ .from(ciStatusEvents)
366
+ .where(and(eq(ciStatusEvents.accountId, accountId), inArray(ciStatusEvents.prId, ids)))
367
+ .execute();
368
+ const latest = new Map();
369
+ for (const o of observations) {
370
+ const t = o.observedAt.getTime();
371
+ if (t > (latest.get(o.prId) ?? 0))
372
+ latest.set(o.prId, t);
373
+ }
374
+ let nudged = 0;
375
+ for (const prId of ids) {
376
+ if (nudged >= CI_NUDGES_PER_WALK)
377
+ break;
378
+ // No observation at all (a row older than the transition log) counts as stale.
379
+ const seen = latest.get(prId) ?? 0;
380
+ if (now - seen < CI_STALE_MS)
381
+ continue;
382
+ const key = `${accountId}:${prId}`;
383
+ const last = ciNudgedAt.get(key);
384
+ if (last != null && now - last < CI_NUDGE_COOLDOWN_MS)
385
+ continue;
386
+ if (last != null)
387
+ ciNudgedAt.delete(key);
388
+ else if (ciNudgedAt.size >= CI_NUDGE_MAX) {
389
+ const oldest = ciNudgedAt.keys().next().value;
390
+ if (oldest !== undefined)
391
+ ciNudgedAt.delete(oldest);
392
+ }
393
+ ciNudgedAt.set(key, now);
394
+ schedulePrSettle(accountId, prId, log);
395
+ nudged += 1;
396
+ }
397
+ return nudged;
398
+ }
399
+ /**
400
+ * The per-walk backstop. Called fire-and-forget after every successful repo walk (the scheduled
401
+ * loop and the manual `runSyncForRepo` tail); NEVER THROWS and never holds the walk's slot.
402
+ */
403
+ export async function runUnsettledPrBackstop(args) {
404
+ const { accountId, repoId, log } = args;
405
+ try {
406
+ if (isLimited(accountId))
407
+ return;
408
+ const r = await recheckRepoMergeStates({ accountId, repoId, log, mode: 'unsettled' });
409
+ if (r.stillUnknown > 0 && !r.paused)
410
+ scheduleSecondRead(accountId, repoId, log);
411
+ if (r.checked > 0) {
412
+ log.info(`unsettled-PR backstop repo ${repoId}: re-read ${r.checked}, ${r.changed} moved, ${r.stillUnknown} still computing`);
413
+ }
414
+ if (isLimited(accountId))
415
+ return;
416
+ await nudgeStaleCiPending(accountId, repoId, log);
417
+ }
418
+ catch (err) {
419
+ log.warn(`unsettled-PR backstop repo ${repoId} failed (non-fatal): ${errMsg(err)}`);
420
+ }
421
+ }
422
+ // ── After a merge lands: the trunk moved ────────────────────────────────────────────────────
423
+ const trunkTimers = new Map();
424
+ function clearTrunkTimers(key) {
425
+ for (const t of trunkTimers.get(key) ?? [])
426
+ clearTimeout(t);
427
+ trunkTimers.delete(key);
428
+ }
429
+ /**
430
+ * Re-read the repo's open PRs' merge state ~30s and ~90s from now. A burst of merges on one repo
431
+ * coalesces: each call restarts both timers, so the reads land after the LAST merge. Timers are
432
+ * unref'd; never throws.
433
+ */
434
+ export function scheduleTrunkMovedRecheck(accountId, repoId, log) {
435
+ try {
436
+ const key = repoKey(accountId, repoId);
437
+ if (trunkTimers.has(key))
438
+ clearTrunkTimers(key);
439
+ else if (trunkTimers.size >= TRUNK_TIMERS_MAX) {
440
+ const oldest = trunkTimers.keys().next().value;
441
+ if (oldest !== undefined)
442
+ clearTrunkTimers(oldest);
443
+ }
444
+ const timers = [];
445
+ TRUNK_RECHECK_OFFSETS_MS.forEach((offset, i) => {
446
+ const t = setTimeout(() => {
447
+ // The last timer releases the key, so the map never holds a finished entry.
448
+ if (i === TRUNK_RECHECK_OFFSETS_MS.length - 1 && trunkTimers.get(key) === timers) {
449
+ trunkTimers.delete(key);
450
+ }
451
+ track(recheckRepoMergeStates({ accountId, repoId, log, mode: 'open' }));
452
+ }, offset);
453
+ if (typeof t.unref === 'function')
454
+ t.unref();
455
+ timers.push(t);
456
+ });
457
+ trunkTimers.set(key, timers);
458
+ }
459
+ catch (err) {
460
+ log.warn(`trunk-moved recheck repo ${repoId}: could not schedule: ${errMsg(err)}`);
461
+ }
462
+ }
463
+ /**
464
+ * The tail of every merge WE landed (the merge route, the auto-merge runner's direct and queue
465
+ * landings): raise the SPA change signal for the repo — the local `markPrMergedLocally` stamp is
466
+ * itself a board-visible move — and schedule the trunk-moved recheck. The repo resolves through
467
+ * the ACCOUNT-SCOPED settle-facts read. Never throws, never rejects.
468
+ *
469
+ * The returned promise settles once the signal is RAISED (one DB read; the recheck itself only
470
+ * arms timers). A ROUTE awaits it before replying, so the write's own ordered `['repos']` read
471
+ * covers the stamp (sync/pr-settle.ts `notePrChangedForPrs`); a background caller may drop it.
472
+ */
473
+ export function noteMergeLanded(accountId, prId, log) {
474
+ const work = (async () => {
475
+ try {
476
+ const facts = await getPrSettleFacts(prId, accountId);
477
+ if (!facts)
478
+ return;
479
+ notePrChanged(accountId, facts.repoId);
480
+ scheduleTrunkMovedRecheck(accountId, facts.repoId, log);
481
+ }
482
+ catch (err) {
483
+ log.warn(`merge-landed follow-up for PR ${prId} failed (non-fatal): ${errMsg(err)}`);
484
+ }
485
+ })();
486
+ track(work);
487
+ return work;
488
+ }
489
+ /** Test-only: resolve once every background re-read already started has finished. */
490
+ export async function __unsettledPrsDrain() {
491
+ while (backgroundWork.size > 0)
492
+ await Promise.all([...backgroundWork]);
493
+ }
494
+ /** Test-only: clear every timer and in-memory claim. */
495
+ export function __resetUnsettledPrs() {
496
+ for (const t of secondReads.values())
497
+ clearTimeout(t);
498
+ secondReads.clear();
499
+ for (const key of [...trunkTimers.keys()])
500
+ clearTrunkTimers(key);
501
+ recheckInFlight.clear();
502
+ ciNudgedAt.clear();
503
+ stuckUnknown.clear();
504
+ failedReadUntil.clear();
505
+ }
506
+ //# sourceMappingURL=unsettled-prs.js.map