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,675 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { foldFile, foldToText, } from '../shared/index.js';
5
+ import { getAccessToken, getAccountById } from '../auth/account.js';
6
+ import { pushForceWithLease, pushRef } from '../coding/git.js';
7
+ import { protectedRefsFor } from '../coding/git-ops.js';
8
+ import { disarmAutoMerge, getPrWriteContext, WRITE_PERMISSIONS } from '../db/queries.js';
9
+ import { ghRestGetText } from '../github/client.js';
10
+ import { createPullRequest, fetchPrHeadInfo } from '../github/mutations.js';
11
+ import { cleanupCloneCache, ensureClone, withRepoLock } from '../review/clone-manager.js';
12
+ import { asSyncLogger, settlePrAfterWrite, syncNewPrBesideAfterWrite, } from '../sync/resync-after-write.js';
13
+ import { git, gitTry } from './git.js';
14
+ import { conflictModelHash } from './hash.js';
15
+ import { allowedDecisions, buildConflictModel } from './model.js';
16
+ /** The land path's own coded error. `coding/git.ts`'s `codedError` carries `CodingErrorCode`,
17
+ * a different vocabulary; this one is the wire's `ConflictLandErrorCode` so the route can
18
+ * pass the code straight through to `ConflictCommitState.error`. */
19
+ export function landError(code, message) {
20
+ const err = new Error(message);
21
+ err.code = code;
22
+ return err;
23
+ }
24
+ export async function landConflictResolution(args) {
25
+ const { model, body, signal } = args;
26
+ const repoKey = `${model.owner}/${model.name}`;
27
+ const ns = `refs/pierre/conflict/${args.sessionId}`;
28
+ args.onPhase('preparing');
29
+ // ---- the free pins, before any I/O ----
30
+ // A decision made against different bytes is not a decision, so the session's own model
31
+ // must be the one the client folded. This is cheap and catches a client bug without
32
+ // spending a clone, a fetch and a merge on it.
33
+ if (body.expectedHeadSha !== model.headSha || body.expectedBaseSha !== model.baseSha) {
34
+ throw landError('ModelStale', 'This session was built against different commits.');
35
+ }
36
+ if (conflictModelHash(model) !== body.modelHash) {
37
+ throw landError('ModelStale', 'This merge changed while it was open. Reopen it.');
38
+ }
39
+ if (model.files.length === 0) {
40
+ throw landError('NoConflicts', 'This merge has no conflicts.');
41
+ }
42
+ if (body.strategy === 'rebase' && !model.strategies.includes('rebase')) {
43
+ throw landError('RebaseNotOffered', model.rebaseUnavailableReason ?? 'Rebase is not available for this pull request.');
44
+ }
45
+ // ---- the model is the allow-list, and the fold is the same one the pane rendered ----
46
+ // Pure CPU over strings already in memory: no side effect can survive a refusal here, which
47
+ // is why it runs before the rebuild rather than after it. `IncompleteDecisions` is worth
48
+ // getting for free.
49
+ const plan = planResolution(model, body.files, args.suggestions, args.edits);
50
+ // ⚠ REBASE IS ALL-OR-NOTHING. The model only OFFERS rebase when every conflicted file is
51
+ // resolvable, but the request can still leave one out — and a rebased commit that reparents
52
+ // onto the base while a conflicted file keeps head's blob has silently dropped the base
53
+ // branch's version of it, with no marker and no second chance to notice.
54
+ if (body.strategy === 'rebase' && !plan.full) {
55
+ throw landError('RebaseNotOffered', 'Rebase needs every conflicted file resolved here.');
56
+ }
57
+ if (body.target.kind === 'new_branch') {
58
+ await assertNewBranchName(model, body.target.branch);
59
+ }
60
+ assertNotCancelled(signal);
61
+ let tmpDir = null;
62
+ let cloneDir = null;
63
+ try {
64
+ // ⚠ Everything ABOVE this line is pure — no token, no clone, no ref. A refusal up there
65
+ // acquired nothing, and the session's own fetch refs are the SESSION's to drop; the
66
+ // teardown below can only reach them once we hold a clone to delete them in.
67
+ const token = await getAccessToken(args.accountId);
68
+ // The clone is resolved BEFORE the rebuild so that teardown can always reach the session
69
+ // refs — including on the bails (`HeadMoved`, `ModelStale`) where the rebuild is the thing
70
+ // that created them. `withRepoLock` is a queue, not a reentrant lock, so this acquisition
71
+ // must complete before `buildConflictModel` takes its own.
72
+ cloneDir = await withRepoLock(repoKey, () => ensureClone(model.owner, model.name, token));
73
+ // ---- re-derive through the ONE builder ----
74
+ // Not a second derivation path: the same function, so `conflictModelHash` keeps exactly
75
+ // one producer. Only the HEAD is pinned — the base branch is allowed to move under us,
76
+ // and rule 3 below decides whether that matters.
77
+ args.onPhase('fetching');
78
+ const rebuilt = await buildConflictModel({
79
+ accountId: args.accountId,
80
+ prId: args.prId,
81
+ expectHeadSha: body.expectedHeadSha,
82
+ sessionId: args.sessionId,
83
+ onPhase: (p) => args.onPhase(p === 'cloning' || p === 'fetching' ? 'fetching' : 'merging'),
84
+ });
85
+ if (!rebuilt) {
86
+ throw landError('NotPermitted', 'This pull request is no longer available.');
87
+ }
88
+ if (rebuilt.status === 'moved') {
89
+ throw landError('HeadMoved', `The pull request moved to ${rebuilt.headSha.slice(0, 7)} while this was open.`);
90
+ }
91
+ if (rebuilt.status === 'clean') {
92
+ throw landError('NoConflicts', 'This merge no longer conflicts.');
93
+ }
94
+ if (rebuilt.status === 'failed') {
95
+ throw landError('GitFailed', rebuilt.message);
96
+ }
97
+ const fresh = rebuilt.model;
98
+ assertNotCancelled(signal);
99
+ // Rule 3: THE BASE MOVED. Reported, never silent, and never a reason to refuse on its own
100
+ // — every contested region the user decided is byte-identical, and the merge picking up
101
+ // the base branch's other commits is what merging the base MEANS.
102
+ //
103
+ // ⚠ The comparison is the CONFLICT CONTENT, not the model hash. `conflictModelHash` folds
104
+ // in `baseSha` and the merged tree oid, both of which move on any base commit at all —
105
+ // so testing the hash here would make this branch unreachable and turn every base advance
106
+ // into `ModelStale`. What has to be unchanged is the material the decisions were made
107
+ // against, which is exactly what `conflictContentMatches` compares.
108
+ const baseAdvanced = fresh.baseSha !== model.baseSha;
109
+ const same = baseAdvanced
110
+ ? conflictContentMatches(model, fresh)
111
+ : conflictModelHash(fresh) === body.modelHash;
112
+ if (!same) {
113
+ throw landError('ModelStale', 'This merge changed while it was open. Reopen it.');
114
+ }
115
+ // ⚠ RE-CHECKED AGAINST THE REBUILT MODEL, not just the session's. A base advance moves
116
+ // the MERGE BASE, and with it the commit count above it — so a PR that was a single
117
+ // commit when the session opened can be two by the time it lands, and reparenting that
118
+ // onto the base tip would silently squash the second commit away, keeping only the
119
+ // first's message.
120
+ if (body.strategy === 'rebase' && !fresh.strategies.includes('rebase')) {
121
+ throw landError('RebaseNotOffered', fresh.rebaseUnavailableReason ?? 'Rebase is not available for this pull request.');
122
+ }
123
+ // ---- build the tree ----
124
+ args.onPhase('committing');
125
+ tmpDir = mkdtempSync(join(tmpdir(), 'pierre-conflict-land-'));
126
+ const indexEnv = { GIT_INDEX_FILE: join(tmpDir, 'index') };
127
+ const built = await withRepoLock(repoKey, async () => {
128
+ const dir = cloneDir;
129
+ // FULL seeds from merge-tree's own tree, so every file NOBODY conflicted over already
130
+ // carries the merge's answer. PARTIAL seeds from HEAD, so an unresolved file keeps
131
+ // head's blob oid EXACTLY and the base branch's version of it is not silently dropped.
132
+ const seed = plan.full ? fresh.mergedTreeSha : fresh.headSha;
133
+ await git(['read-tree', seed], dir, undefined, indexEnv);
134
+ const entries = [];
135
+ for (const file of plan.resolved) {
136
+ const hashed = await git(['hash-object', '-w', '--stdin'], dir, file.content);
137
+ const oid = hashed.stdout.toString('utf8').trim();
138
+ if (!/^[0-9a-f]{40,64}$/.test(oid)) {
139
+ throw landError('GitFailed', `git could not store the resolved ${file.path}.`);
140
+ }
141
+ entries.push(`${file.mode} ${oid}\t${file.path}`);
142
+ }
143
+ if (entries.length > 0) {
144
+ await git(['update-index', '-z', '--index-info'], dir, Buffer.from(`${entries.join('\0')}\0`, 'utf8'), indexEnv);
145
+ }
146
+ // ⚠ FULL means the merge commit claims every conflict was settled. merge-tree's tree
147
+ // stores CONFLICT-MARKER CONTENT at each conflicted path (MEASURED via
148
+ // `cat-file -p $TREE:f.txt`), so a path we seeded from it and then failed to overwrite
149
+ // would commit `<<<<<<<` under a two-parent commit. A miss is a refusal, never a commit.
150
+ if (plan.full) {
151
+ const written = new Set(plan.resolved.map((f) => f.path));
152
+ for (const f of fresh.files) {
153
+ if (!written.has(f.path)) {
154
+ throw landError('GitFailed', `The merged tree still holds a conflict in ${f.path}.`);
155
+ }
156
+ }
157
+ }
158
+ const newTree = (await git(['write-tree'], dir, undefined, indexEnv)).stdout
159
+ .toString('utf8')
160
+ .trim();
161
+ // A PARTIAL whose every decision was "ours" produces head's own tree — a commit that
162
+ // says nothing and pushes nothing. The FULL path is allowed an identical tree: that is
163
+ // `-s ours`, and the merge commit itself is the point.
164
+ if (!plan.full) {
165
+ const headTree = (await git(['rev-parse', `${fresh.headSha}^{tree}`], dir)).stdout
166
+ .toString('utf8')
167
+ .trim();
168
+ if (newTree === headTree) {
169
+ throw landError('NothingToCommit', 'Every file already reads exactly as it does on this branch.');
170
+ }
171
+ }
172
+ const ident = await identFor(args.accountId);
173
+ const commitEnv = {
174
+ GIT_COMMITTER_NAME: ident.name,
175
+ GIT_COMMITTER_EMAIL: ident.email,
176
+ GIT_AUTHOR_NAME: ident.name,
177
+ GIT_AUTHOR_EMAIL: ident.email,
178
+ };
179
+ let message;
180
+ const parents = [];
181
+ if (body.strategy === 'rebase') {
182
+ // The rebase equivalence: for a SINGLE commit C on merge base M rebased onto base tip
183
+ // B, the three-way is (M, B, C) — the same three inputs as the merge with the sides
184
+ // swapped — so the resolved tree IS the merge path's tree and the rebased commit is
185
+ // just that tree reparented onto B. No `git rebase` process runs, so there is no
186
+ // stall, no `--continue` and no abort path to get wrong.
187
+ const original = await readCommitIdent(cloneDir, fresh.headSha);
188
+ commitEnv.GIT_AUTHOR_NAME = original.name;
189
+ commitEnv.GIT_AUTHOR_EMAIL = original.email;
190
+ commitEnv.GIT_AUTHOR_DATE = original.date;
191
+ message = original.message;
192
+ parents.push('-p', fresh.baseSha);
193
+ }
194
+ else if (plan.full) {
195
+ message = fullMergeMessage(fresh, plan);
196
+ parents.push('-p', fresh.headSha, '-p', fresh.baseSha);
197
+ }
198
+ else {
199
+ message = partialMessage(fresh, plan);
200
+ parents.push('-p', fresh.headSha);
201
+ }
202
+ const commit = await git(['commit-tree', newTree, ...parents, '-F', '-'], dir, Buffer.from(capMessage(message), 'utf8'), commitEnv);
203
+ return { newTree, commitSha: commit.stdout.toString('utf8').trim() };
204
+ });
205
+ assertNotCancelled(signal);
206
+ /* ───────────────────────── push safety ─────────────────────────
207
+ Immediately before the irreversible half, and after the tree and the commit already
208
+ exist, so the window between the checks and the push is milliseconds — the
209
+ `auto-merge-runner.ts` land-time re-check pattern. */
210
+ args.onPhase('pushing');
211
+ const ctx = await getPrWriteContext(args.prId, args.accountId);
212
+ if (!ctx || ctx.state !== 'open') {
213
+ throw landError('NotPermitted', 'This pull request is no longer open.');
214
+ }
215
+ if (!WRITE_PERMISSIONS.has(ctx.viewerPermission ?? '')) {
216
+ throw landError('NotPermitted', 'You do not have write access to this repository.');
217
+ }
218
+ const info = await fetchPrHeadInfo(token, model.owner, model.name, model.number);
219
+ if (info.headSha !== body.expectedHeadSha) {
220
+ throw landError('HeadMoved', `The pull request moved to ${info.headSha.slice(0, 7)} while this was open.`);
221
+ }
222
+ // The PR head branch lives in the HEAD repo, which for a fork PR is not the watched one.
223
+ // A NEW branch always lands in the watched repo instead, which is why the fork check is
224
+ // the pr_branch path's alone.
225
+ const target = body.target;
226
+ const toPrBranch = target.kind === 'pr_branch';
227
+ let pushOwner = model.owner;
228
+ let pushName = model.name;
229
+ if (target.kind === 'pr_branch') {
230
+ if (info.isFork && !info.maintainerCanModify) {
231
+ throw landError('PushDenied', 'This pull request comes from a fork that does not allow maintainer edits. Push to a new branch instead.');
232
+ }
233
+ const slash = info.headRepoFullName.indexOf('/');
234
+ if (slash > 0) {
235
+ pushOwner = info.headRepoFullName.slice(0, slash);
236
+ pushName = info.headRepoFullName.slice(slash + 1);
237
+ }
238
+ }
239
+ // ⚠ DISARM FIRST, and not for tidiness. A merge-strategy resolution commit has exactly
240
+ // the two-parent shape `isOurUpdateMerge` proves against, so an intent still holding a
241
+ // live `updateIssuedAgainstOid` would ADOPT this commit, re-pin to it and land code the
242
+ // reader never consented to merge. Disarming makes that unreachable instead of relying on
243
+ // the window being narrow.
244
+ const autoMergeDisarmed = await disarmAutoMerge(args.accountId, args.prId);
245
+ const protect = await protectedRefsFor(args.accountId, model.owner, model.name, model.number, pushOwner, pushName);
246
+ const push = {
247
+ worktree: cloneDir,
248
+ owner: pushOwner,
249
+ name: pushName,
250
+ token,
251
+ committish: built.commitSha,
252
+ protect,
253
+ };
254
+ let branch;
255
+ let compareUrl = null;
256
+ let openedNumber = null;
257
+ if (target.kind === 'pr_branch') {
258
+ branch = info.headRef;
259
+ if (body.strategy === 'rebase') {
260
+ // ⚠ THE VALUE FORM OF THE LEASE. A bare `--force-with-lease` leases against the
261
+ // remote-tracking ref, which our own fetch just updated — it would pass every time.
262
+ await pushForceWithLease({
263
+ ...push,
264
+ remoteBranch: branch,
265
+ leaseSha: body.expectedHeadSha,
266
+ });
267
+ }
268
+ else {
269
+ await pushRef({ ...push, remoteBranch: branch });
270
+ }
271
+ }
272
+ else {
273
+ branch = await pickFreeBranch(token, pushOwner, pushName, target.branch);
274
+ await pushRef({ ...push, remoteBranch: branch });
275
+ compareUrl = `https://github.com/${pushOwner}/${pushName}/compare/${encodeURIComponent(model.baseRef)}...${encodeURIComponent(branch)}`;
276
+ if (target.openPr) {
277
+ // ⚠ ONCE GITHUB 201s, NOTHING BELOW MAY THROW. A retry double-posts, and the copy
278
+ // contract for an unconfirmed write is "it'll show up here shortly", never "it failed".
279
+ const opened = await createPullRequest(token, {
280
+ owner: pushOwner,
281
+ name: pushName,
282
+ head: branch,
283
+ base: model.baseRef,
284
+ title: `Resolve conflicts with ${model.baseRef} for #${model.number}`,
285
+ body: `Conflict resolution for #${model.number}.`,
286
+ }).catch(() => null);
287
+ if (opened) {
288
+ compareUrl = opened.url;
289
+ openedNumber = opened.number;
290
+ }
291
+ }
292
+ }
293
+ // ⚠ CONFIRMING VERIFIES THE HEAD, and says what the push implies about the conflict.
294
+ // A bare resync here reported `visible: true` on ANY successful persist — including a read
295
+ // taken before GitHub attached the push, or one carrying GitHub's still-stale CONFLICTING /
296
+ // DIRTY for the new head — and the Pending board then re-served the "Conflicts" card this
297
+ // resolution had just cleared. Now `visible` means the pushed commit IS the stored head, and
298
+ // anything unsettled (the head, a stale conflict verdict, an unknown merge state) goes to the
299
+ // background settle ladder. The inline wait is deadline-bounded, so the SIGTERM drain that
300
+ // waits on this job is never held by it.
301
+ //
302
+ // `notConflicting` ONLY for a FULL resolution pushed to the PR's OWN branch (the rebase path
303
+ // is full by construction). A partial resolution, or a push to a new branch, legitimately
304
+ // leaves the original PR conflicting — expecting otherwise would run the ladder for nothing.
305
+ args.onPhase('confirming');
306
+ const slog = asSyncLogger(args.log);
307
+ const { visible } = await settlePrAfterWrite({
308
+ prId: args.prId,
309
+ accountId: args.accountId,
310
+ log: slog,
311
+ expect: toPrBranch
312
+ ? { headSha: built.commitSha, ...(plan.full ? { notConflicting: true } : {}) }
313
+ : {},
314
+ });
315
+ // A NEW branch that opened a PR: sync THAT PR too, so it exists locally before the reader is
316
+ // told about it. Its number resolves only inside the original PR's (account, repo).
317
+ if (target.kind === 'new_branch' && openedNumber != null) {
318
+ await syncNewPrBesideAfterWrite({
319
+ accountId: args.accountId,
320
+ prId: args.prId,
321
+ number: openedNumber,
322
+ log: slog,
323
+ });
324
+ }
325
+ return {
326
+ strategy: body.strategy,
327
+ branch,
328
+ pushedToPrBranch: toPrBranch,
329
+ commitSha: built.commitSha,
330
+ resolvedPaths: plan.resolved.map((f) => f.path),
331
+ skipped: plan.skipped,
332
+ stillConflicting: !plan.full,
333
+ baseAdvanced,
334
+ baseShaUsed: fresh.baseSha,
335
+ autoMergeDisarmed,
336
+ compareUrl,
337
+ visible,
338
+ };
339
+ }
340
+ finally {
341
+ // Every step independently swallowed: a teardown failure must never be what the reader
342
+ // sees instead of the result they got.
343
+ if (tmpDir) {
344
+ try {
345
+ rmSync(tmpDir, { recursive: true, force: true });
346
+ }
347
+ catch {
348
+ /* best effort */
349
+ }
350
+ }
351
+ if (cloneDir) {
352
+ for (const ref of [`${ns}/head`, `${ns}/base`]) {
353
+ await gitTry(['update-ref', '-d', ref], cloneDir).catch(() => { });
354
+ }
355
+ }
356
+ setImmediate(() => {
357
+ try {
358
+ cleanupCloneCache();
359
+ }
360
+ catch {
361
+ /* advisory */
362
+ }
363
+ });
364
+ }
365
+ }
366
+ /**
367
+ * Turn the request's per-region decisions into per-file bytes, or refuse.
368
+ *
369
+ * THE MODEL IS THE ALLOW-LIST. Every index, every region id and every decision is checked
370
+ * against it, and a `suggestionId` / `editId` is checked against the session's own store — so
371
+ * the only text that can reach a blob is text the server holds: either produced here, or typed
372
+ * by the reader and validated on the edit route before its id existed.
373
+ */
374
+ function planResolution(model, files, suggestions, edits) {
375
+ const byIndex = new Map(model.files.map((f) => [f.index, f]));
376
+ const resolved = [];
377
+ const seenFiles = new Set();
378
+ for (const req of files) {
379
+ const file = byIndex.get(req.index);
380
+ if (!file) {
381
+ throw landError('UnknownFileIndex', `This merge has no file ${req.index}.`);
382
+ }
383
+ if (seenFiles.has(req.index)) {
384
+ throw landError('UnknownFileIndex', `File ${file.path} was sent twice.`);
385
+ }
386
+ seenFiles.add(req.index);
387
+ // An unsupported file has no blob triple to resolve, and a resolvable one with no stage-2
388
+ // mode has nothing to write the result back AS. Either way it is not on offer, and
389
+ // pretending otherwise would put an unreviewed blob in the tree.
390
+ if (file.unsupported !== null || file.stage2Mode === null) {
391
+ throw landError('UnknownFileIndex', `${file.path} cannot be resolved here${file.unsupportedLabel ? ` (${file.unsupportedLabel.toLowerCase()})` : ''}.`);
392
+ }
393
+ const decisions = new Map();
394
+ const regionsById = new Map(file.regions.map((r) => [r.id, r]));
395
+ for (const d of req.decisions) {
396
+ const region = regionsById.get(d.id);
397
+ if (!region) {
398
+ throw landError('IncompleteDecisions', `${file.path} has no region ${d.id}.`);
399
+ }
400
+ if (decisions.has(d.id)) {
401
+ throw landError('IncompleteDecisions', `${file.path} carries two decisions for the same region.`);
402
+ }
403
+ decisions.set(d.id, resolveDecision(file, region, d, suggestions, edits));
404
+ }
405
+ // Rule 2 of the fold, enforced here so the refusal names the file: EXHAUSTIVE over every
406
+ // non-`unchanged` region. A missing one is a line of code nobody chose.
407
+ for (const region of file.regions) {
408
+ if (region.kind === 'unchanged')
409
+ continue;
410
+ if (!decisions.has(region.id)) {
411
+ throw landError('IncompleteDecisions', `${file.path} still has an undecided change.`);
412
+ }
413
+ }
414
+ const folded = foldFile({ regions: file.regions, terminators: file.terminators }, decisions);
415
+ if (!folded.ok) {
416
+ throw landError('IncompleteDecisions', `${file.path} could not be assembled (${folded.reason}).`);
417
+ }
418
+ resolved.push({
419
+ index: file.index,
420
+ path: file.path,
421
+ mode: file.stage2Mode,
422
+ // Every side decoded STRICTLY as UTF-8 at model build, so this re-encode round-trips
423
+ // byte for byte. That is the whole reason `not_text` is a refusal rather than a
424
+ // lossy decode.
425
+ content: Buffer.from(foldToText(folded), 'utf8'),
426
+ });
427
+ }
428
+ const unresolved = model.files.filter((f) => !seenFiles.has(f.index));
429
+ const skipped = [];
430
+ for (const f of unresolved) {
431
+ if (f.unsupported === null)
432
+ continue;
433
+ skipped.push({
434
+ index: f.index,
435
+ path: f.path,
436
+ reason: f.unsupported,
437
+ label: f.unsupportedLabel ?? '',
438
+ });
439
+ }
440
+ return { resolved, skipped, full: unresolved.length === 0, unresolved };
441
+ }
442
+ /** One region's decision, with the three payload-bearing members resolved from the places their
443
+ * lines actually live — never recomputed, so the fold that lands is the fold that was
444
+ * reviewed. */
445
+ function resolveDecision(file, region, sent, suggestions, edits) {
446
+ const decision = sent.decision;
447
+ if (decision === 'suggestion') {
448
+ if (!sent.suggestionId) {
449
+ throw landError('UnknownSuggestion', `${file.path} accepted a suggestion with no id.`);
450
+ }
451
+ const stored = suggestions.get(sent.suggestionId);
452
+ // The id must address THIS region: a suggestion is a handle on one hunk's lines, and a
453
+ // handle that travels to another hunk is text nobody read in the place it lands.
454
+ if (!stored || stored.fileIndex !== file.index || stored.regionId !== region.id) {
455
+ throw landError('UnknownSuggestion', 'That suggestion is no longer available.');
456
+ }
457
+ return {
458
+ decision: 'suggestion',
459
+ lines: stored.lines,
460
+ endsWithNewline: stored.endsWithNewline,
461
+ };
462
+ }
463
+ if (decision === 'edited') {
464
+ if (!sent.editId) {
465
+ throw landError('UnknownEdit', `${file.path} carries an edit with no id.`);
466
+ }
467
+ const stored = edits.get(sent.editId);
468
+ // ⚠ THE SAME GUARD, FOR THE SAME REASON, AND IT IS NOT REDUNDANT WITH THE FINGERPRINT CHECK
469
+ // THE EDIT ROUTE ALREADY MADE. That one proved the text was written against THIS region's
470
+ // bytes; this one proves the handle has not since been moved onto a different region by the
471
+ // commit body. Both are "text nobody read, in the place it lands" — one across time, one
472
+ // across regions.
473
+ if (!stored || stored.fileIndex !== file.index || stored.regionId !== region.id) {
474
+ throw landError('UnknownEdit', 'That edit is no longer available.');
475
+ }
476
+ return {
477
+ decision: 'edited',
478
+ lines: stored.lines,
479
+ endsWithNewline: stored.endsWithNewline,
480
+ };
481
+ }
482
+ if (!allowedDecisions(region).includes(decision)) {
483
+ throw landError('IncompleteDecisions', `${file.path} cannot take '${decision}' on this change.`);
484
+ }
485
+ if (decision === 'disjoint_merge') {
486
+ // Non-null iff the wand called it `disjoint_words`, which `allowedDecisions` already
487
+ // gates on — this is the belt to that braces, because a null here would fold to nothing.
488
+ if (region.mergedLines === null) {
489
+ throw landError('IncompleteDecisions', `${file.path} has no word-level merge for this change.`);
490
+ }
491
+ return {
492
+ decision: 'disjoint_merge',
493
+ lines: region.mergedLines,
494
+ // Fold rule 4: the word merge is built on the ours side, so it inherits its terminator.
495
+ endsWithNewline: file.terminators.ours,
496
+ };
497
+ }
498
+ return { decision };
499
+ }
500
+ /* ═════════════════════════════════ staleness ═════════════════════════════════ */
501
+ /**
502
+ * Did the material the decisions were made against change?
503
+ *
504
+ * ⚠ THIS IS NOT `conflictModelHash`, and it must not become it. The hash pins the two
505
+ * COMMITS and the merged tree, all of which move when the base branch gains any commit at
506
+ * all — so it answers "is this the same merge", while the question here is "are these the
507
+ * same conflicts". A base advance that leaves every contested region byte-identical is
508
+ * reported as `baseAdvanced`, not refused.
509
+ */
510
+ function conflictContentMatches(a, b) {
511
+ if (a.files.length !== b.files.length)
512
+ return false;
513
+ if (a.renameDetection !== b.renameDetection || a.truncated !== b.truncated)
514
+ return false;
515
+ for (let i = 0; i < a.files.length; i++) {
516
+ const x = a.files[i];
517
+ const y = b.files[i];
518
+ if (!x || !y)
519
+ return false;
520
+ if (x.index !== y.index || x.path !== y.path)
521
+ return false;
522
+ if (x.unsupported !== y.unsupported || x.stage2Mode !== y.stage2Mode)
523
+ return false;
524
+ if (x.terminators.base !== y.terminators.base ||
525
+ x.terminators.ours !== y.terminators.ours ||
526
+ x.terminators.theirs !== y.terminators.theirs) {
527
+ return false;
528
+ }
529
+ if (x.regions.length !== y.regions.length)
530
+ return false;
531
+ for (let r = 0; r < x.regions.length; r++) {
532
+ const p = x.regions[r];
533
+ const q = y.regions[r];
534
+ if (!p || !q)
535
+ return false;
536
+ if (p.id !== q.id || p.kind !== q.kind || p.fingerprint !== q.fingerprint)
537
+ return false;
538
+ if (p.wand?.decision !== q.wand?.decision || p.wand?.reason !== q.wand?.reason)
539
+ return false;
540
+ if ((p.mergedLines === null) !== (q.mergedLines === null))
541
+ return false;
542
+ if (p.mergedLines && q.mergedLines) {
543
+ if (p.mergedLines.length !== q.mergedLines.length)
544
+ return false;
545
+ for (let l = 0; l < p.mergedLines.length; l++) {
546
+ if (p.mergedLines[l] !== q.mergedLines[l])
547
+ return false;
548
+ }
549
+ }
550
+ }
551
+ }
552
+ return true;
553
+ }
554
+ /* ═════════════════════════════════ branch names ═════════════════════════════════ */
555
+ /** Reserved BY NAME — the repo default branch and the PR base ref. Case-insensitive, because
556
+ * on a case-folding filesystem `Main` and `main` are the same branch and the refusal has to
557
+ * hold for the spelling the user typed. */
558
+ async function assertNewBranchName(model, branch) {
559
+ const wanted = branch.trim();
560
+ if (!wanted)
561
+ throw landError('InvalidBranch', 'Give the new branch a name.');
562
+ const lowered = wanted.toLowerCase();
563
+ if (model.reservedBranchNames.some((r) => r.toLowerCase() === lowered)) {
564
+ throw landError('ReservedBranch', `${wanted} is this repository's own branch.`);
565
+ }
566
+ if (wanted.startsWith('-') ||
567
+ wanted.startsWith('/') ||
568
+ wanted.endsWith('/') ||
569
+ wanted.includes('//')) {
570
+ throw landError('InvalidBranch', `git does not accept ${wanted} as a branch name.`);
571
+ }
572
+ // git's own format check is the authority; a regex here would refuse real branch names.
573
+ const res = await gitTry(['check-ref-format', `refs/heads/${wanted}`], tmpdir());
574
+ if (res.code !== 0) {
575
+ throw landError('InvalidBranch', `git does not accept ${wanted} as a branch name.`);
576
+ }
577
+ }
578
+ /**
579
+ * The first free name at or after `wanted`, `-2` … `-9`, then a refusal.
580
+ *
581
+ * A live ref under this name belongs to someone — an earlier attempt, a person — so it is
582
+ * never overwritten. The ladder is what makes a second attempt after a failed push work
583
+ * without the reader having to invent a name; the result names the branch that was actually
584
+ * created, so nothing about it is silent.
585
+ */
586
+ async function pickFreeBranch(token, owner, name, wanted) {
587
+ for (let n = 1; n <= 9; n++) {
588
+ const candidate = n === 1 ? wanted : `${wanted}-${n}`;
589
+ const existing = await ghRestGetText(token, `/repos/${owner}/${name}/git/ref/heads/${candidate}`);
590
+ if (!existing.ok)
591
+ return candidate;
592
+ }
593
+ throw landError('BranchExists', `${wanted} and its numbered variants all exist already.`);
594
+ }
595
+ /* ═════════════════════════════════ commit messages ═════════════════════════════════ */
596
+ const MESSAGE_MAX_BYTES = 8 * 1024;
597
+ const PATHS_LISTED = 20;
598
+ function capMessage(message) {
599
+ const buf = Buffer.from(message, 'utf8');
600
+ if (buf.length <= MESSAGE_MAX_BYTES)
601
+ return message;
602
+ // Slice on a character boundary, not a byte one: a half-encoded sequence in a commit
603
+ // message is a mojibake bug that outlives the session by a long way.
604
+ let cut = message;
605
+ while (Buffer.byteLength(cut, 'utf8') > MESSAGE_MAX_BYTES - 1) {
606
+ cut = cut.slice(0, Math.floor(cut.length * 0.9) || 0);
607
+ }
608
+ return `${cut}\n`;
609
+ }
610
+ function pathList(paths) {
611
+ const shown = paths.slice(0, PATHS_LISTED).map((p) => ` ${p}`);
612
+ if (paths.length > PATHS_LISTED)
613
+ shown.push(` …and ${paths.length - PATHS_LISTED} more`);
614
+ return shown;
615
+ }
616
+ function fullMergeMessage(model, plan) {
617
+ const paths = plan.resolved.map((f) => f.path);
618
+ return [
619
+ `Merge branch '${model.baseRef}' into ${model.headRef || 'this branch'}`,
620
+ '',
621
+ `Resolved conflicts in ${paths.length} ${paths.length === 1 ? 'file' : 'files'}:`,
622
+ ...pathList(paths),
623
+ '',
624
+ ].join('\n');
625
+ }
626
+ /** ⚠ THE WORD "MERGE" MUST NOT APPEAR. A partial resolution is a plain one-parent commit on
627
+ * the PR branch, and calling it a merge in the log is a claim that the base branch landed. */
628
+ function partialMessage(model, plan) {
629
+ const paths = plan.resolved.map((f) => f.path);
630
+ const lines = [
631
+ `Resolve conflicts in ${paths.length} of ${model.files.length} files`,
632
+ '',
633
+ ...pathList(paths),
634
+ ];
635
+ if (plan.unresolved.length > 0) {
636
+ lines.push('');
637
+ for (const f of plan.unresolved.slice(0, PATHS_LISTED)) {
638
+ lines.push(`Still conflicting: ${f.path}${f.unsupportedLabel ? ` (${f.unsupportedLabel})` : ''}`);
639
+ }
640
+ if (plan.unresolved.length > PATHS_LISTED) {
641
+ lines.push(`…and ${plan.unresolved.length - PATHS_LISTED} more`);
642
+ }
643
+ }
644
+ lines.push('');
645
+ return lines.join('\n');
646
+ }
647
+ /* ═════════════════════════════════ small helpers ═════════════════════════════════ */
648
+ function assertNotCancelled(signal) {
649
+ if (signal.aborted)
650
+ throw landError('Cancelled', 'Cancelled.');
651
+ }
652
+ /** The original commit's author and message, for the rebase path. The committer is the
653
+ * account doing the resolving — the author wrote the code, we only moved it. */
654
+ async function readCommitIdent(cloneDir, sha) {
655
+ const out = await git(['show', '-s', '--format=%an%x00%ae%x00%aI%x00%B', sha], cloneDir);
656
+ const parts = out.stdout.toString('utf8').split('\0');
657
+ return {
658
+ name: parts[0] ?? 'pierre-review',
659
+ email: parts[1] ?? 'pierre-review@users.noreply.github.com',
660
+ date: parts[2] ?? new Date().toISOString(),
661
+ message: parts[3] ?? '',
662
+ };
663
+ }
664
+ /** The `coding/merge.ts` ident, which is module-private there. Same fallbacks on purpose:
665
+ * two spellings of "who committed this" would show up in the git log as two people. */
666
+ async function identFor(accountId) {
667
+ const account = await getAccountById(accountId);
668
+ return {
669
+ name: account?.displayName || account?.githubLogin || 'pierre-review',
670
+ email: account?.githubLogin
671
+ ? `${account.githubLogin}@users.noreply.github.com`
672
+ : 'pierre-review@users.noreply.github.com',
673
+ };
674
+ }
675
+ //# sourceMappingURL=land.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model-types.js.map