@aikdna/kdna-cli 0.9.0 → 0.11.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 +109 -31
- package/package.json +1 -1
- package/src/agent.js +410 -2
- package/src/cli.js +267 -38
- package/src/cmds/_common.js +65 -3
- package/src/cmds/badge.js +244 -0
- package/src/cmds/changelog.js +226 -0
- package/src/cmds/cluster.js +214 -3
- package/src/cmds/doctor.js +160 -0
- package/src/cmds/domain.js +110 -33
- package/src/cmds/governance.js +471 -0
- package/src/cmds/identity.js +5 -4
- package/src/cmds/quality.js +34 -9
- package/src/cmds/registry.js +62 -22
- package/src/cmds/studio.js +577 -0
- package/src/cmds/test.js +177 -0
- package/src/compare.js +46 -32
- package/src/diff.js +136 -38
- package/src/identity.js +20 -4
- package/src/install.js +181 -91
- package/src/publish.js +4 -3
- package/src/search.js +33 -2
- package/src/verify.js +76 -13
- package/src/version.js +110 -3
package/src/cmds/cluster.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { error, readJson } = require('./_common');
|
|
3
|
+
const { error, readJson, EXIT } = require('./_common');
|
|
4
4
|
const {
|
|
5
5
|
loadCluster,
|
|
6
6
|
classifySignalsAcrossDomains,
|
|
@@ -29,6 +29,15 @@ function cmdCluster(args) {
|
|
|
29
29
|
} else if (sub === 'match') {
|
|
30
30
|
if (!target) error('Usage: kdna cluster match <cluster.json> --input "<task>"');
|
|
31
31
|
cmdClusterMatch(target, args);
|
|
32
|
+
} else if (sub === 'compose') {
|
|
33
|
+
if (!target) error('Usage: kdna cluster compose <cluster.json> --input "<task>" [--profile=compact] [--json]');
|
|
34
|
+
cmdClusterCompose(target, args);
|
|
35
|
+
} else if (sub === 'conflicts') {
|
|
36
|
+
if (!target) error('Usage: kdna cluster conflicts <cluster.json> --input "<task>" [--json]');
|
|
37
|
+
cmdClusterConflicts(target, args);
|
|
38
|
+
} else if (sub === 'graph') {
|
|
39
|
+
if (!target) error('Usage: kdna cluster graph <cluster.json> [--format=dot|json]');
|
|
40
|
+
cmdClusterGraph(target, args);
|
|
32
41
|
} else if (sub === 'apply') {
|
|
33
42
|
error(
|
|
34
43
|
'kdna cluster apply was removed in v0.9.\n' +
|
|
@@ -42,12 +51,15 @@ function cmdCluster(args) {
|
|
|
42
51
|
' kdna cluster init <name>\n' +
|
|
43
52
|
' kdna cluster info <cluster.json>\n' +
|
|
44
53
|
' kdna cluster match <cluster.json> --input "<task>"\n' +
|
|
45
|
-
' kdna cluster load <cluster.json> --input "<task>"'
|
|
54
|
+
' kdna cluster load <cluster.json> --input "<task>"\n' +
|
|
55
|
+
' kdna cluster compose <cluster.json> --input "<task>"\n' +
|
|
56
|
+
' kdna cluster conflicts <cluster.json> --input "<task>"\n' +
|
|
57
|
+
' kdna cluster graph <cluster.json>',
|
|
46
58
|
);
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
function cmdClusterInfo(target,
|
|
62
|
+
function cmdClusterInfo(target, _format = 'human') {
|
|
51
63
|
const abs = path.resolve(target);
|
|
52
64
|
if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
|
|
53
65
|
|
|
@@ -232,4 +244,203 @@ function cmdClusterMatch(target, args = []) {
|
|
|
232
244
|
});
|
|
233
245
|
}
|
|
234
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Compose: classify input signals, then compose context with source attribution.
|
|
249
|
+
* Unlike load (which includes trace), compose focuses on the composed context.
|
|
250
|
+
*/
|
|
251
|
+
function cmdClusterCompose(target, args = []) {
|
|
252
|
+
const jsonMode = args.includes('--json');
|
|
253
|
+
const abs = path.resolve(target);
|
|
254
|
+
if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
|
|
255
|
+
|
|
256
|
+
const profileIdx = args.indexOf('--profile');
|
|
257
|
+
let profile = 'compact';
|
|
258
|
+
if (profileIdx >= 0) {
|
|
259
|
+
const val = args[profileIdx + 1];
|
|
260
|
+
if (val && !val.startsWith('--')) profile = val;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const inputIdx = args.indexOf('--input');
|
|
264
|
+
const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
|
|
265
|
+
if (!input) error('Usage: kdna cluster compose <cluster.json> --input "<task>"');
|
|
266
|
+
|
|
267
|
+
const manifest = readJson(abs);
|
|
268
|
+
if (!manifest || !manifest.cluster_id) error('Not a valid cluster manifest');
|
|
269
|
+
|
|
270
|
+
const domains = loadClusterDomains(manifest);
|
|
271
|
+
|
|
272
|
+
const classification = classifySignalsAcrossDomains(input, domains);
|
|
273
|
+
const { context } = composeContextWithAttribution(classification.selected);
|
|
274
|
+
|
|
275
|
+
if (jsonMode) {
|
|
276
|
+
console.log(JSON.stringify({
|
|
277
|
+
cluster: manifest.cluster_id,
|
|
278
|
+
input: input.slice(0, 200),
|
|
279
|
+
selected: classification.selected.map((d) => ({
|
|
280
|
+
id: d.id,
|
|
281
|
+
role: d.role,
|
|
282
|
+
reason: d.reason,
|
|
283
|
+
})),
|
|
284
|
+
excluded: classification.excluded.map((d) => ({
|
|
285
|
+
id: d.id,
|
|
286
|
+
reason: d.reason,
|
|
287
|
+
})),
|
|
288
|
+
context,
|
|
289
|
+
}, null, 2));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log(`Cluster: ${manifest.cluster_id}`);
|
|
294
|
+
console.log(`Profile: ${profile}`);
|
|
295
|
+
console.log(`Input: ${input.slice(0, 100)}${input.length > 100 ? '...' : ''}`);
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log(`Selected: ${classification.selected.length} | Excluded: ${classification.excluded.length}`);
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log('─'.repeat(64));
|
|
300
|
+
console.log(context);
|
|
301
|
+
console.log('─'.repeat(64));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Conflicts: detect conflicts between selected domains for given input.
|
|
306
|
+
*/
|
|
307
|
+
function cmdClusterConflicts(target, args = []) {
|
|
308
|
+
const jsonMode = args.includes('--json');
|
|
309
|
+
const abs = path.resolve(target);
|
|
310
|
+
if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
|
|
311
|
+
|
|
312
|
+
const inputIdx = args.indexOf('--input');
|
|
313
|
+
const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
|
|
314
|
+
if (!input) error('Usage: kdna cluster conflicts <cluster.json> --input "<task>"');
|
|
315
|
+
|
|
316
|
+
const manifest = readJson(abs);
|
|
317
|
+
if (!manifest || !manifest.cluster_id) error('Not a valid cluster manifest');
|
|
318
|
+
|
|
319
|
+
const domains = loadClusterDomains(manifest);
|
|
320
|
+
const classification = classifySignalsAcrossDomains(input, domains);
|
|
321
|
+
const conflicts = detectDomainConflicts(classification.selected);
|
|
322
|
+
|
|
323
|
+
if (jsonMode) {
|
|
324
|
+
console.log(JSON.stringify({
|
|
325
|
+
cluster: manifest.cluster_id,
|
|
326
|
+
input: input.slice(0, 200),
|
|
327
|
+
selected: classification.selected.map((d) => ({ id: d.id, role: d.role })),
|
|
328
|
+
conflicts: conflicts.map((c) => ({
|
|
329
|
+
type: c.type,
|
|
330
|
+
domains: c.domains,
|
|
331
|
+
description: c.description,
|
|
332
|
+
severity: c.severity || 'warn',
|
|
333
|
+
})),
|
|
334
|
+
conflict_count: conflicts.length,
|
|
335
|
+
safe: conflicts.length === 0,
|
|
336
|
+
}, null, 2));
|
|
337
|
+
process.exit(conflicts.length ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(`Cluster: ${manifest.cluster_id}`);
|
|
341
|
+
console.log(`Input: ${input.slice(0, 100)}${input.length > 100 ? '...' : ''}`);
|
|
342
|
+
console.log(`Selected: ${classification.selected.length} domains | Conflicts: ${conflicts.length}`);
|
|
343
|
+
console.log('');
|
|
344
|
+
|
|
345
|
+
if (!conflicts.length) {
|
|
346
|
+
console.log('✓ No conflicts detected.');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const c of conflicts) {
|
|
351
|
+
const severity = c.severity === 'error' ? '✗' : '⚠';
|
|
352
|
+
console.log(`${severity} [${c.type}] ${c.domains.join(' vs ')}`);
|
|
353
|
+
console.log(` ${c.description}`);
|
|
354
|
+
console.log('');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Graph: output the domain relationship graph from a cluster manifest.
|
|
360
|
+
*/
|
|
361
|
+
function cmdClusterGraph(target, args = []) {
|
|
362
|
+
const abs = path.resolve(target);
|
|
363
|
+
if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
|
|
364
|
+
|
|
365
|
+
const manifest = readJson(abs);
|
|
366
|
+
if (!manifest || !manifest.cluster_id) error('Not a valid cluster manifest');
|
|
367
|
+
|
|
368
|
+
const formatIdx = args.indexOf('--format');
|
|
369
|
+
let format = 'dot';
|
|
370
|
+
if (formatIdx >= 0) {
|
|
371
|
+
const val = args[formatIdx + 1];
|
|
372
|
+
if (val && ['dot', 'json'].includes(val)) format = val;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (format === 'json') {
|
|
376
|
+
const graph = {
|
|
377
|
+
cluster: manifest.cluster_id,
|
|
378
|
+
version: manifest.version || '?',
|
|
379
|
+
type: manifest.type || 'horizontal',
|
|
380
|
+
nodes: (manifest.domains || []).map((d) => ({
|
|
381
|
+
id: d.id || d.role,
|
|
382
|
+
role: d.role,
|
|
383
|
+
required: d.required !== false,
|
|
384
|
+
})),
|
|
385
|
+
edges: (manifest.relationships || []).map((r) => ({
|
|
386
|
+
from: r.from,
|
|
387
|
+
to: r.to,
|
|
388
|
+
type: r.type,
|
|
389
|
+
})),
|
|
390
|
+
};
|
|
391
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// DOT format output
|
|
396
|
+
console.log(`digraph "${manifest.cluster_id}" {`);
|
|
397
|
+
console.log(' rankdir=LR;');
|
|
398
|
+
console.log(` label="${manifest.cluster_id} v${manifest.version || '?'}";`);
|
|
399
|
+
console.log(' fontsize=14;');
|
|
400
|
+
console.log('');
|
|
401
|
+
|
|
402
|
+
// Nodes
|
|
403
|
+
for (const d of manifest.domains || []) {
|
|
404
|
+
const shape = d.role === 'primary' ? 'box' : d.role === 'critic' ? 'diamond' : 'ellipse';
|
|
405
|
+
const required = d.required !== false ? ',style=filled,fillcolor="#e8f0fe"' : ',style=dashed';
|
|
406
|
+
console.log(` "${d.id || d.role}" [label="${d.id || d.role}\\n[${d.role}]",shape=${shape}${required}];`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Edges
|
|
410
|
+
if (manifest.relationships) {
|
|
411
|
+
console.log('');
|
|
412
|
+
for (const r of manifest.relationships) {
|
|
413
|
+
const style = r.type === 'conflicts' ? ',style=dashed,color=red' :
|
|
414
|
+
r.type === 'extends' ? ',style=bold' : '';
|
|
415
|
+
console.log(` "${r.from}" -> "${r.to}" [label="${r.type}"${style}];`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log('}');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Shared domain loader for cluster commands.
|
|
424
|
+
*/
|
|
425
|
+
function loadClusterDomains(manifest) {
|
|
426
|
+
const INSTALL_DIR = path.join(
|
|
427
|
+
process.env.HOME || process.env.USERPROFILE || '.',
|
|
428
|
+
'.kdna',
|
|
429
|
+
'domains',
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return (manifest.domains || []).map((d) => {
|
|
433
|
+
const domainId = d.id;
|
|
434
|
+
if (!domainId) return null;
|
|
435
|
+
const [scope, ident] = domainId.startsWith('@')
|
|
436
|
+
? [domainId.slice(0, domainId.indexOf('/')), domainId.slice(domainId.indexOf('/') + 1)]
|
|
437
|
+
: ['@aikdna', domainId];
|
|
438
|
+
const dir = path.join(INSTALL_DIR, scope, ident);
|
|
439
|
+
const core = readJson(path.join(dir, 'KDNA_Core.json'));
|
|
440
|
+
const pat = readJson(path.join(dir, 'KDNA_Patterns.json'));
|
|
441
|
+
if (!core || !pat) return null;
|
|
442
|
+
return { id: domainId, role: d.role, required: d.required !== false, core, patterns: pat };
|
|
443
|
+
}).filter(Boolean);
|
|
444
|
+
}
|
|
445
|
+
|
|
235
446
|
module.exports = { cmdCluster };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { EXIT, error } = require('./_common');
|
|
4
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
5
|
+
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
6
|
+
|
|
7
|
+
function cmdDoctor(args) {
|
|
8
|
+
const json = args.includes('--json');
|
|
9
|
+
const quiet = args.includes('--quiet');
|
|
10
|
+
|
|
11
|
+
const checks = [];
|
|
12
|
+
|
|
13
|
+
// 1. Node.js version
|
|
14
|
+
const nodeVersion = process.version;
|
|
15
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
16
|
+
checks.push({
|
|
17
|
+
name: 'Node.js',
|
|
18
|
+
status: major >= 18 ? 'ok' : 'fail',
|
|
19
|
+
detail: `${nodeVersion} (${major >= 18 ? '>=18 required' : 'requires >=18'})`,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// 2. @aikdna/kdna-core available
|
|
23
|
+
let coreVersion = null;
|
|
24
|
+
try {
|
|
25
|
+
const corePkg = require.resolve('@aikdna/kdna-core/package.json');
|
|
26
|
+
coreVersion = JSON.parse(fs.readFileSync(corePkg, 'utf8')).version;
|
|
27
|
+
checks.push({ name: '@aikdna/kdna-core', status: 'ok', detail: `v${coreVersion}` });
|
|
28
|
+
} catch {
|
|
29
|
+
checks.push({ name: '@aikdna/kdna-core', status: 'fail', detail: 'not installed' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. ~/.kdna/ exists
|
|
33
|
+
if (fs.existsSync(USER_KDNA_DIR)) {
|
|
34
|
+
checks.push({ name: 'KDNA data directory', status: 'ok', detail: USER_KDNA_DIR });
|
|
35
|
+
} else {
|
|
36
|
+
checks.push({ name: 'KDNA data directory', status: 'warn', detail: `~/.kdna/ not found` });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 4. ~/.kdna/domains/ exists and has domains
|
|
40
|
+
if (fs.existsSync(INSTALL_DIR)) {
|
|
41
|
+
const domains = fs
|
|
42
|
+
.readdirSync(INSTALL_DIR, { withFileTypes: true })
|
|
43
|
+
.filter((d) => d.isDirectory())
|
|
44
|
+
.reduce((acc, scopeDir) => {
|
|
45
|
+
if (scopeDir.name.startsWith('@')) {
|
|
46
|
+
try {
|
|
47
|
+
return acc + fs.readdirSync(path.join(INSTALL_DIR, scopeDir.name)).length;
|
|
48
|
+
} catch {
|
|
49
|
+
return acc;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return acc + 1;
|
|
53
|
+
}, 0);
|
|
54
|
+
checks.push({
|
|
55
|
+
name: 'Installed domains',
|
|
56
|
+
status: domains > 0 ? 'ok' : 'warn',
|
|
57
|
+
detail: `${domains} domain${domains !== 1 ? 's' : ''} installed`,
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
checks.push({ name: 'Domains directory', status: 'warn', detail: '~/.kdna/domains/ not found' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. Identity key available
|
|
64
|
+
const identityDir = path.join(USER_KDNA_DIR, 'identity');
|
|
65
|
+
const identityDirOfficial = path.join(USER_KDNA_DIR, 'identity-official');
|
|
66
|
+
const hasIdentity =
|
|
67
|
+
(fs.existsSync(identityDir) && fs.readdirSync(identityDir).length > 0) ||
|
|
68
|
+
(fs.existsSync(identityDirOfficial) && fs.readdirSync(identityDirOfficial).length > 0);
|
|
69
|
+
checks.push({
|
|
70
|
+
name: 'Signing identity',
|
|
71
|
+
status: hasIdentity ? 'ok' : 'warn',
|
|
72
|
+
detail: hasIdentity ? 'key available' : 'no identity (run: kdna identity init)',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 6. Registry cache
|
|
76
|
+
const registryCache = path.join(USER_KDNA_DIR, 'registry-cache.json');
|
|
77
|
+
if (fs.existsSync(registryCache)) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(registryCache);
|
|
80
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
81
|
+
const ageH = Math.round(ageMs / 3600000);
|
|
82
|
+
const fresh = ageH < 24;
|
|
83
|
+
checks.push({
|
|
84
|
+
name: 'Registry cache',
|
|
85
|
+
status: fresh ? 'ok' : 'warn',
|
|
86
|
+
detail: `updated ${ageH < 1 ? '<1h' : ageH + 'h'} ago`,
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
checks.push({ name: 'Registry cache', status: 'warn', detail: 'cannot read cache' });
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
checks.push({ name: 'Registry cache', status: 'warn', detail: 'not cached (run: kdna registry refresh)' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 7. Schema files available
|
|
96
|
+
let schemaCount = 0;
|
|
97
|
+
try {
|
|
98
|
+
const schemaDir = path.join(
|
|
99
|
+
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
100
|
+
'schema',
|
|
101
|
+
);
|
|
102
|
+
schemaCount = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.schema.json')).length;
|
|
103
|
+
checks.push({
|
|
104
|
+
name: 'Schema files',
|
|
105
|
+
status: schemaCount >= 6 ? 'ok' : 'warn',
|
|
106
|
+
detail: `${schemaCount} schemas`,
|
|
107
|
+
});
|
|
108
|
+
} catch {
|
|
109
|
+
checks.push({ name: 'Schema files', status: 'fail', detail: 'not found' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 8. Project .kdna/config.json
|
|
113
|
+
const projectConfig = path.join(process.cwd(), '.kdna', 'config.json');
|
|
114
|
+
if (fs.existsSync(projectConfig)) {
|
|
115
|
+
checks.push({ name: 'Project config', status: 'ok', detail: projectConfig });
|
|
116
|
+
} else {
|
|
117
|
+
checks.push({ name: 'Project config', status: 'warn', detail: 'No .kdna/config.json in current project' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Output
|
|
121
|
+
if (json) {
|
|
122
|
+
const result = {
|
|
123
|
+
checks: checks.map((c) => ({
|
|
124
|
+
name: c.name,
|
|
125
|
+
status: c.status,
|
|
126
|
+
detail: c.detail,
|
|
127
|
+
})),
|
|
128
|
+
ok: checks.filter((c) => c.status === 'ok').length,
|
|
129
|
+
warnings: checks.filter((c) => c.status === 'warn').length,
|
|
130
|
+
failures: checks.filter((c) => c.status === 'fail').length,
|
|
131
|
+
healthy: checks.every((c) => c.status !== 'fail'),
|
|
132
|
+
};
|
|
133
|
+
console.log(JSON.stringify(result, null, 2));
|
|
134
|
+
process.exit(result.healthy ? 0 : EXIT.VALIDATION_FAILED);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!quiet) {
|
|
138
|
+
for (const c of checks) {
|
|
139
|
+
const mark = c.status === 'ok' ? '✓' : c.status === 'warn' ? '⚠' : '✗';
|
|
140
|
+
console.log(`${mark} ${c.name}: ${c.detail}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ok = checks.filter((c) => c.status === 'ok').length;
|
|
144
|
+
const warns = checks.filter((c) => c.status === 'warn').length;
|
|
145
|
+
const fails = checks.filter((c) => c.status === 'fail').length;
|
|
146
|
+
console.log('');
|
|
147
|
+
if (fails > 0) {
|
|
148
|
+
console.log(`${ok}/${checks.length} checks passed (${fails} failure${fails !== 1 ? 's' : ''}, ${warns} warning${warns !== 1 ? 's' : ''})`);
|
|
149
|
+
} else if (warns > 0) {
|
|
150
|
+
console.log(`${ok}/${checks.length} checks passed (${warns} warning${warns !== 1 ? 's' : ''})`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`${ok}/${checks.length} checks passed`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const hasFail = checks.some((c) => c.status === 'fail');
|
|
157
|
+
process.exit(hasFail ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { cmdDoctor };
|
package/src/cmds/domain.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
-
const { error, readJson, writeJson } = require('./_common');
|
|
4
|
+
const { error, readJson, writeJson, EXIT } = require('./_common');
|
|
5
5
|
|
|
6
6
|
// ─── Validate ────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
|
-
function cmdValidate(dir, schemaOnly) {
|
|
8
|
+
function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
9
9
|
const abs = path.resolve(dir);
|
|
10
10
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
11
|
-
error(`Not a directory: ${abs}
|
|
11
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
|
|
@@ -87,6 +87,25 @@ function cmdValidate(dir, schemaOnly) {
|
|
|
87
87
|
errors.push(...crossResult.errors);
|
|
88
88
|
warnings.push(...crossResult.warnings);
|
|
89
89
|
|
|
90
|
+
const validCount = Object.keys(dataMap).filter((k) => dataMap[k]).length;
|
|
91
|
+
const schemaInfo = schemaOnly
|
|
92
|
+
? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
|
|
93
|
+
: '';
|
|
94
|
+
|
|
95
|
+
if (jsonMode) {
|
|
96
|
+
const result = {
|
|
97
|
+
path: abs,
|
|
98
|
+
valid: errors.length === 0,
|
|
99
|
+
files: validCount,
|
|
100
|
+
schemas_loaded: loadedSchemas.length,
|
|
101
|
+
schema_only: schemaOnly,
|
|
102
|
+
errors,
|
|
103
|
+
warnings,
|
|
104
|
+
};
|
|
105
|
+
console.log(JSON.stringify(result, null, 2));
|
|
106
|
+
process.exit(errors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
if (warnings.length) {
|
|
91
110
|
console.log('Warnings:');
|
|
92
111
|
warnings.forEach((w) => console.log(` - ${w}`));
|
|
@@ -94,13 +113,9 @@ function cmdValidate(dir, schemaOnly) {
|
|
|
94
113
|
if (errors.length) {
|
|
95
114
|
console.error('Errors:');
|
|
96
115
|
errors.forEach((e) => console.error(` - ${e}`));
|
|
97
|
-
process.exit(
|
|
116
|
+
process.exit(EXIT.VALIDATION_FAILED);
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
const validCount = Object.keys(dataMap).filter((k) => dataMap[k]).length;
|
|
101
|
-
const schemaInfo = schemaOnly
|
|
102
|
-
? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
|
|
103
|
-
: '';
|
|
104
119
|
console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
|
|
105
120
|
}
|
|
106
121
|
|
|
@@ -109,7 +124,7 @@ function cmdValidate(dir, schemaOnly) {
|
|
|
109
124
|
function cmdPack(dir, outputDir) {
|
|
110
125
|
const abs = path.resolve(dir);
|
|
111
126
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
112
|
-
error(`Not a directory: ${abs}
|
|
127
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
@@ -126,7 +141,7 @@ function cmdPack(dir, outputDir) {
|
|
|
126
141
|
.readdirSync(abs)
|
|
127
142
|
.filter((f) => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
128
143
|
manifest = {
|
|
129
|
-
kdna_spec: '0
|
|
144
|
+
kdna_spec: '1.0-rc',
|
|
130
145
|
name: domainName,
|
|
131
146
|
version: core.meta?.version || '0.1.0',
|
|
132
147
|
status: 'experimental',
|
|
@@ -300,17 +315,17 @@ function crc32(buf) {
|
|
|
300
315
|
function cmdUnpack(filePath, force) {
|
|
301
316
|
const abs = path.resolve(filePath);
|
|
302
317
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
303
|
-
error(`Not a file: ${abs}
|
|
318
|
+
error(`Not a file: ${abs}`, EXIT.INPUT_ERROR);
|
|
304
319
|
}
|
|
305
320
|
if (!abs.endsWith('.kdna')) {
|
|
306
|
-
error(`Not a .kdna file: ${abs}
|
|
321
|
+
error(`Not a .kdna file: ${abs}`, EXIT.INPUT_ERROR);
|
|
307
322
|
}
|
|
308
323
|
|
|
309
324
|
const domainName = path.basename(abs, '.kdna');
|
|
310
325
|
const outDir = path.join(path.dirname(abs), domainName);
|
|
311
326
|
|
|
312
327
|
if (fs.existsSync(outDir)) {
|
|
313
|
-
if (!force) error(`Directory already exists: ${outDir}\nUse --force to overwrite
|
|
328
|
+
if (!force) error(`Directory already exists: ${outDir}\nUse --force to overwrite.`, EXIT.INPUT_ERROR);
|
|
314
329
|
fs.rmSync(outDir, { recursive: true, force: true });
|
|
315
330
|
}
|
|
316
331
|
|
|
@@ -352,7 +367,7 @@ zf.close()
|
|
|
352
367
|
|
|
353
368
|
// ─── Inspect .kdna file (ZIP container or legacy merged JSON) ────────────
|
|
354
369
|
|
|
355
|
-
function inspectKdnaFile(filePath) {
|
|
370
|
+
function inspectKdnaFile(filePath, jsonMode = false) {
|
|
356
371
|
const abs = path.resolve(filePath);
|
|
357
372
|
fs.statSync(abs); // verify file exists
|
|
358
373
|
|
|
@@ -458,6 +473,33 @@ zf.close()
|
|
|
458
473
|
const c = core;
|
|
459
474
|
const p = patterns || {};
|
|
460
475
|
|
|
476
|
+
if (jsonMode) {
|
|
477
|
+
const result = {
|
|
478
|
+
name: m.name || c.meta?.domain || path.basename(abs, '.kdna'),
|
|
479
|
+
format: isZip ? 'kdna-zip' : 'legacy-merged',
|
|
480
|
+
spec: m.spec_version || m.kdna_spec || null,
|
|
481
|
+
version: m.version || null,
|
|
482
|
+
status: m.status || 'experimental',
|
|
483
|
+
access: m.access || 'open',
|
|
484
|
+
author: m.author?.name || null,
|
|
485
|
+
license: m.license?.type || null,
|
|
486
|
+
created: m.created || c.meta?.created || null,
|
|
487
|
+
description: m.description || c.meta?.purpose || null,
|
|
488
|
+
content: {
|
|
489
|
+
axioms: (c.axioms || []).length,
|
|
490
|
+
ontology: (c.ontology || []).length,
|
|
491
|
+
frameworks: (c.frameworks || []).length,
|
|
492
|
+
stances: (c.stances || []).length,
|
|
493
|
+
banned_terms: (p.terminology?.banned_terms || []).length,
|
|
494
|
+
misunderstandings: (p.misunderstandings || []).length,
|
|
495
|
+
self_checks: (p.self_check || []).length,
|
|
496
|
+
},
|
|
497
|
+
files: presentFiles,
|
|
498
|
+
};
|
|
499
|
+
console.log(JSON.stringify(result, null, 2));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
461
503
|
console.log('═'.repeat(50));
|
|
462
504
|
console.log(` ${m.name || c.meta?.domain || path.basename(abs, '.kdna')} — KDNA Domain`);
|
|
463
505
|
console.log('═'.repeat(50));
|
|
@@ -537,29 +579,78 @@ function parseSimpleYaml(raw) {
|
|
|
537
579
|
|
|
538
580
|
// ─── Inspect ───────────────────────────────────────────────────────────
|
|
539
581
|
|
|
540
|
-
function cmdInspect(dir) {
|
|
582
|
+
function cmdInspect(dir, jsonMode = false) {
|
|
541
583
|
const abs = path.resolve(dir);
|
|
542
584
|
const stat = fs.existsSync(abs) ? fs.statSync(abs) : null;
|
|
543
|
-
if (!stat) error(`Path not found: ${abs}
|
|
585
|
+
if (!stat) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
544
586
|
|
|
545
587
|
// Single .kdna file
|
|
546
588
|
if (stat.isFile() && abs.endsWith('.kdna')) {
|
|
547
|
-
inspectKdnaFile(abs);
|
|
589
|
+
inspectKdnaFile(abs, jsonMode);
|
|
548
590
|
return;
|
|
549
591
|
}
|
|
550
592
|
|
|
551
593
|
// Directory — existing logic
|
|
552
|
-
if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}
|
|
594
|
+
if (!stat.isDirectory()) error(`Not a KDNA domain: ${abs}`, EXIT.INPUT_ERROR);
|
|
553
595
|
|
|
554
596
|
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
555
597
|
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
556
598
|
|
|
557
599
|
if (!core) {
|
|
558
|
-
error(`Not a KDNA domain (KDNA_Core.json not found in ${abs})
|
|
600
|
+
error(`Not a KDNA domain (KDNA_Core.json not found in ${abs})`, EXIT.INPUT_ERROR);
|
|
559
601
|
}
|
|
560
602
|
|
|
561
603
|
const m = manifest || {};
|
|
562
604
|
const c = core;
|
|
605
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
606
|
+
const sce = readJson(path.join(abs, 'KDNA_Scenarios.json'));
|
|
607
|
+
const cas = readJson(path.join(abs, 'KDNA_Cases.json'));
|
|
608
|
+
const rea = readJson(path.join(abs, 'KDNA_Reasoning.json'));
|
|
609
|
+
const evo = readJson(path.join(abs, 'KDNA_Evolution.json'));
|
|
610
|
+
|
|
611
|
+
const expected = [
|
|
612
|
+
'KDNA_Core.json',
|
|
613
|
+
'KDNA_Patterns.json',
|
|
614
|
+
'KDNA_Scenarios.json',
|
|
615
|
+
'KDNA_Cases.json',
|
|
616
|
+
'KDNA_Reasoning.json',
|
|
617
|
+
'KDNA_Evolution.json',
|
|
618
|
+
];
|
|
619
|
+
const filesPresent = expected.filter((f) => fs.existsSync(path.join(abs, f)));
|
|
620
|
+
|
|
621
|
+
if (jsonMode) {
|
|
622
|
+
const result = {
|
|
623
|
+
name: m.name || c.meta?.domain || path.basename(abs),
|
|
624
|
+
version: m.version || c.meta?.version || null,
|
|
625
|
+
status: m.status || 'experimental',
|
|
626
|
+
access: m.access || 'open',
|
|
627
|
+
language: m.language || c.meta?.language || null,
|
|
628
|
+
author: m.author?.name || null,
|
|
629
|
+
author_id: m.author?.id || null,
|
|
630
|
+
license: m.license?.type || null,
|
|
631
|
+
created: c.meta?.created || null,
|
|
632
|
+
description: m.description || c.meta?.purpose || null,
|
|
633
|
+
files: filesPresent,
|
|
634
|
+
content: {
|
|
635
|
+
axioms: (c.axioms || []).length,
|
|
636
|
+
ontology: (c.ontology || []).length,
|
|
637
|
+
frameworks: (c.frameworks || []).length,
|
|
638
|
+
core_structures: (c.core_structure || []).length,
|
|
639
|
+
stances: (c.stances || []).length,
|
|
640
|
+
preferred_terms: (pat?.terminology?.preferred_terms || pat?.terminology?.standard_terms || []).length,
|
|
641
|
+
banned_terms: (pat?.terminology?.banned_terms || []).length,
|
|
642
|
+
misunderstandings: (pat?.misunderstandings || []).length,
|
|
643
|
+
self_checks: (pat?.self_check || []).length,
|
|
644
|
+
scenarios: sce ? (sce.scenes || []).length : 0,
|
|
645
|
+
cases: cas ? (cas.cases || []).length : 0,
|
|
646
|
+
reasoning_chains: rea ? (rea.reasoning_chains || []).length : 0,
|
|
647
|
+
evolution_stages: evo ? (evo.stages || []).length : 0,
|
|
648
|
+
},
|
|
649
|
+
axioms: (c.axioms || []).map((a) => a.one_sentence || null).filter(Boolean),
|
|
650
|
+
};
|
|
651
|
+
console.log(JSON.stringify(result, null, 2));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
563
654
|
|
|
564
655
|
console.log('═'.repeat(50));
|
|
565
656
|
console.log(` ${m.name || c.meta?.domain || path.basename(abs)} — KDNA Domain`);
|
|
@@ -576,15 +667,6 @@ function cmdInspect(dir) {
|
|
|
576
667
|
console.log(` Description: ${m.description || c.meta?.purpose || '?'}`);
|
|
577
668
|
console.log('');
|
|
578
669
|
|
|
579
|
-
const expected = [
|
|
580
|
-
'KDNA_Core.json',
|
|
581
|
-
'KDNA_Patterns.json',
|
|
582
|
-
'KDNA_Scenarios.json',
|
|
583
|
-
'KDNA_Cases.json',
|
|
584
|
-
'KDNA_Reasoning.json',
|
|
585
|
-
'KDNA_Evolution.json',
|
|
586
|
-
];
|
|
587
|
-
|
|
588
670
|
console.log(' ── File Set ──');
|
|
589
671
|
for (const f of expected) {
|
|
590
672
|
const exists = fs.existsSync(path.join(abs, f));
|
|
@@ -599,7 +681,6 @@ function cmdInspect(dir) {
|
|
|
599
681
|
console.log(` Core structures: ${(c.core_structure || []).length}`);
|
|
600
682
|
console.log(` Stances: ${(c.stances || []).length}`);
|
|
601
683
|
|
|
602
|
-
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
603
684
|
if (pat) {
|
|
604
685
|
const preferred = pat.terminology?.preferred_terms || pat.terminology?.standard_terms || [];
|
|
605
686
|
console.log(` Preferred terms: ${preferred.length}`);
|
|
@@ -608,16 +689,12 @@ function cmdInspect(dir) {
|
|
|
608
689
|
console.log(` Self-checks: ${(pat.self_check || []).length}`);
|
|
609
690
|
}
|
|
610
691
|
|
|
611
|
-
const sce = readJson(path.join(abs, 'KDNA_Scenarios.json'));
|
|
612
692
|
if (sce) console.log(` Scenarios: ${(sce.scenes || []).length}`);
|
|
613
693
|
|
|
614
|
-
const cas = readJson(path.join(abs, 'KDNA_Cases.json'));
|
|
615
694
|
if (cas) console.log(` Cases: ${(cas.cases || []).length}`);
|
|
616
695
|
|
|
617
|
-
const rea = readJson(path.join(abs, 'KDNA_Reasoning.json'));
|
|
618
696
|
if (rea) console.log(` Reasoning chains: ${(rea.reasoning_chains || []).length}`);
|
|
619
697
|
|
|
620
|
-
const evo = readJson(path.join(abs, 'KDNA_Evolution.json'));
|
|
621
698
|
if (evo) console.log(` Evolution stages: ${(evo.stages || []).length}`);
|
|
622
699
|
|
|
623
700
|
console.log('');
|