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 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/signals.ts"],"sourcesContent":["import type { ResolvedConfig } from '../config/index.ts';\nimport { semanticCandidates } from '../embed/query.ts';\nimport { LINK_EDGES_SQL, toEdges } from '../features/index.ts';\nimport { personalizedRank } from '../graph/graph.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\n\n// RRF composes whatever the preset declares; the constant lives here since it belongs to\n// the fusion these three functions share, not to search() itself.\nconst RRF_K = 60;\n\nexport type Candidates = Map<string, { score: number; via: string }>;\n\n// BM25 match rows (store.lexical.query), folded into `candidates` by reciprocal rank scaled by\n// the preset's declared weight for this signal. Every other signal is seeded from its output.\nexport async function wordsCandidates(store: Store, candidates: Candidates, query: string, whereJoin: string, whereCond: string, scopeCond: string, fetch: number, weight: number): Promise<LexicalHit[]> {\n const matchRows = await store.lexical.query(query, { whereJoin, whereCond, scopeCond, limit: fetch });\n matchRows.forEach((r, i) => {\n candidates.set(r.path, { score: weight / (RRF_K + i), via: 'match' });\n });\n return matchRows;\n}\n\n// Personalized-PageRank expansion from the word-match seeds, folded into `candidates` at this\n// signal's weight. A no-op when there is nothing to seed from (matchRows empty).\nexport async function linksCandidates(store: Store, candidates: Candidates, matchRows: LexicalHit[], allPaths: string[], allowedPaths: Set<string>, fetch: number, weight: number): Promise<void> {\n if (matchRows.length === 0) return;\n const stmt = await store.prepare(LINK_EDGES_SQL);\n const edges = toEdges((await stmt.all()) as Array<{ src: string; dst: string }>);\n if (edges.length === 0) return;\n // Gates the label only, not the score: restart mass ranks every seed without an incident\n // edge, but dropping it from the score reweights fusion (FEVER hit@10 0.997 -> 0.907).\n const linked = new Set(edges.flat());\n const seeds = new Map(matchRows.map((r, i) => [r.path, 1 / (i + 1)]));\n const ranked = [...personalizedRank(allPaths, edges, seeds)]\n .filter(([path, score]) => score > 1e-9 && allowedPaths.has(path))\n .sort((a, b) => b[1] - a[1])\n .slice(0, fetch);\n ranked.forEach(([path], i) => {\n const existing = candidates.get(path);\n if (existing) {\n existing.score += weight / (RRF_K + i);\n if (linked.has(path)) existing.via = 'match+link';\n } else {\n candidates.set(path, { score: weight / (RRF_K + i), via: 'link' });\n }\n });\n}\n\n// Vector expansion at the swept flat-region constants (pool = fetch), folded into `candidates`\n// at this signal's weight; each row also carries its best chunk's line range and similarity.\nexport async function vectorsCandidates(store: Store, cfg: ResolvedConfig, candidates: Candidates, terms: string, fetch: number, allowedPaths: Set<string>, weight: number): Promise<{ chunkLines: Map<string, string>; chunkSimilarity: Map<string, number> }> {\n const chunkLines = new Map<string, string>();\n const chunkSimilarity = new Map<string, number>();\n const vec = await semanticCandidates(store, cfg, terms, fetch, allowedPaths);\n vec.forEach(({ path, lines, similarity }, i) => {\n chunkLines.set(path, lines);\n chunkSimilarity.set(path, similarity);\n const existing = candidates.get(path);\n if (existing) {\n existing.score += weight / (RRF_K + i);\n existing.via = `${existing.via}+vector`;\n } else {\n candidates.set(path, { score: weight / (RRF_K + i), via: 'vector' });\n }\n });\n return { chunkLines, chunkSimilarity };\n}\n"],"names":["semanticCandidates","LINK_EDGES_SQL","toEdges","personalizedRank","RRF_K","wordsCandidates","store","candidates","query","whereJoin","whereCond","scopeCond","fetch","weight","matchRows","lexical","limit","forEach","r","i","set","path","score","via","linksCandidates","allPaths","allowedPaths","length","stmt","prepare","edges","all","linked","Set","flat","seeds","Map","map","ranked","filter","has","sort","a","b","slice","existing","get","vectorsCandidates","cfg","terms","chunkLines","chunkSimilarity","vec","lines","similarity"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/signals.ts"],"sourcesContent":["import type { SignalName } from '../config/index.ts';\nimport type { PreparedSemanticQuery } from '../embed/query.ts';\nimport { semanticCandidates } from '../embed/query.ts';\nimport { LINK_EDGES_SQL, toEdges } from '../features/index.ts';\nimport { personalizedRank } from '../graph/graph.ts';\nimport type { LexicalHit, Store } from '../store/types.ts';\nimport { withMaterializedVectorScope } from './scope.ts';\n\n// RRF composes whatever the preset declares; the constant lives here since it belongs to\n// the fusion these three functions share, not to search() itself.\nconst RRF_K = 60;\n\nexport type SearchResultVia = 'match' | 'match+link' | 'link' | 'match+vector' | 'match+link+vector' | 'link+vector' | 'vector';\n\n/** One participating signal's contribution captured during the existing ranking computation. */\nexport interface SearchSignalContribution {\n signal: SignalName;\n /** One-based position; fusion uses weight / (60 + rank - 1). */\n rank: number;\n /** Effective weight from the selected preset. */\n weight: number;\n /** Unrounded value added to the fused score, independent of the rounded public score and via label. */\n contribution: number;\n}\n\ninterface Candidate {\n score: number;\n via: SearchResultVia;\n explanation?: SearchSignalContribution[];\n}\n\nexport type Candidates = Map<string, Candidate>;\n\nfunction withVector(via: SearchResultVia): SearchResultVia {\n if (via === 'match') return 'match+vector';\n if (via === 'match+link') return 'match+link+vector';\n if (via === 'link') return 'link+vector';\n return via;\n}\n\n// BM25 match rows (store.lexical.query), folded into `candidates` by reciprocal rank scaled by\n// the preset's declared weight for this signal. Every other signal is seeded from its output.\nexport async function wordsCandidates(store: Store, candidates: Candidates, query: string, whereJoin: string, whereCond: string, scopeCond: string, fetch: number, weight: number, explain = false): Promise<LexicalHit[]> {\n const matchRows = await store.lexical.query(query, { whereJoin, whereCond, scopeCond, limit: fetch });\n matchRows.forEach((r, i) => {\n const contribution = weight / (RRF_K + i);\n const candidate: Candidate = { score: contribution, via: 'match' };\n if (explain) candidate.explanation = [{ signal: 'words', rank: i + 1, weight, contribution }];\n candidates.set(r.path, candidate);\n });\n return matchRows;\n}\n\n// Personalized-PageRank expansion from the word-match seeds, folded into `candidates` at this\n// signal's weight. A no-op when there is nothing to seed from (matchRows empty).\nexport async function linksCandidates(store: Store, candidates: Candidates, matchRows: LexicalHit[], allPaths: string[], allowedPaths: Set<string>, fetch: number, weight: number, explain = false): Promise<void> {\n if (matchRows.length === 0) return;\n const stmt = await store.prepare(LINK_EDGES_SQL);\n const edges = toEdges((await stmt.all()) as Array<{ src: string; dst: string }>);\n if (edges.length === 0) return;\n // Gates the label only, not the score: restart mass ranks every seed without an incident\n // edge, but dropping it from the score reweights fusion (FEVER hit@10 0.997 -> 0.907).\n const linked = new Set(edges.flat());\n const seeds = new Map(matchRows.map((r, i) => [r.path, 1 / (i + 1)]));\n const ranked = [...personalizedRank(allPaths, edges, seeds)]\n .filter(([path, score]) => score > 1e-9 && allowedPaths.has(path))\n .sort((a, b) => b[1] - a[1] || Buffer.compare(Buffer.from(a[0]), Buffer.from(b[0])))\n .slice(0, fetch);\n ranked.forEach(([path], i) => {\n const contribution = weight / (RRF_K + i);\n const existing = candidates.get(path);\n if (existing) {\n existing.score += contribution;\n if (explain) {\n existing.explanation ??= [];\n existing.explanation.push({ signal: 'links', rank: i + 1, weight, contribution });\n }\n if (linked.has(path)) existing.via = 'match+link';\n } else {\n const candidate: Candidate = { score: contribution, via: 'link' };\n if (explain) candidate.explanation = [{ signal: 'links', rank: i + 1, weight, contribution }];\n candidates.set(path, candidate);\n }\n });\n}\n\n// Vector expansion at the swept flat-region constants (pool = fetch), folded into `candidates`\n// at this signal's weight; each row also carries its best chunk's line range and similarity.\nexport async function vectorsCandidates(store: Store, prepared: PreparedSemanticQuery, candidates: Candidates, fetch: number, allowedPaths: Set<string>, totalPaths: number, weight: number, explain = false): Promise<{ chunkLines: Map<string, string>; chunkSimilarity: Map<string, number> }> {\n const chunkLines = new Map<string, string>();\n const chunkSimilarity = new Map<string, number>();\n const vec = await withMaterializedVectorScope(store, allowedPaths, () => semanticCandidates(store, prepared, fetch, allowedPaths), totalPaths);\n vec.forEach(({ path, lines, similarity }, i) => {\n const contribution = weight / (RRF_K + i);\n chunkLines.set(path, lines);\n chunkSimilarity.set(path, similarity);\n const existing = candidates.get(path);\n if (existing) {\n existing.score += contribution;\n if (explain) {\n existing.explanation ??= [];\n existing.explanation.push({ signal: 'vectors', rank: i + 1, weight, contribution });\n }\n existing.via = withVector(existing.via);\n } else {\n const candidate: Candidate = { score: contribution, via: 'vector' };\n if (explain) candidate.explanation = [{ signal: 'vectors', rank: i + 1, weight, contribution }];\n candidates.set(path, candidate);\n }\n });\n return { chunkLines, chunkSimilarity };\n}\n"],"names":["semanticCandidates","LINK_EDGES_SQL","toEdges","personalizedRank","withMaterializedVectorScope","RRF_K","withVector","via","wordsCandidates","store","candidates","query","whereJoin","whereCond","scopeCond","fetch","weight","explain","matchRows","lexical","limit","forEach","r","i","contribution","candidate","score","explanation","signal","rank","set","path","linksCandidates","allPaths","allowedPaths","length","stmt","prepare","edges","all","linked","Set","flat","seeds","Map","map","ranked","filter","has","sort","a","b","Buffer","compare","from","slice","existing","get","push","vectorsCandidates","prepared","totalPaths","chunkLines","chunkSimilarity","vec","lines","similarity"],"mappings":"AAEA,SAASA,kBAAkB,QAAQ,oBAAoB;AACvD,SAASC,cAAc,EAAEC,OAAO,QAAQ,uBAAuB;AAC/D,SAASC,gBAAgB,QAAQ,oBAAoB;AAErD,SAASC,2BAA2B,QAAQ,aAAa;AAEzD,yFAAyF;AACzF,kEAAkE;AAClE,MAAMC,QAAQ;AAuBd,SAASC,WAAWC,GAAoB;IACtC,IAAIA,QAAQ,SAAS,OAAO;IAC5B,IAAIA,QAAQ,cAAc,OAAO;IACjC,IAAIA,QAAQ,QAAQ,OAAO;IAC3B,OAAOA;AACT;AAEA,+FAA+F;AAC/F,8FAA8F;AAC9F,OAAO,eAAeC,gBAAgBC,KAAY,EAAEC,UAAsB,EAAEC,KAAa,EAAEC,SAAiB,EAAEC,SAAiB,EAAEC,SAAiB,EAAEC,KAAa,EAAEC,MAAc,EAAEC,UAAU,KAAK;IAChM,MAAMC,YAAY,MAAMT,MAAMU,OAAO,CAACR,KAAK,CAACA,OAAO;QAAEC;QAAWC;QAAWC;QAAWM,OAAOL;IAAM;IACnGG,UAAUG,OAAO,CAAC,CAACC,GAAGC;QACpB,MAAMC,eAAeR,SAAUX,CAAAA,QAAQkB,CAAAA;QACvC,MAAME,YAAuB;YAAEC,OAAOF;YAAcjB,KAAK;QAAQ;QACjE,IAAIU,SAASQ,UAAUE,WAAW,GAAG;YAAC;gBAAEC,QAAQ;gBAASC,MAAMN,IAAI;gBAAGP;gBAAQQ;YAAa;SAAE;QAC7Fd,WAAWoB,GAAG,CAACR,EAAES,IAAI,EAAEN;IACzB;IACA,OAAOP;AACT;AAEA,8FAA8F;AAC9F,iFAAiF;AACjF,OAAO,eAAec,gBAAgBvB,KAAY,EAAEC,UAAsB,EAAEQ,SAAuB,EAAEe,QAAkB,EAAEC,YAAyB,EAAEnB,KAAa,EAAEC,MAAc,EAAEC,UAAU,KAAK;IAChM,IAAIC,UAAUiB,MAAM,KAAK,GAAG;IAC5B,MAAMC,OAAO,MAAM3B,MAAM4B,OAAO,CAACpC;IACjC,MAAMqC,QAAQpC,QAAS,MAAMkC,KAAKG,GAAG;IACrC,IAAID,MAAMH,MAAM,KAAK,GAAG;IACxB,yFAAyF;IACzF,uFAAuF;IACvF,MAAMK,SAAS,IAAIC,IAAIH,MAAMI,IAAI;IACjC,MAAMC,QAAQ,IAAIC,IAAI1B,UAAU2B,GAAG,CAAC,CAACvB,GAAGC,IAAM;YAACD,EAAES,IAAI;YAAE,IAAKR,CAAAA,IAAI,CAAA;SAAG;IACnE,MAAMuB,SAAS;WAAI3C,iBAAiB8B,UAAUK,OAAOK;KAAO,CACzDI,MAAM,CAAC,CAAC,CAAChB,MAAML,MAAM,GAAKA,QAAQ,QAAQQ,aAAac,GAAG,CAACjB,OAC3DkB,IAAI,CAAC,CAACC,GAAGC,IAAMA,CAAC,CAAC,EAAE,GAAGD,CAAC,CAAC,EAAE,IAAIE,OAAOC,OAAO,CAACD,OAAOE,IAAI,CAACJ,CAAC,CAAC,EAAE,GAAGE,OAAOE,IAAI,CAACH,CAAC,CAAC,EAAE,IAChFI,KAAK,CAAC,GAAGxC;IACZ+B,OAAOzB,OAAO,CAAC,CAAC,CAACU,KAAK,EAAER;QACtB,MAAMC,eAAeR,SAAUX,CAAAA,QAAQkB,CAAAA;QACvC,MAAMiC,WAAW9C,WAAW+C,GAAG,CAAC1B;QAChC,IAAIyB,UAAU;YACZA,SAAS9B,KAAK,IAAIF;YAClB,IAAIP,SAAS;oBACXuC,WAAAA;iBAAAA,eAAAA,CAAAA,YAAAA,UAAS7B,WAAW,cAApB6B,0BAAAA,eAAAA,UAAS7B,WAAW,GAAK,EAAE;gBAC3B6B,SAAS7B,WAAW,CAAC+B,IAAI,CAAC;oBAAE9B,QAAQ;oBAASC,MAAMN,IAAI;oBAAGP;oBAAQQ;gBAAa;YACjF;YACA,IAAIgB,OAAOQ,GAAG,CAACjB,OAAOyB,SAASjD,GAAG,GAAG;QACvC,OAAO;YACL,MAAMkB,YAAuB;gBAAEC,OAAOF;gBAAcjB,KAAK;YAAO;YAChE,IAAIU,SAASQ,UAAUE,WAAW,GAAG;gBAAC;oBAAEC,QAAQ;oBAASC,MAAMN,IAAI;oBAAGP;oBAAQQ;gBAAa;aAAE;YAC7Fd,WAAWoB,GAAG,CAACC,MAAMN;QACvB;IACF;AACF;AAEA,+FAA+F;AAC/F,6FAA6F;AAC7F,OAAO,eAAekC,kBAAkBlD,KAAY,EAAEmD,QAA+B,EAAElD,UAAsB,EAAEK,KAAa,EAAEmB,YAAyB,EAAE2B,UAAkB,EAAE7C,MAAc,EAAEC,UAAU,KAAK;IAC1M,MAAM6C,aAAa,IAAIlB;IACvB,MAAMmB,kBAAkB,IAAInB;IAC5B,MAAMoB,MAAM,MAAM5D,4BAA4BK,OAAOyB,cAAc,IAAMlC,mBAAmBS,OAAOmD,UAAU7C,OAAOmB,eAAe2B;IACnIG,IAAI3C,OAAO,CAAC,CAAC,EAAEU,IAAI,EAAEkC,KAAK,EAAEC,UAAU,EAAE,EAAE3C;QACxC,MAAMC,eAAeR,SAAUX,CAAAA,QAAQkB,CAAAA;QACvCuC,WAAWhC,GAAG,CAACC,MAAMkC;QACrBF,gBAAgBjC,GAAG,CAACC,MAAMmC;QAC1B,MAAMV,WAAW9C,WAAW+C,GAAG,CAAC1B;QAChC,IAAIyB,UAAU;YACZA,SAAS9B,KAAK,IAAIF;YAClB,IAAIP,SAAS;oBACXuC,WAAAA;iBAAAA,eAAAA,CAAAA,YAAAA,UAAS7B,WAAW,cAApB6B,0BAAAA,eAAAA,UAAS7B,WAAW,GAAK,EAAE;gBAC3B6B,SAAS7B,WAAW,CAAC+B,IAAI,CAAC;oBAAE9B,QAAQ;oBAAWC,MAAMN,IAAI;oBAAGP;oBAAQQ;gBAAa;YACnF;YACAgC,SAASjD,GAAG,GAAGD,WAAWkD,SAASjD,GAAG;QACxC,OAAO;YACL,MAAMkB,YAAuB;gBAAEC,OAAOF;gBAAcjB,KAAK;YAAS;YAClE,IAAIU,SAASQ,UAAUE,WAAW,GAAG;gBAAC;oBAAEC,QAAQ;oBAAWC,MAAMN,IAAI;oBAAGP;oBAAQQ;gBAAa;aAAE;YAC/Fd,WAAWoB,GAAG,CAACC,MAAMN;QACvB;IACF;IACA,OAAO;QAAEqC;QAAYC;IAAgB;AACvC"}
@@ -0,0 +1,3 @@
1
+ import type { ResolvedConfig } from '../config/index.js';
2
+ import type { Store } from '../store/types.js';
3
+ export declare function assertQuerySnapshot(store: Store, cfg: ResolvedConfig): Promise<string>;
@@ -0,0 +1,25 @@
1
+ import { featureSignature } from '../config/index.js';
2
+ import { SenseError } from '../errors.js';
3
+ import { FEATURES } from '../features/index.js';
4
+ import { CORE_READY_META_KEY, FEATURE_SIGNATURE_META_KEY, isCoreReady, isPublishedFeatureSignature } from '../store/readiness.js';
5
+ // Read-only policy check for a caller-owned transaction. The store readiness module owns the
6
+ // durable keys and compatibility rules; commands only require one published generation before
7
+ // reading scope, candidates, graph state, and result details from that same snapshot.
8
+ export async function assertQuerySnapshot(store, cfg) {
9
+ var _values_get, _values_get1;
10
+ const stmt = await store.prepare('SELECT key, value FROM meta WHERE key IN (?, ?)');
11
+ const rows = await stmt.all(CORE_READY_META_KEY, FEATURE_SIGNATURE_META_KEY);
12
+ const values = new Map(rows.map((row)=>[
13
+ row.key,
14
+ row.value
15
+ ]));
16
+ if (!isCoreReady((_values_get = values.get(CORE_READY_META_KEY)) !== null && _values_get !== void 0 ? _values_get : null)) {
17
+ throw new SenseError('INDEX_NOT_READY', 'the index has no complete published generation; wait for the active build to finish, or run `sense build` (or library build(config)) to recover it');
18
+ }
19
+ const actual = (_values_get1 = values.get(FEATURE_SIGNATURE_META_KEY)) !== null && _values_get1 !== void 0 ? _values_get1 : null;
20
+ const expected = featureSignature(cfg, FEATURES);
21
+ if (actual === null || !isPublishedFeatureSignature(actual, expected)) {
22
+ throw new SenseError('INDEX_NOT_READY', 'the published index was built for different configuration features; rebuild it with `sense build` (or library build(config)) before querying');
23
+ }
24
+ return actual;
25
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/commands/snapshot.ts"],"sourcesContent":["import type { ResolvedConfig } from '../config/index.ts';\nimport { featureSignature } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { FEATURES } from '../features/index.ts';\nimport { CORE_READY_META_KEY, FEATURE_SIGNATURE_META_KEY, isCoreReady, isPublishedFeatureSignature } from '../store/readiness.ts';\nimport type { Store } from '../store/types.ts';\n\n// Read-only policy check for a caller-owned transaction. The store readiness module owns the\n// durable keys and compatibility rules; commands only require one published generation before\n// reading scope, candidates, graph state, and result details from that same snapshot.\nexport async function assertQuerySnapshot(store: Store, cfg: ResolvedConfig): Promise<string> {\n const stmt = await store.prepare('SELECT key, value FROM meta WHERE key IN (?, ?)');\n const rows = (await stmt.all(CORE_READY_META_KEY, FEATURE_SIGNATURE_META_KEY)) as Array<{ key: string; value: string }>;\n const values = new Map(rows.map((row) => [row.key, row.value]));\n if (!isCoreReady(values.get(CORE_READY_META_KEY) ?? null)) {\n throw new SenseError('INDEX_NOT_READY', 'the index has no complete published generation; wait for the active build to finish, or run `sense build` (or library build(config)) to recover it');\n }\n\n const actual = values.get(FEATURE_SIGNATURE_META_KEY) ?? null;\n const expected = featureSignature(cfg, FEATURES);\n if (actual === null || !isPublishedFeatureSignature(actual, expected)) {\n throw new SenseError('INDEX_NOT_READY', 'the published index was built for different configuration features; rebuild it with `sense build` (or library build(config)) before querying');\n }\n return actual;\n}\n"],"names":["featureSignature","SenseError","FEATURES","CORE_READY_META_KEY","FEATURE_SIGNATURE_META_KEY","isCoreReady","isPublishedFeatureSignature","assertQuerySnapshot","store","cfg","values","stmt","prepare","rows","all","Map","map","row","key","value","get","actual","expected"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,qBAAqB;AACtD,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,QAAQ,QAAQ,uBAAuB;AAChD,SAASC,mBAAmB,EAAEC,0BAA0B,EAAEC,WAAW,EAAEC,2BAA2B,QAAQ,wBAAwB;AAGlI,6FAA6F;AAC7F,8FAA8F;AAC9F,sFAAsF;AACtF,OAAO,eAAeC,oBAAoBC,KAAY,EAAEC,GAAmB;QAIxDC,aAIFA;IAPf,MAAMC,OAAO,MAAMH,MAAMI,OAAO,CAAC;IACjC,MAAMC,OAAQ,MAAMF,KAAKG,GAAG,CAACX,qBAAqBC;IAClD,MAAMM,SAAS,IAAIK,IAAIF,KAAKG,GAAG,CAAC,CAACC,MAAQ;YAACA,IAAIC,GAAG;YAAED,IAAIE,KAAK;SAAC;IAC7D,IAAI,CAACd,aAAYK,cAAAA,OAAOU,GAAG,CAACjB,kCAAXO,yBAAAA,cAAmC,OAAO;QACzD,MAAM,IAAIT,WAAW,mBAAmB;IAC1C;IAEA,MAAMoB,UAASX,eAAAA,OAAOU,GAAG,CAAChB,yCAAXM,0BAAAA,eAA0C;IACzD,MAAMY,WAAWtB,iBAAiBS,KAAKP;IACvC,IAAImB,WAAW,QAAQ,CAACf,4BAA4Be,QAAQC,WAAW;QACrE,MAAM,IAAIrB,WAAW,mBAAmB;IAC1C;IACA,OAAOoB;AACT"}
@@ -1,3 +1,4 @@
1
+ import { channel } from 'node:diagnostics_channel';
1
2
  import { existsSync, readFileSync } from 'node:fs';
2
3
  import { dirname, join, resolve } from 'node:path';
3
4
  import { SenseError } from '../errors.js';
@@ -7,6 +8,7 @@ import { unknownConfigKeys, validateConfig, validateLegacyScan } from './validat
7
8
  // v1 and v2 were `scan`/`find`-shaped; v3 introduced `presets`. Only the former need the
8
9
  // legacy shape check before migrating.
9
10
  const FIRST_PRESET_VERSION = 3;
11
+ const migrationReady = channel('sensemaking.config.migration-ready');
10
12
  // v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).
11
13
  const V2_OPT_OUT_NAMES = [
12
14
  'links',
@@ -282,6 +284,10 @@ export function loadConfig(explicitPath, options = {}) {
282
284
  migratedFrom = result.from;
283
285
  if (options.writeMigration !== false) {
284
286
  const migrated = `${JSON.stringify(cfg, null, 2)}\n`;
287
+ if (migrationReady.hasSubscribers) migrationReady.publish({
288
+ configPath,
289
+ from: migratedFrom
290
+ });
285
291
  try {
286
292
  writeFileAtomic(configPath, migrated);
287
293
  } catch (err) {
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/load.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from '../errors.ts';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\nimport type { Config, ResolvedConfig } from './types.ts';\nimport { CONFIG_FILENAME, DEFAULT_EMBED_MODEL, SUPPORTED_CONFIG_VERSION } from './types.ts';\nimport { unknownConfigKeys, validateConfig, validateLegacyScan } from './validate.ts';\n\n// v1 and v2 were `scan`/`find`-shaped; v3 introduced `presets`. Only the former need the\n// legacy shape check before migrating.\nconst FIRST_PRESET_VERSION = 3;\n\n// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).\nconst V2_OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\n\n// Pure per-version steps, chained by loadConfig. Intermediate shapes predate the Config type,\n// so steps are loosely typed and only the final result is cast back.\nconst MIGRATIONS: Record<number, (cfg: Record<string, unknown>) => Record<string, unknown>> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name) => [name, true])) }),\n // v2 -> v3: mechanical-minimal by decision -- only what keeps an existing config loading (no\n // restructuring, no preset inference, no query rewriting); real trees get hand-migrated to use presets.\n 2: (cfg) => {\n const { scan, defaults, features, checks, queries, ...rest } = cfg;\n const scanInclude = (scan as { include: string[] }).include;\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((queries as Record<string, unknown> | undefined) ?? {})) {\n if (typeof value === 'string') {\n nextQueries[name] = value;\n continue;\n }\n const entry = value as { find: string; k?: number; where?: string; semantic?: boolean };\n // Loosely typed on purpose: this step emits the v3 shape, which still had `semantic`.\n // The v3 -> v4 step below strips it.\n const search: Record<string, unknown> = { search: entry.find };\n if (entry.k !== undefined) search.k = entry.k;\n if (entry.where !== undefined) search.where = entry.where;\n // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.\n if (entry.semantic === false) search.semantic = false;\n nextQueries[name] = search;\n }\n\n const prevDefaults = defaults as { find?: { where?: string } } | undefined;\n const defaultWhere = prevDefaults?.find?.where;\n\n // features.embed false becomes semantic:false on every preset produced here.\n const prevFeatures = (features as Record<string, unknown> | undefined) ?? {};\n const embedWasOff = prevFeatures.embed === false;\n // Object form carries provider settings (model/type/url/key), not a toggle -- they move to\n // the v3 top-level `embed` block verbatim; dropping them would silently switch an api tree to the static model.\n const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;\n const { embed: _embed, ...restFeatures } = prevFeatures;\n\n const defaultPreset: Record<string, unknown> = { include: scanInclude };\n if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;\n if (embedWasOff) defaultPreset.semantic = false;\n\n if (checks !== undefined) {\n console.error('sense: v2 \"checks\" was removed in v3 (sense check no longer asserts on saved queries); its queries are carried over under \"queries\" -- a returned row set is now the reader\\'s judgment');\n }\n\n const result: Record<string, unknown> = { ...rest, version: 3, presets: { default: defaultPreset }, queries: nextQueries };\n if (embedProvider !== undefined) result.embed = embedProvider;\n if (Object.keys(restFeatures).length > 0) result.features = restFeatures;\n return result;\n },\n // v3 -> v4: write the model into the file, wrap bare-string queries as { sql }, drop a saved\n // search's `semantic` (its preset decides). Same effective model, so nothing re-embeds.\n 3: (cfg) => {\n const presets = cfg.presets as Record<string, Record<string, unknown>>;\n const embedded = Object.values(presets).some((p) => p.semantic !== false);\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((cfg.queries as Record<string, unknown> | undefined) ?? {})) {\n // v3's bare string meant SQL by inference; v4 makes every entry name its verb.\n if (typeof value === 'string') {\n nextQueries[name] = { sql: value };\n continue;\n }\n if (typeof value !== 'object' || value === null || !('search' in value)) {\n nextQueries[name] = value;\n continue;\n }\n const { semantic: _semantic, ...rest } = value as Record<string, unknown>;\n nextQueries[name] = rest;\n }\n\n const prev = (cfg.embed as Record<string, unknown> | undefined) ?? {};\n const result: Record<string, unknown> = { ...cfg, version: 4, queries: nextQueries };\n if (embedded) {\n result.embed = { model: (prev.model as string | undefined) ?? DEFAULT_EMBED_MODEL, type: (prev.type as string | undefined) ?? 'static', ...(prev.url !== undefined ? { url: prev.url } : {}), ...(prev.key !== undefined ? { key: prev.key } : {}) };\n } else {\n delete result.embed;\n // Dropping settings silently would look like a bug in a tree that configured an api endpoint\n // then turned vectors off; say it, since v4 has no way to carry provider settings for a tree that doesn't embed.\n if (Object.keys(prev).length > 0) console.error('sense: every preset has \"semantic\": false, so v4 drops the \"embed\" block (vectors stay off); re-add it, or set a preset\\'s semantic back to true, to turn them on');\n }\n return result;\n },\n // v4 -> v5: embed.type renames to embed.provider (\"api\" migrates to \"openai\", the wire protocol\n // it always was; other values pass through). A preset's `semantic` also migrates to `signals` (false -> explicit weights, true/absent -> no key).\n 4: (cfg) => {\n const prevEmbed = cfg.embed as Record<string, unknown> | undefined;\n const result: Record<string, unknown> = { ...cfg, version: 5 };\n if (prevEmbed) {\n const { type, ...rest } = prevEmbed;\n result.embed = type === undefined ? rest : { ...rest, provider: type === 'api' ? 'openai' : type };\n }\n\n const linksOn = (cfg.features as Record<string, unknown> | undefined)?.links !== false;\n const presets = cfg.presets as Record<string, Record<string, unknown>> | undefined;\n if (presets) {\n const nextPresets: Record<string, unknown> = {};\n for (const [name, preset] of Object.entries(presets)) {\n const { semantic, ...rest } = preset;\n if (semantic === undefined) {\n nextPresets[name] = preset;\n continue;\n }\n if (typeof semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `presets.${name}.semantic must be a boolean`);\n }\n nextPresets[name] = semantic === false ? { ...rest, signals: linksOn ? { words: 1, links: 1 } : { words: 1 } } : rest;\n }\n result.presets = nextPresets;\n }\n return result;\n },\n // v5 -> v6: query-triggered builds are on by default. Preserve an explicit opt-out.\n 5: (cfg) => ({ ...cfg, version: 6, build: cfg.build === undefined ? true : cfg.build }),\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current: Record<string, unknown> = cfg as unknown as Record<string, unknown>;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current as unknown as Config, from };\n}\n\n// `sense init`'s flags map onto these three `embed` keys one-to-one; an absent field\n// keeps the built-in default rather than merging partial input.\nexport interface InitOverrides {\n model?: string;\n provider?: string;\n url?: string;\n}\n\nfunction starterConfig(overrides?: InitOverrides): Config {\n const embed: Record<string, unknown> = { model: DEFAULT_EMBED_MODEL, provider: 'static' };\n if (overrides?.model !== undefined) embed.model = overrides.model;\n if (overrides?.provider !== undefined) embed.provider = overrides.provider;\n if (overrides?.url !== undefined) embed.url = overrides.url;\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n build: true,\n presets: {\n default: { include: ['**/*.md'], k: 10 },\n large: { include: ['**/*.md'], k: 20 },\n },\n // Written out rather than defaulted in code: this line is what makes `sense download`\n // fetch a model, so it belongs where it can be read and changed.\n embed: embed as Config['embed'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config. `overrides` come from `sense init`'s flags; an\n// invalid --provider fails validateConfig's own shape check before anything is written.\nexport function initConfig(dir: string, overrides?: InitOverrides): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const cfg = starterConfig(overrides);\n validateConfig(cfg, configPath);\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** Controls whether loading an older config may persist its in-memory migration. */\nexport interface LoadConfigOptions {\n /** Write a successful migration back to the config file. Defaults to true. */\n writeMigration?: boolean;\n}\n\nexport function loadConfig(explicitPath?: string, options: LoadConfigOptions = {}): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Ahead of both the version gate and shape checks: a non-object file (or array -- typeof []\n // is also 'object') is malformed at every version, so it gets one error regardless of branch.\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n // validateConfig only knows the current shape, so scan-shaped files (pre-v3) get a minimal\n // check, migrate, then the full one on the result.\n let cfg: Config;\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n if (version < FIRST_PRESET_VERSION) validateLegacyScan(parsed, configPath);\n const result = migrateConfig(parsed as unknown as Config);\n cfg = validateConfig(result.cfg, configPath);\n migratedFrom = result.from;\n if (options.writeMigration !== false) {\n const migrated = `${JSON.stringify(cfg, null, 2)}\\n`;\n try {\n writeFileAtomic(configPath, migrated);\n } catch (err) {\n // Only a rename-contention error might already be published by a racing writer.\n // Any other failure, including a failed initial write, rethrows immediately.\n const errno = err as NodeJS.ErrnoException;\n if (!(err instanceof Error) || errno.syscall !== 'rename' || !['EPERM', 'EACCES', 'EBUSY'].includes(errno.code ?? '')) throw err;\n let existing: string;\n try {\n existing = readFileSync(configPath, 'utf8');\n } catch {\n throw err;\n }\n if (existing !== migrated) throw err;\n }\n }\n } else {\n cfg = validateConfig(parsed, configPath);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n const configDir = dirname(configPath);\n const rootDir = resolve(configDir, cfg.root ?? '.');\n return { ...cfg, configDir, rootDir, baseDir: rootDir, configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["existsSync","readFileSync","dirname","join","resolve","SenseError","writeFileAtomic","CONFIG_FILENAME","DEFAULT_EMBED_MODEL","SUPPORTED_CONFIG_VERSION","unknownConfigKeys","validateConfig","validateLegacyScan","FIRST_PRESET_VERSION","V2_OPT_OUT_NAMES","MIGRATIONS","cfg","version","features","Object","fromEntries","map","name","prevDefaults","scan","defaults","checks","queries","rest","scanInclude","include","nextQueries","value","entries","entry","search","find","k","undefined","where","semantic","defaultWhere","prevFeatures","embedWasOff","embed","embedProvider","_embed","restFeatures","defaultPreset","console","error","result","presets","default","keys","length","embedded","values","some","p","sql","_semantic","prev","model","type","url","key","prevEmbed","provider","linksOn","links","nextPresets","preset","signals","words","build","migrateConfig","from","current","v","step","starterConfig","overrides","$schema","large","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","options","process","cwd","found","raw","parsed","parse","Array","isArray","migratedFrom","writeMigration","migrated","err","errno","Error","syscall","includes","code","existing","unknownKeys","configDir","rootDir","root","baseDir"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config/load.ts"],"sourcesContent":["import { channel } from 'node:diagnostics_channel';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from '../errors.ts';\nimport { writeFileAtomic } from '../lib/atomic-write.ts';\nimport type { Config, ResolvedConfig } from './types.ts';\nimport { CONFIG_FILENAME, DEFAULT_EMBED_MODEL, SUPPORTED_CONFIG_VERSION } from './types.ts';\nimport { unknownConfigKeys, validateConfig, validateLegacyScan } from './validate.ts';\n\n// v1 and v2 were `scan`/`find`-shaped; v3 introduced `presets`. Only the former need the\n// legacy shape check before migrating.\nconst FIRST_PRESET_VERSION = 3;\nconst migrationReady = channel('sensemaking.config.migration-ready');\n\n// v1 -> v2 migration only: the features v2 introduced as opt-out (embed was opt-in then).\nconst V2_OPT_OUT_NAMES = ['links', 'sections', 'rank'] as const;\n\n// Pure per-version steps, chained by loadConfig. Intermediate shapes predate the Config type,\n// so steps are loosely typed and only the final result is cast back.\nconst MIGRATIONS: Record<number, (cfg: Record<string, unknown>) => Record<string, unknown>> = {\n // v1 -> v2: features block introduced, opt-out features enabled (matches the old implicit\n // behavior of `links` etc. not existing). embed stays absent -- opt-in.\n 1: (cfg) => ({ ...cfg, version: 2, features: Object.fromEntries(V2_OPT_OUT_NAMES.map((name) => [name, true])) }),\n // v2 -> v3: mechanical-minimal by decision -- only what keeps an existing config loading (no\n // restructuring, no preset inference, no query rewriting); real trees get hand-migrated to use presets.\n 2: (cfg) => {\n const { scan, defaults, features, checks, queries, ...rest } = cfg;\n const scanInclude = (scan as { include: string[] }).include;\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((queries as Record<string, unknown> | undefined) ?? {})) {\n if (typeof value === 'string') {\n nextQueries[name] = value;\n continue;\n }\n const entry = value as { find: string; k?: number; where?: string; semantic?: boolean };\n // Loosely typed on purpose: this step emits the v3 shape, which still had `semantic`.\n // The v3 -> v4 step below strips it.\n const search: Record<string, unknown> = { search: entry.find };\n if (entry.k !== undefined) search.k = entry.k;\n if (entry.where !== undefined) search.where = entry.where;\n // semantic: true was opt-in; it is now the default, so it drops. semantic: false is kept.\n if (entry.semantic === false) search.semantic = false;\n nextQueries[name] = search;\n }\n\n const prevDefaults = defaults as { find?: { where?: string } } | undefined;\n const defaultWhere = prevDefaults?.find?.where;\n\n // features.embed false becomes semantic:false on every preset produced here.\n const prevFeatures = (features as Record<string, unknown> | undefined) ?? {};\n const embedWasOff = prevFeatures.embed === false;\n // Object form carries provider settings (model/type/url/key), not a toggle -- they move to\n // the v3 top-level `embed` block verbatim; dropping them would silently switch an api tree to the static model.\n const embedProvider = typeof prevFeatures.embed === 'object' && prevFeatures.embed !== null ? prevFeatures.embed : undefined;\n const { embed: _embed, ...restFeatures } = prevFeatures;\n\n const defaultPreset: Record<string, unknown> = { include: scanInclude };\n if (defaultWhere !== undefined) defaultPreset.where = defaultWhere;\n if (embedWasOff) defaultPreset.semantic = false;\n\n if (checks !== undefined) {\n console.error('sense: v2 \"checks\" was removed in v3 (sense check no longer asserts on saved queries); its queries are carried over under \"queries\" -- a returned row set is now the reader\\'s judgment');\n }\n\n const result: Record<string, unknown> = { ...rest, version: 3, presets: { default: defaultPreset }, queries: nextQueries };\n if (embedProvider !== undefined) result.embed = embedProvider;\n if (Object.keys(restFeatures).length > 0) result.features = restFeatures;\n return result;\n },\n // v3 -> v4: write the model into the file, wrap bare-string queries as { sql }, drop a saved\n // search's `semantic` (its preset decides). Same effective model, so nothing re-embeds.\n 3: (cfg) => {\n const presets = cfg.presets as Record<string, Record<string, unknown>>;\n const embedded = Object.values(presets).some((p) => p.semantic !== false);\n\n const nextQueries: Record<string, unknown> = {};\n for (const [name, value] of Object.entries((cfg.queries as Record<string, unknown> | undefined) ?? {})) {\n // v3's bare string meant SQL by inference; v4 makes every entry name its verb.\n if (typeof value === 'string') {\n nextQueries[name] = { sql: value };\n continue;\n }\n if (typeof value !== 'object' || value === null || !('search' in value)) {\n nextQueries[name] = value;\n continue;\n }\n const { semantic: _semantic, ...rest } = value as Record<string, unknown>;\n nextQueries[name] = rest;\n }\n\n const prev = (cfg.embed as Record<string, unknown> | undefined) ?? {};\n const result: Record<string, unknown> = { ...cfg, version: 4, queries: nextQueries };\n if (embedded) {\n result.embed = { model: (prev.model as string | undefined) ?? DEFAULT_EMBED_MODEL, type: (prev.type as string | undefined) ?? 'static', ...(prev.url !== undefined ? { url: prev.url } : {}), ...(prev.key !== undefined ? { key: prev.key } : {}) };\n } else {\n delete result.embed;\n // Dropping settings silently would look like a bug in a tree that configured an api endpoint\n // then turned vectors off; say it, since v4 has no way to carry provider settings for a tree that doesn't embed.\n if (Object.keys(prev).length > 0) console.error('sense: every preset has \"semantic\": false, so v4 drops the \"embed\" block (vectors stay off); re-add it, or set a preset\\'s semantic back to true, to turn them on');\n }\n return result;\n },\n // v4 -> v5: embed.type renames to embed.provider (\"api\" migrates to \"openai\", the wire protocol\n // it always was; other values pass through). A preset's `semantic` also migrates to `signals` (false -> explicit weights, true/absent -> no key).\n 4: (cfg) => {\n const prevEmbed = cfg.embed as Record<string, unknown> | undefined;\n const result: Record<string, unknown> = { ...cfg, version: 5 };\n if (prevEmbed) {\n const { type, ...rest } = prevEmbed;\n result.embed = type === undefined ? rest : { ...rest, provider: type === 'api' ? 'openai' : type };\n }\n\n const linksOn = (cfg.features as Record<string, unknown> | undefined)?.links !== false;\n const presets = cfg.presets as Record<string, Record<string, unknown>> | undefined;\n if (presets) {\n const nextPresets: Record<string, unknown> = {};\n for (const [name, preset] of Object.entries(presets)) {\n const { semantic, ...rest } = preset;\n if (semantic === undefined) {\n nextPresets[name] = preset;\n continue;\n }\n if (typeof semantic !== 'boolean') {\n throw new SenseError('CONFIG_INVALID', `presets.${name}.semantic must be a boolean`);\n }\n nextPresets[name] = semantic === false ? { ...rest, signals: linksOn ? { words: 1, links: 1 } : { words: 1 } } : rest;\n }\n result.presets = nextPresets;\n }\n return result;\n },\n // v5 -> v6: query-triggered builds are on by default. Preserve an explicit opt-out.\n 5: (cfg) => ({ ...cfg, version: 6, build: cfg.build === undefined ? true : cfg.build }),\n};\n\nexport function migrateConfig(cfg: Config): { cfg: Config; from: number } {\n const from = cfg.version ?? 1;\n let current: Record<string, unknown> = cfg as unknown as Record<string, unknown>;\n for (let v = from; v < SUPPORTED_CONFIG_VERSION; v++) {\n const step = MIGRATIONS[v];\n if (!step) throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `no migration from config version ${v}`);\n current = step(current);\n }\n return { cfg: current as unknown as Config, from };\n}\n\n// `sense init`'s flags map onto these three `embed` keys one-to-one; an absent field\n// keeps the built-in default rather than merging partial input.\nexport interface InitOverrides {\n model?: string;\n provider?: string;\n url?: string;\n}\n\nfunction starterConfig(overrides?: InitOverrides): Config {\n const embed: Record<string, unknown> = { model: DEFAULT_EMBED_MODEL, provider: 'static' };\n if (overrides?.model !== undefined) embed.model = overrides.model;\n if (overrides?.provider !== undefined) embed.provider = overrides.provider;\n if (overrides?.url !== undefined) embed.url = overrides.url;\n return {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: SUPPORTED_CONFIG_VERSION,\n build: true,\n presets: {\n default: { include: ['**/*.md'], k: 10 },\n large: { include: ['**/*.md'], k: 20 },\n },\n // Written out rather than defaulted in code: this line is what makes `sense download`\n // fetch a model, so it belongs where it can be read and changed.\n embed: embed as Config['embed'],\n queries: {},\n };\n}\n\n// Refuses to overwrite an existing config. `overrides` come from `sense init`'s flags; an\n// invalid --provider fails validateConfig's own shape check before anything is written.\nexport function initConfig(dir: string, overrides?: InitOverrides): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const cfg = starterConfig(overrides);\n validateConfig(cfg, configPath);\n writeFileAtomic(configPath, `${JSON.stringify(cfg, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** Controls whether loading an older config may persist its in-memory migration. */\nexport interface LoadConfigOptions {\n /** Write a successful migration back to the config file. Defaults to true. */\n writeMigration?: boolean;\n}\n\nexport function loadConfig(explicitPath?: string, options: LoadConfigOptions = {}): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const parsed: unknown = JSON.parse(raw);\n\n // Ahead of both the version gate and shape checks: a non-object file (or array -- typeof []\n // is also 'object') is malformed at every version, so it gets one error regardless of branch.\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new SenseError('CONFIG_INVALID', `${configPath}: config must be a JSON object`);\n }\n\n // Version gate before shape validation: a config written by a newer sense should fail\n // with \"requires a newer sense\", not with shape errors its own version may not have.\n const version = typeof parsed === 'object' && parsed !== null && typeof (parsed as { version?: unknown }).version === 'number' ? (parsed as { version: number }).version : 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n // validateConfig only knows the current shape, so scan-shaped files (pre-v3) get a minimal\n // check, migrate, then the full one on the result.\n let cfg: Config;\n let migratedFrom: number | undefined;\n if (version < SUPPORTED_CONFIG_VERSION) {\n if (version < FIRST_PRESET_VERSION) validateLegacyScan(parsed, configPath);\n const result = migrateConfig(parsed as unknown as Config);\n cfg = validateConfig(result.cfg, configPath);\n migratedFrom = result.from;\n if (options.writeMigration !== false) {\n const migrated = `${JSON.stringify(cfg, null, 2)}\\n`;\n if (migrationReady.hasSubscribers) migrationReady.publish({ configPath, from: migratedFrom });\n try {\n writeFileAtomic(configPath, migrated);\n } catch (err) {\n // Only a rename-contention error might already be published by a racing writer.\n // Any other failure, including a failed initial write, rethrows immediately.\n const errno = err as NodeJS.ErrnoException;\n if (!(err instanceof Error) || errno.syscall !== 'rename' || !['EPERM', 'EACCES', 'EBUSY'].includes(errno.code ?? '')) throw err;\n let existing: string;\n try {\n existing = readFileSync(configPath, 'utf8');\n } catch {\n throw err;\n }\n if (existing !== migrated) throw err;\n }\n }\n } else {\n cfg = validateConfig(parsed, configPath);\n }\n\n const unknownKeys = unknownConfigKeys(cfg as unknown as Record<string, unknown>);\n const configDir = dirname(configPath);\n const rootDir = resolve(configDir, cfg.root ?? '.');\n return { ...cfg, configDir, rootDir, baseDir: rootDir, configPath, migratedFrom, unknownKeys: unknownKeys.length > 0 ? unknownKeys : undefined };\n}\n"],"names":["channel","existsSync","readFileSync","dirname","join","resolve","SenseError","writeFileAtomic","CONFIG_FILENAME","DEFAULT_EMBED_MODEL","SUPPORTED_CONFIG_VERSION","unknownConfigKeys","validateConfig","validateLegacyScan","FIRST_PRESET_VERSION","migrationReady","V2_OPT_OUT_NAMES","MIGRATIONS","cfg","version","features","Object","fromEntries","map","name","prevDefaults","scan","defaults","checks","queries","rest","scanInclude","include","nextQueries","value","entries","entry","search","find","k","undefined","where","semantic","defaultWhere","prevFeatures","embedWasOff","embed","embedProvider","_embed","restFeatures","defaultPreset","console","error","result","presets","default","keys","length","embedded","values","some","p","sql","_semantic","prev","model","type","url","key","prevEmbed","provider","linksOn","links","nextPresets","preset","signals","words","build","migrateConfig","from","current","v","step","starterConfig","overrides","$schema","large","initConfig","dir","configPath","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","options","process","cwd","found","raw","parsed","parse","Array","isArray","migratedFrom","writeMigration","migrated","hasSubscribers","publish","err","errno","Error","syscall","includes","code","existing","unknownKeys","configDir","rootDir","root","baseDir"],"mappings":"AAAA,SAASA,OAAO,QAAQ,2BAA2B;AACnD,SAASC,UAAU,EAAEC,YAAY,QAAQ,UAAU;AACnD,SAASC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,YAAY;AACnD,SAASC,UAAU,QAAQ,eAAe;AAC1C,SAASC,eAAe,QAAQ,yBAAyB;AAEzD,SAASC,eAAe,EAAEC,mBAAmB,EAAEC,wBAAwB,QAAQ,aAAa;AAC5F,SAASC,iBAAiB,EAAEC,cAAc,EAAEC,kBAAkB,QAAQ,gBAAgB;AAEtF,yFAAyF;AACzF,uCAAuC;AACvC,MAAMC,uBAAuB;AAC7B,MAAMC,iBAAiBf,QAAQ;AAE/B,0FAA0F;AAC1F,MAAMgB,mBAAmB;IAAC;IAAS;IAAY;CAAO;AAEtD,8FAA8F;AAC9F,qEAAqE;AACrE,MAAMC,aAAwF;IAC5F,0FAA0F;IAC1F,wEAAwE;IACxE,GAAG,CAACC,MAAS,CAAA;YAAE,GAAGA,GAAG;YAAEC,SAAS;YAAGC,UAAUC,OAAOC,WAAW,CAACN,iBAAiBO,GAAG,CAAC,CAACC,OAAS;oBAACA;oBAAM;iBAAK;QAAG,CAAA;IAC9G,6FAA6F;IAC7F,wGAAwG;IACxG,GAAG,CAACN;YAsBmBO;QArBrB,MAAM,EAAEC,IAAI,EAAEC,QAAQ,EAAEP,QAAQ,EAAEQ,MAAM,EAAEC,OAAO,EAAE,GAAGC,MAAM,GAAGZ;QAC/D,MAAMa,cAAc,AAACL,KAA+BM,OAAO;QAE3D,MAAMC,cAAuC,CAAC;QAC9C,KAAK,MAAM,CAACT,MAAMU,MAAM,IAAIb,OAAOc,OAAO,CAAEN,oBAAAA,qBAAAA,UAAmD,CAAC,GAAI;YAClG,IAAI,OAAOK,UAAU,UAAU;gBAC7BD,WAAW,CAACT,KAAK,GAAGU;gBACpB;YACF;YACA,MAAME,QAAQF;YACd,sFAAsF;YACtF,qCAAqC;YACrC,MAAMG,SAAkC;gBAAEA,QAAQD,MAAME,IAAI;YAAC;YAC7D,IAAIF,MAAMG,CAAC,KAAKC,WAAWH,OAAOE,CAAC,GAAGH,MAAMG,CAAC;YAC7C,IAAIH,MAAMK,KAAK,KAAKD,WAAWH,OAAOI,KAAK,GAAGL,MAAMK,KAAK;YACzD,0FAA0F;YAC1F,IAAIL,MAAMM,QAAQ,KAAK,OAAOL,OAAOK,QAAQ,GAAG;YAChDT,WAAW,CAACT,KAAK,GAAGa;QACtB;QAEA,MAAMZ,eAAeE;QACrB,MAAMgB,eAAelB,yBAAAA,oCAAAA,qBAAAA,aAAca,IAAI,cAAlBb,yCAAAA,mBAAoBgB,KAAK;QAE9C,6EAA6E;QAC7E,MAAMG,eAAgBxB,qBAAAA,sBAAAA,WAAoD,CAAC;QAC3E,MAAMyB,cAAcD,aAAaE,KAAK,KAAK;QAC3C,2FAA2F;QAC3F,gHAAgH;QAChH,MAAMC,gBAAgB,OAAOH,aAAaE,KAAK,KAAK,YAAYF,aAAaE,KAAK,KAAK,OAAOF,aAAaE,KAAK,GAAGN;QACnH,MAAM,EAAEM,OAAOE,MAAM,EAAE,GAAGC,cAAc,GAAGL;QAE3C,MAAMM,gBAAyC;YAAElB,SAASD;QAAY;QACtE,IAAIY,iBAAiBH,WAAWU,cAAcT,KAAK,GAAGE;QACtD,IAAIE,aAAaK,cAAcR,QAAQ,GAAG;QAE1C,IAAId,WAAWY,WAAW;YACxBW,QAAQC,KAAK,CAAC;QAChB;QAEA,MAAMC,SAAkC;YAAE,GAAGvB,IAAI;YAAEX,SAAS;YAAGmC,SAAS;gBAAEC,SAASL;YAAc;YAAGrB,SAASI;QAAY;QACzH,IAAIc,kBAAkBP,WAAWa,OAAOP,KAAK,GAAGC;QAChD,IAAI1B,OAAOmC,IAAI,CAACP,cAAcQ,MAAM,GAAG,GAAGJ,OAAOjC,QAAQ,GAAG6B;QAC5D,OAAOI;IACT;IACA,6FAA6F;IAC7F,wFAAwF;IACxF,GAAG,CAACnC;YAK0CA,cAc9BA;QAlBd,MAAMoC,UAAUpC,IAAIoC,OAAO;QAC3B,MAAMI,WAAWrC,OAAOsC,MAAM,CAACL,SAASM,IAAI,CAAC,CAACC,IAAMA,EAAEnB,QAAQ,KAAK;QAEnE,MAAMT,cAAuC,CAAC;QAC9C,KAAK,MAAM,CAACT,MAAMU,MAAM,IAAIb,OAAOc,OAAO,EAAEjB,eAAAA,IAAIW,OAAO,cAAXX,0BAAAA,eAAuD,CAAC,GAAI;YACtG,+EAA+E;YAC/E,IAAI,OAAOgB,UAAU,UAAU;gBAC7BD,WAAW,CAACT,KAAK,GAAG;oBAAEsC,KAAK5B;gBAAM;gBACjC;YACF;YACA,IAAI,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAAE,CAAA,YAAYA,KAAI,GAAI;gBACvED,WAAW,CAACT,KAAK,GAAGU;gBACpB;YACF;YACA,MAAM,EAAEQ,UAAUqB,SAAS,EAAE,GAAGjC,MAAM,GAAGI;YACzCD,WAAW,CAACT,KAAK,GAAGM;QACtB;QAEA,MAAMkC,QAAQ9C,aAAAA,IAAI4B,KAAK,cAAT5B,wBAAAA,aAAqD,CAAC;QACpE,MAAMmC,SAAkC;YAAE,GAAGnC,GAAG;YAAEC,SAAS;YAAGU,SAASI;QAAY;QACnF,IAAIyB,UAAU;gBACaM,aAAiEA;YAA1FX,OAAOP,KAAK,GAAG;gBAAEmB,KAAK,GAAGD,cAAAA,KAAKC,KAAK,cAAVD,yBAAAA,cAAqCvD;gBAAqByD,IAAI,GAAGF,aAAAA,KAAKE,IAAI,cAATF,wBAAAA,aAAoC;gBAAU,GAAIA,KAAKG,GAAG,KAAK3B,YAAY;oBAAE2B,KAAKH,KAAKG,GAAG;gBAAC,IAAI,CAAC,CAAC;gBAAG,GAAIH,KAAKI,GAAG,KAAK5B,YAAY;oBAAE4B,KAAKJ,KAAKI,GAAG;gBAAC,IAAI,CAAC,CAAC;YAAE;QACrP,OAAO;YACL,OAAOf,OAAOP,KAAK;YACnB,6FAA6F;YAC7F,iHAAiH;YACjH,IAAIzB,OAAOmC,IAAI,CAACQ,MAAMP,MAAM,GAAG,GAAGN,QAAQC,KAAK,CAAC;QAClD;QACA,OAAOC;IACT;IACA,gGAAgG;IAChG,kJAAkJ;IAClJ,GAAG,CAACnC;YAQeA;QAPjB,MAAMmD,YAAYnD,IAAI4B,KAAK;QAC3B,MAAMO,SAAkC;YAAE,GAAGnC,GAAG;YAAEC,SAAS;QAAE;QAC7D,IAAIkD,WAAW;YACb,MAAM,EAAEH,IAAI,EAAE,GAAGpC,MAAM,GAAGuC;YAC1BhB,OAAOP,KAAK,GAAGoB,SAAS1B,YAAYV,OAAO;gBAAE,GAAGA,IAAI;gBAAEwC,UAAUJ,SAAS,QAAQ,WAAWA;YAAK;QACnG;QAEA,MAAMK,UAAU,EAACrD,gBAAAA,IAAIE,QAAQ,cAAZF,oCAAD,AAACA,cAAsDsD,KAAK,MAAK;QACjF,MAAMlB,UAAUpC,IAAIoC,OAAO;QAC3B,IAAIA,SAAS;YACX,MAAMmB,cAAuC,CAAC;YAC9C,KAAK,MAAM,CAACjD,MAAMkD,OAAO,IAAIrD,OAAOc,OAAO,CAACmB,SAAU;gBACpD,MAAM,EAAEZ,QAAQ,EAAE,GAAGZ,MAAM,GAAG4C;gBAC9B,IAAIhC,aAAaF,WAAW;oBAC1BiC,WAAW,CAACjD,KAAK,GAAGkD;oBACpB;gBACF;gBACA,IAAI,OAAOhC,aAAa,WAAW;oBACjC,MAAM,IAAIpC,WAAW,kBAAkB,CAAC,QAAQ,EAAEkB,KAAK,2BAA2B,CAAC;gBACrF;gBACAiD,WAAW,CAACjD,KAAK,GAAGkB,aAAa,QAAQ;oBAAE,GAAGZ,IAAI;oBAAE6C,SAASJ,UAAU;wBAAEK,OAAO;wBAAGJ,OAAO;oBAAE,IAAI;wBAAEI,OAAO;oBAAE;gBAAE,IAAI9C;YACnH;YACAuB,OAAOC,OAAO,GAAGmB;QACnB;QACA,OAAOpB;IACT;IACA,oFAAoF;IACpF,GAAG,CAACnC,MAAS,CAAA;YAAE,GAAGA,GAAG;YAAEC,SAAS;YAAG0D,OAAO3D,IAAI2D,KAAK,KAAKrC,YAAY,OAAOtB,IAAI2D,KAAK;QAAC,CAAA;AACvF;AAEA,OAAO,SAASC,cAAc5D,GAAW;QAC1BA;IAAb,MAAM6D,QAAO7D,eAAAA,IAAIC,OAAO,cAAXD,0BAAAA,eAAe;IAC5B,IAAI8D,UAAmC9D;IACvC,IAAK,IAAI+D,IAAIF,MAAME,IAAIvE,0BAA0BuE,IAAK;QACpD,MAAMC,OAAOjE,UAAU,CAACgE,EAAE;QAC1B,IAAI,CAACC,MAAM,MAAM,IAAI5E,WAAW,8BAA8B,CAAC,iCAAiC,EAAE2E,GAAG;QACrGD,UAAUE,KAAKF;IACjB;IACA,OAAO;QAAE9D,KAAK8D;QAA8BD;IAAK;AACnD;AAUA,SAASI,cAAcC,SAAyB;IAC9C,MAAMtC,QAAiC;QAAEmB,OAAOxD;QAAqB6D,UAAU;IAAS;IACxF,IAAIc,CAAAA,sBAAAA,gCAAAA,UAAWnB,KAAK,MAAKzB,WAAWM,MAAMmB,KAAK,GAAGmB,UAAUnB,KAAK;IACjE,IAAImB,CAAAA,sBAAAA,gCAAAA,UAAWd,QAAQ,MAAK9B,WAAWM,MAAMwB,QAAQ,GAAGc,UAAUd,QAAQ;IAC1E,IAAIc,CAAAA,sBAAAA,gCAAAA,UAAWjB,GAAG,MAAK3B,WAAWM,MAAMqB,GAAG,GAAGiB,UAAUjB,GAAG;IAC3D,OAAO;QACLkB,SAAS;QACTlE,SAAST;QACTmE,OAAO;QACPvB,SAAS;YACPC,SAAS;gBAAEvB,SAAS;oBAAC;iBAAU;gBAAEO,GAAG;YAAG;YACvC+C,OAAO;gBAAEtD,SAAS;oBAAC;iBAAU;gBAAEO,GAAG;YAAG;QACvC;QACA,sFAAsF;QACtF,iEAAiE;QACjEO,OAAOA;QACPjB,SAAS,CAAC;IACZ;AACF;AAEA,0FAA0F;AAC1F,wFAAwF;AACxF,OAAO,SAAS0D,WAAWC,GAAW,EAAEJ,SAAyB;IAC/D,MAAMK,aAAarF,KAAKoF,KAAKhF;IAC7B,IAAIP,WAAWwF,aAAa;QAC1B,MAAM,IAAInF,WAAW,iBAAiB,GAAGE,gBAAgB,mBAAmB,EAAEgF,KAAK;IACrF;IACA,MAAMtE,MAAMiE,cAAcC;IAC1BxE,eAAeM,KAAKuE;IACpBlF,gBAAgBkF,YAAY,GAAGC,KAAKC,SAAS,CAACzE,KAAK,MAAM,GAAG,EAAE,CAAC;IAC/D,OAAOuE;AACT;AAEA,OAAO,SAASG,eAAeC,QAAgB;IAC7C,IAAIL,MAAMnF,QAAQwF;IAClB,OAAS;QACP,MAAMC,YAAY1F,KAAKoF,KAAKhF;QAC5B,IAAIP,WAAW6F,YAAY,OAAOA;QAClC,MAAMC,SAAS5F,QAAQqF;QACvB,IAAIO,WAAWP,KAAK,OAAO;QAC3BA,MAAMO;IACR;AACF;AAQA,OAAO,SAASC,WAAWC,YAAqB,EAAEC,UAA6B,CAAC,CAAC;QA+D5ChF;IA9DnC,IAAIuE;IACJ,IAAIQ,cAAc;QAChBR,aAAapF,QAAQ8F,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAAChG,WAAWwF,aAAa,MAAM,IAAInF,WAAW,oBAAoB,CAAC,kBAAkB,EAAEmF,YAAY;IACzG,OAAO;QACL,MAAMY,QAAQT,eAAeO,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAI/F,WAAW,oBAAoB,CAAC,eAAe,EAAEE,gBAAgB,IAAI,EAAE2F,QAAQC,GAAG,GAAG,wBAAwB,CAAC;QAC1H;QACAX,aAAaY;IACf;IAEA,MAAMC,MAAMpG,aAAauF,YAAY;IACrC,MAAMc,SAAkBb,KAAKc,KAAK,CAACF;IAEnC,4FAA4F;IAC5F,8FAA8F;IAC9F,IAAI,OAAOC,WAAW,YAAYA,WAAW,QAAQE,MAAMC,OAAO,CAACH,SAAS;QAC1E,MAAM,IAAIjG,WAAW,kBAAkB,GAAGmF,WAAW,8BAA8B,CAAC;IACtF;IAEA,sFAAsF;IACtF,qFAAqF;IACrF,MAAMtE,UAAU,OAAOoF,WAAW,YAAYA,WAAW,QAAQ,OAAO,AAACA,OAAiCpF,OAAO,KAAK,WAAW,AAACoF,OAA+BpF,OAAO,GAAG;IAC3K,IAAIA,UAAUT,0BAA0B;QACtC,MAAM,IAAIJ,WAAW,8BAA8B,CAAC,eAAe,EAAEa,QAAQ,uBAAuB,CAAC;IACvG;IAEA,2FAA2F;IAC3F,mDAAmD;IACnD,IAAID;IACJ,IAAIyF;IACJ,IAAIxF,UAAUT,0BAA0B;QACtC,IAAIS,UAAUL,sBAAsBD,mBAAmB0F,QAAQd;QAC/D,MAAMpC,SAASyB,cAAcyB;QAC7BrF,MAAMN,eAAeyC,OAAOnC,GAAG,EAAEuE;QACjCkB,eAAetD,OAAO0B,IAAI;QAC1B,IAAImB,QAAQU,cAAc,KAAK,OAAO;YACpC,MAAMC,WAAW,GAAGnB,KAAKC,SAAS,CAACzE,KAAK,MAAM,GAAG,EAAE,CAAC;YACpD,IAAIH,eAAe+F,cAAc,EAAE/F,eAAegG,OAAO,CAAC;gBAAEtB;gBAAYV,MAAM4B;YAAa;YAC3F,IAAI;gBACFpG,gBAAgBkF,YAAYoB;YAC9B,EAAE,OAAOG,KAAK;oBAIwFC;gBAHpG,gFAAgF;gBAChF,6EAA6E;gBAC7E,MAAMA,QAAQD;gBACd,IAAI,CAAEA,CAAAA,eAAeE,KAAI,KAAMD,MAAME,OAAO,KAAK,YAAY,CAAC;oBAAC;oBAAS;oBAAU;iBAAQ,CAACC,QAAQ,EAACH,cAAAA,MAAMI,IAAI,cAAVJ,yBAAAA,cAAc,KAAK,MAAMD;gBAC7H,IAAIM;gBACJ,IAAI;oBACFA,WAAWpH,aAAauF,YAAY;gBACtC,EAAE,OAAM;oBACN,MAAMuB;gBACR;gBACA,IAAIM,aAAaT,UAAU,MAAMG;YACnC;QACF;IACF,OAAO;QACL9F,MAAMN,eAAe2F,QAAQd;IAC/B;IAEA,MAAM8B,cAAc5G,kBAAkBO;IACtC,MAAMsG,YAAYrH,QAAQsF;IAC1B,MAAMgC,UAAUpH,QAAQmH,YAAWtG,YAAAA,IAAIwG,IAAI,cAARxG,uBAAAA,YAAY;IAC/C,OAAO;QAAE,GAAGA,GAAG;QAAEsG;QAAWC;QAASE,SAASF;QAAShC;QAAYkB;QAAcY,aAAaA,YAAY9D,MAAM,GAAG,IAAI8D,cAAc/E;IAAU;AACjJ"}
@@ -1,16 +1,37 @@
1
1
  import { SenseError } from '../errors.js';
2
- import { fetchWithRetry } from './http.js';
2
+ import { effectiveEmbedEndpoint } from './endpoint.js';
3
+ import { fetchWithRetry, HTTP_ATTEMPT_TIMEOUT_MS } from './http.js';
3
4
  const BATCH_CAP = 96;
5
+ function isNumberArray(value) {
6
+ return Array.isArray(value) && value.every((part)=>typeof part === 'number');
7
+ }
8
+ function isEmbeddingArray(value) {
9
+ return Array.isArray(value) && value.every(isNumberArray);
10
+ }
11
+ function parseBody(body, endpoint) {
12
+ let decoded;
13
+ try {
14
+ decoded = JSON.parse(new TextDecoder().decode(body));
15
+ } catch {
16
+ throw new SenseError('EMBED_MODEL', `${endpoint} returned invalid JSON`);
17
+ }
18
+ if (decoded === null || typeof decoded !== 'object' || !('embeddings' in decoded) || decoded.embeddings === null || typeof decoded.embeddings !== 'object' || !('float' in decoded.embeddings) || !isEmbeddingArray(decoded.embeddings.float)) {
19
+ throw new SenseError('EMBED_MODEL', `${endpoint} returned an invalid embedding response`);
20
+ }
21
+ return decoded.embeddings.float;
22
+ }
4
23
  // Cohere's native /v2/embed: input_type distinguishes doc vs query embeddings, which the
5
24
  // OpenAI-compatible shape cannot express.
6
25
  export async function cohereProvider(model, url, keyEnv) {
7
- const base = (url !== null && url !== void 0 ? url : 'https://api.cohere.com').replace(/\/+$/, '');
26
+ const base = effectiveEmbedEndpoint('cohere', url);
27
+ if (!base) throw new SenseError('EMBED_MODEL', 'cohere endpoint resolution failed');
8
28
  const headers = {
9
29
  'content-type': 'application/json'
10
30
  };
11
31
  const key = keyEnv ? process.env[keyEnv] : undefined;
12
32
  if (key) headers.authorization = `Bearer ${key}`;
13
- async function post(texts, inputType) {
33
+ async function post(texts, inputType, options) {
34
+ var _options_signal;
14
35
  const res = await fetchWithRetry(`${base}/v2/embed`, {
15
36
  method: 'POST',
16
37
  headers,
@@ -22,13 +43,17 @@ export async function cohereProvider(model, url, keyEnv) {
22
43
  'float'
23
44
  ]
24
45
  })
46
+ }, {
47
+ signal: options === null || options === void 0 ? void 0 : options.signal,
48
+ attemptTimeoutMs: HTTP_ATTEMPT_TIMEOUT_MS
25
49
  });
26
- if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/v2/embed -> HTTP ${res.status}`);
27
- const body = await res.json();
50
+ options === null || options === void 0 ? void 0 : (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
51
+ if (!res.ok) throw new SenseError('EMBED_MODEL', `${res.url} -> HTTP ${res.status}`);
52
+ const body = parseBody(res.body, res.url);
28
53
  // Cohere returns one vector per text in order, with no index field to re-sort by; a caller
29
54
  // maps them positionally, so the count below must be trustworthy.
30
- const vectors = body.embeddings.float;
31
- if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/v2/embed returned ${vectors.length} embeddings for ${texts.length} inputs`);
55
+ const vectors = body;
56
+ if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${res.url} returned ${vectors.length} embeddings for ${texts.length} inputs`);
32
57
  return vectors.map((v)=>Float32Array.from(v));
33
58
  }
34
59
  const dims = (await post([
@@ -38,9 +63,9 @@ export async function cohereProvider(model, url, keyEnv) {
38
63
  id: `cohere:${base}:${model}`,
39
64
  dims,
40
65
  batchCap: BATCH_CAP,
41
- embedDocuments: (texts)=>post(texts, 'search_document'),
42
- embedQuery: async (text)=>(await post([
66
+ embedDocuments: (texts, options)=>post(texts, 'search_document', options),
67
+ embedQuery: async (text, options)=>(await post([
43
68
  text
44
- ], 'search_query'))[0]
69
+ ], 'search_query', options))[0]
45
70
  };
46
71
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/cohere.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { fetchWithRetry } from './http.ts';\nimport type { EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 96;\n\n// Cohere's native /v2/embed: input_type distinguishes doc vs query embeddings, which the\n// OpenAI-compatible shape cannot express.\nexport async function cohereProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n const base = (url ?? 'https://api.cohere.com').replace(/\\/+$/, '');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[], inputType: 'search_document' | 'search_query'): Promise<Float32Array[]> {\n const res = await fetchWithRetry(`${base}/v2/embed`, { method: 'POST', headers, body: JSON.stringify({ model, input_type: inputType, texts, embedding_types: ['float'] }) });\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/v2/embed -> HTTP ${res.status}`);\n const body = (await res.json()) as { embeddings: { float: number[][] } };\n // Cohere returns one vector per text in order, with no index field to re-sort by; a caller\n // maps them positionally, so the count below must be trustworthy.\n const vectors = body.embeddings.float;\n if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${base}/v2/embed returned ${vectors.length} embeddings for ${texts.length} inputs`);\n return vectors.map((v) => Float32Array.from(v));\n }\n\n const dims = (await post(['dimension probe'], 'search_query'))[0].length;\n return { id: `cohere:${base}:${model}`, dims, batchCap: BATCH_CAP, embedDocuments: (texts) => post(texts, 'search_document'), embedQuery: async (text) => (await post([text], 'search_query'))[0] };\n}\n"],"names":["SenseError","fetchWithRetry","BATCH_CAP","cohereProvider","model","url","keyEnv","base","replace","headers","key","process","env","undefined","authorization","post","texts","inputType","res","method","body","JSON","stringify","input_type","embedding_types","ok","status","json","vectors","embeddings","float","length","map","v","Float32Array","from","dims","id","batchCap","embedDocuments","embedQuery","text"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAC1C,SAASC,cAAc,QAAQ,YAAY;AAG3C,MAAMC,YAAY;AAElB,yFAAyF;AACzF,0CAA0C;AAC1C,OAAO,eAAeC,eAAeC,KAAa,EAAEC,GAAuB,EAAEC,MAA0B;IACrG,MAAMC,OAAO,CAACF,gBAAAA,iBAAAA,MAAO,0BAA0BG,OAAO,CAAC,QAAQ;IAC/D,MAAMC,UAAkC;QAAE,gBAAgB;IAAmB;IAC7E,MAAMC,MAAMJ,SAASK,QAAQC,GAAG,CAACN,OAAO,GAAGO;IAC3C,IAAIH,KAAKD,QAAQK,aAAa,GAAG,CAAC,OAAO,EAAEJ,KAAK;IAEhD,eAAeK,KAAKC,KAAe,EAAEC,SAA6C;QAChF,MAAMC,MAAM,MAAMjB,eAAe,GAAGM,KAAK,SAAS,CAAC,EAAE;YAAEY,QAAQ;YAAQV;YAASW,MAAMC,KAAKC,SAAS,CAAC;gBAAElB;gBAAOmB,YAAYN;gBAAWD;gBAAOQ,iBAAiB;oBAAC;iBAAQ;YAAC;QAAG;QAC1K,IAAI,CAACN,IAAIO,EAAE,EAAE,MAAM,IAAIzB,WAAW,eAAe,GAAGO,KAAK,kBAAkB,EAAEW,IAAIQ,MAAM,EAAE;QACzF,MAAMN,OAAQ,MAAMF,IAAIS,IAAI;QAC5B,2FAA2F;QAC3F,kEAAkE;QAClE,MAAMC,UAAUR,KAAKS,UAAU,CAACC,KAAK;QACrC,IAAIF,QAAQG,MAAM,KAAKf,MAAMe,MAAM,EAAE,MAAM,IAAI/B,WAAW,eAAe,GAAGO,KAAK,mBAAmB,EAAEqB,QAAQG,MAAM,CAAC,gBAAgB,EAAEf,MAAMe,MAAM,CAAC,OAAO,CAAC;QAC5J,OAAOH,QAAQI,GAAG,CAAC,CAACC,IAAMC,aAAaC,IAAI,CAACF;IAC9C;IAEA,MAAMG,OAAO,AAAC,CAAA,MAAMrB,KAAK;QAAC;KAAkB,EAAE,eAAc,CAAE,CAAC,EAAE,CAACgB,MAAM;IACxE,OAAO;QAAEM,IAAI,CAAC,OAAO,EAAE9B,KAAK,CAAC,EAAEH,OAAO;QAAEgC;QAAME,UAAUpC;QAAWqC,gBAAgB,CAACvB,QAAUD,KAAKC,OAAO;QAAoBwB,YAAY,OAAOC,OAAS,AAAC,CAAA,MAAM1B,KAAK;gBAAC0B;aAAK,EAAE,eAAc,CAAE,CAAC,EAAE;IAAC;AACpM"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/cohere.ts"],"sourcesContent":["import { SenseError } from '../errors.ts';\nimport { effectiveEmbedEndpoint } from './endpoint.ts';\nimport { fetchWithRetry, HTTP_ATTEMPT_TIMEOUT_MS } from './http.ts';\nimport type { EmbedCallOptions, EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 96;\n\nfunction isNumberArray(value: unknown): value is number[] {\n return Array.isArray(value) && value.every((part) => typeof part === 'number');\n}\n\nfunction isEmbeddingArray(value: unknown): value is number[][] {\n return Array.isArray(value) && value.every(isNumberArray);\n}\n\nfunction parseBody(body: ArrayBuffer, endpoint: string): number[][] {\n let decoded: unknown;\n try {\n decoded = JSON.parse(new TextDecoder().decode(body));\n } catch {\n throw new SenseError('EMBED_MODEL', `${endpoint} returned invalid JSON`);\n }\n if (decoded === null || typeof decoded !== 'object' || !('embeddings' in decoded) || decoded.embeddings === null || typeof decoded.embeddings !== 'object' || !('float' in decoded.embeddings) || !isEmbeddingArray(decoded.embeddings.float)) {\n throw new SenseError('EMBED_MODEL', `${endpoint} returned an invalid embedding response`);\n }\n return decoded.embeddings.float;\n}\n\n// Cohere's native /v2/embed: input_type distinguishes doc vs query embeddings, which the\n// OpenAI-compatible shape cannot express.\nexport async function cohereProvider(model: string, url: string | undefined, keyEnv: string | undefined): Promise<EmbedProvider> {\n const base = effectiveEmbedEndpoint('cohere', url);\n if (!base) throw new SenseError('EMBED_MODEL', 'cohere endpoint resolution failed');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n const key = keyEnv ? process.env[keyEnv] : undefined;\n if (key) headers.authorization = `Bearer ${key}`;\n\n async function post(texts: string[], inputType: 'search_document' | 'search_query', options?: EmbedCallOptions): Promise<Float32Array[]> {\n const res = await fetchWithRetry(`${base}/v2/embed`, { method: 'POST', headers, body: JSON.stringify({ model, input_type: inputType, texts, embedding_types: ['float'] }) }, { signal: options?.signal, attemptTimeoutMs: HTTP_ATTEMPT_TIMEOUT_MS });\n options?.signal?.throwIfAborted();\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${res.url} -> HTTP ${res.status}`);\n const body = parseBody(res.body, res.url);\n // Cohere returns one vector per text in order, with no index field to re-sort by; a caller\n // maps them positionally, so the count below must be trustworthy.\n const vectors = body;\n if (vectors.length !== texts.length) throw new SenseError('EMBED_MODEL', `${res.url} returned ${vectors.length} embeddings for ${texts.length} inputs`);\n return vectors.map((v) => Float32Array.from(v));\n }\n\n const dims = (await post(['dimension probe'], 'search_query'))[0].length;\n return { id: `cohere:${base}:${model}`, dims, batchCap: BATCH_CAP, embedDocuments: (texts, options) => post(texts, 'search_document', options), embedQuery: async (text, options) => (await post([text], 'search_query', options))[0] };\n}\n"],"names":["SenseError","effectiveEmbedEndpoint","fetchWithRetry","HTTP_ATTEMPT_TIMEOUT_MS","BATCH_CAP","isNumberArray","value","Array","isArray","every","part","isEmbeddingArray","parseBody","body","endpoint","decoded","JSON","parse","TextDecoder","decode","embeddings","float","cohereProvider","model","url","keyEnv","base","headers","key","process","env","undefined","authorization","post","texts","inputType","options","res","method","stringify","input_type","embedding_types","signal","attemptTimeoutMs","throwIfAborted","ok","status","vectors","length","map","v","Float32Array","from","dims","id","batchCap","embedDocuments","embedQuery","text"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function effectiveEmbedEndpoint(provider: 'static' | 'openai' | 'cohere', url: string | undefined): string | undefined;
2
+ export declare function embedEndpointIdentity(provider: 'static' | 'openai' | 'cohere', url: string | undefined): string | undefined;
@@ -0,0 +1,11 @@
1
+ import { createHash } from 'node:crypto';
2
+ const DEFAULT_COHERE_ENDPOINT = 'https://api.cohere.com';
3
+ export function effectiveEmbedEndpoint(provider, url) {
4
+ if (provider === 'static') return undefined;
5
+ const endpoint = provider === 'cohere' ? url !== null && url !== void 0 ? url : DEFAULT_COHERE_ENDPOINT : url;
6
+ return endpoint === null || endpoint === void 0 ? void 0 : endpoint.replace(/\/+$/, '');
7
+ }
8
+ export function embedEndpointIdentity(provider, url) {
9
+ const endpoint = effectiveEmbedEndpoint(provider, url);
10
+ return endpoint === undefined ? undefined : createHash('sha256').update(endpoint).digest('hex');
11
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/endpoint.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nconst DEFAULT_COHERE_ENDPOINT = 'https://api.cohere.com';\n\nexport function effectiveEmbedEndpoint(provider: 'static' | 'openai' | 'cohere', url: string | undefined): string | undefined {\n if (provider === 'static') return undefined;\n const endpoint = provider === 'cohere' ? (url ?? DEFAULT_COHERE_ENDPOINT) : url;\n return endpoint?.replace(/\\/+$/, '');\n}\n\nexport function embedEndpointIdentity(provider: 'static' | 'openai' | 'cohere', url: string | undefined): string | undefined {\n const endpoint = effectiveEmbedEndpoint(provider, url);\n return endpoint === undefined ? undefined : createHash('sha256').update(endpoint).digest('hex');\n}\n"],"names":["createHash","DEFAULT_COHERE_ENDPOINT","effectiveEmbedEndpoint","provider","url","undefined","endpoint","replace","embedEndpointIdentity","update","digest"],"mappings":"AAAA,SAASA,UAAU,QAAQ,cAAc;AAEzC,MAAMC,0BAA0B;AAEhC,OAAO,SAASC,uBAAuBC,QAAwC,EAAEC,GAAuB;IACtG,IAAID,aAAa,UAAU,OAAOE;IAClC,MAAMC,WAAWH,aAAa,WAAYC,gBAAAA,iBAAAA,MAAOH,0BAA2BG;IAC5E,OAAOE,qBAAAA,+BAAAA,SAAUC,OAAO,CAAC,QAAQ;AACnC;AAEA,OAAO,SAASC,sBAAsBL,QAAwC,EAAEC,GAAuB;IACrG,MAAME,WAAWJ,uBAAuBC,UAAUC;IAClD,OAAOE,aAAaD,YAAYA,YAAYL,WAAW,UAAUS,MAAM,CAACH,UAAUI,MAAM,CAAC;AAC3F"}
@@ -1,3 +1,8 @@
1
- export declare function stashChunkText(key: object, texts: Map<string, string[]>): void;
1
+ export interface ChunkText {
2
+ text: string;
3
+ identity: string;
4
+ }
5
+ export declare function identifyChunkText(text: string): ChunkText;
6
+ export declare function stashChunkText(key: object, texts: Map<string, ChunkText[]>): void;
2
7
  export declare function rekeyChunkText(from: object, to: object): void;
3
- export declare function takeChunkText(key: object): Map<string, string[]> | undefined;
8
+ export declare function takeChunkText(key: object): Map<string, ChunkText[]> | undefined;
@@ -1,3 +1,10 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function identifyChunkText(text) {
3
+ return {
4
+ text,
5
+ identity: createHash('sha256').update(text).digest('hex')
6
+ };
7
+ }
1
8
  // Chunk text from reconcile to embed time when both run in one process, so the text is neither
2
9
  // re-derived nor persisted. Keyed by the store it was stashed against, so an entry dies with it.
3
10
  const HANDOFF = new WeakMap();
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/handoff.ts"],"sourcesContent":["// Chunk text from reconcile to embed time when both run in one process, so the text is neither\n// re-derived nor persisted. Keyed by the store it was stashed against, so an entry dies with it.\nconst HANDOFF = new WeakMap<object, Map<string, string[]>>();\n\nexport function stashChunkText(key: object, texts: Map<string, string[]>): void {\n if (texts.size > 0) HANDOFF.set(key, texts);\n}\n\n// Moves an entry from one key to another: reconcile runs before the store object exists, so it\n// stashes against the connection and open() re-keys onto the store it then builds.\nexport function rekeyChunkText(from: object, to: object): void {\n const texts = HANDOFF.get(from);\n if (!texts) return;\n HANDOFF.delete(from);\n HANDOFF.set(to, texts);\n}\n\n// Read once and drop: the text is consumed by the embed pass that follows reconcile, and holding\n// it past that would pin the tree's prose in memory for the life of the store.\nexport function takeChunkText(key: object): Map<string, string[]> | undefined {\n const texts = HANDOFF.get(key);\n HANDOFF.delete(key);\n return texts;\n}\n"],"names":["HANDOFF","WeakMap","stashChunkText","key","texts","size","set","rekeyChunkText","from","to","get","delete","takeChunkText"],"mappings":"AAAA,+FAA+F;AAC/F,iGAAiG;AACjG,MAAMA,UAAU,IAAIC;AAEpB,OAAO,SAASC,eAAeC,GAAW,EAAEC,KAA4B;IACtE,IAAIA,MAAMC,IAAI,GAAG,GAAGL,QAAQM,GAAG,CAACH,KAAKC;AACvC;AAEA,+FAA+F;AAC/F,mFAAmF;AACnF,OAAO,SAASG,eAAeC,IAAY,EAAEC,EAAU;IACrD,MAAML,QAAQJ,QAAQU,GAAG,CAACF;IAC1B,IAAI,CAACJ,OAAO;IACZJ,QAAQW,MAAM,CAACH;IACfR,QAAQM,GAAG,CAACG,IAAIL;AAClB;AAEA,iGAAiG;AACjG,+EAA+E;AAC/E,OAAO,SAASQ,cAAcT,GAAW;IACvC,MAAMC,QAAQJ,QAAQU,GAAG,CAACP;IAC1BH,QAAQW,MAAM,CAACR;IACf,OAAOC;AACT"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/handoff.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nexport interface ChunkText {\n text: string;\n identity: string;\n}\n\nexport function identifyChunkText(text: string): ChunkText {\n return { text, identity: createHash('sha256').update(text).digest('hex') };\n}\n\n// Chunk text from reconcile to embed time when both run in one process, so the text is neither\n// re-derived nor persisted. Keyed by the store it was stashed against, so an entry dies with it.\nconst HANDOFF = new WeakMap<object, Map<string, ChunkText[]>>();\n\nexport function stashChunkText(key: object, texts: Map<string, ChunkText[]>): void {\n if (texts.size > 0) HANDOFF.set(key, texts);\n}\n\n// Moves an entry from one key to another: reconcile runs before the store object exists, so it\n// stashes against the connection and open() re-keys onto the store it then builds.\nexport function rekeyChunkText(from: object, to: object): void {\n const texts = HANDOFF.get(from);\n if (!texts) return;\n HANDOFF.delete(from);\n HANDOFF.set(to, texts);\n}\n\n// Read once and drop: the text is consumed by the embed pass that follows reconcile, and holding\n// it past that would pin the tree's prose in memory for the life of the store.\nexport function takeChunkText(key: object): Map<string, ChunkText[]> | undefined {\n const texts = HANDOFF.get(key);\n HANDOFF.delete(key);\n return texts;\n}\n"],"names":["createHash","identifyChunkText","text","identity","update","digest","HANDOFF","WeakMap","stashChunkText","key","texts","size","set","rekeyChunkText","from","to","get","delete","takeChunkText"],"mappings":"AAAA,SAASA,UAAU,QAAQ,cAAc;AAOzC,OAAO,SAASC,kBAAkBC,IAAY;IAC5C,OAAO;QAAEA;QAAMC,UAAUH,WAAW,UAAUI,MAAM,CAACF,MAAMG,MAAM,CAAC;IAAO;AAC3E;AAEA,+FAA+F;AAC/F,iGAAiG;AACjG,MAAMC,UAAU,IAAIC;AAEpB,OAAO,SAASC,eAAeC,GAAW,EAAEC,KAA+B;IACzE,IAAIA,MAAMC,IAAI,GAAG,GAAGL,QAAQM,GAAG,CAACH,KAAKC;AACvC;AAEA,+FAA+F;AAC/F,mFAAmF;AACnF,OAAO,SAASG,eAAeC,IAAY,EAAEC,EAAU;IACrD,MAAML,QAAQJ,QAAQU,GAAG,CAACF;IAC1B,IAAI,CAACJ,OAAO;IACZJ,QAAQW,MAAM,CAACH;IACfR,QAAQM,GAAG,CAACG,IAAIL;AAClB;AAEA,iGAAiG;AACjG,+EAA+E;AAC/E,OAAO,SAASQ,cAAcT,GAAW;IACvC,MAAMC,QAAQJ,QAAQU,GAAG,CAACP;IAC1BH,QAAQW,MAAM,CAACR;IACf,OAAOC;AACT"}
@@ -1,2 +1,19 @@
1
- export declare function fetchWithRetry(url: string, init: RequestInit, baseDelayMs?: number): Promise<Response>;
1
+ export declare const HTTP_ATTEMPT_TIMEOUT_MS = 30000;
2
+ export declare const MODEL_FILE_ATTEMPT_TIMEOUT_MS: number;
3
+ export type FetchRequestInit = Omit<RequestInit, 'signal'> & {
4
+ signal?: never;
5
+ };
6
+ export interface FetchRetryOptions {
7
+ signal?: AbortSignal;
8
+ attemptTimeoutMs?: number;
9
+ baseDelayMs?: number;
10
+ }
11
+ export interface BufferedResponse {
12
+ readonly ok: boolean;
13
+ readonly status: number;
14
+ readonly headers: Headers;
15
+ readonly body: ArrayBuffer;
16
+ readonly url: string;
17
+ }
18
+ export declare function fetchWithRetry(url: string, init: FetchRequestInit, options?: FetchRetryOptions): Promise<BufferedResponse>;
2
19
  export declare function probeReachable(url: string, timeoutMs?: number): Promise<boolean>;
@@ -1,33 +1,153 @@
1
+ import { SenseError } from '../errors.js';
1
2
  const MAX_ATTEMPTS = 3;
2
- function sleep(ms) {
3
- return new Promise((resolve)=>setTimeout(resolve, ms));
3
+ const MAX_RETRY_AFTER_MS = 10000;
4
+ const DEFAULT_BASE_DELAY_MS = 200;
5
+ // Initial policy bounds small API/metadata responses separately from model-file downloads.
6
+ export const HTTP_ATTEMPT_TIMEOUT_MS = 30000;
7
+ export const MODEL_FILE_ATTEMPT_TIMEOUT_MS = 5 * 60000;
8
+ function safeUrl(url) {
9
+ try {
10
+ const parsed = new URL(url);
11
+ parsed.username = '';
12
+ parsed.password = '';
13
+ parsed.search = '';
14
+ parsed.hash = '';
15
+ return parsed.href;
16
+ } catch {
17
+ return 'the configured embedding endpoint';
18
+ }
19
+ }
20
+ function attemptController(callerSignal, timeoutMs) {
21
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.throwIfAborted();
22
+ const controller = new AbortController();
23
+ const timeoutReason = {};
24
+ const relayAbort = ()=>controller.abort(callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.reason);
25
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.addEventListener('abort', relayAbort, {
26
+ once: true
27
+ });
28
+ if (callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.aborted) relayAbort();
29
+ const timer = setTimeout(()=>controller.abort(timeoutReason), timeoutMs);
30
+ return {
31
+ controller,
32
+ timeoutReason,
33
+ cleanup: ()=>{
34
+ clearTimeout(timer);
35
+ callerSignal === null || callerSignal === void 0 ? void 0 : callerSignal.removeEventListener('abort', relayAbort);
36
+ }
37
+ };
38
+ }
39
+ function disposeResponse(response, controller) {
40
+ const cancellation = response.body && !response.bodyUsed ? response.body.cancel() : undefined;
41
+ controller.abort();
42
+ // The controller owns socket cleanup; observe stream cancellation without delaying the attempt.
43
+ if (cancellation) void cancellation.then(()=>undefined, ()=>undefined);
44
+ }
45
+ function sleep(ms, signal) {
46
+ signal === null || signal === void 0 ? void 0 : signal.throwIfAborted();
47
+ if (!signal) return new Promise((resolve)=>setTimeout(resolve, ms));
48
+ return new Promise((resolve, reject)=>{
49
+ let timer;
50
+ const cleanup = ()=>{
51
+ if (timer) clearTimeout(timer);
52
+ signal.removeEventListener('abort', onAbort);
53
+ };
54
+ const onAbort = ()=>{
55
+ cleanup();
56
+ reject(signal.reason);
57
+ };
58
+ signal.addEventListener('abort', onAbort, {
59
+ once: true
60
+ });
61
+ timer = setTimeout(()=>{
62
+ cleanup();
63
+ resolve();
64
+ }, ms);
65
+ if (signal.aborted) onAbort();
66
+ });
67
+ }
68
+ function failedRequest(url, phase, attempts, timeoutMs, timedOut) {
69
+ const endpoint = safeUrl(url);
70
+ if (timedOut) {
71
+ return new SenseError('EMBED_MODEL', `${endpoint} timed out after ${timeoutMs}ms while waiting for ${phase} (attempt ${attempts}/${MAX_ATTEMPTS}); check the embedding service and network`);
72
+ }
73
+ return new SenseError('EMBED_MODEL', `${endpoint} failed while waiting for ${phase} after ${attempts} attempts; check the embedding service and network`);
74
+ }
75
+ function buffered(response, body, requestUrl) {
76
+ return {
77
+ ok: response.ok,
78
+ status: response.status,
79
+ headers: response.headers,
80
+ body,
81
+ url: safeUrl(response.url || requestUrl)
82
+ };
4
83
  }
5
- // Shared retry glue for the HTTP providers: 429/5xx/network failure retry with exponential
6
- // backoff honoring Retry-After; any other status returns immediately for the caller to handle.
7
- export async function fetchWithRetry(url, init, baseDelayMs = 200) {
84
+ // Each attempt covers response headers and the complete successful body. Retry cleanup and
85
+ // backoff honor one caller signal without transferring ownership of that signal.
86
+ export async function fetchWithRetry(url, init, options = {}) {
87
+ var _options_attemptTimeoutMs, _options_baseDelayMs;
88
+ const timeoutMs = (_options_attemptTimeoutMs = options.attemptTimeoutMs) !== null && _options_attemptTimeoutMs !== void 0 ? _options_attemptTimeoutMs : HTTP_ATTEMPT_TIMEOUT_MS;
89
+ const baseDelayMs = (_options_baseDelayMs = options.baseDelayMs) !== null && _options_baseDelayMs !== void 0 ? _options_baseDelayMs : DEFAULT_BASE_DELAY_MS;
8
90
  for(let attempt = 0; attempt < MAX_ATTEMPTS; attempt++){
91
+ var _options_signal;
92
+ (_options_signal = options.signal) === null || _options_signal === void 0 ? void 0 : _options_signal.throwIfAborted();
93
+ const { controller, timeoutReason, cleanup } = attemptController(options.signal, timeoutMs);
94
+ let response;
95
+ let phase = 'response headers';
96
+ let retryDelayMs;
9
97
  try {
10
- const res = await fetch(url, init);
11
- if (res.ok || res.status !== 429 && res.status < 500 || attempt === MAX_ATTEMPTS - 1) return res;
12
- const retryAfter = Number(res.headers.get('retry-after'));
13
- // Capped: a server naming a huge Retry-After must not stall an embed run for it.
14
- await sleep(retryAfter > 0 ? Math.min(retryAfter * 1000, 10000) : baseDelayMs * 2 ** attempt);
15
- } catch (err) {
16
- if (attempt === MAX_ATTEMPTS - 1) throw err;
17
- await sleep(baseDelayMs * 2 ** attempt);
98
+ response = await fetch(url, {
99
+ ...init,
100
+ signal: controller.signal
101
+ });
102
+ if (!response.ok) {
103
+ const retryable = response.status === 429 || response.status >= 500;
104
+ if (retryable && attempt < MAX_ATTEMPTS - 1) {
105
+ const retryAfter = Number(response.headers.get('retry-after'));
106
+ retryDelayMs = retryAfter > 0 ? Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS) : baseDelayMs * 2 ** attempt;
107
+ disposeResponse(response, controller);
108
+ } else {
109
+ var _options_signal1;
110
+ disposeResponse(response, controller);
111
+ (_options_signal1 = options.signal) === null || _options_signal1 === void 0 ? void 0 : _options_signal1.throwIfAborted();
112
+ return buffered(response, new ArrayBuffer(0), url);
113
+ }
114
+ } else {
115
+ var _options_signal2;
116
+ phase = 'response body';
117
+ const body = await response.arrayBuffer();
118
+ (_options_signal2 = options.signal) === null || _options_signal2 === void 0 ? void 0 : _options_signal2.throwIfAborted();
119
+ return buffered(response, body, url);
120
+ }
121
+ } catch {
122
+ try {
123
+ var _options_signal3;
124
+ (_options_signal3 = options.signal) === null || _options_signal3 === void 0 ? void 0 : _options_signal3.throwIfAborted();
125
+ } finally{
126
+ if (response) disposeResponse(response, controller);
127
+ }
128
+ const timedOut = controller.signal.reason === timeoutReason;
129
+ if (attempt === MAX_ATTEMPTS - 1) throw failedRequest(url, phase, attempt + 1, timeoutMs, timedOut);
130
+ retryDelayMs = baseDelayMs * 2 ** attempt;
131
+ } finally{
132
+ cleanup();
18
133
  }
134
+ await sleep(retryDelayMs !== null && retryDelayMs !== void 0 ? retryDelayMs : baseDelayMs * 2 ** attempt, options.signal);
19
135
  }
20
136
  throw new Error('unreachable');
21
137
  }
22
- // Any HTTP response means the endpoint is up; only a network failure or the timeout counts as
23
- // unreachable, so an air-gapped machine never waits more than timeoutMs on `sense status`.
138
+ // Any HTTP response means the endpoint is up. Abort it immediately because status does not
139
+ // consume its body, and bound the headers wait for an air-gapped machine.
24
140
  export async function probeReachable(url, timeoutMs = 1500) {
141
+ const { controller, cleanup } = attemptController(undefined, timeoutMs);
25
142
  try {
26
- await fetch(url, {
27
- signal: AbortSignal.timeout(timeoutMs)
143
+ const response = await fetch(url, {
144
+ signal: controller.signal
28
145
  });
146
+ disposeResponse(response, controller);
29
147
  return true;
30
148
  } catch {
31
149
  return false;
150
+ } finally{
151
+ cleanup();
32
152
  }
33
153
  }