sensemaking 0.24.2 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # sensemaking
2
2
 
3
- Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step.
3
+ Search and query a directory of Markdown notes from the command line. `sense` indexes frontmatter, prose and links in a local database. It can also combine word matches with links and semantic similarity.
4
+
5
+ Results contain file paths, snippets and line ranges. A person or agent can inspect the relevant passages without loading every note. No server or build step is required.
4
6
 
5
7
  ## Problem
6
8
 
@@ -16,7 +18,7 @@ cd your-notes && sense init
16
18
  sense download # the embedding model, once per machine; the first vector search fetches it otherwise
17
19
  ```
18
20
 
19
- Needs Node 22.20 or newer: the first release whose built-in SQLite has both FTS5, which `sense search` indexes prose with, and row-returning `INSERT ... RETURNING`, which `sense path` and `peek` walk the link graph with.
21
+ Requires Node.js 22.20 or newer. The default store uses Node's built-in SQLite.
20
22
 
21
23
  ```bash
22
24
  sense map # orient: fields, hub notes, recent changes
@@ -25,7 +27,16 @@ sense peek notes/q3-report.md # structure: outline + links, b
25
27
  sense sql "SELECT path FROM frontmatter WHERE has(tags, ?)" urgent
26
28
  ```
27
29
 
28
- ## Model
30
+ A search result identifies the matching note, the evidence used to rank it and the relevant line range:
31
+
32
+ ```text
33
+ path snippets via score lines
34
+ pricing-decision.md …«pricing» decision … renewal «price»… match 0.0167 L4-7
35
+ ```
36
+
37
+ The row is illustrative. Actual paths, snippets and scores depend on the notes and configured search signals.
38
+
39
+ ## What sense indexes
29
40
 
30
41
  Every file becomes rows in these tables, plus whatever an enabled feature adds of its own:
31
42
 
@@ -144,7 +155,7 @@ Every query starts with a freshness check against the cache in `.sense/`; only c
144
155
  - **Output is flat.** `map`, `peek`, and a search row cost the same on a small tree as a large one: context cost is bounded by what you ask for, not by how much there is.
145
156
  - **Bulk changes are paid by whoever queries next.** `sense watch` moves that re-parse into the background ([DESIGN.md](DESIGN.md#watch-coordination)): it changes latency, never answers, since every query reconciles for itself. To start the cache over, delete the directory `sense status` prints.
146
157
 
147
- These are release gates rather than hopes: every release regenerates the numbers on pinned corpora spanning a 4x range in note count plus a stress tree that packs the worst measured shapes into one place: a megabyte-scale note, heading-dense outlines, dense link graphs, hundreds of frontmatter fields. Configured performance and output-size bands remain release gates. Report-only total rows are exempt from performance thresholds only; invocation failures and invalid artifacts block independently. Current figures: [BENCHMARKING.md](BENCHMARKING.md).
158
+ Release assessments keep these claims measured. The ordinary assessment runs required common behavior and current-store work plus full portable NFCorpus on all three stores. The explicit deep profile adds portable FEVER, pinned corpora spanning a 4x range in note count, a stress tree that packs the worst measured shapes into one place, and legacy SQLite OR-bag continuity. Historical performance, quality, and output bands warn; failed required behavior, invalid or missing required evidence, explicit caller bounds or quality floors, and approved performance guards block. Current figures: [BENCHMARKING.md](BENCHMARKING.md).
148
159
 
149
160
  ## For AI agents
150
161
 
@@ -758,7 +758,7 @@ function createConnection(db) {
758
758
  4,
759
759
  (0, _transactionts.withTransaction)(conn, function() {
760
760
  return _async_to_generator(function() {
761
- var rewritten, stmt, stmt1, _stmt, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, row, err;
761
+ var rewritten, stmt, stmt1, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, row, err;
762
762
  return _ts_generator(this, function(_state) {
763
763
  switch(_state.label){
764
764
  case 0:
@@ -841,7 +841,7 @@ function createConnection(db) {
841
841
  row = _step.value;
842
842
  return [
843
843
  4,
844
- (_stmt = stmt1).run.apply(_stmt, _to_consumable_array(row))
844
+ stmt1.run(row)
845
845
  ];
846
846
  case 12:
847
847
  _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and nothing else owns this statement's lifetime.\n const stmt = await db.prepare(sql);\n try {\n for (const row of paramRows) await stmt.run(...row);\n } finally {\n await stmt.close();\n }\n },\n BEGIN_WRITE\n );\n },\n };\n return conn;\n}\n"],"names":["cacheFilePath","checkpointWal","createConnection","fileSize","reclaimSpace","TursoStatementWrapper","stmt","run","params","get","all","iterate","columns","setReadBigInts","enabled","safeIntegers","db","err","exec","console","error","message","conn","rows","prepare","find","r","name","file","path","stat","size","turso","tursoApi","connect","CONNECT_OPTS","experimental","setMeta","String","close","sql","rewriteFunctions","runBatch","paramRows","length","withTransaction","rewritten","row","rewriteInsert","flat","BEGIN_WRITE"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and nothing else owns this statement's lifetime.\n const stmt = await db.prepare(sql);\n try {\n for (const row of paramRows) await stmt.run(row);\n } finally {\n await stmt.close();\n }\n },\n BEGIN_WRITE\n );\n },\n };\n return conn;\n}\n"],"names":["cacheFilePath","checkpointWal","createConnection","fileSize","reclaimSpace","TursoStatementWrapper","stmt","run","params","get","all","iterate","columns","setReadBigInts","enabled","safeIntegers","db","err","exec","console","error","message","conn","rows","prepare","find","r","name","file","path","stat","size","turso","tursoApi","connect","CONNECT_OPTS","experimental","setMeta","String","close","sql","rewriteFunctions","runBatch","paramRows","length","withTransaction","rewritten","row","rewriteInsert","flat","BEGIN_WRITE"],"mappings":"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"}
@@ -198,7 +198,7 @@ function tursoFtsStrategy(delta) {
198
198
  }
199
199
  function reconcileTursoContentWithStrategy(conn, touched, docs, strategy) {
200
200
  return _async_to_generator(function() {
201
- var bulk, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, name, err, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, ddl, err;
201
+ var bulk, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, name, err, placeholders, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, ddl, err;
202
202
  return _ts_generator(this, function(_state) {
203
203
  switch(_state.label){
204
204
  case 0:
@@ -270,13 +270,14 @@ function reconcileTursoContentWithStrategy(conn, touched, docs, strategy) {
270
270
  3,
271
271
  10
272
272
  ];
273
+ placeholders = touched.map(function() {
274
+ return '?';
275
+ }).join(', ');
273
276
  return [
274
277
  4,
275
- conn.runBatch('DELETE FROM content WHERE "path" = ?', touched.map(function(p) {
276
- return [
277
- p
278
- ];
279
- }))
278
+ conn.runBatch('DELETE FROM content WHERE "path" IN ('.concat(placeholders, ")"), [
279
+ touched
280
+ ])
280
281
  ];
281
282
  case 9:
282
283
  _state.sent();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// 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\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0)\n 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\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["CONTENT_FTS_DDL","CONTENT_FTS_NAMES","FTS_REBUILD_THRESHOLD","reconcileTursoContentWithStrategy","tursoDialect","tursoFtsStrategy","ngramSidecar","text","hasUnspacedRun","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","contentRow","doc","search","title","summary","relPath","stemFolded","delta","churn","reparsed","length","vanished","files","conn","touched","docs","strategy","bulk","name","ddl","exec","runBatch","map","p","reconcileTursoContent","_cfg","addColumns","names","quoteIdent","beginMode","BEGIN_WRITE","checkColumnLimit","count","SenseError","reconcileContent","recordDuration","recordReconcileDuration"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Retained policy boundary from 2026-08-30 corpus-scale measurements; the optimum after later\n// update-path changes remains pending matched corpus-scale crossover evidence.\nexport const FTS_REBUILD_THRESHOLD = 250;\n\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0) {\n const placeholders = touched.map(() => '?').join(', ');\n await conn.runBatch(`DELETE FROM content WHERE \"path\" IN (${placeholders})`, [touched]);\n }\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n if (bulk) for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n}\n\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["CONTENT_FTS_DDL","CONTENT_FTS_NAMES","FTS_REBUILD_THRESHOLD","reconcileTursoContentWithStrategy","tursoDialect","tursoFtsStrategy","ngramSidecar","text","hasUnspacedRun","MAX_FRONTMATTER_COLUMNS","INSERT_CONTENT_SQL","contentRow","doc","search","title","summary","relPath","stemFolded","delta","churn","reparsed","length","vanished","files","conn","touched","docs","strategy","bulk","name","placeholders","ddl","exec","map","join","runBatch","reconcileTursoContent","_cfg","addColumns","names","quoteIdent","beginMode","BEGIN_WRITE","checkColumnLimit","count","SenseError","reconcileContent","recordDuration","recordReconcileDuration"],"mappings":"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"}
@@ -106,7 +106,7 @@ export function createConnection(db) {
106
106
  // can hold across many batches, and nothing else owns this statement's lifetime.
107
107
  const stmt = await db.prepare(sql);
108
108
  try {
109
- for (const row of paramRows)await stmt.run(...row);
109
+ for (const row of paramRows)await stmt.run(row);
110
110
  } finally{
111
111
  await stmt.close();
112
112
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and nothing else owns this statement's lifetime.\n const stmt = await db.prepare(sql);\n try {\n for (const row of paramRows) await stmt.run(...row);\n } finally {\n await stmt.close();\n }\n },\n BEGIN_WRITE\n );\n },\n };\n return conn;\n}\n"],"names":["stat","rewriteInsert","setMeta","BEGIN_WRITE","withTransaction","CONNECT_OPTS","tursoApi","rewriteFunctions","TursoStatementWrapper","run","params","stmt","get","all","iterate","columns","setReadBigInts","enabled","safeIntegers","checkpointWal","db","exec","err","console","error","message","cacheFilePath","conn","rows","prepare","find","r","name","file","fileSize","path","size","reclaimSpace","turso","connect","experimental","createConnection","String","close","sql","runBatch","paramRows","length","rewritten","flat","row"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/connection.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport type { Database } from '@tursodatabase/database';\nimport { rewriteInsert } from '../batch.ts';\nimport { setMeta } from '../shared.ts';\nimport { BEGIN_WRITE, withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { CONNECT_OPTS, tursoApi } from './native.ts';\nimport { rewriteFunctions } from './sql-functions.ts';\n\n// The client's own Statement class isn't re-exported by name from '@tursodatabase/database',\n// so its type is derived structurally from Database.prepare()'s return type instead.\ntype TursoStatement = Awaited<ReturnType<Database['prepare']>>;\n\n// @tursodatabase/database/compat offers a synchronous escape hatch, but it measured only 4-12%\n// faster than this promise client, so this wraps the async client in the same Connection/Statement shape 1:1.\nclass TursoStatementWrapper implements Statement {\n private stmt: TursoStatement;\n\n constructor(stmt: TursoStatement) {\n this.stmt = stmt;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n return this.stmt.run(...params);\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n return this.stmt.get(...params);\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n return this.stmt.all(...params);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n yield* this.stmt.iterate(...params);\n }\n\n columns(): Array<{ name: string }> {\n return this.stmt.columns();\n }\n\n setReadBigInts(enabled: boolean): void {\n this.stmt.safeIntegers(enabled);\n }\n}\n\n// This client leaves the WAL behind for the next opener, where node:sqlite checkpoints on the last\n// close, so a tree reconciled over and over grows one without bound. Best-effort: close must not throw.\nexport async function checkpointWal(db: Database): Promise<void> {\n try {\n await db.exec('PRAGMA wal_checkpoint(TRUNCATE)');\n } catch (err) {\n console.error(`sense: turso WAL checkpoint failed, the -wal file will keep growing until one succeeds: ${(err as Error).message}`);\n }\n}\n\n// The main cache file's path, or null when unresolvable. The store holds a Database and\n// Connection, not a path (PLAN 3.52), so the path is read from the database itself.\nexport async function cacheFilePath(conn: Connection): Promise<string | null> {\n const rows = (await (await conn.prepare('PRAGMA database_list')).all()) as Array<{ name: string; file: string }>;\n return rows.find((r) => r.name === 'main')?.file || null;\n}\n\nexport async function fileSize(path: string): Promise<number | null> {\n try {\n return (await stat(path)).size;\n } catch {\n return null;\n }\n}\n\n// Reclaims turso#8170's FTS space amplification (PLAN 3.41), on a throwaway connection after the\n// store's own has closed: `vacuum` doubles incremental reconcile cost merely by being enabled (3.54).\nexport async function reclaimSpace(path: string): Promise<void> {\n try {\n const turso = await tursoApi();\n const db = await turso.connect(path, { ...CONNECT_OPTS, experimental: [...CONNECT_OPTS.experimental, 'vacuum'] });\n try {\n await db.exec('VACUUM');\n const size = await fileSize(path);\n // Recorded here, not by the caller: this connection is the only one still open on the file.\n if (size !== null) await setMeta(createConnection(db), 'compact_size', String(size));\n await checkpointWal(db);\n } finally {\n await db.close();\n }\n } catch (err) {\n console.error(`sense: turso VACUUM failed, the cache will keep the space until one succeeds: ${(err as Error).message}`);\n }\n}\n\nexport function createConnection(db: Database): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await db.exec(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new TursoStatementWrapper(await db.prepare(rewriteFunctions(sql)));\n },\n // Folds a plain INSERT into one multi-row VALUES statement (shared rewriteInsert, ../batch.ts):\n // no bind-variable ceiling to chunk against, measured empirically. UPDATE/DELETE keep the per-row loop.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(\n conn,\n async () => {\n const rewritten = rewriteInsert(sql, paramRows.length);\n if (rewritten) {\n const stmt = await db.prepare(rewritten.sql);\n try {\n // One flat array, not spread: spreading tens of thousands of args hits a JS call-stack\n // limit well before turso enforces any variable-count ceiling (measured; see PLAN.md).\n await stmt.run(paramRows.flat());\n } finally {\n await stmt.close();\n }\n return;\n }\n // Finalized here because nothing else will: open() hands back a connection the caller\n // can hold across many batches, and nothing else owns this statement's lifetime.\n const stmt = await db.prepare(sql);\n try {\n for (const row of paramRows) await stmt.run(row);\n } finally {\n await stmt.close();\n }\n },\n BEGIN_WRITE\n );\n },\n };\n return conn;\n}\n"],"names":["stat","rewriteInsert","setMeta","BEGIN_WRITE","withTransaction","CONNECT_OPTS","tursoApi","rewriteFunctions","TursoStatementWrapper","run","params","stmt","get","all","iterate","columns","setReadBigInts","enabled","safeIntegers","checkpointWal","db","exec","err","console","error","message","cacheFilePath","conn","rows","prepare","find","r","name","file","fileSize","path","size","reclaimSpace","turso","connect","experimental","createConnection","String","close","sql","runBatch","paramRows","length","rewritten","flat","row"],"mappings":"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"}
@@ -25,8 +25,8 @@ function ngramSidecar(text) {
25
25
  // Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with
26
26
  // no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.
27
27
  const MAX_FRONTMATTER_COLUMNS = 2000;
28
- // Changed files above which rebuilding the FTS index beats maintaining it per row. Measured
29
- // 2026-08-30; the derivation and its bounds are pinned in this file's spec.
28
+ // Retained policy boundary from 2026-08-30 corpus-scale measurements; the optimum after later
29
+ // update-path changes remains pending matched corpus-scale crossover evidence.
30
30
  export const FTS_REBUILD_THRESHOLD = 250;
31
31
  // No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.
32
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
@@ -58,9 +58,12 @@ export async function reconcileTursoContentWithStrategy(conn, touched, docs, str
58
58
  if (bulk) for (const name of CONTENT_FTS_NAMES)await conn.exec(`DROP INDEX IF EXISTS ${name}`);
59
59
  // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5
60
60
  // content), so vanished and reparsed docs delete in one pass.
61
- if (touched.length > 0) await conn.runBatch('DELETE FROM content WHERE "path" = ?', touched.map((p)=>[
62
- p
63
- ]));
61
+ if (touched.length > 0) {
62
+ const placeholders = touched.map(()=>'?').join(', ');
63
+ await conn.runBatch(`DELETE FROM content WHERE "path" IN (${placeholders})`, [
64
+ touched
65
+ ]);
66
+ }
64
67
  if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));
65
68
  if (bulk) for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
66
69
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// 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\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0)\n 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\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","stemFolded","CONTENT_FTS_DDL","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","tursoFtsStrategy","delta","churn","reparsed","length","vanished","files","reconcileTursoContentWithStrategy","conn","touched","docs","strategy","bulk","name","exec","runBatch","map","p","ddl","reconcileTursoContent","_cfg","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","reconcileContent","recordDuration"],"mappings":"AACA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,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;AAIzC,yFAAyF;AACzF,MAAMC,qBAAqB,CAAC,qKAAqK,CAAC;AAElM,SAASC,WAAWC,GAAc;IAChC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEP,IAAI,EAAE,GAAGK,IAAIG,MAAM;IAC3C,OAAO;QAACH,IAAII,OAAO;QAAEH;QAAOC;QAASP;QAAMJ,WAAWU;QAAQV,WAAWW;QAAUX,WAAWI;QAAOD,aAAaO;QAAQP,aAAaQ;QAAUR,aAAaC;KAAM;AACtK;AAEA,OAAO,SAASU,iBAAiBC,KAAqB;IACpD,MAAMC,QAAQD,MAAME,QAAQ,CAACC,MAAM,GAAGH,MAAMI,QAAQ,CAACD,MAAM;IAC3D,OAAOH,MAAMK,KAAK,CAACF,MAAM,KAAK,KAAKF,QAAQV,wBAAwB,YAAY;AACjF;AAEA,kGAAkG;AAClG,kGAAkG;AAClG,OAAO,eAAee,kCAAkCC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,QAA0B;IACxI,6FAA6F;IAC7F,qDAAqD;IACrD,MAAMC,OAAOD,aAAa;IAC1B,IAAIC,MAAM,KAAK,MAAMC,QAAQzB,kBAAmB,MAAMoB,KAAKM,IAAI,CAAC,CAAC,qBAAqB,EAAED,MAAM;IAE9F,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAIJ,QAAQL,MAAM,GAAG,GACnB,MAAMI,KAAKO,QAAQ,CACjB,wCACAN,QAAQO,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAE1B,IAAIP,KAAKN,MAAM,GAAG,GAAG,MAAMI,KAAKO,QAAQ,CAACtB,oBAAoBiB,KAAKM,GAAG,CAACtB;IACtE,IAAIkB,MAAM,KAAK,MAAMM,OAAO/B,gBAAiB,MAAMqB,KAAKM,IAAI,CAACI;AAC/D;AAEA,eAAeC,sBAAsBX,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAET,KAAqB,EAAEmB,IAAY;IAC9H,MAAMb,kCAAkCC,MAAMC,SAASC,MAAMV,iBAAiBC;AAChF;AAEA,yFAAyF;AACzF,eAAeoB,WAAWb,IAAgB,EAAEc,KAAe;IACzD,KAAK,MAAMT,QAAQS,MAAO,MAAMd,KAAKM,IAAI,CAAC,CAAC,mCAAmC,EAAE/B,WAAW8B,OAAO;AACpG;AAEA,OAAO,MAAMU,eAAiC;IAC5CC,WAAW,IAAMvC;IACjBwC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQnC,yBAAyB;YACnC,MAAM,IAAIV,WACR,gBACA,CAAC,uBAAuB,EAAE6C,MAAM,2DAA2D,EAAEnC,wBAAwB,gPAAgP,CAAC;QAE1W;IACF;IACA8B;IACAM,kBAAkBR;IAClBS,gBAAgB5C;AAClB,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import type { Config } from '../../config/index.ts';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Retained policy boundary from 2026-08-30 corpus-scale measurements; the optimum after later\n// update-path changes remains pending matched corpus-scale crossover evidence.\nexport const FTS_REBUILD_THRESHOLD = 250;\n\nexport type TursoFtsStrategy = 'incremental' | 'rebuild';\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nexport function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy {\n const churn = delta.reparsed.length + delta.vanished.length;\n return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';\n}\n\n// Kept transaction-free because the shared reconcile owns the one write transaction. The explicit\n// strategy is internal composition for real-engine diagnostics; production selects it from delta.\nexport async function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void> {\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = strategy === 'rebuild';\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0) {\n const placeholders = touched.map(() => '?').join(', ');\n await conn.runBatch(`DELETE FROM content WHERE \"path\" IN (${placeholders})`, [touched]);\n }\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n if (bulk) for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n}\n\nasync function reconcileTursoContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, _cfg: Config): Promise<void> {\n await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent: reconcileTursoContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","stemFolded","CONTENT_FTS_DDL","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","tursoFtsStrategy","delta","churn","reparsed","length","vanished","files","reconcileTursoContentWithStrategy","conn","touched","docs","strategy","bulk","name","exec","placeholders","map","join","runBatch","ddl","reconcileTursoContent","_cfg","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","reconcileContent","recordDuration"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sensemaking",
3
- "version": "0.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step.",
5
5
  "keywords": [
6
6
  "markdown",
@@ -77,6 +77,7 @@
77
77
  "format": "tsds format",
78
78
  "prepublishOnly": "tsds validate",
79
79
  "test": "tsds test:node --no-timeouts",
80
+ "test:benchmark-framework": "tsds test:node --no-timeouts test/integration/benchmark-workload-identity.test.ts test/integration/benchmark-row-classes.test.ts test/integration/gate-dependencies.test.ts test/integration/gate-profile.test.ts test/integration/gate-scheduling.test.ts test/integration/quality-retrieval-identity.test.ts test/integration/quality-validity.test.ts test/integration/portable-quality.test.ts test/integration/portable-quality-stage.test.ts test/integration/retained-quality.test.ts test/integration/benchmark-worker-lifetime.test.ts test/integration/benchmark-harness.test.ts test/integration/quality-cache.test.ts",
80
81
  "test:engines": "nvu engines tsds test:node --no-timeouts",
81
82
  "version": "tsds version"
82
83
  },