@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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # @aikdna/kdna-cli
2
2
 
3
- KDNA CLI create, validate, install, and manage domain cognition packages for AI agents.
3
+ **KDNA CLI is the runtime control plane for loading, validating, composing, testing, and governing domain judgment for AI agents.**
4
+
5
+ KDNA CLI 是 AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
6
+
7
+ CLI 不是 Studio,不是 Chat,不是 Governance Console。它是这些产品共同依赖的底层协议接口。
4
8
 
5
9
  Part of the [KDNA](https://github.com/knowledge-dna/KDNA) ecosystem.
6
10
 
@@ -13,45 +17,119 @@ npm install -g @aikdna/kdna-cli
13
17
  ## Quick Start
14
18
 
15
19
  ```bash
16
- # Validate a domain
17
- kdna validate ./my-domain
20
+ kdna install @aikdna/writing # Install a domain
21
+ kdna verify @aikdna/writing # 3-layer verification
22
+ kdna available # List installed domains
23
+ kdna match "improve this post" # Find relevant domains
24
+ kdna load @aikdna/writing # Load for agent consumption
25
+ ```
18
26
 
19
- # Verify a domain (structure + trust + judgment)
20
- kdna verify @aikdna/writing
27
+ ## Commands by Role
21
28
 
22
- # Install a domain
23
- kdna install @aikdna/writing
29
+ ### Domain Authoring
24
30
 
25
- # Create a new domain
26
- kdna init my-domain
31
+ | Command | Description |
32
+ |---------|-------------|
33
+ | `kdna init <name>` | Scaffold a new domain from template |
34
+ | `kdna validate <path>` | Validate domain structure |
35
+ | `kdna validate --schema <path>` | Schema-only validation |
36
+ | `kdna pack <path>` | Pack into .kdna container |
37
+ | `kdna unpack <file>` | Unpack .kdna container |
38
+ | `kdna inspect <path>` | Inspect domain or .kdna file |
39
+ | `kdna publish <path>` | Pack + sign + publish to registry |
40
+ | `kdna publish --check <path>` | Quality gate check only |
41
+ | `kdna version bump <level> [path]` | Bump domain version |
42
+
43
+ ### Agent Runtime
27
44
 
28
- # Search the registry
29
- kdna search writing
45
+ | Command | Description |
46
+ |---------|-------------|
47
+ | `kdna available [--json]` | List installed domains with v2.1 fields |
48
+ | `kdna match "<task>" [--json]` | Signal matching — find relevant domains |
49
+ | `kdna load <name> [--as=prompt\|json\|raw]` | Emit domain in agent-ready format |
30
50
 
31
- # Compare with/without KDNA
32
- kdna compare @aikdna/writing --input "..."
33
- ```
51
+ ### Testing & Verification
34
52
 
35
- ## Commands
53
+ | Command | Description |
54
+ |---------|-------------|
55
+ | `kdna verify <name>` | 3-layer verification: structure + trust + judgment |
56
+ | `kdna compare <name> --input "..."` | With/without KDNA reasoning diff |
57
+ | `kdna diff <name>@<v1> <name>@<v2>` | Judgment-level diff between versions |
58
+ | `kdna doctor` | Check runtime environment health |
59
+
60
+ ### Cluster Composition
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `kdna cluster lint <path>` | Validate cluster manifest |
65
+
66
+ ### Registry & Distribution
36
67
 
37
68
  | Command | Description |
38
69
  |---------|-------------|
39
- | `kdna validate <dir>` | Validate domain structure |
40
- | `kdna verify <name>` | Full 3-layer verification (structure/trust/judgment) |
41
- | `kdna install <name>` | Install a domain from the registry |
42
- | `kdna remove <name>` | Remove an installed domain |
43
- | `kdna info <name>` | Show domain information |
44
- | `kdna inspect <dir\|file>` | Inspect a domain or .kdna file |
45
- | `kdna list` | List installed domains |
46
- | `kdna search <keyword>` | Search the registry |
47
- | `kdna init <name>` | Create a new domain from template |
48
- | `kdna pack <dir>` | Pack a domain into .kdna file |
49
- | `kdna unpack <file>` | Unpack a .kdna file |
50
- | `kdna publish <dir>` | Publish a domain to the registry |
51
- | `kdna compare <name>` | Compare AI output with/without KDNA |
52
- | `kdna diff <name@v1> <name@v2>` | Diff two domain versions |
53
- | `kdna project init` | Initialize .kdna/config.json for a project |
54
- | `kdna identity init` | Create a signing identity |
70
+ | `kdna install <name>` | Install domain from registry |
71
+ | `kdna remove <name>` | Uninstall a domain |
72
+ | `kdna update <name>` | Update installed domain |
73
+ | `kdna info <name>` | Show domain metadata and trust status |
74
+ | `kdna list [--available]` | List installed or available domains |
75
+ | `kdna search <keyword>` | Search registry |
76
+ | `kdna registry refresh` | Refresh registry cache |
77
+
78
+ ### Identity & Signing
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `kdna identity init` | Generate Ed25519 signing key |
83
+ | `kdna identity show` | Display public key and buyer ID |
84
+ | `kdna identity export [--out]` | Backup private key (encrypted) |
85
+ | `kdna identity import <file>` | Restore identity from backup |
86
+
87
+ ### Setup
88
+
89
+ | Command | Description |
90
+ |---------|-------------|
91
+ | `kdna setup` | One-command setup: CLI + skill + data root |
92
+
93
+ ## Exit Codes
94
+
95
+ | Code | Name | Meaning |
96
+ |------|------|---------|
97
+ | 0 | `OK` | Success |
98
+ | 1 | `VALIDATION_FAILED` | Structure or schema validation failed |
99
+ | 2 | `INPUT_ERROR` | Invalid input, missing argument, not found |
100
+ | 3 | `TRUST_FAILED` | Signature or trust verification failed |
101
+ | 4 | `JUDGMENT_QUALITY_FAILED` | Judgment governance fields missing or insufficient |
102
+ | 5 | `REGISTRY_ERROR` | Registry lookup or network error |
103
+ | 6 | `PROVIDER_ERROR` | LLM provider (API key, rate limit) error |
104
+ | 7 | `POLICY_VIOLATION` | Publishing or governance policy violation |
105
+ | 8 | `HUMAN_LOCK_REQUIRED` | Human lock required but not present |
106
+
107
+ ## JSON Output
108
+
109
+ Machine-consumable commands support `--json` for structured output:
110
+
111
+ ```bash
112
+ kdna verify @aikdna/writing --json
113
+ kdna available --json
114
+ kdna match "help me write" --json
115
+ kdna search writing --json
116
+ kdna info @aikdna/writing --json
117
+ kdna doctor --json
118
+ ```
119
+
120
+ ## Product Matrix
121
+
122
+ | Layer | Product | Responsibility |
123
+ |-------|---------|---------------|
124
+ | Protocol | KDNA SPEC | Define judgment asset format |
125
+ | Core Library | @aikdna/kdna-core | load / validate / compose / render |
126
+ | Runtime | @aikdna/kdna-cli | Agent runtime + compile + verify + test + publish |
127
+ | Authoring | KDNA Studio | Human-led judgment production |
128
+ | Consumption | KDNAChat | Load, use, compare |
129
+ | Governance | KDNA Governance Console | Approve, release, audit |
130
+ | Distribution | Registry | Discover, install, trade |
131
+
132
+ CLI 不应该成为一个"命令行 Studio",而是所有 KDNA 产品共同依赖的协议控制平面。
55
133
 
56
134
  ## Development
57
135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -327,6 +327,22 @@ function cmdLoad(input, args = []) {
327
327
  format = eq > 0 ? args[formatIdx].slice(eq + 1) : args[formatIdx + 1];
328
328
  }
329
329
 
330
+ // --profile=<name> for load profiles (Phase 2)
331
+ const profileIdx = args.findIndex((a) => a.startsWith('--profile'));
332
+ let profile = null;
333
+ let profileInput = null;
334
+ if (profileIdx >= 0) {
335
+ const eq = args[profileIdx].indexOf('=');
336
+ const raw = eq > 0 ? args[profileIdx].slice(eq + 1) : args[profileIdx + 1];
337
+ profile = raw || 'compact';
338
+ }
339
+
340
+ // --input for scenario profile
341
+ const inputIdx = args.indexOf('--input');
342
+ if (inputIdx >= 0) {
343
+ profileInput = args[inputIdx + 1] || null;
344
+ }
345
+
330
346
  const parsed = parseName(input);
331
347
  if (!parsed) {
332
348
  console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
@@ -347,11 +363,13 @@ function cmdLoad(input, args = []) {
347
363
  const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
348
364
  const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
349
365
 
366
+ // JSON format
350
367
  if (format === 'json') {
351
368
  process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
352
369
  return;
353
370
  }
354
371
 
372
+ // Raw format
355
373
  if (format === 'raw') {
356
374
  for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
357
375
  const p = path.join(dir, f);
@@ -363,8 +381,142 @@ function cmdLoad(input, args = []) {
363
381
  return;
364
382
  }
365
383
 
384
+ // Load profiles
385
+ if (profile) {
386
+ emitProfile(parsed, manifest, core, pat, profile, profileInput);
387
+ return;
388
+ }
389
+
366
390
  // Default: --as=prompt — compact text optimized for system-prompt injection.
367
- // Goal: minimum token cost while preserving all judgment surface.
391
+ emitCompact(parsed, manifest, core, pat);
392
+ }
393
+
394
+ // ─── Load profiles ─────────────────────────────────────────────────────
395
+
396
+ function emitProfile(parsed, manifest, core, pat, profile, input) {
397
+ const lines = [];
398
+ lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
399
+ if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
400
+ lines.push('');
401
+
402
+ const axioms = core.axioms || [];
403
+
404
+ switch (profile) {
405
+ case 'index':
406
+ // Minimal: name + axioms list + applies_when only
407
+ if (manifest.core_insight) lines.push(`# insight: ${manifest.core_insight}`);
408
+ lines.push('');
409
+ if (axioms.length) {
410
+ lines.push('## Axiom index');
411
+ for (const a of axioms) {
412
+ lines.push(`- ${a.one_sentence}`);
413
+ if (a.applies_when?.length) lines.push(` APPLIES: ${a.applies_when.join('; ')}`);
414
+ if (a.does_not_apply_when?.length) lines.push(` NOT: ${a.does_not_apply_when.join('; ')}`);
415
+ }
416
+ lines.push('');
417
+ }
418
+ break;
419
+
420
+ case 'scenario':
421
+ // Scenario-aware: include axioms whose applies_when matches the input
422
+ lines.push(`# Scenario input: ${(input || '').slice(0, 200)}`);
423
+ lines.push('');
424
+ if (axioms.length) {
425
+ const taskTokens = tokenize(input || '');
426
+ const relevant = axioms.filter((a) => {
427
+ if (!a.applies_when?.length) return false;
428
+ const combinedText = [...a.applies_when, a.one_sentence || '', a.full_statement || ''].join(' ');
429
+ const score = overlapScore(taskTokens, combinedText);
430
+ return score.hits >= 1 || score.coverage >= 0.1;
431
+ });
432
+ const selected = relevant.length > 0 ? relevant : axioms;
433
+ lines.push(`## Axioms (${selected.length}/${axioms.length} relevant)`);
434
+ for (const a of selected) {
435
+ lines.push(`- ${a.one_sentence}`);
436
+ if (a.applies_when?.length) {
437
+ lines.push(` APPLIES WHEN: ${a.applies_when.join('; ')}`);
438
+ }
439
+ if (a.failure_risk) lines.push(` RISK IF MISAPPLIED: ${a.failure_risk}`);
440
+ }
441
+ lines.push('');
442
+ }
443
+ break;
444
+
445
+ case 'full':
446
+ // Full: all axiom details including full_statement + why
447
+ if (axioms.length) {
448
+ lines.push('## Axioms (full)');
449
+ for (const a of axioms) {
450
+ lines.push(`### ${a.one_sentence}`);
451
+ if (a.full_statement) lines.push(`${a.full_statement}`);
452
+ if (a.why) lines.push(`Why: ${a.why}`);
453
+ if (a.applies_when?.length) {
454
+ lines.push(`Applies when: ${a.applies_when.join('; ')}`);
455
+ }
456
+ if (a.does_not_apply_when?.length) {
457
+ lines.push(`Does not apply when: ${a.does_not_apply_when.join('; ')}`);
458
+ }
459
+ if (a.failure_risk) lines.push(`Failure risk: ${a.failure_risk}`);
460
+ lines.push('');
461
+ }
462
+ }
463
+ break;
464
+
465
+ case 'compact':
466
+ default:
467
+ emitCompact(parsed, manifest, core, pat);
468
+ return;
469
+ }
470
+
471
+ // Add stances, misunderstandings, self-checks for all non-index profiles
472
+ if (profile !== 'index') {
473
+ if (core.stances?.length) {
474
+ lines.push('## Stances');
475
+ for (const s of core.stances) {
476
+ const text = typeof s === 'string' ? s : s.stance;
477
+ if (text) lines.push(`- ${text}`);
478
+ }
479
+ lines.push('');
480
+ }
481
+
482
+ if (pat.terminology?.banned_terms?.length) {
483
+ lines.push('## Banned terms');
484
+ for (const t of pat.terminology.banned_terms) {
485
+ const term = typeof t === 'string' ? t : t.term;
486
+ const replace = typeof t === 'object' ? t.replace_with : null;
487
+ lines.push(`- "${term}"${replace ? ` → use: ${replace}` : ''}`);
488
+ }
489
+ lines.push('');
490
+ }
491
+
492
+ if (pat.misunderstandings?.length) {
493
+ lines.push('## Misunderstandings to avoid');
494
+ for (const m of pat.misunderstandings) {
495
+ lines.push(`- WRONG: ${m.wrong}`);
496
+ lines.push(` CORRECT: ${m.correct}`);
497
+ if (m.failure_risk) lines.push(` RISK: ${m.failure_risk}`);
498
+ }
499
+ lines.push('');
500
+ }
501
+
502
+ if (pat.self_check?.length) {
503
+ lines.push('## Self-checks');
504
+ for (const q of pat.self_check) {
505
+ const text = typeof q === 'string' ? q : q.question;
506
+ if (text) lines.push(`- ${text}`);
507
+ }
508
+ lines.push('');
509
+ }
510
+ }
511
+
512
+ lines.push('---');
513
+ lines.push('Apply silently. Do not quote KDNA to the user.');
514
+ lines.push('User intent + evidence always override KDNA axioms.');
515
+
516
+ process.stdout.write(lines.join('\n') + '\n');
517
+ }
518
+
519
+ function emitCompact(parsed, manifest, core, pat) {
368
520
  const lines = [];
369
521
  lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
370
522
  if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
@@ -431,4 +583,260 @@ function cmdLoad(input, args = []) {
431
583
  process.stdout.write(lines.join('\n') + '\n');
432
584
  }
433
585
 
434
- module.exports = { cmdAvailable, cmdMatch, cmdLoad };
586
+ // ─── kdna select ───────────────────────────────────────────────────────
587
+
588
+ function cmdSelect(args = []) {
589
+ const wantJson = args.includes('--json');
590
+ const inputIdx = args.indexOf('--input');
591
+ const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
592
+ const maxIdx = args.indexOf('--max-domains');
593
+ const maxDomains = maxIdx >= 0 ? parseInt(args[maxIdx + 1], 10) || 3 : 3;
594
+
595
+ if (!input) {
596
+ if (wantJson) {
597
+ console.log(JSON.stringify({ error: 'Usage: kdna select --input "<task>" [--max-domains=N] [--json]' }));
598
+ process.exit(2);
599
+ }
600
+ console.error('Usage: kdna select --input "<task>" [--max-domains=N] [--json]');
601
+ process.exit(2);
602
+ }
603
+
604
+ const taskTokens = tokenize(input);
605
+ const installed = listInstalled();
606
+ const scores = [];
607
+
608
+ for (const e of installed) {
609
+ const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
610
+ if (manifest.yanked === true) continue;
611
+ const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
612
+
613
+ // Check does_not_apply_when hard exclusion
614
+ let excluded = false;
615
+ for (const a of core.axioms || []) {
616
+ for (const d of a.does_not_apply_when || []) {
617
+ if (overlapScore(taskTokens, d).hits >= 2) {
618
+ excluded = true;
619
+ break;
620
+ }
621
+ }
622
+ if (excluded) break;
623
+ }
624
+ if (excluded) continue;
625
+
626
+ // Score: applies_when matches + domain relevance
627
+ let score = 0;
628
+ const reasons = [];
629
+
630
+ for (const a of core.axioms || []) {
631
+ for (const ap of a.applies_when || []) {
632
+ const s = overlapScore(taskTokens, ap);
633
+ if (s.hits >= 2) {
634
+ score += s.hits * 3;
635
+ reasons.push({ source: `${a.id}.applies_when`, hits: s.hits, text: ap.slice(0, 120) });
636
+ }
637
+ }
638
+ }
639
+
640
+ score += domainRelevanceScore(taskTokens, manifest);
641
+
642
+ if (score > 0) {
643
+ scores.push({
644
+ domain: manifest.name || e.full,
645
+ version: manifest.version || null,
646
+ status: manifest.status || 'experimental',
647
+ score,
648
+ reasons: reasons.slice(0, 5),
649
+ });
650
+ }
651
+ }
652
+
653
+ // Sort descending, take top N
654
+ scores.sort((a, b) => b.score - a.score);
655
+ const selected = scores.slice(0, maxDomains);
656
+
657
+ if (wantJson) {
658
+ console.log(JSON.stringify({
659
+ input: input.slice(0, 200),
660
+ selected,
661
+ max_domains: maxDomains,
662
+ total_candidates: scores.length,
663
+ }, null, 2));
664
+ return;
665
+ }
666
+
667
+ if (!selected.length) {
668
+ console.log(`No domains selected for input: "${input.slice(0, 100)}"`);
669
+ console.log('Run: kdna match "<task>" for candidate hints');
670
+ console.log('Run: kdna list --available to see all registered domains');
671
+ return;
672
+ }
673
+
674
+ console.log(`Selected ${selected.length} domain(s) for: "${input.slice(0, 100)}"`);
675
+ console.log('');
676
+ for (const s of selected) {
677
+ console.log(` ${s.domain.padEnd(36)} score:${s.score} v${s.version || '?'} [${s.status}]`);
678
+ for (const r of s.reasons) {
679
+ console.log(` ↳ ${r.source} (${r.hits} hits): ${r.text}`);
680
+ }
681
+ }
682
+ }
683
+
684
+ // ─── kdna postvalidate ─────────────────────────────────────────────────
685
+
686
+ function cmdPostvalidate(args = []) {
687
+ const wantJson = args.includes('--json');
688
+ const positional = args.filter((a) => !a.startsWith('--'));
689
+ const input = positional[1];
690
+ const outputIdx = args.indexOf('--output');
691
+ const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null;
692
+
693
+ if (!input) {
694
+ console.error('Usage: kdna postvalidate <domain> --output <response-file> [--json]');
695
+ process.exit(2);
696
+ }
697
+
698
+ const parsed = parseName(input);
699
+ if (!parsed) {
700
+ console.error(`Invalid name "${input}".`);
701
+ process.exit(2);
702
+ }
703
+ const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
704
+ if (!fs.existsSync(dir)) {
705
+ console.error(`${parsed.full} is not installed.`);
706
+ process.exit(2);
707
+ }
708
+
709
+ const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
710
+ const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
711
+
712
+ // Read agent output
713
+ let agentOutput = '';
714
+ if (outputFile) {
715
+ try {
716
+ agentOutput = fs.readFileSync(outputFile, 'utf8');
717
+ } catch {
718
+ console.error(`Cannot read output file: ${outputFile}`);
719
+ process.exit(2);
720
+ }
721
+ } else {
722
+ // Read from stdin
723
+ try {
724
+ agentOutput = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
725
+ } catch {
726
+ // ignore
727
+ }
728
+ }
729
+
730
+ const results = {
731
+ violations: [],
732
+ warnings: [],
733
+ passed: [],
734
+ };
735
+
736
+ // Check banned terms
737
+ const bannedTerms = (pat.terminology?.banned_terms || []).map((t) =>
738
+ typeof t === 'string' ? t : t.term,
739
+ );
740
+ for (const term of bannedTerms) {
741
+ const regex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
742
+ if (regex.test(agentOutput)) {
743
+ results.violations.push(`banned term used: "${term}"`);
744
+ } else {
745
+ results.passed.push(`banned term avoided: "${term}"`);
746
+ }
747
+ }
748
+
749
+ // Check misunderstandings (wrong patterns)
750
+ const misunderstandings = pat.misunderstandings || [];
751
+ for (const ms of misunderstandings) {
752
+ const wrongTokens = tokenize(ms.wrong || '');
753
+ const agentTokens = tokenize(agentOutput);
754
+ const overlap = wrongTokens.filter((t) => agentTokens.includes(t));
755
+ if (overlap.length >= 3) {
756
+ results.warnings.push(`possible misunderstanding: "${ms.wrong?.slice(0, 80)}"`);
757
+ } else {
758
+ results.passed.push(`misunderstanding avoided: "${(ms.wrong || '').slice(0, 60)}"`);
759
+ }
760
+ }
761
+
762
+ // Check self-checks absence (can't verify answers, but flag missing checks)
763
+ const selfChecks = pat.self_check || [];
764
+ if (selfChecks.length > 0) {
765
+ let foundChecks = 0;
766
+ for (const sc of selfChecks) {
767
+ const text = typeof sc === 'string' ? sc : sc.question;
768
+ if (text) {
769
+ const keywords = tokenize(text).filter((t) => t.length > 3).slice(0, 3);
770
+ const found = keywords.some((k) => agentOutput.toLowerCase().includes(k));
771
+ if (found) foundChecks++;
772
+ }
773
+ }
774
+ if (foundChecks === 0) {
775
+ results.warnings.push('no self-check traces found in output');
776
+ } else {
777
+ results.passed.push(`self-check traces: ${foundChecks}/${selfChecks.length}`);
778
+ }
779
+ }
780
+
781
+ // Check boundary violations
782
+ const boundaries = core.axioms || [];
783
+ let boundaryViolations = 0;
784
+ for (const ax of boundaries) {
785
+ for (const notApply of ax.does_not_apply_when || []) {
786
+ if (overlapScore(tokenize(agentOutput), notApply).hits >= 2) {
787
+ boundaryViolations++;
788
+ results.violations.push(`boundary violation: ${ax.id} (should not apply when "${notApply.slice(0, 80)}")`);
789
+ break;
790
+ }
791
+ }
792
+ }
793
+ if (boundaryViolations === 0) {
794
+ results.passed.push('no boundary violations detected');
795
+ }
796
+
797
+ // Risk flags
798
+ for (const ax of core.axioms || []) {
799
+ if (ax.failure_risk) {
800
+ const riskTokens = tokenize(ax.failure_risk);
801
+ const match = riskTokens.filter((t) => tokenize(agentOutput).includes(t)).length;
802
+ if (match >= riskTokens.length * 0.5) {
803
+ results.warnings.push(`failure risk matched: ${ax.id} — "${ax.failure_risk.slice(0, 80)}"`);
804
+ }
805
+ }
806
+ }
807
+
808
+ if (wantJson) {
809
+ console.log(JSON.stringify({
810
+ domain: parsed.full,
811
+ violations: results.violations.length,
812
+ warnings: results.warnings.length,
813
+ passed: results.passed.length,
814
+ details: results,
815
+ }, null, 2));
816
+ process.exit(results.violations.length ? 1 : 0);
817
+ }
818
+
819
+ console.log(`Post-validation: ${parsed.full}`);
820
+ console.log('');
821
+ console.log(` Violations: ${results.violations.length} Warnings: ${results.warnings.length} Passed: ${results.passed.length}`);
822
+ console.log('');
823
+
824
+ if (results.violations.length) {
825
+ console.log('Violations:');
826
+ results.violations.forEach((v) => console.log(` ✗ ${v}`));
827
+ console.log('');
828
+ }
829
+ if (results.warnings.length) {
830
+ console.log('Warnings:');
831
+ results.warnings.forEach((w) => console.log(` ⚠ ${w}`));
832
+ console.log('');
833
+ }
834
+ if (results.passed.length) {
835
+ console.log('Passed:');
836
+ results.passed.forEach((p) => console.log(` ✓ ${p}`));
837
+ }
838
+
839
+ process.exit(results.violations.length ? 1 : 0);
840
+ }
841
+
842
+ module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate };