sensemaking 0.19.2 → 0.21.0

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 (221) hide show
  1. package/README.md +0 -1
  2. package/dist/cjs/chunk/extract.d.cts +1 -0
  3. package/dist/cjs/chunk/extract.d.ts +1 -0
  4. package/dist/cjs/chunk/extract.js +23 -5
  5. package/dist/cjs/chunk/extract.js.map +1 -1
  6. package/dist/cjs/chunk/index.d.cts +1 -1
  7. package/dist/cjs/chunk/index.d.ts +1 -1
  8. package/dist/cjs/chunk/index.js +3 -0
  9. package/dist/cjs/chunk/index.js.map +1 -1
  10. package/dist/cjs/config/access.d.cts +0 -1
  11. package/dist/cjs/config/access.d.ts +0 -1
  12. package/dist/cjs/config/access.js +10 -17
  13. package/dist/cjs/config/access.js.map +1 -1
  14. package/dist/cjs/config/index.d.cts +1 -1
  15. package/dist/cjs/config/index.d.ts +1 -1
  16. package/dist/cjs/config/index.js +0 -3
  17. package/dist/cjs/config/index.js.map +1 -1
  18. package/dist/cjs/config/types.d.cts +0 -3
  19. package/dist/cjs/config/types.d.ts +0 -3
  20. package/dist/cjs/config/types.js.map +1 -1
  21. package/dist/cjs/config/validate.js +6 -24
  22. package/dist/cjs/config/validate.js.map +1 -1
  23. package/dist/cjs/errors.d.cts +1 -1
  24. package/dist/cjs/errors.d.ts +1 -1
  25. package/dist/cjs/errors.js.map +1 -1
  26. package/dist/cjs/features/links.js +4 -5
  27. package/dist/cjs/features/links.js.map +1 -1
  28. package/dist/cjs/features/sections.js +4 -1
  29. package/dist/cjs/features/sections.js.map +1 -1
  30. package/dist/cjs/scan/pool.d.cts +15 -0
  31. package/dist/cjs/scan/pool.d.ts +15 -0
  32. package/dist/cjs/scan/pool.js +263 -0
  33. package/dist/cjs/scan/pool.js.map +1 -0
  34. package/dist/cjs/scan/reparse.d.cts +2 -0
  35. package/dist/cjs/scan/reparse.d.ts +2 -0
  36. package/dist/cjs/scan/reparse.js +15 -76
  37. package/dist/cjs/scan/reparse.js.map +1 -1
  38. package/dist/cjs/store/builder.d.cts +19 -0
  39. package/dist/cjs/store/builder.d.ts +19 -0
  40. package/dist/cjs/store/builder.js +618 -0
  41. package/dist/cjs/store/builder.js.map +1 -0
  42. package/dist/cjs/store/duckdb/lexical.d.cts +1 -0
  43. package/dist/cjs/store/duckdb/lexical.d.ts +1 -0
  44. package/dist/cjs/store/duckdb/lexical.js +21 -5
  45. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  46. package/dist/cjs/store/duckdb/open.d.cts +9 -8
  47. package/dist/cjs/store/duckdb/open.d.ts +9 -8
  48. package/dist/cjs/store/duckdb/open.js +77 -123
  49. package/dist/cjs/store/duckdb/open.js.map +1 -1
  50. package/dist/cjs/store/duckdb/reconcile.js +34 -5
  51. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  52. package/dist/cjs/store/duckdb/store.d.cts +1 -2
  53. package/dist/cjs/store/duckdb/store.d.ts +1 -2
  54. package/dist/cjs/store/duckdb/store.js +1 -26
  55. package/dist/cjs/store/duckdb/store.js.map +1 -1
  56. package/dist/cjs/store/embed-scope.d.cts +2 -0
  57. package/dist/cjs/store/embed-scope.d.ts +2 -0
  58. package/dist/cjs/store/embed-scope.js +85 -0
  59. package/dist/cjs/store/embed-scope.js.map +1 -0
  60. package/dist/cjs/store/feature-scope.d.cts +8 -0
  61. package/dist/cjs/store/feature-scope.d.ts +8 -0
  62. package/dist/cjs/store/feature-scope.js +107 -0
  63. package/dist/cjs/store/feature-scope.js.map +1 -0
  64. package/dist/cjs/store/index.d.cts +8 -3
  65. package/dist/cjs/store/index.d.ts +8 -3
  66. package/dist/cjs/store/index.js +36 -7
  67. package/dist/cjs/store/index.js.map +1 -1
  68. package/dist/cjs/store/open.d.cts +15 -0
  69. package/dist/cjs/store/open.d.ts +15 -0
  70. package/dist/cjs/store/open.js +608 -0
  71. package/dist/cjs/store/open.js.map +1 -0
  72. package/dist/cjs/store/preset-scope.d.cts +3 -0
  73. package/dist/cjs/store/preset-scope.d.ts +3 -0
  74. package/dist/cjs/store/preset-scope.js +293 -0
  75. package/dist/cjs/store/preset-scope.js.map +1 -0
  76. package/dist/cjs/store/reconcile.d.cts +2 -1
  77. package/dist/cjs/store/reconcile.d.ts +2 -1
  78. package/dist/cjs/store/reconcile.js +175 -179
  79. package/dist/cjs/store/reconcile.js.map +1 -1
  80. package/dist/cjs/store/{sqlite/signature.d.cts → signature.d.cts} +0 -3
  81. package/dist/cjs/store/{sqlite/signature.d.ts → signature.d.ts} +0 -3
  82. package/dist/cjs/store/signature.js +164 -0
  83. package/dist/cjs/store/signature.js.map +1 -0
  84. package/dist/cjs/store/sqlite/lexical.d.cts +1 -1
  85. package/dist/cjs/store/sqlite/lexical.d.ts +1 -1
  86. package/dist/cjs/store/sqlite/lexical.js +2 -2
  87. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  88. package/dist/cjs/store/sqlite/open.d.cts +8 -9
  89. package/dist/cjs/store/sqlite/open.d.ts +8 -9
  90. package/dist/cjs/store/sqlite/open.js +82 -311
  91. package/dist/cjs/store/sqlite/open.js.map +1 -1
  92. package/dist/cjs/store/sqlite/reconcile.d.cts +1 -1
  93. package/dist/cjs/store/sqlite/reconcile.d.ts +1 -1
  94. package/dist/cjs/store/sqlite/reconcile.js +78 -16
  95. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  96. package/dist/cjs/store/sqlite/sql-functions.d.cts +1 -1
  97. package/dist/cjs/store/sqlite/sql-functions.d.ts +1 -1
  98. package/dist/cjs/store/sqlite/sql-functions.js +3 -3
  99. package/dist/cjs/store/sqlite/sql-functions.js.map +1 -1
  100. package/dist/cjs/store/sqlite/store.d.cts +1 -2
  101. package/dist/cjs/store/sqlite/store.d.ts +1 -2
  102. package/dist/cjs/store/sqlite/store.js +2 -15
  103. package/dist/cjs/store/sqlite/store.js.map +1 -1
  104. package/dist/cjs/store/turso/open.d.cts +6 -11
  105. package/dist/cjs/store/turso/open.d.ts +6 -11
  106. package/dist/cjs/store/turso/open.js +85 -171
  107. package/dist/cjs/store/turso/open.js.map +1 -1
  108. package/dist/cjs/store/turso/reconcile.d.cts +2 -0
  109. package/dist/cjs/store/turso/reconcile.d.ts +2 -0
  110. package/dist/cjs/store/turso/reconcile.js +88 -8
  111. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  112. package/dist/cjs/store/turso/store.d.cts +1 -2
  113. package/dist/cjs/store/turso/store.d.ts +1 -2
  114. package/dist/cjs/store/turso/store.js +1 -13
  115. package/dist/cjs/store/turso/store.js.map +1 -1
  116. package/dist/cjs/store/types.d.cts +16 -6
  117. package/dist/cjs/store/types.d.ts +16 -6
  118. package/dist/cjs/store/types.js.map +1 -1
  119. package/dist/cjs/text/strip.js +3 -6
  120. package/dist/cjs/text/strip.js.map +1 -1
  121. package/dist/cjs/watch.js +28 -16
  122. package/dist/cjs/watch.js.map +1 -1
  123. package/dist/esm/chunk/extract.d.ts +1 -0
  124. package/dist/esm/chunk/extract.js +10 -2
  125. package/dist/esm/chunk/extract.js.map +1 -1
  126. package/dist/esm/chunk/index.d.ts +1 -1
  127. package/dist/esm/chunk/index.js +1 -1
  128. package/dist/esm/chunk/index.js.map +1 -1
  129. package/dist/esm/config/access.d.ts +0 -1
  130. package/dist/esm/config/access.js +12 -17
  131. package/dist/esm/config/access.js.map +1 -1
  132. package/dist/esm/config/index.d.ts +1 -1
  133. package/dist/esm/config/index.js +1 -1
  134. package/dist/esm/config/index.js.map +1 -1
  135. package/dist/esm/config/types.d.ts +0 -3
  136. package/dist/esm/config/types.js.map +1 -1
  137. package/dist/esm/config/validate.js +6 -22
  138. package/dist/esm/config/validate.js.map +1 -1
  139. package/dist/esm/errors.d.ts +1 -1
  140. package/dist/esm/errors.js.map +1 -1
  141. package/dist/esm/features/links.js +3 -4
  142. package/dist/esm/features/links.js.map +1 -1
  143. package/dist/esm/features/sections.js +4 -1
  144. package/dist/esm/features/sections.js.map +1 -1
  145. package/dist/esm/scan/pool.d.ts +15 -0
  146. package/dist/esm/scan/pool.js +77 -0
  147. package/dist/esm/scan/pool.js.map +1 -0
  148. package/dist/esm/scan/reparse.d.ts +2 -0
  149. package/dist/esm/scan/reparse.js +11 -58
  150. package/dist/esm/scan/reparse.js.map +1 -1
  151. package/dist/esm/store/builder.d.ts +19 -0
  152. package/dist/esm/store/builder.js +163 -0
  153. package/dist/esm/store/builder.js.map +1 -0
  154. package/dist/esm/store/duckdb/lexical.d.ts +1 -0
  155. package/dist/esm/store/duckdb/lexical.js +22 -9
  156. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  157. package/dist/esm/store/duckdb/open.d.ts +9 -8
  158. package/dist/esm/store/duckdb/open.js +36 -58
  159. package/dist/esm/store/duckdb/open.js.map +1 -1
  160. package/dist/esm/store/duckdb/reconcile.js +15 -3
  161. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  162. package/dist/esm/store/duckdb/store.d.ts +1 -2
  163. package/dist/esm/store/duckdb/store.js +2 -11
  164. package/dist/esm/store/duckdb/store.js.map +1 -1
  165. package/dist/esm/store/embed-scope.d.ts +2 -0
  166. package/dist/esm/store/embed-scope.js +28 -0
  167. package/dist/esm/store/embed-scope.js.map +1 -0
  168. package/dist/esm/store/feature-scope.d.ts +8 -0
  169. package/dist/esm/store/feature-scope.js +49 -0
  170. package/dist/esm/store/feature-scope.js.map +1 -0
  171. package/dist/esm/store/index.d.ts +8 -3
  172. package/dist/esm/store/index.js +22 -7
  173. package/dist/esm/store/index.js.map +1 -1
  174. package/dist/esm/store/open.d.ts +15 -0
  175. package/dist/esm/store/open.js +187 -0
  176. package/dist/esm/store/open.js.map +1 -0
  177. package/dist/esm/store/preset-scope.d.ts +3 -0
  178. package/dist/esm/store/preset-scope.js +77 -0
  179. package/dist/esm/store/preset-scope.js.map +1 -0
  180. package/dist/esm/store/reconcile.d.ts +2 -1
  181. package/dist/esm/store/reconcile.js +33 -6
  182. package/dist/esm/store/reconcile.js.map +1 -1
  183. package/dist/esm/store/{sqlite/signature.d.ts → signature.d.ts} +0 -3
  184. package/dist/esm/store/signature.js +37 -0
  185. package/dist/esm/store/signature.js.map +1 -0
  186. package/dist/esm/store/sqlite/lexical.d.ts +1 -1
  187. package/dist/esm/store/sqlite/lexical.js +4 -4
  188. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  189. package/dist/esm/store/sqlite/open.d.ts +8 -9
  190. package/dist/esm/store/sqlite/open.js +41 -147
  191. package/dist/esm/store/sqlite/open.js.map +1 -1
  192. package/dist/esm/store/sqlite/reconcile.d.ts +1 -1
  193. package/dist/esm/store/sqlite/reconcile.js +14 -14
  194. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  195. package/dist/esm/store/sqlite/sql-functions.d.ts +1 -1
  196. package/dist/esm/store/sqlite/sql-functions.js +3 -3
  197. package/dist/esm/store/sqlite/sql-functions.js.map +1 -1
  198. package/dist/esm/store/sqlite/store.d.ts +1 -2
  199. package/dist/esm/store/sqlite/store.js +3 -9
  200. package/dist/esm/store/sqlite/store.js.map +1 -1
  201. package/dist/esm/store/turso/open.d.ts +6 -11
  202. package/dist/esm/store/turso/open.js +34 -78
  203. package/dist/esm/store/turso/open.js.map +1 -1
  204. package/dist/esm/store/turso/reconcile.d.ts +2 -0
  205. package/dist/esm/store/turso/reconcile.js +18 -3
  206. package/dist/esm/store/turso/reconcile.js.map +1 -1
  207. package/dist/esm/store/turso/store.d.ts +1 -2
  208. package/dist/esm/store/turso/store.js +2 -7
  209. package/dist/esm/store/turso/store.js.map +1 -1
  210. package/dist/esm/store/types.d.ts +16 -6
  211. package/dist/esm/store/types.js.map +1 -1
  212. package/dist/esm/text/strip.js +3 -4
  213. package/dist/esm/text/strip.js.map +1 -1
  214. package/dist/esm/watch.js +5 -3
  215. package/dist/esm/watch.js.map +1 -1
  216. package/package.json +4 -3
  217. package/schema.json +0 -11
  218. package/dist/cjs/store/sqlite/signature.js +0 -380
  219. package/dist/cjs/store/sqlite/signature.js.map +0 -1
  220. package/dist/esm/store/sqlite/signature.js +0 -58
  221. package/dist/esm/store/sqlite/signature.js.map +0 -1
@@ -1,13 +1,12 @@
1
+ import { DatabaseSync } from 'node:sqlite';
1
2
  import type { ResolvedConfig } from '../../config/index.js';
2
- import type { Store } from '../types.js';
3
+ import type { OpenResult } from '../open.js';
4
+ import type { OpenDialect } from '../types.js';
3
5
  export declare const DB_FILENAME = "cache.db";
4
- export declare const SCHEMA_VERSION = "18";
5
- export interface OpenResult {
6
- store: Store;
7
- cfg: ResolvedConfig;
8
- dbPath: string;
9
- parsed: number;
10
- warnings: string[];
6
+ export declare const SCHEMA_VERSION = "19";
7
+ export type { OpenResult };
8
+ interface SqliteHandle {
9
+ db: DatabaseSync;
11
10
  }
11
+ export declare const sqliteOpenDialect: OpenDialect<SqliteHandle>;
12
12
  export declare function openSqlite(cfg: ResolvedConfig): Promise<OpenResult>;
13
- export declare function docCount(store: Store): Promise<number>;
@@ -1,63 +1,32 @@
1
1
  // The Node floor (>=22.20) is explained here and nowhere else: 22.20 is the first release with
2
2
  // both FTS5 and row-returning INSERT ... RETURNING. Raise it only for a load-bearing capability.
3
- import { mkdirSync } from 'node:fs';
4
- import { join } from 'node:path';
5
3
  import { DatabaseSync } from 'node:sqlite';
6
- import { contentTokenize, featureSignature, STATE_DIR } from '../../config/index.js';
7
- import { rekeyChunkText } from '../../embed/handoff.js';
8
- import { SenseError } from '../../errors.js';
4
+ import { featureSignature } from '../../config/index.js';
9
5
  import { activeFeatures, FEATURES } from '../../features/index.js';
10
- import { clearCache } from '../cache.js';
11
- import { reconcile } from '../reconcile.js';
6
+ import { openWithDialect } from '../open.js';
12
7
  import { getMeta, setMeta } from '../shared.js';
13
8
  import { BEGIN_WRITE, withTransaction } from '../transaction.js';
14
9
  import { createConnection } from './connection.js';
15
10
  import { sqliteDialect } from './reconcile.js';
16
- import { changedSignatureKeys, embedIdentityAdopted, rebuildContentTable, signatureDiff } from './signature.js';
17
11
  import { registerFunctions } from './sql-functions.js';
18
12
  import { createStore } from './store.js';
19
13
  export const DB_FILENAME = 'cache.db';
20
14
  // Cache shape version, independent of the config's own `version`. Bumping it rebuilds
21
15
  // existing trees on first query.
22
- export const SCHEMA_VERSION = '18';
16
+ export const SCHEMA_VERSION = '19';
23
17
  // unicode61 splits on spaces, so a language written without them indexes a whole run as one
24
- // token and word search finds nothing; `content.tokenize` is how such a tree picks trigram.
25
- const DEFAULT_TOKENIZE = 'porter unicode61';
26
- // FTS5 takes its tokenizer as a DDL string literal where nothing can bind, so the configured
27
- // value is concatenated; probing a throwaway table first is both the safety and the validation.
28
- function resolveTokenize(db, cfg) {
29
- const configured = contentTokenize(cfg);
30
- if (configured === undefined) return DEFAULT_TOKENIZE;
31
- const literal = configured.replace(/'/g, "''");
32
- try {
33
- db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');
34
- db.exec(`CREATE VIRTUAL TABLE temp.sense_tokenize_probe USING fts5(x, tokenize = '${literal}')`);
35
- db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');
36
- } catch (err) {
37
- throw new SenseError('CONFIG_INVALID', `content.tokenize "${configured}" is not a tokenizer this SQLite accepts (${err.message}); the built-in choices are unicode61, ascii, porter, and trigram, each with their own options`);
38
- }
39
- return literal;
40
- }
41
- // The tokenizer the content table was actually built with, from its own DDL -- the one
42
- // record that cannot desynchronize from the table. NULL when the table does not exist yet.
43
- function storedTokenize(db) {
44
- const row = db.prepare(`SELECT sql FROM sqlite_master WHERE name = 'content'`).get();
45
- if (!row) return null;
46
- const m = row.sql.match(/tokenize = '((?:[^']|'')*)'/);
47
- return m ? m[1] : null;
48
- }
18
+ // token and word search finds nothing; the `_seg` sidecars (always populated) cover that case.
19
+ const TOKENIZE = 'porter unicode61';
49
20
  // The `_seg` sidecars are appended after path, never inserted: bm25() and snippet(content, 2)
50
21
  // are documented against the first three columns. Each holds its field's exploded unspaced runs.
51
- async function createContentTable(conn, tokenize) {
52
- 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}')`);
22
+ async function createContentTable(conn) {
23
+ 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}')`);
53
24
  }
54
25
  // Content is a separate table (not a column on frontmatter) so `SELECT * FROM frontmatter`
55
26
  // can't dump file text into context. Features add their own tables after the core ones.
56
- async function ensureSchema(conn, cfg, tokenize) {
27
+ async function ensureSchemaTables(conn, cfg) {
57
28
  await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_ctime" REAL, "_size" INTEGER, "_parse_error" TEXT)`);
58
- // IF NOT EXISTS is safe against a tokenizer change: open() compares the table's own DDL
59
- // against the resolved tokenizer before this runs, so a stale table is already gone by now.
60
- await createContentTable(conn, tokenize);
29
+ await createContentTable(conn);
61
30
  // Coverage, not ownership: a path can appear under several presets. path leads the PK so the
62
31
  // per-doc delete is an index hit -- keyed the other way, cold builds went quadratic.
63
32
  await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
@@ -80,127 +49,52 @@ function setJournalWal(db) {
80
49
  }
81
50
  }
82
51
  }
83
- async function connect(cfg) {
84
- const stateDir = join(cfg.baseDir, STATE_DIR);
85
- mkdirSync(stateDir, {
86
- recursive: true
87
- });
88
- const dbPath = join(stateDir, DB_FILENAME);
52
+ async function connect(dbPath, _cfg) {
89
53
  const db = new DatabaseSync(dbPath);
90
- // A throw below must release this handle, or on Windows the leaked WAL db is undeletable and
91
- // scratch cleanup fails with EPERM/EBUSY. `closed` keeps the catch from double-closing.
92
- let closed = false;
93
54
  try {
94
- var _ref;
95
55
  // Before journal_mode, not after: converting a fresh database to WAL takes a brief exclusive
96
56
  // lock, and with no timeout set yet a second process opening the same tree fails in 1ms.
97
57
  db.exec('PRAGMA busy_timeout = 30000');
98
58
  // busy_timeout does not cover the WAL conversion itself: SQLite does not invoke the busy
99
59
  // handler for it, so a concurrent cold open needs its own bounded wait.
100
60
  setJournalWal(db);
101
- registerFunctions(db, contentTokenize(cfg) === undefined);
61
+ registerFunctions(db);
102
62
  const conn = createConnection(db);
103
- db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
104
- // Before the rebuild branch below, never after: that branch deletes the cache, so a typo'd
105
- // tokenizer validated later would cost a full re-index (and re-embed) to reach its own error.
106
- const tokenize = resolveTokenize(db, cfg);
107
- // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an
108
- // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).
109
- const version = await getMeta(conn, 'schema_version');
110
- const features = await getMeta(conn, 'features');
111
- const wantFeatures = featureSignature(cfg, FEATURES);
112
- let tokenizeOnlyRebuild = false;
113
- if (version !== null && version !== SCHEMA_VERSION || features !== null && features !== wantFeatures) {
114
- // Indexing derives from presets, so a config edit rebuilding the cache must say so and
115
- // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.
116
- if (version !== null && version !== SCHEMA_VERSION) {
117
- console.error('sense: cache format changed (new sensemaking version); rebuilding the index');
118
- closed = true;
119
- db.close();
120
- clearCache(cfg);
121
- return connect(cfg);
122
- }
123
- const changedKeys = changedSignatureKeys(features !== null && features !== void 0 ? features : '', wantFeatures);
124
- // Only the tokenizer moved, and frontmatter, links, sections and embeddings are file-derived
125
- // and tokenizer-independent. Any other signature change takes the full clear/reopen below.
126
- if (changedKeys.size === 1 && changedKeys.has('tokenize')) {
127
- console.error('sense: config change (content tokenizer) rebuilds the text index; vectors, links, and sections are kept');
128
- // One transaction, so a crash before COMMIT rolls back to the old tokenizer's table rather
129
- // than to no table. IF EXISTS makes a retry after such a crash a no-op, not a raw error.
130
- await withTransaction(conn, async ()=>{
131
- await conn.exec('DROP TABLE IF EXISTS content');
132
- await createContentTable(conn, tokenize);
133
- }, BEGIN_WRITE);
134
- tokenizeOnlyRebuild = true;
135
- } else if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features !== null && features !== void 0 ? features : '', wantFeatures)) {
136
- // First sight of a resolved weight identity: the model itself hasn't changed, so
137
- // adopt it into meta with no rebuild and no re-embed, mirroring the tokenize precedent.
138
- console.error("sense: recorded the embedding model's resolved identity; vectors are unaffected");
139
- await setMeta(conn, 'features', wantFeatures);
140
- } else {
141
- const changed = signatureDiff(features !== null && features !== void 0 ? features : '', wantFeatures);
142
- console.error(`sense: config change (${changed}) rebuilds the index`);
143
- closed = true;
144
- db.close();
145
- clearCache(cfg);
146
- return connect(cfg);
147
- }
148
- }
149
- // Meta can lie after a crash between table creation and the signature write; the table's own
150
- // DDL cannot, so a mismatch here rebuilds whatever meta says.
151
- const stored = storedTokenize(db);
152
- if (stored !== null && stored !== tokenize) {
153
- console.error('sense: cache was built with a different content tokenizer; rebuilding the index');
154
- closed = true;
155
- db.close();
156
- clearCache(cfg);
157
- return connect(cfg);
158
- }
159
- // One writer at a time: the feature hooks check a column then add it, so two cold opens racing
160
- // here both see it missing and the second ALTER fails with a duplicate column.
161
- await withTransaction(conn, ()=>ensureSchema(conn, cfg, tokenize), BEGIN_WRITE);
162
- let rebuildWarnings = [];
163
- if (tokenizeOnlyRebuild) {
164
- rebuildWarnings = await rebuildContentTable(conn, cfg, cfg.baseDir);
165
- await setMeta(conn, 'features', wantFeatures);
166
- }
167
- // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.
168
- // Installed before reconcile() -- that call is the one that races a watcher's transaction.
169
- const recordedMaxMs = Number((_ref = await getMeta(conn, 'reconcile_max_ms')) !== null && _ref !== void 0 ? _ref : '0');
170
- db.exec(`PRAGMA busy_timeout = ${Math.min(Math.max(30000, 3 * recordedMaxMs), 600000)}`);
171
- const { parsed, warnings } = await reconcile(conn, cfg, cfg.baseDir, sqliteDialect);
172
63
  return {
173
- db,
174
- conn,
175
- cfg,
176
- dbPath,
177
- parsed,
178
- warnings: [
179
- ...rebuildWarnings,
180
- ...warnings
181
- ]
64
+ handle: {
65
+ db
66
+ },
67
+ conn
182
68
  };
183
69
  } catch (err) {
184
- if (!closed) db.close();
70
+ // A throw here must release this handle, or on Windows the leaked WAL db is undeletable and
71
+ // scratch cleanup fails with EPERM/EBUSY.
72
+ db.close();
185
73
  throw err;
186
74
  }
187
75
  }
188
- // The sqlite store's open: connects synchronously (see connect() above), then wraps the
189
- // resulting connection in the async Store interface.
190
- export async function openSqlite(cfg) {
191
- const { db, conn, cfg: resolvedCfg, dbPath, parsed, warnings } = await connect(cfg);
192
- const store = createStore(db, conn, resolvedCfg, resolvedCfg.baseDir);
193
- // reconcile ran before this object existed, so its chunk text is keyed by the connection.
194
- rekeyChunkText(conn, store);
195
- return {
196
- store: store,
197
- cfg: resolvedCfg,
198
- dbPath,
199
- parsed,
200
- warnings
201
- };
76
+ async function close(handle) {
77
+ handle.db.close();
202
78
  }
203
- export async function docCount(store) {
204
- const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');
205
- return (await stmt.get()).n;
79
+ async function ensureSchema(_handle, conn, cfg) {
80
+ // One writer at a time: the feature hooks check a column then add it, so two cold opens racing
81
+ // here both see it missing and the second ALTER fails with a duplicate column.
82
+ await withTransaction(conn, ()=>ensureSchemaTables(conn, cfg), BEGIN_WRITE);
83
+ }
84
+ async function setDerivedBusyTimeout(handle, _conn, ms) {
85
+ handle.db.exec(`PRAGMA busy_timeout = ${ms}`);
86
+ }
87
+ // This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.
88
+ export const sqliteOpenDialect = {
89
+ filename: DB_FILENAME,
90
+ schemaVersion: SCHEMA_VERSION,
91
+ reconcileDialect: sqliteDialect,
92
+ connect,
93
+ close,
94
+ ensureSchema,
95
+ setDerivedBusyTimeout,
96
+ createStore: (handle, conn)=>createStore(handle.db, conn)
97
+ };
98
+ export async function openSqlite(cfg) {
99
+ return openWithDialect(cfg, sqliteOpenDialect);
206
100
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/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 { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { contentTokenize, featureSignature, STATE_DIR } from '../../config/index.ts';\nimport { rekeyChunkText } from '../../embed/handoff.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport { clearCache } from '../cache.ts';\nimport { reconcile } from '../reconcile.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, Store } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { sqliteDialect } from './reconcile.ts';\nimport { changedSignatureKeys, embedIdentityAdopted, rebuildContentTable, signatureDiff } from './signature.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 = '18';\n\ninterface ConnectResult {\n db: DatabaseSync;\n conn: Connection;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\n\nexport interface OpenResult {\n store: Store;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n}\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; `content.tokenize` is how such a tree picks trigram.\nconst DEFAULT_TOKENIZE = 'porter unicode61';\n\n// FTS5 takes its tokenizer as a DDL string literal where nothing can bind, so the configured\n// value is concatenated; probing a throwaway table first is both the safety and the validation.\nfunction resolveTokenize(db: DatabaseSync, cfg: Config): string {\n const configured = contentTokenize(cfg);\n if (configured === undefined) return DEFAULT_TOKENIZE;\n const literal = configured.replace(/'/g, \"''\");\n try {\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n db.exec(`CREATE VIRTUAL TABLE temp.sense_tokenize_probe USING fts5(x, tokenize = '${literal}')`);\n db.exec('DROP TABLE IF EXISTS temp.sense_tokenize_probe');\n } catch (err) {\n throw new SenseError('CONFIG_INVALID', `content.tokenize \"${configured}\" is not a tokenizer this SQLite accepts (${(err as Error).message}); the built-in choices are unicode61, ascii, porter, and trigram, each with their own options`);\n }\n return literal;\n}\n\n// The tokenizer the content table was actually built with, from its own DDL -- the one\n// record that cannot desynchronize from the table. NULL when the table does not exist yet.\nfunction storedTokenize(db: DatabaseSync): string | null {\n const row = db.prepare(`SELECT sql FROM sqlite_master WHERE name = 'content'`).get() as { sql: string } | undefined;\n if (!row) return null;\n const m = row.sql.match(/tokenize = '((?:[^']|'')*)'/);\n return m ? m[1] : null;\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, tokenize: string): 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. Features add their own tables after the core ones.\nasync function ensureSchema(conn: Connection, cfg: Config, tokenize: string): 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 // IF NOT EXISTS is safe against a tokenizer change: open() compares the table's own DDL\n // against the resolved tokenizer before this runs, so a stale table is already gone by now.\n await createContentTable(conn, tokenize);\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 for (const feature of activeFeatures(cfg)) await feature.schema(conn);\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\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(cfg: ResolvedConfig): Promise<ConnectResult> {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n // A throw below must release this handle, or on Windows the leaked WAL db is undeletable and\n // scratch cleanup fails with EPERM/EBUSY. `closed` keeps the catch from double-closing.\n let closed = false;\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 setJournalWal(db);\n registerFunctions(db, contentTokenize(cfg) === undefined);\n const conn = createConnection(db);\n\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Before the rebuild branch below, never after: that branch deletes the cache, so a typo'd\n // tokenizer validated later would cost a full re-index (and re-embed) to reach its own error.\n const tokenize = resolveTokenize(db, cfg);\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = await getMeta(conn, 'schema_version');\n const features = await getMeta(conn, 'features');\n const wantFeatures = featureSignature(cfg, FEATURES);\n let tokenizeOnlyRebuild = false;\n if ((version !== null && version !== SCHEMA_VERSION) || (features !== null && features !== wantFeatures)) {\n // Indexing derives from presets, so a config edit rebuilding the cache must say so and\n // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.\n if (version !== null && version !== SCHEMA_VERSION) {\n console.error('sense: cache format changed (new sensemaking version); rebuilding the index');\n closed = true;\n db.close();\n clearCache(cfg);\n return connect(cfg);\n }\n const changedKeys = changedSignatureKeys(features ?? '', wantFeatures);\n // Only the tokenizer moved, and frontmatter, links, sections and embeddings are file-derived\n // and tokenizer-independent. Any other signature change takes the full clear/reopen below.\n if (changedKeys.size === 1 && changedKeys.has('tokenize')) {\n console.error('sense: config change (content tokenizer) rebuilds the text index; vectors, links, and sections are kept');\n // One transaction, so a crash before COMMIT rolls back to the old tokenizer's table rather\n // than to no table. IF EXISTS makes a retry after such a crash a no-op, not a raw error.\n await withTransaction(\n conn,\n async () => {\n await conn.exec('DROP TABLE IF EXISTS content');\n await createContentTable(conn, tokenize);\n },\n BEGIN_WRITE\n );\n tokenizeOnlyRebuild = true;\n } else if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features ?? '', wantFeatures)) {\n // First sight of a resolved weight identity: the model itself hasn't changed, so\n // adopt it into meta with no rebuild and no re-embed, mirroring the tokenize precedent.\n console.error(\"sense: recorded the embedding model's resolved identity; vectors are unaffected\");\n await setMeta(conn, 'features', wantFeatures);\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n closed = true;\n db.close();\n clearCache(cfg);\n return connect(cfg);\n }\n }\n\n // Meta can lie after a crash between table creation and the signature write; the table's own\n // DDL cannot, so a mismatch here rebuilds whatever meta says.\n const stored = storedTokenize(db);\n if (stored !== null && stored !== tokenize) {\n console.error('sense: cache was built with a different content tokenizer; rebuilding the index');\n closed = true;\n db.close();\n clearCache(cfg);\n return connect(cfg);\n }\n\n // One writer at a time: the feature hooks check a column then add it, so two cold opens racing\n // here both see it missing and the second ALTER fails with a duplicate column.\n await withTransaction(conn, () => ensureSchema(conn, cfg, tokenize), BEGIN_WRITE);\n\n let rebuildWarnings: string[] = [];\n if (tokenizeOnlyRebuild) {\n rebuildWarnings = await rebuildContentTable(conn, cfg, cfg.baseDir);\n await setMeta(conn, 'features', wantFeatures);\n }\n\n // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.\n // Installed before reconcile() -- that call is the one that races a watcher's transaction.\n const recordedMaxMs = Number((await getMeta(conn, 'reconcile_max_ms')) ?? '0');\n db.exec(`PRAGMA busy_timeout = ${Math.min(Math.max(30000, 3 * recordedMaxMs), 600_000)}`);\n\n const { parsed, warnings } = await reconcile(conn, cfg, cfg.baseDir, sqliteDialect);\n\n return { db, conn, cfg, dbPath, parsed, warnings: [...rebuildWarnings, ...warnings] };\n } catch (err) {\n if (!closed) db.close();\n throw err;\n }\n}\n\n// The sqlite store's open: connects synchronously (see connect() above), then wraps the\n// resulting connection in the async Store interface.\nexport async function openSqlite(cfg: ResolvedConfig): Promise<OpenResult> {\n const { db, conn, cfg: resolvedCfg, dbPath, parsed, warnings } = await connect(cfg);\n const store = createStore(db, conn, resolvedCfg, resolvedCfg.baseDir);\n // reconcile ran before this object existed, so its chunk text is keyed by the connection.\n rekeyChunkText(conn, store);\n return { store: store, cfg: resolvedCfg, dbPath, parsed, warnings };\n}\n\nexport async function docCount(store: Store): Promise<number> {\n const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');\n return ((await stmt.get()) as { n: number }).n;\n}\n"],"names":["mkdirSync","join","DatabaseSync","contentTokenize","featureSignature","STATE_DIR","rekeyChunkText","SenseError","activeFeatures","FEATURES","clearCache","reconcile","getMeta","setMeta","BEGIN_WRITE","withTransaction","createConnection","sqliteDialect","changedSignatureKeys","embedIdentityAdopted","rebuildContentTable","signatureDiff","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","DEFAULT_TOKENIZE","resolveTokenize","db","cfg","configured","undefined","literal","replace","exec","err","message","storedTokenize","row","prepare","get","m","sql","match","createContentTable","conn","tokenize","ensureSchema","feature","schema","setJournalWal","deadline","Date","now","test","Atomics","wait","Int32Array","SharedArrayBuffer","connect","stateDir","baseDir","recursive","dbPath","closed","version","features","wantFeatures","tokenizeOnlyRebuild","console","error","close","changedKeys","size","has","changed","stored","rebuildWarnings","recordedMaxMs","Number","Math","min","max","parsed","warnings","openSqlite","resolvedCfg","store","docCount","stmt","n"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/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 { featureSignature } from '../../config/index.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 { 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 = '19';\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. Features add their own tables after the core ones.\nasync function ensureSchemaTables(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 createContentTable(conn);\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 for (const feature of activeFeatures(cfg)) await feature.schema(conn);\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\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): Promise<{ handle: SqliteHandle; conn: Connection }> {\n const db = new DatabaseSync(dbPath);\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 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 ensureSchema(_handle: SqliteHandle, conn: Connection, cfg: Config): Promise<void> {\n // One writer at a time: the feature hooks check a column then add it, so two cold opens racing\n // here both see it missing and the second ALTER fails with a duplicate column.\n await withTransaction(conn, () => ensureSchemaTables(conn, cfg), BEGIN_WRITE);\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 ensureSchema,\n setDerivedBusyTimeout,\n createStore: (handle, conn) => createStore(handle.db, conn),\n};\n\nexport async function openSqlite(cfg: ResolvedConfig): Promise<OpenResult> {\n return openWithDialect(cfg, sqliteOpenDialect);\n}\n"],"names":["DatabaseSync","featureSignature","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","BEGIN_WRITE","withTransaction","createConnection","sqliteDialect","registerFunctions","createStore","DB_FILENAME","SCHEMA_VERSION","TOKENIZE","createContentTable","conn","exec","ensureSchemaTables","cfg","feature","schema","setJournalWal","db","deadline","Date","now","err","test","message","Atomics","wait","Int32Array","SharedArrayBuffer","connect","dbPath","_cfg","handle","close","ensureSchema","_handle","setDerivedBusyTimeout","_conn","ms","sqliteOpenDialect","filename","schemaVersion","reconcileDialect","openSqlite"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { ParsedDoc } from '../../scan/index.js';
2
2
  import type { ReconcileDialect } from '../types.js';
3
3
  export declare const INSERT_CONTENT_SQL = "INSERT INTO content (rowid, title, summary, text, \"path\", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?, ?, ?, ?)";
4
- export declare function contentRow(doc: ParsedDoc, segmenting: boolean): unknown[];
4
+ export declare function contentRow(doc: ParsedDoc): unknown[];
5
5
  export declare const sqliteDialect: ReconcileDialect;
@@ -1,7 +1,6 @@
1
- import { contentTokenize } from '../../config/index.js';
2
1
  import { SenseError } from '../../errors.js';
3
2
  import { segmentField } from '../../text/segment.js';
4
- import { recordReconcileDuration } from '../shared.js';
3
+ import { quoteIdent, recordReconcileDuration } from '../shared.js';
5
4
  import { BEGIN_WRITE } from '../transaction.js';
6
5
  // This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in
7
6
  // store/reconcile.ts. content is FTS5-virtual with "_seg" sidecars for lexical.ts.
@@ -12,30 +11,31 @@ const MAX_FRONTMATTER_COLUMNS = 2000;
12
11
  export const INSERT_CONTENT_SQL = `INSERT INTO content (rowid, title, summary, text, "path", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE "path" = ?), ?, ?, ?, ?, ?, ?, ?)`;
13
12
  // A single content row's param tuple, matching INSERT_CONTENT_SQL's placeholder order.
14
13
  // Assumes the frontmatter row for doc.relPath already exists (rowid subquery).
15
- export function contentRow(doc, segmenting) {
14
+ export function contentRow(doc) {
16
15
  return [
17
16
  doc.relPath,
18
17
  doc.search.title,
19
18
  doc.search.summary,
20
19
  doc.search.text,
21
20
  doc.relPath,
22
- segmenting ? segmentField(doc.search.title) : '',
23
- segmenting ? segmentField(doc.search.summary) : '',
24
- segmenting ? segmentField(doc.search.text) : ''
21
+ segmentField(doc.search.title),
22
+ segmentField(doc.search.summary),
23
+ segmentField(doc.search.text)
25
24
  ];
26
25
  }
27
- async function reconcileContent(conn, touched, docs, _delta, cfg) {
26
+ async function reconcileContent(conn, touched, docs, _delta, _cfg) {
28
27
  // FTS5 has no upsert, so delete-before-insert, coupled to the frontmatter rowid (indexed via
29
28
  // its PRIMARY KEY) rather than the UNINDEXED `path` column, which a per-row DELETE would scan to find.
29
+ // `touched` already covers a path a concurrent reconcile turned out to have created first --
30
+ // see reconcile.ts's post-lock revalidation, which folds such paths in before calling here.
30
31
  if (touched.length > 0) await conn.runBatch('DELETE FROM content WHERE rowid = (SELECT rowid FROM frontmatter WHERE "path" = ?)', touched.map((p)=>[
31
32
  p
32
33
  ]));
33
- if (docs.length > 0) {
34
- // A non-default tokenizer means the tree has chosen its own scheme; a phrase query over
35
- // grapheme runs would be nonsense against trigram, so the sidecars stay empty.
36
- const segmenting = contentTokenize(cfg) === undefined;
37
- await conn.runBatch(INSERT_CONTENT_SQL, docs.map((doc)=>contentRow(doc, segmenting)));
38
- }
34
+ if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));
35
+ }
36
+ // SQLite's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.
37
+ async function addColumns(conn, names) {
38
+ for (const name of names)await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);
39
39
  }
40
40
  export const sqliteDialect = {
41
41
  beginMode: ()=>BEGIN_WRITE,
@@ -46,7 +46,7 @@ export const sqliteDialect = {
46
46
  throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${count} columns, crossing SQLite's compile-time SQLITE_MAX_COLUMN limit (default ${MAX_FRONTMATTER_COLUMNS}; see https://www.sqlite.org/limits.html). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);
47
47
  }
48
48
  },
49
- columnTypeSuffix: ()=>'',
49
+ addColumns,
50
50
  reconcileContent,
51
51
  recordDuration: recordReconcileDuration
52
52
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { contentTokenize } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { segmentField } from '../../text/segment.ts';\nimport { 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 FTS5-virtual with \"_seg\" sidecars for lexical.ts.\n\n// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Shared by reconcileContent() and the tokenize-only rebuild in open.ts, so both prepare the\n// same literal instead of two copies drifting apart.\nexport const INSERT_CONTENT_SQL = `INSERT INTO content (rowid, title, summary, text, \"path\", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?, ?, ?, ?)`;\n\n// A single content row's param tuple, matching INSERT_CONTENT_SQL's placeholder order.\n// Assumes the frontmatter row for doc.relPath already exists (rowid subquery).\nexport function contentRow(doc: ParsedDoc, segmenting: boolean): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath, segmenting ? segmentField(doc.search.title) : '', segmenting ? segmentField(doc.search.summary) : '', segmenting ? segmentField(doc.search.text) : ''];\n}\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], _delta: ReconcileDelta, cfg: Config): Promise<void> {\n // FTS5 has no upsert, so delete-before-insert, coupled to the frontmatter rowid (indexed via\n // its PRIMARY KEY) rather than the UNINDEXED `path` column, which a per-row DELETE would scan to find.\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE rowid = (SELECT rowid FROM frontmatter WHERE \"path\" = ?)',\n touched.map((p) => [p])\n );\n\n if (docs.length > 0) {\n // A non-default tokenizer means the tree has chosen its own scheme; a phrase query over\n // grapheme runs would be nonsense against trigram, so the sidecars stay empty.\n const segmenting = contentTokenize(cfg) === undefined;\n await conn.runBatch(\n INSERT_CONTENT_SQL,\n docs.map((doc) => contentRow(doc, segmenting))\n );\n }\n}\n\nexport const sqliteDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n // Fence before ALTERing: SQLite's own failure past this point is a raw\n // \"too many columns on sqlite_altertab_frontmatter\" with no indication of the boundary or the levers.\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing SQLite's compile-time SQLITE_MAX_COLUMN limit (default ${MAX_FRONTMATTER_COLUMNS}; see https://www.sqlite.org/limits.html). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n columnTypeSuffix: () => '',\n reconcileContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["contentTokenize","SenseError","segmentField","recordReconcileDuration","BEGIN_WRITE","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","contentRow","doc","segmenting","relPath","search","title","summary","text","reconcileContent","conn","touched","docs","_delta","cfg","length","runBatch","map","p","undefined","sqliteDialect","beginMode","checkColumnLimit","count","columnTypeSuffix","recordDuration"],"mappings":"AACA,SAASA,eAAe,QAAQ,wBAAwB;AACxD,SAASC,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,YAAY,QAAQ,wBAAwB;AACrD,SAASC,uBAAuB,QAAQ,eAAe;AACvD,SAASC,WAAW,QAAQ,oBAAoB;AAGhD,qFAAqF;AACrF,mFAAmF;AAEnF,8FAA8F;AAC9F,MAAMC,0BAA0B;AAEhC,6FAA6F;AAC7F,qDAAqD;AACrD,OAAO,MAAMC,qBAAqB,CAAC,0KAA0K,CAAC,CAAC;AAE/M,uFAAuF;AACvF,+EAA+E;AAC/E,OAAO,SAASC,WAAWC,GAAc,EAAEC,UAAmB;IAC5D,OAAO;QAACD,IAAIE,OAAO;QAAEF,IAAIG,MAAM,CAACC,KAAK;QAAEJ,IAAIG,MAAM,CAACE,OAAO;QAAEL,IAAIG,MAAM,CAACG,IAAI;QAAEN,IAAIE,OAAO;QAAED,aAAaP,aAAaM,IAAIG,MAAM,CAACC,KAAK,IAAI;QAAIH,aAAaP,aAAaM,IAAIG,MAAM,CAACE,OAAO,IAAI;QAAIJ,aAAaP,aAAaM,IAAIG,MAAM,CAACG,IAAI,IAAI;KAAG;AACjP;AAEA,eAAeC,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,MAAsB,EAAEC,GAAW;IACzH,6FAA6F;IAC7F,uGAAuG;IACvG,IAAIH,QAAQI,MAAM,GAAG,GACnB,MAAML,KAAKM,QAAQ,CACjB,sFACAL,QAAQM,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAG1B,IAAIN,KAAKG,MAAM,GAAG,GAAG;QACnB,wFAAwF;QACxF,+EAA+E;QAC/E,MAAMZ,aAAaT,gBAAgBoB,SAASK;QAC5C,MAAMT,KAAKM,QAAQ,CACjBhB,oBACAY,KAAKK,GAAG,CAAC,CAACf,MAAQD,WAAWC,KAAKC;IAEtC;AACF;AAEA,OAAO,MAAMiB,gBAAkC;IAC7CC,WAAW,IAAMvB;IACjBwB,kBAAiBC,KAAK;QACpB,uEAAuE;QACvE,sGAAsG;QACtG,IAAIA,QAAQxB,yBAAyB;YACnC,MAAM,IAAIJ,WACR,gBACA,CAAC,uBAAuB,EAAE4B,MAAM,0EAA0E,EAAExB,wBAAwB,wKAAwK,CAAC;QAEjT;IACF;IACAyB,kBAAkB,IAAM;IACxBf;IACAgB,gBAAgB5B;AAClB,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/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 { segmentField } 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 FTS5-virtual with \"_seg\" sidecars for lexical.ts.\n\n// SQLite's compile-time SQLITE_MAX_COLUMN, default 2000 (https://www.sqlite.org/limits.html).\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Shared by reconcileContent() and the tokenize-only rebuild in open.ts, so both prepare the\n// same literal instead of two copies drifting apart.\nexport const INSERT_CONTENT_SQL = `INSERT INTO content (rowid, title, summary, text, \"path\", title_seg, summary_seg, text_seg) VALUES ((SELECT rowid FROM frontmatter WHERE \"path\" = ?), ?, ?, ?, ?, ?, ?, ?)`;\n\n// A single content row's param tuple, matching INSERT_CONTENT_SQL's placeholder order.\n// Assumes the frontmatter row for doc.relPath already exists (rowid subquery).\nexport function contentRow(doc: ParsedDoc): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text, doc.relPath, segmentField(doc.search.title), segmentField(doc.search.summary), segmentField(doc.search.text)];\n}\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], _delta: ReconcileDelta, _cfg: Config): Promise<void> {\n // FTS5 has no upsert, so delete-before-insert, coupled to the frontmatter rowid (indexed via\n // its PRIMARY KEY) rather than the UNINDEXED `path` column, which a per-row DELETE would scan to find.\n // `touched` already covers a path a concurrent reconcile turned out to have created first --\n // see reconcile.ts's post-lock revalidation, which folds such paths in before calling here.\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE rowid = (SELECT rowid FROM frontmatter WHERE \"path\" = ?)',\n touched.map((p) => [p])\n );\n\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n}\n\n// SQLite'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 sqliteDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n // Fence before ALTERing: SQLite's own failure past this point is a raw\n // \"too many columns on sqlite_altertab_frontmatter\" with no indication of the boundary or the levers.\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing SQLite's compile-time SQLITE_MAX_COLUMN limit (default ${MAX_FRONTMATTER_COLUMNS}; see https://www.sqlite.org/limits.html). 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","segmentField","quoteIdent","recordReconcileDuration","BEGIN_WRITE","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","contentRow","doc","relPath","search","title","summary","text","reconcileContent","conn","touched","docs","_delta","_cfg","length","runBatch","map","p","addColumns","names","name","exec","sqliteDialect","beginMode","checkColumnLimit","count","recordDuration"],"mappings":"AACA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,YAAY,QAAQ,wBAAwB;AACrD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAGhD,qFAAqF;AACrF,mFAAmF;AAEnF,8FAA8F;AAC9F,MAAMC,0BAA0B;AAEhC,6FAA6F;AAC7F,qDAAqD;AACrD,OAAO,MAAMC,qBAAqB,CAAC,0KAA0K,CAAC,CAAC;AAE/M,uFAAuF;AACvF,+EAA+E;AAC/E,OAAO,SAASC,WAAWC,GAAc;IACvC,OAAO;QAACA,IAAIC,OAAO;QAAED,IAAIE,MAAM,CAACC,KAAK;QAAEH,IAAIE,MAAM,CAACE,OAAO;QAAEJ,IAAIE,MAAM,CAACG,IAAI;QAAEL,IAAIC,OAAO;QAAER,aAAaO,IAAIE,MAAM,CAACC,KAAK;QAAGV,aAAaO,IAAIE,MAAM,CAACE,OAAO;QAAGX,aAAaO,IAAIE,MAAM,CAACG,IAAI;KAAE;AAC3L;AAEA,eAAeC,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,MAAsB,EAAEC,IAAY;IAC1H,6FAA6F;IAC7F,uGAAuG;IACvG,6FAA6F;IAC7F,4FAA4F;IAC5F,IAAIH,QAAQI,MAAM,GAAG,GACnB,MAAML,KAAKM,QAAQ,CACjB,sFACAL,QAAQM,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAG1B,IAAIN,KAAKG,MAAM,GAAG,GAAG,MAAML,KAAKM,QAAQ,CAACf,oBAAoBW,KAAKK,GAAG,CAACf;AACxE;AAEA,0FAA0F;AAC1F,eAAeiB,WAAWT,IAAgB,EAAEU,KAAe;IACzD,KAAK,MAAMC,QAAQD,MAAO,MAAMV,KAAKY,IAAI,CAAC,CAAC,mCAAmC,EAAEzB,WAAWwB,OAAO;AACpG;AAEA,OAAO,MAAME,gBAAkC;IAC7CC,WAAW,IAAMzB;IACjB0B,kBAAiBC,KAAK;QACpB,uEAAuE;QACvE,sGAAsG;QACtG,IAAIA,QAAQ1B,yBAAyB;YACnC,MAAM,IAAIL,WACR,gBACA,CAAC,uBAAuB,EAAE+B,MAAM,0EAA0E,EAAE1B,wBAAwB,wKAAwK,CAAC;QAEjT;IACF;IACAmB;IACAV;IACAkB,gBAAgB7B;AAClB,EAAE"}
@@ -1,2 +1,2 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
- export declare function registerFunctions(db: DatabaseSync, segmenting: boolean): void;
2
+ export declare function registerFunctions(db: DatabaseSync): void;
@@ -1,6 +1,6 @@
1
1
  import { segmentMatch } from '../../text/segment.js';
2
2
  import { basenameImpl, hasImpl } from '../sql-functions.js';
3
- export function registerFunctions(db, segmenting) {
3
+ export function registerFunctions(db) {
4
4
  db.function('has', {
5
5
  deterministic: true,
6
6
  varargs: false
@@ -10,12 +10,12 @@ export function registerFunctions(db, segmenting) {
10
10
  varargs: true
11
11
  }, basenameImpl);
12
12
  // Raw `content MATCH '<unspaced text>'` cannot be rewritten behind the author's back and matches
13
- // nothing, so the same transform is available to hand-written SQL by name. `segmenting` is the same config predicate commands.ts's search() gates on (contentTokenize(cfg) === undefined), passed in by open().
13
+ // nothing, so the same transform is available to hand-written SQL by name.
14
14
  db.function('segment', {
15
15
  deterministic: true,
16
16
  varargs: false
17
17
  }, (terms)=>{
18
18
  const text = terms === null || terms === undefined ? '' : String(terms);
19
- return segmenting ? segmentMatch(text) : text;
19
+ return segmentMatch(text);
20
20
  });
21
21
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/sql-functions.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { segmentMatch } from '../../text/segment.ts';\nimport { basenameImpl, hasImpl } from '../sql-functions.ts';\n\nexport function registerFunctions(db: DatabaseSync, segmenting: boolean): void {\n db.function('has', { deterministic: true, varargs: false }, hasImpl);\n\n db.function('basename', { deterministic: true, varargs: true }, basenameImpl);\n\n // Raw `content MATCH '<unspaced text>'` cannot be rewritten behind the author's back and matches\n // nothing, so the same transform is available to hand-written SQL by name. `segmenting` is the same config predicate commands.ts's search() gates on (contentTokenize(cfg) === undefined), passed in by open().\n db.function('segment', { deterministic: true, varargs: false }, (terms: unknown): string => {\n const text = terms === null || terms === undefined ? '' : String(terms);\n return segmenting ? segmentMatch(text) : text;\n });\n}\n"],"names":["segmentMatch","basenameImpl","hasImpl","registerFunctions","db","segmenting","function","deterministic","varargs","terms","text","undefined","String"],"mappings":"AACA,SAASA,YAAY,QAAQ,wBAAwB;AACrD,SAASC,YAAY,EAAEC,OAAO,QAAQ,sBAAsB;AAE5D,OAAO,SAASC,kBAAkBC,EAAgB,EAAEC,UAAmB;IACrED,GAAGE,QAAQ,CAAC,OAAO;QAAEC,eAAe;QAAMC,SAAS;IAAM,GAAGN;IAE5DE,GAAGE,QAAQ,CAAC,YAAY;QAAEC,eAAe;QAAMC,SAAS;IAAK,GAAGP;IAEhE,iGAAiG;IACjG,gNAAgN;IAChNG,GAAGE,QAAQ,CAAC,WAAW;QAAEC,eAAe;QAAMC,SAAS;IAAM,GAAG,CAACC;QAC/D,MAAMC,OAAOD,UAAU,QAAQA,UAAUE,YAAY,KAAKC,OAAOH;QACjE,OAAOJ,aAAaL,aAAaU,QAAQA;IAC3C;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/sql-functions.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { segmentMatch } from '../../text/segment.ts';\nimport { basenameImpl, hasImpl } from '../sql-functions.ts';\n\nexport function registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, hasImpl);\n\n db.function('basename', { deterministic: true, varargs: true }, basenameImpl);\n\n // Raw `content MATCH '<unspaced text>'` cannot be rewritten behind the author's back and matches\n // nothing, so the same transform is available to hand-written SQL by name.\n db.function('segment', { deterministic: true, varargs: false }, (terms: unknown): string => {\n const text = terms === null || terms === undefined ? '' : String(terms);\n return segmentMatch(text);\n });\n}\n"],"names":["segmentMatch","basenameImpl","hasImpl","registerFunctions","db","function","deterministic","varargs","terms","text","undefined","String"],"mappings":"AACA,SAASA,YAAY,QAAQ,wBAAwB;AACrD,SAASC,YAAY,EAAEC,OAAO,QAAQ,sBAAsB;AAE5D,OAAO,SAASC,kBAAkBC,EAAgB;IAChDA,GAAGC,QAAQ,CAAC,OAAO;QAAEC,eAAe;QAAMC,SAAS;IAAM,GAAGL;IAE5DE,GAAGC,QAAQ,CAAC,YAAY;QAAEC,eAAe;QAAMC,SAAS;IAAK,GAAGN;IAEhE,iGAAiG;IACjG,2EAA2E;IAC3EG,GAAGC,QAAQ,CAAC,WAAW;QAAEC,eAAe;QAAMC,SAAS;IAAM,GAAG,CAACC;QAC/D,MAAMC,OAAOD,UAAU,QAAQA,UAAUE,YAAY,KAAKC,OAAOH;QACjE,OAAOR,aAAaS;IACtB;AACF"}
@@ -1,5 +1,4 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
- import type { Config } from '../../config/index.js';
3
2
  import type { Capability, Connection, Store } from '../types.js';
4
3
  export declare const CAPABILITIES: ReadonlySet<Capability>;
5
- export declare function createStore(db: DatabaseSync, conn: Connection, cfg: Config, baseDir: string): Store;
4
+ export declare function createStore(db: DatabaseSync, conn: Connection): Store;
@@ -1,11 +1,8 @@
1
- import { contentTokenize } from '../../config/index.js';
2
- import { reconcile } from '../reconcile.js';
3
1
  import { getColumns } from '../shared.js';
4
2
  import { withTransaction } from '../transaction.js';
5
3
  import { hasVectorRow, pendingRows } from '../vectors.js';
6
4
  import { fieldStats } from './fieldStats.js';
7
5
  import { queryLexical } from './lexical.js';
8
- import { sqliteDialect } from './reconcile.js';
9
6
  import { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.js';
10
7
  export const CAPABILITIES = new Set([
11
8
  'phrases',
@@ -16,8 +13,8 @@ export const CAPABILITIES = new Set([
16
13
  'sql-functions'
17
14
  ]);
18
15
  // Wraps the synchronous DatabaseSync connection in the async Store interface, sharing one
19
- // Connection instance (conn) with open()'s own reconcile call so transaction depth is tracked against the same object everywhere.
20
- export function createStore(db, conn, cfg, baseDir) {
16
+ // Connection instance (conn) with the builder's own reconcile call so transaction depth is tracked against the same object everywhere.
17
+ export function createStore(db, conn) {
21
18
  return {
22
19
  name: 'sqlite',
23
20
  capabilities: CAPABILITIES,
@@ -33,9 +30,6 @@ export function createStore(db, conn, cfg, baseDir) {
33
30
  async transaction (fn) {
34
31
  return withTransaction(conn, fn);
35
32
  },
36
- async reconcile () {
37
- return reconcile(conn, cfg, baseDir, sqliteDialect);
38
- },
39
33
  docs: {
40
34
  async columns () {
41
35
  return [
@@ -48,7 +42,7 @@ export function createStore(db, conn, cfg, baseDir) {
48
42
  },
49
43
  lexical: {
50
44
  async query (terms, opts) {
51
- return queryLexical(conn, terms, opts, contentTokenize(cfg) === undefined);
45
+ return queryLexical(conn, terms, opts);
52
46
  }
53
47
  },
54
48
  vectors: {
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/store.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport type { Config } from '../../config/index.ts';\nimport { contentTokenize } from '../../config/index.ts';\nimport { reconcile } from '../reconcile.ts';\nimport { getColumns } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store, VectorWriteRow } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { sqliteDialect } from './reconcile.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['phrases', 'snippets', 'watch-concurrency', 'lexical', 'vectors', 'sql-functions']);\n\n// Wraps the synchronous DatabaseSync connection in the async Store interface, sharing one\n// Connection instance (conn) with open()'s own reconcile call so transaction depth is tracked against the same object everywhere.\nexport function createStore(db: DatabaseSync, conn: Connection, cfg: Config, baseDir: string): Store {\n return {\n name: 'sqlite',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n async reconcile() {\n return reconcile(conn, cfg, baseDir, sqliteDialect);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n async fieldStats(columns, scopeWhere) {\n return fieldStats(conn, columns, scopeWhere);\n },\n },\n lexical: {\n async query(terms, opts) {\n return queryLexical(conn, terms, opts, contentTokenize(cfg) === undefined);\n },\n },\n vectors: {\n async pending() {\n return pendingRows(conn);\n },\n async writeVectors(rows: VectorWriteRow[]) {\n await writeVectorBatch(conn, rows);\n },\n async candidates(qv, storeDims, fetch, allowed) {\n return scanCandidates(conn, qv, storeDims, fetch, allowed);\n },\n async similar(path, opts) {\n return scanSimilar(conn, path, opts);\n },\n async hasVector(path) {\n return hasVectorRow(conn, path);\n },\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min), not a value re-derived here.\n const row = db.prepare('PRAGMA busy_timeout').get() as { timeout: number };\n return { busy_timeout: `${row.timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = db.prepare(sql);\n stmt.setReadBigInts(true); // int64 past 2^53 arrives as BigInt instead of throwing at step time\n return {\n columns: () => stmt.columns(),\n // sense sql streams through an async iterator wrapping the sync iterate().\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...(params as Parameters<typeof stmt.iterate>));\n },\n };\n },\n },\n async close() {\n db.close();\n },\n };\n}\n"],"names":["contentTokenize","reconcile","getColumns","withTransaction","hasVectorRow","pendingRows","fieldStats","queryLexical","sqliteDialect","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","createStore","db","conn","cfg","baseDir","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","undefined","vectors","pending","writeVectors","rows","candidates","qv","storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","timeout","raw","stmt","setReadBigInts","iterate","params","close"],"mappings":"AAEA,SAASA,eAAe,QAAQ,wBAAwB;AACxD,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,eAAe,QAAQ,oBAAoB;AAEpD,SAASC,YAAY,EAAEC,WAAW,QAAQ,gBAAgB;AAC1D,SAASC,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,YAAY,QAAQ,eAAe;AAC5C,SAASC,aAAa,QAAQ,iBAAiB;AAC/C,SAASC,cAAc,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,eAAe;AAE7E,OAAO,MAAMC,eAAwC,IAAIC,IAAI;IAAC;IAAW;IAAY;IAAqB;IAAW;IAAW;CAAgB,EAAE;AAElJ,0FAA0F;AAC1F,kIAAkI;AAClI,OAAO,SAASC,YAAYC,EAAgB,EAAEC,IAAgB,EAAEC,GAAW,EAAEC,OAAe;IAC1F,OAAO;QACLC,MAAM;QACNC,cAAcR;QACd,MAAMS,MAAKC,GAAW;YACpB,MAAMN,KAAKK,IAAI,CAACC;QAClB;QACA,MAAMC,SAAQD,GAAW;YACvB,OAAON,KAAKO,OAAO,CAACD;QACtB;QACA,MAAME,UAASF,GAAW,EAAEG,SAAsB;YAChD,MAAMT,KAAKQ,QAAQ,CAACF,KAAKG;QAC3B;QACA,MAAMC,aAAeC,EAAoB;YACvC,OAAOxB,gBAAgBa,MAAMW;QAC/B;QACA,MAAM1B;YACJ,OAAOA,UAAUe,MAAMC,KAAKC,SAASV;QACvC;QACAoB,MAAM;YACJ,MAAMC;gBACJ,OAAO;uBAAK,MAAM3B,WAAWc;iBAAO;YACtC;YACA,MAAMV,YAAWuB,OAAO,EAAEC,UAAU;gBAClC,OAAOxB,WAAWU,MAAMa,SAASC;YACnC;QACF;QACAC,SAAS;YACP,MAAMC,OAAMC,KAAK,EAAEC,IAAI;gBACrB,OAAO3B,aAAaS,MAAMiB,OAAOC,MAAMlC,gBAAgBiB,SAASkB;YAClE;QACF;QACAC,SAAS;YACP,MAAMC;gBACJ,OAAOhC,YAAYW;YACrB;YACA,MAAMsB,cAAaC,IAAsB;gBACvC,MAAM5B,iBAAiBK,MAAMuB;YAC/B;YACA,MAAMC,YAAWC,EAAE,EAAEC,SAAS,EAAEC,KAAK,EAAEC,OAAO;gBAC5C,OAAOnC,eAAeO,MAAMyB,IAAIC,WAAWC,OAAOC;YACpD;YACA,MAAMC,SAAQC,IAAI,EAAEZ,IAAI;gBACtB,OAAOxB,YAAYM,MAAM8B,MAAMZ;YACjC;YACA,MAAMa,WAAUD,IAAI;gBAClB,OAAO1C,aAAaY,MAAM8B;YAC5B;QACF;QACA,MAAME;YACJ,qFAAqF;YACrF,qFAAqF;YACrF,MAAMC,MAAMlC,GAAGQ,OAAO,CAAC,uBAAuB2B,GAAG;YACjD,OAAO;gBAAEC,cAAc,GAAGF,IAAIG,OAAO,CAAC,kFAAkF,CAAC;YAAC;QAC5H;QACAC,KAAK;YACH,MAAM9B,SAAQD,GAAW;gBACvB,MAAMgC,OAAOvC,GAAGQ,OAAO,CAACD;gBACxBgC,KAAKC,cAAc,CAAC,OAAO,qEAAqE;gBAChG,OAAO;oBACL1B,SAAS,IAAMyB,KAAKzB,OAAO;oBAC3B,2EAA2E;oBAC3E2B,SAAS,gBAAiB,GAAGC,MAAiB;wBAC5C,OAAOH,KAAKE,OAAO,IAAKC;oBAC1B;gBACF;YACF;QACF;QACA,MAAMC;YACJ3C,GAAG2C,KAAK;QACV;IACF;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/store.ts"],"sourcesContent":["import type { DatabaseSync } from 'node:sqlite';\nimport { getColumns } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Capability, Connection, Statement, Store, VectorWriteRow } from '../types.ts';\nimport { hasVectorRow, pendingRows } from '../vectors.ts';\nimport { fieldStats } from './fieldStats.ts';\nimport { queryLexical } from './lexical.ts';\nimport { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.ts';\n\nexport const CAPABILITIES: ReadonlySet<Capability> = new Set(['phrases', 'snippets', 'watch-concurrency', 'lexical', 'vectors', 'sql-functions']);\n\n// Wraps the synchronous DatabaseSync connection in the async Store interface, sharing one\n// Connection instance (conn) with the builder's own reconcile call so transaction depth is tracked against the same object everywhere.\nexport function createStore(db: DatabaseSync, conn: Connection): Store {\n return {\n name: 'sqlite',\n capabilities: CAPABILITIES,\n async exec(sql: string): Promise<void> {\n await conn.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return conn.prepare(sql);\n },\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n await conn.runBatch(sql, paramRows);\n },\n async transaction<T>(fn: () => Promise<T>): Promise<T> {\n return withTransaction(conn, fn);\n },\n docs: {\n async columns() {\n return [...(await getColumns(conn))];\n },\n async fieldStats(columns, scopeWhere) {\n return fieldStats(conn, columns, scopeWhere);\n },\n },\n lexical: {\n async query(terms, opts) {\n return queryLexical(conn, terms, opts);\n },\n },\n vectors: {\n async pending() {\n return pendingRows(conn);\n },\n async writeVectors(rows: VectorWriteRow[]) {\n await writeVectorBatch(conn, rows);\n },\n async candidates(qv, storeDims, fetch, allowed) {\n return scanCandidates(conn, qv, storeDims, fetch, allowed);\n },\n async similar(path, opts) {\n return scanSimilar(conn, path, opts);\n },\n async hasVector(path) {\n return hasVectorRow(conn, path);\n },\n },\n async engineStatus() {\n // Read back rather than recomputed: this is what open() actually set (3x the largest\n // recorded reconcile, floored at 30s, capped at 10min), not a value re-derived here.\n const row = db.prepare('PRAGMA busy_timeout').get() as { timeout: number };\n return { busy_timeout: `${row.timeout}ms (derived: 3x the largest reconcile this cache has recorded, floored at 30000ms)` };\n },\n raw: {\n async prepare(sql: string) {\n const stmt = db.prepare(sql);\n stmt.setReadBigInts(true); // int64 past 2^53 arrives as BigInt instead of throwing at step time\n return {\n columns: () => stmt.columns(),\n // sense sql streams through an async iterator wrapping the sync iterate().\n iterate: async function* (...params: unknown[]) {\n yield* stmt.iterate(...(params as Parameters<typeof stmt.iterate>));\n },\n };\n },\n },\n async close() {\n db.close();\n },\n };\n}\n"],"names":["getColumns","withTransaction","hasVectorRow","pendingRows","fieldStats","queryLexical","scanCandidates","scanSimilar","writeVectorBatch","CAPABILITIES","Set","createStore","db","conn","name","capabilities","exec","sql","prepare","runBatch","paramRows","transaction","fn","docs","columns","scopeWhere","lexical","query","terms","opts","vectors","pending","writeVectors","rows","candidates","qv","storeDims","fetch","allowed","similar","path","hasVector","engineStatus","row","get","busy_timeout","timeout","raw","stmt","setReadBigInts","iterate","params","close"],"mappings":"AACA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,eAAe,QAAQ,oBAAoB;AAEpD,SAASC,YAAY,EAAEC,WAAW,QAAQ,gBAAgB;AAC1D,SAASC,UAAU,QAAQ,kBAAkB;AAC7C,SAASC,YAAY,QAAQ,eAAe;AAC5C,SAASC,cAAc,EAAEC,WAAW,EAAEC,gBAAgB,QAAQ,eAAe;AAE7E,OAAO,MAAMC,eAAwC,IAAIC,IAAI;IAAC;IAAW;IAAY;IAAqB;IAAW;IAAW;CAAgB,EAAE;AAElJ,0FAA0F;AAC1F,uIAAuI;AACvI,OAAO,SAASC,YAAYC,EAAgB,EAAEC,IAAgB;IAC5D,OAAO;QACLC,MAAM;QACNC,cAAcN;QACd,MAAMO,MAAKC,GAAW;YACpB,MAAMJ,KAAKG,IAAI,CAACC;QAClB;QACA,MAAMC,SAAQD,GAAW;YACvB,OAAOJ,KAAKK,OAAO,CAACD;QACtB;QACA,MAAME,UAASF,GAAW,EAAEG,SAAsB;YAChD,MAAMP,KAAKM,QAAQ,CAACF,KAAKG;QAC3B;QACA,MAAMC,aAAeC,EAAoB;YACvC,OAAOrB,gBAAgBY,MAAMS;QAC/B;QACAC,MAAM;YACJ,MAAMC;gBACJ,OAAO;uBAAK,MAAMxB,WAAWa;iBAAO;YACtC;YACA,MAAMT,YAAWoB,OAAO,EAAEC,UAAU;gBAClC,OAAOrB,WAAWS,MAAMW,SAASC;YACnC;QACF;QACAC,SAAS;YACP,MAAMC,OAAMC,KAAK,EAAEC,IAAI;gBACrB,OAAOxB,aAAaQ,MAAMe,OAAOC;YACnC;QACF;QACAC,SAAS;YACP,MAAMC;gBACJ,OAAO5B,YAAYU;YACrB;YACA,MAAMmB,cAAaC,IAAsB;gBACvC,MAAMzB,iBAAiBK,MAAMoB;YAC/B;YACA,MAAMC,YAAWC,EAAE,EAAEC,SAAS,EAAEC,KAAK,EAAEC,OAAO;gBAC5C,OAAOhC,eAAeO,MAAMsB,IAAIC,WAAWC,OAAOC;YACpD;YACA,MAAMC,SAAQC,IAAI,EAAEX,IAAI;gBACtB,OAAOtB,YAAYM,MAAM2B,MAAMX;YACjC;YACA,MAAMY,WAAUD,IAAI;gBAClB,OAAOtC,aAAaW,MAAM2B;YAC5B;QACF;QACA,MAAME;YACJ,qFAAqF;YACrF,qFAAqF;YACrF,MAAMC,MAAM/B,GAAGM,OAAO,CAAC,uBAAuB0B,GAAG;YACjD,OAAO;gBAAEC,cAAc,GAAGF,IAAIG,OAAO,CAAC,kFAAkF,CAAC;YAAC;QAC5H;QACAC,KAAK;YACH,MAAM7B,SAAQD,GAAW;gBACvB,MAAM+B,OAAOpC,GAAGM,OAAO,CAACD;gBACxB+B,KAAKC,cAAc,CAAC,OAAO,qEAAqE;gBAChG,OAAO;oBACLzB,SAAS,IAAMwB,KAAKxB,OAAO;oBAC3B,2EAA2E;oBAC3E0B,SAAS,gBAAiB,GAAGC,MAAiB;wBAC5C,OAAOH,KAAKE,OAAO,IAAKC;oBAC1B;gBACF;YACF;QACF;QACA,MAAMC;YACJxC,GAAGwC,KAAK;QACV;IACF;AACF"}
@@ -1,14 +1,9 @@
1
+ import type { Database } from '@tursodatabase/database';
1
2
  import type { ResolvedConfig } from '../../config/index.js';
2
- import type { Store } from '../types.js';
3
+ import type { OpenResult } from '../open.js';
4
+ import type { OpenDialect } from '../types.js';
3
5
  export declare const DB_FILENAME = "cache.turso.db";
4
- export declare const SCHEMA_VERSION = "3";
5
- export interface OpenResult {
6
- store: Store;
7
- cfg: ResolvedConfig;
8
- dbPath: string;
9
- parsed: number;
10
- warnings: string[];
11
- }
12
- 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')"];
13
- export declare const CONTENT_FTS_NAMES: readonly ["content_fts", "content_fts_ngram"];
6
+ export declare const SCHEMA_VERSION = "4";
7
+ export type { OpenResult };
8
+ export declare const tursoOpenDialect: OpenDialect<Database>;
14
9
  export declare function openTurso(cfg: ResolvedConfig): Promise<OpenResult>;