@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/dist/src/cli.js CHANGED
@@ -34,13 +34,16 @@
34
34
  * `file` defaults to stdin (`-`); `--db` defaults to `$CAVE_DB`, then
35
35
  * `cave.db`. Every command answers `--help` with options and examples.
36
36
  */
37
- import { readFileSync, readSync, statSync, writeFileSync } from 'node:fs';
38
- import { resolve } from 'node:path';
37
+ import { readFileSync, readSync, realpathSync, statSync } from 'node:fs';
38
+ import { readHooks } from "./hooks-config.js";
39
+ import { basename, dirname, join, resolve } from 'node:path';
39
40
  import { parseArgs } from 'node:util';
40
- import { Version } from '@cavelang/core';
41
+ import { writeOutput } from "./atomic-output.js";
42
+ import { errorMessage } from "./error-message.js";
43
+ import { Time, Version } from '@cavelang/core';
41
44
  import { parseDocument, Token } from '@cavelang/parser';
42
45
  import { Registry, standardRegistry } from '@cavelang/canonical';
43
- import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store';
46
+ import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, QuerySql, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store';
44
47
  import { Declared, assemble as assembleSources } from '@cavelang/cli/connect';
45
48
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query';
46
49
  import { check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply, generateClient, suggestAliases, writeSuggestions } from '@cavelang/cli/shape';
@@ -487,7 +490,7 @@ Usage:
487
490
 
488
491
  Options:
489
492
  ${dbHelp}
490
- --out <file> write the generated module instead of stdout
493
+ --out <file> atomically replace a file with the generated module
491
494
  --version <n> generated-client format version (default and currently
492
495
  supported: 1)
493
496
  --no-prelude open the store without the standard verb registry
@@ -510,7 +513,7 @@ Usage:
510
513
  Options:
511
514
  ${dbHelp}
512
515
  --min <s> minimum evidence score, 0..1 or N% (default 60%)
513
- --limit <n> at most n suggestions, strongest first
516
+ --limit <n> positive safe integer; at most n suggestions, strongest first
514
517
  --agent <template> LLM judge filtering the candidates — the cave
515
518
  ingest/eval shell contract: the prompt (each pair
516
519
  with both sides' current claims) is piped to stdin
@@ -541,6 +544,10 @@ claim, so the opt-in §13.6 closure links it until reviewed:
541
544
  claim identity, §9.2, so a plain @ 0% append would start a new series
542
545
  and leave the suggested link standing).
543
546
 
547
+ Writes recheck pair history under their transaction reservation and skip
548
+ reviewed pairs. --json returns the selected suggestion array; --json --write
549
+ performs the write and returns { suggestions, appended }, with the actual count.
550
+
544
551
  Examples:
545
552
  cave suggest-alias --db k.db
546
553
  cave suggest-alias --db k.db --min 80% --limit 10
@@ -596,7 +603,7 @@ Usage:
596
603
 
597
604
  Options:
598
605
  ${dbHelp}
599
- --out <file> write to a file instead of stdout — refused when it
606
+ --out <file> atomically replace a file instead of stdout — refused when it
600
607
  names the store's own database file
601
608
  --current current beliefs only (skip superseded rows); compacting
602
609
  view, not sanitization or selective erasure (spec §9.6)
@@ -612,6 +619,10 @@ Options:
612
619
  Retention: claim history is permanent. Retraction and --current do not
613
620
  guarantee erasure from the store, exports, peers, or backups (spec §9.6).
614
621
 
622
+ File output is staged and flushed before replacement. Existing permissions
623
+ and valid symlinks are preserved; hard-link aliases retain the previous file.
624
+ Dangling symlinks and non-regular output files are refused.
625
+
615
626
  Examples:
616
627
  cave export --db k.db
617
628
  cave export --db k.db --out internal-export.cave
@@ -630,7 +641,7 @@ Usage:
630
641
 
631
642
  Options:
632
643
  ${dbHelp}
633
- --out <file> write the rendered markdown to a file instead of stdout
644
+ --out <file> atomically replace a file with the rendered markdown
634
645
  --aliases queries match through the alias closure (spec §13.6)
635
646
  --resolve queries match resolved winners only (spec §26) — the fix
636
647
  when an inline splice reports an ambiguous fact
@@ -724,6 +735,15 @@ Examples:
724
735
  cave help query
725
736
  cave help serve`
726
737
  };
738
+ const decodeInput = (bytes, source) => {
739
+ try {
740
+ // Keep the existing BOM behavior of Buffer.toString while rejecting loss.
741
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
742
+ }
743
+ catch (cause) {
744
+ throw new TypeError(`${source}: invalid UTF-8 input`, { cause });
745
+ }
746
+ };
727
747
  const readStdin = () => {
728
748
  const chunks = [];
729
749
  const buffer = Buffer.allocUnsafe(64 * 1024);
@@ -732,7 +752,7 @@ const readStdin = () => {
732
752
  try {
733
753
  const size = readSync(0, buffer, 0, buffer.length, null);
734
754
  if (size === 0)
735
- return Buffer.concat(chunks).toString('utf8');
755
+ return decodeInput(Buffer.concat(chunks), 'stdin');
736
756
  chunks.push(Buffer.from(buffer.subarray(0, size)));
737
757
  }
738
758
  catch (error) {
@@ -759,6 +779,38 @@ const openDb = (values, intent) => openAt(values.db ?? defaultDbPath(), {
759
779
  assemble: assembleSources,
760
780
  ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
761
781
  });
782
+ /** Retain command output and diagnostics when its owned store fails to close. */
783
+ const closeCommandStore = (store, output) => {
784
+ try {
785
+ store.close();
786
+ }
787
+ catch (error) {
788
+ const separator = output.err === '' || output.err.endsWith('\n') ? '' : '\n';
789
+ return { ...output, code: output.code === 0 ? 1 : output.code,
790
+ err: `${output.err}${separator}store close failed: ${errorMessage(error)}\n` };
791
+ }
792
+ return output;
793
+ };
794
+ const withCommandStore = (store, run) => {
795
+ let output;
796
+ try {
797
+ output = run();
798
+ }
799
+ catch (error) {
800
+ output = fail(`${errorMessage(error)}\n`);
801
+ }
802
+ return closeCommandStore(store, output);
803
+ };
804
+ const withAsyncCommandStore = async (store, run) => {
805
+ let output;
806
+ try {
807
+ output = await run();
808
+ }
809
+ catch (error) {
810
+ output = fail(`${errorMessage(error)}\n`);
811
+ }
812
+ return closeCommandStore(store, output);
813
+ };
762
814
  const sourcesRollback = Symbol('cave query --sources rollback');
763
815
  /**
764
816
  * Runs `body` with the store's declared sources overlaid (spec §23.4): the
@@ -794,7 +846,7 @@ const withSources = (store, discovered, body) => {
794
846
  };
795
847
  const readInput = (files) => files.length === 0 || (files.length === 1 && files[0] === '-') ?
796
848
  readStdin() :
797
- files.map(file => readFileSync(file, 'utf8')).join('\n');
849
+ files.map(file => decodeInput(readFileSync(file), file)).join('\n');
798
850
  export const parseCommand = (argv) => {
799
851
  const { values, positionals } = parseArgs({
800
852
  args: [...argv],
@@ -869,7 +921,7 @@ const ingestCommand = (name) => (argv) => {
869
921
  });
870
922
  const input = readInput(positionals);
871
923
  const store = openDb(values, 'write');
872
- try {
924
+ return withCommandStore(store, () => {
873
925
  const options = {
874
926
  strict: values.strict === true,
875
927
  ...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
@@ -891,13 +943,7 @@ const ingestCommand = (name) => (argv) => {
891
943
  out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
892
944
  err: problems === '' ? '' : `${problems}\n`
893
945
  };
894
- }
895
- catch (error) {
896
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
897
- }
898
- finally {
899
- store.close();
900
- }
946
+ });
901
947
  };
902
948
  export const addCommand = ingestCommand('add');
903
949
  /**
@@ -959,7 +1005,7 @@ export const querySourcesCommand = async (argv, runtime = {}) => {
959
1005
  // The overlay appends inside a transaction it rolls back: writable, but
960
1006
  // never creating or migrating (spec §13.7).
961
1007
  const store = openDb(values, 'scratch');
962
- try {
1008
+ return await withAsyncCommandStore(store, async () => {
963
1009
  for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
964
1010
  const discovered = await Declared.discovery(store, root, {
965
1011
  // The overlay applies every source it loads (force), so discovery
@@ -974,13 +1020,7 @@ export const querySourcesCommand = async (argv, runtime = {}) => {
974
1020
  }
975
1021
  }
976
1022
  throw Declared.staleOverlay();
977
- }
978
- catch (error) {
979
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
980
- }
981
- finally {
982
- store.close();
983
- }
1023
+ });
984
1024
  };
985
1025
  export const queryCommand = (argv) => {
986
1026
  const parsed = parseQuery(argv);
@@ -994,15 +1034,9 @@ export const queryCommand = (argv) => {
994
1034
  return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n');
995
1035
  }
996
1036
  const store = openDb(values, 'read');
997
- try {
1037
+ return withCommandStore(store, () => {
998
1038
  return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern));
999
- }
1000
- catch (error) {
1001
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1002
- }
1003
- finally {
1004
- store.close();
1005
- }
1039
+ });
1006
1040
  };
1007
1041
  /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
1008
1042
  const wholeQuery = (store, values, pattern) => {
@@ -1090,7 +1124,7 @@ export const searchCommand = (argv) => {
1090
1124
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`);
1091
1125
  }
1092
1126
  const store = openDb(values, 'read');
1093
- try {
1127
+ return withCommandStore(store, () => {
1094
1128
  const raw = values.raw === true;
1095
1129
  const found = store.search(query, { raw, limit: limit + 1 });
1096
1130
  const matches = found.slice(0, limit);
@@ -1114,13 +1148,7 @@ export const searchCommand = (argv) => {
1114
1148
  lines.push(`more matches beyond ${limit}; raise --limit`);
1115
1149
  }
1116
1150
  return ok(`${lines.join('\n')}\n`);
1117
- }
1118
- catch (error) {
1119
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1120
- }
1121
- finally {
1122
- store.close();
1123
- }
1151
+ });
1124
1152
  };
1125
1153
  /**
1126
1154
  * `cave resolve` — the human view of contradiction resolution (spec §26.4):
@@ -1139,15 +1167,22 @@ export const resolveCommand = (argv) => {
1139
1167
  },
1140
1168
  allowPositionals: false
1141
1169
  });
1142
- const percent = (value) => `${Math.round(value * 1000) / 10}%`;
1170
+ const percent = (value) => {
1171
+ const rounded = Math.round(value * 1000) / 10;
1172
+ if (value > 0 && rounded === 0)
1173
+ return '<0.1%';
1174
+ if (value < 1 && rounded === 100)
1175
+ return '>99.9%';
1176
+ return `${rounded}%`;
1177
+ };
1143
1178
  const store = openDb(values, 'read');
1144
- try {
1179
+ return withCommandStore(store, () => {
1145
1180
  if (values.policy === true) {
1146
1181
  const policy = store.resolutionPolicy();
1147
1182
  if (values.json === true) {
1148
1183
  return ok(`${JSON.stringify(policy, undefined, 2)}\n`);
1149
1184
  }
1150
- const width = Math.max(...policy.map(entry => entry.prefix.length + 7));
1185
+ const width = policy.reduce((maximum, entry) => Math.max(maximum, entry.prefix.length + 7), 0);
1151
1186
  const lines = policy.map(entry => {
1152
1187
  const subject = entry.prefix === '' ? 'source' : `source/${entry.prefix}`;
1153
1188
  const dimensions = [
@@ -1167,13 +1202,7 @@ export const resolveCommand = (argv) => {
1167
1202
  }
1168
1203
  const lines = contested.flatMap(group => group.rows.map((row, at) => `${at === 0 ? '' : ' over '}${row.raw_line} ; class ${row.res_class}, effective ${percent(row.res_conf)}`));
1169
1204
  return ok(`${lines.join('\n')}\n`);
1170
- }
1171
- catch (error) {
1172
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1173
- }
1174
- finally {
1175
- store.close();
1176
- }
1205
+ });
1177
1206
  };
1178
1207
  export const deriveCommand = (argv) => {
1179
1208
  const { values, positionals } = parseArgs({
@@ -1199,15 +1228,15 @@ export const deriveCommand = (argv) => {
1199
1228
  return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`);
1200
1229
  }
1201
1230
  const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes']);
1202
- if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1203
- return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`);
1231
+ if (maxPasses !== undefined && (!Number.isSafeInteger(maxPasses) || maxPasses < 1)) {
1232
+ return fail(`cave derive: --max-passes expects a positive safe integer, got ${JSON.stringify(values['max-passes'])}\n`);
1204
1233
  }
1205
1234
  // The intent follows the branch order below: listing reads, retracting
1206
1235
  // writes, a dry run appends inside a rolled-back transaction.
1207
1236
  const store = openDb(values, values.list === true ? 'read' :
1208
1237
  values.retract !== undefined ? 'write' :
1209
1238
  values['dry-run'] === true ? 'scratch' : 'write');
1210
- try {
1239
+ return withCommandStore(store, () => {
1211
1240
  if (values.list === true) {
1212
1241
  const rules = listRules(store);
1213
1242
  if (values.json === true) {
@@ -1267,13 +1296,16 @@ export const deriveCommand = (argv) => {
1267
1296
  report = derive(store, options);
1268
1297
  }
1269
1298
  if (declaration !== undefined) {
1270
- err.push(...declaration.problems.map(problem => `rules line ${problem.line}: ${problem.message}`));
1299
+ for (const problem of declaration.problems)
1300
+ err.push(`rules line ${problem.line}: ${problem.message}`);
1271
1301
  out.push(`declared ${declaration.declared} rule(s)` +
1272
1302
  (declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
1273
1303
  (declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''));
1274
1304
  }
1305
+ const hasProblems = (declaration?.problems.length ?? 0) > 0 || !report.complete ||
1306
+ report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0);
1275
1307
  if (values.json === true) {
1276
- return { code: report.problems.length > 0 || !report.complete ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') };
1308
+ return { code: hasProblems ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') };
1277
1309
  }
1278
1310
  for (const problem of report.problems) {
1279
1311
  err.push(`${problem.subject}: ${problem.problems.join('; ')}`);
@@ -1283,33 +1315,16 @@ export const deriveCommand = (argv) => {
1283
1315
  `${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
1284
1316
  'unchanged premises, skipped';
1285
1317
  out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`);
1286
- err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`));
1318
+ for (const problem of rule.problems)
1319
+ err.push(`${rule.subject}: ${problem}`);
1287
1320
  }
1288
- out.push(...report.notes.map(note => `note: ${note}`));
1289
- out.push(`derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1321
+ for (const note of report.notes)
1322
+ out.push(`note: ${note}`);
1323
+ out.push(`derived${report.complete ? '' : ' (incomplete)'}${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
1290
1324
  `+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
1291
1325
  `${report.unchanged} unchanged (${report.passes} pass(es))`);
1292
- const hasProblems = !report.complete || report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0);
1293
1326
  return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` };
1294
- }
1295
- catch (error) {
1296
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1297
- }
1298
- finally {
1299
- store.close();
1300
- }
1301
- };
1302
- /**
1303
- * Loads a hooks configuration file (spec §25.4): a JSON object mapping
1304
- * hook names to shell command templates.
1305
- */
1306
- const readHooks = (path) => {
1307
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
1308
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
1309
- Object.values(parsed).some(value => typeof value !== 'string')) {
1310
- throw new Error(`${path}: hooks must be a JSON object of name → shell template strings`);
1311
- }
1312
- return parsed;
1327
+ });
1313
1328
  };
1314
1329
  /** Text rendering of a spec §25.2 execution report. */
1315
1330
  const renderActReport = (report) => {
@@ -1328,17 +1343,16 @@ const renderActReport = (report) => {
1328
1343
  ];
1329
1344
  let code = 0;
1330
1345
  if (report.hook !== undefined) {
1331
- if (!report.hook.fired) {
1332
- lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`);
1346
+ if (report.hook.error !== undefined) {
1347
+ // Effects may already be committed even when hook lookup failed before spawn.
1348
+ lines.push(`hook ${report.hook.name}: ${report.hook.error}`);
1349
+ code = 1;
1333
1350
  }
1334
- else if (report.hook.error === undefined) {
1335
- lines.push(`hook ${report.hook.name}: ok`);
1351
+ else if (!report.hook.fired) {
1352
+ lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`);
1336
1353
  }
1337
1354
  else {
1338
- // The claims committed before the hook ran (spec §25.4) — the
1339
- // failure is reported, and the exit code carries it.
1340
- lines.push(`hook ${report.hook.name}: ${report.hook.error}`);
1341
- code = 1;
1355
+ lines.push(`hook ${report.hook.name}: ok`);
1342
1356
  }
1343
1357
  }
1344
1358
  return { lines, code };
@@ -1367,7 +1381,7 @@ export const actCommand = (argv) => {
1367
1381
  values.list === true ? 'read' :
1368
1382
  values.retract !== undefined ? 'write' :
1369
1383
  values['dry-run'] === true ? 'scratch' : 'write');
1370
- try {
1384
+ return withCommandStore(store, () => {
1371
1385
  if (values.declare === true) {
1372
1386
  const declaration = declareActions(store, readInput(positionals));
1373
1387
  const err = declaration.problems.map(problem => `line ${problem.line}: ${problem.message}`);
@@ -1413,7 +1427,13 @@ export const actCommand = (argv) => {
1413
1427
  if (at <= 0) {
1414
1428
  return fail(`cave act: expected param=value, got ${JSON.stringify(pair)}\n`);
1415
1429
  }
1416
- args[pair.slice(0, at)] = pair.slice(at + 1);
1430
+ const parameter = pair.slice(0, at);
1431
+ if (Object.hasOwn(args, parameter)) {
1432
+ return fail(`cave act: duplicate parameter ${JSON.stringify(parameter)}\n`);
1433
+ }
1434
+ Object.defineProperty(args, parameter, {
1435
+ value: pair.slice(at + 1), enumerable: true, writable: true, configurable: true
1436
+ });
1417
1437
  }
1418
1438
  const hooksPath = values.hooks ?? process.env['CAVE_HOOKS'];
1419
1439
  const report = act(store, name, args, {
@@ -1429,13 +1449,7 @@ export const actCommand = (argv) => {
1429
1449
  return report.ok ?
1430
1450
  { code, out: `${lines.join('\n')}\n`, err: '' } :
1431
1451
  { code, out: '', err: `${lines.join('\n')}\n` };
1432
- }
1433
- catch (error) {
1434
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1435
- }
1436
- finally {
1437
- store.close();
1438
- }
1452
+ });
1439
1453
  };
1440
1454
  /** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
1441
1455
  const renderReport = (report, staleDays) => {
@@ -1444,26 +1458,38 @@ const renderReport = (report, staleDays) => {
1444
1458
  lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`);
1445
1459
  if (report.violations.length > 0) {
1446
1460
  lines.push(`violations (${report.violations.length}):`);
1447
- lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`));
1461
+ for (const violation of report.violations)
1462
+ lines.push(` ${formatViolation(violation)}`);
1448
1463
  }
1449
1464
  if (report.stale.length > 0) {
1450
1465
  lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`);
1451
- lines.push(...report.stale.map(({ row, ageDays }) => ` ${row.raw_line} (${ageDays}d)`));
1466
+ for (const { row, ageDays } of report.stale)
1467
+ lines.push(` ${row.raw_line} (${ageDays}d)`);
1452
1468
  }
1453
1469
  if (report.review.length > 0) {
1454
1470
  lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`);
1455
- lines.push(...report.review.map(row => ` ${row.raw_line}`));
1471
+ for (const row of report.review)
1472
+ lines.push(` ${row.raw_line}`);
1456
1473
  }
1457
1474
  if (report.disagreements.length > 0) {
1458
1475
  lines.push(`alias disagreements (${report.disagreements.length}):`);
1459
1476
  for (const disagreement of report.disagreements) {
1460
1477
  lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`);
1461
- lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`));
1478
+ for (const row of disagreement.rows)
1479
+ lines.push(` ${row.raw_line}`);
1462
1480
  }
1463
1481
  }
1482
+ const confidencePercent = (value) => {
1483
+ const rounded = Math.round(value * 100);
1484
+ if (value > 0 && rounded === 0)
1485
+ return '<1%';
1486
+ if (value < 1 && rounded === 100)
1487
+ return '>99%';
1488
+ return `${rounded}%`;
1489
+ };
1464
1490
  const confidence = coverage.averageConfidence === null ?
1465
1491
  '' :
1466
- `; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`;
1492
+ `; avg conf ${confidencePercent(coverage.averageConfidence)}, ${coverage.lowConfidence} low (< 0.3)`;
1467
1493
  lines.push(`coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
1468
1494
  `${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
1469
1495
  `${confidence}; ${coverage.entities} entities, ${coverage.typedEntities} typed`);
@@ -1485,17 +1511,14 @@ export const checkCommand = (argv) => {
1485
1511
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`);
1486
1512
  }
1487
1513
  const store = openDb(values, 'read');
1488
- try {
1514
+ return withCommandStore(store, () => {
1489
1515
  const report = caveCheck(store, { staleDays });
1490
1516
  const out = values.json === true ?
1491
1517
  `${stableJson(store, report)}\n` :
1492
1518
  renderReport(report, staleDays);
1493
1519
  // Violations fail the check (spec §20.2); the other sections are advisory.
1494
1520
  return { code: report.violations.length > 0 ? 1 : 0, out, err: '' };
1495
- }
1496
- finally {
1497
- store.close();
1498
- }
1521
+ });
1499
1522
  };
1500
1523
  const snapshotLine = (action, snapshot) => `${action} exact backup (${snapshot.rows} row(s), schema v${snapshot.schemaVersion}, ` +
1501
1524
  `${snapshot.bytes} bytes, sha256:${snapshot.sha256}) at ${snapshot.path}\n`;
@@ -1533,19 +1556,17 @@ export const backupCommand = (argv) => {
1533
1556
  // the destination aliases the source: refuse before the file could be
1534
1557
  // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1535
1558
  // the API's own check).
1536
- if (sameFile(dbPath, values.out)) {
1559
+ if (sameStoreFile(dbPath, values.out)) {
1537
1560
  return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1538
1561
  }
1562
+ const destination = values.out;
1539
1563
  const store = openAt(dbPath, { intent: 'read', assemble: assembleSources });
1540
- try {
1541
- return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })));
1542
- }
1543
- finally {
1544
- store.close();
1545
- }
1564
+ return withCommandStore(store, () => {
1565
+ return ok(snapshotLine('created', backupStore(store, destination, { force: values.force === true })));
1566
+ });
1546
1567
  }
1547
1568
  catch (error) {
1548
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1569
+ return fail(`${errorMessage(error)}\n`);
1549
1570
  }
1550
1571
  };
1551
1572
  /** `cave restore` — verify and atomically restore an exact snapshot (§13.2.2). */
@@ -1575,7 +1596,7 @@ export const restoreCommand = (argv) => {
1575
1596
  })));
1576
1597
  }
1577
1598
  catch (error) {
1578
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1599
+ return fail(`${errorMessage(error)}\n`);
1579
1600
  }
1580
1601
  };
1581
1602
  /**
@@ -1610,15 +1631,15 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1610
1631
  return fail(`cave suggest-alias: --min expects a score in 0..1 or N%, got ${JSON.stringify(values.min)}\n`);
1611
1632
  }
1612
1633
  const limit = values.limit === undefined ? undefined : Number(values.limit);
1613
- if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
1614
- return fail(`cave suggest-alias: --limit expects a positive integer, got ${JSON.stringify(values.limit)}\n`);
1634
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
1635
+ return fail(`cave suggest-alias: --limit expects a positive safe integer, got ${JSON.stringify(values.limit)}\n`);
1615
1636
  }
1616
1637
  const timeoutSeconds = values.timeout === undefined ? undefined : Number(values.timeout);
1617
1638
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1618
1639
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1619
1640
  }
1620
1641
  const store = openDb(values, values.write === true ? 'write' : 'read');
1621
- try {
1642
+ return await withAsyncCommandStore(store, async () => {
1622
1643
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } });
1623
1644
  if (values.agent !== undefined && suggestions.length > 0) {
1624
1645
  const reply = await shellComplete(values.agent, {
@@ -1628,7 +1649,9 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1628
1649
  suggestions = parseJudgeReply(reply, suggestions.length).map(index => suggestions[index]);
1629
1650
  }
1630
1651
  if (values.json === true) {
1631
- return ok(`${JSON.stringify(suggestions, undefined, 2)}\n`);
1652
+ const result = values.write === true ?
1653
+ { suggestions, ...writeSuggestions(store, suggestions) } : suggestions;
1654
+ return ok(`${JSON.stringify(result, undefined, 2)}\n`);
1632
1655
  }
1633
1656
  if (suggestions.length === 0) {
1634
1657
  return ok('no alias suggestions\n');
@@ -1639,13 +1662,10 @@ export const suggestAliasCommand = async (argv, runtime = {}) => {
1639
1662
  return ok(`${lines.join('\n')}\nappended ${appended} suggested alias claim(s)\n`);
1640
1663
  }
1641
1664
  return ok(`${lines.join('\n')}\n`);
1642
- }
1643
- finally {
1644
- store.close();
1645
- }
1665
+ });
1646
1666
  }
1647
1667
  catch (error) {
1648
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1668
+ return fail(`${errorMessage(error)}\n`);
1649
1669
  }
1650
1670
  };
1651
1671
  /**
@@ -1674,7 +1694,7 @@ export const syncCommand = (argv) => {
1674
1694
  }
1675
1695
  const dbPath = values.db ?? defaultDbPath();
1676
1696
  const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write');
1677
- try {
1697
+ return withCommandStore(store, () => {
1678
1698
  const options = {
1679
1699
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
1680
1700
  record: values['no-record'] !== true,
@@ -1688,7 +1708,7 @@ export const syncCommand = (argv) => {
1688
1708
  return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' };
1689
1709
  }
1690
1710
  if (report.problems.length > 0) {
1691
- const detail = report.problems.map(problem => ` line ${problem.line}: ${problem.message}`).join('\n');
1711
+ const detail = report.problems.map(problem => ` ${problem.line === 0 ? '' : `line ${problem.line}: `}${problem.message}`).join('\n');
1692
1712
  return fail(`cave sync: ${source}: ${report.problems.length} problem(s), nothing merged\n${detail}\n`);
1693
1713
  }
1694
1714
  const lines = [
@@ -1700,18 +1720,13 @@ export const syncCommand = (argv) => {
1700
1720
  lines.push(`record: ${report.record}`);
1701
1721
  }
1702
1722
  return ok(`${lines.join('\n')}\n`);
1703
- }
1704
- catch (error) {
1705
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1706
- }
1707
- finally {
1708
- store.close();
1709
- }
1723
+ });
1710
1724
  };
1711
1725
  /**
1712
1726
  * `cave report` — the §31 deliverable: a markdown template's cave-q
1713
- * blocks and inline splices render from the store, every stated fact
1714
- * cited back to its claim. Problems mark the text, land on stderr with
1727
+ * blocks and inline splices render from the store, with matched rows
1728
+ * cited back to their claims. Ordinary prose is not automatically checked
1729
+ * or cited. Problems mark the text, land on stderr with
1715
1730
  * template line numbers, and fail the exit code — the render never
1716
1731
  * silently drops a fact.
1717
1732
  */
@@ -1730,16 +1745,22 @@ export const reportCommand = (argv) => {
1730
1745
  },
1731
1746
  allowPositionals: true
1732
1747
  });
1733
- const template = readInput(positionals);
1748
+ if (values.at !== undefined && Time.parseInstant(values.at) === undefined) {
1749
+ return fail(`cave report: --at cannot parse at anchor ${JSON.stringify(values.at)}\n`);
1750
+ }
1751
+ if (values['as-of'] !== undefined && QuerySql.asOfBoundary(values['as-of']) === undefined) {
1752
+ return fail(`cave report: --as-of cannot parse as-of boundary ${JSON.stringify(values['as-of'])}\n`);
1753
+ }
1734
1754
  const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum);
1735
1755
  if (maximum === undefined) {
1736
1756
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`);
1737
1757
  }
1738
- if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1758
+ if (values.out !== undefined && sameStoreFile(values.db ?? defaultDbPath(), values.out)) {
1739
1759
  return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1740
1760
  }
1761
+ const template = readInput(positionals);
1741
1762
  const store = openDb(values, 'read');
1742
- try {
1763
+ return withCommandStore(store, () => {
1743
1764
  const rendered = caveReport(store, template, {
1744
1765
  aliases: values.aliases === true,
1745
1766
  resolve: values.resolve === true,
@@ -1754,19 +1775,13 @@ export const reportCommand = (argv) => {
1754
1775
  if (values.out === undefined) {
1755
1776
  return { code, out: rendered.markdown, err: err === '' ? '' : `${err}\n` };
1756
1777
  }
1757
- writeFileSync(values.out, rendered.markdown);
1778
+ writeOutput(values.out, rendered.markdown);
1758
1779
  return {
1759
1780
  code,
1760
1781
  out: `rendered ${rendered.citations} citation(s) to ${values.out}\n`,
1761
1782
  err: err === '' ? '' : `${err}\n`
1762
1783
  };
1763
- }
1764
- catch (error) {
1765
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1766
- }
1767
- finally {
1768
- store.close();
1769
- }
1784
+ });
1770
1785
  };
1771
1786
  /**
1772
1787
  * True when both paths name the same file: equal once resolved, or — when
@@ -1786,6 +1801,21 @@ const sameFile = (a, b) => {
1786
1801
  return false;
1787
1802
  }
1788
1803
  };
1804
+ /** Protect every physical file SQLite uses, including through a database symlink. */
1805
+ const sameStoreFile = (db, output) => {
1806
+ if (sameFile(db, output))
1807
+ return true;
1808
+ try {
1809
+ if (kindOf(db) !== 'sqlite')
1810
+ return false;
1811
+ const canonicalOutput = join(realpathSync(dirname(resolve(output))), basename(output));
1812
+ return [...new Set([db, realpathSync(db)])].some(base => ['-wal', '-shm', '-journal'].some(suffix => sameFile(`${base}${suffix}`, output) || sameFile(`${base}${suffix}`, canonicalOutput)));
1813
+ }
1814
+ catch {
1815
+ // The normal store-open path reports missing or unreadable source files.
1816
+ return false;
1817
+ }
1818
+ };
1789
1819
  export const exportCommand = (argv) => {
1790
1820
  const { values } = parseArgs({
1791
1821
  args: [...argv],
@@ -1804,28 +1834,22 @@ export const exportCommand = (argv) => {
1804
1834
  if (maximum === undefined) {
1805
1835
  return fail(`cave export: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`);
1806
1836
  }
1807
- if (values.out !== undefined && sameFile(db, values.out)) {
1837
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1808
1838
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1809
1839
  }
1810
1840
  const store = openDb(values, 'read');
1811
- try {
1841
+ return withCommandStore(store, () => {
1812
1842
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum });
1813
1843
  if (values.out === undefined) {
1814
1844
  return ok(text);
1815
1845
  }
1816
- writeFileSync(values.out, text);
1846
+ writeOutput(values.out, text);
1817
1847
  // Root claims only: shorthand prefixes and §28.4 annotations are not
1818
1848
  // claims, while qualifier/grouping leaves have a materialized parent.
1819
1849
  const claims = parseDocument(text).lines
1820
1850
  .filter(line => line.kind === 'claim' && line.parent === undefined).length;
1821
1851
  return ok(`exported ${claims} claim(s) to ${values.out}\n`);
1822
- }
1823
- catch (error) {
1824
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1825
- }
1826
- finally {
1827
- store.close();
1828
- }
1852
+ });
1829
1853
  };
1830
1854
  /** `cave generate` — spec §20.4's versioned TypeScript client artifact. */
1831
1855
  export const generateCommand = (argv) => {
@@ -1844,11 +1868,11 @@ export const generateCommand = (argv) => {
1844
1868
  return fail(`cave generate: --version expects a positive integer, got ${JSON.stringify(values.version)}\n`);
1845
1869
  }
1846
1870
  const db = values.db ?? defaultDbPath();
1847
- if (values.out !== undefined && sameFile(db, values.out)) {
1871
+ if (values.out !== undefined && sameStoreFile(db, values.out)) {
1848
1872
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`);
1849
1873
  }
1850
1874
  const store = openDb(values, 'read');
1851
- try {
1875
+ return withCommandStore(store, () => {
1852
1876
  const generated = generateClient(store, version === undefined ? {} : { version });
1853
1877
  if (!generated.ok) {
1854
1878
  return fail(`cave generate: schema cannot be generated:\n${generated.problems.map(problem => ` ${problem}`).join('\n')}\n`);
@@ -1856,16 +1880,10 @@ export const generateCommand = (argv) => {
1856
1880
  if (values.out === undefined) {
1857
1881
  return ok(generated.code);
1858
1882
  }
1859
- writeFileSync(values.out, generated.code);
1883
+ writeOutput(values.out, generated.code);
1860
1884
  return ok(`generated typed client v${generated.version} (${generated.fields.length} field(s), ` +
1861
1885
  `sha256:${generated.digest}) to ${values.out}\n`);
1862
- }
1863
- catch (error) {
1864
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1865
- }
1866
- finally {
1867
- store.close();
1868
- }
1886
+ });
1869
1887
  };
1870
1888
  /**
1871
1889
  * `cave reconstruct` — the §18 loop over the store: heuristic policy by
@@ -1911,7 +1929,7 @@ export const reconstructCommand = async (argv, runtime = {}) => {
1911
1929
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`);
1912
1930
  }
1913
1931
  const store = openDb(values, 'read');
1914
- try {
1932
+ return await withAsyncCommandStore(store, async () => {
1915
1933
  const graph = sqliteStore(store);
1916
1934
  const result = values.agent === undefined ?
1917
1935
  reconstruct(graph, heuristicPolicy(budgets), positionals) :
@@ -1928,13 +1946,10 @@ export const reconstructCommand = async (argv, runtime = {}) => {
1928
1946
  ...result.claims.map(claim => emitClaim(claim))
1929
1947
  ];
1930
1948
  return ok(lines.length === 0 ? '' : `${lines.join('\n')}\n`);
1931
- }
1932
- finally {
1933
- store.close();
1934
- }
1949
+ });
1935
1950
  }
1936
1951
  catch (error) {
1937
- return fail(`${error instanceof Error ? error.message : String(error)}\n`);
1952
+ return fail(`${errorMessage(error)}\n`);
1938
1953
  }
1939
1954
  };
1940
1955
  /**