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