@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
@@ -0,0 +1,83 @@
1
+ const { error } = require('./_common');
2
+
3
+ function cmdPreview() {
4
+ // Removed in v0.9 — no real user scenario for browser preview.
5
+ // To inspect a .kdna file, use: kdna inspect <path>
6
+ error(
7
+ 'kdna preview was removed in v0.9.\n' +
8
+ 'Use: kdna inspect <path> to view a .kdna file or domain directory.',
9
+ );
10
+ }
11
+
12
+ function cmdProject() {
13
+ // Removed in v0.9 — project-level .kdna/config.json violated the
14
+ // "install ≠ load" safety model. KDNA loading is now a per-task
15
+ // decision made by the agent (via kdna-loader skill), not a
16
+ // project-level whitelist.
17
+ error(
18
+ 'kdna project was removed in v0.9. The .kdna/config.json file is no\n' +
19
+ 'longer read by the kdna-loader skill — it would have forced KDNA\n' +
20
+ 'loading on tasks where the user did not ask for it.\n\n' +
21
+ 'The agent now discovers KDNA on demand by reading ~/.kdna/domains/\n' +
22
+ 'and matching the task against v2.1 applies_when fields.\n\n' +
23
+ 'If you have stale .kdna/config.json files in your projects, you\n' +
24
+ 'can delete them — nothing reads them anymore.',
25
+ );
26
+ }
27
+
28
+ function cmdEval() {
29
+ // Removed in v0.9 — overlapped with kdna compare without adding
30
+ // distinct value, and the agent-facing match/load commands cover
31
+ // the discovery path.
32
+ error(
33
+ 'kdna eval was removed in v0.9.\n' +
34
+ 'To compare with/without KDNA reasoning, use:\n' +
35
+ ' kdna compare <name> --input "<task>"\n' +
36
+ 'To inspect a domain, use:\n' +
37
+ ' kdna info <name>',
38
+ );
39
+ }
40
+
41
+ function cmdSelect() {
42
+ // Removed in v0.9 — replaced by the agent-facing kdna-loader skill.
43
+ // The skill discovers KDNA via 'kdna available' and decides fit
44
+ // using v2.1 applies_when fields. The agent makes the selection.
45
+ error(
46
+ 'kdna select was removed in v0.9.\n' +
47
+ 'KDNA selection is now done by the kdna-loader skill (installed\n' +
48
+ 'into your agent at ~/.claude/skills/kdna-loader/ etc.).\n\n' +
49
+ 'To inspect what an agent would see, use:\n' +
50
+ ' kdna available --json\n' +
51
+ ' kdna match "<task>" --json',
52
+ );
53
+ }
54
+
55
+ function cmdExport() {
56
+ // Removed in v0.9 — was an alias for `kdna pack`.
57
+ error(
58
+ 'kdna export was removed in v0.9 (it was an alias for pack).\n' +
59
+ 'Use: kdna pack <path> [--output <dir>]',
60
+ );
61
+ }
62
+
63
+ function cmdDemo() {
64
+ // Removed in v0.9 — internal demo, not a user feature. To see
65
+ // before/after on a real input, use:
66
+ // kdna compare <name> --input "<task>" (requires LLM API key)
67
+ error(
68
+ 'kdna demo was removed in v0.9.\n' +
69
+ 'To see KDNA before/after on a real input, use:\n' +
70
+ ' kdna compare @aikdna/writing --input "<your task>"\n' +
71
+ '(requires ANTHROPIC_API_KEY, OPENAI_API_KEY, or an OpenAI-compatible\n' +
72
+ 'endpoint in ~/.kdna/config.json)',
73
+ );
74
+ }
75
+
76
+ module.exports = {
77
+ cmdPreview,
78
+ cmdProject,
79
+ cmdEval,
80
+ cmdSelect,
81
+ cmdExport,
82
+ cmdDemo,
83
+ };
@@ -0,0 +1,87 @@
1
+ const { error } = require('./_common');
2
+
3
+ function cmdCompare(args) {
4
+ const { cmdCompare } = require('../compare');
5
+ const target = args.filter((a) => !a.startsWith('--'))[1];
6
+ if (!target || !args.includes('--input')) {
7
+ error(
8
+ 'Usage:\n' +
9
+ ' kdna compare <name> --input "<text>"\n' +
10
+ '\n' +
11
+ 'Runs your input through the LLM twice (with/without KDNA loaded),\n' +
12
+ 'then diffs the reasoning trajectory. Requires ANTHROPIC_API_KEY or\n' +
13
+ 'OPENAI_API_KEY in the environment.',
14
+ );
15
+ }
16
+ (async () => {
17
+ try {
18
+ await cmdCompare(target, args);
19
+ } catch (e) {
20
+ console.error(`Error: ${e.message}`);
21
+ process.exit(1);
22
+ }
23
+ })();
24
+ }
25
+
26
+ function cmdDiff(args) {
27
+ const { cmdDiff } = require('../diff');
28
+ const positional = args.filter((a) => !a.startsWith('--'));
29
+ const a = positional[1];
30
+ const b = positional[2];
31
+ if (!a) {
32
+ error(
33
+ 'Usage:\n' +
34
+ ' kdna diff <name>@<v1> <name>@<v2> Compare two versions\n' +
35
+ ' kdna diff <name> Installed vs registry-current\n' +
36
+ '\n' +
37
+ 'Surfaces judgment-level diff: added/removed/changed axioms,\n' +
38
+ 'misunderstandings, banned terms, stances.',
39
+ );
40
+ }
41
+ (async () => {
42
+ try {
43
+ await cmdDiff(a, b);
44
+ } catch (e) {
45
+ console.error(`Error: ${e.message}`);
46
+ process.exit(1);
47
+ }
48
+ })();
49
+ }
50
+
51
+ function cmdSearch(args) {
52
+ const { cmdSearch } = require('../search');
53
+ const query = args.slice(1).join(' ').trim();
54
+ cmdSearch(query);
55
+ }
56
+
57
+ function cmdAvailable(args) {
58
+ const { cmdAvailable } = require('../agent');
59
+ cmdAvailable(args);
60
+ }
61
+
62
+ function cmdMatch(args) {
63
+ const { cmdMatch } = require('../agent');
64
+ const positional = [];
65
+ const flags = [];
66
+ for (let i = 1; i < args.length; i++) {
67
+ if (args[i].startsWith('--')) flags.push(args[i]);
68
+ else positional.push(args[i]);
69
+ }
70
+ cmdMatch(positional.join(' ').trim(), flags);
71
+ }
72
+
73
+ function cmdLoad(args) {
74
+ const { cmdLoad } = require('../agent');
75
+ const target = args.filter((a) => !a.startsWith('--'))[1];
76
+ if (!target) error('Usage: kdna load <name> [--as=prompt|json|raw]');
77
+ cmdLoad(target, args);
78
+ }
79
+
80
+ module.exports = {
81
+ cmdCompare,
82
+ cmdDiff,
83
+ cmdSearch,
84
+ cmdAvailable,
85
+ cmdMatch,
86
+ cmdLoad,
87
+ };
@@ -0,0 +1,114 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { CANONICAL_REGISTRY_URL, REGISTRY_CACHE, fetchRegistry } = require('../registry');
4
+ const { error, readJson, loadRegistry, INSTALL_DIR } = require('./_common');
5
+
6
+ function cmdList(showAvailable) {
7
+ if (showAvailable) {
8
+ const domains = loadRegistry({ allowNetwork: true });
9
+ if (!domains || !domains.length) {
10
+ error('No registry found.');
11
+ }
12
+
13
+ console.log('Available KDNA domains:');
14
+ console.log(`Registry: ${REGISTRY_CACHE}`);
15
+ console.log('');
16
+ for (const d of domains) {
17
+ const name = d.name || d.id || '?';
18
+ const [scope, ident] = name.includes('/') ? name.split('/') : [null, name];
19
+ const installedPath = scope ? path.join(INSTALL_DIR, scope, ident) : null;
20
+ const installed = installedPath && fs.existsSync(installedPath) ? '[installed]' : '';
21
+ const yanked = d.yanked ? '[yanked] ' : '';
22
+ const dep = d.deprecated ? '[deprecated] ' : '';
23
+ console.log(
24
+ ` ${name.padEnd(36)} ${(d.version || '?').padEnd(8)} ${(d.type || 'domain').padEnd(8)} ${(d.status || '').padEnd(14)} ${yanked}${dep}${installed}`,
25
+ );
26
+ if (d.description) console.log(` ${d.description}`);
27
+ console.log('');
28
+ }
29
+ return;
30
+ }
31
+
32
+ if (!fs.existsSync(INSTALL_DIR)) {
33
+ console.log('No domains installed.');
34
+ console.log(`Installation directory: ${INSTALL_DIR}`);
35
+ return;
36
+ }
37
+
38
+ // v0.7 layout: ~/.kdna/domains/@scope/name/
39
+ const scopes = fs.readdirSync(INSTALL_DIR).filter((d) => {
40
+ if (!d.startsWith('@')) return false;
41
+ try {
42
+ return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
43
+ } catch {
44
+ return false;
45
+ }
46
+ });
47
+
48
+ const installed = [];
49
+ for (const scope of scopes) {
50
+ const sd = path.join(INSTALL_DIR, scope);
51
+ for (const ident of fs.readdirSync(sd)) {
52
+ if (ident.startsWith('.')) continue;
53
+ const full = path.join(sd, ident);
54
+ try {
55
+ if (!fs.statSync(full).isDirectory()) continue;
56
+ } catch {
57
+ continue;
58
+ }
59
+ installed.push({ scope, ident, full });
60
+ }
61
+ }
62
+
63
+ // Detect and warn about legacy (un-scoped) installs
64
+ const legacy = fs.readdirSync(INSTALL_DIR).filter((d) => {
65
+ if (d.startsWith('@') || d.startsWith('.')) return false;
66
+ try {
67
+ return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
68
+ } catch {
69
+ return false;
70
+ }
71
+ });
72
+ if (legacy.length) {
73
+ console.log('⚠ Legacy (un-scoped) directories detected — please remove + re-install:');
74
+ legacy.forEach((d) => console.log(` ~/.kdna/domains/${d}/`));
75
+ console.log('');
76
+ }
77
+
78
+ if (!installed.length) {
79
+ console.log('No v0.7 domains installed.');
80
+ console.log(`Run: kdna install <name> # e.g. kdna install writing`);
81
+ return;
82
+ }
83
+
84
+ console.log('Installed KDNA domains:');
85
+ console.log('');
86
+ for (const { scope, ident, full } of installed) {
87
+ const core = readJson(path.join(full, 'KDNA_Core.json'));
88
+ const manifest = readJson(path.join(full, 'kdna.json'));
89
+ const cluster = readJson(path.join(full, 'cluster.json'));
90
+ const name = `${scope}/${ident}`;
91
+ const version = manifest?.version || manifest?._source?.version || core?.meta?.version || '?';
92
+ const kind = cluster ? '[cluster]' : '';
93
+ const desc = manifest?.description || core?.meta?.purpose || '';
94
+ console.log(` ${name.padEnd(36)} v${version} ${kind}`);
95
+ if (desc) console.log(` ${desc}`);
96
+ }
97
+ console.log('');
98
+ console.log(`Location: ${INSTALL_DIR}`);
99
+ }
100
+
101
+ function cmdRegistry(subcommand) {
102
+ if (subcommand !== 'refresh') {
103
+ error('Usage: kdna registry refresh');
104
+ }
105
+ const domains = fetchRegistry();
106
+ console.log(`✓ Registry refreshed from ${CANONICAL_REGISTRY_URL}`);
107
+ console.log(` Cache: ${REGISTRY_CACHE}`);
108
+ console.log(` Domains: ${domains.length}`);
109
+ }
110
+
111
+ module.exports = {
112
+ cmdList,
113
+ cmdRegistry,
114
+ };
@@ -0,0 +1,8 @@
1
+ function cmdSetup() {
2
+ const { cmdSetup } = require('../setup');
3
+ cmdSetup();
4
+ }
5
+
6
+ module.exports = {
7
+ cmdSetup,
8
+ };
package/src/compare.js ADDED
@@ -0,0 +1,324 @@
1
+ /**
2
+ * kdna compare <name> --input "<text>" — Reasoning trajectory diff.
3
+ *
4
+ * Runs the same prompt twice on a real LLM:
5
+ * 1. Without KDNA loaded (baseline)
6
+ * 2. With KDNA injected into the system prompt (treatment)
7
+ * Then asks a third call to diff the two responses along the
8
+ * judgment-trajectory axes the domain claims to change.
9
+ *
10
+ * Config file: ~/.kdna/config.json
11
+ * {
12
+ * "llm": {
13
+ * "provider": "anthropic" | "openai",
14
+ * "model": "<model-id>",
15
+ * "api_key_env": "ANTHROPIC_API_KEY"
16
+ * }
17
+ * }
18
+ *
19
+ * MVP scope: no caching, no batch, no offline mode. One invocation = 3 API calls.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const https = require('https');
25
+
26
+ const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
27
+ const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
28
+ const CONFIG_FILE = path.join(USER_KDNA_DIR, 'config.json');
29
+
30
+ const { parseName } = require('./registry');
31
+
32
+ function readJson(p) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function error(msg) {
41
+ console.error(`Error: ${msg}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ // ─── Config ─────────────────────────────────────────────────────────────
46
+
47
+ function loadLlmConfig() {
48
+ const cfg = readJson(CONFIG_FILE) || {};
49
+ const llm = cfg.llm || {};
50
+ const provider = llm.provider || 'anthropic';
51
+ const model = llm.model || (provider === 'anthropic' ? 'claude-sonnet-4-5' : 'gpt-4o-mini');
52
+ const envName =
53
+ llm.api_key_env || (provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
54
+ const apiKey = process.env[envName] || llm.api_key || null;
55
+
56
+ // base_url lets users point the "openai" provider at any OpenAI-compatible
57
+ // endpoint (SiliconFlow, Groq, OpenRouter, local llama.cpp, etc.).
58
+ // Default: official endpoints for each provider.
59
+ const defaultBase =
60
+ provider === 'anthropic' ? 'https://api.anthropic.com' : 'https://api.openai.com';
61
+ const baseUrl = llm.base_url || defaultBase;
62
+
63
+ if (!apiKey) {
64
+ error(
65
+ `LLM API key not found. Set ${envName} in your environment, or edit ~/.kdna/config.json:\n` +
66
+ ` {\n` +
67
+ ` "llm": {\n` +
68
+ ` "provider": "anthropic" | "openai",\n` +
69
+ ` "model": "<model-id>",\n` +
70
+ ` "api_key_env": "${envName}",\n` +
71
+ ` "base_url": "https://... (optional, for OpenAI-compatible endpoints)"\n` +
72
+ ` }\n` +
73
+ ` }`,
74
+ );
75
+ }
76
+ return { provider, model, apiKey, envName, baseUrl };
77
+ }
78
+
79
+ // Parse "https://host[:port]/path/prefix" → { host, port, basePath }
80
+ function parseBaseUrl(url) {
81
+ const u = new URL(url);
82
+ return {
83
+ host: u.hostname,
84
+ port: u.port ? parseInt(u.port, 10) : 443,
85
+ basePath: u.pathname.replace(/\/$/, ''), // strip trailing slash
86
+ };
87
+ }
88
+
89
+ // ─── HTTP helpers ──────────────────────────────────────────────────────
90
+
91
+ function httpsPost(host, port, pathPart, headers, body) {
92
+ return new Promise((resolve, reject) => {
93
+ const data = JSON.stringify(body);
94
+ const req = https.request(
95
+ {
96
+ host,
97
+ port: port || 443,
98
+ path: pathPart,
99
+ method: 'POST',
100
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(data) },
101
+ },
102
+ (res) => {
103
+ const chunks = [];
104
+ res.on('data', (c) => chunks.push(c));
105
+ res.on('end', () => {
106
+ const text = Buffer.concat(chunks).toString('utf8');
107
+ if (res.statusCode >= 400) {
108
+ reject(new Error(`HTTP ${res.statusCode}: ${text.slice(0, 500)}`));
109
+ return;
110
+ }
111
+ try {
112
+ resolve(JSON.parse(text));
113
+ } catch {
114
+ reject(new Error(`Bad JSON from ${host}: ${text.slice(0, 500)}`));
115
+ }
116
+ });
117
+ },
118
+ );
119
+ req.on('error', reject);
120
+ req.setTimeout(120000, () => req.destroy(new Error(`timeout after 120s`)));
121
+ req.write(data);
122
+ req.end();
123
+ });
124
+ }
125
+
126
+ async function callLlm(cfg, systemPrompt, userMessage) {
127
+ const { host, port, basePath } = parseBaseUrl(cfg.baseUrl);
128
+
129
+ if (cfg.provider === 'anthropic') {
130
+ const resp = await httpsPost(
131
+ host,
132
+ port,
133
+ `${basePath}/v1/messages`,
134
+ {
135
+ 'content-type': 'application/json',
136
+ 'anthropic-version': '2023-06-01',
137
+ 'x-api-key': cfg.apiKey,
138
+ },
139
+ {
140
+ model: cfg.model,
141
+ max_tokens: 4096,
142
+ system: systemPrompt,
143
+ messages: [{ role: 'user', content: userMessage }],
144
+ },
145
+ );
146
+ return resp.content?.map((c) => c.text || '').join('') || '';
147
+ }
148
+ if (cfg.provider === 'openai') {
149
+ // For OpenAI-compatible endpoints the base may already include /v1 (e.g.
150
+ // SiliconFlow: https://api.siliconflow.cn/v1). Append /chat/completions
151
+ // if the basePath doesn't already end with /v1.
152
+ const endpoint = basePath.endsWith('/v1')
153
+ ? `${basePath}/chat/completions`
154
+ : `${basePath}/v1/chat/completions`;
155
+ const resp = await httpsPost(
156
+ host,
157
+ port,
158
+ endpoint,
159
+ {
160
+ 'content-type': 'application/json',
161
+ authorization: `Bearer ${cfg.apiKey}`,
162
+ },
163
+ {
164
+ model: cfg.model,
165
+ max_tokens: 4096,
166
+ messages: [
167
+ { role: 'system', content: systemPrompt },
168
+ { role: 'user', content: userMessage },
169
+ ],
170
+ },
171
+ );
172
+ return resp.choices?.[0]?.message?.content || '';
173
+ }
174
+ error(`Unknown provider: ${cfg.provider}`);
175
+ }
176
+
177
+ // ─── KDNA → system prompt ─────────────────────────────────────────────
178
+
179
+ function buildKdnaPrompt(destDir) {
180
+ const core = readJson(path.join(destDir, 'KDNA_Core.json'));
181
+ const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
182
+ const manifest = readJson(path.join(destDir, 'kdna.json'));
183
+
184
+ if (!core || !pat) return '';
185
+
186
+ const sections = [];
187
+ sections.push(`# Domain judgment loaded: ${manifest?.name || core?.meta?.domain}`);
188
+ sections.push(`# ${core?.meta?.purpose || ''}`);
189
+ sections.push('');
190
+
191
+ if (core.axioms) {
192
+ sections.push('## Axioms (judgment principles)');
193
+ for (const a of core.axioms) {
194
+ sections.push(`- **${a.one_sentence}** ${a.full_statement}`);
195
+ if (a.applies_when?.length) sections.push(` - APPLIES WHEN: ${a.applies_when.join('; ')}`);
196
+ if (a.does_not_apply_when?.length)
197
+ sections.push(` - DOES NOT APPLY WHEN: ${a.does_not_apply_when.join('; ')}`);
198
+ if (a.failure_risk) sections.push(` - FAILURE RISK: ${a.failure_risk}`);
199
+ }
200
+ sections.push('');
201
+ }
202
+
203
+ if (pat.misunderstandings) {
204
+ sections.push('## Common misdiagnoses to avoid');
205
+ for (const m of pat.misunderstandings) {
206
+ sections.push(`- WRONG: ${m.wrong}`);
207
+ sections.push(` CORRECT: ${m.correct}`);
208
+ if (m.key_distinction) sections.push(` KEY DISTINCTION: ${m.key_distinction}`);
209
+ }
210
+ sections.push('');
211
+ }
212
+
213
+ if (pat.self_check?.length) {
214
+ sections.push('## Self-checks before answering');
215
+ pat.self_check.forEach((q, i) => sections.push(`${i + 1}. ${q}`));
216
+ sections.push('');
217
+ }
218
+
219
+ if (core.stances) {
220
+ sections.push('## Stances');
221
+ for (const s of core.stances) {
222
+ const txt = typeof s === 'string' ? s : s.stance;
223
+ if (txt) sections.push(`- ${txt}`);
224
+ }
225
+ }
226
+
227
+ return sections.join('\n');
228
+ }
229
+
230
+ // ─── Diff prompt ───────────────────────────────────────────────────────
231
+
232
+ const DIFF_SYSTEM = `You are comparing two AI responses to the same user request. Your job is NOT to judge which is better, but to surface the difference in REASONING TRAJECTORY along these axes:
233
+
234
+ 1. CLASSIFICATION — how each response classifies the task
235
+ 2. DIAGNOSIS — root cause each response names (surface vs structural)
236
+ 3. ACTIONS — what each response actually suggests doing
237
+ 4. BOUNDARY AWARENESS — does either response recognize when something is outside its scope
238
+ 5. TERMINOLOGY — domain-specific terms one uses but the other doesn't
239
+
240
+ For each axis, output:
241
+ <axis>: <one-line difference> | SAME if no meaningful difference
242
+
243
+ End with a single line:
244
+ VERDICT: <one of: trajectory_changed | trajectory_unchanged | trajectory_degraded>
245
+
246
+ Be terse. Quote at most 8 words from each response.`;
247
+
248
+ function makeDiffPrompt(input, responseA, responseB) {
249
+ return `INPUT (same for both):
250
+ ${input}
251
+
252
+ RESPONSE A (no KDNA loaded):
253
+ ${responseA}
254
+
255
+ RESPONSE B (KDNA loaded):
256
+ ${responseB}
257
+
258
+ Diff the reasoning trajectory.`;
259
+ }
260
+
261
+ // ─── Main ──────────────────────────────────────────────────────────────
262
+
263
+ async function cmdCompare(input, args = []) {
264
+ const idxInput = args.indexOf('--input');
265
+ if (idxInput < 0 || !args[idxInput + 1]) {
266
+ error('Usage: kdna compare <name> --input "<text>"');
267
+ }
268
+ const userInput = args[idxInput + 1];
269
+
270
+ const parsed = parseName(input);
271
+ if (!parsed) error(`Invalid name "${input}".`);
272
+ const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
273
+ if (!fs.existsSync(destDir)) {
274
+ error(`${parsed.full} not installed. Run: kdna install ${input}`);
275
+ }
276
+
277
+ const llm = loadLlmConfig();
278
+
279
+ console.log('═'.repeat(64));
280
+ console.log(` kdna compare ${parsed.full}`);
281
+ console.log(` provider: ${llm.provider} / ${llm.model}`);
282
+ console.log(` input length: ${userInput.length} chars`);
283
+ console.log('═'.repeat(64));
284
+ console.log('');
285
+
286
+ const BASELINE_SYSTEM =
287
+ 'You are a helpful assistant. Respond to the user request concisely and specifically.';
288
+ const kdnaPrompt = buildKdnaPrompt(destDir);
289
+ if (!kdnaPrompt) error('Could not build KDNA prompt — missing KDNA_Core or KDNA_Patterns.');
290
+ const TREATMENT_SYSTEM =
291
+ 'You are a helpful assistant. The following domain judgment is loaded and you MUST apply it when relevant.\n\n' +
292
+ kdnaPrompt;
293
+
294
+ console.log('[1/3] Running baseline (no KDNA)...');
295
+ const responseA = await callLlm(llm, BASELINE_SYSTEM, userInput);
296
+ console.log(` ${responseA.length} chars returned`);
297
+
298
+ console.log('[2/3] Running with KDNA loaded...');
299
+ const responseB = await callLlm(llm, TREATMENT_SYSTEM, userInput);
300
+ console.log(` ${responseB.length} chars returned`);
301
+
302
+ console.log('[3/3] Diffing reasoning trajectories...');
303
+ const diffPrompt = makeDiffPrompt(userInput, responseA, responseB);
304
+ const diff = await callLlm(llm, DIFF_SYSTEM, diffPrompt);
305
+
306
+ console.log('');
307
+ console.log('─'.repeat(64));
308
+ console.log(' WITHOUT KDNA');
309
+ console.log('─'.repeat(64));
310
+ console.log(responseA);
311
+ console.log('');
312
+ console.log('─'.repeat(64));
313
+ console.log(' WITH KDNA');
314
+ console.log('─'.repeat(64));
315
+ console.log(responseB);
316
+ console.log('');
317
+ console.log('─'.repeat(64));
318
+ console.log(' REASONING TRAJECTORY DIFF');
319
+ console.log('─'.repeat(64));
320
+ console.log(diff);
321
+ console.log('');
322
+ }
323
+
324
+ module.exports = { cmdCompare, buildKdnaPrompt };