@plebeiantech/lat.md 0.12.3-fork.16 → 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 (121) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/index.js +12 -6
  3. package/dist/src/cli/init.js +3 -4
  4. package/dist/src/cli/reindex.js +4 -8
  5. package/dist/src/cli/search.d.ts +3 -2
  6. package/dist/src/cli/search.js +69 -59
  7. package/dist/src/format.d.ts +5 -0
  8. package/dist/src/format.js +32 -4
  9. package/dist/src/lattice-model.d.ts +8 -1
  10. package/dist/src/lattice-model.js +4 -1
  11. package/dist/src/lattice.d.ts +2 -2
  12. package/dist/src/lattice.js +17 -34
  13. package/dist/src/markdown-analysis.d.ts +11 -0
  14. package/dist/src/markdown-analysis.js +31 -1
  15. package/dist/src/mcp/server.js +6 -6
  16. package/dist/src/parser-cache.d.ts +1 -1
  17. package/dist/src/parser-cache.js +1 -1
  18. package/dist/src/search/cache.d.ts +3 -0
  19. package/dist/src/search/cache.js +71 -0
  20. package/dist/src/search/chunks.d.ts +23 -0
  21. package/dist/src/search/chunks.js +197 -0
  22. package/dist/src/search/db.d.ts +27 -16
  23. package/dist/src/search/db.js +108 -38
  24. package/dist/src/search/index.d.ts +5 -2
  25. package/dist/src/search/index.js +173 -69
  26. package/dist/src/search/lexical.d.ts +6 -0
  27. package/dist/src/search/lexical.js +36 -0
  28. package/dist/src/search/lock.d.ts +2 -0
  29. package/dist/src/search/lock.js +23 -0
  30. package/dist/src/search/query.d.ts +2 -2
  31. package/dist/src/search/query.js +24 -9
  32. package/dist/src/search/search.d.ts +13 -8
  33. package/dist/src/search/search.js +155 -23
  34. package/dist/src/search/types.d.ts +31 -0
  35. package/dist/src/search/types.js +1 -0
  36. package/dist/src/source-formats.d.ts +1 -1
  37. package/dist/src/source-formats.js +2 -0
  38. package/dist/src/source-parser.js +197 -1
  39. package/dist/src/view/client/assets/{GraphView-BuZg_wrj.js → GraphView-BWlRdQM4.js} +2 -2
  40. package/dist/src/view/client/assets/{MarkdownEditor-BKqJtZYv.js → MarkdownEditor-MW-Ck1O0.js} +1 -1
  41. package/dist/src/view/client/assets/{abnfDiagram-VCTEODGH-CAu3KwfO.js → abnfDiagram-VCTEODGH-Bn3RshMV.js} +1 -1
  42. package/dist/src/view/client/assets/{arc-CFnPZhKM.js → arc-En9tdFQF.js} +1 -1
  43. package/dist/src/view/client/assets/{architectureDiagram-5GKGNRK7-Cf9Kd0OA.js → architectureDiagram-5GKGNRK7-B-9J1VAM.js} +1 -1
  44. package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-dPn7g_vm.js → blockDiagram-I7D4REHJ-QlznwalP.js} +1 -1
  45. package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-DdZrwaPd.js → c4Diagram-7LVT6UL2-Bp2y8B4_.js} +1 -1
  46. package/dist/src/view/client/assets/channel-uy0fFvcW.js +1 -0
  47. package/dist/src/view/client/assets/{chunk-2Q5K7J3B-C42JuteH.js → chunk-2Q5K7J3B-CnBKL-lm.js} +1 -1
  48. package/dist/src/view/client/assets/{chunk-5VM5RSS4-BKK3z8Zy.js → chunk-5VM5RSS4-LLYMi_n6.js} +1 -1
  49. package/dist/src/view/client/assets/{chunk-F27PBJKO-qZl_uKcW.js → chunk-F27PBJKO-VIeO7VF0.js} +1 -1
  50. package/dist/src/view/client/assets/{chunk-IMKFNOWR-Bs4Rnoj_.js → chunk-IMKFNOWR-D5i67DVD.js} +1 -1
  51. package/dist/src/view/client/assets/{chunk-JWPE2WC7-BpxgV2rA.js → chunk-JWPE2WC7-D4qAtoL3.js} +1 -1
  52. package/dist/src/view/client/assets/{chunk-POPQ4Y6H-hIynofdE.js → chunk-POPQ4Y6H-BMzOyiW_.js} +1 -1
  53. package/dist/src/view/client/assets/{chunk-SVP7TREG-t3gKMqeu.js → chunk-SVP7TREG-DWy5nbfp.js} +1 -1
  54. package/dist/src/view/client/assets/{chunk-TICWLB2K-B2x394mh.js → chunk-TICWLB2K-CVqCANFB.js} +1 -1
  55. package/dist/src/view/client/assets/{chunk-XXDRQBXY-CHaVKDsH.js → chunk-XXDRQBXY-Bp23h6Tf.js} +1 -1
  56. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +1 -0
  57. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +1 -0
  58. package/dist/src/view/client/assets/{cose-bilkent-JH36ORCC-DE2-Jgqu.js → cose-bilkent-JH36ORCC-BCw7fCC2.js} +1 -1
  59. package/dist/src/view/client/assets/{cynefin-OW5HDTMX-BsEtOvRy.js → cynefin-OW5HDTMX--lYN4N9m.js} +1 -1
  60. package/dist/src/view/client/assets/{cynefinDiagram-5FMLGOSQ-BKYG1Xs8.js → cynefinDiagram-5FMLGOSQ-9sK5EV7v.js} +1 -1
  61. package/dist/src/view/client/assets/{dagre-GXQ25YYZ-hgrdNjl7.js → dagre-GXQ25YYZ-CoMdEFCq.js} +1 -1
  62. package/dist/src/view/client/assets/{diagram-S7CK7UJ4-VIx8zAxf.js → diagram-S7CK7UJ4-D13dyWdd.js} +1 -1
  63. package/dist/src/view/client/assets/{diagram-UQ7AKVKN-yfloehRm.js → diagram-UQ7AKVKN-PdwLPPPA.js} +1 -1
  64. package/dist/src/view/client/assets/{diagram-VSXAHHWV-BT_dA4Q9.js → diagram-VSXAHHWV-CtWWa_A7.js} +1 -1
  65. package/dist/src/view/client/assets/{diagram-VX7I27RA-Gfmwxx6u.js → diagram-VX7I27RA-BeCYcSU-.js} +1 -1
  66. package/dist/src/view/client/assets/{diagram-Z3DM3KII-C2yM7EOo.js → diagram-Z3DM3KII-DiWabOBM.js} +1 -1
  67. package/dist/src/view/client/assets/{ebnfDiagram-PWID7BFC-Bkb_Lj59.js → ebnfDiagram-PWID7BFC-CZQ3MD_K.js} +1 -1
  68. package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-C7wQHPye.js → erDiagram-RLTQ6QDP-B1AhXIrP.js} +1 -1
  69. package/dist/src/view/client/assets/{flowDiagram-HODETNUW-CXoKr3oI.js → flowDiagram-HODETNUW-Bhr0JM6P.js} +1 -1
  70. package/dist/src/view/client/assets/{ganttDiagram-EL5Y4UJY-DqqXKgLa.js → ganttDiagram-EL5Y4UJY-CbP8iAkg.js} +1 -1
  71. package/dist/src/view/client/assets/{gitGraphDiagram-WWUBYQGX-D_DAhSgZ.js → gitGraphDiagram-WWUBYQGX-B9uuGdLF.js} +1 -1
  72. package/dist/src/view/client/assets/{index-DAxFk0q-.css → index-DG0Ez15D.css} +1 -1
  73. package/dist/src/view/client/assets/index-nBmbYfun.js +13 -0
  74. package/dist/src/view/client/assets/{infoDiagram-27XIBGKW-B50EOats.js → infoDiagram-27XIBGKW-BrfwQ_Cq.js} +1 -1
  75. package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-D1rny9gj.js → ishikawaDiagram-5VMMS53U-CHtJ36re.js} +1 -1
  76. package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-B5UD2Xo4.js → journeyDiagram-3NMN7TZE-BChcgX1s.js} +1 -1
  77. package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-CTgey9zw.js → kanban-definition-UXKFOSKX-BnwFsKiB.js} +1 -1
  78. package/dist/src/view/client/assets/{layout-4X2n4os-.js → layout-Dnd0pLdb.js} +1 -1
  79. package/dist/src/view/client/assets/{linear-D0RIBXsp.js → linear-CFhW2ozn.js} +1 -1
  80. package/dist/src/view/client/assets/{mermaid.core-BAnH8y-D.js → mermaid.core--OP6lXtW.js} +5 -5
  81. package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-WiFsh3Jg.js → mindmap-definition-YA3MSWOX-D51y2zbm.js} +1 -1
  82. package/dist/src/view/client/assets/{pegDiagram-XKGWAZYB-DCJOrckP.js → pegDiagram-XKGWAZYB--zyQJDvI.js} +1 -1
  83. package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-C78IkUE2.js → pieDiagram-E7YTZNPT-Cj8mvpJy.js} +1 -1
  84. package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-xZ6guleB.js → quadrantDiagram-AXDQQJYC-qvhJrnBf.js} +1 -1
  85. package/dist/src/view/client/assets/{railroadDiagram-O6MQD6OU-D3PwmgX0.js → railroadDiagram-O6MQD6OU-CtWvxpSo.js} +1 -1
  86. package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-v-I8wLGH.js → requirementDiagram-BXWQKSXE-CBxen_KG.js} +1 -1
  87. package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-DBY-sdO7.js → sankeyDiagram-P5KCCOFB-B_fUkmfc.js} +1 -1
  88. package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-D9HApBFi.js → sequenceDiagram-WJ2MYXX4-Dclv4p68.js} +1 -1
  89. package/dist/src/view/client/assets/{sizeCapture-INFHLROL-ERAytLnQ.js → sizeCapture-INFHLROL-GfG0sMTZ.js} +1 -1
  90. package/dist/src/view/client/assets/{stateDiagram-D77RDMKH-BKef_Qwv.js → stateDiagram-D77RDMKH-YQWPnzxU.js} +1 -1
  91. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +1 -0
  92. package/dist/src/view/client/assets/{swimlanes-42K2YHIH-D7dS_tnF.js → swimlanes-42K2YHIH-5yiCveVS.js} +1 -1
  93. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +8 -0
  94. package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-Di1UCseG.js → timeline-definition-24CTP7MA-BvAbB5Zs.js} +1 -1
  95. package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-g7Je3H2H.js → vennDiagram-4TSXK5OY-guC-r60n.js} +1 -1
  96. package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-C_diPEka.js → wardleyDiagram-VM6X3IG4-C0yXcU2x.js} +1 -1
  97. package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-C8uIQy_I.js → xychartDiagram-S5SC5T6Z-RPmkPNfK.js} +1 -1
  98. package/dist/src/view/client/index.html +2 -2
  99. package/dist/src/view/highlight.js +2 -0
  100. package/dist/src/view/markdown.js +15 -0
  101. package/dist/src/view/protocol.d.ts +7 -1
  102. package/dist/src/view/references.js +3 -3
  103. package/dist/src/view/repository.js +10 -2
  104. package/dist/src/view/search-result.js +7 -2
  105. package/dist/src/view/server-build.js +1 -1
  106. package/dist/src/view/server-deployment.js +2 -1
  107. package/dist/src/view/server-index-worker.js +1 -1
  108. package/dist/src/view/source-target.d.ts +2 -0
  109. package/dist/src/view/source-target.js +24 -0
  110. package/dist/src/view/store.js +3 -3
  111. package/dist/src/view/vercel-build.js +7 -0
  112. package/package.json +6 -3
  113. package/templates/AGENTS.md +1 -1
  114. package/templates/cursor-rules.md +1 -1
  115. package/templates/skill/SKILL.md +1 -1
  116. package/dist/src/view/client/assets/channel-C-byJRR7.js +0 -1
  117. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-FlqGrEwv.js +0 -1
  118. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-FlqGrEwv.js +0 -1
  119. package/dist/src/view/client/assets/index-DPuOWFj2.js +0 -10
  120. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-BePfayWI.js +0 -1
  121. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DaqCEguT.js +0 -8
@@ -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>;
@@ -1,54 +1,124 @@
1
- import { createClient } from '@libsql/client';
2
- import { mkdirSync } from 'node:fs';
1
+ import { connect } from '@tursodatabase/database';
2
+ import { existsSync, mkdirSync, mkdtempSync, copyFileSync } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
3
5
  import { join } from 'node:path';
4
- export function openDb(latDir, requestedCacheDir) {
6
+ export const INDEX_FILE = 'search.db';
7
+ export 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')";
8
+ export function hasIndex(cacheDir) {
9
+ return existsSync(join(cacheDir, INDEX_FILE));
10
+ }
11
+ /** Small SQL adapter; callers never depend on a libSQL connection. */
12
+ export class SearchDb {
13
+ path;
14
+ snapshot;
15
+ connection;
16
+ constructor(path, snapshot = false) {
17
+ this.path = path;
18
+ this.snapshot = snapshot;
19
+ }
20
+ snapshotDir;
21
+ get() {
22
+ if (this.connection)
23
+ return this.connection;
24
+ let path = this.path;
25
+ if (this.snapshot) {
26
+ this.snapshotDir = mkdtempSync(join(tmpdir(), 'lat-search-reader-'));
27
+ path = join(this.snapshotDir, 'index.db');
28
+ copyFileSync(this.path, path);
29
+ }
30
+ return (this.connection = connect(path, {
31
+ experimental: ['index_method'],
32
+ timeout: 10000,
33
+ }));
34
+ }
35
+ async execute(statement) {
36
+ const { sql, args = [] } = typeof statement === 'string' ? { sql: statement } : statement;
37
+ const db = await this.get();
38
+ const prepared = await db.prepare(sql);
39
+ try {
40
+ return { rows: await prepared.all(...args) };
41
+ }
42
+ finally {
43
+ prepared.close();
44
+ }
45
+ }
46
+ async close() {
47
+ try {
48
+ if (this.connection) {
49
+ const db = await this.connection;
50
+ await db.close();
51
+ this.connection = undefined;
52
+ }
53
+ }
54
+ finally {
55
+ if (this.snapshotDir) {
56
+ try {
57
+ await rm(this.snapshotDir, {
58
+ recursive: true,
59
+ force: true,
60
+ maxRetries: 3,
61
+ retryDelay: 20,
62
+ });
63
+ }
64
+ catch (error) {
65
+ const code = error.code;
66
+ if (process.platform !== 'win32' ||
67
+ !['EBUSY', 'EPERM', 'ENOTEMPTY'].includes(code ?? ''))
68
+ throw error;
69
+ // Native handles can outlive close on Windows; this is only an owned temp copy.
70
+ }
71
+ this.snapshotDir = undefined;
72
+ }
73
+ }
74
+ }
75
+ async checkpoint() {
76
+ await this.execute('PRAGMA wal_checkpoint(TRUNCATE)');
77
+ }
78
+ }
79
+ export function openDb(latDir, requestedCacheDir, readOnly = false) {
5
80
  const cacheDir = requestedCacheDir ?? join(latDir, '.cache');
6
81
  mkdirSync(cacheDir, { recursive: true });
7
- const client = createClient({
8
- url: `file:${join(cacheDir, 'vectors.db')}`,
9
- });
10
- return client;
82
+ return new SearchDb(join(cacheDir, INDEX_FILE), readOnly);
11
83
  }
12
- /** Create the `meta` key/value table if absent. */
13
84
  export async function ensureMeta(db) {
14
- await db.execute(`CREATE TABLE IF NOT EXISTS meta (
15
- key TEXT PRIMARY KEY,
16
- value TEXT NOT NULL
17
- )`);
18
- }
19
- /**
20
- * The embedding backend this index was built with, e.g. `local:minilm-l6-v2:384`
21
- * or `openai:1536`. This record is *authoritative*: it governs which backend
22
- * `lat search` uses. Null when the index has never been built.
23
- */
85
+ await db.execute('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
86
+ }
24
87
  export async function getStoredModel(db) {
25
- const rows = await db.execute("SELECT value FROM meta WHERE key = 'embedding_model'");
88
+ const rows = await db.execute("SELECT value FROM meta WHERE key='embedding_model'");
26
89
  return rows.rows[0]?.value ?? null;
27
90
  }
28
91
  export async function setStoredModel(db, value) {
29
92
  await db.execute({
30
- sql: "INSERT OR REPLACE INTO meta (key, value) VALUES ('embedding_model', ?)",
31
- args: [value],
93
+ sql: 'INSERT OR REPLACE INTO meta VALUES (?,?)',
94
+ args: ['embedding_model', value],
32
95
  });
33
96
  }
34
- /** Create the `sections` table (fixed-width vector column) + index if absent. */
35
- export async function ensureSectionsSchema(db, dimensions) {
36
- await db.execute(`CREATE TABLE IF NOT EXISTS sections (
37
- id TEXT PRIMARY KEY,
38
- file TEXT NOT NULL,
39
- heading TEXT NOT NULL,
40
- content TEXT NOT NULL,
41
- content_hash TEXT NOT NULL,
42
- embedding F32_BLOB(${dimensions}),
43
- updated_at INTEGER NOT NULL
44
- )`);
45
- await db.execute(`CREATE INDEX IF NOT EXISTS sections_vec_idx
46
- ON sections (libsql_vector_idx(embedding))`);
47
- }
48
- /** Drop the vector table (used by `lat reindex` before a full rebuild). */
97
+ export async function ensureSectionsSchema(db, _dimensions) {
98
+ for (const sql of [
99
+ 'CREATE TABLE IF NOT EXISTS sections (id TEXT PRIMARY KEY, file TEXT NOT NULL, heading TEXT NOT NULL, content TEXT NOT NULL, content_hash TEXT NOT NULL, parent_id TEXT, start_line INTEGER, end_line INTEGER)',
100
+ 'CREATE TABLE IF NOT EXISTS embeddings (hash TEXT PRIMARY KEY, embedding BLOB NOT NULL)',
101
+ 'CREATE TABLE IF NOT EXISTS chunks (id INTEGER PRIMARY KEY, source_id TEXT UNIQUE NOT NULL, section_id TEXT NOT NULL, ordinal INTEGER NOT NULL, type TEXT NOT NULL, spans TEXT NOT NULL, body TEXT NOT NULL, heading TEXT NOT NULL, path TEXT NOT NULL, input_hash TEXT NOT NULL)',
102
+ 'CREATE INDEX IF NOT EXISTS chunks_section ON chunks(section_id)',
103
+ 'CREATE TABLE IF NOT EXISTS lexical_chunks (id INTEGER PRIMARY KEY, body TEXT NOT NULL, heading TEXT NOT NULL, path TEXT NOT NULL)',
104
+ 'CREATE TABLE IF NOT EXISTS identifiers (token TEXT NOT NULL, chunk_id INTEGER NOT NULL, PRIMARY KEY(token,chunk_id))',
105
+ ])
106
+ await db.execute(sql);
107
+ const oldIndex = (await db.execute("SELECT tbl_name FROM sqlite_master WHERE name='chunks_fts'")).rows[0];
108
+ if (oldIndex?.tbl_name === 'chunks')
109
+ await db.execute('DROP INDEX chunks_fts');
110
+ await db.execute(CREATE_PASSAGE_FTS);
111
+ }
49
112
  export async function dropSections(db) {
50
- await db.execute('DROP TABLE IF EXISTS sections');
113
+ for (const name of [
114
+ 'identifiers',
115
+ 'lexical_chunks',
116
+ 'chunks',
117
+ 'embeddings',
118
+ 'sections',
119
+ ])
120
+ await db.execute(`DROP TABLE IF EXISTS ${name}`);
51
121
  }
52
122
  export async function closeDb(db) {
53
- db.close();
123
+ await db.close();
54
124
  }
@@ -1,10 +1,13 @@
1
- import type { Client } from '@libsql/client';
1
+ import { type SearchDb } from './db.js';
2
2
  import { type MarkdownProjectAnalysis } from '../project-analysis.js';
3
3
  import type { Embedder } from './embedder.js';
4
+ export declare function projectFingerprint(project: MarkdownProjectAnalysis): string;
4
5
  export type IndexStats = {
5
6
  added: number;
6
7
  updated: number;
7
8
  removed: number;
8
9
  unchanged: number;
9
10
  };
10
- export declare function indexSections(latDir: string, db: Client, embedder: Embedder, onProgress?: (done: number, total: number) => void, analyzedProject?: MarkdownProjectAnalysis): Promise<IndexStats>;
11
+ /** Exact identifier tokens complement FTS's punctuation and long-token handling. */
12
+ export declare function identifierTokens(text: string): string[];
13
+ export declare function indexSections(latDir: string, db: SearchDb, embedder: Embedder, onProgress?: (done: number, total: number) => void, analyzedProject?: MarkdownProjectAnalysis): Promise<IndexStats>;