@plebeiantech/lat.md 0.12.3-fork.13 → 0.12.3-fork.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/package.json +46 -8
- 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/format.js
CHANGED
|
@@ -10,9 +10,13 @@ export function formatSectionPreview(ctx, section, opts) {
|
|
|
10
10
|
const s = ctx.styler;
|
|
11
11
|
const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
|
|
12
12
|
const kind = section.id.includes('#') ? 'Section' : 'File';
|
|
13
|
-
const
|
|
13
|
+
const details = [
|
|
14
|
+
opts?.reason,
|
|
15
|
+
opts?.score === undefined ? undefined : `score: ${opts.score.toFixed(6)}`,
|
|
16
|
+
].filter((detail) => detail !== undefined);
|
|
17
|
+
const detailsSuffix = details.length > 0 ? ' ' + s.dim(`(${details.join(', ')})`) : '';
|
|
14
18
|
const lines = [
|
|
15
|
-
`${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${
|
|
19
|
+
`${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${detailsSuffix}`,
|
|
16
20
|
` ${s.dim('Defined in')} ${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`,
|
|
17
21
|
];
|
|
18
22
|
if (section.firstParagraph) {
|
|
@@ -20,13 +24,14 @@ export function formatSectionPreview(ctx, section, opts) {
|
|
|
20
24
|
}
|
|
21
25
|
return lines.join('\n');
|
|
22
26
|
}
|
|
23
|
-
export function formatResultList(ctx, header, matches) {
|
|
27
|
+
export function formatResultList(ctx, header, matches, opts) {
|
|
24
28
|
const lines = ['', `## ${header}`, ''];
|
|
25
29
|
for (let i = 0; i < matches.length; i++) {
|
|
26
30
|
if (i > 0)
|
|
27
31
|
lines.push('');
|
|
28
32
|
lines.push(formatSectionPreview(ctx, matches[i].section, {
|
|
29
33
|
reason: matches[i].reason,
|
|
34
|
+
score: opts?.showScores ? matches[i].score : undefined,
|
|
30
35
|
}));
|
|
31
36
|
}
|
|
32
37
|
lines.push('');
|
package/dist/src/graph-export.js
CHANGED
|
@@ -16,7 +16,7 @@ import { scanCodeRefs } from './code-refs.js';
|
|
|
16
16
|
import { DIATAXIS_MODES, MODE_DIRS } from './cli/check-mode.js';
|
|
17
17
|
import { readProvenance } from './cli/check-status.js';
|
|
18
18
|
import { cleanUntrustedId } from './untrusted.js';
|
|
19
|
-
import { toPosix } from './
|
|
19
|
+
import { toPosix } from './path.js';
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
21
|
/** Bytes. Node's `execFile` default is 1 MB, which a large document exceeds. */
|
|
22
22
|
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ForkFrontmatter } from './fork/frontmatter-fields.js';
|
|
2
|
+
export type Section = {
|
|
3
|
+
id: string;
|
|
4
|
+
heading: string;
|
|
5
|
+
depth: number;
|
|
6
|
+
file: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
children: Section[];
|
|
9
|
+
startLine: number;
|
|
10
|
+
endLine: number;
|
|
11
|
+
firstParagraph: string;
|
|
12
|
+
/** GitHub-compatible anchor generated from the rendered heading text. */
|
|
13
|
+
githubSlug?: string;
|
|
14
|
+
};
|
|
15
|
+
export type Ref = {
|
|
16
|
+
target: string;
|
|
17
|
+
fromSection: string;
|
|
18
|
+
file: string;
|
|
19
|
+
line: number;
|
|
20
|
+
};
|
|
21
|
+
/** An ordinary markdown destination or an undefined reference-style link. */
|
|
22
|
+
export type MdLink = {
|
|
23
|
+
/** Destination exactly as authored, percent-escapes and all. */
|
|
24
|
+
url: string;
|
|
25
|
+
kind: 'link' | 'image' | 'definition';
|
|
26
|
+
line: number;
|
|
27
|
+
} | {
|
|
28
|
+
/** Authored label that should have a matching definition. */
|
|
29
|
+
identifier: string;
|
|
30
|
+
/** Full reference syntax exactly as authored. */
|
|
31
|
+
source: string;
|
|
32
|
+
kind: 'linkReference' | 'imageReference';
|
|
33
|
+
style: 'full' | 'collapsed' | 'shortcut' | 'definition';
|
|
34
|
+
line: number;
|
|
35
|
+
};
|
|
36
|
+
export type LatFrontmatter = {
|
|
37
|
+
requireCodeMention?: boolean;
|
|
38
|
+
} & ForkFrontmatter;
|
|
39
|
+
export declare function flattenSections(sections: Section[]): Section[];
|
|
40
|
+
/**
|
|
41
|
+
* Build an index mapping path suffixes to their full vault-relative paths.
|
|
42
|
+
* Used by resolveRef to allow short references when a suffix is unambiguous.
|
|
43
|
+
*
|
|
44
|
+
* For a file like `lat.md/guides/setup`, indexes both `guides/setup` and `setup`.
|
|
45
|
+
* This ensures backward-compatible short refs after the vault root moved to the
|
|
46
|
+
* project root (so section IDs now include the `lat.md/` prefix).
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildFileIndex(sections: Section[]): Map<string, string[]>;
|
|
49
|
+
export type SectionSlugIndex = ReadonlyMap<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Map GitHub-slugged heading paths back to canonical, literal-heading ids.
|
|
52
|
+
* Every path segment accepts either representation so an implicit literal h1
|
|
53
|
+
* can still prefix slugged child headings during short-ref resolution.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildSectionSlugIndex(sections: Section[]): Map<string, string>;
|
|
56
|
+
export type ResolveResult = {
|
|
57
|
+
resolved: string;
|
|
58
|
+
ambiguous: string[] | null;
|
|
59
|
+
/** When ambiguous but exactly one candidate has the section, suggest it. */
|
|
60
|
+
suggested: string | null;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a potentially short reference to its canonical full-path form.
|
|
64
|
+
* If the file segment of the ref is a bare stem that uniquely maps to one
|
|
65
|
+
* full path, expands it. Otherwise returns the ref unchanged.
|
|
66
|
+
*
|
|
67
|
+
* When ambiguous (multiple files share the stem), returns all candidates.
|
|
68
|
+
* If exactly one candidate actually contains the referenced section,
|
|
69
|
+
* `suggested` is set to that candidate so the caller can propose a fix.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveRef(target: string, sectionIds: Set<string>, fileIndex: Map<string, string[]>, slugIndex?: SectionSlugIndex): ResolveResult;
|
|
72
|
+
export type SectionMatch = {
|
|
73
|
+
section: Section;
|
|
74
|
+
reason: string;
|
|
75
|
+
score?: number;
|
|
76
|
+
};
|
|
77
|
+
export declare function findSections(sections: Section[], query: string): SectionMatch[];
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import GithubSlugger from 'github-slugger';
|
|
2
|
+
import { toPosix } from './path.js';
|
|
3
|
+
export function flattenSections(sections) {
|
|
4
|
+
const result = [];
|
|
5
|
+
for (const s of sections) {
|
|
6
|
+
result.push(s);
|
|
7
|
+
result.push(...flattenSections(s.children));
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
function levenshtein(a, b) {
|
|
12
|
+
const m = a.length;
|
|
13
|
+
const n = b.length;
|
|
14
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
15
|
+
for (let i = 0; i <= m; i++)
|
|
16
|
+
dp[i][0] = i;
|
|
17
|
+
for (let j = 0; j <= n; j++)
|
|
18
|
+
dp[0][j] = j;
|
|
19
|
+
for (let i = 1; i <= m; i++) {
|
|
20
|
+
for (let j = 1; j <= n; j++) {
|
|
21
|
+
dp[i][j] =
|
|
22
|
+
a[i - 1] === b[j - 1]
|
|
23
|
+
? dp[i - 1][j - 1]
|
|
24
|
+
: 1 + Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return dp[m][n];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the trailing segment(s) of a section id.
|
|
31
|
+
* e.g. "markdown#Frontmatter#require-code-mention" → ["Frontmatter#require-code-mention", "require-code-mention"]
|
|
32
|
+
* The full id itself is not included (handled by exact match).
|
|
33
|
+
*/
|
|
34
|
+
function tailSegments(id) {
|
|
35
|
+
const parts = id.split('#');
|
|
36
|
+
const tails = [];
|
|
37
|
+
for (let i = 1; i < parts.length; i++) {
|
|
38
|
+
tails.push(parts.slice(i).join('#'));
|
|
39
|
+
}
|
|
40
|
+
return tails;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build an index mapping path suffixes to their full vault-relative paths.
|
|
44
|
+
* Used by resolveRef to allow short references when a suffix is unambiguous.
|
|
45
|
+
*
|
|
46
|
+
* For a file like `lat.md/guides/setup`, indexes both `guides/setup` and `setup`.
|
|
47
|
+
* This ensures backward-compatible short refs after the vault root moved to the
|
|
48
|
+
* project root (so section IDs now include the `lat.md/` prefix).
|
|
49
|
+
*/
|
|
50
|
+
export function buildFileIndex(sections) {
|
|
51
|
+
const flat = flattenSections(sections);
|
|
52
|
+
const index = new Map();
|
|
53
|
+
for (const s of flat) {
|
|
54
|
+
const parts = s.file.split('/');
|
|
55
|
+
// Index all trailing path suffixes (excluding the full path itself,
|
|
56
|
+
// which is handled by exact match). Keys are lowercase for
|
|
57
|
+
// case-insensitive lookup.
|
|
58
|
+
for (let i = 1; i < parts.length; i++) {
|
|
59
|
+
const suffix = parts.slice(i).join('/').toLowerCase();
|
|
60
|
+
if (!index.has(suffix))
|
|
61
|
+
index.set(suffix, new Set());
|
|
62
|
+
index.get(suffix).add(s.file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const result = new Map();
|
|
66
|
+
for (const [stem, paths] of index) {
|
|
67
|
+
result.set(stem, [...paths]);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Map GitHub-slugged heading paths back to canonical, literal-heading ids.
|
|
73
|
+
* Every path segment accepts either representation so an implicit literal h1
|
|
74
|
+
* can still prefix slugged child headings during short-ref resolution.
|
|
75
|
+
*/
|
|
76
|
+
export function buildSectionSlugIndex(sections) {
|
|
77
|
+
const aliases = new Map();
|
|
78
|
+
const stacks = new Map();
|
|
79
|
+
const sluggers = new Map();
|
|
80
|
+
for (const section of flattenSections(sections)) {
|
|
81
|
+
let slugger = sluggers.get(section.file);
|
|
82
|
+
if (!slugger) {
|
|
83
|
+
slugger = new GithubSlugger();
|
|
84
|
+
sluggers.set(section.file, slugger);
|
|
85
|
+
}
|
|
86
|
+
// Always advance the fallback slugger so manually constructed Section
|
|
87
|
+
// objects without githubSlug still get correct duplicate suffixes.
|
|
88
|
+
const fallbackSlug = slugger.slug(section.heading);
|
|
89
|
+
const headingSlug = section.githubSlug ?? fallbackSlug;
|
|
90
|
+
const stack = stacks.get(section.file) ?? [];
|
|
91
|
+
while (stack.length > 0 && stack[stack.length - 1].depth >= section.depth) {
|
|
92
|
+
stack.pop();
|
|
93
|
+
}
|
|
94
|
+
const levels = [
|
|
95
|
+
...stack,
|
|
96
|
+
{
|
|
97
|
+
depth: section.depth,
|
|
98
|
+
literal: section.heading.toLowerCase(),
|
|
99
|
+
slug: headingSlug.toLowerCase(),
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
let paths = [''];
|
|
103
|
+
for (const level of levels) {
|
|
104
|
+
const options = [...new Set([level.literal, level.slug])];
|
|
105
|
+
paths = paths.flatMap((path) => options.map((part) => (path ? `${path}#${part}` : part)));
|
|
106
|
+
}
|
|
107
|
+
for (const path of paths) {
|
|
108
|
+
const alias = `${section.file.toLowerCase()}#${path}`;
|
|
109
|
+
if (!aliases.has(alias))
|
|
110
|
+
aliases.set(alias, section.id);
|
|
111
|
+
}
|
|
112
|
+
stack.push(levels[levels.length - 1]);
|
|
113
|
+
stacks.set(section.file, stack);
|
|
114
|
+
}
|
|
115
|
+
return aliases;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Normalize separators only in the file portion of a section reference.
|
|
119
|
+
* Headings are authored text and may legitimately contain backslashes.
|
|
120
|
+
*/
|
|
121
|
+
function normalizeRefFilePath(target) {
|
|
122
|
+
const hashIdx = target.indexOf('#');
|
|
123
|
+
const file = toPosix(hashIdx === -1 ? target : target.slice(0, hashIdx));
|
|
124
|
+
const normalizedFile = file.replace(/\.md$/i, '');
|
|
125
|
+
return normalizedFile + (hashIdx === -1 ? '' : target.slice(hashIdx));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a potentially short reference to its canonical full-path form.
|
|
129
|
+
* If the file segment of the ref is a bare stem that uniquely maps to one
|
|
130
|
+
* full path, expands it. Otherwise returns the ref unchanged.
|
|
131
|
+
*
|
|
132
|
+
* When ambiguous (multiple files share the stem), returns all candidates.
|
|
133
|
+
* If exactly one candidate actually contains the referenced section,
|
|
134
|
+
* `suggested` is set to that candidate so the caller can propose a fix.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveRef(target, sectionIds, fileIndex, slugIndex) {
|
|
137
|
+
target = normalizeRefFilePath(target);
|
|
138
|
+
const resolveKnown = (candidate) => {
|
|
139
|
+
// Preserve existing wiki-link meaning when a literal heading happens to
|
|
140
|
+
// collide with a different heading's GitHub slug.
|
|
141
|
+
if (sectionIds.has(candidate.toLowerCase()))
|
|
142
|
+
return candidate;
|
|
143
|
+
return slugIndex?.get(candidate.toLowerCase()) ?? null;
|
|
144
|
+
};
|
|
145
|
+
const resolveInFile = (file, rest) => {
|
|
146
|
+
const direct = resolveKnown(file + rest);
|
|
147
|
+
if (direct)
|
|
148
|
+
return direct;
|
|
149
|
+
// Try inserting root headings between file and rest. This also works for
|
|
150
|
+
// slugged child segments because the slug index contains mixed paths.
|
|
151
|
+
const rootHeadings = findRootHeadings(file, sectionIds);
|
|
152
|
+
for (const h1 of rootHeadings) {
|
|
153
|
+
const withRoot = rest ? `${file}#${h1}${rest}` : `${file}#${h1}`;
|
|
154
|
+
const resolved = resolveKnown(withRoot);
|
|
155
|
+
if (resolved)
|
|
156
|
+
return resolved;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
160
|
+
// Already matches a known section — no resolution needed
|
|
161
|
+
const direct = resolveKnown(target);
|
|
162
|
+
if (direct) {
|
|
163
|
+
return { resolved: direct, ambiguous: null, suggested: null };
|
|
164
|
+
}
|
|
165
|
+
// Extract the file segment (before first #) and try resolving it
|
|
166
|
+
const hashIdx = target.indexOf('#');
|
|
167
|
+
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
168
|
+
const rest = hashIdx === -1 ? '' : target.slice(hashIdx);
|
|
169
|
+
// Try resolving the file part: either it's a full path or a bare stem
|
|
170
|
+
// File index keys are lowercase for case-insensitive lookup.
|
|
171
|
+
const lcFilePart = filePart.toLowerCase();
|
|
172
|
+
const filePaths = fileIndex.has(lcFilePart)
|
|
173
|
+
? fileIndex.get(lcFilePart)
|
|
174
|
+
: [filePart];
|
|
175
|
+
if (filePaths.length === 1) {
|
|
176
|
+
const fp = filePaths[0];
|
|
177
|
+
const resolved = resolveInFile(fp, rest);
|
|
178
|
+
if (resolved) {
|
|
179
|
+
return { resolved, ambiguous: null, suggested: null };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (filePaths.length > 1) {
|
|
183
|
+
// Multiple files share this stem — ambiguous at the filename level
|
|
184
|
+
const all = filePaths.map((c) => c + rest);
|
|
185
|
+
const valid = filePaths.filter((c) => resolveInFile(c, rest) !== null);
|
|
186
|
+
return {
|
|
187
|
+
resolved: target,
|
|
188
|
+
ambiguous: all,
|
|
189
|
+
suggested: valid.length === 1 ? valid[0] + rest : null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { resolved: target, ambiguous: null, suggested: null };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Find root (h1) headings for a file by scanning sectionIds for entries
|
|
196
|
+
* that have exactly the pattern `file#heading` (no further # segments).
|
|
197
|
+
*/
|
|
198
|
+
function findRootHeadings(file, sectionIds) {
|
|
199
|
+
const prefix = file.toLowerCase() + '#';
|
|
200
|
+
const headings = [];
|
|
201
|
+
for (const id of sectionIds) {
|
|
202
|
+
if (id.startsWith(prefix) && !id.includes('#', prefix.length)) {
|
|
203
|
+
headings.push(id.slice(prefix.length));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return headings;
|
|
207
|
+
}
|
|
208
|
+
const MAX_DISTANCE_RATIO = 0.4;
|
|
209
|
+
export function findSections(sections, query) {
|
|
210
|
+
const flat = flattenSections(sections);
|
|
211
|
+
// Leading # means "search for a heading", strip it
|
|
212
|
+
const normalized = normalizeRefFilePath(query.startsWith('#') ? query.slice(1) : query);
|
|
213
|
+
const q = normalized.toLowerCase();
|
|
214
|
+
const isFullPath = normalized.includes('#');
|
|
215
|
+
const byId = new Map(flat.map((s) => [s.id.toLowerCase(), s]));
|
|
216
|
+
const slugIndex = buildSectionSlugIndex(sections);
|
|
217
|
+
const sectionFor = (candidate) => {
|
|
218
|
+
const key = candidate.toLowerCase();
|
|
219
|
+
return byId.get(key) ?? byId.get(slugIndex.get(key)?.toLowerCase() ?? '');
|
|
220
|
+
};
|
|
221
|
+
// Tier 1: exact full-id match
|
|
222
|
+
const literalExact = flat.filter((s) => s.id.toLowerCase() === q);
|
|
223
|
+
const slugExact = literalExact.length === 0 && isFullPath
|
|
224
|
+
? sectionFor(normalized)
|
|
225
|
+
: undefined;
|
|
226
|
+
const exact = slugExact ? [slugExact] : literalExact;
|
|
227
|
+
const exactMatches = exact.map((s) => ({
|
|
228
|
+
section: s,
|
|
229
|
+
reason: 'exact match',
|
|
230
|
+
}));
|
|
231
|
+
if (exactMatches.length > 0 && isFullPath)
|
|
232
|
+
return exactMatches;
|
|
233
|
+
// Build file index early — used by both tier 1a and 1b
|
|
234
|
+
const fileIndex = buildFileIndex(sections);
|
|
235
|
+
// Tier 1a: bare name matches file — return root sections of that file
|
|
236
|
+
// Also checks via file index (e.g. "dev-process" → "lat.md/dev-process")
|
|
237
|
+
if (!isFullPath && exactMatches.length === 0) {
|
|
238
|
+
const matchFiles = new Set();
|
|
239
|
+
// Direct match
|
|
240
|
+
for (const s of flat) {
|
|
241
|
+
if (s.file.toLowerCase() === q &&
|
|
242
|
+
!s.id.includes('#', s.file.length + 1)) {
|
|
243
|
+
matchFiles.add(s.file);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// File index expansion (keys are lowercase)
|
|
247
|
+
const indexPaths = fileIndex.get(q) ?? [];
|
|
248
|
+
for (const p of indexPaths) {
|
|
249
|
+
matchFiles.add(p);
|
|
250
|
+
}
|
|
251
|
+
if (matchFiles.size > 0) {
|
|
252
|
+
const fileRoots = flat.filter((s) => matchFiles.has(s.file) && !s.id.includes('#', s.file.length + 1));
|
|
253
|
+
if (fileRoots.length > 0) {
|
|
254
|
+
return fileRoots.map((s) => ({
|
|
255
|
+
section: s,
|
|
256
|
+
reason: 'exact match',
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Tier 1b: file stem expansion
|
|
262
|
+
// For bare names: "locate" → matches root section of "tests/locate.md"
|
|
263
|
+
// For paths with #: "setup#Install" → expands to "guides/setup#Install"
|
|
264
|
+
const stemMatches = [];
|
|
265
|
+
if (isFullPath) {
|
|
266
|
+
// Expand file stem in the file part of the query
|
|
267
|
+
const hashIdx = normalized.indexOf('#');
|
|
268
|
+
const filePart = normalized.slice(0, hashIdx);
|
|
269
|
+
const rest = normalized.slice(hashIdx);
|
|
270
|
+
const stemPaths = fileIndex.get(filePart.toLowerCase()) ?? [];
|
|
271
|
+
// Also try filePart as a direct file path (for root-level files not in index)
|
|
272
|
+
const allPaths = stemPaths.length > 0 ? stemPaths : filePart ? [filePart] : [];
|
|
273
|
+
for (const p of allPaths) {
|
|
274
|
+
const s = sectionFor(p + rest);
|
|
275
|
+
if (s) {
|
|
276
|
+
if (exact.includes(s))
|
|
277
|
+
continue;
|
|
278
|
+
stemMatches.push({
|
|
279
|
+
section: s,
|
|
280
|
+
reason: stemPaths.length > 0
|
|
281
|
+
? `file stem expanded: ${filePart} → ${p}`
|
|
282
|
+
: 'exact match',
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Try inserting root headings: file#rest → file#h1#rest
|
|
287
|
+
const rootsOfFile = flat.filter((s) => s.file.toLowerCase() === p.toLowerCase() &&
|
|
288
|
+
!s.id.includes('#', s.file.length + 1));
|
|
289
|
+
for (const root of rootsOfFile) {
|
|
290
|
+
const match = sectionFor(root.id + rest);
|
|
291
|
+
if (match) {
|
|
292
|
+
if (exact.includes(match))
|
|
293
|
+
continue;
|
|
294
|
+
stemMatches.push({
|
|
295
|
+
section: match,
|
|
296
|
+
reason: stemPaths.length > 0
|
|
297
|
+
? `file stem expanded: ${filePart} → ${p}`
|
|
298
|
+
: 'exact match',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (stemMatches.length > 0)
|
|
304
|
+
return [...exactMatches, ...stemMatches];
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// Bare name: match root sections of files via stem index (keys lowercase)
|
|
308
|
+
const paths = fileIndex.get(q) ?? [];
|
|
309
|
+
for (const p of paths) {
|
|
310
|
+
for (const s of flat) {
|
|
311
|
+
if (exact.includes(s))
|
|
312
|
+
continue;
|
|
313
|
+
// Root sections have id = "file#heading" (exactly 2 segments)
|
|
314
|
+
if (s.file.toLowerCase() === p.toLowerCase() &&
|
|
315
|
+
!s.id.includes('#', s.file.length + 1)) {
|
|
316
|
+
stemMatches.push({ section: s, reason: 'file stem match' });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// Tier 2: exact match on trailing segments (subsection name match)
|
|
322
|
+
const seen = new Set([
|
|
323
|
+
...exact.map((s) => s.id),
|
|
324
|
+
...stemMatches.map((m) => m.section.id),
|
|
325
|
+
]);
|
|
326
|
+
const slugTailMatches = new Set();
|
|
327
|
+
if (!isFullPath) {
|
|
328
|
+
for (const [alias, canonical] of slugIndex) {
|
|
329
|
+
if (alias.slice(alias.lastIndexOf('#') + 1) === q) {
|
|
330
|
+
slugTailMatches.add(canonical.toLowerCase());
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const literalTailMatches = new Set(isFullPath
|
|
335
|
+
? []
|
|
336
|
+
: flat
|
|
337
|
+
.filter((s) => tailSegments(s.id).some((tail) => tail.toLowerCase() === q))
|
|
338
|
+
.map((s) => s.id.toLowerCase()));
|
|
339
|
+
const subsection = isFullPath
|
|
340
|
+
? []
|
|
341
|
+
: flat
|
|
342
|
+
.filter((s) => {
|
|
343
|
+
if (seen.has(s.id))
|
|
344
|
+
return false;
|
|
345
|
+
const id = s.id.toLowerCase();
|
|
346
|
+
return literalTailMatches.size > 0
|
|
347
|
+
? literalTailMatches.has(id)
|
|
348
|
+
: slugTailMatches.has(id);
|
|
349
|
+
})
|
|
350
|
+
.map((s) => ({ section: s, reason: 'section name match' }));
|
|
351
|
+
// Tier 2b: subsequence match — query segments are a subsequence of section id segments
|
|
352
|
+
// e.g. "Markdown#Resolution Rules" matches "markdown#Wiki Links#Resolution Rules"
|
|
353
|
+
// Also tries expanding the file part via the file index for short refs.
|
|
354
|
+
const seenSub = new Set([...seen, ...subsection.map((m) => m.section.id)]);
|
|
355
|
+
const qParts = q.split('#');
|
|
356
|
+
// Build query variants: original + file-index-expanded forms
|
|
357
|
+
const qVariants = [qParts];
|
|
358
|
+
if (qParts.length >= 2) {
|
|
359
|
+
const expanded = fileIndex.get(qParts[0]);
|
|
360
|
+
if (expanded) {
|
|
361
|
+
for (const exp of expanded) {
|
|
362
|
+
qVariants.push([exp.toLowerCase(), ...qParts.slice(1)]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const subsequence = qParts.length >= 2
|
|
367
|
+
? flat
|
|
368
|
+
.filter((s) => {
|
|
369
|
+
if (seenSub.has(s.id))
|
|
370
|
+
return false;
|
|
371
|
+
const sParts = s.id.toLowerCase().split('#');
|
|
372
|
+
return qVariants.some((variant) => {
|
|
373
|
+
if (sParts.length <= variant.length)
|
|
374
|
+
return false;
|
|
375
|
+
let qi = 0;
|
|
376
|
+
for (const sp of sParts) {
|
|
377
|
+
if (sp === variant[qi])
|
|
378
|
+
qi++;
|
|
379
|
+
if (qi === variant.length)
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
});
|
|
384
|
+
})
|
|
385
|
+
.map((s) => {
|
|
386
|
+
const skipped = s.id.split('#').length - qParts.length;
|
|
387
|
+
return {
|
|
388
|
+
section: s,
|
|
389
|
+
reason: `path match, ${skipped} intermediate ${skipped === 1 ? 'section' : 'sections'} skipped`,
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
: [];
|
|
393
|
+
// Tier 3: fuzzy match by edit distance on each segment tail and full id
|
|
394
|
+
const seenAll = new Set([
|
|
395
|
+
...seenSub,
|
|
396
|
+
...subsequence.map((m) => m.section.id),
|
|
397
|
+
]);
|
|
398
|
+
const fuzzy = [];
|
|
399
|
+
// For full-path queries, extract the file and heading parts so we can
|
|
400
|
+
// fuzzy-match only the heading portion when the file part matches exactly.
|
|
401
|
+
// This prevents the shared file prefix from inflating similarity scores
|
|
402
|
+
// (e.g. "cli#locat" would otherwise fuzzy-match "cli#prompt").
|
|
403
|
+
const qHashIdx = normalized.indexOf('#');
|
|
404
|
+
const qFile = qHashIdx === -1 ? null : normalized.slice(0, qHashIdx).toLowerCase();
|
|
405
|
+
const qHeading = qHashIdx === -1 ? null : normalized.slice(qHashIdx + 1).toLowerCase();
|
|
406
|
+
for (const s of flat) {
|
|
407
|
+
if (seenAll.has(s.id))
|
|
408
|
+
continue;
|
|
409
|
+
const candidates = [s.id, ...tailSegments(s.id)];
|
|
410
|
+
let best = Infinity;
|
|
411
|
+
let bestCandidate = '';
|
|
412
|
+
for (const c of candidates) {
|
|
413
|
+
let d;
|
|
414
|
+
let maxLen;
|
|
415
|
+
const cl = c.toLowerCase();
|
|
416
|
+
const cHashIdx = cl.indexOf('#');
|
|
417
|
+
// When both query and candidate have # and their file parts match,
|
|
418
|
+
// compare only the heading portions to avoid file-prefix inflation
|
|
419
|
+
if (qFile && qHeading && cHashIdx !== -1) {
|
|
420
|
+
const cFile = cl.slice(0, cHashIdx);
|
|
421
|
+
const cHeading = cl.slice(cHashIdx + 1);
|
|
422
|
+
if (cFile === qFile) {
|
|
423
|
+
d = levenshtein(cHeading, qHeading);
|
|
424
|
+
maxLen = Math.max(cHeading.length, qHeading.length);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
d = levenshtein(cl, q);
|
|
428
|
+
maxLen = Math.max(c.length, q.length);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
d = levenshtein(cl, q);
|
|
433
|
+
maxLen = Math.max(c.length, q.length);
|
|
434
|
+
}
|
|
435
|
+
if (maxLen > 0 && d / maxLen <= MAX_DISTANCE_RATIO && d < best) {
|
|
436
|
+
best = d;
|
|
437
|
+
bestCandidate = c;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (best < Infinity) {
|
|
441
|
+
fuzzy.push({ section: s, distance: best, matched: bestCandidate });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
fuzzy.sort((a, b) => a.distance - b.distance);
|
|
445
|
+
const fuzzyMatches = fuzzy.map((f) => ({
|
|
446
|
+
section: f.section,
|
|
447
|
+
reason: f.matched.toLowerCase() === f.section.id.toLowerCase()
|
|
448
|
+
? `fuzzy match, distance ${f.distance}`
|
|
449
|
+
: `fuzzy match on "${f.matched}", distance ${f.distance}`,
|
|
450
|
+
}));
|
|
451
|
+
// Sort results: shallower depth first, then fewer path segments
|
|
452
|
+
const sortKey = (s) => {
|
|
453
|
+
const pathDepth = (s.file.match(/\//g) || []).length;
|
|
454
|
+
return s.depth * 100 + pathDepth;
|
|
455
|
+
};
|
|
456
|
+
const sortedStems = [...stemMatches].sort((a, b) => sortKey(a.section) - sortKey(b.section));
|
|
457
|
+
return [
|
|
458
|
+
...exactMatches,
|
|
459
|
+
...sortedStems,
|
|
460
|
+
...subsection,
|
|
461
|
+
...subsequence,
|
|
462
|
+
...fuzzyMatches,
|
|
463
|
+
];
|
|
464
|
+
}
|