@aikdna/kdna-core 0.15.11 → 0.15.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.15.12 (2026-07-12)
4
+
5
+ **Single-format refactor + CBOR wire contract + Runtime Capsule.**
6
+
7
+ ### Breaking changes
8
+
9
+ - `MIMETYPE_V1`, `MIMETYPE_V2`, `MIMETYPE_LEGACY`, `isV1SourceDir`, `isV2SourceDir`
10
+ removed from public API. Use `MIMETYPE`, `isKdnaSourceDir`, `detectContainerFormat`.
11
+ - `detectContainerFormat` returns only `"kdna"` or `null`.
12
+ - `kdna_version: "2.0"` is no longer accepted. Only `"1.0"`.
13
+ - `payload.encoding` accepts only `"cbor"`. JSON payload bytes are rejected;
14
+ there is no production JSON fallback.
15
+ - Version-qualified public APIs (`V1_REQUIRED_DIR_ENTRIES`, `readV1Layout`,
16
+ `buildChecksumsV1`, `loadV1`, and the `./v1` export) are removed. Use the
17
+ unversioned APIs below.
18
+ - `kdna load --as=json` now returns Runtime Capsule format
19
+ (`type: "kdna.context.capsule"`) instead of raw `{status, content}`.
20
+
21
+ ### Added
22
+
23
+ - `REQUIRED_DIR_ENTRIES`, `readLayout`, `buildChecksums`, `load`, and
24
+ `loadAsset` are the sole public container/loading APIs.
25
+ - `buildCapsule(loadResult, v1, profile, opts)` — wraps load output as
26
+ `kdna.context.capsule` per `specs/runtime-capsule.md`.
27
+ - `parsePayloadEntry` — strict CBOR decoding for `payload.kdnab`.
28
+
29
+ ### Fixes
30
+
31
+ - Producer/consumer encoding alignment: `dev-pack.js`, `studio.js` write CBOR;
32
+ validation and loading decode CBOR, including encrypted envelopes.
33
+ - `loadV1Unsafe` extends/deps processing occurs before Capsule wrapping (Story 12 fix).
34
+ - All test fixtures converted to CBOR; golden files regenerated.
35
+
3
36
  ## v0.15.11 (2026-07-03)
4
37
 
5
38
  **Fix**: repair Core v1 container dispatcher module resolution in the
package/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # @aikdna/kdna-core
2
2
 
3
- Core library for local `.kdna` judgment assets.
4
-
5
- KDNA Core v1 defines the `.kdna` file format, schemas, secure container
6
- reader, LoadPlan contract, and runtime projection helpers used by the CLI,
7
- Studio export, skills, MCP integrations, and downstream agent runtimes.
3
+ Core library for `.kdna` judgment assets. It implements the single KDNA Asset
4
+ Container, strict-CBOR payload decoding, validation, LoadPlan authorization,
5
+ and Runtime Capsule projection used by the CLI, Studio, skills, MCP
6
+ integrations, and downstream Agent runtimes.
8
7
 
9
8
  ## Installation
10
9
 
@@ -18,9 +17,10 @@ If you need full JSON Schema validation through Ajv, also install:
18
17
  npm install ajv ajv-formats
19
18
  ```
20
19
 
21
- ## KDNA Core v1 API
20
+ ## Public API
22
21
 
23
- Use the `./v1` entrypoint for current KDNA Core v1 tooling:
22
+ Use the package root. Version-qualified entrypoints and API names are not part
23
+ of the public contract.
24
24
 
25
25
  ```js
26
26
  const {
@@ -30,8 +30,8 @@ const {
30
30
  loadAuthorized,
31
31
  pack,
32
32
  unpack,
33
- buildChecksumsV1
34
- } = require('@aikdna/kdna-core/v1');
33
+ buildChecksums
34
+ } = require('@aikdna/kdna-core');
35
35
 
36
36
  const validation = validate('./asset.kdna');
37
37
  if (!validation.overall_valid) {
@@ -43,10 +43,14 @@ if (plan.can_load_now !== true) {
43
43
  throw new Error(`Asset is not loadable yet: ${plan.required_action}`);
44
44
  }
45
45
 
46
- const compact = loadAuthorized('./asset.kdna', {
46
+ const capsule = loadAuthorized('./asset.kdna', {
47
47
  profile: 'compact',
48
- as: 'prompt'
48
+ as: 'json'
49
49
  });
50
+
51
+ if (capsule.type !== 'kdna.context.capsule') {
52
+ throw new Error('Expected a Runtime Capsule');
53
+ }
50
54
  ```
51
55
 
52
56
  ## Supported Runtime Flow
@@ -56,7 +60,8 @@ const compact = loadAuthorized('./asset.kdna', {
56
60
  → validate
57
61
  → planLoad
58
62
  → loadAuthorized
59
- → agent/runtime context
63
+ → Runtime Capsule
64
+ → Agent/runtime context
60
65
  ```
61
66
 
62
67
  For authoring and test fixtures, `pack()` can turn a local working folder into
@@ -71,9 +76,9 @@ local working folder
71
76
  → loadAuthorized
72
77
  ```
73
78
 
74
- ## v1 Container
79
+ ## KDNA Asset Container
75
80
 
76
- A v1 `.kdna` container contains:
81
+ A `.kdna` asset contains:
77
82
 
78
83
  - `mimetype`
79
84
  - `kdna.json`
@@ -88,6 +93,10 @@ A v1 `.kdna` container contains:
88
93
  - checksum consistency
89
94
  - load-profile contract
90
95
 
96
+ `payload.kdnab` is CBOR. A password-protected or licensed asset stores a CBOR
97
+ encryption envelope in the same entry; authorized plaintext is decrypted in
98
+ memory and projected into the Runtime Capsule.
99
+
91
100
  ## Load Profiles
92
101
 
93
102
  `loadAuthorized()` supports:
@@ -104,15 +113,9 @@ Output formats:
104
113
 
105
114
  ## Boundary
106
115
 
107
- KDNA Core v1 is content-neutral. It validates file structure and loadability;
116
+ KDNA Core is content-neutral. It validates file structure and loadability;
108
117
  it does not rank judgment quality or endorse specific assets.
109
118
 
110
- ## Legacy API
111
-
112
- The package root still exports compatibility APIs for older KDNA paths. New
113
- tooling should prefer `@aikdna/kdna-core/v1` and the `planLoad` /
114
- `loadAuthorized` path.
115
-
116
119
  ## License
117
120
 
118
121
  Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.15.11",
3
+ "version": "0.15.12",
4
4
  "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -15,12 +15,7 @@
15
15
  "types": "./src/types.d.ts"
16
16
  },
17
17
  "./package.json": "./package.json",
18
- "./schema/*": "./schema/*",
19
- "./v1": {
20
- "import": "./src/v1/index.mjs",
21
- "require": "./src/v1/index.js",
22
- "types": "./src/types.d.ts"
23
- }
18
+ "./schema/*": "./schema/*"
24
19
  },
25
20
  "types": "src/types.d.ts",
26
21
  "scripts": {
@@ -22,8 +22,8 @@
22
22
  "properties": {
23
23
  "kdna_version": {
24
24
  "type": "string",
25
- "description": "KDNA Core version this manifest conforms to.",
26
- "enum": ["1.0", "2.0"]
25
+ "description": "KDNA Asset Container wire discriminator.",
26
+ "enum": ["1.0"]
27
27
  },
28
28
  "asset_id": {
29
29
  "type": "string",
@@ -102,7 +102,7 @@
102
102
  "additionalProperties": true,
103
103
  "properties": {
104
104
  "path": { "type": "string", "const": "payload.kdnab" },
105
- "encoding": { "type": "string", "enum": ["json", "cbor"] },
105
+ "encoding": { "type": "string", "enum": ["cbor"] },
106
106
  "encrypted": { "type": "boolean" }
107
107
  }
108
108
  },
@@ -8,7 +8,7 @@ const zlib = require('zlib');
8
8
  const cbor = require('cbor-x');
9
9
  const { loadDomainFromFiles, formatContext } = require('./loader');
10
10
 
11
- const KDNA_MEDIA_TYPE = 'application/vnd.aikdna.kdna+zip';
11
+ const KDNA_MEDIA_TYPE = 'application/vnd.kdna.asset';
12
12
 
13
13
  // Standard KDNA domain data entries — kept in lockstep with loader.FILE_MAP.
14
14
  // Asset reader pre-loads these so callers don't have to enumerate entry names.
@@ -363,19 +363,18 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
363
363
 
364
364
  function validateManifestIdentity(manifest, errors, _warnings) {
365
365
  if (manifest.kdna_spec) {
366
- errors.push('kdna.json: kdna_spec is not allowed. Use spec_version.');
366
+ errors.push('kdna.json: kdna_spec is not allowed. Use kdna_version.');
367
367
  }
368
368
  if (manifest.format && manifest.format !== 'kdna') {
369
369
  errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
370
370
  }
371
- if (manifest.format_version && manifest.format_version !== '2.0') {
371
+ if (!manifest.kdna_version) {
372
+ errors.push('kdna.json: missing required field "kdna_version"');
373
+ } else if (manifest.kdna_version !== '1.0') {
372
374
  errors.push(
373
- `kdna.json.format_version: invalid value "${manifest.format_version}". Expected "2.0".`,
375
+ `kdna.json.kdna_version: invalid value "${manifest.kdna_version}". Expected "1.0".`,
374
376
  );
375
377
  }
376
- if (!manifest.spec_version) errors.push('kdna.json: missing required field "spec_version"');
377
- if (!manifest.format) errors.push('kdna.json: missing required field "format"');
378
- if (!manifest.format_version) errors.push('kdna.json: missing required field "format_version"');
379
378
  if (manifest.language) {
380
379
  errors.push('kdna.json: language is not allowed. Use default_language and languages.');
381
380
  }
@@ -624,10 +623,7 @@ function createKdnaAssetReader() {
624
623
 
625
624
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
626
625
  verifyMediaType(asset, errors);
627
- if (!asset.entries.has('KDNA_Core.json')) errors.push('required entry missing: KDNA_Core.json');
628
- if (!asset.entries.has('KDNA_Patterns.json')) {
629
- errors.push('required entry missing: KDNA_Patterns.json');
630
- }
626
+ if (!asset.entries.has('payload.kdnab')) errors.push('required entry missing: payload.kdnab');
631
627
 
632
628
  const content_digest = buildContentDigest(asset);
633
629
  const asset_digest = asset.asset_digest;
@@ -4,45 +4,25 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const crypto = require('node:crypto');
6
6
 
7
- const { createKdnaAssetReader } = require('./asset-reader.js');
8
-
9
- const MIMETYPE_V1 = 'application/vnd.kdna.asset';
10
- const MIMETYPE_V2 = 'application/vnd.aikdna.kdna+zip';
7
+ const MIMETYPE = 'application/vnd.kdna.asset';
11
8
 
12
9
  /**
13
- * CanonicalAssetModel — the single internal representation of any KDNA asset,
14
- * regardless of whether it came from a source directory, Container v1,
15
- * or Canonical Distribution Container (Container v2 / SPEC 2.0).
16
- *
17
- * All downstream code (planLoad, loadAuthorized, decrypt, loadV1Unsafe)
18
- * operates on this model. No consumer should check container format directly.
10
+ * CanonicalAssetModel — the single internal representation of a KDNA asset,
11
+ * regardless of source (directory or .kdna container).
19
12
  *
20
13
  * @typedef {object} CanonicalAssetModel
21
- * @property {'dir'|'file'} sourceKind — original input type
22
- * @property {string} sourcePath — resolved absolute path
23
- * @property {string} format — 'v1-dir' | 'v1-container' | 'v2-container'
24
- * @property {string} mimetype — normalized mimetype
14
+ * @property {'dir'|'file'} sourceKind
15
+ * @property {string} sourcePath
16
+ * @property {string} format — 'dir' | 'kdna'
17
+ * @property {string} mimetype
25
18
  * @property {object} manifest — parsed kdna.json
26
- * @property {Buffer} payloadRaw — raw payload.kdnab bytes (CBOR/JSON)
27
- * @property {object|null} checksums — parsed checksums.json, or null
28
- * @property {Buffer|null} assetDigest — SHA-256 of entire container (file only)
29
- * @property {Buffer|null} containerBuffer — full container bytes (for ZIP inputs, may be null for dirs)
30
- * @property {Map<string, Buffer>|null} entries — raw entry map (for v1 dir layout; null for v2/container)
19
+ * @property {Buffer} payloadRaw — raw payload.kdnab bytes
20
+ * @property {object|null} checksums
21
+ * @property {Buffer|null} assetDigest
22
+ * @property {Buffer|null} containerBuffer
23
+ * @property {Map<string, Buffer>|null} entries
31
24
  */
32
25
 
33
- /**
34
- * Read any KDNA asset into a CanonicalAssetModel.
35
- *
36
- * Handles:
37
- * - Source directories (mimetype + kdna.json + payload.kdnab on disk)
38
- * - Container v1 (.kdna ZIP with application/vnd.kdna.asset mimetype)
39
- * - Canonical containers (.kdna ZIP with application/vnd.aikdna.kdna+zip mimetype)
40
- *
41
- * @param {string} inputPath - absolute path to asset file or directory
42
- * @param {object} [opts]
43
- * @param {object} [opts.limits] - container size/entry limits
44
- * @returns {CanonicalAssetModel}
45
- */
46
26
  function readAsset(inputPath, opts = {}) {
47
27
  const absPath = path.resolve(inputPath);
48
28
  const stat = fs.statSync(absPath);
@@ -59,11 +39,8 @@ function readAsset(inputPath, opts = {}) {
59
39
  }
60
40
 
61
41
  const format = detectFormat(absPath);
62
- if (format === 'v1-container') {
63
- return readV1Container(absPath, opts);
64
- }
65
- if (format === 'v2-container') {
66
- return readV2Container(absPath, opts);
42
+ if (format === 'kdna') {
43
+ return readKdnaContainer(absPath, opts);
67
44
  }
68
45
 
69
46
  throw Object.assign(
@@ -72,10 +49,6 @@ function readAsset(inputPath, opts = {}) {
72
49
  );
73
50
  }
74
51
 
75
- /**
76
- * Detect the container format of a .kdna file.
77
- * Returns 'v1-container', 'v2-container', or throws.
78
- */
79
52
  function detectFormat(absPath) {
80
53
  const buf = fs.readFileSync(absPath);
81
54
  if (buf.length < 4) {
@@ -85,7 +58,6 @@ function detectFormat(absPath) {
85
58
  );
86
59
  }
87
60
 
88
- // Must be a ZIP (PK signature)
89
61
  const sig = buf.readUInt32LE(0);
90
62
  if (sig !== 0x04034b50) {
91
63
  throw Object.assign(
@@ -94,7 +66,6 @@ function detectFormat(absPath) {
94
66
  );
95
67
  }
96
68
 
97
- // Read the mimetype from the first entry (central directory first entry)
98
69
  const eocdOffset = findEocd(buf);
99
70
  let cdOffset = buf.readUInt32LE(eocdOffset + 16);
100
71
  const cdEntryCount = buf.readUInt16LE(eocdOffset + 10);
@@ -126,8 +97,7 @@ function detectFormat(absPath) {
126
97
  const compressedSize = buf.readUInt32LE(entryOffset + 20);
127
98
  const mime = buf.toString('utf8', dataOffset, dataOffset + compressedSize).trim();
128
99
 
129
- if (mime === MIMETYPE_V1) return 'v1-container';
130
- if (mime === MIMETYPE_V2) return 'v2-container';
100
+ if (mime === MIMETYPE) return 'kdna';
131
101
 
132
102
  throw Object.assign(
133
103
  new Error(`Unknown KDNA mimetype: ${mime}`),
@@ -154,9 +124,6 @@ function findEocd(buf) {
154
124
  );
155
125
  }
156
126
 
157
- /**
158
- * Read a source directory into CanonicalAssetModel.
159
- */
160
127
  function readSourceDirectory(absPath, _opts) {
161
128
  const mimetypePath = path.join(absPath, 'mimetype');
162
129
  const manifestPath = path.join(absPath, 'kdna.json');
@@ -171,7 +138,7 @@ function readSourceDirectory(absPath, _opts) {
171
138
  }
172
139
 
173
140
  const mimetype = fs.readFileSync(mimetypePath, 'utf8').trim();
174
- if (![MIMETYPE_V1, MIMETYPE_V2].includes(mimetype)) {
141
+ if (mimetype !== MIMETYPE) {
175
142
  throw Object.assign(
176
143
  new Error(`Unknown mimetype in source directory: ${mimetype}`),
177
144
  { code: 'KDNA_FORMAT_UNKNOWN' },
@@ -204,7 +171,7 @@ function readSourceDirectory(absPath, _opts) {
204
171
  return {
205
172
  sourceKind: 'dir',
206
173
  sourcePath: absPath,
207
- format: 'v1-dir',
174
+ format: 'dir',
208
175
  mimetype,
209
176
  manifest,
210
177
  payloadRaw,
@@ -215,21 +182,16 @@ function readSourceDirectory(absPath, _opts) {
215
182
  };
216
183
  }
217
184
 
218
- /**
219
- * Read a Container v1 (.kdna with v1 mimetype) into CanonicalAssetModel.
220
- */
221
- function readV1Container(absPath, _opts) {
222
- // Delegate to the existing v1 layout reader for validation
223
- const v1Layout = require('./v1/index.js').readV1Layout(absPath);
224
-
185
+ function readKdnaContainer(absPath, _opts) {
186
+ const v1Layout = require('./v1/index.js').readLayout(absPath);
225
187
  const containerBuf = fs.readFileSync(absPath);
226
188
  const assetDigest = crypto.createHash('sha256').update(containerBuf).digest();
227
189
 
228
190
  return {
229
191
  sourceKind: 'file',
230
192
  sourcePath: absPath,
231
- format: 'v1-container',
232
- mimetype: MIMETYPE_V1,
193
+ format: 'kdna',
194
+ mimetype: MIMETYPE,
233
195
  manifest: v1Layout.manifest,
234
196
  payloadRaw: v1Layout.map['payload.kdnab'] || null,
235
197
  checksums: v1Layout.map['checksums.json']
@@ -241,58 +203,18 @@ function readV1Container(absPath, _opts) {
241
203
  };
242
204
  }
243
205
 
244
- /**
245
- * Read a Canonical Container (v2 mimetype) into CanonicalAssetModel.
246
- * Uses the existing KdnaAssetReader for ZIP parsing.
247
- */
248
- function readV2Container(absPath, _opts) {
249
- const reader = createKdnaAssetReader();
250
- const asset = reader.openSync(absPath);
251
-
252
- const manifest = reader.readManifestSync(asset);
253
- const payloadRaw = reader.readEntrySync(asset, 'payload.kdnab');
254
- const checksums = (() => {
255
- try {
256
- const raw = reader.readEntrySync(asset, 'checksums.json');
257
- return raw ? JSON.parse(raw.toString('utf8')) : null;
258
- } catch {
259
- return null;
260
- }
261
- })();
262
-
263
- return {
264
- sourceKind: 'file',
265
- sourcePath: absPath,
266
- format: 'v2-container',
267
- mimetype: MIMETYPE_V2,
268
- manifest,
269
- payloadRaw,
270
- checksums,
271
- assetDigest: Buffer.from(asset.asset_digest.replace(/^sha256:/, ''), 'hex'),
272
- containerBuffer: fs.readFileSync(absPath),
273
- entries: null, // v2 uses lazy reads, not pre-loaded map
274
- };
275
- }
276
-
277
- /**
278
- * Extract the mimetype from a CanonicalAssetModel.
279
- */
280
206
  function getMimetype(asset) {
281
207
  return asset.mimetype;
282
208
  }
283
209
 
284
- /**
285
- * Determine if a CanonicalAssetModel represents a source directory.
286
- */
287
- function isSourceDirectory(asset) {
210
+ function isKdnaSourceDirectory(asset) {
288
211
  return asset.sourceKind === 'dir';
289
212
  }
290
213
 
291
214
  module.exports = {
292
- MIMETYPE_V1,
293
- MIMETYPE_V2,
215
+ MIMETYPE,
294
216
  readAsset,
295
217
  detectFormat,
296
218
  getMimetype,
297
- isSourceDirectory,
219
+ isKdnaSourceDirectory,
298
220
  };
@@ -1,4 +1,5 @@
1
1
  const crypto = require('crypto');
2
+ const cbor = require('cbor-x');
2
3
 
3
4
  let argon2id;
4
5
  try {
@@ -56,11 +57,13 @@ function decodeBase64(value, label) {
56
57
 
57
58
  function normalizeEnvelope(value) {
58
59
  if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
59
- return JSON.parse(Buffer.from(value).toString('utf8'));
60
+ try { return cbor.decode(Buffer.from(value)); } catch { /* fall through */ }
61
+ }
62
+ if (typeof value === 'string') {
63
+ try { return cbor.decode(Buffer.from(value, 'utf8')); } catch { /* fall through */ }
60
64
  }
61
- if (typeof value === 'string') return JSON.parse(value);
62
65
  if (value && typeof value === 'object') return value;
63
- throw new Error('encrypted entry envelope must be JSON');
66
+ throw new Error('encrypted entry envelope must be CBOR (kdna_version 1.0)');
64
67
  }
65
68
 
66
69
  function encryptedEntryAad(entryName, manifest = {}, profile = LICENSED_ENTRY_PROFILE) {
package/src/index.mjs CHANGED
@@ -12,6 +12,28 @@ export {
12
12
 
13
13
  export { lintDomain } from './lint-pure.js';
14
14
 
15
+ export {
16
+ MIMETYPE,
17
+ REQUIRED_DIR_ENTRIES,
18
+ isKdnaSourceDir,
19
+ detectContainerFormat,
20
+ readLayout,
21
+ inspect,
22
+ validate,
23
+ planLoad,
24
+ loadAuthorized,
25
+ buildChecksums,
26
+ pack,
27
+ unpack,
28
+ load,
29
+ loadAsset,
30
+ buildCapsule,
31
+ FORBIDDEN_OUTPUT_TERMS,
32
+ parseSemver,
33
+ compareSemver,
34
+ satisfies,
35
+ } from './v1/index.js';
36
+
15
37
  export { validateDomainSchema, validateCrossFile } from './validate-pure.js';
16
38
 
17
39
  export { renderPreviewHTML, escHtml, renderCard } from './render.js';
package/src/lint-pure.js CHANGED
@@ -273,13 +273,13 @@ const VALID_ASSET_TYPE = new Set([
273
273
  ]);
274
274
 
275
275
  const MANIFEST_REQUIRED = [
276
- 'format', 'format_version', 'spec_version', 'name', 'version',
276
+ 'kdna_version', 'name', 'version',
277
277
  'judgment_version', 'description', 'author', 'license', 'status',
278
278
  'quality_badge', 'access', 'languages', 'default_language',
279
279
  ];
280
280
 
281
281
  /**
282
- * Validate a kdna.json manifest against the canonical v1.0-rc schema.
282
+ * Validate a kdna.json manifest against the canonical KDNA Asset format.
283
283
  *
284
284
  * @param {Object} manifest — parsed kdna.json
285
285
  * @returns {{ errors: string[], warnings: string[] }}
@@ -296,7 +296,7 @@ function validateManifest(manifest) {
296
296
  // 1. Check disallowed pre-v1.0 manifest aliases
297
297
  if ('kdna_spec' in manifest) {
298
298
  errors.push(
299
- 'kdna.json: kdna_spec is not allowed. Use spec_version.',
299
+ 'kdna.json: kdna_spec is not allowed. Use kdna_version.',
300
300
  );
301
301
  }
302
302
  if ('language' in manifest) {
@@ -319,9 +319,11 @@ function validateManifest(manifest) {
319
319
  if (manifest.format && manifest.format !== 'kdna') {
320
320
  errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
321
321
  }
322
- if (manifest.format_version && manifest.format_version !== '2.0') {
322
+ if (!manifest.kdna_version) {
323
+ errors.push('kdna.json: missing required field "kdna_version"');
324
+ } else if (manifest.kdna_version !== '1.0') {
323
325
  errors.push(
324
- `kdna.json.format_version: invalid value "${manifest.format_version}". Expected "2.0".`,
326
+ `kdna.json.kdna_version: invalid value "${manifest.kdna_version}". Expected "1.0".`,
325
327
  );
326
328
  }
327
329
  if (manifest.status && !VALID_STATUS.has(manifest.status)) {
@@ -394,14 +396,7 @@ function validateManifest(manifest) {
394
396
  errors.push('kdna.json.license: missing "type"');
395
397
  }
396
398
 
397
- // 9. Validate spec_version value
398
- if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
399
- warnings.push(
400
- `kdna.json.spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
401
- );
402
- }
403
-
404
- // 10. Validate version format
399
+ // 9. Validate version format
405
400
  if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
406
401
  warnings.push(
407
402
  `kdna.json.version: non-semver format "${manifest.version}". Expected MAJOR.MINOR.PATCH.`,
package/src/types.d.ts CHANGED
@@ -203,9 +203,7 @@ export interface KDNAFileDataMap {
203
203
  }
204
204
 
205
205
  export interface KDNAManifest {
206
- format: 'kdna';
207
- format_version: '2.0';
208
- spec_version: string;
206
+ kdna_version: '1.0';
209
207
  name: string;
210
208
  version: string;
211
209
  judgment_version: string;
@@ -484,36 +482,33 @@ export function matchDomain(input: string, candidates: Array<KDNAAssetInput | KD
484
482
  export function matchDomainSync(input: string, candidates: Array<KDNAAssetInput | KDNAInspectResult>, options?: KdnaDecryptOptions): KDNAMatchResult[];
485
483
  export function composeKDNA(inputs: KDNAAssetInput[], options?: { input?: string; profile?: 'compact' | 'scenario' | 'full' | string; separator?: string } & KdnaDecryptOptions): Promise<KDNAComposeResult>;
486
484
 
487
- // KDNA Core v1 — source directory / container API
485
+ // KDNA Core — source directory / container API
488
486
  export const MIMETYPE: string;
489
- export const MIMETYPE_V1: string;
490
- export const MIMETYPE_V2: string;
491
- export const V1_REQUIRED_DIR_ENTRIES: string[];
487
+ export const REQUIRED_DIR_ENTRIES: string[];
492
488
 
493
- export interface KDNAV1ChecksumEntry {
489
+ export interface KDNAChecksumEntry {
494
490
  algorithm: 'sha256';
495
491
  value: string;
496
492
  }
497
493
 
498
- export interface KDNAV1Checksums {
494
+ export interface KDNAChecksums {
499
495
  algorithm: 'sha256';
500
496
  manifest_digest: string;
501
497
  payload_digest: string;
502
498
  asset_digest: string;
503
- entries: Record<string, KDNAV1ChecksumEntry>;
499
+ entries: Record<string, KDNAChecksumEntry>;
504
500
  }
505
501
 
506
- export function isV1SourceDir(inputPath: string): boolean;
507
- export function isV2SourceDir(inputPath: string): boolean;
508
- export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
502
+ export function isKdnaSourceDir(inputPath: string): boolean;
503
+ export function detectContainerFormat(inputPath: string): 'kdna' | null;
509
504
  export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
510
505
  export function validate(inputPath: string, options?: Record<string, any>): Record<string, any>;
511
- export interface KDNAV1LoadPlanIssue {
506
+ export interface KDNALoadPlanIssue {
512
507
  code: string;
513
508
  severity: 'info' | 'warning' | 'blocking' | string;
514
509
  message: string;
515
510
  }
516
- export interface KDNAV1LoadPlan {
511
+ export interface KDNALoadPlan {
517
512
  kdna_version: string | null;
518
513
  asset: {
519
514
  asset_id: string | null;
@@ -552,18 +547,19 @@ export interface KDNAV1LoadPlan {
552
547
  projection_policy: 'minimal' | 'remote' | 'none' | string;
553
548
  input_fingerprint: string | null;
554
549
  checks: Record<string, boolean>;
555
- issues: KDNAV1LoadPlanIssue[];
550
+ issues: KDNALoadPlanIssue[];
556
551
  source: {
557
552
  kind: 'dir' | 'file' | string | null;
558
553
  path: string;
559
554
  };
560
555
  }
561
- export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }): KDNAV1LoadPlan;
562
- export function buildChecksumsV1(sourceDir: string): KDNAV1Checksums;
556
+ export function planLoad(inputPath: string, options?: { password?: string; hasPassword?: boolean; entitlement?: Record<string, any> }): KDNALoadPlan;
557
+ export function buildChecksums(sourceDir: string): KDNAChecksums;
563
558
  export function pack(sourceDir: string, outputPath: string): void;
564
559
  export function unpack(inputPath: string, outputDir: string): void;
565
560
  export function loadAuthorized(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
566
- export function loadV1(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
561
+ export function load(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
562
+ export function loadAsset(inputPath: string, options?: { profile?: 'index' | 'compact' | 'scenario' | 'full' | string; as?: 'json' | 'prompt' | string }): Record<string, any>;
567
563
  export const FORBIDDEN_OUTPUT_TERMS: readonly string[];
568
564
 
569
565
  // Lint
package/src/v1/index.js CHANGED
@@ -38,24 +38,24 @@ const fs = require('node:fs');
38
38
  const path = require('node:path');
39
39
  const zlib = require('node:zlib');
40
40
  const crypto = require('node:crypto');
41
+ const cbor = require('cbor-x');
41
42
 
42
43
  const { readAsset } = (() => {
43
44
  try {
44
45
  return require('../container-dispatcher.js');
45
46
  } catch {
46
- return { readAsset: null, MIMETYPE_V1: null, MIMETYPE_V2: null };
47
+ return { readAsset: null };
47
48
  }
48
49
  })();
49
50
 
50
- const MIMETYPE_V1 = 'application/vnd.kdna.asset';
51
- const MIMETYPE_V2 = 'application/vnd.aikdna.kdna+zip';
52
- const V1_REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
53
- const V1_OPTIONAL_DIR_ENTRIES = ['checksums.json', 'signatures', 'attachments'];
54
- const V1_ALLOWED_TOP_LEVEL_ENTRIES = new Set([
55
- ...V1_REQUIRED_DIR_ENTRIES,
56
- ...V1_OPTIONAL_DIR_ENTRIES,
51
+ const MIMETYPE = 'application/vnd.kdna.asset';
52
+ const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
53
+ const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'signatures', 'attachments'];
54
+ const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
55
+ ...REQUIRED_DIR_ENTRIES,
56
+ ...OPTIONAL_DIR_ENTRIES,
57
57
  ]);
58
- const V1_FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
58
+ const FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
59
59
  'KDNA_Core.json',
60
60
  'KDNA_Patterns.json',
61
61
  'KDNA_Scenarios.json',
@@ -64,6 +64,12 @@ const V1_FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
64
64
  'KDNA_Evolution.json',
65
65
  ]);
66
66
 
67
+ // Legacy aliases — prefer the unversioned names above
68
+ const V1_REQUIRED_DIR_ENTRIES = REQUIRED_DIR_ENTRIES;
69
+ const V1_OPTIONAL_DIR_ENTRIES = OPTIONAL_DIR_ENTRIES;
70
+ const V1_ALLOWED_TOP_LEVEL_ENTRIES = ALLOWED_TOP_LEVEL_ENTRIES;
71
+ const V1_FORBIDDEN_LEGACY_TOP_LEVEL = FORBIDDEN_LEGACY_TOP_LEVEL;
72
+
67
73
  const DEFAULT_CONTAINER_LIMITS = Object.freeze({
68
74
  maxContainerBytes: 25 * 1024 * 1024,
69
75
  maxEntries: 128,
@@ -176,45 +182,28 @@ function loadSchemas() {
176
182
  * Required entries: mimetype, kdna.json, payload.kdnab.
177
183
  * mimetype content must equal "application/vnd.kdna.asset".
178
184
  */
179
- function isV1SourceDir(absPath) {
185
+ function isKdnaSourceDir(absPath) {
180
186
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
181
187
  for (const f of V1_REQUIRED_DIR_ENTRIES) {
182
188
  if (!fs.existsSync(path.join(absPath, f))) return false;
183
189
  }
184
190
  const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
185
- return mime === MIMETYPE_V1;
191
+ return mime === MIMETYPE;
186
192
  }
187
193
 
188
194
  /**
189
- * Detect whether a directory is a v2 source layout.
190
- * Required entries: mimetype, kdna.json, payload.kdnab.
191
- * mimetype content must equal "application/vnd.aikdna.kdna+zip".
192
- */
193
- function isV2SourceDir(absPath) {
194
- if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
195
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
196
- if (!fs.existsSync(path.join(absPath, f))) return false;
197
- }
198
- const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
199
- return mime === MIMETYPE_V2;
200
- }
201
-
202
195
  /**
203
- * Detect whether a file is a v1 .kdna container.
204
- * Returns 'v1' | 'v2' | null. null = not a .kdna file or unreadable.
196
+ * Detect whether a file is a .kdna container.
197
+ * Returns 'kdna' | null. null = not a .kdna file or unreadable.
205
198
  */
206
199
  function detectContainerFormat(absPath) {
207
200
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) return null;
208
- // Quick header check: must look like a ZIP.
209
201
  const fd = fs.openSync(absPath, 'r');
210
202
  const head = Buffer.alloc(4);
211
203
  fs.readSync(fd, head, 0, 4, 0);
212
204
  fs.closeSync(fd);
213
205
  if (head[0] !== 0x50 || head[1] !== 0x4b) return null;
214
206
 
215
- // Read only the first central-directory entry. Dangerous later entries
216
- // must not make detection fall through to a less strict legacy route;
217
- // readV1Layout/validate will reject them with the secure container reader.
218
207
  let first;
219
208
  try {
220
209
  first = readFirstZipEntry(absPath);
@@ -222,12 +211,9 @@ function detectContainerFormat(absPath) {
222
211
  return null;
223
212
  }
224
213
  if (first.name !== 'mimetype') return null;
225
- // The mimetype entry must be STORED (method 0).
226
214
  if (first.method !== 0) return null;
227
215
  const mime = first.method === 0 ? first.data.toString('utf8') : '';
228
- if (mime === MIMETYPE_V1) return 'v1';
229
- if (mime === MIMETYPE_V2) return 'v2';
230
- return null;
216
+ return (mime === MIMETYPE) ? 'kdna' : null;
231
217
  }
232
218
 
233
219
  // ─── ZIP I/O ────────────────────────────────────────────────────────────
@@ -599,9 +585,9 @@ function readV1Layout(absPath) {
599
585
 
600
586
  // mimetype content must equal the literal v1 media type.
601
587
  const mime = map.mimetype.toString('utf8');
602
- if (mime !== MIMETYPE_V1 && mime !== MIMETYPE_V2) {
588
+ if (mime !== MIMETYPE) {
603
589
  throw new Error(
604
- `not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE_V1}" or "${MIMETYPE_V2}"`,
590
+ `not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`,
605
591
  );
606
592
  }
607
593
 
@@ -657,6 +643,24 @@ function assertJsonWithinLimits(value, name, limits) {
657
643
  walk(value, 0);
658
644
  }
659
645
 
646
+ function parsePayloadEntry(name, bytes, opts = {}) {
647
+ if (!Buffer.isBuffer(bytes)) {
648
+ throw new Error(`${name} is not a file entry`);
649
+ }
650
+ const limits = { ...DEFAULT_CONTAINER_LIMITS, ...(opts.limits || {}) };
651
+ if (bytes.length > limits.maxEntryBytes) {
652
+ throw new Error(`${name} exceeds maximum size (${limits.maxEntryBytes} bytes)`);
653
+ }
654
+ let parsed;
655
+ try {
656
+ parsed = cbor.decode(bytes);
657
+ } catch {
658
+ throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (kdna_version 1.0).`);
659
+ }
660
+ assertJsonWithinLimits(parsed, name, limits);
661
+ return parsed;
662
+ }
663
+
660
664
  // ─── inspect ───────────────────────────────────────────────────────────
661
665
 
662
666
  /**
@@ -710,9 +714,9 @@ function runValidate(v1) {
710
714
  }
711
715
  if (v1.map.mimetype) {
712
716
  const mimeStr = v1.map.mimetype.toString('utf8');
713
- if (mimeStr !== MIMETYPE_V1 && mimeStr !== MIMETYPE_V2) {
717
+ if (mimeStr !== MIMETYPE) {
714
718
  result.format_valid = false;
715
- problems.push(`format: mimetype is not ${MIMETYPE_V1} or ${MIMETYPE_V2}`);
719
+ problems.push(`format: mimetype is not ${MIMETYPE}`);
716
720
  }
717
721
  }
718
722
 
@@ -736,10 +740,10 @@ function runValidate(v1) {
736
740
  // For actually encrypted payloads (envelopes), skip schema validation.
737
741
  let payload;
738
742
  try {
739
- payload = parseJsonEntry('payload.kdnab', v1.map['payload.kdnab']);
743
+ payload = parsePayloadEntry("payload.kdnab", v1.map['payload.kdnab']);
740
744
  } catch (e) {
741
745
  result.payload_valid = false;
742
- problems.push(`payload: not valid JSON (${e.message})`);
746
+ problems.push(`payload: not valid KDNA payload (${e.message})`);
743
747
  return finalizeValidate(result, problems);
744
748
  }
745
749
 
@@ -931,12 +935,22 @@ function pack(sourceDir, outputPath) {
931
935
  }
932
936
  }
933
937
  const mime = fs.readFileSync(path.join(absSrc, 'mimetype'), 'utf8');
934
- if (mime !== MIMETYPE_V1) {
935
- throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE_V1}"`);
938
+ if (mime !== MIMETYPE) {
939
+ throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE}"`);
936
940
  }
937
941
 
938
942
  // Collect deterministically; mimetype is forced first.
939
943
  const collected = listSourceDir(absSrc);
944
+ for (const entry of collected) {
945
+ const normalizedName = normalizeContainerEntryName(entry.rel);
946
+ const topLevel = normalizedName.split('/')[0];
947
+ if (!ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
948
+ if (FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
949
+ throw new Error(`cannot pack forbidden top-level source entry: ${topLevel}`);
950
+ }
951
+ throw new Error(`cannot pack unsupported top-level entry: ${topLevel}`);
952
+ }
953
+ }
940
954
  const order = ['mimetype', ...collected.map((e) => e.rel).filter((n) => n !== 'mimetype')];
941
955
 
942
956
  // Build the ZIP body.
@@ -946,7 +960,7 @@ function pack(sourceDir, outputPath) {
946
960
  for (const rel of order) {
947
961
  let data;
948
962
  if (rel === 'mimetype') {
949
- data = Buffer.from(MIMETYPE_V1, 'utf8');
963
+ data = Buffer.from(MIMETYPE, 'utf8');
950
964
  } else {
951
965
  const found = collected.find((e) => e.rel === rel);
952
966
  if (!found) continue;
@@ -1011,9 +1025,9 @@ function unpack(inputPath, outputDir) {
1011
1025
  throw new Error('not a KDNA container: mimetype must be uncompressed');
1012
1026
  }
1013
1027
  const mime = entries[0].data.toString('utf8');
1014
- if (mime !== MIMETYPE_V1 && mime !== MIMETYPE_V2) {
1028
+ if (mime !== MIMETYPE) {
1015
1029
  throw new Error(
1016
- `not a KDNA container: mimetype is "${mime}", expected "${MIMETYPE_V1}" or "${MIMETYPE_V2}"`,
1030
+ `not a KDNA container: mimetype is "${mime}", expected "${MIMETYPE}"`,
1017
1031
  );
1018
1032
  }
1019
1033
  const absOut = path.resolve(outputDir);
@@ -1621,26 +1635,22 @@ function assertNoForbiddenTerms(obj) {
1621
1635
  }
1622
1636
 
1623
1637
  module.exports = {
1624
- MIMETYPE: MIMETYPE_V1,
1625
- MIMETYPE_V1,
1626
- MIMETYPE_V2,
1627
- V1_REQUIRED_DIR_ENTRIES,
1628
- isV1SourceDir,
1629
- isV2SourceDir,
1638
+ MIMETYPE,
1639
+ REQUIRED_DIR_ENTRIES,
1640
+ isKdnaSourceDir,
1630
1641
  detectContainerFormat,
1631
- readV1Layout,
1642
+ readLayout: readV1Layout,
1632
1643
  inspect,
1633
1644
  validate,
1634
1645
  planLoad,
1635
1646
  loadAuthorized,
1636
- buildChecksumsV1,
1647
+ buildChecksums: buildChecksumsV1,
1637
1648
  pack,
1638
1649
  unpack,
1639
- loadV1,
1650
+ load: loadV1,
1651
+ loadAsset: loadV1,
1652
+ buildCapsule,
1640
1653
  FORBIDDEN_OUTPUT_TERMS,
1641
- // Semver utilities (Story 6 / Story 13) — exported so consumers
1642
- // (e.g. kdna-cli) can use the canonical implementation instead of
1643
- // duplicating it.
1644
1654
  parseSemver,
1645
1655
  compareSemver,
1646
1656
  satisfies,
@@ -1742,8 +1752,11 @@ function loadAuthorized(inputPath, opts = {}) {
1742
1752
  const mergedOpts = {
1743
1753
  ...opts,
1744
1754
  resolvedDependencies: plan.resolved_dependencies,
1745
- // Pass extends_chain from plan to loadV1Unsafe so inheritance is applied (Story 12)
1746
1755
  extendsChain: plan.extends_chain || [],
1756
+ _validation: {
1757
+ schema_valid: plan.checks && plan.checks.overall_valid === true,
1758
+ signature_valid: plan.signature_valid === true,
1759
+ },
1747
1760
  };
1748
1761
  return loadV1Unsafe(inputPath, mergedOpts);
1749
1762
  }
@@ -1793,9 +1806,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
1793
1806
  let payload;
1794
1807
  try {
1795
1808
  const rawPayload = v1.map['payload.kdnab'];
1796
- payload = parseJsonEntry('payload.kdnab', rawPayload);
1809
+ payload = parsePayloadEntry("payload.kdnab", rawPayload);
1797
1810
  } catch (e) {
1798
- throw new Error(`payload.kdnab is not valid JSON: ${e.message}`, { cause: e });
1811
+ throw new Error(`payload.kdnab is not valid KDNA payload: ${e.message}`, { cause: e });
1799
1812
  }
1800
1813
 
1801
1814
  if (payload.profile && payload.ciphertext && (m.payload?.encrypted || m.encryption?.encrypted_entries?.includes('payload.kdnab'))) {
@@ -1812,7 +1825,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
1812
1825
  const decryptedBuf = typeof decrypted === 'string'
1813
1826
  ? Buffer.from(decrypted, 'utf8')
1814
1827
  : Buffer.from(decrypted);
1815
- payload = parseJsonEntry('payload.kdnab', decryptedBuf);
1828
+ payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
1816
1829
  } catch (e) {
1817
1830
  const err = new Error(`Decryption failed: ${e.message}`);
1818
1831
  err.code = 'KDNA_DECRYPT_FAILED';
@@ -1827,8 +1840,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
1827
1840
  decryptProtectedEntryScrypt,
1828
1841
  PASSWORD_PROTECTED_SCRYPT_PROFILE,
1829
1842
  } = require('../crypto-profile.js');
1830
- const encryptedEnvelope = v1.map['payload.kdnab'].toString('utf8');
1831
- const envelope = JSON.parse(encryptedEnvelope);
1843
+ const encryptedEnvelope = v1.map['payload.kdnab'];
1844
+ let envelope;
1845
+ try { envelope = cbor.decode(Buffer.isBuffer(encryptedEnvelope) ? encryptedEnvelope : Buffer.from(encryptedEnvelope)); }
1846
+ catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (kdna_version 1.0)'); }
1832
1847
  const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
1833
1848
  ? decryptProtectedEntryScrypt
1834
1849
  : decryptProtectedEntry;
@@ -1837,7 +1852,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
1837
1852
  manifest: m,
1838
1853
  password: opts.password,
1839
1854
  });
1840
- payload = parseJsonEntry('payload.kdnab', decryptedBuf);
1855
+ payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
1841
1856
  } catch (e) {
1842
1857
  const err = new Error(`Decryption failed with provided password: ${e.message}`);
1843
1858
  err.code = 'KDNA_DECRYPT_FAILED';
@@ -1938,8 +1953,9 @@ function loadV1Unsafe(inputPath, opts = {}) {
1938
1953
  resolvedDependencies: [],
1939
1954
  extendsChain: [],
1940
1955
  });
1941
- if (baseLoaded && baseLoaded.content) {
1942
- const bc = baseLoaded.content;
1956
+ const baseContent = baseLoaded && (baseLoaded.content || baseLoaded.context);
1957
+ if (baseContent) {
1958
+ const bc = baseContent;
1943
1959
  const cc = result.content;
1944
1960
  // Merge axioms: child overrides by id; parent axioms not in child are added
1945
1961
  if (bc.axioms || cc.axioms) {
@@ -2048,9 +2064,65 @@ function loadV1Unsafe(inputPath, opts = {}) {
2048
2064
  return { status: result.status, profile: result.profile, text: textOut };
2049
2065
  }
2050
2066
 
2067
+ if (as === 'prompt') {
2068
+ return result;
2069
+ }
2070
+
2071
+ return wrapAsCapsule(result, v1, profile, opts);
2072
+ }
2073
+
2074
+ function wrapAsCapsule(result, v1, profile, opts) {
2075
+ if (opts.as === 'json') {
2076
+ return buildCapsule(result, v1, profile, opts);
2077
+ }
2051
2078
  return result;
2052
2079
  }
2053
2080
 
2081
+ function buildCapsule(loadResult, v1, profile, opts = {}) {
2082
+ const m = v1.manifest;
2083
+ const val = opts._validation || {};
2084
+ const assetDigest = v1.assetDigest || (v1.map && v1.map['checksums.json']
2085
+ ? (() => {
2086
+ try {
2087
+ const checks = JSON.parse(v1.map['checksums.json'].toString('utf8'));
2088
+ return checks.asset_digest || null;
2089
+ } catch { return null; }
2090
+ })()
2091
+ : null);
2092
+
2093
+ const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
2094
+ const sigVerified = val.signature_valid === true;
2095
+ const capsule = {
2096
+ type: 'kdna.context.capsule',
2097
+ version: '1.0',
2098
+ domain: m.name || m.asset_id || null,
2099
+ judgment_version: m.judgment_version || null,
2100
+ asset_digest: assetDigest,
2101
+ signature: pubkey
2102
+ ? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
2103
+ : { state: 'absent' },
2104
+ access: m.access || 'public',
2105
+ risk_level: m.risk_level || null,
2106
+ profile: loadResult.profile,
2107
+ context: loadResult.content || {},
2108
+ trace: {
2109
+ payload_encoding: (m.payload && m.payload.encoding) || 'cbor',
2110
+ loaded_by: 'kdna-core',
2111
+ loaded_at: new Date().toISOString(),
2112
+ schema_valid: val.schema_valid === true,
2113
+ signature_state: sigVerified ? 'verified' : (pubkey ? 'not_checked' : 'absent'),
2114
+ profile: profile,
2115
+ },
2116
+ };
2117
+
2118
+ if (loadResult.extends_chain) capsule.extends_chain = loadResult.extends_chain;
2119
+ if (loadResult.inheritance_applied !== undefined) capsule.inheritance_applied = loadResult.inheritance_applied;
2120
+ if (loadResult.resolved_dependencies) capsule.resolved_dependencies = loadResult.resolved_dependencies;
2121
+ if (loadResult.rag_isolation_policy) capsule.rag_isolation_policy = loadResult.rag_isolation_policy;
2122
+
2123
+ return capsule;
2124
+ }
2125
+
2054
2126
  // ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
2055
2127
 
2056
2128
  function parseSemver(v) {
package/src/v1/index.mjs CHANGED
@@ -1,21 +1,20 @@
1
1
  import v1 from './index.js';
2
2
 
3
3
  export const MIMETYPE = v1.MIMETYPE;
4
- export const MIMETYPE_V1 = v1.MIMETYPE_V1;
5
- export const MIMETYPE_V2 = v1.MIMETYPE_V2;
6
- export const V1_REQUIRED_DIR_ENTRIES = v1.V1_REQUIRED_DIR_ENTRIES;
7
- export const isV1SourceDir = v1.isV1SourceDir;
8
- export const isV2SourceDir = v1.isV2SourceDir;
4
+ export const REQUIRED_DIR_ENTRIES = v1.REQUIRED_DIR_ENTRIES;
5
+ export const isKdnaSourceDir = v1.isKdnaSourceDir;
9
6
  export const detectContainerFormat = v1.detectContainerFormat;
10
- export const readV1Layout = v1.readV1Layout;
7
+ export const readLayout = v1.readLayout;
11
8
  export const inspect = v1.inspect;
12
9
  export const validate = v1.validate;
13
10
  export const planLoad = v1.planLoad;
14
11
  export const loadAuthorized = v1.loadAuthorized;
15
- export const buildChecksumsV1 = v1.buildChecksumsV1;
12
+ export const buildChecksums = v1.buildChecksums;
16
13
  export const pack = v1.pack;
17
14
  export const unpack = v1.unpack;
18
- export const loadV1 = v1.loadV1;
15
+ export const load = v1.load;
16
+ export const loadAsset = v1.loadAsset;
17
+ export const buildCapsule = v1.buildCapsule;
19
18
  export const FORBIDDEN_OUTPUT_TERMS = v1.FORBIDDEN_OUTPUT_TERMS;
20
19
 
21
20
  export default v1;