@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 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.11.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
- console.log(JSON.stringify({
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
- }, null, 2));
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;
@@ -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
 
@@ -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
- // 1. Node.js version
14
- const nodeVersion = process.version;
15
- const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
16
- checks.push({
17
- name: 'Node.js',
18
- status: major >= 18 ? 'ok' : 'fail',
19
- detail: `${nodeVersion} (${major >= 18 ? '>=18 required' : 'requires >=18'})`,
20
- });
21
-
22
- // 2. @aikdna/kdna-core available
23
- let coreVersion = null;
24
- try {
25
- const corePkg = require.resolve('@aikdna/kdna-core/package.json');
26
- coreVersion = JSON.parse(fs.readFileSync(corePkg, 'utf8')).version;
27
- checks.push({ name: '@aikdna/kdna-core', status: 'ok', detail: `v${coreVersion}` });
28
- } catch {
29
- checks.push({ name: '@aikdna/kdna-core', status: 'fail', detail: 'not installed' });
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
- // 3. ~/.kdna/ exists
33
- if (fs.existsSync(USER_KDNA_DIR)) {
34
- checks.push({ name: 'KDNA data directory', status: 'ok', detail: USER_KDNA_DIR });
35
- } else {
36
- checks.push({ name: 'KDNA data directory', status: 'warn', detail: `~/.kdna/ not found` });
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
- // 4. ~/.kdna/domains/ exists and has domains
40
- if (fs.existsSync(INSTALL_DIR)) {
41
- const domains = fs
42
- .readdirSync(INSTALL_DIR, { withFileTypes: true })
43
- .filter((d) => d.isDirectory())
44
- .reduce((acc, scopeDir) => {
45
- if (scopeDir.name.startsWith('@')) {
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: 'Installed domains',
56
- status: domains > 0 ? 'ok' : 'warn',
57
- detail: `${domains} domain${domains !== 1 ? 's' : ''} installed`,
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
- // 5. Identity key available
64
- const identityDir = path.join(USER_KDNA_DIR, 'identity');
65
- const identityDirOfficial = path.join(USER_KDNA_DIR, 'identity-official');
66
- const hasIdentity =
67
- (fs.existsSync(identityDir) && fs.readdirSync(identityDir).length > 0) ||
68
- (fs.existsSync(identityDirOfficial) && fs.readdirSync(identityDirOfficial).length > 0);
69
- checks.push({
70
- name: 'Signing identity',
71
- status: hasIdentity ? 'ok' : 'warn',
72
- detail: hasIdentity ? 'key available' : 'no identity (run: kdna identity init)',
73
- });
74
-
75
- // 6. Registry cache
76
- const registryCache = path.join(USER_KDNA_DIR, 'registry-cache.json');
77
- if (fs.existsSync(registryCache)) {
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 stat = fs.statSync(registryCache);
80
- const ageMs = Date.now() - stat.mtimeMs;
81
- const ageH = Math.round(ageMs / 3600000);
82
- const fresh = ageH < 24;
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: 'Registry cache',
85
- status: fresh ? 'ok' : 'warn',
86
- detail: `updated ${ageH < 1 ? '<1h' : ageH + 'h'} ago`,
174
+ name: 'Schema files',
175
+ status: schemaCount >= 6 ? 'ok' : 'warn',
176
+ detail: `${schemaCount} schemas`,
87
177
  });
88
178
  } catch {
89
- checks.push({ name: 'Registry cache', status: 'warn', detail: 'cannot read cache' });
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
- // 7. Schema files available
96
- let schemaCount = 0;
97
- try {
98
- const schemaDir = path.join(
99
- path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
100
- 'schema',
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
- // 8. Project .kdna/config.json
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
- bump(2, 1, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
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
- bump(2, 0, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
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) bump(2, 2, `evals/ directory has ${files.length} case files`);
317
- else if (files.length > 0)
318
- bump(2, 1, `evals/ has ${files.length} files (recommend ≥4: core/boundary/failure/excluded)`);
319
- else bump(2, 0, 'evals/ has case files');
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
- bump(2, 0, 'evals/ directory present');
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
- bump(1, 0, 'kdna.json has judgment_version');
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 };