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,722 @@
1
+ // ── Bot comment VOLUME per PR (CORE, free, deterministic — no model, no new table) ──────────
2
+ //
3
+ // Answers two questions the ROI tab could not: "how much does this bot say on a PR" and "does it
4
+ // scale with the diff". Three getters over ONE shared base scan:
5
+ // getBotVolume → the per-bot column + workspace totals (Bots ROI tab)
6
+ // getPrBotVolume → the paginated PR drill-down behind that column
7
+ // getBotVolumeScatter → the LOC-vs-volume chart series (Bots BEHAVIOUR tab)
8
+ //
9
+ // ⚠ ALL THREE FOLD THE SAME `loadVolumeBase` RESULT. That is the point: a column, the list behind
10
+ // it and the chart beside it are three renderings of one count, so they cannot drift the way a
11
+ // tile and its drill-down drift when each runs its own scan (the failure db/bot-flagging.test.ts
12
+ // exists to prevent). Nothing client-side re-derives a count from items — the server's number is
13
+ // the number.
14
+ //
15
+ // ⚠ THE POPULATION IS **MERGED** PRs AND THE WINDOW APPLIES TO `pullRequests.mergedAt`. An open PR
16
+ // has not finished collecting bot comments, so including it drags every mean down by an amount
17
+ // that depends only on how recently the window opened; and `mergedAt` is already the anchor
18
+ // `BotVendorAnalytics.mergedPastPrs` uses, so the ROI tab keeps ONE time grain across its columns.
19
+ // Measured at 180d on the dev corpus, erxes/erxes holds 997 PRs by `openedAt` and 686 by
20
+ // `mergedAt` — the choice moves every number materially and every caption must state it.
21
+ //
22
+ // ⚠ SIZE IS SUBLINEAR IN COMMENTS, WHICH IS WHY `expected` EXISTS. Measured (erxes, 180d, mean
23
+ // bot comments per PR by bucket): 6.80 → 9.49 → 16.18 → 23.45 → 35.32 across <50 / 50-200 /
24
+ // 200-600 / 600-2k / 2k+ LOC, i.e. comments per 100 LOC FALLING monotonically — 32.91 → 8.33 →
25
+ // 4.32 → 2.08 → 0.30 aggregated (Σcomments/Σloc, the form `BotVolumeSizeBucketStat` returns), or
26
+ // 57.65 → 8.99 → 4.46 → 2.23 → 0.83 as a mean of per-PR densities. go-redis says the same thing
27
+ // louder: 0.32 → 2.08 → 3.00 → 13.00 → 28.40. So ranking PRs by raw comment count mostly ranks
28
+ // them BY SIZE (correlation of log10(LOC+1) against count: go-redis 0.615, erxes 0.539 — and
29
+ // ~nothing where bots are quiet: three.js 0.125).
30
+ //
31
+ // The product question is "where did a bot tear a PR apart", and the answer is the BUCKET-RELATIVE
32
+ // ratio. THE PROOF, on this corpus: erxes #7802 is 17 LOC across 1 file and drew 25 bot comments —
33
+ // 3.68× its bucket's expectation of 6.80. Under the default `sort=comments` it ranks **123rd of
34
+ // 686**, below every large PR; under `sort=ratio` it ranks **8th**. That is the entire reason the
35
+ // second sort exists, and why a UI that only ever shows the first has shipped a size ranking.
36
+ //
37
+ // ⚠ AND `expected` IS NOT A DENSITY. `comments per 100 LOC` was the obvious first form and it is
38
+ // unusable as a ranking: it explodes on tiny PRs by construction (#7802's is 147.06). It ships as
39
+ // `commentsPer100Loc`, never as the sort, and the aggregated per-bucket version is the only place
40
+ // it reads honestly.
41
+ //
42
+ // ⚠ KNOWN LIMIT OF THE RATIO — A NEAR-ZERO EXPECTATION INFLATES IT. In a repo where bots are
43
+ // almost silent the bucket mean can be a fraction, and a PR with 3 comments then reads 42.86×
44
+ // (measured: bevyengine/bevy #24971, 3 comments against an expectation of 0.07 over 61 PRs). That
45
+ // is arithmetically correct and genuinely notable, but it is not the same claim as erxes #7802 and
46
+ // must not be presented as one — which is why `expected` and `baselinePrs` ride every row and are
47
+ // meant to be rendered BESIDE the multiplier, never behind a tooltip. The floor below fixes small
48
+ // SAMPLES; nothing can fix a small MEAN except showing it.
49
+ //
50
+ // ⚑ ITS OWN FILE, NOT queries.ts: that file is 13k lines and CONTAINS LITERAL NUL BYTES around
51
+ // offset 132k, so every search tool silently under-reports matches inside it. Nothing imports
52
+ // this file back, so importing queries.ts here creates no cycle (the db/bot-overlap.ts
53
+ // precedent).
54
+ //
55
+ // Nothing here may feed `botVerdict` — display only (bot-analytics-verdict.test.ts pins that
56
+ // verdict's inputs).
57
+ import { and, count, desc, eq, gte, inArray, lte, ne } from 'drizzle-orm';
58
+ import { db, schema } from './client.js';
59
+ import { botWindowMs } from './bot-window.js';
60
+ import { labelFor as labelForKind } from '../sync/reviewer-classify.js';
61
+ import { automatedReviewerUserIds, classificationKindForUser, classificationLabelMap, reviewerRoleForUser, } from './queries.js';
62
+ const { prComments, pullRequests, repos, reviewComments, reviews, users } = schema;
63
+ // The merged-PR scan bound. Generous because a PR row is small and the fold is O(n): the busiest
64
+ // repo in the dev corpus contributes 997 merged PRs over 180 days, so a workspace has to be very
65
+ // large before this bites. Newest-merged-first, and `truncated` says so when it does — the
66
+ // ROLLUP_SCAN_CAP honesty rule (a capped scan reports a most-recent sample, never a total).
67
+ const VOLUME_PR_SCAN_CAP = 5000;
68
+ // The chart's own, tighter bound. This is a scatter, not a listing: 2000 points already paint a
69
+ // solid cloud, and each one is ~60 bytes of JSON. Set independently of the scan cap so the UI can
70
+ // say "the most recent 2000" without implying the aggregates were sampled too.
71
+ const SCATTER_POINT_CAP = 2000;
72
+ // Drill-down page bound (the flagging route's FLAGGING_PAGE_MAX precedent).
73
+ const VOLUME_PAGE_MAX = 50;
74
+ // ── THE SMALL-SAMPLE FLOOR ──────────────────────────────────────────────────────────────────
75
+ // A (repo × bucket) cell needs at least this many SIZED PRs before its mean may be called an
76
+ // expectation. Below it the cell degrades to the repo mean, and a repo under the floor answers
77
+ // no baseline at all. "3.7× expectation" computed off two PRs is noise dressed as a finding, and
78
+ // it is exactly the kind of number a user acts on — so the wire carries `baseline` and
79
+ // `baselinePrs` and never leaves the UI to infer sample size from a null.
80
+ const BASELINE_MIN_PRS = 5;
81
+ // ── THE EXPECTATION FLOOR (a SEPARATE guard from BASELINE_MIN_PRS above) ────────────────────
82
+ // A cell may hold plenty of PRs and still be useless as a comparison, because the floor that
83
+ // matters is the EXPECTED COUNT, not the sample size. Measured on erxes/30d: the `<50` cell held
84
+ // 43 merged PRs — comfortably past BASELINE_MIN_PRS — at a mean of 0.9 comments. A single PR
85
+ // drawing 4 comments there reads 4.4×, and four of the top eleven "most vs expected" rows were
86
+ // 3-4 raw comments against that 0.9. With λ≈0.9, P(X≥4) ≈ 1.3% per PR, so across 43 PRs you
87
+ // EXPECT about one such row from Poisson noise alone — indistinguishable, in the same list, from
88
+ // erxes #9013 (36 comments against an expectation of 6.2), which is a real finding.
89
+ //
90
+ // So below this floor the row reports its `expected` and `baselinePrs` but NO ratio: there is
91
+ // nothing here a multiplier can honestly say. Suppressing the number is the point — a greyed
92
+ // "—" that explains itself beats a confident 4.4× that is noise.
93
+ const BASELINE_MIN_EXPECTED = 3;
94
+ // ── THE BASELINE SPAN ───────────────────────────────────────────────────────────────────────
95
+ // The expectation is ALWAYS computed over this many days, regardless of the window being VIEWED.
96
+ // Two reasons, both learned from the 30d screenshots:
97
+ // 1. STABILITY. A baseline tied to the display window moves under the reader — the same PR
98
+ // reads 3.3× at 30d and something else at 7d, with nothing on screen having changed about
99
+ // the PR. A comparison that shifts when you change the view is not a comparison.
100
+ // 2. SAMPLE. At 7d most (repo × bucket) cells fall under BASELINE_MIN_PRS and degrade to the
101
+ // repo mean or to no baseline at all, so the column empties out exactly when the user
102
+ // narrows to look closely.
103
+ // The LIST is still the selected window; only what it is COMPARED AGAINST is fixed. When the
104
+ // selected window is already longer, it wins (`min(from, to − 90d)`) — never a narrower baseline
105
+ // than the rows it judges.
106
+ const BASELINE_DAYS = 90;
107
+ const BASELINE_SPAN_MS = BASELINE_DAYS * 86_400_000;
108
+ // The ONE runtime table of LOC bucket edges. Half-open [minLoc, maxLoc); `maxLoc: null` is the
109
+ // open-ended top. These are the buckets the corpus measurement was taken on — changing an edge
110
+ // invalidates every measured figure quoted in the comments above and in the shared types.
111
+ // Kept HERE and shipped on the wire (BotVolumeSizeBucketStat.minLoc/maxLoc + `label`) rather than
112
+ // duplicated into the SPA: `packages/shared` is types-only, so a client copy could only be a
113
+ // second spelling waiting to drift.
114
+ const SIZE_BUCKETS = [
115
+ { bucket: 'xs', label: '<50', minLoc: 0, maxLoc: 50 },
116
+ { bucket: 's', label: '50–200', minLoc: 50, maxLoc: 200 },
117
+ { bucket: 'm', label: '200–600', minLoc: 200, maxLoc: 600 },
118
+ { bucket: 'l', label: '600–2k', minLoc: 600, maxLoc: 2000 },
119
+ { bucket: 'xl', label: '2k+', minLoc: 2000, maxLoc: null },
120
+ ];
121
+ function bucketFor(loc) {
122
+ for (const b of SIZE_BUCKETS) {
123
+ if (b.maxLoc == null || loc < b.maxLoc)
124
+ return b.bucket;
125
+ }
126
+ // Unreachable — the last bucket is open-ended. Non-null return keeps the caller honest.
127
+ return 'xl';
128
+ }
129
+ /** 2dp, or null. Money-style rounding is deliberate: these are display figures, and an
130
+ * unrounded 16.888888888888889 in a table cell is worse than useless. */
131
+ function round2(n) {
132
+ return Math.round(n * 100) / 100;
133
+ }
134
+ function avg2(sum, n) {
135
+ return n > 0 ? round2(sum / n) : null;
136
+ }
137
+ function emptyBase(windowKind, from, to, generatedAt) {
138
+ return {
139
+ windowKind,
140
+ from,
141
+ to,
142
+ generatedAt,
143
+ prs: [],
144
+ truncated: false,
145
+ automatedIds: [],
146
+ repoFullName: new Map(),
147
+ };
148
+ }
149
+ /**
150
+ * The merged-PR population plus its per-(PR, bot) comment counts.
151
+ *
152
+ * ⚠ THE BOT SET IS ROLE `'all'`, NOT `'review'` — the ROI / flagging convention. Quality checks
153
+ * (SonarQube, Codecov …) post exactly the kind of text this counts, and a drill-down that
154
+ * narrowed the bot set would list fewer comments than the column it was opened from. The response
155
+ * carries each bot's `role` so the UI can split them the way `BotAnalyticsResponse` splits
156
+ * `vendors` from `qualityChecks`; it does NOT pre-split, because the chart wants the whole cloud.
157
+ * (The Behaviour tab's own panels use `'review'` — this file deliberately does not follow them.)
158
+ */
159
+ async function loadVolumeBase(accountId, window, scope) {
160
+ const nowMs = Date.now();
161
+ const windowKind = typeof window === 'string' ? window : window.kind;
162
+ // The one shared window→duration mapping (db/bot-window.ts), unless the caller supplied real
163
+ // bounds — the `getBotAnalytics` window contract verbatim, so the Pro Insights chat's ranges
164
+ // and a verification script can both reach a span the enum cannot spell.
165
+ const to = new Date(typeof window === 'string' ? nowMs : window.toMs);
166
+ const from = new Date(typeof window === 'string' ? nowMs - botWindowMs(window) : window.fromMs);
167
+ const generatedAt = new Date(nowMs).toISOString();
168
+ // The SCAN reaches back to whichever is earlier: the window's own start, or BASELINE_DAYS
169
+ // before its end. Everything loaded feeds the expectation; only `inWindow` rows are reported.
170
+ // One scan, not two — a second query for the baseline would double the cost of every call to
171
+ // buy nothing, since the wider span is a strict superset.
172
+ const scanFrom = new Date(Math.min(from.getTime(), to.getTime() - BASELINE_SPAN_MS));
173
+ // `repoIds: []` is a real empty workspace, never "widen to the account".
174
+ if (scope.repoIds.length === 0)
175
+ return emptyBase(windowKind, from, to, generatedAt);
176
+ const automatedIds = await automatedReviewerUserIds(accountId, scope.workspaceId, 'all');
177
+ if (automatedIds.length === 0) {
178
+ // No bots ⇒ no comments, but the PR population is still real and `totals.prs` must report it
179
+ // (a workspace with 800 merged PRs and no bots reads "0 of 800", not "no data").
180
+ const bare = await loadMergedPrs(accountId, scope, scanFrom, to, from);
181
+ return { windowKind, from, to, generatedAt, ...bare, automatedIds: [] };
182
+ }
183
+ const bare = await loadMergedPrs(accountId, scope, scanFrom, to, from);
184
+ const byId = new Map(bare.prs.map((p) => [p.id, p]));
185
+ // The three text kinds, one grouped count each. Deliberately NOT one clever aggregate:
186
+ // `count(*) FILTER (WHERE …)` is not portable across the two dialects (the listDetectedReviewers
187
+ // precedent), and three indexed group-bys are cheap.
188
+ //
189
+ // ⚠ EACH RE-APPLIES THE FULL WINDOW + SCOPE PREDICATE against `pullRequests` rather than an
190
+ // `inArray` over the scanned PR ids: the id list can reach VOLUME_PR_SCAN_CAP, which is far past
191
+ // sqlite's bound-parameter ceiling. Rows whose PR fell outside the (capped) scan simply find no
192
+ // entry in `byId` and are dropped.
193
+ const prPred = and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), gte(pullRequests.mergedAt, scanFrom), lte(pullRequests.mergedAt, to));
194
+ const [rcRows, pcRows, rvRows] = await Promise.all([
195
+ // Inline review comments. Joined on `reviewComments.prId` — provably identical to routing
196
+ // through `reviewThreads.prId` (sync writes both from the same thread, and 0 of 21,475 rows
197
+ // in the dev corpus disagree), and `rc_pr_node` makes `pr_id` a leading indexed column while
198
+ // the thread hop would be an extra join per row.
199
+ db
200
+ .select({ prId: reviewComments.prId, authorId: reviewComments.authorId, c: count() })
201
+ .from(reviewComments)
202
+ .innerJoin(pullRequests, eq(pullRequests.id, reviewComments.prId))
203
+ .where(and(prPred, inArray(reviewComments.authorId, automatedIds)))
204
+ .groupBy(reviewComments.prId, reviewComments.authorId)
205
+ .execute(),
206
+ db
207
+ .select({ prId: prComments.prId, authorId: prComments.authorId, c: count() })
208
+ .from(prComments)
209
+ .innerJoin(pullRequests, eq(pullRequests.id, prComments.prId))
210
+ .where(and(prPred, inArray(prComments.authorId, automatedIds)))
211
+ .groupBy(prComments.prId, prComments.authorId)
212
+ .execute(),
213
+ // Submitted review BODIES (the walkthrough / summary post).
214
+ //
215
+ // ⚠ A BODILESS REVIEW COUNTS. A bare approval with no text is one row here, and that is the
216
+ // definition the corpus measurement these surfaces were specced against used. The measured
217
+ // cost of the choice: erxes/erxes 16.89 vs 15.98 avg, go-redis 4.91 vs 4.32 — and the
218
+ // zero-comment PR counts are IDENTICAL under both, so no product signal turns on it. To flip
219
+ // it, add a non-empty-body predicate HERE and nowhere else, and restate the numbers quoted at
220
+ // the top of this file.
221
+ //
222
+ // `pending` reviews are drafts, invisible on GitHub, and are excluded — the same predicate
223
+ // getBotAnalytics uses for review activity.
224
+ db
225
+ .select({ prId: reviews.prId, authorId: reviews.authorId, c: count() })
226
+ .from(reviews)
227
+ .innerJoin(pullRequests, eq(pullRequests.id, reviews.prId))
228
+ .where(and(prPred, inArray(reviews.authorId, automatedIds), ne(reviews.state, 'pending')))
229
+ .groupBy(reviews.prId, reviews.authorId)
230
+ .execute(),
231
+ ]);
232
+ for (const rows of [rcRows, pcRows, rvRows]) {
233
+ for (const r of rows) {
234
+ if (r.authorId == null)
235
+ continue;
236
+ const pr = byId.get(r.prId);
237
+ if (!pr)
238
+ continue; // outside the capped scan
239
+ pr.byBot.set(r.authorId, (pr.byBot.get(r.authorId) ?? 0) + r.c);
240
+ pr.total += r.c;
241
+ }
242
+ }
243
+ return { windowKind, from, to, generatedAt, ...bare, automatedIds };
244
+ }
245
+ /** The PR half of the base — split out so the no-bots early return can reuse it verbatim. */
246
+ async function loadMergedPrs(accountId, scope,
247
+ // The SCAN span — reaches back to the baseline horizon, which is at or before `windowFrom`.
248
+ from, to,
249
+ // The SELECTED window's start. Rows merged before it are loaded to condition the expectation
250
+ // and are flagged `inWindow: false`; every user-facing fold must drop them.
251
+ windowFrom) {
252
+ const rows = await db
253
+ .select({
254
+ id: pullRequests.id,
255
+ repoId: pullRequests.repoId,
256
+ number: pullRequests.number,
257
+ title: pullRequests.title,
258
+ openedAt: pullRequests.openedAt,
259
+ mergedAt: pullRequests.mergedAt,
260
+ additions: pullRequests.additions,
261
+ deletions: pullRequests.deletions,
262
+ changedFiles: pullRequests.changedFiles,
263
+ owner: repos.owner,
264
+ name: repos.name,
265
+ })
266
+ .from(pullRequests)
267
+ .innerJoin(repos, eq(repos.id, pullRequests.repoId))
268
+ .where(and(eq(pullRequests.accountId, accountId), inArray(pullRequests.repoId, scope.repoIds), gte(pullRequests.mergedAt, from), lte(pullRequests.mergedAt, to)))
269
+ // Newest-merged first: both caps slice from the front, so a truncated scan is a RECENT
270
+ // sample rather than an arbitrary one. `pullRequests.id` breaks ties so the cap is
271
+ // deterministic across requests (two PRs can share a merge second).
272
+ .orderBy(desc(pullRequests.mergedAt), desc(pullRequests.id))
273
+ .limit(VOLUME_PR_SCAN_CAP)
274
+ .execute();
275
+ const repoFullName = new Map();
276
+ const prs = [];
277
+ for (const r of rows) {
278
+ // `mergedAt` cannot be null here (the gte/lte predicate excludes nulls in both dialects), but
279
+ // the column IS nullable, so the narrowing is explicit rather than a `!`.
280
+ if (!r.mergedAt)
281
+ continue;
282
+ repoFullName.set(r.repoId, `${r.owner}/${r.name}`);
283
+ // ⚠ UNKNOWN SIZE, NOT ZERO SIZE. Under lean storage these three columns default to 0, so a PR
284
+ // whose detail never hydrated looks exactly like an empty one. 135 of three.js's 796 merged
285
+ // PRs are in this state in the dev corpus — fabricating a 0 would drop every one of them into
286
+ // the `xs` bucket, where the smallest expectation lives, and manufacture spectacular ratios.
287
+ const observed = r.changedFiles > 0 || r.additions > 0 || r.deletions > 0;
288
+ const loc = observed ? r.additions + r.deletions : null;
289
+ prs.push({
290
+ id: r.id,
291
+ repoId: r.repoId,
292
+ number: r.number,
293
+ title: r.title,
294
+ openedAt: r.openedAt,
295
+ mergedAt: r.mergedAt,
296
+ additions: r.additions,
297
+ deletions: r.deletions,
298
+ changedFiles: r.changedFiles,
299
+ loc,
300
+ bucket: loc == null ? null : bucketFor(loc),
301
+ inWindow: r.mergedAt.getTime() >= windowFrom.getTime(),
302
+ byBot: new Map(),
303
+ total: 0,
304
+ });
305
+ }
306
+ return { prs, truncated: rows.length >= VOLUME_PR_SCAN_CAP, repoFullName };
307
+ }
308
+ export async function loadBotIdentities(accountId, scope, automatedIds) {
309
+ const out = new Map();
310
+ if (automatedIds.length === 0)
311
+ return out;
312
+ const [kindMap, roleMap, classLabel, userRows] = await Promise.all([
313
+ classificationKindForUser(accountId, scope.workspaceId),
314
+ reviewerRoleForUser(accountId, scope.workspaceId),
315
+ classificationLabelMap(accountId, scope.workspaceId),
316
+ db
317
+ .select({ id: users.id, login: users.githubLogin, name: users.displayName })
318
+ .from(users)
319
+ .where(inArray(users.id, automatedIds))
320
+ .execute(),
321
+ ]);
322
+ const display = new Map();
323
+ const login = new Map();
324
+ for (const u of userRows) {
325
+ display.set(u.id, u.name?.trim() || u.login || `#${u.id}`);
326
+ login.set(u.id, u.login || null);
327
+ }
328
+ for (const id of automatedIds) {
329
+ const kind = kindMap.get(id) ?? 'in_house';
330
+ // getBotAnalytics' `reviewerLabel` precedence verbatim — custom workspace label → vendor
331
+ // pretty name → display name/login — so one bot reads with one name across the Bots interface.
332
+ const custom = classLabel.get(id);
333
+ const label = custom ??
334
+ (kind !== 'in_house' && kind !== 'pierre' && kind !== 'vendor'
335
+ ? labelForKind(kind)
336
+ : (display.get(id) ?? labelForKind(kind)));
337
+ out.set(id, {
338
+ label,
339
+ login: login.get(id) ?? null,
340
+ kind,
341
+ role: roleMap.get(id) ?? 'review',
342
+ });
343
+ }
344
+ return out;
345
+ }
346
+ // ── (1) The per-bot column + workspace totals ───────────────────────────────────────────────
347
+ export async function getBotVolume(accountId, window, scope) {
348
+ const base = await loadVolumeBase(accountId, window, scope);
349
+ const win = {
350
+ kind: base.windowKind,
351
+ from: base.from.toISOString(),
352
+ to: base.to.toISOString(),
353
+ };
354
+ const byBot = new Map();
355
+ let comments = 0;
356
+ let sizedPrs = 0;
357
+ let prsWithBotComments = 0;
358
+ let maxOnOnePr = 0;
359
+ // ⚠ WINDOW ONLY. `base.prs` reaches back to the baseline horizon (BASELINE_DAYS) so the
360
+ // expectation has a stable sample; every number below is reported to the user under the
361
+ // SELECTED window's heading, so it must fold `windowPrs`, never `base.prs`.
362
+ const windowPrs = base.prs.filter((p) => p.inWindow);
363
+ for (const pr of windowPrs) {
364
+ if (pr.loc != null)
365
+ sizedPrs += 1;
366
+ if (pr.total > 0)
367
+ prsWithBotComments += 1;
368
+ if (pr.total > maxOnOnePr)
369
+ maxOnOnePr = pr.total;
370
+ comments += pr.total;
371
+ for (const [userId, n] of pr.byBot) {
372
+ let a = byBot.get(userId);
373
+ if (!a) {
374
+ a = { comments: 0, prs: new Set(), max: 0 };
375
+ byBot.set(userId, a);
376
+ }
377
+ a.comments += n;
378
+ a.prs.add(pr.id);
379
+ if (n > a.max)
380
+ a.max = n;
381
+ }
382
+ }
383
+ const identities = await loadBotIdentities(accountId, scope, [...byBot.keys()]);
384
+ const prs = windowPrs.length;
385
+ const bots = [...byBot.entries()]
386
+ .map(([userId, a]) => {
387
+ const id = identities.get(userId);
388
+ return {
389
+ key: `u${userId}`,
390
+ authorUserId: userId,
391
+ label: id?.label ?? `#${userId}`,
392
+ login: id?.login ?? null,
393
+ kind: id?.kind ?? 'in_house',
394
+ role: id?.role ?? 'review',
395
+ comments: a.comments,
396
+ prsCommentedOn: a.prs.size,
397
+ // ⚠ TWO DENOMINATORS, NAMED APART ON PURPOSE — see the shared type. On three.js the same
398
+ // bot reads ~6× higher per-commented-PR than per-scope-PR (656 of 796 merged PRs draw
399
+ // nothing), and nothing in the number itself tells them apart.
400
+ avgCommentsPerCommentedPr: avg2(a.comments, a.prs.size),
401
+ avgCommentsPerScopePr: avg2(a.comments, prs),
402
+ maxCommentsOnOnePr: a.max,
403
+ };
404
+ })
405
+ // Most comments first; label as a stable tiebreak so two equal bots keep a fixed order
406
+ // across requests (a table that reshuffles on refresh reads as data changing).
407
+ .sort((x, y) => y.comments - x.comments || x.label.localeCompare(y.label));
408
+ const totals = {
409
+ prs,
410
+ sizedPrs,
411
+ comments,
412
+ prsWithBotComments,
413
+ prsWithNoBotComments: prs - prsWithBotComments,
414
+ avgCommentsPerCommentedPr: avg2(comments, prsWithBotComments),
415
+ avgCommentsPerScopePr: avg2(comments, prs),
416
+ maxCommentsOnOnePr: maxOnOnePr,
417
+ };
418
+ return {
419
+ workspaceId: scope.workspaceId,
420
+ window: win,
421
+ bots,
422
+ totals,
423
+ truncated: base.truncated,
424
+ generatedAt: base.generatedAt,
425
+ };
426
+ }
427
+ /**
428
+ * Build both baselines from a per-PR count function.
429
+ *
430
+ * ⚠ IT TAKES `countOf` RATHER THAN READING `pr.total` because `refine.authorUserIds` MOVES THE
431
+ * BASELINE. Comparing one bot's 12 comments against every bot's combined expectation would make
432
+ * every single-bot ratio read low, and uniformly so — the most convincing kind of wrong number.
433
+ */
434
+ function buildBaselines(prs, countOf) {
435
+ const byRepoBucket = new Map();
436
+ const byRepo = new Map();
437
+ for (const pr of prs) {
438
+ const n = countOf(pr);
439
+ const repo = byRepo.get(pr.repoId) ?? { n: 0, sum: 0 };
440
+ repo.n += 1;
441
+ repo.sum += n;
442
+ byRepo.set(pr.repoId, repo);
443
+ if (pr.bucket == null)
444
+ continue; // unsized PRs condition nothing
445
+ const key = `${pr.repoId}:${pr.bucket}`;
446
+ const cell = byRepoBucket.get(key) ?? { n: 0, sum: 0 };
447
+ cell.n += 1;
448
+ cell.sum += n;
449
+ byRepoBucket.set(key, cell);
450
+ }
451
+ return { byRepoBucket, byRepo };
452
+ }
453
+ function expectationFor(pr, baselines) {
454
+ // No observed size ⇒ no size-conditioned claim, and the repo mean would be answering a question
455
+ // nobody asked (it is a FALLBACK for a thin bucket, not a size-free expectation).
456
+ if (pr.bucket == null)
457
+ return { expected: null, baseline: 'none', baselinePrs: 0 };
458
+ const cell = baselines.byRepoBucket.get(`${pr.repoId}:${pr.bucket}`);
459
+ if (cell && cell.n >= BASELINE_MIN_PRS) {
460
+ const expected = round2(cell.sum / cell.n);
461
+ // ⚠ SAMPLE SIZE PASSED; THE EXPECTATION ITSELF IS TOO SMALL TO DIVIDE BY. Reported as its own
462
+ // kind rather than folded into 'none' so the row can still SHOW what it was measured against
463
+ // (`expected` + `baselinePrs` stay populated) while withholding the multiplier. The two
464
+ // failures read identically as a null ratio and are not the same fact: 'none' means "we could
465
+ // not find comparable PRs", this means "we found plenty and they are all near zero".
466
+ if (expected < BASELINE_MIN_EXPECTED) {
467
+ return { expected, baseline: 'low_expectation', baselinePrs: cell.n };
468
+ }
469
+ return { expected, baseline: 'bucket', baselinePrs: cell.n };
470
+ }
471
+ const repo = baselines.byRepo.get(pr.repoId);
472
+ if (repo && repo.n >= BASELINE_MIN_PRS) {
473
+ const expected = round2(repo.sum / repo.n);
474
+ if (expected < BASELINE_MIN_EXPECTED) {
475
+ return { expected, baseline: 'low_expectation', baselinePrs: repo.n };
476
+ }
477
+ return { expected, baseline: 'repo', baselinePrs: repo.n };
478
+ }
479
+ return { expected: null, baseline: 'none', baselinePrs: 0 };
480
+ }
481
+ function ratioFor(botComments, expected, baseline) {
482
+ // The expectation floor is enforced HERE, at the one place a multiplier is produced, so no
483
+ // caller can route around it by reading `expected` and dividing for itself.
484
+ if (baseline === 'low_expectation')
485
+ return null;
486
+ // `expected === 0` cannot hide a finding: the PR is a member of its own baseline population, so
487
+ // a mean of 0 forces this PR's own count to 0 too. Reporting null beats reporting Infinity or a
488
+ // fabricated 0 — both of which would sort.
489
+ if (expected == null || expected === 0)
490
+ return null;
491
+ return round2(botComments / expected);
492
+ }
493
+ export async function foldPrBotVolumePopulation(accountId, window, scope, refine, sort) {
494
+ const base = await loadVolumeBase(accountId, window, scope);
495
+ const win = {
496
+ kind: base.windowKind,
497
+ from: base.from.toISOString(),
498
+ to: base.to.toISOString(),
499
+ };
500
+ // ⚠ `[]` MEANS "NO BOTS", NEVER "EVERY BOT" — the `repoIds` rule (`if (ids)`, never
501
+ // `ids.length > 0`), and the BotFlaggingRefine contract restated. Only `null` widens.
502
+ const only = refine.authorUserIds == null ? null : new Set(refine.authorUserIds);
503
+ const countOf = (pr) => {
504
+ if (!only)
505
+ return pr.total;
506
+ let n = 0;
507
+ for (const [userId, c] of pr.byBot)
508
+ if (only.has(userId))
509
+ n += c;
510
+ return n;
511
+ };
512
+ // ⚠ THE BASELINE IS THE ONE FOLD ENTITLED TO THE UNFILTERED SET. It spans BASELINE_DAYS on
513
+ // purpose (see BASELINE_SPAN_MS): an expectation tied to the display window moves under the
514
+ // reader and empties out at 7d. Rebuilt over the SAME narrowed bot set — see buildBaselines.
515
+ const baselines = buildBaselines(base.prs, countOf);
516
+ // …and the LIST is the selected window, nothing wider.
517
+ const windowPrs = base.prs.filter((p) => p.inWindow);
518
+ // Only PRs the refined bot set actually touched are enumerated. `total` stays the whole merged
519
+ // population so the caption can say "140 of 796 merged PRs drew bot comments" — the zero rows
520
+ // are the point of that sentence, not rows to page through (three.js: 656 of them).
521
+ const scored = [];
522
+ for (const pr of windowPrs) {
523
+ const botComments = countOf(pr);
524
+ if (botComments === 0)
525
+ continue;
526
+ const { expected, baseline, baselinePrs } = expectationFor(pr, baselines);
527
+ scored.push({
528
+ pr,
529
+ botComments,
530
+ expected,
531
+ ratio: ratioFor(botComments, expected, baseline),
532
+ baseline,
533
+ baselinePrs,
534
+ });
535
+ }
536
+ // ⚠ EVERY COMPARATOR IS TOTAL. The offset is a slice over this fold, so two rows that compare
537
+ // equal must still hold a fixed order or a cursor walk silently duplicates and drops rows.
538
+ // `prId` ascending is the final tiebreak in both sorts.
539
+ //
540
+ // Under `'ratio'`, rows with NO baseline sort LAST rather than as 0: an unmeasurable PR is not
541
+ // a below-average one, and burying the null rows is the only reading that keeps the top of the
542
+ // list meaningful.
543
+ scored.sort((a, b) => {
544
+ if (sort === 'ratio') {
545
+ const ar = a.ratio;
546
+ const br = b.ratio;
547
+ if (ar == null && br != null)
548
+ return 1;
549
+ if (br == null && ar != null)
550
+ return -1;
551
+ if (ar != null && br != null && ar !== br)
552
+ return br - ar;
553
+ }
554
+ if (b.botComments !== a.botComments)
555
+ return b.botComments - a.botComments;
556
+ const at = a.pr.mergedAt.getTime();
557
+ const bt = b.pr.mergedAt.getTime();
558
+ if (bt !== at)
559
+ return bt - at;
560
+ return a.pr.id - b.pr.id;
561
+ });
562
+ return {
563
+ win,
564
+ generatedAt: base.generatedAt,
565
+ scored,
566
+ windowPrCount: windowPrs.length,
567
+ truncated: base.truncated,
568
+ repoFullName: base.repoFullName,
569
+ only,
570
+ };
571
+ }
572
+ export async function getPrBotVolume(accountId, window, scope, refine, page) {
573
+ const offset = Math.max(0, Math.trunc(page.offset));
574
+ const limit = Math.max(1, Math.min(Math.trunc(page.limit), VOLUME_PAGE_MAX));
575
+ // The ONE population fold (shared with the synthesis input — §8.3).
576
+ const pop = await foldPrBotVolumePopulation(accountId, window, scope, refine, page.sort);
577
+ const { scored, only } = pop;
578
+ const pageRows = scored.slice(offset, offset + limit);
579
+ const consumed = offset + pageRows.length;
580
+ // Identities only for the bots that actually appear on THIS page — the drill-down spans many
581
+ // PRs, so a per-row lookup is out, but so is resolving every bot in the workspace.
582
+ const pageBotIds = new Set();
583
+ for (const s of pageRows) {
584
+ for (const [userId] of s.pr.byBot) {
585
+ if (!only || only.has(userId))
586
+ pageBotIds.add(userId);
587
+ }
588
+ }
589
+ const identities = await loadBotIdentities(accountId, scope, [...pageBotIds]);
590
+ const items = pageRows.map((s) => {
591
+ const byBot = [...s.pr.byBot.entries()]
592
+ .filter(([userId]) => !only || only.has(userId))
593
+ .map(([userId, comments]) => ({
594
+ key: `u${userId}`,
595
+ authorUserId: userId,
596
+ label: identities.get(userId)?.label ?? `#${userId}`,
597
+ comments,
598
+ }))
599
+ .sort((x, y) => y.comments - x.comments || x.label.localeCompare(y.label));
600
+ const repoFullName = pop.repoFullName.get(s.pr.repoId) ?? `#${s.pr.repoId}`;
601
+ return {
602
+ prId: s.pr.id,
603
+ prNumber: s.pr.number,
604
+ prTitle: s.pr.title,
605
+ prUrl: `https://github.com/${repoFullName}/pull/${s.pr.number}`,
606
+ repoId: s.pr.repoId,
607
+ repoFullName,
608
+ createdAt: s.pr.openedAt.toISOString(),
609
+ mergedAt: s.pr.mergedAt.toISOString(),
610
+ additions: s.pr.additions,
611
+ deletions: s.pr.deletions,
612
+ loc: s.pr.loc,
613
+ // Null in lockstep with `loc`: the two are unknown together (they come from the same
614
+ // unhydrated row), and a lone `changedFiles: 0` beside a null LOC reads as a real zero.
615
+ changedFiles: s.pr.loc == null ? null : s.pr.changedFiles,
616
+ botComments: s.botComments,
617
+ byBot,
618
+ sizeBucket: s.pr.bucket,
619
+ expected: s.expected,
620
+ ratio: s.ratio,
621
+ baseline: s.baseline,
622
+ baselinePrs: s.baselinePrs,
623
+ // The raw density, shipped but never ranked on — it explodes on small PRs by construction
624
+ // (see the file header). `max(loc, 1)` keeps a rename-only PR finite.
625
+ commentsPer100Loc: s.pr.loc == null ? null : round2((s.botComments / Math.max(s.pr.loc, 1)) * 100),
626
+ };
627
+ });
628
+ return {
629
+ workspaceId: scope.workspaceId,
630
+ window: pop.win,
631
+ refine,
632
+ sort: page.sort,
633
+ total: pop.windowPrCount,
634
+ filteredTotal: scored.length,
635
+ items,
636
+ // Opaque on the wire; `o:<n>` is today's encoding of "offset into the sorted fold".
637
+ nextCursor: consumed < scored.length ? `o:${consumed}` : null,
638
+ truncated: pop.truncated,
639
+ generatedAt: pop.generatedAt,
640
+ };
641
+ }
642
+ // ── (3) The LOC-vs-volume chart series ──────────────────────────────────────────────────────
643
+ export async function getBotVolumeScatter(accountId, window, scope) {
644
+ const base = await loadVolumeBase(accountId, window, scope);
645
+ const win = {
646
+ kind: base.windowKind,
647
+ from: base.from.toISOString(),
648
+ to: base.to.toISOString(),
649
+ };
650
+ // Bucket aggregates cover EVERY sized PR the SCAN returned, independently of the point cap — the
651
+ // expectation curve must describe the whole corpus even when the cloud beneath it is a sample.
652
+ //
653
+ // ⚠ "the scan returned", not "the scope holds": when `base.truncated` is set the scan itself
654
+ // stopped at VOLUME_PR_SCAN_CAP, so these means describe the most-recent 5000 merged PRs and not
655
+ // the window. That is reported as `scanTruncated` — a field DISTINCT from `truncated` (the point
656
+ // cap), because the two weaken different claims: `truncated` means "not every dot is drawn",
657
+ // `scanTruncated` means "the curve those dots are judged against is itself a sample". Folding
658
+ // them into one flag would leave a reader unable to tell which, and a sampled baseline presented
659
+ // as the scope's baseline is precisely the sin the post-merge autopsy was deleted for.
660
+ const agg = new Map();
661
+ const points = [];
662
+ let sizedPrs = 0;
663
+ let unsizedPrs = 0;
664
+ // ⚠ WINDOW ONLY — this card is captioned with the selected window, so its bars must describe
665
+ // that window. The wider scan exists for the drill-down's expectation, not for this chart.
666
+ const windowPrs = base.prs.filter((p) => p.inWindow);
667
+ for (const pr of windowPrs) {
668
+ if (pr.loc == null || pr.bucket == null) {
669
+ unsizedPrs += 1;
670
+ continue;
671
+ }
672
+ sizedPrs += 1;
673
+ const a = agg.get(pr.bucket) ?? { prs: 0, comments: 0, loc: 0 };
674
+ a.prs += 1;
675
+ a.comments += pr.total;
676
+ a.loc += Math.max(pr.loc, 1);
677
+ agg.set(pr.bucket, a);
678
+ // `base.prs` is newest-merged first, so the cap keeps the most recent points.
679
+ if (points.length < SCATTER_POINT_CAP) {
680
+ points.push({
681
+ prId: pr.id,
682
+ repoId: pr.repoId,
683
+ loc: pr.loc,
684
+ changedFiles: pr.changedFiles,
685
+ botComments: pr.total,
686
+ });
687
+ }
688
+ }
689
+ // Dense: every bucket present even at zero, so a gap in the curve is a gap in the DATA and not
690
+ // an omitted key the chart has to guess about.
691
+ const buckets = SIZE_BUCKETS.map((b) => {
692
+ const a = agg.get(b.bucket) ?? { prs: 0, comments: 0, loc: 0 };
693
+ return {
694
+ bucket: b.bucket,
695
+ label: b.label,
696
+ minLoc: b.minLoc,
697
+ maxLoc: b.maxLoc,
698
+ prs: a.prs,
699
+ comments: a.comments,
700
+ avgComments: avg2(a.comments, a.prs),
701
+ // The ONE place a density belongs: aggregated per bucket it is stable and it is exactly the
702
+ // sublinearity readout (measured on erxes: 32.91 → 8.33 → 4.32 → 2.08 → 0.30).
703
+ commentsPer100Loc: a.loc > 0 ? round2((a.comments / a.loc) * 100) : null,
704
+ };
705
+ });
706
+ return {
707
+ workspaceId: scope.workspaceId,
708
+ window: win,
709
+ points,
710
+ buckets,
711
+ sizedPrs,
712
+ unsizedPrs,
713
+ // The POINT cap, not the scan cap — reported separately so "showing the most recent 2000"
714
+ // does not imply the aggregates beside it were sampled too.
715
+ truncated: sizedPrs > points.length,
716
+ // The SCAN cap, straight off the shared loader — the same bit `/volume` and `/volume/prs`
717
+ // surface, so all three routes agree about whether the window was fully walked.
718
+ scanTruncated: base.truncated,
719
+ generatedAt: base.generatedAt,
720
+ };
721
+ }
722
+ //# sourceMappingURL=bot-volume.js.map