@cavelang/cli 0.5.0 → 0.13.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.
- package/README.md +4 -3
- package/dist/src/cli.d.ts +18 -9
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +504 -24
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/main.js +7 -2
- package/dist/src/main.js.map +1 -1
- package/package.json +14 -10
- package/src/cli.ts +516 -25
- package/src/index.ts +2 -2
- package/src/main.ts +6 -2
package/src/cli.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `cave parse [file]` — lint / dump the AST (`--json`)
|
|
9
9
|
* - `cave highlight [file]` — ANSI syntax colors (async, routed in `main.ts`)
|
|
10
|
-
* - `cave add [--db <path>] [file…]` — ingest into a store (`--strict`)
|
|
11
|
-
* - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`)
|
|
10
|
+
* - `cave add [--db <path>] [file…]` — ingest into a store (`--strict`, `--check`)
|
|
11
|
+
* - `cave query [--db <path>] '<pattern>'` — CAVE-Q (`--json`, `--all`, `--aliases`, `--as-of`)
|
|
12
|
+
* - `cave derive [--db <path>] [rules.cave…]` — fire rules (`--dry-run`, `--full`, `--list`, `--retract`)
|
|
13
|
+
* - `cave act [--db <path>] <name> [param=value…]` — execute an action (spec §25; `--declare`, `--list`, `--retract`)
|
|
14
|
+
* - `cave check [--db <path>]` — knowledge health report (`--stale`, `--json`)
|
|
12
15
|
* - `cave export [--db <path>]` — canonical text out (`--current`)
|
|
16
|
+
* - `cave connect [--db <path>] <source>` — deterministic structured ingestion (async, routed in `main.ts`)
|
|
13
17
|
* - `cave demo` — the cave-loop multi-hop recovery demo
|
|
14
18
|
* - `cave version` — print the cave version
|
|
15
19
|
* - `cave help [command]` — overview, or one command's help
|
|
@@ -25,6 +29,12 @@ import { parseDocument } from '@cavelang/parser'
|
|
|
25
29
|
import { Registry, standardRegistry } from '@cavelang/canonical'
|
|
26
30
|
import { defaultDbPath, open } from '@cavelang/store'
|
|
27
31
|
import { query as caveQuery } from '@cavelang/query'
|
|
32
|
+
import { check as caveCheck, defaultStaleDays, gatedIngest } from '@cavelang/shape'
|
|
33
|
+
import type { Report, Violation } from '@cavelang/shape'
|
|
34
|
+
import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/rules'
|
|
35
|
+
import type { Declaration, DeriveReport } from '@cavelang/rules'
|
|
36
|
+
import { act, declareActions, listActions, retractAction } from '@cavelang/act'
|
|
37
|
+
import type { ActReport } from '@cavelang/act'
|
|
28
38
|
import { Demo } from '@cavelang/loop'
|
|
29
39
|
|
|
30
40
|
export type Output = {
|
|
@@ -44,12 +54,17 @@ export const usage = `cave — Compressed Atomic Verb Expressions
|
|
|
44
54
|
Usage:
|
|
45
55
|
cave parse [file...] [--json] lint CAVE text (stdin when no file)
|
|
46
56
|
cave highlight [file...] print CAVE text with ANSI syntax colors
|
|
47
|
-
cave add [--db <path>] [file...] ingest into a store [--strict] [--no-prelude]
|
|
48
|
-
cave import [--db <path>] [file...] restore/merge from CAVE text (
|
|
49
|
-
cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--no-prelude]
|
|
57
|
+
cave add [--db <path>] [file...] ingest into a store [--strict] [--check] [--no-prelude] [--no-src]
|
|
58
|
+
cave import [--db <path>] [file...] restore/merge from CAVE text (add without @src: stamping)
|
|
59
|
+
cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
|
|
60
|
+
cave derive [--db <path>] [rules.cave..] declare + fire rules (spec §24) [--dry-run] [--full] [--list] [--retract <rule>]
|
|
61
|
+
cave act [--db <path>] <name> [p=v...] execute an action (spec §25) [--dry-run] [--no-check] [--hooks <file>]
|
|
62
|
+
cave act --declare [file...] declare actions from a CAVE document; --list / --retract <name> manage them
|
|
63
|
+
cave check [--db <path>] knowledge health report (spec §20) [--stale <days>] [--json]
|
|
50
64
|
cave export [--db <path>] [--out <file>] emit canonical CAVE text [--current] [--no-prelude]
|
|
51
65
|
cave mcp [--db <path>] serve the engine as an MCP server on stdio [--no-prelude]
|
|
52
66
|
cave ingest [--db <path>] <globs/urls..> LLM-driven ingestion of files and web pages
|
|
67
|
+
cave connect <source> --map <file> deterministic structured ingestion (CSV/JSON/SQLite/URL, spec §23)
|
|
53
68
|
cave demo run the cave-loop reconstruction demo
|
|
54
69
|
cave version print the cave version
|
|
55
70
|
cave help [command] this text, or one command's options and examples
|
|
@@ -97,21 +112,27 @@ Examples:
|
|
|
97
112
|
add: `cave add — ingest CAVE text into a knowledge database
|
|
98
113
|
|
|
99
114
|
Usage:
|
|
100
|
-
cave add [--db <path>] [file...] [--strict] [--no-prelude]
|
|
115
|
+
cave add [--db <path>] [file...] [--strict] [--check] [--no-prelude] [--no-src]
|
|
101
116
|
|
|
102
117
|
Options:
|
|
103
118
|
${dbHelp}
|
|
104
119
|
--strict reject the whole ingest on any problem
|
|
120
|
+
--check shape gate (spec §20.3): roll the append back if it
|
|
121
|
+
introduces new expectation violations
|
|
105
122
|
--no-prelude open the store without the standard verb registry
|
|
123
|
+
--no-src do not stamp actor provenance on appended claims
|
|
106
124
|
|
|
107
|
-
Reads stdin when no file (or \`-\`) is given.
|
|
125
|
+
Reads stdin when no file (or \`-\`) is given. Claims that carry no @src:
|
|
126
|
+
context are stamped @src:cli (spec §9.5); use \`cave import\` to replay
|
|
127
|
+
exported text without stamping.
|
|
108
128
|
|
|
109
129
|
Examples:
|
|
110
130
|
cave add --db k.db notes.cave
|
|
111
131
|
echo 'auth USES jwt @ 90%' | cave add
|
|
112
|
-
cave add --db k.db --strict reviewed.cave
|
|
132
|
+
cave add --db k.db --strict reviewed.cave
|
|
133
|
+
cave add --db k.db --check new-services.cave`,
|
|
113
134
|
|
|
114
|
-
import: `cave import — restore/merge a database from CAVE text (
|
|
135
|
+
import: `cave import — restore/merge a database from CAVE text (add without @src: stamping)
|
|
115
136
|
|
|
116
137
|
Usage:
|
|
117
138
|
cave import [--db <path>] [file...] [--strict] [--no-prelude]
|
|
@@ -123,7 +144,9 @@ Options:
|
|
|
123
144
|
|
|
124
145
|
Canonical CAVE text is the interchange format (spec §2.2): importing a
|
|
125
146
|
file written by \`cave export\` replays its claims append-only, preserving
|
|
126
|
-
the belief series, qualifier edges and in-band declarations.
|
|
147
|
+
the belief series, qualifier edges and in-band declarations. Unlike
|
|
148
|
+
\`cave add\`, import never stamps actor provenance — replayed claims must
|
|
149
|
+
keep the claim keys they were exported with (spec §9.5).
|
|
127
150
|
|
|
128
151
|
Examples:
|
|
129
152
|
cave export --db old.db --out backup.cave
|
|
@@ -132,12 +155,16 @@ Examples:
|
|
|
132
155
|
query: `cave query — run a CAVE-Q pattern against a store
|
|
133
156
|
|
|
134
157
|
Usage:
|
|
135
|
-
cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--no-prelude]
|
|
158
|
+
cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
|
|
136
159
|
|
|
137
160
|
Options:
|
|
138
161
|
${dbHelp}
|
|
139
162
|
--json emit matches as JSON
|
|
140
163
|
--all match all beliefs, not just current ones
|
|
164
|
+
--aliases resolve entities through current ALIAS claims (spec §13.6)
|
|
165
|
+
--as-of <t> resolve beliefs as of a past moment (spec §12.3): a date
|
|
166
|
+
(whole day included), a timestamp (whole second), or a
|
|
167
|
+
transaction id — rows recorded later are invisible
|
|
141
168
|
--no-prelude open the store without the standard verb registry
|
|
142
169
|
|
|
143
170
|
Patterns are claim triples with ?variables and optional metadata filters
|
|
@@ -149,8 +176,119 @@ Examples:
|
|
|
149
176
|
cave query --db k.db '?x HAS bug: ?bug #security'
|
|
150
177
|
cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
|
|
151
178
|
cave query --db k.db 'terrier EXTENDS+ animal'
|
|
179
|
+
cave query --db k.db '?x USES postgres' --aliases
|
|
180
|
+
cave query --db k.db 'server IS compromised' --as-of 2026-01-15
|
|
152
181
|
cave query --db k.db '?x ?verb ?y @production' --json`,
|
|
153
182
|
|
|
183
|
+
derive: `cave derive — declare and fire rules (spec §24)
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
cave derive [--db <path>] [rules.cave...] [--dry-run] [--full] [--aliases]
|
|
187
|
+
[--min-conf <p>] [--max-passes <n>] [--json] [--no-prelude]
|
|
188
|
+
cave derive [--db <path>] --list
|
|
189
|
+
cave derive [--db <path>] --retract <digest|subject>
|
|
190
|
+
|
|
191
|
+
Options:
|
|
192
|
+
${dbHelp}
|
|
193
|
+
--dry-run compute and report inside a rolled-back transaction
|
|
194
|
+
--full ignore stored watermarks — re-fire every rule (spec §24.4)
|
|
195
|
+
--aliases premises match through the alias closure (spec §13.6)
|
|
196
|
+
--min-conf <p> do not assert conclusions below this confidence
|
|
197
|
+
(0..1 or N%, default ${defaultMinConf})
|
|
198
|
+
--max-passes <n> fixpoint guard (default ${defaultMaxPasses})
|
|
199
|
+
--list print the store's current rules and exit
|
|
200
|
+
--retract <r> retract a rule (digest, unambiguous prefix, or subject)
|
|
201
|
+
together with everything it derived, and exit
|
|
202
|
+
--json emit the report as JSON
|
|
203
|
+
--no-prelude open the store without the standard verb registry
|
|
204
|
+
|
|
205
|
+
Rule files are CAVE documents whose \`premises => conclusion\` lines are
|
|
206
|
+
rules (spec §24.1); other lines — verb declarations the rules need — are
|
|
207
|
+
prelude, ingested first. Rules are stored in-band as
|
|
208
|
+
\`rule/<digest> HAS rule: \\\`…\\\`\` claims, so \`cave derive\` with no file
|
|
209
|
+
fires whatever the store already knows. Derived claims are stamped
|
|
210
|
+
@src:rule/<digest>, carry BECAUSE edges to their premise rows and a VIA
|
|
211
|
+
edge to the rule, and update append-only: re-runs are idempotent and
|
|
212
|
+
watermark-incremental, and conclusions whose premises no longer hold are
|
|
213
|
+
retracted (spec §24.2–§24.5).
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
cave derive --db k.db rules.cave
|
|
217
|
+
cave derive --db k.db
|
|
218
|
+
echo '?x NEEDS ?y, ?y NEEDS ?z => ?x NEEDS ?z' | cave derive --db k.db -
|
|
219
|
+
cave derive --db k.db --dry-run --json
|
|
220
|
+
cave derive --db k.db --list
|
|
221
|
+
cave derive --db k.db --retract 4a0bb974f43c`,
|
|
222
|
+
|
|
223
|
+
act: `cave act — execute, declare, list and retract actions (spec §25)
|
|
224
|
+
|
|
225
|
+
Usage:
|
|
226
|
+
cave act [--db <path>] <name> [param=value...] [--dry-run] [--no-check]
|
|
227
|
+
[--aliases] [--hooks <file>] [--json] [--no-prelude]
|
|
228
|
+
cave act [--db <path>] --declare [file...]
|
|
229
|
+
cave act [--db <path>] --list [--json]
|
|
230
|
+
cave act [--db <path>] --retract <name>
|
|
231
|
+
|
|
232
|
+
Options:
|
|
233
|
+
${dbHelp}
|
|
234
|
+
--dry-run validate and report inside a rolled-back transaction;
|
|
235
|
+
hooks never fire
|
|
236
|
+
--no-check skip the shape gate (spec §25.3) — by default an
|
|
237
|
+
execution that introduces new EXPECTS violations is
|
|
238
|
+
rolled back
|
|
239
|
+
--aliases preconditions match through the alias closure (spec §13.6)
|
|
240
|
+
--hooks <file> JSON file of out-of-band hook command templates,
|
|
241
|
+
name → shell template (spec §25.4); default: $CAVE_HOOKS
|
|
242
|
+
--declare declare the actions of CAVE documents (stdin when no
|
|
243
|
+
file); other lines are prelude, ingested first
|
|
244
|
+
--list print the store's current actions and exit
|
|
245
|
+
--retract <n> retract an action's declaration (effects of past
|
|
246
|
+
executions stay recorded) and exit
|
|
247
|
+
--json emit the report as JSON
|
|
248
|
+
--no-prelude open the store without the standard verb registry
|
|
249
|
+
|
|
250
|
+
An action is a named, governed write template (spec §25):
|
|
251
|
+
|
|
252
|
+
action/mark-deployed HAS action: \`?service, ?version,
|
|
253
|
+
?service IS service => ?service HAS deployed-version: ?version\`
|
|
254
|
+
|
|
255
|
+
Executing validates the parameters, checks each precondition against
|
|
256
|
+
current belief (a premise with no match fails the action — nothing is
|
|
257
|
+
appended), then appends the effects atomically, stamped
|
|
258
|
+
@src:action/<name> with BECAUSE/VIA lineage. Re-runs are idempotent. A
|
|
259
|
+
declared hook (action/<name> HAS hook: <hook>) runs after commit when
|
|
260
|
+
the configuration defines it — {action} and {param} placeholders are
|
|
261
|
+
shell-quoted, the appended claims arrive on stdin.
|
|
262
|
+
|
|
263
|
+
Examples:
|
|
264
|
+
cave act --db k.db --declare actions.cave
|
|
265
|
+
cave act --db k.db mark-deployed service=api-gateway version=1.2.3
|
|
266
|
+
cave act --db k.db mark-deployed service=api version=2 --dry-run
|
|
267
|
+
cave act --db k.db mark-deployed service=api version=2 --hooks hooks.json
|
|
268
|
+
cave act --db k.db --list
|
|
269
|
+
cave act --db k.db --retract mark-deployed`,
|
|
270
|
+
|
|
271
|
+
check: `cave check — knowledge health report (spec §20)
|
|
272
|
+
|
|
273
|
+
Usage:
|
|
274
|
+
cave check [--db <path>] [--stale <days>] [--json] [--no-prelude]
|
|
275
|
+
|
|
276
|
+
Options:
|
|
277
|
+
${dbHelp}
|
|
278
|
+
--stale <days> staleness horizon (default: ${defaultStaleDays})
|
|
279
|
+
--json emit the full report as JSON
|
|
280
|
+
--no-prelude open the store without the standard verb registry
|
|
281
|
+
|
|
282
|
+
Reads the store against its own in-band EXPECTS declarations (spec §20.1)
|
|
283
|
+
and reports shape violations, stale current beliefs, review candidates
|
|
284
|
+
(conf 0.3–0.7), alias disagreements (spec §13.6) and coverage stats.
|
|
285
|
+
Exits 1 when violations exist; everything else is advisory.
|
|
286
|
+
|
|
287
|
+
Examples:
|
|
288
|
+
cave check --db k.db
|
|
289
|
+
cave check --db k.db --stale 30
|
|
290
|
+
cave check --db k.db --json | jq '.violations'`,
|
|
291
|
+
|
|
154
292
|
export: `cave export — emit canonical CAVE text from a store
|
|
155
293
|
|
|
156
294
|
Usage:
|
|
@@ -226,26 +364,50 @@ export const highlightCommand = async (argv: readonly string[]): Promise<Output>
|
|
|
226
364
|
return ok(ansi(input))
|
|
227
365
|
}
|
|
228
366
|
|
|
229
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Shared by `add` and `import`; the difference is actor provenance
|
|
369
|
+
* (spec §9.5). `add` records knowledge authored now, so claims without a
|
|
370
|
+
* `@src:` context are stamped `@src:cli` (`--no-src` opts out); `import`
|
|
371
|
+
* replays interchange text, which must preserve claim keys as exported,
|
|
372
|
+
* so it never stamps.
|
|
373
|
+
*/
|
|
374
|
+
const formatViolation = (violation: Violation): string =>
|
|
375
|
+
`${violation.entity} missing ${violation.expectation.kind} ${violation.expectation.name} ` +
|
|
376
|
+
`(${violation.entity} IS ${violation.via}; ${violation.expectation.type} EXPECTS ${violation.expectation.name})`
|
|
377
|
+
|
|
378
|
+
const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Output => {
|
|
230
379
|
const { values, positionals } = parseArgs({
|
|
231
380
|
args: [...argv],
|
|
232
381
|
options: {
|
|
233
382
|
db: { type: 'string' },
|
|
234
383
|
strict: { type: 'boolean' },
|
|
235
|
-
'no-prelude': { type: 'boolean' }
|
|
384
|
+
'no-prelude': { type: 'boolean' },
|
|
385
|
+
...name === 'add' ? { 'no-src': { type: 'boolean' }, check: { type: 'boolean' } } : {}
|
|
236
386
|
},
|
|
237
387
|
allowPositionals: true
|
|
238
388
|
})
|
|
239
389
|
const input = readInput(positionals)
|
|
240
390
|
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
241
391
|
try {
|
|
242
|
-
const
|
|
243
|
-
|
|
392
|
+
const options = {
|
|
393
|
+
strict: values.strict === true,
|
|
394
|
+
...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
|
|
395
|
+
}
|
|
396
|
+
// The shape gate (spec §20.3): append + check in one transaction,
|
|
397
|
+
// rolled back when the append introduces new expectation violations.
|
|
398
|
+
const outcome = name === 'add' && values.check === true ?
|
|
399
|
+
gatedIngest(store, input, options) :
|
|
400
|
+
{ ok: true as const, result: store.ingest(input, options) }
|
|
401
|
+
if (!outcome.ok) {
|
|
402
|
+
const detail = outcome.violations.map(violation => ` ${formatViolation(violation)}`).join('\n')
|
|
403
|
+
return fail(`rejected: ${outcome.violations.length} new violation(s), nothing added (spec §20.3)\n${detail}\n`)
|
|
404
|
+
}
|
|
405
|
+
const problems = outcome.result.problems
|
|
244
406
|
.map(problem => `line ${problem.line}: ${problem.message}`)
|
|
245
407
|
.join('\n')
|
|
246
408
|
return {
|
|
247
409
|
code: 0,
|
|
248
|
-
out: `added ${result.ids.length} claim(s), ${result.edges} edge(s)\n`,
|
|
410
|
+
out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
|
|
249
411
|
err: problems === '' ? '' : `${problems}\n`
|
|
250
412
|
}
|
|
251
413
|
} catch (error) {
|
|
@@ -259,13 +421,15 @@ export const addCommand = ingestCommand('add')
|
|
|
259
421
|
|
|
260
422
|
/**
|
|
261
423
|
* `cave import` — restore/merge a knowledge database from CAVE text files.
|
|
262
|
-
*
|
|
424
|
+
* `add` minus provenance stamping: canonical CAVE text *is* the interchange
|
|
263
425
|
* format (spec §2.2), so importing a file exported with `cave export`
|
|
264
|
-
* replays its claims append-only
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
426
|
+
* replays its claims append-only — and because contexts are part of claim
|
|
427
|
+
* identity, replay must not add a `@src:` stamp (spec §9.5). What the text
|
|
428
|
+
* round trip preserves: every claim with metadata, full belief-series order
|
|
429
|
+
* (rows export in tx order and re-ingest with fresh monotonic tx ids),
|
|
430
|
+
* qualifier/grouping edges, and in-band registry declarations. Original tx
|
|
431
|
+
* timestamps are re-minted — the text format carries no transaction
|
|
432
|
+
* identity.
|
|
269
433
|
*/
|
|
270
434
|
export const importCommand = ingestCommand('import')
|
|
271
435
|
|
|
@@ -276,6 +440,8 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
276
440
|
db: { type: 'string' },
|
|
277
441
|
json: { type: 'boolean' },
|
|
278
442
|
all: { type: 'boolean' },
|
|
443
|
+
aliases: { type: 'boolean' },
|
|
444
|
+
'as-of': { type: 'string' },
|
|
279
445
|
'no-prelude': { type: 'boolean' }
|
|
280
446
|
},
|
|
281
447
|
allowPositionals: true
|
|
@@ -286,7 +452,11 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
286
452
|
const pattern = positionals.join('\n')
|
|
287
453
|
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
288
454
|
try {
|
|
289
|
-
const matches = caveQuery(store, pattern, {
|
|
455
|
+
const matches = caveQuery(store, pattern, {
|
|
456
|
+
all: values.all === true,
|
|
457
|
+
aliases: values.aliases === true,
|
|
458
|
+
...values['as-of'] === undefined ? {} : { asOf: values['as-of'] }
|
|
459
|
+
})
|
|
290
460
|
if (values.json === true) {
|
|
291
461
|
return ok(`${JSON.stringify(matches, undefined, 2)}\n`)
|
|
292
462
|
}
|
|
@@ -309,6 +479,321 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
309
479
|
}
|
|
310
480
|
}
|
|
311
481
|
|
|
482
|
+
export const deriveCommand = (argv: readonly string[]): Output => {
|
|
483
|
+
const { values, positionals } = parseArgs({
|
|
484
|
+
args: [...argv],
|
|
485
|
+
options: {
|
|
486
|
+
db: { type: 'string' },
|
|
487
|
+
'dry-run': { type: 'boolean' },
|
|
488
|
+
full: { type: 'boolean' },
|
|
489
|
+
aliases: { type: 'boolean' },
|
|
490
|
+
'min-conf': { type: 'string' },
|
|
491
|
+
'max-passes': { type: 'string' },
|
|
492
|
+
list: { type: 'boolean' },
|
|
493
|
+
retract: { type: 'string' },
|
|
494
|
+
json: { type: 'boolean' },
|
|
495
|
+
'no-prelude': { type: 'boolean' }
|
|
496
|
+
},
|
|
497
|
+
allowPositionals: true
|
|
498
|
+
})
|
|
499
|
+
const minConf = values['min-conf'] === undefined ?
|
|
500
|
+
undefined :
|
|
501
|
+
values['min-conf'].endsWith('%') ? Number(values['min-conf'].slice(0, -1)) / 100 : Number(values['min-conf'])
|
|
502
|
+
if (minConf !== undefined && (!Number.isFinite(minConf) || minConf < 0 || minConf > 1)) {
|
|
503
|
+
return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`)
|
|
504
|
+
}
|
|
505
|
+
const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes'])
|
|
506
|
+
if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
|
|
507
|
+
return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
|
|
508
|
+
}
|
|
509
|
+
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
510
|
+
try {
|
|
511
|
+
if (values.list === true) {
|
|
512
|
+
const rules = listRules(store)
|
|
513
|
+
if (values.json === true) {
|
|
514
|
+
return ok(`${JSON.stringify(rules, undefined, 2)}\n`)
|
|
515
|
+
}
|
|
516
|
+
if (rules.length === 0) {
|
|
517
|
+
return ok('no rules\n')
|
|
518
|
+
}
|
|
519
|
+
const lines = rules.map(rule =>
|
|
520
|
+
`${rule.subject} \`${rule.text}\`${rule.label === undefined ? '' : ` ; ${rule.label}`}` +
|
|
521
|
+
(rule.ok ? '' : `\n problems: ${rule.problems.join('; ')}`))
|
|
522
|
+
return ok(`${lines.join('\n')}\n`)
|
|
523
|
+
}
|
|
524
|
+
if (values.retract !== undefined) {
|
|
525
|
+
const outcome = retractRule(store, values.retract)
|
|
526
|
+
if (!outcome.ok) {
|
|
527
|
+
return fail(`cave derive: ${outcome.error}\n`)
|
|
528
|
+
}
|
|
529
|
+
return ok(`retracted ${outcome.subjects.join(', ')} and ${outcome.derived} derived claim(s)\n`)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const err: string[] = []
|
|
533
|
+
const out: string[] = []
|
|
534
|
+
// Unlike parse/add, no positional means "fire the stored rules", never
|
|
535
|
+
// stdin — pass `-` to read a rule file from stdin explicitly.
|
|
536
|
+
const input = positionals.length > 0 ? readInput(positionals) : undefined
|
|
537
|
+
const options = {
|
|
538
|
+
full: values.full === true,
|
|
539
|
+
aliases: values.aliases === true,
|
|
540
|
+
...minConf === undefined ? {} : { minConf },
|
|
541
|
+
...maxPasses === undefined ? {} : { maxPasses }
|
|
542
|
+
}
|
|
543
|
+
let declaration: undefined | Declaration
|
|
544
|
+
let report: DeriveReport
|
|
545
|
+
if (values['dry-run'] === true) {
|
|
546
|
+
// A dry run persists nothing — rule declarations from the file
|
|
547
|
+
// included, so declare + fire share one rolled-back transaction.
|
|
548
|
+
const rolledBack = Symbol('cave derive --dry-run')
|
|
549
|
+
let outcome: undefined | { declaration?: Declaration, report: DeriveReport }
|
|
550
|
+
try {
|
|
551
|
+
store.transaction(() => {
|
|
552
|
+
outcome = {
|
|
553
|
+
...input === undefined ? {} : { declaration: declareRules(store, input) },
|
|
554
|
+
report: derive(store, options)
|
|
555
|
+
}
|
|
556
|
+
throw rolledBack
|
|
557
|
+
})
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error !== rolledBack) {
|
|
560
|
+
throw error
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
declaration = outcome!.declaration
|
|
564
|
+
report = outcome!.report
|
|
565
|
+
} else {
|
|
566
|
+
declaration = input === undefined ? undefined : declareRules(store, input)
|
|
567
|
+
report = derive(store, options)
|
|
568
|
+
}
|
|
569
|
+
if (declaration !== undefined) {
|
|
570
|
+
err.push(...declaration.problems.map(problem => `rules line ${problem.line}: ${problem.message}`))
|
|
571
|
+
out.push(`declared ${declaration.declared} rule(s)` +
|
|
572
|
+
(declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
|
|
573
|
+
(declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''))
|
|
574
|
+
}
|
|
575
|
+
if (values.json === true) {
|
|
576
|
+
return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
|
|
577
|
+
}
|
|
578
|
+
for (const problem of report.problems) {
|
|
579
|
+
err.push(`${problem.subject}: ${problem.problems.join('; ')}`)
|
|
580
|
+
}
|
|
581
|
+
for (const rule of report.rules) {
|
|
582
|
+
const state = rule.fired ?
|
|
583
|
+
`${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
|
|
584
|
+
'unchanged premises, skipped'
|
|
585
|
+
out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`)
|
|
586
|
+
err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`))
|
|
587
|
+
}
|
|
588
|
+
out.push(...report.notes.map(note => `note: ${note}`))
|
|
589
|
+
out.push(
|
|
590
|
+
`derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
|
|
591
|
+
`+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
|
|
592
|
+
`${report.unchanged} unchanged (${report.passes} pass(es))`)
|
|
593
|
+
const hasProblems = report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
|
|
594
|
+
return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` }
|
|
595
|
+
} catch (error) {
|
|
596
|
+
return fail(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
597
|
+
} finally {
|
|
598
|
+
store.close()
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Loads a hooks configuration file (spec §25.4): a JSON object mapping
|
|
604
|
+
* hook names to shell command templates.
|
|
605
|
+
*/
|
|
606
|
+
const readHooks = (path: string): Record<string, string> => {
|
|
607
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
|
608
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
|
|
609
|
+
Object.values(parsed).some(value => typeof value !== 'string')) {
|
|
610
|
+
throw new Error(`${path}: hooks must be a JSON object of name → shell template strings`)
|
|
611
|
+
}
|
|
612
|
+
return parsed as Record<string, string>
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Text rendering of a spec §25.2 execution report. */
|
|
616
|
+
const renderActReport = (report: ActReport): { lines: string[], code: number } => {
|
|
617
|
+
if (!report.ok) {
|
|
618
|
+
const lines = [`cave act: ${report.error}`]
|
|
619
|
+
for (const violation of report.violations ?? []) {
|
|
620
|
+
lines.push(` ${formatViolation(violation)}`)
|
|
621
|
+
}
|
|
622
|
+
return { lines, code: 1 }
|
|
623
|
+
}
|
|
624
|
+
const lines = [
|
|
625
|
+
`executed ${report.subject}${report.dryRun ? ' (dry run)' : ''}: ` +
|
|
626
|
+
`+${report.appended} appended, ${report.updated} updated, ${report.unchanged} unchanged ` +
|
|
627
|
+
`(${report.solutions} solution(s))`,
|
|
628
|
+
...report.effects.map(effect => ` ${effect.outcome}: ${effect.line}`)
|
|
629
|
+
]
|
|
630
|
+
let code = 0
|
|
631
|
+
if (report.hook !== undefined) {
|
|
632
|
+
if (!report.hook.fired) {
|
|
633
|
+
lines.push(`hook ${report.hook.name}: not fired (${report.hook.note})`)
|
|
634
|
+
} else if (report.hook.error === undefined) {
|
|
635
|
+
lines.push(`hook ${report.hook.name}: ok`)
|
|
636
|
+
} else {
|
|
637
|
+
// The claims committed before the hook ran (spec §25.4) — the
|
|
638
|
+
// failure is reported, and the exit code carries it.
|
|
639
|
+
lines.push(`hook ${report.hook.name}: ${report.hook.error}`)
|
|
640
|
+
code = 1
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { lines, code }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export const actCommand = (argv: readonly string[]): Output => {
|
|
647
|
+
const { values, positionals } = parseArgs({
|
|
648
|
+
args: [...argv],
|
|
649
|
+
options: {
|
|
650
|
+
db: { type: 'string' },
|
|
651
|
+
declare: { type: 'boolean' },
|
|
652
|
+
list: { type: 'boolean' },
|
|
653
|
+
retract: { type: 'string' },
|
|
654
|
+
'dry-run': { type: 'boolean' },
|
|
655
|
+
'no-check': { type: 'boolean' },
|
|
656
|
+
aliases: { type: 'boolean' },
|
|
657
|
+
hooks: { type: 'string' },
|
|
658
|
+
json: { type: 'boolean' },
|
|
659
|
+
'no-prelude': { type: 'boolean' }
|
|
660
|
+
},
|
|
661
|
+
allowPositionals: true
|
|
662
|
+
})
|
|
663
|
+
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
664
|
+
try {
|
|
665
|
+
if (values.declare === true) {
|
|
666
|
+
const declaration = declareActions(store, readInput(positionals))
|
|
667
|
+
const err = declaration.problems.map(problem => `line ${problem.line}: ${problem.message}`)
|
|
668
|
+
const out = `declared ${declaration.declared} action(s)` +
|
|
669
|
+
(declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
|
|
670
|
+
(declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : '')
|
|
671
|
+
return {
|
|
672
|
+
code: declaration.problems.length > 0 ? 1 : 0,
|
|
673
|
+
out: `${out}\n`,
|
|
674
|
+
err: err.length === 0 ? '' : `${err.join('\n')}\n`
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (values.list === true) {
|
|
678
|
+
const actions = listActions(store)
|
|
679
|
+
if (values.json === true) {
|
|
680
|
+
return ok(`${JSON.stringify(actions, undefined, 2)}\n`)
|
|
681
|
+
}
|
|
682
|
+
if (actions.length === 0) {
|
|
683
|
+
return ok('no actions\n')
|
|
684
|
+
}
|
|
685
|
+
const lines = actions.flatMap(action => [
|
|
686
|
+
`${action.subject} \`${action.text}\`${action.description === undefined ? '' : ` ; ${action.description}`}`,
|
|
687
|
+
...action.params.map(param => ` ?${param.name}${param.doc === undefined ? '' : ` — ${param.doc}`}`),
|
|
688
|
+
...action.hook === undefined ? [] : [` hook: ${action.hook}`],
|
|
689
|
+
...action.ok ? [] : [` problems: ${action.problems.join('; ')}`]
|
|
690
|
+
])
|
|
691
|
+
return ok(`${lines.join('\n')}\n`)
|
|
692
|
+
}
|
|
693
|
+
if (values.retract !== undefined) {
|
|
694
|
+
const outcome = retractAction(store, values.retract)
|
|
695
|
+
if (!outcome.ok) {
|
|
696
|
+
return fail(`cave act: ${outcome.error}\n`)
|
|
697
|
+
}
|
|
698
|
+
return ok(`retracted ${outcome.subject} — effects of past executions stay recorded (spec §25.1)\n`)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const [name, ...pairs] = positionals
|
|
702
|
+
if (name === undefined) {
|
|
703
|
+
return fail('cave act: an action name is required — or --declare, --list, --retract (spec §25.2)\n')
|
|
704
|
+
}
|
|
705
|
+
const args: Record<string, string> = {}
|
|
706
|
+
for (const pair of pairs) {
|
|
707
|
+
const at = pair.indexOf('=')
|
|
708
|
+
if (at <= 0) {
|
|
709
|
+
return fail(`cave act: expected param=value, got ${JSON.stringify(pair)}\n`)
|
|
710
|
+
}
|
|
711
|
+
args[pair.slice(0, at)] = pair.slice(at + 1)
|
|
712
|
+
}
|
|
713
|
+
const hooksPath = values.hooks ?? process.env['CAVE_HOOKS']
|
|
714
|
+
const report = act(store, name, args, {
|
|
715
|
+
dryRun: values['dry-run'] === true,
|
|
716
|
+
check: values['no-check'] !== true,
|
|
717
|
+
aliases: values.aliases === true,
|
|
718
|
+
...hooksPath === undefined ? {} : { hooks: readHooks(hooksPath) }
|
|
719
|
+
})
|
|
720
|
+
if (values.json === true) {
|
|
721
|
+
return { code: report.ok && report.hook?.error === undefined ? 0 : 1, out: `${JSON.stringify(report, undefined, 2)}\n`, err: '' }
|
|
722
|
+
}
|
|
723
|
+
const { lines, code } = renderActReport(report)
|
|
724
|
+
return report.ok ?
|
|
725
|
+
{ code, out: `${lines.join('\n')}\n`, err: '' } :
|
|
726
|
+
{ code, out: '', err: `${lines.join('\n')}\n` }
|
|
727
|
+
} catch (error) {
|
|
728
|
+
return fail(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
729
|
+
} finally {
|
|
730
|
+
store.close()
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
|
|
735
|
+
const renderReport = (report: Report, staleDays: number): string => {
|
|
736
|
+
const lines: string[] = []
|
|
737
|
+
const { coverage } = report
|
|
738
|
+
lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`)
|
|
739
|
+
if (report.violations.length > 0) {
|
|
740
|
+
lines.push(`violations (${report.violations.length}):`)
|
|
741
|
+
lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`))
|
|
742
|
+
}
|
|
743
|
+
if (report.stale.length > 0) {
|
|
744
|
+
lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`)
|
|
745
|
+
lines.push(...report.stale.map(({ row, ageDays }) => ` ${row.raw_line} (${ageDays}d)`))
|
|
746
|
+
}
|
|
747
|
+
if (report.review.length > 0) {
|
|
748
|
+
lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`)
|
|
749
|
+
lines.push(...report.review.map(row => ` ${row.raw_line}`))
|
|
750
|
+
}
|
|
751
|
+
if (report.disagreements.length > 0) {
|
|
752
|
+
lines.push(`alias disagreements (${report.disagreements.length}):`)
|
|
753
|
+
for (const disagreement of report.disagreements) {
|
|
754
|
+
lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`)
|
|
755
|
+
lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`))
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const confidence = coverage.averageConfidence === null ?
|
|
759
|
+
'' :
|
|
760
|
+
`; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`
|
|
761
|
+
lines.push(
|
|
762
|
+
`coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
|
|
763
|
+
`${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
|
|
764
|
+
`${confidence}; ${coverage.entities} entities, ${coverage.typedEntities} typed`
|
|
765
|
+
)
|
|
766
|
+
return `${lines.join('\n')}\n`
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export const checkCommand = (argv: readonly string[]): Output => {
|
|
770
|
+
const { values } = parseArgs({
|
|
771
|
+
args: [...argv],
|
|
772
|
+
options: {
|
|
773
|
+
db: { type: 'string' },
|
|
774
|
+
stale: { type: 'string' },
|
|
775
|
+
json: { type: 'boolean' },
|
|
776
|
+
'no-prelude': { type: 'boolean' }
|
|
777
|
+
},
|
|
778
|
+
allowPositionals: false
|
|
779
|
+
})
|
|
780
|
+
const staleDays = values.stale === undefined ? defaultStaleDays : Number(values.stale)
|
|
781
|
+
if (!Number.isFinite(staleDays) || staleDays < 0) {
|
|
782
|
+
return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
|
|
783
|
+
}
|
|
784
|
+
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
785
|
+
try {
|
|
786
|
+
const report = caveCheck(store, { staleDays })
|
|
787
|
+
const out = values.json === true ?
|
|
788
|
+
`${JSON.stringify(report, undefined, 2)}\n` :
|
|
789
|
+
renderReport(report, staleDays)
|
|
790
|
+
// Violations fail the check (spec §20.2); the other sections are advisory.
|
|
791
|
+
return { code: report.violations.length > 0 ? 1 : 0, out, err: '' }
|
|
792
|
+
} finally {
|
|
793
|
+
store.close()
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
312
797
|
export const exportCommand = (argv: readonly string[]): Output => {
|
|
313
798
|
const { values } = parseArgs({
|
|
314
799
|
args: [...argv],
|
|
@@ -350,8 +835,8 @@ export const helpCommand = (argv: readonly string[]): Output => {
|
|
|
350
835
|
if (text !== undefined) {
|
|
351
836
|
return ok(`${text}\n`)
|
|
352
837
|
}
|
|
353
|
-
// mcp and
|
|
354
|
-
if (topic === 'mcp' || topic === 'ingest') {
|
|
838
|
+
// mcp, ingest and connect own their help (main.ts forwards `help X` to `X --help`).
|
|
839
|
+
if (topic === 'mcp' || topic === 'ingest' || topic === 'connect') {
|
|
355
840
|
return ok(`see: cave ${topic} --help\n`)
|
|
356
841
|
}
|
|
357
842
|
return fail(`cave help: unknown command ${JSON.stringify(topic)}\n\n${usage}\n`, 2)
|
|
@@ -375,6 +860,12 @@ export const cave = (argv: readonly string[]): Output => {
|
|
|
375
860
|
return importCommand(rest)
|
|
376
861
|
case 'query':
|
|
377
862
|
return queryCommand(rest)
|
|
863
|
+
case 'derive':
|
|
864
|
+
return deriveCommand(rest)
|
|
865
|
+
case 'act':
|
|
866
|
+
return actCommand(rest)
|
|
867
|
+
case 'check':
|
|
868
|
+
return checkCommand(rest)
|
|
378
869
|
case 'export':
|
|
379
870
|
return exportCommand(rest)
|
|
380
871
|
case 'demo':
|