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,213 @@
1
+ // WHO DID THE WORK — the seven lanes, resolved per user id for one workspace.
2
+ //
3
+ // The period report's twelve metrics answer "what happened"; this answers "how much of it was a
4
+ // person". Both are needed, because on a real workspace the blend is severe enough to make the
5
+ // headline figures describe nobody:
6
+ //
7
+ // BNG, one fortnight — 117 merged PRs, of which 46 were Dependabot.
8
+ // median PR size: Dependabot 14 lines · humans 142 lines · REPORTED 68
9
+ //
10
+ // That 68 is not a compromise between two numbers, it is a number no pull request in that
11
+ // workspace resembled, and it understated real human PR size by 2.1×.
12
+ //
13
+ // ── WHY LANES AND NOT "BOT vs HUMAN" ─────────────────────────────────────────────────────────
14
+ //
15
+ // Because automation contaminates DIFFERENT metrics depending on what it does, and one bucket
16
+ // cannot tell those apart:
17
+ //
18
+ // • a DEPENDENCY bot authors bumps → distorts throughput, lead time, PR size
19
+ // • a CODE AGENT authors real changes → distorts the same metrics and means the OPPOSITE
20
+ // • a QUALITY GATE responds/approves → distorts review counts and approvals
21
+ // • an AI REVIEWER writes findings → the only automation whose review volume means anything
22
+ // • RELEASE automation merges/tags → its merges are governance events, not work
23
+ // • HOUSEKEEPING greets and labels → pure noise in every one of the above
24
+ //
25
+ // "Your throughput is inflated by bumps" and "your approvals are automated" are different
26
+ // problems with different fixes, and a single `isBot` flag can state neither. The same workspace
27
+ // showed SonarQube posting 786 comments — every one of them a "Quality Gate Passed/Failed" badge
28
+ // — so folding its volume in with an AI reviewer's findings would report 786 pieces of feedback
29
+ // where there were none.
30
+ //
31
+ // The dependency/code-agent split is the one that most resists being collapsed back: both author
32
+ // pull requests, so any "bot vs human" view puts them together, and yet a merged Dependabot bump
33
+ // is overhead a team absorbed while a merged agent PR is work it shipped. A single "automation
34
+ // authored 40% of merges" figure that mixes them tells the reader nothing they can act on.
35
+ //
36
+ // ── THE DUPLICATE-IDENTITY DEFENCE ───────────────────────────────────────────────────────────
37
+ //
38
+ // Real accounts carry the SAME actor as two user rows with CONFLICTING flags: `dependabot` and
39
+ // `dependabot[bot]`, `github-actions` and `github-actions[bot]`. On the measured account one of
40
+ // each pair sat at `workspace_reviewers.automated = 0`, i.e. counted as a human. Merging the rows
41
+ // is not an option — they have different GitHub node ids and may be genuinely different accounts
42
+ // (an App vs a user of the same name) — so this resolver never trusts a single signal:
43
+ //
44
+ // automated ⇐ workspace verdict ∪ users.isBot ∪ the login vocabularies
45
+ // human ⇐ only when NO signal fires, or a human explicitly said so
46
+ //
47
+ // A genuine manual "this is a person" still wins (that rule is load-bearing elsewhere and is not
48
+ // weakened here); what does not win is the mere ABSENCE of an automated verdict.
49
+ import { inArray } from 'drizzle-orm';
50
+ import { db, schema } from './client.js';
51
+ import { roleForBotLogin } from '../sync/bot-detection.js';
52
+ import { automatedReviewerUserIds, classificationKindForUser, manualHumanUserIds, manualRoleUserIds, reviewerRoleForUser, } from './queries.js';
53
+ const { users } = schema;
54
+ /** The vendor `kind`s that denote SUBSTANTIVE AI review, as opposed to a scanner wearing a vendor
55
+ * brand. Anything classified with one of these is in the `ai_review` lane even if its login is
56
+ * unknown to the local vocabulary — a user's manual vendor classification is a real judgement. */
57
+ const AI_REVIEW_KINDS = new Set([
58
+ 'coderabbit',
59
+ 'greptile',
60
+ 'copilot',
61
+ 'qodo',
62
+ 'sourcery',
63
+ 'bito',
64
+ 'ellipsis',
65
+ 'korbit',
66
+ 'baz',
67
+ 'graphite',
68
+ 'cursor',
69
+ 'devin',
70
+ 'entelligence',
71
+ 'codex',
72
+ // `vendor` = a generic proprietary reviewer a HUMAN classified when it wasn't a known brand.
73
+ // That is a real judgement that the actor reviews, so it belongs here.
74
+ 'vendor',
75
+ ]);
76
+ // ⚠ `in_house` IS NOT IN THAT SET, and the shared type's description ("the account's OWN AI")
77
+ // is not what the column actually holds. Measured on the live database: of 37 rows carrying
78
+ // `kind: 'in_house'`, 25 were assigned by `source: 'github_type'` — the fallback for "this is a
79
+ // GitHub App and we don't recognise the brand". That bucket contains sonarqubecloud,
80
+ // dependabot[bot], github-actions[bot], gitguardian, socket-security, google-cla and jit-ci.
81
+ // Not one of them is an AI reviewer.
82
+ //
83
+ // Treating it as one produced the exact failure this module exists to prevent: `github-actions`
84
+ // landed in `quality_gate` while `github-actions[bot]` — the SAME actor, second user row, the one
85
+ // carrying the `in_house` classification — landed in `ai_review`. One CI bot, two lanes, both
86
+ // under-counted, and the "is our AI review tooling earning its licence" number quietly inflated
87
+ // by 384 automated approvals.
88
+ //
89
+ // So an unbranded automation falls through to the `quality_gate` default. That is the asymmetric
90
+ // choice stated in `resolveActorLanes`: under-claiming an AI reviewer costs nothing anyone acts
91
+ // on, over-claiming one corrupts the single number a team would use to cancel a subscription.
92
+ // MIRRORED from `REVIEWER_ROLE_LANE` in @pierre-review/shared — the backend cannot import shared
93
+ // at RUNTIME (a value import fails the release build), so the map is spelled twice and
94
+ // `actor-lanes.test.ts` asserts the two are identical. It is deliberately 1:1: a user who picks
95
+ // "Release automation" and finds the actor filed under "Quality gate" has been told their choice
96
+ // did not take.
97
+ const ROLE_LANE = {
98
+ review: 'ai_review',
99
+ quality_check: 'quality_gate',
100
+ dependency: 'dependency',
101
+ code_agent: 'code_agent',
102
+ release: 'release',
103
+ housekeeping: 'housekeeping',
104
+ };
105
+ /**
106
+ * Resolve the lane of every actor that could appear in a workspace's metrics.
107
+ *
108
+ * ORDER IS THE CONTRACT, because the categories genuinely overlap — `github-advanced-security` is
109
+ * a scanner with a vendor brand, SonarQube is a quality check that some accounts have classified
110
+ * by hand, and Copilot both reviews and (elsewhere) authors. Earlier rules win:
111
+ *
112
+ * 1. manual "this is a person" → human (nothing overrules it)
113
+ * 2. no automation signal at all → human
114
+ * 3. MANUAL role → that role's lane
115
+ * 4. known login vocabulary → that role's lane
116
+ * 5. stored role other than 'review' → that role's lane (a seed we derived)
117
+ * 6. vendor kind in AI_REVIEW_KINDS → ai_review
118
+ * 7. anything else automated → quality_gate (CI scripts, unknown apps)
119
+ *
120
+ * ⚠ RULES 3 AND 5 ARE THE SAME COLUMN READ TWICE, AND SPLITTING THEM IS THE POINT. `role` defaults
121
+ * to `'review'`, so a stored `'review'` is ambiguous: it means "a person chose Review bot" on a
122
+ * manual row and "we have never heard of this login" on every other. Collapsing the two either
123
+ * ignores the user's choice (mark Copilot a code agent, watch the report keep calling it AI
124
+ * review) or lets a stale default beat a login we positively recognise. `manualRoleUserIds`
125
+ * carries the disambiguation, and it is automated-rows-only so a manual HUMAN cannot be handed a
126
+ * bot lane by whatever role their row happens to carry.
127
+ *
128
+ * Rule 7 is deliberately NOT `ai_review`, and it is also NOT `housekeeping`: an unrecognised
129
+ * automation is far more likely to be a CI script than a reviewer, and the cost of the mistakes is
130
+ * asymmetric. Miscounting a script as a quality gate understates nothing anyone acts on;
131
+ * miscounting it as an AI reviewer inflates the one number a team would use to judge whether their
132
+ * review tooling is earning its licence, and filing it under housekeeping would quietly drop it
133
+ * out of every count instead of merely declining to credit it.
134
+ */
135
+ export async function resolveActorLanes(accountId, scope) {
136
+ const [automatedFromWorkspace, kindMap, roleMap, vouchedHuman, manualRoles] = await Promise.all([
137
+ automatedReviewerUserIds(accountId, scope.workspaceId, 'all'),
138
+ classificationKindForUser(accountId, scope.workspaceId),
139
+ reviewerRoleForUser(accountId, scope.workspaceId),
140
+ manualHumanUserIds(accountId, scope.workspaceId),
141
+ manualRoleUserIds(accountId, scope.workspaceId),
142
+ ]);
143
+ // ⚠ THE ONE SIGNAL THAT BEATS THE UNION. Widening automation detection to `users.isBot` and the
144
+ // login vocabularies is what fixes the duplicated identities — and it also re-admits an actor a
145
+ // person has explicitly marked human, because such an actor usually has a bot-ish login or a
146
+ // stale global flag, which is exactly WHY someone corrected it by hand.
147
+ const vouched = new Set(vouchedHuman);
148
+ // The GLOBAL bot flag, which catches exactly what the workspace verdict misses: an actor nobody
149
+ // has opened the Bots tab for, and the second row of a duplicated identity. `users` is a global
150
+ // table, so this is read by id — never handed to a tenant.
151
+ const flaggedBotIds = new Set();
152
+ const loginById = new Map();
153
+ const candidateIds = new Set([...automatedFromWorkspace, ...kindMap.keys(), ...roleMap.keys()]);
154
+ const botRows = await db
155
+ .select({ id: users.id, login: users.githubLogin, isBot: users.isBot })
156
+ .from(users)
157
+ .where(inArray(users.isBot, [true]))
158
+ .execute();
159
+ for (const r of botRows) {
160
+ flaggedBotIds.add(r.id);
161
+ loginById.set(r.id, r.login);
162
+ candidateIds.add(r.id);
163
+ }
164
+ // Logins for the workspace-classified ids the global flag did not cover.
165
+ const missing = [...candidateIds].filter((id) => !loginById.has(id));
166
+ if (missing.length > 0) {
167
+ const rows = await db
168
+ .select({ id: users.id, login: users.githubLogin })
169
+ .from(users)
170
+ .where(inArray(users.id, missing))
171
+ .execute();
172
+ for (const r of rows)
173
+ loginById.set(r.id, r.login);
174
+ }
175
+ const workspaceAutomated = new Set(automatedFromWorkspace);
176
+ const lane = new Map();
177
+ const automatedIds = new Set();
178
+ for (const id of candidateIds) {
179
+ if (vouched.has(id))
180
+ continue; // a person said this is a person — nothing overrules that
181
+ const login = loginById.get(id) ?? '';
182
+ // ONE call, not five predicates in an order this file gets to choose: the vocabularies are
183
+ // asserted pairwise disjoint, so `roleForBotLogin` has exactly one answer per login.
184
+ const loginRole = roleForBotLogin(login);
185
+ // THE UNION (see the header). A login vocabulary hit counts on its own, which is what covers
186
+ // the duplicated-identity row whose stored verdict says `automated: 0`.
187
+ const automated = workspaceAutomated.has(id) || flaggedBotIds.has(id) || loginRole != null;
188
+ if (!automated)
189
+ continue;
190
+ automatedIds.add(id);
191
+ const manualRole = manualRoles.get(id);
192
+ const storedRole = roleMap.get(id);
193
+ const kind = kindMap.get(id);
194
+ if (manualRole != null)
195
+ lane.set(id, ROLE_LANE[manualRole]);
196
+ else if (loginRole != null)
197
+ lane.set(id, ROLE_LANE[loginRole]);
198
+ // A DERIVED `'review'` is the default an unknown login gets, so it is not evidence and does
199
+ // not short-circuit; every other derived role was positively concluded and does.
200
+ else if (storedRole != null && storedRole !== 'review')
201
+ lane.set(id, ROLE_LANE[storedRole]);
202
+ else if (kind != null && AI_REVIEW_KINDS.has(kind))
203
+ lane.set(id, 'ai_review');
204
+ else
205
+ lane.set(id, 'quality_gate');
206
+ }
207
+ return {
208
+ lane,
209
+ automatedIds,
210
+ laneOf: (userId) => (userId == null ? 'human' : (lane.get(userId) ?? 'human')),
211
+ };
212
+ }
213
+ //# sourceMappingURL=actor-lanes.js.map
@@ -0,0 +1,97 @@
1
+ // ── THE GLOBAL AUTOMATION READERS (CORE) ─────────────────────────────────────────────────────
2
+ //
3
+ // Two account-free signals that an actor is automation, beside `users.isBot` and the review-bot
4
+ // login table (queries.ts `botUserIds` / `reviewBotUserIds`):
5
+ //
6
+ // • GitHub TYPES the account a Bot (`users.github_type = 'Bot'`) — the App accounts `isLikelyBot`
7
+ // misses because GraphQL returns their login WITHOUT `[bot]`. Measured on the dev DB: seven such
8
+ // users with `is_bot = 0` (Copilot, lumberbot-app, gitguardian, socket-security, google-cla,
9
+ // cdp-github-action, jit-ci), who wrote 112 PR comments on open PRs.
10
+ // • the login is a known NON-review vendor (`AUTOMATION_VENDORS`, bare or `[bot]`, plus the
11
+ // per-org prefixes such as Semgrep's `semgrep-code-<org>`) — Renovate, Snyk, ImgBot…
12
+ //
13
+ // A separate module, not private functions in queries.ts, because My Turn's mention scanner
14
+ // (`db/pr-mentions.ts`, which imports nothing but `client.js`) builds its automation set from these
15
+ // too; reaching them through queries.ts would drag that whole module (and its cycle with
16
+ // ml-labels.ts) into it.
17
+ //
18
+ // ⚠ IDS, used only as MEMBERSHIP tests. `users` is a GLOBAL table: nothing here may hand a tenant
19
+ // a row, and a caller that needs a workspace's answer folds these through
20
+ // `resolveWorkspaceReviewers`, where a person's manual "this is a human" still wins.
21
+ // ⚠ ONE reader returns a LOGIN, and it matches EXACT vendor logins only — a public identity, like
22
+ // the REVIEW_BOTS seed. The PREFIX half (`semgrep-code-<org>`) matches logins that name ANOTHER
23
+ // tenant's organisation, so it only ever yields ids. A new reader here returns ids.
24
+ import { eq, inArray, like, or, sql } from 'drizzle-orm';
25
+ import { db, schema } from './client.js';
26
+ import { automationVendorLoginsForSql, automationVendorPrefixes } from '../sync/bot-detection.js';
27
+ const { users } = schema;
28
+ /** Users GitHub itself types as a Bot (`users.github_type = 'Bot'`) — the App accounts
29
+ * `isLikelyBot` misses because GraphQL returns their login WITHOUT `[bot]` (socket-security,
30
+ * gitguardian, jit-ci, Copilot, google-cla, lumberbot-app, cdp-github-action on the dev DB). */
31
+ export async function githubTypeBotUserIds() {
32
+ const rows = await db
33
+ .select({ id: users.id })
34
+ .from(users)
35
+ .where(eq(users.githubType, 'Bot'))
36
+ .execute();
37
+ return rows.map((r) => r.id);
38
+ }
39
+ /** `lower(github_login)` — built per call, never at import: tests that mock `client.js` with a
40
+ * partial schema import this module too. */
41
+ const loweredLogin = () => sql `lower(${users.githubLogin})`;
42
+ /** Users whose login is in AUTOMATION_VENDORS (bare or `[bot]`) or matches a vendor PREFIX:
43
+ * `lower(github_login) IN (…) OR lower(github_login) LIKE 'semgrep-code-%' OR … 'semgrepcode-%'`.
44
+ * ⚠ Lowercased on BOTH sides, so pg's case-sensitive `LIKE` and SQLite's case-blind one agree;
45
+ * no prefix contains `_`, LIKE's single-character wildcard. */
46
+ export async function automationVendorUserIds() {
47
+ const rows = await db
48
+ .select({ id: users.id })
49
+ .from(users)
50
+ .where(or(inArray(loweredLogin(), automationVendorLoginsForSql()), ...automationVendorPrefixes().map((p) => like(loweredLogin(), `${p}%`))))
51
+ .execute();
52
+ return rows.map((r) => r.id);
53
+ }
54
+ /**
55
+ * THE BALL RULE'S AUTOMATION SET — every actor we know to be automation WITHOUT a workspace:
56
+ * `users.isBot` ∪ `users.github_type = 'Bot'` ∪ the AUTOMATION_VENDORS logins and prefixes. GLOBAL,
57
+ * so the unscoped `getMyTurn` (browser notifications, the CLI) and the scoped one (the board, the
58
+ * brief) agree, and the mention scanner (db/pr-mentions.ts) honours exactly the mentions the ball
59
+ * rule would.
60
+ *
61
+ * WHY NOT `users.isBot` ALONE: My Turn's newer rules read OTHER people's comments ("a person
62
+ * commented after you", "a person replied to you", "a person @-mentioned you"), and on the dev DB
63
+ * the accounts GitHub types as a Bot but `isBot` misses wrote 112 PR comments on 82 open PRs
64
+ * (google-cla 73, socket-security 24, cdp-github-action 12, gitguardian 2, jit-ci 1). Under the
65
+ * `isBot`-only set each of those summons the viewer as though a colleague had answered them. The
66
+ * Pending cards already call the same accounts automation.
67
+ *
68
+ * ⚠ STILL NOT `hiddenBotUserIds`: that union needs a workspaceId, and `getMyTurn` also runs
69
+ * unscoped. A workspace's manual "this is a human" therefore cannot reach this set — accepted,
70
+ * because every member here is an account GitHub types as an App or a known vendor login.
71
+ * ⚠ `users.isBot` keeps its own meaning; this set is a union READ, never a write.
72
+ */
73
+ export async function globalAutomationUserIds() {
74
+ const [flagged, typed, vendors] = await Promise.all([
75
+ db
76
+ .select({ id: users.id })
77
+ .from(users)
78
+ .where(eq(users.isBot, true))
79
+ .execute()
80
+ .then((rows) => rows.map((r) => r.id)),
81
+ githubTypeBotUserIds(),
82
+ automationVendorUserIds(),
83
+ ]);
84
+ return new Set([...flagged, ...typed, ...vendors]);
85
+ }
86
+ /** The users whose login is EXACTLY a vendor login (bare or `[bot]`), with that login — the kind
87
+ * seed in queries.ts (`classificationKindForUser`) names each one's vendor from it. ⚠ NEVER the
88
+ * prefix half: that kind map is also the bot drill-downs' "is this id classified here" gate, which
89
+ * then resolves the login, so a prefix match would read another org's name back to any tenant. */
90
+ export async function exactAutomationVendorUsers() {
91
+ return db
92
+ .select({ id: users.id, login: users.githubLogin })
93
+ .from(users)
94
+ .where(inArray(loweredLogin(), automationVendorLoginsForSql()))
95
+ .execute();
96
+ }
97
+ //# sourceMappingURL=automation-ids.js.map
@@ -0,0 +1,349 @@
1
+ // ── THE AUTHORING-AUTOMATION OUTPUT VECTOR (CORE, deterministic) ─────────────────────────────
2
+ //
3
+ // What a bot that WRITES PRs produced in one period, in one workspace. The sibling of
4
+ // db/person-period.ts, and it copies that file's discipline wholesale (window-pure two-sided
5
+ // predicates, computed on read, no table, no migration) — but it is deliberately NOT the same
6
+ // vector, because the question is different. A person's numbers answer "how is this teammate
7
+ // doing"; an automation's answer "what is this thing costing us", which is why
8
+ // `prs_merged_without_human_review` and `human_review_comments_received` are here and response
9
+ // times are not.
10
+ //
11
+ // WHY IT EXISTS: the People report's bot sections read `getBotAnalytics`, whose columns are all
12
+ // REVIEW output (threads, comments, acted-on). That is the whole story for the `review` and
13
+ // `quality_check` roles only. For `dependency` / `code_agent` / `release` / `housekeeping`
14
+ // automation every one of those columns is legitimately zero, and a wall of zeros reads as "it
15
+ // did nothing" when the truth is "it did a different thing". The section used to say so in prose
16
+ // and chart nothing; this fold is what it charts instead.
17
+ //
18
+ // ⚠ EVERY WINDOWED METRIC IS WINDOW-PURE with a TWO-SIDED `>= from AND < to` predicate, so a
19
+ // stored/forwarded period stays reproducible. There is deliberately no live-basis escape hatch
20
+ // here (PersonPeriod has three, marked on the wire): every question this vector asks is about
21
+ // what HAPPENED in the window, so a "currently open" count would be the only unreproducible
22
+ // figure on the page and it would not earn its place.
23
+ //
24
+ // ⚠ `prs_merged_without_human_review` looks like it breaks window purity — it asks "did any human
25
+ // ever review this PR?", which is not itself a windowed question. It does not: the population is
26
+ // PRs MERGED INSIDE the window, and a review of a merged PR necessarily predates its merge, so
27
+ // the answer is fixed the moment the PR lands and can never move afterwards. Restricting the
28
+ // review probe to the window would be the bug — a PR opened and reviewed in March and merged in
29
+ // April would count as "merged without review".
30
+ //
31
+ // ⚠ `users` IS A GLOBAL TABLE (the listUsers precedent). The subject is admitted only after an
32
+ // authored-PR probe INSIDE the workspace scope, and only the login and display name ever leave.
33
+ //
34
+ // ⚠ MIRROR IMAGE OF THE PERSON FOLD'S LANE CHECK: person-period refuses anything the lane
35
+ // resolver calls automation; this refuses anything it calls `'human'`. Neither is a login
36
+ // heuristic — both go through db/actor-lanes.ts, so a manual "this account is a person"
37
+ // judgement moves an actor between the two folds in one place.
38
+ import { and, count, desc, eq, gte, inArray, isNull, lt, ne, or } from 'drizzle-orm';
39
+ import { db, schema } from './client.js';
40
+ import { resolveActorLanes } from './actor-lanes.js';
41
+ import { median, round2 } from './period-metrics.js';
42
+ import { PERSON_EVIDENCE_CAP } from './person-period.js';
43
+ import { reviewerRoleForUser } from './queries.js';
44
+ const { pullRequests, repos, reviewComments, reviews, users } = schema;
45
+ // MIRRORED from @pierre-review/shared (AUTOMATION_METRIC_KEYS) for the same types-only reason the
46
+ // person keys are inlined: `shared` ships no runtime. `automation-output.test.ts` imports the
47
+ // shared original and asserts the two spellings are identical, so drift fails CI, not a reader.
48
+ export const AUTOMATION_METRIC_KEYS = [
49
+ 'prs_opened',
50
+ 'prs_merged',
51
+ 'prs_closed_unmerged',
52
+ 'merge_rate_pct',
53
+ 'median_hours_to_merge',
54
+ 'median_pr_size_lines',
55
+ 'prs_merged_without_human_review',
56
+ 'human_review_comments_received',
57
+ 'repos_touched',
58
+ ];
59
+ /** Repos listed under the vector. Rendering cap only — the `repos_touched` METRIC counts them
60
+ * all, so a workspace with 40 repos reports 40 and lists the busiest few. */
61
+ const REPO_LIST_CAP = 8;
62
+ /** The merged-PR scan that feeds the medians and the no-human-review probe. Bounded for the same
63
+ * reason PERIOD_FIRST_REVIEW_PR_CAP is: the ids travel onward as BIND PARAMETERS. A dependency
64
+ * bot can merge hundreds of PRs in a fortnight, so this is the one fold here that can get big. */
65
+ const AUTOMATION_MERGED_PR_CAP = 2000;
66
+ function cell(key, value, sampleSize) {
67
+ return { key, value, sampleSize };
68
+ }
69
+ /** A count is never null — zero merges IS zero, not "no sample". Medians and rates are the ones
70
+ * that go null (the period-metrics rule: a null renders "—", never 0). */
71
+ function countCell(key, n) {
72
+ return cell(key, n, n);
73
+ }
74
+ export async function getAutomationOutput(accountId, scope, userId, window, opts = {}) {
75
+ const from = new Date(window.fromMs);
76
+ const to = new Date(window.toMs);
77
+ // An empty workspace admits nobody — and without this guard the probe below would read the
78
+ // whole account (`inArray(…, [])` is false, but be explicit about the state).
79
+ if (scope.repoIds.length === 0)
80
+ return null;
81
+ const lanes = await resolveActorLanes(accountId, scope);
82
+ if (lanes.laneOf(userId) === 'human')
83
+ return null;
84
+ const inScope = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds));
85
+ const authored = and(inScope, eq(pullRequests.authorId, userId));
86
+ // ── Admission probe (ALL TIME, inside the scope) ────────────────────────────────────────────
87
+ // An automation that has never authored a PR in this workspace is not this fold's subject —
88
+ // it is a reviewer, and `getBotAnalytics` already describes it. Returning null here is what
89
+ // lets the caller decide which of the two sections to render, and it is also why the route is
90
+ // not an existence oracle: an unknown id degrades identically.
91
+ const [probe] = await db
92
+ .select({ c: count() })
93
+ .from(pullRequests)
94
+ .where(authored)
95
+ .execute();
96
+ if ((probe?.c ?? 0) === 0)
97
+ return null;
98
+ const [identity] = await db
99
+ .select({ login: users.githubLogin, name: users.displayName })
100
+ .from(users)
101
+ .where(eq(users.id, userId))
102
+ .execute();
103
+ // ── The windowed counts ────────────────────────────────────────────────────────────────────
104
+ const [openedRow, mergedRow, closedRow, humanCommentsRow, repoRows, mergedPrs] = await Promise.all([
105
+ db
106
+ .select({ c: count() })
107
+ .from(pullRequests)
108
+ .where(and(authored, gte(pullRequests.openedAt, from), lt(pullRequests.openedAt, to)))
109
+ .execute(),
110
+ db
111
+ .select({ c: count() })
112
+ .from(pullRequests)
113
+ .where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
114
+ .execute(),
115
+ // Closed WITHOUT merging, inside the window. `mergedAt IS NULL` is the discriminator —
116
+ // GitHub stamps closedAt on a merge too, so a bare closedAt window would double-count every
117
+ // merged PR as wasted churn.
118
+ db
119
+ .select({ c: count() })
120
+ .from(pullRequests)
121
+ .where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
122
+ .execute(),
123
+ // Human review comments left ON its PRs in the window — the time it cost people. Bot-authored
124
+ // comments are excluded through the lane resolver, not a login test, so one bot reviewing
125
+ // another bot's PRs never reads as human attention.
126
+ db
127
+ .select({ authorId: reviewComments.authorId, c: count() })
128
+ .from(reviewComments)
129
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
130
+ .where(and(authored, gte(reviewComments.createdAt, from), lt(reviewComments.createdAt, to)))
131
+ .groupBy(reviewComments.authorId)
132
+ .execute(),
133
+ // Repos touched: PRs it OPENED or MERGED in the window, by repo. One two-sided predicate per
134
+ // column, OR-ed — a PR opened before the window and merged inside it still counts the repo.
135
+ db
136
+ .select({ repoId: pullRequests.repoId, owner: repos.owner, name: repos.name, c: count() })
137
+ .from(pullRequests)
138
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
139
+ .where(and(authored,
140
+ // Portable `or(and(…), and(…))`, never a raw `sql` template: drizzle binds a template's
141
+ // `${date}` straight through, and better-sqlite3 refuses a Date — the same expression
142
+ // that type-checks fine throws at the first call. Two-sided on both columns.
143
+ or(and(gte(pullRequests.openedAt, from), lt(pullRequests.openedAt, to)), and(gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))))
144
+ .groupBy(pullRequests.repoId, repos.owner, repos.name)
145
+ .orderBy(desc(count()))
146
+ .execute(),
147
+ // The merged-in-window population, carrying everything the medians need in one read.
148
+ db
149
+ .select({
150
+ id: pullRequests.id,
151
+ openedAt: pullRequests.openedAt,
152
+ mergedAt: pullRequests.mergedAt,
153
+ additions: pullRequests.additions,
154
+ deletions: pullRequests.deletions,
155
+ })
156
+ .from(pullRequests)
157
+ .where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
158
+ .orderBy(desc(pullRequests.mergedAt))
159
+ .limit(AUTOMATION_MERGED_PR_CAP)
160
+ .execute(),
161
+ ]);
162
+ const opened = openedRow[0]?.c ?? 0;
163
+ const merged = mergedRow[0]?.c ?? 0;
164
+ const closedUnmerged = closedRow[0]?.c ?? 0;
165
+ const humanComments = humanCommentsRow
166
+ .filter((r) => lanes.laneOf(r.authorId) === 'human')
167
+ .reduce((sum, r) => sum + r.c, 0);
168
+ // Merge rate over the RESOLVED population (merged + closed-unmerged, both windowed). Not over
169
+ // `opened`: a PR opened on the last day of the window has not had its chance yet, and dividing
170
+ // by it would make a busy final week look like a failing bot.
171
+ const resolved = merged + closedUnmerged;
172
+ const mergeRate = resolved > 0 ? round2((merged / resolved) * 100) : null;
173
+ const hoursToMerge = mergedPrs
174
+ .map((p) => p.mergedAt != null && p.openedAt != null
175
+ ? (p.mergedAt.getTime() - p.openedAt.getTime()) / 3_600_000
176
+ : null)
177
+ .filter((v) => v != null && Number.isFinite(v) && v >= 0);
178
+ const sizes = mergedPrs.map((p) => p.additions + p.deletions);
179
+ // ── "Merged with no human in the loop" ─────────────────────────────────────────────────────
180
+ // Over the merged-in-window ids, ALL-TIME (see the header note on why that is still
181
+ // window-pure). A PR counts as human-touched if any human submitted a review OR left a review
182
+ // comment on it, ever.
183
+ const mergedIds = mergedPrs.map((p) => p.id);
184
+ let mergedNoHuman = null;
185
+ if (mergedIds.length > 0) {
186
+ const [reviewRows, commentRows] = await Promise.all([
187
+ db
188
+ .select({ prId: reviews.prId, authorId: reviews.authorId })
189
+ .from(reviews)
190
+ .where(and(inArray(reviews.prId, mergedIds), ne(reviews.state, 'pending')))
191
+ .execute(),
192
+ db
193
+ .select({ prId: reviewComments.prId, authorId: reviewComments.authorId })
194
+ .from(reviewComments)
195
+ .where(inArray(reviewComments.prId, mergedIds))
196
+ .execute(),
197
+ ]);
198
+ const touched = new Set();
199
+ for (const r of [...reviewRows, ...commentRows]) {
200
+ if (lanes.laneOf(r.authorId) === 'human')
201
+ touched.add(r.prId);
202
+ }
203
+ mergedNoHuman = mergedIds.filter((id) => !touched.has(id)).length;
204
+ }
205
+ const metrics = [
206
+ countCell('prs_opened', opened),
207
+ countCell('prs_merged', merged),
208
+ countCell('prs_closed_unmerged', closedUnmerged),
209
+ cell('merge_rate_pct', mergeRate, resolved),
210
+ cell('median_hours_to_merge', hoursToMerge.length > 0 ? round2(median(hoursToMerge)) : null, hoursToMerge.length),
211
+ cell('median_pr_size_lines', sizes.length > 0 ? Math.round(median(sizes)) : null, sizes.length),
212
+ cell('prs_merged_without_human_review', mergedNoHuman, mergedIds.length),
213
+ countCell('human_review_comments_received', humanComments),
214
+ countCell('repos_touched', repoRows.length),
215
+ ];
216
+ const roleByUser = await reviewerRoleForUser(accountId, scope.workspaceId);
217
+ const role = roleByUser.get(userId) ?? null;
218
+ const out = {
219
+ userId,
220
+ login: identity?.login ?? null,
221
+ displayName: identity?.name ?? null,
222
+ role,
223
+ repos: repoRows.slice(0, REPO_LIST_CAP).map((r) => ({
224
+ repoId: r.repoId,
225
+ repoFullName: `${r.owner}/${r.name}`,
226
+ prs: r.c,
227
+ })),
228
+ metrics,
229
+ };
230
+ if (opts.evidence === true) {
231
+ out.evidence = await loadAutomationEvidence(accountId, scope, userId, out.login, from, to, lanes);
232
+ }
233
+ return out;
234
+ }
235
+ /** The receipt rows. Each group is capped at PERSON_EVIDENCE_CAP with an honest remainder, and
236
+ * each is the SAME population as the metric it sits under — a card a reader can check the
237
+ * figure against, never a differently-filtered list that happens to look related. */
238
+ async function loadAutomationEvidence(accountId, scope, userId, subjectLogin, from, to, lanes) {
239
+ const inScope = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds));
240
+ const authored = and(inScope, eq(pullRequests.authorId, userId));
241
+ // Everything DigestPrRef renders, selected once. The table draws PR | CI | age | author | diff,
242
+ // so omitting `ciStatus`/`openedAt`/`changedFiles` leaves two columns as bare dashes on every
243
+ // row — the shape looks finished and reads as "no data" rather than "not selected".
244
+ const selectPr = {
245
+ prId: pullRequests.id,
246
+ prNumber: pullRequests.number,
247
+ repoId: pullRequests.repoId,
248
+ repoOwner: repos.owner,
249
+ repoName: repos.name,
250
+ title: pullRequests.title,
251
+ state: pullRequests.state,
252
+ ciStatus: pullRequests.ciStatus,
253
+ openedAt: pullRequests.openedAt,
254
+ additions: pullRequests.additions,
255
+ deletions: pullRequests.deletions,
256
+ changedFiles: pullRequests.changedFiles,
257
+ };
258
+ const [mergedRows, mergedTotal, closedRows, closedTotal, commentedRows] = await Promise.all([
259
+ db
260
+ .select(selectPr)
261
+ .from(pullRequests)
262
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
263
+ .where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
264
+ .orderBy(desc(pullRequests.mergedAt))
265
+ .limit(PERSON_EVIDENCE_CAP)
266
+ .execute(),
267
+ db
268
+ .select({ c: count() })
269
+ .from(pullRequests)
270
+ .where(and(authored, gte(pullRequests.mergedAt, from), lt(pullRequests.mergedAt, to)))
271
+ .execute(),
272
+ db
273
+ .select(selectPr)
274
+ .from(pullRequests)
275
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
276
+ .where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
277
+ .orderBy(desc(pullRequests.closedAt))
278
+ .limit(PERSON_EVIDENCE_CAP)
279
+ .execute(),
280
+ db
281
+ .select({ c: count() })
282
+ .from(pullRequests)
283
+ .where(and(authored, isNull(pullRequests.mergedAt), gte(pullRequests.closedAt, from), lt(pullRequests.closedAt, to)))
284
+ .execute(),
285
+ // PRs of its that drew review comments in the window, with the commenter so the human filter
286
+ // runs through the lane resolver rather than in SQL. Capped generously before folding, since
287
+ // the fold collapses many comments onto few PRs.
288
+ db
289
+ .select({
290
+ ...selectPr,
291
+ commenterId: reviewComments.authorId,
292
+ commentedAt: reviewComments.createdAt,
293
+ })
294
+ .from(reviewComments)
295
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
296
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
297
+ .where(and(authored, gte(reviewComments.createdAt, from), lt(reviewComments.createdAt, to)))
298
+ .orderBy(desc(reviewComments.createdAt))
299
+ .limit(PERSON_EVIDENCE_CAP * 20)
300
+ .execute(),
301
+ ]);
302
+ // No cast: every DigestPrRef field is spelled out, so dropping one from `selectPr` becomes a
303
+ // compile error rather than a silently blank column.
304
+ const toRef = (r) => ({
305
+ prId: r.prId,
306
+ prNumber: r.prNumber,
307
+ repoId: r.repoId,
308
+ repoFullName: `${r.repoOwner}/${r.repoName}`,
309
+ title: r.title,
310
+ // The author is the subject of this whole vector, so the login is resolved once by the caller
311
+ // rather than re-joined per row; `authorId` is what the table renders an avatar from.
312
+ authorLogin: subjectLogin,
313
+ authorId: userId,
314
+ state: r.state,
315
+ ciStatus: r.ciStatus,
316
+ additions: r.additions,
317
+ deletions: r.deletions,
318
+ changedFiles: r.changedFiles,
319
+ openedAt: r.openedAt?.toISOString() ?? null,
320
+ });
321
+ // One row per PR, newest comment first, humans only.
322
+ const seen = new Set();
323
+ const humanReviewed = [];
324
+ let humanReviewedSeen = 0;
325
+ for (const r of commentedRows) {
326
+ if (lanes.laneOf(r.commenterId) !== 'human')
327
+ continue;
328
+ if (seen.has(r.prId))
329
+ continue;
330
+ seen.add(r.prId);
331
+ humanReviewedSeen += 1;
332
+ if (humanReviewed.length < PERSON_EVIDENCE_CAP)
333
+ humanReviewed.push(toRef(r));
334
+ }
335
+ const mergedCount = mergedTotal[0]?.c ?? 0;
336
+ const closedCount = closedTotal[0]?.c ?? 0;
337
+ return {
338
+ merged: mergedRows.map(toRef),
339
+ mergedMore: Math.max(0, mergedCount - mergedRows.length),
340
+ closedUnmerged: closedRows.map(toRef),
341
+ closedUnmergedMore: Math.max(0, closedCount - closedRows.length),
342
+ humanReviewed,
343
+ // The remainder here is over what the capped scan SAW, so it can under-report on a very
344
+ // chatty period. That is why the caption says "at least" — it must never claim to be the
345
+ // population figure the way the two above do.
346
+ humanReviewedMore: Math.max(0, humanReviewedSeen - humanReviewed.length),
347
+ };
348
+ }
349
+ //# sourceMappingURL=automation-output.js.map