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,634 @@
1
+ import { and, desc, eq, inArray } from 'drizzle-orm';
2
+ import { db, schema, runTransaction } from '../db/client.js';
3
+ import { TREND_DAYS } from '../db/branch-queries.js';
4
+ // The Feed's rolling read window, imported rather than re-stated: the trunk CI transition log's
5
+ // retention is defined BY it (see TRUNK_CI_EVENT_WINDOW), and two copies of "14" would drift
6
+ // into a trim that silently deletes rows the Feed still reads.
7
+ import { FEED_WINDOW_DAYS } from '../db/queries.js';
8
+ import { getGraphqlClientFor, graphqlTolerant, withGithubRetry, } from '../github/client.js';
9
+ import { gateBudget } from '../github/rate-budget.js';
10
+ import { buildCommitChecksQuery, COMMIT_CHECKS_ALIAS_CAP, DEFAULT_BRANCH_QUERY, DEFAULT_BRANCH_HISTORY_QUERY, } from '../github/branch-queries.js';
11
+ // One shared mapper: GitHub's `StatusState` enum is identical on a Commit's rollup and on a PR
12
+ // head's, so this reuses sync/upsert.ts's rather than keeping a second copy that could drift.
13
+ // `checkContextState` + `parseActionsIds` are shared for the same reason one level down: a
14
+ // failing check on trunk must be the same object as a failing check on a PR.
15
+ import { checkContextState, ciStatusFrom, parseActionsIds } from './upsert.js';
16
+ const { repos, users, branchCommits, trunkCiStatusEvents } = schema;
17
+ // How many trunk commits the LIVE snapshot fetches per repo — GitHub's page maximum — and the
18
+ // unconditional floor of the trim below (the newest this-many rows are never deleted, however
19
+ // old). Rows beyond it survive only inside the TREND_DAYS window; they come from the one-time
20
+ // history backfill (`backfillBranchHistory`), whose ciStatus is final by the time it runs, so
21
+ // the live walk not re-observing them is by design rather than a staleness hole.
22
+ //
23
+ // Cost of the widening (20 → 100): phase 1's node budget is N + 3N (the history page plus its
24
+ // nested associatedPullRequests(first: 3)), scored ceil(total/100) — so N=100 is 400 nodes ⇒
25
+ // 4 rate-limit points per repo per sync, where N=20 was 1. An ACCEPTED cost: the wider window is
26
+ // what feeds the branch-trends charts (90 days of trunk CI + what landed), and it rides the
27
+ // per-repo sync, not every adaptive tick.
28
+ export const BRANCH_COMMIT_WINDOW = 100;
29
+ // The trim is a HYBRID bound: the newest BRANCH_COMMIT_WINDOW rows are kept UNCONDITIONALLY,
30
+ // and rows below that survive as long as they sit inside the TREND_DAYS window (which is what
31
+ // lets the one-time history backfill's deeper rows live — a pure count bound would delete them
32
+ // on the very next sync tick). The unconditional half is a landmine guard, not a nicety: an
33
+ // age bound applied to ALL rows would mean a repo whose newest trunk commit is older than the
34
+ // cutoff (dormant repo, or an active one whose committer dates were backdated by a
35
+ // rebase/import) has its ENTIRE set inserted and deleted in the same transaction on every sync
36
+ // — the strip row reads "never synced", the expander is permanently disabled, and the walk
37
+ // burns its 4 points writing rows it immediately destroys. The trends read
38
+ // (db/branch-queries.ts getBranchTrends) applies its own `committedAt >= now - 90d` filter,
39
+ // which is where the "no year-old bars" promise actually lives.
40
+ // Which rollup states earn the SECOND round trip for failing-check detail. A green commit is
41
+ // skipped entirely — that is what keeps the common case at the ~1 point this file's query header
42
+ // promises. 'pending' is included because GitHub reports the ROLLUP as PENDING while other checks
43
+ // are still running even after one has already FAILED: that is the classic amber-with-a-real-
44
+ // failure case, and without it an amber commit could never get a caret.
45
+ const DETAIL_STATES = new Set(['failure', 'error', 'pending']);
46
+ // The rollup states that are a POSITIVE statement that nothing is failing, and therefore the only
47
+ // ones allowed to CLEAR stored detail. See the partial-response policy below for why 'unknown'
48
+ // (which is also what a nulled-by-partial-error rollup maps to) is deliberately not in here.
49
+ const CLEAR_STATES = new Set(['success', 'expected']);
50
+ // Defensive cap on the stored array. The whole point of this column is that it is tiny; a
51
+ // pathological repo with 300 failing contexts must not write a fat row per commit.
52
+ export const MAX_FAILING_CHECKS_PER_COMMIT = 20;
53
+ const DAY_MS = 86_400_000;
54
+ // How many pages the one-time history backfill will walk (at BRANCH_COMMIT_WINDOW commits and
55
+ // ~4 rate-limit points each). 10 pages = 1000 trunk commits — deeper than 90 days of all but
56
+ // the busiest repos; on those the strip's oldest days simply stay unpopulated, disclosed by
57
+ // the `truncated` flag in the result (and the caller's log line), never silently.
58
+ export const BRANCH_BACKFILL_MAX_PAGES = 10;
59
+ // ---- The partial-response policy, applied identically to BOTH new columns ------------------
60
+ //
61
+ // `graphqlTolerant` returns GitHub's PARTIAL data when a token may read most of a query but is
62
+ // forbidden one sub-field — the forbidden field arrives NULLED. So "the response says null" and
63
+ // "we never received that selection" look the same at the JSON level, and the two demand opposite
64
+ // writes: the first should clear the column, the second must leave it alone. Writing
65
+ // unconditionally would NULL yesterday's good detail on every tick for such a token, and a red
66
+ // commit would permanently lose its caret (and a chip would blink out) for a reason no user can
67
+ // see. Writing conditionally on non-null would instead leave a stale value alive forever after a
68
+ // re-run turned a check green.
69
+ //
70
+ // The resolution, used by both `failingChecksToWrite` and `prNumberToWrite`: a value is written
71
+ // ONLY when the response actually CARRIED the selection it is derived from —
72
+ // • `undefined` ⇒ not received / not knowable ⇒ omit the column from the write entirely.
73
+ // • `null` / `[]` ⇒ GitHub positively stated there is nothing ⇒ write it, clearing any
74
+ // previously stored value.
75
+ // For failing checks the positive statement is a green (or 'expected') ROLLUP from phase 1, or a
76
+ // phase-2 response that actually contained a `contexts` list. For the PR reference it is an
77
+ // `associatedPullRequests.nodes` ARRAY (an empty one means "this commit came from no PR" — a
78
+ // direct push, which is a legitimate steady state, not a gap).
79
+ /**
80
+ * The FAILING checks among a commit's rollup contexts — never the passing ones.
81
+ *
82
+ * Reuses the PR side's `checkContextState` + `parseActionsIds` verbatim so a trunk failure and a
83
+ * PR failure are the same object, then dedupes by display name keeping the newest Actions run:
84
+ * `contexts` returns EVERY check suite attached to the commit and does NOT collapse to
85
+ * latest-per-name the way GitHub's own PR UI does (a re-run, or a workflow fired by both `push`
86
+ * and `pull_request`, contributes a second same-named CheckRun). Actions run ids increase
87
+ * monotonically, so the highest runId is the most recent.
88
+ */
89
+ export function failingChecksFrom(nodes) {
90
+ const byName = new Map();
91
+ for (const c of nodes) {
92
+ if (c == null)
93
+ continue;
94
+ const state = checkContextState(c);
95
+ if (state !== 'failure' && state !== 'error')
96
+ continue;
97
+ const url = c.__typename === 'CheckRun' ? c.detailsUrl : c.targetUrl;
98
+ const { runId, jobId } = c.__typename === 'CheckRun' ? parseActionsIds(url) : { runId: null, jobId: null };
99
+ const item = {
100
+ // Third-party text (a check's name is whatever the CI vendor chose), so it is length-capped
101
+ // here rather than trusted; the frontend renders it as a plain text node.
102
+ name: (c.__typename === 'CheckRun' ? c.name : c.context).slice(0, 200),
103
+ state,
104
+ url,
105
+ runId,
106
+ jobId,
107
+ workflowName: c.__typename === 'CheckRun'
108
+ ? (c.checkSuite?.workflowRun?.workflow?.name ?? null)
109
+ : null,
110
+ };
111
+ const prev = byName.get(item.name);
112
+ if (!prev || (item.runId ?? -1) > (prev.runId ?? -1))
113
+ byName.set(item.name, item);
114
+ }
115
+ return [...byName.values()].slice(0, MAX_FAILING_CHECKS_PER_COMMIT);
116
+ }
117
+ /**
118
+ * Which single PR number to store for a trunk commit, or null for a direct push.
119
+ *
120
+ * The 0 / 1 / many contract:
121
+ * 0 candidates → a direct push to trunk (the legitimate no-chip case) → null.
122
+ * 1 → the normal squash/merge landing → that number.
123
+ * 2+ → the commit is reachable from several PRs (a branch merged into another open
124
+ * PR, a cherry-pick, a revert-then-reland). Store exactly ONE, ranked
125
+ * (merged into THIS repo's default branch) > (merged anywhere) > (open), with
126
+ * the LOWEST number as a stable tiebreak.
127
+ *
128
+ * Determinism is the whole point: the ranking is ours precisely so a re-sync can never flip the
129
+ * displayed number, which `first: 1` on an unordered connection would.
130
+ *
131
+ * A candidate from another repository is DROPPED: `associatedPullRequests` spans the repo network,
132
+ * so a fork's own PR can appear, and the read layer resolves a number within (accountId, repoId)
133
+ * — a foreign number would resolve to a completely unrelated local PR. A null `nameWithOwner` (a
134
+ * tolerant partial nulled it) is accepted rather than discarding an otherwise good candidate.
135
+ */
136
+ export function pickAssociatedPrNumber(n, fullName, defaultBranchName) {
137
+ const cands = (n.associatedPullRequests?.nodes ?? []).filter((p) => p != null &&
138
+ typeof p.number === 'number' &&
139
+ (p.repository?.nameWithOwner == null || p.repository.nameWithOwner === fullName));
140
+ const first = cands[0];
141
+ if (first == null)
142
+ return null;
143
+ const rank = (p) => {
144
+ if (p.merged !== true)
145
+ return 2;
146
+ return defaultBranchName != null && p.baseRefName === defaultBranchName ? 0 : 1;
147
+ };
148
+ let best = first;
149
+ for (const p of cands.slice(1)) {
150
+ const d = rank(p) - rank(best);
151
+ if (d < 0 || (d === 0 && p.number < best.number))
152
+ best = p;
153
+ }
154
+ return best.number;
155
+ }
156
+ /**
157
+ * Which commits in the window earn a phase-2 detail fetch: the non-green ones, newest first.
158
+ *
159
+ * `history()` returns newest-first, so slicing to the alias cap keeps the NEWEST failing commits —
160
+ * the ones anyone will actually expand. Exported so the "a green trunk issues no second query"
161
+ * claim is a test rather than a comment.
162
+ */
163
+ export function detailTargetShas(nodes) {
164
+ return nodes
165
+ .filter((n) => DETAIL_STATES.has(ciStatusFrom(n.statusCheckRollup?.state)))
166
+ .slice(0, COMMIT_CHECKS_ALIAS_CAP)
167
+ .map((n) => n.oid);
168
+ }
169
+ /**
170
+ * The failing-checks value to WRITE for one commit, per the partial-response policy above:
171
+ * `undefined` ⇒ omit the column (we did not learn anything), `null` ⇒ positively no failures.
172
+ */
173
+ export function failingChecksToWrite(n, failingBySha) {
174
+ const rollup = ciStatusFrom(n.statusCheckRollup?.state);
175
+ if (CLEAR_STATES.has(rollup))
176
+ return null;
177
+ // 'unknown' — either no rollup was received (a partial error nulled it) or GitHub's state isn't
178
+ // one we model. Neither is a statement that nothing is failing, so nothing is cleared.
179
+ if (!DETAIL_STATES.has(rollup))
180
+ return undefined;
181
+ const found = failingBySha.get(n.oid);
182
+ // Absent ⇒ past the alias cap, or the contexts selection wasn't received: keep what we have.
183
+ if (found == null)
184
+ return undefined;
185
+ return found.length > 0 ? found : null;
186
+ }
187
+ /**
188
+ * The PR-number value to WRITE for one commit, per the same policy. An `associatedPullRequests`
189
+ * that arrived as null (a partial error) yields `undefined` — writing null there would erase a good
190
+ * stored number and make the chip blink out of the UI for a reason the user cannot see.
191
+ */
192
+ export function prNumberToWrite(n, fullName, defaultBranchName) {
193
+ return Array.isArray(n.associatedPullRequests?.nodes)
194
+ ? pickAssociatedPrNumber(n, fullName, defaultBranchName)
195
+ : undefined;
196
+ }
197
+ /**
198
+ * Phase 2: the failing checks for the given shas, in ONE query.
199
+ *
200
+ * The returned map contains an entry ONLY for a sha whose `contexts` list actually arrived — an
201
+ * absent key means "not received", which the caller must not confuse with "no failures". See the
202
+ * partial-response policy above. Exported for the request-shape unit test.
203
+ */
204
+ export async function fetchFailingChecks(client, owner, name, shas, log) {
205
+ const variables = { owner, name };
206
+ shas.forEach((sha, i) => {
207
+ variables[`s${i}`] = sha;
208
+ });
209
+ const resp = await withGithubRetry(() => graphqlTolerant(client, buildCommitChecksQuery(shas.length), variables, (errors) => log?.warn(`branch-status ${owner}/${name}: partial GraphQL on commit checks — ` +
210
+ `keeping previously stored detail for the affected commits`, errors)));
211
+ const bySha = new Map();
212
+ shas.forEach((sha, i) => {
213
+ const nodes = resp.repository?.[`c${i}`]?.statusCheckRollup?.contexts?.nodes;
214
+ // `nodes == null` covers all three not-received shapes: the alias resolved to nothing, the
215
+ // rollup was nulled, or the contexts selection itself was forbidden.
216
+ if (nodes == null)
217
+ return;
218
+ bySha.set(sha, failingChecksFrom(nodes));
219
+ });
220
+ return { bySha, cost: resp.rateLimit?.cost ?? 0 };
221
+ }
222
+ /**
223
+ * The commit-author logins we already know, resolved in ONE query. Lookup-only on purpose —
224
+ * see the author-mapping note on `syncBranchStatus`: no `users` rows are ever created here.
225
+ */
226
+ async function resolveKnownLogins(nodes) {
227
+ const logins = [
228
+ ...new Set(nodes
229
+ .map((n) => n.author?.user?.login)
230
+ .filter((l) => typeof l === 'string' && l.length > 0)),
231
+ ];
232
+ const userIdByLogin = new Map();
233
+ if (logins.length > 0) {
234
+ const rows = await db
235
+ .select({ id: users.id, login: users.githubLogin })
236
+ .from(users)
237
+ .where(inArray(users.githubLogin, logins))
238
+ .execute();
239
+ for (const r of rows)
240
+ userIdByLogin.set(r.login, r.id);
241
+ }
242
+ return userIdByLogin;
243
+ }
244
+ /**
245
+ * Upsert one page of trunk-commit nodes — shared verbatim between the live snapshot and the
246
+ * history backfill so the two walks can never store differently-shaped rows. Idempotent on
247
+ * `(accountId, repoId, sha)`; the observed-key spread implements the partial-response policy
248
+ * above (an omitted key leaves the column at its default on INSERT and untouched on UPDATE,
249
+ * which is exactly what "we did not receive that selection" means).
250
+ */
251
+ async function upsertBranchCommitRows(tx, args, nodes, userIdByLogin, failingBySha) {
252
+ for (const n of nodes) {
253
+ const login = n.author?.user?.login ?? null;
254
+ const authorUserId = login != null ? userIdByLogin.get(login) ?? null : null;
255
+ const failingChecks = failingChecksToWrite(n, failingBySha);
256
+ const prNumber = prNumberToWrite(n, args.fullName, args.branchName);
257
+ const observed = {
258
+ ...(failingChecks !== undefined ? { failingChecks } : {}),
259
+ ...(prNumber !== undefined ? { prNumber } : {}),
260
+ };
261
+ await tx
262
+ .insert(branchCommits)
263
+ .values({
264
+ accountId: args.accountId,
265
+ repoId: args.repoId,
266
+ sha: n.oid,
267
+ messageHeadline: n.messageHeadline,
268
+ authorUserId,
269
+ // Prefer the git author name; fall back to the login so a commit by a known GitHub
270
+ // account with a blank git name still shows SOMETHING attributable.
271
+ authorName: n.author?.name ?? login,
272
+ authorAvatarUrl: n.author?.avatarUrl ?? null,
273
+ committedAt: new Date(n.committedDate),
274
+ ciStatus: ciStatusFrom(n.statusCheckRollup?.state),
275
+ ...observed,
276
+ })
277
+ .onConflictDoUpdate({
278
+ target: [branchCommits.accountId, branchCommits.repoId, branchCommits.sha],
279
+ set: {
280
+ messageHeadline: n.messageHeadline,
281
+ authorUserId,
282
+ authorName: n.author?.name ?? login,
283
+ authorAvatarUrl: n.author?.avatarUrl ?? null,
284
+ committedAt: new Date(n.committedDate),
285
+ ciStatus: ciStatusFrom(n.statusCheckRollup?.state),
286
+ ...observed,
287
+ },
288
+ })
289
+ .execute();
290
+ }
291
+ }
292
+ /**
293
+ * Which of the retained rows the trim deletes, given rows sorted NEWEST-FIRST: anything both
294
+ * below the newest-BRANCH_COMMIT_WINDOW floor AND outside the TREND_DAYS window. Pure and
295
+ * exported so the hybrid bound's two halves are pinned by tests rather than a comment.
296
+ */
297
+ export function staleBranchCommitIds(rows, nowMs) {
298
+ const cutoffMs = nowMs - TREND_DAYS * DAY_MS;
299
+ return rows
300
+ .filter((r, i) => i >= BRANCH_COMMIT_WINDOW && r.committedAt.getTime() < cutoffMs)
301
+ .map((r) => r.id);
302
+ }
303
+ // ---- The trunk CI TRANSITION log (trunk_ci_status_events) ----------------------------------
304
+ //
305
+ // `branch_commits.ci_status` is updated IN PLACE by the upsert above, so the moment trunk turned
306
+ // red is nowhere on that row — its only timestamps are `committedAt` (git commit time) and
307
+ // `createdAt` (first insertion). Approximating a failure time with either would be a quiet lie,
308
+ // so an observation gets its own append-only row, exactly as `ci_status_events` does for a PR
309
+ // head (sync/upsert.ts). It is what makes the Feed's trunk CI-failure cards honest.
310
+ //
311
+ // How many rows one repo's log keeps UNCONDITIONALLY. The trim is HYBRID, exactly like
312
+ // `branch_commits` above (newest-N ∪ inside the read window), and for the same two reasons in
313
+ // opposite directions:
314
+ //
315
+ // • The count half alone was WRONG. `trunkCiTransitionChanged` records a transition on any head
316
+ // move, and `syncBranchStatus` runs at the end of every walk (as often as every 120s on a hot
317
+ // repo), so an active repo produces transitions far faster than the Feed's window elapses —
318
+ // and a pure newest-200 bound evicted the failure rows `getTrunkCiFailureFeedItems` reads over
319
+ // FEED_WINDOW_DAYS. The symptom is invisible: the Feed just quietly stops showing trunk
320
+ // failures on exactly the repos that have the most of them.
321
+ // • The age half alone would be wrong too. A dormant repo's whole log is older than the cutoff,
322
+ // so an unconditional age bound would delete every row it has and the next observation would
323
+ // read as a first observation forever.
324
+ export const TRUNK_CI_EVENT_WINDOW = 200;
325
+ /**
326
+ * Which of a repo's trunk CI transition rows the trim deletes, given rows sorted NEWEST-FIRST:
327
+ * anything both below the newest-TRUNK_CI_EVENT_WINDOW floor AND older than the Feed's read
328
+ * window. Pure and exported so the hybrid bound's two halves are pinned by tests, not comments.
329
+ *
330
+ * `observedAt` (our observation time) is the right axis because it is exactly what
331
+ * `getTrunkCiFailureFeedItems` filters on.
332
+ */
333
+ export function staleTrunkCiEventIds(rows, nowMs) {
334
+ const cutoffMs = nowMs - FEED_WINDOW_DAYS * DAY_MS;
335
+ return rows
336
+ .filter((r, i) => i >= TRUNK_CI_EVENT_WINDOW && r.observedAt.getTime() < cutoffMs)
337
+ .map((r) => r.id);
338
+ }
339
+ /**
340
+ * Whether a fresh observation is a TRANSITION worth recording, given this repo's last row.
341
+ *
342
+ * Mirrors the PR-side predicate in `sync/upsert.ts` (status / head sha / failing-check name set)
343
+ * with TWO deliberate differences:
344
+ *
345
+ * • `failingCheckNames` may be `undefined`, meaning "phase 2 never told us which checks failed".
346
+ * Per the partial-response policy at the top of this file, that is not a statement that the set
347
+ * changed, so the name dimension is simply DROPPED from the comparison rather than compared
348
+ * against an empty list — otherwise a repo whose detail fetch fails would log a spurious
349
+ * "checks changed" transition on every single sync.
350
+ * • A head move on a GREEN trunk is not a transition. Trunk's head changes on every landed PR,
351
+ * so recording each one filled an active repo's log with rows that state nothing (the Feed
352
+ * reads failure/error rows only) and pushed real failures out of it. The narrowing is
353
+ * deliberately minimal: it applies ONLY when the rollup is a positive "nothing is failing"
354
+ * (CLEAR_STATES) on BOTH sides with no named failures on either, so no red — or amber-with-a-
355
+ * real-failure — observation can be swallowed. A new head that is still red IS a new failure
356
+ * (its own card, on its own commit) and is always recorded, and the first green after a red is
357
+ * a status change, so recovery timing survives.
358
+ *
359
+ * Pure + exported so all of that is pinned by tests rather than a comment.
360
+ */
361
+ export function trunkCiTransitionChanged(last, next) {
362
+ if (last == null)
363
+ return true;
364
+ if (last.status !== next.status)
365
+ return true;
366
+ const before = [...new Set((last.failingChecks ?? []).map((c) => c.name))].sort();
367
+ if (next.failingCheckNames !== undefined &&
368
+ JSON.stringify(before) !== JSON.stringify(next.failingCheckNames)) {
369
+ return true;
370
+ }
371
+ if (last.headSha === next.headSha)
372
+ return false;
373
+ // Head moved. Record it unless BOTH observations are a positive green with nothing named.
374
+ const stillClean = CLEAR_STATES.has(next.status) &&
375
+ before.length === 0 &&
376
+ (next.failingCheckNames?.length ?? 0) === 0;
377
+ return !stillClean;
378
+ }
379
+ /**
380
+ * Append one trunk CI observation, if it is a transition — then trim the repo's log.
381
+ *
382
+ * TWO GATES, both load-bearing:
383
+ * • `headSha == null` — the response never positively identified a branch head, so there is
384
+ * nothing to attribute an observation to.
385
+ * • `status === 'unknown'` — GitHub's rollup states we model are success/failure/pending/
386
+ * error/expected; 'unknown' is BOTH "a state we don't model" AND what `graphqlTolerant`
387
+ * yields when a partial response NULLs the rollup. Recording it would manufacture a
388
+ * "trunk changed" event out of a permissions error. (Same gate the PR-side writer uses.)
389
+ *
390
+ * Never throws: every caller treats the branch snapshot as strictly non-fatal, and a feed
391
+ * nicety must not be able to cost a repo its sync. Runs OUTSIDE the snapshot's transaction for
392
+ * the same reason — a failure here rolls back nothing.
393
+ */
394
+ async function recordTrunkCiTransition(args) {
395
+ const { accountId, repoId, headSha, status } = args;
396
+ if (headSha == null || status === 'unknown')
397
+ return;
398
+ const failingCheckNames = args.failingChecks === undefined
399
+ ? undefined
400
+ : [...new Set(args.failingChecks?.map((c) => c.name) ?? [])].sort();
401
+ const [last] = await db
402
+ .select({
403
+ status: trunkCiStatusEvents.status,
404
+ headSha: trunkCiStatusEvents.headSha,
405
+ failingChecks: trunkCiStatusEvents.failingChecks,
406
+ })
407
+ .from(trunkCiStatusEvents)
408
+ .where(and(eq(trunkCiStatusEvents.accountId, accountId), eq(trunkCiStatusEvents.repoId, repoId)))
409
+ // `id` breaks a same-instant tie deterministically (autoincrement ⇒ insertion order).
410
+ .orderBy(desc(trunkCiStatusEvents.observedAt), desc(trunkCiStatusEvents.id))
411
+ .limit(1)
412
+ .execute();
413
+ if (!trunkCiTransitionChanged(last ?? null, { status, headSha, failingCheckNames }))
414
+ return;
415
+ await db
416
+ .insert(trunkCiStatusEvents)
417
+ .values({
418
+ accountId,
419
+ repoId,
420
+ branchName: args.branchName,
421
+ headSha,
422
+ status,
423
+ // `undefined` names ⇒ store null. NOT inheriting the previous row's list: this is an
424
+ // append-only observation log, and copying names forward would fabricate evidence.
425
+ failingChecks: args.failingChecks ?? null,
426
+ observedAt: new Date(),
427
+ })
428
+ .execute();
429
+ // HYBRID trim (see TRUNK_CI_EVENT_WINDOW + staleTrunkCiEventIds): the newest N rows
430
+ // unconditionally, plus everything still inside the Feed's read window. select-then-
431
+ // delete-by-id rather than a correlated DELETE … LIMIT, which is not portable across sqlite
432
+ // and Postgres.
433
+ const kept = await db
434
+ .select({ id: trunkCiStatusEvents.id, observedAt: trunkCiStatusEvents.observedAt })
435
+ .from(trunkCiStatusEvents)
436
+ .where(and(eq(trunkCiStatusEvents.accountId, accountId), eq(trunkCiStatusEvents.repoId, repoId)))
437
+ .orderBy(desc(trunkCiStatusEvents.observedAt), desc(trunkCiStatusEvents.id))
438
+ .execute();
439
+ const stale = staleTrunkCiEventIds(kept, Date.now());
440
+ // `inArray(col, [])` is a degenerate predicate whose behaviour differs by dialect, and there
441
+ // is nothing to delete anyway.
442
+ if (stale.length > 0) {
443
+ await db.delete(trunkCiStatusEvents).where(inArray(trunkCiStatusEvents.id, stale)).execute();
444
+ }
445
+ }
446
+ /**
447
+ * Snapshot a repo's DEFAULT BRANCH: its head sha + CI rollup onto `repos`, and the most recent
448
+ * `BRANCH_COMMIT_WINDOW` trunk commits (with their own per-commit CI state, the checks that were
449
+ * failing on them, and the PR each landed from) into `branch_commits`.
450
+ *
451
+ * Why this can't come from the existing tables: `commits` is PR-scoped, and a squash-merged PR
452
+ * never appears there under the SHA that actually landed on trunk. So "is main green, and what
453
+ * landed on it" is unanswerable without its own read.
454
+ *
455
+ * Idempotent — commits upsert on `(accountId, repoId, sha)`, so a re-sync of the same window
456
+ * updates each commit's CI state in place (a commit that was `pending` last tick becomes
457
+ * `success` on this one) rather than duplicating rows.
458
+ *
459
+ * Author mapping is deliberately LOOKUP-ONLY: a trunk committer who resolves to an already-known
460
+ * `users` row gets `authorUserId` (so the UI can render their synced avatar + the maintainer
461
+ * shield); anyone else is stored as raw `authorName`/`authorAvatarUrl`. We do NOT insert users
462
+ * here — `users` is a GLOBAL table, and creating rows for every drive-by trunk committer would
463
+ * grow it with identities that appear nowhere else in the account's data.
464
+ */
465
+ export async function syncBranchStatus(opts) {
466
+ const { owner, name, repoId, accountId, token } = opts;
467
+ const client = getGraphqlClientFor(token);
468
+ const fullName = `${owner}/${name}`;
469
+ const resp = await withGithubRetry(() => graphqlTolerant(client, DEFAULT_BRANCH_QUERY, { owner, name, first: BRANCH_COMMIT_WINDOW }, (errors) => opts.log?.warn(`branch-status ${fullName}: partial GraphQL — continuing without forbidden fields`, errors)));
470
+ const ref = resp.repository?.defaultBranchRef ?? null;
471
+ const branchName = ref?.name ?? null;
472
+ // `target` is a nullable INTERFACE — a branch whose tip isn't a Commit selects nothing, so
473
+ // every field below is optional even when `target` itself is present.
474
+ const target = ref?.target ?? null;
475
+ const headSha = target?.oid ?? null;
476
+ const headCi = ciStatusFrom(target?.statusCheckRollup?.state);
477
+ const nodes = (target?.history?.nodes ?? []).filter((n) => n != null && typeof n.oid === 'string');
478
+ // Resolve the commit authors we already know, in ONE query. GraphQL gives a login only when
479
+ // the commit email maps to a GitHub account; the rest keep their raw git name.
480
+ const userIdByLogin = await resolveKnownLogins(nodes);
481
+ // Phase 2, for the non-green commits only — a green trunk issues no second query at all.
482
+ const detailTargets = detailTargetShas(nodes);
483
+ let failingBySha = new Map();
484
+ let detailCost = 0;
485
+ if (detailTargets.length > 0) {
486
+ // LOAD-BEARING try/catch: sync-repo.ts already treats the whole of `syncBranchStatus` as
487
+ // non-fatal, so an unguarded throw here would discard the phase-1 snapshot too — dots,
488
+ // headSha, authors, the entire strip for this repo. Detail failure must degrade to "no
489
+ // carets", never to "no strip".
490
+ try {
491
+ const r = await fetchFailingChecks(client, owner, name, detailTargets, opts.log);
492
+ failingBySha = r.bySha;
493
+ detailCost = r.cost;
494
+ }
495
+ catch (err) {
496
+ opts.log?.warn(`branch-status ${fullName}: failing-check detail failed (non-fatal): ` +
497
+ `${err instanceof Error ? err.message.slice(0, 200) : String(err)}`);
498
+ }
499
+ }
500
+ await runTransaction(async (tx) => {
501
+ await upsertBranchCommitRows(tx, { accountId, repoId, fullName, branchName }, nodes, userIdByLogin, failingBySha);
502
+ // Trim IN the same transaction, so the table can never be observed holding an unbounded
503
+ // history. HYBRID bound (see staleBranchCommitIds + the constants' comments): the newest
504
+ // BRANCH_COMMIT_WINDOW rows unconditionally, plus backfilled rows inside the TREND_DAYS
505
+ // window. Done as select-then-delete-by-id rather than a correlated DELETE … LIMIT, which
506
+ // is not portable across sqlite and Postgres.
507
+ const kept = await tx
508
+ .select({ id: branchCommits.id, committedAt: branchCommits.committedAt })
509
+ .from(branchCommits)
510
+ .where(and(eq(branchCommits.accountId, accountId), eq(branchCommits.repoId, repoId)))
511
+ .orderBy(desc(branchCommits.committedAt))
512
+ .execute();
513
+ const stale = staleBranchCommitIds(kept, Date.now());
514
+ // Guard the empty case: `inArray(col, [])` is a degenerate predicate whose behaviour
515
+ // differs by dialect, and there is nothing to delete anyway.
516
+ if (stale.length > 0) {
517
+ await tx.delete(branchCommits).where(inArray(branchCommits.id, stale)).execute();
518
+ }
519
+ await tx
520
+ .update(repos)
521
+ .set({
522
+ defaultBranchName: branchName,
523
+ defaultBranchHeadSha: headSha,
524
+ defaultBranchCiStatus: headCi,
525
+ defaultBranchUpdatedAt: new Date(),
526
+ })
527
+ // Ownership is part of the predicate, not an assumption about the caller.
528
+ .where(and(eq(repos.id, repoId), eq(repos.accountId, accountId)))
529
+ .execute();
530
+ });
531
+ // The trunk CI transition log — AFTER the snapshot's transaction and inside its own guard, so
532
+ // this feed nicety can never roll back (or fail) the strip that just succeeded. The failing
533
+ // checks handed over come from the head COMMIT's own node under the same partial-response
534
+ // policy as the column write (`undefined` = we did not learn them).
535
+ try {
536
+ const headNode = headSha != null ? nodes.find((n) => n.oid === headSha) : undefined;
537
+ await recordTrunkCiTransition({
538
+ accountId,
539
+ repoId,
540
+ fullName,
541
+ branchName,
542
+ headSha,
543
+ status: headCi,
544
+ failingChecks: headNode != null ? failingChecksToWrite(headNode, failingBySha) : undefined,
545
+ log: opts.log,
546
+ });
547
+ }
548
+ catch (err) {
549
+ opts.log?.warn(`branch-status ${fullName}: trunk CI transition log failed (non-fatal): ` +
550
+ `${err instanceof Error ? err.message.slice(0, 200) : String(err)}`);
551
+ }
552
+ const rateLimitCost = (resp.rateLimit?.cost ?? 0) + detailCost;
553
+ if (detailTargets.length > 0) {
554
+ // The one place the two-phase cost claim is observable. Logged only when phase 2 actually
555
+ // ran, so a healthy repo stays silent.
556
+ opts.log?.info(`branch-status ${fullName}: ${detailTargets.length} non-green commit(s), ` +
557
+ `${failingBySha.size} with retrievable checks, ${rateLimitCost} rate-limit point(s)`);
558
+ }
559
+ return {
560
+ branchName,
561
+ headSha,
562
+ ciStatus: headCi,
563
+ commitCount: nodes.length,
564
+ failingCommitCount: detailTargets.length,
565
+ rateLimitCost,
566
+ };
567
+ }
568
+ /**
569
+ * ONE-TIME deepening of the trunk window: walk `history(since: now − TREND_DAYS)` page by page
570
+ * and upsert every commit, so the branch-trends DayStrip spans its whole 90-day read window
571
+ * instead of only the newest BRANCH_COMMIT_WINDOW commits the live snapshot retains. Runs after
572
+ * a repo's first full sync (and a forced deep re-sync) — see sync-manager.ts.
573
+ *
574
+ * Deliberate differences from the live snapshot, all cost-driven:
575
+ * • NO phase-2 failing-check detail — the trend cells need only `ciStatus`, and per the
576
+ * partial-response policy an absent detail simply omits the column (`failingChecksToWrite`
577
+ * returns undefined for a non-green commit with no phase-2 entry), so any detail the live
578
+ * window already stored is left untouched where the walks overlap.
579
+ * • NO `repos` head-column write and NO trim — the live snapshot owns both; every row this
580
+ * walk inserts sits inside the TREND_DAYS window the hybrid trim retains.
581
+ * • The first page re-reads the live window from the branch tip rather than resuming a stored
582
+ * cursor — history cursors are position-relative and trunk moves during the PR walk, so a
583
+ * handed-off cursor could silently skip commits; one duplicate page is 4 points, and the
584
+ * upsert is idempotent.
585
+ *
586
+ * Old commits' rollups are FINAL for our purposes (a historical re-run that flips one is rarer
587
+ * than the live window's own retro-flips, which docs/MERGE-CI-TRUNK.md already documents), so
588
+ * never re-observing these rows is by design. A commit whose CI never finished stays `pending`
589
+ * and counts as neither pass nor fail in the trends read.
590
+ */
591
+ export async function backfillBranchHistory(opts) {
592
+ const { owner, name, repoId, accountId, token } = opts;
593
+ const client = getGraphqlClientFor(token);
594
+ const fullName = `${owner}/${name}`;
595
+ const since = new Date(Date.now() - TREND_DAYS * DAY_MS).toISOString();
596
+ let after = null;
597
+ let pages = 0;
598
+ let commitCount = 0;
599
+ let rateLimitCost = 0;
600
+ while (pages < BRANCH_BACKFILL_MAX_PAGES) {
601
+ if (opts.shouldCancel?.())
602
+ break;
603
+ // Shares its token with the account's PR walks: when the budget is low or a limit was
604
+ // observed, wait (cancellably) before spending ~4 points on another history page. A
605
+ // cancel mid-wait breaks like any cancel — the walk stays strictly non-fatal and the
606
+ // result is disclosed as truncated.
607
+ if ((await gateBudget(accountId, { shouldCancel: opts.shouldCancel })) === 'cancelled') {
608
+ break;
609
+ }
610
+ const resp = await withGithubRetry(() => graphqlTolerant(client, DEFAULT_BRANCH_HISTORY_QUERY, { owner, name, first: BRANCH_COMMIT_WINDOW, since, after }, (errors) => opts.log?.warn(`branch-backfill ${fullName}: partial GraphQL — continuing without forbidden fields`, errors)));
611
+ pages += 1;
612
+ rateLimitCost += resp.rateLimit?.cost ?? 0;
613
+ const ref = resp.repository?.defaultBranchRef ?? null;
614
+ const history = ref?.target?.history ?? null;
615
+ const nodes = (history?.nodes ?? []).filter((n) => n != null && typeof n.oid === 'string');
616
+ if (nodes.length > 0) {
617
+ const userIdByLogin = await resolveKnownLogins(nodes);
618
+ const branchName = ref?.name ?? null;
619
+ await runTransaction(async (tx) => {
620
+ await upsertBranchCommitRows(tx, { accountId, repoId, fullName, branchName }, nodes, userIdByLogin, new Map());
621
+ });
622
+ commitCount += nodes.length;
623
+ }
624
+ // An absent pageInfo is "not received" (tolerant partial), which must stop the walk rather
625
+ // than throw or spin; endCursor null with hasNextPage true is treated the same way.
626
+ const pageInfo = history?.pageInfo ?? null;
627
+ if (pageInfo?.hasNextPage !== true || pageInfo.endCursor == null) {
628
+ return { pages, commitCount, rateLimitCost, truncated: false };
629
+ }
630
+ after = pageInfo.endCursor;
631
+ }
632
+ return { pages, commitCount, rateLimitCost, truncated: true };
633
+ }
634
+ //# sourceMappingURL=branch-status.js.map