@cavelang/cli 0.4.0 → 0.12.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 +23 -9
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +348 -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 +31 -8
- package/dist/src/main.js.map +1 -1
- package/package.json +13 -9
- package/src/cli.ts +360 -25
- package/src/index.ts +2 -2
- package/src/main.ts +30 -9
- package/License.md +0 -6
package/src/cli.ts
CHANGED
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* Commands:
|
|
7
7
|
*
|
|
8
8
|
* - `cave parse [file]` — lint / dump the AST (`--json`)
|
|
9
|
-
* - `cave
|
|
10
|
-
* - `cave
|
|
9
|
+
* - `cave highlight [file]` — ANSI syntax colors (async, routed in `main.ts`)
|
|
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 check [--db <path>]` — knowledge health report (`--stale`, `--json`)
|
|
11
14
|
* - `cave export [--db <path>]` — canonical text out (`--current`)
|
|
15
|
+
* - `cave connect [--db <path>] <source>` — deterministic structured ingestion (async, routed in `main.ts`)
|
|
12
16
|
* - `cave demo` — the cave-loop multi-hop recovery demo
|
|
13
17
|
* - `cave version` — print the cave version
|
|
14
18
|
* - `cave help [command]` — overview, or one command's help
|
|
@@ -24,6 +28,10 @@ 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 type { Report, Violation } from '@cavelang/shape'
|
|
33
|
+
import { declareRules, defaultMaxPasses, defaultMinConf, derive, listRules, retractRule } from '@cavelang/rules'
|
|
34
|
+
import type { Declaration, DeriveReport } from '@cavelang/rules'
|
|
27
35
|
import { Demo } from '@cavelang/loop'
|
|
28
36
|
|
|
29
37
|
export type Output = {
|
|
@@ -42,12 +50,16 @@ export const usage = `cave — Compressed Atomic Verb Expressions
|
|
|
42
50
|
|
|
43
51
|
Usage:
|
|
44
52
|
cave parse [file...] [--json] lint CAVE text (stdin when no file)
|
|
45
|
-
cave
|
|
46
|
-
cave
|
|
47
|
-
cave
|
|
53
|
+
cave highlight [file...] print CAVE text with ANSI syntax colors
|
|
54
|
+
cave add [--db <path>] [file...] ingest into a store [--strict] [--check] [--no-prelude] [--no-src]
|
|
55
|
+
cave import [--db <path>] [file...] restore/merge from CAVE text (add without @src: stamping)
|
|
56
|
+
cave query [--db <path>] <pattern> run a CAVE-Q pattern [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
|
|
57
|
+
cave derive [--db <path>] [rules.cave..] declare + fire rules (spec §24) [--dry-run] [--full] [--list] [--retract <rule>]
|
|
58
|
+
cave check [--db <path>] knowledge health report (spec §20) [--stale <days>] [--json]
|
|
48
59
|
cave export [--db <path>] [--out <file>] emit canonical CAVE text [--current] [--no-prelude]
|
|
49
60
|
cave mcp [--db <path>] serve the engine as an MCP server on stdio [--no-prelude]
|
|
50
61
|
cave ingest [--db <path>] <globs/urls..> LLM-driven ingestion of files and web pages
|
|
62
|
+
cave connect <source> --map <file> deterministic structured ingestion (CSV/JSON/SQLite/URL, spec §23)
|
|
51
63
|
cave demo run the cave-loop reconstruction demo
|
|
52
64
|
cave version print the cave version
|
|
53
65
|
cave help [command] this text, or one command's options and examples
|
|
@@ -76,24 +88,46 @@ Examples:
|
|
|
76
88
|
echo 'auth USES jwt @ 90%' | cave parse
|
|
77
89
|
cave parse notes.cave --json | jq '.lines[0]'`,
|
|
78
90
|
|
|
91
|
+
highlight: `cave highlight — print CAVE text with ANSI syntax colors
|
|
92
|
+
|
|
93
|
+
Usage:
|
|
94
|
+
cave highlight [file...]
|
|
95
|
+
|
|
96
|
+
Reads stdin when no file (or \`-\`) is given. Colors come from the
|
|
97
|
+
tree-sitter grammar's highlight query (@cavelang/tree-sitter-cave), the
|
|
98
|
+
same source editors use. Output always carries ANSI codes — pipe through
|
|
99
|
+
\`less -R\` to page. \`cave export\` colors its output the same way when
|
|
100
|
+
stdout is a terminal (disable with NO_COLOR).
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
cave highlight notes.cave
|
|
104
|
+
echo 'auth USES jwt @ 90%' | cave highlight
|
|
105
|
+
cave highlight notes.cave | less -R`,
|
|
106
|
+
|
|
79
107
|
add: `cave add — ingest CAVE text into a knowledge database
|
|
80
108
|
|
|
81
109
|
Usage:
|
|
82
|
-
cave add [--db <path>] [file...] [--strict] [--no-prelude]
|
|
110
|
+
cave add [--db <path>] [file...] [--strict] [--check] [--no-prelude] [--no-src]
|
|
83
111
|
|
|
84
112
|
Options:
|
|
85
113
|
${dbHelp}
|
|
86
114
|
--strict reject the whole ingest on any problem
|
|
115
|
+
--check shape gate (spec §20.3): roll the append back if it
|
|
116
|
+
introduces new expectation violations
|
|
87
117
|
--no-prelude open the store without the standard verb registry
|
|
118
|
+
--no-src do not stamp actor provenance on appended claims
|
|
88
119
|
|
|
89
|
-
Reads stdin when no file (or \`-\`) is given.
|
|
120
|
+
Reads stdin when no file (or \`-\`) is given. Claims that carry no @src:
|
|
121
|
+
context are stamped @src:cli (spec §9.5); use \`cave import\` to replay
|
|
122
|
+
exported text without stamping.
|
|
90
123
|
|
|
91
124
|
Examples:
|
|
92
125
|
cave add --db k.db notes.cave
|
|
93
126
|
echo 'auth USES jwt @ 90%' | cave add
|
|
94
|
-
cave add --db k.db --strict reviewed.cave
|
|
127
|
+
cave add --db k.db --strict reviewed.cave
|
|
128
|
+
cave add --db k.db --check new-services.cave`,
|
|
95
129
|
|
|
96
|
-
import: `cave import — restore/merge a database from CAVE text (
|
|
130
|
+
import: `cave import — restore/merge a database from CAVE text (add without @src: stamping)
|
|
97
131
|
|
|
98
132
|
Usage:
|
|
99
133
|
cave import [--db <path>] [file...] [--strict] [--no-prelude]
|
|
@@ -105,7 +139,9 @@ Options:
|
|
|
105
139
|
|
|
106
140
|
Canonical CAVE text is the interchange format (spec §2.2): importing a
|
|
107
141
|
file written by \`cave export\` replays its claims append-only, preserving
|
|
108
|
-
the belief series, qualifier edges and in-band declarations.
|
|
142
|
+
the belief series, qualifier edges and in-band declarations. Unlike
|
|
143
|
+
\`cave add\`, import never stamps actor provenance — replayed claims must
|
|
144
|
+
keep the claim keys they were exported with (spec §9.5).
|
|
109
145
|
|
|
110
146
|
Examples:
|
|
111
147
|
cave export --db old.db --out backup.cave
|
|
@@ -114,12 +150,16 @@ Examples:
|
|
|
114
150
|
query: `cave query — run a CAVE-Q pattern against a store
|
|
115
151
|
|
|
116
152
|
Usage:
|
|
117
|
-
cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--no-prelude]
|
|
153
|
+
cave query [--db <path>] <pattern> [WHERE <filter>] [--json] [--all] [--aliases] [--as-of <t>] [--no-prelude]
|
|
118
154
|
|
|
119
155
|
Options:
|
|
120
156
|
${dbHelp}
|
|
121
157
|
--json emit matches as JSON
|
|
122
158
|
--all match all beliefs, not just current ones
|
|
159
|
+
--aliases resolve entities through current ALIAS claims (spec §13.6)
|
|
160
|
+
--as-of <t> resolve beliefs as of a past moment (spec §12.3): a date
|
|
161
|
+
(whole day included), a timestamp (whole second), or a
|
|
162
|
+
transaction id — rows recorded later are invisible
|
|
123
163
|
--no-prelude open the store without the standard verb registry
|
|
124
164
|
|
|
125
165
|
Patterns are claim triples with ?variables and optional metadata filters
|
|
@@ -131,8 +171,71 @@ Examples:
|
|
|
131
171
|
cave query --db k.db '?x HAS bug: ?bug #security'
|
|
132
172
|
cave query --db k.db '?cause CAUSE app/crash' 'WHERE conf >= 0.5'
|
|
133
173
|
cave query --db k.db 'terrier EXTENDS+ animal'
|
|
174
|
+
cave query --db k.db '?x USES postgres' --aliases
|
|
175
|
+
cave query --db k.db 'server IS compromised' --as-of 2026-01-15
|
|
134
176
|
cave query --db k.db '?x ?verb ?y @production' --json`,
|
|
135
177
|
|
|
178
|
+
derive: `cave derive — declare and fire rules (spec §24)
|
|
179
|
+
|
|
180
|
+
Usage:
|
|
181
|
+
cave derive [--db <path>] [rules.cave...] [--dry-run] [--full] [--aliases]
|
|
182
|
+
[--min-conf <p>] [--max-passes <n>] [--json] [--no-prelude]
|
|
183
|
+
cave derive [--db <path>] --list
|
|
184
|
+
cave derive [--db <path>] --retract <digest|subject>
|
|
185
|
+
|
|
186
|
+
Options:
|
|
187
|
+
${dbHelp}
|
|
188
|
+
--dry-run compute and report inside a rolled-back transaction
|
|
189
|
+
--full ignore stored watermarks — re-fire every rule (spec §24.4)
|
|
190
|
+
--aliases premises match through the alias closure (spec §13.6)
|
|
191
|
+
--min-conf <p> do not assert conclusions below this confidence
|
|
192
|
+
(0..1 or N%, default ${defaultMinConf})
|
|
193
|
+
--max-passes <n> fixpoint guard (default ${defaultMaxPasses})
|
|
194
|
+
--list print the store's current rules and exit
|
|
195
|
+
--retract <r> retract a rule (digest, unambiguous prefix, or subject)
|
|
196
|
+
together with everything it derived, and exit
|
|
197
|
+
--json emit the report as JSON
|
|
198
|
+
--no-prelude open the store without the standard verb registry
|
|
199
|
+
|
|
200
|
+
Rule files are CAVE documents whose \`premises => conclusion\` lines are
|
|
201
|
+
rules (spec §24.1); other lines — verb declarations the rules need — are
|
|
202
|
+
prelude, ingested first. Rules are stored in-band as
|
|
203
|
+
\`rule/<digest> HAS rule: \\\`…\\\`\` claims, so \`cave derive\` with no file
|
|
204
|
+
fires whatever the store already knows. Derived claims are stamped
|
|
205
|
+
@src:rule/<digest>, carry BECAUSE edges to their premise rows and a VIA
|
|
206
|
+
edge to the rule, and update append-only: re-runs are idempotent and
|
|
207
|
+
watermark-incremental, and conclusions whose premises no longer hold are
|
|
208
|
+
retracted (spec §24.2–§24.5).
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
cave derive --db k.db rules.cave
|
|
212
|
+
cave derive --db k.db
|
|
213
|
+
echo '?x NEEDS ?y, ?y NEEDS ?z => ?x NEEDS ?z' | cave derive --db k.db -
|
|
214
|
+
cave derive --db k.db --dry-run --json
|
|
215
|
+
cave derive --db k.db --list
|
|
216
|
+
cave derive --db k.db --retract 4a0bb974f43c`,
|
|
217
|
+
|
|
218
|
+
check: `cave check — knowledge health report (spec §20)
|
|
219
|
+
|
|
220
|
+
Usage:
|
|
221
|
+
cave check [--db <path>] [--stale <days>] [--json] [--no-prelude]
|
|
222
|
+
|
|
223
|
+
Options:
|
|
224
|
+
${dbHelp}
|
|
225
|
+
--stale <days> staleness horizon (default: ${defaultStaleDays})
|
|
226
|
+
--json emit the full report as JSON
|
|
227
|
+
--no-prelude open the store without the standard verb registry
|
|
228
|
+
|
|
229
|
+
Reads the store against its own in-band EXPECTS declarations (spec §20.1)
|
|
230
|
+
and reports shape violations, stale current beliefs, review candidates
|
|
231
|
+
(conf 0.3–0.7), alias disagreements (spec §13.6) and coverage stats.
|
|
232
|
+
Exits 1 when violations exist; everything else is advisory.
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
cave check --db k.db
|
|
236
|
+
cave check --db k.db --stale 30
|
|
237
|
+
cave check --db k.db --json | jq '.violations'`,
|
|
238
|
+
|
|
136
239
|
export: `cave export — emit canonical CAVE text from a store
|
|
137
240
|
|
|
138
241
|
Usage:
|
|
@@ -195,26 +298,63 @@ export const parseCommand = (argv: readonly string[]): Output => {
|
|
|
195
298
|
return { code: 1, out: `${summary}\n`, err: `${problems}\n` }
|
|
196
299
|
}
|
|
197
300
|
|
|
198
|
-
|
|
301
|
+
/**
|
|
302
|
+
* `cave highlight` — ANSI syntax colors from the tree-sitter grammar's own
|
|
303
|
+
* highlight query. Async (the grammar WASM loads on first use), so `main.ts`
|
|
304
|
+
* routes it separately, like `mcp` and `ingest`.
|
|
305
|
+
*/
|
|
306
|
+
export const highlightCommand = async (argv: readonly string[]): Promise<Output> => {
|
|
307
|
+
const { positionals } = parseArgs({ args: [...argv], options: {}, allowPositionals: true })
|
|
308
|
+
const input = readInput(positionals)
|
|
309
|
+
const { highlighter } = await import('@cavelang/highlight')
|
|
310
|
+
const { ansi } = await highlighter()
|
|
311
|
+
return ok(ansi(input))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Shared by `add` and `import`; the difference is actor provenance
|
|
316
|
+
* (spec §9.5). `add` records knowledge authored now, so claims without a
|
|
317
|
+
* `@src:` context are stamped `@src:cli` (`--no-src` opts out); `import`
|
|
318
|
+
* replays interchange text, which must preserve claim keys as exported,
|
|
319
|
+
* so it never stamps.
|
|
320
|
+
*/
|
|
321
|
+
const formatViolation = (violation: Violation): string =>
|
|
322
|
+
`${violation.entity} missing ${violation.expectation.kind} ${violation.expectation.name} ` +
|
|
323
|
+
`(${violation.entity} IS ${violation.via}; ${violation.expectation.type} EXPECTS ${violation.expectation.name})`
|
|
324
|
+
|
|
325
|
+
const ingestCommand = (name: 'add' | 'import') => (argv: readonly string[]): Output => {
|
|
199
326
|
const { values, positionals } = parseArgs({
|
|
200
327
|
args: [...argv],
|
|
201
328
|
options: {
|
|
202
329
|
db: { type: 'string' },
|
|
203
330
|
strict: { type: 'boolean' },
|
|
204
|
-
'no-prelude': { type: 'boolean' }
|
|
331
|
+
'no-prelude': { type: 'boolean' },
|
|
332
|
+
...name === 'add' ? { 'no-src': { type: 'boolean' }, check: { type: 'boolean' } } : {}
|
|
205
333
|
},
|
|
206
334
|
allowPositionals: true
|
|
207
335
|
})
|
|
208
336
|
const input = readInput(positionals)
|
|
209
337
|
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
210
338
|
try {
|
|
211
|
-
const
|
|
212
|
-
|
|
339
|
+
const options = {
|
|
340
|
+
strict: values.strict === true,
|
|
341
|
+
...name === 'add' && values['no-src'] !== true ? { source: 'cli' } : {}
|
|
342
|
+
}
|
|
343
|
+
// The shape gate (spec §20.3): append + check in one transaction,
|
|
344
|
+
// rolled back when the append introduces new expectation violations.
|
|
345
|
+
const outcome = name === 'add' && values.check === true ?
|
|
346
|
+
gatedIngest(store, input, options) :
|
|
347
|
+
{ ok: true as const, result: store.ingest(input, options) }
|
|
348
|
+
if (!outcome.ok) {
|
|
349
|
+
const detail = outcome.violations.map(violation => ` ${formatViolation(violation)}`).join('\n')
|
|
350
|
+
return fail(`rejected: ${outcome.violations.length} new violation(s), nothing added (spec §20.3)\n${detail}\n`)
|
|
351
|
+
}
|
|
352
|
+
const problems = outcome.result.problems
|
|
213
353
|
.map(problem => `line ${problem.line}: ${problem.message}`)
|
|
214
354
|
.join('\n')
|
|
215
355
|
return {
|
|
216
356
|
code: 0,
|
|
217
|
-
out: `added ${result.ids.length} claim(s), ${result.edges} edge(s)\n`,
|
|
357
|
+
out: `added ${outcome.result.ids.length} claim(s), ${outcome.result.edges} edge(s)\n`,
|
|
218
358
|
err: problems === '' ? '' : `${problems}\n`
|
|
219
359
|
}
|
|
220
360
|
} catch (error) {
|
|
@@ -228,13 +368,15 @@ export const addCommand = ingestCommand('add')
|
|
|
228
368
|
|
|
229
369
|
/**
|
|
230
370
|
* `cave import` — restore/merge a knowledge database from CAVE text files.
|
|
231
|
-
*
|
|
371
|
+
* `add` minus provenance stamping: canonical CAVE text *is* the interchange
|
|
232
372
|
* format (spec §2.2), so importing a file exported with `cave export`
|
|
233
|
-
* replays its claims append-only
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
373
|
+
* replays its claims append-only — and because contexts are part of claim
|
|
374
|
+
* identity, replay must not add a `@src:` stamp (spec §9.5). What the text
|
|
375
|
+
* round trip preserves: every claim with metadata, full belief-series order
|
|
376
|
+
* (rows export in tx order and re-ingest with fresh monotonic tx ids),
|
|
377
|
+
* qualifier/grouping edges, and in-band registry declarations. Original tx
|
|
378
|
+
* timestamps are re-minted — the text format carries no transaction
|
|
379
|
+
* identity.
|
|
238
380
|
*/
|
|
239
381
|
export const importCommand = ingestCommand('import')
|
|
240
382
|
|
|
@@ -245,6 +387,8 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
245
387
|
db: { type: 'string' },
|
|
246
388
|
json: { type: 'boolean' },
|
|
247
389
|
all: { type: 'boolean' },
|
|
390
|
+
aliases: { type: 'boolean' },
|
|
391
|
+
'as-of': { type: 'string' },
|
|
248
392
|
'no-prelude': { type: 'boolean' }
|
|
249
393
|
},
|
|
250
394
|
allowPositionals: true
|
|
@@ -255,7 +399,11 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
255
399
|
const pattern = positionals.join('\n')
|
|
256
400
|
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
257
401
|
try {
|
|
258
|
-
const matches = caveQuery(store, pattern, {
|
|
402
|
+
const matches = caveQuery(store, pattern, {
|
|
403
|
+
all: values.all === true,
|
|
404
|
+
aliases: values.aliases === true,
|
|
405
|
+
...values['as-of'] === undefined ? {} : { asOf: values['as-of'] }
|
|
406
|
+
})
|
|
259
407
|
if (values.json === true) {
|
|
260
408
|
return ok(`${JSON.stringify(matches, undefined, 2)}\n`)
|
|
261
409
|
}
|
|
@@ -278,6 +426,189 @@ export const queryCommand = (argv: readonly string[]): Output => {
|
|
|
278
426
|
}
|
|
279
427
|
}
|
|
280
428
|
|
|
429
|
+
export const deriveCommand = (argv: readonly string[]): Output => {
|
|
430
|
+
const { values, positionals } = parseArgs({
|
|
431
|
+
args: [...argv],
|
|
432
|
+
options: {
|
|
433
|
+
db: { type: 'string' },
|
|
434
|
+
'dry-run': { type: 'boolean' },
|
|
435
|
+
full: { type: 'boolean' },
|
|
436
|
+
aliases: { type: 'boolean' },
|
|
437
|
+
'min-conf': { type: 'string' },
|
|
438
|
+
'max-passes': { type: 'string' },
|
|
439
|
+
list: { type: 'boolean' },
|
|
440
|
+
retract: { type: 'string' },
|
|
441
|
+
json: { type: 'boolean' },
|
|
442
|
+
'no-prelude': { type: 'boolean' }
|
|
443
|
+
},
|
|
444
|
+
allowPositionals: true
|
|
445
|
+
})
|
|
446
|
+
const minConf = values['min-conf'] === undefined ?
|
|
447
|
+
undefined :
|
|
448
|
+
values['min-conf'].endsWith('%') ? Number(values['min-conf'].slice(0, -1)) / 100 : Number(values['min-conf'])
|
|
449
|
+
if (minConf !== undefined && (!Number.isFinite(minConf) || minConf < 0 || minConf > 1)) {
|
|
450
|
+
return fail(`cave derive: --min-conf expects a confidence in 0..1 or N%, got ${JSON.stringify(values['min-conf'])}\n`)
|
|
451
|
+
}
|
|
452
|
+
const maxPasses = values['max-passes'] === undefined ? undefined : Number(values['max-passes'])
|
|
453
|
+
if (maxPasses !== undefined && (!Number.isInteger(maxPasses) || maxPasses < 1)) {
|
|
454
|
+
return fail(`cave derive: --max-passes expects a positive integer, got ${JSON.stringify(values['max-passes'])}\n`)
|
|
455
|
+
}
|
|
456
|
+
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
457
|
+
try {
|
|
458
|
+
if (values.list === true) {
|
|
459
|
+
const rules = listRules(store)
|
|
460
|
+
if (values.json === true) {
|
|
461
|
+
return ok(`${JSON.stringify(rules, undefined, 2)}\n`)
|
|
462
|
+
}
|
|
463
|
+
if (rules.length === 0) {
|
|
464
|
+
return ok('no rules\n')
|
|
465
|
+
}
|
|
466
|
+
const lines = rules.map(rule =>
|
|
467
|
+
`${rule.subject} \`${rule.text}\`${rule.label === undefined ? '' : ` ; ${rule.label}`}` +
|
|
468
|
+
(rule.ok ? '' : `\n problems: ${rule.problems.join('; ')}`))
|
|
469
|
+
return ok(`${lines.join('\n')}\n`)
|
|
470
|
+
}
|
|
471
|
+
if (values.retract !== undefined) {
|
|
472
|
+
const outcome = retractRule(store, values.retract)
|
|
473
|
+
if (!outcome.ok) {
|
|
474
|
+
return fail(`cave derive: ${outcome.error}\n`)
|
|
475
|
+
}
|
|
476
|
+
return ok(`retracted ${outcome.subjects.join(', ')} and ${outcome.derived} derived claim(s)\n`)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const err: string[] = []
|
|
480
|
+
const out: string[] = []
|
|
481
|
+
// Unlike parse/add, no positional means "fire the stored rules", never
|
|
482
|
+
// stdin — pass `-` to read a rule file from stdin explicitly.
|
|
483
|
+
const input = positionals.length > 0 ? readInput(positionals) : undefined
|
|
484
|
+
const options = {
|
|
485
|
+
full: values.full === true,
|
|
486
|
+
aliases: values.aliases === true,
|
|
487
|
+
...minConf === undefined ? {} : { minConf },
|
|
488
|
+
...maxPasses === undefined ? {} : { maxPasses }
|
|
489
|
+
}
|
|
490
|
+
let declaration: undefined | Declaration
|
|
491
|
+
let report: DeriveReport
|
|
492
|
+
if (values['dry-run'] === true) {
|
|
493
|
+
// A dry run persists nothing — rule declarations from the file
|
|
494
|
+
// included, so declare + fire share one rolled-back transaction.
|
|
495
|
+
const rolledBack = Symbol('cave derive --dry-run')
|
|
496
|
+
let outcome: undefined | { declaration?: Declaration, report: DeriveReport }
|
|
497
|
+
try {
|
|
498
|
+
store.transaction(() => {
|
|
499
|
+
outcome = {
|
|
500
|
+
...input === undefined ? {} : { declaration: declareRules(store, input) },
|
|
501
|
+
report: derive(store, options)
|
|
502
|
+
}
|
|
503
|
+
throw rolledBack
|
|
504
|
+
})
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (error !== rolledBack) {
|
|
507
|
+
throw error
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
declaration = outcome!.declaration
|
|
511
|
+
report = outcome!.report
|
|
512
|
+
} else {
|
|
513
|
+
declaration = input === undefined ? undefined : declareRules(store, input)
|
|
514
|
+
report = derive(store, options)
|
|
515
|
+
}
|
|
516
|
+
if (declaration !== undefined) {
|
|
517
|
+
err.push(...declaration.problems.map(problem => `rules line ${problem.line}: ${problem.message}`))
|
|
518
|
+
out.push(`declared ${declaration.declared} rule(s)` +
|
|
519
|
+
(declaration.unchanged > 0 ? `, ${declaration.unchanged} unchanged` : '') +
|
|
520
|
+
(declaration.prelude > 0 ? `, +${declaration.prelude} prelude claim(s)` : ''))
|
|
521
|
+
}
|
|
522
|
+
if (values.json === true) {
|
|
523
|
+
return { code: report.problems.length > 0 ? 1 : 0, out: `${JSON.stringify(report, undefined, 2)}\n`, err: err.join('\n') + (err.length > 0 ? '\n' : '') }
|
|
524
|
+
}
|
|
525
|
+
for (const problem of report.problems) {
|
|
526
|
+
err.push(`${problem.subject}: ${problem.problems.join('; ')}`)
|
|
527
|
+
}
|
|
528
|
+
for (const rule of report.rules) {
|
|
529
|
+
const state = rule.fired ?
|
|
530
|
+
`${rule.solutions} solution(s), +${rule.appended} appended, ${rule.updated} updated, ${rule.retracted} retracted, ${rule.unchanged} unchanged` :
|
|
531
|
+
'unchanged premises, skipped'
|
|
532
|
+
out.push(`${rule.subject}: ${state}${rule.label === undefined ? '' : ` ; ${rule.label}`}`)
|
|
533
|
+
err.push(...rule.problems.map(problem => `${rule.subject}: ${problem}`))
|
|
534
|
+
}
|
|
535
|
+
out.push(...report.notes.map(note => `note: ${note}`))
|
|
536
|
+
out.push(
|
|
537
|
+
`derived${values['dry-run'] === true ? ' (dry run)' : ''}: ` +
|
|
538
|
+
`+${report.appended} appended, ${report.updated} updated, ${report.retracted} retracted, ` +
|
|
539
|
+
`${report.unchanged} unchanged (${report.passes} pass(es))`)
|
|
540
|
+
const hasProblems = report.problems.length > 0 || report.rules.some(rule => rule.problems.length > 0)
|
|
541
|
+
return { code: hasProblems ? 1 : 0, out: `${out.join('\n')}\n`, err: err.length === 0 ? '' : `${err.join('\n')}\n` }
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return fail(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
544
|
+
} finally {
|
|
545
|
+
store.close()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Text rendering of the §20.2 report: always shape + coverage, non-empty advisory sections. */
|
|
550
|
+
const renderReport = (report: Report, staleDays: number): string => {
|
|
551
|
+
const lines: string[] = []
|
|
552
|
+
const { coverage } = report
|
|
553
|
+
lines.push(`shape: ${coverage.expectations} expectation(s), ${coverage.instances} instance(s), ${coverage.satisfied}/${coverage.checks} satisfied`)
|
|
554
|
+
if (report.violations.length > 0) {
|
|
555
|
+
lines.push(`violations (${report.violations.length}):`)
|
|
556
|
+
lines.push(...report.violations.map(violation => ` ${formatViolation(violation)}`))
|
|
557
|
+
}
|
|
558
|
+
if (report.stale.length > 0) {
|
|
559
|
+
lines.push(`stale (${report.stale.length}, older than ${staleDays} day(s)):`)
|
|
560
|
+
lines.push(...report.stale.map(({ row, ageDays }) => ` ${row.raw_line} (${ageDays}d)`))
|
|
561
|
+
}
|
|
562
|
+
if (report.review.length > 0) {
|
|
563
|
+
lines.push(`review candidates (${report.review.length}, conf 0.3-0.7):`)
|
|
564
|
+
lines.push(...report.review.map(row => ` ${row.raw_line}`))
|
|
565
|
+
}
|
|
566
|
+
if (report.disagreements.length > 0) {
|
|
567
|
+
lines.push(`alias disagreements (${report.disagreements.length}):`)
|
|
568
|
+
for (const disagreement of report.disagreements) {
|
|
569
|
+
lines.push(` ${disagreement.about} across ${disagreement.entities.join(', ')}:`)
|
|
570
|
+
lines.push(...disagreement.rows.map(row => ` ${row.raw_line}`))
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const confidence = coverage.averageConfidence === null ?
|
|
574
|
+
'' :
|
|
575
|
+
`; avg conf ${Math.round(coverage.averageConfidence * 100)}%, ${coverage.lowConfidence} low (< 0.3)`
|
|
576
|
+
lines.push(
|
|
577
|
+
`coverage: ${coverage.rows} row(s), ${coverage.facts} fact(s) — ` +
|
|
578
|
+
`${coverage.current} current, ${coverage.retracted} retracted, ${coverage.negated} negated` +
|
|
579
|
+
`${confidence}; ${coverage.entities} entities, ${coverage.typedEntities} typed`
|
|
580
|
+
)
|
|
581
|
+
return `${lines.join('\n')}\n`
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export const checkCommand = (argv: readonly string[]): Output => {
|
|
585
|
+
const { values } = parseArgs({
|
|
586
|
+
args: [...argv],
|
|
587
|
+
options: {
|
|
588
|
+
db: { type: 'string' },
|
|
589
|
+
stale: { type: 'string' },
|
|
590
|
+
json: { type: 'boolean' },
|
|
591
|
+
'no-prelude': { type: 'boolean' }
|
|
592
|
+
},
|
|
593
|
+
allowPositionals: false
|
|
594
|
+
})
|
|
595
|
+
const staleDays = values.stale === undefined ? defaultStaleDays : Number(values.stale)
|
|
596
|
+
if (!Number.isFinite(staleDays) || staleDays < 0) {
|
|
597
|
+
return fail(`cave check: --stale expects a non-negative number of days, got ${JSON.stringify(values.stale)}\n`)
|
|
598
|
+
}
|
|
599
|
+
const store = open(values.db ?? defaultDbPath(), values['no-prelude'] === true ? { registry: Registry.empty } : {})
|
|
600
|
+
try {
|
|
601
|
+
const report = caveCheck(store, { staleDays })
|
|
602
|
+
const out = values.json === true ?
|
|
603
|
+
`${JSON.stringify(report, undefined, 2)}\n` :
|
|
604
|
+
renderReport(report, staleDays)
|
|
605
|
+
// Violations fail the check (spec §20.2); the other sections are advisory.
|
|
606
|
+
return { code: report.violations.length > 0 ? 1 : 0, out, err: '' }
|
|
607
|
+
} finally {
|
|
608
|
+
store.close()
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
281
612
|
export const exportCommand = (argv: readonly string[]): Output => {
|
|
282
613
|
const { values } = parseArgs({
|
|
283
614
|
args: [...argv],
|
|
@@ -319,8 +650,8 @@ export const helpCommand = (argv: readonly string[]): Output => {
|
|
|
319
650
|
if (text !== undefined) {
|
|
320
651
|
return ok(`${text}\n`)
|
|
321
652
|
}
|
|
322
|
-
// mcp and
|
|
323
|
-
if (topic === 'mcp' || topic === 'ingest') {
|
|
653
|
+
// mcp, ingest and connect own their help (main.ts forwards `help X` to `X --help`).
|
|
654
|
+
if (topic === 'mcp' || topic === 'ingest' || topic === 'connect') {
|
|
324
655
|
return ok(`see: cave ${topic} --help\n`)
|
|
325
656
|
}
|
|
326
657
|
return fail(`cave help: unknown command ${JSON.stringify(topic)}\n\n${usage}\n`, 2)
|
|
@@ -344,6 +675,10 @@ export const cave = (argv: readonly string[]): Output => {
|
|
|
344
675
|
return importCommand(rest)
|
|
345
676
|
case 'query':
|
|
346
677
|
return queryCommand(rest)
|
|
678
|
+
case 'derive':
|
|
679
|
+
return deriveCommand(rest)
|
|
680
|
+
case 'check':
|
|
681
|
+
return checkCommand(rest)
|
|
347
682
|
case 'export':
|
|
348
683
|
return exportCommand(rest)
|
|
349
684
|
case 'demo':
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@cavelang/cli` — the `cave` command (parse / add / query / export / demo).
|
|
2
|
+
* `@cavelang/cli` — the `cave` command (parse / add / query / derive / check / export / demo).
|
|
3
3
|
* Programmatic access to the same implementations lives here.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export { addCommand, cave, commandHelp, demoCommand, exportCommand, helpCommand, importCommand, parseCommand, queryCommand, usage, versionCommand } from './cli.ts'
|
|
6
|
+
export { addCommand, cave, checkCommand, commandHelp, demoCommand, deriveCommand, exportCommand, helpCommand, highlightCommand, importCommand, parseCommand, queryCommand, usage, versionCommand } from './cli.ts'
|
|
7
7
|
export type { Output } from './cli.ts'
|
package/src/main.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/** `cave` binary entry point — see `cli.ts` for the command implementations. */
|
|
3
3
|
|
|
4
|
-
import { cave } from './cli.ts'
|
|
4
|
+
import { cave, highlightCommand } from './cli.ts'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const write = (code: number, out: string, err: string): void => {
|
|
7
|
+
if (out !== '') {
|
|
8
|
+
process.stdout.write(out)
|
|
9
|
+
}
|
|
10
|
+
if (err !== '') {
|
|
11
|
+
process.stderr.write(err)
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = code
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// mcp, ingest and connect own their help text, so `cave help X` becomes `cave X --help`.
|
|
7
17
|
const raw = process.argv.slice(2)
|
|
8
|
-
const argv = raw[0] === 'help' && (raw[1] === 'mcp' || raw[1] === 'ingest') ?
|
|
18
|
+
const argv = raw[0] === 'help' && (raw[1] === 'mcp' || raw[1] === 'ingest' || raw[1] === 'connect') ?
|
|
9
19
|
[raw[1], '--help'] :
|
|
10
20
|
raw
|
|
11
21
|
if (argv[0] === 'mcp') {
|
|
@@ -16,13 +26,24 @@ if (argv[0] === 'mcp') {
|
|
|
16
26
|
// Long-running: drives an LLM agent over batches of files.
|
|
17
27
|
const { runIngest } = await import('@cavelang/ingest')
|
|
18
28
|
process.exitCode = await runIngest(argv.slice(1))
|
|
29
|
+
} else if (argv[0] === 'connect') {
|
|
30
|
+
// Async (URL sources fetch) and potentially long-running (--watch).
|
|
31
|
+
const { runConnect } = await import('@cavelang/connect')
|
|
32
|
+
process.exitCode = await runConnect(argv.slice(1))
|
|
33
|
+
} else if (argv[0] === 'highlight' && !argv.includes('--help') && !argv.includes('-h')) {
|
|
34
|
+
// Async (grammar WASM loads on first use); --help stays on the sync path.
|
|
35
|
+
const { code, out, err } = await highlightCommand(argv.slice(1))
|
|
36
|
+
write(code, out, err)
|
|
19
37
|
} else {
|
|
20
38
|
const { code, out, err } = cave(argv)
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
// Canonical CAVE text going to a terminal gets syntax colors from the same
|
|
40
|
+
// grammar query `cave highlight` uses; piping or NO_COLOR keeps bytes clean.
|
|
41
|
+
const colorize = code === 0 && argv[0] === 'export' && !argv.includes('--out') &&
|
|
42
|
+
out !== '' && process.stdout.isTTY === true && process.env['NO_COLOR'] === undefined
|
|
43
|
+
if (colorize) {
|
|
44
|
+
const { highlighter } = await import('@cavelang/highlight')
|
|
45
|
+
write(code, (await highlighter()).ansi(out), err)
|
|
46
|
+
} else {
|
|
47
|
+
write(code, out, err)
|
|
23
48
|
}
|
|
24
|
-
if (err !== '') {
|
|
25
|
-
process.stderr.write(err)
|
|
26
|
-
}
|
|
27
|
-
process.exitCode = code
|
|
28
49
|
}
|
package/License.md
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
# CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
2
|
-
|
|
3
|
-
To the extent possible under law, the authors listed in [Authors.md](./Authors.md) have waived all copyright and related or neighboring rights to this software and associated documentation files (the "Software").
|
|
4
|
-
|
|
5
|
-
For more information, please see:
|
|
6
|
-
https://creativecommons.org/publicdomain/zero/1.0/
|