@aikdna/kdna-cli 0.18.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/src/publish.js CHANGED
@@ -53,17 +53,17 @@ const SLOGAN_PATTERNS = [
53
53
  // PRIORITIZE or AVOID, not steps to follow.
54
54
 
55
55
  const SOP_PATTERNS = [
56
- /^Step\s+\d/i, // "Step 1: identify the topic"
56
+ /^Step\s+\d/i, // "Step 1: identify the topic"
57
57
  /^First,?\s|^Next,?\s|^Then,?\s|^Finally,?\s/i, // "First, do X. Then do Y."
58
- /^Check\s(for|if|whether)\s/i, // "Check for spelling errors"
58
+ /^Check\s(for|if|whether)\s/i, // "Check for spelling errors"
59
59
  /^Always\s+(use|do|make|include)/i, // "Always use active voice"
60
- /^Never\s+(use|do|make)/i, // "Never use passive voice"
61
- /^Generate\s/i, // "Generate three options"
62
- /^Create\s+(a|the)\s/i, // "Create a list of..."
63
- /^Make\s+sure\s/i, // "Make sure to check..."
64
- /^Remember\s+to\s/i, // "Remember to validate..."
60
+ /^Never\s+(use|do|make)/i, // "Never use passive voice"
61
+ /^Generate\s/i, // "Generate three options"
62
+ /^Create\s+(a|the)\s/i, // "Create a list of..."
63
+ /^Make\s+sure\s/i, // "Make sure to check..."
64
+ /^Remember\s+to\s/i, // "Remember to validate..."
65
65
  /^(You|The agent)\s+should\s+(use|do|make|include)/i, // "You should use X"
66
- /^Avoid\s+(using|doing)/i, // "Avoid using X" (too procedural)
66
+ /^Avoid\s+(using|doing)/i, // "Avoid using X" (too procedural)
67
67
  ];
68
68
 
69
69
  function isSOP(text) {
@@ -188,13 +188,19 @@ function checkHumanLock(domainPath) {
188
188
  // Rule 3: Lock must confirm judgment fields were reviewed
189
189
  const checked = card.human_lock.checked || {};
190
190
  if (!checked.applies_when) {
191
- issues.push(`${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`);
191
+ issues.push(
192
+ `${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`,
193
+ );
192
194
  }
193
195
  if (!checked.does_not_apply_when) {
194
- issues.push(`${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`);
196
+ issues.push(
197
+ `${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`,
198
+ );
195
199
  }
196
200
  if (!checked.failure_risk) {
197
- issues.push(`${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`);
201
+ issues.push(
202
+ `${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`,
203
+ );
198
204
  }
199
205
  }
200
206
 
@@ -527,29 +533,24 @@ function identityPaths() {
527
533
  }
528
534
 
529
535
  /**
530
- * Canonical signing payload: sorted (filename, sha256) pairs of all .json files
531
- * inside the .kdna ZIP, joined as `name:hex\n`. This is what the signature covers.
536
+ * Canonical signing payload: sorted (filename, sha256) pairs of all published
537
+ * content entries inside the .kdna ZIP, joined as `name:hex\n`.
532
538
  *
533
539
  * Excludes the `signature` field from kdna.json itself (computed by removing it
534
540
  * before hashing). Digest self-reference fields are also excluded. All other files included as-is.
535
541
  */
536
542
  function canonicalPayload(srcDir, opts = {}) {
537
- const files = fs
538
- .readdirSync(srcDir)
539
- .filter((f) => f.endsWith('.json'))
540
- .sort();
543
+ const files = listPublishEntries(srcDir);
541
544
  const parts = [];
542
545
  for (const f of files) {
543
- const full = path.join(srcDir, f);
546
+ const full = f === 'mimetype' ? null : path.join(srcDir, f);
544
547
  let buf;
545
- if (f === 'kdna.json') {
548
+ if (f === 'mimetype') {
549
+ buf = Buffer.from('application/vnd.aikdna.kdna+zip');
550
+ } else if (f.endsWith('.json')) {
546
551
  const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
547
- delete obj.signature;
548
- delete obj.asset_digest;
549
- delete obj.container_sha256;
550
- if (!opts.includeContentDigest) delete obj.content_digest;
551
- delete obj._source;
552
- buf = Buffer.from(JSON.stringify(obj));
552
+ const value = f === 'kdna.json' ? manifestForSigning(obj, opts) : obj;
553
+ buf = Buffer.from(stableStringify(value));
553
554
  } else {
554
555
  buf = fs.readFileSync(full);
555
556
  }
@@ -559,6 +560,16 @@ function canonicalPayload(srcDir, opts = {}) {
559
560
  return parts.join('\n');
560
561
  }
561
562
 
563
+ function manifestForSigning(manifest, opts = {}) {
564
+ const copy = { ...(manifest || {}) };
565
+ delete copy.signature;
566
+ delete copy.asset_digest;
567
+ delete copy.container_sha256;
568
+ if (!opts.includeContentDigest) delete copy.content_digest;
569
+ delete copy._source;
570
+ return copy;
571
+ }
572
+
562
573
  function stableStringify(value) {
563
574
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
564
575
  if (value && typeof value === 'object') {
@@ -581,22 +592,55 @@ function manifestForContentDigest(manifest) {
581
592
  }
582
593
 
583
594
  function sourceContentDigest(srcDir) {
584
- const files = fs
585
- .readdirSync(srcDir)
586
- .filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE')
587
- .sort();
595
+ const files = listPublishEntries(srcDir);
588
596
  const parts = [];
589
597
  for (const f of files) {
590
598
  let buf;
591
- if (f === 'kdna.json') {
599
+ if (f === 'mimetype') {
600
+ buf = Buffer.from('application/vnd.aikdna.kdna+zip');
601
+ } else if (f.endsWith('.json')) {
592
602
  const obj = JSON.parse(fs.readFileSync(path.join(srcDir, f), 'utf8'));
593
- buf = Buffer.from(stableStringify(manifestForContentDigest(obj)));
603
+ const value = f === 'kdna.json' ? manifestForContentDigest(obj) : obj;
604
+ buf = Buffer.from(stableStringify(value));
594
605
  } else {
595
606
  buf = fs.readFileSync(path.join(srcDir, f));
596
607
  }
597
608
  parts.push(`${f}:${crypto.createHash('sha256').update(buf).digest('hex')}`);
598
609
  }
599
- return `sha256:${crypto.createHash('sha256').update(Buffer.from(parts.join('\n'))).digest('hex')}`;
610
+ return `sha256:${crypto
611
+ .createHash('sha256')
612
+ .update(Buffer.from(parts.join('\n')))
613
+ .digest('hex')}`;
614
+ }
615
+
616
+ function listPublishEntries(domainDir) {
617
+ const entries = ['mimetype'];
618
+ const skipDirs = new Set(['.git', 'node_modules', 'dist']);
619
+ function walk(dir, prefix = '') {
620
+ for (const name of fs.readdirSync(dir).sort()) {
621
+ if (name === 'mimetype') continue;
622
+ if (name === '.DS_Store' || name === 'signature.json') continue;
623
+ const abs = path.join(dir, name);
624
+ const rel = prefix ? `${prefix}/${name}` : name;
625
+ const stat = fs.statSync(abs);
626
+ if (stat.isDirectory()) {
627
+ if (!skipDirs.has(name)) walk(abs, rel);
628
+ continue;
629
+ }
630
+ if (
631
+ rel.endsWith('.json') ||
632
+ rel === 'README.md' ||
633
+ rel === 'LICENSE' ||
634
+ rel.startsWith('evals/') ||
635
+ rel.startsWith('examples/') ||
636
+ rel.startsWith('reports/')
637
+ ) {
638
+ entries.push(rel);
639
+ }
640
+ }
641
+ }
642
+ walk(domainDir);
643
+ return entries;
600
644
  }
601
645
 
602
646
  function signPayload(payload, privateKeyPem) {
@@ -623,17 +667,17 @@ function publicKeyToScopeFormat(publicKeyPem) {
623
667
  }
624
668
 
625
669
  function packToFile(domainDir, outPath) {
626
- const files = fs
627
- .readdirSync(domainDir)
628
- .filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE');
629
- if (!files.includes('kdna.json')) error('kdna.json required in dev source directory for publish.');
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.');
630
673
 
631
674
  const script = `import zipfile, os
632
675
  src = ${JSON.stringify(domainDir)}
633
676
  out = ${JSON.stringify(outPath)}
634
677
  files = ${JSON.stringify(files)}
635
678
  with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
636
- for f in sorted(files):
679
+ zf.writestr(zipfile.ZipInfo('mimetype'), 'application/vnd.aikdna.kdna+zip', compress_type=zipfile.ZIP_STORED)
680
+ for f in files:
637
681
  zf.write(os.path.join(src, f), f)
638
682
  `;
639
683
  const tmpPy = `/tmp/kdna-publish-pack-${Date.now()}.py`;
@@ -729,13 +773,11 @@ function cmdPublish(domainPath, args = []) {
729
773
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
730
774
  manifest.content_digest = sourceContentDigest(abs);
731
775
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
732
- const signedPayload = canonicalPayload(abs, { includeContentDigest: true });
776
+ const signedPayload = canonicalPayload(abs);
733
777
  const sig = signPayload(signedPayload, privateKey);
734
778
  manifest.signature = 'ed25519:' + sig;
735
779
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
736
- console.log(
737
- ` ✓ Signed (payload covers ${fs.readdirSync(abs).filter((f) => f.endsWith('.json')).length} json files)`,
738
- );
780
+ console.log(` ✓ Signed (payload covers ${listPublishEntries(abs).length} content entries)`);
739
781
 
740
782
  // 3. Pack
741
783
  const fileName = `${m[2]}-${manifest.version}.kdna`;
@@ -795,4 +837,10 @@ function cmdPublish(domainPath, args = []) {
795
837
  );
796
838
  }
797
839
 
798
- module.exports = { cmdPublishCheck, cmdPublish, checkHumanLock, canonicalPayload, publicKeyToScopeFormat };
840
+ module.exports = {
841
+ cmdPublishCheck,
842
+ cmdPublish,
843
+ checkHumanLock,
844
+ canonicalPayload,
845
+ publicKeyToScopeFormat,
846
+ };
package/src/registry.js CHANGED
@@ -69,7 +69,8 @@ function registryTrustIssues(registry, { now = new Date() } = {}) {
69
69
  const snapshotExpires = parseDate(trust.snapshot?.expires_at);
70
70
  const timestampExpires = parseDate(trust.timestamp?.expires_at);
71
71
  if (!snapshotExpires) issues.push('registry.trust.snapshot.expires_at must be an ISO timestamp');
72
- if (!timestampExpires) issues.push('registry.trust.timestamp.expires_at must be an ISO timestamp');
72
+ if (!timestampExpires)
73
+ issues.push('registry.trust.timestamp.expires_at must be an ISO timestamp');
73
74
  if (snapshotExpires && snapshotExpires <= now) {
74
75
  issues.push(`registry snapshot expired at ${trust.snapshot.expires_at}`);
75
76
  }
@@ -86,12 +87,14 @@ function registryRevocations(registry) {
86
87
 
87
88
  function isEntryRevoked(registry, entry) {
88
89
  const revocations = registryRevocations(registry);
89
- return revocations.find((rev) => {
90
- if (rev.name && rev.name !== entry.name) return false;
91
- if (rev.version && rev.version !== entry.version) return false;
92
- if (rev.asset_digest && rev.asset_digest !== entry.asset_digest) return false;
93
- return rev.name || rev.asset_digest;
94
- }) || null;
90
+ return (
91
+ revocations.find((rev) => {
92
+ if (rev.name && rev.name !== entry.name) return false;
93
+ if (rev.version && rev.version !== entry.version) return false;
94
+ if (rev.asset_digest && rev.asset_digest !== entry.asset_digest) return false;
95
+ return rev.name || rev.asset_digest;
96
+ }) || null
97
+ );
95
98
  }
96
99
 
97
100
  // ─── Name parsing ───────────────────────────────────────────────────────
@@ -213,7 +216,9 @@ class RegistryResolver {
213
216
  trustIssues = data ? registryTrustIssues(data) : [];
214
217
  }
215
218
  if (trustIssues.length) {
216
- throw new Error(`Registry trust check failed:\n${trustIssues.map((i) => `- ${i}`).join('\n')}`);
219
+ throw new Error(
220
+ `Registry trust check failed:\n${trustIssues.map((i) => `- ${i}`).join('\n')}`,
221
+ );
217
222
  }
218
223
  this._registries.set(scopeName, data);
219
224
  return data;
package/src/verify.js CHANGED
@@ -21,13 +21,6 @@ const { RegistryResolver, parseName, registryTrustIssues, isEntryRevoked } = req
21
21
  const { EXIT, isYesNoSelfCheck } = require('./cmds/_common');
22
22
  const { licenseDecryptOptionsForManifest } = require('./cmds/license');
23
23
 
24
- let validateManifestFn;
25
- try {
26
- validateManifestFn = require('@aikdna/kdna-core').validateManifest;
27
- } catch {
28
- // kdna-core not available — manifest validation skipped
29
- }
30
-
31
24
  const {
32
25
  getInstalled,
33
26
  listContainerEntries,
@@ -37,6 +30,50 @@ const {
37
30
  verifyAsset,
38
31
  } = require('./package-store');
39
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
+ ];
52
+
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
+ }
76
+
40
77
  function readJson(p) {
41
78
  try {
42
79
  return JSON.parse(fs.readFileSync(p, 'utf8'));
@@ -672,15 +709,21 @@ function cmdVerify(input, args = []) {
672
709
  }
673
710
  const { checkTrust: agentCheckTrust } = require('./agent');
674
711
  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));
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
+ );
684
727
  process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
685
728
  }
686
729
 
@@ -743,7 +786,8 @@ function cmdVerify(input, args = []) {
743
786
  ? manifest.encryption.encrypted_entries
744
787
  : [];
745
788
  const requiresProtectedRead =
746
- encryptedEntries.length > 0 && (want.structure || want.judgment || want.i18n || want.governance);
789
+ encryptedEntries.length > 0 &&
790
+ (want.structure || want.judgment || want.i18n || want.governance);
747
791
  if (requiresProtectedRead) {
748
792
  const licensed = licenseDecryptOptionsForManifest(manifest);
749
793
  if (licensed.ok) {
package/src/version.js CHANGED
@@ -147,12 +147,18 @@ function cmdVersionSuggest(domainPath = '.', args = []) {
147
147
  const changes = detectChanges(abs);
148
148
 
149
149
  if (jsonMode) {
150
- console.log(JSON.stringify({
151
- current_version: currentVersion,
152
- suggested_bump: changes.suggestion,
153
- reasons: changes.reasons,
154
- change_summary: changes.summary,
155
- }, null, 2));
150
+ console.log(
151
+ JSON.stringify(
152
+ {
153
+ current_version: currentVersion,
154
+ suggested_bump: changes.suggestion,
155
+ reasons: changes.reasons,
156
+ change_summary: changes.summary,
157
+ },
158
+ null,
159
+ 2,
160
+ ),
161
+ );
156
162
  return;
157
163
  }
158
164
 
@@ -194,7 +200,9 @@ function detectChanges(domainPath) {
194
200
  // Count axioms with applies_when (v2.1 governance) vs without
195
201
  if (core?.axioms) {
196
202
  const total = core.axioms.length;
197
- const governed = core.axioms.filter((a) => a.applies_when?.length && a.does_not_apply_when?.length).length;
203
+ const governed = core.axioms.filter(
204
+ (a) => a.applies_when?.length && a.does_not_apply_when?.length,
205
+ ).length;
198
206
  if (governed < total) {
199
207
  axiomChanges = total - governed;
200
208
  reasons.push(`${axiomChanges} axioms missing v2.1 governance fields`);
@@ -1,9 +1,12 @@
1
1
  {
2
- "kdna_spec": "1.0-rc",
2
+ "format": "kdna",
3
+ "format_version": "1.0",
4
+ "spec_version": "1.0-rc",
3
5
  "name": "@aikdna/example_domain",
4
6
  "version": "0.1.0",
5
- "language": "en",
7
+ "judgment_version": "0.1.0",
6
8
  "languages": ["en"],
9
+ "default_language": "en",
7
10
  "created": "YYYY-MM-DD",
8
11
  "updated": "YYYY-MM-DD",
9
12
  "description": "[TODO: describe what judgment this domain improves in one sentence]",
@@ -11,6 +14,7 @@
11
14
  "keywords": ["[TODO: add relevant keywords]"],
12
15
  "access": "open",
13
16
  "status": "experimental",
17
+ "quality_badge": "untested",
14
18
  "author": {
15
19
  "name": "Your Name",
16
20
  "id": "your-id"
@@ -22,10 +26,6 @@
22
26
  "allow_redistribution": true,
23
27
  "allow_training": true
24
28
  },
25
- "registry": {
26
- "repo": "https://github.com/your-org/your-repo"
27
- },
28
29
  "file_count": 3,
29
- "kdna_file_count": 3,
30
- "files": ["KDNA_Core.json", "KDNA_Patterns.json", "tests/before-after.json"]
30
+ "risk_level": "R0"
31
31
  }
@@ -54,20 +54,20 @@ If any of the above is true, the agent should decline to load this domain.
54
54
 
55
55
  ## Known Failure Risks
56
56
 
57
- | Risk | When it shows up |
58
- |------|---|
59
- | [risk 1 from axiom_one.failure_risk] | [trigger] |
60
- | [risk 2 from axiom_two.failure_risk] | [trigger] |
61
- | [risk 3 from misread_one.failure_risk] | [trigger] |
57
+ | Risk | When it shows up |
58
+ | -------------------------------------- | ---------------- |
59
+ | [risk 1 from axiom_one.failure_risk] | [trigger] |
60
+ | [risk 2 from axiom_two.failure_risk] | [trigger] |
61
+ | [risk 3 from misread_one.failure_risk] | [trigger] |
62
62
 
63
63
  ## Files
64
64
 
65
- | File | Purpose |
66
- |------|---------|
67
- | `KDNA_Core.json` | Axioms (with v2.1 boundaries), ontology, frameworks, causal structure, stances |
65
+ | File | Purpose |
66
+ | -------------------- | -------------------------------------------------------------------------------- |
67
+ | `KDNA_Core.json` | Axioms (with v2.1 boundaries), ontology, frameworks, causal structure, stances |
68
68
  | `KDNA_Patterns.json` | Terminology, banned terms, misunderstandings (with v2.1 boundaries), self-checks |
69
- | `evals/` | Test cases for `kdna compare` and quality scoring |
70
- | `kdna.json` | Domain manifest (name, version, judgment_version, signature) |
69
+ | `evals/` | Test cases for `kdna compare` and quality scoring |
70
+ | `kdna.json` | Domain manifest (name, version, judgment_version, signature) |
71
71
 
72
72
  ## License
73
73
 
@@ -1,11 +1,14 @@
1
1
  {
2
- "kdna_spec": "1.0-rc",
2
+ "format": "kdna",
3
+ "format_version": "1.0",
4
+ "spec_version": "1.0-rc",
3
5
  "name": "@yourscope/your_domain_id",
4
6
  "version": "0.1.0",
5
7
  "judgment_version": "YYYY.MM",
6
8
  "status": "experimental",
7
9
  "access": "open",
8
- "language": "en",
10
+ "languages": ["en"],
11
+ "default_language": "en",
9
12
  "created": "YYYY-MM-DD",
10
13
  "updated": "YYYY-MM-DD",
11
14
  "description": "[one-sentence description; appears on registry and in install output]",
@@ -25,5 +28,5 @@
25
28
  "allow_training": true
26
29
  },
27
30
  "file_count": 2,
28
- "files": ["KDNA_Core.json", "KDNA_Patterns.json"]
31
+ "risk_level": "R0"
29
32
  }
@@ -51,23 +51,57 @@ for (const f of files) {
51
51
 
52
52
  const result = lintDomain(dataMap);
53
53
 
54
+ function validateManifest(manifest) {
55
+ const errors = [];
56
+ const warnings = [];
57
+ const required = [
58
+ 'format',
59
+ 'format_version',
60
+ 'spec_version',
61
+ 'name',
62
+ 'version',
63
+ 'judgment_version',
64
+ 'description',
65
+ 'author',
66
+ 'license',
67
+ 'status',
68
+ 'quality_badge',
69
+ 'access',
70
+ 'languages',
71
+ 'default_language',
72
+ ];
73
+
74
+ if (manifest.kdna_spec) errors.push('kdna_spec is not allowed. Use spec_version.');
75
+ if (manifest.language)
76
+ errors.push('language is not allowed. Use default_language and languages.');
77
+ for (const field of required) {
78
+ if (!(field in manifest) || manifest[field] === undefined || manifest[field] === '') {
79
+ errors.push(`missing required field "${field}"`);
80
+ }
81
+ }
82
+ if (manifest.format && manifest.format !== 'kdna') {
83
+ errors.push(`format: invalid value "${manifest.format}". Expected "kdna".`);
84
+ }
85
+ if (manifest.format_version && manifest.format_version !== '1.0') {
86
+ errors.push(`format_version: invalid value "${manifest.format_version}". Expected "1.0".`);
87
+ }
88
+ if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
89
+ warnings.push(
90
+ `spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
91
+ );
92
+ }
93
+ return { errors, warnings };
94
+ }
95
+
54
96
  // Also validate kdna.json manifest if present and validateManifest is available
55
97
  let manifestPath;
56
98
  try {
57
99
  manifestPath = path.join(domainDir, 'kdna.json');
58
100
  if (fs.existsSync(manifestPath)) {
59
101
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
60
- let validateManifestFn;
61
- try {
62
- validateManifestFn = require('@aikdna/kdna-core').validateManifest;
63
- } catch {
64
- // validateManifest not yet available in installed kdna-core — skip manifest check
65
- }
66
- if (validateManifestFn) {
67
- const mResult = validateManifestFn(manifest);
68
- for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
69
- for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
70
- }
102
+ const mResult = validateManifest(manifest);
103
+ for (const e of mResult.errors) result.errors.push(`kdna.json: ${e}`);
104
+ for (const w of mResult.warnings) result.warnings.push(`kdna.json: ${w}`);
71
105
  }
72
106
  } catch (e) {
73
107
  if (e.code !== 'ENOENT') {