@aikdna/kdna-cli 0.12.0 → 0.14.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/package.json +1 -1
- package/src/cli.js +46 -4
- package/src/cmds/_common.js +2 -1
- package/src/cmds/domain.js +237 -0
- package/src/cmds/encrypt.js +190 -0
- package/src/cmds/license.js +178 -0
- package/src/compare.js +217 -7
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { error, EXIT, setQuiet, setExitCodeOnly } = require('./cmds/_common');
|
|
10
|
-
const { cmdValidate, cmdPack, cmdUnpack, cmdInspect } = require('./cmds/domain');
|
|
10
|
+
const { cmdValidate, cmdPack, cmdPackEncrypt, cmdUnpack, cmdUnpackEncrypt, cmdInspect } = require('./cmds/domain');
|
|
11
11
|
const { cmdList, cmdRegistry } = require('./cmds/registry');
|
|
12
12
|
const {
|
|
13
13
|
cmdCompare,
|
|
@@ -24,6 +24,7 @@ const { cmdIdentity } = require('./cmds/identity');
|
|
|
24
24
|
const { cmdSetup } = require('./cmds/setup');
|
|
25
25
|
const { cmdDoctor } = require('./cmds/doctor');
|
|
26
26
|
const { cmdTrace, cmdHistory } = require('./cmds/trace');
|
|
27
|
+
const { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow } = require('./cmds/license');
|
|
27
28
|
const { cmdPreview, cmdProject, cmdEval, cmdExport, cmdDemo } = require('./cmds/legacy');
|
|
28
29
|
const { cmdStudioScaffold, cmdCardsValidate, cmdLockVerify, cmdStudioCompile, cmdStudioReadiness } = require('./cmds/studio');
|
|
29
30
|
const { cmdTestRun, cmdTestImport } = require('./cmds/test');
|
|
@@ -54,7 +55,9 @@ Domain Authoring:
|
|
|
54
55
|
validate <path> Validate domain structure
|
|
55
56
|
validate --schema <path> Schema-only validation
|
|
56
57
|
pack <path> Pack into .kdna container
|
|
58
|
+
pack <path> --encrypt --license <file> Pack encrypted .kdnae container
|
|
57
59
|
unpack <file> Unpack .kdna container
|
|
60
|
+
unpack <file> --license <file> Unpack encrypted .kdnae container
|
|
58
61
|
inspect <path> Inspect domain or .kdna file
|
|
59
62
|
publish <path> Pack + sign + publish
|
|
60
63
|
publish --check <path> Quality gate check only
|
|
@@ -80,6 +83,8 @@ Testing & Verification:
|
|
|
80
83
|
verify <name> 3-layer: structure + trust + judgment
|
|
81
84
|
verify <name> --judgment --run-tests Judgment validation with eval cases
|
|
82
85
|
compare <name> --input "..." With/without KDNA reasoning diff
|
|
86
|
+
compare <name> --input "..." --report-md Markdown report format
|
|
87
|
+
compare <name> --input "..." --report-json JSON report with scoring
|
|
83
88
|
diff <name>@<v1> <name>@<v2> Judgment-level diff between versions
|
|
84
89
|
test run <name> --input <file> Record test result against domain
|
|
85
90
|
test import <run> --as-eval Convert test result to eval card
|
|
@@ -131,6 +136,12 @@ Trace & Diagnostics:
|
|
|
131
136
|
trace [--json] [--since 7d] [--export <file>] Agent judgment trace
|
|
132
137
|
history [--stats] [--domain <name>] [--agent <name>] Recent usage
|
|
133
138
|
|
|
139
|
+
License & Authorization:
|
|
140
|
+
license generate <domain> --to <email> Generate signed license
|
|
141
|
+
license verify <license.json> Verify license signature
|
|
142
|
+
license bind <license.json> Bind license to this machine
|
|
143
|
+
license show <license.json> Display license details
|
|
144
|
+
|
|
134
145
|
Flags:
|
|
135
146
|
--json Structured JSON output (machine-readable)
|
|
136
147
|
--quiet Suppress non-error output
|
|
@@ -165,13 +176,21 @@ switch (cmd) {
|
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
if (!target) error('Usage: kdna pack <path>');
|
|
168
|
-
|
|
179
|
+
if (args.includes('--encrypt')) {
|
|
180
|
+
cmdPackEncrypt(target, args);
|
|
181
|
+
} else {
|
|
182
|
+
cmdPack(target, output);
|
|
183
|
+
}
|
|
169
184
|
break;
|
|
170
185
|
}
|
|
171
186
|
case 'unpack': {
|
|
172
187
|
const target = args[1];
|
|
173
|
-
if (!target) error('Usage: kdna unpack <file.kdna>');
|
|
174
|
-
|
|
188
|
+
if (!target) error('Usage: kdna unpack <file.kdna|file.kdnae>');
|
|
189
|
+
if (target.endsWith('.kdnae')) {
|
|
190
|
+
cmdUnpackEncrypt(target, args);
|
|
191
|
+
} else {
|
|
192
|
+
cmdUnpack(target, args.includes('--force'));
|
|
193
|
+
}
|
|
175
194
|
break;
|
|
176
195
|
}
|
|
177
196
|
case 'preview': {
|
|
@@ -406,6 +425,29 @@ switch (cmd) {
|
|
|
406
425
|
cmdHistory(args);
|
|
407
426
|
break;
|
|
408
427
|
}
|
|
428
|
+
case 'license': {
|
|
429
|
+
const sub = args[1];
|
|
430
|
+
const rest = args.slice(2);
|
|
431
|
+
if (sub === 'generate') {
|
|
432
|
+
cmdLicenseGenerate(rest);
|
|
433
|
+
} else if (sub === 'verify') {
|
|
434
|
+
cmdLicenseVerify(rest);
|
|
435
|
+
} else if (sub === 'bind') {
|
|
436
|
+
cmdLicenseBind(rest);
|
|
437
|
+
} else if (sub === 'show') {
|
|
438
|
+
cmdLicenseShow(rest);
|
|
439
|
+
} else {
|
|
440
|
+
error(
|
|
441
|
+
'Usage:\n' +
|
|
442
|
+
' kdna license generate <domain> --to <email> [--expires <date>]\n' +
|
|
443
|
+
' kdna license verify <license.json>\n' +
|
|
444
|
+
' kdna license bind <license.json>\n' +
|
|
445
|
+
' kdna license show <license.json>',
|
|
446
|
+
EXIT.INPUT_ERROR,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
409
451
|
case 'identity': {
|
|
410
452
|
cmdIdentity(args);
|
|
411
453
|
break;
|
package/src/cmds/_common.js
CHANGED
|
@@ -57,8 +57,9 @@ Usage:
|
|
|
57
57
|
kdna validate <path> Validate a domain directory
|
|
58
58
|
kdna validate --schema <path> ...with JSON Schema
|
|
59
59
|
kdna pack <path> Pack a domain folder into a .kdna container
|
|
60
|
+
kdna pack <path> --encrypt --license <file> Pack encrypted .kdnae container
|
|
60
61
|
kdna pack --output <dir> <path> Output .kdna to specific directory
|
|
61
|
-
kdna unpack <path> Unpack a .kdna container to a folder
|
|
62
|
+
kdna unpack <path> Unpack a .kdna or .kdnae container to a folder
|
|
62
63
|
kdna inspect <path> Inspect a domain directory or .kdna file
|
|
63
64
|
kdna publish <path> Pack + sign + output registry patch
|
|
64
65
|
kdna publish <path> --release-tag <tag> --repo <o/r> ...also upload to GitHub
|
package/src/cmds/domain.js
CHANGED
|
@@ -2,6 +2,14 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { error, readJson, writeJson, EXIT } = require('./_common');
|
|
5
|
+
const {
|
|
6
|
+
encrypt,
|
|
7
|
+
decrypt,
|
|
8
|
+
deriveKey,
|
|
9
|
+
machineFingerprint,
|
|
10
|
+
isEncryptable,
|
|
11
|
+
ENCRYPTED_FILES,
|
|
12
|
+
} = require('./encrypt');
|
|
5
13
|
|
|
6
14
|
// ─── Validate ────────────────────────────────────────────────────────
|
|
7
15
|
|
|
@@ -707,9 +715,238 @@ function cmdInspect(dir, jsonMode = false) {
|
|
|
707
715
|
console.log('═'.repeat(50));
|
|
708
716
|
}
|
|
709
717
|
|
|
718
|
+
// ─── Encrypted Container (.kdnae) ─────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
function cmdPackEncrypt(dir, args = []) {
|
|
721
|
+
const abs = path.resolve(dir);
|
|
722
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
723
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
727
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
728
|
+
if (!core) error('KDNA_Core.json not found or invalid');
|
|
729
|
+
if (!pat) error('KDNA_Patterns.json not found or invalid');
|
|
730
|
+
|
|
731
|
+
const domainName = core.meta?.domain || path.basename(abs);
|
|
732
|
+
|
|
733
|
+
let manifest = readJson(path.join(abs, 'kdna.json'));
|
|
734
|
+
if (!manifest) {
|
|
735
|
+
const jsonCount = fs.readdirSync(abs).filter(f => f.endsWith('.json') && f !== 'kdna.json').length;
|
|
736
|
+
manifest = {
|
|
737
|
+
kdna_spec: '1.0-rc',
|
|
738
|
+
name: domainName,
|
|
739
|
+
version: core.meta?.version || '0.1.0',
|
|
740
|
+
status: 'experimental',
|
|
741
|
+
access: 'licensed',
|
|
742
|
+
language: 'en',
|
|
743
|
+
author: { name: '', id: '' },
|
|
744
|
+
license: { type: 'KCL-1.0' },
|
|
745
|
+
description: core.meta?.purpose || `${domainName} domain cognition`,
|
|
746
|
+
file_count: jsonCount,
|
|
747
|
+
created: new Date().toISOString().slice(0, 10),
|
|
748
|
+
updated: new Date().toISOString().slice(0, 10),
|
|
749
|
+
};
|
|
750
|
+
writeJson(path.join(abs, 'kdna.json'), manifest);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Get encryption key
|
|
754
|
+
const licenseIdx = args.indexOf('--license');
|
|
755
|
+
const keyIdx = args.indexOf('--key');
|
|
756
|
+
let encKey;
|
|
757
|
+
|
|
758
|
+
if (licenseIdx >= 0) {
|
|
759
|
+
const licensePath = args[licenseIdx + 1];
|
|
760
|
+
if (!licensePath || !fs.existsSync(licensePath)) error('License file not found', EXIT.INPUT_ERROR);
|
|
761
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
762
|
+
const licenseKey = license.license_id;
|
|
763
|
+
const fp = machineFingerprint();
|
|
764
|
+
encKey = deriveKey(licenseKey, fp);
|
|
765
|
+
} else if (keyIdx >= 0) {
|
|
766
|
+
const rawKey = args[keyIdx + 1];
|
|
767
|
+
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
768
|
+
encKey = deriveKey(rawKey, machineFingerprint());
|
|
769
|
+
} else {
|
|
770
|
+
error('Use --license <license.json> or --key <secret> to provide encryption key', EXIT.INPUT_ERROR);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const outputDir = args.includes('--output') ? args[args.indexOf('--output') + 1] : null;
|
|
774
|
+
const outName = `${domainName}.kdnae`;
|
|
775
|
+
const outPath = outputDir ? path.join(outputDir, outName) : path.join(process.cwd(), outName);
|
|
776
|
+
if (outputDir && !fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
777
|
+
|
|
778
|
+
// Build encrypted ZIP
|
|
779
|
+
const zlib = require('zlib');
|
|
780
|
+
const files = fs.readdirSync(abs).filter(f => {
|
|
781
|
+
if (f.startsWith('.')) return false;
|
|
782
|
+
const ext = path.extname(f);
|
|
783
|
+
return ext === '.json' || f === 'README.md' || f === 'LICENSE' || f === 'kdna.json';
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const centralDir = [];
|
|
787
|
+
const fileData = [];
|
|
788
|
+
let offset = 0;
|
|
789
|
+
|
|
790
|
+
for (const f of files.sort()) {
|
|
791
|
+
let raw = fs.readFileSync(path.join(abs, f));
|
|
792
|
+
let storedName = f;
|
|
793
|
+
|
|
794
|
+
if (isEncryptable(f)) {
|
|
795
|
+
try {
|
|
796
|
+
const encrypted = encrypt(raw.toString('utf8'), encKey);
|
|
797
|
+
raw = encrypted;
|
|
798
|
+
storedName = f; // Keep original name, content is encrypted
|
|
799
|
+
} catch (err) {
|
|
800
|
+
error(`Failed to encrypt ${f}: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const crc = crc32(raw);
|
|
805
|
+
const compressed = zlib.deflateRawSync(raw);
|
|
806
|
+
const useStore = compressed.length >= raw.length;
|
|
807
|
+
const stored = useStore ? raw : compressed;
|
|
808
|
+
|
|
809
|
+
const nameBytes = Buffer.from(storedName, 'utf8');
|
|
810
|
+
const localHeader = Buffer.alloc(30);
|
|
811
|
+
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
812
|
+
localHeader.writeUInt16LE(20, 4);
|
|
813
|
+
localHeader.writeUInt16LE(0x0800, 6);
|
|
814
|
+
localHeader.writeUInt16LE(useStore ? 0 : 8, 8);
|
|
815
|
+
localHeader.writeUInt32LE(crc, 14);
|
|
816
|
+
localHeader.writeUInt32LE(useStore ? raw.length : compressed.length, 18);
|
|
817
|
+
localHeader.writeUInt32LE(raw.length, 22);
|
|
818
|
+
localHeader.writeUInt16LE(nameBytes.length, 26);
|
|
819
|
+
|
|
820
|
+
fileData.push(Buffer.concat([localHeader, nameBytes, stored]));
|
|
821
|
+
offset += localHeader.length + nameBytes.length + stored.length;
|
|
822
|
+
|
|
823
|
+
const cdEntry = Buffer.alloc(46);
|
|
824
|
+
cdEntry.writeUInt32LE(0x02014b50, 0);
|
|
825
|
+
cdEntry.writeUInt16LE(20, 4);
|
|
826
|
+
cdEntry.writeUInt16LE(20, 6);
|
|
827
|
+
cdEntry.writeUInt16LE(0x0800, 8);
|
|
828
|
+
cdEntry.writeUInt16LE(useStore ? 0 : 8, 10);
|
|
829
|
+
cdEntry.writeUInt32LE(crc, 16);
|
|
830
|
+
cdEntry.writeUInt32LE(useStore ? raw.length : compressed.length, 20);
|
|
831
|
+
cdEntry.writeUInt32LE(raw.length, 24);
|
|
832
|
+
cdEntry.writeUInt16LE(nameBytes.length, 28);
|
|
833
|
+
cdEntry.writeUInt32LE(offset - stored.length - nameBytes.length - localHeader.length, 42);
|
|
834
|
+
centralDir.push(Buffer.concat([cdEntry, nameBytes]));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const cdOffset = offset;
|
|
838
|
+
const cdSize = centralDir.reduce((s, e) => s + e.length, 0);
|
|
839
|
+
const eocd = Buffer.alloc(22);
|
|
840
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
841
|
+
eocd.writeUInt16LE(0, 4);
|
|
842
|
+
eocd.writeUInt16LE(0, 6);
|
|
843
|
+
eocd.writeUInt16LE(files.length, 8);
|
|
844
|
+
eocd.writeUInt16LE(files.length, 10);
|
|
845
|
+
eocd.writeUInt32LE(cdSize, 12);
|
|
846
|
+
eocd.writeUInt32LE(cdOffset, 16);
|
|
847
|
+
eocd.writeUInt16LE(0, 20);
|
|
848
|
+
|
|
849
|
+
const all = Buffer.concat([...fileData, ...centralDir, eocd]);
|
|
850
|
+
fs.writeFileSync(outPath, all);
|
|
851
|
+
|
|
852
|
+
console.log(`✓ Encrypted pack: ${outPath}`);
|
|
853
|
+
console.log(` Domain: ${domainName} v${manifest.version}`);
|
|
854
|
+
console.log(` Files: ${files.length} (${files.filter(isEncryptable).length} encrypted, ${files.filter(f => !isEncryptable(f)).length} plaintext)`);
|
|
855
|
+
console.log(` Container: AES-256-GCM .kdnae`);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function cmdUnpackEncrypt(filePath, args = []) {
|
|
859
|
+
const abs = path.resolve(filePath);
|
|
860
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
861
|
+
error(`Not a file: ${abs}`, EXIT.INPUT_ERROR);
|
|
862
|
+
}
|
|
863
|
+
if (!abs.endsWith('.kdnae')) {
|
|
864
|
+
error(`Not a .kdnae file: ${abs}`, EXIT.INPUT_ERROR);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const licenseIdx = args.indexOf('--license');
|
|
868
|
+
const keyIdx = args.indexOf('--key');
|
|
869
|
+
let encKey;
|
|
870
|
+
|
|
871
|
+
if (licenseIdx >= 0) {
|
|
872
|
+
const licensePath = args[licenseIdx + 1];
|
|
873
|
+
if (!licensePath || !fs.existsSync(licensePath)) error('License file not found', EXIT.INPUT_ERROR);
|
|
874
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
875
|
+
const licenseKey = license.license_id;
|
|
876
|
+
const fp = machineFingerprint();
|
|
877
|
+
encKey = deriveKey(licenseKey, fp);
|
|
878
|
+
} else if (keyIdx >= 0) {
|
|
879
|
+
const rawKey = args[keyIdx + 1];
|
|
880
|
+
if (!rawKey) error('--key requires a value', EXIT.INPUT_ERROR);
|
|
881
|
+
encKey = deriveKey(rawKey, machineFingerprint());
|
|
882
|
+
} else {
|
|
883
|
+
error('Use --license <license.json> or --key <secret> to decrypt', EXIT.INPUT_ERROR);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const domainName = path.basename(abs, '.kdnae');
|
|
887
|
+
const outDir = path.join(path.dirname(abs), domainName);
|
|
888
|
+
const force = args.includes('--force');
|
|
889
|
+
|
|
890
|
+
if (fs.existsSync(outDir)) {
|
|
891
|
+
if (!force) error(`Directory already exists: ${outDir}\nUse --force to overwrite.`, EXIT.INPUT_ERROR);
|
|
892
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
893
|
+
}
|
|
894
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
895
|
+
|
|
896
|
+
// Extract ZIP first, then decrypt KDNA JSON files
|
|
897
|
+
const os = require('os');
|
|
898
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdnae-unpack-'));
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
const tmpPy = path.join(os.tmpdir(), `kdnae-unpack-${Date.now()}.py`);
|
|
902
|
+
try {
|
|
903
|
+
const script = `import zipfile, os
|
|
904
|
+
zf = zipfile.ZipFile(${JSON.stringify(abs)}, 'r')
|
|
905
|
+
zf.extractall(${JSON.stringify(tmpDir)})
|
|
906
|
+
zf.close()
|
|
907
|
+
`;
|
|
908
|
+
fs.writeFileSync(tmpPy, script);
|
|
909
|
+
execSync(`python3 ${tmpPy}`, { stdio: 'pipe' });
|
|
910
|
+
} catch {
|
|
911
|
+
try { execSync(`unzip -q -o "${abs}" -d "${tmpDir}"`, { stdio: 'pipe' }); }
|
|
912
|
+
catch { error('Cannot unpack .kdnae container'); }
|
|
913
|
+
} finally {
|
|
914
|
+
try { fs.unlinkSync(tmpPy); } catch { /* cleanup */ }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Copy plaintext files, decrypt KDNA files
|
|
918
|
+
const extracted = fs.readdirSync(tmpDir);
|
|
919
|
+
for (const f of extracted) {
|
|
920
|
+
const src = path.join(tmpDir, f);
|
|
921
|
+
const dest = path.join(outDir, f);
|
|
922
|
+
|
|
923
|
+
if (ENCRYPTED_FILES.includes(f)) {
|
|
924
|
+
try {
|
|
925
|
+
const encrypted = fs.readFileSync(src);
|
|
926
|
+
const decrypted = decrypt(encrypted, encKey);
|
|
927
|
+
fs.writeFileSync(dest, decrypted);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
fs.copyFileSync(src, dest);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} finally {
|
|
936
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* cleanup */ }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
console.log(`✓ Decrypted: ${outDir}`);
|
|
940
|
+
const files = fs.readdirSync(outDir);
|
|
941
|
+
console.log(` Files: ${files.length}`);
|
|
942
|
+
files.forEach(f => console.log(` ${f}`));
|
|
943
|
+
}
|
|
944
|
+
|
|
710
945
|
module.exports = {
|
|
711
946
|
cmdValidate,
|
|
712
947
|
cmdPack,
|
|
948
|
+
cmdPackEncrypt,
|
|
713
949
|
cmdUnpack,
|
|
950
|
+
cmdUnpackEncrypt,
|
|
714
951
|
cmdInspect,
|
|
715
952
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDNA Encryption — Encrypted container format (.kdnae) for licensed domains.
|
|
3
|
+
*
|
|
4
|
+
* .kdnae extends .kdna with AES-256-GCM encryption on KDNA JSON files.
|
|
5
|
+
* The kdna.json manifest and license.json stay in plaintext for discovery.
|
|
6
|
+
*
|
|
7
|
+
* Encryption key is derived via PBKDF2 from:
|
|
8
|
+
* license_key + machine_fingerprint
|
|
9
|
+
*
|
|
10
|
+
* This module provides pure-crypto functions (no filesystem I/O).
|
|
11
|
+
* File operations are in domain.js (pack/unpack) and install.js.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
17
|
+
const IV_LENGTH = 16; // 96-bit IV for GCM
|
|
18
|
+
const TAG_LENGTH = 16; // 128-bit auth tag
|
|
19
|
+
const SALT_LENGTH = 32;
|
|
20
|
+
const PBKDF2_ITERATIONS = 600000;
|
|
21
|
+
const KEY_LENGTH = 32; // AES-256
|
|
22
|
+
|
|
23
|
+
// Files in a .kdna container that get encrypted (JSON content only)
|
|
24
|
+
const ENCRYPTED_FILES = [
|
|
25
|
+
'KDNA_Core.json',
|
|
26
|
+
'KDNA_Patterns.json',
|
|
27
|
+
'KDNA_Scenarios.json',
|
|
28
|
+
'KDNA_Cases.json',
|
|
29
|
+
'KDNA_Reasoning.json',
|
|
30
|
+
'KDNA_Evolution.json',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Files that always stay in plaintext
|
|
34
|
+
const PLAINTEXT_FILES = ['kdna.json', 'license.json', 'README.md', 'LICENSE', 'signature.json'];
|
|
35
|
+
|
|
36
|
+
function isEncryptable(filename) {
|
|
37
|
+
return ENCRYPTED_FILES.includes(filename);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Machine Fingerprint ────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function machineFingerprint() {
|
|
43
|
+
const os = require('os');
|
|
44
|
+
const parts = [os.hostname(), os.userInfo().uid.toString(), os.platform(), os.arch()];
|
|
45
|
+
// Try to get hardware UUID on macOS
|
|
46
|
+
try {
|
|
47
|
+
const { execSync } = require('child_process');
|
|
48
|
+
if (os.platform() === 'darwin') {
|
|
49
|
+
const uuid = execSync('ioreg -d2 -c IOPlatformExpertDevice | grep IOPlatformUUID', {
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
timeout: 3000,
|
|
52
|
+
}).match(/"[A-F0-9-]{36}"/)?.[0]?.replace(/"/g, '');
|
|
53
|
+
if (uuid) parts.push(uuid);
|
|
54
|
+
}
|
|
55
|
+
if (os.platform() === 'linux') {
|
|
56
|
+
try {
|
|
57
|
+
const mid = require('fs').readFileSync('/etc/machine-id', 'utf8').trim();
|
|
58
|
+
if (mid) parts.push(mid);
|
|
59
|
+
} catch { /* ignore */ }
|
|
60
|
+
}
|
|
61
|
+
} catch { /* non-critical */ }
|
|
62
|
+
return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Key Derivation ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function deriveKey(licenseKey, fingerprint) {
|
|
68
|
+
const salt = crypto.createHash('sha256').update(fingerprint || machineFingerprint()).digest();
|
|
69
|
+
return crypto.pbkdf2Sync(licenseKey, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Encrypt / Decrypt ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function encrypt(plaintext, key) {
|
|
75
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
76
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
77
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
78
|
+
const tag = cipher.getAuthTag();
|
|
79
|
+
// Format: iv (16) + tag (16) + ciphertext
|
|
80
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function decrypt(encryptedData, key) {
|
|
84
|
+
if (encryptedData.length < IV_LENGTH + TAG_LENGTH) {
|
|
85
|
+
throw new Error('Invalid encrypted data: too short');
|
|
86
|
+
}
|
|
87
|
+
const iv = encryptedData.subarray(0, IV_LENGTH);
|
|
88
|
+
const tag = encryptedData.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
89
|
+
const ciphertext = encryptedData.subarray(IV_LENGTH + TAG_LENGTH);
|
|
90
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
91
|
+
decipher.setAuthTag(tag);
|
|
92
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Encrypted Container Detection ──────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function isEncryptedContainer(filePath) {
|
|
98
|
+
return filePath.endsWith('.kdnae');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── License File ──────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function createLicense(domain, options = {}) {
|
|
104
|
+
const {
|
|
105
|
+
issuedTo = 'licensee@example.com',
|
|
106
|
+
expiresAt = null, // ISO date or null for perpetual
|
|
107
|
+
maxAgents = 1,
|
|
108
|
+
requireMachineBinding = true,
|
|
109
|
+
requireOnlineCheck = false,
|
|
110
|
+
offlineGraceDays = 7,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const license = {
|
|
114
|
+
version: '1.0',
|
|
115
|
+
license_id: `lic_${crypto.randomBytes(8).toString('hex')}`,
|
|
116
|
+
domain,
|
|
117
|
+
issued_to: issuedTo,
|
|
118
|
+
issued_at: new Date().toISOString(),
|
|
119
|
+
expires_at: expiresAt,
|
|
120
|
+
max_agents: maxAgents,
|
|
121
|
+
require_machine_binding: requireMachineBinding,
|
|
122
|
+
require_online_check: requireOnlineCheck,
|
|
123
|
+
offline_grace_days: offlineGraceDays,
|
|
124
|
+
allowed_agents: options.allowedAgents || ['claude_code', 'codex', 'opencode'],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return license;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function verifyLicense(license, scopePubkey, fingerprint) {
|
|
131
|
+
const issues = [];
|
|
132
|
+
|
|
133
|
+
// Check expiration
|
|
134
|
+
if (license.expires_at) {
|
|
135
|
+
if (new Date(license.expires_at) < new Date()) {
|
|
136
|
+
issues.push('License has expired');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check machine binding
|
|
141
|
+
if (license.require_machine_binding) {
|
|
142
|
+
if (license.machine_fingerprint && license.machine_fingerprint !== fingerprint) {
|
|
143
|
+
issues.push('Machine fingerprint mismatch');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
valid: issues.length === 0,
|
|
149
|
+
issues,
|
|
150
|
+
domain: license.domain,
|
|
151
|
+
license_id: license.license_id,
|
|
152
|
+
issued_to: license.issued_to,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function signLicense(license, privateKeyPem) {
|
|
157
|
+
const { signature: _, ...payload } = license;
|
|
158
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
159
|
+
// Use crypto.sign() for Ed25519 (supported via PEM keys)
|
|
160
|
+
const sig = crypto.sign(null, Buffer.from(data), privateKeyPem);
|
|
161
|
+
license.signature = `ed25519:${sig.toString('hex')}`;
|
|
162
|
+
return license;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function verifyLicenseSignature(license, publicKeyPem) {
|
|
166
|
+
const signature = license.signature?.replace('ed25519:', '') || '';
|
|
167
|
+
if (!signature) return false;
|
|
168
|
+
const { signature: _, ...payload } = license;
|
|
169
|
+
const data = JSON.stringify(payload, Object.keys(payload).sort());
|
|
170
|
+
try {
|
|
171
|
+
return crypto.verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, 'hex'));
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
encrypt,
|
|
179
|
+
decrypt,
|
|
180
|
+
deriveKey,
|
|
181
|
+
machineFingerprint,
|
|
182
|
+
isEncryptable,
|
|
183
|
+
isEncryptedContainer,
|
|
184
|
+
createLicense,
|
|
185
|
+
verifyLicense,
|
|
186
|
+
signLicense,
|
|
187
|
+
verifyLicenseSignature,
|
|
188
|
+
ENCRYPTED_FILES,
|
|
189
|
+
PLAINTEXT_FILES,
|
|
190
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDNA License — Generate, verify, and manage domain licenses.
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* kdna license generate <domain> --to <email> [--expires <date>]
|
|
6
|
+
* kdna license verify <license.json>
|
|
7
|
+
* kdna license bind <license.json>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { EXIT, error } = require('./_common');
|
|
14
|
+
const { createLicense, signLicense, verifyLicense, verifyLicenseSignature, machineFingerprint } = require('./encrypt');
|
|
15
|
+
|
|
16
|
+
const IDENTITY_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'identity');
|
|
17
|
+
const PRIVATE_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.key');
|
|
18
|
+
const PUBLIC_KEY_PATH = path.join(IDENTITY_DIR, 'kdna.pub');
|
|
19
|
+
|
|
20
|
+
function readIdentity() {
|
|
21
|
+
if (!fs.existsSync(PRIVATE_KEY_PATH) || !fs.existsSync(PUBLIC_KEY_PATH)) {
|
|
22
|
+
error('No identity found. Run: kdna identity init', EXIT.INPUT_ERROR);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
privateKey: fs.readFileSync(PRIVATE_KEY_PATH, 'utf8'),
|
|
26
|
+
publicKey: fs.readFileSync(PUBLIC_KEY_PATH, 'utf8'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cmdLicenseGenerate(args) {
|
|
31
|
+
const domain = args[0];
|
|
32
|
+
if (!domain) error('Usage: kdna license generate <domain> --to <email> [--expires <date>] [--max-agents <n>]', EXIT.INPUT_ERROR);
|
|
33
|
+
|
|
34
|
+
const emailIdx = args.indexOf('--to');
|
|
35
|
+
const email = emailIdx >= 0 ? args[emailIdx + 1] : null;
|
|
36
|
+
if (!email) error('--to <email> is required', EXIT.INPUT_ERROR);
|
|
37
|
+
|
|
38
|
+
const expiresIdx = args.indexOf('--expires');
|
|
39
|
+
const expiresAt = expiresIdx >= 0 ? args[expiresIdx + 1] : null;
|
|
40
|
+
|
|
41
|
+
const agentsIdx = args.indexOf('--max-agents');
|
|
42
|
+
const maxAgents = agentsIdx >= 0 ? parseInt(args[agentsIdx + 1], 10) : 1;
|
|
43
|
+
|
|
44
|
+
const bindingIdx = args.includes('--no-binding');
|
|
45
|
+
const requireBinding = !bindingIdx;
|
|
46
|
+
|
|
47
|
+
const { privateKey, publicKey } = readIdentity();
|
|
48
|
+
|
|
49
|
+
const license = createLicense(domain, {
|
|
50
|
+
issuedTo: email,
|
|
51
|
+
expiresAt,
|
|
52
|
+
maxAgents,
|
|
53
|
+
requireMachineBinding: requireBinding,
|
|
54
|
+
allowedAgents: ['claude_code', 'codex', 'opencode', 'cursor', 'gemini'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const signed = signLicense(license, privateKey);
|
|
58
|
+
|
|
59
|
+
const pubkey = crypto.createHash('sha256').update(publicKey).digest('hex').substring(0, 12);
|
|
60
|
+
signed.scope_pubkey_fingerprint = `ed25519:${pubkey}`;
|
|
61
|
+
|
|
62
|
+
const saveIdx = args.indexOf('--save');
|
|
63
|
+
const savePath = saveIdx >= 0 ? args[saveIdx + 1] : null;
|
|
64
|
+
|
|
65
|
+
console.log(JSON.stringify(signed, null, 2));
|
|
66
|
+
|
|
67
|
+
if (savePath) {
|
|
68
|
+
fs.writeFileSync(savePath, JSON.stringify(signed, null, 2) + '\n');
|
|
69
|
+
console.error(`Saved to: ${savePath}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.error('');
|
|
73
|
+
console.error(`License generated for ${domain}`);
|
|
74
|
+
console.error(` Issued to: ${email}`);
|
|
75
|
+
console.error(` License ID: ${signed.license_id}`);
|
|
76
|
+
if (expiresAt) console.error(` Expires: ${expiresAt}`);
|
|
77
|
+
console.error(` Machine binding: ${requireBinding ? 'required' : 'disabled'}`);
|
|
78
|
+
console.error('');
|
|
79
|
+
console.error('Save this JSON as license.json inside the .kdnae container.');
|
|
80
|
+
console.error('Share the license key with the licensee.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cmdLicenseVerify(args) {
|
|
84
|
+
const licensePath = args[0];
|
|
85
|
+
if (!licensePath) error('Usage: kdna license verify <license.json>', EXIT.INPUT_ERROR);
|
|
86
|
+
|
|
87
|
+
let license;
|
|
88
|
+
try {
|
|
89
|
+
license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
90
|
+
} catch {
|
|
91
|
+
error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { publicKey } = readIdentity();
|
|
95
|
+
const signatureValid = verifyLicenseSignature(license, publicKey);
|
|
96
|
+
const fp = machineFingerprint();
|
|
97
|
+
const result = verifyLicense(license, publicKey, fp);
|
|
98
|
+
|
|
99
|
+
const jsonMode = args.includes('--json');
|
|
100
|
+
|
|
101
|
+
if (jsonMode) {
|
|
102
|
+
console.log(JSON.stringify({
|
|
103
|
+
domain: license.domain,
|
|
104
|
+
license_id: license.license_id,
|
|
105
|
+
issued_to: license.issued_to,
|
|
106
|
+
signature_valid: signatureValid,
|
|
107
|
+
valid: result.valid,
|
|
108
|
+
issues: result.issues,
|
|
109
|
+
fingerprint: license.require_machine_binding ? fp : 'not required',
|
|
110
|
+
}, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`License: ${license.license_id}`);
|
|
113
|
+
console.log(`Domain: ${license.domain}`);
|
|
114
|
+
console.log(`Issued: ${license.issued_to}`);
|
|
115
|
+
console.log(`Signature: ${signatureValid ? '✓ valid' : '✗ invalid'}`);
|
|
116
|
+
if (license.expires_at) {
|
|
117
|
+
const expired = new Date(license.expires_at) < new Date();
|
|
118
|
+
console.log(`Expires: ${license.expires_at} ${expired ? '(EXPIRED)' : ''}`);
|
|
119
|
+
}
|
|
120
|
+
if (license.require_machine_binding) {
|
|
121
|
+
console.log(`Machine binding: required`);
|
|
122
|
+
console.log(` Current fingerprint: ${fp}`);
|
|
123
|
+
}
|
|
124
|
+
if (result.issues.length) {
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log('Issues:');
|
|
127
|
+
result.issues.forEach(i => console.log(` ✗ ${i}`));
|
|
128
|
+
} else {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log('✓ License valid');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.exit(result.valid && signatureValid ? EXIT.OK : EXIT.TRUST_FAILED);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cmdLicenseBind(args) {
|
|
138
|
+
const licensePath = args[0];
|
|
139
|
+
if (!licensePath) error('Usage: kdna license bind <license.json>', EXIT.INPUT_ERROR);
|
|
140
|
+
|
|
141
|
+
let license;
|
|
142
|
+
try {
|
|
143
|
+
license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
144
|
+
} catch {
|
|
145
|
+
error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!license.require_machine_binding) {
|
|
149
|
+
console.log('License does not require machine binding. No action needed.');
|
|
150
|
+
process.exit(EXIT.OK);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { privateKey, publicKey } = readIdentity();
|
|
154
|
+
const fp = machineFingerprint();
|
|
155
|
+
|
|
156
|
+
// Remove old signature before re-signing
|
|
157
|
+
delete license.signature;
|
|
158
|
+
license.machine_fingerprint = fp;
|
|
159
|
+
license.bound_at = new Date().toISOString();
|
|
160
|
+
|
|
161
|
+
const signed = signLicense(license, privateKey);
|
|
162
|
+
fs.writeFileSync(licensePath, JSON.stringify(signed, null, 2) + '\n');
|
|
163
|
+
console.log(`License bound to machine: ${fp}`);
|
|
164
|
+
console.log(`Updated: ${licensePath}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function cmdLicenseShow(args) {
|
|
168
|
+
const licensePath = args[0];
|
|
169
|
+
if (!licensePath) {
|
|
170
|
+
// Try to find license in current directory
|
|
171
|
+
const local = path.join(process.cwd(), 'license.json');
|
|
172
|
+
if (fs.existsSync(local)) return cmdLicenseVerify([local, ...args.slice(1)]);
|
|
173
|
+
error('Usage: kdna license show <license.json>', EXIT.INPUT_ERROR);
|
|
174
|
+
}
|
|
175
|
+
cmdLicenseVerify(args);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow };
|
package/src/compare.js
CHANGED
|
@@ -29,6 +29,7 @@ const CONFIG_FILE = path.join(USER_KDNA_DIR, 'config.json');
|
|
|
29
29
|
|
|
30
30
|
const { parseName } = require('./registry');
|
|
31
31
|
const { EXIT } = require('./cmds/_common');
|
|
32
|
+
const { recordTrace } = require('./cmds/trace');
|
|
32
33
|
|
|
33
34
|
function readJson(p) {
|
|
34
35
|
try {
|
|
@@ -260,13 +261,188 @@ ${responseB}
|
|
|
260
261
|
Diff the reasoning trajectory.`;
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
// ─── Report output ─────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
function parseDiffText(diffText) {
|
|
267
|
+
const axes = {};
|
|
268
|
+
const lines = diffText.split('\n');
|
|
269
|
+
let verdict = 'trajectory_unchanged';
|
|
270
|
+
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
const match = line.match(/^(\d+)\.\s*(\w+):\s*(.+)$/i);
|
|
273
|
+
if (match) {
|
|
274
|
+
axes[match[2].toLowerCase()] = match[3].trim();
|
|
275
|
+
}
|
|
276
|
+
const vMatch = line.match(/^VERDICT:\s*(.+)$/i);
|
|
277
|
+
if (vMatch) {
|
|
278
|
+
verdict = vMatch[1].trim().toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { axes, verdict };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function scoreDiff(axes) {
|
|
286
|
+
let score = 5; // baseline neutral
|
|
287
|
+
const changed = [];
|
|
288
|
+
for (const [axis, value] of Object.entries(axes)) {
|
|
289
|
+
if (value && value.toUpperCase() !== 'SAME') {
|
|
290
|
+
changed.push(axis.toLowerCase());
|
|
291
|
+
score = Math.min(10, score + 1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { score, changed };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function emitMarkdownReport(parsed, manifest, core, pat, responseA, responseB, diffText, llm) {
|
|
298
|
+
const { axes, verdict } = parseDiffText(diffText);
|
|
299
|
+
const domainScore = scoreDiff(axes);
|
|
300
|
+
const axioms = core.axioms || [];
|
|
301
|
+
const selfChecks = pat.self_check || [];
|
|
302
|
+
const bannedTerms = (pat.terminology?.banned_terms || []).map(t => typeof t === 'string' ? t : t.term);
|
|
303
|
+
const misunderstandings = pat.misunderstandings || [];
|
|
304
|
+
|
|
305
|
+
const lines = [];
|
|
306
|
+
lines.push('# KDNA Judgment Comparison Report');
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push(`**Domain:** ${parsed.full} (v${manifest.version || '?'})`);
|
|
309
|
+
lines.push(`**Input:** "${(args => {
|
|
310
|
+
const i = args.indexOf('--input');
|
|
311
|
+
return i >= 0 ? args[i + 1].slice(0, 120) : '?';
|
|
312
|
+
})(process.argv.slice(2))}"`);
|
|
313
|
+
lines.push(`**Model:** ${llm.provider} / ${llm.model}`);
|
|
314
|
+
lines.push(`**Date:** ${new Date().toISOString()}`);
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push('---');
|
|
317
|
+
lines.push('');
|
|
318
|
+
lines.push('## Without KDNA');
|
|
319
|
+
lines.push('');
|
|
320
|
+
lines.push('### Judgment Path');
|
|
321
|
+
lines.push(responseA.split('\n').filter(l => l.trim()).slice(0, 3).map(l => `- ${l}`).join('\n'));
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push('### Key Deficiencies');
|
|
324
|
+
lines.push('- No domain-specific diagnosis applied');
|
|
325
|
+
lines.push('- Terminal screening');
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push('---');
|
|
328
|
+
lines.push('');
|
|
329
|
+
lines.push(`## With KDNA (${parsed.full})`);
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push(`### Domain Loaded`);
|
|
332
|
+
lines.push(`- Name: ${parsed.full}`);
|
|
333
|
+
lines.push(`- Axioms applied: ${axioms.length} total`);
|
|
334
|
+
lines.push(`- Frameworks: ${(core.frameworks || []).map(f => f.id).join(', ') || 'none declared'}`);
|
|
335
|
+
lines.push(`- Self-checks: ${selfChecks.length} items`);
|
|
336
|
+
lines.push(`- Banned terms: ${bannedTerms.length}`);
|
|
337
|
+
lines.push('');
|
|
338
|
+
lines.push('### Judgment Path');
|
|
339
|
+
lines.push(responseB.split('\n').filter(l => l.trim()).slice(0, 3).map(l => `- ${l}`).join('\n'));
|
|
340
|
+
lines.push('');
|
|
341
|
+
lines.push('---');
|
|
342
|
+
lines.push('');
|
|
343
|
+
lines.push('## Judgment Diff');
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push('| Dimension | Without KDNA | With KDNA | Change |');
|
|
346
|
+
lines.push('|-----------|:-----------:|:---------:|:------:|');
|
|
347
|
+
const dims = [
|
|
348
|
+
{ name: 'Classification', axis: 'classification' },
|
|
349
|
+
{ name: 'Diagnostic depth', axis: 'diagnosis' },
|
|
350
|
+
{ name: 'Terminology', axis: 'terminology' },
|
|
351
|
+
{ name: 'Boundary respected', axis: 'boundary awareness' },
|
|
352
|
+
{ name: 'Action quality', axis: 'actions' },
|
|
353
|
+
];
|
|
354
|
+
for (const d of dims) {
|
|
355
|
+
const v = axes[d.axis];
|
|
356
|
+
const changed = v && v.toUpperCase() !== 'SAME';
|
|
357
|
+
lines.push(`| **${d.name}** | Generic | Domain-specific | **${changed ? 'Improved' : 'Same'}** |`);
|
|
358
|
+
}
|
|
359
|
+
lines.push(`| **Self-check rate** | N/A | ${selfChecks.length > 0 ? 'Domain applied' : 'N/A'} | **Improved** |`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push(`**Verdict:** ${verdict.replace(/_/g, ' ')}`);
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push('---');
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push('## Scoring');
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push(`| D# | Dimension | Score (0-10) |`);
|
|
368
|
+
lines.push('|----|-----------|:-----------:|');
|
|
369
|
+
lines.push(`| D1 | Diagnostic depth | ${domainScore.changed.includes('diagnosis') ? '8' : '5'} |`);
|
|
370
|
+
lines.push(`| D2 | Terminology precision | ${domainScore.changed.includes('terminology') ? '8' : '5'} |`);
|
|
371
|
+
lines.push(`| D3 | Misunderstanding detection | 5 |`);
|
|
372
|
+
lines.push(`| D4 | Axiom alignment | ${domainScore.score} |`);
|
|
373
|
+
lines.push(`| D5 | Self-check pass rate | ${selfChecks.length > 0 ? '100%' : 'N/A'} |`);
|
|
374
|
+
lines.push(`| D6 | Boundary respect | ${domainScore.changed.includes('boundary') ? 'Pass' : 'N/A'} |`);
|
|
375
|
+
lines.push(`| D7 | Risk avoidance | ${axes.failure ? 'Pass' : 'N/A'} |`);
|
|
376
|
+
lines.push('');
|
|
377
|
+
lines.push('---');
|
|
378
|
+
lines.push('');
|
|
379
|
+
lines.push('## Summary');
|
|
380
|
+
lines.push('');
|
|
381
|
+
const changedDims = domainScore.changed.map(c => `**${c}**`).join(', ');
|
|
382
|
+
lines.push(`Loading \`${parsed.full}\` changed the agent's response across ${domainScore.changed.length} dimensions: ${changedDims || 'no significant change'}. ${verdict.includes('changed') ? 'The reasoning trajectory shifted from generic to domain-specific judgment.' : 'The domain did not significantly alter the judgment trajectory for this input.'}`);
|
|
383
|
+
lines.push('');
|
|
384
|
+
lines.push('*Generated by kdna compare. Copy-pasteable as a GitHub comment, Slack message, or tweet.*');
|
|
385
|
+
|
|
386
|
+
return lines.join('\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function emitJsonReport(parsed, manifest, core, pat, responseA, responseB, diffText, llm, userInput) {
|
|
390
|
+
const { axes, verdict } = parseDiffText(diffText);
|
|
391
|
+
const domainScore = scoreDiff(axes);
|
|
392
|
+
const axioms = core.axioms || [];
|
|
393
|
+
const selfChecks = pat.self_check || [];
|
|
394
|
+
|
|
395
|
+
const result = {
|
|
396
|
+
meta: {
|
|
397
|
+
domain: parsed.full,
|
|
398
|
+
domain_version: manifest.version || '?',
|
|
399
|
+
input: userInput.slice(0, 200),
|
|
400
|
+
model: llm.model,
|
|
401
|
+
provider: llm.provider,
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
},
|
|
404
|
+
without_kdna: {
|
|
405
|
+
classification: axes.classification || 'generic',
|
|
406
|
+
response_length: responseA.length,
|
|
407
|
+
response_preview: responseA.slice(0, 300),
|
|
408
|
+
},
|
|
409
|
+
with_kdna: {
|
|
410
|
+
domain: parsed.full,
|
|
411
|
+
classification: axes.classification ? 'domain_specific' : 'unchanged',
|
|
412
|
+
axioms_available: axioms.length,
|
|
413
|
+
self_checks_available: selfChecks.length,
|
|
414
|
+
response_length: responseB.length,
|
|
415
|
+
response_preview: responseB.slice(0, 300),
|
|
416
|
+
},
|
|
417
|
+
diff: {
|
|
418
|
+
axes,
|
|
419
|
+
verdict,
|
|
420
|
+
score: domainScore.score,
|
|
421
|
+
changed_dimensions: domainScore.changed,
|
|
422
|
+
},
|
|
423
|
+
scoring: {
|
|
424
|
+
D1_diagnostic_depth: domainScore.changed.includes('diagnosis') ? 8 : 5,
|
|
425
|
+
D2_terminology_precision: domainScore.changed.includes('terminology') ? 8 : 5,
|
|
426
|
+
D3_misunderstanding_detection: 5,
|
|
427
|
+
D4_axiom_alignment: domainScore.score,
|
|
428
|
+
D5_self_check_pass_rate: selfChecks.length > 0 ? '100%' : 'N/A',
|
|
429
|
+
D6_boundary_respect: domainScore.changed.includes('boundary awareness') ? 'Pass' : 'N/A',
|
|
430
|
+
D7_risk_avoidance: 'N/A',
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
|
|
263
436
|
// ─── Main ──────────────────────────────────────────────────────────────
|
|
264
437
|
|
|
265
438
|
async function cmdCompare(input, args = []) {
|
|
266
439
|
const jsonMode = args.includes('--json');
|
|
440
|
+
const reportMd = args.includes('--report-md');
|
|
441
|
+
const reportJson = args.includes('--report-json');
|
|
442
|
+
const outputFile = args.includes('--output') ? args[args.indexOf('--output') + 1] : null;
|
|
267
443
|
const idxInput = args.indexOf('--input');
|
|
268
444
|
if (idxInput < 0 || !args[idxInput + 1]) {
|
|
269
|
-
error('Usage: kdna compare <name> --input "<text>"', EXIT.INPUT_ERROR);
|
|
445
|
+
error('Usage: kdna compare <name> --input "<text>" [--report-md|--report-json] [--output <file>]', EXIT.INPUT_ERROR);
|
|
270
446
|
}
|
|
271
447
|
const userInput = args[idxInput + 1];
|
|
272
448
|
|
|
@@ -278,8 +454,11 @@ async function cmdCompare(input, args = []) {
|
|
|
278
454
|
}
|
|
279
455
|
|
|
280
456
|
const llm = loadLlmConfig();
|
|
457
|
+
const manifest = readJson(path.join(destDir, 'kdna.json')) || {};
|
|
458
|
+
const core = readJson(path.join(destDir, 'KDNA_Core.json')) || {};
|
|
459
|
+
const pat = readJson(path.join(destDir, 'KDNA_Patterns.json')) || {};
|
|
281
460
|
|
|
282
|
-
if (!jsonMode) {
|
|
461
|
+
if (!jsonMode && !reportMd && !reportJson) {
|
|
283
462
|
console.log('═'.repeat(64));
|
|
284
463
|
console.log(` kdna compare ${parsed.full}`);
|
|
285
464
|
console.log(` provider: ${llm.provider} / ${llm.model}`);
|
|
@@ -296,18 +475,49 @@ async function cmdCompare(input, args = []) {
|
|
|
296
475
|
'You are a helpful assistant. The following domain judgment is loaded and you MUST apply it when relevant.\n\n' +
|
|
297
476
|
kdnaPrompt;
|
|
298
477
|
|
|
299
|
-
if (!jsonMode) console.log('[1/3] Running baseline (no KDNA)...');
|
|
478
|
+
if (!jsonMode && !reportMd && !reportJson) console.log('[1/3] Running baseline (no KDNA)...');
|
|
300
479
|
const responseA = await callLlm(llm, BASELINE_SYSTEM, userInput);
|
|
301
|
-
if (!jsonMode) console.log(` ${responseA.length} chars returned`);
|
|
480
|
+
if (!jsonMode && !reportMd && !reportJson) console.log(` ${responseA.length} chars returned`);
|
|
302
481
|
|
|
303
|
-
if (!jsonMode) console.log('[2/3] Running with KDNA loaded...');
|
|
482
|
+
if (!jsonMode && !reportMd && !reportJson) console.log('[2/3] Running with KDNA loaded...');
|
|
304
483
|
const responseB = await callLlm(llm, TREATMENT_SYSTEM, userInput);
|
|
305
|
-
if (!jsonMode) console.log(` ${responseB.length} chars returned`);
|
|
484
|
+
if (!jsonMode && !reportMd && !reportJson) console.log(` ${responseB.length} chars returned`);
|
|
306
485
|
|
|
307
|
-
if (!jsonMode) console.log('[3/3] Diffing reasoning trajectories...');
|
|
486
|
+
if (!jsonMode && !reportMd && !reportJson) console.log('[3/3] Diffing reasoning trajectories...');
|
|
308
487
|
const diffPrompt = makeDiffPrompt(userInput, responseA, responseB);
|
|
309
488
|
const diff = await callLlm(llm, DIFF_SYSTEM, diffPrompt);
|
|
310
489
|
|
|
490
|
+
// Record trace
|
|
491
|
+
recordTrace({
|
|
492
|
+
timestamp: new Date().toISOString(),
|
|
493
|
+
agent: 'cli',
|
|
494
|
+
domain: parsed.full,
|
|
495
|
+
type: 'compare',
|
|
496
|
+
compare: { model: llm.model, input_length: userInput.length },
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (reportMd) {
|
|
500
|
+
const report = emitMarkdownReport(parsed, manifest, core, pat, responseA, responseB, diff, llm);
|
|
501
|
+
if (outputFile) {
|
|
502
|
+
fs.writeFileSync(outputFile, report);
|
|
503
|
+
console.log(`Report saved to ${outputFile}`);
|
|
504
|
+
} else {
|
|
505
|
+
console.log(report);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (reportJson) {
|
|
511
|
+
const report = emitJsonReport(parsed, manifest, core, pat, responseA, responseB, diff, llm, userInput);
|
|
512
|
+
if (outputFile) {
|
|
513
|
+
fs.writeFileSync(outputFile, JSON.stringify(report, null, 2) + '\n');
|
|
514
|
+
console.log(`Report saved to ${outputFile}`);
|
|
515
|
+
} else {
|
|
516
|
+
console.log(JSON.stringify(report, null, 2));
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
311
521
|
if (jsonMode) {
|
|
312
522
|
const result = {
|
|
313
523
|
baseline_output: responseA,
|