@aikdna/kdna-cli 0.17.0 → 0.19.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 +120 -101
- package/SECURITY.md +1 -1
- package/package.json +6 -4
- package/skills/kdna-loader/SKILL.md +23 -22
- package/src/agent.js +290 -159
- package/src/cli.js +117 -67
- package/src/cmds/_common.js +40 -18
- package/src/cmds/badge.js +14 -9
- package/src/cmds/changelog.js +32 -12
- package/src/cmds/cluster.js +80 -85
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +114 -427
- package/src/cmds/explain.js +119 -0
- package/src/cmds/governance.js +111 -42
- package/src/cmds/legacy.js +8 -9
- package/src/cmds/license.js +491 -26
- package/src/cmds/quality.js +10 -3
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +99 -47
- package/src/cmds/test.js +9 -6
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +41 -22
- package/src/diff.js +38 -24
- package/src/identity.js +9 -7
- package/src/init.js +2 -2
- package/src/install.js +147 -459
- package/src/loader.js +10 -10
- package/src/package-store.js +232 -0
- package/src/paths.js +44 -0
- package/src/publish.js +150 -51
- package/src/registry.js +81 -9
- package/src/setup.js +19 -20
- package/src/verify.js +293 -140
- package/src/version.js +15 -7
- package/templates/minimal-domain/kdna.json +7 -7
- package/templates/standard-domain/README.md +10 -10
- package/templates/standard-domain/kdna.json +7 -3
- package/validators/kdna-lint.js +45 -11
- 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,19 +17,62 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
-
const
|
|
21
|
-
const {
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const { RegistryResolver, parseName, registryTrustIssues, isEntryRevoked } = require('./registry');
|
|
21
|
+
const { EXIT, isYesNoSelfCheck } = require('./cmds/_common');
|
|
22
|
+
const { licenseDecryptOptionsForManifest } = require('./cmds/license');
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
getInstalled,
|
|
26
|
+
listContainerEntries,
|
|
27
|
+
readContainerEntry,
|
|
28
|
+
readContainerJson,
|
|
29
|
+
resolveAsset,
|
|
30
|
+
verifyAsset,
|
|
31
|
+
} = require('./package-store');
|
|
32
|
+
|
|
33
|
+
function validateManifestFn(manifest) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
const warnings = [];
|
|
36
|
+
const required = [
|
|
37
|
+
'format',
|
|
38
|
+
'format_version',
|
|
39
|
+
'spec_version',
|
|
40
|
+
'name',
|
|
41
|
+
'version',
|
|
42
|
+
'judgment_version',
|
|
43
|
+
'description',
|
|
44
|
+
'author',
|
|
45
|
+
'license',
|
|
46
|
+
'status',
|
|
47
|
+
'quality_badge',
|
|
48
|
+
'access',
|
|
49
|
+
'languages',
|
|
50
|
+
'default_language',
|
|
51
|
+
];
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
if (manifest.kdna_spec) errors.push('kdna.json: kdna_spec is not allowed. Use spec_version.');
|
|
54
|
+
if (manifest.language)
|
|
55
|
+
errors.push('kdna.json: language is not allowed. Use default_language and languages.');
|
|
56
|
+
for (const field of required) {
|
|
57
|
+
if (!(field in manifest) || manifest[field] === undefined || manifest[field] === '') {
|
|
58
|
+
errors.push(`kdna.json: missing required field "${field}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (manifest.format && manifest.format !== 'kdna') {
|
|
62
|
+
errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
|
|
63
|
+
}
|
|
64
|
+
if (manifest.format_version && manifest.format_version !== '1.0') {
|
|
65
|
+
errors.push(
|
|
66
|
+
`kdna.json.format_version: invalid value "${manifest.format_version}". Expected "1.0".`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
|
|
70
|
+
warnings.push(
|
|
71
|
+
`kdna.json.spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return { errors, warnings };
|
|
75
|
+
}
|
|
33
76
|
|
|
34
77
|
function readJson(p) {
|
|
35
78
|
try {
|
|
@@ -39,11 +82,84 @@ function readJson(p) {
|
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
|
|
85
|
+
function directoryView(root) {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'directory',
|
|
88
|
+
exists(name) {
|
|
89
|
+
return fs.existsSync(path.join(root, name));
|
|
90
|
+
},
|
|
91
|
+
readJson(name) {
|
|
92
|
+
return readJson(path.join(root, name));
|
|
93
|
+
},
|
|
94
|
+
readText(name) {
|
|
95
|
+
try {
|
|
96
|
+
return fs.readFileSync(path.join(root, name), 'utf8');
|
|
97
|
+
} catch {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
listDirFiles(dirName) {
|
|
102
|
+
const dir = path.join(root, dirName);
|
|
103
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
104
|
+
return fs.readdirSync(dir);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function assetView(kdnaPath, options = {}) {
|
|
110
|
+
const entries = new Set(listContainerEntries(kdnaPath));
|
|
111
|
+
return {
|
|
112
|
+
kind: 'asset',
|
|
113
|
+
path: kdnaPath,
|
|
114
|
+
exists(name) {
|
|
115
|
+
return entries.has(name);
|
|
116
|
+
},
|
|
117
|
+
readJson(name) {
|
|
118
|
+
return readContainerJson(kdnaPath, name, options);
|
|
119
|
+
},
|
|
120
|
+
readText(name) {
|
|
121
|
+
if (!entries.has(name)) return '';
|
|
122
|
+
return readContainerEntry(kdnaPath, name).toString('utf8');
|
|
123
|
+
},
|
|
124
|
+
listDirFiles(dirName) {
|
|
125
|
+
const prefix = `${dirName.replace(/\/+$/, '')}/`;
|
|
126
|
+
const files = [];
|
|
127
|
+
for (const entryName of entries) {
|
|
128
|
+
if (!entryName.startsWith(prefix)) continue;
|
|
129
|
+
const rest = entryName.slice(prefix.length);
|
|
130
|
+
if (rest && !rest.includes('/')) files.push(rest);
|
|
131
|
+
}
|
|
132
|
+
return files;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function asView(input, options = {}) {
|
|
138
|
+
if (input && typeof input.exists === 'function') return input;
|
|
139
|
+
return directoryView(input, options);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readJsonFromView(view, entryName, issues = null) {
|
|
143
|
+
try {
|
|
144
|
+
return view.readJson(entryName);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (issues) issues.push({ severity: 'error', msg: `${entryName}: ${e.message}` });
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
42
151
|
// ─── Structure layer ───────────────────────────────────────────────────
|
|
43
152
|
|
|
44
|
-
function checkStructure(
|
|
153
|
+
function checkStructure(input, options = {}) {
|
|
154
|
+
const view = asView(input);
|
|
45
155
|
const issues = [];
|
|
46
156
|
const passed = [];
|
|
157
|
+
if (options.licenseError) {
|
|
158
|
+
issues.push({
|
|
159
|
+
severity: 'error',
|
|
160
|
+
msg: `license required to verify encrypted entries: ${options.licenseError}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
47
163
|
|
|
48
164
|
const required = ['KDNA_Core.json', 'KDNA_Patterns.json', 'kdna.json'];
|
|
49
165
|
const optional = [
|
|
@@ -54,7 +170,7 @@ function checkStructure(destDir) {
|
|
|
54
170
|
];
|
|
55
171
|
|
|
56
172
|
for (const f of required) {
|
|
57
|
-
if (!
|
|
173
|
+
if (!view.exists(f)) {
|
|
58
174
|
issues.push({ severity: 'error', msg: `required file missing: ${f}` });
|
|
59
175
|
} else {
|
|
60
176
|
passed.push(`has ${f}`);
|
|
@@ -63,7 +179,7 @@ function checkStructure(destDir) {
|
|
|
63
179
|
|
|
64
180
|
// Validate kdna.json against canonical manifest schema
|
|
65
181
|
if (validateManifestFn) {
|
|
66
|
-
const manifest =
|
|
182
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
67
183
|
if (manifest) {
|
|
68
184
|
const mResult = validateManifestFn(manifest);
|
|
69
185
|
for (const e of mResult.errors) issues.push({ severity: 'error', msg: e });
|
|
@@ -73,12 +189,12 @@ function checkStructure(destDir) {
|
|
|
73
189
|
}
|
|
74
190
|
|
|
75
191
|
for (const f of optional) {
|
|
76
|
-
if (
|
|
192
|
+
if (view.exists(f)) passed.push(`has ${f}`);
|
|
77
193
|
}
|
|
78
194
|
|
|
79
195
|
// Schema check using kdna-core if available
|
|
80
196
|
try {
|
|
81
|
-
const core =
|
|
197
|
+
const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
82
198
|
if (core) {
|
|
83
199
|
if (!core.axioms || !Array.isArray(core.axioms) || core.axioms.length === 0) {
|
|
84
200
|
issues.push({ severity: 'error', msg: 'KDNA_Core.axioms missing or empty' });
|
|
@@ -90,7 +206,7 @@ function checkStructure(destDir) {
|
|
|
90
206
|
issues.push({ severity: 'warn', msg: 'KDNA_Core.stances missing or empty' });
|
|
91
207
|
}
|
|
92
208
|
}
|
|
93
|
-
const pat =
|
|
209
|
+
const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
|
|
94
210
|
if (pat) {
|
|
95
211
|
if (!pat.misunderstandings || pat.misunderstandings.length === 0) {
|
|
96
212
|
issues.push({ severity: 'warn', msg: 'KDNA_Patterns.misunderstandings missing or empty' });
|
|
@@ -111,11 +227,12 @@ function checkStructure(destDir) {
|
|
|
111
227
|
|
|
112
228
|
// ─── Trust layer ───────────────────────────────────────────────────────
|
|
113
229
|
|
|
114
|
-
function checkTrust(
|
|
230
|
+
function checkTrust(input, scope, entry, options = {}) {
|
|
231
|
+
const view = asView(input);
|
|
115
232
|
const issues = [];
|
|
116
233
|
const passed = [];
|
|
117
234
|
|
|
118
|
-
const manifest =
|
|
235
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
119
236
|
if (!manifest) {
|
|
120
237
|
issues.push({ severity: 'error', msg: 'kdna.json missing — cannot verify trust' });
|
|
121
238
|
return { layer: 'trust', issues, passed };
|
|
@@ -151,58 +268,56 @@ function checkTrust(destDir, scope, entry) {
|
|
|
151
268
|
});
|
|
152
269
|
} else {
|
|
153
270
|
passed.push('embedded public_key_pem present');
|
|
271
|
+
}
|
|
154
272
|
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
273
|
+
if (options.assetPath) {
|
|
274
|
+
const verification = verifyAsset(options.assetPath, { requireSignature: true });
|
|
275
|
+
for (const warning of verification.warnings || []) {
|
|
276
|
+
if (!/signature missing/.test(warning)) issues.push({ severity: 'warn', msg: warning });
|
|
277
|
+
}
|
|
278
|
+
for (const err of verification.errors || []) {
|
|
279
|
+
if (/signature|author\.|public_key|pubkey|fingerprint|Ed25519/i.test(err)) {
|
|
280
|
+
issues.push({ severity: 'error', msg: err });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (verification.signature_valid === true) {
|
|
284
|
+
passed.push('Ed25519 signature VALID over canonical payload');
|
|
285
|
+
} else if (manifest.signature) {
|
|
159
286
|
issues.push({
|
|
160
287
|
severity: 'error',
|
|
161
|
-
msg: '
|
|
288
|
+
msg: 'Ed25519 signature INVALID or unavailable',
|
|
162
289
|
});
|
|
163
|
-
} else {
|
|
164
|
-
passed.push('PEM fingerprint matches author.pubkey');
|
|
165
|
-
|
|
166
|
-
// Full Ed25519 verify
|
|
167
|
-
try {
|
|
168
|
-
const files = fs
|
|
169
|
-
.readdirSync(destDir)
|
|
170
|
-
.filter((f) => f.endsWith('.json'))
|
|
171
|
-
.sort();
|
|
172
|
-
const parts = [];
|
|
173
|
-
for (const f of files) {
|
|
174
|
-
const full = path.join(destDir, f);
|
|
175
|
-
let buf;
|
|
176
|
-
if (f === 'kdna.json') {
|
|
177
|
-
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
178
|
-
delete obj.signature;
|
|
179
|
-
delete obj._source;
|
|
180
|
-
buf = Buffer.from(JSON.stringify(obj));
|
|
181
|
-
} else {
|
|
182
|
-
buf = fs.readFileSync(full);
|
|
183
|
-
}
|
|
184
|
-
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
185
|
-
parts.push(`${f}:${hash}`);
|
|
186
|
-
}
|
|
187
|
-
const payload = parts.join('\n');
|
|
188
|
-
const sigHex = manifest.signature.replace(/^ed25519:/, '');
|
|
189
|
-
const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
|
|
190
|
-
const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
|
|
191
|
-
if (ok) passed.push('Ed25519 signature VALID over canonical payload');
|
|
192
|
-
else
|
|
193
|
-
issues.push({
|
|
194
|
-
severity: 'error',
|
|
195
|
-
msg: 'Ed25519 signature INVALID — package may be tampered',
|
|
196
|
-
});
|
|
197
|
-
} catch (e) {
|
|
198
|
-
issues.push({ severity: 'error', msg: `signature verify failed: ${e.message}` });
|
|
199
|
-
}
|
|
200
290
|
}
|
|
201
291
|
}
|
|
202
292
|
|
|
203
|
-
// 4.
|
|
204
|
-
|
|
205
|
-
|
|
293
|
+
// 4. asset digest vs registry (if entry provided)
|
|
294
|
+
const registry = options.registry || null;
|
|
295
|
+
const registryIssues = registry ? registryTrustIssues(registry) : [];
|
|
296
|
+
for (const issue of registryIssues) {
|
|
297
|
+
issues.push({ severity: 'error', msg: issue });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const revocation = registry && entry ? isEntryRevoked(registry, entry) : null;
|
|
301
|
+
if (revocation) {
|
|
302
|
+
issues.push({
|
|
303
|
+
severity: 'error',
|
|
304
|
+
msg: `registry revokes this asset${revocation.reason ? `: ${revocation.reason}` : ''}`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const registryDigest = entry?.asset_digest || null;
|
|
309
|
+
if (registryDigest) {
|
|
310
|
+
passed.push(`registry asset_digest declared: ${registryDigest.slice(0, 23)}…`);
|
|
311
|
+
if (options.assetDigest && options.assetDigest !== registryDigest) {
|
|
312
|
+
issues.push({
|
|
313
|
+
severity: 'error',
|
|
314
|
+
msg: `asset digest mismatch: registry ${registryDigest}, local ${options.assetDigest}`,
|
|
315
|
+
});
|
|
316
|
+
} else if (options.assetDigest) {
|
|
317
|
+
passed.push('local asset_digest matches registry');
|
|
318
|
+
}
|
|
319
|
+
} else if (entry) {
|
|
320
|
+
issues.push({ severity: 'error', msg: 'registry asset_digest missing' });
|
|
206
321
|
}
|
|
207
322
|
|
|
208
323
|
// 5. scope governance
|
|
@@ -218,7 +333,8 @@ function checkTrust(destDir, scope, entry) {
|
|
|
218
333
|
|
|
219
334
|
// ─── Judgment layer ────────────────────────────────────────────────────
|
|
220
335
|
|
|
221
|
-
function checkJudgment(
|
|
336
|
+
function checkJudgment(input, options = {}) {
|
|
337
|
+
const view = asView(input);
|
|
222
338
|
const issues = [];
|
|
223
339
|
const passed = [];
|
|
224
340
|
const score = { total: 0, max: 0 };
|
|
@@ -231,17 +347,23 @@ function checkJudgment(destDir) {
|
|
|
231
347
|
else issues.push({ severity: 'warn', msg: `missing: ${label}` });
|
|
232
348
|
}
|
|
233
349
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
350
|
+
if (options.licenseError) {
|
|
351
|
+
issues.push({
|
|
352
|
+
severity: 'error',
|
|
353
|
+
msg: `license required to verify encrypted judgment: ${options.licenseError}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
358
|
+
const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
|
|
359
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues);
|
|
237
360
|
|
|
238
361
|
// 1. Boundary declaration in README (REQUIRED)
|
|
239
362
|
// Either classic "## Scope" + "## Out of Scope" pair,
|
|
240
363
|
// OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
|
|
241
|
-
const readmePath = path.join(destDir, 'README.md');
|
|
242
364
|
let readme = '';
|
|
243
365
|
try {
|
|
244
|
-
readme =
|
|
366
|
+
readme = view.readText('README.md');
|
|
245
367
|
} catch {
|
|
246
368
|
/* ok */
|
|
247
369
|
}
|
|
@@ -330,16 +452,16 @@ function checkJudgment(destDir) {
|
|
|
330
452
|
// 4. self_check format: yes/no questions
|
|
331
453
|
if (pat?.self_check) {
|
|
332
454
|
const total = pat.self_check.length;
|
|
333
|
-
const yn = pat.self_check.filter((q) =>
|
|
334
|
-
bump(total, yn, `self_check questions
|
|
455
|
+
const yn = pat.self_check.filter((q) => isYesNoSelfCheck(q)).length;
|
|
456
|
+
bump(total, yn, `self_check yes/no questions (${yn}/${total})`);
|
|
335
457
|
if (total < 3)
|
|
336
458
|
issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
|
|
337
459
|
}
|
|
338
460
|
|
|
339
461
|
// 5. eval cases present (REQUIRED: ≥4 cases)
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
342
|
-
const files =
|
|
462
|
+
const evalFiles = view.listDirFiles('evals').filter((f) => f.endsWith('.json'));
|
|
463
|
+
if (evalFiles.length) {
|
|
464
|
+
const files = evalFiles;
|
|
343
465
|
if (files.length >= 4) {
|
|
344
466
|
bump(2, 2, `evals/ directory has ${files.length} case files`);
|
|
345
467
|
} else if (files.length > 0) {
|
|
@@ -399,10 +521,11 @@ function renderLayer(result) {
|
|
|
399
521
|
|
|
400
522
|
// ─── I18N layer ──────────────────────────────────────────────────────
|
|
401
523
|
|
|
402
|
-
function checkI18n(
|
|
524
|
+
function checkI18n(input) {
|
|
525
|
+
const view = asView(input);
|
|
403
526
|
const issues = [];
|
|
404
527
|
const passed = [];
|
|
405
|
-
const manifest =
|
|
528
|
+
const manifest = readJsonFromView(view, 'kdna.json', issues) || {};
|
|
406
529
|
const languages = manifest.languages || [];
|
|
407
530
|
const i18nLevel = manifest.i18n_level || 'L0';
|
|
408
531
|
|
|
@@ -417,14 +540,13 @@ function checkI18n(destDir) {
|
|
|
417
540
|
const canonical = manifest.default_language || languages[0] || 'en';
|
|
418
541
|
for (const lang of languages) {
|
|
419
542
|
if (lang === canonical) continue;
|
|
420
|
-
const localeDir = path.join(destDir, 'locales', lang);
|
|
421
543
|
|
|
422
544
|
// L1: card + readme
|
|
423
545
|
if (['L1', 'L2', 'L3', 'L4'].includes(i18nLevel)) {
|
|
424
|
-
if (!
|
|
546
|
+
if (!view.exists(`locales/${lang}/KDNA_CARD.json`)) {
|
|
425
547
|
issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_CARD.json missing` });
|
|
426
548
|
} else {
|
|
427
|
-
const card =
|
|
549
|
+
const card = readJsonFromView(view, `locales/${lang}/KDNA_CARD.json`, issues);
|
|
428
550
|
if (card) {
|
|
429
551
|
passed.push(`locales/${lang}/KDNA_CARD.json OK`);
|
|
430
552
|
if (!card.display_name)
|
|
@@ -433,7 +555,7 @@ function checkI18n(destDir) {
|
|
|
433
555
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} card missing intended_use` });
|
|
434
556
|
}
|
|
435
557
|
}
|
|
436
|
-
if (!
|
|
558
|
+
if (!view.exists(`locales/${lang}/README.md`)) {
|
|
437
559
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} README.md missing` });
|
|
438
560
|
} else {
|
|
439
561
|
passed.push(`locales/${lang}/README.md OK`);
|
|
@@ -442,13 +564,13 @@ function checkI18n(destDir) {
|
|
|
442
564
|
|
|
443
565
|
// L2: overlay files
|
|
444
566
|
if (['L2', 'L3', 'L4'].includes(i18nLevel)) {
|
|
445
|
-
const coreOverlay =
|
|
446
|
-
if (!
|
|
567
|
+
const coreOverlay = `locales/${lang}/KDNA_Core.overlay.json`;
|
|
568
|
+
if (!view.exists(coreOverlay)) {
|
|
447
569
|
issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_Core.overlay.json missing` });
|
|
448
570
|
} else {
|
|
449
|
-
const overlay =
|
|
571
|
+
const overlay = readJsonFromView(view, coreOverlay, issues);
|
|
450
572
|
if (overlay?.translations) {
|
|
451
|
-
const core =
|
|
573
|
+
const core = readJsonFromView(view, 'KDNA_Core.json', issues);
|
|
452
574
|
if (core?.axioms) {
|
|
453
575
|
const validIds = new Set(core.axioms.map((a) => a.id));
|
|
454
576
|
for (const key of Object.keys(overlay.translations)) {
|
|
@@ -466,7 +588,7 @@ function checkI18n(destDir) {
|
|
|
466
588
|
);
|
|
467
589
|
}
|
|
468
590
|
}
|
|
469
|
-
if (!
|
|
591
|
+
if (!view.exists(`locales/${lang}/KDNA_Patterns.overlay.json`)) {
|
|
470
592
|
issues.push({ severity: 'warn', msg: `i18n: ${lang} KDNA_Patterns.overlay.json missing` });
|
|
471
593
|
}
|
|
472
594
|
}
|
|
@@ -487,12 +609,13 @@ function checkI18n(destDir) {
|
|
|
487
609
|
|
|
488
610
|
// ─── Governance layer ───────────────────────────────────────────────
|
|
489
611
|
|
|
490
|
-
function checkGovernance(
|
|
612
|
+
function checkGovernance(input) {
|
|
613
|
+
const view = asView(input);
|
|
491
614
|
const issues = [];
|
|
492
615
|
const passed = [];
|
|
493
|
-
const card =
|
|
616
|
+
const card = readJsonFromView(view, 'KDNA_CARD.json', issues) || {};
|
|
494
617
|
|
|
495
|
-
if (!
|
|
618
|
+
if (!card || !view.exists('KDNA_CARD.json')) {
|
|
496
619
|
issues.push({ severity: 'error', msg: 'governance: KDNA_CARD.json missing — required' });
|
|
497
620
|
return { layer: 'governance', passed: false, issues, results: issues.map((i) => i.msg) };
|
|
498
621
|
}
|
|
@@ -579,22 +702,28 @@ function cmdVerify(input, args = []) {
|
|
|
579
702
|
console.log(JSON.stringify({ ok: false, error: `Invalid name: ${input}` }));
|
|
580
703
|
process.exit(EXIT.INPUT_ERROR);
|
|
581
704
|
}
|
|
582
|
-
const
|
|
583
|
-
if (!
|
|
705
|
+
const installed = getInstalled(parsed.full);
|
|
706
|
+
if (!installed) {
|
|
584
707
|
console.log(JSON.stringify({ ok: false, error: `Domain not installed: ${input}` }));
|
|
585
708
|
process.exit(EXIT.INPUT_ERROR);
|
|
586
709
|
}
|
|
587
710
|
const { checkTrust: agentCheckTrust } = require('./agent');
|
|
588
711
|
const trust = agentCheckTrust(parsed.full);
|
|
589
|
-
console.log(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
712
|
+
console.log(
|
|
713
|
+
JSON.stringify(
|
|
714
|
+
{
|
|
715
|
+
domain: parsed.full,
|
|
716
|
+
passed: trust.passed,
|
|
717
|
+
failures: trust.failures,
|
|
718
|
+
warnings: trust.warnings,
|
|
719
|
+
risk_level: trust.riskLevel,
|
|
720
|
+
spec_version: trust.specVersion,
|
|
721
|
+
signature_valid: trust.signatureValid,
|
|
722
|
+
},
|
|
723
|
+
null,
|
|
724
|
+
2,
|
|
725
|
+
),
|
|
726
|
+
);
|
|
598
727
|
process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
|
|
599
728
|
}
|
|
600
729
|
|
|
@@ -608,58 +737,81 @@ function cmdVerify(input, args = []) {
|
|
|
608
737
|
const all = !want.structure && !want.trust && !want.judgment && !want.i18n && !want.governance;
|
|
609
738
|
if (all) want.structure = want.trust = want.judgment = true;
|
|
610
739
|
|
|
611
|
-
// Resolve name
|
|
612
|
-
const
|
|
613
|
-
|
|
740
|
+
// Resolve installed name or direct local .kdna asset path.
|
|
741
|
+
const asset = resolveAsset(input);
|
|
742
|
+
const parsed = asset?.parsed || parseName(asset?.name || '');
|
|
743
|
+
const displayName = parsed?.full || asset?.name || input;
|
|
744
|
+
if (!asset) {
|
|
614
745
|
if (jsonMode) {
|
|
615
746
|
console.log(
|
|
616
747
|
JSON.stringify({
|
|
617
748
|
name: input,
|
|
618
749
|
ok: false,
|
|
619
|
-
error: `
|
|
750
|
+
error: `KDNA asset not found: ${input}. Use an installed name or a .kdna file.`,
|
|
620
751
|
}),
|
|
621
752
|
);
|
|
622
753
|
} else {
|
|
623
|
-
console.error(`
|
|
624
|
-
}
|
|
625
|
-
process.exit(EXIT.INPUT_ERROR);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
|
|
629
|
-
if (!fs.existsSync(destDir)) {
|
|
630
|
-
if (jsonMode) {
|
|
631
|
-
console.log(
|
|
632
|
-
JSON.stringify({
|
|
633
|
-
name: parsed.full,
|
|
634
|
-
ok: false,
|
|
635
|
-
error: `${parsed.full} is not installed. Run: kdna install ${input}`,
|
|
636
|
-
}),
|
|
637
|
-
);
|
|
638
|
-
} else {
|
|
639
|
-
console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
|
|
754
|
+
console.error(`KDNA asset not found: ${input}. Use an installed name or a .kdna file.`);
|
|
640
755
|
}
|
|
641
756
|
process.exit(EXIT.INPUT_ERROR);
|
|
642
757
|
}
|
|
643
758
|
|
|
644
759
|
let scope = null,
|
|
645
|
-
entry = null
|
|
760
|
+
entry = null,
|
|
761
|
+
registry = null;
|
|
646
762
|
if (want.trust) {
|
|
647
763
|
try {
|
|
648
764
|
const resolver = new RegistryResolver({ allowNetwork: false });
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
765
|
+
if (parsed) {
|
|
766
|
+
const r = resolver.resolve(parsed.full);
|
|
767
|
+
scope = r.scope;
|
|
768
|
+
entry = r.entry;
|
|
769
|
+
registry = r.registry;
|
|
770
|
+
}
|
|
652
771
|
} catch (e) {
|
|
653
772
|
if (!jsonMode) console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
|
|
654
773
|
}
|
|
655
774
|
}
|
|
656
775
|
|
|
776
|
+
let decryptOptions = {};
|
|
777
|
+
let licenseError = null;
|
|
778
|
+
let manifest = null;
|
|
779
|
+
try {
|
|
780
|
+
manifest = readContainerJson(asset.asset_path, 'kdna.json') || {};
|
|
781
|
+
} catch (e) {
|
|
782
|
+
licenseError = e.message;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const encryptedEntries = Array.isArray(manifest?.encryption?.encrypted_entries)
|
|
786
|
+
? manifest.encryption.encrypted_entries
|
|
787
|
+
: [];
|
|
788
|
+
const requiresProtectedRead =
|
|
789
|
+
encryptedEntries.length > 0 &&
|
|
790
|
+
(want.structure || want.judgment || want.i18n || want.governance);
|
|
791
|
+
if (requiresProtectedRead) {
|
|
792
|
+
const licensed = licenseDecryptOptionsForManifest(manifest);
|
|
793
|
+
if (licensed.ok) {
|
|
794
|
+
decryptOptions = { decryptEntry: licensed.decryptEntry };
|
|
795
|
+
} else {
|
|
796
|
+
licenseError = licensed.error;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const view = assetView(asset.asset_path, decryptOptions);
|
|
657
801
|
const results = [];
|
|
658
|
-
if (want.structure) results.push(checkStructure(
|
|
659
|
-
if (want.trust)
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
802
|
+
if (want.structure) results.push(checkStructure(view, { licenseError }));
|
|
803
|
+
if (want.trust) {
|
|
804
|
+
results.push(
|
|
805
|
+
checkTrust(view, scope, entry, {
|
|
806
|
+
registry,
|
|
807
|
+
assetDigest: asset.asset_digest || null,
|
|
808
|
+
assetPath: asset.asset_path,
|
|
809
|
+
}),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (want.judgment) results.push(checkJudgment(view, { licenseError }));
|
|
813
|
+
if (want.i18n) results.push(checkI18n(view, { licenseError }));
|
|
814
|
+
if (want.governance) results.push(checkGovernance(view, { licenseError }));
|
|
663
815
|
|
|
664
816
|
// ── JSON output ──────────────────────────────────────────────────────
|
|
665
817
|
if (jsonMode) {
|
|
@@ -676,9 +828,6 @@ function cmdVerify(input, args = []) {
|
|
|
676
828
|
const structureResult = results.find((r) => r.layer === 'structure');
|
|
677
829
|
const trustResult = results.find((r) => r.layer === 'trust');
|
|
678
830
|
const judgmentResult = results.find((r) => r.layer === 'judgment');
|
|
679
|
-
const i18nResult = results.find((r) => r.layer === 'i18n');
|
|
680
|
-
const governanceResult = results.find((r) => r.layer === 'governance');
|
|
681
|
-
|
|
682
831
|
let exitCode = EXIT.OK;
|
|
683
832
|
if (structureResult && structureResult.issues.some((i) => i.severity === 'error')) {
|
|
684
833
|
exitCode = EXIT.VALIDATION_FAILED;
|
|
@@ -691,8 +840,10 @@ function cmdVerify(input, args = []) {
|
|
|
691
840
|
console.log(
|
|
692
841
|
JSON.stringify(
|
|
693
842
|
{
|
|
694
|
-
name:
|
|
695
|
-
path:
|
|
843
|
+
name: displayName,
|
|
844
|
+
path: asset.asset_path,
|
|
845
|
+
asset_digest: asset.asset_digest || null,
|
|
846
|
+
content_digest: asset.content_digest || null,
|
|
696
847
|
layers,
|
|
697
848
|
ok: exitCode === EXIT.OK,
|
|
698
849
|
},
|
|
@@ -705,8 +856,10 @@ function cmdVerify(input, args = []) {
|
|
|
705
856
|
|
|
706
857
|
// ── Text output (default) ────────────────────────────────────────────
|
|
707
858
|
console.log('═'.repeat(64));
|
|
708
|
-
console.log(` Verify ${
|
|
709
|
-
console.log(`
|
|
859
|
+
console.log(` Verify ${displayName}`);
|
|
860
|
+
console.log(` Asset: ${asset.asset_path}`);
|
|
861
|
+
if (asset.asset_digest) console.log(` Asset digest: ${asset.asset_digest}`);
|
|
862
|
+
if (asset.content_digest) console.log(` Content digest: ${asset.content_digest}`);
|
|
710
863
|
console.log('═'.repeat(64));
|
|
711
864
|
|
|
712
865
|
for (const r of results) renderLayer(r);
|