@plebeiantech/lat.md 0.12.3-fork.13 → 0.12.3-fork.15
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.
- package/README.md +24 -0
- package/dist/src/cli/check-context.d.ts +56 -0
- package/dist/src/cli/check-context.js +215 -0
- package/dist/src/cli/check-coverage.js +9 -5
- package/dist/src/cli/check-frontmatter.js +1 -1
- package/dist/src/cli/check-mode.js +1 -1
- package/dist/src/cli/check-status.js +1 -1
- package/dist/src/cli/check.d.ts +19 -12
- package/dist/src/cli/check.js +259 -206
- package/dist/src/cli/context.js +1 -1
- package/dist/src/cli/expand.d.ts +1 -2
- package/dist/src/cli/expand.js +52 -5
- package/dist/src/cli/external.d.ts +11 -0
- package/dist/src/cli/external.js +185 -0
- package/dist/src/cli/fork-instructions.d.ts +1 -0
- package/dist/src/cli/fork-instructions.js +3 -2
- package/dist/src/cli/hook.d.ts +26 -0
- package/dist/src/cli/hook.js +112 -40
- package/dist/src/cli/index.js +55 -3
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +40 -2
- package/dist/src/cli/locate.js +4 -3
- package/dist/src/cli/refs.d.ts +2 -1
- package/dist/src/cli/refs.js +88 -43
- package/dist/src/cli/reindex.js +2 -1
- package/dist/src/cli/search.d.ts +12 -3
- package/dist/src/cli/search.js +45 -49
- package/dist/src/cli/section.d.ts +3 -1
- package/dist/src/cli/section.js +114 -38
- package/dist/src/code-refs.d.ts +17 -5
- package/dist/src/code-refs.js +246 -65
- package/dist/src/context.d.ts +3 -0
- package/dist/src/document-formats.d.ts +7 -0
- package/dist/src/document-formats.js +39 -0
- package/dist/src/extensions/alert-marker.d.ts +25 -0
- package/dist/src/extensions/alert-marker.js +55 -0
- package/dist/src/external-documents.d.ts +56 -0
- package/dist/src/external-documents.js +404 -0
- package/dist/src/external-sources.d.ts +136 -0
- package/dist/src/external-sources.js +1098 -0
- package/dist/src/fork/antigravity-hook.d.ts +47 -0
- package/dist/src/fork/antigravity-hook.js +424 -0
- package/dist/src/fork/antigravity-init.d.ts +17 -0
- package/dist/src/fork/antigravity-init.js +124 -0
- package/dist/src/fork/frontmatter-fields.d.ts +36 -0
- package/dist/src/fork/frontmatter-fields.js +23 -0
- package/dist/src/format.d.ts +5 -2
- package/dist/src/format.js +8 -3
- package/dist/src/graph-export.js +1 -1
- package/dist/src/lattice-model.d.ts +77 -0
- package/dist/src/lattice-model.js +464 -0
- package/dist/src/lattice.d.ts +9 -104
- package/dist/src/lattice.js +69 -516
- package/dist/src/markdown-analysis-cache.d.ts +20 -0
- package/dist/src/markdown-analysis-cache.js +139 -0
- package/dist/src/markdown-analysis-worker.d.ts +16 -0
- package/dist/src/markdown-analysis-worker.js +33 -0
- package/dist/src/markdown-analysis.d.ts +50 -0
- package/dist/src/markdown-analysis.js +139 -0
- package/dist/src/markdown-analyzer-loader.d.ts +4 -0
- package/dist/src/markdown-analyzer-loader.js +13 -0
- package/dist/src/markdown-validation.d.ts +25 -0
- package/dist/src/markdown-validation.js +104 -0
- package/dist/src/mcp/server.js +27 -11
- package/dist/src/parser-cache.d.ts +17 -0
- package/dist/src/parser-cache.js +65 -0
- package/dist/src/parser-import.d.ts +8 -0
- package/dist/src/parser-import.js +1 -0
- package/dist/src/parser.js +80 -5
- package/dist/src/path.d.ts +2 -0
- package/dist/src/path.js +4 -0
- package/dist/src/profiler.d.ts +16 -0
- package/dist/src/profiler.js +81 -0
- package/dist/src/project-analysis.d.ts +59 -0
- package/dist/src/project-analysis.js +233 -0
- package/dist/src/project-discovery.d.ts +6 -0
- package/dist/src/project-discovery.js +30 -0
- package/dist/src/repository-path.d.ts +11 -0
- package/dist/src/repository-path.js +52 -0
- package/dist/src/search/db.d.ts +1 -1
- package/dist/src/search/db.js +2 -2
- package/dist/src/search/embedder.d.ts +2 -1
- package/dist/src/search/embedder.js +6 -5
- package/dist/src/search/index.d.ts +2 -1
- package/dist/src/search/index.js +10 -8
- package/dist/src/search/query.d.ts +20 -0
- package/dist/src/search/query.js +67 -0
- package/dist/src/search/search.d.ts +3 -1
- package/dist/src/search/search.js +5 -2
- package/dist/src/source-formats.d.ts +7 -0
- package/dist/src/source-formats.js +26 -0
- package/dist/src/source-parser.d.ts +44 -4
- package/dist/src/source-parser.js +494 -57
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/src/view/client/assets/MarkdownEditor-B7quqPb8.js +28 -0
- package/dist/src/view/client/assets/OrbitControls-DwcpQNja.js +1 -0
- package/dist/src/view/client/assets/STLLoader-CLWTKt5i.js +1 -0
- package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +1 -0
- package/dist/src/view/client/assets/arc-BnwR8Bu7.js +1 -0
- package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +36 -0
- package/dist/src/view/client/assets/blockDiagram-I7D4REHJ-DETl8e_H.js +129 -0
- package/dist/src/view/client/assets/c4Diagram-7LVT6UL2-DJRAT1Ve.js +38 -0
- package/dist/src/view/client/assets/channel-BwQbnWCN.js +1 -0
- package/dist/src/view/client/assets/chunk-2Q5K7J3B-C0TmOC1Q.js +1 -0
- package/dist/src/view/client/assets/chunk-5VM5RSS4-dfBOrYE6.js +15 -0
- package/dist/src/view/client/assets/chunk-F27PBJKO-DAnks99K.js +1 -0
- package/dist/src/view/client/assets/chunk-IMKFNOWR-CPjB07if.js +231 -0
- package/dist/src/view/client/assets/chunk-JWPE2WC7-VvU_gZI6.js +1 -0
- package/dist/src/view/client/assets/chunk-POPQ4Y6H-DDUbVF55.js +1 -0
- package/dist/src/view/client/assets/chunk-SVP7TREG-BHROkr6D.js +88 -0
- package/dist/src/view/client/assets/chunk-TICWLB2K-95PzGIOg.js +206 -0
- package/dist/src/view/client/assets/chunk-XXDRQBXY-MdmuLhIs.js +1 -0
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +1 -0
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +1 -0
- package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +1 -0
- package/dist/src/view/client/assets/cynefin-OW5HDTMX-CeA8IKWo.js +166 -0
- package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +62 -0
- package/dist/src/view/client/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/dist/src/view/client/assets/dagre-GXQ25YYZ-DLQwTa3H.js +4 -0
- package/dist/src/view/client/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/src/view/client/assets/diagram-S7CK7UJ4-kbKdIyXG.js +30 -0
- package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +41 -0
- package/dist/src/view/client/assets/diagram-VSXAHHWV-EXJTU8qq.js +3 -0
- package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +24 -0
- package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +24 -0
- package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +1 -0
- package/dist/src/view/client/assets/erDiagram-RLTQ6QDP-C925J5A3.js +99 -0
- package/dist/src/view/client/assets/flowDiagram-HODETNUW-DzoCa-Nq.js +168 -0
- package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +292 -0
- package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +106 -0
- package/dist/src/view/client/assets/index-BnENo9bV.js +1 -0
- package/dist/src/view/client/assets/index-D0ajr_Ja.css +1 -0
- package/dist/src/view/client/assets/index-KZcnpXJH.js +321 -0
- package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +2 -0
- package/dist/src/view/client/assets/init-Gi6I4Gst.js +1 -0
- package/dist/src/view/client/assets/ishikawaDiagram-5VMMS53U-CW4mto2d.js +70 -0
- package/dist/src/view/client/assets/journeyDiagram-3NMN7TZE-8R4eacFW.js +139 -0
- package/dist/src/view/client/assets/kanban-definition-UXKFOSKX-Cy9ioXEJ.js +89 -0
- package/dist/src/view/client/assets/katex-HP8lGamR.js +257 -0
- package/dist/src/view/client/assets/layout-C0MFuhth.js +1 -0
- package/dist/src/view/client/assets/linear-Dv3N4dQR.js +1 -0
- package/dist/src/view/client/assets/logo-small-RR_KBOAT.svg +149 -0
- package/dist/src/view/client/assets/maplibre-gl-DnqZgfUS.js +800 -0
- package/dist/src/view/client/assets/mermaid.core-bds9A7e2.js +310 -0
- package/dist/src/view/client/assets/mindmap-definition-YA3MSWOX-DjmGW355.js +96 -0
- package/dist/src/view/client/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +1 -0
- package/dist/src/view/client/assets/pieDiagram-E7YTZNPT-CCH2gGR-.js +39 -0
- package/dist/src/view/client/assets/quadrantDiagram-AXDQQJYC-CjocPJFz.js +7 -0
- package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +1 -0
- package/dist/src/view/client/assets/requirementDiagram-BXWQKSXE-Ct6AoXRr.js +84 -0
- package/dist/src/view/client/assets/sankeyDiagram-P5KCCOFB-CIpgGN_w.js +40 -0
- package/dist/src/view/client/assets/sequenceDiagram-WJ2MYXX4-DrmLFj6w.js +162 -0
- package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +1 -0
- package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +1 -0
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +8 -0
- package/dist/src/view/client/assets/three.module-PDSP0dbZ.js +4108 -0
- package/dist/src/view/client/assets/timeline-definition-24CTP7MA-BExPu6X5.js +120 -0
- package/dist/src/view/client/assets/vennDiagram-4TSXK5OY-PC1MU3si.js +34 -0
- package/dist/src/view/client/assets/wardleyDiagram-VM6X3IG4-DNWqU3fa.js +78 -0
- package/dist/src/view/client/assets/xychartDiagram-S5SC5T6Z-E1uA28_D.js +7 -0
- package/dist/src/view/client/index.html +2 -2
- package/dist/src/view/client-shell.d.ts +2 -0
- package/dist/src/view/client-shell.js +5 -0
- package/dist/src/view/diagnostics.d.ts +3 -2
- package/dist/src/view/diagnostics.js +49 -58
- package/dist/src/view/document-edit.d.ts +8 -0
- package/dist/src/view/document-edit.js +25 -0
- package/dist/src/view/document-route.d.ts +12 -0
- package/dist/src/view/document-route.js +93 -0
- package/dist/src/view/document-tree.d.ts +18 -0
- package/dist/src/view/document-tree.js +139 -0
- package/dist/src/view/external-document-tree.d.ts +3 -0
- package/dist/src/view/external-document-tree.js +674 -0
- package/dist/src/view/git-diff.js +187 -0
- package/dist/src/view/git.js +1 -1
- package/dist/src/view/graph.d.ts +3 -2
- package/dist/src/view/graph.js +94 -7
- package/dist/src/view/highlight.d.ts +10 -2
- package/dist/src/view/highlight.js +4540 -1360
- package/dist/src/view/markdown.d.ts +3 -2
- package/dist/src/view/markdown.js +214 -53
- package/dist/src/view/protocol.d.ts +59 -6
- package/dist/src/view/references.d.ts +10 -25
- package/dist/src/view/references.js +117 -107
- package/dist/src/view/repository.d.ts +13 -3
- package/dist/src/view/repository.js +234 -15
- package/dist/src/view/search.d.ts +1 -1
- package/dist/src/view/search.js +3 -7
- package/dist/src/view/server.d.ts +11 -2
- package/dist/src/view/server.js +212 -70
- package/dist/src/view/source-target.js +5 -8
- package/dist/src/view/static-build.d.ts +1 -0
- package/dist/src/view/static-build.js +245 -42
- package/dist/src/view/static-protocol.d.ts +19 -2
- package/dist/src/view/store.d.ts +16 -3
- package/dist/src/view/store.js +228 -36
- package/dist/src/view/table-of-contents.d.ts +2 -2
- package/dist/src/view/table-of-contents.js +2 -22
- package/dist/src/walk.d.ts +5 -3
- package/dist/src/walk.js +102 -8
- package/dist/vendor/lat.md-server/cli.d.ts +2 -0
- package/dist/vendor/lat.md-server/cli.js +12 -0
- package/dist/vendor/lat.md-server/index.d.ts +38 -0
- package/dist/vendor/lat.md-server/index.js +128 -0
- package/package.json +52 -12
- package/plugins/lat-md/README.md +61 -0
- package/plugins/lat-md/hooks.json +38 -0
- package/plugins/lat-md/plugin.json +9 -0
- package/plugins/lat-md/rules/AGENTS.md +118 -0
- package/plugins/lat-md/skills/lat-md/SKILL.md +178 -0
- package/plugins/lat-md/skills/lat-md-conventions/SKILL.md +182 -0
- package/templates/AGENTS.md +15 -3
- package/templates/cursor-rules.md +12 -3
- package/templates/skill/SKILL.md +22 -1
- package/dist/src/view/client/assets/index-C3iaZyYT.css +0 -1
- package/dist/src/view/client/assets/index-KWi67IMz.js +0 -334
- package/dist/src/view/client/assets/logo-CNF8zs47.svg +0 -182
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, join, relative } from 'node:path';
|
|
4
|
+
import { toPosix } from './path.js';
|
|
5
|
+
/** Version of persistent parser outputs and their shared on-disk contract. */
|
|
6
|
+
export const PARSER_CACHE_VERSION = 1;
|
|
7
|
+
/** Hash complete parser input bytes for cache invalidation. */
|
|
8
|
+
export function hashParserContent(content) {
|
|
9
|
+
return createHash('sha1').update(content).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
/** Normalize an on-disk file into its project-relative parser identity. */
|
|
12
|
+
export function parserCacheIdentity(absolutePath, projectRoot) {
|
|
13
|
+
return toPosix(relative(projectRoot, absolutePath)).normalize('NFC');
|
|
14
|
+
}
|
|
15
|
+
function readablePath(identity) {
|
|
16
|
+
const normalized = identity
|
|
17
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
18
|
+
.replace(/^_+|_+$/g, '')
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
return (normalized || 'parsed').slice(-120);
|
|
21
|
+
}
|
|
22
|
+
function cacheShard(identity) {
|
|
23
|
+
const shortName = basename(identity);
|
|
24
|
+
const stem = shortName.slice(0, shortName.length - extname(shortName).length);
|
|
25
|
+
const firstTwo = [...stem.toLowerCase()].slice(0, 2).join('');
|
|
26
|
+
return firstTwo.replace(/[^a-z0-9]/g, '_') || '_';
|
|
27
|
+
}
|
|
28
|
+
/** Return the collision-safe, short-name-sharded cache path for an identity. */
|
|
29
|
+
export function parsedCachePath(latDir, identity) {
|
|
30
|
+
const normalizedIdentity = toPosix(identity).normalize('NFC');
|
|
31
|
+
const digest = hashParserContent(normalizedIdentity);
|
|
32
|
+
return join(latDir, '.cache', 'parsed', cacheShard(normalizedIdentity), `${digest}_${readablePath(normalizedIdentity)}`);
|
|
33
|
+
}
|
|
34
|
+
/** Read and decode a cache entry, treating every malformed state as a miss. */
|
|
35
|
+
export async function readParsedCache(path) {
|
|
36
|
+
try {
|
|
37
|
+
const serialized = await readFile(path, 'utf8');
|
|
38
|
+
const newline = serialized.indexOf('\n');
|
|
39
|
+
if (newline < 0)
|
|
40
|
+
return null;
|
|
41
|
+
const match = /^v(\d+):([a-f0-9]{40})$/.exec(serialized.slice(0, newline).trim());
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
version: Number(match[1]),
|
|
46
|
+
contentHash: match[2],
|
|
47
|
+
value: JSON.parse(serialized.slice(newline + 1)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Atomically publish one disposable parser cache entry. */
|
|
55
|
+
export async function writeParsedCache(path, contentHash, value) {
|
|
56
|
+
await mkdir(dirname(path), { recursive: true });
|
|
57
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
await writeFile(temp, `v${PARSER_CACHE_VERSION}:${contentHash}\n${JSON.stringify(value)}\n`);
|
|
60
|
+
await rename(temp, path);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await rm(temp, { force: true }).catch(() => { });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** One observable decision to load or skip a lazily imported parser module. */
|
|
2
|
+
export type ParserImportEvent = {
|
|
3
|
+
parser: 'AsciiDoc parser' | 'Markdown analyzer' | 'reStructuredText parser';
|
|
4
|
+
imported: boolean;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
detail?: string;
|
|
7
|
+
};
|
|
8
|
+
export type ParserImportObserver = (event: ParserImportEvent) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/parser.js
CHANGED
|
@@ -2,17 +2,92 @@ import { unified } from 'unified';
|
|
|
2
2
|
import remarkParse from 'remark-parse';
|
|
3
3
|
import remarkStringify from 'remark-stringify';
|
|
4
4
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
5
|
+
import remarkMath from 'remark-math';
|
|
6
|
+
import { gfmAutolinkLiteralFromMarkdown } from 'mdast-util-gfm-autolink-literal';
|
|
7
|
+
import { gfmFootnoteFromMarkdown, gfmFootnoteToMarkdown, } from 'mdast-util-gfm-footnote';
|
|
8
|
+
import { gfmStrikethroughFromMarkdown, gfmStrikethroughToMarkdown, } from 'mdast-util-gfm-strikethrough';
|
|
9
|
+
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
|
10
|
+
import { gfmTaskListItemFromMarkdown, gfmTaskListItemToMarkdown, } from 'mdast-util-gfm-task-list-item';
|
|
11
|
+
import { gfmAutolinkLiteral } from 'micromark-extension-gfm-autolink-literal';
|
|
12
|
+
import { gfmFootnote } from 'micromark-extension-gfm-footnote';
|
|
13
|
+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough';
|
|
14
|
+
import { gfmTable } from 'micromark-extension-gfm-table';
|
|
15
|
+
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item';
|
|
16
|
+
import { visit } from 'unist-util-visit';
|
|
17
|
+
import { alertMarkerToMarkdown, markAlertMarkers, } from './extensions/alert-marker.js';
|
|
5
18
|
import { wikiLinkSyntax, wikiLinkFromMarkdown, wikiLinkToMarkdown, } from './extensions/wiki-link/index.js';
|
|
6
19
|
const processor = unified()
|
|
7
20
|
.use(remarkParse)
|
|
8
21
|
.use(remarkFrontmatter)
|
|
22
|
+
.use(remarkMath)
|
|
9
23
|
.use(remarkStringify)
|
|
10
|
-
.data('micromarkExtensions', [
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
.data('micromarkExtensions', [
|
|
25
|
+
gfmAutolinkLiteral(),
|
|
26
|
+
gfmFootnote(),
|
|
27
|
+
gfmStrikethrough(),
|
|
28
|
+
gfmTable(),
|
|
29
|
+
gfmTaskListItem(),
|
|
30
|
+
wikiLinkSyntax(),
|
|
31
|
+
])
|
|
32
|
+
.data('fromMarkdownExtensions', [
|
|
33
|
+
gfmAutolinkLiteralFromMarkdown(),
|
|
34
|
+
gfmFootnoteFromMarkdown(),
|
|
35
|
+
gfmStrikethroughFromMarkdown(),
|
|
36
|
+
gfmTableFromMarkdown(),
|
|
37
|
+
gfmTaskListItemFromMarkdown(),
|
|
38
|
+
wikiLinkFromMarkdown(),
|
|
39
|
+
])
|
|
40
|
+
.data('toMarkdownExtensions', [
|
|
41
|
+
alertMarkerToMarkdown(),
|
|
42
|
+
gfmFootnoteToMarkdown(),
|
|
43
|
+
gfmStrikethroughToMarkdown(),
|
|
44
|
+
gfmTableToMarkdown(),
|
|
45
|
+
gfmTaskListItemToMarkdown(),
|
|
46
|
+
wikiLinkToMarkdown(),
|
|
47
|
+
]);
|
|
48
|
+
const BARE_AUTOLINK_DATA_KEY = 'latBareAutolink';
|
|
49
|
+
function bareAutolinkText(node) {
|
|
50
|
+
if (node.children.length !== 1 || node.children[0].type !== 'text') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const text = node.children[0].value;
|
|
54
|
+
if (node.url !== text &&
|
|
55
|
+
node.url !== `http://${text}` &&
|
|
56
|
+
node.url !== `mailto:${text}`) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
13
61
|
export function parse(markdown) {
|
|
14
|
-
|
|
62
|
+
const tree = processor.parse(markdown);
|
|
63
|
+
markAlertMarkers(tree);
|
|
64
|
+
visit(tree, 'link', (node) => {
|
|
65
|
+
const text = bareAutolinkText(node);
|
|
66
|
+
const sourceLength = node.position?.end.offset !== undefined &&
|
|
67
|
+
node.position.start.offset !== undefined
|
|
68
|
+
? node.position.end.offset - node.position.start.offset
|
|
69
|
+
: null;
|
|
70
|
+
if (text === null || sourceLength !== text.length)
|
|
71
|
+
return;
|
|
72
|
+
node.data = {
|
|
73
|
+
...node.data,
|
|
74
|
+
[BARE_AUTOLINK_DATA_KEY]: true,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
return tree;
|
|
15
78
|
}
|
|
16
79
|
export function toMarkdown(tree) {
|
|
17
|
-
|
|
80
|
+
const serializable = structuredClone(tree);
|
|
81
|
+
visit(serializable, 'link', (node, index, parent) => {
|
|
82
|
+
if (index === undefined ||
|
|
83
|
+
!parent ||
|
|
84
|
+
node.data?.[BARE_AUTOLINK_DATA_KEY] !== true) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const text = bareAutolinkText(node);
|
|
88
|
+
if (text === null)
|
|
89
|
+
return;
|
|
90
|
+
parent.children[index] = { type: 'text', value: text };
|
|
91
|
+
});
|
|
92
|
+
return processor.stringify(serializable);
|
|
18
93
|
}
|
package/dist/src/path.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Profiler = {
|
|
2
|
+
time<T>(label: string, work: () => Promise<T>, detail?: string): Promise<T>;
|
|
3
|
+
timeSync<T>(label: string, work: () => T, detail?: string): T;
|
|
4
|
+
record(label: string, durationMs: number, detail?: string): void;
|
|
5
|
+
};
|
|
6
|
+
export declare class TimingProfiler implements Profiler {
|
|
7
|
+
private readonly root;
|
|
8
|
+
private readonly activeNode;
|
|
9
|
+
private node;
|
|
10
|
+
time<T>(label: string, work: () => Promise<T>, detail?: string): Promise<T>;
|
|
11
|
+
timeSync<T>(label: string, work: () => T, detail?: string): T;
|
|
12
|
+
record(label: string, durationMs: number, detail?: string): void;
|
|
13
|
+
private recordNode;
|
|
14
|
+
format(totalMs: number): string[];
|
|
15
|
+
private formatNode;
|
|
16
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
export class TimingProfiler {
|
|
4
|
+
root = {
|
|
5
|
+
label: '',
|
|
6
|
+
calls: 0,
|
|
7
|
+
totalMs: 0,
|
|
8
|
+
maxMs: 0,
|
|
9
|
+
children: new Map(),
|
|
10
|
+
};
|
|
11
|
+
activeNode = new AsyncLocalStorage();
|
|
12
|
+
node(label) {
|
|
13
|
+
const parent = this.activeNode.getStore() ?? this.root;
|
|
14
|
+
let node = parent.children.get(label);
|
|
15
|
+
if (!node) {
|
|
16
|
+
node = {
|
|
17
|
+
label,
|
|
18
|
+
calls: 0,
|
|
19
|
+
totalMs: 0,
|
|
20
|
+
maxMs: 0,
|
|
21
|
+
children: new Map(),
|
|
22
|
+
};
|
|
23
|
+
parent.children.set(label, node);
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
async time(label, work, detail) {
|
|
28
|
+
const node = this.node(label);
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
try {
|
|
31
|
+
return await this.activeNode.run(node, work);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
this.recordNode(node, performance.now() - start, detail);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
timeSync(label, work, detail) {
|
|
38
|
+
const node = this.node(label);
|
|
39
|
+
const start = performance.now();
|
|
40
|
+
try {
|
|
41
|
+
return this.activeNode.run(node, work);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
this.recordNode(node, performance.now() - start, detail);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
record(label, durationMs, detail) {
|
|
48
|
+
this.recordNode(this.node(label), durationMs, detail);
|
|
49
|
+
}
|
|
50
|
+
recordNode(node, durationMs, detail) {
|
|
51
|
+
node.calls++;
|
|
52
|
+
node.totalMs += durationMs;
|
|
53
|
+
if (durationMs > node.maxMs) {
|
|
54
|
+
node.maxMs = durationMs;
|
|
55
|
+
node.maxDetail = detail;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
format(totalMs) {
|
|
59
|
+
const lines = [`Profile (${totalMs.toFixed(1)}ms total):`];
|
|
60
|
+
for (const node of this.root.children.values()) {
|
|
61
|
+
this.formatNode(node, 1, lines);
|
|
62
|
+
}
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
formatNode(node, depth, lines) {
|
|
66
|
+
const indent = ' '.repeat(depth);
|
|
67
|
+
let summary = `${indent}${node.label}: ${node.totalMs.toFixed(1)}ms`;
|
|
68
|
+
if (node.calls > 1) {
|
|
69
|
+
summary +=
|
|
70
|
+
` total across ${node.calls} calls` +
|
|
71
|
+
` (${(node.totalMs / node.calls).toFixed(1)}ms avg,` +
|
|
72
|
+
` ${node.maxMs.toFixed(1)}ms max` +
|
|
73
|
+
(node.maxDetail ? `: ${node.maxDetail}` : '') +
|
|
74
|
+
')';
|
|
75
|
+
}
|
|
76
|
+
lines.push(summary);
|
|
77
|
+
for (const child of node.children.values()) {
|
|
78
|
+
this.formatNode(child, depth + 1, lines);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Ref, type Section, type SectionSlugIndex } from './lattice-model.js';
|
|
2
|
+
import type { MarkdownFileAnalysis } from './markdown-analysis.js';
|
|
3
|
+
import { type ScanResult } from './code-refs.js';
|
|
4
|
+
import { type ResolveSourceSymbolOptions } from './source-parser.js';
|
|
5
|
+
import { type ExternalResolver } from './external-sources.js';
|
|
6
|
+
import type { ParserImportObserver } from './parser-import.js';
|
|
7
|
+
export type MarkdownAnalysisExecutor = 'inline' | 'workers' | 'auto';
|
|
8
|
+
export type MarkdownProjectAnalysis = {
|
|
9
|
+
latDir: string;
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
entries: string[];
|
|
12
|
+
markdownFiles: string[];
|
|
13
|
+
files: ReadonlyMap<string, MarkdownFileAnalysis>;
|
|
14
|
+
filesByAbsolutePath: ReadonlyMap<string, MarkdownFileAnalysis>;
|
|
15
|
+
allSections: Section[];
|
|
16
|
+
sections: Section[];
|
|
17
|
+
sectionById: ReadonlyMap<string, Section>;
|
|
18
|
+
sectionIds: ReadonlySet<string>;
|
|
19
|
+
fileIndex: Map<string, string[]>;
|
|
20
|
+
slugIndex: SectionSlugIndex;
|
|
21
|
+
wikiRefs: readonly Ref[];
|
|
22
|
+
outgoingRefsBySection: ReadonlyMap<string, readonly Ref[]>;
|
|
23
|
+
refsByTarget: ReadonlyMap<string, readonly Ref[]>;
|
|
24
|
+
incomingRefsBySection: ReadonlyMap<string, readonly Ref[]>;
|
|
25
|
+
};
|
|
26
|
+
export type AnalyzeMarkdownProjectOptions = {
|
|
27
|
+
executor?: MarkdownAnalysisExecutor;
|
|
28
|
+
maxWorkers?: number;
|
|
29
|
+
cache?: boolean;
|
|
30
|
+
onFileAnalyzed?: (analysis: MarkdownFileAnalysis) => void;
|
|
31
|
+
onParserImport?: ParserImportObserver;
|
|
32
|
+
};
|
|
33
|
+
/** Read and analyze every Markdown file into one immutable command snapshot. */
|
|
34
|
+
export declare function analyzeMarkdownProject(latDir: string, projectRoot: string, options?: AnalyzeMarkdownProjectOptions): Promise<MarkdownProjectAnalysis>;
|
|
35
|
+
/** Lazy request-scoped owner for one immutable project analysis. */
|
|
36
|
+
export declare class MarkdownProjectSession {
|
|
37
|
+
readonly latDir: string;
|
|
38
|
+
readonly projectRoot: string;
|
|
39
|
+
readonly options: AnalyzeMarkdownProjectOptions;
|
|
40
|
+
private analysisPromise?;
|
|
41
|
+
private codeRefsPromise?;
|
|
42
|
+
private externalPromise?;
|
|
43
|
+
private readonly sourceParserRuntime;
|
|
44
|
+
constructor(latDir: string, projectRoot: string, options?: AnalyzeMarkdownProjectOptions);
|
|
45
|
+
analysis(): Promise<MarkdownProjectAnalysis>;
|
|
46
|
+
codeRefs(): Promise<ScanResult>;
|
|
47
|
+
external(): Promise<ExternalResolver>;
|
|
48
|
+
sourceSymbolOptions(): ResolveSourceSymbolOptions;
|
|
49
|
+
}
|
|
50
|
+
type AnalysisContext = {
|
|
51
|
+
latDir: string;
|
|
52
|
+
projectRoot: string;
|
|
53
|
+
analysis?: MarkdownProjectSession;
|
|
54
|
+
};
|
|
55
|
+
/** Reuse one lazy project snapshot throughout a CLI or MCP request. */
|
|
56
|
+
export declare function commandProjectAnalysis(context: AnalysisContext): Promise<MarkdownProjectAnalysis>;
|
|
57
|
+
/** Obtain the complete lazy request session for nested semantic operations. */
|
|
58
|
+
export declare function commandProjectSession(context: AnalysisContext): MarkdownProjectSession;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { availableParallelism } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { buildFileIndex, buildSectionSlugIndex, flattenSections, resolveRef, } from './lattice-model.js';
|
|
5
|
+
import { prepareMarkdownAnalysis, publishMarkdownAnalysis, } from './markdown-analysis-cache.js';
|
|
6
|
+
import { walkEntries } from './walk.js';
|
|
7
|
+
import { scanCodeRefs } from './code-refs.js';
|
|
8
|
+
import { SourceParserRuntime, } from './source-parser.js';
|
|
9
|
+
import { createExternalResolver, } from './external-sources.js';
|
|
10
|
+
import { loadMarkdownAnalyzer } from './markdown-analyzer-loader.js';
|
|
11
|
+
async function analyzeInline(files, latDir, projectRoot, onParserImport) {
|
|
12
|
+
if (files.length === 0)
|
|
13
|
+
return [];
|
|
14
|
+
const analyzeMarkdownFile = await loadMarkdownAnalyzer(onParserImport, 'main thread');
|
|
15
|
+
return Promise.all(files.map((file) => publishMarkdownAnalysis(file, analyzeMarkdownFile(file.absolutePath, file.content, latDir, projectRoot))));
|
|
16
|
+
}
|
|
17
|
+
function runWorkerTask(worker, task) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const onMessage = (response) => {
|
|
20
|
+
cleanup();
|
|
21
|
+
if ('error' in response) {
|
|
22
|
+
reject(new Error(`Failed to analyze ${task.absolutePath}: ${response.error}`));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
resolve(response);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const onError = (error) => {
|
|
29
|
+
cleanup();
|
|
30
|
+
reject(error);
|
|
31
|
+
};
|
|
32
|
+
const onExit = (code) => {
|
|
33
|
+
cleanup();
|
|
34
|
+
reject(new Error(`Markdown analysis worker exited with code ${code} while reading ${task.absolutePath}`));
|
|
35
|
+
};
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
worker.off('message', onMessage);
|
|
38
|
+
worker.off('error', onError);
|
|
39
|
+
worker.off('exit', onExit);
|
|
40
|
+
};
|
|
41
|
+
worker.once('message', onMessage);
|
|
42
|
+
worker.once('error', onError);
|
|
43
|
+
worker.once('exit', onExit);
|
|
44
|
+
worker.postMessage(task);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function analyzeWithWorkers(files, latDir, projectRoot, maxWorkers, onParserImport) {
|
|
48
|
+
const workerCount = Math.max(1, Math.min(files.length, maxWorkers ?? 10, Math.max(1, availableParallelism() - 1)));
|
|
49
|
+
const workers = Array.from({ length: workerCount }, () => {
|
|
50
|
+
const sourceRuntime = import.meta.url.endsWith('.ts');
|
|
51
|
+
return new Worker(new URL(sourceRuntime
|
|
52
|
+
? './markdown-analysis-worker.ts'
|
|
53
|
+
: './markdown-analysis-worker.js', import.meta.url), {
|
|
54
|
+
execArgv: sourceRuntime
|
|
55
|
+
? ['--import', 'tsx']
|
|
56
|
+
: process.execArgv.filter((argument) => !argument.startsWith('--input-type')),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const analyses = new Array(files.length);
|
|
60
|
+
let next = 0;
|
|
61
|
+
try {
|
|
62
|
+
await Promise.all(workers.map(async (worker, workerIndex) => {
|
|
63
|
+
while (true) {
|
|
64
|
+
const id = next++;
|
|
65
|
+
if (id >= files.length)
|
|
66
|
+
return;
|
|
67
|
+
const file = files[id];
|
|
68
|
+
const response = await runWorkerTask(worker, {
|
|
69
|
+
id,
|
|
70
|
+
absolutePath: file.absolutePath,
|
|
71
|
+
content: file.content,
|
|
72
|
+
latDir,
|
|
73
|
+
projectRoot,
|
|
74
|
+
});
|
|
75
|
+
analyses[id] = response.analysis;
|
|
76
|
+
if (response.importMs !== undefined) {
|
|
77
|
+
onParserImport?.({
|
|
78
|
+
parser: 'Markdown analyzer',
|
|
79
|
+
imported: true,
|
|
80
|
+
durationMs: response.importMs,
|
|
81
|
+
detail: `worker ${workerIndex + 1}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
return Promise.all(analyses.map((analysis, index) => publishMarkdownAnalysis(files[index], analysis)));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await Promise.all(workers.map((worker) => worker.terminate()));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Read and analyze every Markdown file into one immutable command snapshot. */
|
|
93
|
+
export async function analyzeMarkdownProject(latDir, projectRoot, options = {}) {
|
|
94
|
+
const entries = await walkEntries(latDir);
|
|
95
|
+
const markdownFiles = entries
|
|
96
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
97
|
+
.sort()
|
|
98
|
+
.map((entry) => join(latDir, entry));
|
|
99
|
+
const prepared = await Promise.all(markdownFiles.map((absolutePath) => prepareMarkdownAnalysis(absolutePath, latDir, projectRoot, options.cache !== false)));
|
|
100
|
+
const analyses = new Array(markdownFiles.length);
|
|
101
|
+
const misses = [];
|
|
102
|
+
const missIndexes = [];
|
|
103
|
+
for (const [index, file] of prepared.entries()) {
|
|
104
|
+
if (file.analysis)
|
|
105
|
+
analyses[index] = file.analysis;
|
|
106
|
+
else {
|
|
107
|
+
misses.push(file);
|
|
108
|
+
missIndexes.push(index);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (misses.length === 0) {
|
|
112
|
+
options.onParserImport?.({
|
|
113
|
+
parser: 'Markdown analyzer',
|
|
114
|
+
imported: false,
|
|
115
|
+
durationMs: 0,
|
|
116
|
+
detail: prepared.length === 0
|
|
117
|
+
? 'no Markdown files'
|
|
118
|
+
: `all ${prepared.length} Markdown files cached`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const executor = options.executor === 'auto' || options.executor === undefined
|
|
122
|
+
? misses.length >= 8
|
|
123
|
+
? 'workers'
|
|
124
|
+
: 'inline'
|
|
125
|
+
: options.executor;
|
|
126
|
+
const parsed = executor === 'workers' && misses.length > 1
|
|
127
|
+
? await analyzeWithWorkers(misses, latDir, projectRoot, options.maxWorkers, options.onParserImport)
|
|
128
|
+
: await analyzeInline(misses, latDir, projectRoot, options.onParserImport);
|
|
129
|
+
for (const [index, analysis] of parsed.entries()) {
|
|
130
|
+
analyses[missIndexes[index]] = analysis;
|
|
131
|
+
}
|
|
132
|
+
const files = new Map();
|
|
133
|
+
const filesByAbsolutePath = new Map();
|
|
134
|
+
for (const analysis of analyses) {
|
|
135
|
+
files.set(analysis.path, analysis);
|
|
136
|
+
filesByAbsolutePath.set(analysis.absolutePath, analysis);
|
|
137
|
+
options.onFileAnalyzed?.(analysis);
|
|
138
|
+
}
|
|
139
|
+
const allSections = analyses.flatMap((analysis) => analysis.sections);
|
|
140
|
+
const sections = flattenSections(allSections);
|
|
141
|
+
const sectionById = new Map(sections.map((section) => [section.id.toLowerCase(), section]));
|
|
142
|
+
const sectionIds = new Set(sectionById.keys());
|
|
143
|
+
const fileIndex = buildFileIndex(allSections);
|
|
144
|
+
const slugIndex = buildSectionSlugIndex(allSections);
|
|
145
|
+
const wikiRefs = analyses.flatMap((analysis) => analysis.wikiRefs);
|
|
146
|
+
const outgoingRefsBySection = new Map();
|
|
147
|
+
const refsByTarget = new Map();
|
|
148
|
+
const incomingRefsBySection = new Map();
|
|
149
|
+
const append = (index, key, ref) => {
|
|
150
|
+
const refs = index.get(key);
|
|
151
|
+
if (refs)
|
|
152
|
+
refs.push(ref);
|
|
153
|
+
else
|
|
154
|
+
index.set(key, [ref]);
|
|
155
|
+
};
|
|
156
|
+
for (const ref of wikiRefs) {
|
|
157
|
+
const from = ref.fromSection.toLowerCase();
|
|
158
|
+
const target = ref.target.toLowerCase();
|
|
159
|
+
append(outgoingRefsBySection, from, ref);
|
|
160
|
+
append(refsByTarget, target, ref);
|
|
161
|
+
const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
|
|
162
|
+
if (!resolved.ambiguous) {
|
|
163
|
+
const resolvedId = resolved.resolved.toLowerCase();
|
|
164
|
+
if (sectionIds.has(resolvedId)) {
|
|
165
|
+
append(incomingRefsBySection, resolvedId, ref);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
latDir,
|
|
171
|
+
projectRoot,
|
|
172
|
+
entries,
|
|
173
|
+
markdownFiles,
|
|
174
|
+
files,
|
|
175
|
+
filesByAbsolutePath,
|
|
176
|
+
allSections,
|
|
177
|
+
sections,
|
|
178
|
+
sectionById,
|
|
179
|
+
sectionIds,
|
|
180
|
+
fileIndex,
|
|
181
|
+
slugIndex,
|
|
182
|
+
wikiRefs,
|
|
183
|
+
outgoingRefsBySection,
|
|
184
|
+
refsByTarget,
|
|
185
|
+
incomingRefsBySection,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Lazy request-scoped owner for one immutable project analysis. */
|
|
189
|
+
export class MarkdownProjectSession {
|
|
190
|
+
latDir;
|
|
191
|
+
projectRoot;
|
|
192
|
+
options;
|
|
193
|
+
analysisPromise;
|
|
194
|
+
codeRefsPromise;
|
|
195
|
+
externalPromise;
|
|
196
|
+
sourceParserRuntime = new SourceParserRuntime();
|
|
197
|
+
constructor(latDir, projectRoot, options = {}) {
|
|
198
|
+
this.latDir = latDir;
|
|
199
|
+
this.projectRoot = projectRoot;
|
|
200
|
+
this.options = options;
|
|
201
|
+
}
|
|
202
|
+
analysis() {
|
|
203
|
+
this.analysisPromise ??= analyzeMarkdownProject(this.latDir, this.projectRoot, this.options);
|
|
204
|
+
return this.analysisPromise;
|
|
205
|
+
}
|
|
206
|
+
codeRefs() {
|
|
207
|
+
this.codeRefsPromise ??= scanCodeRefs(this.projectRoot);
|
|
208
|
+
return this.codeRefsPromise;
|
|
209
|
+
}
|
|
210
|
+
external() {
|
|
211
|
+
this.externalPromise ??= (async () => {
|
|
212
|
+
const resolver = await createExternalResolver(this.latDir, this.projectRoot);
|
|
213
|
+
await resolver.reconcile();
|
|
214
|
+
return resolver;
|
|
215
|
+
})();
|
|
216
|
+
return this.externalPromise;
|
|
217
|
+
}
|
|
218
|
+
sourceSymbolOptions() {
|
|
219
|
+
return {
|
|
220
|
+
latDir: this.latDir,
|
|
221
|
+
runtime: this.sourceParserRuntime,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Reuse one lazy project snapshot throughout a CLI or MCP request. */
|
|
226
|
+
export function commandProjectAnalysis(context) {
|
|
227
|
+
return commandProjectSession(context).analysis();
|
|
228
|
+
}
|
|
229
|
+
/** Obtain the complete lazy request session for nested semantic operations. */
|
|
230
|
+
export function commandProjectSession(context) {
|
|
231
|
+
context.analysis ??= new MarkdownProjectSession(context.latDir, context.projectRoot);
|
|
232
|
+
return context.analysis;
|
|
233
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Find the nearest ancestor containing the project's lat.md directory. */
|
|
2
|
+
export declare function findLatticeDir(from?: string): string | null;
|
|
3
|
+
/** Find the root of the nearest project containing a lat.md directory. */
|
|
4
|
+
export declare function findProjectRoot(from?: string): string | null;
|
|
5
|
+
/** List project Markdown files without loading the Markdown parser. */
|
|
6
|
+
export declare function listLatticeFiles(latticeDir: string): Promise<string[]>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
/** Find the nearest ancestor containing the project's lat.md directory. */
|
|
4
|
+
export function findLatticeDir(from) {
|
|
5
|
+
let dir = resolve(from ?? process.cwd());
|
|
6
|
+
while (true) {
|
|
7
|
+
const candidate = join(dir, 'lat.md');
|
|
8
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
9
|
+
return candidate;
|
|
10
|
+
}
|
|
11
|
+
const parent = dirname(dir);
|
|
12
|
+
if (parent === dir)
|
|
13
|
+
return null;
|
|
14
|
+
dir = parent;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Find the root of the nearest project containing a lat.md directory. */
|
|
18
|
+
export function findProjectRoot(from) {
|
|
19
|
+
const latDir = findLatticeDir(from);
|
|
20
|
+
return latDir ? dirname(latDir) : null;
|
|
21
|
+
}
|
|
22
|
+
/** List project Markdown files without loading the Markdown parser. */
|
|
23
|
+
export async function listLatticeFiles(latticeDir) {
|
|
24
|
+
const { walkEntries } = await import('./walk.js');
|
|
25
|
+
const entries = await walkEntries(latticeDir);
|
|
26
|
+
return entries
|
|
27
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
28
|
+
.sort()
|
|
29
|
+
.map((entry) => join(latticeDir, entry));
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RepositoryPathKind = 'directory' | 'file' | 'other';
|
|
2
|
+
export type RepositoryPathInspection = {
|
|
3
|
+
kind: RepositoryPathKind;
|
|
4
|
+
realPath: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'missing' | 'outside';
|
|
7
|
+
};
|
|
8
|
+
/** Normalize a portable project-root-relative path without allowing escapes. */
|
|
9
|
+
export declare function normalizeRepositoryPath(authoredPath: string): string | null;
|
|
10
|
+
/** Inspect an existing path after resolving symlinks beneath the project root. */
|
|
11
|
+
export declare function inspectRepositoryPath(projectRoot: string, path: string): Promise<RepositoryPathInspection>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, posix, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { toPosix } from './path.js';
|
|
4
|
+
/** Normalize a portable project-root-relative path without allowing escapes. */
|
|
5
|
+
export function normalizeRepositoryPath(authoredPath) {
|
|
6
|
+
if (!authoredPath || authoredPath.includes('\0'))
|
|
7
|
+
return null;
|
|
8
|
+
const portable = toPosix(authoredPath);
|
|
9
|
+
if (posix.isAbsolute(portable) || /^[a-z]:\//i.test(portable))
|
|
10
|
+
return null;
|
|
11
|
+
const normalized = posix.normalize(portable).replace(/^\.\//, '');
|
|
12
|
+
if (normalized === '..' ||
|
|
13
|
+
normalized.startsWith('../') ||
|
|
14
|
+
normalized.includes('\0')) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
function isInside(root, candidate) {
|
|
20
|
+
const path = relative(root, candidate);
|
|
21
|
+
return (path === '' ||
|
|
22
|
+
(!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`)));
|
|
23
|
+
}
|
|
24
|
+
/** Inspect an existing path after resolving symlinks beneath the project root. */
|
|
25
|
+
export async function inspectRepositoryPath(projectRoot, path) {
|
|
26
|
+
const candidate = resolve(projectRoot, ...path.split('/'));
|
|
27
|
+
let realRoot;
|
|
28
|
+
let realPath;
|
|
29
|
+
try {
|
|
30
|
+
[realRoot, realPath] = await Promise.all([
|
|
31
|
+
realpath(projectRoot),
|
|
32
|
+
realpath(candidate),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return { kind: 'missing' };
|
|
37
|
+
}
|
|
38
|
+
if (!isInside(realRoot, realPath))
|
|
39
|
+
return { kind: 'outside' };
|
|
40
|
+
try {
|
|
41
|
+
const info = await stat(realPath);
|
|
42
|
+
const kind = info.isFile()
|
|
43
|
+
? 'file'
|
|
44
|
+
: info.isDirectory()
|
|
45
|
+
? 'directory'
|
|
46
|
+
: 'other';
|
|
47
|
+
return { kind, realPath };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { kind: 'missing' };
|
|
51
|
+
}
|
|
52
|
+
}
|