@aikdna/kdna-cli 0.16.10 → 0.18.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 +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- package/src/cmds/encrypt.js +0 -199
package/src/agent.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* not treat as a fit decision; many false positives expected)
|
|
17
17
|
* The agent makes the final call using its own language understanding.
|
|
18
18
|
*
|
|
19
|
-
* kdna load <name> [--as=prompt|json|raw]
|
|
19
|
+
* kdna load <name|file.kdna> [--as=prompt|json|raw]
|
|
20
20
|
* Read the domain's Core + Patterns and emit:
|
|
21
21
|
* --as=prompt (default): compact text suitable for system-prompt
|
|
22
22
|
* injection (axioms one-liners + stances +
|
|
@@ -30,44 +30,44 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
const fs = require('fs');
|
|
33
|
-
const path = require('path');
|
|
34
33
|
const { parseName } = require('./registry');
|
|
35
34
|
const { recordTrace } = require('./cmds/trace');
|
|
35
|
+
const {
|
|
36
|
+
getInstalled,
|
|
37
|
+
listInstalled: listInstalledAssets,
|
|
38
|
+
readContainer,
|
|
39
|
+
readContainerEntry,
|
|
40
|
+
readContainerJson,
|
|
41
|
+
resolveAsset,
|
|
42
|
+
} = require('./package-store');
|
|
43
|
+
const { licenseDecryptOptionsForManifest } = require('./cmds/license');
|
|
36
44
|
|
|
37
45
|
function detectAgent() {
|
|
38
46
|
return process.env.KDNA_AGENT || 'cli';
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
function listInstalled() {
|
|
50
|
+
return listInstalledAssets().map((entry) => {
|
|
51
|
+
const parsed = parseName(entry.full);
|
|
52
|
+
return { ...entry, scope: parsed.scope, ident: parsed.ident };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
43
55
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
56
|
+
function assetLabel(asset, fallback) {
|
|
57
|
+
return asset.name || asset.parsed?.full || fallback;
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!fs.statSync(dir).isDirectory()) continue;
|
|
64
|
-
out.push({ scope: scopeName, ident, dir, full: `${scopeName}/${ident}` });
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
/* skip */
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
60
|
+
function traceAssetFields(asset, manifest = {}, license = null) {
|
|
61
|
+
const fields = {
|
|
62
|
+
asset_path: asset.asset_path,
|
|
63
|
+
asset_digest: asset.asset_digest || null,
|
|
64
|
+
content_digest: asset.content_digest || null,
|
|
65
|
+
version: manifest.version || asset.version || null,
|
|
66
|
+
judgment_version: manifest.judgment_version || asset.judgment_version || null,
|
|
67
|
+
access: manifest.access || asset.access || null,
|
|
68
|
+
};
|
|
69
|
+
if (license?.license_id) fields.license_id = license.license_id;
|
|
70
|
+
return fields;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// ─── kdna available ────────────────────────────────────────────────────
|
|
@@ -78,11 +78,9 @@ function cmdAvailable(args = []) {
|
|
|
78
78
|
|
|
79
79
|
const out = [];
|
|
80
80
|
for (const e of installed) {
|
|
81
|
-
const manifest =
|
|
81
|
+
const { manifest = {}, core = {} } = readContainer(e.asset_path);
|
|
82
82
|
if (manifest.yanked === true) continue;
|
|
83
83
|
|
|
84
|
-
const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
|
|
85
|
-
|
|
86
84
|
// Pull applies_when across all axioms (this is what the agent needs
|
|
87
85
|
// for fit-check). Collapsing per-axiom into one set makes the agent's
|
|
88
86
|
// matching decision much cheaper.
|
|
@@ -110,14 +108,7 @@ function cmdAvailable(args = []) {
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
if (wantJson) {
|
|
113
|
-
|
|
114
|
-
? out
|
|
115
|
-
: {
|
|
116
|
-
count: 0,
|
|
117
|
-
domains: [],
|
|
118
|
-
note: 'No domains installed. Run: kdna install <name> See: kdna list --available',
|
|
119
|
-
};
|
|
120
|
-
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
111
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
121
112
|
return;
|
|
122
113
|
}
|
|
123
114
|
|
|
@@ -195,12 +186,11 @@ function cmdMatch(taskText, args = []) {
|
|
|
195
186
|
const hints = [];
|
|
196
187
|
|
|
197
188
|
for (const e of installed) {
|
|
198
|
-
const manifest =
|
|
189
|
+
const { manifest = {}, core = {} } = readContainer(e.asset_path);
|
|
199
190
|
if (manifest.yanked === true) {
|
|
200
191
|
dropped.push({ name: manifest.name || e.full, reason: 'yanked' });
|
|
201
192
|
continue;
|
|
202
193
|
}
|
|
203
|
-
const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
|
|
204
194
|
|
|
205
195
|
// does_not_apply_when disqualification (HARD signal)
|
|
206
196
|
let disqualified = null;
|
|
@@ -318,7 +308,7 @@ function cmdMatch(taskText, args = []) {
|
|
|
318
308
|
}
|
|
319
309
|
}
|
|
320
310
|
console.log('');
|
|
321
|
-
console.log('To consider any of these, read its full data: kdna load <name> --as=json');
|
|
311
|
+
console.log('To consider any of these, read its full data: kdna load <name|file.kdna> --as=json');
|
|
322
312
|
}
|
|
323
313
|
}
|
|
324
314
|
|
|
@@ -348,34 +338,88 @@ function cmdLoad(input, args = []) {
|
|
|
348
338
|
profileInput = args[inputIdx + 1] || null;
|
|
349
339
|
}
|
|
350
340
|
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
353
|
-
console.error(`
|
|
341
|
+
const asset = resolveAsset(input);
|
|
342
|
+
if (!asset) {
|
|
343
|
+
console.error(`KDNA asset not found: ${input}. Use an installed name or a .kdna file.`);
|
|
354
344
|
process.exit(2);
|
|
355
345
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
346
|
+
|
|
347
|
+
const manifest = readContainerJson(asset.asset_path, 'kdna.json') || {};
|
|
348
|
+
const encryptedEntries = manifest.encryption?.encrypted_entries || [];
|
|
349
|
+
let decryptOptions = {};
|
|
350
|
+
let licenseActivation = null;
|
|
351
|
+
if (manifest.access === 'licensed' || encryptedEntries.length > 0) {
|
|
352
|
+
const activation = licenseDecryptOptionsForManifest(manifest);
|
|
353
|
+
if (!activation.ok) {
|
|
354
|
+
console.error(`KDNA license required for ${manifest.name || input}: ${activation.error}`);
|
|
355
|
+
console.error(`Install a license with: kdna license install <license.json>`);
|
|
356
|
+
process.exit(3);
|
|
357
|
+
}
|
|
358
|
+
decryptOptions = { decryptEntry: activation.decryptEntry };
|
|
359
|
+
licenseActivation = activation.license;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
let container;
|
|
363
|
+
try {
|
|
364
|
+
container = readContainer(asset.asset_path, decryptOptions);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error(`Failed to load KDNA asset: ${e.message}`);
|
|
367
|
+
process.exit(3);
|
|
368
|
+
}
|
|
369
|
+
const parsed = asset.parsed || parseName(manifest.name || '');
|
|
370
|
+
const label = assetLabel(asset, input);
|
|
363
371
|
if (manifest.yanked === true) {
|
|
364
|
-
console.error(`${
|
|
372
|
+
console.error(`${label}@${manifest.version} has been yanked.`);
|
|
365
373
|
if (manifest.replaced_by) console.error(`Try: ${manifest.replaced_by}`);
|
|
366
374
|
process.exit(2);
|
|
367
375
|
}
|
|
368
|
-
|
|
369
|
-
|
|
376
|
+
|
|
377
|
+
// ═══ Trust check before loading ═══
|
|
378
|
+
const loadWarnings = [];
|
|
379
|
+
const signature = manifest.signature;
|
|
380
|
+
const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
|
|
381
|
+
if (isPlaceholder) {
|
|
382
|
+
loadWarnings.push('⚠ Domain is unsigned — no cryptographic proof of authorship. Trust depends on source.');
|
|
383
|
+
}
|
|
384
|
+
if (manifest.status === 'deprecated') {
|
|
385
|
+
loadWarnings.push(`⚠ Domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}.`);
|
|
386
|
+
}
|
|
387
|
+
const riskLevel = manifest.risk_level || 'R1';
|
|
388
|
+
if (riskLevel === 'R3' || riskLevel === 'R4') {
|
|
389
|
+
loadWarnings.push(`⚠ High risk domain (${riskLevel}) — may influence agent behavior in safety-critical ways.`);
|
|
390
|
+
if (manifest.quality_badge === 'untested' || !manifest.quality_badge) {
|
|
391
|
+
loadWarnings.push('⚠ High risk + untested — load only if you trust the source and understand the risks.');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (loadWarnings.length > 0) {
|
|
395
|
+
console.error(loadWarnings.join('\n'));
|
|
396
|
+
}
|
|
397
|
+
const core = container.core || {};
|
|
398
|
+
const pat = container.patterns || {};
|
|
370
399
|
|
|
371
400
|
// JSON format
|
|
372
401
|
if (format === 'json') {
|
|
373
|
-
process.stdout.write(JSON.stringify({
|
|
402
|
+
process.stdout.write(JSON.stringify({
|
|
403
|
+
manifest,
|
|
404
|
+
core,
|
|
405
|
+
patterns: pat,
|
|
406
|
+
trust: {
|
|
407
|
+
signature: isPlaceholder ? 'unsigned' : 'present',
|
|
408
|
+
risk_level: riskLevel,
|
|
409
|
+
deprecated: manifest.status === 'deprecated',
|
|
410
|
+
yanked: false,
|
|
411
|
+
warnings: loadWarnings,
|
|
412
|
+
asset_digest: asset.asset_digest || null,
|
|
413
|
+
content_digest: asset.content_digest || null,
|
|
414
|
+
license_id: licenseActivation?.license_id || null,
|
|
415
|
+
},
|
|
416
|
+
}, null, 2) + '\n');
|
|
374
417
|
recordTrace({
|
|
375
418
|
timestamp: new Date().toISOString(),
|
|
376
419
|
agent: detectAgent(),
|
|
377
|
-
domain:
|
|
420
|
+
domain: label,
|
|
378
421
|
format: 'json',
|
|
422
|
+
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
379
423
|
});
|
|
380
424
|
return;
|
|
381
425
|
}
|
|
@@ -383,40 +427,46 @@ function cmdLoad(input, args = []) {
|
|
|
383
427
|
// Raw format
|
|
384
428
|
if (format === 'raw') {
|
|
385
429
|
for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
|
|
386
|
-
const
|
|
387
|
-
|
|
430
|
+
const encrypted = encryptedEntries.includes(f);
|
|
431
|
+
const buf = encrypted
|
|
432
|
+
? Buffer.from(JSON.stringify(container[f === 'KDNA_Core.json' ? 'core' : 'patterns'], null, 2))
|
|
433
|
+
: readContainerEntry(asset.asset_path, f);
|
|
434
|
+
if (buf) {
|
|
388
435
|
process.stdout.write(`\n=== ${f} ===\n`);
|
|
389
|
-
process.stdout.write(
|
|
436
|
+
process.stdout.write(buf.toString('utf8'));
|
|
390
437
|
}
|
|
391
438
|
}
|
|
392
439
|
recordTrace({
|
|
393
440
|
timestamp: new Date().toISOString(),
|
|
394
441
|
agent: detectAgent(),
|
|
395
|
-
domain:
|
|
442
|
+
domain: label,
|
|
396
443
|
format: 'raw',
|
|
444
|
+
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
397
445
|
});
|
|
398
446
|
return;
|
|
399
447
|
}
|
|
400
448
|
|
|
401
449
|
// Load profiles
|
|
402
450
|
if (profile) {
|
|
403
|
-
emitProfile(parsed, manifest, core, pat, profile, profileInput);
|
|
451
|
+
emitProfile(parsed || { full: label }, manifest, core, pat, profile, profileInput);
|
|
404
452
|
recordTrace({
|
|
405
453
|
timestamp: new Date().toISOString(),
|
|
406
454
|
agent: detectAgent(),
|
|
407
|
-
domain:
|
|
455
|
+
domain: label,
|
|
408
456
|
format: `profile:${profile}`,
|
|
457
|
+
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
409
458
|
});
|
|
410
459
|
return;
|
|
411
460
|
}
|
|
412
461
|
|
|
413
462
|
// Default: --as=prompt — compact text optimized for system-prompt injection.
|
|
414
|
-
emitCompact(parsed, manifest, core, pat);
|
|
463
|
+
emitCompact(parsed || { full: label }, manifest, core, pat);
|
|
415
464
|
recordTrace({
|
|
416
465
|
timestamp: new Date().toISOString(),
|
|
417
466
|
agent: detectAgent(),
|
|
418
|
-
domain:
|
|
467
|
+
domain: label,
|
|
419
468
|
format: 'prompt',
|
|
469
|
+
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
420
470
|
});
|
|
421
471
|
}
|
|
422
472
|
|
|
@@ -429,6 +479,7 @@ function emitProfile(parsed, manifest, core, pat, profile, input) {
|
|
|
429
479
|
lines.push('');
|
|
430
480
|
|
|
431
481
|
const axioms = core.axioms || [];
|
|
482
|
+
emitRequiredOutput(lines, manifest, core, pat);
|
|
432
483
|
|
|
433
484
|
switch (profile) {
|
|
434
485
|
case 'index':
|
|
@@ -514,11 +565,11 @@ function emitProfile(parsed, manifest, core, pat, profile, input) {
|
|
|
514
565
|
}
|
|
515
566
|
|
|
516
567
|
if (pat.terminology?.banned_terms?.length) {
|
|
517
|
-
lines.push('##
|
|
568
|
+
lines.push('## MUST NOT SAY');
|
|
518
569
|
for (const t of pat.terminology.banned_terms) {
|
|
519
570
|
const term = typeof t === 'string' ? t : t.term;
|
|
520
571
|
const replace = typeof t === 'object' ? t.replace_with : null;
|
|
521
|
-
lines.push(`- "${term}"${replace ? `
|
|
572
|
+
lines.push(`- "${term}"${replace ? ` -> use: ${replace}` : ''}`);
|
|
522
573
|
}
|
|
523
574
|
lines.push('');
|
|
524
575
|
}
|
|
@@ -557,8 +608,11 @@ function emitCompact(parsed, manifest, core, pat) {
|
|
|
557
608
|
if (manifest.core_insight) lines.push(`# core insight: ${manifest.core_insight}`);
|
|
558
609
|
lines.push('');
|
|
559
610
|
|
|
611
|
+
emitRequiredOutput(lines, manifest, core, pat);
|
|
612
|
+
|
|
560
613
|
if (core.axioms?.length) {
|
|
561
|
-
lines.push('##
|
|
614
|
+
lines.push('## JUDGMENT GUIDANCE');
|
|
615
|
+
lines.push('### Axioms (reason from these)');
|
|
562
616
|
for (const a of core.axioms) {
|
|
563
617
|
lines.push(`- ${a.one_sentence}`);
|
|
564
618
|
if (a.applies_when?.length) {
|
|
@@ -573,7 +627,7 @@ function emitCompact(parsed, manifest, core, pat) {
|
|
|
573
627
|
}
|
|
574
628
|
|
|
575
629
|
if (core.stances?.length) {
|
|
576
|
-
lines.push('
|
|
630
|
+
lines.push('### Stances');
|
|
577
631
|
for (const s of core.stances) {
|
|
578
632
|
const text = typeof s === 'string' ? s : s.stance;
|
|
579
633
|
if (text) lines.push(`- ${text}`);
|
|
@@ -582,17 +636,18 @@ function emitCompact(parsed, manifest, core, pat) {
|
|
|
582
636
|
}
|
|
583
637
|
|
|
584
638
|
if (pat.terminology?.banned_terms?.length) {
|
|
585
|
-
lines.push('##
|
|
639
|
+
lines.push('## MUST NOT SAY');
|
|
586
640
|
for (const t of pat.terminology.banned_terms) {
|
|
587
641
|
const term = typeof t === 'string' ? t : t.term;
|
|
588
642
|
const replace = typeof t === 'object' ? t.replace_with : null;
|
|
589
|
-
lines.push(`- "${term}"${replace ? `
|
|
643
|
+
lines.push(`- "${term}"${replace ? ` -> use: ${replace}` : ''}`);
|
|
590
644
|
}
|
|
591
645
|
lines.push('');
|
|
592
646
|
}
|
|
593
647
|
|
|
594
648
|
if (pat.misunderstandings?.length) {
|
|
595
|
-
lines.push('##
|
|
649
|
+
if (!core.axioms?.length) lines.push('## JUDGMENT GUIDANCE');
|
|
650
|
+
lines.push('### Misunderstandings to detect and avoid');
|
|
596
651
|
for (const m of pat.misunderstandings) {
|
|
597
652
|
lines.push(`- WRONG: ${m.wrong}`);
|
|
598
653
|
lines.push(` CORRECT: ${m.correct}`);
|
|
@@ -602,7 +657,8 @@ function emitCompact(parsed, manifest, core, pat) {
|
|
|
602
657
|
}
|
|
603
658
|
|
|
604
659
|
if (pat.self_check?.length) {
|
|
605
|
-
lines.push('##
|
|
660
|
+
lines.push('## SELF-CHECK');
|
|
661
|
+
lines.push('Answer before final output.');
|
|
606
662
|
for (const q of pat.self_check) {
|
|
607
663
|
const text = typeof q === 'string' ? q : q.question;
|
|
608
664
|
if (text) lines.push(`- ${text}`);
|
|
@@ -617,6 +673,37 @@ function emitCompact(parsed, manifest, core, pat) {
|
|
|
617
673
|
process.stdout.write(lines.join('\n') + '\n');
|
|
618
674
|
}
|
|
619
675
|
|
|
676
|
+
function emitRequiredOutput(lines, manifest, core, pat) {
|
|
677
|
+
const required = uniqueStrings([
|
|
678
|
+
...asStringArray(manifest.required_output),
|
|
679
|
+
...asStringArray(manifest.must_include),
|
|
680
|
+
...asStringArray(core.required_output),
|
|
681
|
+
...asStringArray(core.must_include),
|
|
682
|
+
...asStringArray(pat.required_output),
|
|
683
|
+
...asStringArray(pat.must_include),
|
|
684
|
+
...asStringArray(pat.output_constraints?.required_output),
|
|
685
|
+
...asStringArray(pat.output_constraints?.must_include),
|
|
686
|
+
]);
|
|
687
|
+
|
|
688
|
+
if (!required.length) return;
|
|
689
|
+
|
|
690
|
+
lines.push('## REQUIRED OUTPUT');
|
|
691
|
+
lines.push('Include these statements when they are relevant to the user request.');
|
|
692
|
+
for (const item of required) lines.push(`- ${item}`);
|
|
693
|
+
lines.push('');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function asStringArray(value) {
|
|
697
|
+
if (!value) return [];
|
|
698
|
+
if (typeof value === 'string') return [value];
|
|
699
|
+
if (!Array.isArray(value)) return [];
|
|
700
|
+
return value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim());
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function uniqueStrings(items) {
|
|
704
|
+
return Array.from(new Set(items.map((item) => item.trim()).filter(Boolean)));
|
|
705
|
+
}
|
|
706
|
+
|
|
620
707
|
// ─── kdna select ───────────────────────────────────────────────────────
|
|
621
708
|
|
|
622
709
|
function cmdSelect(args = []) {
|
|
@@ -642,9 +729,8 @@ function cmdSelect(args = []) {
|
|
|
642
729
|
const scores = [];
|
|
643
730
|
|
|
644
731
|
for (const e of installed) {
|
|
645
|
-
const manifest =
|
|
732
|
+
const { manifest = {}, core = {} } = readContainer(e.asset_path);
|
|
646
733
|
if (manifest.yanked === true) continue;
|
|
647
|
-
const core = readJson(path.join(e.dir, 'KDNA_Core.json')) || {};
|
|
648
734
|
|
|
649
735
|
// Check does_not_apply_when hard exclusion
|
|
650
736
|
let excluded = false;
|
|
@@ -742,14 +828,15 @@ function cmdPostvalidate(args = []) {
|
|
|
742
828
|
console.error(`Invalid name "${input}".`);
|
|
743
829
|
process.exit(2);
|
|
744
830
|
}
|
|
745
|
-
const
|
|
746
|
-
if (!
|
|
831
|
+
const installed = getInstalled(parsed.full);
|
|
832
|
+
if (!installed) {
|
|
747
833
|
console.error(`${parsed.full} is not installed.`);
|
|
748
834
|
process.exit(2);
|
|
749
835
|
}
|
|
750
836
|
|
|
751
|
-
const
|
|
752
|
-
const
|
|
837
|
+
const container = readContainer(installed.asset_path);
|
|
838
|
+
const core = container.core || {};
|
|
839
|
+
const pat = container.patterns || {};
|
|
753
840
|
|
|
754
841
|
// Read agent output
|
|
755
842
|
let agentOutput = '';
|
|
@@ -910,4 +997,327 @@ function cmdPostvalidate(args = []) {
|
|
|
910
997
|
process.exit(results.violations.length ? 1 : 0);
|
|
911
998
|
}
|
|
912
999
|
|
|
913
|
-
|
|
1000
|
+
// ─── kdna route ─────────────────────────────────────────────────────────
|
|
1001
|
+
|
|
1002
|
+
function cmdRoute(taskText, args = []) {
|
|
1003
|
+
const wantJson = args.includes('--json');
|
|
1004
|
+
|
|
1005
|
+
if (!taskText) {
|
|
1006
|
+
const err = { error: 'Usage: kdna route "<task description>" [--json] [--discover]' };
|
|
1007
|
+
if (wantJson) { console.log(JSON.stringify(err)); process.exit(2); }
|
|
1008
|
+
console.error(err.error);
|
|
1009
|
+
process.exit(2);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const traceId = `route_${require('crypto').randomUUID()}`;
|
|
1013
|
+
const taskTokens = tokenize(taskText);
|
|
1014
|
+
const installed = listInstalled();
|
|
1015
|
+
const result = {
|
|
1016
|
+
status: 'SKIP_NO_JUDGMENT_NEEDED',
|
|
1017
|
+
action: 'skip',
|
|
1018
|
+
needs_kdna: false,
|
|
1019
|
+
selected_domain: null,
|
|
1020
|
+
reason: '',
|
|
1021
|
+
confidence: 0,
|
|
1022
|
+
candidates: [],
|
|
1023
|
+
rejected_domains: [],
|
|
1024
|
+
trust: null,
|
|
1025
|
+
ambiguity: null,
|
|
1026
|
+
registry_suggestions: [],
|
|
1027
|
+
auto_install: false,
|
|
1028
|
+
trace_id: traceId,
|
|
1029
|
+
created_at: new Date().toISOString(),
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// ═══ Gate 1: Intent — does this task need domain judgment? ═══
|
|
1033
|
+
const judgmentKeywords = [
|
|
1034
|
+
'review', 'diagnose', 'critique', 'evaluate', 'assess', 'judge',
|
|
1035
|
+
'should i', 'is this good', 'is this correct', 'how would you rate',
|
|
1036
|
+
'分析', '诊断', '评估', '判断', '审查', '该怎么', '好不好',
|
|
1037
|
+
];
|
|
1038
|
+
const mechanicalKeywords = [
|
|
1039
|
+
'format', 'translate', 'convert', 'list', 'find', 'lookup', 'search',
|
|
1040
|
+
'run', 'execute', 'compile', 'build', 'fix syntax', 'fix the bug',
|
|
1041
|
+
'格式化', '翻译', '转换', '列出', '查找', '搜索', '运行', '执行', '编译', '修复语法',
|
|
1042
|
+
];
|
|
1043
|
+
|
|
1044
|
+
const taskLower = taskText.toLowerCase();
|
|
1045
|
+
const hasJudgmentSignal = judgmentKeywords.some(k => taskLower.includes(k));
|
|
1046
|
+
const hasMechanicalSignal = mechanicalKeywords.some(k => taskLower.includes(k));
|
|
1047
|
+
|
|
1048
|
+
result.needs_kdna = hasJudgmentSignal && !hasMechanicalSignal;
|
|
1049
|
+
|
|
1050
|
+
if (!result.needs_kdna) {
|
|
1051
|
+
result.status = 'SKIP_NO_JUDGMENT_NEEDED';
|
|
1052
|
+
result.action = 'skip';
|
|
1053
|
+
result.reason = hasMechanicalSignal
|
|
1054
|
+
? 'task is mechanical — no domain judgment required'
|
|
1055
|
+
: 'task does not appear to need domain judgment';
|
|
1056
|
+
if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
1057
|
+
console.log('SKIP (no judgment needed)');
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (!installed.length) {
|
|
1062
|
+
result.status = 'SKIP_NO_LOCAL_DOMAIN';
|
|
1063
|
+
result.action = 'skip';
|
|
1064
|
+
result.reason = 'task may benefit from judgment, but no KDNA domains are installed';
|
|
1065
|
+
if (wantJson) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
1066
|
+
console.log('SKIP (no domains installed)');
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ═══ Gate 2: Negative Match First — check does_not_apply_when ═══
|
|
1071
|
+
// ═══ Gate 3: Domain Fit — evaluate applies_when + relevance ═══
|
|
1072
|
+
const candidates = [];
|
|
1073
|
+
|
|
1074
|
+
for (const e of installed) {
|
|
1075
|
+
const { manifest = {}, core = {} } = readContainer(e.asset_path);
|
|
1076
|
+
if (manifest.yanked === true) {
|
|
1077
|
+
result.rejected_domains.push({
|
|
1078
|
+
domain: manifest.name || e.full,
|
|
1079
|
+
triggered_rule: 'yanked',
|
|
1080
|
+
reason: 'domain has been yanked',
|
|
1081
|
+
});
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Negative match: does_not_apply_when
|
|
1086
|
+
let disqualified = null;
|
|
1087
|
+
for (const a of core.axioms || []) {
|
|
1088
|
+
for (const d of a.does_not_apply_when || []) {
|
|
1089
|
+
const score = overlapScore(taskTokens, d);
|
|
1090
|
+
if (score.hits >= 2) {
|
|
1091
|
+
disqualified = { axiom: a.id, text: d, hits: score.hits };
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (disqualified) break;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (disqualified) {
|
|
1099
|
+
result.rejected_domains.push({
|
|
1100
|
+
domain: manifest.name || e.full,
|
|
1101
|
+
triggered_rule: `${disqualified.axiom}.does_not_apply_when`,
|
|
1102
|
+
reason: `"${disqualified.text.slice(0, 100)}"`,
|
|
1103
|
+
});
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Positive fit: applies_when + domain relevance
|
|
1108
|
+
let fitScore = 0;
|
|
1109
|
+
const fitReasons = [];
|
|
1110
|
+
|
|
1111
|
+
for (const a of core.axioms || []) {
|
|
1112
|
+
for (const ap of a.applies_when || []) {
|
|
1113
|
+
const score = overlapScore(taskTokens, ap);
|
|
1114
|
+
if (score.hits >= 2) {
|
|
1115
|
+
fitScore += score.hits * 3;
|
|
1116
|
+
fitReasons.push({ source: a.id, hits: score.hits, text: ap.slice(0, 120) });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
fitScore += domainRelevanceScore(taskTokens, manifest);
|
|
1121
|
+
|
|
1122
|
+
// Confidence based on fitScore normalized
|
|
1123
|
+
const confidence = Math.min(0.95, fitScore > 0 ? 0.5 + fitScore * 0.05 : 0.15);
|
|
1124
|
+
|
|
1125
|
+
candidates.push({
|
|
1126
|
+
domain: manifest.name || e.full,
|
|
1127
|
+
version: manifest.version || '?',
|
|
1128
|
+
status: manifest.status || 'experimental',
|
|
1129
|
+
score: fitScore,
|
|
1130
|
+
confidence,
|
|
1131
|
+
reasons: fitReasons.slice(0, 5),
|
|
1132
|
+
description: manifest.description || '',
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Sort by score
|
|
1137
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
1138
|
+
|
|
1139
|
+
// ═══ Gate 4: Decision ═══
|
|
1140
|
+
const strongCandidates = candidates.filter(c => c.score >= 6);
|
|
1141
|
+
const weakCandidates = candidates.filter(c => c.score > 0 && c.score < 6);
|
|
1142
|
+
|
|
1143
|
+
if (strongCandidates.length === 0 && weakCandidates.length === 0) {
|
|
1144
|
+
// No matches at all
|
|
1145
|
+
result.status = 'SKIP_NO_LOCAL_DOMAIN';
|
|
1146
|
+
result.action = 'skip';
|
|
1147
|
+
result.reason = 'no installed domain matches this task';
|
|
1148
|
+
if (result.rejected_domains.length > 0) {
|
|
1149
|
+
result.reason += ` (${result.rejected_domains.length} domains explicitly excluded by does_not_apply_when)`;
|
|
1150
|
+
}
|
|
1151
|
+
result.candidates = candidates.map(c => ({
|
|
1152
|
+
domain: c.domain,
|
|
1153
|
+
decision: 'rejected',
|
|
1154
|
+
reason: 'insufficient match score',
|
|
1155
|
+
confidence: c.confidence,
|
|
1156
|
+
}));
|
|
1157
|
+
} else if (strongCandidates.length > 1) {
|
|
1158
|
+
// Multiple strong matches — ambiguity
|
|
1159
|
+
result.status = 'ASK_AMBIGUOUS_DOMAIN';
|
|
1160
|
+
result.action = 'ask';
|
|
1161
|
+
result.reason = `${strongCandidates.length} domains strongly match this task with different judgment frames`;
|
|
1162
|
+
|
|
1163
|
+
result.ambiguity = {
|
|
1164
|
+
domains: strongCandidates.slice(0, 3).map(c => ({
|
|
1165
|
+
domain: c.domain,
|
|
1166
|
+
description: c.description,
|
|
1167
|
+
judgment_frame: c.reasons.length > 0 ? c.reasons[0].text : c.description,
|
|
1168
|
+
risk_if_wrong: `may misclassify the task as a ${c.domain.split('/').pop()} problem`,
|
|
1169
|
+
})),
|
|
1170
|
+
recommendation: 'Choose the domain whose judgment frame best matches the task intent. Do not blend domains.',
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
result.candidates = strongCandidates.map(c => ({
|
|
1174
|
+
domain: c.domain, decision: 'ambiguous', reason: `score ${c.score}`, confidence: c.confidence,
|
|
1175
|
+
}));
|
|
1176
|
+
} else if (strongCandidates.length === 1) {
|
|
1177
|
+
// One strong match + possible weak matches
|
|
1178
|
+
const selected = strongCandidates[0];
|
|
1179
|
+
result.candidates = [
|
|
1180
|
+
{ domain: selected.domain, decision: 'strong_match', reason: `score ${selected.score}`, confidence: selected.confidence },
|
|
1181
|
+
...weakCandidates.map(c => ({ domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence })),
|
|
1182
|
+
];
|
|
1183
|
+
|
|
1184
|
+
// ═══ Trust Gate ═══
|
|
1185
|
+
const trust = checkTrust(selected.domain);
|
|
1186
|
+
result.trust = trust;
|
|
1187
|
+
|
|
1188
|
+
if (!trust.passed) {
|
|
1189
|
+
result.status = 'BLOCK_TRUST_FAILED';
|
|
1190
|
+
result.action = 'block';
|
|
1191
|
+
result.reason = `domain matched but trust check failed: ${trust.failures.join(', ')}`;
|
|
1192
|
+
} else {
|
|
1193
|
+
result.status = 'LOAD_STRONG_FIT';
|
|
1194
|
+
result.action = 'load';
|
|
1195
|
+
result.selected_domain = selected.domain;
|
|
1196
|
+
result.confidence = selected.confidence;
|
|
1197
|
+
result.reason = `match: "${selected.description.slice(0, 100)}"`;
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
// Only weak matches — skip
|
|
1201
|
+
result.status = 'SKIP_WEAK_FIT';
|
|
1202
|
+
result.action = 'skip';
|
|
1203
|
+
result.reason = weakCandidates.length > 0
|
|
1204
|
+
? `${weakCandidates.length} domain(s) have weak match only — skipping to avoid contamination`
|
|
1205
|
+
: 'no installed domain matches this task';
|
|
1206
|
+
result.candidates = weakCandidates.map(c => ({
|
|
1207
|
+
domain: c.domain, decision: 'weak_match', reason: `score ${c.score}`, confidence: c.confidence,
|
|
1208
|
+
}));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Add rejected domains to candidates array for full trace
|
|
1212
|
+
for (const r of result.rejected_domains) {
|
|
1213
|
+
result.candidates.push({
|
|
1214
|
+
domain: r.domain,
|
|
1215
|
+
decision: 'rejected',
|
|
1216
|
+
reason: r.reason,
|
|
1217
|
+
confidence: 0,
|
|
1218
|
+
matched_does_not_apply_when: r.triggered_rule,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (wantJson) {
|
|
1223
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Human output
|
|
1228
|
+
console.log(`Task: ${taskText.slice(0, 100)}${taskText.length > 100 ? '…' : ''}`);
|
|
1229
|
+
console.log(`Route: ${result.status} → ${result.action}`);
|
|
1230
|
+
if (result.reason) console.log(`Reason: ${result.reason}`);
|
|
1231
|
+
if (result.selected_domain) console.log(`Domain: ${result.selected_domain}`);
|
|
1232
|
+
if (result.rejected_domains.length) {
|
|
1233
|
+
console.log(`Rejected: ${result.rejected_domains.map(r => r.domain).join(', ')}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function checkTrust(domainName) {
|
|
1238
|
+
const failures = [];
|
|
1239
|
+
const warnings = [];
|
|
1240
|
+
const entry = getInstalled(domainName);
|
|
1241
|
+
if (!entry) {
|
|
1242
|
+
failures.push('domain asset not found in package index');
|
|
1243
|
+
return { passed: false, failures, warnings };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const { manifest = {}, core = {}, evolution = {} } = readContainer(entry.asset_path);
|
|
1247
|
+
|
|
1248
|
+
// 1. Yank check
|
|
1249
|
+
if (manifest.yanked === true) {
|
|
1250
|
+
failures.push('domain is yanked');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// 2. Deprecation check
|
|
1254
|
+
if (manifest.status === 'deprecated') {
|
|
1255
|
+
warnings.push(`domain is deprecated${manifest.replaced_by ? ', replaced by ' + manifest.replaced_by : ''}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// 3. Signature check
|
|
1259
|
+
const signature = manifest.signature;
|
|
1260
|
+
const isPlaceholder = !signature || signature === '' || signature.includes('placeholder');
|
|
1261
|
+
if (manifest.access === 'licensed' || manifest.access === 'runtime') {
|
|
1262
|
+
if (isPlaceholder) {
|
|
1263
|
+
failures.push('commercial domain has no valid signature');
|
|
1264
|
+
}
|
|
1265
|
+
} else if (isPlaceholder) {
|
|
1266
|
+
warnings.push('domain is unsigned — trust depends on source');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// 4. Risk level check
|
|
1270
|
+
const riskLevel = manifest.risk_level || entry.risk_level || 'R1';
|
|
1271
|
+
const riskMap = { R0: 0, R1: 1, R2: 2, R3: 3, R4: 4 };
|
|
1272
|
+
const riskNum = riskMap[riskLevel] || 1;
|
|
1273
|
+
if (riskNum >= 3) {
|
|
1274
|
+
warnings.push(`domain risk level is ${riskLevel} — high-risk judgment may influence agent behavior`);
|
|
1275
|
+
}
|
|
1276
|
+
if (riskNum >= 2 && (manifest.quality_badge === 'untested' || !manifest.quality_badge)) {
|
|
1277
|
+
warnings.push(`risk level ${riskLevel} with quality_badge '${manifest.quality_badge || 'none'}' — consider requiring review`);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// 5. SPEC compatibility check
|
|
1281
|
+
const specVersion = manifest.spec_version || manifest.kdna_spec || 'unknown';
|
|
1282
|
+
const supportedSpecs = ['1.0-rc', '1.0', '0.7'];
|
|
1283
|
+
if (!supportedSpecs.includes(specVersion)) {
|
|
1284
|
+
warnings.push(`SPEC version '${specVersion}' may not be fully compatible with current loader`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// 6. License validity (commercial domains)
|
|
1288
|
+
if (manifest.access === 'licensed' || manifest.access === 'runtime') {
|
|
1289
|
+
const licenseCheck = licenseDecryptOptionsForManifest({ ...manifest, name: domainName });
|
|
1290
|
+
if (!licenseCheck.ok) {
|
|
1291
|
+
warnings.push(
|
|
1292
|
+
'commercial domain has no active entitlement — run: kdna license activate ' +
|
|
1293
|
+
domainName +
|
|
1294
|
+
' --key <license-key> --server <url>'
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// 7. Human Lock check (judgment-class cards)
|
|
1300
|
+
const axioms = core.axioms || [];
|
|
1301
|
+
const hasJudgmentCards = axioms.length > 0;
|
|
1302
|
+
if (hasJudgmentCards) {
|
|
1303
|
+
const humanLocks = evolution.human_locks || [];
|
|
1304
|
+
const lockedAxioms = axioms.filter(a => {
|
|
1305
|
+
// Check if axiom has a human_lock field OR if an evolution lock covers it
|
|
1306
|
+
return a.human_lock || humanLocks.some(hl => hl.lock_type === 'accept');
|
|
1307
|
+
}).length;
|
|
1308
|
+
if (lockedAxioms === 0 && humanLocks.length === 0) {
|
|
1309
|
+
warnings.push('domain has no Human Lock records — judgment-class content may not be human-verified');
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return {
|
|
1314
|
+
passed: failures.length === 0,
|
|
1315
|
+
failures,
|
|
1316
|
+
warnings,
|
|
1317
|
+
riskLevel,
|
|
1318
|
+
specVersion,
|
|
1319
|
+
signatureValid: !isPlaceholder,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate, cmdRoute, checkTrust };
|