@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/src/loader.js CHANGED
@@ -15,8 +15,8 @@ const FILE_MAP = core.FILE_MAP;
15
15
  * Read and parse a KDNA JSON file.
16
16
  * Returns null if the file does not exist.
17
17
  */
18
- function readFile(domainDir, filename) {
19
- const filePath = path.join(domainDir, filename);
18
+ function readFile(sourceDir, filename) {
19
+ const filePath = path.join(sourceDir, filename);
20
20
  if (!fs.existsSync(filePath)) return null;
21
21
  try {
22
22
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -29,32 +29,32 @@ function readFile(domainDir, filename) {
29
29
  * Load the minimum required KDNA files (Core + Patterns).
30
30
  * Always load these. They form the cognition baseline.
31
31
  */
32
- function loadCorePatterns(domainDir) {
33
- const coreData = readFile(domainDir, FILE_MAP.core);
34
- const patternsData = readFile(domainDir, FILE_MAP.patterns);
32
+ function loadCorePatterns(sourceDir) {
33
+ const coreData = readFile(sourceDir, FILE_MAP.core);
34
+ const patternsData = readFile(sourceDir, FILE_MAP.patterns);
35
35
  return core.loadCorePatternsFromData(coreData, patternsData);
36
36
  }
37
37
 
38
38
  /**
39
39
  * Load a complete KDNA domain.
40
40
  *
41
- * @param {string} domainDir — path to the domain folder
41
+ * @param {string} sourceDir — path to a dev source directory
42
42
  * @param {object} [options]
43
43
  * @param {string} [options.input] — user input text for conditional loading
44
44
  * @param {'all'|'minimum'|'auto'} [options.mode='auto'] — loading mode
45
45
  * @returns {object|null} loaded KDNA files keyed by type, or null if minimum files are missing
46
46
  */
47
- function loadDomain(domainDir, options = {}) {
47
+ function loadDomain(sourceDir, options = {}) {
48
48
  const dataMap = { core: null, patterns: null };
49
49
 
50
- dataMap.core = readFile(domainDir, FILE_MAP.core);
51
- dataMap.patterns = readFile(domainDir, FILE_MAP.patterns);
50
+ dataMap.core = readFile(sourceDir, FILE_MAP.core);
51
+ dataMap.patterns = readFile(sourceDir, FILE_MAP.patterns);
52
52
 
53
53
  if (!dataMap.core || !dataMap.patterns) return null;
54
54
 
55
55
  // Also read optional files that might be present
56
56
  for (const key of ['scenarios', 'cases', 'reasoning', 'evolution']) {
57
- const data = readFile(domainDir, FILE_MAP[key]);
57
+ const data = readFile(sourceDir, FILE_MAP[key]);
58
58
  if (data) dataMap[key] = data;
59
59
  }
60
60
 
@@ -0,0 +1,232 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const PATHS = require('./paths');
5
+ const { parseName } = require('./registry');
6
+ const core = require('@aikdna/kdna-core');
7
+
8
+ if (typeof core.createKdnaAssetReader !== 'function') {
9
+ throw new Error('@aikdna/kdna-core >=0.5.0 is required for direct .kdna asset loading');
10
+ }
11
+
12
+ const assetReader = core.createKdnaAssetReader();
13
+
14
+ const INDEX_VERSION = 2;
15
+
16
+ function ensureDir(dir) {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ }
19
+
20
+ function readJsonFile(file) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function writeJsonFile(file, data) {
29
+ ensureDir(path.dirname(file));
30
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
31
+ }
32
+
33
+ function readIndex() {
34
+ const data = readJsonFile(PATHS.packageIndex);
35
+ if (data?.packages && typeof data.packages === 'object') return data;
36
+ return { version: INDEX_VERSION, packages: {} };
37
+ }
38
+
39
+ function writeIndex(index) {
40
+ index.version = INDEX_VERSION;
41
+ writeJsonFile(PATHS.packageIndex, index);
42
+ }
43
+
44
+ function sha256File(filePath) {
45
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
46
+ }
47
+
48
+ function assetDigest(filePath) {
49
+ return `sha256:${sha256File(filePath)}`;
50
+ }
51
+
52
+ function contentDigest(kdnaPath) {
53
+ return assetReader.contentDigestSync(assetReader.openSync(kdnaPath));
54
+ }
55
+
56
+ function assetDir(scope, ident, version) {
57
+ return path.join(PATHS.packages, scope, ident, version || 'unknown');
58
+ }
59
+
60
+ function assetFileName(ident, version) {
61
+ return `${ident}-${version || 'unknown'}.kdna`;
62
+ }
63
+
64
+ function readContainerJson(kdnaPath, fileName, options = {}) {
65
+ const asset = assetReader.openSync(kdnaPath);
66
+ return assetReader.readJsonSync(asset, fileName, options);
67
+ }
68
+
69
+ function readContainerEntry(kdnaPath, fileName) {
70
+ const asset = assetReader.openSync(kdnaPath);
71
+ return assetReader.readEntrySync(asset, fileName);
72
+ }
73
+
74
+ function listContainerEntries(kdnaPath) {
75
+ const asset = assetReader.openSync(kdnaPath);
76
+ return assetReader.listEntriesSync(asset);
77
+ }
78
+
79
+ function readContainer(kdnaPath, options = {}) {
80
+ const asset = assetReader.openSync(kdnaPath);
81
+ return {
82
+ manifest: assetReader.readJsonSync(asset, 'kdna.json'),
83
+ core: assetReader.readJsonSync(asset, 'KDNA_Core.json', options),
84
+ patterns: assetReader.readJsonSync(asset, 'KDNA_Patterns.json', options),
85
+ scenarios: assetReader.readJsonSync(asset, 'KDNA_Scenarios.json', options),
86
+ cases: assetReader.readJsonSync(asset, 'KDNA_Cases.json', options),
87
+ reasoning: assetReader.readJsonSync(asset, 'KDNA_Reasoning.json', options),
88
+ evolution: assetReader.readJsonSync(asset, 'KDNA_Evolution.json', options),
89
+ files: assetReader.listEntriesSync(asset),
90
+ };
91
+ }
92
+
93
+ function verifyAsset(kdnaPath, options = {}) {
94
+ const asset = assetReader.openSync(kdnaPath);
95
+ return assetReader.verifySync(asset, options);
96
+ }
97
+
98
+ function getInstalled(input) {
99
+ const parsed = parseName(input);
100
+ if (!parsed) return null;
101
+ const index = readIndex();
102
+ const entry = index.packages[parsed.full];
103
+ if (!entry?.asset_path || !fs.existsSync(entry.asset_path)) return null;
104
+ return { ...entry, parsed };
105
+ }
106
+
107
+ function listInstalled() {
108
+ const index = readIndex();
109
+ return Object.entries(index.packages)
110
+ .map(([full, entry]) => ({ full, ...entry }))
111
+ .filter((entry) => entry.asset_path && fs.existsSync(entry.asset_path))
112
+ .sort((a, b) => a.full.localeCompare(b.full));
113
+ }
114
+
115
+ function readAssetManifest(assetPath) {
116
+ return readContainerJson(assetPath, 'kdna.json') || {};
117
+ }
118
+
119
+ function receiptPathForAsset(assetPath) {
120
+ return path.join(path.dirname(assetPath), 'receipt.json');
121
+ }
122
+
123
+ function installAsset({ sourcePath, name, version, source = {} }) {
124
+ const parsed = parseName(name);
125
+ if (!parsed) throw new Error(`Invalid scoped domain name: ${name}`);
126
+ const finalVersion = version || 'unknown';
127
+ const destDir = assetDir(parsed.scope, parsed.ident, finalVersion);
128
+ const dest = path.join(destDir, assetFileName(parsed.ident, finalVersion));
129
+ ensureDir(destDir);
130
+ fs.copyFileSync(sourcePath, dest);
131
+
132
+ const manifest = readAssetManifest(dest);
133
+ const installedAt = new Date().toISOString();
134
+ const computedAssetDigest = assetDigest(dest);
135
+ const computedContentDigest = contentDigest(dest);
136
+ const receiptPath = receiptPathForAsset(dest);
137
+ const receipt = {
138
+ version: 1,
139
+ name: parsed.full,
140
+ asset_path: dest,
141
+ asset_digest: computedAssetDigest,
142
+ content_digest: computedContentDigest,
143
+ package_version: finalVersion,
144
+ judgment_version: manifest.judgment_version || null,
145
+ access: manifest.access || 'open',
146
+ signature: manifest.signature || null,
147
+ installed_at: installedAt,
148
+ source,
149
+ };
150
+ writeJsonFile(receiptPath, receipt);
151
+
152
+ const index = readIndex();
153
+ index.packages[parsed.full] = {
154
+ name: parsed.full,
155
+ version: finalVersion,
156
+ asset_path: dest,
157
+ receipt_path: receiptPath,
158
+ asset_digest: computedAssetDigest,
159
+ content_digest: computedContentDigest,
160
+ judgment_version: manifest.judgment_version || null,
161
+ access: manifest.access || 'open',
162
+ signature: manifest.signature || null,
163
+ installed_at: installedAt,
164
+ source,
165
+ };
166
+ writeIndex(index);
167
+ return index.packages[parsed.full];
168
+ }
169
+
170
+ function resolveAsset(input) {
171
+ const expanded = input.replace(/^~/, process.env.HOME || '');
172
+ const looksLikeFile =
173
+ input.endsWith('.kdna') &&
174
+ (input.startsWith('./') ||
175
+ input.startsWith('/') ||
176
+ input.startsWith('~/') ||
177
+ fs.existsSync(expanded));
178
+ if (looksLikeFile) {
179
+ const abs = path.resolve(expanded);
180
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
181
+ const manifest = readAssetManifest(abs);
182
+ const full = manifest.name;
183
+ const parsed = full ? parseName(full) : null;
184
+ return {
185
+ name: full || path.basename(abs, '.kdna'),
186
+ parsed,
187
+ asset_path: abs,
188
+ receipt_path: null,
189
+ version: manifest.version || null,
190
+ judgment_version: manifest.judgment_version || null,
191
+ access: manifest.access || 'open',
192
+ asset_digest: assetDigest(abs),
193
+ content_digest: contentDigest(abs),
194
+ source: { type: 'local-file', path: abs },
195
+ local_file: true,
196
+ };
197
+ }
198
+ return getInstalled(input);
199
+ }
200
+
201
+ function removeInstalled(input) {
202
+ const parsed = parseName(input);
203
+ if (!parsed) return false;
204
+ const index = readIndex();
205
+ const entry = index.packages[parsed.full];
206
+ if (!entry) return false;
207
+ delete index.packages[parsed.full];
208
+ writeIndex(index);
209
+ if (entry.asset_path) {
210
+ const versionDir = path.dirname(entry.asset_path);
211
+ fs.rmSync(versionDir, { recursive: true, force: true });
212
+ }
213
+ return true;
214
+ }
215
+
216
+ module.exports = {
217
+ readIndex,
218
+ writeIndex,
219
+ sha256File,
220
+ assetDigest,
221
+ contentDigest,
222
+ readContainer,
223
+ readContainerEntry,
224
+ readContainerJson,
225
+ listContainerEntries,
226
+ verifyAsset,
227
+ getInstalled,
228
+ listInstalled,
229
+ installAsset,
230
+ resolveAsset,
231
+ removeInstalled,
232
+ };
package/src/paths.js ADDED
@@ -0,0 +1,44 @@
1
+ // KDNA shared path configuration — canonical source for ~/.kdna structure
2
+ // Spec: docs/local-kdna-home-spec.md
3
+
4
+ const path = require('path');
5
+
6
+ const KDNA_HOME =
7
+ process.env.KDNA_HOME || path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
8
+
9
+ const PATHS = {
10
+ root: KDNA_HOME,
11
+ config: path.join(KDNA_HOME, 'config.json'),
12
+ identity: path.join(KDNA_HOME, 'identity'),
13
+ domains: {
14
+ root: path.join(KDNA_HOME, 'domains'),
15
+ official: path.join(KDNA_HOME, 'domains', 'official'),
16
+ local: path.join(KDNA_HOME, 'domains', 'local'),
17
+ private: path.join(KDNA_HOME, 'domains', 'private'),
18
+ // Legacy flat path — used for migration only
19
+ legacy: path.join(KDNA_HOME, 'domains'),
20
+ // All three directories for scanning
21
+ all: [
22
+ path.join(KDNA_HOME, 'domains', 'official'),
23
+ path.join(KDNA_HOME, 'domains', 'local'),
24
+ path.join(KDNA_HOME, 'domains', 'private'),
25
+ ],
26
+ },
27
+ clusters: path.join(KDNA_HOME, 'clusters'),
28
+ packages: path.join(KDNA_HOME, 'packages'),
29
+ packageIndex: path.join(KDNA_HOME, 'index.json'),
30
+ registry: path.join(KDNA_HOME, 'registry'),
31
+ registryCache: path.join(KDNA_HOME, 'registry', 'cache.json'),
32
+ traces: path.join(KDNA_HOME, 'traces'),
33
+ feedback: path.join(KDNA_HOME, 'feedback'),
34
+ evals: path.join(KDNA_HOME, 'evals'),
35
+ cache: path.join(KDNA_HOME, 'cache'),
36
+ licenses: path.join(KDNA_HOME, 'licenses'),
37
+ };
38
+
39
+ // Runtime asset store aliases
40
+ PATHS.USER_KDNA_DIR = KDNA_HOME;
41
+ PATHS.INSTALL_DIR = PATHS.packages;
42
+
43
+ module.exports = PATHS;
44
+ module.exports.KDNA_HOME = KDNA_HOME;
package/src/publish.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const { EXIT } = require('./cmds/_common');
10
+ const { EXIT, selfCheckText, isYesNoSelfCheck } = require('./cmds/_common');
11
11
 
12
12
  function error(msg, code = EXIT.VALIDATION_FAILED) {
13
13
  console.error(`Error: ${msg}`);
@@ -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) {
@@ -143,8 +143,6 @@ function isGenericSelfCheck(question) {
143
143
 
144
144
  // ─── Human Lock Gate ──────────────────────────────────────────────────
145
145
 
146
- const JUDGMENT_CARD_TYPES = ['axiom', 'boundary', 'risk', 'aesthetic'];
147
-
148
146
  /**
149
147
  * Check whether the domain satisfies Human Lock requirements.
150
148
  * Returns { passed, issues[] } — publish should be blocked if !passed.
@@ -190,13 +188,19 @@ function checkHumanLock(domainPath) {
190
188
  // Rule 3: Lock must confirm judgment fields were reviewed
191
189
  const checked = card.human_lock.checked || {};
192
190
  if (!checked.applies_when) {
193
- 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
+ );
194
194
  }
195
195
  if (!checked.does_not_apply_when) {
196
- 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
+ );
197
199
  }
198
200
  if (!checked.failure_risk) {
199
- 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
+ );
200
204
  }
201
205
  }
202
206
 
@@ -384,21 +388,22 @@ function cmdPublishCheck(domainPath, args = []) {
384
388
  }
385
389
  for (let i = 0; i < core.stances.length; i++) {
386
390
  const s = core.stances[i];
387
- if (typeof s !== 'string') {
391
+ const stanceText = typeof s === 'string' ? s : s && typeof s === 'object' ? s.stance : null;
392
+ if (!stanceText || typeof stanceText !== 'string') {
388
393
  fail(
389
394
  'KDNA_Core.json',
390
395
  `stances[${i}]`,
391
396
  JSON.stringify(s),
392
- 'Must be a string, not an object.',
397
+ 'Must be a string or an object with a stance string.',
393
398
  );
394
- } else if (isSlogan(s)) {
399
+ } else if (isSlogan(stanceText)) {
395
400
  fail(
396
401
  'KDNA_Core.json',
397
402
  `stances[${i}]`,
398
- s,
403
+ stanceText,
399
404
  'Reads like a slogan. Stances must be prescriptive positions that bias agent behavior.',
400
405
  );
401
- } else if (isVague(s)) {
406
+ } else if (isVague(stanceText)) {
402
407
  warn('KDNA_Core.json', `stances[${i}]`, 'Contains vague language.');
403
408
  } else {
404
409
  pass('KDNA_Core.json', `stances[${i}]`);
@@ -442,22 +447,23 @@ function cmdPublishCheck(domainPath, args = []) {
442
447
  if (patterns.self_check && Array.isArray(patterns.self_check)) {
443
448
  for (let i = 0; i < patterns.self_check.length; i++) {
444
449
  const sc = patterns.self_check[i];
445
- if (typeof sc !== 'string') {
450
+ const text = selfCheckText(sc);
451
+ if (!text) {
446
452
  fail(
447
453
  'KDNA_Patterns.json',
448
454
  `self_check[${i}]`,
449
455
  JSON.stringify(sc),
450
- 'Must be a string, not an object.',
456
+ 'Must be a string or an object with a question string.',
451
457
  );
452
- } else if (isGenericSelfCheck(sc)) {
458
+ } else if (isGenericSelfCheck(text)) {
453
459
  fail(
454
460
  'KDNA_Patterns.json',
455
461
  `self_check[${i}]`,
456
- sc,
462
+ text,
457
463
  'Generic question. Self-checks must be domain-specific, not "is this helpful?".',
458
464
  );
459
- } else if (!sc.endsWith('?')) {
460
- warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should end with a question mark.');
465
+ } else if (!isYesNoSelfCheck(sc)) {
466
+ warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should be answerable with yes/no.');
461
467
  passes++;
462
468
  } else {
463
469
  pass('KDNA_Patterns.json', `self_check[${i}]`);
@@ -527,25 +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
- * before hashing). All other files included as-is.
540
+ * before hashing). Digest self-reference fields are also excluded. All other files included as-is.
535
541
  */
536
- function canonicalPayload(srcDir) {
537
- const files = fs
538
- .readdirSync(srcDir)
539
- .filter((f) => f.endsWith('.json'))
540
- .sort();
542
+ function canonicalPayload(srcDir, opts = {}) {
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
- buf = Buffer.from(JSON.stringify(obj));
552
+ const value = f === 'kdna.json' ? manifestForSigning(obj, opts) : obj;
553
+ buf = Buffer.from(stableStringify(value));
549
554
  } else {
550
555
  buf = fs.readFileSync(full);
551
556
  }
@@ -555,6 +560,89 @@ function canonicalPayload(srcDir) {
555
560
  return parts.join('\n');
556
561
  }
557
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
+
573
+ function stableStringify(value) {
574
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
575
+ if (value && typeof value === 'object') {
576
+ return `{${Object.keys(value)
577
+ .sort()
578
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
579
+ .join(',')}}`;
580
+ }
581
+ return JSON.stringify(value);
582
+ }
583
+
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);
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
+ 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;
644
+ }
645
+
558
646
  function signPayload(payload, privateKeyPem) {
559
647
  const privateKey = crypto.createPrivateKey(privateKeyPem);
560
648
  const sig = crypto.sign(null, Buffer.from(payload), privateKey);
@@ -579,17 +667,17 @@ function publicKeyToScopeFormat(publicKeyPem) {
579
667
  }
580
668
 
581
669
  function packToFile(domainDir, outPath) {
582
- const files = fs
583
- .readdirSync(domainDir)
584
- .filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE');
585
- if (!files.includes('kdna.json')) error('kdna.json required in domain folder 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.');
586
673
 
587
674
  const script = `import zipfile, os
588
675
  src = ${JSON.stringify(domainDir)}
589
676
  out = ${JSON.stringify(outPath)}
590
677
  files = ${JSON.stringify(files)}
591
678
  with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
592
- 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:
593
681
  zf.write(os.path.join(src, f), f)
594
682
  `;
595
683
  const tmpPy = `/tmp/kdna-publish-pack-${Date.now()}.py`;
@@ -679,14 +767,17 @@ function cmdPublish(domainPath, args = []) {
679
767
 
680
768
  // 2. Write signature
681
769
  delete manifest.signature;
770
+ delete manifest.asset_digest;
771
+ delete manifest.container_sha256;
772
+ delete manifest.content_digest;
682
773
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
683
- const payload = canonicalPayload(abs);
684
- const sig = signPayload(payload, privateKey);
774
+ manifest.content_digest = sourceContentDigest(abs);
775
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
776
+ const signedPayload = canonicalPayload(abs);
777
+ const sig = signPayload(signedPayload, privateKey);
685
778
  manifest.signature = 'ed25519:' + sig;
686
779
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
687
- console.log(
688
- ` ✓ Signed (payload covers ${fs.readdirSync(abs).filter((f) => f.endsWith('.json')).length} json files)`,
689
- );
780
+ console.log(` ✓ Signed (payload covers ${listPublishEntries(abs).length} content entries)`);
690
781
 
691
782
  // 3. Pack
692
783
  const fileName = `${m[2]}-${manifest.version}.kdna`;
@@ -697,9 +788,10 @@ function cmdPublish(domainPath, args = []) {
697
788
  const outPath = path.join(outDir, fileName);
698
789
  packToFile(abs, outPath);
699
790
  const sha256 = sha256File(outPath);
791
+ const assetDigest = `sha256:${sha256}`;
700
792
  const size = fs.statSync(outPath).size;
701
793
  console.log(` ✓ Packed: ${outPath} (${size} bytes)`);
702
- console.log(` ✓ sha256: ${sha256}`);
794
+ console.log(` ✓ asset_digest: ${assetDigest}`);
703
795
 
704
796
  // 4. Optional upload via gh CLI
705
797
  const tagIdx = args.indexOf('--release-tag');
@@ -726,8 +818,9 @@ function cmdPublish(domainPath, args = []) {
726
818
  name,
727
819
  type: manifest.cluster ? 'cluster' : 'domain',
728
820
  version: manifest.version,
729
- kdna_url: kdnaUrl,
730
- sha256,
821
+ asset_url: kdnaUrl,
822
+ asset_digest: assetDigest,
823
+ content_digest: manifest.content_digest || null,
731
824
  signature: manifest.signature,
732
825
  release_status: kdnaUrl ? 'published_signed' : 'published_signed_local',
733
826
  author: { ...manifest.author },
@@ -744,4 +837,10 @@ function cmdPublish(domainPath, args = []) {
744
837
  );
745
838
  }
746
839
 
747
- module.exports = { cmdPublishCheck, cmdPublish, checkHumanLock, canonicalPayload, publicKeyToScopeFormat };
840
+ module.exports = {
841
+ cmdPublishCheck,
842
+ cmdPublish,
843
+ checkHumanLock,
844
+ canonicalPayload,
845
+ publicKeyToScopeFormat,
846
+ };