sensemaking 0.24.0 → 0.24.2

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 (64) hide show
  1. package/README.md +2 -1
  2. package/dist/cjs/cli/status.js +6 -3
  3. package/dist/cjs/cli/status.js.map +1 -1
  4. package/dist/cjs/cli/watch.js +1 -1
  5. package/dist/cjs/cli/watch.js.map +1 -1
  6. package/dist/cjs/commands/related.js +2 -2
  7. package/dist/cjs/commands/related.js.map +1 -1
  8. package/dist/cjs/commands/search.js +3 -2
  9. package/dist/cjs/commands/search.js.map +1 -1
  10. package/dist/cjs/config/access.js +7 -1
  11. package/dist/cjs/config/access.js.map +1 -1
  12. package/dist/cjs/config/load.js +6 -1
  13. package/dist/cjs/config/load.js.map +1 -1
  14. package/dist/cjs/config/types.d.cts +3 -0
  15. package/dist/cjs/config/types.d.ts +3 -0
  16. package/dist/cjs/config/types.js.map +1 -1
  17. package/dist/cjs/config/validate.js +4 -0
  18. package/dist/cjs/config/validate.js.map +1 -1
  19. package/dist/cjs/embed/query.js +4 -4
  20. package/dist/cjs/embed/query.js.map +1 -1
  21. package/dist/cjs/store/cache.js +2 -1
  22. package/dist/cjs/store/cache.js.map +1 -1
  23. package/dist/cjs/store/open.js +13 -6
  24. package/dist/cjs/store/open.js.map +1 -1
  25. package/dist/cjs/store/turso/reconcile.d.cts +6 -1
  26. package/dist/cjs/store/turso/reconcile.d.ts +6 -1
  27. package/dist/cjs/store/turso/reconcile.js +32 -5
  28. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  29. package/dist/cjs/watch.d.cts +1 -1
  30. package/dist/cjs/watch.d.ts +1 -1
  31. package/dist/cjs/watch.js +5 -4
  32. package/dist/cjs/watch.js.map +1 -1
  33. package/dist/esm/cli/status.js +6 -3
  34. package/dist/esm/cli/status.js.map +1 -1
  35. package/dist/esm/cli/watch.js +1 -1
  36. package/dist/esm/cli/watch.js.map +1 -1
  37. package/dist/esm/commands/related.js +2 -1
  38. package/dist/esm/commands/related.js.map +1 -1
  39. package/dist/esm/commands/search.js +2 -1
  40. package/dist/esm/commands/search.js.map +1 -1
  41. package/dist/esm/config/access.js +7 -0
  42. package/dist/esm/config/access.js.map +1 -1
  43. package/dist/esm/config/load.js +6 -1
  44. package/dist/esm/config/load.js.map +1 -1
  45. package/dist/esm/config/types.d.ts +3 -0
  46. package/dist/esm/config/types.js.map +1 -1
  47. package/dist/esm/config/validate.js +4 -0
  48. package/dist/esm/config/validate.js.map +1 -1
  49. package/dist/esm/embed/query.js +4 -4
  50. package/dist/esm/embed/query.js.map +1 -1
  51. package/dist/esm/store/cache.js +2 -1
  52. package/dist/esm/store/cache.js.map +1 -1
  53. package/dist/esm/store/open.js +14 -4
  54. package/dist/esm/store/open.js.map +1 -1
  55. package/dist/esm/store/turso/reconcile.d.ts +6 -1
  56. package/dist/esm/store/turso/reconcile.js +11 -3
  57. package/dist/esm/store/turso/reconcile.js.map +1 -1
  58. package/dist/esm/watch.d.ts +1 -1
  59. package/dist/esm/watch.js +5 -4
  60. package/dist/esm/watch.js.map +1 -1
  61. package/package.json +1 -1
  62. package/schema.json +6 -1
  63. package/skills/sense-setup/EXAMPLES.md +21 -0
  64. package/skills/sense-setup/SKILL.md +2 -2
@@ -108,10 +108,10 @@ export async function embedPending(store, cfg, baseDir) {
108
108
  // Best chunk per file by cosine, its line range riding along; FTS5 operators are stripped as
109
109
  // lexical syntax. Similarity comes back because the fused score cannot express match quality.
110
110
  export async function semanticCandidates(store, cfg, terms, fetch, allowed) {
111
- var _terms_match;
112
- const baseDir = cfg.baseDir;
113
- if (!baseDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a config with baseDir (use loadConfig/open)');
114
- await embedPending(store, cfg, baseDir);
111
+ var _cfg_rootDir, _terms_match;
112
+ const rootDir = (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir;
113
+ if (!rootDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a resolved config (use loadConfig/open)');
114
+ await embedPending(store, cfg, rootDir);
115
115
  const provider = await getProvider(cfg);
116
116
  const storeDims = Math.min(STORE_DIMS, provider.dims);
117
117
  const text = ((_terms_match = terms.match(/[\p{L}\p{N}]+/gu)) !== null && _terms_match !== void 0 ? _terms_match : []).filter((t)=>![
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/query.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Config, ResolvedConfig } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport type { Chunk } from '../features/embed.ts';\nimport { embed } from '../features/embed.ts';\nimport { progress } from '../output/progress.ts';\nimport { parseFile } from '../scan/index.ts';\nimport type { Store, VectorCandidate, VectorSimilar } from '../store/types.ts';\nimport { takeChunkText } from './handoff.ts';\nimport { checkLanguageFit } from './langfit.ts';\nimport { getProvider } from './registry.ts';\nimport { STORE_DIMS } from './types.ts';\n\n// Slice + re-normalize (Matryoshka); optionally round through int8 storage. Exported for\n// benchmark/lib/embed.mjs, which scores the same lever math offline.\nexport function toStore(full: Float32Array, dims: number, int8: boolean): { v: Float32Array; scale: number } {\n const v = new Float32Array(dims);\n let norm = 0;\n for (let d = 0; d < dims; d++) norm += full[d] * full[d];\n norm = Math.sqrt(norm) + 1e-32;\n for (let d = 0; d < dims; d++) v[d] = full[d] / norm;\n if (!int8) return { v, scale: 1 };\n let max = 0;\n for (let d = 0; d < dims; d++) max = Math.max(max, Math.abs(v[d]));\n return { v, scale: max / 127 || 1 };\n}\n\n// Embed rows whose vector is NULL. Reconcile hands its chunk text over in memory when it ran in\n// this process (handoff.ts); a row left pending by an earlier command re-derives from the file.\nexport async function embedPending(store: Store, cfg: Config, baseDir: string): Promise<void> {\n const provider = await getProvider(cfg); // throws EMBED_DISABLED before touching the table\n const dirty = await store.vectors.pending();\n if (dirty.length === 0) return;\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n\n const byPath = new Map<string, number[]>();\n for (const row of dirty) {\n const list = byPath.get(row.path) ?? [];\n list.push(row.chunk);\n byPath.set(row.path, list);\n }\n\n const textByPath = takeChunkText(store) ?? new Map<string, string[]>();\n\n const jobs: Array<{ path: string; chunk: number; text: string }> = [];\n for (const [path, chunkIdxs] of byPath) {\n const texts = textByPath.get(path);\n if (texts && chunkIdxs.every((idx) => idx < texts.length)) {\n // A stat, not a parse: a file that vanished since reconcile is skipped here even though its\n // text comes from memory, and the next reconcile removes its rows.\n if (!existsSync(join(baseDir, path))) continue;\n for (const idx of chunkIdxs) jobs.push({ path, chunk: idx, text: texts[idx] });\n continue;\n }\n // Reconcile ran in another process (`sense map` earlier, or a background `sense watch`), so\n // its chunk text is gone: re-derive this one file.\n let chunks: Chunk[];\n try {\n chunks = parseFile({ relPath: path, absPath: join(baseDir, path), mtimeMs: 0, ctimeMs: 0, size: 0, presets: [], embed: true }, [embed], cfg).doc.extracted.embed as Chunk[];\n } catch {\n continue; // vanished since reconcile; the next reconcile removes its rows\n }\n for (const idx of chunkIdxs) if (chunks[idx]) jobs.push({ path, chunk: idx, text: chunks[idx].text });\n }\n\n // Over the exact texts about to be embedded, before any of them are: a mismatch fails\n // this run loudly instead of quietly storing vectors from the wrong model.\n await checkLanguageFit(\n store,\n provider,\n jobs.map((j) => j.text)\n );\n\n // The lazy build is the one long silence a first search hits (measured 23s at\n // 26k notes); progress makes it distinguishable from a hang.\n const report = progress('embedding chunks', jobs.length);\n for (let i = 0; i < jobs.length; i += provider.batchCap) {\n const batch = jobs.slice(i, i + provider.batchCap);\n const vectors = await provider.embedDocuments(batch.map((j) => j.text));\n await store.vectors.writeVectors(\n batch.map((job, j) => {\n const { v, scale } = toStore(vectors[j], storeDims, true);\n const q = new Int8Array(storeDims);\n for (let d = 0; d < storeDims; d++) q[d] = Math.round(v[d] / scale);\n return { path: job.path, chunk: job.chunk, scale, vector: Buffer.from(q.buffer) };\n })\n );\n report.tick(Math.min(i + provider.batchCap, jobs.length));\n }\n report.finish();\n}\n\n// Best chunk per file by cosine, its line range riding along; FTS5 operators are stripped as\n// lexical syntax. Similarity comes back because the fused score cannot express match quality.\nexport async function semanticCandidates(store: Store, cfg: Config, terms: string, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const baseDir = (cfg as Partial<ResolvedConfig>).baseDir;\n if (!baseDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a config with baseDir (use loadConfig/open)');\n await embedPending(store, cfg, baseDir);\n\n const provider = await getProvider(cfg);\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n const text = (terms.match(/[\\p{L}\\p{N}]+/gu) ?? []).filter((t) => !['AND', 'OR', 'NOT', 'NEAR'].includes(t)).join(' ');\n const { v: qv } = toStore(await provider.embedQuery(text), storeDims, false);\n\n return store.vectors.candidates(qv, storeDims, fetch, allowed);\n}\n\n// Whether a note has any chunk with a vector. Distinguishes \"nothing is near this note\" from\n// \"this note has no text\", which look the same in an empty result.\nexport async function hasEmbedding(store: Store, path: string): Promise<boolean> {\n return store.vectors.hasVector(path);\n}\n\n// Note-to-note similarity is the max cosine over (target chunk, other chunk) pairs, one linear\n// scan of stored vectors.\nexport async function similarNotes(store: Store, _cfg: Config, path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]> {\n return store.vectors.similar(path, opts);\n}\n"],"names":["existsSync","join","SenseError","embed","progress","parseFile","takeChunkText","checkLanguageFit","getProvider","STORE_DIMS","toStore","full","dims","int8","v","Float32Array","norm","d","Math","sqrt","scale","max","abs","embedPending","store","cfg","baseDir","provider","dirty","vectors","pending","length","storeDims","min","byPath","Map","row","list","get","path","push","chunk","set","textByPath","jobs","chunkIdxs","texts","every","idx","text","chunks","relPath","absPath","mtimeMs","ctimeMs","size","presets","doc","extracted","map","j","report","i","batchCap","batch","slice","embedDocuments","writeVectors","job","q","Int8Array","round","vector","Buffer","from","buffer","tick","finish","semanticCandidates","terms","fetch","allowed","match","filter","t","includes","qv","embedQuery","candidates","hasEmbedding","hasVector","similarNotes","_cfg","opts","similar"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/query.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Config, ResolvedConfig } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport type { Chunk } from '../features/embed.ts';\nimport { embed } from '../features/embed.ts';\nimport { progress } from '../output/progress.ts';\nimport { parseFile } from '../scan/index.ts';\nimport type { Store, VectorCandidate, VectorSimilar } from '../store/types.ts';\nimport { takeChunkText } from './handoff.ts';\nimport { checkLanguageFit } from './langfit.ts';\nimport { getProvider } from './registry.ts';\nimport { STORE_DIMS } from './types.ts';\n\n// Slice + re-normalize (Matryoshka); optionally round through int8 storage. Exported for\n// benchmark/lib/embed.mjs, which scores the same lever math offline.\nexport function toStore(full: Float32Array, dims: number, int8: boolean): { v: Float32Array; scale: number } {\n const v = new Float32Array(dims);\n let norm = 0;\n for (let d = 0; d < dims; d++) norm += full[d] * full[d];\n norm = Math.sqrt(norm) + 1e-32;\n for (let d = 0; d < dims; d++) v[d] = full[d] / norm;\n if (!int8) return { v, scale: 1 };\n let max = 0;\n for (let d = 0; d < dims; d++) max = Math.max(max, Math.abs(v[d]));\n return { v, scale: max / 127 || 1 };\n}\n\n// Embed rows whose vector is NULL. Reconcile hands its chunk text over in memory when it ran in\n// this process (handoff.ts); a row left pending by an earlier command re-derives from the file.\nexport async function embedPending(store: Store, cfg: Config, baseDir: string): Promise<void> {\n const provider = await getProvider(cfg); // throws EMBED_DISABLED before touching the table\n const dirty = await store.vectors.pending();\n if (dirty.length === 0) return;\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n\n const byPath = new Map<string, number[]>();\n for (const row of dirty) {\n const list = byPath.get(row.path) ?? [];\n list.push(row.chunk);\n byPath.set(row.path, list);\n }\n\n const textByPath = takeChunkText(store) ?? new Map<string, string[]>();\n\n const jobs: Array<{ path: string; chunk: number; text: string }> = [];\n for (const [path, chunkIdxs] of byPath) {\n const texts = textByPath.get(path);\n if (texts && chunkIdxs.every((idx) => idx < texts.length)) {\n // A stat, not a parse: a file that vanished since reconcile is skipped here even though its\n // text comes from memory, and the next reconcile removes its rows.\n if (!existsSync(join(baseDir, path))) continue;\n for (const idx of chunkIdxs) jobs.push({ path, chunk: idx, text: texts[idx] });\n continue;\n }\n // Reconcile ran in another process (`sense map` earlier, or a background `sense watch`), so\n // its chunk text is gone: re-derive this one file.\n let chunks: Chunk[];\n try {\n chunks = parseFile({ relPath: path, absPath: join(baseDir, path), mtimeMs: 0, ctimeMs: 0, size: 0, presets: [], embed: true }, [embed], cfg).doc.extracted.embed as Chunk[];\n } catch {\n continue; // vanished since reconcile; the next reconcile removes its rows\n }\n for (const idx of chunkIdxs) if (chunks[idx]) jobs.push({ path, chunk: idx, text: chunks[idx].text });\n }\n\n // Over the exact texts about to be embedded, before any of them are: a mismatch fails\n // this run loudly instead of quietly storing vectors from the wrong model.\n await checkLanguageFit(\n store,\n provider,\n jobs.map((j) => j.text)\n );\n\n // The lazy build is the one long silence a first search hits (measured 23s at\n // 26k notes); progress makes it distinguishable from a hang.\n const report = progress('embedding chunks', jobs.length);\n for (let i = 0; i < jobs.length; i += provider.batchCap) {\n const batch = jobs.slice(i, i + provider.batchCap);\n const vectors = await provider.embedDocuments(batch.map((j) => j.text));\n await store.vectors.writeVectors(\n batch.map((job, j) => {\n const { v, scale } = toStore(vectors[j], storeDims, true);\n const q = new Int8Array(storeDims);\n for (let d = 0; d < storeDims; d++) q[d] = Math.round(v[d] / scale);\n return { path: job.path, chunk: job.chunk, scale, vector: Buffer.from(q.buffer) };\n })\n );\n report.tick(Math.min(i + provider.batchCap, jobs.length));\n }\n report.finish();\n}\n\n// Best chunk per file by cosine, its line range riding along; FTS5 operators are stripped as\n// lexical syntax. Similarity comes back because the fused score cannot express match quality.\nexport async function semanticCandidates(store: Store, cfg: Config, terms: string, fetch: number, allowed?: Set<string>): Promise<VectorCandidate[]> {\n const rootDir = (cfg as Partial<ResolvedConfig>).rootDir ?? (cfg as Partial<ResolvedConfig>).baseDir;\n if (!rootDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a resolved config (use loadConfig/open)');\n await embedPending(store, cfg, rootDir);\n\n const provider = await getProvider(cfg);\n const storeDims = Math.min(STORE_DIMS, provider.dims);\n const text = (terms.match(/[\\p{L}\\p{N}]+/gu) ?? []).filter((t) => !['AND', 'OR', 'NOT', 'NEAR'].includes(t)).join(' ');\n const { v: qv } = toStore(await provider.embedQuery(text), storeDims, false);\n\n return store.vectors.candidates(qv, storeDims, fetch, allowed);\n}\n\n// Whether a note has any chunk with a vector. Distinguishes \"nothing is near this note\" from\n// \"this note has no text\", which look the same in an empty result.\nexport async function hasEmbedding(store: Store, path: string): Promise<boolean> {\n return store.vectors.hasVector(path);\n}\n\n// Note-to-note similarity is the max cosine over (target chunk, other chunk) pairs, one linear\n// scan of stored vectors.\nexport async function similarNotes(store: Store, _cfg: Config, path: string, opts: { exclude: Set<string>; allowed?: Set<string>; k: number }): Promise<VectorSimilar[]> {\n return store.vectors.similar(path, opts);\n}\n"],"names":["existsSync","join","SenseError","embed","progress","parseFile","takeChunkText","checkLanguageFit","getProvider","STORE_DIMS","toStore","full","dims","int8","v","Float32Array","norm","d","Math","sqrt","scale","max","abs","embedPending","store","cfg","baseDir","provider","dirty","vectors","pending","length","storeDims","min","byPath","Map","row","list","get","path","push","chunk","set","textByPath","jobs","chunkIdxs","texts","every","idx","text","chunks","relPath","absPath","mtimeMs","ctimeMs","size","presets","doc","extracted","map","j","report","i","batchCap","batch","slice","embedDocuments","writeVectors","job","q","Int8Array","round","vector","Buffer","from","buffer","tick","finish","semanticCandidates","terms","fetch","allowed","rootDir","match","filter","t","includes","qv","embedQuery","candidates","hasEmbedding","hasVector","similarNotes","_cfg","opts","similar"],"mappings":"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"}
@@ -4,7 +4,8 @@ import { STATE_DIR } from '../config/index.js';
4
4
  // Deletes the cache directory and nothing else: the next open() reconciles anyway. Engine-neutral,
5
5
  // since every store keeps its files under the same STATE_DIR.
6
6
  export function clearCache(cfg) {
7
- rmSync(join(cfg.baseDir, STATE_DIR), {
7
+ var _cfg_configDir;
8
+ rmSync(join((_cfg_configDir = cfg.configDir) !== null && _cfg_configDir !== void 0 ? _cfg_configDir : cfg.baseDir, STATE_DIR), {
8
9
  recursive: true,
9
10
  force: true
10
11
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/cache.ts"],"sourcesContent":["import { rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { STATE_DIR } from '../config/index.ts';\n\n// Deletes the cache directory and nothing else: the next open() reconciles anyway. Engine-neutral,\n// since every store keeps its files under the same STATE_DIR.\nexport function clearCache(cfg: ResolvedConfig): void {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n}\n"],"names":["rmSync","join","STATE_DIR","clearCache","cfg","baseDir","recursive","force"],"mappings":"AAAA,SAASA,MAAM,QAAQ,UAAU;AACjC,SAASC,IAAI,QAAQ,YAAY;AAEjC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,mGAAmG;AACnG,8DAA8D;AAC9D,OAAO,SAASC,WAAWC,GAAmB;IAC5CJ,OAAOC,KAAKG,IAAIC,OAAO,EAAEH,YAAY;QAAEI,WAAW;QAAMC,OAAO;IAAK;AACtE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/cache.ts"],"sourcesContent":["import { rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { STATE_DIR } from '../config/index.ts';\n\n// Deletes the cache directory and nothing else: the next open() reconciles anyway. Engine-neutral,\n// since every store keeps its files under the same STATE_DIR.\nexport function clearCache(cfg: ResolvedConfig): void {\n rmSync(join(cfg.configDir ?? cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n}\n"],"names":["rmSync","join","STATE_DIR","clearCache","cfg","configDir","baseDir","recursive","force"],"mappings":"AAAA,SAASA,MAAM,QAAQ,UAAU;AACjC,SAASC,IAAI,QAAQ,YAAY;AAEjC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,mGAAmG;AACnG,8DAA8D;AAC9D,OAAO,SAASC,WAAWC,GAAmB;QAChCA;IAAZJ,OAAOC,MAAKG,iBAAAA,IAAIC,SAAS,cAAbD,4BAAAA,iBAAiBA,IAAIE,OAAO,EAAEJ,YAAY;QAAEK,WAAW;QAAMC,OAAO;IAAK;AACvF"}
@@ -18,7 +18,8 @@ import { changedSignatureKeys, embedIdentityAdopted, signatureDiff } from './sig
18
18
  const LOCK_POLL_MS = 50;
19
19
  const lockWait = channel('sensemaking.store.lock-wait');
20
20
  async function connectUnlocked(dbPath, cfg, dialect) {
21
- const budgetMs = lockWaitBudgetMs(cfg.baseDir);
21
+ var _cfg_configDir;
22
+ const budgetMs = lockWaitBudgetMs((_cfg_configDir = cfg.configDir) !== null && _cfg_configDir !== void 0 ? _cfg_configDir : cfg.baseDir);
22
23
  const deadline = Date.now() + budgetMs;
23
24
  for(;;){
24
25
  try {
@@ -39,7 +40,8 @@ async function connectUnlocked(dbPath, cfg, dialect) {
39
40
  }
40
41
  }
41
42
  async function connectWithDialect(cfg, dialect) {
42
- const stateDir = join(cfg.baseDir, STATE_DIR);
43
+ var _cfg_configDir, _cfg_rootDir;
44
+ const stateDir = join((_cfg_configDir = cfg.configDir) !== null && _cfg_configDir !== void 0 ? _cfg_configDir : cfg.baseDir, STATE_DIR);
43
45
  mkdirSync(stateDir, {
44
46
  recursive: true
45
47
  });
@@ -50,7 +52,7 @@ async function connectWithDialect(cfg, dialect) {
50
52
  let closed = false;
51
53
  // Created here, not inside reconcile(): invalidate()/invalidateFeatures() below share its pool
52
54
  // with the build() call that follows them.
53
- const builder = createBuilder(conn, cfg, cfg.baseDir, dialect.reconcileDialect);
55
+ const builder = createBuilder(conn, cfg, (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir, dialect.reconcileDialect);
54
56
  try {
55
57
  await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
56
58
  // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an
@@ -66,6 +68,7 @@ async function connectWithDialect(cfg, dialect) {
66
68
  // Set only by the feature-toggle branch below; applied via builder.invalidateFeatures() after ensureSchema.
67
69
  let featureToggles;
68
70
  if (version !== null && version !== dialect.schemaVersion || features !== null && features !== wantFeatures) {
71
+ var _cfg_rootDir1;
69
72
  // Indexing derives from presets, so a config edit rebuilding the cache must say so and
70
73
  // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.
71
74
  if (version !== null && version !== dialect.schemaVersion) {
@@ -80,7 +83,7 @@ async function connectWithDialect(cfg, dialect) {
80
83
  // Null (rather than the empty set) when changedKeys isn't preset-only, or a changed
81
84
  // preset's stored segment can't be parsed back into old include/exclude/vectors -- either
82
85
  // way the branch below is skipped and the full rebuild stays the fallback.
83
- const presetForced = isPresetOnlyChange(changedKeys) ? forcedPresetPaths(cfg, cfg.baseDir, features !== null && features !== void 0 ? features : '', changedKeys) : null;
86
+ const presetForced = isPresetOnlyChange(changedKeys) ? forcedPresetPaths(cfg, (_cfg_rootDir1 = cfg.rootDir) !== null && _cfg_rootDir1 !== void 0 ? _cfg_rootDir1 : cfg.baseDir, features !== null && features !== void 0 ? features : '', changedKeys) : null;
84
87
  // Null unless exactly one segment changed and it decomposes into a recognised embed-only
85
88
  // case (embed-scope.ts): anything else, including embed toggled on/off, stays null.
86
89
  const embedKind = changedKeys.size === 1 && changedKeys.has('embed') ? classifyEmbedChange(features !== null && features !== void 0 ? features : '', wantFeatures) : null;
@@ -160,6 +163,13 @@ async function connectWithDialect(cfg, dialect) {
160
163
  // A store's open(): connects (see connectWithDialect above), then wraps the resulting connection
161
164
  // in the Store interface. The builder's pool closes right after the initial build.
162
165
  export async function openWithDialect(cfg, dialect) {
166
+ // `baseDir` was the original public open() input. Keep direct API callers working while
167
+ // making the two ownership paths explicit for every internal operation.
168
+ if (!cfg.rootDir) {
169
+ if (!cfg.baseDir) throw new SenseError('CONFIG_INVALID', 'open requires rootDir (or legacy baseDir)');
170
+ cfg.rootDir = cfg.baseDir;
171
+ }
172
+ if (!cfg.configDir) cfg.configDir = cfg.rootDir;
163
173
  const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings, stages } = await connectWithDialect(cfg, dialect);
164
174
  const store = dialect.createStore(handle, conn, resolvedCfg);
165
175
  // reconcile ran before this object existed, so its chunk text is keyed by the connection.
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/open.ts"],"sourcesContent":["import { channel } from 'node:diagnostics_channel';\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { featureSignature, STATE_DIR } from '../config/index.ts';\nimport { rekeyChunkText } from '../embed/handoff.ts';\nimport { SenseError } from '../errors.ts';\nimport { FEATURES } from '../features/index.ts';\nimport { createBuilder } from './builder.ts';\nimport { clearCache } from './cache.ts';\nimport type { EmbedChangeKind } from './embed-scope.ts';\nimport { classifyEmbedChange } from './embed-scope.ts';\nimport type { FeatureToggle } from './feature-scope.ts';\nimport { classifyFeatureToggles, isFeatureOnlyChange } from './feature-scope.ts';\nimport { lockWaitBudgetMs } from './lock-wait.ts';\nimport { forcedPresetPaths, isPresetOnlyChange } from './preset-scope.ts';\nimport { getMeta, setMeta } from './shared.ts';\nimport { changedSignatureKeys, embedIdentityAdopted, signatureDiff } from './signature.ts';\nimport type { Stages } from './stages.ts';\nimport type { Connection, OpenDialect, Store } from './types.ts';\n\n// One open algorithm shared by every store, parameterised by a per-engine OpenDialect (types.ts).\n// Ordering is universal, not a dialect concern: connect, meta table, schema-version and\n// feature-signature comparison (rebuild, adopt, or partial rebuild), any extra rebuild trigger,\n// ensureSchema, the derived busy_timeout PRAGMA, reconcile, then the store itself.\n\nexport interface OpenResult {\n store: Store;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n // Per-stage wall time for this open's build pass (stages.ts), not for any narrow embed or\n // feature invalidation that ran beside it -- on a cold build there is none, so it is the whole cost.\n stages: Stages;\n}\n\ninterface ConnectResult<Handle> {\n handle: Handle;\n conn: Connection;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n stages: Stages;\n}\n\n// duckdb/turso hold the cache file for their connection's whole life, so a second command waits.\n// Bounded by this tree's recorded reconcile time (lock-wait.ts), not a fixed guess.\nconst LOCK_POLL_MS = 50;\nconst lockWait = channel('sensemaking.store.lock-wait');\n\nasync function connectUnlocked<Handle>(dbPath: string, cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<{ handle: Handle; conn: Connection }> {\n const budgetMs = lockWaitBudgetMs(cfg.baseDir);\n const deadline = Date.now() + budgetMs;\n for (;;) {\n try {\n return await dialect.connect(dbPath, cfg);\n } catch (err) {\n if (!dialect.isLocked?.(err as Error)) throw err;\n // Observe a real rejected native connection, never merely the start of an async open.\n if (lockWait.hasSubscribers) lockWait.publish({ store: cfg.store, dbPath });\n if (Date.now() >= deadline) {\n throw new SenseError('STORE_BUSY', `another sense process is using this tree's ${cfg.store} cache (${dbPath}) and did not release it within ${Math.round(budgetMs / 1000)}s; wait for that command to finish, or set \"store\" to \"sqlite\" in sense.config.json, which serves concurrent commands`);\n }\n await new Promise((resolve) => setTimeout(resolve, LOCK_POLL_MS));\n }\n }\n}\n\nasync function connectWithDialect<Handle>(cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<ConnectResult<Handle>> {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, dialect.filename);\n\n const { handle, conn } = await connectUnlocked(dbPath, cfg, dialect);\n // A throw below must release this handle, or the leaked WAL makes the cache file undeletable on\n // Windows. `closed` keeps the catch from double-closing a handle a rebuild branch already closed.\n let closed = false;\n // Created here, not inside reconcile(): invalidate()/invalidateFeatures() below share its pool\n // with the build() call that follows them.\n const builder = createBuilder(conn, cfg, cfg.baseDir, dialect.reconcileDialect);\n try {\n await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = await getMeta(conn, 'schema_version');\n const features = await getMeta(conn, 'features');\n const wantFeatures = featureSignature(cfg, FEATURES);\n // Set only by the preset-membership branch below; reconcile treats these paths as touched\n // despite an unchanged stamp, for this one build() call.\n let forcedPaths: Set<string> | undefined;\n // Set only by the embed-narrow branch below; applied via builder.invalidate() after ensureSchema.\n let embedInvalidate: EmbedChangeKind | undefined;\n // Set only by the feature-toggle branch below; applied via builder.invalidateFeatures() after ensureSchema.\n let featureToggles: FeatureToggle[] | undefined;\n if ((version !== null && version !== dialect.schemaVersion) || (features !== null && features !== wantFeatures)) {\n // Indexing derives from presets, so a config edit rebuilding the cache must say so and\n // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.\n if (version !== null && version !== dialect.schemaVersion) {\n console.error('sense: cache format changed (new sensemaking version); rebuilding the index');\n // Close before clearCache deletes the files out from under the still-open handle.\n closed = true;\n await dialect.close(handle);\n clearCache(cfg);\n return connectWithDialect(cfg, dialect);\n }\n const changedKeys = changedSignatureKeys(features ?? '', wantFeatures);\n // Null (rather than the empty set) when changedKeys isn't preset-only, or a changed\n // preset's stored segment can't be parsed back into old include/exclude/vectors -- either\n // way the branch below is skipped and the full rebuild stays the fallback.\n const presetForced = isPresetOnlyChange(changedKeys) ? forcedPresetPaths(cfg, cfg.baseDir, features ?? '', changedKeys) : null;\n // Null unless exactly one segment changed and it decomposes into a recognised embed-only\n // case (embed-scope.ts): anything else, including embed toggled on/off, stays null.\n const embedKind = changedKeys.size === 1 && changedKeys.has('embed') ? classifyEmbedChange(features ?? '', wantFeatures) : null;\n // Null (rather than the empty set) when changedKeys isn't feature-toggle-only, or a changed\n // feature's stored segment can't be parsed back into old/new on-off -- either way the branch\n // below is skipped and the full rebuild stays the fallback.\n const toggles = isFeatureOnlyChange(changedKeys) ? classifyFeatureToggles(features ?? '', wantFeatures, changedKeys) : null;\n if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features ?? '', wantFeatures)) {\n // First sight of a resolved weight identity: the model itself hasn't changed, so\n // adopt it into meta with no rebuild and no re-embed.\n console.error(\"sense: recorded the embedding model's resolved identity; vectors are unaffected\");\n await setMeta(conn, 'features', wantFeatures);\n } else if (embedKind !== null) {\n // Chunk boundaries are file-derived, not model-derived (embed.ts's chunksOf), so neither\n // case needs any file reparsed for any feature but embed itself.\n embedInvalidate = embedKind;\n console.error(embedKind === 'model' ? 'sense: config change (embed settings) invalidates only the stale vectors' : 'sense: config change (embed settings) rebuilds only the embeddings it affects');\n await setMeta(conn, 'features', wantFeatures);\n } else if (presetForced !== null) {\n // Membership only: which files a preset covers moved, not a file's own content, a\n // global feature, or the embed model. reconcile() already knows how to\n // add, update, and remove a file correctly (every cross-feature cascade included); it\n // just needs telling which unchanged files to treat as touched (forcedPaths, above).\n forcedPaths = presetForced;\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) reparses only the files it affects`);\n await setMeta(conn, 'features', wantFeatures);\n } else if (toggles !== null) {\n // A feature turned off or on: reconcile.ts's activeFeatures skips a disabled feature's\n // hooks entirely, so its rows rot while off -- invalidateFeatures drops them and, for a\n // feature turning back on, fully re-derives across every indexed file rather than\n // trusting rows written before or during that window.\n featureToggles = toggles;\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) reparses only the feature it affects`);\n await setMeta(conn, 'features', wantFeatures);\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n closed = true;\n await dialect.close(handle);\n clearCache(cfg);\n return connectWithDialect(cfg, dialect);\n }\n }\n\n await dialect.ensureSchema(handle, conn, cfg);\n\n // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.\n // Installed before build() -- that call is the one that races a watcher's transaction.\n if (dialect.setDerivedBusyTimeout) {\n const recordedMaxMs = Number((await getMeta(conn, 'reconcile_max_ms')) ?? '0');\n await dialect.setDerivedBusyTimeout(handle, conn, Math.min(Math.max(30000, 3 * recordedMaxMs), 600_000));\n }\n\n // Runs before build(): forcedPaths, embedInvalidate and featureToggles never target the same\n // call (each comes from its own mutually exclusive branch above: a preset-only, embed-only, or\n // feature-toggle-only changed-key set).\n const embedParsed = embedInvalidate ? (await builder.invalidate(embedInvalidate)).parsed : 0;\n const featureParsed = featureToggles ? (await builder.invalidateFeatures(featureToggles)).parsed : 0;\n\n const { parsed, warnings, stages } = await builder.build(forcedPaths);\n // Every open reconciles once and is done with it -- a watcher opens fresh per event instead of holding this connection (src/watch.ts).\n await builder.close();\n\n return { handle, conn, cfg, dbPath, parsed: parsed + embedParsed + featureParsed, warnings, stages };\n } catch (err) {\n await builder.close();\n if (!closed) await dialect.close(handle);\n throw err;\n }\n}\n\n// A store's open(): connects (see connectWithDialect above), then wraps the resulting connection\n// in the Store interface. The builder's pool closes right after the initial build.\nexport async function openWithDialect<Handle>(cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<OpenResult> {\n const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings, stages } = await connectWithDialect(cfg, dialect);\n const store = dialect.createStore(handle, conn, resolvedCfg);\n // reconcile ran before this object existed, so its chunk text is keyed by the connection.\n rekeyChunkText(conn, store);\n return { store, cfg: resolvedCfg, dbPath, parsed, warnings, stages };\n}\n\nexport async function docCount(store: Store): Promise<number> {\n const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');\n return ((await stmt.get()) as { n: number }).n;\n}\n"],"names":["channel","mkdirSync","join","featureSignature","STATE_DIR","rekeyChunkText","SenseError","FEATURES","createBuilder","clearCache","classifyEmbedChange","classifyFeatureToggles","isFeatureOnlyChange","lockWaitBudgetMs","forcedPresetPaths","isPresetOnlyChange","getMeta","setMeta","changedSignatureKeys","embedIdentityAdopted","signatureDiff","LOCK_POLL_MS","lockWait","connectUnlocked","dbPath","cfg","dialect","budgetMs","baseDir","deadline","Date","now","connect","err","isLocked","hasSubscribers","publish","store","Math","round","Promise","resolve","setTimeout","connectWithDialect","stateDir","recursive","filename","handle","conn","closed","builder","reconcileDialect","exec","version","features","wantFeatures","forcedPaths","embedInvalidate","featureToggles","schemaVersion","console","error","close","changedKeys","presetForced","embedKind","size","has","toggles","changed","ensureSchema","setDerivedBusyTimeout","recordedMaxMs","Number","min","max","embedParsed","invalidate","parsed","featureParsed","invalidateFeatures","warnings","stages","build","openWithDialect","resolvedCfg","createStore","docCount","stmt","prepare","get","n"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/open.ts"],"sourcesContent":["import { channel } from 'node:diagnostics_channel';\nimport { mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { featureSignature, STATE_DIR } from '../config/index.ts';\nimport { rekeyChunkText } from '../embed/handoff.ts';\nimport { SenseError } from '../errors.ts';\nimport { FEATURES } from '../features/index.ts';\nimport { createBuilder } from './builder.ts';\nimport { clearCache } from './cache.ts';\nimport type { EmbedChangeKind } from './embed-scope.ts';\nimport { classifyEmbedChange } from './embed-scope.ts';\nimport type { FeatureToggle } from './feature-scope.ts';\nimport { classifyFeatureToggles, isFeatureOnlyChange } from './feature-scope.ts';\nimport { lockWaitBudgetMs } from './lock-wait.ts';\nimport { forcedPresetPaths, isPresetOnlyChange } from './preset-scope.ts';\nimport { getMeta, setMeta } from './shared.ts';\nimport { changedSignatureKeys, embedIdentityAdopted, signatureDiff } from './signature.ts';\nimport type { Stages } from './stages.ts';\nimport type { Connection, OpenDialect, Store } from './types.ts';\n\n// One open algorithm shared by every store, parameterised by a per-engine OpenDialect (types.ts).\n// Ordering is universal, not a dialect concern: connect, meta table, schema-version and\n// feature-signature comparison (rebuild, adopt, or partial rebuild), any extra rebuild trigger,\n// ensureSchema, the derived busy_timeout PRAGMA, reconcile, then the store itself.\n\nexport interface OpenResult {\n store: Store;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n // Per-stage wall time for this open's build pass (stages.ts), not for any narrow embed or\n // feature invalidation that ran beside it -- on a cold build there is none, so it is the whole cost.\n stages: Stages;\n}\n\ninterface ConnectResult<Handle> {\n handle: Handle;\n conn: Connection;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n stages: Stages;\n}\n\n// duckdb/turso hold the cache file for their connection's whole life, so a second command waits.\n// Bounded by this tree's recorded reconcile time (lock-wait.ts), not a fixed guess.\nconst LOCK_POLL_MS = 50;\nconst lockWait = channel('sensemaking.store.lock-wait');\n\nasync function connectUnlocked<Handle>(dbPath: string, cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<{ handle: Handle; conn: Connection }> {\n const budgetMs = lockWaitBudgetMs(cfg.configDir ?? cfg.baseDir);\n const deadline = Date.now() + budgetMs;\n for (;;) {\n try {\n return await dialect.connect(dbPath, cfg);\n } catch (err) {\n if (!dialect.isLocked?.(err as Error)) throw err;\n // Observe a real rejected native connection, never merely the start of an async open.\n if (lockWait.hasSubscribers) lockWait.publish({ store: cfg.store, dbPath });\n if (Date.now() >= deadline) {\n throw new SenseError('STORE_BUSY', `another sense process is using this tree's ${cfg.store} cache (${dbPath}) and did not release it within ${Math.round(budgetMs / 1000)}s; wait for that command to finish, or set \"store\" to \"sqlite\" in sense.config.json, which serves concurrent commands`);\n }\n await new Promise((resolve) => setTimeout(resolve, LOCK_POLL_MS));\n }\n }\n}\n\nasync function connectWithDialect<Handle>(cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<ConnectResult<Handle>> {\n const stateDir = join(cfg.configDir ?? cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, dialect.filename);\n\n const { handle, conn } = await connectUnlocked(dbPath, cfg, dialect);\n // A throw below must release this handle, or the leaked WAL makes the cache file undeletable on\n // Windows. `closed` keeps the catch from double-closing a handle a rebuild branch already closed.\n let closed = false;\n // Created here, not inside reconcile(): invalidate()/invalidateFeatures() below share its pool\n // with the build() call that follows them.\n const builder = createBuilder(conn, cfg, cfg.rootDir ?? cfg.baseDir, dialect.reconcileDialect);\n try {\n await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n\n // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an\n // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).\n const version = await getMeta(conn, 'schema_version');\n const features = await getMeta(conn, 'features');\n const wantFeatures = featureSignature(cfg, FEATURES);\n // Set only by the preset-membership branch below; reconcile treats these paths as touched\n // despite an unchanged stamp, for this one build() call.\n let forcedPaths: Set<string> | undefined;\n // Set only by the embed-narrow branch below; applied via builder.invalidate() after ensureSchema.\n let embedInvalidate: EmbedChangeKind | undefined;\n // Set only by the feature-toggle branch below; applied via builder.invalidateFeatures() after ensureSchema.\n let featureToggles: FeatureToggle[] | undefined;\n if ((version !== null && version !== dialect.schemaVersion) || (features !== null && features !== wantFeatures)) {\n // Indexing derives from presets, so a config edit rebuilding the cache must say so and\n // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.\n if (version !== null && version !== dialect.schemaVersion) {\n console.error('sense: cache format changed (new sensemaking version); rebuilding the index');\n // Close before clearCache deletes the files out from under the still-open handle.\n closed = true;\n await dialect.close(handle);\n clearCache(cfg);\n return connectWithDialect(cfg, dialect);\n }\n const changedKeys = changedSignatureKeys(features ?? '', wantFeatures);\n // Null (rather than the empty set) when changedKeys isn't preset-only, or a changed\n // preset's stored segment can't be parsed back into old include/exclude/vectors -- either\n // way the branch below is skipped and the full rebuild stays the fallback.\n const presetForced = isPresetOnlyChange(changedKeys) ? forcedPresetPaths(cfg, cfg.rootDir ?? cfg.baseDir, features ?? '', changedKeys) : null;\n // Null unless exactly one segment changed and it decomposes into a recognised embed-only\n // case (embed-scope.ts): anything else, including embed toggled on/off, stays null.\n const embedKind = changedKeys.size === 1 && changedKeys.has('embed') ? classifyEmbedChange(features ?? '', wantFeatures) : null;\n // Null (rather than the empty set) when changedKeys isn't feature-toggle-only, or a changed\n // feature's stored segment can't be parsed back into old/new on-off -- either way the branch\n // below is skipped and the full rebuild stays the fallback.\n const toggles = isFeatureOnlyChange(changedKeys) ? classifyFeatureToggles(features ?? '', wantFeatures, changedKeys) : null;\n if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features ?? '', wantFeatures)) {\n // First sight of a resolved weight identity: the model itself hasn't changed, so\n // adopt it into meta with no rebuild and no re-embed.\n console.error(\"sense: recorded the embedding model's resolved identity; vectors are unaffected\");\n await setMeta(conn, 'features', wantFeatures);\n } else if (embedKind !== null) {\n // Chunk boundaries are file-derived, not model-derived (embed.ts's chunksOf), so neither\n // case needs any file reparsed for any feature but embed itself.\n embedInvalidate = embedKind;\n console.error(embedKind === 'model' ? 'sense: config change (embed settings) invalidates only the stale vectors' : 'sense: config change (embed settings) rebuilds only the embeddings it affects');\n await setMeta(conn, 'features', wantFeatures);\n } else if (presetForced !== null) {\n // Membership only: which files a preset covers moved, not a file's own content, a\n // global feature, or the embed model. reconcile() already knows how to\n // add, update, and remove a file correctly (every cross-feature cascade included); it\n // just needs telling which unchanged files to treat as touched (forcedPaths, above).\n forcedPaths = presetForced;\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) reparses only the files it affects`);\n await setMeta(conn, 'features', wantFeatures);\n } else if (toggles !== null) {\n // A feature turned off or on: reconcile.ts's activeFeatures skips a disabled feature's\n // hooks entirely, so its rows rot while off -- invalidateFeatures drops them and, for a\n // feature turning back on, fully re-derives across every indexed file rather than\n // trusting rows written before or during that window.\n featureToggles = toggles;\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) reparses only the feature it affects`);\n await setMeta(conn, 'features', wantFeatures);\n } else {\n const changed = signatureDiff(features ?? '', wantFeatures);\n console.error(`sense: config change (${changed}) rebuilds the index`);\n closed = true;\n await dialect.close(handle);\n clearCache(cfg);\n return connectWithDialect(cfg, dialect);\n }\n }\n\n await dialect.ensureSchema(handle, conn, cfg);\n\n // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.\n // Installed before build() -- that call is the one that races a watcher's transaction.\n if (dialect.setDerivedBusyTimeout) {\n const recordedMaxMs = Number((await getMeta(conn, 'reconcile_max_ms')) ?? '0');\n await dialect.setDerivedBusyTimeout(handle, conn, Math.min(Math.max(30000, 3 * recordedMaxMs), 600_000));\n }\n\n // Runs before build(): forcedPaths, embedInvalidate and featureToggles never target the same\n // call (each comes from its own mutually exclusive branch above: a preset-only, embed-only, or\n // feature-toggle-only changed-key set).\n const embedParsed = embedInvalidate ? (await builder.invalidate(embedInvalidate)).parsed : 0;\n const featureParsed = featureToggles ? (await builder.invalidateFeatures(featureToggles)).parsed : 0;\n\n const { parsed, warnings, stages } = await builder.build(forcedPaths);\n // Every open reconciles once and is done with it -- a watcher opens fresh per event instead of holding this connection (src/watch.ts).\n await builder.close();\n\n return { handle, conn, cfg, dbPath, parsed: parsed + embedParsed + featureParsed, warnings, stages };\n } catch (err) {\n await builder.close();\n if (!closed) await dialect.close(handle);\n throw err;\n }\n}\n\n// A store's open(): connects (see connectWithDialect above), then wraps the resulting connection\n// in the Store interface. The builder's pool closes right after the initial build.\nexport async function openWithDialect<Handle>(cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<OpenResult> {\n // `baseDir` was the original public open() input. Keep direct API callers working while\n // making the two ownership paths explicit for every internal operation.\n if (!cfg.rootDir) {\n if (!cfg.baseDir) throw new SenseError('CONFIG_INVALID', 'open requires rootDir (or legacy baseDir)');\n cfg.rootDir = cfg.baseDir;\n }\n if (!cfg.configDir) cfg.configDir = cfg.rootDir;\n const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings, stages } = await connectWithDialect(cfg, dialect);\n const store = dialect.createStore(handle, conn, resolvedCfg);\n // reconcile ran before this object existed, so its chunk text is keyed by the connection.\n rekeyChunkText(conn, store);\n return { store, cfg: resolvedCfg, dbPath, parsed, warnings, stages };\n}\n\nexport async function docCount(store: Store): Promise<number> {\n const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');\n return ((await stmt.get()) as { n: number }).n;\n}\n"],"names":["channel","mkdirSync","join","featureSignature","STATE_DIR","rekeyChunkText","SenseError","FEATURES","createBuilder","clearCache","classifyEmbedChange","classifyFeatureToggles","isFeatureOnlyChange","lockWaitBudgetMs","forcedPresetPaths","isPresetOnlyChange","getMeta","setMeta","changedSignatureKeys","embedIdentityAdopted","signatureDiff","LOCK_POLL_MS","lockWait","connectUnlocked","dbPath","cfg","dialect","budgetMs","configDir","baseDir","deadline","Date","now","connect","err","isLocked","hasSubscribers","publish","store","Math","round","Promise","resolve","setTimeout","connectWithDialect","stateDir","recursive","filename","handle","conn","closed","builder","rootDir","reconcileDialect","exec","version","features","wantFeatures","forcedPaths","embedInvalidate","featureToggles","schemaVersion","console","error","close","changedKeys","presetForced","embedKind","size","has","toggles","changed","ensureSchema","setDerivedBusyTimeout","recordedMaxMs","Number","min","max","embedParsed","invalidate","parsed","featureParsed","invalidateFeatures","warnings","stages","build","openWithDialect","resolvedCfg","createStore","docCount","stmt","prepare","get","n"],"mappings":"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"}
@@ -1,5 +1,10 @@
1
- import type { ReconcileDialect } from '../types.js';
1
+ import type { ReconcileDelta } from '../../features/types.js';
2
+ import type { ParsedDoc } from '../../scan/index.js';
3
+ import type { Connection, ReconcileDialect } from '../types.js';
2
4
  export declare const CONTENT_FTS_DDL: readonly ["CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')", "CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')"];
3
5
  export declare const CONTENT_FTS_NAMES: readonly ["content_fts", "content_fts_ngram"];
4
6
  export declare const FTS_REBUILD_THRESHOLD = 250;
7
+ export type TursoFtsStrategy = 'incremental' | 'rebuild';
8
+ export declare function tursoFtsStrategy(delta: ReconcileDelta): TursoFtsStrategy;
9
+ export declare function reconcileTursoContentWithStrategy(conn: Connection, touched: string[], docs: ParsedDoc[], strategy: TursoFtsStrategy): Promise<void>;
5
10
  export declare const tursoDialect: ReconcileDialect;
@@ -45,11 +45,16 @@ function contentRow(doc) {
45
45
  ngramSidecar(text)
46
46
  ];
47
47
  }
48
- async function reconcileContent(conn, touched, docs, delta) {
48
+ export function tursoFtsStrategy(delta) {
49
49
  const churn = delta.reparsed.length + delta.vanished.length;
50
+ return delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD ? 'rebuild' : 'incremental';
51
+ }
52
+ // Kept transaction-free because the shared reconcile owns the one write transaction. The explicit
53
+ // strategy is internal composition for real-engine diagnostics; production selects it from delta.
54
+ export async function reconcileTursoContentWithStrategy(conn, touched, docs, strategy) {
50
55
  // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is
51
56
  // superlinear and a rebuild wins past the threshold.
52
- const bulk = delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD;
57
+ const bulk = strategy === 'rebuild';
53
58
  if (bulk) for (const name of CONTENT_FTS_NAMES)await conn.exec(`DROP INDEX IF EXISTS ${name}`);
54
59
  // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5
55
60
  // content), so vanished and reparsed docs delete in one pass.
@@ -59,6 +64,9 @@ async function reconcileContent(conn, touched, docs, delta) {
59
64
  if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));
60
65
  if (bulk) for (const ddl of CONTENT_FTS_DDL)await conn.exec(ddl);
61
66
  }
67
+ async function reconcileTursoContent(conn, touched, docs, delta, _cfg) {
68
+ await reconcileTursoContentWithStrategy(conn, touched, docs, tursoFtsStrategy(delta));
69
+ }
62
70
  // turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.
63
71
  async function addColumns(conn, names) {
64
72
  for (const name of names)await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);
@@ -71,6 +79,6 @@ export const tursoDialect = {
71
79
  }
72
80
  },
73
81
  addColumns,
74
- reconcileContent,
82
+ reconcileContent: reconcileTursoContent,
75
83
  recordDuration: recordReconcileDuration
76
84
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/turso/reconcile.ts"],"sourcesContent":["import { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { hasUnspacedRun } from '../../text/segment.ts';\nimport { quoteIdent, recordReconcileDuration } from '../shared.ts';\nimport { BEGIN_WRITE } from '../transaction.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { stemFolded } from './lexical-text.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. content is a plain table with \"_ngram\" sidecars for lexical.ts.\n\n// The ngram index is scoped to disjoint \"_ngram\" sidecar columns: a second index over the same\n// columns makes a bare substring match a whole word, defeating the prefix-query rejection.\n// The two FTS indexes, named here (not open.ts) because reconcile drops and rebuilds them around\n// a bulk load: Tantivy maintains them per inserted row, which is quadratic in what is already indexed.\nexport const CONTENT_FTS_DDL = [\n `CREATE INDEX IF NOT EXISTS content_fts ON content USING fts (title_stem, summary_stem, text_stem) WITH (weights = 'title_stem=10.0,summary_stem=5.0,text_stem=1.0')`,\n `CREATE INDEX IF NOT EXISTS content_fts_ngram ON content USING fts (title_ngram, summary_ngram, text_ngram) WITH (tokenizer='ngram', weights='title_ngram=10.0,summary_ngram=5.0,text_ngram=1.0')`,\n] as const;\nexport const CONTENT_FTS_NAMES = ['content_fts', 'content_fts_ngram'] as const;\n\n// '' when the field has no unspaced-script run (the common case), so the ngram index carries\n// nothing for it -- same \"pay for nothing when absent\" shape as sqlite's segmentField sidecars.\nfunction ngramSidecar(text: string): string {\n return hasUnspacedRun(text) ? text : '';\n}\n\n// Not a compile-time cap on ALTER TABLE ADD COLUMN (spike-measured: turso accepts 10,000 with\n// no error); the real fence is a SELECT projecting more than this many result columns, which fails to prepare.\nconst MAX_FRONTMATTER_COLUMNS = 2000;\n\n// Changed files above which rebuilding the FTS index beats maintaining it per row. Measured\n// 2026-08-30; the derivation and its bounds are pinned in this file's spec.\nexport const FTS_REBUILD_THRESHOLD = 250;\n\n// No rowid coupling (unlike sqlite's FTS5 content): `path` is content's own primary key.\nconst INSERT_CONTENT_SQL = `INSERT INTO content (\"path\", title, summary, text, title_stem, summary_stem, text_stem, title_ngram, summary_ngram, text_ngram) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n const { title, summary, text } = doc.search;\n return [doc.relPath, title, summary, text, stemFolded(title), stemFolded(summary), stemFolded(text), ngramSidecar(title), ngramSidecar(summary), ngramSidecar(text)];\n}\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta): Promise<void> {\n const churn = delta.reparsed.length + delta.vanished.length;\n\n // Tantivy indexes per inserted row at a cost that grows with the batch, so a large insert is\n // superlinear and a rebuild wins past the threshold.\n const bulk = delta.files.length === 0 || churn > FTS_REBUILD_THRESHOLD;\n if (bulk) for (const name of CONTENT_FTS_NAMES) await conn.exec(`DROP INDEX IF EXISTS ${name}`);\n\n // content is a plain table keyed by its own path (no rowid subquery, unlike sqlite's FTS5\n // content), so vanished and reparsed docs delete in one pass.\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n if (docs.length > 0) await conn.runBatch(INSERT_CONTENT_SQL, docs.map(contentRow));\n if (bulk) for (const ddl of CONTENT_FTS_DDL) await conn.exec(ddl);\n}\n\n// turso's ADD COLUMN is metadata-only, so a loop costs nothing extra over one statement.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n for (const name of names) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)}`);\n}\n\nexport const tursoDialect: ReconcileDialect = {\n beginMode: () => BEGIN_WRITE,\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError(\n 'COLUMN_LIMIT',\n `frontmatter would need ${count} columns, crossing turso's SELECT result-set column limit (${MAX_FRONTMATTER_COLUMNS}; ALTER TABLE ADD COLUMN itself accepts far more, but a query projecting past this many columns fails to prepare). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`\n );\n }\n },\n addColumns,\n reconcileContent,\n recordDuration: recordReconcileDuration,\n};\n"],"names":["SenseError","hasUnspacedRun","quoteIdent","recordReconcileDuration","BEGIN_WRITE","stemFolded","CONTENT_FTS_DDL","CONTENT_FTS_NAMES","ngramSidecar","text","MAX_FRONTMATTER_COLUMNS","FTS_REBUILD_THRESHOLD","INSERT_CONTENT_SQL","contentRow","doc","title","summary","search","relPath","reconcileContent","conn","touched","docs","delta","churn","reparsed","length","vanished","bulk","files","name","exec","runBatch","map","p","ddl","addColumns","names","tursoDialect","beginMode","checkColumnLimit","count","recordDuration"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,cAAc,QAAQ,wBAAwB;AACvD,SAASC,UAAU,EAAEC,uBAAuB,QAAQ,eAAe;AACnE,SAASC,WAAW,QAAQ,oBAAoB;AAEhD,SAASC,UAAU,QAAQ,oBAAoB;AAE/C,qFAAqF;AACrF,sFAAsF;AAEtF,+FAA+F;AAC/F,2FAA2F;AAC3F,iGAAiG;AACjG,uGAAuG;AACvG,OAAO,MAAMC,kBAAkB;IAC7B,CAAC,mKAAmK,CAAC;IACrK,CAAC,gMAAgM,CAAC;CACnM,CAAU;AACX,OAAO,MAAMC,oBAAoB;IAAC;IAAe;CAAoB,CAAU;AAE/E,6FAA6F;AAC7F,gGAAgG;AAChG,SAASC,aAAaC,IAAY;IAChC,OAAOR,eAAeQ,QAAQA,OAAO;AACvC;AAEA,8FAA8F;AAC9F,+GAA+G;AAC/G,MAAMC,0BAA0B;AAEhC,4FAA4F;AAC5F,4EAA4E;AAC5E,OAAO,MAAMC,wBAAwB,IAAI;AAEzC,yFAAyF;AACzF,MAAMC,qBAAqB,CAAC,qKAAqK,CAAC;AAElM,SAASC,WAAWC,GAAc;IAChC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEP,IAAI,EAAE,GAAGK,IAAIG,MAAM;IAC3C,OAAO;QAACH,IAAII,OAAO;QAAEH;QAAOC;QAASP;QAAMJ,WAAWU;QAAQV,WAAWW;QAAUX,WAAWI;QAAOD,aAAaO;QAAQP,aAAaQ;QAAUR,aAAaC;KAAM;AACtK;AAEA,eAAeU,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,KAAqB;IAC3G,MAAMC,QAAQD,MAAME,QAAQ,CAACC,MAAM,GAAGH,MAAMI,QAAQ,CAACD,MAAM;IAE3D,6FAA6F;IAC7F,qDAAqD;IACrD,MAAME,OAAOL,MAAMM,KAAK,CAACH,MAAM,KAAK,KAAKF,QAAQb;IACjD,IAAIiB,MAAM,KAAK,MAAME,QAAQvB,kBAAmB,MAAMa,KAAKW,IAAI,CAAC,CAAC,qBAAqB,EAAED,MAAM;IAE9F,0FAA0F;IAC1F,8DAA8D;IAC9D,IAAIT,QAAQK,MAAM,GAAG,GACnB,MAAMN,KAAKY,QAAQ,CACjB,wCACAX,QAAQY,GAAG,CAAC,CAACC,IAAM;YAACA;SAAE;IAE1B,IAAIZ,KAAKI,MAAM,GAAG,GAAG,MAAMN,KAAKY,QAAQ,CAACpB,oBAAoBU,KAAKW,GAAG,CAACpB;IACtE,IAAIe,MAAM,KAAK,MAAMO,OAAO7B,gBAAiB,MAAMc,KAAKW,IAAI,CAACI;AAC/D;AAEA,yFAAyF;AACzF,eAAeC,WAAWhB,IAAgB,EAAEiB,KAAe;IACzD,KAAK,MAAMP,QAAQO,MAAO,MAAMjB,KAAKW,IAAI,CAAC,CAAC,mCAAmC,EAAE7B,WAAW4B,OAAO;AACpG;AAEA,OAAO,MAAMQ,eAAiC;IAC5CC,WAAW,IAAMnC;IACjBoC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQ/B,yBAAyB;YACnC,MAAM,IAAIV,WACR,gBACA,CAAC,uBAAuB,EAAEyC,MAAM,2DAA2D,EAAE/B,wBAAwB,gPAAgP,CAAC;QAE1W;IACF;IACA0B;IACAjB;IACAuB,gBAAgBvC;AAClB,EAAE"}
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,7 +1,7 @@
1
1
  import type { ResolvedConfig } from './config/index.js';
2
2
  export type WatchEvent = {
3
3
  type: 'started';
4
- baseDir: string;
4
+ rootDir: string;
5
5
  dbPath: string;
6
6
  } | {
7
7
  type: 'reconciled';
package/dist/esm/watch.js CHANGED
@@ -10,12 +10,13 @@ const STALE_HEARTBEAT_MS = 15000;
10
10
  // Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to
11
11
  // one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.
12
12
  export async function runWatch(cfg, opts = {}) {
13
- var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs;
13
+ var _opts_onEvent, _opts_debounceMs, _opts_heartbeatIntervalMs, _cfg_rootDir;
14
14
  const onEvent = (_opts_onEvent = opts.onEvent) !== null && _opts_onEvent !== void 0 ? _opts_onEvent : ()=>{};
15
15
  const debounceMs = (_opts_debounceMs = opts.debounceMs) !== null && _opts_debounceMs !== void 0 ? _opts_debounceMs : DEBOUNCE_MS;
16
16
  const heartbeatIntervalMs = (_opts_heartbeatIntervalMs = opts.heartbeatIntervalMs) !== null && _opts_heartbeatIntervalMs !== void 0 ? _opts_heartbeatIntervalMs : HEARTBEAT_INTERVAL_MS;
17
- const baseDir = cfg.baseDir;
18
17
  const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);
18
+ // openStore normalizes the legacy direct-API baseDir before a watcher touches the filesystem.
19
+ const rootDir = (_cfg_rootDir = cfg.rootDir) !== null && _cfg_rootDir !== void 0 ? _cfg_rootDir : cfg.baseDir;
19
20
  const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');
20
21
  if (existingHeartbeat && !opts.force) {
21
22
  const age = Date.now() - Date.parse(existingHeartbeat);
@@ -31,7 +32,7 @@ export async function runWatch(cfg, opts = {}) {
31
32
  await initialStore.close();
32
33
  onEvent({
33
34
  type: 'started',
34
- baseDir,
35
+ rootDir,
35
36
  dbPath
36
37
  });
37
38
  if (initialWarnings.length > 0 || initialParsed > 0) {
@@ -99,7 +100,7 @@ export async function runWatch(cfg, opts = {}) {
99
100
  };
100
101
  // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with
101
102
  // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.
102
- const watcher = fsWatch(baseDir, {
103
+ const watcher = fsWatch(rootDir, {
103
104
  recursive: true
104
105
  }, (_event, filename)=>{
105
106
  if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, getMeta, openStore, setMeta } from './store/index.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\nexport type WatchEvent = { type: 'started'; baseDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to\n// one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;\n const baseDir = cfg.baseDir;\n\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n await initialStore.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n // total is read before close: an event fires only once nothing is held open again.\n const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());\n await setMeta(initialStore, 'watch_pid', String(process.pid));\n await initialStore.close();\n\n onEvent({ type: 'started', baseDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n await setMeta(store, 'watch_heartbeat', new Date().toISOString());\n await setMeta(store, 'watch_pid', String(process.pid));\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with\n // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(baseDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n heartbeatIntervalMs\n );\n\n return new Promise<void>((resolveShutdown) => {\n // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is\n // unregistered here too so a second runWatch call in the same process starts clean.\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n opts.signal?.removeEventListener('abort', shutdown);\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n const { store: finalStore } = await openStore(cfg);\n await setMeta(finalStore, 'watch_heartbeat', null);\n await setMeta(finalStore, 'watch_pid', null);\n await finalStore.close();\n resolveShutdown();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n if (opts.signal?.aborted) shutdown();\n else opts.signal?.addEventListener('abort', shutdown, { once: true });\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","getMeta","openStore","setMeta","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","debounceMs","heartbeatIntervalMs","baseDir","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","existingHeartbeat","force","age","Date","now","parse","close","Math","round","initialTotal","length","toISOString","String","process","pid","type","total","stopping","running","queuedAlways","tick","alwaysEmit","err","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","shutdown","off","signal","removeEventListener","clearInterval","finalStore","once","aborted","addEventListener"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config/index.ts';\nimport { STATE_DIR } from './config/index.ts';\nimport { SenseError } from './errors.ts';\nimport { guardedTick } from './lib/guarded-tick.ts';\nimport { docCount, getMeta, openStore, setMeta } from './store/index.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\nexport type WatchEvent = { type: 'started'; rootDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n // Aborting runs the same shutdown path as SIGINT/SIGTERM.\n signal?: AbortSignal;\n debounceMs?: number;\n heartbeatIntervalMs?: number;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM/signal abort. duckdb/turso hand the cache file to\n// one process at a time, so every cycle opens, reconciles, writes the heartbeat/pid rows, and closes.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;\n const { store: initialStore, dbPath, warnings: initialWarnings, parsed: initialParsed } = await openStore(cfg);\n // openStore normalizes the legacy direct-API baseDir before a watcher touches the filesystem.\n const rootDir = cfg.rootDir ?? cfg.baseDir;\n const existingHeartbeat = await getMeta(initialStore, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n await initialStore.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n // total is read before close: an event fires only once nothing is held open again.\n const initialTotal = initialWarnings.length > 0 || initialParsed > 0 ? await docCount(initialStore) : 0;\n await setMeta(initialStore, 'watch_heartbeat', new Date().toISOString());\n await setMeta(initialStore, 'watch_pid', String(process.pid));\n await initialStore.close();\n\n onEvent({ type: 'started', rootDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: initialTotal, warnings: initialWarnings });\n }\n\n let stopping = false;\n // At most one open+reconcile+close cycle runs at a time; a trigger that arrives mid-cycle is\n // coalesced into a single rerun instead of opening a second, overlapping connection.\n let running: Promise<void> | null = null;\n let queuedAlways: boolean | null = null;\n\n const tick = async (alwaysEmit: boolean): Promise<void> => {\n try {\n const { store, parsed, warnings } = await openStore(cfg);\n try {\n await setMeta(store, 'watch_heartbeat', new Date().toISOString());\n await setMeta(store, 'watch_pid', String(process.pid));\n if (alwaysEmit || parsed > 0 || warnings.length > 0) {\n onEvent({ type: 'reconciled', parsed, total: await docCount(store), warnings });\n }\n } finally {\n await store.close();\n }\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n };\n\n const startCycle = (alwaysEmit: boolean) => {\n running = tick(alwaysEmit).then(() => {\n running = null;\n if (queuedAlways !== null && !stopping) {\n const again = queuedAlways;\n queuedAlways = null;\n startCycle(again);\n }\n });\n };\n\n const requestCycle = (alwaysEmit: boolean) => {\n if (stopping) return;\n if (running) {\n queuedAlways = queuedAlways || alwaysEmit;\n return;\n }\n startCycle(alwaysEmit);\n };\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n requestCycle(true);\n }, debounceMs);\n };\n\n // Ignore our own state dir, or the heartbeat write would retrigger itself forever. An event with\n // an unresolvable filename (null, which fs.watch delivers under load) reconciles: parsing nothing costs less than missing a real edit.\n const watcher = fsWatch(rootDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(\n guardedTick(\n async () => requestCycle(false),\n () => stopping\n ),\n heartbeatIntervalMs\n );\n\n return new Promise<void>((resolveShutdown) => {\n // SIGINT/SIGTERM and an aborted signal all run this same path exactly once; each is\n // unregistered here too so a second runWatch call in the same process starts clean.\n const shutdown = async () => {\n if (stopping) return;\n stopping = true;\n process.off('SIGINT', shutdown);\n process.off('SIGTERM', shutdown);\n opts.signal?.removeEventListener('abort', shutdown);\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n await running;\n const { store: finalStore } = await openStore(cfg);\n await setMeta(finalStore, 'watch_heartbeat', null);\n await setMeta(finalStore, 'watch_pid', null);\n await finalStore.close();\n resolveShutdown();\n };\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n if (opts.signal?.aborted) shutdown();\n else opts.signal?.addEventListener('abort', shutdown, { once: true });\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","SenseError","guardedTick","docCount","getMeta","openStore","setMeta","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","debounceMs","heartbeatIntervalMs","store","initialStore","dbPath","warnings","initialWarnings","parsed","initialParsed","rootDir","baseDir","existingHeartbeat","force","age","Date","now","parse","close","Math","round","initialTotal","length","toISOString","String","process","pid","type","total","stopping","running","queuedAlways","tick","alwaysEmit","err","message","startCycle","then","again","requestCycle","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","shutdown","off","signal","removeEventListener","clearInterval","finalStore","once","aborted","addEventListener"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sensemaking",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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",
package/schema.json CHANGED
@@ -15,6 +15,11 @@
15
15
  "enum": [1, 2, 3, 4, 5],
16
16
  "description": "Config format version. Older versions are auto-migrated (and the file rewritten) on load; a version newer than this sense build supports makes it exit with an error rather than misinterpret the file. Omit to default to 1."
17
17
  },
18
+ "root": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "description": "Optional markdown tree root. Relative paths resolve against the directory containing sense.config.json; omitted means that directory, preserving colocated-config behavior. Preset globs and stored paths are relative to this root, while .sense state remains beside the config."
22
+ },
18
23
  "presets": {
19
24
  "type": "object",
20
25
  "description": "File selection, index-time, and named search defaults. A preset is self-contained: no inheritance between presets, and no ordering -- presets may overlap freely (they are views, not partitions), and a preset whose globs match nothing is valid. A file is indexed iff any preset's include/exclude covers it (union across presets); `default` is used when a command names no preset, and must be declared. A file is embedded when the top-level `embed` block names a model and some covering preset's `signals` include `vectors`. `sense init` generates `default` plus one `large` example.",
@@ -28,7 +33,7 @@
28
33
  "type": "array",
29
34
  "items": { "type": "string" },
30
35
  "minItems": 1,
31
- "description": "Glob patterns (globby syntax), resolved relative to this config file's own directory -- never the invocation cwd. Required for every preset, including `default`."
36
+ "description": "Glob patterns (globby syntax), resolved relative to the configured root (or the config file's directory when root is omitted) -- never the invocation cwd. Required for every preset, including `default`."
32
37
  },
33
38
  "exclude": {
34
39
  "type": "array",
@@ -106,3 +106,24 @@ sense peek "Plugins/dataview.md" # outline + links before reading
106
106
  ```
107
107
 
108
108
  Presets earn their place only when a tree has parts deserving different treatment; a tree that is one kind of thing needs none of the vocabulary above. On a big vault, raise `default`'s `k` and read `lines` ranges instead of whole files, or start from the starter's `large` preset.
109
+
110
+ ## E. One vault, separate consumers
111
+
112
+ Keep each consumer's configuration and cache under its own configuration directory while indexing one shared vault. `root` is relative to this config file, so saved SQL continues to use stable vault-relative paths.
113
+
114
+ `~/.config/obsidian/tasks/sense.config.json`:
115
+
116
+ ```json
117
+ {
118
+ "version": 5,
119
+ "root": "../../../.obsidian",
120
+ "presets": {
121
+ "default": { "include": ["vault/tasks/**/*.md"], "k": 20 }
122
+ },
123
+ "queries": {
124
+ "active": { "sql": "SELECT path, title FROM frontmatter WHERE path LIKE 'vault/tasks/%' AND status = 'active' ORDER BY path" }
125
+ }
126
+ }
127
+ ```
128
+
129
+ The database is `~/.config/obsidian/tasks/.sense/cache.db`; the indexed row remains `vault/tasks/...`, never a path back out through the configuration directory. A `general/` sibling can point at the same root with different presets and a separate `.sense/` cache.
@@ -9,9 +9,9 @@ Querying an existing tree is the `sense` skill. This one covers making a tree: i
9
9
 
10
10
  ## Setup
11
11
 
12
- - `npm install -g sensemaking`, then `sense init` at the tree root writes `sense.config.json`: two presets (`default`, and `large` showing what a big tree tunes) and an `embed` block naming the model. The model fetches once per machine at the first vector search (progress on stderr); `sense download` prefetches it instead where that timing matters (CI, air-gapped setup). Config discovery walks up from cwd; `--config <path>` overrides.
12
+ - `npm install -g sensemaking`, then `sense init` at the tree root writes `sense.config.json`: two presets (`default`, and `large` showing what a big tree tunes) and an `embed` block naming the model. The model fetches once per machine at the first vector search (progress on stderr); `sense download` prefetches it instead where that timing matters (CI, air-gapped setup). Config discovery walks up from cwd; `--config <path>` overrides. A top-level `root` can point the config at another markdown tree: it resolves relative to the config directory, makes all globs and stored paths tree-relative, while `.sense/` remains beside the config. Use this when one vault needs independent consumer-specific presets, queries, and caches.
13
13
  - **Backing store.** The config's `store` key: `sqlite` (default, zero-dependency, Node's built-in SQLite), or the experimental `duckdb` and `turso`, whose engine package the first command that opens such a tree installs on its own (`@duckdb/node-api`, a one-time native download of about 110 MB; `@tursodatabase/database`, much smaller). The same commands and tables run on all three. Two things do not port, and each one decides a tree. **FTS5 syntax:** under `duckdb` and `turso`, `search` text and raw `MATCH` reject FTS5's prefix, boolean, `NEAR`, initial-token and column-filter operators with a named error, and sqlite's FTS5 SQL (`MATCH`, `snippet()`, `bm25()`) does not run, so saved queries written in that syntax are sqlite dialect; a tree whose saved queries or search vocabulary depend on FTS5 operators stays on `sqlite`. **SQL functions:** `has`/`basename` run on all three (`turso` rewrites them into portable SQL rather than registering them); `segment` runs on `sqlite` and `duckdb` only, so a tree whose queries call `segment` stays off `turso`. `sense watch` runs on all three; `duckdb` and `turso` lock their cache file per connection, so a concurrent command waits out the watcher's current cycle instead of failing. Each store keeps its own cache file (`.sense/cache.db`, `.sense/cache.duckdb`, `.sense/cache.turso.db`); switching stores is a rebuild, not a migration. Speed is a separate axis and must come from a current identical-work measurement. Historical per-store rows can include different ranked candidates and are diagnostics, not an overall store leaderboard. The cache remains an ordinary database file, so a tree indexed under duckdb is readable by DuckDB's ecosystem, while turso brings concurrent access, non-blocking I/O and encryption. Pick for the capabilities you need, then review the current evidence in BENCHMARKING.md.
14
- - Globs resolve relative to the config file, never the cwd.
14
+ - Globs resolve relative to `root` when present, otherwise the config file directory; never the cwd.
15
15
  - `sense status` and `sense map` show each preset's coverage (files matched, embedded count), so what a config actually indexes is always visible in output. A config edit that changes coverage rebuilds the cache and names the preset that caused it on stderr.
16
16
 
17
17
  ## Presets