sensemaking 0.23.1 → 0.24.1
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 +15 -10
- package/dist/cjs/cli/index.d.cts +1 -1
- package/dist/cjs/cli/index.d.ts +1 -1
- package/dist/cjs/cli/index.js +1 -1
- package/dist/cjs/cli/index.js.map +1 -1
- package/dist/cjs/cli/named.js +6 -2
- package/dist/cjs/cli/named.js.map +1 -1
- package/dist/cjs/cli/search.js +6 -2
- package/dist/cjs/cli/search.js.map +1 -1
- package/dist/cjs/cli/shared.d.cts +2 -0
- package/dist/cjs/cli/shared.d.ts +2 -0
- package/dist/cjs/cli/shared.js +24 -0
- package/dist/cjs/cli/shared.js.map +1 -1
- package/dist/cjs/cli/status.js +6 -3
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/cli/watch.js +1 -1
- package/dist/cjs/cli/watch.js.map +1 -1
- package/dist/cjs/commands/map.js +2 -2
- package/dist/cjs/commands/map.js.map +1 -1
- package/dist/cjs/commands/related.js +2 -2
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/commands/search.d.cts +10 -0
- package/dist/cjs/commands/search.d.ts +10 -0
- package/dist/cjs/commands/search.js +489 -180
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/config/access.js +7 -1
- package/dist/cjs/config/access.js.map +1 -1
- package/dist/cjs/config/load.js +6 -1
- package/dist/cjs/config/load.js.map +1 -1
- package/dist/cjs/config/types.d.cts +3 -0
- package/dist/cjs/config/types.d.ts +3 -0
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +4 -0
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/embed/query.js +4 -4
- package/dist/cjs/embed/query.js.map +1 -1
- package/dist/cjs/output/output.js +45 -8
- package/dist/cjs/output/output.js.map +1 -1
- package/dist/cjs/output/search-error.d.cts +1 -0
- package/dist/cjs/output/search-error.d.ts +1 -0
- package/dist/cjs/output/search-error.js +28 -4
- package/dist/cjs/output/search-error.js.map +1 -1
- package/dist/cjs/scan/frontmatter.js +1 -1
- package/dist/cjs/scan/frontmatter.js.map +1 -1
- package/dist/cjs/scan/index.js +1 -0
- package/dist/cjs/scan/index.js.map +1 -1
- package/dist/cjs/store/cache.js +2 -1
- package/dist/cjs/store/cache.js.map +1 -1
- package/dist/cjs/store/duckdb/fieldStats.js +4 -7
- package/dist/cjs/store/duckdb/fieldStats.js.map +1 -1
- package/dist/cjs/store/duckdb/lexical.d.cts +2 -2
- package/dist/cjs/store/duckdb/lexical.d.ts +2 -2
- package/dist/cjs/store/duckdb/lexical.js +215 -64
- package/dist/cjs/store/duckdb/lexical.js.map +1 -1
- package/dist/cjs/store/duckdb/reconcile.js +7 -2
- package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
- package/dist/cjs/store/duckdb/store.js +1 -3
- package/dist/cjs/store/duckdb/store.js.map +1 -1
- package/dist/cjs/store/duckdb/vectors.js +13 -8
- package/dist/cjs/store/duckdb/vectors.js.map +1 -1
- package/dist/cjs/store/native.js +5 -4
- package/dist/cjs/store/native.js.map +1 -1
- package/dist/cjs/store/open.js +20 -6
- package/dist/cjs/store/open.js.map +1 -1
- package/dist/cjs/store/shared.js.map +1 -1
- package/dist/cjs/store/sqlite/lexical.js +1 -4
- package/dist/cjs/store/sqlite/lexical.js.map +1 -1
- package/dist/cjs/store/sqlite/store.js +1 -4
- package/dist/cjs/store/sqlite/store.js.map +1 -1
- package/dist/cjs/store/sqlite/vectors.js +78 -24
- package/dist/cjs/store/sqlite/vectors.js.map +1 -1
- package/dist/cjs/store/turso/connection.js +2 -1
- package/dist/cjs/store/turso/connection.js.map +1 -1
- package/dist/cjs/store/turso/lexical-text.d.cts +1 -0
- package/dist/cjs/store/turso/lexical-text.d.ts +1 -0
- package/dist/cjs/store/turso/lexical-text.js +40 -0
- package/dist/cjs/store/turso/lexical-text.js.map +1 -0
- package/dist/cjs/store/turso/lexical.js +89 -16
- package/dist/cjs/store/turso/lexical.js.map +1 -1
- package/dist/cjs/store/turso/open.d.cts +1 -1
- package/dist/cjs/store/turso/open.d.ts +1 -1
- package/dist/cjs/store/turso/open.js +2 -2
- package/dist/cjs/store/turso/open.js.map +1 -1
- package/dist/cjs/store/turso/reconcile.d.cts +1 -1
- package/dist/cjs/store/turso/reconcile.d.ts +1 -1
- package/dist/cjs/store/turso/reconcile.js +6 -2
- package/dist/cjs/store/turso/reconcile.js.map +1 -1
- package/dist/cjs/store/turso/sql-functions.d.cts +1 -0
- package/dist/cjs/store/turso/sql-functions.d.ts +1 -0
- package/dist/cjs/store/turso/sql-functions.js +174 -0
- package/dist/cjs/store/turso/sql-functions.js.map +1 -0
- package/dist/cjs/store/turso/store.js +2 -3
- package/dist/cjs/store/turso/store.js.map +1 -1
- package/dist/cjs/store/turso/vectors.js +50 -14
- package/dist/cjs/store/turso/vectors.js.map +1 -1
- package/dist/cjs/store/types.d.cts +1 -2
- package/dist/cjs/store/types.d.ts +1 -2
- package/dist/cjs/store/types.js +1 -4
- package/dist/cjs/store/types.js.map +1 -1
- package/dist/cjs/store/vectors.d.cts +7 -0
- package/dist/cjs/store/vectors.d.ts +7 -0
- package/dist/cjs/store/vectors.js +8 -0
- package/dist/cjs/store/vectors.js.map +1 -1
- package/dist/cjs/text/segment.d.cts +13 -0
- package/dist/cjs/text/segment.d.ts +13 -0
- package/dist/cjs/text/segment.js +109 -4
- package/dist/cjs/text/segment.js.map +1 -1
- package/dist/cjs/watch.d.cts +1 -1
- package/dist/cjs/watch.d.ts +1 -1
- package/dist/cjs/watch.js +5 -4
- package/dist/cjs/watch.js.map +1 -1
- package/dist/esm/cli/index.d.ts +1 -1
- package/dist/esm/cli/index.js +1 -1
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/named.js +6 -2
- package/dist/esm/cli/named.js.map +1 -1
- package/dist/esm/cli/search.js +5 -1
- package/dist/esm/cli/search.js.map +1 -1
- package/dist/esm/cli/shared.d.ts +2 -0
- package/dist/esm/cli/shared.js +19 -0
- package/dist/esm/cli/shared.js.map +1 -1
- package/dist/esm/cli/status.js +6 -3
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/cli/watch.js +1 -1
- package/dist/esm/cli/watch.js.map +1 -1
- package/dist/esm/commands/map.js +2 -2
- package/dist/esm/commands/map.js.map +1 -1
- package/dist/esm/commands/related.js +2 -1
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/commands/search.d.ts +10 -0
- package/dist/esm/commands/search.js +272 -91
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/config/access.js +7 -0
- package/dist/esm/config/access.js.map +1 -1
- package/dist/esm/config/load.js +6 -1
- package/dist/esm/config/load.js.map +1 -1
- package/dist/esm/config/types.d.ts +3 -0
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +4 -0
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/embed/query.js +4 -4
- package/dist/esm/embed/query.js.map +1 -1
- package/dist/esm/output/output.js +27 -4
- package/dist/esm/output/output.js.map +1 -1
- package/dist/esm/output/search-error.d.ts +1 -0
- package/dist/esm/output/search-error.js +17 -1
- package/dist/esm/output/search-error.js.map +1 -1
- package/dist/esm/scan/frontmatter.js +1 -1
- package/dist/esm/scan/frontmatter.js.map +1 -1
- package/dist/esm/scan/index.js +1 -0
- package/dist/esm/scan/index.js.map +1 -1
- package/dist/esm/store/cache.js +2 -1
- package/dist/esm/store/cache.js.map +1 -1
- package/dist/esm/store/duckdb/fieldStats.js +4 -7
- package/dist/esm/store/duckdb/fieldStats.js.map +1 -1
- package/dist/esm/store/duckdb/lexical.d.ts +2 -2
- package/dist/esm/store/duckdb/lexical.js +94 -40
- package/dist/esm/store/duckdb/lexical.js.map +1 -1
- package/dist/esm/store/duckdb/reconcile.js +2 -2
- package/dist/esm/store/duckdb/reconcile.js.map +1 -1
- package/dist/esm/store/duckdb/store.js +2 -4
- package/dist/esm/store/duckdb/store.js.map +1 -1
- package/dist/esm/store/duckdb/vectors.js +26 -13
- package/dist/esm/store/duckdb/vectors.js.map +1 -1
- package/dist/esm/store/native.js +5 -4
- package/dist/esm/store/native.js.map +1 -1
- package/dist/esm/store/open.js +21 -4
- package/dist/esm/store/open.js.map +1 -1
- package/dist/esm/store/shared.js +1 -2
- package/dist/esm/store/shared.js.map +1 -1
- package/dist/esm/store/sqlite/lexical.js +2 -5
- package/dist/esm/store/sqlite/lexical.js.map +1 -1
- package/dist/esm/store/sqlite/store.js +1 -4
- package/dist/esm/store/sqlite/store.js.map +1 -1
- package/dist/esm/store/sqlite/vectors.js +48 -19
- package/dist/esm/store/sqlite/vectors.js.map +1 -1
- package/dist/esm/store/turso/connection.js +3 -2
- package/dist/esm/store/turso/connection.js.map +1 -1
- package/dist/esm/store/turso/lexical-text.d.ts +1 -0
- package/dist/esm/store/turso/lexical-text.js +12 -0
- package/dist/esm/store/turso/lexical-text.js.map +1 -0
- package/dist/esm/store/turso/lexical.js +56 -21
- package/dist/esm/store/turso/lexical.js.map +1 -1
- package/dist/esm/store/turso/open.d.ts +1 -1
- package/dist/esm/store/turso/open.js +2 -2
- package/dist/esm/store/turso/open.js.map +1 -1
- package/dist/esm/store/turso/reconcile.d.ts +1 -1
- package/dist/esm/store/turso/reconcile.js +6 -2
- package/dist/esm/store/turso/reconcile.js.map +1 -1
- package/dist/esm/store/turso/sql-functions.d.ts +1 -0
- package/dist/esm/store/turso/sql-functions.js +179 -0
- package/dist/esm/store/turso/sql-functions.js.map +1 -0
- package/dist/esm/store/turso/store.js +3 -5
- package/dist/esm/store/turso/store.js.map +1 -1
- package/dist/esm/store/turso/vectors.js +19 -10
- package/dist/esm/store/turso/vectors.js.map +1 -1
- package/dist/esm/store/types.d.ts +1 -2
- package/dist/esm/store/types.js +6 -8
- package/dist/esm/store/types.js.map +1 -1
- package/dist/esm/store/vectors.d.ts +7 -0
- package/dist/esm/store/vectors.js +8 -2
- package/dist/esm/store/vectors.js.map +1 -1
- package/dist/esm/text/segment.d.ts +13 -0
- package/dist/esm/text/segment.js +70 -0
- package/dist/esm/text/segment.js.map +1 -1
- package/dist/esm/watch.d.ts +1 -1
- package/dist/esm/watch.js +5 -4
- package/dist/esm/watch.js.map +1 -1
- package/package.json +4 -3
- package/schema.json +7 -2
- package/skills/sense/EXAMPLES.md +3 -3
- package/skills/sense/SKILL.md +6 -6
- package/skills/sense-setup/EXAMPLES.md +21 -0
- package/skills/sense-setup/SKILL.md +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/shared.ts"],"sourcesContent":["import type { Connection } from './types.ts';\n\n// SQL and meta-table primitives both stores' open()/reconcile() need. Engine-neutral: both\n// stores' Connection satisfies the same async exec/prepare/runBatch shape (types.ts).\n\n// Rows that cannot conflict, at whatever speed this connection offers: the append path where the\n// store has one, else
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/shared.ts"],"sourcesContent":["import type { Connection } from './types.ts';\n\n// SQL and meta-table primitives both stores' open()/reconcile() need. Engine-neutral: both\n// stores' Connection satisfies the same async exec/prepare/runBatch shape (types.ts).\n\n// Rows that cannot conflict, at whatever speed this connection offers: the append path where the\n// store has one, else the caller's own guarded `conflictSql` statement bound the ordinary way.\nexport async function appendRows(conn: Connection, table: string, columns: string[], conflictSql: string, rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n if (conn.appendRows) await conn.appendRows(table, columns, rows);\n else await conn.runBatch(conflictSql, rows);\n}\n\nexport function quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\nexport async function getColumns(conn: Connection): Promise<Set<string>> {\n const stmt = await conn.prepare('PRAGMA table_info(frontmatter)');\n const rows = (await stmt.all()) as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nexport async function getMeta(conn: Connection, key: string): Promise<string | null> {\n const stmt = await conn.prepare('SELECT value FROM meta WHERE key = ?');\n const row = (await stmt.get(key)) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport async function setMeta(conn: Connection, key: string, value: string | null): Promise<void> {\n if (value === null) {\n const stmt = await conn.prepare('DELETE FROM meta WHERE key = ?');\n await stmt.run(key);\n return;\n }\n const stmt = await conn.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');\n await stmt.run(key, value);\n}\n\n// Reconcile's own write-transaction duration, for open()'s derived busy_timeout: keep the\n// observed max so a big watcher reconcile's lock hold is what the next open bounds its wait against.\nexport async function recordReconcileDuration(conn: Connection, ms: number): Promise<void> {\n const prevRaw = await getMeta(conn, 'reconcile_max_ms');\n // -1, not 0, so a genuinely 0ms first reconcile (sub-millisecond, common on a tiny tree)\n // still gets recorded instead of losing to the \"nothing recorded yet\" default.\n const prevMax = prevRaw === null ? -1 : Number(prevRaw);\n if (ms > prevMax) await setMeta(conn, 'reconcile_max_ms', String(ms));\n}\n"],"names":["appendRows","conn","table","columns","conflictSql","rows","length","runBatch","quoteIdent","name","split","join","getColumns","stmt","prepare","all","Set","map","r","getMeta","key","row","get","value","setMeta","run","recordReconcileDuration","ms","prevRaw","prevMax","Number","String"],"mappings":"AAEA,2FAA2F;AAC3F,sFAAsF;AAEtF,iGAAiG;AACjG,+FAA+F;AAC/F,OAAO,eAAeA,WAAWC,IAAgB,EAAEC,KAAa,EAAEC,OAAiB,EAAEC,WAAmB,EAAEC,IAAiB;IACzH,IAAIA,KAAKC,MAAM,KAAK,GAAG;IACvB,IAAIL,KAAKD,UAAU,EAAE,MAAMC,KAAKD,UAAU,CAACE,OAAOC,SAASE;SACtD,MAAMJ,KAAKM,QAAQ,CAACH,aAAaC;AACxC;AAEA,OAAO,SAASG,WAAWC,IAAY;IACrC,OAAO,CAAC,CAAC,EAAEA,KAAKC,KAAK,CAAC,KAAKC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1C;AAEA,OAAO,eAAeC,WAAWX,IAAgB;IAC/C,MAAMY,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMT,OAAQ,MAAMQ,KAAKE,GAAG;IAC5B,OAAO,IAAIC,IAAIX,KAAKY,GAAG,CAAC,CAACC,IAAMA,EAAET,IAAI;AACvC;AAEA,OAAO,eAAeU,QAAQlB,IAAgB,EAAEmB,GAAW;IACzD,MAAMP,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMO,MAAO,MAAMR,KAAKS,GAAG,CAACF;IAC5B,OAAOC,MAAMA,IAAIE,KAAK,GAAG;AAC3B;AAEA,OAAO,eAAeC,QAAQvB,IAAgB,EAAEmB,GAAW,EAAEG,KAAoB;IAC/E,IAAIA,UAAU,MAAM;QAClB,MAAMV,OAAO,MAAMZ,KAAKa,OAAO,CAAC;QAChC,MAAMD,KAAKY,GAAG,CAACL;QACf;IACF;IACA,MAAMP,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMD,KAAKY,GAAG,CAACL,KAAKG;AACtB;AAEA,0FAA0F;AAC1F,qGAAqG;AACrG,OAAO,eAAeG,wBAAwBzB,IAAgB,EAAE0B,EAAU;IACxE,MAAMC,UAAU,MAAMT,QAAQlB,MAAM;IACpC,yFAAyF;IACzF,+EAA+E;IAC/E,MAAM4B,UAAUD,YAAY,OAAO,CAAC,IAAIE,OAAOF;IAC/C,IAAID,KAAKE,SAAS,MAAML,QAAQvB,MAAM,oBAAoB8B,OAAOJ;AACnE"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { segmentMatch } from '../../text/segment.js';
|
|
2
|
-
// snippet() re-tokenizes each candidate doc, superlinearly: ~10s for one 1MB doc
|
|
3
|
-
// (benchmark/reports/2026-08-23-0.13.2-hub-release-battery.md); past this bound, hit stays NULL for the caller's JS excerpt fallback.
|
|
4
|
-
const SNIPPET_BOUND = 16384;
|
|
5
2
|
const WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)';
|
|
6
|
-
// Ranked BM25 word-match query
|
|
3
|
+
// Ranked BM25 word-match query, scoped by the caller-built SQL fragments. An
|
|
7
4
|
// unspaced-script run in `terms` becomes a quoted grapheme phrase against the `_seg` sidecars.
|
|
8
5
|
export async function queryLexical(conn, terms, opts) {
|
|
9
6
|
const { whereJoin, whereCond, scopeCond, limit } = opts;
|
|
@@ -11,7 +8,7 @@ export async function queryLexical(conn, terms, opts) {
|
|
|
11
8
|
// A run of pure unspaced-script punctuation segments to nothing searchable: an empty MATCH
|
|
12
9
|
// string is an FTS5 syntax error, not zero rows, so return zero rows directly rather than issuing it.
|
|
13
10
|
if (query.trim() === '') return [];
|
|
14
|
-
const sql = `SELECT content.path AS path
|
|
11
|
+
const sql = `SELECT content.path AS path FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ${scopeCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${limit}`;
|
|
15
12
|
const stmt = await conn.prepare(sql);
|
|
16
13
|
return await stmt.all(query);
|
|
17
14
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/lexical.ts"],"sourcesContent":["import { segmentMatch } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/lexical.ts"],"sourcesContent":["import { segmentMatch } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\nconst WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)';\n\n// Ranked BM25 word-match query, scoped by the caller-built SQL fragments. An\n// unspaced-script run in `terms` becomes a quoted grapheme phrase against the `_seg` sidecars.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]> {\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const query = segmentMatch(terms);\n // A run of pure unspaced-script punctuation segments to nothing searchable: an empty MATCH\n // string is an FTS5 syntax error, not zero rows, so return zero rows directly rather than issuing it.\n if (query.trim() === '') return [];\n const sql = `SELECT content.path AS path FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ${scopeCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${limit}`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(query)) as unknown as LexicalHit[];\n}\n"],"names":["segmentMatch","WEIGHTED_BM25","queryLexical","conn","terms","opts","whereJoin","whereCond","scopeCond","limit","query","trim","sql","stmt","prepare","all"],"mappings":"AAAA,SAASA,YAAY,QAAQ,wBAAwB;AAGrD,MAAMC,gBAAgB;AAEtB,6EAA6E;AAC7E,+FAA+F;AAC/F,OAAO,eAAeC,aAAaC,IAAgB,EAAEC,KAAa,EAAEC,IAAyB;IAC3F,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGJ;IACnD,MAAMK,QAAQV,aAAaI;IAC3B,2FAA2F;IAC3F,sGAAsG;IACtG,IAAIM,MAAMC,IAAI,OAAO,IAAI,OAAO,EAAE;IAClC,MAAMC,MAAM,CAAC,yCAAyC,EAAEN,UAAU,uBAAuB,EAAEC,UAAU,CAAC,EAAEC,UAAU,UAAU,EAAEP,cAAc,OAAO,EAAEQ,OAAO;IAC5J,MAAMI,OAAO,MAAMV,KAAKW,OAAO,CAACF;IAChC,OAAQ,MAAMC,KAAKE,GAAG,CAACL;AACzB"}
|
|
@@ -5,11 +5,8 @@ import { fieldStats } from './fieldStats.js';
|
|
|
5
5
|
import { queryLexical } from './lexical.js';
|
|
6
6
|
import { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.js';
|
|
7
7
|
export const CAPABILITIES = new Set([
|
|
8
|
-
'phrases',
|
|
9
|
-
'snippets',
|
|
10
|
-
'lexical',
|
|
11
8
|
'vectors',
|
|
12
|
-
'
|
|
9
|
+
'segment'
|
|
13
10
|
]);
|
|
14
11
|
// Wraps the synchronous DatabaseSync connection in the async Store interface, sharing one
|
|
15
12
|
// Connection instance (conn) with the builder's own reconcile call so transaction depth is tracked against the same object everywhere.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/store.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { getColumns } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store, VectorWriteRow } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/store.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { getColumns } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store, VectorWriteRow } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['vectors', 'segment']);\n\n// Wraps the synchronous DatabaseSync connection in the async Store interface, sharing one\n// Connection instance (conn) with the builder's own reconcile call so transaction depth is tracked against the same object everywhere.\nexport function createStore(db: DatabaseSync, conn: Connection): Store {\n return {\n name: 'sqlite',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n async fieldStats(columns, scopeWhere) {\n return fieldStats(conn, columns, scopeWhere);\n },\n },\n lexical: {\n async query(terms, opts) {\n return queryLexical(conn, terms, opts);\n },\n },\n vectors: {\n async pending() {\n return pendingRows(conn);\n },\n async writeVectors(rows: VectorWriteRow[]) {\n await writeVectorBatch(conn, rows);\n },\n async candidates(qv, storeDims, fetch, allowed) {\n return scanCandidates(conn, qv, storeDims, fetch, allowed);\n },\n async similar(path, opts) {\n return scanSimilar(conn, path, opts);\n },\n async hasVector(path) {\n return hasVectorRow(conn, path);\n },\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min), not a value re-derived here.\n const row = db.prepare('PRAGMA busy_timeout').get() as { timeout: number };\n return { busy_timeout: `${row.timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = db.prepare(sql);\n stmt.setReadBigInts(true); // int64 past 2^53 arrives as BigInt instead of throwing at step time\n return {\n columns: () => stmt.columns(),\n // sense sql streams through an async iterator wrapping the sync iterate().\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...(params as Parameters<typeof stmt.iterate>));\n },\n };\n },\n },\n async close() {\n db.close();\n },\n };\n}\n"],"names":["getColumns","withTransaction","hasVectorRow","pendingRows","fieldStats","queryLexical","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","createStore","db","conn","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","timeout","raw","stmt","setReadBigInts","iterate","params","close"],"mappings":"AACA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,eAAe,QAAQ,oBAAoB;AAEpD,SAASC,YAAY,EAAEC,WAAW,QAAQ,gBAAgB;AAC1D,SAASC,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,YAAY,QAAQ,eAAe;AAC5C,SAASC,cAAc,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,eAAe;AAE7E,OAAO,MAAMC,eAAwC,IAAIC,IAAI;IAAC;IAAW;CAAU,EAAE;AAErF,0FAA0F;AAC1F,uIAAuI;AACvI,OAAO,SAASC,YAAYC,EAAgB,EAAEC,IAAgB;IAC5D,OAAO;QACLC,MAAM;QACNC,cAAcN;QACd,MAAMO,MAAKC,GAAW;YACpB,MAAMJ,KAAKG,IAAI,CAACC;QAClB;QACA,MAAMC,SAAQD,GAAW;YACvB,OAAOJ,KAAKK,OAAO,CAACD;QACtB;QACA,MAAME,UAASF,GAAW,EAAEG,SAAsB;YAChD,MAAMP,KAAKM,QAAQ,CAACF,KAAKG;QAC3B;QACA,MAAMC,aAAeC,EAAoB;YACvC,OAAOrB,gBAAgBY,MAAMS;QAC/B;QACAC,MAAM;YACJ,MAAMC;gBACJ,OAAO;uBAAK,MAAMxB,WAAWa;iBAAO;YACtC;YACA,MAAMT,YAAWoB,OAAO,EAAEC,UAAU;gBAClC,OAAOrB,WAAWS,MAAMW,SAASC;YACnC;QACF;QACAC,SAAS;YACP,MAAMC,OAAMC,KAAK,EAAEC,IAAI;gBACrB,OAAOxB,aAAaQ,MAAMe,OAAOC;YACnC;QACF;QACAC,SAAS;YACP,MAAMC;gBACJ,OAAO5B,YAAYU;YACrB;YACA,MAAMmB,cAAaC,IAAsB;gBACvC,MAAMzB,iBAAiBK,MAAMoB;YAC/B;YACA,MAAMC,YAAWC,EAAE,EAAEC,SAAS,EAAEC,KAAK,EAAEC,OAAO;gBAC5C,OAAOhC,eAAeO,MAAMsB,IAAIC,WAAWC,OAAOC;YACpD;YACA,MAAMC,SAAQC,IAAI,EAAEX,IAAI;gBACtB,OAAOtB,YAAYM,MAAM2B,MAAMX;YACjC;YACA,MAAMY,WAAUD,IAAI;gBAClB,OAAOtC,aAAaW,MAAM2B;YAC5B;QACF;QACA,MAAME;YACJ,qFAAqF;YACrF,qFAAqF;YACrF,MAAMC,MAAM/B,GAAGM,OAAO,CAAC,uBAAuB0B,GAAG;YACjD,OAAO;gBAAEC,cAAc,GAAGF,IAAIG,OAAO,CAAC,kFAAkF,CAAC;YAAC;QAC5H;QACAC,KAAK;YACH,MAAM7B,SAAQD,GAAW;gBACvB,MAAM+B,OAAOpC,GAAGM,OAAO,CAACD;gBACxB+B,KAAKC,cAAc,CAAC,OAAO,qEAAqE;gBAChG,OAAO;oBACLzB,SAAS,IAAMwB,KAAKxB,OAAO;oBAC3B,2EAA2E;oBAC3E0B,SAAS,gBAAiB,GAAGC,MAAiB;wBAC5C,OAAOH,KAAKE,OAAO,IAAKC;oBAC1B;gBACF;YACF;QACF;QACA,MAAMC;YACJxC,GAAGwC,KAAK;QACV;IACF;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asCosine, sampleEvenly } from '../vectors.js';
|
|
1
|
+
import { asCosine, compareVectorScores, sampleEvenly } from '../vectors.js';
|
|
2
2
|
// One runBatch call per provider batch: the caller (embed/query.ts) already batches by the
|
|
3
3
|
// provider's batchCap, so this is one crossing per batch, never per row.
|
|
4
4
|
export async function writeVectorBatch(conn, rows) {
|
|
@@ -13,57 +13,86 @@ export async function writeVectorBatch(conn, rows) {
|
|
|
13
13
|
// Best chunk per file by cosine, its line range riding along: the JS loop over stored int8
|
|
14
14
|
// BLOBs this store's scan strategy is (a native store scans differently, same contract).
|
|
15
15
|
export async function scanCandidates(conn, qv, storeDims, fetch, allowed) {
|
|
16
|
-
const stmt = await conn.prepare('SELECT "path", start_line, end_line,
|
|
16
|
+
const stmt = await conn.prepare('SELECT "path", chunk, start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL ORDER BY "path", chunk');
|
|
17
17
|
const rows = await stmt.all();
|
|
18
|
+
let queryNorm = 0;
|
|
19
|
+
for(let d = 0; d < Math.min(storeDims, qv.length); d++)queryNorm += qv[d] * qv[d];
|
|
20
|
+
queryNorm = Math.sqrt(queryNorm);
|
|
18
21
|
const best = new Map();
|
|
19
22
|
for (const row of rows){
|
|
20
23
|
if (allowed && !allowed.has(row.path)) continue;
|
|
21
24
|
const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, Math.min(storeDims, row.vector.byteLength));
|
|
25
|
+
// Rounding to int8 costs the stored row its unit norm, so dividing by that norm is what makes
|
|
26
|
+
// this a cosine rather than a scaled dot product. `scale` cancels out of the ratio.
|
|
22
27
|
let dot = 0;
|
|
23
|
-
|
|
24
|
-
const
|
|
28
|
+
let norm = 0;
|
|
29
|
+
const length = Math.min(q.length, qv.length);
|
|
30
|
+
for(let d = 0; d < length; d++){
|
|
31
|
+
dot += q[d] * qv[d];
|
|
32
|
+
norm += q[d] * q[d];
|
|
33
|
+
}
|
|
34
|
+
for(let d = length; d < q.length; d++)norm += q[d] * q[d];
|
|
35
|
+
const score = norm === 0 || queryNorm === 0 ? 0 : dot / (Math.sqrt(norm) * queryNorm);
|
|
25
36
|
const existing = best.get(row.path);
|
|
26
|
-
if (!existing || score > existing.score
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
if (!existing || score > existing.score || score === existing.score && row.chunk < existing.chunk) {
|
|
38
|
+
best.set(row.path, {
|
|
39
|
+
score,
|
|
40
|
+
chunk: row.chunk,
|
|
41
|
+
lines: `L${row.start_line}-${row.end_line}`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
return [
|
|
32
46
|
...best.entries()
|
|
33
|
-
].
|
|
47
|
+
].map(([path, value])=>({
|
|
48
|
+
path,
|
|
49
|
+
...value
|
|
50
|
+
})).sort(compareVectorScores).slice(0, fetch).map(({ path, lines, score })=>({
|
|
34
51
|
path,
|
|
35
|
-
lines
|
|
36
|
-
similarity: asCosine(
|
|
52
|
+
lines,
|
|
53
|
+
similarity: asCosine(score)
|
|
37
54
|
}));
|
|
38
55
|
}
|
|
39
56
|
// Note-to-note similarity is the max cosine over (target chunk, other chunk) pairs, one linear
|
|
40
57
|
// scan of stored vectors.
|
|
41
58
|
export async function scanSimilar(conn, path, opts) {
|
|
42
|
-
const targetStmt = await conn.prepare('SELECT
|
|
59
|
+
const targetStmt = await conn.prepare('SELECT vector FROM embeddings WHERE "path" = ? AND vector IS NOT NULL ORDER BY chunk');
|
|
43
60
|
const targetRows = await targetStmt.all(path);
|
|
44
61
|
if (targetRows.length === 0) return [];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
// Each side's norm divides its own dot product, so both are taken once here rather than per pair.
|
|
63
|
+
const target = sampleEvenly(targetRows).map((row)=>{
|
|
64
|
+
const v = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);
|
|
65
|
+
let norm = 0;
|
|
66
|
+
for(let d = 0; d < v.length; d++)norm += v[d] * v[d];
|
|
67
|
+
return {
|
|
68
|
+
v,
|
|
69
|
+
norm: Math.sqrt(norm)
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const stmt = await conn.prepare('SELECT "path", vector FROM embeddings WHERE vector IS NOT NULL');
|
|
50
73
|
const rows = await stmt.all();
|
|
51
74
|
const best = new Map();
|
|
52
75
|
for (const row of rows){
|
|
53
76
|
if (row.path === path || opts.exclude.has(row.path) || opts.allowed && !opts.allowed.has(row.path)) continue;
|
|
54
77
|
const other = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);
|
|
78
|
+
let otherNorm = 0;
|
|
79
|
+
for(let d = 0; d < other.length; d++)otherNorm += other[d] * other[d];
|
|
80
|
+
otherNorm = Math.sqrt(otherNorm);
|
|
55
81
|
for (const t of target){
|
|
56
82
|
let dot = 0;
|
|
57
83
|
const len = Math.min(t.v.length, other.length);
|
|
58
84
|
for(let d = 0; d < len; d++)dot += t.v[d] * other[d];
|
|
59
|
-
const score = dot
|
|
85
|
+
const score = t.norm === 0 || otherNorm === 0 ? 0 : dot / (t.norm * otherNorm);
|
|
60
86
|
const existing = best.get(row.path);
|
|
61
87
|
if (existing === undefined || score > existing) best.set(row.path, score);
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
return [
|
|
65
91
|
...best.entries()
|
|
66
|
-
].
|
|
92
|
+
].map(([p, score])=>({
|
|
93
|
+
path: p,
|
|
94
|
+
score
|
|
95
|
+
})).sort(compareVectorScores).slice(0, opts.k).map(({ path: p, score })=>({
|
|
67
96
|
path: p,
|
|
68
97
|
similarity: asCosine(score)
|
|
69
98
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/vectors.ts"],"sourcesContent":["import type { Connection, VectorCandidate, VectorSimilar, VectorWriteRow } from '../types.ts';\nimport { asCosine, sampleEvenly } from '../vectors.ts';\n\n// One runBatch call per provider batch: the caller (embed/query.ts) already batches by the\n// provider's batchCap, so this is one crossing per batch, never per row.\nexport async function writeVectorBatch(conn: Connection, rows: VectorWriteRow[]): Promise<void> {\n if (rows.length === 0) return;\n await conn.runBatch(\n 'UPDATE embeddings SET scale = ?, vector = ? WHERE \"path\" = ? AND chunk = ?',\n rows.map((row) => [row.scale, row.vector, row.path, row.chunk])\n );\n}\n\n// Best chunk per file by cosine, its line range riding along: the JS loop over stored int8\n// BLOBs this store's scan strategy is (a native store scans differently, same contract).\nexport async function scanCandidates(conn: Connection, qv: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const stmt = await conn.prepare('SELECT \"path\", start_line, end_line,
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/vectors.ts"],"sourcesContent":["import type { Connection, VectorCandidate, VectorSimilar, VectorWriteRow } from '../types.ts';\nimport { asCosine, compareVectorScores, sampleEvenly } from '../vectors.ts';\n\n// One runBatch call per provider batch: the caller (embed/query.ts) already batches by the\n// provider's batchCap, so this is one crossing per batch, never per row.\nexport async function writeVectorBatch(conn: Connection, rows: VectorWriteRow[]): Promise<void> {\n if (rows.length === 0) return;\n await conn.runBatch(\n 'UPDATE embeddings SET scale = ?, vector = ? WHERE \"path\" = ? AND chunk = ?',\n rows.map((row) => [row.scale, row.vector, row.path, row.chunk])\n );\n}\n\n// Best chunk per file by cosine, its line range riding along: the JS loop over stored int8\n// BLOBs this store's scan strategy is (a native store scans differently, same contract).\nexport async function scanCandidates(conn: Connection, qv: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const stmt = await conn.prepare('SELECT \"path\", chunk, start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL ORDER BY \"path\", chunk');\n const rows = (await stmt.all()) as Array<{\n path: string;\n chunk: number;\n start_line: number;\n end_line: number;\n vector: Uint8Array;\n }>;\n\n let queryNorm = 0;\n for (let d = 0; d < Math.min(storeDims, qv.length); d++) queryNorm += qv[d] * qv[d];\n queryNorm = Math.sqrt(queryNorm);\n\n const best = new Map<string, { score: number; chunk: number; lines: string }>();\n for (const row of rows) {\n if (allowed && !allowed.has(row.path)) continue;\n const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, Math.min(storeDims, row.vector.byteLength));\n // Rounding to int8 costs the stored row its unit norm, so dividing by that norm is what makes\n // this a cosine rather than a scaled dot product. `scale` cancels out of the ratio.\n let dot = 0;\n let norm = 0;\n const length = Math.min(q.length, qv.length);\n for (let d = 0; d < length; d++) {\n dot += q[d] * qv[d];\n norm += q[d] * q[d];\n }\n for (let d = length; d < q.length; d++) norm += q[d] * q[d];\n const score = norm === 0 || queryNorm === 0 ? 0 : dot / (Math.sqrt(norm) * queryNorm);\n const existing = best.get(row.path);\n if (!existing || score > existing.score || (score === existing.score && row.chunk < existing.chunk)) {\n best.set(row.path, { score, chunk: row.chunk, lines: `L${row.start_line}-${row.end_line}` });\n }\n }\n return [...best.entries()]\n .map(([path, value]) => ({ path, ...value }))\n .sort(compareVectorScores)\n .slice(0, fetch)\n .map(({ path, lines, score }) => ({ path, lines, similarity: asCosine(score) }));\n}\n\n// Note-to-note similarity is the max cosine over (target chunk, other chunk) pairs, one linear\n// scan of stored vectors.\nexport async function scanSimilar(conn: Connection, path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]> {\n const targetStmt = await conn.prepare('SELECT vector FROM embeddings WHERE \"path\" = ? AND vector IS NOT NULL ORDER BY chunk');\n const targetRows = (await targetStmt.all(path)) as Array<{ vector: Uint8Array }>;\n if (targetRows.length === 0) return [];\n // Each side's norm divides its own dot product, so both are taken once here rather than per pair.\n const target = sampleEvenly(targetRows).map((row) => {\n const v = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);\n let norm = 0;\n for (let d = 0; d < v.length; d++) norm += v[d] * v[d];\n return { v, norm: Math.sqrt(norm) };\n });\n\n const stmt = await conn.prepare('SELECT \"path\", vector FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string; vector: Uint8Array }>;\n const best = new Map<string, number>();\n for (const row of rows) {\n if (row.path === path || opts.exclude.has(row.path) || (opts.allowed && !opts.allowed.has(row.path))) continue;\n const other = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);\n let otherNorm = 0;\n for (let d = 0; d < other.length; d++) otherNorm += other[d] * other[d];\n otherNorm = Math.sqrt(otherNorm);\n for (const t of target) {\n let dot = 0;\n const len = Math.min(t.v.length, other.length);\n for (let d = 0; d < len; d++) dot += t.v[d] * other[d];\n const score = t.norm === 0 || otherNorm === 0 ? 0 : dot / (t.norm * otherNorm);\n const existing = best.get(row.path);\n if (existing === undefined || score > existing) best.set(row.path, score);\n }\n }\n\n return [...best.entries()]\n .map(([p, score]) => ({ path: p, score }))\n .sort(compareVectorScores)\n .slice(0, opts.k)\n .map(({ path: p, score }) => ({ path: p, similarity: asCosine(score) }));\n}\n"],"names":["asCosine","compareVectorScores","sampleEvenly","writeVectorBatch","conn","rows","length","runBatch","map","row","scale","vector","path","chunk","scanCandidates","qv","storeDims","fetch","allowed","stmt","prepare","all","queryNorm","d","Math","min","sqrt","best","Map","has","q","Int8Array","buffer","byteOffset","byteLength","dot","norm","score","existing","get","set","lines","start_line","end_line","entries","value","sort","slice","similarity","scanSimilar","opts","targetStmt","targetRows","target","v","exclude","other","otherNorm","t","len","undefined","p","k"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { rewriteInsert } from '../batch.js';
|
|
|
3
3
|
import { setMeta } from '../shared.js';
|
|
4
4
|
import { BEGIN_WRITE, withTransaction } from '../transaction.js';
|
|
5
5
|
import { CONNECT_OPTS, tursoApi } from './native.js';
|
|
6
|
+
import { rewriteFunctions } from './sql-functions.js';
|
|
6
7
|
// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%
|
|
7
8
|
// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.
|
|
8
9
|
let TursoStatementWrapper = class TursoStatementWrapper {
|
|
@@ -82,7 +83,7 @@ export function createConnection(db) {
|
|
|
82
83
|
await db.exec(sql);
|
|
83
84
|
},
|
|
84
85
|
async prepare (sql) {
|
|
85
|
-
return new TursoStatementWrapper(await db.prepare(sql));
|
|
86
|
+
return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));
|
|
86
87
|
},
|
|
87
88
|
// Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):
|
|
88
89
|
// no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.
|
|
@@ -102,7 +103,7 @@ export function createConnection(db) {
|
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
105
|
// Finalized here because nothing else will: open() hands back a connection the caller
|
|
105
|
-
// can hold across many batches, and
|
|
106
|
+
// can hold across many batches, and nothing else owns this statement's lifetime.
|
|
106
107
|
const stmt = await db.prepare(sql);
|
|
107
108
|
try {
|
|
108
109
|
for (const row of paramRows)await stmt.run(...row);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(sql));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and nothing else owns this statement's lifetime.\n const stmt = await db.prepare(sql);\n try {\n for (const row of paramRows) await stmt.run(...row);\n } finally {\n await stmt.close();\n }\n },\n BEGIN_WRITE\n );\n },\n };\n return conn;\n}\n"],"names":["stat","rewriteInsert","setMeta","BEGIN_WRITE","withTransaction","CONNECT_OPTS","tursoApi","rewriteFunctions","TursoStatementWrapper","run","params","stmt","get","all","iterate","columns","setReadBigInts","enabled","safeIntegers","checkpointWal","db","exec","err","console","error","message","cacheFilePath","conn","rows","prepare","find","r","name","file","fileSize","path","size","reclaimSpace","turso","connect","experimental","createConnection","String","close","sql","runBatch","paramRows","length","rewritten","flat","row"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function stemFolded(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { stemmer } from 'stemmer';
|
|
2
|
+
import { foldForSearch, searchTokens } from '../../text/segment.js';
|
|
3
|
+
export function stemFolded(text) {
|
|
4
|
+
let out = '';
|
|
5
|
+
let end = 0;
|
|
6
|
+
for (const token of searchTokens(text)){
|
|
7
|
+
out += text.slice(end, token.start).replace(/[\u0027\u2019]/gu, ' ');
|
|
8
|
+
out += token.unspaced ? token.text : stemmer(foldForSearch(token.text));
|
|
9
|
+
end = token.end;
|
|
10
|
+
}
|
|
11
|
+
return out + text.slice(end).replace(/[\u0027\u2019]/gu, ' ');
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/lexical-text.ts"],"sourcesContent":["import { stemmer } from 'stemmer';\nimport { foldForSearch, searchTokens } from '../../text/segment.ts';\n\nexport function stemFolded(text: string): string {\n let out = '';\n let end = 0;\n for (const token of searchTokens(text)) {\n out += text.slice(end, token.start).replace(/[\\u0027\\u2019]/gu, ' ');\n out += token.unspaced ? token.text : stemmer(foldForSearch(token.text));\n end = token.end;\n }\n return out + text.slice(end).replace(/[\\u0027\\u2019]/gu, ' ');\n}\n"],"names":["stemmer","foldForSearch","searchTokens","stemFolded","text","out","end","token","slice","start","replace","unspaced"],"mappings":"AAAA,SAASA,OAAO,QAAQ,UAAU;AAClC,SAASC,aAAa,EAAEC,YAAY,QAAQ,wBAAwB;AAEpE,OAAO,SAASC,WAAWC,IAAY;IACrC,IAAIC,MAAM;IACV,IAAIC,MAAM;IACV,KAAK,MAAMC,SAASL,aAAaE,MAAO;QACtCC,OAAOD,KAAKI,KAAK,CAACF,KAAKC,MAAME,KAAK,EAAEC,OAAO,CAAC,oBAAoB;QAChEL,OAAOE,MAAMI,QAAQ,GAAGJ,MAAMH,IAAI,GAAGJ,QAAQC,cAAcM,MAAMH,IAAI;QACrEE,MAAMC,MAAMD,GAAG;IACjB;IACA,OAAOD,MAAMD,KAAKI,KAAK,CAACF,KAAKI,OAAO,CAAC,oBAAoB;AAC3D"}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
// T1: Tantivy FTS via fts_match/fts_score on the default-tokenizer index for bare words and
|
|
2
2
|
// quoted phrases, plus a second ngram-tokenized index over "_ngram" sidecars for unspaced-script runs.
|
|
3
3
|
import { SenseError } from '../../errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { matchesSearchPhrase, searchTokens } from '../../text/segment.js';
|
|
5
|
+
import { stemFolded } from './lexical-text.js';
|
|
5
6
|
const FIELDS = [
|
|
6
|
-
'
|
|
7
|
-
'
|
|
8
|
-
'
|
|
7
|
+
'title_stem',
|
|
8
|
+
'summary_stem',
|
|
9
|
+
'text_stem'
|
|
9
10
|
];
|
|
10
11
|
const NGRAM_FIELDS = [
|
|
11
12
|
'title_ngram',
|
|
12
13
|
'summary_ngram',
|
|
13
14
|
'text_ngram'
|
|
14
15
|
];
|
|
16
|
+
const AUTHORED_FIELDS = [
|
|
17
|
+
'title',
|
|
18
|
+
'summary',
|
|
19
|
+
'text'
|
|
20
|
+
];
|
|
15
21
|
// FTS5 operator syntax a bare query would otherwise treat as literal text (PRINCIPLES:
|
|
16
22
|
// no-silent-modes). `^` is rejected either way: boost to Tantivy, initial-token match to FTS5.
|
|
17
23
|
const FTS5_OPERATORS = [
|
|
@@ -53,22 +59,30 @@ function splitTerms(terms) {
|
|
|
53
59
|
const words = [];
|
|
54
60
|
const phrases = [];
|
|
55
61
|
const unspaced = [];
|
|
62
|
+
let emptyPhrase = false;
|
|
56
63
|
const withoutPhrases = terms.replace(/"([^"]*)"/g, (_m, inner)=>{
|
|
57
64
|
const phrase = inner.trim();
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
65
|
+
const tokens = searchTokens(phrase);
|
|
66
|
+
if (tokens.length === 0) {
|
|
67
|
+
emptyPhrase = true;
|
|
68
|
+
return ' ';
|
|
69
|
+
}
|
|
70
|
+
unspaced.push(...tokens.filter((token)=>token.unspaced).map((token)=>token.text));
|
|
71
|
+
phrases.push(tokens);
|
|
61
72
|
return ' ';
|
|
62
73
|
});
|
|
63
74
|
for (const tok of withoutPhrases.split(/\s+/)){
|
|
64
75
|
if (tok.length === 0) continue;
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
for (const token of searchTokens(tok)){
|
|
77
|
+
if (token.unspaced) unspaced.push(token.text);
|
|
78
|
+
else words.push(token.text);
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
return {
|
|
69
82
|
words,
|
|
70
83
|
phrases,
|
|
71
|
-
unspaced
|
|
84
|
+
unspaced,
|
|
85
|
+
emptyPhrase
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
88
|
// fts_match/fts_score's query argument must be a SQL literal: bound as `?`, the row set stays
|
|
@@ -79,9 +93,16 @@ function escapeFtsLiteral(query) {
|
|
|
79
93
|
// Terms are quoted ("O'Brien's" is a parse error bare) and AND-joined: Tantivy's default for
|
|
80
94
|
// bare terms is disjunctive, so space-joining would loosen "apple banana" into "apple OR banana".
|
|
81
95
|
function buildQueries(words, phrases, unspaced) {
|
|
96
|
+
const phraseParts = phrases.flatMap((phrase)=>{
|
|
97
|
+
const latin = phrase.filter((token)=>!token.unspaced).map((token)=>stemFolded(token.text));
|
|
98
|
+
if (latin.length === 0) return [];
|
|
99
|
+
return phrase.some((token)=>token.unspaced) ? latin : [
|
|
100
|
+
latin.join(' ')
|
|
101
|
+
];
|
|
102
|
+
});
|
|
82
103
|
const defaultParts = [
|
|
83
|
-
...words,
|
|
84
|
-
...
|
|
104
|
+
...words.map(stemFolded),
|
|
105
|
+
...phraseParts
|
|
85
106
|
].map((t)=>`"${t}"`);
|
|
86
107
|
const unspacedParts = unspaced.map((t)=>`"${t}"`);
|
|
87
108
|
return {
|
|
@@ -98,31 +119,45 @@ function ftsBranch(cols, query, opts) {
|
|
|
98
119
|
FROM content ${opts.whereJoin} WHERE fts_match(${colList}, '${lit}') ${opts.whereCond} ${opts.scopeCond}`;
|
|
99
120
|
}
|
|
100
121
|
// Ranked query, scoped by the caller-built SQL fragments (same shape sqlite's/duckdb's
|
|
101
|
-
// queryLexical take). `
|
|
122
|
+
// queryLexical take). `snippets` is always NULL: fts_highlight returns the full column text, not a bounded snippet, so every row goes through the caller's JS snippet fallback (commands/search.ts).
|
|
102
123
|
export async function queryLexical(conn, terms, opts) {
|
|
103
124
|
const unsupported = unsupportedOperator(terms);
|
|
104
125
|
if (unsupported !== null) {
|
|
105
126
|
throw new SenseError('STORE_CAPABILITY_MISSING', `store "turso" does not implement FTS5's ${unsupported.label} ("${unsupported.token}") in this build; rephrase "${terms.trim()}" without it, or set "store" to "sqlite" in this tree's config to search it as written`);
|
|
106
127
|
}
|
|
107
|
-
const { words, phrases, unspaced } = splitTerms(terms);
|
|
128
|
+
const { words, phrases, unspaced, emptyPhrase } = splitTerms(terms);
|
|
129
|
+
if (emptyPhrase) return [];
|
|
108
130
|
const { defaultQuery, ngramQuery } = buildQueries(words, phrases, unspaced);
|
|
109
131
|
if (defaultQuery === '' && ngramQuery === '') return [];
|
|
110
132
|
const branches = [];
|
|
111
133
|
if (defaultQuery !== '') branches.push(ftsBranch(FIELDS, defaultQuery, opts));
|
|
112
134
|
if (ngramQuery !== '') branches.push(ftsBranch(NGRAM_FIELDS, ngramQuery, opts));
|
|
113
135
|
// HAVING COUNT(*) = branches.length is the AND across indexes; with one branch it is a no-op,
|
|
114
|
-
// `path` being content's primary key. fts_score is higher-is-better BM25, so DESC.
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
// `path` being content's primary key. fts_score is higher-is-better BM25, so DESC. Phrase
|
|
137
|
+
// candidates carry authored fields through the join below so the shared verifier can reject
|
|
138
|
+
// cross-field, reordered, and non-adjacent matches before applying the caller's limit.
|
|
139
|
+
const ranked = `SELECT path, SUM(score) AS score FROM (
|
|
117
140
|
${branches.join(' UNION ALL ')}
|
|
118
|
-
) parts GROUP BY path HAVING COUNT(*) = ${branches.length}
|
|
119
|
-
|
|
141
|
+
) parts GROUP BY path HAVING COUNT(*) = ${branches.length}`;
|
|
142
|
+
const phraseSelect = phrases.length > 0 ? ', content.title, content.summary, content.text' : '';
|
|
143
|
+
const phraseJoin = phrases.length > 0 ? ' JOIN content ON content.path = sq.path' : '';
|
|
144
|
+
const sql = `SELECT sq.path, sq.score${phraseSelect} FROM (
|
|
145
|
+
${ranked}
|
|
146
|
+
) sq${phraseJoin} ORDER BY sq.score DESC, sq.path${phrases.length > 0 ? '' : ' LIMIT ?'}`;
|
|
120
147
|
const stmt = await conn.prepare(sql);
|
|
121
|
-
const rows = await stmt.all(
|
|
148
|
+
const rows = await stmt.all(...phrases.length > 0 ? [] : [
|
|
149
|
+
opts.limit
|
|
150
|
+
]);
|
|
122
151
|
// A concurrent FTS read during an open write transaction can yield a {path: null} row. Raised,
|
|
123
152
|
// not filtered: filtering returns a silently short result (PRINCIPLES: no-silent-modes).
|
|
124
153
|
if (rows.some((r)=>r.path === null)) {
|
|
125
154
|
throw new SenseError('LEXICAL_NULL_PATH', 'store "turso" returned a search hit with no path (a known engine anomaly under concurrent FTS reads); retry the query');
|
|
126
155
|
}
|
|
127
|
-
|
|
156
|
+
const matches = phrases.length === 0 ? rows : rows.filter((row)=>phrases.every((phrase)=>AUTHORED_FIELDS.some((field)=>{
|
|
157
|
+
var _row_field;
|
|
158
|
+
return matchesSearchPhrase((_row_field = row[field]) !== null && _row_field !== void 0 ? _row_field : '', phrase);
|
|
159
|
+
})));
|
|
160
|
+
return matches.slice(0, opts.limit).map(({ path })=>({
|
|
161
|
+
path
|
|
162
|
+
}));
|
|
128
163
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/lexical.ts"],"sourcesContent":["// T1: Tantivy FTS via fts_match/fts_score on the default-tokenizer index for bare words and\n// quoted phrases, plus a second ngram-tokenized index over \"_ngram\" sidecars for unspaced-script runs.\nimport { SenseError } from '../../errors.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\nconst FIELDS = ['title', 'summary', 'text'] as const;\nconst NGRAM_FIELDS = ['title_ngram', 'summary_ngram', 'text_ngram'] as const;\n\n// FTS5 operator syntax a bare query would otherwise treat as literal text (PRINCIPLES:\n// no-silent-modes). `^` is rejected either way: boost to Tantivy, initial-token match to FTS5.\nconst FTS5_OPERATORS: Array<{ label: string; re: RegExp }> = [\n { label: 'prefix query', re: /[\\p{L}\\p{N}_]+\\*(?=\\s|$)/u },\n { label: 'boolean operator', re: /(?:^|\\s)(?:AND|OR|NOT)(?=\\s|$)/ },\n { label: 'NEAR operator', re: /(?:^|\\s)NEAR\\b/ },\n { label: 'boost/initial-token operator', re: /\\S*\\^\\S*/ },\n { label: 'column filter', re: /(?:^|\\s)[\\p{L}_]\\w*\\s*:/u },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, ' ');\n for (const { label, re } of FTS5_OPERATORS) {\n const m = withoutPhrases.match(re);\n if (m) return { label, token: m[0].trim() };\n }\n return null;\n}\n\n// A run whose script marks no word boundaries indexes as one opaque token under every tokenizer\n// but ngram, so such runs -- bare or quoted -- go to the ngram sidecar index instead.\nfunction splitTerms(terms: string): { words: string[]; phrases: string[]; unspaced: string[] } {\n const words: string[] = [];\n const phrases: string[] = [];\n const unspaced: string[] = [];\n const withoutPhrases = terms.replace(/\"([^\"]*)\"/g, (_m, inner: string) => {\n const phrase = inner.trim();\n if (phrase.length === 0) return ' ';\n if (hasUnspacedRun(phrase)) unspaced.push(phrase);\n else phrases.push(phrase);\n return ' ';\n });\n for (const tok of withoutPhrases.split(/\\s+/)) {\n if (tok.length === 0) continue;\n if (hasUnspacedRun(tok)) unspaced.push(tok);\n else words.push(tok);\n }\n return { words, phrases, unspaced };\n}\n\n// fts_match/fts_score's query argument must be a SQL literal: bound as `?`, the row set stays\n// correct but fts_score silently returns 0 for every row (PRINCIPLES: no-silent-modes).\nfunction escapeFtsLiteral(query: string): string {\n return query.replace(/'/g, \"''\");\n}\n\n// Terms are quoted (\"O'Brien's\" is a parse error bare) and AND-joined: Tantivy's default for\n// bare terms is disjunctive, so space-joining would loosen \"apple banana\" into \"apple OR banana\".\nfunction buildQueries(words: string[], phrases: string[], unspaced: string[]): { defaultQuery: string; ngramQuery: string } {\n const defaultParts = [...words, ...phrases].map((t) => `\"${t}\"`);\n const unspacedParts = unspaced.map((t) => `\"${t}\"`);\n return { defaultQuery: defaultParts.join(' AND '), ngramQuery: unspacedParts.join(' AND ') };\n}\n\n// fts_score() is real only as a SELECT's bare projected expression, and only for the index that\n// SELECT's fts_match() chose; every other form silently returns 0. Hence one SELECT per index.\nfunction ftsBranch(cols: readonly string[], query: string, opts: LexicalQueryOptions): string {\n const colList = cols.map((c) => `content.${c}`).join(', ');\n const lit = escapeFtsLiteral(query);\n return `SELECT content.\"path\" AS path, fts_score(${colList}, '${lit}') AS score\n FROM content ${opts.whereJoin} WHERE fts_match(${colList}, '${lit}') ${opts.whereCond} ${opts.scopeCond}`;\n}\n\n// Ranked query, scoped by the caller-built SQL fragments (same shape sqlite's/duckdb's\n// queryLexical take). `hit` is always NULL: fts_highlight returns the full column text, not a bounded snippet, so every row goes through the caller's JS excerpt fallback (commands/search.ts).\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]> {\n const unsupported = unsupportedOperator(terms);\n if (unsupported !== null) {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"turso\" does not implement FTS5's ${unsupported.label} (\"${unsupported.token}\") in this build; rephrase \"${terms.trim()}\" without it, or set \"store\" to \"sqlite\" in this tree's config to search it as written`);\n }\n const { words, phrases, unspaced } = splitTerms(terms);\n const { defaultQuery, ngramQuery } = buildQueries(words, phrases, unspaced);\n if (defaultQuery === '' && ngramQuery === '') return [];\n\n const branches: string[] = [];\n if (defaultQuery !== '') branches.push(ftsBranch(FIELDS, defaultQuery, opts));\n if (ngramQuery !== '') branches.push(ftsBranch(NGRAM_FIELDS, ngramQuery, opts));\n\n // HAVING COUNT(*) = branches.length is the AND across indexes; with one branch it is a no-op,\n // `path` being content's primary key. fts_score is higher-is-better BM25, so DESC.\n const sql = `SELECT path, NULL AS hit FROM (\n SELECT path, SUM(score) AS score FROM (\n ${branches.join(' UNION ALL ')}\n ) parts GROUP BY path HAVING COUNT(*) = ${branches.length}\n ) sq ORDER BY score DESC, path LIMIT ?`;\n const stmt = await conn.prepare(sql);\n const rows = (await stmt.all(opts.limit)) as unknown as LexicalHit[];\n // A concurrent FTS read during an open write transaction can yield a {path: null} row. Raised,\n // not filtered: filtering returns a silently short result (PRINCIPLES: no-silent-modes).\n if (rows.some((r) => r.path === null)) {\n throw new SenseError('LEXICAL_NULL_PATH', 'store \"turso\" returned a search hit with no path (a known engine anomaly under concurrent FTS reads); retry the query');\n }\n return rows;\n}\n"],"names":["SenseError","hasUnspacedRun","FIELDS","NGRAM_FIELDS","FTS5_OPERATORS","label","re","unsupportedOperator","terms","withoutPhrases","replace","m","match","token","trim","splitTerms","words","phrases","unspaced","_m","inner","phrase","length","push","tok","split","escapeFtsLiteral","query","buildQueries","defaultParts","map","t","unspacedParts","defaultQuery","join","ngramQuery","ftsBranch","cols","opts","colList","c","lit","whereJoin","whereCond","scopeCond","queryLexical","conn","unsupported","branches","sql","stmt","prepare","rows","all","limit","some","r","path"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/lexical.ts"],"sourcesContent":["// T1: Tantivy FTS via fts_match/fts_score on the default-tokenizer index for bare words and\n// quoted phrases, plus a second ngram-tokenized index over \"_ngram\" sidecars for unspaced-script runs.\nimport { SenseError } from '../../errors.ts';\nimport { matchesSearchPhrase, type SearchToken, searchTokens } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\nconst FIELDS = ['title_stem', 'summary_stem', 'text_stem'] as const;\nconst NGRAM_FIELDS = ['title_ngram', 'summary_ngram', 'text_ngram'] as const;\nconst AUTHORED_FIELDS = ['title', 'summary', 'text'] as const;\ntype AuthoredField = (typeof AUTHORED_FIELDS)[number];\n\n// FTS5 operator syntax a bare query would otherwise treat as literal text (PRINCIPLES:\n// no-silent-modes). `^` is rejected either way: boost to Tantivy, initial-token match to FTS5.\nconst FTS5_OPERATORS: Array<{ label: string; re: RegExp }> = [\n { label: 'prefix query', re: /[\\p{L}\\p{N}_]+\\*(?=\\s|$)/u },\n { label: 'boolean operator', re: /(?:^|\\s)(?:AND|OR|NOT)(?=\\s|$)/ },\n { label: 'NEAR operator', re: /(?:^|\\s)NEAR\\b/ },\n { label: 'boost/initial-token operator', re: /\\S*\\^\\S*/ },\n { label: 'column filter', re: /(?:^|\\s)[\\p{L}_]\\w*\\s*:/u },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, ' ');\n for (const { label, re } of FTS5_OPERATORS) {\n const m = withoutPhrases.match(re);\n if (m) return { label, token: m[0].trim() };\n }\n return null;\n}\n\n// A run whose script marks no word boundaries indexes as one opaque token under every tokenizer\n// but ngram, so such runs -- bare or quoted -- go to the ngram sidecar index instead.\nfunction splitTerms(terms: string): { words: string[]; phrases: SearchToken[][]; unspaced: string[]; emptyPhrase: boolean } {\n const words: string[] = [];\n const phrases: SearchToken[][] = [];\n const unspaced: string[] = [];\n let emptyPhrase = false;\n const withoutPhrases = terms.replace(/\"([^\"]*)\"/g, (_m, inner: string) => {\n const phrase = inner.trim();\n const tokens = searchTokens(phrase);\n if (tokens.length === 0) {\n emptyPhrase = true;\n return ' ';\n }\n unspaced.push(...tokens.filter((token) => token.unspaced).map((token) => token.text));\n phrases.push(tokens);\n return ' ';\n });\n for (const tok of withoutPhrases.split(/\\s+/)) {\n if (tok.length === 0) continue;\n for (const token of searchTokens(tok)) {\n if (token.unspaced) unspaced.push(token.text);\n else words.push(token.text);\n }\n }\n return { words, phrases, unspaced, emptyPhrase };\n}\n\n// fts_match/fts_score's query argument must be a SQL literal: bound as `?`, the row set stays\n// correct but fts_score silently returns 0 for every row (PRINCIPLES: no-silent-modes).\nfunction escapeFtsLiteral(query: string): string {\n return query.replace(/'/g, \"''\");\n}\n\n// Terms are quoted (\"O'Brien's\" is a parse error bare) and AND-joined: Tantivy's default for\n// bare terms is disjunctive, so space-joining would loosen \"apple banana\" into \"apple OR banana\".\nfunction buildQueries(words: string[], phrases: SearchToken[][], unspaced: string[]): { defaultQuery: string; ngramQuery: string } {\n const phraseParts = phrases.flatMap((phrase) => {\n const latin = phrase.filter((token) => !token.unspaced).map((token) => stemFolded(token.text));\n if (latin.length === 0) return [];\n return phrase.some((token) => token.unspaced) ? latin : [latin.join(' ')];\n });\n const defaultParts = [...words.map(stemFolded), ...phraseParts].map((t) => `\"${t}\"`);\n const unspacedParts = unspaced.map((t) => `\"${t}\"`);\n return { defaultQuery: defaultParts.join(' AND '), ngramQuery: unspacedParts.join(' AND ') };\n}\n\n// fts_score() is real only as a SELECT's bare projected expression, and only for the index that\n// SELECT's fts_match() chose; every other form silently returns 0. Hence one SELECT per index.\nfunction ftsBranch(cols: readonly string[], query: string, opts: LexicalQueryOptions): string {\n const colList = cols.map((c) => `content.${c}`).join(', ');\n const lit = escapeFtsLiteral(query);\n return `SELECT content.\"path\" AS path, fts_score(${colList}, '${lit}') AS score\n FROM content ${opts.whereJoin} WHERE fts_match(${colList}, '${lit}') ${opts.whereCond} ${opts.scopeCond}`;\n}\n\n// Ranked query, scoped by the caller-built SQL fragments (same shape sqlite's/duckdb's\n// queryLexical take). `snippets` is always NULL: fts_highlight returns the full column text, not a bounded snippet, so every row goes through the caller's JS snippet fallback (commands/search.ts).\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]> {\n const unsupported = unsupportedOperator(terms);\n if (unsupported !== null) {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"turso\" does not implement FTS5's ${unsupported.label} (\"${unsupported.token}\") in this build; rephrase \"${terms.trim()}\" without it, or set \"store\" to \"sqlite\" in this tree's config to search it as written`);\n }\n const { words, phrases, unspaced, emptyPhrase } = splitTerms(terms);\n if (emptyPhrase) return [];\n const { defaultQuery, ngramQuery } = buildQueries(words, phrases, unspaced);\n if (defaultQuery === '' && ngramQuery === '') return [];\n\n const branches: string[] = [];\n if (defaultQuery !== '') branches.push(ftsBranch(FIELDS, defaultQuery, opts));\n if (ngramQuery !== '') branches.push(ftsBranch(NGRAM_FIELDS, ngramQuery, opts));\n\n // HAVING COUNT(*) = branches.length is the AND across indexes; with one branch it is a no-op,\n // `path` being content's primary key. fts_score is higher-is-better BM25, so DESC. Phrase\n // candidates carry authored fields through the join below so the shared verifier can reject\n // cross-field, reordered, and non-adjacent matches before applying the caller's limit.\n const ranked = `SELECT path, SUM(score) AS score FROM (\n ${branches.join(' UNION ALL ')}\n ) parts GROUP BY path HAVING COUNT(*) = ${branches.length}`;\n const phraseSelect = phrases.length > 0 ? ', content.title, content.summary, content.text' : '';\n const phraseJoin = phrases.length > 0 ? ' JOIN content ON content.path = sq.path' : '';\n const sql = `SELECT sq.path, sq.score${phraseSelect} FROM (\n ${ranked}\n ) sq${phraseJoin} ORDER BY sq.score DESC, sq.path${phrases.length > 0 ? '' : ' LIMIT ?'}`;\n const stmt = await conn.prepare(sql);\n const rows = (await stmt.all(...(phrases.length > 0 ? [] : [opts.limit]))) as Array<LexicalHit & Partial<Record<AuthoredField, string>>>;\n // A concurrent FTS read during an open write transaction can yield a {path: null} row. Raised,\n // not filtered: filtering returns a silently short result (PRINCIPLES: no-silent-modes).\n if (rows.some((r) => r.path === null)) {\n throw new SenseError('LEXICAL_NULL_PATH', 'store \"turso\" returned a search hit with no path (a known engine anomaly under concurrent FTS reads); retry the query');\n }\n const matches = phrases.length === 0 ? rows : rows.filter((row) => phrases.every((phrase) => AUTHORED_FIELDS.some((field) => matchesSearchPhrase(row[field] ?? '', phrase))));\n return matches.slice(0, opts.limit).map(({ path }) => ({ path }));\n}\n"],"names":["SenseError","matchesSearchPhrase","searchTokens","stemFolded","FIELDS","NGRAM_FIELDS","AUTHORED_FIELDS","FTS5_OPERATORS","label","re","unsupportedOperator","terms","withoutPhrases","replace","m","match","token","trim","splitTerms","words","phrases","unspaced","emptyPhrase","_m","inner","phrase","tokens","length","push","filter","map","text","tok","split","escapeFtsLiteral","query","buildQueries","phraseParts","flatMap","latin","some","join","defaultParts","t","unspacedParts","defaultQuery","ngramQuery","ftsBranch","cols","opts","colList","c","lit","whereJoin","whereCond","scopeCond","queryLexical","conn","unsupported","branches","ranked","phraseSelect","phraseJoin","sql","stmt","prepare","rows","all","limit","r","path","matches","row","every","field","slice"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import type { ResolvedConfig } from '../../config/index.js';
|
|
|
3
3
|
import type { OpenResult } from '../open.js';
|
|
4
4
|
import type { OpenDialect } from '../types.js';
|
|
5
5
|
export declare const DB_FILENAME = "cache.turso.db";
|
|
6
|
-
export declare const SCHEMA_VERSION = "
|
|
6
|
+
export declare const SCHEMA_VERSION = "8";
|
|
7
7
|
export type { OpenResult };
|
|
8
8
|
export declare const tursoOpenDialect: OpenDialect<Database>;
|
|
9
9
|
export declare function openTurso(cfg: ResolvedConfig): Promise<OpenResult>;
|
|
@@ -11,12 +11,12 @@ import { createStore } from './store.js';
|
|
|
11
11
|
export const DB_FILENAME = 'cache.turso.db';
|
|
12
12
|
// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves
|
|
13
13
|
// separately. Covers the FTS indexes, the "_ngram" sidecar columns, and embeddings.vector's width.
|
|
14
|
-
export const SCHEMA_VERSION = '
|
|
14
|
+
export const SCHEMA_VERSION = '8';
|
|
15
15
|
// This store's Handle (types.ts's OpenDialect<Handle>) is the connected Database itself: no
|
|
16
16
|
// extra native state to thread, unlike duckdb's separate instance/connection pair.
|
|
17
17
|
async function ensureSchema(_handle, conn, cfg) {
|
|
18
18
|
await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_ctime" REAL, "_size" INTEGER, "_parse_error" TEXT)`);
|
|
19
|
-
await conn.exec(`CREATE TABLE IF NOT EXISTS content ("path" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT, title_ngram TEXT, summary_ngram TEXT, text_ngram TEXT)`);
|
|
19
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS content ("path" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT, title_stem TEXT, summary_stem TEXT, text_stem TEXT, title_ngram TEXT, summary_ngram TEXT, text_ngram TEXT)`);
|
|
20
20
|
for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
|
|
21
21
|
await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
|
|
22
22
|
await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
|