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,730 @@
1
+ import { assertPushTarget } from '../../coding/git.js';
2
+ import { config } from '../../config.js';
3
+ import { gitSupportsMergeTree } from '../../conflict/git.js';
4
+ import { conflictModelHash } from '../../conflict/hash.js';
5
+ import { landConflictResolution } from '../../conflict/land.js';
6
+ import { buildConflictModel, conflictFileContent } from '../../conflict/model.js';
7
+ import { claimCommitSlot, claimSession, dropSession, getSession, sessionView, setCommitPhase, setPreparePhase, settleCommitDone, settleCommitFailed, settleFailed, settleReady, storeEdit, subscribe, } from '../../conflict/session.js';
8
+ import { editShapeFor, validateConflictEdit } from '../../conflict/suggestion.js';
9
+ import { getPrWriteContext, WRITE_PERMISSIONS } from '../../db/queries.js';
10
+ import { accountIdOf } from '../plugins/auth.js';
11
+ /**
12
+ * THE MERGE-CONFLICT RESOLVER'S SEVEN ROUTES (CORE / free, BOTH MODES).
13
+ *
14
+ * ⚠ REGISTERED UNCONDITIONALLY. These were local-only; they are not any more. There is still no
15
+ * `CONFLICT_RESOLVER_ENABLED` and there must not be one, because a per-handler env check LOOKS
16
+ * like a gate while being one Railway variable away from not being one.
17
+ *
18
+ * ⚠ WHAT MAKES THEM SAFE IN A MULTI-TENANT PROCESS is not anything in this file's cloud story —
19
+ * it is that nothing here was ever single-tenant. Ownership is `getPrWriteContext(id, accountId)`
20
+ * on every route, and every git fetch goes out under the CALLER'S OWN token
21
+ * (`fetchRefIntoClone`), into a ref namespaced by session id. The clone cache is shared across
22
+ * accounts and holds OBJECTS, never permission: a tenant who cannot read a private repository
23
+ * cannot fetch from it, whoever else has cloned it.
24
+ *
25
+ * POST /api/prs/:id/conflicts open → 202 + a `preparing` session
26
+ * GET /api/prs/:id/conflicts?session= the manifest (no regions)
27
+ * GET /api/prs/:id/conflicts/stream the ONE SSE channel — prepare AND commit
28
+ * GET /api/prs/:id/conflicts/files/:index one file's regions
29
+ * POST /api/prs/:id/conflicts/edit one region's text, typed → an opaque `editId`
30
+ * POST /api/prs/:id/conflicts/commit the push → 202, phases on the stream
31
+ * DELETE /api/prs/:id/conflicts?session= drop the server session
32
+ *
33
+ * ⚠ EXACTLY ONE OF THESE ACCEPTS FILE CONTENT, AND IT IS NOT THE COMMIT. `…/conflicts/edit`
34
+ * takes the lines a reader typed for ONE region, pinned by `fingerprint`, and VALIDATES THEM
35
+ * BEFORE AN ID EXISTS — text-ness, no surviving conflict marker, a size cap. Every field of
36
+ * every other request, the commit included, is still an index, an id or an enum member: an
37
+ * accepted model suggestion travels as an opaque `suggestionId` and a hand-edited region as an
38
+ * opaque `editId`, both addressing text the SERVER holds. The property that mattered is
39
+ * unchanged — the server commits only bytes it folded, from a request that names no content.
40
+ *
41
+ * WHY OPEN AND COMMIT ARE ASYNCHRONOUS. Fastify's `requestTimeout` is 60s and a git command may
42
+ * take 120; a cold `ensureClone` on a large repository blows straight past both. So each of the
43
+ * two POSTs validates everything CHEAP synchronously — ownership, permission, the JSON schema,
44
+ * the session match, the SHA pins, the branch name, decision completeness — and refuses with a
45
+ * real status code. Only then does it answer 202 and run. A malformed request never gets a
46
+ * terminal stream frame where an error belongs.
47
+ *
48
+ * ⚠ THE SESSION ID IS NOT AUTHORISATION. Ownership is re-checked on EVERY route through
49
+ * `getPrWriteContext` (→ 404, no existence oracle) and write permission (→ 403). `sessionId` is
50
+ * a concurrency token: a commit whose id no longer matches is `SessionExpired`.
51
+ */
52
+ const idParamSchema = {
53
+ params: {
54
+ type: 'object',
55
+ required: ['id'],
56
+ properties: { id: { type: 'integer' } },
57
+ },
58
+ };
59
+ const sessionQuerySchema = {
60
+ ...idParamSchema,
61
+ querystring: {
62
+ type: 'object',
63
+ required: ['session'],
64
+ properties: { session: { type: 'string', minLength: 1, maxLength: 100 } },
65
+ },
66
+ };
67
+ const openSchema = {
68
+ ...idParamSchema,
69
+ body: {
70
+ type: 'object',
71
+ additionalProperties: false,
72
+ properties: {
73
+ autoApply: { type: 'boolean' },
74
+ restart: { type: 'boolean' },
75
+ },
76
+ },
77
+ };
78
+ const fileSchema = {
79
+ params: {
80
+ type: 'object',
81
+ required: ['id', 'fileIndex'],
82
+ properties: {
83
+ id: { type: 'integer' },
84
+ fileIndex: { type: 'integer', minimum: 0 },
85
+ },
86
+ },
87
+ querystring: sessionQuerySchema.querystring,
88
+ };
89
+ /**
90
+ * The ONE body in this family that carries typed text.
91
+ *
92
+ * ⚠ `text`'s `maxLength` HERE IS A STRUCTURAL BOUND, NOT THE PRODUCT CAP. Its job is to stop a
93
+ * megabyte-scale body reaching the validator's scans at all; the cap the reader is actually held
94
+ * to is `config.conflictSuggestMaxChars`, checked in `validateConflictEdit` so the refusal is
95
+ * `too_long` with our own sentence rather than an ajv type error. Two numbers, two jobs — do not
96
+ * collapse them into one, and do not put the product cap here where a config change could not
97
+ * move it.
98
+ */
99
+ const editSchema = {
100
+ ...idParamSchema,
101
+ body: {
102
+ type: 'object',
103
+ required: ['sessionId', 'fileIndex', 'regionId', 'fingerprint', 'text'],
104
+ additionalProperties: false,
105
+ properties: {
106
+ sessionId: { type: 'string', minLength: 1, maxLength: 100 },
107
+ fileIndex: { type: 'integer', minimum: 0 },
108
+ regionId: { type: 'integer', minimum: 0 },
109
+ fingerprint: { type: 'string', minLength: 1, maxLength: 128 },
110
+ text: { type: 'string', maxLength: 1_000_000 },
111
+ },
112
+ },
113
+ };
114
+ const commitSchema = {
115
+ ...idParamSchema,
116
+ body: {
117
+ type: 'object',
118
+ required: ['sessionId', 'expectedHeadSha', 'expectedBaseSha', 'modelHash', 'strategy', 'target', 'files'],
119
+ additionalProperties: false,
120
+ properties: {
121
+ sessionId: { type: 'string', minLength: 1, maxLength: 100 },
122
+ expectedHeadSha: { type: 'string', minLength: 4, maxLength: 64 },
123
+ expectedBaseSha: { type: 'string', minLength: 4, maxLength: 64 },
124
+ modelHash: { type: 'string', minLength: 4, maxLength: 128 },
125
+ strategy: { type: 'string', enum: ['merge', 'rebase'] },
126
+ target: {
127
+ type: 'object',
128
+ required: ['kind'],
129
+ additionalProperties: false,
130
+ properties: {
131
+ kind: { type: 'string', enum: ['pr_branch', 'new_branch'] },
132
+ branch: { type: 'string', minLength: 1, maxLength: 255 },
133
+ openPr: { type: 'boolean' },
134
+ },
135
+ },
136
+ files: {
137
+ type: 'array',
138
+ maxItems: 200,
139
+ items: {
140
+ type: 'object',
141
+ required: ['index', 'decisions'],
142
+ additionalProperties: false,
143
+ properties: {
144
+ index: { type: 'integer', minimum: 0 },
145
+ decisions: {
146
+ type: 'array',
147
+ maxItems: 2000,
148
+ items: {
149
+ type: 'object',
150
+ required: ['id', 'decision'],
151
+ additionalProperties: false,
152
+ properties: {
153
+ id: { type: 'integer', minimum: 0 },
154
+ // ⚠ THE ENUM AND THE TWO HANDLES ARE ALL DECLARED HERE OR THEY DO NOT ARRIVE.
155
+ // This object is `additionalProperties: false`, so ajv STRIPS a field it does
156
+ // not name — silently, with no error anywhere — and a commit would then
157
+ // resolve a hand-edited region with no id at all. A test pins `editId`
158
+ // surviving for exactly that reason; the same defect already bit `autoApply`
159
+ // on the open schema and the contact form's honeypot one feature over.
160
+ decision: {
161
+ type: 'string',
162
+ enum: [
163
+ 'base',
164
+ 'ours',
165
+ 'theirs',
166
+ 'both_ours_first',
167
+ 'both_theirs_first',
168
+ 'disjoint_merge',
169
+ 'suggestion',
170
+ 'edited',
171
+ ],
172
+ },
173
+ suggestionId: { type: 'string', minLength: 1, maxLength: 100 },
174
+ editId: { type: 'string', minLength: 1, maxLength: 100 },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ },
180
+ },
181
+ },
182
+ },
183
+ };
184
+ /** Every refusal is `{error, message}` with ONE sentence that states the fact and stops. No
185
+ * sentence here names a region, a hunk, a slot, a stage or an engine. */
186
+ function refuse(reply, status, error, message) {
187
+ reply.status(status);
188
+ return { error, message };
189
+ }
190
+ const notFound = (reply, id) => refuse(reply, 404, 'NotFound', `PR ${id} not found`);
191
+ const SESSION_EXPIRED = 'This session is no longer open. Reopen it and take your decisions again.';
192
+ /**
193
+ * The edit route's refusals, one sentence each.
194
+ *
195
+ * ⚠ EVERY ONE NAMES WHAT IS WRONG WITH THE TEXT AND STOPS. None of them explains what a merge
196
+ * conflict is, none apologises, and none offers a workaround the reader can already see — the
197
+ * textarea is still open with their words in it, and Cancel is right there.
198
+ *
199
+ * ⚠ AND NONE ECHOES THE INPUT. The same rule the suggestion validator's caller follows: the one
200
+ * screen whose job is to show text about to be committed is the last place to render a string
201
+ * we have just said we will not vouch for.
202
+ */
203
+ const EDIT_REFUSAL_SENTENCE = {
204
+ unknown_region: 'That change isn’t part of this session.',
205
+ not_editable: 'Unchanged lines can’t be edited here.',
206
+ moved: 'This change moved while you were editing it. Reopen the resolver.',
207
+ not_text: 'This has characters that can’t be saved to a file. Retype the odd one out.',
208
+ markers: 'This still has conflict markers in it. Take them out and save again.',
209
+ too_long: `An edit can be at most ${config.conflictSuggestMaxChars.toLocaleString('en-GB')} characters.`,
210
+ too_many_edits: 'This session is holding as many edits as it can. Commit what you have.',
211
+ };
212
+ const editRefusal = (refusal, message) => ({ ok: false, refusal, message });
213
+ /**
214
+ * The `(accountId, prId)` a request may act on, or a refusal.
215
+ *
216
+ * ⚠ ONE RESOLVER FOR ALL SEVEN ROUTES. A 404 for another tenant's id is what keeps the family from
217
+ * being an existence oracle, and a per-handler copy of that rule is a per-handler chance to
218
+ * forget it.
219
+ */
220
+ async function requireWritablePr(reply, accountId, id) {
221
+ const ctx = await getPrWriteContext(id, accountId);
222
+ if (!ctx)
223
+ return { ok: false, body: notFound(reply, id) };
224
+ if (!WRITE_PERMISSIONS.has(ctx.viewerPermission ?? '')) {
225
+ return {
226
+ ok: false,
227
+ body: refuse(reply, 403, 'NotPermitted', 'You need write access to resolve conflicts on this pull request.'),
228
+ };
229
+ }
230
+ return { ok: true };
231
+ }
232
+ /**
233
+ * FOUR REFUSALS, FOUR FACTS. A claim can fail for four different reasons and they are NOT
234
+ * interchangeable sentences — nor one sentence with a variable in it.
235
+ *
236
+ * ⚠ THE READER IS ONLY EVER TOLD ABOUT THEIR OWN WORK. "You already have one open" is a fact
237
+ * about this account; "the service is busy" is a fact about the service. Neither names a count,
238
+ * an account or a pull request belonging to anyone else. The predecessor collapsed the last two
239
+ * into "Two pull requests are already being prepared", which in cloud was both false about the
240
+ * reader's work and a disclosure about somebody else's.
241
+ */
242
+ const BUSY_PR = 'This pull request is already being worked on. Give it a moment.';
243
+ const BUSY_ACCOUNT = 'You’re already resolving another pull request. Finish that one first.';
244
+ const BUSY_SERVICE = 'The service is busy. Try again in a moment.';
245
+ const RESTARTING = 'This server is restarting. Try again in a moment.';
246
+ /** A fact about the reader's own windows, like `BUSY_ACCOUNT` — never about the service. */
247
+ const TOO_MANY_STREAMS = 'This resolver is open in too many windows. Close one and try again.';
248
+ /** Live streams one session may hold. Two or three tabs is the real ceiling; past that it is
249
+ * not a reader. */
250
+ const MAX_STREAM_SUBSCRIBERS = 4;
251
+ /** How long the server holds one hijacked socket before ending it itself. Inside Railway's
252
+ * 15-minute request ceiling on purpose, so the close is OURS and the client's recovery path is
253
+ * the one that runs. */
254
+ const STREAM_MAX_MS = 10 * 60_000;
255
+ /** One mapper, so the seven routes cannot disagree about what a claim refusal means. */
256
+ function refuseBusy(reply, reason) {
257
+ switch (reason) {
258
+ case 'pr':
259
+ return refuse(reply, 409, 'Busy', BUSY_PR);
260
+ case 'account':
261
+ return refuse(reply, 409, 'Busy', BUSY_ACCOUNT);
262
+ case 'capacity':
263
+ // 503, not 409: nothing about THIS request conflicts with anything — the service has no
264
+ // room, and the honest status for that is "try later".
265
+ return refuse(reply, 503, 'Busy', BUSY_SERVICE);
266
+ case 'shutdown':
267
+ return refuse(reply, 503, 'Restarting', RESTARTING);
268
+ }
269
+ }
270
+ export async function conflictRoutes(app) {
271
+ // ---- 1. Open ----------------------------------------------------------------------------
272
+ app.post('/api/prs/:id/conflicts', { schema: openSchema }, async (req, reply) => {
273
+ const { id } = req.params;
274
+ const body = (req.body ?? {});
275
+ const accountId = accountIdOf(req);
276
+ const allowed = await requireWritablePr(reply, accountId, id);
277
+ if (!allowed.ok)
278
+ return allowed.body;
279
+ // `merge-tree --write-tree` needs git 2.38. Probed ONCE per process, here rather than on
280
+ // `/api/me` — a version probe on every SPA boot answers a question this route can answer
281
+ // once, and only this route can do anything about the answer.
282
+ if (!(await gitSupportsMergeTree())) {
283
+ return refuse(reply, 501, 'GitUnavailable', 'git isn’t available here. Resolve conflicts on GitHub.');
284
+ }
285
+ const claim = claimSession(accountId, id, {
286
+ restart: body.restart === true,
287
+ autoApply: body.autoApply !== false,
288
+ });
289
+ if (claim.kind === 'busy')
290
+ return refuseBusy(reply, claim.reason);
291
+ if (claim.kind === 'reused') {
292
+ // Re-attaching to the live session is the whole answer to a second tab: no second clone,
293
+ // no second model, and the stream the client is about to subscribe to is already running.
294
+ reply.status(claim.session.status === 'preparing' ? 202 : 200);
295
+ return sessionView(claim.session);
296
+ }
297
+ const rec = claim.session;
298
+ reply.status(202);
299
+ void runOpen(rec, req.log);
300
+ return sessionView(rec);
301
+ });
302
+ // ---- 2. The manifest --------------------------------------------------------------------
303
+ app.get('/api/prs/:id/conflicts', { schema: sessionQuerySchema }, async (req, reply) => {
304
+ const { id } = req.params;
305
+ const { session } = req.query;
306
+ const accountId = accountIdOf(req);
307
+ const allowed = await requireWritablePr(reply, accountId, id);
308
+ if (!allowed.ok)
309
+ return allowed.body;
310
+ const rec = getSession(accountId, id, session);
311
+ if (!rec)
312
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
313
+ return sessionView(rec);
314
+ });
315
+ // ---- 3. The ONE stream ------------------------------------------------------------------
316
+ //
317
+ // Prepare progress AND the commit's phases arrive here; there is no second channel and no
318
+ // second job registry. It is a GET, so `req.raw.on('close')` IS the disconnect signal — the
319
+ // reply-socket rule is a POST rule (a POST's request `close` fires the moment Fastify has read
320
+ // the body), and both POSTs in this family answer in milliseconds and hold no disconnect state.
321
+ app.get('/api/prs/:id/conflicts/stream', { schema: sessionQuerySchema }, async (req, reply) => {
322
+ const { id } = req.params;
323
+ const { session } = req.query;
324
+ const accountId = accountIdOf(req);
325
+ // ⚠ BEFORE `reply.hijack()`. Once the socket is hijacked there is no clean way to 404, and
326
+ // an ownership check that runs after it is an ownership check that cannot refuse.
327
+ const allowed = await requireWritablePr(reply, accountId, id);
328
+ if (!allowed.ok)
329
+ return allowed.body;
330
+ const rec = getSession(accountId, id, session);
331
+ if (!rec)
332
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
333
+ // ⚠ ALSO BEFORE `reply.hijack()`. A hijacked socket is held until the client drops it or the
334
+ // session ends, and nothing else bounds how many of them one caller may hold: this route is
335
+ // on the 600/min `read` bucket, so a client that opens streams and never reads them
336
+ // accumulates sockets and 15-second heartbeat timers until the proxy's own request ceiling
337
+ // reaps them — about 9,000 of each in cloud. A real reader has one per open tab.
338
+ if (rec.subscribers.size >= MAX_STREAM_SUBSCRIBERS) {
339
+ return refuse(reply, 409, 'Busy', TOO_MANY_STREAMS);
340
+ }
341
+ reply.hijack();
342
+ const raw = reply.raw;
343
+ raw.writeHead(200, {
344
+ 'Content-Type': 'text/event-stream',
345
+ 'Cache-Control': 'no-cache, no-transform',
346
+ Connection: 'keep-alive',
347
+ 'X-Accel-Buffering': 'no',
348
+ });
349
+ const send = (e) => {
350
+ if (!raw.writableEnded)
351
+ raw.write(`data: ${JSON.stringify(e)}\n\n`);
352
+ };
353
+ let closed = false;
354
+ const hb = setInterval(() => {
355
+ if (!raw.writableEnded)
356
+ raw.write(': hb\n\n');
357
+ }, 15_000);
358
+ // ⚠ THE SERVER ENDS IT FIRST, DELIBERATELY. A stream is otherwise held for as long as the
359
+ // client keeps the socket open, and the session outliving the stream is the design — so the
360
+ // SPA already recovers from a stream that ends (`useConflictSession` falls through to the
361
+ // manifest poll, which carries the whole commit state). Ending it ourselves, well inside the
362
+ // proxy's 15-minute request ceiling, costs the reader nothing and is what stops a held socket
363
+ // being unbounded in a process every tenant shares.
364
+ const lifetime = setTimeout(() => cleanup(), STREAM_MAX_MS);
365
+ const cleanup = () => {
366
+ if (closed)
367
+ return;
368
+ closed = true;
369
+ clearInterval(hb);
370
+ clearTimeout(lifetime);
371
+ unsub();
372
+ if (!raw.writableEnded)
373
+ raw.end();
374
+ };
375
+ // ⚠ SUBSCRIBE, THEN SNAPSHOT. The build is already running by the time a client can reach
376
+ // this route, so a `ready` emitted between the snapshot and the subscribe would be lost and
377
+ // the overlay would sit on "Reading the conflicting files…" forever.
378
+ const unsub = subscribe(rec, (e) => {
379
+ send(e);
380
+ if (e.type === 'done')
381
+ cleanup();
382
+ });
383
+ req.raw.on('close', cleanup);
384
+ send({ type: 'snapshot', session: sessionView(rec) });
385
+ // Deliberately NOT terminal on a settled session: a `ready` session's commit phases arrive
386
+ // on this same stream, minutes later. The client closes it, or the session is dropped.
387
+ });
388
+ // ---- 4. One file's regions --------------------------------------------------------------
389
+ app.get('/api/prs/:id/conflicts/files/:fileIndex', { schema: fileSchema }, async (req, reply) => {
390
+ const { id, fileIndex } = req.params;
391
+ const { session } = req.query;
392
+ const accountId = accountIdOf(req);
393
+ const allowed = await requireWritablePr(reply, accountId, id);
394
+ if (!allowed.ok)
395
+ return allowed.body;
396
+ const rec = getSession(accountId, id, session);
397
+ if (!rec)
398
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
399
+ const model = rec.model;
400
+ const content = model ? conflictFileContent(model, fileIndex, rec.autoApply) : null;
401
+ if (!content) {
402
+ return refuse(reply, 400, 'UnknownFileIndex', 'That file isn’t part of this session. Reopen the resolver.');
403
+ }
404
+ return content;
405
+ });
406
+ // ---- 5. One region's text, typed by the reader ------------------------------------------
407
+ //
408
+ // ⚠ THIS IS THE ONE PLACE IN THE FAMILY THAT TAKES FILE CONTENT, AND EVERYTHING ABOUT IT IS
409
+ // SHAPED BY THAT. It validates BEFORE it mints, so an id that exists is an id whose text has
410
+ // been checked; it refuses rather than trimming, so nothing is stored that the reader did not
411
+ // see; and it decides NOTHING — a successful edit hands back a handle, and the region stays
412
+ // undecided until the SPA sends `{decision:'edited', editId}`.
413
+ //
414
+ // ⚠ IT IS SYNCHRONOUS AND CHEAP, UNLIKE THE OTHER TWO POSTS. No clone, no fetch, no git: a
415
+ // string scan and a Map write against a session already in memory. So it answers 200 with the
416
+ // outcome rather than 202 with a promise.
417
+ //
418
+ // A REFUSAL IS A 200 CARRYING `{ok:false}`, the suggestion route's shape. These are outcomes
419
+ // the reader acts on — fix the text, take a side instead — not transport errors, and the SPA
420
+ // renders the server's sentence verbatim either way. The three real errors (not this
421
+ // account's PR, no push rights, no session) keep their status codes.
422
+ app.post('/api/prs/:id/conflicts/edit', { schema: editSchema }, async (req, reply) => {
423
+ const { id } = req.params;
424
+ const body = req.body;
425
+ const accountId = accountIdOf(req);
426
+ const allowed = await requireWritablePr(reply, accountId, id);
427
+ if (!allowed.ok)
428
+ return allowed.body;
429
+ const rec = getSession(accountId, id, body.sessionId);
430
+ const model = rec?.model ?? null;
431
+ // The family's own answer for a dead session, not a `{ok:false}` refusal: the reader's whole
432
+ // resolve is gone, which is not a fact about the text they just typed.
433
+ if (!rec || !model)
434
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
435
+ const file = model.files.find((f) => f.index === body.fileIndex) ?? null;
436
+ const region = file?.regions.find((r) => r.id === body.regionId) ?? null;
437
+ if (!file || !region || file.unsupported !== null) {
438
+ return editRefusal('unknown_region', EDIT_REFUSAL_SENTENCE.unknown_region);
439
+ }
440
+ // Context lines are read-only: an `unchanged` region is what the edit sits BETWEEN, it takes
441
+ // no decision, and the fold would ignore a decision on it anyway (rule 1).
442
+ if (region.kind === 'unchanged') {
443
+ return editRefusal('not_editable', EDIT_REFUSAL_SENTENCE.not_editable);
444
+ }
445
+ // ⚠ THE CONTENT PIN. The id is an ADDRESS and a rebuild can hand the same address different
446
+ // bytes; without this, text written against one version of a region could be redeemed
447
+ // against another — text nobody read, in the place it lands. The suggestion route makes
448
+ // exactly this check for exactly this reason.
449
+ if (region.fingerprint !== body.fingerprint) {
450
+ return editRefusal('moved', EDIT_REFUSAL_SENTENCE.moved);
451
+ }
452
+ // ⚠ THE REGION'S OWN INVISIBLE BYTES, INHERITED RATHER THAN READ BACK OFF THE WIRE. A
453
+ // textarea normalises every CRLF it is handed to a bare LF before the SPA can see it, so
454
+ // without this one keystroke in a Windows-authored file rewrote the whole hunk's line
455
+ // endings; and the file's own UTF-8 BOM, which `model.ts` deliberately keeps as a character,
456
+ // would otherwise refuse the first region of every such file as `not_text`. See
457
+ // `editShapeFor`.
458
+ const shape = editShapeFor([region.base, region.ours, region.theirs], file.regions[0]?.id === region.id);
459
+ const checked = validateConflictEdit(body.text, config.conflictSuggestMaxChars, shape);
460
+ if (!checked.ok)
461
+ return editRefusal(checked.refusal, EDIT_REFUSAL_SENTENCE[checked.refusal]);
462
+ const editId = storeEdit(rec, {
463
+ fileIndex: file.index,
464
+ regionId: region.id,
465
+ lines: checked.lines,
466
+ // Fold rule 4. The reader edited lines INSIDE a region, not the file's final newline, so
467
+ // there is nothing in their text to read this off — and a payload-bearing decision has to
468
+ // carry one. The ours side's is the same inheritance a suggestion takes, and it matters
469
+ // only when the edited region is the file's last.
470
+ endsWithNewline: file.terminators.ours,
471
+ });
472
+ if (editId == null) {
473
+ return editRefusal('too_many_edits', EDIT_REFUSAL_SENTENCE.too_many_edits);
474
+ }
475
+ const response = {
476
+ ok: true,
477
+ // ⚠ THE SERVER'S OWN SPLIT, ECHOED. The centre pane renders these, so it renders exactly
478
+ // the lines the commit will splice — rather than the client's second opinion about where
479
+ // its own text breaks.
480
+ edit: { fileIndex: file.index, regionId: region.id, editId, lines: checked.lines },
481
+ };
482
+ return response;
483
+ });
484
+ // ---- 6. Commit --------------------------------------------------------------------------
485
+ app.post('/api/prs/:id/conflicts/commit', { schema: commitSchema }, async (req, reply) => {
486
+ const { id } = req.params;
487
+ const body = req.body;
488
+ const accountId = accountIdOf(req);
489
+ const allowed = await requireWritablePr(reply, accountId, id);
490
+ if (!allowed.ok)
491
+ return allowed.body;
492
+ const rec = getSession(accountId, id, body.sessionId);
493
+ if (!rec)
494
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
495
+ const model = rec.model;
496
+ if (!model)
497
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
498
+ if (rec.status === 'clean' || model.files.length === 0) {
499
+ return refuse(reply, 409, 'NoConflicts', `This pull request no longer conflicts with ${model.baseRef}.`);
500
+ }
501
+ // ---- the pins ----
502
+ // A decision made against different bytes is not a decision. Three separate facts, three
503
+ // separate sentences: which commit the branch was on, which one the base was on, and
504
+ // whether the merge itself came out the same. `modelHash` is the only one that can catch a
505
+ // fold or chunker change (`CONFLICT_MODEL_VERSION` is folded into it), and it is checked
506
+ // against the SESSION's hash rather than a recomputation so the two cannot drift.
507
+ if (body.expectedHeadSha !== model.headSha) {
508
+ return refuse(reply, 409, 'HeadMoved', `Someone pushed to ${model.headRef} while you were resolving. Reopen the resolver.`);
509
+ }
510
+ if (body.expectedBaseSha !== model.baseSha) {
511
+ return refuse(reply, 409, 'BaseMoved', `${model.baseRef} moved and the conflicts changed. Reopen the resolver.`);
512
+ }
513
+ if (body.modelHash !== rec.modelHash) {
514
+ return refuse(reply, 409, 'ModelStale', 'The conflicts changed while you were resolving them. Reopen the resolver.');
515
+ }
516
+ if (body.strategy === 'rebase' && !model.strategies.includes('rebase')) {
517
+ return refuse(reply, 409, 'RebaseNotOffered', model.rebaseUnavailableReason ??
518
+ `This branch has ${model.commitsAboveBase} commits. Rebasing can conflict once per commit — merge instead.`);
519
+ }
520
+ if (body.target.kind === 'new_branch') {
521
+ const branchRefusal = await checkNewBranch(reply, model.reservedBranchNames, model.baseRef, body.target.branch);
522
+ if (branchRefusal)
523
+ return branchRefusal;
524
+ }
525
+ const planRefusal = preflightDecisions(reply, rec, body);
526
+ if (planRefusal)
527
+ return planRefusal;
528
+ // ⚠ CLAIMED SYNCHRONOUSLY, with no `await` between the check and the write: two clicks a
529
+ // tick apart must not become two pushes.
530
+ const slot = claimCommitSlot(rec);
531
+ if (!slot.ok)
532
+ return refuseBusy(reply, slot.reason);
533
+ reply.status(202);
534
+ void runCommit(rec, body, slot.signal, req.log);
535
+ return sessionView(rec);
536
+ });
537
+ // ---- 7. Close -------------------------------------------------------------------------
538
+ app.delete('/api/prs/:id/conflicts', { schema: sessionQuerySchema }, async (req, reply) => {
539
+ const { id } = req.params;
540
+ const { session } = req.query;
541
+ const accountId = accountIdOf(req);
542
+ const allowed = await requireWritablePr(reply, accountId, id);
543
+ if (!allowed.ok)
544
+ return allowed.body;
545
+ // 204 whether or not there was anything to drop: "it is not there" is what the caller asked
546
+ // for, and a session that expired on its own is not an error the reader can act on.
547
+ dropSession(accountId, id, session);
548
+ reply.status(204);
549
+ return null;
550
+ });
551
+ }
552
+ /* ═════════════════════════════ the two jobs ═════════════════════════════ */
553
+ /**
554
+ * Build the model behind the 202.
555
+ *
556
+ * ⚠ NEVER REJECTS. It is fired with `void`, so an escaping rejection is an unhandled one — and
557
+ * the session it was building would sit at `preparing` until the TTL, with the overlay reading
558
+ * "Reading the conflicting files…" the whole time.
559
+ */
560
+ async function runOpen(rec, log) {
561
+ try {
562
+ const result = await buildConflictModel({
563
+ accountId: rec.accountId,
564
+ prId: rec.prId,
565
+ // ⚠ THE SAME id the session was opened with, twice over: it namespaces the fetch refs
566
+ // (`refs/pierre/conflict/<sessionId>/…`, never FETCH_HEAD, which is one file per clone
567
+ // and shared across jobs) and it names the two refs the land path's teardown deletes.
568
+ sessionId: rec.sessionId,
569
+ onPhase: (p) => setPreparePhase(rec, p),
570
+ });
571
+ if (!result) {
572
+ // The PR stopped being this account's between the ownership check and here.
573
+ settleFailed(rec, 'objects_unavailable', 'This pull request is no longer available.');
574
+ return;
575
+ }
576
+ switch (result.status) {
577
+ case 'ready':
578
+ case 'clean':
579
+ settleReady(rec, result.model, conflictModelHash(result.model), result.status === 'clean');
580
+ return;
581
+ case 'failed':
582
+ settleFailed(rec, result.code, result.message);
583
+ return;
584
+ case 'moved':
585
+ // Unreachable: the open pins nothing, so the builder has nothing to find moved. Settled
586
+ // rather than ignored, because a session stuck at `preparing` is a spinner forever.
587
+ settleFailed(rec, 'objects_unavailable', 'This pull request moved while it was opening.');
588
+ return;
589
+ }
590
+ }
591
+ catch (err) {
592
+ log.error({ err, prId: rec.prId }, 'conflict model build failed');
593
+ settleFailed(rec, 'objects_unavailable', 'Couldn’t work out the conflicts in this pull request.');
594
+ }
595
+ }
596
+ /** Land the resolution behind the 202. NEVER REJECTS, for the same reason `runOpen` does not. */
597
+ async function runCommit(rec, body, signal, log) {
598
+ const model = rec.model;
599
+ if (!model) {
600
+ settleCommitFailed(rec, 'SessionExpired', SESSION_EXPIRED);
601
+ return;
602
+ }
603
+ try {
604
+ const result = await landConflictResolution({
605
+ accountId: rec.accountId,
606
+ prId: rec.prId,
607
+ sessionId: rec.sessionId,
608
+ model,
609
+ body,
610
+ suggestions: rec.suggestions,
611
+ edits: rec.edits,
612
+ onPhase: (p) => setCommitPhase(rec, p),
613
+ signal,
614
+ log,
615
+ });
616
+ settleCommitDone(rec, result);
617
+ }
618
+ catch (err) {
619
+ const code = err?.code;
620
+ const message = err instanceof Error ? err.message : String(err);
621
+ if (isLandErrorCode(code)) {
622
+ settleCommitFailed(rec, code, message);
623
+ return;
624
+ }
625
+ log.error({ err, prId: rec.prId }, 'conflict commit failed');
626
+ settleCommitFailed(rec, 'GitFailed', 'The resolved commit didn’t reach GitHub.');
627
+ }
628
+ }
629
+ const LAND_ERROR_CODES = new Set([
630
+ 'ModelStale',
631
+ 'HeadMoved',
632
+ 'BaseMoved',
633
+ 'SessionExpired',
634
+ 'NoConflicts',
635
+ 'NotPermitted',
636
+ 'NothingToCommit',
637
+ 'UnknownFileIndex',
638
+ 'IncompleteDecisions',
639
+ 'UnknownSuggestion',
640
+ 'UnknownEdit',
641
+ 'InvalidBranch',
642
+ 'ReservedBranch',
643
+ 'BranchExists',
644
+ 'PushDenied',
645
+ 'RebaseNotOffered',
646
+ 'GitFailed',
647
+ 'Cancelled',
648
+ ]);
649
+ function isLandErrorCode(code) {
650
+ return typeof code === 'string' && LAND_ERROR_CODES.has(code);
651
+ }
652
+ /* ═════════════════════════════ the cheap pre-flight ═════════════════════════════ */
653
+ /**
654
+ * The new-branch name, before the 202.
655
+ *
656
+ * ⚠ THIS IS A PRE-FLIGHT, NOT THE AUTHORITY. `land.ts` validates the name again on its own path
657
+ * — it has to, because it is also reachable with a name this never saw. What this buys is a
658
+ * real status code and the table's own sentence instead of a 202 followed by a failure frame.
659
+ * `assertPushTarget` is the SAME guard the push itself goes through (`coding/git.ts`), so the
660
+ * two cannot disagree about what git accepts.
661
+ */
662
+ async function checkNewBranch(reply, reserved, baseRef, branch) {
663
+ const wanted = (branch ?? '').trim();
664
+ if (!wanted) {
665
+ return refuse(reply, 400, 'InvalidBranch', 'Give the new branch a name.');
666
+ }
667
+ const lowered = wanted.toLowerCase();
668
+ // Reserved first: these are real, well-formed branch names, so the format check would pass
669
+ // them and the reader would be told the name is malformed when it is merely taken.
670
+ if (reserved.some((r) => r.toLowerCase() === lowered)) {
671
+ const what = lowered === baseRef.toLowerCase() ? 'this PR’s base branch' : 'the default branch';
672
+ return refuse(reply, 409, 'ReservedBranch', `${wanted} is ${what}. Pick another name.`);
673
+ }
674
+ try {
675
+ await assertPushTarget(wanted, [...reserved]);
676
+ }
677
+ catch {
678
+ return refuse(reply, 400, 'InvalidBranch', 'Branch names start with a letter or digit and can use letters, digits, . _ / and -.');
679
+ }
680
+ return null;
681
+ }
682
+ /**
683
+ * The decisions, before the 202: is every file in this model, and is every change decided?
684
+ *
685
+ * ⚠ ALSO A PRE-FLIGHT. `land.ts`'s `planResolution` is the authority and re-runs all of it
686
+ * against the REBUILT model — which is the one that matters, because the base may have moved
687
+ * since. This exists so an incomplete set is a 400 naming the file rather than a 202 followed by
688
+ * a failure frame. Both call the same `allowedDecisions`; neither invents a default, because a
689
+ * silently defaulted region is a line of code nobody chose.
690
+ */
691
+ function preflightDecisions(reply, rec, body) {
692
+ const model = rec.model;
693
+ if (!model)
694
+ return refuse(reply, 409, 'SessionExpired', SESSION_EXPIRED);
695
+ const byIndex = new Map(model.files.map((f) => [f.index, f]));
696
+ const seen = new Set();
697
+ for (const req of body.files) {
698
+ const file = byIndex.get(req.index);
699
+ if (!file || seen.has(req.index)) {
700
+ return refuse(reply, 400, 'UnknownFileIndex', 'That file isn’t part of this session. Reopen the resolver.');
701
+ }
702
+ seen.add(req.index);
703
+ const decided = new Set();
704
+ for (const d of req.decisions) {
705
+ decided.add(d.id);
706
+ // ⚠ BOTH HANDLE-BEARING MEMBERS, AND TWO SENTENCES. `land.ts` re-checks each against the
707
+ // region it names — the authority, because it runs against the rebuilt model — but a dead
708
+ // handle is worth catching here for the real status code, and one refusal sends the reader
709
+ // back to Claude while the other sends them back to their own text.
710
+ if (d.decision === 'suggestion') {
711
+ if (d.suggestionId == null || !rec.suggestions.has(d.suggestionId)) {
712
+ return refuse(reply, 400, 'UnknownSuggestion', 'That suggestion has expired. Ask Claude again.');
713
+ }
714
+ }
715
+ if (d.decision === 'edited') {
716
+ if (d.editId == null || !rec.edits.has(d.editId)) {
717
+ return refuse(reply, 400, 'UnknownEdit', 'One of your edits has expired. Make it again.');
718
+ }
719
+ }
720
+ }
721
+ // Rule 2 of the fold: EXHAUSTIVE over the file's non-`unchanged` regions. A one-sided change
722
+ // is decidable too — leaving it out is not "keep the default", it is nobody choosing.
723
+ const missing = file.regions.filter((r) => r.kind !== 'unchanged' && !decided.has(r.id)).length;
724
+ if (missing > 0) {
725
+ return refuse(reply, 400, 'IncompleteDecisions', `${file.path} still has ${missing} conflict${missing === 1 ? '' : 's'} to decide.`);
726
+ }
727
+ }
728
+ return null;
729
+ }
730
+ //# sourceMappingURL=conflicts.js.map