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,661 @@
1
+ // ── CHRONOLOGY, PR BY PR: working hours, budgets, and what the slow ones have in common ──────
2
+ //
3
+ // `db/pr-intervals.ts` replays each merged pull request's life and charges every interval to a
4
+ // court. This module takes those replays — one `FlowPrFacts` per measured pull request — and
5
+ // answers the questions the share bar could not:
6
+ //
7
+ // · how long each wait took in WORKING hours, against a budget (`budgets`);
8
+ // · which pull requests carry the weight (`prs`, for the scatter);
9
+ // · what the slowest quarter has that the fastest does not (`contrast`, `sizeBands`, `weekdays`);
10
+ // · which approved pull requests sat waiting, and whether a ticket explains it (`landingTail`);
11
+ // · how much of each repository's first reviewing rests on one person (`concentration`).
12
+ //
13
+ // ⚠ PURE AND DETERMINISTIC. No database, no clock, no model: everything arrives in the facts and
14
+ // the calendar, so the unit tests drive it directly and every sentence is templated here.
15
+ //
16
+ // ⚠ NO PERSON LEAVES THIS MODULE. `firstReviewerId` is read to COUNT, never copied onto the wire:
17
+ // the concentration rows carry shares and medians, and the per-PR rows carry no actor at all. A
18
+ // repository with one first reviewer can still be deduced by a colleague; that is the accepted
19
+ // cost of saying "one person does all the first reviews here", which is the finding.
20
+ //
21
+ // ⚠ WORKING HOURS HERE, CLOCK HOURS IN THE REPO ROWS. The rule that names a repository ("lopsided
22
+ // AND slow", FLOW_SLOW_P75_HOURS) was calibrated on clock hours over 66,088 public pull requests.
23
+ // It stays on clock hours, and the screen labels those rows as such.
24
+ import { FLOW_BUDGET_MEASURES, FLOW_RULES, } from '../shared/index.js';
25
+ const HOUR_MS = 3_600_000;
26
+ // ── Floors and caps ─────────────────────────────────────────────────────────────────────────
27
+ // ⚠ Every floor the page QUOTES is assigned from `FLOW_RULES` (packages/shared/src/flow-settings.ts),
28
+ // so the explanation behind each "i" and the fold cannot drift apart. The caps below it are not
29
+ // quoted anywhere and stay local.
30
+ /** A budget verdict needs this many pull requests behind its percentile. */
31
+ const FLOW_BUDGET_MIN_PRS = FLOW_RULES.budgetMinPrs;
32
+ /** The contrast compares quarters, so it needs this many in each. */
33
+ const FLOW_CONTRAST_MIN_QUARTILE = FLOW_RULES.contrastMinQuartile;
34
+ /** A size band or weekday needs this many before its median is shown. */
35
+ const FLOW_CUT_MIN_PRS = FLOW_RULES.cutMinPrs;
36
+ /** Per-PR rows on the wire. Past it: every slow PR, and an even sample of the rest. */
37
+ const FLOW_PR_ROWS_CAP = 1_000;
38
+ const FLOW_PR_ROWS_SLOW_KEEP = 250;
39
+ const FLOW_LANDING_ROWS_CAP = FLOW_RULES.landingRows;
40
+ const FLOW_SIBLINGS_CAP = 5;
41
+ /** A sibling merging this long after the PR itself still counts — they often land together. */
42
+ const SIBLING_AFTER_MS = 24 * HOUR_MS;
43
+ /** Concentration: each side of the comparison needs this many first looks. */
44
+ const FLOW_CONCENTRATION_MIN_SIDE = FLOW_RULES.concentrationMinSide;
45
+ /** "Slower" means at least this much slower than everyone else, and by at least half an hour. */
46
+ const FLOW_CONCENTRATION_SLOWER_RATIO = FLOW_RULES.slowerRatio;
47
+ const FLOW_CONCENTRATION_SLOWER_MIN_HOURS = FLOW_RULES.slowerMinHours;
48
+ /** A contrast row "separates" at 2×, is "weak" from 1.3×. */
49
+ const FLOW_SEPARATES_RATIO = FLOW_RULES.separatesRatio;
50
+ const FLOW_WEAK_RATIO = FLOW_RULES.weakRatio;
51
+ /** Size bands in lines added plus removed — fixed edges, this workspace's own medians. */
52
+ const SIZE_BANDS = [
53
+ { label: 'Up to 100 lines', min: 0, max: 100 },
54
+ { label: '101–400 lines', min: 101, max: 400 },
55
+ { label: '401–1,000 lines', min: 401, max: 1000 },
56
+ { label: 'Over 1,000 lines', min: 1001, max: null },
57
+ ];
58
+ // ── Small helpers ───────────────────────────────────────────────────────────────────────────
59
+ /** Nearest-rank percentile, the same definition `db/pr-intervals.ts` uses. */
60
+ function percentile(xs, p) {
61
+ if (xs.length === 0)
62
+ return 0;
63
+ const s = [...xs].sort((a, b) => a - b);
64
+ const i = Math.min(s.length - 1, Math.max(0, Math.ceil(p * s.length) - 1));
65
+ return s[i] ?? 0;
66
+ }
67
+ function median(xs) {
68
+ return percentile(xs, 0.5);
69
+ }
70
+ function medianOrNull(xs, floor) {
71
+ return xs.length >= floor ? median(xs) : null;
72
+ }
73
+ /** Two decimals on the wire — enough for a chart, and it halves the payload. */
74
+ function r2(n) {
75
+ return Math.round(n * 100) / 100;
76
+ }
77
+ function oneDp(n) {
78
+ return n.toFixed(1).replace(/\.0$/, '');
79
+ }
80
+ function pct(share) {
81
+ return `${Math.round(share * 100)}%`;
82
+ }
83
+ function plural(n, one, many) {
84
+ return n === 1 ? one : many;
85
+ }
86
+ /** A working duration in prose. Past two working days it reads in working days. */
87
+ export function fmtWork(h, dayHours) {
88
+ if (h < 1)
89
+ return `${Math.max(1, Math.round(h * 60))} minutes`;
90
+ if (dayHours > 0 && h >= 2 * dayHours)
91
+ return `${oneDp(h / dayHours)} working days`;
92
+ const n = h < 10 ? oneDp(h) : String(Math.round(h));
93
+ return `${n} working ${n === '1' ? 'hour' : 'hours'}`;
94
+ }
95
+ function budgetHours(h) {
96
+ return `${oneDp(h)}-hour`;
97
+ }
98
+ function workOf(f, cal) {
99
+ const work = { reviewer: 0, author: 0, landing: 0 };
100
+ const clock = { reviewer: 0, author: 0, landing: 0 };
101
+ let longestReply = null;
102
+ for (const s of f.spells) {
103
+ const w = cal.between(s.fromMs, s.toMs);
104
+ work[s.court] += w;
105
+ clock[s.court] += (s.toMs - s.fromMs) / HOUR_MS;
106
+ if (s.court === 'author')
107
+ longestReply = Math.max(longestReply ?? 0, w);
108
+ }
109
+ const leadWork = work.reviewer + work.author + work.landing;
110
+ const courts = ['reviewer', 'author', 'landing'];
111
+ const basis = leadWork > 0 ? work : clock;
112
+ const dominant = courts.reduce((m, c) => (basis[c] > basis[m] ? c : m), 'reviewer');
113
+ return {
114
+ f,
115
+ work,
116
+ clock,
117
+ leadWork,
118
+ leadClock: (f.mergedMs - f.openedMs) / HOUR_MS,
119
+ firstLookWork: f.firstLookMs == null ? null : cal.between(f.openedMs, f.firstLookMs),
120
+ longestReplyWork: f.rounds > 0 ? (longestReply ?? 0) : null,
121
+ landWork: work.landing,
122
+ landClock: clock.landing,
123
+ weekday: cal.weekday(f.openedMs),
124
+ dominant,
125
+ requestToLookWork: f.firstRequestMs != null && f.firstLookMs != null && f.firstRequestMs <= f.firstLookMs
126
+ ? cal.between(f.firstRequestMs, f.firstLookMs)
127
+ : null,
128
+ lookedBeforeAsked: f.firstRequestMs != null && f.firstLookMs != null && f.firstLookMs < f.firstRequestMs,
129
+ };
130
+ }
131
+ // ── Budgets ─────────────────────────────────────────────────────────────────────────────────
132
+ const BUDGET_SUBJECT = {
133
+ firstLook: (p) => `Three in four pull requests had a first look within ${p}`,
134
+ reply: (p) => `Three in four pull requests that went back to their author had every reply within ${p}`,
135
+ land: (p) => `Three in four approved pull requests merged within ${p} of approval`,
136
+ lead: (p) => `Three in four pull requests merged within ${p} of opening`,
137
+ };
138
+ const BUDGET_EMPTY = {
139
+ firstLook: 'No pull request had a first look before it merged.',
140
+ reply: 'No pull request went back to its author.',
141
+ land: 'No pull request was approved before it merged.',
142
+ lead: 'No pull request to measure.',
143
+ };
144
+ function budgetRow(measure, values, settings) {
145
+ const { good, ok } = settings.budgets[measure];
146
+ const n = values.length;
147
+ const p50 = percentile(values, 0.5);
148
+ const p75 = percentile(values, 0.75);
149
+ const p90 = percentile(values, 0.9);
150
+ let verdict = null;
151
+ let sentence;
152
+ if (n === 0) {
153
+ sentence = BUDGET_EMPTY[measure];
154
+ }
155
+ else if (n < FLOW_BUDGET_MIN_PRS) {
156
+ sentence = `Only ${n} pull ${plural(n, 'request', 'requests')} to measure — too few to hold against the budget.`;
157
+ }
158
+ else {
159
+ verdict = p75 <= good ? 'good' : p75 <= ok ? 'ok' : 'slow';
160
+ // In HOURS, never working days: the budget beside it is in hours, and a reader comparing
161
+ // "2.2 working days" with a "16-hour limit" has to do the conversion we should have done.
162
+ const base = BUDGET_SUBJECT[measure](fmtWork(p75, 0));
163
+ const tail = verdict === 'good'
164
+ ? ` — inside the ${budgetHours(good)} budget.`
165
+ : verdict === 'ok'
166
+ ? ` — over the ${budgetHours(good)} budget, inside the ${budgetHours(ok)} limit.`
167
+ : ` — past the ${budgetHours(ok)} limit.`;
168
+ sentence = base + tail;
169
+ }
170
+ return { measure, good, ok, prs: n, p50: r2(p50), p75: r2(p75), p90: r2(p90), verdict, sentence };
171
+ }
172
+ const share = (rows, pred) => rows.length === 0 ? 0 : rows.filter(pred).length / rows.length;
173
+ const SIGNALS = [
174
+ {
175
+ signal: 'firstLook',
176
+ label: 'Wait for a first look',
177
+ unit: 'workHours',
178
+ phrase: 'a longer wait for a first look',
179
+ floor: 0.5,
180
+ value: (rows) => median(rows.map((w) => w.firstLookWork).filter((v) => v != null)),
181
+ },
182
+ {
183
+ signal: 'lines',
184
+ label: 'Lines changed',
185
+ unit: 'count',
186
+ phrase: 'more lines changed',
187
+ floor: 20,
188
+ value: (rows) => median(rows.map((w) => w.f.lines).filter((v) => v != null)),
189
+ },
190
+ {
191
+ signal: 'files',
192
+ label: 'Files changed',
193
+ unit: 'count',
194
+ phrase: 'more files changed',
195
+ floor: 2,
196
+ value: (rows) => median(rows.map((w) => w.f.files).filter((v) => v != null)),
197
+ },
198
+ {
199
+ signal: 'wentBack',
200
+ label: 'Went back to the author',
201
+ unit: 'percent',
202
+ phrase: 'more trips back to the author',
203
+ floor: 0.1,
204
+ value: (rows) => share(rows, (w) => w.f.rounds > 0),
205
+ },
206
+ {
207
+ signal: 'ciRed',
208
+ label: 'Checks went red',
209
+ unit: 'percent',
210
+ phrase: 'more red checks',
211
+ floor: 0.1,
212
+ value: (rows) => share(rows, (w) => w.f.ciRedHours > 0),
213
+ },
214
+ {
215
+ signal: 'land',
216
+ label: 'Approved to merged',
217
+ unit: 'workHours',
218
+ phrase: 'a longer wait to merge after approval',
219
+ floor: 0.5,
220
+ value: (rows) => median(rows.filter((w) => w.f.approvedAtMs != null).map((w) => w.landWork)),
221
+ },
222
+ {
223
+ signal: 'lastDay',
224
+ label: 'Opened on the last working day of the week',
225
+ unit: 'percent',
226
+ phrase: 'more opened on the last working day of the week',
227
+ floor: 0.1,
228
+ value: (rows, lastDay) => share(rows, (w) => w.weekday === lastDay),
229
+ },
230
+ {
231
+ signal: 'reach',
232
+ label: 'Touches a high-reach area',
233
+ unit: 'percent',
234
+ phrase: 'more changes to high-reach areas (dependencies, CI, auth, database schema)',
235
+ floor: 0.1,
236
+ value: (rows) => share(rows, (w) => w.f.reachAreas.length > 0),
237
+ },
238
+ ];
239
+ function verdictOf(fast, slow, floor) {
240
+ if (slow - fast < floor)
241
+ return 'none';
242
+ if (fast <= 0)
243
+ return 'separates';
244
+ const ratio = slow / fast;
245
+ if (ratio >= FLOW_SEPARATES_RATIO)
246
+ return 'separates';
247
+ if (ratio >= FLOW_WEAK_RATIO)
248
+ return 'weak';
249
+ return 'none';
250
+ }
251
+ function joinPhrases(xs) {
252
+ if (xs.length <= 1)
253
+ return xs.join('');
254
+ return `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`;
255
+ }
256
+ function contrastOf(worked, lastDay) {
257
+ const q = Math.floor(worked.length / 4);
258
+ if (q < FLOW_CONTRAST_MIN_QUARTILE)
259
+ return null;
260
+ const sorted = [...worked].sort((a, b) => a.leadWork - b.leadWork || a.f.prId - b.f.prId);
261
+ const fast = sorted.slice(0, q);
262
+ const slow = sorted.slice(-q);
263
+ const rows = SIGNALS.map((s) => {
264
+ const f = s.value(fast, lastDay);
265
+ const sl = s.value(slow, lastDay);
266
+ return {
267
+ signal: s.signal,
268
+ label: s.label,
269
+ unit: s.unit,
270
+ fast: r2(f),
271
+ slow: r2(sl),
272
+ verdict: verdictOf(f, sl, s.floor),
273
+ };
274
+ });
275
+ const sep = new Set(rows.filter((r) => r.verdict === 'separates').map((r) => r.signal));
276
+ // "more lines and files changed" reads as one fact; two phrases for it read as padding.
277
+ const both = sep.has('lines') && sep.has('files');
278
+ const separating = SIGNALS.filter((s) => sep.has(s.signal) && !(both && s.signal === 'files')).map((s) => (both && s.signal === 'lines' ? 'more lines and files changed' : s.phrase));
279
+ const sentence = separating.length > 0
280
+ ? `Against the fastest quarter, the slowest quarter had ${joinPhrases(separating)}.`
281
+ : 'Nothing measured here separates the slowest quarter from the fastest.';
282
+ return { quartilePrs: q, rows, sentence };
283
+ }
284
+ // ── Landing tail ────────────────────────────────────────────────────────────────────────────
285
+ function linkOf(f) {
286
+ return {
287
+ prId: f.prId,
288
+ repoFullName: f.repoFullName,
289
+ prNumber: f.number,
290
+ prTitle: f.title,
291
+ githubUrl: f.githubUrl,
292
+ };
293
+ }
294
+ function landingTailOf(worked, dayHours) {
295
+ const approved = worked.filter((w) => w.f.approvedAtMs != null);
296
+ if (approved.length === 0)
297
+ return null;
298
+ const threshold = dayHours;
299
+ const over = approved
300
+ .filter((w) => w.landWork > threshold)
301
+ .sort((a, b) => b.landWork - a.landWork || a.f.prId - b.f.prId);
302
+ const total = approved.reduce((s, w) => s + w.landWork, 0);
303
+ const held = over.reduce((s, w) => s + w.landWork, 0);
304
+ const shareOfLanding = total > 0 ? held / total : 0;
305
+ const selfMergedOver = over.filter((w) => w.f.selfMerged).length;
306
+ // Siblings: other measured PRs under the same ticket, in ANOTHER repository, that merged while
307
+ // this one sat approved (or within a day after it) — a PR held for a coordinated change is
308
+ // waiting on its sibling, not on the merge step. A sibling that landed before the approval
309
+ // cannot have held it, so it is not offered as the reason.
310
+ const siblingsOf = (w) => {
311
+ if (!w.f.ticketKey || w.f.approvedAtMs == null)
312
+ return [];
313
+ const from = w.f.approvedAtMs;
314
+ const to = w.f.mergedMs + SIBLING_AFTER_MS;
315
+ return (byTicket.get(w.f.ticketKey) ?? []).filter((o) => o.f.repoId !== w.f.repoId && o.f.mergedMs >= from && o.f.mergedMs <= to);
316
+ };
317
+ const byTicket = new Map();
318
+ for (const w of worked) {
319
+ if (!w.f.ticketKey)
320
+ continue;
321
+ const list = byTicket.get(w.f.ticketKey);
322
+ if (list)
323
+ list.push(w);
324
+ else
325
+ byTicket.set(w.f.ticketKey, [w]);
326
+ }
327
+ const rows = over.slice(0, FLOW_LANDING_ROWS_CAP).map((w) => ({
328
+ ...linkOf(w.f),
329
+ landWorkHours: r2(w.landWork),
330
+ landHours: r2(w.landClock),
331
+ selfMerged: w.f.selfMerged,
332
+ ticketKey: w.f.ticketKey,
333
+ siblings: siblingsOf(w)
334
+ .sort((a, b) => a.f.mergedMs - b.f.mergedMs || a.f.prId - b.f.prId)
335
+ .slice(0, FLOW_SIBLINGS_CAP)
336
+ .map((o) => linkOf(o.f)),
337
+ }));
338
+ const withSiblings = over.filter((w) => siblingsOf(w).length > 0).length;
339
+ let sentence;
340
+ if (over.length === 0) {
341
+ sentence = 'No pull request sat approved for more than a working day.';
342
+ }
343
+ else {
344
+ sentence =
345
+ `${over.length} pull ${plural(over.length, 'request', 'requests')} sat approved for more than a ` +
346
+ `working day, holding ${pct(shareOfLanding)} of all the time spent approved and waiting to merge.`;
347
+ if (selfMergedOver > 0) {
348
+ sentence += ` ${selfMergedOver} of them ${plural(selfMergedOver, 'was', 'were')} merged by ${plural(selfMergedOver, 'its', 'their')} own author.`;
349
+ }
350
+ if (withSiblings > 0) {
351
+ sentence += ` ${withSiblings} ${plural(withSiblings, 'shares', 'share')} a ticket with a pull request in another repository that merged while ${plural(withSiblings, 'it', 'they')} waited.`;
352
+ }
353
+ }
354
+ return {
355
+ thresholdWorkHours: r2(threshold),
356
+ prsOver: over.length,
357
+ shareOfLanding: r2(shareOfLanding),
358
+ selfMergedOver,
359
+ siblingsOver: withSiblings,
360
+ rows,
361
+ sentence,
362
+ };
363
+ }
364
+ // ── Concentration ───────────────────────────────────────────────────────────────────────────
365
+ function concentrationOf(worked, minRepoPrs) {
366
+ const byRepo = new Map();
367
+ for (const w of worked) {
368
+ if (w.f.firstReviewerId == null || w.firstLookWork == null)
369
+ continue;
370
+ const list = byRepo.get(w.f.repoId);
371
+ if (list)
372
+ list.push(w);
373
+ else
374
+ byRepo.set(w.f.repoId, [w]);
375
+ }
376
+ const out = [];
377
+ for (const [repoId, rows] of byRepo) {
378
+ if (rows.length < minRepoPrs)
379
+ continue;
380
+ const counts = new Map();
381
+ for (const w of rows) {
382
+ const id = w.f.firstReviewerId;
383
+ counts.set(id, (counts.get(id) ?? 0) + 1);
384
+ }
385
+ // Busiest first; ties broken by id so the row is stable between polls. The id stays here.
386
+ const [topId, topCount] = [...counts].sort((a, b) => b[1] - a[1] || a[0] - b[0])[0];
387
+ const mine = rows.filter((w) => w.f.firstReviewerId === topId).map((w) => w.firstLookWork);
388
+ const rest = rows.filter((w) => w.f.firstReviewerId !== topId).map((w) => w.firstLookWork);
389
+ const topMed = medianOrNull(mine, FLOW_CONCENTRATION_MIN_SIDE);
390
+ const restMed = medianOrNull(rest, FLOW_CONCENTRATION_MIN_SIDE);
391
+ const slower = topMed != null &&
392
+ restMed != null &&
393
+ topMed - restMed >= FLOW_CONCENTRATION_SLOWER_MIN_HOURS &&
394
+ topMed >= restMed * FLOW_CONCENTRATION_SLOWER_RATIO;
395
+ out.push({
396
+ repoId,
397
+ repoFullName: rows[0]?.f.repoFullName ?? '',
398
+ prs: rows.length,
399
+ firstReviewers: counts.size,
400
+ topShare: r2(topCount / rows.length),
401
+ topFirstLookWorkHours: topMed == null ? null : r2(topMed),
402
+ othersFirstLookWorkHours: restMed == null ? null : r2(restMed),
403
+ slower,
404
+ });
405
+ }
406
+ return out.sort((a, b) => b.topShare - a.topShare || a.repoFullName.localeCompare(b.repoFullName));
407
+ }
408
+ // ── Asking for a review: a person, a team, or nobody ──────────────────────────────────────────
409
+ const REQUEST_LABEL = {
410
+ person: 'a named person',
411
+ team: 'a team',
412
+ none: 'nobody',
413
+ };
414
+ function requestsOf(worked, dayHours) {
415
+ const known = worked.filter((w) => w.f.requestKind != null);
416
+ if (known.length === 0)
417
+ return null;
418
+ const rows = ['person', 'team', 'none'].map((kind) => {
419
+ const inKind = known.filter((w) => w.f.requestKind === kind);
420
+ const toLook = medianOrNull(inKind.map((w) => w.requestToLookWork).filter((v) => v != null), FLOW_CUT_MIN_PRS);
421
+ const look = medianOrNull(inKind.map((w) => w.firstLookWork).filter((v) => v != null), FLOW_CUT_MIN_PRS);
422
+ return {
423
+ kind,
424
+ prs: inKind.length,
425
+ medianRequestToLookWorkHours: kind === 'none' || toLook == null ? null : r2(toLook),
426
+ medianFirstLookWorkHours: look == null ? null : r2(look),
427
+ };
428
+ });
429
+ const lookedBeforeAsked = known.filter((w) => w.lookedBeforeAsked).length;
430
+ const by = new Map(rows.map((r) => [r.kind, r]));
431
+ const parts = [];
432
+ const person = by.get('person');
433
+ const team = by.get('team');
434
+ if (person?.medianRequestToLookWorkHours != null && team?.medianRequestToLookWorkHours != null) {
435
+ parts.push(`Asking a named person got a first look a median ${fmtWork(person.medianRequestToLookWorkHours, 0)} ` +
436
+ `after the request; asking a team, ${fmtWork(team.medianRequestToLookWorkHours, 0)}.`);
437
+ }
438
+ else {
439
+ for (const r of [person, team]) {
440
+ if (r == null || r.prs === 0)
441
+ continue;
442
+ parts.push(r.medianRequestToLookWorkHours != null
443
+ ? `Asking ${REQUEST_LABEL[r.kind]} got a first look a median ${fmtWork(r.medianRequestToLookWorkHours, 0)} after the request.`
444
+ : `${r.prs} pull ${plural(r.prs, 'request', 'requests')} asked ${REQUEST_LABEL[r.kind]} — too few for a median.`);
445
+ }
446
+ }
447
+ const none = by.get('none');
448
+ if (none != null && none.prs > 0) {
449
+ parts.push(`${none.prs} pull ${plural(none.prs, 'request was', 'requests were')} never assigned a reviewer` +
450
+ (none.medianFirstLookWorkHours != null
451
+ ? `; their first look came a median ${fmtWork(none.medianFirstLookWorkHours, 0)} after opening.`
452
+ : '.'));
453
+ }
454
+ if (known.length < worked.length) {
455
+ parts.unshift(`Request history is in for ${known.length} of ${worked.length} pull requests so far; the rest arrive over the next few syncs.`);
456
+ }
457
+ void dayHours;
458
+ return { known: known.length, measured: worked.length, lookedBeforeAsked, rows, sentence: parts.join(' ') };
459
+ }
460
+ // ── The per-PR rows ─────────────────────────────────────────────────────────────────────────
461
+ function prRowOf(w) {
462
+ return {
463
+ prId: w.f.prId,
464
+ repoFullName: w.f.repoFullName,
465
+ prNumber: w.f.number,
466
+ prTitle: w.f.title,
467
+ githubUrl: w.f.githubUrl,
468
+ openedAt: new Date(w.f.openedMs).toISOString(),
469
+ mergedAt: new Date(w.f.mergedMs).toISOString(),
470
+ openedWeekday: w.weekday,
471
+ leadHours: r2(w.leadClock),
472
+ leadWorkHours: r2(w.leadWork),
473
+ workHours: { reviewer: r2(w.work.reviewer), author: r2(w.work.author), landing: r2(w.work.landing) },
474
+ firstLookWorkHours: w.firstLookWork == null ? null : r2(w.firstLookWork),
475
+ rounds: w.f.rounds,
476
+ lines: w.f.lines,
477
+ files: w.f.files,
478
+ reachAreas: w.f.reachAreas,
479
+ ciRedHours: r2(w.f.ciRedHours),
480
+ ticketKey: w.f.ticketKey,
481
+ selfMerged: w.f.selfMerged,
482
+ dominant: w.dominant,
483
+ requestKind: w.f.requestKind,
484
+ requestToLookWorkHours: w.requestToLookWork == null ? null : r2(w.requestToLookWork),
485
+ };
486
+ }
487
+ /** Every slow PR, then an even stride over the rest — never a sample that hides an outlier. */
488
+ function capRows(worked) {
489
+ if (worked.length <= FLOW_PR_ROWS_CAP)
490
+ return { rows: worked, capped: false };
491
+ const bySlow = [...worked].sort((a, b) => b.leadWork - a.leadWork || a.f.prId - b.f.prId);
492
+ const keep = bySlow.slice(0, FLOW_PR_ROWS_SLOW_KEEP);
493
+ const rest = bySlow.slice(FLOW_PR_ROWS_SLOW_KEEP);
494
+ const room = FLOW_PR_ROWS_CAP - keep.length;
495
+ const stride = rest.length / room;
496
+ for (let i = 0; i < room; i += 1) {
497
+ const pick = rest[Math.floor(i * stride)];
498
+ if (pick)
499
+ keep.push(pick);
500
+ }
501
+ return { rows: keep, capped: true };
502
+ }
503
+ // ── The per-PR headline figures ─────────────────────────────────────────────────────────────
504
+ /**
505
+ * The scatter's and the triangle's headline figures, over EVERY measured pull request.
506
+ *
507
+ * ⚠ NEVER OVER `prs`. The per-PR rows are capped at FLOW_PR_ROWS_CAP (every slow PR, then an even
508
+ * stride), so a count folded over them is wrong exactly when `prsCapped` — and wrong in the
509
+ * inflating direction, because the slow ones are all kept. The same definitions the SPA used to
510
+ * apply to the capped list: over a working day, the slowest tenth's share, never went back.
511
+ */
512
+ function prFiguresOf(worked, dayHours) {
513
+ const leads = worked.map((w) => w.leadWork).sort((a, b) => b - a);
514
+ const slowestTenthCount = Math.ceil(leads.length / 10);
515
+ const total = leads.reduce((s, v) => s + v, 0);
516
+ const top = leads.slice(0, slowestTenthCount).reduce((s, v) => s + v, 0);
517
+ return {
518
+ overWorkingDay: worked.filter((w) => w.leadWork > dayHours).length,
519
+ slowestTenthCount,
520
+ slowestTenthShare: total > 0 ? top / total : 0,
521
+ neverWentBack: worked.filter((w) => w.f.rounds === 0).length,
522
+ };
523
+ }
524
+ // ── The fold ────────────────────────────────────────────────────────────────────────────────
525
+ const COURT_PHRASE = {
526
+ reviewer: 'waiting for a reviewer',
527
+ author: 'waiting for the author',
528
+ landing: 'approved and waiting to land',
529
+ };
530
+ export function buildFlowDetail(facts, cal, settings, minRepoPrs) {
531
+ const worked = facts.map((f) => workOf(f, cal));
532
+ const dayHours = cal.dayHours;
533
+ const totals = { reviewer: 0, author: 0, landing: 0 };
534
+ for (const w of worked) {
535
+ totals.reviewer += w.work.reviewer;
536
+ totals.author += w.work.author;
537
+ totals.landing += w.work.landing;
538
+ }
539
+ const sum = totals.reviewer + totals.author + totals.landing;
540
+ const courtsWork = ['reviewer', 'author', 'landing'].map((court) => ({
541
+ court,
542
+ hours: r2(totals[court]),
543
+ share: sum > 0 ? totals[court] / sum : 0,
544
+ }));
545
+ const leads = worked.map((w) => w.leadWork);
546
+ const medianLeadWorkHours = median(leads);
547
+ const p75LeadWorkHours = percentile(leads, 0.75);
548
+ const workHeadline = worked.length === 0
549
+ ? null
550
+ : `Counting working hours only, time split ` +
551
+ courtsWork.map((c) => `${pct(c.share)} ${COURT_PHRASE[c.court]}`).join(', ').replace(/, ([^,]*)$/, ', and $1') +
552
+ // In hours, like the budget rows beneath it — the same p75 spelled "2.2 working days" here
553
+ // and "20 working hours" one row down is one figure in two spellings.
554
+ `. Half of pull requests merged within ${fmtWork(medianLeadWorkHours, 0)} of opening; ` +
555
+ `the slowest quarter took ${fmtWork(p75LeadWorkHours, 0)} or more.`;
556
+ const measures = {
557
+ firstLook: worked.map((w) => w.firstLookWork).filter((v) => v != null),
558
+ reply: worked.map((w) => w.longestReplyWork).filter((v) => v != null),
559
+ land: worked.filter((w) => w.f.approvedAtMs != null).map((w) => w.landWork),
560
+ lead: leads,
561
+ };
562
+ const budgets = FLOW_BUDGET_MEASURES.map((m) => budgetRow(m, measures[m], settings));
563
+ const lastDay = Math.max(...settings.days);
564
+ const sizeBands = SIZE_BANDS.map((b) => {
565
+ const rows = worked.filter((w) => w.f.lines != null && w.f.lines >= b.min && (b.max == null || w.f.lines <= b.max));
566
+ const lead = medianOrNull(rows.map((w) => w.leadWork), FLOW_CUT_MIN_PRS);
567
+ const look = medianOrNull(rows.map((w) => w.firstLookWork).filter((v) => v != null), FLOW_CUT_MIN_PRS);
568
+ return {
569
+ label: b.label,
570
+ minLines: b.min,
571
+ maxLines: b.max,
572
+ prs: rows.length,
573
+ medianLeadWorkHours: lead == null ? null : r2(lead),
574
+ medianFirstLookWorkHours: look == null ? null : r2(look),
575
+ };
576
+ });
577
+ const weekdays = [1, 2, 3, 4, 5, 6, 7].map((d) => {
578
+ const rows = worked.filter((w) => w.weekday === d);
579
+ const clock = medianOrNull(rows.map((w) => w.leadClock), FLOW_CUT_MIN_PRS);
580
+ const work = medianOrNull(rows.map((w) => w.leadWork), FLOW_CUT_MIN_PRS);
581
+ return {
582
+ weekday: d,
583
+ working: settings.days.includes(d),
584
+ prs: rows.length,
585
+ medianLeadHours: clock == null ? null : r2(clock),
586
+ medianLeadWorkHours: work == null ? null : r2(work),
587
+ };
588
+ });
589
+ const { rows: shown, capped } = capRows(worked);
590
+ return {
591
+ courtsWork,
592
+ medianLeadWorkHours: r2(medianLeadWorkHours),
593
+ p75LeadWorkHours: r2(p75LeadWorkHours),
594
+ workHeadline,
595
+ budgets,
596
+ prs: shown.map(prRowOf),
597
+ prsCapped: capped,
598
+ contrast: contrastOf(worked, lastDay),
599
+ sizeBands,
600
+ weekdays,
601
+ landingTail: landingTailOf(worked, dayHours),
602
+ concentration: concentrationOf(worked, minRepoPrs),
603
+ requests: requestsOf(worked, dayHours),
604
+ prFigures: prFiguresOf(worked, dayHours),
605
+ };
606
+ }
607
+ // ── Ticket keys ─────────────────────────────────────────────────────────────────────────────
608
+ //
609
+ // A Jira/Linear key (PROJ-123) from the title, then the branch name. The Pro issue-links
610
+ // extractor (packages/pro/src/issue-links/extract.ts) is the full version, with per-workspace
611
+ // allow-lists; core cannot import the plugin, so this is its NO-ALLOWLIST fallback only — the same
612
+ // boundary rules and the same deny-list floor. It is used for ONE thing: noticing that two PRs in
613
+ // DIFFERENT repositories share a ticket. A false positive costs a wrong "may have been held for
614
+ // it" beside a PR the reader can open, which is why nothing else leans on it.
615
+ const KEY_RE = /(?<![A-Za-z0-9._-])([A-Za-z][A-Za-z0-9]{1,9})-(\d{1,7})(?![A-Za-z0-9_]|\.\d|-\d)/g;
616
+ const NON_TICKET_PREFIXES = new Set([
617
+ 'UTF', 'UTF8', 'UTF16', 'SHA', 'SHA1', 'SHA256', 'SHA512', 'MD5', 'CRC', 'CRC32', 'AES', 'RSA',
618
+ 'BASE64', 'EC', 'ISO', 'RFC', 'CVE', 'CWE', 'WCAG', 'IEEE', 'ANSI', 'ASCII', 'PEP', 'SOC', 'PCI',
619
+ 'FIPS', 'NIST', 'HTTP', 'HTTPS', 'HTTP2', 'IPV4', 'IPV6', 'TLS', 'SSL', 'OAUTH', 'GPT', 'GPT3',
620
+ 'GPT4', 'LLAMA', 'RTX', 'GTX', 'X86', 'ARM', 'K8S', 'CUDA', 'JPEG', 'MP3', 'MP4', 'H264', 'H265',
621
+ 'COVID', 'ES', 'ES2015', 'UTC', 'P50', 'P90', 'P95', 'P99', 'Q1', 'Q2', 'Q3', 'Q4', 'H1', 'H2',
622
+ 'FY', 'NODE', 'PYTHON', 'JAVA', 'GO', 'V1', 'V2', 'V3',
623
+ ]);
624
+ export function ticketKeyOf(title, branch) {
625
+ const pick = (text, requireUpper) => {
626
+ if (!text)
627
+ return null;
628
+ for (const m of text.matchAll(KEY_RE)) {
629
+ const raw = m[1];
630
+ if (requireUpper && raw !== raw.toUpperCase())
631
+ continue;
632
+ const prefix = raw.toUpperCase();
633
+ if (NON_TICKET_PREFIXES.has(prefix) || /^V\d+$/.test(prefix))
634
+ continue;
635
+ if (!/[A-Z]/.test(prefix))
636
+ continue;
637
+ return `${prefix}-${m[2]}`;
638
+ }
639
+ return null;
640
+ };
641
+ // A title is typed by a person, so only an upper-case key counts there; a branch name is
642
+ // conventionally lower-case (`feature/proj-123-fix`), so any case counts.
643
+ return pick(title, true) ?? pick(branch, false);
644
+ }
645
+ /** Exposed for the unit test. */
646
+ export const __flowDetailTesting = {
647
+ FLOW_BUDGET_MIN_PRS,
648
+ FLOW_CONTRAST_MIN_QUARTILE,
649
+ FLOW_CUT_MIN_PRS,
650
+ FLOW_LANDING_ROWS_CAP,
651
+ FLOW_CONCENTRATION_MIN_SIDE,
652
+ FLOW_CONCENTRATION_SLOWER_RATIO,
653
+ FLOW_CONCENTRATION_SLOWER_MIN_HOURS,
654
+ FLOW_SEPARATES_RATIO,
655
+ FLOW_WEAK_RATIO,
656
+ FLOW_PR_ROWS_CAP,
657
+ FLOW_PR_ROWS_SLOW_KEEP,
658
+ verdictOf,
659
+ capRows,
660
+ };
661
+ //# sourceMappingURL=flow-detail.js.map