@aikdna/kdna-cli 0.19.3 → 0.20.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/publish.js CHANGED
@@ -510,28 +510,14 @@ function cmdPublishCheck(domainPath, args = []) {
510
510
  }
511
511
 
512
512
  // ═══════════════════════════════════════════════════════════════════════
513
- // v0.7: full publish pipeline (validate + pack + sign + upload + patch)
513
+ // Registry publish pipeline for existing .kdna assets.
514
514
  // ═══════════════════════════════════════════════════════════════════════
515
515
 
516
516
  const crypto = require('crypto');
517
- const { execSync, execFileSync } = require('child_process');
518
- const identity = require('./identity');
519
- const { fingerprint } = identity;
517
+ const { execFileSync } = require('child_process');
520
518
 
521
519
  const NAME_RE = /^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9_]*)$/;
522
520
 
523
- function identityPaths() {
524
- // Recompute each call so KDNA_IDENTITY_DIR env var can be changed at runtime
525
- const dir =
526
- process.env.KDNA_IDENTITY_DIR ||
527
- path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'identity');
528
- return {
529
- privateKeyPath: path.join(dir, 'kdna.key'),
530
- publicKeyPath: path.join(dir, 'kdna.pub'),
531
- dir,
532
- };
533
- }
534
-
535
521
  /**
536
522
  * Canonical signing payload: sorted (filename, sha256) pairs of all published
537
523
  * content entries inside the .kdna ZIP, joined as `name:hex\n`.
@@ -581,38 +567,6 @@ function stableStringify(value) {
581
567
  return JSON.stringify(value);
582
568
  }
583
569
 
584
- function manifestForContentDigest(manifest) {
585
- const copy = { ...(manifest || {}) };
586
- delete copy.signature;
587
- delete copy.asset_digest;
588
- delete copy.container_sha256;
589
- delete copy.content_digest;
590
- delete copy._source;
591
- return copy;
592
- }
593
-
594
- function sourceContentDigest(srcDir) {
595
- const files = listPublishEntries(srcDir).sort();
596
- const parts = [];
597
- for (const f of files) {
598
- let buf;
599
- if (f === 'mimetype') {
600
- buf = Buffer.from('application/vnd.aikdna.kdna+zip');
601
- } else if (f.endsWith('.json')) {
602
- const obj = JSON.parse(fs.readFileSync(path.join(srcDir, f), 'utf8'));
603
- const value = f === 'kdna.json' ? manifestForContentDigest(obj) : obj;
604
- buf = Buffer.from(stableStringify(value));
605
- } else {
606
- buf = fs.readFileSync(path.join(srcDir, f));
607
- }
608
- parts.push(`${f}:${crypto.createHash('sha256').update(buf).digest('hex')}`);
609
- }
610
- return `sha256:${crypto
611
- .createHash('sha256')
612
- .update(Buffer.from(parts.join('\n')))
613
- .digest('hex')}`;
614
- }
615
-
616
570
  function listPublishEntries(domainDir) {
617
571
  const entries = ['mimetype'];
618
572
  const skipDirs = new Set(['.git', 'node_modules', 'dist']);
@@ -643,88 +597,32 @@ function listPublishEntries(domainDir) {
643
597
  return entries;
644
598
  }
645
599
 
646
- function signPayload(payload, privateKeyPem) {
647
- const privateKey = crypto.createPrivateKey(privateKeyPem);
648
- const sig = crypto.sign(null, Buffer.from(payload), privateKey);
649
- return sig.toString('hex');
650
- }
651
-
652
- function loadIdentity() {
653
- const { privateKeyPath, publicKeyPath, dir } = identityPaths();
654
- if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
655
- error(`No identity found at ${dir}. Run: kdna identity init (or set KDNA_IDENTITY_DIR)`);
656
- }
657
- return {
658
- privateKey: fs.readFileSync(privateKeyPath, 'utf8'),
659
- publicKey: fs.readFileSync(publicKeyPath, 'utf8'),
660
- };
661
- }
662
-
663
600
  function publicKeyToScopeFormat(publicKeyPem) {
664
601
  // The trust_pubkey in registry is stored as "ed25519:<sha256-of-PEM-hex>"
665
602
  // because Ed25519 PEM is multi-line; the scope key is a stable fingerprint.
666
603
  return 'ed25519:' + crypto.createHash('sha256').update(publicKeyPem).digest('hex');
667
604
  }
668
605
 
669
- function packToFile(domainDir, outPath) {
670
- const files = listPublishEntries(domainDir).filter((f) => f !== 'mimetype');
671
- if (!files.includes('kdna.json'))
672
- error('kdna.json required in dev source directory for publish.');
673
-
674
- const script = `import zipfile, os
675
- src = ${JSON.stringify(domainDir)}
676
- out = ${JSON.stringify(outPath)}
677
- files = ${JSON.stringify(files)}
678
- with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
679
- zf.writestr(zipfile.ZipInfo('mimetype'), 'application/vnd.aikdna.kdna+zip', compress_type=zipfile.ZIP_STORED)
680
- for f in files:
681
- zf.write(os.path.join(src, f), f)
682
- `;
683
- const tmpPy = `/tmp/kdna-publish-pack-${Date.now()}.py`;
684
- try {
685
- fs.writeFileSync(tmpPy, script);
686
- execSync(`python3 ${tmpPy}`, { stdio: 'pipe' });
687
- } finally {
688
- try {
689
- fs.unlinkSync(tmpPy);
690
- } catch {
691
- /* ignore */
692
- }
693
- }
694
- }
695
-
696
- function sha256File(p) {
697
- return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
698
- }
699
-
700
- function outputDirFromArgs(args, fallback) {
701
- for (const flag of ['--output', '--out', '-o']) {
702
- const idx = args.indexOf(flag);
703
- if (idx >= 0) return args[idx + 1];
704
- }
705
- return fallback;
706
- }
707
-
708
606
  /**
709
- * kdna publish <path> Full publish pipeline.
607
+ * kdna publish <file.kdna> Publish an existing Studio-compiled asset.
710
608
  *
711
- * Steps:
712
- * 1. Validate name = @scope/name; load identity; validate author.pubkey
713
- * 2. Quality gate (cmdPublishCheck, soft)
714
- * 3. Write signature into kdna.json (canonical payload signed with identity)
715
- * 4. Pack into .kdna
716
- * 5. Compute sha256
717
- * 6. If --release-tag <tag> and --repo <owner/name>: upload via gh CLI
718
- * 7. Print registry patch JSON
609
+ * Publishing no longer packs arbitrary source directories. Source directories
610
+ * are non-canonical dev workspaces; trusted assets come from Studio-compatible
611
+ * compile/export pipelines.
719
612
  */
720
- function cmdPublish(domainPath, args = []) {
721
- const abs = path.resolve(domainPath);
722
- if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) error(`Not a directory: ${abs}`);
723
-
724
- const manifestPath = path.join(abs, 'kdna.json');
725
- if (!fs.existsSync(manifestPath)) error('kdna.json required at domain root.');
613
+ function cmdPublish(assetPath, args = []) {
614
+ const abs = path.resolve(assetPath);
615
+ if (!fs.existsSync(abs)) error(`Path not found: ${abs}`, EXIT.INPUT_ERROR);
616
+ if (fs.statSync(abs).isDirectory()) {
617
+ error(
618
+ 'kdna publish only accepts existing .kdna assets. Source directories are non-canonical; use KDNA Studio compile/export, then run kdna publish <file.kdna>.',
619
+ EXIT.INPUT_ERROR,
620
+ );
621
+ }
622
+ if (!abs.endsWith('.kdna')) error('kdna publish requires a .kdna asset file.', EXIT.INPUT_ERROR);
726
623
 
727
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
624
+ const { readAssetManifest, assetDigest, contentDigest } = require('./package-store');
625
+ const manifest = readAssetManifest(abs);
728
626
  const name = manifest.name;
729
627
  const m = name && name.match(NAME_RE);
730
628
  if (!m) {
@@ -732,72 +630,28 @@ function cmdPublish(domainPath, args = []) {
732
630
  }
733
631
  if (!manifest.version) error('kdna.json.version required.');
734
632
 
735
- const { privateKey, publicKey } = loadIdentity();
736
- const scopeKey = publicKeyToScopeFormat(publicKey);
737
-
738
633
  console.log('═'.repeat(60));
739
634
  console.log(` Publishing ${name}@${manifest.version}`);
740
635
  console.log('═'.repeat(60));
741
636
 
742
- // ─── Human Lock Gate ──────────────────────────────────────────────
743
- const hl = checkHumanLock(abs);
744
- if (!hl.passed) {
745
- console.error('');
746
- console.error(' Human Lock Gate: BLOCKED');
747
- for (const issue of hl.issues) {
748
- console.error(` ✗ ${issue}`);
749
- }
750
- console.error('');
751
- console.error(' Use kdna publish --check for details, or --force to override.');
752
- if (!args.includes('--force')) {
753
- process.exit(EXIT.HUMAN_LOCK_REQUIRED);
754
- }
755
- console.warn(' ⚠ --force override: publishing without Human Lock (emergency only)');
756
- } else {
757
- console.log(` ✓ Human Lock Gate: passed`);
637
+ const provenanceIssues = validateAuthoringProvenance(manifest);
638
+ if (provenanceIssues.length) {
639
+ error(
640
+ `Authoring provenance gate failed:\n${provenanceIssues.map((issue) => ` - ${issue}`).join('\n')}`,
641
+ );
758
642
  }
759
- console.log('');
760
643
 
761
- console.log(` Identity fingerprint: ${fingerprint(publicKey)}`);
762
- console.log(` Scope trust key: ${scopeKey.slice(0, 28)}…`);
763
- console.log('');
764
-
765
- // 1. Update author.pubkey if missing/mismatch
766
- if (!manifest.author) manifest.author = {};
767
- if (manifest.author.pubkey && manifest.author.pubkey !== scopeKey) {
768
- error(
769
- `kdna.json.author.pubkey (${manifest.author.pubkey.slice(0, 20)}…) does not match your identity (${scopeKey.slice(0, 20)}…). Refusing to overwrite. Either remove the field, or use the matching identity.`,
644
+ const digest = assetDigest(abs);
645
+ const content = contentDigest(abs);
646
+ const size = fs.statSync(abs).size;
647
+ console.log(` ✓ Asset: ${abs} (${size} bytes)`);
648
+ console.log(` ✓ asset_digest: ${digest}`);
649
+ console.log(` ✓ content_digest: ${content}`);
650
+ if (manifest.authoring) {
651
+ console.log(
652
+ ` Authoring: ${manifest.authoring.created_by} / ${manifest.authoring.compiler || '?'}`,
770
653
  );
771
654
  }
772
- manifest.author.pubkey = scopeKey;
773
- // Embed full PEM so consumers can verify the signature against author.pubkey fingerprint
774
- manifest.author.public_key_pem = publicKey;
775
-
776
- // 2. Write signature
777
- delete manifest.signature;
778
- delete manifest.asset_digest;
779
- delete manifest.container_sha256;
780
- delete manifest.content_digest;
781
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
782
- manifest.content_digest = sourceContentDigest(abs);
783
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
784
- const signedPayload = canonicalPayload(abs);
785
- const sig = signPayload(signedPayload, privateKey);
786
- manifest.signature = 'ed25519:' + sig;
787
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
788
- console.log(` ✓ Signed (payload covers ${listPublishEntries(abs).length} content entries)`);
789
-
790
- // 3. Pack
791
- const fileName = `${m[2]}-${manifest.version}.kdna`;
792
- const outDir = outputDirFromArgs(args, path.join(abs, 'dist'));
793
- fs.mkdirSync(outDir, { recursive: true });
794
- const outPath = path.join(outDir, fileName);
795
- packToFile(abs, outPath);
796
- const sha256 = sha256File(outPath);
797
- const assetDigest = `sha256:${sha256}`;
798
- const size = fs.statSync(outPath).size;
799
- console.log(` ✓ Packed: ${outPath} (${size} bytes)`);
800
- console.log(` ✓ asset_digest: ${assetDigest}`);
801
655
 
802
656
  // 4. Optional upload via gh CLI
803
657
  const tagIdx = args.indexOf('--release-tag');
@@ -809,13 +663,13 @@ function cmdPublish(domainPath, args = []) {
809
663
  console.log('');
810
664
  console.log(` Uploading to ${repo} release ${tag}...`);
811
665
  try {
812
- execFileSync('gh', ['release', 'upload', tag, outPath, '--repo', repo, '--clobber'], {
666
+ execFileSync('gh', ['release', 'upload', tag, abs, '--repo', repo, '--clobber'], {
813
667
  stdio: 'inherit',
814
668
  });
815
- kdnaUrl = `https://github.com/${repo}/releases/download/${tag}/${fileName}`;
669
+ kdnaUrl = `https://github.com/${repo}/releases/download/${tag}/${path.basename(abs)}`;
816
670
  console.log(` ✓ Uploaded: ${kdnaUrl}`);
817
671
  } catch {
818
- console.warn(` ⚠ Upload failed. You can manually upload ${outPath}.`);
672
+ console.warn(` ⚠ Upload failed. You can manually upload ${abs}.`);
819
673
  }
820
674
  }
821
675
 
@@ -825,9 +679,10 @@ function cmdPublish(domainPath, args = []) {
825
679
  type: manifest.cluster ? 'cluster' : 'domain',
826
680
  version: manifest.version,
827
681
  asset_url: kdnaUrl,
828
- asset_digest: assetDigest,
829
- content_digest: manifest.content_digest || null,
682
+ asset_digest: digest,
683
+ content_digest: manifest.content_digest || content,
830
684
  signature: manifest.signature,
685
+ authoring: manifest.authoring || null,
831
686
  release_status: kdnaUrl ? 'published_signed' : 'published_signed_local',
832
687
  author: { ...manifest.author },
833
688
  };
@@ -843,10 +698,63 @@ function cmdPublish(domainPath, args = []) {
843
698
  );
844
699
  }
845
700
 
701
+ function validateAuthoringProvenance(manifest) {
702
+ const issues = [];
703
+ const badgeRank = {
704
+ untested: 0,
705
+ tested: 1,
706
+ validated: 2,
707
+ expert_reviewed: 3,
708
+ production_ready: 4,
709
+ };
710
+ const badge = manifest.quality_badge || 'untested';
711
+ const highTrust = (badgeRank[badge] || 0) >= badgeRank.tested;
712
+ const authoring = manifest.authoring;
713
+ const studioCompatible = new Set([
714
+ 'kdna-studio',
715
+ 'kdna-studio-cli',
716
+ 'kdna-studio-sdk',
717
+ 'third-party-studio-compatible',
718
+ ]);
719
+
720
+ if (!authoring) {
721
+ if (highTrust) issues.push(`quality_badge "${badge}" requires authoring provenance`);
722
+ return issues;
723
+ }
724
+ if (authoring.created_by === 'manual-dev-source' && highTrust) {
725
+ issues.push('manual-dev-source assets cannot claim tested or higher quality');
726
+ }
727
+ if (highTrust && !studioCompatible.has(authoring.created_by)) {
728
+ issues.push(`quality_badge "${badge}" requires Studio-compatible created_by`);
729
+ }
730
+ if (highTrust && !authoring.compiler) issues.push('trusted assets require authoring.compiler');
731
+ if (highTrust && !authoring.compiler_version) {
732
+ issues.push('trusted assets require authoring.compiler_version');
733
+ }
734
+ if (highTrust && !authoring.compiled_at)
735
+ issues.push('trusted assets require authoring.compiled_at');
736
+ for (const field of ['asset_uid', 'project_uid', 'build_id', 'domain_id', 'content_digest']) {
737
+ if (highTrust && !authoring[field] && !manifest[field]) {
738
+ issues.push(`trusted assets require ${field} in authoring provenance or manifest`);
739
+ }
740
+ }
741
+ if (highTrust && authoring.human_confirmed !== true) {
742
+ issues.push('trusted assets require authoring.human_confirmed = true');
743
+ }
744
+ if (highTrust && !Number.isInteger(authoring.human_lock_count)) {
745
+ issues.push('trusted assets require authoring.human_lock_count');
746
+ }
747
+ if (highTrust && Number.isInteger(authoring.human_lock_count) && authoring.human_lock_count < 1) {
748
+ issues.push('trusted assets require at least one Human Lock');
749
+ }
750
+ return issues;
751
+ }
752
+
846
753
  module.exports = {
847
754
  cmdPublishCheck,
848
755
  cmdPublish,
849
756
  checkHumanLock,
850
757
  canonicalPayload,
851
758
  publicKeyToScopeFormat,
759
+ validateAuthoringProvenance,
852
760
  };
package/src/registry.js CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+ const crypto = require('crypto');
15
16
  const { execFileSync } = require('child_process');
16
17
 
17
18
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
@@ -81,12 +82,75 @@ function registryTrustIssues(registry, { now = new Date() } = {}) {
81
82
  return issues;
82
83
  }
83
84
 
84
- function registryRevocations(registry) {
85
- return registry?.trust?.revocations || [];
85
+ function verifyRegistrySignature(registry, rawPayload) {
86
+ const trust = registry?.trust;
87
+ if (!trust) return { verified: false, error: 'No trust metadata in registry' };
88
+
89
+ const rootKeys = (trust.root?.keys || []).filter((k) => k.scheme === 'ed25519');
90
+ if (rootKeys.length === 0)
91
+ return { verified: false, error: 'No Ed25519 root keys in trust metadata' };
92
+
93
+ // Check if the registry has a signature file
94
+ const sigUrl = CANONICAL_REGISTRY_URL.replace(/\.json$/, '.sig');
95
+ let signature;
96
+ try {
97
+ const sigResult = execFileSync('curl', ['-sL', '--max-time', '10', sigUrl], {
98
+ encoding: 'utf8',
99
+ timeout: 15000,
100
+ });
101
+ signature = sigResult.trim();
102
+ } catch {
103
+ // .sig file may not exist yet (pre-signing transition)
104
+ return { verified: false, error: 'No registry signature file found' };
105
+ }
106
+
107
+ if (!rawPayload) {
108
+ try {
109
+ rawPayload = execFileSync('curl', ['-sL', '--max-time', '10', CANONICAL_REGISTRY_URL], {
110
+ encoding: 'utf8',
111
+ timeout: 15000,
112
+ });
113
+ } catch {
114
+ return { verified: false, error: 'Cannot fetch registry for verification' };
115
+ }
116
+ }
117
+
118
+ for (const key of rootKeys) {
119
+ try {
120
+ if (
121
+ crypto.verify(
122
+ null,
123
+ Buffer.from(rawPayload),
124
+ crypto.createPublicKey(key.pubkey),
125
+ Buffer.from(signature, 'hex'),
126
+ )
127
+ ) {
128
+ return { verified: true, keyid: key.keyid };
129
+ }
130
+ } catch {
131
+ /* try next key */
132
+ }
133
+ }
134
+
135
+ return { verified: false, error: 'Signature verification failed against all root keys' };
136
+ }
137
+
138
+ function checkRegistryRevocations(registry, scope) {
139
+ const revocations = registry?.trust?.revocations || [];
140
+ const scopeKey = scope?.trust_pubkey || '';
141
+ if (!scopeKey) return [];
142
+
143
+ const active = [];
144
+ for (const r of revocations) {
145
+ if (r.scope && r.scope !== scopeKey) continue;
146
+ if (r.expires_at && new Date(r.expires_at) < new Date()) continue;
147
+ active.push(r);
148
+ }
149
+ return active;
86
150
  }
87
151
 
88
152
  function isEntryRevoked(registry, entry) {
89
- const revocations = registryRevocations(registry);
153
+ const revocations = checkRegistryRevocations(registry, entry);
90
154
  return (
91
155
  revocations.find((rev) => {
92
156
  if (rev.name && rev.name !== entry.name) return false;
@@ -220,6 +284,12 @@ class RegistryResolver {
220
284
  `Registry trust check failed:\n${trustIssues.map((i) => `- ${i}`).join('\n')}`,
221
285
  );
222
286
  }
287
+ // Verify cryptographic signature
288
+ const sigResult = verifyRegistrySignature(data);
289
+ if (sigResult.error) {
290
+ // Non-fatal: allow operation but log warning (transitional)
291
+ console.error(`Warning: Registry signature verification: ${sigResult.error}`);
292
+ }
223
293
  this._registries.set(scopeName, data);
224
294
  return data;
225
295
  }
@@ -0,0 +1,39 @@
1
+ // Ed25519 signing and verification — registry, revocation, key management
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ function signPayload(payload, privateKeyPem) {
8
+ if (typeof payload === 'string') payload = Buffer.from(payload, 'utf8');
9
+ const sig = crypto.sign(null, payload, privateKeyPem);
10
+ return `ed25519:${sig.toString('hex')}`;
11
+ }
12
+
13
+ function verifyPayload(payload, signature, publicKeyPem) {
14
+ if (typeof payload === 'string') payload = Buffer.from(payload, 'utf8');
15
+ if (typeof signature === 'string' && signature.startsWith('ed25519:')) {
16
+ signature = Buffer.from(signature.slice(8), 'hex');
17
+ }
18
+ return crypto.verify(null, payload, publicKeyPem, signature);
19
+ }
20
+
21
+ function loadIdentityKeys() {
22
+ const identityDir = process.env.KDNA_IDENTITY_DIR || path.join(os.homedir(), '.kdna', 'identity');
23
+ const privateKeyPath = path.join(identityDir, 'kdna.key');
24
+ const publicKeyPath = path.join(identityDir, 'kdna.pub');
25
+ if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
26
+ throw new Error('No identity found. Run: kdna identity init --name "Your Name"');
27
+ }
28
+ return {
29
+ privatePem: fs.readFileSync(privateKeyPath, 'utf8'),
30
+ publicPem: fs.readFileSync(publicKeyPath, 'utf8'),
31
+ publicKeyPem: fs.readFileSync(publicKeyPath, 'utf8'),
32
+ };
33
+ }
34
+
35
+ function computeFingerprint(publicKeyPem) {
36
+ return `ed25519:${crypto.createHash('sha256').update(Buffer.from(publicKeyPem)).digest('hex').slice(0, 12)}`;
37
+ }
38
+
39
+ module.exports = { signPayload, verifyPayload, loadIdentityKeys, computeFingerprint };
package/src/verify.js CHANGED
@@ -491,6 +491,84 @@ function checkJudgment(input, options = {}) {
491
491
  issues.push({ severity: 'error', msg: 'kdna.json missing required field: judgment_version' });
492
492
  }
493
493
 
494
+ // 7. Authoring provenance gate for trusted quality claims.
495
+ const badgeRank = {
496
+ untested: 0,
497
+ tested: 1,
498
+ validated: 2,
499
+ expert_reviewed: 3,
500
+ production_ready: 4,
501
+ };
502
+ const badge = manifest?.quality_badge || 'untested';
503
+ const highTrust = (badgeRank[badge] || 0) >= badgeRank.tested;
504
+ const authoring = manifest?.authoring;
505
+ const studioCompatible = new Set([
506
+ 'kdna-studio',
507
+ 'kdna-studio-cli',
508
+ 'kdna-studio-sdk',
509
+ 'third-party-studio-compatible',
510
+ ]);
511
+ if (highTrust) {
512
+ if (!authoring) {
513
+ score.max += 2;
514
+ issues.push({
515
+ severity: 'error',
516
+ msg: `quality_badge ${badge} requires authoring provenance`,
517
+ });
518
+ } else {
519
+ const okSource = studioCompatible.has(authoring.created_by);
520
+ bump(1, okSource ? 1 : 0, `authoring.created_by: ${authoring.created_by || '?'}`);
521
+ if (!okSource) {
522
+ issues.push({
523
+ severity: 'error',
524
+ msg: 'trusted quality requires Studio-compatible authoring.created_by',
525
+ });
526
+ }
527
+ const hasCompiler = !!(
528
+ authoring.compiler &&
529
+ authoring.compiler_version &&
530
+ authoring.compiled_at
531
+ );
532
+ bump(1, hasCompiler ? 1 : 0, 'authoring compiler metadata present');
533
+ if (!hasCompiler) {
534
+ issues.push({
535
+ severity: 'error',
536
+ msg: 'trusted quality requires compiler, compiler_version, and compiled_at',
537
+ });
538
+ }
539
+ const hasIdentity = [
540
+ 'asset_uid',
541
+ 'project_uid',
542
+ 'build_id',
543
+ 'domain_id',
544
+ 'content_digest',
545
+ ].every((field) => !!(authoring[field] || manifest[field]));
546
+ bump(1, hasIdentity ? 1 : 0, 'authoring asset identity present');
547
+ if (!hasIdentity) {
548
+ issues.push({
549
+ severity: 'error',
550
+ msg: 'trusted quality requires asset_uid, project_uid, build_id, domain_id, and content_digest',
551
+ });
552
+ }
553
+ const humanConfirmed =
554
+ authoring.human_confirmed === true && Number(authoring.human_lock_count) > 0;
555
+ bump(1, humanConfirmed ? 1 : 0, `Human Lock provenance (${authoring.human_lock_count || 0})`);
556
+ if (!humanConfirmed) {
557
+ issues.push({
558
+ severity: 'error',
559
+ msg: 'trusted quality requires human_confirmed=true and human_lock_count > 0',
560
+ });
561
+ }
562
+ }
563
+ } else if (!authoring) {
564
+ issues.push({
565
+ severity: 'warn',
566
+ msg: 'authoring provenance missing; asset cannot be promoted above untested',
567
+ });
568
+ } else if (authoring.created_by === 'manual-dev-source') {
569
+ passed.push('authoring provenance: manual-dev-source (untested ceiling)');
570
+ }
571
+
494
572
  return { layer: 'judgment', issues, passed, score };
495
573
  }
496
574
 
@@ -29,4 +29,4 @@ This copies `templates/cluster/` and `templates/minimal-domain/` into a new dire
29
29
 
30
30
  ## Adding sub-domains
31
31
 
32
- Run `kdna init <cluster>/<sub_domain>` inside the cluster root (or copy `templates/minimal-domain/`).
32
+ Run `kdna dev scaffold <cluster>/<sub_domain>` inside the cluster root (or copy `templates/minimal-domain/`). This creates a non-canonical dev source workspace; trusted `.kdna` output still requires Studio-compatible compile/export.
@@ -45,7 +45,8 @@ kdna verify ./.
45
45
 
46
46
  # 7. Publish
47
47
  KDNA_IDENTITY_DIR=~/.kdna/identity-official \
48
- kdna publish ./. \
48
+ # Trusted publish starts from a Studio-compiled .kdna asset.
49
+ kdna publish ./dist/your-domain.kdna \
49
50
  --release-tag v0.1.0 \
50
51
  --repo yourname/kdna-<your_domain_id>
51
52
 
@@ -18,7 +18,7 @@
18
18
  "author": {
19
19
  "name": "Your Name",
20
20
  "id": "your-id",
21
- "pubkey": "[set automatically by kdna publish — must match your scope trust_pubkey]"
21
+ "pubkey": "[set by Studio export/signing — must match your scope trust_pubkey]"
22
22
  },
23
23
  "license": {
24
24
  "type": "CC-BY-4.0",