sensemaking 0.25.1 → 0.27.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 (332) hide show
  1. package/README.md +48 -12
  2. package/dist/cjs/cli/index.d.cts +3 -3
  3. package/dist/cjs/cli/index.d.ts +3 -3
  4. package/dist/cjs/cli/index.js +3 -3
  5. package/dist/cjs/cli/index.js.map +1 -1
  6. package/dist/cjs/cli/path.js +5 -29
  7. package/dist/cjs/cli/path.js.map +1 -1
  8. package/dist/cjs/cli/peek.js +43 -2
  9. package/dist/cjs/cli/peek.js.map +1 -1
  10. package/dist/cjs/cli/related.js +4 -3
  11. package/dist/cjs/cli/related.js.map +1 -1
  12. package/dist/cjs/cli/search.js +38 -6
  13. package/dist/cjs/cli/search.js.map +1 -1
  14. package/dist/cjs/commands/index.d.cts +6 -3
  15. package/dist/cjs/commands/index.d.ts +6 -3
  16. package/dist/cjs/commands/index.js +4 -0
  17. package/dist/cjs/commands/index.js.map +1 -1
  18. package/dist/cjs/commands/map.d.cts +14 -5
  19. package/dist/cjs/commands/map.d.ts +14 -5
  20. package/dist/cjs/commands/map.js +42 -35
  21. package/dist/cjs/commands/map.js.map +1 -1
  22. package/dist/cjs/commands/path.d.cts +14 -0
  23. package/dist/cjs/commands/path.d.ts +14 -0
  24. package/dist/cjs/commands/path.js +209 -0
  25. package/dist/cjs/commands/path.js.map +1 -0
  26. package/dist/cjs/commands/peek.d.cts +25 -2
  27. package/dist/cjs/commands/peek.d.ts +25 -2
  28. package/dist/cjs/commands/peek.js +131 -50
  29. package/dist/cjs/commands/peek.js.map +1 -1
  30. package/dist/cjs/commands/related.d.cts +16 -3
  31. package/dist/cjs/commands/related.d.ts +16 -3
  32. package/dist/cjs/commands/related.js +131 -98
  33. package/dist/cjs/commands/related.js.map +1 -1
  34. package/dist/cjs/commands/scope.d.cts +1 -0
  35. package/dist/cjs/commands/scope.d.ts +1 -0
  36. package/dist/cjs/commands/scope.js +149 -0
  37. package/dist/cjs/commands/scope.js.map +1 -1
  38. package/dist/cjs/commands/search.d.cts +30 -3
  39. package/dist/cjs/commands/search.d.ts +30 -3
  40. package/dist/cjs/commands/search.js +240 -73
  41. package/dist/cjs/commands/search.js.map +1 -1
  42. package/dist/cjs/commands/signals.d.cts +22 -7
  43. package/dist/cjs/commands/signals.d.ts +22 -7
  44. package/dist/cjs/commands/signals.js +77 -15
  45. package/dist/cjs/commands/signals.js.map +1 -1
  46. package/dist/cjs/commands/snapshot.d.cts +3 -0
  47. package/dist/cjs/commands/snapshot.d.ts +3 -0
  48. package/dist/cjs/commands/snapshot.js +180 -0
  49. package/dist/cjs/commands/snapshot.js.map +1 -0
  50. package/dist/cjs/config/load.js +6 -0
  51. package/dist/cjs/config/load.js.map +1 -1
  52. package/dist/cjs/embed/cohere.js +41 -16
  53. package/dist/cjs/embed/cohere.js.map +1 -1
  54. package/dist/cjs/embed/endpoint.d.cts +2 -0
  55. package/dist/cjs/embed/endpoint.d.ts +2 -0
  56. package/dist/cjs/embed/endpoint.js +30 -0
  57. package/dist/cjs/embed/endpoint.js.map +1 -0
  58. package/dist/cjs/embed/handoff.d.cts +7 -2
  59. package/dist/cjs/embed/handoff.d.ts +7 -2
  60. package/dist/cjs/embed/handoff.js +10 -0
  61. package/dist/cjs/embed/handoff.js.map +1 -1
  62. package/dist/cjs/embed/http.d.cts +18 -1
  63. package/dist/cjs/embed/http.d.ts +18 -1
  64. package/dist/cjs/embed/http.js +239 -40
  65. package/dist/cjs/embed/http.js.map +1 -1
  66. package/dist/cjs/embed/openai.js +39 -17
  67. package/dist/cjs/embed/openai.js.map +1 -1
  68. package/dist/cjs/embed/query.d.cts +9 -3
  69. package/dist/cjs/embed/query.d.ts +9 -3
  70. package/dist/cjs/embed/query.js +257 -90
  71. package/dist/cjs/embed/query.js.map +1 -1
  72. package/dist/cjs/embed/registry.d.cts +2 -2
  73. package/dist/cjs/embed/registry.d.ts +2 -2
  74. package/dist/cjs/embed/registry.js +36 -4
  75. package/dist/cjs/embed/registry.js.map +1 -1
  76. package/dist/cjs/embed/static.js +6 -2
  77. package/dist/cjs/embed/static.js.map +1 -1
  78. package/dist/cjs/embed/store.js +42 -77
  79. package/dist/cjs/embed/store.js.map +1 -1
  80. package/dist/cjs/embed/types.d.cts +5 -2
  81. package/dist/cjs/embed/types.d.ts +5 -2
  82. package/dist/cjs/embed/types.js.map +1 -1
  83. package/dist/cjs/features/embed.js +290 -43
  84. package/dist/cjs/features/embed.js.map +1 -1
  85. package/dist/cjs/graph/traverse.d.cts +2 -2
  86. package/dist/cjs/graph/traverse.d.ts +2 -2
  87. package/dist/cjs/graph/traverse.js +15 -5
  88. package/dist/cjs/graph/traverse.js.map +1 -1
  89. package/dist/cjs/index.d.cts +2 -2
  90. package/dist/cjs/index.d.ts +2 -2
  91. package/dist/cjs/index.js +6 -0
  92. package/dist/cjs/index.js.map +1 -1
  93. package/dist/cjs/output/output.d.cts +4 -36
  94. package/dist/cjs/output/output.d.ts +4 -36
  95. package/dist/cjs/output/output.js +1 -2
  96. package/dist/cjs/output/output.js.map +1 -1
  97. package/dist/cjs/scan/pool.d.cts +2 -1
  98. package/dist/cjs/scan/pool.d.ts +2 -1
  99. package/dist/cjs/scan/pool.js +116 -27
  100. package/dist/cjs/scan/pool.js.map +1 -1
  101. package/dist/cjs/scan/reparse.d.cts +1 -0
  102. package/dist/cjs/scan/reparse.d.ts +1 -0
  103. package/dist/cjs/scan/reparse.js +75 -21
  104. package/dist/cjs/scan/reparse.js.map +1 -1
  105. package/dist/cjs/store/builder.d.cts +11 -21
  106. package/dist/cjs/store/builder.d.ts +11 -21
  107. package/dist/cjs/store/builder.js +1077 -438
  108. package/dist/cjs/store/builder.js.map +1 -1
  109. package/dist/cjs/store/duckdb/connection.js +16 -2
  110. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  111. package/dist/cjs/store/duckdb/open.d.cts +1 -1
  112. package/dist/cjs/store/duckdb/open.d.ts +1 -1
  113. package/dist/cjs/store/duckdb/open.js +73 -65
  114. package/dist/cjs/store/duckdb/open.js.map +1 -1
  115. package/dist/cjs/store/duckdb/reconcile.js +533 -15
  116. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  117. package/dist/cjs/store/embed-scope.js +19 -9
  118. package/dist/cjs/store/embed-scope.js.map +1 -1
  119. package/dist/cjs/store/index.d.cts +6 -0
  120. package/dist/cjs/store/index.d.ts +6 -0
  121. package/dist/cjs/store/index.js +8 -4
  122. package/dist/cjs/store/index.js.map +1 -1
  123. package/dist/cjs/store/open.d.cts +6 -1
  124. package/dist/cjs/store/open.d.ts +6 -1
  125. package/dist/cjs/store/open.js +230 -307
  126. package/dist/cjs/store/open.js.map +1 -1
  127. package/dist/cjs/store/readiness.d.cts +5 -0
  128. package/dist/cjs/store/readiness.d.ts +5 -0
  129. package/dist/cjs/store/readiness.js +39 -0
  130. package/dist/cjs/store/readiness.js.map +1 -0
  131. package/dist/cjs/store/reconcile.d.cts +26 -1
  132. package/dist/cjs/store/reconcile.d.ts +26 -1
  133. package/dist/cjs/store/reconcile.js +670 -576
  134. package/dist/cjs/store/reconcile.js.map +1 -1
  135. package/dist/cjs/store/signature.d.cts +1 -0
  136. package/dist/cjs/store/signature.d.ts +1 -0
  137. package/dist/cjs/store/signature.js +9 -0
  138. package/dist/cjs/store/signature.js.map +1 -1
  139. package/dist/cjs/store/sqlite/open.d.cts +1 -1
  140. package/dist/cjs/store/sqlite/open.d.ts +1 -1
  141. package/dist/cjs/store/sqlite/open.js +84 -90
  142. package/dist/cjs/store/sqlite/open.js.map +1 -1
  143. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  144. package/dist/cjs/store/sqlite/store.js +63 -60
  145. package/dist/cjs/store/sqlite/store.js.map +1 -1
  146. package/dist/cjs/store/sqlite/vectors.d.cts +2 -2
  147. package/dist/cjs/store/sqlite/vectors.d.ts +2 -2
  148. package/dist/cjs/store/sqlite/vectors.js +8 -4
  149. package/dist/cjs/store/sqlite/vectors.js.map +1 -1
  150. package/dist/cjs/store/stages.js.map +1 -1
  151. package/dist/cjs/store/turso/connection.d.cts +1 -0
  152. package/dist/cjs/store/turso/connection.d.ts +1 -0
  153. package/dist/cjs/store/turso/connection.js +319 -94
  154. package/dist/cjs/store/turso/connection.js.map +1 -1
  155. package/dist/cjs/store/turso/open.d.cts +1 -1
  156. package/dist/cjs/store/turso/open.d.ts +1 -1
  157. package/dist/cjs/store/turso/open.js +70 -74
  158. package/dist/cjs/store/turso/open.js.map +1 -1
  159. package/dist/cjs/store/turso/store.js +25 -23
  160. package/dist/cjs/store/turso/store.js.map +1 -1
  161. package/dist/cjs/store/turso/vectors.d.cts +2 -2
  162. package/dist/cjs/store/turso/vectors.d.ts +2 -2
  163. package/dist/cjs/store/turso/vectors.js +26 -16
  164. package/dist/cjs/store/turso/vectors.js.map +1 -1
  165. package/dist/cjs/store/types.d.cts +4 -1
  166. package/dist/cjs/store/types.d.ts +4 -1
  167. package/dist/cjs/store/types.js.map +1 -1
  168. package/dist/cjs/store/vectors.d.cts +14 -1
  169. package/dist/cjs/store/vectors.d.ts +14 -1
  170. package/dist/cjs/store/vectors.js +61 -0
  171. package/dist/cjs/store/vectors.js.map +1 -1
  172. package/dist/cjs/watch.d.cts +6 -0
  173. package/dist/cjs/watch.d.ts +6 -0
  174. package/dist/cjs/watch.js +42 -10
  175. package/dist/cjs/watch.js.map +1 -1
  176. package/dist/cjs/workers/parse.d.cts +4 -3
  177. package/dist/cjs/workers/parse.d.ts +4 -3
  178. package/dist/cjs/workers/parse.js +19 -17
  179. package/dist/cjs/workers/parse.js.map +1 -1
  180. package/dist/esm/cli/index.d.ts +3 -3
  181. package/dist/esm/cli/index.js +3 -3
  182. package/dist/esm/cli/index.js.map +1 -1
  183. package/dist/esm/cli/path.js +4 -10
  184. package/dist/esm/cli/path.js.map +1 -1
  185. package/dist/esm/cli/peek.js +20 -2
  186. package/dist/esm/cli/peek.js.map +1 -1
  187. package/dist/esm/cli/related.js +5 -3
  188. package/dist/esm/cli/related.js.map +1 -1
  189. package/dist/esm/cli/search.js +12 -2
  190. package/dist/esm/cli/search.js.map +1 -1
  191. package/dist/esm/commands/index.d.ts +6 -3
  192. package/dist/esm/commands/index.js +1 -0
  193. package/dist/esm/commands/index.js.map +1 -1
  194. package/dist/esm/commands/map.d.ts +14 -5
  195. package/dist/esm/commands/map.js +2 -0
  196. package/dist/esm/commands/map.js.map +1 -1
  197. package/dist/esm/commands/path.d.ts +14 -0
  198. package/dist/esm/commands/path.js +33 -0
  199. package/dist/esm/commands/path.js.map +1 -0
  200. package/dist/esm/commands/peek.d.ts +25 -2
  201. package/dist/esm/commands/peek.js +59 -19
  202. package/dist/esm/commands/peek.js.map +1 -1
  203. package/dist/esm/commands/related.d.ts +16 -3
  204. package/dist/esm/commands/related.js +45 -28
  205. package/dist/esm/commands/related.js.map +1 -1
  206. package/dist/esm/commands/scope.d.ts +1 -0
  207. package/dist/esm/commands/scope.js +58 -0
  208. package/dist/esm/commands/scope.js.map +1 -1
  209. package/dist/esm/commands/search.d.ts +30 -3
  210. package/dist/esm/commands/search.js +95 -20
  211. package/dist/esm/commands/search.js.map +1 -1
  212. package/dist/esm/commands/signals.d.ts +22 -7
  213. package/dist/esm/commands/signals.js +74 -17
  214. package/dist/esm/commands/signals.js.map +1 -1
  215. package/dist/esm/commands/snapshot.d.ts +3 -0
  216. package/dist/esm/commands/snapshot.js +25 -0
  217. package/dist/esm/commands/snapshot.js.map +1 -0
  218. package/dist/esm/config/load.js +6 -0
  219. package/dist/esm/config/load.js.map +1 -1
  220. package/dist/esm/embed/cohere.js +35 -10
  221. package/dist/esm/embed/cohere.js.map +1 -1
  222. package/dist/esm/embed/endpoint.d.ts +2 -0
  223. package/dist/esm/embed/endpoint.js +11 -0
  224. package/dist/esm/embed/endpoint.js.map +1 -0
  225. package/dist/esm/embed/handoff.d.ts +7 -2
  226. package/dist/esm/embed/handoff.js +7 -0
  227. package/dist/esm/embed/handoff.js.map +1 -1
  228. package/dist/esm/embed/http.d.ts +18 -1
  229. package/dist/esm/embed/http.js +137 -17
  230. package/dist/esm/embed/http.js.map +1 -1
  231. package/dist/esm/embed/openai.js +34 -12
  232. package/dist/esm/embed/openai.js.map +1 -1
  233. package/dist/esm/embed/query.d.ts +9 -3
  234. package/dist/esm/embed/query.js +98 -26
  235. package/dist/esm/embed/query.js.map +1 -1
  236. package/dist/esm/embed/registry.d.ts +2 -2
  237. package/dist/esm/embed/registry.js +34 -5
  238. package/dist/esm/embed/registry.js.map +1 -1
  239. package/dist/esm/embed/static.js +10 -2
  240. package/dist/esm/embed/static.js.map +1 -1
  241. package/dist/esm/embed/store.js +30 -21
  242. package/dist/esm/embed/store.js.map +1 -1
  243. package/dist/esm/embed/types.d.ts +5 -2
  244. package/dist/esm/embed/types.js.map +1 -1
  245. package/dist/esm/features/embed.js +82 -21
  246. package/dist/esm/features/embed.js.map +1 -1
  247. package/dist/esm/graph/traverse.d.ts +2 -2
  248. package/dist/esm/graph/traverse.js +4 -2
  249. package/dist/esm/graph/traverse.js.map +1 -1
  250. package/dist/esm/index.d.ts +2 -2
  251. package/dist/esm/index.js +1 -1
  252. package/dist/esm/index.js.map +1 -1
  253. package/dist/esm/output/output.d.ts +4 -36
  254. package/dist/esm/output/output.js +1 -2
  255. package/dist/esm/output/output.js.map +1 -1
  256. package/dist/esm/scan/pool.d.ts +2 -1
  257. package/dist/esm/scan/pool.js +80 -23
  258. package/dist/esm/scan/pool.js.map +1 -1
  259. package/dist/esm/scan/reparse.d.ts +1 -0
  260. package/dist/esm/scan/reparse.js +40 -12
  261. package/dist/esm/scan/reparse.js.map +1 -1
  262. package/dist/esm/store/builder.d.ts +11 -21
  263. package/dist/esm/store/builder.js +337 -133
  264. package/dist/esm/store/builder.js.map +1 -1
  265. package/dist/esm/store/duckdb/connection.js +9 -1
  266. package/dist/esm/store/duckdb/connection.js.map +1 -1
  267. package/dist/esm/store/duckdb/open.d.ts +1 -1
  268. package/dist/esm/store/duckdb/open.js +13 -9
  269. package/dist/esm/store/duckdb/open.js.map +1 -1
  270. package/dist/esm/store/duckdb/reconcile.js +145 -10
  271. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  272. package/dist/esm/store/embed-scope.js +16 -6
  273. package/dist/esm/store/embed-scope.js.map +1 -1
  274. package/dist/esm/store/index.d.ts +6 -0
  275. package/dist/esm/store/index.js +10 -4
  276. package/dist/esm/store/index.js.map +1 -1
  277. package/dist/esm/store/open.d.ts +6 -1
  278. package/dist/esm/store/open.js +77 -87
  279. package/dist/esm/store/open.js.map +1 -1
  280. package/dist/esm/store/readiness.d.ts +5 -0
  281. package/dist/esm/store/readiness.js +11 -0
  282. package/dist/esm/store/readiness.js.map +1 -0
  283. package/dist/esm/store/reconcile.d.ts +26 -1
  284. package/dist/esm/store/reconcile.js +142 -120
  285. package/dist/esm/store/reconcile.js.map +1 -1
  286. package/dist/esm/store/signature.d.ts +1 -0
  287. package/dist/esm/store/signature.js +8 -0
  288. package/dist/esm/store/signature.js.map +1 -1
  289. package/dist/esm/store/sqlite/open.d.ts +1 -1
  290. package/dist/esm/store/sqlite/open.js +12 -12
  291. package/dist/esm/store/sqlite/open.js.map +1 -1
  292. package/dist/esm/store/sqlite/reconcile.js +2 -2
  293. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  294. package/dist/esm/store/sqlite/store.js +21 -18
  295. package/dist/esm/store/sqlite/store.js.map +1 -1
  296. package/dist/esm/store/sqlite/vectors.d.ts +2 -2
  297. package/dist/esm/store/sqlite/vectors.js +7 -5
  298. package/dist/esm/store/sqlite/vectors.js.map +1 -1
  299. package/dist/esm/store/stages.js.map +1 -1
  300. package/dist/esm/store/turso/connection.d.ts +1 -0
  301. package/dist/esm/store/turso/connection.js +97 -28
  302. package/dist/esm/store/turso/connection.js.map +1 -1
  303. package/dist/esm/store/turso/open.d.ts +1 -1
  304. package/dist/esm/store/turso/open.js +9 -8
  305. package/dist/esm/store/turso/open.js.map +1 -1
  306. package/dist/esm/store/turso/store.js +16 -14
  307. package/dist/esm/store/turso/store.js.map +1 -1
  308. package/dist/esm/store/turso/vectors.d.ts +2 -2
  309. package/dist/esm/store/turso/vectors.js +27 -17
  310. package/dist/esm/store/turso/vectors.js.map +1 -1
  311. package/dist/esm/store/types.d.ts +4 -1
  312. package/dist/esm/store/types.js.map +1 -1
  313. package/dist/esm/store/vectors.d.ts +14 -1
  314. package/dist/esm/store/vectors.js +55 -0
  315. package/dist/esm/store/vectors.js.map +1 -1
  316. package/dist/esm/watch.d.ts +6 -0
  317. package/dist/esm/watch.js +40 -14
  318. package/dist/esm/watch.js.map +1 -1
  319. package/dist/esm/workers/parse.d.ts +4 -3
  320. package/dist/esm/workers/parse.js +19 -17
  321. package/dist/esm/workers/parse.js.map +1 -1
  322. package/package.json +1 -1
  323. package/skills/sense/EXAMPLES.md +3 -3
  324. package/skills/sense/SKILL.md +11 -3
  325. package/skills/sense/references/search.md +17 -1
  326. package/skills/sense/references/sql.md +1 -1
  327. package/skills/sense/references/stores/duckdb.md +1 -1
  328. package/skills/sense/references/terminology.md +32 -0
  329. package/skills/sense-setup/SKILL.md +2 -2
  330. package/skills/sense-setup/references/embeddings.md +1 -1
  331. package/skills/sense-setup/references/store-benchmarks.md +5 -5
  332. package/skills/sense-setup/references/stores/duckdb.md +1 -1
@@ -83,6 +83,7 @@ export function createConnection(duckdb) {
83
83
  ]));
84
84
  await withTransaction(conn, async ()=>{
85
85
  const appender = await duckdb.createAppender(table);
86
+ const errors = [];
86
87
  try {
87
88
  for (const row of rows){
88
89
  for (const column of physical){
@@ -96,9 +97,16 @@ export function createConnection(duckdb) {
96
97
  appender.endRow();
97
98
  }
98
99
  appender.flushSync();
99
- } finally{
100
+ } catch (err) {
101
+ errors.push(err);
102
+ }
103
+ try {
100
104
  appender.closeSync();
105
+ } catch (err) {
106
+ errors.push(err);
101
107
  }
108
+ if (errors.length === 1) throw errors[0];
109
+ if (errors.length > 1) throw new AggregateError(errors, errors[0] instanceof Error ? `${errors[0].message}; appender cleanup also failed` : 'DuckDB appender execution and cleanup failed');
102
110
  });
103
111
  },
104
112
  // One crossing regardless of row count: rewriteBatch folds recognized shapes into a multi-row statement (batch.ts),
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/connection.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBPreparedStatement, DuckDBValue } from '@duckdb/node-api';\nimport { quoteIdent } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { rewriteBatch } from './batch.ts';\nimport { duckdbApi } from './native.ts';\n\n// getRowObjectsJS returns INT64 columns as BigInt regardless of magnitude, while sqlite's small\n// ints are numbers and consumers assume number; in-range values convert here, out-of-range stays BigInt.\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);\nexport function storeValueToJs(value: unknown): unknown {\n return typeof value === 'bigint' && value >= -MAX_SAFE && value <= MAX_SAFE ? Number(value) : value;\n}\nexport function storeRowToJs(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) out[key] = storeValueToJs(value);\n return out;\n}\n\n// @duckdb/node-api only destroys a prepared statement when its connection closes, but a connection can stay open for a whole `sense watch` session.\n// Statement has no dispose (callers reuse one instance across several calls), so this FinalizationRegistry reclaims it once nothing references the wrapper.\nconst preparedFinalizer = new FinalizationRegistry<DuckDBPreparedStatement>((prepared) => {\n prepared.destroySync();\n});\n\n// Wraps one already-prepared DuckDB statement: prepare() is the only async step, so run/get/all rebind and\n// re-execute the same native statement, matching how callers like graph/traverse.ts's ring loop reuse one Statement across several calls.\nclass DuckdbStatement implements Statement {\n private prepared: DuckDBPreparedStatement;\n\n constructor(prepared: DuckDBPreparedStatement) {\n this.prepared = prepared;\n preparedFinalizer.register(this, prepared);\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const result = await this.prepared.run();\n return { changes: result.rowsChanged, lastInsertRowid: 0 };\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n const rows = await this.all(...params);\n return rows[0];\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const reader = await this.prepared.runAndReadAll();\n return (reader.getRowObjectsJS() as Array<Record<string, unknown>>).map(storeRowToJs);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n // Never called on the portable Connection/Statement surface today (only Store.raw streams);\n // materializing keeps this a correct, if eager, AsyncIterable.\n yield* await this.all(...params);\n }\n\n columns(): Array<{ name: string }> {\n const out: Array<{ name: string }> = [];\n for (let i = 0; i < this.prepared.columnCount; i++) out.push({ name: this.prepared.columnName(i) });\n return out;\n }\n\n setReadBigInts(_enabled: boolean): void {\n // No-op: in-range ints are already numbers here (storeRowToJs converts what getRowObjectsJS\n // hands back as BigInt), so the node:sqlite throw-at-step-time problem does not exist.\n }\n}\n\n// Adds the native DuckDBConnection to the portable Connection, so duckdb's own dialect code\n// (reconcile.ts's insertNew) can reach createAppender(); sqlite/turso have no such member.\nexport interface DuckdbConnection extends Connection {\n readonly duckdb: DuckDBConnection;\n}\n\nexport function createConnection(duckdb: DuckDBConnection): DuckdbConnection {\n const conn: DuckdbConnection = {\n duckdb,\n async exec(sql: string): Promise<void> {\n await duckdb.run(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new DuckdbStatement(await duckdb.prepare(sql));\n },\n // The appender writes columnar vectors with no per-parameter binding, which is the cost\n // runBatch pays: measured 7.2x on frontmatter at 6,566 rows. Alignment is against the table's\n // own physical column order, read fresh, so a column the caller does not write (a\n // feature-owned \"_rank\", say) takes appendDefault() rather than shifting every value one slot.\n async appendRows(table: string, columns: string[], rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n const { variantValue } = await duckdbApi();\n const infoStmt = await conn.prepare(`PRAGMA table_info(${quoteIdent(table)})`);\n const physical = (await infoStmt.all()) as Array<{ name: string; type: string }>;\n const rowIndexOf = new Map(columns.map((name, i) => [name, i]));\n\n await withTransaction(conn, async () => {\n const appender = await duckdb.createAppender(table);\n try {\n for (const row of rows) {\n for (const column of physical) {\n const idx = rowIndexOf.get(column.name);\n const value = idx === undefined ? undefined : row[idx];\n if (idx === undefined) appender.appendDefault();\n else if (value === null || value === undefined) appender.appendNull();\n // VARIANT is frontmatter's dynamic columns only; every other table is statically\n // typed, and appendValue is what preserves those types.\n else if (column.type === 'VARIANT') appender.appendVariant(variantValue(value as DuckDBValue));\n else appender.appendValue(value as DuckDBValue);\n }\n appender.endRow();\n }\n appender.flushSync();\n } finally {\n appender.closeSync();\n }\n });\n },\n // One crossing regardless of row count: rewriteBatch folds recognized shapes into a multi-row statement (batch.ts),\n // else falls back to a bind-and-run loop, one call either way. Joins the caller's transaction when there is one, else opens its own.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(conn, async () => {\n const rewritten = rewriteBatch(sql, paramRows.length);\n if (rewritten) {\n const stmt = await duckdb.prepare(rewritten.sql);\n try {\n stmt.bind(paramRows.flat() as DuckDBValue[]);\n await stmt.run();\n } finally {\n stmt.destroySync();\n }\n return;\n }\n const stmt = await duckdb.prepare(sql);\n try {\n for (const row of paramRows) {\n stmt.bind(row as DuckDBValue[]);\n await stmt.run();\n }\n } finally {\n stmt.destroySync();\n }\n });\n },\n };\n return conn;\n}\n"],"names":["quoteIdent","withTransaction","rewriteBatch","duckdbApi","MAX_SAFE","BigInt","Number","MAX_SAFE_INTEGER","storeValueToJs","value","storeRowToJs","row","out","key","Object","entries","preparedFinalizer","FinalizationRegistry","prepared","destroySync","DuckdbStatement","run","params","length","bind","result","changes","rowsChanged","lastInsertRowid","get","rows","all","reader","runAndReadAll","getRowObjectsJS","map","iterate","columns","i","columnCount","push","name","columnName","setReadBigInts","_enabled","register","createConnection","duckdb","conn","exec","sql","prepare","appendRows","table","variantValue","infoStmt","physical","rowIndexOf","Map","appender","createAppender","column","idx","undefined","appendDefault","appendNull","type","appendVariant","appendValue","endRow","flushSync","closeSync","runBatch","paramRows","rewritten","stmt","flat"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/connection.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBPreparedStatement, DuckDBValue } from '@duckdb/node-api';\nimport { quoteIdent } from '../shared.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { rewriteBatch } from './batch.ts';\nimport { duckdbApi } from './native.ts';\n\n// getRowObjectsJS returns INT64 columns as BigInt regardless of magnitude, while sqlite's small\n// ints are numbers and consumers assume number; in-range values convert here, out-of-range stays BigInt.\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);\nexport function storeValueToJs(value: unknown): unknown {\n return typeof value === 'bigint' && value >= -MAX_SAFE && value <= MAX_SAFE ? Number(value) : value;\n}\nexport function storeRowToJs(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) out[key] = storeValueToJs(value);\n return out;\n}\n\n// @duckdb/node-api only destroys a prepared statement when its connection closes, but a connection can stay open for a whole `sense watch` session.\n// Statement has no dispose (callers reuse one instance across several calls), so this FinalizationRegistry reclaims it once nothing references the wrapper.\nconst preparedFinalizer = new FinalizationRegistry<DuckDBPreparedStatement>((prepared) => {\n prepared.destroySync();\n});\n\n// Wraps one already-prepared DuckDB statement: prepare() is the only async step, so run/get/all rebind and\n// re-execute the same native statement, matching how callers like graph/traverse.ts's ring loop reuse one Statement across several calls.\nclass DuckdbStatement implements Statement {\n private prepared: DuckDBPreparedStatement;\n\n constructor(prepared: DuckDBPreparedStatement) {\n this.prepared = prepared;\n preparedFinalizer.register(this, prepared);\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const result = await this.prepared.run();\n return { changes: result.rowsChanged, lastInsertRowid: 0 };\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n const rows = await this.all(...params);\n return rows[0];\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const reader = await this.prepared.runAndReadAll();\n return (reader.getRowObjectsJS() as Array<Record<string, unknown>>).map(storeRowToJs);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n // Never called on the portable Connection/Statement surface today (only Store.raw streams);\n // materializing keeps this a correct, if eager, AsyncIterable.\n yield* await this.all(...params);\n }\n\n columns(): Array<{ name: string }> {\n const out: Array<{ name: string }> = [];\n for (let i = 0; i < this.prepared.columnCount; i++) out.push({ name: this.prepared.columnName(i) });\n return out;\n }\n\n setReadBigInts(_enabled: boolean): void {\n // No-op: in-range ints are already numbers here (storeRowToJs converts what getRowObjectsJS\n // hands back as BigInt), so the node:sqlite throw-at-step-time problem does not exist.\n }\n}\n\n// Adds the native DuckDBConnection to the portable Connection, so duckdb's own dialect code\n// (reconcile.ts's insertNew) can reach createAppender(); sqlite/turso have no such member.\nexport interface DuckdbConnection extends Connection {\n readonly duckdb: DuckDBConnection;\n}\n\nexport function createConnection(duckdb: DuckDBConnection): DuckdbConnection {\n const conn: DuckdbConnection = {\n duckdb,\n async exec(sql: string): Promise<void> {\n await duckdb.run(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new DuckdbStatement(await duckdb.prepare(sql));\n },\n // The appender writes columnar vectors with no per-parameter binding, which is the cost\n // runBatch pays: measured 7.2x on frontmatter at 6,566 rows. Alignment is against the table's\n // own physical column order, read fresh, so a column the caller does not write (a\n // feature-owned \"_rank\", say) takes appendDefault() rather than shifting every value one slot.\n async appendRows(table: string, columns: string[], rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n const { variantValue } = await duckdbApi();\n const infoStmt = await conn.prepare(`PRAGMA table_info(${quoteIdent(table)})`);\n const physical = (await infoStmt.all()) as Array<{ name: string; type: string }>;\n const rowIndexOf = new Map(columns.map((name, i) => [name, i]));\n\n await withTransaction(conn, async () => {\n const appender = await duckdb.createAppender(table);\n const errors: unknown[] = [];\n try {\n for (const row of rows) {\n for (const column of physical) {\n const idx = rowIndexOf.get(column.name);\n const value = idx === undefined ? undefined : row[idx];\n if (idx === undefined) appender.appendDefault();\n else if (value === null || value === undefined) appender.appendNull();\n // VARIANT is frontmatter's dynamic columns only; every other table is statically\n // typed, and appendValue is what preserves those types.\n else if (column.type === 'VARIANT') appender.appendVariant(variantValue(value as DuckDBValue));\n else appender.appendValue(value as DuckDBValue);\n }\n appender.endRow();\n }\n appender.flushSync();\n } catch (err) {\n errors.push(err);\n }\n try {\n appender.closeSync();\n } catch (err) {\n errors.push(err);\n }\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, errors[0] instanceof Error ? `${errors[0].message}; appender cleanup also failed` : 'DuckDB appender execution and cleanup failed');\n });\n },\n // One crossing regardless of row count: rewriteBatch folds recognized shapes into a multi-row statement (batch.ts),\n // else falls back to a bind-and-run loop, one call either way. Joins the caller's transaction when there is one, else opens its own.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(conn, async () => {\n const rewritten = rewriteBatch(sql, paramRows.length);\n if (rewritten) {\n const stmt = await duckdb.prepare(rewritten.sql);\n try {\n stmt.bind(paramRows.flat() as DuckDBValue[]);\n await stmt.run();\n } finally {\n stmt.destroySync();\n }\n return;\n }\n const stmt = await duckdb.prepare(sql);\n try {\n for (const row of paramRows) {\n stmt.bind(row as DuckDBValue[]);\n await stmt.run();\n }\n } finally {\n stmt.destroySync();\n }\n });\n },\n };\n return conn;\n}\n"],"names":["quoteIdent","withTransaction","rewriteBatch","duckdbApi","MAX_SAFE","BigInt","Number","MAX_SAFE_INTEGER","storeValueToJs","value","storeRowToJs","row","out","key","Object","entries","preparedFinalizer","FinalizationRegistry","prepared","destroySync","DuckdbStatement","run","params","length","bind","result","changes","rowsChanged","lastInsertRowid","get","rows","all","reader","runAndReadAll","getRowObjectsJS","map","iterate","columns","i","columnCount","push","name","columnName","setReadBigInts","_enabled","register","createConnection","duckdb","conn","exec","sql","prepare","appendRows","table","variantValue","infoStmt","physical","rowIndexOf","Map","appender","createAppender","errors","column","idx","undefined","appendDefault","appendNull","type","appendVariant","appendValue","endRow","flushSync","err","closeSync","AggregateError","Error","message","runBatch","paramRows","rewritten","stmt","flat"],"mappings":"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"}
@@ -3,7 +3,7 @@ import type { ResolvedConfig } from '../../config/index.js';
3
3
  import type { InternalOpenOptions, OpenResult } from '../open.js';
4
4
  import type { OpenDialect } from '../types.js';
5
5
  export declare const DB_FILENAME = "cache.duckdb";
6
- export declare const SCHEMA_VERSION = "5";
6
+ export declare const SCHEMA_VERSION = "6";
7
7
  export type { OpenResult };
8
8
  interface DuckdbHandle {
9
9
  instance: DuckDBInstance;
@@ -1,7 +1,6 @@
1
- import { featureSignature } from '../../config/index.js';
2
1
  import { STORE_DIMS } from '../../embed/types.js';
3
2
  import { SenseError } from '../../errors.js';
4
- import { activeFeatures, FEATURES } from '../../features/index.js';
3
+ import { activeFeatures } from '../../features/index.js';
5
4
  import { openWithDialect } from '../open.js';
6
5
  import { getMeta, setMeta } from '../shared.js';
7
6
  import { createConnection } from './connection.js';
@@ -14,28 +13,31 @@ import { createVectorWriteStage } from './vectors.js';
14
13
  export const DB_FILENAME = 'cache.duckdb';
15
14
  // Independent of sqlite's SCHEMA_VERSION -- the two stores' cache shapes evolve separately (VARIANT frontmatter columns vs untyped).
16
15
  // The store name already joins the feature signature, so switching a config's `store` key rebuilds rather than reusing the other engine's cache.
17
- export const SCHEMA_VERSION = '5';
16
+ export const SCHEMA_VERSION = '6';
18
17
  // `content` is a plain table (not FTS-virtual): build prepares the persistent FTS index over it,
19
18
  // while contains() verification reads the table directly.
20
19
  // No tokenizer resolution: this store always uses the fts extension's default (porter) stemmer.
21
- async function ensureSchema(handle, conn, cfg) {
20
+ async function ensureCoreSchema(_handle, conn) {
22
21
  await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter ("path" TEXT PRIMARY KEY, "_mtime" DOUBLE, "_ctime" DOUBLE, "_size" INTEGER, "_parse_error" TEXT)`);
23
22
  await conn.exec(`CREATE TABLE IF NOT EXISTS content ("path" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT)`);
24
23
  await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources ("path" TEXT PRIMARY KEY, text TEXT NOT NULL)`);
25
24
  await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files ("path" TEXT, preset TEXT, PRIMARY KEY ("path", preset))`);
26
25
  await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');
26
+ if (await getMeta(conn, 'schema_version') === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);
27
+ }
28
+ async function ensureFeatureSchema(_handle, conn, cfg) {
27
29
  for (const feature of activeFeatures(cfg)){
28
30
  // Native FLOAT[STORE_DIMS] instead of the embed feature's engine-neutral BLOB+scale DDL (vectors.ts). `scale` is kept, unused,
29
31
  // so the feature's shared reconcile-time INSERT/DELETE (features/embed.ts) names a column that exists on both stores.
30
32
  if (feature.name === 'embed') {
31
- await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings ("path" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector FLOAT[${STORE_DIMS}], PRIMARY KEY ("path", chunk))`);
32
- await createVectorWriteStage(handle.duckdb, STORE_DIMS);
33
+ await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings ("path" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, content_identity TEXT NOT NULL, scale REAL, vector FLOAT[${STORE_DIMS}], PRIMARY KEY ("path", chunk))`);
33
34
  continue;
34
35
  }
35
36
  await feature.schema(conn);
36
37
  }
37
- if (await getMeta(conn, 'schema_version') === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);
38
- if (await getMeta(conn, 'features') === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));
38
+ }
39
+ async function prepareConnection(handle, _conn, _cfg) {
40
+ await createVectorWriteStage(handle.duckdb, STORE_DIMS);
39
41
  }
40
42
  // Order matters: the connection must be gone before the instance closes the WAL.
41
43
  async function close(handle) {
@@ -98,7 +100,9 @@ export const duckdbOpenDialect = {
98
100
  // way: posix "Could not set lock on file ... Conflicting lock is held in <exe> (PID n)", Windows
99
101
  // "Cannot open file ... being used by another process". Both are the same condition.
100
102
  isLocked: (err)=>/Could not set lock on file|being used by another process/.test(err.message),
101
- ensureSchema,
103
+ ensureCoreSchema,
104
+ ensureFeatureSchema,
105
+ prepareConnection,
102
106
  prepareLexical: prepareFts,
103
107
  assertLexicalReady: assertFtsReady,
104
108
  createStore: (handle, conn)=>createStore(handle.instance, handle.duckdb, conn)
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/open.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { featureSignature } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures, FEATURES } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { assertFtsReady, prepareFts } from './lexical.ts';\nimport { DUCKDB_PACKAGE, duckdbApi } from './native.ts';\nimport { duckdbDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\nimport { createVectorWriteStage } from './vectors.ts';\n\nexport const DB_FILENAME = 'cache.duckdb';\n// Independent of sqlite's SCHEMA_VERSION -- the two stores' cache shapes evolve separately (VARIANT frontmatter columns vs untyped).\n// The store name already joins the feature signature, so switching a config's `store` key rebuilds rather than reusing the other engine's cache.\nexport const SCHEMA_VERSION = '5';\n\nexport type { OpenResult };\n\n// The connection plus this store's own native handles (types.ts's OpenDialect<Handle>): the\n// instance owns the WAL and must outlive the connection borrowed from it.\ninterface DuckdbHandle {\n instance: DuckDBInstance;\n duckdb: DuckDBConnection;\n}\n\n// `content` is a plain table (not FTS-virtual): build prepares the persistent FTS index over it,\n// while contains() verification reads the table directly.\n// No tokenizer resolution: this store always uses the fts extension's default (porter) stemmer.\nasync function ensureSchema(handle: DuckdbHandle, conn: Connection, cfg: Config): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" DOUBLE, \"_ctime\" DOUBLE, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS content (\"path\" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n for (const feature of activeFeatures(cfg)) {\n // Native FLOAT[STORE_DIMS] instead of the embed feature's engine-neutral BLOB+scale DDL (vectors.ts). `scale` is kept, unused,\n // so the feature's shared reconcile-time INSERT/DELETE (features/embed.ts) names a column that exists on both stores.\n if (feature.name === 'embed') {\n await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, scale REAL, vector FLOAT[${STORE_DIMS}], PRIMARY KEY (\"path\", chunk))`);\n await createVectorWriteStage(handle.duckdb, STORE_DIMS);\n continue;\n }\n await feature.schema(conn);\n }\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n if ((await getMeta(conn, 'features')) === null) await setMeta(conn, 'features', featureSignature(cfg, FEATURES));\n}\n\n// Order matters: the connection must be gone before the instance closes the WAL.\nasync function close(handle: DuckdbHandle): Promise<void> {\n handle.duckdb.disconnectSync();\n handle.instance.closeSync();\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: DuckdbHandle; conn: Connection }> {\n // Dynamic, not a top-level import: sqlite trees must never attempt to resolve this optional peer dependency, so nothing imports\n // it as a value until a duckdb tree opens (types-only imports are erased). Installed on first use if missing, shared with sql-functions.ts and vectors.ts via native.ts's duckdbApi.\n let DuckDBInstance: typeof import('@duckdb/node-api').DuckDBInstance;\n try {\n ({ DuckDBInstance } = await duckdbApi());\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" needs the ${DUCKDB_PACKAGE} package (${(err as Error).message})`);\n }\n\n let duckdb: DuckDBConnection;\n let instance: DuckDBInstance | undefined;\n try {\n // On-disk files default to an older storage format for cross-version compatibility, which rejects VARIANT columns (\"VARIANT\n // columns are not supported in storage versions prior to v1.5.0\"); this store's dynamic frontmatter columns need VARIANT (reconcile.ts), so the floor is pinned explicitly.\n instance = await DuckDBInstance.create(dbPath, options.observational ? { access_mode: 'READ_ONLY' } : { storage_compatibility_version: 'v1.5.0' });\n duckdb = await instance.connect();\n } catch (err) {\n // create() may have succeeded before connect() failed: close it, or its WAL stays open.\n instance?.closeSync();\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n // A throw below would leak the open instance, whose WAL then locks the .duckdb file undeletable on Windows.\n try {\n await registerFunctions(duckdb);\n const conn = createConnection(duckdb);\n return { handle: { instance, duckdb }, conn };\n } catch (err) {\n await close({ instance, duckdb });\n throw err;\n }\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const duckdbOpenDialect: OpenDialect<DuckdbHandle> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: duckdbDialect,\n connect,\n close,\n // duckdb's file lock spans the connection's life, and each platform words the refusal its own\n // way: posix \"Could not set lock on file ... Conflicting lock is held in <exe> (PID n)\", Windows\n // \"Cannot open file ... being used by another process\". Both are the same condition.\n isLocked: (err) => /Could not set lock on file|being used by another process/.test(err.message),\n ensureSchema,\n prepareLexical: prepareFts,\n assertLexicalReady: assertFtsReady,\n createStore: (handle, conn) => createStore(handle.instance, handle.duckdb, conn),\n};\n\nexport async function openDuckdb(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, duckdbOpenDialect, options);\n}\n"],"names":["featureSignature","STORE_DIMS","SenseError","activeFeatures","FEATURES","openWithDialect","getMeta","setMeta","createConnection","assertFtsReady","prepareFts","DUCKDB_PACKAGE","duckdbApi","duckdbDialect","registerFunctions","createStore","createVectorWriteStage","DB_FILENAME","SCHEMA_VERSION","ensureSchema","handle","conn","cfg","exec","feature","name","duckdb","schema","close","disconnectSync","instance","closeSync","connect","dbPath","_cfg","options","DuckDBInstance","err","message","create","observational","access_mode","storage_compatibility_version","duckdbOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","prepareLexical","assertLexicalReady","openDuckdb"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/open.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBInstance } from '@duckdb/node-api';\nimport type { Config, ResolvedConfig } from '../../config/index.ts';\nimport { STORE_DIMS } from '../../embed/types.ts';\nimport { SenseError } from '../../errors.ts';\nimport { activeFeatures } from '../../features/index.ts';\nimport type { InternalOpenOptions, OpenResult } from '../open.ts';\nimport { openWithDialect } from '../open.ts';\nimport { getMeta, setMeta } from '../shared.ts';\nimport type { Connection, OpenDialect } from '../types.ts';\nimport { createConnection } from './connection.ts';\nimport { assertFtsReady, prepareFts } from './lexical.ts';\nimport { DUCKDB_PACKAGE, duckdbApi } from './native.ts';\nimport { duckdbDialect } from './reconcile.ts';\nimport { registerFunctions } from './sql-functions.ts';\nimport { createStore } from './store.ts';\nimport { createVectorWriteStage } from './vectors.ts';\n\nexport const DB_FILENAME = 'cache.duckdb';\n// Independent of sqlite's SCHEMA_VERSION -- the two stores' cache shapes evolve separately (VARIANT frontmatter columns vs untyped).\n// The store name already joins the feature signature, so switching a config's `store` key rebuilds rather than reusing the other engine's cache.\nexport const SCHEMA_VERSION = '6';\n\nexport type { OpenResult };\n\n// The connection plus this store's own native handles (types.ts's OpenDialect<Handle>): the\n// instance owns the WAL and must outlive the connection borrowed from it.\ninterface DuckdbHandle {\n instance: DuckDBInstance;\n duckdb: DuckDBConnection;\n}\n\n// `content` is a plain table (not FTS-virtual): build prepares the persistent FTS index over it,\n// while contains() verification reads the table directly.\n// No tokenizer resolution: this store always uses the fts extension's default (porter) stemmer.\nasync function ensureCoreSchema(_handle: DuckdbHandle, conn: Connection): Promise<void> {\n await conn.exec(`CREATE TABLE IF NOT EXISTS frontmatter (\"path\" TEXT PRIMARY KEY, \"_mtime\" DOUBLE, \"_ctime\" DOUBLE, \"_size\" INTEGER, \"_parse_error\" TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS content (\"path\" TEXT PRIMARY KEY, title TEXT, summary TEXT, text TEXT)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS indexed_sources (\"path\" TEXT PRIMARY KEY, text TEXT NOT NULL)`);\n await conn.exec(`CREATE TABLE IF NOT EXISTS preset_files (\"path\" TEXT, preset TEXT, PRIMARY KEY (\"path\", preset))`);\n await conn.exec('CREATE INDEX IF NOT EXISTS preset_files_preset ON preset_files(preset)');\n if ((await getMeta(conn, 'schema_version')) === null) await setMeta(conn, 'schema_version', SCHEMA_VERSION);\n}\n\nasync function ensureFeatureSchema(_handle: DuckdbHandle, conn: Connection, cfg: Config): Promise<void> {\n for (const feature of activeFeatures(cfg)) {\n // Native FLOAT[STORE_DIMS] instead of the embed feature's engine-neutral BLOB+scale DDL (vectors.ts). `scale` is kept, unused,\n // so the feature's shared reconcile-time INSERT/DELETE (features/embed.ts) names a column that exists on both stores.\n if (feature.name === 'embed') {\n await conn.exec(`CREATE TABLE IF NOT EXISTS embeddings (\"path\" TEXT, chunk INTEGER, start_line INTEGER, end_line INTEGER, content_identity TEXT NOT NULL, scale REAL, vector FLOAT[${STORE_DIMS}], PRIMARY KEY (\"path\", chunk))`);\n continue;\n }\n await feature.schema(conn);\n }\n}\n\nasync function prepareConnection(handle: DuckdbHandle, _conn: Connection, _cfg: Config): Promise<void> {\n await createVectorWriteStage(handle.duckdb, STORE_DIMS);\n}\n\n// Order matters: the connection must be gone before the instance closes the WAL.\nasync function close(handle: DuckdbHandle): Promise<void> {\n handle.duckdb.disconnectSync();\n handle.instance.closeSync();\n}\n\nasync function connect(dbPath: string, _cfg: ResolvedConfig, options: { existingOnly: boolean; observational: boolean }): Promise<{ handle: DuckdbHandle; conn: Connection }> {\n // Dynamic, not a top-level import: sqlite trees must never attempt to resolve this optional peer dependency, so nothing imports\n // it as a value until a duckdb tree opens (types-only imports are erased). Installed on first use if missing, shared with sql-functions.ts and vectors.ts via native.ts's duckdbApi.\n let DuckDBInstance: typeof import('@duckdb/node-api').DuckDBInstance;\n try {\n ({ DuckDBInstance } = await duckdbApi());\n } catch (err) {\n if (err instanceof SenseError) throw err;\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" needs the ${DUCKDB_PACKAGE} package (${(err as Error).message})`);\n }\n\n let duckdb: DuckDBConnection;\n let instance: DuckDBInstance | undefined;\n try {\n // On-disk files default to an older storage format for cross-version compatibility, which rejects VARIANT columns (\"VARIANT\n // columns are not supported in storage versions prior to v1.5.0\"); this store's dynamic frontmatter columns need VARIANT (reconcile.ts), so the floor is pinned explicitly.\n instance = await DuckDBInstance.create(dbPath, options.observational ? { access_mode: 'READ_ONLY' } : { storage_compatibility_version: 'v1.5.0' });\n duckdb = await instance.connect();\n } catch (err) {\n // create() may have succeeded before connect() failed: close it, or its WAL stays open.\n instance?.closeSync();\n throw new SenseError('STORE_DEPENDENCY_MISSING', `store \"duckdb\" failed to open ${dbPath}: ${(err as Error).message}`);\n }\n // A throw below would leak the open instance, whose WAL then locks the .duckdb file undeletable on Windows.\n try {\n await registerFunctions(duckdb);\n const conn = createConnection(duckdb);\n return { handle: { instance, duckdb }, conn };\n } catch (err) {\n await close({ instance, duckdb });\n throw err;\n }\n}\n\n// This store's dialect (types.ts's OpenDialect) for the shared orchestration in store/open.ts.\nexport const duckdbOpenDialect: OpenDialect<DuckdbHandle> = {\n filename: DB_FILENAME,\n schemaVersion: SCHEMA_VERSION,\n reconcileDialect: duckdbDialect,\n connect,\n close,\n // duckdb's file lock spans the connection's life, and each platform words the refusal its own\n // way: posix \"Could not set lock on file ... Conflicting lock is held in <exe> (PID n)\", Windows\n // \"Cannot open file ... being used by another process\". Both are the same condition.\n isLocked: (err) => /Could not set lock on file|being used by another process/.test(err.message),\n ensureCoreSchema,\n ensureFeatureSchema,\n prepareConnection,\n prepareLexical: prepareFts,\n assertLexicalReady: assertFtsReady,\n createStore: (handle, conn) => createStore(handle.instance, handle.duckdb, conn),\n};\n\nexport async function openDuckdb(cfg: ResolvedConfig, options?: InternalOpenOptions): Promise<OpenResult> {\n return openWithDialect(cfg, duckdbOpenDialect, options);\n}\n"],"names":["STORE_DIMS","SenseError","activeFeatures","openWithDialect","getMeta","setMeta","createConnection","assertFtsReady","prepareFts","DUCKDB_PACKAGE","duckdbApi","duckdbDialect","registerFunctions","createStore","createVectorWriteStage","DB_FILENAME","SCHEMA_VERSION","ensureCoreSchema","_handle","conn","exec","ensureFeatureSchema","cfg","feature","name","schema","prepareConnection","handle","_conn","_cfg","duckdb","close","disconnectSync","instance","closeSync","connect","dbPath","options","DuckDBInstance","err","message","create","observational","access_mode","storage_compatibility_version","duckdbOpenDialect","filename","schemaVersion","reconcileDialect","isLocked","test","prepareLexical","assertLexicalReady","openDuckdb"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { SenseError } from '../../errors.js';
2
2
  import { appendRows, quoteIdent } from '../shared.js';
3
3
  import { markContentStale } from './lexical.js';
4
+ import { duckdbApi } from './native.js';
4
5
  // This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in
5
- // store/reconcile.ts. `content` is a plain table, not FTS-virtual, so rows are maintained here
6
- // unconditionally; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter
6
+ // store/reconcile.ts. `content` is a plain table, not FTS-virtual, so changed rows are maintained
7
+ // here; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter
7
8
  // column is VARIANT (holds mapValue()'s mixed JS types for one key across files).
8
9
  // No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):
9
10
  // `path` is content's own primary key, so this is a plain per-doc row.
@@ -14,6 +15,8 @@ const CONTENT_COLUMNS = [
14
15
  'text'
15
16
  ];
16
17
  const INSERT_CONTENT_SQL = `INSERT INTO content (${CONTENT_COLUMNS.map(quoteIdent).join(', ')}) VALUES (?, ?, ?, ?)`;
18
+ // Bound the extra persisted text held for exact comparison, even during a full reparse.
19
+ const CONTENT_LOOKUP_CHUNK = 128;
17
20
  function contentRow(doc) {
18
21
  return [
19
22
  doc.relPath,
@@ -25,16 +28,147 @@ function contentRow(doc) {
25
28
  // No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity fence anyway
26
29
  // so a runaway frontmatter generator fails with a clear message instead of an unbounded ALTER TABLE loop.
27
30
  const MAX_FRONTMATTER_COLUMNS = 10000;
28
- async function reconcileContent(conn, touched, docs, _delta) {
29
- if (touched.length > 0) await conn.runBatch('DELETE FROM content WHERE "path" = ?', touched.map((p)=>[
31
+ const FRONTMATTER_UPDATE_CHUNK = 128;
32
+ async function withNativeStatement(native, sql, fn) {
33
+ const stmt = await native.prepare(sql);
34
+ const errors = [];
35
+ let result;
36
+ try {
37
+ result = await fn(stmt);
38
+ } catch (err) {
39
+ errors.push(err);
40
+ }
41
+ try {
42
+ stmt.destroySync();
43
+ } catch (err) {
44
+ errors.push(err);
45
+ }
46
+ if (errors.length === 1) throw errors[0];
47
+ if (errors.length > 1) throw new AggregateError(errors, 'frontmatter statement and cleanup failed');
48
+ return result;
49
+ }
50
+ // Wide writes amplify native commit allocations even for one changed stamp.
51
+ // Skip only known, exactly equal values; uncertain representations still write.
52
+ async function updateFrontmatter(conn, writableColumns, rows) {
53
+ if (rows.length === 0) return;
54
+ const native = conn.duckdb;
55
+ const { DuckDBVariantValue, DuckDBTypeId } = await duckdbApi();
56
+ const physical = await withNativeStatement(native, 'PRAGMA table_info(frontmatter)', async (stmt)=>{
57
+ const reader = await stmt.runAndReadAll();
58
+ return reader.getRowObjectsJS();
59
+ });
60
+ const writable = new Map(writableColumns.map((name, i)=>[
61
+ name,
62
+ i
63
+ ]));
64
+ const pathIndex = writable.get('path');
65
+ if (pathIndex === undefined) throw new Error('frontmatter update requires a writable path column');
66
+ const columnTypes = new Map(physical.map((column)=>[
67
+ column.name,
68
+ column.type
69
+ ]));
70
+ const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(()=>'?').join(', ')})`;
71
+ const sameValue = (old, value, column)=>{
72
+ var _old_type;
73
+ if (value === null || value === undefined) return old === null;
74
+ if (columnTypes.get(column) !== 'VARIANT') return Object.is(old, value);
75
+ if (!(old instanceof DuckDBVariantValue)) return false;
76
+ const typeId = typeof value === 'string' ? DuckDBTypeId.VARCHAR : typeof value === 'bigint' ? DuckDBTypeId.HUGEINT : typeof value === 'number' && !Number.isInteger(value) ? DuckDBTypeId.DOUBLE : undefined;
77
+ return typeId !== undefined && ((_old_type = old.type) === null || _old_type === void 0 ? void 0 : _old_type.typeId) === typeId && Object.is(old.value, value);
78
+ };
79
+ for(let i = 0; i < rows.length; i += FRONTMATTER_UPDATE_CHUNK){
80
+ const chunk = rows.slice(i, i + FRONTMATTER_UPDATE_CHUNK);
81
+ const paths = chunk.map((row)=>row[pathIndex]);
82
+ const placeholders = paths.map(()=>'?').join(', ');
83
+ const selected = writableColumns.map(quoteIdent).join(', ');
84
+ const existing = await withNativeStatement(native, `SELECT ${selected} FROM frontmatter WHERE "path" IN (${placeholders})`, async (stmt)=>{
85
+ stmt.bind(paths);
86
+ const reader = await stmt.runAndReadAll();
87
+ // Native wrappers distinguish numeric tags and SQL NULL from VARIANT null.
88
+ return new Map(reader.getRowObjects().map((row)=>[
89
+ row.path,
90
+ row
91
+ ]));
92
+ });
93
+ const plans = chunk.map((row)=>{
94
+ const old = existing.get(row[pathIndex]);
95
+ const changed = old ? writableColumns.flatMap((column, index)=>sameValue(old[column], row[index], column) ? [] : [
96
+ index
97
+ ]) : [];
98
+ return {
99
+ row,
100
+ missing: old === undefined,
101
+ changed,
102
+ signature: changed.join(',')
103
+ };
104
+ });
105
+ for(let position = 0; position < plans.length;){
106
+ const plan = plans[position];
107
+ if (!plan.missing && plan.changed.length === 0) {
108
+ ++position;
109
+ continue;
110
+ }
111
+ let end = position + 1;
112
+ while(end < plans.length && plans[end].missing === plan.missing && plans[end].signature === plan.signature)++end;
113
+ if (plan.missing) {
114
+ await appendRows(conn, 'frontmatter', writableColumns, insertSql, plans.slice(position, end).map((repair)=>repair.row));
115
+ } else {
116
+ const assignments = plan.changed.map((index)=>`${quoteIdent(writableColumns[index])} = ?`).join(', ');
117
+ await withNativeStatement(native, `UPDATE frontmatter SET ${assignments} WHERE "path" = ?`, async (stmt)=>{
118
+ for(let rowIndex = position; rowIndex < end; ++rowIndex){
119
+ const row = plans[rowIndex].row;
120
+ stmt.bind([
121
+ ...plan.changed.map((index)=>{
122
+ var _row_index;
123
+ return (_row_index = row[index]) !== null && _row_index !== void 0 ? _row_index : null;
124
+ }),
125
+ row[pathIndex]
126
+ ]);
127
+ await stmt.run();
128
+ }
129
+ });
130
+ }
131
+ position = end;
132
+ }
133
+ }
134
+ }
135
+ async function reconcileContent(conn, touched, docs, delta) {
136
+ const added = new Set(delta.added);
137
+ const reparsed = docs.filter((doc)=>!added.has(doc.relPath));
138
+ const changed = new Set(delta.added);
139
+ const removed = [];
140
+ for(let i = 0; i < reparsed.length; i += CONTENT_LOOKUP_CHUNK){
141
+ const chunk = reparsed.slice(i, i + CONTENT_LOOKUP_CHUNK);
142
+ const stmt = await conn.prepare(`SELECT "path", title, summary, text FROM content WHERE "path" IN (${chunk.map(()=>'?').join(', ')})`);
143
+ const rows = await stmt.all(...chunk.map((doc)=>doc.relPath));
144
+ const existing = new Map(rows.map((row)=>[
145
+ row.path,
146
+ row
147
+ ]));
148
+ for (const doc of chunk){
149
+ const row = existing.get(doc.relPath);
150
+ if (row && row.title === doc.search.title && row.summary === doc.search.summary && row.text === doc.search.text) continue;
151
+ changed.add(doc.relPath);
152
+ if (row) removed.push(doc.relPath);
153
+ }
154
+ }
155
+ const docPaths = new Set(docs.map((doc)=>doc.relPath));
156
+ const vanished = touched.filter((path)=>!docPaths.has(path));
157
+ for(let i = 0; i < vanished.length; i += CONTENT_LOOKUP_CHUNK){
158
+ const chunk = vanished.slice(i, i + CONTENT_LOOKUP_CHUNK);
159
+ const stmt = await conn.prepare(`SELECT "path" FROM content WHERE "path" IN (${chunk.map(()=>'?').join(', ')})`);
160
+ const rows = await stmt.all(...chunk);
161
+ removed.push(...rows.map((row)=>row.path));
162
+ }
163
+ if (removed.length > 0) await conn.runBatch('DELETE FROM content WHERE "path" = ?', removed.map((p)=>[
30
164
  p
31
165
  ]));
32
- // The delete above cleared every touched path, and duckdb holds the cache file for its whole
33
- // connection, so no second writer can have landed one of these rows: nothing can conflict.
34
- await appendRows(conn, 'content', CONTENT_COLUMNS, INSERT_CONTENT_SQL, docs.map(contentRow));
35
- // Build prepares the FTS index after reconcile. This persisted mark commits with the content
36
- // change, so an interrupted build cannot expose stale lexical results.
37
- await markContentStale(conn);
166
+ // Keep document order and the existing insert/error semantics for added paths.
167
+ const changedDocs = docs.filter((doc)=>changed.has(doc.relPath));
168
+ await appendRows(conn, 'content', CONTENT_COLUMNS, INSERT_CONTENT_SQL, changedDocs.map(contentRow));
169
+ // Commit the stale mark with actual content changes. Identical rows leave prior staleness
170
+ // untouched, and FTS preparation still owns missing-index recovery.
171
+ if (removed.length > 0 || changedDocs.length > 0) await markContentStale(conn);
38
172
  }
39
173
  // DuckDB rejects more than one ALTER command per statement ("Parser Error: Only one ALTER
40
174
  // command per statement is supported", measured), so every name's clause joins into one string
@@ -53,5 +187,6 @@ export const duckdbDialect = {
53
187
  }
54
188
  },
55
189
  addColumns,
190
+ updateFrontmatter,
56
191
  reconcileContent
57
192
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/reconcile.ts"],"sourcesContent":["import { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { appendRows, quoteIdent } from '../shared.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport { markContentStale } from './lexical.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. `content` is a plain table, not FTS-virtual, so rows are maintained here\n// unconditionally; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter\n// column is VARIANT (holds mapValue()'s mixed JS types for one key across files).\n\n// No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):\n// `path` is content's own primary key, so this is a plain per-doc row.\nconst CONTENT_COLUMNS = ['path', 'title', 'summary', 'text'];\nconst INSERT_CONTENT_SQL = `INSERT INTO content (${CONTENT_COLUMNS.map(quoteIdent).join(', ')}) VALUES (?, ?, ?, ?)`;\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text];\n}\n\n// No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity fence anyway\n// so a runaway frontmatter generator fails with a clear message instead of an unbounded ALTER TABLE loop.\nconst MAX_FRONTMATTER_COLUMNS = 10_000;\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], _delta: ReconcileDelta): Promise<void> {\n if (touched.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n touched.map((p) => [p])\n );\n // The delete above cleared every touched path, and duckdb holds the cache file for its whole\n // connection, so no second writer can have landed one of these rows: nothing can conflict.\n await appendRows(conn, 'content', CONTENT_COLUMNS, INSERT_CONTENT_SQL, docs.map(contentRow));\n // Build prepares the FTS index after reconcile. This persisted mark commits with the content\n // change, so an interrupted build cannot expose stale lexical results.\n await markContentStale(conn);\n}\n\n// DuckDB rejects more than one ALTER command per statement (\"Parser Error: Only one ALTER\n// command per statement is supported\", measured), so every name's clause joins into one string\n// and runs as a single exec() -- one column-add \"leg\" through the driver instead of `names.length`.\n// VARIANT is the only type that can hold the mixed bigint/number/string/null shapes mapValue()\n// produces for one key across files.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n if (names.length === 0) return;\n await conn.exec(names.map((name) => `ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)} VARIANT`).join('; '));\n}\n\nexport const duckdbDialect: ReconcileDialect = {\n beginMode: () => 'BEGIN',\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${count} columns, crossing this store's sanity limit (${MAX_FRONTMATTER_COLUMNS}). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);\n }\n },\n addColumns,\n reconcileContent,\n};\n"],"names":["SenseError","appendRows","quoteIdent","markContentStale","CONTENT_COLUMNS","INSERT_CONTENT_SQL","map","join","contentRow","doc","relPath","search","title","summary","text","MAX_FRONTMATTER_COLUMNS","reconcileContent","conn","touched","docs","_delta","length","runBatch","p","addColumns","names","exec","name","duckdbDialect","beginMode","checkColumnLimit","count"],"mappings":"AAAA,SAASA,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,UAAU,EAAEC,UAAU,QAAQ,eAAe;AAEtD,SAASC,gBAAgB,QAAQ,eAAe;AAEhD,qFAAqF;AACrF,+FAA+F;AAC/F,4FAA4F;AAC5F,kFAAkF;AAElF,0FAA0F;AAC1F,uEAAuE;AACvE,MAAMC,kBAAkB;IAAC;IAAQ;IAAS;IAAW;CAAO;AAC5D,MAAMC,qBAAqB,CAAC,qBAAqB,EAAED,gBAAgBE,GAAG,CAACJ,YAAYK,IAAI,CAAC,MAAM,qBAAqB,CAAC;AAEpH,SAASC,WAAWC,GAAc;IAChC,OAAO;QAACA,IAAIC,OAAO;QAAED,IAAIE,MAAM,CAACC,KAAK;QAAEH,IAAIE,MAAM,CAACE,OAAO;QAAEJ,IAAIE,MAAM,CAACG,IAAI;KAAC;AAC7E;AAEA,0GAA0G;AAC1G,0GAA0G;AAC1G,MAAMC,0BAA0B;AAEhC,eAAeC,iBAAiBC,IAAgB,EAAEC,OAAiB,EAAEC,IAAiB,EAAEC,MAAsB;IAC5G,IAAIF,QAAQG,MAAM,GAAG,GACnB,MAAMJ,KAAKK,QAAQ,CACjB,wCACAJ,QAAQZ,GAAG,CAAC,CAACiB,IAAM;YAACA;SAAE;IAE1B,6FAA6F;IAC7F,2FAA2F;IAC3F,MAAMtB,WAAWgB,MAAM,WAAWb,iBAAiBC,oBAAoBc,KAAKb,GAAG,CAACE;IAChF,6FAA6F;IAC7F,uEAAuE;IACvE,MAAML,iBAAiBc;AACzB;AAEA,0FAA0F;AAC1F,+FAA+F;AAC/F,oGAAoG;AACpG,+FAA+F;AAC/F,qCAAqC;AACrC,eAAeO,WAAWP,IAAgB,EAAEQ,KAAe;IACzD,IAAIA,MAAMJ,MAAM,KAAK,GAAG;IACxB,MAAMJ,KAAKS,IAAI,CAACD,MAAMnB,GAAG,CAAC,CAACqB,OAAS,CAAC,mCAAmC,EAAEzB,WAAWyB,MAAM,QAAQ,CAAC,EAAEpB,IAAI,CAAC;AAC7G;AAEA,OAAO,MAAMqB,gBAAkC;IAC7CC,WAAW,IAAM;IACjBC,kBAAiBC,KAAK;QACpB,IAAIA,QAAQhB,yBAAyB;YACnC,MAAM,IAAIf,WAAW,gBAAgB,CAAC,uBAAuB,EAAE+B,MAAM,8CAA8C,EAAEhB,wBAAwB,gIAAgI,CAAC;QAChR;IACF;IACAS;IACAR;AACF,EAAE"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/reconcile.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBPreparedStatement, DuckDBValue } from '@duckdb/node-api';\nimport { SenseError } from '../../errors.ts';\nimport type { ReconcileDelta } from '../../features/types.ts';\nimport type { ParsedDoc } from '../../scan/index.ts';\nimport { appendRows, quoteIdent } from '../shared.ts';\nimport type { Connection, ReconcileDialect } from '../types.ts';\nimport type { DuckdbConnection } from './connection.ts';\nimport { markContentStale } from './lexical.ts';\nimport { duckdbApi } from './native.ts';\n\n// This store's dialect (types.ts's ReconcileDialect) for the shared orchestration in\n// store/reconcile.ts. `content` is a plain table, not FTS-virtual, so changed rows are maintained\n// here; DuckDB's ALTER TABLE needs a declared type, so every dynamic frontmatter\n// column is VARIANT (holds mapValue()'s mixed JS types for one key across files).\n\n// No rowid coupling needed (unlike sqlite's content, which links to frontmatter's rowid):\n// `path` is content's own primary key, so this is a plain per-doc row.\nconst CONTENT_COLUMNS = ['path', 'title', 'summary', 'text'];\nconst INSERT_CONTENT_SQL = `INSERT INTO content (${CONTENT_COLUMNS.map(quoteIdent).join(', ')}) VALUES (?, ?, ?, ?)`;\n// Bound the extra persisted text held for exact comparison, even during a full reparse.\nconst CONTENT_LOOKUP_CHUNK = 128;\n\ninterface ContentRow {\n path: string;\n title: string | null;\n summary: string | null;\n text: string | null;\n}\n\nfunction contentRow(doc: ParsedDoc): unknown[] {\n return [doc.relPath, doc.search.title, doc.search.summary, doc.search.text];\n}\n\n// No compile-time column cap in DuckDB (unlike SQLite's SQLITE_MAX_COLUMN); kept as a sanity fence anyway\n// so a runaway frontmatter generator fails with a clear message instead of an unbounded ALTER TABLE loop.\nconst MAX_FRONTMATTER_COLUMNS = 10_000;\nconst FRONTMATTER_UPDATE_CHUNK = 128;\n\nasync function withNativeStatement<T>(native: DuckDBConnection, sql: string, fn: (stmt: DuckDBPreparedStatement) => Promise<T>): Promise<T> {\n const stmt = await native.prepare(sql);\n const errors: unknown[] = [];\n let result!: T;\n try {\n result = await fn(stmt);\n } catch (err) {\n errors.push(err);\n }\n try {\n stmt.destroySync();\n } catch (err) {\n errors.push(err);\n }\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1) throw new AggregateError(errors, 'frontmatter statement and cleanup failed');\n return result;\n}\n\n// Wide writes amplify native commit allocations even for one changed stamp.\n// Skip only known, exactly equal values; uncertain representations still write.\nasync function updateFrontmatter(conn: Connection, writableColumns: string[], rows: unknown[][]): Promise<void> {\n if (rows.length === 0) return;\n const native = (conn as DuckdbConnection).duckdb;\n const { DuckDBVariantValue, DuckDBTypeId } = await duckdbApi();\n const physical = await withNativeStatement(native, 'PRAGMA table_info(frontmatter)', async (stmt) => {\n const reader = await stmt.runAndReadAll();\n return reader.getRowObjectsJS() as Array<{ name: string; type: string }>;\n });\n const writable = new Map(writableColumns.map((name, i) => [name, i]));\n const pathIndex = writable.get('path');\n if (pathIndex === undefined) throw new Error('frontmatter update requires a writable path column');\n const columnTypes = new Map(physical.map((column) => [column.name, column.type]));\n const insertSql = `INSERT INTO frontmatter (${writableColumns.map(quoteIdent).join(', ')}) VALUES (${writableColumns.map(() => '?').join(', ')})`;\n const sameValue = (old: DuckDBValue, value: unknown, column: string): boolean => {\n if (value === null || value === undefined) return old === null;\n if (columnTypes.get(column) !== 'VARIANT') return Object.is(old, value);\n if (!(old instanceof DuckDBVariantValue)) return false;\n const typeId = typeof value === 'string' ? DuckDBTypeId.VARCHAR : typeof value === 'bigint' ? DuckDBTypeId.HUGEINT : typeof value === 'number' && !Number.isInteger(value) ? DuckDBTypeId.DOUBLE : undefined;\n return typeId !== undefined && old.type?.typeId === typeId && Object.is(old.value, value);\n };\n for (let i = 0; i < rows.length; i += FRONTMATTER_UPDATE_CHUNK) {\n const chunk = rows.slice(i, i + FRONTMATTER_UPDATE_CHUNK);\n const paths = chunk.map((row) => row[pathIndex] as string);\n const placeholders = paths.map(() => '?').join(', ');\n const selected = writableColumns.map(quoteIdent).join(', ');\n const existing = await withNativeStatement(native, `SELECT ${selected} FROM frontmatter WHERE \"path\" IN (${placeholders})`, async (stmt) => {\n stmt.bind(paths);\n const reader = await stmt.runAndReadAll();\n // Native wrappers distinguish numeric tags and SQL NULL from VARIANT null.\n return new Map(reader.getRowObjects().map((row) => [row.path as string, row]));\n });\n const plans = chunk.map((row) => {\n const old = existing.get(row[pathIndex] as string);\n const changed = old ? writableColumns.flatMap((column, index) => (sameValue(old[column], row[index], column) ? [] : [index])) : [];\n return { row, missing: old === undefined, changed, signature: changed.join(',') };\n });\n for (let position = 0; position < plans.length; ) {\n const plan = plans[position];\n if (!plan.missing && plan.changed.length === 0) {\n ++position;\n continue;\n }\n let end = position + 1;\n while (end < plans.length && plans[end].missing === plan.missing && plans[end].signature === plan.signature) ++end;\n if (plan.missing) {\n await appendRows(\n conn,\n 'frontmatter',\n writableColumns,\n insertSql,\n plans.slice(position, end).map((repair) => repair.row)\n );\n } else {\n const assignments = plan.changed.map((index) => `${quoteIdent(writableColumns[index])} = ?`).join(', ');\n await withNativeStatement(native, `UPDATE frontmatter SET ${assignments} WHERE \"path\" = ?`, async (stmt) => {\n for (let rowIndex = position; rowIndex < end; ++rowIndex) {\n const row = plans[rowIndex].row;\n stmt.bind([...plan.changed.map((index) => row[index] ?? null), row[pathIndex]] as DuckDBValue[]);\n await stmt.run();\n }\n });\n }\n position = end;\n }\n }\n}\n\nasync function reconcileContent(conn: Connection, touched: string[], docs: ParsedDoc[], delta: ReconcileDelta): Promise<void> {\n const added = new Set(delta.added);\n const reparsed = docs.filter((doc) => !added.has(doc.relPath));\n const changed = new Set(delta.added);\n const removed: string[] = [];\n for (let i = 0; i < reparsed.length; i += CONTENT_LOOKUP_CHUNK) {\n const chunk = reparsed.slice(i, i + CONTENT_LOOKUP_CHUNK);\n const stmt = await conn.prepare(`SELECT \"path\", title, summary, text FROM content WHERE \"path\" IN (${chunk.map(() => '?').join(', ')})`);\n const rows = (await stmt.all(...chunk.map((doc) => doc.relPath))) as ContentRow[];\n const existing = new Map(rows.map((row) => [row.path, row]));\n for (const doc of chunk) {\n const row = existing.get(doc.relPath);\n if (row && row.title === doc.search.title && row.summary === doc.search.summary && row.text === doc.search.text) continue;\n changed.add(doc.relPath);\n if (row) removed.push(doc.relPath);\n }\n }\n\n const docPaths = new Set(docs.map((doc) => doc.relPath));\n const vanished = touched.filter((path) => !docPaths.has(path));\n for (let i = 0; i < vanished.length; i += CONTENT_LOOKUP_CHUNK) {\n const chunk = vanished.slice(i, i + CONTENT_LOOKUP_CHUNK);\n const stmt = await conn.prepare(`SELECT \"path\" FROM content WHERE \"path\" IN (${chunk.map(() => '?').join(', ')})`);\n const rows = (await stmt.all(...chunk)) as Array<{ path: string }>;\n removed.push(...rows.map((row) => row.path));\n }\n\n if (removed.length > 0)\n await conn.runBatch(\n 'DELETE FROM content WHERE \"path\" = ?',\n removed.map((p) => [p])\n );\n // Keep document order and the existing insert/error semantics for added paths.\n const changedDocs = docs.filter((doc) => changed.has(doc.relPath));\n await appendRows(conn, 'content', CONTENT_COLUMNS, INSERT_CONTENT_SQL, changedDocs.map(contentRow));\n // Commit the stale mark with actual content changes. Identical rows leave prior staleness\n // untouched, and FTS preparation still owns missing-index recovery.\n if (removed.length > 0 || changedDocs.length > 0) await markContentStale(conn);\n}\n\n// DuckDB rejects more than one ALTER command per statement (\"Parser Error: Only one ALTER\n// command per statement is supported\", measured), so every name's clause joins into one string\n// and runs as a single exec() -- one column-add \"leg\" through the driver instead of `names.length`.\n// VARIANT is the only type that can hold the mixed bigint/number/string/null shapes mapValue()\n// produces for one key across files.\nasync function addColumns(conn: Connection, names: string[]): Promise<void> {\n if (names.length === 0) return;\n await conn.exec(names.map((name) => `ALTER TABLE frontmatter ADD COLUMN ${quoteIdent(name)} VARIANT`).join('; '));\n}\n\nexport const duckdbDialect: ReconcileDialect = {\n beginMode: () => 'BEGIN',\n checkColumnLimit(count) {\n if (count > MAX_FRONTMATTER_COLUMNS) {\n throw new SenseError('COLUMN_LIMIT', `frontmatter would need ${count} columns, crossing this store's sanity limit (${MAX_FRONTMATTER_COLUMNS}). Narrow the presets' include globs so fewer/other files are indexed, or fix whatever is generating unbounded frontmatter keys.`);\n }\n },\n addColumns,\n updateFrontmatter,\n reconcileContent,\n};\n"],"names":["SenseError","appendRows","quoteIdent","markContentStale","duckdbApi","CONTENT_COLUMNS","INSERT_CONTENT_SQL","map","join","CONTENT_LOOKUP_CHUNK","contentRow","doc","relPath","search","title","summary","text","MAX_FRONTMATTER_COLUMNS","FRONTMATTER_UPDATE_CHUNK","withNativeStatement","native","sql","fn","stmt","prepare","errors","result","err","push","destroySync","length","AggregateError","updateFrontmatter","conn","writableColumns","rows","duckdb","DuckDBVariantValue","DuckDBTypeId","physical","reader","runAndReadAll","getRowObjectsJS","writable","Map","name","i","pathIndex","get","undefined","Error","columnTypes","column","type","insertSql","sameValue","old","value","Object","is","typeId","VARCHAR","HUGEINT","Number","isInteger","DOUBLE","chunk","slice","paths","row","placeholders","selected","existing","bind","getRowObjects","path","plans","changed","flatMap","index","missing","signature","position","plan","end","repair","assignments","rowIndex","run","reconcileContent","touched","docs","delta","added","Set","reparsed","filter","has","removed","all","add","docPaths","vanished","runBatch","p","changedDocs","addColumns","names","exec","duckdbDialect","beginMode","checkColumnLimit","count"],"mappings":"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"}
@@ -1,15 +1,25 @@
1
1
  // Decomposition of the `embed:` featureSignature segment (config/access.ts's featureSignature,
2
2
  // features/embed.ts's signature()), for open()'s narrow embed-invalidation routing.
3
- // One embed segment's provider/model/chunkVersion, ignoring any trailing `@identity`. `embed:off`
4
- // and any segment not in that exact shape return null, so the caller falls back to a full rebuild.
3
+ // One embed segment's provider/model/endpoint/chunkVersion, ignoring any trailing `@identity`.
4
+ // The endpoint-less HTTP shape remains readable so upgrading invalidates vectors, not core data.
5
5
  function parseEmbedSegment(sig) {
6
6
  const part = sig.split('|').find((p)=>p.startsWith('embed:'));
7
7
  if (part === undefined || part === 'embed:off') return null;
8
8
  const at = part.indexOf('@');
9
9
  const body = at === -1 ? part : part.slice(0, at);
10
- const match = /^embed:(static|openai|cohere):(.+):(chunk:v\d+(?::\d+)?)$/.exec(body);
11
- if (!match) return null;
12
- const [, provider, model, chunkVersion] = match;
10
+ const withEndpoint = /^embed:(openai|cohere):(.+):endpoint=([0-9a-f]{64}):(chunk:v\d+(?::\d+)?)$/.exec(body);
11
+ if (withEndpoint) {
12
+ const [, provider, model, endpoint, chunkVersion] = withEndpoint;
13
+ return {
14
+ provider,
15
+ model,
16
+ endpoint,
17
+ chunkVersion
18
+ };
19
+ }
20
+ const legacy = /^embed:(static|openai|cohere):(.+):(chunk:v\d+(?::\d+)?)$/.exec(body);
21
+ if (!legacy) return null;
22
+ const [, provider, model, chunkVersion] = legacy;
13
23
  return {
14
24
  provider,
15
25
  model,
@@ -23,6 +33,6 @@ export function classifyEmbedChange(before, after) {
23
33
  const a = parseEmbedSegment(after);
24
34
  if (b === null || a === null) return null;
25
35
  if (b.chunkVersion !== a.chunkVersion) return 'chunk';
26
- if (b.provider !== a.provider || b.model !== a.model) return 'model';
36
+ if (b.provider !== a.provider || b.model !== a.model || b.endpoint !== a.endpoint) return 'model';
27
37
  return null;
28
38
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/embed-scope.ts"],"sourcesContent":["// Decomposition of the `embed:` featureSignature segment (config/access.ts's featureSignature,\n// features/embed.ts's signature()), for open()'s narrow embed-invalidation routing.\n\nexport type EmbedChangeKind = 'model' | 'chunk';\n\ninterface EmbedSegment {\n provider: string;\n model: string;\n chunkVersion: string;\n}\n\n// One embed segment's provider/model/chunkVersion, ignoring any trailing `@identity`. `embed:off`\n// and any segment not in that exact shape return null, so the caller falls back to a full rebuild.\nfunction parseEmbedSegment(sig: string): EmbedSegment | null {\n const part = sig.split('|').find((p) => p.startsWith('embed:'));\n if (part === undefined || part === 'embed:off') return null;\n const at = part.indexOf('@');\n const body = at === -1 ? part : part.slice(0, at);\n const match = /^embed:(static|openai|cohere):(.+):(chunk:v\\d+(?::\\d+)?)$/.exec(body);\n if (!match) return null;\n const [, provider, model, chunkVersion] = match;\n return { provider, model, chunkVersion };\n}\n\n// 'chunk' when chunkTokens or the chunk version moved (boundaries changed, embeddings rebuild);\n// 'model' when only the provider or model moved (rows unchanged, values stale); null otherwise.\nexport function classifyEmbedChange(before: string, after: string): EmbedChangeKind | null {\n const b = parseEmbedSegment(before);\n const a = parseEmbedSegment(after);\n if (b === null || a === null) return null;\n if (b.chunkVersion !== a.chunkVersion) return 'chunk';\n if (b.provider !== a.provider || b.model !== a.model) return 'model';\n return null;\n}\n"],"names":["parseEmbedSegment","sig","part","split","find","p","startsWith","undefined","at","indexOf","body","slice","match","exec","provider","model","chunkVersion","classifyEmbedChange","before","after","b","a"],"mappings":"AAAA,+FAA+F;AAC/F,oFAAoF;AAUpF,kGAAkG;AAClG,mGAAmG;AACnG,SAASA,kBAAkBC,GAAW;IACpC,MAAMC,OAAOD,IAAIE,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,IAAMA,EAAEC,UAAU,CAAC;IACrD,IAAIJ,SAASK,aAAaL,SAAS,aAAa,OAAO;IACvD,MAAMM,KAAKN,KAAKO,OAAO,CAAC;IACxB,MAAMC,OAAOF,OAAO,CAAC,IAAIN,OAAOA,KAAKS,KAAK,CAAC,GAAGH;IAC9C,MAAMI,QAAQ,4DAA4DC,IAAI,CAACH;IAC/E,IAAI,CAACE,OAAO,OAAO;IACnB,MAAM,GAAGE,UAAUC,OAAOC,aAAa,GAAGJ;IAC1C,OAAO;QAAEE;QAAUC;QAAOC;IAAa;AACzC;AAEA,gGAAgG;AAChG,gGAAgG;AAChG,OAAO,SAASC,oBAAoBC,MAAc,EAAEC,KAAa;IAC/D,MAAMC,IAAIpB,kBAAkBkB;IAC5B,MAAMG,IAAIrB,kBAAkBmB;IAC5B,IAAIC,MAAM,QAAQC,MAAM,MAAM,OAAO;IACrC,IAAID,EAAEJ,YAAY,KAAKK,EAAEL,YAAY,EAAE,OAAO;IAC9C,IAAII,EAAEN,QAAQ,KAAKO,EAAEP,QAAQ,IAAIM,EAAEL,KAAK,KAAKM,EAAEN,KAAK,EAAE,OAAO;IAC7D,OAAO;AACT"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/embed-scope.ts"],"sourcesContent":["// Decomposition of the `embed:` featureSignature segment (config/access.ts's featureSignature,\n// features/embed.ts's signature()), for open()'s narrow embed-invalidation routing.\n\nexport type EmbedChangeKind = 'model' | 'chunk';\n\ninterface EmbedSegment {\n provider: string;\n model: string;\n endpoint?: string;\n chunkVersion: string;\n}\n\n// One embed segment's provider/model/endpoint/chunkVersion, ignoring any trailing `@identity`.\n// The endpoint-less HTTP shape remains readable so upgrading invalidates vectors, not core data.\nfunction parseEmbedSegment(sig: string): EmbedSegment | null {\n const part = sig.split('|').find((p) => p.startsWith('embed:'));\n if (part === undefined || part === 'embed:off') return null;\n const at = part.indexOf('@');\n const body = at === -1 ? part : part.slice(0, at);\n const withEndpoint = /^embed:(openai|cohere):(.+):endpoint=([0-9a-f]{64}):(chunk:v\\d+(?::\\d+)?)$/.exec(body);\n if (withEndpoint) {\n const [, provider, model, endpoint, chunkVersion] = withEndpoint;\n return { provider, model, endpoint, chunkVersion };\n }\n const legacy = /^embed:(static|openai|cohere):(.+):(chunk:v\\d+(?::\\d+)?)$/.exec(body);\n if (!legacy) return null;\n const [, provider, model, chunkVersion] = legacy;\n return { provider, model, chunkVersion };\n}\n\n// 'chunk' when chunkTokens or the chunk version moved (boundaries changed, embeddings rebuild);\n// 'model' when only the provider or model moved (rows unchanged, values stale); null otherwise.\nexport function classifyEmbedChange(before: string, after: string): EmbedChangeKind | null {\n const b = parseEmbedSegment(before);\n const a = parseEmbedSegment(after);\n if (b === null || a === null) return null;\n if (b.chunkVersion !== a.chunkVersion) return 'chunk';\n if (b.provider !== a.provider || b.model !== a.model || b.endpoint !== a.endpoint) return 'model';\n return null;\n}\n"],"names":["parseEmbedSegment","sig","part","split","find","p","startsWith","undefined","at","indexOf","body","slice","withEndpoint","exec","provider","model","endpoint","chunkVersion","legacy","classifyEmbedChange","before","after","b","a"],"mappings":"AAAA,+FAA+F;AAC/F,oFAAoF;AAWpF,+FAA+F;AAC/F,iGAAiG;AACjG,SAASA,kBAAkBC,GAAW;IACpC,MAAMC,OAAOD,IAAIE,KAAK,CAAC,KAAKC,IAAI,CAAC,CAACC,IAAMA,EAAEC,UAAU,CAAC;IACrD,IAAIJ,SAASK,aAAaL,SAAS,aAAa,OAAO;IACvD,MAAMM,KAAKN,KAAKO,OAAO,CAAC;IACxB,MAAMC,OAAOF,OAAO,CAAC,IAAIN,OAAOA,KAAKS,KAAK,CAAC,GAAGH;IAC9C,MAAMI,eAAe,6EAA6EC,IAAI,CAACH;IACvG,IAAIE,cAAc;QAChB,MAAM,GAAGE,UAAUC,OAAOC,UAAUC,aAAa,GAAGL;QACpD,OAAO;YAAEE;YAAUC;YAAOC;YAAUC;QAAa;IACnD;IACA,MAAMC,SAAS,4DAA4DL,IAAI,CAACH;IAChF,IAAI,CAACQ,QAAQ,OAAO;IACpB,MAAM,GAAGJ,UAAUC,OAAOE,aAAa,GAAGC;IAC1C,OAAO;QAAEJ;QAAUC;QAAOE;IAAa;AACzC;AAEA,gGAAgG;AAChG,gGAAgG;AAChG,OAAO,SAASE,oBAAoBC,MAAc,EAAEC,KAAa;IAC/D,MAAMC,IAAItB,kBAAkBoB;IAC5B,MAAMG,IAAIvB,kBAAkBqB;IAC5B,IAAIC,MAAM,QAAQC,MAAM,MAAM,OAAO;IACrC,IAAID,EAAEL,YAAY,KAAKM,EAAEN,YAAY,EAAE,OAAO;IAC9C,IAAIK,EAAER,QAAQ,KAAKS,EAAET,QAAQ,IAAIQ,EAAEP,KAAK,KAAKQ,EAAER,KAAK,IAAIO,EAAEN,QAAQ,KAAKO,EAAEP,QAAQ,EAAE,OAAO;IAC1F,OAAO;AACT"}
@@ -3,13 +3,18 @@ import { clearCache } from './cache.js';
3
3
  import type { InternalOpenOptions, OpenOptions, OpenResult } from './open.js';
4
4
  import type { Stages } from './stages.js';
5
5
  export declare function openStoreFor(cfg: ResolvedConfig, options: InternalOpenOptions): Promise<OpenResult>;
6
+ /** Opens a store through the root `open` API, preparing capabilities unless `build` is `false`. */
6
7
  export declare function openStore(cfg: ResolvedConfig, options?: OpenOptions): Promise<OpenResult>;
7
8
  export declare function lexicalReadiness(cfg: ResolvedConfig): Promise<{
8
9
  ready: boolean;
9
10
  error: string | null;
10
11
  }>;
12
+ /** Options for the root `build(config, options)` API. */
11
13
  export interface BuildOptions {
14
+ /** Recreates the derived cache before building. */
12
15
  force?: boolean;
16
+ /** Cancels supported provider I/O and stops between phases after active native work finishes. */
17
+ signal?: AbortSignal;
13
18
  }
14
19
  export interface BuildResult {
15
20
  cfg: ResolvedConfig;
@@ -18,6 +23,7 @@ export interface BuildResult {
18
23
  warnings: string[];
19
24
  stages: Stages;
20
25
  }
26
+ /** Builds every configured capability, waiting for active native work and cleanup before settling. */
21
27
  export declare function buildIndex(cfg: ResolvedConfig, options?: BuildOptions): Promise<BuildResult>;
22
28
  export declare function storeFilename(cfg: ResolvedConfig): string;
23
29
  export type { OpenOptions, OpenResult } from './open.js';
@@ -47,19 +47,22 @@ function validateRequirements(cfg, entry, requirements) {
47
47
  }
48
48
  export async function openStoreFor(cfg, options) {
49
49
  var _options_requirements;
50
+ var _options_signal;
51
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
50
52
  const entry = entryFor(storeName(cfg));
51
53
  validateRequirements(cfg, entry, (_options_requirements = options.requirements) !== null && _options_requirements !== void 0 ? _options_requirements : new Set([
52
54
  'core'
53
55
  ]));
54
56
  return entry.open(cfg, options);
55
57
  }
56
- export async function openStore(cfg, options = {}) {
58
+ /** Opens a store through the root `open` API, preparing capabilities unless `build` is `false`. */ export async function openStore(cfg, options = {}) {
57
59
  const buildEnabled = options.build !== false;
58
60
  return openStoreFor(cfg, {
59
61
  build: buildEnabled,
60
62
  requirements: buildEnabled ? requirementsFor(cfg) : new Set([
61
63
  'core'
62
- ])
64
+ ]),
65
+ signal: options.signal
63
66
  });
64
67
  }
65
68
  export async function lexicalReadiness(cfg) {
@@ -88,11 +91,14 @@ export async function lexicalReadiness(cfg) {
88
91
  await opened.store.close();
89
92
  }
90
93
  }
91
- export async function buildIndex(cfg, options = {}) {
94
+ /** Builds every configured capability, waiting for active native work and cleanup before settling. */ export async function buildIndex(cfg, options = {}) {
95
+ var _options_signal;
96
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
92
97
  if (options.force) clearCache(cfg);
93
98
  const opened = await openStoreFor(cfg, {
94
99
  build: true,
95
- requirements: requirementsFor(cfg)
100
+ requirements: requirementsFor(cfg),
101
+ signal: options.signal
96
102
  });
97
103
  try {
98
104
  const { store: _store, ...result } = opened;