@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/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
- const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
42
- const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
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 readJson(p) {
45
- try {
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 listInstalled() {
53
- if (!fs.existsSync(INSTALL_DIR)) return [];
54
- const out = [];
55
- for (const scopeName of fs.readdirSync(INSTALL_DIR)) {
56
- if (!scopeName.startsWith('@')) continue;
57
- const scopeDir = path.join(INSTALL_DIR, scopeName);
58
- try {
59
- if (!fs.statSync(scopeDir).isDirectory()) continue;
60
- for (const ident of fs.readdirSync(scopeDir)) {
61
- if (ident.startsWith('.')) continue;
62
- const dir = path.join(scopeDir, ident);
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 = readJson(path.join(e.dir, 'kdna.json')) || {};
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
- const result = out.length
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 = readJson(path.join(e.dir, 'kdna.json')) || {};
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 parsed = parseName(input);
352
- if (!parsed) {
353
- console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
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
- const dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
357
- if (!fs.existsSync(dir)) {
358
- console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
359
- process.exit(2);
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
- const manifest = readJson(path.join(dir, 'kdna.json')) || {};
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(`${parsed.full}@${manifest.version} has been yanked.`);
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
- const core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
369
- const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
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({ manifest, core, patterns: pat }, null, 2) + '\n');
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: parsed.full,
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 p = path.join(dir, f);
387
- if (fs.existsSync(p)) {
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(fs.readFileSync(p, 'utf8'));
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: parsed.full,
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: parsed.full,
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: parsed.full,
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('## Banned terms');
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 ? ` use: ${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('## Axioms (reason from these)');
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('## Stances');
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('## Banned terms (do not use even if user uses them)');
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 ? ` use: ${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('## Misunderstandings to detect and avoid');
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('## Self-checks (answer before final output)');
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 = readJson(path.join(e.dir, 'kdna.json')) || {};
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 dir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
746
- if (!fs.existsSync(dir)) {
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 core = readJson(path.join(dir, 'KDNA_Core.json')) || {};
752
- const pat = readJson(path.join(dir, 'KDNA_Patterns.json')) || {};
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
- module.exports = { cmdAvailable, cmdMatch, cmdLoad, cmdSelect, cmdPostvalidate };
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 };