@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.
- package/README.md +1 -1
- package/dist/src/cli/checklist-menu.d.ts +2 -1
- package/dist/src/cli/checklist-menu.js +3 -2
- package/dist/src/cli/index.js +59 -16
- package/dist/src/cli/init-preferences.d.ts +4 -0
- package/dist/src/cli/init-preferences.js +35 -0
- package/dist/src/cli/init.js +10 -5
- package/dist/src/cli/reindex.js +4 -8
- package/dist/src/cli/search.d.ts +3 -2
- package/dist/src/cli/search.js +71 -66
- package/dist/src/cli/ui-build-server.d.ts +12 -0
- package/dist/src/cli/ui-build-server.js +26 -0
- package/dist/src/cli/ui-build.js +1 -1
- package/dist/src/cli/ui.js +1 -1
- package/dist/src/format.d.ts +5 -0
- package/dist/src/format.js +32 -4
- package/dist/src/lattice-model.d.ts +8 -1
- package/dist/src/lattice-model.js +4 -1
- package/dist/src/lattice.d.ts +2 -2
- package/dist/src/lattice.js +17 -34
- package/dist/src/markdown-analysis.d.ts +11 -0
- package/dist/src/markdown-analysis.js +31 -1
- package/dist/src/mcp/server.js +6 -6
- package/dist/src/parser-cache.d.ts +1 -1
- package/dist/src/parser-cache.js +1 -1
- package/dist/src/search/cache.d.ts +3 -0
- package/dist/src/search/cache.js +71 -0
- package/dist/src/search/chunks.d.ts +23 -0
- package/dist/src/search/chunks.js +197 -0
- package/dist/src/search/db.d.ts +27 -16
- package/dist/src/search/db.js +108 -38
- package/dist/src/search/index.d.ts +5 -2
- package/dist/src/search/index.js +173 -69
- package/dist/src/search/lexical.d.ts +6 -0
- package/dist/src/search/lexical.js +36 -0
- package/dist/src/search/lock.d.ts +2 -0
- package/dist/src/search/lock.js +23 -0
- package/dist/src/search/query.d.ts +6 -3
- package/dist/src/search/query.js +45 -26
- package/dist/src/search/search.d.ts +13 -8
- package/dist/src/search/search.js +155 -23
- package/dist/src/search/types.d.ts +31 -0
- package/dist/src/search/types.js +1 -0
- package/dist/src/source-formats.d.ts +1 -1
- package/dist/src/source-formats.js +4 -0
- package/dist/src/source-parser.js +397 -1
- package/dist/src/view/client/assets/GraphView-BWlRdQM4.js +456 -0
- package/dist/src/view/client/assets/{MarkdownEditor-B7quqPb8.js → MarkdownEditor-MW-Ck1O0.js} +8 -8
- package/dist/src/view/client/assets/_commonjsHelpers-CqkleIqs.js +1 -0
- package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-Bn3RshMV.js +1 -0
- package/dist/src/view/client/assets/{arc-BnwR8Bu7.js → arc-En9tdFQF.js} +1 -1
- package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-B-9J1VAM.js +36 -0
- package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-DETl8e_H.js → blockDiagram-I7D4REHJ-QlznwalP.js} +2 -2
- package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-DJRAT1Ve.js → c4Diagram-7LVT6UL2-Bp2y8B4_.js} +2 -2
- package/dist/src/view/client/assets/channel-uy0fFvcW.js +1 -0
- package/dist/src/view/client/assets/{chunk-2Q5K7J3B-C0TmOC1Q.js → chunk-2Q5K7J3B-CnBKL-lm.js} +1 -1
- package/dist/src/view/client/assets/{chunk-5VM5RSS4-dfBOrYE6.js → chunk-5VM5RSS4-LLYMi_n6.js} +1 -1
- package/dist/src/view/client/assets/{chunk-F27PBJKO-DAnks99K.js → chunk-F27PBJKO-VIeO7VF0.js} +1 -1
- package/dist/src/view/client/assets/{chunk-IMKFNOWR-CPjB07if.js → chunk-IMKFNOWR-D5i67DVD.js} +1 -1
- package/dist/src/view/client/assets/{chunk-JWPE2WC7-VvU_gZI6.js → chunk-JWPE2WC7-D4qAtoL3.js} +1 -1
- package/dist/src/view/client/assets/{chunk-POPQ4Y6H-DDUbVF55.js → chunk-POPQ4Y6H-BMzOyiW_.js} +1 -1
- package/dist/src/view/client/assets/{chunk-SVP7TREG-BHROkr6D.js → chunk-SVP7TREG-DWy5nbfp.js} +1 -1
- package/dist/src/view/client/assets/{chunk-TICWLB2K-95PzGIOg.js → chunk-TICWLB2K-CVqCANFB.js} +1 -1
- package/dist/src/view/client/assets/{chunk-XXDRQBXY-MdmuLhIs.js → chunk-XXDRQBXY-Bp23h6Tf.js} +1 -1
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +1 -0
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +1 -0
- package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-BCw7fCC2.js +1 -0
- package/dist/src/view/client/assets/{cynefin-OW5HDTMX-CeA8IKWo.js → cynefin-OW5HDTMX--lYN4N9m.js} +1 -1
- package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-9sK5EV7v.js +62 -0
- package/dist/src/view/client/assets/{dagre-GXQ25YYZ-DLQwTa3H.js → dagre-GXQ25YYZ-CoMdEFCq.js} +2 -2
- package/dist/src/view/client/assets/{diagram-S7CK7UJ4-kbKdIyXG.js → diagram-S7CK7UJ4-D13dyWdd.js} +2 -2
- package/dist/src/view/client/assets/diagram-UQ7AKVKN-PdwLPPPA.js +41 -0
- package/dist/src/view/client/assets/{diagram-VSXAHHWV-EXJTU8qq.js → diagram-VSXAHHWV-CtWWa_A7.js} +2 -2
- package/dist/src/view/client/assets/diagram-VX7I27RA-BeCYcSU-.js +24 -0
- package/dist/src/view/client/assets/diagram-Z3DM3KII-DiWabOBM.js +24 -0
- package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-CZQ3MD_K.js +1 -0
- package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-C925J5A3.js → erDiagram-RLTQ6QDP-B1AhXIrP.js} +2 -2
- package/dist/src/view/client/assets/{flowDiagram-HODETNUW-DzoCa-Nq.js → flowDiagram-HODETNUW-Bhr0JM6P.js} +1 -1
- package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-CbP8iAkg.js +292 -0
- package/dist/src/view/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/src/view/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/src/view/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/src/view/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/src/view/client/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/src/view/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B9uuGdLF.js +106 -0
- package/dist/src/view/client/assets/index-DG0Ez15D.css +1 -0
- package/dist/src/view/client/assets/index-nBmbYfun.js +13 -0
- package/dist/src/view/client/assets/infoDiagram-27XIBGKW-BrfwQ_Cq.js +2 -0
- package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-CW4mto2d.js → ishikawaDiagram-5VMMS53U-CHtJ36re.js} +2 -2
- package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-8R4eacFW.js → journeyDiagram-3NMN7TZE-BChcgX1s.js} +2 -2
- package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-Cy9ioXEJ.js → kanban-definition-UXKFOSKX-BnwFsKiB.js} +6 -6
- package/dist/src/view/client/assets/{layout-C0MFuhth.js → layout-Dnd0pLdb.js} +1 -1
- package/dist/src/view/client/assets/{linear-Dv3N4dQR.js → linear-CFhW2ozn.js} +1 -1
- package/dist/src/view/client/assets/{maplibre-gl-DnqZgfUS.js → maplibre-gl-CDX6Zuow.js} +1 -1
- package/dist/src/view/client/assets/{mermaid.core-bds9A7e2.js → mermaid.core--OP6lXtW.js} +11 -11
- package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-DjmGW355.js → mindmap-definition-YA3MSWOX-D51y2zbm.js} +2 -2
- package/dist/src/view/client/assets/pegDiagram-XKGWAZYB--zyQJDvI.js +1 -0
- package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-CCH2gGR-.js → pieDiagram-E7YTZNPT-Cj8mvpJy.js} +2 -2
- package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-CjocPJFz.js → quadrantDiagram-AXDQQJYC-qvhJrnBf.js} +4 -4
- package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-CtWvxpSo.js +1 -0
- package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-Ct6AoXRr.js → requirementDiagram-BXWQKSXE-CBxen_KG.js} +2 -2
- package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-CIpgGN_w.js → sankeyDiagram-P5KCCOFB-B_fUkmfc.js} +2 -2
- package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-DrmLFj6w.js → sequenceDiagram-WJ2MYXX4-Dclv4p68.js} +2 -2
- package/dist/src/view/client/assets/sizeCapture-INFHLROL-GfG0sMTZ.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-D77RDMKH-YQWPnzxU.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +1 -0
- package/dist/src/view/client/assets/swimlanes-42K2YHIH-5yiCveVS.js +1 -0
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +8 -0
- package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-BExPu6X5.js → timeline-definition-24CTP7MA-BvAbB5Zs.js} +2 -2
- package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-PC1MU3si.js → vennDiagram-4TSXK5OY-guC-r60n.js} +2 -2
- package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-DNWqU3fa.js → wardleyDiagram-VM6X3IG4-C0yXcU2x.js} +2 -2
- package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-E1uA28_D.js → xychartDiagram-S5SC5T6Z-RPmkPNfK.js} +2 -2
- package/dist/src/view/client/index.html +2 -2
- package/dist/src/view/document-route.d.ts +2 -0
- package/dist/src/view/document-route.js +40 -7
- package/dist/src/view/graph.d.ts +2 -2
- package/dist/src/view/graph.js +7 -3
- package/dist/src/view/highlight.js +207 -203
- package/dist/src/view/markdown.js +19 -0
- package/dist/src/view/preindexed-search.d.ts +15 -0
- package/dist/src/view/preindexed-search.js +25 -0
- package/dist/src/view/protocol.d.ts +10 -1
- package/dist/src/view/references.js +6 -7
- package/dist/src/view/repository.js +10 -2
- package/dist/src/view/search-result.d.ts +4 -0
- package/dist/src/view/search-result.js +25 -0
- package/dist/src/view/search.d.ts +7 -2
- package/dist/src/view/search.js +23 -30
- package/dist/src/view/server-build.d.ts +21 -0
- package/dist/src/view/server-build.js +145 -0
- package/dist/src/view/server-deployment.d.ts +32 -0
- package/dist/src/view/server-deployment.js +134 -0
- package/dist/src/view/server-index-worker.d.ts +6 -0
- package/dist/src/view/server-index-worker.js +14 -0
- package/dist/src/view/server.js +25 -7
- package/dist/src/view/source-target.d.ts +2 -0
- package/dist/src/view/source-target.js +24 -0
- package/dist/src/view/static-build.d.ts +16 -1
- package/dist/src/view/static-build.js +187 -89
- package/dist/src/view/static-protocol.d.ts +7 -0
- package/dist/src/view/static-protocol.js +5 -0
- package/dist/src/view/store.js +17 -7
- package/dist/src/view/vercel-build.d.ts +24 -0
- package/dist/src/view/vercel-build.js +140 -0
- package/dist/src/view/vercel-server-build.d.ts +10 -0
- package/dist/src/view/vercel-server-build.js +61 -0
- package/dist/vendor/lat.md-server/index.d.ts +1 -1
- package/dist/vendor/lat.md-server/index.js +1 -1
- package/package.json +20 -5
- package/templates/AGENTS.md +1 -1
- package/templates/cursor-rules.md +1 -1
- package/templates/skill/SKILL.md +1 -1
- package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +0 -1
- package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +0 -36
- package/dist/src/view/client/assets/channel-BwQbnWCN.js +0 -1
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +0 -1
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +0 -1
- package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +0 -1
- package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +0 -62
- package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +0 -41
- package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +0 -24
- package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +0 -24
- package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +0 -1
- package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +0 -292
- package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +0 -106
- package/dist/src/view/client/assets/index-D0ajr_Ja.css +0 -1
- package/dist/src/view/client/assets/index-KZcnpXJH.js +0 -321
- package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +0 -2
- package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +0 -1
- package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +0 -1
- package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +0 -1
- package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +0 -1
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +0 -1
- package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +0 -1
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +0 -8
- /package/dist/src/view/client/assets/{logo-small-RR_KBOAT.svg → logo-RR_KBOAT.svg} +0 -0
package/dist/src/search/db.js
CHANGED
|
@@ -1,54 +1,124 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
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(
|
|
15
|
-
|
|
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
|
|
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:
|
|
31
|
-
args: [value],
|
|
93
|
+
sql: 'INSERT OR REPLACE INTO meta VALUES (?,?)',
|
|
94
|
+
args: ['embedding_model', value],
|
|
32
95
|
});
|
|
33
96
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>;
|
package/dist/src/search/index.js
CHANGED
|
@@ -1,85 +1,189 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { PARSER_CACHE_VERSION } from '../parser-cache.js';
|
|
2
|
+
import { synchronizeLexical } from './lexical.js';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { CREATE_PASSAGE_FTS } from './db.js';
|
|
3
5
|
import { analyzeMarkdownProject, } from '../project-analysis.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
import { chunkFile, digest, embeddingFingerprint } from './chunks.js';
|
|
7
|
+
export function projectFingerprint(project) {
|
|
8
|
+
return digest(`parser:${PARSER_CACHE_VERSION}\n` +
|
|
9
|
+
[...project.files.values()]
|
|
10
|
+
.map((f) => `${f.projectPath}\0${digest(f.content)}`)
|
|
11
|
+
.sort()
|
|
12
|
+
.join('\n'));
|
|
6
13
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const lines = new Map();
|
|
15
|
-
for (const { filePath } of sections) {
|
|
16
|
-
if (lines.has(filePath))
|
|
17
|
-
continue;
|
|
18
|
-
const content = project.filesByAbsolutePath.get(join(projectRoot, filePath))?.content;
|
|
19
|
-
if (content === undefined)
|
|
20
|
-
throw new Error(`Analyzed Markdown file not found: ${filePath}`);
|
|
21
|
-
lines.set(filePath, content.split('\n'));
|
|
22
|
-
}
|
|
23
|
-
return lines;
|
|
24
|
-
}
|
|
25
|
-
function sectionContent(section, lines) {
|
|
26
|
-
return lines
|
|
27
|
-
.get(section.filePath)
|
|
28
|
-
.slice(section.startLine - 1, section.endLine)
|
|
29
|
-
.join('\n');
|
|
14
|
+
/** Exact identifier tokens complement FTS's punctuation and long-token handling. */
|
|
15
|
+
export function identifierTokens(text) {
|
|
16
|
+
return [
|
|
17
|
+
...new Set((text.match(/[\p{L}\p{N}_][\p{L}\p{N}_./:#@-]*/gu) ?? [])
|
|
18
|
+
.map((t) => t.toLowerCase())
|
|
19
|
+
.filter((t) => /[_./:#@-]/.test(t) || t.length >= 40)),
|
|
20
|
+
];
|
|
30
21
|
}
|
|
31
22
|
export async function indexSections(latDir, db, embedder, onProgress, analyzedProject) {
|
|
32
|
-
const projectRoot = dirname(latDir);
|
|
33
23
|
const project = analyzedProject ??
|
|
34
|
-
(await analyzeMarkdownProject(latDir,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
(await analyzeMarkdownProject(latDir, dirname(latDir), {
|
|
25
|
+
executor: 'auto',
|
|
26
|
+
}));
|
|
27
|
+
const fingerprint = embeddingFingerprint(embedder);
|
|
28
|
+
const projectHash = projectFingerprint(project);
|
|
29
|
+
const oldFingerprint = (await db.execute("SELECT value FROM meta WHERE key='fingerprint'")).rows[0]?.value;
|
|
30
|
+
if (oldFingerprint && oldFingerprint !== fingerprint)
|
|
31
|
+
throw new Error('Search chunking or embedding model changed; run lat reindex.');
|
|
32
|
+
const byFile = new Map();
|
|
33
|
+
for (const s of project.sections) {
|
|
34
|
+
const list = byFile.get(s.filePath) ?? [];
|
|
35
|
+
list.push(s);
|
|
36
|
+
byFile.set(s.filePath, list);
|
|
37
|
+
}
|
|
38
|
+
const files = new Map([...project.files.values()].map((f) => [f.projectPath, f]));
|
|
39
|
+
const passages = [...byFile].flatMap(([path, sections]) => {
|
|
40
|
+
const file = files.get(path);
|
|
41
|
+
if (!file)
|
|
42
|
+
throw new Error(`Missing analyzed file: ${path}`);
|
|
43
|
+
return chunkFile(file, sections, embedder);
|
|
44
|
+
});
|
|
45
|
+
const existing = new Map((await db.execute('SELECT id,content_hash FROM sections')).rows.map((r) => [
|
|
46
|
+
r.id,
|
|
47
|
+
r.content_hash,
|
|
48
|
+
]));
|
|
49
|
+
const sectionHashes = new Map();
|
|
50
|
+
const owned = new Map();
|
|
51
|
+
for (const p of passages) {
|
|
52
|
+
const list = owned.get(p.sectionId) ?? [];
|
|
53
|
+
list.push(p);
|
|
54
|
+
owned.set(p.sectionId, list);
|
|
42
55
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
for (const s of project.sections) {
|
|
57
|
+
sectionHashes.set(s.id, digest(JSON.stringify([
|
|
58
|
+
{ ...s, children: undefined },
|
|
59
|
+
(owned.get(s.id) ?? []).map((p) => [p.inputHash, p.spans]),
|
|
60
|
+
])));
|
|
48
61
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
const changed = new Set([...sectionHashes]
|
|
63
|
+
.filter(([id, hash]) => existing.get(id) !== hash)
|
|
64
|
+
.map(([id]) => id));
|
|
65
|
+
const removed = [...existing.keys()].filter((id) => !sectionHashes.has(id));
|
|
66
|
+
const stats = {
|
|
67
|
+
added: [...changed].filter((id) => !existing.has(id)).length,
|
|
68
|
+
updated: [...changed].filter((id) => existing.has(id)).length,
|
|
69
|
+
removed: removed.length,
|
|
70
|
+
unchanged: project.sections.length - changed.size,
|
|
71
|
+
};
|
|
72
|
+
if (!changed.size && !removed.length && oldFingerprint === fingerprint) {
|
|
73
|
+
// Lexical maintenance can repair old generations without embedding work.
|
|
74
|
+
await db.execute('BEGIN');
|
|
75
|
+
try {
|
|
76
|
+
await synchronizeLexical(db);
|
|
77
|
+
await db.execute({
|
|
78
|
+
sql: "UPDATE meta SET value=? WHERE key='project_hash' AND value<>?",
|
|
79
|
+
args: [projectHash, projectHash],
|
|
80
|
+
});
|
|
81
|
+
await db.execute('COMMIT');
|
|
56
82
|
}
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
catch (error) {
|
|
84
|
+
await db.execute('ROLLBACK');
|
|
85
|
+
throw error;
|
|
59
86
|
}
|
|
87
|
+
return stats;
|
|
60
88
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
const storedHashes = new Set((await db.execute('SELECT hash FROM embeddings')).rows.map((r) => r.hash));
|
|
90
|
+
const missing = new Map(passages
|
|
91
|
+
.filter((p) => !storedHashes.has(p.inputHash))
|
|
92
|
+
.map((p) => [p.inputHash, p.input]));
|
|
93
|
+
const entries = [...missing];
|
|
94
|
+
const vectors = entries.length
|
|
95
|
+
? await embedder.embed(entries.map(([, input]) => input), onProgress)
|
|
96
|
+
: [];
|
|
97
|
+
if (vectors.length !== entries.length ||
|
|
98
|
+
vectors.some((v) => v.length !== embedder.dimensions || v.some((n) => !Number.isFinite(n))))
|
|
99
|
+
throw new Error('Embedding backend returned invalid vectors');
|
|
100
|
+
const rebuildFts = !existing.size ||
|
|
101
|
+
// Tantivy retains deleted versions in BM25 statistics until rebuilt.
|
|
102
|
+
stats.updated > 0 ||
|
|
103
|
+
removed.length > 0 ||
|
|
104
|
+
passages.filter((p) => changed.has(p.sectionId)).length > 512;
|
|
105
|
+
await db.execute('BEGIN');
|
|
106
|
+
try {
|
|
107
|
+
if (rebuildFts)
|
|
108
|
+
await db.execute('DROP INDEX IF EXISTS chunks_fts');
|
|
109
|
+
for (let i = 0; i < entries.length; i++)
|
|
110
|
+
await db.execute({
|
|
111
|
+
sql: 'INSERT INTO embeddings VALUES (?,vector32(?))',
|
|
112
|
+
args: [entries[i][0], JSON.stringify(vectors[i])],
|
|
113
|
+
});
|
|
114
|
+
for (const id of [...changed, ...removed]) {
|
|
115
|
+
await db.execute({
|
|
116
|
+
sql: 'DELETE FROM lexical_chunks WHERE id IN (SELECT id FROM chunks WHERE section_id=?)',
|
|
117
|
+
args: [id],
|
|
118
|
+
});
|
|
119
|
+
await db.execute({
|
|
120
|
+
sql: 'DELETE FROM identifiers WHERE chunk_id IN (SELECT id FROM chunks WHERE section_id=?)',
|
|
121
|
+
args: [id],
|
|
122
|
+
});
|
|
71
123
|
await db.execute({
|
|
72
|
-
sql:
|
|
73
|
-
|
|
74
|
-
args: [id, section.file, section.heading, content, hash, vecJson, now],
|
|
124
|
+
sql: 'DELETE FROM chunks WHERE section_id=?',
|
|
125
|
+
args: [id],
|
|
75
126
|
});
|
|
127
|
+
await db.execute({ sql: 'DELETE FROM sections WHERE id=?', args: [id] });
|
|
76
128
|
}
|
|
129
|
+
const parents = new Map();
|
|
130
|
+
for (const s of project.sections)
|
|
131
|
+
for (const child of s.children)
|
|
132
|
+
parents.set(child.id, s.id);
|
|
133
|
+
for (const s of project.sections)
|
|
134
|
+
if (changed.has(s.id))
|
|
135
|
+
await db.execute({
|
|
136
|
+
sql: 'INSERT INTO sections VALUES (?,?,?,?,?,?,?,?)',
|
|
137
|
+
args: [
|
|
138
|
+
s.id,
|
|
139
|
+
s.file,
|
|
140
|
+
s.heading,
|
|
141
|
+
s.firstParagraph,
|
|
142
|
+
sectionHashes.get(s.id),
|
|
143
|
+
parents.get(s.id) ?? null,
|
|
144
|
+
s.startLine,
|
|
145
|
+
s.endLine,
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
for (const p of passages)
|
|
149
|
+
if (changed.has(p.sectionId)) {
|
|
150
|
+
const row = (await db.execute({
|
|
151
|
+
sql: 'INSERT INTO chunks(source_id,section_id,ordinal,type,spans,body,heading,path,input_hash) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id',
|
|
152
|
+
args: [
|
|
153
|
+
p.id,
|
|
154
|
+
p.sectionId,
|
|
155
|
+
p.ordinal,
|
|
156
|
+
p.type,
|
|
157
|
+
JSON.stringify(p.spans),
|
|
158
|
+
p.text,
|
|
159
|
+
p.heading,
|
|
160
|
+
p.path,
|
|
161
|
+
p.inputHash,
|
|
162
|
+
],
|
|
163
|
+
})).rows[0];
|
|
164
|
+
for (const token of identifierTokens(`${p.sectionId}\n${project.sectionById.get(p.sectionId.toLowerCase())?.filePath ?? ''}\n${p.heading}\n${p.path}\n${p.text}`))
|
|
165
|
+
await db.execute({
|
|
166
|
+
sql: 'INSERT INTO identifiers VALUES (?,?)',
|
|
167
|
+
args: [token, row.id],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
await db.execute('DELETE FROM embeddings WHERE hash NOT IN (SELECT input_hash FROM chunks)');
|
|
171
|
+
await db.execute({
|
|
172
|
+
sql: 'INSERT OR REPLACE INTO meta VALUES (?,?)',
|
|
173
|
+
args: ['fingerprint', fingerprint],
|
|
174
|
+
});
|
|
175
|
+
await db.execute({
|
|
176
|
+
sql: 'INSERT OR REPLACE INTO meta VALUES (?,?)',
|
|
177
|
+
args: ['project_hash', projectHash],
|
|
178
|
+
});
|
|
179
|
+
await synchronizeLexical(db);
|
|
180
|
+
if (rebuildFts)
|
|
181
|
+
await db.execute(CREATE_PASSAGE_FTS);
|
|
182
|
+
await db.execute('COMMIT');
|
|
77
183
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
184
|
+
catch (error) {
|
|
185
|
+
await db.execute('ROLLBACK');
|
|
186
|
+
throw error;
|
|
81
187
|
}
|
|
82
|
-
|
|
83
|
-
const updated = toEmbed.filter((e) => existing.has(e.id)).length;
|
|
84
|
-
return { added, updated, removed: toDelete.length, unchanged };
|
|
188
|
+
return stats;
|
|
85
189
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SearchDb } from './db.js';
|
|
2
|
+
export declare const LEXICAL_VERSION = "snowball-english-rust-stemmers-1.2.0-v1:unicode-words-v1:live-statistics-v1";
|
|
3
|
+
/** Same analysis for indexed fields and queries; exact identifiers bypass it. */
|
|
4
|
+
export declare function lexicalTokens(text: string): string[];
|
|
5
|
+
/** Populate derived lexical rows, reusing original passages and vectors. */
|
|
6
|
+
export declare function synchronizeLexical(db: SearchDb): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { stemWords, STEMMER_VERSION } from '@lat.md/stemmer';
|
|
2
|
+
import { CREATE_PASSAGE_FTS } from './db.js';
|
|
3
|
+
export const LEXICAL_VERSION = `${STEMMER_VERSION}:unicode-words-v1:live-statistics-v1`;
|
|
4
|
+
/** Same analysis for indexed fields and queries; exact identifiers bypass it. */
|
|
5
|
+
export function lexicalTokens(text) {
|
|
6
|
+
const tokens = text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
7
|
+
const english = tokens.filter((token) => /^[a-z]+$/.test(token));
|
|
8
|
+
const stems = stemWords(english);
|
|
9
|
+
let i = 0;
|
|
10
|
+
return tokens.map((token) => (/^[a-z]+$/.test(token) ? stems[i++] : token));
|
|
11
|
+
}
|
|
12
|
+
/** Populate derived lexical rows, reusing original passages and vectors. */
|
|
13
|
+
export async function synchronizeLexical(db) {
|
|
14
|
+
const version = (await db.execute("SELECT value FROM meta WHERE key='lexical_version'")).rows[0]?.value;
|
|
15
|
+
if (version !== LEXICAL_VERSION) {
|
|
16
|
+
await db.execute('DROP INDEX IF EXISTS chunks_fts');
|
|
17
|
+
await db.execute('DELETE FROM lexical_chunks');
|
|
18
|
+
}
|
|
19
|
+
await db.execute('DELETE FROM lexical_chunks WHERE id NOT IN (SELECT id FROM chunks)');
|
|
20
|
+
const rows = (await db.execute('SELECT id,body,heading,path FROM chunks WHERE id NOT IN (SELECT id FROM lexical_chunks)')).rows;
|
|
21
|
+
for (const row of rows)
|
|
22
|
+
await db.execute({
|
|
23
|
+
sql: 'INSERT INTO lexical_chunks VALUES (?,?,?,?)',
|
|
24
|
+
args: [
|
|
25
|
+
row.id,
|
|
26
|
+
...[row.body, row.heading, row.path].map((text) => lexicalTokens(text).join(' ')),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
if (version !== LEXICAL_VERSION) {
|
|
30
|
+
await db.execute(CREATE_PASSAGE_FTS);
|
|
31
|
+
await db.execute({
|
|
32
|
+
sql: 'INSERT OR REPLACE INTO meta VALUES (?,?)',
|
|
33
|
+
args: ['lexical_version', LEXICAL_VERSION],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
4
|
+
/** The persistent file must never be unlinked: all writers lock the same inode. */
|
|
5
|
+
export async function acquireSearchLock(cacheDir, timeoutMs = 120_000) {
|
|
6
|
+
const { tryLock } = await import('fs-native-extensions');
|
|
7
|
+
const handle = await open(join(cacheDir, 'search-write.lock'), 'a+');
|
|
8
|
+
const deadline = performance.now() + timeoutMs;
|
|
9
|
+
try {
|
|
10
|
+
while (!tryLock(handle.fd)) {
|
|
11
|
+
const remaining = deadline - performance.now();
|
|
12
|
+
if (remaining <= 0)
|
|
13
|
+
throw new Error('Search index writer is busy; retry shortly.');
|
|
14
|
+
await setTimeout(Math.min(100, remaining));
|
|
15
|
+
}
|
|
16
|
+
// Closing releases the kernel lock, including on abnormal process exit.
|
|
17
|
+
return () => handle.close();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
await handle.close();
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import type { Section, SectionMatch } from '../lattice-model.js';
|
|
2
2
|
import { type CreateSearchEngine } from './embedder.js';
|
|
3
|
+
import { type SearchResult } from './search.js';
|
|
3
4
|
export type IndexedSearchResult = {
|
|
4
5
|
query: string;
|
|
5
6
|
matches: SectionMatch[];
|
|
6
7
|
};
|
|
7
8
|
export type IndexedSearchSession = {
|
|
8
|
-
search: (query: string, limit: number,
|
|
9
|
+
search: (query: string, limit: number, minSimilarity?: number) => Promise<SearchResult[]>;
|
|
9
10
|
close: () => Promise<void>;
|
|
10
11
|
};
|
|
12
|
+
/** Resolve scored index rows against one already-analyzed project snapshot. */
|
|
13
|
+
export declare function resolveSearchMatches(results: readonly SearchResult[], sectionById: ReadonlyMap<string, Section>): SectionMatch[];
|
|
11
14
|
/** Open one reusable database and embedder for a sequence of index queries. */
|
|
12
|
-
export declare function openIndexedSearchSession(latDir: string,
|
|
15
|
+
export declare function openIndexedSearchSession(latDir: string, options?: {
|
|
13
16
|
cacheDir?: string;
|
|
14
17
|
createSearchEngine?: CreateSearchEngine;
|
|
15
18
|
}): Promise<IndexedSearchSession>;
|
|
16
19
|
/** Query a finished index and resolve its ids from precomputed section data. */
|
|
17
20
|
export declare function searchIndexedSections(latDir: string, query: string, limit: number, sectionById: ReadonlyMap<string, Section>, options?: {
|
|
18
21
|
cacheDir?: string;
|
|
19
|
-
|
|
22
|
+
minSimilarity?: number;
|
|
20
23
|
}): Promise<IndexedSearchResult>;
|
package/dist/src/search/query.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LEXICAL_VERSION } from './lexical.js';
|
|
2
|
+
import { ReindexRequiredError } from './embedder.js';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { hasIndex } from './db.js';
|
|
6
|
+
import { closeDb, getStoredModel, openDb } from './db.js';
|
|
2
7
|
import { embedderForIndex } from './embedder.js';
|
|
3
8
|
import { searchSections } from './search.js';
|
|
9
|
+
/** Resolve scored index rows against one already-analyzed project snapshot. */
|
|
10
|
+
export function resolveSearchMatches(results, sectionById) {
|
|
11
|
+
return results.flatMap((result) => {
|
|
12
|
+
const section = sectionById.get(result.id.toLowerCase());
|
|
13
|
+
return section
|
|
14
|
+
? [
|
|
15
|
+
{
|
|
16
|
+
section,
|
|
17
|
+
reason: 'hybrid match',
|
|
18
|
+
rankScore: result.rankScore,
|
|
19
|
+
semanticSimilarity: result.semanticSimilarity,
|
|
20
|
+
lexicalScore: result.lexicalScore,
|
|
21
|
+
semanticRank: result.semanticRank,
|
|
22
|
+
lexicalRank: result.lexicalRank,
|
|
23
|
+
evidence: result.evidence,
|
|
24
|
+
diagnostics: result.diagnostics,
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
: [];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
4
30
|
/** Open one reusable database and embedder for a sequence of index queries. */
|
|
5
|
-
export async function openIndexedSearchSession(latDir,
|
|
6
|
-
const
|
|
31
|
+
export async function openIndexedSearchSession(latDir, options = {}) {
|
|
32
|
+
const cacheDir = options.cacheDir ?? join(latDir, '.cache');
|
|
33
|
+
if (!existsSync(cacheDir) || !hasIndex(cacheDir))
|
|
34
|
+
return { search: async () => [], close: async () => { } };
|
|
35
|
+
const db = openDb(latDir, options.cacheDir, true);
|
|
7
36
|
let closed = false;
|
|
8
37
|
const close = async () => {
|
|
9
38
|
if (closed)
|
|
@@ -12,40 +41,26 @@ export async function openIndexedSearchSession(latDir, sectionById, options = {}
|
|
|
12
41
|
await closeDb(db);
|
|
13
42
|
};
|
|
14
43
|
try {
|
|
15
|
-
await ensureMeta(db);
|
|
16
44
|
const stored = await getStoredModel(db);
|
|
17
45
|
if (stored === null) {
|
|
18
46
|
return {
|
|
19
|
-
search: async (
|
|
47
|
+
search: async () => {
|
|
20
48
|
if (closed)
|
|
21
49
|
throw new Error('Search session is closed');
|
|
22
|
-
return
|
|
50
|
+
return [];
|
|
23
51
|
},
|
|
24
52
|
close,
|
|
25
53
|
};
|
|
26
54
|
}
|
|
55
|
+
const lexicalVersion = (await db.execute("SELECT value FROM meta WHERE key='lexical_version'")).rows[0]?.value;
|
|
56
|
+
if (lexicalVersion !== LEXICAL_VERSION)
|
|
57
|
+
throw new ReindexRequiredError('Search lexical index changed; run lat search to update it.');
|
|
27
58
|
const embedder = await embedderForIndex(stored, latDir, options.createSearchEngine);
|
|
28
|
-
await ensureSectionsSchema(db, embedder.dimensions);
|
|
29
59
|
return {
|
|
30
|
-
async search(query, limit,
|
|
60
|
+
async search(query, limit, minSimilarity) {
|
|
31
61
|
if (closed)
|
|
32
62
|
throw new Error('Search session is closed');
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
query,
|
|
36
|
-
matches: results.flatMap((result) => {
|
|
37
|
-
const section = sectionById.get(result.id.toLowerCase());
|
|
38
|
-
return section
|
|
39
|
-
? [
|
|
40
|
-
{
|
|
41
|
-
section,
|
|
42
|
-
reason: 'semantic match',
|
|
43
|
-
score: result.score,
|
|
44
|
-
},
|
|
45
|
-
]
|
|
46
|
-
: [];
|
|
47
|
-
}),
|
|
48
|
-
};
|
|
63
|
+
return searchSections(db, query, embedder, limit, minSimilarity);
|
|
49
64
|
},
|
|
50
65
|
close,
|
|
51
66
|
};
|
|
@@ -57,9 +72,13 @@ export async function openIndexedSearchSession(latDir, sectionById, options = {}
|
|
|
57
72
|
}
|
|
58
73
|
/** Query a finished index and resolve its ids from precomputed section data. */
|
|
59
74
|
export async function searchIndexedSections(latDir, query, limit, sectionById, options = {}) {
|
|
60
|
-
const session = await openIndexedSearchSession(latDir,
|
|
75
|
+
const session = await openIndexedSearchSession(latDir, options);
|
|
61
76
|
try {
|
|
62
|
-
|
|
77
|
+
const results = await session.search(query, limit, options.minSimilarity);
|
|
78
|
+
return {
|
|
79
|
+
query,
|
|
80
|
+
matches: resolveSearchMatches(results, sectionById),
|
|
81
|
+
};
|
|
63
82
|
}
|
|
64
83
|
finally {
|
|
65
84
|
await session.close();
|