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
@@ -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) {
@@ -271,6 +274,9 @@ async function lineRangeFor(store, path, line) {
271
274
  return row ? `L${row.start_line}-${row.end_line}` : null;
272
275
  }
273
276
  function validateSearchOptions(opts) {
277
+ if (opts.explain !== undefined && typeof opts.explain !== 'boolean') {
278
+ throw new SenseError('SEARCH_OPTION_INVALID', `search option "explain" must be a boolean, got ${String(opts.explain)}`);
279
+ }
274
280
  for (const [name, value] of [
275
281
  [
276
282
  'k',
@@ -308,38 +314,99 @@ export async function hydrateSearchRows(store, cfg, rows, matchedPaths, terms, o
308
314
  if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path, lineNumberAt(text, offset)) : null;
309
315
  }
310
316
  }
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 = {}) {
317
+ /**
318
+ * Searches one committed published generation. Provider work precedes the final read transaction;
319
+ * cancellation waits for active native work and transaction cleanup before rejection.
320
+ */ export function search(store, cfg, terms, opts = {}) {
321
+ var _opts_signal;
322
+ if ((_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.aborted) return Promise.reject(opts.signal.reason);
314
323
  return serialQuery(store, ()=>searchIndexed(store, cfg, terms, opts));
315
324
  }
316
- async function searchIndexed(store, cfg, terms, opts) {
317
- validateSearchOptions(opts);
325
+ async function readSearchScope(store, cfg, terms, opts) {
326
+ const signature = await assertQuerySnapshot(store, cfg);
318
327
  const effective = resolveSearch(cfg, opts);
319
- const { k, signals } = effective;
320
328
  const allPaths = (await (await store.prepare('SELECT "path" FROM frontmatter')).all()).map((r)=>r.path);
321
329
  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
330
  let allowedPaths;
326
331
  try {
327
332
  allowedPaths = await narrowByWhere(store, scopePaths, effective.where);
328
333
  } catch (err) {
329
334
  throw searchError(err, terms, effective.where);
330
335
  }
331
- const fetch = Math.max(k * 3, 30);
336
+ const wantsVectors = effective.signals.vectors !== undefined;
337
+ if (wantsVectors) await ensureDocumentEmbeddings(store, cfg, allowedPaths);
338
+ const semanticEligible = wantsVectors && await scopeHasEmbeddings(store, cfg, allowedPaths);
339
+ return {
340
+ signature,
341
+ effective,
342
+ allPaths,
343
+ scopePaths,
344
+ allowedPaths,
345
+ semanticEligible
346
+ };
347
+ }
348
+ function needsSemanticPreparation(result) {
349
+ return !Array.isArray(result);
350
+ }
351
+ async function searchIndexed(store, cfg, terms, opts) {
352
+ var _opts_signal, _opts_signal1, _opts_signal2, _opts_signal3, _opts_signal4;
353
+ validateSearchOptions(opts);
354
+ const initial = resolveSearch(cfg, opts);
355
+ (_opts_signal = opts.signal) === null || _opts_signal === void 0 ? void 0 : _opts_signal.throwIfAborted();
332
356
  // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a
333
357
  // local path with missing files errors here, since nothing will ever fetch it for itself.
334
- const wantsVectors = signals.vectors !== undefined;
358
+ const wantsVectors = initial.signals.vectors !== undefined;
335
359
  if (wantsVectors) {
336
360
  const e = embedConfig(cfg); // validate.ts guarantees this is set whenever "vectors" is declared
337
361
  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`);
362
+ 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`);
363
+ }
364
+ }
365
+ let prepared;
366
+ let preparedSignature;
367
+ if (wantsVectors) {
368
+ var _opts_signal5;
369
+ // This first snapshot only decides whether a semantic query can contribute. It closes before
370
+ // provider/model work, and the complete scope/readiness decision is repeated below.
371
+ const preflight = await store.transaction(()=>readSearchScope(store, cfg, terms, opts));
372
+ (_opts_signal5 = opts.signal) === null || _opts_signal5 === void 0 ? void 0 : _opts_signal5.throwIfAborted();
373
+ if (preflight.semanticEligible) {
374
+ var _opts_signal6;
375
+ preparedSignature = preflight.signature;
376
+ prepared = await prepareSemanticQuery(cfg, terms, {
377
+ signal: opts.signal
378
+ });
379
+ (_opts_signal6 = opts.signal) === null || _opts_signal6 === void 0 ? void 0 : _opts_signal6.throwIfAborted();
339
380
  }
340
- await ensureDocumentEmbeddings(store, cfg, allowedPaths);
341
381
  }
342
- const semanticEnabled = wantsVectors && await scopeHasEmbeddings(store, cfg, allowedPaths);
382
+ (_opts_signal1 = opts.signal) === null || _opts_signal1 === void 0 ? void 0 : _opts_signal1.throwIfAborted();
383
+ let result = await store.transaction(()=>searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));
384
+ (_opts_signal2 = opts.signal) === null || _opts_signal2 === void 0 ? void 0 : _opts_signal2.throwIfAborted();
385
+ if (!needsSemanticPreparation(result)) return result;
386
+ // The scope became vector-eligible after an empty/ineligible preflight. Leave that snapshot,
387
+ // prepare once, then take a fresh final snapshot rather than waiting on the provider inside it.
388
+ preparedSignature = result.signature;
389
+ prepared = await prepareSemanticQuery(cfg, terms, {
390
+ signal: opts.signal
391
+ });
392
+ (_opts_signal3 = opts.signal) === null || _opts_signal3 === void 0 ? void 0 : _opts_signal3.throwIfAborted();
393
+ result = await store.transaction(()=>searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));
394
+ (_opts_signal4 = opts.signal) === null || _opts_signal4 === void 0 ? void 0 : _opts_signal4.throwIfAborted();
395
+ 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');
396
+ return result;
397
+ }
398
+ async function searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature) {
399
+ const { signature, effective, allPaths, scopePaths, allowedPaths, semanticEligible } = await readSearchScope(store, cfg, terms, opts);
400
+ if (prepared !== undefined && signature !== preparedSignature) {
401
+ 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');
402
+ }
403
+ if (semanticEligible && prepared === undefined) return {
404
+ signature
405
+ };
406
+ const { k, signals } = effective;
407
+ const fetch = Math.max(k * 3, 30);
408
+ const scopeActive = scopePaths.size < allPaths.length;
409
+ const semanticEnabled = semanticEligible && prepared !== undefined;
343
410
  // --where applies inside the candidate query (a post-filter would drop matches ranked past
344
411
  // the pool) and again on the final select, for link-derived rows.
345
412
  const scope = effective.where;
@@ -361,7 +428,7 @@ async function searchIndexed(store, cfg, terms, opts) {
361
428
  const syntaxError = bareTermSyntaxError(terms);
362
429
  if (syntaxError) throw syntaxError;
363
430
  try {
364
- matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words);
431
+ matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words, opts.explain);
365
432
  } catch (err) {
366
433
  throw searchError(err, terms, scope);
367
434
  }
@@ -369,11 +436,11 @@ async function searchIndexed(store, cfg, terms, opts) {
369
436
  // Every store's lexical `snippets` is NULL; the snippet text is always computed below.
370
437
  // Distinguish from via='link' rows, which never appear in matchRows.
371
438
  const matchedPaths = new Set(matchRows.map((r)=>r.path));
372
- if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links);
439
+ if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links, opts.explain);
373
440
  let chunkLines = new Map();
374
441
  let chunkSimilarity = new Map();
375
- if (semanticEnabled) {
376
- ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, signals.vectors));
442
+ if (semanticEnabled && signals.vectors !== undefined && prepared !== undefined) {
443
+ ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, prepared, candidates, fetch, allowedPaths, allPaths.length, signals.vectors, opts.explain));
377
444
  }
378
445
  await store.exec('DROP TABLE IF EXISTS _search');
379
446
  // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the
@@ -403,9 +470,17 @@ async function searchIndexed(store, cfg, terms, opts) {
403
470
  // overwritten below, always to an array, never left null (DESIGN.md).
404
471
  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
472
  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 ?`);
473
+ ${where} ORDER BY _search.score DESC, _search."path" ASC LIMIT ?`);
407
474
  const rows = await selectStmt.all(k);
475
+ if (searchCandidatesRead.hasSubscribers) searchCandidatesRead.publish({
476
+ paths: rows.map((row)=>row.path)
477
+ });
408
478
  for (const row of rows)row.snippets = [];
409
479
  await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);
480
+ if (opts.explain) {
481
+ var _ref;
482
+ var _candidates_get;
483
+ for (const row of rows)row.explanation = (_ref = (_candidates_get = candidates.get(row.path)) === null || _candidates_get === void 0 ? void 0 : _candidates_get.explanation) !== null && _ref !== void 0 ? _ref : [];
484
+ }
410
485
  return rows;
411
486
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/search.ts"],"sourcesContent":["import { stemmer } from 'stemmer';\nimport type { ResolvedConfig } from '../config/index.ts';\nimport { embedConfig, featureEnabled, resolveSearch } from '../config/index.ts';\nimport { ensureDocumentEmbeddings } 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 { Row } from '../output/output.ts';\nimport { bareTermSyntaxError, searchError } from '../output/search-error.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\nimport { foldForSearch, hasUnspacedRun, searchTokens, unspacedRuns } from '../text/segment.ts';\nimport { materializeScope, narrowByWhere, rawScope, scopeHasEmbeddings } from './scope.ts';\nimport { linksCandidates, vectorsCandidates, wordsCandidates } from './signals.ts';\n\n// Default budget behind --snippet-char-limit/--snippet-count-limit: the limit is whatever the\n// caller passes, these only say what happens when they pass nothing (DESIGN.md \"Search snippets\").\nexport const SNIPPET_CHAR_LIMIT_DEFAULT = 80;\nexport const SNIPPET_COUNT_LIMIT_DEFAULT = 1;\n\n// Bare terms from an FTS5 query string: strips operators/quoting, not real terms, so the\n// snippet scan matches what the query matched -- FTS5 operators are uppercase-only, so this is too.\nfunction extractBareTerms(query: string): string[] {\n const cleaned = query\n .replace(/\"/g, ' ')\n .replace(/[()*]/g, ' ')\n .replace(/\\b(AND|OR|NOT|NEAR)\\b(\\/\\d+)?/g, ' ');\n return cleaned\n .split(/\\s+/)\n .map((tok) => tok.replace(/^[A-Za-z_]\\w*:/, '')) // column filter, e.g. title:term\n .flatMap((tok) => searchTokens(tok).map(({ text }) => text))\n .map((tok) => tok.toLowerCase().trim())\n .filter((tok) => tok.length > 0);\n}\n\n// House convention (src/chunk/group.ts): Intl.Segmenter word boundaries, not a bare \\b, which\n// is ASCII-only. Built on first use: construction is a few ms, paid only when a term matches.\nlet wordSegmenter: Intl.Segmenter | undefined;\n\nfunction findOccurrences(text: string, terms: string[]): Array<{ start: number; end: number; term: string }> {\n const occ: Array<{ start: number; end: number; term: string }> = [];\n const textLower = text.toLowerCase();\n\n // Unspaced scripts (UNSPACED_SCRIPTS) mark no word boundaries: the sense-setup skill promises\n // substring semantics there, so a matching term is found wherever it occurs in the run.\n const unspacedTerms = terms.filter(hasUnspacedRun);\n if (unspacedTerms.length > 0) {\n for (const run of unspacedRuns(text)) {\n const runLower = textLower.slice(run.start, run.end);\n for (const term of unspacedTerms) {\n let idx = 0;\n for (;;) {\n const found = runLower.indexOf(term, idx);\n if (found === -1) break;\n occ.push({ start: run.start + found, end: run.start + found + term.length, term });\n idx = found + term.length;\n }\n }\n }\n }\n\n // Spaced scripts: a word is marked when its porter stem equals a query term's stem, the same\n // algorithm both indexes tokenize with (sqlite's TOKENIZE = 'porter unicode61'; duckdb's fts default).\n const spacedTerms = terms.filter((t) => !hasUnspacedRun(t));\n if (spacedTerms.length > 0) {\n const termByStem = new Map(spacedTerms.map((t) => [stemmer(foldForSearch(t)), t]));\n const stemInitials = new Set(Array.from(termByStem.keys(), (term) => term[0]));\n for (const match of searchTokens(text)) {\n const segment = match.text;\n if (match.unspaced) continue;\n const folded = foldForSearch(segment);\n // Porter stemming changes suffixes only, so a different initial cannot match a query stem.\n if (!stemInitials.has(folded[0])) continue;\n const term = termByStem.get(stemmer(folded));\n if (term !== undefined) occ.push({ start: match.start, end: match.end, term });\n }\n }\n\n return occ.sort((a, b) => a.start - b.start || b.end - a.end);\n}\n\n// Segment boundaries of the whole text, word granularity: the only cut points a snippet edge\n// may land on. One rule for every script (DESIGN.md \"Cutting\") -- a regex \\b is ASCII-only and\n// would mis-cut Japanese, Chinese and Thai, which Intl.Segmenter divides by dictionary instead.\nfunction wordBoundaries(text: string): number[] {\n wordSegmenter ??= new Intl.Segmenter('en', { granularity: 'word' });\n const bounds = [0];\n for (const { index, segment } of wordSegmenter.segment(text)) bounds.push(index + segment.length);\n return bounds;\n}\n\n// bounds is sorted: snaps are binary searches, since each runs once per candidate window.\nfunction snapForward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] < pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\nfunction snapBackward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n let result = bounds[0];\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= pos) {\n lo = mid;\n result = bounds[mid];\n } else hi = mid - 1;\n }\n return result;\n}\n\nfunction firstBoundAfter(bounds: number[], pos: number): number | undefined {\n let lo = 0;\n let hi = bounds.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] <= pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\n// A raw, unmarked upper bound: [start, end) inside [anchor, anchor+charLimit), edges on\n// boundaries. Markup (guillemets, ellipses) added on top of this can push the rendered result\n// past charLimit, which is what fitEnd() below corrects for; this is only the starting guess.\n// A single segment spanning the whole target window (a word longer than the char limit) is the\n// one case where holding the boundary rule means the budget is not a hard ceiling.\nfunction cutWindow(text: string, bounds: number[], anchor: number, charLimit: number): { start: number; end: number } {\n const rawEnd = Math.min(text.length, anchor + charLimit);\n const start = snapForward(bounds, anchor);\n let end = snapBackward(bounds, rawEnd);\n if (end <= start) end = firstBoundAfter(bounds, start) ?? text.length;\n return { start, end: Math.min(end, text.length) };\n}\n\n// The largest boundary <= upperBound, > floor, whose rendered [*, end) fits charLimit. Markup\n// only ever adds characters as end grows, so rendered length is non-decreasing in end and\n// shrinking from upperBound is guaranteed to find the true fit rather than guess at it.\n// `floor` is the one end this never shrinks past (the anchor's own marked word, or the first\n// word in the unmarked fallback): DESIGN.md's budget is a ceiling except here, where returning\n// nothing is worse than a single word that overruns it.\nfunction fitEnd(render: (end: number) => string, bounds: number[], floor: number, upperBound: number, charLimit: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= upperBound) lo = mid;\n else hi = mid - 1;\n }\n for (; lo >= 0 && bounds[lo] > floor; lo--) {\n if (render(bounds[lo]).length <= charLimit) return bounds[lo];\n }\n return floor;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\ninterface SnippetWindow extends Span {\n distinct: number;\n count: number;\n}\n\n// occ is start-sorted: a window's first candidate occurrence is a binary search, not a walk.\nfunction firstOccAtOrAfter(occ: Array<{ start: number; end: number; term: string }>, pos: number): number {\n let lo = 0;\n let hi = occ.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (occ[mid].start < pos) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n\nfunction renderWindow(text: string, occ: Array<{ start: number; end: number; term: string }>, win: Span): string {\n let out = '';\n let cursor = win.start;\n for (let i = firstOccAtOrAfter(occ, win.start); i < occ.length && occ[i].start < win.end; i++) {\n const o = occ[i];\n if (o.end > win.end) continue;\n // Occurrences of duplicate or substring-overlapping terms (\"test tests\") can overlap;\n // emitting each would duplicate document text. Keep the first, absorb the rest.\n if (o.start < cursor) continue;\n out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;\n cursor = o.end;\n }\n out += text.slice(cursor, win.end);\n const prefix = win.start > 0 ? '…' : '';\n const suffix = win.end < text.length ? '…' : '';\n return `${prefix}${out.replace(/\\s+/g, ' ')}${suffix}`;\n}\n\n// Scores every occurrence-anchored candidate window: most distinct terms wins, ties broken by\n// most total hits -- coverage now that occurrences are real words, not the false-fragment counts\n// a substring scan produced. Selection itself is unevaluated (DESIGN.md \"Selection, unsettled\").\n// The window's `end` is fitted to the *rendered* text, markup included, not the raw span: a\n// window dense with marks (every guillemet pair adds two characters) shrinks to fit the same\n// budget a sparse one fills almost exactly.\nfunction scoreWindows(occ: Array<{ start: number; end: number; term: string }>, bounds: number[], text: string, charLimit: number): SnippetWindow[] {\n const candidates: SnippetWindow[] = [];\n for (let i = 0; i < occ.length; i++) {\n const winEnd = occ[i].start + charLimit;\n const seen = new Set<string>();\n let count = 0;\n for (let j = i; j < occ.length && occ[j].start < winEnd; j++) {\n seen.add(occ[j].term);\n count++;\n }\n const start = snapForward(bounds, occ[i].start);\n const floor = Math.max(start, occ[i].end);\n const upperBound = Math.max(floor, cutWindow(text, bounds, occ[i].start, charLimit).end);\n const end = fitEnd((e) => renderWindow(text, occ, { start, end: e }), bounds, floor, upperBound, charLimit);\n candidates.push({ start, end, distinct: seen.size, count });\n }\n return candidates;\n}\n\nfunction overlaps(a: Span, b: Span): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\n// Same scoring function as a single snippet, kept rather than discarded (DESIGN.md \"Several\n// snippets per note\"): rank by score, take the top non-overlapping windows, then reorder to the\n// note's own order, since several passages from one note read as an abridged note.\nfunction selectWindows(candidates: SnippetWindow[], countLimit: number): { chosen: SnippetWindow[]; best: SnippetWindow } {\n const ranked = candidates.slice().sort((a, b) => b.distinct - a.distinct || b.count - a.count);\n const chosen: SnippetWindow[] = [];\n for (const c of ranked) {\n if (chosen.length >= countLimit) break;\n if (chosen.some((s) => overlaps(s, c))) continue;\n chosen.push(c);\n }\n chosen.sort((a, b) => a.start - b.start);\n return { chosen, best: ranked[0] };\n}\n\n// Builds bounded marked passages and returns the best window's offset for section lookup.\nexport function computeSnippets(text: string, terms: string[], charLimit: number, countLimit: number): { snippets: string[]; offset: number } {\n const occ = findOccurrences(text, terms);\n const bounds = wordBoundaries(text);\n if (occ.length === 0) {\n // A term matched only via title/summary, not this body text, finds no occurrence here;\n // fall back to the doc's start, unmarked.\n const render = (end: number): string => `${text.slice(0, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`;\n const floor = bounds.find((b) => b > 0) ?? text.length;\n const upperBound = Math.max(floor, cutWindow(text, bounds, 0, charLimit).end);\n const end = fitEnd(render, bounds, floor, upperBound, charLimit);\n return { snippets: [render(end)], offset: 0 };\n }\n const candidates = scoreWindows(occ, bounds, text, charLimit);\n const { chosen, best } = selectWindows(candidates, countLimit);\n return { snippets: chosen.map((win) => renderWindow(text, occ, win)), offset: best.start };\n}\n\n// Converts an offset in the original text to its one-based line number.\nexport function lineNumberAt(text: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n\nasync function lineRangeFor(store: Store, path: string, line: number): Promise<string | null> {\n const stmt = await store.prepare('SELECT start_line, end_line FROM sections WHERE \"path\" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1');\n const row = (await stmt.get(path, line, line)) as { start_line: number; end_line: number } | undefined;\n return row ? `L${row.start_line}-${row.end_line}` : null;\n}\n\nexport interface SearchOptions {\n k?: number;\n where?: string; // SQL fragment against frontmatter alias `f`, e.g. \"f.status = 'active'\"\n preset?: string; // named preset; unknown name throws listing declared presets, undefined -> \"default\"\n include?: string[]; // ad hoc scope override (repeatable --include); independent of exclude\n exclude?: string[]; // ad hoc scope override (repeatable --exclude); independent of include\n noExclude?: boolean; // --no-exclude: drop the preset's exclude for this command\n snippetCharLimit?: number; // --snippet-char-limit; defaults to SNIPPET_CHAR_LIMIT_DEFAULT\n snippetCountLimit?: number; // --snippet-count-limit; defaults to SNIPPET_COUNT_LIMIT_DEFAULT\n}\n\nfunction validateSearchOptions(opts: SearchOptions): void {\n for (const [name, value] of [\n ['k', opts.k],\n ['snippetCharLimit', opts.snippetCharLimit],\n ['snippetCountLimit', opts.snippetCountLimit],\n ] as const) {\n if (value !== undefined && (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)) {\n throw new SenseError('SEARCH_OPTION_INVALID', `search option \"${name}\" must be a positive finite integer, got ${String(value)}`);\n }\n }\n}\n\n// Hydrates already-ranked rows with snippets and containing-section ranges without reranking.\nexport async function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: Row[], matchedPaths: Set<string>, terms: string, opts: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'> = {}): Promise<void> {\n if (matchedPaths.size === 0) return;\n const bareTerms = extractBareTerms(terms);\n const charLimit = opts.snippetCharLimit ?? SNIPPET_CHAR_LIMIT_DEFAULT;\n const countLimit = opts.snippetCountLimit ?? SNIPPET_COUNT_LIMIT_DEFAULT;\n const sourceStmt = await store.prepare('SELECT text FROM indexed_sources WHERE \"path\" = ?');\n for (const row of rows) {\n if (!matchedPaths.has(row.path as string)) continue;\n const source = (await sourceStmt.get(row.path)) as { text: string } | undefined;\n if (!source) throw new SenseError('INDEX_NOT_READY', `indexed source text is missing for ${String(row.path)}; run \\`sense build --force\\` (or library build(config, { force: true })) before querying`);\n const text = source.text;\n const { snippets, offset } = computeSnippets(text, bareTerms, charLimit, countLimit);\n row.snippets = snippets;\n if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path as string, lineNumberAt(text, offset)) : null;\n }\n}\n\n// The declared or defaulted signals compose via RRF; `via` names which ones produced each row.\n// `opts` arrives already resolved (config.ts:resolveSearch).\nexport function search(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions = {}): Promise<Row[]> {\n return serialQuery(store, () => searchIndexed(store, cfg, terms, opts));\n}\n\nasync function searchIndexed(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions): Promise<Row[]> {\n validateSearchOptions(opts);\n const effective = resolveSearch(cfg, opts);\n const { k, signals } = effective;\n\n const allPaths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((r) => r.path);\n const scopePaths = await rawScope(store, cfg, opts, allPaths);\n const scopeActive = scopePaths.size < allPaths.length;\n // The set every candidate pool must be filtered to before truncation: scope narrowed by\n // --where, the same composition scopedPaths() gives the other commands.\n let allowedPaths: Set<string>;\n try {\n allowedPaths = await narrowByWhere(store, scopePaths, effective.where);\n } catch (err) {\n throw searchError(err as Error, terms, effective.where);\n }\n const fetch = Math.max(k * 3, 30);\n\n // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a\n // local path with missing files errors here, since nothing will ever fetch it for itself.\n const wantsVectors = signals.vectors !== undefined;\n if (wantsVectors) {\n const e = embedConfig(cfg); // validate.ts guarantees this is set whenever \"vectors\" is declared\n if (localModelMissing(e)) {\n 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`);\n }\n await ensureDocumentEmbeddings(store, cfg, allowedPaths);\n }\n const semanticEnabled = wantsVectors && (await scopeHasEmbeddings(store, cfg, allowedPaths));\n\n // --where applies inside the candidate query (a post-filter would drop matches ranked past\n // the pool) and again on the final select, for link-derived rows.\n const scope = effective.where;\n const whereJoin = scope ? `JOIN frontmatter f ON f.\"path\" = content.path` : '';\n const whereCond = scope ? `AND (${scope})` : '';\n // Filtered before LIMIT, not after, or scoped notes ranking below the global top-`fetch`\n // never reach the filter; joined against a temp table since real scopes exceed SQLITE_MAX_VARIABLE_NUMBER.\n if (scopeActive) await materializeScope(store, '_search_scope', scopePaths);\n const scopeCond = scopeActive ? `AND content.path IN (SELECT \"path\" FROM _search_scope)` : '';\n\n const candidates = new Map<string, { score: number; via: string }>();\n let matchRows: LexicalHit[] = [];\n const hasEmptyPhrase = [...terms.matchAll(/\"([^\"]*)\"/g)].some((match) => !/[\\p{L}\\p{N}_]/u.test(match[1]));\n const hasBareSyntaxOnly = /[()]/u.test(terms.replace(/\"[^\"]*\"/g, ' '));\n // A query that is only whitespace or punctuation has no lexical rows; other declared signals\n // compose normally. In particular, unspaced-script punctuation can rewrite to an empty MATCH.\n if (signals.words !== undefined && terms.trim() !== '' && (/[\\p{L}\\p{N}_]/u.test(terms) || hasBareSyntaxOnly) && !hasEmptyPhrase) {\n const syntaxError = bareTermSyntaxError(terms);\n if (syntaxError) throw syntaxError;\n try {\n matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words);\n } catch (err) {\n throw searchError(err as Error, terms, scope);\n }\n }\n // Every store's lexical `snippets` is NULL; the snippet text is always computed below.\n // Distinguish from via='link' rows, which never appear in matchRows.\n const matchedPaths = new Set(matchRows.map((r) => r.path));\n\n if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links);\n\n let chunkLines = new Map<string, string>();\n let chunkSimilarity = new Map<string, number>();\n if (semanticEnabled) {\n ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, signals.vectors as number));\n }\n\n await store.exec('DROP TABLE IF EXISTS _search');\n // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the\n // rounded score/similarity are printed at full precision in rows.\n await store.exec('CREATE TEMP TABLE _search (\"path\" TEXT PRIMARY KEY, score DOUBLE, via TEXT, lines TEXT, similarity DOUBLE)');\n if (candidates.size > 0) {\n await store.runBatch(\n 'INSERT INTO _search (\"path\", score, via, lines, similarity) VALUES (?, ?, ?, ?, ?)',\n [...candidates].map(([path, c]) => [path, c.score, c.via, chunkLines.get(path) ?? null, chunkSimilarity.get(path) ?? null])\n );\n }\n\n // Reapplies --where even though _search is already scope+where filtered, since the join to\n // frontmatter is already needed for the path column.\n const where = scope ? `WHERE (${scope})` : '';\n // lines: semantic rows carry their chunk's range, lexical rows gain one below,\n // everything else stays null; similarity stays semantic-only.\n const similarityCol = semanticEnabled ? ', _search.similarity' : '';\n // NULL AS snippets holds this column's position (between summary and via) for every row;\n // overwritten below, always to an array, never left null (DESIGN.md).\n const selectStmt = await store.prepare(\n `SELECT f.\"path\" AS path, content.title, content.summary, NULL AS snippets, _search.via, round(_search.score, 4) AS score, _search.lines${similarityCol}\n FROM _search JOIN frontmatter f ON f.\"path\" = _search.\"path\" JOIN content ON content.path = _search.\"path\"\n ${where} ORDER BY _search.score DESC LIMIT ?`\n );\n const rows = (await selectStmt.all(k)) as Row[];\n for (const row of rows) row.snippets = [];\n\n await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);\n\n return rows;\n}\n"],"names":["stemmer","embedConfig","featureEnabled","resolveSearch","ensureDocumentEmbeddings","localModelMissing","MODEL_FILENAMES","SenseError","serialQuery","bareTermSyntaxError","searchError","foldForSearch","hasUnspacedRun","searchTokens","unspacedRuns","materializeScope","narrowByWhere","rawScope","scopeHasEmbeddings","linksCandidates","vectorsCandidates","wordsCandidates","SNIPPET_CHAR_LIMIT_DEFAULT","SNIPPET_COUNT_LIMIT_DEFAULT","extractBareTerms","query","cleaned","replace","split","map","tok","flatMap","text","toLowerCase","trim","filter","length","wordSegmenter","findOccurrences","terms","occ","textLower","unspacedTerms","run","runLower","slice","start","end","term","idx","found","indexOf","push","spacedTerms","t","termByStem","Map","stemInitials","Set","Array","from","keys","match","segment","unspaced","folded","has","get","undefined","sort","a","b","wordBoundaries","Intl","Segmenter","granularity","bounds","index","snapForward","pos","lo","hi","mid","snapBackward","result","firstBoundAfter","cutWindow","anchor","charLimit","rawEnd","Math","min","fitEnd","render","floor","upperBound","firstOccAtOrAfter","renderWindow","win","out","cursor","i","o","prefix","suffix","scoreWindows","candidates","winEnd","seen","count","j","add","max","e","distinct","size","overlaps","selectWindows","countLimit","ranked","chosen","c","some","s","best","computeSnippets","find","snippets","offset","lineNumberAt","line","charCodeAt","lineRangeFor","store","path","stmt","prepare","row","start_line","end_line","validateSearchOptions","opts","name","value","k","snippetCharLimit","snippetCountLimit","Number","isFinite","isInteger","String","hydrateSearchRows","cfg","rows","matchedPaths","bareTerms","sourceStmt","source","lines","search","searchIndexed","effective","signals","allPaths","all","r","scopePaths","scopeActive","allowedPaths","where","err","fetch","wantsVectors","vectors","presetName","model","semanticEnabled","scope","whereJoin","whereCond","scopeCond","matchRows","hasEmptyPhrase","matchAll","test","hasBareSyntaxOnly","words","syntaxError","links","chunkLines","chunkSimilarity","exec","runBatch","score","via","similarityCol","selectStmt"],"mappings":"AAAA,SAASA,OAAO,QAAQ,UAAU;AAElC,SAASC,WAAW,EAAEC,cAAc,EAAEC,aAAa,QAAQ,qBAAqB;AAChF,SAASC,wBAAwB,QAAQ,oBAAoB;AAC7D,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,oBAAoB;AACvE,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,WAAW,QAAQ,yBAAyB;AAErD,SAASC,mBAAmB,EAAEC,WAAW,QAAQ,4BAA4B;AAE7E,SAASC,aAAa,EAAEC,cAAc,EAAEC,YAAY,EAAEC,YAAY,QAAQ,qBAAqB;AAC/F,SAASC,gBAAgB,EAAEC,aAAa,EAAEC,QAAQ,EAAEC,kBAAkB,QAAQ,aAAa;AAC3F,SAASC,eAAe,EAAEC,iBAAiB,EAAEC,eAAe,QAAQ,eAAe;AAEnF,8FAA8F;AAC9F,mGAAmG;AACnG,OAAO,MAAMC,6BAA6B,GAAG;AAC7C,OAAO,MAAMC,8BAA8B,EAAE;AAE7C,yFAAyF;AACzF,oGAAoG;AACpG,SAASC,iBAAiBC,KAAa;IACrC,MAAMC,UAAUD,MACbE,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,kCAAkC;IAC7C,OAAOD,QACJE,KAAK,CAAC,OACNC,GAAG,CAAC,CAACC,MAAQA,IAAIH,OAAO,CAAC,kBAAkB,KAAK,iCAAiC;KACjFI,OAAO,CAAC,CAACD,MAAQjB,aAAaiB,KAAKD,GAAG,CAAC,CAAC,EAAEG,IAAI,EAAE,GAAKA,OACrDH,GAAG,CAAC,CAACC,MAAQA,IAAIG,WAAW,GAAGC,IAAI,IACnCC,MAAM,CAAC,CAACL,MAAQA,IAAIM,MAAM,GAAG;AAClC;AAEA,8FAA8F;AAC9F,8FAA8F;AAC9F,IAAIC;AAEJ,SAASC,gBAAgBN,IAAY,EAAEO,KAAe;IACpD,MAAMC,MAA2D,EAAE;IACnE,MAAMC,YAAYT,KAAKC,WAAW;IAElC,8FAA8F;IAC9F,wFAAwF;IACxF,MAAMS,gBAAgBH,MAAMJ,MAAM,CAACvB;IACnC,IAAI8B,cAAcN,MAAM,GAAG,GAAG;QAC5B,KAAK,MAAMO,OAAO7B,aAAakB,MAAO;YACpC,MAAMY,WAAWH,UAAUI,KAAK,CAACF,IAAIG,KAAK,EAAEH,IAAII,GAAG;YACnD,KAAK,MAAMC,QAAQN,cAAe;gBAChC,IAAIO,MAAM;gBACV,OAAS;oBACP,MAAMC,QAAQN,SAASO,OAAO,CAACH,MAAMC;oBACrC,IAAIC,UAAU,CAAC,GAAG;oBAClBV,IAAIY,IAAI,CAAC;wBAAEN,OAAOH,IAAIG,KAAK,GAAGI;wBAAOH,KAAKJ,IAAIG,KAAK,GAAGI,QAAQF,KAAKZ,MAAM;wBAAEY;oBAAK;oBAChFC,MAAMC,QAAQF,KAAKZ,MAAM;gBAC3B;YACF;QACF;IACF;IAEA,6FAA6F;IAC7F,uGAAuG;IACvG,MAAMiB,cAAcd,MAAMJ,MAAM,CAAC,CAACmB,IAAM,CAAC1C,eAAe0C;IACxD,IAAID,YAAYjB,MAAM,GAAG,GAAG;QAC1B,MAAMmB,aAAa,IAAIC,IAAIH,YAAYxB,GAAG,CAAC,CAACyB,IAAM;gBAACtD,QAAQW,cAAc2C;gBAAKA;aAAE;QAChF,MAAMG,eAAe,IAAIC,IAAIC,MAAMC,IAAI,CAACL,WAAWM,IAAI,IAAI,CAACb,OAASA,IAAI,CAAC,EAAE;QAC5E,KAAK,MAAMc,SAASjD,aAAamB,MAAO;YACtC,MAAM+B,UAAUD,MAAM9B,IAAI;YAC1B,IAAI8B,MAAME,QAAQ,EAAE;YACpB,MAAMC,SAAStD,cAAcoD;YAC7B,2FAA2F;YAC3F,IAAI,CAACN,aAAaS,GAAG,CAACD,MAAM,CAAC,EAAE,GAAG;YAClC,MAAMjB,OAAOO,WAAWY,GAAG,CAACnE,QAAQiE;YACpC,IAAIjB,SAASoB,WAAW5B,IAAIY,IAAI,CAAC;gBAAEN,OAAOgB,MAAMhB,KAAK;gBAAEC,KAAKe,MAAMf,GAAG;gBAAEC;YAAK;QAC9E;IACF;IAEA,OAAOR,IAAI6B,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAExB,KAAK,GAAGyB,EAAEzB,KAAK,IAAIyB,EAAExB,GAAG,GAAGuB,EAAEvB,GAAG;AAC9D;AAEA,6FAA6F;AAC7F,+FAA+F;AAC/F,gGAAgG;AAChG,SAASyB,eAAexC,IAAY;IAClCK,0BAAAA,2BAAAA,gBAAAA,gBAAkB,IAAIoC,KAAKC,SAAS,CAAC,MAAM;QAAEC,aAAa;IAAO;IACjE,MAAMC,SAAS;QAAC;KAAE;IAClB,KAAK,MAAM,EAAEC,KAAK,EAAEd,OAAO,EAAE,IAAI1B,cAAc0B,OAAO,CAAC/B,MAAO4C,OAAOxB,IAAI,CAACyB,QAAQd,QAAQ3B,MAAM;IAChG,OAAOwC;AACT;AAEA,0FAA0F;AAC1F,SAASE,YAAYF,MAAgB,EAAEG,GAAW;IAChD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIL,MAAM,CAACM,IAAI,GAAGH,KAAKC,KAAKE,MAAM;aAC7BD,KAAKC;IACZ;IACA,OAAON,MAAM,CAACI,GAAG;AACnB;AAEA,SAASG,aAAaP,MAAgB,EAAEG,GAAW;IACjD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,IAAIgD,SAASR,MAAM,CAAC,EAAE;IACtB,MAAOI,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,KAAK,KAAM;QAC7B,IAAIL,MAAM,CAACM,IAAI,IAAIH,KAAK;YACtBC,KAAKE;YACLE,SAASR,MAAM,CAACM,IAAI;QACtB,OAAOD,KAAKC,MAAM;IACpB;IACA,OAAOE;AACT;AAEA,SAASC,gBAAgBT,MAAgB,EAAEG,GAAW;IACpD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM;IACtB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIL,MAAM,CAACM,IAAI,IAAIH,KAAKC,KAAKE,MAAM;aAC9BD,KAAKC;IACZ;IACA,OAAON,MAAM,CAACI,GAAG;AACnB;AAEA,wFAAwF;AACxF,8FAA8F;AAC9F,8FAA8F;AAC9F,+FAA+F;AAC/F,mFAAmF;AACnF,SAASM,UAAUtD,IAAY,EAAE4C,MAAgB,EAAEW,MAAc,EAAEC,SAAiB;QAI1DH;IAHxB,MAAMI,SAASC,KAAKC,GAAG,CAAC3D,KAAKI,MAAM,EAAEmD,SAASC;IAC9C,MAAM1C,QAAQgC,YAAYF,QAAQW;IAClC,IAAIxC,MAAMoC,aAAaP,QAAQa;IAC/B,IAAI1C,OAAOD,OAAOC,OAAMsC,mBAAAA,gBAAgBT,QAAQ9B,oBAAxBuC,8BAAAA,mBAAkCrD,KAAKI,MAAM;IACrE,OAAO;QAAEU;QAAOC,KAAK2C,KAAKC,GAAG,CAAC5C,KAAKf,KAAKI,MAAM;IAAE;AAClD;AAEA,8FAA8F;AAC9F,0FAA0F;AAC1F,wFAAwF;AACxF,6FAA6F;AAC7F,+FAA+F;AAC/F,wDAAwD;AACxD,SAASwD,OAAOC,MAA+B,EAAEjB,MAAgB,EAAEkB,KAAa,EAAEC,UAAkB,EAAEP,SAAiB;IACrH,IAAIR,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,KAAK,KAAM;QAC7B,IAAIL,MAAM,CAACM,IAAI,IAAIa,YAAYf,KAAKE;aAC/BD,KAAKC,MAAM;IAClB;IACA,MAAOF,MAAM,KAAKJ,MAAM,CAACI,GAAG,GAAGc,OAAOd,KAAM;QAC1C,IAAIa,OAAOjB,MAAM,CAACI,GAAG,EAAE5C,MAAM,IAAIoD,WAAW,OAAOZ,MAAM,CAACI,GAAG;IAC/D;IACA,OAAOc;AACT;AAYA,6FAA6F;AAC7F,SAASE,kBAAkBxD,GAAwD,EAAEuC,GAAW;IAC9F,IAAIC,KAAK;IACT,IAAIC,KAAKzC,IAAIJ,MAAM;IACnB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIzC,GAAG,CAAC0C,IAAI,CAACpC,KAAK,GAAGiC,KAAKC,KAAKE,MAAM;aAChCD,KAAKC;IACZ;IACA,OAAOF;AACT;AAEA,SAASiB,aAAajE,IAAY,EAAEQ,GAAwD,EAAE0D,GAAS;IACrG,IAAIC,MAAM;IACV,IAAIC,SAASF,IAAIpD,KAAK;IACtB,IAAK,IAAIuD,IAAIL,kBAAkBxD,KAAK0D,IAAIpD,KAAK,GAAGuD,IAAI7D,IAAIJ,MAAM,IAAII,GAAG,CAAC6D,EAAE,CAACvD,KAAK,GAAGoD,IAAInD,GAAG,EAAEsD,IAAK;QAC7F,MAAMC,IAAI9D,GAAG,CAAC6D,EAAE;QAChB,IAAIC,EAAEvD,GAAG,GAAGmD,IAAInD,GAAG,EAAE;QACrB,sFAAsF;QACtF,gFAAgF;QAChF,IAAIuD,EAAExD,KAAK,GAAGsD,QAAQ;QACtBD,OAAO,GAAGnE,KAAKa,KAAK,CAACuD,QAAQE,EAAExD,KAAK,EAAE,CAAC,EAAEd,KAAKa,KAAK,CAACyD,EAAExD,KAAK,EAAEwD,EAAEvD,GAAG,EAAE,CAAC,CAAC;QACtEqD,SAASE,EAAEvD,GAAG;IAChB;IACAoD,OAAOnE,KAAKa,KAAK,CAACuD,QAAQF,IAAInD,GAAG;IACjC,MAAMwD,SAASL,IAAIpD,KAAK,GAAG,IAAI,MAAM;IACrC,MAAM0D,SAASN,IAAInD,GAAG,GAAGf,KAAKI,MAAM,GAAG,MAAM;IAC7C,OAAO,GAAGmE,SAASJ,IAAIxE,OAAO,CAAC,QAAQ,OAAO6E,QAAQ;AACxD;AAEA,8FAA8F;AAC9F,iGAAiG;AACjG,iGAAiG;AACjG,4FAA4F;AAC5F,6FAA6F;AAC7F,4CAA4C;AAC5C,SAASC,aAAajE,GAAwD,EAAEoC,MAAgB,EAAE5C,IAAY,EAAEwD,SAAiB;IAC/H,MAAMkB,aAA8B,EAAE;IACtC,IAAK,IAAIL,IAAI,GAAGA,IAAI7D,IAAIJ,MAAM,EAAEiE,IAAK;QACnC,MAAMM,SAASnE,GAAG,CAAC6D,EAAE,CAACvD,KAAK,GAAG0C;QAC9B,MAAMoB,OAAO,IAAIlD;QACjB,IAAImD,QAAQ;QACZ,IAAK,IAAIC,IAAIT,GAAGS,IAAItE,IAAIJ,MAAM,IAAII,GAAG,CAACsE,EAAE,CAAChE,KAAK,GAAG6D,QAAQG,IAAK;YAC5DF,KAAKG,GAAG,CAACvE,GAAG,CAACsE,EAAE,CAAC9D,IAAI;YACpB6D;QACF;QACA,MAAM/D,QAAQgC,YAAYF,QAAQpC,GAAG,CAAC6D,EAAE,CAACvD,KAAK;QAC9C,MAAMgD,QAAQJ,KAAKsB,GAAG,CAAClE,OAAON,GAAG,CAAC6D,EAAE,CAACtD,GAAG;QACxC,MAAMgD,aAAaL,KAAKsB,GAAG,CAAClB,OAAOR,UAAUtD,MAAM4C,QAAQpC,GAAG,CAAC6D,EAAE,CAACvD,KAAK,EAAE0C,WAAWzC,GAAG;QACvF,MAAMA,MAAM6C,OAAO,CAACqB,IAAMhB,aAAajE,MAAMQ,KAAK;gBAAEM;gBAAOC,KAAKkE;YAAE,IAAIrC,QAAQkB,OAAOC,YAAYP;QACjGkB,WAAWtD,IAAI,CAAC;YAAEN;YAAOC;YAAKmE,UAAUN,KAAKO,IAAI;YAAEN;QAAM;IAC3D;IACA,OAAOH;AACT;AAEA,SAASU,SAAS9C,CAAO,EAAEC,CAAO;IAChC,OAAOD,EAAExB,KAAK,GAAGyB,EAAExB,GAAG,IAAIwB,EAAEzB,KAAK,GAAGwB,EAAEvB,GAAG;AAC3C;AAEA,4FAA4F;AAC5F,gGAAgG;AAChG,mFAAmF;AACnF,SAASsE,cAAcX,UAA2B,EAAEY,UAAkB;IACpE,MAAMC,SAASb,WAAW7D,KAAK,GAAGwB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAE2C,QAAQ,GAAG5C,EAAE4C,QAAQ,IAAI3C,EAAEsC,KAAK,GAAGvC,EAAEuC,KAAK;IAC7F,MAAMW,SAA0B,EAAE;IAClC,KAAK,MAAMC,KAAKF,OAAQ;QACtB,IAAIC,OAAOpF,MAAM,IAAIkF,YAAY;QACjC,IAAIE,OAAOE,IAAI,CAAC,CAACC,IAAMP,SAASO,GAAGF,KAAK;QACxCD,OAAOpE,IAAI,CAACqE;IACd;IACAD,OAAOnD,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAExB,KAAK,GAAGyB,EAAEzB,KAAK;IACvC,OAAO;QAAE0E;QAAQI,MAAML,MAAM,CAAC,EAAE;IAAC;AACnC;AAEA,0FAA0F;AAC1F,OAAO,SAASM,gBAAgB7F,IAAY,EAAEO,KAAe,EAAEiD,SAAiB,EAAE8B,UAAkB;IAClG,MAAM9E,MAAMF,gBAAgBN,MAAMO;IAClC,MAAMqC,SAASJ,eAAexC;IAC9B,IAAIQ,IAAIJ,MAAM,KAAK,GAAG;YAINwC;QAHd,uFAAuF;QACvF,0CAA0C;QAC1C,MAAMiB,SAAS,CAAC9C,MAAwB,GAAGf,KAAKa,KAAK,CAAC,GAAGE,KAAKpB,OAAO,CAAC,QAAQ,KAAKO,IAAI,KAAKa,MAAMf,KAAKI,MAAM,GAAG,MAAM,IAAI;QAC1H,MAAM0D,SAAQlB,eAAAA,OAAOkD,IAAI,CAAC,CAACvD,IAAMA,IAAI,gBAAvBK,0BAAAA,eAA6B5C,KAAKI,MAAM;QACtD,MAAM2D,aAAaL,KAAKsB,GAAG,CAAClB,OAAOR,UAAUtD,MAAM4C,QAAQ,GAAGY,WAAWzC,GAAG;QAC5E,MAAMA,MAAM6C,OAAOC,QAAQjB,QAAQkB,OAAOC,YAAYP;QACtD,OAAO;YAAEuC,UAAU;gBAAClC,OAAO9C;aAAK;YAAEiF,QAAQ;QAAE;IAC9C;IACA,MAAMtB,aAAaD,aAAajE,KAAKoC,QAAQ5C,MAAMwD;IACnD,MAAM,EAAEgC,MAAM,EAAEI,IAAI,EAAE,GAAGP,cAAcX,YAAYY;IACnD,OAAO;QAAES,UAAUP,OAAO3F,GAAG,CAAC,CAACqE,MAAQD,aAAajE,MAAMQ,KAAK0D;QAAO8B,QAAQJ,KAAK9E,KAAK;IAAC;AAC3F;AAEA,wEAAwE;AACxE,OAAO,SAASmF,aAAajG,IAAY,EAAEgG,MAAc;IACvD,IAAIE,OAAO;IACX,IAAK,IAAI7B,IAAI,GAAGA,IAAI2B,QAAQ3B,IAAK,IAAIrE,KAAKmG,UAAU,CAAC9B,OAAO,IAAI6B;IAChE,OAAOA;AACT;AAEA,eAAeE,aAAaC,KAAY,EAAEC,IAAY,EAAEJ,IAAY;IAClE,MAAMK,OAAO,MAAMF,MAAMG,OAAO,CAAC;IACjC,MAAMC,MAAO,MAAMF,KAAKpE,GAAG,CAACmE,MAAMJ,MAAMA;IACxC,OAAOO,MAAM,CAAC,CAAC,EAAEA,IAAIC,UAAU,CAAC,CAAC,EAAED,IAAIE,QAAQ,EAAE,GAAG;AACtD;AAaA,SAASC,sBAAsBC,IAAmB;IAChD,KAAK,MAAM,CAACC,MAAMC,MAAM,IAAI;QAC1B;YAAC;YAAKF,KAAKG,CAAC;SAAC;QACb;YAAC;YAAoBH,KAAKI,gBAAgB;SAAC;QAC3C;YAAC;YAAqBJ,KAAKK,iBAAiB;SAAC;KAC9C,CAAW;QACV,IAAIH,UAAU3E,aAAc,CAAA,CAAC+E,OAAOC,QAAQ,CAACL,UAAU,CAACI,OAAOE,SAAS,CAACN,UAAUA,SAAS,CAAA,GAAI;YAC9F,MAAM,IAAIxI,WAAW,yBAAyB,CAAC,eAAe,EAAEuI,KAAK,yCAAyC,EAAEQ,OAAOP,QAAQ;QACjI;IACF;AACF;AAEA,8FAA8F;AAC9F,OAAO,eAAeQ,kBAAkBlB,KAAY,EAAEmB,GAAmB,EAAEC,IAAW,EAAEC,YAAyB,EAAEnH,KAAa,EAAEsG,OAAsE,CAAC,CAAC;QAGtLA,wBACCA;IAHnB,IAAIa,aAAavC,IAAI,KAAK,GAAG;IAC7B,MAAMwC,YAAYnI,iBAAiBe;IACnC,MAAMiD,aAAYqD,yBAAAA,KAAKI,gBAAgB,cAArBJ,oCAAAA,yBAAyBvH;IAC3C,MAAMgG,cAAauB,0BAAAA,KAAKK,iBAAiB,cAAtBL,qCAAAA,0BAA0BtH;IAC7C,MAAMqI,aAAa,MAAMvB,MAAMG,OAAO,CAAC;IACvC,KAAK,MAAMC,OAAOgB,KAAM;QACtB,IAAI,CAACC,aAAaxF,GAAG,CAACuE,IAAIH,IAAI,GAAa;QAC3C,MAAMuB,SAAU,MAAMD,WAAWzF,GAAG,CAACsE,IAAIH,IAAI;QAC7C,IAAI,CAACuB,QAAQ,MAAM,IAAItJ,WAAW,mBAAmB,CAAC,mCAAmC,EAAE+I,OAAOb,IAAIH,IAAI,EAAE,yFAAyF,CAAC;QACtM,MAAMtG,OAAO6H,OAAO7H,IAAI;QACxB,MAAM,EAAE+F,QAAQ,EAAEC,MAAM,EAAE,GAAGH,gBAAgB7F,MAAM2H,WAAWnE,WAAW8B;QACzEmB,IAAIV,QAAQ,GAAGA;QACf,IAAIU,IAAIqB,KAAK,IAAI,MAAMrB,IAAIqB,KAAK,GAAG5J,eAAesJ,KAAK,cAAc,MAAMpB,aAAaC,OAAOI,IAAIH,IAAI,EAAYL,aAAajG,MAAMgG,WAAW;IACnJ;AACF;AAEA,+FAA+F;AAC/F,6DAA6D;AAC7D,OAAO,SAAS+B,OAAO1B,KAAY,EAAEmB,GAAmB,EAAEjH,KAAa,EAAEsG,OAAsB,CAAC,CAAC;IAC/F,OAAOrI,YAAY6H,OAAO,IAAM2B,cAAc3B,OAAOmB,KAAKjH,OAAOsG;AACnE;AAEA,eAAemB,cAAc3B,KAAY,EAAEmB,GAAmB,EAAEjH,KAAa,EAAEsG,IAAmB;IAChGD,sBAAsBC;IACtB,MAAMoB,YAAY9J,cAAcqJ,KAAKX;IACrC,MAAM,EAAEG,CAAC,EAAEkB,OAAO,EAAE,GAAGD;IAEvB,MAAME,WAAW,AAAE,CAAA,MAAM,AAAC,CAAA,MAAM9B,MAAMG,OAAO,CAAC,iCAAgC,EAAG4B,GAAG,EAAC,EAA+BvI,GAAG,CAAC,CAACwI,IAAMA,EAAE/B,IAAI;IACrI,MAAMgC,aAAa,MAAMrJ,SAASoH,OAAOmB,KAAKX,MAAMsB;IACpD,MAAMI,cAAcD,WAAWnD,IAAI,GAAGgD,SAAS/H,MAAM;IACrD,wFAAwF;IACxF,wEAAwE;IACxE,IAAIoI;IACJ,IAAI;QACFA,eAAe,MAAMxJ,cAAcqH,OAAOiC,YAAYL,UAAUQ,KAAK;IACvE,EAAE,OAAOC,KAAK;QACZ,MAAMhK,YAAYgK,KAAcnI,OAAO0H,UAAUQ,KAAK;IACxD;IACA,MAAME,QAAQjF,KAAKsB,GAAG,CAACgC,IAAI,GAAG;IAE9B,oFAAoF;IACpF,0FAA0F;IAC1F,MAAM4B,eAAeV,QAAQW,OAAO,KAAKzG;IACzC,IAAIwG,cAAc;QAChB,MAAM3D,IAAIhH,YAAYuJ,MAAM,oEAAoE;QAChG,IAAInJ,kBAAkB4G,IAAI;YACxB,MAAM,IAAI1G,WAAW,uBAAuB,CAAC,QAAQ,EAAE0J,UAAUa,UAAU,CAAC,mDAAmD,EAAE7D,EAAE8D,KAAK,CAAC,aAAa,EAAEzK,gBAAgB,uHAAuH,CAAC;QAClS;QACA,MAAMF,yBAAyBiI,OAAOmB,KAAKgB;IAC7C;IACA,MAAMQ,kBAAkBJ,gBAAiB,MAAM1J,mBAAmBmH,OAAOmB,KAAKgB;IAE9E,2FAA2F;IAC3F,kEAAkE;IAClE,MAAMS,QAAQhB,UAAUQ,KAAK;IAC7B,MAAMS,YAAYD,QAAQ,CAAC,6CAA6C,CAAC,GAAG;IAC5E,MAAME,YAAYF,QAAQ,CAAC,KAAK,EAAEA,MAAM,CAAC,CAAC,GAAG;IAC7C,yFAAyF;IACzF,2GAA2G;IAC3G,IAAIV,aAAa,MAAMxJ,iBAAiBsH,OAAO,iBAAiBiC;IAChE,MAAMc,YAAYb,cAAc,CAAC,sDAAsD,CAAC,GAAG;IAE3F,MAAM7D,aAAa,IAAIlD;IACvB,IAAI6H,YAA0B,EAAE;IAChC,MAAMC,iBAAiB;WAAI/I,MAAMgJ,QAAQ,CAAC;KAAc,CAAC7D,IAAI,CAAC,CAAC5D,QAAU,CAAC,iBAAiB0H,IAAI,CAAC1H,KAAK,CAAC,EAAE;IACxG,MAAM2H,oBAAoB,QAAQD,IAAI,CAACjJ,MAAMZ,OAAO,CAAC,YAAY;IACjE,6FAA6F;IAC7F,8FAA8F;IAC9F,IAAIuI,QAAQwB,KAAK,KAAKtH,aAAa7B,MAAML,IAAI,OAAO,MAAO,CAAA,iBAAiBsJ,IAAI,CAACjJ,UAAUkJ,iBAAgB,KAAM,CAACH,gBAAgB;QAChI,MAAMK,cAAclL,oBAAoB8B;QACxC,IAAIoJ,aAAa,MAAMA;QACvB,IAAI;YACFN,YAAY,MAAMhK,gBAAgBgH,OAAO3B,YAAYnE,OAAO2I,WAAWC,WAAWC,WAAWT,OAAOT,QAAQwB,KAAK;QACnH,EAAE,OAAOhB,KAAK;YACZ,MAAMhK,YAAYgK,KAAcnI,OAAO0I;QACzC;IACF;IACA,uFAAuF;IACvF,qEAAqE;IACrE,MAAMvB,eAAe,IAAIhG,IAAI2H,UAAUxJ,GAAG,CAAC,CAACwI,IAAMA,EAAE/B,IAAI;IAExD,IAAI4B,QAAQ0B,KAAK,KAAKxH,WAAW,MAAMjD,gBAAgBkH,OAAO3B,YAAY2E,WAAWlB,UAAUK,cAAcG,OAAOT,QAAQ0B,KAAK;IAEjI,IAAIC,aAAa,IAAIrI;IACrB,IAAIsI,kBAAkB,IAAItI;IAC1B,IAAIwH,iBAAiB;QAClB,CAAA,EAAEa,UAAU,EAAEC,eAAe,EAAE,GAAG,MAAM1K,kBAAkBiH,OAAOmB,KAAK9C,YAAYnE,OAAOoI,OAAOH,cAAcN,QAAQW,OAAO,CAAU;IAC1I;IAEA,MAAMxC,MAAM0D,IAAI,CAAC;IACjB,8FAA8F;IAC9F,kEAAkE;IAClE,MAAM1D,MAAM0D,IAAI,CAAC;IACjB,IAAIrF,WAAWS,IAAI,GAAG,GAAG;QACvB,MAAMkB,MAAM2D,QAAQ,CAClB,sFACA;eAAItF;SAAW,CAAC7E,GAAG,CAAC,CAAC,CAACyG,MAAMb,EAAE;gBAA4BoE,iBAA8BC;mBAArD;gBAACxD;gBAAMb,EAAEwE,KAAK;gBAAExE,EAAEyE,GAAG;iBAAEL,kBAAAA,WAAW1H,GAAG,CAACmE,mBAAfuD,6BAAAA,kBAAwB;iBAAMC,uBAAAA,gBAAgB3H,GAAG,CAACmE,mBAApBwD,kCAAAA,uBAA6B;aAAK;;IAE9H;IAEA,2FAA2F;IAC3F,qDAAqD;IACrD,MAAMrB,QAAQQ,QAAQ,CAAC,OAAO,EAAEA,MAAM,CAAC,CAAC,GAAG;IAC3C,+EAA+E;IAC/E,8DAA8D;IAC9D,MAAMkB,gBAAgBnB,kBAAkB,yBAAyB;IACjE,yFAAyF;IACzF,sEAAsE;IACtE,MAAMoB,aAAa,MAAM/D,MAAMG,OAAO,CACpC,CAAC,uIAAuI,EAAE2D,cAAc;;OAErJ,EAAE1B,MAAM,oCAAoC,CAAC;IAElD,MAAMhB,OAAQ,MAAM2C,WAAWhC,GAAG,CAACpB;IACnC,KAAK,MAAMP,OAAOgB,KAAMhB,IAAIV,QAAQ,GAAG,EAAE;IAEzC,MAAMwB,kBAAkBlB,OAAOmB,KAAKC,MAAMC,cAAcnH,OAAOsG;IAE/D,OAAOY;AACT"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/search.ts"],"sourcesContent":["import { channel } from 'node:diagnostics_channel';\nimport { stemmer } from 'stemmer';\nimport type { EffectiveSearch, ResolvedConfig } from '../config/index.ts';\nimport { embedConfig, featureEnabled, resolveSearch } from '../config/index.ts';\nimport type { PreparedSemanticQuery } from '../embed/query.ts';\nimport { ensureDocumentEmbeddings, prepareSemanticQuery } 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 { bareTermSyntaxError, searchError } from '../output/search-error.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\nimport { foldForSearch, hasUnspacedRun, searchTokens, unspacedRuns } from '../text/segment.ts';\nimport { materializeScope, narrowByWhere, rawScope, scopeHasEmbeddings } from './scope.ts';\nimport type { Candidates, SearchResultVia, SearchSignalContribution } from './signals.ts';\nimport { linksCandidates, vectorsCandidates, wordsCandidates } from './signals.ts';\nimport { assertQuerySnapshot } from './snapshot.ts';\n\nexport type { SearchResultVia, SearchSignalContribution } from './signals.ts';\n\n// Default budget behind --snippet-char-limit/--snippet-count-limit: the limit is whatever the\n// caller passes, these only say what happens when they pass nothing (DESIGN.md \"Search snippets\").\nexport const SNIPPET_CHAR_LIMIT_DEFAULT = 80;\nexport const SNIPPET_COUNT_LIMIT_DEFAULT = 1;\n\nconst searchCandidatesRead = channel('sensemaking.search.candidates-read');\n\n// Bare terms from an FTS5 query string: strips operators/quoting, not real terms, so the\n// snippet scan matches what the query matched -- FTS5 operators are uppercase-only, so this is too.\nfunction extractBareTerms(query: string): string[] {\n const cleaned = query\n .replace(/\"/g, ' ')\n .replace(/[()*]/g, ' ')\n .replace(/\\b(AND|OR|NOT|NEAR)\\b(\\/\\d+)?/g, ' ');\n return cleaned\n .split(/\\s+/)\n .map((tok) => tok.replace(/^[A-Za-z_]\\w*:/, '')) // column filter, e.g. title:term\n .flatMap((tok) => searchTokens(tok).map(({ text }) => text))\n .map((tok) => tok.toLowerCase().trim())\n .filter((tok) => tok.length > 0);\n}\n\n// House convention (src/chunk/group.ts): Intl.Segmenter word boundaries, not a bare \\b, which\n// is ASCII-only. Built on first use: construction is a few ms, paid only when a term matches.\nlet wordSegmenter: Intl.Segmenter | undefined;\n\nfunction findOccurrences(text: string, terms: string[]): Array<{ start: number; end: number; term: string }> {\n const occ: Array<{ start: number; end: number; term: string }> = [];\n const textLower = text.toLowerCase();\n\n // Unspaced scripts (UNSPACED_SCRIPTS) mark no word boundaries: the sense-setup skill promises\n // substring semantics there, so a matching term is found wherever it occurs in the run.\n const unspacedTerms = terms.filter(hasUnspacedRun);\n if (unspacedTerms.length > 0) {\n for (const run of unspacedRuns(text)) {\n const runLower = textLower.slice(run.start, run.end);\n for (const term of unspacedTerms) {\n let idx = 0;\n for (;;) {\n const found = runLower.indexOf(term, idx);\n if (found === -1) break;\n occ.push({ start: run.start + found, end: run.start + found + term.length, term });\n idx = found + term.length;\n }\n }\n }\n }\n\n // Spaced scripts: a word is marked when its porter stem equals a query term's stem, the same\n // algorithm both indexes tokenize with (sqlite's TOKENIZE = 'porter unicode61'; duckdb's fts default).\n const spacedTerms = terms.filter((t) => !hasUnspacedRun(t));\n if (spacedTerms.length > 0) {\n const termByStem = new Map(spacedTerms.map((t) => [stemmer(foldForSearch(t)), t]));\n const stemInitials = new Set(Array.from(termByStem.keys(), (term) => term[0]));\n for (const match of searchTokens(text)) {\n const segment = match.text;\n if (match.unspaced) continue;\n const folded = foldForSearch(segment);\n // Porter stemming changes suffixes only, so a different initial cannot match a query stem.\n if (!stemInitials.has(folded[0])) continue;\n const term = termByStem.get(stemmer(folded));\n if (term !== undefined) occ.push({ start: match.start, end: match.end, term });\n }\n }\n\n return occ.sort((a, b) => a.start - b.start || b.end - a.end);\n}\n\n// Segment boundaries of the whole text, word granularity: the only cut points a snippet edge\n// may land on. One rule for every script (DESIGN.md \"Cutting\") -- a regex \\b is ASCII-only and\n// would mis-cut Japanese, Chinese and Thai, which Intl.Segmenter divides by dictionary instead.\nfunction wordBoundaries(text: string): number[] {\n wordSegmenter ??= new Intl.Segmenter('en', { granularity: 'word' });\n const bounds = [0];\n for (const { index, segment } of wordSegmenter.segment(text)) bounds.push(index + segment.length);\n return bounds;\n}\n\n// bounds is sorted: snaps are binary searches, since each runs once per candidate window.\nfunction snapForward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] < pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\nfunction snapBackward(bounds: number[], pos: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n let result = bounds[0];\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= pos) {\n lo = mid;\n result = bounds[mid];\n } else hi = mid - 1;\n }\n return result;\n}\n\nfunction firstBoundAfter(bounds: number[], pos: number): number | undefined {\n let lo = 0;\n let hi = bounds.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (bounds[mid] <= pos) lo = mid + 1;\n else hi = mid;\n }\n return bounds[lo];\n}\n\n// A raw, unmarked upper bound: [start, end) inside [anchor, anchor+charLimit), edges on\n// boundaries. Markup (guillemets, ellipses) added on top of this can push the rendered result\n// past charLimit, which is what fitEnd() below corrects for; this is only the starting guess.\n// A single segment spanning the whole target window (a word longer than the char limit) is the\n// one case where holding the boundary rule means the budget is not a hard ceiling.\nfunction cutWindow(text: string, bounds: number[], anchor: number, charLimit: number): { start: number; end: number } {\n const rawEnd = Math.min(text.length, anchor + charLimit);\n const start = snapForward(bounds, anchor);\n let end = snapBackward(bounds, rawEnd);\n if (end <= start) end = firstBoundAfter(bounds, start) ?? text.length;\n return { start, end: Math.min(end, text.length) };\n}\n\n// The largest boundary <= upperBound, > floor, whose rendered [*, end) fits charLimit. Markup\n// only ever adds characters as end grows, so rendered length is non-decreasing in end and\n// shrinking from upperBound is guaranteed to find the true fit rather than guess at it.\n// `floor` is the one end this never shrinks past (the anchor's own marked word, or the first\n// word in the unmarked fallback): DESIGN.md's budget is a ceiling except here, where returning\n// nothing is worse than a single word that overruns it.\nfunction fitEnd(render: (end: number) => string, bounds: number[], floor: number, upperBound: number, charLimit: number): number {\n let lo = 0;\n let hi = bounds.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >> 1;\n if (bounds[mid] <= upperBound) lo = mid;\n else hi = mid - 1;\n }\n for (; lo >= 0 && bounds[lo] > floor; lo--) {\n if (render(bounds[lo]).length <= charLimit) return bounds[lo];\n }\n return floor;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\ninterface SnippetWindow extends Span {\n distinct: number;\n count: number;\n}\n\n// occ is start-sorted: a window's first candidate occurrence is a binary search, not a walk.\nfunction firstOccAtOrAfter(occ: Array<{ start: number; end: number; term: string }>, pos: number): number {\n let lo = 0;\n let hi = occ.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (occ[mid].start < pos) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n}\n\nfunction renderWindow(text: string, occ: Array<{ start: number; end: number; term: string }>, win: Span): string {\n let out = '';\n let cursor = win.start;\n for (let i = firstOccAtOrAfter(occ, win.start); i < occ.length && occ[i].start < win.end; i++) {\n const o = occ[i];\n if (o.end > win.end) continue;\n // Occurrences of duplicate or substring-overlapping terms (\"test tests\") can overlap;\n // emitting each would duplicate document text. Keep the first, absorb the rest.\n if (o.start < cursor) continue;\n out += `${text.slice(cursor, o.start)}«${text.slice(o.start, o.end)}»`;\n cursor = o.end;\n }\n out += text.slice(cursor, win.end);\n const prefix = win.start > 0 ? '…' : '';\n const suffix = win.end < text.length ? '…' : '';\n return `${prefix}${out.replace(/\\s+/g, ' ')}${suffix}`;\n}\n\n// Scores every occurrence-anchored candidate window: most distinct terms wins, ties broken by\n// most total hits -- coverage now that occurrences are real words, not the false-fragment counts\n// a substring scan produced. Selection itself is unevaluated (DESIGN.md \"Selection, unsettled\").\n// The window's `end` is fitted to the *rendered* text, markup included, not the raw span: a\n// window dense with marks (every guillemet pair adds two characters) shrinks to fit the same\n// budget a sparse one fills almost exactly.\nfunction scoreWindows(occ: Array<{ start: number; end: number; term: string }>, bounds: number[], text: string, charLimit: number): SnippetWindow[] {\n const candidates: SnippetWindow[] = [];\n for (let i = 0; i < occ.length; i++) {\n const winEnd = occ[i].start + charLimit;\n const seen = new Set<string>();\n let count = 0;\n for (let j = i; j < occ.length && occ[j].start < winEnd; j++) {\n seen.add(occ[j].term);\n count++;\n }\n const start = snapForward(bounds, occ[i].start);\n const floor = Math.max(start, occ[i].end);\n const upperBound = Math.max(floor, cutWindow(text, bounds, occ[i].start, charLimit).end);\n const end = fitEnd((e) => renderWindow(text, occ, { start, end: e }), bounds, floor, upperBound, charLimit);\n candidates.push({ start, end, distinct: seen.size, count });\n }\n return candidates;\n}\n\nfunction overlaps(a: Span, b: Span): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\n// Same scoring function as a single snippet, kept rather than discarded (DESIGN.md \"Several\n// snippets per note\"): rank by score, take the top non-overlapping windows, then reorder to the\n// note's own order, since several passages from one note read as an abridged note.\nfunction selectWindows(candidates: SnippetWindow[], countLimit: number): { chosen: SnippetWindow[]; best: SnippetWindow } {\n const ranked = candidates.slice().sort((a, b) => b.distinct - a.distinct || b.count - a.count);\n const chosen: SnippetWindow[] = [];\n for (const c of ranked) {\n if (chosen.length >= countLimit) break;\n if (chosen.some((s) => overlaps(s, c))) continue;\n chosen.push(c);\n }\n chosen.sort((a, b) => a.start - b.start);\n return { chosen, best: ranked[0] };\n}\n\n// Builds bounded marked passages and returns the best window's offset for section lookup.\nexport function computeSnippets(text: string, terms: string[], charLimit: number, countLimit: number): { snippets: string[]; offset: number } {\n const occ = findOccurrences(text, terms);\n const bounds = wordBoundaries(text);\n if (occ.length === 0) {\n // A term matched only via title/summary, not this body text, finds no occurrence here;\n // fall back to the doc's start, unmarked.\n const render = (end: number): string => `${text.slice(0, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`;\n const floor = bounds.find((b) => b > 0) ?? text.length;\n const upperBound = Math.max(floor, cutWindow(text, bounds, 0, charLimit).end);\n const end = fitEnd(render, bounds, floor, upperBound, charLimit);\n return { snippets: [render(end)], offset: 0 };\n }\n const candidates = scoreWindows(occ, bounds, text, charLimit);\n const { chosen, best } = selectWindows(candidates, countLimit);\n return { snippets: chosen.map((win) => renderWindow(text, occ, win)), offset: best.start };\n}\n\n// Converts an offset in the original text to its one-based line number.\nexport function lineNumberAt(text: string, offset: number): number {\n let line = 1;\n for (let i = 0; i < offset; i++) if (text.charCodeAt(i) === 10) line++;\n return line;\n}\n\nasync function lineRangeFor(store: Store, path: string, line: number): Promise<string | null> {\n const stmt = await store.prepare('SELECT start_line, end_line FROM sections WHERE \"path\" = ? AND start_line <= ? AND end_line >= ? ORDER BY start_line DESC LIMIT 1');\n const row = (await stmt.get(path, line, line)) as { start_line: number; end_line: number } | undefined;\n return row ? `L${row.start_line}-${row.end_line}` : null;\n}\n\n/** Options for the root {@link search} API. */\nexport interface SearchOptions {\n k?: number;\n /** Collects ranking contributions for returned candidates; omitted or false preserves ordinary output. */\n explain?: boolean;\n where?: string; // SQL fragment against frontmatter alias `f`, e.g. \"f.status = 'active'\"\n preset?: string; // named preset; unknown name throws listing declared presets, undefined -> \"default\"\n include?: string[]; // ad hoc scope override (repeatable --include); independent of exclude\n exclude?: string[]; // ad hoc scope override (repeatable --exclude); independent of include\n noExclude?: boolean; // --no-exclude: drop the preset's exclude for this command\n snippetCharLimit?: number; // --snippet-char-limit; defaults to SNIPPET_CHAR_LIMIT_DEFAULT\n snippetCountLimit?: number; // --snippet-count-limit; defaults to SNIPPET_COUNT_LIMIT_DEFAULT\n /** Cancels supported provider I/O and stops between phases after active native work finishes. */\n signal?: AbortSignal;\n}\n\nexport type SearchResult = {\n path: string;\n title: string;\n summary: string;\n snippets: string[];\n via: SearchResultVia;\n score: number;\n lines: string | null;\n similarity?: number | null;\n /** Present only with explain: true, in words/links/vectors order; contributions can differ from via labels. */\n explanation?: SearchSignalContribution[];\n};\n\nexport interface SearchHydrationRow {\n path: string;\n snippets?: string[];\n lines?: string | null;\n}\n\nfunction validateSearchOptions(opts: SearchOptions): void {\n if (opts.explain !== undefined && typeof opts.explain !== 'boolean') {\n throw new SenseError('SEARCH_OPTION_INVALID', `search option \"explain\" must be a boolean, got ${String(opts.explain)}`);\n }\n for (const [name, value] of [\n ['k', opts.k],\n ['snippetCharLimit', opts.snippetCharLimit],\n ['snippetCountLimit', opts.snippetCountLimit],\n ] as const) {\n if (value !== undefined && (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)) {\n throw new SenseError('SEARCH_OPTION_INVALID', `search option \"${name}\" must be a positive finite integer, got ${String(value)}`);\n }\n }\n}\n\n// Hydrates already-ranked rows with snippets and containing-section ranges without reranking.\nexport async function hydrateSearchRows(store: Store, cfg: ResolvedConfig, rows: SearchHydrationRow[], matchedPaths: Set<string>, terms: string, opts: Pick<SearchOptions, 'snippetCharLimit' | 'snippetCountLimit'> = {}): Promise<void> {\n if (matchedPaths.size === 0) return;\n const bareTerms = extractBareTerms(terms);\n const charLimit = opts.snippetCharLimit ?? SNIPPET_CHAR_LIMIT_DEFAULT;\n const countLimit = opts.snippetCountLimit ?? SNIPPET_COUNT_LIMIT_DEFAULT;\n const sourceStmt = await store.prepare('SELECT text FROM indexed_sources WHERE \"path\" = ?');\n for (const row of rows) {\n if (!matchedPaths.has(row.path)) continue;\n const source = (await sourceStmt.get(row.path)) as { text: string } | undefined;\n if (!source) throw new SenseError('INDEX_NOT_READY', `indexed source text is missing for ${String(row.path)}; run \\`sense build --force\\` (or library build(config, { force: true })) before querying`);\n const text = source.text;\n const { snippets, offset } = computeSnippets(text, bareTerms, charLimit, countLimit);\n row.snippets = snippets;\n if (row.lines == null) row.lines = featureEnabled(cfg, 'sections') ? await lineRangeFor(store, row.path, lineNumberAt(text, offset)) : null;\n }\n}\n\n/**\n * Searches one committed published generation. Provider work precedes the final read transaction;\n * cancellation waits for active native work and transaction cleanup before rejection.\n */\nexport function search(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions = {}): Promise<SearchResult[]> {\n if (opts.signal?.aborted) return Promise.reject(opts.signal.reason);\n return serialQuery(store, () => searchIndexed(store, cfg, terms, opts));\n}\n\ninterface SearchScope {\n signature: string;\n effective: EffectiveSearch;\n allPaths: string[];\n scopePaths: Set<string>;\n allowedPaths: Set<string>;\n semanticEligible: boolean;\n}\n\nasync function readSearchScope(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions): Promise<SearchScope> {\n const signature = await assertQuerySnapshot(store, cfg);\n const effective = resolveSearch(cfg, opts);\n const allPaths = ((await (await store.prepare('SELECT \"path\" FROM frontmatter')).all()) as Array<{ path: string }>).map((r) => r.path);\n const scopePaths = await rawScope(store, cfg, opts, allPaths);\n let allowedPaths: Set<string>;\n try {\n allowedPaths = await narrowByWhere(store, scopePaths, effective.where);\n } catch (err) {\n throw searchError(err as Error, terms, effective.where);\n }\n const wantsVectors = effective.signals.vectors !== undefined;\n if (wantsVectors) await ensureDocumentEmbeddings(store, cfg, allowedPaths);\n const semanticEligible = wantsVectors && (await scopeHasEmbeddings(store, cfg, allowedPaths));\n return { signature, effective, allPaths, scopePaths, allowedPaths, semanticEligible };\n}\n\ninterface SemanticPreparationNeeded {\n signature: string;\n}\n\nfunction needsSemanticPreparation(result: SearchResult[] | SemanticPreparationNeeded): result is SemanticPreparationNeeded {\n return !Array.isArray(result);\n}\n\nasync function searchIndexed(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions): Promise<SearchResult[]> {\n validateSearchOptions(opts);\n const initial = resolveSearch(cfg, opts);\n opts.signal?.throwIfAborted();\n // A downloadable HF id proceeds -- getProvider fetches it lazily on consent. Only a\n // local path with missing files errors here, since nothing will ever fetch it for itself.\n const wantsVectors = initial.signals.vectors !== undefined;\n if (wantsVectors) {\n const e = embedConfig(cfg); // validate.ts guarantees this is set whenever \"vectors\" is declared\n if (localModelMissing(e)) {\n 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`);\n }\n }\n\n let prepared: PreparedSemanticQuery | undefined;\n let preparedSignature: string | undefined;\n if (wantsVectors) {\n // This first snapshot only decides whether a semantic query can contribute. It closes before\n // provider/model work, and the complete scope/readiness decision is repeated below.\n const preflight = await store.transaction(() => readSearchScope(store, cfg, terms, opts));\n opts.signal?.throwIfAborted();\n if (preflight.semanticEligible) {\n preparedSignature = preflight.signature;\n prepared = await prepareSemanticQuery(cfg, terms, { signal: opts.signal });\n opts.signal?.throwIfAborted();\n }\n }\n\n opts.signal?.throwIfAborted();\n let result = await store.transaction(() => searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));\n opts.signal?.throwIfAborted();\n if (!needsSemanticPreparation(result)) return result;\n\n // The scope became vector-eligible after an empty/ineligible preflight. Leave that snapshot,\n // prepare once, then take a fresh final snapshot rather than waiting on the provider inside it.\n preparedSignature = result.signature;\n prepared = await prepareSemanticQuery(cfg, terms, { signal: opts.signal });\n opts.signal?.throwIfAborted();\n result = await store.transaction(() => searchSnapshot(store, cfg, terms, opts, prepared, preparedSignature));\n opts.signal?.throwIfAborted();\n 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');\n return result;\n}\n\nasync function searchSnapshot(store: Store, cfg: ResolvedConfig, terms: string, opts: SearchOptions, prepared: PreparedSemanticQuery | undefined, preparedSignature: string | undefined): Promise<SearchResult[] | SemanticPreparationNeeded> {\n const { signature, effective, allPaths, scopePaths, allowedPaths, semanticEligible } = await readSearchScope(store, cfg, terms, opts);\n if (prepared !== undefined && signature !== preparedSignature) {\n 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');\n }\n if (semanticEligible && prepared === undefined) return { signature };\n\n const { k, signals } = effective;\n const fetch = Math.max(k * 3, 30);\n const scopeActive = scopePaths.size < allPaths.length;\n const semanticEnabled = semanticEligible && prepared !== undefined;\n\n // --where applies inside the candidate query (a post-filter would drop matches ranked past\n // the pool) and again on the final select, for link-derived rows.\n const scope = effective.where;\n const whereJoin = scope ? `JOIN frontmatter f ON f.\"path\" = content.path` : '';\n const whereCond = scope ? `AND (${scope})` : '';\n // Filtered before LIMIT, not after, or scoped notes ranking below the global top-`fetch`\n // never reach the filter; joined against a temp table since real scopes exceed SQLITE_MAX_VARIABLE_NUMBER.\n if (scopeActive) await materializeScope(store, '_search_scope', scopePaths);\n const scopeCond = scopeActive ? `AND content.path IN (SELECT \"path\" FROM _search_scope)` : '';\n\n const candidates: Candidates = new Map();\n let matchRows: LexicalHit[] = [];\n const hasEmptyPhrase = [...terms.matchAll(/\"([^\"]*)\"/g)].some((match) => !/[\\p{L}\\p{N}_]/u.test(match[1]));\n const hasBareSyntaxOnly = /[()]/u.test(terms.replace(/\"[^\"]*\"/g, ' '));\n // A query that is only whitespace or punctuation has no lexical rows; other declared signals\n // compose normally. In particular, unspaced-script punctuation can rewrite to an empty MATCH.\n if (signals.words !== undefined && terms.trim() !== '' && (/[\\p{L}\\p{N}_]/u.test(terms) || hasBareSyntaxOnly) && !hasEmptyPhrase) {\n const syntaxError = bareTermSyntaxError(terms);\n if (syntaxError) throw syntaxError;\n try {\n matchRows = await wordsCandidates(store, candidates, terms, whereJoin, whereCond, scopeCond, fetch, signals.words, opts.explain);\n } catch (err) {\n throw searchError(err as Error, terms, scope);\n }\n }\n // Every store's lexical `snippets` is NULL; the snippet text is always computed below.\n // Distinguish from via='link' rows, which never appear in matchRows.\n const matchedPaths = new Set(matchRows.map((r) => r.path));\n\n if (signals.links !== undefined) await linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, signals.links, opts.explain);\n\n let chunkLines = new Map<string, string>();\n let chunkSimilarity = new Map<string, number>();\n if (semanticEnabled && signals.vectors !== undefined && prepared !== undefined) {\n ({ chunkLines, chunkSimilarity } = await vectorsCandidates(store, prepared, candidates, fetch, allowedPaths, allPaths.length, signals.vectors, opts.explain));\n }\n\n await store.exec('DROP TABLE IF EXISTS _search');\n // DOUBLE, not REAL: sqlite's REAL is an 8-byte double but duckdb's is a 4-byte float, and the\n // rounded score/similarity are printed at full precision in rows.\n await store.exec('CREATE TEMP TABLE _search (\"path\" TEXT PRIMARY KEY, score DOUBLE, via TEXT, lines TEXT, similarity DOUBLE)');\n if (candidates.size > 0) {\n await store.runBatch(\n 'INSERT INTO _search (\"path\", score, via, lines, similarity) VALUES (?, ?, ?, ?, ?)',\n [...candidates].map(([path, c]) => [path, c.score, c.via, chunkLines.get(path) ?? null, chunkSimilarity.get(path) ?? null])\n );\n }\n\n // Reapplies --where even though _search is already scope+where filtered, since the join to\n // frontmatter is already needed for the path column.\n const where = scope ? `WHERE (${scope})` : '';\n // lines: semantic rows carry their chunk's range, lexical rows gain one below,\n // everything else stays null; similarity stays semantic-only.\n const similarityCol = semanticEnabled ? ', _search.similarity' : '';\n // NULL AS snippets holds this column's position (between summary and via) for every row;\n // overwritten below, always to an array, never left null (DESIGN.md).\n const selectStmt = await store.prepare(\n `SELECT f.\"path\" AS path, content.title, content.summary, NULL AS snippets, _search.via, round(_search.score, 4) AS score, _search.lines${similarityCol}\n FROM _search JOIN frontmatter f ON f.\"path\" = _search.\"path\" JOIN content ON content.path = _search.\"path\"\n ${where} ORDER BY _search.score DESC, _search.\"path\" ASC LIMIT ?`\n );\n const rows = (await selectStmt.all(k)) as SearchResult[];\n if (searchCandidatesRead.hasSubscribers) searchCandidatesRead.publish({ paths: rows.map((row) => row.path) });\n for (const row of rows) row.snippets = [];\n\n await hydrateSearchRows(store, cfg, rows, matchedPaths, terms, opts);\n if (opts.explain) {\n for (const row of rows) row.explanation = candidates.get(row.path)?.explanation ?? [];\n }\n\n return rows;\n}\n"],"names":["channel","stemmer","embedConfig","featureEnabled","resolveSearch","ensureDocumentEmbeddings","prepareSemanticQuery","localModelMissing","MODEL_FILENAMES","SenseError","serialQuery","bareTermSyntaxError","searchError","foldForSearch","hasUnspacedRun","searchTokens","unspacedRuns","materializeScope","narrowByWhere","rawScope","scopeHasEmbeddings","linksCandidates","vectorsCandidates","wordsCandidates","assertQuerySnapshot","SNIPPET_CHAR_LIMIT_DEFAULT","SNIPPET_COUNT_LIMIT_DEFAULT","searchCandidatesRead","extractBareTerms","query","cleaned","replace","split","map","tok","flatMap","text","toLowerCase","trim","filter","length","wordSegmenter","findOccurrences","terms","occ","textLower","unspacedTerms","run","runLower","slice","start","end","term","idx","found","indexOf","push","spacedTerms","t","termByStem","Map","stemInitials","Set","Array","from","keys","match","segment","unspaced","folded","has","get","undefined","sort","a","b","wordBoundaries","Intl","Segmenter","granularity","bounds","index","snapForward","pos","lo","hi","mid","snapBackward","result","firstBoundAfter","cutWindow","anchor","charLimit","rawEnd","Math","min","fitEnd","render","floor","upperBound","firstOccAtOrAfter","renderWindow","win","out","cursor","i","o","prefix","suffix","scoreWindows","candidates","winEnd","seen","count","j","add","max","e","distinct","size","overlaps","selectWindows","countLimit","ranked","chosen","c","some","s","best","computeSnippets","find","snippets","offset","lineNumberAt","line","charCodeAt","lineRangeFor","store","path","stmt","prepare","row","start_line","end_line","validateSearchOptions","opts","explain","String","name","value","k","snippetCharLimit","snippetCountLimit","Number","isFinite","isInteger","hydrateSearchRows","cfg","rows","matchedPaths","bareTerms","sourceStmt","source","lines","search","signal","aborted","Promise","reject","reason","searchIndexed","readSearchScope","signature","effective","allPaths","all","r","scopePaths","allowedPaths","where","err","wantsVectors","signals","vectors","semanticEligible","needsSemanticPreparation","isArray","initial","throwIfAborted","presetName","model","prepared","preparedSignature","preflight","transaction","searchSnapshot","fetch","scopeActive","semanticEnabled","scope","whereJoin","whereCond","scopeCond","matchRows","hasEmptyPhrase","matchAll","test","hasBareSyntaxOnly","words","syntaxError","links","chunkLines","chunkSimilarity","exec","runBatch","score","via","similarityCol","selectStmt","hasSubscribers","publish","paths","explanation"],"mappings":"AAAA,SAASA,OAAO,QAAQ,2BAA2B;AACnD,SAASC,OAAO,QAAQ,UAAU;AAElC,SAASC,WAAW,EAAEC,cAAc,EAAEC,aAAa,QAAQ,qBAAqB;AAEhF,SAASC,wBAAwB,EAAEC,oBAAoB,QAAQ,oBAAoB;AACnF,SAASC,iBAAiB,EAAEC,eAAe,QAAQ,oBAAoB;AACvE,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,WAAW,QAAQ,yBAAyB;AACrD,SAASC,mBAAmB,EAAEC,WAAW,QAAQ,4BAA4B;AAE7E,SAASC,aAAa,EAAEC,cAAc,EAAEC,YAAY,EAAEC,YAAY,QAAQ,qBAAqB;AAC/F,SAASC,gBAAgB,EAAEC,aAAa,EAAEC,QAAQ,EAAEC,kBAAkB,QAAQ,aAAa;AAE3F,SAASC,eAAe,EAAEC,iBAAiB,EAAEC,eAAe,QAAQ,eAAe;AACnF,SAASC,mBAAmB,QAAQ,gBAAgB;AAIpD,8FAA8F;AAC9F,mGAAmG;AACnG,OAAO,MAAMC,6BAA6B,GAAG;AAC7C,OAAO,MAAMC,8BAA8B,EAAE;AAE7C,MAAMC,uBAAuB3B,QAAQ;AAErC,yFAAyF;AACzF,oGAAoG;AACpG,SAAS4B,iBAAiBC,KAAa;IACrC,MAAMC,UAAUD,MACbE,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,kCAAkC;IAC7C,OAAOD,QACJE,KAAK,CAAC,OACNC,GAAG,CAAC,CAACC,MAAQA,IAAIH,OAAO,CAAC,kBAAkB,KAAK,iCAAiC;KACjFI,OAAO,CAAC,CAACD,MAAQnB,aAAamB,KAAKD,GAAG,CAAC,CAAC,EAAEG,IAAI,EAAE,GAAKA,OACrDH,GAAG,CAAC,CAACC,MAAQA,IAAIG,WAAW,GAAGC,IAAI,IACnCC,MAAM,CAAC,CAACL,MAAQA,IAAIM,MAAM,GAAG;AAClC;AAEA,8FAA8F;AAC9F,8FAA8F;AAC9F,IAAIC;AAEJ,SAASC,gBAAgBN,IAAY,EAAEO,KAAe;IACpD,MAAMC,MAA2D,EAAE;IACnE,MAAMC,YAAYT,KAAKC,WAAW;IAElC,8FAA8F;IAC9F,wFAAwF;IACxF,MAAMS,gBAAgBH,MAAMJ,MAAM,CAACzB;IACnC,IAAIgC,cAAcN,MAAM,GAAG,GAAG;QAC5B,KAAK,MAAMO,OAAO/B,aAAaoB,MAAO;YACpC,MAAMY,WAAWH,UAAUI,KAAK,CAACF,IAAIG,KAAK,EAAEH,IAAII,GAAG;YACnD,KAAK,MAAMC,QAAQN,cAAe;gBAChC,IAAIO,MAAM;gBACV,OAAS;oBACP,MAAMC,QAAQN,SAASO,OAAO,CAACH,MAAMC;oBACrC,IAAIC,UAAU,CAAC,GAAG;oBAClBV,IAAIY,IAAI,CAAC;wBAAEN,OAAOH,IAAIG,KAAK,GAAGI;wBAAOH,KAAKJ,IAAIG,KAAK,GAAGI,QAAQF,KAAKZ,MAAM;wBAAEY;oBAAK;oBAChFC,MAAMC,QAAQF,KAAKZ,MAAM;gBAC3B;YACF;QACF;IACF;IAEA,6FAA6F;IAC7F,uGAAuG;IACvG,MAAMiB,cAAcd,MAAMJ,MAAM,CAAC,CAACmB,IAAM,CAAC5C,eAAe4C;IACxD,IAAID,YAAYjB,MAAM,GAAG,GAAG;QAC1B,MAAMmB,aAAa,IAAIC,IAAIH,YAAYxB,GAAG,CAAC,CAACyB,IAAM;gBAACzD,QAAQY,cAAc6C;gBAAKA;aAAE;QAChF,MAAMG,eAAe,IAAIC,IAAIC,MAAMC,IAAI,CAACL,WAAWM,IAAI,IAAI,CAACb,OAASA,IAAI,CAAC,EAAE;QAC5E,KAAK,MAAMc,SAASnD,aAAaqB,MAAO;YACtC,MAAM+B,UAAUD,MAAM9B,IAAI;YAC1B,IAAI8B,MAAME,QAAQ,EAAE;YACpB,MAAMC,SAASxD,cAAcsD;YAC7B,2FAA2F;YAC3F,IAAI,CAACN,aAAaS,GAAG,CAACD,MAAM,CAAC,EAAE,GAAG;YAClC,MAAMjB,OAAOO,WAAWY,GAAG,CAACtE,QAAQoE;YACpC,IAAIjB,SAASoB,WAAW5B,IAAIY,IAAI,CAAC;gBAAEN,OAAOgB,MAAMhB,KAAK;gBAAEC,KAAKe,MAAMf,GAAG;gBAAEC;YAAK;QAC9E;IACF;IAEA,OAAOR,IAAI6B,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAExB,KAAK,GAAGyB,EAAEzB,KAAK,IAAIyB,EAAExB,GAAG,GAAGuB,EAAEvB,GAAG;AAC9D;AAEA,6FAA6F;AAC7F,+FAA+F;AAC/F,gGAAgG;AAChG,SAASyB,eAAexC,IAAY;IAClCK,0BAAAA,2BAAAA,gBAAAA,gBAAkB,IAAIoC,KAAKC,SAAS,CAAC,MAAM;QAAEC,aAAa;IAAO;IACjE,MAAMC,SAAS;QAAC;KAAE;IAClB,KAAK,MAAM,EAAEC,KAAK,EAAEd,OAAO,EAAE,IAAI1B,cAAc0B,OAAO,CAAC/B,MAAO4C,OAAOxB,IAAI,CAACyB,QAAQd,QAAQ3B,MAAM;IAChG,OAAOwC;AACT;AAEA,0FAA0F;AAC1F,SAASE,YAAYF,MAAgB,EAAEG,GAAW;IAChD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIL,MAAM,CAACM,IAAI,GAAGH,KAAKC,KAAKE,MAAM;aAC7BD,KAAKC;IACZ;IACA,OAAON,MAAM,CAACI,GAAG;AACnB;AAEA,SAASG,aAAaP,MAAgB,EAAEG,GAAW;IACjD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,IAAIgD,SAASR,MAAM,CAAC,EAAE;IACtB,MAAOI,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,KAAK,KAAM;QAC7B,IAAIL,MAAM,CAACM,IAAI,IAAIH,KAAK;YACtBC,KAAKE;YACLE,SAASR,MAAM,CAACM,IAAI;QACtB,OAAOD,KAAKC,MAAM;IACpB;IACA,OAAOE;AACT;AAEA,SAASC,gBAAgBT,MAAgB,EAAEG,GAAW;IACpD,IAAIC,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM;IACtB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIL,MAAM,CAACM,IAAI,IAAIH,KAAKC,KAAKE,MAAM;aAC9BD,KAAKC;IACZ;IACA,OAAON,MAAM,CAACI,GAAG;AACnB;AAEA,wFAAwF;AACxF,8FAA8F;AAC9F,8FAA8F;AAC9F,+FAA+F;AAC/F,mFAAmF;AACnF,SAASM,UAAUtD,IAAY,EAAE4C,MAAgB,EAAEW,MAAc,EAAEC,SAAiB;QAI1DH;IAHxB,MAAMI,SAASC,KAAKC,GAAG,CAAC3D,KAAKI,MAAM,EAAEmD,SAASC;IAC9C,MAAM1C,QAAQgC,YAAYF,QAAQW;IAClC,IAAIxC,MAAMoC,aAAaP,QAAQa;IAC/B,IAAI1C,OAAOD,OAAOC,OAAMsC,mBAAAA,gBAAgBT,QAAQ9B,oBAAxBuC,8BAAAA,mBAAkCrD,KAAKI,MAAM;IACrE,OAAO;QAAEU;QAAOC,KAAK2C,KAAKC,GAAG,CAAC5C,KAAKf,KAAKI,MAAM;IAAE;AAClD;AAEA,8FAA8F;AAC9F,0FAA0F;AAC1F,wFAAwF;AACxF,6FAA6F;AAC7F,+FAA+F;AAC/F,wDAAwD;AACxD,SAASwD,OAAOC,MAA+B,EAAEjB,MAAgB,EAAEkB,KAAa,EAAEC,UAAkB,EAAEP,SAAiB;IACrH,IAAIR,KAAK;IACT,IAAIC,KAAKL,OAAOxC,MAAM,GAAG;IACzB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,KAAK,KAAM;QAC7B,IAAIL,MAAM,CAACM,IAAI,IAAIa,YAAYf,KAAKE;aAC/BD,KAAKC,MAAM;IAClB;IACA,MAAOF,MAAM,KAAKJ,MAAM,CAACI,GAAG,GAAGc,OAAOd,KAAM;QAC1C,IAAIa,OAAOjB,MAAM,CAACI,GAAG,EAAE5C,MAAM,IAAIoD,WAAW,OAAOZ,MAAM,CAACI,GAAG;IAC/D;IACA,OAAOc;AACT;AAYA,6FAA6F;AAC7F,SAASE,kBAAkBxD,GAAwD,EAAEuC,GAAW;IAC9F,IAAIC,KAAK;IACT,IAAIC,KAAKzC,IAAIJ,MAAM;IACnB,MAAO4C,KAAKC,GAAI;QACd,MAAMC,MAAM,AAACF,KAAKC,MAAO;QACzB,IAAIzC,GAAG,CAAC0C,IAAI,CAACpC,KAAK,GAAGiC,KAAKC,KAAKE,MAAM;aAChCD,KAAKC;IACZ;IACA,OAAOF;AACT;AAEA,SAASiB,aAAajE,IAAY,EAAEQ,GAAwD,EAAE0D,GAAS;IACrG,IAAIC,MAAM;IACV,IAAIC,SAASF,IAAIpD,KAAK;IACtB,IAAK,IAAIuD,IAAIL,kBAAkBxD,KAAK0D,IAAIpD,KAAK,GAAGuD,IAAI7D,IAAIJ,MAAM,IAAII,GAAG,CAAC6D,EAAE,CAACvD,KAAK,GAAGoD,IAAInD,GAAG,EAAEsD,IAAK;QAC7F,MAAMC,IAAI9D,GAAG,CAAC6D,EAAE;QAChB,IAAIC,EAAEvD,GAAG,GAAGmD,IAAInD,GAAG,EAAE;QACrB,sFAAsF;QACtF,gFAAgF;QAChF,IAAIuD,EAAExD,KAAK,GAAGsD,QAAQ;QACtBD,OAAO,GAAGnE,KAAKa,KAAK,CAACuD,QAAQE,EAAExD,KAAK,EAAE,CAAC,EAAEd,KAAKa,KAAK,CAACyD,EAAExD,KAAK,EAAEwD,EAAEvD,GAAG,EAAE,CAAC,CAAC;QACtEqD,SAASE,EAAEvD,GAAG;IAChB;IACAoD,OAAOnE,KAAKa,KAAK,CAACuD,QAAQF,IAAInD,GAAG;IACjC,MAAMwD,SAASL,IAAIpD,KAAK,GAAG,IAAI,MAAM;IACrC,MAAM0D,SAASN,IAAInD,GAAG,GAAGf,KAAKI,MAAM,GAAG,MAAM;IAC7C,OAAO,GAAGmE,SAASJ,IAAIxE,OAAO,CAAC,QAAQ,OAAO6E,QAAQ;AACxD;AAEA,8FAA8F;AAC9F,iGAAiG;AACjG,iGAAiG;AACjG,4FAA4F;AAC5F,6FAA6F;AAC7F,4CAA4C;AAC5C,SAASC,aAAajE,GAAwD,EAAEoC,MAAgB,EAAE5C,IAAY,EAAEwD,SAAiB;IAC/H,MAAMkB,aAA8B,EAAE;IACtC,IAAK,IAAIL,IAAI,GAAGA,IAAI7D,IAAIJ,MAAM,EAAEiE,IAAK;QACnC,MAAMM,SAASnE,GAAG,CAAC6D,EAAE,CAACvD,KAAK,GAAG0C;QAC9B,MAAMoB,OAAO,IAAIlD;QACjB,IAAImD,QAAQ;QACZ,IAAK,IAAIC,IAAIT,GAAGS,IAAItE,IAAIJ,MAAM,IAAII,GAAG,CAACsE,EAAE,CAAChE,KAAK,GAAG6D,QAAQG,IAAK;YAC5DF,KAAKG,GAAG,CAACvE,GAAG,CAACsE,EAAE,CAAC9D,IAAI;YACpB6D;QACF;QACA,MAAM/D,QAAQgC,YAAYF,QAAQpC,GAAG,CAAC6D,EAAE,CAACvD,KAAK;QAC9C,MAAMgD,QAAQJ,KAAKsB,GAAG,CAAClE,OAAON,GAAG,CAAC6D,EAAE,CAACtD,GAAG;QACxC,MAAMgD,aAAaL,KAAKsB,GAAG,CAAClB,OAAOR,UAAUtD,MAAM4C,QAAQpC,GAAG,CAAC6D,EAAE,CAACvD,KAAK,EAAE0C,WAAWzC,GAAG;QACvF,MAAMA,MAAM6C,OAAO,CAACqB,IAAMhB,aAAajE,MAAMQ,KAAK;gBAAEM;gBAAOC,KAAKkE;YAAE,IAAIrC,QAAQkB,OAAOC,YAAYP;QACjGkB,WAAWtD,IAAI,CAAC;YAAEN;YAAOC;YAAKmE,UAAUN,KAAKO,IAAI;YAAEN;QAAM;IAC3D;IACA,OAAOH;AACT;AAEA,SAASU,SAAS9C,CAAO,EAAEC,CAAO;IAChC,OAAOD,EAAExB,KAAK,GAAGyB,EAAExB,GAAG,IAAIwB,EAAEzB,KAAK,GAAGwB,EAAEvB,GAAG;AAC3C;AAEA,4FAA4F;AAC5F,gGAAgG;AAChG,mFAAmF;AACnF,SAASsE,cAAcX,UAA2B,EAAEY,UAAkB;IACpE,MAAMC,SAASb,WAAW7D,KAAK,GAAGwB,IAAI,CAAC,CAACC,GAAGC,IAAMA,EAAE2C,QAAQ,GAAG5C,EAAE4C,QAAQ,IAAI3C,EAAEsC,KAAK,GAAGvC,EAAEuC,KAAK;IAC7F,MAAMW,SAA0B,EAAE;IAClC,KAAK,MAAMC,KAAKF,OAAQ;QACtB,IAAIC,OAAOpF,MAAM,IAAIkF,YAAY;QACjC,IAAIE,OAAOE,IAAI,CAAC,CAACC,IAAMP,SAASO,GAAGF,KAAK;QACxCD,OAAOpE,IAAI,CAACqE;IACd;IACAD,OAAOnD,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAExB,KAAK,GAAGyB,EAAEzB,KAAK;IACvC,OAAO;QAAE0E;QAAQI,MAAML,MAAM,CAAC,EAAE;IAAC;AACnC;AAEA,0FAA0F;AAC1F,OAAO,SAASM,gBAAgB7F,IAAY,EAAEO,KAAe,EAAEiD,SAAiB,EAAE8B,UAAkB;IAClG,MAAM9E,MAAMF,gBAAgBN,MAAMO;IAClC,MAAMqC,SAASJ,eAAexC;IAC9B,IAAIQ,IAAIJ,MAAM,KAAK,GAAG;YAINwC;QAHd,uFAAuF;QACvF,0CAA0C;QAC1C,MAAMiB,SAAS,CAAC9C,MAAwB,GAAGf,KAAKa,KAAK,CAAC,GAAGE,KAAKpB,OAAO,CAAC,QAAQ,KAAKO,IAAI,KAAKa,MAAMf,KAAKI,MAAM,GAAG,MAAM,IAAI;QAC1H,MAAM0D,SAAQlB,eAAAA,OAAOkD,IAAI,CAAC,CAACvD,IAAMA,IAAI,gBAAvBK,0BAAAA,eAA6B5C,KAAKI,MAAM;QACtD,MAAM2D,aAAaL,KAAKsB,GAAG,CAAClB,OAAOR,UAAUtD,MAAM4C,QAAQ,GAAGY,WAAWzC,GAAG;QAC5E,MAAMA,MAAM6C,OAAOC,QAAQjB,QAAQkB,OAAOC,YAAYP;QACtD,OAAO;YAAEuC,UAAU;gBAAClC,OAAO9C;aAAK;YAAEiF,QAAQ;QAAE;IAC9C;IACA,MAAMtB,aAAaD,aAAajE,KAAKoC,QAAQ5C,MAAMwD;IACnD,MAAM,EAAEgC,MAAM,EAAEI,IAAI,EAAE,GAAGP,cAAcX,YAAYY;IACnD,OAAO;QAAES,UAAUP,OAAO3F,GAAG,CAAC,CAACqE,MAAQD,aAAajE,MAAMQ,KAAK0D;QAAO8B,QAAQJ,KAAK9E,KAAK;IAAC;AAC3F;AAEA,wEAAwE;AACxE,OAAO,SAASmF,aAAajG,IAAY,EAAEgG,MAAc;IACvD,IAAIE,OAAO;IACX,IAAK,IAAI7B,IAAI,GAAGA,IAAI2B,QAAQ3B,IAAK,IAAIrE,KAAKmG,UAAU,CAAC9B,OAAO,IAAI6B;IAChE,OAAOA;AACT;AAEA,eAAeE,aAAaC,KAAY,EAAEC,IAAY,EAAEJ,IAAY;IAClE,MAAMK,OAAO,MAAMF,MAAMG,OAAO,CAAC;IACjC,MAAMC,MAAO,MAAMF,KAAKpE,GAAG,CAACmE,MAAMJ,MAAMA;IACxC,OAAOO,MAAM,CAAC,CAAC,EAAEA,IAAIC,UAAU,CAAC,CAAC,EAAED,IAAIE,QAAQ,EAAE,GAAG;AACtD;AAqCA,SAASC,sBAAsBC,IAAmB;IAChD,IAAIA,KAAKC,OAAO,KAAK1E,aAAa,OAAOyE,KAAKC,OAAO,KAAK,WAAW;QACnE,MAAM,IAAIzI,WAAW,yBAAyB,CAAC,+CAA+C,EAAE0I,OAAOF,KAAKC,OAAO,GAAG;IACxH;IACA,KAAK,MAAM,CAACE,MAAMC,MAAM,IAAI;QAC1B;YAAC;YAAKJ,KAAKK,CAAC;SAAC;QACb;YAAC;YAAoBL,KAAKM,gBAAgB;SAAC;QAC3C;YAAC;YAAqBN,KAAKO,iBAAiB;SAAC;KAC9C,CAAW;QACV,IAAIH,UAAU7E,aAAc,CAAA,CAACiF,OAAOC,QAAQ,CAACL,UAAU,CAACI,OAAOE,SAAS,CAACN,UAAUA,SAAS,CAAA,GAAI;YAC9F,MAAM,IAAI5I,WAAW,yBAAyB,CAAC,eAAe,EAAE2I,KAAK,yCAAyC,EAAED,OAAOE,QAAQ;QACjI;IACF;AACF;AAEA,8FAA8F;AAC9F,OAAO,eAAeO,kBAAkBnB,KAAY,EAAEoB,GAAmB,EAAEC,IAA0B,EAAEC,YAAyB,EAAEpH,KAAa,EAAEsG,OAAsE,CAAC,CAAC;QAGrMA,wBACCA;IAHnB,IAAIc,aAAaxC,IAAI,KAAK,GAAG;IAC7B,MAAMyC,YAAYpI,iBAAiBe;IACnC,MAAMiD,aAAYqD,yBAAAA,KAAKM,gBAAgB,cAArBN,oCAAAA,yBAAyBxH;IAC3C,MAAMiG,cAAauB,0BAAAA,KAAKO,iBAAiB,cAAtBP,qCAAAA,0BAA0BvH;IAC7C,MAAMuI,aAAa,MAAMxB,MAAMG,OAAO,CAAC;IACvC,KAAK,MAAMC,OAAOiB,KAAM;QACtB,IAAI,CAACC,aAAazF,GAAG,CAACuE,IAAIH,IAAI,GAAG;QACjC,MAAMwB,SAAU,MAAMD,WAAW1F,GAAG,CAACsE,IAAIH,IAAI;QAC7C,IAAI,CAACwB,QAAQ,MAAM,IAAIzJ,WAAW,mBAAmB,CAAC,mCAAmC,EAAE0I,OAAON,IAAIH,IAAI,EAAE,yFAAyF,CAAC;QACtM,MAAMtG,OAAO8H,OAAO9H,IAAI;QACxB,MAAM,EAAE+F,QAAQ,EAAEC,MAAM,EAAE,GAAGH,gBAAgB7F,MAAM4H,WAAWpE,WAAW8B;QACzEmB,IAAIV,QAAQ,GAAGA;QACf,IAAIU,IAAIsB,KAAK,IAAI,MAAMtB,IAAIsB,KAAK,GAAGhK,eAAe0J,KAAK,cAAc,MAAMrB,aAAaC,OAAOI,IAAIH,IAAI,EAAEL,aAAajG,MAAMgG,WAAW;IACzI;AACF;AAEA;;;CAGC,GACD,OAAO,SAASgC,OAAO3B,KAAY,EAAEoB,GAAmB,EAAElH,KAAa,EAAEsG,OAAsB,CAAC,CAAC;QAC3FA;IAAJ,KAAIA,eAAAA,KAAKoB,MAAM,cAAXpB,mCAAAA,aAAaqB,OAAO,EAAE,OAAOC,QAAQC,MAAM,CAACvB,KAAKoB,MAAM,CAACI,MAAM;IAClE,OAAO/J,YAAY+H,OAAO,IAAMiC,cAAcjC,OAAOoB,KAAKlH,OAAOsG;AACnE;AAWA,eAAe0B,gBAAgBlC,KAAY,EAAEoB,GAAmB,EAAElH,KAAa,EAAEsG,IAAmB;IAClG,MAAM2B,YAAY,MAAMpJ,oBAAoBiH,OAAOoB;IACnD,MAAMgB,YAAYzK,cAAcyJ,KAAKZ;IACrC,MAAM6B,WAAW,AAAE,CAAA,MAAM,AAAC,CAAA,MAAMrC,MAAMG,OAAO,CAAC,iCAAgC,EAAGmC,GAAG,EAAC,EAA+B9I,GAAG,CAAC,CAAC+I,IAAMA,EAAEtC,IAAI;IACrI,MAAMuC,aAAa,MAAM9J,SAASsH,OAAOoB,KAAKZ,MAAM6B;IACpD,IAAII;IACJ,IAAI;QACFA,eAAe,MAAMhK,cAAcuH,OAAOwC,YAAYJ,UAAUM,KAAK;IACvE,EAAE,OAAOC,KAAK;QACZ,MAAMxK,YAAYwK,KAAczI,OAAOkI,UAAUM,KAAK;IACxD;IACA,MAAME,eAAeR,UAAUS,OAAO,CAACC,OAAO,KAAK/G;IACnD,IAAI6G,cAAc,MAAMhL,yBAAyBoI,OAAOoB,KAAKqB;IAC7D,MAAMM,mBAAmBH,gBAAiB,MAAMjK,mBAAmBqH,OAAOoB,KAAKqB;IAC/E,OAAO;QAAEN;QAAWC;QAAWC;QAAUG;QAAYC;QAAcM;IAAiB;AACtF;AAMA,SAASC,yBAAyBjG,MAAkD;IAClF,OAAO,CAACzB,MAAM2H,OAAO,CAAClG;AACxB;AAEA,eAAekF,cAAcjC,KAAY,EAAEoB,GAAmB,EAAElH,KAAa,EAAEsG,IAAmB;QAGhGA,cAyBAA,eAEAA,eAOAA,eAEAA;IAtCAD,sBAAsBC;IACtB,MAAM0C,UAAUvL,cAAcyJ,KAAKZ;KACnCA,eAAAA,KAAKoB,MAAM,cAAXpB,mCAAAA,aAAa2C,cAAc;IAC3B,oFAAoF;IACpF,0FAA0F;IAC1F,MAAMP,eAAeM,QAAQL,OAAO,CAACC,OAAO,KAAK/G;IACjD,IAAI6G,cAAc;QAChB,MAAMhE,IAAInH,YAAY2J,MAAM,oEAAoE;QAChG,IAAItJ,kBAAkB8G,IAAI;YACxB,MAAM,IAAI5G,WAAW,uBAAuB,CAAC,QAAQ,EAAEkL,QAAQE,UAAU,CAAC,mDAAmD,EAAExE,EAAEyE,KAAK,CAAC,aAAa,EAAEtL,gBAAgB,uHAAuH,CAAC;QAChS;IACF;IAEA,IAAIuL;IACJ,IAAIC;IACJ,IAAIX,cAAc;YAIhBpC;QAHA,6FAA6F;QAC7F,oFAAoF;QACpF,MAAMgD,YAAY,MAAMxD,MAAMyD,WAAW,CAAC,IAAMvB,gBAAgBlC,OAAOoB,KAAKlH,OAAOsG;SACnFA,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;QAC3B,IAAIK,UAAUT,gBAAgB,EAAE;gBAG9BvC;YAFA+C,oBAAoBC,UAAUrB,SAAS;YACvCmB,WAAW,MAAMzL,qBAAqBuJ,KAAKlH,OAAO;gBAAE0H,QAAQpB,KAAKoB,MAAM;YAAC;aACxEpB,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;QAC7B;IACF;KAEA3C,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;IAC3B,IAAIpG,SAAS,MAAMiD,MAAMyD,WAAW,CAAC,IAAMC,eAAe1D,OAAOoB,KAAKlH,OAAOsG,MAAM8C,UAAUC;KAC7F/C,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;IAC3B,IAAI,CAACH,yBAAyBjG,SAAS,OAAOA;IAE9C,6FAA6F;IAC7F,gGAAgG;IAChGwG,oBAAoBxG,OAAOoF,SAAS;IACpCmB,WAAW,MAAMzL,qBAAqBuJ,KAAKlH,OAAO;QAAE0H,QAAQpB,KAAKoB,MAAM;IAAC;KACxEpB,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;IAC3BpG,SAAS,MAAMiD,MAAMyD,WAAW,CAAC,IAAMC,eAAe1D,OAAOoB,KAAKlH,OAAOsG,MAAM8C,UAAUC;KACzF/C,gBAAAA,KAAKoB,MAAM,cAAXpB,oCAAAA,cAAa2C,cAAc;IAC3B,IAAIH,yBAAyBjG,SAAS,MAAM,IAAI/E,WAAW,mBAAmB;IAC9E,OAAO+E;AACT;AAEA,eAAe2G,eAAe1D,KAAY,EAAEoB,GAAmB,EAAElH,KAAa,EAAEsG,IAAmB,EAAE8C,QAA2C,EAAEC,iBAAqC;IACrL,MAAM,EAAEpB,SAAS,EAAEC,SAAS,EAAEC,QAAQ,EAAEG,UAAU,EAAEC,YAAY,EAAEM,gBAAgB,EAAE,GAAG,MAAMb,gBAAgBlC,OAAOoB,KAAKlH,OAAOsG;IAChI,IAAI8C,aAAavH,aAAaoG,cAAcoB,mBAAmB;QAC7D,MAAM,IAAIvL,WAAW,mBAAmB;IAC1C;IACA,IAAI+K,oBAAoBO,aAAavH,WAAW,OAAO;QAAEoG;IAAU;IAEnE,MAAM,EAAEtB,CAAC,EAAEgC,OAAO,EAAE,GAAGT;IACvB,MAAMuB,QAAQtG,KAAKsB,GAAG,CAACkC,IAAI,GAAG;IAC9B,MAAM+C,cAAcpB,WAAW1D,IAAI,GAAGuD,SAAStI,MAAM;IACrD,MAAM8J,kBAAkBd,oBAAoBO,aAAavH;IAEzD,2FAA2F;IAC3F,kEAAkE;IAClE,MAAM+H,QAAQ1B,UAAUM,KAAK;IAC7B,MAAMqB,YAAYD,QAAQ,CAAC,6CAA6C,CAAC,GAAG;IAC5E,MAAME,YAAYF,QAAQ,CAAC,KAAK,EAAEA,MAAM,CAAC,CAAC,GAAG;IAC7C,yFAAyF;IACzF,2GAA2G;IAC3G,IAAIF,aAAa,MAAMpL,iBAAiBwH,OAAO,iBAAiBwC;IAChE,MAAMyB,YAAYL,cAAc,CAAC,sDAAsD,CAAC,GAAG;IAE3F,MAAMvF,aAAyB,IAAIlD;IACnC,IAAI+I,YAA0B,EAAE;IAChC,MAAMC,iBAAiB;WAAIjK,MAAMkK,QAAQ,CAAC;KAAc,CAAC/E,IAAI,CAAC,CAAC5D,QAAU,CAAC,iBAAiB4I,IAAI,CAAC5I,KAAK,CAAC,EAAE;IACxG,MAAM6I,oBAAoB,QAAQD,IAAI,CAACnK,MAAMZ,OAAO,CAAC,YAAY;IACjE,6FAA6F;IAC7F,8FAA8F;IAC9F,IAAIuJ,QAAQ0B,KAAK,KAAKxI,aAAa7B,MAAML,IAAI,OAAO,MAAO,CAAA,iBAAiBwK,IAAI,CAACnK,UAAUoK,iBAAgB,KAAM,CAACH,gBAAgB;QAChI,MAAMK,cAActM,oBAAoBgC;QACxC,IAAIsK,aAAa,MAAMA;QACvB,IAAI;YACFN,YAAY,MAAMpL,gBAAgBkH,OAAO3B,YAAYnE,OAAO6J,WAAWC,WAAWC,WAAWN,OAAOd,QAAQ0B,KAAK,EAAE/D,KAAKC,OAAO;QACjI,EAAE,OAAOkC,KAAK;YACZ,MAAMxK,YAAYwK,KAAczI,OAAO4J;QACzC;IACF;IACA,uFAAuF;IACvF,qEAAqE;IACrE,MAAMxC,eAAe,IAAIjG,IAAI6I,UAAU1K,GAAG,CAAC,CAAC+I,IAAMA,EAAEtC,IAAI;IAExD,IAAI4C,QAAQ4B,KAAK,KAAK1I,WAAW,MAAMnD,gBAAgBoH,OAAO3B,YAAY6F,WAAW7B,UAAUI,cAAckB,OAAOd,QAAQ4B,KAAK,EAAEjE,KAAKC,OAAO;IAE/I,IAAIiE,aAAa,IAAIvJ;IACrB,IAAIwJ,kBAAkB,IAAIxJ;IAC1B,IAAI0I,mBAAmBhB,QAAQC,OAAO,KAAK/G,aAAauH,aAAavH,WAAW;QAC7E,CAAA,EAAE2I,UAAU,EAAEC,eAAe,EAAE,GAAG,MAAM9L,kBAAkBmH,OAAOsD,UAAUjF,YAAYsF,OAAOlB,cAAcJ,SAAStI,MAAM,EAAE8I,QAAQC,OAAO,EAAEtC,KAAKC,OAAO,CAAA;IAC7J;IAEA,MAAMT,MAAM4E,IAAI,CAAC;IACjB,8FAA8F;IAC9F,kEAAkE;IAClE,MAAM5E,MAAM4E,IAAI,CAAC;IACjB,IAAIvG,WAAWS,IAAI,GAAG,GAAG;QACvB,MAAMkB,MAAM6E,QAAQ,CAClB,sFACA;eAAIxG;SAAW,CAAC7E,GAAG,CAAC,CAAC,CAACyG,MAAMb,EAAE;gBAA4BsF,iBAA8BC;mBAArD;gBAAC1E;gBAAMb,EAAE0F,KAAK;gBAAE1F,EAAE2F,GAAG;iBAAEL,kBAAAA,WAAW5I,GAAG,CAACmE,mBAAfyE,6BAAAA,kBAAwB;iBAAMC,uBAAAA,gBAAgB7I,GAAG,CAACmE,mBAApB0E,kCAAAA,uBAA6B;aAAK;;IAE9H;IAEA,2FAA2F;IAC3F,qDAAqD;IACrD,MAAMjC,QAAQoB,QAAQ,CAAC,OAAO,EAAEA,MAAM,CAAC,CAAC,GAAG;IAC3C,+EAA+E;IAC/E,8DAA8D;IAC9D,MAAMkB,gBAAgBnB,kBAAkB,yBAAyB;IACjE,yFAAyF;IACzF,sEAAsE;IACtE,MAAMoB,aAAa,MAAMjF,MAAMG,OAAO,CACpC,CAAC,uIAAuI,EAAE6E,cAAc;;OAErJ,EAAEtC,MAAM,wDAAwD,CAAC;IAEtE,MAAMrB,OAAQ,MAAM4D,WAAW3C,GAAG,CAACzB;IACnC,IAAI3H,qBAAqBgM,cAAc,EAAEhM,qBAAqBiM,OAAO,CAAC;QAAEC,OAAO/D,KAAK7H,GAAG,CAAC,CAAC4G,MAAQA,IAAIH,IAAI;IAAE;IAC3G,KAAK,MAAMG,OAAOiB,KAAMjB,IAAIV,QAAQ,GAAG,EAAE;IAEzC,MAAMyB,kBAAkBnB,OAAOoB,KAAKC,MAAMC,cAAcpH,OAAOsG;IAC/D,IAAIA,KAAKC,OAAO,EAAE;;YAC0BpC;QAA1C,KAAK,MAAM+B,OAAOiB,KAAMjB,IAAIiF,WAAW,YAAGhH,kBAAAA,WAAWvC,GAAG,CAACsE,IAAIH,IAAI,eAAvB5B,sCAAAA,gBAA0BgH,WAAW,uCAAI,EAAE;IACvF;IAEA,OAAOhE;AACT"}
@@ -1,12 +1,27 @@
1
- import type { ResolvedConfig } from '../config/index.js';
1
+ import type { SignalName } from '../config/index.js';
2
+ import type { PreparedSemanticQuery } from '../embed/query.js';
2
3
  import type { LexicalHit, Store } from '../store/types.js';
3
- export type Candidates = Map<string, {
4
+ export type SearchResultVia = 'match' | 'match+link' | 'link' | 'match+vector' | 'match+link+vector' | 'link+vector' | 'vector';
5
+ /** One participating signal's contribution captured during the existing ranking computation. */
6
+ export interface SearchSignalContribution {
7
+ signal: SignalName;
8
+ /** One-based position; fusion uses weight / (60 + rank - 1). */
9
+ rank: number;
10
+ /** Effective weight from the selected preset. */
11
+ weight: number;
12
+ /** Unrounded value added to the fused score, independent of the rounded public score and via label. */
13
+ contribution: number;
14
+ }
15
+ interface Candidate {
4
16
  score: number;
5
- via: string;
6
- }>;
7
- export declare function wordsCandidates(store: Store, candidates: Candidates, query: string, whereJoin: string, whereCond: string, scopeCond: string, fetch: number, weight: number): Promise<LexicalHit[]>;
8
- export declare function linksCandidates(store: Store, candidates: Candidates, matchRows: LexicalHit[], allPaths: string[], allowedPaths: Set<string>, fetch: number, weight: number): Promise<void>;
9
- export declare function vectorsCandidates(store: Store, cfg: ResolvedConfig, candidates: Candidates, terms: string, fetch: number, allowedPaths: Set<string>, weight: number): Promise<{
17
+ via: SearchResultVia;
18
+ explanation?: SearchSignalContribution[];
19
+ }
20
+ export type Candidates = Map<string, Candidate>;
21
+ export declare function wordsCandidates(store: Store, candidates: Candidates, query: string, whereJoin: string, whereCond: string, scopeCond: string, fetch: number, weight: number, explain?: boolean): Promise<LexicalHit[]>;
22
+ export declare function linksCandidates(store: Store, candidates: Candidates, matchRows: LexicalHit[], allPaths: string[], allowedPaths: Set<string>, fetch: number, weight: number, explain?: boolean): Promise<void>;
23
+ export declare function vectorsCandidates(store: Store, prepared: PreparedSemanticQuery, candidates: Candidates, fetch: number, allowedPaths: Set<string>, totalPaths: number, weight: number, explain?: boolean): Promise<{
10
24
  chunkLines: Map<string, string>;
11
25
  chunkSimilarity: Map<string, number>;
12
26
  }>;
27
+ export {};
@@ -1,12 +1,19 @@
1
1
  import { semanticCandidates } from '../embed/query.js';
2
2
  import { LINK_EDGES_SQL, toEdges } from '../features/index.js';
3
3
  import { personalizedRank } from '../graph/graph.js';
4
+ import { withMaterializedVectorScope } from './scope.js';
4
5
  // RRF composes whatever the preset declares; the constant lives here since it belongs to
5
6
  // the fusion these three functions share, not to search() itself.
6
7
  const RRF_K = 60;
8
+ function withVector(via) {
9
+ if (via === 'match') return 'match+vector';
10
+ if (via === 'match+link') return 'match+link+vector';
11
+ if (via === 'link') return 'link+vector';
12
+ return via;
13
+ }
7
14
  // BM25 match rows (store.lexical.query), folded into `candidates` by reciprocal rank scaled by
8
15
  // the preset's declared weight for this signal. Every other signal is seeded from its output.
9
- export async function wordsCandidates(store, candidates, query, whereJoin, whereCond, scopeCond, fetch, weight) {
16
+ export async function wordsCandidates(store, candidates, query, whereJoin, whereCond, scopeCond, fetch, weight, explain = false) {
10
17
  const matchRows = await store.lexical.query(query, {
11
18
  whereJoin,
12
19
  whereCond,
@@ -14,16 +21,26 @@ export async function wordsCandidates(store, candidates, query, whereJoin, where
14
21
  limit: fetch
15
22
  });
16
23
  matchRows.forEach((r, i)=>{
17
- candidates.set(r.path, {
18
- score: weight / (RRF_K + i),
24
+ const contribution = weight / (RRF_K + i);
25
+ const candidate = {
26
+ score: contribution,
19
27
  via: 'match'
20
- });
28
+ };
29
+ if (explain) candidate.explanation = [
30
+ {
31
+ signal: 'words',
32
+ rank: i + 1,
33
+ weight,
34
+ contribution
35
+ }
36
+ ];
37
+ candidates.set(r.path, candidate);
21
38
  });
22
39
  return matchRows;
23
40
  }
24
41
  // Personalized-PageRank expansion from the word-match seeds, folded into `candidates` at this
25
42
  // signal's weight. A no-op when there is nothing to seed from (matchRows empty).
26
- export async function linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, weight) {
43
+ export async function linksCandidates(store, candidates, matchRows, allPaths, allowedPaths, fetch, weight, explain = false) {
27
44
  if (matchRows.length === 0) return;
28
45
  const stmt = await store.prepare(LINK_EDGES_SQL);
29
46
  const edges = toEdges(await stmt.all());
@@ -37,38 +54,78 @@ export async function linksCandidates(store, candidates, matchRows, allPaths, al
37
54
  ]));
38
55
  const ranked = [
39
56
  ...personalizedRank(allPaths, edges, seeds)
40
- ].filter(([path, score])=>score > 1e-9 && allowedPaths.has(path)).sort((a, b)=>b[1] - a[1]).slice(0, fetch);
57
+ ].filter(([path, score])=>score > 1e-9 && allowedPaths.has(path)).sort((a, b)=>b[1] - a[1] || Buffer.compare(Buffer.from(a[0]), Buffer.from(b[0]))).slice(0, fetch);
41
58
  ranked.forEach(([path], i)=>{
59
+ const contribution = weight / (RRF_K + i);
42
60
  const existing = candidates.get(path);
43
61
  if (existing) {
44
- existing.score += weight / (RRF_K + i);
62
+ existing.score += contribution;
63
+ if (explain) {
64
+ var _existing, _explanation;
65
+ (_explanation = (_existing = existing).explanation) !== null && _explanation !== void 0 ? _explanation : _existing.explanation = [];
66
+ existing.explanation.push({
67
+ signal: 'links',
68
+ rank: i + 1,
69
+ weight,
70
+ contribution
71
+ });
72
+ }
45
73
  if (linked.has(path)) existing.via = 'match+link';
46
74
  } else {
47
- candidates.set(path, {
48
- score: weight / (RRF_K + i),
75
+ const candidate = {
76
+ score: contribution,
49
77
  via: 'link'
50
- });
78
+ };
79
+ if (explain) candidate.explanation = [
80
+ {
81
+ signal: 'links',
82
+ rank: i + 1,
83
+ weight,
84
+ contribution
85
+ }
86
+ ];
87
+ candidates.set(path, candidate);
51
88
  }
52
89
  });
53
90
  }
54
91
  // Vector expansion at the swept flat-region constants (pool = fetch), folded into `candidates`
55
92
  // at this signal's weight; each row also carries its best chunk's line range and similarity.
56
- export async function vectorsCandidates(store, cfg, candidates, terms, fetch, allowedPaths, weight) {
93
+ export async function vectorsCandidates(store, prepared, candidates, fetch, allowedPaths, totalPaths, weight, explain = false) {
57
94
  const chunkLines = new Map();
58
95
  const chunkSimilarity = new Map();
59
- const vec = await semanticCandidates(store, cfg, terms, fetch, allowedPaths);
96
+ const vec = await withMaterializedVectorScope(store, allowedPaths, ()=>semanticCandidates(store, prepared, fetch, allowedPaths), totalPaths);
60
97
  vec.forEach(({ path, lines, similarity }, i)=>{
98
+ const contribution = weight / (RRF_K + i);
61
99
  chunkLines.set(path, lines);
62
100
  chunkSimilarity.set(path, similarity);
63
101
  const existing = candidates.get(path);
64
102
  if (existing) {
65
- existing.score += weight / (RRF_K + i);
66
- existing.via = `${existing.via}+vector`;
103
+ existing.score += contribution;
104
+ if (explain) {
105
+ var _existing, _explanation;
106
+ (_explanation = (_existing = existing).explanation) !== null && _explanation !== void 0 ? _explanation : _existing.explanation = [];
107
+ existing.explanation.push({
108
+ signal: 'vectors',
109
+ rank: i + 1,
110
+ weight,
111
+ contribution
112
+ });
113
+ }
114
+ existing.via = withVector(existing.via);
67
115
  } else {
68
- candidates.set(path, {
69
- score: weight / (RRF_K + i),
116
+ const candidate = {
117
+ score: contribution,
70
118
  via: 'vector'
71
- });
119
+ };
120
+ if (explain) candidate.explanation = [
121
+ {
122
+ signal: 'vectors',
123
+ rank: i + 1,
124
+ weight,
125
+ contribution
126
+ }
127
+ ];
128
+ candidates.set(path, candidate);
72
129
  }
73
130
  });
74
131
  return {