@aikdna/kdna-cli 0.9.0 → 0.12.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/SECURITY.md +41 -0
- package/package.json +3 -2
- package/src/agent.js +430 -2
- package/src/cli.js +280 -38
- package/src/cmds/_common.js +68 -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 +233 -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/cmds/trace.js +225 -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 +39 -3
- package/src/search.js +33 -2
- package/src/verify.js +98 -24
- 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/SECURITY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 0.11.x | :white_check_mark: |
|
|
8
|
+
| 0.10.x | :white_check_mark: |
|
|
9
|
+
| < 0.10 | :x: |
|
|
10
|
+
|
|
11
|
+
## Reporting a Vulnerability
|
|
12
|
+
|
|
13
|
+
KDNA CLI is the runtime control plane for domain judgment. The primary
|
|
14
|
+
security surface is signature verification, identity key management,
|
|
15
|
+
and registry trust.
|
|
16
|
+
|
|
17
|
+
If you discover a security vulnerability:
|
|
18
|
+
|
|
19
|
+
1. **Do not** open a public issue.
|
|
20
|
+
2. Report by email to security@aikdna.com or via GitHub private vulnerability reporting.
|
|
21
|
+
3. Include: affected version, steps to reproduce, potential impact.
|
|
22
|
+
|
|
23
|
+
We will acknowledge within 5 business days and provide a timeline for a fix.
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
- `kdna verify --trust`: Ed25519 signature verification
|
|
28
|
+
- `kdna identity init/export/import`: key generation and backup encryption
|
|
29
|
+
- `kdna install`: registry trust chain and SHA-256 verification
|
|
30
|
+
- `kdna publish`: signing and key material handling
|
|
31
|
+
|
|
32
|
+
## Out of Scope
|
|
33
|
+
|
|
34
|
+
- Domain content files (KDNA_*.json) — these are user-authored judgment assets
|
|
35
|
+
- Network-level attacks (man-in-the-middle on registry fetch) — use HTTPS
|
|
36
|
+
- Local filesystem access — CLI runs with user privileges
|
|
37
|
+
|
|
38
|
+
## Supply Chain
|
|
39
|
+
|
|
40
|
+
KDNA CLI publishes to npm as `@aikdna/kdna-cli`. Builds are reproducible
|
|
41
|
+
from source. Dependencies are pinned in `package-lock.json`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"templates/",
|
|
15
15
|
"skills/",
|
|
16
16
|
"LICENSE",
|
|
17
|
-
"NOTICE"
|
|
17
|
+
"NOTICE",
|
|
18
|
+
"SECURITY.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"lint": "eslint src/ validators/ tests/",
|
package/src/agent.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const { parseName } = require('./registry');
|
|
35
|
+
const { recordTrace } = require('./cmds/trace');
|
|
35
36
|
|
|
36
37
|
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
37
38
|
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
@@ -327,6 +328,22 @@ function cmdLoad(input, args = []) {
|
|
|
327
328
|
format = eq > 0 ? args[formatIdx].slice(eq + 1) : args[formatIdx + 1];
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
// --profile=<name> for load profiles (Phase 2)
|
|
332
|
+
const profileIdx = args.findIndex((a) => a.startsWith('--profile'));
|
|
333
|
+
let profile = null;
|
|
334
|
+
let profileInput = null;
|
|
335
|
+
if (profileIdx >= 0) {
|
|
336
|
+
const eq = args[profileIdx].indexOf('=');
|
|
337
|
+
const raw = eq > 0 ? args[profileIdx].slice(eq + 1) : args[profileIdx + 1];
|
|
338
|
+
profile = raw || 'compact';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --input for scenario profile
|
|
342
|
+
const inputIdx = args.indexOf('--input');
|
|
343
|
+
if (inputIdx >= 0) {
|
|
344
|
+
profileInput = args[inputIdx + 1] || null;
|
|
345
|
+
}
|
|
346
|
+
|
|
330
347
|
const parsed = parseName(input);
|
|
331
348
|
if (!parsed) {
|
|
332
349
|
console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
|
|
@@ -347,11 +364,14 @@ function cmdLoad(input, args = []) {
|
|
|
347
364
|
const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
|
|
348
365
|
const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
|
|
349
366
|
|
|
367
|
+
// JSON format
|
|
350
368
|
if (format === 'json') {
|
|
351
369
|
process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
|
|
370
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'json' });
|
|
352
371
|
return;
|
|
353
372
|
}
|
|
354
373
|
|
|
374
|
+
// Raw format
|
|
355
375
|
if (format === 'raw') {
|
|
356
376
|
for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
|
|
357
377
|
const p = path.join(dir, f);
|
|
@@ -360,11 +380,148 @@ function cmdLoad(input, args = []) {
|
|
|
360
380
|
process.stdout.write(fs.readFileSync(p, 'utf8'));
|
|
361
381
|
}
|
|
362
382
|
}
|
|
383
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'raw' });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Load profiles
|
|
388
|
+
if (profile) {
|
|
389
|
+
emitProfile(parsed, manifest, core, pat, profile, profileInput);
|
|
390
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: `profile:${profile}` });
|
|
363
391
|
return;
|
|
364
392
|
}
|
|
365
393
|
|
|
366
394
|
// Default: --as=prompt — compact text optimized for system-prompt injection.
|
|
367
|
-
|
|
395
|
+
emitCompact(parsed, manifest, core, pat);
|
|
396
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'prompt' });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Load profiles ─────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function emitProfile(parsed, manifest, core, pat, profile, input) {
|
|
402
|
+
const lines = [];
|
|
403
|
+
lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
|
|
404
|
+
if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
|
|
405
|
+
lines.push('');
|
|
406
|
+
|
|
407
|
+
const axioms = core.axioms || [];
|
|
408
|
+
|
|
409
|
+
switch (profile) {
|
|
410
|
+
case 'index':
|
|
411
|
+
// Minimal: name + axioms list + applies_when only
|
|
412
|
+
if (manifest.core_insight) lines.push(`# insight: ${manifest.core_insight}`);
|
|
413
|
+
lines.push('');
|
|
414
|
+
if (axioms.length) {
|
|
415
|
+
lines.push('## Axiom index');
|
|
416
|
+
for (const a of axioms) {
|
|
417
|
+
lines.push(`- ${a.one_sentence}`);
|
|
418
|
+
if (a.applies_when?.length) lines.push(` APPLIES: ${a.applies_when.join('; ')}`);
|
|
419
|
+
if (a.does_not_apply_when?.length) lines.push(` NOT: ${a.does_not_apply_when.join('; ')}`);
|
|
420
|
+
}
|
|
421
|
+
lines.push('');
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'scenario':
|
|
426
|
+
// Scenario-aware: include axioms whose applies_when matches the input
|
|
427
|
+
lines.push(`# Scenario input: ${(input || '').slice(0, 200)}`);
|
|
428
|
+
lines.push('');
|
|
429
|
+
if (axioms.length) {
|
|
430
|
+
const taskTokens = tokenize(input || '');
|
|
431
|
+
const relevant = axioms.filter((a) => {
|
|
432
|
+
if (!a.applies_when?.length) return false;
|
|
433
|
+
const combinedText = [...a.applies_when, a.one_sentence || '', a.full_statement || ''].join(' ');
|
|
434
|
+
const score = overlapScore(taskTokens, combinedText);
|
|
435
|
+
return score.hits >= 1 || score.coverage >= 0.1;
|
|
436
|
+
});
|
|
437
|
+
const selected = relevant.length > 0 ? relevant : axioms;
|
|
438
|
+
lines.push(`## Axioms (${selected.length}/${axioms.length} relevant)`);
|
|
439
|
+
for (const a of selected) {
|
|
440
|
+
lines.push(`- ${a.one_sentence}`);
|
|
441
|
+
if (a.applies_when?.length) {
|
|
442
|
+
lines.push(` APPLIES WHEN: ${a.applies_when.join('; ')}`);
|
|
443
|
+
}
|
|
444
|
+
if (a.failure_risk) lines.push(` RISK IF MISAPPLIED: ${a.failure_risk}`);
|
|
445
|
+
}
|
|
446
|
+
lines.push('');
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case 'full':
|
|
451
|
+
// Full: all axiom details including full_statement + why
|
|
452
|
+
if (axioms.length) {
|
|
453
|
+
lines.push('## Axioms (full)');
|
|
454
|
+
for (const a of axioms) {
|
|
455
|
+
lines.push(`### ${a.one_sentence}`);
|
|
456
|
+
if (a.full_statement) lines.push(`${a.full_statement}`);
|
|
457
|
+
if (a.why) lines.push(`Why: ${a.why}`);
|
|
458
|
+
if (a.applies_when?.length) {
|
|
459
|
+
lines.push(`Applies when: ${a.applies_when.join('; ')}`);
|
|
460
|
+
}
|
|
461
|
+
if (a.does_not_apply_when?.length) {
|
|
462
|
+
lines.push(`Does not apply when: ${a.does_not_apply_when.join('; ')}`);
|
|
463
|
+
}
|
|
464
|
+
if (a.failure_risk) lines.push(`Failure risk: ${a.failure_risk}`);
|
|
465
|
+
lines.push('');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case 'compact':
|
|
471
|
+
default:
|
|
472
|
+
emitCompact(parsed, manifest, core, pat);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Add stances, misunderstandings, self-checks for all non-index profiles
|
|
477
|
+
if (profile !== 'index') {
|
|
478
|
+
if (core.stances?.length) {
|
|
479
|
+
lines.push('## Stances');
|
|
480
|
+
for (const s of core.stances) {
|
|
481
|
+
const text = typeof s === 'string' ? s : s.stance;
|
|
482
|
+
if (text) lines.push(`- ${text}`);
|
|
483
|
+
}
|
|
484
|
+
lines.push('');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (pat.terminology?.banned_terms?.length) {
|
|
488
|
+
lines.push('## Banned terms');
|
|
489
|
+
for (const t of pat.terminology.banned_terms) {
|
|
490
|
+
const term = typeof t === 'string' ? t : t.term;
|
|
491
|
+
const replace = typeof t === 'object' ? t.replace_with : null;
|
|
492
|
+
lines.push(`- "${term}"${replace ? ` → use: ${replace}` : ''}`);
|
|
493
|
+
}
|
|
494
|
+
lines.push('');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (pat.misunderstandings?.length) {
|
|
498
|
+
lines.push('## Misunderstandings to avoid');
|
|
499
|
+
for (const m of pat.misunderstandings) {
|
|
500
|
+
lines.push(`- WRONG: ${m.wrong}`);
|
|
501
|
+
lines.push(` CORRECT: ${m.correct}`);
|
|
502
|
+
if (m.failure_risk) lines.push(` RISK: ${m.failure_risk}`);
|
|
503
|
+
}
|
|
504
|
+
lines.push('');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (pat.self_check?.length) {
|
|
508
|
+
lines.push('## Self-checks');
|
|
509
|
+
for (const q of pat.self_check) {
|
|
510
|
+
const text = typeof q === 'string' ? q : q.question;
|
|
511
|
+
if (text) lines.push(`- ${text}`);
|
|
512
|
+
}
|
|
513
|
+
lines.push('');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
lines.push('---');
|
|
518
|
+
lines.push('Apply silently. Do not quote KDNA to the user.');
|
|
519
|
+
lines.push('User intent + evidence always override KDNA axioms.');
|
|
520
|
+
|
|
521
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function emitCompact(parsed, manifest, core, pat) {
|
|
368
525
|
const lines = [];
|
|
369
526
|
lines.push(`# KDNA loaded: ${manifest.name || parsed.full}`);
|
|
370
527
|
if (manifest.judgment_version) lines.push(`# judgment_version: ${manifest.judgment_version}`);
|
|
@@ -431,4 +588,275 @@ function cmdLoad(input, args = []) {
|
|
|
431
588
|
process.stdout.write(lines.join('\n') + '\n');
|
|
432
589
|
}
|
|
433
590
|
|
|
434
|
-
|
|
591
|
+
// ─── kdna select ───────────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
function cmdSelect(args = []) {
|
|
594
|
+
const wantJson = args.includes('--json');
|
|
595
|
+
const inputIdx = args.indexOf('--input');
|
|
596
|
+
const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
|
|
597
|
+
const maxIdx = args.indexOf('--max-domains');
|
|
598
|
+
const maxDomains = maxIdx >= 0 ? parseInt(args[maxIdx + 1], 10) || 3 : 3;
|
|
599
|
+
|
|
600
|
+
if (!input) {
|
|
601
|
+
if (wantJson) {
|
|
602
|
+
console.log(JSON.stringify({ error: 'Usage: kdna select --input "<task>" [--max-domains=N] [--json]' }));
|
|
603
|
+
process.exit(2);
|
|
604
|
+
}
|
|
605
|
+
console.error('Usage: kdna select --input "<task>" [--max-domains=N] [--json]');
|
|
606
|
+
process.exit(2);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const taskTokens = tokenize(input);
|
|
610
|
+
const installed = listInstalled();
|
|
611
|
+
const scores = [];
|
|
612
|
+
|
|
613
|
+
for (const e of installed) {
|
|
614
|
+
const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
|
|
615
|
+
if (manifest.yanked === true) continue;
|
|
616
|
+
const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
|
|
617
|
+
|
|
618
|
+
// Check does_not_apply_when hard exclusion
|
|
619
|
+
let excluded = false;
|
|
620
|
+
for (const a of core.axioms || []) {
|
|
621
|
+
for (const d of a.does_not_apply_when || []) {
|
|
622
|
+
if (overlapScore(taskTokens, d).hits >= 2) {
|
|
623
|
+
excluded = true;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (excluded) break;
|
|
628
|
+
}
|
|
629
|
+
if (excluded) continue;
|
|
630
|
+
|
|
631
|
+
// Score: applies_when matches + domain relevance
|
|
632
|
+
let score = 0;
|
|
633
|
+
const reasons = [];
|
|
634
|
+
|
|
635
|
+
for (const a of core.axioms || []) {
|
|
636
|
+
for (const ap of a.applies_when || []) {
|
|
637
|
+
const s = overlapScore(taskTokens, ap);
|
|
638
|
+
if (s.hits >= 2) {
|
|
639
|
+
score += s.hits * 3;
|
|
640
|
+
reasons.push({ source: `${a.id}.applies_when`, hits: s.hits, text: ap.slice(0, 120) });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
score += domainRelevanceScore(taskTokens, manifest);
|
|
646
|
+
|
|
647
|
+
if (score > 0) {
|
|
648
|
+
scores.push({
|
|
649
|
+
domain: manifest.name || e.full,
|
|
650
|
+
version: manifest.version || null,
|
|
651
|
+
status: manifest.status || 'experimental',
|
|
652
|
+
score,
|
|
653
|
+
reasons: reasons.slice(0, 5),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Sort descending, take top N
|
|
659
|
+
scores.sort((a, b) => b.score - a.score);
|
|
660
|
+
const selected = scores.slice(0, maxDomains);
|
|
661
|
+
|
|
662
|
+
if (wantJson) {
|
|
663
|
+
console.log(JSON.stringify({
|
|
664
|
+
input: input.slice(0, 200),
|
|
665
|
+
selected,
|
|
666
|
+
max_domains: maxDomains,
|
|
667
|
+
total_candidates: scores.length,
|
|
668
|
+
}, null, 2));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!selected.length) {
|
|
673
|
+
console.log(`No domains selected for input: "${input.slice(0, 100)}"`);
|
|
674
|
+
console.log('Run: kdna match "<task>" for candidate hints');
|
|
675
|
+
console.log('Run: kdna list --available to see all registered domains');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
console.log(`Selected ${selected.length} domain(s) for: "${input.slice(0, 100)}"`);
|
|
680
|
+
console.log('');
|
|
681
|
+
for (const s of selected) {
|
|
682
|
+
console.log(` ${s.domain.padEnd(36)} score:${s.score} v${s.version || '?'} [${s.status}]`);
|
|
683
|
+
for (const r of s.reasons) {
|
|
684
|
+
console.log(` ↳ ${r.source} (${r.hits} hits): ${r.text}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ─── kdna postvalidate ─────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
function cmdPostvalidate(args = []) {
|
|
692
|
+
const wantJson = args.includes('--json');
|
|
693
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
694
|
+
const input = positional[1];
|
|
695
|
+
const outputIdx = args.indexOf('--output');
|
|
696
|
+
const outputFile = outputIdx >= 0 ? args[outputIdx + 1] : null;
|
|
697
|
+
|
|
698
|
+
if (!input) {
|
|
699
|
+
console.error('Usage: kdna postvalidate <domain> --output <response-file> [--json]');
|
|
700
|
+
process.exit(2);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const parsed = parseName(input);
|
|
704
|
+
if (!parsed) {
|
|
705
|
+
console.error(`Invalid name "${input}".`);
|
|
706
|
+
process.exit(2);
|
|
707
|
+
}
|
|
708
|
+
const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
|
|
709
|
+
if (!fs.existsSync(dir)) {
|
|
710
|
+
console.error(`${parsed.full} is not installed.`);
|
|
711
|
+
process.exit(2);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
|
|
715
|
+
const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
|
|
716
|
+
|
|
717
|
+
// Read agent output
|
|
718
|
+
let agentOutput = '';
|
|
719
|
+
if (outputFile) {
|
|
720
|
+
try {
|
|
721
|
+
agentOutput = fs.readFileSync(outputFile, 'utf8');
|
|
722
|
+
} catch {
|
|
723
|
+
console.error(`Cannot read output file: ${outputFile}`);
|
|
724
|
+
process.exit(2);
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
// Read from stdin
|
|
728
|
+
try {
|
|
729
|
+
agentOutput = fs.readFileSync(0, 'utf8'); // fd 0 = stdin
|
|
730
|
+
} catch {
|
|
731
|
+
// ignore
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const results = {
|
|
736
|
+
violations: [],
|
|
737
|
+
warnings: [],
|
|
738
|
+
passed: [],
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// Check banned terms
|
|
742
|
+
const bannedTerms = (pat.terminology?.banned_terms || []).map((t) =>
|
|
743
|
+
typeof t === 'string' ? t : t.term,
|
|
744
|
+
);
|
|
745
|
+
for (const term of bannedTerms) {
|
|
746
|
+
const regex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
747
|
+
if (regex.test(agentOutput)) {
|
|
748
|
+
results.violations.push(`banned term used: "${term}"`);
|
|
749
|
+
} else {
|
|
750
|
+
results.passed.push(`banned term avoided: "${term}"`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Check misunderstandings (wrong patterns)
|
|
755
|
+
const misunderstandings = pat.misunderstandings || [];
|
|
756
|
+
for (const ms of misunderstandings) {
|
|
757
|
+
const wrongTokens = tokenize(ms.wrong || '');
|
|
758
|
+
const agentTokens = tokenize(agentOutput);
|
|
759
|
+
const overlap = wrongTokens.filter((t) => agentTokens.includes(t));
|
|
760
|
+
if (overlap.length >= 3) {
|
|
761
|
+
results.warnings.push(`possible misunderstanding: "${ms.wrong?.slice(0, 80)}"`);
|
|
762
|
+
} else {
|
|
763
|
+
results.passed.push(`misunderstanding avoided: "${(ms.wrong || '').slice(0, 60)}"`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Check self-checks absence (can't verify answers, but flag missing checks)
|
|
768
|
+
const selfChecks = pat.self_check || [];
|
|
769
|
+
if (selfChecks.length > 0) {
|
|
770
|
+
let foundChecks = 0;
|
|
771
|
+
for (const sc of selfChecks) {
|
|
772
|
+
const text = typeof sc === 'string' ? sc : sc.question;
|
|
773
|
+
if (text) {
|
|
774
|
+
const keywords = tokenize(text).filter((t) => t.length > 3).slice(0, 3);
|
|
775
|
+
const found = keywords.some((k) => agentOutput.toLowerCase().includes(k));
|
|
776
|
+
if (found) foundChecks++;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (foundChecks === 0) {
|
|
780
|
+
results.warnings.push('no self-check traces found in output');
|
|
781
|
+
} else {
|
|
782
|
+
results.passed.push(`self-check traces: ${foundChecks}/${selfChecks.length}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Check boundary violations
|
|
787
|
+
const boundaries = core.axioms || [];
|
|
788
|
+
let boundaryViolations = 0;
|
|
789
|
+
for (const ax of boundaries) {
|
|
790
|
+
for (const notApply of ax.does_not_apply_when || []) {
|
|
791
|
+
if (overlapScore(tokenize(agentOutput), notApply).hits >= 2) {
|
|
792
|
+
boundaryViolations++;
|
|
793
|
+
results.violations.push(`boundary violation: ${ax.id} (should not apply when "${notApply.slice(0, 80)}")`);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (boundaryViolations === 0) {
|
|
799
|
+
results.passed.push('no boundary violations detected');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Risk flags
|
|
803
|
+
for (const ax of core.axioms || []) {
|
|
804
|
+
if (ax.failure_risk) {
|
|
805
|
+
const riskTokens = tokenize(ax.failure_risk);
|
|
806
|
+
const match = riskTokens.filter((t) => tokenize(agentOutput).includes(t)).length;
|
|
807
|
+
if (match >= riskTokens.length * 0.5) {
|
|
808
|
+
results.warnings.push(`failure risk matched: ${ax.id} — "${ax.failure_risk.slice(0, 80)}"`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (wantJson) {
|
|
814
|
+
const result = {
|
|
815
|
+
domain: parsed.full,
|
|
816
|
+
violations: results.violations.length,
|
|
817
|
+
warnings: results.warnings.length,
|
|
818
|
+
passed: results.passed.length,
|
|
819
|
+
details: results,
|
|
820
|
+
};
|
|
821
|
+
console.log(JSON.stringify(result, null, 2));
|
|
822
|
+
recordTrace({
|
|
823
|
+
timestamp: new Date().toISOString(),
|
|
824
|
+
agent: 'cli',
|
|
825
|
+
domain: parsed.full,
|
|
826
|
+
type: 'postvalidate',
|
|
827
|
+
postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
|
|
828
|
+
});
|
|
829
|
+
process.exit(results.violations.length ? 1 : 0);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
console.log(`Post-validation: ${parsed.full}`);
|
|
833
|
+
console.log('');
|
|
834
|
+
console.log(` Violations: ${results.violations.length} Warnings: ${results.warnings.length} Passed: ${results.passed.length}`);
|
|
835
|
+
console.log('');
|
|
836
|
+
|
|
837
|
+
if (results.violations.length) {
|
|
838
|
+
console.log('Violations:');
|
|
839
|
+
results.violations.forEach((v) => console.log(` ✗ ${v}`));
|
|
840
|
+
console.log('');
|
|
841
|
+
}
|
|
842
|
+
if (results.warnings.length) {
|
|
843
|
+
console.log('Warnings:');
|
|
844
|
+
results.warnings.forEach((w) => console.log(` ⚠ ${w}`));
|
|
845
|
+
console.log('');
|
|
846
|
+
}
|
|
847
|
+
if (results.passed.length) {
|
|
848
|
+
console.log('Passed:');
|
|
849
|
+
results.passed.forEach((p) => console.log(` ✓ ${p}`));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
recordTrace({
|
|
853
|
+
timestamp: new Date().toISOString(),
|
|
854
|
+
agent: 'cli',
|
|
855
|
+
domain: parsed.full,
|
|
856
|
+
type: 'postvalidate',
|
|
857
|
+
postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
|
|
858
|
+
});
|
|
859
|
+
process.exit(results.violations.length ? 1 : 0);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate };
|