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,318 @@
1
+ // The public contact form: POST /api/contact, delivered to a Slack incoming webhook.
2
+ //
3
+ // WHY SLACK AND NOT EMAIL. A contact form's job is to make sure a message ARRIVES.
4
+ // Email is the medium with the worst odds of that: it bounces, it lands in spam, and
5
+ // it needs SPF/DKIM/DMARC on the domain plus a transactional provider before the first
6
+ // message even has a chance. A webhook POST either returns 2xx or throws, and the
7
+ // failure is visible in this process's logs at the moment it happens. There is also
8
+ // precedent and a working shape already in the repo — the Pro plugin's per-workspace
9
+ // digest delivery (packages/pro/src/slack/) — though core cannot import from the
10
+ // plugin, so the URL validation below is a deliberate small copy rather than a shared
11
+ // helper. Email can be added later as a SECOND sink; it should never be the only one.
12
+ //
13
+ // ⚠ NO NEW DEPENDENCY, AND THAT IS A CONSTRAINT RATHER THAN A PREFERENCE.
14
+ // scripts/build-release.mjs generates release/package.json from a CURATED dependency
15
+ // list, so an SDK here ships to every `npx pierre-review` install for the sake of a
16
+ // hosted-only marketing page. Node ≥20's global fetch is the whole client.
17
+ //
18
+ // ---- Deciding a submission is genuine ----
19
+ //
20
+ // Three layers, none of which needs a third party, a script tag or a CSP change (the
21
+ // landing's CSP is `default-src 'self'` with `frame-src` absent entirely — a Turnstile
22
+ // or reCAPTCHA widget would need `script-src` AND `frame-src` widened, plus a line in
23
+ // the privacy policy, which is a decision worth taking deliberately rather than as a
24
+ // side effect of shipping a form):
25
+ //
26
+ // 1. A HONEYPOT field. `website` is rendered, labelled and reachable in the DOM, and
27
+ // hidden from sighted users by CSS + aria-hidden + tabindex=-1. A person never
28
+ // fills it; a form-filling bot fills every input it finds. A filled honeypot is
29
+ // answered with 200 and DROPPED — never a 400, because an error tells whoever is
30
+ // running the bot exactly which field gave them away.
31
+ //
32
+ // 2. A SIGNED TICKET with a minimum fill time. GET /api/contact/ticket returns
33
+ // `<issuedAtMs>.<hmac>`; the POST verifies the signature and requires the age to
34
+ // sit between MIN_FILL_MS and MAX_TICKET_MS. This is the layer that a client-sent
35
+ // "elapsed" number cannot be: the timestamp is signed by this server, so a bot
36
+ // cannot forge an age, and to get a valid one it has to make a second request and
37
+ // then WAIT. Stateless on purpose — no store, no replay table. A ticket is
38
+ // therefore reusable within its window, which the rate limit below bounds.
39
+ //
40
+ // 3. A RATE LIMIT. The `contact` tier in api/plugins/rate-limit.ts, keyed by IP
41
+ // because this route is deliberately anonymous. Five an hour.
42
+ //
43
+ // A determined human will still get a message through, which is correct — the target
44
+ // is automated submission volume, not a wall.
45
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
46
+ import { config } from '../../config.js';
47
+ // ---------------------------------------------------------------------------
48
+ // The ticket
49
+ // ---------------------------------------------------------------------------
50
+ /** A person needs longer than this to write a message. Below it, it was not typed. */
51
+ export const MIN_FILL_MS = 3_000;
52
+ /** Past this the ticket is stale — a page left open, or one harvested days ago. */
53
+ export const MAX_TICKET_MS = 6 * 60 * 60 * 1000;
54
+ // The signing key. SESSION_SECRET where there is one (cloud, where this form actually
55
+ // runs), otherwise a per-process random — which means a restart invalidates every
56
+ // outstanding ticket. That is acceptable and better than a constant: the failure mode
57
+ // is one visitor being asked to reload, and the alternative is a key an attacker can
58
+ // read out of the source.
59
+ //
60
+ // Derived rather than used directly so this never shares key material with the session
61
+ // cookie: the same secret signing two different things is how one forgery becomes two.
62
+ const TICKET_KEY = createHmac('sha256', config.sessionSecret || randomBytes(32))
63
+ .update('pierre:contact-ticket:v1')
64
+ .digest();
65
+ function sign(issuedAtMs) {
66
+ return createHmac('sha256', TICKET_KEY).update(String(issuedAtMs)).digest('hex');
67
+ }
68
+ /** Mint a ticket for a form that is being opened now. */
69
+ export function issueTicket(nowMs = Date.now()) {
70
+ return `${nowMs}.${sign(nowMs)}`;
71
+ }
72
+ /**
73
+ * Check a ticket's signature and age. Pure — `nowMs` is injected so the windows can be
74
+ * tested without waiting for them.
75
+ */
76
+ export function verifyTicket(raw, nowMs = Date.now()) {
77
+ if (typeof raw !== 'string')
78
+ return 'malformed';
79
+ const dot = raw.indexOf('.');
80
+ if (dot <= 0)
81
+ return 'malformed';
82
+ const issuedAt = Number.parseInt(raw.slice(0, dot), 10);
83
+ const mac = raw.slice(dot + 1);
84
+ if (!Number.isFinite(issuedAt) || mac === '')
85
+ return 'malformed';
86
+ // Constant-time, and length-checked first: timingSafeEqual THROWS on a length
87
+ // mismatch rather than returning false, so an attacker-supplied short mac would be a
88
+ // 500 instead of a 400.
89
+ const expected = Buffer.from(sign(issuedAt), 'utf-8');
90
+ const got = Buffer.from(mac, 'utf-8');
91
+ if (got.length !== expected.length || !timingSafeEqual(got, expected)) {
92
+ return 'bad_signature';
93
+ }
94
+ const age = nowMs - issuedAt;
95
+ // A negative age means a clock moved, not an attack — it is signed, so it can only
96
+ // have been minted here. Treated as "too fast", which is the safe side.
97
+ if (age < MIN_FILL_MS)
98
+ return 'too_fast';
99
+ if (age > MAX_TICKET_MS)
100
+ return 'expired';
101
+ return 'ok';
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // The webhook URL
105
+ // ---------------------------------------------------------------------------
106
+ const SLACK_WEBHOOK_HOST = 'hooks.slack.com';
107
+ /**
108
+ * Validate and normalise a Slack incoming-webhook URL.
109
+ *
110
+ * A near-copy of the Pro plugin's `normalizeSlackWebhookUrl` — core cannot import from
111
+ * `packages/pro` (it is a private submodule loaded by path, absent in OSS builds), so
112
+ * this is duplicated deliberately rather than shared. Keep the two in step.
113
+ *
114
+ * The value arrives from the environment, so the threat is a misconfiguration or a
115
+ * compromised variable turning this process into an SSRF proxy, not a user submission.
116
+ * Validating at the SINK makes the guarantee structural.
117
+ */
118
+ export function normalizeContactWebhookUrl(raw) {
119
+ const t = raw.trim();
120
+ if (t === '')
121
+ return null;
122
+ let url;
123
+ try {
124
+ url = new URL(t);
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ // https only: the secret is in the path, so http would leak it on the wire, and the
130
+ // scheme check also rules out file:/gopher:/ftp: redirect tricks.
131
+ if (url.protocol !== 'https:')
132
+ return null;
133
+ // Exact host. NOT endsWith('slack.com'), which accepts hooks.slack.com.evil.example.
134
+ if (url.hostname.toLowerCase() !== SLACK_WEBHOOK_HOST)
135
+ return null;
136
+ // A real webhook is /services/<team>/<bot>/<token>.
137
+ if (!url.pathname.startsWith('/services/'))
138
+ return null;
139
+ // Query and fragment are dropped — nothing legitimate uses them, and they are a
140
+ // place to hide a redirect target.
141
+ return `https://${SLACK_WEBHOOK_HOST}${url.pathname}`;
142
+ }
143
+ /** What the form offers, and what each one means when it lands in Slack. */
144
+ export const CONTACT_TOPICS = {
145
+ pro_trial: 'Free Pro month',
146
+ question: 'Question about the product',
147
+ problem: 'Something is broken',
148
+ other: 'Something else',
149
+ };
150
+ /**
151
+ * Slack's own escaping rules: `&`, `<` and `>` are the only three characters that are
152
+ * special in message text, and they must be escaped BEFORE the markup is assembled.
153
+ * Applied to every visitor-supplied string so a submission cannot forge a link, a
154
+ * channel reference or an @-mention in the notification it produces.
155
+ */
156
+ function slackEscape(s) {
157
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
158
+ }
159
+ /**
160
+ * Build the Block Kit payload. Pure, so the test asserts the shape and the escaping
161
+ * without a network.
162
+ *
163
+ * The message is ONE block per fact plus the body, because a contact form's whole
164
+ * value is that the reply address is readable at a glance on a phone.
165
+ */
166
+ export function buildContactMessage(s) {
167
+ // `?? 'Something else'` rather than `?? CONTACT_TOPICS['other']`: under
168
+ // noUncheckedIndexedAccess the second lookup is `string | undefined` too, so it
169
+ // would not narrow. The literal is the same string the 'other' key holds.
170
+ const topic = CONTACT_TOPICS[s.topic] ?? 'Something else';
171
+ // Angle brackets around the address would only be escaped back into literal ones, so
172
+ // the notification line uses parentheses and reads the same in every client.
173
+ const summary = `${topic} — ${s.name} (${s.email})`;
174
+ return {
175
+ // `text` is the notification line and the fallback for any client that cannot
176
+ // render blocks. Escaped like everything else.
177
+ text: slackEscape(summary),
178
+ blocks: [
179
+ {
180
+ type: 'section',
181
+ text: { type: 'mrkdwn', text: `*${slackEscape(topic)}*` },
182
+ },
183
+ {
184
+ type: 'section',
185
+ fields: [
186
+ { type: 'mrkdwn', text: `*From*\n${slackEscape(s.name)}` },
187
+ { type: 'mrkdwn', text: `*Reply to*\n${slackEscape(s.email)}` },
188
+ ],
189
+ },
190
+ {
191
+ type: 'section',
192
+ text: { type: 'mrkdwn', text: slackEscape(s.message) },
193
+ },
194
+ ],
195
+ };
196
+ }
197
+ /** POST the message. Throws on a non-2xx so the route can log it and 502. */
198
+ export async function postContactToSlack(webhookUrl, message) {
199
+ const safe = normalizeContactWebhookUrl(webhookUrl);
200
+ if (!safe)
201
+ throw new Error('CONTACT_SLACK_WEBHOOK_URL is not a Slack webhook URL');
202
+ const res = await fetch(safe, {
203
+ method: 'POST',
204
+ headers: { 'content-type': 'application/json' },
205
+ body: JSON.stringify(message),
206
+ // Never follow a redirect: a 30x is the standard way past a host allowlist that
207
+ // was only applied to the first URL.
208
+ redirect: 'error',
209
+ signal: AbortSignal.timeout(10_000),
210
+ });
211
+ if (!res.ok) {
212
+ // The response BODY is deliberately not surfaced — including it would turn a
213
+ // failed request into a read primitive (point the webhook at something internal
214
+ // and the error tells you what it said). The status is enough to diagnose.
215
+ throw new Error(`Slack webhook responded ${res.status}`);
216
+ }
217
+ }
218
+ // ---------------------------------------------------------------------------
219
+ // The routes
220
+ // ---------------------------------------------------------------------------
221
+ // Length caps are on the SCHEMA, so an oversized body is rejected by Fastify before any
222
+ // of this file runs. Sized for a real message rather than for an essay: Slack truncates
223
+ // a section block past 3000 characters anyway, and a 4000-character contact-form
224
+ // message is not a contact-form message.
225
+ const BODY_SCHEMA = {
226
+ type: 'object',
227
+ required: ['name', 'email', 'message', 'ticket'],
228
+ additionalProperties: false,
229
+ properties: {
230
+ name: { type: 'string', minLength: 1, maxLength: 120 },
231
+ // A deliberately loose pattern. Email addresses that are legal and that a strict
232
+ // regex rejects are common; the address is only ever used as a reply-to that a
233
+ // human reads, never as a delivery target, so a wrong one costs a reply and
234
+ // nothing else.
235
+ email: { type: 'string', minLength: 3, maxLength: 254, pattern: '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$' },
236
+ topic: { type: 'string', enum: Object.keys(CONTACT_TOPICS) },
237
+ message: { type: 'string', minLength: 1, maxLength: 2500 },
238
+ ticket: { type: 'string', minLength: 1, maxLength: 200 },
239
+ // The honeypot, and IT MUST BE DECLARED HERE OR THE WHOLE LAYER IS INERT. Fastify
240
+ // configures ajv with `removeAdditional: true`, so an undeclared property is
241
+ // SILENTLY STRIPPED from req.body before the handler runs — the request still 200s,
242
+ // the check below still sees `undefined`, and every bot passes. Verified by injection,
243
+ // not assumed: an unknown field posted to this route returns 200 with the field gone.
244
+ website: { type: 'string', maxLength: 200 },
245
+ },
246
+ };
247
+ export async function contactRoutes(app) {
248
+ const configured = normalizeContactWebhookUrl(config.contactSlackWebhookUrl) != null;
249
+ // Mint a ticket. Also the form's readiness probe: with no webhook configured it 503s
250
+ // here, so the page can say so BEFORE someone writes two hundred words into a form
251
+ // that has nowhere to send them.
252
+ app.get('/api/contact/ticket', async (_req, reply) => {
253
+ if (!configured) {
254
+ return reply.code(503).send({ error: 'contact_unavailable' });
255
+ }
256
+ // A ticket is a signed timestamp and nothing else — it identifies no one and
257
+ // carries no state — but it must not be cached by anything in front of this
258
+ // server, or every visitor shares one issue time.
259
+ reply.header('cache-control', 'no-store');
260
+ return { ticket: issueTicket() };
261
+ });
262
+ app.post('/api/contact', { schema: { body: BODY_SCHEMA } }, async (req, reply) => {
263
+ if (!configured) {
264
+ return reply.code(503).send({
265
+ error: 'contact_unavailable',
266
+ message: 'The contact form is not available right now.',
267
+ });
268
+ }
269
+ const body = req.body;
270
+ // Layer 1. Answered with the SUCCESS shape on purpose: a bot that is told it failed
271
+ // learns which field to leave alone next time, and a person can never reach this.
272
+ if (typeof body.website === 'string' && body.website.trim() !== '') {
273
+ req.log.info({ ip: req.ip }, 'contact form: honeypot filled, dropped');
274
+ return { ok: true };
275
+ }
276
+ // Layer 2.
277
+ const verdict = verifyTicket(body.ticket);
278
+ if (verdict !== 'ok') {
279
+ req.log.warn({ ip: req.ip, verdict }, 'contact form: ticket rejected');
280
+ // Two answers, because they mean different things to a person. An expired ticket
281
+ // is a page that sat open and the fix is to reload; anything else is either a bot
282
+ // or a genuinely broken client, and the message says what to do without saying
283
+ // which check failed.
284
+ return reply.code(400).send({
285
+ error: verdict === 'expired' ? 'ticket_expired' : 'ticket_invalid',
286
+ message: verdict === 'expired'
287
+ ? 'This form has been open for a while. Reload the page and send it again.'
288
+ : 'We could not verify that form. Reload the page and try again.',
289
+ });
290
+ }
291
+ try {
292
+ await postContactToSlack(config.contactSlackWebhookUrl, buildContactMessage({
293
+ name: body.name.trim(),
294
+ email: body.email.trim(),
295
+ topic: body.topic ?? 'other',
296
+ message: body.message.trim(),
297
+ }));
298
+ }
299
+ catch (err) {
300
+ // The submitter's words only exist in the request they just made, so a failure
301
+ // here loses them. Log enough to reconstruct the message by hand, and tell the
302
+ // visitor plainly that it did not send rather than showing a success screen over
303
+ // a dropped message.
304
+ // ⚠ `from:`, NEVER `name:`. Pino treats `name` as the LOGGER's name, so a `name`
305
+ // key here is swallowed into the log prefix — the line came out as
306
+ // `ERROR (Sam Okafor/19949)` with the submitter's name where the process name
307
+ // belongs and no `name` field in the body at all. Observed, not theorised.
308
+ req.log.error({ err, from: body.name, email: body.email, topic: body.topic, message: body.message }, 'contact form: delivery FAILED — the message is in this log line and nowhere else');
309
+ return reply.code(502).send({
310
+ error: 'delivery_failed',
311
+ message: 'That did not send. Please try again in a minute.',
312
+ });
313
+ }
314
+ req.log.info({ topic: body.topic }, 'contact form: delivered');
315
+ return { ok: true };
316
+ });
317
+ }
318
+ //# sourceMappingURL=contact.js.map
@@ -0,0 +1,58 @@
1
+ import { getDailyBriefCounts, getDailyBriefEntry } from '../../db/daily-brief.js';
2
+ import { listWorkspaces, resolveWorkspaceScope } from '../../db/queries.js';
3
+ import { accountIdOf } from '../plugins/auth.js';
4
+ // The daily brief (plan P3.1/N1 + the P3.3/N5 roll-up) — CORE / FREE, counts only. The Pro
5
+ // narration lives entirely on the plugin's synthesis seam (`kind: 'brief'` / `'rollup'`) and is
6
+ // fetched separately by the SPA; this route never touches AI and never carries cost/money.
7
+ //
8
+ // `?workspace=` is the ONE scope parameter (resolveWorkspaceScope: absent/unknown/foreign →
9
+ // Default, never a 404) and the resolved id is echoed. `?rollup=1` additionally returns one
10
+ // count line per OTHER workspace of the account (the "Elsewhere" strip line) — workspaces are
11
+ // few, and each line rides the fold's 5-min TTL cache (db/daily-brief.ts), so the loop is
12
+ // bounded and mostly cache hits. Defensively capped all the same.
13
+ //
14
+ // ⚠ THE TWO LINES ARE NOT SERVED THE SAME WAY, ON PURPOSE. The ACTIVE workspace's `counts` go
15
+ // through `getDailyBriefEntry`, which computes them FRESH: every one of those figures is the
16
+ // count of a list a strip line CLICKS INTO (`myTurn` is literally the my_turn card count that
17
+ // GET /api/attention recomputes per request), and a cached headline over a live list is a number
18
+ // the click disproves — which is exactly the "5 items need your review" over a board of 3 this
19
+ // split fixes. The `?rollup=1` lines describe OTHER workspaces and keep the cache: clicking one
20
+ // switches workspace, which re-fetches THAT workspace's fresh brief before any list is shown.
21
+ //
22
+ // ⚠ `generatedAt` therefore stamps the COUNTS (== this request); the `botAnomalies` array inside
23
+ // them keeps its own ≤5-min window (an 84-day events scan behind no clickable list). Two
24
+ // computation times in one response, deliberately — see db/daily-brief.ts's header before
25
+ // "simplifying" either half.
26
+ //
27
+ // Rate tier: `search` (60/min) — a DELIBERATE entry in tierFor, not the blanket read bucket:
28
+ // one call folds the insights cards, the resolve backlog and the trunk snapshot (× workspace
29
+ // count under `?rollup=1`). Pinned in rate-limit.test.ts.
30
+ const ROLLUP_WORKSPACE_CAP = 12;
31
+ export async function dailyBriefRoutes(app) {
32
+ app.get('/api/daily-brief', async (req) => {
33
+ const q = req.query;
34
+ const accountId = accountIdOf(req);
35
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
36
+ const { counts, computedAt } = await getDailyBriefEntry(accountId, scope.workspaceId);
37
+ const resp = {
38
+ workspaceId: scope.workspaceId,
39
+ counts,
40
+ generatedAt: computedAt.toISOString(),
41
+ };
42
+ if (q.rollup === '1' || q.rollup === 'true') {
43
+ const others = (await listWorkspaces(accountId))
44
+ .filter((w) => w.id !== scope.workspaceId)
45
+ .slice(0, ROLLUP_WORKSPACE_CAP);
46
+ if (others.length > 0) {
47
+ const lines = await Promise.all(others.map(async (w) => ({
48
+ workspaceId: w.id,
49
+ name: w.name,
50
+ counts: await getDailyBriefCounts(accountId, w.id),
51
+ })));
52
+ resp.rollup = lines;
53
+ }
54
+ }
55
+ return resp;
56
+ });
57
+ }
58
+ //# sourceMappingURL=daily-brief.js.map
@@ -0,0 +1,71 @@
1
+ import { FLOW_DEFAULT_WINDOW_DAYS, getFlowCourts } from '../../db/pr-intervals.js';
2
+ import { resolveWorkspaceScope } from '../../db/queries.js';
3
+ import { accountIdOf } from '../plugins/auth.js';
4
+ import { entitledProCapabilities } from '../../pro/contract.js';
5
+ // GET /api/flow-findings — "Chronology": the COURT LEDGER for a workspace.
6
+ //
7
+ // PAID, on the `periodReports` capability. Deterministic all the same — no model, no plugin call,
8
+ // no GitHub call — and every sentence it returns is still templated in db/pr-intervals.ts; what it
9
+ // costs is DB work, not tokens.
10
+ //
11
+ // ⚠ IT RIDES `periodReports` RATHER THAN A CAPABILITY OF ITS OWN, deliberately. Chronology and the
12
+ // period report answer the same question at two grains — "where did a completed stretch of work
13
+ // actually go" — and a fifteenth member on `ProCapabilities` would mean bumping `apiVersion` across
14
+ // four literals in two repositories (host contract, plugin index, plugin contract-types, the
15
+ // runtime gate in pro/bind.ts) to gate one route. One flag, no wire change, no plugin edit.
16
+ //
17
+ // ⚠ THE GATE IS A ROUTING DECISION AND MUST STAY ONE. `getFlowCourts` reads no capability and takes
18
+ // no flag: `verify:isolation` calls that fold DIRECTLY — no request, no account row, just the ids
19
+ // it is handed — and the day entitlement is threaded into the engine is the day every one of those
20
+ // cross-tenant assertions needs an account to run. (The `[7, 90]` window clamp is the mirror case:
21
+ // it lives in the ENGINE so a second caller cannot skip it, while this check lives HERE so the
22
+ // engine stays testable.)
23
+ //
24
+ // ⚠ THE SPA MUST GATE `useFlowFindings`' `enabled` ON THE SAME FLAG (it does). This route is
25
+ // POLLED — the panel refetches every five minutes — so an ungated hook against a 402 is a request
26
+ // every five minutes, per mounted pane, forever, and the panel renders "Could not load this
27
+ // workspace's flow.": an ERROR where the truth is a paywall, which is the worst of both. The
28
+ // unentitled SPA issues no request at all and paints the locked pane instead.
29
+ //
30
+ // `?workspace=<int>` is THE scope parameter and goes through `resolveWorkspaceScope`: absent,
31
+ // unparseable, unknown or ANOTHER TENANT'S id all resolve to the account's DEFAULT workspace —
32
+ // never a 404, so the route is not an existence oracle. The resolved id is echoed on the response
33
+ // so a client can correct a stale bookmark.
34
+ //
35
+ // `?days=<int>` is the window, default 30. The engine CLAMPS it to [7, 90] rather than trusting
36
+ // it: below seven days a median rests on three observations, and above ninety the retroactive
37
+ // COVERAGE BIAS dominates (docs/PERIOD-REPORTING.md — a workspace that onboarded repos over the
38
+ // span shows a "trend" that is entirely onboarding). `coverage` rides every response for the same
39
+ // reason. The clamp lives in the engine, not here, so a future second caller cannot skip it.
40
+ //
41
+ // Rate tier: `search` (60/min), a DELIBERATE entry in `tierFor` and pinned in rate-limit.test.ts.
42
+ // It is DB-only, but "this route is DB-only" is exactly the sentence that file exists to distrust:
43
+ // one call runs the lane resolver and then FOUR chunked action scans (reviews, review comments,
44
+ // conversation comments, commits) across every pull request merged in the window — the same shape
45
+ // of cost that put `GET /api/attention` and `GET /api/daily-brief` on this bucket.
46
+ export async function flowRoutes(app) {
47
+ app.get('/api/flow-findings', async (req, reply) => {
48
+ // The same entitlement view /api/me hands the SPA and the /api/pro/* 402 gate mirrors, so the
49
+ // tab's locked pane and this status code can never disagree about one account: a local account
50
+ // is entitled whenever the bound plugin advertises the capability, a free cloud account gets
51
+ // the 402. `!req.account` cannot happen in the running app (local synthesizes one, cloud 401s
52
+ // first) — it is here so a bare test harness fails closed rather than open.
53
+ //
54
+ // Ahead of `resolveWorkspaceScope` on purpose: an unentitled caller does no DB work, and the
55
+ // 402 is identical for every `?workspace=` value, so the gate cannot be read as an oracle for
56
+ // which workspaces exist.
57
+ if (!req.account || !entitledProCapabilities(req.account).periodReports) {
58
+ reply.status(402);
59
+ return { error: 'pro required' };
60
+ }
61
+ const q = req.query;
62
+ const accountId = accountIdOf(req);
63
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
64
+ // A garbage `?days=` is the DEFAULT, not a 400: this is a dashboard parameter carried in a
65
+ // bookmark, and the same "never an error out of a scope hint" rule `?workspace=` follows.
66
+ const parsed = q.days == null ? Number.NaN : Number.parseInt(q.days, 10);
67
+ const days = Number.isInteger(parsed) && parsed > 0 ? parsed : FLOW_DEFAULT_WINDOW_DAYS;
68
+ return getFlowCourts(accountId, scope, days);
69
+ });
70
+ }
71
+ //# sourceMappingURL=flow.js.map
@@ -0,0 +1,7 @@
1
+ export async function healthRoutes(app) {
2
+ app.get('/api/health', async () => ({
3
+ status: 'ok',
4
+ time: new Date().toISOString(),
5
+ }));
6
+ }
7
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,200 @@
1
+ import { getInsights, getRepoAnalytics, getWorkspaceMetricsDetail, getWorkspaceMetricsForScope, resolveWorkspaceScope, } from '../../db/queries.js';
2
+ import { getWorkspaceRepoActivity } from '../../db/repo-activity.js';
3
+ import { buildPendingBoard } from '../../db/pending-tabs.js';
4
+ import { PR_LIVENESS_MAX_IDS, sweepPrLiveness, } from '../../sync/pr-liveness-sweep.js';
5
+ import { accountIdOf } from '../plugins/auth.js';
6
+ // The liveness body. `maxItems` is a coarse guard only — the real cap is applied in the handler
7
+ // AFTER de-duplication (a board legitimately renders several cards for one PR), and it 400s
8
+ // rather than truncating.
9
+ const livenessSchema = {
10
+ querystring: {
11
+ type: 'object',
12
+ properties: { workspace: { type: 'string' } },
13
+ },
14
+ body: {
15
+ type: 'object',
16
+ required: ['prIds'],
17
+ additionalProperties: false,
18
+ properties: {
19
+ prIds: {
20
+ type: 'array',
21
+ items: { type: 'integer', minimum: 1 },
22
+ minItems: 1,
23
+ maxItems: 400,
24
+ },
25
+ },
26
+ },
27
+ };
28
+ function parseIntList(raw) {
29
+ if (!raw)
30
+ return null;
31
+ const ids = raw
32
+ .split(',')
33
+ .map((s) => Number.parseInt(s.trim(), 10))
34
+ .filter((n) => Number.isFinite(n));
35
+ return ids.length > 0 ? ids : null;
36
+ }
37
+ export async function insightsRoutes(app) {
38
+ // Per-repo sprint stats for the Insights panel. Scoped to the account; `repoIds` narrows to the
39
+ // repos the CALLER names. NOT workspace-scoped: it is a per-repo snapshot the caller already
40
+ // names its repos for, so there is no scope for a workspace to decide.
41
+ app.get('/api/insights', async (req) => {
42
+ const q = req.query;
43
+ return getInsights({
44
+ accountId: accountIdOf(req),
45
+ repoIds: parseIntList(q.repoIds),
46
+ });
47
+ });
48
+ // Workspace flow-metric header (DORA-ish tiles + trend charts) — CORE/free. It sits under the
49
+ // "Flow metrics" heading on the REPORTS rail entry, not the Feed: a workspace-wide survey on top
50
+ // of a chronological stream pushed the feed two screens down, and it is precisely why the Reports
51
+ // entry is ungated on every tier.
52
+ //
53
+ // `?workspace=<id>` is the ONE scope parameter: `resolveWorkspaceScope` resolves an absent,
54
+ // unparseable, unknown or foreign id to the account's DEFAULT workspace (never a 404 — every id
55
+ // yields the same response shape, so it is not an existence oracle, and the resolved id is always
56
+ // one the caller owns). Its `repoIds` are that workspace's membership, and `[]` — a workspace
57
+ // with no repos — is a legal state that yields `metrics: null`, NOT "every repo in the account".
58
+ // NO `narrow` is passed: Reports covers every repo in the workspace (the repo picker is
59
+ // Timeline-only), so this route takes no `?repoIds=` and must not gain one.
60
+ //
61
+ // It also carries `repoActivity` — the "where is the work happening?" per-repo breakdown under
62
+ // the same heading. RIDING THIS RESPONSE RATHER THAN A ROUTE OF ITS OWN IS THE DECISION: it is
63
+ // the same scope resolved once, the same 14-day window, painted in the same section by the same
64
+ // component, and it multiplies by nothing (two indexed scans plus the lane resolver's fixed
65
+ // handful), so `/api/workspace-metrics` keeps the `read` fall-through that api/plugins/
66
+ // rate-limit.ts records for it. A second route would have re-resolved the scope, doubled the
67
+ // round trips for one panel, and needed a tier decision to say the same thing.
68
+ app.get('/api/workspace-metrics', async (req) => {
69
+ const q = req.query;
70
+ const accountId = accountIdOf(req);
71
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
72
+ const [metrics, repoActivity] = await Promise.all([
73
+ getWorkspaceMetricsForScope(accountId, scope.repoIds),
74
+ getWorkspaceRepoActivity(accountId, scope, Date.now()),
75
+ ]);
76
+ // `workspaceId` is the scope echo every scoped response owes the client (docs/API.md) — this
77
+ // route was the one that never sent it, so a SPA holding a stale `?workspace=` had no way to
78
+ // learn it had been resolved to Default.
79
+ return {
80
+ metrics,
81
+ workspaceId: scope.workspaceId,
82
+ ...(repoActivity != null ? { repoActivity } : {}),
83
+ };
84
+ });
85
+ // The PR lists behind each flow-metric tile (the tile drill-down) — also CORE/free, so a Feed
86
+ // tile opens the same drill-down for everyone. Mirrors the Pro route's `{enabled, detail}` shape
87
+ // (enabled always true here). Same workspace resolution as its sibling above.
88
+ app.get('/api/workspace-metrics/detail', async (req) => {
89
+ const q = req.query;
90
+ const accountId = accountIdOf(req);
91
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
92
+ const detail = await getWorkspaceMetricsDetail(accountId, undefined, scope.repoIds);
93
+ return { enabled: true, detail };
94
+ });
95
+ // (GET /api/workspace-metrics/compare was DELETED with the "Compare workspaces" rail entry —
96
+ // cross-workspace comparison now lives inside Reports as the plugin-served "By workspace" axis,
97
+ // which rides the window-pure getPeriodMetricsForWorkspaces seam instead of the snapshot
98
+ // WorkspaceMetrics matrix.)
99
+ // The attention cards (your turn / red builds that are yours / stalled reviews / untouched
100
+ // threads / reviewer load / needs-a-reviewer / ready-to-land / behind-trunk) — CORE/free (the
101
+ // same cards Pro Insights computes in core getWorkspaceInsights), for the **Pending** rail
102
+ // entry. The bot cards are excluded (they live in the free Bots console).
103
+ //
104
+ // SERVED AS SIX TABS (`db/pending-tabs.ts`), each a scored list with its uncapped count — My
105
+ // turn grouped by the reader's type order and Dependencies by kind (security, then bumps) — free
106
+ // on every tier. The rank is code, with the reader's weights; only the Pro plan's NARRATION is
107
+ // paid.
108
+ //
109
+ // ⚠ THE TABS ARE AN ALLOW-LIST (`PENDING_TABS`), so a NEW InsightKind reaches this board only
110
+ // once it is given a tab — otherwise it is folded, counted and never listed. pending-tabs.test.ts
111
+ // fails when a non-bot kind has no tab, rather than letting it vanish quietly.
112
+ app.get('/api/attention', async (req) => {
113
+ const q = req.query;
114
+ // THE ONE FOLD (`buildPendingBoard`, db/pending-tabs.ts) — the Pro Slack digest reads the same
115
+ // function through `ProHostQueries.getPendingBoard`, so the two cannot drift. `live: true` is
116
+ // THIS route only: failing-check names on red cards (the brief, the work plan and the Pro
117
+ // payloads render no names, and must not pay for them or hash them) and suggested reviewers
118
+ // for the top "Needs a reviewer" cards.
119
+ const { workspaceId: _ws, ...board } = await buildPendingBoard(accountIdOf(req), q.workspace, {
120
+ live: true,
121
+ });
122
+ return board;
123
+ });
124
+ // POST /api/attention/liveness — THE BOARD'S ONE GITHUB QUESTION.
125
+ //
126
+ // The route above is DB-only and must stay that way: it is the board's paint path, and a
127
+ // GitHub call inside it would be the 200-calls-to-render-fifty-cards failure the board exists
128
+ // to avoid. This is its sibling — one batched `nodes(ids:)` sweep for the WHOLE board, so a PR
129
+ // merged by somebody else stops being a card within seconds instead of within an adaptive
130
+ // bucket (2-15 min). Cost is 2 GraphQL points per sweep, measured; the mechanics and the
131
+ // measured wall-time cliff that forces two passes are in sync/pr-liveness-sweep.ts and
132
+ // github/queries.ts's PR_LIVENESS_NODES_QUERY header.
133
+ //
134
+ // ⚠ IT RETURNS COUNTS, NEVER CARDS. `changed > 0` tells the SPA to refetch the board and the
135
+ // brief TOGETHER; it must never splice a card out locally (that breaks `capFor`'s
136
+ // `shown === count` guard and silently deletes the cap disclosure). Keeping cards out of this
137
+ // response is what makes that structural rather than a rule someone has to remember.
138
+ //
139
+ // ⚠ A POST WITH A GET'S COST, deliberately: the id list is a body (a query string of 90 ids is
140
+ // a header-size question nobody should have), and a POST puts it behind the cross-origin guard
141
+ // in cloud. That is the `POST /api/reactions/lookup` shape exactly, and it takes the same
142
+ // `prDetail` tier — spelled as an EXACT match in tierFor, because nothing else would catch it.
143
+ //
144
+ // Rate-limit PAUSES are reported, never thrown: `paused` is a fact about the tenant's GitHub
145
+ // window, and the board renders its synced rows through it.
146
+ app.post('/api/attention/liveness', { schema: livenessSchema }, async (req, reply) => {
147
+ const q = req.query;
148
+ const { prIds } = req.body;
149
+ const accountId = accountIdOf(req);
150
+ // ⚠ 400 ON OVER-CAP, NEVER A TRUNCATION (the peer benchmark's `?cells=` rule): a caller
151
+ // whose tail was silently dropped believes its whole board was freshened. `additionalItems`
152
+ // in the schema cannot express "dedupe then count", so the cap is checked here after the
153
+ // dedupe the client is asked to do anyway.
154
+ const unique = [...new Set(prIds)];
155
+ if (unique.length > PR_LIVENESS_MAX_IDS) {
156
+ return reply.code(400).send({
157
+ error: 'TooManyIds',
158
+ message: `At most ${PR_LIVENESS_MAX_IDS} PR ids per liveness sweep (received ${unique.length}).`,
159
+ });
160
+ }
161
+ // `?workspace=` is THE scope parameter — absent/unknown/foreign resolves to the account's
162
+ // DEFAULT workspace, never a 404, and the resolved id is echoed. Its `repoIds` are what
163
+ // bound the id resolve below, so a PR the caller owns in ANOTHER workspace is not swept
164
+ // here: the board that asked is the board that gets refreshed.
165
+ const scope = await resolveWorkspaceScope(accountId, q.workspace);
166
+ const swept = await sweepPrLiveness({
167
+ accountId,
168
+ repoIds: scope.repoIds,
169
+ prIds: unique,
170
+ log: req.log,
171
+ });
172
+ // null = a sweep for this account is already in flight (two tabs, or an interval overlapping
173
+ // its own focus refetch). A no-op report, not an error: the running sweep is about to
174
+ // produce this very answer, and the caller's next tick will see its effect.
175
+ if (!swept) {
176
+ return {
177
+ workspaceId: scope.workspaceId,
178
+ checked: 0,
179
+ mergeStateChecked: 0,
180
+ changed: 0,
181
+ leftOpenSet: 0,
182
+ paused: null,
183
+ };
184
+ }
185
+ return { workspaceId: scope.workspaceId, ...swept };
186
+ });
187
+ // Heavier per-repo analytics for the drill-down chart panel — loaded on demand.
188
+ // Ownership-scoped: a repo not owned by the account 404s.
189
+ app.get('/api/insights/:repoId/analytics', async (req, reply) => {
190
+ const { repoId } = req.params;
191
+ const id = Number.parseInt(repoId, 10);
192
+ const data = Number.isFinite(id)
193
+ ? await getRepoAnalytics(accountIdOf(req), id)
194
+ : null;
195
+ if (!data)
196
+ return reply.code(404).send({ error: 'repo not found' });
197
+ return data;
198
+ });
199
+ }
200
+ //# sourceMappingURL=insights.js.map