@cavelang/cli 0.34.0 → 0.36.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 (344) hide show
  1. package/README.md +225 -13
  2. package/dist/internal/act/action.js +2 -1
  3. package/dist/internal/act/action.js.map +1 -1
  4. package/dist/internal/act/declare.d.ts.map +1 -1
  5. package/dist/internal/act/declare.js +82 -53
  6. package/dist/internal/act/declare.js.map +1 -1
  7. package/dist/internal/act/engine.d.ts +4 -3
  8. package/dist/internal/act/engine.d.ts.map +1 -1
  9. package/dist/internal/act/engine.js +121 -32
  10. package/dist/internal/act/engine.js.map +1 -1
  11. package/dist/internal/automate/cancellation.d.ts +3 -0
  12. package/dist/internal/automate/cancellation.d.ts.map +1 -0
  13. package/dist/internal/automate/cancellation.js +38 -0
  14. package/dist/internal/automate/cancellation.js.map +1 -0
  15. package/dist/internal/automate/declare.d.ts.map +1 -1
  16. package/dist/internal/automate/declare.js +49 -24
  17. package/dist/internal/automate/declare.js.map +1 -1
  18. package/dist/internal/automate/engine.d.ts +7 -3
  19. package/dist/internal/automate/engine.d.ts.map +1 -1
  20. package/dist/internal/automate/engine.js +196 -93
  21. package/dist/internal/automate/engine.js.map +1 -1
  22. package/dist/internal/automate/error-message.d.ts +3 -0
  23. package/dist/internal/automate/error-message.d.ts.map +1 -0
  24. package/dist/internal/automate/error-message.js +10 -0
  25. package/dist/internal/automate/error-message.js.map +1 -0
  26. package/dist/internal/automate/main.d.ts.map +1 -1
  27. package/dist/internal/automate/main.js +240 -35
  28. package/dist/internal/automate/main.js.map +1 -1
  29. package/dist/internal/automate/options.d.ts +4 -0
  30. package/dist/internal/automate/options.d.ts.map +1 -0
  31. package/dist/internal/automate/options.js +19 -0
  32. package/dist/internal/automate/options.js.map +1 -0
  33. package/dist/internal/connect/declared.d.ts +204 -0
  34. package/dist/internal/connect/declared.d.ts.map +1 -0
  35. package/dist/internal/connect/declared.js +610 -0
  36. package/dist/internal/connect/declared.js.map +1 -0
  37. package/dist/internal/connect/error-message.d.ts +3 -0
  38. package/dist/internal/connect/error-message.d.ts.map +1 -0
  39. package/dist/internal/connect/error-message.js +10 -0
  40. package/dist/internal/connect/error-message.js.map +1 -0
  41. package/dist/internal/connect/federation.d.ts +9 -0
  42. package/dist/internal/connect/federation.d.ts.map +1 -0
  43. package/dist/internal/connect/federation.js +19 -0
  44. package/dist/internal/connect/federation.js.map +1 -0
  45. package/dist/internal/connect/fetch-failure.d.ts +3 -0
  46. package/dist/internal/connect/fetch-failure.d.ts.map +1 -0
  47. package/dist/internal/connect/fetch-failure.js +36 -0
  48. package/dist/internal/connect/fetch-failure.js.map +1 -0
  49. package/dist/internal/connect/index.d.ts +4 -2
  50. package/dist/internal/connect/index.d.ts.map +1 -1
  51. package/dist/internal/connect/index.js +3 -1
  52. package/dist/internal/connect/index.js.map +1 -1
  53. package/dist/internal/connect/main.d.ts +1 -0
  54. package/dist/internal/connect/main.d.ts.map +1 -1
  55. package/dist/internal/connect/main.js +575 -59
  56. package/dist/internal/connect/main.js.map +1 -1
  57. package/dist/internal/connect/options.d.ts +3 -0
  58. package/dist/internal/connect/options.d.ts.map +1 -0
  59. package/dist/internal/connect/options.js +9 -0
  60. package/dist/internal/connect/options.js.map +1 -0
  61. package/dist/internal/connect/records.d.ts +3 -0
  62. package/dist/internal/connect/records.d.ts.map +1 -0
  63. package/dist/internal/connect/records.js +21 -0
  64. package/dist/internal/connect/records.js.map +1 -0
  65. package/dist/internal/connect/run.d.ts +48 -19
  66. package/dist/internal/connect/run.d.ts.map +1 -1
  67. package/dist/internal/connect/run.js +185 -77
  68. package/dist/internal/connect/run.js.map +1 -1
  69. package/dist/internal/connect/source.d.ts +36 -3
  70. package/dist/internal/connect/source.d.ts.map +1 -1
  71. package/dist/internal/connect/source.js +378 -39
  72. package/dist/internal/connect/source.js.map +1 -1
  73. package/dist/internal/connect/template.d.ts +20 -2
  74. package/dist/internal/connect/template.d.ts.map +1 -1
  75. package/dist/internal/connect/template.js +53 -9
  76. package/dist/internal/connect/template.js.map +1 -1
  77. package/dist/internal/connect/utf8.d.ts +3 -0
  78. package/dist/internal/connect/utf8.d.ts.map +1 -0
  79. package/dist/internal/connect/utf8.js +12 -0
  80. package/dist/internal/connect/utf8.js.map +1 -0
  81. package/dist/internal/eval/cleanup.d.ts +5 -0
  82. package/dist/internal/eval/cleanup.d.ts.map +1 -0
  83. package/dist/internal/eval/cleanup.js +50 -0
  84. package/dist/internal/eval/cleanup.js.map +1 -0
  85. package/dist/internal/eval/error-message.d.ts +3 -0
  86. package/dist/internal/eval/error-message.d.ts.map +1 -0
  87. package/dist/internal/eval/error-message.js +10 -0
  88. package/dist/internal/eval/error-message.js.map +1 -0
  89. package/dist/internal/eval/judge.d.ts.map +1 -1
  90. package/dist/internal/eval/judge.js +3 -31
  91. package/dist/internal/eval/judge.js.map +1 -1
  92. package/dist/internal/eval/loop.d.ts.map +1 -1
  93. package/dist/internal/eval/loop.js +18 -4
  94. package/dist/internal/eval/loop.js.map +1 -1
  95. package/dist/internal/eval/main.d.ts.map +1 -1
  96. package/dist/internal/eval/main.js +22 -10
  97. package/dist/internal/eval/main.js.map +1 -1
  98. package/dist/internal/eval/options.d.ts +6 -0
  99. package/dist/internal/eval/options.d.ts.map +1 -0
  100. package/dist/internal/eval/options.js +31 -0
  101. package/dist/internal/eval/options.js.map +1 -0
  102. package/dist/internal/eval/queries.d.ts.map +1 -1
  103. package/dist/internal/eval/queries.js +9 -6
  104. package/dist/internal/eval/queries.js.map +1 -1
  105. package/dist/internal/eval/run.d.ts.map +1 -1
  106. package/dist/internal/eval/run.js +112 -24
  107. package/dist/internal/eval/run.js.map +1 -1
  108. package/dist/internal/eval/score.d.ts.map +1 -1
  109. package/dist/internal/eval/score.js +9 -2
  110. package/dist/internal/eval/score.js.map +1 -1
  111. package/dist/internal/eval/suite.d.ts.map +1 -1
  112. package/dist/internal/eval/suite.js +14 -2
  113. package/dist/internal/eval/suite.js.map +1 -1
  114. package/dist/internal/ingest/cleanup.d.ts +5 -0
  115. package/dist/internal/ingest/cleanup.d.ts.map +1 -0
  116. package/dist/internal/ingest/cleanup.js +50 -0
  117. package/dist/internal/ingest/cleanup.js.map +1 -0
  118. package/dist/internal/ingest/content.d.ts +6 -0
  119. package/dist/internal/ingest/content.d.ts.map +1 -0
  120. package/dist/internal/ingest/content.js +25 -0
  121. package/dist/internal/ingest/content.js.map +1 -0
  122. package/dist/internal/ingest/context.d.ts.map +1 -1
  123. package/dist/internal/ingest/context.js +25 -7
  124. package/dist/internal/ingest/context.js.map +1 -1
  125. package/dist/internal/ingest/error-message.d.ts +3 -0
  126. package/dist/internal/ingest/error-message.d.ts.map +1 -0
  127. package/dist/internal/ingest/error-message.js +10 -0
  128. package/dist/internal/ingest/error-message.js.map +1 -0
  129. package/dist/internal/ingest/files.d.ts +4 -12
  130. package/dist/internal/ingest/files.d.ts.map +1 -1
  131. package/dist/internal/ingest/files.js +22 -10
  132. package/dist/internal/ingest/files.js.map +1 -1
  133. package/dist/internal/ingest/main.d.ts.map +1 -1
  134. package/dist/internal/ingest/main.js +48 -17
  135. package/dist/internal/ingest/main.js.map +1 -1
  136. package/dist/internal/ingest/options.d.ts +6 -0
  137. package/dist/internal/ingest/options.d.ts.map +1 -0
  138. package/dist/internal/ingest/options.js +27 -0
  139. package/dist/internal/ingest/options.js.map +1 -0
  140. package/dist/internal/ingest/prompt.d.ts.map +1 -1
  141. package/dist/internal/ingest/prompt.js +14 -10
  142. package/dist/internal/ingest/prompt.js.map +1 -1
  143. package/dist/internal/ingest/run.d.ts +9 -2
  144. package/dist/internal/ingest/run.d.ts.map +1 -1
  145. package/dist/internal/ingest/run.js +206 -62
  146. package/dist/internal/ingest/run.js.map +1 -1
  147. package/dist/internal/ingest/timeout.d.ts +3 -0
  148. package/dist/internal/ingest/timeout.d.ts.map +1 -0
  149. package/dist/internal/ingest/timeout.js +14 -0
  150. package/dist/internal/ingest/timeout.js.map +1 -0
  151. package/dist/internal/ingest/web.d.ts +2 -11
  152. package/dist/internal/ingest/web.d.ts.map +1 -1
  153. package/dist/internal/ingest/web.js +131 -25
  154. package/dist/internal/ingest/web.js.map +1 -1
  155. package/dist/internal/loop/budgets.d.ts +3 -0
  156. package/dist/internal/loop/budgets.d.ts.map +1 -0
  157. package/dist/internal/loop/budgets.js +10 -0
  158. package/dist/internal/loop/budgets.js.map +1 -0
  159. package/dist/internal/loop/index.d.ts +2 -0
  160. package/dist/internal/loop/index.d.ts.map +1 -1
  161. package/dist/internal/loop/index.js +2 -0
  162. package/dist/internal/loop/index.js.map +1 -1
  163. package/dist/internal/loop/json-answer.d.ts +7 -0
  164. package/dist/internal/loop/json-answer.d.ts.map +1 -0
  165. package/dist/internal/loop/json-answer.js +79 -0
  166. package/dist/internal/loop/json-answer.js.map +1 -0
  167. package/dist/internal/loop/json-spans.d.ts +3 -0
  168. package/dist/internal/loop/json-spans.d.ts.map +1 -0
  169. package/dist/internal/loop/json-spans.js +88 -0
  170. package/dist/internal/loop/json-spans.js.map +1 -0
  171. package/dist/internal/loop/llm.d.ts +3 -2
  172. package/dist/internal/loop/llm.d.ts.map +1 -1
  173. package/dist/internal/loop/llm.js +115 -32
  174. package/dist/internal/loop/llm.js.map +1 -1
  175. package/dist/internal/loop/process.d.ts +3 -1
  176. package/dist/internal/loop/process.d.ts.map +1 -1
  177. package/dist/internal/loop/process.js +159 -59
  178. package/dist/internal/loop/process.js.map +1 -1
  179. package/dist/internal/loop/read-snapshot.d.ts +4 -0
  180. package/dist/internal/loop/read-snapshot.d.ts.map +1 -0
  181. package/dist/internal/loop/read-snapshot.js +29 -0
  182. package/dist/internal/loop/read-snapshot.js.map +1 -0
  183. package/dist/internal/loop/reconstruct.d.ts +6 -5
  184. package/dist/internal/loop/reconstruct.d.ts.map +1 -1
  185. package/dist/internal/loop/reconstruct.js +19 -8
  186. package/dist/internal/loop/reconstruct.js.map +1 -1
  187. package/dist/internal/loop/score-settings.d.ts +5 -0
  188. package/dist/internal/loop/score-settings.d.ts.map +1 -0
  189. package/dist/internal/loop/score-settings.js +14 -0
  190. package/dist/internal/loop/score-settings.js.map +1 -0
  191. package/dist/internal/loop/sqlite.d.ts.map +1 -1
  192. package/dist/internal/loop/sqlite.js +15 -7
  193. package/dist/internal/loop/sqlite.js.map +1 -1
  194. package/dist/internal/loop/store.d.ts.map +1 -1
  195. package/dist/internal/loop/store.js +11 -7
  196. package/dist/internal/loop/store.js.map +1 -1
  197. package/dist/internal/loop/timeout.d.ts +3 -0
  198. package/dist/internal/loop/timeout.d.ts.map +1 -0
  199. package/dist/internal/loop/timeout.js +13 -0
  200. package/dist/internal/loop/timeout.js.map +1 -0
  201. package/dist/internal/mcp/error-message.d.ts +3 -0
  202. package/dist/internal/mcp/error-message.d.ts.map +1 -0
  203. package/dist/internal/mcp/error-message.js +10 -0
  204. package/dist/internal/mcp/error-message.js.map +1 -0
  205. package/dist/internal/mcp/main.d.ts.map +1 -1
  206. package/dist/internal/mcp/main.js +44 -27
  207. package/dist/internal/mcp/main.js.map +1 -1
  208. package/dist/internal/mcp/read-snapshot.d.ts +4 -0
  209. package/dist/internal/mcp/read-snapshot.d.ts.map +1 -0
  210. package/dist/internal/mcp/read-snapshot.js +22 -0
  211. package/dist/internal/mcp/read-snapshot.js.map +1 -0
  212. package/dist/internal/mcp/server.d.ts.map +1 -1
  213. package/dist/internal/mcp/server.js +181 -21
  214. package/dist/internal/mcp/server.js.map +1 -1
  215. package/dist/internal/mcp/source.d.ts +3 -0
  216. package/dist/internal/mcp/source.d.ts.map +1 -0
  217. package/dist/internal/mcp/source.js +13 -0
  218. package/dist/internal/mcp/source.js.map +1 -0
  219. package/dist/internal/mcp/tools.d.ts.map +1 -1
  220. package/dist/internal/mcp/tools.js +135 -72
  221. package/dist/internal/mcp/tools.js.map +1 -1
  222. package/dist/internal/rules/declarations.d.ts +6 -0
  223. package/dist/internal/rules/declarations.d.ts.map +1 -0
  224. package/dist/internal/rules/declarations.js +14 -0
  225. package/dist/internal/rules/declarations.js.map +1 -0
  226. package/dist/internal/rules/declare.d.ts.map +1 -1
  227. package/dist/internal/rules/declare.js +30 -16
  228. package/dist/internal/rules/declare.js.map +1 -1
  229. package/dist/internal/rules/engine.d.ts +8 -7
  230. package/dist/internal/rules/engine.d.ts.map +1 -1
  231. package/dist/internal/rules/engine.js +150 -44
  232. package/dist/internal/rules/engine.js.map +1 -1
  233. package/dist/internal/rules/rule.d.ts.map +1 -1
  234. package/dist/internal/rules/rule.js +1 -22
  235. package/dist/internal/rules/rule.js.map +1 -1
  236. package/dist/internal/shape/alias-root.d.ts +3 -0
  237. package/dist/internal/shape/alias-root.d.ts.map +1 -0
  238. package/dist/internal/shape/alias-root.js +13 -0
  239. package/dist/internal/shape/alias-root.js.map +1 -0
  240. package/dist/internal/shape/check.d.ts +15 -3
  241. package/dist/internal/shape/check.d.ts.map +1 -1
  242. package/dist/internal/shape/check.js +230 -100
  243. package/dist/internal/shape/check.js.map +1 -1
  244. package/dist/internal/shape/client.d.ts.map +1 -1
  245. package/dist/internal/shape/client.js +42 -20
  246. package/dist/internal/shape/client.js.map +1 -1
  247. package/dist/internal/shape/constraints.d.ts +8 -0
  248. package/dist/internal/shape/constraints.d.ts.map +1 -0
  249. package/dist/internal/shape/constraints.js +17 -0
  250. package/dist/internal/shape/constraints.js.map +1 -0
  251. package/dist/internal/shape/edit-distance.d.ts +3 -0
  252. package/dist/internal/shape/edit-distance.d.ts.map +1 -0
  253. package/dist/internal/shape/edit-distance.js +33 -0
  254. package/dist/internal/shape/edit-distance.js.map +1 -0
  255. package/dist/internal/shape/gate.d.ts.map +1 -1
  256. package/dist/internal/shape/gate.js +0 -0
  257. package/dist/internal/shape/gate.js.map +1 -1
  258. package/dist/internal/shape/snapshot.d.ts +4 -0
  259. package/dist/internal/shape/snapshot.d.ts.map +1 -0
  260. package/dist/internal/shape/snapshot.js +20 -0
  261. package/dist/internal/shape/snapshot.js.map +1 -0
  262. package/dist/internal/shape/suggest.d.ts +8 -6
  263. package/dist/internal/shape/suggest.d.ts.map +1 -1
  264. package/dist/internal/shape/suggest.js +208 -92
  265. package/dist/internal/shape/suggest.js.map +1 -1
  266. package/dist/internal/sync/error-message.d.ts +3 -0
  267. package/dist/internal/sync/error-message.d.ts.map +1 -0
  268. package/dist/internal/sync/error-message.js +10 -0
  269. package/dist/internal/sync/error-message.js.map +1 -0
  270. package/dist/internal/sync/index.d.ts +3 -2
  271. package/dist/internal/sync/index.d.ts.map +1 -1
  272. package/dist/internal/sync/index.js +3 -2
  273. package/dist/internal/sync/index.js.map +1 -1
  274. package/dist/internal/sync/sync.d.ts +8 -5
  275. package/dist/internal/sync/sync.d.ts.map +1 -1
  276. package/dist/internal/sync/sync.js +322 -65
  277. package/dist/internal/sync/sync.js.map +1 -1
  278. package/dist/internal/view/api.d.ts +3 -1
  279. package/dist/internal/view/api.d.ts.map +1 -1
  280. package/dist/internal/view/api.js +154 -90
  281. package/dist/internal/view/api.js.map +1 -1
  282. package/dist/internal/view/error-message.d.ts +3 -0
  283. package/dist/internal/view/error-message.d.ts.map +1 -0
  284. package/dist/internal/view/error-message.js +10 -0
  285. package/dist/internal/view/error-message.js.map +1 -0
  286. package/dist/internal/view/main.d.ts.map +1 -1
  287. package/dist/internal/view/main.js +63 -9
  288. package/dist/internal/view/main.js.map +1 -1
  289. package/dist/internal/view/page.d.ts.map +1 -1
  290. package/dist/internal/view/page.js +119 -20
  291. package/dist/internal/view/page.js.map +1 -1
  292. package/dist/internal/view/report.d.ts.map +1 -1
  293. package/dist/internal/view/report.js +180 -145
  294. package/dist/internal/view/report.js.map +1 -1
  295. package/dist/internal/view/scope.d.ts +6 -2
  296. package/dist/internal/view/scope.d.ts.map +1 -1
  297. package/dist/internal/view/scope.js +107 -24
  298. package/dist/internal/view/scope.js.map +1 -1
  299. package/dist/internal/view/sensitivity.d.ts +4 -0
  300. package/dist/internal/view/sensitivity.d.ts.map +1 -0
  301. package/dist/internal/view/sensitivity.js +11 -0
  302. package/dist/internal/view/sensitivity.js.map +1 -0
  303. package/dist/internal/view/server.d.ts.map +1 -1
  304. package/dist/internal/view/server.js +57 -20
  305. package/dist/internal/view/server.js.map +1 -1
  306. package/dist/src/atomic-output.d.ts +3 -0
  307. package/dist/src/atomic-output.d.ts.map +1 -0
  308. package/dist/src/atomic-output.js +67 -0
  309. package/dist/src/atomic-output.js.map +1 -0
  310. package/dist/src/cli.d.ts +17 -2
  311. package/dist/src/cli.d.ts.map +1 -1
  312. package/dist/src/cli.js +333 -195
  313. package/dist/src/cli.js.map +1 -1
  314. package/dist/src/commands.d.ts +2 -2
  315. package/dist/src/commands.js +2 -2
  316. package/dist/src/commands.js.map +1 -1
  317. package/dist/src/dispatch.d.ts.map +1 -1
  318. package/dist/src/dispatch.js +10 -3
  319. package/dist/src/dispatch.js.map +1 -1
  320. package/dist/src/doctor.d.ts +1 -1
  321. package/dist/src/doctor.d.ts.map +1 -1
  322. package/dist/src/doctor.js +192 -91
  323. package/dist/src/doctor.js.map +1 -1
  324. package/dist/src/error-message.d.ts +3 -0
  325. package/dist/src/error-message.d.ts.map +1 -0
  326. package/dist/src/error-message.js +20 -0
  327. package/dist/src/error-message.js.map +1 -0
  328. package/dist/src/hooks-config.d.ts +3 -0
  329. package/dist/src/hooks-config.d.ts.map +1 -0
  330. package/dist/src/hooks-config.js +23 -0
  331. package/dist/src/hooks-config.js.map +1 -0
  332. package/dist/src/index.d.ts +1 -1
  333. package/dist/src/index.d.ts.map +1 -1
  334. package/dist/src/index.js +1 -1
  335. package/dist/src/index.js.map +1 -1
  336. package/package.json +23 -20
  337. package/src/atomic-output.ts +47 -0
  338. package/src/cli.ts +345 -171
  339. package/src/commands.ts +2 -2
  340. package/src/dispatch.ts +10 -3
  341. package/src/doctor.ts +207 -100
  342. package/src/error-message.ts +13 -0
  343. package/src/hooks-config.ts +22 -0
  344. package/src/index.ts +1 -1
@@ -2,39 +2,65 @@
2
2
  * `cave connect` entry — argument parsing, pass orchestration (single,
3
3
  * `--watch`, `--query`), and report rendering around `run.ts`.
4
4
  */
5
- import { readFileSync, watch as watchFs } from 'node:fs';
5
+ import { rethrowFetchFailure } from "./fetch-failure.js";
6
+ import { errorMessage } from "./error-message.js";
7
+ import { existsSync, watch as watchFs } from 'node:fs';
8
+ import { readText } from "./utf8.js";
6
9
  import { basename, dirname, resolve } from 'node:path';
7
10
  import { parseArgs } from 'node:util';
8
11
  import { Registry } from '@cavelang/canonical';
9
- import { defaultDbPath, kindOf, open, openAt } from '@cavelang/store';
10
- import { Record as QueryRecord } from '@cavelang/query';
12
+ import { LocateError, defaultDbPath, kindOf, open, openAt } from '@cavelang/store';
13
+ import { Record as QueryRecord, query as caveQuery } from '@cavelang/query';
14
+ import * as Declared from "./declared.js";
11
15
  import * as Source from "./source.js";
12
16
  import * as Template from "./template.js";
13
- import { connect, federatedQuery } from "./run.js";
17
+ import { connect } from "./run.js";
18
+ import { withFederatedSource } from "./federation.js";
14
19
  const usage = `cave connect — deterministic structured ingestion through a mapping template (spec §23)
15
20
 
16
21
  Usage:
17
22
  cave connect [--db <path>] <source> --map <file> [options]
23
+ cave connect [--db <path>] [--name <n>] [--force] [--prune] [--dry-run] [--watch]
24
+ cave connect [--db <path>] --list
25
+ cave connect [--db <path>] --query '<pattern>'
18
26
 
19
27
  The source is a .csv/.tsv/.json/.jsonl/.ndjson file, a SQLite database
20
- (with --table or --sql), or an http(s) URL serving JSON or CSV. The
28
+ (with --table or --sql), or an http(s) URL serving JSON, JSONL, CSV or TSV. The
21
29
  mapping is an ordinary CAVE document whose ?field variables stand for
22
30
  record fields; variable-free blocks append once per run, variable blocks
23
31
  instantiate once per record — no LLM in the loop, same input, same claims.
32
+ A short mapping can be written inline, and --sql reshapes any tabular
33
+ source through SQLite before the mapping sees it.
24
34
  Mapped claims retain the physical source; CSV/TSV and JSONL records also carry
25
35
  their exact one-based inclusive line span (spec §9.8).
26
36
 
37
+ Without a source, the store's declared sources run (spec §23.4): every
38
+ source/<name> entity whose current claims name a path, with the other
39
+ options as attributes — HAS map:, key:, format:, delimiter:, table:, sql:,
40
+ records:. A .cave path needs no map (the file is its own template). Paths
41
+ resolve against the store's directory. Declared sources stamp
42
+ @src:<name>/<key> and keep digests under source/<name>/<key>, so the
43
+ source/<name> entity also carries the §26.3 policy for what it yields. A
44
+ CAVE text file used as --db follows its declared sources on every open.
45
+
27
46
  Options:
28
47
  --db <path> knowledge database (default: $CAVE_DB, or cave.db)
29
- --map <file> mapping template (required)
48
+ --map <file|inline> mapping template (required): a file, or the template
49
+ itself — a comma-separated list of claim lines,
50
+ '?name IS person, ?name WORKS-AT ?company'
30
51
  --name <name> source name for record identity (default: file basename)
31
52
  --key <field> record key field — keyed records diff against their
32
53
  previous claims on change; unkeyed records are
33
54
  content-addressed
34
55
  --format <fmt> csv | tsv | json | jsonl | sqlite (default: by extension)
35
56
  --delimiter <char> CSV field delimiter (default , — tab for .tsv)
36
- --table <name> SQLite table to read (SELECT *)
37
- --sql <query> SQLite query (alternative to --table)
57
+ --table <name> SQLite table to read (SELECT *); for other formats
58
+ the temporary table --sql runs against (records)
59
+ --sql <query> SQLite query (alternative to --table); for csv, tsv,
60
+ json and jsonl the records load into a temporary
61
+ in-memory table, values as parsed (CSV cells are
62
+ text: cast for arithmetic), and the query's rows are
63
+ the records
38
64
  --records <path> dot path to the record array inside a JSON document
39
65
  --force re-map records whose digest is unchanged
40
66
  --prune retract claims of records that disappeared from the source
@@ -42,19 +68,40 @@ Options:
42
68
  --watch keep running; re-map when the source or mapping changes
43
69
  --query <pattern> federation-lite (spec §23.3): map, query the union,
44
70
  roll back — nothing persists; uses an in-memory store
45
- when the database file does not exist
71
+ when the database file does not exist; without a
72
+ source, overlays every declared source
73
+ --list print the declared sources as cave connect invocations
74
+ --name <name> without a source: run only this declared source
46
75
  --json with --query: emit matches as JSON
47
76
  --all with --query: match all beliefs, not just current ones
48
77
  --aliases with --query: resolve entities through ALIAS claims
49
78
  --no-prelude open the store without the standard §5.5 registry
50
79
 
51
80
  Examples:
81
+ cave connect --db k.db --list
82
+ cave connect --db k.db
83
+ cave connect --db k.db --watch
84
+ cave query --db notes.cave '?who WORKS-AT acme' # a text store follows its sources
52
85
  cave connect people.csv --map people.map.cave --db k.db --key id
53
86
  cave connect crm.sqlite --table contacts --map contacts.map.cave --key email
54
87
  cave connect https://api.example.com/deps.json --records data.items --map deps.map.cave
55
88
  cave connect events.jsonl --map events.map.cave --watch
56
89
  cave connect people.csv --map people.map.cave --query '?who WORKS-AT acme'`;
57
- const waitForAbort = (signal) => signal?.aborted === true ? Promise.resolve() : new Promise(resolve => signal?.addEventListener('abort', () => resolve(), { once: true }));
90
+ /** Abort and fatal callbacks share one owned completion boundary. */
91
+ const watchEnd = (signal) => {
92
+ let failure;
93
+ let wake;
94
+ const done = new Promise(resolve => { wake = resolve; });
95
+ signal?.addEventListener('abort', wake, { once: true });
96
+ if (signal?.aborted)
97
+ wake();
98
+ return {
99
+ fail(error) { failure ??= { error }; wake(); },
100
+ async wait() { await done; if (failure !== undefined)
101
+ throw failure.error; },
102
+ close() { signal?.removeEventListener('abort', wake); }
103
+ };
104
+ };
58
105
  const renderReport = (report) => {
59
106
  const lines = [
60
107
  `connect: ${report.records} record(s): ${report.mapped} mapped, ${report.skipped} skipped (unchanged)` +
@@ -69,25 +116,35 @@ const renderReport = (report) => {
69
116
  }
70
117
  for (const failure of report.failures) {
71
118
  lines.push(` ${failure.record}: FAILED`);
72
- lines.push(...failure.problems.map(problem => ` ${problem}`));
119
+ for (const problem of failure.problems)
120
+ lines.push(` ${problem}`);
73
121
  }
74
122
  return lines.join('\n');
75
123
  };
76
- const loadMapping = (path) => Template.parse(readFileSync(path, 'utf8'));
124
+ /** `--map` is a template file, or the template itself written inline (spec §23.1). */
125
+ const loadMapping = (spec) => Template.isInline(spec) && !existsSync(spec) ?
126
+ Template.parse(Template.inlineDocument(spec)) :
127
+ Template.parse(readText(spec));
77
128
  const sourceOptions = (values, context) => ({
78
129
  ...values.format === undefined ? {} : { format: values.format },
79
130
  ...values.delimiter === undefined ? {} : { delimiter: values.delimiter },
80
131
  ...values.table === undefined ? {} : { table: values.table },
81
132
  ...values.sql === undefined ? {} : { sql: values.sql },
82
133
  ...values.records === undefined ? {} : { records: values.records },
134
+ ...context.signal === undefined ? {} : { signal: context.signal },
83
135
  ...context.fetchImpl === undefined ? {} : { fetchImpl: context.fetchImpl }
84
136
  });
85
137
  const loadSource = async (source, values, context) => {
86
138
  try {
87
- return await Source.load(source, sourceOptions(values, context));
139
+ context.signal?.throwIfAborted();
140
+ const loaded = await Source.load(source, sourceOptions(values, context));
141
+ context.signal?.throwIfAborted();
142
+ return loaded;
88
143
  }
89
144
  catch (error) {
90
- throw new Error(`load ${source}: ${error instanceof Error ? error.message : String(error)}`);
145
+ if (context.signal?.aborted)
146
+ rethrowFetchFailure(context.signal, error);
147
+ throw new Error(`load ${source}: ${errorMessage(error)}`, { cause: error });
91
148
  }
92
149
  };
93
150
  const runPass = async (store, source, values, name, io, context) => {
@@ -135,6 +192,25 @@ const runDry = async (source, values, io, context) => {
135
192
  io.stdout.write(`${sections.join('\n\n')}\n`);
136
193
  return failures > 0 ? 1 : 0;
137
194
  };
195
+ /** Own the command store until its work settles, preserving cleanup failures. */
196
+ const withCommandStore = async (store, run) => {
197
+ let result;
198
+ try {
199
+ result = await run();
200
+ }
201
+ catch (error) {
202
+ try {
203
+ store.close();
204
+ }
205
+ catch (closeError) {
206
+ const messages = [error, closeError].map(errorMessage);
207
+ throw new AggregateError([error, closeError], `command failed: ${messages[0]}; store close also failed: ${messages[1]}`, { cause: error });
208
+ }
209
+ throw error;
210
+ }
211
+ store.close();
212
+ return result;
213
+ };
138
214
  const runQuery = async (source, values, name, io, context) => {
139
215
  const { mapping, problems } = loadMapping(values.map);
140
216
  if (mapping === undefined) {
@@ -145,14 +221,17 @@ const runQuery = async (source, values, name, io, context) => {
145
221
  const db = values.db ?? defaultDbPath();
146
222
  const registry = values['no-prelude'] === true ? { registry: Registry.empty } : {};
147
223
  // Federation over a store that does not exist yet queries the source alone.
148
- const store = kindOf(db) === 'missing' ? open(':memory:', registry) : openAt(db, { intent: 'scratch', ...registry });
149
- try {
150
- const { matches, report } = federatedQuery(store, mapping, loaded.records, {
224
+ const store = kindOf(db) === 'missing' ? open(':memory:', registry) : openAt(db, { intent: 'scratch', assemble: Declared.assemble, ...registry });
225
+ return await withCommandStore(store, () => {
226
+ const { result: { matches, records }, report } = withFederatedSource(store, mapping, loaded.records, {
151
227
  name,
152
228
  source,
153
229
  ...loaded.spans === undefined ? {} : { spans: loaded.spans },
154
230
  ...values.key === undefined ? {} : { key: values.key }
155
- }, values.query, { all: values.all === true, aliases: values.aliases === true });
231
+ }, () => {
232
+ const matches = caveQuery(store, values.query, { all: values.all === true, aliases: values.aliases === true });
233
+ return { matches, records: values.json === true ? matches.map(match => QueryRecord.of(store, match)) : undefined };
234
+ });
156
235
  // A failed record means the union was incomplete — matches still print
157
236
  // as partial results, but the exit code must not read as success.
158
237
  const code = report.failures.length > 0 ? 1 : 0;
@@ -160,7 +239,7 @@ const runQuery = async (source, values, name, io, context) => {
160
239
  io.stderr.write(`${renderReport(report)}\n`);
161
240
  }
162
241
  if (values.json === true) {
163
- io.stdout.write(`${JSON.stringify(matches.map(match => QueryRecord.of(store, match)), undefined, 2)}\n`);
242
+ io.stdout.write(`${JSON.stringify(records, undefined, 2)}\n`);
164
243
  return code;
165
244
  }
166
245
  if (matches.length === 0) {
@@ -175,18 +254,55 @@ const runQuery = async (source, values, name, io, context) => {
175
254
  });
176
255
  io.stdout.write(`${lines.join('\n')}\n`);
177
256
  return code;
257
+ });
258
+ };
259
+ /** Finish every owned watch resource before returning or propagating failures. */
260
+ const watchSession = async (run, stop, watchers, active) => {
261
+ const errors = [];
262
+ let result;
263
+ try {
264
+ result = await run();
265
+ }
266
+ catch (error) {
267
+ errors.push(error);
268
+ }
269
+ try {
270
+ stop();
271
+ }
272
+ catch (error) {
273
+ errors.push(error);
274
+ }
275
+ for (const watcher of watchers()) {
276
+ try {
277
+ watcher.close();
278
+ }
279
+ catch (error) {
280
+ errors.push(error);
281
+ }
282
+ }
283
+ try {
284
+ await active();
178
285
  }
179
- finally {
180
- store.close();
286
+ catch (error) {
287
+ errors.push(error);
288
+ }
289
+ if (errors.length === 1)
290
+ throw errors[0];
291
+ if (errors.length > 1) {
292
+ throw new AggregateError(errors, `watch session failed: ${errors.map(errorMessage).join('; ')}`, { cause: errors[0] });
181
293
  }
294
+ return result;
182
295
  };
183
296
  const runWatch = async (store, source, values, name, io, context) => {
297
+ let stopped = false;
298
+ const end = watchEnd(io.signal);
299
+ const fail = (error) => { stopped = true; end.fail(error); };
184
300
  const passOnce = async () => {
185
301
  try {
186
302
  await runPass(store, source, values, name, io, context);
187
303
  }
188
304
  catch (error) {
189
- io.stderr.write(`cave connect watch pass: ${error instanceof Error ? error.message : String(error)}\n`);
305
+ io.stderr.write(`cave connect watch pass: ${errorMessage(error)}\n`);
190
306
  }
191
307
  };
192
308
  let running = false;
@@ -199,63 +315,458 @@ const runWatch = async (store, source, values, name, io, context) => {
199
315
  return;
200
316
  }
201
317
  running = true;
202
- do {
203
- queued = false;
204
- await passOnce();
205
- } while (queued);
206
- running = false;
318
+ try {
319
+ do {
320
+ queued = false;
321
+ await passOnce();
322
+ } while (queued && !stopped && !io.signal?.aborted);
323
+ }
324
+ finally {
325
+ running = false;
326
+ }
207
327
  };
208
328
  const schedule = context.schedule ?? ((callback, delayMs) => setTimeout(() => { void callback(); }, delayMs));
209
329
  const cancelScheduled = context.cancelScheduled ?? (handle => clearTimeout(handle));
210
330
  const trigger = () => {
331
+ if (stopped || io.signal?.aborted)
332
+ return;
211
333
  if (running) {
212
334
  queued = true;
213
335
  return;
214
336
  }
215
- if (timer !== undefined)
216
- cancelScheduled(timer);
217
- timer = schedule(async () => {
218
- timer = undefined;
219
- const pass = fire();
220
- active = pass;
221
- await pass;
222
- if (active === pass)
223
- active = undefined;
224
- }, 200);
337
+ try {
338
+ if (timer !== undefined)
339
+ cancelScheduled(timer);
340
+ timer = schedule(async () => {
341
+ timer = undefined;
342
+ if (stopped)
343
+ return;
344
+ const pass = fire();
345
+ active = pass;
346
+ try {
347
+ await pass;
348
+ }
349
+ catch (error) {
350
+ fail(error);
351
+ }
352
+ if (active === pass)
353
+ active = undefined;
354
+ }, 200);
355
+ }
356
+ catch (error) {
357
+ fail(error);
358
+ }
225
359
  };
226
360
  // Watch the parent directories — editors replace files on save, and a
227
361
  // watcher on the file itself dies with the old inode.
228
- const targets = [...new Set([resolve(source), resolve(values.map)])];
362
+ const targets = [...new Set([resolve(source), ...Template.isInline(values.map) && !existsSync(values.map) ? [] : [resolve(values.map)]])];
229
363
  const watch = context.watch ?? ((path, listener) => watchFs(path, listener));
230
- const watchers = targets.map(target => watch(dirname(target), (_event, filename) => {
231
- // Some platforms and watcher backends cannot identify the changed
232
- // directory entry. A filename-less event is therefore a rescan signal,
233
- // while Buffer filenames retain the same exact target filtering.
234
- if (filename === null || filename.toString() === basename(target)) {
235
- trigger();
236
- }
237
- }));
238
- try {
364
+ const watchers = [];
365
+ return watchSession(async () => {
366
+ for (const target of targets) {
367
+ const subscription = watch(dirname(target), (_event, filename) => {
368
+ // Filename-less events rescan; editors may replace the watched file.
369
+ if (filename === null || filename.toString() === basename(target))
370
+ trigger();
371
+ });
372
+ watchers.push(subscription);
373
+ subscription.on?.('error', error => { if (!stopped)
374
+ fail(error); });
375
+ }
239
376
  // Install watchers before the initial pass. A save during startup is
240
377
  // therefore either read by this pass or queued for the next one.
241
- await passOnce();
378
+ await fire();
379
+ if (stopped || io.signal?.aborted) {
380
+ await end.wait();
381
+ return 0;
382
+ }
242
383
  io.stdout.write('watching (ctrl-c to stop)\n');
243
- await waitForAbort(io.signal);
384
+ await end.wait();
244
385
  return 0;
386
+ }, () => {
387
+ stopped = true;
388
+ end.close();
389
+ if (timer !== undefined)
390
+ cancelScheduled(timer);
391
+ }, () => watchers, () => active);
392
+ };
393
+ const printMatches = (matches, pattern, io, records) => {
394
+ if (records !== undefined) {
395
+ io.stdout.write(`${JSON.stringify(records, undefined, 2)}\n`);
396
+ return;
397
+ }
398
+ if (matches.length === 0) {
399
+ io.stdout.write('no matches\n');
400
+ return;
401
+ }
402
+ const lines = matches.map(match => {
403
+ const bindings = Object.entries(match.bindings)
404
+ .map(([variable, value]) => `?${variable} = ${value}`)
405
+ .join(' ');
406
+ return bindings !== '' ? bindings : match.row?.raw_line ?? pattern;
407
+ });
408
+ io.stdout.write(`${lines.join('\n')}\n`);
409
+ };
410
+ const registryOf = (values) => values['no-prelude'] === true ? { registry: Registry.empty } : {};
411
+ /** The declared sources of the store, or the one `--name` selects. */
412
+ const selectDeclared = (store, values) => {
413
+ const declared = Declared.declaredSources(store);
414
+ if (values.name === undefined) {
415
+ return declared;
416
+ }
417
+ const selected = declared.filter(source => source.name === values.name);
418
+ if (selected.length === 0) {
419
+ throw new Error(`no declared source/${values.name}` +
420
+ (declared.length === 0 ? ' — the store declares no sources' : ` — declared: ${declared.map(source => source.name).join(', ')}`));
421
+ }
422
+ return selected;
423
+ };
424
+ const declaredPass = async (store, root, values, io, context) => {
425
+ const dir = Declared.directoryOf(root);
426
+ let failed = 0;
427
+ const done = new Map();
428
+ // `--name` selects one declaration and what it declares in turn. The
429
+ // declarations are re-read after every source: a followed .cave source
430
+ // may declare new sources or re-declare existing ones, and the current
431
+ // declaration is what runs, until nothing changes — as `assemble` does.
432
+ // The selection starts as the named source and everything it already
433
+ // owns, so a parent that fails to load does not hide its descendants.
434
+ const allowed = values.name === undefined ? undefined :
435
+ new Set(Declared.closure(store, selectDeclared(store, values).map(declared => declared.name)).map(declared => declared.name));
436
+ const version = Declared.versionCounter();
437
+ for (;;) {
438
+ io.signal?.throwIfAborted();
439
+ const next = Declared.declaredSources(store)
440
+ .find(declared => (allowed === undefined || allowed.has(declared.name)) && done.get(declared.name) !== Declared.signature(declared));
441
+ if (next === undefined) {
442
+ return failed > 0 ? 1 : 0;
443
+ }
444
+ version(next.name);
445
+ done.set(next.name, Declared.signature(next));
446
+ try {
447
+ const ready = await Declared.prepare(next, dir, context.fetchImpl, io.signal);
448
+ io.signal?.throwIfAborted();
449
+ const before = Declared.signatures(Declared.declaredSources(store));
450
+ const report = Declared.run(store, ready, { force: values.force === true, prune: values.prune === true });
451
+ if (allowed !== undefined) {
452
+ // The selection grows by whatever this source changed — added,
453
+ // re-declared, or retracted declarations alike — and by what it
454
+ // still owns, so an unchanged source keeps its descendants.
455
+ for (const name of Declared.changedNames(before, Declared.signatures(Declared.declaredSources(store))))
456
+ allowed.add(name);
457
+ for (const name of Declared.ownedDeclarations(store, next.name))
458
+ allowed.add(name);
459
+ }
460
+ io.stdout.write(`source/${next.name}: ${renderReport(report).replace(/^connect: /, '')}\n`);
461
+ if (report.failures.length > 0)
462
+ failed += 1;
463
+ }
464
+ catch (error) {
465
+ io.signal?.throwIfAborted();
466
+ io.stderr.write(`source/${next.name} (${next.path}): ${errorMessage(error)}\n`);
467
+ failed += 1;
468
+ }
469
+ }
470
+ };
471
+ const discoverOptions = (values, context, force = values.force === true) => ({
472
+ ...values.name === undefined ? {} : { only: values.name },
473
+ ...context.fetchImpl === undefined ? {} : { fetchImpl: context.fetchImpl },
474
+ ...context.signal === undefined ? {} : { signal: context.signal },
475
+ force,
476
+ prune: values.prune === true
477
+ });
478
+ /** Previews every declared source, nested declarations included, without touching the store. */
479
+ const declaredDry = async (store, root, values, io, context) => {
480
+ const sections = [];
481
+ let failures = 0;
482
+ const sequence = await Declared.discover(store, root, discoverOptions(values, context));
483
+ io.signal?.throwIfAborted();
484
+ for (const ready of sequence) {
485
+ sections.push(`; === source/${ready.declared.name} (${ready.declared.path})`);
486
+ if (ready.mapping.prelude !== '') {
487
+ sections.push(`; --- prelude\n\n${ready.mapping.prelude.trimEnd()}`);
488
+ }
489
+ ready.records.forEach((record, at) => {
490
+ const instantiation = Template.instantiate(ready.mapping.templates, name => Template.fieldOf(record, name));
491
+ if (instantiation.problems.length > 0) {
492
+ failures += 1;
493
+ sections.push([`; --- record ${at + 1}`, ...instantiation.problems.map(problem => `; FAILED — ${problem}`)].join('\n'));
494
+ return;
495
+ }
496
+ sections.push(`; --- record ${at + 1}\n\n${instantiation.text.trimEnd()}`);
497
+ });
498
+ }
499
+ io.stdout.write(`${sections.join('\n\n')}\n`);
500
+ return failures > 0 ? 1 : 0;
501
+ };
502
+ const rollback = Symbol('cave-connect declared query rollback');
503
+ /**
504
+ * Federation-lite over every declared source (spec §23.3, §23.4): the
505
+ * sources load first, URLs included, then append inside one transaction
506
+ * that rolls back after the query.
507
+ */
508
+ const stale = Symbol('cave-connect declared query stale');
509
+ const declaredQuery = async (store, root, values, io, context) => {
510
+ // The overlay applies every source (force), so discovery simulates that.
511
+ // The sequence was discovered on a snapshot; a writer may have changed
512
+ // the declarations since, so the replay checks and rediscovers if so.
513
+ for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
514
+ const { sequence, baseline } = await Declared.discovery(store, root, discoverOptions(values, context, true));
515
+ io.signal?.throwIfAborted();
516
+ let matches;
517
+ let records;
518
+ let failed = 0;
519
+ try {
520
+ store.transaction(() => {
521
+ if (!Declared.sameDeclarations(baseline, Declared.declarationState(store))) {
522
+ throw stale;
523
+ }
524
+ for (const source of sequence) {
525
+ const report = Declared.run(store, source, { force: true, prune: values.prune === true });
526
+ if (report.failures.length > 0) {
527
+ failed += 1;
528
+ io.stderr.write(`source/${source.declared.name}: ${renderReport(report).replace(/^connect: /, '')}\n`);
529
+ }
530
+ }
531
+ matches = caveQuery(store, values.query, { all: values.all === true, aliases: values.aliases === true });
532
+ if (values.json === true)
533
+ records = matches.map(match => QueryRecord.of(store, match));
534
+ throw rollback;
535
+ });
536
+ }
537
+ catch (error) {
538
+ if (error === stale) {
539
+ continue;
540
+ }
541
+ if (error !== rollback) {
542
+ throw error;
543
+ }
544
+ }
545
+ printMatches(matches, values.query, io, records);
546
+ return failed > 0 ? 1 : 0;
245
547
  }
246
- finally {
548
+ throw Declared.staleOverlay();
549
+ };
550
+ const declaredWatch = async (store, root, values, io, context) => {
551
+ let stopped = false;
552
+ const end = watchEnd(io.signal);
553
+ const fail = (error) => { stopped = true; end.fail(error); };
554
+ const dir = Declared.directoryOf(root);
555
+ const passOnce = async () => {
556
+ try {
557
+ await declaredPass(store, root, values, io, context);
558
+ }
559
+ catch (error) {
560
+ io.stderr.write(`cave connect watch pass: ${errorMessage(error)}\n`);
561
+ }
562
+ try {
563
+ if (!stopped && !io.signal?.aborted)
564
+ refreshWatchers();
565
+ }
566
+ catch (error) {
567
+ // Keep current subscriptions available to retry on the next save.
568
+ io.stderr.write(`cave connect watch subscriptions: ${errorMessage(error)}\n`);
569
+ }
570
+ };
571
+ let running = false;
572
+ let queued = false;
573
+ let timer;
574
+ let active;
575
+ const fire = async () => {
576
+ if (running) {
577
+ queued = true;
578
+ return;
579
+ }
580
+ running = true;
581
+ try {
582
+ do {
583
+ queued = false;
584
+ await passOnce();
585
+ } while (queued && !stopped && !io.signal?.aborted);
586
+ }
587
+ finally {
588
+ running = false;
589
+ }
590
+ };
591
+ const schedule = context.schedule ?? ((callback, delayMs) => setTimeout(() => { void callback(); }, delayMs));
592
+ const cancelScheduled = context.cancelScheduled ?? (handle => clearTimeout(handle));
593
+ const trigger = () => {
594
+ if (stopped || io.signal?.aborted)
595
+ return;
596
+ if (running) {
597
+ queued = true;
598
+ return;
599
+ }
600
+ try {
601
+ if (timer !== undefined)
602
+ cancelScheduled(timer);
603
+ timer = schedule(async () => {
604
+ timer = undefined;
605
+ if (stopped)
606
+ return;
607
+ // Retained so shutdown waits for a pass in flight (a URL fetch, say)
608
+ // instead of closing the store under it.
609
+ const pass = fire();
610
+ active = pass;
611
+ try {
612
+ await pass;
613
+ }
614
+ catch (error) {
615
+ fail(error);
616
+ }
617
+ if (active === pass)
618
+ active = undefined;
619
+ }, 200);
620
+ }
621
+ catch (error) {
622
+ fail(error);
623
+ }
624
+ };
625
+ // Every local declared file and mapping; URL sources only run on file
626
+ // changes. A followed .cave source may declare more, so the set is
627
+ // refreshed after every pass.
628
+ const watch = context.watch ?? ((path, listener) => watchFs(path, listener));
629
+ const watched = new Map();
630
+ const refreshWatchers = () => {
631
+ let declared;
632
+ try {
633
+ // A named watch watches the selection and everything it owns.
634
+ declared = values.name === undefined ?
635
+ selectDeclared(store, values) :
636
+ Declared.closure(store, selectDeclared(store, values).map(source => source.name));
637
+ }
638
+ catch {
639
+ return;
640
+ }
641
+ const targets = new Set(declared.flatMap(source => [
642
+ ...Source.isUrl(source.path) ? [] : [Declared.resolvePath(source.path, dir)],
643
+ // A map is a file when it resolves to one, as the loader decides; only
644
+ // otherwise is it inline and nothing to watch.
645
+ ...source.map === undefined || (Template.isInline(source.map) && !existsSync(Declared.resolvePath(source.map, dir))) ?
646
+ [] : [Declared.resolvePath(source.map, dir)]
647
+ ]));
648
+ for (const target of targets) {
649
+ if (watched.has(target))
650
+ continue;
651
+ let active = true;
652
+ const subscription = watch(dirname(target), (_event, filename) => {
653
+ if (!active)
654
+ return;
655
+ if (filename === null || filename.toString() === basename(target)) {
656
+ trigger();
657
+ }
658
+ });
659
+ watched.set(target, { close: () => { active = false; subscription.close(); } });
660
+ subscription.on?.('error', error => { if (active && !stopped)
661
+ fail(error); });
662
+ }
663
+ const errors = [];
664
+ for (const [target, subscription] of watched) {
665
+ if (targets.has(target))
666
+ continue;
667
+ // Retire ownership before closing: a failed close must not suppress a
668
+ // fresh subscription if this path is declared again on a later pass.
669
+ watched.delete(target);
670
+ try {
671
+ subscription.close();
672
+ }
673
+ catch (error) {
674
+ errors.push(error);
675
+ }
676
+ }
677
+ if (errors.length === 1)
678
+ throw errors[0];
679
+ if (errors.length > 1) {
680
+ throw new AggregateError(errors, `retiring watch subscriptions failed: ${errors.map(errorMessage).join('; ')}`, { cause: errors[0] });
681
+ }
682
+ };
683
+ return watchSession(async () => {
684
+ refreshWatchers();
685
+ if (watched.size === 0) {
686
+ io.stderr.write('cave connect: nothing to watch — no declared local source\n');
687
+ return 1;
688
+ }
689
+ await fire();
690
+ if (stopped || io.signal?.aborted) {
691
+ await end.wait();
692
+ return 0;
693
+ }
694
+ io.stdout.write('watching (ctrl-c to stop)\n');
695
+ await end.wait();
696
+ return 0;
697
+ }, () => {
698
+ stopped = true;
699
+ end.close();
247
700
  if (timer !== undefined)
248
701
  cancelScheduled(timer);
249
- watchers.forEach(watcher => watcher.close());
250
- await active;
702
+ }, () => watched.values(), () => active);
703
+ };
704
+ /**
705
+ * `cave connect` without a source (spec §23.4): the store's declared
706
+ * sources. `--list` prints them, `--dry-run` previews, `--query` overlays
707
+ * them in a rolled-back transaction, `--watch` re-runs on file changes,
708
+ * and the plain form runs one pass over each.
709
+ */
710
+ const runDeclared = async (values, io, context) => {
711
+ const perSource = ['map', 'key', 'format', 'delimiter', 'table', 'sql', 'records']
712
+ .filter(option => values[option] !== undefined);
713
+ if (perSource.length > 0) {
714
+ io.stderr.write(`cave connect: ${perSource.map(option => `--${option}`).join(', ')} describe a source argument — ` +
715
+ 'without one, declare them on the source/<name> entity (spec §23.4)\n');
716
+ return 1;
717
+ }
718
+ const db = values.db ?? defaultDbPath();
719
+ try {
720
+ if (values.list === true) {
721
+ const store = openAt(db, { intent: 'read', assemble: Declared.assemble, ...registryOf(values) });
722
+ return await withCommandStore(store, () => {
723
+ const declared = selectDeclared(store, values);
724
+ io.stdout.write(declared.length === 0 ? 'no declared sources\n' : `${declared.map(Declared.describe).join('\n')}\n`);
725
+ return 0;
726
+ });
727
+ }
728
+ if (values['dry-run'] === true || values.query !== undefined) {
729
+ // Discovery works on a private snapshot, so the dry run only reads;
730
+ // the overlay appends into the store's own rolled-back transaction.
731
+ const store = openAt(db, { intent: values.query === undefined ? 'read' : 'scratch', assemble: Declared.assemble, ...registryOf(values) });
732
+ return await withCommandStore(store, async () => {
733
+ return values.query === undefined ?
734
+ await declaredDry(store, db, values, io, context) :
735
+ await declaredQuery(store, db, values, io, context);
736
+ });
737
+ }
738
+ const store = openAt(db, { intent: 'write', ...registryOf(values) });
739
+ return await withCommandStore(store, async () => {
740
+ return values.watch === true ?
741
+ await declaredWatch(store, db, values, io, context) :
742
+ await declaredPass(store, db, values, io, context);
743
+ });
744
+ }
745
+ catch (error) {
746
+ io.stderr.write(`cave connect: ${errorMessage(error)}\n`);
747
+ return error instanceof LocateError ? 1 : 1;
251
748
  }
252
749
  };
253
750
  export const runConnect = async (argv, context = {}) => {
751
+ const signal = context.signal;
752
+ if (signal?.aborted === true)
753
+ return 0;
754
+ context = {
755
+ signal,
756
+ stdout: context.stdout,
757
+ stderr: context.stderr,
758
+ fetchImpl: context.fetchImpl,
759
+ watch: context.watch,
760
+ schedule: context.schedule,
761
+ cancelScheduled: context.cancelScheduled
762
+ };
254
763
  const io = {
255
764
  stdout: context.stdout ?? process.stdout,
256
765
  stderr: context.stderr ?? process.stderr,
257
- ...context.signal === undefined ? {} : { signal: context.signal }
766
+ ...signal === undefined ? {} : { signal }
258
767
  };
768
+ if (Boolean(io.signal?.aborted))
769
+ return 0;
259
770
  const { values, positionals } = parseArgs({
260
771
  args: [...argv],
261
772
  options: {
@@ -277,6 +788,7 @@ export const runConnect = async (argv, context = {}) => {
277
788
  all: { type: 'boolean' },
278
789
  aliases: { type: 'boolean' },
279
790
  'no-prelude': { type: 'boolean' },
791
+ list: { type: 'boolean' },
280
792
  help: { type: 'boolean', short: 'h' }
281
793
  },
282
794
  allowPositionals: true
@@ -285,11 +797,18 @@ export const runConnect = async (argv, context = {}) => {
285
797
  io.stdout.write(`${usage}\n`);
286
798
  return 0;
287
799
  }
800
+ if (positionals.length === 0) {
801
+ return runDeclared(values, io, context);
802
+ }
288
803
  const [source] = positionals;
289
804
  if (source === undefined || positionals.length !== 1) {
290
805
  io.stderr.write(`cave connect: exactly one source is required\n\n${usage}\n`);
291
806
  return 1;
292
807
  }
808
+ if (values.list === true) {
809
+ io.stderr.write('cave connect: --list takes no source\n');
810
+ return 1;
811
+ }
293
812
  if (values.map === undefined) {
294
813
  io.stderr.write(`cave connect: --map <file> is required\n\n${usage}\n`);
295
814
  return 1;
@@ -318,18 +837,15 @@ export const runConnect = async (argv, context = {}) => {
318
837
  intent: 'write',
319
838
  ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
320
839
  });
321
- try {
840
+ return await withCommandStore(store, async () => {
322
841
  if (values.watch === true) {
323
842
  return await runWatch(store, source, values, name, io, context);
324
843
  }
325
844
  return await runPass(store, source, values, name, io, context);
326
- }
327
- finally {
328
- store.close();
329
- }
845
+ });
330
846
  }
331
847
  catch (error) {
332
- io.stderr.write(`cave connect: ${error instanceof Error ? error.message : String(error)}\n`);
848
+ io.stderr.write(`cave connect: ${errorMessage(error)}\n`);
333
849
  return 1;
334
850
  }
335
851
  };