sensemaking 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cjs/features/embed.js +9 -1
  2. package/dist/cjs/features/embed.js.map +1 -1
  3. package/dist/cjs/features/links.js +30 -8
  4. package/dist/cjs/features/links.js.map +1 -1
  5. package/dist/cjs/features/sections.js +13 -5
  6. package/dist/cjs/features/sections.js.map +1 -1
  7. package/dist/cjs/features/tags.js +7 -4
  8. package/dist/cjs/features/tags.js.map +1 -1
  9. package/dist/cjs/store/duckdb/connection.js +120 -0
  10. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  11. package/dist/cjs/store/duckdb/reconcile.js +11 -113
  12. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  13. package/dist/cjs/store/reconcile.js +14 -37
  14. package/dist/cjs/store/reconcile.js.map +1 -1
  15. package/dist/cjs/store/shared.d.cts +1 -0
  16. package/dist/cjs/store/shared.d.ts +1 -0
  17. package/dist/cjs/store/shared.js +41 -0
  18. package/dist/cjs/store/shared.js.map +1 -1
  19. package/dist/cjs/store/types.d.cts +1 -1
  20. package/dist/cjs/store/types.d.ts +1 -1
  21. package/dist/cjs/store/types.js.map +1 -1
  22. package/dist/esm/features/embed.js +9 -1
  23. package/dist/esm/features/embed.js.map +1 -1
  24. package/dist/esm/features/links.js +26 -9
  25. package/dist/esm/features/links.js.map +1 -1
  26. package/dist/esm/features/sections.js +13 -3
  27. package/dist/esm/features/sections.js.map +1 -1
  28. package/dist/esm/features/tags.js +7 -2
  29. package/dist/esm/features/tags.js.map +1 -1
  30. package/dist/esm/store/duckdb/connection.js +35 -0
  31. package/dist/esm/store/duckdb/connection.js.map +1 -1
  32. package/dist/esm/store/duckdb/reconcile.js +12 -37
  33. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  34. package/dist/esm/store/reconcile.js +11 -12
  35. package/dist/esm/store/reconcile.js.map +1 -1
  36. package/dist/esm/store/shared.d.ts +1 -0
  37. package/dist/esm/store/shared.js +8 -0
  38. package/dist/esm/store/shared.js.map +1 -1
  39. package/dist/esm/store/types.d.ts +1 -1
  40. package/dist/esm/store/types.js.map +1 -1
  41. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/reconcile.ts"],"sourcesContent":["import type { DuckDBValue } from '@duckdb/node-api';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { CORE_FRONTMATTER_COLUMNS } from '../reconcile.ts';\nimport { quoteIdent } from '../shared.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport type { DuckdbConnection } from './connection.ts';\nimport { markContentStale } from './lexical.ts';\nimport { duckdbApi } from './native.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, not FTS-virtual, so rows are maintained here\n// unconditionally; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter\n// column is VARIANT (holds mapValue()'s mixed JS types for one key across files).\n\n// No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):\n// `path` is content's own primary key, so this is a plain per-doc row.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text) VALUES (?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text];\n}\n\n// No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity fence anyway\n// so a runaway frontmatter generator fails with a clear message instead of an unbounded ALTER TABLE loop.\nconst MAX_FRONTMATTER_COLUMNS = 10_000;\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], _delta: ReconcileDelta): Promise<void> {\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 // content changed: the fts index is rebuilt lazily, on the next lexical query that needs it,\n // not here (lexical.ts's FtsIndexState).\n markContentStale(conn);\n}\n\n// DuckDB rejects more than one ALTER command per statement (\"Parser Error: Only one ALTER\n// command per statement is supported\", measured), so every name's clause joins into one string\n// and runs as a single exec() -- one column-add \"leg\" through the driver instead of `names.length`.\n// VARIANT is the only type that can hold the mixed bigint/number/string/null shapes mapValue()\n// produces for one key across files.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n if (names.length === 0) return;\n await conn.exec(names.map((name) => `ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)} VARIANT`).join('; '));\n}\n\n// Appender path for rows that cannot conflict (reconcile.ts's `added`); no ON CONFLICT support, so\n// alignment reads the table's own physical column order fresh -- `columns` omits feature-owned reserved columns (e.g. \"_rank\") that still exist on the table, and get appendDefault().\nasync function insertNew(conn: Connection, table: string, columns: string[], rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n const { variantValue } = await duckdbApi();\n const infoStmt = await conn.prepare(`PRAGMA table_info(${quoteIdent(table)})`);\n const physicalColumns = ((await infoStmt.all()) as Array<{ name: string }>).map((c) => c.name);\n const rowIndexOf = new Map(columns.map((name, i) => [name, i]));\n\n const native = (conn as DuckdbConnection).duckdb;\n const appender = await native.createAppender(table);\n try {\n for (const row of rows) {\n for (const name of physicalColumns) {\n const idx = rowIndexOf.get(name);\n const value = idx === undefined ? undefined : row[idx];\n if (idx === undefined) appender.appendDefault();\n else if (value === null || value === undefined) appender.appendNull();\n else if (CORE_FRONTMATTER_COLUMNS.has(name)) appender.appendValue(value as DuckDBValue);\n else appender.appendVariant(variantValue(value as DuckDBValue));\n }\n appender.endRow();\n }\n appender.flushSync();\n } finally {\n appender.closeSync();\n }\n}\n\nexport const duckdbDialect: ReconcileDialect = {\n beginMode: () => 'BEGIN',\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${count} columns, crossing this store's sanity limit (${MAX_FRONTMATTER_COLUMNS}). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);\n }\n },\n addColumns,\n reconcileContent,\n insertNew,\n};\n"],"names":["SenseError","CORE_FRONTMATTER_COLUMNS","quoteIdent","markContentStale","duckdbApi","INSERT_CONTENT_SQL","contentRow","doc","relPath","search","title","summary","text","MAX_FRONTMATTER_COLUMNS","reconcileContent","conn","touched","docs","_delta","length","runBatch","map","p","addColumns","names","exec","name","join","insertNew","table","columns","rows","variantValue","infoStmt","prepare","physicalColumns","all","c","rowIndexOf","Map","i","native","duckdb","appender","createAppender","row","idx","get","value","undefined","appendDefault","appendNull","has","appendValue","appendVariant","endRow","flushSync","closeSync","duckdbDialect","beginMode","checkColumnLimit","count"],"mappings":"AACA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,wBAAwB,QAAQ,kBAAkB;AAC3D,SAASC,UAAU,QAAQ,eAAe;AAG1C,SAASC,gBAAgB,QAAQ,eAAe;AAChD,SAASC,SAAS,QAAQ,cAAc;AAExC,qFAAqF;AACrF,+FAA+F;AAC/F,4FAA4F;AAC5F,kFAAkF;AAElF,0FAA0F;AAC1F,uEAAuE;AACvE,MAAMC,qBAAqB,CAAC,sEAAsE,CAAC;AAEnG,SAASC,WAAWC,GAAc;IAChC,OAAO;QAACA,IAAIC,OAAO;QAAED,IAAIE,MAAM,CAACC,KAAK;QAAEH,IAAIE,MAAM,CAACE,OAAO;QAAEJ,IAAIE,MAAM,CAACG,IAAI;KAAC;AAC7E;AAEA,0GAA0G;AAC1G,0GAA0G;AAC1G,MAAMC,0BAA0B;AAEhC,eAAeC,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,MAAsB;IAC5G,IAAIF,QAAQG,MAAM,GAAG,GACnB,MAAMJ,KAAKK,QAAQ,CACjB,wCACAJ,QAAQK,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAE1B,IAAIL,KAAKE,MAAM,GAAG,GAAG,MAAMJ,KAAKK,QAAQ,CAACf,oBAAoBY,KAAKI,GAAG,CAACf;IACtE,6FAA6F;IAC7F,yCAAyC;IACzCH,iBAAiBY;AACnB;AAEA,0FAA0F;AAC1F,+FAA+F;AAC/F,oGAAoG;AACpG,+FAA+F;AAC/F,qCAAqC;AACrC,eAAeQ,WAAWR,IAAgB,EAAES,KAAe;IACzD,IAAIA,MAAML,MAAM,KAAK,GAAG;IACxB,MAAMJ,KAAKU,IAAI,CAACD,MAAMH,GAAG,CAAC,CAACK,OAAS,CAAC,mCAAmC,EAAExB,WAAWwB,MAAM,QAAQ,CAAC,EAAEC,IAAI,CAAC;AAC7G;AAEA,mGAAmG;AACnG,uLAAuL;AACvL,eAAeC,UAAUb,IAAgB,EAAEc,KAAa,EAAEC,OAAiB,EAAEC,IAAiB;IAC5F,IAAIA,KAAKZ,MAAM,KAAK,GAAG;IACvB,MAAM,EAAEa,YAAY,EAAE,GAAG,MAAM5B;IAC/B,MAAM6B,WAAW,MAAMlB,KAAKmB,OAAO,CAAC,CAAC,kBAAkB,EAAEhC,WAAW2B,OAAO,CAAC,CAAC;IAC7E,MAAMM,kBAAkB,AAAE,CAAA,MAAMF,SAASG,GAAG,EAAC,EAA+Bf,GAAG,CAAC,CAACgB,IAAMA,EAAEX,IAAI;IAC7F,MAAMY,aAAa,IAAIC,IAAIT,QAAQT,GAAG,CAAC,CAACK,MAAMc,IAAM;YAACd;YAAMc;SAAE;IAE7D,MAAMC,SAAS,AAAC1B,KAA0B2B,MAAM;IAChD,MAAMC,WAAW,MAAMF,OAAOG,cAAc,CAACf;IAC7C,IAAI;QACF,KAAK,MAAMgB,OAAOd,KAAM;YACtB,KAAK,MAAML,QAAQS,gBAAiB;gBAClC,MAAMW,MAAMR,WAAWS,GAAG,CAACrB;gBAC3B,MAAMsB,QAAQF,QAAQG,YAAYA,YAAYJ,GAAG,CAACC,IAAI;gBACtD,IAAIA,QAAQG,WAAWN,SAASO,aAAa;qBACxC,IAAIF,UAAU,QAAQA,UAAUC,WAAWN,SAASQ,UAAU;qBAC9D,IAAIlD,yBAAyBmD,GAAG,CAAC1B,OAAOiB,SAASU,WAAW,CAACL;qBAC7DL,SAASW,aAAa,CAACtB,aAAagB;YAC3C;YACAL,SAASY,MAAM;QACjB;QACAZ,SAASa,SAAS;IACpB,SAAU;QACRb,SAASc,SAAS;IACpB;AACF;AAEA,OAAO,MAAMC,gBAAkC;IAC7CC,WAAW,IAAM;IACjBC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQhD,yBAAyB;YACnC,MAAM,IAAIb,WAAW,gBAAgB,CAAC,uBAAuB,EAAE6D,MAAM,8CAA8C,EAAEhD,wBAAwB,gIAAgI,CAAC;QAChR;IACF;IACAU;IACAT;IACAc;AACF,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/reconcile.ts"],"sourcesContent":["import { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { appendRows, quoteIdent } from '../shared.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { markContentStale } from './lexical.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, not FTS-virtual, so rows are maintained here\n// unconditionally; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter\n// column is VARIANT (holds mapValue()'s mixed JS types for one key across files).\n\n// No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):\n// `path` is content's own primary key, so this is a plain per-doc row.\nconst CONTENT_COLUMNS = ['path', 'title', 'summary', 'text'];\nconst INSERT_CONTENT_SQL = `INSERT INTO content (${CONTENT_COLUMNS.map(quoteIdent).join(', ')}) VALUES (?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text];\n}\n\n// No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity fence anyway\n// so a runaway frontmatter generator fails with a clear message instead of an unbounded ALTER TABLE loop.\nconst MAX_FRONTMATTER_COLUMNS = 10_000;\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], _delta: ReconcileDelta): Promise<void> {\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n // The delete above cleared every touched path, and duckdb holds the cache file for its whole\n // connection, so no second writer can have landed one of these rows: nothing can conflict.\n await appendRows(conn, 'content', CONTENT_COLUMNS, INSERT_CONTENT_SQL, docs.map(contentRow));\n // content changed: the fts index is rebuilt lazily, on the next lexical query that needs it,\n // not here (lexical.ts's FtsIndexState).\n markContentStale(conn);\n}\n\n// DuckDB rejects more than one ALTER command per statement (\"Parser Error: Only one ALTER\n// command per statement is supported\", measured), so every name's clause joins into one string\n// and runs as a single exec() -- one column-add \"leg\" through the driver instead of `names.length`.\n// VARIANT is the only type that can hold the mixed bigint/number/string/null shapes mapValue()\n// produces for one key across files.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n if (names.length === 0) return;\n await conn.exec(names.map((name) => `ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)} VARIANT`).join('; '));\n}\n\nexport const duckdbDialect: ReconcileDialect = {\n beginMode: () => 'BEGIN',\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${count} columns, crossing this store's sanity limit (${MAX_FRONTMATTER_COLUMNS}). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);\n }\n },\n addColumns,\n reconcileContent,\n};\n"],"names":["SenseError","appendRows","quoteIdent","markContentStale","CONTENT_COLUMNS","INSERT_CONTENT_SQL","map","join","contentRow","doc","relPath","search","title","summary","text","MAX_FRONTMATTER_COLUMNS","reconcileContent","conn","touched","docs","_delta","length","runBatch","p","addColumns","names","exec","name","duckdbDialect","beginMode","checkColumnLimit","count"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,UAAU,EAAEC,UAAU,QAAQ,eAAe;AAEtD,SAASC,gBAAgB,QAAQ,eAAe;AAEhD,qFAAqF;AACrF,+FAA+F;AAC/F,4FAA4F;AAC5F,kFAAkF;AAElF,0FAA0F;AAC1F,uEAAuE;AACvE,MAAMC,kBAAkB;IAAC;IAAQ;IAAS;IAAW;CAAO;AAC5D,MAAMC,qBAAqB,CAAC,qBAAqB,EAAED,gBAAgBE,GAAG,CAACJ,YAAYK,IAAI,CAAC,MAAM,qBAAqB,CAAC;AAEpH,SAASC,WAAWC,GAAc;IAChC,OAAO;QAACA,IAAIC,OAAO;QAAED,IAAIE,MAAM,CAACC,KAAK;QAAEH,IAAIE,MAAM,CAACE,OAAO;QAAEJ,IAAIE,MAAM,CAACG,IAAI;KAAC;AAC7E;AAEA,0GAA0G;AAC1G,0GAA0G;AAC1G,MAAMC,0BAA0B;AAEhC,eAAeC,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,MAAsB;IAC5G,IAAIF,QAAQG,MAAM,GAAG,GACnB,MAAMJ,KAAKK,QAAQ,CACjB,wCACAJ,QAAQZ,GAAG,CAAC,CAACiB,IAAM;YAACA;SAAE;IAE1B,6FAA6F;IAC7F,2FAA2F;IAC3F,MAAMtB,WAAWgB,MAAM,WAAWb,iBAAiBC,oBAAoBc,KAAKb,GAAG,CAACE;IAChF,6FAA6F;IAC7F,yCAAyC;IACzCL,iBAAiBc;AACnB;AAEA,0FAA0F;AAC1F,+FAA+F;AAC/F,oGAAoG;AACpG,+FAA+F;AAC/F,qCAAqC;AACrC,eAAeO,WAAWP,IAAgB,EAAEQ,KAAe;IACzD,IAAIA,MAAMJ,MAAM,KAAK,GAAG;IACxB,MAAMJ,KAAKS,IAAI,CAACD,MAAMnB,GAAG,CAAC,CAACqB,OAAS,CAAC,mCAAmC,EAAEzB,WAAWyB,MAAM,QAAQ,CAAC,EAAEpB,IAAI,CAAC;AAC7G;AAEA,OAAO,MAAMqB,gBAAkC;IAC7CC,WAAW,IAAM;IACjBC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQhB,yBAAyB;YACnC,MAAM,IAAIf,WAAW,gBAAgB,CAAC,uBAAuB,EAAE+B,MAAM,8CAA8C,EAAEhB,wBAAwB,gIAAgI,CAAC;QAChR;IACF;IACAS;IACAR;AACF,EAAE"}
@@ -3,7 +3,7 @@ import { progress } from '../output/progress.js';
3
3
  import { listFiles, RESERVED_COLUMNS } from '../scan/index.js';
4
4
  import { reparseFiles } from '../scan/reparse.js';
5
5
  import { recordLockWaitMs } from './lock-wait.js';
6
- import { getColumns, quoteIdent } from './shared.js';
6
+ import { appendRows, getColumns, quoteIdent } from './shared.js';
7
7
  import { featureStage, stageRecorder } from './stages.js';
8
8
  import { withTransaction } from './transaction.js';
9
9
  // One reconcile algorithm shared by every store, parameterised by a per-engine ReconcileDialect
@@ -121,17 +121,13 @@ export async function reconcile(conn, cfg, baseDir, dialect, pool, forcedPaths)
121
121
  if (col === '_parse_error') return doc.parseError;
122
122
  return (_doc_data_col = doc.data[col]) !== null && _doc_data_col !== void 0 ? _doc_data_col : null;
123
123
  });
124
- // A path in `added` has no existing frontmatter row, so it can never conflict: where a
125
- // dialect offers a faster append-only path (insertNew), only the rest go through the upsert.
124
+ // A path in `added` has no existing frontmatter row, so it can never conflict; the rest
125
+ // genuinely can, and keep the upsert.
126
126
  await stages.time('fm-upsert', async ()=>{
127
- if (dialect.insertNew) {
128
- const newDocs = parsedDocs.filter((d)=>addedSet.has(d.relPath));
129
- const updateDocs = parsedDocs.filter((d)=>!addedSet.has(d.relPath));
130
- if (newDocs.length > 0) await dialect.insertNew(conn, 'frontmatter', writableColumns, newDocs.map(toRow));
131
- if (updateDocs.length > 0) await conn.runBatch(insertSql, updateDocs.map(toRow));
132
- } else {
133
- await conn.runBatch(insertSql, parsedDocs.map(toRow));
134
- }
127
+ const newDocs = parsedDocs.filter((d)=>addedSet.has(d.relPath));
128
+ const updateDocs = parsedDocs.filter((d)=>!addedSet.has(d.relPath));
129
+ await appendRows(conn, 'frontmatter', writableColumns, insertSql, newDocs.map(toRow));
130
+ if (updateDocs.length > 0) await conn.runBatch(insertSql, updateDocs.map(toRow));
135
131
  });
136
132
  }
137
133
  // After the upsert, so every doc already has the frontmatter row sqlite's content rowid
@@ -151,7 +147,10 @@ export async function reconcile(conn, cfg, baseDir, dialect, pool, forcedPaths)
151
147
  // DO NOTHING, not a bare INSERT: the added/touched split above comes from a read taken
152
148
  // before this transaction's lock, so a path this process calls "added" can already have
153
149
  // its (path, preset) row committed by a concurrent reconcile -- the row would be identical either way.
154
- if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files ("path", preset) VALUES (?, ?) ON CONFLICT("path", preset) DO NOTHING', presetRows);
150
+ await appendRows(conn, 'preset_files', [
151
+ 'path',
152
+ 'preset'
153
+ ], 'INSERT INTO preset_files ("path", preset) VALUES (?, ?) ON CONFLICT("path", preset) DO NOTHING', presetRows);
155
154
  });
156
155
  // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's
157
156
  // node set, so a lingering vanished row would dilute rank mass across every surviving note.
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/reconcile.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport type { ExtractedDoc, ReconcileDelta } from '../features/types.ts';\nimport { progress } from '../output/progress.ts';\nimport { listFiles, RESERVED_COLUMNS } from '../scan/index.ts';\nimport type { ParsePool } from '../scan/pool.ts';\nimport { reparseFiles } from '../scan/reparse.ts';\nimport { recordLockWaitMs } from './lock-wait.ts';\nimport { getColumns, quoteIdent } from './shared.ts';\nimport { featureStage, type Stages, stageRecorder } from './stages.ts';\nimport { withTransaction } from './transaction.ts';\nimport type { Connection, ReconcileDialect } from './types.ts';\n\n// One reconcile algorithm shared by every store, parameterised by a per-engine ReconcileDialect\n// (types.ts). Ordering is universal, not a dialect concern: ALTER, then the frontmatter upsert,\n// then reconcileContent, then preset_files, then the vanished-frontmatter delete, then feature hooks\n// -- a vanished path's content delete (inside reconcileContent) must precede its frontmatter\n// delete, since sqlite's delete SQL resolves the row via its frontmatter rowid.\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nexport const CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\nexport async function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect, pool?: ParsePool, forcedPaths?: ReadonlySet<string>): Promise<{ parsed: number; warnings: string[]; stages: Stages }> {\n const start = process.hrtime.bigint();\n const features = activeFeatures(cfg);\n const stages = stageRecorder(features.map((f) => f.name));\n const elapsed = () => Number(process.hrtime.bigint() - start) / 1e6;\n const files = await stages.time('list', () => listFiles(cfg, baseDir));\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = await stages.time('existing', async () => {\n const existingStmt = await conn.prepare('SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter');\n return (await existingStmt.all()) as Array<{ path: string; _mtime: number; _size: number }>;\n });\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n // A path whose coverage moved between presets (forcedPaths) but is no longer covered at all is\n // already caught below by !currentSet.has, since it can only be forced by having existed under\n // an old preset's match, which means it was reconciled into `existing` already.\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n // forcedPaths treats an unchanged file as touched because its preset coverage moved, not its\n // stamp -- reconcile still owns add/update/remove and every cross-feature cascade for it.\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size || (forcedPaths?.has(f.relPath) ?? false);\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [], stages: stages.take(elapsed(), 0) };\n\n const seenColumns = await getColumns(conn);\n\n // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short\n // reconciles stay silent (progress() has a threshold).\n const report = progress('reparsing files', toReparse.length);\n // Pool wall time, dispatch to drain, so this stage shares a clock with every other one.\n const { docs: parsedDocs, warnings, newColumns, workerParseMs } = await stages.time('parse', () => reparseFiles(toReparse, features, cfg, seenColumns, report.tick, { pool }));\n report.finish();\n for (const col of newColumns) seenColumns.add(col);\n\n const allColumns = [...seenColumns];\n // Fence before ALTERing: a store's own failure past this point is a raw, engine-specific\n // error with no indication of the boundary or the levers -- dialect.checkColumnLimit names both.\n dialect.checkColumnLimit(allColumns.length);\n // Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a\n // feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).\n const writableColumns = allColumns.filter((c) => CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));\n // ON CONFLICT DO UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --\n // sqlite's content rows are coupled to that rowid.\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')}) ON CONFLICT(\"path\") DO UPDATE SET ${writableColumns\n .filter((c) => c !== 'path')\n .map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`)\n .join(', ')}`;\n\n const added = toReparse.filter((f) => !existing.has(f.relPath)).map((f) => f.relPath);\n const delta: ReconcileDelta = { files, reparsed: parsedDocs.map((d) => d.relPath), added, vanished };\n const addedSet = new Set(added);\n const reparsedExisting = parsedDocs.map((d) => d.relPath).filter((p) => !addedSet.has(p));\n // Paths whose content (and, per feature, other rows) need clearing: gone entirely, or about to\n // be reinserted fresh. Disjoint from `added`, which has nothing to clear.\n const touched = [...vanished, ...reparsedExisting];\n\n const txStart = Date.now();\n await withTransaction(\n conn,\n async () => {\n // Re-read inside the write transaction: newColumns came from a read taken before it opened, so a\n // concurrent reconcile may have added some of them since. ALTER has no IF NOT EXISTS.\n const present = await getColumns(conn);\n const missingColumns = newColumns.filter((col) => !present.has(col));\n if (missingColumns.length > 0) await stages.time('alter', () => dialect.addColumns(conn, missingColumns));\n\n // Revalidated once the lock is held, before this process's own frontmatter upsert below:\n // `added` came from a path read taken before this transaction's lock, so a path still\n // called \"added\" here may already have a content row from a concurrent reconcile that\n // committed while this one waited. One SELECT, not a DELETE per added file -- with no\n // contention it returns the same set `existing` already ruled out, so nothing extra clears.\n let contentTouched = touched;\n if (added.length > 0)\n contentTouched = await stages.time('added-recheck', async () => {\n const currentPathsStmt = await conn.prepare('SELECT \"path\" FROM frontmatter');\n const currentPaths = new Set(((await currentPathsStmt.all()) as Array<{ path: string }>).map((r) => r.path));\n const staleAdded = added.filter((p) => currentPaths.has(p));\n return staleAdded.length > 0 ? [...touched, ...staleAdded] : touched;\n });\n\n if (parsedDocs.length > 0) {\n const toRow = (doc: (typeof parsedDocs)[number]) =>\n writableColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\n return doc.data[col] ?? null;\n });\n // A path in `added` has no existing frontmatter row, so it can never conflict: where a\n // dialect offers a faster append-only path (insertNew), only the rest go through the upsert.\n await stages.time('fm-upsert', async () => {\n if (dialect.insertNew) {\n const newDocs = parsedDocs.filter((d) => addedSet.has(d.relPath));\n const updateDocs = parsedDocs.filter((d) => !addedSet.has(d.relPath));\n if (newDocs.length > 0) await dialect.insertNew(conn, 'frontmatter', writableColumns, newDocs.map(toRow));\n if (updateDocs.length > 0) await conn.runBatch(insertSql, updateDocs.map(toRow));\n } else {\n await conn.runBatch(insertSql, parsedDocs.map(toRow));\n }\n });\n }\n\n // After the upsert, so every doc already has the frontmatter row sqlite's content rowid\n // couples to. ON CONFLICT DO UPDATE preserves that rowid, so a reparse keeps its identity.\n await stages.time('text-index', () => dialect.reconcileContent(conn, contentTouched, parsedDocs, delta, cfg));\n\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;\n // new docs have nothing to clear, which keeps cold builds linear.\n await stages.time('presets', async () => {\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM preset_files WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n const presetRows: unknown[][] = [];\n for (const doc of parsedDocs) for (const presetName of doc.presets) presetRows.push([doc.relPath, presetName]);\n // DO NOTHING, not a bare INSERT: the added/touched split above comes from a read taken\n // before this transaction's lock, so a path this process calls \"added\" can already have\n // its (path, preset) row committed by a concurrent reconcile -- the row would be identical either way.\n if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files (\"path\", preset) VALUES (?, ?) ON CONFLICT(\"path\", preset) DO NOTHING', presetRows);\n });\n\n // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's\n // node set, so a lingering vanished row would dilute rank mass across every surviving note.\n if (vanished.length > 0)\n await stages.time('vanished', () =>\n conn.runBatch(\n 'DELETE FROM frontmatter WHERE \"path\" = ?',\n vanished.map((p) => [p])\n )\n );\n\n // Timed per feature per hook, so link resolution and PageRank are named stages without\n // links.ts or rank.ts knowing anything about this, and a new feature is visible for free.\n if (touched.length > 0) for (const feature of features) await stages.time(featureStage(feature.name, 'remove'), () => feature.remove?.(conn, touched, delta));\n for (const feature of features) {\n const docsForFeature: ExtractedDoc[] = parsedDocs.map((doc) => ({ path: doc.relPath, extracted: doc.extracted[feature.name] }));\n await stages.time(featureStage(feature.name, 'store'), () => feature.store?.(conn, docsForFeature, delta));\n }\n for (const feature of features) await stages.time(featureStage(feature.name, 'after'), () => feature.afterReconcile?.(conn, delta));\n },\n dialect.beginMode()\n );\n\n const durationMs = Date.now() - txStart;\n // Every store, not just the ones with a PRAGMA to derive: connectUnlocked's lock-wait budget needs it too.\n recordLockWaitMs(baseDir, durationMs);\n if (dialect.recordDuration) await stages.time('meta', () => dialect.recordDuration?.(conn, durationMs));\n\n return { parsed: parsedDocs.length, warnings, stages: stages.take(elapsed(), durationMs, workerParseMs) };\n}\n"],"names":["activeFeatures","progress","listFiles","RESERVED_COLUMNS","reparseFiles","recordLockWaitMs","getColumns","quoteIdent","featureStage","stageRecorder","withTransaction","CORE_FRONTMATTER_COLUMNS","Set","reconcile","conn","cfg","baseDir","dialect","pool","forcedPaths","start","process","hrtime","bigint","features","stages","map","f","name","elapsed","Number","files","time","currentSet","relPath","existingRows","existingStmt","prepare","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","take","seenColumns","report","docs","parsedDocs","newColumns","workerParseMs","tick","finish","col","add","allColumns","checkColumnLimit","writableColumns","c","insertSql","join","added","delta","reparsed","d","addedSet","reparsedExisting","p","touched","txStart","Date","now","present","missingColumns","addColumns","contentTouched","currentPathsStmt","currentPaths","staleAdded","toRow","doc","ctimeMs","parseError","data","insertNew","newDocs","updateDocs","runBatch","reconcileContent","presetRows","presetName","presets","push","feature","remove","docsForFeature","extracted","store","afterReconcile","beginMode","durationMs","recordDuration"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/reconcile.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport type { ExtractedDoc, ReconcileDelta } from '../features/types.ts';\nimport { progress } from '../output/progress.ts';\nimport { listFiles, RESERVED_COLUMNS } from '../scan/index.ts';\nimport type { ParsePool } from '../scan/pool.ts';\nimport { reparseFiles } from '../scan/reparse.ts';\nimport { recordLockWaitMs } from './lock-wait.ts';\nimport { appendRows, getColumns, quoteIdent } from './shared.ts';\nimport { featureStage, type Stages, stageRecorder } from './stages.ts';\nimport { withTransaction } from './transaction.ts';\nimport type { Connection, ReconcileDialect } from './types.ts';\n\n// One reconcile algorithm shared by every store, parameterised by a per-engine ReconcileDialect\n// (types.ts). Ordering is universal, not a dialect concern: ALTER, then the frontmatter upsert,\n// then reconcileContent, then preset_files, then the vanished-frontmatter delete, then feature hooks\n// -- a vanished path's content delete (inside reconcileContent) must precede its frontmatter\n// delete, since sqlite's delete SQL resolves the row via its frontmatter rowid.\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nexport const CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\nexport async function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect, pool?: ParsePool, forcedPaths?: ReadonlySet<string>): Promise<{ parsed: number; warnings: string[]; stages: Stages }> {\n const start = process.hrtime.bigint();\n const features = activeFeatures(cfg);\n const stages = stageRecorder(features.map((f) => f.name));\n const elapsed = () => Number(process.hrtime.bigint() - start) / 1e6;\n const files = await stages.time('list', () => listFiles(cfg, baseDir));\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = await stages.time('existing', async () => {\n const existingStmt = await conn.prepare('SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter');\n return (await existingStmt.all()) as Array<{ path: string; _mtime: number; _size: number }>;\n });\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n // A path whose coverage moved between presets (forcedPaths) but is no longer covered at all is\n // already caught below by !currentSet.has, since it can only be forced by having existed under\n // an old preset's match, which means it was reconciled into `existing` already.\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n // forcedPaths treats an unchanged file as touched because its preset coverage moved, not its\n // stamp -- reconcile still owns add/update/remove and every cross-feature cascade for it.\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size || (forcedPaths?.has(f.relPath) ?? false);\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [], stages: stages.take(elapsed(), 0) };\n\n const seenColumns = await getColumns(conn);\n\n // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short\n // reconciles stay silent (progress() has a threshold).\n const report = progress('reparsing files', toReparse.length);\n // Pool wall time, dispatch to drain, so this stage shares a clock with every other one.\n const { docs: parsedDocs, warnings, newColumns, workerParseMs } = await stages.time('parse', () => reparseFiles(toReparse, features, cfg, seenColumns, report.tick, { pool }));\n report.finish();\n for (const col of newColumns) seenColumns.add(col);\n\n const allColumns = [...seenColumns];\n // Fence before ALTERing: a store's own failure past this point is a raw, engine-specific\n // error with no indication of the boundary or the levers -- dialect.checkColumnLimit names both.\n dialect.checkColumnLimit(allColumns.length);\n // Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a\n // feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).\n const writableColumns = allColumns.filter((c) => CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));\n // ON CONFLICT DO UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --\n // sqlite's content rows are coupled to that rowid.\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')}) ON CONFLICT(\"path\") DO UPDATE SET ${writableColumns\n .filter((c) => c !== 'path')\n .map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`)\n .join(', ')}`;\n\n const added = toReparse.filter((f) => !existing.has(f.relPath)).map((f) => f.relPath);\n const delta: ReconcileDelta = { files, reparsed: parsedDocs.map((d) => d.relPath), added, vanished };\n const addedSet = new Set(added);\n const reparsedExisting = parsedDocs.map((d) => d.relPath).filter((p) => !addedSet.has(p));\n // Paths whose content (and, per feature, other rows) need clearing: gone entirely, or about to\n // be reinserted fresh. Disjoint from `added`, which has nothing to clear.\n const touched = [...vanished, ...reparsedExisting];\n\n const txStart = Date.now();\n await withTransaction(\n conn,\n async () => {\n // Re-read inside the write transaction: newColumns came from a read taken before it opened, so a\n // concurrent reconcile may have added some of them since. ALTER has no IF NOT EXISTS.\n const present = await getColumns(conn);\n const missingColumns = newColumns.filter((col) => !present.has(col));\n if (missingColumns.length > 0) await stages.time('alter', () => dialect.addColumns(conn, missingColumns));\n\n // Revalidated once the lock is held, before this process's own frontmatter upsert below:\n // `added` came from a path read taken before this transaction's lock, so a path still\n // called \"added\" here may already have a content row from a concurrent reconcile that\n // committed while this one waited. One SELECT, not a DELETE per added file -- with no\n // contention it returns the same set `existing` already ruled out, so nothing extra clears.\n let contentTouched = touched;\n if (added.length > 0)\n contentTouched = await stages.time('added-recheck', async () => {\n const currentPathsStmt = await conn.prepare('SELECT \"path\" FROM frontmatter');\n const currentPaths = new Set(((await currentPathsStmt.all()) as Array<{ path: string }>).map((r) => r.path));\n const staleAdded = added.filter((p) => currentPaths.has(p));\n return staleAdded.length > 0 ? [...touched, ...staleAdded] : touched;\n });\n\n if (parsedDocs.length > 0) {\n const toRow = (doc: (typeof parsedDocs)[number]) =>\n writableColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\n return doc.data[col] ?? null;\n });\n // A path in `added` has no existing frontmatter row, so it can never conflict; the rest\n // genuinely can, and keep the upsert.\n await stages.time('fm-upsert', async () => {\n const newDocs = parsedDocs.filter((d) => addedSet.has(d.relPath));\n const updateDocs = parsedDocs.filter((d) => !addedSet.has(d.relPath));\n await appendRows(conn, 'frontmatter', writableColumns, insertSql, newDocs.map(toRow));\n if (updateDocs.length > 0) await conn.runBatch(insertSql, updateDocs.map(toRow));\n });\n }\n\n // After the upsert, so every doc already has the frontmatter row sqlite's content rowid\n // couples to. ON CONFLICT DO UPDATE preserves that rowid, so a reparse keeps its identity.\n await stages.time('text-index', () => dialect.reconcileContent(conn, contentTouched, parsedDocs, delta, cfg));\n\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;\n // new docs have nothing to clear, which keeps cold builds linear.\n await stages.time('presets', async () => {\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM preset_files WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n const presetRows: unknown[][] = [];\n for (const doc of parsedDocs) for (const presetName of doc.presets) presetRows.push([doc.relPath, presetName]);\n // DO NOTHING, not a bare INSERT: the added/touched split above comes from a read taken\n // before this transaction's lock, so a path this process calls \"added\" can already have\n // its (path, preset) row committed by a concurrent reconcile -- the row would be identical either way.\n await appendRows(conn, 'preset_files', ['path', 'preset'], 'INSERT INTO preset_files (\"path\", preset) VALUES (?, ?) ON CONFLICT(\"path\", preset) DO NOTHING', presetRows);\n });\n\n // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's\n // node set, so a lingering vanished row would dilute rank mass across every surviving note.\n if (vanished.length > 0)\n await stages.time('vanished', () =>\n conn.runBatch(\n 'DELETE FROM frontmatter WHERE \"path\" = ?',\n vanished.map((p) => [p])\n )\n );\n\n // Timed per feature per hook, so link resolution and PageRank are named stages without\n // links.ts or rank.ts knowing anything about this, and a new feature is visible for free.\n if (touched.length > 0) for (const feature of features) await stages.time(featureStage(feature.name, 'remove'), () => feature.remove?.(conn, touched, delta));\n for (const feature of features) {\n const docsForFeature: ExtractedDoc[] = parsedDocs.map((doc) => ({ path: doc.relPath, extracted: doc.extracted[feature.name] }));\n await stages.time(featureStage(feature.name, 'store'), () => feature.store?.(conn, docsForFeature, delta));\n }\n for (const feature of features) await stages.time(featureStage(feature.name, 'after'), () => feature.afterReconcile?.(conn, delta));\n },\n dialect.beginMode()\n );\n\n const durationMs = Date.now() - txStart;\n // Every store, not just the ones with a PRAGMA to derive: connectUnlocked's lock-wait budget needs it too.\n recordLockWaitMs(baseDir, durationMs);\n if (dialect.recordDuration) await stages.time('meta', () => dialect.recordDuration?.(conn, durationMs));\n\n return { parsed: parsedDocs.length, warnings, stages: stages.take(elapsed(), durationMs, workerParseMs) };\n}\n"],"names":["activeFeatures","progress","listFiles","RESERVED_COLUMNS","reparseFiles","recordLockWaitMs","appendRows","getColumns","quoteIdent","featureStage","stageRecorder","withTransaction","CORE_FRONTMATTER_COLUMNS","Set","reconcile","conn","cfg","baseDir","dialect","pool","forcedPaths","start","process","hrtime","bigint","features","stages","map","f","name","elapsed","Number","files","time","currentSet","relPath","existingRows","existingStmt","prepare","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","take","seenColumns","report","docs","parsedDocs","newColumns","workerParseMs","tick","finish","col","add","allColumns","checkColumnLimit","writableColumns","c","insertSql","join","added","delta","reparsed","d","addedSet","reparsedExisting","p","touched","txStart","Date","now","present","missingColumns","addColumns","contentTouched","currentPathsStmt","currentPaths","staleAdded","toRow","doc","ctimeMs","parseError","data","newDocs","updateDocs","runBatch","reconcileContent","presetRows","presetName","presets","push","feature","remove","docsForFeature","extracted","store","afterReconcile","beginMode","durationMs","recordDuration"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { Connection } from './types.js';
2
+ export declare function appendRows(conn: Connection, table: string, columns: string[], conflictSql: string, rows: unknown[][]): Promise<void>;
2
3
  export declare function quoteIdent(name: string): string;
3
4
  export declare function getColumns(conn: Connection): Promise<Set<string>>;
4
5
  export declare function getMeta(conn: Connection, key: string): Promise<string | null>;
@@ -1,5 +1,13 @@
1
1
  // SQL and meta-table primitives both stores' open()/reconcile() need. Engine-neutral: both
2
2
  // stores' Connection satisfies the same async exec/prepare/runBatch shape (types.ts).
3
+ // Rows that cannot conflict, at whatever speed this connection offers: the append path where the
4
+ // store has one, else `conflictSql` bound the ordinary way. `conflictSql` stays the caller's own
5
+ // guarded statement, so a store without appendRows behaves exactly as it did before.
6
+ export async function appendRows(conn, table, columns, conflictSql, rows) {
7
+ if (rows.length === 0) return;
8
+ if (conn.appendRows) await conn.appendRows(table, columns, rows);
9
+ else await conn.runBatch(conflictSql, rows);
10
+ }
3
11
  export function quoteIdent(name) {
4
12
  return `"${name.split('"').join('""')}"`;
5
13
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/shared.ts"],"sourcesContent":["import type { Connection } from './types.ts';\n\n// SQL and meta-table primitives both stores' open()/reconcile() need. Engine-neutral: both\n// stores' Connection satisfies the same async exec/prepare/runBatch shape (types.ts).\n\nexport function quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\nexport async function getColumns(conn: Connection): Promise<Set<string>> {\n const stmt = await conn.prepare('PRAGMA table_info(frontmatter)');\n const rows = (await stmt.all()) as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nexport async function getMeta(conn: Connection, key: string): Promise<string | null> {\n const stmt = await conn.prepare('SELECT value FROM meta WHERE key = ?');\n const row = (await stmt.get(key)) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport async function setMeta(conn: Connection, key: string, value: string | null): Promise<void> {\n if (value === null) {\n const stmt = await conn.prepare('DELETE FROM meta WHERE key = ?');\n await stmt.run(key);\n return;\n }\n const stmt = await conn.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');\n await stmt.run(key, value);\n}\n\n// Reconcile's own write-transaction duration, for open()'s derived busy_timeout: keep the\n// observed max so a big watcher reconcile's lock hold is what the next open bounds its wait against.\nexport async function recordReconcileDuration(conn: Connection, ms: number): Promise<void> {\n const prevRaw = await getMeta(conn, 'reconcile_max_ms');\n // -1, not 0, so a genuinely 0ms first reconcile (sub-millisecond, common on a tiny tree)\n // still gets recorded instead of losing to the \"nothing recorded yet\" default.\n const prevMax = prevRaw === null ? -1 : Number(prevRaw);\n if (ms > prevMax) await setMeta(conn, 'reconcile_max_ms', String(ms));\n}\n"],"names":["quoteIdent","name","split","join","getColumns","conn","stmt","prepare","rows","all","Set","map","r","getMeta","key","row","get","value","setMeta","run","recordReconcileDuration","ms","prevRaw","prevMax","Number","String"],"mappings":"AAEA,2FAA2F;AAC3F,sFAAsF;AAEtF,OAAO,SAASA,WAAWC,IAAY;IACrC,OAAO,CAAC,CAAC,EAAEA,KAAKC,KAAK,CAAC,KAAKC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1C;AAEA,OAAO,eAAeC,WAAWC,IAAgB;IAC/C,MAAMC,OAAO,MAAMD,KAAKE,OAAO,CAAC;IAChC,MAAMC,OAAQ,MAAMF,KAAKG,GAAG;IAC5B,OAAO,IAAIC,IAAIF,KAAKG,GAAG,CAAC,CAACC,IAAMA,EAAEX,IAAI;AACvC;AAEA,OAAO,eAAeY,QAAQR,IAAgB,EAAES,GAAW;IACzD,MAAMR,OAAO,MAAMD,KAAKE,OAAO,CAAC;IAChC,MAAMQ,MAAO,MAAMT,KAAKU,GAAG,CAACF;IAC5B,OAAOC,MAAMA,IAAIE,KAAK,GAAG;AAC3B;AAEA,OAAO,eAAeC,QAAQb,IAAgB,EAAES,GAAW,EAAEG,KAAoB;IAC/E,IAAIA,UAAU,MAAM;QAClB,MAAMX,OAAO,MAAMD,KAAKE,OAAO,CAAC;QAChC,MAAMD,KAAKa,GAAG,CAACL;QACf;IACF;IACA,MAAMR,OAAO,MAAMD,KAAKE,OAAO,CAAC;IAChC,MAAMD,KAAKa,GAAG,CAACL,KAAKG;AACtB;AAEA,0FAA0F;AAC1F,qGAAqG;AACrG,OAAO,eAAeG,wBAAwBf,IAAgB,EAAEgB,EAAU;IACxE,MAAMC,UAAU,MAAMT,QAAQR,MAAM;IACpC,yFAAyF;IACzF,+EAA+E;IAC/E,MAAMkB,UAAUD,YAAY,OAAO,CAAC,IAAIE,OAAOF;IAC/C,IAAID,KAAKE,SAAS,MAAML,QAAQb,MAAM,oBAAoBoB,OAAOJ;AACnE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/shared.ts"],"sourcesContent":["import type { Connection } from './types.ts';\n\n// SQL and meta-table primitives both stores' open()/reconcile() need. Engine-neutral: both\n// stores' Connection satisfies the same async exec/prepare/runBatch shape (types.ts).\n\n// Rows that cannot conflict, at whatever speed this connection offers: the append path where the\n// store has one, else `conflictSql` bound the ordinary way. `conflictSql` stays the caller's own\n// guarded statement, so a store without appendRows behaves exactly as it did before.\nexport async function appendRows(conn: Connection, table: string, columns: string[], conflictSql: string, rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n if (conn.appendRows) await conn.appendRows(table, columns, rows);\n else await conn.runBatch(conflictSql, rows);\n}\n\nexport function quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\nexport async function getColumns(conn: Connection): Promise<Set<string>> {\n const stmt = await conn.prepare('PRAGMA table_info(frontmatter)');\n const rows = (await stmt.all()) as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nexport async function getMeta(conn: Connection, key: string): Promise<string | null> {\n const stmt = await conn.prepare('SELECT value FROM meta WHERE key = ?');\n const row = (await stmt.get(key)) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport async function setMeta(conn: Connection, key: string, value: string | null): Promise<void> {\n if (value === null) {\n const stmt = await conn.prepare('DELETE FROM meta WHERE key = ?');\n await stmt.run(key);\n return;\n }\n const stmt = await conn.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');\n await stmt.run(key, value);\n}\n\n// Reconcile's own write-transaction duration, for open()'s derived busy_timeout: keep the\n// observed max so a big watcher reconcile's lock hold is what the next open bounds its wait against.\nexport async function recordReconcileDuration(conn: Connection, ms: number): Promise<void> {\n const prevRaw = await getMeta(conn, 'reconcile_max_ms');\n // -1, not 0, so a genuinely 0ms first reconcile (sub-millisecond, common on a tiny tree)\n // still gets recorded instead of losing to the \"nothing recorded yet\" default.\n const prevMax = prevRaw === null ? -1 : Number(prevRaw);\n if (ms > prevMax) await setMeta(conn, 'reconcile_max_ms', String(ms));\n}\n"],"names":["appendRows","conn","table","columns","conflictSql","rows","length","runBatch","quoteIdent","name","split","join","getColumns","stmt","prepare","all","Set","map","r","getMeta","key","row","get","value","setMeta","run","recordReconcileDuration","ms","prevRaw","prevMax","Number","String"],"mappings":"AAEA,2FAA2F;AAC3F,sFAAsF;AAEtF,iGAAiG;AACjG,iGAAiG;AACjG,qFAAqF;AACrF,OAAO,eAAeA,WAAWC,IAAgB,EAAEC,KAAa,EAAEC,OAAiB,EAAEC,WAAmB,EAAEC,IAAiB;IACzH,IAAIA,KAAKC,MAAM,KAAK,GAAG;IACvB,IAAIL,KAAKD,UAAU,EAAE,MAAMC,KAAKD,UAAU,CAACE,OAAOC,SAASE;SACtD,MAAMJ,KAAKM,QAAQ,CAACH,aAAaC;AACxC;AAEA,OAAO,SAASG,WAAWC,IAAY;IACrC,OAAO,CAAC,CAAC,EAAEA,KAAKC,KAAK,CAAC,KAAKC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1C;AAEA,OAAO,eAAeC,WAAWX,IAAgB;IAC/C,MAAMY,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMT,OAAQ,MAAMQ,KAAKE,GAAG;IAC5B,OAAO,IAAIC,IAAIX,KAAKY,GAAG,CAAC,CAACC,IAAMA,EAAET,IAAI;AACvC;AAEA,OAAO,eAAeU,QAAQlB,IAAgB,EAAEmB,GAAW;IACzD,MAAMP,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMO,MAAO,MAAMR,KAAKS,GAAG,CAACF;IAC5B,OAAOC,MAAMA,IAAIE,KAAK,GAAG;AAC3B;AAEA,OAAO,eAAeC,QAAQvB,IAAgB,EAAEmB,GAAW,EAAEG,KAAoB;IAC/E,IAAIA,UAAU,MAAM;QAClB,MAAMV,OAAO,MAAMZ,KAAKa,OAAO,CAAC;QAChC,MAAMD,KAAKY,GAAG,CAACL;QACf;IACF;IACA,MAAMP,OAAO,MAAMZ,KAAKa,OAAO,CAAC;IAChC,MAAMD,KAAKY,GAAG,CAACL,KAAKG;AACtB;AAEA,0FAA0F;AAC1F,qGAAqG;AACrG,OAAO,eAAeG,wBAAwBzB,IAAgB,EAAE0B,EAAU;IACxE,MAAMC,UAAU,MAAMT,QAAQlB,MAAM;IACpC,yFAAyF;IACzF,+EAA+E;IAC/E,MAAM4B,UAAUD,YAAY,OAAO,CAAC,IAAIE,OAAOF;IAC/C,IAAID,KAAKE,SAAS,MAAML,QAAQvB,MAAM,oBAAoB8B,OAAOJ;AACnE"}
@@ -21,6 +21,7 @@ export interface Connection {
21
21
  exec(sql: string): Promise<void>;
22
22
  prepare(sql: string): Promise<Statement>;
23
23
  runBatch(sql: string, paramRows: unknown[][]): Promise<void>;
24
+ appendRows?(table: string, columns: string[], rows: unknown[][]): Promise<void>;
24
25
  }
25
26
  export interface ReconcileDialect {
26
27
  beginMode(): string;
@@ -28,7 +29,6 @@ export interface ReconcileDialect {
28
29
  addColumns(conn: Connection, names: string[]): Promise<void>;
29
30
  reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, cfg: Config): Promise<void>;
30
31
  recordDuration?(conn: Connection, ms: number): Promise<void>;
31
- insertNew?(conn: Connection, table: string, columns: string[], rows: unknown[][]): Promise<void>;
32
32
  }
33
33
  export interface OpenDialect<Handle> {
34
34
  filename: string;
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/types.ts"],"sourcesContent":["// The backing-store interface: a minimal portable statement surface (exec/prepare), plus\n// dedicated interfaces exactly where engines diverge (lexical index, vector scan, raw sql).\n\nimport type { Config, ResolvedConfig, StoreName } from '../config/index.ts';\nimport type { ReconcileDelta } from '../features/types.ts';\nimport type { ParsedDoc } from '../scan/index.ts';\n\n// 'lexical'/'vectors': the store's LexicalIndex/VectorStore is functionally implemented, not\n// present-but-inert. 'sql-functions': the engine can register has/basename/segment as UDFs at all,\n// which turso's client cannot. The rest are finer FTS5-only behaviors; a missing one fails at open\n// or first use. An array, not a bare union, so a runtime check reads the same list the type does.\nexport const CAPABILITY_NAMES = ['phrases', 'snippets', 'lexical', 'vectors', 'sql-functions'] as const;\nexport type Capability = (typeof CAPABILITY_NAMES)[number];\n\nexport interface RunResult {\n changes: number | bigint;\n lastInsertRowid: number | bigint;\n}\n\n// A prepared statement's async surface: every supported engine crosses a real async boundary\n// for a query (DuckDB has no synchronous client), so run/get/all return Promises.\nexport interface Statement {\n run(...params: unknown[]): Promise<RunResult>;\n get(...params: unknown[]): Promise<unknown>;\n all(...params: unknown[]): Promise<unknown[]>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n columns(): Array<{ name: string }>;\n setReadBigInts(enabled: boolean): void;\n}\n\n// Connection surface feature-owned SQL runs against inside a store's hot loops (schema,\n// reconcile), portable so a feature never imports an engine-specific client type.\nexport interface Connection {\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n}\n\n// One reconcile algorithm (src/store/reconcile.ts), parameterised per engine. reconcileContent is\n// the load-bearing member: it lands every content-table change and owns its own multi-step\n// strategy (sqlite FTS5 incremental, duckdb combined delete/insert, turso incremental-or-rebuild).\nexport interface ReconcileDialect {\n // BEGIN mode for the whole reconcile: sqlite/turso 'BEGIN IMMEDIATE', duckdb 'BEGIN'.\n beginMode(): string;\n // Throws SenseError('COLUMN_LIMIT', ...) past this store's own column ceiling and reasoning.\n checkColumnLimit(count: number): void;\n // Adds `names` to frontmatter, already filtered to columns this connection doesn't have yet.\n // sqlite/turso loop (their ADD COLUMN is metadata-only); duckdb issues one statement per call.\n addColumns(conn: Connection, names: string[]): Promise<void>;\n // Deletes content rows for `touched`, inserts rows for `docs`; `delta` carries the tree state a\n // strategy may need. Must not open its own transaction, and must not return before its own\n // multi-step strategy (e.g. turso's DROP/rebuild) completes.\n reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, cfg: Config): Promise<void>;\n // Records this reconcile's write-transaction duration. sqlite/turso use it for open()'s derived\n // busy_timeout; duckdb has no such PRAGMA and omits it.\n recordDuration?(conn: Connection, ms: number): Promise<void>;\n // Inserts rows whose path cannot already exist in `table` (no ON CONFLICT needed), through a\n // faster append-only path. Optional: sqlite/turso omit it and every row goes through the upsert.\n insertNew?(conn: Connection, table: string, columns: string[], rows: unknown[][]): Promise<void>;\n}\n\n// One open algorithm (src/store/open.ts), parameterised per engine. `Handle` is whatever this\n// store needs to close the connection and construct its Store (sqlite: {db}; duckdb:\n// {instance, duckdb}; turso: db) -- opaque to the shared orchestration, threaded through unchanged.\nexport interface OpenDialect<Handle> {\n // Cache filename under STATE_DIR, e.g. 'cache.db'.\n filename: string;\n // Cache shape version, independent of the config's own `version`; bumping it rebuilds an existing tree.\n schemaVersion: string;\n reconcileDialect: ReconcileDialect;\n // Opens the physical connection and applies pragmas due before any SQL runs (sqlite: busy_timeout\n // + WAL; turso: connect-time timeout; duckdb: none).\n connect(dbPath: string, cfg: ResolvedConfig): Promise<{ handle: Handle; conn: Connection }>;\n // Releases the handle, for both the rebuild-and-reopen branch and error cleanup on this attempt.\n close(handle: Handle): Promise<void>;\n // True when connect() failed because another process holds the cache file, which the orchestration\n // retries. Each engine words it differently, so the match is the dialect's own (`native-not-emulated`).\n // Absent for sqlite, whose file lock is shared and whose concurrent-open failure is a different defect.\n // Matching on message text is forced, not chosen: measured 2026-09-02, duckdb throws a plain Error\n // whose only own properties are stack and message, and turso sets code to the constant\n // 'GenericFailure' on every failure alike with rawCode undefined. Neither exposes anything a\n // predicate could switch on, so each dialect pins its engine's wordings and unit-tests them.\n isLocked?(err: Error): boolean;\n // Schema DDL beyond frontmatter/preset_files/meta (content table, feature hooks); sole owner of\n // whether it wraps itself in a write transaction (sqlite: yes, guards a cold-open ALTER race; duckdb/turso: no).\n ensureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;\n // Installs the derived busy_timeout PRAGMA right before reconcile (sqlite/turso); absent for\n // duckdb, which has no such PRAGMA.\n setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;\n createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig): Store;\n}\n\nexport interface FieldStat {\n field: string;\n coverage: number;\n type: string;\n}\n\nexport interface DocumentStore {\n // Frontmatter column names (including internal ones; callers filter).\n columns(): Promise<string[]>;\n // Per-column coverage (non-null count) and observed type set, aggregated in one SQL query,\n // never one row per note. `scopeWhere` is a caller-built WHERE fragment, per LexicalQueryOptions.\n fieldStats(columns: string[], scopeWhere: string): Promise<FieldStat[]>;\n}\n\nexport interface LexicalHit {\n path: string;\n hit: string | null;\n}\n\nexport interface LexicalQueryOptions {\n whereJoin: string;\n whereCond: string;\n scopeCond: string;\n limit: number;\n}\n\nexport interface LexicalIndex {\n // Ranked word-match query with excerpt, scoped by the caller-built SQL fragments (the same\n // fragments narrowByWhere/materializeScope produce elsewhere).\n query(terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;\n}\n\nexport interface VectorCandidate {\n path: string;\n lines: string;\n similarity: number;\n}\n\nexport interface VectorSimilar {\n path: string;\n similarity: number;\n}\n\nexport interface VectorWriteRow {\n path: string;\n chunk: number;\n scale: number;\n vector: Buffer;\n}\n\nexport interface VectorStore {\n // Rows whose vector is NULL: never embedded, or added since.\n pending(): Promise<Array<{ path: string; chunk: number }>>;\n // One batch write per call (never per row) so a provider's embedding batch stays inside a\n // single store method.\n writeVectors(rows: VectorWriteRow[]): Promise<void>;\n candidates(queryVector: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;\n similar(path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]>;\n hasVector(path: string): Promise<boolean>;\n}\n\n// The `sense sql` passthrough: string in, streamed rows out. Each store registers the same\n// sense-supplied functions and applies its own read-bigints/error-translation behavior.\nexport interface RawStatement {\n columns(): Array<{ name: string }>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n}\n\nexport interface SqlSession {\n prepare(sql: string): Promise<RawStatement>;\n}\n\nexport interface Store {\n readonly name: StoreName;\n readonly capabilities: ReadonlySet<Capability>;\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n // One crossing, N rows: every external write loop (search's candidate insert, graph ring's\n // temp-table writes) goes through this instead of looping over `run()`. Same contract as Connection.runBatch.\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Pins one snapshot across multi-statement reads; `map` and `peek` use it. A network-bound\n // write (search's embedding top-up) must stay outside it. Nesting joins the enclosing transaction.\n transaction<T>(fn: () => Promise<T>): Promise<T>;\n docs: DocumentStore;\n lexical: LexicalIndex;\n vectors: VectorStore;\n raw: SqlSession;\n // Engine-level facts for `sense status` (e.g. a derived busy_timeout PRAGMA reading); each\n // store owns what it reports and how it is worded. Empty when there is nothing to report.\n engineStatus(): Promise<Record<string, string>>;\n close(): Promise<void>;\n}\n"],"names":["CAPABILITY_NAMES"],"mappings":"AAAA,yFAAyF;AACzF,4FAA4F;AAM5F,6FAA6F;AAC7F,mGAAmG;AACnG,mGAAmG;AACnG,kGAAkG;AAClG,OAAO,MAAMA,mBAAmB;IAAC;IAAW;IAAY;IAAW;IAAW;CAAgB,CAAU"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/types.ts"],"sourcesContent":["// The backing-store interface: a minimal portable statement surface (exec/prepare), plus\n// dedicated interfaces exactly where engines diverge (lexical index, vector scan, raw sql).\n\nimport type { Config, ResolvedConfig, StoreName } from '../config/index.ts';\nimport type { ReconcileDelta } from '../features/types.ts';\nimport type { ParsedDoc } from '../scan/index.ts';\n\n// 'lexical'/'vectors': the store's LexicalIndex/VectorStore is functionally implemented, not\n// present-but-inert. 'sql-functions': the engine can register has/basename/segment as UDFs at all,\n// which turso's client cannot. The rest are finer FTS5-only behaviors; a missing one fails at open\n// or first use. An array, not a bare union, so a runtime check reads the same list the type does.\nexport const CAPABILITY_NAMES = ['phrases', 'snippets', 'lexical', 'vectors', 'sql-functions'] as const;\nexport type Capability = (typeof CAPABILITY_NAMES)[number];\n\nexport interface RunResult {\n changes: number | bigint;\n lastInsertRowid: number | bigint;\n}\n\n// A prepared statement's async surface: every supported engine crosses a real async boundary\n// for a query (DuckDB has no synchronous client), so run/get/all return Promises.\nexport interface Statement {\n run(...params: unknown[]): Promise<RunResult>;\n get(...params: unknown[]): Promise<unknown>;\n all(...params: unknown[]): Promise<unknown[]>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n columns(): Array<{ name: string }>;\n setReadBigInts(enabled: boolean): void;\n}\n\n// Connection surface feature-owned SQL runs against inside a store's hot loops (schema,\n// reconcile), portable so a feature never imports an engine-specific client type.\nexport interface Connection {\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Bulk-inserts rows that cannot conflict, through a path that binds no per-value parameters.\n // Optional: a store that has nothing faster than its own INSERT omits it and callers fall back\n // (appendRows in shared.ts). `columns` names the values each row carries; a table column the\n // caller does not write takes its default.\n appendRows?(table: string, columns: string[], rows: unknown[][]): Promise<void>;\n}\n\n// One reconcile algorithm (src/store/reconcile.ts), parameterised per engine. reconcileContent is\n// the load-bearing member: it lands every content-table change and owns its own multi-step\n// strategy (sqlite FTS5 incremental, duckdb combined delete/insert, turso incremental-or-rebuild).\nexport interface ReconcileDialect {\n // BEGIN mode for the whole reconcile: sqlite/turso 'BEGIN IMMEDIATE', duckdb 'BEGIN'.\n beginMode(): string;\n // Throws SenseError('COLUMN_LIMIT', ...) past this store's own column ceiling and reasoning.\n checkColumnLimit(count: number): void;\n // Adds `names` to frontmatter, already filtered to columns this connection doesn't have yet.\n // sqlite/turso loop (their ADD COLUMN is metadata-only); duckdb issues one statement per call.\n addColumns(conn: Connection, names: string[]): Promise<void>;\n // Deletes content rows for `touched`, inserts rows for `docs`; `delta` carries the tree state a\n // strategy may need. Must not open its own transaction, and must not return before its own\n // multi-step strategy (e.g. turso's DROP/rebuild) completes.\n reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta, cfg: Config): Promise<void>;\n // Records this reconcile's write-transaction duration. sqlite/turso use it for open()'s derived\n // busy_timeout; duckdb has no such PRAGMA and omits it.\n recordDuration?(conn: Connection, ms: number): Promise<void>;\n}\n\n// One open algorithm (src/store/open.ts), parameterised per engine. `Handle` is whatever this\n// store needs to close the connection and construct its Store (sqlite: {db}; duckdb:\n// {instance, duckdb}; turso: db) -- opaque to the shared orchestration, threaded through unchanged.\nexport interface OpenDialect<Handle> {\n // Cache filename under STATE_DIR, e.g. 'cache.db'.\n filename: string;\n // Cache shape version, independent of the config's own `version`; bumping it rebuilds an existing tree.\n schemaVersion: string;\n reconcileDialect: ReconcileDialect;\n // Opens the physical connection and applies pragmas due before any SQL runs (sqlite: busy_timeout\n // + WAL; turso: connect-time timeout; duckdb: none).\n connect(dbPath: string, cfg: ResolvedConfig): Promise<{ handle: Handle; conn: Connection }>;\n // Releases the handle, for both the rebuild-and-reopen branch and error cleanup on this attempt.\n close(handle: Handle): Promise<void>;\n // True when connect() failed because another process holds the cache file, which the orchestration\n // retries. Each engine words it differently, so the match is the dialect's own (`native-not-emulated`).\n // Absent for sqlite, whose file lock is shared and whose concurrent-open failure is a different defect.\n // Matching on message text is forced, not chosen: measured 2026-09-02, duckdb throws a plain Error\n // whose only own properties are stack and message, and turso sets code to the constant\n // 'GenericFailure' on every failure alike with rawCode undefined. Neither exposes anything a\n // predicate could switch on, so each dialect pins its engine's wordings and unit-tests them.\n isLocked?(err: Error): boolean;\n // Schema DDL beyond frontmatter/preset_files/meta (content table, feature hooks); sole owner of\n // whether it wraps itself in a write transaction (sqlite: yes, guards a cold-open ALTER race; duckdb/turso: no).\n ensureSchema(handle: Handle, conn: Connection, cfg: Config): Promise<void>;\n // Installs the derived busy_timeout PRAGMA right before reconcile (sqlite/turso); absent for\n // duckdb, which has no such PRAGMA.\n setDerivedBusyTimeout?(handle: Handle, conn: Connection, ms: number): Promise<void>;\n createStore(handle: Handle, conn: Connection, cfg: ResolvedConfig): Store;\n}\n\nexport interface FieldStat {\n field: string;\n coverage: number;\n type: string;\n}\n\nexport interface DocumentStore {\n // Frontmatter column names (including internal ones; callers filter).\n columns(): Promise<string[]>;\n // Per-column coverage (non-null count) and observed type set, aggregated in one SQL query,\n // never one row per note. `scopeWhere` is a caller-built WHERE fragment, per LexicalQueryOptions.\n fieldStats(columns: string[], scopeWhere: string): Promise<FieldStat[]>;\n}\n\nexport interface LexicalHit {\n path: string;\n hit: string | null;\n}\n\nexport interface LexicalQueryOptions {\n whereJoin: string;\n whereCond: string;\n scopeCond: string;\n limit: number;\n}\n\nexport interface LexicalIndex {\n // Ranked word-match query with excerpt, scoped by the caller-built SQL fragments (the same\n // fragments narrowByWhere/materializeScope produce elsewhere).\n query(terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;\n}\n\nexport interface VectorCandidate {\n path: string;\n lines: string;\n similarity: number;\n}\n\nexport interface VectorSimilar {\n path: string;\n similarity: number;\n}\n\nexport interface VectorWriteRow {\n path: string;\n chunk: number;\n scale: number;\n vector: Buffer;\n}\n\nexport interface VectorStore {\n // Rows whose vector is NULL: never embedded, or added since.\n pending(): Promise<Array<{ path: string; chunk: number }>>;\n // One batch write per call (never per row) so a provider's embedding batch stays inside a\n // single store method.\n writeVectors(rows: VectorWriteRow[]): Promise<void>;\n candidates(queryVector: Float32Array, storeDims: number, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]>;\n similar(path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]>;\n hasVector(path: string): Promise<boolean>;\n}\n\n// The `sense sql` passthrough: string in, streamed rows out. Each store registers the same\n// sense-supplied functions and applies its own read-bigints/error-translation behavior.\nexport interface RawStatement {\n columns(): Array<{ name: string }>;\n iterate(...params: unknown[]): AsyncIterable<unknown>;\n}\n\nexport interface SqlSession {\n prepare(sql: string): Promise<RawStatement>;\n}\n\nexport interface Store {\n readonly name: StoreName;\n readonly capabilities: ReadonlySet<Capability>;\n exec(sql: string): Promise<void>;\n prepare(sql: string): Promise<Statement>;\n // One crossing, N rows: every external write loop (search's candidate insert, graph ring's\n // temp-table writes) goes through this instead of looping over `run()`. Same contract as Connection.runBatch.\n runBatch(sql: string, paramRows: unknown[][]): Promise<void>;\n // Pins one snapshot across multi-statement reads; `map` and `peek` use it. A network-bound\n // write (search's embedding top-up) must stay outside it. Nesting joins the enclosing transaction.\n transaction<T>(fn: () => Promise<T>): Promise<T>;\n docs: DocumentStore;\n lexical: LexicalIndex;\n vectors: VectorStore;\n raw: SqlSession;\n // Engine-level facts for `sense status` (e.g. a derived busy_timeout PRAGMA reading); each\n // store owns what it reports and how it is worded. Empty when there is nothing to report.\n engineStatus(): Promise<Record<string, string>>;\n close(): Promise<void>;\n}\n"],"names":["CAPABILITY_NAMES"],"mappings":"AAAA,yFAAyF;AACzF,4FAA4F;AAM5F,6FAA6F;AAC7F,mGAAmG;AACnG,mGAAmG;AACnG,kGAAkG;AAClG,OAAO,MAAMA,mBAAmB;IAAC;IAAW;IAAY;IAAW;IAAW;CAAgB,CAAU"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sensemaking",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
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",