@aikdna/kdna-cli 0.11.0 → 0.13.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/SECURITY.md +41 -0
- package/package.json +3 -2
- package/src/agent.js +22 -2
- package/src/cli.js +16 -1
- package/src/cmds/_common.js +3 -0
- package/src/cmds/doctor.js +165 -92
- package/src/cmds/trace.js +225 -0
- package/src/compare.js +217 -7
- package/src/publish.js +35 -0
- package/src/verify.js +22 -11
package/SECURITY.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
| ------- | ------------------ |
|
|
7
|
+
| 0.11.x | :white_check_mark: |
|
|
8
|
+
| 0.10.x | :white_check_mark: |
|
|
9
|
+
| < 0.10 | :x: |
|
|
10
|
+
|
|
11
|
+
## Reporting a Vulnerability
|
|
12
|
+
|
|
13
|
+
KDNA CLI is the runtime control plane for domain judgment. The primary
|
|
14
|
+
security surface is signature verification, identity key management,
|
|
15
|
+
and registry trust.
|
|
16
|
+
|
|
17
|
+
If you discover a security vulnerability:
|
|
18
|
+
|
|
19
|
+
1. **Do not** open a public issue.
|
|
20
|
+
2. Report by email to security@aikdna.com or via GitHub private vulnerability reporting.
|
|
21
|
+
3. Include: affected version, steps to reproduce, potential impact.
|
|
22
|
+
|
|
23
|
+
We will acknowledge within 5 business days and provide a timeline for a fix.
|
|
24
|
+
|
|
25
|
+
## Scope
|
|
26
|
+
|
|
27
|
+
- `kdna verify --trust`: Ed25519 signature verification
|
|
28
|
+
- `kdna identity init/export/import`: key generation and backup encryption
|
|
29
|
+
- `kdna install`: registry trust chain and SHA-256 verification
|
|
30
|
+
- `kdna publish`: signing and key material handling
|
|
31
|
+
|
|
32
|
+
## Out of Scope
|
|
33
|
+
|
|
34
|
+
- Domain content files (KDNA_*.json) — these are user-authored judgment assets
|
|
35
|
+
- Network-level attacks (man-in-the-middle on registry fetch) — use HTTPS
|
|
36
|
+
- Local filesystem access — CLI runs with user privileges
|
|
37
|
+
|
|
38
|
+
## Supply Chain
|
|
39
|
+
|
|
40
|
+
KDNA CLI publishes to npm as `@aikdna/kdna-cli`. Builds are reproducible
|
|
41
|
+
from source. Dependencies are pinned in `package-lock.json`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"templates/",
|
|
15
15
|
"skills/",
|
|
16
16
|
"LICENSE",
|
|
17
|
-
"NOTICE"
|
|
17
|
+
"NOTICE",
|
|
18
|
+
"SECURITY.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"lint": "eslint src/ validators/ tests/",
|
package/src/agent.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const { parseName } = require('./registry');
|
|
35
|
+
const { recordTrace } = require('./cmds/trace');
|
|
35
36
|
|
|
36
37
|
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
37
38
|
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
@@ -366,6 +367,7 @@ function cmdLoad(input, args = []) {
|
|
|
366
367
|
// JSON format
|
|
367
368
|
if (format === 'json') {
|
|
368
369
|
process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
|
|
370
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'json' });
|
|
369
371
|
return;
|
|
370
372
|
}
|
|
371
373
|
|
|
@@ -378,17 +380,20 @@ function cmdLoad(input, args = []) {
|
|
|
378
380
|
process.stdout.write(fs.readFileSync(p, 'utf8'));
|
|
379
381
|
}
|
|
380
382
|
}
|
|
383
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'raw' });
|
|
381
384
|
return;
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
// Load profiles
|
|
385
388
|
if (profile) {
|
|
386
389
|
emitProfile(parsed, manifest, core, pat, profile, profileInput);
|
|
390
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: `profile:${profile}` });
|
|
387
391
|
return;
|
|
388
392
|
}
|
|
389
393
|
|
|
390
394
|
// Default: --as=prompt — compact text optimized for system-prompt injection.
|
|
391
395
|
emitCompact(parsed, manifest, core, pat);
|
|
396
|
+
recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'prompt' });
|
|
392
397
|
}
|
|
393
398
|
|
|
394
399
|
// ─── Load profiles ─────────────────────────────────────────────────────
|
|
@@ -806,13 +811,21 @@ function cmdPostvalidate(args = []) {
|
|
|
806
811
|
}
|
|
807
812
|
|
|
808
813
|
if (wantJson) {
|
|
809
|
-
|
|
814
|
+
const result = {
|
|
810
815
|
domain: parsed.full,
|
|
811
816
|
violations: results.violations.length,
|
|
812
817
|
warnings: results.warnings.length,
|
|
813
818
|
passed: results.passed.length,
|
|
814
819
|
details: results,
|
|
815
|
-
}
|
|
820
|
+
};
|
|
821
|
+
console.log(JSON.stringify(result, null, 2));
|
|
822
|
+
recordTrace({
|
|
823
|
+
timestamp: new Date().toISOString(),
|
|
824
|
+
agent: 'cli',
|
|
825
|
+
domain: parsed.full,
|
|
826
|
+
type: 'postvalidate',
|
|
827
|
+
postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
|
|
828
|
+
});
|
|
816
829
|
process.exit(results.violations.length ? 1 : 0);
|
|
817
830
|
}
|
|
818
831
|
|
|
@@ -836,6 +849,13 @@ function cmdPostvalidate(args = []) {
|
|
|
836
849
|
results.passed.forEach((p) => console.log(` ✓ ${p}`));
|
|
837
850
|
}
|
|
838
851
|
|
|
852
|
+
recordTrace({
|
|
853
|
+
timestamp: new Date().toISOString(),
|
|
854
|
+
agent: 'cli',
|
|
855
|
+
domain: parsed.full,
|
|
856
|
+
type: 'postvalidate',
|
|
857
|
+
postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
|
|
858
|
+
});
|
|
839
859
|
process.exit(results.violations.length ? 1 : 0);
|
|
840
860
|
}
|
|
841
861
|
|
package/src/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ const { cmdCluster } = require('./cmds/cluster');
|
|
|
23
23
|
const { cmdIdentity } = require('./cmds/identity');
|
|
24
24
|
const { cmdSetup } = require('./cmds/setup');
|
|
25
25
|
const { cmdDoctor } = require('./cmds/doctor');
|
|
26
|
+
const { cmdTrace, cmdHistory } = require('./cmds/trace');
|
|
26
27
|
const { cmdPreview, cmdProject, cmdEval, cmdExport, cmdDemo } = require('./cmds/legacy');
|
|
27
28
|
const { cmdStudioScaffold, cmdCardsValidate, cmdLockVerify, cmdStudioCompile, cmdStudioReadiness } = require('./cmds/studio');
|
|
28
29
|
const { cmdTestRun, cmdTestImport } = require('./cmds/test');
|
|
@@ -79,11 +80,12 @@ Testing & Verification:
|
|
|
79
80
|
verify <name> 3-layer: structure + trust + judgment
|
|
80
81
|
verify <name> --judgment --run-tests Judgment validation with eval cases
|
|
81
82
|
compare <name> --input "..." With/without KDNA reasoning diff
|
|
83
|
+
compare <name> --input "..." --report-md Markdown report format
|
|
84
|
+
compare <name> --input "..." --report-json JSON report with scoring
|
|
82
85
|
diff <name>@<v1> <name>@<v2> Judgment-level diff between versions
|
|
83
86
|
test run <name> --input <file> Record test result against domain
|
|
84
87
|
test import <run> --as-eval Convert test result to eval card
|
|
85
88
|
changelog <name> --from --to Generate judgment changelog
|
|
86
|
-
doctor Check runtime environment health
|
|
87
89
|
|
|
88
90
|
Cluster Composition:
|
|
89
91
|
cluster lint <path> Validate cluster manifest
|
|
@@ -126,6 +128,11 @@ Identity & Signing:
|
|
|
126
128
|
Setup:
|
|
127
129
|
setup One-command setup: CLI + skill + data root
|
|
128
130
|
|
|
131
|
+
Trace & Diagnostics:
|
|
132
|
+
doctor [--agents] [--domains] [--json] System health check
|
|
133
|
+
trace [--json] [--since 7d] [--export <file>] Agent judgment trace
|
|
134
|
+
history [--stats] [--domain <name>] [--agent <name>] Recent usage
|
|
135
|
+
|
|
129
136
|
Flags:
|
|
130
137
|
--json Structured JSON output (machine-readable)
|
|
131
138
|
--quiet Suppress non-error output
|
|
@@ -393,6 +400,14 @@ switch (cmd) {
|
|
|
393
400
|
cmdDoctor(args);
|
|
394
401
|
break;
|
|
395
402
|
}
|
|
403
|
+
case 'trace': {
|
|
404
|
+
cmdTrace(args);
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
case 'history': {
|
|
408
|
+
cmdHistory(args);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
396
411
|
case 'identity': {
|
|
397
412
|
cmdIdentity(args);
|
|
398
413
|
break;
|
package/src/cmds/_common.js
CHANGED
|
@@ -99,6 +99,9 @@ Usage:
|
|
|
99
99
|
|
|
100
100
|
--- Other ---
|
|
101
101
|
kdna setup One-command setup: CLI + skill + data root
|
|
102
|
+
kdna doctor [--agents] [--domains] [--json] System health check
|
|
103
|
+
kdna trace [--json] [--since 7d] [--export <file>] Agent judgment trace
|
|
104
|
+
kdna history [--stats] [--domain <name>] [--agent <name>] Recent usage
|
|
102
105
|
kdna version Show kdna CLI version
|
|
103
106
|
kdna help Show this help
|
|
104
107
|
|
package/src/cmds/doctor.js
CHANGED
|
@@ -4,126 +4,199 @@ const { EXIT, error } = require('./_common');
|
|
|
4
4
|
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
5
5
|
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
6
6
|
|
|
7
|
+
const AGENTS = [
|
|
8
|
+
{ name: 'OpenCode', dir: path.join(process.env.HOME || '', '.agents'), skillsDir: 'skills' },
|
|
9
|
+
{ name: 'Codex', dir: path.join(process.env.HOME || '', '.codex'), skillsDir: 'skills' },
|
|
10
|
+
{ name: 'Claude Code', dir: path.join(process.env.HOME || '', '.claude'), skillsDir: 'skills' },
|
|
11
|
+
{ name: 'Cursor', dir: path.join(process.env.HOME || '', '.cursor'), skillsDir: 'skills' },
|
|
12
|
+
{ name: 'Gemini Antigravity', dir: path.join(process.env.HOME || '', '.gemini', 'antigravity'), skillsDir: 'skills' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const V2_1_MARKER = 'kdna available';
|
|
16
|
+
|
|
17
|
+
function detectAgents() {
|
|
18
|
+
return AGENTS.filter((a) => fs.existsSync(a.dir));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkAgentSkill(agent) {
|
|
22
|
+
const skillPath = path.join(agent.dir, agent.skillsDir, 'kdna-loader', 'SKILL.md');
|
|
23
|
+
if (!fs.existsSync(skillPath)) return { installed: false, version: null, path: skillPath };
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
27
|
+
const isV2 = content.includes(V2_1_MARKER);
|
|
28
|
+
return {
|
|
29
|
+
installed: true,
|
|
30
|
+
version: isV2 ? 'v2026.05' : 'outdated',
|
|
31
|
+
path: skillPath,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return { installed: false, version: null, path: skillPath };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
function cmdDoctor(args) {
|
|
8
39
|
const json = args.includes('--json');
|
|
9
40
|
const quiet = args.includes('--quiet');
|
|
41
|
+
const agentsOnly = args.includes('--agents');
|
|
42
|
+
const domainsOnly = args.includes('--domains');
|
|
10
43
|
|
|
11
44
|
const checks = [];
|
|
12
45
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
if (!agentsOnly) {
|
|
47
|
+
// 1. Node.js version
|
|
48
|
+
const nodeVersion = process.version;
|
|
49
|
+
const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
50
|
+
checks.push({
|
|
51
|
+
name: 'Node.js',
|
|
52
|
+
status: major >= 18 ? 'ok' : 'fail',
|
|
53
|
+
detail: `${nodeVersion} (${major >= 18 ? '>=18 required' : 'requires >=18'})`,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2. @aikdna/kdna-core available
|
|
57
|
+
let coreVersion = null;
|
|
58
|
+
try {
|
|
59
|
+
const corePkg = require.resolve('@aikdna/kdna-core/package.json');
|
|
60
|
+
coreVersion = JSON.parse(fs.readFileSync(corePkg, 'utf8')).version;
|
|
61
|
+
checks.push({ name: '@aikdna/kdna-core', status: 'ok', detail: `v${coreVersion}` });
|
|
62
|
+
} catch {
|
|
63
|
+
checks.push({ name: '@aikdna/kdna-core', status: 'fail', detail: 'not installed' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. ~/.kdna/ exists
|
|
67
|
+
if (fs.existsSync(USER_KDNA_DIR)) {
|
|
68
|
+
checks.push({ name: 'KDNA data directory', status: 'ok', detail: USER_KDNA_DIR });
|
|
69
|
+
} else {
|
|
70
|
+
checks.push({ name: 'KDNA data directory', status: 'warn', detail: '~/.kdna/ not found' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. ~/.kdna/domains/ exists and has domains
|
|
74
|
+
if (fs.existsSync(INSTALL_DIR)) {
|
|
75
|
+
const domains = fs
|
|
76
|
+
.readdirSync(INSTALL_DIR, { withFileTypes: true })
|
|
77
|
+
.filter((d) => d.isDirectory())
|
|
78
|
+
.reduce((acc, scopeDir) => {
|
|
79
|
+
if (scopeDir.name.startsWith('@')) {
|
|
80
|
+
try {
|
|
81
|
+
return acc + fs.readdirSync(path.join(INSTALL_DIR, scopeDir.name)).length;
|
|
82
|
+
} catch {
|
|
83
|
+
return acc;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return acc + 1;
|
|
87
|
+
}, 0);
|
|
88
|
+
checks.push({
|
|
89
|
+
name: 'Installed domains',
|
|
90
|
+
status: domains > 0 ? 'ok' : 'warn',
|
|
91
|
+
detail: `${domains} domain${domains !== 1 ? 's' : ''} installed`,
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
checks.push({ name: 'Domains directory', status: 'warn', detail: '~/.kdna/domains/ not found' });
|
|
95
|
+
}
|
|
30
96
|
}
|
|
31
97
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
98
|
+
if (!domainsOnly) {
|
|
99
|
+
// 5. Agent integration check
|
|
100
|
+
const detected = detectAgents();
|
|
101
|
+
for (const agent of AGENTS) {
|
|
102
|
+
const agentDirExists = fs.existsSync(agent.dir);
|
|
103
|
+
const skill = agentDirExists ? checkAgentSkill(agent) : { installed: false, version: null, path: null };
|
|
104
|
+
|
|
105
|
+
let status, detail;
|
|
106
|
+
if (!agentDirExists) {
|
|
107
|
+
status = 'warn';
|
|
108
|
+
detail = 'agent not detected';
|
|
109
|
+
} else if (skill.installed && skill.version === 'v2026.05') {
|
|
110
|
+
status = 'ok';
|
|
111
|
+
detail = `kdna-loader installed (${skill.version})`;
|
|
112
|
+
} else if (skill.installed && skill.version === 'outdated') {
|
|
113
|
+
status = 'warn';
|
|
114
|
+
detail = 'kdna-loader outdated (run kdna setup --force)';
|
|
115
|
+
} else {
|
|
116
|
+
status = 'warn';
|
|
117
|
+
detail = 'kdna-loader not installed (run kdna setup)';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
checks.push({
|
|
121
|
+
name: `Agent: ${agent.name}`,
|
|
122
|
+
status,
|
|
123
|
+
detail,
|
|
124
|
+
agent: agent.name,
|
|
125
|
+
skillInstalled: skill.installed,
|
|
126
|
+
skillVersion: skill.version,
|
|
127
|
+
skillPath: skill.path,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
37
130
|
}
|
|
38
131
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
return acc + fs.readdirSync(path.join(INSTALL_DIR, scopeDir.name)).length;
|
|
48
|
-
} catch {
|
|
49
|
-
return acc;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return acc + 1;
|
|
53
|
-
}, 0);
|
|
132
|
+
if (!agentsOnly && !domainsOnly) {
|
|
133
|
+
// 6. Identity key available
|
|
134
|
+
const identityDir = path.join(USER_KDNA_DIR, 'identity');
|
|
135
|
+
const identityDirOfficial = path.join(USER_KDNA_DIR, 'identity-official');
|
|
136
|
+
const hasIdentity =
|
|
137
|
+
(fs.existsSync(identityDir) && fs.readdirSync(identityDir).length > 0) ||
|
|
138
|
+
(fs.existsSync(identityDirOfficial) && fs.readdirSync(identityDirOfficial).length > 0);
|
|
54
139
|
checks.push({
|
|
55
|
-
name: '
|
|
56
|
-
status:
|
|
57
|
-
detail:
|
|
140
|
+
name: 'Signing identity',
|
|
141
|
+
status: hasIdentity ? 'ok' : 'warn',
|
|
142
|
+
detail: hasIdentity ? 'key available' : 'no identity (run: kdna identity init)',
|
|
58
143
|
});
|
|
59
|
-
} else {
|
|
60
|
-
checks.push({ name: 'Domains directory', status: 'warn', detail: '~/.kdna/domains/ not found' });
|
|
61
|
-
}
|
|
62
144
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
145
|
+
// 7. Registry cache
|
|
146
|
+
const registryCache = path.join(USER_KDNA_DIR, 'registry-cache.json');
|
|
147
|
+
if (fs.existsSync(registryCache)) {
|
|
148
|
+
try {
|
|
149
|
+
const stat = fs.statSync(registryCache);
|
|
150
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
151
|
+
const ageH = Math.round(ageMs / 3600000);
|
|
152
|
+
const fresh = ageH < 24;
|
|
153
|
+
checks.push({
|
|
154
|
+
name: 'Registry cache',
|
|
155
|
+
status: fresh ? 'ok' : 'warn',
|
|
156
|
+
detail: `updated ${ageH < 1 ? '<1h' : ageH + 'h'} ago`,
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
checks.push({ name: 'Registry cache', status: 'warn', detail: 'cannot read cache' });
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
checks.push({ name: 'Registry cache', status: 'warn', detail: 'not cached (run: kdna registry refresh)' });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 8. Schema files available
|
|
166
|
+
let schemaCount = 0;
|
|
78
167
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
168
|
+
const schemaDir = path.join(
|
|
169
|
+
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
170
|
+
'schema',
|
|
171
|
+
);
|
|
172
|
+
schemaCount = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.schema.json')).length;
|
|
83
173
|
checks.push({
|
|
84
|
-
name: '
|
|
85
|
-
status:
|
|
86
|
-
detail:
|
|
174
|
+
name: 'Schema files',
|
|
175
|
+
status: schemaCount >= 6 ? 'ok' : 'warn',
|
|
176
|
+
detail: `${schemaCount} schemas`,
|
|
87
177
|
});
|
|
88
178
|
} catch {
|
|
89
|
-
checks.push({ name: '
|
|
179
|
+
checks.push({ name: 'Schema files', status: 'fail', detail: 'not found' });
|
|
90
180
|
}
|
|
91
|
-
} else {
|
|
92
|
-
checks.push({ name: 'Registry cache', status: 'warn', detail: 'not cached (run: kdna registry refresh)' });
|
|
93
|
-
}
|
|
94
181
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'
|
|
101
|
-
|
|
102
|
-
schemaCount = fs.readdirSync(schemaDir).filter((f) => f.endsWith('.schema.json')).length;
|
|
103
|
-
checks.push({
|
|
104
|
-
name: 'Schema files',
|
|
105
|
-
status: schemaCount >= 6 ? 'ok' : 'warn',
|
|
106
|
-
detail: `${schemaCount} schemas`,
|
|
107
|
-
});
|
|
108
|
-
} catch {
|
|
109
|
-
checks.push({ name: 'Schema files', status: 'fail', detail: 'not found' });
|
|
182
|
+
// 9. Project .kdna/config.json
|
|
183
|
+
const projectConfig = path.join(process.cwd(), '.kdna', 'config.json');
|
|
184
|
+
if (fs.existsSync(projectConfig)) {
|
|
185
|
+
checks.push({ name: 'Project config', status: 'ok', detail: projectConfig });
|
|
186
|
+
} else {
|
|
187
|
+
checks.push({ name: 'Project config', status: 'warn', detail: 'No .kdna/config.json in current project' });
|
|
188
|
+
}
|
|
110
189
|
}
|
|
111
190
|
|
|
112
|
-
//
|
|
113
|
-
const projectConfig = path.join(process.cwd(), '.kdna', 'config.json');
|
|
114
|
-
if (fs.existsSync(projectConfig)) {
|
|
115
|
-
checks.push({ name: 'Project config', status: 'ok', detail: projectConfig });
|
|
116
|
-
} else {
|
|
117
|
-
checks.push({ name: 'Project config', status: 'warn', detail: 'No .kdna/config.json in current project' });
|
|
118
|
-
}
|
|
191
|
+
// ── Output ───────────────────────────────────────────────────────
|
|
119
192
|
|
|
120
|
-
// Output
|
|
121
193
|
if (json) {
|
|
122
194
|
const result = {
|
|
123
195
|
checks: checks.map((c) => ({
|
|
124
196
|
name: c.name,
|
|
125
197
|
status: c.status,
|
|
126
198
|
detail: c.detail,
|
|
199
|
+
...(c.agent && { agent: c.agent, skillInstalled: c.skillInstalled, skillVersion: c.skillVersion }),
|
|
127
200
|
})),
|
|
128
201
|
ok: checks.filter((c) => c.status === 'ok').length,
|
|
129
202
|
warnings: checks.filter((c) => c.status === 'warn').length,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { EXIT, error, readJson } = require('./_common');
|
|
5
|
+
|
|
6
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
7
|
+
const TRACES_DIR = path.join(USER_KDNA_DIR, 'traces');
|
|
8
|
+
|
|
9
|
+
function ensureTracesDir() {
|
|
10
|
+
fs.mkdirSync(TRACES_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function todayFile() {
|
|
14
|
+
const d = new Date();
|
|
15
|
+
const yyyy = d.getFullYear();
|
|
16
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
18
|
+
return path.join(TRACES_DIR, `${yyyy}-${mm}-${dd}.jsonl`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function traceFiles(sinceDate) {
|
|
22
|
+
ensureTracesDir();
|
|
23
|
+
let files = fs.readdirSync(TRACES_DIR).filter((f) => f.endsWith('.jsonl')).sort();
|
|
24
|
+
if (sinceDate) {
|
|
25
|
+
const since = sinceDate instanceof Date ? sinceDate : new Date(sinceDate);
|
|
26
|
+
files = files.filter((f) => {
|
|
27
|
+
const d = f.replace('.jsonl', '');
|
|
28
|
+
return new Date(d) >= since;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return files.map((f) => path.join(TRACES_DIR, f));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readAllTraces(opts = {}) {
|
|
35
|
+
const { since, agent, domain } = opts;
|
|
36
|
+
const entries = [];
|
|
37
|
+
const files = traceFiles(since);
|
|
38
|
+
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
try {
|
|
41
|
+
const lines = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean);
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
try {
|
|
44
|
+
const entry = JSON.parse(line);
|
|
45
|
+
if (agent && entry.agent !== agent) continue;
|
|
46
|
+
if (domain && entry.domain !== domain) continue;
|
|
47
|
+
entries.push(entry);
|
|
48
|
+
} catch { /* skip malformed lines */ }
|
|
49
|
+
}
|
|
50
|
+
} catch { /* skip unreadable files */ }
|
|
51
|
+
}
|
|
52
|
+
return entries;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function recordTrace(entry) {
|
|
56
|
+
ensureTracesDir();
|
|
57
|
+
const line = JSON.stringify(entry) + '\n';
|
|
58
|
+
fs.appendFileSync(todayFile(), line);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseSinceFlag(args) {
|
|
62
|
+
const idx = args.indexOf('--since');
|
|
63
|
+
if (idx >= 0 && idx < args.length - 1) {
|
|
64
|
+
const val = args[idx + 1];
|
|
65
|
+
if (val === '7d') {
|
|
66
|
+
const d = new Date();
|
|
67
|
+
d.setDate(d.getDate() - 7);
|
|
68
|
+
return d;
|
|
69
|
+
}
|
|
70
|
+
if (val === '30d') {
|
|
71
|
+
const d = new Date();
|
|
72
|
+
d.setDate(d.getDate() - 30);
|
|
73
|
+
return d;
|
|
74
|
+
}
|
|
75
|
+
if (val === '90d') {
|
|
76
|
+
const d = new Date();
|
|
77
|
+
d.setDate(d.getDate() - 90);
|
|
78
|
+
return d;
|
|
79
|
+
}
|
|
80
|
+
// ISO date
|
|
81
|
+
const parsed = new Date(val);
|
|
82
|
+
if (!isNaN(parsed.getTime())) return parsed;
|
|
83
|
+
}
|
|
84
|
+
// default: last 7 days
|
|
85
|
+
const d = new Date();
|
|
86
|
+
d.setDate(d.getDate() - 7);
|
|
87
|
+
return d;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cmdTrace(args) {
|
|
91
|
+
const json = args.includes('--json');
|
|
92
|
+
const exportPath = args.includes('--export') ? args[args.indexOf('--export') + 1] : null;
|
|
93
|
+
const clear = args.includes('--clear');
|
|
94
|
+
const since = parseSinceFlag(args);
|
|
95
|
+
|
|
96
|
+
if (clear) {
|
|
97
|
+
if (fs.existsSync(TRACES_DIR)) {
|
|
98
|
+
const files = fs.readdirSync(TRACES_DIR).filter((f) => f.endsWith('.jsonl'));
|
|
99
|
+
for (const f of files) fs.unlinkSync(path.join(TRACES_DIR, f));
|
|
100
|
+
}
|
|
101
|
+
console.log('Trace logs cleared.');
|
|
102
|
+
process.exit(EXIT.OK);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entries = readAllTraces({ since });
|
|
106
|
+
|
|
107
|
+
if (exportPath) {
|
|
108
|
+
const data = {
|
|
109
|
+
period: { since: since.toISOString(), until: new Date().toISOString() },
|
|
110
|
+
entries,
|
|
111
|
+
};
|
|
112
|
+
fs.writeFileSync(exportPath, JSON.stringify(data, null, 2) + '\n');
|
|
113
|
+
console.log(`Exported ${entries.length} trace entries to ${exportPath}`);
|
|
114
|
+
process.exit(EXIT.OK);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (json) {
|
|
118
|
+
console.log(JSON.stringify({ entries, count: entries.length }, null, 2));
|
|
119
|
+
process.exit(EXIT.OK);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Human-readable table
|
|
123
|
+
if (entries.length === 0) {
|
|
124
|
+
console.log('No trace entries found.');
|
|
125
|
+
console.log('Load a domain via kdna load or use KDNA in an agent to generate traces.');
|
|
126
|
+
process.exit(EXIT.OK);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`${'Timestamp'.padEnd(20)} ${'Agent'.padEnd(15)} ${'Domain'.padEnd(25)} ${'Result'}`);
|
|
130
|
+
console.log('-'.repeat(75));
|
|
131
|
+
for (const e of entries.slice(-50).reverse()) {
|
|
132
|
+
const ts = e.timestamp ? new Date(e.timestamp).toISOString().replace('T', ' ').slice(0, 19) : 'unknown';
|
|
133
|
+
const agent = (e.agent || 'unknown').padEnd(15);
|
|
134
|
+
const domain = (e.domain || '(none)').padEnd(25);
|
|
135
|
+
const result = e.postvalidate?.result || 'loaded';
|
|
136
|
+
console.log(`${ts} ${agent} ${domain} ${result}`);
|
|
137
|
+
}
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(`${entries.length} entries total. --export <file> for audit export. --clear to reset.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cmdHistory(args) {
|
|
143
|
+
const json = args.includes('--json');
|
|
144
|
+
const stats = args.includes('--stats');
|
|
145
|
+
const agentFilter = args.includes('--agent') ? args[args.indexOf('--agent') + 1] : null;
|
|
146
|
+
const domainFilter = args.includes('--domain') ? args[args.indexOf('--domain') + 1] : null;
|
|
147
|
+
const count = parseInt(args.includes('-n') ? args[args.indexOf('-n') + 1] : '20', 10);
|
|
148
|
+
|
|
149
|
+
const entries = readAllTraces({ agent: agentFilter, domain: domainFilter });
|
|
150
|
+
|
|
151
|
+
if (stats) {
|
|
152
|
+
const total = entries.length;
|
|
153
|
+
const domainCounts = {};
|
|
154
|
+
const agentCounts = {};
|
|
155
|
+
let skipped = 0;
|
|
156
|
+
|
|
157
|
+
for (const e of entries) {
|
|
158
|
+
if (e.domain) {
|
|
159
|
+
domainCounts[e.domain] = (domainCounts[e.domain] || 0) + 1;
|
|
160
|
+
} else {
|
|
161
|
+
skipped++;
|
|
162
|
+
}
|
|
163
|
+
if (e.agent) {
|
|
164
|
+
agentCounts[e.agent] = (agentCounts[e.agent] || 0) + 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (json) {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
total,
|
|
171
|
+
skipped,
|
|
172
|
+
domainCounts,
|
|
173
|
+
agentCounts,
|
|
174
|
+
skipRate: total > 0 ? Math.round((skipped / total) * 100) : 0,
|
|
175
|
+
}, null, 2));
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`Total KDNA loads: ${total}`);
|
|
178
|
+
console.log(`Skipped (no domain): ${skipped}`);
|
|
179
|
+
if (total > 0) console.log(`Skip rate: ${Math.round((skipped / total) * 100)}%`);
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log('By domain:');
|
|
182
|
+
const sortedDomains = Object.entries(domainCounts).sort((a, b) => b[1] - a[1]);
|
|
183
|
+
for (const [domain, c] of sortedDomains) {
|
|
184
|
+
const pct = total > 0 ? Math.round((c / total) * 100) : 0;
|
|
185
|
+
console.log(` ${domain}: ${c} (${pct}%)`);
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(agentCounts).length > 0) {
|
|
188
|
+
console.log('');
|
|
189
|
+
console.log('By agent:');
|
|
190
|
+
for (const [agent, c] of Object.entries(agentCounts)) {
|
|
191
|
+
console.log(` ${agent}: ${c}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
process.exit(EXIT.OK);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Recent entries
|
|
199
|
+
const recent = entries.slice(-count).reverse();
|
|
200
|
+
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({ entries: recent, total: entries.length }, null, 2));
|
|
203
|
+
process.exit(EXIT.OK);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (recent.length === 0) {
|
|
207
|
+
console.log('No history entries found.');
|
|
208
|
+
process.exit(EXIT.OK);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`${'Timestamp'.padEnd(20)} ${'Agent'.padEnd(15)} ${'Domain'.padEnd(28)} ${'Result'.padEnd(10)} ${'Score'}`);
|
|
212
|
+
console.log('-'.repeat(85));
|
|
213
|
+
for (const e of recent) {
|
|
214
|
+
const ts = e.timestamp ? new Date(e.timestamp).toISOString().replace('T', ' ').slice(0, 19) : 'unknown';
|
|
215
|
+
const agent = (e.agent || 'unknown').padEnd(15);
|
|
216
|
+
const domain = (e.domain || '(none)').padEnd(28);
|
|
217
|
+
const result = (e.postvalidate?.result || 'loaded').padEnd(10);
|
|
218
|
+
const score = e.postvalidate?.score ? e.postvalidate.score.toFixed(1) : '-';
|
|
219
|
+
console.log(`${ts} ${agent} ${domain} ${result} ${score}`);
|
|
220
|
+
}
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(`Showing ${recent.length} of ${entries.length} total entries. --stats for summary. --domain <name> to filter.`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { cmdTrace, cmdHistory, recordTrace, readAllTraces };
|
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,
|
package/src/publish.js
CHANGED
|
@@ -47,6 +47,33 @@ const SLOGAN_PATTERNS = [
|
|
|
47
47
|
/^[A-Z][a-z]+ matters\.?$/, // "Quality matters."
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
+
// ─── Anti-SOP checks ──────────────────────────────────────────────────
|
|
51
|
+
// Detects when KDNA content degrades into procedural instructions
|
|
52
|
+
// rather than judgment principles. Axioms should express what to
|
|
53
|
+
// PRIORITIZE or AVOID, not steps to follow.
|
|
54
|
+
|
|
55
|
+
const SOP_PATTERNS = [
|
|
56
|
+
/^Step\s+\d/i, // "Step 1: identify the topic"
|
|
57
|
+
/^First,?\s|^Next,?\s|^Then,?\s|^Finally,?\s/i, // "First, do X. Then do Y."
|
|
58
|
+
/^Check\s(for|if|whether)\s/i, // "Check for spelling errors"
|
|
59
|
+
/^Always\s+(use|do|make|include)/i, // "Always use active voice"
|
|
60
|
+
/^Never\s+(use|do|make)/i, // "Never use passive voice"
|
|
61
|
+
/^Generate\s/i, // "Generate three options"
|
|
62
|
+
/^Create\s+(a|the)\s/i, // "Create a list of..."
|
|
63
|
+
/^Make\s+sure\s/i, // "Make sure to check..."
|
|
64
|
+
/^Remember\s+to\s/i, // "Remember to validate..."
|
|
65
|
+
/^(You|The agent)\s+should\s+(use|do|make|include)/i, // "You should use X"
|
|
66
|
+
/^Avoid\s+(using|doing)/i, // "Avoid using X" (too procedural)
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function isSOP(text) {
|
|
70
|
+
if (!text || typeof text !== 'string') return false;
|
|
71
|
+
for (const pattern of SOP_PATTERNS) {
|
|
72
|
+
if (pattern.test(text.trim())) return { pattern: pattern.source, text };
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
function isVague(text) {
|
|
51
78
|
if (!text || typeof text !== 'string') return false;
|
|
52
79
|
const lower = text.toLowerCase();
|
|
@@ -168,6 +195,14 @@ function cmdPublishCheck(domainPath) {
|
|
|
168
195
|
ax.one_sentence,
|
|
169
196
|
'Reads like a slogan. Axioms must be specific judgment principles.',
|
|
170
197
|
);
|
|
198
|
+
} else if (isSOP(ax.one_sentence)) {
|
|
199
|
+
const s = isSOP(ax.one_sentence);
|
|
200
|
+
fail(
|
|
201
|
+
'KDNA_Core.json',
|
|
202
|
+
`axioms.${label}.one_sentence`,
|
|
203
|
+
ax.one_sentence,
|
|
204
|
+
`Reads like a SOP ("${s.pattern}"). Axioms must be judgment principles, not step-by-step instructions.`,
|
|
205
|
+
);
|
|
171
206
|
} else if (isVague(ax.one_sentence)) {
|
|
172
207
|
const v = isVague(ax.one_sentence);
|
|
173
208
|
fail(
|
package/src/verify.js
CHANGED
|
@@ -217,7 +217,7 @@ function checkJudgment(destDir) {
|
|
|
217
217
|
const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
|
|
218
218
|
const manifest = readJson(path.join(destDir, 'kdna.json'));
|
|
219
219
|
|
|
220
|
-
// 1. Boundary declaration in README
|
|
220
|
+
// 1. Boundary declaration in README (REQUIRED)
|
|
221
221
|
// Either classic "## Scope" + "## Out of Scope" pair,
|
|
222
222
|
// OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
|
|
223
223
|
const readmePath = path.join(destDir, 'README.md');
|
|
@@ -238,9 +238,12 @@ function checkJudgment(destDir) {
|
|
|
238
238
|
if ((hasScope && hasOutOfScope) || hasFourQuestions) {
|
|
239
239
|
bump(2, 2, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
|
|
240
240
|
} else if (hasScope || hasOutOfScope) {
|
|
241
|
-
|
|
241
|
+
score.max += 2;
|
|
242
|
+
score.total += 1;
|
|
243
|
+
issues.push({ severity: 'warn', msg: 'partial: README boundary declaration incomplete (missing Scope or Out-of-Scope section)' });
|
|
242
244
|
} else {
|
|
243
|
-
|
|
245
|
+
score.max += 2;
|
|
246
|
+
issues.push({ severity: 'error', msg: 'README missing boundary declaration: require ## Scope + ## Out of Scope (or v2.1 Four Questions)' });
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
// 2. v2.1 axiom governance fields
|
|
@@ -309,23 +312,31 @@ function checkJudgment(destDir) {
|
|
|
309
312
|
issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
// 5. eval cases present
|
|
315
|
+
// 5. eval cases present (REQUIRED: ≥4 cases)
|
|
313
316
|
const evalDir = path.join(destDir, 'evals');
|
|
314
317
|
if (fs.existsSync(evalDir)) {
|
|
315
318
|
const files = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
|
|
316
|
-
if (files.length >= 4)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
319
|
+
if (files.length >= 4) {
|
|
320
|
+
bump(2, 2, `evals/ directory has ${files.length} case files`);
|
|
321
|
+
} else if (files.length > 0) {
|
|
322
|
+
score.max += 2;
|
|
323
|
+
score.total += 1;
|
|
324
|
+
issues.push({ severity: 'warn', msg: `evals/ has only ${files.length} files (require ≥4: core/boundary/failure/excluded)` });
|
|
325
|
+
} else {
|
|
326
|
+
score.max += 2;
|
|
327
|
+
issues.push({ severity: 'error', msg: 'evals/ directory exists but contains no case files' });
|
|
328
|
+
}
|
|
320
329
|
} else {
|
|
321
|
-
|
|
330
|
+
score.max += 2;
|
|
331
|
+
issues.push({ severity: 'error', msg: 'evals/ directory missing: require ≥4 evaluation cases' });
|
|
322
332
|
}
|
|
323
333
|
|
|
324
|
-
// 6. judgment_version manifest field
|
|
334
|
+
// 6. judgment_version manifest field (REQUIRED)
|
|
325
335
|
if (manifest?.judgment_version) {
|
|
326
336
|
bump(1, 1, `judgment_version: ${manifest.judgment_version}`);
|
|
327
337
|
} else {
|
|
328
|
-
|
|
338
|
+
score.max += 1;
|
|
339
|
+
issues.push({ severity: 'error', msg: 'kdna.json missing required field: judgment_version' });
|
|
329
340
|
}
|
|
330
341
|
|
|
331
342
|
return { layer: 'judgment', issues, passed, score };
|