@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +73 -0
- package/package.json +58 -0
- package/skills/kdna-loader/SKILL.md +257 -0
- package/src/agent.js +434 -0
- package/src/cli.js +260 -0
- package/src/cluster.js +235 -0
- package/src/cmds/_common.js +100 -0
- package/src/cmds/cluster.js +235 -0
- package/src/cmds/domain.js +638 -0
- package/src/cmds/identity.js +31 -0
- package/src/cmds/legacy.js +83 -0
- package/src/cmds/quality.js +87 -0
- package/src/cmds/registry.js +114 -0
- package/src/cmds/setup.js +8 -0
- package/src/compare.js +324 -0
- package/src/diff.js +288 -0
- package/src/identity.js +211 -0
- package/src/init.js +168 -0
- package/src/install.js +849 -0
- package/src/loader.js +70 -0
- package/src/publish.js +600 -0
- package/src/registry.js +258 -0
- package/src/search.js +73 -0
- package/src/setup.js +197 -0
- package/src/verify.js +423 -0
- package/src/version.js +112 -0
- package/templates/cluster/KDNA_Cluster.json +25 -0
- package/templates/cluster/README.md +32 -0
- package/templates/minimal-domain/KDNA_Core.json +54 -0
- package/templates/minimal-domain/KDNA_Patterns.json +37 -0
- package/templates/minimal-domain/kdna.json +31 -0
- package/templates/minimal-domain/tests/before-after.json +16 -0
- package/templates/standard-domain/KDNA_Core.json +76 -0
- package/templates/standard-domain/KDNA_Patterns.json +44 -0
- package/templates/standard-domain/README.md +74 -0
- package/templates/standard-domain/USAGE.md +59 -0
- package/templates/standard-domain/evals/1_excluded_case.json +16 -0
- package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
- package/templates/standard-domain/evals/3_core_cases.json +35 -0
- package/templates/standard-domain/evals/3_failure_cases.json +35 -0
- package/templates/standard-domain/evals/scoring.json +60 -0
- package/templates/standard-domain/kdna.json +28 -0
- package/validators/kdna-lint.js +53 -0
- 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 };
|