@knolo/cli 5.5.0 → 6.0.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 +18 -2
- package/bin/kar-benchmark.mjs +323 -0
- package/bin/kar-graph.mjs +469 -0
- package/bin/kar-limits.mjs +36 -0
- package/bin/kar-produce.mjs +299 -0
- package/bin/kar.mjs +528 -0
- package/bin/knolo.mjs +207 -20
- package/package.json +4 -4
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +1 -1
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +2 -2
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
|
+
}
|