@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/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @aikdna/kdna-cli
|
|
2
2
|
|
|
3
|
-
KDNA CLI
|
|
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
|
-
#
|
|
17
|
-
kdna
|
|
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
|
-
|
|
20
|
-
kdna verify @aikdna/writing
|
|
27
|
+
## Commands by Role
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
kdna install @aikdna/writing
|
|
29
|
+
### Domain Authoring
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
kdna compare @aikdna/writing --input "..."
|
|
33
|
-
```
|
|
51
|
+
### Testing & Verification
|
|
34
52
|
|
|
35
|
-
|
|
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
|
|
40
|
-
| `kdna
|
|
41
|
-
| `kdna
|
|
42
|
-
| `kdna
|
|
43
|
-
| `kdna
|
|
44
|
-
| `kdna
|
|
45
|
-
| `kdna
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
| `kdna
|
|
52
|
-
| `kdna
|
|
53
|
-
| `kdna
|
|
54
|
-
| `kdna 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
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
|
-
|
|
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
|
-
|
|
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 };
|