@aikdna/kdna-cli 0.9.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +73 -0
- package/package.json +58 -0
- package/skills/kdna-loader/SKILL.md +257 -0
- package/src/agent.js +434 -0
- package/src/cli.js +260 -0
- package/src/cluster.js +235 -0
- package/src/cmds/_common.js +100 -0
- package/src/cmds/cluster.js +235 -0
- package/src/cmds/domain.js +638 -0
- package/src/cmds/identity.js +31 -0
- package/src/cmds/legacy.js +83 -0
- package/src/cmds/quality.js +87 -0
- package/src/cmds/registry.js +114 -0
- package/src/cmds/setup.js +8 -0
- package/src/compare.js +324 -0
- package/src/diff.js +288 -0
- package/src/identity.js +211 -0
- package/src/init.js +168 -0
- package/src/install.js +849 -0
- package/src/loader.js +70 -0
- package/src/publish.js +600 -0
- package/src/registry.js +258 -0
- package/src/search.js +73 -0
- package/src/setup.js +197 -0
- package/src/verify.js +423 -0
- package/src/version.js +112 -0
- package/templates/cluster/KDNA_Cluster.json +25 -0
- package/templates/cluster/README.md +32 -0
- package/templates/minimal-domain/KDNA_Core.json +54 -0
- package/templates/minimal-domain/KDNA_Patterns.json +37 -0
- package/templates/minimal-domain/kdna.json +31 -0
- package/templates/minimal-domain/tests/before-after.json +16 -0
- package/templates/standard-domain/KDNA_Core.json +76 -0
- package/templates/standard-domain/KDNA_Patterns.json +44 -0
- package/templates/standard-domain/README.md +74 -0
- package/templates/standard-domain/USAGE.md +59 -0
- package/templates/standard-domain/evals/1_excluded_case.json +16 -0
- package/templates/standard-domain/evals/3_boundary_cases.json +38 -0
- package/templates/standard-domain/evals/3_core_cases.json +35 -0
- package/templates/standard-domain/evals/3_failure_cases.json +35 -0
- package/templates/standard-domain/evals/scoring.json +60 -0
- package/templates/standard-domain/kdna.json +28 -0
- package/validators/kdna-lint.js +53 -0
- package/validators/kdna-validate.js +92 -0
package/src/verify.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kdna verify <name> — Quality signal across three layers.
|
|
3
|
+
*
|
|
4
|
+
* --structure files exist, schema OK
|
|
5
|
+
* --trust sha256 + Ed25519 signature against scope trust key
|
|
6
|
+
* --judgment v2.1 governance fields (boundary, applies_when, eval cases)
|
|
7
|
+
*
|
|
8
|
+
* No flag = run all three.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 all checks passed (warnings allowed)
|
|
12
|
+
* 1 one or more layers failed
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { RegistryResolver, parseName } = require('./registry');
|
|
19
|
+
|
|
20
|
+
const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
21
|
+
const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
|
|
22
|
+
|
|
23
|
+
function readJson(p) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Structure layer ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function checkStructure(destDir) {
|
|
34
|
+
const issues = [];
|
|
35
|
+
const passed = [];
|
|
36
|
+
|
|
37
|
+
const required = ['KDNA_Core.json', 'KDNA_Patterns.json', 'kdna.json'];
|
|
38
|
+
const optional = [
|
|
39
|
+
'KDNA_Scenarios.json',
|
|
40
|
+
'KDNA_Cases.json',
|
|
41
|
+
'KDNA_Reasoning.json',
|
|
42
|
+
'KDNA_Evolution.json',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const f of required) {
|
|
46
|
+
if (!fs.existsSync(path.join(destDir, f))) {
|
|
47
|
+
issues.push({ severity: 'error', msg: `required file missing: ${f}` });
|
|
48
|
+
} else {
|
|
49
|
+
passed.push(`has ${f}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const f of optional) {
|
|
54
|
+
if (fs.existsSync(path.join(destDir, f))) passed.push(`has ${f}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Schema check using kdna-core if available
|
|
58
|
+
try {
|
|
59
|
+
const core = readJson(path.join(destDir, 'KDNA_Core.json'));
|
|
60
|
+
if (core) {
|
|
61
|
+
if (!core.axioms || !Array.isArray(core.axioms) || core.axioms.length === 0) {
|
|
62
|
+
issues.push({ severity: 'error', msg: 'KDNA_Core.axioms missing or empty' });
|
|
63
|
+
} else passed.push(`${core.axioms.length} axioms`);
|
|
64
|
+
if (!core.ontology || !Array.isArray(core.ontology) || core.ontology.length === 0) {
|
|
65
|
+
issues.push({ severity: 'warn', msg: 'KDNA_Core.ontology missing or empty' });
|
|
66
|
+
}
|
|
67
|
+
if (!core.stances || !Array.isArray(core.stances) || core.stances.length === 0) {
|
|
68
|
+
issues.push({ severity: 'warn', msg: 'KDNA_Core.stances missing or empty' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
|
|
72
|
+
if (pat) {
|
|
73
|
+
if (!pat.misunderstandings || pat.misunderstandings.length === 0) {
|
|
74
|
+
issues.push({ severity: 'warn', msg: 'KDNA_Patterns.misunderstandings missing or empty' });
|
|
75
|
+
} else passed.push(`${pat.misunderstandings.length} misunderstandings`);
|
|
76
|
+
if (!pat.self_check || pat.self_check.length < 3) {
|
|
77
|
+
issues.push({
|
|
78
|
+
severity: 'warn',
|
|
79
|
+
msg: `KDNA_Patterns.self_check has ${(pat.self_check || []).length} entries (recommend ≥3)`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
issues.push({ severity: 'error', msg: `schema parse failed: ${e.message}` });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { layer: 'structure', issues, passed };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Trust layer ───────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function checkTrust(destDir, scope, entry) {
|
|
93
|
+
const issues = [];
|
|
94
|
+
const passed = [];
|
|
95
|
+
|
|
96
|
+
const manifest = readJson(path.join(destDir, 'kdna.json'));
|
|
97
|
+
if (!manifest) {
|
|
98
|
+
issues.push({ severity: 'error', msg: 'kdna.json missing — cannot verify trust' });
|
|
99
|
+
return { layer: 'trust', issues, passed };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 1. author.pubkey
|
|
103
|
+
if (!manifest.author?.pubkey) {
|
|
104
|
+
issues.push({ severity: 'error', msg: 'author.pubkey missing in kdna.json' });
|
|
105
|
+
} else {
|
|
106
|
+
passed.push(`author.pubkey present`);
|
|
107
|
+
if (scope?.trust_pubkey && manifest.author.pubkey !== scope.trust_pubkey) {
|
|
108
|
+
issues.push({
|
|
109
|
+
severity: 'error',
|
|
110
|
+
msg: `author.pubkey does not match scope trust_pubkey`,
|
|
111
|
+
});
|
|
112
|
+
} else if (scope?.trust_pubkey) {
|
|
113
|
+
passed.push('author.pubkey matches scope trust_pubkey');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2. signature
|
|
118
|
+
if (!manifest.signature) {
|
|
119
|
+
issues.push({ severity: 'error', msg: 'signature missing in kdna.json' });
|
|
120
|
+
} else {
|
|
121
|
+
passed.push('signature present');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. embedded PEM (v0.7.1+)
|
|
125
|
+
if (!manifest.author?.public_key_pem) {
|
|
126
|
+
issues.push({
|
|
127
|
+
severity: 'warn',
|
|
128
|
+
msg: 'author.public_key_pem missing (pre-v0.7.1 package — full Ed25519 verify unavailable)',
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
passed.push('embedded public_key_pem present');
|
|
132
|
+
|
|
133
|
+
// Recompute fingerprint
|
|
134
|
+
const fp =
|
|
135
|
+
'ed25519:' + crypto.createHash('sha256').update(manifest.author.public_key_pem).digest('hex');
|
|
136
|
+
if (fp !== manifest.author.pubkey) {
|
|
137
|
+
issues.push({
|
|
138
|
+
severity: 'error',
|
|
139
|
+
msg: 'embedded PEM does not match author.pubkey fingerprint',
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
passed.push('PEM fingerprint matches author.pubkey');
|
|
143
|
+
|
|
144
|
+
// Full Ed25519 verify
|
|
145
|
+
try {
|
|
146
|
+
const files = fs
|
|
147
|
+
.readdirSync(destDir)
|
|
148
|
+
.filter((f) => f.endsWith('.json'))
|
|
149
|
+
.sort();
|
|
150
|
+
const parts = [];
|
|
151
|
+
for (const f of files) {
|
|
152
|
+
const full = path.join(destDir, f);
|
|
153
|
+
let buf;
|
|
154
|
+
if (f === 'kdna.json') {
|
|
155
|
+
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
156
|
+
delete obj.signature;
|
|
157
|
+
delete obj._source;
|
|
158
|
+
buf = Buffer.from(JSON.stringify(obj));
|
|
159
|
+
} else {
|
|
160
|
+
buf = fs.readFileSync(full);
|
|
161
|
+
}
|
|
162
|
+
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
163
|
+
parts.push(`${f}:${hash}`);
|
|
164
|
+
}
|
|
165
|
+
const payload = parts.join('\n');
|
|
166
|
+
const sigHex = manifest.signature.replace(/^ed25519:/, '');
|
|
167
|
+
const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
|
|
168
|
+
const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
|
|
169
|
+
if (ok) passed.push('Ed25519 signature VALID over canonical payload');
|
|
170
|
+
else
|
|
171
|
+
issues.push({
|
|
172
|
+
severity: 'error',
|
|
173
|
+
msg: 'Ed25519 signature INVALID — package may be tampered',
|
|
174
|
+
});
|
|
175
|
+
} catch (e) {
|
|
176
|
+
issues.push({ severity: 'error', msg: `signature verify failed: ${e.message}` });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4. sha256 vs registry (if entry provided)
|
|
182
|
+
if (entry?.sha256) {
|
|
183
|
+
passed.push(`registry sha256 declared: ${entry.sha256.slice(0, 16)}…`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. scope governance
|
|
187
|
+
if (scope) {
|
|
188
|
+
passed.push(`scope type: ${scope.type}`);
|
|
189
|
+
if (scope.type === 'private' && !scope.registry_url) {
|
|
190
|
+
issues.push({ severity: 'error', msg: 'private scope missing registry_url' });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { layer: 'trust', issues, passed };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Judgment layer ────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function checkJudgment(destDir) {
|
|
200
|
+
const issues = [];
|
|
201
|
+
const passed = [];
|
|
202
|
+
const score = { total: 0, max: 0 };
|
|
203
|
+
|
|
204
|
+
function bump(max, gain, label) {
|
|
205
|
+
score.max += max;
|
|
206
|
+
score.total += gain;
|
|
207
|
+
if (gain === max) passed.push(`✓ ${label}`);
|
|
208
|
+
else if (gain > 0) issues.push({ severity: 'warn', msg: `partial: ${label} (${gain}/${max})` });
|
|
209
|
+
else issues.push({ severity: 'warn', msg: `missing: ${label}` });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const core = readJson(path.join(destDir, 'KDNA_Core.json'));
|
|
213
|
+
const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
|
|
214
|
+
const manifest = readJson(path.join(destDir, 'kdna.json'));
|
|
215
|
+
|
|
216
|
+
// 1. Boundary declaration in README
|
|
217
|
+
// Either classic "## Scope" + "## Out of Scope" pair,
|
|
218
|
+
// OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
|
|
219
|
+
const readmePath = path.join(destDir, 'README.md');
|
|
220
|
+
let readme = '';
|
|
221
|
+
try {
|
|
222
|
+
readme = fs.readFileSync(readmePath, 'utf8');
|
|
223
|
+
} catch {
|
|
224
|
+
/* ok */
|
|
225
|
+
}
|
|
226
|
+
const hasScope = /^##\s+Scope\b/im.test(readme);
|
|
227
|
+
const hasOutOfScope = /^##\s+(Out of Scope|Out-of-Scope|Not for|Where this does)/im.test(readme);
|
|
228
|
+
const hasFourQuestions =
|
|
229
|
+
/(Four Questions|四个问题|四问)/i.test(readme) &&
|
|
230
|
+
/(Where (does it|it) apply|适用|2\.\s+(Where|Applies))/i.test(readme) &&
|
|
231
|
+
/(does(?:\s+it)?\s+NOT\s+apply|when it does not apply|何时不|when not to (use|load))/i.test(
|
|
232
|
+
readme,
|
|
233
|
+
);
|
|
234
|
+
if ((hasScope && hasOutOfScope) || hasFourQuestions) {
|
|
235
|
+
bump(2, 2, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
|
|
236
|
+
} else if (hasScope || hasOutOfScope) {
|
|
237
|
+
bump(2, 1, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
|
|
238
|
+
} else {
|
|
239
|
+
bump(2, 0, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. v2.1 axiom governance fields
|
|
243
|
+
if (core?.axioms) {
|
|
244
|
+
const axioms = core.axioms;
|
|
245
|
+
const withApplies = axioms.filter(
|
|
246
|
+
(a) => Array.isArray(a.applies_when) && a.applies_when.length,
|
|
247
|
+
).length;
|
|
248
|
+
const withDoesNotApply = axioms.filter(
|
|
249
|
+
(a) => Array.isArray(a.does_not_apply_when) && a.does_not_apply_when.length,
|
|
250
|
+
).length;
|
|
251
|
+
const withFailureRisk = axioms.filter((a) => a.failure_risk).length;
|
|
252
|
+
const withConfidence = axioms.filter((a) => a.confidence).length;
|
|
253
|
+
const withEvidence = axioms.filter(
|
|
254
|
+
(a) => Array.isArray(a.evidence_type) && a.evidence_type.length,
|
|
255
|
+
).length;
|
|
256
|
+
|
|
257
|
+
bump(axioms.length, withApplies, `axioms with applies_when (${withApplies}/${axioms.length})`);
|
|
258
|
+
bump(
|
|
259
|
+
axioms.length,
|
|
260
|
+
withDoesNotApply,
|
|
261
|
+
`axioms with does_not_apply_when (${withDoesNotApply}/${axioms.length})`,
|
|
262
|
+
);
|
|
263
|
+
bump(
|
|
264
|
+
axioms.length,
|
|
265
|
+
withFailureRisk,
|
|
266
|
+
`axioms with failure_risk (${withFailureRisk}/${axioms.length})`,
|
|
267
|
+
);
|
|
268
|
+
bump(
|
|
269
|
+
axioms.length,
|
|
270
|
+
withConfidence,
|
|
271
|
+
`axioms with confidence (${withConfidence}/${axioms.length})`,
|
|
272
|
+
);
|
|
273
|
+
bump(
|
|
274
|
+
axioms.length,
|
|
275
|
+
withEvidence,
|
|
276
|
+
`axioms with evidence_type (${withEvidence}/${axioms.length})`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 3. v2.1 misunderstanding governance fields
|
|
281
|
+
if (pat?.misunderstandings) {
|
|
282
|
+
const ms = pat.misunderstandings;
|
|
283
|
+
const withApplies = ms.filter(
|
|
284
|
+
(m) => Array.isArray(m.applies_when) && m.applies_when.length,
|
|
285
|
+
).length;
|
|
286
|
+
const withFailureRisk = ms.filter((m) => m.failure_risk).length;
|
|
287
|
+
bump(
|
|
288
|
+
ms.length,
|
|
289
|
+
withApplies,
|
|
290
|
+
`misunderstandings with applies_when (${withApplies}/${ms.length})`,
|
|
291
|
+
);
|
|
292
|
+
bump(
|
|
293
|
+
ms.length,
|
|
294
|
+
withFailureRisk,
|
|
295
|
+
`misunderstandings with failure_risk (${withFailureRisk}/${ms.length})`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 4. self_check format: yes/no questions
|
|
300
|
+
if (pat?.self_check) {
|
|
301
|
+
const total = pat.self_check.length;
|
|
302
|
+
const yn = pat.self_check.filter((q) => typeof q === 'string' && q.trim().endsWith('?')).length;
|
|
303
|
+
bump(total, yn, `self_check questions ending in "?" (${yn}/${total})`);
|
|
304
|
+
if (total < 3)
|
|
305
|
+
issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 5. eval cases present
|
|
309
|
+
const evalDir = path.join(destDir, 'evals');
|
|
310
|
+
if (fs.existsSync(evalDir)) {
|
|
311
|
+
const files = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
|
|
312
|
+
if (files.length >= 4) bump(2, 2, `evals/ directory has ${files.length} case files`);
|
|
313
|
+
else if (files.length > 0)
|
|
314
|
+
bump(2, 1, `evals/ has ${files.length} files (recommend ≥4: core/boundary/failure/excluded)`);
|
|
315
|
+
else bump(2, 0, 'evals/ has case files');
|
|
316
|
+
} else {
|
|
317
|
+
bump(2, 0, 'evals/ directory present');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 6. judgment_version manifest field
|
|
321
|
+
if (manifest?.judgment_version) {
|
|
322
|
+
bump(1, 1, `judgment_version: ${manifest.judgment_version}`);
|
|
323
|
+
} else {
|
|
324
|
+
bump(1, 0, 'kdna.json has judgment_version');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { layer: 'judgment', issues, passed, score };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Render ────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
function renderLayer(result) {
|
|
333
|
+
const errors = result.issues.filter((i) => i.severity === 'error').length;
|
|
334
|
+
const warns = result.issues.filter((i) => i.severity === 'warn').length;
|
|
335
|
+
const passCount = result.passed.length;
|
|
336
|
+
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log('─'.repeat(64));
|
|
339
|
+
let header = ` ${result.layer.toUpperCase().padEnd(10)} passed:${passCount} warn:${warns} errors:${errors}`;
|
|
340
|
+
if (result.score) {
|
|
341
|
+
const pct =
|
|
342
|
+
result.score.max > 0 ? Math.round((result.score.total / result.score.max) * 100) : 0;
|
|
343
|
+
header += ` score:${result.score.total}/${result.score.max} (${pct}%)`;
|
|
344
|
+
}
|
|
345
|
+
console.log(header);
|
|
346
|
+
console.log('─'.repeat(64));
|
|
347
|
+
|
|
348
|
+
for (const p of result.passed) console.log(` ✓ ${p}`);
|
|
349
|
+
for (const i of result.issues) {
|
|
350
|
+
const mark = i.severity === 'error' ? '✗' : '⚠';
|
|
351
|
+
console.log(` ${mark} ${i.msg}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─── Main ──────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
function cmdVerify(input, args = []) {
|
|
358
|
+
const want = {
|
|
359
|
+
structure: args.includes('--structure'),
|
|
360
|
+
trust: args.includes('--trust'),
|
|
361
|
+
judgment: args.includes('--judgment'),
|
|
362
|
+
};
|
|
363
|
+
const all = !want.structure && !want.trust && !want.judgment;
|
|
364
|
+
if (all) want.structure = want.trust = want.judgment = true;
|
|
365
|
+
|
|
366
|
+
// Resolve name → installed path + scope/entry
|
|
367
|
+
const parsed = parseName(input);
|
|
368
|
+
if (!parsed) {
|
|
369
|
+
console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
|
|
370
|
+
process.exit(2);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
|
|
374
|
+
if (!fs.existsSync(destDir)) {
|
|
375
|
+
console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
|
|
376
|
+
process.exit(2);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let scope = null,
|
|
380
|
+
entry = null;
|
|
381
|
+
if (want.trust) {
|
|
382
|
+
try {
|
|
383
|
+
const resolver = new RegistryResolver({ allowNetwork: false });
|
|
384
|
+
const r = resolver.resolve(parsed.full);
|
|
385
|
+
scope = r.scope;
|
|
386
|
+
entry = r.entry;
|
|
387
|
+
} catch (e) {
|
|
388
|
+
console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log('═'.repeat(64));
|
|
393
|
+
console.log(` Verify ${parsed.full}`);
|
|
394
|
+
console.log(` Path: ${destDir}`);
|
|
395
|
+
console.log('═'.repeat(64));
|
|
396
|
+
|
|
397
|
+
const results = [];
|
|
398
|
+
if (want.structure) results.push(checkStructure(destDir));
|
|
399
|
+
if (want.trust) results.push(checkTrust(destDir, scope, entry));
|
|
400
|
+
if (want.judgment) results.push(checkJudgment(destDir));
|
|
401
|
+
|
|
402
|
+
for (const r of results) renderLayer(r);
|
|
403
|
+
|
|
404
|
+
const totalErrors = results.reduce(
|
|
405
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === 'error').length,
|
|
406
|
+
0,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
console.log('');
|
|
410
|
+
console.log('═'.repeat(64));
|
|
411
|
+
if (totalErrors === 0) {
|
|
412
|
+
console.log(
|
|
413
|
+
` ✓ All ${results.length} layer(s) passed (warnings are quality signals, not failures)`,
|
|
414
|
+
);
|
|
415
|
+
} else {
|
|
416
|
+
console.log(` ✗ ${totalErrors} hard failure(s)`);
|
|
417
|
+
}
|
|
418
|
+
console.log('═'.repeat(64));
|
|
419
|
+
|
|
420
|
+
process.exit(totalErrors === 0 ? 0 : 1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
module.exports = { cmdVerify, checkStructure, checkTrust, checkJudgment };
|
package/src/version.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kdna version bump <patch|minor|major> [path] — Bump domain version.
|
|
3
|
+
*
|
|
4
|
+
* Updates kdna.json and all KDNA JSON file meta versions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
function error(msg) {
|
|
11
|
+
console.error(`Error: ${msg}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readJson(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cmdVersionBump(level, domainPath) {
|
|
24
|
+
if (!level || !['patch', 'minor', 'major'].includes(level)) {
|
|
25
|
+
error(
|
|
26
|
+
'Usage: kdna version bump <patch|minor|major> [path]\n\n patch — fix wording, no judgment change\n minor — add axiom/concept/framework (may change judgment)\n major — remove/redefine axiom or change scope (breaking)',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const targetDir = path.resolve(domainPath || '.');
|
|
31
|
+
|
|
32
|
+
// Read kdna.json
|
|
33
|
+
const manifestPath = path.join(targetDir, 'kdna.json');
|
|
34
|
+
const manifest = readJson(manifestPath);
|
|
35
|
+
if (!manifest) error(`kdna.json not found in ${targetDir}`);
|
|
36
|
+
|
|
37
|
+
const oldVersion = manifest.version;
|
|
38
|
+
if (!oldVersion) error('No version field in kdna.json');
|
|
39
|
+
|
|
40
|
+
// Parse semver
|
|
41
|
+
const parts = oldVersion.split('.').map(Number);
|
|
42
|
+
if (parts.length !== 3 || parts.some(isNaN)) {
|
|
43
|
+
error(`Invalid semver: "${oldVersion}". Expected MAJOR.MINOR.PATCH`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let [major, minor, patch] = parts;
|
|
47
|
+
|
|
48
|
+
switch (level) {
|
|
49
|
+
case 'patch':
|
|
50
|
+
patch++;
|
|
51
|
+
break;
|
|
52
|
+
case 'minor':
|
|
53
|
+
minor++;
|
|
54
|
+
patch = 0;
|
|
55
|
+
break;
|
|
56
|
+
case 'major':
|
|
57
|
+
major++;
|
|
58
|
+
minor = 0;
|
|
59
|
+
patch = 0;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const newVersion = `${major}.${minor}.${patch}`;
|
|
64
|
+
|
|
65
|
+
console.log(`Bumping version: ${oldVersion} → ${newVersion} (${level})`);
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
// Update kdna.json
|
|
69
|
+
manifest.version = newVersion;
|
|
70
|
+
manifest.updated = new Date().toISOString().slice(0, 10);
|
|
71
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
72
|
+
console.log(` ✓ kdna.json`);
|
|
73
|
+
|
|
74
|
+
// Update all KDNA JSON files
|
|
75
|
+
const kdnaFiles = fs
|
|
76
|
+
.readdirSync(targetDir)
|
|
77
|
+
.filter((f) => f.startsWith('KDNA_') && f.endsWith('.json'));
|
|
78
|
+
|
|
79
|
+
for (const file of kdnaFiles) {
|
|
80
|
+
const filePath = path.join(targetDir, file);
|
|
81
|
+
const data = readJson(filePath);
|
|
82
|
+
if (data && data.meta) {
|
|
83
|
+
// meta.version is spec version, not package version — keep it
|
|
84
|
+
// But update any updated/created dates
|
|
85
|
+
if (data.meta.updated) {
|
|
86
|
+
data.meta.updated = new Date().toISOString().slice(0, 10);
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
|
|
89
|
+
console.log(` ✓ ${file} (meta unchanged — spec version)`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// CHANGELOG reminder
|
|
94
|
+
const changelogPath = path.join(targetDir, 'CHANGELOG.md');
|
|
95
|
+
console.log('');
|
|
96
|
+
if (fs.existsSync(changelogPath)) {
|
|
97
|
+
console.log(` ⚠ Remember to add ${newVersion} entry to CHANGELOG.md`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(` ⚠ Consider creating CHANGELOG.md`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Benchmark reminder for minor/major
|
|
103
|
+
if (level === 'minor' || level === 'major') {
|
|
104
|
+
console.log(` ⚠ MINOR/MAJOR bump — must re-run benchmark before release`);
|
|
105
|
+
console.log(` kdna verify ${domainPath || '.'}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(`Done. Version: ${oldVersion} → ${newVersion}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { cmdVersionBump };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example_cluster",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"purpose": "Describe what judgment this cluster provides in one paragraph.",
|
|
5
|
+
"packages": [
|
|
6
|
+
{
|
|
7
|
+
"id": "sub_domain_one",
|
|
8
|
+
"role": "primary",
|
|
9
|
+
"use_when": ["Task matches this sub-domain's scope"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "sub_domain_two",
|
|
13
|
+
"role": "primary",
|
|
14
|
+
"use_when": ["Task matches this sub-domain's scope"]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"composition_rules": [
|
|
18
|
+
"Rule 1: when these domains conflict, this principle takes priority.",
|
|
19
|
+
"Rule 2: domain-specific guidance overrides generic advice."
|
|
20
|
+
],
|
|
21
|
+
"routing_questions": [
|
|
22
|
+
"Question 1 that determines which sub-domain to load.",
|
|
23
|
+
"Question 2 that determines which sub-domain to load."
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Cluster: example_cluster
|
|
2
|
+
|
|
3
|
+
A KDNA cluster packs multiple related domains into a single loadable unit.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
Each sub-domain is a folder inside the cluster root. Every sub-domain follows the standard 6-file KDNA structure:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
example_cluster/
|
|
11
|
+
KDNA_Cluster.json # Cluster manifest
|
|
12
|
+
sub_domain_one/
|
|
13
|
+
KDNA_Core.json
|
|
14
|
+
KDNA_Patterns.json
|
|
15
|
+
KDNA_Scenarios.json
|
|
16
|
+
KDNA_Cases.json
|
|
17
|
+
KDNA_Reasoning.json
|
|
18
|
+
KDNA_Evolution.json
|
|
19
|
+
README.md
|
|
20
|
+
sub_domain_two/
|
|
21
|
+
... (same structure)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Creating a cluster
|
|
25
|
+
|
|
26
|
+
Use: `kdna cluster init <name>`
|
|
27
|
+
|
|
28
|
+
This copies `templates/cluster/` and `templates/minimal-domain/` into a new directory with one example sub-domain, ready to customize.
|
|
29
|
+
|
|
30
|
+
## Adding sub-domains
|
|
31
|
+
|
|
32
|
+
Run `kdna init <cluster>/<sub_domain>` inside the cluster root (or copy `templates/minimal-domain/`).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"version": "0.4",
|
|
4
|
+
"domain": "example_domain",
|
|
5
|
+
"created": "YYYY-MM-DD",
|
|
6
|
+
"purpose": "Define the core cognition of this domain.",
|
|
7
|
+
"load_condition": "Always load when this domain is selected."
|
|
8
|
+
},
|
|
9
|
+
"axioms": [
|
|
10
|
+
{
|
|
11
|
+
"id": "ed_ax_foundation",
|
|
12
|
+
"one_sentence": "One core judgment principle that changes how the agent evaluates situations.",
|
|
13
|
+
"full_statement": "A testable, domain-specific principle. Must be specific enough that two experts would agree on when it applies. Avoid slogans.",
|
|
14
|
+
"why": "Explain what the agent would get wrong WITHOUT this axiom, and what it gets right WITH it.",
|
|
15
|
+
"applies_when": ["When the task involves evaluating [situation type]"],
|
|
16
|
+
"does_not_apply_when": ["When the task is purely mechanical or factual lookup"],
|
|
17
|
+
"failure_risk": "Without this axiom, the agent would treat [situation] as [wrong assumption], leading to [bad outcome].",
|
|
18
|
+
"confidence": "medium",
|
|
19
|
+
"evidence_type": "practice_patterns"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"ontology": [
|
|
23
|
+
{
|
|
24
|
+
"id": "ed_con_core_distinction",
|
|
25
|
+
"one_sentence": "Name one central concept the agent must distinguish.",
|
|
26
|
+
"essence": "What this concept really means in this domain. Not a dictionary definition — the operational meaning.",
|
|
27
|
+
"boundary": "What this concept is NOT. A real distinction the agent must not blur.",
|
|
28
|
+
"trigger_signal": "Words, phrases, or situations that signal this concept is relevant."
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"frameworks": [
|
|
32
|
+
{
|
|
33
|
+
"id": "ed_fw_assessment",
|
|
34
|
+
"name": "Assessment Framework",
|
|
35
|
+
"when_to_use": "When the agent needs to evaluate a situation in this domain.",
|
|
36
|
+
"steps": [
|
|
37
|
+
"Step 1: Identify the core signal",
|
|
38
|
+
"Step 2: Check against domain principles",
|
|
39
|
+
"Step 3: Form a domain-informed judgment"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"core_structure": [
|
|
44
|
+
{
|
|
45
|
+
"from": "Surface symptom or common mistake the agent makes without domain cognition",
|
|
46
|
+
"to": "Deeper cause or correct judgment the agent should reach",
|
|
47
|
+
"via": "The cognitive shift or diagnostic path that bridges the gap"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"stances": [
|
|
51
|
+
"The domain's default position on an issue. A concise assertion the agent should bias toward.",
|
|
52
|
+
"What the domain rejects. The position the agent should argue against by default."
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"version": "0.4",
|
|
4
|
+
"domain": "example_domain",
|
|
5
|
+
"created": "YYYY-MM-DD",
|
|
6
|
+
"purpose": "Define terminology, common misunderstandings, and self-checks.",
|
|
7
|
+
"load_condition": "Always load when this domain is selected."
|
|
8
|
+
},
|
|
9
|
+
"terminology": {
|
|
10
|
+
"standard_terms": [
|
|
11
|
+
{
|
|
12
|
+
"term": "preferred term",
|
|
13
|
+
"definition": "What this term means in this domain. Be operational, not dictionary-like."
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"banned_terms": [
|
|
17
|
+
{
|
|
18
|
+
"term": "term to avoid",
|
|
19
|
+
"why": "Why this term misleads agent judgment. What wrong assumption it encodes.",
|
|
20
|
+
"replace_with": "A concrete, domain-specific replacement."
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"misunderstandings": [
|
|
25
|
+
{
|
|
26
|
+
"id": "ms-common-confusion",
|
|
27
|
+
"wrong": "Common wrong interpretation that an agent without domain cognition would make.",
|
|
28
|
+
"correct": "The correct interpretation according to this domain's principles.",
|
|
29
|
+
"key_distinction": "The specific conceptual boundary the agent must preserve.",
|
|
30
|
+
"why": "What bad judgment results when the agent holds the wrong interpretation."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"self_check": [
|
|
34
|
+
"Did the answer apply the domain's core distinction instead of giving generic advice?",
|
|
35
|
+
"Would a domain expert agree with this judgment, or did the agent default to common sense?"
|
|
36
|
+
]
|
|
37
|
+
}
|