@aikdna/kdna-cli 0.16.9 → 0.17.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 +138 -74
- package/package.json +3 -4
- package/src/agent.js +376 -2
- package/src/cli.js +8 -2
- package/src/cmds/domain.js +137 -44
- package/src/cmds/quality.js +12 -0
- package/src/publish.js +113 -2
- package/src/verify.js +45 -0
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# @aikdna/kdna-cli
|
|
2
2
|
|
|
3
|
-
**KDNA CLI is the
|
|
3
|
+
**KDNA CLI is the official open-source reference implementation for KDNA validation, loading, packaging, comparison, registry access, and agent-facing runtime workflows.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It is the runtime control plane for loading, validating, composing, testing, and governing domain judgment for AI agents.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
KDNA CLI 是 KDNA 验证、加载、打包、比较、注册表访问和 Agent 运行时工作流的官方开源参考实现,也是 AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
|
|
8
|
+
|
|
9
|
+
The CLI is how a KDNA cognitive kernel becomes usable by agents. It installs KDNA domains, verifies their structure and trust metadata, loads them into agent-readable form, compares judgment paths with and without KDNA, and records traces for audit.
|
|
10
|
+
|
|
11
|
+
KDNA CLI 让一个认知内核真正被 Agent 使用。它负责安装 KDNA、验证结构与信任信息、把 KDNA 转换成 Agent 可加载的形式、对比加载前后的判断路径,并记录可审计的使用痕迹。
|
|
12
|
+
|
|
13
|
+
Part of the [KDNA](https://github.com/aikdna/kdna) ecosystem.
|
|
8
14
|
|
|
9
15
|
## Install
|
|
10
16
|
|
|
@@ -28,99 +34,157 @@ kdna doctor --agents
|
|
|
28
34
|
|
|
29
35
|
### Domain Authoring
|
|
30
36
|
|
|
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 pack <path> --encrypt --license <file>` | Pack encrypted .kdnae container |
|
|
38
|
-
| `kdna unpack <file>` | Unpack .kdna or .kdnae container |
|
|
39
|
-
| `kdna inspect <path>` | Inspect domain or .kdna file |
|
|
40
|
-
| `kdna publish <path>` | Pack + sign + publish to registry |
|
|
41
|
-
| `kdna publish --check <path>` | Quality gate check only |
|
|
42
|
-
| `kdna version bump <level> [path]` | Bump domain version |
|
|
37
|
+
| Command | Status | Description |
|
|
38
|
+
|---------|--------|-------------|
|
|
39
|
+
| `kdna init <name>` | Beta | Scaffold a new domain from template |
|
|
40
|
+
| `kdna validate <path>` | Stable | Validate domain structure |
|
|
41
|
+
| `kdna validate --schema <path>` | Stable | Schema-only validation |
|
|
42
|
+
| `kdna pack <path>` | Beta | Pack into .kdna container |
|
|
43
|
+
| `kdna pack <path> --encrypt --license <file>` | Beta | Pack encrypted .kdnae container |
|
|
44
|
+
| `kdna unpack <file>` | Beta | Unpack .kdna or .kdnae container |
|
|
45
|
+
| `kdna inspect <path>` | Beta | Inspect domain or .kdna file |
|
|
46
|
+
| `kdna publish <path>` | Experimental | Pack + sign + publish to registry |
|
|
47
|
+
| `kdna publish --check <path>` | Experimental | Quality gate check only |
|
|
48
|
+
| `kdna version bump <level> [path]` | Beta | Bump domain version |
|
|
43
49
|
|
|
44
50
|
### Agent Runtime
|
|
45
51
|
|
|
46
|
-
| Command | Description |
|
|
47
|
-
|
|
48
|
-
| `kdna available [--json]` | List installed domains with v2.1 fields |
|
|
49
|
-
| `kdna match "<task>" [--json]` | Signal matching — find relevant domains |
|
|
50
|
-
| `kdna load <name> [--as=prompt\|json\|raw]` | Emit domain in agent-ready format |
|
|
51
|
-
| `kdna postvalidate <name> --output <file>` | Post-generation judgment check |
|
|
52
|
+
| Command | Status | Description |
|
|
53
|
+
|---------|--------|-------------|
|
|
54
|
+
| `kdna available [--json]` | Beta | List installed domains with v2.1 fields |
|
|
55
|
+
| `kdna match "<task>" [--json]` | Beta | Signal matching — find relevant domains |
|
|
56
|
+
| `kdna load <name> [--as=prompt\|json\|raw]` | Beta | Emit domain in agent-ready format |
|
|
57
|
+
| `kdna postvalidate <name> --output <file>` | Beta | Post-generation judgment check |
|
|
52
58
|
|
|
53
59
|
### Testing & Verification
|
|
54
60
|
|
|
55
|
-
| Command | Description |
|
|
56
|
-
|
|
57
|
-
| `kdna verify <name>` | 3-layer: structure + trust + judgment |
|
|
58
|
-
| `kdna compare <name> --input "..."` | With/without KDNA reasoning diff |
|
|
59
|
-
| `kdna compare <name> --input "..." --report-md` | Markdown report with scoring |
|
|
60
|
-
| `kdna compare <name> --input "..." --report-json` | JSON report with scoring |
|
|
61
|
-
| `kdna diff <name>@<v1> <name>@<v2>` | Judgment-level diff between versions |
|
|
61
|
+
| Command | Status | Description |
|
|
62
|
+
|---------|--------|-------------|
|
|
63
|
+
| `kdna verify <name>` | Beta | 3-layer: structure + trust + judgment |
|
|
64
|
+
| `kdna compare <name> --input "..."` | Beta | With/without KDNA reasoning diff |
|
|
65
|
+
| `kdna compare <name> --input "..." --report-md` | Beta | Markdown report with scoring |
|
|
66
|
+
| `kdna compare <name> --input "..." --report-json` | Beta | JSON report with scoring |
|
|
67
|
+
| `kdna diff <name>@<v1> <name>@<v2>` | Beta | Judgment-level diff between versions |
|
|
62
68
|
|
|
63
69
|
### Diagnostics & Trace
|
|
64
70
|
|
|
65
|
-
| Command | Description |
|
|
66
|
-
|
|
67
|
-
| `kdna doctor` | System health check |
|
|
68
|
-
| `kdna doctor --agents` | Agent integration check (Codex/Claude/OpenCode/Cursor/Gemini) |
|
|
69
|
-
| `kdna doctor --json` | Machine-readable health report |
|
|
70
|
-
| `kdna trace` | View recent load/postvalidate traces |
|
|
71
|
-
| `kdna trace --json` | Machine-readable trace output |
|
|
72
|
-
| `kdna trace --export <file>` | Export traces for audit |
|
|
73
|
-
| `kdna trace --since 7d\|30d\|90d` | Filter by time range |
|
|
74
|
-
| `kdna history` | Recent domain usage (last 20) |
|
|
75
|
-
| `kdna history --stats` | Aggregate by domain and agent |
|
|
76
|
-
| `kdna history --domain <name>` | Filter by domain |
|
|
71
|
+
| Command | Status | Description |
|
|
72
|
+
|---------|--------|-------------|
|
|
73
|
+
| `kdna doctor` | Beta | System health check |
|
|
74
|
+
| `kdna doctor --agents` | Beta | Agent integration check (Codex/Claude/OpenCode/Cursor/Gemini) |
|
|
75
|
+
| `kdna doctor --json` | Beta | Machine-readable health report |
|
|
76
|
+
| `kdna trace` | Experimental | View recent load/postvalidate traces |
|
|
77
|
+
| `kdna trace --json` | Experimental | Machine-readable trace output |
|
|
78
|
+
| `kdna trace --export <file>` | Experimental | Export traces for audit |
|
|
79
|
+
| `kdna trace --since 7d\|30d\|90d` | Experimental | Filter by time range |
|
|
80
|
+
| `kdna history` | Experimental | Recent domain usage (last 20) |
|
|
81
|
+
| `kdna history --stats` | Experimental | Aggregate by domain and agent |
|
|
82
|
+
| `kdna history --domain <name>` | Experimental | Filter by domain |
|
|
77
83
|
|
|
78
84
|
### License & Authorization
|
|
79
85
|
|
|
80
|
-
| Command | Description |
|
|
81
|
-
|
|
82
|
-
| `kdna license generate <domain> --to <email>` | Generate signed license |
|
|
83
|
-
| `kdna license install <license.json>` | Register license for auto-decrypt |
|
|
84
|
-
| `kdna license verify <license.json>` | Verify license signature and validity |
|
|
85
|
-
| `kdna license bind <license.json>` | Bind license to this machine |
|
|
86
|
-
| `kdna license show <license.json>` | Display license details |
|
|
86
|
+
| Command | Status | Description |
|
|
87
|
+
|---------|--------|-------------|
|
|
88
|
+
| `kdna license generate <domain> --to <email>` | Experimental | Generate signed license |
|
|
89
|
+
| `kdna license install <license.json>` | Experimental | Register license for auto-decrypt |
|
|
90
|
+
| `kdna license verify <license.json>` | Experimental | Verify license signature and validity |
|
|
91
|
+
| `kdna license bind <license.json>` | Experimental | Bind license to this machine |
|
|
92
|
+
| `kdna license show <license.json>` | Experimental | Display license details |
|
|
87
93
|
|
|
88
94
|
### Cluster Composition
|
|
89
95
|
|
|
90
|
-
| Command | Description |
|
|
91
|
-
|
|
92
|
-
| `kdna cluster lint <path>` | Validate cluster manifest |
|
|
96
|
+
| Command | Status | Description |
|
|
97
|
+
|---------|--------|-------------|
|
|
98
|
+
| `kdna cluster lint <path>` | Planned | Validate cluster manifest |
|
|
93
99
|
|
|
94
100
|
### Registry & Distribution
|
|
95
101
|
|
|
96
|
-
| Command | Description |
|
|
97
|
-
|
|
98
|
-
| `kdna install <name>` | Install domain from registry |
|
|
99
|
-
| `kdna install ./file.kdna` | Install from local .kdna file |
|
|
100
|
-
| `kdna install ./file.kdnae` | Install from encrypted .kdnae (auto-decrypt with license) |
|
|
101
|
-
| `kdna remove <name>` | Uninstall a domain |
|
|
102
|
-
| `kdna update <name>` | Update installed domain |
|
|
103
|
-
| `kdna info <name>` | Show domain metadata and trust status |
|
|
104
|
-
| `kdna list [--available]` | List installed or available domains |
|
|
105
|
-
| `kdna search <keyword>` | Search registry |
|
|
106
|
-
| `kdna registry refresh` | Refresh registry cache |
|
|
102
|
+
| Command | Status | Description |
|
|
103
|
+
|---------|--------|-------------|
|
|
104
|
+
| `kdna install <name>` | Beta | Install domain from registry |
|
|
105
|
+
| `kdna install ./file.kdna` | Beta | Install from local .kdna file |
|
|
106
|
+
| `kdna install ./file.kdnae` | Beta | Install from encrypted .kdnae (auto-decrypt with license) |
|
|
107
|
+
| `kdna remove <name>` | Beta | Uninstall a domain |
|
|
108
|
+
| `kdna update <name>` | Beta | Update installed domain |
|
|
109
|
+
| `kdna info <name>` | Beta | Show domain metadata and trust status |
|
|
110
|
+
| `kdna list [--available]` | Beta | List installed or available domains |
|
|
111
|
+
| `kdna search <keyword>` | Beta | Search registry |
|
|
112
|
+
| `kdna registry refresh` | Beta | Refresh registry cache |
|
|
107
113
|
|
|
108
114
|
### Identity & Signing
|
|
109
115
|
|
|
110
|
-
| Command | Description |
|
|
111
|
-
|
|
112
|
-
| `kdna identity init` | Generate Ed25519 signing key |
|
|
113
|
-
| `kdna identity show` | Display public key and buyer ID |
|
|
114
|
-
| `kdna identity export [--out]` | Backup private key (encrypted) |
|
|
115
|
-
| `kdna identity import <file>` | Restore identity from backup |
|
|
116
|
+
| Command | Status | Description |
|
|
117
|
+
|---------|--------|-------------|
|
|
118
|
+
| `kdna identity init` | Experimental | Generate Ed25519 signing key |
|
|
119
|
+
| `kdna identity show` | Experimental | Display public key and buyer ID |
|
|
120
|
+
| `kdna identity export [--out]` | Experimental | Backup private key (encrypted) |
|
|
121
|
+
| `kdna identity import <file>` | Experimental | Restore identity from backup |
|
|
116
122
|
|
|
117
123
|
### Setup
|
|
118
124
|
|
|
119
|
-
| Command | Description |
|
|
120
|
-
|
|
121
|
-
| `kdna setup` | One-command setup: CLI + skill + data root |
|
|
125
|
+
| Command | Status | Description |
|
|
126
|
+
|---------|--------|-------------|
|
|
127
|
+
| `kdna setup` | Beta | One-command setup: CLI + skill + data root |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## SPEC Compatibility
|
|
132
|
+
|
|
133
|
+
KDNA CLI follows the canonical KDNA domain structure defined in [`aikdna/kdna`](https://github.com/aikdna/kdna).
|
|
134
|
+
|
|
135
|
+
A valid KDNA domain is a lowercase `snake_case` folder. A complete domain may include up to six files:
|
|
136
|
+
|
|
137
|
+
- `KDNA_Core.json`
|
|
138
|
+
- `KDNA_Patterns.json`
|
|
139
|
+
- `KDNA_Scenarios.json`
|
|
140
|
+
- `KDNA_Cases.json`
|
|
141
|
+
- `KDNA_Reasoning.json`
|
|
142
|
+
- `KDNA_Evolution.json`
|
|
143
|
+
|
|
144
|
+
The minimum valid domain requires:
|
|
145
|
+
|
|
146
|
+
- `KDNA_Core.json`
|
|
147
|
+
- `KDNA_Patterns.json`
|
|
148
|
+
|
|
149
|
+
Each file must include `meta.version`, `meta.domain`, `meta.created`, `meta.purpose`, and `meta.load_condition`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Default Registry
|
|
154
|
+
|
|
155
|
+
By default, KDNA CLI uses the official KDNA registry. Users may override it with `KDNA_REGISTRY_URL`.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Use the official registry (default)
|
|
159
|
+
kdna install @aikdna/writing
|
|
160
|
+
|
|
161
|
+
# Use a custom registry
|
|
162
|
+
export KDNA_REGISTRY_URL="https://my-registry.example.com/domains.json"
|
|
163
|
+
kdna install @myorg/internal
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Open Source and Commercial Boundary
|
|
169
|
+
|
|
170
|
+
KDNA keeps the protocol, schemas, validator, core CLI, benchmark tools, and reference examples open source.
|
|
171
|
+
|
|
172
|
+
Commercial or hosted layers may include:
|
|
173
|
+
|
|
174
|
+
- Managed registry services
|
|
175
|
+
- Quality badge review workflows
|
|
176
|
+
- Hosted runtime guard
|
|
177
|
+
- Enterprise private registry
|
|
178
|
+
- Team collaboration in KDNA Studio
|
|
179
|
+
- Licensed/private judgment asset distribution
|
|
180
|
+
|
|
181
|
+
KDNA supports both open judgment assets and licensed/private judgment assets. Open domains remain the default path for community adoption, while encrypted containers and licenses support professional and enterprise distribution.
|
|
182
|
+
|
|
183
|
+
KDNA 同时支持开放判断资产和授权/私有判断资产。开放 domain 是社区采用的默认路径;加密容器和 license 用于专业资产与企业分发。
|
|
184
|
+
|
|
185
|
+
---
|
|
122
186
|
|
|
123
|
-
|
|
187
|
+
## Environment Variables
|
|
124
188
|
|
|
125
189
|
| Variable | Purpose |
|
|
126
190
|
|----------|---------|
|
|
@@ -165,7 +229,7 @@ kdna license verify --json <file>
|
|
|
165
229
|
| Authoring | KDNA Studio | Human-led judgment production |
|
|
166
230
|
| Consumption | KDNAChat | Load, use, compare |
|
|
167
231
|
| Governance | KDNA Governance Console | Approve, release, audit |
|
|
168
|
-
| Distribution | Registry | Discover, install,
|
|
232
|
+
| Distribution | Registry | Discover, install, license, distribute |
|
|
169
233
|
|
|
170
234
|
## Development
|
|
171
235
|
|
|
@@ -178,9 +242,9 @@ npm test
|
|
|
178
242
|
|
|
179
243
|
## Related
|
|
180
244
|
|
|
181
|
-
- [@aikdna/kdna-core](https://github.com/aikdna/
|
|
245
|
+
- [@aikdna/kdna-core](https://github.com/aikdna/kdna/tree/main/packages/kdna-core) — Pure logic library
|
|
182
246
|
- [KDNA Registry](https://github.com/aikdna/kdna-registry) — Domain catalog
|
|
183
|
-
- [KDNA SPEC](https://github.com/aikdna/
|
|
247
|
+
- [KDNA SPEC](https://github.com/aikdna/kdna) — Protocol specification
|
|
184
248
|
- [aikdna.com](https://aikdna.com) — Website
|
|
185
249
|
|
|
186
250
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"kdna-cli",
|
|
32
32
|
"ai-agent",
|
|
33
33
|
"domain-cognition",
|
|
34
|
-
"knowledge-dna",
|
|
35
34
|
"cli"
|
|
36
35
|
],
|
|
37
36
|
"license": "Apache-2.0",
|
|
@@ -41,13 +40,13 @@
|
|
|
41
40
|
},
|
|
42
41
|
"homepage": "https://aikdna.com",
|
|
43
42
|
"bugs": {
|
|
44
|
-
"url": "https://github.com/
|
|
43
|
+
"url": "https://github.com/aikdna/kdna-cli/issues"
|
|
45
44
|
},
|
|
46
45
|
"engines": {
|
|
47
46
|
"node": ">=18"
|
|
48
47
|
},
|
|
49
48
|
"dependencies": {
|
|
50
|
-
"@aikdna/kdna-core": "^0.
|
|
49
|
+
"@aikdna/kdna-core": "^0.4.0"
|
|
51
50
|
},
|
|
52
51
|
"optionalDependencies": {
|
|
53
52
|
"ajv": "^8.17.1",
|
package/src/agent.js
CHANGED
|
@@ -365,12 +365,44 @@ function cmdLoad(input, args = []) {
|
|
|
365
365
|
if (manifest.replaced_by) console.error(`Try: ${manifest.replaced_by}`);
|
|
366
366
|
process.exit(2);
|
|
367
367
|
}
|
|
368
|
+
|
|
369
|
+
// ═══ Trust check before loading ═══
|
|
370
|
+
const loadWarnings = [];
|
|
371
|
+
const signature = manifest.signature;
|
|
372
|
+
const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
|
|
373
|
+
if (isPlaceholder) {
|
|
374
|
+
loadWarnings.push('⚠ Domain is unsigned — no cryptographic proof of authorship. Trust depends on source.');
|
|
375
|
+
}
|
|
376
|
+
if (manifest.status === 'deprecated') {
|
|
377
|
+
loadWarnings.push(`⚠ Domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}.`);
|
|
378
|
+
}
|
|
379
|
+
const riskLevel = manifest.risk_level || 'R1';
|
|
380
|
+
if (riskLevel === 'R3' || riskLevel === 'R4') {
|
|
381
|
+
loadWarnings.push(`⚠ High risk domain (${riskLevel}) — may influence agent behavior in safety-critical ways.`);
|
|
382
|
+
if (manifest.quality_badge === 'untested' || !manifest.quality_badge) {
|
|
383
|
+
loadWarnings.push('⚠ High risk + untested — load only if you trust the source and understand the risks.');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (loadWarnings.length > 0) {
|
|
387
|
+
console.error(loadWarnings.join('\n'));
|
|
388
|
+
}
|
|
368
389
|
const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
|
|
369
390
|
const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
|
|
370
391
|
|
|
371
392
|
// JSON format
|
|
372
393
|
if (format === 'json') {
|
|
373
|
-
process.stdout.write(JSON.stringify({
|
|
394
|
+
process.stdout.write(JSON.stringify({
|
|
395
|
+
manifest,
|
|
396
|
+
core,
|
|
397
|
+
patterns: pat,
|
|
398
|
+
trust: {
|
|
399
|
+
signature: isPlaceholder ? 'unsigned' : 'present',
|
|
400
|
+
risk_level: riskLevel,
|
|
401
|
+
deprecated: manifest.status === 'deprecated',
|
|
402
|
+
yanked: false,
|
|
403
|
+
warnings: loadWarnings,
|
|
404
|
+
},
|
|
405
|
+
}, null, 2) + '\n');
|
|
374
406
|
recordTrace({
|
|
375
407
|
timestamp: new Date().toISOString(),
|
|
376
408
|
agent: detectAgent(),
|
|
@@ -910,4 +942,346 @@ function cmdPostvalidate(args = []) {
|
|
|
910
942
|
process.exit(results.violations.length ? 1 : 0);
|
|
911
943
|
}
|
|
912
944
|
|
|
913
|
-
|
|
945
|
+
// ─── kdna route ─────────────────────────────────────────────────────────
|
|
946
|
+
|
|
947
|
+
function cmdRoute(taskText, args = []) {
|
|
948
|
+
const wantJson = args.includes('--json');
|
|
949
|
+
|
|
950
|
+
if (!taskText) {
|
|
951
|
+
const err = { error: 'Usage: kdna route "<task description>" [--json] [--discover]' };
|
|
952
|
+
if (wantJson) { console.log(JSON.stringify(err)); process.exit(2); }
|
|
953
|
+
console.error(err.error);
|
|
954
|
+
process.exit(2);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const traceId = `route_${require('crypto').randomUUID()}`;
|
|
958
|
+
const taskTokens = tokenize(taskText);
|
|
959
|
+
const installed = listInstalled();
|
|
960
|
+
const result = {
|
|
961
|
+
status: 'SKIP_NO_JUDGMENT_NEEDED',
|
|
962
|
+
action: 'skip',
|
|
963
|
+
needs_kdna: false,
|
|
964
|
+
selected_domain: null,
|
|
965
|
+
reason: '',
|
|
966
|
+
confidence: 0,
|
|
967
|
+
candidates: [],
|
|
968
|
+
rejected_domains: [],
|
|
969
|
+
trust: null,
|
|
970
|
+
ambiguity: null,
|
|
971
|
+
registry_suggestions: [],
|
|
972
|
+
auto_install: false,
|
|
973
|
+
trace_id: traceId,
|
|
974
|
+
created_at: new Date().toISOString(),
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// ═══ Gate 1: Intent — does this task need domain judgment? ═══
|
|
978
|
+
const judgmentKeywords = [
|
|
979
|
+
'review', 'diagnose', 'critique', 'evaluate', 'assess', 'judge',
|
|
980
|
+
'should i', 'is this good', 'is this correct', 'how would you rate',
|
|
981
|
+
'分析', '诊断', '评估', '判断', '审查', '该怎么', '好不好',
|
|
982
|
+
];
|
|
983
|
+
const mechanicalKeywords = [
|
|
984
|
+
'format', 'translate', 'convert', 'list', 'find', 'lookup', 'search',
|
|
985
|
+
'run', 'execute', 'compile', 'build', 'fix syntax', 'fix the bug',
|
|
986
|
+
'格式化', '翻译', '转换', '列出', '查找', '搜索', '运行', '执行', '编译', '修复语法',
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
const taskLower = taskText.toLowerCase();
|
|
990
|
+
const hasJudgmentSignal = judgmentKeywords.some(k => taskLower.includes(k));
|
|
991
|
+
const hasMechanicalSignal = mechanicalKeywords.some(k => taskLower.includes(k));
|
|
992
|
+
|
|
993
|
+
result.needs_kdna = hasJudgmentSignal && !hasMechanicalSignal;
|
|
994
|
+
|
|
995
|
+
if (!result.needs_kdna) {
|
|
996
|
+
result.status = 'SKIP_NO_JUDGMENT_NEEDED';
|
|
997
|
+
result.action = 'skip';
|
|
998
|
+
result.reason = hasMechanicalSignal
|
|
999
|
+
? 'task is mechanical — no domain judgment required'
|
|
1000
|
+
: 'task does not appear to need domain judgment';
|
|
1001
|
+
if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
1002
|
+
console.log('SKIP (no judgment needed)');
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (!installed.length) {
|
|
1007
|
+
result.status = 'SKIP_NO_LOCAL_DOMAIN';
|
|
1008
|
+
result.action = 'skip';
|
|
1009
|
+
result.reason = 'task may benefit from judgment, but no KDNA domains are installed';
|
|
1010
|
+
if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
1011
|
+
console.log('SKIP (no domains installed)');
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ═══ Gate 2: Negative Match First — check does_not_apply_when ═══
|
|
1016
|
+
// ═══ Gate 3: Domain Fit — evaluate applies_when + relevance ═══
|
|
1017
|
+
const candidates = [];
|
|
1018
|
+
|
|
1019
|
+
for (const e of installed) {
|
|
1020
|
+
const manifest = readJson(path.join(e.dir, 'kdna.json')) || {};
|
|
1021
|
+
if (manifest.yanked === true) {
|
|
1022
|
+
result.rejected_domains.push({
|
|
1023
|
+
domain: manifest.name || e.full,
|
|
1024
|
+
triggered_rule: 'yanked',
|
|
1025
|
+
reason: 'domain has been yanked',
|
|
1026
|
+
});
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
|
|
1031
|
+
|
|
1032
|
+
// Negative match: does_not_apply_when
|
|
1033
|
+
let disqualified = null;
|
|
1034
|
+
for (const a of core.axioms || []) {
|
|
1035
|
+
for (const d of a.does_not_apply_when || []) {
|
|
1036
|
+
const score = overlapScore(taskTokens, d);
|
|
1037
|
+
if (score.hits >= 2) {
|
|
1038
|
+
disqualified = { axiom: a.id, text: d, hits: score.hits };
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (disqualified) break;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (disqualified) {
|
|
1046
|
+
result.rejected_domains.push({
|
|
1047
|
+
domain: manifest.name || e.full,
|
|
1048
|
+
triggered_rule: `${disqualified.axiom}.does_not_apply_when`,
|
|
1049
|
+
reason: `"${disqualified.text.slice(0, 100)}"`,
|
|
1050
|
+
});
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Positive fit: applies_when + domain relevance
|
|
1055
|
+
let fitScore = 0;
|
|
1056
|
+
const fitReasons = [];
|
|
1057
|
+
|
|
1058
|
+
for (const a of core.axioms || []) {
|
|
1059
|
+
for (const ap of a.applies_when || []) {
|
|
1060
|
+
const score = overlapScore(taskTokens, ap);
|
|
1061
|
+
if (score.hits >= 2) {
|
|
1062
|
+
fitScore += score.hits * 3;
|
|
1063
|
+
fitReasons.push({ source: a.id, hits: score.hits, text: ap.slice(0, 120) });
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
fitScore += domainRelevanceScore(taskTokens, manifest);
|
|
1068
|
+
|
|
1069
|
+
// Confidence based on fitScore normalized
|
|
1070
|
+
const confidence = Math.min(0.95, fitScore > 0 ? 0.5 + fitScore * 0.05 : 0.15);
|
|
1071
|
+
|
|
1072
|
+
candidates.push({
|
|
1073
|
+
domain: manifest.name || e.full,
|
|
1074
|
+
version: manifest.version || '?',
|
|
1075
|
+
status: manifest.status || 'experimental',
|
|
1076
|
+
score: fitScore,
|
|
1077
|
+
confidence,
|
|
1078
|
+
reasons: fitReasons.slice(0, 5),
|
|
1079
|
+
description: manifest.description || '',
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Sort by score
|
|
1084
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
1085
|
+
|
|
1086
|
+
// ═══ Gate 4: Decision ═══
|
|
1087
|
+
const strongCandidates = candidates.filter(c => c.score >= 6);
|
|
1088
|
+
const weakCandidates = candidates.filter(c => c.score > 0 && c.score < 6);
|
|
1089
|
+
|
|
1090
|
+
if (strongCandidates.length === 0 && weakCandidates.length === 0) {
|
|
1091
|
+
// No matches at all
|
|
1092
|
+
result.status = 'SKIP_NO_LOCAL_DOMAIN';
|
|
1093
|
+
result.action = 'skip';
|
|
1094
|
+
result.reason = 'no installed domain matches this task';
|
|
1095
|
+
if (result.rejected_domains.length > 0) {
|
|
1096
|
+
result.reason += ` (${result.rejected_domains.length} domains explicitly excluded by does_not_apply_when)`;
|
|
1097
|
+
}
|
|
1098
|
+
result.candidates = candidates.map(c => ({
|
|
1099
|
+
domain: c.domain,
|
|
1100
|
+
decision: 'rejected',
|
|
1101
|
+
reason: 'insufficient match score',
|
|
1102
|
+
confidence: c.confidence,
|
|
1103
|
+
}));
|
|
1104
|
+
} else if (strongCandidates.length > 1) {
|
|
1105
|
+
// Multiple strong matches — ambiguity
|
|
1106
|
+
result.status = 'ASK_AMBIGUOUS_DOMAIN';
|
|
1107
|
+
result.action = 'ask';
|
|
1108
|
+
result.reason = `${strongCandidates.length} domains strongly match this task with different judgment frames`;
|
|
1109
|
+
|
|
1110
|
+
result.ambiguity = {
|
|
1111
|
+
domains: strongCandidates.slice(0, 3).map(c => ({
|
|
1112
|
+
domain: c.domain,
|
|
1113
|
+
description: c.description,
|
|
1114
|
+
judgment_frame: c.reasons.length > 0 ? c.reasons[0].text : c.description,
|
|
1115
|
+
risk_if_wrong: `may misclassify the task as a ${c.domain.split('/').pop()} problem`,
|
|
1116
|
+
})),
|
|
1117
|
+
recommendation: 'Choose the domain whose judgment frame best matches the task intent. Do not blend domains.',
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
result.candidates = strongCandidates.map(c => ({
|
|
1121
|
+
domain: c.domain, decision: 'ambiguous', reason: `score ${c.score}`, confidence: c.confidence,
|
|
1122
|
+
}));
|
|
1123
|
+
} else if (strongCandidates.length === 1) {
|
|
1124
|
+
// One strong match + possible weak matches
|
|
1125
|
+
const selected = strongCandidates[0];
|
|
1126
|
+
result.candidates = [
|
|
1127
|
+
{ domain: selected.domain, decision: 'strong_match', reason: `score ${selected.score}`, confidence: selected.confidence },
|
|
1128
|
+
...weakCandidates.map(c => ({ domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence })),
|
|
1129
|
+
];
|
|
1130
|
+
|
|
1131
|
+
// ═══ Trust Gate ═══
|
|
1132
|
+
const trust = checkTrust(selected.domain);
|
|
1133
|
+
result.trust = trust;
|
|
1134
|
+
|
|
1135
|
+
if (!trust.passed) {
|
|
1136
|
+
result.status = 'BLOCK_TRUST_FAILED';
|
|
1137
|
+
result.action = 'block';
|
|
1138
|
+
result.reason = `domain matched but trust check failed: ${trust.failures.join(', ')}`;
|
|
1139
|
+
} else {
|
|
1140
|
+
result.status = 'LOAD_STRONG_FIT';
|
|
1141
|
+
result.action = 'load';
|
|
1142
|
+
result.selected_domain = selected.domain;
|
|
1143
|
+
result.confidence = selected.confidence;
|
|
1144
|
+
result.reason = `match: "${selected.description.slice(0, 100)}"`;
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
// Only weak matches — skip
|
|
1148
|
+
result.status = 'SKIP_WEAK_FIT';
|
|
1149
|
+
result.action = 'skip';
|
|
1150
|
+
result.reason = weakCandidates.length > 0
|
|
1151
|
+
? `${weakCandidates.length} domain(s) have weak match only — skipping to avoid contamination`
|
|
1152
|
+
: 'no installed domain matches this task';
|
|
1153
|
+
result.candidates = weakCandidates.map(c => ({
|
|
1154
|
+
domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence,
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Add rejected domains to candidates array for full trace
|
|
1159
|
+
for (const r of result.rejected_domains) {
|
|
1160
|
+
result.candidates.push({
|
|
1161
|
+
domain: r.domain,
|
|
1162
|
+
decision: 'rejected',
|
|
1163
|
+
reason: r.reason,
|
|
1164
|
+
confidence: 0,
|
|
1165
|
+
matched_does_not_apply_when: r.triggered_rule,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (wantJson) {
|
|
1170
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Human output
|
|
1175
|
+
console.log(`Task: ${taskText.slice(0, 100)}${taskText.length > 100 ? '…' : ''}`);
|
|
1176
|
+
console.log(`Route: ${result.status} → ${result.action}`);
|
|
1177
|
+
if (result.reason) console.log(`Reason: ${result.reason}`);
|
|
1178
|
+
if (result.selected_domain) console.log(`Domain: ${result.selected_domain}`);
|
|
1179
|
+
if (result.rejected_domains.length) {
|
|
1180
|
+
console.log(`Rejected: ${result.rejected_domains.map(r => r.domain).join(', ')}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function checkTrust(domainName, options = {}) {
|
|
1185
|
+
const failures = [];
|
|
1186
|
+
const warnings = [];
|
|
1187
|
+
const installed = listInstalled();
|
|
1188
|
+
const entry = installed.find(e => `${e.scope}/${e.ident}` === domainName || e.full === domainName);
|
|
1189
|
+
if (!entry) {
|
|
1190
|
+
failures.push('domain not found in installed directory');
|
|
1191
|
+
return { passed: false, failures, warnings };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const manifest = readJson(path.join(entry.dir, 'kdna.json')) || {};
|
|
1195
|
+
const core = readJson(path.join(entry.dir, 'KDNA_Core.json')) || {};
|
|
1196
|
+
const evolution = readJson(path.join(entry.dir, 'KDNA_Evolution.json')) || {};
|
|
1197
|
+
|
|
1198
|
+
// 1. Yank check
|
|
1199
|
+
if (manifest.yanked === true) {
|
|
1200
|
+
failures.push('domain is yanked');
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 2. Deprecation check
|
|
1204
|
+
if (manifest.status === 'deprecated') {
|
|
1205
|
+
warnings.push(`domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}`);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// 3. Signature check
|
|
1209
|
+
const signature = manifest.signature;
|
|
1210
|
+
const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
|
|
1211
|
+
if (manifest.access === 'licensed' || manifest.access === 'runtime') {
|
|
1212
|
+
if (isPlaceholder) {
|
|
1213
|
+
failures.push('commercial domain has no valid signature');
|
|
1214
|
+
}
|
|
1215
|
+
} else if (isPlaceholder) {
|
|
1216
|
+
warnings.push('domain is unsigned — trust depends on source');
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// 4. Risk level check
|
|
1220
|
+
const riskLevel = manifest.risk_level || entry.risk_level || 'R1';
|
|
1221
|
+
const riskMap = { R0: 0, R1: 1, R2: 2, R3: 3, R4: 4 };
|
|
1222
|
+
const riskNum = riskMap[riskLevel] || 1;
|
|
1223
|
+
if (riskNum >= 3) {
|
|
1224
|
+
warnings.push(`domain risk level is ${riskLevel} — high-risk judgment may influence agent behavior`);
|
|
1225
|
+
}
|
|
1226
|
+
if (riskNum >= 2 && (manifest.quality_badge === 'untested' || !manifest.quality_badge)) {
|
|
1227
|
+
warnings.push(`risk level ${riskLevel} with quality_badge '${manifest.quality_badge || 'none'}' — consider requiring review`);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// 5. SPEC compatibility check
|
|
1231
|
+
const specVersion = manifest.spec_version || manifest.kdna_spec || 'unknown';
|
|
1232
|
+
const supportedSpecs = ['1.0-rc', '1.0', '0.7'];
|
|
1233
|
+
if (!supportedSpecs.includes(specVersion)) {
|
|
1234
|
+
warnings.push(`SPEC version '${specVersion}' may not be fully compatible with current loader`);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// 6. License validity (commercial domains)
|
|
1238
|
+
if (manifest.access === 'licensed' || manifest.access === 'runtime') {
|
|
1239
|
+
const licenseStorePath = path.join(process.env.HOME || '.', '.kdna', 'licenses.json');
|
|
1240
|
+
let licenseOk = false;
|
|
1241
|
+
try {
|
|
1242
|
+
if (require('fs').existsSync(licenseStorePath)) {
|
|
1243
|
+
const store = JSON.parse(require('fs').readFileSync(licenseStorePath, 'utf8'));
|
|
1244
|
+
const keys = store.keys || {};
|
|
1245
|
+
for (const [hash, entry] of Object.entries(keys)) {
|
|
1246
|
+
if (entry.active && entry.domain_id === domainName) {
|
|
1247
|
+
if (!entry.expires_at || new Date(entry.expires_at) > new Date()) {
|
|
1248
|
+
licenseOk = true;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (!licenseOk) {
|
|
1254
|
+
warnings.push('commercial domain has no active license — install with: kdna install ' + domainName + ' --license <key>');
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
warnings.push('no license store found — commercial domain may require a license');
|
|
1258
|
+
}
|
|
1259
|
+
} catch { /* license check unavailable */ }
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// 7. Human Lock check (judgment-class cards)
|
|
1263
|
+
const judgmentCardTypes = ['axiom', 'boundary', 'risk'];
|
|
1264
|
+
const axioms = core.axioms || [];
|
|
1265
|
+
const hasJudgmentCards = axioms.length > 0;
|
|
1266
|
+
if (hasJudgmentCards) {
|
|
1267
|
+
const humanLocks = evolution.human_locks || [];
|
|
1268
|
+
const lockedAxioms = axioms.filter(a => {
|
|
1269
|
+
// Check if axiom has a human_lock field OR if an evolution lock covers it
|
|
1270
|
+
return a.human_lock || humanLocks.some(hl => hl.lock_type === 'accept');
|
|
1271
|
+
}).length;
|
|
1272
|
+
if (lockedAxioms === 0 && humanLocks.length === 0) {
|
|
1273
|
+
warnings.push('domain has no Human Lock records — judgment-class content may not be human-verified');
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return {
|
|
1278
|
+
passed: failures.length === 0,
|
|
1279
|
+
failures,
|
|
1280
|
+
warnings,
|
|
1281
|
+
riskLevel,
|
|
1282
|
+
specVersion,
|
|
1283
|
+
signatureValid: !isPlaceholder,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate, cmdRoute, checkTrust };
|
package/src/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
cmdSelect,
|
|
27
27
|
cmdLoad,
|
|
28
28
|
cmdPostvalidate,
|
|
29
|
+
cmdRoute,
|
|
29
30
|
} = require('./cmds/quality');
|
|
30
31
|
const { cmdCluster } = require('./cmds/cluster');
|
|
31
32
|
const { cmdIdentity } = require('./cmds/identity');
|
|
@@ -101,6 +102,7 @@ Studio Integration (Phase 1):
|
|
|
101
102
|
studio readiness <project.json> Generate domain readiness card
|
|
102
103
|
|
|
103
104
|
Agent Runtime:
|
|
105
|
+
route "<task>" [--json] [--discover] 5-Gate 7-State routing decision
|
|
104
106
|
available [--json] List installed domains with v2.1 fields
|
|
105
107
|
match "<task>" [--json] Signal matching — find relevant domains
|
|
106
108
|
select --input "..." [--json] Selection policy — decide which domains to load
|
|
@@ -294,7 +296,7 @@ switch (cmd) {
|
|
|
294
296
|
if (!target) error('Usage: kdna card <path> [--json] [--locale zh-CN]');
|
|
295
297
|
const localeIdx = args.indexOf('--locale');
|
|
296
298
|
const locale = localeIdx >= 0 ? args[localeIdx + 1] : null;
|
|
297
|
-
cmdCard(target, locale);
|
|
299
|
+
cmdCard(target, args.includes('--json'), locale);
|
|
298
300
|
break;
|
|
299
301
|
}
|
|
300
302
|
case 'verify': {
|
|
@@ -340,6 +342,10 @@ switch (cmd) {
|
|
|
340
342
|
cmdSelect(args);
|
|
341
343
|
break;
|
|
342
344
|
}
|
|
345
|
+
case 'route': {
|
|
346
|
+
cmdRoute(args);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
343
349
|
case 'postvalidate': {
|
|
344
350
|
cmdPostvalidate(args);
|
|
345
351
|
break;
|
|
@@ -513,7 +519,7 @@ switch (cmd) {
|
|
|
513
519
|
const idx = args.indexOf('--check');
|
|
514
520
|
const target = args[idx + 1] || args.filter((a) => !a.startsWith('--'))[1] || '.';
|
|
515
521
|
if (!target || target.startsWith('--')) error('Usage: kdna publish --check <path>');
|
|
516
|
-
cmdPublishCheck(target);
|
|
522
|
+
cmdPublishCheck(target, args);
|
|
517
523
|
} else {
|
|
518
524
|
const { cmdPublish } = require('./publish');
|
|
519
525
|
const target = args.filter((a) => !a.startsWith('--'))[1];
|
package/src/cmds/domain.js
CHANGED
|
@@ -11,38 +11,31 @@ const {
|
|
|
11
11
|
ENCRYPTED_FILES,
|
|
12
12
|
} = require('./encrypt');
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Resolve schemas from @aikdna/kdna-core package
|
|
25
|
-
const SCHEMA_DIR = path.join(
|
|
26
|
-
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
27
|
-
'schema',
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// Read all KDNA JSON files
|
|
31
|
-
const files = fs.readdirSync(abs).filter((f) => f.endsWith('.json') && f !== 'kdna.json');
|
|
14
|
+
const KDNA_DOMAIN_FILES = new Set([
|
|
15
|
+
'KDNA_Core.json',
|
|
16
|
+
'KDNA_Patterns.json',
|
|
17
|
+
'KDNA_Scenarios.json',
|
|
18
|
+
'KDNA_Cases.json',
|
|
19
|
+
'KDNA_Reasoning.json',
|
|
20
|
+
'KDNA_Evolution.json',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function readKDNAContentFiles(abs) {
|
|
32
24
|
const dataMap = {};
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
for (const f of files) {
|
|
25
|
+
const parseErrors = [];
|
|
26
|
+
for (const f of fs.readdirSync(abs).filter((name) => KDNA_DOMAIN_FILES.has(name))) {
|
|
36
27
|
try {
|
|
37
28
|
dataMap[f] = JSON.parse(fs.readFileSync(path.join(abs, f), 'utf8'));
|
|
38
29
|
} catch (e) {
|
|
39
30
|
dataMap[f] = null;
|
|
40
|
-
|
|
31
|
+
parseErrors.push(`${f}: ${e.message}`);
|
|
41
32
|
}
|
|
42
33
|
}
|
|
34
|
+
return { dataMap, parseErrors };
|
|
35
|
+
}
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
const
|
|
37
|
+
function loadSchemaMap(schemaDir) {
|
|
38
|
+
const fileToSchema = {
|
|
46
39
|
'KDNA_Core.json': 'KDNA_Core.schema.json',
|
|
47
40
|
'KDNA_Patterns.json': 'KDNA_Patterns.schema.json',
|
|
48
41
|
'KDNA_Scenarios.json': 'KDNA_Scenarios.schema.json',
|
|
@@ -51,10 +44,11 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
|
51
44
|
'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
|
|
52
45
|
};
|
|
53
46
|
|
|
47
|
+
const schemaMap = {};
|
|
54
48
|
const loadedSchemas = [];
|
|
55
49
|
const missingSchemas = [];
|
|
56
|
-
for (const
|
|
57
|
-
const schemaPath = path.join(
|
|
50
|
+
for (const schemaFile of Object.values(fileToSchema)) {
|
|
51
|
+
const schemaPath = path.join(schemaDir, schemaFile);
|
|
58
52
|
if (fs.existsSync(schemaPath)) {
|
|
59
53
|
try {
|
|
60
54
|
schemaMap[schemaFile] = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
@@ -67,50 +61,132 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
|
|
73
|
-
);
|
|
74
|
-
console.log(` Schema dir: ${SCHEMA_DIR}`);
|
|
75
|
-
}
|
|
64
|
+
return { schemaMap, loadedSchemas, missingSchemas };
|
|
65
|
+
}
|
|
76
66
|
|
|
77
|
-
|
|
78
|
-
const
|
|
67
|
+
function validateDomainDirectory(abs, schemaMap, schemaOnly) {
|
|
68
|
+
const { lintDomain, validateDomainSchema, validateCrossFile } = require('@aikdna/kdna-core');
|
|
69
|
+
const { dataMap, parseErrors } = readKDNAContentFiles(abs);
|
|
70
|
+
const errors = parseErrors.map((msg) => `JSON parse error in ${msg}`);
|
|
79
71
|
const warnings = [];
|
|
80
72
|
|
|
81
|
-
// Layer 1: Lint (structural + content checks)
|
|
82
73
|
if (!schemaOnly) {
|
|
83
74
|
const lintResult = lintDomain(dataMap);
|
|
84
75
|
errors.push(...lintResult.errors);
|
|
85
76
|
warnings.push(...lintResult.warnings);
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
// Layer 2: JSON Schema validation against loaded schemas
|
|
89
79
|
const schemaResult = validateDomainSchema(dataMap, schemaMap);
|
|
90
80
|
errors.push(...schemaResult.errors);
|
|
91
81
|
warnings.push(...schemaResult.warnings);
|
|
92
82
|
|
|
93
|
-
// Layer 3: Cross-file consistency
|
|
94
83
|
const crossResult = validateCrossFile(dataMap);
|
|
95
84
|
errors.push(...crossResult.errors);
|
|
96
85
|
warnings.push(...crossResult.warnings);
|
|
97
86
|
|
|
98
|
-
|
|
87
|
+
return {
|
|
88
|
+
path: abs,
|
|
89
|
+
valid: errors.length === 0,
|
|
90
|
+
files: Object.keys(dataMap).filter((k) => dataMap[k]).length,
|
|
91
|
+
errors,
|
|
92
|
+
warnings,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isClusterDirectory(abs) {
|
|
97
|
+
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
98
|
+
return !!(
|
|
99
|
+
manifest?.cluster ||
|
|
100
|
+
fs.existsSync(path.join(abs, 'KDNA_Cluster.json')) ||
|
|
101
|
+
fs.existsSync(path.join(abs, 'cluster_manifest.json'))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateClusterDirectory(abs, schemaMap, schemaOnly) {
|
|
106
|
+
const manifest = readJson(path.join(abs, 'kdna.json')) || {};
|
|
107
|
+
const clusterManifest = readJson(path.join(abs, 'KDNA_Cluster.json')) || {};
|
|
108
|
+
const fallbackManifest = readJson(path.join(abs, 'cluster_manifest.json')) || {};
|
|
109
|
+
const subDomains = manifest.sub_domains || fallbackManifest.domains || [];
|
|
110
|
+
const errors = [];
|
|
111
|
+
const warnings = [];
|
|
112
|
+
|
|
113
|
+
if (!Array.isArray(subDomains) || subDomains.length === 0) {
|
|
114
|
+
errors.push('Cluster has no sub_domains/domains list to validate');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const domains = [];
|
|
118
|
+
for (const name of subDomains) {
|
|
119
|
+
const domainPath = path.join(abs, name);
|
|
120
|
+
if (!fs.existsSync(domainPath) || !fs.statSync(domainPath).isDirectory()) {
|
|
121
|
+
errors.push(`Cluster sub-domain not found: ${name}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const result = validateDomainDirectory(domainPath, schemaMap, schemaOnly);
|
|
125
|
+
domains.push({ name, ...result });
|
|
126
|
+
warnings.push(...result.warnings.map((w) => `${name}: ${w}`));
|
|
127
|
+
errors.push(...result.errors.map((e) => `${name}: ${e}`));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!manifest.cluster && !clusterManifest.name && !fallbackManifest.name) {
|
|
131
|
+
warnings.push('Cluster metadata is minimal: no cluster marker or cluster name found');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
path: abs,
|
|
136
|
+
valid: errors.length === 0,
|
|
137
|
+
cluster: true,
|
|
138
|
+
domains,
|
|
139
|
+
files: domains.reduce((sum, d) => sum + d.files, 0),
|
|
140
|
+
errors,
|
|
141
|
+
warnings,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Validate ────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
148
|
+
const abs = path.resolve(dir);
|
|
149
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
150
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Resolve schemas from @aikdna/kdna-core package
|
|
154
|
+
const SCHEMA_DIR = path.join(
|
|
155
|
+
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
156
|
+
'schema',
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const { schemaMap, loadedSchemas, missingSchemas } = loadSchemaMap(SCHEMA_DIR);
|
|
160
|
+
|
|
161
|
+
if (missingSchemas.length) {
|
|
162
|
+
console.log(
|
|
163
|
+
` Note: ${missingSchemas.length} schema file(s) not found (optional file schemas): ${missingSchemas.join(', ')}`,
|
|
164
|
+
);
|
|
165
|
+
console.log(` Schema dir: ${SCHEMA_DIR}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = isClusterDirectory(abs)
|
|
169
|
+
? validateClusterDirectory(abs, schemaMap, schemaOnly)
|
|
170
|
+
: validateDomainDirectory(abs, schemaMap, schemaOnly);
|
|
171
|
+
const { errors, warnings } = result;
|
|
172
|
+
const validCount = result.files;
|
|
99
173
|
const schemaInfo = schemaOnly
|
|
100
174
|
? ` (schema-only mode, ${loadedSchemas.length} schemas loaded)`
|
|
101
175
|
: '';
|
|
102
176
|
|
|
103
177
|
if (jsonMode) {
|
|
104
|
-
const
|
|
178
|
+
const payload = {
|
|
105
179
|
path: abs,
|
|
106
180
|
valid: errors.length === 0,
|
|
107
181
|
files: validCount,
|
|
182
|
+
cluster: !!result.cluster,
|
|
183
|
+
domains: result.domains,
|
|
108
184
|
schemas_loaded: loadedSchemas.length,
|
|
109
185
|
schema_only: schemaOnly,
|
|
110
186
|
errors,
|
|
111
187
|
warnings,
|
|
112
188
|
};
|
|
113
|
-
console.log(JSON.stringify(
|
|
189
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
114
190
|
process.exit(errors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
115
191
|
}
|
|
116
192
|
|
|
@@ -124,7 +200,13 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
|
124
200
|
process.exit(EXIT.VALIDATION_FAILED);
|
|
125
201
|
}
|
|
126
202
|
|
|
127
|
-
|
|
203
|
+
if (result.cluster) {
|
|
204
|
+
console.log(
|
|
205
|
+
`✓ KDNA cluster valid: ${abs} (${result.domains.length} domains, ${validCount} KDNA files, schema OK${schemaInfo})`,
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`✓ KDNA domain valid: ${abs} (${validCount} files, schema OK${schemaInfo})`);
|
|
209
|
+
}
|
|
128
210
|
}
|
|
129
211
|
|
|
130
212
|
// ─── Pack / Unpack (.kdna ZIP container) ──────────────────────────────────
|
|
@@ -140,6 +222,19 @@ function cmdPack(dir, outputDir) {
|
|
|
140
222
|
if (!core) error('KDNA_Core.json not found or invalid');
|
|
141
223
|
if (!pat) error('KDNA_Patterns.json not found or invalid');
|
|
142
224
|
|
|
225
|
+
// Human Lock Gate — check judgment-class cards before packing
|
|
226
|
+
const { checkHumanLock } = require('../publish');
|
|
227
|
+
const hl = checkHumanLock(abs);
|
|
228
|
+
if (!hl.passed) {
|
|
229
|
+
console.error('Human Lock Gate: BLOCKED');
|
|
230
|
+
for (const issue of hl.issues) {
|
|
231
|
+
console.error(` ✗ ${issue}`);
|
|
232
|
+
}
|
|
233
|
+
console.error('Judgment-class cards must be locked with valid Human Lock before packing.');
|
|
234
|
+
console.error('Use kdna publish --check for details.');
|
|
235
|
+
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
236
|
+
}
|
|
237
|
+
|
|
143
238
|
const domainName = core.meta?.domain || path.basename(abs);
|
|
144
239
|
|
|
145
240
|
// Ensure kdna.json manifest exists (generate if missing)
|
|
@@ -1025,7 +1120,7 @@ zf.close()
|
|
|
1025
1120
|
|
|
1026
1121
|
// ─── KDNA Card (locale-aware) ────────────────────────────────────
|
|
1027
1122
|
|
|
1028
|
-
function cmdCard(dir, locale = null) {
|
|
1123
|
+
function cmdCard(dir, jsonMode = false, locale = null) {
|
|
1029
1124
|
const abs = path.resolve(dir);
|
|
1030
1125
|
let card = readJson(path.join(abs, 'KDNA_CARD.json'));
|
|
1031
1126
|
|
|
@@ -1043,8 +1138,6 @@ function cmdCard(dir, locale = null) {
|
|
|
1043
1138
|
);
|
|
1044
1139
|
}
|
|
1045
1140
|
|
|
1046
|
-
const jsonMode = process.argv.includes('--json');
|
|
1047
|
-
|
|
1048
1141
|
if (jsonMode) {
|
|
1049
1142
|
console.log(JSON.stringify(card, null, 2));
|
|
1050
1143
|
return;
|
package/src/cmds/quality.js
CHANGED
|
@@ -100,6 +100,17 @@ function cmdPostvalidate(args) {
|
|
|
100
100
|
cmdPostvalidate(args);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function cmdRoute(args) {
|
|
104
|
+
const { cmdRoute } = require('../agent');
|
|
105
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
106
|
+
const flags = args.filter((a) => a.startsWith('--'));
|
|
107
|
+
if (!positional[1]) {
|
|
108
|
+
const { error, EXIT } = require('./_common');
|
|
109
|
+
error('Usage: kdna route "<task description>" [--json] [--discover]', EXIT.INPUT_ERROR);
|
|
110
|
+
}
|
|
111
|
+
cmdRoute(positional.slice(1).join(' ').trim(), flags);
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
module.exports = {
|
|
104
115
|
cmdCompare,
|
|
105
116
|
cmdDiff,
|
|
@@ -109,4 +120,5 @@ module.exports = {
|
|
|
109
120
|
cmdSelect,
|
|
110
121
|
cmdLoad,
|
|
111
122
|
cmdPostvalidate,
|
|
123
|
+
cmdRoute,
|
|
112
124
|
};
|
package/src/publish.js
CHANGED
|
@@ -141,9 +141,71 @@ function isGenericSelfCheck(question) {
|
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// ─── Human Lock Gate ──────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
const JUDGMENT_CARD_TYPES = ['axiom', 'boundary', 'risk', 'aesthetic'];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check whether the domain satisfies Human Lock requirements.
|
|
150
|
+
* Returns { passed, issues[] } — publish should be blocked if !passed.
|
|
151
|
+
*/
|
|
152
|
+
function checkHumanLock(domainPath) {
|
|
153
|
+
const core = readJson(path.join(domainPath, 'KDNA_Core.json'));
|
|
154
|
+
if (!core) return { passed: false, issues: ['KDNA_Core.json not found'] };
|
|
155
|
+
|
|
156
|
+
const issues = [];
|
|
157
|
+
const cards = [];
|
|
158
|
+
|
|
159
|
+
// Collect judgment-class cards from axioms, boundaries, risks
|
|
160
|
+
if (core.axioms) {
|
|
161
|
+
for (const a of core.axioms) {
|
|
162
|
+
cards.push({ type: 'axiom', id: a.id || '?', status: a.status, human_lock: a.human_lock });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (core.boundaries) {
|
|
166
|
+
for (const b of core.boundaries) {
|
|
167
|
+
cards.push({ type: 'boundary', id: b.id || '?', status: b.status, human_lock: b.human_lock });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (core.risks || core.risk_model) {
|
|
171
|
+
const risks = core.risks || core.risk_model || [];
|
|
172
|
+
for (const r of risks) {
|
|
173
|
+
cards.push({ type: 'risk', id: r.id || '?', status: r.status, human_lock: r.human_lock });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (cards.length === 0) return { passed: true, issues: [] };
|
|
178
|
+
|
|
179
|
+
for (const card of cards) {
|
|
180
|
+
// Rule 1: Must be locked
|
|
181
|
+
if (!card.status || !['locked', 'tested', 'published'].includes(card.status)) {
|
|
182
|
+
issues.push(`${card.type} "${card.id}" is not locked. Human Lock required before publish.`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Rule 2: Must have human_lock record
|
|
186
|
+
if (!card.human_lock || !card.human_lock.by || !card.human_lock.statement) {
|
|
187
|
+
issues.push(`${card.type} "${card.id}" is locked but has no valid Human Lock record.`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Rule 3: Lock must confirm judgment fields were reviewed
|
|
191
|
+
const checked = card.human_lock.checked || {};
|
|
192
|
+
if (!checked.applies_when) {
|
|
193
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`);
|
|
194
|
+
}
|
|
195
|
+
if (!checked.does_not_apply_when) {
|
|
196
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`);
|
|
197
|
+
}
|
|
198
|
+
if (!checked.failure_risk) {
|
|
199
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { passed: issues.length === 0, issues };
|
|
204
|
+
}
|
|
205
|
+
|
|
144
206
|
// ─── Main check function ──────────────────────────────────────────────
|
|
145
207
|
|
|
146
|
-
function cmdPublishCheck(domainPath) {
|
|
208
|
+
function cmdPublishCheck(domainPath, args = []) {
|
|
147
209
|
const abs = path.resolve(domainPath);
|
|
148
210
|
if (!fs.existsSync(abs)) error(`Domain not found: ${abs}`);
|
|
149
211
|
|
|
@@ -152,6 +214,35 @@ function cmdPublishCheck(domainPath) {
|
|
|
152
214
|
console.log('═'.repeat(60));
|
|
153
215
|
console.log('');
|
|
154
216
|
|
|
217
|
+
// ─── Human Lock Gate (must pass before any other checks) ──────────
|
|
218
|
+
const hl = checkHumanLock(abs);
|
|
219
|
+
if (!hl.passed) {
|
|
220
|
+
if (args.includes('--force')) {
|
|
221
|
+
console.warn(' ⚠ Human Lock Gate: OVERRIDDEN (--force). Proceeding with checks.');
|
|
222
|
+
console.warn(` ${hl.issues.length} unresolved Human Lock issue(s):`);
|
|
223
|
+
for (const issue of hl.issues) {
|
|
224
|
+
console.warn(` ${issue}`);
|
|
225
|
+
}
|
|
226
|
+
console.warn('');
|
|
227
|
+
} else {
|
|
228
|
+
console.error(' Human Lock Gate: BLOCKED');
|
|
229
|
+
console.error(` ${hl.issues.length} issue(s) found:`);
|
|
230
|
+
for (const issue of hl.issues) {
|
|
231
|
+
console.error(` ✗ ${issue}`);
|
|
232
|
+
}
|
|
233
|
+
console.error('');
|
|
234
|
+
console.error(' Judgment-class cards (axiom, boundary, risk, aesthetic)');
|
|
235
|
+
console.error(' must be locked with a valid Human Lock record before publishing.');
|
|
236
|
+
console.error(' Use kdna-studio or manually add human_lock to each card.');
|
|
237
|
+
console.error(' Use --force for emergency override (audited).');
|
|
238
|
+
console.error('');
|
|
239
|
+
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
console.log(' ✓ Human Lock Gate: passed');
|
|
243
|
+
console.log('');
|
|
244
|
+
}
|
|
245
|
+
|
|
155
246
|
let errors = 0;
|
|
156
247
|
let warnings = 0;
|
|
157
248
|
let passes = 0;
|
|
@@ -551,6 +642,26 @@ function cmdPublish(domainPath, args = []) {
|
|
|
551
642
|
console.log('═'.repeat(60));
|
|
552
643
|
console.log(` Publishing ${name}@${manifest.version}`);
|
|
553
644
|
console.log('═'.repeat(60));
|
|
645
|
+
|
|
646
|
+
// ─── Human Lock Gate ──────────────────────────────────────────────
|
|
647
|
+
const hl = checkHumanLock(abs);
|
|
648
|
+
if (!hl.passed) {
|
|
649
|
+
console.error('');
|
|
650
|
+
console.error(' Human Lock Gate: BLOCKED');
|
|
651
|
+
for (const issue of hl.issues) {
|
|
652
|
+
console.error(` ✗ ${issue}`);
|
|
653
|
+
}
|
|
654
|
+
console.error('');
|
|
655
|
+
console.error(' Use kdna publish --check for details, or --force to override.');
|
|
656
|
+
if (!args.includes('--force')) {
|
|
657
|
+
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
658
|
+
}
|
|
659
|
+
console.warn(' ⚠ --force override: publishing without Human Lock (emergency only)');
|
|
660
|
+
} else {
|
|
661
|
+
console.log(` ✓ Human Lock Gate: passed`);
|
|
662
|
+
}
|
|
663
|
+
console.log('');
|
|
664
|
+
|
|
554
665
|
console.log(` Identity fingerprint: ${fingerprint(publicKey)}`);
|
|
555
666
|
console.log(` Scope trust key: ${scopeKey.slice(0, 28)}…`);
|
|
556
667
|
console.log('');
|
|
@@ -633,4 +744,4 @@ function cmdPublish(domainPath, args = []) {
|
|
|
633
744
|
);
|
|
634
745
|
}
|
|
635
746
|
|
|
636
|
-
module.exports = { cmdPublishCheck, cmdPublish, canonicalPayload, publicKeyToScopeFormat };
|
|
747
|
+
module.exports = { cmdPublishCheck, cmdPublish, checkHumanLock, canonicalPayload, publicKeyToScopeFormat };
|
package/src/verify.js
CHANGED
|
@@ -21,6 +21,13 @@ const crypto = require('crypto');
|
|
|
21
21
|
const { RegistryResolver, parseName } = require('./registry');
|
|
22
22
|
const { EXIT } = require('./cmds/_common');
|
|
23
23
|
|
|
24
|
+
let validateManifestFn;
|
|
25
|
+
try {
|
|
26
|
+
validateManifestFn = require('@aikdna/kdna-core').validateManifest;
|
|
27
|
+
} catch {
|
|
28
|
+
// kdna-core not available — manifest validation skipped
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
25
32
|
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
26
33
|
|
|
@@ -54,6 +61,17 @@ function checkStructure(destDir) {
|
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// Validate kdna.json against canonical manifest schema
|
|
65
|
+
if (validateManifestFn) {
|
|
66
|
+
const manifest = readJson(path.join(destDir, 'kdna.json'));
|
|
67
|
+
if (manifest) {
|
|
68
|
+
const mResult = validateManifestFn(manifest);
|
|
69
|
+
for (const e of mResult.errors) issues.push({ severity: 'error', msg: e });
|
|
70
|
+
for (const w of mResult.warnings) issues.push({ severity: 'warn', msg: w });
|
|
71
|
+
if (mResult.errors.length === 0) passed.push('kdna.json conforms to manifest schema v1.0-rc');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
for (const f of optional) {
|
|
58
76
|
if (fs.existsSync(path.join(destDir, f))) passed.push(`has ${f}`);
|
|
59
77
|
}
|
|
@@ -552,6 +570,33 @@ function checkGovernance(destDir) {
|
|
|
552
570
|
|
|
553
571
|
function cmdVerify(input, args = []) {
|
|
554
572
|
const jsonMode = args.includes('--json');
|
|
573
|
+
const trustReport = args.includes('--trust-report');
|
|
574
|
+
|
|
575
|
+
// --trust-report: standalone mode — output full trust report and exit
|
|
576
|
+
if (trustReport) {
|
|
577
|
+
const parsed = parseName(input);
|
|
578
|
+
if (!parsed) {
|
|
579
|
+
console.log(JSON.stringify({ ok: false, error: `Invalid name: ${input}` }));
|
|
580
|
+
process.exit(EXIT.INPUT_ERROR);
|
|
581
|
+
}
|
|
582
|
+
const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
|
|
583
|
+
if (!destDir || !require('fs').existsSync(destDir)) {
|
|
584
|
+
console.log(JSON.stringify({ ok: false, error: `Domain not installed: ${input}` }));
|
|
585
|
+
process.exit(EXIT.INPUT_ERROR);
|
|
586
|
+
}
|
|
587
|
+
const { checkTrust: agentCheckTrust } = require('./agent');
|
|
588
|
+
const trust = agentCheckTrust(parsed.full);
|
|
589
|
+
console.log(JSON.stringify({
|
|
590
|
+
domain: parsed.full,
|
|
591
|
+
passed: trust.passed,
|
|
592
|
+
failures: trust.failures,
|
|
593
|
+
warnings: trust.warnings,
|
|
594
|
+
risk_level: trust.riskLevel,
|
|
595
|
+
spec_version: trust.specVersion,
|
|
596
|
+
signature_valid: trust.signatureValid,
|
|
597
|
+
}, null, 2));
|
|
598
|
+
process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
|
|
599
|
+
}
|
|
555
600
|
|
|
556
601
|
const want = {
|
|
557
602
|
structure: args.includes('--structure'),
|
package/validators/kdna-lint.js
CHANGED
|
@@ -10,6 +10,15 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { lintDomain } = require('@aikdna/kdna-core');
|
|
12
12
|
|
|
13
|
+
const KDNA_DOMAIN_FILES = new Set([
|
|
14
|
+
'KDNA_Core.json',
|
|
15
|
+
'KDNA_Patterns.json',
|
|
16
|
+
'KDNA_Scenarios.json',
|
|
17
|
+
'KDNA_Cases.json',
|
|
18
|
+
'KDNA_Reasoning.json',
|
|
19
|
+
'KDNA_Evolution.json',
|
|
20
|
+
]);
|
|
21
|
+
|
|
13
22
|
const domainDir = process.argv[2];
|
|
14
23
|
if (!domainDir || domainDir === '--help' || domainDir === '-h') {
|
|
15
24
|
console.log(`kdna-lint — Structural and content validation for KDNA domains.
|
|
@@ -28,19 +37,44 @@ if (!fs.existsSync(domainDir) || !fs.statSync(domainDir).isDirectory()) {
|
|
|
28
37
|
process.exit(2);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
// Read
|
|
32
|
-
|
|
40
|
+
// Read only canonical domain content files. Governance metadata such as
|
|
41
|
+
// KDNA_CARD.json is part of the package, but not one of the 6 KDNA JSON files.
|
|
42
|
+
const files = fs.readdirSync(domainDir).filter((f) => KDNA_DOMAIN_FILES.has(f));
|
|
33
43
|
const dataMap = {};
|
|
34
44
|
for (const f of files) {
|
|
35
45
|
try {
|
|
36
46
|
dataMap[f] = JSON.parse(fs.readFileSync(path.join(domainDir, f), 'utf8'));
|
|
37
|
-
} catch
|
|
47
|
+
} catch {
|
|
38
48
|
// lintDomain will report missing required files; skip unparseable here
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
const result = lintDomain(dataMap);
|
|
43
53
|
|
|
54
|
+
// Also validate kdna.json manifest if present and validateManifest is available
|
|
55
|
+
let manifestPath;
|
|
56
|
+
try {
|
|
57
|
+
manifestPath = path.join(domainDir, 'kdna.json');
|
|
58
|
+
if (fs.existsSync(manifestPath)) {
|
|
59
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
60
|
+
let validateManifestFn;
|
|
61
|
+
try {
|
|
62
|
+
validateManifestFn = require('@aikdna/kdna-core').validateManifest;
|
|
63
|
+
} catch {
|
|
64
|
+
// validateManifest not yet available in installed kdna-core — skip manifest check
|
|
65
|
+
}
|
|
66
|
+
if (validateManifestFn) {
|
|
67
|
+
const mResult = validateManifestFn(manifest);
|
|
68
|
+
for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
|
|
69
|
+
for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== 'ENOENT') {
|
|
74
|
+
result.errors.push(`kdna.json: failed to parse — ${e.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
if (result.warnings.length) {
|
|
45
79
|
console.log('Warnings:');
|
|
46
80
|
result.warnings.forEach((w) => console.log(` - ${w}`));
|
|
@@ -42,7 +42,8 @@ const FILE_MAP = {
|
|
|
42
42
|
'KDNA_Evolution.json': 'KDNA_Evolution.schema.json',
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
// Read
|
|
45
|
+
// Read only canonical domain content files. Governance metadata such as
|
|
46
|
+
// KDNA_CARD.json is valid package metadata, but not part of the 6-file domain set.
|
|
46
47
|
const dataMap = {};
|
|
47
48
|
for (const [file] of Object.entries(FILE_MAP)) {
|
|
48
49
|
const filePath = path.join(domainDir, file);
|
|
@@ -57,7 +58,7 @@ for (const [file] of Object.entries(FILE_MAP)) {
|
|
|
57
58
|
|
|
58
59
|
// Read schemas
|
|
59
60
|
const schemaMap = {};
|
|
60
|
-
for (const
|
|
61
|
+
for (const schemaFile of Object.values(FILE_MAP)) {
|
|
61
62
|
const schemaPath = path.join(SCHEMA_DIR, schemaFile);
|
|
62
63
|
if (fs.existsSync(schemaPath)) {
|
|
63
64
|
try {
|