@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
package/dist/src/lattice.js
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname,
|
|
3
|
-
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, basename, relative } from 'node:path';
|
|
4
3
|
import GithubSlugger from 'github-slugger';
|
|
5
4
|
import { parse as parseYaml } from 'yaml';
|
|
6
5
|
import { parse } from './parser.js';
|
|
7
|
-
import { toPosix
|
|
6
|
+
import { toPosix } from './path.js';
|
|
8
7
|
import { visit } from 'unist-util-visit';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
export const LAT_FIELDS = [
|
|
16
|
-
'require-code-mention',
|
|
17
|
-
'mode',
|
|
18
|
-
'require-mode',
|
|
19
|
-
'status',
|
|
20
|
-
'reviewed-hash',
|
|
21
|
-
'tags',
|
|
22
|
-
];
|
|
8
|
+
import { listLatticeFiles } from './project-discovery.js';
|
|
9
|
+
import { LAT_FIELDS, } from './fork/frontmatter-fields.js';
|
|
10
|
+
export { buildFileIndex, buildSectionSlugIndex, findSections, flattenSections, resolveRef, } from './lattice-model.js';
|
|
11
|
+
export { findLatticeDir, findProjectRoot, listLatticeFiles, } from './project-discovery.js';
|
|
12
|
+
export { LAT_FIELDS } from './fork/frontmatter-fields.js';
|
|
23
13
|
export function parseFrontmatter(content) {
|
|
24
14
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
25
15
|
if (!match)
|
|
@@ -66,30 +56,6 @@ export function parseFrontmatter(content) {
|
|
|
66
56
|
result.problems = problems;
|
|
67
57
|
return result;
|
|
68
58
|
}
|
|
69
|
-
export function findLatticeDir(from) {
|
|
70
|
-
let dir = resolve(from ?? process.cwd());
|
|
71
|
-
while (true) {
|
|
72
|
-
const candidate = join(dir, 'lat.md');
|
|
73
|
-
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
74
|
-
return candidate;
|
|
75
|
-
}
|
|
76
|
-
const parent = dirname(dir);
|
|
77
|
-
if (parent === dir)
|
|
78
|
-
return null;
|
|
79
|
-
dir = parent;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
export function findProjectRoot(from) {
|
|
83
|
-
const latDir = findLatticeDir(from);
|
|
84
|
-
return latDir ? dirname(latDir) : null;
|
|
85
|
-
}
|
|
86
|
-
export async function listLatticeFiles(latticeDir) {
|
|
87
|
-
const entries = await walkEntries(latticeDir);
|
|
88
|
-
return entries
|
|
89
|
-
.filter((e) => e.endsWith('.md'))
|
|
90
|
-
.sort()
|
|
91
|
-
.map((e) => join(latticeDir, e));
|
|
92
|
-
}
|
|
93
59
|
function headingText(node) {
|
|
94
60
|
return node.children
|
|
95
61
|
.filter((c) => c.type === 'text')
|
|
@@ -207,477 +173,23 @@ export function parseSections(filePath, content, projectRoot, tree = parse(conte
|
|
|
207
173
|
}
|
|
208
174
|
return roots;
|
|
209
175
|
}
|
|
210
|
-
export async function loadAllSections(latticeDir, projectRoot = dirname(latticeDir)) {
|
|
211
|
-
const files =
|
|
176
|
+
export async function loadAllSections(latticeDir, projectRoot = dirname(latticeDir), profile) {
|
|
177
|
+
const files = profile
|
|
178
|
+
? await profile.time('list Markdown files', () => listLatticeFiles(latticeDir))
|
|
179
|
+
: await listLatticeFiles(latticeDir);
|
|
212
180
|
const all = [];
|
|
213
181
|
for (const file of files) {
|
|
214
|
-
const
|
|
215
|
-
|
|
182
|
+
const detail = toPosix(relative(latticeDir, file));
|
|
183
|
+
const content = profile
|
|
184
|
+
? await profile.time('read Markdown file', () => readFile(file, 'utf-8'), detail)
|
|
185
|
+
: await readFile(file, 'utf-8');
|
|
186
|
+
const sections = profile
|
|
187
|
+
? profile.timeSync('parse Markdown sections', () => parseSections(file, content, projectRoot), detail)
|
|
188
|
+
: parseSections(file, content, projectRoot);
|
|
189
|
+
all.push(...sections);
|
|
216
190
|
}
|
|
217
191
|
return all;
|
|
218
192
|
}
|
|
219
|
-
export function flattenSections(sections) {
|
|
220
|
-
const result = [];
|
|
221
|
-
for (const s of sections) {
|
|
222
|
-
result.push(s);
|
|
223
|
-
result.push(...flattenSections(s.children));
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
function levenshtein(a, b) {
|
|
228
|
-
const m = a.length;
|
|
229
|
-
const n = b.length;
|
|
230
|
-
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
231
|
-
for (let i = 0; i <= m; i++)
|
|
232
|
-
dp[i][0] = i;
|
|
233
|
-
for (let j = 0; j <= n; j++)
|
|
234
|
-
dp[0][j] = j;
|
|
235
|
-
for (let i = 1; i <= m; i++) {
|
|
236
|
-
for (let j = 1; j <= n; j++) {
|
|
237
|
-
dp[i][j] =
|
|
238
|
-
a[i - 1] === b[j - 1]
|
|
239
|
-
? dp[i - 1][j - 1]
|
|
240
|
-
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return dp[m][n];
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Returns the trailing segment(s) of a section id.
|
|
247
|
-
* e.g. "markdown#Frontmatter#require-code-mention" → ["Frontmatter#require-code-mention", "require-code-mention"]
|
|
248
|
-
* The full id itself is not included (handled by exact match).
|
|
249
|
-
*/
|
|
250
|
-
function tailSegments(id) {
|
|
251
|
-
const parts = id.split('#');
|
|
252
|
-
const tails = [];
|
|
253
|
-
for (let i = 1; i < parts.length; i++) {
|
|
254
|
-
tails.push(parts.slice(i).join('#'));
|
|
255
|
-
}
|
|
256
|
-
return tails;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Build an index mapping path suffixes to their full vault-relative paths.
|
|
260
|
-
* Used by resolveRef to allow short references when a suffix is unambiguous.
|
|
261
|
-
*
|
|
262
|
-
* For a file like `lat.md/guides/setup`, indexes both `guides/setup` and `setup`.
|
|
263
|
-
* This ensures backward-compatible short refs after the vault root moved to the
|
|
264
|
-
* project root (so section IDs now include the `lat.md/` prefix).
|
|
265
|
-
*/
|
|
266
|
-
export function buildFileIndex(sections) {
|
|
267
|
-
const flat = flattenSections(sections);
|
|
268
|
-
const index = new Map();
|
|
269
|
-
for (const s of flat) {
|
|
270
|
-
const parts = s.file.split('/');
|
|
271
|
-
// Index all trailing path suffixes (excluding the full path itself,
|
|
272
|
-
// which is handled by exact match). Keys are lowercase for
|
|
273
|
-
// case-insensitive lookup.
|
|
274
|
-
for (let i = 1; i < parts.length; i++) {
|
|
275
|
-
const suffix = parts.slice(i).join('/').toLowerCase();
|
|
276
|
-
if (!index.has(suffix))
|
|
277
|
-
index.set(suffix, new Set());
|
|
278
|
-
index.get(suffix).add(s.file);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
const result = new Map();
|
|
282
|
-
for (const [stem, paths] of index) {
|
|
283
|
-
result.set(stem, [...paths]);
|
|
284
|
-
}
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Map GitHub-slugged heading paths back to canonical, literal-heading ids.
|
|
289
|
-
* Every path segment accepts either representation so an implicit literal h1
|
|
290
|
-
* can still prefix slugged child headings during short-ref resolution.
|
|
291
|
-
*/
|
|
292
|
-
export function buildSectionSlugIndex(sections) {
|
|
293
|
-
const aliases = new Map();
|
|
294
|
-
const stacks = new Map();
|
|
295
|
-
const sluggers = new Map();
|
|
296
|
-
for (const section of flattenSections(sections)) {
|
|
297
|
-
let slugger = sluggers.get(section.file);
|
|
298
|
-
if (!slugger) {
|
|
299
|
-
slugger = new GithubSlugger();
|
|
300
|
-
sluggers.set(section.file, slugger);
|
|
301
|
-
}
|
|
302
|
-
// Always advance the fallback slugger so manually constructed Section
|
|
303
|
-
// objects without githubSlug still get correct duplicate suffixes.
|
|
304
|
-
const fallbackSlug = slugger.slug(section.heading);
|
|
305
|
-
const headingSlug = section.githubSlug ?? fallbackSlug;
|
|
306
|
-
const stack = stacks.get(section.file) ?? [];
|
|
307
|
-
while (stack.length > 0 && stack[stack.length - 1].depth >= section.depth) {
|
|
308
|
-
stack.pop();
|
|
309
|
-
}
|
|
310
|
-
const levels = [
|
|
311
|
-
...stack,
|
|
312
|
-
{
|
|
313
|
-
depth: section.depth,
|
|
314
|
-
literal: section.heading.toLowerCase(),
|
|
315
|
-
slug: headingSlug.toLowerCase(),
|
|
316
|
-
},
|
|
317
|
-
];
|
|
318
|
-
let paths = [''];
|
|
319
|
-
for (const level of levels) {
|
|
320
|
-
const options = [...new Set([level.literal, level.slug])];
|
|
321
|
-
paths = paths.flatMap((path) => options.map((part) => (path ? `${path}#${part}` : part)));
|
|
322
|
-
}
|
|
323
|
-
for (const path of paths) {
|
|
324
|
-
const alias = `${section.file.toLowerCase()}#${path}`;
|
|
325
|
-
if (!aliases.has(alias))
|
|
326
|
-
aliases.set(alias, section.id);
|
|
327
|
-
}
|
|
328
|
-
stack.push(levels[levels.length - 1]);
|
|
329
|
-
stacks.set(section.file, stack);
|
|
330
|
-
}
|
|
331
|
-
return aliases;
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Normalize separators only in the file portion of a section reference.
|
|
335
|
-
* Headings are authored text and may legitimately contain backslashes.
|
|
336
|
-
*/
|
|
337
|
-
function normalizeRefFilePath(target) {
|
|
338
|
-
const hashIdx = target.indexOf('#');
|
|
339
|
-
if (hashIdx === -1)
|
|
340
|
-
return toPosix(target);
|
|
341
|
-
return toPosix(target.slice(0, hashIdx)) + target.slice(hashIdx);
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* Resolve a potentially short reference to its canonical full-path form.
|
|
345
|
-
* If the file segment of the ref is a bare stem that uniquely maps to one
|
|
346
|
-
* full path, expands it. Otherwise returns the ref unchanged.
|
|
347
|
-
*
|
|
348
|
-
* When ambiguous (multiple files share the stem), returns all candidates.
|
|
349
|
-
* If exactly one candidate actually contains the referenced section,
|
|
350
|
-
* `suggested` is set to that candidate so the caller can propose a fix.
|
|
351
|
-
*/
|
|
352
|
-
export function resolveRef(target, sectionIds, fileIndex, slugIndex) {
|
|
353
|
-
target = normalizeRefFilePath(target);
|
|
354
|
-
const resolveKnown = (candidate) => {
|
|
355
|
-
// Preserve existing wiki-link meaning when a literal heading happens to
|
|
356
|
-
// collide with a different heading's GitHub slug.
|
|
357
|
-
if (sectionIds.has(candidate.toLowerCase()))
|
|
358
|
-
return candidate;
|
|
359
|
-
return slugIndex?.get(candidate.toLowerCase()) ?? null;
|
|
360
|
-
};
|
|
361
|
-
const resolveInFile = (file, rest) => {
|
|
362
|
-
const direct = resolveKnown(file + rest);
|
|
363
|
-
if (direct)
|
|
364
|
-
return direct;
|
|
365
|
-
// Try inserting root headings between file and rest. This also works for
|
|
366
|
-
// slugged child segments because the slug index contains mixed paths.
|
|
367
|
-
const rootHeadings = findRootHeadings(file, sectionIds);
|
|
368
|
-
for (const h1 of rootHeadings) {
|
|
369
|
-
const withRoot = rest ? `${file}#${h1}${rest}` : `${file}#${h1}`;
|
|
370
|
-
const resolved = resolveKnown(withRoot);
|
|
371
|
-
if (resolved)
|
|
372
|
-
return resolved;
|
|
373
|
-
}
|
|
374
|
-
return null;
|
|
375
|
-
};
|
|
376
|
-
// Already matches a known section — no resolution needed
|
|
377
|
-
const direct = resolveKnown(target);
|
|
378
|
-
if (direct) {
|
|
379
|
-
return { resolved: direct, ambiguous: null, suggested: null };
|
|
380
|
-
}
|
|
381
|
-
// Extract the file segment (before first #) and try resolving it
|
|
382
|
-
const hashIdx = target.indexOf('#');
|
|
383
|
-
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
384
|
-
const rest = hashIdx === -1 ? '' : target.slice(hashIdx);
|
|
385
|
-
// Try resolving the file part: either it's a full path or a bare stem
|
|
386
|
-
// File index keys are lowercase for case-insensitive lookup.
|
|
387
|
-
const lcFilePart = filePart.toLowerCase();
|
|
388
|
-
const filePaths = fileIndex.has(lcFilePart)
|
|
389
|
-
? fileIndex.get(lcFilePart)
|
|
390
|
-
: [filePart];
|
|
391
|
-
if (filePaths.length === 1) {
|
|
392
|
-
const fp = filePaths[0];
|
|
393
|
-
const resolved = resolveInFile(fp, rest);
|
|
394
|
-
if (resolved) {
|
|
395
|
-
return { resolved, ambiguous: null, suggested: null };
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
else if (filePaths.length > 1) {
|
|
399
|
-
// Multiple files share this stem — ambiguous at the filename level
|
|
400
|
-
const all = filePaths.map((c) => c + rest);
|
|
401
|
-
const valid = filePaths.filter((c) => resolveInFile(c, rest) !== null);
|
|
402
|
-
return {
|
|
403
|
-
resolved: target,
|
|
404
|
-
ambiguous: all,
|
|
405
|
-
suggested: valid.length === 1 ? valid[0] + rest : null,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
return { resolved: target, ambiguous: null, suggested: null };
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Find root (h1) headings for a file by scanning sectionIds for entries
|
|
412
|
-
* that have exactly the pattern `file#heading` (no further # segments).
|
|
413
|
-
*/
|
|
414
|
-
function findRootHeadings(file, sectionIds) {
|
|
415
|
-
const prefix = file.toLowerCase() + '#';
|
|
416
|
-
const headings = [];
|
|
417
|
-
for (const id of sectionIds) {
|
|
418
|
-
if (id.startsWith(prefix) && !id.includes('#', prefix.length)) {
|
|
419
|
-
headings.push(id.slice(prefix.length));
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return headings;
|
|
423
|
-
}
|
|
424
|
-
const MAX_DISTANCE_RATIO = 0.4;
|
|
425
|
-
export function findSections(sections, query) {
|
|
426
|
-
const flat = flattenSections(sections);
|
|
427
|
-
// Leading # means "search for a heading", strip it
|
|
428
|
-
const normalized = normalizeRefFilePath(query.startsWith('#') ? query.slice(1) : query);
|
|
429
|
-
const q = normalized.toLowerCase();
|
|
430
|
-
const isFullPath = normalized.includes('#');
|
|
431
|
-
const byId = new Map(flat.map((s) => [s.id.toLowerCase(), s]));
|
|
432
|
-
const slugIndex = buildSectionSlugIndex(sections);
|
|
433
|
-
const sectionFor = (candidate) => {
|
|
434
|
-
const key = candidate.toLowerCase();
|
|
435
|
-
return byId.get(key) ?? byId.get(slugIndex.get(key)?.toLowerCase() ?? '');
|
|
436
|
-
};
|
|
437
|
-
// Tier 1: exact full-id match
|
|
438
|
-
const literalExact = flat.filter((s) => s.id.toLowerCase() === q);
|
|
439
|
-
const slugExact = literalExact.length === 0 && isFullPath
|
|
440
|
-
? sectionFor(normalized)
|
|
441
|
-
: undefined;
|
|
442
|
-
const exact = slugExact ? [slugExact] : literalExact;
|
|
443
|
-
const exactMatches = exact.map((s) => ({
|
|
444
|
-
section: s,
|
|
445
|
-
reason: 'exact match',
|
|
446
|
-
}));
|
|
447
|
-
if (exactMatches.length > 0 && isFullPath)
|
|
448
|
-
return exactMatches;
|
|
449
|
-
// Build file index early — used by both tier 1a and 1b
|
|
450
|
-
const fileIndex = buildFileIndex(sections);
|
|
451
|
-
// Tier 1a: bare name matches file — return root sections of that file
|
|
452
|
-
// Also checks via file index (e.g. "dev-process" → "lat.md/dev-process")
|
|
453
|
-
if (!isFullPath && exactMatches.length === 0) {
|
|
454
|
-
const matchFiles = new Set();
|
|
455
|
-
// Direct match
|
|
456
|
-
for (const s of flat) {
|
|
457
|
-
if (s.file.toLowerCase() === q &&
|
|
458
|
-
!s.id.includes('#', s.file.length + 1)) {
|
|
459
|
-
matchFiles.add(s.file);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// File index expansion (keys are lowercase)
|
|
463
|
-
const indexPaths = fileIndex.get(q) ?? [];
|
|
464
|
-
for (const p of indexPaths) {
|
|
465
|
-
matchFiles.add(p);
|
|
466
|
-
}
|
|
467
|
-
if (matchFiles.size > 0) {
|
|
468
|
-
const fileRoots = flat.filter((s) => matchFiles.has(s.file) && !s.id.includes('#', s.file.length + 1));
|
|
469
|
-
if (fileRoots.length > 0) {
|
|
470
|
-
return fileRoots.map((s) => ({
|
|
471
|
-
section: s,
|
|
472
|
-
reason: 'exact match',
|
|
473
|
-
}));
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// Tier 1b: file stem expansion
|
|
478
|
-
// For bare names: "locate" → matches root section of "tests/locate.md"
|
|
479
|
-
// For paths with #: "setup#Install" → expands to "guides/setup#Install"
|
|
480
|
-
const stemMatches = [];
|
|
481
|
-
if (isFullPath) {
|
|
482
|
-
// Expand file stem in the file part of the query
|
|
483
|
-
const hashIdx = normalized.indexOf('#');
|
|
484
|
-
const filePart = normalized.slice(0, hashIdx);
|
|
485
|
-
const rest = normalized.slice(hashIdx);
|
|
486
|
-
const stemPaths = fileIndex.get(filePart.toLowerCase()) ?? [];
|
|
487
|
-
// Also try filePart as a direct file path (for root-level files not in index)
|
|
488
|
-
const allPaths = stemPaths.length > 0 ? stemPaths : filePart ? [filePart] : [];
|
|
489
|
-
for (const p of allPaths) {
|
|
490
|
-
const s = sectionFor(p + rest);
|
|
491
|
-
if (s) {
|
|
492
|
-
if (exact.includes(s))
|
|
493
|
-
continue;
|
|
494
|
-
stemMatches.push({
|
|
495
|
-
section: s,
|
|
496
|
-
reason: stemPaths.length > 0
|
|
497
|
-
? `file stem expanded: ${filePart} → ${p}`
|
|
498
|
-
: 'exact match',
|
|
499
|
-
});
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
// Try inserting root headings: file#rest → file#h1#rest
|
|
503
|
-
const rootsOfFile = flat.filter((s) => s.file.toLowerCase() === p.toLowerCase() &&
|
|
504
|
-
!s.id.includes('#', s.file.length + 1));
|
|
505
|
-
for (const root of rootsOfFile) {
|
|
506
|
-
const match = sectionFor(root.id + rest);
|
|
507
|
-
if (match) {
|
|
508
|
-
if (exact.includes(match))
|
|
509
|
-
continue;
|
|
510
|
-
stemMatches.push({
|
|
511
|
-
section: match,
|
|
512
|
-
reason: stemPaths.length > 0
|
|
513
|
-
? `file stem expanded: ${filePart} → ${p}`
|
|
514
|
-
: 'exact match',
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (stemMatches.length > 0)
|
|
520
|
-
return [...exactMatches, ...stemMatches];
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
// Bare name: match root sections of files via stem index (keys lowercase)
|
|
524
|
-
const paths = fileIndex.get(q) ?? [];
|
|
525
|
-
for (const p of paths) {
|
|
526
|
-
for (const s of flat) {
|
|
527
|
-
if (exact.includes(s))
|
|
528
|
-
continue;
|
|
529
|
-
// Root sections have id = "file#heading" (exactly 2 segments)
|
|
530
|
-
if (s.file.toLowerCase() === p.toLowerCase() &&
|
|
531
|
-
!s.id.includes('#', s.file.length + 1)) {
|
|
532
|
-
stemMatches.push({ section: s, reason: 'file stem match' });
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
// Tier 2: exact match on trailing segments (subsection name match)
|
|
538
|
-
const seen = new Set([
|
|
539
|
-
...exact.map((s) => s.id),
|
|
540
|
-
...stemMatches.map((m) => m.section.id),
|
|
541
|
-
]);
|
|
542
|
-
const slugTailMatches = new Set();
|
|
543
|
-
if (!isFullPath) {
|
|
544
|
-
for (const [alias, canonical] of slugIndex) {
|
|
545
|
-
if (alias.slice(alias.lastIndexOf('#') + 1) === q) {
|
|
546
|
-
slugTailMatches.add(canonical.toLowerCase());
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
const literalTailMatches = new Set(isFullPath
|
|
551
|
-
? []
|
|
552
|
-
: flat
|
|
553
|
-
.filter((s) => tailSegments(s.id).some((tail) => tail.toLowerCase() === q))
|
|
554
|
-
.map((s) => s.id.toLowerCase()));
|
|
555
|
-
const subsection = isFullPath
|
|
556
|
-
? []
|
|
557
|
-
: flat
|
|
558
|
-
.filter((s) => {
|
|
559
|
-
if (seen.has(s.id))
|
|
560
|
-
return false;
|
|
561
|
-
const id = s.id.toLowerCase();
|
|
562
|
-
return literalTailMatches.size > 0
|
|
563
|
-
? literalTailMatches.has(id)
|
|
564
|
-
: slugTailMatches.has(id);
|
|
565
|
-
})
|
|
566
|
-
.map((s) => ({ section: s, reason: 'section name match' }));
|
|
567
|
-
// Tier 2b: subsequence match — query segments are a subsequence of section id segments
|
|
568
|
-
// e.g. "Markdown#Resolution Rules" matches "markdown#Wiki Links#Resolution Rules"
|
|
569
|
-
// Also tries expanding the file part via the file index for short refs.
|
|
570
|
-
const seenSub = new Set([...seen, ...subsection.map((m) => m.section.id)]);
|
|
571
|
-
const qParts = q.split('#');
|
|
572
|
-
// Build query variants: original + file-index-expanded forms
|
|
573
|
-
const qVariants = [qParts];
|
|
574
|
-
if (qParts.length >= 2) {
|
|
575
|
-
const expanded = fileIndex.get(qParts[0]);
|
|
576
|
-
if (expanded) {
|
|
577
|
-
for (const exp of expanded) {
|
|
578
|
-
qVariants.push([exp.toLowerCase(), ...qParts.slice(1)]);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
const subsequence = qParts.length >= 2
|
|
583
|
-
? flat
|
|
584
|
-
.filter((s) => {
|
|
585
|
-
if (seenSub.has(s.id))
|
|
586
|
-
return false;
|
|
587
|
-
const sParts = s.id.toLowerCase().split('#');
|
|
588
|
-
return qVariants.some((variant) => {
|
|
589
|
-
if (sParts.length <= variant.length)
|
|
590
|
-
return false;
|
|
591
|
-
let qi = 0;
|
|
592
|
-
for (const sp of sParts) {
|
|
593
|
-
if (sp === variant[qi])
|
|
594
|
-
qi++;
|
|
595
|
-
if (qi === variant.length)
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
return false;
|
|
599
|
-
});
|
|
600
|
-
})
|
|
601
|
-
.map((s) => {
|
|
602
|
-
const skipped = s.id.split('#').length - qParts.length;
|
|
603
|
-
return {
|
|
604
|
-
section: s,
|
|
605
|
-
reason: `path match, ${skipped} intermediate ${skipped === 1 ? 'section' : 'sections'} skipped`,
|
|
606
|
-
};
|
|
607
|
-
})
|
|
608
|
-
: [];
|
|
609
|
-
// Tier 3: fuzzy match by edit distance on each segment tail and full id
|
|
610
|
-
const seenAll = new Set([
|
|
611
|
-
...seenSub,
|
|
612
|
-
...subsequence.map((m) => m.section.id),
|
|
613
|
-
]);
|
|
614
|
-
const fuzzy = [];
|
|
615
|
-
// For full-path queries, extract the file and heading parts so we can
|
|
616
|
-
// fuzzy-match only the heading portion when the file part matches exactly.
|
|
617
|
-
// This prevents the shared file prefix from inflating similarity scores
|
|
618
|
-
// (e.g. "cli#locat" would otherwise fuzzy-match "cli#prompt").
|
|
619
|
-
const qHashIdx = normalized.indexOf('#');
|
|
620
|
-
const qFile = qHashIdx === -1 ? null : normalized.slice(0, qHashIdx).toLowerCase();
|
|
621
|
-
const qHeading = qHashIdx === -1 ? null : normalized.slice(qHashIdx + 1).toLowerCase();
|
|
622
|
-
for (const s of flat) {
|
|
623
|
-
if (seenAll.has(s.id))
|
|
624
|
-
continue;
|
|
625
|
-
const candidates = [s.id, ...tailSegments(s.id)];
|
|
626
|
-
let best = Infinity;
|
|
627
|
-
let bestCandidate = '';
|
|
628
|
-
for (const c of candidates) {
|
|
629
|
-
let d;
|
|
630
|
-
let maxLen;
|
|
631
|
-
const cl = c.toLowerCase();
|
|
632
|
-
const cHashIdx = cl.indexOf('#');
|
|
633
|
-
// When both query and candidate have # and their file parts match,
|
|
634
|
-
// compare only the heading portions to avoid file-prefix inflation
|
|
635
|
-
if (qFile && qHeading && cHashIdx !== -1) {
|
|
636
|
-
const cFile = cl.slice(0, cHashIdx);
|
|
637
|
-
const cHeading = cl.slice(cHashIdx + 1);
|
|
638
|
-
if (cFile === qFile) {
|
|
639
|
-
d = levenshtein(cHeading, qHeading);
|
|
640
|
-
maxLen = Math.max(cHeading.length, qHeading.length);
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
d = levenshtein(cl, q);
|
|
644
|
-
maxLen = Math.max(c.length, q.length);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
d = levenshtein(cl, q);
|
|
649
|
-
maxLen = Math.max(c.length, q.length);
|
|
650
|
-
}
|
|
651
|
-
if (maxLen > 0 && d / maxLen <= MAX_DISTANCE_RATIO && d < best) {
|
|
652
|
-
best = d;
|
|
653
|
-
bestCandidate = c;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (best < Infinity) {
|
|
657
|
-
fuzzy.push({ section: s, distance: best, matched: bestCandidate });
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
fuzzy.sort((a, b) => a.distance - b.distance);
|
|
661
|
-
const fuzzyMatches = fuzzy.map((f) => ({
|
|
662
|
-
section: f.section,
|
|
663
|
-
reason: f.matched.toLowerCase() === f.section.id.toLowerCase()
|
|
664
|
-
? `fuzzy match, distance ${f.distance}`
|
|
665
|
-
: `fuzzy match on "${f.matched}", distance ${f.distance}`,
|
|
666
|
-
}));
|
|
667
|
-
// Sort results: shallower depth first, then fewer path segments
|
|
668
|
-
const sortKey = (s) => {
|
|
669
|
-
const pathDepth = (s.file.match(/\//g) || []).length;
|
|
670
|
-
return s.depth * 100 + pathDepth;
|
|
671
|
-
};
|
|
672
|
-
const sortedStems = [...stemMatches].sort((a, b) => sortKey(a.section) - sortKey(b.section));
|
|
673
|
-
return [
|
|
674
|
-
...exactMatches,
|
|
675
|
-
...sortedStems,
|
|
676
|
-
...subsection,
|
|
677
|
-
...subsequence,
|
|
678
|
-
...fuzzyMatches,
|
|
679
|
-
];
|
|
680
|
-
}
|
|
681
193
|
export function extractRefs(filePath, content, projectRoot, tree = parse(content)) {
|
|
682
194
|
const file = projectRoot
|
|
683
195
|
? toPosix(relative(projectRoot, filePath)).replace(/\.md$/, '')
|
|
@@ -749,7 +261,7 @@ function closingBracket(value, open) {
|
|
|
749
261
|
}
|
|
750
262
|
return -1;
|
|
751
263
|
}
|
|
752
|
-
/** Extract link destinations and undefined
|
|
264
|
+
/** Extract link destinations and undefined reference syntax. */
|
|
753
265
|
export function extractLinks(content, tree = parse(content)) {
|
|
754
266
|
const links = [];
|
|
755
267
|
visit(tree, ['link', 'image', 'definition'], (node) => {
|
|
@@ -764,12 +276,16 @@ export function extractLinks(content, tree = parse(content)) {
|
|
|
764
276
|
'code',
|
|
765
277
|
'inlineCode',
|
|
766
278
|
'html',
|
|
279
|
+
'math',
|
|
280
|
+
'inlineMath',
|
|
767
281
|
'yaml',
|
|
768
282
|
'link',
|
|
769
283
|
'image',
|
|
770
284
|
'definition',
|
|
771
285
|
'linkReference',
|
|
772
286
|
'imageReference',
|
|
287
|
+
'footnoteReference',
|
|
288
|
+
'alertMarker',
|
|
773
289
|
'wikiLink',
|
|
774
290
|
], (node) => {
|
|
775
291
|
const start = node.position?.start.offset;
|
|
@@ -778,28 +294,58 @@ export function extractLinks(content, tree = parse(content)) {
|
|
|
778
294
|
excluded.push({ start, end });
|
|
779
295
|
}
|
|
780
296
|
});
|
|
297
|
+
// GFM removes task-list markers from the paragraph AST. Exclude just the
|
|
298
|
+
// marker prefix so strict shortcut checks still apply to the item body.
|
|
299
|
+
visit(tree, 'listItem', (node) => {
|
|
300
|
+
if (typeof node.checked !== 'boolean')
|
|
301
|
+
return;
|
|
302
|
+
const start = node.position?.start.offset;
|
|
303
|
+
const bodyStart = node.children[0]?.position?.start.offset;
|
|
304
|
+
if (start === undefined || bodyStart === undefined)
|
|
305
|
+
return;
|
|
306
|
+
const marker = /\[[ xX]\]/.exec(content.slice(start, bodyStart));
|
|
307
|
+
if (marker) {
|
|
308
|
+
excluded.push({
|
|
309
|
+
start: start + marker.index,
|
|
310
|
+
end: start + marker.index + marker[0].length,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// Likewise, exclude only a footnote definition's label and delimiter. Its
|
|
315
|
+
// body remains prose and must still obey strict shortcut validation.
|
|
316
|
+
visit(tree, 'footnoteDefinition', (node) => {
|
|
317
|
+
const start = node.position?.start.offset;
|
|
318
|
+
const bodyStart = node.children[0]?.position?.start.offset;
|
|
319
|
+
if (start !== undefined && bodyStart !== undefined) {
|
|
320
|
+
excluded.push({ start, end: bodyStart });
|
|
321
|
+
}
|
|
322
|
+
});
|
|
781
323
|
const overlapsExcluded = (start, end) => excluded.some((range) => start < range.end && end > range.start);
|
|
782
324
|
for (let open = 0; open < content.length; open++) {
|
|
783
325
|
if (content[open] !== '[' || isEscapedAt(content, open))
|
|
784
326
|
continue;
|
|
785
327
|
const labelEnd = closingBracket(content, open);
|
|
786
|
-
|
|
787
|
-
if (labelEnd === -1 ||
|
|
788
|
-
content[identifierStart] !== '[' ||
|
|
789
|
-
isEscapedAt(content, identifierStart)) {
|
|
328
|
+
if (labelEnd === -1)
|
|
790
329
|
continue;
|
|
791
|
-
|
|
792
|
-
const
|
|
330
|
+
const identifierStart = labelEnd + 1;
|
|
331
|
+
const hasSecondLabel = content[identifierStart] === '[' &&
|
|
332
|
+
!isEscapedAt(content, identifierStart);
|
|
333
|
+
const identifierEnd = hasSecondLabel
|
|
334
|
+
? closingBracket(content, identifierStart)
|
|
335
|
+
: labelEnd;
|
|
793
336
|
if (identifierEnd === -1)
|
|
794
337
|
continue;
|
|
795
338
|
const isImage = open > 0 && content[open - 1] === '!' && !isEscapedAt(content, open - 1);
|
|
339
|
+
const isMalformedDefinition = !isImage && !hasSecondLabel && content[labelEnd + 1] === ':';
|
|
796
340
|
const start = isImage ? open - 1 : open;
|
|
797
|
-
const end = identifierEnd + 1;
|
|
341
|
+
const end = identifierEnd + 1 + (isMalformedDefinition ? 1 : 0);
|
|
798
342
|
if (overlapsExcluded(start, end)) {
|
|
799
343
|
open = identifierEnd;
|
|
800
344
|
continue;
|
|
801
345
|
}
|
|
802
|
-
const explicitIdentifier =
|
|
346
|
+
const explicitIdentifier = hasSecondLabel
|
|
347
|
+
? content.slice(identifierStart + 1, identifierEnd)
|
|
348
|
+
: '';
|
|
803
349
|
const identifier = explicitIdentifier || content.slice(open + 1, labelEnd);
|
|
804
350
|
if (identifier.trim() === '') {
|
|
805
351
|
open = identifierEnd;
|
|
@@ -809,6 +355,13 @@ export function extractLinks(content, tree = parse(content)) {
|
|
|
809
355
|
identifier: identifier.trim(),
|
|
810
356
|
source: content.slice(start, end),
|
|
811
357
|
kind: isImage ? 'imageReference' : 'linkReference',
|
|
358
|
+
style: !hasSecondLabel
|
|
359
|
+
? isMalformedDefinition
|
|
360
|
+
? 'definition'
|
|
361
|
+
: 'shortcut'
|
|
362
|
+
: explicitIdentifier
|
|
363
|
+
? 'full'
|
|
364
|
+
: 'collapsed',
|
|
812
365
|
line: content.slice(0, start).split('\n').length,
|
|
813
366
|
});
|
|
814
367
|
open = identifierEnd;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MarkdownAnalysisTimings, MarkdownFileAnalysis } from './markdown-analysis.js';
|
|
2
|
+
import type { ParserImportObserver } from './parser-import.js';
|
|
3
|
+
export { PARSER_CACHE_VERSION } from './parser-cache.js';
|
|
4
|
+
export type MarkdownAnalysisCacheStatus = 'disabled' | 'hit' | 'miss';
|
|
5
|
+
export type PreparedMarkdownAnalysis = {
|
|
6
|
+
absolutePath: string;
|
|
7
|
+
cachePath: string;
|
|
8
|
+
content: string;
|
|
9
|
+
contentHash: string;
|
|
10
|
+
analysis?: MarkdownFileAnalysis;
|
|
11
|
+
timings: Pick<MarkdownAnalysisTimings, 'readMs' | 'hashMs' | 'cacheReadMs' | 'cacheStatus'>;
|
|
12
|
+
};
|
|
13
|
+
/** Return the collision-safe, sharded cache path for one Markdown file. */
|
|
14
|
+
export declare function markdownAnalysisCachePath(latDir: string, projectRoot: string, absolutePath: string): string;
|
|
15
|
+
/** Read, hash, and attempt to hydrate one Markdown file from persistent cache. */
|
|
16
|
+
export declare function prepareMarkdownAnalysis(absolutePath: string, latDir: string, projectRoot: string, cache?: boolean): Promise<PreparedMarkdownAnalysis>;
|
|
17
|
+
/** Attach I/O timings and best-effort publish one newly parsed analysis. */
|
|
18
|
+
export declare function publishMarkdownAnalysis(prepared: PreparedMarkdownAnalysis, analysis: MarkdownFileAnalysis): Promise<MarkdownFileAnalysis>;
|
|
19
|
+
/** Analyze one path through the same persistent cache used by project runs. */
|
|
20
|
+
export declare function analyzeMarkdownPath(absolutePath: string, latDir: string, projectRoot: string, cache?: boolean, onParserImport?: ParserImportObserver): Promise<MarkdownFileAnalysis>;
|