@knolo/cli 5.5.1 → 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.
@@ -0,0 +1,299 @@
1
+ import { existsSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { KAR_CLI_MAX_IMAGE_BYTES, readBounded, readBoundedJson, readBoundedText } from './kar-limits.mjs';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ const PRODUCE_USAGE = `Usage: knolo kar produce <rules|ontology|model|review|apply> [options]
9
+
10
+ Producers propose CEG Source. They do not write a Committed Evidence Graph.
11
+ compileCegSource remains the only writer of committed bytes.
12
+
13
+ knolo kar produce rules --image knowledge.knolo --domain domains/contracts --out proposals.json
14
+ knolo kar produce rules --image knowledge.knolo --domain domains/contracts --out generated.ceg.yaml --auto
15
+ knolo kar produce review proposals.json
16
+ knolo kar produce apply --proposals proposals.json --decisions decisions.json --out generated.ceg.yaml
17
+ knolo kar produce ontology --input ontology.json --mapping mapping.json --out proposals.json [--image knowledge.knolo]
18
+ knolo kar produce model --input model.json --out proposals.json [--image knowledge.knolo] [--domain domains/contracts]
19
+
20
+ --auto Write accepted deterministic output as CEG Source
21
+ --json Print machine-readable output
22
+
23
+ --auto does not accept model output. An existing --out file is left unchanged.
24
+ `;
25
+
26
+ const DOMAIN_USAGE = `Usage: knolo kar domain <validate|inspect|test> <domain-directory>
27
+
28
+ knolo kar domain validate domains/contracts
29
+ knolo kar domain inspect domains/contracts
30
+ knolo kar domain test domains/contracts
31
+
32
+ --json Print machine-readable output
33
+ `;
34
+
35
+ export async function runKarProduce(command, positionals, flags) {
36
+ if (command === 'domain') return runDomain(positionals, flags);
37
+ return runProduce(positionals, flags);
38
+ }
39
+
40
+ async function runProduce(positionals, flags) {
41
+ const sub = positionals[0] ?? '';
42
+ if (!sub || sub === 'help') {
43
+ console.log(PRODUCE_USAGE.trim());
44
+ return;
45
+ }
46
+ const authoring = await loadAuthoring();
47
+ if (sub === 'rules') return produceRules(authoring, flags);
48
+ if (sub === 'ontology') return produceOntology(authoring, flags);
49
+ if (sub === 'model') return produceModel(authoring, flags);
50
+ if (sub === 'review') return reviewProposals(authoring, positionals, flags);
51
+ if (sub === 'apply') return applyProposals(authoring, flags);
52
+ throw new Error(`Unknown kar produce command "${sub}".\n${PRODUCE_USAGE}`);
53
+ }
54
+
55
+ async function runDomain(positionals, flags) {
56
+ const sub = positionals[0] ?? '';
57
+ if (!sub || sub === 'help') {
58
+ console.log(DOMAIN_USAGE.trim());
59
+ return;
60
+ }
61
+ const authoring = await loadAuthoring();
62
+ const directory = positionals[1] ?? flags.domain;
63
+ if (!directory) throw new Error('Missing domain directory.');
64
+ const loaded = loadDomainDirectory(authoring, directory);
65
+ if (!loaded.ok) return fail(flags, loaded.diagnostics);
66
+ if (sub === 'validate') return validateDomain(authoring, loaded, flags);
67
+ if (sub === 'inspect') return inspectDomain(authoring, loaded, flags);
68
+ if (sub === 'test') return testDomain(authoring, loaded, flags);
69
+ throw new Error(`Unknown kar domain command "${sub}".\n${DOMAIN_USAGE}`);
70
+ }
71
+
72
+ async function produceRules(authoring, flags) {
73
+ const image = readBytes(required(flags, 'image'));
74
+ const loaded = loadDomainDirectory(authoring, required(flags, 'domain'));
75
+ if (!loaded.ok) return fail(flags, loaded.diagnostics);
76
+ const ran = authoring.runRuleProducer({ pack: loaded.pack, image });
77
+ if (!ran.ok) return fail(flags, ran.diagnostics);
78
+ if (flags.auto) return writeAutoSource(authoring, flags, ran.run);
79
+ writeRun(required(flags, 'out'), ran.run);
80
+ if (flags.json) console.log(JSON.stringify({ ok: true, proposals: ran.run.proposals.length, domainPackRoot: ran.run.provenance.domainPackRoot }, null, 2));
81
+ else console.log(`CEG PRODUCER RUN\nproposals ${ran.run.proposals.length}\ndomain pack ${ran.run.provenance.domainPackRoot}\nwrote ${flags.out}`);
82
+ }
83
+
84
+ async function produceOntology(authoring, flags) {
85
+ const ontology = readJson(required(flags, 'input'));
86
+ const mapping = readJson(required(flags, 'mapping'));
87
+ const image = flags.image ? readBytes(flags.image) : undefined;
88
+ const ran = authoring.runOntologyProducer({ ontology, mapping, image, auto: flags.auto === true });
89
+ if (!ran.ok) return fail(flags, ran.diagnostics);
90
+ if (flags.auto) return writeAutoSource(authoring, flags, ran.run);
91
+ writeRun(required(flags, 'out'), ran.run);
92
+ if (flags.json) console.log(JSON.stringify({ ok: true, proposals: ran.run.proposals.length }, null, 2));
93
+ else console.log(`CEG PRODUCER RUN\nproposals ${ran.run.proposals.length}\nwrote ${flags.out}`);
94
+ }
95
+
96
+ async function produceModel(authoring, flags) {
97
+ if (flags.auto) throw new Error('Model output cannot be auto-accepted. Review proposals, then apply explicit decisions.');
98
+ const output = readJson(required(flags, 'input'));
99
+ const image = flags.image ? readBytes(flags.image) : undefined;
100
+ let catalog;
101
+ if (image) {
102
+ const catalogModule = await loadCatalog();
103
+ const opened = catalogModule.openEvidenceCatalog(image);
104
+ if (!opened.ok) return fail(flags, opened.diagnostics);
105
+ catalog = opened.catalog;
106
+ }
107
+ let domainPack;
108
+ if (flags.domain) {
109
+ const loaded = loadDomainDirectory(authoring, flags.domain);
110
+ if (!loaded.ok) return fail(flags, loaded.diagnostics);
111
+ domainPack = loaded.pack;
112
+ }
113
+ const ran = authoring.validateModelProposals({ output, catalog, domainPack });
114
+ if (!ran.ok) return fail(flags, ran.diagnostics);
115
+ writeRun(required(flags, 'out'), ran.run);
116
+ if (flags.json) console.log(JSON.stringify({ ok: true, proposals: ran.run.proposals.length, autoAcceptModelOutput: false }, null, 2));
117
+ else console.log(`CEG PRODUCER RUN\nproposals ${ran.run.proposals.length}\nmodel output PROPOSED\nwrote ${flags.out}`);
118
+ }
119
+
120
+ function reviewProposals(authoring, positionals, flags) {
121
+ const file = positionals[1] ?? flags.proposals;
122
+ if (!file) throw new Error('Missing producer run path.');
123
+ const run = readJson(file);
124
+ const text = authoring.renderProducerReview(run);
125
+ if (flags.json) {
126
+ console.log(JSON.stringify(run, null, 2));
127
+ return;
128
+ }
129
+ console.log(text);
130
+ }
131
+
132
+ function applyProposals(authoring, flags) {
133
+ const run = readJson(required(flags, 'proposals'));
134
+ const decisions = flags.decisions
135
+ ? readJson(flags.decisions)
136
+ : { format: 'ceg-decisions-1', decisions: [] };
137
+ const applied = authoring.applyProducerDecisions(run, decisions);
138
+ if (!applied.ok) return fail(flags, applied.diagnostics);
139
+ const out = required(flags, 'out');
140
+ writeNew(out, out.endsWith('.json') ? authoring.emitCegSourceJson(applied.source) : authoring.emitCegSourceYaml(applied.source));
141
+ if (flags.json) console.log(JSON.stringify({ ok: true, concepts: Object.keys(applied.source.concepts).length, relations: applied.source.relations.length }, null, 2));
142
+ else console.log(`CEG SOURCE\nconcepts ${Object.keys(applied.source.concepts).length}\nrelations ${applied.source.relations.length}\nwrote ${out}`);
143
+ }
144
+
145
+ function validateDomain(authoring, loaded, flags) {
146
+ const report = {
147
+ ok: true,
148
+ id: loaded.pack.id,
149
+ version: loaded.pack.version,
150
+ domainPackRoot: authoring.domainPackRoot(loaded.pack),
151
+ rules: loaded.pack.rules.length,
152
+ fixtures: loaded.fixtures.length,
153
+ plans: Object.keys(loaded.pack.planTemplates).sort(),
154
+ };
155
+ if (flags.json) console.log(JSON.stringify(report, null, 2));
156
+ else console.log(`CEG DOMAIN VALID\nid ${report.id}\nversion ${report.version}\nroot ${report.domainPackRoot}`);
157
+ }
158
+
159
+ function inspectDomain(authoring, loaded, flags) {
160
+ if (flags.json) {
161
+ console.log(JSON.stringify({
162
+ id: loaded.pack.id,
163
+ version: loaded.pack.version,
164
+ domainPackRoot: authoring.domainPackRoot(loaded.pack),
165
+ description: loaded.pack.description ?? null,
166
+ relations: [...loaded.pack.relations].sort(),
167
+ conceptKinds: loaded.pack.conceptKinds.map((kind) => kind.id).sort(),
168
+ rules: loaded.pack.rules.length,
169
+ planTemplates: Object.keys(loaded.pack.planTemplates).sort(),
170
+ fixtures: loaded.fixtures.length,
171
+ }, null, 2));
172
+ return;
173
+ }
174
+ console.log(authoring.inspectDomainPack(loaded.pack, loaded.fixtures.length));
175
+ }
176
+
177
+ function testDomain(authoring, loaded, flags) {
178
+ const report = authoring.testDomainFixtures(loaded.pack, loaded.fixtures);
179
+ if (flags.json) console.log(JSON.stringify(report, null, 2));
180
+ else console.log(authoring.renderDomainTest(report));
181
+ if (!report.ok || report.unmatchedRules.length > 0) process.exitCode = 1;
182
+ }
183
+
184
+ function writeAutoSource(authoring, flags, run) {
185
+ const out = required(flags, 'out');
186
+ const production = productionPath(out);
187
+ writeNew(out, out.endsWith('.json') ? authoring.emitCegSourceJson(run.fragment) : authoring.emitCegSourceYaml(run.fragment));
188
+ writeNew(production, `${JSON.stringify(run, null, 2)}\n`);
189
+ console.log(`CEG SOURCE\nwrote ${out}\nproduction ${production}`);
190
+ }
191
+
192
+ function writeRun(file, run) {
193
+ writeNew(file, `${JSON.stringify(run, null, 2)}\n`);
194
+ }
195
+
196
+ export function loadDomainDirectory(authoring, directory) {
197
+ const resolved = resolve(directory);
198
+ const parsed = authoring.parseAuthoringDocument(readBoundedText(path.join(resolved, 'domain.yaml')));
199
+ if (!parsed.ok) return parsed;
200
+ const value = parsed.value;
201
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
202
+ return { ok: false, diagnostics: [{ severity: 'error', code: 'CEG_DOMAIN_INVALID', path: 'domain.yaml', message: 'Domain document must be an object.' }] };
203
+ }
204
+ const rules = [];
205
+ const rulesDir = path.join(resolved, 'rules');
206
+ if (existsSync(rulesDir)) {
207
+ for (const name of readdirSync(rulesDir).filter((entry) => /\.(ya?ml|json)$/i.test(entry)).sort()) {
208
+ const item = authoring.parseAuthoringDocument(readBoundedText(path.join(rulesDir, name)));
209
+ if (!item.ok) return item;
210
+ if (Array.isArray(item.value)) rules.push(...item.value);
211
+ else rules.push(item.value);
212
+ }
213
+ }
214
+ if (Array.isArray(value.rules)) rules.push(...value.rules);
215
+ value.rules = rules;
216
+ const planTemplates = value.planTemplates && typeof value.planTemplates === 'object' ? { ...value.planTemplates } : {};
217
+ const plansDir = path.join(resolved, 'plans');
218
+ if (existsSync(plansDir)) {
219
+ for (const name of readdirSync(plansDir).filter((entry) => entry.endsWith('.json')).sort()) {
220
+ planTemplates[name.replace(/\.json$/, '')] = readBoundedJson(path.join(plansDir, name));
221
+ }
222
+ }
223
+ value.planTemplates = planTemplates;
224
+ const fixtures = [];
225
+ const fixturesDir = path.join(resolved, 'fixtures');
226
+ if (existsSync(fixturesDir)) {
227
+ for (const name of readdirSync(fixturesDir).filter((entry) => entry.endsWith('.json')).sort()) {
228
+ fixtures.push(readBoundedJson(path.join(fixturesDir, name)));
229
+ }
230
+ }
231
+ const interpreted = authoring.interpretDomainPack(value);
232
+ if (!interpreted.ok) return interpreted;
233
+ return { ok: true, pack: interpreted.pack, fixtures, diagnostics: interpreted.diagnostics };
234
+ }
235
+
236
+ function productionPath(file) {
237
+ return `${file.replace(/\.(ya?ml|json)$/i, '')}.production.json`;
238
+ }
239
+
240
+ function writeNew(file, text) {
241
+ const resolved = resolve(file);
242
+ if (existsSync(resolved)) throw new Error(`Refusing to overwrite ${file}. Choose a new --out path.`);
243
+ writeFileSync(resolved, text);
244
+ }
245
+
246
+ function fail(flags, diagnostics) {
247
+ if (flags.json) console.log(JSON.stringify({ ok: false, diagnostics }, null, 2));
248
+ else {
249
+ for (const item of diagnostics) console.log(`${item.severity} ${item.code} ${item.path} ${item.message}`);
250
+ }
251
+ process.exitCode = 1;
252
+ }
253
+
254
+ function required(flags, name) {
255
+ if (!flags[name]) throw new Error(`Missing --${name}`);
256
+ return flags[name];
257
+ }
258
+
259
+ function resolve(file) {
260
+ return path.resolve(process.cwd(), file);
261
+ }
262
+
263
+ function readBytes(file) {
264
+ return readBounded(file, KAR_CLI_MAX_IMAGE_BYTES);
265
+ }
266
+
267
+ function readJson(file) {
268
+ return readBoundedJson(file);
269
+ }
270
+
271
+ async function loadAuthoring() {
272
+ const loaded = await loadFirst([
273
+ path.resolve(__dirname, '../../core/dist/experimental/kar/authoring/index.js'),
274
+ '@knolo/core/experimental/kar/authoring',
275
+ ], (mod) => mod.runRuleProducer && mod.interpretDomainPack && mod.compileCegSource);
276
+ if (!loaded) throw new Error('Could not load @knolo/core/experimental/kar/authoring. Build packages/core first.');
277
+ return loaded;
278
+ }
279
+
280
+ async function loadCatalog() {
281
+ const loaded = await loadFirst([
282
+ path.resolve(__dirname, '../../core/dist/experimental/kar/authoring/catalog.js'),
283
+ ], (mod) => mod.openEvidenceCatalog);
284
+ if (!loaded) throw new Error('Could not load the CEG evidence catalog. Build packages/core first.');
285
+ return loaded;
286
+ }
287
+
288
+ async function loadFirst(candidates, accept) {
289
+ for (const candidate of candidates) {
290
+ try {
291
+ const href = candidate.startsWith('@') ? candidate : pathToFileURL(candidate).href;
292
+ const loaded = await import(href);
293
+ if (accept(loaded)) return loaded;
294
+ } catch {
295
+ // Try the next candidate.
296
+ }
297
+ }
298
+ return null;
299
+ }