@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/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 sha256 + Ed25519 signature against scope trust key
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 crypto = require('crypto');
21
- const { RegistryResolver, parseName } = require('./registry');
22
- const { EXIT } = require('./cmds/_common');
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 USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
25
- const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
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(destDir) {
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 (!fs.existsSync(path.join(destDir, f))) {
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 (fs.existsSync(path.join(destDir, f))) passed.push(`has ${f}`);
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 = readJson(path.join(destDir, 'KDNA_Core.json'));
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 = readJson(path.join(destDir, 'KDNA_Patterns.json'));
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(destDir, scope, entry) {
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 = readJson(path.join(destDir, 'kdna.json'));
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
- // Recompute fingerprint
138
- const fp =
139
- 'ed25519:' + crypto.createHash('sha256').update(manifest.author.public_key_pem).digest('hex');
140
- if (fp !== manifest.author.pubkey) {
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: 'embedded PEM does not match author.pubkey fingerprint',
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. sha256 vs registry (if entry provided)
186
- if (entry?.sha256) {
187
- passed.push(`registry sha256 declared: ${entry.sha256.slice(0, 16)}…`);
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(destDir) {
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
- const core = readJson(path.join(destDir, 'KDNA_Core.json'));
217
- const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
218
- const manifest = readJson(path.join(destDir, 'kdna.json'));
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 = fs.readFileSync(readmePath, 'utf8');
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) => typeof q === 'string' && q.trim().endsWith('?')).length;
316
- bump(total, yn, `self_check questions ending in "?" (${yn}/${total})`);
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 evalDir = path.join(destDir, 'evals');
323
- if (fs.existsSync(evalDir)) {
324
- const files = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
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(destDir) {
487
+ function checkI18n(input) {
488
+ const view = asView(input);
385
489
  const issues = [];
386
490
  const passed = [];
387
- const manifest = readJson(path.join(destDir, 'kdna.json')) || {};
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 (!fs.existsSync(path.join(localeDir, 'KDNA_CARD.json'))) {
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 = readJson(path.join(localeDir, 'KDNA_CARD.json'));
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 (!fs.existsSync(path.join(localeDir, 'README.md'))) {
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 = path.join(localeDir, 'KDNA_Core.overlay.json');
428
- if (!fs.existsSync(coreOverlay)) {
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 = readJson(coreOverlay);
534
+ const overlay = readJsonFromView(view, coreOverlay, issues);
432
535
  if (overlay?.translations) {
433
- const core = readJson(path.join(destDir, 'KDNA_Core.json'));
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 (!fs.existsSync(path.join(localeDir, 'KDNA_Patterns.overlay.json'))) {
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(destDir) {
575
+ function checkGovernance(input) {
576
+ const view = asView(input);
473
577
  const issues = [];
474
578
  const passed = [];
475
- const card = readJson(path.join(destDir, 'KDNA_CARD.json')) || {};
579
+ const card = readJsonFromView(view, 'KDNA_CARD.json', issues) || {};
476
580
 
477
- if (!readJson(path.join(destDir, 'KDNA_CARD.json'))) {
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 installed path + scope/entry
567
- const parsed = parseName(input);
568
- if (!parsed) {
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: `Invalid name "${input}". Use @scope/name or bare name.`,
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(`Invalid name "${input}". Use @scope/name or bare name.`);
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
- const r = resolver.resolve(parsed.full);
605
- scope = r.scope;
606
- entry = r.entry;
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(destDir));
614
- if (want.trust) results.push(checkTrust(destDir, scope, entry));
615
- if (want.judgment) results.push(checkJudgment(destDir));
616
- if (want.i18n) results.push(checkI18n(destDir));
617
- if (want.governance) results.push(checkGovernance(destDir));
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: parsed.full,
650
- path: destDir,
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 ${parsed.full}`);
664
- console.log(` Path: ${destDir}`);
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",