@cavelang/cli 0.27.14 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/Authors.md +5 -0
  2. package/License.md +6 -0
  3. package/README.md +122 -24
  4. package/dist/internal/act/action.d.ts +69 -0
  5. package/dist/internal/act/action.d.ts.map +1 -0
  6. package/dist/internal/act/action.js +201 -0
  7. package/dist/internal/act/action.js.map +1 -0
  8. package/dist/internal/act/declare.d.ts +102 -0
  9. package/dist/internal/act/declare.d.ts.map +1 -0
  10. package/dist/internal/act/declare.js +270 -0
  11. package/dist/internal/act/declare.js.map +1 -0
  12. package/dist/internal/act/engine.d.ts +103 -0
  13. package/dist/internal/act/engine.d.ts.map +1 -0
  14. package/dist/internal/act/engine.js +298 -0
  15. package/dist/internal/act/engine.js.map +1 -0
  16. package/dist/internal/act/index.d.ts +27 -0
  17. package/dist/internal/act/index.d.ts.map +1 -0
  18. package/dist/internal/act/index.js +25 -0
  19. package/dist/internal/act/index.js.map +1 -0
  20. package/dist/internal/automate/automation.d.ts +78 -0
  21. package/dist/internal/automate/automation.d.ts.map +1 -0
  22. package/dist/internal/automate/automation.js +135 -0
  23. package/dist/internal/automate/automation.js.map +1 -0
  24. package/dist/internal/automate/declare.d.ts +84 -0
  25. package/dist/internal/automate/declare.d.ts.map +1 -0
  26. package/dist/internal/automate/declare.js +233 -0
  27. package/dist/internal/automate/declare.js.map +1 -0
  28. package/dist/internal/automate/engine.d.ts +125 -0
  29. package/dist/internal/automate/engine.d.ts.map +1 -0
  30. package/dist/internal/automate/engine.js +388 -0
  31. package/dist/internal/automate/engine.js.map +1 -0
  32. package/dist/internal/automate/index.d.ts +28 -0
  33. package/dist/internal/automate/index.d.ts.map +1 -0
  34. package/dist/internal/automate/index.js +26 -0
  35. package/dist/internal/automate/index.js.map +1 -0
  36. package/dist/internal/automate/main.d.ts +26 -0
  37. package/dist/internal/automate/main.d.ts.map +1 -0
  38. package/dist/internal/automate/main.js +298 -0
  39. package/dist/internal/automate/main.js.map +1 -0
  40. package/dist/internal/connect/index.d.ts +26 -0
  41. package/dist/internal/connect/index.d.ts.map +1 -0
  42. package/dist/internal/connect/index.js +24 -0
  43. package/dist/internal/connect/index.js.map +1 -0
  44. package/dist/internal/connect/main.d.ts +23 -0
  45. package/dist/internal/connect/main.d.ts.map +1 -0
  46. package/dist/internal/connect/main.js +330 -0
  47. package/dist/internal/connect/main.js.map +1 -0
  48. package/dist/internal/connect/run.d.ts +85 -0
  49. package/dist/internal/connect/run.d.ts.map +1 -0
  50. package/dist/internal/connect/run.js +242 -0
  51. package/dist/internal/connect/run.js.map +1 -0
  52. package/dist/internal/connect/source.d.ts +44 -0
  53. package/dist/internal/connect/source.d.ts.map +1 -0
  54. package/dist/internal/connect/source.js +215 -0
  55. package/dist/internal/connect/source.js.map +1 -0
  56. package/dist/internal/connect/template.d.ts +69 -0
  57. package/dist/internal/connect/template.d.ts.map +1 -0
  58. package/dist/internal/connect/template.js +221 -0
  59. package/dist/internal/connect/template.js.map +1 -0
  60. package/dist/internal/eval/index.d.ts +28 -0
  61. package/dist/internal/eval/index.d.ts.map +1 -0
  62. package/dist/internal/eval/index.js +27 -0
  63. package/dist/internal/eval/index.js.map +1 -0
  64. package/dist/internal/eval/judge.d.ts +30 -0
  65. package/dist/internal/eval/judge.d.ts.map +1 -0
  66. package/dist/internal/eval/judge.js +101 -0
  67. package/dist/internal/eval/judge.js.map +1 -0
  68. package/dist/internal/eval/loop.d.ts +64 -0
  69. package/dist/internal/eval/loop.d.ts.map +1 -0
  70. package/dist/internal/eval/loop.js +123 -0
  71. package/dist/internal/eval/loop.js.map +1 -0
  72. package/dist/internal/eval/main.d.ts +17 -0
  73. package/dist/internal/eval/main.d.ts.map +1 -0
  74. package/dist/internal/eval/main.js +236 -0
  75. package/dist/internal/eval/main.js.map +1 -0
  76. package/dist/internal/eval/queries.d.ts +74 -0
  77. package/dist/internal/eval/queries.d.ts.map +1 -0
  78. package/dist/internal/eval/queries.js +161 -0
  79. package/dist/internal/eval/queries.js.map +1 -0
  80. package/dist/internal/eval/run.d.ts +143 -0
  81. package/dist/internal/eval/run.d.ts.map +1 -0
  82. package/dist/internal/eval/run.js +376 -0
  83. package/dist/internal/eval/run.js.map +1 -0
  84. package/dist/internal/eval/score.d.ts +81 -0
  85. package/dist/internal/eval/score.d.ts.map +1 -0
  86. package/dist/internal/eval/score.js +126 -0
  87. package/dist/internal/eval/score.js.map +1 -0
  88. package/dist/internal/eval/suite.d.ts +66 -0
  89. package/dist/internal/eval/suite.d.ts.map +1 -0
  90. package/dist/internal/eval/suite.js +130 -0
  91. package/dist/internal/eval/suite.js.map +1 -0
  92. package/dist/internal/ingest/context.d.ts +20 -0
  93. package/dist/internal/ingest/context.d.ts.map +1 -0
  94. package/dist/internal/ingest/context.js +59 -0
  95. package/dist/internal/ingest/context.js.map +1 -0
  96. package/dist/internal/ingest/files.d.ts +46 -0
  97. package/dist/internal/ingest/files.d.ts.map +1 -0
  98. package/dist/internal/ingest/files.js +115 -0
  99. package/dist/internal/ingest/files.js.map +1 -0
  100. package/dist/internal/ingest/index.d.ts +31 -0
  101. package/dist/internal/ingest/index.d.ts.map +1 -0
  102. package/dist/internal/ingest/index.js +29 -0
  103. package/dist/internal/ingest/index.js.map +1 -0
  104. package/dist/internal/ingest/main.d.ts +12 -0
  105. package/dist/internal/ingest/main.d.ts.map +1 -0
  106. package/dist/internal/ingest/main.js +180 -0
  107. package/dist/internal/ingest/main.js.map +1 -0
  108. package/dist/internal/ingest/prompt.d.ts +32 -0
  109. package/dist/internal/ingest/prompt.d.ts.map +1 -0
  110. package/dist/internal/ingest/prompt.js +75 -0
  111. package/dist/internal/ingest/prompt.js.map +1 -0
  112. package/dist/internal/ingest/run.d.ts +142 -0
  113. package/dist/internal/ingest/run.d.ts.map +1 -0
  114. package/dist/internal/ingest/run.js +350 -0
  115. package/dist/internal/ingest/run.js.map +1 -0
  116. package/dist/internal/ingest/web.d.ts +48 -0
  117. package/dist/internal/ingest/web.d.ts.map +1 -0
  118. package/dist/internal/ingest/web.js +140 -0
  119. package/dist/internal/ingest/web.js.map +1 -0
  120. package/dist/internal/loop/demo.d.ts +22 -0
  121. package/dist/internal/loop/demo.d.ts.map +1 -0
  122. package/dist/internal/loop/demo.js +67 -0
  123. package/dist/internal/loop/demo.js.map +1 -0
  124. package/dist/internal/loop/index.d.ts +20 -0
  125. package/dist/internal/loop/index.d.ts.map +1 -0
  126. package/dist/internal/loop/index.js +17 -0
  127. package/dist/internal/loop/index.js.map +1 -0
  128. package/dist/internal/loop/llm.d.ts +99 -0
  129. package/dist/internal/loop/llm.d.ts.map +1 -0
  130. package/dist/internal/loop/llm.js +231 -0
  131. package/dist/internal/loop/llm.js.map +1 -0
  132. package/dist/internal/loop/reconstruct.d.ts +87 -0
  133. package/dist/internal/loop/reconstruct.d.ts.map +1 -0
  134. package/dist/internal/loop/reconstruct.js +157 -0
  135. package/dist/internal/loop/reconstruct.js.map +1 -0
  136. package/dist/internal/loop/sqlite.d.ts +13 -0
  137. package/dist/internal/loop/sqlite.d.ts.map +1 -0
  138. package/dist/internal/loop/sqlite.js +33 -0
  139. package/dist/internal/loop/sqlite.js.map +1 -0
  140. package/dist/internal/loop/store.d.ts +50 -0
  141. package/dist/internal/loop/store.d.ts.map +1 -0
  142. package/dist/internal/loop/store.js +100 -0
  143. package/dist/internal/loop/store.js.map +1 -0
  144. package/dist/internal/mcp/bin.d.ts +7 -0
  145. package/dist/internal/mcp/bin.d.ts.map +1 -0
  146. package/dist/internal/mcp/bin.js +8 -0
  147. package/dist/internal/mcp/bin.js.map +1 -0
  148. package/dist/internal/mcp/index.d.ts +25 -0
  149. package/dist/internal/mcp/index.d.ts.map +1 -0
  150. package/dist/internal/mcp/index.js +23 -0
  151. package/dist/internal/mcp/index.js.map +1 -0
  152. package/dist/internal/mcp/main.d.ts +23 -0
  153. package/dist/internal/mcp/main.d.ts.map +1 -0
  154. package/dist/internal/mcp/main.js +156 -0
  155. package/dist/internal/mcp/main.js.map +1 -0
  156. package/dist/internal/mcp/server.d.ts +92 -0
  157. package/dist/internal/mcp/server.d.ts.map +1 -0
  158. package/dist/internal/mcp/server.js +251 -0
  159. package/dist/internal/mcp/server.js.map +1 -0
  160. package/dist/internal/mcp/tools.d.ts +91 -0
  161. package/dist/internal/mcp/tools.d.ts.map +1 -0
  162. package/dist/internal/mcp/tools.js +584 -0
  163. package/dist/internal/mcp/tools.js.map +1 -0
  164. package/dist/internal/rules/declare.d.ts +58 -0
  165. package/dist/internal/rules/declare.d.ts.map +1 -0
  166. package/dist/internal/rules/declare.js +152 -0
  167. package/dist/internal/rules/declare.js.map +1 -0
  168. package/dist/internal/rules/engine.d.ts +125 -0
  169. package/dist/internal/rules/engine.d.ts.map +1 -0
  170. package/dist/internal/rules/engine.js +575 -0
  171. package/dist/internal/rules/engine.js.map +1 -0
  172. package/dist/internal/rules/index.d.ts +23 -0
  173. package/dist/internal/rules/index.d.ts.map +1 -0
  174. package/dist/internal/rules/index.js +21 -0
  175. package/dist/internal/rules/index.js.map +1 -0
  176. package/dist/internal/rules/rule.d.ts +88 -0
  177. package/dist/internal/rules/rule.d.ts.map +1 -0
  178. package/dist/internal/rules/rule.js +221 -0
  179. package/dist/internal/rules/rule.js.map +1 -0
  180. package/dist/internal/shape/check.d.ts +118 -0
  181. package/dist/internal/shape/check.d.ts.map +1 -0
  182. package/dist/internal/shape/check.js +331 -0
  183. package/dist/internal/shape/check.js.map +1 -0
  184. package/dist/internal/shape/client.d.ts +31 -0
  185. package/dist/internal/shape/client.d.ts.map +1 -0
  186. package/dist/internal/shape/client.js +170 -0
  187. package/dist/internal/shape/client.js.map +1 -0
  188. package/dist/internal/shape/gate.d.ts +33 -0
  189. package/dist/internal/shape/gate.d.ts.map +1 -0
  190. package/dist/internal/shape/gate.js +0 -0
  191. package/dist/internal/shape/gate.js.map +1 -0
  192. package/dist/internal/shape/index.d.ts +27 -0
  193. package/dist/internal/shape/index.d.ts.map +1 -0
  194. package/dist/internal/shape/index.js +23 -0
  195. package/dist/internal/shape/index.js.map +1 -0
  196. package/dist/internal/shape/suggest.d.ts +82 -0
  197. package/dist/internal/shape/suggest.d.ts.map +1 -0
  198. package/dist/internal/shape/suggest.js +374 -0
  199. package/dist/internal/shape/suggest.js.map +1 -0
  200. package/dist/internal/sync/index.d.ts +22 -0
  201. package/dist/internal/sync/index.d.ts.map +1 -0
  202. package/dist/internal/sync/index.js +21 -0
  203. package/dist/internal/sync/index.js.map +1 -0
  204. package/dist/internal/sync/sync.d.ts +95 -0
  205. package/dist/internal/sync/sync.d.ts.map +1 -0
  206. package/dist/internal/sync/sync.js +342 -0
  207. package/dist/internal/sync/sync.js.map +1 -0
  208. package/dist/internal/view/api.d.ts +196 -0
  209. package/dist/internal/view/api.d.ts.map +1 -0
  210. package/dist/internal/view/api.js +189 -0
  211. package/dist/internal/view/api.js.map +1 -0
  212. package/dist/internal/view/index.d.ts +33 -0
  213. package/dist/internal/view/index.d.ts.map +1 -0
  214. package/dist/internal/view/index.js +30 -0
  215. package/dist/internal/view/index.js.map +1 -0
  216. package/dist/internal/view/main.d.ts +11 -0
  217. package/dist/internal/view/main.d.ts.map +1 -0
  218. package/dist/internal/view/main.js +98 -0
  219. package/dist/internal/view/main.js.map +1 -0
  220. package/dist/internal/view/page.d.ts +14 -0
  221. package/dist/internal/view/page.d.ts.map +1 -0
  222. package/dist/internal/view/page.js +370 -0
  223. package/dist/internal/view/page.js.map +1 -0
  224. package/dist/internal/view/report.d.ts +52 -0
  225. package/dist/internal/view/report.d.ts.map +1 -0
  226. package/dist/internal/view/report.js +367 -0
  227. package/dist/internal/view/report.js.map +1 -0
  228. package/dist/internal/view/scope.d.ts +11 -0
  229. package/dist/internal/view/scope.d.ts.map +1 -0
  230. package/dist/internal/view/scope.js +41 -0
  231. package/dist/internal/view/scope.js.map +1 -0
  232. package/dist/internal/view/server.d.ts +35 -0
  233. package/dist/internal/view/server.d.ts.map +1 -0
  234. package/dist/internal/view/server.js +141 -0
  235. package/dist/internal/view/server.js.map +1 -0
  236. package/dist/src/act.d.ts +3 -0
  237. package/dist/src/act.d.ts.map +1 -0
  238. package/dist/src/act.js +3 -0
  239. package/dist/src/act.js.map +1 -0
  240. package/dist/src/automate.d.ts +3 -0
  241. package/dist/src/automate.d.ts.map +1 -0
  242. package/dist/src/automate.js +3 -0
  243. package/dist/src/automate.js.map +1 -0
  244. package/dist/src/cli.d.ts +24 -17
  245. package/dist/src/cli.d.ts.map +1 -1
  246. package/dist/src/cli.js +408 -107
  247. package/dist/src/cli.js.map +1 -1
  248. package/dist/src/commands.d.ts +104 -0
  249. package/dist/src/commands.d.ts.map +1 -0
  250. package/dist/src/commands.js +33 -0
  251. package/dist/src/commands.js.map +1 -0
  252. package/dist/src/connect.d.ts +3 -0
  253. package/dist/src/connect.d.ts.map +1 -0
  254. package/dist/src/connect.js +3 -0
  255. package/dist/src/connect.js.map +1 -0
  256. package/dist/src/dispatch.d.ts +14 -0
  257. package/dist/src/dispatch.d.ts.map +1 -0
  258. package/dist/src/dispatch.js +116 -0
  259. package/dist/src/dispatch.js.map +1 -0
  260. package/dist/src/doctor.d.ts +45 -0
  261. package/dist/src/doctor.d.ts.map +1 -0
  262. package/dist/src/doctor.js +284 -0
  263. package/dist/src/doctor.js.map +1 -0
  264. package/dist/src/eval.d.ts +3 -0
  265. package/dist/src/eval.d.ts.map +1 -0
  266. package/dist/src/eval.js +3 -0
  267. package/dist/src/eval.js.map +1 -0
  268. package/dist/src/index.d.ts +9 -2
  269. package/dist/src/index.d.ts.map +1 -1
  270. package/dist/src/index.js +6 -2
  271. package/dist/src/index.js.map +1 -1
  272. package/dist/src/ingest.d.ts +3 -0
  273. package/dist/src/ingest.d.ts.map +1 -0
  274. package/dist/src/ingest.js +3 -0
  275. package/dist/src/ingest.js.map +1 -0
  276. package/dist/src/loop.d.ts +3 -0
  277. package/dist/src/loop.d.ts.map +1 -0
  278. package/dist/src/loop.js +3 -0
  279. package/dist/src/loop.js.map +1 -0
  280. package/dist/src/main.d.ts +1 -1
  281. package/dist/src/main.d.ts.map +1 -1
  282. package/dist/src/main.js +3 -75
  283. package/dist/src/main.js.map +1 -1
  284. package/dist/src/mcp.d.ts +3 -0
  285. package/dist/src/mcp.d.ts.map +1 -0
  286. package/dist/src/mcp.js +3 -0
  287. package/dist/src/mcp.js.map +1 -0
  288. package/dist/src/rules.d.ts +3 -0
  289. package/dist/src/rules.d.ts.map +1 -0
  290. package/dist/src/rules.js +3 -0
  291. package/dist/src/rules.js.map +1 -0
  292. package/dist/src/shape.d.ts +3 -0
  293. package/dist/src/shape.d.ts.map +1 -0
  294. package/dist/src/shape.js +3 -0
  295. package/dist/src/shape.js.map +1 -0
  296. package/dist/src/sync.d.ts +3 -0
  297. package/dist/src/sync.d.ts.map +1 -0
  298. package/dist/src/sync.js +3 -0
  299. package/dist/src/sync.js.map +1 -0
  300. package/dist/src/view.d.ts +3 -0
  301. package/dist/src/view.d.ts.map +1 -0
  302. package/dist/src/view.js +3 -0
  303. package/dist/src/view.js.map +1 -0
  304. package/package.json +81 -21
  305. package/src/act.ts +2 -0
  306. package/src/automate.ts +2 -0
  307. package/src/cli.ts +416 -102
  308. package/src/commands.ts +48 -0
  309. package/src/connect.ts +2 -0
  310. package/src/dispatch.ts +141 -0
  311. package/src/doctor.ts +370 -0
  312. package/src/eval.ts +2 -0
  313. package/src/index.ts +9 -2
  314. package/src/ingest.ts +2 -0
  315. package/src/loop.ts +2 -0
  316. package/src/main.ts +3 -66
  317. package/src/mcp.ts +2 -0
  318. package/src/rules.ts +2 -0
  319. package/src/shape.ts +2 -0
  320. package/src/sync.ts +2 -0
  321. package/src/view.ts +2 -0
package/src/cli.ts CHANGED
@@ -1,27 +1,31 @@
1
1
  /**
2
2
  * `cave` command implementation — pure functions from parsed arguments to
3
- * `{ code, out, err }`, so tests can call commands directly and `main.ts`
4
- * stays a thin dispatcher.
3
+ * `{ code, out, err }`, so tests can call commands directly while
4
+ * `dispatch.ts` gives every command one awaited process lifecycle.
5
5
  *
6
6
  * Commands:
7
7
  *
8
8
  * - `cave parse [file]` — lint / dump the AST (`--json`)
9
- * - `cave highlight [file]` — ANSI syntax colors (async, routed in `main.ts`)
9
+ * - `cave highlight [file]` — ANSI syntax colors (async)
10
10
  * - `cave add [--db <path>] [file…]` — ingest into a store (`--strict`, `--check`)
11
11
  * - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`, `--aliases`, `--as-of`, `--resolve`)
12
12
  * - `cave resolve [--db <path>]` — contested facts under the §26 policy (`--aliases`, `--policy`, `--json`)
13
13
  * - `cave derive [--db <path>] [rules.cave…]` — fire rules (`--dry-run`, `--full`, `--list`, `--retract`)
14
14
  * - `cave act [--db <path>] <name> [param=value…]` — execute an action (spec §25; `--declare`, `--list`, `--retract`)
15
- * - `cave automate [--db <path>]` — the event-driven loop (spec §29; async, routed in `main.ts`)
15
+ * - `cave automate [--db <path>]` — the event-driven loop (spec §29; async)
16
16
  * - `cave check [--db <path>]` — knowledge health report (`--stale`, `--json`)
17
- * - `cave suggest-alias [--db <path>]` — same-entity candidates (spec §27; async, routed in `main.ts`)
17
+ * - `cave backup [--db <path>] --out <path>` — exact verified SQLite snapshot
18
+ * - `cave restore <snapshot> --db <path>` — verify and atomically restore a snapshot
19
+ * - `cave generate [--db <path>]` — versioned TypeScript client from EXPECTS declarations
20
+ * - `cave suggest-alias [--db <path>]` — same-entity candidates (spec §27; async)
18
21
  * - `cave sync [--db <path>] <source>` — merge another store by row identity (spec §28; `--dry-run`, `--as`, `--into`)
19
22
  * - `cave export [--db <path>]` — canonical text out (`--current`, `--tx`)
20
- * - `cave serve [--db <path>]` — the human read surface in a browser (spec §30; async, routed in `main.ts`)
23
+ * - `cave serve [--db <path>]` — the human read surface in a browser (spec §30; async)
21
24
  * - `cave report [--db <path>] [template.md…]` — cited markdown from CAVE-Q template blocks (spec §31)
22
- * - `cave connect [--db <path>] <source>` — deterministic structured ingestion (async, routed in `main.ts`)
23
- * - `cave eval <suite...>` — golden-fixture extraction/query/reconstruction evals (async, routed in `main.ts`)
24
- * - `cave reconstruct [--db <path>] <seed...>` — §18 reconstruction from seed cues (async, routed in `main.ts`)
25
+ * - `cave connect [--db <path>] <source>` — deterministic structured ingestion (async)
26
+ * - `cave eval <suite...>` — golden-fixture extraction/query/reconstruction evals (async)
27
+ * - `cave reconstruct [--db <path>] <seed...>` — §18 reconstruction from seed cues (async)
28
+ * - `cave doctor [--db <path>]` — read-only runtime, installation, and store diagnostics
25
29
  * - `cave demo` — the cave-loop multi-hop recovery demo
26
30
  * - `cave version` — print the cave version
27
31
  * - `cave help [command]` — overview, or one command's help
@@ -30,17 +34,18 @@
30
34
  * `cave.db`. Every command answers `--help` with options and examples.
31
35
  */
32
36
 
33
- import { readFileSync, statSync, writeFileSync } from 'node:fs'
37
+ import { readFileSync, readSync, statSync, writeFileSync } from 'node:fs'
34
38
  import { resolve } from 'node:path'
35
39
  import { parseArgs } from 'node:util'
36
40
  import { Version } from '@cavelang/core'
37
41
  import { parseDocument } from '@cavelang/parser'
38
42
  import { Registry, standardRegistry } from '@cavelang/canonical'
39
- import { defaultDbPath, open } from '@cavelang/store'
40
- import { query as caveQuery } from '@cavelang/query'
43
+ import { Sensitivity, backup as backupStore, defaultDbPath, open, restoreBackup, verifyBackup } from '@cavelang/store'
44
+ import type { Store } from '@cavelang/store'
45
+ import { defaultLimit as defaultQueryLimit, page as caveQueryPage } from '@cavelang/query'
41
46
  import {
42
47
  check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
43
- suggestAliases, writeSuggestions
48
+ generateClient, suggestAliases, writeSuggestions
44
49
  } from '@cavelang/shape'
45
50
  import type { Report, Violation } from '@cavelang/shape'
46
51
  import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/rules'
@@ -54,6 +59,8 @@ import { labelOf, sanitizeLabel, syncFile, syncText } from '@cavelang/sync'
54
59
  import type { SyncReport } from '@cavelang/sync'
55
60
  import { report as caveReport } from '@cavelang/view'
56
61
  import { emitClaim } from '@cavelang/canonical'
62
+ import { doctorCommand } from './doctor.ts'
63
+ import { delegatedCommandNames } from './commands.ts'
57
64
 
58
65
  export type Output = {
59
66
  readonly code: number
@@ -61,6 +68,39 @@ export type Output = {
61
68
  readonly err: string
62
69
  }
63
70
 
71
+ const storageRow = (value: unknown): value is Parameters<Store['recordOf']>[0] =>
72
+ typeof value === 'object' && value !== null &&
73
+ typeof (value as Record<string, unknown>)['id'] === 'string' &&
74
+ typeof (value as Record<string, unknown>)['tx'] === 'string' &&
75
+ typeof (value as Record<string, unknown>)['claim_key'] === 'string' &&
76
+ typeof (value as Record<string, unknown>)['raw_line'] === 'string'
77
+
78
+ /** Recursively replace storage rows in JSON reports with cave.claim/v1 records. */
79
+ const stableJsonValue = (store: Store, value: unknown): unknown => {
80
+ if (storageRow(value)) {
81
+ const record = store.recordOf(value)
82
+ const ranked = value as typeof value & Record<string, unknown>
83
+ return typeof ranked['res_rank'] === 'number' ? {
84
+ ...record,
85
+ resolution: {
86
+ group: ranked['res_group'],
87
+ class: ranked['res_class'],
88
+ effectiveConfidence: ranked['res_conf'],
89
+ rank: ranked['res_rank'],
90
+ },
91
+ } : record
92
+ }
93
+ if (Array.isArray(value)) return value.map(entry => stableJsonValue(store, entry))
94
+ if (typeof value === 'object' && value !== null) {
95
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) =>
96
+ [key, stableJsonValue(store, entry)]))
97
+ }
98
+ return value
99
+ }
100
+
101
+ const stableJson = (store: Store, value: unknown): string =>
102
+ JSON.stringify(stableJsonValue(store, value), undefined, 2)
103
+
64
104
  const ok = (out: string): Output =>
65
105
  ({ code: 0, out, err: '' })
66
106
 
@@ -74,23 +114,27 @@ Usage:
74
114
  cave highlight [file...] print CAVE text with ANSI syntax colors
75
115
  cave add [--db <path>] [file...] ingest into a store [--strict] [--check] [--no-prelude] [--no-src]
76
116
  cave import [--db <path>] [file...] restore/merge from CAVE text (add without @src: stamping)
77
- cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
117
+ cave query [--db <path>] <pattern> run a bounded CAVE-Q page [--limit <n>] [--cursor <token>] [--json]
78
118
  cave resolve [--db <path>] contested facts + winners (spec §26) [--aliases] [--policy] [--json]
79
119
  cave derive [--db <path>] [rules.cave..] declare + fire rules (spec §24) [--dry-run] [--full] [--list] [--retract <rule>]
80
120
  cave act [--db <path>] <name> [p=v...] execute an action (spec §25) [--dry-run] [--no-check] [--hooks <file>]
81
121
  cave act --declare [file...] declare actions from a CAVE document; --list / --retract <name> manage them
82
122
  cave automate [--db <path>] event-driven loop (spec §29): new claims fire rules/actions/hooks/agent prompts [--once]
83
123
  cave check [--db <path>] knowledge health report (spec §20) [--stale <days>] [--json]
124
+ cave backup [--db <path>] --out <file> exact verified SQLite snapshot [--force]
125
+ cave restore <snapshot> --db <path> verify and atomically restore an exact snapshot [--force]
126
+ cave generate [--db <path>] generate a typed client from EXPECTS [--out <file>] [--version <n>]
84
127
  cave suggest-alias [--db <path>] propose same-entity ALIAS candidates (spec §27) [--min <s>] [--agent] [--write]
85
128
  cave sync [--db <path>] <source> merge another store by row identity (spec §28) [--dry-run] [--as] [--into]
86
- cave export [--db <path>] [--out <file>] emit canonical CAVE text [--current] [--tx] [--no-prelude]
87
- cave serve [--db <path>] browse the store in a browser (spec §30) [--port <n>] [--host <a>]
88
- cave report [--db <path>] [template...] render cited markdown from a CAVE-Q template (spec §31) [--out <file>] [--resolve]
129
+ cave export [--db <path>] [--out <file>] emit filtered canonical CAVE text [--max-sensitivity <level>]
130
+ cave serve [--db <path>] browse a filtered store (spec §30) [--max-sensitivity <level>]
131
+ cave report [--db <path>] [template...] render filtered cited markdown (spec §31) [--max-sensitivity <level>]
89
132
  cave mcp [--db <path>] serve the engine as an MCP server on stdio [--no-prelude]
90
133
  cave ingest [--db <path>] <globs/urls..> LLM-driven ingestion of files and web pages
91
- cave eval <suite..> --agent '<command>' golden-fixture extraction/query/reconstruction evals (items 9, 10)
134
+ cave eval <suite..> --agent '<command>' golden-fixture extraction/query/reconstruction evals
92
135
  cave connect <source> --map <file> deterministic structured ingestion (CSV/JSON/SQLite/URL, spec §23)
93
136
  cave reconstruct [--db <path>] <seed..> reconstruct memory from seed cues (spec §18) [--agent] [--query] [--trace]
137
+ cave doctor [--db <path>] diagnose runtime, installation, and store health [--hooks <file>] [--json]
94
138
  cave demo run the cave-loop reconstruction demo
95
139
  cave version print the cave version
96
140
  cave help [command] this text, or one command's options and examples
@@ -103,6 +147,26 @@ const dbHelp = `--db <path> database file (default: $CAVE_DB, or cave.db)`
103
147
 
104
148
  /** Per-command help, printed for \`cave <command> --help\` and \`cave help <command>\`. */
105
149
  export const commandHelp: Record<string, string> = {
150
+ doctor: `cave doctor — diagnose runtime, installation, and store health
151
+
152
+ Usage:
153
+ cave doctor [--db <path>] [--hooks <file>] [--json]
154
+
155
+ Options:
156
+ ${dbHelp}
157
+ --hooks <file> validate an optional hooks JSON file
158
+ --json emit the versioned cave.doctor/v1 report
159
+
160
+ The command is read-only: it never creates or migrates a database and never
161
+ runs hooks. Its output is safe to share — database and hooks paths, hook names
162
+ and commands, URLs, and environment values are not included. Warnings such as
163
+ a not-yet-created database exit 0; failed required checks exit 1.
164
+
165
+ Examples:
166
+ cave doctor
167
+ cave doctor --db knowledge.db
168
+ cave doctor --hooks hooks.json --json`,
169
+
106
170
  parse: `cave parse — lint CAVE text, or dump the parsed document
107
171
 
108
172
  Usage:
@@ -178,24 +242,28 @@ file read as ordinary comments; to merge under the recorded row
178
242
  identity, use \`cave sync\` (spec §28.4).
179
243
 
180
244
  Examples:
181
- cave export --db old.db --out backup.cave
245
+ cave export --db old.db --max-sensitivity restricted --out backup.cave
182
246
  cave import --db new.db backup.cave`,
183
247
 
184
248
  query: `cave query — run a CAVE-Q pattern against a store
185
249
 
186
250
  Usage:
187
- cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
251
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
188
252
 
189
253
  Options:
190
254
  ${dbHelp}
191
- --json emit matches as JSON
255
+ --json emit a versioned cave.query-page/v1 JSON object
256
+ --limit <n> matches per page (default ${defaultQueryLimit}, maximum 1000)
257
+ --cursor <t> continue the same frozen query snapshot
192
258
  --all match all beliefs, not just current ones
193
259
  --aliases resolve entities through current ALIAS claims (spec §13.6)
194
260
  --as-of <t> resolve beliefs as of a past moment (spec §12.3): a date
195
261
  (whole day included), a timestamp (whole second), or a
196
- transaction id — rows recorded later are invisible
262
+ transaction id — rows recorded later are invisible;
263
+ timestamps without an offset mean UTC
197
264
  --at <t> anchor in valid time (spec §32.4): a date-like period
198
- (read as its start instant) or a timestamp. Claims whose
265
+ (read as its start instant) or a timestamp (offset-less
266
+ means UTC). Claims whose
199
267
  time contexts — @2026-Q1 points, @2025..2028 ranges — do
200
268
  not cover the instant are invisible; timeless claims
201
269
  always match; trajectory values (20B -> 40B USD/yr)
@@ -355,7 +423,8 @@ Options:
355
423
  --no-prelude open the store without the standard verb registry
356
424
 
357
425
  Reads the store against its own in-band EXPECTS declarations (spec §20.1)
358
- and reports shape violations, stale current beliefs, review candidates
426
+ and reports actionable presence, #cardinality:one, and #unit:<unit>
427
+ violations, stale current beliefs, review candidates
359
428
  (conf 0.3–0.7), alias disagreements (spec §13.6) and coverage stats.
360
429
  Exits 1 when violations exist; everything else is advisory.
361
430
 
@@ -364,6 +433,69 @@ Examples:
364
433
  cave check --db k.db --stale 30
365
434
  cave check --db k.db --json | jq '.violations'`,
366
435
 
436
+ backup: `cave backup — exact, verified SQLite snapshot (spec §13.2.2)
437
+
438
+ Usage:
439
+ cave backup [--db <path>] --out <file> [--force]
440
+ cave backup --verify <file> [--sha256 <hex>]
441
+
442
+ Options:
443
+ ${dbHelp}
444
+ --out <file> destination snapshot (required when creating)
445
+ --force atomically replace an existing snapshot
446
+ --verify <file> verify integrity, foreign keys, schema and SHA-256 only
447
+ --sha256 <hex> expected digest for --verify
448
+
449
+ The source remains online. SQLite VACUUM INTO takes one consistent committed
450
+ snapshot, including WAL-visible rows; CAVE writes to a temporary file,
451
+ validates it, fsyncs it, then publishes it atomically. The summary SHA-256 is
452
+ the restore verification token.
453
+
454
+ Examples:
455
+ cave backup --db k.db --out backups/k.db
456
+ cave backup --verify backups/k.db --sha256 <hex>`,
457
+
458
+ restore: `cave restore — verify and atomically restore an exact SQLite snapshot (spec §13.2.2)
459
+
460
+ Usage:
461
+ cave restore <snapshot> --db <path> [--force] [--sha256 <hex>]
462
+
463
+ Options:
464
+ --db <path> destination database (required)
465
+ --force atomically replace an existing destination
466
+ --sha256 <hex> require the recorded backup digest
467
+
468
+ Stop every process using the destination first. Restore refuses WAL/SHM
469
+ sidecars, verifies the input and copied temporary database, then publishes one
470
+ atomic file. WAL, SHM, or rollback-journal sidecars cause a refusal. On
471
+ failure the prior destination remains untouched.
472
+
473
+ Example:
474
+ cave restore backups/k.db --db restored.db --sha256 <hex>`,
475
+
476
+ generate: `cave generate — deterministic TypeScript client from EXPECTS declarations (spec §20.4)
477
+
478
+ Usage:
479
+ cave generate [--db <path>] [--out <file>] [--version <n>] [--no-prelude]
480
+
481
+ Options:
482
+ ${dbHelp}
483
+ --out <file> write the generated module instead of stdout
484
+ --version <n> generated-client format version (default and currently
485
+ supported: 1)
486
+ --no-prelude open the store without the standard verb registry
487
+
488
+ Current positive EXPECTS claims become TypeScript interfaces plus store-backed
489
+ reader functions. #cardinality:one emits a scalar checked at runtime;
490
+ attributes preserve text, number and unit; relation readers honor declared
491
+ inverse verbs. The file embeds its format version and deterministic schema
492
+ SHA-256. Conflicting declarations, invalid cardinality/unit tags, unsupported
493
+ versions and generated type-name collisions fail without writing output.
494
+
495
+ Examples:
496
+ cave generate --db k.db > cave-client.ts
497
+ cave generate --db k.db --out src/generated/cave-client.ts`,
498
+
367
499
  'suggest-alias': `cave suggest-alias — propose same-entity ALIAS candidates for review (spec §27)
368
500
 
369
501
  Usage:
@@ -447,7 +579,7 @@ a merge driver on *.cave files.
447
579
  Examples:
448
580
  cave sync --db main.db laptop.db
449
581
  cave sync --db main.db laptop.db --dry-run --json
450
- cave export --db laptop.db --tx | cave sync --db main.db -
582
+ cave export --db laptop.db --tx --max-sensitivity restricted | cave sync --db main.db -
451
583
  cave sync --db a.db b.db && cave sync --db b.db a.db # converge both
452
584
  cave sync --db work.db knowledge.cave --no-record # checkout (§28.6)
453
585
  cave sync --db main.db knowledge.cave --as reorg-auth # land the branch`,
@@ -455,32 +587,43 @@ Examples:
455
587
  export: `cave export — emit canonical CAVE text from a store
456
588
 
457
589
  Usage:
458
- cave export [--db <path>] [--out <file>] [--current] [--tx] [--no-prelude]
590
+ cave export [--db <path>] [--out <file>] [--current] [--tx]
591
+ [--max-sensitivity <level>] [--no-prelude]
459
592
 
460
593
  Options:
461
594
  ${dbHelp}
462
595
  --out <file> write to a file instead of stdout — refused when it
463
596
  names the store's own database file
464
- --current current beliefs only (skip superseded rows)
597
+ --current current beliefs only (skip superseded rows); compacting
598
+ view, not sanitization or selective erasure (spec §9.6)
599
+ --max-sensitivity <level>
600
+ public, internal, confidential, or restricted (default
601
+ internal; restricted includes complete portable history)
465
602
  --tx precede every claim line with its ;@ transaction
466
603
  annotation (spec §28.4) — the text carries row identity,
467
604
  so cave sync replays it idempotently; other readers see
468
605
  ordinary comments
469
606
  --no-prelude open the store without the standard verb registry
470
607
 
608
+ Retention: claim history is permanent. Retraction and --current do not
609
+ guarantee erasure from the store, exports, peers, or backups (spec §9.6).
610
+
471
611
  Examples:
472
612
  cave export --db k.db
473
- cave export --db k.db --out backup.cave
613
+ cave export --db k.db --out internal-export.cave
614
+ cave export --db k.db --max-sensitivity restricted --out complete-history.cave
474
615
  cave export --db k.db --current --out snapshot.cave
475
- cave export --db k.db --tx | cave sync --db other.db -
476
- cave export --db work.db --tx --out knowledge.cave # the committed,
616
+ cave export --db k.db --tx --max-sensitivity restricted | cave sync --db other.db -
617
+ cave export --db work.db --tx --max-sensitivity restricted --out knowledge.cave
618
+ # the committed,
477
619
  # reviewable store text (spec §28.6)`,
478
620
 
479
621
  report: `cave report — render cited markdown from a CAVE-Q template (spec §31)
480
622
 
481
623
  Usage:
482
624
  cave report [--db <path>] [template.md...] [--out <file>]
483
- [--aliases] [--resolve] [--as-of <t>] [--at <t>] [--no-prelude]
625
+ [--aliases] [--resolve] [--as-of <t>] [--at <t>]
626
+ [--max-sensitivity <level>] [--no-prelude]
484
627
 
485
628
  Options:
486
629
  ${dbHelp}
@@ -493,6 +636,9 @@ Options:
493
636
  --at <t> anchor every query in valid time (spec §32.4): claims
494
637
  whose time contexts don't cover the instant drop out,
495
638
  trajectory values render interpolated
639
+ --max-sensitivity <level>
640
+ public, internal, confidential, or restricted (default
641
+ internal; malformed/unknown claim labels fail closed)
496
642
  --no-prelude open the store without the standard verb registry
497
643
 
498
644
  Reads stdin when no file (or \`-\`) is given. A template is ordinary
@@ -568,9 +714,27 @@ Usage:
568
714
  cave version`
569
715
  }
570
716
 
717
+ const readStdin = (): string => {
718
+ const chunks: Buffer[] = []
719
+ const buffer = Buffer.allocUnsafe(64 * 1024)
720
+ const pause = new Int32Array(new SharedArrayBuffer(4))
721
+ for (;;) {
722
+ try {
723
+ const size = readSync(0, buffer, 0, buffer.length, null)
724
+ if (size === 0) return Buffer.concat(chunks).toString('utf8')
725
+ chunks.push(Buffer.from(buffer.subarray(0, size)))
726
+ } catch (error) {
727
+ if ((error as NodeJS.ErrnoException).code !== 'EAGAIN') throw error
728
+ // A pipe may be non-blocking on macOS. The producer is another process,
729
+ // so a short synchronous retry does not prevent it from making progress.
730
+ Atomics.wait(pause, 0, 0, 5)
731
+ }
732
+ }
733
+ }
734
+
571
735
  const readInput = (files: readonly string[]): string =>
572
736
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
573
- readFileSync(0, 'utf8') :
737
+ readStdin() :
574
738
  files.map(file => readFileSync(file, 'utf8')).join('\n')
575
739
 
576
740
  export const parseCommand = (argv: readonly string[]): Output => {
@@ -602,8 +766,7 @@ export const parseCommand = (argv: readonly string[]): Output => {
602
766
 
603
767
  /**
604
768
  * `cave highlight` — ANSI syntax colors from the tree-sitter grammar's own
605
- * highlight query. Async (the grammar WASM loads on first use), so `main.ts`
606
- * routes it separately, like `mcp` and `ingest`.
769
+ * highlight query. Async because the grammar WASM loads on first use.
607
770
  */
608
771
  export const highlightCommand = async (argv: readonly string[]): Promise<Output> => {
609
772
  const { positionals } = parseArgs({ args: [...argv], options: {}, allowPositionals: true })
@@ -620,9 +783,25 @@ export const highlightCommand = async (argv: readonly string[]): Promise<Output>
620
783
  * replays interchange text, which must preserve claim keys as exported,
621
784
  * so it never stamps.
622
785
  */
786
+ const formatViolationProblem = (violation: Violation): string => {
787
+ const { entity, expectation, actualCount, actualUnits } = violation
788
+ if (actualCount === 0) {
789
+ return `${entity} missing ${expectation.kind} ${expectation.name}`
790
+ }
791
+ const problems: string[] = []
792
+ if (expectation.cardinality === 'one' && actualCount !== 1) {
793
+ problems.push(`${entity} has ${actualCount} ${expectation.kind}s ${expectation.name}; expected exactly one`)
794
+ }
795
+ if (expectation.unit !== undefined && actualUnits.some(unit => unit !== expectation.unit)) {
796
+ const units = actualUnits.map(unit => unit ?? '(none)').join(', ')
797
+ problems.push(`${entity} attribute ${expectation.name} has unit${actualUnits.length === 1 ? '' : 's'} ${units}; expected ${expectation.unit}`)
798
+ }
799
+ return problems.join('; ')
800
+ }
801
+
623
802
  const formatViolation = (violation: Violation): string =>
624
- `${violation.entity} missing ${violation.expectation.kind} ${violation.expectation.name} ` +
625
- `(${violation.entity} IS ${violation.via}; ${violation.expectation.type} EXPECTS ${violation.expectation.name})`
803
+ `${formatViolationProblem(violation)} ` +
804
+ `(${violation.entity} IS ${violation.via}; ${violation.expectation.row.raw_line})`
626
805
 
627
806
  const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Output => {
628
807
  const { values, positionals } = parseArgs({
@@ -693,6 +872,8 @@ export const queryCommand = (argv: readonly string[]): Output => {
693
872
  'as-of': { type: 'string' },
694
873
  at: { type: 'string' },
695
874
  resolve: { type: 'boolean' },
875
+ limit: { type: 'string' },
876
+ cursor: { type: 'string' },
696
877
  'no-prelude': { type: 'boolean' }
697
878
  },
698
879
  allowPositionals: true
@@ -703,18 +884,22 @@ export const queryCommand = (argv: readonly string[]): Output => {
703
884
  const pattern = positionals.join('\n')
704
885
  const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
705
886
  try {
706
- const matches = caveQuery(store, pattern, {
887
+ const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
888
+ const result = caveQueryPage(store, pattern, {
707
889
  all: values.all === true,
708
890
  aliases: values.aliases === true,
709
891
  resolve: values.resolve === true,
892
+ limit,
893
+ ...values.cursor === undefined ? {} : { cursor: values.cursor },
710
894
  ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
711
895
  ...values.at === undefined ? {} : { at: values.at }
712
896
  })
713
897
  if (values.json === true) {
714
- return ok(`${JSON.stringify(matches, undefined, 2)}\n`)
898
+ return ok(`${JSON.stringify(result, undefined, 2)}\n`)
715
899
  }
900
+ const matches = result.matches
716
901
  if (matches.length === 0) {
717
- return ok('no matches\n')
902
+ return ok(`no matches\n${result.next === undefined ? '' : `next: ${result.next}\n`}`)
718
903
  }
719
904
  const lines = matches.map(match => {
720
905
  const bindings = Object.entries(match.bindings)
@@ -722,14 +907,14 @@ export const queryCommand = (argv: readonly string[]): Output => {
722
907
  .join(' ')
723
908
  // A fully bound pattern has no bindings to print; transitive matches
724
909
  // additionally carry no row — confirm with the pattern itself.
725
- const line = bindings !== '' ? bindings : match.row?.raw_line ?? pattern.split('\n')[0]!
910
+ const line = bindings !== '' ? bindings : match.claim?.claim.raw ?? pattern.split('\n')[0]!
726
911
  // An interpolated trajectory shows its value at the anchor
727
912
  // (spec §32.4) — bindings already carry it in the value slot.
728
913
  return match.at !== undefined && bindings === '' ?
729
914
  `${line} ; at ${values.at}: ${match.at.text}` :
730
915
  line
731
916
  })
732
- return ok(`${lines.join('\n')}\n`)
917
+ return ok(`${lines.join('\n')}\n${result.next === undefined ? '' : `next: ${result.next}\n`}`)
733
918
  } catch (error) {
734
919
  return fail(`${error instanceof Error ? error.message : String(error)}\n`)
735
920
  } finally {
@@ -775,7 +960,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
775
960
  }
776
961
  const contested = store.contested({ aliases: values.aliases === true })
777
962
  if (values.json === true) {
778
- return ok(`${JSON.stringify(contested, undefined, 2)}\n`)
963
+ return ok(`${stableJson(store, contested)}\n`)
779
964
  }
780
965
  if (contested.length === 0) {
781
966
  return ok('no contested facts\n')
@@ -823,7 +1008,7 @@ export const deriveCommand = (argv: readonly string[]): Output => {
823
1008
  if (values.list === true) {
824
1009
  const rules = listRules(store)
825
1010
  if (values.json === true) {
826
- return ok(`${JSON.stringify(rules, undefined, 2)}\n`)
1011
+ return ok(`${stableJson(store, rules)}\n`)
827
1012
  }
828
1013
  if (rules.length === 0) {
829
1014
  return ok('no rules\n')
@@ -885,7 +1070,7 @@ export const deriveCommand = (argv: readonly string[]): Output => {
885
1070
  (declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''))
886
1071
  }
887
1072
  if (values.json === true) {
888
- return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
1073
+ return { code: report.problems.length > 0 || !report.complete ? 1 : 0, out: `${stableJson(store, report)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
889
1074
  }
890
1075
  for (const problem of report.problems) {
891
1076
  err.push(`${problem.subject}: ${problem.problems.join('; ')}`)
@@ -902,7 +1087,7 @@ export const deriveCommand = (argv: readonly string[]): Output => {
902
1087
  `derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
903
1088
  `+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
904
1089
  `${report.unchanged} unchanged (${report.passes} pass(es))`)
905
- const hasProblems = report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
1090
+ const hasProblems = !report.complete || report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
906
1091
  return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` }
907
1092
  } catch (error) {
908
1093
  return fail(`${error instanceof Error ? error.message : String(error)}\n`)
@@ -989,7 +1174,7 @@ export const actCommand = (argv: readonly string[]): Output => {
989
1174
  if (values.list === true) {
990
1175
  const actions = listActions(store)
991
1176
  if (values.json === true) {
992
- return ok(`${JSON.stringify(actions, undefined, 2)}\n`)
1177
+ return ok(`${stableJson(store, actions)}\n`)
993
1178
  }
994
1179
  if (actions.length === 0) {
995
1180
  return ok('no actions\n')
@@ -1030,7 +1215,7 @@ export const actCommand = (argv: readonly string[]): Output => {
1030
1215
  ...hooksPath === undefined ? {} : { hooks: readHooks(hooksPath) }
1031
1216
  })
1032
1217
  if (values.json === true) {
1033
- return { code: report.ok && report.hook?.error === undefined ? 0 : 1, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
1218
+ return { code: report.ok && report.hook?.error === undefined ? 0 : 1, out: `${stableJson(store, report)}\n`, err: '' }
1034
1219
  }
1035
1220
  const { lines, code } = renderActReport(report)
1036
1221
  return report.ok ?
@@ -1097,7 +1282,7 @@ export const checkCommand = (argv: readonly string[]): Output => {
1097
1282
  try {
1098
1283
  const report = caveCheck(store, { staleDays })
1099
1284
  const out = values.json === true ?
1100
- `${JSON.stringify(report, undefined, 2)}\n` :
1285
+ `${stableJson(store, report)}\n` :
1101
1286
  renderReport(report, staleDays)
1102
1287
  // Violations fail the check (spec §20.2); the other sections are advisory.
1103
1288
  return { code: report.violations.length > 0 ? 1 : 0, out, err: '' }
@@ -1106,11 +1291,87 @@ export const checkCommand = (argv: readonly string[]): Output => {
1106
1291
  }
1107
1292
  }
1108
1293
 
1294
+ const snapshotLine = (
1295
+ action: 'created' | 'verified' | 'restored',
1296
+ snapshot: ReturnType<typeof verifyBackup>
1297
+ ): string =>
1298
+ `${action} exact backup (${snapshot.rows} row(s), schema v${snapshot.schemaVersion}, ` +
1299
+ `${snapshot.bytes} bytes, sha256:${snapshot.sha256}) at ${snapshot.path}\n`
1300
+
1301
+ /** `cave backup` — create or verify an exact SQLite snapshot (§13.2.2). */
1302
+ export const backupCommand = (argv: readonly string[]): Output => {
1303
+ const { values } = parseArgs({
1304
+ args: [...argv],
1305
+ options: {
1306
+ db: { type: 'string' },
1307
+ out: { type: 'string' },
1308
+ force: { type: 'boolean' },
1309
+ verify: { type: 'string' },
1310
+ sha256: { type: 'string' }
1311
+ },
1312
+ allowPositionals: false
1313
+ })
1314
+ if (values.sha256 !== undefined && !/^[0-9a-f]{64}$/i.test(values.sha256)) {
1315
+ return fail('cave backup: --sha256 must be 64 hexadecimal characters\n')
1316
+ }
1317
+ try {
1318
+ if (values.verify !== undefined) {
1319
+ if (values.out !== undefined || values.force === true || values.db !== undefined) {
1320
+ return fail('cave backup: --verify excludes --db, --out, and --force\n')
1321
+ }
1322
+ return ok(snapshotLine('verified', verifyBackup(values.verify, values.sha256)))
1323
+ }
1324
+ if (values.sha256 !== undefined) {
1325
+ return fail('cave backup: --sha256 requires --verify\n')
1326
+ }
1327
+ if (values.out === undefined) {
1328
+ return fail('cave backup: --out <file> is required\n')
1329
+ }
1330
+ const store = open(values.db ?? defaultDbPath())
1331
+ try {
1332
+ return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1333
+ } finally {
1334
+ store.close()
1335
+ }
1336
+ } catch (error) {
1337
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1338
+ }
1339
+ }
1340
+
1341
+ /** `cave restore` — verify and atomically restore an exact snapshot (§13.2.2). */
1342
+ export const restoreCommand = (argv: readonly string[]): Output => {
1343
+ const { values, positionals } = parseArgs({
1344
+ args: [...argv],
1345
+ options: {
1346
+ db: { type: 'string' },
1347
+ force: { type: 'boolean' },
1348
+ sha256: { type: 'string' }
1349
+ },
1350
+ allowPositionals: true
1351
+ })
1352
+ if (positionals.length !== 1) {
1353
+ return fail('cave restore: exactly one snapshot file is required\n')
1354
+ }
1355
+ if (values.db === undefined) {
1356
+ return fail('cave restore: --db <path> destination is required\n')
1357
+ }
1358
+ if (values.sha256 !== undefined && !/^[0-9a-f]{64}$/i.test(values.sha256)) {
1359
+ return fail('cave restore: --sha256 must be 64 hexadecimal characters\n')
1360
+ }
1361
+ try {
1362
+ return ok(snapshotLine('restored', restoreBackup(positionals[0]!, values.db, {
1363
+ force: values.force === true,
1364
+ ...values.sha256 === undefined ? {} : { expectedSha256: values.sha256 }
1365
+ })))
1366
+ } catch (error) {
1367
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1368
+ }
1369
+ }
1370
+
1109
1371
  /**
1110
1372
  * `cave suggest-alias` — alias discovery (spec §27): same-entity
1111
- * candidates as suggested `ALIAS` claims for review. Async (the optional
1112
- * judge runs a shell agent), so `main.ts` routes it separately, like
1113
- * `reconstruct`.
1373
+ * candidates as suggested `ALIAS` claims for review. Async when the optional
1374
+ * judge runs a shell agent.
1114
1375
  */
1115
1376
  export const suggestAliasCommand = async (argv: readonly string[]): Promise<Output> => {
1116
1377
  try {
@@ -1209,7 +1470,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1209
1470
  ...values.as === undefined ? {} : { from: sanitizeLabel(values.as) }
1210
1471
  }
1211
1472
  const report: SyncReport = source === '-' ?
1212
- syncText(store, readFileSync(0, 'utf8'), { from: 'stdin', ...options }) :
1473
+ syncText(store, readStdin(), { from: 'stdin', ...options }) :
1213
1474
  syncFile(store, source, options)
1214
1475
  if (values.json === true) {
1215
1476
  return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
@@ -1251,16 +1512,22 @@ export const reportCommand = (argv: readonly string[]): Output => {
1251
1512
  resolve: { type: 'boolean' },
1252
1513
  'as-of': { type: 'string' },
1253
1514
  at: { type: 'string' },
1515
+ 'max-sensitivity': { type: 'string' },
1254
1516
  'no-prelude': { type: 'boolean' }
1255
1517
  },
1256
1518
  allowPositionals: true
1257
1519
  })
1258
1520
  const template = readInput(positionals)
1521
+ const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum)
1522
+ if (maximum === undefined) {
1523
+ return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1524
+ }
1259
1525
  const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1260
1526
  try {
1261
1527
  const rendered = caveReport(store, template, {
1262
1528
  aliases: values.aliases === true,
1263
1529
  resolve: values.resolve === true,
1530
+ maxSensitivity: maximum,
1264
1531
  ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
1265
1532
  ...values.at === undefined ? {} : { at: values.at }
1266
1533
  })
@@ -1310,17 +1577,22 @@ export const exportCommand = (argv: readonly string[]): Output => {
1310
1577
  current: { type: 'boolean' },
1311
1578
  tx: { type: 'boolean' },
1312
1579
  out: { type: 'string' },
1580
+ 'max-sensitivity': { type: 'string' },
1313
1581
  'no-prelude': { type: 'boolean' }
1314
1582
  },
1315
1583
  allowPositionals: false
1316
1584
  })
1317
1585
  const db = values.db ?? defaultDbPath()
1586
+ const maximum = Sensitivity.parse(values['max-sensitivity'] ?? Sensitivity.defaultMaximum)
1587
+ if (maximum === undefined) {
1588
+ return fail(`cave export: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1589
+ }
1318
1590
  if (values.out !== undefined && sameFile(db, values.out)) {
1319
1591
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1320
1592
  }
1321
1593
  const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1322
1594
  try {
1323
- const text = store.exportText({ current: values.current === true, tx: values.tx === true })
1595
+ const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1324
1596
  if (values.out === undefined) {
1325
1597
  return ok(text)
1326
1598
  }
@@ -1337,11 +1609,49 @@ export const exportCommand = (argv: readonly string[]): Output => {
1337
1609
  }
1338
1610
  }
1339
1611
 
1612
+ /** `cave generate` — spec §20.4's versioned TypeScript client artifact. */
1613
+ export const generateCommand = (argv: readonly string[]): Output => {
1614
+ const { values } = parseArgs({
1615
+ args: [...argv],
1616
+ options: {
1617
+ db: { type: 'string' },
1618
+ out: { type: 'string' },
1619
+ version: { type: 'string' },
1620
+ 'no-prelude': { type: 'boolean' }
1621
+ },
1622
+ allowPositionals: false
1623
+ })
1624
+ const version = values.version === undefined ? undefined : Number(values.version)
1625
+ if (version !== undefined && (!Number.isInteger(version) || version < 1)) {
1626
+ return fail(`cave generate: --version expects a positive integer, got ${JSON.stringify(values.version)}\n`)
1627
+ }
1628
+ const db = values.db ?? defaultDbPath()
1629
+ if (values.out !== undefined && sameFile(db, values.out)) {
1630
+ return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1631
+ }
1632
+ const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1633
+ try {
1634
+ const generated = generateClient(store, version === undefined ? {} : { version })
1635
+ if (!generated.ok) {
1636
+ return fail(`cave generate: schema cannot be generated:\n${generated.problems.map(problem => ` ${problem}`).join('\n')}\n`)
1637
+ }
1638
+ if (values.out === undefined) {
1639
+ return ok(generated.code)
1640
+ }
1641
+ writeFileSync(values.out, generated.code)
1642
+ return ok(`generated typed client v${generated.version} (${generated.fields.length} field(s), ` +
1643
+ `sha256:${generated.digest}) to ${values.out}\n`)
1644
+ } catch (error) {
1645
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1646
+ } finally {
1647
+ store.close()
1648
+ }
1649
+ }
1650
+
1340
1651
  /**
1341
1652
  * `cave reconstruct` — the §18 loop over the store: heuristic policy by
1342
1653
  * default (the eval baseline), the LLM policy over a shell-agent template
1343
- * with `--agent`. Async (the agent runs once per step), so `main.ts`
1344
- * routes it separately, like `connect`.
1654
+ * with `--agent`. Async because the agent runs once per step.
1345
1655
  */
1346
1656
  export const reconstructCommand = async (argv: readonly string[]): Promise<Output> => {
1347
1657
  try {
@@ -1425,8 +1735,8 @@ export const helpCommand = (argv: readonly string[]): Output => {
1425
1735
  if (text !== undefined) {
1426
1736
  return ok(`${text}\n`)
1427
1737
  }
1428
- // mcp, ingest, connect, eval, automate and serve own their help (main.ts forwards `help X` to `X --help`).
1429
- if (topic === 'mcp' || topic === 'ingest' || topic === 'connect' || topic === 'eval' || topic === 'automate' || topic === 'serve') {
1738
+ // Delegated commands own their help; dispatch.ts forwards `help X` to `X --help`.
1739
+ if (delegatedCommandNames.includes(topic)) {
1430
1740
  return ok(`see: cave ${topic} --help\n`)
1431
1741
  }
1432
1742
  return fail(`cave help: unknown command ${JSON.stringify(topic)}\n\n${usage}\n`, 2)
@@ -1436,50 +1746,54 @@ export const helpCommand = (argv: readonly string[]): Output => {
1436
1746
  export const cave = (argv: readonly string[]): Output => {
1437
1747
  const [command, ...rest] = argv
1438
1748
  const canonical = command === 'q' ? 'query' : command
1439
- try {
1440
- if (canonical !== undefined && canonical in commandHelp &&
1441
- (rest.includes('--help') || rest.includes('-h'))) {
1442
- return ok(`${commandHelp[canonical]}\n`)
1443
- }
1444
- switch (canonical) {
1445
- case 'parse':
1446
- return parseCommand(rest)
1447
- case 'add':
1448
- return addCommand(rest)
1449
- case 'import':
1450
- return importCommand(rest)
1451
- case 'query':
1452
- return queryCommand(rest)
1453
- case 'resolve':
1454
- return resolveCommand(rest)
1455
- case 'derive':
1456
- return deriveCommand(rest)
1457
- case 'act':
1458
- return actCommand(rest)
1459
- case 'check':
1460
- return checkCommand(rest)
1461
- case 'sync':
1462
- return syncCommand(rest)
1463
- case 'export':
1464
- return exportCommand(rest)
1465
- case 'report':
1466
- return reportCommand(rest)
1467
- case 'demo':
1468
- return demoCommand()
1469
- case 'version':
1470
- case '--version':
1471
- case '-v':
1472
- return versionCommand()
1473
- case 'help':
1474
- return helpCommand(rest)
1475
- case undefined:
1476
- case '--help':
1477
- case '-h':
1478
- return ok(`${usage}\n`)
1479
- default:
1480
- return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2)
1481
- }
1482
- } catch (error) {
1483
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1749
+ if (canonical !== undefined && canonical in commandHelp &&
1750
+ (rest.includes('--help') || rest.includes('-h'))) {
1751
+ return ok(`${commandHelp[canonical]}\n`)
1752
+ }
1753
+ switch (canonical) {
1754
+ case 'parse':
1755
+ return parseCommand(rest)
1756
+ case 'add':
1757
+ return addCommand(rest)
1758
+ case 'import':
1759
+ return importCommand(rest)
1760
+ case 'query':
1761
+ return queryCommand(rest)
1762
+ case 'resolve':
1763
+ return resolveCommand(rest)
1764
+ case 'derive':
1765
+ return deriveCommand(rest)
1766
+ case 'act':
1767
+ return actCommand(rest)
1768
+ case 'check':
1769
+ return checkCommand(rest)
1770
+ case 'backup':
1771
+ return backupCommand(rest)
1772
+ case 'restore':
1773
+ return restoreCommand(rest)
1774
+ case 'generate':
1775
+ return generateCommand(rest)
1776
+ case 'sync':
1777
+ return syncCommand(rest)
1778
+ case 'export':
1779
+ return exportCommand(rest)
1780
+ case 'report':
1781
+ return reportCommand(rest)
1782
+ case 'doctor':
1783
+ return doctorCommand(rest)
1784
+ case 'demo':
1785
+ return demoCommand()
1786
+ case 'version':
1787
+ case '--version':
1788
+ case '-v':
1789
+ return versionCommand()
1790
+ case 'help':
1791
+ return helpCommand(rest)
1792
+ case undefined:
1793
+ case '--help':
1794
+ case '-h':
1795
+ return ok(`${usage}\n`)
1796
+ default:
1797
+ return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2)
1484
1798
  }
1485
1799
  }