@aikdna/kdna-studio-cli 0.9.2 → 0.10.2

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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/bin/kdna-studio.js +420 -142
  3. package/package.json +13 -7
package/README.md CHANGED
@@ -125,6 +125,11 @@ A KDNA runtime export contains only these top-level entries:
125
125
  - `payload.kdnab`
126
126
  - `checksums.json`
127
127
 
128
+ The current producer writes `format_version: 0.1.0`, payload profile
129
+ `kdna.payload.judgment` with `profile_version: 0.1.0`, and digest profile
130
+ `kdna.digest-basis.runtime-entry-set` with `digest_profile_version: 0.1.0`.
131
+ These compatibility coordinates are independent of package versions.
132
+
128
133
  Authoring/source entries such as `KDNA_Core.json`, `KDNA_Patterns.json`, and
129
134
  `source_cards` are not runtime distribution entries. They may exist in Studio
130
135
  compile output or legacy imports, but they must not be emitted by the runtime
@@ -161,6 +166,16 @@ kdna-studio create forked --from-kdna ./parent.kdna --name @scope/forked
161
166
  kdna-studio create migrated --from-folder ./old-domain-json --name @scope/migrated
162
167
  ```
163
168
 
169
+ Current `.kdna` imports also preserve explicitly declared `highest_question`,
170
+ `worldview`, ordered `value_order`, and `judgment_role` values in the editable
171
+ project. Re-export writes those values back exactly; Studio does not infer a
172
+ replacement judgment core from an axiom.
173
+
174
+ Imports preserve authored source references, `core_structure` relations,
175
+ pattern subtypes, scenario and case metadata, extended reasoning fields, and
176
+ source-authored evolution. Studio-only card state and lock audit records remain
177
+ authoring provenance and are not rewritten as Runtime judgment evolution.
178
+
164
179
  ## Related
165
180
 
166
181
  - [KDNA Core](https://github.com/aikdna/kdna) — Official format specification
@@ -9,7 +9,7 @@ const { execFileSync } = require('child_process');
9
9
 
10
10
  function loadStudioCore() {
11
11
  const publishedCore = require('@aikdna/kdna-studio-core');
12
- if (publishedCore.exportRuntime) return publishedCore;
12
+ if (publishedCore.exportRuntime && publishedCore.protocolContract) return publishedCore;
13
13
  try {
14
14
  return require('../../kdna-studio-core/src');
15
15
  } catch (_) {
@@ -26,6 +26,7 @@ const {
26
26
  creator: creatorApi,
27
27
  distillation: distillationApi,
28
28
  exportRuntime,
29
+ protocolContract,
29
30
  } = loadStudioCore();
30
31
 
31
32
  const llm = require('../src/llm');
@@ -365,12 +366,23 @@ function cleanFields(fields = {}) {
365
366
  return cleaned;
366
367
  }
367
368
 
369
+ function importedFields(entry = {}, options = {}) {
370
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return {};
371
+ const fields = JSON.parse(JSON.stringify(entry));
372
+ for (const key of ['id', 'status', 'locked', 'human_lock', 'audit_log', 'source_authored']) {
373
+ delete fields[key];
374
+ }
375
+ if (options.keepType !== true) delete fields.type;
376
+ return cleanFields(fields);
377
+ }
378
+
368
379
  function importedCard(type, fields = {}, id = null) {
369
- try {
370
- return cardApi.createCard(type, cleanFields(fields), id || null);
371
- } catch (_) {
372
- return null;
380
+ const card = cardApi.createCard(type, cleanFields(fields), id || null);
381
+ if (card === null) {
382
+ throw new Error(`failed to create card type="${type}" id="${id || '(auto)'}" — ` +
383
+ `the card type may require additional required fields or the fields may be malformed`);
373
384
  }
385
+ return card;
374
386
  }
375
387
 
376
388
  // Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
@@ -400,8 +412,10 @@ function pushImportedCard(cards, type, fields = {}, id = null) {
400
412
  }
401
413
  return;
402
414
  }
403
- if (id) _importedIds.add(id);
404
415
  const card = importedCard(type, fields, id);
416
+ // Register the id only after card creation succeeds; a failed creation
417
+ // must not consume the id and prevent retry.
418
+ if (card && id) _importedIds.add(id);
405
419
  if (card) cards.push(card);
406
420
  }
407
421
 
@@ -437,31 +451,22 @@ function cardsFromRuntimePayload(payload) {
437
451
  if (cards.length > 0) return cards;
438
452
 
439
453
  for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
440
- pushImportedCard(cards, 'axiom', {
441
- one_sentence: ax.one_sentence || '',
442
- full_statement: ax.full_statement || '',
443
- why: ax.why || '',
444
- applies_when: ax.applies_when || [],
445
- does_not_apply_when: ax.does_not_apply_when || [],
446
- failure_risk: ax.failure_risk || '',
447
- confidence: ax.confidence || '',
448
- evidence_type: ax.evidence_type || '',
449
- }, ax.id || null);
454
+ pushImportedCard(cards, 'axiom', importedFields(ax), ax.id || null);
450
455
  }
451
456
  for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
452
- pushImportedCard(cards, 'ontology', ont, ont.id || null);
457
+ pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
453
458
  }
454
459
  for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
455
- pushImportedCard(cards, 'framework', fw, fw.id || null);
460
+ pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
456
461
  }
457
462
  for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
458
- pushImportedCard(cards, 'stance', stance, stance.id || null);
463
+ pushImportedCard(cards, 'stance', importedFields(stance), stance.id || null);
459
464
  }
460
465
  for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
461
- pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
466
+ pushImportedCard(cards, 'aesthetic', importedFields(aesthetic), aesthetic.id || null);
462
467
  }
463
468
  for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
464
- pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
469
+ pushImportedCard(cards, 'boundary', importedFields(boundary), boundary.id || null);
465
470
  }
466
471
  // risk_model may be either an array of risk items (legacy) or
467
472
  // `{risks: [...]}` (current). Treat both shapes; ignore everything else
@@ -469,7 +474,7 @@ function cardsFromRuntimePayload(payload) {
469
474
  const riskModel = payload.core?.risk_model;
470
475
  const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
471
476
  for (const risk of riskItems) {
472
- pushImportedCard(cards, 'risk', risk, risk.id || null);
477
+ pushImportedCard(cards, 'risk', importedFields(risk), risk.id || null);
473
478
  }
474
479
  for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
475
480
  // Bug (#146): prior version dropped any payload.patterns entry
@@ -499,32 +504,34 @@ function cardsFromRuntimePayload(payload) {
499
504
  );
500
505
  continue;
501
506
  }
502
- fields = { ...pattern };
503
- delete fields.type;
507
+ fields = importedFields(pattern);
504
508
  } else {
505
- const { type: _stripped, ...rest } = pattern;
506
- fields = rest;
509
+ fields = importedFields(pattern, { keepType: type === 'pattern' || LEGACY_PATTERN_SUBTYPES.has(type) });
510
+ }
511
+ if (LEGACY_PATTERN_SUBTYPES.has(type)) {
512
+ fields = {
513
+ ...fields,
514
+ type,
515
+ legacy_subtype: fields.legacy_subtype || type,
516
+ };
517
+ type = 'pattern';
507
518
  }
508
519
  pushImportedCard(cards, type, fields, pattern.id || null);
509
520
  }
510
521
  for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
511
- pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
522
+ pushImportedCard(cards, 'scenario', importedFields(scenario), scenario.id || null);
512
523
  }
513
524
  for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
514
- pushImportedCard(cards, 'case', item, item.id || null);
515
- }
516
- // Accept both `self_check` (canonical) and `self_checks` (legacy schema
517
- // name) so we round-trip assets written by either version of the
518
- // payload-profile schema. Bug (#53): prior version used
519
- // `arr || []`, which would crash with "object is not iterable" if
520
- // the field was a truthy non-array (e.g. a single `{question: ...}`
521
- // object instead of an array). The fix guards with Array.isArray.
522
- const rawSelfCheck = payload.reasoning?.self_check
523
- || payload.reasoning?.self_checks;
525
+ pushImportedCard(cards, 'case', importedFields(item), item.id || null);
526
+ }
527
+ // The current payload contract has one self-check field. Refuse to infer a
528
+ // parallel field name: Core validation owns format migration, while Studio
529
+ // imports only the current validated payload shape.
530
+ const rawSelfCheck = payload.reasoning?.self_check;
524
531
  const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
525
532
  for (const selfCheck of selfCheckArr) {
526
- const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
527
- pushImportedCard(cards, 'self_check', fields, fields.id || null);
533
+ const fields = typeof selfCheck === 'string' ? { question: selfCheck } : importedFields(selfCheck);
534
+ pushImportedCard(cards, 'self_check', fields, typeof selfCheck === 'object' ? selfCheck.id || null : null);
528
535
  }
529
536
  for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
530
537
  // Bug (#145 follow-up): the prior version dropped three
@@ -533,7 +540,8 @@ function cardsFromRuntimePayload(payload) {
533
540
  // project that round-tripped through `migrate` lost
534
541
  // these fields every time. The fix reads them through, matching
535
542
  // the export-side map in kdna-studio-core's buildPayload.
536
- pushImportedCard(cards, 'misunderstanding', {
543
+ const fields = {
544
+ ...importedFields(failureMode),
537
545
  wrong: failureMode.mode || failureMode.wrong || '',
538
546
  correct: failureMode.correct || '',
539
547
  key_distinction: failureMode.key_distinction || '',
@@ -541,7 +549,9 @@ function cardsFromRuntimePayload(payload) {
541
549
  failure_risk: failureMode.failure_risk || '',
542
550
  applies_when: Array.isArray(failureMode.applies_when) ? failureMode.applies_when : [],
543
551
  does_not_apply_when: Array.isArray(failureMode.does_not_apply_when) ? failureMode.does_not_apply_when : [],
544
- }, failureMode.id || null);
552
+ };
553
+ delete fields.mode;
554
+ pushImportedCard(cards, 'misunderstanding', fields, failureMode.id || null);
545
555
  }
546
556
  for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
547
557
  // Bug (#4 UX): compile/index.js synthesises one reasoning chain
@@ -550,7 +560,7 @@ function cardsFromRuntimePayload(payload) {
550
560
  // doubled the card count on round-trip. The fix skips chains
551
561
  // that the producer explicitly marked as synthesised.
552
562
  if (chain.source_authored === false) continue;
553
- pushImportedCard(cards, 'reasoning', chain, chain.id || null);
563
+ pushImportedCard(cards, 'reasoning', importedFields(chain), chain.id || null);
554
564
  }
555
565
  for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
556
566
  // Distinguish source-authored stages (preserved identity) from
@@ -559,7 +569,7 @@ function cardsFromRuntimePayload(payload) {
559
569
  // so the importer doesn't double-count what was already in the
560
570
  // locked judgment cards.
561
571
  if (stage.source_authored === true) {
562
- pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
572
+ pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
563
573
  }
564
574
  }
565
575
  return cards;
@@ -567,12 +577,27 @@ function cardsFromRuntimePayload(payload) {
567
577
 
568
578
  function cardsFromPayload(payload) {
569
579
  if (!payload || typeof payload !== 'object') return [];
570
- if (payload.profile === 'judgment-profile-v1' || payload.core || payload.source_cards) {
580
+ if (
581
+ payload.profile === protocolContract.PAYLOAD_PROFILE &&
582
+ payload.profile_version === protocolContract.PAYLOAD_PROFILE_VERSION
583
+ ) {
571
584
  return cardsFromRuntimePayload(payload);
572
585
  }
573
586
  return [];
574
587
  }
575
588
 
589
+ function judgmentCoreFromRuntimePayload(payload) {
590
+ const core = payload?.core;
591
+ if (!core || typeof core !== 'object' || Array.isArray(core)) return null;
592
+ const declared = {};
593
+ for (const field of ['highest_question', 'worldview', 'value_order', 'judgment_role']) {
594
+ if (Object.hasOwn(core, field)) {
595
+ declared[field] = JSON.parse(JSON.stringify(core[field]));
596
+ }
597
+ }
598
+ return Object.keys(declared).length > 0 ? declared : null;
599
+ }
600
+
576
601
  function buildManifest(project, name) {
577
602
  // Bug (#58): prior version was a hand-rolled manifest builder that
578
603
  // shared zero logic with the canonical buildManifest in
@@ -591,12 +616,12 @@ function buildManifest(project, name) {
591
616
  const assetId = assetIdFromName(name || source.name || project.name);
592
617
  const author = source.author || project.author || {};
593
618
  return {
594
- kdna_version: '1.0',
619
+ format_version: protocolContract.FORMAT_VERSION,
595
620
  asset_id: assetId,
596
621
  asset_uid: project.asset_uid || source.asset_uid || 'urn:uuid:' + crypto.randomUUID(),
597
622
  asset_type: project.type === 'cluster' ? 'cluster' : 'domain',
598
623
  source_asset_type: source.asset_type || null,
599
- title: titleFromName(source.name || name || project.name, assetId),
624
+ title: source.title || project.title || titleFromName(source.name || name || project.name, assetId),
600
625
  version: semverValue(project.release?.version || source.version, '1.0.0'),
601
626
  judgment_version: semverValue(project.release?.judgment_version || source.judgment_version || source.version, '1.0.0'),
602
627
  created_at: isoDateTime(source.created_at || source.created || project.created),
@@ -605,7 +630,11 @@ function buildManifest(project, name) {
605
630
  name: author.name || project.author?.name || 'Studio Export',
606
631
  id: author.id || project.author?.id || undefined,
607
632
  },
608
- compatibility: { min_loader_version: '1.0.0', profile: 'judgment-profile-v1' },
633
+ compatibility: {
634
+ min_loader_version: '0.19.0',
635
+ profile: protocolContract.PAYLOAD_PROFILE,
636
+ profile_version: protocolContract.PAYLOAD_PROFILE_VERSION,
637
+ },
609
638
  payload: { path: 'payload.kdnab', encoding: 'cbor', encrypted: false },
610
639
  summary: source.core_insight || project.release?.description || source.description || '',
611
640
  description: source.description || project.release?.description || '',
@@ -664,6 +693,7 @@ function cmdCreate(args) {
664
693
  sourceMode,
665
694
  creatorIdentity,
666
695
  lineage,
696
+ judgmentCore: kdnaData.judgment_core,
667
697
  });
668
698
  project.cards = kdnaData.cards;
669
699
  // Bug #15 / #28 follow-up: store the source KDNA_* files on the
@@ -674,7 +704,13 @@ function cmdCreate(args) {
674
704
  if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
675
705
  if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
676
706
  if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
707
+ if (kdnaData.source_core_structure) project.source_core_structure = kdnaData.source_core_structure;
677
708
  if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
709
+ if (kdnaData.source_manifest?.asset_uid) project.asset_uid = kdnaData.source_manifest.asset_uid;
710
+ if (kdnaData.source_manifest?.title) project.title = kdnaData.source_manifest.title;
711
+ if (kdnaData.source_manifest?.license) project.license = kdnaData.source_manifest.license;
712
+ if (kdnaData.source_manifest?.languages) project.languages = kdnaData.source_manifest.languages;
713
+ if (kdnaData.source_manifest?.language) project.default_language = kdnaData.source_manifest.language;
678
714
  if (kdnaData.source_manifest?.version) {
679
715
  if (!project.release) project.release = {};
680
716
  project.release.version = kdnaData.source_manifest.version;
@@ -683,9 +719,9 @@ function cmdCreate(args) {
683
719
  if (!project.release) project.release = {};
684
720
  project.release.judgment_version = kdnaData.source_manifest.judgment_version;
685
721
  }
686
- if (kdnaData.source_manifest?.description) {
722
+ if (kdnaData.source_manifest?.description || kdnaData.source_manifest?.summary) {
687
723
  if (!project.release) project.release = {};
688
- project.release.description = kdnaData.source_manifest.description;
724
+ project.release.description = kdnaData.source_manifest.description || kdnaData.source_manifest.summary;
689
725
  }
690
726
  if (project.stages?.judgment_cards) {
691
727
  project.stages.judgment_cards.total = kdnaData.cards.length;
@@ -739,19 +775,36 @@ function importFromKdna(kdnaPath) {
739
775
  if (stats.size > 50 * 1024 * 1024) {
740
776
  fail(`KDNA file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${absKdna}`);
741
777
  }
742
- let runtimeCore;
743
- try { runtimeCore = require('@aikdna/kdna-core'); } catch {
744
- fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
745
- }
746
- const validation = runtimeCore.validate(absKdna);
747
- if (!validation.overall_valid) {
748
- fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
749
- }
778
+
779
+ // ── read ZIP before Core validation so the profile can branch ──
750
780
  const zipBuf = fs.readFileSync(absKdna);
751
781
  const entries = readZipEntries(zipBuf);
782
+ validateZipContainer(entries);
752
783
  if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
784
+ if (!entries.has('payload.kdnab')) fail('Not a valid .kdna asset: missing payload.kdnab. Use --from-folder for legacy JSON source migration.');
753
785
 
754
786
  const manifest = JSON.parse(entries.get('kdna.json').toString());
787
+ const payload = decodePayload(entries.get('payload.kdnab'), manifest);
788
+ const profile = payload?.profile;
789
+
790
+ // ── branch: current profile requires Core validate; legacy uses a frozen contract ──
791
+ let runtimeCore, isLegacy;
792
+ try { runtimeCore = require('@aikdna/kdna-core'); } catch {
793
+ fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
794
+ }
795
+ if (profile === 'kdna.payload.judgment') {
796
+ const validation = runtimeCore.validate(absKdna);
797
+ if (!validation.overall_valid) {
798
+ fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
799
+ }
800
+ isLegacy = false;
801
+ } else if (profile === 'judgment-profile-v1') {
802
+ validateLegacyImportContract(manifest, payload);
803
+ isLegacy = true;
804
+ } else {
805
+ fail(`Unsupported payload profile "${profile || '(none)'}". Only kdna.payload.judgment and judgment-profile-v1 are accepted for --from-kdna.`);
806
+ }
807
+
755
808
  const lineage = {
756
809
  type: 'fork',
757
810
  parent_name: packageNameFromManifest(manifest),
@@ -760,15 +813,16 @@ function importFromKdna(kdnaPath) {
760
813
  parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
761
814
  };
762
815
 
763
- // Current assets carry strict-CBOR `payload.kdnab`. Legacy source folders
764
- // are migrated explicitly with --from-folder; they are not runtime assets.
765
816
  const importedCards = [];
766
- if (!entries.has('payload.kdnab')) {
767
- fail('Not a current .kdna asset: missing payload.kdnab. Use --from-folder for legacy JSON source migration.');
768
- }
817
+ let judgmentCore = null;
769
818
  try {
770
- const payload = decodePayload(entries.get('payload.kdnab'), manifest);
771
- importedCards.push(...cardsFromPayload(payload));
819
+ if (isLegacy) {
820
+ importedCards.push(...cardsFromLegacyPayload(payload));
821
+ judgmentCore = judgmentCoreFromLegacyPayload(payload);
822
+ } else {
823
+ importedCards.push(...cardsFromPayload(payload));
824
+ judgmentCore = judgmentCoreFromRuntimePayload(payload);
825
+ }
772
826
  } catch (e) {
773
827
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
774
828
  }
@@ -777,22 +831,239 @@ function importFromKdna(kdnaPath) {
777
831
  fail(`No current judgment cards could be extracted from ${absKdna}.`);
778
832
  }
779
833
 
780
- // Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
781
- // KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
782
- // them on the project. The next export then forwards them to
783
- // compileDomain via exportRuntimeAsset, which preserves
784
- // changelog / version_notes / reasoning_chains / banned_terms
785
- // through the round-trip.
786
834
  return {
787
835
  lineage,
788
836
  cards: importedCards,
837
+ judgment_core: judgmentCore,
789
838
  source_manifest: manifest,
790
839
  source_patterns: null,
791
- source_reasoning: null,
792
- source_evolution: null,
840
+ source_reasoning: payload.reasoning && typeof payload.reasoning === 'object'
841
+ ? JSON.parse(JSON.stringify(payload.reasoning))
842
+ : null,
843
+ source_evolution: payload.evolution && typeof payload.evolution === 'object'
844
+ ? JSON.parse(JSON.stringify(payload.evolution))
845
+ : null,
846
+ source_core_structure: Array.isArray(payload.core?.core_structure)
847
+ ? JSON.parse(JSON.stringify(payload.core.core_structure))
848
+ : null,
793
849
  };
794
850
  }
795
851
 
852
+ // ── legacy judgment-profile-v1 import support ───────────────────────────
853
+
854
+ const LEGACY_PROFILE = 'judgment-profile-v1';
855
+
856
+ const LEGACY_PATTERN_TYPE_MAP = {
857
+ term: 'term',
858
+ banned_term: 'banned_term',
859
+ misunderstanding: 'misunderstanding',
860
+ self_check: 'self_check',
861
+ scenario: 'scenario',
862
+ case: 'case',
863
+ reasoning: 'reasoning',
864
+ risk: 'risk',
865
+ axiom: 'axiom',
866
+ ontology: 'ontology',
867
+ framework: 'framework',
868
+ stance: 'stance',
869
+ aesthetic: 'aesthetic',
870
+ boundary: 'boundary',
871
+ evolution_stage: 'evolution_stage',
872
+ };
873
+
874
+ // Pattern subtypes that map to the generic 'pattern' card; the original
875
+ // subtype is preserved in fields.legacy_subtype for audit and re-export.
876
+ const LEGACY_PATTERN_SUBTYPES = new Set([
877
+ 'failure_pattern', 'design_pattern', 'response_pattern',
878
+ 'stopping_pattern', 'completion_pattern', 'decision_pattern',
879
+ 'reasoning_pattern', 'action_pattern', 'recovery_pattern',
880
+ 'responsibility_pattern',
881
+ ]);
882
+
883
+ const MAX_ZIP_ENTRIES = 100;
884
+ const MAX_SINGLE_ENTRY_UNCOMPRESSED = 10 * 1024 * 1024;
885
+ const MAX_TOTAL_UNCOMPRESSED = 50 * 1024 * 1024;
886
+ const REQUIRED_ZIP_ENTRIES = ['kdna.json', 'payload.kdnab', 'checksums.json', 'mimetype'];
887
+
888
+ function validateZipContainer(entries) {
889
+ if (entries.size > MAX_ZIP_ENTRIES) {
890
+ fail(`ZIP container has too many entries (${entries.size}, max ${MAX_ZIP_ENTRIES})`);
891
+ }
892
+ for (const name of REQUIRED_ZIP_ENTRIES) {
893
+ if (!entries.has(name)) fail(`Not a valid .kdna container: missing required entry "${name}"`);
894
+ }
895
+ let totalSize = 0;
896
+ for (const [name, buf] of entries) {
897
+ if (path.isAbsolute(name) || name.includes('..') || name.includes('\\')) {
898
+ fail(`ZIP entry "${name}" has an unsafe path`);
899
+ }
900
+ if (Buffer.isBuffer(buf) && buf.length > MAX_SINGLE_ENTRY_UNCOMPRESSED) {
901
+ fail(`ZIP entry "${name}" is too large (${buf.length} bytes)`);
902
+ }
903
+ totalSize += Buffer.isBuffer(buf) ? buf.length : 0;
904
+ }
905
+ if (totalSize > MAX_TOTAL_UNCOMPRESSED) {
906
+ fail(`ZIP container total uncompressed size exceeds limit (${totalSize} bytes)`);
907
+ }
908
+ }
909
+
910
+ function validateLegacyImportContract(manifest, payload) {
911
+ // Frozen validation for judgment-profile-v1 assets.
912
+ if (!manifest || typeof manifest !== 'object') fail('Legacy asset manifest is missing or invalid');
913
+ if (manifest.kdna_version !== '1.0') fail(`Legacy asset must declare kdna_version "1.0", got "${manifest.kdna_version}"`);
914
+ if (!manifest.asset_id || typeof manifest.asset_id !== 'string') fail('Legacy asset_id is missing');
915
+ if (!manifest.compatibility || manifest.compatibility.profile !== LEGACY_PROFILE) {
916
+ fail(`Legacy asset compatibility.profile must be "${LEGACY_PROFILE}"`);
917
+ }
918
+ if (!manifest.payload || manifest.payload.path !== 'payload.kdnab') {
919
+ fail('Legacy asset payload.path must be "payload.kdnab"');
920
+ }
921
+ if (manifest.payload.encoding !== 'cbor') {
922
+ fail('Legacy asset payload.encoding must be "cbor"');
923
+ }
924
+ if (manifest.payload.encrypted) fail('Legacy asset must not be encrypted');
925
+ if (!payload || typeof payload !== 'object') fail('Legacy asset payload is missing or invalid');
926
+ if (payload.profile !== LEGACY_PROFILE) fail('Legacy payload must declare profile "judgment-profile-v1"');
927
+ if (!payload.core || typeof payload.core !== 'object') fail('Legacy payload must contain a "core" object');
928
+ if (!Array.isArray(payload.core.axioms)) fail('Legacy payload must contain axioms');
929
+ if (!payload.patterns || !Array.isArray(payload.patterns)) fail('Legacy payload must contain patterns');
930
+ }
931
+
932
+ function judgmentCoreFromLegacyPayload(payload) {
933
+ const core = payload.core || {};
934
+ const result = {};
935
+ if (typeof core.highest_question === 'string' && core.highest_question) result.highest_question = core.highest_question;
936
+ if (Array.isArray(core.worldview) && core.worldview.length) result.worldview = core.worldview;
937
+ if (Array.isArray(core.value_order) && core.value_order.length) result.value_order = core.value_order;
938
+ if (core.judgment_role && typeof core.judgment_role === 'object' && Object.keys(core.judgment_role).length) result.judgment_role = core.judgment_role;
939
+ return Object.keys(result).length ? result : null;
940
+ }
941
+
942
+ function legacyStableId(type, fields, prefix) {
943
+ // Deterministic ID for legacy items that lack a declared id.
944
+ // Prefix is the parent scope (e.g. the asset name or domain id).
945
+ const sorted = JSON.stringify(Object.entries(fields).sort());
946
+ const h = require('crypto').createHash('sha256').update(`${type}:${sorted}`).digest('hex');
947
+ return `${prefix}_${h.slice(0, 12)}`;
948
+ }
949
+
950
+ const _legacyDedupIds = new Set();
951
+
952
+ function _legacyDedupPush(cards, type, fields, id) {
953
+ // Deduplicate by id IF an id is declared. Items without ids are unique
954
+ // (deterministic by content). A second push with the same id merges.
955
+ if (!id) { pushImportedCard(cards, type, fields, null); return; }
956
+ if (_legacyDedupIds.has(id)) { pushImportedCard(cards, type, fields, id); return; }
957
+ _legacyDedupIds.add(id);
958
+ pushImportedCard(cards, type, fields, id);
959
+ }
960
+
961
+ function cardsFromLegacyPayload(payload) {
962
+ _legacyDedupIds.clear();
963
+ const cards = [];
964
+ const core = payload.core || {};
965
+
966
+ // axioms
967
+ for (const ax of (core.axioms || [])) {
968
+ _legacyDedupPush(cards, 'axiom', importedFields(ax), ax.id || null);
969
+ }
970
+
971
+ // ontology
972
+ for (const ont of (core.ontology || [])) {
973
+ pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
974
+ }
975
+
976
+ // frameworks
977
+ for (const fw of (core.frameworks || [])) {
978
+ pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
979
+ }
980
+
981
+ // stances
982
+ for (const st of (core.stances || [])) {
983
+ const fields = typeof st === 'string' ? { statement: st } : importedFields(st);
984
+ pushImportedCard(cards, 'stance', fields, typeof st === 'object' ? st.id || null : null);
985
+ }
986
+
987
+ // boundaries
988
+ for (const bd of (core.boundaries || [])) {
989
+ pushImportedCard(cards, 'boundary', importedFields(bd), bd.id || null);
990
+ }
991
+
992
+ // risks
993
+ const legacyRisks = Array.isArray(core.risk_model)
994
+ ? core.risk_model
995
+ : (Array.isArray(core.risk_model?.risks) ? core.risk_model.risks : []);
996
+ for (const risk of legacyRisks) {
997
+ pushImportedCard(cards, 'risk', importedFields(risk.fields || risk), risk.id || null);
998
+ }
999
+
1000
+ // patterns with explicit types (including legacy subtypes)
1001
+ const patterns = payload.patterns || [];
1002
+ for (const p of patterns) {
1003
+ if (!p || typeof p !== 'object') continue;
1004
+ const rawType = p.type;
1005
+ if (!rawType) continue; // type-less items handled separately (misunderstandings below)
1006
+
1007
+ if (LEGACY_PATTERN_SUBTYPES.has(rawType)) {
1008
+ pushImportedCard(cards, 'pattern', {
1009
+ ...importedFields(p, { keepType: true }),
1010
+ legacy_subtype: rawType,
1011
+ }, p.id || null);
1012
+ } else if (rawType === 'term') {
1013
+ pushImportedCard(cards, 'term', importedFields(p), p.id || null);
1014
+ } else if (rawType === 'banned_term') {
1015
+ pushImportedCard(cards, 'banned_term', importedFields(p), p.id || null);
1016
+ } else if (LEGACY_PATTERN_TYPE_MAP.hasOwnProperty(rawType)) {
1017
+ pushImportedCard(cards, LEGACY_PATTERN_TYPE_MAP[rawType], importedFields(p), p.id || null);
1018
+ } else {
1019
+ fail(`Unknown legacy pattern type "${rawType}" for card "${p.id || '(no id)'}"`);
1020
+ }
1021
+ }
1022
+
1023
+ // misunderstandings: appear in patterns[] with no type and in
1024
+ // reasoning.failure_modes[] with slightly different field names.
1025
+ // Import from patterns[] (richer fields). The pushImportedCard
1026
+ // dedup handles the failure_modes view via id matching.
1027
+ for (const p of patterns) {
1028
+ if (!p || typeof p !== 'object') continue;
1029
+ if (p.type) continue; // already handled above
1030
+ if (!p.wrong && !p.correct) continue;
1031
+ pushImportedCard(cards, 'misunderstanding', importedFields(p), p.id || null);
1032
+ }
1033
+
1034
+ // reasoning chains
1035
+ for (const chain of (payload.reasoning?.reasoning_chains || [])) {
1036
+ const fields = importedFields(chain);
1037
+ pushImportedCard(cards, 'reasoning', fields, chain.id || null);
1038
+ }
1039
+
1040
+ // self checks: array of plain strings in legacy payload
1041
+ for (const check of (payload.reasoning?.self_check || [])) {
1042
+ if (typeof check === 'string' && check.trim()) {
1043
+ const fields = { question: check.trim() };
1044
+ const id = legacyStableId('self_check', fields, 'legacy_sc');
1045
+ pushImportedCard(cards, 'self_check', fields, id);
1046
+ }
1047
+ }
1048
+
1049
+ // scenarios
1050
+ for (const sc of (payload.scenarios || [])) {
1051
+ pushImportedCard(cards, 'scenario', importedFields(sc), sc.id || null);
1052
+ }
1053
+
1054
+ // cases
1055
+ for (const cs of (payload.cases || [])) {
1056
+ pushImportedCard(cards, 'case', importedFields(cs), cs.id || null);
1057
+ }
1058
+
1059
+ // evolution stages
1060
+ for (const stage of (payload.evolution?.stages || [])) {
1061
+ pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
1062
+ }
1063
+
1064
+ return cards;
1065
+ }
1066
+
796
1067
  /**
797
1068
  * Import cards from a legacy source folder into a new Studio project.
798
1069
  * Reads KDNA_*.json files and converts entries to draft judgment cards.
@@ -827,6 +1098,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
827
1098
  if (manifest.default_language) manifestData.default_language = manifest.default_language;
828
1099
  if (manifest.description) manifestData.description = manifest.description;
829
1100
  if (manifest.name) manifestData.name = manifest.name;
1101
+ if (manifest.title) manifestData.title = manifest.title;
830
1102
  if (manifest.keywords) manifestData.keywords = manifest.keywords;
831
1103
  if (manifest.license) manifestData.license = manifest.license;
832
1104
  if (manifest.author) manifestData.author = manifest.author;
@@ -834,6 +1106,8 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
834
1106
  if (manifest.core_insight) manifestData.core_insight = manifest.core_insight;
835
1107
  if (manifest.created_at || manifest.created) manifestData.created = manifest.created_at || manifest.created;
836
1108
  if (manifest.updated_at || manifest.updated) manifestData.updated = manifest.updated_at || manifest.updated;
1109
+ // legacy-migration bookkeeping only: preserved in the Studio project for
1110
+ // issuer-scoped governance; the Core 0.20 export manifest never projects it
837
1111
  if (manifest.risk_level) manifestData.risk_level = manifest.risk_level;
838
1112
  if (manifest.fitness_for_purpose) manifestData.fitness_for_purpose = manifest.fitness_for_purpose;
839
1113
  }
@@ -865,25 +1139,17 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
865
1139
 
866
1140
  if (core) {
867
1141
  for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
868
- const fields = {};
1142
+ const fields = importedFields(ax);
869
1143
  for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type']) {
870
- if (key in ax) fields[key] = ax[key];
871
- else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
1144
+ if (!(key in ax)) audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
872
1145
  }
873
1146
  importedCards.push(cardApi.createCard('axiom', fields, ax.id || null));
874
1147
  }
875
1148
  for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
876
- importedCards.push(cardApi.createCard('ontology', {
877
- one_sentence: ont.one_sentence || ont.essence || '',
878
- essence: ont.essence || '', boundary: ont.boundary || '',
879
- trigger_signal: ont.trigger_signal || '',
880
- }, ont.id || null));
1149
+ importedCards.push(cardApi.createCard('ontology', importedFields(ont), ont.id || null));
881
1150
  }
882
1151
  for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
883
- importedCards.push(cardApi.createCard('boundary', {
884
- scope: b.scope || '', out_of_scope: b.out_of_scope || '',
885
- acceptable_exceptions: b.acceptable_exceptions || [],
886
- }, b.id || null));
1152
+ importedCards.push(cardApi.createCard('boundary', importedFields(b), b.id || null));
887
1153
  }
888
1154
  // Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
889
1155
  // (either as `{risks: [...]}` or as an array itself).
@@ -894,23 +1160,16 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
894
1160
  else risksSource = [];
895
1161
  }
896
1162
  for (const r of risksSource) {
897
- importedCards.push(cardApi.createCard('risk', r.fields || r, r.id || null));
1163
+ importedCards.push(cardApi.createCard('risk', importedFields(r.fields || r), r.id || null));
898
1164
  }
899
1165
  // NEW: import stances
900
1166
  for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
901
- const text = typeof s === 'string' ? s : s.statement || s.stance || '';
902
- importedCards.push(cardApi.createCard('stance', {
903
- statement: text,
904
- applies_when: s.applies_when || [],
905
- does_not_apply_when: s.does_not_apply_when || [],
906
- }, typeof s === 'object' ? s.id || null : null));
1167
+ const fields = typeof s === 'string' ? { statement: s } : importedFields(s);
1168
+ importedCards.push(cardApi.createCard('stance', fields, typeof s === 'object' ? s.id || null : null));
907
1169
  }
908
1170
  // NEW: import frameworks
909
1171
  for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
910
- importedCards.push(cardApi.createCard('framework', {
911
- name: fw.name || '', when_to_use: fw.when_to_use || '',
912
- steps: fw.steps || [],
913
- }, fw.id || null));
1172
+ importedCards.push(cardApi.createCard('framework', importedFields(fw), fw.id || null));
914
1173
  }
915
1174
  }
916
1175
 
@@ -918,27 +1177,25 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
918
1177
  // NEW: import terminology
919
1178
  if (patterns.terminology) {
920
1179
  for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
921
- importedCards.push(cardApi.createCard('term', {
922
- term: t.term || '', definition: t.definition || '',
923
- }, t.id || null));
1180
+ importedCards.push(cardApi.createCard('term', importedFields(t), t.id || null));
924
1181
  }
925
1182
  for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
926
- importedCards.push(cardApi.createCard('banned_term', {
927
- term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
928
- }, bt.id || null));
1183
+ importedCards.push(cardApi.createCard('banned_term', importedFields(bt), bt.id || null));
929
1184
  }
930
1185
  }
1186
+ for (const pattern of (Array.isArray(patterns.patterns) ? patterns.patterns : [])) {
1187
+ const fields = importedFields(pattern, { keepType: true });
1188
+ if (!fields.type) fields.type = 'pattern';
1189
+ importedCards.push(cardApi.createCard('pattern', fields, pattern.id || null));
1190
+ }
931
1191
  for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
932
- importedCards.push(cardApi.createCard('misunderstanding', {
933
- wrong: ms.wrong || '', correct: ms.correct || '',
934
- key_distinction: ms.key_distinction || '', why: ms.why || '',
935
- }, ms.id || null));
1192
+ importedCards.push(cardApi.createCard('misunderstanding', importedFields(ms), ms.id || null));
936
1193
  }
937
1194
  for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
938
- const q = typeof sc === 'string' ? sc : sc.question || '';
1195
+ const fields = typeof sc === 'string' ? { question: sc } : importedFields(sc);
939
1196
  importedCards.push(cardApi.createCard(
940
1197
  'self_check',
941
- { question: q },
1198
+ fields,
942
1199
  typeof sc === 'object' ? sc.id || null : null,
943
1200
  ));
944
1201
  }
@@ -951,7 +1208,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
951
1208
  for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
952
1209
  importedCards.push(cardApi.createCard(
953
1210
  'aesthetic',
954
- aesthetic.fields || aesthetic,
1211
+ importedFields(aesthetic.fields || aesthetic),
955
1212
  aesthetic.id || null,
956
1213
  ));
957
1214
  }
@@ -960,20 +1217,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
960
1217
  // NEW: import scenarios
961
1218
  if (scenarios && Array.isArray(scenarios.scenes)) {
962
1219
  for (const scene of scenarios.scenes) {
963
- importedCards.push(cardApi.createCard('scenario', {
964
- name: scene.name || scene.id || '',
965
- trigger: scene.trigger || '', action: scene.action || '',
966
- }, scene.id || null));
1220
+ importedCards.push(cardApi.createCard('scenario', importedFields(scene), scene.id || null));
967
1221
  }
968
1222
  }
969
1223
 
970
1224
  // NEW: import cases
971
1225
  if (cases && Array.isArray(cases.cases)) {
972
1226
  for (const c of cases.cases) {
973
- importedCards.push(cardApi.createCard('case', {
974
- title: c.title || c.id || '',
975
- scenario: c.scenario || '', expected: c.expected || '',
976
- }, c.id || null));
1227
+ importedCards.push(cardApi.createCard('case', importedFields(c), c.id || null));
977
1228
  }
978
1229
  }
979
1230
 
@@ -986,26 +1237,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
986
1237
  // identity.
987
1238
  if (reasoning && Array.isArray(reasoning.reasoning_chains)) {
988
1239
  for (const rc of reasoning.reasoning_chains) {
989
- importedCards.push(cardApi.createCard('reasoning', {
990
- axiom: rc.axiom || '',
991
- one_sentence: rc.one_sentence || rc.conclusion || '',
992
- chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
993
- principle: rc.principle || rc.name || '',
994
- concrete_action: rc.concrete_action || rc.so_what || '',
995
- so_what: rc.so_what || rc.concrete_action || '',
996
- conclusion: rc.conclusion || '',
997
- }, rc.id || null));
1240
+ importedCards.push(cardApi.createCard('reasoning', importedFields(rc), rc.id || null));
998
1241
  }
999
1242
  }
1000
1243
 
1001
1244
  // NEW: import evolution stages
1002
1245
  if (evolution && Array.isArray(evolution.stages)) {
1003
1246
  for (const stage of evolution.stages) {
1004
- pushImportedCard(importedCards, 'evolution_stage', {
1005
- name: stage.name || stage.id || '',
1006
- level: stage.level != null ? stage.level : '',
1007
- description: stage.description || '',
1008
- }, stage.id || null);
1247
+ pushImportedCard(importedCards, 'evolution_stage', importedFields(stage), stage.id || null);
1009
1248
  }
1010
1249
  }
1011
1250
 
@@ -1016,6 +1255,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1016
1255
  sourcePath: absSource,
1017
1256
  creatorIdentity: creatorIdentity || null,
1018
1257
  lineage: { type: 'migrated', parent_name: manifestData.name || null, parent_asset_uid: null, parent_version: manifestData.version || null, parent_asset_digest: null },
1258
+ judgmentCore: judgmentCoreFromLegacyPayload({ core: core || {} }),
1019
1259
  });
1020
1260
  // Preserve manifest metadata
1021
1261
  if (manifestData.asset_uid) project.asset_uid = manifestData.asset_uid;
@@ -1031,11 +1271,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1031
1271
  if (!project.release) project.release = {};
1032
1272
  project.release.description = manifestData.description;
1033
1273
  }
1274
+ if (manifestData.title) project.title = manifestData.title;
1034
1275
  if (manifestData.license) project.license = manifestData.license;
1035
1276
  if (manifestData.author) project.author = manifestData.author;
1036
1277
  if (manifestData.languages) project.languages = manifestData.languages;
1037
1278
  if (manifestData.default_language) project.default_language = manifestData.default_language;
1038
1279
  if (manifestData.raw) project.source_manifest = publicManifestMetadata(manifestData.raw);
1280
+ if (patterns) project.source_patterns = JSON.parse(JSON.stringify(patterns));
1281
+ if (reasoning) project.source_reasoning = JSON.parse(JSON.stringify(reasoning));
1282
+ if (evolution) project.source_evolution = JSON.parse(JSON.stringify(evolution));
1283
+ if (Array.isArray(core?.core_structure)) {
1284
+ project.source_core_structure = JSON.parse(JSON.stringify(core.core_structure));
1285
+ }
1039
1286
 
1040
1287
  project.cards = importedCards;
1041
1288
  if (project.stages?.judgment_cards) {
@@ -1052,10 +1299,12 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1052
1299
  }
1053
1300
 
1054
1301
  /**
1055
- * Minimal ZIP central directory parser — reads entries into a Map.
1302
+ * Minimal ZIP central directory parser — validates every entry before decompressing.
1056
1303
  */
1057
1304
  function readZipEntries(buf) {
1058
1305
  const entries = new Map();
1306
+ const metadata = [];
1307
+
1059
1308
  // Find end-of-central-directory record
1060
1309
  let eocdOffset = buf.length - 22;
1061
1310
  while (eocdOffset >= 0) {
@@ -1067,6 +1316,8 @@ function readZipEntries(buf) {
1067
1316
  const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
1068
1317
  let offset = centralDirOffset;
1069
1318
 
1319
+ // Pass 1: collect metadata, validate safety before any decompression
1320
+ let totalUncomp = 0;
1070
1321
  while (offset < eocdOffset) {
1071
1322
  const sig = buf.readUInt32LE(offset);
1072
1323
  if (sig !== 0x02014b50) break;
@@ -1079,20 +1330,47 @@ function readZipEntries(buf) {
1079
1330
  const localOffset = buf.readUInt32LE(offset + 42);
1080
1331
  const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
1081
1332
 
1082
- // Read local file header to get data
1083
- const localNameLen = buf.readUInt16LE(localOffset + 26);
1084
- const localExtraLen = buf.readUInt16LE(localOffset + 28);
1085
- const dataStart = localOffset + 30 + localNameLen + localExtraLen;
1086
-
1087
- if (compMethod === 0) {
1088
- entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
1089
- } else if (compMethod === 8) {
1090
- entries.set(name, zlib.inflateRawSync(buf.subarray(dataStart, dataStart + compSize)));
1333
+ // Security: path must not be absolute, must not escape
1334
+ if (!name || name.startsWith('/') || name.includes('..') || name.includes('\\')) {
1335
+ throw new Error(`ZIP entry "${name}" has an unsafe path`);
1091
1336
  }
1092
-
1337
+ // Duplicate detection
1338
+ if (entries.has(name) || metadata.some(m => m.name === name)) {
1339
+ throw new Error(`Duplicate ZIP entry: "${name}"`);
1340
+ }
1341
+ if (compMethod !== 0 && compMethod !== 8) {
1342
+ throw new Error(`ZIP entry "${name}" uses unsupported compression method ${compMethod}`);
1343
+ }
1344
+ totalUncomp += uncompSize;
1345
+ if (totalUncomp > MAX_TOTAL_UNCOMPRESSED) {
1346
+ throw new Error(`ZIP total uncompressed size ${totalUncomp} exceeds limit`);
1347
+ }
1348
+ if (uncompSize > MAX_SINGLE_ENTRY_UNCOMPRESSED) {
1349
+ throw new Error(`ZIP entry "${name}" declared size ${uncompSize} exceeds limit`);
1350
+ }
1351
+ const dataStart = localOffset + 30 + buf.readUInt16LE(localOffset + 26) + buf.readUInt16LE(localOffset + 28);
1352
+ metadata.push({ name, compMethod, compSize, uncompSize, dataStart });
1093
1353
  offset += 46 + nameLen + extraLen + commentLen;
1094
1354
  }
1095
1355
 
1356
+ if (metadata.length > MAX_ZIP_ENTRIES) {
1357
+ throw new Error(`ZIP container has too many entries (${metadata.length}, max ${MAX_ZIP_ENTRIES})`);
1358
+ }
1359
+
1360
+ // Pass 2: decompress into entries
1361
+ for (const m of metadata) {
1362
+ if (m.compMethod === 0) {
1363
+ entries.set(m.name, buf.subarray(m.dataStart, m.dataStart + m.uncompSize));
1364
+ } else {
1365
+ const raw = buf.subarray(m.dataStart, m.dataStart + m.compSize);
1366
+ const inflated = zlib.inflateRawSync(raw, { maxOutputLength: m.uncompSize });
1367
+ if (inflated.length !== m.uncompSize) {
1368
+ throw new Error(`ZIP entry "${m.name}" decompressed length ${inflated.length} != declared ${m.uncompSize}`);
1369
+ }
1370
+ entries.set(m.name, inflated);
1371
+ }
1372
+ }
1373
+
1096
1374
  return entries;
1097
1375
  }
1098
1376
 
@@ -1936,7 +2214,7 @@ function cmdStudioInstall(args) {
1936
2214
  function cmdStudioUpdate(args) {
1937
2215
  const target = args[0];
1938
2216
  if (!target) fail('Usage: kdna-studio update <@scope/name>');
1939
- // kdna update does not exist in kdna-cli v0.28.x. The correct
2217
+ // kdna update does not exist in kdna-cli 0.28.x. The correct
1940
2218
  // path is to re-install via npm or use kdna install to pull
1941
2219
  // the latest published version of the asset.
1942
2220
  console.log(`To update ${target}, run:`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.9.2",
3
+ "version": "0.10.2",
4
4
  "description": "Official KDNA Studio command-line authoring entry for creating, reviewing, compiling, and exporting .kdna judgment assets through the KDNA toolchain.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -14,11 +14,18 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "lint": "node --check bin/kdna-studio.js && node --check tests/cli.test.js",
17
+ "lint": "node scripts/run-lint.js",
18
+ "pretest": "node scripts/acquire-trusted-npm-release.js",
18
19
  "test": "node --test tests/*.test.js",
19
- "test:all": "npm run lint && npm test",
20
+ "test:all": "node scripts/run-test-all.js",
21
+ "test:runtime": "node scripts/run-product-tests.js",
22
+ "test:candidate-chain": "node scripts/verify-runtime-candidate-chain.js",
23
+ "verify:candidate-sources": "node scripts/verify-runtime-candidate-sources.js",
24
+ "check:protocol-names": "node scripts/check-current-protocol-names.js",
20
25
  "release:check": "node scripts/release-check.js",
21
- "prepublishOnly": "npm test && npm run release:check"
26
+ "release:generate-evidence": "node scripts/generate-release-evidence.js",
27
+ "release:publish-verified": "node scripts/publish-verified-artifact.js",
28
+ "prepublishOnly": "node scripts/run-test-all.js && node scripts/verify-runtime-candidate-chain.js && node scripts/release-check.js"
22
29
  },
23
30
  "keywords": [
24
31
  "kdna",
@@ -41,13 +48,12 @@
41
48
  "node": ">=18"
42
49
  },
43
50
  "dependencies": {
44
- "@aikdna/kdna-core": "0.17.0",
45
- "@aikdna/kdna-studio-core": "1.9.0",
51
+ "@aikdna/kdna-core": "0.20.0",
52
+ "@aikdna/kdna-studio-core": "2.0.2",
46
53
  "ajv": "^8.20.0",
47
54
  "ajv-formats": "^3.0.1",
48
55
  "cbor-x": "^1.6.4"
49
56
  },
50
- "main": "index.js",
51
57
  "directories": {
52
58
  "test": "tests"
53
59
  },