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,454 @@
1
+ import { graphql, GraphqlResponseError } from '@octokit/graphql';
2
+ import { getGithubToken } from './auth.js';
3
+ // True when the partial errors are GitHub's SAML-SSO wall: the token authenticates but isn't
4
+ // authorized for the repo owner's org, so the whole `repository` node is forbidden. GitHub's
5
+ // message is "Resource protected by organization SAML enforcement. You must grant your OAuth
6
+ // token access to this organization." This gates PRs/checks even on PUBLIC repos of a SAML org.
7
+ export function isSamlBlock(errors) {
8
+ return (Array.isArray(errors) &&
9
+ errors.some((e) => {
10
+ // GraphQL exposes a machine-readable flag on the error — the most reliable signal
11
+ // (docs: FORBIDDEN + extensions.saml_failure === true). Fall back to the message text.
12
+ if (e.extensions?.saml_failure === true)
13
+ return true;
14
+ return /saml enforcement|grant your oauth token access/i.test(e.message ?? '');
15
+ }));
16
+ }
17
+ // A parenthetical hint appended to a partial-GraphQL log. SAML enforcement gets the actionable
18
+ // re-auth hint; otherwise, ONLY when the forbidden field is a CI-checks field (so a NOT_FOUND —
19
+ // deleted/inaccessible PR — isn't mislabelled). Empty otherwise (the raw errors still get logged).
20
+ export function graphqlChecksHint(errors) {
21
+ if (isSamlBlock(errors)) {
22
+ return " — the sign-in token isn't authorized for this org's SAML SSO; re-authorize it inside an active SAML session for the org (see docs/GITHUB-AUTH-SETUP.md)";
23
+ }
24
+ const mentionsChecks = Array.isArray(errors) &&
25
+ errors.some((e) => {
26
+ const path = e.path;
27
+ const inPath = Array.isArray(path) && path.some((p) => /statuscheckrollup|check/i.test(String(p)));
28
+ const inMsg = /check\b|statuscheckrollup/i.test(e.message ?? '');
29
+ return inPath || inMsg;
30
+ });
31
+ return mentionsChecks
32
+ ? " — the sign-in token can't read CI checks here (GitHub App: install on this org with Checks read; OAuth App: re-authorize; or it's a private repo)"
33
+ : '';
34
+ }
35
+ // Compact one-line summary of a GraphQL `errors` array for logging.
36
+ export function summarizeGraphqlErrors(errors) {
37
+ if (!Array.isArray(errors))
38
+ return String(errors);
39
+ return errors
40
+ .slice(0, 5)
41
+ .map((e) => {
42
+ const type = e.type ?? 'ERROR';
43
+ const path = e.path?.join('.') ?? '?';
44
+ const message = e.message ?? '';
45
+ return `${type}@${path}: ${message}`;
46
+ })
47
+ .join(' | ');
48
+ }
49
+ // Run a GraphQL query, TOLERATING GitHub's *partial* errors. GitHub answers HTTP 200 with a
50
+ // non-empty `errors` array AND partial `data` when the token may read most of a query but is
51
+ // FORBIDDEN one sub-field — e.g. CI check runs (`statusCheckRollup.contexts`) on a private repo
52
+ // the token can't reach, or on a token minted before it was granted the scope to read them. By
53
+ // default `@octokit/graphql` THROWS on any such response, throwing away the usable partial data
54
+ // — so one un-permitted field would wipe the PR body/comments/reviewers. This returns the
55
+ // partial data instead (the forbidden fields arrive as null) and reports the errors via
56
+ // `onPartial`. A response with NO usable data (auth failure, rate limit, network) still throws.
57
+ export async function graphqlTolerant(client, query, variables, onPartial) {
58
+ try {
59
+ return await client(query, variables);
60
+ }
61
+ catch (err) {
62
+ if (err instanceof GraphqlResponseError && err.data != null) {
63
+ onPartial?.(err.errors);
64
+ return err.data;
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ // True for GitHub failures worth RETRYING: transient upstream/gateway faults (a 5xx
70
+ // from GitHub's edge — the fat activity query on a huge repo routinely 502s at nginx)
71
+ // and low-level network faults (reset/timeout/DNS). Deliberately NOT ordinary 4xx
72
+ // (auth / not-found / rate-limit-with-reset) nor GraphQL partial-data errors — those
73
+ // are handled elsewhere (graphqlTolerant) and must not be silently re-attempted. A 502
74
+ // arrives as an HTML error page (not GraphQL JSON), so match BOTH a structured status
75
+ // (octokit RequestError `.status`) AND the message text as a fallback.
76
+ export function isRetryableGithubError(err) {
77
+ const e = err;
78
+ const status = e.status ?? e.response?.status;
79
+ if (status === 500 || status === 502 || status === 503 || status === 504)
80
+ return true;
81
+ if (/^(ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|ECONNREFUSED|EPIPE)$/.test(e.code ?? '')) {
82
+ return true;
83
+ }
84
+ return /\b(50[0234])\b|bad gateway|gateway time-?out|service unavailable|temporarily unavailable|socket hang up|fetch failed|econnreset|etimedout/i.test(e.message ?? '');
85
+ }
86
+ // Classify a GitHub failure as RATE-LIMITED (primary GraphQL/REST exhaustion, or the
87
+ // secondary "abuse" limiter) and extract a resume time when the response carried one.
88
+ // Deliberately a SEPARATE classifier from isRetryableGithubError: retryable means "try
89
+ // again in milliseconds", limited means "pause until the window resets" — the sync's
90
+ // budget gate (github/rate-budget.ts + sync-repo.ts) owns that wait, and widening the
91
+ // retry predicate would instead hammer a limited token.
92
+ //
93
+ // Shapes recognised:
94
+ // • GraphQL: `errors[].type === 'RATE_LIMITED'` (GitHub answers HTTP 200, data null —
95
+ // which is why graphqlTolerant RETHROWS it), or a rate-limit message with no usable
96
+ // data. GraphqlResponseError exposes the response headers, so a reset time rides in.
97
+ // • REST: status 429 (Too Many Requests IS the limiter, by definition), or a 403 whose
98
+ // message carries GitHub's rate-limit/abuse wording (a plain 403 forbidden is NOT
99
+ // limited). ghRest attaches `status` + the rate-limit headers onto its thrown Error
100
+ // so this works without re-fetching anything.
101
+ //
102
+ // resumeAt: `retry-after` (relative seconds — the secondary limiter's header) wins, else
103
+ // `x-ratelimit-reset` (epoch seconds), else null (the caller falls back to a default).
104
+ export function isRateLimitError(err) {
105
+ const e = err;
106
+ const header = (name) => {
107
+ const h = e.headers?.[name] ?? e.response?.headers?.[name];
108
+ if (typeof h === 'string' && h.length > 0)
109
+ return h;
110
+ if (typeof h === 'number')
111
+ return String(h);
112
+ return null;
113
+ };
114
+ const resumeAt = () => {
115
+ const retryAfter = header('retry-after');
116
+ if (retryAfter != null) {
117
+ const secs = Number.parseInt(retryAfter, 10);
118
+ if (Number.isFinite(secs) && secs >= 0)
119
+ return new Date(Date.now() + secs * 1000);
120
+ }
121
+ const reset = header('x-ratelimit-reset');
122
+ if (reset != null) {
123
+ const epoch = Number.parseInt(reset, 10);
124
+ if (Number.isFinite(epoch) && epoch > 0)
125
+ return new Date(epoch * 1000);
126
+ }
127
+ return null;
128
+ };
129
+ if (Array.isArray(e.errors) &&
130
+ e.errors.some((er) => er.type === 'RATE_LIMITED')) {
131
+ return { limited: true, resumeAt: resumeAt() };
132
+ }
133
+ const status = e.status ?? e.response?.status;
134
+ if (status === 429)
135
+ return { limited: true, resumeAt: resumeAt() };
136
+ if (status === 403 && /rate limit|abuse/i.test(e.message ?? '')) {
137
+ return { limited: true, resumeAt: resumeAt() };
138
+ }
139
+ // Message-only fallback (a GraphQL transport variant with neither structured errors nor
140
+ // a status). Guarded on "no usable data" + "no status" so a 4xx/5xx that merely MENTIONS
141
+ // rate limits can't sneak in.
142
+ if (status == null &&
143
+ e.errors == null &&
144
+ e.data == null &&
145
+ /rate limit/i.test(e.message ?? '')) {
146
+ return { limited: true, resumeAt: resumeAt() };
147
+ }
148
+ return { limited: false, resumeAt: null };
149
+ }
150
+ // Run a GitHub call with bounded exponential-backoff retries on TRANSIENT failures
151
+ // (see isRetryableGithubError). A single 502 on any page must not abort a multi-page
152
+ // backfill of a large repo — without this, adding a big repo (every curated suggestion
153
+ // is one) frequently loads only a partial window before the walk dies. Non-retryable
154
+ // errors (4xx, GraphQL partial) propagate immediately. Backoff: 500ms, 1s, 2s.
155
+ export async function withGithubRetry(fn, opts = {}) {
156
+ const retries = opts.retries ?? 3;
157
+ const base = opts.baseDelayMs ?? 500;
158
+ for (let attempt = 1;; attempt++) {
159
+ try {
160
+ return await fn();
161
+ }
162
+ catch (err) {
163
+ if (attempt > retries || !isRetryableGithubError(err))
164
+ throw err;
165
+ const delayMs = base * 2 ** (attempt - 1);
166
+ opts.onRetry?.(attempt, delayMs, err);
167
+ await new Promise((r) => setTimeout(r, delayMs));
168
+ }
169
+ }
170
+ }
171
+ // Per-account GitHub clients. There is deliberately NO module-level token /
172
+ // client cache: in a multi-tenant (cloud) process a cached token would leak one
173
+ // account's credentials into another account's request. The token is ALWAYS
174
+ // passed in by the caller (resolved from the owning account), so isolation is
175
+ // structural. See auth/account.ts#getAccessToken for where the token comes from.
176
+ export function getGraphqlClientFor(token) {
177
+ return graphql.defaults({ headers: { authorization: `token ${token}` } });
178
+ }
179
+ async function ghRest(token, method, path, body) {
180
+ const res = await fetch(`https://api.github.com${path}`, {
181
+ method,
182
+ headers: {
183
+ authorization: `token ${token}`,
184
+ accept: 'application/vnd.github+json',
185
+ 'x-github-api-version': '2022-11-28',
186
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
187
+ },
188
+ body: body !== undefined ? JSON.stringify(body) : undefined,
189
+ });
190
+ if (!res.ok) {
191
+ const text = await res.text().catch(() => '');
192
+ const err = new Error(`GitHub REST ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
193
+ // Structured status + the rate-limit headers, so isRateLimitError can classify a REST
194
+ // 403/429 and derive a resume time without message archaeology. Nulls kept as-is —
195
+ // the classifier's header lookup only honours strings.
196
+ Object.assign(err, {
197
+ status: res.status,
198
+ headers: {
199
+ 'retry-after': res.headers.get('retry-after'),
200
+ 'x-ratelimit-remaining': res.headers.get('x-ratelimit-remaining'),
201
+ 'x-ratelimit-reset': res.headers.get('x-ratelimit-reset'),
202
+ },
203
+ });
204
+ throw err;
205
+ }
206
+ return res.json();
207
+ }
208
+ // REST GET (per-commit changed-files etc.) for a specific account's token.
209
+ export function ghRestGetFor(token, path) {
210
+ return ghRest(token, 'GET', path);
211
+ }
212
+ // Conditional REST GET for the adaptive-sync change probe (Phase 2 — see
213
+ // docs/REALTIME-SYNC.md). Sends `If-None-Match: <etag>` when a prior ETag is known and
214
+ // returns the status + fresh ETag WITHOUT throwing — a 304 (Not Modified) means nothing
215
+ // changed and, crucially, does NOT count against GitHub's primary rate limit. The body is
216
+ // drained and discarded (we only care about changed-or-not + the new ETag). A non-2xx /
217
+ // non-304 (403/404/network) is reported as `notModified: false` so the caller falls back
218
+ // to a full walk rather than silently skipping.
219
+ export async function ghRestGetConditional(token, path, etag) {
220
+ const res = await fetch(`https://api.github.com${path}`, {
221
+ method: 'GET',
222
+ headers: {
223
+ authorization: `token ${token}`,
224
+ accept: 'application/vnd.github+json',
225
+ 'x-github-api-version': '2022-11-28',
226
+ ...(etag ? { 'if-none-match': etag } : {}),
227
+ },
228
+ });
229
+ // Drain the body so the socket is freed (we never read it).
230
+ await res.arrayBuffer().catch(() => { });
231
+ return {
232
+ status: res.status,
233
+ notModified: res.status === 304,
234
+ etag: res.headers.get('etag'),
235
+ };
236
+ }
237
+ // REST GET returning the raw response TEXT (not JSON), for endpoints with a plain-text
238
+ // body — notably the Actions job-logs endpoint, which 302-redirects to a signed
239
+ // download URL. fetch follows the redirect automatically AND strips the Authorization
240
+ // header on the cross-origin hop, so our token is never sent to the signed URL. Does
241
+ // NOT throw on a non-2xx status — returns it so the caller can degrade gracefully
242
+ // (logs expire after ~90 days / on a re-run → 404/410; missing actions:read → 403).
243
+ export async function ghRestGetText(token, path) {
244
+ const res = await fetch(`https://api.github.com${path}`, {
245
+ method: 'GET',
246
+ headers: {
247
+ authorization: `token ${token}`,
248
+ 'x-github-api-version': '2022-11-28',
249
+ },
250
+ });
251
+ const text = await res.text().catch(() => '');
252
+ return { status: res.status, ok: res.ok, text };
253
+ }
254
+ // ADDITIVE (Actions job logs, byte-window viewer): REST GET returning the RAW Response
255
+ // WITHOUT reading the body and WITHOUT following redirects (`redirect: 'manual'`), so
256
+ // the caller can:
257
+ // (a) read the `location` of the Actions job-logs 302 and issue its OWN byte-`Range`
258
+ // request against the short-lived signed blob URL (which does honour Range), and
259
+ // (b) STREAM + cap a potentially multi-MB body instead of buffering the whole thing.
260
+ // The signed URL must NEVER be handed to a client — it is unauthenticated and would
261
+ // bypass our per-account ownership check. Nothing here throws on a non-2xx; the caller
262
+ // branches on `res.status`. The Authorization header is only ever sent to api.github.com
263
+ // because we do not follow the cross-origin hop ourselves.
264
+ export async function ghRestGetRaw(token, path, init) {
265
+ return fetch(`https://api.github.com${path}`, {
266
+ method: 'GET',
267
+ redirect: 'manual',
268
+ headers: {
269
+ authorization: `token ${token}`,
270
+ 'x-github-api-version': '2022-11-28',
271
+ },
272
+ signal: init?.signal,
273
+ });
274
+ }
275
+ // REST GET a repo file's RAW contents (Accept: application/vnd.github.raw), NOT throwing
276
+ // on a non-2xx — returns the status + body so the caller can branch. Used to fetch a
277
+ // repo's CODEOWNERS file (404 when absent → degrade to no CODEOWNERS suggestions) and the
278
+ // advisor's bot-config reads. The raw media type returns the file bytes directly (no
279
+ // base64/JSON envelope to decode). `ref` (optional) reads at a specific branch/sha —
280
+ // omitted, GitHub serves the default branch, which is the config that governs FUTURE
281
+ // reviews (the advisor's read point).
282
+ export async function ghRestGetContentRaw(token, path, ref) {
283
+ const url = ref
284
+ ? `https://api.github.com${path}?ref=${encodeURIComponent(ref)}`
285
+ : `https://api.github.com${path}`;
286
+ const res = await fetch(url, {
287
+ method: 'GET',
288
+ headers: {
289
+ authorization: `token ${token}`,
290
+ accept: 'application/vnd.github.raw',
291
+ 'x-github-api-version': '2022-11-28',
292
+ },
293
+ });
294
+ const text = await res.text().catch(() => '');
295
+ return { status: res.status, ok: res.ok, text };
296
+ }
297
+ // REST GET one directory's contents listing (the JSON contents API — for a FILE it returns
298
+ // an object, for a directory an array). Status-returning like ghRestGetContentRaw: a 404 is
299
+ // the ordinary "that directory doesn't exist" outcome (e.g. a repo with no
300
+ // .github/workflows/). Entries are name/path/type/size only — file BYTES go through
301
+ // ghRestGetContentRaw so there is exactly one place decoding repo-authored content.
302
+ export async function ghRestGetContentDir(token, path, ref) {
303
+ const url = ref
304
+ ? `https://api.github.com${path}?ref=${encodeURIComponent(ref)}`
305
+ : `https://api.github.com${path}`;
306
+ const res = await fetch(url, {
307
+ method: 'GET',
308
+ headers: {
309
+ authorization: `token ${token}`,
310
+ accept: 'application/vnd.github+json',
311
+ 'x-github-api-version': '2022-11-28',
312
+ },
313
+ });
314
+ if (!res.ok) {
315
+ await res.text().catch(() => '');
316
+ return { status: res.status, ok: false, entries: [] };
317
+ }
318
+ const body = await res.json().catch(() => null);
319
+ if (!Array.isArray(body)) {
320
+ // A file path (object body) or an unreadable payload — not a directory listing.
321
+ return { status: res.status, ok: false, entries: [] };
322
+ }
323
+ const entries = body
324
+ .map((e) => {
325
+ const it = e;
326
+ if (typeof it.name !== 'string' || typeof it.path !== 'string')
327
+ return null;
328
+ const type = it.type === 'dir' ? 'dir' : it.type === 'file' ? 'file' : null;
329
+ if (!type)
330
+ return null;
331
+ return {
332
+ name: it.name,
333
+ path: it.path,
334
+ type,
335
+ size: typeof it.size === 'number' ? it.size : 0,
336
+ };
337
+ })
338
+ .filter((e) => Boolean(e));
339
+ return { status: res.status, ok: true, entries };
340
+ }
341
+ // REST GET in GitHub's raw `diff` media type (Accept: application/vnd.github.diff),
342
+ // NOT throwing on a non-2xx — returns the status + body so the caller can branch. GitHub
343
+ // caps this media type at 20,000 lines and 406s past it (a huge PR), which the caller
344
+ // handles by falling back to the per-file endpoint. Mirrors ghRestGetText's non-throwing
345
+ // contract.
346
+ export async function ghRestGetDiffStatus(token, path) {
347
+ const res = await fetch(`https://api.github.com${path}`, {
348
+ method: 'GET',
349
+ headers: {
350
+ authorization: `token ${token}`,
351
+ accept: 'application/vnd.github.diff',
352
+ 'x-github-api-version': '2022-11-28',
353
+ },
354
+ });
355
+ const text = await res.text().catch(() => '');
356
+ return { status: res.status, ok: res.ok, text };
357
+ }
358
+ // Throwing variant of the above — the per-account, cloud-ready way to fetch a PR's
359
+ // unified diff (vs the local-only `gh pr diff`). Throws on a non-2xx status.
360
+ export async function ghRestGetDiff(token, path) {
361
+ const res = await ghRestGetDiffStatus(token, path);
362
+ if (!res.ok) {
363
+ throw new Error(`GitHub REST GET(diff) ${path} -> ${res.status}: ${res.text.slice(0, 300)}`);
364
+ }
365
+ return res.text;
366
+ }
367
+ // REST PUT returning the parsed STATUS + body WITHOUT throwing on a non-2xx, so the caller can
368
+ // map GitHub's meaningful merge / update-branch statuses to structured results (405 not
369
+ // mergeable, 409 head-sha mismatch, 422 method disallowed / can't update). Per-account token.
370
+ export async function ghRestPutStatus(token, path, body) {
371
+ const res = await fetch(`https://api.github.com${path}`, {
372
+ method: 'PUT',
373
+ headers: {
374
+ authorization: `token ${token}`,
375
+ accept: 'application/vnd.github+json',
376
+ 'x-github-api-version': '2022-11-28',
377
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
378
+ },
379
+ body: body !== undefined ? JSON.stringify(body) : undefined,
380
+ });
381
+ const text = await res.text().catch(() => '');
382
+ let json = null;
383
+ try {
384
+ json = text ? JSON.parse(text) : null;
385
+ }
386
+ catch {
387
+ /* non-JSON body (rare on these endpoints) */
388
+ }
389
+ return { status: res.status, ok: res.ok, json, text };
390
+ }
391
+ // REST PATCH returning status (updating a pull request — e.g. closing it via
392
+ // `{ state: 'closed' }`). Mirrors ghRestPutStatus: never throws on a non-2xx, returns the
393
+ // status + parsed body so the caller can map GitHub's error to an HTTP response.
394
+ export async function ghRestPatchStatus(token, path, body) {
395
+ const res = await fetch(`https://api.github.com${path}`, {
396
+ method: 'PATCH',
397
+ headers: {
398
+ authorization: `token ${token}`,
399
+ accept: 'application/vnd.github+json',
400
+ 'x-github-api-version': '2022-11-28',
401
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
402
+ },
403
+ body: body !== undefined ? JSON.stringify(body) : undefined,
404
+ });
405
+ const text = await res.text().catch(() => '');
406
+ let json = null;
407
+ try {
408
+ json = text ? JSON.parse(text) : null;
409
+ }
410
+ catch {
411
+ /* non-JSON body (rare on these endpoints) */
412
+ }
413
+ return { status: res.status, ok: res.ok, json, text };
414
+ }
415
+ // REST POST (submitting a PR review — inline line comments require the REST
416
+ // reviews endpoint) for a specific account's token.
417
+ export function ghRestPostFor(token, path, body) {
418
+ return ghRest(token, 'POST', path, body);
419
+ }
420
+ // REST POST to an endpoint that returns NO body (201/204 No Content) — e.g. the Actions
421
+ // "rerun" / "rerun-failed-jobs" endpoints. Same auth + fail-loud-on-non-2xx as ghRest,
422
+ // but does NOT call res.json() (which would throw `Unexpected end of JSON input` on the
423
+ // empty success body). Returns nothing.
424
+ export async function ghRestPostNoContent(token, path, body) {
425
+ const res = await fetch(`https://api.github.com${path}`, {
426
+ method: 'POST',
427
+ headers: {
428
+ authorization: `token ${token}`,
429
+ accept: 'application/vnd.github+json',
430
+ 'x-github-api-version': '2022-11-28',
431
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
432
+ },
433
+ body: body !== undefined ? JSON.stringify(body) : undefined,
434
+ });
435
+ if (!res.ok) {
436
+ const text = await res.text().catch(() => '');
437
+ throw new Error(`GitHub REST POST ${path} -> ${res.status}: ${text.slice(0, 300)}`);
438
+ }
439
+ }
440
+ // ---- Local-only convenience wrappers ----
441
+ // Use the gh CLI token. ONLY for code paths that run in local mode — the Claude
442
+ // Review posting path (post-review.ts) and clone-manager.ts, both force-disabled
443
+ // in cloud. Caching the gh token is safe because local mode is single-account.
444
+ let localToken = null;
445
+ function localGhToken() {
446
+ return (localToken ??= getGithubToken());
447
+ }
448
+ export function ghRestGet(path) {
449
+ return ghRestGetFor(localGhToken(), path);
450
+ }
451
+ export function ghRestPost(path, body) {
452
+ return ghRestPostFor(localGhToken(), path, body);
453
+ }
454
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,221 @@
1
+ // CODEOWNERS-based reviewer suggestions (CORE). Fetches a repo's CODEOWNERS file via the
2
+ // contents API, parses it, and — given a PR's changed paths — returns the owning users +
3
+ // teams. Best-effort throughout: any failure (no file, token can't read the repo, a parse
4
+ // hiccup) degrades to "no CODEOWNERS suggestions", NEVER an error on the PR-detail path.
5
+ //
6
+ // Why CODEOWNERS and not the org/teams API: the contents API is reachable in every auth
7
+ // mode (local gh token, cloud OAuth `public_repo`, GitHub App with contents:read), and a
8
+ // `@org/team` owner is directly requestable as a review via `team_reviewers` — no need to
9
+ // expand team membership (which needs `read:org` + org membership and is unreliable under
10
+ // the cloud GitHub App). Individual `@user` owners become user suggestions directly.
11
+ import { ghRestGetContentRaw } from './client.js';
12
+ // The candidate file locations GitHub honours, in the order it resolves them.
13
+ const CODEOWNERS_PATHS = ['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS'];
14
+ // Per-(account, repo) cache. CODEOWNERS changes rarely, so a short TTL keeps the extra
15
+ // contents-API call off the hot path while staying reasonably fresh. A `null` rules value
16
+ // is cached too (the common "repo has no CODEOWNERS" case) so we don't refetch a 404 on
17
+ // every PR open. Keyed by account so one tenant's token result never leaks to another.
18
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
19
+ const cache = new Map();
20
+ // ---- Hard limits on attacker-supplied input ----
21
+ // A CODEOWNERS file is fetched from a repository, which means its contents are chosen by
22
+ // whoever controls that repository — in cloud, anyone who can sign up and add their own
23
+ // repo. Every one of these rules is then regex-matched against every changed path of every
24
+ // PR on the request thread of a SINGLE-threaded server, so unbounded input here is a
25
+ // denial-of-service on the whole deployment, not just on the caller.
26
+ //
27
+ // A real CODEOWNERS file is a few dozen lines; GitHub itself stops honouring one past 3 MB
28
+ // and only applies the first 1000 rules. These caps sit comfortably above any legitimate
29
+ // file and turn a hostile one into a truncated, harmless one.
30
+ const MAX_CODEOWNERS_BYTES = 256 * 1024;
31
+ const MAX_RULES = 500;
32
+ const MAX_PATTERN_LENGTH = 512;
33
+ /** Cap the paths matched per PR. A PR touching more files than this is already an outlier,
34
+ * and suggestions from the first N are as useful as suggestions from all of them. */
35
+ const MAX_PATHS_MATCHED = 300;
36
+ // Convert a CODEOWNERS (gitignore-style) path pattern into a regex over POSIX file paths.
37
+ // A best-effort subset matching GitHub's documented CODEOWNERS semantics: `*` matches
38
+ // within a single segment (NOT across `/`), `**` across segments, `?` one non-slash char;
39
+ // a leading `/` or any internal `/` anchors to the repo root, otherwise the pattern may
40
+ // match at any directory depth; a trailing `/` (or a bare directory name) owns everything
41
+ // under that directory. Exported for the unit test that locks these semantics.
42
+ export function globToRegExp(raw) {
43
+ let p = raw.trim();
44
+ if (p.length > MAX_PATTERN_LENGTH) {
45
+ throw new Error('codeowners pattern too long');
46
+ }
47
+ // ---- ReDoS guard: collapse repeated globs BEFORE compiling ----
48
+ // Each segment-aligned `**/` compiles to `(?:.*/)?` below. Two of those in a row give the
49
+ // regex engine two nullable, overlapping ways to consume the same directory prefix, and
50
+ // N in a row make that 2^N — so `**/**/**/…/zzz.txt` against a deep path backtracks
51
+ // effectively forever, freezing the event loop for every tenant.
52
+ //
53
+ // Collapsing is not merely a mitigation, it is SEMANTICALLY EXACT: "any number of leading
54
+ // directories, then any number of leading directories" is the same language as "any
55
+ // number of leading directories". Same for a run of bare `**`, and for `**` adjacent to
56
+ // `*`. After this, at most one `(?:.*/)?` can come from each distinct run, so the compiled
57
+ // pattern is linear to match.
58
+ p = p
59
+ // `**/**/` … → `**/`
60
+ .replace(/(?:\*\*\/)+/g, '**/')
61
+ // `****…` (a run of 3+ stars, or `**` next to `*`) → `**`
62
+ .replace(/\*{3,}/g, '**')
63
+ // `**/**` (trailing, not slash-terminated) → `**`
64
+ .replace(/\*\*(?:\/\*\*)+/g, '**');
65
+ const dirOnly = p.endsWith('/');
66
+ if (dirOnly)
67
+ p = p.slice(0, -1);
68
+ const startSlash = p.startsWith('/');
69
+ if (startSlash)
70
+ p = p.slice(1);
71
+ // Git: a pattern with a slash anywhere but the end is relative to the root (anchored);
72
+ // a pattern with no internal slash matches by name at any level.
73
+ const anchored = startSlash || p.includes('/');
74
+ let re = '';
75
+ for (let i = 0; i < p.length; i++) {
76
+ const c = p[i];
77
+ if (c === '*') {
78
+ if (p[i + 1] === '*') {
79
+ // `**` spans path segments. A segment-aligned `**/` becomes OPTIONAL leading
80
+ // directories that preserve the `/` boundary (so `**/logs` matches `a/logs` but
81
+ // NOT `mylogs`); a trailing/standalone `**` matches anything.
82
+ if (p[i + 2] === '/') {
83
+ re += '(?:.*/)?';
84
+ i += 2; // consume the second '*' and the '/'
85
+ }
86
+ else {
87
+ re += '.*';
88
+ i += 1; // consume the second '*'
89
+ }
90
+ }
91
+ else {
92
+ re += '[^/]*';
93
+ }
94
+ }
95
+ else if (c === '?') {
96
+ re += '[^/]';
97
+ }
98
+ else if ('.+^${}()|[]\\'.includes(c)) {
99
+ re += `\\${c}`;
100
+ }
101
+ else {
102
+ re += c;
103
+ }
104
+ }
105
+ // A trailing-slash directory (or a bare literal path) OWNS everything under it, so it
106
+ // gets a "…/.*" / "(?:/.*)?" tail. A wildcard-TERMINATED pattern (e.g. `docs/*`) is
107
+ // single-level per GitHub — it must NOT gain that tail, or it would over-match nested
108
+ // files (`docs/*` matches `docs/a.md` but not `docs/sub/a.md`).
109
+ const endsWithWildcard = /[*?]$/.test(p);
110
+ const body = dirOnly ? `${re}/.*` : endsWithWildcard ? re : `${re}(?:/.*)?`;
111
+ const full = anchored ? `^${body}$` : `^(?:.*/)?${body}$`;
112
+ // Last line of defence: refuse a compiled source that still contains several nullable
113
+ // `(?:.*/)?` groups. The collapse above should make this unreachable for any input, so
114
+ // reaching it means a pattern shape we did not anticipate — drop the rule (parseCodeowners
115
+ // catches this) rather than hand the engine something that might backtrack.
116
+ const nullableGroups = (full.match(/\(\?:\.\*\/\)\?/g) ?? []).length;
117
+ if (nullableGroups > 2) {
118
+ throw new Error('codeowners pattern too ambiguous to match safely');
119
+ }
120
+ return new RegExp(full);
121
+ }
122
+ // Parse CODEOWNERS text into ordered rules (order matters: the LAST matching rule wins).
123
+ function parseCodeowners(text) {
124
+ const rules = [];
125
+ for (const line of text.split('\n')) {
126
+ // Stop at MAX_RULES rather than compiling a hostile 100k-line file. GitHub itself only
127
+ // honours the first 1000 rules, so truncating is consistent with the real semantics.
128
+ if (rules.length >= MAX_RULES)
129
+ break;
130
+ const trimmed = line.trim();
131
+ if (!trimmed || trimmed.startsWith('#'))
132
+ continue;
133
+ // Strip an inline comment, then split on whitespace: first token = pattern, rest = owners.
134
+ const noComment = trimmed.split('#')[0].trim();
135
+ if (!noComment)
136
+ continue;
137
+ const parts = noComment.split(/\s+/);
138
+ const pattern = parts[0];
139
+ // Owners are the @-prefixed tokens (drop bare emails — not requestable as reviewers).
140
+ const owners = parts.slice(1).filter((o) => o.startsWith('@'));
141
+ if (owners.length === 0)
142
+ continue;
143
+ try {
144
+ rules.push({ re: globToRegExp(pattern), owners });
145
+ }
146
+ catch {
147
+ /* skip an un-compilable pattern rather than fail the whole file */
148
+ }
149
+ }
150
+ return rules;
151
+ }
152
+ // Fetch + parse a repo's CODEOWNERS (cached). Returns null when there's no file / it can't
153
+ // be read. Never throws.
154
+ async function loadRules(token, owner, name, accountId) {
155
+ const key = `${accountId}:${owner}/${name}`;
156
+ const hit = cache.get(key);
157
+ if (hit && Date.now() - hit.at < CACHE_TTL_MS)
158
+ return hit.rules;
159
+ let rules = null;
160
+ try {
161
+ for (const path of CODEOWNERS_PATHS) {
162
+ const res = await ghRestGetContentRaw(token, `/repos/${owner}/${name}/contents/${path}`);
163
+ if (res.ok && res.text) {
164
+ // Truncate before parsing: the body length is chosen by whoever owns the repo.
165
+ rules = parseCodeowners(res.text.length > MAX_CODEOWNERS_BYTES
166
+ ? res.text.slice(0, MAX_CODEOWNERS_BYTES)
167
+ : res.text);
168
+ break;
169
+ }
170
+ // Any non-404 (403 org wall / 401) also means "give up on CODEOWNERS" — stop trying.
171
+ if (res.status !== 404)
172
+ break;
173
+ }
174
+ }
175
+ catch {
176
+ rules = null;
177
+ }
178
+ cache.set(key, { rules, at: Date.now() });
179
+ return rules;
180
+ }
181
+ // Given a PR's changed paths, return the CODEOWNERS-derived owners (users + teams). The
182
+ // LAST matching rule for each file wins (git semantics); owners are unioned across files.
183
+ export async function getCodeownersMatch(token, owner, name, accountId, changedPaths) {
184
+ const empty = { logins: [], teams: [] };
185
+ if (changedPaths.length === 0)
186
+ return empty;
187
+ const rules = await loadRules(token, owner, name, accountId);
188
+ if (!rules || rules.length === 0)
189
+ return empty;
190
+ // Bound the total match work to MAX_PATHS_MATCHED × MAX_RULES regex tests. Both factors
191
+ // are attacker-influenceable (the PR's file count and the repo's CODEOWNERS), and this runs
192
+ // synchronously on the request thread of a single-threaded server.
193
+ const paths = changedPaths.length > MAX_PATHS_MATCHED
194
+ ? changedPaths.slice(0, MAX_PATHS_MATCHED)
195
+ : changedPaths;
196
+ const ownerTokens = new Set();
197
+ for (const path of paths) {
198
+ let last = null;
199
+ for (const r of rules)
200
+ if (r.re.test(path))
201
+ last = r; // last match wins
202
+ if (last)
203
+ for (const o of last.owners)
204
+ ownerTokens.add(o);
205
+ }
206
+ const logins = [];
207
+ const teams = [];
208
+ for (const tok of ownerTokens) {
209
+ const handle = tok.slice(1); // drop '@'
210
+ if (handle.includes('/')) {
211
+ const slug = handle.slice(handle.indexOf('/') + 1);
212
+ if (slug)
213
+ teams.push({ slug, name: handle });
214
+ }
215
+ else if (handle) {
216
+ logins.push(handle);
217
+ }
218
+ }
219
+ return { logins, teams };
220
+ }
221
+ //# sourceMappingURL=codeowners.js.map