@knolo/cli 5.5.1 → 6.1.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/bin/kar.mjs ADDED
@@ -0,0 +1,528 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { KAR_CLI_MAX_IMAGE_BYTES, readBounded, readBoundedJson, readBoundedText } from './kar-limits.mjs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ const USAGE = `Usage: knolo kar <evaluate|verify|explain|inspect|graph|package|produce|domain|dossier|coverage|benchmark|demo|conformance> [options]
10
+
11
+ Knolo Adversarial Retrieval over a Committed Evidence Graph. Semantics are kar-1-research-1.
12
+ This command does not change V4 query, BM25, or MMR.
13
+ CEG authoring compiles source into the frozen sidecar. It does not change retrieval.
14
+ Producers propose CEG Source. They do not write committed graph bytes.
15
+ Domain Pack relation rules are review-required until a human gold gate says otherwise.
16
+
17
+ knolo kar evaluate --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..."
18
+ knolo kar verify --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --result result.json
19
+ knolo kar explain --result result.json [--image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..."]
20
+ knolo kar inspect --image knowledge.knolo --graph knowledge.kar.json [--plan plan.json] [--query "..."]
21
+ knolo kar graph build --image knowledge.knolo --source knowledge.ceg.yaml --out knowledge.kar.json
22
+ knolo kar graph lint|review|inspect|diff|validate|check|rebuild
23
+ knolo kar graph import claim-graph|json
24
+ knolo kar package --image knowledge.knolo --graph knowledge.kar.json --out dist/my-knowledge-kar/
25
+ knolo kar package verify dist/my-knowledge-kar/
26
+ knolo kar produce rules --image knowledge.knolo --domain domains/contracts --out proposals.json
27
+ knolo kar produce review proposals.json
28
+ knolo kar produce apply --proposals proposals.json --out generated.ceg.yaml
29
+ knolo kar domain validate domains/contracts
30
+ knolo kar domain inspect domains/contracts
31
+ knolo kar domain test domains/contracts
32
+ knolo kar dossier build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out dossier/
33
+ knolo kar dossier verify dossier/
34
+ knolo kar coverage build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out coverage/
35
+ knolo kar coverage build --image knowledge.knolo --graph knowledge.kar.json --domain domains/contracts-v4 --query "..." --out coverage/
36
+ knolo kar coverage verify coverage/
37
+ knolo kar benchmark annotate --packet packet.json --decisions decisions.json --out labels.json --role human --reviewer-id <id>
38
+ knolo kar benchmark adjudicate --packet packet.json --a pass-a.json --b pass-b.json --decisions decisions.json --out gold.json --role human-adjudicator --reviewer-id <id>
39
+ knolo kar benchmark agreement --a pass-a.json --b pass-b.json
40
+ knolo kar demo
41
+ knolo kar conformance
42
+
43
+ --json Print the machine-readable evaluation, build, or inspection
44
+ --auto Write accepted deterministic producer output as CEG Source
45
+
46
+ Exit status is 0 for a completed command, including a KAR abstention.
47
+ coverage build exits 0 when the report is written, including gap and blocked rows.
48
+ verify, dossier verify, coverage verify, and conformance return non-zero when a check fails.
49
+ `;
50
+
51
+ export async function runKarCli(argv) {
52
+ const { command, positionals, flags } = parseArgs(argv);
53
+ if (!command || command === 'help' || flags.help) {
54
+ console.log(USAGE.trim());
55
+ return;
56
+ }
57
+ if (command === 'graph' || command === 'package') {
58
+ const { runKarAuthoring } = await import('./kar-graph.mjs');
59
+ return runKarAuthoring(command, positionals.slice(1), flags);
60
+ }
61
+ if (command === 'produce' || command === 'domain') {
62
+ const { runKarProduce } = await import('./kar-produce.mjs');
63
+ return runKarProduce(command, positionals.slice(1), flags);
64
+ }
65
+ if (command === 'benchmark') {
66
+ const { runKarBenchmark } = await import('./kar-benchmark.mjs');
67
+ return runKarBenchmark(positionals.slice(1), flags);
68
+ }
69
+ if (command === 'demo') return runRepoScript(path.join('examples', 'kar-v6', 'run.mjs'));
70
+ if (command === 'conformance') return runRepoScript(path.join('conformance', 'kar', 'v1', 'run.mjs'));
71
+ const kar = await loadKar();
72
+ if (command === 'dossier') return dossierCommand(kar, positionals.slice(1), flags);
73
+ if (command === 'coverage') return coverageCommand(kar, positionals.slice(1), flags);
74
+ if (command === 'evaluate') return evaluateCommand(kar, flags);
75
+ if (command === 'verify') return verifyCommand(kar, flags);
76
+ if (command === 'explain') return explainCommand(kar, flags);
77
+ if (command === 'inspect') return inspectCommand(kar, flags);
78
+ throw new Error(`Unknown kar command "${command}".\n${USAGE}`);
79
+ }
80
+
81
+ async function evaluateCommand(kar, flags) {
82
+ const { session, plan, query } = openSession(kar, flags, true);
83
+ const result = kar.evaluateKar(session, { proposition: query, plan });
84
+ if (flags.json) {
85
+ console.log(JSON.stringify(result, null, 2));
86
+ return;
87
+ }
88
+ console.log(kar.renderKarEvaluation(result));
89
+ }
90
+
91
+ async function verifyCommand(kar, flags) {
92
+ const image = readBytes(required(flags, 'image'));
93
+ const graph = readJson(required(flags, 'graph'));
94
+ const plan = readJson(required(flags, 'plan'));
95
+ const query = required(flags, 'query');
96
+ const result = readJson(required(flags, 'result'));
97
+ const verdict = kar.verifyKar({ image, graph, proposition: query, plan, result });
98
+ if (!verdict.ok) {
99
+ console.log(verdict.code);
100
+ console.log(verdict.reason);
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ console.log('VERIFIED');
105
+ }
106
+
107
+ async function explainCommand(kar, flags) {
108
+ const result = readJson(required(flags, 'result'));
109
+ const wantsResolution = flags.image || flags.graph || flags.plan || flags.query;
110
+ if (wantsResolution && !(flags.image && flags.graph && flags.plan && flags.query)) {
111
+ throw new Error('Resolving evidence text requires --image, --graph, --plan, and --query together. Certificate-only explanation uses --result alone.');
112
+ }
113
+ const explanation = wantsResolution
114
+ ? kar.explainKarResult(openSession(kar, flags, true).session, result)
115
+ : kar.explainKarCertificate(result);
116
+ if (flags.json) {
117
+ console.log(JSON.stringify(explanation, null, 2));
118
+ return;
119
+ }
120
+ console.log(kar.renderKarExplanation(explanation));
121
+ }
122
+
123
+ async function inspectCommand(kar, flags) {
124
+ const { session, plan } = openSession(kar, flags, false);
125
+ const graph = session.graph;
126
+ const symbols = [...new Set(graph.relations.map((relation) => relation.relation))].sort();
127
+ const complexity = plan ? kar.inspectKarComplexity(session, plan, flags.query ?? '') : null;
128
+ const report = {
129
+ experimental: true,
130
+ semantics: 'kar-1-research-1',
131
+ stateRoot: session.image.stateRoot,
132
+ knowledgeRoot: session.image.knowledgeRoot,
133
+ objectRoot: session.image.objectRoot,
134
+ commitDigest: session.image.commitDigest,
135
+ semanticRoot: session.roots.semanticRoot,
136
+ nodes: graph.nodes.length,
137
+ relations: graph.relations.length,
138
+ evidenceBindings: graph.bindings.length,
139
+ relationTypes: symbols,
140
+ cover: complexity,
141
+ bounds: plan ? plan.bounds : null,
142
+ };
143
+ if (flags.json) {
144
+ console.log(JSON.stringify(report, null, 2));
145
+ return;
146
+ }
147
+ const lines = [
148
+ 'KAR INSPECTION',
149
+ `V5 state root ${report.stateRoot ?? '(none)'}`,
150
+ `KAR knowledge root ${report.knowledgeRoot}`,
151
+ `CEG semantic root ${report.semanticRoot}`,
152
+ `object root ${report.objectRoot ?? '(none)'}`,
153
+ `commit digest ${report.commitDigest ?? '(none)'}`,
154
+ `nodes ${report.nodes}`,
155
+ `relations ${report.relations}`,
156
+ `evidence bindings ${report.evidenceBindings}`,
157
+ `relation types ${symbols.join(', ') || '(none)'}`,
158
+ ];
159
+ if (!complexity) {
160
+ lines.push('cover complexity plan not supplied');
161
+ } else {
162
+ lines.push(`cover risk ${complexity.risk}`);
163
+ lines.push(`candidates ${complexity.candidates}`);
164
+ lines.push(`distinct masks ${complexity.distinctRequirementMasks}`);
165
+ lines.push(`cardinality bound ${complexity.cardinalityBound}`);
166
+ lines.push(`max cover visits ${complexity.maxCoverVisits}`);
167
+ lines.push(`combination bound ${complexity.estimatedCombinationUpperBound}`);
168
+ }
169
+ if (plan) {
170
+ lines.push(`resource bounds closureNodes ${plan.bounds.maxClosureNodes}, closureEdges ${plan.bounds.maxClosureEdges}, coverVisits ${plan.bounds.maxCoverVisits}`);
171
+ }
172
+ console.log(lines.join('\n'));
173
+ }
174
+
175
+ function dossierCommand(kar, args, flags) {
176
+ if (!kar.createKarDossier || !kar.renderKarDossier || !kar.verifyKarDossier) {
177
+ throw new Error('Could not load the KAR dossier API. Build packages/core first.');
178
+ }
179
+ const sub = args[0];
180
+ if (sub === 'build') return buildDossier(kar, flags);
181
+ if (sub === 'verify') return verifyDossier(kar, args[1]);
182
+ throw new Error(`Unknown dossier command "${sub ?? ''}".\n${USAGE}`);
183
+ }
184
+
185
+ function buildDossier(kar, flags) {
186
+ const out = path.resolve(process.cwd(), required(flags, 'out'));
187
+ if (existsSync(path.join(out, 'manifest.json'))) {
188
+ throw new Error(`Refusing to overwrite ${out}`);
189
+ }
190
+ const imagePath = path.resolve(process.cwd(), required(flags, 'image'));
191
+ const graphPath = path.resolve(process.cwd(), required(flags, 'graph'));
192
+ const planPath = path.resolve(process.cwd(), required(flags, 'plan'));
193
+ const proposition = required(flags, 'query');
194
+ const image = readBytes(imagePath);
195
+ const graph = readJson(graphPath);
196
+ const plan = readJson(planPath);
197
+ const dossier = kar.createKarDossier({ image, graph, plan, proposition });
198
+ mkdirSync(out, { recursive: true });
199
+ copyFileSync(imagePath, path.join(out, 'knowledge.knolo'));
200
+ copyFileSync(graphPath, path.join(out, 'knowledge.kar.json'));
201
+ copyFileSync(planPath, path.join(out, 'plan.json'));
202
+ writeFileSync(path.join(out, 'manifest.json'), `${JSON.stringify(dossier.manifest, null, 2)}\n`);
203
+ writeFileSync(path.join(out, 'certificate.json'), `${JSON.stringify(dossier.certificate, null, 2)}\n`);
204
+ writeFileSync(path.join(out, 'result.json'), `${JSON.stringify(dossier.evaluation, null, 2)}\n`);
205
+ writeFileSync(path.join(out, 'evidence.md'), kar.renderKarDossier(dossier));
206
+ if (flags.json) console.log(JSON.stringify(dossier.manifest, null, 2));
207
+ else console.log(`${dossier.statusStatement}\n${out}`);
208
+ }
209
+
210
+ function verifyDossier(kar, directory) {
211
+ if (!directory) throw new Error('Missing dossier directory.\nknolo kar dossier verify dossier/');
212
+ const dir = path.resolve(process.cwd(), directory);
213
+ const image = readBytes(path.join(dir, 'knowledge.knolo'));
214
+ const graph = readJson(path.join(dir, 'knowledge.kar.json'));
215
+ const plan = readJson(path.join(dir, 'plan.json'));
216
+ const manifest = readJson(path.join(dir, 'manifest.json'));
217
+ const certificate = readJson(path.join(dir, 'certificate.json'));
218
+ const evaluation = readJson(path.join(dir, 'result.json'));
219
+ const evidenceMarkdown = readBoundedText(path.join(dir, 'evidence.md'));
220
+ const proposition = evaluation && typeof evaluation.proposition === 'string' ? evaluation.proposition : '';
221
+ if (!proposition) {
222
+ console.log('KAR_RESULT_INVALID');
223
+ console.log('The dossier result has no proposition.');
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+ const verdict = kar.verifyKarDossier({
228
+ image,
229
+ graph,
230
+ plan,
231
+ proposition,
232
+ manifest,
233
+ certificate,
234
+ evaluation,
235
+ evidenceMarkdown,
236
+ });
237
+ if (!verdict.ok) {
238
+ console.log(verdict.code);
239
+ console.log(verdict.reason);
240
+ process.exitCode = 1;
241
+ return;
242
+ }
243
+ console.log('VERIFIED');
244
+ }
245
+
246
+ async function coverageCommand(kar, args, flags) {
247
+ if (!kar.createKarCoverage || !kar.verifyKarCoverage || !kar.renderKarCoverage) {
248
+ throw new Error('Could not load the KAR coverage API. Build packages/core first.');
249
+ }
250
+ const sub = args[0];
251
+ if (sub === 'build') return buildCoverage(kar, flags);
252
+ if (sub === 'verify') return verifyCoverage(kar, args[1], flags);
253
+ throw new Error(`Unknown coverage command "${sub ?? ''}".\n${USAGE}`);
254
+ }
255
+
256
+ async function buildCoverage(kar, flags) {
257
+ const out = path.resolve(process.cwd(), required(flags, 'out'));
258
+ if (existsSync(path.join(out, 'manifest.json'))) {
259
+ throw new Error(`Refusing to overwrite ${out}`);
260
+ }
261
+ const imagePath = path.resolve(process.cwd(), required(flags, 'image'));
262
+ const graphPath = path.resolve(process.cwd(), required(flags, 'graph'));
263
+ const image = readBytes(imagePath);
264
+ const graph = readJson(graphPath);
265
+ const prepared = await coverageInputs(flags);
266
+ if (!prepared.ok) {
267
+ console.log(prepared.code);
268
+ console.log(prepared.message);
269
+ process.exitCode = 1;
270
+ return;
271
+ }
272
+ let report;
273
+ try {
274
+ report = kar.createKarCoverage({
275
+ image,
276
+ graph,
277
+ cases: prepared.cases,
278
+ domainPack: prepared.domainPack,
279
+ });
280
+ } catch (error) {
281
+ if (error && typeof error.code === 'string') {
282
+ console.log(error.code);
283
+ console.log(error.message);
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+ throw error;
288
+ }
289
+ const ordered = [...prepared.cases].sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
290
+ mkdirSync(out, { recursive: true });
291
+ copyFileSync(imagePath, path.join(out, 'knowledge.knolo'));
292
+ copyFileSync(graphPath, path.join(out, 'knowledge.kar.json'));
293
+ const casesFile = { format: 'kar-coverage-cases-1', domainPack: report.domainPack, cases: ordered };
294
+ writeFileSync(path.join(out, 'cases.json'), `${JSON.stringify(casesFile, null, 2)}\n`);
295
+ writeFileSync(path.join(out, 'report.json'), `${JSON.stringify(report, null, 2)}\n`);
296
+ writeFileSync(path.join(out, 'coverage.md'), kar.renderKarCoverage(report));
297
+ writeFileSync(path.join(out, 'manifest.json'), `${JSON.stringify(report.manifest, null, 2)}\n`);
298
+ if (flags.json) console.log(JSON.stringify(report.manifest, null, 2));
299
+ else {
300
+ for (const row of report.cases) console.log(`${row.id} ${row.disposition} ${row.statement}`);
301
+ console.log(out);
302
+ }
303
+ }
304
+
305
+ async function verifyCoverage(kar, directory, flags) {
306
+ if (!directory) throw new Error('Missing coverage directory.\nknolo kar coverage verify coverage/');
307
+ const dir = path.resolve(process.cwd(), directory);
308
+ const image = readBytes(path.join(dir, 'knowledge.knolo'));
309
+ const graph = readJson(path.join(dir, 'knowledge.kar.json'));
310
+ const stored = readJson(path.join(dir, 'cases.json'));
311
+ const manifest = readJson(path.join(dir, 'manifest.json'));
312
+ const report = readJson(path.join(dir, 'report.json'));
313
+ const rendering = readBoundedText(path.join(dir, 'coverage.md'));
314
+ if (!stored || stored.format !== 'kar-coverage-cases-1' || !Array.isArray(stored.cases)) {
315
+ console.log('KAR_COVERAGE_INVALID');
316
+ console.log('cases.json is not a coverage case file.');
317
+ process.exitCode = 1;
318
+ return;
319
+ }
320
+ if (flags.domain) {
321
+ const authoring = await loadAuthoring();
322
+ const { loadDomainDirectory } = await import('./kar-produce.mjs');
323
+ const loaded = loadDomainDirectory(authoring, flags.domain);
324
+ if (!loaded.ok) {
325
+ console.log(loaded.diagnostics[0].code);
326
+ console.log(loaded.diagnostics[0].message);
327
+ process.exitCode = 1;
328
+ return;
329
+ }
330
+ const root = authoring.domainPackRoot(loaded.pack);
331
+ const recorded = stored.domainPack;
332
+ if (!recorded || recorded.root !== root || recorded.id !== loaded.pack.id || String(recorded.version) !== String(loaded.pack.version)) {
333
+ console.log('KAR_RESULT_MISMATCH');
334
+ console.log('domain pack root mismatch');
335
+ process.exitCode = 1;
336
+ return;
337
+ }
338
+ }
339
+ const verdict = kar.verifyKarCoverage({
340
+ image,
341
+ graph,
342
+ cases: stored.cases,
343
+ domainPack: stored.domainPack ?? null,
344
+ manifest,
345
+ report,
346
+ rendering,
347
+ });
348
+ if (!verdict.ok) {
349
+ console.log(verdict.code);
350
+ console.log(verdict.reason);
351
+ process.exitCode = 1;
352
+ return;
353
+ }
354
+ console.log('VERIFIED');
355
+ }
356
+
357
+ async function coverageInputs(flags) {
358
+ const plans = flags.planList ?? [];
359
+ const queries = flags.queryList ?? [];
360
+ const templates = flags.templateList ?? [];
361
+ if (plans.length > 0 && flags.domain) {
362
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Use either repeated --plan or --domain, not both.' };
363
+ }
364
+ if (templates.length > 0 && !flags.domain) {
365
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: '--template requires --domain.' };
366
+ }
367
+ if (flags.queries && !flags.domain) {
368
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: '--queries requires --domain.' };
369
+ }
370
+ if (plans.length > 0) {
371
+ if (flags.queries) {
372
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Pair each --plan with --query. --queries is for --domain.' };
373
+ }
374
+ if (queries.length !== plans.length) {
375
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Each --plan needs one --query in the same order.' };
376
+ }
377
+ return {
378
+ ok: true,
379
+ domainPack: null,
380
+ cases: plans.map((file, index) => ({
381
+ id: path.basename(file, path.extname(file)),
382
+ proposition: queries[index],
383
+ plan: readJson(path.resolve(process.cwd(), file)),
384
+ })),
385
+ };
386
+ }
387
+ if (!flags.domain) {
388
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Coverage requires repeated --plan and --query, or --domain.' };
389
+ }
390
+ if (flags.queries && queries.length > 0) {
391
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Use either --query or --queries.' };
392
+ }
393
+ if (!flags.queries && queries.length !== 1) {
394
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: 'Domain coverage needs one --query, or a --queries file.' };
395
+ }
396
+ const authoring = await loadAuthoring();
397
+ const { loadDomainDirectory } = await import('./kar-produce.mjs');
398
+ const loaded = loadDomainDirectory(authoring, flags.domain);
399
+ if (!loaded.ok) {
400
+ const diagnostic = loaded.diagnostics[0];
401
+ return { ok: false, code: diagnostic?.code ?? 'CEG_DOMAIN_INVALID', message: diagnostic?.message ?? 'Domain pack did not load.' };
402
+ }
403
+ let queryMap;
404
+ if (flags.queries) {
405
+ const parsed = readJson(path.resolve(process.cwd(), flags.queries));
406
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
407
+ return { ok: false, code: 'KAR_COVERAGE_INVALID', message: '--queries must be a JSON object.' };
408
+ }
409
+ queryMap = parsed;
410
+ }
411
+ const expanded = authoring.coverageCasesFromDomainPack({
412
+ pack: loaded.pack,
413
+ proposition: queryMap ? undefined : queries[0],
414
+ templates: templates.length > 0 ? templates : undefined,
415
+ queries: queryMap,
416
+ });
417
+ if (!expanded.ok) {
418
+ const diagnostic = expanded.diagnostics[0];
419
+ return { ok: false, code: diagnostic.code, message: diagnostic.message };
420
+ }
421
+ return { ok: true, cases: expanded.cases, domainPack: expanded.domainPack };
422
+ }
423
+
424
+ async function loadAuthoring() {
425
+ const candidates = [
426
+ '@knolo/core/kar/authoring',
427
+ path.resolve(__dirname, '../../core/dist/kar/authoring/index.js'),
428
+ path.resolve(__dirname, '../../core/dist/experimental/kar/authoring/index.js'),
429
+ '@knolo/core/experimental/kar/authoring',
430
+ ];
431
+ for (const candidate of candidates) {
432
+ try {
433
+ const href = candidate.startsWith('@') ? candidate : pathToFileURL(candidate).href;
434
+ const loaded = await import(href);
435
+ if (loaded.coverageCasesFromDomainPack && loaded.domainPackRoot && loaded.parseAuthoringDocument) return loaded;
436
+ } catch {
437
+ // Try the next candidate.
438
+ }
439
+ }
440
+ throw new Error('Could not load @knolo/core/kar/authoring. Build packages/core first.');
441
+ }
442
+
443
+ function openSession(kar, flags, requirePlan) {
444
+ const image = readBytes(required(flags, 'image'));
445
+ const graph = readJson(required(flags, 'graph'));
446
+ const session = kar.createKarSession({ image, graph });
447
+ if (!requirePlan && !flags.plan) return { session, plan: null, query: flags.query ?? '' };
448
+ const plan = readJson(required(flags, 'plan'));
449
+ const query = requirePlan ? required(flags, 'query') : flags.query ?? '';
450
+ return { session, plan, query };
451
+ }
452
+
453
+ function runRepoScript(relative) {
454
+ const root = findRepoRoot(process.cwd()) ?? findRepoRoot(path.resolve(__dirname, '../../..'));
455
+ const script = root ? path.join(root, relative) : '';
456
+ if (!root || !existsSync(script)) {
457
+ throw new Error(`Could not find ${relative}. Run this command from a knolo-core checkout.`);
458
+ }
459
+ const result = spawnSync(process.execPath, [script], { stdio: 'inherit' });
460
+ process.exitCode = result.status === null ? 1 : result.status;
461
+ }
462
+
463
+ function findRepoRoot(start) {
464
+ let dir = start;
465
+ for (let hop = 0; hop < 8; hop += 1) {
466
+ if (existsSync(path.join(dir, 'packages', 'core', 'package.json')) && existsSync(path.join(dir, 'conformance', 'kar', 'v1', 'run.mjs'))) {
467
+ return dir;
468
+ }
469
+ const parent = path.dirname(dir);
470
+ if (parent === dir) return null;
471
+ dir = parent;
472
+ }
473
+ return null;
474
+ }
475
+
476
+ async function loadKar() {
477
+ const candidates = [
478
+ '@knolo/core/kar',
479
+ path.resolve(__dirname, '../../core/dist/kar/index.js'),
480
+ path.resolve(__dirname, '../../core/dist/experimental/kar/index.js'),
481
+ '@knolo/core/experimental/kar',
482
+ ];
483
+ for (const candidate of candidates) {
484
+ try {
485
+ const href = candidate.startsWith('@') ? candidate : pathToFileURL(candidate).href;
486
+ const loaded = await import(href);
487
+ if (loaded.createKarSession && loaded.evaluateKar && loaded.verifyKar) return loaded;
488
+ } catch {
489
+ // Try the next candidate.
490
+ }
491
+ }
492
+ throw new Error('Could not load @knolo/core/kar. Build packages/core first.');
493
+ }
494
+
495
+ function parseArgs(argv) {
496
+ const flags = { planList: [], queryList: [], templateList: [] };
497
+ const positionals = [];
498
+ for (let index = 0; index < argv.length; index += 1) {
499
+ const arg = argv[index];
500
+ if (arg === '--json') flags.json = true;
501
+ else if (arg === '--auto') flags.auto = true;
502
+ else if (arg === '--help' || arg === '-h') flags.help = true;
503
+ else if (arg.startsWith('--')) {
504
+ const key = arg.slice(2);
505
+ const value = argv[index + 1];
506
+ if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for --${key}`);
507
+ if (key === 'plan') flags.planList.push(value);
508
+ else if (key === 'query') flags.queryList.push(value);
509
+ else if (key === 'template') flags.templateList.push(value);
510
+ flags[key] = value;
511
+ index += 1;
512
+ } else positionals.push(arg);
513
+ }
514
+ return { command: positionals[0] ?? '', positionals, flags };
515
+ }
516
+
517
+ function required(flags, name) {
518
+ if (!flags[name]) throw new Error(`Missing --${name}`);
519
+ return flags[name];
520
+ }
521
+
522
+ function readBytes(file) {
523
+ return readBounded(file, KAR_CLI_MAX_IMAGE_BYTES);
524
+ }
525
+
526
+ function readJson(file) {
527
+ return readBoundedJson(file);
528
+ }
package/bin/knolo.mjs CHANGED
@@ -72,6 +72,10 @@ const SUBCOMMANDS = new Set([
72
72
  'semantic:index',
73
73
  'semantic:inspect',
74
74
  'semantic:validate',
75
+ 'kar',
76
+ 'compose',
77
+ 'attest',
78
+ 'verify-attestation',
75
79
  ]);
76
80
  const ICP_SUBCOMMANDS = new Set([
77
81
  'init',
@@ -128,6 +132,18 @@ async function loadCore() {
128
132
  );
129
133
  }
130
134
 
135
+ function printVersion() {
136
+ const cli = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
137
+ let coreVersion = 'unavailable';
138
+ try {
139
+ coreVersion = JSON.parse(readFileSync(require.resolve('@knolo/core/package.json'), 'utf8')).version;
140
+ } catch {
141
+ coreVersion = 'unavailable';
142
+ }
143
+ console.log(`@knolo/cli ${cli.version}`);
144
+ console.log(`@knolo/core ${coreVersion}`);
145
+ }
146
+
131
147
  function printRootHelp() {
132
148
  console.log(`KnoLo CLI
133
149
 
@@ -154,11 +170,15 @@ Commands:
154
170
  diff <pack-a> <pack-b> Compare pack identities and source/chunk counts
155
171
  dev Watch config/sources and rebuild on change
156
172
  v5 Inspect, compress, decompress, and health-check a V5 Knowledge Image
173
+ kar KAR evidence-set retrieval over a Committed Evidence Graph
174
+ compose Compose Knowledge Images, packs, and attestations offline
175
+ attest Create or verify a composition attestation
157
176
  icp Scaffold and operate an ICP-native knowledge canister
158
177
 
159
178
  Global options:
160
179
  --debug Print stack traces for errors
161
180
  -h, --help Show help
181
+ -v, --version Print the CLI and @knolo/core versions
162
182
 
163
183
  Run "knolo <command> --help" for command details.`);
164
184
  }
@@ -188,6 +208,10 @@ function printCommandHelp(command) {
188
208
  diff: 'Usage: knolo diff <pack-a.knolo> <pack-b.knolo>',
189
209
  dev: 'Usage: knolo dev',
190
210
  v5: 'Usage: knolo v5 <info|health|studio|query|compress|decompress> <image.v5> [--out <file>] [--mode fast|balanced|max] [--attach-index]\n\nFor query: knolo v5 query <image.v5> <EQL> [--json] [--receipt <file>].',
211
+ kar: 'Usage: knolo kar <evaluate|verify|explain|inspect|graph|package|produce|domain|dossier|coverage|benchmark|demo|conformance> [options]\n\nKnolo Adversarial Retrieval. Semantics are frozen at kar-1-research-1.\n\n knolo kar evaluate --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..."\n knolo kar verify --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --result result.json\n knolo kar explain --result result.json\n knolo kar inspect --image knowledge.knolo --graph knowledge.kar.json [--plan plan.json]\n knolo kar graph build --image knowledge.knolo --source knowledge.ceg.yaml --out knowledge.kar.json\n knolo kar package --image knowledge.knolo --graph knowledge.kar.json --out dist/my-knowledge-kar/\n knolo kar produce rules --image knowledge.knolo --domain domains/contracts-v4 --out proposals.json\n knolo kar domain validate domains/contracts-v4\n knolo kar dossier build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out dossier/\n knolo kar dossier verify dossier/\n knolo kar coverage build --image knowledge.knolo --graph knowledge.kar.json --plan plan.json --query "..." --out coverage/\n knolo kar coverage verify coverage/\n knolo kar benchmark annotate --packet packet.json --decisions decisions.json --out labels.json --role human --reviewer-id <id>\n knolo kar demo\n knolo kar conformance\n\n--json prints the machine-readable result. verify exits non-zero when the certificate does not match. Abstention is a result and exits 0. graph and package author a Committed Evidence Graph without changing retrieval. produce proposes CEG Source and does not write committed bytes. dossier packages one evaluation and fails closed when committed fields change. coverage reports met requirements, gaps, and plans that did not decide. A gap is not a false claim. benchmark labels gold without showing producer output. demo and conformance run from a knolo-core checkout.',
212
+ compose: 'Usage: knolo compose <create|verify|excerpt|query> [options]\n\nOffline composition of Knowledge Images, packs, KAR sidecars, Domain Packs, and bounded excerpts.\n\n knolo compose create --members members.json --out composition/ [--host knowledge.knolo] [--availability material|digest] [--policy policy.json]\n knolo compose verify composition/ [--require-material] [--member alias=path]\n knolo compose excerpt --image knowledge.knolo --object <id> --out excerpt.json\n knolo compose query --composition composition/ --eql "FROM chunk SEARCH \\"valve\\"" [--question "..."] [--json]\n\ncompositionRoot does not change a V5 state root or a KAR root. statement used means a query or evaluation ran. It does not mean SATISFIED, and it does not mean a claim is true or false. Hub attested: true is a listing flag and is not an attestation root. The CLI does not fetch Hub.',
213
+ attest: 'Usage: knolo attest <create|verify> [options]\n\n knolo attest create --composition composition/composition.json --subject composition --statement mount|used|cited --out attestation.json [--sign-key key.json --principal <id> --key-id <id> --issued-at <ms> --expires-at <ms>]\n knolo attest verify attestation.json [--composition composition.json] [--public-key key.json] [--now <ms>] [--content-only]\n\nstatement used means a query or KAR evaluation ran. It does not mean the claim is true or false. Hub attested: true is unrelated. Signing uses the supplied times. The CLI does not read the clock.',
214
+ 'verify-attestation': 'Usage: knolo verify-attestation <attestation.json> [--composition composition.json] [--public-key key.json] [--keyring keyring.json] [--now <ms>] [--content-only]\n\nSame check as knolo attest verify. A bad signature exits 1 even when the content root matches. --content-only checks the content root and ignores the envelope. statement used is not a truth verdict. Hub attested: true is unrelated.',
191
215
  icp: 'Usage: knolo icp <command> [options]',
192
216
  'semantic:index':
193
217
  'Usage: knolo semantic:index --pack <path> [--out <path>] [--model <id>] [--endpoint <url>]',
@@ -288,6 +312,8 @@ function parseArgv(argv) {
288
312
  }
289
313
  if (args[0] === '--help' || args[0] === '-h')
290
314
  return { global, command: 'help', commandArgs: [] };
315
+ if (args[0] === '--version' || args[0] === '-v')
316
+ return { global, command: 'version', commandArgs: [] };
291
317
  return { global, command: args[0] || 'help', commandArgs: args.slice(1) };
292
318
  }
293
319
 
@@ -1816,6 +1842,7 @@ async function main() {
1816
1842
  const { global, command, commandArgs } = parseArgv(process.argv.slice(2));
1817
1843
  try {
1818
1844
  if (command === 'help') return printRootHelp();
1845
+ if (command === 'version') return printVersion();
1819
1846
  if (command === 'icp') {
1820
1847
  if (
1821
1848
  commandArgs.length === 0 ||
@@ -1830,6 +1857,14 @@ async function main() {
1830
1857
  if (SUBCOMMANDS.has(command)) {
1831
1858
  if (commandArgs.includes('--help') || commandArgs.includes('-h'))
1832
1859
  return printCommandHelp(command);
1860
+ if (command === 'kar') {
1861
+ const { runKarCli } = await import('./kar.mjs');
1862
+ return runKarCli(commandArgs);
1863
+ }
1864
+ if (command === 'compose' || command === 'attest' || command === 'verify-attestation') {
1865
+ const { runComposeCli } = await import('./compose.mjs');
1866
+ return runComposeCli(command, commandArgs);
1867
+ }
1833
1868
  if (command === 'init') return await cmdInit();
1834
1869
  if (command === 'add') return await cmdAdd(commandArgs);
1835
1870
  if (command === 'search') return await runHubSearch(commandArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knolo/cli",
3
- "version": "5.5.1",
3
+ "version": "6.1.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "knolo": "bin/knolo.mjs"
@@ -9,7 +9,7 @@
9
9
  "bin/",
10
10
  "templates/"
11
11
  ],
12
- "description": "CLI for building, inspecting, verifying, and operating Knolo V4 packs and V5 Knowledge Images.",
12
+ "description": "CLI for Knolo V4 packs, V5 Knowledge Images, KAR evidence-set retrieval, and offline composition.",
13
13
  "license": "Apache-2.0",
14
14
  "repository": {
15
15
  "type": "git",
@@ -28,11 +28,11 @@
28
28
  },
29
29
  "scripts": {
30
30
  "build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
31
- "test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/hub.integration.test.mjs",
31
+ "test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/kar.test.mjs test/kar-graph.test.mjs test/kar-produce.test.mjs test/hub.integration.test.mjs test/compose.test.mjs",
32
32
  "test:hub": "node --test test/hub.integration.test.mjs"
33
33
  },
34
34
  "dependencies": {
35
- "@knolo/core": "^5.5.0"
35
+ "@knolo/core": "^6.1.0"
36
36
  },
37
37
  "private": false
38
38
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "knolo-core-rust"
3
- version = "5.5.0"
3
+ version = "5.5.1"
4
4
  edition = "2021"
5
5
  license = "Apache-2.0"
6
6
  description = "Rust runtime support for reading and querying .knolo packs"
@@ -245,11 +245,11 @@ dependencies = [
245
245
 
246
246
  [[package]]
247
247
  name = "knolo-core-rust"
248
- version = "5.5.0"
248
+ version = "5.5.1"
249
249
 
250
250
  [[package]]
251
251
  name = "knolo-icp-canister"
252
- version = "5.5.0"
252
+ version = "5.5.1"
253
253
  dependencies = [
254
254
  "candid",
255
255
  "ic-cdk",