@cavelang/cli 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +224 -12
  2. package/dist/internal/act/action.js +2 -1
  3. package/dist/internal/act/action.js.map +1 -1
  4. package/dist/internal/act/declare.d.ts.map +1 -1
  5. package/dist/internal/act/declare.js +82 -53
  6. package/dist/internal/act/declare.js.map +1 -1
  7. package/dist/internal/act/engine.d.ts +4 -3
  8. package/dist/internal/act/engine.d.ts.map +1 -1
  9. package/dist/internal/act/engine.js +121 -32
  10. package/dist/internal/act/engine.js.map +1 -1
  11. package/dist/internal/automate/cancellation.d.ts +3 -0
  12. package/dist/internal/automate/cancellation.d.ts.map +1 -0
  13. package/dist/internal/automate/cancellation.js +38 -0
  14. package/dist/internal/automate/cancellation.js.map +1 -0
  15. package/dist/internal/automate/declare.d.ts.map +1 -1
  16. package/dist/internal/automate/declare.js +49 -24
  17. package/dist/internal/automate/declare.js.map +1 -1
  18. package/dist/internal/automate/engine.d.ts +7 -3
  19. package/dist/internal/automate/engine.d.ts.map +1 -1
  20. package/dist/internal/automate/engine.js +196 -93
  21. package/dist/internal/automate/engine.js.map +1 -1
  22. package/dist/internal/automate/error-message.d.ts +3 -0
  23. package/dist/internal/automate/error-message.d.ts.map +1 -0
  24. package/dist/internal/automate/error-message.js +10 -0
  25. package/dist/internal/automate/error-message.js.map +1 -0
  26. package/dist/internal/automate/main.d.ts.map +1 -1
  27. package/dist/internal/automate/main.js +238 -35
  28. package/dist/internal/automate/main.js.map +1 -1
  29. package/dist/internal/automate/options.d.ts +4 -0
  30. package/dist/internal/automate/options.d.ts.map +1 -0
  31. package/dist/internal/automate/options.js +19 -0
  32. package/dist/internal/automate/options.js.map +1 -0
  33. package/dist/internal/connect/declared.d.ts +6 -27
  34. package/dist/internal/connect/declared.d.ts.map +1 -1
  35. package/dist/internal/connect/declared.js +165 -76
  36. package/dist/internal/connect/declared.js.map +1 -1
  37. package/dist/internal/connect/error-message.d.ts +3 -0
  38. package/dist/internal/connect/error-message.d.ts.map +1 -0
  39. package/dist/internal/connect/error-message.js +10 -0
  40. package/dist/internal/connect/error-message.js.map +1 -0
  41. package/dist/internal/connect/federation.d.ts +9 -0
  42. package/dist/internal/connect/federation.d.ts.map +1 -0
  43. package/dist/internal/connect/federation.js +19 -0
  44. package/dist/internal/connect/federation.js.map +1 -0
  45. package/dist/internal/connect/fetch-failure.d.ts +3 -0
  46. package/dist/internal/connect/fetch-failure.d.ts.map +1 -0
  47. package/dist/internal/connect/fetch-failure.js +36 -0
  48. package/dist/internal/connect/fetch-failure.js.map +1 -0
  49. package/dist/internal/connect/main.d.ts +1 -0
  50. package/dist/internal/connect/main.d.ts.map +1 -1
  51. package/dist/internal/connect/main.js +288 -114
  52. package/dist/internal/connect/main.js.map +1 -1
  53. package/dist/internal/connect/options.d.ts +3 -0
  54. package/dist/internal/connect/options.d.ts.map +1 -0
  55. package/dist/internal/connect/options.js +9 -0
  56. package/dist/internal/connect/options.js.map +1 -0
  57. package/dist/internal/connect/records.d.ts +3 -0
  58. package/dist/internal/connect/records.d.ts.map +1 -0
  59. package/dist/internal/connect/records.js +21 -0
  60. package/dist/internal/connect/records.js.map +1 -0
  61. package/dist/internal/connect/run.d.ts +1 -18
  62. package/dist/internal/connect/run.d.ts.map +1 -1
  63. package/dist/internal/connect/run.js +115 -55
  64. package/dist/internal/connect/run.js.map +1 -1
  65. package/dist/internal/connect/source.d.ts +3 -1
  66. package/dist/internal/connect/source.d.ts.map +1 -1
  67. package/dist/internal/connect/source.js +226 -47
  68. package/dist/internal/connect/source.js.map +1 -1
  69. package/dist/internal/connect/template.d.ts +2 -2
  70. package/dist/internal/connect/template.d.ts.map +1 -1
  71. package/dist/internal/connect/template.js +25 -9
  72. package/dist/internal/connect/template.js.map +1 -1
  73. package/dist/internal/connect/utf8.d.ts +3 -0
  74. package/dist/internal/connect/utf8.d.ts.map +1 -0
  75. package/dist/internal/connect/utf8.js +12 -0
  76. package/dist/internal/connect/utf8.js.map +1 -0
  77. package/dist/internal/eval/cleanup.d.ts +5 -0
  78. package/dist/internal/eval/cleanup.d.ts.map +1 -0
  79. package/dist/internal/eval/cleanup.js +50 -0
  80. package/dist/internal/eval/cleanup.js.map +1 -0
  81. package/dist/internal/eval/error-message.d.ts +3 -0
  82. package/dist/internal/eval/error-message.d.ts.map +1 -0
  83. package/dist/internal/eval/error-message.js +10 -0
  84. package/dist/internal/eval/error-message.js.map +1 -0
  85. package/dist/internal/eval/judge.d.ts.map +1 -1
  86. package/dist/internal/eval/judge.js +3 -31
  87. package/dist/internal/eval/judge.js.map +1 -1
  88. package/dist/internal/eval/loop.d.ts.map +1 -1
  89. package/dist/internal/eval/loop.js +18 -4
  90. package/dist/internal/eval/loop.js.map +1 -1
  91. package/dist/internal/eval/main.d.ts.map +1 -1
  92. package/dist/internal/eval/main.js +22 -10
  93. package/dist/internal/eval/main.js.map +1 -1
  94. package/dist/internal/eval/options.d.ts +6 -0
  95. package/dist/internal/eval/options.d.ts.map +1 -0
  96. package/dist/internal/eval/options.js +31 -0
  97. package/dist/internal/eval/options.js.map +1 -0
  98. package/dist/internal/eval/queries.d.ts.map +1 -1
  99. package/dist/internal/eval/queries.js +9 -6
  100. package/dist/internal/eval/queries.js.map +1 -1
  101. package/dist/internal/eval/run.d.ts.map +1 -1
  102. package/dist/internal/eval/run.js +112 -24
  103. package/dist/internal/eval/run.js.map +1 -1
  104. package/dist/internal/eval/score.d.ts.map +1 -1
  105. package/dist/internal/eval/score.js +9 -2
  106. package/dist/internal/eval/score.js.map +1 -1
  107. package/dist/internal/eval/suite.d.ts.map +1 -1
  108. package/dist/internal/eval/suite.js +14 -2
  109. package/dist/internal/eval/suite.js.map +1 -1
  110. package/dist/internal/ingest/cleanup.d.ts +5 -0
  111. package/dist/internal/ingest/cleanup.d.ts.map +1 -0
  112. package/dist/internal/ingest/cleanup.js +50 -0
  113. package/dist/internal/ingest/cleanup.js.map +1 -0
  114. package/dist/internal/ingest/content.d.ts +6 -0
  115. package/dist/internal/ingest/content.d.ts.map +1 -0
  116. package/dist/internal/ingest/content.js +25 -0
  117. package/dist/internal/ingest/content.js.map +1 -0
  118. package/dist/internal/ingest/context.d.ts.map +1 -1
  119. package/dist/internal/ingest/context.js +25 -7
  120. package/dist/internal/ingest/context.js.map +1 -1
  121. package/dist/internal/ingest/error-message.d.ts +3 -0
  122. package/dist/internal/ingest/error-message.d.ts.map +1 -0
  123. package/dist/internal/ingest/error-message.js +10 -0
  124. package/dist/internal/ingest/error-message.js.map +1 -0
  125. package/dist/internal/ingest/files.d.ts +4 -12
  126. package/dist/internal/ingest/files.d.ts.map +1 -1
  127. package/dist/internal/ingest/files.js +22 -10
  128. package/dist/internal/ingest/files.js.map +1 -1
  129. package/dist/internal/ingest/main.d.ts.map +1 -1
  130. package/dist/internal/ingest/main.js +46 -16
  131. package/dist/internal/ingest/main.js.map +1 -1
  132. package/dist/internal/ingest/options.d.ts +6 -0
  133. package/dist/internal/ingest/options.d.ts.map +1 -0
  134. package/dist/internal/ingest/options.js +27 -0
  135. package/dist/internal/ingest/options.js.map +1 -0
  136. package/dist/internal/ingest/prompt.d.ts.map +1 -1
  137. package/dist/internal/ingest/prompt.js +14 -10
  138. package/dist/internal/ingest/prompt.js.map +1 -1
  139. package/dist/internal/ingest/run.d.ts +9 -2
  140. package/dist/internal/ingest/run.d.ts.map +1 -1
  141. package/dist/internal/ingest/run.js +206 -62
  142. package/dist/internal/ingest/run.js.map +1 -1
  143. package/dist/internal/ingest/timeout.d.ts +3 -0
  144. package/dist/internal/ingest/timeout.d.ts.map +1 -0
  145. package/dist/internal/ingest/timeout.js +14 -0
  146. package/dist/internal/ingest/timeout.js.map +1 -0
  147. package/dist/internal/ingest/web.d.ts +2 -11
  148. package/dist/internal/ingest/web.d.ts.map +1 -1
  149. package/dist/internal/ingest/web.js +131 -25
  150. package/dist/internal/ingest/web.js.map +1 -1
  151. package/dist/internal/loop/budgets.d.ts +3 -0
  152. package/dist/internal/loop/budgets.d.ts.map +1 -0
  153. package/dist/internal/loop/budgets.js +10 -0
  154. package/dist/internal/loop/budgets.js.map +1 -0
  155. package/dist/internal/loop/index.d.ts +2 -0
  156. package/dist/internal/loop/index.d.ts.map +1 -1
  157. package/dist/internal/loop/index.js +2 -0
  158. package/dist/internal/loop/index.js.map +1 -1
  159. package/dist/internal/loop/json-answer.d.ts +7 -0
  160. package/dist/internal/loop/json-answer.d.ts.map +1 -0
  161. package/dist/internal/loop/json-answer.js +79 -0
  162. package/dist/internal/loop/json-answer.js.map +1 -0
  163. package/dist/internal/loop/json-spans.d.ts +3 -0
  164. package/dist/internal/loop/json-spans.d.ts.map +1 -0
  165. package/dist/internal/loop/json-spans.js +88 -0
  166. package/dist/internal/loop/json-spans.js.map +1 -0
  167. package/dist/internal/loop/llm.d.ts +3 -2
  168. package/dist/internal/loop/llm.d.ts.map +1 -1
  169. package/dist/internal/loop/llm.js +115 -32
  170. package/dist/internal/loop/llm.js.map +1 -1
  171. package/dist/internal/loop/process.d.ts +3 -1
  172. package/dist/internal/loop/process.d.ts.map +1 -1
  173. package/dist/internal/loop/process.js +159 -59
  174. package/dist/internal/loop/process.js.map +1 -1
  175. package/dist/internal/loop/read-snapshot.d.ts +4 -0
  176. package/dist/internal/loop/read-snapshot.d.ts.map +1 -0
  177. package/dist/internal/loop/read-snapshot.js +29 -0
  178. package/dist/internal/loop/read-snapshot.js.map +1 -0
  179. package/dist/internal/loop/reconstruct.d.ts +6 -5
  180. package/dist/internal/loop/reconstruct.d.ts.map +1 -1
  181. package/dist/internal/loop/reconstruct.js +19 -8
  182. package/dist/internal/loop/reconstruct.js.map +1 -1
  183. package/dist/internal/loop/score-settings.d.ts +5 -0
  184. package/dist/internal/loop/score-settings.d.ts.map +1 -0
  185. package/dist/internal/loop/score-settings.js +14 -0
  186. package/dist/internal/loop/score-settings.js.map +1 -0
  187. package/dist/internal/loop/sqlite.d.ts.map +1 -1
  188. package/dist/internal/loop/sqlite.js +15 -7
  189. package/dist/internal/loop/sqlite.js.map +1 -1
  190. package/dist/internal/loop/store.d.ts.map +1 -1
  191. package/dist/internal/loop/store.js +11 -7
  192. package/dist/internal/loop/store.js.map +1 -1
  193. package/dist/internal/loop/timeout.d.ts +3 -0
  194. package/dist/internal/loop/timeout.d.ts.map +1 -0
  195. package/dist/internal/loop/timeout.js +13 -0
  196. package/dist/internal/loop/timeout.js.map +1 -0
  197. package/dist/internal/mcp/error-message.d.ts +3 -0
  198. package/dist/internal/mcp/error-message.d.ts.map +1 -0
  199. package/dist/internal/mcp/error-message.js +10 -0
  200. package/dist/internal/mcp/error-message.js.map +1 -0
  201. package/dist/internal/mcp/main.d.ts.map +1 -1
  202. package/dist/internal/mcp/main.js +42 -27
  203. package/dist/internal/mcp/main.js.map +1 -1
  204. package/dist/internal/mcp/read-snapshot.d.ts +4 -0
  205. package/dist/internal/mcp/read-snapshot.d.ts.map +1 -0
  206. package/dist/internal/mcp/read-snapshot.js +22 -0
  207. package/dist/internal/mcp/read-snapshot.js.map +1 -0
  208. package/dist/internal/mcp/server.d.ts.map +1 -1
  209. package/dist/internal/mcp/server.js +181 -21
  210. package/dist/internal/mcp/server.js.map +1 -1
  211. package/dist/internal/mcp/source.d.ts +3 -0
  212. package/dist/internal/mcp/source.d.ts.map +1 -0
  213. package/dist/internal/mcp/source.js +13 -0
  214. package/dist/internal/mcp/source.js.map +1 -0
  215. package/dist/internal/mcp/tools.d.ts.map +1 -1
  216. package/dist/internal/mcp/tools.js +135 -72
  217. package/dist/internal/mcp/tools.js.map +1 -1
  218. package/dist/internal/rules/declarations.d.ts +6 -0
  219. package/dist/internal/rules/declarations.d.ts.map +1 -0
  220. package/dist/internal/rules/declarations.js +14 -0
  221. package/dist/internal/rules/declarations.js.map +1 -0
  222. package/dist/internal/rules/declare.d.ts.map +1 -1
  223. package/dist/internal/rules/declare.js +30 -16
  224. package/dist/internal/rules/declare.js.map +1 -1
  225. package/dist/internal/rules/engine.d.ts +8 -7
  226. package/dist/internal/rules/engine.d.ts.map +1 -1
  227. package/dist/internal/rules/engine.js +150 -44
  228. package/dist/internal/rules/engine.js.map +1 -1
  229. package/dist/internal/shape/alias-root.d.ts +3 -0
  230. package/dist/internal/shape/alias-root.d.ts.map +1 -0
  231. package/dist/internal/shape/alias-root.js +13 -0
  232. package/dist/internal/shape/alias-root.js.map +1 -0
  233. package/dist/internal/shape/check.d.ts +15 -3
  234. package/dist/internal/shape/check.d.ts.map +1 -1
  235. package/dist/internal/shape/check.js +230 -100
  236. package/dist/internal/shape/check.js.map +1 -1
  237. package/dist/internal/shape/client.d.ts.map +1 -1
  238. package/dist/internal/shape/client.js +42 -20
  239. package/dist/internal/shape/client.js.map +1 -1
  240. package/dist/internal/shape/constraints.d.ts +8 -0
  241. package/dist/internal/shape/constraints.d.ts.map +1 -0
  242. package/dist/internal/shape/constraints.js +17 -0
  243. package/dist/internal/shape/constraints.js.map +1 -0
  244. package/dist/internal/shape/edit-distance.d.ts +3 -0
  245. package/dist/internal/shape/edit-distance.d.ts.map +1 -0
  246. package/dist/internal/shape/edit-distance.js +33 -0
  247. package/dist/internal/shape/edit-distance.js.map +1 -0
  248. package/dist/internal/shape/gate.d.ts.map +1 -1
  249. package/dist/internal/shape/gate.js +0 -0
  250. package/dist/internal/shape/gate.js.map +1 -1
  251. package/dist/internal/shape/snapshot.d.ts +4 -0
  252. package/dist/internal/shape/snapshot.d.ts.map +1 -0
  253. package/dist/internal/shape/snapshot.js +20 -0
  254. package/dist/internal/shape/snapshot.js.map +1 -0
  255. package/dist/internal/shape/suggest.d.ts +8 -6
  256. package/dist/internal/shape/suggest.d.ts.map +1 -1
  257. package/dist/internal/shape/suggest.js +208 -92
  258. package/dist/internal/shape/suggest.js.map +1 -1
  259. package/dist/internal/sync/error-message.d.ts +3 -0
  260. package/dist/internal/sync/error-message.d.ts.map +1 -0
  261. package/dist/internal/sync/error-message.js +10 -0
  262. package/dist/internal/sync/error-message.js.map +1 -0
  263. package/dist/internal/sync/index.d.ts +3 -2
  264. package/dist/internal/sync/index.d.ts.map +1 -1
  265. package/dist/internal/sync/index.js +3 -2
  266. package/dist/internal/sync/index.js.map +1 -1
  267. package/dist/internal/sync/sync.d.ts +8 -5
  268. package/dist/internal/sync/sync.d.ts.map +1 -1
  269. package/dist/internal/sync/sync.js +322 -65
  270. package/dist/internal/sync/sync.js.map +1 -1
  271. package/dist/internal/view/api.d.ts +3 -1
  272. package/dist/internal/view/api.d.ts.map +1 -1
  273. package/dist/internal/view/api.js +154 -90
  274. package/dist/internal/view/api.js.map +1 -1
  275. package/dist/internal/view/error-message.d.ts +3 -0
  276. package/dist/internal/view/error-message.d.ts.map +1 -0
  277. package/dist/internal/view/error-message.js +10 -0
  278. package/dist/internal/view/error-message.js.map +1 -0
  279. package/dist/internal/view/main.d.ts.map +1 -1
  280. package/dist/internal/view/main.js +61 -8
  281. package/dist/internal/view/main.js.map +1 -1
  282. package/dist/internal/view/page.d.ts.map +1 -1
  283. package/dist/internal/view/page.js +119 -20
  284. package/dist/internal/view/page.js.map +1 -1
  285. package/dist/internal/view/report.d.ts.map +1 -1
  286. package/dist/internal/view/report.js +180 -145
  287. package/dist/internal/view/report.js.map +1 -1
  288. package/dist/internal/view/scope.d.ts +6 -2
  289. package/dist/internal/view/scope.d.ts.map +1 -1
  290. package/dist/internal/view/scope.js +107 -24
  291. package/dist/internal/view/scope.js.map +1 -1
  292. package/dist/internal/view/sensitivity.d.ts +4 -0
  293. package/dist/internal/view/sensitivity.d.ts.map +1 -0
  294. package/dist/internal/view/sensitivity.js +11 -0
  295. package/dist/internal/view/sensitivity.js.map +1 -0
  296. package/dist/internal/view/server.d.ts.map +1 -1
  297. package/dist/internal/view/server.js +57 -20
  298. package/dist/internal/view/server.js.map +1 -1
  299. package/dist/src/atomic-output.d.ts +3 -0
  300. package/dist/src/atomic-output.d.ts.map +1 -0
  301. package/dist/src/atomic-output.js +67 -0
  302. package/dist/src/atomic-output.js.map +1 -0
  303. package/dist/src/cli.d.ts +3 -2
  304. package/dist/src/cli.d.ts.map +1 -1
  305. package/dist/src/cli.js +188 -173
  306. package/dist/src/cli.js.map +1 -1
  307. package/dist/src/dispatch.d.ts.map +1 -1
  308. package/dist/src/dispatch.js +3 -2
  309. package/dist/src/dispatch.js.map +1 -1
  310. package/dist/src/doctor.d.ts.map +1 -1
  311. package/dist/src/doctor.js +178 -102
  312. package/dist/src/doctor.js.map +1 -1
  313. package/dist/src/error-message.d.ts +3 -0
  314. package/dist/src/error-message.d.ts.map +1 -0
  315. package/dist/src/error-message.js +20 -0
  316. package/dist/src/error-message.js.map +1 -0
  317. package/dist/src/hooks-config.d.ts +3 -0
  318. package/dist/src/hooks-config.d.ts.map +1 -0
  319. package/dist/src/hooks-config.js +23 -0
  320. package/dist/src/hooks-config.js.map +1 -0
  321. package/package.json +23 -20
  322. package/src/atomic-output.ts +47 -0
  323. package/src/cli.ts +170 -147
  324. package/src/dispatch.ts +3 -2
  325. package/src/doctor.ts +192 -111
  326. package/src/error-message.ts +13 -0
  327. package/src/hooks-config.ts +22 -0
package/src/dispatch.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { cave, highlightCommand, querySourcesCommand, reconstructCommand, suggestAliasCommand } from './cli.ts'
4
4
  import { delegatedCommandNames } from './commands.ts'
5
5
  import type { Output } from './cli.ts'
6
+ import { errorMessage } from './error-message.ts'
6
7
 
7
8
  export type CommandRuntime = {
8
9
  readonly stdin: NodeJS.ReadableStream
@@ -114,12 +115,12 @@ export const dispatch = async (
114
115
  runtime: CommandRuntime = processRuntime()
115
116
  ): Promise<number> => {
116
117
  try {
118
+ runtime.signal?.throwIfAborted()
117
119
  return await execute(argv, runtime)
118
120
  } catch (error) {
119
121
  if (runtime.signal?.aborted === true) return 0
120
122
  const command = argv[0] === 'q' ? 'query' : argv[0]
121
- const message = runtime.debug === true && error instanceof Error && error.stack !== undefined ?
122
- error.stack : error instanceof Error ? error.message : String(error)
123
+ const message = errorMessage(error, runtime.debug === true)
123
124
  runtime.stderr.write(`cave${command === undefined || command.startsWith('-') ? '' : ` ${command}`}: ${message}\n`)
124
125
  return 1
125
126
  }
package/src/doctor.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  /** Read-only installation, runtime, and store diagnostics for `cave doctor`. */
2
2
 
3
3
  import { existsSync, readFileSync, statSync } from 'node:fs'
4
+ import { readHooks } from './hooks-config.ts'
4
5
  import { createRequire } from 'node:module'
5
6
  import { dirname, join, parse } from 'node:path'
6
7
  import { parseArgs } from 'node:util'
7
- import { Version } from '@cavelang/core'
8
+ import type { StatementSync } from 'node:sqlite'
9
+ import { Key, Uuidv7, Version } from '@cavelang/core'
10
+ import { emitClaim } from '@cavelang/canonical'
8
11
  import { directCommand, runProcessSync } from '@cavelang/loop'
9
- import { LocateError, Schema, defaultDbPath, kindOf, openText } from '@cavelang/store'
12
+ import { Row, Schema, defaultDbPath, kindOf, openText } from '@cavelang/store'
10
13
  import { assemble } from '@cavelang/connect'
11
14
  import { nodeSqliteAdapter } from '@cavelang/store/adapter/node'
12
15
  import type { SqliteDatabase } from '@cavelang/store/adapter'
@@ -53,8 +56,8 @@ export type DoctorOutput = {
53
56
  readonly err: string
54
57
  }
55
58
 
56
- const requiredNode = '22.18.0'
57
- const supportedNodeRange = '^22.18.0 || ^24.0.0 || ^26.0.0'
59
+ const requiredNode = '24.16.0'
60
+ const supportedNodeRange = '^24.16.0 || ^26.1.0'
58
61
 
59
62
  const versionParts = (value: string): readonly [number, number, number] | undefined => {
60
63
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value)
@@ -84,8 +87,8 @@ const stableVersion = /^\d+\.\d+\.\d+$/
84
87
  export const isSupportedNodeVersion = (value: string): boolean => {
85
88
  if (!stableVersion.test(value)) return false
86
89
  const major = versionParts(value)?.[0]
87
- if (major === 22) return atLeast(value, requiredNode)
88
- return major === 24 || major === 26
90
+ if (major === 24) return atLeast(value, requiredNode)
91
+ return major === 26 && atLeast(value, '26.1.0')
89
92
  }
90
93
 
91
94
  const check = (
@@ -99,30 +102,35 @@ const nodeCheck = (): DoctorCheck =>
99
102
  isSupportedNodeVersion(process.versions.node) ?
100
103
  check('runtime.node', 'pass', `Node ${process.versions.node} satisfies ${supportedNodeRange}`) :
101
104
  check('runtime.node', 'fail', `Node ${process.versions.node} is unsupported`,
102
- `Install Node ${requiredNode}+, Node 24, or Node 26 and rerun cave doctor.`)
105
+ `Install Node 24.16.0+ in the 24.x line or Node 26.1.0+ in the 26.x line and rerun cave doctor.`)
103
106
 
104
- const sqliteCheck = (): DoctorCheck => {
107
+ const sqliteCheck = (): readonly DoctorCheck[] => {
105
108
  let db: SqliteDatabase | undefined
106
- try {
107
- db = nodeSqliteAdapter.open(':memory:')
108
- db.exec('PRAGMA foreign_keys = ON')
109
- const sqlite = db.prepare('SELECT sqlite_version() AS version').get()?.['version']
110
- const json = db.prepare(`SELECT json_valid('{}') AS available`).get()?.['available']
111
- const foreignKeys = db.prepare('PRAGMA foreign_keys').get()?.['foreign_keys']
112
- db.exec('CREATE VIRTUAL TABLE doctor_fts USING fts5(value)')
113
- db.exec('DROP TABLE doctor_fts')
114
- if (typeof sqlite !== 'string' || json !== 1 || foreignKeys !== 1 ||
115
- nodeSqliteAdapter.capabilities.loadExtension === undefined) {
116
- throw new Error('capability unavailable')
109
+ const result = (() => {
110
+ try {
111
+ db = nodeSqliteAdapter.open(':memory:')
112
+ db.exec('PRAGMA foreign_keys = ON')
113
+ const sqlite = db.prepare('SELECT sqlite_version() AS version').get()?.['version']
114
+ const json = db.prepare(`SELECT json_valid('{}') AS available`).get()?.['available']
115
+ const foreignKeys = db.prepare('PRAGMA foreign_keys').get()?.['foreign_keys']
116
+ db.exec('CREATE VIRTUAL TABLE doctor_fts USING fts5(value)')
117
+ db.exec('DROP TABLE doctor_fts')
118
+ if (typeof sqlite !== 'string' || json !== 1 || foreignKeys !== 1 ||
119
+ nodeSqliteAdapter.capabilities.loadExtension === undefined) {
120
+ throw new Error('capability unavailable')
121
+ }
122
+ return check('runtime.sqlite', 'pass',
123
+ `SQLite ${sqlite} supports FTS5, JSON functions, foreign keys, and extension loading`)
124
+ } catch {
125
+ return check('runtime.sqlite', 'fail', 'The Node SQLite runtime lacks a required capability',
126
+ `Use an official Node ${requiredNode}+ build with SQLite FTS5 and JSON support.`)
117
127
  }
118
- return check('runtime.sqlite', 'pass',
119
- `SQLite ${sqlite} supports FTS5, JSON functions, foreign keys, and extension loading`)
120
- } catch {
121
- return check('runtime.sqlite', 'fail', 'The Node SQLite runtime lacks a required capability',
122
- `Use an official Node ${requiredNode}+ build with SQLite FTS5 and JSON support.`)
123
- } finally {
124
- db?.close()
128
+ })()
129
+ try { db?.close() } catch {
130
+ return [result, check('runtime.sqlite.cleanup', 'fail', 'The SQLite capability probe could not be closed cleanly',
131
+ 'Retry diagnosis in a new process.')]
125
132
  }
133
+ return [result]
126
134
  }
127
135
 
128
136
  const grammarCheck = (): DoctorCheck => {
@@ -203,10 +211,23 @@ const pnpmCheck = (): DoctorCheck => {
203
211
 
204
212
  type DatabaseConfiguration = DoctorReport['configuration']['database']
205
213
 
206
- const inspectDatabase = (path: string, source: DatabaseConfiguration['source']): {
214
+ type DatabaseInspection = {
207
215
  readonly configuration: DatabaseConfiguration
208
216
  readonly checks: readonly DoctorCheck[]
209
- } => {
217
+ }
218
+
219
+ const finishDatabaseInspection = (result: DatabaseInspection, close: () => void): DatabaseInspection => {
220
+ try { close() } catch {
221
+ return {
222
+ ...result,
223
+ checks: [...result.checks, check('store.cleanup', 'fail', 'The database connection could not be closed cleanly',
224
+ 'Retry diagnosis in a new process; cave doctor made no repairs.')]
225
+ }
226
+ }
227
+ return result
228
+ }
229
+
230
+ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']): DatabaseInspection => {
210
231
  const memory = path === ':memory:'
211
232
  const exists = memory || existsSync(path)
212
233
  const base: DatabaseConfiguration = { source, kind: memory ? 'memory' : 'file', exists }
@@ -221,99 +242,163 @@ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']):
221
242
  // A CAVE text file is a read-only store assembled in memory (spec
222
243
  // §13.7, §23.4): replay it, sources included, and count what it holds.
223
244
  const configuration: DatabaseConfiguration = { ...base, kind: 'text' }
224
- try {
225
- const store = openText(path, { assemble })
245
+ let store: ReturnType<typeof openText> | undefined
246
+ const result: DatabaseInspection = (() => {
226
247
  try {
248
+ store = openText(path, { assemble })
227
249
  const claims = store.currentBeliefs().filter(row => row.conf > 0).length
228
250
  return {
229
251
  configuration: { ...configuration, claims },
230
252
  checks: [check('store.database', 'pass',
231
253
  `CAVE text store assembled in memory (${claims} current claim(s)); text stores are read-only`)]
232
254
  }
233
- } finally {
234
- store.close()
235
- }
236
- } catch (error) {
237
- return {
238
- configuration,
239
- checks: [check('store.database', 'fail',
240
- `The CAVE text store does not load: ${error instanceof LocateError ? error.message.split('\n')[0] : error instanceof Error ? error.message : String(error)}`,
241
- 'Fix the file (cave parse) or the source it declares; a text store is read-only.')]
255
+ } catch {
256
+ return {
257
+ configuration,
258
+ checks: [check('store.database', 'fail',
259
+ 'The CAVE text store or one of its declared sources could not be loaded',
260
+ 'Fix the file (cave parse) or the source it declares; a text store is read-only.')]
261
+ }
242
262
  }
243
- }
263
+ })()
264
+ return finishDatabaseInspection(result, () => store?.close())
244
265
  }
245
266
 
246
267
  let db: SqliteDatabase | undefined
247
268
  let observed = base
248
- try {
249
- db = nodeSqliteAdapter.open(path, { readOnly: true })
250
- const version = db.prepare('PRAGMA user_version').get()?.['user_version']
251
- if (typeof version !== 'number') throw new Error('invalid version')
252
- const caveObjects = db.prepare(
253
- `SELECT count(*) AS count FROM sqlite_schema WHERE name LIKE 'cave_%' OR name LIKE 'idx_cave_%'`
254
- ).get()?.['count']
255
- const configuration = { ...base, schemaVersion: version }
256
- observed = configuration
269
+ const result = (() => {
270
+ try {
271
+ db = nodeSqliteAdapter.open(path, { readOnly: true })
272
+ // Keep schema, integrity, row and search diagnostics on one read snapshot.
273
+ // Closing the owned connection releases this read-only transaction.
274
+ db.exec('BEGIN')
275
+ const version = db.prepare('PRAGMA user_version').get()?.['user_version']
276
+ if (typeof version !== 'number') throw new Error('invalid version')
277
+ const caveObjects = db.prepare(
278
+ `SELECT count(*) AS count FROM sqlite_schema WHERE name LIKE 'cave!_%' ESCAPE '!' OR name LIKE 'idx!_cave!_%' ESCAPE '!'`
279
+ ).get()?.['count']
280
+ const configuration = { ...base, schemaVersion: version }
281
+ observed = configuration
257
282
 
258
- if (version > Schema.currentVersion) {
259
- return {
260
- configuration,
261
- checks: [check('store.database', 'fail',
262
- `Database schema ${version} is newer than supported schema ${Schema.currentVersion}`,
263
- 'Upgrade CAVE before opening this database.')]
283
+ if (version > Schema.currentVersion) {
284
+ return {
285
+ configuration,
286
+ checks: [check('store.database', 'fail',
287
+ `Database schema ${version} is newer than supported schema ${Schema.currentVersion}`,
288
+ 'Upgrade CAVE before opening this database.')]
289
+ }
264
290
  }
265
- }
266
- if (version === 0 && caveObjects === 0) {
267
- return {
268
- configuration,
269
- checks: [check('store.database', 'warn', 'The database is readable but is not initialized as a CAVE store',
270
- 'Run cave add to initialize it; cave doctor made no changes.')]
291
+ if (version === 0 && caveObjects === 0) {
292
+ return {
293
+ configuration,
294
+ checks: [check('store.database', 'warn', 'The database is readable but is not initialized as a CAVE store',
295
+ 'Run cave add to initialize it; cave doctor made no changes.')]
296
+ }
271
297
  }
272
- }
273
- if (version < Schema.currentVersion) {
274
- return {
275
- configuration,
276
- checks: [check('store.database', 'warn',
277
- `Database schema ${version} needs migration to schema ${Schema.currentVersion}`,
278
- 'Close every user and copy the database file as a rollback point, then open it with a writing command such as cave add to migrate it; reads never migrate.')]
298
+ if (version < Schema.currentVersion) {
299
+ return {
300
+ configuration,
301
+ checks: [check('store.database', 'warn',
302
+ `Database schema ${version} needs migration to schema ${Schema.currentVersion}`,
303
+ 'Close every user and copy the database file as a rollback point, then open it with a writing command such as cave add to migrate it; reads never migrate.')]
304
+ }
279
305
  }
280
- }
281
306
 
282
- Schema.validate(db, version)
283
- const integrity = db.prepare('PRAGMA integrity_check').all()
284
- if (integrity.length !== 1 || integrity[0]?.['integrity_check'] !== 'ok') {
307
+ Schema.validate(db, version)
308
+ const integrity = db.prepare('PRAGMA integrity_check').all()
309
+ if (integrity.length !== 1 || integrity[0]?.['integrity_check'] !== 'ok') {
310
+ return {
311
+ configuration,
312
+ checks: [check('store.integrity', 'fail', 'SQLite integrity checking failed',
313
+ 'Restore a verified backup or recover the database with SQLite tooling.')]
314
+ }
315
+ }
316
+ const foreignKeys = db.prepare('PRAGMA foreign_key_check').all()
317
+ if (foreignKeys.length > 0) {
318
+ return {
319
+ configuration,
320
+ checks: [check('store.integrity', 'fail', 'The store contains broken foreign-key references',
321
+ 'Restore a verified backup or repair the affected database rows.')]
322
+ }
323
+ }
324
+ const claims = db.prepare('SELECT count(*) AS count FROM cave_claim').get()?.['count']
325
+ if (typeof claims !== 'number') throw new Error('invalid count')
326
+ let invalidRows = db.prepare(`SELECT EXISTS (
327
+ SELECT 1 FROM cave_claim WHERE negated NOT IN (0, 1) OR importance NOT IN (0, 1)
328
+ OR value_approx NOT IN (0, 1)
329
+ OR (object IS NOT NULL AND (attribute IS NOT NULL OR value_text IS NOT NULL))
330
+ OR (attribute IS NOT NULL AND value_text IS NULL)
331
+ OR typeof(conf) NOT IN ('integer', 'real') OR conf NOT BETWEEN 0 AND 1
332
+ OR (sigma_level IS NOT NULL AND (typeof(sigma_level) NOT IN ('integer', 'real')
333
+ OR NOT (sigma_level > 0 AND sigma_level <= ?)))
334
+ ) OR EXISTS (
335
+ SELECT 1 FROM cave_provenance WHERE typeof(value) <> 'text' OR value = ''
336
+ ) OR EXISTS (
337
+ SELECT 1 FROM cave_edge WHERE role IS NULL OR role NOT IN ('WHEN', 'VIA', 'BECAUSE', 'QUALIFIES')
338
+ ) AS invalid`).get(Number.MAX_VALUE)?.['invalid']
339
+ if (typeof invalidRows !== 'number') throw new Error('invalid row check')
340
+ if (invalidRows === 0) {
341
+ // Stream stored history without collecting rows; projections must agree with
342
+ // authored values even when a SQL filter would otherwise hide the row.
343
+ // This connection is owned by nodeSqliteAdapter, which returns native statements.
344
+ const rows = db.prepare('SELECT * FROM cave_claim') as StatementSync
345
+ const contexts = db.prepare('SELECT context FROM cave_context WHERE claim_id = ?')
346
+ const tags = db.prepare('SELECT key, value FROM cave_tag WHERE claim_id = ?')
347
+ for (const row of rows.iterate()) {
348
+ if (typeof row.id !== 'string' || !Uuidv7.is(row.id) || row.tx !== row.id) {
349
+ invalidRows = 1
350
+ break
351
+ }
352
+ try {
353
+ const claim = Row.toClaim(row as unknown as Row.t,
354
+ contexts.all(row.id).map(entry => entry.context as string),
355
+ tags.all(row.id) as { key: string, value: null | string }[])
356
+ emitClaim(claim)
357
+ if (Key.of(claim) !== row.claim_key) {
358
+ invalidRows = 1
359
+ break
360
+ }
361
+ } catch {
362
+ invalidRows = 1
363
+ break
364
+ }
365
+ }
366
+ }
367
+ // SQLite's integrity_check cannot tell whether the separate FTS content
368
+ // still represents the claims. Compare one snapshot, including counts so
369
+ // duplicate entries cannot disappear under EXCEPT's set semantics.
370
+ const searchColumns = 'subject, verb, object, attribute, value_text, comment, raw_line'
371
+ const searchMismatch = db.prepare(`SELECT
372
+ (SELECT count(*) FROM cave_claim) <> (SELECT count(*) FROM cave_fts)
373
+ OR EXISTS (
374
+ SELECT id, ${searchColumns} FROM cave_claim
375
+ EXCEPT SELECT claim_id, ${searchColumns} FROM cave_fts
376
+ ) AS mismatch`).get()?.['mismatch']
377
+ if (typeof searchMismatch !== 'number') throw new Error('invalid search check')
285
378
  return {
286
- configuration,
287
- checks: [check('store.integrity', 'fail', 'SQLite integrity checking failed',
288
- 'Restore a verified backup or recover the database with SQLite tooling.')]
379
+ configuration: { ...configuration, claims },
380
+ checks: [
381
+ check('store.database', 'pass', `CAVE schema ${version} is compatible (${claims} claim(s))`),
382
+ check('store.integrity', 'pass', 'SQLite integrity and foreign-key checks passed'),
383
+ invalidRows === 0 ?
384
+ check('store.rows', 'pass', 'Stored payloads, provenance, flags, confidence and sigma levels are valid') :
385
+ check('store.rows', 'fail', 'Stored claims contain invalid transaction identities, claim keys, terms, tags, edge roles, payload columns, provenance values, boolean flags, confidence, sigma levels, authored values or numeric caches',
386
+ 'Restore a verified backup or repair the affected stored rows before exporting; cave doctor made no changes.'),
387
+ searchMismatch === 0 ?
388
+ check('store.search', 'pass', 'Search entries match the stored claims') :
389
+ check('store.search', 'fail', 'Search entries are missing, duplicated, orphaned, or stale',
390
+ 'Restore a verified backup or rebuild the search index from the stored claims with SQLite tooling; cave doctor made no changes.')
391
+ ]
289
392
  }
290
- }
291
- const foreignKeys = db.prepare('PRAGMA foreign_key_check').all()
292
- if (foreignKeys.length > 0) {
393
+ } catch {
293
394
  return {
294
- configuration,
295
- checks: [check('store.integrity', 'fail', 'The store contains broken foreign-key references',
296
- 'Restore a verified backup or repair the affected database rows.')]
395
+ configuration: observed,
396
+ checks: [check('store.database', 'fail', 'The configured database is unreadable or has an incompatible schema',
397
+ 'Check file permissions, restore a verified backup, or pass --db for another store.')]
297
398
  }
298
399
  }
299
- const claims = db.prepare('SELECT count(*) AS count FROM cave_claim').get()?.['count']
300
- if (typeof claims !== 'number') throw new Error('invalid count')
301
- return {
302
- configuration: { ...configuration, claims },
303
- checks: [
304
- check('store.database', 'pass', `CAVE schema ${version} is compatible (${claims} claim(s))`),
305
- check('store.integrity', 'pass', 'SQLite integrity and foreign-key checks passed')
306
- ]
307
- }
308
- } catch {
309
- return {
310
- configuration: observed,
311
- checks: [check('store.database', 'fail', 'The configured database is unreadable or has an incompatible schema',
312
- 'Check file permissions, restore a verified backup, or pass --db for another store.')]
313
- }
314
- } finally {
315
- db?.close()
316
- }
400
+ })()
401
+ return finishDatabaseInspection(result, () => db?.close())
317
402
  }
318
403
 
319
404
  type HooksConfiguration = DoctorReport['configuration']['hooks']
@@ -333,16 +418,11 @@ const inspectHooks = (path: string | undefined): {
333
418
  return {
334
419
  configuration,
335
420
  check: check('config.hooks', 'fail', 'The configured hooks file does not exist',
336
- 'Create a JSON object of hook names to shell command strings, or omit --hooks.')
421
+ 'Create a UTF-8 JSON object with nonblank hook names and Unicode shell command strings, or omit --hooks.')
337
422
  }
338
423
  }
339
424
  try {
340
- const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
341
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) throw new Error('invalid')
342
- const entries = Object.entries(parsed)
343
- if (entries.some(([name, command]) => name.trim() === '' || typeof command !== 'string')) {
344
- throw new Error('invalid')
345
- }
425
+ const entries = Object.entries(readHooks(path))
346
426
  return {
347
427
  configuration: { ...configuration, entries: entries.length },
348
428
  check: check('config.hooks', 'pass', `Hooks configuration is valid (${entries.length} entr${entries.length === 1 ? 'y' : 'ies'})`)
@@ -351,20 +431,21 @@ const inspectHooks = (path: string | undefined): {
351
431
  return {
352
432
  configuration,
353
433
  check: check('config.hooks', 'fail', 'The configured hooks file is unreadable or malformed',
354
- 'Use a JSON object whose values are shell command strings.')
434
+ 'Use valid UTF-8 JSON with nonblank hook names and string commands; names and commands must have no unpaired Unicode surrogates.')
355
435
  }
356
436
  }
357
437
  }
358
438
 
359
439
  export const diagnose = (options: { readonly db?: string, readonly hooks?: string } = {}): DoctorReport => {
360
- const databasePath = options.db ?? defaultDbPath()
361
- const databaseSource: DatabaseConfiguration['source'] = options.db !== undefined ? 'flag' :
440
+ const configuredDb = options.db, configuredHooks = options.hooks
441
+ const databasePath = configuredDb ?? defaultDbPath()
442
+ const databaseSource: DatabaseConfiguration['source'] = configuredDb !== undefined ? 'flag' :
362
443
  process.env['CAVE_DB'] !== undefined ? 'environment' : 'default'
363
444
  const database = inspectDatabase(databasePath, databaseSource)
364
- const hooks = inspectHooks(options.hooks)
445
+ const hooks = inspectHooks(configuredHooks)
365
446
  const checks = [
366
447
  nodeCheck(),
367
- sqliteCheck(),
448
+ ...sqliteCheck(),
368
449
  grammarCheck(),
369
450
  pnpmCheck(),
370
451
  ...database.checks,
@@ -0,0 +1,13 @@
1
+ /** Formatting an exception must not replace the failure being reported. */
2
+ export const errorMessage = (error: unknown, debug = false): string => {
3
+ if (debug) {
4
+ try {
5
+ if (error instanceof Error) {
6
+ const stack = error.stack
7
+ if (stack !== undefined) return String(stack)
8
+ }
9
+ } catch { /* An unavailable stack can still have a useful message. */ }
10
+ }
11
+ try { return error instanceof Error ? String(error.message) : String(error) }
12
+ catch { return '[unprintable thrown value]' }
13
+ }
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ /** One configuration contract for execution and redacted doctor diagnostics. */
4
+ export const readHooks = (path: string): Record<string, string> => {
5
+ const bytes = readFileSync(path)
6
+ let text: string
7
+ try {
8
+ text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
9
+ } catch (cause) {
10
+ throw new TypeError(`${path}: invalid UTF-8 hooks configuration`, { cause })
11
+ }
12
+ const parsed: unknown = JSON.parse(text)
13
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
14
+ Object.entries(parsed).some(([name, command]) => name.trim() === '' || typeof command !== 'string')) {
15
+ throw new Error(`${path}: hooks must be a JSON object of nonblank names to shell template strings`)
16
+ }
17
+ const hooks = parsed as Record<string, string>
18
+ if (Object.entries(hooks).some(([name, command]) => /[\uD800-\uDFFF]/u.test(name) || /[\uD800-\uDFFF]/u.test(command))) {
19
+ throw new Error(`${path}: hook names and commands must contain well-formed Unicode`)
20
+ }
21
+ return hooks
22
+ }