@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/src/cli.ts CHANGED
@@ -35,14 +35,18 @@
35
35
  * `cave.db`. Every command answers `--help` with options and examples.
36
36
  */
37
37
 
38
- import { readFileSync, readSync, statSync, writeFileSync } from 'node:fs'
39
- import { resolve } from 'node:path'
38
+ import { readFileSync, readSync, realpathSync, statSync } from 'node:fs'
39
+ import { readHooks } from './hooks-config.ts'
40
+ import { basename, dirname, join, resolve } from 'node:path'
40
41
  import { parseArgs } from 'node:util'
41
- import { Version } from '@cavelang/core'
42
+ import { writeOutput } from './atomic-output.ts'
43
+ import { errorMessage } from './error-message.ts'
44
+ import { Time, Version } from '@cavelang/core'
42
45
  import { parseDocument, Token } from '@cavelang/parser'
43
46
  import { Registry, standardRegistry } from '@cavelang/canonical'
44
- import { LocateError, Sensitivity, backup as backupStore, defaultDbPath, openAt, restoreBackup, verifyBackup } from '@cavelang/store'
47
+ import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, QuerySql, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store'
45
48
  import type { OpenIntent, Store } from '@cavelang/store'
49
+ import { Declared, assemble as assembleSources } from '@cavelang/connect'
46
50
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query'
47
51
  import {
48
52
  check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
@@ -252,7 +256,7 @@ Examples:
252
256
  query: `cave query — run a CAVE-Q pattern against a store
253
257
 
254
258
  Usage:
255
- cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
259
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--sources] [--no-prelude]
256
260
 
257
261
  Options:
258
262
  ${dbHelp}
@@ -278,6 +282,12 @@ Options:
278
282
  collapse to the row the resolution policy picks
279
283
  (precedence class, reliability-weighted confidence, tx);
280
284
  incompatible with --all
285
+ --sources overlay the store's declared sources (spec §23.4) inside a
286
+ rolled-back transaction — federation-lite: external data is
287
+ consulted at query time, nothing persists. The answer is
288
+ whole (no --cursor, --limit is not a page size); a text
289
+ store followed its local sources on open, so only URL
290
+ sources are loaded here
281
291
  --no-prelude open the store without the standard verb registry
282
292
 
283
293
  Patterns are claim triples with ?variables and optional metadata filters
@@ -297,6 +307,7 @@ Examples:
297
307
  cave query --db k.db 'revenue IS' --at 2026-07
298
308
  cave query --db k.db 'alice WORKS-AT ?org' --at 2021
299
309
  cave query --db k.db 'service HAS owner: ?who' --resolve
310
+ cave query --db k.db '?who WORKS-AT acme' --sources
300
311
  cave query --db k.db '?x ?verb ?y @production' --json`,
301
312
 
302
313
  search: `cave search — full-text search over claims and comments (spec §13.2)
@@ -521,7 +532,7 @@ Usage:
521
532
 
522
533
  Options:
523
534
  ${dbHelp}
524
- --out <file> write the generated module instead of stdout
535
+ --out <file> atomically replace a file with the generated module
525
536
  --version <n> generated-client format version (default and currently
526
537
  supported: 1)
527
538
  --no-prelude open the store without the standard verb registry
@@ -545,7 +556,7 @@ Usage:
545
556
  Options:
546
557
  ${dbHelp}
547
558
  --min <s> minimum evidence score, 0..1 or N% (default 60%)
548
- --limit <n> at most n suggestions, strongest first
559
+ --limit <n> positive safe integer; at most n suggestions, strongest first
549
560
  --agent <template> LLM judge filtering the candidates — the cave
550
561
  ingest/eval shell contract: the prompt (each pair
551
562
  with both sides' current claims) is piped to stdin
@@ -576,6 +587,10 @@ claim, so the opt-in §13.6 closure links it until reviewed:
576
587
  claim identity, §9.2, so a plain @ 0% append would start a new series
577
588
  and leave the suggested link standing).
578
589
 
590
+ Writes recheck pair history under their transaction reservation and skip
591
+ reviewed pairs. --json returns the selected suggestion array; --json --write
592
+ performs the write and returns { suggestions, appended }, with the actual count.
593
+
579
594
  Examples:
580
595
  cave suggest-alias --db k.db
581
596
  cave suggest-alias --db k.db --min 80% --limit 10
@@ -633,7 +648,7 @@ Usage:
633
648
 
634
649
  Options:
635
650
  ${dbHelp}
636
- --out <file> write to a file instead of stdout — refused when it
651
+ --out <file> atomically replace a file instead of stdout — refused when it
637
652
  names the store's own database file
638
653
  --current current beliefs only (skip superseded rows); compacting
639
654
  view, not sanitization or selective erasure (spec §9.6)
@@ -649,6 +664,10 @@ Options:
649
664
  Retention: claim history is permanent. Retraction and --current do not
650
665
  guarantee erasure from the store, exports, peers, or backups (spec §9.6).
651
666
 
667
+ File output is staged and flushed before replacement. Existing permissions
668
+ and valid symlinks are preserved; hard-link aliases retain the previous file.
669
+ Dangling symlinks and non-regular output files are refused.
670
+
652
671
  Examples:
653
672
  cave export --db k.db
654
673
  cave export --db k.db --out internal-export.cave
@@ -668,7 +687,7 @@ Usage:
668
687
 
669
688
  Options:
670
689
  ${dbHelp}
671
- --out <file> write the rendered markdown to a file instead of stdout
690
+ --out <file> atomically replace a file with the rendered markdown
672
691
  --aliases queries match through the alias closure (spec §13.6)
673
692
  --resolve queries match resolved winners only (spec §26) — the fix
674
693
  when an inline splice reports an ambiguous fact
@@ -767,6 +786,15 @@ Examples:
767
786
  cave help serve`
768
787
  }
769
788
 
789
+ const decodeInput = (bytes: Uint8Array, source: string): string => {
790
+ try {
791
+ // Keep the existing BOM behavior of Buffer.toString while rejecting loss.
792
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
793
+ } catch (cause) {
794
+ throw new TypeError(`${source}: invalid UTF-8 input`, { cause })
795
+ }
796
+ }
797
+
770
798
  const readStdin = (): string => {
771
799
  const chunks: Buffer[] = []
772
800
  const buffer = Buffer.allocUnsafe(64 * 1024)
@@ -774,7 +802,7 @@ const readStdin = (): string => {
774
802
  for (;;) {
775
803
  try {
776
804
  const size = readSync(0, buffer, 0, buffer.length, null)
777
- if (size === 0) return Buffer.concat(chunks).toString('utf8')
805
+ if (size === 0) return decodeInput(Buffer.concat(chunks), 'stdin')
778
806
  chunks.push(Buffer.from(buffer.subarray(0, size)))
779
807
  } catch (error) {
780
808
  if ((error as NodeJS.ErrnoException).code !== 'EAGAIN') throw error
@@ -797,13 +825,75 @@ const readStdin = (): string => {
797
825
  const openDb = (values: { db?: string, 'no-prelude'?: boolean }, intent: OpenIntent): Store =>
798
826
  openAt(values.db ?? defaultDbPath(), {
799
827
  intent,
828
+ // A text store follows its declared sources on open (spec §23.4).
829
+ assemble: assembleSources,
800
830
  ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
801
831
  })
802
832
 
833
+ /** Retain command output and diagnostics when its owned store fails to close. */
834
+ const closeCommandStore = (store: Store, output: Output): Output => {
835
+ try { store.close() } catch (error) {
836
+ const separator = output.err === '' || output.err.endsWith('\n') ? '' : '\n'
837
+ return { ...output, code: output.code === 0 ? 1 : output.code,
838
+ err: `${output.err}${separator}store close failed: ${errorMessage(error)}\n` }
839
+ }
840
+ return output
841
+ }
842
+
843
+ const withCommandStore = (store: Store, run: () => Output): Output => {
844
+ let output: Output
845
+ try { output = run() } catch (error) {
846
+ output = fail(`${errorMessage(error)}\n`)
847
+ }
848
+ return closeCommandStore(store, output)
849
+ }
850
+
851
+ const withAsyncCommandStore = async (store: Store, run: () => Promise<Output>): Promise<Output> => {
852
+ let output: Output
853
+ try { output = await run() } catch (error) {
854
+ output = fail(`${errorMessage(error)}\n`)
855
+ }
856
+ return closeCommandStore(store, output)
857
+ }
858
+
859
+ const sourcesRollback = Symbol('cave query --sources rollback')
860
+
861
+ /**
862
+ * Runs `body` with the store's declared sources overlaid (spec §23.4): the
863
+ * sources, loaded beforehand (URLs included), append inside a transaction
864
+ * that always rolls back, so external data is consulted at query time and
865
+ * nothing persists, digests included.
866
+ */
867
+ const withSources = <T>(store: Store, discovered: Declared.Discovery, body: () => T): undefined | { value: T } => {
868
+ let result: undefined | { value: T }
869
+ try {
870
+ store.transaction(() => {
871
+ // Discovered on a snapshot: if a writer changed the declarations
872
+ // since, the sequence is stale and the caller rediscovers.
873
+ if (!Declared.sameDeclarations(discovered.baseline, Declared.declarationState(store))) {
874
+ throw sourcesRollback
875
+ }
876
+ for (const source of discovered.sequence) {
877
+ const report = Declared.run(store, source, { force: true })
878
+ if (report.failures.length > 0) {
879
+ throw new LocateError(`source/${source.declared.name}: ${report.failures.map(failure => `${failure.record}: ${failure.problems.join('; ')}`).join('; ')}`)
880
+ }
881
+ }
882
+ result = { value: body() }
883
+ throw sourcesRollback
884
+ })
885
+ } catch (error) {
886
+ if (error !== sourcesRollback) {
887
+ throw error
888
+ }
889
+ }
890
+ return result
891
+ }
892
+
803
893
  const readInput = (files: readonly string[]): string =>
804
894
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
805
895
  readStdin() :
806
- files.map(file => readFileSync(file, 'utf8')).join('\n')
896
+ files.map(file => decodeInput(readFileSync(file), file)).join('\n')
807
897
 
808
898
  export const parseCommand = (argv: readonly string[]): Output => {
809
899
  const { values, positionals } = parseArgs({
@@ -884,7 +974,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
884
974
  })
885
975
  const input = readInput(positionals)
886
976
  const store = openDb(values, 'write')
887
- try {
977
+ return withCommandStore(store, () => {
888
978
  const options = {
889
979
  strict: values.strict === true,
890
980
  ...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
@@ -906,11 +996,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
906
996
  out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
907
997
  err: problems === '' ? '' : `${problems}\n`
908
998
  }
909
- } catch (error) {
910
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
911
- } finally {
912
- store.close()
913
- }
999
+ })
914
1000
  }
915
1001
 
916
1002
  export const addCommand = ingestCommand('add')
@@ -929,39 +1015,142 @@ export const addCommand = ingestCommand('add')
929
1015
  */
930
1016
  export const importCommand = ingestCommand('import')
931
1017
 
932
- export const queryCommand = (argv: readonly string[]): Output => {
933
- const { values, positionals } = parseArgs({
934
- args: [...argv],
935
- options: {
936
- db: { type: 'string' },
937
- json: { type: 'boolean' },
938
- all: { type: 'boolean' },
939
- aliases: { type: 'boolean' },
940
- 'as-of': { type: 'string' },
941
- at: { type: 'string' },
942
- resolve: { type: 'boolean' },
943
- limit: { type: 'string' },
944
- cursor: { type: 'string' },
945
- 'no-prelude': { type: 'boolean' }
946
- },
947
- allowPositionals: true
948
- })
1018
+ const queryOptions = {
1019
+ db: { type: 'string' },
1020
+ json: { type: 'boolean' },
1021
+ all: { type: 'boolean' },
1022
+ aliases: { type: 'boolean' },
1023
+ 'as-of': { type: 'string' },
1024
+ at: { type: 'string' },
1025
+ resolve: { type: 'boolean' },
1026
+ sources: { type: 'boolean' },
1027
+ limit: { type: 'string' },
1028
+ cursor: { type: 'string' },
1029
+ 'no-prelude': { type: 'boolean' }
1030
+ } as const
1031
+
1032
+ type QueryValues = {
1033
+ db?: string
1034
+ json?: boolean
1035
+ all?: boolean
1036
+ aliases?: boolean
1037
+ 'as-of'?: string
1038
+ at?: string
1039
+ resolve?: boolean
1040
+ sources?: boolean
1041
+ limit?: string
1042
+ cursor?: string
1043
+ 'no-prelude'?: boolean
1044
+ }
1045
+
1046
+ const parseQuery = (argv: readonly string[]): { values: QueryValues, pattern: string } | { failure: Output } => {
1047
+ const { values, positionals } = parseArgs({ args: [...argv], options: queryOptions, allowPositionals: true })
949
1048
  if (positionals.length === 0) {
950
- return fail('cave query: a pattern is required (spec §12.1)\n')
1049
+ return { failure: fail('cave query: a pattern is required (spec §12.1)\n') }
1050
+ }
1051
+ if (values.sources === true && values.cursor !== undefined) {
1052
+ return { failure: fail('cave query: --cursor does not apply to --sources — the overlay exists for one invocation and is answered whole\n') }
1053
+ }
1054
+ return { values, pattern: positionals.join('\n') }
1055
+ }
1056
+
1057
+ /**
1058
+ * `cave query --sources` — the store with its declared sources overlaid
1059
+ * (spec §23.4). Asynchronous because the sources load first, URLs
1060
+ * included, before the overlay appends inside a transaction that rolls
1061
+ * back; the dispatcher routes `--sources` here. A text store followed its
1062
+ * local sources on open, so only what assembly could not follow — URL
1063
+ * sources and what they declare — is loaded. The overlay exists for this
1064
+ * invocation alone, so the answer is whole: pages are followed inside the
1065
+ * transaction and no cursor is handed out.
1066
+ */
1067
+ export const querySourcesCommand = async (
1068
+ argv: readonly string[],
1069
+ runtime: { readonly fetchImpl?: Declared.DiscoverOptions['fetchImpl'] } = {}
1070
+ ): Promise<Output> => {
1071
+ const parsed = parseQuery(argv)
1072
+ if ('failure' in parsed) {
1073
+ return parsed.failure
1074
+ }
1075
+ const { values, pattern } = parsed
1076
+ if (values.sources !== true) {
1077
+ return queryCommand(argv)
1078
+ }
1079
+ const root = values.db ?? defaultDbPath()
1080
+ // The overlay appends inside a transaction it rolls back: writable, but
1081
+ // never creating or migrating (spec §13.7).
1082
+ const store = openDb(values, 'scratch')
1083
+ return await withAsyncCommandStore(store, async () => {
1084
+ for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
1085
+ const discovered = await Declared.discovery(store, root, {
1086
+ // The overlay applies every source it loads (force), so discovery
1087
+ // simulates that; a text store skips what assembly already followed.
1088
+ force: true,
1089
+ ...kindOf(root) === 'text' ? { skipFollowed: true } : {},
1090
+ ...runtime.fetchImpl === undefined ? {} : { fetchImpl: runtime.fetchImpl }
1091
+ })
1092
+ const page = withSources(store, discovered, () => wholeQuery(store, values, pattern))
1093
+ if (page !== undefined) {
1094
+ return renderQueryPage(store, values, pattern, () => page.value)
1095
+ }
1096
+ }
1097
+ throw Declared.staleOverlay()
1098
+ })
1099
+ }
1100
+
1101
+ export const queryCommand = (argv: readonly string[]): Output => {
1102
+ const parsed = parseQuery(argv)
1103
+ if ('failure' in parsed) {
1104
+ return parsed.failure
1105
+ }
1106
+ const { values, pattern } = parsed
1107
+ if (values.sources === true) {
1108
+ // Loading sources cannot be synchronous: the in-process entry for the
1109
+ // overlay is `querySourcesCommand`, which the dispatcher uses.
1110
+ return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n')
951
1111
  }
952
- const pattern = positionals.join('\n')
953
1112
  const store = openDb(values, 'read')
954
- try {
955
- const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
956
- const result = caveQueryPage(store, pattern, {
957
- all: values.all === true,
958
- aliases: values.aliases === true,
959
- resolve: values.resolve === true,
960
- limit,
961
- ...values.cursor === undefined ? {} : { cursor: values.cursor },
962
- ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
963
- ...values.at === undefined ? {} : { at: values.at }
964
- })
1113
+ return withCommandStore(store, () => {
1114
+ return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern))
1115
+ })
1116
+ }
1117
+
1118
+ /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
1119
+ const wholeQuery = (store: Store, values: QueryValues, pattern: string): ReturnType<typeof caveQueryPage> => {
1120
+ const matches: ReturnType<typeof caveQueryPage>['matches'][number][] = []
1121
+ let first: undefined | ReturnType<typeof caveQueryPage>
1122
+ let cursor: undefined | string
1123
+ for (;;) {
1124
+ const page = queryPage(store, { ...values, limit: '1000', ...cursor === undefined ? {} : { cursor } }, pattern)
1125
+ first ??= page
1126
+ matches.push(...page.matches)
1127
+ if (page.next === undefined) {
1128
+ const { next: _next, ...rest } = first
1129
+ return { ...rest, matches }
1130
+ }
1131
+ cursor = page.next
1132
+ }
1133
+ }
1134
+
1135
+ const queryPage = (store: Store, values: QueryValues, pattern: string): ReturnType<typeof caveQueryPage> =>
1136
+ caveQueryPage(store, pattern, {
1137
+ all: values.all === true,
1138
+ aliases: values.aliases === true,
1139
+ resolve: values.resolve === true,
1140
+ limit: values.limit === undefined ? defaultQueryLimit : Number(values.limit),
1141
+ ...values.cursor === undefined ? {} : { cursor: values.cursor },
1142
+ ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
1143
+ ...values.at === undefined ? {} : { at: values.at }
1144
+ })
1145
+
1146
+ const renderQueryPage = (
1147
+ store: Store,
1148
+ values: QueryValues,
1149
+ pattern: string,
1150
+ page: () => ReturnType<typeof caveQueryPage>
1151
+ ): Output => {
1152
+ {
1153
+ const result = page()
965
1154
  if (values.json === true) {
966
1155
  return ok(`${JSON.stringify(result, undefined, 2)}\n`)
967
1156
  }
@@ -990,10 +1179,6 @@ export const queryCommand = (argv: readonly string[]): Output => {
990
1179
  line
991
1180
  })
992
1181
  return ok(`${lines.join('\n')}\n${result.next === undefined ? '' : `next: ${result.next}\n`}`)
993
- } catch (error) {
994
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
995
- } finally {
996
- store.close()
997
1182
  }
998
1183
  }
999
1184
 
@@ -1026,7 +1211,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1026
1211
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`)
1027
1212
  }
1028
1213
  const store = openDb(values, 'read')
1029
- try {
1214
+ return withCommandStore(store, () => {
1030
1215
  const raw = values.raw === true
1031
1216
  const found = store.search(query, { raw, limit: limit + 1 })
1032
1217
  const matches = found.slice(0, limit)
@@ -1050,11 +1235,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1050
1235
  lines.push(`more matches beyond ${limit}; raise --limit`)
1051
1236
  }
1052
1237
  return ok(`${lines.join('\n')}\n`)
1053
- } catch (error) {
1054
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1055
- } finally {
1056
- store.close()
1057
- }
1238
+ })
1058
1239
  }
1059
1240
 
1060
1241
  /**
@@ -1074,15 +1255,20 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1074
1255
  },
1075
1256
  allowPositionals: false
1076
1257
  })
1077
- const percent = (value: number): string => `${Math.round(value * 1000) / 10}%`
1258
+ const percent = (value: number): string => {
1259
+ const rounded = Math.round(value * 1000) / 10
1260
+ if (value > 0 && rounded === 0) return '<0.1%'
1261
+ if (value < 1 && rounded === 100) return '>99.9%'
1262
+ return `${rounded}%`
1263
+ }
1078
1264
  const store = openDb(values, 'read')
1079
- try {
1265
+ return withCommandStore(store, () => {
1080
1266
  if (values.policy === true) {
1081
1267
  const policy = store.resolutionPolicy()
1082
1268
  if (values.json === true) {
1083
1269
  return ok(`${JSON.stringify(policy, undefined, 2)}\n`)
1084
1270
  }
1085
- const width = Math.max(...policy.map(entry => entry.prefix.length + 7))
1271
+ const width = policy.reduce((maximum, entry) => Math.max(maximum, entry.prefix.length + 7), 0)
1086
1272
  const lines = policy.map(entry => {
1087
1273
  const subject = entry.prefix === '' ? 'source' : `source/${entry.prefix}`
1088
1274
  const dimensions = [
@@ -1104,11 +1290,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1104
1290
  group.rows.map((row, at) =>
1105
1291
  `${at === 0 ? '' : ' over '}${row.raw_line} ; class ${row.res_class}, effective ${percent(row.res_conf)}`))
1106
1292
  return ok(`${lines.join('\n')}\n`)
1107
- } catch (error) {
1108
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1109
- } finally {
1110
- store.close()
1111
- }
1293
+ })
1112
1294
  }
1113
1295
 
1114
1296
  export const deriveCommand = (argv: readonly string[]): Output => {
@@ -1135,8 +1317,8 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1135
1317
  return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`)
1136
1318
  }
1137
1319
  const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes'])
1138
- if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1139
- return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
1320
+ if (maxPasses !== undefined && (!Number.isSafeInteger(maxPasses) || maxPasses < 1)) {
1321
+ return fail(`cave derive: --max-passes expects a positive safe integer, got ${JSON.stringify(values['max-passes'])}\n`)
1140
1322
  }
1141
1323
  // The intent follows the branch order below: listing reads, retracting
1142
1324
  // writes, a dry run appends inside a rolled-back transaction.
@@ -1144,7 +1326,7 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1144
1326
  values.list === true ? 'read' :
1145
1327
  values.retract !== undefined ? 'write' :
1146
1328
  values['dry-run'] === true ? 'scratch' : 'write')
1147
- try {
1329
+ return withCommandStore(store, () => {
1148
1330
  if (values.list === true) {
1149
1331
  const rules = listRules(store)
1150
1332
  if (values.json === true) {
@@ -1204,13 +1386,15 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1204
1386
  report = derive(store, options)
1205
1387
  }
1206
1388
  if (declaration !== undefined) {
1207
- err.push(...declaration.problems.map(problem => `rules line ${problem.line}: ${problem.message}`))
1389
+ for (const problem of declaration.problems) err.push(`rules line ${problem.line}: ${problem.message}`)
1208
1390
  out.push(`declared ${declaration.declared} rule(s)` +
1209
1391
  (declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
1210
1392
  (declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''))
1211
1393
  }
1394
+ const hasProblems = (declaration?.problems.length ?? 0) > 0 || !report.complete ||
1395
+ report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
1212
1396
  if (values.json === true) {
1213
- return { code: report.problems.length > 0 || !report.complete ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
1397
+ return { code: hasProblems ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
1214
1398
  }
1215
1399
  for (const problem of report.problems) {
1216
1400
  err.push(`${problem.subject}: ${problem.problems.join('; ')}`)
@@ -1220,33 +1404,15 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1220
1404
  `${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
1221
1405
  'unchanged premises, skipped'
1222
1406
  out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`)
1223
- err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`))
1407
+ for (const problem of rule.problems) err.push(`${rule.subject}: ${problem}`)
1224
1408
  }
1225
- out.push(...report.notes.map(note => `note: ${note}`))
1409
+ for (const note of report.notes) out.push(`note: ${note}`)
1226
1410
  out.push(
1227
- `derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1411
+ `derived${report.complete ? '' : ' (incomplete)'}${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1228
1412
  `+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
1229
1413
  `${report.unchanged} unchanged (${report.passes} pass(es))`)
1230
- const hasProblems = !report.complete || report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
1231
1414
  return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` }
1232
- } catch (error) {
1233
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1234
- } finally {
1235
- store.close()
1236
- }
1237
- }
1238
-
1239
- /**
1240
- * Loads a hooks configuration file (spec §25.4): a JSON object mapping
1241
- * hook names to shell command templates.
1242
- */
1243
- const readHooks = (path: string): Record<string, string> => {
1244
- const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
1245
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
1246
- Object.values(parsed).some(value => typeof value !== 'string')) {
1247
- throw new Error(`${path}: hooks must be a JSON object of name → shell template strings`)
1248
- }
1249
- return parsed as Record<string, string>
1415
+ })
1250
1416
  }
1251
1417
 
1252
1418
  /** Text rendering of a spec §25.2 execution report. */
@@ -1266,15 +1432,14 @@ const renderActReport = (report: ActReport): { lines: string[], code: number } =
1266
1432
  ]
1267
1433
  let code = 0
1268
1434
  if (report.hook !== undefined) {
1269
- if (!report.hook.fired) {
1270
- lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`)
1271
- } else if (report.hook.error === undefined) {
1272
- lines.push(`hook ${report.hook.name}: ok`)
1273
- } else {
1274
- // The claims committed before the hook ran (spec §25.4) — the
1275
- // failure is reported, and the exit code carries it.
1435
+ if (report.hook.error !== undefined) {
1436
+ // Effects may already be committed even when hook lookup failed before spawn.
1276
1437
  lines.push(`hook ${report.hook.name}: ${report.hook.error}`)
1277
1438
  code = 1
1439
+ } else if (!report.hook.fired) {
1440
+ lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`)
1441
+ } else {
1442
+ lines.push(`hook ${report.hook.name}: ok`)
1278
1443
  }
1279
1444
  }
1280
1445
  return { lines, code }
@@ -1305,7 +1470,7 @@ export const actCommand = (argv: readonly string[]): Output => {
1305
1470
  values.list === true ? 'read' :
1306
1471
  values.retract !== undefined ? 'write' :
1307
1472
  values['dry-run'] === true ? 'scratch' : 'write')
1308
- try {
1473
+ return withCommandStore(store, () => {
1309
1474
  if (values.declare === true) {
1310
1475
  const declaration = declareActions(store, readInput(positionals))
1311
1476
  const err = declaration.problems.map(problem => `line ${problem.line}: ${problem.message}`)
@@ -1352,7 +1517,13 @@ export const actCommand = (argv: readonly string[]): Output => {
1352
1517
  if (at <= 0) {
1353
1518
  return fail(`cave act: expected param=value, got ${JSON.stringify(pair)}\n`)
1354
1519
  }
1355
- args[pair.slice(0, at)] = pair.slice(at + 1)
1520
+ const parameter = pair.slice(0, at)
1521
+ if (Object.hasOwn(args, parameter)) {
1522
+ return fail(`cave act: duplicate parameter ${JSON.stringify(parameter)}\n`)
1523
+ }
1524
+ Object.defineProperty(args, parameter, {
1525
+ value: pair.slice(at + 1), enumerable: true, writable: true, configurable: true
1526
+ })
1356
1527
  }
1357
1528
  const hooksPath = values.hooks ?? process.env['CAVE_HOOKS']
1358
1529
  const report = act(store, name, args, {
@@ -1368,11 +1539,7 @@ export const actCommand = (argv: readonly string[]): Output => {
1368
1539
  return report.ok ?
1369
1540
  { code, out: `${lines.join('\n')}\n`, err: '' } :
1370
1541
  { code, out: '', err: `${lines.join('\n')}\n` }
1371
- } catch (error) {
1372
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1373
- } finally {
1374
- store.close()
1375
- }
1542
+ })
1376
1543
  }
1377
1544
 
1378
1545
  /** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
@@ -1382,26 +1549,32 @@ const renderReport = (report: Report, staleDays: number): string => {
1382
1549
  lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`)
1383
1550
  if (report.violations.length > 0) {
1384
1551
  lines.push(`violations (${report.violations.length}):`)
1385
- lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`))
1552
+ for (const violation of report.violations) lines.push(` ${formatViolation(violation)}`)
1386
1553
  }
1387
1554
  if (report.stale.length > 0) {
1388
1555
  lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`)
1389
- lines.push(...report.stale.map(({ row, ageDays }) => ` ${row.raw_line} (${ageDays}d)`))
1556
+ for (const { row, ageDays } of report.stale) lines.push(` ${row.raw_line} (${ageDays}d)`)
1390
1557
  }
1391
1558
  if (report.review.length > 0) {
1392
1559
  lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`)
1393
- lines.push(...report.review.map(row => ` ${row.raw_line}`))
1560
+ for (const row of report.review) lines.push(` ${row.raw_line}`)
1394
1561
  }
1395
1562
  if (report.disagreements.length > 0) {
1396
1563
  lines.push(`alias disagreements (${report.disagreements.length}):`)
1397
1564
  for (const disagreement of report.disagreements) {
1398
1565
  lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`)
1399
- lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`))
1566
+ for (const row of disagreement.rows) lines.push(` ${row.raw_line}`)
1400
1567
  }
1401
1568
  }
1569
+ const confidencePercent = (value: number): string => {
1570
+ const rounded = Math.round(value * 100)
1571
+ if (value > 0 && rounded === 0) return '<1%'
1572
+ if (value < 1 && rounded === 100) return '>99%'
1573
+ return `${rounded}%`
1574
+ }
1402
1575
  const confidence = coverage.averageConfidence === null ?
1403
1576
  '' :
1404
- `; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`
1577
+ `; avg conf ${confidencePercent(coverage.averageConfidence)}, ${coverage.lowConfidence} low (< 0.3)`
1405
1578
  lines.push(
1406
1579
  `coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
1407
1580
  `${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
@@ -1426,16 +1599,14 @@ export const checkCommand = (argv: readonly string[]): Output => {
1426
1599
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
1427
1600
  }
1428
1601
  const store = openDb(values, 'read')
1429
- try {
1602
+ return withCommandStore(store, () => {
1430
1603
  const report = caveCheck(store, { staleDays })
1431
1604
  const out = values.json === true ?
1432
1605
  `${stableJson(store, report)}\n` :
1433
1606
  renderReport(report, staleDays)
1434
1607
  // Violations fail the check (spec §20.2); the other sections are advisory.
1435
1608
  return { code: report.violations.length > 0 ? 1 : 0, out, err: '' }
1436
- } finally {
1437
- store.close()
1438
- }
1609
+ })
1439
1610
  }
1440
1611
 
1441
1612
  const snapshotLine = (
@@ -1479,17 +1650,16 @@ export const backupCommand = (argv: readonly string[]): Output => {
1479
1650
  // the destination aliases the source: refuse before the file could be
1480
1651
  // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1481
1652
  // the API's own check).
1482
- if (sameFile(dbPath, values.out)) {
1653
+ if (sameStoreFile(dbPath, values.out)) {
1483
1654
  return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1484
1655
  }
1485
- const store = openAt(dbPath, { intent: 'read' })
1486
- try {
1487
- return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1488
- } finally {
1489
- store.close()
1490
- }
1656
+ const destination = values.out
1657
+ const store = openAt(dbPath, { intent: 'read', assemble: assembleSources })
1658
+ return withCommandStore(store, () => {
1659
+ return ok(snapshotLine('created', backupStore(store, destination, { force: values.force === true })))
1660
+ })
1491
1661
  } catch (error) {
1492
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1662
+ return fail(`${errorMessage(error)}\n`)
1493
1663
  }
1494
1664
  }
1495
1665
 
@@ -1519,7 +1689,7 @@ export const restoreCommand = (argv: readonly string[]): Output => {
1519
1689
  ...values.sha256 === undefined ? {} : { expectedSha256: values.sha256 }
1520
1690
  })))
1521
1691
  } catch (error) {
1522
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1692
+ return fail(`${errorMessage(error)}\n`)
1523
1693
  }
1524
1694
  }
1525
1695
 
@@ -1558,15 +1728,15 @@ export const suggestAliasCommand = async (
1558
1728
  return fail(`cave suggest-alias: --min expects a score in 0..1 or N%, got ${JSON.stringify(values.min)}\n`)
1559
1729
  }
1560
1730
  const limit = values.limit === undefined ? undefined : Number(values.limit)
1561
- if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
1562
- return fail(`cave suggest-alias: --limit expects a positive integer, got ${JSON.stringify(values.limit)}\n`)
1731
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
1732
+ return fail(`cave suggest-alias: --limit expects a positive safe integer, got ${JSON.stringify(values.limit)}\n`)
1563
1733
  }
1564
1734
  const timeoutSeconds = values.timeout === undefined ? undefined : Number(values.timeout)
1565
1735
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1566
1736
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1567
1737
  }
1568
1738
  const store = openDb(values, values.write === true ? 'write' : 'read')
1569
- try {
1739
+ return await withAsyncCommandStore(store, async () => {
1570
1740
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1571
1741
  if (values.agent !== undefined && suggestions.length > 0) {
1572
1742
  const reply = await shellComplete(values.agent, {
@@ -1578,7 +1748,9 @@ export const suggestAliasCommand = async (
1578
1748
  suggestions = parseJudgeReply(reply, suggestions.length).map(index => suggestions[index]!)
1579
1749
  }
1580
1750
  if (values.json === true) {
1581
- return ok(`${JSON.stringify(suggestions, undefined, 2)}\n`)
1751
+ const result = values.write === true ?
1752
+ { suggestions, ...writeSuggestions(store, suggestions) } : suggestions
1753
+ return ok(`${JSON.stringify(result, undefined, 2)}\n`)
1582
1754
  }
1583
1755
  if (suggestions.length === 0) {
1584
1756
  return ok('no alias suggestions\n')
@@ -1589,11 +1761,9 @@ export const suggestAliasCommand = async (
1589
1761
  return ok(`${lines.join('\n')}\nappended ${appended} suggested alias claim(s)\n`)
1590
1762
  }
1591
1763
  return ok(`${lines.join('\n')}\n`)
1592
- } finally {
1593
- store.close()
1594
- }
1764
+ })
1595
1765
  } catch (error) {
1596
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1766
+ return fail(`${errorMessage(error)}\n`)
1597
1767
  }
1598
1768
  }
1599
1769
 
@@ -1623,7 +1793,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1623
1793
  }
1624
1794
  const dbPath = values.db ?? defaultDbPath()
1625
1795
  const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write')
1626
- try {
1796
+ return withCommandStore(store, () => {
1627
1797
  const options = {
1628
1798
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
1629
1799
  record: values['no-record'] !== true,
@@ -1637,7 +1807,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1637
1807
  return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
1638
1808
  }
1639
1809
  if (report.problems.length > 0) {
1640
- const detail = report.problems.map(problem => ` line ${problem.line}: ${problem.message}`).join('\n')
1810
+ const detail = report.problems.map(problem => ` ${problem.line === 0 ? '' : `line ${problem.line}: `}${problem.message}`).join('\n')
1641
1811
  return fail(`cave sync: ${source}: ${report.problems.length} problem(s), nothing merged\n${detail}\n`)
1642
1812
  }
1643
1813
  const lines = [
@@ -1649,17 +1819,14 @@ export const syncCommand = (argv: readonly string[]): Output => {
1649
1819
  lines.push(`record: ${report.record}`)
1650
1820
  }
1651
1821
  return ok(`${lines.join('\n')}\n`)
1652
- } catch (error) {
1653
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1654
- } finally {
1655
- store.close()
1656
- }
1822
+ })
1657
1823
  }
1658
1824
 
1659
1825
  /**
1660
1826
  * `cave report` — the §31 deliverable: a markdown template's cave-q
1661
- * blocks and inline splices render from the store, every stated fact
1662
- * cited back to its claim. Problems mark the text, land on stderr with
1827
+ * blocks and inline splices render from the store, with matched rows
1828
+ * cited back to their claims. Ordinary prose is not automatically checked
1829
+ * or cited. Problems mark the text, land on stderr with
1663
1830
  * template line numbers, and fail the exit code — the render never
1664
1831
  * silently drops a fact.
1665
1832
  */
@@ -1678,16 +1845,22 @@ export const reportCommand = (argv: readonly string[]): Output => {
1678
1845
  },
1679
1846
  allowPositionals: true
1680
1847
  })
1681
- const template = readInput(positionals)
1848
+ if (values.at !== undefined && Time.parseInstant(values.at) === undefined) {
1849
+ return fail(`cave report: --at cannot parse at anchor ${JSON.stringify(values.at)}\n`)
1850
+ }
1851
+ if (values['as-of'] !== undefined && QuerySql.asOfBoundary(values['as-of']) === undefined) {
1852
+ return fail(`cave report: --as-of cannot parse as-of boundary ${JSON.stringify(values['as-of'])}\n`)
1853
+ }
1682
1854
  const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum)
1683
1855
  if (maximum === undefined) {
1684
1856
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1685
1857
  }
1686
- if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1858
+ if (values.out !== undefined && sameStoreFile(values.db ?? defaultDbPath(), values.out)) {
1687
1859
  return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1688
1860
  }
1861
+ const template = readInput(positionals)
1689
1862
  const store = openDb(values, 'read')
1690
- try {
1863
+ return withCommandStore(store, () => {
1691
1864
  const rendered = caveReport(store, template, {
1692
1865
  aliases: values.aliases === true,
1693
1866
  resolve: values.resolve === true,
@@ -1702,17 +1875,13 @@ export const reportCommand = (argv: readonly string[]): Output => {
1702
1875
  if (values.out === undefined) {
1703
1876
  return { code, out: rendered.markdown, err: err === '' ? '' : `${err}\n` }
1704
1877
  }
1705
- writeFileSync(values.out, rendered.markdown)
1878
+ writeOutput(values.out, rendered.markdown)
1706
1879
  return {
1707
1880
  code,
1708
1881
  out: `rendered ${rendered.citations} citation(s) to ${values.out}\n`,
1709
1882
  err: err === '' ? '' : `${err}\n`
1710
1883
  }
1711
- } catch (error) {
1712
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1713
- } finally {
1714
- store.close()
1715
- }
1884
+ })
1716
1885
  }
1717
1886
 
1718
1887
  /**
@@ -1733,6 +1902,21 @@ const sameFile = (a: string, b: string): boolean => {
1733
1902
  }
1734
1903
  }
1735
1904
 
1905
+ /** Protect every physical file SQLite uses, including through a database symlink. */
1906
+ const sameStoreFile = (db: string, output: string): boolean => {
1907
+ if (sameFile(db, output)) return true
1908
+ try {
1909
+ if (kindOf(db) !== 'sqlite') return false
1910
+ const canonicalOutput = join(realpathSync(dirname(resolve(output))), basename(output))
1911
+ return [...new Set([db, realpathSync(db)])].some(base =>
1912
+ ['-wal', '-shm', '-journal'].some(suffix =>
1913
+ sameFile(`${base}${suffix}`, output) || sameFile(`${base}${suffix}`, canonicalOutput)))
1914
+ } catch {
1915
+ // The normal store-open path reports missing or unreadable source files.
1916
+ return false
1917
+ }
1918
+ }
1919
+
1736
1920
  export const exportCommand = (argv: readonly string[]): Output => {
1737
1921
  const { values } = parseArgs({
1738
1922
  args: [...argv],
@@ -1751,26 +1935,22 @@ export const exportCommand = (argv: readonly string[]): Output => {
1751
1935
  if (maximum === undefined) {
1752
1936
  return fail(`cave export: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1753
1937
  }
1754
- if (values.out !== undefined && sameFile(db, values.out)) {
1938
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1755
1939
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1756
1940
  }
1757
1941
  const store = openDb(values, 'read')
1758
- try {
1942
+ return withCommandStore(store, () => {
1759
1943
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1760
1944
  if (values.out === undefined) {
1761
1945
  return ok(text)
1762
1946
  }
1763
- writeFileSync(values.out, text)
1947
+ writeOutput(values.out, text)
1764
1948
  // Root claims only: shorthand prefixes and §28.4 annotations are not
1765
1949
  // claims, while qualifier/grouping leaves have a materialized parent.
1766
1950
  const claims = parseDocument(text).lines
1767
1951
  .filter(line => line.kind === 'claim' && line.parent === undefined).length
1768
1952
  return ok(`exported ${claims} claim(s) to ${values.out}\n`)
1769
- } catch (error) {
1770
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1771
- } finally {
1772
- store.close()
1773
- }
1953
+ })
1774
1954
  }
1775
1955
 
1776
1956
  /** `cave generate` — spec §20.4's versioned TypeScript client artifact. */
@@ -1790,11 +1970,11 @@ export const generateCommand = (argv: readonly string[]): Output => {
1790
1970
  return fail(`cave generate: --version expects a positive integer, got ${JSON.stringify(values.version)}\n`)
1791
1971
  }
1792
1972
  const db = values.db ?? defaultDbPath()
1793
- if (values.out !== undefined && sameFile(db, values.out)) {
1973
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1794
1974
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1795
1975
  }
1796
1976
  const store = openDb(values, 'read')
1797
- try {
1977
+ return withCommandStore(store, () => {
1798
1978
  const generated = generateClient(store, version === undefined ? {} : { version })
1799
1979
  if (!generated.ok) {
1800
1980
  return fail(`cave generate: schema cannot be generated:\n${generated.problems.map(problem => ` ${problem}`).join('\n')}\n`)
@@ -1802,14 +1982,10 @@ export const generateCommand = (argv: readonly string[]): Output => {
1802
1982
  if (values.out === undefined) {
1803
1983
  return ok(generated.code)
1804
1984
  }
1805
- writeFileSync(values.out, generated.code)
1985
+ writeOutput(values.out, generated.code)
1806
1986
  return ok(`generated typed client v${generated.version} (${generated.fields.length} field(s), ` +
1807
1987
  `sha256:${generated.digest}) to ${values.out}\n`)
1808
- } catch (error) {
1809
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1810
- } finally {
1811
- store.close()
1812
- }
1988
+ })
1813
1989
  }
1814
1990
 
1815
1991
  /**
@@ -1859,7 +2035,7 @@ export const reconstructCommand = async (
1859
2035
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1860
2036
  }
1861
2037
  const store = openDb(values, 'read')
1862
- try {
2038
+ return await withAsyncCommandStore(store, async () => {
1863
2039
  const graph = sqliteStore(store)
1864
2040
  const result = values.agent === undefined ?
1865
2041
  reconstruct(graph, heuristicPolicy(budgets), positionals) :
@@ -1883,11 +2059,9 @@ export const reconstructCommand = async (
1883
2059
  ...result.claims.map(claim => emitClaim(claim))
1884
2060
  ]
1885
2061
  return ok(lines.length === 0 ? '' : `${lines.join('\n')}\n`)
1886
- } finally {
1887
- store.close()
1888
- }
2062
+ })
1889
2063
  } catch (error) {
1890
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
2064
+ return fail(`${errorMessage(error)}\n`)
1891
2065
  }
1892
2066
  }
1893
2067