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,398 @@
1
+ import { hasConflictMarkers } from '../coding/merge.js';
2
+ /**
3
+ * THE TWO TEXT INGRESSES' VALIDATORS. The Pro per-hunk suggestion's, and the CORE manual
4
+ * edit's — in one file because they are the same job (nothing untrusted becomes a blob until
5
+ * something here has vouched for it) and because they must not drift into two vocabularies for
6
+ * one fact. They are DIFFERENT functions, though, and the bottom of this file says why.
7
+ *
8
+ * THE PRO PER-HUNK SUGGESTION'S VALIDATORS — and they live in CORE, deliberately.
9
+ *
10
+ * ⚠ WHY THEY ARE NOT IN THE PLUGIN. `packages/pro/test/` does not run in CI and is not
11
+ * typechecked (`pnpm test` is recursive vitest and the plugin's tsconfig includes only `src`).
12
+ * These functions are the only thing standing between attacker-authored conflict text and a
13
+ * blob in somebody's repository, and they have to agree byte-for-byte with the model the host
14
+ * built. In core they run on every `pnpm test`, against the same `ConflictModelRegion` the land
15
+ * path folds.
16
+ *
17
+ * ⚠ EVERY CHECK REFUSES. There is no "accept with a warning" path and there must not be one: a
18
+ * suggestion that fails any check never reaches the centre pane, so the reader never sees text
19
+ * we could not vouch for sitting where their code will be. `cannot_reconcile` — the model saying
20
+ * the two sides genuinely contest — is a DESIGNED outcome on the same footing, not a failure.
21
+ *
22
+ * ⚠ THE RAW MODEL TEXT NEVER LEAVES THIS FILE'S CALLER. On a refusal the route sends the code
23
+ * and OUR sentence; echoing the answer back would hand an injection payload a rendering surface
24
+ * in the one screen whose whole job is to show text the reader is about to commit.
25
+ */
26
+ /* ═════════════════════════════════ the fences ═════════════════════════════════ */
27
+ /**
28
+ * The six fenced regions of the prompt. Five are input the model READS; `RESOLVED` is the one it
29
+ * WRITES, and the only one this file extracts.
30
+ *
31
+ * ⚠ ONE PRODUCER, IN CORE, REACHED BY THE PLUGIN THROUGH THE SEAM (`ConflictSeam.fences`). The
32
+ * prompt's fences and the validator's extraction are the same strings by construction — a second
33
+ * spelling in the plugin would make every answer refuse `unparseable` against a host whose
34
+ * markers had moved, which is loud but pointless. Nothing here is a secret: the NONCE is what
35
+ * makes a fence unforgeable, not the word.
36
+ */
37
+ export const CONFLICT_FENCE_REGIONS = [
38
+ 'BASE',
39
+ 'OURS',
40
+ 'THEIRS',
41
+ 'CONTEXT BEFORE',
42
+ 'CONTEXT AFTER',
43
+ 'RESOLVED',
44
+ ];
45
+ /**
46
+ * The marker pair for every fenced region, at one per-request nonce.
47
+ *
48
+ * ⚠ THE NONCE IS THE WHOLE MECHANISM. A conflict hunk is attacker-authored on any pull request,
49
+ * so a line inside one reading `---END OURS---` would close a fixed fence and let the rest of
50
+ * that side be read as instructions. With a fresh 16-hex nonce in every marker, such a line is
51
+ * data: it closes nothing, and the caller re-rolls the nonce on the ~2⁻⁶⁴ chance the untrusted
52
+ * text contains it (see `nonceCollides`).
53
+ */
54
+ export function conflictFences(nonce) {
55
+ const out = {};
56
+ for (const region of CONFLICT_FENCE_REGIONS) {
57
+ out[region] = {
58
+ begin: `---BEGIN ${region} ${nonce}---`,
59
+ end: `---END ${region} ${nonce}---`,
60
+ };
61
+ }
62
+ return out;
63
+ }
64
+ /** True when the nonce appears anywhere in the untrusted material, so the caller must re-roll it
65
+ * before building a prompt. Checked against everything that gets fenced, not just the sides. */
66
+ export function nonceCollides(hunk, nonce) {
67
+ const needle = nonce.toLowerCase();
68
+ for (const side of [
69
+ hunk.base,
70
+ hunk.ours,
71
+ hunk.theirs,
72
+ hunk.contextBefore,
73
+ hunk.contextAfter,
74
+ [hunk.path, hunk.headRef, hunk.baseRef],
75
+ ]) {
76
+ for (const line of side)
77
+ if (line.toLowerCase().includes(needle))
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ /** Context lines either side of the hunk, and the per-line clip. Both are prompt budget, and
83
+ * clipping CONTEXT is safe in a way clipping the region is not: context is never spliced back. */
84
+ export const HUNK_CONTEXT_LINES = 8;
85
+ export const HUNK_CONTEXT_LINE_CHARS = 200;
86
+ /** The characters `config.conflictSuggestMaxChars` is measured against: the three SIDES only.
87
+ * Context is bounded by construction, so it cannot be the thing that pushes a small hunk over. */
88
+ export function hunkRegionChars(base, ours, theirs) {
89
+ let n = 0;
90
+ for (const side of [base, ours, theirs])
91
+ for (const line of side)
92
+ n += line.length + 1;
93
+ return n;
94
+ }
95
+ /* ═════════════════════════════════ validation ═════════════════════════════════ */
96
+ /** A lone half of a surrogate pair — legal in a JS string, not encodable as UTF-8. */
97
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
98
+ const ALPHANUMERIC = /[\p{L}\p{N}]/u;
99
+ /** Check 7's filter: a line substantial enough that losing it is a real loss. Punctuation-only
100
+ * lines are excluded HERE and deliberately included in check 8 — a dropped `}` is exactly the
101
+ * failure worth refusing on when the line was one side's own contribution. */
102
+ function significant(line) {
103
+ const t = line.trim();
104
+ return t.length >= 3 && ALPHANUMERIC.test(t);
105
+ }
106
+ function countBy(lines, keep) {
107
+ const m = new Map();
108
+ for (const line of lines) {
109
+ if (!keep(line))
110
+ continue;
111
+ const k = line.trim();
112
+ m.set(k, (m.get(k) ?? 0) + 1);
113
+ }
114
+ return m;
115
+ }
116
+ /** Trailing BLANK lines only — never leading ones, and never whitespace inside a line. A model
117
+ * that indents its answer is right to; a model that pads it with newlines is just verbose. */
118
+ function trimTrailingBlank(lines) {
119
+ let end = lines.length;
120
+ while (end > 0 && (lines[end - 1] ?? '').trim() === '')
121
+ end -= 1;
122
+ return lines.slice(0, end);
123
+ }
124
+ /** The last `n` non-blank lines of `lines`, trimmed. */
125
+ function tailSignificant(lines, n) {
126
+ const out = [];
127
+ for (let i = lines.length - 1; i >= 0 && out.length < n; i -= 1) {
128
+ const t = (lines[i] ?? '').trim();
129
+ if (t !== '')
130
+ out.unshift(t);
131
+ }
132
+ return out;
133
+ }
134
+ /** The first `n` non-blank lines of `lines`, trimmed. */
135
+ function headSignificant(lines, n) {
136
+ const out = [];
137
+ for (let i = 0; i < lines.length && out.length < n; i += 1) {
138
+ const t = (lines[i] ?? '').trim();
139
+ if (t !== '')
140
+ out.push(t);
141
+ }
142
+ return out;
143
+ }
144
+ const sameLines = (a, b) => a.length > 0 && a.length === b.length && a.every((l, i) => l === b[i]);
145
+ /**
146
+ * THE VALIDATOR. Pure — no I/O, no git, no clock. First failure wins, and every failure refuses.
147
+ *
148
+ * The order is load-bearing and pinned by a test: an answer that both kept a conflict marker and
149
+ * ran long reports `markers`, because the marker is the fact worth telling the reader about.
150
+ *
151
+ * 1. Shape — one `RESOLVED` fence, each marker alone on its line, in order.
152
+ * 2. Designed refusal — `CANNOT RECONCILE` alone in the block.
153
+ * 3. Text-ness — no NUL, no lone surrogate, no BOM.
154
+ * 4. Leakage — no nonce, no `---BEGIN `/`---END ` at a line start.
155
+ * 5. Markers — no surviving conflict marker.
156
+ * 6. Context not repeated — it must not re-emit the lines it sits between.
157
+ * 7. Common lines kept — what BOTH sides already had must survive.
158
+ * 8. Side lines kept — what EITHER side added must survive.
159
+ * 9. Length — bounded in lines AND bytes.
160
+ * 10. Not empty — when either side had content.
161
+ */
162
+ export function validateConflictSuggestion(hunk, nonce, rawModelText) {
163
+ // Normalise once: CRLF → LF so a Windows-flavoured answer behaves as its LF twin, and one
164
+ // leading BOM stripped (a transport artefact). Any OTHER BOM is a check-3 refusal.
165
+ const text = rawModelText.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
166
+ const fence = conflictFences(nonce).RESOLVED;
167
+ const all = text.split('\n');
168
+ // ---- 1. Shape --------------------------------------------------------------------------
169
+ const begins = [];
170
+ const ends = [];
171
+ all.forEach((line, i) => {
172
+ const t = line.trim();
173
+ if (t === fence.begin)
174
+ begins.push(i);
175
+ if (t === fence.end)
176
+ ends.push(i);
177
+ });
178
+ const begin = begins[0];
179
+ const end = ends[0];
180
+ if (begins.length !== 1 || ends.length !== 1 || begin === undefined || end === undefined) {
181
+ return { ok: false, refusal: 'unparseable' };
182
+ }
183
+ if (end <= begin)
184
+ return { ok: false, refusal: 'unparseable' };
185
+ const region = trimTrailingBlank(all.slice(begin + 1, end));
186
+ const body = region.join('\n');
187
+ // ---- 2. The designed refusal -----------------------------------------------------------
188
+ // Its own outcome, ABOVE every content check: a model that correctly says the two sides
189
+ // genuinely contest must not then be told its answer was unparseable or empty.
190
+ if (body.trim() === 'CANNOT RECONCILE')
191
+ return { ok: false, refusal: 'cannot_reconcile' };
192
+ // ---- 3. Text-ness ----------------------------------------------------------------------
193
+ if (body.includes('\0') || body.includes('\uFEFF') || LONE_SURROGATE.test(body)) {
194
+ return { ok: false, refusal: 'not_text' };
195
+ }
196
+ // ---- 4. Leakage ------------------------------------------------------------------------
197
+ // The nonce inside the block means the model echoed our scaffolding; a `---BEGIN `/`---END `
198
+ // line start means it invented a fence of its own. Both are the same fact — the answer is not
199
+ // the shape we asked for — so they share `unparseable` and share one sentence on screen.
200
+ if (body.toLowerCase().includes(nonce.toLowerCase())) {
201
+ return { ok: false, refusal: 'unparseable' };
202
+ }
203
+ if (region.some((l) => l.startsWith('---BEGIN ') || l.startsWith('---END '))) {
204
+ return { ok: false, refusal: 'unparseable' };
205
+ }
206
+ // ---- 5. Conflict markers ---------------------------------------------------------------
207
+ // ⚠ IMPORTED, NOT RE-SPELT. `coding/merge.ts`'s matcher deliberately EXCLUDES a bare
208
+ // `=======` — real files contain rows of equals signs — and relies on the unambiguous
209
+ // open/close markers. A local copy here would drift from the one the merge path trusts.
210
+ if (hasConflictMarkers(body))
211
+ return { ok: false, refusal: 'markers' };
212
+ // ---- 6. The context is not repeated ----------------------------------------------------
213
+ // ⚠ NARROW ON PURPOSE, AND NOT A WHOLE-FILE COMPARISON. The context bytes are byte-identical
214
+ // BY CONSTRUCTION — the model is never given the chance to write them, because only the
215
+ // region between the two fences is ever read back. This check exists solely to catch an
216
+ // answer that smuggles them back INSIDE the region, which would duplicate the surrounding
217
+ // lines when the fold splices it. Do not "harden" it into a diff of the whole file.
218
+ const edge = (n) => Math.max(1, Math.min(3, n));
219
+ if (hunk.contextBefore.length > 0 && region.length > 0) {
220
+ const n = edge(Math.min(hunk.contextBefore.length, region.length));
221
+ if (sameLines(headSignificant(region, n), tailSignificant(hunk.contextBefore, n))) {
222
+ return { ok: false, refusal: 'context_duplicated' };
223
+ }
224
+ }
225
+ if (hunk.contextAfter.length > 0 && region.length > 0) {
226
+ const n = edge(Math.min(hunk.contextAfter.length, region.length));
227
+ if (sameLines(tailSignificant(region, n), headSignificant(hunk.contextAfter, n))) {
228
+ return { ok: false, refusal: 'context_duplicated' };
229
+ }
230
+ }
231
+ // ---- 7. Lines BOTH versions kept ------------------------------------------------------
232
+ // The multiset intersection of the two sides' significant lines: text neither side touched
233
+ // and both therefore still want. Compared TRIMMED, so a whitespace-only reindent on an agreed
234
+ // line is not a loss.
235
+ const outCounts = countBy(region, () => true);
236
+ const oursSig = countBy(hunk.ours, significant);
237
+ const theirsSig = countBy(hunk.theirs, significant);
238
+ let keptCommonLines = 0;
239
+ for (const [line, n] of oursSig) {
240
+ const need = Math.min(n, theirsSig.get(line) ?? 0);
241
+ if (need === 0)
242
+ continue;
243
+ if ((outCounts.get(line) ?? 0) < need)
244
+ return { ok: false, refusal: 'dropped_common_lines' };
245
+ keptCommonLines += need;
246
+ }
247
+ // ---- 8. Lines EITHER side added -------------------------------------------------------
248
+ // ⚠ THE CHECK THAT MAKES 7 WORTH HAVING. Check 7 only protects text both versions already
249
+ // agreed on, so a model that emits THEIRS alone — deleting the pull request's entire
250
+ // contribution — sails through it. Here a line one side has and the ancestor does not must
251
+ // survive, unless the OTHER side deliberately deleted it (which is what "also in base" means
252
+ // once its own side's excess is accounted for).
253
+ //
254
+ // ⚠ NO ≥3-CHAR FILTER HERE, unlike check 7. A dropped `}` from one side's addition is exactly
255
+ // the failure worth refusing on.
256
+ const nonBlank = (l) => l.trim() !== '';
257
+ const baseAll = countBy(hunk.base, nonBlank);
258
+ const oursAll = countBy(hunk.ours, nonBlank);
259
+ const theirsAll = countBy(hunk.theirs, nonBlank);
260
+ const required = new Map();
261
+ for (const counts of [oursAll, theirsAll]) {
262
+ for (const [line, n] of counts) {
263
+ // What THIS side has beyond the ancestor. Two sides adding the same line require it
264
+ // ONCE, not twice — hence `max`, not a sum.
265
+ const excess = n - (baseAll.get(line) ?? 0);
266
+ if (excess <= 0)
267
+ continue;
268
+ required.set(line, Math.max(required.get(line) ?? 0, excess));
269
+ }
270
+ }
271
+ for (const [line, need] of required) {
272
+ if ((outCounts.get(line) ?? 0) < need)
273
+ return { ok: false, refusal: 'dropped_side_lines' };
274
+ }
275
+ // ---- 9. Length -------------------------------------------------------------------------
276
+ const maxSideLines = Math.max(hunk.base.length, hunk.ours.length, hunk.theirs.length);
277
+ const sideBytes = Buffer.byteLength(hunk.ours.join('\n'), 'utf8') +
278
+ Buffer.byteLength(hunk.theirs.join('\n'), 'utf8');
279
+ if (region.length > maxSideLines * 3 + 10)
280
+ return { ok: false, refusal: 'too_long' };
281
+ if (Buffer.byteLength(body, 'utf8') > sideBytes * 2 + 512) {
282
+ return { ok: false, refusal: 'too_long' };
283
+ }
284
+ // ---- 10. Not empty ---------------------------------------------------------------------
285
+ // Reached only when checks 7 and 8 found nothing to require, which is the case where both
286
+ // sides are pure deletions of ancestor text — and even then an answer of nothing is a claim
287
+ // the reader should make themselves, not one to accept silently from a model.
288
+ if (region.length === 0 && (hunk.ours.length > 0 || hunk.theirs.length > 0)) {
289
+ return { ok: false, refusal: 'empty' };
290
+ }
291
+ return { ok: true, lines: region, keptCommonLines };
292
+ }
293
+ export const NO_EDIT_SHAPE = { crlf: false, leadingBom: false };
294
+ /**
295
+ * What an edit to this region has to inherit from it.
296
+ *
297
+ * ⚠ A TEXTAREA DESTROYS BOTH FACTS AND SAYS NOTHING. The browser's API value normalises every
298
+ * CRLF to a bare LF before React ever sees a keystroke (HTML's "normalize newlines"), so one
299
+ * character typed into a Windows-authored file used to rewrite the WHOLE hunk's line endings —
300
+ * a diff of every line, and a genuinely wrong file in any repo carrying `* text eol=crlf`. A BOM
301
+ * survives the round trip but is zero-width, so `validateConflictEdit`'s blanket U+FEFF refusal
302
+ * made the first region of every BOM file permanently uneditable, with a sentence
303
+ * ("Retype the odd one out") naming a character nobody can see.
304
+ *
305
+ * @param sides the region's own `base`, `ours` and `theirs` line texts, terminators attached.
306
+ * @param isFirstRegion is this the file's FIRST region? Only there can a BOM be legitimate.
307
+ */
308
+ export function editShapeFor(sides, isFirstRegion) {
309
+ const lines = sides.flat();
310
+ return {
311
+ // ⚠ UNANIMOUS, NOT A MAJORITY. A CRLF file's final line carries no `\r` when the file has no
312
+ // trailing newline, so that region reads as mixed and is left alone — which is correct: there
313
+ // is nothing there to re-impose without inventing a byte.
314
+ crlf: lines.length > 0 && lines.every((l) => l.endsWith('\r')),
315
+ leadingBom: isFirstRegion && sides.some((side) => side[0]?.startsWith('') === true),
316
+ };
317
+ }
318
+ /**
319
+ * THE MANUAL EDIT'S VALIDATOR — three checks, and the list of what it deliberately does NOT
320
+ * check is the more important half.
321
+ *
322
+ * 1. Text-ness — no NUL, no lone surrogate, no BOM.
323
+ * 2. Markers — no surviving conflict marker.
324
+ * 3. Length — bounded by `config.conflictSuggestMaxChars`, ONE budget for both ingresses.
325
+ *
326
+ * ⚠ 1 IS NOT A FORMALITY HERE. `land.ts` does `Buffer.from(foldToText(...), 'utf8')`, and its
327
+ * byte-for-byte round-trip claim rests on every side having decoded STRICTLY as UTF-8 at model
328
+ * build — which is why `not_text` is a file-level REFUSAL there rather than a lossy decode.
329
+ * Typed text does not inherit that provenance: a lone surrogate reaches `Buffer.from` and is
330
+ * silently substituted with U+FFFD, so the bytes committed would not be the bytes anybody saw.
331
+ *
332
+ * ⚠ THE U+FEFF HALF OF 1 IS NOT BLANKET, AND THE EXCEPTION IS THE FILE'S OWN BOM. `model.ts`
333
+ * decodes with `ignoreBOM: true` deliberately, so a BOM file's first line really does begin
334
+ * U+FEFF and the textarea really is seeded with it. Refusing that made the first region of every
335
+ * Visual-Studio-authored file permanently uneditable — and the only escape, retyping the hunk
336
+ * from scratch, silently stripped the BOM that the decoder exists to preserve. So when
337
+ * `shape.leadingBom` says this region owns the file's BOM, one LEADING U+FEFF is taken off before
338
+ * the scan and put back on the first stored line. Anywhere else it is still `not_text`, and
339
+ * `validateConflictSuggestion` — whose input is MODEL output, where a BOM is junk — is untouched.
340
+ *
341
+ * ⚠ AND THE LINE ENDING IS RE-IMPOSED, BECAUSE THE BROWSER ALREADY DESTROYED IT. See
342
+ * `editShapeFor`: a textarea hands back LF for every CRLF it was given, so without this one
343
+ * keystroke in a CRLF file rewrites the whole hunk's endings. Imposing costs nothing when the
344
+ * region is LF (`shape.crlf` false ⇒ this is a no-op) and is the only way the stored lines can
345
+ * agree with their own neighbours.
346
+ *
347
+ * ⚠ 2 IS NOT COVERED BY ANYTHING ELSE. The land path's full-resolution guard only proves each
348
+ * conflicted path was OVERWRITTEN (merge-tree's tree stores marker content at those paths); it
349
+ * never inspects the bytes. A typed `<<<<<<<` would commit with nothing refusing it.
350
+ *
351
+ * ⚠ AND CHECKS 6, 7 AND 8 OF THE SUGGESTION VALIDATOR ARE DELIBERATELY ABSENT — context not
352
+ * repeated, common lines kept, side lines kept. Those three exist because a MODEL silently drops
353
+ * lines it was not asked to drop, and the reader has no way to know it happened. A PERSON
354
+ * deleting a line is not a failure mode, it is the feature: the whole reason this route exists
355
+ * is the trailing comma, the duplicated import, the brace that belongs to neither side. Porting
356
+ * them over would refuse exactly the edits somebody opened the textarea to make.
357
+ *
358
+ * The order is text-ness → markers → length, matching the suggestion validator's: the marker is
359
+ * the fact worth telling somebody about, and "this is also too long" can wait. The scans cannot
360
+ * run away on a huge body because the route's ajv schema bounds `text` structurally first.
361
+ */
362
+ export function validateConflictEdit(text, maxChars, shape = NO_EDIT_SHAPE) {
363
+ // The file's own BOM, off the front before the scan and back on after it. Only ever ONE, only
364
+ // ever leading, and only when this region is where the file's BOM lives.
365
+ const bom = shape.leadingBom && text.startsWith('\uFEFF');
366
+ const body = bom ? text.slice(1) : text;
367
+ if (body.includes('\0') || body.includes('\uFEFF') || LONE_SURROGATE.test(body)) {
368
+ return { ok: false, refusal: 'not_text' };
369
+ }
370
+ if (hasConflictMarkers(body))
371
+ return { ok: false, refusal: 'markers' };
372
+ if (body.length > maxChars)
373
+ return { ok: false, refusal: 'too_long' };
374
+ const lines = splitEditLines(body).map((l) => shape.crlf && !l.endsWith('\r') ? `${l}\r` : l);
375
+ // \u26A0 RE-ATTACHED TO THE FIRST LINE, NOT TO THE TEXT. The reader may have deleted it \u2014 it is
376
+ // zero-width, so they cannot have done so deliberately \u2014 and the file's decoder is documented
377
+ // to keep it. An edit that leaves the region EMPTY has no first line, and in that case the BOM
378
+ // goes with the lines the reader deleted, which is the one place they really did choose.
379
+ if (shape.leadingBom && lines[0] != null && !lines[0].startsWith('\uFEFF')) {
380
+ lines[0] = `\uFEFF${lines[0]}`;
381
+ }
382
+ return { ok: true, lines };
383
+ }
384
+ /**
385
+ * The reader's text as fold lines.
386
+ *
387
+ * ⚠ AN EMPTY TEXTAREA IS ZERO LINES, NOT ONE BLANK ONE. `''.split('\n')` is `['']`, which folds
388
+ * to a region containing one empty line — not what somebody who selected everything and pressed
389
+ * delete asked for. Deleting a whole hunk has to be reachable; it is half of why this exists.
390
+ *
391
+ * ⚠ A TRAILING NEWLINE IS A REAL TRAILING BLANK LINE, and stays one. The suggestion validator
392
+ * trims trailing blanks because a verbose model pads its answer; a person who left a blank line
393
+ * at the end of the box can see it there and meant it.
394
+ */
395
+ function splitEditLines(text) {
396
+ return text === '' ? [] : text.split('\n');
397
+ }
398
+ //# sourceMappingURL=suggestion.js.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Tokenisers + the interner the diff runs on.
3
+ *
4
+ * Everything the resolver diffs — lines at file grain, words inside one contested region — is
5
+ * turned into an array of small integers first. The diff then compares numbers, which is what
6
+ * makes the O(ND) inner loop cheap enough to run over a whole file and again over every
7
+ * conflicted region.
8
+ *
9
+ * ⚠ `tokenizeWords(text).join('') === text` is a HARD invariant, not a nicety. The wand's
10
+ * word-level merge reassembles a region by concatenating token runs, so a tokeniser that
11
+ * dropped or normalised a byte would silently commit different text than the user read.
12
+ * `tokens.test.ts` pins it over code, CRLF, tabs, unicode identifiers and emoji.
13
+ */
14
+ /**
15
+ * Split into: runs of word characters, runs of whitespace, and one code point for anything
16
+ * else. Unicode-aware on both counts — `ünicode` is ONE token, and an astral code point (an
17
+ * emoji, a rare CJK ideograph) is one token rather than two lone surrogates.
18
+ */
19
+ const WORD_TOKEN_RE = /[\p{L}\p{N}_]+|\s+|[\s\S]/gu;
20
+ export function tokenizeWords(text) {
21
+ return text.match(WORD_TOKEN_RE) ?? [];
22
+ }
23
+ /** True when a token is entirely whitespace. The disjointness rule needs REAL evidence that
24
+ * two authors worked on different things, and a single space between two edits is not it. */
25
+ export function isWhitespaceToken(token) {
26
+ return token.length > 0 && token.trim().length === 0;
27
+ }
28
+ /**
29
+ * Map token strings to dense integer ids. One interner per diff pair, so ids are only ever
30
+ * compared within the arrays that produced them.
31
+ */
32
+ export class TokenInterner {
33
+ ids = new Map();
34
+ id(token) {
35
+ const existing = this.ids.get(token);
36
+ if (existing !== undefined)
37
+ return existing;
38
+ const next = this.ids.size;
39
+ this.ids.set(token, next);
40
+ return next;
41
+ }
42
+ /** How many distinct tokens have been seen. */
43
+ get size() {
44
+ return this.ids.size;
45
+ }
46
+ }
47
+ /** Intern a whole array in one pass. */
48
+ export function internAll(tokens, interner) {
49
+ const out = new Array(tokens.length);
50
+ for (let i = 0; i < tokens.length; i++) {
51
+ // noUncheckedIndexedAccess: the loop bound guarantees it, the compiler does not.
52
+ out[i] = interner.id(tokens[i]);
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Split a UTF-8 string into lines WITHOUT their terminators, plus whether the text ended with
58
+ * one. `\r` stays attached to its line, so a CRLF file round-trips byte for byte.
59
+ *
60
+ * An empty file is zero lines and no terminator — NOT one empty line, which would commit a
61
+ * newline into a file that never had one.
62
+ */
63
+ export function splitLines(text) {
64
+ if (text.length === 0)
65
+ return { lines: [], finalNewline: false };
66
+ const finalNewline = text.endsWith('\n');
67
+ const lines = text.split('\n');
68
+ if (finalNewline)
69
+ lines.pop();
70
+ return { lines, finalNewline };
71
+ }
72
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1,112 @@
1
+ // THE LOCAL DATA DIRECTORY — ~/.limn, and the one-time move from ~/.pierre-review.
2
+ //
3
+ // Everything Limn keeps on a user's machine lives under ONE directory: the installed CLI's SQLite
4
+ // database, the clone cache, `config.json` (the per-review budget) and the AI runtime the agentic
5
+ // features download on first use (`ai-runtime/`, see ai/runtime.ts). Before the npm package was
6
+ // renamed `pierre-review` → `limn-review` that directory was `~/.pierre-review`.
7
+ //
8
+ // ⚠ THE MOVE IS ONE `rename`, AND IT NEVER CLOBBERS. On the first local boot after the upgrade an
9
+ // existing `~/.pierre-review` is renamed to `~/.limn` — DB, WAL files, clones, config, everything,
10
+ // in one atomic step on the same filesystem (both live in the home directory). If `~/.limn`
11
+ // already exists nothing is moved and nothing is deleted: the old directory is simply left where
12
+ // it is. A copy-then-delete would be the dangerous version (a half-copied SQLite file next to a
13
+ // deleted original), which is why there is none.
14
+ //
15
+ // ⚠ AN EXPLICIT LOCATION WINS AND IS NEVER MIGRATED INTO. `LIMN_DATA_DIR` names the directory; when
16
+ // it is set this module moves nothing, because the user chose where their data lives. The finer
17
+ // overrides keep working on top of either: `DATABASE_URL` / `--db` (the database file) and
18
+ // `CLONE_DIR` (the clone cache).
19
+ //
20
+ // ⚠ A PINNED PATH INSIDE THE OLD DIRECTORY STOPS THE MOVE. The old README told people to pass
21
+ // `--db ~/.pierre-review/pierre-review.sqlite`, so a shell profile may still say so. Moving the
22
+ // directory out from under that value would make the next open CREATE an empty database at the old
23
+ // path while the real one sat unseen in ~/.limn. So nothing moves (`pinned_path`), and everything
24
+ // keeps using the old directory (below).
25
+ //
26
+ // ⚠ UNTIL THE MOVE HAS HAPPENED, THE OLD DIRECTORY IS THE DATA DIRECTORY. `resolveDataDir` answers
27
+ // `~/.pierre-review` while it exists and `~/.limn` does not — after a pinned path, or a rename that
28
+ // failed (Windows EBUSY on an open SQLite file, EXDEV, EACCES). Answering `~/.limn` there would
29
+ // create it, fresh and empty, and from then on every boot would stop at `target_exists` with the
30
+ // user's history stranded. Instead the next boot simply tries the move again.
31
+ //
32
+ // No imports outside `node:*`, so cli.ts can load it before config.ts reads the environment.
33
+ import { existsSync, renameSync } from 'node:fs';
34
+ import { homedir } from 'node:os';
35
+ import { isAbsolute, join, relative, resolve } from 'node:path';
36
+ /** The directory name under the home directory. */
37
+ export const DATA_DIR_NAME = '.limn';
38
+ /** The pre-rename directory name, moved once on first boot. */
39
+ export const LEGACY_DATA_DIR_NAME = '.pierre-review';
40
+ /**
41
+ * The local data directory: `LIMN_DATA_DIR` when set; else `~/.pierre-review` while it exists and
42
+ * `~/.limn` does not (the move has not happened yet — see the header); else `~/.limn`.
43
+ */
44
+ export function resolveDataDir(env = process.env, home = homedir()) {
45
+ const explicit = env.LIMN_DATA_DIR?.trim();
46
+ if (explicit)
47
+ return resolve(explicit);
48
+ const target = join(home, DATA_DIR_NAME);
49
+ const legacy = join(home, LEGACY_DATA_DIR_NAME);
50
+ if (existsSync(legacy) && !existsSync(target))
51
+ return legacy;
52
+ return target;
53
+ }
54
+ /** The database file the CLI and the server open by default (the name predates the rename). */
55
+ const DB_FILE = 'pierre-review.sqlite';
56
+ function isInside(dir, path) {
57
+ const rel = relative(dir, path);
58
+ return rel === '' || (!!rel && !rel.startsWith('..') && !isAbsolute(rel));
59
+ }
60
+ // The same failure can be reported by the CLI's call and the server's; say it once per process.
61
+ const said = new Set();
62
+ /**
63
+ * Move `~/.pierre-review` to `~/.limn`, once. Safe to call on every boot: after the first move
64
+ * there is no legacy directory left, so it is a single `existsSync`.
65
+ */
66
+ export function migrateLegacyDataDir(opts = {}) {
67
+ const env = opts.env ?? process.env;
68
+ const home = opts.home ?? homedir();
69
+ const say = (msg) => {
70
+ if (said.has(msg))
71
+ return;
72
+ said.add(msg);
73
+ opts.log?.(msg);
74
+ };
75
+ if (env.LIMN_DATA_DIR?.trim())
76
+ return { moved: false, reason: 'explicit_dir' };
77
+ const from = join(home, LEGACY_DATA_DIR_NAME);
78
+ const to = join(home, DATA_DIR_NAME);
79
+ if (!existsSync(from))
80
+ return { moved: false, reason: 'no_legacy_dir' };
81
+ // Never clobber: an existing ~/.limn is the user's data too. The old directory stays as it is —
82
+ // but a ~/.limn with no database next to an old one that has one is worth a line.
83
+ if (existsSync(to)) {
84
+ if (!existsSync(join(to, DB_FILE)) && existsSync(join(from, DB_FILE))) {
85
+ say(`Your earlier Limn data is still in ${from}, and ${to} already exists, so it was not moved. To use it, stop Limn and move ${join(from, DB_FILE)} into ${to}.`);
86
+ }
87
+ return { moved: false, reason: 'target_exists' };
88
+ }
89
+ // A database or clone path pinned inside the old directory: moving it would orphan that path.
90
+ const resolvePath = opts.resolvePath ?? ((p) => resolve(p));
91
+ for (const key of ['DATABASE_URL', 'CLONE_DIR']) {
92
+ const raw = env[key]?.trim();
93
+ if (raw && isInside(from, resolvePath(raw))) {
94
+ say(`Your data stays in ${from} because ${key} points inside it. To move it to ${to}, stop Limn, move the folder, and update ${key}.`);
95
+ return { moved: false, reason: 'pinned_path' };
96
+ }
97
+ }
98
+ try {
99
+ renameSync(from, to);
100
+ }
101
+ catch (err) {
102
+ // Leave everything where it was. `resolveDataDir` keeps answering the old directory while
103
+ // ~/.limn does not exist, so this boot runs on the user's real data and the next one tries
104
+ // the move again.
105
+ const error = err instanceof Error ? err.message : String(err);
106
+ say(`Could not move ${from} to ${to} (${error}). Limn is using ${from} for now and will try again next time.`);
107
+ return { moved: false, reason: 'rename_failed', error };
108
+ }
109
+ say(`Moved your Limn data from ${from} to ${to}.`);
110
+ return { moved: true, from, to };
111
+ }
112
+ //# sourceMappingURL=data-dir.js.map