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,458 @@
1
+ import { MY_TURN_TOGGLES, validateMyTurnSettings } from '../../shared/index.js';
2
+ import { config } from '../../config.js';
3
+ import { accountToLocalUser, setBenchmarkConsent, setBlastRadiusConfig, setLargePrCodeLocThreshold, setMyTurnSettings, } from '../../auth/account.js';
4
+ import { resolveLargePrThreshold } from '../../db/code-loc.js';
5
+ import { aiCreditStatus, monthStartMs } from '../../db/credits.js';
6
+ import { eraseAccountData } from '../../db/erase-account.js';
7
+ import { exportAccountData } from '../../db/export-account.js';
8
+ import { runBenchmarkRollupForAccount } from '../../sync/benchmark-rollup.js';
9
+ import { accountIdOf } from '../plugins/auth.js';
10
+ import { EMPTY_CAPABILITIES, entitledProCapabilities, } from '../../pro/contract.js';
11
+ import { getAuthNotices } from '../../sync/auth-notices.js';
12
+ import { isSeverityApiConfigured } from '../../ml/severity-client.js';
13
+ import { getAiRuntimeStatus } from '../../ai/runtime.js';
14
+ import { detectClaudeAuth, NO_CLAUDE_AUTH_MESSAGE } from '../../review/auth.js';
15
+ import { getMyTurn } from '../../db/queries.js';
16
+ import { clearDailyBriefCountsFor } from '../../db/daily-brief.js';
17
+ import { dismissMyTurn, restoreMyTurn } from '../../db/my-turn-dismissals.js';
18
+ const benchmarkConsentSchema = {
19
+ body: {
20
+ type: 'object',
21
+ required: ['optIn'],
22
+ additionalProperties: false,
23
+ properties: { optIn: { type: 'boolean' } },
24
+ },
25
+ };
26
+ // The LARGE-PR FLAG's threshold. `null` is a first-class value meaning "reset to the product
27
+ // default", so the type is the union — NOT an optional key, which would make "clear it" and
28
+ // "don't change it" the same request. The bounds are validation, not taste: `minimum: 1` because a
29
+ // threshold of 0 flags literally every pull request, and an upper bound because an unbounded
30
+ // integer is a number nobody could ever hit, i.e. a setting that silently means "off" while
31
+ // looking like it is on. `multipleOf: 1` rejects 1500.5 (ajv's `integer` already does, but the
32
+ // column is an INTEGER in both dialects and the intent is worth spelling).
33
+ const largePrThresholdSchema = {
34
+ body: {
35
+ type: 'object',
36
+ required: ['threshold'],
37
+ additionalProperties: false,
38
+ properties: {
39
+ threshold: {
40
+ type: ['integer', 'null'],
41
+ minimum: 1,
42
+ maximum: 1_000_000,
43
+ multipleOf: 1,
44
+ },
45
+ },
46
+ },
47
+ };
48
+ // The BLAST-RADIUS reading settings. `config: null` RESETS to the product defaults, the same
49
+ // first-class "no opinion" value `threshold: null` is above — a union, not an optional key, so
50
+ // "clear it" and "leave it alone" stay different requests.
51
+ //
52
+ // The schema is deliberately LOOSE where the sanitizer is strict: `surfacesOff` is a bounded
53
+ // array of short strings here, and `sanitizeBlastRadiusConfig` is what decides which strings are
54
+ // real surfaces (dropping the rest rather than 400-ing the whole write, so an older backend
55
+ // reading a newer client degrades to ignoring one opt-out instead of discarding the dial). The
56
+ // bounds that ARE here are the ones a validator must own: an enum for the dial, and caps so the
57
+ // body cannot be used to push a large payload into a JSON column.
58
+ const blastRadiusConfigSchema = {
59
+ body: {
60
+ type: 'object',
61
+ required: ['config'],
62
+ additionalProperties: false,
63
+ properties: {
64
+ config: {
65
+ type: ['object', 'null'],
66
+ additionalProperties: false,
67
+ required: ['sensitivity'],
68
+ properties: {
69
+ sensitivity: { type: 'string', enum: ['cautious', 'balanced', 'relaxed'] },
70
+ // Show the Pro AI impact note on the PR pane. Absent = shown; only `false` is stored.
71
+ showImpactNote: { type: 'boolean' },
72
+ surfacesOff: {
73
+ type: 'array',
74
+ maxItems: 32,
75
+ items: { type: 'string', maxLength: 32 },
76
+ },
77
+ overrides: {
78
+ type: 'object',
79
+ additionalProperties: false,
80
+ properties: {
81
+ highCodeLoc: { type: 'integer', minimum: 1, maximum: 1_000_000 },
82
+ highCodeFiles: { type: 'integer', minimum: 1, maximum: 100_000 },
83
+ highDirs: { type: 'integer', minimum: 1, maximum: 100_000 },
84
+ highSubsystems: { type: 'integer', minimum: 1, maximum: 100_000 },
85
+ lowCodeLoc: { type: 'integer', minimum: 1, maximum: 1_000_000 },
86
+ lowCodeFiles: { type: 'integer', minimum: 1, maximum: 100_000 },
87
+ },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ };
94
+ // The MY TURN settings. `settings: null` RESETS to the product defaults — a union, not an optional
95
+ // key, like the two settings above.
96
+ //
97
+ // ⚠ EVERY PROPERTY IS DECLARED, down to each `show` key (built from `MY_TURN_TOGGLES`, the one list).
98
+ // Fastify's ajv STRIPS an undeclared key silently rather than rejecting it (the contact-form
99
+ // honeypot defect), so a key missing here would be a switch that saves as nothing. The schema
100
+ // checks SHAPE only; MEANING — a known type, no repeats, weights in steps of 10 summing to 100 — is
101
+ // `validateMyTurnSettings`, whose sentence the Settings form shows before it ever sends.
102
+ const myTurnSettingsSchema = {
103
+ body: {
104
+ type: 'object',
105
+ required: ['settings'],
106
+ additionalProperties: false,
107
+ properties: {
108
+ settings: {
109
+ type: ['object', 'null'],
110
+ additionalProperties: false,
111
+ properties: {
112
+ show: {
113
+ type: 'object',
114
+ additionalProperties: false,
115
+ properties: Object.fromEntries(MY_TURN_TOGGLES.map((k) => [k, { type: 'boolean' }])),
116
+ },
117
+ trunkScope: { type: 'string', maxLength: 32 },
118
+ order: {
119
+ type: 'array',
120
+ maxItems: 15,
121
+ items: { type: 'string', maxLength: 32 },
122
+ },
123
+ weights: {
124
+ type: 'object',
125
+ additionalProperties: false,
126
+ properties: {
127
+ proximity: { type: 'integer' },
128
+ stall: { type: 'integer' },
129
+ relevance: { type: 'integer' },
130
+ },
131
+ },
132
+ },
133
+ },
134
+ },
135
+ },
136
+ };
137
+ // Erasure requires the caller to retype their own GitHub login. maxLength bounds the string
138
+ // (every GitHub login is ≤ 39 chars) so the body can't be used to push a large payload.
139
+ const deleteAccountSchema = {
140
+ body: {
141
+ type: 'object',
142
+ required: ['confirmLogin'],
143
+ additionalProperties: false,
144
+ properties: { confirmLogin: { type: 'string', minLength: 1, maxLength: 64 } },
145
+ },
146
+ };
147
+ /**
148
+ * `MeResponse.ai`. `enabled` is the ONE agentic switch (config.aiEnabled: local and not
149
+ * LIMN_AI_DISABLED) — the same value that decides whether `registerAgenticRoutes` registers
150
+ * anything. `runtime` is the one-time SDK download (ai/runtime.ts). `auth` is `detectClaudeAuth`'s
151
+ * HEURISTIC (a `~/.claude` directory counts): it decides only whether a Run button or the one
152
+ * "Sign in to Claude Code or set ANTHROPIC_API_KEY" line shows, never whether the feature is
153
+ * visible — the run itself is the authoritative check. Disabled ⇒ the other fields are inert.
154
+ */
155
+ function agenticAiStatus() {
156
+ if (!config.aiEnabled) {
157
+ return { enabled: false, runtime: 'absent', runtimeMessage: null, auth: 'none', authMessage: null };
158
+ }
159
+ const rt = getAiRuntimeStatus();
160
+ const auth = detectClaudeAuth();
161
+ return {
162
+ enabled: true,
163
+ runtime: rt.runtime,
164
+ runtimeMessage: rt.runtimeMessage,
165
+ auth: auth.status === 'ok' ? 'ok' : 'none',
166
+ authMessage: auth.status === 'ok' ? null : NO_CLAUDE_AUTH_MESSAGE,
167
+ };
168
+ }
169
+ export async function meRoutes(app) {
170
+ app.get('/api/me', async (req) => {
171
+ const accountId = accountIdOf(req);
172
+ const user = accountToLocalUser(req.account);
173
+ // Per-account entitlement: local = full capabilities; cloud = full only when the
174
+ // account's plan isn't 'free' (Stripe billing seam) — used for the `pro` passthrough.
175
+ const entitled = req.account
176
+ ? entitledProCapabilities(req.account)
177
+ : EMPTY_CAPABILITIES;
178
+ // Month-to-date AI balances, computed CORE-side (the plan/allowance rules live in db/credits)
179
+ // so the SPA has the spend baseline on the very first authenticated call — no separate Pro
180
+ // fetch needed on login. Split by seam: summary is metered by TURN count, agent by CREDITS.
181
+ let aiUsage = null;
182
+ if (req.account) {
183
+ const nowMs = Date.now();
184
+ const c = await aiCreditStatus(req.account, nowMs);
185
+ aiUsage = {
186
+ enabled: true,
187
+ monthStart: new Date(monthStartMs(nowMs)).toISOString(),
188
+ summaryTurnsUsed: c.summaryTurnsUsed,
189
+ summaryTurnLimit: c.summaryTurnLimit,
190
+ summaryTurnsRemaining: c.summaryTurnsRemaining,
191
+ agentCreditsUsed: c.agentCreditsUsed,
192
+ agentAllowanceCredits: c.agentAllowanceCredits,
193
+ agentCreditsRemaining: c.agentCreditsRemaining,
194
+ };
195
+ }
196
+ return {
197
+ user,
198
+ // NOTE: this response deliberately carries NO My-Turn counts. It used to return a
199
+ // `counts` object (a whole `getMyTurn` fold) plus `feedLastSeenAt`/`newFeedItems` (a
200
+ // second fold over the feed) for the Welcome-back banner. The banner is now
201
+ // per-workspace and reads standing `my_turn` card counts off the daily brief, so all
202
+ // three had no reader left and were pure per-request work on the SPA's first call.
203
+ // If you need a count here again, prefer the brief's fold over re-adding one.
204
+ deploymentMode: config.deploymentMode,
205
+ pro: entitled,
206
+ // THE AGENTIC FEATURES — Claude Review (+ chat, follow-up, ticket check, auto review),
207
+ // review memory, AI Fix. CORE, FREE, LOCAL ONLY. TOP-LEVEL and NOT inside `pro`, so
208
+ // `entitledProCapabilities` can never zero it (the `mlSeverity` argument).
209
+ ai: agenticAiStatus(),
210
+ // ML severity/category enrichment of bot comments — FREE TIER, so a TOP-LEVEL field and
211
+ // NOT part of `pro` above: `entitledProCapabilities` returns all-false for a cloud
212
+ // account on the free plan, which would hide this from exactly the users it is for.
213
+ // True iff a severity-api is reachable for this deployment; false under `npx`.
214
+ mlSeverity: isSeverityApiConfigured(),
215
+ // The in-app merge conflict resolver. CORE and free, and now registered in BOTH modes —
216
+ // so this is a constant, kept on the wire because the SPA gates its "Resolve conflicts"
217
+ // button on it and a field that disappears is a field every caller has to re-learn.
218
+ // ⚠ NOT a git-version probe: /api/me does not shell out on every SPA boot. A git too old
219
+ // for `merge-tree --write-tree` is refused by the OPEN route with `git_too_old`.
220
+ conflictResolver: true,
221
+ // Chronology's default working-hours zone for a workspace that never set one.
222
+ workTimeZone: config.defaultWorkTimezone,
223
+ // Cross-org benchmark consent (cloud-only; always false in local). Drives the Settings toggle.
224
+ benchmarkOptIn: config.isCloud ? req.account?.benchmarkOptIn ?? false : false,
225
+ // The LARGE-PR FLAG's threshold, RESOLVED (stored value, else the product default) so no
226
+ // renderer has to know about the null. TOP-LEVEL and not inside `pro` above: the flag is
227
+ // free, and `entitledProCapabilities` zeroes that object for free cloud accounts — exactly
228
+ // this feature's audience (the `mlSeverity` argument, verbatim).
229
+ largePrCodeLocThreshold: resolveLargePrThreshold(req.account?.largePrCodeLocThreshold ?? null),
230
+ largePrCodeLocThresholdIsDefault: req.account?.largePrCodeLocThreshold == null,
231
+ // The BLAST-RADIUS reading settings, RAW and nullable — the one place this response does
232
+ // NOT resolve a default, and deliberately. The defaults are an 18-number table across
233
+ // three dial positions living in `packages/shared`, which is types-only on this side
234
+ // (PACKAGING); resolving here would mean hand-mirroring it forever. The SPA's one
235
+ // `resolveBlastConfig()` imports that table as a real value instead, so `null` travels and
236
+ // nothing is duplicated. Free feature → top-level, not inside `pro` (the `mlSeverity`
237
+ // argument again).
238
+ blastRadius: req.account?.blastRadiusConfig ?? null,
239
+ // The MY TURN settings, RAW and nullable (the stored overrides) — the `blastRadius` rule: the
240
+ // SPA resolves them through the ONE `resolveMyTurnSettings`, so nothing is duplicated here.
241
+ // Free feature → top-level, not inside `pro` (the `mlSeverity` argument).
242
+ myTurnSettings: req.account?.myTurnSettings ?? null,
243
+ // Orgs currently SAML-blocked for this account (empty in the normal case + in local).
244
+ authNotices: getAuthNotices(accountId),
245
+ aiUsage,
246
+ };
247
+ });
248
+ // Cross-org benchmark consent (CLOUD-ONLY, opt-in). Setting it true seeds the account's
249
+ // contributions immediately (best-effort, in the background); false withdraws + deletes them
250
+ // (handled in setBenchmarkConsent). Available to every cloud account — free or paid — because
251
+ // the network needs volume to be worth anything (viewing the benchmark is the paid part, later).
252
+ app.post('/api/me/benchmark-consent', { schema: benchmarkConsentSchema }, async (req, reply) => {
253
+ if (!config.isCloud) {
254
+ return reply.code(400).send({ error: 'BadRequest', message: 'Benchmark is cloud-only' });
255
+ }
256
+ const accountId = accountIdOf(req);
257
+ const { optIn } = req.body;
258
+ await setBenchmarkConsent(accountId, optIn);
259
+ if (optIn) {
260
+ // Fire-and-forget: don't block the response on the rollup; a failure is logged, not fatal.
261
+ void runBenchmarkRollupForAccount(accountId, req.log).catch((err) => req.log.error({ err, accountId }, 'benchmark seed after opt-in failed'));
262
+ }
263
+ return { status: 'ok', benchmarkOptIn: optIn };
264
+ });
265
+ // The LARGE-PR FLAG's threshold — ONE PER-ACCOUNT SETTING, in lines of CODE churn (the
266
+ // docs/config/lockfile/generated churn is excluded before the sum; see db/code-loc.ts).
267
+ //
268
+ // Deliberately NOT per-workspace and not per-repo: a second grain would need a resolver, and
269
+ // "which grain am I reading?" is the question the reviewer object spent migration 0045 removing.
270
+ // `threshold: null` RESETS to the product default rather than storing 1500 — the two states are
271
+ // "the user has an opinion" and "the user does not", so a future change to the default still
272
+ // reaches everyone who never overrode it.
273
+ //
274
+ // Available in BOTH modes and on every tier: the flag is free, so its setting must be too.
275
+ // Rate tier: the schema-validated single-column UPDATE falls through `tierFor` to the blanket
276
+ // `read` bucket, which is DECIDED (and pinned in rate-limit.test.ts), not inherited — it reaches
277
+ // no GitHub API and no model, exactly like POST /api/me/benchmark-consent beside it.
278
+ app.post('/api/me/large-pr-threshold', { schema: largePrThresholdSchema }, async (req, reply) => {
279
+ const accountId = accountIdOf(req);
280
+ const { threshold } = req.body;
281
+ // Belt and braces over the JSON schema: `Number.isInteger` also rejects NaN/Infinity, which
282
+ // a hand-rolled body validator elsewhere could let through as `type: 'integer'` never sees.
283
+ if (threshold !== null && !(Number.isInteger(threshold) && threshold > 0)) {
284
+ return reply.code(400).send({
285
+ error: 'BadRequest',
286
+ message: 'threshold must be a positive whole number of lines, or null to reset.',
287
+ });
288
+ }
289
+ await setLargePrCodeLocThreshold(accountId, threshold);
290
+ const body = {
291
+ status: 'ok',
292
+ largePrCodeLocThreshold: resolveLargePrThreshold(threshold),
293
+ largePrCodeLocThresholdIsDefault: threshold == null,
294
+ };
295
+ return body;
296
+ });
297
+ // The BLAST-RADIUS reading settings — the second ONE-PER-ACCOUNT setting on this route, and
298
+ // account-grained for the same reasons as the threshold above plus one of its own: the
299
+ // comparison it feeds is RENDER-TIME, so it rides /api/me and the vis-timeline tooltip (raw
300
+ // HTML strings, no hooks) can read it off a module cell. A per-workspace value would put a new
301
+ // "workspace not resolved yet" null-state on four surfaces.
302
+ //
303
+ // A PUT rather than a POST because the body is the WHOLE settings object — this replaces the
304
+ // stored blob, it does not merge into it, and the verb should say so.
305
+ //
306
+ // Available in BOTH modes and on every tier: blast radius is free, so its setting must be too.
307
+ // Rate tier: one schema-validated single-column UPDATE, no GitHub and no model — the blanket
308
+ // `read` bucket, DECIDED and pinned in rate-limit.test.ts rather than inherited.
309
+ app.put('/api/me/blast-radius-config', { schema: blastRadiusConfigSchema }, async (req) => {
310
+ const accountId = accountIdOf(req);
311
+ const { config: incoming } = req.body;
312
+ // ⚠ ECHO WHAT WAS STORED, NOT WHAT WAS SENT. The sanitizer drops unknown surfaces and
313
+ // out-of-range overrides, so returning the request body would let Settings render a choice
314
+ // the database does not hold — the same disagreement `largePrCodeLocThresholdIsDefault`
315
+ // exists to prevent one field over.
316
+ const stored = await setBlastRadiusConfig(accountId, incoming);
317
+ const body = { status: 'ok', blastRadius: stored };
318
+ return body;
319
+ });
320
+ // The MY TURN settings — the third ONE-PER-ACCOUNT setting on this route: which card types count
321
+ // as your turn, the order My turn groups them in, and the Do next weights every Pending tab
322
+ // ranks by. Account-grained because one account is one reader, in every workspace.
323
+ //
324
+ // A PUT (the body is the WHOLE settings object, replacing the stored one). ⚠ ECHOES WHAT WAS
325
+ // STORED: `setMyTurnSettings` compacts to overrides only, so a sent default is not stored and
326
+ // Settings must render what the database holds, not what it sent.
327
+ //
328
+ // No 404: the target is the caller's own account (an unauthenticated cloud call is a 401 at the
329
+ // gate). Both modes, every tier — the feature is free. Rate tier `read`, DECIDED and pinned in
330
+ // rate-limit.test.ts: one validated UPDATE, no GitHub and no model.
331
+ app.put('/api/me/my-turn-settings', { schema: myTurnSettingsSchema }, async (req, reply) => {
332
+ const accountId = accountIdOf(req);
333
+ const { settings } = req.body;
334
+ if (settings !== null) {
335
+ const problem = validateMyTurnSettings(settings);
336
+ if (problem != null) {
337
+ return reply.code(400).send({ error: 'InvalidMyTurnSettings', message: problem });
338
+ }
339
+ }
340
+ const stored = await setMyTurnSettings(accountId, settings);
341
+ // The brief's roll-up counts are cached for five minutes; a save that switched a type off
342
+ // must not leave "Elsewhere" lines counting it until the TTL runs out.
343
+ clearDailyBriefCountsFor(accountId);
344
+ const body = { status: 'ok', myTurnSettings: stored };
345
+ return body;
346
+ });
347
+ // ---- Data-subject rights (UK/EU GDPR Arts. 15, 17, 20; CCPA/CPRA) ----
348
+ //
349
+ // Both are SELF-SERVICE by design. A privacy policy that says "email us to be deleted" is a
350
+ // promise backed by a human remembering to run SQL; these two routes are the promise backed
351
+ // by code, and they are what the policy at /privacy §9 points at.
352
+ // Access + portability: the whole account as one JSON document. The sealed GitHub token is
353
+ // excluded (see db/export-account.ts) — an export is a file people email to themselves.
354
+ app.get('/api/me/export', async (req, reply) => {
355
+ const accountId = accountIdOf(req);
356
+ const data = await exportAccountData(accountId);
357
+ if (!data) {
358
+ return reply.code(404).send({ error: 'NotFound', message: 'Account not found' });
359
+ }
360
+ // Content-Disposition so the browser saves a file rather than rendering a huge JSON blob;
361
+ // the date in the name makes successive exports distinguishable.
362
+ const stamp = new Date().toISOString().slice(0, 10);
363
+ reply.header('content-disposition', `attachment; filename="pierre-export-${data.account.githubLogin ?? accountId}-${stamp}.json"`);
364
+ reply.type('application/json');
365
+ return data;
366
+ });
367
+ // Erasure. Irreversible, and deliberately requires the caller to type their own GitHub login
368
+ // into `confirmLogin` — not as security (the session already proves who they are) but as
369
+ // INTENT: this destroys every synced repository and cannot be undone, so a mis-click or a
370
+ // stray fetch must not be sufficient. The cross-origin guard already blocks a foreign page
371
+ // from issuing it at all.
372
+ app.delete('/api/me/account', { schema: deleteAccountSchema }, async (req, reply) => {
373
+ const account = req.account;
374
+ if (!account) {
375
+ return reply.code(401).send({ error: 'Unauthorized', message: 'Sign in first.' });
376
+ }
377
+ const { confirmLogin } = req.body;
378
+ if (!account.githubLogin ||
379
+ confirmLogin.trim().toLowerCase() !== account.githubLogin.toLowerCase()) {
380
+ return reply.code(400).send({
381
+ error: 'BadRequest',
382
+ message: 'confirmLogin must match your GitHub username exactly.',
383
+ });
384
+ }
385
+ // A local install is a single implicit account synthesized from `gh api user` at every
386
+ // startup — deleting it would be recreated seconds later, and the user's actual delete
387
+ // action is removing the SQLite file. Refuse rather than pretend.
388
+ if (account.isLocal) {
389
+ return reply.code(400).send({
390
+ error: 'BadRequest',
391
+ message: 'This is a local install: there is no hosted account to delete. Remove the ' +
392
+ 'database file (see `pierre --help` for its location) to erase everything.',
393
+ });
394
+ }
395
+ req.log.warn({ accountId: account.id }, 'account erasure requested');
396
+ const result = await eraseAccountData(account.id);
397
+ req.log.warn({ accountId: account.id, reposDeleted: result.reposDeleted }, 'account erased');
398
+ // Drop the session too, or the browser keeps presenting a cookie for an account that no
399
+ // longer exists (which resolves to `null` and 401s confusingly).
400
+ try {
401
+ req.session.delete();
402
+ }
403
+ catch {
404
+ /* no session plugin in local mode — unreachable here, but harmless */
405
+ }
406
+ return { status: 'deleted', reposDeleted: result.reposDeleted };
407
+ });
408
+ // The account-wide inbox. Unscoped on purpose (see `getMyTurn`): the browser-notification
409
+ // watcher reads exactly this call — and without `onePerPr`, which is the board's rule.
410
+ app.get('/api/my-turn', async (req) => getMyTurn(accountIdOf(req)));
411
+ // DISMISS / BRING BACK one My Turn subject — a pull request (`pr`) or, for a red default branch,
412
+ // a repository (`repo`). ⚠ NOT the "Done" routes 0060 deleted: a dismissal lasts only until
413
+ // something newer happens on the subject, and a subject that leaves your plate discharges it
414
+ // (db/my-turn-dismissals.ts). DB-only, both modes, free.
415
+ //
416
+ // The id is in the PATH; ownership is checked (→ 404, so the family is no existence oracle) and
417
+ // the composite FK refuses a cross-account pair in the database regardless.
418
+ app.put('/api/my-turn/dismissals/:kind/:id', async (req, reply) => {
419
+ const target = parseDismissTarget(req.params);
420
+ if (target == null) {
421
+ return reply.code(400).send({ error: 'BadRequest', message: 'Unknown My Turn entry.' });
422
+ }
423
+ const accountId = accountIdOf(req);
424
+ const at = await dismissMyTurn(accountId, target);
425
+ if (at == null)
426
+ return reply.code(404).send({ error: 'NotFound', message: 'Not found' });
427
+ // The brief's roll-up counts are cached for five minutes; a dismissed item must not go on
428
+ // being counted in the "Elsewhere" lines until the TTL runs out.
429
+ clearDailyBriefCountsFor(accountId);
430
+ const body = { target, dismissedAt: at.toISOString() };
431
+ return body;
432
+ });
433
+ app.delete('/api/my-turn/dismissals/:kind/:id', async (req, reply) => {
434
+ const target = parseDismissTarget(req.params);
435
+ if (target == null) {
436
+ return reply.code(400).send({ error: 'BadRequest', message: 'Unknown My Turn entry.' });
437
+ }
438
+ const accountId = accountIdOf(req);
439
+ // Scoped on the row itself, so another account's id deletes nothing — and answers 404.
440
+ if (!(await restoreMyTurn(accountId, target))) {
441
+ return reply.code(404).send({ error: 'NotFound', message: 'Not found' });
442
+ }
443
+ clearDailyBriefCountsFor(accountId);
444
+ return reply.code(204).send();
445
+ });
446
+ }
447
+ /** `:kind/:id` → a target, or null for anything else (a non-integer id, an unknown kind). */
448
+ export function parseDismissTarget(params) {
449
+ const p = (params ?? {});
450
+ if (p.kind !== 'pr' && p.kind !== 'repo')
451
+ return null;
452
+ const raw = typeof p.id === 'string' ? p.id : '';
453
+ if (!/^[1-9][0-9]{0,15}$/.test(raw))
454
+ return null;
455
+ const id = Number(raw);
456
+ return Number.isSafeInteger(id) ? { kind: p.kind, id } : null;
457
+ }
458
+ //# sourceMappingURL=me.js.map
@@ -0,0 +1,22 @@
1
+ import { getScopeMentionCandidates, resolveWorkspaceScope } from '../../db/queries.js';
2
+ import { accountIdOf } from '../plugins/auth.js';
3
+ // Workspace-wide @mention candidates (CORE) — the scope-wide sibling of
4
+ // GET /api/prs/:id/mention-candidates. Powers the ad-hoc Insights "Ask about the workspace" box,
5
+ // whose questions span the whole selected scope rather than one PR.
6
+ //
7
+ // `?workspace=<id>` is a plain integer; absent / unparseable / another tenant's id all resolve to
8
+ // the account's DEFAULT workspace (never a 404 — every id yields the same response shape, so it is
9
+ // not an existence oracle). `resolveWorkspaceScope` turns it into the workspace's repo ids
10
+ // server-side, so a caller cannot widen it, and an empty workspace yields `[]` → no candidates,
11
+ // rather than the account's whole roster. Self + bots excluded. Returns a bare MentionCandidate[]
12
+ // exactly like the PR route so MentionTextarea can consume it directly — each row carrying
13
+ // `isMaintainer` (has merged a PR in the SCOPE's repos), which the picker sorts and shields on.
14
+ export async function mentionsRoutes(app) {
15
+ app.get('/api/mention-candidates', async (req) => {
16
+ const q = req.query;
17
+ const accountId = accountIdOf(req);
18
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
19
+ return getScopeMentionCandidates(accountId, scope.repoIds);
20
+ });
21
+ }
22
+ //# sourceMappingURL=mentions.js.map
@@ -0,0 +1,8 @@
1
+ import { getMergers } from '../../db/queries.js';
2
+ import { accountIdOf } from '../plugins/auth.js';
3
+ // Per-repo "merge rights" inference (distinct users who've merged a PR there).
4
+ // Reference data — no filters; the frontend looks it up per timeline row.
5
+ export async function mergersRoutes(app) {
6
+ app.get('/api/mergers', async (req) => getMergers(accountIdOf(req)));
7
+ }
8
+ //# sourceMappingURL=mergers.js.map
@@ -0,0 +1,109 @@
1
+ import { getMlBacklogForAccount, getPrMlLabels, } from '../../db/ml-labels.js';
2
+ import { isSeverityApiConfigured } from '../../ml/severity-client.js';
3
+ import { getMlEnrichmentState } from '../../sync/ml-enrichment.js';
4
+ import { accountIdOf } from '../plugins/auth.js';
5
+ // ML severity/category labels on BOT-authored text (CORE, FREE TIER — not a Pro capability, so
6
+ // deliberately NOT under /api/pro/, which is 402'd for free cloud accounts and auto-tiered as
7
+ // AI for any mutating verb).
8
+ //
9
+ // BOTH ROUTES ARE PURE DB READS. Nothing here calls the model, spends money or touches GitHub —
10
+ // generation is the background worker's job (sync/ml-enrichment.ts). That is why they are
11
+ // registered UNCONDITIONALLY, even where no severity-api is configured: they answer honestly
12
+ // (an empty label set / `enabled:false`) instead of 404ing, so a deployment that later gains
13
+ // the service needs no client change, and the SPA's own gate is `MeResponse.mlSeverity`.
14
+ export async function mlLabelRoutes(app) {
15
+ // Every ML label on one PR — THE per-PR index. One request serves every badge on the page
16
+ // (a 60-thread PR must not become 60 requests), so the client caches it with
17
+ // `staleTime: Infinity` and each card looks itself up by (targetKind, targetId).
18
+ //
19
+ // A target with no label is simply absent from the array: "no label" and "not enriched yet"
20
+ // are the same thing to a badge, and both mean render nothing.
21
+ app.get('/api/prs/:id/ml-labels', {
22
+ schema: {
23
+ params: {
24
+ type: 'object',
25
+ properties: { id: { type: 'integer' } },
26
+ required: ['id'],
27
+ },
28
+ },
29
+ }, async (req, reply) => {
30
+ const { id } = req.params;
31
+ // Ownership lives in the getter, which returns null for a foreign PR (the repo-wide rule
32
+ // for id-addressed reads).
33
+ const labels = await getPrMlLabels(id, accountIdOf(req));
34
+ if (labels === null) {
35
+ reply.status(404);
36
+ return { error: 'NotFound', message: `PR ${id} not found` };
37
+ }
38
+ return { prId: id, labels, generatedAt: new Date().toISOString() };
39
+ });
40
+ // (GET /api/bot-severity — the standalone Bots-interface severity rollup — was REMOVED on the
41
+ // C7 cut list: no SPA consumer since the merged Bots ROI table took its fold off
42
+ // `getBotAnalytics`. `getBotSeverityRollup` itself stays in db/ml-labels.ts — its exclusion
43
+ // semantics are the pinned reference the windowed twin declares itself VERBATIM-equal to.)
44
+ // Live state of the background enrichment worker — what every sync surface polls so that
45
+ // "sync complete" is not announced while the model is still scoring the text the walk just
46
+ // fetched. See MlEnrichmentStatus in @pierre-review/shared for why the sync UI needs it.
47
+ //
48
+ // NO SCOPE PARAMETER, on purpose: the worker walks every workspace of the account, so a
49
+ // workspace-scoped backlog would under-report exactly the work that is running.
50
+ app.get('/api/ml-status', { schema: { querystring: { type: 'object', additionalProperties: false, properties: {} } } }, async (req) => {
51
+ const accountId = accountIdOf(req);
52
+ const enabled = isSeverityApiConfigured();
53
+ const state = getMlEnrichmentState();
54
+ // Skip the scan entirely when the feature is off: the client is polling only because it
55
+ // cannot know that until it asks once, and the counts would be meaningless anyway.
56
+ const backlog = enabled
57
+ ? await cachedBacklog(accountId)
58
+ : { pending: 0, unscorable: 0, labelled: 0 };
59
+ const iso = (ms) => ms == null ? null : new Date(ms).toISOString();
60
+ return {
61
+ enabled,
62
+ running: state.running,
63
+ pending: backlog.pending,
64
+ unscorable: backlog.unscorable,
65
+ labelled: backlog.labelled,
66
+ scoredThisRun: state.labelled,
67
+ batchesThisRun: state.batches,
68
+ failuresThisRun: state.failures,
69
+ startedAt: iso(state.startedAt),
70
+ finishedAt: iso(state.finishedAt),
71
+ pausedUntil: iso(state.pausedUntil),
72
+ serviceHealthy: state.serviceHealthy,
73
+ markerFallback: state.markerFallback,
74
+ generatedAt: new Date().toISOString(),
75
+ };
76
+ });
77
+ }
78
+ // ---- Backlog cache ----
79
+ //
80
+ // This route is POLLED — every few seconds while a sync round is open — and its backlog is a
81
+ // bot-set resolve plus three indexed counts PER WORKSPACE. The counts move only when the worker
82
+ // writes (at most a batch every few seconds) or a sync lands, so serving a couple of seconds of
83
+ // staleness costs the reader nothing and stops N pollers from multiplying the scan. A rate-limit
84
+ // tier alone would not help: it caps requests, not the work each one does.
85
+ const BACKLOG_TTL_MS = 3_000;
86
+ const backlogCache = new Map();
87
+ function cachedBacklog(accountId) {
88
+ const hit = backlogCache.get(accountId);
89
+ if (hit && Date.now() - hit.at < BACKLOG_TTL_MS)
90
+ return hit.value;
91
+ // Cache the PROMISE, not the resolved value, so concurrent polls that arrive while the scan
92
+ // is still running share it instead of each starting their own.
93
+ const value = getMlBacklogForAccount(accountId).catch((err) => {
94
+ // A failed scan must not be cached as a permanent zero, and must not reject every waiter
95
+ // repeatedly: drop the entry so the next poll retries, and report an honest empty backlog.
96
+ backlogCache.delete(accountId);
97
+ throw err;
98
+ });
99
+ backlogCache.set(accountId, { at: Date.now(), value });
100
+ // Bounded: process-wide across tenants in cloud, so evict anything past the window.
101
+ if (backlogCache.size > 1_000) {
102
+ const cutoff = Date.now() - BACKLOG_TTL_MS;
103
+ for (const [id, entry] of backlogCache)
104
+ if (entry.at < cutoff)
105
+ backlogCache.delete(id);
106
+ }
107
+ return value;
108
+ }
109
+ //# sourceMappingURL=ml-labels.js.map
@@ -0,0 +1,35 @@
1
+ import { getOpenPrs, resolveWorkspaceScope } from '../../db/queries.js';
2
+ import { accountIdOf } from '../plugins/auth.js';
3
+ // `repoIds` → the `narrow` argument of `resolveWorkspaceScope`, never a scope in its own right.
4
+ // Empty/absent → null = no narrowing (every repo in the workspace).
5
+ function parseIntList(raw) {
6
+ if (!raw)
7
+ return null;
8
+ const ids = raw
9
+ .split(',')
10
+ .map((s) => Number.parseInt(s.trim(), 10))
11
+ .filter((n) => Number.isFinite(n));
12
+ return ids.length > 0 ? ids : null;
13
+ }
14
+ export async function openPrsRoutes(app) {
15
+ // Every currently-open PR in the active WORKSPACE (ignores the board's date range).
16
+ //
17
+ // `?workspace=<id>` is the scope; `?repoIds=` only narrows within it. This route used to carry
18
+ // repo ids alone, with `null` meaning "every repo of the account" — so an EMPTY workspace, which
19
+ // sends no ids, listed the whole account's open PRs, and two workspaces sending no ids collided
20
+ // in one client cache slot. `resolveWorkspaceScope` returns `membership ∩ (?repoIds= ??
21
+ // membership)`: always concrete, `[]` for an empty workspace, which `getOpenPrs` renders as an
22
+ // empty list.
23
+ app.get('/api/open-prs', async (req) => {
24
+ const q = req.query;
25
+ const accountId = accountIdOf(req);
26
+ const scope = await resolveWorkspaceScope(accountId, q.workspace, parseIntList(q.repoIds));
27
+ const prs = await getOpenPrs({
28
+ accountId,
29
+ repoIds: scope.repoIds,
30
+ userIds: parseIntList(q.userIds),
31
+ });
32
+ return { prs };
33
+ });
34
+ }
35
+ //# sourceMappingURL=open-prs.js.map