@aikdna/kdna-cli 0.9.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.
Files changed (46) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +73 -0
  4. package/package.json +58 -0
  5. package/skills/kdna-loader/SKILL.md +257 -0
  6. package/src/agent.js +434 -0
  7. package/src/cli.js +260 -0
  8. package/src/cluster.js +235 -0
  9. package/src/cmds/_common.js +100 -0
  10. package/src/cmds/cluster.js +235 -0
  11. package/src/cmds/domain.js +638 -0
  12. package/src/cmds/identity.js +31 -0
  13. package/src/cmds/legacy.js +83 -0
  14. package/src/cmds/quality.js +87 -0
  15. package/src/cmds/registry.js +114 -0
  16. package/src/cmds/setup.js +8 -0
  17. package/src/compare.js +324 -0
  18. package/src/diff.js +288 -0
  19. package/src/identity.js +211 -0
  20. package/src/init.js +168 -0
  21. package/src/install.js +849 -0
  22. package/src/loader.js +70 -0
  23. package/src/publish.js +600 -0
  24. package/src/registry.js +258 -0
  25. package/src/search.js +73 -0
  26. package/src/setup.js +197 -0
  27. package/src/verify.js +423 -0
  28. package/src/version.js +112 -0
  29. package/templates/cluster/KDNA_Cluster.json +25 -0
  30. package/templates/cluster/README.md +32 -0
  31. package/templates/minimal-domain/KDNA_Core.json +54 -0
  32. package/templates/minimal-domain/KDNA_Patterns.json +37 -0
  33. package/templates/minimal-domain/kdna.json +31 -0
  34. package/templates/minimal-domain/tests/before-after.json +16 -0
  35. package/templates/standard-domain/KDNA_Core.json +76 -0
  36. package/templates/standard-domain/KDNA_Patterns.json +44 -0
  37. package/templates/standard-domain/README.md +74 -0
  38. package/templates/standard-domain/USAGE.md +59 -0
  39. package/templates/standard-domain/evals/1_excluded_case.json +16 -0
  40. package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
  41. package/templates/standard-domain/evals/3_core_cases.json +35 -0
  42. package/templates/standard-domain/evals/3_failure_cases.json +35 -0
  43. package/templates/standard-domain/evals/scoring.json +60 -0
  44. package/templates/standard-domain/kdna.json +28 -0
  45. package/validators/kdna-lint.js +53 -0
  46. package/validators/kdna-validate.js +92 -0
package/src/cluster.js ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * KDNA Cluster — Composable judgment system operations.
3
+ *
4
+ * Commands:
5
+ * cluster lint <path> Validate cluster manifest
6
+ * cluster apply <path> [input] Simulate cluster routing
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function error(msg) {
13
+ console.error(`Error: ${msg}`);
14
+ process.exit(1);
15
+ }
16
+
17
+ function readJson(filePath) {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ // ─── Lint ──────────────────────────────────────────────────────────────
26
+
27
+ function cmdClusterLint(clusterPath) {
28
+ const abs = path.resolve(clusterPath);
29
+ if (!fs.existsSync(abs)) error(`Cluster file not found: ${abs}`);
30
+
31
+ const cluster = readJson(abs);
32
+ if (!cluster) error('Invalid JSON');
33
+ if (!cluster.name) error('Missing cluster name');
34
+ if (!cluster.version) error('Missing cluster version');
35
+ if (!cluster.packages || !Array.isArray(cluster.packages)) error('Missing packages array');
36
+ if (cluster.packages.length < 2) error('Cluster must have ≥2 packages');
37
+
38
+ let warnings = 0;
39
+ let errors = 0;
40
+
41
+ // Check each package
42
+ const roles = ['primary', 'advisor', 'constraint', 'critic'];
43
+ let primaryCount = 0;
44
+
45
+ for (const pkg of cluster.packages) {
46
+ if (!pkg.id) {
47
+ console.error(` ✗ Package missing id`);
48
+ errors++;
49
+ continue;
50
+ }
51
+ if (!pkg.role) {
52
+ console.error(` ✗ ${pkg.id}: missing role`);
53
+ errors++;
54
+ continue;
55
+ }
56
+ if (!roles.includes(pkg.role)) {
57
+ console.error(` ✗ ${pkg.id}: invalid role "${pkg.role}" (must be: ${roles.join(', ')})`);
58
+ errors++;
59
+ }
60
+ if (pkg.role === 'primary') primaryCount++;
61
+ if (!pkg.use_when || !Array.isArray(pkg.use_when) || pkg.use_when.length === 0) {
62
+ console.warn(` ⚠ ${pkg.id}: no use_when conditions (will never be auto-selected)`);
63
+ warnings++;
64
+ }
65
+ }
66
+
67
+ if (primaryCount === 0) {
68
+ console.error(` ✗ No primary package defined (exactly one required)`);
69
+ errors++;
70
+ } else if (primaryCount > 1) {
71
+ console.warn(` ⚠ ${primaryCount} primary packages (typically exactly one)`);
72
+ warnings++;
73
+ }
74
+
75
+ // Check composition rules
76
+ if (!cluster.composition_rules || cluster.composition_rules.length === 0) {
77
+ console.warn(` ⚠ No composition rules defined`);
78
+ warnings++;
79
+ }
80
+
81
+ // Check routing questions
82
+ if (!cluster.routing_questions || cluster.routing_questions.length === 0) {
83
+ console.warn(` ⚠ No routing questions defined`);
84
+ warnings++;
85
+ }
86
+
87
+ // Check for duplicate package ids
88
+ const ids = cluster.packages.map((p) => p.id);
89
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
90
+ if (dupes.length > 0) {
91
+ for (const d of [...new Set(dupes)]) {
92
+ console.error(` ✗ Duplicate package id: "${d}"`);
93
+ errors++;
94
+ }
95
+ }
96
+
97
+ if (errors > 0) {
98
+ console.error(`\n ${errors} error(s), ${warnings} warning(s)`);
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log(`✓ KDNA Cluster valid: ${cluster.name} v${cluster.version}`);
103
+ console.log(
104
+ ` Packages: ${cluster.packages.length} (${primaryCount} primary, ${cluster.packages.filter((p) => p.role === 'advisor').length} advisor, ${cluster.packages.filter((p) => p.role === 'constraint').length} constraint, ${cluster.packages.filter((p) => p.role === 'critic').length} critic)`,
105
+ );
106
+ console.log(` Rules: ${cluster.composition_rules?.length || 0}`);
107
+ console.log(` Routing questions: ${cluster.routing_questions?.length || 0}`);
108
+ if (warnings > 0) console.log(` ${warnings} warning(s)`);
109
+ }
110
+
111
+ // ─── Apply ─────────────────────────────────────────────────────────────
112
+
113
+ function cmdClusterApply(clusterPath, input) {
114
+ const abs = path.resolve(clusterPath);
115
+ if (!fs.existsSync(abs)) error(`Cluster file not found: ${abs}`);
116
+
117
+ const cluster = readJson(abs);
118
+ if (!cluster || !cluster.packages) error('Invalid cluster file');
119
+
120
+ // Use input from argument, or from stdin, or prompt
121
+ let taskInput = input;
122
+ if (!taskInput) {
123
+ // Try reading from stdin if not a TTY
124
+ if (!process.stdin.isTTY) {
125
+ taskInput = fs.readFileSync(0, 'utf8').trim();
126
+ }
127
+ if (!taskInput) {
128
+ console.log('Enter task description (Ctrl+D when done):');
129
+ taskInput = fs.readFileSync(0, 'utf8').trim();
130
+ }
131
+ }
132
+
133
+ if (!taskInput) {
134
+ error('No input provided. Usage: kdna cluster apply <path> "<task description>"');
135
+ }
136
+
137
+ const inputLower = taskInput.toLowerCase();
138
+
139
+ // Select primary: best-matching package by use_when keyword overlap
140
+ const primaryCandidates = cluster.packages.filter((p) => p.role === 'primary');
141
+ const advisorCandidates = cluster.packages.filter((p) => p.role === 'advisor');
142
+ const constraintCandidates = cluster.packages.filter((p) => p.role === 'constraint');
143
+
144
+ function matchScore(pkg) {
145
+ if (!pkg.use_when) return 0;
146
+ let score = 0;
147
+ for (const kw of pkg.use_when) {
148
+ if (inputLower.includes(kw.toLowerCase())) score++;
149
+ }
150
+ return score;
151
+ }
152
+
153
+ // Pick primary with highest keyword match
154
+ const scored = primaryCandidates.map((p) => ({ pkg: p, score: matchScore(p) }));
155
+ scored.sort((a, b) => b.score - a.score);
156
+ const primary = scored[0]?.pkg || primaryCandidates[0];
157
+
158
+ // Pick advisors that match (max 3, score > 0)
159
+ const advisors = advisorCandidates
160
+ .map((p) => ({ pkg: p, score: matchScore(p) }))
161
+ .filter((a) => a.score > 0)
162
+ .sort((a, b) => b.score - a.score)
163
+ .slice(0, 3)
164
+ .map((a) => a.pkg);
165
+
166
+ // Load constraints that match (always active if triggered)
167
+ const constraints = constraintCandidates
168
+ .map((p) => ({ pkg: p, score: matchScore(p) }))
169
+ .filter((c) => c.score > 0)
170
+ .map((c) => c.pkg);
171
+
172
+ // Output
173
+ console.log('═'.repeat(60));
174
+ console.log(` Cluster: ${cluster.name} v${cluster.version}`);
175
+ console.log('═'.repeat(60));
176
+ console.log('');
177
+ console.log(` Task: ${taskInput.length > 80 ? taskInput.slice(0, 80) + '...' : taskInput}`);
178
+ console.log('');
179
+
180
+ console.log(' ── Selected Packages ──');
181
+ console.log('');
182
+ console.log(` Primary: ${primary.id} (${primary.role})`);
183
+ if (primary.use_when) {
184
+ const matched = primary.use_when.filter((kw) => inputLower.includes(kw.toLowerCase()));
185
+ console.log(
186
+ ` Matched: ${matched.length > 0 ? matched.join(', ') : '(fallback — no keyword match)'}`,
187
+ );
188
+ }
189
+
190
+ if (advisors.length > 0) {
191
+ console.log('');
192
+ console.log(` Advisors (${advisors.length}):`);
193
+ for (const a of advisors) {
194
+ const matched = (a.use_when || []).filter((kw) => inputLower.includes(kw.toLowerCase()));
195
+ console.log(` • ${a.id} — matched: ${matched.join(', ')}`);
196
+ }
197
+ }
198
+
199
+ if (constraints.length > 0) {
200
+ console.log('');
201
+ console.log(` Constraints (${constraints.length}):`);
202
+ for (const c of constraints) {
203
+ const matched = (c.use_when || []).filter((kw) => inputLower.includes(kw.toLowerCase()));
204
+ console.log(` • ${c.id} — triggered by: ${matched.join(', ')}`);
205
+ }
206
+ }
207
+
208
+ if (advisors.length === 0 && constraints.length === 0) {
209
+ console.log('');
210
+ console.log(' No advisors or constraints matched. Only primary loaded.');
211
+ }
212
+
213
+ console.log('');
214
+ console.log(' ── Composition Rules ──');
215
+ if (cluster.composition_rules) {
216
+ for (const rule of cluster.composition_rules) {
217
+ console.log(` • ${rule}`);
218
+ }
219
+ }
220
+
221
+ console.log('');
222
+ console.log(' ── Routing Questions (to refine selection) ──');
223
+ if (cluster.routing_questions) {
224
+ for (const q of cluster.routing_questions) {
225
+ console.log(` ? ${q}`);
226
+ }
227
+ }
228
+
229
+ console.log('');
230
+ console.log('═'.repeat(60));
231
+ console.log(` Total packages to load: ${1 + advisors.length + constraints.length}`);
232
+ console.log('═'.repeat(60));
233
+ }
234
+
235
+ module.exports = { cmdClusterLint, cmdClusterApply };
@@ -0,0 +1,100 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { loadRegistry: loadCanonicalRegistry } = require('../registry');
4
+
5
+ const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
6
+ const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
7
+
8
+ function usage() {
9
+ console.log(`kdna — KDNA domain cognition asset tool
10
+
11
+ Usage:
12
+
13
+ --- Domain authors ---
14
+ kdna init <name> Scaffold a new KDNA domain from template
15
+ kdna validate <path> Validate a domain directory
16
+ kdna validate --schema <path> ...with JSON Schema
17
+ kdna pack <path> Pack a domain folder into a .kdna container
18
+ kdna pack --output <dir> <path> Output .kdna to specific directory
19
+ kdna unpack <path> Unpack a .kdna container to a folder
20
+ kdna inspect <path> Inspect a domain directory or .kdna file
21
+ kdna publish <path> Pack + sign + output registry patch
22
+ kdna publish <path> --release-tag <tag> --repo <o/r> ...also upload to GitHub
23
+ kdna publish --check <path> Run quality gate only (no pack/upload)
24
+ kdna version bump <patch|minor|major> [path] Bump domain version
25
+ kdna cluster lint <path> Validate a cluster manifest
26
+
27
+ --- Domain consumers ---
28
+ kdna install <name> Install official domain: @aikdna/<name>
29
+ kdna install @scope/name Install any scoped domain
30
+ kdna install @aikdna/animation Install a cluster (installs all sub-domains)
31
+ kdna install ./file.kdna Install from a local .kdna file
32
+ kdna install ./folder Install from a local directory (dev)
33
+ kdna remove <name> Uninstall a domain
34
+ kdna update <name> Update an installed domain
35
+ kdna update --all Update all installed domains
36
+ kdna info <name> Show version, signature, governance, risks
37
+ kdna list List installed domains
38
+ kdna list --available List available domains from registry
39
+ kdna search <keyword> Search registry by name/keywords/insight
40
+ kdna registry refresh Refresh the canonical registry cache
41
+
42
+ --- Quality + judgment ---
43
+ kdna verify <name> Quality check: structure + trust + judgment
44
+ kdna compare <name> --input "<text>" With/without KDNA reasoning diff
45
+ kdna diff <name>@<v1> <name>@<v2> Judgment-level diff between versions
46
+
47
+ --- Agent-facing (called by the kdna-loader skill) ---
48
+ kdna available [--json] List installed domains + v2.1 fields
49
+ kdna match "<task>" [--json] Hint signals (dropped + weak overlap)
50
+ kdna load <name> [--as=prompt|json|raw] Emit domain in agent-ready format
51
+
52
+ --- Identity ---
53
+ kdna identity init Generate Ed25519 identity key pair
54
+ kdna identity show Display public key and buyer ID
55
+ kdna identity export [--out] Backup private key (passphrase-encrypted)
56
+ kdna identity import <file> Restore identity from backup
57
+
58
+ --- Other ---
59
+ kdna setup One-command setup: CLI + skill + data root
60
+ kdna version Show kdna CLI version
61
+ kdna help Show this help
62
+
63
+ Examples:
64
+ kdna install writing
65
+ kdna verify @aikdna/writing
66
+ kdna available
67
+ kdna init my_domain
68
+ kdna publish ./my_domain --release-tag v0.1.0 --repo yourname/kdna-my_domain`);
69
+ }
70
+
71
+ function error(msg) {
72
+ console.error(`Error: ${msg}`);
73
+ process.exit(1);
74
+ }
75
+
76
+ function readJson(file) {
77
+ try {
78
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function writeJson(file, data) {
85
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
86
+ }
87
+
88
+ function loadRegistry() {
89
+ return loadCanonicalRegistry({ allowNetwork: true });
90
+ }
91
+
92
+ module.exports = {
93
+ USER_KDNA_DIR,
94
+ INSTALL_DIR,
95
+ usage,
96
+ error,
97
+ readJson,
98
+ writeJson,
99
+ loadRegistry,
100
+ };
@@ -0,0 +1,235 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { error, readJson } = require('./_common');
4
+ const {
5
+ loadCluster,
6
+ classifySignalsAcrossDomains,
7
+ composeContextWithAttribution,
8
+ detectDomainConflicts,
9
+ generateClusterTrace,
10
+ } = require('@aikdna/kdna-core');
11
+
12
+ function cmdCluster(args) {
13
+ const { cmdClusterLint } = require('../cluster');
14
+ const sub = args[1];
15
+ const target = args[2];
16
+
17
+ if (sub === 'lint') {
18
+ if (!target) error('Usage: kdna cluster lint <path>');
19
+ cmdClusterLint(target);
20
+ } else if (sub === 'init') {
21
+ const { cmdClusterInit } = require('../init');
22
+ cmdClusterInit(target);
23
+ } else if (sub === 'info') {
24
+ if (!target) error('Usage: kdna cluster info <path>');
25
+ cmdClusterInfo(target);
26
+ } else if (sub === 'load') {
27
+ if (!target) error('Usage: kdna cluster load <cluster.json> --input "<task>"');
28
+ cmdClusterLoad(target, args);
29
+ } else if (sub === 'match') {
30
+ if (!target) error('Usage: kdna cluster match <cluster.json> --input "<task>"');
31
+ cmdClusterMatch(target, args);
32
+ } else if (sub === 'apply') {
33
+ error(
34
+ 'kdna cluster apply was removed in v0.9.\n' +
35
+ 'To install a cluster (which installs all its sub-domains):\n' +
36
+ ' kdna install @aikdna/animation',
37
+ );
38
+ } else {
39
+ error(
40
+ `Unknown cluster subcommand: ${sub || '(none)'}\n` +
41
+ 'Usage: kdna cluster lint <path>\n' +
42
+ ' kdna cluster init <name>\n' +
43
+ ' kdna cluster info <cluster.json>\n' +
44
+ ' kdna cluster match <cluster.json> --input "<task>"\n' +
45
+ ' kdna cluster load <cluster.json> --input "<task>"',
46
+ );
47
+ }
48
+ }
49
+
50
+ function cmdClusterInfo(target, format = 'human') {
51
+ const abs = path.resolve(target);
52
+ if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
53
+
54
+ const manifest = readJson(abs);
55
+ if (!manifest) error(`Invalid cluster manifest (not valid JSON)`);
56
+ if (!manifest.cluster_id) error(`Not a valid cluster manifest (missing cluster_id)`);
57
+
58
+ const domainCount = (manifest.domains || []).length;
59
+ const requiredCount = (manifest.domains || []).filter((d) => d.required !== false).length;
60
+ const composition = manifest.composition || {};
61
+
62
+ console.log(`${manifest.name || manifest.cluster_id}`);
63
+ console.log(` Cluster ID: ${manifest.cluster_id}`);
64
+ console.log(` Version: ${manifest.version || '?'}`);
65
+ console.log(` Type: ${manifest.type || 'horizontal'}`);
66
+ console.log(` Status: ${manifest.status || 'experimental'}`);
67
+ console.log(` Domains: ${domainCount} total, ${requiredCount} required`);
68
+ console.log(` Strategy: ${composition.strategy || 'fixed'}`);
69
+ console.log(` Max active: ${composition.max_active_domains || 'unlimited'}`);
70
+ console.log(` Conflict policy: ${composition.conflict_policy || 'surface'}`);
71
+ console.log('');
72
+
73
+ if (manifest.domains?.length) {
74
+ console.log(' Domain inventory:');
75
+ for (const d of manifest.domains) {
76
+ const req = d.required !== false ? '(required)' : '(optional)';
77
+ console.log(` ${d.role.padEnd(16)} ${d.id} ${req}`);
78
+ }
79
+ console.log('');
80
+ }
81
+
82
+ if (manifest.relationships?.length) {
83
+ console.log(' Relationships:');
84
+ for (const r of manifest.relationships) {
85
+ console.log(` ${r.from} --${r.type}--> ${r.to}`);
86
+ }
87
+ console.log('');
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Load a cluster: resolve domains from installed ~/.kdna/domains/,
93
+ * classify input signals, compose context with attribution, detect
94
+ * conflicts, and emit the composed context.
95
+ */
96
+ function cmdClusterLoad(target, args = []) {
97
+ const abs = path.resolve(target);
98
+ if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
99
+
100
+ const inputIdx = args.indexOf('--input');
101
+ const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
102
+ if (!input) error('Usage: kdna cluster load <cluster.json> --input "<task>"');
103
+
104
+ const manifest = readJson(abs);
105
+ if (!manifest || !manifest.cluster_id) error('Not a valid cluster manifest');
106
+
107
+ const INSTALL_DIR = path.join(
108
+ process.env.HOME || process.env.USERPROFILE || '.',
109
+ '.kdna',
110
+ 'domains',
111
+ );
112
+
113
+ // Domain loader: resolve from installed ~/.kdna/domains/
114
+ const domainLoader = (domainId) => {
115
+ const [scope, ident] = domainId.startsWith('@')
116
+ ? [domainId.slice(0, domainId.indexOf('/')), domainId.slice(domainId.indexOf('/') + 1)]
117
+ : ['@aikdna', domainId];
118
+ const dir = path.join(INSTALL_DIR, scope, ident);
119
+ const core = readJson(path.join(dir, 'KDNA_Core.json'));
120
+ const pat = readJson(path.join(dir, 'KDNA_Patterns.json'));
121
+ if (!core || !pat) return null;
122
+ return { core, patterns: pat };
123
+ };
124
+
125
+ const result = loadCluster(abs, domainLoader);
126
+ if (result.errors.length) {
127
+ console.error('Warnings:');
128
+ result.errors.forEach((e) => console.error(` - ${e}`));
129
+ }
130
+
131
+ // Classify signals
132
+ const classification = classifySignalsAcrossDomains(input, result.domains);
133
+
134
+ console.log(`Cluster: ${manifest.cluster_id}`);
135
+ console.log(`Input: ${input.slice(0, 100)}${input.length > 100 ? '...' : ''}`);
136
+ console.log('');
137
+
138
+ if (classification.excluded.length) {
139
+ console.log(`Excluded domains (${classification.excluded.length}):`);
140
+ classification.excluded.forEach((d) => {
141
+ console.log(` - ${d.id} (${d.reason})`);
142
+ });
143
+ console.log('');
144
+ }
145
+
146
+ if (!classification.selected.length) {
147
+ console.log('No domains matched. Try a different input or check domain trigger_signals.');
148
+ return;
149
+ }
150
+
151
+ console.log(`Selected domains (${classification.selected.length}):`);
152
+ classification.selected.forEach((d) => {
153
+ console.log(` + ${d.id} (${d.role}) ← ${d.reason}`);
154
+ });
155
+ console.log('');
156
+
157
+ // Detect conflicts
158
+ const conflicts = detectDomainConflicts(classification.selected);
159
+ if (conflicts.length) {
160
+ console.log(`Conflicts detected (${conflicts.length}):`);
161
+ conflicts.forEach((c) => {
162
+ console.log(` ⚠ [${c.type}] ${c.domains.join(' vs ')}: ${c.description}`);
163
+ });
164
+ console.log('');
165
+ }
166
+
167
+ // Compose context with attribution
168
+ const { context } = composeContextWithAttribution(classification.selected);
169
+ console.log('─'.repeat(64));
170
+ console.log(context);
171
+ console.log('─'.repeat(64));
172
+
173
+ // Judgment trace
174
+ const trace = generateClusterTrace({
175
+ input,
176
+ loadedDomains: result.domains,
177
+ activeDomains: classification.selected,
178
+ conflicts,
179
+ });
180
+ console.log('');
181
+ console.log('Judgment trace:');
182
+ console.log(JSON.stringify(trace, null, 2));
183
+ }
184
+
185
+ /**
186
+ * Match input against cluster domains without composing full context.
187
+ */
188
+ function cmdClusterMatch(target, args = []) {
189
+ const abs = path.resolve(target);
190
+ if (!fs.existsSync(abs)) error(`Cluster manifest not found: ${abs}`);
191
+
192
+ const inputIdx = args.indexOf('--input');
193
+ const input = inputIdx >= 0 ? args[inputIdx + 1] : '';
194
+ if (!input) error('Usage: kdna cluster match <cluster.json> --input "<task>"');
195
+
196
+ const manifest = readJson(abs);
197
+ if (!manifest || !manifest.cluster_id) error('Not a valid cluster manifest');
198
+
199
+ const INSTALL_DIR = path.join(
200
+ process.env.HOME || process.env.USERPROFILE || '.',
201
+ '.kdna',
202
+ 'domains',
203
+ );
204
+
205
+ const domainLoader = (domainId) => {
206
+ const [scope, ident] = domainId.startsWith('@')
207
+ ? [domainId.slice(0, domainId.indexOf('/')), domainId.slice(domainId.indexOf('/') + 1)]
208
+ : ['@aikdna', domainId];
209
+ const dir = path.join(INSTALL_DIR, scope, ident);
210
+ const core = readJson(path.join(dir, 'KDNA_Core.json'));
211
+ const pat = readJson(path.join(dir, 'KDNA_Patterns.json'));
212
+ if (!core || !pat) return null;
213
+ return { core, patterns: pat };
214
+ };
215
+
216
+ const result = loadCluster(abs, domainLoader);
217
+ const classification = classifySignalsAcrossDomains(input, result.domains);
218
+
219
+ console.log(`Input: ${input.slice(0, 100)}${input.length > 100 ? '...' : ''}`);
220
+ console.log(`Cluster: ${manifest.cluster_id} (${result.domains.length} domains loaded)`);
221
+ console.log('');
222
+ console.log(
223
+ `Matched: ${classification.selected.length} | Excluded: ${classification.excluded.length}`,
224
+ );
225
+ console.log('');
226
+
227
+ classification.selected.forEach((d) => {
228
+ console.log(` ✓ ${d.id} [${d.role}]`);
229
+ });
230
+ classification.excluded.forEach((d) => {
231
+ console.log(` ✗ ${d.id}: ${d.reason}`);
232
+ });
233
+ }
234
+
235
+ module.exports = { cmdCluster };