@plebeiantech/lat.md 0.12.3-fork.15 → 0.12.3-fork.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/checklist-menu.d.ts +2 -1
  3. package/dist/src/cli/checklist-menu.js +3 -2
  4. package/dist/src/cli/index.js +59 -16
  5. package/dist/src/cli/init-preferences.d.ts +4 -0
  6. package/dist/src/cli/init-preferences.js +35 -0
  7. package/dist/src/cli/init.js +10 -5
  8. package/dist/src/cli/reindex.js +4 -8
  9. package/dist/src/cli/search.d.ts +3 -2
  10. package/dist/src/cli/search.js +71 -66
  11. package/dist/src/cli/ui-build-server.d.ts +12 -0
  12. package/dist/src/cli/ui-build-server.js +26 -0
  13. package/dist/src/cli/ui-build.js +1 -1
  14. package/dist/src/cli/ui.js +1 -1
  15. package/dist/src/format.d.ts +5 -0
  16. package/dist/src/format.js +32 -4
  17. package/dist/src/lattice-model.d.ts +8 -1
  18. package/dist/src/lattice-model.js +4 -1
  19. package/dist/src/lattice.d.ts +2 -2
  20. package/dist/src/lattice.js +17 -34
  21. package/dist/src/markdown-analysis.d.ts +11 -0
  22. package/dist/src/markdown-analysis.js +31 -1
  23. package/dist/src/mcp/server.js +6 -6
  24. package/dist/src/parser-cache.d.ts +1 -1
  25. package/dist/src/parser-cache.js +1 -1
  26. package/dist/src/search/cache.d.ts +3 -0
  27. package/dist/src/search/cache.js +71 -0
  28. package/dist/src/search/chunks.d.ts +23 -0
  29. package/dist/src/search/chunks.js +197 -0
  30. package/dist/src/search/db.d.ts +27 -16
  31. package/dist/src/search/db.js +108 -38
  32. package/dist/src/search/index.d.ts +5 -2
  33. package/dist/src/search/index.js +173 -69
  34. package/dist/src/search/lexical.d.ts +6 -0
  35. package/dist/src/search/lexical.js +36 -0
  36. package/dist/src/search/lock.d.ts +2 -0
  37. package/dist/src/search/lock.js +23 -0
  38. package/dist/src/search/query.d.ts +6 -3
  39. package/dist/src/search/query.js +45 -26
  40. package/dist/src/search/search.d.ts +13 -8
  41. package/dist/src/search/search.js +155 -23
  42. package/dist/src/search/types.d.ts +31 -0
  43. package/dist/src/search/types.js +1 -0
  44. package/dist/src/source-formats.d.ts +1 -1
  45. package/dist/src/source-formats.js +4 -0
  46. package/dist/src/source-parser.js +397 -1
  47. package/dist/src/view/client/assets/GraphView-BWlRdQM4.js +456 -0
  48. package/dist/src/view/client/assets/{MarkdownEditor-B7quqPb8.js → MarkdownEditor-MW-Ck1O0.js} +8 -8
  49. package/dist/src/view/client/assets/_commonjsHelpers-CqkleIqs.js +1 -0
  50. package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-Bn3RshMV.js +1 -0
  51. package/dist/src/view/client/assets/{arc-BnwR8Bu7.js → arc-En9tdFQF.js} +1 -1
  52. package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-B-9J1VAM.js +36 -0
  53. package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-DETl8e_H.js → blockDiagram-I7D4REHJ-QlznwalP.js} +2 -2
  54. package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-DJRAT1Ve.js → c4Diagram-7LVT6UL2-Bp2y8B4_.js} +2 -2
  55. package/dist/src/view/client/assets/channel-uy0fFvcW.js +1 -0
  56. package/dist/src/view/client/assets/{chunk-2Q5K7J3B-C0TmOC1Q.js → chunk-2Q5K7J3B-CnBKL-lm.js} +1 -1
  57. package/dist/src/view/client/assets/{chunk-5VM5RSS4-dfBOrYE6.js → chunk-5VM5RSS4-LLYMi_n6.js} +1 -1
  58. package/dist/src/view/client/assets/{chunk-F27PBJKO-DAnks99K.js → chunk-F27PBJKO-VIeO7VF0.js} +1 -1
  59. package/dist/src/view/client/assets/{chunk-IMKFNOWR-CPjB07if.js → chunk-IMKFNOWR-D5i67DVD.js} +1 -1
  60. package/dist/src/view/client/assets/{chunk-JWPE2WC7-VvU_gZI6.js → chunk-JWPE2WC7-D4qAtoL3.js} +1 -1
  61. package/dist/src/view/client/assets/{chunk-POPQ4Y6H-DDUbVF55.js → chunk-POPQ4Y6H-BMzOyiW_.js} +1 -1
  62. package/dist/src/view/client/assets/{chunk-SVP7TREG-BHROkr6D.js → chunk-SVP7TREG-DWy5nbfp.js} +1 -1
  63. package/dist/src/view/client/assets/{chunk-TICWLB2K-95PzGIOg.js → chunk-TICWLB2K-CVqCANFB.js} +1 -1
  64. package/dist/src/view/client/assets/{chunk-XXDRQBXY-MdmuLhIs.js → chunk-XXDRQBXY-Bp23h6Tf.js} +1 -1
  65. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +1 -0
  66. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +1 -0
  67. package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-BCw7fCC2.js +1 -0
  68. package/dist/src/view/client/assets/{cynefin-OW5HDTMX-CeA8IKWo.js → cynefin-OW5HDTMX--lYN4N9m.js} +1 -1
  69. package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-9sK5EV7v.js +62 -0
  70. package/dist/src/view/client/assets/{dagre-GXQ25YYZ-DLQwTa3H.js → dagre-GXQ25YYZ-CoMdEFCq.js} +2 -2
  71. package/dist/src/view/client/assets/{diagram-S7CK7UJ4-kbKdIyXG.js → diagram-S7CK7UJ4-D13dyWdd.js} +2 -2
  72. package/dist/src/view/client/assets/diagram-UQ7AKVKN-PdwLPPPA.js +41 -0
  73. package/dist/src/view/client/assets/{diagram-VSXAHHWV-EXJTU8qq.js → diagram-VSXAHHWV-CtWWa_A7.js} +2 -2
  74. package/dist/src/view/client/assets/diagram-VX7I27RA-BeCYcSU-.js +24 -0
  75. package/dist/src/view/client/assets/diagram-Z3DM3KII-DiWabOBM.js +24 -0
  76. package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-CZQ3MD_K.js +1 -0
  77. package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-C925J5A3.js → erDiagram-RLTQ6QDP-B1AhXIrP.js} +2 -2
  78. package/dist/src/view/client/assets/{flowDiagram-HODETNUW-DzoCa-Nq.js → flowDiagram-HODETNUW-Bhr0JM6P.js} +1 -1
  79. package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-CbP8iAkg.js +292 -0
  80. package/dist/src/view/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  81. package/dist/src/view/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  82. package/dist/src/view/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  83. package/dist/src/view/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  84. package/dist/src/view/client/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  85. package/dist/src/view/client/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  86. package/dist/src/view/client/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  87. package/dist/src/view/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  88. package/dist/src/view/client/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  89. package/dist/src/view/client/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  90. package/dist/src/view/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  91. package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B9uuGdLF.js +106 -0
  92. package/dist/src/view/client/assets/index-DG0Ez15D.css +1 -0
  93. package/dist/src/view/client/assets/index-nBmbYfun.js +13 -0
  94. package/dist/src/view/client/assets/infoDiagram-27XIBGKW-BrfwQ_Cq.js +2 -0
  95. package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-CW4mto2d.js → ishikawaDiagram-5VMMS53U-CHtJ36re.js} +2 -2
  96. package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-8R4eacFW.js → journeyDiagram-3NMN7TZE-BChcgX1s.js} +2 -2
  97. package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-Cy9ioXEJ.js → kanban-definition-UXKFOSKX-BnwFsKiB.js} +6 -6
  98. package/dist/src/view/client/assets/{layout-C0MFuhth.js → layout-Dnd0pLdb.js} +1 -1
  99. package/dist/src/view/client/assets/{linear-Dv3N4dQR.js → linear-CFhW2ozn.js} +1 -1
  100. package/dist/src/view/client/assets/{maplibre-gl-DnqZgfUS.js → maplibre-gl-CDX6Zuow.js} +1 -1
  101. package/dist/src/view/client/assets/{mermaid.core-bds9A7e2.js → mermaid.core--OP6lXtW.js} +11 -11
  102. package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-DjmGW355.js → mindmap-definition-YA3MSWOX-D51y2zbm.js} +2 -2
  103. package/dist/src/view/client/assets/pegDiagram-XKGWAZYB--zyQJDvI.js +1 -0
  104. package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-CCH2gGR-.js → pieDiagram-E7YTZNPT-Cj8mvpJy.js} +2 -2
  105. package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-CjocPJFz.js → quadrantDiagram-AXDQQJYC-qvhJrnBf.js} +4 -4
  106. package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-CtWvxpSo.js +1 -0
  107. package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-Ct6AoXRr.js → requirementDiagram-BXWQKSXE-CBxen_KG.js} +2 -2
  108. package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-CIpgGN_w.js → sankeyDiagram-P5KCCOFB-B_fUkmfc.js} +2 -2
  109. package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-DrmLFj6w.js → sequenceDiagram-WJ2MYXX4-Dclv4p68.js} +2 -2
  110. package/dist/src/view/client/assets/sizeCapture-INFHLROL-GfG0sMTZ.js +1 -0
  111. package/dist/src/view/client/assets/stateDiagram-D77RDMKH-YQWPnzxU.js +1 -0
  112. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +1 -0
  113. package/dist/src/view/client/assets/swimlanes-42K2YHIH-5yiCveVS.js +1 -0
  114. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +8 -0
  115. package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-BExPu6X5.js → timeline-definition-24CTP7MA-BvAbB5Zs.js} +2 -2
  116. package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-PC1MU3si.js → vennDiagram-4TSXK5OY-guC-r60n.js} +2 -2
  117. package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-DNWqU3fa.js → wardleyDiagram-VM6X3IG4-C0yXcU2x.js} +2 -2
  118. package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-E1uA28_D.js → xychartDiagram-S5SC5T6Z-RPmkPNfK.js} +2 -2
  119. package/dist/src/view/client/index.html +2 -2
  120. package/dist/src/view/document-route.d.ts +2 -0
  121. package/dist/src/view/document-route.js +40 -7
  122. package/dist/src/view/graph.d.ts +2 -2
  123. package/dist/src/view/graph.js +7 -3
  124. package/dist/src/view/highlight.js +207 -203
  125. package/dist/src/view/markdown.js +19 -0
  126. package/dist/src/view/preindexed-search.d.ts +15 -0
  127. package/dist/src/view/preindexed-search.js +25 -0
  128. package/dist/src/view/protocol.d.ts +10 -1
  129. package/dist/src/view/references.js +6 -7
  130. package/dist/src/view/repository.js +10 -2
  131. package/dist/src/view/search-result.d.ts +4 -0
  132. package/dist/src/view/search-result.js +25 -0
  133. package/dist/src/view/search.d.ts +7 -2
  134. package/dist/src/view/search.js +23 -30
  135. package/dist/src/view/server-build.d.ts +21 -0
  136. package/dist/src/view/server-build.js +145 -0
  137. package/dist/src/view/server-deployment.d.ts +32 -0
  138. package/dist/src/view/server-deployment.js +134 -0
  139. package/dist/src/view/server-index-worker.d.ts +6 -0
  140. package/dist/src/view/server-index-worker.js +14 -0
  141. package/dist/src/view/server.js +25 -7
  142. package/dist/src/view/source-target.d.ts +2 -0
  143. package/dist/src/view/source-target.js +24 -0
  144. package/dist/src/view/static-build.d.ts +16 -1
  145. package/dist/src/view/static-build.js +187 -89
  146. package/dist/src/view/static-protocol.d.ts +7 -0
  147. package/dist/src/view/static-protocol.js +5 -0
  148. package/dist/src/view/store.js +17 -7
  149. package/dist/src/view/vercel-build.d.ts +24 -0
  150. package/dist/src/view/vercel-build.js +140 -0
  151. package/dist/src/view/vercel-server-build.d.ts +10 -0
  152. package/dist/src/view/vercel-server-build.js +61 -0
  153. package/dist/vendor/lat.md-server/index.d.ts +1 -1
  154. package/dist/vendor/lat.md-server/index.js +1 -1
  155. package/package.json +20 -5
  156. package/templates/AGENTS.md +1 -1
  157. package/templates/cursor-rules.md +1 -1
  158. package/templates/skill/SKILL.md +1 -1
  159. package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +0 -1
  160. package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +0 -36
  161. package/dist/src/view/client/assets/channel-BwQbnWCN.js +0 -1
  162. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +0 -1
  163. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +0 -1
  164. package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +0 -1
  165. package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +0 -62
  166. package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +0 -41
  167. package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +0 -24
  168. package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +0 -24
  169. package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +0 -1
  170. package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +0 -292
  171. package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +0 -106
  172. package/dist/src/view/client/assets/index-D0ajr_Ja.css +0 -1
  173. package/dist/src/view/client/assets/index-KZcnpXJH.js +0 -321
  174. package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +0 -2
  175. package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +0 -1
  176. package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +0 -1
  177. package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +0 -1
  178. package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +0 -1
  179. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +0 -1
  180. package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +0 -1
  181. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +0 -8
  182. /package/dist/src/view/client/assets/{logo-small-RR_KBOAT.svg → logo-RR_KBOAT.svg} +0 -0
@@ -1,3 +1,4 @@
1
+ import type { SearchEvidence, SearchDiagnostics } from './search/types.js';
1
2
  import type { ForkFrontmatter } from './fork/frontmatter-fields.js';
2
3
  export type Section = {
3
4
  id: string;
@@ -72,6 +73,12 @@ export declare function resolveRef(target: string, sectionIds: Set<string>, file
72
73
  export type SectionMatch = {
73
74
  section: Section;
74
75
  reason: string;
75
- score?: number;
76
+ rankScore?: number;
77
+ semanticSimilarity?: number;
78
+ lexicalScore?: number;
79
+ semanticRank?: number;
80
+ lexicalRank?: number;
81
+ evidence?: SearchEvidence[];
82
+ diagnostics?: SearchDiagnostics;
76
83
  };
77
84
  export declare function findSections(sections: Section[], query: string): SectionMatch[];
@@ -94,8 +94,11 @@ export function buildSectionSlugIndex(sections) {
94
94
  const levels = [
95
95
  ...stack,
96
96
  {
97
+ id: section.id,
97
98
  depth: section.depth,
98
- literal: section.heading.toLowerCase(),
99
+ literal: section.id
100
+ .slice((stack.at(-1)?.id ?? section.file).length + 1)
101
+ .toLowerCase(),
99
102
  slug: headingSlug.toLowerCase(),
100
103
  },
101
104
  ];
@@ -1,4 +1,4 @@
1
- import type { LatFrontmatter, MdLink, Ref, Section } from './lattice-model.js';
1
+ import { type LatFrontmatter, type MdLink, type Ref, type Section } from './lattice-model.js';
2
2
  import type { Root } from 'mdast';
3
3
  import type { Profiler } from './profiler.js';
4
4
  export { buildFileIndex, buildSectionSlugIndex, findSections, flattenSections, resolveRef, } from './lattice-model.js';
@@ -9,6 +9,6 @@ export type { ForkFrontmatter, FrontmatterProblem, } from './fork/frontmatter-fi
9
9
  export declare function parseFrontmatter(content: string): LatFrontmatter;
10
10
  export declare function parseSections(filePath: string, content: string, projectRoot?: string, tree?: Root): Section[];
11
11
  export declare function loadAllSections(latticeDir: string, projectRoot?: string, profile?: Profiler): Promise<Section[]>;
12
- export declare function extractRefs(filePath: string, content: string, projectRoot?: string, tree?: Root): Ref[];
12
+ export declare function extractRefs(filePath: string, content: string, projectRoot?: string, tree?: Root, sections?: Section[]): Ref[];
13
13
  /** Extract link destinations and undefined reference syntax. */
14
14
  export declare function extractLinks(content: string, tree?: Root): MdLink[];
@@ -6,6 +6,7 @@ import { parse } from './parser.js';
6
6
  import { toPosix } from './path.js';
7
7
  import { visit } from 'unist-util-visit';
8
8
  import { listLatticeFiles } from './project-discovery.js';
9
+ import { flattenSections, } from './lattice-model.js';
9
10
  import { LAT_FIELDS, } from './fork/frontmatter-fields.js';
10
11
  export { buildFileIndex, buildSectionSlugIndex, findSections, flattenSections, resolveRef, } from './lattice-model.js';
11
12
  export { findLatticeDir, findProjectRoot, listLatticeFiles, } from './project-discovery.js';
@@ -56,14 +57,8 @@ export function parseFrontmatter(content) {
56
57
  result.problems = problems;
57
58
  return result;
58
59
  }
59
- function headingText(node) {
60
- return node.children
61
- .filter((c) => c.type === 'text')
62
- .map((c) => c.value)
63
- .join('');
64
- }
65
60
  /** Extract the rendered text GitHub uses as input to its heading slugger. */
66
- function githubHeadingText(node) {
61
+ function headingText(node) {
67
62
  if (!node || typeof node !== 'object')
68
63
  return '';
69
64
  const value = node;
@@ -77,7 +72,7 @@ function githubHeadingText(node) {
77
72
  return value.data?.alias ?? value.value ?? '';
78
73
  }
79
74
  if (value.children) {
80
- return value.children.map(githubHeadingText).join('');
75
+ return value.children.map(headingText).join('');
81
76
  }
82
77
  return '';
83
78
  }
@@ -112,6 +107,8 @@ export function parseSections(filePath, content, projectRoot, tree = parse(conte
112
107
  const stack = [];
113
108
  const flat = [];
114
109
  const slugger = new GithubSlugger();
110
+ const usedIds = new Set();
111
+ const nextSuffix = new Map();
115
112
  visit(tree, 'heading', (node) => {
116
113
  const heading = headingText(node);
117
114
  const depth = node.depth;
@@ -121,7 +118,14 @@ export function parseSections(filePath, content, projectRoot, tree = parse(conte
121
118
  stack.pop();
122
119
  }
123
120
  const parent = stack.length > 0 ? stack[stack.length - 1] : null;
124
- const id = parent ? `${parent.id}#${heading}` : `${file}#${heading}`;
121
+ const baseId = parent ? `${parent.id}#${heading}` : `${file}#${heading}`;
122
+ let id = baseId;
123
+ const key = baseId.toLowerCase();
124
+ let suffix = nextSuffix.get(key) ?? 1;
125
+ while (usedIds.has(id.toLowerCase()))
126
+ id = `${baseId}-${suffix++}`;
127
+ nextSuffix.set(key, suffix);
128
+ usedIds.add(id.toLowerCase());
125
129
  const section = {
126
130
  id,
127
131
  heading,
@@ -132,7 +136,7 @@ export function parseSections(filePath, content, projectRoot, tree = parse(conte
132
136
  startLine,
133
137
  endLine: 0,
134
138
  firstParagraph: '',
135
- githubSlug: slugger.slug(githubHeadingText(node)),
139
+ githubSlug: slugger.slug(headingText(node)),
136
140
  };
137
141
  if (parent) {
138
142
  parent.children.push(section);
@@ -190,34 +194,13 @@ export async function loadAllSections(latticeDir, projectRoot = dirname(latticeD
190
194
  }
191
195
  return all;
192
196
  }
193
- export function extractRefs(filePath, content, projectRoot, tree = parse(content)) {
197
+ export function extractRefs(filePath, content, projectRoot, tree = parse(content), sections = parseSections(filePath, content, projectRoot, tree)) {
194
198
  const file = projectRoot
195
199
  ? toPosix(relative(projectRoot, filePath)).replace(/\.md$/, '')
196
200
  : basename(filePath, '.md');
197
201
  const refs = [];
198
- // Build a flat list of sections to determine enclosing section for each wiki link
199
- const flat = [];
200
- visit(tree, 'heading', (node) => {
201
- flat.push({
202
- id: '', // filled below
203
- startLine: node.position.start.line,
204
- });
205
- });
206
- // Re-derive ids using the same logic as parseSections
207
- const stack = [];
208
- let idx = 0;
209
- visit(tree, 'heading', (node) => {
210
- const heading = headingText(node);
211
- const depth = node.depth;
212
- while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
213
- stack.pop();
214
- }
215
- const parent = stack.length > 0 ? stack[stack.length - 1] : null;
216
- const id = parent ? `${parent.id}#${heading}` : `${file}#${heading}`;
217
- flat[idx].id = id;
218
- stack.push({ id, depth });
219
- idx++;
220
- });
202
+ // Reuse canonical identities, including duplicate suffixes and formatted text.
203
+ const flat = flattenSections(sections);
221
204
  visit(tree, 'wikiLink', (node) => {
222
205
  const line = node.position.start.line;
223
206
  // Find enclosing section: last heading with startLine <= link line
@@ -6,6 +6,16 @@ export type MarkdownParagraph = {
6
6
  endLine: number;
7
7
  text: string;
8
8
  };
9
+ /** Source-positioned block facts; never retains the parser AST. */
10
+ export type MarkdownBlock = {
11
+ type: string;
12
+ start: number;
13
+ end: number;
14
+ startLine: number;
15
+ endLine: number;
16
+ language?: string;
17
+ children: MarkdownBlock[];
18
+ };
9
19
  export type MarkdownDestinationLink = {
10
20
  kind: 'image' | 'link';
11
21
  line: number;
@@ -37,6 +47,7 @@ export type MarkdownFileAnalysis = {
37
47
  headingTitles: string[];
38
48
  wikiRefs: Ref[];
39
49
  paragraphs: MarkdownParagraph[];
50
+ blocks: MarkdownBlock[];
40
51
  markdownLinks: MarkdownDestinationLink[];
41
52
  validationLinks: MdLink[];
42
53
  indexEntries: string[];
@@ -5,6 +5,35 @@ import { extractLinks, extractRefs, parseFrontmatter, parseSections, } from './l
5
5
  import { parse } from './parser.js';
6
6
  import { toPosix } from './path.js';
7
7
  import { analyzeLocalMarkdownDiagnostics, } from './markdown-validation.js';
8
+ function extractBlocks(tree) {
9
+ const block = (node) => {
10
+ if (node.position?.start.offset === undefined ||
11
+ node.position?.end.offset === undefined)
12
+ return;
13
+ return {
14
+ type: node.type,
15
+ start: node.position.start.offset,
16
+ end: node.position.end.offset,
17
+ startLine: node.position.start.line,
18
+ endLine: node.position.end.line,
19
+ ...('lang' in node && node.lang ? { language: node.lang } : {}),
20
+ children: [
21
+ 'list',
22
+ 'listItem',
23
+ 'blockquote',
24
+ 'table',
25
+ 'tableRow',
26
+ ].includes(node.type)
27
+ ? ('children' in node ? node.children : [])
28
+ .map(block)
29
+ .filter((item) => !!item)
30
+ : [],
31
+ };
32
+ };
33
+ return tree.children
34
+ .map(block)
35
+ .filter((item) => !!item);
36
+ }
8
37
  function elapsed(work) {
9
38
  const start = performance.now();
10
39
  return [work(), performance.now() - start];
@@ -96,7 +125,7 @@ function extractIndexEntries(content) {
96
125
  export function analyzeMarkdownFile(absolutePath, content, latDir, projectRoot) {
97
126
  const [tree, parseMs] = elapsed(() => parse(content));
98
127
  const [sections, sectionsMs] = elapsed(() => parseSections(absolutePath, content, projectRoot, tree));
99
- const [wikiRefs, refsMs] = elapsed(() => extractRefs(absolutePath, content, projectRoot, tree));
128
+ const [wikiRefs, refsMs] = elapsed(() => extractRefs(absolutePath, content, projectRoot, tree, sections));
100
129
  const [validationLinks, linksMs] = elapsed(() => extractLinks(content, tree));
101
130
  const [[paragraphs, markdownLinks, headingTitles], paragraphsMs] = elapsed(() => [
102
131
  extractParagraphs(content, tree),
@@ -116,6 +145,7 @@ export function analyzeMarkdownFile(absolutePath, content, latDir, projectRoot)
116
145
  headingTitles,
117
146
  wikiRefs,
118
147
  paragraphs,
148
+ blocks: extractBlocks(tree),
119
149
  markdownLinks,
120
150
  validationLinks,
121
151
  indexEntries,
@@ -7,7 +7,7 @@ import { plainStyler } from '../context.js';
7
7
  import { locateCommand } from '../cli/locate.js';
8
8
  import { sectionCommand } from '../cli/section.js';
9
9
  import { searchCommand } from '../cli/search.js';
10
- import { DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_THRESHOLD, } from '../search/search.js';
10
+ import { DEFAULT_SEARCH_LIMIT, DEFAULT_MIN_SIMILARITY, } from '../search/search.js';
11
11
  import { expandCommand } from '../cli/expand.js';
12
12
  import { checkAllCommand } from '../cli/check.js';
13
13
  import { refsCommand } from '../cli/refs.js';
@@ -40,21 +40,21 @@ export async function startMcpServer() {
40
40
  .string()
41
41
  .describe('Local section id or exact handle:path#fragment target'),
42
42
  }, async ({ query }) => toMcp(await sectionCommand(requestContext(), query)));
43
- server.tool('lat_search', 'Semantic search across lat.md sections using embeddings', {
43
+ server.tool('lat_search', 'Hybrid lexical and semantic search across lat.md sections', {
44
44
  query: z.string().describe('Search query in natural language'),
45
45
  limit: z
46
46
  .number()
47
47
  .optional()
48
48
  .default(DEFAULT_SEARCH_LIMIT)
49
49
  .describe(`Max results (default ${DEFAULT_SEARCH_LIMIT})`),
50
- threshold: z
50
+ minSimilarity: z
51
51
  .number()
52
52
  .min(0)
53
53
  .max(1)
54
54
  .optional()
55
- .default(DEFAULT_SEARCH_THRESHOLD)
56
- .describe('Minimum cosine similarity score (default 0.35)'),
57
- }, async ({ query, limit, threshold }) => toMcp(await searchCommand(requestContext(), query, { limit, threshold })));
55
+ .default(DEFAULT_MIN_SIMILARITY)
56
+ .describe('Minimum cosine similarity score (default 0.20)'),
57
+ }, async ({ query, limit, minSimilarity }) => toMcp(await searchCommand(requestContext(), query, { limit, minSimilarity })));
58
58
  server.tool('lat_expand', 'Expand [[refs]] in text to resolved lat.md section paths with context', { text: z.string().describe('Text containing [[refs]] to expand') }, async ({ text: input }) => toMcp(await expandCommand(requestContext(), input)));
59
59
  server.tool('lat_check', 'Run full lat.md validation: links, code references, indexes, and section structure', {}, async () => toMcp(await checkAllCommand(requestContext())));
60
60
  server.tool('lat_refs', 'Find sections that reference a given section via wiki links or @lat code comments', {
@@ -1,5 +1,5 @@
1
1
  /** Version of persistent parser outputs and their shared on-disk contract. */
2
- export declare const PARSER_CACHE_VERSION = 1;
2
+ export declare const PARSER_CACHE_VERSION = 3;
3
3
  export type ParsedCacheEntry = {
4
4
  version: number;
5
5
  contentHash: string;
@@ -3,7 +3,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, dirname, extname, join, relative } from 'node:path';
4
4
  import { toPosix } from './path.js';
5
5
  /** Version of persistent parser outputs and their shared on-disk contract. */
6
- export const PARSER_CACHE_VERSION = 1;
6
+ export const PARSER_CACHE_VERSION = 3;
7
7
  /** Hash complete parser input bytes for cache invalidation. */
8
8
  export function hashParserContent(content) {
9
9
  return createHash('sha1').update(content).digest('hex');
@@ -0,0 +1,3 @@
1
+ import { SearchDb } from './db.js';
2
+ /** Build a private staging database; readers only open snapshots of search.db. */
3
+ export declare function writeIndex<T>(latDir: string, cacheDir: string | undefined, rebuild: boolean, work: (db: SearchDb, storedModel: string | null) => Promise<T>): Promise<T>;
@@ -0,0 +1,71 @@
1
+ import { copyFile, mkdir, rename, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { acquireSearchLock } from './lock.js';
4
+ import { SearchDb, hasIndex, INDEX_FILE, ensureMeta, getStoredModel, } from './db.js';
5
+ /** Retry transient Windows handles without deleting the usable index first. */
6
+ async function withFileRetry(work) {
7
+ for (let attempt = 0;; attempt++) {
8
+ try {
9
+ await work();
10
+ return;
11
+ }
12
+ catch (error) {
13
+ const code = error.code;
14
+ // Windows may briefly retain a staging handle or a reader's copy handle.
15
+ if (process.platform !== 'win32' ||
16
+ attempt >= 9 ||
17
+ !['EBUSY', 'EPERM', 'EACCES'].includes(code ?? ''))
18
+ throw error;
19
+ await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
20
+ }
21
+ }
22
+ }
23
+ async function removeStaging(path) {
24
+ for (const suffix of ['', '-wal', '-shm', '-journal'])
25
+ await withFileRetry(() => rm(path + suffix, { force: true }));
26
+ }
27
+ /** Build a private staging database; readers only open snapshots of search.db. */
28
+ export async function writeIndex(latDir, cacheDir, rebuild, work) {
29
+ const dir = cacheDir ?? join(latDir, '.cache');
30
+ await mkdir(dir, { recursive: true });
31
+ const path = join(dir, '_search.db');
32
+ const activePath = join(dir, INDEX_FILE);
33
+ let db;
34
+ const release = await acquireSearchLock(dir);
35
+ try {
36
+ // A crashed writer may have left a partial database and journal behind.
37
+ await removeStaging(path);
38
+ const existing = hasIndex(dir);
39
+ if (existing && !rebuild)
40
+ await copyFile(activePath, path);
41
+ db = new SearchDb(path);
42
+ const model = existing && !rebuild ? await getStoredModel(db) : null;
43
+ await ensureMeta(db);
44
+ const changesBefore = (await db.execute('SELECT total_changes() AS n'))
45
+ .rows[0].n;
46
+ const result = await work(db, model);
47
+ const unchanged = existing &&
48
+ !rebuild &&
49
+ (await db.execute('SELECT total_changes() AS n')).rows[0].n ===
50
+ changesBefore;
51
+ await db.checkpoint();
52
+ await db.close();
53
+ db = undefined;
54
+ if (!unchanged)
55
+ await withFileRetry(() => rename(path, activePath));
56
+ return result;
57
+ }
58
+ finally {
59
+ try {
60
+ await db?.close();
61
+ }
62
+ finally {
63
+ try {
64
+ await removeStaging(path);
65
+ }
66
+ finally {
67
+ await release();
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,23 @@
1
+ import type { SourceSpan } from './types.js';
2
+ import type { Embedder } from './embedder.js';
3
+ import type { MarkdownFileAnalysis } from '../markdown-analysis.js';
4
+ import type { Section } from '../lattice-model.js';
5
+ export declare const CHUNK_POLICY = "owned-blocks-v1";
6
+ export type Passage = {
7
+ id: string;
8
+ sectionId: string;
9
+ ordinal: number;
10
+ type: string;
11
+ spans: SourceSpan[];
12
+ text: string;
13
+ input: string;
14
+ inputHash: string;
15
+ heading: string;
16
+ path: string;
17
+ };
18
+ export declare const digest: (text: string) => string;
19
+ export declare function embeddingFingerprint(embedder: Embedder): string;
20
+ /** Find a fitting Unicode-safe prefix without assuming token-count monotonicity. */
21
+ export declare function fittingPrefix(text: string, fits: (text: string) => boolean): number;
22
+ /** Every body block is owned once; headings provide context instead of copied subtrees. */
23
+ export declare function chunkFile(file: MarkdownFileAnalysis, sections: readonly Section[], embedder: Embedder): Passage[];
@@ -0,0 +1,197 @@
1
+ import { createHash } from 'node:crypto';
2
+ export const CHUNK_POLICY = 'owned-blocks-v1';
3
+ export const digest = (text) => createHash('sha256').update(text).digest('hex');
4
+ export function embeddingFingerprint(embedder) {
5
+ return `${CHUNK_POLICY}:${embedder.name}:${embedder.dimensions}:${embedder.maxInputTokens}:${embedder.tokenizerFingerprint}`;
6
+ }
7
+ /** Find a fitting Unicode-safe prefix without assuming token-count monotonicity. */
8
+ export function fittingPrefix(text, fits) {
9
+ if (fits(text))
10
+ return text.length;
11
+ let size = text.length;
12
+ while (size > 0) {
13
+ size = Math.floor(size / 2);
14
+ if (size && /[\uD800-\uDBFF]/.test(text[size - 1]))
15
+ size--;
16
+ if (size && fits(text.slice(0, size)))
17
+ return size;
18
+ }
19
+ return 0;
20
+ }
21
+ /** Every body block is owned once; headings provide context instead of copied subtrees. */
22
+ export function chunkFile(file, sections, embedder) {
23
+ const local = embedder.name.startsWith('local:');
24
+ const target = local ? 192 : 512;
25
+ const contextBudget = local ? 48 : 96;
26
+ const source = file.content;
27
+ const lineStarts = [0];
28
+ for (let i = 0; i < source.length; i++)
29
+ if (source[i] === '\n')
30
+ lineStarts.push(i + 1);
31
+ const lineAt = (offset) => {
32
+ let lo = 0, hi = lineStarts.length;
33
+ while (lo + 1 < hi) {
34
+ const mid = (lo + hi) >>> 1;
35
+ if (lineStarts[mid] <= offset)
36
+ lo = mid;
37
+ else
38
+ hi = mid;
39
+ }
40
+ return lo + 1;
41
+ };
42
+ const own = new Map();
43
+ const ordered = [...sections].sort((a, b) => a.startLine - b.startLine || a.depth - b.depth);
44
+ let sectionIndex = 0;
45
+ let ancestors = [];
46
+ for (const block of file.blocks) {
47
+ if (block.type === 'heading' || block.type === 'yaml')
48
+ continue;
49
+ while (sectionIndex < ordered.length &&
50
+ ordered[sectionIndex].startLine <= block.startLine) {
51
+ const next = ordered[sectionIndex++];
52
+ ancestors = ancestors.filter((s) => s.endLine >= next.startLine);
53
+ ancestors.push(next);
54
+ }
55
+ ancestors = ancestors.filter((s) => s.endLine >= block.endLine);
56
+ const owner = ancestors.at(-1);
57
+ if (!owner)
58
+ throw new Error(`No section owns ${file.path}:${block.startLine}`);
59
+ const blocks = own.get(owner.id) ?? [];
60
+ blocks.push(block);
61
+ own.set(owner.id, blocks);
62
+ }
63
+ const result = [];
64
+ for (const section of sections) {
65
+ const headings = section.id.split('#').slice(1);
66
+ const rawContext = `Section: ${section.heading}\nPage: ${file.headingTitles[0] ?? file.path}\nPath: ${headings.slice(0, -1).join(' > ')}`;
67
+ const contextLength = fittingPrefix(rawContext, (t) => embedder.countTokens(t) <= contextBudget);
68
+ const context = rawContext.slice(0, contextLength);
69
+ const path = headings.slice(0, -1).join(' > ');
70
+ let ordinal = 0;
71
+ const inputFor = (text, extra) => {
72
+ const extraSize = fittingPrefix(extra, (t) => embedder.countTokens(t) <= contextBudget / 2);
73
+ const fullContext = [extra.slice(0, extraSize), context]
74
+ .filter(Boolean)
75
+ .join('\n');
76
+ const n = fittingPrefix(fullContext, (c) => embedder.countTokens(c) <= contextBudget &&
77
+ embedder.countTokens(`${c}\n\n${text}`) <= embedder.maxInputTokens);
78
+ return n ? `${fullContext.slice(0, n)}\n\n${text}` : text;
79
+ };
80
+ const fits = (text, extra = '') => embedder.countTokens(text) <= target &&
81
+ embedder.countTokens(inputFor(text, extra)) <= embedder.maxInputTokens;
82
+ const split = (block, extra = '') => {
83
+ const text = source.slice(block.start, block.end);
84
+ if (fits(text, extra))
85
+ return [
86
+ { start: block.start, end: block.end, type: block.type, extra },
87
+ ];
88
+ if (block.children.length) {
89
+ const pieces = [];
90
+ let start = block.start;
91
+ for (let i = 0; i < block.children.length; i++) {
92
+ const child = block.children[i];
93
+ const end = i + 1 < block.children.length
94
+ ? block.children[i + 1].start
95
+ : block.end;
96
+ let label = extra;
97
+ if (block.type === 'table')
98
+ label = `Row: ${i}\nTable headers: ${source.slice(block.children[0].start, block.children[0].end)}\n${extra}`;
99
+ if (block.type === 'tableRow')
100
+ label = `Column: ${i + 1}\n${extra}`;
101
+ if (block.type === 'listItem')
102
+ label += `\nList item: ${text.split('\n')[0]}`;
103
+ pieces.push(...split({ ...child, start, end }, label));
104
+ start = end;
105
+ }
106
+ return pieces;
107
+ }
108
+ const pieces = [];
109
+ let start = block.start;
110
+ const label = block.type === 'code'
111
+ ? `${extra}\nCode: ${block.language ?? ''}`
112
+ : extra;
113
+ while (start < block.end) {
114
+ const rest = source.slice(start, block.end);
115
+ let n = fittingPrefix(rest, (t) => fits(t, label));
116
+ if (!n)
117
+ throw new Error(`Cannot fit source text at ${file.path}:${lineAt(start)}`);
118
+ if (n < rest.length) {
119
+ const prefix = rest.slice(0, n);
120
+ const boundary = block.type === 'code'
121
+ ? prefix.lastIndexOf('\n') + 1
122
+ : [...prefix.matchAll(/[.!?](?:\s+|$)/g)].at(-1)?.index;
123
+ const preferred = typeof boundary === 'number' && boundary > n / 2
124
+ ? boundary
125
+ : prefix.search(/\s+\S*$/);
126
+ if (preferred > n / 2 && fits(rest.slice(0, preferred), label))
127
+ n = preferred;
128
+ }
129
+ pieces.push({ start, end: start + n, type: block.type, extra: label });
130
+ start += n;
131
+ }
132
+ return pieces;
133
+ };
134
+ const pieces = (own.get(section.id) ?? []).flatMap((block) => split(block));
135
+ if (!pieces.length) {
136
+ const start = lineStarts[section.startLine - 1] ?? 0;
137
+ const end = source.indexOf('\n', start);
138
+ pieces.push(...split({
139
+ type: 'heading',
140
+ start,
141
+ end: end < 0 ? source.length : end,
142
+ startLine: section.startLine,
143
+ endLine: section.startLine,
144
+ children: [],
145
+ }));
146
+ }
147
+ let pending;
148
+ const emit = (piece) => {
149
+ const text = source.slice(piece.start, piece.end);
150
+ if (!text.trim())
151
+ return;
152
+ const input = inputFor(text, piece.extra);
153
+ if (embedder.countTokens(input) > embedder.maxInputTokens)
154
+ throw new Error('Chunk exceeds embedding limit');
155
+ const inputHash = digest(`${embeddingFingerprint(embedder)}\0${input}`);
156
+ result.push({
157
+ id: `${section.id}:${ordinal}`,
158
+ sectionId: section.id,
159
+ ordinal: ordinal++,
160
+ type: piece.type,
161
+ spans: [
162
+ {
163
+ start: piece.start,
164
+ end: piece.end,
165
+ startLine: lineAt(piece.start),
166
+ endLine: lineAt(Math.max(piece.start, piece.end - 1)),
167
+ },
168
+ ],
169
+ text,
170
+ input,
171
+ inputHash,
172
+ heading: section.heading,
173
+ path,
174
+ });
175
+ };
176
+ for (const piece of pieces) {
177
+ if (pending &&
178
+ !pending.extra &&
179
+ !piece.extra &&
180
+ !source.slice(pending.end, piece.start).trim() &&
181
+ fits(source.slice(pending.start, piece.end)))
182
+ pending = {
183
+ ...pending,
184
+ end: piece.end,
185
+ type: pending.type === piece.type ? piece.type : 'mixed',
186
+ };
187
+ else {
188
+ if (pending)
189
+ emit(pending);
190
+ pending = piece;
191
+ }
192
+ }
193
+ if (pending)
194
+ emit(pending);
195
+ }
196
+ return result;
197
+ }
@@ -1,16 +1,27 @@
1
- import { type Client } from '@libsql/client';
2
- export declare function openDb(latDir: string, requestedCacheDir?: string): Client;
3
- /** Create the `meta` key/value table if absent. */
4
- export declare function ensureMeta(db: Client): Promise<void>;
5
- /**
6
- * The embedding backend this index was built with, e.g. `local:minilm-l6-v2:384`
7
- * or `openai:1536`. This record is *authoritative*: it governs which backend
8
- * `lat search` uses. Null when the index has never been built.
9
- */
10
- export declare function getStoredModel(db: Client): Promise<string | null>;
11
- export declare function setStoredModel(db: Client, value: string): Promise<void>;
12
- /** Create the `sections` table (fixed-width vector column) + index if absent. */
13
- export declare function ensureSectionsSchema(db: Client, dimensions: number): Promise<void>;
14
- /** Drop the vector table (used by `lat reindex` before a full rebuild). */
15
- export declare function dropSections(db: Client): Promise<void>;
16
- export declare function closeDb(db: Client): Promise<void>;
1
+ export declare const INDEX_FILE = "search.db";
2
+ export declare const CREATE_PASSAGE_FTS = "CREATE INDEX IF NOT EXISTS chunks_fts ON lexical_chunks USING fts(body,heading,path) WITH (tokenizer='whitespace',weights='body=1.0,heading=2.0,path=0.5')";
3
+ export declare function hasIndex(cacheDir: string): boolean;
4
+ /** Small SQL adapter; callers never depend on a libSQL connection. */
5
+ export declare class SearchDb {
6
+ readonly path: string;
7
+ private readonly snapshot;
8
+ private connection;
9
+ constructor(path: string, snapshot?: boolean);
10
+ private snapshotDir;
11
+ private get;
12
+ execute(statement: string | {
13
+ sql: string;
14
+ args?: any[];
15
+ }): Promise<{
16
+ rows: any[];
17
+ }>;
18
+ close(): Promise<void>;
19
+ checkpoint(): Promise<void>;
20
+ }
21
+ export declare function openDb(latDir: string, requestedCacheDir?: string, readOnly?: boolean): SearchDb;
22
+ export declare function ensureMeta(db: SearchDb): Promise<void>;
23
+ export declare function getStoredModel(db: SearchDb): Promise<string | null>;
24
+ export declare function setStoredModel(db: SearchDb, value: string): Promise<void>;
25
+ export declare function ensureSectionsSchema(db: SearchDb, _dimensions: number): Promise<void>;
26
+ export declare function dropSections(db: SearchDb): Promise<void>;
27
+ export declare function closeDb(db: SearchDb): Promise<void>;