@cavelang/cli 0.35.0 → 0.36.1

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 (327) hide show
  1. package/README.md +224 -12
  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 +238 -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 +6 -27
  34. package/dist/internal/connect/declared.d.ts.map +1 -1
  35. package/dist/internal/connect/declared.js +165 -76
  36. package/dist/internal/connect/declared.js.map +1 -1
  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/main.d.ts +1 -0
  50. package/dist/internal/connect/main.d.ts.map +1 -1
  51. package/dist/internal/connect/main.js +288 -114
  52. package/dist/internal/connect/main.js.map +1 -1
  53. package/dist/internal/connect/options.d.ts +3 -0
  54. package/dist/internal/connect/options.d.ts.map +1 -0
  55. package/dist/internal/connect/options.js +9 -0
  56. package/dist/internal/connect/options.js.map +1 -0
  57. package/dist/internal/connect/records.d.ts +3 -0
  58. package/dist/internal/connect/records.d.ts.map +1 -0
  59. package/dist/internal/connect/records.js +21 -0
  60. package/dist/internal/connect/records.js.map +1 -0
  61. package/dist/internal/connect/run.d.ts +1 -18
  62. package/dist/internal/connect/run.d.ts.map +1 -1
  63. package/dist/internal/connect/run.js +115 -55
  64. package/dist/internal/connect/run.js.map +1 -1
  65. package/dist/internal/connect/source.d.ts +3 -1
  66. package/dist/internal/connect/source.d.ts.map +1 -1
  67. package/dist/internal/connect/source.js +226 -47
  68. package/dist/internal/connect/source.js.map +1 -1
  69. package/dist/internal/connect/template.d.ts +2 -2
  70. package/dist/internal/connect/template.d.ts.map +1 -1
  71. package/dist/internal/connect/template.js +25 -9
  72. package/dist/internal/connect/template.js.map +1 -1
  73. package/dist/internal/connect/utf8.d.ts +3 -0
  74. package/dist/internal/connect/utf8.d.ts.map +1 -0
  75. package/dist/internal/connect/utf8.js +12 -0
  76. package/dist/internal/connect/utf8.js.map +1 -0
  77. package/dist/internal/eval/cleanup.d.ts +5 -0
  78. package/dist/internal/eval/cleanup.d.ts.map +1 -0
  79. package/dist/internal/eval/cleanup.js +50 -0
  80. package/dist/internal/eval/cleanup.js.map +1 -0
  81. package/dist/internal/eval/error-message.d.ts +3 -0
  82. package/dist/internal/eval/error-message.d.ts.map +1 -0
  83. package/dist/internal/eval/error-message.js +10 -0
  84. package/dist/internal/eval/error-message.js.map +1 -0
  85. package/dist/internal/eval/judge.d.ts.map +1 -1
  86. package/dist/internal/eval/judge.js +3 -31
  87. package/dist/internal/eval/judge.js.map +1 -1
  88. package/dist/internal/eval/loop.d.ts.map +1 -1
  89. package/dist/internal/eval/loop.js +18 -4
  90. package/dist/internal/eval/loop.js.map +1 -1
  91. package/dist/internal/eval/main.d.ts.map +1 -1
  92. package/dist/internal/eval/main.js +22 -10
  93. package/dist/internal/eval/main.js.map +1 -1
  94. package/dist/internal/eval/options.d.ts +6 -0
  95. package/dist/internal/eval/options.d.ts.map +1 -0
  96. package/dist/internal/eval/options.js +31 -0
  97. package/dist/internal/eval/options.js.map +1 -0
  98. package/dist/internal/eval/queries.d.ts.map +1 -1
  99. package/dist/internal/eval/queries.js +9 -6
  100. package/dist/internal/eval/queries.js.map +1 -1
  101. package/dist/internal/eval/run.d.ts.map +1 -1
  102. package/dist/internal/eval/run.js +112 -24
  103. package/dist/internal/eval/run.js.map +1 -1
  104. package/dist/internal/eval/score.d.ts.map +1 -1
  105. package/dist/internal/eval/score.js +9 -2
  106. package/dist/internal/eval/score.js.map +1 -1
  107. package/dist/internal/eval/suite.d.ts.map +1 -1
  108. package/dist/internal/eval/suite.js +14 -2
  109. package/dist/internal/eval/suite.js.map +1 -1
  110. package/dist/internal/ingest/cleanup.d.ts +5 -0
  111. package/dist/internal/ingest/cleanup.d.ts.map +1 -0
  112. package/dist/internal/ingest/cleanup.js +50 -0
  113. package/dist/internal/ingest/cleanup.js.map +1 -0
  114. package/dist/internal/ingest/content.d.ts +6 -0
  115. package/dist/internal/ingest/content.d.ts.map +1 -0
  116. package/dist/internal/ingest/content.js +25 -0
  117. package/dist/internal/ingest/content.js.map +1 -0
  118. package/dist/internal/ingest/context.d.ts.map +1 -1
  119. package/dist/internal/ingest/context.js +25 -7
  120. package/dist/internal/ingest/context.js.map +1 -1
  121. package/dist/internal/ingest/error-message.d.ts +3 -0
  122. package/dist/internal/ingest/error-message.d.ts.map +1 -0
  123. package/dist/internal/ingest/error-message.js +10 -0
  124. package/dist/internal/ingest/error-message.js.map +1 -0
  125. package/dist/internal/ingest/files.d.ts +4 -12
  126. package/dist/internal/ingest/files.d.ts.map +1 -1
  127. package/dist/internal/ingest/files.js +22 -10
  128. package/dist/internal/ingest/files.js.map +1 -1
  129. package/dist/internal/ingest/main.d.ts.map +1 -1
  130. package/dist/internal/ingest/main.js +46 -16
  131. package/dist/internal/ingest/main.js.map +1 -1
  132. package/dist/internal/ingest/options.d.ts +6 -0
  133. package/dist/internal/ingest/options.d.ts.map +1 -0
  134. package/dist/internal/ingest/options.js +27 -0
  135. package/dist/internal/ingest/options.js.map +1 -0
  136. package/dist/internal/ingest/prompt.d.ts.map +1 -1
  137. package/dist/internal/ingest/prompt.js +14 -10
  138. package/dist/internal/ingest/prompt.js.map +1 -1
  139. package/dist/internal/ingest/run.d.ts +9 -2
  140. package/dist/internal/ingest/run.d.ts.map +1 -1
  141. package/dist/internal/ingest/run.js +206 -62
  142. package/dist/internal/ingest/run.js.map +1 -1
  143. package/dist/internal/ingest/timeout.d.ts +3 -0
  144. package/dist/internal/ingest/timeout.d.ts.map +1 -0
  145. package/dist/internal/ingest/timeout.js +14 -0
  146. package/dist/internal/ingest/timeout.js.map +1 -0
  147. package/dist/internal/ingest/web.d.ts +2 -11
  148. package/dist/internal/ingest/web.d.ts.map +1 -1
  149. package/dist/internal/ingest/web.js +131 -25
  150. package/dist/internal/ingest/web.js.map +1 -1
  151. package/dist/internal/loop/budgets.d.ts +3 -0
  152. package/dist/internal/loop/budgets.d.ts.map +1 -0
  153. package/dist/internal/loop/budgets.js +10 -0
  154. package/dist/internal/loop/budgets.js.map +1 -0
  155. package/dist/internal/loop/index.d.ts +2 -0
  156. package/dist/internal/loop/index.d.ts.map +1 -1
  157. package/dist/internal/loop/index.js +2 -0
  158. package/dist/internal/loop/index.js.map +1 -1
  159. package/dist/internal/loop/json-answer.d.ts +7 -0
  160. package/dist/internal/loop/json-answer.d.ts.map +1 -0
  161. package/dist/internal/loop/json-answer.js +79 -0
  162. package/dist/internal/loop/json-answer.js.map +1 -0
  163. package/dist/internal/loop/json-spans.d.ts +3 -0
  164. package/dist/internal/loop/json-spans.d.ts.map +1 -0
  165. package/dist/internal/loop/json-spans.js +88 -0
  166. package/dist/internal/loop/json-spans.js.map +1 -0
  167. package/dist/internal/loop/llm.d.ts +3 -2
  168. package/dist/internal/loop/llm.d.ts.map +1 -1
  169. package/dist/internal/loop/llm.js +115 -32
  170. package/dist/internal/loop/llm.js.map +1 -1
  171. package/dist/internal/loop/process.d.ts +3 -1
  172. package/dist/internal/loop/process.d.ts.map +1 -1
  173. package/dist/internal/loop/process.js +159 -59
  174. package/dist/internal/loop/process.js.map +1 -1
  175. package/dist/internal/loop/read-snapshot.d.ts +4 -0
  176. package/dist/internal/loop/read-snapshot.d.ts.map +1 -0
  177. package/dist/internal/loop/read-snapshot.js +29 -0
  178. package/dist/internal/loop/read-snapshot.js.map +1 -0
  179. package/dist/internal/loop/reconstruct.d.ts +6 -5
  180. package/dist/internal/loop/reconstruct.d.ts.map +1 -1
  181. package/dist/internal/loop/reconstruct.js +19 -8
  182. package/dist/internal/loop/reconstruct.js.map +1 -1
  183. package/dist/internal/loop/score-settings.d.ts +5 -0
  184. package/dist/internal/loop/score-settings.d.ts.map +1 -0
  185. package/dist/internal/loop/score-settings.js +14 -0
  186. package/dist/internal/loop/score-settings.js.map +1 -0
  187. package/dist/internal/loop/sqlite.d.ts.map +1 -1
  188. package/dist/internal/loop/sqlite.js +15 -7
  189. package/dist/internal/loop/sqlite.js.map +1 -1
  190. package/dist/internal/loop/store.d.ts.map +1 -1
  191. package/dist/internal/loop/store.js +11 -7
  192. package/dist/internal/loop/store.js.map +1 -1
  193. package/dist/internal/loop/timeout.d.ts +3 -0
  194. package/dist/internal/loop/timeout.d.ts.map +1 -0
  195. package/dist/internal/loop/timeout.js +13 -0
  196. package/dist/internal/loop/timeout.js.map +1 -0
  197. package/dist/internal/mcp/error-message.d.ts +3 -0
  198. package/dist/internal/mcp/error-message.d.ts.map +1 -0
  199. package/dist/internal/mcp/error-message.js +10 -0
  200. package/dist/internal/mcp/error-message.js.map +1 -0
  201. package/dist/internal/mcp/main.d.ts.map +1 -1
  202. package/dist/internal/mcp/main.js +42 -27
  203. package/dist/internal/mcp/main.js.map +1 -1
  204. package/dist/internal/mcp/read-snapshot.d.ts +4 -0
  205. package/dist/internal/mcp/read-snapshot.d.ts.map +1 -0
  206. package/dist/internal/mcp/read-snapshot.js +22 -0
  207. package/dist/internal/mcp/read-snapshot.js.map +1 -0
  208. package/dist/internal/mcp/server.d.ts.map +1 -1
  209. package/dist/internal/mcp/server.js +181 -21
  210. package/dist/internal/mcp/server.js.map +1 -1
  211. package/dist/internal/mcp/source.d.ts +3 -0
  212. package/dist/internal/mcp/source.d.ts.map +1 -0
  213. package/dist/internal/mcp/source.js +13 -0
  214. package/dist/internal/mcp/source.js.map +1 -0
  215. package/dist/internal/mcp/tools.d.ts.map +1 -1
  216. package/dist/internal/mcp/tools.js +135 -72
  217. package/dist/internal/mcp/tools.js.map +1 -1
  218. package/dist/internal/rules/declarations.d.ts +6 -0
  219. package/dist/internal/rules/declarations.d.ts.map +1 -0
  220. package/dist/internal/rules/declarations.js +14 -0
  221. package/dist/internal/rules/declarations.js.map +1 -0
  222. package/dist/internal/rules/declare.d.ts.map +1 -1
  223. package/dist/internal/rules/declare.js +30 -16
  224. package/dist/internal/rules/declare.js.map +1 -1
  225. package/dist/internal/rules/engine.d.ts +8 -7
  226. package/dist/internal/rules/engine.d.ts.map +1 -1
  227. package/dist/internal/rules/engine.js +150 -44
  228. package/dist/internal/rules/engine.js.map +1 -1
  229. package/dist/internal/shape/alias-root.d.ts +3 -0
  230. package/dist/internal/shape/alias-root.d.ts.map +1 -0
  231. package/dist/internal/shape/alias-root.js +13 -0
  232. package/dist/internal/shape/alias-root.js.map +1 -0
  233. package/dist/internal/shape/check.d.ts +15 -3
  234. package/dist/internal/shape/check.d.ts.map +1 -1
  235. package/dist/internal/shape/check.js +230 -100
  236. package/dist/internal/shape/check.js.map +1 -1
  237. package/dist/internal/shape/client.d.ts.map +1 -1
  238. package/dist/internal/shape/client.js +42 -20
  239. package/dist/internal/shape/client.js.map +1 -1
  240. package/dist/internal/shape/constraints.d.ts +8 -0
  241. package/dist/internal/shape/constraints.d.ts.map +1 -0
  242. package/dist/internal/shape/constraints.js +17 -0
  243. package/dist/internal/shape/constraints.js.map +1 -0
  244. package/dist/internal/shape/edit-distance.d.ts +3 -0
  245. package/dist/internal/shape/edit-distance.d.ts.map +1 -0
  246. package/dist/internal/shape/edit-distance.js +33 -0
  247. package/dist/internal/shape/edit-distance.js.map +1 -0
  248. package/dist/internal/shape/gate.d.ts.map +1 -1
  249. package/dist/internal/shape/gate.js +0 -0
  250. package/dist/internal/shape/gate.js.map +1 -1
  251. package/dist/internal/shape/snapshot.d.ts +4 -0
  252. package/dist/internal/shape/snapshot.d.ts.map +1 -0
  253. package/dist/internal/shape/snapshot.js +20 -0
  254. package/dist/internal/shape/snapshot.js.map +1 -0
  255. package/dist/internal/shape/suggest.d.ts +8 -6
  256. package/dist/internal/shape/suggest.d.ts.map +1 -1
  257. package/dist/internal/shape/suggest.js +208 -92
  258. package/dist/internal/shape/suggest.js.map +1 -1
  259. package/dist/internal/sync/error-message.d.ts +3 -0
  260. package/dist/internal/sync/error-message.d.ts.map +1 -0
  261. package/dist/internal/sync/error-message.js +10 -0
  262. package/dist/internal/sync/error-message.js.map +1 -0
  263. package/dist/internal/sync/index.d.ts +3 -2
  264. package/dist/internal/sync/index.d.ts.map +1 -1
  265. package/dist/internal/sync/index.js +3 -2
  266. package/dist/internal/sync/index.js.map +1 -1
  267. package/dist/internal/sync/sync.d.ts +8 -5
  268. package/dist/internal/sync/sync.d.ts.map +1 -1
  269. package/dist/internal/sync/sync.js +322 -65
  270. package/dist/internal/sync/sync.js.map +1 -1
  271. package/dist/internal/view/api.d.ts +3 -1
  272. package/dist/internal/view/api.d.ts.map +1 -1
  273. package/dist/internal/view/api.js +154 -90
  274. package/dist/internal/view/api.js.map +1 -1
  275. package/dist/internal/view/error-message.d.ts +3 -0
  276. package/dist/internal/view/error-message.d.ts.map +1 -0
  277. package/dist/internal/view/error-message.js +10 -0
  278. package/dist/internal/view/error-message.js.map +1 -0
  279. package/dist/internal/view/main.d.ts.map +1 -1
  280. package/dist/internal/view/main.js +61 -8
  281. package/dist/internal/view/main.js.map +1 -1
  282. package/dist/internal/view/page.d.ts.map +1 -1
  283. package/dist/internal/view/page.js +119 -20
  284. package/dist/internal/view/page.js.map +1 -1
  285. package/dist/internal/view/report.d.ts.map +1 -1
  286. package/dist/internal/view/report.js +180 -145
  287. package/dist/internal/view/report.js.map +1 -1
  288. package/dist/internal/view/scope.d.ts +6 -2
  289. package/dist/internal/view/scope.d.ts.map +1 -1
  290. package/dist/internal/view/scope.js +107 -24
  291. package/dist/internal/view/scope.js.map +1 -1
  292. package/dist/internal/view/sensitivity.d.ts +4 -0
  293. package/dist/internal/view/sensitivity.d.ts.map +1 -0
  294. package/dist/internal/view/sensitivity.js +11 -0
  295. package/dist/internal/view/sensitivity.js.map +1 -0
  296. package/dist/internal/view/server.d.ts.map +1 -1
  297. package/dist/internal/view/server.js +57 -20
  298. package/dist/internal/view/server.js.map +1 -1
  299. package/dist/src/atomic-output.d.ts +3 -0
  300. package/dist/src/atomic-output.d.ts.map +1 -0
  301. package/dist/src/atomic-output.js +67 -0
  302. package/dist/src/atomic-output.js.map +1 -0
  303. package/dist/src/cli.d.ts +3 -2
  304. package/dist/src/cli.d.ts.map +1 -1
  305. package/dist/src/cli.js +188 -173
  306. package/dist/src/cli.js.map +1 -1
  307. package/dist/src/dispatch.d.ts.map +1 -1
  308. package/dist/src/dispatch.js +3 -2
  309. package/dist/src/dispatch.js.map +1 -1
  310. package/dist/src/doctor.d.ts.map +1 -1
  311. package/dist/src/doctor.js +178 -102
  312. package/dist/src/doctor.js.map +1 -1
  313. package/dist/src/error-message.d.ts +3 -0
  314. package/dist/src/error-message.d.ts.map +1 -0
  315. package/dist/src/error-message.js +20 -0
  316. package/dist/src/error-message.js.map +1 -0
  317. package/dist/src/hooks-config.d.ts +3 -0
  318. package/dist/src/hooks-config.d.ts.map +1 -0
  319. package/dist/src/hooks-config.js +23 -0
  320. package/dist/src/hooks-config.js.map +1 -0
  321. package/package.json +23 -20
  322. package/src/atomic-output.ts +47 -0
  323. package/src/cli.ts +170 -147
  324. package/src/dispatch.ts +3 -2
  325. package/src/doctor.ts +192 -111
  326. package/src/error-message.ts +13 -0
  327. package/src/hooks-config.ts +22 -0
package/src/cli.ts CHANGED
@@ -35,13 +35,16 @@
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 { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, restoreBackup, Sensitivity, 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'
46
49
  import { Declared, assemble as assembleSources } from '@cavelang/connect'
47
50
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query'
@@ -529,7 +532,7 @@ Usage:
529
532
 
530
533
  Options:
531
534
  ${dbHelp}
532
- --out <file> write the generated module instead of stdout
535
+ --out <file> atomically replace a file with the generated module
533
536
  --version <n> generated-client format version (default and currently
534
537
  supported: 1)
535
538
  --no-prelude open the store without the standard verb registry
@@ -553,7 +556,7 @@ Usage:
553
556
  Options:
554
557
  ${dbHelp}
555
558
  --min <s> minimum evidence score, 0..1 or N% (default 60%)
556
- --limit <n> at most n suggestions, strongest first
559
+ --limit <n> positive safe integer; at most n suggestions, strongest first
557
560
  --agent <template> LLM judge filtering the candidates — the cave
558
561
  ingest/eval shell contract: the prompt (each pair
559
562
  with both sides' current claims) is piped to stdin
@@ -584,6 +587,10 @@ claim, so the opt-in §13.6 closure links it until reviewed:
584
587
  claim identity, §9.2, so a plain @ 0% append would start a new series
585
588
  and leave the suggested link standing).
586
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
+
587
594
  Examples:
588
595
  cave suggest-alias --db k.db
589
596
  cave suggest-alias --db k.db --min 80% --limit 10
@@ -641,7 +648,7 @@ Usage:
641
648
 
642
649
  Options:
643
650
  ${dbHelp}
644
- --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
645
652
  names the store's own database file
646
653
  --current current beliefs only (skip superseded rows); compacting
647
654
  view, not sanitization or selective erasure (spec §9.6)
@@ -657,6 +664,10 @@ Options:
657
664
  Retention: claim history is permanent. Retraction and --current do not
658
665
  guarantee erasure from the store, exports, peers, or backups (spec §9.6).
659
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
+
660
671
  Examples:
661
672
  cave export --db k.db
662
673
  cave export --db k.db --out internal-export.cave
@@ -676,7 +687,7 @@ Usage:
676
687
 
677
688
  Options:
678
689
  ${dbHelp}
679
- --out <file> write the rendered markdown to a file instead of stdout
690
+ --out <file> atomically replace a file with the rendered markdown
680
691
  --aliases queries match through the alias closure (spec §13.6)
681
692
  --resolve queries match resolved winners only (spec §26) — the fix
682
693
  when an inline splice reports an ambiguous fact
@@ -775,6 +786,15 @@ Examples:
775
786
  cave help serve`
776
787
  }
777
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
+
778
798
  const readStdin = (): string => {
779
799
  const chunks: Buffer[] = []
780
800
  const buffer = Buffer.allocUnsafe(64 * 1024)
@@ -782,7 +802,7 @@ const readStdin = (): string => {
782
802
  for (;;) {
783
803
  try {
784
804
  const size = readSync(0, buffer, 0, buffer.length, null)
785
- if (size === 0) return Buffer.concat(chunks).toString('utf8')
805
+ if (size === 0) return decodeInput(Buffer.concat(chunks), 'stdin')
786
806
  chunks.push(Buffer.from(buffer.subarray(0, size)))
787
807
  } catch (error) {
788
808
  if ((error as NodeJS.ErrnoException).code !== 'EAGAIN') throw error
@@ -810,6 +830,32 @@ const openDb = (values: { db?: string, 'no-prelude'?: boolean }, intent: OpenInt
810
830
  ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
811
831
  })
812
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
+
813
859
  const sourcesRollback = Symbol('cave query --sources rollback')
814
860
 
815
861
  /**
@@ -847,7 +893,7 @@ const withSources = <T>(store: Store, discovered: Declared.Discovery, body: () =
847
893
  const readInput = (files: readonly string[]): string =>
848
894
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
849
895
  readStdin() :
850
- files.map(file => readFileSync(file, 'utf8')).join('\n')
896
+ files.map(file => decodeInput(readFileSync(file), file)).join('\n')
851
897
 
852
898
  export const parseCommand = (argv: readonly string[]): Output => {
853
899
  const { values, positionals } = parseArgs({
@@ -928,7 +974,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
928
974
  })
929
975
  const input = readInput(positionals)
930
976
  const store = openDb(values, 'write')
931
- try {
977
+ return withCommandStore(store, () => {
932
978
  const options = {
933
979
  strict: values.strict === true,
934
980
  ...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
@@ -950,11 +996,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
950
996
  out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
951
997
  err: problems === '' ? '' : `${problems}\n`
952
998
  }
953
- } catch (error) {
954
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
955
- } finally {
956
- store.close()
957
- }
999
+ })
958
1000
  }
959
1001
 
960
1002
  export const addCommand = ingestCommand('add')
@@ -1038,7 +1080,7 @@ export const querySourcesCommand = async (
1038
1080
  // The overlay appends inside a transaction it rolls back: writable, but
1039
1081
  // never creating or migrating (spec §13.7).
1040
1082
  const store = openDb(values, 'scratch')
1041
- try {
1083
+ return await withAsyncCommandStore(store, async () => {
1042
1084
  for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
1043
1085
  const discovered = await Declared.discovery(store, root, {
1044
1086
  // The overlay applies every source it loads (force), so discovery
@@ -1053,11 +1095,7 @@ export const querySourcesCommand = async (
1053
1095
  }
1054
1096
  }
1055
1097
  throw Declared.staleOverlay()
1056
- } catch (error) {
1057
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1058
- } finally {
1059
- store.close()
1060
- }
1098
+ })
1061
1099
  }
1062
1100
 
1063
1101
  export const queryCommand = (argv: readonly string[]): Output => {
@@ -1072,13 +1110,9 @@ export const queryCommand = (argv: readonly string[]): Output => {
1072
1110
  return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n')
1073
1111
  }
1074
1112
  const store = openDb(values, 'read')
1075
- try {
1113
+ return withCommandStore(store, () => {
1076
1114
  return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern))
1077
- } catch (error) {
1078
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1079
- } finally {
1080
- store.close()
1081
- }
1115
+ })
1082
1116
  }
1083
1117
 
1084
1118
  /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
@@ -1177,7 +1211,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1177
1211
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`)
1178
1212
  }
1179
1213
  const store = openDb(values, 'read')
1180
- try {
1214
+ return withCommandStore(store, () => {
1181
1215
  const raw = values.raw === true
1182
1216
  const found = store.search(query, { raw, limit: limit + 1 })
1183
1217
  const matches = found.slice(0, limit)
@@ -1201,11 +1235,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1201
1235
  lines.push(`more matches beyond ${limit}; raise --limit`)
1202
1236
  }
1203
1237
  return ok(`${lines.join('\n')}\n`)
1204
- } catch (error) {
1205
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1206
- } finally {
1207
- store.close()
1208
- }
1238
+ })
1209
1239
  }
1210
1240
 
1211
1241
  /**
@@ -1225,15 +1255,20 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1225
1255
  },
1226
1256
  allowPositionals: false
1227
1257
  })
1228
- 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
+ }
1229
1264
  const store = openDb(values, 'read')
1230
- try {
1265
+ return withCommandStore(store, () => {
1231
1266
  if (values.policy === true) {
1232
1267
  const policy = store.resolutionPolicy()
1233
1268
  if (values.json === true) {
1234
1269
  return ok(`${JSON.stringify(policy, undefined, 2)}\n`)
1235
1270
  }
1236
- 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)
1237
1272
  const lines = policy.map(entry => {
1238
1273
  const subject = entry.prefix === '' ? 'source' : `source/${entry.prefix}`
1239
1274
  const dimensions = [
@@ -1255,11 +1290,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1255
1290
  group.rows.map((row, at) =>
1256
1291
  `${at === 0 ? '' : ' over '}${row.raw_line} ; class ${row.res_class}, effective ${percent(row.res_conf)}`))
1257
1292
  return ok(`${lines.join('\n')}\n`)
1258
- } catch (error) {
1259
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1260
- } finally {
1261
- store.close()
1262
- }
1293
+ })
1263
1294
  }
1264
1295
 
1265
1296
  export const deriveCommand = (argv: readonly string[]): Output => {
@@ -1286,8 +1317,8 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1286
1317
  return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`)
1287
1318
  }
1288
1319
  const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes'])
1289
- if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1290
- 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`)
1291
1322
  }
1292
1323
  // The intent follows the branch order below: listing reads, retracting
1293
1324
  // writes, a dry run appends inside a rolled-back transaction.
@@ -1295,7 +1326,7 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1295
1326
  values.list === true ? 'read' :
1296
1327
  values.retract !== undefined ? 'write' :
1297
1328
  values['dry-run'] === true ? 'scratch' : 'write')
1298
- try {
1329
+ return withCommandStore(store, () => {
1299
1330
  if (values.list === true) {
1300
1331
  const rules = listRules(store)
1301
1332
  if (values.json === true) {
@@ -1355,13 +1386,15 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1355
1386
  report = derive(store, options)
1356
1387
  }
1357
1388
  if (declaration !== undefined) {
1358
- 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}`)
1359
1390
  out.push(`declared ${declaration.declared} rule(s)` +
1360
1391
  (declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
1361
1392
  (declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''))
1362
1393
  }
1394
+ const hasProblems = (declaration?.problems.length ?? 0) > 0 || !report.complete ||
1395
+ report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
1363
1396
  if (values.json === true) {
1364
- 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' : '') }
1365
1398
  }
1366
1399
  for (const problem of report.problems) {
1367
1400
  err.push(`${problem.subject}: ${problem.problems.join('; ')}`)
@@ -1371,33 +1404,15 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1371
1404
  `${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
1372
1405
  'unchanged premises, skipped'
1373
1406
  out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`)
1374
- err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`))
1407
+ for (const problem of rule.problems) err.push(`${rule.subject}: ${problem}`)
1375
1408
  }
1376
- out.push(...report.notes.map(note => `note: ${note}`))
1409
+ for (const note of report.notes) out.push(`note: ${note}`)
1377
1410
  out.push(
1378
- `derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1411
+ `derived${report.complete ? '' : ' (incomplete)'}${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1379
1412
  `+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
1380
1413
  `${report.unchanged} unchanged (${report.passes} pass(es))`)
1381
- const hasProblems = !report.complete || report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
1382
1414
  return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` }
1383
- } catch (error) {
1384
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1385
- } finally {
1386
- store.close()
1387
- }
1388
- }
1389
-
1390
- /**
1391
- * Loads a hooks configuration file (spec §25.4): a JSON object mapping
1392
- * hook names to shell command templates.
1393
- */
1394
- const readHooks = (path: string): Record<string, string> => {
1395
- const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
1396
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
1397
- Object.values(parsed).some(value => typeof value !== 'string')) {
1398
- throw new Error(`${path}: hooks must be a JSON object of name → shell template strings`)
1399
- }
1400
- return parsed as Record<string, string>
1415
+ })
1401
1416
  }
1402
1417
 
1403
1418
  /** Text rendering of a spec §25.2 execution report. */
@@ -1417,15 +1432,14 @@ const renderActReport = (report: ActReport): { lines: string[], code: number } =
1417
1432
  ]
1418
1433
  let code = 0
1419
1434
  if (report.hook !== undefined) {
1420
- if (!report.hook.fired) {
1421
- lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`)
1422
- } else if (report.hook.error === undefined) {
1423
- lines.push(`hook ${report.hook.name}: ok`)
1424
- } else {
1425
- // The claims committed before the hook ran (spec §25.4) — the
1426
- // 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.
1427
1437
  lines.push(`hook ${report.hook.name}: ${report.hook.error}`)
1428
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`)
1429
1443
  }
1430
1444
  }
1431
1445
  return { lines, code }
@@ -1456,7 +1470,7 @@ export const actCommand = (argv: readonly string[]): Output => {
1456
1470
  values.list === true ? 'read' :
1457
1471
  values.retract !== undefined ? 'write' :
1458
1472
  values['dry-run'] === true ? 'scratch' : 'write')
1459
- try {
1473
+ return withCommandStore(store, () => {
1460
1474
  if (values.declare === true) {
1461
1475
  const declaration = declareActions(store, readInput(positionals))
1462
1476
  const err = declaration.problems.map(problem => `line ${problem.line}: ${problem.message}`)
@@ -1503,7 +1517,13 @@ export const actCommand = (argv: readonly string[]): Output => {
1503
1517
  if (at <= 0) {
1504
1518
  return fail(`cave act: expected param=value, got ${JSON.stringify(pair)}\n`)
1505
1519
  }
1506
- 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
+ })
1507
1527
  }
1508
1528
  const hooksPath = values.hooks ?? process.env['CAVE_HOOKS']
1509
1529
  const report = act(store, name, args, {
@@ -1519,11 +1539,7 @@ export const actCommand = (argv: readonly string[]): Output => {
1519
1539
  return report.ok ?
1520
1540
  { code, out: `${lines.join('\n')}\n`, err: '' } :
1521
1541
  { code, out: '', err: `${lines.join('\n')}\n` }
1522
- } catch (error) {
1523
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1524
- } finally {
1525
- store.close()
1526
- }
1542
+ })
1527
1543
  }
1528
1544
 
1529
1545
  /** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
@@ -1533,26 +1549,32 @@ const renderReport = (report: Report, staleDays: number): string => {
1533
1549
  lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`)
1534
1550
  if (report.violations.length > 0) {
1535
1551
  lines.push(`violations (${report.violations.length}):`)
1536
- lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`))
1552
+ for (const violation of report.violations) lines.push(` ${formatViolation(violation)}`)
1537
1553
  }
1538
1554
  if (report.stale.length > 0) {
1539
1555
  lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`)
1540
- 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)`)
1541
1557
  }
1542
1558
  if (report.review.length > 0) {
1543
1559
  lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`)
1544
- lines.push(...report.review.map(row => ` ${row.raw_line}`))
1560
+ for (const row of report.review) lines.push(` ${row.raw_line}`)
1545
1561
  }
1546
1562
  if (report.disagreements.length > 0) {
1547
1563
  lines.push(`alias disagreements (${report.disagreements.length}):`)
1548
1564
  for (const disagreement of report.disagreements) {
1549
1565
  lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`)
1550
- lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`))
1566
+ for (const row of disagreement.rows) lines.push(` ${row.raw_line}`)
1551
1567
  }
1552
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
+ }
1553
1575
  const confidence = coverage.averageConfidence === null ?
1554
1576
  '' :
1555
- `; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`
1577
+ `; avg conf ${confidencePercent(coverage.averageConfidence)}, ${coverage.lowConfidence} low (< 0.3)`
1556
1578
  lines.push(
1557
1579
  `coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
1558
1580
  `${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
@@ -1577,16 +1599,14 @@ export const checkCommand = (argv: readonly string[]): Output => {
1577
1599
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
1578
1600
  }
1579
1601
  const store = openDb(values, 'read')
1580
- try {
1602
+ return withCommandStore(store, () => {
1581
1603
  const report = caveCheck(store, { staleDays })
1582
1604
  const out = values.json === true ?
1583
1605
  `${stableJson(store, report)}\n` :
1584
1606
  renderReport(report, staleDays)
1585
1607
  // Violations fail the check (spec §20.2); the other sections are advisory.
1586
1608
  return { code: report.violations.length > 0 ? 1 : 0, out, err: '' }
1587
- } finally {
1588
- store.close()
1589
- }
1609
+ })
1590
1610
  }
1591
1611
 
1592
1612
  const snapshotLine = (
@@ -1630,17 +1650,16 @@ export const backupCommand = (argv: readonly string[]): Output => {
1630
1650
  // the destination aliases the source: refuse before the file could be
1631
1651
  // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1632
1652
  // the API's own check).
1633
- if (sameFile(dbPath, values.out)) {
1653
+ if (sameStoreFile(dbPath, values.out)) {
1634
1654
  return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1635
1655
  }
1656
+ const destination = values.out
1636
1657
  const store = openAt(dbPath, { intent: 'read', assemble: assembleSources })
1637
- try {
1638
- return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1639
- } finally {
1640
- store.close()
1641
- }
1658
+ return withCommandStore(store, () => {
1659
+ return ok(snapshotLine('created', backupStore(store, destination, { force: values.force === true })))
1660
+ })
1642
1661
  } catch (error) {
1643
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1662
+ return fail(`${errorMessage(error)}\n`)
1644
1663
  }
1645
1664
  }
1646
1665
 
@@ -1670,7 +1689,7 @@ export const restoreCommand = (argv: readonly string[]): Output => {
1670
1689
  ...values.sha256 === undefined ? {} : { expectedSha256: values.sha256 }
1671
1690
  })))
1672
1691
  } catch (error) {
1673
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1692
+ return fail(`${errorMessage(error)}\n`)
1674
1693
  }
1675
1694
  }
1676
1695
 
@@ -1709,15 +1728,15 @@ export const suggestAliasCommand = async (
1709
1728
  return fail(`cave suggest-alias: --min expects a score in 0..1 or N%, got ${JSON.stringify(values.min)}\n`)
1710
1729
  }
1711
1730
  const limit = values.limit === undefined ? undefined : Number(values.limit)
1712
- if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
1713
- 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`)
1714
1733
  }
1715
1734
  const timeoutSeconds = values.timeout === undefined ? undefined : Number(values.timeout)
1716
1735
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1717
1736
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1718
1737
  }
1719
1738
  const store = openDb(values, values.write === true ? 'write' : 'read')
1720
- try {
1739
+ return await withAsyncCommandStore(store, async () => {
1721
1740
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1722
1741
  if (values.agent !== undefined && suggestions.length > 0) {
1723
1742
  const reply = await shellComplete(values.agent, {
@@ -1729,7 +1748,9 @@ export const suggestAliasCommand = async (
1729
1748
  suggestions = parseJudgeReply(reply, suggestions.length).map(index => suggestions[index]!)
1730
1749
  }
1731
1750
  if (values.json === true) {
1732
- 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`)
1733
1754
  }
1734
1755
  if (suggestions.length === 0) {
1735
1756
  return ok('no alias suggestions\n')
@@ -1740,11 +1761,9 @@ export const suggestAliasCommand = async (
1740
1761
  return ok(`${lines.join('\n')}\nappended ${appended} suggested alias claim(s)\n`)
1741
1762
  }
1742
1763
  return ok(`${lines.join('\n')}\n`)
1743
- } finally {
1744
- store.close()
1745
- }
1764
+ })
1746
1765
  } catch (error) {
1747
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1766
+ return fail(`${errorMessage(error)}\n`)
1748
1767
  }
1749
1768
  }
1750
1769
 
@@ -1774,7 +1793,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1774
1793
  }
1775
1794
  const dbPath = values.db ?? defaultDbPath()
1776
1795
  const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write')
1777
- try {
1796
+ return withCommandStore(store, () => {
1778
1797
  const options = {
1779
1798
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
1780
1799
  record: values['no-record'] !== true,
@@ -1788,7 +1807,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1788
1807
  return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
1789
1808
  }
1790
1809
  if (report.problems.length > 0) {
1791
- 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')
1792
1811
  return fail(`cave sync: ${source}: ${report.problems.length} problem(s), nothing merged\n${detail}\n`)
1793
1812
  }
1794
1813
  const lines = [
@@ -1800,17 +1819,14 @@ export const syncCommand = (argv: readonly string[]): Output => {
1800
1819
  lines.push(`record: ${report.record}`)
1801
1820
  }
1802
1821
  return ok(`${lines.join('\n')}\n`)
1803
- } catch (error) {
1804
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1805
- } finally {
1806
- store.close()
1807
- }
1822
+ })
1808
1823
  }
1809
1824
 
1810
1825
  /**
1811
1826
  * `cave report` — the §31 deliverable: a markdown template's cave-q
1812
- * blocks and inline splices render from the store, every stated fact
1813
- * 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
1814
1830
  * template line numbers, and fail the exit code — the render never
1815
1831
  * silently drops a fact.
1816
1832
  */
@@ -1829,16 +1845,22 @@ export const reportCommand = (argv: readonly string[]): Output => {
1829
1845
  },
1830
1846
  allowPositionals: true
1831
1847
  })
1832
- 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
+ }
1833
1854
  const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum)
1834
1855
  if (maximum === undefined) {
1835
1856
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1836
1857
  }
1837
- if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1858
+ if (values.out !== undefined && sameStoreFile(values.db ?? defaultDbPath(), values.out)) {
1838
1859
  return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1839
1860
  }
1861
+ const template = readInput(positionals)
1840
1862
  const store = openDb(values, 'read')
1841
- try {
1863
+ return withCommandStore(store, () => {
1842
1864
  const rendered = caveReport(store, template, {
1843
1865
  aliases: values.aliases === true,
1844
1866
  resolve: values.resolve === true,
@@ -1853,17 +1875,13 @@ export const reportCommand = (argv: readonly string[]): Output => {
1853
1875
  if (values.out === undefined) {
1854
1876
  return { code, out: rendered.markdown, err: err === '' ? '' : `${err}\n` }
1855
1877
  }
1856
- writeFileSync(values.out, rendered.markdown)
1878
+ writeOutput(values.out, rendered.markdown)
1857
1879
  return {
1858
1880
  code,
1859
1881
  out: `rendered ${rendered.citations} citation(s) to ${values.out}\n`,
1860
1882
  err: err === '' ? '' : `${err}\n`
1861
1883
  }
1862
- } catch (error) {
1863
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1864
- } finally {
1865
- store.close()
1866
- }
1884
+ })
1867
1885
  }
1868
1886
 
1869
1887
  /**
@@ -1884,6 +1902,21 @@ const sameFile = (a: string, b: string): boolean => {
1884
1902
  }
1885
1903
  }
1886
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
+
1887
1920
  export const exportCommand = (argv: readonly string[]): Output => {
1888
1921
  const { values } = parseArgs({
1889
1922
  args: [...argv],
@@ -1902,26 +1935,22 @@ export const exportCommand = (argv: readonly string[]): Output => {
1902
1935
  if (maximum === undefined) {
1903
1936
  return fail(`cave export: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1904
1937
  }
1905
- if (values.out !== undefined && sameFile(db, values.out)) {
1938
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1906
1939
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1907
1940
  }
1908
1941
  const store = openDb(values, 'read')
1909
- try {
1942
+ return withCommandStore(store, () => {
1910
1943
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1911
1944
  if (values.out === undefined) {
1912
1945
  return ok(text)
1913
1946
  }
1914
- writeFileSync(values.out, text)
1947
+ writeOutput(values.out, text)
1915
1948
  // Root claims only: shorthand prefixes and §28.4 annotations are not
1916
1949
  // claims, while qualifier/grouping leaves have a materialized parent.
1917
1950
  const claims = parseDocument(text).lines
1918
1951
  .filter(line => line.kind === 'claim' && line.parent === undefined).length
1919
1952
  return ok(`exported ${claims} claim(s) to ${values.out}\n`)
1920
- } catch (error) {
1921
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1922
- } finally {
1923
- store.close()
1924
- }
1953
+ })
1925
1954
  }
1926
1955
 
1927
1956
  /** `cave generate` — spec §20.4's versioned TypeScript client artifact. */
@@ -1941,11 +1970,11 @@ export const generateCommand = (argv: readonly string[]): Output => {
1941
1970
  return fail(`cave generate: --version expects a positive integer, got ${JSON.stringify(values.version)}\n`)
1942
1971
  }
1943
1972
  const db = values.db ?? defaultDbPath()
1944
- if (values.out !== undefined && sameFile(db, values.out)) {
1973
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1945
1974
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1946
1975
  }
1947
1976
  const store = openDb(values, 'read')
1948
- try {
1977
+ return withCommandStore(store, () => {
1949
1978
  const generated = generateClient(store, version === undefined ? {} : { version })
1950
1979
  if (!generated.ok) {
1951
1980
  return fail(`cave generate: schema cannot be generated:\n${generated.problems.map(problem => ` ${problem}`).join('\n')}\n`)
@@ -1953,14 +1982,10 @@ export const generateCommand = (argv: readonly string[]): Output => {
1953
1982
  if (values.out === undefined) {
1954
1983
  return ok(generated.code)
1955
1984
  }
1956
- writeFileSync(values.out, generated.code)
1985
+ writeOutput(values.out, generated.code)
1957
1986
  return ok(`generated typed client v${generated.version} (${generated.fields.length} field(s), ` +
1958
1987
  `sha256:${generated.digest}) to ${values.out}\n`)
1959
- } catch (error) {
1960
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1961
- } finally {
1962
- store.close()
1963
- }
1988
+ })
1964
1989
  }
1965
1990
 
1966
1991
  /**
@@ -2010,7 +2035,7 @@ export const reconstructCommand = async (
2010
2035
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
2011
2036
  }
2012
2037
  const store = openDb(values, 'read')
2013
- try {
2038
+ return await withAsyncCommandStore(store, async () => {
2014
2039
  const graph = sqliteStore(store)
2015
2040
  const result = values.agent === undefined ?
2016
2041
  reconstruct(graph, heuristicPolicy(budgets), positionals) :
@@ -2034,11 +2059,9 @@ export const reconstructCommand = async (
2034
2059
  ...result.claims.map(claim => emitClaim(claim))
2035
2060
  ]
2036
2061
  return ok(lines.length === 0 ? '' : `${lines.join('\n')}\n`)
2037
- } finally {
2038
- store.close()
2039
- }
2062
+ })
2040
2063
  } catch (error) {
2041
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
2064
+ return fail(`${errorMessage(error)}\n`)
2042
2065
  }
2043
2066
  }
2044
2067