@untangle-code/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (571) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +424 -2
  4. package/THIRD_PARTY_LICENSES.md +152 -0
  5. package/dist/cli/args.d.ts +48 -0
  6. package/dist/cli/args.d.ts.map +1 -0
  7. package/dist/cli/args.js +167 -0
  8. package/dist/cli/args.js.map +1 -0
  9. package/dist/cli/command.d.ts +69 -0
  10. package/dist/cli/command.d.ts.map +1 -0
  11. package/dist/cli/command.js +82 -0
  12. package/dist/cli/command.js.map +1 -0
  13. package/dist/cli/deadline.d.ts +33 -0
  14. package/dist/cli/deadline.d.ts.map +1 -0
  15. package/dist/cli/deadline.js +49 -0
  16. package/dist/cli/deadline.js.map +1 -0
  17. package/dist/cli/embedder-deadline.d.ts +72 -0
  18. package/dist/cli/embedder-deadline.d.ts.map +1 -0
  19. package/dist/cli/embedder-deadline.js +117 -0
  20. package/dist/cli/embedder-deadline.js.map +1 -0
  21. package/dist/cli/exit.d.ts +71 -0
  22. package/dist/cli/exit.d.ts.map +1 -0
  23. package/dist/cli/exit.js +81 -0
  24. package/dist/cli/exit.js.map +1 -0
  25. package/dist/cli/graphs.d.ts +65 -0
  26. package/dist/cli/graphs.d.ts.map +1 -0
  27. package/dist/cli/graphs.js +108 -0
  28. package/dist/cli/graphs.js.map +1 -0
  29. package/dist/cli/index-freshness.d.ts +13 -0
  30. package/dist/cli/index-freshness.d.ts.map +1 -0
  31. package/dist/cli/index-freshness.js +226 -0
  32. package/dist/cli/index-freshness.js.map +1 -0
  33. package/dist/cli/index-refresh.d.ts +10 -0
  34. package/dist/cli/index-refresh.d.ts.map +1 -0
  35. package/dist/cli/index-refresh.js +76 -0
  36. package/dist/cli/index-refresh.js.map +1 -0
  37. package/dist/cli/index-state.d.ts +184 -0
  38. package/dist/cli/index-state.d.ts.map +1 -0
  39. package/dist/cli/index-state.js +852 -0
  40. package/dist/cli/index-state.js.map +1 -0
  41. package/dist/cli/io.d.ts +29 -0
  42. package/dist/cli/io.d.ts.map +1 -0
  43. package/dist/cli/io.js +42 -0
  44. package/dist/cli/io.js.map +1 -0
  45. package/dist/cli/language-support.d.ts +77 -0
  46. package/dist/cli/language-support.d.ts.map +1 -0
  47. package/dist/cli/language-support.js +152 -0
  48. package/dist/cli/language-support.js.map +1 -0
  49. package/dist/cli/renewing-lease.d.ts +26 -0
  50. package/dist/cli/renewing-lease.d.ts.map +1 -0
  51. package/dist/cli/renewing-lease.js +102 -0
  52. package/dist/cli/renewing-lease.js.map +1 -0
  53. package/dist/cli/search-graph-enrichment.d.ts +16 -0
  54. package/dist/cli/search-graph-enrichment.d.ts.map +1 -0
  55. package/dist/cli/search-graph-enrichment.js +428 -0
  56. package/dist/cli/search-graph-enrichment.js.map +1 -0
  57. package/dist/cli/system.d.ts +34 -0
  58. package/dist/cli/system.d.ts.map +1 -0
  59. package/dist/cli/system.js +89 -0
  60. package/dist/cli/system.js.map +1 -0
  61. package/dist/cli/vector-store-policy.d.ts +116 -0
  62. package/dist/cli/vector-store-policy.d.ts.map +1 -0
  63. package/dist/cli/vector-store-policy.js +160 -0
  64. package/dist/cli/vector-store-policy.js.map +1 -0
  65. package/dist/cli/version.d.ts +3 -0
  66. package/dist/cli/version.d.ts.map +1 -0
  67. package/dist/cli/version.js +3 -0
  68. package/dist/cli/version.js.map +1 -0
  69. package/dist/cli/workspace.d.ts +105 -0
  70. package/dist/cli/workspace.d.ts.map +1 -0
  71. package/dist/cli/workspace.js +215 -0
  72. package/dist/cli/workspace.js.map +1 -0
  73. package/dist/commands/complexity.d.ts +72 -0
  74. package/dist/commands/complexity.d.ts.map +1 -0
  75. package/dist/commands/complexity.js +580 -0
  76. package/dist/commands/complexity.js.map +1 -0
  77. package/dist/commands/doctor.d.ts +66 -0
  78. package/dist/commands/doctor.d.ts.map +1 -0
  79. package/dist/commands/doctor.js +764 -0
  80. package/dist/commands/doctor.js.map +1 -0
  81. package/dist/commands/doctor.languages.d.ts +37 -0
  82. package/dist/commands/doctor.languages.d.ts.map +1 -0
  83. package/dist/commands/doctor.languages.js +143 -0
  84. package/dist/commands/doctor.languages.js.map +1 -0
  85. package/dist/commands/doctor.storage.d.ts +17 -0
  86. package/dist/commands/doctor.storage.d.ts.map +1 -0
  87. package/dist/commands/doctor.storage.js +91 -0
  88. package/dist/commands/doctor.storage.js.map +1 -0
  89. package/dist/commands/extensions/add.d.ts +10 -0
  90. package/dist/commands/extensions/add.d.ts.map +1 -0
  91. package/dist/commands/extensions/add.js +213 -0
  92. package/dist/commands/extensions/add.js.map +1 -0
  93. package/dist/commands/extensions/configure.d.ts +5 -0
  94. package/dist/commands/extensions/configure.d.ts.map +1 -0
  95. package/dist/commands/extensions/configure.js +58 -0
  96. package/dist/commands/extensions/configure.js.map +1 -0
  97. package/dist/commands/extensions/container.d.ts +15 -0
  98. package/dist/commands/extensions/container.d.ts.map +1 -0
  99. package/dist/commands/extensions/container.js +135 -0
  100. package/dist/commands/extensions/container.js.map +1 -0
  101. package/dist/commands/extensions/context.d.ts +37 -0
  102. package/dist/commands/extensions/context.d.ts.map +1 -0
  103. package/dist/commands/extensions/context.js +72 -0
  104. package/dist/commands/extensions/context.js.map +1 -0
  105. package/dist/commands/extensions/enable.d.ts +6 -0
  106. package/dist/commands/extensions/enable.d.ts.map +1 -0
  107. package/dist/commands/extensions/enable.js +180 -0
  108. package/dist/commands/extensions/enable.js.map +1 -0
  109. package/dist/commands/extensions/environment.d.ts +74 -0
  110. package/dist/commands/extensions/environment.d.ts.map +1 -0
  111. package/dist/commands/extensions/environment.js +168 -0
  112. package/dist/commands/extensions/environment.js.map +1 -0
  113. package/dist/commands/extensions/feature-scan.d.ts +48 -0
  114. package/dist/commands/extensions/feature-scan.d.ts.map +1 -0
  115. package/dist/commands/extensions/feature-scan.js +156 -0
  116. package/dist/commands/extensions/feature-scan.js.map +1 -0
  117. package/dist/commands/extensions/invoke.d.ts +60 -0
  118. package/dist/commands/extensions/invoke.d.ts.map +1 -0
  119. package/dist/commands/extensions/invoke.js +275 -0
  120. package/dist/commands/extensions/invoke.js.map +1 -0
  121. package/dist/commands/extensions/list.d.ts +5 -0
  122. package/dist/commands/extensions/list.d.ts.map +1 -0
  123. package/dist/commands/extensions/list.js +125 -0
  124. package/dist/commands/extensions/list.js.map +1 -0
  125. package/dist/commands/extensions/logs.d.ts +5 -0
  126. package/dist/commands/extensions/logs.d.ts.map +1 -0
  127. package/dist/commands/extensions/logs.js +74 -0
  128. package/dist/commands/extensions/logs.js.map +1 -0
  129. package/dist/commands/extensions/provision.d.ts +36 -0
  130. package/dist/commands/extensions/provision.d.ts.map +1 -0
  131. package/dist/commands/extensions/provision.js +137 -0
  132. package/dist/commands/extensions/provision.js.map +1 -0
  133. package/dist/commands/extensions/remove.d.ts +5 -0
  134. package/dist/commands/extensions/remove.d.ts.map +1 -0
  135. package/dist/commands/extensions/remove.js +77 -0
  136. package/dist/commands/extensions/remove.js.map +1 -0
  137. package/dist/commands/extensions/render.d.ts +16 -0
  138. package/dist/commands/extensions/render.d.ts.map +1 -0
  139. package/dist/commands/extensions/render.js +25 -0
  140. package/dist/commands/extensions/render.js.map +1 -0
  141. package/dist/commands/extensions/run.d.ts +30 -0
  142. package/dist/commands/extensions/run.d.ts.map +1 -0
  143. package/dist/commands/extensions/run.js +292 -0
  144. package/dist/commands/extensions/run.js.map +1 -0
  145. package/dist/commands/extensions/secret-store.d.ts +15 -0
  146. package/dist/commands/extensions/secret-store.d.ts.map +1 -0
  147. package/dist/commands/extensions/secret-store.js +164 -0
  148. package/dist/commands/extensions/secret-store.js.map +1 -0
  149. package/dist/commands/extensions/setup.d.ts +17 -0
  150. package/dist/commands/extensions/setup.d.ts.map +1 -0
  151. package/dist/commands/extensions/setup.js +94 -0
  152. package/dist/commands/extensions/setup.js.map +1 -0
  153. package/dist/commands/extensions/source.d.ts +47 -0
  154. package/dist/commands/extensions/source.d.ts.map +1 -0
  155. package/dist/commands/extensions/source.js +142 -0
  156. package/dist/commands/extensions/source.js.map +1 -0
  157. package/dist/commands/extensions/state.d.ts +62 -0
  158. package/dist/commands/extensions/state.d.ts.map +1 -0
  159. package/dist/commands/extensions/state.js +177 -0
  160. package/dist/commands/extensions/state.js.map +1 -0
  161. package/dist/commands/extensions/status.d.ts +9 -0
  162. package/dist/commands/extensions/status.d.ts.map +1 -0
  163. package/dist/commands/extensions/status.js +239 -0
  164. package/dist/commands/extensions/status.js.map +1 -0
  165. package/dist/commands/extensions.d.ts +3 -0
  166. package/dist/commands/extensions.d.ts.map +1 -0
  167. package/dist/commands/extensions.js +223 -0
  168. package/dist/commands/extensions.js.map +1 -0
  169. package/dist/commands/flow/index.d.ts +3 -0
  170. package/dist/commands/flow/index.d.ts.map +1 -0
  171. package/dist/commands/flow/index.js +295 -0
  172. package/dist/commands/flow/index.js.map +1 -0
  173. package/dist/commands/flow/limits.d.ts +12 -0
  174. package/dist/commands/flow/limits.d.ts.map +1 -0
  175. package/dist/commands/flow/limits.js +13 -0
  176. package/dist/commands/flow/limits.js.map +1 -0
  177. package/dist/commands/flow/output.d.ts +8 -0
  178. package/dist/commands/flow/output.d.ts.map +1 -0
  179. package/dist/commands/flow/output.js +11 -0
  180. package/dist/commands/flow/output.js.map +1 -0
  181. package/dist/commands/flow/render.d.ts +51 -0
  182. package/dist/commands/flow/render.d.ts.map +1 -0
  183. package/dist/commands/flow/render.js +220 -0
  184. package/dist/commands/flow/render.js.map +1 -0
  185. package/dist/commands/flow/select.d.ts +31 -0
  186. package/dist/commands/flow/select.d.ts.map +1 -0
  187. package/dist/commands/flow/select.js +62 -0
  188. package/dist/commands/flow/select.js.map +1 -0
  189. package/dist/commands/graph/boundaries-input.d.ts +26 -0
  190. package/dist/commands/graph/boundaries-input.d.ts.map +1 -0
  191. package/dist/commands/graph/boundaries-input.js +54 -0
  192. package/dist/commands/graph/boundaries-input.js.map +1 -0
  193. package/dist/commands/graph/boundaries.d.ts +4 -0
  194. package/dist/commands/graph/boundaries.d.ts.map +1 -0
  195. package/dist/commands/graph/boundaries.js +141 -0
  196. package/dist/commands/graph/boundaries.js.map +1 -0
  197. package/dist/commands/graph/bounds.d.ts +16 -0
  198. package/dist/commands/graph/bounds.d.ts.map +1 -0
  199. package/dist/commands/graph/bounds.js +22 -0
  200. package/dist/commands/graph/bounds.js.map +1 -0
  201. package/dist/commands/graph/category-clusters.d.ts +20 -0
  202. package/dist/commands/graph/category-clusters.d.ts.map +1 -0
  203. package/dist/commands/graph/category-clusters.js +158 -0
  204. package/dist/commands/graph/category-clusters.js.map +1 -0
  205. package/dist/commands/graph/feature-3d.d.ts +22 -0
  206. package/dist/commands/graph/feature-3d.d.ts.map +1 -0
  207. package/dist/commands/graph/feature-3d.js +114 -0
  208. package/dist/commands/graph/feature-3d.js.map +1 -0
  209. package/dist/commands/graph/feature-clusters.d.ts +17 -0
  210. package/dist/commands/graph/feature-clusters.d.ts.map +1 -0
  211. package/dist/commands/graph/feature-clusters.js +94 -0
  212. package/dist/commands/graph/feature-clusters.js.map +1 -0
  213. package/dist/commands/graph/feature-html.d.ts +26 -0
  214. package/dist/commands/graph/feature-html.d.ts.map +1 -0
  215. package/dist/commands/graph/feature-html.js +314 -0
  216. package/dist/commands/graph/feature-html.js.map +1 -0
  217. package/dist/commands/graph/feature-map.d.ts +31 -0
  218. package/dist/commands/graph/feature-map.d.ts.map +1 -0
  219. package/dist/commands/graph/feature-map.js +217 -0
  220. package/dist/commands/graph/feature-map.js.map +1 -0
  221. package/dist/commands/graph/feature-model.d.ts +55 -0
  222. package/dist/commands/graph/feature-model.d.ts.map +1 -0
  223. package/dist/commands/graph/feature-model.js +167 -0
  224. package/dist/commands/graph/feature-model.js.map +1 -0
  225. package/dist/commands/graph/feature-untangle-motion.d.ts +33 -0
  226. package/dist/commands/graph/feature-untangle-motion.d.ts.map +1 -0
  227. package/dist/commands/graph/feature-untangle-motion.js +155 -0
  228. package/dist/commands/graph/feature-untangle-motion.js.map +1 -0
  229. package/dist/commands/graph/index.d.ts +3 -0
  230. package/dist/commands/graph/index.d.ts.map +1 -0
  231. package/dist/commands/graph/index.js +414 -0
  232. package/dist/commands/graph/index.js.map +1 -0
  233. package/dist/commands/graph/render.d.ts +63 -0
  234. package/dist/commands/graph/render.d.ts.map +1 -0
  235. package/dist/commands/graph/render.js +352 -0
  236. package/dist/commands/graph/render.js.map +1 -0
  237. package/dist/commands/graph/select.d.ts +34 -0
  238. package/dist/commands/graph/select.d.ts.map +1 -0
  239. package/dist/commands/graph/select.js +55 -0
  240. package/dist/commands/graph/select.js.map +1 -0
  241. package/dist/commands/index/index.d.ts +6 -0
  242. package/dist/commands/index/index.d.ts.map +1 -0
  243. package/dist/commands/index/index.js +461 -0
  244. package/dist/commands/index/index.js.map +1 -0
  245. package/dist/commands/index/render.d.ts +53 -0
  246. package/dist/commands/index/render.d.ts.map +1 -0
  247. package/dist/commands/index/render.js +205 -0
  248. package/dist/commands/index/render.js.map +1 -0
  249. package/dist/commands/index/target.d.ts +34 -0
  250. package/dist/commands/index/target.d.ts.map +1 -0
  251. package/dist/commands/index/target.js +97 -0
  252. package/dist/commands/index/target.js.map +1 -0
  253. package/dist/commands/index-watch.d.ts +30 -0
  254. package/dist/commands/index-watch.d.ts.map +1 -0
  255. package/dist/commands/index-watch.js +138 -0
  256. package/dist/commands/index-watch.js.map +1 -0
  257. package/dist/commands/init/command.d.ts +25 -0
  258. package/dist/commands/init/command.d.ts.map +1 -0
  259. package/dist/commands/init/command.js +198 -0
  260. package/dist/commands/init/command.js.map +1 -0
  261. package/dist/commands/init/config-draft.d.ts +48 -0
  262. package/dist/commands/init/config-draft.d.ts.map +1 -0
  263. package/dist/commands/init/config-draft.js +201 -0
  264. package/dist/commands/init/config-draft.js.map +1 -0
  265. package/dist/commands/init/diff.d.ts +33 -0
  266. package/dist/commands/init/diff.d.ts.map +1 -0
  267. package/dist/commands/init/diff.js +109 -0
  268. package/dist/commands/init/diff.js.map +1 -0
  269. package/dist/commands/init/http-probe.d.ts +26 -0
  270. package/dist/commands/init/http-probe.d.ts.map +1 -0
  271. package/dist/commands/init/http-probe.js +144 -0
  272. package/dist/commands/init/http-probe.js.map +1 -0
  273. package/dist/commands/init/ignore-rules.d.ts +70 -0
  274. package/dist/commands/init/ignore-rules.d.ts.map +1 -0
  275. package/dist/commands/init/ignore-rules.js +146 -0
  276. package/dist/commands/init/ignore-rules.js.map +1 -0
  277. package/dist/commands/init/index.d.ts +33 -0
  278. package/dist/commands/init/index.d.ts.map +1 -0
  279. package/dist/commands/init/index.js +32 -0
  280. package/dist/commands/init/index.js.map +1 -0
  281. package/dist/commands/init/integrations-apply.d.ts +50 -0
  282. package/dist/commands/init/integrations-apply.d.ts.map +1 -0
  283. package/dist/commands/init/integrations-apply.js +452 -0
  284. package/dist/commands/init/integrations-apply.js.map +1 -0
  285. package/dist/commands/init/integrations-plan.d.ts +68 -0
  286. package/dist/commands/init/integrations-plan.d.ts.map +1 -0
  287. package/dist/commands/init/integrations-plan.js +515 -0
  288. package/dist/commands/init/integrations-plan.js.map +1 -0
  289. package/dist/commands/init/integrations.d.ts +5 -0
  290. package/dist/commands/init/integrations.d.ts.map +1 -0
  291. package/dist/commands/init/integrations.js +4 -0
  292. package/dist/commands/init/integrations.js.map +1 -0
  293. package/dist/commands/init/languages.d.ts +55 -0
  294. package/dist/commands/init/languages.d.ts.map +1 -0
  295. package/dist/commands/init/languages.js +245 -0
  296. package/dist/commands/init/languages.js.map +1 -0
  297. package/dist/commands/init/product-context.d.ts +63 -0
  298. package/dist/commands/init/product-context.d.ts.map +1 -0
  299. package/dist/commands/init/product-context.js +390 -0
  300. package/dist/commands/init/product-context.js.map +1 -0
  301. package/dist/commands/init/render.d.ts +38 -0
  302. package/dist/commands/init/render.d.ts.map +1 -0
  303. package/dist/commands/init/render.js +501 -0
  304. package/dist/commands/init/render.js.map +1 -0
  305. package/dist/commands/init/report.d.ts +24 -0
  306. package/dist/commands/init/report.d.ts.map +1 -0
  307. package/dist/commands/init/report.js +163 -0
  308. package/dist/commands/init/report.js.map +1 -0
  309. package/dist/commands/init/run.d.ts +71 -0
  310. package/dist/commands/init/run.d.ts.map +1 -0
  311. package/dist/commands/init/run.js +253 -0
  312. package/dist/commands/init/run.js.map +1 -0
  313. package/dist/commands/init/services.d.ts +82 -0
  314. package/dist/commands/init/services.d.ts.map +1 -0
  315. package/dist/commands/init/services.js +209 -0
  316. package/dist/commands/init/services.js.map +1 -0
  317. package/dist/commands/init/storage.d.ts +8 -0
  318. package/dist/commands/init/storage.d.ts.map +1 -0
  319. package/dist/commands/init/storage.js +102 -0
  320. package/dist/commands/init/storage.js.map +1 -0
  321. package/dist/commands/init/toml.d.ts +65 -0
  322. package/dist/commands/init/toml.d.ts.map +1 -0
  323. package/dist/commands/init/toml.js +159 -0
  324. package/dist/commands/init/toml.js.map +1 -0
  325. package/dist/commands/init/topology.d.ts +54 -0
  326. package/dist/commands/init/topology.d.ts.map +1 -0
  327. package/dist/commands/init/topology.js +146 -0
  328. package/dist/commands/init/topology.js.map +1 -0
  329. package/dist/commands/init/types.d.ts +334 -0
  330. package/dist/commands/init/types.d.ts.map +1 -0
  331. package/dist/commands/init/types.js +31 -0
  332. package/dist/commands/init/types.js.map +1 -0
  333. package/dist/commands/init/wizard-terminal.d.ts +36 -0
  334. package/dist/commands/init/wizard-terminal.d.ts.map +1 -0
  335. package/dist/commands/init/wizard-terminal.js +163 -0
  336. package/dist/commands/init/wizard-terminal.js.map +1 -0
  337. package/dist/commands/init/wizard.d.ts +25 -0
  338. package/dist/commands/init/wizard.d.ts.map +1 -0
  339. package/dist/commands/init/wizard.js +358 -0
  340. package/dist/commands/init/wizard.js.map +1 -0
  341. package/dist/commands/mcp.d.ts +4 -0
  342. package/dist/commands/mcp.d.ts.map +1 -0
  343. package/dist/commands/mcp.js +111 -0
  344. package/dist/commands/mcp.js.map +1 -0
  345. package/dist/commands/product-test-runner.d.ts +4 -0
  346. package/dist/commands/product-test-runner.d.ts.map +1 -0
  347. package/dist/commands/product-test-runner.js +85 -0
  348. package/dist/commands/product-test-runner.js.map +1 -0
  349. package/dist/commands/product.d.ts +16 -0
  350. package/dist/commands/product.d.ts.map +1 -0
  351. package/dist/commands/product.js +262 -0
  352. package/dist/commands/product.js.map +1 -0
  353. package/dist/commands/query.d.ts +114 -0
  354. package/dist/commands/query.d.ts.map +1 -0
  355. package/dist/commands/query.js +783 -0
  356. package/dist/commands/query.js.map +1 -0
  357. package/dist/commands/scan.d.ts +10 -0
  358. package/dist/commands/scan.d.ts.map +1 -0
  359. package/dist/commands/scan.js +302 -0
  360. package/dist/commands/scan.js.map +1 -0
  361. package/dist/commands/sdd.d.ts +24 -0
  362. package/dist/commands/sdd.d.ts.map +1 -0
  363. package/dist/commands/sdd.js +195 -0
  364. package/dist/commands/sdd.js.map +1 -0
  365. package/dist/commands/unavailable.d.ts +40 -0
  366. package/dist/commands/unavailable.d.ts.map +1 -0
  367. package/dist/commands/unavailable.js +73 -0
  368. package/dist/commands/unavailable.js.map +1 -0
  369. package/dist/commands/watch/analyze.d.ts +69 -0
  370. package/dist/commands/watch/analyze.d.ts.map +1 -0
  371. package/dist/commands/watch/analyze.js +402 -0
  372. package/dist/commands/watch/analyze.js.map +1 -0
  373. package/dist/commands/watch/cli.d.ts +8 -0
  374. package/dist/commands/watch/cli.d.ts.map +1 -0
  375. package/dist/commands/watch/cli.js +97 -0
  376. package/dist/commands/watch/cli.js.map +1 -0
  377. package/dist/commands/watch/delta.d.ts +46 -0
  378. package/dist/commands/watch/delta.d.ts.map +1 -0
  379. package/dist/commands/watch/delta.js +170 -0
  380. package/dist/commands/watch/delta.js.map +1 -0
  381. package/dist/commands/watch/events.d.ts +214 -0
  382. package/dist/commands/watch/events.d.ts.map +1 -0
  383. package/dist/commands/watch/events.js +13 -0
  384. package/dist/commands/watch/events.js.map +1 -0
  385. package/dist/commands/watch/index.d.ts +19 -0
  386. package/dist/commands/watch/index.d.ts.map +1 -0
  387. package/dist/commands/watch/index.js +15 -0
  388. package/dist/commands/watch/index.js.map +1 -0
  389. package/dist/commands/watch/options.d.ts +43 -0
  390. package/dist/commands/watch/options.d.ts.map +1 -0
  391. package/dist/commands/watch/options.js +193 -0
  392. package/dist/commands/watch/options.js.map +1 -0
  393. package/dist/commands/watch/render.d.ts +26 -0
  394. package/dist/commands/watch/render.d.ts.map +1 -0
  395. package/dist/commands/watch/render.js +175 -0
  396. package/dist/commands/watch/render.js.map +1 -0
  397. package/dist/commands/watch/run.d.ts +14 -0
  398. package/dist/commands/watch/run.d.ts.map +1 -0
  399. package/dist/commands/watch/run.js +460 -0
  400. package/dist/commands/watch/run.js.map +1 -0
  401. package/dist/commands/watch/runtime.d.ts +67 -0
  402. package/dist/commands/watch/runtime.d.ts.map +1 -0
  403. package/dist/commands/watch/runtime.js +149 -0
  404. package/dist/commands/watch/runtime.js.map +1 -0
  405. package/dist/commands/watch-command.d.ts +3 -0
  406. package/dist/commands/watch-command.d.ts.map +1 -0
  407. package/dist/commands/watch-command.js +273 -0
  408. package/dist/commands/watch-command.js.map +1 -0
  409. package/dist/entry-point.d.ts +11 -0
  410. package/dist/entry-point.d.ts.map +1 -0
  411. package/dist/entry-point.js +65 -0
  412. package/dist/entry-point.js.map +1 -0
  413. package/dist/index.d.ts +38 -0
  414. package/dist/index.d.ts.map +1 -0
  415. package/dist/index.js +245 -0
  416. package/dist/index.js.map +1 -0
  417. package/dist/mcp/boundaries-schema.d.ts +262 -0
  418. package/dist/mcp/boundaries-schema.d.ts.map +1 -0
  419. package/dist/mcp/boundaries-schema.js +183 -0
  420. package/dist/mcp/boundaries-schema.js.map +1 -0
  421. package/dist/mcp/default-runtime.d.ts +16 -0
  422. package/dist/mcp/default-runtime.d.ts.map +1 -0
  423. package/dist/mcp/default-runtime.js +718 -0
  424. package/dist/mcp/default-runtime.js.map +1 -0
  425. package/dist/mcp/envelope.d.ts +13 -0
  426. package/dist/mcp/envelope.d.ts.map +1 -0
  427. package/dist/mcp/envelope.js +29 -0
  428. package/dist/mcp/envelope.js.map +1 -0
  429. package/dist/mcp/index.d.ts +8 -0
  430. package/dist/mcp/index.d.ts.map +1 -0
  431. package/dist/mcp/index.js +8 -0
  432. package/dist/mcp/index.js.map +1 -0
  433. package/dist/mcp/output-schemas.d.ts +5928 -0
  434. package/dist/mcp/output-schemas.d.ts.map +1 -0
  435. package/dist/mcp/output-schemas.js +2814 -0
  436. package/dist/mcp/output-schemas.js.map +1 -0
  437. package/dist/mcp/path-sandbox.d.ts +35 -0
  438. package/dist/mcp/path-sandbox.d.ts.map +1 -0
  439. package/dist/mcp/path-sandbox.js +128 -0
  440. package/dist/mcp/path-sandbox.js.map +1 -0
  441. package/dist/mcp/server.d.ts +155 -0
  442. package/dist/mcp/server.d.ts.map +1 -0
  443. package/dist/mcp/server.js +257 -0
  444. package/dist/mcp/server.js.map +1 -0
  445. package/dist/mcp/stdio.d.ts +20 -0
  446. package/dist/mcp/stdio.d.ts.map +1 -0
  447. package/dist/mcp/stdio.js +71 -0
  448. package/dist/mcp/stdio.js.map +1 -0
  449. package/dist/mcp/tools.d.ts +355 -0
  450. package/dist/mcp/tools.d.ts.map +1 -0
  451. package/dist/mcp/tools.js +319 -0
  452. package/dist/mcp/tools.js.map +1 -0
  453. package/dist/mcp/types.d.ts +15 -0
  454. package/dist/mcp/types.d.ts.map +1 -0
  455. package/dist/mcp/types.js +45 -0
  456. package/dist/mcp/types.js.map +1 -0
  457. package/package.json +99 -3
  458. package/src/cli/args.ts +231 -0
  459. package/src/cli/command.ts +146 -0
  460. package/src/cli/deadline.ts +56 -0
  461. package/src/cli/embedder-deadline.ts +187 -0
  462. package/src/cli/exit.ts +94 -0
  463. package/src/cli/graphs.ts +191 -0
  464. package/src/cli/index-freshness.ts +303 -0
  465. package/src/cli/index-refresh.ts +125 -0
  466. package/src/cli/index-state.ts +1217 -0
  467. package/src/cli/io.ts +56 -0
  468. package/src/cli/language-support.ts +203 -0
  469. package/src/cli/renewing-lease.ts +121 -0
  470. package/src/cli/search-graph-enrichment.ts +639 -0
  471. package/src/cli/system.ts +94 -0
  472. package/src/cli/vector-store-policy.ts +213 -0
  473. package/src/cli/version.ts +2 -0
  474. package/src/cli/workspace.ts +324 -0
  475. package/src/commands/complexity.ts +776 -0
  476. package/src/commands/doctor.languages.ts +187 -0
  477. package/src/commands/doctor.storage.ts +124 -0
  478. package/src/commands/doctor.ts +946 -0
  479. package/src/commands/extensions/add.ts +312 -0
  480. package/src/commands/extensions/configure.ts +82 -0
  481. package/src/commands/extensions/container.ts +209 -0
  482. package/src/commands/extensions/context.ts +109 -0
  483. package/src/commands/extensions/enable.ts +237 -0
  484. package/src/commands/extensions/environment.ts +218 -0
  485. package/src/commands/extensions/feature-scan.ts +232 -0
  486. package/src/commands/extensions/invoke.ts +368 -0
  487. package/src/commands/extensions/list.ts +158 -0
  488. package/src/commands/extensions/logs.ts +104 -0
  489. package/src/commands/extensions/provision.ts +206 -0
  490. package/src/commands/extensions/remove.ts +110 -0
  491. package/src/commands/extensions/render.ts +32 -0
  492. package/src/commands/extensions/run.ts +386 -0
  493. package/src/commands/extensions/secret-store.ts +223 -0
  494. package/src/commands/extensions/setup.ts +140 -0
  495. package/src/commands/extensions/source.ts +189 -0
  496. package/src/commands/extensions/state.ts +252 -0
  497. package/src/commands/extensions/status.ts +320 -0
  498. package/src/commands/extensions.ts +280 -0
  499. package/src/commands/flow/index.ts +402 -0
  500. package/src/commands/flow/limits.ts +14 -0
  501. package/src/commands/flow/output.ts +16 -0
  502. package/src/commands/flow/render.ts +318 -0
  503. package/src/commands/flow/select.ts +101 -0
  504. package/src/commands/graph/boundaries-input.ts +63 -0
  505. package/src/commands/graph/boundaries.ts +182 -0
  506. package/src/commands/graph/bounds.ts +25 -0
  507. package/src/commands/graph/category-clusters.ts +194 -0
  508. package/src/commands/graph/feature-3d.ts +141 -0
  509. package/src/commands/graph/feature-clusters.ts +125 -0
  510. package/src/commands/graph/feature-html.ts +336 -0
  511. package/src/commands/graph/feature-map.ts +270 -0
  512. package/src/commands/graph/feature-model.ts +234 -0
  513. package/src/commands/graph/feature-untangle-motion.ts +192 -0
  514. package/src/commands/graph/index.ts +562 -0
  515. package/src/commands/graph/render.ts +503 -0
  516. package/src/commands/graph/select.ts +77 -0
  517. package/src/commands/index/index.ts +595 -0
  518. package/src/commands/index/render.ts +325 -0
  519. package/src/commands/index/target.ts +136 -0
  520. package/src/commands/index-watch.ts +206 -0
  521. package/src/commands/init/command.ts +238 -0
  522. package/src/commands/init/config-draft.ts +235 -0
  523. package/src/commands/init/diff.ts +117 -0
  524. package/src/commands/init/http-probe.ts +150 -0
  525. package/src/commands/init/ignore-rules.ts +170 -0
  526. package/src/commands/init/index.ts +63 -0
  527. package/src/commands/init/integrations-apply.ts +618 -0
  528. package/src/commands/init/integrations-plan.ts +634 -0
  529. package/src/commands/init/integrations.ts +8 -0
  530. package/src/commands/init/languages.ts +320 -0
  531. package/src/commands/init/product-context.ts +576 -0
  532. package/src/commands/init/render.ts +598 -0
  533. package/src/commands/init/report.ts +204 -0
  534. package/src/commands/init/run.ts +349 -0
  535. package/src/commands/init/services.ts +271 -0
  536. package/src/commands/init/storage.ts +136 -0
  537. package/src/commands/init/toml.ts +174 -0
  538. package/src/commands/init/topology.ts +192 -0
  539. package/src/commands/init/types.ts +366 -0
  540. package/src/commands/init/wizard-terminal.ts +206 -0
  541. package/src/commands/init/wizard.ts +450 -0
  542. package/src/commands/mcp.ts +123 -0
  543. package/src/commands/product-test-runner.ts +80 -0
  544. package/src/commands/product.ts +331 -0
  545. package/src/commands/query.ts +1032 -0
  546. package/src/commands/scan.ts +360 -0
  547. package/src/commands/sdd.ts +243 -0
  548. package/src/commands/unavailable.ts +92 -0
  549. package/src/commands/watch/README.md +160 -0
  550. package/src/commands/watch/analyze.ts +600 -0
  551. package/src/commands/watch/cli.ts +108 -0
  552. package/src/commands/watch/delta.ts +186 -0
  553. package/src/commands/watch/events.ts +235 -0
  554. package/src/commands/watch/index.ts +53 -0
  555. package/src/commands/watch/options.ts +234 -0
  556. package/src/commands/watch/render.ts +240 -0
  557. package/src/commands/watch/run.ts +553 -0
  558. package/src/commands/watch/runtime.ts +167 -0
  559. package/src/commands/watch-command.ts +294 -0
  560. package/src/entry-point.ts +67 -0
  561. package/src/index.ts +291 -0
  562. package/src/mcp/boundaries-schema.ts +193 -0
  563. package/src/mcp/default-runtime.ts +1034 -0
  564. package/src/mcp/envelope.ts +57 -0
  565. package/src/mcp/index.ts +28 -0
  566. package/src/mcp/output-schemas.ts +3071 -0
  567. package/src/mcp/path-sandbox.ts +156 -0
  568. package/src/mcp/server.ts +358 -0
  569. package/src/mcp/stdio.ts +92 -0
  570. package/src/mcp/tools.ts +563 -0
  571. package/src/mcp/types.ts +66 -0
@@ -0,0 +1,1217 @@
1
+ /**
2
+ * Der lokale Indexzustand: welches Einbettungsprofil welchen Repo/Ref-Index
3
+ * gebaut hat.
4
+ *
5
+ * Seit Welle 9 ist das keine Singleton-Datei mehr. Eine physische
6
+ * Repository-Wurzel kann mehrere konfigurierte Repo-IDs, Refs und
7
+ * Einbettungsprofile tragen. Jeder dieser Zustaende liegt deshalb in einem
8
+ * eigenen, aus seiner kanonischen Identitaet gehashten Datensatz. Ein
9
+ * separater, generationsgebundener Pointer pro Repo/Ref bezeichnet das
10
+ * Profil des letzten gestarteten Laufs fuer `--embedder=auto`.
11
+ *
12
+ * `index-state.json` bleibt als lesbarer Migrations-Fallback erhalten. Sobald
13
+ * die Registry dieselbe logische Identitaet kennt, gewinnt jedoch immer ihr
14
+ * Datensatz; der Singleton wird dann nicht ein zweites Mal ausgegeben.
15
+ *
16
+ * Alle Dateinamen entstehen ausschliesslich aus SHA-256-Hashes. Registry und
17
+ * Datensaetze sind hart begrenzt. Korrupte, uebergrosse oder unbeschraenkte
18
+ * Registry-Inhalte werden nicht teilweise als gesund behandelt.
19
+ */
20
+
21
+ import { createHash, randomUUID } from "node:crypto";
22
+ import { constants as fsConstants } from "node:fs";
23
+ import { lstat, mkdir, open, opendir, rename, rm, writeFile } from "node:fs/promises";
24
+ import path from "node:path";
25
+
26
+ /** Welcher Einbetter einen Index gebaut hat. */
27
+ export type EmbedderChoice = "service" | "offline";
28
+
29
+ /** Lifecycle persisted for query routing and index-completeness provenance. */
30
+ export type IndexStateOutcome = "running" | "completed" | "cancelled" | "failed";
31
+
32
+ /** Der Zustand eines indizierten Repositories. Reine Daten. */
33
+ export interface IndexStateV1 {
34
+ readonly schemaVersion: 1;
35
+ readonly workspaceId: string;
36
+ readonly repoId: string;
37
+ readonly refId: string;
38
+ readonly refName: string;
39
+ /** `service` = der konfigurierte Dienst, `offline` = netzlos im Prozess. */
40
+ readonly embedder: EmbedderChoice;
41
+ readonly embeddingProfileId: string;
42
+ readonly collection: string;
43
+ /** Legacy Qdrant endpoint. Absent for embedded records. */
44
+ readonly qdrantUrl?: string;
45
+ readonly storageBackend?: "lancedb" | "qdrant";
46
+ readonly storeUrl?: string;
47
+ /** Generation des Indexlaufs; fehlt nur in vor Welle 9 geschriebenen Zuständen. */
48
+ readonly generationId?: string;
49
+ /** SHA-256 fingerprint of the complete confirmed file/content manifest. */
50
+ readonly corpusRevisionFingerprint?: string;
51
+ /** Completion time, indexing configuration and lexical format of this snapshot. */
52
+ readonly indexedAt?: string;
53
+ readonly indexConfigFingerprint?: string;
54
+ readonly lexicalTokenizerVersion?: string;
55
+ /** Ergebnis des Laufs, der diesen Zustand geschrieben hat. */
56
+ readonly outcome: IndexStateOutcome;
57
+ }
58
+
59
+ /** Identitaet eines Registry-Datensatzes. Collection/URL duerfen sich aendern. */
60
+ export type IndexStateRecordIdentity = Pick<
61
+ IndexStateV1,
62
+ "workspaceId" | "repoId" | "refId" | "embeddingProfileId"
63
+ > &
64
+ StorageStateIdentity;
65
+
66
+ /** Identitaet des aktuellen Profils eines Repo/Ref-Paares. */
67
+ interface StorageStateIdentity {
68
+ readonly storageBackend?: "lancedb" | "qdrant";
69
+ readonly storeUrl?: string;
70
+ }
71
+
72
+ export type IndexStateCurrentIdentity = Pick<IndexStateV1, "workspaceId" | "repoId" | "refId"> &
73
+ StorageStateIdentity;
74
+
75
+ /**
76
+ * Index-Report-Daten, aus denen der CLI-Adapter einen persistierbaren Zustand
77
+ * ableitet. Ein Readiness-Fehler vor der Generation darf einen nutzbaren
78
+ * Zustand nicht ueberschreiben. Ein spaeter Fehler muss ihn invalidieren.
79
+ */
80
+ export type IndexStateAfterRunInput = Omit<IndexStateV1, "generationId" | "outcome"> & {
81
+ readonly generationId: string | null;
82
+ readonly outcome: Exclude<IndexStateOutcome, "running">;
83
+ };
84
+
85
+ export function indexStateAfterRun(input: IndexStateAfterRunInput): IndexStateV1 | null {
86
+ const { generationId, corpusRevisionFingerprint, ...state } = input;
87
+ if (state.outcome === "failed" && generationId === null) return null;
88
+ return {
89
+ ...state,
90
+ ...(generationId === null ? {} : { generationId }),
91
+ ...(state.outcome === "completed" && corpusRevisionFingerprint !== undefined
92
+ ? { corpusRevisionFingerprint }
93
+ : {}),
94
+ };
95
+ }
96
+
97
+ /** Alter Singleton-Name unterhalb von `.untangle/`. Nur noch Lesefallback. */
98
+ export const INDEX_STATE_FILE = "index-state.json";
99
+ export const INDEX_STATE_REGISTRY_DIR = "index-states-v1";
100
+ export const INDEX_STATE_RECORDS_DIR = "records";
101
+ export const INDEX_STATE_CURRENT_DIR = "current";
102
+
103
+ /** Harte Ressourcenlimits fuer lokal kontrollierbare Registry-Lesevorgaenge. */
104
+ export const MAX_INDEX_STATE_FILE_BYTES = 64 * 1024;
105
+ export const MAX_INDEX_STATE_REGISTRY_ENTRIES = 512;
106
+ const MAX_INDEX_STATE_FIELD_LENGTH = 8 * 1024;
107
+
108
+ const HASH_FILE_PATTERN = /^[0-9a-f]{64}\.json$/u;
109
+ const TEMP_FILE_PATTERN = /^[0-9a-f]{64}\.json\.[0-9a-f-]{36}\.tmp$/u;
110
+ const READ_ONLY_NO_FOLLOW = fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW;
111
+
112
+ /** `<repoRoot>/.untangle` — dasselbe Verzeichnis, das `untangle index` anlegt. */
113
+ export function stateRootOf(repoRoot: string): string {
114
+ return path.join(repoRoot, ".untangle");
115
+ }
116
+
117
+ /** Der alte Singleton-Pfad. Externe Hinweise und Migration duerfen ihn weiter nennen. */
118
+ export function indexStatePath(stateRoot: string): string {
119
+ return path.join(stateRoot, INDEX_STATE_FILE);
120
+ }
121
+
122
+ export function indexStateRecordKey(identity: IndexStateRecordIdentity): string {
123
+ return hashCanonical([
124
+ "untangle-index-state-record-v1",
125
+ identity.workspaceId,
126
+ identity.repoId,
127
+ identity.refId,
128
+ identity.embeddingProfileId,
129
+ ...storageIdentitySuffix(identity),
130
+ ]);
131
+ }
132
+
133
+ export function indexStateCurrentKey(identity: IndexStateCurrentIdentity): string {
134
+ return hashCanonical([
135
+ "untangle-index-state-current-v1",
136
+ identity.workspaceId,
137
+ identity.repoId,
138
+ identity.refId,
139
+ ...storageIdentitySuffix(identity),
140
+ ]);
141
+ }
142
+
143
+ /** Preserve old Qdrant keys; embedded paths get independent publication namespaces. */
144
+ function storageIdentitySuffix(identity: StorageStateIdentity): string[] {
145
+ return identity.storageBackend === "lancedb" ? ["lancedb", identity.storeUrl ?? ""] : [];
146
+ }
147
+
148
+ /** Lock key shared by every profile publication for one repo/ref/backend pointer. */
149
+ export function indexStateLockKey(identity: IndexStateCurrentIdentity): string {
150
+ return `index-state:${indexStateCurrentKey(identity)}`;
151
+ }
152
+
153
+ export function indexStateRecordPath(
154
+ stateRoot: string,
155
+ identity: IndexStateRecordIdentity,
156
+ ): string {
157
+ return path.join(
158
+ stateRoot,
159
+ INDEX_STATE_REGISTRY_DIR,
160
+ INDEX_STATE_RECORDS_DIR,
161
+ `${indexStateRecordKey(identity)}.json`,
162
+ );
163
+ }
164
+
165
+ export function indexStateCurrentPath(
166
+ stateRoot: string,
167
+ identity: IndexStateCurrentIdentity,
168
+ ): string {
169
+ return path.join(
170
+ stateRoot,
171
+ INDEX_STATE_REGISTRY_DIR,
172
+ INDEX_STATE_CURRENT_DIR,
173
+ `${indexStateCurrentKey(identity)}.json`,
174
+ );
175
+ }
176
+
177
+ export type IndexStateWriteResult =
178
+ | { ok: true; path: string }
179
+ | {
180
+ ok: false;
181
+ error: unknown;
182
+ path: string;
183
+ /** `true` means an earlier state cannot still masquerade as this run. */
184
+ previousStateInvalidated: boolean;
185
+ invalidationError?: unknown;
186
+ };
187
+
188
+ export type TerminalIndexStateWriteResult =
189
+ (IndexStateWriteResult & { superseded: false }) | { ok: true; path: string; superseded: true };
190
+
191
+ /** Owner check supplied by the renewable publication lease. */
192
+ export interface IndexStatePublicationOptions {
193
+ /** Must reject after lease ownership was lost. Called directly before every live rename. */
194
+ readonly assertOwnership?: () => Promise<void>;
195
+ }
196
+
197
+ interface CurrentPointerV1 extends StorageStateIdentity {
198
+ readonly schemaVersion: 1;
199
+ readonly workspaceId: string;
200
+ readonly repoId: string;
201
+ readonly refId: string;
202
+ readonly embeddingProfileId: string;
203
+ readonly generationId: string;
204
+ readonly recordKey: string;
205
+ }
206
+
207
+ /**
208
+ * Publiziert einen Registry-Datensatz und macht ihn zum aktuellen Profil des
209
+ * Repo/Ref-Paares. Beide Dateien werden einzeln atomar ersetzt. Der Aufrufer
210
+ * serialisiert die kurze Sequenz mit {@link indexStateLockKey}.
211
+ *
212
+ * Misslingt ein Teil, werden der neue Datensatz, der current-Pointer und ein
213
+ * passender Legacy-Fallback invalidiert. Damit kann ein neuer Lauf nie einen
214
+ * alten `completed`-Hinweis gesund aussehen lassen.
215
+ */
216
+ export async function writeIndexState(
217
+ stateRoot: string,
218
+ state: IndexStateV1,
219
+ options: IndexStatePublicationOptions = {},
220
+ ): Promise<IndexStateWriteResult> {
221
+ const recordTarget = indexStateRecordPath(stateRoot, state);
222
+ const serializedState = serializeRegistryState(state);
223
+ if (serializedState === null) {
224
+ const error = new Error("Registry index states require a valid generation-bound state.");
225
+ const invalidated = await invalidateAfterFailure(stateRoot, state, "unconditional", options);
226
+ return failedWrite(recordTarget, error, invalidated);
227
+ }
228
+
229
+ try {
230
+ await atomicWrite(recordTarget, serializedState, options.assertOwnership);
231
+ } catch (error) {
232
+ const invalidated = await invalidateAfterFailure(stateRoot, state, "unconditional", options);
233
+ return failedWrite(recordTarget, error, invalidated);
234
+ }
235
+
236
+ const pointerTarget = indexStateCurrentPath(stateRoot, state);
237
+ const pointer = currentPointerFor(state);
238
+ try {
239
+ await atomicWrite(
240
+ pointerTarget,
241
+ `${JSON.stringify(pointer, null, 2)}\n`,
242
+ options.assertOwnership,
243
+ );
244
+ return { ok: true, path: recordTarget };
245
+ } catch (error) {
246
+ const invalidated = await invalidateAfterFailure(stateRoot, state, "unconditional", options);
247
+ return failedWrite(pointerTarget, error, invalidated);
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Replaces exactly one `running` registry record while its generation still
253
+ * matches. The current pointer is deliberately not republished: if another
254
+ * profile became current while this run finished, a late terminal write may
255
+ * archive its own result but cannot steal `auto` routing back.
256
+ */
257
+ export async function replaceRunningIndexState(
258
+ stateRoot: string,
259
+ terminalState: IndexStateV1,
260
+ options: IndexStatePublicationOptions = {},
261
+ ): Promise<TerminalIndexStateWriteResult> {
262
+ const target = indexStateRecordPath(stateRoot, terminalState);
263
+ const current = await readExactRegistryRecord(target, terminalState);
264
+ if (current.kind === "absent") {
265
+ return { ok: true, path: target, superseded: true };
266
+ }
267
+ if (current.kind === "invalid") {
268
+ const invalidated = await invalidateAfterFailure(stateRoot, terminalState, "matching", options);
269
+ return {
270
+ ...failedWrite(target, new Error(current.message), invalidated),
271
+ superseded: false,
272
+ };
273
+ }
274
+ if (!isRunningStateFor(current.state, terminalState)) {
275
+ return { ok: true, path: target, superseded: true };
276
+ }
277
+
278
+ const serializedState = serializeRegistryState(terminalState);
279
+ if (serializedState === null) {
280
+ const error = new Error("Terminal registry states require the running generation identity.");
281
+ const invalidated = await invalidateAfterFailure(stateRoot, terminalState, "matching", options);
282
+ return { ...failedWrite(target, error, invalidated), superseded: false };
283
+ }
284
+ try {
285
+ await atomicWrite(target, serializedState, options.assertOwnership);
286
+ return { ok: true, path: target, superseded: false };
287
+ } catch (error) {
288
+ const invalidated = await invalidateAfterFailure(stateRoot, terminalState, "matching", options);
289
+ return { ...failedWrite(target, error, invalidated), superseded: false };
290
+ }
291
+ }
292
+
293
+ function isRunningStateFor(current: IndexStateV1, terminal: IndexStateV1): boolean {
294
+ return (
295
+ current.outcome === "running" &&
296
+ terminal.outcome !== "running" &&
297
+ current.workspaceId === terminal.workspaceId &&
298
+ current.repoId === terminal.repoId &&
299
+ current.refId === terminal.refId &&
300
+ current.refName === terminal.refName &&
301
+ current.embedder === terminal.embedder &&
302
+ current.embeddingProfileId === terminal.embeddingProfileId &&
303
+ current.collection === terminal.collection &&
304
+ (current.storageBackend ?? "qdrant") === (terminal.storageBackend ?? "qdrant") &&
305
+ (current.storeUrl ?? current.qdrantUrl) === (terminal.storeUrl ?? terminal.qdrantUrl) &&
306
+ current.generationId !== undefined &&
307
+ current.generationId === terminal.generationId
308
+ );
309
+ }
310
+
311
+ /**
312
+ * Compatibility reader for old callers. It returns a state only when exactly
313
+ * one current repo/ref state exists below this physical root; callers needing
314
+ * every context use {@link resolveIndexState}.
315
+ */
316
+ export async function readIndexState(stateRoot: string): Promise<IndexStateV1 | null> {
317
+ const registry = await readLocalRegistry(stateRoot);
318
+ if (registry.kind === "invalid") return null;
319
+ const current = registry.entries.filter((entry) => entry.current);
320
+ return current.length === 1 ? (current[0]?.state ?? null) : null;
321
+ }
322
+
323
+ /** The parser remains public for legacy migration and focused tests. */
324
+ export function parseIndexState(text: string): IndexStateV1 | null {
325
+ if (Buffer.byteLength(text, "utf8") > MAX_INDEX_STATE_FILE_BYTES) return null;
326
+ let raw: unknown;
327
+ try {
328
+ raw = JSON.parse(text);
329
+ } catch {
330
+ return null;
331
+ }
332
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
333
+ const record = raw as Record<string, unknown>;
334
+ if (record["schemaVersion"] !== 1) return null;
335
+ const embedder = record["embedder"];
336
+ if (embedder !== "service" && embedder !== "offline") return null;
337
+ const outcome = record["outcome"];
338
+ if (
339
+ outcome !== "running" &&
340
+ outcome !== "completed" &&
341
+ outcome !== "cancelled" &&
342
+ outcome !== "failed"
343
+ ) {
344
+ return null;
345
+ }
346
+
347
+ const strings = [
348
+ "workspaceId",
349
+ "repoId",
350
+ "refId",
351
+ "refName",
352
+ "embeddingProfileId",
353
+ "collection",
354
+ ] as const;
355
+ const values: Record<string, string> = {};
356
+ for (const key of strings) {
357
+ const value = record[key];
358
+ if (!isBoundedString(value)) return null;
359
+ values[key] = value;
360
+ }
361
+ const storage = parseStorageIdentity(record, true);
362
+ if (storage === null) return null;
363
+ const generationId = record["generationId"];
364
+ if (generationId !== undefined && !isBoundedString(generationId)) return null;
365
+ const corpusRevisionFingerprint = record["corpusRevisionFingerprint"];
366
+ if (
367
+ corpusRevisionFingerprint !== undefined &&
368
+ (typeof corpusRevisionFingerprint !== "string" ||
369
+ !/^[0-9a-f]{64}$/u.test(corpusRevisionFingerprint))
370
+ ) {
371
+ return null;
372
+ }
373
+ const indexedAt = record["indexedAt"];
374
+ const indexConfigFingerprint = record["indexConfigFingerprint"];
375
+ const lexicalTokenizerVersion = record["lexicalTokenizerVersion"];
376
+ if (
377
+ indexedAt !== undefined &&
378
+ (typeof indexedAt !== "string" || !Number.isFinite(Date.parse(indexedAt)))
379
+ )
380
+ return null;
381
+ if (
382
+ indexConfigFingerprint !== undefined &&
383
+ (typeof indexConfigFingerprint !== "string" || !/^[0-9a-f]{64}$/u.test(indexConfigFingerprint))
384
+ )
385
+ return null;
386
+ if (lexicalTokenizerVersion !== undefined && !isBoundedString(lexicalTokenizerVersion))
387
+ return null;
388
+
389
+ return {
390
+ schemaVersion: 1,
391
+ workspaceId: values["workspaceId"] as string,
392
+ repoId: values["repoId"] as string,
393
+ refId: values["refId"] as string,
394
+ refName: values["refName"] as string,
395
+ embedder,
396
+ embeddingProfileId: values["embeddingProfileId"] as string,
397
+ collection: values["collection"] as string,
398
+ ...storage,
399
+ ...(generationId === undefined ? {} : { generationId }),
400
+ ...(corpusRevisionFingerprint === undefined ? {} : { corpusRevisionFingerprint }),
401
+ ...(indexedAt === undefined ? {} : { indexedAt }),
402
+ ...(indexConfigFingerprint === undefined ? {} : { indexConfigFingerprint }),
403
+ ...(lexicalTokenizerVersion === undefined ? {} : { lexicalTokenizerVersion }),
404
+ outcome,
405
+ };
406
+ }
407
+
408
+ /** Ein gefundener Zustand samt Herkunft und Auto-Routing-Rolle. */
409
+ export interface FoundIndexState {
410
+ readonly repoRoot: string;
411
+ readonly statePath: string;
412
+ readonly source: "registry" | "legacy";
413
+ readonly current: boolean;
414
+ readonly state: IndexStateV1;
415
+ }
416
+
417
+ /** Optional query scope applied before states can influence auto-routing. */
418
+ export interface IndexStateScope {
419
+ readonly repoIds?: readonly string[];
420
+ /** Accepts human ref names as well as canonical repo-qualified ref IDs. */
421
+ readonly refs?: readonly string[];
422
+ readonly storageBackend?: "lancedb" | "qdrant";
423
+ readonly storeUrl?: string;
424
+ }
425
+
426
+ export interface IndexStateDiagnostic {
427
+ readonly code:
428
+ "INDEX_STATE_CORRUPT" | "INDEX_STATE_IO" | "INDEX_STATE_OVERSIZED" | "INDEX_STATE_UNBOUNDED";
429
+ readonly path: string;
430
+ readonly message: string;
431
+ }
432
+
433
+ /**
434
+ * `all` contains every selected profile record. `found`/`current` only use
435
+ * the generation-checked current pointer and therefore drive `auto` routing.
436
+ */
437
+ export type IndexStateResolution =
438
+ | {
439
+ readonly kind: "resolved";
440
+ readonly embedder: EmbedderChoice;
441
+ readonly found: FoundIndexState;
442
+ readonly current: readonly FoundIndexState[];
443
+ readonly all: readonly FoundIndexState[];
444
+ }
445
+ | { readonly kind: "none" }
446
+ | {
447
+ readonly kind: "ambiguous";
448
+ readonly found: readonly FoundIndexState[];
449
+ readonly all: readonly FoundIndexState[];
450
+ }
451
+ | {
452
+ readonly kind: "invalid";
453
+ readonly diagnostics: readonly IndexStateDiagnostic[];
454
+ /** Valid states from other roots are exposed for diagnosis, never auto-used. */
455
+ readonly all: readonly FoundIndexState[];
456
+ };
457
+
458
+ /**
459
+ * Enumerates every bounded registry under the candidate physical roots.
460
+ * Roots are de-duplicated, records are not: one root can legitimately contain
461
+ * multiple repo IDs, refs and profiles.
462
+ */
463
+ export async function resolveIndexState(
464
+ candidateRoots: readonly string[],
465
+ workspaceId: string,
466
+ scope: IndexStateScope = {},
467
+ ): Promise<IndexStateResolution> {
468
+ const collected = await collectScopedIndexStates(candidateRoots, workspaceId, scope);
469
+ collected.found.sort(compareFoundStates);
470
+ if (collected.diagnostics.length > 0) {
471
+ collected.diagnostics.sort(
472
+ (a, b) => compareText(a.path, b.path) || compareText(a.code, b.code),
473
+ );
474
+ return { kind: "invalid", diagnostics: collected.diagnostics, all: collected.found };
475
+ }
476
+ return resolveCollectedStates(collected.found);
477
+ }
478
+
479
+ async function collectScopedIndexStates(
480
+ candidateRoots: readonly string[],
481
+ workspaceId: string,
482
+ scope: IndexStateScope,
483
+ ): Promise<{ found: FoundIndexState[]; diagnostics: IndexStateDiagnostic[] }> {
484
+ const selectedRepoIds = new Set(scope.repoIds ?? []);
485
+ const selectedRefs = new Set(scope.refs ?? []);
486
+ const seenRoots = new Set<string>();
487
+ const found: FoundIndexState[] = [];
488
+ const diagnostics: IndexStateDiagnostic[] = [];
489
+
490
+ for (const candidateRoot of candidateRoots) {
491
+ const repoRoot = path.resolve(candidateRoot);
492
+ if (seenRoots.has(repoRoot)) continue;
493
+ seenRoots.add(repoRoot);
494
+ const registry = await readLocalRegistry(stateRootOf(repoRoot));
495
+ if (registry.kind === "invalid") {
496
+ diagnostics.push(...registry.diagnostics);
497
+ continue;
498
+ }
499
+ for (const entry of registry.entries) {
500
+ if (!stateMatchesScope(entry.state, workspaceId, selectedRepoIds, selectedRefs, scope))
501
+ continue;
502
+ found.push({ repoRoot, ...entry });
503
+ }
504
+ }
505
+ return { found, diagnostics };
506
+ }
507
+
508
+ function stateMatchesScope(
509
+ state: IndexStateV1,
510
+ workspaceId: string,
511
+ selectedRepoIds: ReadonlySet<string>,
512
+ selectedRefs: ReadonlySet<string>,
513
+ scope: IndexStateScope,
514
+ ): boolean {
515
+ return (
516
+ state.workspaceId === workspaceId &&
517
+ (scope.storageBackend === undefined ||
518
+ (state.storageBackend ?? "qdrant") === scope.storageBackend) &&
519
+ (scope.storeUrl === undefined || (state.storeUrl ?? state.qdrantUrl) === scope.storeUrl) &&
520
+ (selectedRepoIds.size === 0 || selectedRepoIds.has(state.repoId)) &&
521
+ (selectedRefs.size === 0 || selectedRefs.has(state.refId) || selectedRefs.has(state.refName))
522
+ );
523
+ }
524
+
525
+ function resolveCollectedStates(found: readonly FoundIndexState[]): IndexStateResolution {
526
+ if (found.length === 0) return { kind: "none" };
527
+
528
+ const current = found.filter((entry) => entry.current);
529
+ if (current.length === 0) {
530
+ return {
531
+ kind: "invalid",
532
+ diagnostics: [
533
+ {
534
+ code: "INDEX_STATE_CORRUPT",
535
+ path: found[0]?.statePath ?? "(index-state registry)",
536
+ message: "The selected registry states have no valid current pointer.",
537
+ },
538
+ ],
539
+ all: found,
540
+ };
541
+ }
542
+ const first = current[0] as FoundIndexState;
543
+ if (current.some((entry) => entry.state.embedder !== first.state.embedder)) {
544
+ return { kind: "ambiguous", found: current, all: found };
545
+ }
546
+ return { kind: "resolved", embedder: first.state.embedder, found: first, current, all: found };
547
+ }
548
+
549
+ interface LocalRegistryEntry {
550
+ readonly statePath: string;
551
+ readonly source: "registry" | "legacy";
552
+ readonly current: boolean;
553
+ readonly state: IndexStateV1;
554
+ }
555
+
556
+ type LocalRegistryRead =
557
+ | { readonly kind: "valid"; readonly entries: readonly LocalRegistryEntry[] }
558
+ | { readonly kind: "invalid"; readonly diagnostics: readonly IndexStateDiagnostic[] };
559
+ type InvalidRegistryRead = Extract<LocalRegistryRead, { readonly kind: "invalid" }>;
560
+
561
+ type RegistryPartRead<Value> =
562
+ | { readonly kind: "valid"; readonly value: Value }
563
+ | { readonly kind: "invalid"; readonly diagnostics: readonly IndexStateDiagnostic[] };
564
+
565
+ type RegistryRecordMap = Map<string, { state: IndexStateV1; statePath: string }>;
566
+
567
+ async function readLocalRegistry(stateRoot: string): Promise<LocalRegistryRead> {
568
+ const recordsDirectory = path.join(stateRoot, INDEX_STATE_REGISTRY_DIR, INDEX_STATE_RECORDS_DIR);
569
+ const currentDirectory = path.join(stateRoot, INDEX_STATE_REGISTRY_DIR, INDEX_STATE_CURRENT_DIR);
570
+ const recordsListing = await listRegistryFiles(recordsDirectory);
571
+ if (recordsListing.kind === "invalid") return recordsListing;
572
+ const currentListing = await listRegistryFiles(currentDirectory);
573
+ if (currentListing.kind === "invalid") return currentListing;
574
+
575
+ const recordRead = await readRegistryRecords(recordsListing.paths);
576
+ if (recordRead.kind === "invalid") return recordRead;
577
+ const currentRead = await readCurrentPointers(currentListing.paths, recordRead.value);
578
+ if (currentRead.kind === "invalid") return currentRead;
579
+ const missingPointer = firstRecordWithoutCurrent(recordRead.value, currentRead.value);
580
+ if (missingPointer !== null) return missingPointer;
581
+
582
+ const entries = registryEntries(recordRead.value, currentRead.value);
583
+ const legacyMerge = await mergeLegacyState(
584
+ stateRoot,
585
+ recordRead.value,
586
+ currentRead.value,
587
+ entries,
588
+ );
589
+ if (legacyMerge !== null) return legacyMerge;
590
+
591
+ entries.sort((a, b) => compareStateEntries(a, b));
592
+ return { kind: "valid", entries };
593
+ }
594
+
595
+ async function readRegistryRecords(
596
+ paths: readonly string[],
597
+ ): Promise<RegistryPartRead<RegistryRecordMap>> {
598
+ const records: RegistryRecordMap = new Map();
599
+ for (const recordPath of paths) {
600
+ const read = await readBoundedRegularFile(recordPath);
601
+ if (read.kind === "absent") {
602
+ return invalidRegistry(recordPath, "Registry record disappeared while being read.");
603
+ }
604
+ if (read.kind === "invalid") return invalidFileRead(recordPath, read);
605
+ const state = parseIndexState(read.text);
606
+ if (state === null || state.generationId === undefined) {
607
+ return invalidRegistry(recordPath, "Registry record is invalid or has no generation.");
608
+ }
609
+ const key = indexStateRecordKey(state);
610
+ if (path.basename(recordPath) !== `${key}.json`) {
611
+ return invalidRegistry(
612
+ recordPath,
613
+ "Registry filename does not match the canonical identity.",
614
+ );
615
+ }
616
+ records.set(key, { state, statePath: recordPath });
617
+ }
618
+ return { kind: "valid", value: records };
619
+ }
620
+
621
+ async function readCurrentPointers(
622
+ paths: readonly string[],
623
+ records: RegistryRecordMap,
624
+ ): Promise<RegistryPartRead<Map<string, string>>> {
625
+ const currentByGroup = new Map<string, string>();
626
+ for (const pointerPath of paths) {
627
+ const read = await readBoundedRegularFile(pointerPath);
628
+ if (read.kind === "absent") {
629
+ return invalidRegistry(pointerPath, "Current pointer disappeared while being read.");
630
+ }
631
+ if (read.kind === "invalid") return invalidFileRead(pointerPath, read);
632
+ const parsed = validateCurrentPointer(pointerPath, read.text, records, currentByGroup);
633
+ if (parsed.kind === "invalid") return parsed;
634
+ currentByGroup.set(parsed.value.currentKey, parsed.value.recordKey);
635
+ }
636
+ return { kind: "valid", value: currentByGroup };
637
+ }
638
+
639
+ function validateCurrentPointer(
640
+ pointerPath: string,
641
+ text: string,
642
+ records: RegistryRecordMap,
643
+ currentByGroup: ReadonlyMap<string, string>,
644
+ ): RegistryPartRead<{ currentKey: string; recordKey: string }> {
645
+ const pointer = parseCurrentPointer(text);
646
+ if (pointer === null) return invalidRegistry(pointerPath, "Current pointer is invalid.");
647
+ const currentKey = indexStateCurrentKey(pointer);
648
+ if (path.basename(pointerPath) !== `${currentKey}.json`) {
649
+ return invalidRegistry(
650
+ pointerPath,
651
+ "Pointer filename does not match the repository/ref identity.",
652
+ );
653
+ }
654
+ const pointed = records.get(pointer.recordKey);
655
+ if (pointed === undefined || !pointerMatchesState(pointer, pointed.state)) {
656
+ return invalidRegistry(
657
+ pointerPath,
658
+ "Current pointer does not reference the same profile- and generation-bound record.",
659
+ );
660
+ }
661
+ if (currentByGroup.has(currentKey)) {
662
+ return invalidRegistry(pointerPath, "Repository/ref has more than one current pointer.");
663
+ }
664
+ return { kind: "valid", value: { currentKey, recordKey: pointer.recordKey } };
665
+ }
666
+
667
+ function firstRecordWithoutCurrent(
668
+ records: RegistryRecordMap,
669
+ currentByGroup: ReadonlyMap<string, string>,
670
+ ): LocalRegistryRead | null {
671
+ for (const { state, statePath } of records.values()) {
672
+ if (!currentByGroup.has(indexStateCurrentKey(state))) {
673
+ return invalidRegistry(
674
+ statePath,
675
+ "Registry record belongs to a repository/ref without a current pointer.",
676
+ );
677
+ }
678
+ }
679
+ return null;
680
+ }
681
+
682
+ function registryEntries(
683
+ records: RegistryRecordMap,
684
+ currentByGroup: ReadonlyMap<string, string>,
685
+ ): LocalRegistryEntry[] {
686
+ return [...records.entries()].map(([recordKey, { state, statePath }]) => ({
687
+ state,
688
+ statePath,
689
+ source: "registry",
690
+ current: currentByGroup.get(indexStateCurrentKey(state)) === recordKey,
691
+ }));
692
+ }
693
+
694
+ async function mergeLegacyState(
695
+ stateRoot: string,
696
+ records: RegistryRecordMap,
697
+ currentByGroup: ReadonlyMap<string, string>,
698
+ entries: LocalRegistryEntry[],
699
+ ): Promise<LocalRegistryRead | null> {
700
+ const legacyPath = indexStatePath(stateRoot);
701
+ const legacyRead = await readBoundedRegularFile(legacyPath);
702
+ if (legacyRead.kind === "absent") return null;
703
+ if (legacyRead.kind === "invalid") return invalidFileRead(legacyPath, legacyRead);
704
+ const legacyState = parseIndexState(legacyRead.text);
705
+ if (legacyState === null) {
706
+ return invalidRegistry(legacyPath, "Legacy index state is invalid.");
707
+ }
708
+ if (records.has(indexStateRecordKey(legacyState))) return null;
709
+ entries.push({
710
+ state: legacyState,
711
+ statePath: legacyPath,
712
+ source: "legacy",
713
+ current: !currentByGroup.has(indexStateCurrentKey(legacyState)),
714
+ });
715
+ return null;
716
+ }
717
+
718
+ type DirectoryListing =
719
+ | { readonly kind: "valid"; readonly paths: readonly string[] }
720
+ | { readonly kind: "invalid"; readonly diagnostics: readonly IndexStateDiagnostic[] };
721
+
722
+ async function listRegistryFiles(directory: string): Promise<DirectoryListing> {
723
+ try {
724
+ const information = await lstat(directory);
725
+ if (!information.isDirectory()) {
726
+ return {
727
+ kind: "invalid",
728
+ diagnostics: [
729
+ diagnostic(
730
+ "INDEX_STATE_CORRUPT",
731
+ directory,
732
+ "Registry directory is not a regular local directory.",
733
+ ),
734
+ ],
735
+ };
736
+ }
737
+ } catch (error) {
738
+ if (isNotFound(error)) return { kind: "valid", paths: [] };
739
+ return {
740
+ kind: "invalid",
741
+ diagnostics: [
742
+ diagnostic("INDEX_STATE_IO", directory, `Cannot inspect registry: ${errorText(error)}`),
743
+ ],
744
+ };
745
+ }
746
+
747
+ let handle;
748
+ try {
749
+ handle = await opendir(directory);
750
+ } catch (error) {
751
+ if (isNotFound(error)) return { kind: "valid", paths: [] };
752
+ return {
753
+ kind: "invalid",
754
+ diagnostics: [
755
+ diagnostic("INDEX_STATE_IO", directory, `Cannot read registry: ${errorText(error)}`),
756
+ ],
757
+ };
758
+ }
759
+
760
+ const names: string[] = [];
761
+ try {
762
+ for await (const entry of handle) {
763
+ names.push(entry.name);
764
+ if (names.length > MAX_INDEX_STATE_REGISTRY_ENTRIES) {
765
+ return {
766
+ kind: "invalid",
767
+ diagnostics: [
768
+ diagnostic(
769
+ "INDEX_STATE_UNBOUNDED",
770
+ directory,
771
+ `Registry contains more than ${String(MAX_INDEX_STATE_REGISTRY_ENTRIES)} Eintraege.`,
772
+ ),
773
+ ],
774
+ };
775
+ }
776
+ if (TEMP_FILE_PATTERN.test(entry.name) && entry.isFile()) continue;
777
+ if (!HASH_FILE_PATTERN.test(entry.name) || !entry.isFile()) {
778
+ return {
779
+ kind: "invalid",
780
+ diagnostics: [
781
+ diagnostic(
782
+ "INDEX_STATE_CORRUPT",
783
+ path.join(directory, entry.name),
784
+ "Registry accepts only regular JSON records with canonical filenames.",
785
+ ),
786
+ ],
787
+ };
788
+ }
789
+ }
790
+ } catch (error) {
791
+ return {
792
+ kind: "invalid",
793
+ diagnostics: [
794
+ diagnostic("INDEX_STATE_IO", directory, `Registry read failed: ${errorText(error)}`),
795
+ ],
796
+ };
797
+ } finally {
798
+ await handle.close().catch(() => undefined);
799
+ }
800
+
801
+ return {
802
+ kind: "valid",
803
+ paths: names
804
+ .filter((name) => HASH_FILE_PATTERN.test(name))
805
+ .sort(compareText)
806
+ .map((name) => path.join(directory, name)),
807
+ };
808
+ }
809
+
810
+ type BoundedFileRead =
811
+ | { readonly kind: "absent" }
812
+ | { readonly kind: "valid"; readonly text: string }
813
+ | {
814
+ readonly kind: "invalid";
815
+ readonly reason: "io" | "oversized" | "not_regular";
816
+ readonly message: string;
817
+ };
818
+
819
+ async function readBoundedRegularFile(filePath: string): Promise<BoundedFileRead> {
820
+ try {
821
+ const information = await lstat(filePath);
822
+ if (!information.isFile()) {
823
+ return { kind: "invalid", reason: "not_regular", message: "Path is not a regular file." };
824
+ }
825
+ if (information.size > MAX_INDEX_STATE_FILE_BYTES) {
826
+ return {
827
+ kind: "invalid",
828
+ reason: "oversized",
829
+ message: `File exceeds ${String(MAX_INDEX_STATE_FILE_BYTES)} Bytes.`,
830
+ };
831
+ }
832
+ } catch (error) {
833
+ if (isNotFound(error)) return { kind: "absent" };
834
+ return { kind: "invalid", reason: "io", message: errorText(error) };
835
+ }
836
+
837
+ let handle;
838
+ try {
839
+ // `lstat` rejects an existing symlink; O_NOFOLLOW also closes the race in
840
+ // which that path is swapped between the check and open.
841
+ handle = await open(filePath, READ_ONLY_NO_FOLLOW);
842
+ const buffer = Buffer.alloc(MAX_INDEX_STATE_FILE_BYTES + 1);
843
+ let totalBytesRead = 0;
844
+ while (totalBytesRead < buffer.length) {
845
+ const { bytesRead } = await handle.read(
846
+ buffer,
847
+ totalBytesRead,
848
+ buffer.length - totalBytesRead,
849
+ totalBytesRead,
850
+ );
851
+ if (bytesRead === 0) break;
852
+ totalBytesRead += bytesRead;
853
+ }
854
+ if (totalBytesRead > MAX_INDEX_STATE_FILE_BYTES) {
855
+ return {
856
+ kind: "invalid",
857
+ reason: "oversized",
858
+ message: `File exceeds ${String(MAX_INDEX_STATE_FILE_BYTES)} Bytes.`,
859
+ };
860
+ }
861
+ return { kind: "valid", text: buffer.subarray(0, totalBytesRead).toString("utf8") };
862
+ } catch (error) {
863
+ if (isNotFound(error)) return { kind: "absent" };
864
+ return { kind: "invalid", reason: "io", message: errorText(error) };
865
+ } finally {
866
+ await handle?.close().catch(() => undefined);
867
+ }
868
+ }
869
+
870
+ /** Embedded state identities must be canonical absolute local file URLs. */
871
+ function isLocalStoreUrl(value: string): boolean {
872
+ try {
873
+ const url = new URL(value);
874
+ return (
875
+ url.protocol === "file:" &&
876
+ url.hostname === "" &&
877
+ url.search === "" &&
878
+ url.hash === "" &&
879
+ url.href === value &&
880
+ !url.pathname.includes("%00")
881
+ );
882
+ } catch {
883
+ return false;
884
+ }
885
+ }
886
+
887
+ function parseStorageIdentity(
888
+ record: Record<string, unknown>,
889
+ requireEndpoint: boolean,
890
+ ): (StorageStateIdentity & { qdrantUrl?: string }) | null {
891
+ const backend = record["storageBackend"];
892
+ const storeUrl = record["storeUrl"];
893
+ const qdrantUrl = record["qdrantUrl"];
894
+ if (backend !== undefined && backend !== "qdrant" && backend !== "lancedb") return null;
895
+ if (backend === "lancedb") {
896
+ if (!isBoundedString(storeUrl) || !isLocalStoreUrl(storeUrl)) return null;
897
+ if (qdrantUrl !== undefined) return null;
898
+ return { storageBackend: "lancedb", storeUrl };
899
+ }
900
+ if (storeUrl !== undefined && (!isBoundedString(storeUrl) || storeUrl !== qdrantUrl)) return null;
901
+ if (requireEndpoint && !isBoundedString(qdrantUrl)) return null;
902
+ if (qdrantUrl !== undefined && !isBoundedString(qdrantUrl)) return null;
903
+ return {
904
+ ...(backend === undefined ? {} : { storageBackend: backend }),
905
+ ...(storeUrl === undefined ? {} : { storeUrl }),
906
+ ...(qdrantUrl === undefined ? {} : { qdrantUrl }),
907
+ };
908
+ }
909
+
910
+ function parseCurrentPointer(text: string): CurrentPointerV1 | null {
911
+ if (Buffer.byteLength(text, "utf8") > MAX_INDEX_STATE_FILE_BYTES) return null;
912
+ let raw: unknown;
913
+ try {
914
+ raw = JSON.parse(text);
915
+ } catch {
916
+ return null;
917
+ }
918
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
919
+ const record = raw as Record<string, unknown>;
920
+ if (record["schemaVersion"] !== 1) return null;
921
+ const storage = parseStorageIdentity(record, false);
922
+ if (storage === null) return null;
923
+ const workspaceId = record["workspaceId"];
924
+ const repoId = record["repoId"];
925
+ const refId = record["refId"];
926
+ const embeddingProfileId = record["embeddingProfileId"];
927
+ const generationId = record["generationId"];
928
+ const recordKey = record["recordKey"];
929
+ if (
930
+ !isBoundedString(workspaceId) ||
931
+ !isBoundedString(repoId) ||
932
+ !isBoundedString(refId) ||
933
+ !isBoundedString(embeddingProfileId) ||
934
+ !isBoundedString(generationId) ||
935
+ typeof recordKey !== "string" ||
936
+ !/^[0-9a-f]{64}$/u.test(recordKey)
937
+ ) {
938
+ return null;
939
+ }
940
+ return {
941
+ schemaVersion: 1,
942
+ ...storage,
943
+ workspaceId,
944
+ repoId,
945
+ refId,
946
+ embeddingProfileId,
947
+ generationId,
948
+ recordKey,
949
+ };
950
+ }
951
+
952
+ function currentPointerFor(state: IndexStateV1): CurrentPointerV1 {
953
+ if (state.generationId === undefined) {
954
+ throw new Error("A current pointer requires a generation id.");
955
+ }
956
+ return {
957
+ schemaVersion: 1,
958
+ workspaceId: state.workspaceId,
959
+ repoId: state.repoId,
960
+ refId: state.refId,
961
+ embeddingProfileId: state.embeddingProfileId,
962
+ generationId: state.generationId,
963
+ recordKey: indexStateRecordKey(state),
964
+ ...(state.storageBackend === "lancedb"
965
+ ? ({ storageBackend: "lancedb", storeUrl: state.storeUrl } as const)
966
+ : {}),
967
+ };
968
+ }
969
+
970
+ function pointerMatchesState(pointer: CurrentPointerV1, state: IndexStateV1): boolean {
971
+ return (
972
+ pointer.workspaceId === state.workspaceId &&
973
+ pointer.repoId === state.repoId &&
974
+ pointer.refId === state.refId &&
975
+ pointer.embeddingProfileId === state.embeddingProfileId &&
976
+ pointer.generationId === state.generationId &&
977
+ pointer.recordKey === indexStateRecordKey(state) &&
978
+ indexStateCurrentKey(pointer) === indexStateCurrentKey(state)
979
+ );
980
+ }
981
+
982
+ type ExactRecordRead =
983
+ | { readonly kind: "absent" }
984
+ | { readonly kind: "valid"; readonly state: IndexStateV1 }
985
+ | { readonly kind: "invalid"; readonly message: string };
986
+
987
+ async function readExactRegistryRecord(
988
+ target: string,
989
+ identity: IndexStateRecordIdentity,
990
+ ): Promise<ExactRecordRead> {
991
+ const read = await readBoundedRegularFile(target);
992
+ if (read.kind === "absent") return read;
993
+ if (read.kind === "invalid") return { kind: "invalid", message: read.message };
994
+ const state = parseIndexState(read.text);
995
+ if (
996
+ state === null ||
997
+ state.generationId === undefined ||
998
+ indexStateRecordKey(state) !== indexStateRecordKey(identity)
999
+ ) {
1000
+ return { kind: "invalid", message: "Registry record does not match its key." };
1001
+ }
1002
+ return { kind: "valid", state };
1003
+ }
1004
+
1005
+ async function atomicWrite(
1006
+ target: string,
1007
+ text: string,
1008
+ assertOwnership?: () => Promise<void>,
1009
+ ): Promise<void> {
1010
+ await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
1011
+ const temporary = `${target}.${randomUUID()}.tmp`;
1012
+ try {
1013
+ await writeFile(temporary, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
1014
+ await assertOwnership?.();
1015
+ await rename(temporary, target);
1016
+ } catch (error) {
1017
+ await rm(temporary, { force: true }).catch(() => undefined);
1018
+ throw error;
1019
+ }
1020
+ }
1021
+
1022
+ function serializeRegistryState(state: IndexStateV1): string | null {
1023
+ if (state.generationId === undefined) return null;
1024
+ const text = `${JSON.stringify(state, null, 2)}\n`;
1025
+ const parsed = parseIndexState(text);
1026
+ if (parsed === null || parsed.generationId === undefined) return null;
1027
+ return text;
1028
+ }
1029
+
1030
+ type PointerInvalidation = "unconditional" | "matching";
1031
+
1032
+ async function invalidateAfterFailure(
1033
+ stateRoot: string,
1034
+ state: IndexStateV1,
1035
+ pointerInvalidation: PointerInvalidation,
1036
+ options: IndexStatePublicationOptions,
1037
+ ): Promise<{ ok: boolean; error?: unknown }> {
1038
+ if (options.assertOwnership !== undefined) {
1039
+ try {
1040
+ // Never let a writer that already lost its lease remove a successor's
1041
+ // record or pointer. The surviving running marker remains fail-closed.
1042
+ await options.assertOwnership();
1043
+ } catch (error) {
1044
+ return { ok: false, error };
1045
+ }
1046
+ }
1047
+ return invalidatePublication(stateRoot, state, pointerInvalidation);
1048
+ }
1049
+
1050
+ async function invalidatePublication(
1051
+ stateRoot: string,
1052
+ state: IndexStateV1,
1053
+ pointerInvalidation: PointerInvalidation,
1054
+ ): Promise<{ ok: boolean; error?: unknown }> {
1055
+ const errors: unknown[] = [];
1056
+ await removeAndCollect(indexStateRecordPath(stateRoot, state), errors);
1057
+ await invalidateCurrentPointer(stateRoot, state, pointerInvalidation, errors);
1058
+ await invalidateLegacyFallback(stateRoot, state, pointerInvalidation, errors);
1059
+ return errors.length === 0 ? { ok: true } : { ok: false, error: errors[0] };
1060
+ }
1061
+
1062
+ async function invalidateCurrentPointer(
1063
+ stateRoot: string,
1064
+ state: IndexStateV1,
1065
+ pointerInvalidation: PointerInvalidation,
1066
+ errors: unknown[],
1067
+ ): Promise<void> {
1068
+ const pointerPath = indexStateCurrentPath(stateRoot, state);
1069
+ if (pointerInvalidation === "unconditional") {
1070
+ await removeAndCollect(pointerPath, errors);
1071
+ return;
1072
+ }
1073
+ const pointerRead = await readBoundedRegularFile(pointerPath);
1074
+ if (pointerRead.kind === "invalid") {
1075
+ await removeAndCollect(pointerPath, errors);
1076
+ return;
1077
+ }
1078
+ if (pointerRead.kind !== "valid") return;
1079
+ const pointer = parseCurrentPointer(pointerRead.text);
1080
+ if (pointer === null || pointerIdentifiesState(pointer, state)) {
1081
+ await removeAndCollect(pointerPath, errors);
1082
+ }
1083
+ }
1084
+
1085
+ function pointerIdentifiesState(pointer: CurrentPointerV1, state: IndexStateV1): boolean {
1086
+ return (
1087
+ pointer.recordKey === indexStateRecordKey(state) && pointer.generationId === state.generationId
1088
+ );
1089
+ }
1090
+
1091
+ async function invalidateLegacyFallback(
1092
+ stateRoot: string,
1093
+ state: IndexStateV1,
1094
+ pointerInvalidation: PointerInvalidation,
1095
+ errors: unknown[],
1096
+ ): Promise<void> {
1097
+ const legacyPath = indexStatePath(stateRoot);
1098
+ const legacyRead = await readBoundedRegularFile(legacyPath);
1099
+ if (legacyRead.kind !== "valid") return;
1100
+ const legacy = parseIndexState(legacyRead.text);
1101
+ if (legacy !== null && legacyMustBeInvalidated(legacy, state, pointerInvalidation)) {
1102
+ await removeAndCollect(legacyPath, errors);
1103
+ }
1104
+ }
1105
+
1106
+ function legacyMustBeInvalidated(
1107
+ legacy: IndexStateV1,
1108
+ state: IndexStateV1,
1109
+ pointerInvalidation: PointerInvalidation,
1110
+ ): boolean {
1111
+ const sameGroup = indexStateCurrentKey(legacy) === indexStateCurrentKey(state);
1112
+ return (
1113
+ sameGroup &&
1114
+ (pointerInvalidation === "unconditional" ||
1115
+ legacy.embeddingProfileId === state.embeddingProfileId)
1116
+ );
1117
+ }
1118
+
1119
+ async function removeAndCollect(target: string, errors: unknown[]): Promise<void> {
1120
+ try {
1121
+ await rm(target, { force: true });
1122
+ } catch (error) {
1123
+ errors.push(error);
1124
+ }
1125
+ }
1126
+
1127
+ function failedWrite(
1128
+ target: string,
1129
+ error: unknown,
1130
+ invalidated: { ok: boolean; error?: unknown },
1131
+ ): Extract<IndexStateWriteResult, { ok: false }> {
1132
+ return {
1133
+ ok: false,
1134
+ error,
1135
+ path: target,
1136
+ previousStateInvalidated: invalidated.ok,
1137
+ ...(invalidated.error === undefined ? {} : { invalidationError: invalidated.error }),
1138
+ };
1139
+ }
1140
+
1141
+ function invalidFileRead(
1142
+ filePath: string,
1143
+ read: Extract<BoundedFileRead, { kind: "invalid" }>,
1144
+ ): InvalidRegistryRead {
1145
+ return {
1146
+ kind: "invalid",
1147
+ diagnostics: [
1148
+ diagnostic(
1149
+ read.reason === "oversized"
1150
+ ? "INDEX_STATE_OVERSIZED"
1151
+ : read.reason === "io"
1152
+ ? "INDEX_STATE_IO"
1153
+ : "INDEX_STATE_CORRUPT",
1154
+ filePath,
1155
+ read.message,
1156
+ ),
1157
+ ],
1158
+ };
1159
+ }
1160
+
1161
+ function invalidRegistry(filePath: string, message: string): InvalidRegistryRead {
1162
+ return {
1163
+ kind: "invalid",
1164
+ diagnostics: [diagnostic("INDEX_STATE_CORRUPT", filePath, message)],
1165
+ };
1166
+ }
1167
+
1168
+ function diagnostic(
1169
+ code: IndexStateDiagnostic["code"],
1170
+ filePath: string,
1171
+ message: string,
1172
+ ): IndexStateDiagnostic {
1173
+ return { code, path: filePath, message };
1174
+ }
1175
+
1176
+ function isBoundedString(value: unknown): value is string {
1177
+ return (
1178
+ typeof value === "string" && value.length > 0 && value.length <= MAX_INDEX_STATE_FIELD_LENGTH
1179
+ );
1180
+ }
1181
+
1182
+ function hashCanonical(parts: readonly string[]): string {
1183
+ return createHash("sha256").update(JSON.stringify(parts), "utf8").digest("hex");
1184
+ }
1185
+
1186
+ function compareFoundStates(left: FoundIndexState, right: FoundIndexState): number {
1187
+ return compareText(left.repoRoot, right.repoRoot) || compareStateEntries(left, right);
1188
+ }
1189
+
1190
+ function compareStateEntries(
1191
+ left: Pick<LocalRegistryEntry, "statePath" | "state">,
1192
+ right: Pick<LocalRegistryEntry, "statePath" | "state">,
1193
+ ): number {
1194
+ return (
1195
+ compareText(left.state.repoId, right.state.repoId) ||
1196
+ compareText(left.state.refId, right.state.refId) ||
1197
+ compareText(left.state.embeddingProfileId, right.state.embeddingProfileId) ||
1198
+ compareText(left.statePath, right.statePath)
1199
+ );
1200
+ }
1201
+
1202
+ function compareText(left: string, right: string): number {
1203
+ return left < right ? -1 : left > right ? 1 : 0;
1204
+ }
1205
+
1206
+ function isNotFound(error: unknown): boolean {
1207
+ return (
1208
+ typeof error === "object" &&
1209
+ error !== null &&
1210
+ "code" in error &&
1211
+ (error as { code?: unknown }).code === "ENOENT"
1212
+ );
1213
+ }
1214
+
1215
+ function errorText(error: unknown): string {
1216
+ return error instanceof Error ? error.message : String(error);
1217
+ }