@plebeiantech/lat.md 0.12.3-fork.13 → 0.12.3-fork.14

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 (274) hide show
  1. package/README.md +24 -0
  2. package/dist/src/cli/check-context.d.ts +56 -0
  3. package/dist/src/cli/check-context.js +215 -0
  4. package/dist/src/cli/check-coverage.js +9 -5
  5. package/dist/src/cli/check-frontmatter.js +1 -1
  6. package/dist/src/cli/check-mode.js +1 -1
  7. package/dist/src/cli/check-status.js +1 -1
  8. package/dist/src/cli/check.d.ts +19 -12
  9. package/dist/src/cli/check.js +259 -206
  10. package/dist/src/cli/context.js +1 -1
  11. package/dist/src/cli/expand.d.ts +1 -2
  12. package/dist/src/cli/expand.js +52 -5
  13. package/dist/src/cli/external.d.ts +11 -0
  14. package/dist/src/cli/external.js +185 -0
  15. package/dist/src/cli/fork-instructions.d.ts +1 -0
  16. package/dist/src/cli/fork-instructions.js +3 -2
  17. package/dist/src/cli/hook.d.ts +26 -0
  18. package/dist/src/cli/hook.js +112 -40
  19. package/dist/src/cli/index.js +55 -3
  20. package/dist/src/cli/init.d.ts +1 -0
  21. package/dist/src/cli/init.js +40 -2
  22. package/dist/src/cli/locate.js +4 -3
  23. package/dist/src/cli/refs.d.ts +2 -1
  24. package/dist/src/cli/refs.js +88 -43
  25. package/dist/src/cli/reindex.js +2 -1
  26. package/dist/src/cli/search.d.ts +12 -3
  27. package/dist/src/cli/search.js +45 -49
  28. package/dist/src/cli/section.d.ts +3 -1
  29. package/dist/src/cli/section.js +114 -38
  30. package/dist/src/code-refs.d.ts +17 -5
  31. package/dist/src/code-refs.js +246 -65
  32. package/dist/src/context.d.ts +3 -0
  33. package/dist/src/document-formats.d.ts +7 -0
  34. package/dist/src/document-formats.js +39 -0
  35. package/dist/src/extensions/alert-marker.d.ts +25 -0
  36. package/dist/src/extensions/alert-marker.js +55 -0
  37. package/dist/src/external-documents.d.ts +56 -0
  38. package/dist/src/external-documents.js +404 -0
  39. package/dist/src/external-sources.d.ts +136 -0
  40. package/dist/src/external-sources.js +1098 -0
  41. package/dist/src/fork/antigravity-hook.d.ts +47 -0
  42. package/dist/src/fork/antigravity-hook.js +424 -0
  43. package/dist/src/fork/antigravity-init.d.ts +17 -0
  44. package/dist/src/fork/antigravity-init.js +124 -0
  45. package/dist/src/fork/frontmatter-fields.d.ts +36 -0
  46. package/dist/src/fork/frontmatter-fields.js +23 -0
  47. package/dist/src/format.d.ts +5 -2
  48. package/dist/src/format.js +8 -3
  49. package/dist/src/graph-export.js +1 -1
  50. package/dist/src/lattice-model.d.ts +77 -0
  51. package/dist/src/lattice-model.js +464 -0
  52. package/dist/src/lattice.d.ts +9 -104
  53. package/dist/src/lattice.js +69 -516
  54. package/dist/src/markdown-analysis-cache.d.ts +20 -0
  55. package/dist/src/markdown-analysis-cache.js +139 -0
  56. package/dist/src/markdown-analysis-worker.d.ts +16 -0
  57. package/dist/src/markdown-analysis-worker.js +33 -0
  58. package/dist/src/markdown-analysis.d.ts +50 -0
  59. package/dist/src/markdown-analysis.js +139 -0
  60. package/dist/src/markdown-analyzer-loader.d.ts +4 -0
  61. package/dist/src/markdown-analyzer-loader.js +13 -0
  62. package/dist/src/markdown-validation.d.ts +25 -0
  63. package/dist/src/markdown-validation.js +104 -0
  64. package/dist/src/mcp/server.js +27 -11
  65. package/dist/src/parser-cache.d.ts +17 -0
  66. package/dist/src/parser-cache.js +65 -0
  67. package/dist/src/parser-import.d.ts +8 -0
  68. package/dist/src/parser-import.js +1 -0
  69. package/dist/src/parser.js +80 -5
  70. package/dist/src/path.d.ts +2 -0
  71. package/dist/src/path.js +4 -0
  72. package/dist/src/profiler.d.ts +16 -0
  73. package/dist/src/profiler.js +81 -0
  74. package/dist/src/project-analysis.d.ts +59 -0
  75. package/dist/src/project-analysis.js +233 -0
  76. package/dist/src/project-discovery.d.ts +6 -0
  77. package/dist/src/project-discovery.js +30 -0
  78. package/dist/src/repository-path.d.ts +11 -0
  79. package/dist/src/repository-path.js +52 -0
  80. package/dist/src/search/db.d.ts +1 -1
  81. package/dist/src/search/db.js +2 -2
  82. package/dist/src/search/embedder.d.ts +2 -1
  83. package/dist/src/search/embedder.js +6 -5
  84. package/dist/src/search/index.d.ts +2 -1
  85. package/dist/src/search/index.js +10 -8
  86. package/dist/src/search/query.d.ts +20 -0
  87. package/dist/src/search/query.js +67 -0
  88. package/dist/src/search/search.d.ts +3 -1
  89. package/dist/src/search/search.js +5 -2
  90. package/dist/src/source-formats.d.ts +7 -0
  91. package/dist/src/source-formats.js +26 -0
  92. package/dist/src/source-parser.d.ts +44 -4
  93. package/dist/src/source-parser.js +494 -57
  94. package/dist/src/view/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  95. package/dist/src/view/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  96. package/dist/src/view/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  97. package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  98. package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  99. package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  100. package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  101. package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  102. package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  103. package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  104. package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  105. package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  106. package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  107. package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  108. package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  109. package/dist/src/view/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  110. package/dist/src/view/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  111. package/dist/src/view/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  112. package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  113. package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  114. package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  115. package/dist/src/view/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  116. package/dist/src/view/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  117. package/dist/src/view/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  118. package/dist/src/view/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  119. package/dist/src/view/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  120. package/dist/src/view/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  121. package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  122. package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  123. package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  124. package/dist/src/view/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  125. package/dist/src/view/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  126. package/dist/src/view/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  127. package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  128. package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  129. package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  130. package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  131. package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  132. package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  133. package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  134. package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  135. package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  136. package/dist/src/view/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  137. package/dist/src/view/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  138. package/dist/src/view/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  139. package/dist/src/view/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  140. package/dist/src/view/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  141. package/dist/src/view/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  142. package/dist/src/view/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  143. package/dist/src/view/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  144. package/dist/src/view/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  145. package/dist/src/view/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  146. package/dist/src/view/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  147. package/dist/src/view/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  148. package/dist/src/view/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  149. package/dist/src/view/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  150. package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  151. package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  152. package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  153. package/dist/src/view/client/assets/MarkdownEditor-B7quqPb8.js +28 -0
  154. package/dist/src/view/client/assets/OrbitControls-DwcpQNja.js +1 -0
  155. package/dist/src/view/client/assets/STLLoader-CLWTKt5i.js +1 -0
  156. package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +1 -0
  157. package/dist/src/view/client/assets/arc-BnwR8Bu7.js +1 -0
  158. package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +36 -0
  159. package/dist/src/view/client/assets/blockDiagram-I7D4REHJ-DETl8e_H.js +129 -0
  160. package/dist/src/view/client/assets/c4Diagram-7LVT6UL2-DJRAT1Ve.js +38 -0
  161. package/dist/src/view/client/assets/channel-BwQbnWCN.js +1 -0
  162. package/dist/src/view/client/assets/chunk-2Q5K7J3B-C0TmOC1Q.js +1 -0
  163. package/dist/src/view/client/assets/chunk-5VM5RSS4-dfBOrYE6.js +15 -0
  164. package/dist/src/view/client/assets/chunk-F27PBJKO-DAnks99K.js +1 -0
  165. package/dist/src/view/client/assets/chunk-IMKFNOWR-CPjB07if.js +231 -0
  166. package/dist/src/view/client/assets/chunk-JWPE2WC7-VvU_gZI6.js +1 -0
  167. package/dist/src/view/client/assets/chunk-POPQ4Y6H-DDUbVF55.js +1 -0
  168. package/dist/src/view/client/assets/chunk-SVP7TREG-BHROkr6D.js +88 -0
  169. package/dist/src/view/client/assets/chunk-TICWLB2K-95PzGIOg.js +206 -0
  170. package/dist/src/view/client/assets/chunk-XXDRQBXY-MdmuLhIs.js +1 -0
  171. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +1 -0
  172. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +1 -0
  173. package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +1 -0
  174. package/dist/src/view/client/assets/cynefin-OW5HDTMX-CeA8IKWo.js +166 -0
  175. package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +62 -0
  176. package/dist/src/view/client/assets/cytoscape.esm-DzSz-X2X.js +321 -0
  177. package/dist/src/view/client/assets/dagre-GXQ25YYZ-DLQwTa3H.js +4 -0
  178. package/dist/src/view/client/assets/defaultLocale-DX6XiGOO.js +1 -0
  179. package/dist/src/view/client/assets/diagram-S7CK7UJ4-kbKdIyXG.js +30 -0
  180. package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +41 -0
  181. package/dist/src/view/client/assets/diagram-VSXAHHWV-EXJTU8qq.js +3 -0
  182. package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +24 -0
  183. package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +24 -0
  184. package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +1 -0
  185. package/dist/src/view/client/assets/erDiagram-RLTQ6QDP-C925J5A3.js +99 -0
  186. package/dist/src/view/client/assets/flowDiagram-HODETNUW-DzoCa-Nq.js +168 -0
  187. package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +292 -0
  188. package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +106 -0
  189. package/dist/src/view/client/assets/index-BnENo9bV.js +1 -0
  190. package/dist/src/view/client/assets/index-D0ajr_Ja.css +1 -0
  191. package/dist/src/view/client/assets/index-KZcnpXJH.js +321 -0
  192. package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +2 -0
  193. package/dist/src/view/client/assets/init-Gi6I4Gst.js +1 -0
  194. package/dist/src/view/client/assets/ishikawaDiagram-5VMMS53U-CW4mto2d.js +70 -0
  195. package/dist/src/view/client/assets/journeyDiagram-3NMN7TZE-8R4eacFW.js +139 -0
  196. package/dist/src/view/client/assets/kanban-definition-UXKFOSKX-Cy9ioXEJ.js +89 -0
  197. package/dist/src/view/client/assets/katex-HP8lGamR.js +257 -0
  198. package/dist/src/view/client/assets/layout-C0MFuhth.js +1 -0
  199. package/dist/src/view/client/assets/linear-Dv3N4dQR.js +1 -0
  200. package/dist/src/view/client/assets/logo-small-RR_KBOAT.svg +149 -0
  201. package/dist/src/view/client/assets/maplibre-gl-DnqZgfUS.js +800 -0
  202. package/dist/src/view/client/assets/mermaid.core-bds9A7e2.js +310 -0
  203. package/dist/src/view/client/assets/mindmap-definition-YA3MSWOX-DjmGW355.js +96 -0
  204. package/dist/src/view/client/assets/ordinal-Cboi1Yqb.js +1 -0
  205. package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +1 -0
  206. package/dist/src/view/client/assets/pieDiagram-E7YTZNPT-CCH2gGR-.js +39 -0
  207. package/dist/src/view/client/assets/quadrantDiagram-AXDQQJYC-CjocPJFz.js +7 -0
  208. package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +1 -0
  209. package/dist/src/view/client/assets/requirementDiagram-BXWQKSXE-Ct6AoXRr.js +84 -0
  210. package/dist/src/view/client/assets/sankeyDiagram-P5KCCOFB-CIpgGN_w.js +40 -0
  211. package/dist/src/view/client/assets/sequenceDiagram-WJ2MYXX4-DrmLFj6w.js +162 -0
  212. package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +1 -0
  213. package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +1 -0
  214. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +1 -0
  215. package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +1 -0
  216. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +8 -0
  217. package/dist/src/view/client/assets/three.module-PDSP0dbZ.js +4108 -0
  218. package/dist/src/view/client/assets/timeline-definition-24CTP7MA-BExPu6X5.js +120 -0
  219. package/dist/src/view/client/assets/vennDiagram-4TSXK5OY-PC1MU3si.js +34 -0
  220. package/dist/src/view/client/assets/wardleyDiagram-VM6X3IG4-DNWqU3fa.js +78 -0
  221. package/dist/src/view/client/assets/xychartDiagram-S5SC5T6Z-E1uA28_D.js +7 -0
  222. package/dist/src/view/client/index.html +2 -2
  223. package/dist/src/view/client-shell.d.ts +2 -0
  224. package/dist/src/view/client-shell.js +5 -0
  225. package/dist/src/view/diagnostics.d.ts +3 -2
  226. package/dist/src/view/diagnostics.js +49 -58
  227. package/dist/src/view/document-edit.d.ts +8 -0
  228. package/dist/src/view/document-edit.js +25 -0
  229. package/dist/src/view/document-route.d.ts +12 -0
  230. package/dist/src/view/document-route.js +93 -0
  231. package/dist/src/view/document-tree.d.ts +18 -0
  232. package/dist/src/view/document-tree.js +139 -0
  233. package/dist/src/view/external-document-tree.d.ts +3 -0
  234. package/dist/src/view/external-document-tree.js +674 -0
  235. package/dist/src/view/git-diff.js +187 -0
  236. package/dist/src/view/git.js +1 -1
  237. package/dist/src/view/graph.d.ts +3 -2
  238. package/dist/src/view/graph.js +94 -7
  239. package/dist/src/view/highlight.d.ts +10 -2
  240. package/dist/src/view/highlight.js +4540 -1360
  241. package/dist/src/view/markdown.d.ts +3 -2
  242. package/dist/src/view/markdown.js +214 -53
  243. package/dist/src/view/protocol.d.ts +59 -6
  244. package/dist/src/view/references.d.ts +10 -25
  245. package/dist/src/view/references.js +117 -107
  246. package/dist/src/view/repository.d.ts +13 -3
  247. package/dist/src/view/repository.js +234 -15
  248. package/dist/src/view/search.d.ts +1 -1
  249. package/dist/src/view/search.js +3 -7
  250. package/dist/src/view/server.d.ts +11 -2
  251. package/dist/src/view/server.js +212 -70
  252. package/dist/src/view/source-target.js +5 -8
  253. package/dist/src/view/static-build.d.ts +1 -0
  254. package/dist/src/view/static-build.js +245 -42
  255. package/dist/src/view/static-protocol.d.ts +19 -2
  256. package/dist/src/view/store.d.ts +16 -3
  257. package/dist/src/view/store.js +228 -36
  258. package/dist/src/view/table-of-contents.d.ts +2 -2
  259. package/dist/src/view/table-of-contents.js +2 -22
  260. package/dist/src/walk.d.ts +5 -3
  261. package/dist/src/walk.js +102 -8
  262. package/package.json +46 -8
  263. package/plugins/lat-md/README.md +61 -0
  264. package/plugins/lat-md/hooks.json +38 -0
  265. package/plugins/lat-md/plugin.json +9 -0
  266. package/plugins/lat-md/rules/AGENTS.md +118 -0
  267. package/plugins/lat-md/skills/lat-md/SKILL.md +178 -0
  268. package/plugins/lat-md/skills/lat-md-conventions/SKILL.md +182 -0
  269. package/templates/AGENTS.md +15 -3
  270. package/templates/cursor-rules.md +12 -3
  271. package/templates/skill/SKILL.md +22 -1
  272. package/dist/src/view/client/assets/index-C3iaZyYT.css +0 -1
  273. package/dist/src/view/client/assets/index-KWi67IMz.js +0 -334
  274. package/dist/src/view/client/assets/logo-CNF8zs47.svg +0 -182
@@ -0,0 +1,1098 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync, } from 'node:fs';
3
+ import { mkdir, readFile, readdir, rename, rm, writeFile, } from 'node:fs/promises';
4
+ import { request as httpsRequest } from 'node:https';
5
+ import { homedir, tmpdir } from 'node:os';
6
+ import { dirname, extname, isAbsolute, join, relative, resolve, } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ import { parse as parseYaml, parseDocument } from 'yaml';
9
+ import { z } from 'zod';
10
+ import { DOCUMENT_FILE_EXTENSIONS, isDocumentPath, } from './document-formats.js';
11
+ import { ExternalDocumentParserRuntime, analyzeExternalDocumentCached, findExternalDocumentSection, } from './external-documents.js';
12
+ import { SourceParserRuntime, analyzeSourceSymbols, } from './source-parser.js';
13
+ import { SOURCE_FILE_EXTENSIONS } from './source-formats.js';
14
+ const execFileAsync = promisify(execFile);
15
+ const HANDLE_RE = /^[a-z0-9][a-z0-9_-]*$/;
16
+ const COMMIT_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
17
+ const MAX_BYTES = 5 * 1024 * 1024;
18
+ const HTTP_TIMEOUT_MS = 15_000;
19
+ const MAX_REDIRECTS = 5;
20
+ /** Version of the external cache metadata and on-disk provider layouts. */
21
+ export const EXTERNAL_SOURCES_SCHEMA_VER = 1;
22
+ /** File types external sources can parse and resolve. */
23
+ const EXTERNAL_FILE_EXTENSIONS = new Set([
24
+ ...DOCUMENT_FILE_EXTENSIONS,
25
+ ...SOURCE_FILE_EXTENSIONS,
26
+ ]);
27
+ const canonicalSourceSchema = z
28
+ .strictObject({
29
+ repo: z.string(),
30
+ commit: z.string(),
31
+ prefix: z.string().optional(),
32
+ 'default-file-extension': z.string().optional(),
33
+ strategy: z.enum(['fetch', 'checkout']),
34
+ 'fetch-url': z.string().optional(),
35
+ })
36
+ .superRefine((value, ctx) => {
37
+ if (value.strategy === 'checkout' && value['fetch-url']) {
38
+ ctx.addIssue({
39
+ code: 'custom',
40
+ message: 'fetch-url is forbidden with strategy: checkout',
41
+ });
42
+ }
43
+ });
44
+ const localSourceSchema = z.strictObject({
45
+ 'local-path': z.string().min(1).optional(),
46
+ commit: z.string().optional(),
47
+ });
48
+ function record(value) {
49
+ return !!value && typeof value === 'object' && !Array.isArray(value);
50
+ }
51
+ function frontmatter(content) {
52
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
53
+ return match?.[1] ?? null;
54
+ }
55
+ function portablePath(value, label, allowEmpty = false) {
56
+ if (value === '' && allowEmpty)
57
+ return '';
58
+ if (!value ||
59
+ value.includes('\\') ||
60
+ value.startsWith('/') ||
61
+ /^[a-zA-Z]:/.test(value) ||
62
+ value.startsWith('//')) {
63
+ throw new Error(`${label} must be a relative POSIX path`);
64
+ }
65
+ const parts = value.split('/');
66
+ if (parts.some((part) => !part || part === '.' || part === '..')) {
67
+ throw new Error(`${label} cannot contain empty, ".", or ".." segments`);
68
+ }
69
+ return parts.join('/');
70
+ }
71
+ export function normalizeExternalDefaultFileExtension(value) {
72
+ if (value.startsWith('.')) {
73
+ throw new Error('default-file-extension must not start with "."');
74
+ }
75
+ if (!/^[a-z0-9]+$/i.test(value)) {
76
+ throw new Error('default-file-extension must contain only letters and numbers');
77
+ }
78
+ const normalized = value.toLowerCase();
79
+ const extension = `.${normalized}`;
80
+ if (!EXTERNAL_FILE_EXTENSIONS.has(extension)) {
81
+ const supported = [...EXTERNAL_FILE_EXTENSIONS]
82
+ .map((item) => item.slice(1))
83
+ .sort()
84
+ .join(', ');
85
+ throw new Error(`unsupported default file extension "${normalized}"; supported extensions: ${supported}`);
86
+ }
87
+ return normalized;
88
+ }
89
+ export function normalizeExternalRepoUrl(value) {
90
+ let url;
91
+ try {
92
+ url = new URL(value);
93
+ }
94
+ catch {
95
+ throw new Error('repo must be an absolute HTTPS URL');
96
+ }
97
+ if (url.protocol !== 'https:' ||
98
+ !url.hostname ||
99
+ url.username ||
100
+ url.password ||
101
+ url.search ||
102
+ url.hash) {
103
+ throw new Error('repo must be credential-free HTTPS without query or fragment');
104
+ }
105
+ if (!url.pathname || url.pathname === '/') {
106
+ throw new Error('repo must include a repository path');
107
+ }
108
+ url.hostname = url.hostname.toLowerCase();
109
+ url.protocol = 'https:';
110
+ if (url.port === '443')
111
+ url.port = '';
112
+ url.pathname = url.pathname.replace(/\/+$/, '');
113
+ if ((url.hostname === 'github.com' || url.hostname === 'gitlab.com') &&
114
+ url.pathname.endsWith('.git')) {
115
+ url.pathname = url.pathname.slice(0, -4);
116
+ }
117
+ return url.toString().replace(/\/$/, '');
118
+ }
119
+ function validateFetchUrl(value) {
120
+ const allowed = new Set(['{commit}', '{path}']);
121
+ const found = new Set();
122
+ const coveredBraces = new Set();
123
+ const unsupported = [];
124
+ const malformed = [];
125
+ for (const match of value.matchAll(/\{[^{}]*\}/g)) {
126
+ const placeholder = match[0];
127
+ const start = match.index ?? 0;
128
+ coveredBraces.add(start);
129
+ coveredBraces.add(start + placeholder.length - 1);
130
+ if (allowed.has(placeholder)) {
131
+ found.add(placeholder);
132
+ }
133
+ else if (/^\{[A-Za-z][A-Za-z0-9_-]*\}$/.test(placeholder)) {
134
+ unsupported.push(placeholder);
135
+ }
136
+ else {
137
+ malformed.push(placeholder);
138
+ }
139
+ }
140
+ if (unsupported.length > 0) {
141
+ throw new Error(`fetch-url contains unsupported placeholder ${JSON.stringify(unsupported[0])}; allowed placeholders are "{commit}" and "{path}"`);
142
+ }
143
+ if (malformed.length > 0) {
144
+ throw new Error(`fetch-url contains malformed placeholder ${JSON.stringify(malformed[0])}; placeholders must be exactly "{commit}" or "{path}"`);
145
+ }
146
+ let unmatchedBrace = -1;
147
+ for (let index = 0; index < value.length; index++) {
148
+ const character = value[index];
149
+ if ((character === '{' || character === '}') && !coveredBraces.has(index)) {
150
+ unmatchedBrace = index;
151
+ break;
152
+ }
153
+ }
154
+ if (unmatchedBrace !== -1) {
155
+ throw new Error(`fetch-url contains unmatched ${JSON.stringify(value[unmatchedBrace])} at character ${unmatchedBrace + 1}; placeholders must be exactly "{commit}" or "{path}"`);
156
+ }
157
+ if (!found.has('{commit}')) {
158
+ throw new Error('fetch-url must contain {commit}');
159
+ }
160
+ if (!found.has('{path}')) {
161
+ throw new Error('fetch-url must contain {path}');
162
+ }
163
+ const checked = value
164
+ .replaceAll('{commit}', '0123456789012345678901234567890123456789')
165
+ .replaceAll('{path}', 'docs/file.md');
166
+ let url;
167
+ try {
168
+ url = new URL(checked);
169
+ }
170
+ catch {
171
+ throw new Error('fetch-url must be an absolute HTTPS URL template');
172
+ }
173
+ validateHttpsUrl(url, 'fetch-url');
174
+ return value;
175
+ }
176
+ export function inferExternalFetchUrl(repo) {
177
+ const url = new URL(repo);
178
+ const parts = url.pathname.replace(/^\//, '').split('/');
179
+ if (url.hostname === 'github.com' && parts.length === 2) {
180
+ return `https://raw.githubusercontent.com/${parts.join('/')}/{commit}/{path}`;
181
+ }
182
+ if (url.hostname === 'gitlab.com' && parts.length >= 2) {
183
+ return `https://gitlab.com/${parts.join('/')}/-/raw/{commit}/{path}`;
184
+ }
185
+ return null;
186
+ }
187
+ function validateHttpsUrl(url, label) {
188
+ if (url.protocol !== 'https:' ||
189
+ !url.hostname ||
190
+ url.username ||
191
+ url.password ||
192
+ url.hash) {
193
+ throw new Error(`${label} must be a credential-free HTTPS URL without a fragment`);
194
+ }
195
+ }
196
+ function sourceFromValue(handle, value) {
197
+ if (!HANDLE_RE.test(handle)) {
198
+ throw new Error(`invalid external source name "${handle}"`);
199
+ }
200
+ const parsed = canonicalSourceSchema.parse(value);
201
+ const repo = normalizeExternalRepoUrl(parsed.repo);
202
+ if (!COMMIT_RE.test(parsed.commit)) {
203
+ throw new Error('commit must be a full lowercase Git SHA');
204
+ }
205
+ const prefix = parsed.prefix ? portablePath(parsed.prefix, 'prefix') : '';
206
+ const defaultFileExtension = parsed['default-file-extension']
207
+ ? normalizeExternalDefaultFileExtension(parsed['default-file-extension'])
208
+ : undefined;
209
+ let fetchUrl = parsed['fetch-url'];
210
+ if (parsed.strategy === 'fetch') {
211
+ const inferred = fetchUrl ?? inferExternalFetchUrl(repo);
212
+ if (!inferred) {
213
+ throw new Error('fetch-url is required for fetch sources outside GitHub and GitLab');
214
+ }
215
+ fetchUrl = validateFetchUrl(inferred);
216
+ }
217
+ return {
218
+ handle,
219
+ repo,
220
+ source: parsed.strategy === 'fetch' ? fetchUrl : parsed.repo,
221
+ commit: parsed.commit,
222
+ prefix,
223
+ ...(defaultFileExtension ? { defaultFileExtension } : {}),
224
+ strategy: parsed.strategy,
225
+ ...(fetchUrl ? { fetchUrl } : {}),
226
+ };
227
+ }
228
+ function expandLocalPath(value, projectRoot) {
229
+ if (value === '~')
230
+ return homedir();
231
+ if (value.startsWith('~/'))
232
+ return resolve(homedir(), value.slice(2));
233
+ return isAbsolute(value) ? resolve(value) : resolve(projectRoot, value);
234
+ }
235
+ const gitEnvironment = () => {
236
+ const env = {
237
+ ...process.env,
238
+ GIT_CONFIG_NOSYSTEM: '1',
239
+ GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
240
+ GIT_TERMINAL_PROMPT: '0',
241
+ GIT_ALLOW_PROTOCOL: 'https',
242
+ };
243
+ delete env.GIT_CONFIG_PARAMETERS;
244
+ delete env.GIT_CONFIG_COUNT;
245
+ for (const key of Object.keys(env)) {
246
+ if (/^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(key))
247
+ delete env[key];
248
+ }
249
+ return env;
250
+ };
251
+ async function git(args, options = {}) {
252
+ const { stdout } = await execFileAsync('git', args, {
253
+ cwd: options.cwd,
254
+ encoding: 'utf8',
255
+ env: gitEnvironment(),
256
+ maxBuffer: options.maxBuffer ?? MAX_BYTES + 1024 * 1024,
257
+ timeout: 60_000,
258
+ });
259
+ return options.trim === false ? stdout : stdout.trim();
260
+ }
261
+ async function localCheckoutStatus(source, local, projectRoot) {
262
+ if (!local.localPath)
263
+ return {};
264
+ const path = expandLocalPath(local.localPath, projectRoot);
265
+ const expected = local.commit ?? source.commit;
266
+ try {
267
+ if (lstatSync(path).isSymbolicLink()) {
268
+ throw new Error('local path cannot be a symlink');
269
+ }
270
+ if (!statSync(path).isDirectory())
271
+ throw new Error('not a directory');
272
+ const root = realpathSync(await git(['-C', path, 'rev-parse', '--show-toplevel']));
273
+ const head = await git(['-C', root, 'rev-parse', 'HEAD']);
274
+ if (head !== expected) {
275
+ throw new Error(`HEAD is ${head}, expected ${expected}`);
276
+ }
277
+ const remotes = (await git(['-C', root, 'remote']))
278
+ .split('\n')
279
+ .filter(Boolean);
280
+ const discovered = [];
281
+ let matches = false;
282
+ for (const remote of remotes) {
283
+ const candidates = (await git(['-C', root, 'remote', 'get-url', '--all', remote]))
284
+ .split('\n')
285
+ .filter(Boolean);
286
+ for (const candidate of candidates) {
287
+ discovered.push({ remote, url: candidate });
288
+ try {
289
+ if (normalizeExternalRepoUrl(candidate) === source.repo) {
290
+ matches = true;
291
+ }
292
+ }
293
+ catch {
294
+ // Unsupported local remote; another URL may identify the repository.
295
+ }
296
+ }
297
+ }
298
+ if (!matches) {
299
+ const found = discovered.length === 0
300
+ ? 'found no Git remote URLs'
301
+ : `found ${discovered
302
+ .map(({ remote, url }) => `${remote}=${JSON.stringify(url)}`)
303
+ .join(', ')}`;
304
+ throw new Error(`no Git remote URL matches configured repo ${JSON.stringify(source.repo)}; ${found}`);
305
+ }
306
+ return { path: root };
307
+ }
308
+ catch (error) {
309
+ return {
310
+ error: `external source "${source.handle}" local-path "${path}" is invalid: ${error.message}`,
311
+ };
312
+ }
313
+ }
314
+ export async function loadExternalSources(latDir, projectRoot = dirname(latDir), options = {}) {
315
+ const canonicalPath = join(latDir, 'lat.md');
316
+ const localPath = join(latDir, 'config.local.yaml');
317
+ const errors = [];
318
+ const canonical = new Map();
319
+ const overrides = new Map();
320
+ let validCanonical = true;
321
+ try {
322
+ const yaml = frontmatter(await readFile(canonicalPath, 'utf8'));
323
+ const parsed = yaml ? parseYaml(yaml) : {};
324
+ const raw = record(parsed) && record(parsed.lat)
325
+ ? parsed.lat['external-sources']
326
+ : undefined;
327
+ if (raw !== undefined && !record(raw)) {
328
+ throw new Error('lat.external-sources must be a mapping');
329
+ }
330
+ for (const [handle, value] of Object.entries(record(raw) ? raw : {})) {
331
+ canonical.set(handle, sourceFromValue(handle, value));
332
+ }
333
+ }
334
+ catch (error) {
335
+ if (error.code !== 'ENOENT') {
336
+ validCanonical = false;
337
+ errors.push({ file: canonicalPath, message: error.message });
338
+ }
339
+ }
340
+ if (!options.ignoreLocal && existsSync(localPath)) {
341
+ try {
342
+ const parsed = parseYaml(await readFile(localPath, 'utf8'));
343
+ const raw = record(parsed) ? parsed['external-sources'] : undefined;
344
+ if (raw !== undefined && !record(raw)) {
345
+ throw new Error('external-sources must be a mapping');
346
+ }
347
+ for (const [handle, value] of Object.entries(record(raw) ? raw : {})) {
348
+ if (!canonical.has(handle)) {
349
+ throw new Error(`local override references unknown source "${handle}"`);
350
+ }
351
+ const item = localSourceSchema.parse(value);
352
+ if (item.commit && !COMMIT_RE.test(item.commit)) {
353
+ throw new Error(`${handle}.commit must be a full lowercase Git SHA`);
354
+ }
355
+ overrides.set(handle, {
356
+ ...(item['local-path'] ? { localPath: item['local-path'] } : {}),
357
+ ...(item.commit ? { commit: item.commit } : {}),
358
+ });
359
+ }
360
+ }
361
+ catch (error) {
362
+ errors.push({ file: localPath, message: error.message });
363
+ }
364
+ }
365
+ const sources = new Map();
366
+ for (const source of canonical.values()) {
367
+ const local = overrides.get(source.handle) ?? {};
368
+ const status = await localCheckoutStatus(source, local, projectRoot);
369
+ const effectiveCommit = local.commit ?? source.commit;
370
+ sources.set(source.handle, {
371
+ ...source,
372
+ source: status.path ? local.localPath : source.source,
373
+ canonicalCommit: source.commit,
374
+ commit: effectiveCommit,
375
+ effectiveStrategy: status.path ? 'local' : source.strategy,
376
+ ...(status.path ? { localPath: status.path } : {}),
377
+ ...(status.error ? { localError: status.error } : {}),
378
+ });
379
+ if (status.error)
380
+ errors.push({ file: localPath, message: status.error });
381
+ }
382
+ return { canonicalPath, localPath, sources, errors, validCanonical };
383
+ }
384
+ export function parseExternalTarget(value, snapshot) {
385
+ const target = value.replace(/^\[\[|\]\]$/g, '');
386
+ const colon = target.indexOf(':');
387
+ if (colon <= 0)
388
+ return null;
389
+ const handle = target.slice(0, colon);
390
+ const source = snapshot.sources.get(handle);
391
+ if (!source)
392
+ return null;
393
+ const rest = target.slice(colon + 1);
394
+ const hash = rest.indexOf('#');
395
+ const authoredPath = portablePath(hash === -1 ? rest : rest.slice(0, hash), 'external path');
396
+ const fragment = hash === -1 ? '' : rest.slice(hash + 1);
397
+ if (/^L\d+(?:-L?\d+)?$/i.test(fragment)) {
398
+ throw new Error('external fragments cannot use line numbers');
399
+ }
400
+ const authoredExtension = extname(authoredPath);
401
+ const resolvedPath = !authoredExtension && source.defaultFileExtension
402
+ ? `${authoredPath}.${source.defaultFileExtension}`
403
+ : authoredPath;
404
+ const ext = extname(resolvedPath).toLowerCase();
405
+ if (!EXTERNAL_FILE_EXTENSIONS.has(ext)) {
406
+ throw new Error(`unsupported external file extension "${ext || '(none)'}"`);
407
+ }
408
+ const defaultSuffix = source.defaultFileExtension
409
+ ? `.${source.defaultFileExtension}`
410
+ : '';
411
+ const identityPath = defaultSuffix &&
412
+ resolvedPath.length > defaultSuffix.length &&
413
+ resolvedPath.endsWith(defaultSuffix)
414
+ ? resolvedPath.slice(0, -defaultSuffix.length)
415
+ : authoredPath;
416
+ const repositoryPath = source.prefix
417
+ ? `${source.prefix}/${resolvedPath}`
418
+ : resolvedPath;
419
+ return {
420
+ target,
421
+ handle,
422
+ authoredPath,
423
+ resolvedPath,
424
+ repositoryPath,
425
+ fragment,
426
+ identity: `${handle}:${identityPath}${fragment ? `#${fragment}` : ''}`,
427
+ };
428
+ }
429
+ function editDistance(left, right) {
430
+ const previous = [...Array(right.length + 1).keys()];
431
+ for (let i = 1; i <= left.length; i++) {
432
+ let diagonal = previous[0];
433
+ previous[0] = i;
434
+ for (let j = 1; j <= right.length; j++) {
435
+ const above = previous[j];
436
+ previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
437
+ diagonal = above;
438
+ }
439
+ }
440
+ return previous[right.length];
441
+ }
442
+ function unknownExternalHandle(targetValue, snapshot) {
443
+ const target = targetValue.replace(/^\[\[|\]\]$/g, '');
444
+ const colon = target.indexOf(':');
445
+ if (colon <= 0)
446
+ return null;
447
+ const handle = target.slice(0, colon);
448
+ if (!HANDLE_RE.test(handle) || snapshot.sources.has(handle))
449
+ return null;
450
+ const candidates = [...snapshot.sources.keys()]
451
+ .map((candidate) => ({
452
+ candidate,
453
+ distance: editDistance(handle, candidate),
454
+ }))
455
+ .sort((left, right) => left.distance - right.distance ||
456
+ left.candidate.localeCompare(right.candidate));
457
+ const suggestion = candidates[0];
458
+ return suggestion &&
459
+ suggestion.distance <= Math.max(2, Math.floor(handle.length / 3))
460
+ ? `unknown external source "${handle}"; did you mean "${suggestion.candidate}"?`
461
+ : `unknown external source "${handle}"`;
462
+ }
463
+ function cacheRoot(latDir) {
464
+ return join(latDir, '.cache', 'external');
465
+ }
466
+ export function externalCachePaths(latDir, handle) {
467
+ const root = cacheRoot(latDir);
468
+ return {
469
+ root,
470
+ directory: join(root, handle),
471
+ metadata: join(root, `${handle}.json`),
472
+ };
473
+ }
474
+ export function readExternalCacheMetadata(latDir, handle) {
475
+ try {
476
+ const value = JSON.parse(readFileSync(externalCachePaths(latDir, handle).metadata, 'utf8'));
477
+ if (value.ver !== EXTERNAL_SOURCES_SCHEMA_VER ||
478
+ typeof value.source !== 'string' ||
479
+ value.source.length === 0 ||
480
+ !COMMIT_RE.test(value.commit) ||
481
+ !['fetch', 'checkout', 'local'].includes(value.strategy))
482
+ return null;
483
+ return value;
484
+ }
485
+ catch {
486
+ return null;
487
+ }
488
+ }
489
+ const sourceLocks = new Map();
490
+ const LOCK_RETRY_MS = 25;
491
+ const LOCK_TIMEOUT_MS = 30_000;
492
+ const LOCK_STALE_MS = 10 * 60_000;
493
+ const LOCK_OWNER_GRACE_MS = 1_000;
494
+ async function filesystemLockOwner(path) {
495
+ try {
496
+ const value = JSON.parse(await readFile(join(path, 'owner.json'), 'utf8'));
497
+ if (!value ||
498
+ typeof value.owner !== 'string' ||
499
+ !Number.isInteger(value.pid) ||
500
+ value.pid <= 0 ||
501
+ !Number.isFinite(value.startedAt)) {
502
+ return null;
503
+ }
504
+ return value;
505
+ }
506
+ catch {
507
+ return null;
508
+ }
509
+ }
510
+ function processIsAlive(pid) {
511
+ try {
512
+ process.kill(pid, 0);
513
+ return true;
514
+ }
515
+ catch (error) {
516
+ return error.code !== 'ESRCH';
517
+ }
518
+ }
519
+ async function discardFilesystemLock(path) {
520
+ const discarded = `${path}.stale-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
521
+ try {
522
+ await rename(path, discarded);
523
+ }
524
+ catch (error) {
525
+ if (error.code === 'ENOENT')
526
+ return;
527
+ throw error;
528
+ }
529
+ await rm(discarded, { recursive: true, force: true });
530
+ }
531
+ async function acquireFilesystemLock(key) {
532
+ const path = `${key}.lock`;
533
+ const owner = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
534
+ await mkdir(dirname(path), { recursive: true });
535
+ const started = Date.now();
536
+ while (true) {
537
+ try {
538
+ await mkdir(path);
539
+ await writeFile(join(path, 'owner.json'), `${JSON.stringify({ owner, pid: process.pid, startedAt: Date.now() })}\n`);
540
+ return async () => {
541
+ try {
542
+ const value = JSON.parse(await readFile(join(path, 'owner.json'), 'utf8'));
543
+ if (value.owner === owner) {
544
+ await rm(path, { recursive: true, force: true });
545
+ }
546
+ }
547
+ catch (error) {
548
+ if (error.code !== 'ENOENT')
549
+ throw error;
550
+ }
551
+ };
552
+ }
553
+ catch (error) {
554
+ if (error.code !== 'EEXIST')
555
+ throw error;
556
+ try {
557
+ const age = Date.now() - statSync(path).mtimeMs;
558
+ const currentOwner = await filesystemLockOwner(path);
559
+ if (age > LOCK_STALE_MS ||
560
+ (currentOwner && !processIsAlive(currentOwner.pid)) ||
561
+ (!currentOwner && age > LOCK_OWNER_GRACE_MS)) {
562
+ await discardFilesystemLock(path);
563
+ continue;
564
+ }
565
+ }
566
+ catch (statError) {
567
+ if (statError.code !== 'ENOENT') {
568
+ throw statError;
569
+ }
570
+ continue;
571
+ }
572
+ if (Date.now() - started >= LOCK_TIMEOUT_MS) {
573
+ throw new Error(`timed out waiting for external cache lock "${path}"`);
574
+ }
575
+ await new Promise((resolveWait) => setTimeout(resolveWait, LOCK_RETRY_MS));
576
+ }
577
+ }
578
+ }
579
+ async function serialized(key, fn) {
580
+ const previous = sourceLocks.get(key) ?? Promise.resolve();
581
+ let release;
582
+ const current = new Promise((resolveLock) => (release = resolveLock));
583
+ const tail = previous.then(() => current);
584
+ sourceLocks.set(key, tail);
585
+ await previous;
586
+ let releaseFilesystem;
587
+ try {
588
+ releaseFilesystem = await acquireFilesystemLock(key);
589
+ return await fn();
590
+ }
591
+ finally {
592
+ await releaseFilesystem?.();
593
+ release();
594
+ if (sourceLocks.get(key) === tail)
595
+ sourceLocks.delete(key);
596
+ }
597
+ }
598
+ async function atomicJson(path, value) {
599
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
600
+ await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`);
601
+ await rename(temp, path);
602
+ }
603
+ function expectedMetadata(source) {
604
+ return {
605
+ ver: EXTERNAL_SOURCES_SCHEMA_VER,
606
+ source: source.source,
607
+ commit: source.commit,
608
+ strategy: source.effectiveStrategy,
609
+ };
610
+ }
611
+ function sameMetadata(left, right) {
612
+ return (!!left &&
613
+ left.ver === right.ver &&
614
+ left.source === right.source &&
615
+ left.commit === right.commit &&
616
+ left.strategy === right.strategy);
617
+ }
618
+ async function initializeCheckout(directory, source) {
619
+ await git(['init', '--bare', directory]);
620
+ await git(['-C', directory, 'remote', 'add', 'origin', source.repo]);
621
+ await git(['-C', directory, 'config', 'remote.origin.promisor', 'true']);
622
+ await git([
623
+ '-C',
624
+ directory,
625
+ 'config',
626
+ 'remote.origin.partialclonefilter',
627
+ 'blob:none',
628
+ ]);
629
+ await git([
630
+ '-C',
631
+ directory,
632
+ 'fetch',
633
+ '--depth=1',
634
+ '--filter=blob:none',
635
+ '--no-tags',
636
+ 'origin',
637
+ source.commit,
638
+ ]);
639
+ await git(['-C', directory, 'update-ref', 'refs/lat/commit', 'FETCH_HEAD']);
640
+ }
641
+ async function checkoutHasUnsafeUrlRewrite(directory) {
642
+ try {
643
+ const value = await git([
644
+ '-C',
645
+ directory,
646
+ 'config',
647
+ '--local',
648
+ '--name-only',
649
+ '--get-regexp',
650
+ '^url\\.',
651
+ ]);
652
+ return value.length > 0;
653
+ }
654
+ catch (error) {
655
+ if (error.code === 1)
656
+ return false;
657
+ throw error;
658
+ }
659
+ }
660
+ async function ensureGenerationUnlocked(latDir, source) {
661
+ const paths = externalCachePaths(latDir, source.handle);
662
+ await mkdir(paths.root, { recursive: true });
663
+ const expected = expectedMetadata(source);
664
+ let current = readExternalCacheMetadata(latDir, source.handle);
665
+ if (sameMetadata(current, expected) &&
666
+ source.effectiveStrategy === 'checkout') {
667
+ try {
668
+ const origin = await git([
669
+ '-C',
670
+ paths.directory,
671
+ 'remote',
672
+ 'get-url',
673
+ 'origin',
674
+ ]);
675
+ if (normalizeExternalRepoUrl(origin) !== source.repo ||
676
+ (await checkoutHasUnsafeUrlRewrite(paths.directory)))
677
+ current = null;
678
+ }
679
+ catch {
680
+ current = null;
681
+ }
682
+ }
683
+ const directoryCorrect = source.effectiveStrategy === 'local'
684
+ ? !existsSync(paths.directory)
685
+ : existsSync(paths.directory);
686
+ if (sameMetadata(current, expected) && directoryCorrect)
687
+ return paths.directory;
688
+ await rm(paths.directory, { recursive: true, force: true });
689
+ await rm(paths.metadata, { force: true });
690
+ if (source.effectiveStrategy !== 'local') {
691
+ if (source.effectiveStrategy === 'checkout') {
692
+ const staging = `${paths.directory}.staging-${process.pid}-${Date.now()}`;
693
+ try {
694
+ await initializeCheckout(staging, source);
695
+ await rename(staging, paths.directory);
696
+ }
697
+ catch (error) {
698
+ await rm(staging, { recursive: true, force: true });
699
+ throw error;
700
+ }
701
+ }
702
+ else {
703
+ await mkdir(paths.directory, { recursive: true });
704
+ }
705
+ }
706
+ await atomicJson(paths.metadata, expected);
707
+ return paths.directory;
708
+ }
709
+ async function validateRemovedCaches(snapshot, latDir) {
710
+ if (!snapshot.validCanonical)
711
+ return;
712
+ const root = cacheRoot(latDir);
713
+ let entries;
714
+ try {
715
+ entries = await readdir(root);
716
+ }
717
+ catch {
718
+ return;
719
+ }
720
+ for (const entry of entries) {
721
+ if (!entry.endsWith('.json'))
722
+ continue;
723
+ const handle = entry.slice(0, -5);
724
+ if (!HANDLE_RE.test(handle) || snapshot.sources.has(handle))
725
+ continue;
726
+ const paths = externalCachePaths(latDir, handle);
727
+ await serialized(paths.metadata, async () => {
728
+ await rm(paths.directory, { recursive: true, force: true });
729
+ await rm(paths.metadata, { force: true });
730
+ });
731
+ }
732
+ }
733
+ function fetchUrl(source, path) {
734
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
735
+ return new URL(source
736
+ .fetchUrl.replaceAll('{commit}', source.commit)
737
+ .replaceAll('{path}', encodedPath));
738
+ }
739
+ async function readHttps(url, redirects = 0, ca) {
740
+ validateHttpsUrl(url, 'external file URL');
741
+ if (redirects > MAX_REDIRECTS)
742
+ throw new Error('too many redirects');
743
+ return new Promise((resolveResponse, reject) => {
744
+ const request = httpsRequest(url, {
745
+ ca,
746
+ headers: { accept: 'application/octet-stream', 'user-agent': 'lat.md' },
747
+ }, (response) => {
748
+ const status = response.statusCode ?? 0;
749
+ if ([301, 302, 303, 307, 308].includes(status)) {
750
+ response.resume();
751
+ const location = response.headers.location;
752
+ if (!location)
753
+ return reject(new Error(`redirect ${status} has no location`));
754
+ let next;
755
+ try {
756
+ next = new URL(location, url);
757
+ validateHttpsUrl(next, 'redirect');
758
+ }
759
+ catch (error) {
760
+ return reject(error);
761
+ }
762
+ void readHttps(next, redirects + 1, ca).then(resolveResponse, reject);
763
+ return;
764
+ }
765
+ if (status !== 200) {
766
+ response.resume();
767
+ reject(new Error(`HTTP ${status} for ${url}`));
768
+ return;
769
+ }
770
+ const length = Number(response.headers['content-length'] ?? 0);
771
+ if (length > MAX_BYTES) {
772
+ response.destroy();
773
+ reject(new Error(`response exceeds ${MAX_BYTES} bytes`));
774
+ return;
775
+ }
776
+ const chunks = [];
777
+ let size = 0;
778
+ response.on('data', (chunk) => {
779
+ size += chunk.length;
780
+ if (size > MAX_BYTES) {
781
+ response.destroy(new Error(`response exceeds ${MAX_BYTES} bytes`));
782
+ }
783
+ else
784
+ chunks.push(chunk);
785
+ });
786
+ response.on('end', () => resolveResponse({
787
+ bytes: Buffer.concat(chunks),
788
+ contentType: String(response.headers['content-type'] ?? '')
789
+ .split(';', 1)[0]
790
+ .trim()
791
+ .toLowerCase(),
792
+ }));
793
+ response.on('error', reject);
794
+ });
795
+ request.setTimeout(HTTP_TIMEOUT_MS, () => request.destroy(new Error('external read timed out')));
796
+ request.on('error', reject);
797
+ request.end();
798
+ });
799
+ }
800
+ function assertNoSymlinks(root, file) {
801
+ const rootReal = realpathSync(root);
802
+ const rel = relative(root, file);
803
+ let cursor = root;
804
+ for (const part of rel.split(/[\\/]/)) {
805
+ cursor = join(cursor, part);
806
+ if (lstatSync(cursor).isSymbolicLink())
807
+ throw new Error('external local path contains a symlink');
808
+ }
809
+ const real = realpathSync(file);
810
+ if (relative(rootReal, real).startsWith('..') ||
811
+ isAbsolute(relative(rootReal, real))) {
812
+ throw new Error('external local path escapes the checkout');
813
+ }
814
+ return real;
815
+ }
816
+ async function readProviderContent(latDir, projectRoot, source, target, ca, ignoreLocal = false) {
817
+ const paths = externalCachePaths(latDir, source.handle);
818
+ return serialized(paths.metadata, async () => {
819
+ await assertCurrentGeneration(latDir, projectRoot, source, ignoreLocal);
820
+ const directory = await ensureGenerationUnlocked(latDir, source);
821
+ let content;
822
+ if (source.effectiveStrategy === 'local') {
823
+ const path = assertNoSymlinks(source.localPath, join(source.localPath, target.repositoryPath));
824
+ content = await readFile(path, 'utf8');
825
+ }
826
+ else if (source.effectiveStrategy === 'checkout') {
827
+ content = await git(['-C', directory, 'show', `${source.commit}:${target.repositoryPath}`], { maxBuffer: MAX_BYTES + 1024, trim: false });
828
+ }
829
+ else {
830
+ const path = join(directory, ...target.repositoryPath.split('/'));
831
+ try {
832
+ content = await readFile(path, 'utf8');
833
+ }
834
+ catch {
835
+ const response = await readHttps(fetchUrl(source, target.repositoryPath), 0, ca);
836
+ if (response.contentType === 'text/html') {
837
+ throw new Error(`fetch-url returned HTML for "${target.repositoryPath}" instead of raw file bytes; configure a raw-file URL or use strategy: checkout`);
838
+ }
839
+ content = new TextDecoder('utf-8', { fatal: true }).decode(response.bytes);
840
+ await mkdir(dirname(path), { recursive: true });
841
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
842
+ await writeFile(temp, content);
843
+ await rename(temp, path);
844
+ }
845
+ }
846
+ if (Buffer.byteLength(content) > MAX_BYTES)
847
+ throw new Error(`external file exceeds ${MAX_BYTES} bytes`);
848
+ await assertCurrentGeneration(latDir, projectRoot, source, ignoreLocal);
849
+ return { content, provider: source.effectiveStrategy };
850
+ });
851
+ }
852
+ async function assertCurrentGeneration(latDir, projectRoot, source, ignoreLocal) {
853
+ const current = await loadExternalSources(latDir, projectRoot, {
854
+ ignoreLocal,
855
+ });
856
+ const selected = current.sources.get(source.handle);
857
+ if (!current.validCanonical ||
858
+ !selected ||
859
+ !sameMetadata(expectedMetadata(selected), expectedMetadata(source))) {
860
+ throw new Error(`external source "${source.handle}" configuration changed during retrieval`);
861
+ }
862
+ }
863
+ function sourceSymbol(symbols, fragment) {
864
+ const parts = fragment.split('#');
865
+ if (parts.length === 1)
866
+ return symbols.find((item) => item.name === parts[0] && !item.parent);
867
+ if (parts.length === 2)
868
+ return symbols.find((item) => item.parent === parts[0] && item.name === parts[1]);
869
+ return undefined;
870
+ }
871
+ async function selectFragment(target, fullContent, latDir, externalDocumentParserRuntime, sourceParserRuntime, onDocumentAnalyzed, onParserImport) {
872
+ const lines = fullContent.split('\n');
873
+ if (isDocumentPath(target.resolvedPath)) {
874
+ const { document } = await analyzeExternalDocumentCached(target.resolvedPath, fullContent, latDir, {
875
+ identity: `@external/${target.handle}/${target.resolvedPath}`,
876
+ runtime: externalDocumentParserRuntime,
877
+ onFileAnalyzed: onDocumentAnalyzed,
878
+ onParserImport,
879
+ });
880
+ if (!target.fragment)
881
+ return {
882
+ content: fullContent,
883
+ startLine: 1,
884
+ endLine: lines.length,
885
+ kind: 'document',
886
+ document,
887
+ };
888
+ const match = findExternalDocumentSection(document, target.fragment);
889
+ if (!match)
890
+ throw new Error(`document heading or anchor "${target.fragment}" not found in "${target.resolvedPath}"`);
891
+ return {
892
+ content: lines.slice(match.startLine - 1, match.endLine).join('\n'),
893
+ startLine: match.startLine,
894
+ endLine: match.endLine,
895
+ kind: 'document',
896
+ document,
897
+ };
898
+ }
899
+ if (!target.fragment)
900
+ return {
901
+ content: fullContent,
902
+ startLine: 1,
903
+ endLine: lines.length,
904
+ kind: 'source',
905
+ };
906
+ const { symbols } = await analyzeSourceSymbols(target.resolvedPath, fullContent, latDir, {
907
+ identity: `@external/${target.handle}/${target.resolvedPath}`,
908
+ runtime: sourceParserRuntime,
909
+ });
910
+ const symbol = sourceSymbol(symbols, target.fragment);
911
+ if (!symbol)
912
+ throw new Error(`symbol "${target.fragment}" not found in "${target.resolvedPath}"`);
913
+ return {
914
+ content: lines.slice(symbol.startLine - 1, symbol.endLine).join('\n'),
915
+ startLine: symbol.startLine,
916
+ endLine: symbol.endLine,
917
+ kind: 'source',
918
+ signature: symbol.signature,
919
+ };
920
+ }
921
+ export class ExternalResolver {
922
+ latDir;
923
+ projectRoot;
924
+ snapshot;
925
+ ca;
926
+ ignoreLocal;
927
+ onDocumentAnalyzed;
928
+ onParserImport;
929
+ reconciliationPromise;
930
+ externalFilePromises = new Map();
931
+ externalDocumentParserRuntime = new ExternalDocumentParserRuntime();
932
+ sourceParserRuntime = new SourceParserRuntime();
933
+ constructor(latDir, projectRoot, snapshot, ca, ignoreLocal = false, onDocumentAnalyzed, onParserImport) {
934
+ this.latDir = latDir;
935
+ this.projectRoot = projectRoot;
936
+ this.snapshot = snapshot;
937
+ this.ca = ca;
938
+ this.ignoreLocal = ignoreLocal;
939
+ this.onDocumentAnalyzed = onDocumentAnalyzed;
940
+ this.onParserImport = onParserImport;
941
+ }
942
+ parse(target) {
943
+ return parseExternalTarget(target, this.snapshot);
944
+ }
945
+ unknownTargetMessage(target) {
946
+ return unknownExternalHandle(target, this.snapshot);
947
+ }
948
+ reconcile() {
949
+ this.reconciliationPromise ??= validateRemovedCaches(this.snapshot, this.latDir);
950
+ return this.reconciliationPromise;
951
+ }
952
+ readFile(source, target) {
953
+ const key = `${source.handle}\0${target.repositoryPath}`;
954
+ let loaded = this.externalFilePromises.get(key);
955
+ if (!loaded) {
956
+ loaded = readProviderContent(this.latDir, this.projectRoot, source, target, this.ca, this.ignoreLocal);
957
+ this.externalFilePromises.set(key, loaded);
958
+ }
959
+ return loaded;
960
+ }
961
+ async resolve(targetValue) {
962
+ const target = this.parse(targetValue);
963
+ if (!target)
964
+ throw new Error(`unknown external target "${targetValue}"`);
965
+ const source = this.snapshot.sources.get(target.handle);
966
+ await this.reconcile();
967
+ try {
968
+ const loaded = await this.readFile(source, target);
969
+ const fragment = await selectFragment(target, loaded.content, this.latDir, this.externalDocumentParserRuntime, this.sourceParserRuntime, this.onDocumentAnalyzed, this.onParserImport);
970
+ return {
971
+ target,
972
+ source,
973
+ provider: loaded.provider,
974
+ fullContent: loaded.content,
975
+ ...fragment,
976
+ };
977
+ }
978
+ catch (error) {
979
+ throw new Error(`external source "${source.handle}" could not read "${target.repositoryPath}" at ${source.commit} via ${source.effectiveStrategy}: ${error.message}`, { cause: error });
980
+ }
981
+ }
982
+ }
983
+ export async function createExternalResolver(latDir, projectRoot = dirname(latDir), options = {}) {
984
+ return new ExternalResolver(latDir, projectRoot, await loadExternalSources(latDir, projectRoot, options), options.ca, options.ignoreLocal, options.onDocumentAnalyzed, options.onParserImport);
985
+ }
986
+ export function describeExternalSources(latDir, snapshot) {
987
+ return [...snapshot.sources.values()].map((source) => ({
988
+ handle: source.handle,
989
+ repo: source.repo,
990
+ canonicalCommit: source.canonicalCommit,
991
+ effectiveCommit: source.commit,
992
+ prefix: source.prefix,
993
+ ...(source.defaultFileExtension
994
+ ? { defaultFileExtension: source.defaultFileExtension }
995
+ : {}),
996
+ strategy: source.strategy,
997
+ effectiveStrategy: source.effectiveStrategy,
998
+ ...(source.fetchUrl ? { fetchUrl: source.fetchUrl } : {}),
999
+ ...(source.localPath ? { localPath: source.localPath } : {}),
1000
+ ...(source.localError ? { localError: source.localError } : {}),
1001
+ cache: readExternalCacheMetadata(latDir, source.handle),
1002
+ checkout: [
1003
+ {
1004
+ command: 'git',
1005
+ args: [
1006
+ 'clone',
1007
+ '--filter=blob:none',
1008
+ '--sparse',
1009
+ source.repo,
1010
+ source.handle,
1011
+ ],
1012
+ },
1013
+ ...(source.prefix
1014
+ ? [
1015
+ {
1016
+ command: 'git',
1017
+ args: [
1018
+ '-C',
1019
+ source.handle,
1020
+ 'sparse-checkout',
1021
+ 'set',
1022
+ source.prefix,
1023
+ ],
1024
+ },
1025
+ ]
1026
+ : []),
1027
+ {
1028
+ command: 'git',
1029
+ args: ['-C', source.handle, 'checkout', '--detach', source.commit],
1030
+ },
1031
+ ],
1032
+ }));
1033
+ }
1034
+ export async function resolveExternalCommit(repo, ref) {
1035
+ const normalized = normalizeExternalRepoUrl(repo);
1036
+ const actual = await import('node:fs/promises').then(({ mkdtemp }) => mkdtemp(join(tmpdir(), 'lat-external-ref-')));
1037
+ try {
1038
+ await git(['init', '--bare', actual]);
1039
+ await git([
1040
+ '-C',
1041
+ actual,
1042
+ 'fetch',
1043
+ '--depth=1',
1044
+ '--filter=blob:none',
1045
+ '--no-tags',
1046
+ normalized,
1047
+ ref,
1048
+ ]);
1049
+ const commit = await git([
1050
+ '-C',
1051
+ actual,
1052
+ 'rev-parse',
1053
+ 'FETCH_HEAD^{commit}',
1054
+ ]);
1055
+ if (!COMMIT_RE.test(commit))
1056
+ throw new Error('remote ref did not resolve to a full commit');
1057
+ return commit;
1058
+ }
1059
+ finally {
1060
+ await rm(actual, { recursive: true, force: true });
1061
+ }
1062
+ }
1063
+ export async function addCanonicalExternalSource(latDir, input) {
1064
+ const path = join(latDir, 'lat.md');
1065
+ const content = await readFile(path, 'utf8');
1066
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
1067
+ const document = parseDocument(match?.[1] ?? '');
1068
+ const existing = document.getIn(['lat', 'external-sources', input.handle]);
1069
+ if (existing !== undefined)
1070
+ throw new Error(`external source "${input.handle}" already exists`);
1071
+ const source = sourceFromValue(input.handle, {
1072
+ repo: input.repo,
1073
+ commit: input.commit,
1074
+ ...(input.prefix ? { prefix: input.prefix } : {}),
1075
+ ...(input.defaultFileExtension
1076
+ ? { 'default-file-extension': input.defaultFileExtension }
1077
+ : {}),
1078
+ strategy: input.strategy,
1079
+ ...(input.fetchUrl ? { 'fetch-url': input.fetchUrl } : {}),
1080
+ });
1081
+ document.setIn(['lat', 'external-sources', input.handle], {
1082
+ repo: source.repo,
1083
+ commit: source.commit,
1084
+ ...(source.prefix ? { prefix: source.prefix } : {}),
1085
+ ...(source.defaultFileExtension
1086
+ ? { 'default-file-extension': source.defaultFileExtension }
1087
+ : {}),
1088
+ strategy: source.strategy,
1089
+ ...(input.fetchUrl ? { 'fetch-url': input.fetchUrl } : {}),
1090
+ });
1091
+ const nextFrontmatter = document.toString().trimEnd();
1092
+ const body = match ? content.slice(match[0].length) : content;
1093
+ const next = `---\n${nextFrontmatter}\n---\n${body}`;
1094
+ const temp = `${path}.${process.pid}.tmp`;
1095
+ await writeFile(temp, next);
1096
+ await rename(temp, path);
1097
+ return source;
1098
+ }