@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
package/dist/src/cli.js CHANGED
@@ -34,13 +34,17 @@
34
34
  * `file` defaults to stdin (`-`); `--db` defaults to `$CAVE_DB`, then
35
35
  * `cave.db`. Every command answers `--help` with options and examples.
36
36
  */
37
- import { readFileSync, readSync, statSync, writeFileSync } from 'node:fs';
38
- import { resolve } from 'node:path';
37
+ import { readFileSync, readSync, realpathSync, statSync } from 'node:fs';
38
+ import { readHooks } from "./hooks-config.js";
39
+ import { basename, dirname, join, resolve } from 'node:path';
39
40
  import { parseArgs } from 'node:util';
40
- import { Version } from '@cavelang/core';
41
+ import { writeOutput } from "./atomic-output.js";
42
+ import { errorMessage } from "./error-message.js";
43
+ import { Time, Version } from '@cavelang/core';
41
44
  import { parseDocument, Token } from '@cavelang/parser';
42
45
  import { Registry, standardRegistry } from '@cavelang/canonical';
43
- import { LocateError, Sensitivity, backup as backupStore, defaultDbPath, openAt, restoreBackup, verifyBackup } from '@cavelang/store';
46
+ import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, QuerySql, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store';
47
+ import { Declared, assemble as assembleSources } from '@cavelang/cli/connect';
44
48
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query';
45
49
  import { check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply, generateClient, suggestAliases, writeSuggestions } from '@cavelang/cli/shape';
46
50
  import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/cli/rules';
@@ -218,7 +222,7 @@ Examples:
218
222
  query: `cave query — run a CAVE-Q pattern against a store
219
223
 
220
224
  Usage:
221
- cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
225
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--sources] [--no-prelude]
222
226
 
223
227
  Options:
224
228
  ${dbHelp}
@@ -244,6 +248,12 @@ Options:
244
248
  collapse to the row the resolution policy picks
245
249
  (precedence class, reliability-weighted confidence, tx);
246
250
  incompatible with --all
251
+ --sources overlay the store's declared sources (spec §23.4) inside a
252
+ rolled-back transaction — federation-lite: external data is
253
+ consulted at query time, nothing persists. The answer is
254
+ whole (no --cursor, --limit is not a page size); a text
255
+ store followed its local sources on open, so only URL
256
+ sources are loaded here
247
257
  --no-prelude open the store without the standard verb registry
248
258
 
249
259
  Patterns are claim triples with ?variables and optional metadata filters
@@ -263,6 +273,7 @@ Examples:
263
273
  cave query --db k.db 'revenue IS' --at 2026-07
264
274
  cave query --db k.db 'alice WORKS-AT ?org' --at 2021
265
275
  cave query --db k.db 'service HAS owner: ?who' --resolve
276
+ cave query --db k.db '?who WORKS-AT acme' --sources
266
277
  cave query --db k.db '?x ?verb ?y @production' --json`,
267
278
  search: `cave search — full-text search over claims and comments (spec §13.2)
268
279
 
@@ -479,7 +490,7 @@ Usage:
479
490
 
480
491
  Options:
481
492
  ${dbHelp}
482
- --out <file> write the generated module instead of stdout
493
+ --out <file> atomically replace a file with the generated module
483
494
  --version <n> generated-client format version (default and currently
484
495
  supported: 1)
485
496
  --no-prelude open the store without the standard verb registry
@@ -502,7 +513,7 @@ Usage:
502
513
  Options:
503
514
  ${dbHelp}
504
515
  --min <s> minimum evidence score, 0..1 or N% (default 60%)
505
- --limit <n> at most n suggestions, strongest first
516
+ --limit <n> positive safe integer; at most n suggestions, strongest first
506
517
  --agent <template> LLM judge filtering the candidates — the cave
507
518
  ingest/eval shell contract: the prompt (each pair
508
519
  with both sides' current claims) is piped to stdin
@@ -533,6 +544,10 @@ claim, so the opt-in §13.6 closure links it until reviewed:
533
544
  claim identity, §9.2, so a plain @ 0% append would start a new series
534
545
  and leave the suggested link standing).
535
546
 
547
+ Writes recheck pair history under their transaction reservation and skip
548
+ reviewed pairs. --json returns the selected suggestion array; --json --write
549
+ performs the write and returns { suggestions, appended }, with the actual count.
550
+
536
551
  Examples:
537
552
  cave suggest-alias --db k.db
538
553
  cave suggest-alias --db k.db --min 80% --limit 10
@@ -588,7 +603,7 @@ Usage:
588
603
 
589
604
  Options:
590
605
  ${dbHelp}
591
- --out <file> write to a file instead of stdout — refused when it
606
+ --out <file> atomically replace a file instead of stdout — refused when it
592
607
  names the store's own database file
593
608
  --current current beliefs only (skip superseded rows); compacting
594
609
  view, not sanitization or selective erasure (spec §9.6)
@@ -604,6 +619,10 @@ Options:
604
619
  Retention: claim history is permanent. Retraction and --current do not
605
620
  guarantee erasure from the store, exports, peers, or backups (spec §9.6).
606
621
 
622
+ File output is staged and flushed before replacement. Existing permissions
623
+ and valid symlinks are preserved; hard-link aliases retain the previous file.
624
+ Dangling symlinks and non-regular output files are refused.
625
+
607
626
  Examples:
608
627
  cave export --db k.db
609
628
  cave export --db k.db --out internal-export.cave
@@ -622,7 +641,7 @@ Usage:
622
641
 
623
642
  Options:
624
643
  ${dbHelp}
625
- --out <file> write the rendered markdown to a file instead of stdout
644
+ --out <file> atomically replace a file with the rendered markdown
626
645
  --aliases queries match through the alias closure (spec §13.6)
627
646
  --resolve queries match resolved winners only (spec §26) — the fix
628
647
  when an inline splice reports an ambiguous fact
@@ -716,6 +735,15 @@ Examples:
716
735
  cave help query
717
736
  cave help serve`
718
737
  };
738
+ const decodeInput = (bytes, source) => {
739
+ try {
740
+ // Keep the existing BOM behavior of Buffer.toString while rejecting loss.
741
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
742
+ }
743
+ catch (cause) {
744
+ throw new TypeError(`${source}: invalid UTF-8 input`, { cause });
745
+ }
746
+ };
719
747
  const readStdin = () => {
720
748
  const chunks = [];
721
749
  const buffer = Buffer.allocUnsafe(64 * 1024);
@@ -724,7 +752,7 @@ const readStdin = () => {
724
752
  try {
725
753
  const size = readSync(0, buffer, 0, buffer.length, null);
726
754
  if (size === 0)
727
- return Buffer.concat(chunks).toString('utf8');
755
+ return decodeInput(Buffer.concat(chunks), 'stdin');
728
756
  chunks.push(Buffer.from(buffer.subarray(0, size)));
729
757
  }
730
758
  catch (error) {
@@ -747,11 +775,78 @@ const readStdin = () => {
747
775
  */
748
776
  const openDb = (values, intent) => openAt(values.db ?? defaultDbPath(), {
749
777
  intent,
778
+ // A text store follows its declared sources on open (spec §23.4).
779
+ assemble: assembleSources,
750
780
  ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
751
781
  });
782
+ /** Retain command output and diagnostics when its owned store fails to close. */
783
+ const closeCommandStore = (store, output) => {
784
+ try {
785
+ store.close();
786
+ }
787
+ catch (error) {
788
+ const separator = output.err === '' || output.err.endsWith('\n') ? '' : '\n';
789
+ return { ...output, code: output.code === 0 ? 1 : output.code,
790
+ err: `${output.err}${separator}store close failed: ${errorMessage(error)}\n` };
791
+ }
792
+ return output;
793
+ };
794
+ const withCommandStore = (store, run) => {
795
+ let output;
796
+ try {
797
+ output = run();
798
+ }
799
+ catch (error) {
800
+ output = fail(`${errorMessage(error)}\n`);
801
+ }
802
+ return closeCommandStore(store, output);
803
+ };
804
+ const withAsyncCommandStore = async (store, run) => {
805
+ let output;
806
+ try {
807
+ output = await run();
808
+ }
809
+ catch (error) {
810
+ output = fail(`${errorMessage(error)}\n`);
811
+ }
812
+ return closeCommandStore(store, output);
813
+ };
814
+ const sourcesRollback = Symbol('cave query --sources rollback');
815
+ /**
816
+ * Runs `body` with the store's declared sources overlaid (spec §23.4): the
817
+ * sources, loaded beforehand (URLs included), append inside a transaction
818
+ * that always rolls back, so external data is consulted at query time and
819
+ * nothing persists, digests included.
820
+ */
821
+ const withSources = (store, discovered, body) => {
822
+ let result;
823
+ try {
824
+ store.transaction(() => {
825
+ // Discovered on a snapshot: if a writer changed the declarations
826
+ // since, the sequence is stale and the caller rediscovers.
827
+ if (!Declared.sameDeclarations(discovered.baseline, Declared.declarationState(store))) {
828
+ throw sourcesRollback;
829
+ }
830
+ for (const source of discovered.sequence) {
831
+ const report = Declared.run(store, source, { force: true });
832
+ if (report.failures.length > 0) {
833
+ throw new LocateError(`source/${source.declared.name}: ${report.failures.map(failure => `${failure.record}: ${failure.problems.join('; ')}`).join('; ')}`);
834
+ }
835
+ }
836
+ result = { value: body() };
837
+ throw sourcesRollback;
838
+ });
839
+ }
840
+ catch (error) {
841
+ if (error !== sourcesRollback) {
842
+ throw error;
843
+ }
844
+ }
845
+ return result;
846
+ };
752
847
  const readInput = (files) => files.length === 0 || (files.length === 1 && files[0] === '-') ?
753
848
  readStdin() :
754
- files.map(file => readFileSync(file, 'utf8')).join('\n');
849
+ files.map(file => decodeInput(readFileSync(file), file)).join('\n');
755
850
  export const parseCommand = (argv) => {
756
851
  const { values, positionals } = parseArgs({
757
852
  args: [...argv],
@@ -826,7 +921,7 @@ const ingestCommand = (name) => (argv) => {
826
921
  });
827
922
  const input = readInput(positionals);
828
923
  const store = openDb(values, 'write');
829
- try {
924
+ return withCommandStore(store, () => {
830
925
  const options = {
831
926
  strict: values.strict === true,
832
927
  ...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
@@ -848,13 +943,7 @@ const ingestCommand = (name) => (argv) => {
848
943
  out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
849
944
  err: problems === '' ? '' : `${problems}\n`
850
945
  };
851
- }
852
- catch (error) {
853
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
854
- }
855
- finally {
856
- store.close();
857
- }
946
+ });
858
947
  };
859
948
  export const addCommand = ingestCommand('add');
860
949
  /**
@@ -870,39 +959,113 @@ export const addCommand = ingestCommand('add');
870
959
  * identity.
871
960
  */
872
961
  export const importCommand = ingestCommand('import');
873
- export const queryCommand = (argv) => {
874
- const { values, positionals } = parseArgs({
875
- args: [...argv],
876
- options: {
877
- db: { type: 'string' },
878
- json: { type: 'boolean' },
879
- all: { type: 'boolean' },
880
- aliases: { type: 'boolean' },
881
- 'as-of': { type: 'string' },
882
- at: { type: 'string' },
883
- resolve: { type: 'boolean' },
884
- limit: { type: 'string' },
885
- cursor: { type: 'string' },
886
- 'no-prelude': { type: 'boolean' }
887
- },
888
- allowPositionals: true
889
- });
962
+ const queryOptions = {
963
+ db: { type: 'string' },
964
+ json: { type: 'boolean' },
965
+ all: { type: 'boolean' },
966
+ aliases: { type: 'boolean' },
967
+ 'as-of': { type: 'string' },
968
+ at: { type: 'string' },
969
+ resolve: { type: 'boolean' },
970
+ sources: { type: 'boolean' },
971
+ limit: { type: 'string' },
972
+ cursor: { type: 'string' },
973
+ 'no-prelude': { type: 'boolean' }
974
+ };
975
+ const parseQuery = (argv) => {
976
+ const { values, positionals } = parseArgs({ args: [...argv], options: queryOptions, allowPositionals: true });
890
977
  if (positionals.length === 0) {
891
- return fail('cave query: a pattern is required (spec §12.1)\n');
978
+ return { failure: fail('cave query: a pattern is required (spec §12.1)\n') };
979
+ }
980
+ if (values.sources === true && values.cursor !== undefined) {
981
+ return { failure: fail('cave query: --cursor does not apply to --sources — the overlay exists for one invocation and is answered whole\n') };
982
+ }
983
+ return { values, pattern: positionals.join('\n') };
984
+ };
985
+ /**
986
+ * `cave query --sources` — the store with its declared sources overlaid
987
+ * (spec §23.4). Asynchronous because the sources load first, URLs
988
+ * included, before the overlay appends inside a transaction that rolls
989
+ * back; the dispatcher routes `--sources` here. A text store followed its
990
+ * local sources on open, so only what assembly could not follow — URL
991
+ * sources and what they declare — is loaded. The overlay exists for this
992
+ * invocation alone, so the answer is whole: pages are followed inside the
993
+ * transaction and no cursor is handed out.
994
+ */
995
+ export const querySourcesCommand = async (argv, runtime = {}) => {
996
+ const parsed = parseQuery(argv);
997
+ if ('failure' in parsed) {
998
+ return parsed.failure;
999
+ }
1000
+ const { values, pattern } = parsed;
1001
+ if (values.sources !== true) {
1002
+ return queryCommand(argv);
1003
+ }
1004
+ const root = values.db ?? defaultDbPath();
1005
+ // The overlay appends inside a transaction it rolls back: writable, but
1006
+ // never creating or migrating (spec §13.7).
1007
+ const store = openDb(values, 'scratch');
1008
+ return await withAsyncCommandStore(store, async () => {
1009
+ for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
1010
+ const discovered = await Declared.discovery(store, root, {
1011
+ // The overlay applies every source it loads (force), so discovery
1012
+ // simulates that; a text store skips what assembly already followed.
1013
+ force: true,
1014
+ ...kindOf(root) === 'text' ? { skipFollowed: true } : {},
1015
+ ...runtime.fetchImpl === undefined ? {} : { fetchImpl: runtime.fetchImpl }
1016
+ });
1017
+ const page = withSources(store, discovered, () => wholeQuery(store, values, pattern));
1018
+ if (page !== undefined) {
1019
+ return renderQueryPage(store, values, pattern, () => page.value);
1020
+ }
1021
+ }
1022
+ throw Declared.staleOverlay();
1023
+ });
1024
+ };
1025
+ export const queryCommand = (argv) => {
1026
+ const parsed = parseQuery(argv);
1027
+ if ('failure' in parsed) {
1028
+ return parsed.failure;
1029
+ }
1030
+ const { values, pattern } = parsed;
1031
+ if (values.sources === true) {
1032
+ // Loading sources cannot be synchronous: the in-process entry for the
1033
+ // overlay is `querySourcesCommand`, which the dispatcher uses.
1034
+ return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n');
892
1035
  }
893
- const pattern = positionals.join('\n');
894
1036
  const store = openDb(values, 'read');
895
- try {
896
- const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit);
897
- const result = caveQueryPage(store, pattern, {
898
- all: values.all === true,
899
- aliases: values.aliases === true,
900
- resolve: values.resolve === true,
901
- limit,
902
- ...values.cursor === undefined ? {} : { cursor: values.cursor },
903
- ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
904
- ...values.at === undefined ? {} : { at: values.at }
905
- });
1037
+ return withCommandStore(store, () => {
1038
+ return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern));
1039
+ });
1040
+ };
1041
+ /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
1042
+ const wholeQuery = (store, values, pattern) => {
1043
+ const matches = [];
1044
+ let first;
1045
+ let cursor;
1046
+ for (;;) {
1047
+ const page = queryPage(store, { ...values, limit: '1000', ...cursor === undefined ? {} : { cursor } }, pattern);
1048
+ first ??= page;
1049
+ matches.push(...page.matches);
1050
+ if (page.next === undefined) {
1051
+ const { next: _next, ...rest } = first;
1052
+ return { ...rest, matches };
1053
+ }
1054
+ cursor = page.next;
1055
+ }
1056
+ };
1057
+ const queryPage = (store, values, pattern) => caveQueryPage(store, pattern, {
1058
+ all: values.all === true,
1059
+ aliases: values.aliases === true,
1060
+ resolve: values.resolve === true,
1061
+ limit: values.limit === undefined ? defaultQueryLimit : Number(values.limit),
1062
+ ...values.cursor === undefined ? {} : { cursor: values.cursor },
1063
+ ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
1064
+ ...values.at === undefined ? {} : { at: values.at }
1065
+ });
1066
+ const renderQueryPage = (store, values, pattern, page) => {
1067
+ {
1068
+ const result = page();
906
1069
  if (values.json === true) {
907
1070
  return ok(`${JSON.stringify(result, undefined, 2)}\n`);
908
1071
  }
@@ -932,12 +1095,6 @@ export const queryCommand = (argv) => {
932
1095
  });
933
1096
  return ok(`${lines.join('\n')}\n${result.next === undefined ? '' : `next: ${result.next}\n`}`);
934
1097
  }
935
- catch (error) {
936
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
937
- }
938
- finally {
939
- store.close();
940
- }
941
1098
  };
942
1099
  export const searchFormat = 'cave.search';
943
1100
  export const searchVersion = 1;
@@ -967,7 +1124,7 @@ export const searchCommand = (argv) => {
967
1124
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`);
968
1125
  }
969
1126
  const store = openDb(values, 'read');
970
- try {
1127
+ return withCommandStore(store, () => {
971
1128
  const raw = values.raw === true;
972
1129
  const found = store.search(query, { raw, limit: limit + 1 });
973
1130
  const matches = found.slice(0, limit);
@@ -991,13 +1148,7 @@ export const searchCommand = (argv) => {
991
1148
  lines.push(`more matches beyond ${limit}; raise --limit`);
992
1149
  }
993
1150
  return ok(`${lines.join('\n')}\n`);
994
- }
995
- catch (error) {
996
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
997
- }
998
- finally {
999
- store.close();
1000
- }
1151
+ });
1001
1152
  };
1002
1153
  /**
1003
1154
  * `cave resolve` — the human view of contradiction resolution (spec §26.4):
@@ -1016,15 +1167,22 @@ export const resolveCommand = (argv) => {
1016
1167
  },
1017
1168
  allowPositionals: false
1018
1169
  });
1019
- const percent = (value) => `${Math.round(value * 1000) / 10}%`;
1170
+ const percent = (value) => {
1171
+ const rounded = Math.round(value * 1000) / 10;
1172
+ if (value > 0 && rounded === 0)
1173
+ return '<0.1%';
1174
+ if (value < 1 && rounded === 100)
1175
+ return '>99.9%';
1176
+ return `${rounded}%`;
1177
+ };
1020
1178
  const store = openDb(values, 'read');
1021
- try {
1179
+ return withCommandStore(store, () => {
1022
1180
  if (values.policy === true) {
1023
1181
  const policy = store.resolutionPolicy();
1024
1182
  if (values.json === true) {
1025
1183
  return ok(`${JSON.stringify(policy, undefined, 2)}\n`);
1026
1184
  }
1027
- const width = Math.max(...policy.map(entry => entry.prefix.length + 7));
1185
+ const width = policy.reduce((maximum, entry) => Math.max(maximum, entry.prefix.length + 7), 0);
1028
1186
  const lines = policy.map(entry => {
1029
1187
  const subject = entry.prefix === '' ? 'source' : `source/${entry.prefix}`;
1030
1188
  const dimensions = [
@@ -1044,13 +1202,7 @@ export const resolveCommand = (argv) => {
1044
1202
  }
1045
1203
  const lines = contested.flatMap(group => group.rows.map((row, at) => `${at === 0 ? '' : ' over '}${row.raw_line} ; class ${row.res_class}, effective ${percent(row.res_conf)}`));
1046
1204
  return ok(`${lines.join('\n')}\n`);
1047
- }
1048
- catch (error) {
1049
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1050
- }
1051
- finally {
1052
- store.close();
1053
- }
1205
+ });
1054
1206
  };
1055
1207
  export const deriveCommand = (argv) => {
1056
1208
  const { values, positionals } = parseArgs({
@@ -1076,15 +1228,15 @@ export const deriveCommand = (argv) => {
1076
1228
  return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`);
1077
1229
  }
1078
1230
  const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes']);
1079
- if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1080
- return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`);
1231
+ if (maxPasses !== undefined && (!Number.isSafeInteger(maxPasses) || maxPasses < 1)) {
1232
+ return fail(`cave derive: --max-passes expects a positive safe integer, got ${JSON.stringify(values['max-passes'])}\n`);
1081
1233
  }
1082
1234
  // The intent follows the branch order below: listing reads, retracting
1083
1235
  // writes, a dry run appends inside a rolled-back transaction.
1084
1236
  const store = openDb(values, values.list === true ? 'read' :
1085
1237
  values.retract !== undefined ? 'write' :
1086
1238
  values['dry-run'] === true ? 'scratch' : 'write');
1087
- try {
1239
+ return withCommandStore(store, () => {
1088
1240
  if (values.list === true) {
1089
1241
  const rules = listRules(store);
1090
1242
  if (values.json === true) {
@@ -1144,13 +1296,16 @@ export const deriveCommand = (argv) => {
1144
1296
  report = derive(store, options);
1145
1297
  }
1146
1298
  if (declaration !== undefined) {
1147
- err.push(...declaration.problems.map(problem => `rules line ${problem.line}: ${problem.message}`));
1299
+ for (const problem of declaration.problems)
1300
+ err.push(`rules line ${problem.line}: ${problem.message}`);
1148
1301
  out.push(`declared ${declaration.declared} rule(s)` +
1149
1302
  (declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
1150
1303
  (declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''));
1151
1304
  }
1305
+ const hasProblems = (declaration?.problems.length ?? 0) > 0 || !report.complete ||
1306
+ report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0);
1152
1307
  if (values.json === true) {
1153
- return { code: report.problems.length > 0 || !report.complete ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') };
1308
+ return { code: hasProblems ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') };
1154
1309
  }
1155
1310
  for (const problem of report.problems) {
1156
1311
  err.push(`${problem.subject}: ${problem.problems.join('; ')}`);
@@ -1160,33 +1315,16 @@ export const deriveCommand = (argv) => {
1160
1315
  `${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
1161
1316
  'unchanged premises, skipped';
1162
1317
  out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`);
1163
- err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`));
1318
+ for (const problem of rule.problems)
1319
+ err.push(`${rule.subject}: ${problem}`);
1164
1320
  }
1165
- out.push(...report.notes.map(note => `note: ${note}`));
1166
- out.push(`derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1321
+ for (const note of report.notes)
1322
+ out.push(`note: ${note}`);
1323
+ out.push(`derived${report.complete ? '' : ' (incomplete)'}${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1167
1324
  `+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
1168
1325
  `${report.unchanged} unchanged (${report.passes} pass(es))`);
1169
- const hasProblems = !report.complete || report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0);
1170
1326
  return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` };
1171
- }
1172
- catch (error) {
1173
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1174
- }
1175
- finally {
1176
- store.close();
1177
- }
1178
- };
1179
- /**
1180
- * Loads a hooks configuration file (spec §25.4): a JSON object mapping
1181
- * hook names to shell command templates.
1182
- */
1183
- const readHooks = (path) => {
1184
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
1185
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
1186
- Object.values(parsed).some(value => typeof value !== 'string')) {
1187
- throw new Error(`${path}: hooks must be a JSON object of name → shell template strings`);
1188
- }
1189
- return parsed;
1327
+ });
1190
1328
  };
1191
1329
  /** Text rendering of a spec §25.2 execution report. */
1192
1330
  const renderActReport = (report) => {
@@ -1205,17 +1343,16 @@ const renderActReport = (report) => {
1205
1343
  ];
1206
1344
  let code = 0;
1207
1345
  if (report.hook !== undefined) {
1208
- if (!report.hook.fired) {
1209
- lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`);
1346
+ if (report.hook.error !== undefined) {
1347
+ // Effects may already be committed even when hook lookup failed before spawn.
1348
+ lines.push(`hook ${report.hook.name}: ${report.hook.error}`);
1349
+ code = 1;
1210
1350
  }
1211
- else if (report.hook.error === undefined) {
1212
- lines.push(`hook ${report.hook.name}: ok`);
1351
+ else if (!report.hook.fired) {
1352
+ lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`);
1213
1353
  }
1214
1354
  else {
1215
- // The claims committed before the hook ran (spec §25.4) — the
1216
- // failure is reported, and the exit code carries it.
1217
- lines.push(`hook ${report.hook.name}: ${report.hook.error}`);
1218
- code = 1;
1355
+ lines.push(`hook ${report.hook.name}: ok`);
1219
1356
  }
1220
1357
  }
1221
1358
  return { lines, code };
@@ -1244,7 +1381,7 @@ export const actCommand = (argv) => {
1244
1381
  values.list === true ? 'read' :
1245
1382
  values.retract !== undefined ? 'write' :
1246
1383
  values['dry-run'] === true ? 'scratch' : 'write');
1247
- try {
1384
+ return withCommandStore(store, () => {
1248
1385
  if (values.declare === true) {
1249
1386
  const declaration = declareActions(store, readInput(positionals));
1250
1387
  const err = declaration.problems.map(problem => `line ${problem.line}: ${problem.message}`);
@@ -1290,7 +1427,13 @@ export const actCommand = (argv) => {
1290
1427
  if (at <= 0) {
1291
1428
  return fail(`cave act: expected param=value, got ${JSON.stringify(pair)}\n`);
1292
1429
  }
1293
- args[pair.slice(0, at)] = pair.slice(at + 1);
1430
+ const parameter = pair.slice(0, at);
1431
+ if (Object.hasOwn(args, parameter)) {
1432
+ return fail(`cave act: duplicate parameter ${JSON.stringify(parameter)}\n`);
1433
+ }
1434
+ Object.defineProperty(args, parameter, {
1435
+ value: pair.slice(at + 1), enumerable: true, writable: true, configurable: true
1436
+ });
1294
1437
  }
1295
1438
  const hooksPath = values.hooks ?? process.env['CAVE_HOOKS'];
1296
1439
  const report = act(store, name, args, {
@@ -1306,13 +1449,7 @@ export const actCommand = (argv) => {
1306
1449
  return report.ok ?
1307
1450
  { code, out: `${lines.join('\n')}\n`, err: '' } :
1308
1451
  { code, out: '', err: `${lines.join('\n')}\n` };
1309
- }
1310
- catch (error) {
1311
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1312
- }
1313
- finally {
1314
- store.close();
1315
- }
1452
+ });
1316
1453
  };
1317
1454
  /** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
1318
1455
  const renderReport = (report, staleDays) => {
@@ -1321,26 +1458,38 @@ const renderReport = (report, staleDays) => {
1321
1458
  lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`);
1322
1459
  if (report.violations.length > 0) {
1323
1460
  lines.push(`violations (${report.violations.length}):`);
1324
- lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`));
1461
+ for (const violation of report.violations)
1462
+ lines.push(` ${formatViolation(violation)}`);
1325
1463
  }
1326
1464
  if (report.stale.length > 0) {
1327
1465
  lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`);
1328
- lines.push(...report.stale.map(({ row, ageDays }) => ` ${row.raw_line} (${ageDays}d)`));
1466
+ for (const { row, ageDays } of report.stale)
1467
+ lines.push(` ${row.raw_line} (${ageDays}d)`);
1329
1468
  }
1330
1469
  if (report.review.length > 0) {
1331
1470
  lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`);
1332
- lines.push(...report.review.map(row => ` ${row.raw_line}`));
1471
+ for (const row of report.review)
1472
+ lines.push(` ${row.raw_line}`);
1333
1473
  }
1334
1474
  if (report.disagreements.length > 0) {
1335
1475
  lines.push(`alias disagreements (${report.disagreements.length}):`);
1336
1476
  for (const disagreement of report.disagreements) {
1337
1477
  lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`);
1338
- lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`));
1478
+ for (const row of disagreement.rows)
1479
+ lines.push(` ${row.raw_line}`);
1339
1480
  }
1340
1481
  }
1482
+ const confidencePercent = (value) => {
1483
+ const rounded = Math.round(value * 100);
1484
+ if (value > 0 && rounded === 0)
1485
+ return '<1%';
1486
+ if (value < 1 && rounded === 100)
1487
+ return '>99%';
1488
+ return `${rounded}%`;
1489
+ };
1341
1490
  const confidence = coverage.averageConfidence === null ?
1342
1491
  '' :
1343
- `; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`;
1492
+ `; avg conf ${confidencePercent(coverage.averageConfidence)}, ${coverage.lowConfidence} low (< 0.3)`;
1344
1493
  lines.push(`coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
1345
1494
  `${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
1346
1495
  `${confidence}; ${coverage.entities} entities, ${coverage.typedEntities} typed`);
@@ -1362,17 +1511,14 @@ export const checkCommand = (argv) => {
1362
1511
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`);
1363
1512
  }
1364
1513
  const store = openDb(values, 'read');
1365
- try {
1514
+ return withCommandStore(store, () => {
1366
1515
  const report = caveCheck(store, { staleDays });
1367
1516
  const out = values.json === true ?
1368
1517
  `${stableJson(store, report)}\n` :
1369
1518
  renderReport(report, staleDays);
1370
1519
  // Violations fail the check (spec §20.2); the other sections are advisory.
1371
1520
  return { code: report.violations.length > 0 ? 1 : 0, out, err: '' };
1372
- }
1373
- finally {
1374
- store.close();
1375
- }
1521
+ });
1376
1522
  };
1377
1523
  const snapshotLine = (action, snapshot) => `${action} exact backup (${snapshot.rows} row(s), schema v${snapshot.schemaVersion}, ` +
1378
1524
  `${snapshot.bytes} bytes, sha256:${snapshot.sha256}) at ${snapshot.path}\n`;
@@ -1410,19 +1556,17 @@ export const backupCommand = (argv) => {
1410
1556
  // the destination aliases the source: refuse before the file could be
1411
1557
  // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1412
1558
  // the API's own check).
1413
- if (sameFile(dbPath, values.out)) {
1559
+ if (sameStoreFile(dbPath, values.out)) {
1414
1560
  return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1415
1561
  }
1416
- const store = openAt(dbPath, { intent: 'read' });
1417
- try {
1418
- return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })));
1419
- }
1420
- finally {
1421
- store.close();
1422
- }
1562
+ const destination = values.out;
1563
+ const store = openAt(dbPath, { intent: 'read', assemble: assembleSources });
1564
+ return withCommandStore(store, () => {
1565
+ return ok(snapshotLine('created', backupStore(store, destination, { force: values.force === true })));
1566
+ });
1423
1567
  }
1424
1568
  catch (error) {
1425
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1569
+ return fail(`${errorMessage(error)}\n`);
1426
1570
  }
1427
1571
  };
1428
1572
  /** `cave restore` — verify and atomically restore an exact snapshot (§13.2.2). */
@@ -1452,7 +1596,7 @@ export const restoreCommand = (argv) => {
1452
1596
  })));
1453
1597
  }
1454
1598
  catch (error) {
1455
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1599
+ return fail(`${errorMessage(error)}\n`);
1456
1600
  }
1457
1601
  };
1458
1602
  /**
@@ -1487,15 +1631,15 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1487
1631
  return fail(`cave suggest-alias: --min expects a score in 0..1 or N%, got ${JSON.stringify(values.min)}\n`);
1488
1632
  }
1489
1633
  const limit = values.limit === undefined ? undefined : Number(values.limit);
1490
- if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
1491
- return fail(`cave suggest-alias: --limit expects a positive integer, got ${JSON.stringify(values.limit)}\n`);
1634
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
1635
+ return fail(`cave suggest-alias: --limit expects a positive safe integer, got ${JSON.stringify(values.limit)}\n`);
1492
1636
  }
1493
1637
  const timeoutSeconds = values.timeout === undefined ? undefined : Number(values.timeout);
1494
1638
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1495
1639
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1496
1640
  }
1497
1641
  const store = openDb(values, values.write === true ? 'write' : 'read');
1498
- try {
1642
+ return await withAsyncCommandStore(store, async () => {
1499
1643
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } });
1500
1644
  if (values.agent !== undefined && suggestions.length > 0) {
1501
1645
  const reply = await shellComplete(values.agent, {
@@ -1505,7 +1649,9 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1505
1649
  suggestions = parseJudgeReply(reply, suggestions.length).map(index => suggestions[index]);
1506
1650
  }
1507
1651
  if (values.json === true) {
1508
- return ok(`${JSON.stringify(suggestions, undefined, 2)}\n`);
1652
+ const result = values.write === true ?
1653
+ { suggestions, ...writeSuggestions(store, suggestions) } : suggestions;
1654
+ return ok(`${JSON.stringify(result, undefined, 2)}\n`);
1509
1655
  }
1510
1656
  if (suggestions.length === 0) {
1511
1657
  return ok('no alias suggestions\n');
@@ -1516,13 +1662,10 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1516
1662
  return ok(`${lines.join('\n')}\nappended ${appended} suggested alias claim(s)\n`);
1517
1663
  }
1518
1664
  return ok(`${lines.join('\n')}\n`);
1519
- }
1520
- finally {
1521
- store.close();
1522
- }
1665
+ });
1523
1666
  }
1524
1667
  catch (error) {
1525
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1668
+ return fail(`${errorMessage(error)}\n`);
1526
1669
  }
1527
1670
  };
1528
1671
  /**
@@ -1551,7 +1694,7 @@ export const syncCommand = (argv) => {
1551
1694
  }
1552
1695
  const dbPath = values.db ?? defaultDbPath();
1553
1696
  const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write');
1554
- try {
1697
+ return withCommandStore(store, () => {
1555
1698
  const options = {
1556
1699
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
1557
1700
  record: values['no-record'] !== true,
@@ -1565,7 +1708,7 @@ export const syncCommand = (argv) => {
1565
1708
  return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' };
1566
1709
  }
1567
1710
  if (report.problems.length > 0) {
1568
- const detail = report.problems.map(problem => ` line ${problem.line}: ${problem.message}`).join('\n');
1711
+ const detail = report.problems.map(problem => ` ${problem.line === 0 ? '' : `line ${problem.line}: `}${problem.message}`).join('\n');
1569
1712
  return fail(`cave sync: ${source}: ${report.problems.length} problem(s), nothing merged\n${detail}\n`);
1570
1713
  }
1571
1714
  const lines = [
@@ -1577,18 +1720,13 @@ export const syncCommand = (argv) => {
1577
1720
  lines.push(`record: ${report.record}`);
1578
1721
  }
1579
1722
  return ok(`${lines.join('\n')}\n`);
1580
- }
1581
- catch (error) {
1582
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1583
- }
1584
- finally {
1585
- store.close();
1586
- }
1723
+ });
1587
1724
  };
1588
1725
  /**
1589
1726
  * `cave report` — the §31 deliverable: a markdown template's cave-q
1590
- * blocks and inline splices render from the store, every stated fact
1591
- * cited back to its claim. Problems mark the text, land on stderr with
1727
+ * blocks and inline splices render from the store, with matched rows
1728
+ * cited back to their claims. Ordinary prose is not automatically checked
1729
+ * or cited. Problems mark the text, land on stderr with
1592
1730
  * template line numbers, and fail the exit code — the render never
1593
1731
  * silently drops a fact.
1594
1732
  */
@@ -1607,16 +1745,22 @@ export const reportCommand = (argv) => {
1607
1745
  },
1608
1746
  allowPositionals: true
1609
1747
  });
1610
- const template = readInput(positionals);
1748
+ if (values.at !== undefined && Time.parseInstant(values.at) === undefined) {
1749
+ return fail(`cave report: --at cannot parse at anchor ${JSON.stringify(values.at)}\n`);
1750
+ }
1751
+ if (values['as-of'] !== undefined && QuerySql.asOfBoundary(values['as-of']) === undefined) {
1752
+ return fail(`cave report: --as-of cannot parse as-of boundary ${JSON.stringify(values['as-of'])}\n`);
1753
+ }
1611
1754
  const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum);
1612
1755
  if (maximum === undefined) {
1613
1756
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`);
1614
1757
  }
1615
- if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1758
+ if (values.out !== undefined && sameStoreFile(values.db ?? defaultDbPath(), values.out)) {
1616
1759
  return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1617
1760
  }
1761
+ const template = readInput(positionals);
1618
1762
  const store = openDb(values, 'read');
1619
- try {
1763
+ return withCommandStore(store, () => {
1620
1764
  const rendered = caveReport(store, template, {
1621
1765
  aliases: values.aliases === true,
1622
1766
  resolve: values.resolve === true,
@@ -1631,19 +1775,13 @@ export const reportCommand = (argv) => {
1631
1775
  if (values.out === undefined) {
1632
1776
  return { code, out: rendered.markdown, err: err === '' ? '' : `${err}\n` };
1633
1777
  }
1634
- writeFileSync(values.out, rendered.markdown);
1778
+ writeOutput(values.out, rendered.markdown);
1635
1779
  return {
1636
1780
  code,
1637
1781
  out: `rendered ${rendered.citations} citation(s) to ${values.out}\n`,
1638
1782
  err: err === '' ? '' : `${err}\n`
1639
1783
  };
1640
- }
1641
- catch (error) {
1642
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1643
- }
1644
- finally {
1645
- store.close();
1646
- }
1784
+ });
1647
1785
  };
1648
1786
  /**
1649
1787
  * True when both paths name the same file: equal once resolved, or — when
@@ -1663,6 +1801,21 @@ const sameFile = (a, b) => {
1663
1801
  return false;
1664
1802
  }
1665
1803
  };
1804
+ /** Protect every physical file SQLite uses, including through a database symlink. */
1805
+ const sameStoreFile = (db, output) => {
1806
+ if (sameFile(db, output))
1807
+ return true;
1808
+ try {
1809
+ if (kindOf(db) !== 'sqlite')
1810
+ return false;
1811
+ const canonicalOutput = join(realpathSync(dirname(resolve(output))), basename(output));
1812
+ return [...new Set([db, realpathSync(db)])].some(base => ['-wal', '-shm', '-journal'].some(suffix => sameFile(`${base}${suffix}`, output) || sameFile(`${base}${suffix}`, canonicalOutput)));
1813
+ }
1814
+ catch {
1815
+ // The normal store-open path reports missing or unreadable source files.
1816
+ return false;
1817
+ }
1818
+ };
1666
1819
  export const exportCommand = (argv) => {
1667
1820
  const { values } = parseArgs({
1668
1821
  args: [...argv],
@@ -1681,28 +1834,22 @@ export const exportCommand = (argv) => {
1681
1834
  if (maximum === undefined) {
1682
1835
  return fail(`cave export: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`);
1683
1836
  }
1684
- if (values.out !== undefined && sameFile(db, values.out)) {
1837
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1685
1838
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1686
1839
  }
1687
1840
  const store = openDb(values, 'read');
1688
- try {
1841
+ return withCommandStore(store, () => {
1689
1842
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum });
1690
1843
  if (values.out === undefined) {
1691
1844
  return ok(text);
1692
1845
  }
1693
- writeFileSync(values.out, text);
1846
+ writeOutput(values.out, text);
1694
1847
  // Root claims only: shorthand prefixes and §28.4 annotations are not
1695
1848
  // claims, while qualifier/grouping leaves have a materialized parent.
1696
1849
  const claims = parseDocument(text).lines
1697
1850
  .filter(line => line.kind === 'claim' && line.parent === undefined).length;
1698
1851
  return ok(`exported ${claims} claim(s) to ${values.out}\n`);
1699
- }
1700
- catch (error) {
1701
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1702
- }
1703
- finally {
1704
- store.close();
1705
- }
1852
+ });
1706
1853
  };
1707
1854
  /** `cave generate` — spec §20.4's versioned TypeScript client artifact. */
1708
1855
  export const generateCommand = (argv) => {
@@ -1721,11 +1868,11 @@ export const generateCommand = (argv) => {
1721
1868
  return fail(`cave generate: --version expects a positive integer, got ${JSON.stringify(values.version)}\n`);
1722
1869
  }
1723
1870
  const db = values.db ?? defaultDbPath();
1724
- if (values.out !== undefined && sameFile(db, values.out)) {
1871
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1725
1872
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1726
1873
  }
1727
1874
  const store = openDb(values, 'read');
1728
- try {
1875
+ return withCommandStore(store, () => {
1729
1876
  const generated = generateClient(store, version === undefined ? {} : { version });
1730
1877
  if (!generated.ok) {
1731
1878
  return fail(`cave generate: schema cannot be generated:\n${generated.problems.map(problem => ` ${problem}`).join('\n')}\n`);
@@ -1733,16 +1880,10 @@ export const generateCommand = (argv) => {
1733
1880
  if (values.out === undefined) {
1734
1881
  return ok(generated.code);
1735
1882
  }
1736
- writeFileSync(values.out, generated.code);
1883
+ writeOutput(values.out, generated.code);
1737
1884
  return ok(`generated typed client v${generated.version} (${generated.fields.length} field(s), ` +
1738
1885
  `sha256:${generated.digest}) to ${values.out}\n`);
1739
- }
1740
- catch (error) {
1741
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1742
- }
1743
- finally {
1744
- store.close();
1745
- }
1886
+ });
1746
1887
  };
1747
1888
  /**
1748
1889
  * `cave reconstruct` — the §18 loop over the store: heuristic policy by
@@ -1788,7 +1929,7 @@ export const reconstructCommand = async (argv, runtime = {}) => {
1788
1929
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1789
1930
  }
1790
1931
  const store = openDb(values, 'read');
1791
- try {
1932
+ return await withAsyncCommandStore(store, async () => {
1792
1933
  const graph = sqliteStore(store);
1793
1934
  const result = values.agent === undefined ?
1794
1935
  reconstruct(graph, heuristicPolicy(budgets), positionals) :
@@ -1805,13 +1946,10 @@ export const reconstructCommand = async (argv, runtime = {}) => {
1805
1946
  ...result.claims.map(claim => emitClaim(claim))
1806
1947
  ];
1807
1948
  return ok(lines.length === 0 ? '' : `${lines.join('\n')}\n`);
1808
- }
1809
- finally {
1810
- store.close();
1811
- }
1949
+ });
1812
1950
  }
1813
1951
  catch (error) {
1814
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1952
+ return fail(`${errorMessage(error)}\n`);
1815
1953
  }
1816
1954
  };
1817
1955
  /**