sensemaking 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +15 -10
  2. package/dist/cjs/cli/index.d.cts +1 -1
  3. package/dist/cjs/cli/index.d.ts +1 -1
  4. package/dist/cjs/cli/index.js +1 -1
  5. package/dist/cjs/cli/index.js.map +1 -1
  6. package/dist/cjs/cli/named.js +6 -2
  7. package/dist/cjs/cli/named.js.map +1 -1
  8. package/dist/cjs/cli/search.js +6 -2
  9. package/dist/cjs/cli/search.js.map +1 -1
  10. package/dist/cjs/cli/shared.d.cts +2 -0
  11. package/dist/cjs/cli/shared.d.ts +2 -0
  12. package/dist/cjs/cli/shared.js +24 -0
  13. package/dist/cjs/cli/shared.js.map +1 -1
  14. package/dist/cjs/cli/status.js +6 -3
  15. package/dist/cjs/cli/status.js.map +1 -1
  16. package/dist/cjs/cli/watch.js +1 -1
  17. package/dist/cjs/cli/watch.js.map +1 -1
  18. package/dist/cjs/commands/map.js +2 -2
  19. package/dist/cjs/commands/map.js.map +1 -1
  20. package/dist/cjs/commands/related.js +2 -2
  21. package/dist/cjs/commands/related.js.map +1 -1
  22. package/dist/cjs/commands/search.d.cts +10 -0
  23. package/dist/cjs/commands/search.d.ts +10 -0
  24. package/dist/cjs/commands/search.js +489 -180
  25. package/dist/cjs/commands/search.js.map +1 -1
  26. package/dist/cjs/config/access.js +7 -1
  27. package/dist/cjs/config/access.js.map +1 -1
  28. package/dist/cjs/config/load.js +6 -1
  29. package/dist/cjs/config/load.js.map +1 -1
  30. package/dist/cjs/config/types.d.cts +3 -0
  31. package/dist/cjs/config/types.d.ts +3 -0
  32. package/dist/cjs/config/types.js.map +1 -1
  33. package/dist/cjs/config/validate.js +4 -0
  34. package/dist/cjs/config/validate.js.map +1 -1
  35. package/dist/cjs/embed/query.js +4 -4
  36. package/dist/cjs/embed/query.js.map +1 -1
  37. package/dist/cjs/output/output.js +45 -8
  38. package/dist/cjs/output/output.js.map +1 -1
  39. package/dist/cjs/output/search-error.d.cts +1 -0
  40. package/dist/cjs/output/search-error.d.ts +1 -0
  41. package/dist/cjs/output/search-error.js +28 -4
  42. package/dist/cjs/output/search-error.js.map +1 -1
  43. package/dist/cjs/scan/frontmatter.js +1 -1
  44. package/dist/cjs/scan/frontmatter.js.map +1 -1
  45. package/dist/cjs/scan/index.js +1 -0
  46. package/dist/cjs/scan/index.js.map +1 -1
  47. package/dist/cjs/store/cache.js +2 -1
  48. package/dist/cjs/store/cache.js.map +1 -1
  49. package/dist/cjs/store/duckdb/fieldStats.js +4 -7
  50. package/dist/cjs/store/duckdb/fieldStats.js.map +1 -1
  51. package/dist/cjs/store/duckdb/lexical.d.cts +2 -2
  52. package/dist/cjs/store/duckdb/lexical.d.ts +2 -2
  53. package/dist/cjs/store/duckdb/lexical.js +215 -64
  54. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  55. package/dist/cjs/store/duckdb/reconcile.js +7 -2
  56. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  57. package/dist/cjs/store/duckdb/store.js +1 -3
  58. package/dist/cjs/store/duckdb/store.js.map +1 -1
  59. package/dist/cjs/store/duckdb/vectors.js +13 -8
  60. package/dist/cjs/store/duckdb/vectors.js.map +1 -1
  61. package/dist/cjs/store/native.js +5 -4
  62. package/dist/cjs/store/native.js.map +1 -1
  63. package/dist/cjs/store/open.js +20 -6
  64. package/dist/cjs/store/open.js.map +1 -1
  65. package/dist/cjs/store/shared.js.map +1 -1
  66. package/dist/cjs/store/sqlite/lexical.js +1 -4
  67. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  68. package/dist/cjs/store/sqlite/store.js +1 -4
  69. package/dist/cjs/store/sqlite/store.js.map +1 -1
  70. package/dist/cjs/store/sqlite/vectors.js +78 -24
  71. package/dist/cjs/store/sqlite/vectors.js.map +1 -1
  72. package/dist/cjs/store/turso/connection.js +2 -1
  73. package/dist/cjs/store/turso/connection.js.map +1 -1
  74. package/dist/cjs/store/turso/lexical-text.d.cts +1 -0
  75. package/dist/cjs/store/turso/lexical-text.d.ts +1 -0
  76. package/dist/cjs/store/turso/lexical-text.js +40 -0
  77. package/dist/cjs/store/turso/lexical-text.js.map +1 -0
  78. package/dist/cjs/store/turso/lexical.js +89 -16
  79. package/dist/cjs/store/turso/lexical.js.map +1 -1
  80. package/dist/cjs/store/turso/open.d.cts +1 -1
  81. package/dist/cjs/store/turso/open.d.ts +1 -1
  82. package/dist/cjs/store/turso/open.js +2 -2
  83. package/dist/cjs/store/turso/open.js.map +1 -1
  84. package/dist/cjs/store/turso/reconcile.d.cts +1 -1
  85. package/dist/cjs/store/turso/reconcile.d.ts +1 -1
  86. package/dist/cjs/store/turso/reconcile.js +6 -2
  87. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  88. package/dist/cjs/store/turso/sql-functions.d.cts +1 -0
  89. package/dist/cjs/store/turso/sql-functions.d.ts +1 -0
  90. package/dist/cjs/store/turso/sql-functions.js +174 -0
  91. package/dist/cjs/store/turso/sql-functions.js.map +1 -0
  92. package/dist/cjs/store/turso/store.js +2 -3
  93. package/dist/cjs/store/turso/store.js.map +1 -1
  94. package/dist/cjs/store/turso/vectors.js +50 -14
  95. package/dist/cjs/store/turso/vectors.js.map +1 -1
  96. package/dist/cjs/store/types.d.cts +1 -2
  97. package/dist/cjs/store/types.d.ts +1 -2
  98. package/dist/cjs/store/types.js +1 -4
  99. package/dist/cjs/store/types.js.map +1 -1
  100. package/dist/cjs/store/vectors.d.cts +7 -0
  101. package/dist/cjs/store/vectors.d.ts +7 -0
  102. package/dist/cjs/store/vectors.js +8 -0
  103. package/dist/cjs/store/vectors.js.map +1 -1
  104. package/dist/cjs/text/segment.d.cts +13 -0
  105. package/dist/cjs/text/segment.d.ts +13 -0
  106. package/dist/cjs/text/segment.js +109 -4
  107. package/dist/cjs/text/segment.js.map +1 -1
  108. package/dist/cjs/watch.d.cts +1 -1
  109. package/dist/cjs/watch.d.ts +1 -1
  110. package/dist/cjs/watch.js +5 -4
  111. package/dist/cjs/watch.js.map +1 -1
  112. package/dist/esm/cli/index.d.ts +1 -1
  113. package/dist/esm/cli/index.js +1 -1
  114. package/dist/esm/cli/index.js.map +1 -1
  115. package/dist/esm/cli/named.js +6 -2
  116. package/dist/esm/cli/named.js.map +1 -1
  117. package/dist/esm/cli/search.js +5 -1
  118. package/dist/esm/cli/search.js.map +1 -1
  119. package/dist/esm/cli/shared.d.ts +2 -0
  120. package/dist/esm/cli/shared.js +19 -0
  121. package/dist/esm/cli/shared.js.map +1 -1
  122. package/dist/esm/cli/status.js +6 -3
  123. package/dist/esm/cli/status.js.map +1 -1
  124. package/dist/esm/cli/watch.js +1 -1
  125. package/dist/esm/cli/watch.js.map +1 -1
  126. package/dist/esm/commands/map.js +2 -2
  127. package/dist/esm/commands/map.js.map +1 -1
  128. package/dist/esm/commands/related.js +2 -1
  129. package/dist/esm/commands/related.js.map +1 -1
  130. package/dist/esm/commands/search.d.ts +10 -0
  131. package/dist/esm/commands/search.js +272 -91
  132. package/dist/esm/commands/search.js.map +1 -1
  133. package/dist/esm/config/access.js +7 -0
  134. package/dist/esm/config/access.js.map +1 -1
  135. package/dist/esm/config/load.js +6 -1
  136. package/dist/esm/config/load.js.map +1 -1
  137. package/dist/esm/config/types.d.ts +3 -0
  138. package/dist/esm/config/types.js.map +1 -1
  139. package/dist/esm/config/validate.js +4 -0
  140. package/dist/esm/config/validate.js.map +1 -1
  141. package/dist/esm/embed/query.js +4 -4
  142. package/dist/esm/embed/query.js.map +1 -1
  143. package/dist/esm/output/output.js +27 -4
  144. package/dist/esm/output/output.js.map +1 -1
  145. package/dist/esm/output/search-error.d.ts +1 -0
  146. package/dist/esm/output/search-error.js +17 -1
  147. package/dist/esm/output/search-error.js.map +1 -1
  148. package/dist/esm/scan/frontmatter.js +1 -1
  149. package/dist/esm/scan/frontmatter.js.map +1 -1
  150. package/dist/esm/scan/index.js +1 -0
  151. package/dist/esm/scan/index.js.map +1 -1
  152. package/dist/esm/store/cache.js +2 -1
  153. package/dist/esm/store/cache.js.map +1 -1
  154. package/dist/esm/store/duckdb/fieldStats.js +4 -7
  155. package/dist/esm/store/duckdb/fieldStats.js.map +1 -1
  156. package/dist/esm/store/duckdb/lexical.d.ts +2 -2
  157. package/dist/esm/store/duckdb/lexical.js +94 -40
  158. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  159. package/dist/esm/store/duckdb/reconcile.js +2 -2
  160. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  161. package/dist/esm/store/duckdb/store.js +2 -4
  162. package/dist/esm/store/duckdb/store.js.map +1 -1
  163. package/dist/esm/store/duckdb/vectors.js +26 -13
  164. package/dist/esm/store/duckdb/vectors.js.map +1 -1
  165. package/dist/esm/store/native.js +5 -4
  166. package/dist/esm/store/native.js.map +1 -1
  167. package/dist/esm/store/open.js +21 -4
  168. package/dist/esm/store/open.js.map +1 -1
  169. package/dist/esm/store/shared.js +1 -2
  170. package/dist/esm/store/shared.js.map +1 -1
  171. package/dist/esm/store/sqlite/lexical.js +2 -5
  172. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  173. package/dist/esm/store/sqlite/store.js +1 -4
  174. package/dist/esm/store/sqlite/store.js.map +1 -1
  175. package/dist/esm/store/sqlite/vectors.js +48 -19
  176. package/dist/esm/store/sqlite/vectors.js.map +1 -1
  177. package/dist/esm/store/turso/connection.js +3 -2
  178. package/dist/esm/store/turso/connection.js.map +1 -1
  179. package/dist/esm/store/turso/lexical-text.d.ts +1 -0
  180. package/dist/esm/store/turso/lexical-text.js +12 -0
  181. package/dist/esm/store/turso/lexical-text.js.map +1 -0
  182. package/dist/esm/store/turso/lexical.js +56 -21
  183. package/dist/esm/store/turso/lexical.js.map +1 -1
  184. package/dist/esm/store/turso/open.d.ts +1 -1
  185. package/dist/esm/store/turso/open.js +2 -2
  186. package/dist/esm/store/turso/open.js.map +1 -1
  187. package/dist/esm/store/turso/reconcile.d.ts +1 -1
  188. package/dist/esm/store/turso/reconcile.js +6 -2
  189. package/dist/esm/store/turso/reconcile.js.map +1 -1
  190. package/dist/esm/store/turso/sql-functions.d.ts +1 -0
  191. package/dist/esm/store/turso/sql-functions.js +179 -0
  192. package/dist/esm/store/turso/sql-functions.js.map +1 -0
  193. package/dist/esm/store/turso/store.js +3 -5
  194. package/dist/esm/store/turso/store.js.map +1 -1
  195. package/dist/esm/store/turso/vectors.js +19 -10
  196. package/dist/esm/store/turso/vectors.js.map +1 -1
  197. package/dist/esm/store/types.d.ts +1 -2
  198. package/dist/esm/store/types.js +6 -8
  199. package/dist/esm/store/types.js.map +1 -1
  200. package/dist/esm/store/vectors.d.ts +7 -0
  201. package/dist/esm/store/vectors.js +8 -2
  202. package/dist/esm/store/vectors.js.map +1 -1
  203. package/dist/esm/text/segment.d.ts +13 -0
  204. package/dist/esm/text/segment.js +70 -0
  205. package/dist/esm/text/segment.js.map +1 -1
  206. package/dist/esm/watch.d.ts +1 -1
  207. package/dist/esm/watch.js +5 -4
  208. package/dist/esm/watch.js.map +1 -1
  209. package/package.json +4 -3
  210. package/schema.json +7 -2
  211. package/skills/sense/EXAMPLES.md +3 -3
  212. package/skills/sense/SKILL.md +6 -6
  213. package/skills/sense-setup/EXAMPLES.md +21 -0
  214. package/skills/sense-setup/SKILL.md +3 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { 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 = '5';\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 ensureSchema(_handle: Database, conn: Connection, cfg: Config): 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_ngram TEXT, summary_ngram TEXT, text_ngram TEXT)`);\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 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, scale REAL, vector F32_BLOB(${STORE_DIMS}), PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n if ((await getMeta(conn, 'features')) === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));\n}\n\nasync function close(handle: Database): Promise<void> {\n 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): 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 // `timeout` is connect-time only in this client; the derived value is set via runtime PRAGMA\n // below. Options in native.ts, shared with reclaimSpace's own connection.\n db = await turso.connect(dbPath, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental] });\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 ensureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle, conn),\n};\n\nexport async function openTurso(cfg: ResolvedConfig): Promise<OpenResult> {\n return openWithDialect(cfg, tursoOpenDialect);\n}\n"],"names":["featureSignature","STORE_DIMS","SenseError","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","checkpointWal","createConnection","CONNECT_OPTS","TURSO_PACKAGE","tursoApi","CONTENT_FTS_DDL","tursoDialect","createStore","DB_FILENAME","SCHEMA_VERSION","ensureSchema","_handle","conn","cfg","exec","ddl","feature","name","schema","close","handle","setDerivedBusyTimeout","ms","connect","dbPath","_cfg","turso","err","message","db","experimental","tursoOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","openTurso"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/open.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { 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 = '8';\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 ensureSchema(_handle: Database, conn: Connection, cfg: Config): 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 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 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, scale REAL, vector F32_BLOB(${STORE_DIMS}), PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n if ((await getMeta(conn, 'features')) === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));\n}\n\nasync function close(handle: Database): Promise<void> {\n 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): 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 // `timeout` is connect-time only in this client; the derived value is set via runtime PRAGMA\n // below. Options in native.ts, shared with reclaimSpace's own connection.\n db = await turso.connect(dbPath, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental] });\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 ensureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle, conn),\n};\n\nexport async function openTurso(cfg: ResolvedConfig): Promise<OpenResult> {\n return openWithDialect(cfg, tursoOpenDialect);\n}\n"],"names":["featureSignature","STORE_DIMS","SenseError","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","checkpointWal","createConnection","CONNECT_OPTS","TURSO_PACKAGE","tursoApi","CONTENT_FTS_DDL","tursoDialect","createStore","DB_FILENAME","SCHEMA_VERSION","ensureSchema","_handle","conn","cfg","exec","ddl","feature","name","schema","close","handle","setDerivedBusyTimeout","ms","connect","dbPath","_cfg","turso","err","message","db","experimental","tursoOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","openTurso"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { ReconcileDialect } from '../types.js';
2
- export declare const CONTENT_FTS_DDL: readonly ["CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title, summary, text) WITH (weights = 'title=10.0,summary=5.0,text=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')"];
2
+ 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')"];
3
3
  export declare const CONTENT_FTS_NAMES: readonly ["content_fts", "content_fts_ngram"];
4
4
  export declare const FTS_REBUILD_THRESHOLD = 250;
5
5
  export declare const tursoDialect: ReconcileDialect;
@@ -2,6 +2,7 @@ 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 { stemFolded } from './lexical-text.js';
5
6
  // This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in
6
7
  // store/reconcile.ts. content is a plain table with "_ngram" sidecars for lexical.ts.
7
8
  // The ngram index is scoped to disjoint "_ngram" sidecar columns: a second index over the same
@@ -9,7 +10,7 @@ import { BEGIN_WRITE } from '../transaction.js';
9
10
  // The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around
10
11
  // a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.
11
12
  export const CONTENT_FTS_DDL = [
12
- `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title, summary, text) WITH (weights = 'title=10.0,summary=5.0,text=1.0')`,
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')`,
13
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')`
14
15
  ];
15
16
  export const CONTENT_FTS_NAMES = [
@@ -28,7 +29,7 @@ const MAX_FRONTMATTER_COLUMNS = 2000;
28
29
  // 2026-08-30; the derivation and its bounds are pinned in this file's spec.
29
30
  export const FTS_REBUILD_THRESHOLD = 250;
30
31
  // No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.
31
- const INSERT_CONTENT_SQL = `INSERT INTO content ("path", title, summary, text, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?)`;
32
+ const INSERT_CONTENT_SQL = `INSERT INTO content ("path", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
32
33
  function contentRow(doc) {
33
34
  const { title, summary, text } = doc.search;
34
35
  return [
@@ -36,6 +37,9 @@ function contentRow(doc) {
36
37
  title,
37
38
  summary,
38
39
  text,
40
+ stemFolded(title),
41
+ stemFolded(summary),
42
+ stemFolded(text),
39
43
  ngramSidecar(title),
40
44
  ngramSidecar(summary),
41
45
  ngramSidecar(text)
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import { 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';\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, summary, text) WITH (weights = 'title=10.0,summary=5.0,text=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// Changed files above which rebuilding the FTS index beats maintaining it per row. Measured\n// 2026-08-30; the derivation and its bounds are pinned in this file's spec.\nexport const FTS_REBUILD_THRESHOLD = 250;\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_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, ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta): Promise<void> {\n const churn = delta.reparsed.length + delta.vanished.length;\n\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 = delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD;\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 await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n touched.map((p) => [p])\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\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,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","CONTENT_FTS_DDL","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","reconcileContent","conn","touched","docs","delta","churn","reparsed","length","vanished","bulk","files","name","exec","runBatch","map","p","ddl","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","recordDuration"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAGhD,qFAAqF;AACrF,sFAAsF;AAEtF,+FAA+F;AAC/F,2FAA2F;AAC3F,iGAAiG;AACjG,uGAAuG;AACvG,OAAO,MAAMC,kBAAkB;IAC7B,CAAC,qIAAqI,CAAC;IACvI,CAAC,gMAAgM,CAAC;CACnM,CAAU;AACX,OAAO,MAAMC,oBAAoB;IAAC;IAAe;CAAoB,CAAU;AAE/E,6FAA6F;AAC7F,gGAAgG;AAChG,SAASC,aAAaC,IAAY;IAChC,OAAOP,eAAeO,QAAQA,OAAO;AACvC;AAEA,8FAA8F;AAC9F,+GAA+G;AAC/G,MAAMC,0BAA0B;AAEhC,4FAA4F;AAC5F,4EAA4E;AAC5E,OAAO,MAAMC,wBAAwB,IAAI;AAEzC,yFAAyF;AACzF,MAAMC,qBAAqB,CAAC,uHAAuH,CAAC;AAEpJ,SAASC,WAAWC,GAAc;IAChC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEP,IAAI,EAAE,GAAGK,IAAIG,MAAM;IAC3C,OAAO;QAACH,IAAII,OAAO;QAAEH;QAAOC;QAASP;QAAMD,aAAaO;QAAQP,aAAaQ;QAAUR,aAAaC;KAAM;AAC5G;AAEA,eAAeU,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,KAAqB;IAC3G,MAAMC,QAAQD,MAAME,QAAQ,CAACC,MAAM,GAAGH,MAAMI,QAAQ,CAACD,MAAM;IAE3D,6FAA6F;IAC7F,qDAAqD;IACrD,MAAME,OAAOL,MAAMM,KAAK,CAACH,MAAM,KAAK,KAAKF,QAAQb;IACjD,IAAIiB,MAAM,KAAK,MAAME,QAAQvB,kBAAmB,MAAMa,KAAKW,IAAI,CAAC,CAAC,qBAAqB,EAAED,MAAM;IAE9F,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAIT,QAAQK,MAAM,GAAG,GACnB,MAAMN,KAAKY,QAAQ,CACjB,wCACAX,QAAQY,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAE1B,IAAIZ,KAAKI,MAAM,GAAG,GAAG,MAAMN,KAAKY,QAAQ,CAACpB,oBAAoBU,KAAKW,GAAG,CAACpB;IACtE,IAAIe,MAAM,KAAK,MAAMO,OAAO7B,gBAAiB,MAAMc,KAAKW,IAAI,CAACI;AAC/D;AAEA,yFAAyF;AACzF,eAAeC,WAAWhB,IAAgB,EAAEiB,KAAe;IACzD,KAAK,MAAMP,QAAQO,MAAO,MAAMjB,KAAKW,IAAI,CAAC,CAAC,mCAAmC,EAAE5B,WAAW2B,OAAO;AACpG;AAEA,OAAO,MAAMQ,eAAiC;IAC5CC,WAAW,IAAMlC;IACjBmC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQ/B,yBAAyB;YACnC,MAAM,IAAIT,WACR,gBACA,CAAC,uBAAuB,EAAEwC,MAAM,2DAA2D,EAAE/B,wBAAwB,gPAAgP,CAAC;QAE1W;IACF;IACA0B;IACAjB;IACAuB,gBAAgBtC;AAClB,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import { 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// Changed files above which rebuilding the FTS index beats maintaining it per row. Measured\n// 2026-08-30; the derivation and its bounds are pinned in this file's spec.\nexport const FTS_REBUILD_THRESHOLD = 250;\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\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta): Promise<void> {\n const churn = delta.reparsed.length + delta.vanished.length;\n\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 = delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD;\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 await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n touched.map((p) => [p])\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\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,\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","reconcileContent","conn","touched","docs","delta","churn","reparsed","length","vanished","bulk","files","name","exec","runBatch","map","p","ddl","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","recordDuration"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,UAAU,QAAQ,oBAAoB;AAE/C,qFAAqF;AACrF,sFAAsF;AAEtF,+FAA+F;AAC/F,2FAA2F;AAC3F,iGAAiG;AACjG,uGAAuG;AACvG,OAAO,MAAMC,kBAAkB;IAC7B,CAAC,mKAAmK,CAAC;IACrK,CAAC,gMAAgM,CAAC;CACnM,CAAU;AACX,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,4FAA4F;AAC5F,4EAA4E;AAC5E,OAAO,MAAMC,wBAAwB,IAAI;AAEzC,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;QAAMJ,WAAWU;QAAQV,WAAWW;QAAUX,WAAWI;QAAOD,aAAaO;QAAQP,aAAaQ;QAAUR,aAAaC;KAAM;AACtK;AAEA,eAAeU,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,KAAqB;IAC3G,MAAMC,QAAQD,MAAME,QAAQ,CAACC,MAAM,GAAGH,MAAMI,QAAQ,CAACD,MAAM;IAE3D,6FAA6F;IAC7F,qDAAqD;IACrD,MAAME,OAAOL,MAAMM,KAAK,CAACH,MAAM,KAAK,KAAKF,QAAQb;IACjD,IAAIiB,MAAM,KAAK,MAAME,QAAQvB,kBAAmB,MAAMa,KAAKW,IAAI,CAAC,CAAC,qBAAqB,EAAED,MAAM;IAE9F,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAIT,QAAQK,MAAM,GAAG,GACnB,MAAMN,KAAKY,QAAQ,CACjB,wCACAX,QAAQY,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAE1B,IAAIZ,KAAKI,MAAM,GAAG,GAAG,MAAMN,KAAKY,QAAQ,CAACpB,oBAAoBU,KAAKW,GAAG,CAACpB;IACtE,IAAIe,MAAM,KAAK,MAAMO,OAAO7B,gBAAiB,MAAMc,KAAKW,IAAI,CAACI;AAC/D;AAEA,yFAAyF;AACzF,eAAeC,WAAWhB,IAAgB,EAAEiB,KAAe;IACzD,KAAK,MAAMP,QAAQO,MAAO,MAAMjB,KAAKW,IAAI,CAAC,CAAC,mCAAmC,EAAE7B,WAAW4B,OAAO;AACpG;AAEA,OAAO,MAAMQ,eAAiC;IAC5CC,WAAW,IAAMnC;IACjBoC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQ/B,yBAAyB;YACnC,MAAM,IAAIV,WACR,gBACA,CAAC,uBAAuB,EAAEyC,MAAM,2DAA2D,EAAE/B,wBAAwB,gPAAgP,CAAC;QAE1W;IACF;IACA0B;IACAjB;IACAuB,gBAAgBvC;AAClB,EAAE"}
@@ -0,0 +1 @@
1
+ export declare function rewriteFunctions(sql: string): string;
@@ -0,0 +1,179 @@
1
+ import { SenseError } from '../../errors.js';
2
+ // Turso can't register UDFs: has()/basename() are rewritten into portable SQL below; segment()
3
+ // (Intl.Segmenter grapheme clustering, types.ts's 'segment' capability) has none, so it's rejected like an unsupported FTS5 operator.
4
+ // Non-global: recursion (readArgs' contents rewritten via rewriteFunctions) reenters this module's
5
+ // regexes before the outer scan resumes, and a global regex's lastIndex does not nest across that.
6
+ const CALL_ANYWHERE = /\b(has|basename|segment)\s*\(/i;
7
+ const CALL_AT_START = /^(has|basename|segment)\s*\(/i;
8
+ function isWordChar(ch) {
9
+ return ch !== undefined && /[A-Za-z0-9_]/.test(ch);
10
+ }
11
+ // The index just past the quoted literal or SQL comment starting at i, or null if i starts
12
+ // neither. Shared by the scan and readArgs so both leave quoted/comment text opaque to a call match.
13
+ function skipOpaque(sql, i) {
14
+ const ch = sql[i];
15
+ if (ch === "'" || ch === '"') {
16
+ let j = i + 1;
17
+ while(j < sql.length){
18
+ if (sql[j] === ch) {
19
+ if (sql[j + 1] === ch) {
20
+ j += 2;
21
+ continue;
22
+ }
23
+ return j + 1;
24
+ }
25
+ j++;
26
+ }
27
+ return j; // unterminated literal: consume to end rather than loop forever
28
+ }
29
+ if (ch === '-' && sql[i + 1] === '-') {
30
+ const nl = sql.indexOf('\n', i);
31
+ return nl === -1 ? sql.length : nl + 1;
32
+ }
33
+ if (ch === '/' && sql[i + 1] === '*') {
34
+ const close = sql.indexOf('*/', i + 2);
35
+ return close === -1 ? sql.length : close + 2;
36
+ }
37
+ return null;
38
+ }
39
+ // `?N`/named placeholders bind by number/name, so repeating one is safe; an anonymous `?` takes
40
+ // the next slot per occurrence, so it's numbered here first -- sqlHas/sqlBasename duplicate their arguments.
41
+ function numberPlaceholders(sql) {
42
+ let out = '';
43
+ let i = 0;
44
+ let n = 0;
45
+ while(i < sql.length){
46
+ var _sql_;
47
+ const skip = skipOpaque(sql, i);
48
+ if (skip !== null) {
49
+ out += sql.slice(i, skip);
50
+ i = skip;
51
+ continue;
52
+ }
53
+ if (sql[i] === '?' && !/[0-9]/.test((_sql_ = sql[i + 1]) !== null && _sql_ !== void 0 ? _sql_ : '')) {
54
+ n++;
55
+ out += `?${n}`;
56
+ i++;
57
+ continue;
58
+ }
59
+ out += sql[i];
60
+ i++;
61
+ }
62
+ return out;
63
+ }
64
+ // The un-rewritten argument texts of the call whose '(' sits at parenStart, and the index just
65
+ // past its matching ')'. Paren depth is tracked so a nested call's commas/parens aren't mistaken for ours.
66
+ function readArgs(sql, parenStart) {
67
+ const args = [];
68
+ let depth = 1;
69
+ let cur = '';
70
+ let i = parenStart + 1;
71
+ while(i < sql.length){
72
+ const skip = skipOpaque(sql, i);
73
+ if (skip !== null) {
74
+ cur += sql.slice(i, skip);
75
+ i = skip;
76
+ continue;
77
+ }
78
+ const ch = sql[i];
79
+ if (ch === '(') {
80
+ depth++;
81
+ cur += ch;
82
+ i++;
83
+ continue;
84
+ }
85
+ if (ch === ')') {
86
+ depth--;
87
+ i++;
88
+ if (depth === 0) break;
89
+ cur += ch;
90
+ continue;
91
+ }
92
+ if (ch === ',' && depth === 1) {
93
+ args.push(cur.trim());
94
+ cur = '';
95
+ i++;
96
+ continue;
97
+ }
98
+ cur += ch;
99
+ i++;
100
+ }
101
+ if (depth !== 0) throw new Error(`turso: unterminated argument list starting at "${sql.slice(parenStart, parenStart + 30)}..."`);
102
+ if (cur.trim() !== '') args.push(cur.trim());
103
+ return {
104
+ args,
105
+ end: i
106
+ };
107
+ }
108
+ // field.startsWith('[') && JSON.parse(field) is an array, matched by membership; every other
109
+ // field, stringified, matched by substring (sql-functions.ts's hasImpl, mirrored branch for branch).
110
+ function sqlHas(fieldExpr, valueExpr) {
111
+ const isArray = `substr(${fieldExpr},1,1) = '[' AND json_valid(${fieldExpr})`;
112
+ // json_each's argument is re-guarded by its own CASE (falling back to '[]'): turso evaluates a
113
+ // CASE branch's table function even when untaken, and a non-array argument there raises rather than yielding 0 rows (spike-verified).
114
+ return `(CASE
115
+ WHEN ${fieldExpr} IS NULL THEN 0
116
+ WHEN ${isArray} THEN
117
+ (SELECT COUNT(*) FROM json_each(CASE WHEN ${isArray} THEN ${fieldExpr} ELSE '[]' END) je WHERE CAST(je.value AS TEXT) = CAST(${valueExpr} AS TEXT)) > 0
118
+ ELSE instr(CAST(${fieldExpr} AS TEXT), CAST(${valueExpr} AS TEXT)) > 0
119
+ END)`;
120
+ }
121
+ // posix.basename(String(path)), suffix stripped Unix-style (sql-functions.ts's basenameImpl). No
122
+ // last-'/' primitive exists, so reverse() finds the first '/' from the end, then reverse() restores order.
123
+ function sqlBasename(pathExpr, suffixExpr) {
124
+ const trimmed = `rtrim(CAST(${pathExpr} AS TEXT), '/')`;
125
+ const name = `CASE
126
+ WHEN ${trimmed} = '' THEN ''
127
+ WHEN instr(${trimmed}, '/') = 0 THEN ${trimmed}
128
+ ELSE reverse(substr(reverse(${trimmed}), 1, instr(reverse(${trimmed}), '/') - 1))
129
+ END`;
130
+ return `(CASE WHEN ${pathExpr} IS NULL THEN NULL ELSE
131
+ (CASE WHEN ${suffixExpr} IS NOT NULL AND CAST(${suffixExpr} AS TEXT) != '' AND CAST(${suffixExpr} AS TEXT) != (${name})
132
+ AND substr((${name}), -length(CAST(${suffixExpr} AS TEXT))) = CAST(${suffixExpr} AS TEXT)
133
+ THEN substr((${name}), 1, length((${name})) - length(CAST(${suffixExpr} AS TEXT)))
134
+ ELSE (${name}) END)
135
+ END)`;
136
+ }
137
+ // Rewrites every has()/basename() call into the portable SQL above, recursing into each call's own
138
+ // arguments first so a nested `instr(basename(f.path), 'x')` resolves inside out; segment() instead raises STORE_CAPABILITY_MISSING, named, like an unsupported FTS5 operator.
139
+ export function rewriteFunctions(sql) {
140
+ if (!CALL_ANYWHERE.test(sql)) return sql;
141
+ // Numbered once, over the whole statement, before any argument is duplicated below -- rewrite()
142
+ // recurses over already-numbered substrings, never renumbering them a second time.
143
+ return rewrite(numberPlaceholders(sql));
144
+ }
145
+ function rewrite(sql) {
146
+ let out = '';
147
+ let i = 0;
148
+ while(i < sql.length){
149
+ const skip = skipOpaque(sql, i);
150
+ if (skip !== null) {
151
+ out += sql.slice(i, skip);
152
+ i = skip;
153
+ continue;
154
+ }
155
+ const m = !isWordChar(sql[i - 1]) ? CALL_AT_START.exec(sql.slice(i)) : null;
156
+ if (m) {
157
+ const name = m[1].toLowerCase();
158
+ const parenStart = i + m[0].length - 1;
159
+ if (name === 'segment') {
160
+ throw new SenseError('STORE_CAPABILITY_MISSING', `store "turso" does not implement segment() as a SQL function in this build (its client cannot register UDFs); rephrase the query without it, or set "store" to "sqlite" in this tree's config to run it as written`);
161
+ }
162
+ const { args, end } = readArgs(sql, parenStart);
163
+ const rewrittenArgs = args.map(rewrite);
164
+ if (name === 'has') {
165
+ if (rewrittenArgs.length !== 2) throw new Error(`turso: has() takes exactly 2 arguments (field, value), got ${rewrittenArgs.length}`);
166
+ out += sqlHas(rewrittenArgs[0], rewrittenArgs[1]);
167
+ } else {
168
+ var _rewrittenArgs_;
169
+ if (rewrittenArgs.length < 1 || rewrittenArgs.length > 2) throw new Error(`turso: basename() takes 1 or 2 arguments (path, suffix?), got ${rewrittenArgs.length}`);
170
+ out += sqlBasename(rewrittenArgs[0], (_rewrittenArgs_ = rewrittenArgs[1]) !== null && _rewrittenArgs_ !== void 0 ? _rewrittenArgs_ : 'NULL');
171
+ }
172
+ i = end;
173
+ continue;
174
+ }
175
+ out += sql[i];
176
+ i++;
177
+ }
178
+ return out;
179
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/sql-functions.ts"],"sourcesContent":["import { SenseError } from '../../errors.ts';\n\n// Turso can't register UDFs: has()/basename() are rewritten into portable SQL below; segment()\n// (Intl.Segmenter grapheme clustering, types.ts's 'segment' capability) has none, so it's rejected like an unsupported FTS5 operator.\n\n// Non-global: recursion (readArgs' contents rewritten via rewriteFunctions) reenters this module's\n// regexes before the outer scan resumes, and a global regex's lastIndex does not nest across that.\nconst CALL_ANYWHERE = /\\b(has|basename|segment)\\s*\\(/i;\nconst CALL_AT_START = /^(has|basename|segment)\\s*\\(/i;\n\nfunction isWordChar(ch: string | undefined): boolean {\n return ch !== undefined && /[A-Za-z0-9_]/.test(ch);\n}\n\n// The index just past the quoted literal or SQL comment starting at i, or null if i starts\n// neither. Shared by the scan and readArgs so both leave quoted/comment text opaque to a call match.\nfunction skipOpaque(sql: string, i: number): number | null {\n const ch = sql[i];\n if (ch === \"'\" || ch === '\"') {\n let j = i + 1;\n while (j < sql.length) {\n if (sql[j] === ch) {\n if (sql[j + 1] === ch) {\n j += 2;\n continue;\n }\n return j + 1;\n }\n j++;\n }\n return j; // unterminated literal: consume to end rather than loop forever\n }\n if (ch === '-' && sql[i + 1] === '-') {\n const nl = sql.indexOf('\\n', i);\n return nl === -1 ? sql.length : nl + 1;\n }\n if (ch === '/' && sql[i + 1] === '*') {\n const close = sql.indexOf('*/', i + 2);\n return close === -1 ? sql.length : close + 2;\n }\n return null;\n}\n\n// `?N`/named placeholders bind by number/name, so repeating one is safe; an anonymous `?` takes\n// the next slot per occurrence, so it's numbered here first -- sqlHas/sqlBasename duplicate their arguments.\nfunction numberPlaceholders(sql: string): string {\n let out = '';\n let i = 0;\n let n = 0;\n while (i < sql.length) {\n const skip = skipOpaque(sql, i);\n if (skip !== null) {\n out += sql.slice(i, skip);\n i = skip;\n continue;\n }\n if (sql[i] === '?' && !/[0-9]/.test(sql[i + 1] ?? '')) {\n n++;\n out += `?${n}`;\n i++;\n continue;\n }\n out += sql[i];\n i++;\n }\n return out;\n}\n\n// The un-rewritten argument texts of the call whose '(' sits at parenStart, and the index just\n// past its matching ')'. Paren depth is tracked so a nested call's commas/parens aren't mistaken for ours.\nfunction readArgs(sql: string, parenStart: number): { args: string[]; end: number } {\n const args: string[] = [];\n let depth = 1;\n let cur = '';\n let i = parenStart + 1;\n while (i < sql.length) {\n const skip = skipOpaque(sql, i);\n if (skip !== null) {\n cur += sql.slice(i, skip);\n i = skip;\n continue;\n }\n const ch = sql[i];\n if (ch === '(') {\n depth++;\n cur += ch;\n i++;\n continue;\n }\n if (ch === ')') {\n depth--;\n i++;\n if (depth === 0) break;\n cur += ch;\n continue;\n }\n if (ch === ',' && depth === 1) {\n args.push(cur.trim());\n cur = '';\n i++;\n continue;\n }\n cur += ch;\n i++;\n }\n if (depth !== 0) throw new Error(`turso: unterminated argument list starting at \"${sql.slice(parenStart, parenStart + 30)}...\"`);\n if (cur.trim() !== '') args.push(cur.trim());\n return { args, end: i };\n}\n\n// field.startsWith('[') && JSON.parse(field) is an array, matched by membership; every other\n// field, stringified, matched by substring (sql-functions.ts's hasImpl, mirrored branch for branch).\nfunction sqlHas(fieldExpr: string, valueExpr: string): string {\n const isArray = `substr(${fieldExpr},1,1) = '[' AND json_valid(${fieldExpr})`;\n // json_each's argument is re-guarded by its own CASE (falling back to '[]'): turso evaluates a\n // CASE branch's table function even when untaken, and a non-array argument there raises rather than yielding 0 rows (spike-verified).\n return `(CASE\n WHEN ${fieldExpr} IS NULL THEN 0\n WHEN ${isArray} THEN\n (SELECT COUNT(*) FROM json_each(CASE WHEN ${isArray} THEN ${fieldExpr} ELSE '[]' END) je WHERE CAST(je.value AS TEXT) = CAST(${valueExpr} AS TEXT)) > 0\n ELSE instr(CAST(${fieldExpr} AS TEXT), CAST(${valueExpr} AS TEXT)) > 0\n END)`;\n}\n\n// posix.basename(String(path)), suffix stripped Unix-style (sql-functions.ts's basenameImpl). No\n// last-'/' primitive exists, so reverse() finds the first '/' from the end, then reverse() restores order.\nfunction sqlBasename(pathExpr: string, suffixExpr: string): string {\n const trimmed = `rtrim(CAST(${pathExpr} AS TEXT), '/')`;\n const name = `CASE\n WHEN ${trimmed} = '' THEN ''\n WHEN instr(${trimmed}, '/') = 0 THEN ${trimmed}\n ELSE reverse(substr(reverse(${trimmed}), 1, instr(reverse(${trimmed}), '/') - 1))\n END`;\n return `(CASE WHEN ${pathExpr} IS NULL THEN NULL ELSE\n (CASE WHEN ${suffixExpr} IS NOT NULL AND CAST(${suffixExpr} AS TEXT) != '' AND CAST(${suffixExpr} AS TEXT) != (${name})\n AND substr((${name}), -length(CAST(${suffixExpr} AS TEXT))) = CAST(${suffixExpr} AS TEXT)\n THEN substr((${name}), 1, length((${name})) - length(CAST(${suffixExpr} AS TEXT)))\n ELSE (${name}) END)\n END)`;\n}\n\n// Rewrites every has()/basename() call into the portable SQL above, recursing into each call's own\n// arguments first so a nested `instr(basename(f.path), 'x')` resolves inside out; segment() instead raises STORE_CAPABILITY_MISSING, named, like an unsupported FTS5 operator.\nexport function rewriteFunctions(sql: string): string {\n if (!CALL_ANYWHERE.test(sql)) return sql;\n // Numbered once, over the whole statement, before any argument is duplicated below -- rewrite()\n // recurses over already-numbered substrings, never renumbering them a second time.\n return rewrite(numberPlaceholders(sql));\n}\n\nfunction rewrite(sql: string): string {\n let out = '';\n let i = 0;\n while (i < sql.length) {\n const skip = skipOpaque(sql, i);\n if (skip !== null) {\n out += sql.slice(i, skip);\n i = skip;\n continue;\n }\n const m = !isWordChar(sql[i - 1]) ? CALL_AT_START.exec(sql.slice(i)) : null;\n if (m) {\n const name = m[1].toLowerCase();\n const parenStart = i + m[0].length - 1;\n if (name === 'segment') {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"turso\" does not implement segment() as a SQL function in this build (its client cannot register UDFs); rephrase the query without it, or set \"store\" to \"sqlite\" in this tree's config to run it as written`);\n }\n const { args, end } = readArgs(sql, parenStart);\n const rewrittenArgs = args.map(rewrite);\n if (name === 'has') {\n if (rewrittenArgs.length !== 2) throw new Error(`turso: has() takes exactly 2 arguments (field, value), got ${rewrittenArgs.length}`);\n out += sqlHas(rewrittenArgs[0], rewrittenArgs[1]);\n } else {\n if (rewrittenArgs.length < 1 || rewrittenArgs.length > 2) throw new Error(`turso: basename() takes 1 or 2 arguments (path, suffix?), got ${rewrittenArgs.length}`);\n out += sqlBasename(rewrittenArgs[0], rewrittenArgs[1] ?? 'NULL');\n }\n i = end;\n continue;\n }\n out += sql[i];\n i++;\n }\n return out;\n}\n"],"names":["SenseError","CALL_ANYWHERE","CALL_AT_START","isWordChar","ch","undefined","test","skipOpaque","sql","i","j","length","nl","indexOf","close","numberPlaceholders","out","n","skip","slice","readArgs","parenStart","args","depth","cur","push","trim","Error","end","sqlHas","fieldExpr","valueExpr","isArray","sqlBasename","pathExpr","suffixExpr","trimmed","name","rewriteFunctions","rewrite","m","exec","toLowerCase","rewrittenArgs","map"],"mappings":"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"}
@@ -5,12 +5,10 @@ import { hasVectorRow, pendingRows } from '../vectors.js';
5
5
  import { cacheFilePath, checkpointWal, fileSize, reclaimSpace } from './connection.js';
6
6
  import { fieldStats } from './fieldStats.js';
7
7
  import { queryLexical } from './lexical.js';
8
+ import { rewriteFunctions } from './sql-functions.js';
8
9
  import { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.js';
9
- // No 'snippets': fts_highlight returns the whole column, not a bounded window, so hits use
10
- // the caller's JS excerpt.
10
+ // No 'segment': has()/basename() resolve via SQL rewrite below (sql-functions.ts), but segment() has no SQL form and the client cannot register it as a UDF.
11
11
  export const CAPABILITIES = new Set([
12
- 'lexical',
13
- 'phrases',
14
12
  'vectors'
15
13
  ]);
16
14
  // close() reclaims once the cache file outgrows its compact size by this factor: the disk
@@ -64,7 +62,7 @@ export function createStore(db, conn) {
64
62
  },
65
63
  raw: {
66
64
  async prepare (sql) {
67
- const stmt = await db.prepare(sql);
65
+ const stmt = await db.prepare(rewriteFunctions(sql));
68
66
  stmt.safeIntegers(true); // int64 past 2^53 arrives as BigInt instead of losing precision
69
67
  return {
70
68
  columns: ()=>stmt.columns(),
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/store.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { getColumns, getMeta, setMeta } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { cacheFilePath, checkpointWal, fileSize, reclaimSpace } from './connection.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\n// No 'snippets': fts_highlight returns the whole column, not a bounded window, so hits use\n// the caller's JS excerpt.\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['lexical', 'phrases', 'vectors']);\n\n// close() reclaims once the cache file outgrows its compact size by this factor: the disk\n// overhead a user should accept (PLAN 3.52), in postgres autovacuum scale-factor shape.\nconst BLOAT_FACTOR = 1.5;\n\n// Shares one Connection instance (conn) with the builder's own reconcile call so transaction depth\n// (see transaction.ts) is tracked against the same object everywhere.\nexport function createStore(db: Database, conn: Connection): Store {\n return {\n name: 'turso',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n fieldStats: (columns, scopeWhere) => fieldStats(conn, columns, scopeWhere),\n },\n lexical: {\n query: (terms, opts) => queryLexical(conn, terms, opts),\n },\n // The column's fixed DDL width (STORE_DIMS) is what every scan binds against, not the\n // interface's per-call storeDims -- see vectors.ts's padded() for why a shorter vector is still correct against a wider column.\n vectors: {\n pending: () => pendingRows(conn),\n writeVectors: (rows) => writeVectorBatch(conn, STORE_DIMS, rows),\n candidates: (qv, _storeDims, fetch, allowed) => scanCandidates(conn, qv, STORE_DIMS, fetch, allowed),\n similar: (path, opts) => scanSimilar(conn, STORE_DIMS, path, opts),\n hasVector: (path) => hasVectorRow(conn, path),\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min). Turso's PRAGMA busy_timeout names its column \"busy_timeout\" (spike-verified), not \"timeout\" like real SQLite.\n const row = (await (await db.prepare('PRAGMA busy_timeout')).get()) as { busy_timeout: number };\n return { busy_timeout: `${row.busy_timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = await db.prepare(sql);\n stmt.safeIntegers(true); // int64 past 2^53 arrives as BigInt instead of losing precision\n return {\n columns: () => stmt.columns(),\n // sense sql streams through the client's own async generator.\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...params);\n },\n };\n },\n },\n async close() {\n // Checkpoint before measuring: the last reconcile's pages sit in the WAL, not the file yet.\n await checkpointWal(db);\n let reclaimPath: string | null = null;\n try {\n const path = await cacheFilePath(conn);\n const base = Number((await getMeta(conn, 'compact_size')) ?? '0');\n const size = path === null ? null : await fileSize(path);\n if (size !== null) {\n // No baseline yet (cold build or an upgraded cache): record it, never reclaim here, so a\n // cold build pays no VACUUM (PLAN 3.49 Track A) and an upgraded cache heals on a crossing.\n if (base === 0) {\n await setMeta(conn, 'compact_size', String(size));\n // The meta write landed past the checkpoint; leave no WAL for the next opener.\n await checkpointWal(db);\n } else if (size > BLOAT_FACTOR * base) {\n reclaimPath = path;\n }\n }\n } catch (err) {\n console.error(`sense: turso cache bloat check failed, the cache will keep its space until one succeeds: ${(err as Error).message}`);\n }\n await db.close();\n // After this store's connection is closed, never before: turso hands the file to one\n // connection at a time, and reclaimSpace opens its own to carry the `vacuum` flag (PLAN 3.54).\n if (reclaimPath !== null) await reclaimSpace(reclaimPath);\n },\n };\n}\n"],"names":["STORE_DIMS","getColumns","getMeta","setMeta","withTransaction","hasVectorRow","pendingRows","cacheFilePath","checkpointWal","fileSize","reclaimSpace","fieldStats","queryLexical","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","BLOAT_FACTOR","createStore","db","conn","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","_storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","raw","stmt","safeIntegers","iterate","params","close","reclaimPath","base","Number","size","String","err","console","error","message"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/store.ts"],"sourcesContent":["import type { Database } from '@tursodatabase/database';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { getColumns, getMeta, setMeta } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { cacheFilePath, checkpointWal, fileSize, reclaimSpace } from './connection.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\n// No 'segment': has()/basename() resolve via SQL rewrite below (sql-functions.ts), but segment() has no SQL form and the client cannot register it as a UDF.\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['vectors']);\n\n// close() reclaims once the cache file outgrows its compact size by this factor: the disk\n// overhead a user should accept (PLAN 3.52), in postgres autovacuum scale-factor shape.\nconst BLOAT_FACTOR = 1.5;\n\n// Shares one Connection instance (conn) with the builder's own reconcile call so transaction depth\n// (see transaction.ts) is tracked against the same object everywhere.\nexport function createStore(db: Database, conn: Connection): Store {\n return {\n name: 'turso',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n fieldStats: (columns, scopeWhere) => fieldStats(conn, columns, scopeWhere),\n },\n lexical: {\n query: (terms, opts) => queryLexical(conn, terms, opts),\n },\n // The column's fixed DDL width (STORE_DIMS) is what every scan binds against, not the\n // interface's per-call storeDims -- see vectors.ts's padded() for why a shorter vector is still correct against a wider column.\n vectors: {\n pending: () => pendingRows(conn),\n writeVectors: (rows) => writeVectorBatch(conn, STORE_DIMS, rows),\n candidates: (qv, _storeDims, fetch, allowed) => scanCandidates(conn, qv, STORE_DIMS, fetch, allowed),\n similar: (path, opts) => scanSimilar(conn, STORE_DIMS, path, opts),\n hasVector: (path) => hasVectorRow(conn, path),\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min). Turso's PRAGMA busy_timeout names its column \"busy_timeout\" (spike-verified), not \"timeout\" like real SQLite.\n const row = (await (await db.prepare('PRAGMA busy_timeout')).get()) as { busy_timeout: number };\n return { busy_timeout: `${row.busy_timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = await db.prepare(rewriteFunctions(sql));\n stmt.safeIntegers(true); // int64 past 2^53 arrives as BigInt instead of losing precision\n return {\n columns: () => stmt.columns(),\n // sense sql streams through the client's own async generator.\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...params);\n },\n };\n },\n },\n async close() {\n // Checkpoint before measuring: the last reconcile's pages sit in the WAL, not the file yet.\n await checkpointWal(db);\n let reclaimPath: string | null = null;\n try {\n const path = await cacheFilePath(conn);\n const base = Number((await getMeta(conn, 'compact_size')) ?? '0');\n const size = path === null ? null : await fileSize(path);\n if (size !== null) {\n // No baseline yet (cold build or an upgraded cache): record it, never reclaim here, so a\n // cold build pays no VACUUM (PLAN 3.49 Track A) and an upgraded cache heals on a crossing.\n if (base === 0) {\n await setMeta(conn, 'compact_size', String(size));\n // The meta write landed past the checkpoint; leave no WAL for the next opener.\n await checkpointWal(db);\n } else if (size > BLOAT_FACTOR * base) {\n reclaimPath = path;\n }\n }\n } catch (err) {\n console.error(`sense: turso cache bloat check failed, the cache will keep its space until one succeeds: ${(err as Error).message}`);\n }\n await db.close();\n // After this store's connection is closed, never before: turso hands the file to one\n // connection at a time, and reclaimSpace opens its own to carry the `vacuum` flag (PLAN 3.54).\n if (reclaimPath !== null) await reclaimSpace(reclaimPath);\n },\n };\n}\n"],"names":["STORE_DIMS","getColumns","getMeta","setMeta","withTransaction","hasVectorRow","pendingRows","cacheFilePath","checkpointWal","fileSize","reclaimSpace","fieldStats","queryLexical","rewriteFunctions","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","BLOAT_FACTOR","createStore","db","conn","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","_storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","raw","stmt","safeIntegers","iterate","params","close","reclaimPath","base","Number","size","String","err","console","error","message"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { asCosine, sampleEvenly } from '../vectors.js';
1
+ import { asCosine, compareVectorScores, sampleEvenly } from '../vectors.js';
2
2
  // Native F32_BLOB(dims) columns, dims fixed at DDL time (open.ts, STORE_DIMS). Both scans
3
3
  // score in JS: a per-row vector_distance_cos measured 3.5-8x slower on this row engine.
4
4
  // A model narrower than the column zero-pads: the added dimensions contribute nothing to
@@ -49,24 +49,30 @@ export async function writeVectorBatch(conn, dims, rows) {
49
49
  // is right but the bare start_line/end_line come from an arbitrary row in the group.
50
50
  export async function scanCandidates(conn, qv, dims, fetch, allowed) {
51
51
  const q = padded(qv, dims);
52
- const stmt = await conn.prepare('SELECT "path", start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL');
52
+ const stmt = await conn.prepare('SELECT "path", chunk, start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL ORDER BY "path", chunk');
53
53
  const rows = await stmt.all();
54
54
  const best = new Map();
55
55
  for (const row of rows){
56
56
  if (allowed && !allowed.has(row.path)) continue;
57
57
  const score = cosineSimilarity(q, decode(row.vector, dims));
58
58
  const existing = best.get(row.path);
59
- if (!existing || score > existing.score) best.set(row.path, {
60
- score,
61
- lines: `L${row.start_line}-${row.end_line}`
62
- });
59
+ if (!existing || score > existing.score || score === existing.score && row.chunk < existing.chunk) {
60
+ best.set(row.path, {
61
+ score,
62
+ chunk: row.chunk,
63
+ lines: `L${row.start_line}-${row.end_line}`
64
+ });
65
+ }
63
66
  }
64
67
  return [
65
68
  ...best.entries()
66
- ].sort((a, b)=>b[1].score - a[1].score).slice(0, fetch).map(([path, b])=>({
69
+ ].map(([path, value])=>({
70
+ path,
71
+ ...value
72
+ })).sort(compareVectorScores).slice(0, fetch).map(({ path, lines, score })=>({
67
73
  path,
68
- lines: b.lines,
69
- similarity: asCosine(b.score)
74
+ lines,
75
+ similarity: asCosine(score)
70
76
  }));
71
77
  }
72
78
  // Max cosine over (target chunk, other chunk) pairs. The SQL cross-join duckdb uses measured
@@ -91,7 +97,10 @@ export async function scanSimilar(conn, dims, path, opts) {
91
97
  }
92
98
  return [
93
99
  ...best.entries()
94
- ].sort((a, b)=>b[1] - a[1]).slice(0, opts.k).map(([p, score])=>({
100
+ ].map(([p, score])=>({
101
+ path: p,
102
+ score
103
+ })).sort(compareVectorScores).slice(0, opts.k).map(({ path: p, score })=>({
95
104
  path: p,
96
105
  similarity: asCosine(score)
97
106
  }));
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/vectors.ts"],"sourcesContent":["import type { Connection, VectorCandidate, VectorSimilar, VectorWriteRow } from '../types.ts';\nimport { asCosine, sampleEvenly } from '../vectors.ts';\n\n// Native F32_BLOB(dims) columns, dims fixed at DDL time (open.ts, STORE_DIMS). Both scans\n// score in JS: a per-row vector_distance_cos measured 3.5-8x slower on this row engine.\n\n// A model narrower than the column zero-pads: the added dimensions contribute nothing to\n// either dot product or norm, so cosine is unchanged.\nfunction padded(values: ArrayLike<number>, dims: number): Float32Array {\n const out = new Float32Array(dims);\n for (let d = 0; d < Math.min(values.length, dims); d++) out[d] = values[d];\n return out;\n}\n\n// A bound Float32Array stores truncated bytes with no error; a Buffer view over the same\n// bytes round-trips exact (measured 0.7.2).\nfunction toBlob(v: Float32Array): Buffer {\n return Buffer.from(v.buffer, v.byteOffset, v.byteLength);\n}\n\nfunction decode(vector: Buffer, dims: number): Float32Array {\n return new Float32Array(vector.buffer, vector.byteOffset, dims);\n}\n\n// Rows arrive int8-quantized with a per-vector scale (embed/query.ts's toStore); this store\n// dequantizes to floats, as duckdb does.\nfunction dequantize(row: VectorWriteRow, dims: number): Float32Array {\n const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);\n const out = new Float32Array(dims);\n for (let d = 0; d < Math.min(q.length, dims); d++) out[d] = q[d] * row.scale;\n return out;\n}\n\nfunction cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0;\n let na = 0;\n let nb = 0;\n for (let d = 0; d < a.length; d++) {\n dot += a[d] * b[d];\n na += a[d] * a[d];\n nb += b[d] * b[d];\n }\n return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-32);\n}\n\n// `scale` stays NULL: the column exists so the row shape matches the other stores, and\n// cosine is scale-invariant once the value is dequantized here.\nexport async function writeVectorBatch(conn: Connection, dims: number, rows: VectorWriteRow[]): Promise<void> {\n if (rows.length === 0) return;\n await conn.runBatch(\n 'UPDATE embeddings SET vector = ? WHERE \"path\" = ? AND chunk = ?',\n rows.map((row) => [toBlob(dequantize(row, dims)), row.path, row.chunk])\n );\n}\n\n// Best chunk per file by cosine. GROUP BY with MIN() cannot do this on 0.7.2: the aggregate\n// is right but the bare start_line/end_line come from an arbitrary row in the group.\nexport async function scanCandidates(conn: Connection, qv: Float32Array, dims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const q = padded(qv, dims);\n const stmt = await conn.prepare('SELECT \"path\", start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string; start_line: number; end_line: number; vector: Buffer }>;\n\n const best = new Map<string, { score: number; lines: string }>();\n for (const row of rows) {\n if (allowed && !allowed.has(row.path)) continue;\n const score = cosineSimilarity(q, decode(row.vector, dims));\n const existing = best.get(row.path);\n if (!existing || score > existing.score) best.set(row.path, { score, lines: `L${row.start_line}-${row.end_line}` });\n }\n return [...best.entries()]\n .sort((a, b) => b[1].score - a[1].score)\n .slice(0, fetch)\n .map(([path, b]) => ({ path, lines: b.lines, similarity: asCosine(b.score) }));\n}\n\n// Max cosine over (target chunk, other chunk) pairs. The SQL cross-join duckdb uses measured\n// 6-8x slower here at 26k chunks (2026-08-30).\nexport async function scanSimilar(conn: Connection, dims: number, path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]> {\n if (opts.allowed && opts.allowed.size === 0) return [];\n const targetStmt = await conn.prepare('SELECT vector FROM embeddings WHERE \"path\" = ? AND vector IS NOT NULL ORDER BY chunk');\n const targetRows = (await targetStmt.all(path)) as Array<{ vector: Buffer }>;\n if (targetRows.length === 0) return [];\n const targets = sampleEvenly(targetRows).map((row) => decode(row.vector, dims));\n\n const stmt = await conn.prepare('SELECT \"path\", vector FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string; vector: Buffer }>;\n const best = new Map<string, number>();\n for (const row of rows) {\n if (row.path === path || opts.exclude.has(row.path) || (opts.allowed && !opts.allowed.has(row.path))) continue;\n const other = decode(row.vector, dims);\n for (const t of targets) {\n const score = cosineSimilarity(t, other);\n const existing = best.get(row.path);\n if (existing === undefined || score > existing) best.set(row.path, score);\n }\n }\n\n return [...best.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, opts.k)\n .map(([p, score]) => ({ path: p, similarity: asCosine(score) }));\n}\n"],"names":["asCosine","sampleEvenly","padded","values","dims","out","Float32Array","d","Math","min","length","toBlob","v","Buffer","from","buffer","byteOffset","byteLength","decode","vector","dequantize","row","q","Int8Array","scale","cosineSimilarity","a","b","dot","na","nb","sqrt","writeVectorBatch","conn","rows","runBatch","map","path","chunk","scanCandidates","qv","fetch","allowed","stmt","prepare","all","best","Map","has","score","existing","get","set","lines","start_line","end_line","entries","sort","slice","similarity","scanSimilar","opts","size","targetStmt","targetRows","targets","exclude","other","t","undefined","k","p"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/vectors.ts"],"sourcesContent":["import type { Connection, VectorCandidate, VectorSimilar, VectorWriteRow } from '../types.ts';\nimport { asCosine, compareVectorScores, sampleEvenly } from '../vectors.ts';\n\n// Native F32_BLOB(dims) columns, dims fixed at DDL time (open.ts, STORE_DIMS). Both scans\n// score in JS: a per-row vector_distance_cos measured 3.5-8x slower on this row engine.\n\n// A model narrower than the column zero-pads: the added dimensions contribute nothing to\n// either dot product or norm, so cosine is unchanged.\nfunction padded(values: ArrayLike<number>, dims: number): Float32Array {\n const out = new Float32Array(dims);\n for (let d = 0; d < Math.min(values.length, dims); d++) out[d] = values[d];\n return out;\n}\n\n// A bound Float32Array stores truncated bytes with no error; a Buffer view over the same\n// bytes round-trips exact (measured 0.7.2).\nfunction toBlob(v: Float32Array): Buffer {\n return Buffer.from(v.buffer, v.byteOffset, v.byteLength);\n}\n\nfunction decode(vector: Buffer, dims: number): Float32Array {\n return new Float32Array(vector.buffer, vector.byteOffset, dims);\n}\n\n// Rows arrive int8-quantized with a per-vector scale (embed/query.ts's toStore); this store\n// dequantizes to floats, as duckdb does.\nfunction dequantize(row: VectorWriteRow, dims: number): Float32Array {\n const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);\n const out = new Float32Array(dims);\n for (let d = 0; d < Math.min(q.length, dims); d++) out[d] = q[d] * row.scale;\n return out;\n}\n\nfunction cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0;\n let na = 0;\n let nb = 0;\n for (let d = 0; d < a.length; d++) {\n dot += a[d] * b[d];\n na += a[d] * a[d];\n nb += b[d] * b[d];\n }\n return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-32);\n}\n\n// `scale` stays NULL: the column exists so the row shape matches the other stores, and\n// cosine is scale-invariant once the value is dequantized here.\nexport async function writeVectorBatch(conn: Connection, dims: number, rows: VectorWriteRow[]): Promise<void> {\n if (rows.length === 0) return;\n await conn.runBatch(\n 'UPDATE embeddings SET vector = ? WHERE \"path\" = ? AND chunk = ?',\n rows.map((row) => [toBlob(dequantize(row, dims)), row.path, row.chunk])\n );\n}\n\n// Best chunk per file by cosine. GROUP BY with MIN() cannot do this on 0.7.2: the aggregate\n// is right but the bare start_line/end_line come from an arbitrary row in the group.\nexport async function scanCandidates(conn: Connection, qv: Float32Array, dims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const q = padded(qv, dims);\n const stmt = await conn.prepare('SELECT \"path\", chunk, start_line, end_line, vector FROM embeddings WHERE vector IS NOT NULL ORDER BY \"path\", chunk');\n const rows = (await stmt.all()) as Array<{ path: string; chunk: number; start_line: number; end_line: number; vector: Buffer }>;\n\n const best = new Map<string, { score: number; chunk: number; lines: string }>();\n for (const row of rows) {\n if (allowed && !allowed.has(row.path)) continue;\n const score = cosineSimilarity(q, decode(row.vector, dims));\n const existing = best.get(row.path);\n if (!existing || score > existing.score || (score === existing.score && row.chunk < existing.chunk)) {\n best.set(row.path, { score, chunk: row.chunk, lines: `L${row.start_line}-${row.end_line}` });\n }\n }\n return [...best.entries()]\n .map(([path, value]) => ({ path, ...value }))\n .sort(compareVectorScores)\n .slice(0, fetch)\n .map(({ path, lines, score }) => ({ path, lines, similarity: asCosine(score) }));\n}\n\n// Max cosine over (target chunk, other chunk) pairs. The SQL cross-join duckdb uses measured\n// 6-8x slower here at 26k chunks (2026-08-30).\nexport async function scanSimilar(conn: Connection, dims: number, path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]> {\n if (opts.allowed && opts.allowed.size === 0) return [];\n const targetStmt = await conn.prepare('SELECT vector FROM embeddings WHERE \"path\" = ? AND vector IS NOT NULL ORDER BY chunk');\n const targetRows = (await targetStmt.all(path)) as Array<{ vector: Buffer }>;\n if (targetRows.length === 0) return [];\n const targets = sampleEvenly(targetRows).map((row) => decode(row.vector, dims));\n\n const stmt = await conn.prepare('SELECT \"path\", vector FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string; vector: Buffer }>;\n const best = new Map<string, number>();\n for (const row of rows) {\n if (row.path === path || opts.exclude.has(row.path) || (opts.allowed && !opts.allowed.has(row.path))) continue;\n const other = decode(row.vector, dims);\n for (const t of targets) {\n const score = cosineSimilarity(t, other);\n const existing = best.get(row.path);\n if (existing === undefined || score > existing) best.set(row.path, score);\n }\n }\n\n return [...best.entries()]\n .map(([p, score]) => ({ path: p, score }))\n .sort(compareVectorScores)\n .slice(0, opts.k)\n .map(({ path: p, score }) => ({ path: p, similarity: asCosine(score) }));\n}\n"],"names":["asCosine","compareVectorScores","sampleEvenly","padded","values","dims","out","Float32Array","d","Math","min","length","toBlob","v","Buffer","from","buffer","byteOffset","byteLength","decode","vector","dequantize","row","q","Int8Array","scale","cosineSimilarity","a","b","dot","na","nb","sqrt","writeVectorBatch","conn","rows","runBatch","map","path","chunk","scanCandidates","qv","fetch","allowed","stmt","prepare","all","best","Map","has","score","existing","get","set","lines","start_line","end_line","entries","value","sort","slice","similarity","scanSimilar","opts","size","targetStmt","targetRows","targets","exclude","other","t","undefined","p","k"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import type { Config, ResolvedConfig, StoreName } from '../config/index.js';
2
2
  import type { ReconcileDelta } from '../features/types.js';
3
3
  import type { ParsedDoc } from '../scan/index.js';
4
- export declare const CAPABILITY_NAMES: readonly ["phrases", "snippets", "lexical", "vectors", "sql-functions"];
4
+ export declare const CAPABILITY_NAMES: readonly ["vectors", "segment"];
5
5
  export type Capability = (typeof CAPABILITY_NAMES)[number];
6
6
  export interface RunResult {
7
7
  changes: number | bigint;
@@ -55,7 +55,6 @@ export interface DocumentStore {
55
55
  }
56
56
  export interface LexicalHit {
57
57
  path: string;
58
- hit: string | null;
59
58
  }
60
59
  export interface LexicalQueryOptions {
61
60
  whereJoin: string;
@@ -1,13 +1,11 @@
1
1
  // The backing-store interface: a minimal portable statement surface (exec/prepare), plus
2
2
  // dedicated interfaces exactly where engines diverge (lexical index, vector scan, raw sql).
3
- // 'lexical'/'vectors': the store's LexicalIndex/VectorStore is functionally implemented, not
4
- // present-but-inert. 'sql-functions': the engine can register has/basename/segment as UDFs at all,
5
- // which turso's client cannot. The rest are finer FTS5-only behaviors; a missing one fails at open
6
- // or first use. An array, not a bare union, so a runtime check reads the same list the type does.
3
+ // 'vectors': the store's VectorStore is functionally implemented, not present-but-inert; checked
4
+ // at open (index.ts) before a config with an "embed" block is allowed onto a store lacking it.
5
+ // 'segment': the engine can run segment() (Intl.Segmenter grapheme clustering) as a SQL function;
6
+ // turso's client cannot register it and has no rewrite for it, unlike has()/basename(), which now
7
+ // work everywhere. An array, not a bare union, so a runtime check reads the same list the type does.
7
8
  export const CAPABILITY_NAMES = [
8
- 'phrases',
9
- 'snippets',
10
- 'lexical',
11
9
  'vectors',
12
- 'sql-functions'
10
+ 'segment'
13
11
  ];