@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.
- package/README.md +109 -31
- package/package.json +1 -1
- package/src/agent.js +410 -2
- package/src/cli.js +267 -38
- package/src/cmds/_common.js +65 -3
- package/src/cmds/badge.js +244 -0
- package/src/cmds/changelog.js +226 -0
- package/src/cmds/cluster.js +214 -3
- package/src/cmds/doctor.js +160 -0
- package/src/cmds/domain.js +110 -33
- package/src/cmds/governance.js +471 -0
- package/src/cmds/identity.js +5 -4
- package/src/cmds/quality.js +34 -9
- package/src/cmds/registry.js +62 -22
- package/src/cmds/studio.js +577 -0
- package/src/cmds/test.js +177 -0
- package/src/compare.js +46 -32
- package/src/diff.js +136 -38
- package/src/identity.js +20 -4
- package/src/install.js +181 -91
- package/src/publish.js +4 -3
- package/src/search.js +33 -2
- package/src/verify.js +76 -13
- package/src/version.js +110 -3
package/src/cmds/test.js
ADDED
|
@@ -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(
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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(
|
|
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
|
-
|
|
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}`);
|