@aikdna/kdna-core 0.16.0 → 0.18.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/v1/index.js CHANGED
@@ -228,9 +228,13 @@ function detectContainerFormat(absPath) {
228
228
  * truncated input.
229
229
  */
230
230
  function listZipEntries(absPath, opts = {}) {
231
+ return listZipEntriesFromBuffer(fs.readFileSync(absPath), opts);
232
+ }
233
+
234
+ function listZipEntriesFromBuffer(input, opts = {}) {
231
235
  const secure = opts.secure !== false;
232
236
  const limits = { ...DEFAULT_CONTAINER_LIMITS, ...(opts.limits || {}) };
233
- const buf = fs.readFileSync(absPath);
237
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
234
238
  if (secure && buf.length > limits.maxContainerBytes) {
235
239
  throw new Error(`container exceeds maximum size (${limits.maxContainerBytes} bytes)`);
236
240
  }
@@ -533,6 +537,7 @@ function readV1Layout(absPath) {
533
537
  const map = {};
534
538
  let entries = null; // ZIP entries if container
535
539
  let kind; // 'dir' | 'file'
540
+ let containerBytes = null;
536
541
 
537
542
  if (stat.isDirectory()) {
538
543
  kind = 'dir';
@@ -558,13 +563,10 @@ function readV1Layout(absPath) {
558
563
  }
559
564
  } else if (stat.isFile()) {
560
565
  kind = 'file';
561
- entries = listZipEntries(absPath);
562
- if (entries.length === 0 || entries[0].name !== 'mimetype') {
563
- throw new Error('not a KDNA Asset Container: first entry is not mimetype');
564
- }
565
- if (entries[0].method !== 0) {
566
- throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
567
- }
566
+ // Open once so parsing and the whole-file digest come from the same
567
+ // immutable snapshot, without a path-level TOCTOU window.
568
+ containerBytes = fs.readFileSync(absPath);
569
+ entries = listZipEntriesFromBuffer(containerBytes);
568
570
  for (const e of entries) {
569
571
  // We only need the well-known entries; signatures/ attachments/ etc.
570
572
  // are passed through unchanged by the loader but not parsed here.
@@ -577,25 +579,36 @@ function readV1Layout(absPath) {
577
579
  map[e.name] = e.data;
578
580
  }
579
581
  }
580
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
581
- if (!map[f]) {
582
- throw new Error(`not a KDNA Asset Container: missing ${f}`);
583
- }
584
- }
585
582
  } else {
586
583
  throw new Error(`not a file or directory: ${absPath}`);
587
584
  }
588
585
 
589
- // mimetype content must equal the literal v1 media type.
586
+ const containerDigest = kind === 'file'
587
+ ? `sha256:${crypto.createHash('sha256').update(containerBytes).digest('hex')}`
588
+ : null;
589
+
590
+ return finalizeV1Layout({ kind, map, entries, containerDigest });
591
+ }
592
+
593
+ function finalizeV1Layout({ kind, map, entries, containerDigest }) {
594
+ if (kind !== 'dir' && (!entries || entries.length === 0 || entries[0].name !== 'mimetype')) {
595
+ throw new Error('not a KDNA Asset Container: first entry is not mimetype');
596
+ }
597
+ if (kind !== 'dir' && entries[0].method !== 0) {
598
+ throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
599
+ }
600
+ for (const required of V1_REQUIRED_DIR_ENTRIES) {
601
+ if (!map[required]) {
602
+ const source = kind === 'dir' ? 'KDNA authoring source directory' : 'KDNA Asset Container';
603
+ throw new Error(`not a ${source}: missing ${required}`);
604
+ }
605
+ }
606
+
590
607
  const mime = map.mimetype.toString('utf8');
591
608
  if (mime !== MIMETYPE) {
592
- throw new Error(
593
- `not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`,
594
- );
609
+ throw new Error(`not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`);
595
610
  }
596
611
 
597
- // Lineage must be a single object, not an array. (Format rule from
598
- // docs/core/manifest.md / schema/manifest.schema.json.)
599
612
  let manifest;
600
613
  try {
601
614
  manifest = parseJsonEntry('kdna.json', map['kdna.json']);
@@ -606,7 +619,60 @@ function readV1Layout(absPath) {
606
619
  throw new Error('kdna.json.lineage must be an object, not an array');
607
620
  }
608
621
 
609
- return { kind, map, manifest, entries };
622
+ return { kind, map, manifest, entries, containerDigest };
623
+ }
624
+
625
+ function readV1LayoutBytes(input) {
626
+ const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
627
+ const entries = listZipEntriesFromBuffer(bytes);
628
+ const map = {};
629
+ for (const entry of entries) {
630
+ if (
631
+ entry.name === 'mimetype' ||
632
+ entry.name === 'kdna.json' ||
633
+ entry.name === 'payload.kdnab' ||
634
+ entry.name === 'checksums.json'
635
+ ) {
636
+ map[entry.name] = entry.data;
637
+ }
638
+ }
639
+ return finalizeV1Layout({
640
+ kind: 'memory',
641
+ map,
642
+ entries,
643
+ containerDigest: `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`,
644
+ });
645
+ }
646
+
647
+ function readInputLayout(input) {
648
+ if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
649
+ return readV1LayoutBytes(input);
650
+ }
651
+ return readV1Layout(path.resolve(input));
652
+ }
653
+
654
+ function snapshotPackagedInput(input) {
655
+ if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
656
+ return Buffer.from(input);
657
+ }
658
+ if (typeof input !== 'string') return input;
659
+ const resolved = path.resolve(input);
660
+ let stat;
661
+ try {
662
+ stat = fs.statSync(resolved);
663
+ } catch {
664
+ return input;
665
+ }
666
+ // Authoring directories remain available to inspect/validate. Runtime
667
+ // wrappers reject them; this helper only freezes packaged-file loads.
668
+ return stat.isFile() ? fs.readFileSync(resolved) : input;
669
+ }
670
+
671
+ function inputSource(input, kind) {
672
+ return {
673
+ kind,
674
+ path: typeof input === 'string' ? path.resolve(input) : null,
675
+ };
610
676
  }
611
677
 
612
678
  function parseJsonEntry(name, bytes, opts = {}) {
@@ -739,6 +805,21 @@ function runValidate(v1) {
739
805
  }
740
806
  }
741
807
 
808
+ // Known pre-1.0 aliases are not extension fields. Rejecting them here keeps
809
+ // the current v1 validator as the single manifest authority while allowing
810
+ // genuinely namespaced/forward-compatible manifest extensions.
811
+ const removedAliases = {
812
+ kdna_spec: 'kdna_version',
813
+ format_version: 'kdna_version',
814
+ spec_version: 'kdna_version',
815
+ };
816
+ for (const [field, replacement] of Object.entries(removedAliases)) {
817
+ if (Object.prototype.hasOwnProperty.call(v1.manifest, field)) {
818
+ result.schema_valid = false;
819
+ problems.push(`kdna.json: ${field} is not allowed. Use ${replacement}.`);
820
+ }
821
+ }
822
+
742
823
  // payload gate — payload.kdnab against payload-profile-v1.schema.json
743
824
  // For actually encrypted payloads (envelopes), skip schema validation.
744
825
  let payload;
@@ -828,6 +909,43 @@ function runValidate(v1) {
828
909
  }
829
910
 
830
911
  function verifyDigests(checksums, map, problems, result) {
912
+ const digestMetadataPresent =
913
+ checksums.digest_profile !== undefined || checksums.covered_entries !== undefined;
914
+ if (digestMetadataPresent) {
915
+ const requiredDigestFields = [
916
+ 'digest_profile',
917
+ 'covered_entries',
918
+ 'manifest_digest',
919
+ 'payload_digest',
920
+ 'entry_set_digest',
921
+ ];
922
+ const missingDigestFields = requiredDigestFields.filter(
923
+ (field) => checksums[field] === undefined,
924
+ );
925
+ if (missingDigestFields.length) {
926
+ problems.push(
927
+ `checksums: kdna-runtime-entry-set-v1 metadata requires ${missingDigestFields.join(', ')}`,
928
+ );
929
+ result.checksums_valid = false;
930
+ return false;
931
+ }
932
+ if (checksums.digest_profile !== 'kdna-runtime-entry-set-v1') {
933
+ problems.push('checksums: unsupported digest_profile (expected kdna-runtime-entry-set-v1)');
934
+ result.checksums_valid = false;
935
+ return false;
936
+ }
937
+ const coveredEntries = checksums.covered_entries;
938
+ if (
939
+ !Array.isArray(coveredEntries)
940
+ || coveredEntries.length !== 2
941
+ || coveredEntries[0] !== 'kdna.json'
942
+ || coveredEntries[1] !== 'payload.kdnab'
943
+ ) {
944
+ problems.push('checksums: covered_entries does not match kdna-runtime-entry-set-v1');
945
+ result.checksums_valid = false;
946
+ return false;
947
+ }
948
+ }
831
949
  const algo = checksums.algorithm || 'sha256';
832
950
  if (algo !== 'sha256') {
833
951
  problems.push(`checksums: unsupported digest algorithm ${algo} (supported: sha256)`);
@@ -857,21 +975,30 @@ function verifyDigests(checksums, map, problems, result) {
857
975
  stillValid = false;
858
976
  }
859
977
  }
860
- if (checksums.asset_digest) {
861
- const expectedAssetDigest = checksums.asset_digest.replace(/^sha256:/, '');
978
+ if (
979
+ checksums.entry_set_digest
980
+ && checksums.asset_digest
981
+ && checksums.entry_set_digest !== checksums.asset_digest
982
+ ) {
983
+ problems.push('checksums: entry_set_digest does not match deprecated asset_digest alias');
984
+ stillValid = false;
985
+ }
986
+ const declaredEntrySetDigest = checksums.entry_set_digest || checksums.asset_digest;
987
+ if (declaredEntrySetDigest) {
988
+ const digestLabel = checksums.entry_set_digest ? 'entry_set_digest' : 'asset_digest';
989
+ const expectedEntrySetDigest = declaredEntrySetDigest.replace(/^sha256:/, '');
862
990
  const missingAssetInputs = ['kdna.json', 'payload.kdnab'].filter((entryName) => !entryDigests[entryName]);
863
991
  if (missingAssetInputs.length) {
864
- problems.push(`checksums: asset_digest cannot be verified without ${missingAssetInputs.join(', ')}`);
992
+ problems.push(`checksums: ${digestLabel} cannot be verified without ${missingAssetInputs.join(', ')}`);
865
993
  stillValid = false;
866
994
  } else {
867
- const combined = Object.keys(entryDigests)
868
- .sort()
869
- .map((name) => `${name}:${entryDigests[name]}`)
870
- .join('\n');
871
- const actualAssetDigest = crypto.createHash('sha256').update(combined).digest('hex');
872
- if (actualAssetDigest !== expectedAssetDigest) {
995
+ const actualEntrySetDigest = computeRuntimeEntrySetDigest(
996
+ map['kdna.json'],
997
+ map['payload.kdnab'],
998
+ ).slice('sha256:'.length);
999
+ if (actualEntrySetDigest !== expectedEntrySetDigest) {
873
1000
  problems.push(
874
- `checksums: asset_digest mismatch (declared ${expectedAssetDigest.slice(0, 8)}..., actual ${actualAssetDigest.slice(0, 8)}...)`,
1001
+ `checksums: ${digestLabel} mismatch (declared ${expectedEntrySetDigest.slice(0, 8)}..., actual ${actualEntrySetDigest.slice(0, 8)}...)`,
875
1002
  );
876
1003
  stillValid = false;
877
1004
  }
@@ -904,6 +1031,28 @@ function digestEntry(sourceDir, entry) {
904
1031
  };
905
1032
  }
906
1033
 
1034
+ function computeRuntimeEntrySetDigest(manifestBytes, payloadBytes) {
1035
+ if (
1036
+ (!Buffer.isBuffer(manifestBytes) && !(manifestBytes instanceof Uint8Array))
1037
+ || (!Buffer.isBuffer(payloadBytes) && !(payloadBytes instanceof Uint8Array))
1038
+ ) {
1039
+ const error = new Error(
1040
+ 'E requires raw uncompressed kdna.json and payload.kdnab bytes.',
1041
+ );
1042
+ error.code = 'KDNA_DIGEST_INPUT_INVALID';
1043
+ throw error;
1044
+ }
1045
+ const entries = {
1046
+ 'kdna.json': crypto.createHash('sha256').update(manifestBytes).digest('hex'),
1047
+ 'payload.kdnab': crypto.createHash('sha256').update(payloadBytes).digest('hex'),
1048
+ };
1049
+ const combined = Object.keys(entries)
1050
+ .sort()
1051
+ .map((name) => `${name}:${entries[name]}`)
1052
+ .join('\n');
1053
+ return `sha256:${crypto.createHash('sha256').update(combined).digest('hex')}`;
1054
+ }
1055
+
907
1056
  function buildChecksumsV1(sourceDir) {
908
1057
  const absSrc = path.resolve(sourceDir);
909
1058
  if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
@@ -914,16 +1063,18 @@ function buildChecksumsV1(sourceDir) {
914
1063
  'kdna.json': digestEntry(absSrc, 'kdna.json'),
915
1064
  'payload.kdnab': digestEntry(absSrc, 'payload.kdnab'),
916
1065
  };
917
- const combined = Object.keys(entries)
918
- .sort()
919
- .map((name) => `${name}:${entries[name].value}`)
920
- .join('\n');
921
-
1066
+ const entrySetDigest = computeRuntimeEntrySetDigest(
1067
+ fs.readFileSync(path.join(absSrc, 'kdna.json')),
1068
+ fs.readFileSync(path.join(absSrc, 'payload.kdnab')),
1069
+ );
922
1070
  return {
1071
+ digest_profile: 'kdna-runtime-entry-set-v1',
1072
+ covered_entries: ['kdna.json', 'payload.kdnab'],
923
1073
  algorithm: 'sha256',
924
1074
  manifest_digest: `sha256:${entries['kdna.json'].value}`,
925
1075
  payload_digest: `sha256:${entries['payload.kdnab'].value}`,
926
- asset_digest: `sha256:${crypto.createHash('sha256').update(combined).digest('hex')}`,
1076
+ entry_set_digest: entrySetDigest,
1077
+ asset_digest: entrySetDigest,
927
1078
  entries,
928
1079
  };
929
1080
  }
@@ -1061,7 +1212,7 @@ function unpack(inputPath, outputDir) {
1061
1212
  // ─── Public router entry points ────────────────────────────────────────
1062
1213
 
1063
1214
  function inspect(inputPath, _opts = {}) {
1064
- const v1 = readV1Layout(path.resolve(inputPath));
1215
+ const v1 = readInputLayout(inputPath);
1065
1216
  const out = buildInspectOutput(v1);
1066
1217
  // Guard against accidental forbidden wording in any future field additions.
1067
1218
  assertNoForbiddenTerms(out);
@@ -1069,7 +1220,7 @@ function inspect(inputPath, _opts = {}) {
1069
1220
  }
1070
1221
 
1071
1222
  function validate(inputPath, _opts = {}) {
1072
- const v1 = readV1Layout(path.resolve(inputPath));
1223
+ const v1 = readInputLayout(inputPath);
1073
1224
  return runValidate(v1);
1074
1225
  }
1075
1226
 
@@ -1178,7 +1329,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1178
1329
  issues: [],
1179
1330
  source: {
1180
1331
  kind: v1.kind,
1181
- path: path.resolve(inputPath),
1332
+ path: typeof inputPath === 'string' ? path.resolve(inputPath) : null,
1182
1333
  },
1183
1334
  };
1184
1335
 
@@ -1218,6 +1369,7 @@ function canonicalToV1Layout(asset) {
1218
1369
  map,
1219
1370
  manifest: asset.manifest,
1220
1371
  entries: Object.keys(map),
1372
+ containerDigest: asset.containerDigest || null,
1221
1373
  };
1222
1374
  }
1223
1375
 
@@ -1229,9 +1381,26 @@ function canonicalToV1Layout(asset) {
1229
1381
  function planLoad(inputPath, opts = {}) {
1230
1382
  let v1;
1231
1383
 
1384
+ if (Buffer.isBuffer(inputPath) || inputPath instanceof Uint8Array) {
1385
+ try {
1386
+ v1 = readInputLayout(inputPath);
1387
+ } catch (e) {
1388
+ return finalizeLoadPlan({
1389
+ kdna_version: null,
1390
+ asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
1391
+ access: null, access_alias: null, entitlement_profile: null,
1392
+ state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
1393
+ input_fingerprint: null,
1394
+ checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
1395
+ issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
1396
+ source: inputSource(inputPath, 'memory'),
1397
+ });
1398
+ }
1399
+ }
1400
+
1232
1401
  // Try the unified container dispatcher first; the internal compatibility
1233
1402
  // reader below remains available only for authoring and migration tooling.
1234
- if (readAsset !== null) {
1403
+ if (!v1 && readAsset !== null) {
1235
1404
  try {
1236
1405
  const asset = readAsset(path.resolve(inputPath));
1237
1406
  v1 = canonicalToV1Layout(asset);
@@ -1251,7 +1420,7 @@ function planLoad(inputPath, opts = {}) {
1251
1420
  e.code === 'KDNA_FORMAT_UNKNOWN' ? 'KDNA_FORMAT_UNKNOWN' : 'KDNA_FORMAT_INVALID',
1252
1421
  'blocking', e.message,
1253
1422
  )],
1254
- source: { kind: null, path: path.resolve(inputPath) },
1423
+ source: inputSource(inputPath, null),
1255
1424
  });
1256
1425
  }
1257
1426
  }
@@ -1259,7 +1428,7 @@ function planLoad(inputPath, opts = {}) {
1259
1428
 
1260
1429
  if (!v1) {
1261
1430
  try {
1262
- v1 = readV1Layout(path.resolve(inputPath));
1431
+ v1 = readInputLayout(inputPath);
1263
1432
  } catch (e) {
1264
1433
  return finalizeLoadPlan({
1265
1434
  kdna_version: null,
@@ -1269,7 +1438,7 @@ function planLoad(inputPath, opts = {}) {
1269
1438
  input_fingerprint: null,
1270
1439
  checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
1271
1440
  issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
1272
- source: { kind: null, path: path.resolve(inputPath) },
1441
+ source: inputSource(inputPath, null),
1273
1442
  });
1274
1443
  }
1275
1444
  }
@@ -1714,6 +1883,7 @@ module.exports = {
1714
1883
  planLoad,
1715
1884
  loadAuthorized,
1716
1885
  buildChecksums: buildChecksumsV1,
1886
+ computeRuntimeEntrySetDigest,
1717
1887
  pack,
1718
1888
  unpack,
1719
1889
  load: loadV1,
@@ -1806,7 +1976,11 @@ function sanitizePrompt(s) {
1806
1976
  }
1807
1977
 
1808
1978
  function loadAuthorized(inputPath, opts = {}) {
1809
- const plan = planLoad(inputPath, opts);
1979
+ // A packaged path is read exactly once. Authorization and projection must
1980
+ // operate on the same immutable byte snapshot even if the path changes
1981
+ // while this synchronous call is in progress.
1982
+ const inputSnapshot = snapshotPackagedInput(inputPath);
1983
+ const plan = planLoad(inputSnapshot, opts);
1810
1984
  if (plan.can_load_now !== true) {
1811
1985
  const issueCodes = Array.isArray(plan.issues)
1812
1986
  ? plan.issues.map((issue) => issue.code).filter(Boolean)
@@ -1827,7 +2001,7 @@ function loadAuthorized(inputPath, opts = {}) {
1827
2001
  signature_valid: plan.signature_valid === true,
1828
2002
  },
1829
2003
  };
1830
- return loadV1Unsafe(inputPath, mergedOpts);
2004
+ return loadV1Unsafe(inputSnapshot, mergedOpts);
1831
2005
  }
1832
2006
 
1833
2007
  function loadV1(inputPath, opts = {}) {
@@ -1866,8 +2040,36 @@ function normalizeCompactAxiom(axiom) {
1866
2040
  };
1867
2041
  }
1868
2042
 
2043
+ function judgmentRoleExclusions(role) {
2044
+ if (!role || typeof role !== 'object' || Array.isArray(role)) return [];
2045
+ if (Array.isArray(role.does_not_act_as)) return role.does_not_act_as;
2046
+ return role.does_not_act_as ? [role.does_not_act_as] : [];
2047
+ }
2048
+
2049
+ function hasStandardJudgmentRoleContent(role) {
2050
+ if (!role || typeof role !== 'object' || Array.isArray(role)) return false;
2051
+ return Boolean(
2052
+ role.acts_as || judgmentRoleExclusions(role).length || role.responsibility,
2053
+ );
2054
+ }
2055
+
2056
+ function hasCompactPromptContent(content) {
2057
+ if (!content) return false;
2058
+ return Boolean(
2059
+ content.highest_question ||
2060
+ (Array.isArray(content.worldview) && content.worldview.length > 0) ||
2061
+ (Array.isArray(content.value_order) && content.value_order.length > 0) ||
2062
+ hasStandardJudgmentRoleContent(content.judgment_role) ||
2063
+ (Array.isArray(content.axioms) && content.axioms.length > 0) ||
2064
+ (Array.isArray(content.boundaries) && content.boundaries.length > 0) ||
2065
+ (Array.isArray(content.self_checks) && content.self_checks.length > 0) ||
2066
+ (Array.isArray(content.failure_modes) && content.failure_modes.length > 0) ||
2067
+ (Array.isArray(content.patterns) && content.patterns.length > 0)
2068
+ );
2069
+ }
2070
+
1869
2071
  function loadV1Unsafe(inputPath, opts = {}) {
1870
- const v1 = readV1Layout(path.resolve(inputPath));
2072
+ const v1 = readInputLayout(inputPath);
1871
2073
  const m = v1.manifest;
1872
2074
  const profile = opts.profile || (m.load_contract ? m.load_contract.default_profile : 'compact') || 'compact';
1873
2075
  const as = opts.as || 'json';
@@ -1964,6 +2166,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
1964
2166
  const core = payload.core || {};
1965
2167
  const hasJudgment = (core.axioms && core.axioms.length > 0)
1966
2168
  || (core.boundaries && core.boundaries.length > 0)
2169
+ || Boolean(core.highest_question)
2170
+ || (Array.isArray(core.worldview) && core.worldview.length > 0)
2171
+ || (Array.isArray(core.value_order) && core.value_order.length > 0)
2172
+ || hasStandardJudgmentRoleContent(core.judgment_role)
1967
2173
  || (payload.patterns && payload.patterns.length > 0)
1968
2174
  || (payload.reasoning && ((payload.reasoning.self_check && payload.reasoning.self_check.length > 0) || (payload.reasoning.failure_modes && payload.reasoning.failure_modes.length > 0)));
1969
2175
  profiles.push('index');
@@ -1998,6 +2204,17 @@ function loadV1Unsafe(inputPath, opts = {}) {
1998
2204
  }).filter(Boolean);
1999
2205
  result.content = {
2000
2206
  highest_question: core.highest_question || null,
2207
+ // These scoped domain-level values are already validated by the payload
2208
+ // schema. Compact projection copies them without trimming, reordering,
2209
+ // filtering, or converting a declared string into another shape.
2210
+ worldview: Array.isArray(core.worldview) ? [...core.worldview] : [],
2211
+ value_order: Array.isArray(core.value_order) ? [...core.value_order] : [],
2212
+ judgment_role:
2213
+ core.judgment_role &&
2214
+ typeof core.judgment_role === 'object' &&
2215
+ !Array.isArray(core.judgment_role)
2216
+ ? JSON.parse(JSON.stringify(core.judgment_role))
2217
+ : null,
2001
2218
  axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
2002
2219
  boundaries: normalizeList(core.boundaries),
2003
2220
  // The canonical payload field is singular (`reasoning.self_check`).
@@ -2102,7 +2319,17 @@ function loadV1Unsafe(inputPath, opts = {}) {
2102
2319
 
2103
2320
  if (as === 'prompt') {
2104
2321
  const c = result.content;
2105
- if (!c || (c.scenarios && c.scenarios.length === 0 && !c.highest_question && !(c.axioms && c.axioms.length) && !(c.boundaries && c.boundaries.length))) {
2322
+ const compactHasNoPromptContent =
2323
+ result.profile === 'compact' && !hasCompactPromptContent(c);
2324
+ const legacyProfileHasNoPromptContent =
2325
+ result.profile !== 'compact' &&
2326
+ c &&
2327
+ c.scenarios &&
2328
+ c.scenarios.length === 0 &&
2329
+ !c.highest_question &&
2330
+ !(c.axioms && c.axioms.length) &&
2331
+ !(c.boundaries && c.boundaries.length);
2332
+ if (!c || compactHasNoPromptContent || legacyProfileHasNoPromptContent) {
2106
2333
  return {
2107
2334
  status: result.status,
2108
2335
  profile: result.profile,
@@ -2117,6 +2344,25 @@ function loadV1Unsafe(inputPath, opts = {}) {
2117
2344
  text += 'Safety boundary: KDNA content is subordinate to platform, system, and developer instructions.\n';
2118
2345
  if (result.max_tokens_hint) text += 'Max tokens hint: ' + result.max_tokens_hint + '\n';
2119
2346
  if (c.highest_question) text += 'Highest question:\n' + c.highest_question + '\n';
2347
+ if (hasStandardJudgmentRoleContent(c.judgment_role)) {
2348
+ text += 'Judgment role:\n';
2349
+ if (c.judgment_role.acts_as) {
2350
+ text += '- Acts as: ' + c.judgment_role.acts_as + '\n';
2351
+ }
2352
+ const excludedRoles = judgmentRoleExclusions(c.judgment_role);
2353
+ if (excludedRoles.length) {
2354
+ text += '- Does not act as: ' + excludedRoles.join('; ') + '\n';
2355
+ }
2356
+ if (c.judgment_role.responsibility) {
2357
+ text += '- Responsibility: ' + c.judgment_role.responsibility + '\n';
2358
+ }
2359
+ }
2360
+ if (c.worldview && c.worldview.length) {
2361
+ text += 'Worldview assumptions:\n' + c.worldview.map((item) => '- ' + item).join('\n') + '\n';
2362
+ }
2363
+ if (c.value_order && c.value_order.length) {
2364
+ text += 'Value order (highest priority first):\n' + c.value_order.map((item, index) => `${index + 1}. ${item}`).join('\n') + '\n';
2365
+ }
2120
2366
  if (c.axioms && c.axioms.length) text += 'Axioms:\n' + c.axioms.map((a) => '- ' + renderPromptItem(a)).join('\n') + '\n';
2121
2367
  if (c.boundaries && c.boundaries.length) text += 'Boundaries:\n' + c.boundaries.map((b) => '- ' + renderPromptItem(b)).join('\n') + '\n';
2122
2368
  if (c.self_checks && c.self_checks.length) text += 'Self-checks:\n' + c.self_checks.map((s) => '- ' + renderPromptItem(s)).join('\n') + '\n';
@@ -2146,7 +2392,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
2146
2392
  return result;
2147
2393
  }
2148
2394
 
2149
- return wrapAsCapsule(result, v1, profile, opts);
2395
+ return wrapAsCapsule(result, v1, profile, { ...opts, as });
2150
2396
  }
2151
2397
 
2152
2398
  function wrapAsCapsule(result, v1, profile, opts) {
@@ -2159,14 +2405,10 @@ function wrapAsCapsule(result, v1, profile, opts) {
2159
2405
  function buildCapsule(loadResult, v1, profile, opts = {}) {
2160
2406
  const m = v1.manifest;
2161
2407
  const val = opts._validation || {};
2162
- const assetDigest = v1.assetDigest || (v1.map && v1.map['checksums.json']
2163
- ? (() => {
2164
- try {
2165
- const checks = JSON.parse(v1.map['checksums.json'].toString('utf8'));
2166
- return checks.asset_digest || null;
2167
- } catch { return null; }
2168
- })()
2169
- : null);
2408
+ const assetDigest = computeRuntimeEntrySetDigest(
2409
+ v1.map && v1.map['kdna.json'],
2410
+ v1.map && v1.map['payload.kdnab'],
2411
+ );
2170
2412
 
2171
2413
  const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
2172
2414
  const sigVerified = val.signature_valid === true;
package/src/v1/index.mjs CHANGED
@@ -10,6 +10,7 @@ export const validate = v1.validate;
10
10
  export const planLoad = v1.planLoad;
11
11
  export const loadAuthorized = v1.loadAuthorized;
12
12
  export const buildChecksums = v1.buildChecksums;
13
+ export const computeRuntimeEntrySetDigest = v1.computeRuntimeEntrySetDigest;
13
14
  export const pack = v1.pack;
14
15
  export const unpack = v1.unpack;
15
16
  export const load = v1.load;