@aikdna/kdna-core 0.15.5 → 0.15.6

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.15.6 (2026-06-28)
4
+
5
+ Story 5 — Bundle payload type. Bumps KDNA manifest format schema to v2, introduces bundle-profile-v1 payload profile, and starts KDNA v1 format deprecation window.
6
+
7
+ - **Bundle asset_type support:** added `"bundle"` to `asset_type` enum and `VALID_ASSET_TYPE` check.
8
+ - **V2 Schema support:** bumped manifest schema version to v2.0, supporting `kdna_version` as `"2.0"`.
9
+ - **Bundle profile support:** added `bundle-profile-v1` payload profile and validator.
10
+
11
+
3
12
  ## v0.15.5 (2026-06-28)
4
13
 
5
14
  Phase 12 audit follow-up. Closes 3 issues filed against this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.15.5",
3
+ "version": "0.15.6",
4
4
  "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/aikdna/kdna/schema/bundle-profile-v1.schema.json",
4
+ "title": "Bundle Profile v1",
5
+ "description": "Schema for the payload entry of a KDNA bundle container.",
6
+ "type": "object",
7
+ "required": ["profile", "components"],
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "profile": {
11
+ "type": "string",
12
+ "const": "bundle-profile-v1",
13
+ "description": "Profile identifier. Must be the literal string 'bundle-profile-v1'."
14
+ },
15
+ "components": {
16
+ "type": "array",
17
+ "description": "List of component assets bundled in this container.",
18
+ "items": {
19
+ "type": "object",
20
+ "required": ["id", "path"],
21
+ "additionalProperties": true,
22
+ "properties": {
23
+ "id": {
24
+ "type": "string",
25
+ "description": "The unique identifier of the component asset."
26
+ },
27
+ "path": {
28
+ "type": "string",
29
+ "description": "Relative path to the component asset (.kdna container) within the bundle payload or bundle package."
30
+ },
31
+ "priority": {
32
+ "type": "integer",
33
+ "description": "Optional priority for resolving card conflicts."
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/aikdna/kdna/schema/manifest.schema.json",
4
- "title": "KDNA Manifest v1",
4
+ "title": "KDNA Manifest v2",
5
5
  "description": "Schema for kdna.json, the public metadata layer of a .kdna container.",
6
6
  "type": "object",
7
7
  "required": [
@@ -23,7 +23,7 @@
23
23
  "kdna_version": {
24
24
  "type": "string",
25
25
  "description": "KDNA Core version this manifest conforms to.",
26
- "const": "1.0"
26
+ "enum": ["1.0", "2.0"]
27
27
  },
28
28
  "asset_id": {
29
29
  "type": "string",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "asset_type": {
39
39
  "type": "string",
40
- "enum": ["domain", "cluster", "tool", "sample", "fixture"],
40
+ "enum": ["domain", "cluster", "tool", "sample", "fixture", "bundle"],
41
41
  "description": "What kind of asset this is. Container format is the same; the value tells callers how to interpret the payload."
42
42
  },
43
43
  "title": {
@@ -92,7 +92,7 @@
92
92
  "profile": {
93
93
  "type": "string",
94
94
  "description": "Payload profile identifier (e.g. judgment-profile-v1).",
95
- "const": "judgment-profile-v1"
95
+ "enum": ["judgment-profile-v1", "bundle-profile-v1"]
96
96
  }
97
97
  }
98
98
  },
package/src/lint-pure.js CHANGED
@@ -269,6 +269,7 @@ const VALID_ASSET_TYPE = new Set([
269
269
  'team_policy',
270
270
  'creator_style',
271
271
  'risk_guard',
272
+ 'bundle',
272
273
  ]);
273
274
 
274
275
  const MANIFEST_REQUIRED = [
package/src/types.d.ts CHANGED
@@ -504,6 +504,7 @@ export interface KDNAV1Checksums {
504
504
  }
505
505
 
506
506
  export function isV1SourceDir(inputPath: string): boolean;
507
+ export function isV2SourceDir(inputPath: string): boolean;
507
508
  export function detectContainerFormat(inputPath: string): 'v1' | 'v2' | null;
508
509
  export function inspect(inputPath: string, options?: Record<string, any>): Record<string, any>;
509
510
  export function validate(inputPath: string, options?: Record<string, any>): Record<string, any>;
package/src/v1/index.js CHANGED
@@ -125,6 +125,9 @@ function loadSchemas() {
125
125
  const payloadSchema = JSON.parse(
126
126
  fs.readFileSync(path.join(schemaDir, 'payload-profile-v1.schema.json'), 'utf8'),
127
127
  );
128
+ const bundlePayloadSchema = JSON.parse(
129
+ fs.readFileSync(path.join(schemaDir, 'bundle-profile-v1.schema.json'), 'utf8'),
130
+ );
128
131
  const checksumsSchema = JSON.parse(
129
132
  fs.readFileSync(path.join(schemaDir, 'checksums.schema.json'), 'utf8'),
130
133
  );
@@ -138,6 +141,7 @@ function loadSchemas() {
138
141
  _validators = {
139
142
  manifest: ajv.compile(manifestSchema),
140
143
  payload: ajv.compile(payloadSchema),
144
+ bundlePayload: ajv.compile(bundlePayloadSchema),
141
145
  checksums: ajv.compile(checksumsSchema),
142
146
  };
143
147
  return _validators;
@@ -159,6 +163,20 @@ function isV1SourceDir(absPath) {
159
163
  return mime === MIMETYPE_V1;
160
164
  }
161
165
 
166
+ /**
167
+ * Detect whether a directory is a v2 source layout.
168
+ * Required entries: mimetype, kdna.json, payload.kdnab.
169
+ * mimetype content must equal "application/vnd.aikdna.kdna+zip".
170
+ */
171
+ function isV2SourceDir(absPath) {
172
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
173
+ for (const f of V1_REQUIRED_DIR_ENTRIES) {
174
+ if (!fs.existsSync(path.join(absPath, f))) return false;
175
+ }
176
+ const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
177
+ return mime === MIMETYPE_V2;
178
+ }
179
+
162
180
  /**
163
181
  * Detect whether a file is a v1 .kdna container.
164
182
  * Returns 'v1' | 'v2' | null. null = not a .kdna file or unreadable.
@@ -559,9 +577,9 @@ function readV1Layout(absPath) {
559
577
 
560
578
  // mimetype content must equal the literal v1 media type.
561
579
  const mime = map.mimetype.toString('utf8');
562
- if (mime !== MIMETYPE_V1) {
580
+ if (mime !== MIMETYPE_V1 && mime !== MIMETYPE_V2) {
563
581
  throw new Error(
564
- `not a KDNA v1 layout: mimetype is "${mime}", expected "${MIMETYPE_V1}"`,
582
+ `not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE_V1}" or "${MIMETYPE_V2}"`,
565
583
  );
566
584
  }
567
585
 
@@ -668,9 +686,12 @@ function runValidate(v1) {
668
686
  problems.push(`format: missing required entry ${f}`);
669
687
  }
670
688
  }
671
- if (v1.map.mimetype && v1.map.mimetype.toString('utf8') !== MIMETYPE_V1) {
672
- result.format_valid = false;
673
- problems.push(`format: mimetype is not ${MIMETYPE_V1}`);
689
+ if (v1.map.mimetype) {
690
+ const mimeStr = v1.map.mimetype.toString('utf8');
691
+ if (mimeStr !== MIMETYPE_V1 && mimeStr !== MIMETYPE_V2) {
692
+ result.format_valid = false;
693
+ problems.push(`format: mimetype is not ${MIMETYPE_V1} or ${MIMETYPE_V2}`);
694
+ }
674
695
  }
675
696
 
676
697
  // schema gate — kdna.json against manifest.schema.json
@@ -717,11 +738,15 @@ function runValidate(v1) {
717
738
  problems.push('payload: encrypted envelope missing required fields (profile/ciphertext/key material)');
718
739
  }
719
740
  // Encrypted payload passes payload gate if envelope is structurally valid.
720
- } else if (!validators.payload(payload)) {
741
+ } else {
721
742
  // Plaintext payload — full schema validation.
722
- result.payload_valid = false;
723
- for (const err of validators.payload.errors) {
724
- problems.push(`payload: ${err.instancePath || '<root>'} ${err.message}`);
743
+ const isBundleProfile = v1.manifest.compatibility?.profile === 'bundle-profile-v1' || v1.manifest.asset_type === 'bundle';
744
+ const payloadValidator = isBundleProfile ? validators.bundlePayload : validators.payload;
745
+ if (!payloadValidator(payload)) {
746
+ result.payload_valid = false;
747
+ for (const err of payloadValidator.errors) {
748
+ problems.push(`payload: ${err.instancePath || '<root>'} ${err.message}`);
749
+ }
725
750
  }
726
751
  }
727
752
 
@@ -958,14 +983,15 @@ function unpack(inputPath, outputDir) {
958
983
  const entries = listZipEntries(absIn);
959
984
  // Sanity: v1 container must have mimetype as first entry with the v1 media type.
960
985
  if (entries.length === 0 || entries[0].name !== 'mimetype') {
961
- throw new Error('not a KDNA v1 container: first entry is not mimetype');
986
+ throw new Error('not a KDNA container: first entry is not mimetype');
962
987
  }
963
988
  if (entries[0].method !== 0) {
964
- throw new Error('not a KDNA v1 container: mimetype must be uncompressed');
989
+ throw new Error('not a KDNA container: mimetype must be uncompressed');
965
990
  }
966
- if (entries[0].data.toString('utf8') !== MIMETYPE_V1) {
991
+ const mime = entries[0].data.toString('utf8');
992
+ if (mime !== MIMETYPE_V1 && mime !== MIMETYPE_V2) {
967
993
  throw new Error(
968
- `not a KDNA v1 container: mimetype is "${entries[0].data.toString('utf8')}", expected "${MIMETYPE_V1}"`,
994
+ `not a KDNA container: mimetype is "${mime}", expected "${MIMETYPE_V1}" or "${MIMETYPE_V2}"`,
969
995
  );
970
996
  }
971
997
  const absOut = path.resolve(outputDir);
@@ -1128,6 +1154,17 @@ function canonicalToV1Layout(asset) {
1128
1154
  for (const [name, buf] of asset.entries) {
1129
1155
  map[name] = buf;
1130
1156
  }
1157
+ } else {
1158
+ map['mimetype'] = Buffer.from(asset.mimetype || '', 'utf8');
1159
+ if (asset.manifest) {
1160
+ map['kdna.json'] = Buffer.from(JSON.stringify(asset.manifest), 'utf8');
1161
+ }
1162
+ if (asset.payloadRaw) {
1163
+ map['payload.kdnab'] = asset.payloadRaw;
1164
+ }
1165
+ if (asset.checksums) {
1166
+ map['checksums.json'] = Buffer.from(JSON.stringify(asset.checksums), 'utf8');
1167
+ }
1131
1168
  }
1132
1169
  return {
1133
1170
  kind: asset.sourceKind,
@@ -1478,6 +1515,7 @@ module.exports = {
1478
1515
  MIMETYPE_V2,
1479
1516
  V1_REQUIRED_DIR_ENTRIES,
1480
1517
  isV1SourceDir,
1518
+ isV2SourceDir,
1481
1519
  detectContainerFormat,
1482
1520
  readV1Layout,
1483
1521
  inspect,
package/src/v1/index.mjs CHANGED
@@ -5,6 +5,7 @@ export const MIMETYPE_V1 = v1.MIMETYPE_V1;
5
5
  export const MIMETYPE_V2 = v1.MIMETYPE_V2;
6
6
  export const V1_REQUIRED_DIR_ENTRIES = v1.V1_REQUIRED_DIR_ENTRIES;
7
7
  export const isV1SourceDir = v1.isV1SourceDir;
8
+ export const isV2SourceDir = v1.isV2SourceDir;
8
9
  export const detectContainerFormat = v1.detectContainerFormat;
9
10
  export const readV1Layout = v1.readV1Layout;
10
11
  export const inspect = v1.inspect;