local-review 0.0.1 → 0.2.0

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 (364) hide show
  1. package/AGENT-GUIDE.md +468 -0
  2. package/CHANGELOG.md +53 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -2
  5. package/dist/server/anchors.js +207 -0
  6. package/dist/server/assets.js +147 -0
  7. package/dist/server/backups.js +212 -0
  8. package/dist/server/branches.js +113 -0
  9. package/dist/server/cli.js +255 -0
  10. package/dist/server/commits.js +393 -0
  11. package/dist/server/config.js +59 -0
  12. package/dist/server/context.js +68 -0
  13. package/dist/server/events.js +87 -0
  14. package/dist/server/git.js +103 -0
  15. package/dist/server/index.js +566 -0
  16. package/dist/server/landed.js +71 -0
  17. package/dist/server/plan.js +260 -0
  18. package/dist/server/port.js +3 -0
  19. package/dist/server/projectEdit.js +117 -0
  20. package/dist/server/projects.js +319 -0
  21. package/dist/server/prs.js +314 -0
  22. package/dist/server/reviewer.js +33 -0
  23. package/dist/server/reviews.js +586 -0
  24. package/dist/server/safefs.js +92 -0
  25. package/dist/server/security.js +38 -0
  26. package/dist/server/stackviz.js +446 -0
  27. package/dist/shared/assets.js +54 -0
  28. package/dist/shared/paths.js +13 -0
  29. package/dist/shared/prText.js +22 -0
  30. package/dist/shared/readiness.js +37 -0
  31. package/dist/shared/repoLabel.js +13 -0
  32. package/dist/shared/totals.js +75 -0
  33. package/dist/web/assets/abap-CLvhMVsD.js +1 -0
  34. package/dist/web/assets/actionscript-3-Cs6JJXT5.js +1 -0
  35. package/dist/web/assets/ada-X3Y2mlq2.js +1 -0
  36. package/dist/web/assets/ahk-DwADkxyd.js +1 -0
  37. package/dist/web/assets/ahk2-_6rzW9BS.js +1 -0
  38. package/dist/web/assets/andromeeda-vGVdxbeo.js +1 -0
  39. package/dist/web/assets/angular-html-DJ4eInGE.js +1 -0
  40. package/dist/web/assets/angular-ts-DBhgg2a7.js +1 -0
  41. package/dist/web/assets/apache-U0d_L8uA.js +1 -0
  42. package/dist/web/assets/apex-KFGFyKwG.js +1 -0
  43. package/dist/web/assets/apl-TCQAu201.js +1 -0
  44. package/dist/web/assets/apple-touch-icon-BNrSfFwW.png +0 -0
  45. package/dist/web/assets/applescript-CCn79oCD.js +1 -0
  46. package/dist/web/assets/ara-4CJ0cIlV.js +1 -0
  47. package/dist/web/assets/asciidoc-SCjQUq34.js +1 -0
  48. package/dist/web/assets/asm-Cmm7eHzH.js +1 -0
  49. package/dist/web/assets/astro-DVtFuwC2.js +1 -0
  50. package/dist/web/assets/aurora-x-CDeNXAV0.js +1 -0
  51. package/dist/web/assets/awk-BWXHIvNe.js +1 -0
  52. package/dist/web/assets/ayu-dark-DluEY0Gj.js +1 -0
  53. package/dist/web/assets/ayu-light-C3h-C4tm.js +1 -0
  54. package/dist/web/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  55. package/dist/web/assets/ballerina-B7ZEbQpA.js +1 -0
  56. package/dist/web/assets/bat-qdxAdBWQ.js +1 -0
  57. package/dist/web/assets/beancount-C2IN4Vk8.js +1 -0
  58. package/dist/web/assets/berry-DKpUyyne.js +1 -0
  59. package/dist/web/assets/bibtex-Ci_nEsc7.js +1 -0
  60. package/dist/web/assets/bicep-CUHmPFLl.js +1 -0
  61. package/dist/web/assets/bird2-CYIUgYqB.js +1 -0
  62. package/dist/web/assets/blade-CMp-sZYg.js +1 -0
  63. package/dist/web/assets/bsl-_VjlB9mv.js +1 -0
  64. package/dist/web/assets/c-DgqspIM2.js +1 -0
  65. package/dist/web/assets/c3-D0apG41V.js +1 -0
  66. package/dist/web/assets/cadence-CQ2zXKGN.js +1 -0
  67. package/dist/web/assets/cairo-DLTphjLi.js +1 -0
  68. package/dist/web/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  69. package/dist/web/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  70. package/dist/web/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  71. package/dist/web/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  72. package/dist/web/assets/chapel-NTLt7i8E.js +1 -0
  73. package/dist/web/assets/clarity-CtfBBspg.js +1 -0
  74. package/dist/web/assets/clojure-DqKBuwfJ.js +1 -0
  75. package/dist/web/assets/cmake-Bj61d0ZC.js +1 -0
  76. package/dist/web/assets/cobol-CJiWQrT4.js +1 -0
  77. package/dist/web/assets/codeowners-C8r90Shi.js +1 -0
  78. package/dist/web/assets/codeql-oeQT6MSM.js +1 -0
  79. package/dist/web/assets/coffee-Bv_hE6wf.js +1 -0
  80. package/dist/web/assets/common-lisp-Cv5bFMCO.js +1 -0
  81. package/dist/web/assets/coq-CGaSfhwR.js +1 -0
  82. package/dist/web/assets/cpp-CuUsyAvc.js +1 -0
  83. package/dist/web/assets/crystal-BeLopiD_.js +1 -0
  84. package/dist/web/assets/csharp-dmMxLVLW.js +1 -0
  85. package/dist/web/assets/css-OngwBV3K.js +1 -0
  86. package/dist/web/assets/csv-Dx-8-gkx.js +1 -0
  87. package/dist/web/assets/cue-CE9AQfxI.js +1 -0
  88. package/dist/web/assets/cypher-ClKdZ_lG.js +1 -0
  89. package/dist/web/assets/d-qD-0Kul2.js +1 -0
  90. package/dist/web/assets/dark-plus-Cs2F2srj.js +1 -0
  91. package/dist/web/assets/dart-CHZY9Xhz.js +1 -0
  92. package/dist/web/assets/dax-BkyTk9wS.js +1 -0
  93. package/dist/web/assets/desktop-Dlh5hvp9.js +1 -0
  94. package/dist/web/assets/diff-woXpYk--.js +1 -0
  95. package/dist/web/assets/docker-IyjqRm3v.js +1 -0
  96. package/dist/web/assets/dotenv-CuH5my32.js +1 -0
  97. package/dist/web/assets/dracula-BHWKrbxM.js +1 -0
  98. package/dist/web/assets/dracula-soft-5eyTD99u.js +1 -0
  99. package/dist/web/assets/dream-maker-DW3nJb8Q.js +1 -0
  100. package/dist/web/assets/edge-BFLildoE.js +1 -0
  101. package/dist/web/assets/elixir-1Fs6FjVs.js +1 -0
  102. package/dist/web/assets/elm-piMM67vF.js +1 -0
  103. package/dist/web/assets/emacs-lisp-DR2vtWaj.js +1 -0
  104. package/dist/web/assets/erb-JJ0sF8GL.js +1 -0
  105. package/dist/web/assets/erlang-Cphh6RMH.js +1 -0
  106. package/dist/web/assets/everforest-dark-sB-x3p7T.js +1 -0
  107. package/dist/web/assets/everforest-light-Df2xbC6M.js +1 -0
  108. package/dist/web/assets/favicon-Bld6z4jR.svg +13 -0
  109. package/dist/web/assets/favicon-CDTyigy2.ico +0 -0
  110. package/dist/web/assets/fennel-DQxkIbk2.js +1 -0
  111. package/dist/web/assets/fish-BJitypiv.js +1 -0
  112. package/dist/web/assets/fluent-C03EYrpw.js +1 -0
  113. package/dist/web/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  114. package/dist/web/assets/fortran-free-form-CYNrtFtB.js +1 -0
  115. package/dist/web/assets/fsharp-D13ZGOAj.js +1 -0
  116. package/dist/web/assets/gdresource-xMC7jr7x.js +1 -0
  117. package/dist/web/assets/gdscript-DYvRD3hg.js +1 -0
  118. package/dist/web/assets/gdshader-CBce3t8t.js +1 -0
  119. package/dist/web/assets/genie-CV2tkWYe.js +1 -0
  120. package/dist/web/assets/gherkin-DExj1W_8.js +1 -0
  121. package/dist/web/assets/git-commit-BSykSTBG.js +1 -0
  122. package/dist/web/assets/git-rebase-CtqXywWe.js +1 -0
  123. package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
  124. package/dist/web/assets/github-dark-default-DXG-b-1a.js +1 -0
  125. package/dist/web/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  126. package/dist/web/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  127. package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
  128. package/dist/web/assets/github-light-default-BXViO-2h.js +1 -0
  129. package/dist/web/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  130. package/dist/web/assets/gleam-CSRkHgEL.js +1 -0
  131. package/dist/web/assets/glimmer-js-BPewWxGC.js +1 -0
  132. package/dist/web/assets/glimmer-ts-DyUQWB47.js +1 -0
  133. package/dist/web/assets/glsl-D6tJoMaT.js +1 -0
  134. package/dist/web/assets/gn-CAXYAgV-.js +1 -0
  135. package/dist/web/assets/gnuplot-7GGW24-e.js +1 -0
  136. package/dist/web/assets/go-rLFTqkRN.js +1 -0
  137. package/dist/web/assets/graphql-E6u2hkWE.js +1 -0
  138. package/dist/web/assets/groovy-CacY0gHj.js +1 -0
  139. package/dist/web/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  140. package/dist/web/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  141. package/dist/web/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  142. package/dist/web/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  143. package/dist/web/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  144. package/dist/web/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  145. package/dist/web/assets/hack-Djq191-P.js +1 -0
  146. package/dist/web/assets/haml-C0LqmZG3.js +1 -0
  147. package/dist/web/assets/handlebars-DwdxF6uN.js +1 -0
  148. package/dist/web/assets/haskell-D8IpX4py.js +1 -0
  149. package/dist/web/assets/haxe-C8oCnwJJ.js +1 -0
  150. package/dist/web/assets/hcl-Dh228itO.js +1 -0
  151. package/dist/web/assets/hjson-CxZEssPk.js +1 -0
  152. package/dist/web/assets/hlsl-Cvrh5tZx.js +1 -0
  153. package/dist/web/assets/horizon-CE9ld1lL.js +1 -0
  154. package/dist/web/assets/horizon-bright-DSNQnXHK.js +1 -0
  155. package/dist/web/assets/houston-CsvMBhTu.js +1 -0
  156. package/dist/web/assets/html-B5Myt0fy.js +1 -0
  157. package/dist/web/assets/html-derivative-DTVrLAw3.js +1 -0
  158. package/dist/web/assets/http-CFoh3cQf.js +1 -0
  159. package/dist/web/assets/hurl-B_38naAo.js +1 -0
  160. package/dist/web/assets/hxml-Dcm88srW.js +1 -0
  161. package/dist/web/assets/hy-CZbG8q4J.js +1 -0
  162. package/dist/web/assets/imba-DsUTQ-LC.js +1 -0
  163. package/dist/web/assets/index-CfxIojO0.js +451 -0
  164. package/dist/web/assets/index-QywYYqTw.css +1 -0
  165. package/dist/web/assets/ini-B5eOa1yu.js +1 -0
  166. package/dist/web/assets/java-odnexk6r.js +1 -0
  167. package/dist/web/assets/javascript-DvFCJKws.js +1 -0
  168. package/dist/web/assets/jinja-UeLt2DYu.js +1 -0
  169. package/dist/web/assets/jison-HNaDYeqE.js +1 -0
  170. package/dist/web/assets/json-D57CnrjR.js +1 -0
  171. package/dist/web/assets/json5-BR5RXkoi.js +1 -0
  172. package/dist/web/assets/jsonc-CYpm1nAK.js +1 -0
  173. package/dist/web/assets/jsonl-CmCQp5Yx.js +1 -0
  174. package/dist/web/assets/jsonnet-CJTPZ8u_.js +1 -0
  175. package/dist/web/assets/jssm-DXw9l8Rf.js +1 -0
  176. package/dist/web/assets/jsx-o_5ZAydi.js +1 -0
  177. package/dist/web/assets/julia-20UebbrD.js +1 -0
  178. package/dist/web/assets/just-DqnPCg-Q.js +1 -0
  179. package/dist/web/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  180. package/dist/web/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  181. package/dist/web/assets/kanagawa-wave-BML5mF2J.js +1 -0
  182. package/dist/web/assets/kdl-CsD5j6eV.js +1 -0
  183. package/dist/web/assets/kotlin-DhhofPvG.js +1 -0
  184. package/dist/web/assets/kusto-C7mF5XQf.js +1 -0
  185. package/dist/web/assets/laserwave-C_8bwKvT.js +1 -0
  186. package/dist/web/assets/latex-CZhIB-LV.js +1 -0
  187. package/dist/web/assets/lean-CewbzKMR.js +1 -0
  188. package/dist/web/assets/less-DVTAwKKz.js +1 -0
  189. package/dist/web/assets/light-plus-DVQuIRkW.js +1 -0
  190. package/dist/web/assets/liquid-BzouPsN0.js +1 -0
  191. package/dist/web/assets/llvm-DqHOMS-s.js +1 -0
  192. package/dist/web/assets/log-BNLmms1o.js +1 -0
  193. package/dist/web/assets/logo-Cluzi2Zq.js +1 -0
  194. package/dist/web/assets/lua-BXclv8Fm.js +1 -0
  195. package/dist/web/assets/luau-VGWVLoqX.js +1 -0
  196. package/dist/web/assets/make-Dixweg8N.js +1 -0
  197. package/dist/web/assets/markdown-BYOwaDjH.js +1 -0
  198. package/dist/web/assets/marko-M78_INlG.js +1 -0
  199. package/dist/web/assets/material-theme-Bm3Qr25_.js +1 -0
  200. package/dist/web/assets/material-theme-darker-2IIEA8gg.js +1 -0
  201. package/dist/web/assets/material-theme-lighter-uhdI0v04.js +1 -0
  202. package/dist/web/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  203. package/dist/web/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  204. package/dist/web/assets/matlab-D7qyCx1q.js +1 -0
  205. package/dist/web/assets/mdc-CBluOiY7.js +1 -0
  206. package/dist/web/assets/mdx-DQZ5AkYe.js +1 -0
  207. package/dist/web/assets/mermaid-BxpfTIzt.js +1 -0
  208. package/dist/web/assets/min-dark-BSWPekZh.js +1 -0
  209. package/dist/web/assets/min-light-DDpmG2fV.js +1 -0
  210. package/dist/web/assets/mipsasm-BMqwQI7S.js +1 -0
  211. package/dist/web/assets/mojo-fg40_ntQ.js +1 -0
  212. package/dist/web/assets/monokai-BscAjiQZ.js +1 -0
  213. package/dist/web/assets/moonbit-D9W784E_.js +1 -0
  214. package/dist/web/assets/move-BYnGhZoM.js +1 -0
  215. package/dist/web/assets/narrat-_X_XdTYD.js +1 -0
  216. package/dist/web/assets/nextflow-CSgcYZcx.js +1 -0
  217. package/dist/web/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  218. package/dist/web/assets/nginx-CZFFQFbT.js +1 -0
  219. package/dist/web/assets/night-owl-DhmEMT88.js +1 -0
  220. package/dist/web/assets/night-owl-light-eJ-hLW7d.js +1 -0
  221. package/dist/web/assets/nim-17kKsGqk.js +1 -0
  222. package/dist/web/assets/nix-IvuFDN5E.js +1 -0
  223. package/dist/web/assets/nord-Cb4Vim4T.js +1 -0
  224. package/dist/web/assets/nsis-CjNLYCLg.js +1 -0
  225. package/dist/web/assets/nushell--0fT3PI1.js +1 -0
  226. package/dist/web/assets/objective-c-D1A_Heim.js +1 -0
  227. package/dist/web/assets/objective-cpp-BsSzOQcm.js +1 -0
  228. package/dist/web/assets/ocaml-O90oeIOV.js +1 -0
  229. package/dist/web/assets/odin-B1RWQWA5.js +1 -0
  230. package/dist/web/assets/one-dark-pro-C6YyR8OD.js +1 -0
  231. package/dist/web/assets/one-light-D7Lr4KcI.js +1 -0
  232. package/dist/web/assets/openscad-BUDT5pXO.js +1 -0
  233. package/dist/web/assets/org-BZABHRQH.js +1 -0
  234. package/dist/web/assets/pascal-4ZHwLPI5.js +1 -0
  235. package/dist/web/assets/perl-CMrAQ9Je.js +1 -0
  236. package/dist/web/assets/php-DwA3kETk.js +1 -0
  237. package/dist/web/assets/pierre-dark-CpLgRqie.js +1 -0
  238. package/dist/web/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  239. package/dist/web/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  240. package/dist/web/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  241. package/dist/web/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  242. package/dist/web/assets/pierre-light-CoaEpmwp.js +1 -0
  243. package/dist/web/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  244. package/dist/web/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  245. package/dist/web/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  246. package/dist/web/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  247. package/dist/web/assets/pkl-ot-7Btpt.js +1 -0
  248. package/dist/web/assets/plastic-DQwYfKfQ.js +1 -0
  249. package/dist/web/assets/plsql-DGHpHOYJ.js +1 -0
  250. package/dist/web/assets/po-BiJDBrnU.js +1 -0
  251. package/dist/web/assets/poimandres-DRFjx7u4.js +1 -0
  252. package/dist/web/assets/polar-C7UOKdEL.js +1 -0
  253. package/dist/web/assets/postcss-BXeXVLqQ.js +1 -0
  254. package/dist/web/assets/powerquery-DNMTfnFr.js +1 -0
  255. package/dist/web/assets/powershell-QtSnmXcu.js +1 -0
  256. package/dist/web/assets/prisma-DMgNhnYS.js +1 -0
  257. package/dist/web/assets/prolog-iXnhIJG7.js +1 -0
  258. package/dist/web/assets/proto-DB4EqR-F.js +1 -0
  259. package/dist/web/assets/pug-D4fM85kA.js +1 -0
  260. package/dist/web/assets/puppet-CDv2pdJW.js +1 -0
  261. package/dist/web/assets/purescript-9MfHhQsQ.js +1 -0
  262. package/dist/web/assets/python-gzcpVVnB.js +1 -0
  263. package/dist/web/assets/qml-C3893RHi.js +1 -0
  264. package/dist/web/assets/qmldir-DCQb3MpD.js +1 -0
  265. package/dist/web/assets/qss-Fe1Jh2GI.js +1 -0
  266. package/dist/web/assets/r-DwJhLW77.js +1 -0
  267. package/dist/web/assets/racket-DcIDlBhZ.js +1 -0
  268. package/dist/web/assets/raku-B3gFvitq.js +1 -0
  269. package/dist/web/assets/razor-_KkCNZ8V.js +1 -0
  270. package/dist/web/assets/rbs-C0MpOrj4.js +1 -0
  271. package/dist/web/assets/red-CYlAq2b0.js +1 -0
  272. package/dist/web/assets/reg-CRGYupPL.js +1 -0
  273. package/dist/web/assets/regexp-BZ9NHg3b.js +1 -0
  274. package/dist/web/assets/rel-BtDbiS_P.js +1 -0
  275. package/dist/web/assets/riscv-Ckw8ddFX.js +1 -0
  276. package/dist/web/assets/ron-VUp2lXgN.js +1 -0
  277. package/dist/web/assets/rose-pine-aMsaMD1D.js +1 -0
  278. package/dist/web/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  279. package/dist/web/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  280. package/dist/web/assets/rosmsg-CAekHB0j.js +1 -0
  281. package/dist/web/assets/rst-gdK-1c4t.js +1 -0
  282. package/dist/web/assets/ruby-DsmqYtlH.js +1 -0
  283. package/dist/web/assets/rust-DpiIf5JO.js +1 -0
  284. package/dist/web/assets/sas-BNGv1ScF.js +1 -0
  285. package/dist/web/assets/sass-DXrisJhu.js +1 -0
  286. package/dist/web/assets/scala-BiFt_7mu.js +1 -0
  287. package/dist/web/assets/scheme-DQCgrYNe.js +1 -0
  288. package/dist/web/assets/scss-DjYUXT73.js +1 -0
  289. package/dist/web/assets/sdbl-bTVj8UrX.js +1 -0
  290. package/dist/web/assets/shaderlab-TOUzSsQk.js +1 -0
  291. package/dist/web/assets/shellscript-DBtV-eli.js +1 -0
  292. package/dist/web/assets/shellsession-BKEV8Fnx.js +1 -0
  293. package/dist/web/assets/slack-dark-DnToyrRv.js +1 -0
  294. package/dist/web/assets/slack-ochin-B2OO5cIa.js +1 -0
  295. package/dist/web/assets/smalltalk-BHpQb5nM.js +1 -0
  296. package/dist/web/assets/smithy-Dgaz7aur.js +1 -0
  297. package/dist/web/assets/snazzy-light-4G7pJPwS.js +1 -0
  298. package/dist/web/assets/solarized-dark-b97czJOk.js +1 -0
  299. package/dist/web/assets/solarized-light-DFPxpIYQ.js +1 -0
  300. package/dist/web/assets/solidity-Bj7E1mRn.js +1 -0
  301. package/dist/web/assets/soy-DUapMxos.js +1 -0
  302. package/dist/web/assets/sparql-D_iOobhT.js +1 -0
  303. package/dist/web/assets/splunk-BC2Px7Mm.js +1 -0
  304. package/dist/web/assets/sql-B-UyqKkd.js +1 -0
  305. package/dist/web/assets/ssh-config-BgfXC-Er.js +1 -0
  306. package/dist/web/assets/stata-Xq3iZiMK.js +1 -0
  307. package/dist/web/assets/stylus-B6D30XZt.js +1 -0
  308. package/dist/web/assets/surrealql-CzXacK8i.js +1 -0
  309. package/dist/web/assets/svelte-CZaBEmPP.js +1 -0
  310. package/dist/web/assets/swift-CyEgAFGc.js +1 -0
  311. package/dist/web/assets/synthwave-84-nFMaYfgc.js +1 -0
  312. package/dist/web/assets/system-verilog-Dh5ihWbf.js +1 -0
  313. package/dist/web/assets/systemd-BxMlprV5.js +1 -0
  314. package/dist/web/assets/talonscript-CohzipZa.js +1 -0
  315. package/dist/web/assets/tasl-DMoTqEGO.js +1 -0
  316. package/dist/web/assets/tcl-CZd0xW_V.js +1 -0
  317. package/dist/web/assets/templ-CxkGuUpr.js +1 -0
  318. package/dist/web/assets/terraform-DswuEJGm.js +1 -0
  319. package/dist/web/assets/tex-DP74JH6g.js +1 -0
  320. package/dist/web/assets/tokyo-night-oM2G3aXe.js +1 -0
  321. package/dist/web/assets/toml-CcmNWLt0.js +1 -0
  322. package/dist/web/assets/ts-tags-BAcrCE2Y.js +1 -0
  323. package/dist/web/assets/tsv-sltzmVWM.js +1 -0
  324. package/dist/web/assets/tsx-Dw11YP6z.js +1 -0
  325. package/dist/web/assets/turtle-ByJddavk.js +1 -0
  326. package/dist/web/assets/twig-BMVjBxD6.js +1 -0
  327. package/dist/web/assets/typescript-DoBLlVdC.js +1 -0
  328. package/dist/web/assets/typespec-BGhR2SqG.js +1 -0
  329. package/dist/web/assets/typst-w2u_vl4_.js +1 -0
  330. package/dist/web/assets/v-DBj4K9d_.js +1 -0
  331. package/dist/web/assets/vala-zf12oZj6.js +1 -0
  332. package/dist/web/assets/vb-DOwzaHZ0.js +1 -0
  333. package/dist/web/assets/verilog-B2gwz-sJ.js +1 -0
  334. package/dist/web/assets/vesper-DdrHHSXu.js +1 -0
  335. package/dist/web/assets/vhdl-BroJfC0k.js +1 -0
  336. package/dist/web/assets/viml-DvXPmvsu.js +1 -0
  337. package/dist/web/assets/vitesse-black-fwtXNY1n.js +1 -0
  338. package/dist/web/assets/vitesse-dark-BZCL-v6S.js +1 -0
  339. package/dist/web/assets/vitesse-light-VbXTXTou.js +1 -0
  340. package/dist/web/assets/vue-BmuHTv5p.js +1 -0
  341. package/dist/web/assets/vue-html-Dt0zSF4I.js +1 -0
  342. package/dist/web/assets/vue-vine-wvJ1BTex.js +1 -0
  343. package/dist/web/assets/vyper-CgoNMtux.js +1 -0
  344. package/dist/web/assets/wasm-BnjxR4X6.js +1 -0
  345. package/dist/web/assets/wasm-ByWQv1Qj.js +1 -0
  346. package/dist/web/assets/wenyan-C8pVoKbM.js +1 -0
  347. package/dist/web/assets/wgsl-BsKzXJz4.js +1 -0
  348. package/dist/web/assets/wikitext-ClFFjSW2.js +1 -0
  349. package/dist/web/assets/wit-DdvCle-K.js +1 -0
  350. package/dist/web/assets/wolfram-DLL8P-h_.js +1 -0
  351. package/dist/web/assets/xml-CXftIBCQ.js +1 -0
  352. package/dist/web/assets/xsl-BoHSOMz1.js +1 -0
  353. package/dist/web/assets/yaml-dGreldtD.js +1 -0
  354. package/dist/web/assets/zenscript-BnlCZFoB.js +1 -0
  355. package/dist/web/assets/zig-CMLA9XwU.js +1 -0
  356. package/dist/web/index.html +16 -0
  357. package/examples/project.example.yaml +40 -0
  358. package/package.json +89 -3
  359. package/server/fonts/Inter-OFL.txt +93 -0
  360. package/server/fonts/Inter_400Regular.ttf +0 -0
  361. package/server/fonts/Inter_600SemiBold.ttf +0 -0
  362. package/server/fonts/Inter_700Bold.ttf +0 -0
  363. package/server/fonts/JetBrainsMono-OFL.txt +93 -0
  364. package/server/fonts/JetBrainsMono_400Regular.ttf +0 -0
@@ -0,0 +1,319 @@
1
+ // Projects: projects/<slug>/project.yaml lists repos (each a commit range), and splits each repo's commits
2
+ // into consecutive, colour-coded PR stacks. Everything here is re-read on every request, so edits (by the reviewer or
3
+ // an agent) show up without a restart.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import YAML from 'yaml';
7
+ import { PROJECTS_DIR, SAFE_NAME, expandHome, safeName } from './config.js';
8
+ import { loadRepo, rangeEnd } from './commits.js';
9
+ import { normalizeSubject } from './reviews.js';
10
+ import { loadLanded } from './landed.js';
11
+ import { loadPrs } from './prs.js';
12
+ import { inspectOrphan, liveCommits } from './context.js';
13
+ // the colours a regular stack can have (auto-assigned in this order, cycling). Yellow isn't one of them: it's
14
+ // reserved for follow-on stacks, which are always yellow whatever their `color:` says.
15
+ export const PALETTE = ['blue', 'green', 'purple', 'orange', 'pink', 'teal', 'red'];
16
+ export const FOLLOW_ON_COLOR = 'yellow';
17
+ export const UNSTACKED_COLOR = 'gray';
18
+ const isHexColor = (s) => /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(s);
19
+ /** A project slug from the URL or the file system: must be a plain directory name. */
20
+ export function projectDir(slug) {
21
+ if (!SAFE_NAME.test(slug))
22
+ throw new Error(`bad project name: ${slug}`);
23
+ return path.join(PROJECTS_DIR, slug);
24
+ }
25
+ export function listProjectSlugs() {
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true });
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ return entries
34
+ .filter((e) => e.isDirectory() && SAFE_NAME.test(e.name) && fs.existsSync(path.join(PROJECTS_DIR, e.name, 'project.yaml')))
35
+ .map((e) => e.name)
36
+ .sort();
37
+ }
38
+ /** A YAML flag: true/false, or "yes"/"no" (and "true"/"false") as strings, any case. Anything else: undefined. */
39
+ export function flag(v) {
40
+ if (typeof v === 'boolean')
41
+ return v;
42
+ if (typeof v !== 'string')
43
+ return undefined;
44
+ const t = v.trim().toLowerCase();
45
+ return t === 'yes' || t === 'true' ? true : t === 'no' || t === 'false' ? false : undefined;
46
+ }
47
+ const text = (v) => (v === null || v === undefined ? undefined : typeof v === 'object' ? undefined : String(v));
48
+ /** Parse a project.yaml. Never throws: problems are reported as `error` (whole file) or repo `warnings`. */
49
+ export function parseProject(slug, dir, raw) {
50
+ const file = path.join(dir, 'project.yaml');
51
+ const out = { slug, dir, file, title: slug, description: null, repos: [] };
52
+ let doc;
53
+ try {
54
+ doc = YAML.parse(raw) ?? {};
55
+ }
56
+ catch (e) {
57
+ out.error = `invalid YAML in ${file}: ${e.message}`;
58
+ return out;
59
+ }
60
+ if (typeof doc !== 'object' || Array.isArray(doc)) {
61
+ out.error = `${file}: the top level must be a mapping (title:, repos: …)`;
62
+ return out;
63
+ }
64
+ out.title = text(doc.title)?.trim() || slug;
65
+ out.description = text(doc.description) ?? null;
66
+ const list = Array.isArray(doc.repos) ? doc.repos : [];
67
+ if (doc.repos != null && !Array.isArray(doc.repos))
68
+ out.error = `${file}: \`repos\` must be a list`;
69
+ const used = new Set();
70
+ for (const [index, r] of list.entries()) {
71
+ if (!r || typeof r !== 'object' || typeof r.path !== 'string')
72
+ continue;
73
+ const repoPath = path.resolve(expandHome(r.path));
74
+ const wanted = safeName(text(r.name) ?? path.basename(repoPath), 'repo');
75
+ let name = wanted;
76
+ for (let n = 2; used.has(name); n++)
77
+ name = `${wanted}-${n}`;
78
+ used.add(name);
79
+ const warnings = [];
80
+ // the suffix depends on the order of `repos`, and the name is the reviews/ and prs/ directory: reordering
81
+ // or removing a repo would silently switch which comments a repo shows
82
+ if (name !== wanted)
83
+ warnings.push(`another repo is already named "${wanted}", so this one is "${name}" (its reviews/ and prs/ directories use that name; reordering the repos would change it). Give it a unique \`name\`.`);
84
+ const stacks = [];
85
+ if (r.stacks != null && !Array.isArray(r.stacks))
86
+ warnings.push('`stacks` must be a list; ignored');
87
+ for (const [i, s] of (Array.isArray(r.stacks) ? r.stacks : []).entries()) {
88
+ const spec = s && typeof s === 'object' ? s : { title: text(s) };
89
+ const title = text(spec.title)?.trim() || `Stack ${i + 1}`;
90
+ let followOn = false;
91
+ if (spec.follow_on != null) {
92
+ const f = flag(spec.follow_on);
93
+ if (f === undefined)
94
+ warnings.push(`stack "${title}": follow_on must be true or false (or yes / no), not ${JSON.stringify(spec.follow_on)}; ignored`);
95
+ else
96
+ followOn = f;
97
+ }
98
+ // a follow-on stack is always yellow (its `color:` is ignored); a regular one never is
99
+ let color = followOn ? FOLLOW_ON_COLOR : PALETTE[i % PALETTE.length];
100
+ const c = text(spec.color)?.trim();
101
+ if (c && !followOn) {
102
+ if (PALETTE.includes(c.toLowerCase()))
103
+ color = c.toLowerCase();
104
+ else if (c.toLowerCase() === FOLLOW_ON_COLOR)
105
+ warnings.push(`stack "${title}": yellow is reserved for follow-on stacks; using ${color} (pick another colour, or mark it follow_on: true)`);
106
+ else if (isHexColor(c))
107
+ color = c;
108
+ else
109
+ warnings.push(`stack "${title}": unknown colour '${c}' (use ${PALETTE.join(', ')} or #hex)`);
110
+ }
111
+ const startsAt = text(spec.starts_at)?.trim();
112
+ const description = text(spec.description)?.replace(/\r\n/g, '\n').trim();
113
+ stacks.push({ title, color, ...(startsAt ? { starts_at: startsAt } : {}), ...(description ? { description } : {}), ...(followOn ? { follow_on: true } : {}) });
114
+ }
115
+ let landed = [];
116
+ if (Array.isArray(r.landed))
117
+ landed = r.landed;
118
+ else if (r.landed != null)
119
+ warnings.push('`landed` must be a list of prs file keys (sha prefixes); ignored');
120
+ const ghStack = Number(text(r.github_stack)?.replace(/^#/, ''));
121
+ out.repos.push({
122
+ index,
123
+ name,
124
+ path: repoPath,
125
+ branch: text(r.branch),
126
+ to: text(r.to),
127
+ base: text(r.base),
128
+ from: text(r.from),
129
+ stacks,
130
+ warnings,
131
+ landed,
132
+ ...(Number.isSafeInteger(ghStack) && ghStack > 0 ? { github_stack: ghStack } : {}),
133
+ });
134
+ }
135
+ return out;
136
+ }
137
+ export function loadProjectConfig(slug) {
138
+ const dir = projectDir(slug);
139
+ let raw;
140
+ try {
141
+ raw = fs.readFileSync(path.join(dir, 'project.yaml'), 'utf8');
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ return parseProject(slug, dir, raw);
147
+ }
148
+ /**
149
+ * Split a repo's commits (oldest first) into consecutive stacks. Each declared stack starts at the commit its
150
+ * `starts_at` selects and runs up to the next stack's start. Commits before the first start form an implicit
151
+ * "Unstacked" group. A selector that matches nothing, or only commits at or before the previous stack's start
152
+ * (out of order), gives a warning and an empty stack; it never throws.
153
+ */
154
+ export function partitionStacks(commits, specs, defaultTitle = 'Stack') {
155
+ const warnings = [];
156
+ let unmatched = false;
157
+ if (!specs.length) {
158
+ return { stacks: [{ index: 0, number: null, title: defaultTitle, color: PALETTE[0], implicit: 'branch', shas: commits.map((c) => c.sha) }], warnings };
159
+ }
160
+ // nothing to place (e.g. the branch has no commits yet): empty stacks, no warnings
161
+ const declared = (s, i, index, shas) => ({ index, number: i + 1, title: s.title, color: s.follow_on ? FOLLOW_ON_COLOR : s.color, shas, ...(s.description ? { description: s.description } : {}), ...(s.follow_on ? { follow_on: true } : {}) });
162
+ if (!commits.length)
163
+ return { stacks: specs.map((s, i) => declared(s, i, i, [])), warnings };
164
+ const subjects = commits.map((c) => normalizeSubject(c.subject));
165
+ // a PR's title (override) is tried after the subjects: a landed multi-commit PR has no meaningful single subject
166
+ const titles = commits.map((c) => (c.title ? normalizeSubject(c.title) : ''));
167
+ const starts = [];
168
+ const specWarnings = [];
169
+ let last = -1;
170
+ for (const [i, spec] of specs.entries()) {
171
+ const label = `stack ${i + 1} "${spec.title}"`;
172
+ let cands;
173
+ const sel = spec.starts_at;
174
+ if (sel === undefined) {
175
+ if (i === 0)
176
+ cands = [0];
177
+ else {
178
+ specWarnings.push(`${label} has no starts_at, so it is empty`);
179
+ starts.push(null);
180
+ continue;
181
+ }
182
+ }
183
+ else {
184
+ cands = [];
185
+ if (/^[0-9a-f]{7,40}$/i.test(sel))
186
+ cands = commits.flatMap((c, j) => (c.sha.startsWith(sel.toLowerCase()) ? [j] : []));
187
+ if (!cands.length) {
188
+ const n = normalizeSubject(sel);
189
+ cands = subjects.flatMap((s, j) => (s === n ? [j] : []));
190
+ if (!cands.length)
191
+ cands = titles.flatMap((t, j) => (t && t === n ? [j] : []));
192
+ }
193
+ }
194
+ if (!cands.length) {
195
+ unmatched = true;
196
+ specWarnings.push(`${label}: starts_at "${sel}" doesn't match any commit (sha prefix, subject or PR title), so it is empty`);
197
+ starts.push(null);
198
+ continue;
199
+ }
200
+ const pick = cands.find((j) => j > last);
201
+ if (pick === undefined) {
202
+ specWarnings.push(`${label}: starts_at "${sel}" is at or before the previous stack's start (out of order), so it is empty`);
203
+ starts.push(null);
204
+ continue;
205
+ }
206
+ starts.push(pick);
207
+ specWarnings.push(undefined);
208
+ last = pick;
209
+ }
210
+ // follow-on stacks are parked at the end: a regular stack after one would be stacked on held-back work
211
+ const firstFollowOn = specs.findIndex((s) => s.follow_on);
212
+ if (firstFollowOn >= 0)
213
+ for (const [i, spec] of specs.entries()) {
214
+ if (i <= firstFollowOn || spec.follow_on)
215
+ continue;
216
+ const w = `stack ${i + 1} "${spec.title}" comes after follow-on stack ${firstFollowOn + 1} "${specs[firstFollowOn].title}" but isn't a follow-on itself: its PRs would be stacked on held-back work (move follow-on stacks to the end, or mark this one follow_on: true)`;
217
+ specWarnings[i] = specWarnings[i] ? `${specWarnings[i]}\n${w}` : w;
218
+ }
219
+ const valid = starts.map((s, i) => ({ s, i })).filter((x) => x.s !== null);
220
+ const groups = [];
221
+ const firstStart = valid.length ? valid[0].s : commits.length;
222
+ if (firstStart > 0) {
223
+ groups.push({ index: 0, number: null, title: 'Unstacked', color: UNSTACKED_COLOR, implicit: 'unstacked', shas: commits.slice(0, firstStart).map((c) => c.sha) });
224
+ }
225
+ for (const [i, spec] of specs.entries()) {
226
+ const s = starts[i];
227
+ let shas = [];
228
+ if (s !== null) {
229
+ const k = valid.findIndex((v) => v.i === i);
230
+ const end = valid[k + 1]?.s ?? commits.length;
231
+ shas = commits.slice(s, end).map((c) => c.sha);
232
+ }
233
+ const g = declared(spec, i, groups.length, shas);
234
+ if (specWarnings[i]) {
235
+ g.warning = specWarnings[i];
236
+ warnings.push(...specWarnings[i].split('\n'));
237
+ }
238
+ groups.push(g);
239
+ }
240
+ return { stacks: groups, warnings, ...(unmatched ? { unmatched } : {}) };
241
+ }
242
+ export function repoDirs(project, repo) {
243
+ if (!SAFE_NAME.test(repo))
244
+ throw new Error(`bad repo name: ${repo}`);
245
+ return { reviewsDir: path.join(project.dir, 'reviews', repo), prsDir: path.join(project.dir, 'prs', repo) };
246
+ }
247
+ export async function loadProject(slug) {
248
+ const cfg = loadProjectConfig(slug);
249
+ if (!cfg)
250
+ return null;
251
+ const repos = await Promise.all(cfg.repos.map(async (rc) => {
252
+ const dirs = repoDirs(cfg, rc.name);
253
+ // with landed PRs, a missing / deleted branch or an empty live range is fine: they still show
254
+ const info = await loadRepo(rc, { optional: rc.landed.length > 0 });
255
+ const extra = info.warning ? [info.warning] : [];
256
+ let titles = new Map();
257
+ if (rc.landed.length && !info.error) {
258
+ const landed = await loadLanded(rc.path, dirs.prsDir, rc.landed);
259
+ extra.push(...landed.warnings);
260
+ titles = landed.titles;
261
+ // a frozen PR whose commit is still in the live range (frozen before the rebase) is shown once, as landed
262
+ const keys = new Set(landed.commits.map((c) => c.sha));
263
+ info.commits = [...landed.commits, ...info.commits.filter((c) => !keys.has(c.sha))];
264
+ }
265
+ const defaultTitle = rc.branch || rangeEnd(rc) || rc.name;
266
+ let part = partitionStacks(info.commits.map((c) => ({ sha: c.sha, subject: c.subject, title: titles.get(c.sha) })), rc.stacks, defaultTitle);
267
+ // a selector that's no sha or subject may be a live PR's title: only then read the prs files for titles
268
+ if (part.unmatched && info.commits.some((c) => !c.landed)) {
269
+ const prs = await loadPrs(dirs.prsDir, liveCommits(rc, info), inspectOrphan(rc, info)).catch(() => new Map());
270
+ part = partitionStacks(info.commits.map((c) => ({ sha: c.sha, subject: c.subject, title: titles.get(c.sha) ?? prs.get(c.sha)?.override.title })), rc.stacks, defaultTitle);
271
+ }
272
+ const { stacks, warnings } = part;
273
+ const stackOf = new Map();
274
+ for (const g of stacks)
275
+ for (const sha of g.shas)
276
+ stackOf.set(sha, g.index);
277
+ return { cfg: rc, info, stacks, warnings: [...rc.warnings, ...extra, ...warnings], stackOf, ...dirs };
278
+ }));
279
+ return { cfg, repos };
280
+ }
281
+ /**
282
+ * Archived: everything in the project has landed. Some repo has landed PRs, and no repo has a live (unmerged) commit
283
+ * or an error. The project switcher lists these in a separate, collapsed section.
284
+ */
285
+ export function isArchived(p) {
286
+ if (p.cfg.error || !p.repos.length)
287
+ return false;
288
+ return p.repos.some((r) => r.info.commits.some((c) => c.landed)) && p.repos.every((r) => !r.info.error && r.info.commits.every((c) => c.landed));
289
+ }
290
+ /** Newest mtime of anything in the project directory (project.yaml, prs/, reviews/), for "most recent". */
291
+ export function projectModified(dir) {
292
+ let max = 0;
293
+ const walk = (d, depth) => {
294
+ let entries;
295
+ try {
296
+ entries = fs.readdirSync(d, { withFileTypes: true });
297
+ }
298
+ catch {
299
+ return;
300
+ }
301
+ for (const e of entries) {
302
+ const p = path.join(d, e.name);
303
+ if (e.isDirectory()) {
304
+ if (depth < 3)
305
+ walk(p, depth + 1);
306
+ }
307
+ else {
308
+ try {
309
+ max = Math.max(max, fs.statSync(p).mtimeMs);
310
+ }
311
+ catch {
312
+ /* gone */
313
+ }
314
+ }
315
+ }
316
+ };
317
+ walk(dir, 0);
318
+ return max;
319
+ }
@@ -0,0 +1,314 @@
1
+ // PR title/description overrides: projects/<project>/prs/<repo>/<full-sha>.yaml
2
+ // A commit's PR title and description default to its subject and body. Changing them must never touch the
3
+ // reviewed repo, so overrides live here. Like review files they follow rebases (patch-id, Change-Id, subject;
4
+ // only from files whose commit has left the range), tolerate hand edits, and are written atomically.
5
+ import fs from 'node:fs';
6
+ import YAML from 'yaml';
7
+ import { atomicWrite, renameToBak } from './safefs.js';
8
+ import { normBody, normTitle } from '../shared/prText.js';
9
+ import { storedStatus } from '../shared/readiness.js';
10
+ import { ReviewConflictError, matchOrphans, readRaw, reviewPath, str, withLock } from './reviews.js';
11
+ export const prPath = reviewPath; // same `<dir>/<40-hex>.yaml` rule
12
+ function parse(raw) {
13
+ let doc;
14
+ try {
15
+ doc = YAML.parse(raw);
16
+ }
17
+ catch (e) {
18
+ return { doc: null, error: e.message };
19
+ }
20
+ if (doc === null || doc === undefined)
21
+ return { doc: {} };
22
+ if (typeof doc !== 'object' || Array.isArray(doc))
23
+ return { doc: null, error: 'the top level must be a mapping (title:, body: …)' };
24
+ return { doc };
25
+ }
26
+ /** What the file overrides. A blank title doesn't count; `description:` is accepted as an alias of `body:`. */
27
+ export function overrideOf(doc) {
28
+ if (!doc)
29
+ return {};
30
+ const o = {};
31
+ const title = normTitle(str(doc.title) ?? '');
32
+ if (title)
33
+ o.title = title;
34
+ const body = str(doc.body) ?? str(doc.description);
35
+ if (body !== undefined)
36
+ o.body = normBody(body);
37
+ const branch = branchName(doc.branch);
38
+ if (branch)
39
+ o.branch = branch;
40
+ const gh = githubOf(doc.github);
41
+ if (gh)
42
+ o.github = gh;
43
+ const status = storedStatus(doc.status);
44
+ if (status)
45
+ o.status = status;
46
+ const landed = landedOf(doc.landed);
47
+ if (landed)
48
+ o.landed = landed;
49
+ return o;
50
+ }
51
+ const SHA = /^[0-9a-f]{40}$/;
52
+ const METHODS = ['merge', 'squash', 'rebase'];
53
+ /**
54
+ * The `landed:` block, tolerantly: `{head, base, commit, method, at}`, shas as full 40-hex (any case; anything else is
55
+ * ignored), `method` merge / squash / rebase (any case). Usable once it has a `head` or a `commit`; otherwise null.
56
+ */
57
+ export function landedOf(v) {
58
+ if (!v || typeof v !== 'object' || Array.isArray(v))
59
+ return null;
60
+ const o = v;
61
+ const sha = (x) => {
62
+ const s = str(x)?.trim().toLowerCase();
63
+ return s && SHA.test(s) ? s : undefined;
64
+ };
65
+ const l = {};
66
+ const head = sha(o.head);
67
+ if (head)
68
+ l.head = head;
69
+ const base = sha(o.base);
70
+ if (base)
71
+ l.base = base;
72
+ const commit = sha(o.commit) ?? sha(o.merge_commit);
73
+ if (commit)
74
+ l.commit = commit;
75
+ const m = str(o.method)?.trim().toLowerCase();
76
+ if (m && METHODS.includes(m))
77
+ l.method = m;
78
+ const at = str(o.at)?.trim();
79
+ if (at)
80
+ l.at = at;
81
+ return l.head || l.commit ? l : null;
82
+ }
83
+ /** A hand-written branch name: trimmed, `refs/heads/` dropped. Blank or not a string: none. */
84
+ export function branchName(v) {
85
+ const s = typeof v === 'string' ? v.trim().replace(/^refs\/heads\//, '') : undefined;
86
+ return s || undefined;
87
+ }
88
+ const PR_URL = /^https?:\/\/\S+\/pull\/(\d+)\/?(?:[#?]\S*)?$/;
89
+ function prNumber(v) {
90
+ const s = str(v)?.trim().replace(/^#/, '');
91
+ if (!s || !/^\d{1,9}$/.test(s))
92
+ return undefined;
93
+ const n = Number(s);
94
+ return n > 0 ? n : undefined;
95
+ }
96
+ /**
97
+ * The `github:` field, tolerantly: `{number, url, branch?, base?, created?}` with the number as a number or a
98
+ * string (`"123"`, `"#123"`), or derived from a `.../pull/123` url; or just a number or a PR url on its own.
99
+ * Anything without a usable PR number is ignored (null).
100
+ */
101
+ export function githubOf(v) {
102
+ if (v === null || v === undefined)
103
+ return null;
104
+ if (typeof v !== 'object' || Array.isArray(v)) {
105
+ const s = str(v)?.trim() ?? '';
106
+ const m = PR_URL.exec(s);
107
+ if (m)
108
+ return { number: Number(m[1]), url: s };
109
+ const n = prNumber(s);
110
+ return n ? { number: n, url: '' } : null;
111
+ }
112
+ const o = v;
113
+ const url = str(o.url)?.trim() ?? '';
114
+ const number = prNumber(o.number) ?? (PR_URL.exec(url) ? Number(PR_URL.exec(url)[1]) : undefined);
115
+ if (!number)
116
+ return null;
117
+ const gh = { number, url };
118
+ const branch = branchName(o.branch);
119
+ if (branch)
120
+ gh.branch = branch;
121
+ const base = branchName(o.base);
122
+ if (base)
123
+ gh.base = base;
124
+ const created = str(o.created)?.trim();
125
+ if (created)
126
+ gh.created = created;
127
+ const stack = prNumber(o.stack);
128
+ if (stack)
129
+ gh.stack = stack;
130
+ const stackedOn = branchName(o.stacked_on);
131
+ if (stackedOn)
132
+ gh.stacked_on = stackedOn;
133
+ return gh;
134
+ }
135
+ export function readPrFile(file) {
136
+ const raw = fs.readFileSync(file, 'utf8');
137
+ const { doc, error } = parse(raw);
138
+ return { override: overrideOf(doc), doc, file, raw, ...(error ? { parseError: error } : {}) };
139
+ }
140
+ const none = () => ({ override: {}, doc: null, file: null, raw: null });
141
+ export async function loadPr(ctx) {
142
+ const file = prPath(ctx.dir, ctx.sha);
143
+ if (fs.existsSync(file))
144
+ return readPrFile(file);
145
+ const live = ctx.liveCommits ?? [{ sha: ctx.sha, subject: ctx.subject, changeId: ctx.changeId, getPatchId: ctx.getPatchId }];
146
+ const m = (await matchOrphans(ctx.dir, live, { inspect: ctx.inspectOrphan })).get(ctx.sha);
147
+ if (m) {
148
+ const l = readPrFile(m.orphan.file);
149
+ if (!l.parseError)
150
+ return { ...l, matchedFrom: m.orphan.sha, matchedBy: m.by };
151
+ }
152
+ return none();
153
+ }
154
+ /** Overrides for every commit of a repo at once (one directory scan), for the sidebar. */
155
+ export async function loadPrs(dir, live, inspect) {
156
+ const out = new Map();
157
+ if (!fs.existsSync(dir))
158
+ return out;
159
+ const matched = await matchOrphans(dir, live, { inspect });
160
+ for (const c of live) {
161
+ const file = prPath(dir, c.sha);
162
+ try {
163
+ if (fs.existsSync(file))
164
+ out.set(c.sha, readPrFile(file));
165
+ else {
166
+ const m = matched.get(c.sha);
167
+ if (m) {
168
+ const l = readPrFile(m.orphan.file);
169
+ if (!l.parseError)
170
+ out.set(c.sha, { ...l, matchedFrom: m.orphan.sha, matchedBy: m.by });
171
+ }
172
+ }
173
+ }
174
+ catch {
175
+ /* unreadable: no override */
176
+ }
177
+ }
178
+ return out;
179
+ }
180
+ function toYaml(doc) {
181
+ const { commit, patch_id, change_id, subject, previous_commits, matched_by, title, body, status, branch, github, landed, updated, ...rest } = doc;
182
+ const o = { commit, patch_id: patch_id ?? null };
183
+ if (change_id)
184
+ o.change_id = change_id;
185
+ o.subject = subject;
186
+ if (Array.isArray(previous_commits) && previous_commits.length)
187
+ o.previous_commits = previous_commits;
188
+ if (matched_by)
189
+ o.matched_by = matched_by;
190
+ if (title !== undefined)
191
+ o.title = title;
192
+ if (body !== undefined)
193
+ o.body = body;
194
+ if (status !== undefined)
195
+ o.status = status;
196
+ if (branch !== undefined)
197
+ o.branch = branch;
198
+ if (github !== undefined)
199
+ o.github = github;
200
+ if (landed !== undefined)
201
+ o.landed = landed;
202
+ if (updated !== undefined)
203
+ o.updated = updated;
204
+ Object.assign(o, rest);
205
+ const d = new YAML.Document(o);
206
+ YAML.visit(d, {
207
+ Pair(_, pair) {
208
+ if (YAML.isScalar(pair.key) && pair.key.value === 'body' && YAML.isScalar(pair.value) && typeof pair.value.value === 'string')
209
+ pair.value.type = 'BLOCK_LITERAL';
210
+ },
211
+ });
212
+ return d.toString({ lineWidth: 0, blockQuote: 'literal' });
213
+ }
214
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
215
+ const MAX_ATTEMPTS = 6;
216
+ export function writePr(ctx, change, now = new Date()) {
217
+ const target = prPath(ctx.dir, ctx.sha);
218
+ return withLock(target, async () => {
219
+ for (let attempt = 1;; attempt++) {
220
+ const loaded = await loadPr(ctx);
221
+ if (loaded.parseError) {
222
+ if (attempt < MAX_ATTEMPTS) {
223
+ await sleep(40 * attempt);
224
+ continue;
225
+ }
226
+ throw new ReviewConflictError(`refusing to overwrite ${loaded.file}: it is not valid YAML (${loaded.parseError}). Fix the file by hand.`);
227
+ }
228
+ const doc = { ...(loaded.doc ?? {}) };
229
+ if (change.title !== undefined) {
230
+ if (change.title === null || !change.title.trim())
231
+ delete doc.title;
232
+ else
233
+ doc.title = change.title.replace(/\s+/g, ' ').trim();
234
+ }
235
+ if (change.body !== undefined) {
236
+ delete doc.description; // the alias, if an agent used it
237
+ if (change.body === null)
238
+ delete doc.body;
239
+ else
240
+ doc.body = change.body.replace(/\r\n/g, '\n').trimEnd() + '\n';
241
+ }
242
+ if (change.branch !== undefined) {
243
+ const b = change.branch === null ? undefined : branchName(change.branch);
244
+ if (b)
245
+ doc.branch = b;
246
+ else
247
+ delete doc.branch;
248
+ }
249
+ if (change.github !== undefined) {
250
+ if (change.github === null)
251
+ delete doc.github;
252
+ else {
253
+ const { number, url, branch, base, created, stack, stacked_on } = change.github;
254
+ doc.github = { number, url, ...(branch ? { branch } : {}), ...(base ? { base } : {}), ...(created ? { created } : {}), ...(stack ? { stack } : {}), ...(stacked_on ? { stacked_on } : {}) };
255
+ }
256
+ }
257
+ if (change.status !== undefined) {
258
+ if (change.status === 'ready')
259
+ doc.status = 'ready';
260
+ else
261
+ delete doc.status;
262
+ }
263
+ doc.commit = ctx.sha;
264
+ doc.patch_id = (!loaded.matchedFrom && str(doc.patch_id)) || (await ctx.getPatchId());
265
+ if (ctx.changeId)
266
+ doc.change_id = ctx.changeId;
267
+ doc.subject = ctx.subject;
268
+ if (loaded.matchedFrom) {
269
+ const prev = Array.isArray(doc.previous_commits) ? doc.previous_commits.map(str).filter(Boolean) : [];
270
+ doc.previous_commits = [...new Set([...prev, loaded.matchedFrom])];
271
+ doc.matched_by = loaded.matchedBy;
272
+ }
273
+ doc.updated = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
274
+ const text = toYaml(doc);
275
+ const changed = readRaw(loaded.file) !== loaded.raw || (loaded.file !== target && fs.existsSync(target));
276
+ if (changed) {
277
+ if (attempt < MAX_ATTEMPTS)
278
+ continue;
279
+ throw new ReviewConflictError(`${loaded.file ?? target} keeps changing on disk; not saving over it. Try again.`);
280
+ }
281
+ atomicWrite(target, text);
282
+ if (loaded.matchedFrom && loaded.file && loaded.file !== target) {
283
+ try {
284
+ renameToBak(loaded.file, `superseded-by-${ctx.sha.slice(0, 8)}`); // never over an existing file
285
+ }
286
+ catch {
287
+ /* ignore */
288
+ }
289
+ }
290
+ return readPrFile(target);
291
+ }
292
+ });
293
+ }
294
+ /**
295
+ * Whether the override's title / body actually changes the commit's. One that says the same as the commit (after the
296
+ * normalising every stored title / body gets) isn't an edit: the UI's own save drops it, and a hand-written copy of
297
+ * the commit message reads as unedited too. The file is left alone.
298
+ */
299
+ export const titleEdited = (o, subject) => o?.title !== undefined && o.title !== normTitle(subject);
300
+ export const bodyEdited = (o, body) => o?.body !== undefined && o.body !== normBody(body ?? '');
301
+ /** The PR as shown: the override where it changes something, else the commit message. */
302
+ export function prView(l, commit) {
303
+ const o = l?.override;
304
+ const t = titleEdited(o, commit.subject);
305
+ const b = bodyEdited(o, commit.body);
306
+ return {
307
+ title: t ? o.title : commit.subject,
308
+ body: b ? o.body : (commit.body ?? ''),
309
+ title_source: (t ? 'override' : 'commit'),
310
+ body_source: (b ? 'override' : 'commit'),
311
+ commit_subject: commit.subject,
312
+ commit_body: commit.body ?? '',
313
+ };
314
+ }
@@ -0,0 +1,33 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import os from 'node:os';
3
+ export function reviewerId(name) {
4
+ const first = name.trim().split(/\s+/)[0] ?? '';
5
+ const id = first.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32);
6
+ // `claude` is the agent's id: a reviewer can't take it
7
+ return id && id !== 'claude' ? id : 'reviewer';
8
+ }
9
+ export function resolveReviewer(sources) {
10
+ const name = [sources.env, sources.gitName, sources.user].map((s) => s?.trim()).find((s) => s) || 'reviewer';
11
+ return { id: reviewerId(name), name };
12
+ }
13
+ function gitUserName() {
14
+ try {
15
+ return execFileSync('git', ['config', '--get', 'user.name'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 });
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ function osUser() {
22
+ try {
23
+ return process.env.USER || os.userInfo().username;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ export const REVIEWER = resolveReviewer({
30
+ env: process.env.LOCAL_REVIEW_USER,
31
+ gitName: process.env.LOCAL_REVIEW_USER ? undefined : gitUserName(),
32
+ user: osUser(),
33
+ });