sensemaking 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +0 -1
  2. package/dist/cjs/chunk/extract.d.cts +1 -0
  3. package/dist/cjs/chunk/extract.d.ts +1 -0
  4. package/dist/cjs/chunk/extract.js +23 -5
  5. package/dist/cjs/chunk/extract.js.map +1 -1
  6. package/dist/cjs/chunk/index.d.cts +1 -1
  7. package/dist/cjs/chunk/index.d.ts +1 -1
  8. package/dist/cjs/chunk/index.js +3 -0
  9. package/dist/cjs/chunk/index.js.map +1 -1
  10. package/dist/cjs/config/access.d.cts +0 -1
  11. package/dist/cjs/config/access.d.ts +0 -1
  12. package/dist/cjs/config/access.js +10 -17
  13. package/dist/cjs/config/access.js.map +1 -1
  14. package/dist/cjs/config/index.d.cts +1 -1
  15. package/dist/cjs/config/index.d.ts +1 -1
  16. package/dist/cjs/config/index.js +0 -3
  17. package/dist/cjs/config/index.js.map +1 -1
  18. package/dist/cjs/config/types.d.cts +0 -3
  19. package/dist/cjs/config/types.d.ts +0 -3
  20. package/dist/cjs/config/types.js.map +1 -1
  21. package/dist/cjs/config/validate.js +6 -24
  22. package/dist/cjs/config/validate.js.map +1 -1
  23. package/dist/cjs/errors.d.cts +1 -1
  24. package/dist/cjs/errors.d.ts +1 -1
  25. package/dist/cjs/errors.js.map +1 -1
  26. package/dist/cjs/features/links.js +4 -5
  27. package/dist/cjs/features/links.js.map +1 -1
  28. package/dist/cjs/features/sections.js +4 -1
  29. package/dist/cjs/features/sections.js.map +1 -1
  30. package/dist/cjs/scan/pool.d.cts +15 -0
  31. package/dist/cjs/scan/pool.d.ts +15 -0
  32. package/dist/cjs/scan/pool.js +263 -0
  33. package/dist/cjs/scan/pool.js.map +1 -0
  34. package/dist/cjs/scan/reparse.d.cts +2 -0
  35. package/dist/cjs/scan/reparse.d.ts +2 -0
  36. package/dist/cjs/scan/reparse.js +15 -76
  37. package/dist/cjs/scan/reparse.js.map +1 -1
  38. package/dist/cjs/store/builder.d.cts +19 -0
  39. package/dist/cjs/store/builder.d.ts +19 -0
  40. package/dist/cjs/store/builder.js +618 -0
  41. package/dist/cjs/store/builder.js.map +1 -0
  42. package/dist/cjs/store/duckdb/lexical.d.cts +1 -0
  43. package/dist/cjs/store/duckdb/lexical.d.ts +1 -0
  44. package/dist/cjs/store/duckdb/lexical.js +21 -5
  45. package/dist/cjs/store/duckdb/lexical.js.map +1 -1
  46. package/dist/cjs/store/duckdb/open.d.cts +9 -8
  47. package/dist/cjs/store/duckdb/open.d.ts +9 -8
  48. package/dist/cjs/store/duckdb/open.js +77 -123
  49. package/dist/cjs/store/duckdb/open.js.map +1 -1
  50. package/dist/cjs/store/duckdb/reconcile.js +34 -5
  51. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  52. package/dist/cjs/store/duckdb/store.d.cts +1 -2
  53. package/dist/cjs/store/duckdb/store.d.ts +1 -2
  54. package/dist/cjs/store/duckdb/store.js +1 -26
  55. package/dist/cjs/store/duckdb/store.js.map +1 -1
  56. package/dist/cjs/store/embed-scope.d.cts +2 -0
  57. package/dist/cjs/store/embed-scope.d.ts +2 -0
  58. package/dist/cjs/store/embed-scope.js +85 -0
  59. package/dist/cjs/store/embed-scope.js.map +1 -0
  60. package/dist/cjs/store/feature-scope.d.cts +8 -0
  61. package/dist/cjs/store/feature-scope.d.ts +8 -0
  62. package/dist/cjs/store/feature-scope.js +107 -0
  63. package/dist/cjs/store/feature-scope.js.map +1 -0
  64. package/dist/cjs/store/index.d.cts +8 -3
  65. package/dist/cjs/store/index.d.ts +8 -3
  66. package/dist/cjs/store/index.js +36 -7
  67. package/dist/cjs/store/index.js.map +1 -1
  68. package/dist/cjs/store/open.d.cts +15 -0
  69. package/dist/cjs/store/open.d.ts +15 -0
  70. package/dist/cjs/store/open.js +608 -0
  71. package/dist/cjs/store/open.js.map +1 -0
  72. package/dist/cjs/store/preset-scope.d.cts +3 -0
  73. package/dist/cjs/store/preset-scope.d.ts +3 -0
  74. package/dist/cjs/store/preset-scope.js +293 -0
  75. package/dist/cjs/store/preset-scope.js.map +1 -0
  76. package/dist/cjs/store/reconcile.d.cts +2 -1
  77. package/dist/cjs/store/reconcile.d.ts +2 -1
  78. package/dist/cjs/store/reconcile.js +175 -179
  79. package/dist/cjs/store/reconcile.js.map +1 -1
  80. package/dist/cjs/store/{sqlite/signature.d.cts → signature.d.cts} +0 -3
  81. package/dist/cjs/store/{sqlite/signature.d.ts → signature.d.ts} +0 -3
  82. package/dist/cjs/store/signature.js +164 -0
  83. package/dist/cjs/store/signature.js.map +1 -0
  84. package/dist/cjs/store/sqlite/lexical.d.cts +1 -1
  85. package/dist/cjs/store/sqlite/lexical.d.ts +1 -1
  86. package/dist/cjs/store/sqlite/lexical.js +2 -2
  87. package/dist/cjs/store/sqlite/lexical.js.map +1 -1
  88. package/dist/cjs/store/sqlite/open.d.cts +8 -9
  89. package/dist/cjs/store/sqlite/open.d.ts +8 -9
  90. package/dist/cjs/store/sqlite/open.js +82 -311
  91. package/dist/cjs/store/sqlite/open.js.map +1 -1
  92. package/dist/cjs/store/sqlite/reconcile.d.cts +1 -1
  93. package/dist/cjs/store/sqlite/reconcile.d.ts +1 -1
  94. package/dist/cjs/store/sqlite/reconcile.js +78 -16
  95. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  96. package/dist/cjs/store/sqlite/sql-functions.d.cts +1 -1
  97. package/dist/cjs/store/sqlite/sql-functions.d.ts +1 -1
  98. package/dist/cjs/store/sqlite/sql-functions.js +3 -3
  99. package/dist/cjs/store/sqlite/sql-functions.js.map +1 -1
  100. package/dist/cjs/store/sqlite/store.d.cts +1 -2
  101. package/dist/cjs/store/sqlite/store.d.ts +1 -2
  102. package/dist/cjs/store/sqlite/store.js +2 -15
  103. package/dist/cjs/store/sqlite/store.js.map +1 -1
  104. package/dist/cjs/store/turso/open.d.cts +6 -11
  105. package/dist/cjs/store/turso/open.d.ts +6 -11
  106. package/dist/cjs/store/turso/open.js +85 -171
  107. package/dist/cjs/store/turso/open.js.map +1 -1
  108. package/dist/cjs/store/turso/reconcile.d.cts +2 -0
  109. package/dist/cjs/store/turso/reconcile.d.ts +2 -0
  110. package/dist/cjs/store/turso/reconcile.js +88 -8
  111. package/dist/cjs/store/turso/reconcile.js.map +1 -1
  112. package/dist/cjs/store/turso/store.d.cts +1 -2
  113. package/dist/cjs/store/turso/store.d.ts +1 -2
  114. package/dist/cjs/store/turso/store.js +1 -13
  115. package/dist/cjs/store/turso/store.js.map +1 -1
  116. package/dist/cjs/store/types.d.cts +16 -6
  117. package/dist/cjs/store/types.d.ts +16 -6
  118. package/dist/cjs/store/types.js.map +1 -1
  119. package/dist/cjs/text/strip.js +3 -6
  120. package/dist/cjs/text/strip.js.map +1 -1
  121. package/dist/cjs/watch.js +28 -16
  122. package/dist/cjs/watch.js.map +1 -1
  123. package/dist/esm/chunk/extract.d.ts +1 -0
  124. package/dist/esm/chunk/extract.js +10 -2
  125. package/dist/esm/chunk/extract.js.map +1 -1
  126. package/dist/esm/chunk/index.d.ts +1 -1
  127. package/dist/esm/chunk/index.js +1 -1
  128. package/dist/esm/chunk/index.js.map +1 -1
  129. package/dist/esm/config/access.d.ts +0 -1
  130. package/dist/esm/config/access.js +12 -17
  131. package/dist/esm/config/access.js.map +1 -1
  132. package/dist/esm/config/index.d.ts +1 -1
  133. package/dist/esm/config/index.js +1 -1
  134. package/dist/esm/config/index.js.map +1 -1
  135. package/dist/esm/config/types.d.ts +0 -3
  136. package/dist/esm/config/types.js.map +1 -1
  137. package/dist/esm/config/validate.js +6 -22
  138. package/dist/esm/config/validate.js.map +1 -1
  139. package/dist/esm/errors.d.ts +1 -1
  140. package/dist/esm/errors.js.map +1 -1
  141. package/dist/esm/features/links.js +3 -4
  142. package/dist/esm/features/links.js.map +1 -1
  143. package/dist/esm/features/sections.js +4 -1
  144. package/dist/esm/features/sections.js.map +1 -1
  145. package/dist/esm/scan/pool.d.ts +15 -0
  146. package/dist/esm/scan/pool.js +77 -0
  147. package/dist/esm/scan/pool.js.map +1 -0
  148. package/dist/esm/scan/reparse.d.ts +2 -0
  149. package/dist/esm/scan/reparse.js +11 -58
  150. package/dist/esm/scan/reparse.js.map +1 -1
  151. package/dist/esm/store/builder.d.ts +19 -0
  152. package/dist/esm/store/builder.js +163 -0
  153. package/dist/esm/store/builder.js.map +1 -0
  154. package/dist/esm/store/duckdb/lexical.d.ts +1 -0
  155. package/dist/esm/store/duckdb/lexical.js +22 -9
  156. package/dist/esm/store/duckdb/lexical.js.map +1 -1
  157. package/dist/esm/store/duckdb/open.d.ts +9 -8
  158. package/dist/esm/store/duckdb/open.js +36 -58
  159. package/dist/esm/store/duckdb/open.js.map +1 -1
  160. package/dist/esm/store/duckdb/reconcile.js +15 -3
  161. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  162. package/dist/esm/store/duckdb/store.d.ts +1 -2
  163. package/dist/esm/store/duckdb/store.js +2 -11
  164. package/dist/esm/store/duckdb/store.js.map +1 -1
  165. package/dist/esm/store/embed-scope.d.ts +2 -0
  166. package/dist/esm/store/embed-scope.js +28 -0
  167. package/dist/esm/store/embed-scope.js.map +1 -0
  168. package/dist/esm/store/feature-scope.d.ts +8 -0
  169. package/dist/esm/store/feature-scope.js +49 -0
  170. package/dist/esm/store/feature-scope.js.map +1 -0
  171. package/dist/esm/store/index.d.ts +8 -3
  172. package/dist/esm/store/index.js +22 -7
  173. package/dist/esm/store/index.js.map +1 -1
  174. package/dist/esm/store/open.d.ts +15 -0
  175. package/dist/esm/store/open.js +187 -0
  176. package/dist/esm/store/open.js.map +1 -0
  177. package/dist/esm/store/preset-scope.d.ts +3 -0
  178. package/dist/esm/store/preset-scope.js +77 -0
  179. package/dist/esm/store/preset-scope.js.map +1 -0
  180. package/dist/esm/store/reconcile.d.ts +2 -1
  181. package/dist/esm/store/reconcile.js +33 -6
  182. package/dist/esm/store/reconcile.js.map +1 -1
  183. package/dist/esm/store/{sqlite/signature.d.ts → signature.d.ts} +0 -3
  184. package/dist/esm/store/signature.js +37 -0
  185. package/dist/esm/store/signature.js.map +1 -0
  186. package/dist/esm/store/sqlite/lexical.d.ts +1 -1
  187. package/dist/esm/store/sqlite/lexical.js +4 -4
  188. package/dist/esm/store/sqlite/lexical.js.map +1 -1
  189. package/dist/esm/store/sqlite/open.d.ts +8 -9
  190. package/dist/esm/store/sqlite/open.js +41 -147
  191. package/dist/esm/store/sqlite/open.js.map +1 -1
  192. package/dist/esm/store/sqlite/reconcile.d.ts +1 -1
  193. package/dist/esm/store/sqlite/reconcile.js +14 -14
  194. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  195. package/dist/esm/store/sqlite/sql-functions.d.ts +1 -1
  196. package/dist/esm/store/sqlite/sql-functions.js +3 -3
  197. package/dist/esm/store/sqlite/sql-functions.js.map +1 -1
  198. package/dist/esm/store/sqlite/store.d.ts +1 -2
  199. package/dist/esm/store/sqlite/store.js +3 -9
  200. package/dist/esm/store/sqlite/store.js.map +1 -1
  201. package/dist/esm/store/turso/open.d.ts +6 -11
  202. package/dist/esm/store/turso/open.js +34 -78
  203. package/dist/esm/store/turso/open.js.map +1 -1
  204. package/dist/esm/store/turso/reconcile.d.ts +2 -0
  205. package/dist/esm/store/turso/reconcile.js +18 -3
  206. package/dist/esm/store/turso/reconcile.js.map +1 -1
  207. package/dist/esm/store/turso/store.d.ts +1 -2
  208. package/dist/esm/store/turso/store.js +2 -7
  209. package/dist/esm/store/turso/store.js.map +1 -1
  210. package/dist/esm/store/types.d.ts +16 -6
  211. package/dist/esm/store/types.js.map +1 -1
  212. package/dist/esm/text/strip.js +3 -4
  213. package/dist/esm/text/strip.js.map +1 -1
  214. package/dist/esm/watch.js +5 -3
  215. package/dist/esm/watch.js.map +1 -1
  216. package/package.json +4 -3
  217. package/schema.json +0 -11
  218. package/dist/cjs/store/sqlite/signature.js +0 -380
  219. package/dist/cjs/store/sqlite/signature.js.map +0 -1
  220. package/dist/esm/store/sqlite/signature.js +0 -58
  221. package/dist/esm/store/sqlite/signature.js.map +0 -1
@@ -0,0 +1,187 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { featureSignature, STATE_DIR } from '../config/index.js';
4
+ import { rekeyChunkText } from '../embed/handoff.js';
5
+ import { SenseError } from '../errors.js';
6
+ import { FEATURES } from '../features/index.js';
7
+ import { createBuilder } from './builder.js';
8
+ import { clearCache } from './cache.js';
9
+ import { classifyEmbedChange } from './embed-scope.js';
10
+ import { classifyFeatureToggles, isFeatureOnlyChange } from './feature-scope.js';
11
+ import { forcedPresetPaths, isPresetOnlyChange } from './preset-scope.js';
12
+ import { getMeta, setMeta } from './shared.js';
13
+ import { changedSignatureKeys, embedIdentityAdopted, signatureDiff } from './signature.js';
14
+ // duckdb and turso hold the cache file for their connection's whole life, not for a transaction,
15
+ // so a second command waits on the first command finishing. Bounded rather than open-ended: this
16
+ // clears a warm query's collision (~100ms) and still fails loudly behind a cold build instead of
17
+ // looking hung.
18
+ const LOCK_RETRY_MS = 5000;
19
+ const LOCK_POLL_MS = 50;
20
+ async function connectUnlocked(dbPath, cfg, dialect) {
21
+ const deadline = Date.now() + LOCK_RETRY_MS;
22
+ for(;;){
23
+ try {
24
+ return await dialect.connect(dbPath, cfg);
25
+ } catch (err) {
26
+ var _dialect_isLocked;
27
+ if (!((_dialect_isLocked = dialect.isLocked) === null || _dialect_isLocked === void 0 ? void 0 : _dialect_isLocked.call(dialect, err))) throw err;
28
+ if (Date.now() >= deadline) {
29
+ throw new SenseError('STORE_BUSY', `another sense process is using this tree's ${cfg.store} cache (${dbPath}) and did not release it within ${LOCK_RETRY_MS / 1000}s; wait for that command to finish, or set "store" to "sqlite" in sense.config.json, which serves concurrent commands`);
30
+ }
31
+ await new Promise((resolve)=>setTimeout(resolve, LOCK_POLL_MS));
32
+ }
33
+ }
34
+ }
35
+ async function connectWithDialect(cfg, dialect, keepBuilderOpen) {
36
+ const stateDir = join(cfg.baseDir, STATE_DIR);
37
+ mkdirSync(stateDir, {
38
+ recursive: true
39
+ });
40
+ const dbPath = join(stateDir, dialect.filename);
41
+ const { handle, conn } = await connectUnlocked(dbPath, cfg, dialect);
42
+ // A throw below must release this handle, or the leaked WAL makes the cache file undeletable on
43
+ // Windows. `closed` keeps the catch from double-closing a handle a rebuild branch already closed.
44
+ let closed = false;
45
+ // Created here, not inside reconcile(): its pool must survive across every build() call this
46
+ // connection sees (a watcher's repeated ticks), not just the one below.
47
+ const builder = createBuilder(conn, cfg, cfg.baseDir, dialect.reconcileDialect);
48
+ try {
49
+ await conn.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
50
+ // Schema-version or feature-set mismatch: reconcile only reparses changed files, so an
51
+ // old cache can't be patched incrementally -- rebuild instead (cheap: nothing expensive lives here).
52
+ const version = await getMeta(conn, 'schema_version');
53
+ const features = await getMeta(conn, 'features');
54
+ const wantFeatures = featureSignature(cfg, FEATURES);
55
+ // Set only by the preset-membership branch below; reconcile treats these paths as touched
56
+ // despite an unchanged stamp, for this one build() call.
57
+ let forcedPaths;
58
+ // Set only by the embed-narrow branch below; applied via builder.invalidate() after ensureSchema.
59
+ let embedInvalidate;
60
+ // Set only by the feature-toggle branch below; applied via builder.invalidateFeatures() after ensureSchema.
61
+ let featureToggles;
62
+ if (version !== null && version !== dialect.schemaVersion || features !== null && features !== wantFeatures) {
63
+ // Indexing derives from presets, so a config edit rebuilding the cache must say so and
64
+ // name what changed -- silent rebuilds make derived indexing look like a hang or a bug.
65
+ if (version !== null && version !== dialect.schemaVersion) {
66
+ console.error('sense: cache format changed (new sensemaking version); rebuilding the index');
67
+ // Close before clearCache deletes the files out from under the still-open handle.
68
+ closed = true;
69
+ await dialect.close(handle);
70
+ clearCache(cfg);
71
+ return connectWithDialect(cfg, dialect, keepBuilderOpen);
72
+ }
73
+ const changedKeys = changedSignatureKeys(features !== null && features !== void 0 ? features : '', wantFeatures);
74
+ // Null (rather than the empty set) when changedKeys isn't preset-only, or a changed
75
+ // preset's stored segment can't be parsed back into old include/exclude/vectors -- either
76
+ // way the branch below is skipped and the full rebuild stays the fallback.
77
+ const presetForced = isPresetOnlyChange(changedKeys) ? forcedPresetPaths(cfg, cfg.baseDir, features !== null && features !== void 0 ? features : '', changedKeys) : null;
78
+ // Null unless exactly one segment changed and it decomposes into a recognised embed-only
79
+ // case (embed-scope.ts): anything else, including embed toggled on/off, stays null.
80
+ const embedKind = changedKeys.size === 1 && changedKeys.has('embed') ? classifyEmbedChange(features !== null && features !== void 0 ? features : '', wantFeatures) : null;
81
+ // Null (rather than the empty set) when changedKeys isn't feature-toggle-only, or a changed
82
+ // feature's stored segment can't be parsed back into old/new on-off -- either way the branch
83
+ // below is skipped and the full rebuild stays the fallback.
84
+ const toggles = isFeatureOnlyChange(changedKeys) ? classifyFeatureToggles(features !== null && features !== void 0 ? features : '', wantFeatures, changedKeys) : null;
85
+ if (changedKeys.size === 1 && changedKeys.has('embed') && embedIdentityAdopted(features !== null && features !== void 0 ? features : '', wantFeatures)) {
86
+ // First sight of a resolved weight identity: the model itself hasn't changed, so
87
+ // adopt it into meta with no rebuild and no re-embed.
88
+ console.error("sense: recorded the embedding model's resolved identity; vectors are unaffected");
89
+ await setMeta(conn, 'features', wantFeatures);
90
+ } else if (embedKind !== null) {
91
+ // Chunk boundaries are file-derived, not model-derived (embed.ts's chunksOf), so neither
92
+ // case needs any file reparsed for any feature but embed itself.
93
+ embedInvalidate = embedKind;
94
+ 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');
95
+ await setMeta(conn, 'features', wantFeatures);
96
+ } else if (presetForced !== null) {
97
+ // Membership only: which files a preset covers moved, not a file's own content, a
98
+ // global feature, or the embed model. reconcile() already knows how to
99
+ // add, update, and remove a file correctly (every cross-feature cascade included); it
100
+ // just needs telling which unchanged files to treat as touched (forcedPaths, above).
101
+ forcedPaths = presetForced;
102
+ const changed = signatureDiff(features !== null && features !== void 0 ? features : '', wantFeatures);
103
+ console.error(`sense: config change (${changed}) reparses only the files it affects`);
104
+ await setMeta(conn, 'features', wantFeatures);
105
+ } else if (toggles !== null) {
106
+ // A feature turned off or on: reconcile.ts's activeFeatures skips a disabled feature's
107
+ // hooks entirely, so its rows rot while off -- invalidateFeatures drops them and, for a
108
+ // feature turning back on, fully re-derives across every indexed file rather than
109
+ // trusting rows written before or during that window.
110
+ featureToggles = toggles;
111
+ const changed = signatureDiff(features !== null && features !== void 0 ? features : '', wantFeatures);
112
+ console.error(`sense: config change (${changed}) reparses only the feature it affects`);
113
+ await setMeta(conn, 'features', wantFeatures);
114
+ } else {
115
+ const changed = signatureDiff(features !== null && features !== void 0 ? features : '', wantFeatures);
116
+ console.error(`sense: config change (${changed}) rebuilds the index`);
117
+ closed = true;
118
+ await dialect.close(handle);
119
+ clearCache(cfg);
120
+ return connectWithDialect(cfg, dialect, keepBuilderOpen);
121
+ }
122
+ }
123
+ await dialect.ensureSchema(handle, conn, cfg);
124
+ // 3x the largest reconcile this cache has recorded, floored at 30s and capped at 10min.
125
+ // Installed before build() -- that call is the one that races a watcher's transaction.
126
+ if (dialect.setDerivedBusyTimeout) {
127
+ var _ref;
128
+ const recordedMaxMs = Number((_ref = await getMeta(conn, 'reconcile_max_ms')) !== null && _ref !== void 0 ? _ref : '0');
129
+ await dialect.setDerivedBusyTimeout(handle, conn, Math.min(Math.max(30000, 3 * recordedMaxMs), 600000));
130
+ }
131
+ // Runs before build(): forcedPaths, embedInvalidate and featureToggles never target the same
132
+ // call (each comes from its own mutually exclusive branch above: a preset-only, embed-only, or
133
+ // feature-toggle-only changed-key set).
134
+ const embedParsed = embedInvalidate ? (await builder.invalidate(embedInvalidate)).parsed : 0;
135
+ const featureParsed = featureToggles ? (await builder.invalidateFeatures(featureToggles)).parsed : 0;
136
+ const { parsed, warnings } = await builder.build(forcedPaths);
137
+ // A one-shot open is done reconciling for good; a watcher keeps `builder` alive across its run.
138
+ if (!keepBuilderOpen) await builder.close();
139
+ return {
140
+ handle,
141
+ conn,
142
+ cfg,
143
+ dbPath,
144
+ parsed: parsed + embedParsed + featureParsed,
145
+ warnings,
146
+ builder
147
+ };
148
+ } catch (err) {
149
+ await builder.close();
150
+ if (!closed) await dialect.close(handle);
151
+ throw err;
152
+ }
153
+ }
154
+ // A store's open(): connects (see connectWithDialect above), then wraps the resulting connection
155
+ // in the Store interface. The builder's pool closes right after the initial build.
156
+ export async function openWithDialect(cfg, dialect) {
157
+ const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings } = await connectWithDialect(cfg, dialect, false);
158
+ const store = dialect.createStore(handle, conn, resolvedCfg);
159
+ // reconcile ran before this object existed, so its chunk text is keyed by the connection.
160
+ rekeyChunkText(conn, store);
161
+ return {
162
+ store,
163
+ cfg: resolvedCfg,
164
+ dbPath,
165
+ parsed,
166
+ warnings
167
+ };
168
+ }
169
+ // Same connect+build as openWithDialect, but keeps the builder's pool alive and hands it back
170
+ // alongside the store, for a caller (a watcher) that reconciles repeatedly on this connection.
171
+ export async function openWithBuilder(cfg, dialect) {
172
+ const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings, builder } = await connectWithDialect(cfg, dialect, true);
173
+ const store = dialect.createStore(handle, conn, resolvedCfg);
174
+ rekeyChunkText(conn, store);
175
+ return {
176
+ store,
177
+ cfg: resolvedCfg,
178
+ dbPath,
179
+ parsed,
180
+ warnings,
181
+ builder
182
+ };
183
+ }
184
+ export async function docCount(store) {
185
+ const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');
186
+ return (await stmt.get()).n;
187
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/open.ts"],"sourcesContent":["import { 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 type { Builder } from './builder.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 { forcedPresetPaths, isPresetOnlyChange } from './preset-scope.ts';\nimport { getMeta, setMeta } from './shared.ts';\nimport { changedSignatureKeys, embedIdentityAdopted, signatureDiff } from './signature.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}\n\ninterface ConnectResult<Handle> {\n handle: Handle;\n conn: Connection;\n cfg: ResolvedConfig;\n dbPath: string;\n parsed: number;\n warnings: string[];\n // Closed already unless `keepBuilderOpen` was set: a one-shot open reconciles once and is done\n // with it, a watcher keeps calling build() on the same instance across its whole run.\n builder: Builder;\n}\n\n// duckdb and turso hold the cache file for their connection's whole life, not for a transaction,\n// so a second command waits on the first command finishing. Bounded rather than open-ended: this\n// clears a warm query's collision (~100ms) and still fails loudly behind a cold build instead of\n// looking hung.\nconst LOCK_RETRY_MS = 5_000;\nconst LOCK_POLL_MS = 50;\n\nasync function connectUnlocked<Handle>(dbPath: string, cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<{ handle: Handle; conn: Connection }> {\n const deadline = Date.now() + LOCK_RETRY_MS;\n for (;;) {\n try {\n return await dialect.connect(dbPath, cfg);\n } catch (err) {\n if (!dialect.isLocked?.(err as Error)) throw err;\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 ${LOCK_RETRY_MS / 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>, keepBuilderOpen: boolean): 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(): its pool must survive across every build() call this\n // connection sees (a watcher's repeated ticks), not just the one below.\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, keepBuilderOpen);\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, keepBuilderOpen);\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 } = await builder.build(forcedPaths);\n // A one-shot open is done reconciling for good; a watcher keeps `builder` alive across its run.\n if (!keepBuilderOpen) await builder.close();\n\n return { handle, conn, cfg, dbPath, parsed: parsed + embedParsed + featureParsed, warnings, builder };\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 } = await connectWithDialect(cfg, dialect, false);\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 };\n}\n\n// Same connect+build as openWithDialect, but keeps the builder's pool alive and hands it back\n// alongside the store, for a caller (a watcher) that reconciles repeatedly on this connection.\nexport async function openWithBuilder<Handle>(cfg: ResolvedConfig, dialect: OpenDialect<Handle>): Promise<OpenResult & { builder: Builder }> {\n const { handle, conn, cfg: resolvedCfg, dbPath, parsed, warnings, builder } = await connectWithDialect(cfg, dialect, true);\n const store = dialect.createStore(handle, conn, resolvedCfg);\n rekeyChunkText(conn, store);\n return { store, cfg: resolvedCfg, dbPath, parsed, warnings, builder };\n}\n\nexport async function docCount(store: Store): Promise<number> {\n const stmt = await store.prepare('SELECT COUNT(*) AS n FROM frontmatter');\n return ((await stmt.get()) as { n: number }).n;\n}\n"],"names":["mkdirSync","join","featureSignature","STATE_DIR","rekeyChunkText","SenseError","FEATURES","createBuilder","clearCache","classifyEmbedChange","classifyFeatureToggles","isFeatureOnlyChange","forcedPresetPaths","isPresetOnlyChange","getMeta","setMeta","changedSignatureKeys","embedIdentityAdopted","signatureDiff","LOCK_RETRY_MS","LOCK_POLL_MS","connectUnlocked","dbPath","cfg","dialect","deadline","Date","now","connect","err","isLocked","store","Promise","resolve","setTimeout","connectWithDialect","keepBuilderOpen","stateDir","baseDir","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","Math","min","max","embedParsed","invalidate","parsed","featureParsed","invalidateFeatures","warnings","build","openWithDialect","resolvedCfg","createStore","openWithBuilder","docCount","stmt","prepare","get","n"],"mappings":"AAAA,SAASA,SAAS,QAAQ,UAAU;AACpC,SAASC,IAAI,QAAQ,YAAY;AAEjC,SAASC,gBAAgB,EAAEC,SAAS,QAAQ,qBAAqB;AACjE,SAASC,cAAc,QAAQ,sBAAsB;AACrD,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,QAAQ,QAAQ,uBAAuB;AAEhD,SAASC,aAAa,QAAQ,eAAe;AAC7C,SAASC,UAAU,QAAQ,aAAa;AAExC,SAASC,mBAAmB,QAAQ,mBAAmB;AAEvD,SAASC,sBAAsB,EAAEC,mBAAmB,QAAQ,qBAAqB;AACjF,SAASC,iBAAiB,EAAEC,kBAAkB,QAAQ,oBAAoB;AAC1E,SAASC,OAAO,EAAEC,OAAO,QAAQ,cAAc;AAC/C,SAASC,oBAAoB,EAAEC,oBAAoB,EAAEC,aAAa,QAAQ,iBAAiB;AA4B3F,iGAAiG;AACjG,iGAAiG;AACjG,iGAAiG;AACjG,gBAAgB;AAChB,MAAMC,gBAAgB;AACtB,MAAMC,eAAe;AAErB,eAAeC,gBAAwBC,MAAc,EAAEC,GAAmB,EAAEC,OAA4B;IACtG,MAAMC,WAAWC,KAAKC,GAAG,KAAKR;IAC9B,OAAS;QACP,IAAI;YACF,OAAO,MAAMK,QAAQI,OAAO,CAACN,QAAQC;QACvC,EAAE,OAAOM,KAAK;gBACPL;YAAL,IAAI,GAACA,oBAAAA,QAAQM,QAAQ,cAAhBN,wCAAAA,uBAAAA,SAAmBK,OAAe,MAAMA;YAC7C,IAAIH,KAAKC,GAAG,MAAMF,UAAU;gBAC1B,MAAM,IAAIpB,WAAW,cAAc,CAAC,2CAA2C,EAAEkB,IAAIQ,KAAK,CAAC,QAAQ,EAAET,OAAO,gCAAgC,EAAEH,gBAAgB,KAAK,qHAAqH,CAAC;YAC3R;YACA,MAAM,IAAIa,QAAQ,CAACC,UAAYC,WAAWD,SAASb;QACrD;IACF;AACF;AAEA,eAAee,mBAA2BZ,GAAmB,EAAEC,OAA4B,EAAEY,eAAwB;IACnH,MAAMC,WAAWpC,KAAKsB,IAAIe,OAAO,EAAEnC;IACnCH,UAAUqC,UAAU;QAAEE,WAAW;IAAK;IACtC,MAAMjB,SAASrB,KAAKoC,UAAUb,QAAQgB,QAAQ;IAE9C,MAAM,EAAEC,MAAM,EAAEC,IAAI,EAAE,GAAG,MAAMrB,gBAAgBC,QAAQC,KAAKC;IAC5D,gGAAgG;IAChG,kGAAkG;IAClG,IAAImB,SAAS;IACb,6FAA6F;IAC7F,wEAAwE;IACxE,MAAMC,UAAUrC,cAAcmC,MAAMnB,KAAKA,IAAIe,OAAO,EAAEd,QAAQqB,gBAAgB;IAC9E,IAAI;QACF,MAAMH,KAAKI,IAAI,CAAC;QAEhB,uFAAuF;QACvF,qGAAqG;QACrG,MAAMC,UAAU,MAAMjC,QAAQ4B,MAAM;QACpC,MAAMM,WAAW,MAAMlC,QAAQ4B,MAAM;QACrC,MAAMO,eAAe/C,iBAAiBqB,KAAKjB;QAC3C,0FAA0F;QAC1F,yDAAyD;QACzD,IAAI4C;QACJ,kGAAkG;QAClG,IAAIC;QACJ,4GAA4G;QAC5G,IAAIC;QACJ,IAAI,AAACL,YAAY,QAAQA,YAAYvB,QAAQ6B,aAAa,IAAML,aAAa,QAAQA,aAAaC,cAAe;YAC/G,uFAAuF;YACvF,wFAAwF;YACxF,IAAIF,YAAY,QAAQA,YAAYvB,QAAQ6B,aAAa,EAAE;gBACzDC,QAAQC,KAAK,CAAC;gBACd,kFAAkF;gBAClFZ,SAAS;gBACT,MAAMnB,QAAQgC,KAAK,CAACf;gBACpBjC,WAAWe;gBACX,OAAOY,mBAAmBZ,KAAKC,SAASY;YAC1C;YACA,MAAMqB,cAAczC,qBAAqBgC,qBAAAA,sBAAAA,WAAY,IAAIC;YACzD,oFAAoF;YACpF,0FAA0F;YAC1F,2EAA2E;YAC3E,MAAMS,eAAe7C,mBAAmB4C,eAAe7C,kBAAkBW,KAAKA,IAAIe,OAAO,EAAEU,qBAAAA,sBAAAA,WAAY,IAAIS,eAAe;YAC1H,yFAAyF;YACzF,oFAAoF;YACpF,MAAME,YAAYF,YAAYG,IAAI,KAAK,KAAKH,YAAYI,GAAG,CAAC,WAAWpD,oBAAoBuC,qBAAAA,sBAAAA,WAAY,IAAIC,gBAAgB;YAC3H,4FAA4F;YAC5F,6FAA6F;YAC7F,4DAA4D;YAC5D,MAAMa,UAAUnD,oBAAoB8C,eAAe/C,uBAAuBsC,qBAAAA,sBAAAA,WAAY,IAAIC,cAAcQ,eAAe;YACvH,IAAIA,YAAYG,IAAI,KAAK,KAAKH,YAAYI,GAAG,CAAC,YAAY5C,qBAAqB+B,qBAAAA,sBAAAA,WAAY,IAAIC,eAAe;gBAC5G,iFAAiF;gBACjF,sDAAsD;gBACtDK,QAAQC,KAAK,CAAC;gBACd,MAAMxC,QAAQ2B,MAAM,YAAYO;YAClC,OAAO,IAAIU,cAAc,MAAM;gBAC7B,yFAAyF;gBACzF,iEAAiE;gBACjER,kBAAkBQ;gBAClBL,QAAQC,KAAK,CAACI,cAAc,UAAU,6EAA6E;gBACnH,MAAM5C,QAAQ2B,MAAM,YAAYO;YAClC,OAAO,IAAIS,iBAAiB,MAAM;gBAChC,kFAAkF;gBAClF,uEAAuE;gBACvE,sFAAsF;gBACtF,qFAAqF;gBACrFR,cAAcQ;gBACd,MAAMK,UAAU7C,cAAc8B,qBAAAA,sBAAAA,WAAY,IAAIC;gBAC9CK,QAAQC,KAAK,CAAC,CAAC,sBAAsB,EAAEQ,QAAQ,oCAAoC,CAAC;gBACpF,MAAMhD,QAAQ2B,MAAM,YAAYO;YAClC,OAAO,IAAIa,YAAY,MAAM;gBAC3B,uFAAuF;gBACvF,wFAAwF;gBACxF,kFAAkF;gBAClF,sDAAsD;gBACtDV,iBAAiBU;gBACjB,MAAMC,UAAU7C,cAAc8B,qBAAAA,sBAAAA,WAAY,IAAIC;gBAC9CK,QAAQC,KAAK,CAAC,CAAC,sBAAsB,EAAEQ,QAAQ,sCAAsC,CAAC;gBACtF,MAAMhD,QAAQ2B,MAAM,YAAYO;YAClC,OAAO;gBACL,MAAMc,UAAU7C,cAAc8B,qBAAAA,sBAAAA,WAAY,IAAIC;gBAC9CK,QAAQC,KAAK,CAAC,CAAC,sBAAsB,EAAEQ,QAAQ,oBAAoB,CAAC;gBACpEpB,SAAS;gBACT,MAAMnB,QAAQgC,KAAK,CAACf;gBACpBjC,WAAWe;gBACX,OAAOY,mBAAmBZ,KAAKC,SAASY;YAC1C;QACF;QAEA,MAAMZ,QAAQwC,YAAY,CAACvB,QAAQC,MAAMnB;QAEzC,wFAAwF;QACxF,uFAAuF;QACvF,IAAIC,QAAQyC,qBAAqB,EAAE;gBACH;YAA9B,MAAMC,gBAAgBC,QAAQ,OAAA,MAAMrD,QAAQ4B,MAAM,iCAApB,kBAAA,OAA4C;YAC1E,MAAMlB,QAAQyC,qBAAqB,CAACxB,QAAQC,MAAM0B,KAAKC,GAAG,CAACD,KAAKE,GAAG,CAAC,OAAO,IAAIJ,gBAAgB;QACjG;QAEA,6FAA6F;QAC7F,+FAA+F;QAC/F,wCAAwC;QACxC,MAAMK,cAAcpB,kBAAkB,AAAC,CAAA,MAAMP,QAAQ4B,UAAU,CAACrB,gBAAe,EAAGsB,MAAM,GAAG;QAC3F,MAAMC,gBAAgBtB,iBAAiB,AAAC,CAAA,MAAMR,QAAQ+B,kBAAkB,CAACvB,eAAc,EAAGqB,MAAM,GAAG;QAEnG,MAAM,EAAEA,MAAM,EAAEG,QAAQ,EAAE,GAAG,MAAMhC,QAAQiC,KAAK,CAAC3B;QACjD,gGAAgG;QAChG,IAAI,CAACd,iBAAiB,MAAMQ,QAAQY,KAAK;QAEzC,OAAO;YAAEf;YAAQC;YAAMnB;YAAKD;YAAQmD,QAAQA,SAASF,cAAcG;YAAeE;YAAUhC;QAAQ;IACtG,EAAE,OAAOf,KAAK;QACZ,MAAMe,QAAQY,KAAK;QACnB,IAAI,CAACb,QAAQ,MAAMnB,QAAQgC,KAAK,CAACf;QACjC,MAAMZ;IACR;AACF;AAEA,iGAAiG;AACjG,mFAAmF;AACnF,OAAO,eAAeiD,gBAAwBvD,GAAmB,EAAEC,OAA4B;IAC7F,MAAM,EAAEiB,MAAM,EAAEC,IAAI,EAAEnB,KAAKwD,WAAW,EAAEzD,MAAM,EAAEmD,MAAM,EAAEG,QAAQ,EAAE,GAAG,MAAMzC,mBAAmBZ,KAAKC,SAAS;IAC5G,MAAMO,QAAQP,QAAQwD,WAAW,CAACvC,QAAQC,MAAMqC;IAChD,0FAA0F;IAC1F3E,eAAesC,MAAMX;IACrB,OAAO;QAAEA;QAAOR,KAAKwD;QAAazD;QAAQmD;QAAQG;IAAS;AAC7D;AAEA,8FAA8F;AAC9F,+FAA+F;AAC/F,OAAO,eAAeK,gBAAwB1D,GAAmB,EAAEC,OAA4B;IAC7F,MAAM,EAAEiB,MAAM,EAAEC,IAAI,EAAEnB,KAAKwD,WAAW,EAAEzD,MAAM,EAAEmD,MAAM,EAAEG,QAAQ,EAAEhC,OAAO,EAAE,GAAG,MAAMT,mBAAmBZ,KAAKC,SAAS;IACrH,MAAMO,QAAQP,QAAQwD,WAAW,CAACvC,QAAQC,MAAMqC;IAChD3E,eAAesC,MAAMX;IACrB,OAAO;QAAEA;QAAOR,KAAKwD;QAAazD;QAAQmD;QAAQG;QAAUhC;IAAQ;AACtE;AAEA,OAAO,eAAesC,SAASnD,KAAY;IACzC,MAAMoD,OAAO,MAAMpD,MAAMqD,OAAO,CAAC;IACjC,OAAO,AAAE,CAAA,MAAMD,KAAKE,GAAG,EAAC,EAAqBC,CAAC;AAChD"}
@@ -0,0 +1,3 @@
1
+ import type { Config } from '../config/index.js';
2
+ export declare function isPresetOnlyChange(changedKeys: ReadonlySet<string>): boolean;
3
+ export declare function forcedPresetPaths(cfg: Config, baseDir: string, before: string, changedKeys: ReadonlySet<string>): Set<string> | null;
@@ -0,0 +1,77 @@
1
+ import { presetHasSignal } from '../config/index.js';
2
+ import { listFiles } from '../scan/index.js';
3
+ // A signature change open() can route to a narrow reparse instead of a full clear: every changed
4
+ // segment is a preset's own (glob or vector on/off), never a global feature or embed change.
5
+ export function isPresetOnlyChange(changedKeys) {
6
+ return changedKeys.size > 0 && [
7
+ ...changedKeys
8
+ ].every((key)=>key.startsWith('preset:'));
9
+ }
10
+ // One preset's old include/exclude/vectors-on-off, read back out of a stored `preset:<name>:...`
11
+ // segment (config/access.ts's featureSignature format). Absent (a brand-new preset) is the empty
12
+ // preset, not a parse failure; a segment present but not in that exact shape returns null so the
13
+ // caller falls back to a full rebuild instead of guessing.
14
+ function parseOldPreset(before, name) {
15
+ const part = before.split('|').find((p)=>p.startsWith(`preset:${name}:`));
16
+ if (part === undefined) return {
17
+ include: [],
18
+ exclude: [],
19
+ vectors: false
20
+ };
21
+ const fields = part.split(':');
22
+ if (fields.length !== 5) return null;
23
+ const [, , includeStr, excludeStr, onOff] = fields;
24
+ if (onOff !== 'on' && onOff !== 'off') return null;
25
+ return {
26
+ include: includeStr === '' ? [] : includeStr.split('+'),
27
+ exclude: excludeStr === '' ? [] : excludeStr.split('+'),
28
+ vectors: onOff === 'on'
29
+ };
30
+ }
31
+ // Paths one preset definition covers, via the real listFiles so stat filtering (directories,
32
+ // dangling symlinks) matches production coverage exactly.
33
+ function matchPreset(cfg, baseDir, name, def) {
34
+ const scoped = {
35
+ ...cfg,
36
+ presets: {
37
+ [name]: {
38
+ include: def.include,
39
+ exclude: def.exclude
40
+ }
41
+ }
42
+ };
43
+ return new Set(listFiles(scoped, baseDir).map((f)=>f.relPath));
44
+ }
45
+ // Every path whose coverage by a changed preset genuinely moved: gained or lost it (a glob edit),
46
+ // or kept it under a flipped vectors on/off (that preset's whole match set, since its contribution
47
+ // to the file's union embed flag may have changed even though membership itself did not). Returns
48
+ // null when a changed preset's stored segment can't be parsed, so the caller takes the full rebuild.
49
+ export function forcedPresetPaths(cfg, baseDir, before, changedKeys) {
50
+ const forced = new Set();
51
+ for (const key of changedKeys){
52
+ var _cfg_presets_name_exclude;
53
+ const name = key.slice('preset:'.length);
54
+ const oldDef = parseOldPreset(before, name);
55
+ if (oldDef === null) return null;
56
+ const stillDeclared = Object.hasOwn(cfg.presets, name);
57
+ const newDef = stillDeclared ? {
58
+ include: cfg.presets[name].include,
59
+ exclude: (_cfg_presets_name_exclude = cfg.presets[name].exclude) !== null && _cfg_presets_name_exclude !== void 0 ? _cfg_presets_name_exclude : [],
60
+ vectors: presetHasSignal(cfg, name, 'vectors')
61
+ } : {
62
+ include: [],
63
+ exclude: [],
64
+ vectors: false
65
+ };
66
+ const oldMatch = matchPreset(cfg, baseDir, name, oldDef);
67
+ const newMatch = stillDeclared ? matchPreset(cfg, baseDir, name, newDef) : new Set();
68
+ if (oldDef.vectors !== newDef.vectors) {
69
+ for (const p of oldMatch)forced.add(p);
70
+ for (const p of newMatch)forced.add(p);
71
+ } else {
72
+ for (const p of oldMatch)if (!newMatch.has(p)) forced.add(p);
73
+ for (const p of newMatch)if (!oldMatch.has(p)) forced.add(p);
74
+ }
75
+ }
76
+ return forced;
77
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/preset-scope.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { presetHasSignal } from '../config/index.ts';\nimport { listFiles } from '../scan/index.ts';\n\n// A signature change open() can route to a narrow reparse instead of a full clear: every changed\n// segment is a preset's own (glob or vector on/off), never a global feature or embed change.\nexport function isPresetOnlyChange(changedKeys: ReadonlySet<string>): boolean {\n return changedKeys.size > 0 && [...changedKeys].every((key) => key.startsWith('preset:'));\n}\n\ninterface PresetDef {\n include: string[];\n exclude: string[];\n vectors: boolean;\n}\n\n// One preset's old include/exclude/vectors-on-off, read back out of a stored `preset:<name>:...`\n// segment (config/access.ts's featureSignature format). Absent (a brand-new preset) is the empty\n// preset, not a parse failure; a segment present but not in that exact shape returns null so the\n// caller falls back to a full rebuild instead of guessing.\nfunction parseOldPreset(before: string, name: string): PresetDef | null {\n const part = before.split('|').find((p) => p.startsWith(`preset:${name}:`));\n if (part === undefined) return { include: [], exclude: [], vectors: false };\n const fields = part.split(':');\n if (fields.length !== 5) return null;\n const [, , includeStr, excludeStr, onOff] = fields;\n if (onOff !== 'on' && onOff !== 'off') return null;\n return { include: includeStr === '' ? [] : includeStr.split('+'), exclude: excludeStr === '' ? [] : excludeStr.split('+'), vectors: onOff === 'on' };\n}\n\n// Paths one preset definition covers, via the real listFiles so stat filtering (directories,\n// dangling symlinks) matches production coverage exactly.\nfunction matchPreset(cfg: Config, baseDir: string, name: string, def: PresetDef): Set<string> {\n const scoped: Config = { ...cfg, presets: { [name]: { include: def.include, exclude: def.exclude } } };\n return new Set(listFiles(scoped, baseDir).map((f) => f.relPath));\n}\n\n// Every path whose coverage by a changed preset genuinely moved: gained or lost it (a glob edit),\n// or kept it under a flipped vectors on/off (that preset's whole match set, since its contribution\n// to the file's union embed flag may have changed even though membership itself did not). Returns\n// null when a changed preset's stored segment can't be parsed, so the caller takes the full rebuild.\nexport function forcedPresetPaths(cfg: Config, baseDir: string, before: string, changedKeys: ReadonlySet<string>): Set<string> | null {\n const forced = new Set<string>();\n for (const key of changedKeys) {\n const name = key.slice('preset:'.length);\n const oldDef = parseOldPreset(before, name);\n if (oldDef === null) return null;\n const stillDeclared = Object.hasOwn(cfg.presets, name);\n const newDef: PresetDef = stillDeclared ? { include: cfg.presets[name].include, exclude: cfg.presets[name].exclude ?? [], vectors: presetHasSignal(cfg, name, 'vectors') } : { include: [], exclude: [], vectors: false };\n\n const oldMatch = matchPreset(cfg, baseDir, name, oldDef);\n const newMatch = stillDeclared ? matchPreset(cfg, baseDir, name, newDef) : new Set<string>();\n\n if (oldDef.vectors !== newDef.vectors) {\n for (const p of oldMatch) forced.add(p);\n for (const p of newMatch) forced.add(p);\n } else {\n for (const p of oldMatch) if (!newMatch.has(p)) forced.add(p);\n for (const p of newMatch) if (!oldMatch.has(p)) forced.add(p);\n }\n }\n return forced;\n}\n"],"names":["presetHasSignal","listFiles","isPresetOnlyChange","changedKeys","size","every","key","startsWith","parseOldPreset","before","name","part","split","find","p","undefined","include","exclude","vectors","fields","length","includeStr","excludeStr","onOff","matchPreset","cfg","baseDir","def","scoped","presets","Set","map","f","relPath","forcedPresetPaths","forced","slice","oldDef","stillDeclared","Object","hasOwn","newDef","oldMatch","newMatch","add","has"],"mappings":"AACA,SAASA,eAAe,QAAQ,qBAAqB;AACrD,SAASC,SAAS,QAAQ,mBAAmB;AAE7C,iGAAiG;AACjG,6FAA6F;AAC7F,OAAO,SAASC,mBAAmBC,WAAgC;IACjE,OAAOA,YAAYC,IAAI,GAAG,KAAK;WAAID;KAAY,CAACE,KAAK,CAAC,CAACC,MAAQA,IAAIC,UAAU,CAAC;AAChF;AAQA,iGAAiG;AACjG,iGAAiG;AACjG,iGAAiG;AACjG,2DAA2D;AAC3D,SAASC,eAAeC,MAAc,EAAEC,IAAY;IAClD,MAAMC,OAAOF,OAAOG,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,IAAMA,EAAEP,UAAU,CAAC,CAAC,OAAO,EAAEG,KAAK,CAAC,CAAC;IACzE,IAAIC,SAASI,WAAW,OAAO;QAAEC,SAAS,EAAE;QAAEC,SAAS,EAAE;QAAEC,SAAS;IAAM;IAC1E,MAAMC,SAASR,KAAKC,KAAK,CAAC;IAC1B,IAAIO,OAAOC,MAAM,KAAK,GAAG,OAAO;IAChC,MAAM,KAAKC,YAAYC,YAAYC,MAAM,GAAGJ;IAC5C,IAAII,UAAU,QAAQA,UAAU,OAAO,OAAO;IAC9C,OAAO;QAAEP,SAASK,eAAe,KAAK,EAAE,GAAGA,WAAWT,KAAK,CAAC;QAAMK,SAASK,eAAe,KAAK,EAAE,GAAGA,WAAWV,KAAK,CAAC;QAAMM,SAASK,UAAU;IAAK;AACrJ;AAEA,6FAA6F;AAC7F,0DAA0D;AAC1D,SAASC,YAAYC,GAAW,EAAEC,OAAe,EAAEhB,IAAY,EAAEiB,GAAc;IAC7E,MAAMC,SAAiB;QAAE,GAAGH,GAAG;QAAEI,SAAS;YAAE,CAACnB,KAAK,EAAE;gBAAEM,SAASW,IAAIX,OAAO;gBAAEC,SAASU,IAAIV,OAAO;YAAC;QAAE;IAAE;IACrG,OAAO,IAAIa,IAAI7B,UAAU2B,QAAQF,SAASK,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO;AAChE;AAEA,kGAAkG;AAClG,mGAAmG;AACnG,kGAAkG;AAClG,qGAAqG;AACrG,OAAO,SAASC,kBAAkBT,GAAW,EAAEC,OAAe,EAAEjB,MAAc,EAAEN,WAAgC;IAC9G,MAAMgC,SAAS,IAAIL;IACnB,KAAK,MAAMxB,OAAOH,YAAa;YAK4DsB;QAJzF,MAAMf,OAAOJ,IAAI8B,KAAK,CAAC,UAAUhB,MAAM;QACvC,MAAMiB,SAAS7B,eAAeC,QAAQC;QACtC,IAAI2B,WAAW,MAAM,OAAO;QAC5B,MAAMC,gBAAgBC,OAAOC,MAAM,CAACf,IAAII,OAAO,EAAEnB;QACjD,MAAM+B,SAAoBH,gBAAgB;YAAEtB,SAASS,IAAII,OAAO,CAACnB,KAAK,CAACM,OAAO;YAAEC,OAAO,GAAEQ,4BAAAA,IAAII,OAAO,CAACnB,KAAK,CAACO,OAAO,cAAzBQ,uCAAAA,4BAA6B,EAAE;YAAEP,SAASlB,gBAAgByB,KAAKf,MAAM;QAAW,IAAI;YAAEM,SAAS,EAAE;YAAEC,SAAS,EAAE;YAAEC,SAAS;QAAM;QAExN,MAAMwB,WAAWlB,YAAYC,KAAKC,SAAShB,MAAM2B;QACjD,MAAMM,WAAWL,gBAAgBd,YAAYC,KAAKC,SAAShB,MAAM+B,UAAU,IAAIX;QAE/E,IAAIO,OAAOnB,OAAO,KAAKuB,OAAOvB,OAAO,EAAE;YACrC,KAAK,MAAMJ,KAAK4B,SAAUP,OAAOS,GAAG,CAAC9B;YACrC,KAAK,MAAMA,KAAK6B,SAAUR,OAAOS,GAAG,CAAC9B;QACvC,OAAO;YACL,KAAK,MAAMA,KAAK4B,SAAU,IAAI,CAACC,SAASE,GAAG,CAAC/B,IAAIqB,OAAOS,GAAG,CAAC9B;YAC3D,KAAK,MAAMA,KAAK6B,SAAU,IAAI,CAACD,SAASG,GAAG,CAAC/B,IAAIqB,OAAOS,GAAG,CAAC9B;QAC7D;IACF;IACA,OAAOqB;AACT"}
@@ -1,6 +1,7 @@
1
1
  import type { Config } from '../config/index.js';
2
+ import type { ParsePool } from '../scan/pool.js';
2
3
  import type { Connection, ReconcileDialect } from './types.js';
3
- export declare function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect): Promise<{
4
+ export declare function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect, pool?: ParsePool, forcedPaths?: ReadonlySet<string>): Promise<{
4
5
  parsed: number;
5
6
  warnings: string[];
6
7
  }>;
@@ -18,7 +18,7 @@ const CORE_FRONTMATTER_COLUMNS = new Set([
18
18
  '_size',
19
19
  '_parse_error'
20
20
  ]);
21
- export async function reconcile(conn, cfg, baseDir, dialect) {
21
+ export async function reconcile(conn, cfg, baseDir, dialect, pool, forcedPaths) {
22
22
  const files = listFiles(cfg, baseDir);
23
23
  const currentSet = new Set(files.map((f)=>f.relPath));
24
24
  const existingStmt = await conn.prepare('SELECT "path", "_mtime", "_size" FROM frontmatter');
@@ -27,10 +27,16 @@ export async function reconcile(conn, cfg, baseDir, dialect) {
27
27
  r.path,
28
28
  r
29
29
  ]));
30
+ // A path whose coverage moved between presets (forcedPaths) but is no longer covered at all is
31
+ // already caught below by !currentSet.has, since it can only be forced by having existed under
32
+ // an old preset's match, which means it was reconciled into `existing` already.
30
33
  const vanished = existingRows.filter((r)=>!currentSet.has(r.path)).map((r)=>r.path);
34
+ // forcedPaths treats an unchanged file as touched because its preset coverage moved, not its
35
+ // stamp -- reconcile still owns add/update/remove and every cross-feature cascade for it.
31
36
  const toReparse = files.filter((f)=>{
37
+ var _ref;
32
38
  const row = existing.get(f.relPath);
33
- return !row || row._mtime !== f.mtimeMs || row._size !== f.size;
39
+ return !row || row._mtime !== f.mtimeMs || row._size !== f.size || ((_ref = forcedPaths === null || forcedPaths === void 0 ? void 0 : forcedPaths.has(f.relPath)) !== null && _ref !== void 0 ? _ref : false);
34
40
  });
35
41
  if (vanished.length === 0 && toReparse.length === 0) return {
36
42
  parsed: 0,
@@ -41,7 +47,9 @@ export async function reconcile(conn, cfg, baseDir, dialect) {
41
47
  // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short
42
48
  // reconciles stay silent (progress() has a threshold).
43
49
  const report = progress('reparsing files', toReparse.length);
44
- const { docs: parsedDocs, warnings, newColumns } = await reparseFiles(toReparse, features, cfg, seenColumns, report.tick);
50
+ const { docs: parsedDocs, warnings, newColumns } = await reparseFiles(toReparse, features, cfg, seenColumns, report.tick, {
51
+ pool
52
+ });
45
53
  report.finish();
46
54
  for (const col of newColumns)seenColumns.add(col);
47
55
  const allColumns = [
@@ -77,7 +85,23 @@ export async function reconcile(conn, cfg, baseDir, dialect) {
77
85
  // Re-read inside the write transaction: newColumns came from a read taken before it opened, so a
78
86
  // concurrent reconcile may have added some of them since. ALTER has no IF NOT EXISTS.
79
87
  const present = await getColumns(conn);
80
- for (const col of newColumns)if (!present.has(col)) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}${dialect.columnTypeSuffix()}`);
88
+ const missingColumns = newColumns.filter((col)=>!present.has(col));
89
+ if (missingColumns.length > 0) await dialect.addColumns(conn, missingColumns);
90
+ // Revalidated once the lock is held, before this process's own frontmatter upsert below:
91
+ // `added` came from a path read taken before this transaction's lock, so a path still
92
+ // called "added" here may already have a content row from a concurrent reconcile that
93
+ // committed while this one waited. One SELECT, not a DELETE per added file -- with no
94
+ // contention it returns the same set `existing` already ruled out, so nothing extra clears.
95
+ let contentTouched = touched;
96
+ if (added.length > 0) {
97
+ const currentPathsStmt = await conn.prepare('SELECT "path" FROM frontmatter');
98
+ const currentPaths = new Set((await currentPathsStmt.all()).map((r)=>r.path));
99
+ const staleAdded = added.filter((p)=>currentPaths.has(p));
100
+ if (staleAdded.length > 0) contentTouched = [
101
+ ...touched,
102
+ ...staleAdded
103
+ ];
104
+ }
81
105
  if (parsedDocs.length > 0) {
82
106
  const rows = parsedDocs.map((doc)=>writableColumns.map((col)=>{
83
107
  var _doc_data_col;
@@ -93,7 +117,7 @@ export async function reconcile(conn, cfg, baseDir, dialect) {
93
117
  }
94
118
  // After the upsert, so every doc already has the frontmatter row sqlite's content rowid
95
119
  // couples to. ON CONFLICT DO UPDATE preserves that rowid, so a reparse keeps its identity.
96
- await dialect.reconcileContent(conn, touched, parsedDocs, delta, cfg);
120
+ await dialect.reconcileContent(conn, contentTouched, parsedDocs, delta, cfg);
97
121
  // A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;
98
122
  // new docs have nothing to clear, which keeps cold builds linear.
99
123
  if (touched.length > 0) await conn.runBatch('DELETE FROM preset_files WHERE "path" = ?', touched.map((p)=>[
@@ -104,7 +128,10 @@ export async function reconcile(conn, cfg, baseDir, dialect) {
104
128
  doc.relPath,
105
129
  presetName
106
130
  ]);
107
- if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files ("path", preset) VALUES (?, ?)', presetRows);
131
+ // DO NOTHING, not a bare INSERT: the added/touched split above comes from a read taken
132
+ // before this transaction's lock, so a path this process calls "added" can already have
133
+ // its (path, preset) row committed by a concurrent reconcile -- the row would be identical either way.
134
+ if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files ("path", preset) VALUES (?, ?) ON CONFLICT("path", preset) DO NOTHING', presetRows);
108
135
  // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's
109
136
  // node set, so a lingering vanished row would dilute rank mass across every surviving note.
110
137
  if (vanished.length > 0) await conn.runBatch('DELETE FROM frontmatter WHERE "path" = ?', vanished.map((p)=>[
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/reconcile.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport type { ExtractedDoc, ReconcileDelta } from '../features/types.ts';\nimport { progress } from '../output/progress.ts';\nimport { listFiles, RESERVED_COLUMNS } from '../scan/index.ts';\nimport { reparseFiles } from '../scan/reparse.ts';\nimport { getColumns, quoteIdent } from './shared.ts';\nimport { withTransaction } from './transaction.ts';\nimport type { Connection, ReconcileDialect } from './types.ts';\n\n// One reconcile algorithm shared by every store, parameterised by a per-engine ReconcileDialect\n// (types.ts). Ordering is universal, not a dialect concern: ALTER, then the frontmatter upsert,\n// then reconcileContent, then preset_files, then the vanished-frontmatter delete, then feature hooks\n// -- a vanished path's content delete (inside reconcileContent) must precede its frontmatter\n// delete, since sqlite's delete SQL resolves the row via its frontmatter rowid.\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nconst CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\nexport async function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect): Promise<{ parsed: number; warnings: string[] }> {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingStmt = await conn.prepare('SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter');\n const existingRows = (await existingStmt.all()) as Array<{ path: string; _mtime: number; _size: number }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const features = activeFeatures(cfg);\n const seenColumns = await getColumns(conn);\n\n // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short\n // reconciles stay silent (progress() has a threshold).\n const report = progress('reparsing files', toReparse.length);\n const { docs: parsedDocs, warnings, newColumns } = await reparseFiles(toReparse, features, cfg, seenColumns, report.tick);\n report.finish();\n for (const col of newColumns) seenColumns.add(col);\n\n const allColumns = [...seenColumns];\n // Fence before ALTERing: a store's own failure past this point is a raw, engine-specific\n // error with no indication of the boundary or the levers -- dialect.checkColumnLimit names both.\n dialect.checkColumnLimit(allColumns.length);\n // Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a\n // feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).\n const writableColumns = allColumns.filter((c) => CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));\n // ON CONFLICT DO UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --\n // sqlite's content rows are coupled to that rowid.\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')}) ON CONFLICT(\"path\") DO UPDATE SET ${writableColumns\n .filter((c) => c !== 'path')\n .map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`)\n .join(', ')}`;\n\n const added = toReparse.filter((f) => !existing.has(f.relPath)).map((f) => f.relPath);\n const delta: ReconcileDelta = { files, reparsed: parsedDocs.map((d) => d.relPath), added, vanished };\n const addedSet = new Set(added);\n const reparsedExisting = parsedDocs.map((d) => d.relPath).filter((p) => !addedSet.has(p));\n // Paths whose content (and, per feature, other rows) need clearing: gone entirely, or about to\n // be reinserted fresh. Disjoint from `added`, which has nothing to clear.\n const touched = [...vanished, ...reparsedExisting];\n\n const txStart = Date.now();\n await withTransaction(\n conn,\n async () => {\n // Re-read inside the write transaction: newColumns came from a read taken before it opened, so a\n // concurrent reconcile may have added some of them since. ALTER has no IF NOT EXISTS.\n const present = await getColumns(conn);\n for (const col of newColumns) if (!present.has(col)) await conn.exec(`ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(col)}${dialect.columnTypeSuffix()}`);\n\n if (parsedDocs.length > 0) {\n const rows = parsedDocs.map((doc) =>\n writableColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\n return doc.data[col] ?? null;\n })\n );\n await conn.runBatch(insertSql, rows);\n }\n\n // After the upsert, so every doc already has the frontmatter row sqlite's content rowid\n // couples to. ON CONFLICT DO UPDATE preserves that rowid, so a reparse keeps its identity.\n await dialect.reconcileContent(conn, touched, parsedDocs, delta, cfg);\n\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;\n // new docs have nothing to clear, which keeps cold builds linear.\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM preset_files WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n const presetRows: unknown[][] = [];\n for (const doc of parsedDocs) for (const presetName of doc.presets) presetRows.push([doc.relPath, presetName]);\n if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files (\"path\", preset) VALUES (?, ?)', presetRows);\n\n // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's\n // node set, so a lingering vanished row would dilute rank mass across every surviving note.\n if (vanished.length > 0)\n await conn.runBatch(\n 'DELETE FROM frontmatter WHERE \"path\" = ?',\n vanished.map((p) => [p])\n );\n\n if (touched.length > 0) for (const feature of features) await feature.remove?.(conn, touched, delta);\n for (const feature of features) {\n const docsForFeature: ExtractedDoc[] = parsedDocs.map((doc) => ({ path: doc.relPath, extracted: doc.extracted[feature.name] }));\n await feature.store?.(conn, docsForFeature, delta);\n }\n for (const feature of features) await feature.afterReconcile?.(conn, delta);\n },\n dialect.beginMode()\n );\n\n const durationMs = Date.now() - txStart;\n if (dialect.recordDuration) await dialect.recordDuration(conn, durationMs);\n\n return { parsed: parsedDocs.length, warnings };\n}\n"],"names":["activeFeatures","progress","listFiles","RESERVED_COLUMNS","reparseFiles","getColumns","quoteIdent","withTransaction","CORE_FRONTMATTER_COLUMNS","Set","reconcile","conn","cfg","baseDir","dialect","files","currentSet","map","f","relPath","existingStmt","prepare","existingRows","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","features","seenColumns","report","docs","parsedDocs","newColumns","tick","finish","col","add","allColumns","checkColumnLimit","writableColumns","c","insertSql","join","added","delta","reparsed","d","addedSet","reparsedExisting","p","touched","txStart","Date","now","feature","present","exec","columnTypeSuffix","rows","doc","ctimeMs","parseError","data","runBatch","reconcileContent","presetRows","presetName","presets","push","remove","docsForFeature","extracted","name","store","afterReconcile","beginMode","durationMs","recordDuration"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/reconcile.ts"],"sourcesContent":["import type { Config } from '../config/index.ts';\nimport { activeFeatures } from '../features/index.ts';\nimport type { ExtractedDoc, ReconcileDelta } from '../features/types.ts';\nimport { progress } from '../output/progress.ts';\nimport { listFiles, RESERVED_COLUMNS } from '../scan/index.ts';\nimport type { ParsePool } from '../scan/pool.ts';\nimport { reparseFiles } from '../scan/reparse.ts';\nimport { getColumns, quoteIdent } from './shared.ts';\nimport { withTransaction } from './transaction.ts';\nimport type { Connection, ReconcileDialect } from './types.ts';\n\n// One reconcile algorithm shared by every store, parameterised by a per-engine ReconcileDialect\n// (types.ts). Ordering is universal, not a dialect concern: ALTER, then the frontmatter upsert,\n// then reconcileContent, then preset_files, then the vanished-frontmatter delete, then feature hooks\n// -- a vanished path's content delete (inside reconcileContent) must precede its frontmatter\n// delete, since sqlite's delete SQL resolves the row via its frontmatter rowid.\n\n// Feature-owned columns (`_rank`) must stay out of the upsert: a reparse would null the last\n// computed value on every touch, not just the reconciles that recompute it.\nconst CORE_FRONTMATTER_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_parse_error']);\n\nexport async function reconcile(conn: Connection, cfg: Config, baseDir: string, dialect: ReconcileDialect, pool?: ParsePool, forcedPaths?: ReadonlySet<string>): Promise<{ parsed: number; warnings: string[] }> {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingStmt = await conn.prepare('SELECT \"path\", \"_mtime\", \"_size\" FROM frontmatter');\n const existingRows = (await existingStmt.all()) as Array<{ path: string; _mtime: number; _size: number }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n // A path whose coverage moved between presets (forcedPaths) but is no longer covered at all is\n // already caught below by !currentSet.has, since it can only be forced by having existed under\n // an old preset's match, which means it was reconciled into `existing` already.\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n // forcedPaths treats an unchanged file as touched because its preset coverage moved, not its\n // stamp -- reconcile still owns add/update/remove and every cross-feature cascade for it.\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size || (forcedPaths?.has(f.relPath) ?? false);\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const features = activeFeatures(cfg);\n const seenColumns = await getColumns(conn);\n\n // Bulk reparses (a sync, a cold build) are the long silences a query can hit; short\n // reconciles stay silent (progress() has a threshold).\n const report = progress('reparsing files', toReparse.length);\n const { docs: parsedDocs, warnings, newColumns } = await reparseFiles(toReparse, features, cfg, seenColumns, report.tick, { pool });\n report.finish();\n for (const col of newColumns) seenColumns.add(col);\n\n const allColumns = [...seenColumns];\n // Fence before ALTERing: a store's own failure past this point is a raw, engine-specific\n // error with no indication of the boundary or the levers -- dialect.checkColumnLimit names both.\n dialect.checkColumnLimit(allColumns.length);\n // Columns the frontmatter upsert actually writes: core + parsed frontmatter keys, never a\n // feature-owned reserved column (see CORE_FRONTMATTER_COLUMNS above).\n const writableColumns = allColumns.filter((c) => CORE_FRONTMATTER_COLUMNS.has(c) || !RESERVED_COLUMNS.has(c));\n // ON CONFLICT DO UPDATE (not OR REPLACE) keeps the row's rowid stable across reparses --\n // sqlite's content rows are coupled to that rowid.\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')}) ON CONFLICT(\"path\") DO UPDATE SET ${writableColumns\n .filter((c) => c !== 'path')\n .map((c) => `${quoteIdent(c)} = excluded.${quoteIdent(c)}`)\n .join(', ')}`;\n\n const added = toReparse.filter((f) => !existing.has(f.relPath)).map((f) => f.relPath);\n const delta: ReconcileDelta = { files, reparsed: parsedDocs.map((d) => d.relPath), added, vanished };\n const addedSet = new Set(added);\n const reparsedExisting = parsedDocs.map((d) => d.relPath).filter((p) => !addedSet.has(p));\n // Paths whose content (and, per feature, other rows) need clearing: gone entirely, or about to\n // be reinserted fresh. Disjoint from `added`, which has nothing to clear.\n const touched = [...vanished, ...reparsedExisting];\n\n const txStart = Date.now();\n await withTransaction(\n conn,\n async () => {\n // Re-read inside the write transaction: newColumns came from a read taken before it opened, so a\n // concurrent reconcile may have added some of them since. ALTER has no IF NOT EXISTS.\n const present = await getColumns(conn);\n const missingColumns = newColumns.filter((col) => !present.has(col));\n if (missingColumns.length > 0) await dialect.addColumns(conn, missingColumns);\n\n // Revalidated once the lock is held, before this process's own frontmatter upsert below:\n // `added` came from a path read taken before this transaction's lock, so a path still\n // called \"added\" here may already have a content row from a concurrent reconcile that\n // committed while this one waited. One SELECT, not a DELETE per added file -- with no\n // contention it returns the same set `existing` already ruled out, so nothing extra clears.\n let contentTouched = touched;\n if (added.length > 0) {\n const currentPathsStmt = await conn.prepare('SELECT \"path\" FROM frontmatter');\n const currentPaths = new Set(((await currentPathsStmt.all()) as Array<{ path: string }>).map((r) => r.path));\n const staleAdded = added.filter((p) => currentPaths.has(p));\n if (staleAdded.length > 0) contentTouched = [...touched, ...staleAdded];\n }\n\n if (parsedDocs.length > 0) {\n const rows = parsedDocs.map((doc) =>\n writableColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_ctime') return doc.ctimeMs;\n if (col === '_size') return doc.size;\n // Written per parse, unlike _rank, which a feature pass owns and the upsert skips.\n if (col === '_parse_error') return doc.parseError;\n return doc.data[col] ?? null;\n })\n );\n await conn.runBatch(insertSql, rows);\n }\n\n // After the upsert, so every doc already has the frontmatter row sqlite's content rowid\n // couples to. ON CONFLICT DO UPDATE preserves that rowid, so a reparse keeps its identity.\n await dialect.reconcileContent(conn, contentTouched, parsedDocs, delta, cfg);\n\n // A preset edit forces a full rebuild, so an unchanged doc's coverage is already correct;\n // new docs have nothing to clear, which keeps cold builds linear.\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM preset_files WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n const presetRows: unknown[][] = [];\n for (const doc of parsedDocs) for (const presetName of doc.presets) presetRows.push([doc.relPath, presetName]);\n // DO NOTHING, not a bare INSERT: the added/touched split above comes from a read taken\n // before this transaction's lock, so a path this process calls \"added\" can already have\n // its (path, preset) row committed by a concurrent reconcile -- the row would be identical either way.\n if (presetRows.length > 0) await conn.runBatch('INSERT INTO preset_files (\"path\", preset) VALUES (?, ?) ON CONFLICT(\"path\", preset) DO NOTHING', presetRows);\n\n // Before the feature hooks, never after: rank's afterReconcile reads frontmatter as PageRank's\n // node set, so a lingering vanished row would dilute rank mass across every surviving note.\n if (vanished.length > 0)\n await conn.runBatch(\n 'DELETE FROM frontmatter WHERE \"path\" = ?',\n vanished.map((p) => [p])\n );\n\n if (touched.length > 0) for (const feature of features) await feature.remove?.(conn, touched, delta);\n for (const feature of features) {\n const docsForFeature: ExtractedDoc[] = parsedDocs.map((doc) => ({ path: doc.relPath, extracted: doc.extracted[feature.name] }));\n await feature.store?.(conn, docsForFeature, delta);\n }\n for (const feature of features) await feature.afterReconcile?.(conn, delta);\n },\n dialect.beginMode()\n );\n\n const durationMs = Date.now() - txStart;\n if (dialect.recordDuration) await dialect.recordDuration(conn, durationMs);\n\n return { parsed: parsedDocs.length, warnings };\n}\n"],"names":["activeFeatures","progress","listFiles","RESERVED_COLUMNS","reparseFiles","getColumns","quoteIdent","withTransaction","CORE_FRONTMATTER_COLUMNS","Set","reconcile","conn","cfg","baseDir","dialect","pool","forcedPaths","files","currentSet","map","f","relPath","existingStmt","prepare","existingRows","all","existing","Map","r","path","vanished","filter","has","toReparse","row","get","_mtime","mtimeMs","_size","size","length","parsed","warnings","features","seenColumns","report","docs","parsedDocs","newColumns","tick","finish","col","add","allColumns","checkColumnLimit","writableColumns","c","insertSql","join","added","delta","reparsed","d","addedSet","reparsedExisting","p","touched","txStart","Date","now","feature","present","missingColumns","addColumns","contentTouched","currentPathsStmt","currentPaths","staleAdded","rows","doc","ctimeMs","parseError","data","runBatch","reconcileContent","presetRows","presetName","presets","push","remove","docsForFeature","extracted","name","store","afterReconcile","beginMode","durationMs","recordDuration"],"mappings":"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"}
@@ -1,6 +1,3 @@
1
- import type { Config } from '../../config/index.js';
2
- import type { Connection } from '../types.js';
3
1
  export declare function changedSignatureKeys(before: string, after: string): Set<string>;
4
2
  export declare function embedIdentityAdopted(before: string, after: string): boolean;
5
3
  export declare function signatureDiff(before: string, after: string): string;
6
- export declare function rebuildContentTable(conn: Connection, cfg: Config, baseDir: string): Promise<string[]>;
@@ -0,0 +1,37 @@
1
+ // Feature-signature comparison for open()'s rebuild-vs-adopt decision (config.featureSignature's
2
+ // format: one `feature:<name>:on|off` segment per feature, embed provider, then one segment per
3
+ // preset). Pure string comparison, no SQL, shared by every store's open dialect.
4
+ // Segment keys that moved between two feature signatures. `preset:` and `feature:` segments key
5
+ // on their first two colon-separated parts (e.g. `preset:default`, `feature:tags`) so a change to
6
+ // one preset or one feature is independently detectable; every other segment keys on its prefix alone.
7
+ export function changedSignatureKeys(before, after) {
8
+ const keyOf = (part)=>part.startsWith('preset:') || part.startsWith('feature:') ? part.split(':').slice(0, 2).join(':') : part.split(':')[0];
9
+ const parse = (sig)=>new Map(sig.split('|').map((part)=>[
10
+ keyOf(part),
11
+ part
12
+ ]));
13
+ const a = parse(before);
14
+ const b = parse(after);
15
+ const changed = new Set();
16
+ for (const [key, val] of b)if (a.get(key) !== val) changed.add(key);
17
+ for (const key of a.keys())if (!b.has(key)) changed.add(key);
18
+ return changed;
19
+ }
20
+ // Whether the embed segment only gained its resolved weight identity: same provider and
21
+ // model, no identity recorded before, one now -- adopted into meta without a rebuild.
22
+ export function embedIdentityAdopted(before, after) {
23
+ const embedPart = (sig)=>sig.split('|').find((p)=>p.startsWith('embed:'));
24
+ const b = embedPart(before);
25
+ const a = embedPart(after);
26
+ if (b === undefined || a === undefined) return false;
27
+ const at = a.indexOf('@');
28
+ return b.indexOf('@') === -1 && at !== -1 && a.slice(0, at) === b;
29
+ }
30
+ // Names what moved, for the rebuild notice.
31
+ export function signatureDiff(before, after) {
32
+ const changed = changedSignatureKeys(before, after);
33
+ const label = (key)=>key === 'embed' ? 'embed settings' : key.startsWith('preset:') ? `preset "${key.slice(7)}"` : key.startsWith('feature:') ? `feature "${key.slice(8)}"` : 'features';
34
+ return changed.size === 0 ? 'features' : [
35
+ ...changed
36
+ ].map(label).join(', ');
37
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/signature.ts"],"sourcesContent":["// Feature-signature comparison for open()'s rebuild-vs-adopt decision (config.featureSignature's\n// format: one `feature:<name>:on|off` segment per feature, embed provider, then one segment per\n// preset). Pure string comparison, no SQL, shared by every store's open dialect.\n\n// Segment keys that moved between two feature signatures. `preset:` and `feature:` segments key\n// on their first two colon-separated parts (e.g. `preset:default`, `feature:tags`) so a change to\n// one preset or one feature is independently detectable; every other segment keys on its prefix alone.\nexport function changedSignatureKeys(before: string, after: string): Set<string> {\n const keyOf = (part: string) => (part.startsWith('preset:') || part.startsWith('feature:') ? part.split(':').slice(0, 2).join(':') : part.split(':')[0]);\n const parse = (sig: string) => new Map(sig.split('|').map((part) => [keyOf(part), part]));\n const a = parse(before);\n const b = parse(after);\n const changed = new Set<string>();\n for (const [key, val] of b) if (a.get(key) !== val) changed.add(key);\n for (const key of a.keys()) if (!b.has(key)) changed.add(key);\n return changed;\n}\n\n// Whether the embed segment only gained its resolved weight identity: same provider and\n// model, no identity recorded before, one now -- adopted into meta without a rebuild.\nexport function embedIdentityAdopted(before: string, after: string): boolean {\n const embedPart = (sig: string) => sig.split('|').find((p) => p.startsWith('embed:'));\n const b = embedPart(before);\n const a = embedPart(after);\n if (b === undefined || a === undefined) return false;\n const at = a.indexOf('@');\n return b.indexOf('@') === -1 && at !== -1 && a.slice(0, at) === b;\n}\n\n// Names what moved, for the rebuild notice.\nexport function signatureDiff(before: string, after: string): string {\n const changed = changedSignatureKeys(before, after);\n const label = (key: string) => (key === 'embed' ? 'embed settings' : key.startsWith('preset:') ? `preset \"${key.slice(7)}\"` : key.startsWith('feature:') ? `feature \"${key.slice(8)}\"` : 'features');\n return changed.size === 0 ? 'features' : [...changed].map(label).join(', ');\n}\n"],"names":["changedSignatureKeys","before","after","keyOf","part","startsWith","split","slice","join","parse","sig","Map","map","a","b","changed","Set","key","val","get","add","keys","has","embedIdentityAdopted","embedPart","find","p","undefined","at","indexOf","signatureDiff","label","size"],"mappings":"AAAA,iGAAiG;AACjG,gGAAgG;AAChG,iFAAiF;AAEjF,gGAAgG;AAChG,kGAAkG;AAClG,uGAAuG;AACvG,OAAO,SAASA,qBAAqBC,MAAc,EAAEC,KAAa;IAChE,MAAMC,QAAQ,CAACC,OAAkBA,KAAKC,UAAU,CAAC,cAAcD,KAAKC,UAAU,CAAC,cAAcD,KAAKE,KAAK,CAAC,KAAKC,KAAK,CAAC,GAAG,GAAGC,IAAI,CAAC,OAAOJ,KAAKE,KAAK,CAAC,IAAI,CAAC,EAAE;IACvJ,MAAMG,QAAQ,CAACC,MAAgB,IAAIC,IAAID,IAAIJ,KAAK,CAAC,KAAKM,GAAG,CAAC,CAACR,OAAS;gBAACD,MAAMC;gBAAOA;aAAK;IACvF,MAAMS,IAAIJ,MAAMR;IAChB,MAAMa,IAAIL,MAAMP;IAChB,MAAMa,UAAU,IAAIC;IACpB,KAAK,MAAM,CAACC,KAAKC,IAAI,IAAIJ,EAAG,IAAID,EAAEM,GAAG,CAACF,SAASC,KAAKH,QAAQK,GAAG,CAACH;IAChE,KAAK,MAAMA,OAAOJ,EAAEQ,IAAI,GAAI,IAAI,CAACP,EAAEQ,GAAG,CAACL,MAAMF,QAAQK,GAAG,CAACH;IACzD,OAAOF;AACT;AAEA,wFAAwF;AACxF,sFAAsF;AACtF,OAAO,SAASQ,qBAAqBtB,MAAc,EAAEC,KAAa;IAChE,MAAMsB,YAAY,CAACd,MAAgBA,IAAIJ,KAAK,CAAC,KAAKmB,IAAI,CAAC,CAACC,IAAMA,EAAErB,UAAU,CAAC;IAC3E,MAAMS,IAAIU,UAAUvB;IACpB,MAAMY,IAAIW,UAAUtB;IACpB,IAAIY,MAAMa,aAAad,MAAMc,WAAW,OAAO;IAC/C,MAAMC,KAAKf,EAAEgB,OAAO,CAAC;IACrB,OAAOf,EAAEe,OAAO,CAAC,SAAS,CAAC,KAAKD,OAAO,CAAC,KAAKf,EAAEN,KAAK,CAAC,GAAGqB,QAAQd;AAClE;AAEA,4CAA4C;AAC5C,OAAO,SAASgB,cAAc7B,MAAc,EAAEC,KAAa;IACzD,MAAMa,UAAUf,qBAAqBC,QAAQC;IAC7C,MAAM6B,QAAQ,CAACd,MAAiBA,QAAQ,UAAU,mBAAmBA,IAAIZ,UAAU,CAAC,aAAa,CAAC,QAAQ,EAAEY,IAAIV,KAAK,CAAC,GAAG,CAAC,CAAC,GAAGU,IAAIZ,UAAU,CAAC,cAAc,CAAC,SAAS,EAAEY,IAAIV,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG;IACzL,OAAOQ,QAAQiB,IAAI,KAAK,IAAI,aAAa;WAAIjB;KAAQ,CAACH,GAAG,CAACmB,OAAOvB,IAAI,CAAC;AACxE"}
@@ -1,2 +1,2 @@
1
1
  import type { Connection, LexicalHit, LexicalQueryOptions } from '../types.js';
2
- export declare function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions, segmenting: boolean): Promise<LexicalHit[]>;
2
+ export declare function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]>;
@@ -3,11 +3,11 @@ import { segmentMatch } from '../../text/segment.js';
3
3
  // (benchmark/reports/2026-08-23-hub-release-battery.md); past this bound, hit stays NULL for the caller's JS excerpt fallback.
4
4
  const SNIPPET_BOUND = 16384;
5
5
  const WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)';
6
- // Ranked BM25 word-match query with excerpt, scoped by the caller-built SQL fragments. `segmenting`
7
- // mirrors the predicate that populated the `_seg` sidecars (contentTokenize(cfg) === undefined): an unspaced-script run in `terms` becomes a quoted grapheme phrase against them only when they exist.
8
- export async function queryLexical(conn, terms, opts, segmenting) {
6
+ // Ranked BM25 word-match query with excerpt, scoped by the caller-built SQL fragments. An
7
+ // unspaced-script run in `terms` becomes a quoted grapheme phrase against the `_seg` sidecars.
8
+ export async function queryLexical(conn, terms, opts) {
9
9
  const { whereJoin, whereCond, scopeCond, limit } = opts;
10
- const query = segmenting ? segmentMatch(terms) : terms;
10
+ const query = segmentMatch(terms);
11
11
  // A run of pure unspaced-script punctuation segments to nothing searchable: an empty MATCH
12
12
  // string is an FTS5 syntax error, not zero rows, so return zero rows directly rather than issuing it.
13
13
  if (query.trim() === '') return [];
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/lexical.ts"],"sourcesContent":["import { segmentMatch } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\n// snippet() re-tokenizes each candidate doc, superlinearly: ~10s for one 1MB doc\n// (benchmark/reports/2026-08-23-hub-release-battery.md); past this bound, hit stays NULL for the caller's JS excerpt fallback.\nconst SNIPPET_BOUND = 16_384;\nconst WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)';\n\n// Ranked BM25 word-match query with excerpt, scoped by the caller-built SQL fragments. `segmenting`\n// mirrors the predicate that populated the `_seg` sidecars (contentTokenize(cfg) === undefined): an unspaced-script run in `terms` becomes a quoted grapheme phrase against them only when they exist.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions, segmenting: boolean): Promise<LexicalHit[]> {\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const query = segmenting ? segmentMatch(terms) : terms;\n // A run of pure unspaced-script punctuation segments to nothing searchable: an empty MATCH\n // string is an FTS5 syntax error, not zero rows, so return zero rows directly rather than issuing it.\n if (query.trim() === '') return [];\n const sql = `SELECT content.path AS path, CASE WHEN length(content.text) <= ${SNIPPET_BOUND} THEN snippet(content, 2, '«', '»', '…', 10) ELSE NULL END AS hit FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ${scopeCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${limit}`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(query)) as unknown as LexicalHit[];\n}\n"],"names":["segmentMatch","SNIPPET_BOUND","WEIGHTED_BM25","queryLexical","conn","terms","opts","segmenting","whereJoin","whereCond","scopeCond","limit","query","trim","sql","stmt","prepare","all"],"mappings":"AAAA,SAASA,YAAY,QAAQ,wBAAwB;AAGrD,iFAAiF;AACjF,+HAA+H;AAC/H,MAAMC,gBAAgB;AACtB,MAAMC,gBAAgB;AAEtB,oGAAoG;AACpG,uMAAuM;AACvM,OAAO,eAAeC,aAAaC,IAAgB,EAAEC,KAAa,EAAEC,IAAyB,EAAEC,UAAmB;IAChH,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGL;IACnD,MAAMM,QAAQL,aAAaP,aAAaK,SAASA;IACjD,2FAA2F;IAC3F,sGAAsG;IACtG,IAAIO,MAAMC,IAAI,OAAO,IAAI,OAAO,EAAE;IAClC,MAAMC,MAAM,CAAC,+DAA+D,EAAEb,cAAc,+EAA+E,EAAEO,UAAU,uBAAuB,EAAEC,UAAU,CAAC,EAAEC,UAAU,UAAU,EAAER,cAAc,OAAO,EAAES,OAAO;IACjR,MAAMI,OAAO,MAAMX,KAAKY,OAAO,CAACF;IAChC,OAAQ,MAAMC,KAAKE,GAAG,CAACL;AACzB"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/sqlite/lexical.ts"],"sourcesContent":["import { segmentMatch } from '../../text/segment.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\n// snippet() re-tokenizes each candidate doc, superlinearly: ~10s for one 1MB doc\n// (benchmark/reports/2026-08-23-hub-release-battery.md); past this bound, hit stays NULL for the caller's JS excerpt fallback.\nconst SNIPPET_BOUND = 16_384;\nconst WEIGHTED_BM25 = 'bm25(content, 10.0, 5.0, 1.0, 0, 10.0, 5.0, 1.0)';\n\n// Ranked BM25 word-match query with excerpt, scoped by the caller-built SQL fragments. An\n// unspaced-script run in `terms` becomes a quoted grapheme phrase against the `_seg` sidecars.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions): Promise<LexicalHit[]> {\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const query = segmentMatch(terms);\n // A run of pure unspaced-script punctuation segments to nothing searchable: an empty MATCH\n // string is an FTS5 syntax error, not zero rows, so return zero rows directly rather than issuing it.\n if (query.trim() === '') return [];\n const sql = `SELECT content.path AS path, CASE WHEN length(content.text) <= ${SNIPPET_BOUND} THEN snippet(content, 2, '«', '»', '…', 10) ELSE NULL END AS hit FROM content ${whereJoin} WHERE content MATCH ? ${whereCond} ${scopeCond} ORDER BY ${WEIGHTED_BM25} LIMIT ${limit}`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(query)) as unknown as LexicalHit[];\n}\n"],"names":["segmentMatch","SNIPPET_BOUND","WEIGHTED_BM25","queryLexical","conn","terms","opts","whereJoin","whereCond","scopeCond","limit","query","trim","sql","stmt","prepare","all"],"mappings":"AAAA,SAASA,YAAY,QAAQ,wBAAwB;AAGrD,iFAAiF;AACjF,+HAA+H;AAC/H,MAAMC,gBAAgB;AACtB,MAAMC,gBAAgB;AAEtB,0FAA0F;AAC1F,+FAA+F;AAC/F,OAAO,eAAeC,aAAaC,IAAgB,EAAEC,KAAa,EAAEC,IAAyB;IAC3F,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGJ;IACnD,MAAMK,QAAQX,aAAaK;IAC3B,2FAA2F;IAC3F,sGAAsG;IACtG,IAAIM,MAAMC,IAAI,OAAO,IAAI,OAAO,EAAE;IAClC,MAAMC,MAAM,CAAC,+DAA+D,EAAEZ,cAAc,+EAA+E,EAAEM,UAAU,uBAAuB,EAAEC,UAAU,CAAC,EAAEC,UAAU,UAAU,EAAEP,cAAc,OAAO,EAAEQ,OAAO;IACjR,MAAMI,OAAO,MAAMV,KAAKW,OAAO,CAACF;IAChC,OAAQ,MAAMC,KAAKE,GAAG,CAACL;AACzB"}