@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.
@@ -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, format = 'human') {
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 };
@@ -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(1);
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.4',
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('');