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,579 @@
1
+ // ── "Same-line overlap" drill-down — the CLUSTER half of the bot-flagging drill-down ────────
2
+ //
3
+ // The Bots rail's ML strip ends with a "Same-line overlap" tile: how many line areas more than
4
+ // one bot flagged. This file is what happens when it is clicked. Everything else the strip
5
+ // drills into is an ML LABEL population (db/ml-labels.ts, `getBotFlaggingComments`); this tile
6
+ // alone is DETERMINISTIC — no model, no severity — so it lives here and answers a different
7
+ // item shape (`BotFlaggingClustersResponse`).
8
+ //
9
+ // ⚠ THE ONE HARD REQUIREMENT: `total` must equal the tile. The tile is
10
+ // `getBotAnalytics().totals.overlapClusters`, computed in queries.ts by scanning the window's
11
+ // automated-reviewer threads, dropping three populations IN A FIXED ORDER, clustering them with
12
+ // the shared ±3-line arbiter and counting the groups with ≥2 DISTINCT bots. Every one of those
13
+ // steps is reproduced below, in that order, from the same tables — the ONLY intended difference
14
+ // is that the scan reads `gte(createdAt, from)` directly instead of walking the wider 84-day
15
+ // trend span and re-filtering in JS (`from` is the same instant either way, so the two select
16
+ // the same rows). Get any one of the exclusions wrong and the tile says 34 while the list
17
+ // says 41; that is the whole failure mode this comment exists to prevent.
18
+ //
19
+ // ⚑ A SEPARATE FILE, not an addition to ml-labels.ts: this needs `reviewerRoleForUser`, which
20
+ // ml-labels.ts does not import, and adding it there would deepen the deliberate
21
+ // queries ⇄ ml-labels module cycle. Nothing imports this file back, so no new cycle.
22
+ import { and, eq, gte, inArray } from 'drizzle-orm';
23
+ import { db, schema } from './client.js';
24
+ import { botWindowMs } from './bot-window.js';
25
+ import { clusterThreadsByLine } from './line-overlap.js';
26
+ import { labelFor as labelForKind } from '../sync/reviewer-classify.js';
27
+ import { automatedReviewerUserIds, classificationKindForUser, classificationLabelMap, reviewerRoleForUser, } from './queries.js';
28
+ const { mlCommentLabels, pullRequests, repos, reviewComments, reviewThreads, users } = schema;
29
+ // Hydration bound. Clustering itself is free (the Bots panel already runs the strictly WIDER
30
+ // 84-day version of this scan on every load), so `total` is exact and uncapped; this caps only
31
+ // how many clusters get comment bodies + labels attached, and sets `truncated` when it bites.
32
+ const OVERLAP_CLUSTER_CAP = 2000;
33
+ // SQLite's bound-parameter ceiling is per statement, so every `inArray` over an unbounded id
34
+ // list is chunked (the db/retention.ts precedent).
35
+ const MAX_IN_PARAMS = 900;
36
+ // ⚠ LOCAL COPIES OF `ML_SEVERITY_ORD` / `ML_SEVERITIES` ON PURPOSE. Both exist as value exports
37
+ // in @pierre-review/shared, but the backend may import that package with `import type` ONLY —
38
+ // it is types-only and is NOT a shipped dependency, and `build-release.mjs` greps the emitted
39
+ // `release/dist` and FAILS the build on a surviving runtime import. ml-labels.ts's own
40
+ // `SEVERITY_KEYS` is the same accommodation. Keep these in step with the shared exports.
41
+ const SEVERITY_ORD = { nit: 0, minor: 1, major: 2, critical: 3 };
42
+ const SEVERITY_AXIS = ['critical', 'major', 'minor', 'nit']; // worst-first
43
+ const VENDOR_AXIS = [...SEVERITY_AXIS, 'none'];
44
+ const SEVERITY_KEYS = ['nit', 'minor', 'major', 'critical'];
45
+ const VENDOR_CONFIDENCE_KEYS = ['high', 'medium', 'low'];
46
+ const ML_CATEGORY_VALUES = new Set([
47
+ 'correctness_bug',
48
+ 'security',
49
+ 'performance',
50
+ 'style_readability',
51
+ 'maintainability_refactor',
52
+ 'testing',
53
+ 'documentation',
54
+ 'nitpick',
55
+ 'praise',
56
+ ]);
57
+ // ── Wire coercions, mirroring ml-labels.ts's `toWireLabel` ──────────────────────────────────
58
+ // Restated here because ml-labels.ts keeps its copies module-private. The drizzle `enum` on
59
+ // these columns is a COMPILE-TIME nicety, not a CHECK constraint, so an off-union stored value
60
+ // is representable: an unreadable severity drops the whole LABEL (the comment still renders,
61
+ // unbadged), while an unreadable vendor claim degrades to "no vendor claim" — the row's own
62
+ // severity is the useful one.
63
+ function coerceSeverity(raw) {
64
+ return typeof raw === 'string' && SEVERITY_KEYS.includes(raw)
65
+ ? raw
66
+ : null;
67
+ }
68
+ function coerceVendorConfidence(raw) {
69
+ return typeof raw === 'string' && VENDOR_CONFIDENCE_KEYS.includes(raw)
70
+ ? raw
71
+ : null;
72
+ }
73
+ function coerceCategories(raw) {
74
+ if (!Array.isArray(raw))
75
+ return [];
76
+ return raw.filter((c) => typeof c === 'string' && ML_CATEGORY_VALUES.has(c));
77
+ }
78
+ function chunk(arr, n) {
79
+ const out = [];
80
+ for (let i = 0; i < arr.length; i += n)
81
+ out.push(arr.slice(i, i + n));
82
+ return out;
83
+ }
84
+ /** Run one `inArray` query per parameter-safe batch and concatenate. Empty in ⇒ no query. */
85
+ async function inChunks(ids, run) {
86
+ const out = [];
87
+ for (const batch of chunk(ids, MAX_IN_PARAMS))
88
+ out.push(...(await run(batch)));
89
+ return out;
90
+ }
91
+ // ── The ours-vs-vendor confusion matrix ─────────────────────────────────────────────────────
92
+ // ⚠ A DISPLAY OF TWO CLAIMS, NEVER A RECONCILIATION. `vendorSeverity` is materially LESS
93
+ // accurate than ours on the adjudicated gold-300 (0.474 exact vs 0.700), so it is shown beside
94
+ // ours and read by nothing else: it is not in the cluster predicate, not in `total`, and it
95
+ // never moves a badge. Direction is ordinal on BOTH sides — never "confidence", never
96
+ // `severityProb`.
97
+ function emptyMatrix() {
98
+ const cells = [];
99
+ for (const vendor of VENDOR_AXIS)
100
+ for (const ours of SEVERITY_AXIS)
101
+ cells.push({ vendor, ours, count: 0 });
102
+ return { cells, declared: 0, undeclared: 0, agree: 0, overCall: 0, underCall: 0, total: 0 };
103
+ }
104
+ function buildMatrix(labels) {
105
+ const m = emptyMatrix();
106
+ const index = new Map();
107
+ for (const c of m.cells)
108
+ index.set(`${c.vendor}|${c.ours}`, c);
109
+ for (const l of labels) {
110
+ m.total += 1;
111
+ const vendor = l.vendorSeverity ?? 'none';
112
+ const cell = index.get(`${vendor}|${l.severity}`);
113
+ if (cell)
114
+ cell.count += 1;
115
+ if (l.vendorSeverity == null) {
116
+ m.undeclared += 1;
117
+ continue;
118
+ }
119
+ m.declared += 1;
120
+ const theirs = SEVERITY_ORD[l.vendorSeverity];
121
+ const ours = SEVERITY_ORD[l.severity];
122
+ if (theirs === ours)
123
+ m.agree += 1;
124
+ else if (theirs > ours)
125
+ m.overCall += 1;
126
+ else
127
+ m.underCall += 1;
128
+ }
129
+ return m;
130
+ }
131
+ /** Does one label satisfy `refine`? A target with NO label never matches a refinement. */
132
+ function labelMatchesRefine(label, refine) {
133
+ if (!label)
134
+ return false;
135
+ if (refine.cell) {
136
+ const vendor = label.vendorSeverity ?? 'none';
137
+ if (vendor !== refine.cell.vendor || label.severity !== refine.cell.ours)
138
+ return false;
139
+ }
140
+ if (refine.disagree) {
141
+ // An UNDECLARED vendor claim is not a disagreement — it is silence (the `undeclared`
142
+ // column exists to say so).
143
+ if (label.vendorSeverity == null || label.vendorSeverity === label.severity)
144
+ return false;
145
+ const theirs = SEVERITY_ORD[label.vendorSeverity];
146
+ const ours = SEVERITY_ORD[label.severity];
147
+ if (refine.disagree === 'over' && theirs <= ours)
148
+ return false;
149
+ if (refine.disagree === 'under' && theirs >= ours)
150
+ return false;
151
+ }
152
+ return true;
153
+ }
154
+ /**
155
+ * The "Same-line overlap" tile's drill-down: the line areas more than one bot flagged, in the
156
+ * SAME window and scope the tile was measured at, one card per cluster.
157
+ *
158
+ * `total` ≡ `getBotAnalytics(...).totals.overlapClusters` BY CONSTRUCTION — same thread
159
+ * population, same three exclusions in the same order, same `clusterThreadsByLine` arbiter,
160
+ * same `userIds.size >= 2` gate. Paging is a JS `slice` over that ordered list behind an opaque
161
+ * cursor, so the count can never be a differently-derived number that happens to agree.
162
+ */
163
+ export async function getBotOverlapClusters(accountId, refine, window,
164
+ // The SAME BotScope the strip was computed at — `workspaceId` decides who counts as a bot,
165
+ // `repoIds` narrows which data is measured. `repoIds: []` = an empty workspace, never "widen".
166
+ scope, page) {
167
+ const nowMs = Date.now();
168
+ const to = new Date(nowMs);
169
+ // The one shared window→duration mapping (db/bot-window.ts) — the tile's own `from`.
170
+ const from = new Date(nowMs - botWindowMs(window));
171
+ const win = { kind: window, from: from.toISOString(), to: to.toISOString() };
172
+ const generatedAt = to.toISOString();
173
+ const offset = Math.max(0, page.offset);
174
+ const limit = Math.max(1, page.limit);
175
+ const empty = () => ({
176
+ kind: 'clusters',
177
+ workspaceId: scope.workspaceId,
178
+ window: win,
179
+ selector: { kind: 'overlap' },
180
+ refine,
181
+ total: 0,
182
+ filteredTotal: 0,
183
+ matrix: emptyMatrix(),
184
+ items: [],
185
+ nextCursor: null,
186
+ truncated: false,
187
+ generatedAt,
188
+ });
189
+ // An empty workspace → nothing to analyze (mirrors getBotAnalytics' two early exits).
190
+ if (scope.repoIds.length === 0)
191
+ return empty();
192
+ // `role: 'all'` — the same set the tile counted from. The quality-check EXCLUSION happens
193
+ // below, at the same point in the pipeline it happens in getBotAnalytics; narrowing here
194
+ // instead would also change `classificationKindForUser`'s reach and is not the same filter.
195
+ const automatedIds = await automatedReviewerUserIds(accountId, scope.workspaceId, 'all');
196
+ if (automatedIds.length === 0)
197
+ return empty();
198
+ const kindMap = await classificationKindForUser(accountId, scope.workspaceId);
199
+ const roleMap = await reviewerRoleForUser(accountId, scope.workspaceId);
200
+ const classLabel = await classificationLabelMap(accountId, scope.workspaceId);
201
+ // ── 1. The thread scan ────────────────────────────────────────────────────────────────────
202
+ // getBotAnalytics' `threadRows`, with `from` in place of `trendFrom` (it re-filters to the
203
+ // window in JS anyway) plus the PR/repo columns the cards need — carried HERE so the page
204
+ // hydration below fetches bodies and nothing else. `line` lives on `review_threads` and
205
+ // nowhere else: `review_comments` has no line columns, so overlap never computes from
206
+ // comments.
207
+ const threadRows = await db
208
+ .select({
209
+ id: reviewThreads.id,
210
+ prId: reviewThreads.prId,
211
+ userId: reviewThreads.originalCommenterId,
212
+ path: reviewThreads.path,
213
+ line: reviewThreads.line,
214
+ state: reviewThreads.derivedState,
215
+ addressedConfidence: reviewThreads.addressedConfidence,
216
+ prNumber: pullRequests.number,
217
+ prTitle: pullRequests.title,
218
+ prAuthorId: pullRequests.authorId,
219
+ repoId: pullRequests.repoId,
220
+ owner: repos.owner,
221
+ name: repos.name,
222
+ })
223
+ .from(reviewThreads)
224
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewThreads.prId))
225
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
226
+ .where(and(eq(pullRequests.accountId, accountId), inArray(reviewThreads.originalCommenterId, automatedIds), gte(reviewThreads.createdAt, from), inArray(pullRequests.repoId, scope.repoIds)))
227
+ .execute();
228
+ // ── 2. The three exclusions, IN THE TILE'S ORDER ──────────────────────────────────────────
229
+ // (a) a thread with no original commenter identifies no bot; (b) an id this workspace has not
230
+ // classified as automated is not a bot HERE (identity is per workspace); (c) quality checks
231
+ // are excluded from overlap on both sides — "SonarQube and CodeRabbit both hit line 42" is a
232
+ // rule firing next to a judgement, not two reviewers agreeing.
233
+ const threads = [];
234
+ for (const t of threadRows) {
235
+ if (t.userId == null)
236
+ continue;
237
+ const kind = kindMap.get(t.userId);
238
+ if (!kind)
239
+ continue;
240
+ // ⚠ `!== 'review'`, never `=== 'quality_check'`: the role union now carries dependency /
241
+ // code_agent / release / housekeeping too, and every one of them would pass a "not a quality
242
+ // check" test back into an overlap number that is only meaningful between REVIEWERS.
243
+ // `roleMap` has no entry for an actor with no stored row and no seeded login, and that
244
+ // absence means the historical default 'review' — hence the `?? 'review'`.
245
+ if ((roleMap.get(t.userId) ?? 'review') !== 'review')
246
+ continue;
247
+ threads.push({
248
+ prId: t.prId,
249
+ path: t.path,
250
+ line: t.line,
251
+ userId: t.userId,
252
+ threadId: t.id,
253
+ kind,
254
+ derivedState: t.state,
255
+ addressedConfidence: t.addressedConfidence,
256
+ prNumber: t.prNumber,
257
+ prTitle: t.prTitle,
258
+ prAuthorId: t.prAuthorId ?? null,
259
+ repoId: t.repoId,
260
+ repoFullName: `${t.owner}/${t.name}`,
261
+ });
262
+ }
263
+ // Deterministic input order. The arbiter sorts by line internally and anchors on line VALUES,
264
+ // so pre-sorting cannot move a thread between clusters — but among threads sharing a line the
265
+ // stable sort preserves input order, which out of a join is not a promise. Sorting here is
266
+ // what makes "this bot's FIRST thread in the cluster" (the representative below) the same
267
+ // thread on every run and in both dialects.
268
+ threads.sort((a, b) => a.prId - b.prId ||
269
+ (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ||
270
+ (a.line ?? Number.MAX_SAFE_INTEGER) - (b.line ?? Number.MAX_SAFE_INTEGER) ||
271
+ a.threadId - b.threadId);
272
+ // ── 3. THE shared ±3-line clustering (db/line-overlap.ts) ─────────────────────────────────
273
+ // `nullLineGroup: false` and `userIds.size >= 2` are the tile's settings, not a choice made
274
+ // here: a thread LOSES its line when it outdates, and a per-file null lump manufactures
275
+ // overlap out of any two chatty bots.
276
+ const qualifying = [];
277
+ for (const c of clusterThreadsByLine(threads, { nullLineGroup: false })) {
278
+ if (c.userIds.size < 2)
279
+ continue;
280
+ // Never null with `nullLineGroup: false`; the guard is what narrows the type honestly.
281
+ if (c.line == null)
282
+ continue;
283
+ let lineEnd = c.line;
284
+ for (const t of c.items)
285
+ if (t.line != null && t.line > lineEnd)
286
+ lineEnd = t.line;
287
+ qualifying.push({
288
+ prId: c.prId,
289
+ path: c.path,
290
+ lineStart: c.line,
291
+ lineEnd,
292
+ bots: c.userIds.size,
293
+ threadCount: c.items.length,
294
+ items: c.items,
295
+ });
296
+ }
297
+ // THE tile's number. Counted before the hydration cap below, so a capped page still reports
298
+ // the honest total (with `truncated` saying the LIST is a sample).
299
+ const total = qualifying.length;
300
+ // ── 4. A TOTAL order ──────────────────────────────────────────────────────────────────────
301
+ // Biggest dedup hit first, like the per-PR rollup — but that one sorts on two keys only and
302
+ // is scoped to a single PR, so it leaves ties in DB order. A PAGED cross-PR list cannot: two
303
+ // requests for the same page must return the same clusters, so the comparator runs all the
304
+ // way down to a unique key.
305
+ qualifying.sort((a, b) => b.bots - a.bots ||
306
+ b.threadCount - a.threadCount ||
307
+ a.prId - b.prId ||
308
+ (a.path < b.path ? -1 : a.path > b.path ? 1 : 0) ||
309
+ a.lineStart - b.lineStart);
310
+ const truncated = qualifying.length > OVERLAP_CLUSTER_CAP;
311
+ const capped = truncated ? qualifying.slice(0, OVERLAP_CLUSTER_CAP) : qualifying;
312
+ if (capped.length === 0)
313
+ return { ...empty(), total, truncated };
314
+ // ── 5. Origin comment per member thread ───────────────────────────────────────────────────
315
+ // The thread's OWN bot's earliest comment, ties broken on the LOWER id. Both halves matter:
316
+ // without the author filter a human's reply can become the "origin", and without the id
317
+ // tiebreak two comments sharing a timestamp resolve differently per dialect. (This is
318
+ // getBotAnalytics' rule, NOT the per-PR dedup's excerpt pick, which has neither.)
319
+ //
320
+ // Fetched for every CAPPED cluster's threads, not just the page's, because the matrix below
321
+ // describes the whole population the page is a window into. No bodies here — ids only.
322
+ const memberThreadIds = [...new Set(capped.flatMap((c) => c.items.map((t) => t.threadId)))];
323
+ const commentRows = await inChunks(memberThreadIds, (batch) => db
324
+ .select({
325
+ id: reviewComments.id,
326
+ threadId: reviewComments.threadId,
327
+ authorId: reviewComments.authorId,
328
+ createdAt: reviewComments.createdAt,
329
+ })
330
+ .from(reviewComments)
331
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
332
+ // The ids came from an already-scoped scan; the tenancy predicate is still not optional —
333
+ // "the input was trusted" is how an IDOR gets written.
334
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), inArray(reviewComments.threadId, batch)))
335
+ .execute());
336
+ const commentsByThread = new Map();
337
+ for (const r of commentRows) {
338
+ const arr = commentsByThread.get(r.threadId) ?? [];
339
+ arr.push({ id: r.id, authorId: r.authorId, at: r.createdAt.getTime() });
340
+ commentsByThread.set(r.threadId, arr);
341
+ }
342
+ const botByThread = new Map();
343
+ for (const c of capped)
344
+ for (const t of c.items)
345
+ botByThread.set(t.threadId, t.userId);
346
+ const originByThread = new Map();
347
+ for (const [threadId, comments] of commentsByThread) {
348
+ const botId = botByThread.get(threadId);
349
+ if (botId == null)
350
+ continue;
351
+ let origin = null;
352
+ for (const c of comments) {
353
+ if (c.authorId !== botId)
354
+ continue;
355
+ if (origin == null || c.at < origin.at || (c.at === origin.at && c.id < origin.id))
356
+ origin = { id: c.id, at: c.at };
357
+ }
358
+ // A thread with no comment by its own author keeps no origin — the member still renders,
359
+ // unbodied (`comment: null`).
360
+ if (origin)
361
+ originByThread.set(threadId, { id: origin.id, createdAt: new Date(origin.at) });
362
+ }
363
+ // ── 6. ML labels for those origin comments ────────────────────────────────────────────────
364
+ // Shipped INLINE on each card (a cross-PR list must never mount the per-PR `['ml-labels',
365
+ // prId]` index per row — that is the ThreadAssessment 60-requests-for-60-empty-boxes
366
+ // failure), and folded into the matrix below.
367
+ const originIds = [...new Set([...originByThread.values()].map((o) => o.id))];
368
+ const labelRows = await inChunks(originIds, (batch) => db
369
+ .select({
370
+ targetId: mlCommentLabels.targetId,
371
+ severity: mlCommentLabels.severity,
372
+ severityOrd: mlCommentLabels.severityOrd,
373
+ severityProb: mlCommentLabels.severityProb,
374
+ vendorSeverity: mlCommentLabels.vendorSeverity,
375
+ vendorSeverityConfidence: mlCommentLabels.vendorSeverityConfidence,
376
+ categories: mlCommentLabels.categories,
377
+ isSummary: mlCommentLabels.isSummary,
378
+ backend: mlCommentLabels.backend,
379
+ modelVersion: mlCommentLabels.modelVersion,
380
+ createdAt: mlCommentLabels.createdAt,
381
+ })
382
+ .from(mlCommentLabels)
383
+ .where(and(eq(mlCommentLabels.accountId, accountId),
384
+ // `targetId` lives in THREE id spaces — the kind is never optional on a lookup.
385
+ eq(mlCommentLabels.targetKind, 'review_comment'), inArray(mlCommentLabels.targetId, batch)))
386
+ .execute());
387
+ const labelByCommentId = new Map();
388
+ for (const r of labelRows) {
389
+ const severity = coerceSeverity(r.severity);
390
+ if (!severity)
391
+ continue;
392
+ labelByCommentId.set(r.targetId, {
393
+ targetKind: 'review_comment',
394
+ targetId: r.targetId,
395
+ severity,
396
+ severityOrd: r.severityOrd,
397
+ severityProb: r.severityProb,
398
+ vendorSeverity: coerceSeverity(r.vendorSeverity),
399
+ vendorSeverityConfidence: coerceVendorConfidence(r.vendorSeverityConfidence),
400
+ categories: coerceCategories(r.categories),
401
+ isSummary: r.isSummary,
402
+ backend: r.backend,
403
+ modelVersion: r.modelVersion,
404
+ createdAt: r.createdAt.toISOString(),
405
+ });
406
+ }
407
+ const labelForThread = (threadId) => {
408
+ const origin = originByThread.get(threadId);
409
+ return origin ? labelByCommentId.get(origin.id) : undefined;
410
+ };
411
+ // ── 7. The matrix, PRE-refine ─────────────────────────────────────────────────────────────
412
+ // Over every member thread's origin label, so a cell never zeroes itself out once clicked
413
+ // (the commentFacetCounts rule). ⚠ Its `total` is the count of LABELLED origin comments, NOT
414
+ // the cluster `total` above: a cluster is not an ML row, and only a labelled comment can be
415
+ // placed on either axis. `declared`/`undeclared` state the sparsity honestly.
416
+ const matrix = buildMatrix(capped.flatMap((c) => c.items
417
+ .map((t) => labelForThread(t.threadId))
418
+ .filter((l) => l !== undefined)));
419
+ // ── 8. Refine → slice ─────────────────────────────────────────────────────────────────────
420
+ // A cluster survives when ≥1 of its threads matches. (The wire member shape carries no
421
+ // per-member "matched" flag, so a surviving cluster renders whole — the matching member is
422
+ // recognisable from its own badge.)
423
+ //
424
+ // ⚠ `authorUserIds` (the per-bot narrowing) is a fact about the THREAD's author, not about a
425
+ // label, so it is checked on `t.userId` and — on its own — must NOT acquire the "has a label"
426
+ // requirement `labelMatchesRefine` imposes. A cluster whose members are unlabelled is still a
427
+ // cluster those bots are in. Narrowing a CLUSTER list by a bot set keeps every other member on
428
+ // the card on purpose: the card's whole subject is who else flagged the same lines.
429
+ //
430
+ // ⚠ `[]` MEANS "NO BOTS", NOT "EVERY BOT" — the `repoIds` rule. Both the member predicate and
431
+ // the "is there a narrowing at all" gate test PRESENCE (`!= null`), never length, so an empty
432
+ // set answers with an empty page instead of widening to the whole workspace.
433
+ const memberMatches = (t) => {
434
+ if (refine.authorUserIds != null && !refine.authorUserIds.includes(t.userId))
435
+ return false;
436
+ if (!refine.cell && !refine.disagree)
437
+ return true;
438
+ return labelMatchesRefine(labelForThread(t.threadId), refine);
439
+ };
440
+ const narrowed = refine.cell || refine.disagree || refine.authorUserIds != null
441
+ ? capped.filter((c) => c.items.some(memberMatches))
442
+ : capped;
443
+ // Describes the LIST being paged, so it counts the capped population: with `truncated` set it
444
+ // is deliberately below `total`, which stays the tile's honest, uncapped number.
445
+ const filteredTotal = narrowed.length;
446
+ const pageClusters = narrowed.slice(offset, offset + limit);
447
+ // `items.length > 0` is load-bearing: an offset PAST the end yields an empty page, and
448
+ // `offset + 0 < filteredTotal` would then hand back a cursor pointing at the start again.
449
+ const nextOffset = offset + pageClusters.length;
450
+ const nextCursor = pageClusters.length > 0 && nextOffset < filteredTotal ? `o:${nextOffset}` : null;
451
+ // ── 9. Collapse members, then hydrate the PAGE only ───────────────────────────────────────
452
+ // One member per BOT (the per-PR dedup's rule): a verbose bot's 23 threads render as one ×23
453
+ // pill, not 23 identical cards. Representative = that bot's first thread in cluster (line)
454
+ // order, which the pre-sort above made deterministic.
455
+ const collapsed = pageClusters.map((c) => {
456
+ const byUser = new Map();
457
+ for (const t of c.items) {
458
+ const arr = byUser.get(t.userId) ?? [];
459
+ arr.push(t);
460
+ byUser.set(t.userId, arr);
461
+ }
462
+ return { cluster: c, byUser };
463
+ });
464
+ // Only the representative's comment is rendered, so only its body is fetched.
465
+ const pageOriginIds = [
466
+ ...new Set(collapsed.flatMap(({ byUser }) => [...byUser.values()].flatMap((ts) => {
467
+ const origin = originByThread.get(ts[0].threadId);
468
+ return origin ? [origin.id] : [];
469
+ }))),
470
+ ];
471
+ const bodyByCommentId = new Map();
472
+ if (pageOriginIds.length > 0) {
473
+ for (const r of await inChunks(pageOriginIds, (batch) => db
474
+ .select({ id: reviewComments.id, body: reviewComments.body })
475
+ .from(reviewComments)
476
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
477
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), inArray(reviewComments.id, batch)))
478
+ .execute()))
479
+ bodyByCommentId.set(r.id, r.body);
480
+ }
481
+ // Author identity for the page's bots. Label preference mirrors getBotAnalytics'
482
+ // `reviewerLabel` exactly: the workspace's custom label → the vendor's pretty name (known
483
+ // vendors) → login/display name. Safe to read the GLOBAL users row here — the classification
484
+ // above proves the account association.
485
+ const pageUserIds = [...new Set(collapsed.flatMap(({ byUser }) => [...byUser.keys()]))];
486
+ const displayById = new Map();
487
+ const loginById = new Map();
488
+ for (const r of await inChunks(pageUserIds, (batch) => db
489
+ .select({ id: users.id, login: users.githubLogin, name: users.displayName })
490
+ .from(users)
491
+ .where(inArray(users.id, batch))
492
+ .execute())) {
493
+ displayById.set(r.id, r.name?.trim() || r.login || `#${r.id}`);
494
+ if (r.login)
495
+ loginById.set(r.id, r.login);
496
+ }
497
+ const reviewerLabel = (userId, kind) => {
498
+ const custom = classLabel.get(userId);
499
+ if (custom)
500
+ return custom;
501
+ if (kind !== 'in_house' && kind !== 'pierre' && kind !== 'vendor')
502
+ return labelForKind(kind);
503
+ return displayById.get(userId) ?? labelForKind(kind);
504
+ };
505
+ const items = collapsed.map(({ cluster, byUser }) => {
506
+ const head = cluster.items[0];
507
+ const prUrl = `https://github.com/${head.repoFullName}/pull/${head.prNumber}`;
508
+ const members = [...byUser.values()].map((ts) => {
509
+ const rep = ts[0];
510
+ const origin = originByThread.get(rep.threadId);
511
+ const comment = origin
512
+ ? {
513
+ targetKind: 'review_comment',
514
+ targetId: origin.id,
515
+ prId: rep.prId,
516
+ prNumber: rep.prNumber,
517
+ prTitle: rep.prTitle,
518
+ prAuthorId: rep.prAuthorId,
519
+ repoId: rep.repoId,
520
+ repoFullName: rep.repoFullName,
521
+ path: rep.path,
522
+ threadId: rep.threadId,
523
+ derivedState: rep.derivedState,
524
+ // A body row that vanished between the two reads leaves the card unbodied rather
525
+ // than dropping the member — the thread's own metadata is still true.
526
+ body: bodyByCommentId.get(origin.id) ?? null,
527
+ createdAt: origin.createdAt.toISOString(),
528
+ mlLabel: labelByCommentId.get(origin.id) ?? null,
529
+ authorUserId: rep.userId,
530
+ authorLogin: loginById.get(rep.userId) ?? null,
531
+ authorLabel: reviewerLabel(rep.userId, rep.kind),
532
+ authorKind: rep.kind,
533
+ line: rep.line,
534
+ prUrl,
535
+ }
536
+ : null;
537
+ return {
538
+ threadId: rep.threadId,
539
+ threadIds: ts.map((t) => t.threadId),
540
+ line: rep.line,
541
+ derivedState: rep.derivedState,
542
+ addressedConfidence: rep.addressedConfidence,
543
+ comment,
544
+ };
545
+ });
546
+ return {
547
+ // Opaque, equality only — and keyed on `prId`, never a bare PR number (numbers are unique
548
+ // per REPO, so a bare number cross-links one repo's #12 onto another's).
549
+ clusterId: `${cluster.prId}:${cluster.lineStart}:${cluster.path}`,
550
+ prId: cluster.prId,
551
+ prNumber: head.prNumber,
552
+ prTitle: head.prTitle,
553
+ prAuthorId: head.prAuthorId,
554
+ repoId: head.repoId,
555
+ repoFullName: head.repoFullName,
556
+ prUrl,
557
+ path: cluster.path,
558
+ lineStart: cluster.lineStart,
559
+ lineEnd: cluster.lineEnd,
560
+ members,
561
+ threadCount: members.reduce((n, m) => n + m.threadIds.length, 0),
562
+ };
563
+ });
564
+ return {
565
+ kind: 'clusters',
566
+ workspaceId: scope.workspaceId,
567
+ window: win,
568
+ selector: { kind: 'overlap' },
569
+ refine,
570
+ total,
571
+ filteredTotal,
572
+ matrix,
573
+ items,
574
+ nextCursor,
575
+ truncated,
576
+ generatedAt,
577
+ };
578
+ }
579
+ //# sourceMappingURL=bot-overlap.js.map