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,563 @@
1
+ import { and, eq, inArray } from 'drizzle-orm';
2
+ import { db, schema } from '../../db/client.js';
3
+ import { getGraphqlClientFor, graphqlTolerant } from '../../github/client.js';
4
+ import { getAccessToken } from '../../auth/account.js';
5
+ import { OWNER_TYPE_QUERY, REPO_ID_QUERY, REPO_SEARCH_QUERY, VIEWER_REPOS_QUERY, } from '../../github/queries.js';
6
+ import { upsertRepo } from '../../sync/upsert.js';
7
+ import { enqueueSyncForRepo, getSyncStatus, isSyncQueued, isSyncRunning, listActiveSyncProgress, requestSyncCancel, manualSyncCooldownMs, noteManualSync, waitForSyncToStop, } from '../../sync/sync-manager.js';
8
+ import { deleteRepo, getAddedRepoNodeIds, getRepo, listRepos, resolveWorkspaceScope, } from '../../db/queries.js';
9
+ import { getBranchStatus, getBranchTrends } from '../../db/branch-queries.js';
10
+ import { withPrChangeSignal } from '../../sync/pr-change-signal.js';
11
+ import { accountIdOf } from '../plugins/auth.js';
12
+ // Local copy of the shared MAX_REPOS_PER_ACCOUNT value. `@pierre-review/shared` is
13
+ // a types-only package (not shipped in the published tarball), so the backend must
14
+ // only `import type` from it — runtime constants are duplicated here. Keep in sync.
15
+ const MAX_REPOS_PER_ACCOUNT = 100;
16
+ const createRepoSchema = {
17
+ body: {
18
+ type: 'object',
19
+ required: ['owner', 'name'],
20
+ additionalProperties: false,
21
+ properties: {
22
+ owner: { type: 'string', minLength: 1 },
23
+ name: { type: 'string', minLength: 1 },
24
+ },
25
+ },
26
+ };
27
+ const idParamSchema = {
28
+ params: {
29
+ type: 'object',
30
+ required: ['id'],
31
+ properties: { id: { type: 'integer' } },
32
+ },
33
+ };
34
+ const syncSchema = {
35
+ ...idParamSchema,
36
+ querystring: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: { full: { type: 'boolean', default: false } },
40
+ },
41
+ };
42
+ const searchSchema = {
43
+ querystring: {
44
+ type: 'object',
45
+ required: ['q'],
46
+ additionalProperties: false,
47
+ properties: {
48
+ q: { type: 'string', minLength: 1, maxLength: 256 },
49
+ cursor: { type: 'string', minLength: 1 },
50
+ limit: { type: 'integer', default: 10, minimum: 1, maximum: 25 },
51
+ },
52
+ },
53
+ };
54
+ // `repoIds=1,2,3` → [1,2,3]; absent/blank/garbage → null ("no explicit narrowing"). It is fed to
55
+ // `resolveWorkspaceScope` as the `narrow` argument, never used as a scope in its own right.
56
+ function parseIntList(raw) {
57
+ if (!raw)
58
+ return null;
59
+ const ids = raw
60
+ .split(',')
61
+ .map((s) => Number.parseInt(s.trim(), 10))
62
+ .filter((n) => Number.isFinite(n));
63
+ return ids.length > 0 ? ids : null;
64
+ }
65
+ export async function repoRoutes(app) {
66
+ // `lastPrChangeAt` rides the listing so the SPA's existing 30s `['repos']` poll learns about a
67
+ // PR change made OUTSIDE a walk (a webhook, a post-write settle, a backstop re-read) — see
68
+ // sync/pr-change-signal.ts. In-memory, account-keyed, optional on the wire.
69
+ app.get('/api/repos', async (req) => {
70
+ const accountId = accountIdOf(req);
71
+ return withPrChangeSignal(accountId, await listRepos(accountId));
72
+ });
73
+ // Default-branch status ("is trunk green?") for every repo in the active WORKSPACE: the head
74
+ // snapshot plus the recent trunk commits with their own CI state. A pure DB read off what the
75
+ // branch sync already persisted — never a live GitHub call, hence the plain `read` rate-limit
76
+ // tier. Informational only: nothing here feeds attention counts, My Turn, or any badge.
77
+ //
78
+ // `?workspace=<id>` is the scope; `?repoIds=` only narrows within it. It carried repo ids alone
79
+ // while `null` meant "every repo of the account", which under a workspace scope meant an EMPTY
80
+ // workspace (no ids on the wire) rendered the whole account's trunk strip, and two workspaces
81
+ // sending no ids shared one client cache slot.
82
+ //
83
+ // ⚠ THE EMPTY CASE IS HANDLED HERE, NOT IN THE GETTER. `getBranchStatus` reads `[]` as "every
84
+ // repo the account owns" — the same widening this route exists to close — so an empty workspace
85
+ // must short-circuit before the call. That getter lives in db/branch-queries.ts and is not this
86
+ // unit's to change; the guard is the route's until it is.
87
+ app.get('/api/branch-status', async (req) => {
88
+ const q = req.query;
89
+ const accountId = accountIdOf(req);
90
+ const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
91
+ if (scope.repoIds.length === 0)
92
+ return { repos: [] };
93
+ return getBranchStatus(accountId, scope.repoIds);
94
+ });
95
+ // The lazy per-day trend series behind an expanded default-branch row (failing trunk commits
96
+ // + PRs merged into the default branch, one shared axis). Fetched only on expand, so it takes
97
+ // ONE repo — never the workspace scope — and, unlike /api/branch-status, the id-addressed
98
+ // getter enforces ownership (→ 404). A pure DB read: default `read` tier.
99
+ app.get('/api/branch-trends', {
100
+ schema: {
101
+ querystring: {
102
+ type: 'object',
103
+ required: ['repoId'],
104
+ additionalProperties: false,
105
+ properties: { repoId: { type: 'integer' } },
106
+ },
107
+ },
108
+ }, async (req, reply) => {
109
+ const { repoId } = req.query;
110
+ const trends = await getBranchTrends(accountIdOf(req), repoId);
111
+ if (trends == null) {
112
+ reply.status(404);
113
+ return { error: 'NotFound', message: `Repo ${repoId} not found` };
114
+ }
115
+ return trends;
116
+ });
117
+ // Live GitHub repository search for the Add-repo picker. Best-match ordering
118
+ // (GitHub default), repos already added to this account filtered out, owned/member repos
119
+ // floated to the top. Detail comes straight from GitHub — nothing persisted.
120
+ app.get('/api/repos/search', { schema: searchSchema }, async (req, reply) => {
121
+ const { q, cursor, limit } = req.query;
122
+ const term = q.trim();
123
+ if (!term) {
124
+ reply.status(400);
125
+ return { error: 'BadRequest', message: 'Search query must not be empty' };
126
+ }
127
+ const accountId = accountIdOf(req);
128
+ // Translate the user term into a literal GitHub search query. An `owner/...`
129
+ // prefix scopes results to that owner (org:/user: qualifier — resolved by type);
130
+ // the remainder (or the whole term) is matched against the repo NAME only
131
+ // (`in:name`), and `needle` drives the literal re-rank below.
132
+ const client = getGraphqlClientFor(await getAccessToken(accountId));
133
+ const slash = term.indexOf('/');
134
+ const owner = slash >= 0 ? term.slice(0, slash).trim() : '';
135
+ const rest = slash >= 0 ? term.slice(slash + 1).trim() : term;
136
+ let searchQuery;
137
+ let needle;
138
+ if (owner) {
139
+ // `owner/...` → scope to that owner. Note a stray leading slash ("/foo") leaves
140
+ // owner empty and falls through to the plain branch (no malformed `user:` query).
141
+ needle = rest;
142
+ let qualifier = `user:${owner}`;
143
+ try {
144
+ const ownerResp = await client(OWNER_TYPE_QUERY, {
145
+ login: owner,
146
+ });
147
+ const kind = ownerResp.repositoryOwner?.__typename ?? null;
148
+ if (kind == null) {
149
+ // Owner login doesn't exist → no possible matches.
150
+ return { results: [], hasNextPage: false, cursor: null };
151
+ }
152
+ qualifier = kind === 'Organization' ? `org:${owner}` : `user:${owner}`;
153
+ }
154
+ catch (err) {
155
+ // Lookup failed (network / rate limit) — default to user: scoping and run the
156
+ // search anyway. Logged so a wrong-scope result is diagnosable.
157
+ req.log.warn({ err, owner }, 'repo search: owner-type lookup failed; defaulting to user: scope');
158
+ }
159
+ searchQuery = rest ? `${qualifier} ${rest} in:name` : qualifier;
160
+ }
161
+ else {
162
+ // Plain term (or a stray leading slash) → literal repo-name search.
163
+ needle = rest;
164
+ searchQuery = rest ? `${rest} in:name` : term;
165
+ }
166
+ let resp;
167
+ try {
168
+ // NB: the GraphQL variable is `searchQuery`, not `query` — @octokit/graphql
169
+ // reserves `query` for the document body and rejects it as a variable name.
170
+ resp = await client(REPO_SEARCH_QUERY, {
171
+ searchQuery,
172
+ first: limit,
173
+ cursor: cursor ?? null,
174
+ });
175
+ }
176
+ catch (err) {
177
+ reply.status(502);
178
+ return {
179
+ error: 'GitHubError',
180
+ message: err instanceof Error ? err.message : 'GitHub search failed',
181
+ };
182
+ }
183
+ const added = await getAddedRepoNodeIds(accountId);
184
+ const me = resp.viewer.login.toLowerCase();
185
+ const orgLogins = new Set(resp.viewer.organizations.nodes
186
+ .filter((o) => o != null)
187
+ .map((o) => o.login.toLowerCase()));
188
+ const results = resp.search.nodes
189
+ // GitHub returns a NULL node for any hit the token can't fully resolve (a scoped
190
+ // GitHub-App token in cloud hits this; the local `gh` PAT sees them all), and {} for
191
+ // a (theoretical) non-repo node. Drop both null-safely BEFORE reading `id`/`owner`,
192
+ // then drop the ones already added. (This null node is the cloud-only crash source.)
193
+ .filter((n) => n != null && typeof n.id === 'string')
194
+ .filter((n) => !added.has(n.id))
195
+ .map((n) => {
196
+ const ownerLogin = n.owner.login;
197
+ return {
198
+ githubNodeId: n.id,
199
+ owner: ownerLogin,
200
+ name: n.name,
201
+ fullName: n.nameWithOwner,
202
+ description: n.description,
203
+ ownerAvatarUrl: n.owner.avatarUrl,
204
+ stargazerCount: n.stargazerCount,
205
+ openPrCount: n.pullRequests.totalCount,
206
+ url: n.url,
207
+ isPrivate: n.isPrivate,
208
+ isOwnedOrMember: ownerLogin.toLowerCase() === me ||
209
+ orgLogins.has(ownerLogin.toLowerCase()),
210
+ };
211
+ });
212
+ // Re-rank for literal matching: closest name match first (exact < prefix <
213
+ // substring < other), then your own/org repos, then stars. Array.prototype.sort
214
+ // is stable (Node ≥ 12), so GitHub's best-match order breaks remaining ties.
215
+ const lc = needle.toLowerCase();
216
+ const nameTier = (name) => {
217
+ if (!lc)
218
+ return 0;
219
+ const n = name.toLowerCase();
220
+ if (n === lc)
221
+ return 0;
222
+ if (n.startsWith(lc))
223
+ return 1;
224
+ if (n.includes(lc))
225
+ return 2;
226
+ return 3;
227
+ };
228
+ results.sort((a, b) => {
229
+ const t = nameTier(a.name) - nameTier(b.name);
230
+ if (t !== 0)
231
+ return t;
232
+ const own = Number(b.isOwnedOrMember) - Number(a.isOwnedOrMember);
233
+ if (own !== 0)
234
+ return own;
235
+ return b.stargazerCount - a.stargazerCount;
236
+ });
237
+ const body = {
238
+ results,
239
+ hasNextPage: resp.search.pageInfo.hasNextPage,
240
+ cursor: resp.search.pageInfo.endCursor,
241
+ };
242
+ return body;
243
+ });
244
+ // First-run onboarding: the viewer's recently-active repositories, detected from their
245
+ // GitHub activity (recent pushes + contributions). Same token idiom as /search; repos
246
+ // already added filtered out; mapped to the picker's RepoSearchResult shape and ordered
247
+ // most-recently-pushed first, capped at 30. Detail comes straight from GitHub — nothing
248
+ // persisted. Local's broad `gh` token surfaces private + org repos; a scoped cloud token
249
+ // surfaces only what it can read (both are handled by the same null-tolerant merge below).
250
+ app.get('/api/repos/suggested', async (req, reply) => {
251
+ const accountId = accountIdOf(req);
252
+ const client = getGraphqlClientFor(await getAccessToken(accountId));
253
+ let resp;
254
+ try {
255
+ // Tolerate PARTIAL GraphQL errors (a scoped cloud token forbidden one sub-field answers
256
+ // 200 + partial data) — the null-drop merge below is built for exactly that. Only a
257
+ // response with NO usable data (auth failure, rate limit, network) 502s.
258
+ resp = await graphqlTolerant(client, VIEWER_REPOS_QUERY, {}, (errors) => req.log.warn({ errors }, 'partial viewer-repos response'));
259
+ }
260
+ catch (err) {
261
+ reply.status(502);
262
+ return {
263
+ error: 'GitHubError',
264
+ message: err instanceof Error ? err.message : 'GitHub request failed',
265
+ };
266
+ }
267
+ if (resp?.viewer == null)
268
+ return { results: [] };
269
+ const me = resp.viewer.login.toLowerCase();
270
+ const orgLogins = new Set(resp.viewer.organizations.nodes
271
+ .filter((o) => o != null)
272
+ .map((o) => o.login.toLowerCase()));
273
+ // Merge the two recency-ordered lists, deduping by repo node id and keeping the more
274
+ // recent pushedAt. Null node arrays (scoped token) and null nodes are dropped BEFORE any
275
+ // field read (ISO-8601 pushedAt strings compare lexically; a null pushedAt sorts last).
276
+ const byId = new Map();
277
+ const merged = [
278
+ ...(resp.viewer.repositories.nodes ?? []),
279
+ ...(resp.viewer.repositoriesContributedTo.nodes ?? []),
280
+ ];
281
+ for (const n of merged) {
282
+ if (n == null || typeof n.id !== 'string')
283
+ continue;
284
+ const existing = byId.get(n.id);
285
+ if (existing == null || (n.pushedAt ?? '') > (existing.pushedAt ?? '')) {
286
+ byId.set(n.id, n);
287
+ }
288
+ }
289
+ const added = await getAddedRepoNodeIds(accountId);
290
+ const results = [...byId.values()]
291
+ .filter((n) => !added.has(n.id))
292
+ // Most-recently-pushed first; a missing pushedAt sorts to the bottom.
293
+ .sort((a, b) => (b.pushedAt ?? '').localeCompare(a.pushedAt ?? ''))
294
+ .slice(0, 30)
295
+ .map((n) => {
296
+ const ownerLogin = n.owner.login;
297
+ return {
298
+ githubNodeId: n.id,
299
+ owner: ownerLogin,
300
+ name: n.name,
301
+ fullName: n.nameWithOwner,
302
+ description: n.description,
303
+ ownerAvatarUrl: n.owner.avatarUrl,
304
+ stargazerCount: n.stargazerCount,
305
+ openPrCount: n.pullRequests.totalCount,
306
+ url: n.url,
307
+ isPrivate: n.isPrivate,
308
+ isOwnedOrMember: ownerLogin.toLowerCase() === me ||
309
+ orgLogins.has(ownerLogin.toLowerCase()),
310
+ };
311
+ });
312
+ const body = { results };
313
+ return body;
314
+ });
315
+ app.post('/api/repos', { schema: createRepoSchema }, async (req, reply) => {
316
+ const { owner, name } = req.body;
317
+ const accountId = accountIdOf(req);
318
+ let resp;
319
+ try {
320
+ const client = getGraphqlClientFor(await getAccessToken(accountId));
321
+ resp = await client(REPO_ID_QUERY, { owner, name });
322
+ }
323
+ catch (err) {
324
+ reply.status(502);
325
+ return {
326
+ error: 'GitHubError',
327
+ message: err instanceof Error ? err.message : 'GitHub request failed',
328
+ };
329
+ }
330
+ if (!resp.repository) {
331
+ reply.status(404);
332
+ return {
333
+ error: 'NotFound',
334
+ message: `Repository ${owner}/${name} not found or inaccessible. Check the name and your gh auth / SSO.`,
335
+ };
336
+ }
337
+ // Enforce the per-account repo cap. Re-adding a repo that is already there is an
338
+ // idempotent no-op (it doesn't grow the count), so only genuinely NEW repos
339
+ // are blocked once at the limit.
340
+ const added = await getAddedRepoNodeIds(accountId);
341
+ if (added.size >= MAX_REPOS_PER_ACCOUNT && !added.has(resp.repository.id)) {
342
+ reply.status(409);
343
+ return {
344
+ error: 'RepoLimitExceeded',
345
+ message: `You can add at most ${MAX_REPOS_PER_ACCOUNT} repositories. Remove one to add another.`,
346
+ };
347
+ }
348
+ const canonOwner = resp.repository.owner.login;
349
+ const canonName = resp.repository.name;
350
+ const repoId = await upsertRepo(canonOwner, canonName, resp.repository.id, null, accountId);
351
+ // No second visibility axis to set: `upsertRepo` gives the repo its workspace membership
352
+ // in the same transaction, and a repo in a workspace is fully live — Feed, Activity, My
353
+ // Turn and Bots all cover it. (The old `setRepoInboxWatch(accountId, repoId, true)` call
354
+ // that used to sit here, and the `watch` body field that fed it, are both gone.)
355
+ //
356
+ // My Turn's "New PRs" clock is `repos.createdAt`, stamped by that same upsert, so adding a
357
+ // repo with 400 open PRs still doesn't dump them into the inbox on day one.
358
+ // Kick off the initial backfill in the background — QUEUED behind any other
359
+ // API-triggered sync of this account, so adding several repos consecutively walks
360
+ // them one at a time instead of hammering one token with N concurrent 90-day
361
+ // backfills (each was also a 10-way commit-file REST fan-out). The 201 still
362
+ // returns immediately; the queued repo reports status 'running' with
363
+ // `paused: { reason: 'queued' }` until its turn comes.
364
+ void enqueueSyncForRepo(repoId, app.log).catch((err) => app.log.error(`enqueue initial sync for repo ${repoId} failed: ${err instanceof Error ? err.message : err}`));
365
+ reply.status(201);
366
+ return getRepo(repoId, accountId);
367
+ });
368
+ // NOTE: there is no `PATCH /api/repos/:id`. Its only body was `{ inboxWatch }`, and the
369
+ // "watched" axis is gone — a repo is either in a workspace (fully live) or removed. Moving
370
+ // a repo between workspaces is `PATCH /api/workspaces/:id` / `POST /api/workspaces/:id/repos`,
371
+ // not a mutation of the repo row.
372
+ app.delete('/api/repos/:id', { schema: idParamSchema }, async (req, reply) => {
373
+ const { id } = req.params;
374
+ const accountId = accountIdOf(req);
375
+ // Ownership check first: a repo this account doesn't own is a 404 (and we
376
+ // must not consult the repoId-keyed sync managers for someone else's repo).
377
+ if (!(await getRepo(id, accountId))) {
378
+ reply.status(404);
379
+ return { error: 'NotFound', message: `Repo ${id} not found` };
380
+ }
381
+ // A repo still WAITING in the per-account queue would resurrect the deleted repo
382
+ // when its turn came (the walk's upsertRepo re-creates the row). Cancelling a queued
383
+ // repo is synchronous: it dequeues + clears the progress row, so the delete can go.
384
+ if (isSyncQueued(id))
385
+ requestSyncCancel(id);
386
+ // A sync in flight would re-create the repo (and its rows) right after we
387
+ // delete them, since the sync's upserts are still running. Refuse until it
388
+ // settles — the cron tick / initial backfill is short.
389
+ if (isSyncRunning(id)) {
390
+ reply.status(409);
391
+ return {
392
+ error: 'Conflict',
393
+ message: 'A sync is running for this repo — try removing it again in a moment.',
394
+ };
395
+ }
396
+ const ok = await deleteRepo(id, accountId);
397
+ if (!ok) {
398
+ reply.status(404);
399
+ return { error: 'NotFound', message: `Repo ${id} not found` };
400
+ }
401
+ reply.status(204);
402
+ return null;
403
+ });
404
+ app.post('/api/repos/:id/sync', { schema: syncSchema }, async (req, reply) => {
405
+ const { id } = req.params;
406
+ if (!(await getRepo(id, accountIdOf(req)))) {
407
+ reply.status(404);
408
+ return { error: 'NotFound', message: `Repo ${id} not found` };
409
+ }
410
+ // ?full=true forces a full backfill (catches CI/thread-resolve changes
411
+ // that don't bump PR.updatedAt and so lag the incremental path).
412
+ const { full } = req.query;
413
+ const forceFull = full === true;
414
+ // Per-repo cooldown. `runSyncForRepo`'s own guard only refuses a sync already in
415
+ // flight for this repo — it did not stop a caller re-firing the moment one finished,
416
+ // so a loop could keep a forced 90-day backfill running permanently and drain the
417
+ // tenant's GitHub quota (breaking their real sync for the rest of the hour).
418
+ const waitMs = manualSyncCooldownMs(id, forceFull);
419
+ if (waitMs > 0) {
420
+ const retryAfter = Math.ceil(waitMs / 1000);
421
+ reply.status(429).header('Retry-After', String(retryAfter));
422
+ return {
423
+ error: 'TooManyRequests',
424
+ message: forceFull
425
+ ? `A deep sync for this repo ran recently — try again in ${retryAfter}s.`
426
+ : `This repo was synced moments ago — try again in ${retryAfter}s.`,
427
+ retryAfter,
428
+ };
429
+ }
430
+ // QUEUED behind the account's other API-triggered syncs (the per-account serial
431
+ // queue replaced the old process-wide concurrency 429): "deep sync everything"
432
+ // fires one POST per repo, and each repo now waits its turn with an honest
433
+ // 'queued' progress row instead of either running 100 concurrent GraphQL walks
434
+ // or bouncing off a 429. Returns as soon as the repo is queued.
435
+ const started = await enqueueSyncForRepo(id, app.log, { forceFull });
436
+ if (!started) {
437
+ reply.status(409);
438
+ return { error: 'Conflict', message: 'A sync is already running for this repo' };
439
+ }
440
+ noteManualSync(id);
441
+ reply.status(202);
442
+ return { status: 'started' };
443
+ });
444
+ app.get('/api/repos/:id/sync-status', { schema: idParamSchema }, async (req, reply) => {
445
+ const { id } = req.params;
446
+ if (!(await getRepo(id, accountIdOf(req)))) {
447
+ reply.status(404);
448
+ return { error: 'NotFound', message: `Repo ${id} not found` };
449
+ }
450
+ return getSyncStatus(id);
451
+ });
452
+ // A cold return's catch-up: an incremental walk whose cutoff is older than this is fetching
453
+ // a delta measured in DAYS, not the ~50 minutes a routine tick covers (cold activity bucket
454
+ // 15 min + the 30-min forced re-walk floor + the 20-min overlap). Three orders of magnitude
455
+ // between the two populations, so nothing here needs tuning.
456
+ const CATCHUP_MIN_AGE_MS = 24 * 60 * 60 * 1000;
457
+ // The account's HEAVY sync work, in TWO disjoint arrays:
458
+ // • `backfills` — repos whose CURRENT walk is full-mode: a first-sync backfill, a forced
459
+ // deep re-sync, or a repo QUEUED for one (its seeded progress row rides
460
+ // `paused: { reason: 'queued' }` and counts only when the seeded mode is 'full').
461
+ // • `catchups` — the ONE kind of incremental walk that is reported: one whose `since` is
462
+ // more than 24h old, i.e. somebody came back to the app after days away and the board is
463
+ // that far behind. Routine incremental ticks stay EXCLUDED: this feeds the SPA's global
464
+ // bottom-right loading bar, which must not flicker on every cron tick.
465
+ // They are separate arrays rather than a flag because they are separate populations and the
466
+ // bar says a different sentence about each ("Backfilling" is a repo's first 90 days;
467
+ // "Catching up" is an established repo that is behind).
468
+ //
469
+ // Cost: an in-memory snapshot (`listActiveSyncProgress`) + ONE account-scoped `repos`
470
+ // SELECT. The join is not just for `fullName` — the progress maps are process-wide across
471
+ // tenants in cloud, so the accountId predicate IS the isolation: a repo that doesn't come
472
+ // back from the SELECT (another tenant's, or deleted mid-walk) is silently dropped. Pure
473
+ // DB read + memory, default `read` rate tier.
474
+ app.get('/api/sync-activity', async (req) => {
475
+ const accountId = accountIdOf(req);
476
+ const now = Date.now();
477
+ const generatedAt = new Date(now).toISOString();
478
+ // Returns the cutoff when this walk is a catch-up, else null — one predicate, used for
479
+ // both the filter and the row, so a row can never be classified one way and read another.
480
+ // An absent `sinceMs` is "not recorded", never "old": it must not invent a catch-up.
481
+ const catchupSinceMs = (p) => p.mode === 'incremental' && p.sinceMs != null && now - p.sinceMs > CATCHUP_MIN_AGE_MS
482
+ ? p.sinceMs
483
+ : null;
484
+ const heavy = listActiveSyncProgress().filter((e) => e.progress.mode === 'full' || catchupSinceMs(e.progress) != null);
485
+ if (heavy.length === 0)
486
+ return { backfills: [], catchups: [], generatedAt };
487
+ const { repos, syncState } = schema;
488
+ const rows = await db
489
+ .select({
490
+ id: repos.id,
491
+ owner: repos.owner,
492
+ name: repos.name,
493
+ lastSyncStatus: syncState.lastSyncStatus,
494
+ })
495
+ .from(repos)
496
+ .leftJoin(syncState, eq(syncState.repoId, repos.id))
497
+ .where(and(eq(repos.accountId, accountId), inArray(repos.id, heavy.map((e) => e.repoId))))
498
+ .execute();
499
+ const nameById = new Map(rows.map((r) => [r.id, `${r.owner}/${r.name}`]));
500
+ const erroredIds = new Set(rows.filter((r) => r.lastSyncStatus === 'error').map((r) => r.id));
501
+ const backfills = [];
502
+ const catchups = [];
503
+ for (const { repoId, progress } of heavy) {
504
+ const fullName = nameById.get(repoId);
505
+ if (fullName === undefined)
506
+ continue; // not this account's repo (or deleted mid-walk)
507
+ // A permanently failing first backfill (revoked token, SAML-walled org) is retried
508
+ // by the scheduler indefinitely, each attempt seeding a fresh full-mode row — which
509
+ // would flash the ambient loading bar on an idle board forever. Suppress the
510
+ // zero-progress retries of a repo already marked errored; a walk that is actually
511
+ // making progress (percent > 0) or an explicit user-queued re-sync still shows.
512
+ //
513
+ // ⚠ THIS COVERS CATCH-UPS TOO, AND FOR A SHARPER REASON: a repo whose walk fails every
514
+ // time never stamps lastIncrementalSyncAt, so its `since` gets older every attempt and
515
+ // it would read as a catch-up FOREVER — a permanent "18 days behind" on a repo nothing
516
+ // can fetch. The predicate is mode-blind on purpose; do not narrow it to full-mode.
517
+ if (erroredIds.has(repoId) &&
518
+ progress.percent === 0 &&
519
+ progress.paused?.reason !== 'queued') {
520
+ continue;
521
+ }
522
+ const row = {
523
+ repoId,
524
+ fullName,
525
+ percent: progress.percent,
526
+ prsProcessed: progress.prsProcessed,
527
+ ...(progress.paused ? { paused: progress.paused } : {}),
528
+ };
529
+ const sinceMs = catchupSinceMs(progress);
530
+ if (sinceMs != null)
531
+ catchups.push({ ...row, sinceMs });
532
+ else
533
+ backfills.push(row);
534
+ }
535
+ return { backfills, catchups, generatedAt };
536
+ });
537
+ // Cancel an in-flight sync. Signals the sync loop to stop, waits for it to
538
+ // settle, then — if this repo never completed a sync (an initial backfill the
539
+ // user is aborting) — deletes it and its partially-loaded data. An established
540
+ // repo whose re-sync was cancelled keeps everything. Drives the modal's Cancel.
541
+ app.post('/api/repos/:id/cancel', { schema: idParamSchema }, async (req, reply) => {
542
+ const { id } = req.params;
543
+ const accountId = accountIdOf(req);
544
+ if (!(await getRepo(id, accountId))) {
545
+ reply.status(404);
546
+ return { error: 'NotFound', message: `Repo ${id} not found` };
547
+ }
548
+ requestSyncCancel(id);
549
+ await waitForSyncToStop(id, 30_000);
550
+ // Re-read AFTER it stops: if the sync actually finished during the wait, the
551
+ // repo is now "synced" and must NOT be deleted (avoids a cancel-vs-finish race).
552
+ const fresh = await getRepo(id, accountId);
553
+ const neverSynced = fresh != null &&
554
+ fresh.lastFullSyncAt == null &&
555
+ fresh.lastIncrementalSyncAt == null;
556
+ let deleted = false;
557
+ if (neverSynced && !isSyncRunning(id))
558
+ deleted = await deleteRepo(id, accountId);
559
+ reply.status(200);
560
+ return { repoId: id, deleted };
561
+ });
562
+ }
563
+ //# sourceMappingURL=repos.js.map
@@ -0,0 +1,41 @@
1
+ import { resolveWorkspaceScope } from '../../db/queries.js';
2
+ import { searchPrs } from '../../db/search.js';
3
+ import { accountIdOf } from '../plugins/auth.js';
4
+ // Workspace-wide full-text search (CORE, no AI). GET /api/search?q=&workspace=&kinds=&limit=&offset=
5
+ // searches the local `search_index` (PR titles + descriptions, review bodies, review-comments,
6
+ // PR-comments, and authors) across the active workspace's repos. `workspace` is a plain integer;
7
+ // absent / unparseable / another tenant's id all resolve to the account's DEFAULT workspace (never
8
+ // a 404 — every id yields the same response shape, so it is not an existence oracle). It is
9
+ // resolved to a repo set server-side (`resolveWorkspaceScope`) so a caller can't widen it, and an
10
+ // empty workspace resolves to `[]` → no hits, rather than the whole account. `kinds`
11
+ // (comma-separated) optionally narrows to one or more hit kinds. Account-scoped in the query layer
12
+ // → no cross-tenant leak.
13
+ const VALID_KINDS = new Set(['pr', 'review', 'review_comment', 'pr_comment']);
14
+ const MAX_LIMIT = 50;
15
+ const DEFAULT_LIMIT = 25;
16
+ export async function searchRoutes(app) {
17
+ app.get('/api/search', async (req) => {
18
+ const q = req.query;
19
+ const accountId = accountIdOf(req);
20
+ const query = (q.q ?? '').trim();
21
+ if (query === '')
22
+ return { query: '', hits: [], people: [], total: 0 };
23
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
24
+ const kinds = (q.kinds ?? '')
25
+ .split(',')
26
+ .map((k) => k.trim())
27
+ .filter((k) => VALID_KINDS.has(k));
28
+ const limRaw = Number.parseInt(q.limit ?? '', 10);
29
+ const offRaw = Number.parseInt(q.offset ?? '', 10);
30
+ const limit = Number.isFinite(limRaw) && limRaw > 0 ? Math.min(limRaw, MAX_LIMIT) : DEFAULT_LIMIT;
31
+ const offset = Number.isFinite(offRaw) && offRaw > 0 ? offRaw : 0;
32
+ return searchPrs(accountId, {
33
+ query,
34
+ repoIds: scope.repoIds,
35
+ kinds: kinds.length > 0 ? kinds : undefined,
36
+ limit,
37
+ offset,
38
+ });
39
+ });
40
+ }
41
+ //# sourceMappingURL=search.js.map