@zvec/zvec-grep 0.1.4 → 0.1.6

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 (356) hide show
  1. package/README.md +86 -162
  2. package/README_CN.md +80 -162
  3. package/dist/authorization/index.d.ts +7 -0
  4. package/dist/authorization/index.js +7 -0
  5. package/dist/authorization/index.js.map +1 -0
  6. package/dist/authorization/manager.d.ts +8 -0
  7. package/dist/authorization/manager.js +19 -0
  8. package/dist/authorization/manager.js.map +1 -0
  9. package/dist/authorization/operation.d.ts +7 -0
  10. package/dist/authorization/operation.js +50 -0
  11. package/dist/authorization/operation.js.map +1 -0
  12. package/dist/authorization/planner.d.ts +21 -0
  13. package/dist/authorization/planner.js +97 -0
  14. package/dist/authorization/planner.js.map +1 -0
  15. package/dist/authorization/prompt.d.ts +11 -0
  16. package/dist/authorization/prompt.js +62 -0
  17. package/dist/authorization/prompt.js.map +1 -0
  18. package/dist/authorization/store.d.ts +20 -0
  19. package/dist/authorization/store.js +251 -0
  20. package/dist/authorization/store.js.map +1 -0
  21. package/dist/authorization/target.d.ts +15 -0
  22. package/dist/authorization/target.js +49 -0
  23. package/dist/authorization/target.js.map +1 -0
  24. package/dist/authorization/types.d.ts +61 -0
  25. package/dist/authorization/types.js +2 -0
  26. package/dist/authorization/types.js.map +1 -0
  27. package/dist/cli/args.d.ts +3 -0
  28. package/dist/cli/args.js +749 -201
  29. package/dist/cli/args.js.map +1 -1
  30. package/dist/cli/auth.d.ts +21 -0
  31. package/dist/cli/auth.js +212 -0
  32. package/dist/cli/auth.js.map +1 -0
  33. package/dist/cli/commands.d.ts +3 -1
  34. package/dist/cli/commands.js +529 -180
  35. package/dist/cli/commands.js.map +1 -1
  36. package/dist/cli/errors.js +7 -6
  37. package/dist/cli/errors.js.map +1 -1
  38. package/dist/cli/format/context.d.ts +1 -0
  39. package/dist/cli/format/context.js +270 -70
  40. package/dist/cli/format/context.js.map +1 -1
  41. package/dist/cli/format/debug.js +14 -16
  42. package/dist/cli/format/debug.js.map +1 -1
  43. package/dist/cli/format/highlight.js.map +1 -1
  44. package/dist/cli/format/progress.d.ts +4 -0
  45. package/dist/cli/format/progress.js +222 -44
  46. package/dist/cli/format/progress.js.map +1 -1
  47. package/dist/cli/format/range.js.map +1 -1
  48. package/dist/cli/format/status.d.ts +77 -6
  49. package/dist/cli/format/status.js +343 -114
  50. package/dist/cli/format/status.js.map +1 -1
  51. package/dist/cli/help.d.ts +1 -1
  52. package/dist/cli/help.js +368 -84
  53. package/dist/cli/help.js.map +1 -1
  54. package/dist/cli/index.js +4 -4
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/cli/install.d.ts +4 -0
  57. package/dist/cli/install.js +845 -0
  58. package/dist/cli/install.js.map +1 -0
  59. package/dist/cli/managed-rg.d.ts +10 -0
  60. package/dist/cli/managed-rg.js +329 -0
  61. package/dist/cli/managed-rg.js.map +1 -0
  62. package/dist/cli/types.d.ts +40 -13
  63. package/dist/cli/types.js.map +1 -1
  64. package/dist/cli/version.js +3 -1
  65. package/dist/cli/version.js.map +1 -1
  66. package/dist/client/daemon-client.d.ts +19 -0
  67. package/dist/client/daemon-client.js +180 -0
  68. package/dist/client/daemon-client.js.map +1 -0
  69. package/dist/client/mode-router.d.ts +9 -0
  70. package/dist/client/mode-router.js +28 -0
  71. package/dist/client/mode-router.js.map +1 -0
  72. package/dist/client/search-policy.d.ts +11 -0
  73. package/dist/client/search-policy.js +15 -0
  74. package/dist/client/search-policy.js.map +1 -0
  75. package/dist/daemon/backend.d.ts +76 -0
  76. package/dist/daemon/backend.js +1049 -0
  77. package/dist/daemon/backend.js.map +1 -0
  78. package/dist/daemon/change-set.d.ts +25 -0
  79. package/dist/daemon/change-set.js +101 -0
  80. package/dist/daemon/change-set.js.map +1 -0
  81. package/dist/daemon/config.d.ts +24 -0
  82. package/dist/daemon/config.js +88 -0
  83. package/dist/daemon/config.js.map +1 -0
  84. package/dist/daemon/errors.d.ts +5 -0
  85. package/dist/daemon/errors.js +11 -0
  86. package/dist/daemon/errors.js.map +1 -0
  87. package/dist/daemon/http-server.d.ts +30 -0
  88. package/dist/daemon/http-server.js +287 -0
  89. package/dist/daemon/http-server.js.map +1 -0
  90. package/dist/daemon/index-coordinator.d.ts +23 -0
  91. package/dist/daemon/index-coordinator.js +68 -0
  92. package/dist/daemon/index-coordinator.js.map +1 -0
  93. package/dist/daemon/job-scheduler.d.ts +70 -0
  94. package/dist/daemon/job-scheduler.js +366 -0
  95. package/dist/daemon/job-scheduler.js.map +1 -0
  96. package/dist/daemon/logger.d.ts +9 -0
  97. package/dist/daemon/logger.js +53 -0
  98. package/dist/daemon/logger.js.map +1 -0
  99. package/dist/daemon/model-pool.d.ts +45 -0
  100. package/dist/daemon/model-pool.js +187 -0
  101. package/dist/daemon/model-pool.js.map +1 -0
  102. package/dist/daemon/root-lease.d.ts +14 -0
  103. package/dist/daemon/root-lease.js +188 -0
  104. package/dist/daemon/root-lease.js.map +1 -0
  105. package/dist/daemon/root-runtime.d.ts +78 -0
  106. package/dist/daemon/root-runtime.js +333 -0
  107. package/dist/daemon/root-runtime.js.map +1 -0
  108. package/dist/daemon/runtime-manager.d.ts +45 -0
  109. package/dist/daemon/runtime-manager.js +243 -0
  110. package/dist/daemon/runtime-manager.js.map +1 -0
  111. package/dist/daemon/runtime.d.ts +12 -0
  112. package/dist/daemon/runtime.js +102 -0
  113. package/dist/daemon/runtime.js.map +1 -0
  114. package/dist/daemon/server-controller.d.ts +39 -0
  115. package/dist/daemon/server-controller.js +203 -0
  116. package/dist/daemon/server-controller.js.map +1 -0
  117. package/dist/daemon/watch-manager.d.ts +50 -0
  118. package/dist/daemon/watch-manager.js +293 -0
  119. package/dist/daemon/watch-manager.js.map +1 -0
  120. package/dist/daemon/workspace-read-session-cache.d.ts +31 -0
  121. package/dist/daemon/workspace-read-session-cache.js +115 -0
  122. package/dist/daemon/workspace-read-session-cache.js.map +1 -0
  123. package/dist/engine/config.d.ts +50 -0
  124. package/dist/engine/config.js +321 -0
  125. package/dist/engine/config.js.map +1 -0
  126. package/dist/engine/{errors/index.d.ts → errors.d.ts} +6 -1
  127. package/dist/engine/{errors/details.js → errors.js} +17 -4
  128. package/dist/engine/errors.js.map +1 -0
  129. package/dist/engine/extraction/code/adapter.d.ts +1 -1
  130. package/dist/engine/extraction/code/adapter.js.map +1 -1
  131. package/dist/engine/extraction/code/extractor.d.ts +5 -11
  132. package/dist/engine/extraction/code/extractor.js +184 -109
  133. package/dist/engine/extraction/code/extractor.js.map +1 -1
  134. package/dist/engine/extraction/code/families/c-family.js +13 -7
  135. package/dist/engine/extraction/code/families/c-family.js.map +1 -1
  136. package/dist/engine/extraction/code/families/js-ts.d.ts +1 -1
  137. package/dist/engine/extraction/code/families/js-ts.js +24 -18
  138. package/dist/engine/extraction/code/families/js-ts.js.map +1 -1
  139. package/dist/engine/extraction/code/families/metadata.d.ts +1 -1
  140. package/dist/engine/extraction/code/families/metadata.js +11 -8
  141. package/dist/engine/extraction/code/families/metadata.js.map +1 -1
  142. package/dist/engine/extraction/code/families/name-field.js +5 -5
  143. package/dist/engine/extraction/code/families/name-field.js.map +1 -1
  144. package/dist/engine/extraction/code/languages/c.js +1 -4
  145. package/dist/engine/extraction/code/languages/c.js.map +1 -1
  146. package/dist/engine/extraction/code/languages/cpp.js.map +1 -1
  147. package/dist/engine/extraction/code/languages/go.js +1 -3
  148. package/dist/engine/extraction/code/languages/go.js.map +1 -1
  149. package/dist/engine/extraction/code/languages/java.js.map +1 -1
  150. package/dist/engine/extraction/code/languages/javascript.js +1 -3
  151. package/dist/engine/extraction/code/languages/javascript.js.map +1 -1
  152. package/dist/engine/extraction/code/languages/python.js +5 -6
  153. package/dist/engine/extraction/code/languages/python.js.map +1 -1
  154. package/dist/engine/extraction/code/languages/rust.js +1 -5
  155. package/dist/engine/extraction/code/languages/rust.js.map +1 -1
  156. package/dist/engine/extraction/code/languages/typescript.js.map +1 -1
  157. package/dist/engine/extraction/code/tree-sitter/grammar.js.map +1 -0
  158. package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/nodes.js +2 -1
  159. package/dist/engine/extraction/code/tree-sitter/nodes.js.map +1 -0
  160. package/dist/engine/extraction/code/tree-sitter/parser.js.map +1 -0
  161. package/dist/engine/extraction/ids.js.map +1 -1
  162. package/dist/engine/extraction/image/extractor.d.ts +5 -0
  163. package/dist/engine/extraction/image/extractor.js +35 -0
  164. package/dist/engine/extraction/image/extractor.js.map +1 -0
  165. package/dist/engine/extraction/index.d.ts +3 -44
  166. package/dist/engine/extraction/index.js +1 -280
  167. package/dist/engine/extraction/index.js.map +1 -1
  168. package/dist/engine/extraction/markdown/extractor.d.ts +5 -11
  169. package/dist/engine/extraction/markdown/extractor.js +37 -16
  170. package/dist/engine/extraction/markdown/extractor.js.map +1 -1
  171. package/dist/engine/extraction/runtime.d.ts +4 -0
  172. package/dist/engine/extraction/runtime.js +26 -0
  173. package/dist/engine/extraction/runtime.js.map +1 -0
  174. package/dist/engine/extraction/source.d.ts +18 -0
  175. package/dist/engine/extraction/source.js +22 -0
  176. package/dist/engine/extraction/source.js.map +1 -0
  177. package/dist/engine/extraction/text/extractor.d.ts +7 -0
  178. package/dist/engine/extraction/text/extractor.js +168 -0
  179. package/dist/engine/extraction/text/extractor.js.map +1 -0
  180. package/dist/engine/extraction/types.d.ts +4 -0
  181. package/dist/engine/{models → extraction}/types.js.map +1 -1
  182. package/dist/engine/{files/file-type.d.ts → file-type.d.ts} +1 -1
  183. package/dist/engine/file-type.js.map +1 -0
  184. package/dist/engine/index-status.d.ts +9 -0
  185. package/dist/engine/index-status.js +38 -0
  186. package/dist/engine/index-status.js.map +1 -0
  187. package/dist/engine/manifest.d.ts +12 -0
  188. package/dist/engine/manifest.js +120 -0
  189. package/dist/engine/manifest.js.map +1 -0
  190. package/dist/engine/models/{providers/llama-cpp/embedding.d.ts → backends/llama-cpp.d.ts} +25 -20
  191. package/dist/engine/models/{providers/llama-cpp/embedding.js → backends/llama-cpp.js} +165 -113
  192. package/dist/engine/models/backends/llama-cpp.js.map +1 -0
  193. package/dist/engine/models/backends/model2vec.d.ts +57 -0
  194. package/dist/engine/models/backends/model2vec.js +332 -0
  195. package/dist/engine/models/backends/model2vec.js.map +1 -0
  196. package/dist/engine/models/backends/qwen.d.ts +44 -0
  197. package/dist/engine/models/{providers/qwen/embedding.js → backends/qwen.js} +168 -102
  198. package/dist/engine/models/backends/qwen.js.map +1 -0
  199. package/dist/engine/models/backends/transformers-js.d.ts +87 -0
  200. package/dist/engine/models/backends/transformers-js.js +316 -0
  201. package/dist/engine/models/backends/transformers-js.js.map +1 -0
  202. package/dist/engine/models/catalog.d.ts +188 -23
  203. package/dist/engine/models/catalog.js +172 -41
  204. package/dist/engine/models/catalog.js.map +1 -1
  205. package/dist/engine/models/download-progress.d.ts +15 -0
  206. package/dist/engine/models/download-progress.js +48 -0
  207. package/dist/engine/models/download-progress.js.map +1 -0
  208. package/dist/engine/models/embeddings.d.ts +58 -18
  209. package/dist/engine/models/embeddings.js +53 -29
  210. package/dist/engine/models/embeddings.js.map +1 -1
  211. package/dist/engine/models/factory.d.ts +2 -5
  212. package/dist/engine/models/factory.js +41 -56
  213. package/dist/engine/models/factory.js.map +1 -1
  214. package/dist/engine/models/index.d.ts +4 -6
  215. package/dist/engine/models/index.js +4 -4
  216. package/dist/engine/models/index.js.map +1 -1
  217. package/dist/engine/models/ranking.d.ts +7 -3
  218. package/dist/engine/models/resolution.d.ts +8 -0
  219. package/dist/engine/models/resolution.js +22 -0
  220. package/dist/engine/models/resolution.js.map +1 -0
  221. package/dist/engine/pipeline/indexing/index.d.ts +9 -7
  222. package/dist/engine/pipeline/indexing/index.js +399 -147
  223. package/dist/engine/pipeline/indexing/index.js.map +1 -1
  224. package/dist/engine/pipeline/indexing/root-paths.js +16 -19
  225. package/dist/engine/pipeline/indexing/root-paths.js.map +1 -1
  226. package/dist/engine/pipeline/indexing/scanner/index.d.ts +6 -1
  227. package/dist/engine/pipeline/indexing/scanner/index.js +318 -66
  228. package/dist/engine/pipeline/indexing/scanner/index.js.map +1 -1
  229. package/dist/engine/pipeline/search/index.d.ts +6 -6
  230. package/dist/engine/pipeline/search/index.js +76 -34
  231. package/dist/engine/pipeline/search/index.js.map +1 -1
  232. package/dist/engine/service/index.d.ts +3 -2
  233. package/dist/engine/service/index.js +1 -1
  234. package/dist/engine/service/index.js.map +1 -1
  235. package/dist/engine/service/lexical.d.ts +14 -15
  236. package/dist/engine/service/lexical.js +78 -119
  237. package/dist/engine/service/lexical.js.map +1 -1
  238. package/dist/engine/service/root.d.ts +13 -12
  239. package/dist/engine/service/root.js +35 -24
  240. package/dist/engine/service/root.js.map +1 -1
  241. package/dist/engine/service/structure-enrichment.js +29 -25
  242. package/dist/engine/service/structure-enrichment.js.map +1 -1
  243. package/dist/engine/service/types.d.ts +61 -35
  244. package/dist/engine/service/workspace-index.d.ts +24 -0
  245. package/dist/engine/service/workspace-index.js +151 -0
  246. package/dist/engine/service/workspace-index.js.map +1 -0
  247. package/dist/engine/service/zvec-grep.d.ts +12 -1
  248. package/dist/engine/service/zvec-grep.js +878 -544
  249. package/dist/engine/service/zvec-grep.js.map +1 -1
  250. package/dist/engine/storage/index.d.ts +8 -4
  251. package/dist/engine/storage/index.js +1 -1
  252. package/dist/engine/storage/index.js.map +1 -1
  253. package/dist/engine/storage/zvec.d.ts +10 -33
  254. package/dist/engine/storage/zvec.js +110 -49
  255. package/dist/engine/storage/zvec.js.map +1 -1
  256. package/dist/engine/types.d.ts +29 -23
  257. package/dist/engine/types.js +1 -1
  258. package/dist/engine/types.js.map +1 -1
  259. package/dist/engine/utils/daemon-lease.d.ts +17 -0
  260. package/dist/engine/utils/daemon-lease.js +175 -0
  261. package/dist/engine/utils/daemon-lease.js.map +1 -0
  262. package/dist/engine/utils/file-selection.d.ts +10 -0
  263. package/dist/engine/utils/file-selection.js +157 -0
  264. package/dist/engine/utils/file-selection.js.map +1 -0
  265. package/dist/engine/utils/glob.d.ts +3 -0
  266. package/dist/engine/utils/glob.js +116 -18
  267. package/dist/engine/utils/glob.js.map +1 -1
  268. package/dist/engine/utils/hash.js.map +1 -1
  269. package/dist/engine/utils/json.d.ts +6 -2
  270. package/dist/engine/utils/json.js +50 -12
  271. package/dist/engine/utils/json.js.map +1 -1
  272. package/dist/engine/utils/lock.js +7 -7
  273. package/dist/engine/utils/lock.js.map +1 -1
  274. package/dist/engine/utils/path.js +2 -2
  275. package/dist/engine/utils/path.js.map +1 -1
  276. package/dist/engine/utils/timing.js.map +1 -1
  277. package/dist/index-progress.d.ts +10 -0
  278. package/dist/index-progress.js +159 -0
  279. package/dist/index-progress.js.map +1 -0
  280. package/dist/index.d.ts +6 -6
  281. package/dist/index.js +3 -3
  282. package/dist/index.js.map +1 -1
  283. package/dist/mcp/http-transport.d.ts +27 -0
  284. package/dist/mcp/http-transport.js +182 -0
  285. package/dist/mcp/http-transport.js.map +1 -0
  286. package/dist/mcp/input-normalization.d.ts +38 -0
  287. package/dist/mcp/input-normalization.js +95 -0
  288. package/dist/mcp/input-normalization.js.map +1 -0
  289. package/dist/mcp/progress-heartbeat.d.ts +17 -0
  290. package/dist/mcp/progress-heartbeat.js +46 -0
  291. package/dist/mcp/progress-heartbeat.js.map +1 -0
  292. package/dist/mcp/request-metadata.d.ts +2 -0
  293. package/dist/mcp/request-metadata.js +11 -0
  294. package/dist/mcp/request-metadata.js.map +1 -0
  295. package/dist/mcp/request-state.d.ts +40 -0
  296. package/dist/mcp/request-state.js +210 -0
  297. package/dist/mcp/request-state.js.map +1 -0
  298. package/dist/mcp/result-format.d.ts +13 -0
  299. package/dist/mcp/result-format.js +75 -0
  300. package/dist/mcp/result-format.js.map +1 -0
  301. package/dist/mcp/schemas.d.ts +430 -0
  302. package/dist/mcp/schemas.js +447 -0
  303. package/dist/mcp/schemas.js.map +1 -0
  304. package/dist/mcp/tools.d.ts +150 -0
  305. package/dist/mcp/tools.js +448 -0
  306. package/dist/mcp/tools.js.map +1 -0
  307. package/dist/mcp/toolset.d.ts +5 -0
  308. package/dist/mcp/toolset.js +12 -0
  309. package/dist/mcp/toolset.js.map +1 -0
  310. package/dist/observability/trace-context.d.ts +11 -0
  311. package/dist/observability/trace-context.js +138 -0
  312. package/dist/observability/trace-context.js.map +1 -0
  313. package/docs/01-agents.md +111 -0
  314. package/docs/02-cli.md +229 -0
  315. package/docs/03-mcp.md +167 -0
  316. package/docs/04-pipeline.md +218 -0
  317. package/docs/05-architecture.md +86 -0
  318. package/docs/06-server.md +210 -0
  319. package/docs/07-embedding.md +179 -0
  320. package/docs/08-roadmap.md +85 -0
  321. package/docs/README.md +53 -0
  322. package/package.json +43 -7
  323. package/skills/zvec-grep/SKILL.md +43 -123
  324. package/skills/zvec-grep/agents/openai.yaml +2 -2
  325. package/skills/zvec-grep/references/cli-fallback.md +85 -0
  326. package/dist/engine/collection/index.d.ts +0 -55
  327. package/dist/engine/collection/index.js +0 -710
  328. package/dist/engine/collection/index.js.map +0 -1
  329. package/dist/engine/errors/details.d.ts +0 -6
  330. package/dist/engine/errors/details.js.map +0 -1
  331. package/dist/engine/errors/index.js +0 -16
  332. package/dist/engine/errors/index.js.map +0 -1
  333. package/dist/engine/extraction/tree-sitter/grammar.js.map +0 -1
  334. package/dist/engine/extraction/tree-sitter/nodes.js.map +0 -1
  335. package/dist/engine/extraction/tree-sitter/parser.js.map +0 -1
  336. package/dist/engine/files/file-type.js.map +0 -1
  337. package/dist/engine/models/providers/index.d.ts +0 -2
  338. package/dist/engine/models/providers/index.js +0 -3
  339. package/dist/engine/models/providers/index.js.map +0 -1
  340. package/dist/engine/models/providers/llama-cpp/embedding.js.map +0 -1
  341. package/dist/engine/models/providers/llama-cpp/index.d.ts +0 -1
  342. package/dist/engine/models/providers/llama-cpp/index.js +0 -2
  343. package/dist/engine/models/providers/llama-cpp/index.js.map +0 -1
  344. package/dist/engine/models/providers/qwen/embedding.d.ts +0 -42
  345. package/dist/engine/models/providers/qwen/embedding.js.map +0 -1
  346. package/dist/engine/models/providers/qwen/index.d.ts +0 -1
  347. package/dist/engine/models/providers/qwen/index.js +0 -2
  348. package/dist/engine/models/providers/qwen/index.js.map +0 -1
  349. package/dist/engine/models/types.d.ts +0 -30
  350. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/grammar.d.ts +0 -0
  351. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/grammar.js +0 -0
  352. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/nodes.d.ts +0 -0
  353. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/parser.d.ts +0 -0
  354. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/parser.js +0 -0
  355. /package/dist/engine/{models → extraction}/types.js +0 -0
  356. /package/dist/engine/{files/file-type.js → file-type.js} +0 -0
@@ -1,439 +1,406 @@
1
- import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { createHash } from "node:crypto";
3
- import { dirname, join, relative, resolve } from "node:path";
4
- import { Collection, CollectionRegistry, isCollectionIndexed, } from "../collection/index.js";
5
- import { collectionDetail, detail, EngineError, errorDetails, } from "../errors/index.js";
6
- import { createEmbeddingModel, createEmbeddingModelFromReference, } from "../models/index.js";
7
- import { ANONYMOUS_COLLECTION_NAME, anonymousCollectionPath, anonymousHome, anonymousIndexLocation, resolveZvecGrepRoot, } from "./root.js";
8
- import { runLexicalFallback, runRgSearch } from "./lexical.js";
9
- import { defaultHome } from "../utils/path.js";
10
- import { acquireReadWriteLock, assertNoWriteLock } from "../utils/lock.js";
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { globalConfigPath, readGlobalConfig, resolveEmbeddingRuntimeOptions, } from "../config.js";
5
+ import { isWorkspaceIndexed, WorkspaceIndex } from "./workspace-index.js";
6
+ import { workspaceIndexDetail, detail, EngineError, errorDetails, } from "../errors.js";
7
+ import { createEmbeddingModel, EmbeddingPurpose, resolveEmbeddingReference, } from "../models/index.js";
8
+ import { CURRENT_INDEX_VERSION } from "../types.js";
9
+ import { indexStatusNeedsRefresh } from "../index-status.js";
10
+ import { workspaceIndexInfoFromManifest, CURRENT_MANIFEST_VERSION, readWorkspaceManifest, writeWorkspaceManifest, } from "../manifest.js";
11
+ import { validateRootPaths } from "../pipeline/indexing/root-paths.js";
12
+ import { findNearestWorkspace, hasWorkspaceIndex, resetWorkspaceIndexStorage, resolveZvecGrepRoot, workspaceHome, workspaceIndexLocation, } from "./root.js";
13
+ import { runRgSearch } from "./lexical.js";
14
+ import { defaultHome, normalizePath } from "../utils/path.js";
15
+ import { acquireReadWriteLock, assertNoWriteLock, } from "../utils/lock.js";
16
+ import { assertDaemonWriteAllowed } from "../utils/daemon-lease.js";
11
17
  import { TimingCollector } from "../utils/timing.js";
12
18
  import { enrichLexicalItemsWithStructure } from "./structure-enrichment.js";
19
+ import { remoteEmbeddingAuthorizationGuard } from "../../authorization/operation.js";
20
+ import { RemoteEmbeddingAuthorizationStore } from "../../authorization/store.js";
13
21
  const DEFAULT_CONTEXT_LIMIT = 10;
14
22
  const DEFAULT_CONTEXT_TOTAL_LIMIT = 30;
15
23
  const DEFAULT_LOCAL_EMBEDDING = "local/embeddinggemma-300m";
24
+ const PROVIDER_API_KEY_IDENTITY_SECRET = randomBytes(32);
25
+ const MAX_RECOVERED_EMBEDDING_MODELS = 4;
16
26
  export async function createZvecGrep(options = {}) {
17
27
  return new ZvecGrepService(options);
18
28
  }
29
+ export function openWorkspaceReadSession(startRoot, embeddingModel) {
30
+ const start = resolveZvecGrepRoot(startRoot);
31
+ assertNearestWorkspaceHomeUnlocked(start, "daemon.context.open");
32
+ const nearest = findNearestWorkspaceIndex(start);
33
+ if (!nearest) {
34
+ throw workspaceIndexMissingError(start, "undecided");
35
+ }
36
+ const { location, info } = nearest;
37
+ if (info.indexPolicy === "disabled") {
38
+ throw workspaceIndexDisabledError(location.root);
39
+ }
40
+ if (!isWorkspaceIndexed(info) || !hasWorkspaceIndex(location)) {
41
+ throw workspaceIndexMissingError(location.root, info.indexPolicy ?? "enabled");
42
+ }
43
+ const workspaceIndex = new WorkspaceIndex(info, embeddingModel, true, location.filesPath);
44
+ let closed = false;
45
+ return {
46
+ root: location.root,
47
+ async context(options) {
48
+ if (closed) {
49
+ throw new EngineError("Workspace read session is already closed", {
50
+ code: "ZVEC_GREP.ENGINE.SERVICE.READ_SESSION_CLOSED",
51
+ });
52
+ }
53
+ const timings = new TimingCollector();
54
+ const request = normalizeContextRequest(options);
55
+ const result = await timings.time("total", () => withHomeReadLock(location.home, "daemon.context", () => contextFromOpenWorkspaceIndex({
56
+ root: location.root,
57
+ request,
58
+ workspaceIndex,
59
+ options: { ...options, autoUpdate: false },
60
+ timings,
61
+ })));
62
+ return withContextTimings(result, timings);
63
+ },
64
+ async close() {
65
+ if (closed) {
66
+ return;
67
+ }
68
+ workspaceIndex.close();
69
+ closed = true;
70
+ },
71
+ };
72
+ }
73
+ export function createEmbeddingModelForIdentity(identity, options = {}, workspaceRuntime = {}) {
74
+ const reference = embeddingModelReference(identity);
75
+ return createServiceEmbeddingModel(reference, providerOptions(options, identity, readGlobalConfig(), workspaceRuntime), options);
76
+ }
77
+ export function embeddingModelPoolKeyForIdentity(identity, options = {}, workspaceRuntime = {}) {
78
+ const reference = embeddingModelReference(identity);
79
+ const fingerprint = providerOptionsFingerprint(providerOptions(options, identity, readGlobalConfig(), workspaceRuntime));
80
+ return [reference, fingerprint].join("/");
81
+ }
19
82
  class ZvecGrepService {
20
83
  options;
21
84
  root;
22
- collections;
23
85
  embeddingModel;
24
- embeddingModelExplicit;
25
86
  recoveredEmbeddingModels = new Map();
87
+ retiredEmbeddingModels = new Set();
88
+ activeEmbeddingModelOperations = 0;
26
89
  closed = false;
27
90
  constructor(options) {
28
91
  this.options = options;
29
92
  this.root = resolveZvecGrepRoot(options.root);
30
- this.embeddingModelExplicit = hasExplicitEmbeddingModel(options);
31
- this.embeddingModel = resolveEmbeddingModel(options, this.root);
32
- this.collections = {
33
- list: () => this.listCollections(),
34
- info: (name) => this.collectionInfo(name),
35
- status: (name) => this.collectionStatus(name),
36
- index: (name, paths, indexOptions) => this.indexCollection(name, paths, indexOptions),
37
- remove: (name) => this.removeCollection(name),
38
- };
93
+ this.embeddingModel = options.embeddingModel;
39
94
  }
40
95
  async index(options = {}) {
41
96
  this.ensureOpen();
42
97
  const root = resolveZvecGrepRoot(options.root ?? this.root);
43
- const location = anonymousIndexLocation(root);
44
- return await withHomeWriteLock(location.home, options.rebuild ? "index.rebuild" : "index", async () => {
45
- const existing = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
46
- const embeddingModel = this.embeddingModelForIndex(existing, location.home, "index");
47
- const registry = new CollectionRegistry(location.home, embeddingModel);
48
- try {
98
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
99
+ const location = workspaceIndexLocation(root);
100
+ try {
101
+ return await this.withEmbeddingModelOperation(() => withHomeWriteLock(location.home, options.rebuild ? "index.rebuild" : "index", async () => {
102
+ const existing = readWorkspaceManifest(location.home);
103
+ const existingRuntime = existing?.embeddingRuntime ?? {};
104
+ const embeddingModel = this.embeddingModelForIndex(existing, "index", existingRuntime);
105
+ const effectiveRuntime = effectiveEmbeddingRuntime(this.options, embeddingModel, runtimeForModelProvider(existing, embeddingModel, existingRuntime));
106
+ if (!options.rebuild) {
107
+ assertWorkspaceEndpointMatchesCurrentRuntime(existing, existingRuntime, effectiveRuntime, "zg index --endpoint <url> --rebuild");
108
+ }
49
109
  const rootPaths = resolveIndexRootPaths(existing, options.rootPaths, root, {
50
110
  resetPaths: options.resetPaths === true,
51
111
  includePaths: options.includePaths,
52
112
  excludePaths: options.excludePaths,
113
+ globs: options.globs,
114
+ insensitiveGlobs: options.insensitiveGlobs,
115
+ fileTypes: options.fileTypes,
116
+ excludedFileTypes: options.excludedFileTypes,
117
+ hidden: options.hidden,
118
+ noIgnore: options.noIgnore,
119
+ ignoreFiles: options.ignoreFiles,
120
+ maxDepth: options.maxDepth,
121
+ maxFileSizeBytes: options.maxFileSizeBytes,
122
+ follow: options.follow,
53
123
  });
54
- if (options.rebuild) {
55
- registry.remove(ANONYMOUS_COLLECTION_NAME);
124
+ if (options.rebuild || !isWorkspaceIndexed(existing)) {
125
+ resetWorkspaceIndexStorage(location);
56
126
  }
57
- const existingAfterRebuild = registry.get(ANONYMOUS_COLLECTION_NAME);
58
- if (isCollectionIndexed(existingAfterRebuild)) {
59
- assertCollectionEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg --index --rebuild");
127
+ const existingAfterRebuild = options.rebuild ? null : existing;
128
+ if (isWorkspaceIndexed(existingAfterRebuild)) {
129
+ assertWorkspaceEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg index --rebuild");
60
130
  }
61
- registry.prepareIndex(ANONYMOUS_COLLECTION_NAME, rootPaths, anonymousCollectionPath(root));
62
- return await registry.open(ANONYMOUS_COLLECTION_NAME).index({
63
- rebuild: false,
64
- embeddingConcurrency: options.embeddingConcurrency,
65
- onProgress: options.onProgress,
66
- });
67
- }
68
- finally {
69
- registry.close();
131
+ const manifest = prepareWorkspaceManifest(location, existingAfterRebuild, rootPaths, embeddingModel, existingRuntime);
132
+ const workspaceIndex = new WorkspaceIndex(manifest, embeddingModel, false, location.filesPath);
133
+ writeWorkspaceManifest(location.home, manifest);
134
+ try {
135
+ const releaseWriterContext = options.onWriterContext?.((contextOptions) => this.contextFromWriterWorkspaceIndex(root, workspaceIndex, contextOptions));
136
+ try {
137
+ const result = await workspaceIndex.index({
138
+ rebuild: false,
139
+ embeddingConcurrency: options.embeddingConcurrency,
140
+ onProgress: options.onProgress,
141
+ changedPaths: options.changedPaths,
142
+ signal: options.signal,
143
+ });
144
+ writeWorkspaceManifest(location.home, {
145
+ ...manifest,
146
+ embeddingRuntime: embeddingRuntimeAfterSuccessfulIndex(existing, existingRuntime, embeddingModel, effectiveRuntime, this.options),
147
+ updatedTime: Date.now(),
148
+ });
149
+ return result;
150
+ }
151
+ catch (error) {
152
+ writeWorkspaceManifest(location.home, {
153
+ ...manifest,
154
+ embeddingRuntime: existingRuntime,
155
+ updatedTime: Date.now(),
156
+ });
157
+ throw error;
158
+ }
159
+ finally {
160
+ await releaseWriterContext?.();
161
+ }
162
+ }
163
+ finally {
164
+ workspaceIndex.close();
165
+ }
166
+ }));
167
+ }
168
+ finally {
169
+ daemonWritePermit?.release();
170
+ }
171
+ }
172
+ async dropIndex(options = {}) {
173
+ this.ensureOpen();
174
+ const root = resolveZvecGrepRoot(options.root ?? this.root);
175
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
176
+ const location = workspaceIndexLocation(root);
177
+ try {
178
+ if (!readWorkspaceManifest(location.home)) {
179
+ return false;
70
180
  }
71
- });
181
+ return await withHomeWriteLock(location.home, "index.drop", async () => {
182
+ resetWorkspaceIndexStorage(location);
183
+ return true;
184
+ });
185
+ }
186
+ finally {
187
+ daemonWritePermit?.release();
188
+ }
72
189
  }
73
190
  async disableIndex(options = {}) {
74
191
  this.ensureOpen();
75
192
  const root = resolveZvecGrepRoot(options.root ?? this.root);
76
- const location = anonymousIndexLocation(root);
77
- await withHomeWriteLock(location.home, "index.disable", async () => {
78
- const registry = new CollectionRegistry(location.home, undefined);
79
- try {
80
- registry.disableIndex(ANONYMOUS_COLLECTION_NAME, [root], location.collectionPath);
81
- }
82
- finally {
83
- registry.close();
84
- }
85
- });
86
- return this.info({ root });
193
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
194
+ const location = workspaceIndexLocation(root);
195
+ try {
196
+ await withHomeWriteLock(location.home, "index.disable", async () => {
197
+ const existing = readWorkspaceManifest(location.home);
198
+ const now = Date.now();
199
+ writeWorkspaceManifest(location.home, {
200
+ manifestVersion: CURRENT_MANIFEST_VERSION,
201
+ id: existing?.id ?? randomUUID(),
202
+ name: existing?.name ?? workspaceDisplayName(root),
203
+ path: location.home,
204
+ rootPaths: existing?.rootPaths ?? validateRootPaths([root]),
205
+ indexPolicy: "disabled",
206
+ embedding: null,
207
+ indexVersion: null,
208
+ createdTime: existing?.createdTime ?? now,
209
+ updatedTime: now,
210
+ embeddingRuntime: existing?.embeddingRuntime ?? {},
211
+ });
212
+ });
213
+ return this.info({ root });
214
+ }
215
+ finally {
216
+ daemonWritePermit?.release();
217
+ }
87
218
  }
88
219
  async context(options) {
89
220
  this.ensureOpen();
90
- const timings = new TimingCollector();
91
- const result = await timings.time("total", () => this.contextWithTimings(options, timings));
92
- return withContextTimings(result, timings);
221
+ return await this.withEmbeddingModelOperation(async () => {
222
+ const timings = new TimingCollector();
223
+ const result = await timings.time("total", () => this.contextWithTimings(options, timings));
224
+ return withContextTimings(result, timings);
225
+ });
93
226
  }
94
227
  async contextWithTimings(options, timings) {
95
228
  const request = normalizeContextRequest(options);
96
- if (options.collection) {
97
- return this.contextFromNamedCollection(request, options, timings);
98
- }
99
229
  const startRoot = resolveZvecGrepRoot(options.root ?? this.root);
100
230
  if (options.rg) {
101
231
  return this.contextFromRg(startRoot, request, options, timings);
102
232
  }
103
- assertNearestAnonymousHomeUnlocked(startRoot, "context");
104
- const nearest = findNearestAnonymousCollection(startRoot);
233
+ assertNearestWorkspaceHomeUnlocked(startRoot, "context");
234
+ const nearest = findNearestWorkspaceIndex(startRoot);
105
235
  if (nearest) {
106
236
  const { location, info } = nearest;
107
237
  if (info.indexPolicy === "disabled") {
108
- throw anonymousIndexDisabledError(location.root);
238
+ throw workspaceIndexDisabledError(location.root);
109
239
  }
110
- if (!isCollectionIndexed(info)) {
111
- throw anonymousIndexMissingError(location.root, info.indexPolicy ?? "enabled");
240
+ if (!isWorkspaceIndexed(info) || !hasWorkspaceIndex(location)) {
241
+ throw workspaceIndexMissingError(location.root, info.indexPolicy ?? "enabled");
112
242
  }
113
243
  if (options.autoUpdate !== false) {
114
- await this.refreshAnonymousIndexForContext(location, options, timings);
244
+ await this.refreshWorkspaceIndexForContext(location, options, timings);
115
245
  }
116
- return await withHomeReadLock(location.home, "context", () => this.contextFromAnonymousIndex(location, request, options, timings));
246
+ return await withHomeReadLock(location.home, "context", () => this.contextFromWorkspaceIndex(location, request, options, timings));
117
247
  }
118
- throw anonymousIndexMissingError(startRoot, "undecided");
248
+ throw workspaceIndexMissingError(startRoot, "undecided");
119
249
  }
120
250
  async info(options = {}) {
121
251
  this.ensureOpen();
122
252
  const startRoot = resolveZvecGrepRoot(options.root ?? this.root);
123
- assertNearestAnonymousHomeUnlocked(startRoot, "info");
124
- const nearest = findNearestAnonymousCollection(startRoot);
253
+ assertNearestWorkspaceHomeUnlocked(startRoot, "info");
254
+ const nearest = findNearestWorkspaceIndex(startRoot);
125
255
  if (!nearest) {
126
- const location = anonymousIndexLocation(startRoot);
256
+ const location = workspaceIndexLocation(startRoot);
127
257
  return {
128
258
  root: startRoot,
129
259
  indexed: false,
130
260
  indexPolicy: "undecided",
131
261
  home: location.home,
132
- indexPath: location.collectionPath,
262
+ indexPath: location.indexPath,
133
263
  source: "unindexed",
134
- suggestion: "zg --index or zg --rg",
264
+ suggestion: "zg index or zg query --rg",
135
265
  };
136
266
  }
137
267
  return await withHomeReadLock(nearest.location.home, "info", async () => {
138
- const registry = new CollectionRegistry(nearest.location.home, undefined, true);
139
- try {
140
- const collection = registry.get(ANONYMOUS_COLLECTION_NAME);
141
- const indexed = collection !== null
142
- && collection.indexPolicy !== "disabled"
143
- && isCollectionIndexed(collection);
144
- return {
145
- root: nearest.location.root,
146
- indexed,
147
- indexPolicy: collection?.indexPolicy ?? "undecided",
148
- home: nearest.location.home,
149
- indexPath: nearest.location.collectionPath,
150
- source: indexed ? "index" : "unindexed",
151
- collection: collection ?? undefined,
152
- status: indexed ? await registry.status(ANONYMOUS_COLLECTION_NAME) : null,
153
- suggestion: anonymousInfoSuggestion(collection),
154
- };
155
- }
156
- finally {
157
- registry.close();
158
- }
268
+ const workspaceIndex = readWorkspaceManifest(nearest.location.home);
269
+ const indexed = workspaceIndex !== null &&
270
+ workspaceIndex.indexPolicy !== "disabled" &&
271
+ isWorkspaceIndexed(workspaceIndex) &&
272
+ hasWorkspaceIndex(nearest.location);
273
+ return {
274
+ root: nearest.location.root,
275
+ indexed,
276
+ indexPolicy: workspaceIndex?.indexPolicy ?? "undecided",
277
+ home: nearest.location.home,
278
+ indexPath: nearest.location.indexPath,
279
+ source: indexed ? "index" : "unindexed",
280
+ workspaceIndex: workspaceIndex
281
+ ? workspaceIndexInfoFromManifest(workspaceIndex)
282
+ : undefined,
283
+ status: indexed && options.includeStatus !== false
284
+ ? await workspaceIndexStatus(workspaceIndex, nearest.location)
285
+ : null,
286
+ suggestion: workspaceInfoSuggestion(workspaceIndex),
287
+ };
159
288
  });
160
289
  }
161
290
  async close() {
162
291
  const models = new Set([
163
- ...(this.embeddingModel ? [this.embeddingModel] : []),
292
+ ...(this.embeddingModel &&
293
+ this.options.embeddingModelOwnership !== "borrowed"
294
+ ? [this.embeddingModel]
295
+ : []),
164
296
  ...this.recoveredEmbeddingModels.values(),
297
+ ...this.retiredEmbeddingModels,
165
298
  ]);
166
299
  this.recoveredEmbeddingModels.clear();
300
+ this.retiredEmbeddingModels.clear();
167
301
  for (const model of models) {
168
302
  await model.dispose();
169
303
  }
170
304
  this.closed = true;
171
305
  }
172
- async listCollections() {
173
- this.ensureOpen();
174
- return await withHomeReadLock(serviceHome(this.options), "collections.list", async () => {
175
- const registry = this.createRegistry(true);
176
- try {
177
- return registry.list();
178
- }
179
- finally {
180
- registry.close();
181
- }
182
- });
183
- }
184
- async collectionInfo(name) {
185
- this.ensureOpen();
186
- return await withHomeReadLock(serviceHome(this.options), "collections.info", async () => {
187
- const registry = this.createRegistry(true);
188
- try {
189
- return registry.get(name);
190
- }
191
- finally {
192
- registry.close();
193
- }
194
- });
195
- }
196
- async collectionStatus(name) {
197
- this.ensureOpen();
198
- return await withHomeReadLock(serviceHome(this.options), "collections.status", async () => {
199
- const registry = this.createRegistry(true);
200
- try {
201
- return await registry.status(name);
202
- }
203
- finally {
204
- registry.close();
205
- }
206
- });
207
- }
208
- async indexCollection(name, paths, options = {}) {
209
- this.ensureOpen();
210
- const home = serviceHome(this.options);
211
- return await withHomeWriteLock(home, options.rebuild ? "collections.index.rebuild" : "collections.index", async () => {
212
- const existing = readCollectionInfo(home, name);
213
- const embeddingModel = this.embeddingModelForIndex(existing, home, "collections.index");
214
- const registry = this.createRegistry(false, embeddingModel);
215
- try {
216
- const requestedRootPaths = paths === undefined
217
- ? undefined
218
- : Array.isArray(paths)
219
- ? paths
220
- : [paths];
221
- const rootPaths = resolveIndexRootPaths(existing, requestedRootPaths, this.root, {
222
- resetPaths: options.resetPaths === true,
223
- includePaths: options.includePaths,
224
- excludePaths: options.excludePaths,
225
- });
226
- if (options.rebuild) {
227
- registry.remove(name);
228
- }
229
- const existingAfterRebuild = registry.get(name);
230
- if (isCollectionIndexed(existingAfterRebuild)) {
231
- assertCollectionEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg --collections index <name> --rebuild");
232
- }
233
- registry.prepareIndex(name, rootPaths);
234
- return await registry.open(name).index({
235
- embeddingConcurrency: options.embeddingConcurrency,
236
- onProgress: options.onProgress,
237
- });
238
- }
239
- finally {
240
- registry.close();
241
- }
242
- });
243
- }
244
- async removeCollection(name) {
245
- this.ensureOpen();
246
- return await withHomeWriteLock(serviceHome(this.options), "collections.remove", async () => {
247
- const registry = this.createRegistry(false);
248
- try {
249
- return registry.remove(name);
250
- }
251
- finally {
252
- registry.close();
253
- }
254
- });
255
- }
256
- async contextFromAnonymousIndex(location, request, options, timings) {
257
- const registry = new CollectionRegistry(location.home, undefined, true);
306
+ async contextFromWorkspaceIndex(location, request, options, timings) {
307
+ const info = readWorkspaceManifest(location.home);
308
+ if (!info) {
309
+ throw new EngineError("Workspace index manifest not found", {
310
+ code: "ZVEC_GREP.ENGINE.MANIFEST.NOT_FOUND",
311
+ });
312
+ }
313
+ const workspaceIndex = this.openWorkspaceIndexForSearch(info, request, location, info.embeddingRuntime);
258
314
  try {
259
- const info = registry.get(ANONYMOUS_COLLECTION_NAME);
260
- if (!info) {
261
- throw new EngineError("Collection not found", {
262
- code: "ZVEC_GREP.ENGINE.COLLECTION.NOT_FOUND",
263
- });
264
- }
265
- const collection = this.openCollectionForSearch(info, request, location.home);
266
- try {
267
- return await this.contextFromCollection({
268
- root: location.root,
269
- request,
270
- collection,
271
- anonymous: true,
272
- options,
273
- timings,
274
- });
275
- }
276
- finally {
277
- collection.close();
278
- }
315
+ return await contextFromOpenWorkspaceIndex({
316
+ root: location.root,
317
+ request,
318
+ workspaceIndex,
319
+ options,
320
+ timings,
321
+ });
279
322
  }
280
323
  finally {
281
- registry.close();
324
+ workspaceIndex.close();
282
325
  }
283
326
  }
284
- async refreshAnonymousIndexForContext(location, options, timings) {
285
- const needsRefresh = await timings.time("status_scan", () => withHomeReadLock(location.home, "context.status", async () => {
286
- const registry = new CollectionRegistry(location.home, undefined, true);
287
- try {
288
- const status = await registry.status(ANONYMOUS_COLLECTION_NAME);
289
- return status ? collectionIndexStatusNeedsRefresh(status) : false;
290
- }
291
- finally {
292
- registry.close();
293
- }
294
- }));
295
- if (!needsRefresh) {
296
- return;
297
- }
298
- await timings.time("auto_update", () => withHomeWriteLock(location.home, "context.refresh", async () => {
299
- const existing = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
300
- if (!existing) {
301
- return;
302
- }
303
- const stillNeedsRefresh = await timings.time("refresh_status_scan", () => collectionNeedsRefresh(location.home, ANONYMOUS_COLLECTION_NAME));
304
- if (!stillNeedsRefresh) {
327
+ async refreshWorkspaceIndexForContext(location, options, timings) {
328
+ const daemonWritePermit = assertDaemonWriteAllowed(location.root, this.options.daemonInstanceToken);
329
+ try {
330
+ const needsRefresh = await timings.time("status_scan", () => withHomeReadLock(location.home, "context.status", async () => {
331
+ const manifest = readWorkspaceManifest(location.home);
332
+ const status = manifest
333
+ ? await workspaceIndexStatus(manifest, location)
334
+ : null;
335
+ return indexStatusNeedsRefresh(status);
336
+ }));
337
+ if (!needsRefresh) {
305
338
  return;
306
339
  }
307
- const embeddingModel = this.embeddingModelForIndex(existing, location.home, "context.refresh");
308
- assertCollectionEmbeddingMatchesCurrentModel(existing, embeddingModel, "zg --index --rebuild");
309
- const registry = new CollectionRegistry(location.home, embeddingModel);
310
- try {
311
- const result = await registry.open(ANONYMOUS_COLLECTION_NAME).index({
312
- embeddingConcurrency: options.embeddingConcurrency,
313
- onProgress: options.onAutoUpdateProgress,
314
- });
315
- timings.addEntries(result.timings, "auto_update_");
316
- }
317
- finally {
318
- registry.close();
319
- }
320
- }));
321
- }
322
- async contextFromNamedCollection(request, options, timings) {
323
- const collectionName = options.collection;
324
- return await withHomeReadLock(serviceHome(this.options), "collection.context", async () => {
325
- const registry = this.createRegistry(true, undefined);
326
- try {
327
- const info = registry.get(collectionName);
328
- if (!info) {
329
- throw new EngineError("Collection not found", {
330
- code: "ZVEC_GREP.ENGINE.COLLECTION.NOT_FOUND",
331
- context: errorDetails([collectionDetail(collectionName)]),
332
- });
340
+ await timings.time("auto_update", () => withHomeWriteLock(location.home, "context.refresh", async () => {
341
+ const existing = readWorkspaceManifest(location.home);
342
+ if (!existing) {
343
+ return;
344
+ }
345
+ const stillNeedsRefresh = await timings.time("refresh_status_scan", () => workspaceIndexNeedsRefresh(location));
346
+ if (!stillNeedsRefresh) {
347
+ return;
333
348
  }
334
- const collection = this.openCollectionForSearch(info, request, registry.home);
335
- const root = collection.info.rootPaths[0]?.absolutePath ?? resolveZvecGrepRoot(options.root ?? this.root);
349
+ const workspaceRuntime = existing.embeddingRuntime;
350
+ const embeddingModel = this.embeddingModelForIndex(existing, "context.refresh", workspaceRuntime);
351
+ assertWorkspaceEndpointMatchesCurrentRuntime(existing, workspaceRuntime, effectiveEmbeddingRuntime(this.options, embeddingModel, workspaceRuntime), "zg index --endpoint <url> --rebuild");
352
+ assertWorkspaceEmbeddingMatchesCurrentModel(existing, embeddingModel, "zg index --rebuild");
353
+ const workspaceIndex = new WorkspaceIndex(existing, embeddingModel, false, location.filesPath);
336
354
  try {
337
- return await this.contextFromCollection({
338
- root,
339
- request,
340
- collection,
341
- anonymous: false,
342
- options: {
343
- ...options,
344
- fallback: "disabled",
345
- },
346
- timings,
355
+ const result = await workspaceIndex.index({
356
+ embeddingConcurrency: options.embeddingConcurrency,
357
+ onProgress: options.onAutoUpdateProgress,
347
358
  });
359
+ timings.addEntries(result.timings, "auto_update_");
348
360
  }
349
361
  finally {
350
- collection.close();
362
+ workspaceIndex.close();
351
363
  }
352
- }
353
- finally {
354
- registry.close();
355
- }
356
- });
357
- }
358
- async contextFromCollection(input) {
359
- const searches = [];
360
- const limit = contextGroupLimit(input.options.limit, input.request.groups.length);
361
- for (const group of input.request.groups) {
362
- const search = await input.collection.searchPlan({
363
- routes: group.routes,
364
- limit,
365
- trace: input.options.trace,
366
- preferSymbol: input.options.preferSymbol,
367
- symbolTypes: input.options.symbolTypes,
368
- includePaths: input.options.includePaths,
369
- excludePaths: input.options.excludePaths,
370
- modifiedAfter: input.options.modifiedAfter,
371
- modifiedBefore: input.options.modifiedBefore,
372
- });
373
- input.timings.addEntries(search.timings);
374
- searches.push(search);
364
+ }));
365
+ }
366
+ finally {
367
+ daemonWritePermit?.release();
375
368
  }
376
- const items = dedupeAndRerankContextItems(searches.flatMap((search) => searchPlanToContextItems(search, input.root)));
377
- return {
378
- query: input.request.displayQuery,
379
- root: input.root,
380
- source: "index",
381
- coverage: "ranked_sample",
382
- collection: {
383
- id: input.collection.info.id,
384
- name: input.collection.info.name,
385
- path: input.collection.info.path,
386
- anonymous: input.anonymous,
387
- },
388
- items,
389
- diagnostics: {
390
- emptyReason: items.length === 0 ? "no_matches" : undefined,
391
- index: {
392
- hitsReturned: items.length,
393
- routes: searches.flatMap((search) => search.plan.routes),
394
- },
395
- },
396
- };
397
369
  }
398
- async contextFromLexicalFallback(root, request, options, timings) {
399
- const fallbackResults = [];
400
- const limit = contextGroupLimit(options.limit, request.groups.length);
401
- for (const query of request.fallbackQueries) {
402
- fallbackResults.push(await timings.time("fallback_search", () => runLexicalFallback({
370
+ async contextFromWriterWorkspaceIndex(root, workspaceIndex, options) {
371
+ return await this.withEmbeddingModelOperation(async () => {
372
+ const timings = new TimingCollector();
373
+ const request = normalizeContextRequest(options);
374
+ const result = await timings.time("total", () => contextFromOpenWorkspaceIndex({
403
375
  root,
404
- query,
405
- limit,
406
- includePaths: options.includePaths,
407
- excludePaths: options.excludePaths,
408
- modifiedAfter: options.modifiedAfter,
409
- modifiedBefore: options.modifiedBefore,
410
- rgOptions: options.rgOptions,
411
- })));
412
- }
413
- const items = dedupeAndRerankContextItems(fallbackResults.flatMap((fallback) => fallback.items));
414
- const diagnostics = mergeFallbackDiagnostics(fallbackResults);
415
- return {
416
- query: request.displayQuery,
417
- root,
418
- source: "lexical_fallback",
419
- coverage: diagnostics.truncated ? "lexical_truncated" : "lexical_exhaustive",
420
- items,
421
- diagnostics: {
422
- emptyReason: items.length === 0 ? "no_matches" : undefined,
423
- fallback: diagnostics,
424
- },
425
- };
376
+ request,
377
+ workspaceIndex,
378
+ options: { ...options, autoUpdate: false },
379
+ timings,
380
+ }));
381
+ return withContextTimings(result, timings);
382
+ });
426
383
  }
427
384
  async contextFromRg(root, request, options, timings) {
428
385
  let rgResult;
429
386
  try {
430
387
  rgResult = await timings.time("rg_search", () => runRgSearch({
431
388
  root,
432
- patterns: request.fallbackQueries,
389
+ patterns: request.rgPatterns,
433
390
  paths: options.rgPaths,
434
391
  limit: options.limit,
435
392
  includePaths: options.includePaths,
436
393
  excludePaths: options.excludePaths,
394
+ globs: options.globs,
395
+ insensitiveGlobs: options.insensitiveGlobs,
396
+ fileTypes: options.fileTypes,
397
+ excludedFileTypes: options.excludedFileTypes,
398
+ hidden: options.hidden,
399
+ noIgnore: options.noIgnore,
400
+ ignoreFiles: options.ignoreFiles,
401
+ maxDepth: options.maxDepth,
402
+ maxFileSizeBytes: options.maxFileSizeBytes,
403
+ follow: options.follow,
437
404
  modifiedAfter: options.modifiedAfter,
438
405
  modifiedBefore: options.modifiedBefore,
439
406
  rgOptions: options.rgOptions,
@@ -442,72 +409,142 @@ class ZvecGrepService {
442
409
  catch (cause) {
443
410
  throw new EngineError("Search failed", {
444
411
  code: "ZVEC_GREP.ENGINE.SEARCH.FAILED",
445
- context: errorDetails([
446
- detail("source", "rg"),
447
- detail("root", root),
448
- ]),
412
+ context: errorDetails([detail("source", "rg"), detail("root", root)]),
449
413
  cause,
450
414
  });
451
415
  }
452
416
  const structuralEnrichment = await timings.time("structure_enrichment", () => enrichLexicalItemsWithStructure(root, rgResult.items));
453
417
  const items = dedupeAndRerankContextItems(structuralEnrichment.items);
454
- const emptyReason = items.length === 0
455
- ? rgEmptyReason(rgResult.diagnostics)
456
- : undefined;
418
+ const emptyReason = items.length === 0 ? rgEmptyReason(rgResult.diagnostics) : undefined;
457
419
  return {
458
420
  query: request.displayQuery,
459
421
  root,
460
422
  source: "rg",
461
- coverage: rgResult.diagnostics.truncated ? "lexical_truncated" : "lexical_exhaustive",
423
+ coverage: rgResult.diagnostics.truncated
424
+ ? "rg_truncated"
425
+ : "rg_exhaustive",
462
426
  items,
463
427
  diagnostics: {
464
428
  emptyReason,
465
- fallback: rgResult.diagnostics,
429
+ rg: rgResult.diagnostics,
466
430
  structure: structuralEnrichment.diagnostics,
467
431
  },
468
432
  };
469
433
  }
470
- createRegistry(readOnly, embeddingModel = this.embeddingModel) {
471
- return new CollectionRegistry(this.options.home, embeddingModel, readOnly);
434
+ openWorkspaceIndexForSearch(info, request, location, workspaceRuntime) {
435
+ return new WorkspaceIndex(info, this.embeddingModelForSearch(indexedEmbeddingSchema(info), request, workspaceRuntime, info), true, location.filesPath);
472
436
  }
473
- openCollectionForSearch(info, request, registryHome) {
474
- return new Collection(info, this.embeddingModelForSearch(indexedEmbeddingSchema(info), request, registryHome), true, join(registryHome, "files.zvec"));
475
- }
476
- embeddingModelForSearch(schema, request, registryHome) {
437
+ embeddingModelForSearch(schema, request, workspaceRuntime, info) {
477
438
  if (!request.routes.some((route) => route.mode === "vector")) {
478
439
  return undefined;
479
440
  }
441
+ const identity = {
442
+ provider: schema.provider,
443
+ name: schema.model,
444
+ };
445
+ const effectiveRuntime = resolveEmbeddingRuntimeOptions(embeddingModelReference(identity), this.options, workspaceRuntime, readGlobalConfig());
446
+ assertSearchEndpointMatchesWorkspace(info, workspaceRuntime, effectiveRuntime);
480
447
  if (this.embeddingModel) {
481
448
  return this.embeddingModel;
482
449
  }
483
- return this.recoverEmbeddingModel(schema, registryHome);
450
+ return this.recoverEmbeddingModel(schema, workspaceRuntime);
484
451
  }
485
- embeddingModelForIndex(existing, registryHome, operation) {
486
- if (isCollectionIndexed(existing) && !this.embeddingModelExplicit) {
487
- return this.recoverEmbeddingModel(existing.embedding, registryHome);
452
+ embeddingModelForIndex(existing, operation, workspaceRuntime = {}) {
453
+ if (isWorkspaceIndexed(existing) &&
454
+ !this.embeddingModel &&
455
+ !this.options.embedding) {
456
+ return this.recoverEmbeddingModel(existing.embedding, workspaceRuntime);
488
457
  }
489
- return this.requireEmbeddingModel(operation);
490
- }
491
- recoverEmbeddingModel(schema, registryHome) {
492
- const key = `${schema.provider}/${schema.model}/${modelCacheDir(this.options, this.root, registryHome)}`;
458
+ const config = readGlobalConfig();
459
+ const reference = resolveEmbeddingReference({
460
+ explicit: this.options.embedding,
461
+ globalDefault: config.defaults?.embedding,
462
+ fallback: this.options.defaultEmbedding === true
463
+ ? DEFAULT_LOCAL_EMBEDDING
464
+ : undefined,
465
+ });
466
+ const referenceIdentity = reference
467
+ ? parseEmbeddingModelReference(reference)
468
+ : undefined;
469
+ const selectedWorkspaceRuntime = isWorkspaceIndexed(existing) &&
470
+ referenceIdentity &&
471
+ existing.embedding.provider !== referenceIdentity.provider
472
+ ? {}
473
+ : workspaceRuntime;
474
+ return (this.embeddingModel ??
475
+ (reference
476
+ ? this.embeddingModelFromReference(reference, config, selectedWorkspaceRuntime)
477
+ : undefined) ??
478
+ this.requireEmbeddingModel(operation));
479
+ }
480
+ recoverEmbeddingModel(schema, workspaceRuntime = {}) {
481
+ const config = readGlobalConfig();
482
+ const identity = {
483
+ provider: schema.provider,
484
+ name: schema.model,
485
+ };
486
+ const reference = embeddingModelReference(identity);
487
+ const options = providerOptions(this.options, identity, config, workspaceRuntime);
488
+ const key = `${reference}/${providerOptionsFingerprint(options)}`;
489
+ return this.cachedEmbeddingModel(key, () => createServiceEmbeddingModel(reference, options, this.options));
490
+ }
491
+ embeddingModelFromReference(reference, config = readGlobalConfig(), workspaceRuntime = {}) {
492
+ const identity = parseEmbeddingModelReference(reference);
493
+ const options = providerOptions(this.options, identity, config, workspaceRuntime);
494
+ const key = `configured/${reference}/${providerOptionsFingerprint(options)}`;
495
+ return this.cachedEmbeddingModel(key, () => createServiceEmbeddingModel(reference, options, this.options));
496
+ }
497
+ cachedEmbeddingModel(key, create) {
493
498
  const cached = this.recoveredEmbeddingModels.get(key);
494
499
  if (cached) {
500
+ this.recoveredEmbeddingModels.delete(key);
501
+ this.recoveredEmbeddingModels.set(key, cached);
495
502
  return cached;
496
503
  }
497
- const model = createEmbeddingModel({
498
- provider: schema.provider,
499
- model: schema.model,
500
- }, providerOptions(this.options, this.root, registryHome));
504
+ const model = create();
501
505
  this.recoveredEmbeddingModels.set(key, model);
506
+ this.trimRecoveredEmbeddingModels();
502
507
  return model;
503
508
  }
509
+ trimRecoveredEmbeddingModels() {
510
+ while (this.recoveredEmbeddingModels.size > MAX_RECOVERED_EMBEDDING_MODELS) {
511
+ const oldestKey = this.recoveredEmbeddingModels.keys().next().value;
512
+ if (oldestKey === undefined) {
513
+ return;
514
+ }
515
+ const model = this.recoveredEmbeddingModels.get(oldestKey);
516
+ this.recoveredEmbeddingModels.delete(oldestKey);
517
+ if (model) {
518
+ this.retiredEmbeddingModels.add(model);
519
+ }
520
+ }
521
+ }
522
+ async withEmbeddingModelOperation(operation) {
523
+ this.activeEmbeddingModelOperations += 1;
524
+ try {
525
+ return await operation();
526
+ }
527
+ finally {
528
+ this.activeEmbeddingModelOperations -= 1;
529
+ if (this.activeEmbeddingModelOperations === 0) {
530
+ await this.disposeRetiredEmbeddingModels();
531
+ }
532
+ }
533
+ }
534
+ async disposeRetiredEmbeddingModels() {
535
+ const models = [...this.retiredEmbeddingModels];
536
+ this.retiredEmbeddingModels.clear();
537
+ for (const model of models) {
538
+ await model.dispose();
539
+ }
540
+ }
504
541
  requireEmbeddingModel(operation) {
505
542
  if (!this.embeddingModel) {
506
543
  throw new EngineError("zvec-grep operation requires an embedding model", {
507
544
  code: "ZVEC_GREP.ENGINE.SERVICE.EMBEDDING_MODEL_REQUIRED",
508
545
  context: errorDetails([
509
546
  detail("operation", operation),
510
- detail("hint", 'Pass "--embedding <model>" or set ZVEC_GREP_EMBEDDING. Existing indexes can rerun --index without --embedding to reuse the stored schema.'),
547
+ detail("hint", `Pass "--embedding <model>", set ZVEC_GREP_EMBEDDING, or configure defaults.embedding in ${globalConfigPath()}. Existing indexes can run "zg index" without --embedding to reuse the stored schema.`),
511
548
  detail("examples", "local/embeddinggemma-300m, qwen/text-embedding-v4"),
512
549
  ]),
513
550
  });
@@ -522,6 +559,66 @@ class ZvecGrepService {
522
559
  }
523
560
  }
524
561
  }
562
+ async function contextFromOpenWorkspaceIndex(input) {
563
+ const searches = [];
564
+ const groups = input.options.fuse
565
+ ? [
566
+ {
567
+ id: "Q1",
568
+ query: input.request.displayQuery,
569
+ role: input.request.groups.some((group) => group.role === "primary")
570
+ ? "primary"
571
+ : "supplemental",
572
+ routes: input.request.routes,
573
+ },
574
+ ]
575
+ : input.request.groups;
576
+ const limit = contextGroupLimit(input.options.limit, groups.length);
577
+ for (const group of groups) {
578
+ const search = await input.workspaceIndex.searchPlan({
579
+ routes: group.routes,
580
+ limit,
581
+ trace: input.options.trace,
582
+ preferSymbol: input.options.preferSymbol,
583
+ symbolTypes: input.options.symbolTypes,
584
+ includePaths: input.options.includePaths,
585
+ excludePaths: input.options.excludePaths,
586
+ globs: input.options.globs,
587
+ insensitiveGlobs: input.options.insensitiveGlobs,
588
+ fileTypes: input.options.fileTypes,
589
+ excludedFileTypes: input.options.excludedFileTypes,
590
+ modifiedAfter: input.options.modifiedAfter,
591
+ modifiedBefore: input.options.modifiedBefore,
592
+ });
593
+ input.timings.addEntries(search.timings);
594
+ searches.push(search);
595
+ }
596
+ const items = selectAndRankContextItems(searches.flatMap((search, index) => searchPlanToContextItems(search, input.root, groups[index])), groups.filter((group) => group.role === "primary").map((group) => group.id));
597
+ return {
598
+ query: input.request.displayQuery,
599
+ root: input.root,
600
+ source: "index",
601
+ coverage: "ranked_sample",
602
+ workspaceIndex: {
603
+ id: input.workspaceIndex.info.id,
604
+ name: input.workspaceIndex.info.name,
605
+ path: input.workspaceIndex.info.path,
606
+ },
607
+ items,
608
+ diagnostics: {
609
+ emptyReason: items.length === 0 ? "no_matches" : undefined,
610
+ index: {
611
+ hitsReturned: items.length,
612
+ queryGroups: groups.map((group) => ({
613
+ id: group.id,
614
+ query: group.query,
615
+ role: group.role,
616
+ })),
617
+ routes: searches.flatMap((search) => search.plan.routes),
618
+ },
619
+ },
620
+ };
621
+ }
525
622
  async function withHomeReadLock(home, operation, task) {
526
623
  const lock = acquireHomeLock(home, "read", operation);
527
624
  try {
@@ -540,22 +637,12 @@ async function withHomeWriteLock(home, operation, task) {
540
637
  lock.release();
541
638
  }
542
639
  }
543
- async function collectionNeedsRefresh(home, collectionName) {
544
- const registry = new CollectionRegistry(home, undefined, true);
545
- try {
546
- const status = await registry.status(collectionName);
547
- return status ? collectionIndexStatusNeedsRefresh(status) : false;
548
- }
549
- finally {
550
- registry.close();
551
- }
552
- }
553
- function collectionIndexStatusNeedsRefresh(status) {
554
- return status.filesAdded > 0
555
- || status.filesModified > 0
556
- || status.filesDeleted > 0
557
- || status.filesPending > 0
558
- || status.filesFailed > 0;
640
+ async function workspaceIndexNeedsRefresh(location) {
641
+ const manifest = readWorkspaceManifest(location.home);
642
+ const status = manifest
643
+ ? await workspaceIndexStatus(manifest, location)
644
+ : null;
645
+ return indexStatusNeedsRefresh(status);
559
646
  }
560
647
  function acquireHomeLock(home, mode, operation) {
561
648
  return acquireReadWriteLock(homeLockPath(home), mode, { operation });
@@ -563,10 +650,10 @@ function acquireHomeLock(home, mode, operation) {
563
650
  function assertHomeUnlocked(home, operation) {
564
651
  assertNoWriteLock(homeLockPath(home), operation);
565
652
  }
566
- function assertNearestAnonymousHomeUnlocked(start, operation) {
653
+ function assertNearestWorkspaceHomeUnlocked(start, operation) {
567
654
  let current = resolve(start);
568
655
  while (true) {
569
- assertHomeUnlocked(anonymousHome(current), operation);
656
+ assertHomeUnlocked(workspaceHome(current), operation);
570
657
  const parent = dirname(current);
571
658
  if (parent === current) {
572
659
  return;
@@ -577,13 +664,11 @@ function assertNearestAnonymousHomeUnlocked(start, operation) {
577
664
  function homeLockPath(home) {
578
665
  return join(home, "locks", "home");
579
666
  }
580
- function serviceHome(options) {
581
- return options.home ?? defaultHome();
582
- }
583
667
  function rgEmptyReason(diagnostics) {
584
- return diagnostics.missingPaths && diagnostics.missingPaths.length > 0
585
- && diagnostics.searchedPaths
586
- && diagnostics.searchedPaths.length === 0
668
+ return diagnostics.missingPaths &&
669
+ diagnostics.missingPaths.length > 0 &&
670
+ diagnostics.searchedPaths &&
671
+ diagnostics.searchedPaths.length === 0
587
672
  ? "no_searchable_files"
588
673
  : "no_matches";
589
674
  }
@@ -600,71 +685,106 @@ function withContextTimings(result, timings) {
600
685
  },
601
686
  };
602
687
  }
603
- function findNearestAnonymousCollection(start) {
604
- let current = resolve(start);
605
- while (true) {
606
- const location = anonymousIndexLocation(current);
607
- const info = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
608
- if (info) {
609
- return { location, info };
610
- }
611
- const parent = dirname(current);
612
- if (parent === current) {
613
- return null;
614
- }
615
- current = parent;
616
- }
688
+ function findNearestWorkspaceIndex(start) {
689
+ const location = findNearestWorkspace(start);
690
+ if (!location)
691
+ return null;
692
+ const info = readWorkspaceManifest(location.home);
693
+ return info ? { location, info } : null;
617
694
  }
618
- function anonymousInfoSuggestion(collection) {
619
- if (!collection) {
620
- return "zg --index or zg --rg";
695
+ function workspaceInfoSuggestion(workspaceIndex) {
696
+ if (!workspaceIndex) {
697
+ return "zg index or zg query --rg";
621
698
  }
622
- if (collection.indexPolicy === "disabled") {
623
- return "zg --rg";
699
+ if (workspaceIndex.indexPolicy === "disabled") {
700
+ return "zg query --rg";
624
701
  }
625
- if (!isCollectionIndexed(collection)) {
626
- return "zg --index";
702
+ if (!isWorkspaceIndexed(workspaceIndex)) {
703
+ return "zg index";
627
704
  }
628
705
  return undefined;
629
706
  }
630
- function anonymousIndexMissingError(root, policy) {
631
- return new EngineError("No anonymous zvec-grep index found for this workspace", {
632
- code: "ZVEC_GREP.ENGINE.SERVICE.ANONYMOUS_INDEX_NOT_FOUND",
707
+ function workspaceIndexMissingError(root, policy) {
708
+ return new EngineError("No zvec-grep index found for this workspace", {
709
+ code: "ZVEC_GREP.ENGINE.SERVICE.WORKSPACE_INDEX_NOT_FOUND",
633
710
  context: errorDetails([
634
711
  detail("root", root),
635
712
  detail("policy", policy),
636
713
  detail("hint", policy === "undecided"
637
- ? "Ask the user whether to build an index with zg --index, or use zg --rg for no-index search."
638
- : "Run zg --index to build the enabled workspace index, or use zg --rg for no-index search."),
714
+ ? "Ask the user whether to build an index with zg index, or use zg query --rg for no-index search."
715
+ : "Run zg index to build the enabled workspace index, or use zg query --rg for no-index search."),
639
716
  detail("agent_prompt", policy === "undecided"
640
- ? "Ask the user whether this workspace should be indexed. If yes, run zg --index --embedding <model> with appropriate include/exclude filters; if no, run zg --disable-index. For immediate no-index search, use zg --rg."
641
- : "This workspace is marked index-enabled but has no built index. Ask before running zg --index if an embedding model or cost is involved; otherwise use zg --rg for immediate no-index search."),
717
+ ? "Ask the user whether this workspace should be indexed. If yes, run zg index --embedding <model> with appropriate -g/--glob and -t/--type filters; otherwise use zg query --rg for immediate no-index search."
718
+ : "This workspace is marked index-enabled but has no built index. Ask before running zg index if an embedding model or cost is involved; otherwise use zg query --rg for immediate no-index search."),
642
719
  ]),
643
720
  });
644
721
  }
645
- function anonymousIndexDisabledError(root) {
646
- return new EngineError("Anonymous zvec-grep index is disabled for this workspace", {
647
- code: "ZVEC_GREP.ENGINE.SERVICE.ANONYMOUS_INDEX_DISABLED",
722
+ function workspaceIndexDisabledError(root) {
723
+ return new EngineError("The zvec-grep index is disabled for this workspace", {
724
+ code: "ZVEC_GREP.ENGINE.SERVICE.WORKSPACE_INDEX_DISABLED",
648
725
  context: errorDetails([
649
726
  detail("root", root),
650
727
  detail("policy", "disabled"),
651
- detail("hint", "Use zg --rg for no-index search. Run zg --index only if the user explicitly decides to index this workspace."),
728
+ detail("hint", "Use zg query --rg for no-index search. Run zg index only if the user explicitly decides to index this workspace."),
652
729
  detail("agent_action", "do_not_build_index"),
653
730
  ]),
654
731
  });
655
732
  }
656
733
  function resolveIndexRootPaths(existing, requested, fallbackRoot, options) {
657
734
  let rootPaths = requested
658
- ?? existing?.rootPaths
659
- ?? [fallbackRoot];
735
+ ? inheritRequestedRootPathSettings(requested, existing?.rootPaths)
736
+ : (existing?.rootPaths ?? [fallbackRoot]);
660
737
  if (options.resetPaths) {
661
738
  rootPaths = rootPaths.map(resetRootPathFilters);
662
739
  }
663
- if (options.includePaths !== undefined || options.excludePaths !== undefined) {
664
- rootPaths = rootPaths.map((rootPath) => applyRootPathFilterOverrides(rootPath, options.includePaths, options.excludePaths));
740
+ if (options.includePaths !== undefined ||
741
+ options.excludePaths !== undefined ||
742
+ options.globs !== undefined ||
743
+ options.insensitiveGlobs !== undefined ||
744
+ options.fileTypes !== undefined ||
745
+ options.excludedFileTypes !== undefined ||
746
+ options.hidden !== undefined ||
747
+ options.noIgnore !== undefined ||
748
+ options.ignoreFiles !== undefined ||
749
+ options.maxDepth !== undefined ||
750
+ options.maxFileSizeBytes !== undefined ||
751
+ options.follow !== undefined) {
752
+ rootPaths = rootPaths.map((rootPath) => applyRootPathOverrides(rootPath, options));
665
753
  }
666
754
  return rootPaths;
667
755
  }
756
+ function inheritRequestedRootPathSettings(requested, existing) {
757
+ if (!existing?.length) {
758
+ return requested;
759
+ }
760
+ return requested.map((rootPath) => {
761
+ const absolutePath = normalizePath(typeof rootPath === "string" ? rootPath : rootPath.absolutePath);
762
+ const inherited = existing.find((candidate) => normalizePath(candidate.absolutePath) === absolutePath);
763
+ if (!inherited) {
764
+ return rootPath;
765
+ }
766
+ if (typeof rootPath === "string") {
767
+ return { ...inherited, absolutePath };
768
+ }
769
+ return {
770
+ ...inherited,
771
+ ...rootPath,
772
+ absolutePath,
773
+ include: rootPath.include ?? inherited.include,
774
+ exclude: rootPath.exclude ?? inherited.exclude,
775
+ globs: rootPath.globs ?? inherited.globs,
776
+ insensitiveGlobs: rootPath.insensitiveGlobs ?? inherited.insensitiveGlobs,
777
+ fileTypes: rootPath.fileTypes ?? inherited.fileTypes,
778
+ excludedFileTypes: rootPath.excludedFileTypes ?? inherited.excludedFileTypes,
779
+ hidden: rootPath.hidden ?? inherited.hidden,
780
+ noIgnore: rootPath.noIgnore ?? inherited.noIgnore,
781
+ ignoreFiles: rootPath.ignoreFiles ?? inherited.ignoreFiles,
782
+ maxDepth: rootPath.maxDepth ?? inherited.maxDepth,
783
+ maxFileSizeBytes: rootPath.maxFileSizeBytes ?? inherited.maxFileSizeBytes,
784
+ follow: rootPath.follow ?? inherited.follow,
785
+ };
786
+ });
787
+ }
668
788
  function resetRootPathFilters(rootPath) {
669
789
  if (typeof rootPath === "string") {
670
790
  return rootPath;
@@ -674,7 +794,7 @@ function resetRootPathFilters(rootPath) {
674
794
  recursive: rootPath.recursive,
675
795
  };
676
796
  }
677
- function applyRootPathFilterOverrides(rootPath, include, exclude) {
797
+ function applyRootPathOverrides(rootPath, options) {
678
798
  const normalized = typeof rootPath === "string"
679
799
  ? {
680
800
  absolutePath: rootPath,
@@ -683,118 +803,230 @@ function applyRootPathFilterOverrides(rootPath, include, exclude) {
683
803
  : rootPath;
684
804
  return {
685
805
  ...normalized,
686
- include: include ?? normalized.include,
687
- exclude: exclude ?? normalized.exclude,
806
+ include: options.includePaths ?? normalized.include,
807
+ exclude: options.excludePaths ?? normalized.exclude,
808
+ globs: options.globs ?? normalized.globs,
809
+ insensitiveGlobs: options.insensitiveGlobs ?? normalized.insensitiveGlobs,
810
+ fileTypes: options.fileTypes ?? normalized.fileTypes,
811
+ excludedFileTypes: options.excludedFileTypes ?? normalized.excludedFileTypes,
812
+ hidden: options.hidden ?? normalized.hidden,
813
+ noIgnore: options.noIgnore ?? normalized.noIgnore,
814
+ ignoreFiles: options.ignoreFiles ?? normalized.ignoreFiles,
815
+ maxDepth: options.maxDepth ?? normalized.maxDepth,
816
+ maxFileSizeBytes: options.maxFileSizeBytes ?? normalized.maxFileSizeBytes,
817
+ follow: options.follow ?? normalized.follow,
688
818
  };
689
819
  }
690
- function resolveEmbeddingModel(options, root) {
691
- if (options.embeddingModel) {
692
- return options.embeddingModel;
693
- }
694
- if (options.embedding) {
695
- return createEmbeddingModelFromReference(options.embedding, providerOptions(options, root));
696
- }
697
- if (options.defaultEmbedding === true) {
698
- return createEmbeddingModelFromReference(DEFAULT_LOCAL_EMBEDDING, providerOptions(options, root));
699
- }
700
- return undefined;
820
+ function prepareWorkspaceManifest(location, existing, rootPaths, embeddingModel, embeddingRuntime) {
821
+ const now = Date.now();
822
+ return {
823
+ manifestVersion: CURRENT_MANIFEST_VERSION,
824
+ id: existing?.id ?? randomUUID(),
825
+ name: existing?.name ?? workspaceDisplayName(location.root),
826
+ path: location.home,
827
+ rootPaths: validateRootPaths(rootPaths),
828
+ indexPolicy: "enabled",
829
+ embedding: currentEmbeddingSchema(embeddingModel),
830
+ indexVersion: CURRENT_INDEX_VERSION,
831
+ createdTime: existing?.createdTime ?? now,
832
+ updatedTime: now,
833
+ embeddingRuntime,
834
+ };
701
835
  }
702
- function hasExplicitEmbeddingModel(options) {
703
- return options.embeddingModel !== undefined
704
- || options.embedding !== undefined;
836
+ function currentEmbeddingSchema(embeddingModel) {
837
+ return {
838
+ provider: embeddingModel.info.provider,
839
+ model: embeddingModel.info.name,
840
+ dimension: embeddingModel.info.dimension,
841
+ metric: embeddingModel.info.metric,
842
+ };
705
843
  }
706
- function readCollectionInfo(home, name) {
707
- const registry = new CollectionRegistry(home, undefined, true);
844
+ function workspaceDisplayName(root) {
845
+ return basename(normalizePath(root)) || "workspace";
846
+ }
847
+ async function workspaceIndexStatus(info, location) {
848
+ if (info.indexPolicy === "disabled" ||
849
+ !isWorkspaceIndexed(info) ||
850
+ !hasWorkspaceIndex(location)) {
851
+ return null;
852
+ }
853
+ const workspaceIndex = new WorkspaceIndex(info, undefined, true, location.filesPath);
708
854
  try {
709
- return registry.get(name);
855
+ return await workspaceIndex.status();
710
856
  }
711
857
  finally {
712
- registry.close();
858
+ workspaceIndex.close();
713
859
  }
714
860
  }
715
861
  function indexedEmbeddingSchema(info) {
716
- if (isCollectionIndexed(info)) {
862
+ if (isWorkspaceIndexed(info)) {
717
863
  return info.embedding;
718
864
  }
719
865
  throw new EngineError("zvec-grep index has not been built", {
720
866
  code: "ZVEC_GREP.ENGINE.SERVICE.INDEX_MISSING",
721
867
  context: errorDetails([
722
- collectionDetail(info.name),
723
- detail("hint", "Run zg --index to build this index."),
868
+ workspaceIndexDetail(info.name),
869
+ detail("hint", "Run zg index to build this index."),
724
870
  ]),
725
871
  });
726
872
  }
727
- function providerOptions(options, root, registryHome) {
873
+ function providerOptions(options, identity, config = readGlobalConfig(), workspaceRuntime = {}) {
874
+ const reference = embeddingModelReference(identity);
875
+ const runtime = resolveEmbeddingRuntimeOptions(reference, options, workspaceRuntime, config);
876
+ return {
877
+ apiKey: runtime.apiKey,
878
+ endpoint: runtime.endpoint,
879
+ modelCacheDir: options.modelCacheDir ??
880
+ config.defaults?.modelCacheDir ??
881
+ process.env.ZVEC_GREP_MODEL_CACHE ??
882
+ join(options.home ?? defaultHome(), "models"),
883
+ device: runtime.device,
884
+ };
885
+ }
886
+ function createServiceEmbeddingModel(reference, modelOptions, serviceOptions) {
887
+ const model = createEmbeddingModel(reference, modelOptions);
888
+ if (model.info.provider !== "qwen") {
889
+ return model;
890
+ }
891
+ const endpoint = model.info.endpoint;
892
+ if (endpoint === undefined) {
893
+ throw new EngineError("Remote embedding model did not provide an endpoint", {
894
+ code: "ZVEC_GREP.ENGINE.MODELS.REMOTE_ENDPOINT_NOT_IMPLEMENTED",
895
+ context: `model=${model.info.reference}`,
896
+ });
897
+ }
898
+ const authorize = remoteEmbeddingAuthorizationGuard({
899
+ store: new RemoteEmbeddingAuthorizationStore({
900
+ signingKeyPath: serviceOptions.authorizationSigningKeyPath,
901
+ }),
902
+ });
903
+ const authorizedModel = {
904
+ info: model.info,
905
+ async embed(contents, embedOptions) {
906
+ await authorize({
907
+ provider: model.info.provider,
908
+ model: model.info.name,
909
+ endpoint,
910
+ purpose: embedOptions?.purpose ?? EmbeddingPurpose.Document,
911
+ contentKinds: contents.map((content) => content.kind),
912
+ contentCount: contents.length,
913
+ });
914
+ return await model.embed(contents, embedOptions);
915
+ },
916
+ dispose: () => model.dispose(),
917
+ };
918
+ return authorizedModel;
919
+ }
920
+ function parseEmbeddingModelReference(reference) {
921
+ const separator = reference.indexOf("/");
922
+ if (separator <= 0 || separator === reference.length - 1) {
923
+ throw new EngineError("Embedding model reference is invalid", {
924
+ code: "ZVEC_GREP.ENGINE.MODELS.EMBEDDING_INVALID_REFERENCE",
925
+ context: `reference=${reference}`,
926
+ });
927
+ }
928
+ return {
929
+ provider: reference.slice(0, separator),
930
+ name: reference.slice(separator + 1),
931
+ };
932
+ }
933
+ function embeddingModelReference(identity) {
934
+ return `${identity.provider}/${identity.name}`;
935
+ }
936
+ function providerOptionsFingerprint(options) {
937
+ const publicOptionsFingerprint = createHash("sha256")
938
+ .update(JSON.stringify([options.endpoint, options.modelCacheDir, options.device]))
939
+ .digest("hex");
940
+ return `${providerApiKeyIdentity(options.apiKey)}:${publicOptionsFingerprint}`;
941
+ }
942
+ function providerApiKeyIdentity(apiKey) {
943
+ return createHmac("sha256", PROVIDER_API_KEY_IDENTITY_SECRET)
944
+ .update(apiKey)
945
+ .digest("hex");
946
+ }
947
+ function effectiveEmbeddingRuntime(options, model, workspaceRuntime) {
948
+ const resolved = resolveEmbeddingRuntimeOptions(model.info.reference, options, workspaceRuntime, readGlobalConfig());
728
949
  return {
729
- apiKey: options.apiKey ?? "",
730
- endpoint: options.endpoint,
731
- modelCacheDir: modelCacheDir(options, root, registryHome),
732
- llamaGpu: options.llamaGpu,
733
- embeddingParallelism: options.embeddingParallelism,
950
+ apiKey: resolved.apiKey,
951
+ ...((model.info.endpoint ?? resolved.endpoint)
952
+ ? { endpoint: model.info.endpoint ?? resolved.endpoint }
953
+ : {}),
954
+ ...(model.info.provider === "local"
955
+ ? { device: resolved.device ?? "auto" }
956
+ : {}),
734
957
  };
735
958
  }
736
- function assertCollectionEmbeddingMatchesCurrentModel(info, model, rebuildCommand) {
737
- if (!isCollectionIndexed(info)) {
959
+ function runtimeForModelProvider(existing, model, workspaceRuntime) {
960
+ return isWorkspaceIndexed(existing) &&
961
+ existing.embedding.provider !== model.info.provider
962
+ ? {}
963
+ : workspaceRuntime;
964
+ }
965
+ function embeddingRuntimeAfterSuccessfulIndex(existing, existingRuntime, model, effectiveRuntime, explicit) {
966
+ const sameProvider = isWorkspaceIndexed(existing) &&
967
+ existing.embedding.provider === model.info.provider;
968
+ const apiKey = model.info.provider === "local"
969
+ ? undefined
970
+ : (explicit.apiKey ??
971
+ (sameProvider ? existingRuntime.apiKey : undefined));
972
+ return {
973
+ ...(apiKey !== undefined ? { apiKey } : {}),
974
+ ...(model.info.provider !== "local" &&
975
+ effectiveRuntime.endpoint !== undefined
976
+ ? { endpoint: effectiveRuntime.endpoint }
977
+ : {}),
978
+ ...(model.info.provider === "local"
979
+ ? { device: effectiveRuntime.device ?? "auto" }
980
+ : {}),
981
+ };
982
+ }
983
+ function assertWorkspaceEndpointMatchesCurrentRuntime(info, workspaceRuntime, effectiveRuntime, rebuildCommand) {
984
+ if (!isWorkspaceIndexed(info) ||
985
+ workspaceRuntime.endpoint === effectiveRuntime.endpoint) {
986
+ return;
987
+ }
988
+ throw new EngineError("Existing zvec-grep index uses a different embedding endpoint", {
989
+ code: "ZVEC_GREP.ENGINE.SERVICE.EMBEDDING_ENDPOINT_CHANGE_REQUIRES_REBUILD",
990
+ context: errorDetails([
991
+ workspaceIndexDetail(info.name),
992
+ detail("hint", `Run "${rebuildCommand}" to rebuild this index with the requested endpoint.`),
993
+ ]),
994
+ });
995
+ }
996
+ function assertSearchEndpointMatchesWorkspace(info, workspaceRuntime, effectiveRuntime) {
997
+ if (workspaceRuntime.endpoint === effectiveRuntime.endpoint) {
998
+ return;
999
+ }
1000
+ throw new EngineError("Search cannot override the embedding endpoint of an existing index", {
1001
+ code: "ZVEC_GREP.ENGINE.SERVICE.SEARCH_ENDPOINT_CHANGE_REQUIRES_REBUILD",
1002
+ context: errorDetails([
1003
+ workspaceIndexDetail(info.name),
1004
+ detail("hint", 'Run "zg index --endpoint <url> --rebuild" before searching with this endpoint.'),
1005
+ ]),
1006
+ });
1007
+ }
1008
+ function assertWorkspaceEmbeddingMatchesCurrentModel(info, model, rebuildCommand) {
1009
+ if (!isWorkspaceIndexed(info)) {
738
1010
  return;
739
1011
  }
740
1012
  const expected = info.embedding;
741
- const changed = expected.provider !== model.ref.provider
742
- || expected.model !== model.ref.model
743
- || expected.dimension !== model.dimension
744
- || expected.metric !== model.metric;
1013
+ const changed = expected.provider !== model.info.provider ||
1014
+ expected.model !== model.info.name ||
1015
+ expected.dimension !== model.info.dimension ||
1016
+ expected.metric !== model.info.metric;
745
1017
  if (!changed) {
746
1018
  return;
747
1019
  }
748
1020
  throw new EngineError("Existing zvec-grep index uses a different embedding model", {
749
1021
  code: "ZVEC_GREP.ENGINE.SERVICE.EMBEDDING_SCHEMA_CHANGE_REQUIRES_REBUILD",
750
1022
  context: errorDetails([
751
- collectionDetail(info.name),
1023
+ workspaceIndexDetail(info.name),
752
1024
  detail("existing", `${expected.provider}/${expected.model}`),
753
- detail("requested", `${model.ref.provider}/${model.ref.model}`),
1025
+ detail("requested", model.info.reference),
754
1026
  detail("hint", `Run "${rebuildCommand}" to rebuild this index with the requested embedding model.`),
755
1027
  ]),
756
1028
  });
757
1029
  }
758
- function modelCacheDir(options, root, registryHome) {
759
- if (options.modelCacheDir || process.env.ZVEC_GREP_MODEL_CACHE) {
760
- return options.modelCacheDir ?? process.env.ZVEC_GREP_MODEL_CACHE;
761
- }
762
- const globalCache = join(options.home ?? defaultHome(), "models");
763
- const legacyCaches = [
764
- registryHome ? join(registryHome, "models") : undefined,
765
- join(anonymousHome(root), "models"),
766
- ];
767
- const globalHasModel = directoryHasGguf(globalCache);
768
- for (const legacyCache of legacyCaches) {
769
- if (legacyCache && directoryHasGguf(legacyCache) && !globalHasModel) {
770
- return legacyCache;
771
- }
772
- }
773
- if (globalHasModel || canWriteCacheDirectory(globalCache)) {
774
- return globalCache;
775
- }
776
- return join(anonymousHome(root), "models");
777
- }
778
- function directoryHasGguf(path) {
779
- try {
780
- return readdirSync(path, { withFileTypes: true }).some((entry) => entry.isFile() && entry.name.endsWith(".gguf"));
781
- }
782
- catch {
783
- return false;
784
- }
785
- }
786
- function canWriteCacheDirectory(path) {
787
- const probe = join(path, `.zvec-grep-write-test-${Date.now()}-${Math.random().toString(16).slice(2)}`);
788
- try {
789
- mkdirSync(path, { recursive: true });
790
- writeFileSync(probe, "");
791
- unlinkSync(probe);
792
- return true;
793
- }
794
- catch {
795
- return false;
796
- }
797
- }
798
1030
  function contextGroupLimit(limit, groupCount) {
799
1031
  if (limit !== undefined) {
800
1032
  return limit;
@@ -808,29 +1040,36 @@ function contextGroupLimit(limit, groupCount) {
808
1040
  function normalizeContextRequest(options) {
809
1041
  const primaryQueries = normalizePrimaryQueries(options.query, options.queries);
810
1042
  const extraRoutes = normalizeContextRoutes(options.routes ?? []);
811
- if (primaryQueries.length === 0 && extraRoutes.length === 0) {
1043
+ const hasPatternFiles = options.rg === true && (options.rgOptions?.patternFiles?.length ?? 0) > 0;
1044
+ if (primaryQueries.length === 0 &&
1045
+ extraRoutes.length === 0 &&
1046
+ !hasPatternFiles) {
812
1047
  throw new EngineError("zvec-grep context requires a non-empty query or route", {
813
1048
  code: "ZVEC_GREP.ENGINE.SERVICE.EMPTY_QUERY",
814
1049
  });
815
1050
  }
816
1051
  const groups = contextGroups(primaryQueries, extraRoutes);
817
1052
  const routes = groups.flatMap((group) => group.routes);
818
- const fallbackQueries = groups.map((group) => group.fallbackQuery);
1053
+ const rgPatterns = [
1054
+ ...primaryQueries,
1055
+ ...extraRoutes.map((route) => route.query),
1056
+ ];
819
1057
  const displayQuery = primaryQueries.length > 0
820
1058
  ? primaryQueries.join(" | ")
821
- : extraRoutes.map((route) => route.query).join(" | ");
1059
+ : extraRoutes.length > 0
1060
+ ? extraRoutes.map((route) => route.query).join(" | ")
1061
+ : options
1062
+ .rgOptions.patternFiles.map((path) => `@${path}`)
1063
+ .join(" | ");
822
1064
  return {
823
1065
  displayQuery,
824
- fallbackQueries,
1066
+ rgPatterns,
825
1067
  routes,
826
1068
  groups,
827
1069
  };
828
1070
  }
829
1071
  function normalizePrimaryQueries(query, queries) {
830
- return [
831
- query,
832
- ...(queries ?? []),
833
- ].flatMap((value) => {
1072
+ return [query, ...(queries ?? [])].flatMap((value) => {
834
1073
  const normalized = normalizeOptionalQuery(value);
835
1074
  return normalized ? [normalized] : [];
836
1075
  });
@@ -862,20 +1101,24 @@ function normalizeContextRoutes(routes) {
862
1101
  }
863
1102
  function contextGroups(primaryQueries, extraRoutes) {
864
1103
  return [
865
- ...primaryQueries.map((query) => ({
1104
+ ...primaryQueries.map((query, index) => ({
1105
+ id: `Q${index + 1}`,
1106
+ query,
1107
+ role: "primary",
866
1108
  routes: [
867
1109
  { mode: "fts", query },
868
1110
  { mode: "vector", query },
869
1111
  ],
870
- fallbackQuery: query,
871
1112
  })),
872
- ...extraRoutes.map((route) => ({
1113
+ ...extraRoutes.map((route, index) => ({
1114
+ id: `Q${primaryQueries.length + index + 1}`,
1115
+ query: route.query,
1116
+ role: "supplemental",
873
1117
  routes: [route],
874
- fallbackQuery: route.query,
875
1118
  })),
876
1119
  ];
877
1120
  }
878
- function searchPlanToContextItems(result, root) {
1121
+ function searchPlanToContextItems(result, root, group) {
879
1122
  return result.hits.map((hit) => {
880
1123
  const target = contextItemTarget(hit);
881
1124
  return {
@@ -897,9 +1140,75 @@ function searchPlanToContextItems(result, root) {
897
1140
  metadata: hit.entity.metadata,
898
1141
  entityId: hit.entity.id,
899
1142
  trace: hit.trace,
1143
+ queryGroups: [
1144
+ {
1145
+ id: group.id,
1146
+ query: group.query,
1147
+ role: group.role,
1148
+ rank: hit.rank,
1149
+ matchedBy: hit.matchedBy,
1150
+ },
1151
+ ],
900
1152
  };
901
1153
  });
902
1154
  }
1155
+ const DEFAULT_CONTEXT_DETAIL_LIMIT = 6;
1156
+ const CONTEXT_GROUP_RRF_K = 60;
1157
+ function selectAndRankContextItems(items, coverageGroupIds) {
1158
+ const deduped = new Map();
1159
+ for (const item of items) {
1160
+ const key = contextItemDedupeKey(item);
1161
+ const existing = deduped.get(key);
1162
+ if (existing) {
1163
+ existing.queryGroups = mergeContextQueryGroupMatches(existing.queryGroups ?? [], item.queryGroups ?? []);
1164
+ existing.matchedBy = mergedContextMatchedBy(existing.queryGroups);
1165
+ continue;
1166
+ }
1167
+ deduped.set(key, { ...item });
1168
+ }
1169
+ const candidates = [...deduped.values()];
1170
+ const globallyRanked = [...candidates].sort(compareContextGlobalRank);
1171
+ const selected = new Set();
1172
+ const detailed = [];
1173
+ for (const groupId of coverageGroupIds) {
1174
+ if (detailed.length >= DEFAULT_CONTEXT_DETAIL_LIMIT) {
1175
+ break;
1176
+ }
1177
+ const candidate = candidates
1178
+ .filter((item) => !selected.has(contextItemDedupeKey(item)))
1179
+ .filter((item) => contextQueryGroupMatch(item, groupId) !== undefined)
1180
+ .sort((left, right) => compareContextGroupRank(left, right, groupId))[0];
1181
+ if (!candidate) {
1182
+ continue;
1183
+ }
1184
+ selected.add(contextItemDedupeKey(candidate));
1185
+ detailed.push({
1186
+ ...candidate,
1187
+ selectionReason: "coverage",
1188
+ coverageGroup: groupId,
1189
+ });
1190
+ }
1191
+ for (const candidate of globallyRanked) {
1192
+ if (detailed.length >= DEFAULT_CONTEXT_DETAIL_LIMIT) {
1193
+ break;
1194
+ }
1195
+ const key = contextItemDedupeKey(candidate);
1196
+ if (selected.has(key)) {
1197
+ continue;
1198
+ }
1199
+ selected.add(key);
1200
+ detailed.push({
1201
+ ...candidate,
1202
+ selectionReason: "global_fill",
1203
+ coverageGroup: undefined,
1204
+ });
1205
+ }
1206
+ const additional = globallyRanked.filter((item) => !selected.has(contextItemDedupeKey(item)));
1207
+ return [...detailed, ...additional].map((item, index) => ({
1208
+ ...item,
1209
+ rank: index + 1,
1210
+ }));
1211
+ }
903
1212
  function dedupeAndRerankContextItems(items) {
904
1213
  const seen = new Set();
905
1214
  const deduped = [];
@@ -909,34 +1218,57 @@ function dedupeAndRerankContextItems(items) {
909
1218
  continue;
910
1219
  }
911
1220
  seen.add(key);
912
- deduped.push({
913
- ...item,
914
- rank: deduped.length + 1,
915
- });
1221
+ deduped.push({ ...item, rank: deduped.length + 1 });
916
1222
  }
917
1223
  return deduped;
918
1224
  }
1225
+ function mergeContextQueryGroupMatches(left, right) {
1226
+ const matches = new Map(left.map((match) => [match.id, match]));
1227
+ for (const match of right) {
1228
+ const existing = matches.get(match.id);
1229
+ if (!existing || match.rank < existing.rank) {
1230
+ matches.set(match.id, match);
1231
+ }
1232
+ }
1233
+ return [...matches.values()].sort((a, b) => contextGroupNumber(a.id) - contextGroupNumber(b.id));
1234
+ }
1235
+ function mergedContextMatchedBy(matches) {
1236
+ const hasFts = matches.some((match) => match.matchedBy === "fts" || match.matchedBy === "fts+vector");
1237
+ const hasVector = matches.some((match) => match.matchedBy === "vector" || match.matchedBy === "fts+vector");
1238
+ return hasFts && hasVector ? "fts+vector" : hasFts ? "fts" : "vector";
1239
+ }
1240
+ function compareContextGlobalRank(left, right) {
1241
+ return (contextGlobalRrfScore(right) - contextGlobalRrfScore(left) ||
1242
+ contextBestGroupRank(left) - contextBestGroupRank(right) ||
1243
+ contextFirstGroupNumber(left) - contextFirstGroupNumber(right) ||
1244
+ contextItemDedupeKey(left).localeCompare(contextItemDedupeKey(right)));
1245
+ }
1246
+ function contextGlobalRrfScore(item) {
1247
+ return (item.queryGroups ?? []).reduce((score, match) => score + 1 / (CONTEXT_GROUP_RRF_K + match.rank), 0);
1248
+ }
1249
+ function compareContextGroupRank(left, right, groupId) {
1250
+ return (contextQueryGroupMatch(left, groupId).rank -
1251
+ contextQueryGroupMatch(right, groupId).rank ||
1252
+ compareContextGlobalRank(left, right));
1253
+ }
1254
+ function contextQueryGroupMatch(item, groupId) {
1255
+ return item.queryGroups?.find((match) => match.id === groupId);
1256
+ }
1257
+ function contextBestGroupRank(item) {
1258
+ return Math.min(...(item.queryGroups ?? []).map((match) => match.rank));
1259
+ }
1260
+ function contextFirstGroupNumber(item) {
1261
+ return Math.min(...(item.queryGroups ?? []).map((match) => contextGroupNumber(match.id)));
1262
+ }
1263
+ function contextGroupNumber(id) {
1264
+ const value = Number.parseInt(id.slice(1), 10);
1265
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
1266
+ }
919
1267
  function contextItemDedupeKey(item) {
920
1268
  if (item.entityId) {
921
1269
  return `entity:${item.entityId}`;
922
1270
  }
923
- return [
924
- "range",
925
- item.file.absolutePath,
926
- JSON.stringify(item.range),
927
- ].join(":");
928
- }
929
- function mergeFallbackDiagnostics(results) {
930
- const [first] = results;
931
- if (!first) {
932
- throw new EngineError("zvec-grep lexical fallback requires at least one query", {
933
- code: "ZVEC_GREP.ENGINE.SERVICE.EMPTY_QUERY",
934
- });
935
- }
936
- return {
937
- ...first.diagnostics,
938
- truncated: results.some((result) => result.diagnostics.truncated),
939
- };
1271
+ return ["range", item.file.absolutePath, JSON.stringify(item.range)].join(":");
940
1272
  }
941
1273
  function contextItemTarget(hit) {
942
1274
  const evidence = hit.evidence.find((item) => !item.isEntity);
@@ -979,7 +1311,8 @@ function contextItemOutline(hit, evidence) {
979
1311
  if (!isUsefulOutline(hit.entity.metadata, outline)) {
980
1312
  return undefined;
981
1313
  }
982
- if (evidence?.content.kind === "text" && evidence.content.text.trim() === outline) {
1314
+ if (evidence?.content.kind === "text" &&
1315
+ evidence.content.text.trim() === outline) {
983
1316
  return undefined;
984
1317
  }
985
1318
  return outline;
@@ -988,23 +1321,23 @@ function isUsefulOutline(metadata, outline) {
988
1321
  if (!metadata || metadata.kind !== "code") {
989
1322
  return false;
990
1323
  }
991
- return metadata.symbolType === "class"
992
- || metadata.symbolType === "interface"
993
- || metadata.symbolType === "module"
994
- || outline.includes("\ncalls:");
1324
+ return (metadata.symbolType === "class" ||
1325
+ metadata.symbolType === "interface" ||
1326
+ metadata.symbolType === "module" ||
1327
+ outline.includes("\ncalls:"));
995
1328
  }
996
1329
  function sameDisplayedContent(entity, evidence) {
997
- return JSON.stringify(entity.range) === JSON.stringify(evidence.range)
998
- && contentEquals(entity.content, evidence.content);
1330
+ return (JSON.stringify(entity.range) === JSON.stringify(evidence.range) &&
1331
+ contentEquals(entity.content, evidence.content));
999
1332
  }
1000
1333
  function contentEquals(left, right) {
1001
1334
  if (left.kind !== right.kind) {
1002
1335
  return false;
1003
1336
  }
1004
1337
  if (left.kind === "image") {
1005
- return right.kind === "image"
1006
- && left.format === right.format
1007
- && left.data.byteLength === right.data.byteLength;
1338
+ return (right.kind === "image" &&
1339
+ left.format === right.format &&
1340
+ left.data.byteLength === right.data.byteLength);
1008
1341
  }
1009
1342
  return right.kind === "text" && left.text === right.text;
1010
1343
  }
@@ -1020,7 +1353,8 @@ function fileFreshnessStatus(file) {
1020
1353
  if (file.indexStatus.indexedTime >= info.mtimeMs) {
1021
1354
  return "fresh";
1022
1355
  }
1023
- if (file.contentHash && sha256File(file.absolutePath) === file.contentHash) {
1356
+ if (file.contentHash &&
1357
+ sha256File(file.absolutePath) === file.contentHash) {
1024
1358
  return "fresh";
1025
1359
  }
1026
1360
  }