sensemaking 0.27.1 → 0.28.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.
Files changed (138) hide show
  1. package/README.md +2 -2
  2. package/bin/cli.js +16 -7
  3. package/dist/cjs/cli/constants.d.cts +2 -0
  4. package/dist/cjs/cli/constants.d.ts +2 -0
  5. package/dist/cjs/cli/constants.js +21 -0
  6. package/dist/cjs/cli/constants.js.map +1 -0
  7. package/dist/cjs/cli/named.js +2 -1
  8. package/dist/cjs/cli/named.js.map +1 -1
  9. package/dist/cjs/cli/shared.js +5 -4
  10. package/dist/cjs/cli/shared.js.map +1 -1
  11. package/dist/cjs/cli.js +10 -9
  12. package/dist/cjs/cli.js.map +1 -1
  13. package/dist/cjs/embed/query.js.map +1 -1
  14. package/dist/cjs/store/duckdb/engine.d.cts +11 -0
  15. package/dist/cjs/store/duckdb/engine.d.ts +11 -0
  16. package/dist/cjs/store/duckdb/engine.js +193 -0
  17. package/dist/cjs/store/duckdb/engine.js.map +1 -0
  18. package/dist/cjs/store/duckdb/lexical.js +34 -20
  19. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  20. package/dist/cjs/store/duckdb/native.d.cts +5 -2
  21. package/dist/cjs/store/duckdb/native.d.ts +5 -2
  22. package/dist/cjs/store/duckdb/native.js +157 -13
  23. package/dist/cjs/store/duckdb/native.js.map +1 -1
  24. package/dist/cjs/store/duckdb/open.js +151 -3
  25. package/dist/cjs/store/duckdb/open.js.map +1 -1
  26. package/dist/cjs/store/duckdb/ordered-bm25.d.cts +3 -3
  27. package/dist/cjs/store/duckdb/ordered-bm25.d.ts +3 -3
  28. package/dist/cjs/store/duckdb/ordered-bm25.js +4 -8
  29. package/dist/cjs/store/duckdb/ordered-bm25.js.map +1 -1
  30. package/dist/cjs/store/engine.d.cts +28 -0
  31. package/dist/cjs/store/engine.d.ts +28 -0
  32. package/dist/cjs/store/engine.js +362 -0
  33. package/dist/cjs/store/engine.js.map +1 -0
  34. package/dist/cjs/store/native.d.cts +8 -5
  35. package/dist/cjs/store/native.d.ts +8 -5
  36. package/dist/cjs/store/native.js +78 -73
  37. package/dist/cjs/store/native.js.map +1 -1
  38. package/dist/cjs/store/open.js +100 -40
  39. package/dist/cjs/store/open.js.map +1 -1
  40. package/dist/cjs/store/sqlite/engine.d.cts +12 -0
  41. package/dist/cjs/store/sqlite/engine.d.ts +12 -0
  42. package/dist/cjs/store/sqlite/engine.js +53 -0
  43. package/dist/cjs/store/sqlite/engine.js.map +1 -0
  44. package/dist/cjs/store/sqlite/lexical.js +3 -4
  45. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  46. package/dist/cjs/store/sqlite/open.js +119 -6
  47. package/dist/cjs/store/sqlite/open.js.map +1 -1
  48. package/dist/cjs/store/turso/engine.d.cts +9 -0
  49. package/dist/cjs/store/turso/engine.d.ts +9 -0
  50. package/dist/cjs/store/turso/engine.js +189 -0
  51. package/dist/cjs/store/turso/engine.js.map +1 -0
  52. package/dist/cjs/store/turso/lexical.js +16 -11
  53. package/dist/cjs/store/turso/lexical.js.map +1 -1
  54. package/dist/cjs/store/turso/native.d.cts +5 -1
  55. package/dist/cjs/store/turso/native.d.ts +5 -1
  56. package/dist/cjs/store/turso/native.js +157 -8
  57. package/dist/cjs/store/turso/native.js.map +1 -1
  58. package/dist/cjs/store/turso/open.js +172 -27
  59. package/dist/cjs/store/turso/open.js.map +1 -1
  60. package/dist/cjs/store/turso/reconcile.d.cts +0 -1
  61. package/dist/cjs/store/turso/reconcile.d.ts +0 -1
  62. package/dist/cjs/store/turso/reconcile.js +22 -23
  63. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  64. package/dist/cjs/store/types.d.cts +2 -0
  65. package/dist/cjs/store/types.d.ts +2 -0
  66. package/dist/cjs/store/types.js.map +1 -1
  67. package/dist/esm/cli/constants.d.ts +2 -0
  68. package/dist/esm/cli/constants.js +2 -0
  69. package/dist/esm/cli/constants.js.map +1 -0
  70. package/dist/esm/cli/named.js +2 -1
  71. package/dist/esm/cli/named.js.map +1 -1
  72. package/dist/esm/cli/shared.js +6 -5
  73. package/dist/esm/cli/shared.js.map +1 -1
  74. package/dist/esm/cli.js +10 -9
  75. package/dist/esm/cli.js.map +1 -1
  76. package/dist/esm/embed/query.js +1 -1
  77. package/dist/esm/embed/query.js.map +1 -1
  78. package/dist/esm/store/duckdb/engine.d.ts +11 -0
  79. package/dist/esm/store/duckdb/engine.js +18 -0
  80. package/dist/esm/store/duckdb/engine.js.map +1 -0
  81. package/dist/esm/store/duckdb/lexical.js +11 -11
  82. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  83. package/dist/esm/store/duckdb/native.d.ts +5 -2
  84. package/dist/esm/store/duckdb/native.js +16 -12
  85. package/dist/esm/store/duckdb/native.js.map +1 -1
  86. package/dist/esm/store/duckdb/open.js +40 -2
  87. package/dist/esm/store/duckdb/open.js.map +1 -1
  88. package/dist/esm/store/duckdb/ordered-bm25.d.ts +3 -3
  89. package/dist/esm/store/duckdb/ordered-bm25.js +4 -9
  90. package/dist/esm/store/duckdb/ordered-bm25.js.map +1 -1
  91. package/dist/esm/store/engine.d.ts +28 -0
  92. package/dist/esm/store/engine.js +85 -0
  93. package/dist/esm/store/engine.js.map +1 -0
  94. package/dist/esm/store/native.d.ts +8 -5
  95. package/dist/esm/store/native.js +59 -57
  96. package/dist/esm/store/native.js.map +1 -1
  97. package/dist/esm/store/open.js +24 -7
  98. package/dist/esm/store/open.js.map +1 -1
  99. package/dist/esm/store/sqlite/engine.d.ts +12 -0
  100. package/dist/esm/store/sqlite/engine.js +16 -0
  101. package/dist/esm/store/sqlite/engine.js.map +1 -0
  102. package/dist/esm/store/sqlite/lexical.js +2 -3
  103. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  104. package/dist/esm/store/sqlite/open.js +38 -7
  105. package/dist/esm/store/sqlite/open.js.map +1 -1
  106. package/dist/esm/store/turso/engine.d.ts +9 -0
  107. package/dist/esm/store/turso/engine.js +14 -0
  108. package/dist/esm/store/turso/engine.js.map +1 -0
  109. package/dist/esm/store/turso/lexical.js +9 -9
  110. package/dist/esm/store/turso/lexical.js.map +1 -1
  111. package/dist/esm/store/turso/native.d.ts +5 -1
  112. package/dist/esm/store/turso/native.js +16 -10
  113. package/dist/esm/store/turso/native.js.map +1 -1
  114. package/dist/esm/store/turso/open.js +44 -3
  115. package/dist/esm/store/turso/open.js.map +1 -1
  116. package/dist/esm/store/turso/reconcile.d.ts +0 -1
  117. package/dist/esm/store/turso/reconcile.js +3 -8
  118. package/dist/esm/store/turso/reconcile.js.map +1 -1
  119. package/dist/esm/store/types.d.ts +2 -0
  120. package/dist/esm/store/types.js.map +1 -1
  121. package/package.json +21 -7
  122. package/skills/sense/EXAMPLES.md +0 -99
  123. package/skills/sense/SKILL.md +0 -145
  124. package/skills/sense/references/search.md +0 -63
  125. package/skills/sense/references/sql.md +0 -120
  126. package/skills/sense/references/stores/duckdb.md +0 -35
  127. package/skills/sense/references/stores/sqlite.md +0 -66
  128. package/skills/sense/references/stores/turso.md +0 -36
  129. package/skills/sense/references/terminology.md +0 -32
  130. package/skills/sense-bases/EXAMPLES.md +0 -103
  131. package/skills/sense-bases/SKILL.md +0 -147
  132. package/skills/sense-setup/EXAMPLES.md +0 -129
  133. package/skills/sense-setup/SKILL.md +0 -112
  134. package/skills/sense-setup/references/embeddings.md +0 -51
  135. package/skills/sense-setup/references/store-benchmarks.md +0 -16
  136. package/skills/sense-setup/references/stores/duckdb.md +0 -28
  137. package/skills/sense-setup/references/stores/sqlite.md +0 -21
  138. package/skills/sense-setup/references/stores/turso.md +0 -25
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/sqlite/lexical.ts"],"sourcesContent":["import { segmentMatch } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\nimport { sqliteFts } from './engine.ts';\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 stmt = await conn.prepare(sqliteFts().rankedMatch(whereJoin, whereCond, scopeCond, limit));\n return (await stmt.all(query)) as unknown as LexicalHit[];\n}\n"],"names":["segmentMatch","sqliteFts","queryLexical","conn","terms","opts","whereJoin","whereCond","scopeCond","limit","query","trim","stmt","prepare","rankedMatch","all"],"mappings":"AAAA,SAASA,YAAY,QAAQ,wBAAwB;AAErD,SAASC,SAAS,QAAQ,cAAc;AAExC,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,OAAO,MAAMT,KAAKU,OAAO,CAACZ,YAAYa,WAAW,CAACR,WAAWC,WAAWC,WAAWC;IACzF,OAAQ,MAAMG,KAAKG,GAAG,CAACL;AACzB"}
@@ -2,24 +2,22 @@
2
2
  // both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { activeFeatures } from '../../features/index.js';
5
+ import { ENGINE_CHECK_DOCS, checkEngine as runEngineCheck } from '../engine.js';
5
6
  import { openWithDialect } from '../open.js';
6
7
  import { getMeta, setMeta } from '../shared.js';
7
8
  import { BEGIN_WRITE, withTransaction } from '../transaction.js';
8
9
  import { createConnection } from './connection.js';
9
- import { sqliteDialect } from './reconcile.js';
10
+ import { SQLITE_VERSION, sqliteFts, sqliteIdentity } from './engine.js';
11
+ import { queryLexical } from './lexical.js';
12
+ import { contentRow, INSERT_CONTENT_SQL, sqliteDialect } from './reconcile.js';
10
13
  import { registerFunctions } from './sql-functions.js';
11
14
  import { createStore } from './store.js';
12
15
  export const DB_FILENAME = 'cache.db';
13
16
  // Cache shape version, independent of the config's own `version`. Bumping it rebuilds
14
17
  // existing trees on first query.
15
18
  export const SCHEMA_VERSION = '22';
16
- // unicode61 splits on spaces, so a language written without them indexes a whole run as one
17
- // token and word search finds nothing; the `_seg` sidecars (always populated) cover that case.
18
- const TOKENIZE = 'porter unicode61';
19
- // The `_seg` sidecars are appended after path, never inserted: bm25() and snippet(content, 2)
20
- // are documented against the first three columns. Each holds its field's exploded unspaced runs.
21
19
  async function createContentTable(conn) {
22
- await conn.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, title_seg, summary_seg, text_seg, tokenize = '${TOKENIZE}')`);
20
+ await conn.exec(sqliteFts().createContent());
23
21
  }
24
22
  // Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`
25
23
  // can't dump file text into context. Feature schema is published later with its generation.
@@ -86,6 +84,37 @@ async function ensureFeatureSchema(_handle, conn, cfg) {
86
84
  async function setDerivedBusyTimeout(handle, _conn, ms) {
87
85
  handle.db.exec(`PRAGMA busy_timeout = ${ms}`);
88
86
  }
87
+ // The real schema, content write and lexical query over store/engine.ts's fixture, in memory.
88
+ async function checkSqliteEngine() {
89
+ const db = new DatabaseSync(':memory:');
90
+ try {
91
+ registerFunctions(db);
92
+ const conn = createConnection(db);
93
+ await conn.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)');
94
+ await ensureCoreSchemaTables(conn);
95
+ await conn.runBatch('INSERT INTO frontmatter ("path") VALUES (?)', ENGINE_CHECK_DOCS.map((doc)=>[
96
+ doc.path
97
+ ]));
98
+ const docs = ENGINE_CHECK_DOCS.map((doc)=>({
99
+ relPath: doc.path,
100
+ search: {
101
+ title: doc.title,
102
+ summary: doc.summary,
103
+ text: doc.text
104
+ }
105
+ }));
106
+ await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));
107
+ const remedy = `this SQLite is bundled with Node ${process.version} (SQLite ${SQLITE_VERSION !== null && SQLITE_VERSION !== void 0 ? SQLITE_VERSION : 'unknown'}); run sense on a Node release whose bundled SQLite passes`;
108
+ await runEngineCheck('sqlite', sqliteIdentity(), remedy, async (terms)=>(await queryLexical(conn, terms, {
109
+ whereJoin: '',
110
+ whereCond: '',
111
+ scopeCond: '',
112
+ limit: 10
113
+ })).map((hit)=>hit.path));
114
+ } finally{
115
+ db.close();
116
+ }
117
+ }
89
118
  // This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.
90
119
  export const sqliteOpenDialect = {
91
120
  filename: DB_FILENAME,
@@ -93,6 +122,8 @@ export const sqliteOpenDialect = {
93
122
  reconcileDialect: sqliteDialect,
94
123
  connect,
95
124
  close,
125
+ engineIdentity: async ()=>sqliteIdentity(),
126
+ checkEngine: checkSqliteEngine,
96
127
  ensureCoreSchema,
97
128
  ensureFeatureSchema,
98
129
  setDerivedBusyTimeout,
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/sqlite/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { activeFeatures } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { sqliteDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '22';\n\nexport type { OpenResult };\n\n// unicode61 splits on spaces, so a language written without them indexes a whole run as one\n// token and word search finds nothing; the `_seg` sidecars (always populated) cover that case.\nconst TOKENIZE = 'porter unicode61';\n\ninterface SqliteHandle {\n db: DatabaseSync;\n}\n\n// The `_seg` sidecars are appended after path, never inserted: bm25() and snippet(content, 2)\n// are documented against the first three columns. Each holds its field's exploded unspaced runs.\nasync function createContentTable(conn: Connection): Promise<void> {\n await conn.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS content USING fts5(title, summary, text, path UNINDEXED, title_seg, summary_seg, text_seg, tokenize = '${TOKENIZE}')`);\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Feature schema is published later with its generation.\nasync function ensureCoreSchemaTables(conn: Connection): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await createContentTable(conn);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n // Coverage, not ownership: a path can appear under several presets. path leads the PK so the\n // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n}\n\n// Two processes opening the same fresh tree both try to convert it, and the loser gets SQLITE_BUSY\n// with no busy handler behind it. Bounded because a lock held past this is a real problem, not a race.\nfunction setJournalWal(db: DatabaseSync): void {\n const deadline = Date.now() + 5000;\n for (;;) {\n try {\n db.exec('PRAGMA journal_mode = WAL');\n return;\n } catch (err) {\n if (Date.now() >= deadline || !/database is locked|busy/i.test((err as Error).message)) throw err;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);\n }\n }\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: SqliteHandle; conn: Connection }> {\n const db = new DatabaseSync(dbPath, options.existingOnly ? { readOnly: true } : {});\n try {\n // Before journal_mode, not after: converting a fresh database to WAL takes a brief exclusive\n // lock, and with no timeout set yet a second process opening the same tree fails in 1ms.\n db.exec('PRAGMA busy_timeout = 30000');\n // busy_timeout does not cover the WAL conversion itself: SQLite does not invoke the busy\n // handler for it, so a concurrent cold open needs its own bounded wait.\n if (!options.observational) setJournalWal(db);\n registerFunctions(db);\n const conn = createConnection(db);\n return { handle: { db }, conn };\n } catch (err) {\n // A throw here must release this handle, or on Windows the leaked WAL db is undeletable and\n // scratch cleanup fails with EPERM/EBUSY.\n db.close();\n throw err;\n }\n}\n\nasync function close(handle: SqliteHandle): Promise<void> {\n handle.db.close();\n}\n\nasync function ensureCoreSchema(_handle: SqliteHandle, conn: Connection): Promise<void> {\n // One writer at a time: two cold opens can bootstrap the same database concurrently.\n await withTransaction(conn, () => ensureCoreSchemaTables(conn), BEGIN_WRITE);\n}\n\nasync function ensureFeatureSchema(_handle: SqliteHandle, conn: Connection, cfg: Config): Promise<void> {\n for (const feature of activeFeatures(cfg)) await feature.schema(conn);\n}\n\nasync function setDerivedBusyTimeout(handle: SqliteHandle, _conn: Connection, ms: number): Promise<void> {\n handle.db.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const sqliteOpenDialect: OpenDialect<SqliteHandle> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: sqliteDialect,\n connect,\n close,\n ensureCoreSchema,\n ensureFeatureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle.db, conn),\n};\n\nexport async function openSqlite(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, sqliteOpenDialect, options);\n}\n"],"names":["DatabaseSync","activeFeatures","openWithDialect","getMeta","setMeta","BEGIN_WRITE","withTransaction","createConnection","sqliteDialect","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","TOKENIZE","createContentTable","conn","exec","ensureCoreSchemaTables","setJournalWal","db","deadline","Date","now","err","test","message","Atomics","wait","Int32Array","SharedArrayBuffer","connect","dbPath","_cfg","options","existingOnly","readOnly","observational","handle","close","ensureCoreSchema","_handle","ensureFeatureSchema","cfg","feature","schema","setDerivedBusyTimeout","_conn","ms","sqliteOpenDialect","filename","schemaVersion","reconcileDialect","openSqlite"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/sqlite/open.ts"],"sourcesContent":["// The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with\n// both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { activeFeatures } from '../../features/index.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { ENGINE_CHECK_DOCS, checkEngine as runEngineCheck } from '../engine.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { SQLITE_VERSION, sqliteFts, sqliteIdentity } from './engine.ts';\nimport { queryLexical } from './lexical.ts';\nimport { contentRow, INSERT_CONTENT_SQL, sqliteDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.db';\n// Cache shape version, independent of the config's own `version`. Bumping it rebuilds\n// existing trees on first query.\nexport const SCHEMA_VERSION = '22';\n\nexport type { OpenResult };\n\ninterface SqliteHandle {\n db: DatabaseSync;\n}\n\nasync function createContentTable(conn: Connection): Promise<void> {\n await conn.exec(sqliteFts().createContent());\n}\n\n// Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`\n// can't dump file text into context. Feature schema is published later with its generation.\nasync function ensureCoreSchemaTables(conn: Connection): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await createContentTable(conn);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n // Coverage, not ownership: a path can appear under several presets. path leads the PK so the\n // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n}\n\n// Two processes opening the same fresh tree both try to convert it, and the loser gets SQLITE_BUSY\n// with no busy handler behind it. Bounded because a lock held past this is a real problem, not a race.\nfunction setJournalWal(db: DatabaseSync): void {\n const deadline = Date.now() + 5000;\n for (;;) {\n try {\n db.exec('PRAGMA journal_mode = WAL');\n return;\n } catch (err) {\n if (Date.now() >= deadline || !/database is locked|busy/i.test((err as Error).message)) throw err;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);\n }\n }\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: SqliteHandle; conn: Connection }> {\n const db = new DatabaseSync(dbPath, options.existingOnly ? { readOnly: true } : {});\n try {\n // Before journal_mode, not after: converting a fresh database to WAL takes a brief exclusive\n // lock, and with no timeout set yet a second process opening the same tree fails in 1ms.\n db.exec('PRAGMA busy_timeout = 30000');\n // busy_timeout does not cover the WAL conversion itself: SQLite does not invoke the busy\n // handler for it, so a concurrent cold open needs its own bounded wait.\n if (!options.observational) setJournalWal(db);\n registerFunctions(db);\n const conn = createConnection(db);\n return { handle: { db }, conn };\n } catch (err) {\n // A throw here must release this handle, or on Windows the leaked WAL db is undeletable and\n // scratch cleanup fails with EPERM/EBUSY.\n db.close();\n throw err;\n }\n}\n\nasync function close(handle: SqliteHandle): Promise<void> {\n handle.db.close();\n}\n\nasync function ensureCoreSchema(_handle: SqliteHandle, conn: Connection): Promise<void> {\n // One writer at a time: two cold opens can bootstrap the same database concurrently.\n await withTransaction(conn, () => ensureCoreSchemaTables(conn), BEGIN_WRITE);\n}\n\nasync function ensureFeatureSchema(_handle: SqliteHandle, conn: Connection, cfg: Config): Promise<void> {\n for (const feature of activeFeatures(cfg)) await feature.schema(conn);\n}\n\nasync function setDerivedBusyTimeout(handle: SqliteHandle, _conn: Connection, ms: number): Promise<void> {\n handle.db.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\n// The real schema, content write and lexical query over store/engine.ts's fixture, in memory.\nasync function checkSqliteEngine(): Promise<void> {\n const db = new DatabaseSync(':memory:');\n try {\n registerFunctions(db);\n const conn = createConnection(db);\n await conn.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)');\n await ensureCoreSchemaTables(conn);\n await conn.runBatch(\n 'INSERT INTO frontmatter (\"path\") VALUES (?)',\n ENGINE_CHECK_DOCS.map((doc) => [doc.path])\n );\n const docs = ENGINE_CHECK_DOCS.map((doc) => ({ relPath: doc.path, search: { title: doc.title, summary: doc.summary, text: doc.text } }) as ParsedDoc);\n await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n const remedy = `this SQLite is bundled with Node ${process.version} (SQLite ${SQLITE_VERSION ?? 'unknown'}); run sense on a Node release whose bundled SQLite passes`;\n await runEngineCheck('sqlite', sqliteIdentity(), remedy, async (terms) => (await queryLexical(conn, terms, { whereJoin: '', whereCond: '', scopeCond: '', limit: 10 })).map((hit) => hit.path));\n } finally {\n db.close();\n }\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const sqliteOpenDialect: OpenDialect<SqliteHandle> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: sqliteDialect,\n connect,\n close,\n engineIdentity: async () => sqliteIdentity(),\n checkEngine: checkSqliteEngine,\n ensureCoreSchema,\n ensureFeatureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle.db, conn),\n};\n\nexport async function openSqlite(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, sqliteOpenDialect, options);\n}\n"],"names":["DatabaseSync","activeFeatures","ENGINE_CHECK_DOCS","checkEngine","runEngineCheck","openWithDialect","getMeta","setMeta","BEGIN_WRITE","withTransaction","createConnection","SQLITE_VERSION","sqliteFts","sqliteIdentity","queryLexical","contentRow","INSERT_CONTENT_SQL","sqliteDialect","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","createContentTable","conn","exec","createContent","ensureCoreSchemaTables","setJournalWal","db","deadline","Date","now","err","test","message","Atomics","wait","Int32Array","SharedArrayBuffer","connect","dbPath","_cfg","options","existingOnly","readOnly","observational","handle","close","ensureCoreSchema","_handle","ensureFeatureSchema","cfg","feature","schema","setDerivedBusyTimeout","_conn","ms","checkSqliteEngine","runBatch","map","doc","path","docs","relPath","search","title","summary","text","remedy","process","version","terms","whereJoin","whereCond","scopeCond","limit","hit","sqliteOpenDialect","filename","schemaVersion","reconcileDialect","engineIdentity","openSqlite"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type EnginePatch } from '../engine.js';
2
+ export interface TursoFtsBuilders {
3
+ indexDdl(): readonly string[];
4
+ score(columns: string, literal: string): string;
5
+ match(columns: string, literal: string): string;
6
+ }
7
+ export declare const TURSO_FTS_BUILDERS: TursoFtsBuilders;
8
+ export declare const TURSO_PATCHES: readonly EnginePatch<TursoFtsBuilders>[];
9
+ export declare function tursoFts(): Promise<TursoFtsBuilders>;
@@ -0,0 +1,14 @@
1
+ import { selectBuilders } from '../engine.js';
2
+ import { tursoNative } from './native.js';
3
+ export const TURSO_FTS_BUILDERS = {
4
+ indexDdl: ()=>[
5
+ `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,
6
+ `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`
7
+ ],
8
+ score: (columns, literal)=>`fts_score(${columns}, '${literal}')`,
9
+ match: (columns, literal)=>`fts_match(${columns}, '${literal}')`
10
+ };
11
+ export const TURSO_PATCHES = [];
12
+ export async function tursoFts() {
13
+ return selectBuilders(TURSO_FTS_BUILDERS, TURSO_PATCHES, (await tursoNative()).version);
14
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/engine.ts"],"sourcesContent":["import { type EnginePatch, selectBuilders } from '../engine.ts';\nimport { tursoNative } from './native.ts';\n\n// Every statement this store sends to Turso's native FTS; a version that behaves differently gets a TURSO_PATCHES entry.\nexport interface TursoFtsBuilders {\n // The two FTS indexes. The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n // columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n indexDdl(): readonly string[];\n // `literal` is already escaped: the query argument must be a SQL literal, since bound as `?` the row set stays correct\n // but fts_score silently returns 0. fts_score is real only as a SELECT's bare projected expression for the index\n // that SELECT's fts_match chose; every other form silently returns 0.\n score(columns: string, literal: string): string;\n match(columns: string, literal: string): string;\n}\n\nexport const TURSO_FTS_BUILDERS: TursoFtsBuilders = {\n indexDdl: () => [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n ],\n score: (columns, literal) => `fts_score(${columns}, '${literal}')`,\n match: (columns, literal) => `fts_match(${columns}, '${literal}')`,\n};\n\nexport const TURSO_PATCHES: readonly EnginePatch<TursoFtsBuilders>[] = [];\n\nexport async function tursoFts(): Promise<TursoFtsBuilders> {\n return selectBuilders(TURSO_FTS_BUILDERS, TURSO_PATCHES, (await tursoNative()).version);\n}\n"],"names":["selectBuilders","tursoNative","TURSO_FTS_BUILDERS","indexDdl","score","columns","literal","match","TURSO_PATCHES","tursoFts","version"],"mappings":"AAAA,SAA2BA,cAAc,QAAQ,eAAe;AAChE,SAASC,WAAW,QAAQ,cAAc;AAc1C,OAAO,MAAMC,qBAAuC;IAClDC,UAAU,IAAM;YACd,CAAC,mKAAmK,CAAC;YACrK,CAAC,gMAAgM,CAAC;SACnM;IACDC,OAAO,CAACC,SAASC,UAAY,CAAC,UAAU,EAAED,QAAQ,GAAG,EAAEC,QAAQ,EAAE,CAAC;IAClEC,OAAO,CAACF,SAASC,UAAY,CAAC,UAAU,EAAED,QAAQ,GAAG,EAAEC,QAAQ,EAAE,CAAC;AACpE,EAAE;AAEF,OAAO,MAAME,gBAA0D,EAAE,CAAC;AAE1E,OAAO,eAAeC;IACpB,OAAOT,eAAeE,oBAAoBM,eAAe,AAAC,CAAA,MAAMP,aAAY,EAAGS,OAAO;AACxF"}
@@ -2,6 +2,7 @@
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
4
  import { matchesSearchPhrase, searchTokens } from '../../text/segment.js';
5
+ import { tursoFts } from './engine.js';
5
6
  import { stemFolded } from './lexical-text.js';
6
7
  const FIELDS = [
7
8
  'title_stem',
@@ -89,8 +90,7 @@ function splitTerms(terms) {
89
90
  emptyPhrase
90
91
  };
91
92
  }
92
- // fts_match/fts_score's query argument must be a SQL literal: bound as `?`, the row set stays
93
- // correct but fts_score silently returns 0 for every row (PRINCIPLES: no-silent-modes).
93
+ // engine.ts's score/match take the query as a SQL literal (PRINCIPLES: no-silent-modes).
94
94
  function escapeFtsLiteral(query) {
95
95
  return query.replace(/'/g, "''");
96
96
  }
@@ -114,13 +114,12 @@ function buildQueries(words, phrases, unspaced) {
114
114
  ngramQuery: unspacedParts.join(' AND ')
115
115
  };
116
116
  }
117
- // fts_score() is real only as a SELECT's bare projected expression, and only for the index that
118
- // SELECT's fts_match() chose; every other form silently returns 0. Hence one SELECT per index.
119
- function ftsBranch(cols, query, opts) {
117
+ // One SELECT per index: fts_score is real only for the index its own SELECT's fts_match chose (engine.ts).
118
+ function ftsBranch(fts, cols, query, opts) {
120
119
  const colList = cols.map((c)=>`content.${c}`).join(', ');
121
120
  const lit = escapeFtsLiteral(query);
122
- return `SELECT content."path" AS path, fts_score(${colList}, '${lit}') AS score
123
- FROM content ${opts.whereJoin} WHERE fts_match(${colList}, '${lit}') ${opts.whereCond} ${opts.scopeCond}`;
121
+ return `SELECT content."path" AS path, ${fts.score(colList, lit)} AS score
122
+ FROM content ${opts.whereJoin} WHERE ${fts.match(colList, lit)} ${opts.whereCond} ${opts.scopeCond}`;
124
123
  }
125
124
  // Ranked query, scoped by the caller-built SQL fragments (same shape sqlite's/duckdb's
126
125
  // 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).
@@ -133,9 +132,10 @@ export async function queryLexical(conn, terms, opts) {
133
132
  if (emptyPhrase) return [];
134
133
  const { defaultQuery, ngramQuery } = buildQueries(words, phrases, unspaced);
135
134
  if (defaultQuery === '' && ngramQuery === '') return [];
135
+ const fts = await tursoFts();
136
136
  const branches = [];
137
- if (defaultQuery !== '') branches.push(ftsBranch(FIELDS, defaultQuery, opts));
138
- if (ngramQuery !== '') branches.push(ftsBranch(NGRAM_FIELDS, ngramQuery, opts));
137
+ if (defaultQuery !== '') branches.push(ftsBranch(fts, FIELDS, defaultQuery, opts));
138
+ if (ngramQuery !== '') branches.push(ftsBranch(fts, NGRAM_FIELDS, ngramQuery, opts));
139
139
  // HAVING COUNT(*) = branches.length is the AND across indexes; with one branch it is a no-op,
140
140
  // `path` being content's primary key. fts_score is higher-is-better BM25, so DESC. Phrase
141
141
  // candidates carry authored fields through the join below so the shared verifier can reject
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/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 { label: 'grouping operator', re: /[()]/ },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, 'phrase');\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":"AAAA,4FAA4F;AAC5F,uGAAuG;AACvG,SAASA,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,mBAAmB,EAAoBC,YAAY,QAAQ,wBAAwB;AAE5F,SAASC,UAAU,QAAQ,oBAAoB;AAE/C,MAAMC,SAAS;IAAC;IAAc;IAAgB;CAAY;AAC1D,MAAMC,eAAe;IAAC;IAAe;IAAiB;CAAa;AACnE,MAAMC,kBAAkB;IAAC;IAAS;IAAW;CAAO;AAGpD,uFAAuF;AACvF,+FAA+F;AAC/F,MAAMC,iBAAuD;IAC3D;QAAEC,OAAO;QAAgBC,IAAI;IAA4B;IACzD;QAAED,OAAO;QAAoBC,IAAI;IAAiC;IAClE;QAAED,OAAO;QAAiBC,IAAI;IAAiB;IAC/C;QAAED,OAAO;QAAgCC,IAAI;IAAW;IACxD;QAAED,OAAO;QAAiBC,IAAI;IAA2B;IACzD;QAAED,OAAO;QAAqBC,IAAI;IAAO;CAC1C;AAED,SAASC,oBAAoBC,KAAa;IACxC,MAAMC,iBAAiBD,MAAME,OAAO,CAAC,YAAY;IACjD,KAAK,MAAM,EAAEL,KAAK,EAAEC,EAAE,EAAE,IAAIF,eAAgB;QAC1C,MAAMO,IAAIF,eAAeG,KAAK,CAACN;QAC/B,IAAIK,GAAG,OAAO;YAAEN;YAAOQ,OAAOF,CAAC,CAAC,EAAE,CAACG,IAAI;QAAG;IAC5C;IACA,OAAO;AACT;AAEA,gGAAgG;AAChG,sFAAsF;AACtF,SAASC,WAAWP,KAAa;IAC/B,MAAMQ,QAAkB,EAAE;IAC1B,MAAMC,UAA2B,EAAE;IACnC,MAAMC,WAAqB,EAAE;IAC7B,IAAIC,cAAc;IAClB,MAAMV,iBAAiBD,MAAME,OAAO,CAAC,cAAc,CAACU,IAAIC;QACtD,MAAMC,SAASD,MAAMP,IAAI;QACzB,MAAMS,SAASxB,aAAauB;QAC5B,IAAIC,OAAOC,MAAM,KAAK,GAAG;YACvBL,cAAc;YACd,OAAO;QACT;QACAD,SAASO,IAAI,IAAIF,OAAOG,MAAM,CAAC,CAACb,QAAUA,MAAMK,QAAQ,EAAES,GAAG,CAAC,CAACd,QAAUA,MAAMe,IAAI;QACnFX,QAAQQ,IAAI,CAACF;QACb,OAAO;IACT;IACA,KAAK,MAAMM,OAAOpB,eAAeqB,KAAK,CAAC,OAAQ;QAC7C,IAAID,IAAIL,MAAM,KAAK,GAAG;QACtB,KAAK,MAAMX,SAASd,aAAa8B,KAAM;YACrC,IAAIhB,MAAMK,QAAQ,EAAEA,SAASO,IAAI,CAACZ,MAAMe,IAAI;iBACvCZ,MAAMS,IAAI,CAACZ,MAAMe,IAAI;QAC5B;IACF;IACA,OAAO;QAAEZ;QAAOC;QAASC;QAAUC;IAAY;AACjD;AAEA,8FAA8F;AAC9F,wFAAwF;AACxF,SAASY,iBAAiBC,KAAa;IACrC,OAAOA,MAAMtB,OAAO,CAAC,MAAM;AAC7B;AAEA,6FAA6F;AAC7F,kGAAkG;AAClG,SAASuB,aAAajB,KAAe,EAAEC,OAAwB,EAAEC,QAAkB;IACjF,MAAMgB,cAAcjB,QAAQkB,OAAO,CAAC,CAACb;QACnC,MAAMc,QAAQd,OAAOI,MAAM,CAAC,CAACb,QAAU,CAACA,MAAMK,QAAQ,EAAES,GAAG,CAAC,CAACd,QAAUb,WAAWa,MAAMe,IAAI;QAC5F,IAAIQ,MAAMZ,MAAM,KAAK,GAAG,OAAO,EAAE;QACjC,OAAOF,OAAOe,IAAI,CAAC,CAACxB,QAAUA,MAAMK,QAAQ,IAAIkB,QAAQ;YAACA,MAAME,IAAI,CAAC;SAAK;IAC3E;IACA,MAAMC,eAAe;WAAIvB,MAAMW,GAAG,CAAC3B;WAAgBkC;KAAY,CAACP,GAAG,CAAC,CAACa,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC;IACnF,MAAMC,gBAAgBvB,SAASS,GAAG,CAAC,CAACa,IAAM,CAAC,CAAC,EAAEA,EAAE,CAAC,CAAC;IAClD,OAAO;QAAEE,cAAcH,aAAaD,IAAI,CAAC;QAAUK,YAAYF,cAAcH,IAAI,CAAC;IAAS;AAC7F;AAEA,gGAAgG;AAChG,+FAA+F;AAC/F,SAASM,UAAUC,IAAuB,EAAEb,KAAa,EAAEc,IAAyB;IAClF,MAAMC,UAAUF,KAAKlB,GAAG,CAAC,CAACqB,IAAM,CAAC,QAAQ,EAAEA,GAAG,EAAEV,IAAI,CAAC;IACrD,MAAMW,MAAMlB,iBAAiBC;IAC7B,OAAO,CAAC,yCAAyC,EAAEe,QAAQ,GAAG,EAAEE,IAAI;iBACrD,EAAEH,KAAKI,SAAS,CAAC,iBAAiB,EAAEH,QAAQ,GAAG,EAAEE,IAAI,GAAG,EAAEH,KAAKK,SAAS,CAAC,CAAC,EAAEL,KAAKM,SAAS,EAAE;AAC7G;AAEA,uFAAuF;AACvF,qMAAqM;AACrM,OAAO,eAAeC,aAAaC,IAAgB,EAAE9C,KAAa,EAAEsC,IAAyB;IAC3F,MAAMS,cAAchD,oBAAoBC;IACxC,IAAI+C,gBAAgB,MAAM;QACxB,MAAM,IAAI1D,WAAW,4BAA4B,CAAC,wCAAwC,EAAE0D,YAAYlD,KAAK,CAAC,GAAG,EAAEkD,YAAY1C,KAAK,CAAC,4BAA4B,EAAEL,MAAMM,IAAI,GAAG,sFAAsF,CAAC;IACzQ;IACA,MAAM,EAAEE,KAAK,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,WAAW,EAAE,GAAGJ,WAAWP;IAC7D,IAAIW,aAAa,OAAO,EAAE;IAC1B,MAAM,EAAEuB,YAAY,EAAEC,UAAU,EAAE,GAAGV,aAAajB,OAAOC,SAASC;IAClE,IAAIwB,iBAAiB,MAAMC,eAAe,IAAI,OAAO,EAAE;IAEvD,MAAMa,WAAqB,EAAE;IAC7B,IAAId,iBAAiB,IAAIc,SAAS/B,IAAI,CAACmB,UAAU3C,QAAQyC,cAAcI;IACvE,IAAIH,eAAe,IAAIa,SAAS/B,IAAI,CAACmB,UAAU1C,cAAcyC,YAAYG;IAEzE,8FAA8F;IAC9F,0FAA0F;IAC1F,4FAA4F;IAC5F,uFAAuF;IACvF,MAAMW,SAAS,CAAC;MACZ,EAAED,SAASlB,IAAI,CAAC,eAAe;4CACO,EAAEkB,SAAShC,MAAM,EAAE;IAC7D,MAAMkC,eAAezC,QAAQO,MAAM,GAAG,IAAI,mDAAmD;IAC7F,MAAMmC,aAAa1C,QAAQO,MAAM,GAAG,IAAI,4CAA4C;IACpF,MAAMoC,MAAM,CAAC,wBAAwB,EAAEF,aAAa;IAClD,EAAED,OAAO;MACP,EAAEE,WAAW,gCAAgC,EAAE1C,QAAQO,MAAM,GAAG,IAAI,KAAK,YAAY;IACzF,MAAMqC,OAAO,MAAMP,KAAKQ,OAAO,CAACF;IAChC,MAAMG,OAAQ,MAAMF,KAAKG,GAAG,IAAK/C,QAAQO,MAAM,GAAG,IAAI,EAAE,GAAG;QAACsB,KAAKmB,KAAK;KAAC;IACvE,+FAA+F;IAC/F,yFAAyF;IACzF,IAAIF,KAAK1B,IAAI,CAAC,CAAC6B,IAAMA,EAAEC,IAAI,KAAK,OAAO;QACrC,MAAM,IAAItE,WAAW,qBAAqB;IAC5C;IACA,MAAMuE,UAAUnD,QAAQO,MAAM,KAAK,IAAIuC,OAAOA,KAAKrC,MAAM,CAAC,CAAC2C,MAAQpD,QAAQqD,KAAK,CAAC,CAAChD,SAAWnB,gBAAgBkC,IAAI,CAAC,CAACkC;oBAA8BF;uBAApBvE,qBAAoBuE,aAAAA,GAAG,CAACE,MAAM,cAAVF,wBAAAA,aAAc,IAAI/C;;IACnK,OAAO8C,QAAQI,KAAK,CAAC,GAAG1B,KAAKmB,KAAK,EAAEtC,GAAG,CAAC,CAAC,EAAEwC,IAAI,EAAE,GAAM,CAAA;YAAEA;QAAK,CAAA;AAChE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/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 { type TursoFtsBuilders, tursoFts } from './engine.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 { label: 'grouping operator', re: /[()]/ },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, 'phrase');\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// engine.ts's score/match take the query as a SQL literal (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// One SELECT per index: fts_score is real only for the index its own SELECT's fts_match chose (engine.ts).\nfunction ftsBranch(fts: TursoFtsBuilders, 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 fts = await tursoFts();\n const branches: string[] = [];\n if (defaultQuery !== '') branches.push(ftsBranch(fts, FIELDS, defaultQuery, opts));\n if (ngramQuery !== '') branches.push(ftsBranch(fts, 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","tursoFts","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","fts","cols","opts","colList","c","lit","score","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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"}
@@ -1,6 +1,10 @@
1
+ import { type NativeModule } from '../native.js';
1
2
  export declare const TURSO_PACKAGE = "@tursodatabase/database";
2
3
  export declare const CONNECT_OPTS: {
3
4
  readonly timeout: 30000;
4
5
  readonly experimental: readonly ["index_method"];
5
6
  };
6
- export declare function tursoApi(): Promise<typeof import('@tursodatabase/database')>;
7
+ type TursoApi = typeof import('@tursodatabase/database');
8
+ export declare function tursoNative(): Promise<NativeModule<TursoApi>>;
9
+ export declare function tursoApi(): Promise<TursoApi>;
10
+ export {};
@@ -1,10 +1,5 @@
1
- import { loadOrInstall, packageNodeModules } from '../native.js';
1
+ import { loadOrInstall, packageNodeModules, peerRange } from '../native.js';
2
2
  export const TURSO_PACKAGE = '@tursodatabase/database';
3
- const TURSO = {
4
- store: 'turso',
5
- pkg: TURSO_PACKAGE,
6
- sizeHint: '~16MB'
7
- };
8
3
  // Every connect() in this store goes through these: the same 30s floor sqlite opens with, and the
9
4
  // FTS index method ensureSchema() needs. reclaimSpace adds 'vacuum'; nothing else may (PLAN 3.54).
10
5
  export const CONNECT_OPTS = {
@@ -13,10 +8,21 @@ export const CONNECT_OPTS = {
13
8
  'index_method'
14
9
  ]
15
10
  };
16
- let tursoApiPromise;
11
+ let tursoNativePromise;
17
12
  // One accessor every consumer must go through, not each its own bare-specifier import: Node
18
13
  // keeps returning a pre-install "not found" miss to any new lookup of the same path afterward.
19
- export function tursoApi() {
20
- if (!tursoApiPromise) tursoApiPromise = loadOrInstall(TURSO, packageNodeModules());
21
- return tursoApiPromise;
14
+ export function tursoNative() {
15
+ if (!tursoNativePromise) {
16
+ const descriptor = {
17
+ store: 'turso',
18
+ pkg: TURSO_PACKAGE,
19
+ sizeHint: '~16MB',
20
+ range: peerRange(TURSO_PACKAGE)
21
+ };
22
+ tursoNativePromise = loadOrInstall(descriptor, packageNodeModules());
23
+ }
24
+ return tursoNativePromise;
25
+ }
26
+ export async function tursoApi() {
27
+ return (await tursoNative()).api;
22
28
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/native.ts"],"sourcesContent":["import { loadOrInstall, type NativeDescriptor, packageNodeModules } from '../native.ts';\n\nexport const TURSO_PACKAGE = '@tursodatabase/database';\n\nconst TURSO: NativeDescriptor = { store: 'turso', pkg: TURSO_PACKAGE, sizeHint: '~16MB' };\n\n// Every connect() in this store goes through these: the same 30s floor sqlite opens with, and the\n// FTS index method ensureSchema() needs. reclaimSpace adds 'vacuum'; nothing else may (PLAN 3.54).\nexport const CONNECT_OPTS = { timeout: 30_000, experimental: ['index_method'] } as const;\n\nlet tursoApiPromise: Promise<typeof import('@tursodatabase/database')> | undefined;\n\n// One accessor every consumer must go through, not each its own bare-specifier import: Node\n// keeps returning a pre-install \"not found\" miss to any new lookup of the same path afterward.\nexport function tursoApi(): Promise<typeof import('@tursodatabase/database')> {\n if (!tursoApiPromise) tursoApiPromise = loadOrInstall<typeof import('@tursodatabase/database')>(TURSO, packageNodeModules());\n return tursoApiPromise;\n}\n"],"names":["loadOrInstall","packageNodeModules","TURSO_PACKAGE","TURSO","store","pkg","sizeHint","CONNECT_OPTS","timeout","experimental","tursoApiPromise","tursoApi"],"mappings":"AAAA,SAASA,aAAa,EAAyBC,kBAAkB,QAAQ,eAAe;AAExF,OAAO,MAAMC,gBAAgB,0BAA0B;AAEvD,MAAMC,QAA0B;IAAEC,OAAO;IAASC,KAAKH;IAAeI,UAAU;AAAQ;AAExF,kGAAkG;AAClG,mGAAmG;AACnG,OAAO,MAAMC,eAAe;IAAEC,SAAS;IAAQC,cAAc;QAAC;KAAe;AAAC,EAAW;AAEzF,IAAIC;AAEJ,4FAA4F;AAC5F,+FAA+F;AAC/F,OAAO,SAASC;IACd,IAAI,CAACD,iBAAiBA,kBAAkBV,cAAwDG,OAAOF;IACvG,OAAOS;AACT"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/native.ts"],"sourcesContent":["import { loadOrInstall, type NativeDescriptor, type NativeModule, packageNodeModules, peerRange } from '../native.ts';\n\nexport const TURSO_PACKAGE = '@tursodatabase/database';\n\n// Every connect() in this store goes through these: the same 30s floor sqlite opens with, and the\n// FTS index method ensureSchema() needs. reclaimSpace adds 'vacuum'; nothing else may (PLAN 3.54).\nexport const CONNECT_OPTS = { timeout: 30_000, experimental: ['index_method'] } as const;\n\ntype TursoApi = typeof import('@tursodatabase/database');\n\nlet tursoNativePromise: Promise<NativeModule<TursoApi>> | undefined;\n\n// One accessor every consumer must go through, not each its own bare-specifier import: Node\n// keeps returning a pre-install \"not found\" miss to any new lookup of the same path afterward.\nexport function tursoNative(): Promise<NativeModule<TursoApi>> {\n if (!tursoNativePromise) {\n const descriptor: NativeDescriptor = { store: 'turso', pkg: TURSO_PACKAGE, sizeHint: '~16MB', range: peerRange(TURSO_PACKAGE) };\n tursoNativePromise = loadOrInstall<TursoApi>(descriptor, packageNodeModules());\n }\n return tursoNativePromise;\n}\n\nexport async function tursoApi(): Promise<TursoApi> {\n return (await tursoNative()).api;\n}\n"],"names":["loadOrInstall","packageNodeModules","peerRange","TURSO_PACKAGE","CONNECT_OPTS","timeout","experimental","tursoNativePromise","tursoNative","descriptor","store","pkg","sizeHint","range","tursoApi","api"],"mappings":"AAAA,SAASA,aAAa,EAA4CC,kBAAkB,EAAEC,SAAS,QAAQ,eAAe;AAEtH,OAAO,MAAMC,gBAAgB,0BAA0B;AAEvD,kGAAkG;AAClG,mGAAmG;AACnG,OAAO,MAAMC,eAAe;IAAEC,SAAS;IAAQC,cAAc;QAAC;KAAe;AAAC,EAAW;AAIzF,IAAIC;AAEJ,4FAA4F;AAC5F,+FAA+F;AAC/F,OAAO,SAASC;IACd,IAAI,CAACD,oBAAoB;QACvB,MAAME,aAA+B;YAAEC,OAAO;YAASC,KAAKR;YAAeS,UAAU;YAASC,OAAOX,UAAUC;QAAe;QAC9HI,qBAAqBP,cAAwBS,YAAYR;IAC3D;IACA,OAAOM;AACT;AAEA,OAAO,eAAeO;IACpB,OAAO,AAAC,CAAA,MAAMN,aAAY,EAAGO,GAAG;AAClC"}
@@ -1,11 +1,15 @@
1
1
  import { STORE_DIMS } from '../../embed/types.js';
2
2
  import { SenseError } from '../../errors.js';
3
3
  import { activeFeatures } from '../../features/index.js';
4
+ import { ENGINE_CHECK_DOCS, engineIdentity, npmRemedy, checkEngine as runEngineCheck } from '../engine.js';
5
+ import { peerRange } from '../native.js';
4
6
  import { openWithDialect } from '../open.js';
5
7
  import { getMeta, setMeta } from '../shared.js';
6
8
  import { checkpointWal, createConnection } from './connection.js';
7
- import { CONNECT_OPTS, TURSO_PACKAGE, tursoApi } from './native.js';
8
- import { CONTENT_FTS_DDL, tursoDialect } from './reconcile.js';
9
+ import { tursoFts } from './engine.js';
10
+ import { queryLexical } from './lexical.js';
11
+ import { CONNECT_OPTS, TURSO_PACKAGE, tursoApi, tursoNative } from './native.js';
12
+ import { reconcileTursoContentWithStrategy, tursoDialect } from './reconcile.js';
9
13
  import { createStore } from './store.js';
10
14
  export const DB_FILENAME = 'cache.turso.db';
11
15
  // Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves
@@ -17,7 +21,7 @@ async function ensureCoreSchema(_handle, conn) {
17
21
  await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_ctime" REAL, "_size" INTEGER, "_parse_error" TEXT)`);
18
22
  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)`);
19
23
  await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources ("path" TEXT PRIMARY KEY, text TEXT NOT NULL)`);
20
- for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
24
+ for (const ddl of (await tursoFts()).indexDdl())await conn.exec(ddl);
21
25
  await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
22
26
  await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
23
27
  if (await getMeta(conn, 'schema_version') === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);
@@ -71,6 +75,41 @@ async function connect(dbPath, _cfg, options) {
71
75
  conn: createConnection(db)
72
76
  };
73
77
  }
78
+ async function tursoEngineIdentity() {
79
+ return engineIdentity(TURSO_PACKAGE, (await tursoNative()).version);
80
+ }
81
+ // The real schema, content write and lexical query over store/engine.ts's fixture, in memory.
82
+ async function checkTursoEngine() {
83
+ const { api, version } = await tursoNative();
84
+ const db = await api.connect(':memory:', {
85
+ ...CONNECT_OPTS,
86
+ experimental: [
87
+ ...CONNECT_OPTS.experimental
88
+ ]
89
+ });
90
+ try {
91
+ const conn = createConnection(db);
92
+ await conn.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)');
93
+ await ensureCoreSchema(db, conn);
94
+ const docs = ENGINE_CHECK_DOCS.map((doc)=>({
95
+ relPath: doc.path,
96
+ search: {
97
+ title: doc.title,
98
+ summary: doc.summary,
99
+ text: doc.text
100
+ }
101
+ }));
102
+ await reconcileTursoContentWithStrategy(conn, [], docs, 'incremental');
103
+ await runEngineCheck('turso', engineIdentity(TURSO_PACKAGE, version), npmRemedy(TURSO_PACKAGE, peerRange(TURSO_PACKAGE)), async (terms)=>(await queryLexical(conn, terms, {
104
+ whereJoin: '',
105
+ whereCond: '',
106
+ scopeCond: '',
107
+ limit: 10
108
+ })).map((hit)=>hit.path));
109
+ } finally{
110
+ await db.close();
111
+ }
112
+ }
74
113
  // This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.
75
114
  export const tursoOpenDialect = {
76
115
  filename: DB_FILENAME,
@@ -82,6 +121,8 @@ export const tursoOpenDialect = {
82
121
  // process", Windows "another process has locked a portion of the file (os error 33)". Distinct from
83
122
  // the write-time "database is locked" its connect-time `timeout` covers; that one never reaches here.
84
123
  isLocked: (err)=>/File is locked by another process|locked a portion of the file/.test(err.message),
124
+ engineIdentity: tursoEngineIdentity,
125
+ checkEngine: checkTursoEngine,
85
126
  ensureCoreSchema,
86
127
  ensureFeatureSchema,
87
128
  setDerivedBusyTimeout,
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { checkpointWal, createConnection } from './connection.ts';\nimport { CONNECT_OPTS, TURSO_PACKAGE, tursoApi } from './native.ts';\nimport { CONTENT_FTS_DDL, tursoDialect } from './reconcile.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.turso.db';\n// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves\n// separately. Covers the FTS indexes, the \"_ngram\" sidecar columns, and embeddings.vector's width.\nexport const SCHEMA_VERSION = '10';\n\nexport type { OpenResult };\n\n// This store's Handle (types.ts's OpenDialect<Handle>) is the connected Database itself: no\n// extra native state to thread, unlike duckdb's separate instance/connection pair.\nasync function ensureCoreSchema(_handle: Database, conn: Connection): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n 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)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n}\n\nasync function ensureFeatureSchema(_handle: Database, conn: Connection, cfg: Config): Promise<void> {\n for (const feature of activeFeatures(cfg)) {\n // Native F32_BLOB(STORE_DIMS) instead of the embed feature's engine-neutral BLOB DDL. `scale`\n // is kept unused, so the shared reconcile-time INSERT/DELETE names a column both stores have.\n if (feature.name === 'embed') {\n await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, content_identity TEXT NOT NULL, scale REAL, vector F32_BLOB(${STORE_DIMS}), PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n}\n\nasync function close(handle: Database, options: { observational?: boolean } = {}): Promise<void> {\n if (!options.observational) await checkpointWal(handle);\n await handle.close();\n}\n\nasync function setDerivedBusyTimeout(_handle: Database, conn: Connection, ms: number): Promise<void> {\n await conn.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: Database; conn: Connection }> {\n // Dynamic, not a top-level import: a sqlite or duckdb tree must never attempt to resolve this\n // optional dependency until a turso tree is actually opened. Installed on first use if missing.\n let turso: Awaited<ReturnType<typeof tursoApi>>;\n try {\n turso = await tursoApi();\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" needs the ${TURSO_PACKAGE} package (${(err as Error).message})`);\n }\n\n let db: Database;\n try {\n // Turso's readonly mode rejects native FTS SELECTs and TEMP query tables. Existing-only opens\n // stay query-capable; shared no-build orchestration and observational close prevent upkeep writes.\n db = await turso.connect(dbPath, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental], ...(options.existingOnly ? { fileMustExist: true } : {}) });\n } catch (err) {\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n return { handle: db, conn: createConnection(db) };\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const tursoOpenDialect: OpenDialect<Database> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: tursoDialect,\n connect,\n close,\n // \"Locking error: Failed locking file ...\", worded per platform: posix \"File is locked by another\n // process\", Windows \"another process has locked a portion of the file (os error 33)\". Distinct from\n // the write-time \"database is locked\" its connect-time `timeout` covers; that one never reaches here.\n isLocked: (err) => /File is locked by another process|locked a portion of the file/.test(err.message),\n ensureCoreSchema,\n ensureFeatureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn, _cfg, options) => createStore(handle, conn, options),\n};\n\nexport async function openTurso(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, tursoOpenDialect, options);\n}\n"],"names":["STORE_DIMS","SenseError","activeFeatures","openWithDialect","getMeta","setMeta","checkpointWal","createConnection","CONNECT_OPTS","TURSO_PACKAGE","tursoApi","CONTENT_FTS_DDL","tursoDialect","createStore","DB_FILENAME","SCHEMA_VERSION","ensureCoreSchema","_handle","conn","exec","ddl","ensureFeatureSchema","cfg","feature","name","schema","close","handle","options","observational","setDerivedBusyTimeout","ms","connect","dbPath","_cfg","turso","err","message","db","experimental","existingOnly","fileMustExist","tursoOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","openTurso"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures } from '../../features/index.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { ENGINE_CHECK_DOCS, engineIdentity, npmRemedy, checkEngine as runEngineCheck } from '../engine.ts';\nimport { peerRange } from '../native.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { checkpointWal, createConnection } from './connection.ts';\nimport { tursoFts } from './engine.ts';\nimport { queryLexical } from './lexical.ts';\nimport { CONNECT_OPTS, TURSO_PACKAGE, tursoApi, tursoNative } from './native.ts';\nimport { reconcileTursoContentWithStrategy, tursoDialect } from './reconcile.ts';\nimport { createStore } from './store.ts';\n\nexport const DB_FILENAME = 'cache.turso.db';\n// Independent of sqlite's and duckdb's SCHEMA_VERSION: each store's cache shape evolves\n// separately. Covers the FTS indexes, the \"_ngram\" sidecar columns, and embeddings.vector's width.\nexport const SCHEMA_VERSION = '10';\n\nexport type { OpenResult };\n\n// This store's Handle (types.ts's OpenDialect<Handle>) is the connected Database itself: no\n// extra native state to thread, unlike duckdb's separate instance/connection pair.\nasync function ensureCoreSchema(_handle: Database, conn: Connection): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_ctime\" REAL, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n 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)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n for (const ddl of (await tursoFts()).indexDdl()) await conn.exec(ddl);\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n}\n\nasync function ensureFeatureSchema(_handle: Database, conn: Connection, cfg: Config): Promise<void> {\n for (const feature of activeFeatures(cfg)) {\n // Native F32_BLOB(STORE_DIMS) instead of the embed feature's engine-neutral BLOB DDL. `scale`\n // is kept unused, so the shared reconcile-time INSERT/DELETE names a column both stores have.\n if (feature.name === 'embed') {\n await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, content_identity TEXT NOT NULL, scale REAL, vector F32_BLOB(${STORE_DIMS}), PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n}\n\nasync function close(handle: Database, options: { observational?: boolean } = {}): Promise<void> {\n if (!options.observational) await checkpointWal(handle);\n await handle.close();\n}\n\nasync function setDerivedBusyTimeout(_handle: Database, conn: Connection, ms: number): Promise<void> {\n await conn.exec(`PRAGMA busy_timeout = ${ms}`);\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: Database; conn: Connection }> {\n // Dynamic, not a top-level import: a sqlite or duckdb tree must never attempt to resolve this\n // optional dependency until a turso tree is actually opened. Installed on first use if missing.\n let turso: Awaited<ReturnType<typeof tursoApi>>;\n try {\n turso = await tursoApi();\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" needs the ${TURSO_PACKAGE} package (${(err as Error).message})`);\n }\n\n let db: Database;\n try {\n // Turso's readonly mode rejects native FTS SELECTs and TEMP query tables. Existing-only opens\n // stay query-capable; shared no-build orchestration and observational close prevent upkeep writes.\n db = await turso.connect(dbPath, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental], ...(options.existingOnly ? { fileMustExist: true } : {}) });\n } catch (err) {\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"turso\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n return { handle: db, conn: createConnection(db) };\n}\n\nasync function tursoEngineIdentity(): Promise<string> {\n return engineIdentity(TURSO_PACKAGE, (await tursoNative()).version);\n}\n\n// The real schema, content write and lexical query over store/engine.ts's fixture, in memory.\nasync function checkTursoEngine(): Promise<void> {\n const { api, version } = await tursoNative();\n const db = await api.connect(':memory:', { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental] });\n try {\n const conn = createConnection(db);\n await conn.exec('CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)');\n await ensureCoreSchema(db, conn);\n const docs = ENGINE_CHECK_DOCS.map((doc) => ({ relPath: doc.path, search: { title: doc.title, summary: doc.summary, text: doc.text } }) as ParsedDoc);\n await reconcileTursoContentWithStrategy(conn, [], docs, 'incremental');\n await runEngineCheck('turso', engineIdentity(TURSO_PACKAGE, version), npmRemedy(TURSO_PACKAGE, peerRange(TURSO_PACKAGE)), async (terms) => (await queryLexical(conn, terms, { whereJoin: '', whereCond: '', scopeCond: '', limit: 10 })).map((hit) => hit.path));\n } finally {\n await db.close();\n }\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const tursoOpenDialect: OpenDialect<Database> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: tursoDialect,\n connect,\n close,\n // \"Locking error: Failed locking file ...\", worded per platform: posix \"File is locked by another\n // process\", Windows \"another process has locked a portion of the file (os error 33)\". Distinct from\n // the write-time \"database is locked\" its connect-time `timeout` covers; that one never reaches here.\n isLocked: (err) => /File is locked by another process|locked a portion of the file/.test(err.message),\n engineIdentity: tursoEngineIdentity,\n checkEngine: checkTursoEngine,\n ensureCoreSchema,\n ensureFeatureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn, _cfg, options) => createStore(handle, conn, options),\n};\n\nexport async function openTurso(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, tursoOpenDialect, options);\n}\n"],"names":["STORE_DIMS","SenseError","activeFeatures","ENGINE_CHECK_DOCS","engineIdentity","npmRemedy","checkEngine","runEngineCheck","peerRange","openWithDialect","getMeta","setMeta","checkpointWal","createConnection","tursoFts","queryLexical","CONNECT_OPTS","TURSO_PACKAGE","tursoApi","tursoNative","reconcileTursoContentWithStrategy","tursoDialect","createStore","DB_FILENAME","SCHEMA_VERSION","ensureCoreSchema","_handle","conn","exec","ddl","indexDdl","ensureFeatureSchema","cfg","feature","name","schema","close","handle","options","observational","setDerivedBusyTimeout","ms","connect","dbPath","_cfg","turso","err","message","db","experimental","existingOnly","fileMustExist","tursoEngineIdentity","version","checkTursoEngine","api","docs","map","doc","relPath","path","search","title","summary","text","terms","whereJoin","whereCond","scopeCond","limit","hit","tursoOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","openTurso"],"mappings":"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"}
@@ -1,7 +1,6 @@
1
1
  import type { ReconcileDelta } from '../../features/types.js';
2
2
  import type { ParsedDoc } from '../../scan/index.js';
3
3
  import type { Connection, ReconcileDialect } from '../types.js';
4
- export declare const CONTENT_FTS_DDL: readonly ["CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')", "CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')"];
5
4
  export declare const CONTENT_FTS_NAMES: readonly ["content_fts", "content_fts_ngram"];
6
5
  export declare const FTS_REBUILD_THRESHOLD = 250;
7
6
  export type TursoFtsStrategy = 'incremental' | 'rebuild';
@@ -2,17 +2,12 @@ import { SenseError } from '../../errors.js';
2
2
  import { hasUnspacedRun } from '../../text/segment.js';
3
3
  import { quoteIdent, recordReconcileDuration } from '../shared.js';
4
4
  import { BEGIN_WRITE } from '../transaction.js';
5
+ import { tursoFts } from './engine.js';
5
6
  import { stemFolded } from './lexical-text.js';
6
7
  // This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in
7
8
  // store/reconcile.ts. content is a plain table with "_ngram" sidecars for lexical.ts.
8
- // The ngram index is scoped to disjoint "_ngram" sidecar columns: a second index over the same
9
- // columns makes a bare substring match a whole word, defeating the prefix-query rejection.
10
- // The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around
9
+ // The two FTS indexes (engine.ts's indexDdl), named here because reconcile drops and rebuilds them around
11
10
  // a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.
12
- export const CONTENT_FTS_DDL = [
13
- `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,
14
- `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`
15
- ];
16
11
  export const CONTENT_FTS_NAMES = [
17
12
  'content_fts',
18
13
  'content_fts_ngram'
@@ -65,7 +60,7 @@ export async function reconcileTursoContentWithStrategy(conn, touched, docs, str
65
60
  ]);
66
61
  }
67
62
  if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));
68
- if (bulk) for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
63
+ if (bulk) for (const ddl of (await tursoFts()).indexDdl())await conn.exec(ddl);
69
64
  }
70
65
  async function reconcileTursoContent(conn, touched, docs, delta, _cfg) {
71
66
  await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Retained policy boundary from 2026-08-30 corpus-scale measurements; the optimum after later\n// update-path changes remains pending matched corpus-scale crossover evidence.\nexport const FTS_REBUILD_THRESHOLD = 250;\n\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0) {\n const placeholders = touched.map(() => '?').join(', ');\n await conn.runBatch(`DELETE FROM content WHERE \"path\" IN (${placeholders})`, [touched]);\n }\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n if (bulk) for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n}\n\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","stemFolded","CONTENT_FTS_DDL","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","tursoFtsStrategy","delta","churn","reparsed","length","vanished","files","reconcileTursoContentWithStrategy","conn","touched","docs","strategy","bulk","name","exec","placeholders","map","join","runBatch","ddl","reconcileTursoContent","_cfg","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","reconcileContent","recordDuration"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/open-source/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { tursoFts } from './engine.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The two FTS indexes (engine.ts's indexDdl), named here because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Retained policy boundary from 2026-08-30 corpus-scale measurements; the optimum after later\n// update-path changes remains pending matched corpus-scale crossover evidence.\nexport const FTS_REBUILD_THRESHOLD = 250;\n\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0) {\n const placeholders = touched.map(() => '?').join(', ');\n await conn.runBatch(`DELETE FROM content WHERE \"path\" IN (${placeholders})`, [touched]);\n }\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n if (bulk) for (const ddl of (await tursoFts()).indexDdl()) await conn.exec(ddl);\n}\n\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","tursoFts","stemFolded","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","tursoFtsStrategy","delta","churn","reparsed","length","vanished","files","reconcileTursoContentWithStrategy","conn","touched","docs","strategy","bulk","name","exec","placeholders","map","join","runBatch","ddl","indexDdl","reconcileTursoContent","_cfg","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","reconcileContent","recordDuration"],"mappings":"AACA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,QAAQ,QAAQ,cAAc;AACvC,SAASC,UAAU,QAAQ,oBAAoB;AAE/C,qFAAqF;AACrF,sFAAsF;AAEtF,0GAA0G;AAC1G,uGAAuG;AACvG,OAAO,MAAMC,oBAAoB;IAAC;IAAe;CAAoB,CAAU;AAE/E,6FAA6F;AAC7F,gGAAgG;AAChG,SAASC,aAAaC,IAAY;IAChC,OAAOR,eAAeQ,QAAQA,OAAO;AACvC;AAEA,8FAA8F;AAC9F,+GAA+G;AAC/G,MAAMC,0BAA0B;AAEhC,8FAA8F;AAC9F,+EAA+E;AAC/E,OAAO,MAAMC,wBAAwB,IAAI;AAIzC,yFAAyF;AACzF,MAAMC,qBAAqB,CAAC,qKAAqK,CAAC;AAElM,SAASC,WAAWC,GAAc;IAChC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEP,IAAI,EAAE,GAAGK,IAAIG,MAAM;IAC3C,OAAO;QAACH,IAAII,OAAO;QAAEH;QAAOC;QAASP;QAAMH,WAAWS;QAAQT,WAAWU;QAAUV,WAAWG;QAAOD,aAAaO;QAAQP,aAAaQ;QAAUR,aAAaC;KAAM;AACtK;AAEA,OAAO,SAASU,iBAAiBC,KAAqB;IACpD,MAAMC,QAAQD,MAAME,QAAQ,CAACC,MAAM,GAAGH,MAAMI,QAAQ,CAACD,MAAM;IAC3D,OAAOH,MAAMK,KAAK,CAACF,MAAM,KAAK,KAAKF,QAAQV,wBAAwB,YAAY;AACjF;AAEA,kGAAkG;AAClG,kGAAkG;AAClG,OAAO,eAAee,kCAAkCC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,QAA0B;IACxI,6FAA6F;IAC7F,qDAAqD;IACrD,MAAMC,OAAOD,aAAa;IAC1B,IAAIC,MAAM,KAAK,MAAMC,QAAQzB,kBAAmB,MAAMoB,KAAKM,IAAI,CAAC,CAAC,qBAAqB,EAAED,MAAM;IAE9F,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAIJ,QAAQL,MAAM,GAAG,GAAG;QACtB,MAAMW,eAAeN,QAAQO,GAAG,CAAC,IAAM,KAAKC,IAAI,CAAC;QACjD,MAAMT,KAAKU,QAAQ,CAAC,CAAC,qCAAqC,EAAEH,aAAa,CAAC,CAAC,EAAE;YAACN;SAAQ;IACxF;IACA,IAAIC,KAAKN,MAAM,GAAG,GAAG,MAAMI,KAAKU,QAAQ,CAACzB,oBAAoBiB,KAAKM,GAAG,CAACtB;IACtE,IAAIkB,MAAM,KAAK,MAAMO,OAAO,AAAC,CAAA,MAAMjC,UAAS,EAAGkC,QAAQ,GAAI,MAAMZ,KAAKM,IAAI,CAACK;AAC7E;AAEA,eAAeE,sBAAsBb,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAET,KAAqB,EAAEqB,IAAY;IAC9H,MAAMf,kCAAkCC,MAAMC,SAASC,MAAMV,iBAAiBC;AAChF;AAEA,yFAAyF;AACzF,eAAesB,WAAWf,IAAgB,EAAEgB,KAAe;IACzD,KAAK,MAAMX,QAAQW,MAAO,MAAMhB,KAAKM,IAAI,CAAC,CAAC,mCAAmC,EAAE/B,WAAW8B,OAAO;AACpG;AAEA,OAAO,MAAMY,eAAiC;IAC5CC,WAAW,IAAMzC;IACjB0C,kBAAiBC,KAAK;QACpB,IAAIA,QAAQrC,yBAAyB;YACnC,MAAM,IAAIV,WACR,gBACA,CAAC,uBAAuB,EAAE+C,MAAM,2DAA2D,EAAErC,wBAAwB,gPAAgP,CAAC;QAE1W;IACF;IACAgC;IACAM,kBAAkBR;IAClBS,gBAAgB9C;AAClB,EAAE"}
@@ -50,6 +50,8 @@ export interface OpenDialect<Handle> {
50
50
  ensureFeatureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;
51
51
  prepareConnection?(handle: Handle, conn: Connection, cfg: Config): Promise<void>;
52
52
  setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;
53
+ engineIdentity?(): Promise<string>;
54
+ checkEngine?(): Promise<void>;
53
55
  prepareLexical?(conn: Connection): Promise<void>;
54
56
  assertLexicalReady?(conn: Connection): Promise<void>;
55
57
  createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig, options: {