sensemaking 0.25.0 → 0.26.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 (316) hide show
  1. package/README.md +14 -10
  2. package/dist/cjs/cli/index.d.cts +1 -1
  3. package/dist/cjs/cli/index.d.ts +1 -1
  4. package/dist/cjs/cli/index.js +1 -1
  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/related.js +4 -3
  9. package/dist/cjs/cli/related.js.map +1 -1
  10. package/dist/cjs/commands/index.d.cts +6 -3
  11. package/dist/cjs/commands/index.d.ts +6 -3
  12. package/dist/cjs/commands/index.js +4 -0
  13. package/dist/cjs/commands/index.js.map +1 -1
  14. package/dist/cjs/commands/map.d.cts +14 -5
  15. package/dist/cjs/commands/map.d.ts +14 -5
  16. package/dist/cjs/commands/map.js +42 -35
  17. package/dist/cjs/commands/map.js.map +1 -1
  18. package/dist/cjs/commands/path.d.cts +14 -0
  19. package/dist/cjs/commands/path.d.ts +14 -0
  20. package/dist/cjs/commands/path.js +209 -0
  21. package/dist/cjs/commands/path.js.map +1 -0
  22. package/dist/cjs/commands/peek.d.cts +16 -1
  23. package/dist/cjs/commands/peek.d.ts +16 -1
  24. package/dist/cjs/commands/peek.js +71 -33
  25. package/dist/cjs/commands/peek.js.map +1 -1
  26. package/dist/cjs/commands/related.d.cts +16 -3
  27. package/dist/cjs/commands/related.d.ts +16 -3
  28. package/dist/cjs/commands/related.js +131 -98
  29. package/dist/cjs/commands/related.js.map +1 -1
  30. package/dist/cjs/commands/scope.d.cts +1 -0
  31. package/dist/cjs/commands/scope.d.ts +1 -0
  32. package/dist/cjs/commands/scope.js +149 -0
  33. package/dist/cjs/commands/scope.js.map +1 -1
  34. package/dist/cjs/commands/search.d.cts +26 -3
  35. package/dist/cjs/commands/search.d.ts +26 -3
  36. package/dist/cjs/commands/search.js +211 -71
  37. package/dist/cjs/commands/search.js.map +1 -1
  38. package/dist/cjs/commands/signals.d.cts +4 -3
  39. package/dist/cjs/commands/signals.d.ts +4 -3
  40. package/dist/cjs/commands/signals.js +13 -4
  41. package/dist/cjs/commands/signals.js.map +1 -1
  42. package/dist/cjs/commands/snapshot.d.cts +3 -0
  43. package/dist/cjs/commands/snapshot.d.ts +3 -0
  44. package/dist/cjs/commands/snapshot.js +180 -0
  45. package/dist/cjs/commands/snapshot.js.map +1 -0
  46. package/dist/cjs/embed/cohere.js +41 -16
  47. package/dist/cjs/embed/cohere.js.map +1 -1
  48. package/dist/cjs/embed/endpoint.d.cts +2 -0
  49. package/dist/cjs/embed/endpoint.d.ts +2 -0
  50. package/dist/cjs/embed/endpoint.js +30 -0
  51. package/dist/cjs/embed/endpoint.js.map +1 -0
  52. package/dist/cjs/embed/handoff.d.cts +7 -2
  53. package/dist/cjs/embed/handoff.d.ts +7 -2
  54. package/dist/cjs/embed/handoff.js +10 -0
  55. package/dist/cjs/embed/handoff.js.map +1 -1
  56. package/dist/cjs/embed/http.d.cts +18 -1
  57. package/dist/cjs/embed/http.d.ts +18 -1
  58. package/dist/cjs/embed/http.js +239 -40
  59. package/dist/cjs/embed/http.js.map +1 -1
  60. package/dist/cjs/embed/openai.js +39 -17
  61. package/dist/cjs/embed/openai.js.map +1 -1
  62. package/dist/cjs/embed/query.d.cts +9 -3
  63. package/dist/cjs/embed/query.d.ts +9 -3
  64. package/dist/cjs/embed/query.js +257 -90
  65. package/dist/cjs/embed/query.js.map +1 -1
  66. package/dist/cjs/embed/registry.d.cts +2 -2
  67. package/dist/cjs/embed/registry.d.ts +2 -2
  68. package/dist/cjs/embed/registry.js +36 -4
  69. package/dist/cjs/embed/registry.js.map +1 -1
  70. package/dist/cjs/embed/static.js +6 -2
  71. package/dist/cjs/embed/static.js.map +1 -1
  72. package/dist/cjs/embed/store.js +42 -77
  73. package/dist/cjs/embed/store.js.map +1 -1
  74. package/dist/cjs/embed/types.d.cts +5 -2
  75. package/dist/cjs/embed/types.d.ts +5 -2
  76. package/dist/cjs/embed/types.js.map +1 -1
  77. package/dist/cjs/features/embed.js +290 -43
  78. package/dist/cjs/features/embed.js.map +1 -1
  79. package/dist/cjs/graph/traverse.d.cts +2 -2
  80. package/dist/cjs/graph/traverse.d.ts +2 -2
  81. package/dist/cjs/graph/traverse.js +15 -5
  82. package/dist/cjs/graph/traverse.js.map +1 -1
  83. package/dist/cjs/index.d.cts +2 -2
  84. package/dist/cjs/index.d.ts +2 -2
  85. package/dist/cjs/index.js +6 -0
  86. package/dist/cjs/index.js.map +1 -1
  87. package/dist/cjs/output/output.d.cts +4 -36
  88. package/dist/cjs/output/output.d.ts +4 -36
  89. package/dist/cjs/output/output.js +1 -2
  90. package/dist/cjs/output/output.js.map +1 -1
  91. package/dist/cjs/scan/pool.d.cts +2 -1
  92. package/dist/cjs/scan/pool.d.ts +2 -1
  93. package/dist/cjs/scan/pool.js +116 -27
  94. package/dist/cjs/scan/pool.js.map +1 -1
  95. package/dist/cjs/scan/reparse.d.cts +1 -0
  96. package/dist/cjs/scan/reparse.d.ts +1 -0
  97. package/dist/cjs/scan/reparse.js +75 -21
  98. package/dist/cjs/scan/reparse.js.map +1 -1
  99. package/dist/cjs/store/builder.d.cts +11 -21
  100. package/dist/cjs/store/builder.d.ts +11 -21
  101. package/dist/cjs/store/builder.js +1077 -438
  102. package/dist/cjs/store/builder.js.map +1 -1
  103. package/dist/cjs/store/duckdb/connection.js +16 -2
  104. package/dist/cjs/store/duckdb/connection.js.map +1 -1
  105. package/dist/cjs/store/duckdb/open.d.cts +1 -1
  106. package/dist/cjs/store/duckdb/open.d.ts +1 -1
  107. package/dist/cjs/store/duckdb/open.js +73 -65
  108. package/dist/cjs/store/duckdb/open.js.map +1 -1
  109. package/dist/cjs/store/duckdb/reconcile.js +533 -15
  110. package/dist/cjs/store/duckdb/reconcile.js.map +1 -1
  111. package/dist/cjs/store/embed-scope.js +19 -9
  112. package/dist/cjs/store/embed-scope.js.map +1 -1
  113. package/dist/cjs/store/index.d.cts +6 -0
  114. package/dist/cjs/store/index.d.ts +6 -0
  115. package/dist/cjs/store/index.js +8 -4
  116. package/dist/cjs/store/index.js.map +1 -1
  117. package/dist/cjs/store/open.d.cts +6 -1
  118. package/dist/cjs/store/open.d.ts +6 -1
  119. package/dist/cjs/store/open.js +230 -307
  120. package/dist/cjs/store/open.js.map +1 -1
  121. package/dist/cjs/store/readiness.d.cts +5 -0
  122. package/dist/cjs/store/readiness.d.ts +5 -0
  123. package/dist/cjs/store/readiness.js +39 -0
  124. package/dist/cjs/store/readiness.js.map +1 -0
  125. package/dist/cjs/store/reconcile.d.cts +26 -1
  126. package/dist/cjs/store/reconcile.d.ts +26 -1
  127. package/dist/cjs/store/reconcile.js +670 -576
  128. package/dist/cjs/store/reconcile.js.map +1 -1
  129. package/dist/cjs/store/signature.d.cts +1 -0
  130. package/dist/cjs/store/signature.d.ts +1 -0
  131. package/dist/cjs/store/signature.js +9 -0
  132. package/dist/cjs/store/signature.js.map +1 -1
  133. package/dist/cjs/store/sqlite/open.d.cts +1 -1
  134. package/dist/cjs/store/sqlite/open.d.ts +1 -1
  135. package/dist/cjs/store/sqlite/open.js +84 -90
  136. package/dist/cjs/store/sqlite/open.js.map +1 -1
  137. package/dist/cjs/store/sqlite/reconcile.js.map +1 -1
  138. package/dist/cjs/store/sqlite/store.js +63 -60
  139. package/dist/cjs/store/sqlite/store.js.map +1 -1
  140. package/dist/cjs/store/sqlite/vectors.d.cts +2 -2
  141. package/dist/cjs/store/sqlite/vectors.d.ts +2 -2
  142. package/dist/cjs/store/sqlite/vectors.js +8 -4
  143. package/dist/cjs/store/sqlite/vectors.js.map +1 -1
  144. package/dist/cjs/store/stages.js.map +1 -1
  145. package/dist/cjs/store/turso/connection.d.cts +1 -0
  146. package/dist/cjs/store/turso/connection.d.ts +1 -0
  147. package/dist/cjs/store/turso/connection.js +319 -94
  148. package/dist/cjs/store/turso/connection.js.map +1 -1
  149. package/dist/cjs/store/turso/open.d.cts +1 -1
  150. package/dist/cjs/store/turso/open.d.ts +1 -1
  151. package/dist/cjs/store/turso/open.js +70 -74
  152. package/dist/cjs/store/turso/open.js.map +1 -1
  153. package/dist/cjs/store/turso/store.js +25 -23
  154. package/dist/cjs/store/turso/store.js.map +1 -1
  155. package/dist/cjs/store/turso/vectors.d.cts +2 -2
  156. package/dist/cjs/store/turso/vectors.d.ts +2 -2
  157. package/dist/cjs/store/turso/vectors.js +26 -16
  158. package/dist/cjs/store/turso/vectors.js.map +1 -1
  159. package/dist/cjs/store/types.d.cts +4 -1
  160. package/dist/cjs/store/types.d.ts +4 -1
  161. package/dist/cjs/store/types.js.map +1 -1
  162. package/dist/cjs/store/vectors.d.cts +14 -1
  163. package/dist/cjs/store/vectors.d.ts +14 -1
  164. package/dist/cjs/store/vectors.js +61 -0
  165. package/dist/cjs/store/vectors.js.map +1 -1
  166. package/dist/cjs/watch.d.cts +6 -0
  167. package/dist/cjs/watch.d.ts +6 -0
  168. package/dist/cjs/watch.js +42 -10
  169. package/dist/cjs/watch.js.map +1 -1
  170. package/dist/cjs/workers/parse.d.cts +4 -3
  171. package/dist/cjs/workers/parse.d.ts +4 -3
  172. package/dist/cjs/workers/parse.js +19 -17
  173. package/dist/cjs/workers/parse.js.map +1 -1
  174. package/dist/esm/cli/index.d.ts +1 -1
  175. package/dist/esm/cli/index.js +1 -1
  176. package/dist/esm/cli/index.js.map +1 -1
  177. package/dist/esm/cli/path.js +4 -10
  178. package/dist/esm/cli/path.js.map +1 -1
  179. package/dist/esm/cli/related.js +5 -3
  180. package/dist/esm/cli/related.js.map +1 -1
  181. package/dist/esm/commands/index.d.ts +6 -3
  182. package/dist/esm/commands/index.js +1 -0
  183. package/dist/esm/commands/index.js.map +1 -1
  184. package/dist/esm/commands/map.d.ts +14 -5
  185. package/dist/esm/commands/map.js +2 -0
  186. package/dist/esm/commands/map.js.map +1 -1
  187. package/dist/esm/commands/path.d.ts +14 -0
  188. package/dist/esm/commands/path.js +33 -0
  189. package/dist/esm/commands/path.js.map +1 -0
  190. package/dist/esm/commands/peek.d.ts +16 -1
  191. package/dist/esm/commands/peek.js +35 -9
  192. package/dist/esm/commands/peek.js.map +1 -1
  193. package/dist/esm/commands/related.d.ts +16 -3
  194. package/dist/esm/commands/related.js +45 -28
  195. package/dist/esm/commands/related.js.map +1 -1
  196. package/dist/esm/commands/scope.d.ts +1 -0
  197. package/dist/esm/commands/scope.js +58 -0
  198. package/dist/esm/commands/scope.js.map +1 -1
  199. package/dist/esm/commands/search.d.ts +26 -3
  200. package/dist/esm/commands/search.js +85 -18
  201. package/dist/esm/commands/search.js.map +1 -1
  202. package/dist/esm/commands/signals.d.ts +4 -3
  203. package/dist/esm/commands/signals.js +11 -4
  204. package/dist/esm/commands/signals.js.map +1 -1
  205. package/dist/esm/commands/snapshot.d.ts +3 -0
  206. package/dist/esm/commands/snapshot.js +25 -0
  207. package/dist/esm/commands/snapshot.js.map +1 -0
  208. package/dist/esm/embed/cohere.js +35 -10
  209. package/dist/esm/embed/cohere.js.map +1 -1
  210. package/dist/esm/embed/endpoint.d.ts +2 -0
  211. package/dist/esm/embed/endpoint.js +11 -0
  212. package/dist/esm/embed/endpoint.js.map +1 -0
  213. package/dist/esm/embed/handoff.d.ts +7 -2
  214. package/dist/esm/embed/handoff.js +7 -0
  215. package/dist/esm/embed/handoff.js.map +1 -1
  216. package/dist/esm/embed/http.d.ts +18 -1
  217. package/dist/esm/embed/http.js +137 -17
  218. package/dist/esm/embed/http.js.map +1 -1
  219. package/dist/esm/embed/openai.js +34 -12
  220. package/dist/esm/embed/openai.js.map +1 -1
  221. package/dist/esm/embed/query.d.ts +9 -3
  222. package/dist/esm/embed/query.js +98 -26
  223. package/dist/esm/embed/query.js.map +1 -1
  224. package/dist/esm/embed/registry.d.ts +2 -2
  225. package/dist/esm/embed/registry.js +34 -5
  226. package/dist/esm/embed/registry.js.map +1 -1
  227. package/dist/esm/embed/static.js +10 -2
  228. package/dist/esm/embed/static.js.map +1 -1
  229. package/dist/esm/embed/store.js +30 -21
  230. package/dist/esm/embed/store.js.map +1 -1
  231. package/dist/esm/embed/types.d.ts +5 -2
  232. package/dist/esm/embed/types.js.map +1 -1
  233. package/dist/esm/features/embed.js +82 -21
  234. package/dist/esm/features/embed.js.map +1 -1
  235. package/dist/esm/graph/traverse.d.ts +2 -2
  236. package/dist/esm/graph/traverse.js +4 -2
  237. package/dist/esm/graph/traverse.js.map +1 -1
  238. package/dist/esm/index.d.ts +2 -2
  239. package/dist/esm/index.js +1 -1
  240. package/dist/esm/index.js.map +1 -1
  241. package/dist/esm/output/output.d.ts +4 -36
  242. package/dist/esm/output/output.js +1 -2
  243. package/dist/esm/output/output.js.map +1 -1
  244. package/dist/esm/scan/pool.d.ts +2 -1
  245. package/dist/esm/scan/pool.js +80 -23
  246. package/dist/esm/scan/pool.js.map +1 -1
  247. package/dist/esm/scan/reparse.d.ts +1 -0
  248. package/dist/esm/scan/reparse.js +40 -12
  249. package/dist/esm/scan/reparse.js.map +1 -1
  250. package/dist/esm/store/builder.d.ts +11 -21
  251. package/dist/esm/store/builder.js +337 -133
  252. package/dist/esm/store/builder.js.map +1 -1
  253. package/dist/esm/store/duckdb/connection.js +9 -1
  254. package/dist/esm/store/duckdb/connection.js.map +1 -1
  255. package/dist/esm/store/duckdb/open.d.ts +1 -1
  256. package/dist/esm/store/duckdb/open.js +13 -9
  257. package/dist/esm/store/duckdb/open.js.map +1 -1
  258. package/dist/esm/store/duckdb/reconcile.js +145 -10
  259. package/dist/esm/store/duckdb/reconcile.js.map +1 -1
  260. package/dist/esm/store/embed-scope.js +16 -6
  261. package/dist/esm/store/embed-scope.js.map +1 -1
  262. package/dist/esm/store/index.d.ts +6 -0
  263. package/dist/esm/store/index.js +10 -4
  264. package/dist/esm/store/index.js.map +1 -1
  265. package/dist/esm/store/open.d.ts +6 -1
  266. package/dist/esm/store/open.js +77 -87
  267. package/dist/esm/store/open.js.map +1 -1
  268. package/dist/esm/store/readiness.d.ts +5 -0
  269. package/dist/esm/store/readiness.js +11 -0
  270. package/dist/esm/store/readiness.js.map +1 -0
  271. package/dist/esm/store/reconcile.d.ts +26 -1
  272. package/dist/esm/store/reconcile.js +142 -120
  273. package/dist/esm/store/reconcile.js.map +1 -1
  274. package/dist/esm/store/signature.d.ts +1 -0
  275. package/dist/esm/store/signature.js +8 -0
  276. package/dist/esm/store/signature.js.map +1 -1
  277. package/dist/esm/store/sqlite/open.d.ts +1 -1
  278. package/dist/esm/store/sqlite/open.js +12 -12
  279. package/dist/esm/store/sqlite/open.js.map +1 -1
  280. package/dist/esm/store/sqlite/reconcile.js +2 -2
  281. package/dist/esm/store/sqlite/reconcile.js.map +1 -1
  282. package/dist/esm/store/sqlite/store.js +21 -18
  283. package/dist/esm/store/sqlite/store.js.map +1 -1
  284. package/dist/esm/store/sqlite/vectors.d.ts +2 -2
  285. package/dist/esm/store/sqlite/vectors.js +7 -5
  286. package/dist/esm/store/sqlite/vectors.js.map +1 -1
  287. package/dist/esm/store/stages.js.map +1 -1
  288. package/dist/esm/store/turso/connection.d.ts +1 -0
  289. package/dist/esm/store/turso/connection.js +97 -28
  290. package/dist/esm/store/turso/connection.js.map +1 -1
  291. package/dist/esm/store/turso/open.d.ts +1 -1
  292. package/dist/esm/store/turso/open.js +9 -8
  293. package/dist/esm/store/turso/open.js.map +1 -1
  294. package/dist/esm/store/turso/store.js +16 -14
  295. package/dist/esm/store/turso/store.js.map +1 -1
  296. package/dist/esm/store/turso/vectors.d.ts +2 -2
  297. package/dist/esm/store/turso/vectors.js +27 -17
  298. package/dist/esm/store/turso/vectors.js.map +1 -1
  299. package/dist/esm/store/types.d.ts +4 -1
  300. package/dist/esm/store/types.js.map +1 -1
  301. package/dist/esm/store/vectors.d.ts +14 -1
  302. package/dist/esm/store/vectors.js +55 -0
  303. package/dist/esm/store/vectors.js.map +1 -1
  304. package/dist/esm/watch.d.ts +6 -0
  305. package/dist/esm/watch.js +40 -14
  306. package/dist/esm/watch.js.map +1 -1
  307. package/dist/esm/workers/parse.d.ts +4 -3
  308. package/dist/esm/workers/parse.js +19 -17
  309. package/dist/esm/workers/parse.js.map +1 -1
  310. package/package.json +1 -1
  311. package/skills/sense/SKILL.md +4 -2
  312. package/skills/sense/references/stores/duckdb.md +1 -1
  313. package/skills/sense-setup/SKILL.md +2 -2
  314. package/skills/sense-setup/references/embeddings.md +1 -1
  315. package/skills/sense-setup/references/store-benchmarks.md +5 -5
  316. package/skills/sense-setup/references/stores/duckdb.md +1 -1
@@ -4,25 +4,26 @@ import { localModelMissing, MODEL_FILENAMES } from '../embed/store.js';
4
4
  import { SenseError } from '../errors.js';
5
5
  import { serialQuery } from '../lib/serial-query.js';
6
6
  import { resolveNote } from './peek.js';
7
- import { scopedPaths, scopeHasEmbeddings } from './scope.js';
7
+ import { scopedPaths, scopeHasEmbeddings, withMaterializedVectorScope } from './scope.js';
8
+ import { assertQuerySnapshot } from './snapshot.js';
9
+ export const RELATED_DEFAULT_K = 5;
8
10
  export async function resolveRelatedSeed(store, pathArg) {
9
11
  const paths = (await (await store.prepare('SELECT "path" FROM frontmatter')).all()).map((row)=>row.path);
10
12
  return resolveNote(paths, pathArg);
11
13
  }
12
- // Notes most similar by cosine, excluding self and everything already linked either way.
13
- // Its own command, not a peek section: a full embeddings scan is ~480ms at 26k notes.
14
- export function relatedNotes(store, cfg, pathArg, overrides, k) {
15
- return serialQuery(store, ()=>relatedIndexed(store, cfg, pathArg, overrides, k));
14
+ /**
15
+ * Returns the notes most similar to an indexed seed, excluding the seed and notes linked either
16
+ * way. The seed resolves across the indexed tree even when it is outside the candidate scope.
17
+ */ export function relatedNotes(store, cfg, pathArg, options = {}) {
18
+ return serialQuery(store, ()=>{
19
+ var _options_k;
20
+ return relatedIndexed(store, cfg, pathArg, options, (_options_k = options.k) !== null && _options_k !== void 0 ? _options_k : RELATED_DEFAULT_K);
21
+ });
16
22
  }
17
23
  async function relatedIndexed(store, cfg, pathArg, overrides, k) {
18
- const path = await resolveRelatedSeed(store, pathArg);
19
- const outbound = (await (await store.prepare('SELECT DISTINCT dst FROM links WHERE src = ? AND dst IS NOT NULL AND dst != src')).all(path)).map((r)=>r.dst);
20
- const backlinks = (await (await store.prepare('SELECT DISTINCT src FROM links WHERE dst = ? AND src != dst')).all(path)).map((r)=>r.src);
21
- const exclude = new Set([
22
- path,
23
- ...outbound,
24
- ...backlinks
25
- ]);
24
+ if (!Number.isFinite(k) || !Number.isInteger(k) || k <= 0) {
25
+ throw new SenseError('SEARCH_OPTION_INVALID', `related option "k" must be a positive finite integer, got ${String(k)}`);
26
+ }
26
27
  // Vectors are the only signal `related` has, so every way of not having them is an error
27
28
  // naming the cause; an empty result means one thing, nothing near in meaning.
28
29
  const effective = resolveSearch(cfg, overrides);
@@ -40,20 +41,36 @@ async function relatedIndexed(store, cfg, pathArg, overrides, k) {
40
41
  if (localModelMissing(e)) {
41
42
  throw new SenseError('EMBED_MODEL_MISSING', `related ranks notes by meaning, so it needs the embedding model, but the local model path "${e.model}" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them (search still works without it, on words and links)`);
42
43
  }
43
- const allowed = await scopedPaths(store, cfg, overrides);
44
- // The seed may sit outside a narrowed candidate scope; both it and every eligible result must
45
- // be ready, while pending vectors in unrelated notes do not block this operation.
46
- await ensureDocumentEmbeddings(store, cfg, new Set([
47
- ...allowed,
48
- path
49
- ]));
50
- if (!await hasEmbedding(store, path)) {
51
- throw new SenseError('NOTE_NOT_EMBEDDED', `${path} has no indexed text to compare -- a note that is frontmatter only, or empty, has nothing to rank by meaning`);
52
- }
53
- if (!await scopeHasEmbeddings(store, cfg, allowed)) return [];
54
- return similarNotes(store, cfg, path, {
55
- exclude,
56
- allowed,
57
- k
44
+ return store.transaction(async ()=>{
45
+ await assertQuerySnapshot(store, cfg);
46
+ // Repeat effective preset validation inside the snapshot that supplies its durable scope.
47
+ const current = resolveSearch(cfg, overrides);
48
+ if (current.signals.vectors === undefined) {
49
+ throw new SenseError('PRESET_NOT_SEMANTIC', `preset "${current.presetName}" has no "vectors" signal, so this scope has no vectors and related has no other signal; search it instead (words and links), or add "vectors" to that preset's signals`);
50
+ }
51
+ const path = await resolveRelatedSeed(store, pathArg);
52
+ const outbound = (await (await store.prepare('SELECT DISTINCT dst FROM links WHERE src = ? AND dst IS NOT NULL AND dst != src')).all(path)).map((r)=>r.dst);
53
+ const backlinks = (await (await store.prepare('SELECT DISTINCT src FROM links WHERE dst = ? AND src != dst')).all(path)).map((r)=>r.src);
54
+ const exclude = new Set([
55
+ path,
56
+ ...outbound,
57
+ ...backlinks
58
+ ]);
59
+ const allowed = await scopedPaths(store, cfg, overrides);
60
+ // The seed may sit outside a narrowed candidate scope; both it and every eligible result must
61
+ // be ready, while pending vectors in unrelated notes do not block this operation.
62
+ await ensureDocumentEmbeddings(store, cfg, new Set([
63
+ ...allowed,
64
+ path
65
+ ]));
66
+ if (!await hasEmbedding(store, path)) {
67
+ throw new SenseError('NOTE_NOT_EMBEDDED', `${path} has no indexed text to compare -- a note that is frontmatter only, or empty, has nothing to rank by meaning`);
68
+ }
69
+ if (!await scopeHasEmbeddings(store, cfg, allowed)) return [];
70
+ return withMaterializedVectorScope(store, allowed, ()=>similarNotes(store, cfg, path, {
71
+ exclude,
72
+ allowed,
73
+ k
74
+ }));
58
75
  });
59
76
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/related.ts"],"sourcesContent":["import type { ResolvedConfig, SearchOverrides } from '../config/index.ts';\nimport { embedConfig, embedEnabled, resolveSearch } from '../config/index.ts';\nimport { ensureDocumentEmbeddings, hasEmbedding, similarNotes } from '../embed/query.ts';\nimport { localModelMissing, MODEL_FILENAMES } from '../embed/store.ts';\nimport { SenseError } from '../errors.ts';\nimport { serialQuery } from '../lib/serial-query.ts';\nimport type { Store } from '../store/types.ts';\nimport { resolveNote } from './peek.ts';\nimport { scopedPaths, scopeHasEmbeddings } from './scope.ts';\n\nexport async function resolveRelatedSeed(store: Store, pathArg: string): Promise<string> {\n const paths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((row) => row.path);\n return resolveNote(paths, pathArg);\n}\n\n// Notes most similar by cosine, excluding self and everything already linked either way.\n// Its own command, not a peek section: a full embeddings scan is ~480ms at 26k notes.\nexport function relatedNotes(store: Store, cfg: ResolvedConfig, pathArg: string, overrides: SearchOverrides, k: number): Promise<Array<{ path: string; similarity: number }>> {\n return serialQuery(store, () => relatedIndexed(store, cfg, pathArg, overrides, k));\n}\n\nasync function relatedIndexed(store: Store, cfg: ResolvedConfig, pathArg: string, overrides: SearchOverrides, k: number): Promise<Array<{ path: string; similarity: number }>> {\n const path = await resolveRelatedSeed(store, pathArg);\n\n const outbound = ((await (await store.prepare('SELECT DISTINCT dst FROM links WHERE src = ? AND dst IS NOT NULL AND dst != src')).all(path)) as Array<{ dst: string }>).map((r) => r.dst);\n const backlinks = ((await (await store.prepare('SELECT DISTINCT src FROM links WHERE dst = ? AND src != dst')).all(path)) as Array<{ src: string }>).map((r) => r.src);\n const exclude = new Set([path, ...outbound, ...backlinks]);\n\n // Vectors are the only signal `related` has, so every way of not having them is an error\n // naming the cause; an empty result means one thing, nothing near in meaning.\n const effective = resolveSearch(cfg, overrides);\n if (!embedEnabled(cfg)) {\n throw new SenseError('EMBED_DISABLED', 'related ranks notes by meaning, and this tree has no embedding model; add an \"embed\" block naming one to sense.config.json (a Hugging Face id fetches automatically at first use; `sense download` prefetches it) -- search works without it, on words and links');\n }\n // search gates on the same signal, so a preset's declared signals mean one thing: without\n // this check an overlapping vectors-on preset would answer for a scope that declined them.\n if (effective.signals.vectors === undefined) {\n throw new SenseError('PRESET_NOT_SEMANTIC', `preset \"${effective.presetName}\" has no \"vectors\" signal, so this scope has no vectors and related has no other signal; search it instead (words and links), or add \"vectors\" to that preset's signals`);\n }\n // A downloadable HF id can be fetched by preparation. Only a local path with missing files\n // can never fetch itself.\n const e = embedConfig(cfg); // embedEnabled(cfg) above guarantees this is set\n if (localModelMissing(e)) {\n throw new SenseError('EMBED_MODEL_MISSING', `related ranks notes by meaning, so it needs the embedding model, but the local model path \"${e.model}\" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them (search still works without it, on words and links)`);\n }\n const allowed = await scopedPaths(store, cfg, overrides);\n // The seed may sit outside a narrowed candidate scope; both it and every eligible result must\n // be ready, while pending vectors in unrelated notes do not block this operation.\n await ensureDocumentEmbeddings(store, cfg, new Set([...allowed, path]));\n if (!(await hasEmbedding(store, path))) {\n throw new SenseError('NOTE_NOT_EMBEDDED', `${path} has no indexed text to compare -- a note that is frontmatter only, or empty, has nothing to rank by meaning`);\n }\n if (!(await scopeHasEmbeddings(store, cfg, allowed))) return [];\n return similarNotes(store, cfg, path, { exclude, allowed, k });\n}\n"],"names":["embedConfig","embedEnabled","resolveSearch","ensureDocumentEmbeddings","hasEmbedding","similarNotes","localModelMissing","MODEL_FILENAMES","SenseError","serialQuery","resolveNote","scopedPaths","scopeHasEmbeddings","resolveRelatedSeed","store","pathArg","paths","prepare","all","map","row","path","relatedNotes","cfg","overrides","k","relatedIndexed","outbound","r","dst","backlinks","src","exclude","Set","effective","signals","vectors","undefined","presetName","e","model","allowed"],"mappings":"AACA,SAASA,WAAW,EAAEC,YAAY,EAAEC,aAAa,QAAQ,qBAAqB;AAC9E,SAASC,wBAAwB,EAAEC,YAAY,EAAEC,YAAY,QAAQ,oBAAoB;AACzF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,oBAAoB;AACvE,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,WAAW,QAAQ,yBAAyB;AAErD,SAASC,WAAW,QAAQ,YAAY;AACxC,SAASC,WAAW,EAAEC,kBAAkB,QAAQ,aAAa;AAE7D,OAAO,eAAeC,mBAAmBC,KAAY,EAAEC,OAAe;IACpE,MAAMC,QAAQ,AAAE,CAAA,MAAM,AAAC,CAAA,MAAMF,MAAMG,OAAO,CAAC,iCAAgC,EAAGC,GAAG,EAAC,EAA+BC,GAAG,CAAC,CAACC,MAAQA,IAAIC,IAAI;IACtI,OAAOX,YAAYM,OAAOD;AAC5B;AAEA,yFAAyF;AACzF,sFAAsF;AACtF,OAAO,SAASO,aAAaR,KAAY,EAAES,GAAmB,EAAER,OAAe,EAAES,SAA0B,EAAEC,CAAS;IACpH,OAAOhB,YAAYK,OAAO,IAAMY,eAAeZ,OAAOS,KAAKR,SAASS,WAAWC;AACjF;AAEA,eAAeC,eAAeZ,KAAY,EAAES,GAAmB,EAAER,OAAe,EAAES,SAA0B,EAAEC,CAAS;IACrH,MAAMJ,OAAO,MAAMR,mBAAmBC,OAAOC;IAE7C,MAAMY,WAAW,AAAE,CAAA,MAAM,AAAC,CAAA,MAAMb,MAAMG,OAAO,CAAC,kFAAiF,EAAGC,GAAG,CAACG,KAAI,EAA8BF,GAAG,CAAC,CAACS,IAAMA,EAAEC,GAAG;IACxL,MAAMC,YAAY,AAAE,CAAA,MAAM,AAAC,CAAA,MAAMhB,MAAMG,OAAO,CAAC,8DAA6D,EAAGC,GAAG,CAACG,KAAI,EAA8BF,GAAG,CAAC,CAACS,IAAMA,EAAEG,GAAG;IACrK,MAAMC,UAAU,IAAIC,IAAI;QAACZ;WAASM;WAAaG;KAAU;IAEzD,yFAAyF;IACzF,8EAA8E;IAC9E,MAAMI,YAAYhC,cAAcqB,KAAKC;IACrC,IAAI,CAACvB,aAAasB,MAAM;QACtB,MAAM,IAAIf,WAAW,kBAAkB;IACzC;IACA,0FAA0F;IAC1F,2FAA2F;IAC3F,IAAI0B,UAAUC,OAAO,CAACC,OAAO,KAAKC,WAAW;QAC3C,MAAM,IAAI7B,WAAW,uBAAuB,CAAC,QAAQ,EAAE0B,UAAUI,UAAU,CAAC,uKAAuK,CAAC;IACtP;IACA,2FAA2F;IAC3F,0BAA0B;IAC1B,MAAMC,IAAIvC,YAAYuB,MAAM,iDAAiD;IAC7E,IAAIjB,kBAAkBiC,IAAI;QACxB,MAAM,IAAI/B,WAAW,uBAAuB,CAAC,2FAA2F,EAAE+B,EAAEC,KAAK,CAAC,aAAa,EAAEjC,gBAAgB,sGAAsG,CAAC;IAC1R;IACA,MAAMkC,UAAU,MAAM9B,YAAYG,OAAOS,KAAKC;IAC9C,8FAA8F;IAC9F,kFAAkF;IAClF,MAAMrB,yBAAyBW,OAAOS,KAAK,IAAIU,IAAI;WAAIQ;QAASpB;KAAK;IACrE,IAAI,CAAE,MAAMjB,aAAaU,OAAOO,OAAQ;QACtC,MAAM,IAAIb,WAAW,qBAAqB,GAAGa,KAAK,4GAA4G,CAAC;IACjK;IACA,IAAI,CAAE,MAAMT,mBAAmBE,OAAOS,KAAKkB,UAAW,OAAO,EAAE;IAC/D,OAAOpC,aAAaS,OAAOS,KAAKF,MAAM;QAAEW;QAASS;QAAShB;IAAE;AAC9D"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/related.ts"],"sourcesContent":["import type { ResolvedConfig, SearchOverrides } from '../config/index.ts';\nimport { embedConfig, embedEnabled, resolveSearch } from '../config/index.ts';\nimport { ensureDocumentEmbeddings, hasEmbedding, similarNotes } from '../embed/query.ts';\nimport { localModelMissing, MODEL_FILENAMES } from '../embed/store.ts';\nimport { SenseError } from '../errors.ts';\nimport { serialQuery } from '../lib/serial-query.ts';\nimport type { Store } from '../store/types.ts';\nimport { resolveNote } from './peek.ts';\nimport { scopedPaths, scopeHasEmbeddings, withMaterializedVectorScope } from './scope.ts';\nimport { assertQuerySnapshot } from './snapshot.ts';\n\nexport const RELATED_DEFAULT_K = 5;\n\n/**\n * Options for {@link relatedNotes}. Scope fields filter result candidates, not the seed note;\n * `k` defaults to 5, matching the CLI.\n */\nexport interface RelatedOptions extends SearchOverrides {\n k?: number;\n}\n\nexport type RelatedResult = {\n path: string;\n similarity: number;\n};\n\nexport async function resolveRelatedSeed(store: Store, pathArg: string): Promise<string> {\n const paths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((row) => row.path);\n return resolveNote(paths, pathArg);\n}\n\n/**\n * Returns the notes most similar to an indexed seed, excluding the seed and notes linked either\n * way. The seed resolves across the indexed tree even when it is outside the candidate scope.\n */\nexport function relatedNotes(store: Store, cfg: ResolvedConfig, pathArg: string, options: RelatedOptions = {}): Promise<RelatedResult[]> {\n return serialQuery(store, () => relatedIndexed(store, cfg, pathArg, options, options.k ?? RELATED_DEFAULT_K));\n}\n\nasync function relatedIndexed(store: Store, cfg: ResolvedConfig, pathArg: string, overrides: RelatedOptions, k: number): Promise<RelatedResult[]> {\n if (!Number.isFinite(k) || !Number.isInteger(k) || k <= 0) {\n throw new SenseError('SEARCH_OPTION_INVALID', `related option \"k\" must be a positive finite integer, got ${String(k)}`);\n }\n // Vectors are the only signal `related` has, so every way of not having them is an error\n // naming the cause; an empty result means one thing, nothing near in meaning.\n const effective = resolveSearch(cfg, overrides);\n if (!embedEnabled(cfg)) {\n throw new SenseError('EMBED_DISABLED', 'related ranks notes by meaning, and this tree has no embedding model; add an \"embed\" block naming one to sense.config.json (a Hugging Face id fetches automatically at first use; `sense download` prefetches it) -- search works without it, on words and links');\n }\n // search gates on the same signal, so a preset's declared signals mean one thing: without\n // this check an overlapping vectors-on preset would answer for a scope that declined them.\n if (effective.signals.vectors === undefined) {\n throw new SenseError('PRESET_NOT_SEMANTIC', `preset \"${effective.presetName}\" has no \"vectors\" signal, so this scope has no vectors and related has no other signal; search it instead (words and links), or add \"vectors\" to that preset's signals`);\n }\n // A downloadable HF id can be fetched by preparation. Only a local path with missing files\n // can never fetch itself.\n const e = embedConfig(cfg); // embedEnabled(cfg) above guarantees this is set\n if (localModelMissing(e)) {\n throw new SenseError('EMBED_MODEL_MISSING', `related ranks notes by meaning, so it needs the embedding model, but the local model path \"${e.model}\" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them (search still works without it, on words and links)`);\n }\n\n return store.transaction(async () => {\n await assertQuerySnapshot(store, cfg);\n // Repeat effective preset validation inside the snapshot that supplies its durable scope.\n const current = resolveSearch(cfg, overrides);\n if (current.signals.vectors === undefined) {\n throw new SenseError('PRESET_NOT_SEMANTIC', `preset \"${current.presetName}\" has no \"vectors\" signal, so this scope has no vectors and related has no other signal; search it instead (words and links), or add \"vectors\" to that preset's signals`);\n }\n\n const path = await resolveRelatedSeed(store, pathArg);\n const outbound = ((await (await store.prepare('SELECT DISTINCT dst FROM links WHERE src = ? AND dst IS NOT NULL AND dst != src')).all(path)) as Array<{ dst: string }>).map((r) => r.dst);\n const backlinks = ((await (await store.prepare('SELECT DISTINCT src FROM links WHERE dst = ? AND src != dst')).all(path)) as Array<{ src: string }>).map((r) => r.src);\n const exclude = new Set([path, ...outbound, ...backlinks]);\n const allowed = await scopedPaths(store, cfg, overrides);\n // The seed may sit outside a narrowed candidate scope; both it and every eligible result must\n // be ready, while pending vectors in unrelated notes do not block this operation.\n await ensureDocumentEmbeddings(store, cfg, new Set([...allowed, path]));\n if (!(await hasEmbedding(store, path))) {\n throw new SenseError('NOTE_NOT_EMBEDDED', `${path} has no indexed text to compare -- a note that is frontmatter only, or empty, has nothing to rank by meaning`);\n }\n if (!(await scopeHasEmbeddings(store, cfg, allowed))) return [];\n return withMaterializedVectorScope(store, allowed, () => similarNotes(store, cfg, path, { exclude, allowed, k }));\n });\n}\n"],"names":["embedConfig","embedEnabled","resolveSearch","ensureDocumentEmbeddings","hasEmbedding","similarNotes","localModelMissing","MODEL_FILENAMES","SenseError","serialQuery","resolveNote","scopedPaths","scopeHasEmbeddings","withMaterializedVectorScope","assertQuerySnapshot","RELATED_DEFAULT_K","resolveRelatedSeed","store","pathArg","paths","prepare","all","map","row","path","relatedNotes","cfg","options","relatedIndexed","k","overrides","Number","isFinite","isInteger","String","effective","signals","vectors","undefined","presetName","e","model","transaction","current","outbound","r","dst","backlinks","src","exclude","Set","allowed"],"mappings":"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"}
@@ -7,4 +7,5 @@ export declare function rawScope(store: Store, cfg: ResolvedConfig, overrides: S
7
7
  export declare function narrowByWhere(store: Store, paths: Set<string>, where: string | undefined): Promise<Set<string>>;
8
8
  export declare function scopedPaths(store: Store, cfg: ResolvedConfig, overrides: SearchOverrides): Promise<Set<string>>;
9
9
  export declare function materializeScope(store: Store, table: string, paths: Set<string>): Promise<void>;
10
+ export declare function withMaterializedVectorScope<T>(store: Store, allowed: Set<string>, callback: () => Promise<T>, knownTotal?: number): Promise<T>;
10
11
  export declare function setupMapScope(store: Store, paths: Set<string>): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { matchesGlob } from 'node:path';
2
2
  import { anyPresetEmbeds, resolveSearch } from '../config/index.js';
3
3
  import { columnHint } from '../output/column-hint.js';
4
+ import { NATIVE_VECTOR_SCOPE_TABLE, reserveNativeVectorScope } from '../store/vectors.js';
4
5
  export const INTERNAL_COLUMNS = new Set([
5
6
  'path',
6
7
  '_mtime',
@@ -70,6 +71,63 @@ export async function materializeScope(store, table, paths) {
70
71
  p
71
72
  ]));
72
73
  }
74
+ // Native vector adapters can consume a small exact scope through their connection-local temp
75
+ // table. The reservation is tied to this Store's VectorStore wrapper, so another Store cannot
76
+ // activate it by receiving the same Set object.
77
+ export async function withMaterializedVectorScope(store, allowed, callback, knownTotal) {
78
+ const reservation = reserveNativeVectorScope(store.vectors, allowed);
79
+ if (!reservation) return callback();
80
+ if (allowed.size === 0) {
81
+ reservation.invalidate();
82
+ return callback();
83
+ }
84
+ let total = knownTotal;
85
+ if (total === undefined) {
86
+ try {
87
+ const stmt = await store.prepare('SELECT COUNT(*) AS count FROM frontmatter');
88
+ const row = await stmt.get();
89
+ total = Number(row.count);
90
+ } catch (err) {
91
+ reservation.invalidate();
92
+ throw err;
93
+ }
94
+ }
95
+ // Materialization only buys work when this is a nonempty true narrowing and the allowed side
96
+ // is no larger than its complement. Direct adapter calls and broader scopes retain JS filtering.
97
+ if (allowed.size >= total || allowed.size > total - allowed.size) {
98
+ reservation.invalidate();
99
+ return callback();
100
+ }
101
+ let operationFailed = false;
102
+ let operationError;
103
+ let result;
104
+ try {
105
+ await materializeScope(store, NATIVE_VECTOR_SCOPE_TABLE, allowed);
106
+ reservation.activate();
107
+ result = await callback();
108
+ } catch (err) {
109
+ operationFailed = true;
110
+ operationError = err;
111
+ }
112
+ let cleanupFailed = false;
113
+ let cleanupError;
114
+ try {
115
+ reservation.deactivate();
116
+ await store.exec(`DROP TABLE IF EXISTS ${NATIVE_VECTOR_SCOPE_TABLE}`);
117
+ } catch (err) {
118
+ cleanupFailed = true;
119
+ cleanupError = err;
120
+ } finally{
121
+ reservation.invalidate();
122
+ }
123
+ if (operationFailed && cleanupFailed) throw new AggregateError([
124
+ operationError,
125
+ cleanupError
126
+ ], 'native vector scope operation and cleanup failed');
127
+ if (operationFailed) throw operationError;
128
+ if (cleanupFailed) throw cleanupError;
129
+ return result;
130
+ }
73
131
  export async function setupMapScope(store, paths) {
74
132
  await materializeScope(store, '_map_scope', paths);
75
133
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/scope.ts"],"sourcesContent":["import { matchesGlob } from 'node:path';\nimport type { ResolvedConfig, SearchOverrides } from '../config/index.ts';\nimport { anyPresetEmbeds, resolveSearch } from '../config/index.ts';\nimport { columnHint } from '../output/column-hint.ts';\nimport type { Store } from '../store/types.ts';\n\nexport const INTERNAL_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_rank', '_parse_error']);\n\n// node:path's matchesGlob is experimental (stable behind an unstable-API flag) at the\n// Node >=22.20 engines floor; used here in JS rather than a directory walk in the query path.\nexport function inScope(path: string, include: string[], exclude?: string[]): boolean {\n if (!include.some((g) => matchesGlob(path, g))) return false;\n if (exclude?.some((g) => matchesGlob(path, g))) return false;\n return true;\n}\n\nexport async function scopeHasEmbeddings(store: Store, cfg: ResolvedConfig, scopedPaths: Set<string>): Promise<boolean> {\n if (!anyPresetEmbeds(cfg)) return false; // the embeddings table doesn't exist at all in this case\n const stmt = await store.prepare('SELECT DISTINCT \"path\" FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string }>;\n return rows.some((r) => scopedPaths.has(r.path));\n}\n\n// Scope only (no --where): preset_files for a named preset, JS glob matching for an ad hoc\n// include/exclude override. Shared by scopedPaths() and search()'s candidate-pool sizing.\nexport async function rawScope(store: Store, cfg: ResolvedConfig, overrides: SearchOverrides, allPaths?: string[]): Promise<Set<string>> {\n const effective = resolveSearch(cfg, overrides);\n const { include, exclude } = effective;\n const adHocScope = overrides.include !== undefined || overrides.exclude !== undefined || overrides.noExclude === true;\n if (!adHocScope) {\n const stmt = await store.prepare('SELECT \"path\" FROM preset_files WHERE preset = ?');\n return new Set(((await stmt.all(effective.presetName)) as Array<{ path: string }>).map((r) => r.path));\n }\n const paths = allPaths ?? (await allFrontmatterPaths(store));\n return new Set(paths.filter((p) => inScope(p, include, exclude)));\n}\n\nasync function allFrontmatterPaths(store: Store): Promise<string[]> {\n const stmt = await store.prepare('SELECT \"path\" FROM frontmatter');\n return ((await stmt.all()) as Array<{ path: string }>).map((r) => r.path);\n}\n\nexport async function narrowByWhere(store: Store, paths: Set<string>, where: string | undefined): Promise<Set<string>> {\n if (!where) return paths;\n let whereRows: Array<{ path: string }>;\n try {\n const stmt = await store.prepare(`SELECT \"path\" FROM frontmatter f WHERE (${where})`);\n whereRows = (await stmt.all()) as Array<{ path: string }>;\n } catch (err) {\n throw columnHint(await store.docs.columns(), err as Error);\n }\n const wherePaths = new Set(whereRows.map((r) => r.path));\n return new Set([...paths].filter((p) => wherePaths.has(p)));\n}\n\n// The scope resolver for non-search commands (path, peek, map): same coverage rule search()\n// applies, then narrowed by the resolved `where`.\nexport async function scopedPaths(store: Store, cfg: ResolvedConfig, overrides: SearchOverrides): Promise<Set<string>> {\n const effective = resolveSearch(cfg, overrides);\n return narrowByWhere(store, await rawScope(store, cfg, overrides), effective.where);\n}\n\n// Materializes a path set into a named temp table (same shape as traverse.ts's allowed_nodes)\n// so a query can join/filter against it cheaply instead of binding a parameter per path.\nexport async function materializeScope(store: Store, table: string, paths: Set<string>): Promise<void> {\n await store.exec(`DROP TABLE IF EXISTS ${table}`);\n await store.exec(`CREATE TEMP TABLE ${table} (\"path\" TEXT PRIMARY KEY)`);\n await store.runBatch(\n `INSERT INTO ${table} (\"path\") VALUES (?)`,\n [...paths].map((p) => [p])\n );\n}\n\nexport async function setupMapScope(store: Store, paths: Set<string>): Promise<void> {\n await materializeScope(store, '_map_scope', paths);\n}\n"],"names":["matchesGlob","anyPresetEmbeds","resolveSearch","columnHint","INTERNAL_COLUMNS","Set","inScope","path","include","exclude","some","g","scopeHasEmbeddings","store","cfg","scopedPaths","stmt","prepare","rows","all","r","has","rawScope","overrides","allPaths","effective","adHocScope","undefined","noExclude","presetName","map","paths","allFrontmatterPaths","filter","p","narrowByWhere","where","whereRows","err","docs","columns","wherePaths","materializeScope","table","exec","runBatch","setupMapScope"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/scope.ts"],"sourcesContent":["import { matchesGlob } from 'node:path';\nimport type { ResolvedConfig, SearchOverrides } from '../config/index.ts';\nimport { anyPresetEmbeds, resolveSearch } from '../config/index.ts';\nimport { columnHint } from '../output/column-hint.ts';\nimport type { Store } from '../store/types.ts';\nimport { NATIVE_VECTOR_SCOPE_TABLE, reserveNativeVectorScope } from '../store/vectors.ts';\n\nexport const INTERNAL_COLUMNS = new Set(['path', '_mtime', '_ctime', '_size', '_rank', '_parse_error']);\n\n// node:path's matchesGlob is experimental (stable behind an unstable-API flag) at the\n// Node >=22.20 engines floor; used here in JS rather than a directory walk in the query path.\nexport function inScope(path: string, include: string[], exclude?: string[]): boolean {\n if (!include.some((g) => matchesGlob(path, g))) return false;\n if (exclude?.some((g) => matchesGlob(path, g))) return false;\n return true;\n}\n\nexport async function scopeHasEmbeddings(store: Store, cfg: ResolvedConfig, scopedPaths: Set<string>): Promise<boolean> {\n if (!anyPresetEmbeds(cfg)) return false; // the embeddings table doesn't exist at all in this case\n const stmt = await store.prepare('SELECT DISTINCT \"path\" FROM embeddings WHERE vector IS NOT NULL');\n const rows = (await stmt.all()) as Array<{ path: string }>;\n return rows.some((r) => scopedPaths.has(r.path));\n}\n\n// Scope only (no --where): preset_files for a named preset, JS glob matching for an ad hoc\n// include/exclude override. Shared by scopedPaths() and search()'s candidate-pool sizing.\nexport async function rawScope(store: Store, cfg: ResolvedConfig, overrides: SearchOverrides, allPaths?: string[]): Promise<Set<string>> {\n const effective = resolveSearch(cfg, overrides);\n const { include, exclude } = effective;\n const adHocScope = overrides.include !== undefined || overrides.exclude !== undefined || overrides.noExclude === true;\n if (!adHocScope) {\n const stmt = await store.prepare('SELECT \"path\" FROM preset_files WHERE preset = ?');\n return new Set(((await stmt.all(effective.presetName)) as Array<{ path: string }>).map((r) => r.path));\n }\n const paths = allPaths ?? (await allFrontmatterPaths(store));\n return new Set(paths.filter((p) => inScope(p, include, exclude)));\n}\n\nasync function allFrontmatterPaths(store: Store): Promise<string[]> {\n const stmt = await store.prepare('SELECT \"path\" FROM frontmatter');\n return ((await stmt.all()) as Array<{ path: string }>).map((r) => r.path);\n}\n\nexport async function narrowByWhere(store: Store, paths: Set<string>, where: string | undefined): Promise<Set<string>> {\n if (!where) return paths;\n let whereRows: Array<{ path: string }>;\n try {\n const stmt = await store.prepare(`SELECT \"path\" FROM frontmatter f WHERE (${where})`);\n whereRows = (await stmt.all()) as Array<{ path: string }>;\n } catch (err) {\n throw columnHint(await store.docs.columns(), err as Error);\n }\n const wherePaths = new Set(whereRows.map((r) => r.path));\n return new Set([...paths].filter((p) => wherePaths.has(p)));\n}\n\n// The scope resolver for non-search commands (path, peek, map): same coverage rule search()\n// applies, then narrowed by the resolved `where`.\nexport async function scopedPaths(store: Store, cfg: ResolvedConfig, overrides: SearchOverrides): Promise<Set<string>> {\n const effective = resolveSearch(cfg, overrides);\n return narrowByWhere(store, await rawScope(store, cfg, overrides), effective.where);\n}\n\n// Materializes a path set into a named temp table (same shape as traverse.ts's allowed_nodes)\n// so a query can join/filter against it cheaply instead of binding a parameter per path.\nexport async function materializeScope(store: Store, table: string, paths: Set<string>): Promise<void> {\n await store.exec(`DROP TABLE IF EXISTS ${table}`);\n await store.exec(`CREATE TEMP TABLE ${table} (\"path\" TEXT PRIMARY KEY)`);\n await store.runBatch(\n `INSERT INTO ${table} (\"path\") VALUES (?)`,\n [...paths].map((p) => [p])\n );\n}\n\n// Native vector adapters can consume a small exact scope through their connection-local temp\n// table. The reservation is tied to this Store's VectorStore wrapper, so another Store cannot\n// activate it by receiving the same Set object.\nexport async function withMaterializedVectorScope<T>(store: Store, allowed: Set<string>, callback: () => Promise<T>, knownTotal?: number): Promise<T> {\n const reservation = reserveNativeVectorScope(store.vectors, allowed);\n if (!reservation) return callback();\n\n if (allowed.size === 0) {\n reservation.invalidate();\n return callback();\n }\n\n let total = knownTotal;\n if (total === undefined) {\n try {\n const stmt = await store.prepare('SELECT COUNT(*) AS count FROM frontmatter');\n const row = (await stmt.get()) as { count: number | bigint };\n total = Number(row.count);\n } catch (err) {\n reservation.invalidate();\n throw err;\n }\n }\n // Materialization only buys work when this is a nonempty true narrowing and the allowed side\n // is no larger than its complement. Direct adapter calls and broader scopes retain JS filtering.\n if (allowed.size >= total || allowed.size > total - allowed.size) {\n reservation.invalidate();\n return callback();\n }\n\n let operationFailed = false;\n let operationError: unknown;\n let result!: T;\n try {\n await materializeScope(store, NATIVE_VECTOR_SCOPE_TABLE, allowed);\n reservation.activate();\n result = await callback();\n } catch (err) {\n operationFailed = true;\n operationError = err;\n }\n\n let cleanupFailed = false;\n let cleanupError: unknown;\n try {\n reservation.deactivate();\n await store.exec(`DROP TABLE IF EXISTS ${NATIVE_VECTOR_SCOPE_TABLE}`);\n } catch (err) {\n cleanupFailed = true;\n cleanupError = err;\n } finally {\n reservation.invalidate();\n }\n\n if (operationFailed && cleanupFailed) throw new AggregateError([operationError, cleanupError], 'native vector scope operation and cleanup failed');\n if (operationFailed) throw operationError;\n if (cleanupFailed) throw cleanupError;\n return result;\n}\n\nexport async function setupMapScope(store: Store, paths: Set<string>): Promise<void> {\n await materializeScope(store, '_map_scope', paths);\n}\n"],"names":["matchesGlob","anyPresetEmbeds","resolveSearch","columnHint","NATIVE_VECTOR_SCOPE_TABLE","reserveNativeVectorScope","INTERNAL_COLUMNS","Set","inScope","path","include","exclude","some","g","scopeHasEmbeddings","store","cfg","scopedPaths","stmt","prepare","rows","all","r","has","rawScope","overrides","allPaths","effective","adHocScope","undefined","noExclude","presetName","map","paths","allFrontmatterPaths","filter","p","narrowByWhere","where","whereRows","err","docs","columns","wherePaths","materializeScope","table","exec","runBatch","withMaterializedVectorScope","allowed","callback","knownTotal","reservation","vectors","size","invalidate","total","row","get","Number","count","operationFailed","operationError","result","activate","cleanupFailed","cleanupError","deactivate","AggregateError","setupMapScope"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { ResolvedConfig } from '../config/index.js';
2
- import type { Row } from '../output/output.js';
3
2
  import type { Store } from '../store/types.js';
3
+ import type { SearchResultVia } from './signals.js';
4
+ export type { SearchResultVia } from './signals.js';
4
5
  export declare const SNIPPET_CHAR_LIMIT_DEFAULT = 80;
5
6
  export declare const SNIPPET_COUNT_LIMIT_DEFAULT = 1;
6
7
  export declare function computeSnippets(text: string, terms: string[], charLimit: number, countLimit: number): {
@@ -8,6 +9,7 @@ export declare function computeSnippets(text: string, terms: string[], charLimit
8
9
  offset: number;
9
10
  };
10
11
  export declare function lineNumberAt(text: string, offset: number): number;
12
+ /** Options for the root {@link search} API. */
11
13
  export interface SearchOptions {
12
14
  k?: number;
13
15
  where?: string;
@@ -17,6 +19,27 @@ export interface SearchOptions {
17
19
  noExclude?: boolean;
18
20
  snippetCharLimit?: number;
19
21
  snippetCountLimit?: number;
22
+ /** Cancels supported provider I/O and stops between phases after active native work finishes. */
23
+ signal?: AbortSignal;
20
24
  }
21
- export declare function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: Row[], matchedPaths: Set<string>, terms: string, opts?: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'>): Promise<void>;
22
- export declare function search(store: Store, cfg: ResolvedConfig, terms: string, opts?: SearchOptions): Promise<Row[]>;
25
+ export type SearchResult = {
26
+ path: string;
27
+ title: string;
28
+ summary: string;
29
+ snippets: string[];
30
+ via: SearchResultVia;
31
+ score: number;
32
+ lines: string | null;
33
+ similarity?: number | null;
34
+ };
35
+ export interface SearchHydrationRow {
36
+ path: string;
37
+ snippets?: string[];
38
+ lines?: string | null;
39
+ }
40
+ export declare function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: SearchHydrationRow[], matchedPaths: Set<string>, terms: string, opts?: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'>): Promise<void>;
41
+ /**
42
+ * Searches one committed published generation. Provider work precedes the final read transaction;
43
+ * cancellation waits for active native work and transaction cleanup before rejection.
44
+ */
45
+ export declare function search(store: Store, cfg: ResolvedConfig, terms: string, opts?: SearchOptions): Promise<SearchResult[]>;
@@ -1,6 +1,7 @@
1
+ import { channel } from 'node:diagnostics_channel';
1
2
  import { stemmer } from 'stemmer';
2
3
  import { embedConfig, featureEnabled, resolveSearch } from '../config/index.js';
3
- import { ensureDocumentEmbeddings } from '../embed/query.js';
4
+ import { ensureDocumentEmbeddings, prepareSemanticQuery } from '../embed/query.js';
4
5
  import { localModelMissing, MODEL_FILENAMES } from '../embed/store.js';
5
6
  import { SenseError } from '../errors.js';
6
7
  import { serialQuery } from '../lib/serial-query.js';
@@ -8,10 +9,12 @@ import { bareTermSyntaxError, searchError } from '../output/search-error.js';
8
9
  import { foldForSearch, hasUnspacedRun, searchTokens, unspacedRuns } from '../text/segment.js';
9
10
  import { materializeScope, narrowByWhere, rawScope, scopeHasEmbeddings } from './scope.js';
10
11
  import { linksCandidates, vectorsCandidates, wordsCandidates } from './signals.js';
12
+ import { assertQuerySnapshot } from './snapshot.js';
11
13
  // Default budget behind --snippet-char-limit/--snippet-count-limit: the limit is whatever the
12
14
  // caller passes, these only say what happens when they pass nothing (DESIGN.md "Search snippets").
13
15
  export const SNIPPET_CHAR_LIMIT_DEFAULT = 80;
14
16
  export const SNIPPET_COUNT_LIMIT_DEFAULT = 1;
17
+ const searchCandidatesRead = channel('sensemaking.search.candidates-read');
15
18
  // Bare terms from an FTS5 query string: strips operators/quoting, not real terms, so the
16
19
  // snippet scan matches what the query matched -- FTS5 operators are uppercase-only, so this is too.
17
20
  function extractBareTerms(query) {
@@ -308,38 +311,99 @@ export async function hydrateSearchRows(store, cfg, rows, matchedPaths, terms, o
308
311
  if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path, lineNumberAt(text, offset)) : null;
309
312
  }
310
313
  }
311
- // The declared or defaulted signals compose via RRF; `via` names which ones produced each row.
312
- // `opts` arrives already resolved (config.ts:resolveSearch).
313
- export function search(store, cfg, terms, opts = {}) {
314
+ /**
315
+ * Searches one committed published generation. Provider work precedes the final read transaction;
316
+ * cancellation waits for active native work and transaction cleanup before rejection.
317
+ */ export function search(store, cfg, terms, opts = {}) {
318
+ var _opts_signal;
319
+ if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) return Promise.reject(opts.signal.reason);
314
320
  return serialQuery(store, ()=>searchIndexed(store, cfg, terms, opts));
315
321
  }
316
- async function searchIndexed(store, cfg, terms, opts) {
317
- validateSearchOptions(opts);
322
+ async function readSearchScope(store, cfg, terms, opts) {
323
+ const signature = await assertQuerySnapshot(store, cfg);
318
324
  const effective = resolveSearch(cfg, opts);
319
- const { k, signals } = effective;
320
325
  const allPaths = (await (await store.prepare('SELECT "path" FROM frontmatter')).all()).map((r)=>r.path);
321
326
  const scopePaths = await rawScope(store, cfg, opts, allPaths);
322
- const scopeActive = scopePaths.size < allPaths.length;
323
- // The set every candidate pool must be filtered to before truncation: scope narrowed by
324
- // --where, the same composition scopedPaths() gives the other commands.
325
327
  let allowedPaths;
326
328
  try {
327
329
  allowedPaths = await narrowByWhere(store, scopePaths, effective.where);
328
330
  } catch (err) {
329
331
  throw searchError(err, terms, effective.where);
330
332
  }
331
- const fetch = Math.max(k * 3, 30);
333
+ const wantsVectors = effective.signals.vectors !== undefined;
334
+ if (wantsVectors) await ensureDocumentEmbeddings(store, cfg, allowedPaths);
335
+ const semanticEligible = wantsVectors && await scopeHasEmbeddings(store, cfg, allowedPaths);
336
+ return {
337
+ signature,
338
+ effective,
339
+ allPaths,
340
+ scopePaths,
341
+ allowedPaths,
342
+ semanticEligible
343
+ };
344
+ }
345
+ function needsSemanticPreparation(result) {
346
+ return !Array.isArray(result);
347
+ }
348
+ async function searchIndexed(store, cfg, terms, opts) {
349
+ var _opts_signal, _opts_signal1, _opts_signal2, _opts_signal3, _opts_signal4;
350
+ validateSearchOptions(opts);
351
+ const initial = resolveSearch(cfg, opts);
352
+ (_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.throwIfAborted();
332
353
  // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a
333
354
  // local path with missing files errors here, since nothing will ever fetch it for itself.
334
- const wantsVectors = signals.vectors !== undefined;
355
+ const wantsVectors = initial.signals.vectors !== undefined;
335
356
  if (wantsVectors) {
336
357
  const e = embedConfig(cfg); // validate.ts guarantees this is set whenever "vectors" is declared
337
358
  if (localModelMissing(e)) {
338
- throw new SenseError('EMBED_MODEL_MISSING', `preset "${effective.presetName}" searches with vectors, but the local model path "${e.model}" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them, or drop "vectors" from that preset's signals to search without them`);
359
+ throw new SenseError('EMBED_MODEL_MISSING', `preset "${initial.presetName}" searches with vectors, but the local model path "${e.model}" is missing ${MODEL_FILENAMES}; point embed.model at a directory containing them, or drop "vectors" from that preset's signals to search without them`);
360
+ }
361
+ }
362
+ let prepared;
363
+ let preparedSignature;
364
+ if (wantsVectors) {
365
+ var _opts_signal5;
366
+ // This first snapshot only decides whether a semantic query can contribute. It closes before
367
+ // provider/model work, and the complete scope/readiness decision is repeated below.
368
+ const preflight = await store.transaction(()=>readSearchScope(store, cfg, terms, opts));
369
+ (_opts_signal5 = opts.signal) === null || _opts_signal5 === void 0 ? void 0 : _opts_signal5.throwIfAborted();
370
+ if (preflight.semanticEligible) {
371
+ var _opts_signal6;
372
+ preparedSignature = preflight.signature;
373
+ prepared = await prepareSemanticQuery(cfg, terms, {
374
+ signal: opts.signal
375
+ });
376
+ (_opts_signal6 = opts.signal) === null || _opts_signal6 === void 0 ? void 0 : _opts_signal6.throwIfAborted();
339
377
  }
340
- await ensureDocumentEmbeddings(store, cfg, allowedPaths);
341
378
  }
342
- const semanticEnabled = wantsVectors && await scopeHasEmbeddings(store, cfg, allowedPaths);
379
+ (_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.throwIfAborted();
380
+ let result = await store.transaction(()=>searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));
381
+ (_opts_signal2 = opts.signal) === null || _opts_signal2 === void 0 ? void 0 : _opts_signal2.throwIfAborted();
382
+ if (!needsSemanticPreparation(result)) return result;
383
+ // The scope became vector-eligible after an empty/ineligible preflight. Leave that snapshot,
384
+ // prepare once, then take a fresh final snapshot rather than waiting on the provider inside it.
385
+ preparedSignature = result.signature;
386
+ prepared = await prepareSemanticQuery(cfg, terms, {
387
+ signal: opts.signal
388
+ });
389
+ (_opts_signal3 = opts.signal) === null || _opts_signal3 === void 0 ? void 0 : _opts_signal3.throwIfAborted();
390
+ result = await store.transaction(()=>searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));
391
+ (_opts_signal4 = opts.signal) === null || _opts_signal4 === void 0 ? void 0 : _opts_signal4.throwIfAborted();
392
+ if (needsSemanticPreparation(result)) throw new SenseError('INDEX_NOT_READY', 'the semantic search scope changed while its query was being prepared; retry the search against a stable published index');
393
+ return result;
394
+ }
395
+ async function searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature) {
396
+ const { signature, effective, allPaths, scopePaths, allowedPaths, semanticEligible } = await readSearchScope(store, cfg, terms, opts);
397
+ if (prepared !== undefined && signature !== preparedSignature) {
398
+ throw new SenseError('INDEX_NOT_READY', 'the index configuration changed while the semantic query was being prepared; retry after the current build finishes, or rebuild the index for this configuration');
399
+ }
400
+ if (semanticEligible && prepared === undefined) return {
401
+ signature
402
+ };
403
+ const { k, signals } = effective;
404
+ const fetch = Math.max(k * 3, 30);
405
+ const scopeActive = scopePaths.size < allPaths.length;
406
+ const semanticEnabled = semanticEligible && prepared !== undefined;
343
407
  // --where applies inside the candidate query (a post-filter would drop matches ranked past
344
408
  // the pool) and again on the final select, for link-derived rows.
345
409
  const scope = effective.where;
@@ -372,8 +436,8 @@ async function searchIndexed(store, cfg, terms, opts) {
372
436
  if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links);
373
437
  let chunkLines = new Map();
374
438
  let chunkSimilarity = new Map();
375
- if (semanticEnabled) {
376
- ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, signals.vectors));
439
+ if (semanticEnabled && signals.vectors !== undefined && prepared !== undefined) {
440
+ ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, prepared, candidates, fetch, allowedPaths, allPaths.length, signals.vectors));
377
441
  }
378
442
  await store.exec('DROP TABLE IF EXISTS _search');
379
443
  // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the
@@ -403,8 +467,11 @@ async function searchIndexed(store, cfg, terms, opts) {
403
467
  // overwritten below, always to an array, never left null (DESIGN.md).
404
468
  const selectStmt = await store.prepare(`SELECT f."path" AS path, content.title, content.summary, NULL AS snippets, _search.via, round(_search.score, 4) AS score, _search.lines${similarityCol}
405
469
  FROM _search JOIN frontmatter f ON f."path" = _search."path" JOIN content ON content.path = _search."path"
406
- ${where} ORDER BY _search.score DESC LIMIT ?`);
470
+ ${where} ORDER BY _search.score DESC, _search."path" ASC LIMIT ?`);
407
471
  const rows = await selectStmt.all(k);
472
+ if (searchCandidatesRead.hasSubscribers) searchCandidatesRead.publish({
473
+ paths: rows.map((row)=>row.path)
474
+ });
408
475
  for (const row of rows)row.snippets = [];
409
476
  await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);
410
477
  return rows;