@aikdna/kdna-cli 0.16.10 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- package/src/cmds/encrypt.js +0 -199
package/src/verify.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* kdna verify <name> — Quality signal across three layers.
|
|
2
|
+
* kdna verify <name|file.kdna> — Quality signal across three layers.
|
|
3
3
|
*
|
|
4
4
|
* --structure files exist, schema OK
|
|
5
|
-
* --trust
|
|
5
|
+
* --trust asset digest + Ed25519 signature against scope trust key
|
|
6
6
|
* --judgment v2.1 governance fields (boundary, applies_when, eval cases)
|
|
7
7
|
*
|
|
8
8
|
* No flag = run all three.
|
|
@@ -17,12 +17,25 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
-
const
|
|
21
|
-
const {
|
|
22
|
-
const {
|
|
20
|
+
const { RegistryResolver, parseName, registryTrustIssues, isEntryRevoked } = require('./registry');
|
|
21
|
+
const { EXIT, isYesNoSelfCheck } = require('./cmds/_common');
|
|
22
|
+
const { licenseDecryptOptionsForManifest } = require('./cmds/license');
|
|
23
|
+
|
|
24
|
+
let validateManifestFn;
|
|
25
|
+
try {
|
|
26
|
+
validateManifestFn = require('@aikdna/kdna-core').validateManifest;
|
|
27
|
+
} catch {
|
|
28
|
+
// kdna-core not available — manifest validation skipped
|
|
29
|
+
}
|
|
23
30
|
|
|
24
|
-
const
|
|
25
|
-
|
|
31
|
+
const {
|
|
32
|
+
getInstalled,
|
|
33
|
+
listContainerEntries,
|
|
34
|
+
readContainerEntry,
|
|
35
|
+
readContainerJson,
|
|
36
|
+
resolveAsset,
|
|
37
|
+
verifyAsset,
|
|
38
|
+
} = require('./package-store');
|
|
26
39
|
|
|
27
40
|
function readJson(p) {
|
|
28
41
|
try {
|
|
@@ -32,11 +45,84 @@ function readJson(p) {
|
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
function directoryView(root) {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'directory',
|
|
51
|
+
exists(name) {
|
|
52
|
+
return fs.existsSync(path.join(root, name));
|
|
53
|
+
},
|
|
54
|
+
readJson(name) {
|
|
55
|
+
return readJson(path.join(root, name));
|
|
56
|
+
},
|
|
57
|
+
readText(name) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.readFileSync(path.join(root, name), 'utf8');
|
|
60
|
+
} catch {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
listDirFiles(dirName) {
|
|
65
|
+
const dir = path.join(root, dirName);
|
|
66
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
67
|
+
return fs.readdirSync(dir);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assetView(kdnaPath, options = {}) {
|
|
73
|
+
const entries = new Set(listContainerEntries(kdnaPath));
|
|
74
|
+
return {
|
|
75
|
+
kind: 'asset',
|
|
76
|
+
path: kdnaPath,
|
|
77
|
+
exists(name) {
|
|
78
|
+
return entries.has(name);
|
|
79
|
+
},
|
|
80
|
+
readJson(name) {
|
|
81
|
+
return readContainerJson(kdnaPath, name, options);
|
|
82
|
+
},
|
|
83
|
+
readText(name) {
|
|
84
|
+
if (!entries.has(name)) return '';
|
|
85
|
+
return readContainerEntry(kdnaPath, name).toString('utf8');
|
|
86
|
+
},
|
|
87
|
+
listDirFiles(dirName) {
|
|
88
|
+
const prefix = `${dirName.replace(/\/+$/, '')}/`;
|
|
89
|
+
const files = [];
|
|
90
|
+
for (const entryName of entries) {
|
|
91
|
+
if (!entryName.startsWith(prefix)) continue;
|
|
92
|
+
const rest = entryName.slice(prefix.length);
|
|
93
|
+
if (rest && !rest.includes('/')) files.push(rest);
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function asView(input, options = {}) {
|
|
101
|
+
if (input && typeof input.exists === 'function') return input;
|
|
102
|
+
return directoryView(input, options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readJsonFromView(view, entryName, issues = null) {
|
|
106
|
+
try {
|
|
107
|
+
return view.readJson(entryName);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (issues) issues.push({ severity: 'error', msg: `${entryName}: ${e.message}` });
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
35
114
|
// ─── Structure layer ───────────────────────────────────────────────────
|
|
36
115
|
|
|
37
|
-
function checkStructure(
|
|
116
|
+
function checkStructure(input, options = {}) {
|
|
117
|
+
const view = asView(input);
|
|
38
118
|
const issues = [];
|
|
39
119
|
const passed = [];
|
|
120
|
+
if (options.licenseError) {
|
|
121
|
+
issues.push({
|
|
122
|
+
severity: 'error',
|
|
123
|
+
msg: `license required to verify encrypted entries: ${options.licenseError}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
40
126
|
|
|
41
127
|
const required = ['KDNA_Core.json', 'KDNA_Patterns.json', 'kdna.json'];
|
|
42
128
|
const optional = [
|
|
@@ -47,20 +133,31 @@ function checkStructure(destDir) {
|
|
|
47
133
|
];
|
|
48
134
|
|
|
49
135
|
for (const f of required) {
|
|
50
|
-
if (!
|
|
136
|
+
if (!view.exists(f)) {
|
|
51
137
|
issues.push({ severity: 'error', msg: `required file missing: ${f}` });
|
|
52
138
|
} else {
|
|
53
139
|
passed.push(`has ${f}`);
|
|
54
140
|
}
|
|
55
141
|
}
|
|
56
142
|
|
|
143
|
+
// Validate kdna.json against canonical manifest schema
|
|
144
|
+
if (validateManifestFn) {
|
|
145
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
146
|
+
if (manifest) {
|
|
147
|
+
const mResult = validateManifestFn(manifest);
|
|
148
|
+
for (const e of mResult.errors) issues.push({ severity: 'error', msg: e });
|
|
149
|
+
for (const w of mResult.warnings) issues.push({ severity: 'warn', msg: w });
|
|
150
|
+
if (mResult.errors.length === 0) passed.push('kdna.json conforms to manifest schema v1.0-rc');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
57
154
|
for (const f of optional) {
|
|
58
|
-
if (
|
|
155
|
+
if (view.exists(f)) passed.push(`has ${f}`);
|
|
59
156
|
}
|
|
60
157
|
|
|
61
158
|
// Schema check using kdna-core if available
|
|
62
159
|
try {
|
|
63
|
-
const core =
|
|
160
|
+
const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
64
161
|
if (core) {
|
|
65
162
|
if (!core.axioms || !Array.isArray(core.axioms) || core.axioms.length === 0) {
|
|
66
163
|
issues.push({ severity: 'error', msg: 'KDNA_Core.axioms missing or empty' });
|
|
@@ -72,7 +169,7 @@ function checkStructure(destDir) {
|
|
|
72
169
|
issues.push({ severity: 'warn', msg: 'KDNA_Core.stances missing or empty' });
|
|
73
170
|
}
|
|
74
171
|
}
|
|
75
|
-
const pat =
|
|
172
|
+
const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
|
|
76
173
|
if (pat) {
|
|
77
174
|
if (!pat.misunderstandings || pat.misunderstandings.length === 0) {
|
|
78
175
|
issues.push({ severity: 'warn', msg: 'KDNA_Patterns.misunderstandings missing or empty' });
|
|
@@ -93,11 +190,12 @@ function checkStructure(destDir) {
|
|
|
93
190
|
|
|
94
191
|
// ─── Trust layer ───────────────────────────────────────────────────────
|
|
95
192
|
|
|
96
|
-
function checkTrust(
|
|
193
|
+
function checkTrust(input, scope, entry, options = {}) {
|
|
194
|
+
const view = asView(input);
|
|
97
195
|
const issues = [];
|
|
98
196
|
const passed = [];
|
|
99
197
|
|
|
100
|
-
const manifest =
|
|
198
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
101
199
|
if (!manifest) {
|
|
102
200
|
issues.push({ severity: 'error', msg: 'kdna.json missing — cannot verify trust' });
|
|
103
201
|
return { layer: 'trust', issues, passed };
|
|
@@ -133,58 +231,56 @@ function checkTrust(destDir, scope, entry) {
|
|
|
133
231
|
});
|
|
134
232
|
} else {
|
|
135
233
|
passed.push('embedded public_key_pem present');
|
|
234
|
+
}
|
|
136
235
|
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
236
|
+
if (options.assetPath) {
|
|
237
|
+
const verification = verifyAsset(options.assetPath, { requireSignature: true });
|
|
238
|
+
for (const warning of verification.warnings || []) {
|
|
239
|
+
if (!/signature missing/.test(warning)) issues.push({ severity: 'warn', msg: warning });
|
|
240
|
+
}
|
|
241
|
+
for (const err of verification.errors || []) {
|
|
242
|
+
if (/signature|author\.|public_key|pubkey|fingerprint|Ed25519/i.test(err)) {
|
|
243
|
+
issues.push({ severity: 'error', msg: err });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (verification.signature_valid === true) {
|
|
247
|
+
passed.push('Ed25519 signature VALID over canonical payload');
|
|
248
|
+
} else if (manifest.signature) {
|
|
141
249
|
issues.push({
|
|
142
250
|
severity: 'error',
|
|
143
|
-
msg: '
|
|
251
|
+
msg: 'Ed25519 signature INVALID or unavailable',
|
|
144
252
|
});
|
|
145
|
-
} else {
|
|
146
|
-
passed.push('PEM fingerprint matches author.pubkey');
|
|
147
|
-
|
|
148
|
-
// Full Ed25519 verify
|
|
149
|
-
try {
|
|
150
|
-
const files = fs
|
|
151
|
-
.readdirSync(destDir)
|
|
152
|
-
.filter((f) => f.endsWith('.json'))
|
|
153
|
-
.sort();
|
|
154
|
-
const parts = [];
|
|
155
|
-
for (const f of files) {
|
|
156
|
-
const full = path.join(destDir, f);
|
|
157
|
-
let buf;
|
|
158
|
-
if (f === 'kdna.json') {
|
|
159
|
-
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
160
|
-
delete obj.signature;
|
|
161
|
-
delete obj._source;
|
|
162
|
-
buf = Buffer.from(JSON.stringify(obj));
|
|
163
|
-
} else {
|
|
164
|
-
buf = fs.readFileSync(full);
|
|
165
|
-
}
|
|
166
|
-
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
167
|
-
parts.push(`${f}:${hash}`);
|
|
168
|
-
}
|
|
169
|
-
const payload = parts.join('\n');
|
|
170
|
-
const sigHex = manifest.signature.replace(/^ed25519:/, '');
|
|
171
|
-
const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
|
|
172
|
-
const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
|
|
173
|
-
if (ok) passed.push('Ed25519 signature VALID over canonical payload');
|
|
174
|
-
else
|
|
175
|
-
issues.push({
|
|
176
|
-
severity: 'error',
|
|
177
|
-
msg: 'Ed25519 signature INVALID — package may be tampered',
|
|
178
|
-
});
|
|
179
|
-
} catch (e) {
|
|
180
|
-
issues.push({ severity: 'error', msg: `signature verify failed: ${e.message}` });
|
|
181
|
-
}
|
|
182
253
|
}
|
|
183
254
|
}
|
|
184
255
|
|
|
185
|
-
// 4.
|
|
186
|
-
|
|
187
|
-
|
|
256
|
+
// 4. asset digest vs registry (if entry provided)
|
|
257
|
+
const registry = options.registry || null;
|
|
258
|
+
const registryIssues = registry ? registryTrustIssues(registry) : [];
|
|
259
|
+
for (const issue of registryIssues) {
|
|
260
|
+
issues.push({ severity: 'error', msg: issue });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const revocation = registry && entry ? isEntryRevoked(registry, entry) : null;
|
|
264
|
+
if (revocation) {
|
|
265
|
+
issues.push({
|
|
266
|
+
severity: 'error',
|
|
267
|
+
msg: `registry revokes this asset${revocation.reason ? `: ${revocation.reason}` : ''}`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const registryDigest = entry?.asset_digest || null;
|
|
272
|
+
if (registryDigest) {
|
|
273
|
+
passed.push(`registry asset_digest declared: ${registryDigest.slice(0, 23)}…`);
|
|
274
|
+
if (options.assetDigest && options.assetDigest !== registryDigest) {
|
|
275
|
+
issues.push({
|
|
276
|
+
severity: 'error',
|
|
277
|
+
msg: `asset digest mismatch: registry ${registryDigest}, local ${options.assetDigest}`,
|
|
278
|
+
});
|
|
279
|
+
} else if (options.assetDigest) {
|
|
280
|
+
passed.push('local asset_digest matches registry');
|
|
281
|
+
}
|
|
282
|
+
} else if (entry) {
|
|
283
|
+
issues.push({ severity: 'error', msg: 'registry asset_digest missing' });
|
|
188
284
|
}
|
|
189
285
|
|
|
190
286
|
// 5. scope governance
|
|
@@ -200,7 +296,8 @@ function checkTrust(destDir, scope, entry) {
|
|
|
200
296
|
|
|
201
297
|
// ─── Judgment layer ────────────────────────────────────────────────────
|
|
202
298
|
|
|
203
|
-
function checkJudgment(
|
|
299
|
+
function checkJudgment(input, options = {}) {
|
|
300
|
+
const view = asView(input);
|
|
204
301
|
const issues = [];
|
|
205
302
|
const passed = [];
|
|
206
303
|
const score = { total: 0, max: 0 };
|
|
@@ -213,17 +310,23 @@ function checkJudgment(destDir) {
|
|
|
213
310
|
else issues.push({ severity: 'warn', msg: `missing: ${label}` });
|
|
214
311
|
}
|
|
215
312
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
313
|
+
if (options.licenseError) {
|
|
314
|
+
issues.push({
|
|
315
|
+
severity: 'error',
|
|
316
|
+
msg: `license required to verify encrypted judgment: ${options.licenseError}`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
321
|
+
const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
|
|
322
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
219
323
|
|
|
220
324
|
// 1. Boundary declaration in README (REQUIRED)
|
|
221
325
|
// Either classic "## Scope" + "## Out of Scope" pair,
|
|
222
326
|
// OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
|
|
223
|
-
const readmePath = path.join(destDir, 'README.md');
|
|
224
327
|
let readme = '';
|
|
225
328
|
try {
|
|
226
|
-
readme =
|
|
329
|
+
readme = view.readText('README.md');
|
|
227
330
|
} catch {
|
|
228
331
|
/* ok */
|
|
229
332
|
}
|
|
@@ -312,16 +415,16 @@ function checkJudgment(destDir) {
|
|
|
312
415
|
// 4. self_check format: yes/no questions
|
|
313
416
|
if (pat?.self_check) {
|
|
314
417
|
const total = pat.self_check.length;
|
|
315
|
-
const yn = pat.self_check.filter((q) =>
|
|
316
|
-
bump(total, yn, `self_check questions
|
|
418
|
+
const yn = pat.self_check.filter((q) => isYesNoSelfCheck(q)).length;
|
|
419
|
+
bump(total, yn, `self_check yes/no questions (${yn}/${total})`);
|
|
317
420
|
if (total < 3)
|
|
318
421
|
issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
|
|
319
422
|
}
|
|
320
423
|
|
|
321
424
|
// 5. eval cases present (REQUIRED: ≥4 cases)
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
const files =
|
|
425
|
+
const evalFiles = view.listDirFiles('evals').filter((f) => f.endsWith('.json'));
|
|
426
|
+
if (evalFiles.length) {
|
|
427
|
+
const files = evalFiles;
|
|
325
428
|
if (files.length >= 4) {
|
|
326
429
|
bump(2, 2, `evals/ directory has ${files.length} case files`);
|
|
327
430
|
} else if (files.length > 0) {
|
|
@@ -381,10 +484,11 @@ function renderLayer(result) {
|
|
|
381
484
|
|
|
382
485
|
// ─── I18N layer ──────────────────────────────────────────────────────
|
|
383
486
|
|
|
384
|
-
function checkI18n(
|
|
487
|
+
function checkI18n(input) {
|
|
488
|
+
const view = asView(input);
|
|
385
489
|
const issues = [];
|
|
386
490
|
const passed = [];
|
|
387
|
-
const manifest =
|
|
491
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues) || {};
|
|
388
492
|
const languages = manifest.languages || [];
|
|
389
493
|
const i18nLevel = manifest.i18n_level || 'L0';
|
|
390
494
|
|
|
@@ -399,14 +503,13 @@ function checkI18n(destDir) {
|
|
|
399
503
|
const canonical = manifest.default_language || languages[0] || 'en';
|
|
400
504
|
for (const lang of languages) {
|
|
401
505
|
if (lang === canonical) continue;
|
|
402
|
-
const localeDir = path.join(destDir, 'locales', lang);
|
|
403
506
|
|
|
404
507
|
// L1: card + readme
|
|
405
508
|
if (['L1', 'L2', 'L3', 'L4'].includes(i18nLevel)) {
|
|
406
|
-
if (!
|
|
509
|
+
if (!view.exists(`locales/${lang}/KDNA_CARD.json`)) {
|
|
407
510
|
issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_CARD.json missing` });
|
|
408
511
|
} else {
|
|
409
|
-
const card =
|
|
512
|
+
const card = readJsonFromView(view, `locales/${lang}/KDNA_CARD.json`, issues);
|
|
410
513
|
if (card) {
|
|
411
514
|
passed.push(`locales/${lang}/KDNA_CARD.json OK`);
|
|
412
515
|
if (!card.display_name)
|
|
@@ -415,7 +518,7 @@ function checkI18n(destDir) {
|
|
|
415
518
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} card missing intended_use` });
|
|
416
519
|
}
|
|
417
520
|
}
|
|
418
|
-
if (!
|
|
521
|
+
if (!view.exists(`locales/${lang}/README.md`)) {
|
|
419
522
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} README.md missing` });
|
|
420
523
|
} else {
|
|
421
524
|
passed.push(`locales/${lang}/README.md OK`);
|
|
@@ -424,13 +527,13 @@ function checkI18n(destDir) {
|
|
|
424
527
|
|
|
425
528
|
// L2: overlay files
|
|
426
529
|
if (['L2', 'L3', 'L4'].includes(i18nLevel)) {
|
|
427
|
-
const coreOverlay =
|
|
428
|
-
if (!
|
|
530
|
+
const coreOverlay = `locales/${lang}/KDNA_Core.overlay.json`;
|
|
531
|
+
if (!view.exists(coreOverlay)) {
|
|
429
532
|
issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_Core.overlay.json missing` });
|
|
430
533
|
} else {
|
|
431
|
-
const overlay =
|
|
534
|
+
const overlay = readJsonFromView(view, coreOverlay, issues);
|
|
432
535
|
if (overlay?.translations) {
|
|
433
|
-
const core =
|
|
536
|
+
const core = readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
434
537
|
if (core?.axioms) {
|
|
435
538
|
const validIds = new Set(core.axioms.map((a) => a.id));
|
|
436
539
|
for (const key of Object.keys(overlay.translations)) {
|
|
@@ -448,7 +551,7 @@ function checkI18n(destDir) {
|
|
|
448
551
|
);
|
|
449
552
|
}
|
|
450
553
|
}
|
|
451
|
-
if (!
|
|
554
|
+
if (!view.exists(`locales/${lang}/KDNA_Patterns.overlay.json`)) {
|
|
452
555
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} KDNA_Patterns.overlay.json missing` });
|
|
453
556
|
}
|
|
454
557
|
}
|
|
@@ -469,12 +572,13 @@ function checkI18n(destDir) {
|
|
|
469
572
|
|
|
470
573
|
// ─── Governance layer ───────────────────────────────────────────────
|
|
471
574
|
|
|
472
|
-
function checkGovernance(
|
|
575
|
+
function checkGovernance(input) {
|
|
576
|
+
const view = asView(input);
|
|
473
577
|
const issues = [];
|
|
474
578
|
const passed = [];
|
|
475
|
-
const card =
|
|
579
|
+
const card = readJsonFromView(view, 'KDNA_CARD.json', issues) || {};
|
|
476
580
|
|
|
477
|
-
if (!
|
|
581
|
+
if (!card || !view.exists('KDNA_CARD.json')) {
|
|
478
582
|
issues.push({ severity: 'error', msg: 'governance: KDNA_CARD.json missing — required' });
|
|
479
583
|
return { layer: 'governance', passed: false, issues, results: issues.map((i) => i.msg) };
|
|
480
584
|
}
|
|
@@ -552,6 +656,33 @@ function checkGovernance(destDir) {
|
|
|
552
656
|
|
|
553
657
|
function cmdVerify(input, args = []) {
|
|
554
658
|
const jsonMode = args.includes('--json');
|
|
659
|
+
const trustReport = args.includes('--trust-report');
|
|
660
|
+
|
|
661
|
+
// --trust-report: standalone mode — output full trust report and exit
|
|
662
|
+
if (trustReport) {
|
|
663
|
+
const parsed = parseName(input);
|
|
664
|
+
if (!parsed) {
|
|
665
|
+
console.log(JSON.stringify({ ok: false, error: `Invalid name: ${input}` }));
|
|
666
|
+
process.exit(EXIT.INPUT_ERROR);
|
|
667
|
+
}
|
|
668
|
+
const installed = getInstalled(parsed.full);
|
|
669
|
+
if (!installed) {
|
|
670
|
+
console.log(JSON.stringify({ ok: false, error: `Domain not installed: ${input}` }));
|
|
671
|
+
process.exit(EXIT.INPUT_ERROR);
|
|
672
|
+
}
|
|
673
|
+
const { checkTrust: agentCheckTrust } = require('./agent');
|
|
674
|
+
const trust = agentCheckTrust(parsed.full);
|
|
675
|
+
console.log(JSON.stringify({
|
|
676
|
+
domain: parsed.full,
|
|
677
|
+
passed: trust.passed,
|
|
678
|
+
failures: trust.failures,
|
|
679
|
+
warnings: trust.warnings,
|
|
680
|
+
risk_level: trust.riskLevel,
|
|
681
|
+
spec_version: trust.specVersion,
|
|
682
|
+
signature_valid: trust.signatureValid,
|
|
683
|
+
}, null, 2));
|
|
684
|
+
process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
|
|
685
|
+
}
|
|
555
686
|
|
|
556
687
|
const want = {
|
|
557
688
|
structure: args.includes('--structure'),
|
|
@@ -563,58 +694,80 @@ function cmdVerify(input, args = []) {
|
|
|
563
694
|
const all = !want.structure && !want.trust && !want.judgment && !want.i18n && !want.governance;
|
|
564
695
|
if (all) want.structure = want.trust = want.judgment = true;
|
|
565
696
|
|
|
566
|
-
// Resolve name
|
|
567
|
-
const
|
|
568
|
-
|
|
697
|
+
// Resolve installed name or direct local .kdna asset path.
|
|
698
|
+
const asset = resolveAsset(input);
|
|
699
|
+
const parsed = asset?.parsed || parseName(asset?.name || '');
|
|
700
|
+
const displayName = parsed?.full || asset?.name || input;
|
|
701
|
+
if (!asset) {
|
|
569
702
|
if (jsonMode) {
|
|
570
703
|
console.log(
|
|
571
704
|
JSON.stringify({
|
|
572
705
|
name: input,
|
|
573
706
|
ok: false,
|
|
574
|
-
error: `
|
|
707
|
+
error: `KDNA asset not found: ${input}. Use an installed name or a .kdna file.`,
|
|
575
708
|
}),
|
|
576
709
|
);
|
|
577
710
|
} else {
|
|
578
|
-
console.error(`
|
|
579
|
-
}
|
|
580
|
-
process.exit(EXIT.INPUT_ERROR);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
|
|
584
|
-
if (!fs.existsSync(destDir)) {
|
|
585
|
-
if (jsonMode) {
|
|
586
|
-
console.log(
|
|
587
|
-
JSON.stringify({
|
|
588
|
-
name: parsed.full,
|
|
589
|
-
ok: false,
|
|
590
|
-
error: `${parsed.full} is not installed. Run: kdna install ${input}`,
|
|
591
|
-
}),
|
|
592
|
-
);
|
|
593
|
-
} else {
|
|
594
|
-
console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
|
|
711
|
+
console.error(`KDNA asset not found: ${input}. Use an installed name or a .kdna file.`);
|
|
595
712
|
}
|
|
596
713
|
process.exit(EXIT.INPUT_ERROR);
|
|
597
714
|
}
|
|
598
715
|
|
|
599
716
|
let scope = null,
|
|
600
|
-
entry = null
|
|
717
|
+
entry = null,
|
|
718
|
+
registry = null;
|
|
601
719
|
if (want.trust) {
|
|
602
720
|
try {
|
|
603
721
|
const resolver = new RegistryResolver({ allowNetwork: false });
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
722
|
+
if (parsed) {
|
|
723
|
+
const r = resolver.resolve(parsed.full);
|
|
724
|
+
scope = r.scope;
|
|
725
|
+
entry = r.entry;
|
|
726
|
+
registry = r.registry;
|
|
727
|
+
}
|
|
607
728
|
} catch (e) {
|
|
608
729
|
if (!jsonMode) console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
|
|
609
730
|
}
|
|
610
731
|
}
|
|
611
732
|
|
|
733
|
+
let decryptOptions = {};
|
|
734
|
+
let licenseError = null;
|
|
735
|
+
let manifest = null;
|
|
736
|
+
try {
|
|
737
|
+
manifest = readContainerJson(asset.asset_path, 'kdna.json') || {};
|
|
738
|
+
} catch (e) {
|
|
739
|
+
licenseError = e.message;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const encryptedEntries = Array.isArray(manifest?.encryption?.encrypted_entries)
|
|
743
|
+
? manifest.encryption.encrypted_entries
|
|
744
|
+
: [];
|
|
745
|
+
const requiresProtectedRead =
|
|
746
|
+
encryptedEntries.length > 0 && (want.structure || want.judgment || want.i18n || want.governance);
|
|
747
|
+
if (requiresProtectedRead) {
|
|
748
|
+
const licensed = licenseDecryptOptionsForManifest(manifest);
|
|
749
|
+
if (licensed.ok) {
|
|
750
|
+
decryptOptions = { decryptEntry: licensed.decryptEntry };
|
|
751
|
+
} else {
|
|
752
|
+
licenseError = licensed.error;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const view = assetView(asset.asset_path, decryptOptions);
|
|
612
757
|
const results = [];
|
|
613
|
-
if (want.structure) results.push(checkStructure(
|
|
614
|
-
if (want.trust)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
758
|
+
if (want.structure) results.push(checkStructure(view, { licenseError }));
|
|
759
|
+
if (want.trust) {
|
|
760
|
+
results.push(
|
|
761
|
+
checkTrust(view, scope, entry, {
|
|
762
|
+
registry,
|
|
763
|
+
assetDigest: asset.asset_digest || null,
|
|
764
|
+
assetPath: asset.asset_path,
|
|
765
|
+
}),
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (want.judgment) results.push(checkJudgment(view, { licenseError }));
|
|
769
|
+
if (want.i18n) results.push(checkI18n(view, { licenseError }));
|
|
770
|
+
if (want.governance) results.push(checkGovernance(view, { licenseError }));
|
|
618
771
|
|
|
619
772
|
// ── JSON output ──────────────────────────────────────────────────────
|
|
620
773
|
if (jsonMode) {
|
|
@@ -631,9 +784,6 @@ function cmdVerify(input, args = []) {
|
|
|
631
784
|
const structureResult = results.find((r) => r.layer === 'structure');
|
|
632
785
|
const trustResult = results.find((r) => r.layer === 'trust');
|
|
633
786
|
const judgmentResult = results.find((r) => r.layer === 'judgment');
|
|
634
|
-
const i18nResult = results.find((r) => r.layer === 'i18n');
|
|
635
|
-
const governanceResult = results.find((r) => r.layer === 'governance');
|
|
636
|
-
|
|
637
787
|
let exitCode = EXIT.OK;
|
|
638
788
|
if (structureResult && structureResult.issues.some((i) => i.severity === 'error')) {
|
|
639
789
|
exitCode = EXIT.VALIDATION_FAILED;
|
|
@@ -646,8 +796,10 @@ function cmdVerify(input, args = []) {
|
|
|
646
796
|
console.log(
|
|
647
797
|
JSON.stringify(
|
|
648
798
|
{
|
|
649
|
-
name:
|
|
650
|
-
path:
|
|
799
|
+
name: displayName,
|
|
800
|
+
path: asset.asset_path,
|
|
801
|
+
asset_digest: asset.asset_digest || null,
|
|
802
|
+
content_digest: asset.content_digest || null,
|
|
651
803
|
layers,
|
|
652
804
|
ok: exitCode === EXIT.OK,
|
|
653
805
|
},
|
|
@@ -660,8 +812,10 @@ function cmdVerify(input, args = []) {
|
|
|
660
812
|
|
|
661
813
|
// ── Text output (default) ────────────────────────────────────────────
|
|
662
814
|
console.log('═'.repeat(64));
|
|
663
|
-
console.log(` Verify ${
|
|
664
|
-
console.log(`
|
|
815
|
+
console.log(` Verify ${displayName}`);
|
|
816
|
+
console.log(` Asset: ${asset.asset_path}`);
|
|
817
|
+
if (asset.asset_digest) console.log(` Asset digest: ${asset.asset_digest}`);
|
|
818
|
+
if (asset.content_digest) console.log(` Content digest: ${asset.content_digest}`);
|
|
665
819
|
console.log('═'.repeat(64));
|
|
666
820
|
|
|
667
821
|
for (const r of results) renderLayer(r);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kdna_spec": "1.0",
|
|
2
|
+
"kdna_spec": "1.0-rc",
|
|
3
3
|
"name": "@yourscope/your_domain_id",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"judgment_version": "YYYY.MM",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"description": "[one-sentence description; appears on registry and in install output]",
|
|
12
12
|
"core_insight": "[one-sentence core insight that distinguishes this domain]",
|
|
13
13
|
"keywords": ["[add 3-6 keywords for kdna search]"],
|
|
14
|
+
"quality_badge": "untested",
|
|
14
15
|
"author": {
|
|
15
16
|
"name": "Your Name",
|
|
16
17
|
"id": "your-id",
|