@aikdna/kdna-cli 0.9.0 → 0.11.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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * KDNA Test commands — Phase 3: Test Lab / Evaluation.
3
+ *
4
+ * kdna test run <domain> --input <file> [--json]
5
+ * Run a test case against a domain, recording results.
6
+ *
7
+ * kdna test import <run-file> --as-eval --out <file>
8
+ * Convert a test run result into an eval card draft.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { error, readJson, writeJson, EXIT } = require('./_common');
14
+ const { parseName } = require('../registry');
15
+
16
+ const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
17
+ const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
18
+ const RUNS_DIR = path.join(USER_KDNA_DIR, 'runs');
19
+
20
+ function cmdTestRun(args = []) {
21
+ const jsonMode = args.includes('--json');
22
+ const positional = args.filter((a) => !a.startsWith('--'));
23
+ const domain = positional[1];
24
+ const inputIdx = args.indexOf('--input');
25
+ const inputFile = inputIdx >= 0 ? args[inputIdx + 1] : null;
26
+ const saveIdx = args.indexOf('--save');
27
+ const saveDir = saveIdx >= 0 ? args[saveIdx + 1] : null;
28
+
29
+ if (!domain || !inputFile) {
30
+ error(
31
+ 'Usage:\n' +
32
+ ' kdna test run <domain> --input <test-file> [--save <dir>] [--json]\n' +
33
+ '\n' +
34
+ 'Runs test input through LLM with/without KDNA and records the result.',
35
+ EXIT.INPUT_ERROR,
36
+ );
37
+ }
38
+
39
+ const parsed = parseName(domain);
40
+ if (!parsed) error(`Invalid name "${domain}".`, EXIT.INPUT_ERROR);
41
+ const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
42
+ if (!fs.existsSync(destDir)) {
43
+ error(`${parsed.full} not installed. Run: kdna install ${domain}`, EXIT.INPUT_ERROR);
44
+ }
45
+
46
+ const absInput = path.resolve(inputFile);
47
+ if (!fs.existsSync(absInput)) error(`Input file not found: ${absInput}`, EXIT.INPUT_ERROR);
48
+
49
+ // Read test case
50
+ let testCase;
51
+ try {
52
+ testCase = JSON.parse(fs.readFileSync(absInput, 'utf8'));
53
+ } catch {
54
+ error(`Invalid JSON in test file: ${absInput}`, EXIT.INPUT_ERROR);
55
+ }
56
+
57
+ // Validate test case structure
58
+ const expectedClassification = testCase.expected?.classification;
59
+ const expectedTriggeredAxioms = testCase.expected?.triggered_axioms;
60
+ const expectedAvoidedMisunderstandings = testCase.expected?.avoided_misunderstandings;
61
+ const expectedAvoidedBannedTerms = testCase.expected?.avoided_banned_terms;
62
+
63
+ // Build test result
64
+ const result = {
65
+ test_id: testCase.id || `test_${Date.now()}`,
66
+ domain: parsed.full,
67
+ domain_path: destDir,
68
+ input: typeof testCase.input === 'string' ? testCase.input : JSON.stringify(testCase.input),
69
+ run_at: new Date().toISOString(),
70
+ expected: {
71
+ classification: expectedClassification || null,
72
+ triggered_axioms: expectedTriggeredAxioms || [],
73
+ avoided_misunderstandings: expectedAvoidedMisunderstandings || [],
74
+ avoided_banned_terms: expectedAvoidedBannedTerms || [],
75
+ },
76
+ results: {
77
+ classification: null,
78
+ triggered_axioms: [],
79
+ avoided_misunderstandings: [],
80
+ avoided_banned_terms: [],
81
+ self_checks: [],
82
+ risk_flags: [],
83
+ },
84
+ human_grade: null,
85
+ human_notes: null,
86
+ };
87
+
88
+ /**
89
+ * Note: Full LLM-based compare can be run separately via:
90
+ * kdna compare <domain> --input "<text>"
91
+ * Test run records the structure for human grading.
92
+ */
93
+
94
+ // Save result
95
+ if (saveDir) {
96
+ const outDir = path.resolve(saveDir);
97
+ fs.mkdirSync(outDir, { recursive: true });
98
+ const outFile = path.join(outDir, `run-${result.test_id}.json`);
99
+ writeJson(outFile, result);
100
+ if (!jsonMode) console.log(`Test result saved: ${outFile}`);
101
+ result.saved_to = outFile;
102
+ } else {
103
+ const outDir = RUNS_DIR;
104
+ fs.mkdirSync(outDir, { recursive: true });
105
+ const outFile = path.join(outDir, `run-${result.test_id}.json`);
106
+ writeJson(outFile, result);
107
+ if (!jsonMode) console.log(`Test result saved: ${outFile}`);
108
+ result.saved_to = outFile;
109
+ }
110
+
111
+ if (jsonMode) {
112
+ console.log(JSON.stringify(result, null, 2));
113
+ }
114
+
115
+ if (!jsonMode) {
116
+ console.log(`Test run recorded: ${result.test_id}`);
117
+ console.log(` Domain: ${result.domain}`);
118
+ console.log(` Input: ${result.input.slice(0, 100)}${result.input.length > 100 ? '...' : ''}`);
119
+ if (result.expected.classification) console.log(` Expected classification: ${result.expected.classification}`);
120
+ }
121
+ }
122
+
123
+ function cmdTestImport(args = []) {
124
+ const positional = args.filter((a) => !a.startsWith('--'));
125
+ const runFile = positional[1];
126
+ const outIdx = args.indexOf('--out');
127
+ const outFile = outIdx >= 0 ? args[outIdx + 1] : null;
128
+ const asEval = args.includes('--as-eval');
129
+
130
+ if (!runFile) {
131
+ error('Usage: kdna test import <run-file> --as-eval --out <file>', EXIT.INPUT_ERROR);
132
+ }
133
+
134
+ const abs = path.resolve(runFile);
135
+ if (!fs.existsSync(abs)) error(`Run file not found: ${abs}`, EXIT.INPUT_ERROR);
136
+
137
+ const runData = readJson(abs);
138
+ if (!runData || !runData.test_id) error(`Not a valid test run file: ${abs}`, EXIT.INPUT_ERROR);
139
+
140
+ if (asEval) {
141
+ // Convert run result into an eval card draft
142
+ const evalCard = {
143
+ id: `eval_${runData.test_id}`,
144
+ type: 'eval_case',
145
+ domain: runData.domain,
146
+ input: runData.input,
147
+ expected_classification: runData.expected?.classification || null,
148
+ expected_triggered_axioms: runData.expected?.triggered_axioms || [],
149
+ expected_avoided_misunderstandings: runData.expected?.avoided_misunderstandings || [],
150
+ expected_avoided_banned_terms: runData.expected?.avoided_banned_terms || [],
151
+ actual_classification: runData.results?.classification || null,
152
+ actual_triggered_axioms: runData.results?.triggered_axioms || [],
153
+ actual_avoided_misunderstandings: runData.results?.avoided_misunderstandings || [],
154
+ actual_avoided_banned_terms: runData.results?.avoided_banned_terms || [],
155
+ human_grade: runData.human_grade || null,
156
+ human_notes: runData.human_notes || null,
157
+ source_run: path.basename(abs),
158
+ created: new Date().toISOString(),
159
+ };
160
+
161
+ const outPath = outFile
162
+ ? path.resolve(outFile)
163
+ : path.join(path.dirname(abs), `eval-${runData.test_id}.json`);
164
+
165
+ writeJson(outPath, evalCard);
166
+ console.log(`Eval card created: ${outPath}`);
167
+ console.log(` ID: ${evalCard.id}`);
168
+ console.log(` Domain: ${evalCard.domain}`);
169
+ if (evalCard.expected_classification) {
170
+ console.log(` Expected: ${evalCard.expected_classification}`);
171
+ }
172
+ } else {
173
+ console.log(JSON.stringify(runData, null, 2));
174
+ }
175
+ }
176
+
177
+ module.exports = { cmdTestRun, cmdTestImport };
package/src/compare.js CHANGED
@@ -28,6 +28,7 @@ const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
28
28
  const CONFIG_FILE = path.join(USER_KDNA_DIR, 'config.json');
29
29
 
30
30
  const { parseName } = require('./registry');
31
+ const { EXIT } = require('./cmds/_common');
31
32
 
32
33
  function readJson(p) {
33
34
  try {
@@ -37,9 +38,9 @@ function readJson(p) {
37
38
  }
38
39
  }
39
40
 
40
- function error(msg) {
41
+ function error(msg, code = EXIT.VALIDATION_FAILED) {
41
42
  console.error(`Error: ${msg}`);
42
- process.exit(1);
43
+ process.exit(code);
43
44
  }
44
45
 
45
46
  // ─── Config ─────────────────────────────────────────────────────────────
@@ -71,6 +72,7 @@ function loadLlmConfig() {
71
72
  ` "base_url": "https://... (optional, for OpenAI-compatible endpoints)"\n` +
72
73
  ` }\n` +
73
74
  ` }`,
75
+ EXIT.PROVIDER_ERROR,
74
76
  );
75
77
  }
76
78
  return { provider, model, apiKey, envName, baseUrl };
@@ -261,27 +263,30 @@ Diff the reasoning trajectory.`;
261
263
  // ─── Main ──────────────────────────────────────────────────────────────
262
264
 
263
265
  async function cmdCompare(input, args = []) {
266
+ const jsonMode = args.includes('--json');
264
267
  const idxInput = args.indexOf('--input');
265
268
  if (idxInput < 0 || !args[idxInput + 1]) {
266
- error('Usage: kdna compare <name> --input "<text>"');
269
+ error('Usage: kdna compare <name> --input "<text>"', EXIT.INPUT_ERROR);
267
270
  }
268
271
  const userInput = args[idxInput + 1];
269
272
 
270
273
  const parsed = parseName(input);
271
- if (!parsed) error(`Invalid name "${input}".`);
274
+ if (!parsed) error(`Invalid name "${input}".`, EXIT.INPUT_ERROR);
272
275
  const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
273
276
  if (!fs.existsSync(destDir)) {
274
- error(`${parsed.full} not installed. Run: kdna install ${input}`);
277
+ error(`${parsed.full} not installed. Run: kdna install ${input}`, EXIT.INPUT_ERROR);
275
278
  }
276
279
 
277
280
  const llm = loadLlmConfig();
278
281
 
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('');
282
+ if (!jsonMode) {
283
+ console.log('═'.repeat(64));
284
+ console.log(` kdna compare ${parsed.full}`);
285
+ console.log(` provider: ${llm.provider} / ${llm.model}`);
286
+ console.log(` input length: ${userInput.length} chars`);
287
+ console.log(''.repeat(64));
288
+ console.log('');
289
+ }
285
290
 
286
291
  const BASELINE_SYSTEM =
287
292
  'You are a helpful assistant. Respond to the user request concisely and specifically.';
@@ -291,34 +296,43 @@ async function cmdCompare(input, args = []) {
291
296
  'You are a helpful assistant. The following domain judgment is loaded and you MUST apply it when relevant.\n\n' +
292
297
  kdnaPrompt;
293
298
 
294
- console.log('[1/3] Running baseline (no KDNA)...');
299
+ if (!jsonMode) console.log('[1/3] Running baseline (no KDNA)...');
295
300
  const responseA = await callLlm(llm, BASELINE_SYSTEM, userInput);
296
- console.log(` ${responseA.length} chars returned`);
301
+ if (!jsonMode) console.log(` ${responseA.length} chars returned`);
297
302
 
298
- console.log('[2/3] Running with KDNA loaded...');
303
+ if (!jsonMode) console.log('[2/3] Running with KDNA loaded...');
299
304
  const responseB = await callLlm(llm, TREATMENT_SYSTEM, userInput);
300
- console.log(` ${responseB.length} chars returned`);
305
+ if (!jsonMode) console.log(` ${responseB.length} chars returned`);
301
306
 
302
- console.log('[3/3] Diffing reasoning trajectories...');
307
+ if (!jsonMode) console.log('[3/3] Diffing reasoning trajectories...');
303
308
  const diffPrompt = makeDiffPrompt(userInput, responseA, responseB);
304
309
  const diff = await callLlm(llm, DIFF_SYSTEM, diffPrompt);
305
310
 
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('');
311
+ if (jsonMode) {
312
+ const result = {
313
+ baseline_output: responseA,
314
+ kdna_output: responseB,
315
+ judgment_delta: diff,
316
+ };
317
+ console.log(JSON.stringify(result, null, 2));
318
+ } else {
319
+ console.log('');
320
+ console.log('─'.repeat(64));
321
+ console.log(' WITHOUT KDNA');
322
+ console.log('─'.repeat(64));
323
+ console.log(responseA);
324
+ console.log('');
325
+ console.log('─'.repeat(64));
326
+ console.log(' WITH KDNA');
327
+ console.log('─'.repeat(64));
328
+ console.log(responseB);
329
+ console.log('');
330
+ console.log('─'.repeat(64));
331
+ console.log(' REASONING TRAJECTORY DIFF');
332
+ console.log('─'.repeat(64));
333
+ console.log(diff);
334
+ console.log('');
335
+ }
322
336
  }
323
337
 
324
338
  module.exports = { cmdCompare, buildKdnaPrompt };
package/src/diff.js CHANGED
@@ -20,14 +20,15 @@ const fs = require('fs');
20
20
  const path = require('path');
21
21
  const { execSync, execFileSync } = require('child_process');
22
22
  const { RegistryResolver, parseName } = require('./registry');
23
+ const { EXIT } = require('./cmds/_common');
23
24
 
24
25
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
25
26
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
26
27
  const TMP_DIR = '/tmp';
27
28
 
28
- function error(msg) {
29
+ function error(msg, code = EXIT.VALIDATION_FAILED) {
29
30
  console.error(`Error: ${msg}`);
30
- process.exit(1);
31
+ process.exit(code);
31
32
  }
32
33
 
33
34
  function readJson(p) {
@@ -79,7 +80,7 @@ function downloadAndExtract(url, destDir) {
79
80
  stdio: 'pipe',
80
81
  });
81
82
  } catch (e) {
82
- error(`Failed to download ${url}: ${e.stderr?.toString().trim() || e.message}`);
83
+ error(`Failed to download ${url}: ${e.stderr?.toString().trim() || e.message}`, EXIT.PROVIDER_ERROR);
83
84
  }
84
85
 
85
86
  fs.mkdirSync(destDir, { recursive: true });
@@ -117,7 +118,7 @@ function loadJudgment(domainDir) {
117
118
 
118
119
  // ─── Set diff helpers ─────────────────────────────────────────────────
119
120
 
120
- function diffMaps(label, oldMap, newMap, render) {
121
+ function diffMaps(label, oldMap, newMap, render, jsonMode = false) {
121
122
  const oldIds = new Set(Object.keys(oldMap));
122
123
  const newIds = new Set(Object.keys(newMap));
123
124
  const added = [...newIds].filter((id) => !oldIds.has(id));
@@ -125,7 +126,24 @@ function diffMaps(label, oldMap, newMap, render) {
125
126
  const both = [...newIds].filter((id) => oldIds.has(id));
126
127
  const changed = both.filter((id) => JSON.stringify(oldMap[id]) !== JSON.stringify(newMap[id]));
127
128
 
128
- const out = { label, added, removed, changed };
129
+ // Collect boundary-level diffs for JSON output
130
+ const changedDetails = changed.map((id) => {
131
+ const a = oldMap[id],
132
+ b = newMap[id];
133
+ const boundaryChanges = {};
134
+ for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk', 'confidence']) {
135
+ const before = a[field] ?? null;
136
+ const after = b[field] ?? null;
137
+ if (JSON.stringify(before) !== JSON.stringify(after)) {
138
+ boundaryChanges[field] = { before, after };
139
+ }
140
+ }
141
+ return { id, before: a, after: b, boundary_changes: boundaryChanges };
142
+ });
143
+
144
+ const out = { label, added, removed, changed, changedDetails };
145
+
146
+ if (jsonMode) return out;
129
147
 
130
148
  console.log('');
131
149
  console.log('─'.repeat(64));
@@ -162,34 +180,39 @@ function diffMaps(label, oldMap, newMap, render) {
162
180
  return out;
163
181
  }
164
182
 
165
- function diffStanceList(oldList, newList) {
183
+ function diffStanceList(oldList, newList, jsonMode = false) {
166
184
  const oldSet = new Set(oldList);
167
185
  const newSet = new Set(newList);
168
186
  const added = newList.filter((s) => !oldSet.has(s));
169
187
  const removed = oldList.filter((s) => !newSet.has(s));
188
+ const out = { added, removed };
189
+ if (jsonMode) return out;
170
190
  console.log('');
171
191
  console.log('─'.repeat(64));
172
192
  console.log(` STANCES added:${added.length} removed:${removed.length}`);
173
193
  console.log('─'.repeat(64));
174
194
  for (const s of added) console.log(` + "${s}"`);
175
195
  for (const s of removed) console.log(` - "${s}"`);
196
+ return out;
176
197
  }
177
198
 
178
199
  // ─── Main ──────────────────────────────────────────────────────────────
179
200
 
180
- async function cmdDiff(a, b) {
181
- if (!a) error('Usage: kdna diff <name>@<v1> <name>@<v2> or kdna diff <name>');
201
+ async function cmdDiff(a, b, args = []) {
202
+ const jsonMode = args.includes('--json');
203
+
204
+ if (!a) error('Usage: kdna diff <name>@<v1> <name>@<v2> or kdna diff <name>', EXIT.INPUT_ERROR);
182
205
 
183
206
  const aParsed = parseNameVersion(a);
184
207
  const bParsed = b ? parseNameVersion(b) : null;
185
- if (!aParsed) error(`Cannot parse "${a}"`);
208
+ if (!aParsed) error(`Cannot parse "${a}"`, EXIT.INPUT_ERROR);
186
209
 
187
210
  const resolver = new RegistryResolver({ allowNetwork: true });
188
211
  let entryA;
189
212
  try {
190
213
  ({ entry: entryA } = resolver.resolve(aParsed.full));
191
214
  } catch (e) {
192
- error(e.message);
215
+ error(e.message, EXIT.REGISTRY_ERROR);
193
216
  }
194
217
 
195
218
  // Determine targets
@@ -199,10 +222,10 @@ async function cmdDiff(a, b) {
199
222
  try {
200
223
  ({ entry: entryB } = resolver.resolve(bParsed.full));
201
224
  } catch (e) {
202
- error(e.message);
225
+ error(e.message, EXIT.REGISTRY_ERROR);
203
226
  }
204
227
  if (aParsed.full !== bParsed.full)
205
- error('Comparing across different domains is not supported.');
228
+ error('Comparing across different domains is not supported.', EXIT.INPUT_ERROR);
206
229
  oldVersion = aParsed.version || entryA.version;
207
230
  newVersion = bParsed.version || entryB.version;
208
231
  oldEntry = entryA;
@@ -212,7 +235,7 @@ async function cmdDiff(a, b) {
212
235
  const parsed = parseName(aParsed.full);
213
236
  const localDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
214
237
  if (!fs.existsSync(localDir)) {
215
- error(`${aParsed.full} not installed. Run: kdna install ${aParsed.full}`);
238
+ error(`${aParsed.full} not installed. Run: kdna install ${aParsed.full}`, EXIT.INPUT_ERROR);
216
239
  }
217
240
  const localManifest = readJson(path.join(localDir, 'kdna.json'));
218
241
  oldVersion = localManifest?.version || '?';
@@ -220,6 +243,10 @@ async function cmdDiff(a, b) {
220
243
  oldEntry = entryA;
221
244
  newEntry = entryA;
222
245
  if (oldVersion === newVersion) {
246
+ if (jsonMode) {
247
+ console.log(JSON.stringify({ error: `${aParsed.full}@${oldVersion}: only one version found.` }));
248
+ process.exit(EXIT.OK);
249
+ }
223
250
  console.log(
224
251
  `${aParsed.full}@${oldVersion}: only one version found.\n` +
225
252
  `To compare across versions, specify two: kdna diff ${aParsed.full}@${oldVersion} ${aParsed.full}@<other>`,
@@ -228,41 +255,46 @@ async function cmdDiff(a, b) {
228
255
  }
229
256
  }
230
257
 
231
- console.log('═'.repeat(64));
232
- console.log(` kdna diff ${aParsed.full}`);
233
- console.log(` ${oldVersion} ${newVersion}`);
234
- console.log('═'.repeat(64));
258
+ if (!jsonMode) {
259
+ console.log('═'.repeat(64));
260
+ console.log(` kdna diff ${aParsed.full}`);
261
+ console.log(` ${oldVersion} → ${newVersion}`);
262
+ console.log('═'.repeat(64));
263
+ }
235
264
 
236
265
  // Download both versions to temp dirs
237
266
  const tmpOld = path.join(TMP_DIR, `kdna-diff-${Date.now()}-old`);
238
267
  const tmpNew = path.join(TMP_DIR, `kdna-diff-${Date.now()}-new`);
239
268
 
240
- console.log('Downloading old version...');
269
+ if (!jsonMode) console.log('Downloading old version...');
241
270
  downloadVersion(oldEntry, oldVersion, tmpOld);
242
- console.log('Downloading new version...');
271
+ if (!jsonMode) console.log('Downloading new version...');
243
272
  downloadVersion(newEntry, newVersion, tmpNew);
244
273
 
245
274
  const oldJ = loadJudgment(tmpOld);
246
275
  const newJ = loadJudgment(tmpNew);
247
276
 
248
- console.log('');
249
- console.log(
250
- ' judgment_version: ' +
251
- (oldJ.judgment_version || '(not declared)') +
252
- '' +
253
- (newJ.judgment_version || '(not declared)'),
254
- );
277
+ if (!jsonMode) {
278
+ console.log('');
279
+ console.log(
280
+ ' judgment_version: ' +
281
+ (oldJ.judgment_version || '(not declared)') +
282
+ ' → ' +
283
+ (newJ.judgment_version || '(not declared)'),
284
+ );
285
+ }
255
286
 
256
- diffMaps('axioms', oldJ.axioms, newJ.axioms, (a) => a.one_sentence || a.id);
257
- diffMaps('ontology', oldJ.ontology, newJ.ontology, (o) => o.one_sentence || o.id);
258
- diffMaps(
287
+ const axiomsDiff = diffMaps('axioms', oldJ.axioms, newJ.axioms, (a) => a.one_sentence || a.id, jsonMode);
288
+ const ontologyDiff = diffMaps('ontology', oldJ.ontology, newJ.ontology, (o) => o.one_sentence || o.id, jsonMode);
289
+ const misunderstandingsDiff = diffMaps(
259
290
  'misunderstandings',
260
291
  oldJ.misunderstandings,
261
292
  newJ.misunderstandings,
262
293
  (m) => m.wrong || m.id,
294
+ jsonMode,
263
295
  );
264
- diffMaps('banned_terms', oldJ.banned_terms, newJ.banned_terms, (t) => t.term || '');
265
- diffStanceList(oldJ.stances, newJ.stances);
296
+ const bannedDiff = diffMaps('banned_terms', oldJ.banned_terms, newJ.banned_terms, (t) => t.term || '', jsonMode);
297
+ const stancesDiff = diffStanceList(oldJ.stances, newJ.stances, jsonMode);
266
298
 
267
299
  // Cleanup
268
300
  try {
@@ -276,13 +308,79 @@ async function cmdDiff(a, b) {
276
308
  /* ignore */
277
309
  }
278
310
 
279
- console.log('');
280
- console.log('═'.repeat(64));
281
- const drift = Object.keys(newJ.axioms).length - Object.keys(oldJ.axioms).length;
282
- const note = drift !== 0 ? ` (axiom count drift: ${drift > 0 ? '+' : ''}${drift})` : '';
283
- console.log(` Judgment surface change: ${oldVersion} → ${newVersion}${note}`);
284
- console.log(` Agent loading the new version may classify, diagnose, or recommend differently.`);
285
- console.log('═'.repeat(64));
311
+ // Derive structured JSON fields
312
+ const changedAxioms = axiomsDiff.changedDetails.map((d) => ({
313
+ id: d.id,
314
+ changes: d.boundary_changes,
315
+ }));
316
+
317
+ const changedBoundaries = axiomsDiff.changedDetails
318
+ .filter((d) => Object.keys(d.boundary_changes).length > 0)
319
+ .map((d) => ({
320
+ axiom_id: d.id,
321
+ boundary_changes: d.boundary_changes,
322
+ }));
323
+
324
+ const newMisunderstandings = misunderstandingsDiff.added;
325
+ const deprecatedSelfChecks = []; // self_checks are not part of diffMaps; would need separate tracking
326
+
327
+ const riskModelChanges = axiomsDiff.changedDetails
328
+ .filter((d) => d.boundary_changes.failure_risk)
329
+ .map((d) => ({
330
+ axiom_id: d.id,
331
+ before: d.boundary_changes.failure_risk.before,
332
+ after: d.boundary_changes.failure_risk.after,
333
+ }));
334
+
335
+ const affectedScenarios = axiomsDiff.changedDetails
336
+ .filter(
337
+ (d) =>
338
+ d.boundary_changes.applies_when ||
339
+ d.boundary_changes.does_not_apply_when,
340
+ )
341
+ .map((d) => ({
342
+ axiom_id: d.id,
343
+ applies_when: d.boundary_changes.applies_when || null,
344
+ does_not_apply_when: d.boundary_changes.does_not_apply_when || null,
345
+ }));
346
+
347
+ // Determine recommended version bump
348
+ const axiomDrift = Object.keys(newJ.axioms).length - Object.keys(oldJ.axioms).length;
349
+ const hasRemoved = axiomsDiff.removed.length > 0 || misunderstandingsDiff.removed.length > 0;
350
+ const hasAdded = axiomsDiff.added.length > 0 || misunderstandingsDiff.added.length > 0;
351
+ const hasChanged = axiomsDiff.changed.length > 0 || bannedDiff.changed.length > 0;
352
+ let recommendedVersionBump = 'none';
353
+ if (hasRemoved) recommendedVersionBump = 'major';
354
+ else if (hasAdded || hasChanged) recommendedVersionBump = 'minor';
355
+ else if (stancesDiff.added.length > 0 || stancesDiff.removed.length > 0) recommendedVersionBump = 'patch';
356
+
357
+ if (jsonMode) {
358
+ const result = {
359
+ domain: aParsed.full,
360
+ old_version: oldVersion,
361
+ new_version: newVersion,
362
+ judgment_version: {
363
+ before: oldJ.judgment_version || null,
364
+ after: newJ.judgment_version || null,
365
+ },
366
+ changed_axioms: changedAxioms,
367
+ changed_boundaries: changedBoundaries,
368
+ new_misunderstandings: newMisunderstandings,
369
+ deprecated_self_checks: deprecatedSelfChecks,
370
+ risk_model_changes: riskModelChanges,
371
+ affected_scenarios: affectedScenarios,
372
+ recommended_version_bump: recommendedVersionBump,
373
+ };
374
+ console.log(JSON.stringify(result, null, 2));
375
+ } else {
376
+ console.log('');
377
+ console.log('═'.repeat(64));
378
+ const drift = Object.keys(newJ.axioms).length - Object.keys(oldJ.axioms).length;
379
+ const note = drift !== 0 ? ` (axiom count drift: ${drift > 0 ? '+' : ''}${drift})` : '';
380
+ console.log(` Judgment surface change: ${oldVersion} → ${newVersion}${note}`);
381
+ console.log(` Agent loading the new version may classify, diagnose, or recommend differently.`);
382
+ console.log('═'.repeat(64));
383
+ }
286
384
  }
287
385
 
288
386
  module.exports = { cmdDiff, loadJudgment, parseNameVersion };
package/src/identity.js CHANGED
@@ -11,6 +11,7 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const crypto = require('crypto');
14
+ const { EXIT } = require('./cmds/_common');
14
15
 
15
16
  const IDENTITY_DIR =
16
17
  process.env.KDNA_IDENTITY_DIR ||
@@ -19,9 +20,9 @@ const IDENTITY_DIR =
19
20
  const PRIVATE_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.key');
20
21
  const PUBLIC_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.pub');
21
22
 
22
- function error(msg) {
23
+ function error(msg, code = EXIT.VALIDATION_FAILED) {
23
24
  console.error(`Error: ${msg}`);
24
- process.exit(1);
25
+ process.exit(code);
25
26
  }
26
27
 
27
28
  // ─── Key Generation ────────────────────────────────────────────────────
@@ -75,15 +76,30 @@ function cmdIdentityInit() {
75
76
 
76
77
  // ─── Show ──────────────────────────────────────────────────────────────
77
78
 
78
- function cmdIdentityShow() {
79
+ function cmdIdentityShow(jsonMode = false) {
79
80
  if (!fs.existsSync(PUBLIC_KEY_PATH)) {
80
- error('No identity found. Run: kdna identity init');
81
+ if (jsonMode) {
82
+ console.log(JSON.stringify({ error: 'No identity found. Run: kdna identity init' }));
83
+ process.exit(EXIT.INPUT_ERROR);
84
+ }
85
+ error('No identity found. Run: kdna identity init', EXIT.INPUT_ERROR);
81
86
  }
82
87
 
83
88
  const pub = fs.readFileSync(PUBLIC_KEY_PATH, 'utf8');
84
89
  const id = deriveBuyerId(pub);
85
90
  const fp = fingerprint(pub);
86
91
 
92
+ if (jsonMode) {
93
+ console.log(JSON.stringify({
94
+ pubkey: pub.trim(),
95
+ buyer_id: id,
96
+ fingerprint: fp,
97
+ public_key_path: PUBLIC_KEY_PATH,
98
+ private_key_exists: fs.existsSync(PRIVATE_KEY_PATH),
99
+ }));
100
+ process.exit(EXIT.OK);
101
+ }
102
+
87
103
  console.log(`Buyer ID: ${id}`);
88
104
  console.log(`Fingerprint: ${fp}`);
89
105
  console.log(`Public key: ${PUBLIC_KEY_PATH}`);