@cavelang/cli 0.33.0 → 0.35.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 (68) hide show
  1. package/README.md +15 -3
  2. package/dist/internal/automate/main.d.ts.map +1 -1
  3. package/dist/internal/automate/main.js +9 -2
  4. package/dist/internal/automate/main.js.map +1 -1
  5. package/dist/internal/connect/declared.d.ts +225 -0
  6. package/dist/internal/connect/declared.d.ts.map +1 -0
  7. package/dist/internal/connect/declared.js +521 -0
  8. package/dist/internal/connect/declared.js.map +1 -0
  9. package/dist/internal/connect/index.d.ts +4 -2
  10. package/dist/internal/connect/index.d.ts.map +1 -1
  11. package/dist/internal/connect/index.js +3 -1
  12. package/dist/internal/connect/index.js.map +1 -1
  13. package/dist/internal/connect/main.d.ts.map +1 -1
  14. package/dist/internal/connect/main.js +356 -10
  15. package/dist/internal/connect/main.js.map +1 -1
  16. package/dist/internal/connect/run.d.ts +47 -1
  17. package/dist/internal/connect/run.d.ts.map +1 -1
  18. package/dist/internal/connect/run.js +81 -33
  19. package/dist/internal/connect/run.js.map +1 -1
  20. package/dist/internal/connect/source.d.ts +33 -2
  21. package/dist/internal/connect/source.d.ts.map +1 -1
  22. package/dist/internal/connect/source.js +168 -8
  23. package/dist/internal/connect/source.js.map +1 -1
  24. package/dist/internal/connect/template.d.ts +18 -0
  25. package/dist/internal/connect/template.d.ts.map +1 -1
  26. package/dist/internal/connect/template.js +28 -0
  27. package/dist/internal/connect/template.js.map +1 -1
  28. package/dist/internal/ingest/main.d.ts.map +1 -1
  29. package/dist/internal/ingest/main.js +19 -2
  30. package/dist/internal/ingest/main.js.map +1 -1
  31. package/dist/internal/mcp/main.d.ts +1 -1
  32. package/dist/internal/mcp/main.d.ts.map +1 -1
  33. package/dist/internal/mcp/main.js +15 -3
  34. package/dist/internal/mcp/main.js.map +1 -1
  35. package/dist/internal/rules/rule.d.ts.map +1 -1
  36. package/dist/internal/rules/rule.js +1 -22
  37. package/dist/internal/rules/rule.js.map +1 -1
  38. package/dist/internal/sync/sync.d.ts +2 -3
  39. package/dist/internal/sync/sync.d.ts.map +1 -1
  40. package/dist/internal/sync/sync.js +3 -15
  41. package/dist/internal/sync/sync.js.map +1 -1
  42. package/dist/internal/view/main.d.ts.map +1 -1
  43. package/dist/internal/view/main.js +4 -3
  44. package/dist/internal/view/main.js.map +1 -1
  45. package/dist/src/cli.d.ts +15 -1
  46. package/dist/src/cli.d.ts.map +1 -1
  47. package/dist/src/cli.js +227 -54
  48. package/dist/src/cli.js.map +1 -1
  49. package/dist/src/commands.d.ts +2 -2
  50. package/dist/src/commands.js +2 -2
  51. package/dist/src/commands.js.map +1 -1
  52. package/dist/src/dispatch.d.ts.map +1 -1
  53. package/dist/src/dispatch.js +7 -1
  54. package/dist/src/dispatch.js.map +1 -1
  55. package/dist/src/doctor.d.ts +1 -1
  56. package/dist/src/doctor.d.ts.map +1 -1
  57. package/dist/src/doctor.js +27 -2
  58. package/dist/src/doctor.js.map +1 -1
  59. package/dist/src/index.d.ts +1 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +1 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/package.json +19 -19
  64. package/src/cli.ts +258 -53
  65. package/src/commands.ts +2 -2
  66. package/src/dispatch.ts +7 -1
  67. package/src/doctor.ts +29 -3
  68. package/src/index.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavelang/cli",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "type": "module",
5
5
  "description": "cave — CLI for parsing, storing, querying and exporting CAVE knowledge.",
6
6
  "keywords": [
@@ -97,26 +97,26 @@
97
97
  "@modelcontextprotocol/server": "2.0.0",
98
98
  "@mozilla/readability": "^0.6.0",
99
99
  "linkedom": "^0.18.13",
100
- "@cavelang/fusion": "0.33.0",
101
- "@cavelang/canonical": "0.33.0",
102
- "@cavelang/core": "0.33.0",
103
- "@cavelang/parser": "0.33.0",
104
- "@cavelang/store": "0.33.0",
105
- "@cavelang/query": "0.33.0",
106
- "@cavelang/highlight": "0.33.0"
100
+ "@cavelang/canonical": "0.35.0",
101
+ "@cavelang/fusion": "0.35.0",
102
+ "@cavelang/core": "0.35.0",
103
+ "@cavelang/highlight": "0.35.0",
104
+ "@cavelang/parser": "0.35.0",
105
+ "@cavelang/query": "0.35.0",
106
+ "@cavelang/store": "0.35.0"
107
107
  },
108
108
  "devDependencies": {
109
- "@cavelang/act": "0.33.0",
110
- "@cavelang/automate": "0.33.0",
111
- "@cavelang/connect": "0.33.0",
112
- "@cavelang/eval": "0.33.0",
113
- "@cavelang/ingest": "0.33.0",
114
- "@cavelang/loop": "0.33.0",
115
- "@cavelang/mcp": "0.33.0",
116
- "@cavelang/rules": "0.33.0",
117
- "@cavelang/shape": "0.33.0",
118
- "@cavelang/view": "0.33.0",
119
- "@cavelang/sync": "0.33.0"
109
+ "@cavelang/act": "0.35.0",
110
+ "@cavelang/automate": "0.35.0",
111
+ "@cavelang/eval": "0.35.0",
112
+ "@cavelang/connect": "0.35.0",
113
+ "@cavelang/ingest": "0.35.0",
114
+ "@cavelang/loop": "0.35.0",
115
+ "@cavelang/rules": "0.35.0",
116
+ "@cavelang/shape": "0.35.0",
117
+ "@cavelang/mcp": "0.35.0",
118
+ "@cavelang/sync": "0.35.0",
119
+ "@cavelang/view": "0.35.0"
120
120
  },
121
121
  "scripts": {
122
122
  "build": "tsc -b && node scripts/consolidate.mjs",
package/src/cli.ts CHANGED
@@ -41,8 +41,9 @@ import { parseArgs } from 'node:util'
41
41
  import { Version } from '@cavelang/core'
42
42
  import { parseDocument, Token } from '@cavelang/parser'
43
43
  import { Registry, standardRegistry } from '@cavelang/canonical'
44
- import { Sensitivity, backup as backupStore, defaultDbPath, open, restoreBackup, verifyBackup } from '@cavelang/store'
45
- import type { Store } from '@cavelang/store'
44
+ import { backup as backupStore, defaultDbPath, kindOf, LocateError, openAt, restoreBackup, Sensitivity, verifyBackup } from '@cavelang/store'
45
+ import type { OpenIntent, Store } from '@cavelang/store'
46
+ import { Declared, assemble as assembleSources } from '@cavelang/connect'
46
47
  import { defaultLimit as defaultQueryLimit, maxLimit as maxQueryLimit, page as caveQueryPage } from '@cavelang/query'
47
48
  import {
48
49
  check as caveCheck, defaultMinScore, defaultStaleDays, gatedIngest, judgePrompt, parseJudgeReply,
@@ -142,10 +143,12 @@ Usage:
142
143
  cave help [command] this text, or one command's options and examples
143
144
 
144
145
  Every command answers --help. --db defaults to $CAVE_DB, or cave.db in
145
- the current directory. The spec lives in the .claude/skills/ directory at
146
+ the current directory, and names a SQLite store or a CAVE text file: text
147
+ replays into an in-memory store for commands that only read (writes refuse
148
+ it), and a read never creates a missing database. The spec lives in the .claude/skills/ directory at
146
149
  the repository root (section index in README.md).`
147
150
 
148
- const dbHelp = `--db <path> database file (default: $CAVE_DB, or cave.db)`
151
+ const dbHelp = `--db <path> store: a SQLite file, or CAVE text read into memory (default: $CAVE_DB, or cave.db)`
149
152
 
150
153
  /** Per-command help, printed for \`cave <command> --help\` and \`cave help <command>\`. */
151
154
  export const commandHelp: Record<string, string> = {
@@ -250,7 +253,7 @@ Examples:
250
253
  query: `cave query — run a CAVE-Q pattern against a store
251
254
 
252
255
  Usage:
253
- cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--no-prelude]
256
+ cave query [--db <path>] <pattern> [WHERE <filter>] [--limit <n>] [--cursor <token>] [--json] [--all] [--aliases] [--as-of <t>] [--at <t>] [--resolve] [--sources] [--no-prelude]
254
257
 
255
258
  Options:
256
259
  ${dbHelp}
@@ -276,6 +279,12 @@ Options:
276
279
  collapse to the row the resolution policy picks
277
280
  (precedence class, reliability-weighted confidence, tx);
278
281
  incompatible with --all
282
+ --sources overlay the store's declared sources (spec §23.4) inside a
283
+ rolled-back transaction — federation-lite: external data is
284
+ consulted at query time, nothing persists. The answer is
285
+ whole (no --cursor, --limit is not a page size); a text
286
+ store followed its local sources on open, so only URL
287
+ sources are loaded here
279
288
  --no-prelude open the store without the standard verb registry
280
289
 
281
290
  Patterns are claim triples with ?variables and optional metadata filters
@@ -286,6 +295,7 @@ with the answer; fully bound patterns print the raw line, comment included.
286
295
 
287
296
  Examples:
288
297
  cave query '?x USES jwt'
298
+ cave query --db notes.cave '?x USES jwt'
289
299
  cave query --db k.db '?x HAS bug: ?bug #security'
290
300
  cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
291
301
  cave query --db k.db 'terrier EXTENDS+ animal'
@@ -294,6 +304,7 @@ Examples:
294
304
  cave query --db k.db 'revenue IS' --at 2026-07
295
305
  cave query --db k.db 'alice WORKS-AT ?org' --at 2021
296
306
  cave query --db k.db 'service HAS owner: ?who' --resolve
307
+ cave query --db k.db '?who WORKS-AT acme' --sources
297
308
  cave query --db k.db '?x ?verb ?y @production' --json`,
298
309
 
299
310
  search: `cave search — full-text search over claims and comments (spec §13.2)
@@ -782,6 +793,57 @@ const readStdin = (): string => {
782
793
  }
783
794
  }
784
795
 
796
+ /**
797
+ * Opens the store `--db` names (spec §13.7): a SQLite file, or a CAVE text
798
+ * file replayed into memory. `read` never creates, migrates, or changes a
799
+ * file — a missing path is an error, not an empty database; `scratch` is a
800
+ * dry run that appends inside a rolled-back transaction, so it may write
801
+ * but creates and migrates nothing; `write` creates a missing SQLite store,
802
+ * migrates an older one, and refuses text files with the materialization
803
+ * hint.
804
+ */
805
+ const openDb = (values: { db?: string, 'no-prelude'?: boolean }, intent: OpenIntent): Store =>
806
+ openAt(values.db ?? defaultDbPath(), {
807
+ intent,
808
+ // A text store follows its declared sources on open (spec §23.4).
809
+ assemble: assembleSources,
810
+ ...values['no-prelude'] === true ? { registry: Registry.empty } : {}
811
+ })
812
+
813
+ const sourcesRollback = Symbol('cave query --sources rollback')
814
+
815
+ /**
816
+ * Runs `body` with the store's declared sources overlaid (spec §23.4): the
817
+ * sources, loaded beforehand (URLs included), append inside a transaction
818
+ * that always rolls back, so external data is consulted at query time and
819
+ * nothing persists, digests included.
820
+ */
821
+ const withSources = <T>(store: Store, discovered: Declared.Discovery, body: () => T): undefined | { value: T } => {
822
+ let result: undefined | { value: T }
823
+ try {
824
+ store.transaction(() => {
825
+ // Discovered on a snapshot: if a writer changed the declarations
826
+ // since, the sequence is stale and the caller rediscovers.
827
+ if (!Declared.sameDeclarations(discovered.baseline, Declared.declarationState(store))) {
828
+ throw sourcesRollback
829
+ }
830
+ for (const source of discovered.sequence) {
831
+ const report = Declared.run(store, source, { force: true })
832
+ if (report.failures.length > 0) {
833
+ throw new LocateError(`source/${source.declared.name}: ${report.failures.map(failure => `${failure.record}: ${failure.problems.join('; ')}`).join('; ')}`)
834
+ }
835
+ }
836
+ result = { value: body() }
837
+ throw sourcesRollback
838
+ })
839
+ } catch (error) {
840
+ if (error !== sourcesRollback) {
841
+ throw error
842
+ }
843
+ }
844
+ return result
845
+ }
846
+
785
847
  const readInput = (files: readonly string[]): string =>
786
848
  files.length === 0 || (files.length === 1 && files[0] === '-') ?
787
849
  readStdin() :
@@ -865,7 +927,7 @@ const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Out
865
927
  allowPositionals: true
866
928
  })
867
929
  const input = readInput(positionals)
868
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
930
+ const store = openDb(values, 'write')
869
931
  try {
870
932
  const options = {
871
933
  strict: values.strict === true,
@@ -911,39 +973,150 @@ export const addCommand = ingestCommand('add')
911
973
  */
912
974
  export const importCommand = ingestCommand('import')
913
975
 
914
- export const queryCommand = (argv: readonly string[]): Output => {
915
- const { values, positionals } = parseArgs({
916
- args: [...argv],
917
- options: {
918
- db: { type: 'string' },
919
- json: { type: 'boolean' },
920
- all: { type: 'boolean' },
921
- aliases: { type: 'boolean' },
922
- 'as-of': { type: 'string' },
923
- at: { type: 'string' },
924
- resolve: { type: 'boolean' },
925
- limit: { type: 'string' },
926
- cursor: { type: 'string' },
927
- 'no-prelude': { type: 'boolean' }
928
- },
929
- allowPositionals: true
930
- })
976
+ const queryOptions = {
977
+ db: { type: 'string' },
978
+ json: { type: 'boolean' },
979
+ all: { type: 'boolean' },
980
+ aliases: { type: 'boolean' },
981
+ 'as-of': { type: 'string' },
982
+ at: { type: 'string' },
983
+ resolve: { type: 'boolean' },
984
+ sources: { type: 'boolean' },
985
+ limit: { type: 'string' },
986
+ cursor: { type: 'string' },
987
+ 'no-prelude': { type: 'boolean' }
988
+ } as const
989
+
990
+ type QueryValues = {
991
+ db?: string
992
+ json?: boolean
993
+ all?: boolean
994
+ aliases?: boolean
995
+ 'as-of'?: string
996
+ at?: string
997
+ resolve?: boolean
998
+ sources?: boolean
999
+ limit?: string
1000
+ cursor?: string
1001
+ 'no-prelude'?: boolean
1002
+ }
1003
+
1004
+ const parseQuery = (argv: readonly string[]): { values: QueryValues, pattern: string } | { failure: Output } => {
1005
+ const { values, positionals } = parseArgs({ args: [...argv], options: queryOptions, allowPositionals: true })
931
1006
  if (positionals.length === 0) {
932
- return fail('cave query: a pattern is required (spec §12.1)\n')
1007
+ return { failure: fail('cave query: a pattern is required (spec §12.1)\n') }
1008
+ }
1009
+ if (values.sources === true && values.cursor !== undefined) {
1010
+ return { failure: fail('cave query: --cursor does not apply to --sources — the overlay exists for one invocation and is answered whole\n') }
933
1011
  }
934
- const pattern = positionals.join('\n')
935
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1012
+ return { values, pattern: positionals.join('\n') }
1013
+ }
1014
+
1015
+ /**
1016
+ * `cave query --sources` — the store with its declared sources overlaid
1017
+ * (spec §23.4). Asynchronous because the sources load first, URLs
1018
+ * included, before the overlay appends inside a transaction that rolls
1019
+ * back; the dispatcher routes `--sources` here. A text store followed its
1020
+ * local sources on open, so only what assembly could not follow — URL
1021
+ * sources and what they declare — is loaded. The overlay exists for this
1022
+ * invocation alone, so the answer is whole: pages are followed inside the
1023
+ * transaction and no cursor is handed out.
1024
+ */
1025
+ export const querySourcesCommand = async (
1026
+ argv: readonly string[],
1027
+ runtime: { readonly fetchImpl?: Declared.DiscoverOptions['fetchImpl'] } = {}
1028
+ ): Promise<Output> => {
1029
+ const parsed = parseQuery(argv)
1030
+ if ('failure' in parsed) {
1031
+ return parsed.failure
1032
+ }
1033
+ const { values, pattern } = parsed
1034
+ if (values.sources !== true) {
1035
+ return queryCommand(argv)
1036
+ }
1037
+ const root = values.db ?? defaultDbPath()
1038
+ // The overlay appends inside a transaction it rolls back: writable, but
1039
+ // never creating or migrating (spec §13.7).
1040
+ const store = openDb(values, 'scratch')
936
1041
  try {
937
- const limit = values.limit === undefined ? defaultQueryLimit : Number(values.limit)
938
- const result = caveQueryPage(store, pattern, {
939
- all: values.all === true,
940
- aliases: values.aliases === true,
941
- resolve: values.resolve === true,
942
- limit,
943
- ...values.cursor === undefined ? {} : { cursor: values.cursor },
944
- ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
945
- ...values.at === undefined ? {} : { at: values.at }
946
- })
1042
+ for (let attempt = 0; attempt < Declared.overlayAttempts; attempt += 1) {
1043
+ const discovered = await Declared.discovery(store, root, {
1044
+ // The overlay applies every source it loads (force), so discovery
1045
+ // simulates that; a text store skips what assembly already followed.
1046
+ force: true,
1047
+ ...kindOf(root) === 'text' ? { skipFollowed: true } : {},
1048
+ ...runtime.fetchImpl === undefined ? {} : { fetchImpl: runtime.fetchImpl }
1049
+ })
1050
+ const page = withSources(store, discovered, () => wholeQuery(store, values, pattern))
1051
+ if (page !== undefined) {
1052
+ return renderQueryPage(store, values, pattern, () => page.value)
1053
+ }
1054
+ }
1055
+ throw Declared.staleOverlay()
1056
+ } catch (error) {
1057
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1058
+ } finally {
1059
+ store.close()
1060
+ }
1061
+ }
1062
+
1063
+ export const queryCommand = (argv: readonly string[]): Output => {
1064
+ const parsed = parseQuery(argv)
1065
+ if ('failure' in parsed) {
1066
+ return parsed.failure
1067
+ }
1068
+ const { values, pattern } = parsed
1069
+ if (values.sources === true) {
1070
+ // Loading sources cannot be synchronous: the in-process entry for the
1071
+ // overlay is `querySourcesCommand`, which the dispatcher uses.
1072
+ return fail('cave query: --sources loads the declared sources first and runs asynchronously — use querySourcesCommand\n')
1073
+ }
1074
+ const store = openDb(values, 'read')
1075
+ try {
1076
+ return renderQueryPage(store, values, pattern, () => queryPage(store, values, pattern))
1077
+ } catch (error) {
1078
+ return fail(`${error instanceof Error ? error.message : String(error)}\n`)
1079
+ } finally {
1080
+ store.close()
1081
+ }
1082
+ }
1083
+
1084
+ /** Every match of the overlay: the pages are followed here, inside the transaction that holds it. */
1085
+ const wholeQuery = (store: Store, values: QueryValues, pattern: string): ReturnType<typeof caveQueryPage> => {
1086
+ const matches: ReturnType<typeof caveQueryPage>['matches'][number][] = []
1087
+ let first: undefined | ReturnType<typeof caveQueryPage>
1088
+ let cursor: undefined | string
1089
+ for (;;) {
1090
+ const page = queryPage(store, { ...values, limit: '1000', ...cursor === undefined ? {} : { cursor } }, pattern)
1091
+ first ??= page
1092
+ matches.push(...page.matches)
1093
+ if (page.next === undefined) {
1094
+ const { next: _next, ...rest } = first
1095
+ return { ...rest, matches }
1096
+ }
1097
+ cursor = page.next
1098
+ }
1099
+ }
1100
+
1101
+ const queryPage = (store: Store, values: QueryValues, pattern: string): ReturnType<typeof caveQueryPage> =>
1102
+ caveQueryPage(store, pattern, {
1103
+ all: values.all === true,
1104
+ aliases: values.aliases === true,
1105
+ resolve: values.resolve === true,
1106
+ limit: values.limit === undefined ? defaultQueryLimit : Number(values.limit),
1107
+ ...values.cursor === undefined ? {} : { cursor: values.cursor },
1108
+ ...values['as-of'] === undefined ? {} : { asOf: values['as-of'] },
1109
+ ...values.at === undefined ? {} : { at: values.at }
1110
+ })
1111
+
1112
+ const renderQueryPage = (
1113
+ store: Store,
1114
+ values: QueryValues,
1115
+ pattern: string,
1116
+ page: () => ReturnType<typeof caveQueryPage>
1117
+ ): Output => {
1118
+ {
1119
+ const result = page()
947
1120
  if (values.json === true) {
948
1121
  return ok(`${JSON.stringify(result, undefined, 2)}\n`)
949
1122
  }
@@ -972,10 +1145,6 @@ export const queryCommand = (argv: readonly string[]): Output => {
972
1145
  line
973
1146
  })
974
1147
  return ok(`${lines.join('\n')}\n${result.next === undefined ? '' : `next: ${result.next}\n`}`)
975
- } catch (error) {
976
- return fail(`${error instanceof Error ? error.message : String(error)}\n`)
977
- } finally {
978
- store.close()
979
1148
  }
980
1149
  }
981
1150
 
@@ -1007,7 +1176,7 @@ export const searchCommand = (argv: readonly string[]): Output => {
1007
1176
  if (!Number.isInteger(limit) || limit < 1 || limit > maxQueryLimit) {
1008
1177
  return fail(`cave search: --limit must be an integer from 1 to ${maxQueryLimit}\n`)
1009
1178
  }
1010
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1179
+ const store = openDb(values, 'read')
1011
1180
  try {
1012
1181
  const raw = values.raw === true
1013
1182
  const found = store.search(query, { raw, limit: limit + 1 })
@@ -1057,7 +1226,7 @@ export const resolveCommand = (argv: readonly string[]): Output => {
1057
1226
  allowPositionals: false
1058
1227
  })
1059
1228
  const percent = (value: number): string => `${Math.round(value * 1000) / 10}%`
1060
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1229
+ const store = openDb(values, 'read')
1061
1230
  try {
1062
1231
  if (values.policy === true) {
1063
1232
  const policy = store.resolutionPolicy()
@@ -1120,7 +1289,12 @@ export const deriveCommand = (argv: readonly string[]): Output => {
1120
1289
  if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
1121
1290
  return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
1122
1291
  }
1123
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1292
+ // The intent follows the branch order below: listing reads, retracting
1293
+ // writes, a dry run appends inside a rolled-back transaction.
1294
+ const store = openDb(values,
1295
+ values.list === true ? 'read' :
1296
+ values.retract !== undefined ? 'write' :
1297
+ values['dry-run'] === true ? 'scratch' : 'write')
1124
1298
  try {
1125
1299
  if (values.list === true) {
1126
1300
  const rules = listRules(store)
@@ -1274,7 +1448,14 @@ export const actCommand = (argv: readonly string[]): Output => {
1274
1448
  },
1275
1449
  allowPositionals: true
1276
1450
  })
1277
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1451
+ // The intent follows the branch order below: declaring and retracting
1452
+ // write, listing reads, a dry-run execution appends inside a rolled-back
1453
+ // transaction.
1454
+ const store = openDb(values,
1455
+ values.declare === true ? 'write' :
1456
+ values.list === true ? 'read' :
1457
+ values.retract !== undefined ? 'write' :
1458
+ values['dry-run'] === true ? 'scratch' : 'write')
1278
1459
  try {
1279
1460
  if (values.declare === true) {
1280
1461
  const declaration = declareActions(store, readInput(positionals))
@@ -1395,7 +1576,7 @@ export const checkCommand = (argv: readonly string[]): Output => {
1395
1576
  if (!Number.isFinite(staleDays) || staleDays < 0) {
1396
1577
  return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
1397
1578
  }
1398
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1579
+ const store = openDb(values, 'read')
1399
1580
  try {
1400
1581
  const report = caveCheck(store, { staleDays })
1401
1582
  const out = values.json === true ?
@@ -1444,7 +1625,15 @@ export const backupCommand = (argv: readonly string[]): Output => {
1444
1625
  if (values.out === undefined) {
1445
1626
  return fail('cave backup: --out <file> is required\n')
1446
1627
  }
1447
- const store = open(values.db ?? defaultDbPath())
1628
+ const dbPath = values.db ?? defaultDbPath()
1629
+ // A text store is replayed into memory, so the backup API cannot see that
1630
+ // the destination aliases the source: refuse before the file could be
1631
+ // replaced by a SQLite snapshot of itself (a SQLite source is covered by
1632
+ // the API's own check).
1633
+ if (sameFile(dbPath, values.out)) {
1634
+ return fail(`cave backup: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1635
+ }
1636
+ const store = openAt(dbPath, { intent: 'read', assemble: assembleSources })
1448
1637
  try {
1449
1638
  return ok(snapshotLine('created', backupStore(store, values.out, { force: values.force === true })))
1450
1639
  } finally {
@@ -1527,7 +1716,7 @@ export const suggestAliasCommand = async (
1527
1716
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1528
1717
  return fail(`cave suggest-alias: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1529
1718
  }
1530
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1719
+ const store = openDb(values, values.write === true ? 'write' : 'read')
1531
1720
  try {
1532
1721
  let suggestions = suggestAliases(store, { minScore, ...limit === undefined ? {} : { limit } })
1533
1722
  if (values.agent !== undefined && suggestions.length > 0) {
@@ -1584,7 +1773,7 @@ export const syncCommand = (argv: readonly string[]): Output => {
1584
1773
  return fail('cave sync: exactly one source is required — a CAVE store file, ;@-annotated text, or - for stdin (spec §28.5)\n')
1585
1774
  }
1586
1775
  const dbPath = values.db ?? defaultDbPath()
1587
- const store = open(dbPath, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1776
+ const store = openDb(values, values['dry-run'] === true ? 'scratch' : 'write')
1588
1777
  try {
1589
1778
  const options = {
1590
1779
  into: values.into === undefined ? labelOf(dbPath) : sanitizeLabel(values.into),
@@ -1645,7 +1834,10 @@ export const reportCommand = (argv: readonly string[]): Output => {
1645
1834
  if (maximum === undefined) {
1646
1835
  return fail(`cave report: --max-sensitivity expects ${Sensitivity.levels.join(', ')}, got ${JSON.stringify(values['max-sensitivity'])}\n`)
1647
1836
  }
1648
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
1837
+ if (values.out !== undefined && sameFile(values.db ?? defaultDbPath(), values.out)) {
1838
+ return fail(`cave report: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1839
+ }
1840
+ const store = openDb(values, 'read')
1649
1841
  try {
1650
1842
  const rendered = caveReport(store, template, {
1651
1843
  aliases: values.aliases === true,
@@ -1713,7 +1905,7 @@ export const exportCommand = (argv: readonly string[]): Output => {
1713
1905
  if (values.out !== undefined && sameFile(db, values.out)) {
1714
1906
  return fail(`cave export: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1715
1907
  }
1716
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1908
+ const store = openDb(values, 'read')
1717
1909
  try {
1718
1910
  const text = store.exportText({ current: values.current === true, tx: values.tx === true, maxSensitivity: maximum })
1719
1911
  if (values.out === undefined) {
@@ -1752,7 +1944,7 @@ export const generateCommand = (argv: readonly string[]): Output => {
1752
1944
  if (values.out !== undefined && sameFile(db, values.out)) {
1753
1945
  return fail(`cave generate: --out '${values.out}' is the source database — refusing to overwrite it\n`)
1754
1946
  }
1755
- const store = open(db, values['no-prelude'] === true ? { registry: Registry.empty } : {})
1947
+ const store = openDb(values, 'read')
1756
1948
  try {
1757
1949
  const generated = generateClient(store, version === undefined ? {} : { version })
1758
1950
  if (!generated.ok) {
@@ -1817,7 +2009,7 @@ export const reconstructCommand = async (
1817
2009
  if (timeoutSeconds !== undefined && (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)) {
1818
2010
  return fail(`cave reconstruct: --timeout must be a positive number of seconds, got '${values.timeout}'\n`)
1819
2011
  }
1820
- const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
2012
+ const store = openDb(values, 'read')
1821
2013
  try {
1822
2014
  const graph = sqliteStore(store)
1823
2015
  const result = values.agent === undefined ?
@@ -1902,6 +2094,19 @@ export const cave = (argv: readonly string[]): Output => {
1902
2094
  (rest.includes('--help') || rest.includes('-h'))) {
1903
2095
  return ok(`${commandHelp[canonical]}\n`)
1904
2096
  }
2097
+ try {
2098
+ return run(canonical, rest)
2099
+ } catch (error) {
2100
+ // A --db path that is not what the command needs (spec §13.7) is a
2101
+ // usage failure, reported the way dispatch reports thrown errors.
2102
+ if (error instanceof LocateError) {
2103
+ return fail(`cave ${canonical}: ${error.message}\n`)
2104
+ }
2105
+ throw error
2106
+ }
2107
+ }
2108
+
2109
+ const run = (canonical: undefined | string, rest: readonly string[]): Output => {
1905
2110
  switch (canonical) {
1906
2111
  case 'parse':
1907
2112
  return parseCommand(rest)
@@ -1948,6 +2153,6 @@ export const cave = (argv: readonly string[]): Output => {
1948
2153
  case '-h':
1949
2154
  return ok(`${usage}\n`)
1950
2155
  default:
1951
- return fail(`cave: unknown command ${JSON.stringify(command)}\n\n${usage}\n`, 2)
2156
+ return fail(`cave: unknown command ${JSON.stringify(canonical)}\n\n${usage}\n`, 2)
1952
2157
  }
1953
2158
  }
package/src/commands.ts CHANGED
@@ -18,7 +18,7 @@ export const commandRegistry = [
18
18
  { name: 'highlight', importantOptions: [] },
19
19
  { name: 'add', importantOptions: ['--strict', '--check', '--no-prelude', '--no-src'] },
20
20
  { name: 'import', importantOptions: ['--strict', '--no-prelude'] },
21
- { name: 'query', importantOptions: ['--json', '--limit', '--cursor', '--all', '--aliases', '--as-of', '--at', '--resolve', '--no-prelude'] },
21
+ { name: 'query', importantOptions: ['--json', '--limit', '--cursor', '--all', '--aliases', '--as-of', '--at', '--resolve', '--sources', '--no-prelude'] },
22
22
  { name: 'search', importantOptions: ['--raw', '--limit', '--json', '--no-prelude'] },
23
23
  { name: 'resolve', importantOptions: ['--aliases', '--policy', '--json', '--no-prelude'] },
24
24
  { name: 'derive', importantOptions: ['--dry-run', '--full', '--aliases', '--min-conf', '--max-passes', '--list', '--retract', '--json', '--no-prelude'] },
@@ -36,7 +36,7 @@ export const commandRegistry = [
36
36
  { name: 'mcp', importantOptions: ['--read-only', '--permissions', '--tools', '--hooks', '--no-prelude', '--src', '--no-src'], delegated: true },
37
37
  { name: 'ingest', importantOptions: ['--agent', '--stdout', '--lenient', '--plan', '--dry-run', '--json', '--no-prelude'], delegated: true },
38
38
  { name: 'eval', importantOptions: ['--agent', '--judge', '--runs', '--stdout', '--min', '--json', '--no-prelude'], delegated: true },
39
- { name: 'connect', importantOptions: ['--map', '--key', '--watch', '--prune', '--query', '--dry-run', '--no-prelude'], delegated: true },
39
+ { name: 'connect', importantOptions: ['--map', '--key', '--watch', '--prune', '--query', '--dry-run', '--list', '--no-prelude'], delegated: true },
40
40
  { name: 'reconstruct', importantOptions: ['--query', '--agent', '--steps', '--claims', '--timeout', '--trace', '--no-prelude'] },
41
41
  { name: 'doctor', importantOptions: ['--hooks', '--json'] },
42
42
  { name: 'demo', importantOptions: [] },
package/src/dispatch.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** One promise-based command dispatcher and process lifecycle for every command. */
2
2
 
3
- import { cave, highlightCommand, reconstructCommand, suggestAliasCommand } from './cli.ts'
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
6
 
@@ -78,6 +78,12 @@ const execute = async (raw: readonly string[], runtime: CommandRuntime): Promise
78
78
  const { runServe } = await import('@cavelang/view')
79
79
  return await runServe(rest, context)
80
80
  }
81
+ case 'q':
82
+ case 'query':
83
+ if (rest.includes('--sources')) {
84
+ return await runOutput(runtime, argv, querySourcesCommand)
85
+ }
86
+ break
81
87
  case 'reconstruct':
82
88
  return await runOutput(runtime, argv,
83
89
  args => reconstructCommand(args, { ...runtime.signal === undefined ? {} : { signal: runtime.signal } }))
package/src/doctor.ts CHANGED
@@ -6,7 +6,8 @@ import { dirname, join, parse } from 'node:path'
6
6
  import { parseArgs } from 'node:util'
7
7
  import { Version } from '@cavelang/core'
8
8
  import { directCommand, runProcessSync } from '@cavelang/loop'
9
- import { Schema, defaultDbPath } from '@cavelang/store'
9
+ import { LocateError, Schema, defaultDbPath, kindOf, openText } from '@cavelang/store'
10
+ import { assemble } from '@cavelang/connect'
10
11
  import { nodeSqliteAdapter } from '@cavelang/store/adapter/node'
11
12
  import type { SqliteDatabase } from '@cavelang/store/adapter'
12
13
 
@@ -32,7 +33,7 @@ export type DoctorReport = {
32
33
  readonly configuration: {
33
34
  readonly database: {
34
35
  readonly source: 'flag' | 'environment' | 'default'
35
- readonly kind: 'memory' | 'file'
36
+ readonly kind: 'memory' | 'file' | 'text'
36
37
  readonly exists: boolean
37
38
  readonly schemaVersion?: number
38
39
  readonly claims?: number
@@ -216,6 +217,31 @@ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']):
216
217
  'Run cave add to create it, or pass --db for an existing store.')]
217
218
  }
218
219
  }
220
+ if (!memory && kindOf(path) === 'text') {
221
+ // A CAVE text file is a read-only store assembled in memory (spec
222
+ // §13.7, §23.4): replay it, sources included, and count what it holds.
223
+ const configuration: DatabaseConfiguration = { ...base, kind: 'text' }
224
+ try {
225
+ const store = openText(path, { assemble })
226
+ try {
227
+ const claims = store.currentBeliefs().filter(row => row.conf > 0).length
228
+ return {
229
+ configuration: { ...configuration, claims },
230
+ checks: [check('store.database', 'pass',
231
+ `CAVE text store assembled in memory (${claims} current claim(s)); text stores are read-only`)]
232
+ }
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.')]
242
+ }
243
+ }
244
+ }
219
245
 
220
246
  let db: SqliteDatabase | undefined
221
247
  let observed = base
@@ -249,7 +275,7 @@ const inspectDatabase = (path: string, source: DatabaseConfiguration['source']):
249
275
  configuration,
250
276
  checks: [check('store.database', 'warn',
251
277
  `Database schema ${version} needs migration to schema ${Schema.currentVersion}`,
252
- 'Back up the database, then open it with a normal cave command to migrate it.')]
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.')]
253
279
  }
254
280
  }
255
281
 
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * and `runCli` expose the shared awaited command lifecycle.
5
5
  */
6
6
 
7
- export { actCommand, addCommand, backupCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, generateCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, reconstructCommand, reportCommand, resolveCommand, restoreCommand, searchCommand, suggestAliasCommand, syncCommand, usage, versionCommand } from './cli.ts'
7
+ export { actCommand, addCommand, backupCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, generateCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, querySourcesCommand, reconstructCommand, reportCommand, resolveCommand, restoreCommand, searchCommand, suggestAliasCommand, syncCommand, usage, versionCommand } from './cli.ts'
8
8
  export { dispatch, runCli } from './dispatch.ts'
9
9
  export type { CommandRuntime } from './dispatch.ts'
10
10
  export { commandRegistry } from './commands.ts'