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,503 @@
1
+ // WS1c — the layered reviewer classifier (CORE). Resolution order (first HARD hit wins; soft
2
+ // signals only accumulate into the MEDIUM band):
3
+ //
4
+ // 1. Known vendor login (reviewBotKind) → vendor kind. HIGH
5
+ // 2. githubType==='Bot' OR opt-in app attribution → fingerprint tool ?? in_house. HIGH
6
+ // 3. Branded/marked fingerprint → fingerprint tool ?? in_house. HIGH
7
+ // 4. Behavioral score (+ service-account login promotion) → in_house. MEDIUM (never auto-badges)
8
+ // otherwise → human (automated:false).
9
+ //
10
+ // ⚠ THERE IS NO LLM LEG IN HERE ANY MORE. A fifth step used to sit under the MEDIUM band — one
11
+ // `cheapComplete` call over 3 sample comments — behind the plugin setting `bots.aiTiebreak`. It
12
+ // was UNREACHABLE: the setting lived in the plugin's `pro_settings`, which CORE cannot read, and
13
+ // not one of the four `classifyReviewer` call sites ever passed the flag. It went with the setting,
14
+ // along with the per-account login allowlist (same story), which is why this module no longer
15
+ // imports `review/llm.js`. The `ClassificationSource` union still carries 'ai_tiebreak' for rows
16
+ // stored by an older build. Re-adding an LLM leg here means re-tiering three `read`-bucket routes
17
+ // (see api/plugins/rate-limit.ts) in the SAME change.
18
+ //
19
+ // ── IT DERIVES ONCE PER ACTOR AND WRITES THAT VERDICT TO EVERY ONE OF THE NAMED WORKSPACE ROWS ─
20
+ // A bot is a PER-WORKSPACE object, but the SIGNALS are not: a vendor login, `users.githubType`,
21
+ // app attribution and the branded-marker fingerprint are all properties of the ACTOR and give the
22
+ // same answer in every workspace. Deriving per workspace would multiply the work — and the BILLED
23
+ // Haiku tie-break — for an identical result, and would weaken the behavioural score by computing
24
+ // it on a thin slice. So `classifyReviewer` takes the actor's TARGET WORKSPACE LIST and `persist`
25
+ // fans one verdict out across it.
26
+ //
27
+ // ── IT GATES ON TWO PROVENANCE FLAGS THAT NOW SHARE ONE ROW ───────────────────────────────────
28
+ // `workspace_reviewers.identity_source = 'manual'` ⇒ leave kind/label alone (a human named this
29
+ // vendor; re-deriving reverts the correction)
30
+ // `workspace_reviewers.source = 'manual'` ⇒ leave automated/role/confidence/reasons
31
+ // alone FOR THAT WORKSPACE (a human judged
32
+ // this row; the OTHER workspaces still update)
33
+ //
34
+ // ⚠ THEY USED TO LIVE ON TWO TABLES, AND THAT TABLE BOUNDARY WAS THE ENFORCEMENT. It is gone: both
35
+ // facts are columns of one `workspace_reviewers` row now, so the separation is CODE DISCIPLINE —
36
+ // a `set:` object narrowed per half, built fresh per workspace, honouring each flag independently.
37
+ // The failure it prevents is unchanged: gate identity on the judgement flag and a "not a bot" here
38
+ // blanks the vendor's brand colour; gate the judgement on the identity flag and naming a vendor
39
+ // freezes auto-detection. See §4.6 / §9.9 of the workspace contract, and the paired tests.
40
+ //
41
+ // ⚠ THE SHARED-`values`-OBJECT PATTERN IS GONE ON PURPOSE. One object used as both the INSERT
42
+ // values and the ON CONFLICT `set:` is correct only while a table holds ONE grain. With judgement,
43
+ // identity and price in a single row it would overwrite a human's vendor correction on every auto
44
+ // pass — the exact bug the two-table split was built to kill, reintroduced inside one row.
45
+ //
46
+ // ⚠ Shared is no longer `import type` only here: `GENERIC_REVIEWER_LABELS` is a VALUE import.
47
+ // That is safe now — build-release.mjs compiles the package, vendors it at release/dist/shared
48
+ // and rewrites the specifier to a relative path (CLAUDE.md § Packaging). The big login→kind
49
+ // tables in sync/bot-detection.ts are still hand-copied, for the drift-test reason stated there.
50
+ import { and, eq, inArray } from 'drizzle-orm';
51
+ import { GENERIC_REVIEWER_LABELS } from '../shared/index.js';
52
+ import { db, schema } from '../db/client.js';
53
+ import { automationVendorFor, matchesAutomatedLoginPattern, reviewBotKind, roleForBotLogin, } from './bot-detection.js';
54
+ const { workspaceReviewers } = schema;
55
+ // Fallback display labels for the vendor kinds. The frontend's BOT_VENDOR_META is the
56
+ // source of truth for rendering; this is what lands in the persisted `label` column.
57
+ //
58
+ // ⚠ TWO SIBLINGS, HAND-KEPT: `BOT_VENDOR_META` (apps/frontend/src/lib/ui.ts) and the Pro
59
+ // plugin's Slack `BOT_LABELS` (packages/pro/src/slack/report.ts). Change one branded label,
60
+ // change all three. The three UNBRANDED kinds are no longer restated here — `labelFor` reads
61
+ // them from `GENERIC_REVIEWER_LABELS` in shared, because those were exactly the keys the three
62
+ // copies had drifted apart on.
63
+ //
64
+ // ⚠ `Record<AutomatedReviewerKind, …>` minus the three generic kinds, so a vendor added to the
65
+ // union without a label here is a COMPILE error rather than a card rendering the raw slug
66
+ // (`deepsource_autofix`) at a user. `labelFor` handles the generics separately because they are
67
+ // deliberately NOT branded — they render by login.
68
+ const VENDOR_LABELS = {
69
+ coderabbit: 'CodeRabbit',
70
+ greptile: 'Greptile',
71
+ copilot: 'Copilot',
72
+ qodo: 'Qodo',
73
+ sourcery: 'Sourcery',
74
+ bito: 'Bito',
75
+ ellipsis: 'Ellipsis',
76
+ korbit: 'Korbit',
77
+ baz: 'Baz',
78
+ graphite: 'Graphite',
79
+ cursor: 'Cursor',
80
+ devin: 'Devin',
81
+ entelligence: 'Entelligence',
82
+ deepsource: 'DeepSource',
83
+ github_code_quality: 'GitHub Code Quality',
84
+ github_advanced_security: 'GitHub Advanced Security',
85
+ codex: 'Codex',
86
+ // Quality gates, scanners and CI
87
+ sonarqube: 'SonarQube',
88
+ codecov: 'Codecov',
89
+ codeclimate: 'Code Climate',
90
+ codefactor: 'CodeFactor',
91
+ hound: 'Hound',
92
+ coveralls: 'Coveralls',
93
+ codacy: 'Codacy',
94
+ github_actions: 'GitHub Actions',
95
+ jit: 'Jit',
96
+ socket: 'Socket',
97
+ gitguardian: 'GitGuardian',
98
+ semgrep: 'Semgrep',
99
+ trunk: 'Trunk',
100
+ endor: 'Endor Labs',
101
+ // Dependency & version bumps
102
+ dependabot: 'Dependabot',
103
+ renovate: 'Renovate',
104
+ snyk: 'Snyk',
105
+ pyup: 'PyUp',
106
+ greenkeeper: 'Greenkeeper',
107
+ depfu: 'Depfu',
108
+ aikido: 'Aikido',
109
+ mend: 'Mend',
110
+ frogbot: 'Frogbot',
111
+ checkmarx: 'Checkmarx',
112
+ // Code agents — automation that writes code
113
+ sweep: 'Sweep',
114
+ codegen: 'Codegen',
115
+ deepsource_autofix: 'DeepSource Autofix',
116
+ pre_commit_ci: 'pre-commit.ci',
117
+ restyled: 'Restyled',
118
+ imgbot: 'ImgBot',
119
+ transifex: 'Transifex',
120
+ crowdin: 'Crowdin',
121
+ mintlify: 'Mintlify',
122
+ allstar: 'Allstar',
123
+ step_security: 'StepSecurity',
124
+ orbisai: 'OrbisAI',
125
+ // Release & merge automation
126
+ mergify: 'Mergify',
127
+ kodiak: 'Kodiak',
128
+ bulldozer: 'Bulldozer',
129
+ release_please: 'Release Please',
130
+ semantic_release: 'semantic-release',
131
+ release_drafter: 'Release Drafter',
132
+ changesets: 'Changesets',
133
+ backport: 'Backport bot',
134
+ // Housekeeping — process, compliance, metadata
135
+ cla_assistant: 'CLA Assistant',
136
+ google_cla: 'Google CLA',
137
+ meta_cla: 'Meta CLA',
138
+ dco: 'DCO',
139
+ stale_bot: 'Stale bot',
140
+ welcome_bot: 'Welcome bot',
141
+ lock_bot: 'Lock bot',
142
+ allcontributors: 'All Contributors',
143
+ semantic_pr: 'Semantic PR',
144
+ sizebot: 'Size bot',
145
+ codesandbox: 'CodeSandbox',
146
+ netlify: 'Netlify',
147
+ vercel: 'Vercel',
148
+ gitpod: 'Gitpod',
149
+ };
150
+ // What to call each role in a human-readable `reasons` line. "…is a known Dependabot dependency
151
+ // bot" reads as a classification; the old "…is a known Dependabot review bot" read as a mistake,
152
+ // because it was one.
153
+ const ROLE_NOUN = {
154
+ review: 'review bot',
155
+ quality_check: 'quality check',
156
+ dependency: 'dependency bot',
157
+ code_agent: 'code agent',
158
+ release: 'release automation',
159
+ housekeeping: 'housekeeping bot',
160
+ };
161
+ // Exported so the query layer (db/queries.ts) labels analytics/dedup groupings from the
162
+ // same source of truth as the persisted classification `label`.
163
+ export function labelFor(kind) {
164
+ // The three unbranded kinds come from shared: 'pierre' composes the product name, and
165
+ // 'in_house' is "In-house / custom" rather than "In-house AI" for a reason recorded there.
166
+ // Both spellings used to be restated here, in the SPA and in the Slack digest, and the three
167
+ // had drifted.
168
+ if (kind === 'pierre' || kind === 'in_house' || kind === 'vendor') {
169
+ return GENERIC_REVIEWER_LABELS[kind];
170
+ }
171
+ return VENDOR_LABELS[kind] ?? kind;
172
+ }
173
+ function describeMarkers(fp) {
174
+ if (fp.markers.length === 0)
175
+ return ['matched an automated-review fingerprint'];
176
+ const tool = fp.tool ?? 'in-house';
177
+ return [`matched ${tool} fingerprint: ${fp.markers.join(', ')}`];
178
+ }
179
+ function behavioralVerdict(sig, allowlistMatch) {
180
+ // Require account-level evidence (≥3 reviews) before the behavioral band engages.
181
+ if (!sig || sig.reviews < 3)
182
+ return { medium: false, reasons: [] };
183
+ const reasons = [];
184
+ let score = 0;
185
+ if (sig.medianPushToReviewMins != null && sig.medianPushToReviewMins < 2) {
186
+ score++;
187
+ reasons.push('reviews land within ~2 min of a push');
188
+ }
189
+ if (sig.reviewsPerPr != null && sig.reviewsPerPr <= 1.2) {
190
+ score++;
191
+ reasons.push('~1 review per PR');
192
+ }
193
+ if (sig.replyRate != null && sig.replyRate < 0.05) {
194
+ score++;
195
+ reasons.push('almost never replies in threads');
196
+ }
197
+ if (sig.commentsPerReview != null && sig.commentsPerReview >= 3) {
198
+ score++;
199
+ reasons.push('many inline comments per review');
200
+ }
201
+ if (allowlistMatch) {
202
+ score++;
203
+ reasons.push('login matches a service-account pattern');
204
+ }
205
+ return { medium: score >= 3, reasons };
206
+ }
207
+ // NOTE: the old `rowToClassification(row, login)` helper is GONE and is NOT to be reinstated now
208
+ // that one row does hold both halves. It built a `ReviewerClassification` out of one row, which
209
+ // reads as harmless again — and is exactly how a caller starts treating a row as a single
210
+ // undifferentiated verdict, losing the two provenance flags that make the halves independent. A
211
+ // caller that wants both facts reads both columns and states which flag governs the one it uses;
212
+ // the query layer does exactly that.
213
+ // The DEFAULT role for a login nobody has classified by hand: whichever of the five automation
214
+ // vocabularies claims it, else 'review'.
215
+ //
216
+ // ⚠ THE `'review'` FALLBACK IS NOT A CLAIM THAT AN UNKNOWN BOT REVIEWS — it is the historical
217
+ // default, kept so an unrecognised vendor keeps its row in the ROI table where a user can see and
218
+ // reclassify it. Defaulting an unknown automation to a non-review role would make it VANISH from
219
+ // the one panel that exists to correct it. (`resolveActorLanes` makes the opposite choice for the
220
+ // same input, and the asymmetry is deliberate: it declines to CREDIT an unknown as a reviewer,
221
+ // which costs nothing, whereas hiding it here would cost the user the fix.)
222
+ //
223
+ // This is DERIVED, never carried in from the caller, and that is deliberate — see the landmine
224
+ // on persist() below.
225
+ export function defaultRoleFor(login) {
226
+ return roleForBotLogin(login) ?? 'review';
227
+ }
228
+ // Write ONE derived verdict to each of `workspaceIds`' rows. Returns the classification as it now
229
+ // applies (with the derived role).
230
+ //
231
+ // LANDMINE 1 — ONE STATEMENT PER WORKSPACE, WITH A `set:` NARROWED PER HALF. The loop is not an
232
+ // oversight to be batched away: an account has few workspaces, and two workspaces may legitimately
233
+ // hold DIFFERENT provenance flags for the same actor (manual "not a bot" in one, untouched auto in
234
+ // the next). A single batched insert carries ONE `set:` object and physically cannot express that.
235
+ //
236
+ // LANDMINE 2 — the gate is a READ-THEN-NARROW, not an `onConflictDoUpdate … WHERE`. drizzle
237
+ // spells the conditional-update clause differently per dialect, and `db` is typed as
238
+ // node-postgres, so a `setWhere` here would compile and then behave differently (or not at all)
239
+ // on SQLite. Reading the two provenance flags first and simply not naming the columns a human
240
+ // owns is dialect-free.
241
+ //
242
+ // LANDMINE 3 — `role` is DERIVED from the local quality-check list rather than round-tripped
243
+ // through the ReviewerClassification the caller built. Anything in the `set:` OVERWRITES the
244
+ // stored column on every auto pass, so copying a stale in-memory role would re-write migration
245
+ // 0045's role fold from a stale default and put SonarQube straight back into the review-bot
246
+ // metrics. Deriving keeps the write idempotent AND self-healing for a login added to the list
247
+ // later. (The parameter is role-LESS on purpose, so a future step in the resolution order
248
+ // physically cannot hand one in.)
249
+ //
250
+ // LANDMINE 4 — THE PRICE COLUMN IS NAMED NOWHERE IN THIS FILE: not in a `set:`, not as a derived
251
+ // INSERT value, not as a seed read off a sibling workspace. Price is PER WORKSPACE, so there is no
252
+ // sibling that could be authoritative; a row this pass creates simply has no price until someone
253
+ // sets one, and because the column is never in the `set:`, a conflict leaves a stored price
254
+ // untouched. `ReviewerClassification` carries no cost field either, which makes the omission
255
+ // structural rather than a rule to remember. `setReviewerCost` is the one writer.
256
+ //
257
+ // LANDMINE 5 — the conflict target is the 3-COLUMN tuple
258
+ // `workspace_reviewers_account_workspace_author`. A stale target raises "no unique or exclusion
259
+ // constraint matching the ON CONFLICT specification" at RUNTIME, in both dialects, only when a row
260
+ // is actually written; it type-checks perfectly.
261
+ async function persist(accountId, workspaceIds, c, opts = {}) {
262
+ const now = new Date();
263
+ const role = defaultRoleFor(c.login);
264
+ for (const workspaceId of workspaceIds) {
265
+ const existing = (await db
266
+ .select({
267
+ source: workspaceReviewers.source,
268
+ identitySource: workspaceReviewers.identitySource,
269
+ })
270
+ .from(workspaceReviewers)
271
+ .where(and(eq(workspaceReviewers.accountId, accountId), eq(workspaceReviewers.workspaceId, workspaceId), eq(workspaceReviewers.authorUserId, c.userId)))
272
+ .limit(1)
273
+ .execute())[0];
274
+ const set = { updatedAt: now };
275
+ if (opts.only !== 'identity' && existing?.source !== 'manual') {
276
+ set.automated = c.automated;
277
+ set.role = role;
278
+ set.confidence = c.confidence;
279
+ set.source = c.source;
280
+ set.reasonsJson = c.reasons;
281
+ }
282
+ if (opts.only !== 'judgement' && existing?.identitySource !== 'manual') {
283
+ set.kind = c.kind;
284
+ set.label = c.label;
285
+ }
286
+ // Nothing this pass is allowed to write — emit NO statement at all rather than an UPDATE that
287
+ // only bumps `updated_at`, which would make a skipped row look freshly re-derived.
288
+ if (Object.keys(set).length === 1)
289
+ continue;
290
+ await db
291
+ .insert(workspaceReviewers)
292
+ .values({
293
+ accountId,
294
+ workspaceId,
295
+ authorUserId: c.userId,
296
+ // The INSERT carries the FULL derived verdict regardless of `only`: `automated`,
297
+ // `confidence` and `source` are NOT NULL, so a brand-new row needs them, and a new row has
298
+ // no human opinion on either half to preserve. The price column is omitted ⇒ NULL.
299
+ // `identity_source` is omitted too, taking its 'auto' default — on the UPDATE path it can
300
+ // only be 'auto' already (a 'manual' identity never reaches the kind/label branch), so
301
+ // writing it would be noise that also reads as if this path may set provenance.
302
+ automated: c.automated,
303
+ role,
304
+ confidence: c.confidence,
305
+ source: c.source,
306
+ reasonsJson: c.reasons,
307
+ kind: c.kind,
308
+ label: c.label,
309
+ updatedAt: now,
310
+ })
311
+ .onConflictDoUpdate({
312
+ target: [
313
+ workspaceReviewers.accountId,
314
+ workspaceReviewers.workspaceId,
315
+ workspaceReviewers.authorUserId,
316
+ ],
317
+ set,
318
+ })
319
+ .execute();
320
+ }
321
+ return { ...c, role };
322
+ }
323
+ // Persist the "this is an ordinary human" judgement for an actor that only ever left issue
324
+ // comments — the population `classifyReviewer` is deliberately not run for (there are no reviews
325
+ // to score, so every signal it could read is absent).
326
+ //
327
+ // It exists because THE ROW IS THE BOT OBJECT: a (workspace, actor) pair with no row cannot be
328
+ // listed, and therefore cannot be corrected by hand either. Writing a low-confidence non-manual
329
+ // row makes the pair visible and leaves it fully re-derivable — the moment that person submits a
330
+ // real review, the next classification pass overwrites this.
331
+ //
332
+ // ⚠ TWO PROPERTIES THAT WERE EMERGENT UNDER TWO TABLES AND ARE NOW REQUIREMENTS:
333
+ // (a) the values object contains NO `kind`/`label` AT ALL, so a human judgement can never rename
334
+ // the vendor as a side effect. Under the merged row that omission is the ONLY thing stopping
335
+ // it — there is no longer a table boundary doing the job. (A new row therefore comes up with
336
+ // kind/label NULL, which is correct: a human has no vendor kind, and stamping one is a
337
+ // claim.) The price column is likewise absent, so a price already recorded survives.
338
+ // (b) it still narrows to the workspaces no human owns (`source = 'manual'`) by READING them
339
+ // first, not by a `setWhere` — drizzle spells that clause differently per dialect while `db`
340
+ // is pg-typed.
341
+ // Because the shared values object here names ONLY judgement columns and the manual rows are
342
+ // already filtered out, one batched statement is safe: every writable row takes identical values.
343
+ export async function persistHumanJudgement(accountId, workspaceIds, userId, login) {
344
+ if (workspaceIds.length === 0)
345
+ return;
346
+ const manualRows = await db
347
+ .select({ workspaceId: workspaceReviewers.workspaceId })
348
+ .from(workspaceReviewers)
349
+ .where(and(eq(workspaceReviewers.accountId, accountId), eq(workspaceReviewers.authorUserId, userId), eq(workspaceReviewers.source, 'manual'), inArray(workspaceReviewers.workspaceId, workspaceIds)))
350
+ .execute();
351
+ const manual = new Set(manualRows.map((r) => r.workspaceId));
352
+ const writable = workspaceIds.filter((id) => !manual.has(id));
353
+ if (writable.length === 0)
354
+ return;
355
+ const values = {
356
+ automated: false,
357
+ role: defaultRoleFor(login),
358
+ confidence: 'low',
359
+ source: 'fingerprint',
360
+ reasonsJson: ['commented on PRs but has not submitted a review'],
361
+ updatedAt: new Date(),
362
+ };
363
+ await db
364
+ .insert(workspaceReviewers)
365
+ .values(writable.map((workspaceId) => ({ accountId, workspaceId, authorUserId: userId, ...values })))
366
+ .onConflictDoUpdate({
367
+ target: [
368
+ workspaceReviewers.accountId,
369
+ workspaceReviewers.workspaceId,
370
+ workspaceReviewers.authorUserId,
371
+ ],
372
+ set: values,
373
+ })
374
+ .execute();
375
+ }
376
+ // `workspaceIds` is the set of WORKSPACE ROWS this verdict should be written to. It is REQUIRED
377
+ // and positional so no caller can "just classify" without saying where the answer lands: the row
378
+ // IS the bot object, and a verdict with no row is a verdict nobody can see or correct.
379
+ //
380
+ // ⚠ AN EMPTY LIST IS A NO-OP WITH NO CALLER, NOT A MECHANISM. It used to be the way to write the
381
+ // identity half alone (two statements, two tables, only the judgement gated on the list being
382
+ // non-empty). Under one row and one per-workspace loop it writes NOTHING, so the two reset paths
383
+ // pass a real workspace id plus `opts.only` instead — see PersistOpts.
384
+ //
385
+ // THERE IS NO "MANUAL OVERRIDE WINS" EARLY RETURN, and its absence is the point. A manual flag is
386
+ // per WORKSPACE and per HALF, so the derivation always runs and `persist` declines exactly the
387
+ // columns a human owns — which is what lets a manual "not a bot" in one workspace coexist with a
388
+ // fresh auto verdict in the next, and a manual vendor name coexist with a re-derived judgement in
389
+ // the same row. The value this function RETURNS is therefore the DERIVED verdict, not necessarily
390
+ // what is stored for any given workspace; read the row back if you need that.
391
+ export async function classifyReviewer(accountId, user, evidence, workspaceIds, opts = {}) {
392
+ const login = user.githubLogin;
393
+ const fp = evidence.fingerprint;
394
+ // 1. Known vendor login — an AI REVIEWER.
395
+ const vendor = reviewBotKind(login);
396
+ if (vendor) {
397
+ return persist(accountId, workspaceIds, {
398
+ userId: user.id,
399
+ login,
400
+ automated: true,
401
+ kind: vendor,
402
+ label: labelFor(vendor),
403
+ confidence: 'high',
404
+ source: 'vendor_login',
405
+ reasons: [`login "${login}" is a known ${labelFor(vendor)} review bot`],
406
+ }, opts);
407
+ }
408
+ // 1b. Known vendor login — any OTHER automation family (quality gate, dependency bot, code
409
+ // agent, release or housekeeping automation).
410
+ //
411
+ // ⚠ THIS STEP MUST SIT ABOVE STEP 2, and its absence is why every non-reviewer integration used
412
+ // to render as "In-house AI". Step 2 fires on `githubType === 'Bot'`, which is true of all of
413
+ // these, and assigns `fp?.tool ?? 'in_house'` — so SonarQube, Dependabot, github-actions,
414
+ // GitGuardian, Socket, Google CLA and Jit all landed in the bucket literally labelled with
415
+ // another vendor's job description (25 of 37 such rows on the dev corpus). The user could not
416
+ // tell them apart on the one screen that exists to classify them.
417
+ //
418
+ // The `reasons` string says what the actor DOES rather than calling it a review bot — the
419
+ // difference between a classification a user trusts and one they immediately correct.
420
+ const automation = automationVendorFor(login);
421
+ if (automation) {
422
+ return persist(accountId, workspaceIds, {
423
+ userId: user.id,
424
+ login,
425
+ automated: true,
426
+ kind: automation.kind,
427
+ label: labelFor(automation.kind),
428
+ confidence: 'high',
429
+ source: 'vendor_login',
430
+ reasons: [
431
+ `login "${login}" is a known ${labelFor(automation.kind)} ` +
432
+ `${ROLE_NOUN[automation.role]}`,
433
+ ],
434
+ }, opts);
435
+ }
436
+ // 2. GitHub App / Bot typename, or opt-in app attribution.
437
+ if (user.githubType === 'Bot' || evidence.appAttributed) {
438
+ const kind = fp?.tool ?? 'in_house';
439
+ const isBotType = user.githubType === 'Bot';
440
+ const source = isBotType ? 'github_type' : 'app_attribution';
441
+ const reasons = [
442
+ isBotType ? 'GitHub reports this account as a Bot' : 'posted via a GitHub App',
443
+ ];
444
+ if (fp?.marked)
445
+ reasons.push(...describeMarkers(fp));
446
+ return persist(accountId, workspaceIds, {
447
+ userId: user.id,
448
+ login,
449
+ automated: true,
450
+ kind,
451
+ label: kind === 'in_house' ? login : labelFor(kind),
452
+ confidence: 'high',
453
+ source,
454
+ reasons,
455
+ }, opts);
456
+ }
457
+ // 3. Branded/marked fingerprint.
458
+ if (fp?.marked) {
459
+ const kind = fp.tool ?? 'in_house';
460
+ return persist(accountId, workspaceIds, {
461
+ userId: user.id,
462
+ login,
463
+ automated: true,
464
+ kind,
465
+ label: labelFor(kind),
466
+ confidence: 'high',
467
+ source: 'fingerprint',
468
+ reasons: describeMarkers(fp),
469
+ }, opts);
470
+ }
471
+ // 4. Behavioral band (MEDIUM — never auto-badges; the UI asks "confirm?"). This is the LAST
472
+ // step: the opt-in Haiku tie-break that used to follow it is gone (see the file header).
473
+ const allowlistMatch = matchesAutomatedLoginPattern(login);
474
+ const beh = behavioralVerdict(evidence.behavioral, allowlistMatch);
475
+ if (beh.medium) {
476
+ return persist(accountId, workspaceIds, {
477
+ userId: user.id,
478
+ login,
479
+ automated: true,
480
+ kind: 'in_house',
481
+ label: allowlistMatch ? login : 'In-house / custom',
482
+ confidence: 'medium',
483
+ source: 'behavioral',
484
+ reasons: beh.reasons,
485
+ }, opts);
486
+ }
487
+ // Otherwise → human. Strong (HIGH) when we had enough behavioral evidence to look and
488
+ // it didn't trip; LOW when there was nothing to go on.
489
+ const hadEvidence = (evidence.behavioral?.reviews ?? 0) >= 3;
490
+ return persist(accountId, workspaceIds, {
491
+ userId: user.id,
492
+ login,
493
+ automated: false,
494
+ kind: null,
495
+ label: login,
496
+ confidence: hadEvidence ? 'high' : 'low',
497
+ source: evidence.behavioral ? 'behavioral' : 'fingerprint',
498
+ reasons: hadEvidence
499
+ ? ['no automation markers; reviewing behaviour looks human']
500
+ : ['no automated-reviewer signals'],
501
+ }, opts);
502
+ }
503
+ //# sourceMappingURL=reviewer-classify.js.map
@@ -0,0 +1,97 @@
1
+ import { and, eq, inArray, isNull } from 'drizzle-orm';
2
+ import { getAccessToken } from '../auth/account.js';
3
+ import { db, schema } from '../db/client.js';
4
+ import { fetchPrFilesWithPatch } from '../github/mutations.js';
5
+ const { pullRequests, repos } = schema;
6
+ // Best-effort, BOUNDED backfill of `pull_requests.files` for reviewer-routing
7
+ // candidates that predate the files column (old open PRs never re-synced). Only PRs
8
+ // whose `files` IS NULL are fetched; the result — even `[]` on failure — is persisted
9
+ // so the same PR isn't refetched on every insights refresh (an [] sentinel means "we
10
+ // tried"). The sprint window's PRs already carry files from sync, so this only ever
11
+ // touches a handful of stale orphans. Returns the resolved paths by prId.
12
+ export async function ensureRoutingPrFiles(accountId, prIds) {
13
+ const out = new Map();
14
+ if (prIds.length === 0)
15
+ return out;
16
+ const rows = await db
17
+ .select({
18
+ id: pullRequests.id,
19
+ number: pullRequests.number,
20
+ owner: repos.owner,
21
+ name: repos.name,
22
+ })
23
+ .from(pullRequests)
24
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
25
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.id, prIds), isNull(pullRequests.files)))
26
+ .execute();
27
+ if (rows.length === 0)
28
+ return out;
29
+ let token;
30
+ try {
31
+ token = await getAccessToken(accountId);
32
+ }
33
+ catch {
34
+ return out; // no token (e.g. offline) → leave the fallback rationale in place
35
+ }
36
+ for (const r of rows) {
37
+ let stored = [];
38
+ try {
39
+ const { files } = await fetchPrFilesWithPatch(token, r.owner, r.name, r.number, 100);
40
+ stored = files.map((f) => ({
41
+ path: f.filename,
42
+ additions: f.additions,
43
+ deletions: f.deletions,
44
+ }));
45
+ }
46
+ catch {
47
+ stored = []; // sentinel — persist [] so we don't refetch this PR every refresh
48
+ }
49
+ await db
50
+ .update(pullRequests)
51
+ .set({ files: stored })
52
+ .where(and(eq(pullRequests.id, r.id), eq(pullRequests.accountId, accountId)))
53
+ .execute();
54
+ out.set(r.id, stored.map((s) => s.path));
55
+ }
56
+ return out;
57
+ }
58
+ /** How many missing file lists one repo may backfill in a single sync. */
59
+ const FILES_BACKFILL_PER_RUN = 25;
60
+ /**
61
+ * Backfill `pull_requests.files` for this repo's OPEN pull requests that have none.
62
+ *
63
+ * ---- WHY THIS EXISTS SEPARATELY FROM THE SYNC ITSELF ------------------------
64
+ *
65
+ * The walk stores `files` for the pull requests it touches, but a repo carries older open ones it
66
+ * never revisits — and a pull request with no stored file list has NO BLAST RADIUS AT ALL:
67
+ * `codeLocFor` returns null, `blastSignalsFor` returns null, and every surface correctly renders
68
+ * nothing. Measured on a real corpus: **154 of 1,564 open pull requests, 9.8%**, showed no chip
69
+ * for this reason alone.
70
+ *
71
+ * ⚠ THIS IS A DIFFERENT FETCH FROM THE CHANGE-SHAPE READ, AND IT IS THE CHEAPER, MORE VALUABLE
72
+ * ONE. `sync/classify-change-shape.ts` reads a DIFF to refine a level that already exists; this
73
+ * fetches the FILE LIST so a level can exist at all. Fetching the diff for these pull requests
74
+ * would not help — with no `files` there is nothing for the diff to refine.
75
+ *
76
+ * Bounded exactly like its sibling: capped per run, once per pull request (the `[]`-on-failure
77
+ * sentinel `ensureRoutingPrFiles` already writes means a pull request GitHub will not serve is
78
+ * never retried), and strictly non-fatal.
79
+ */
80
+ export async function backfillMissingPrFiles(accountId, repoId, log) {
81
+ const rows = await db
82
+ .select({ id: pullRequests.id })
83
+ .from(pullRequests)
84
+ .where(and(eq(pullRequests.accountId, accountId), eq(pullRequests.repoId, repoId), eq(pullRequests.state, 'open'),
85
+ // ⚠ `IS NULL` only. A row already holding `[]` is the "we tried and GitHub refused"
86
+ // sentinel; re-querying it every sync forever is the cost this predicate avoids.
87
+ isNull(pullRequests.files)))
88
+ .limit(FILES_BACKFILL_PER_RUN)
89
+ .execute();
90
+ if (rows.length === 0)
91
+ return 0;
92
+ const resolved = await ensureRoutingPrFiles(accountId, rows.map((r) => r.id));
93
+ if (resolved.size > 0)
94
+ log?.info(`files backfill: filled ${resolved.size} pull request(s)`);
95
+ return resolved.size;
96
+ }
97
+ //# sourceMappingURL=routing-files.js.map
@@ -0,0 +1,12 @@
1
+ const jobs = [];
2
+ export function registerScheduledJob(cron, handler, label = 'pro-job') {
3
+ jobs.push({ cron, label, handler });
4
+ }
5
+ export function getScheduledJobs() {
6
+ return jobs;
7
+ }
8
+ // Test/reset hook — registration is process-lifetime, but tests may want a clean slate.
9
+ export function clearScheduledJobs() {
10
+ jobs.length = 0;
11
+ }
12
+ //# sourceMappingURL=scheduled-jobs.js.map