@aikdna/kdna-studio-cli 0.10.1 → 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.
package/README.md CHANGED
@@ -171,6 +171,11 @@ Current `.kdna` imports also preserve explicitly declared `highest_question`,
171
171
  project. Re-export writes those values back exactly; Studio does not infer a
172
172
  replacement judgment core from an axiom.
173
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
+
174
179
  ## Related
175
180
 
176
181
  - [KDNA Core](https://github.com/aikdna/kdna) — Official format specification
@@ -366,12 +366,23 @@ function cleanFields(fields = {}) {
366
366
  return cleaned;
367
367
  }
368
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
+
369
379
  function importedCard(type, fields = {}, id = null) {
370
- try {
371
- return cardApi.createCard(type, cleanFields(fields), id || null);
372
- } catch (_) {
373
- 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`);
374
384
  }
385
+ return card;
375
386
  }
376
387
 
377
388
  // Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
@@ -401,8 +412,10 @@ function pushImportedCard(cards, type, fields = {}, id = null) {
401
412
  }
402
413
  return;
403
414
  }
404
- if (id) _importedIds.add(id);
405
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);
406
419
  if (card) cards.push(card);
407
420
  }
408
421
 
@@ -438,31 +451,22 @@ function cardsFromRuntimePayload(payload) {
438
451
  if (cards.length > 0) return cards;
439
452
 
440
453
  for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
441
- pushImportedCard(cards, 'axiom', {
442
- one_sentence: ax.one_sentence || '',
443
- full_statement: ax.full_statement || '',
444
- why: ax.why || '',
445
- applies_when: ax.applies_when || [],
446
- does_not_apply_when: ax.does_not_apply_when || [],
447
- failure_risk: ax.failure_risk || '',
448
- confidence: ax.confidence || '',
449
- evidence_type: ax.evidence_type || '',
450
- }, ax.id || null);
454
+ pushImportedCard(cards, 'axiom', importedFields(ax), ax.id || null);
451
455
  }
452
456
  for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
453
- pushImportedCard(cards, 'ontology', ont, ont.id || null);
457
+ pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
454
458
  }
455
459
  for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
456
- pushImportedCard(cards, 'framework', fw, fw.id || null);
460
+ pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
457
461
  }
458
462
  for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
459
- pushImportedCard(cards, 'stance', stance, stance.id || null);
463
+ pushImportedCard(cards, 'stance', importedFields(stance), stance.id || null);
460
464
  }
461
465
  for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
462
- pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
466
+ pushImportedCard(cards, 'aesthetic', importedFields(aesthetic), aesthetic.id || null);
463
467
  }
464
468
  for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
465
- pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
469
+ pushImportedCard(cards, 'boundary', importedFields(boundary), boundary.id || null);
466
470
  }
467
471
  // risk_model may be either an array of risk items (legacy) or
468
472
  // `{risks: [...]}` (current). Treat both shapes; ignore everything else
@@ -470,7 +474,7 @@ function cardsFromRuntimePayload(payload) {
470
474
  const riskModel = payload.core?.risk_model;
471
475
  const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
472
476
  for (const risk of riskItems) {
473
- pushImportedCard(cards, 'risk', risk, risk.id || null);
477
+ pushImportedCard(cards, 'risk', importedFields(risk), risk.id || null);
474
478
  }
475
479
  for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
476
480
  // Bug (#146): prior version dropped any payload.patterns entry
@@ -500,19 +504,25 @@ function cardsFromRuntimePayload(payload) {
500
504
  );
501
505
  continue;
502
506
  }
503
- fields = { ...pattern };
504
- delete fields.type;
507
+ fields = importedFields(pattern);
505
508
  } else {
506
- const { type: _stripped, ...rest } = pattern;
507
- 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';
508
518
  }
509
519
  pushImportedCard(cards, type, fields, pattern.id || null);
510
520
  }
511
521
  for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
512
- pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
522
+ pushImportedCard(cards, 'scenario', importedFields(scenario), scenario.id || null);
513
523
  }
514
524
  for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
515
- pushImportedCard(cards, 'case', item, item.id || null);
525
+ pushImportedCard(cards, 'case', importedFields(item), item.id || null);
516
526
  }
517
527
  // The current payload contract has one self-check field. Refuse to infer a
518
528
  // parallel field name: Core validation owns format migration, while Studio
@@ -520,8 +530,8 @@ function cardsFromRuntimePayload(payload) {
520
530
  const rawSelfCheck = payload.reasoning?.self_check;
521
531
  const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
522
532
  for (const selfCheck of selfCheckArr) {
523
- const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
524
- 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);
525
535
  }
526
536
  for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
527
537
  // Bug (#145 follow-up): the prior version dropped three
@@ -530,7 +540,8 @@ function cardsFromRuntimePayload(payload) {
530
540
  // project that round-tripped through `migrate` lost
531
541
  // these fields every time. The fix reads them through, matching
532
542
  // the export-side map in kdna-studio-core's buildPayload.
533
- pushImportedCard(cards, 'misunderstanding', {
543
+ const fields = {
544
+ ...importedFields(failureMode),
534
545
  wrong: failureMode.mode || failureMode.wrong || '',
535
546
  correct: failureMode.correct || '',
536
547
  key_distinction: failureMode.key_distinction || '',
@@ -538,7 +549,9 @@ function cardsFromRuntimePayload(payload) {
538
549
  failure_risk: failureMode.failure_risk || '',
539
550
  applies_when: Array.isArray(failureMode.applies_when) ? failureMode.applies_when : [],
540
551
  does_not_apply_when: Array.isArray(failureMode.does_not_apply_when) ? failureMode.does_not_apply_when : [],
541
- }, failureMode.id || null);
552
+ };
553
+ delete fields.mode;
554
+ pushImportedCard(cards, 'misunderstanding', fields, failureMode.id || null);
542
555
  }
543
556
  for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
544
557
  // Bug (#4 UX): compile/index.js synthesises one reasoning chain
@@ -547,7 +560,7 @@ function cardsFromRuntimePayload(payload) {
547
560
  // doubled the card count on round-trip. The fix skips chains
548
561
  // that the producer explicitly marked as synthesised.
549
562
  if (chain.source_authored === false) continue;
550
- pushImportedCard(cards, 'reasoning', chain, chain.id || null);
563
+ pushImportedCard(cards, 'reasoning', importedFields(chain), chain.id || null);
551
564
  }
552
565
  for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
553
566
  // Distinguish source-authored stages (preserved identity) from
@@ -556,7 +569,7 @@ function cardsFromRuntimePayload(payload) {
556
569
  // so the importer doesn't double-count what was already in the
557
570
  // locked judgment cards.
558
571
  if (stage.source_authored === true) {
559
- pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
572
+ pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
560
573
  }
561
574
  }
562
575
  return cards;
@@ -608,7 +621,7 @@ function buildManifest(project, name) {
608
621
  asset_uid: project.asset_uid || source.asset_uid || 'urn:uuid:' + crypto.randomUUID(),
609
622
  asset_type: project.type === 'cluster' ? 'cluster' : 'domain',
610
623
  source_asset_type: source.asset_type || null,
611
- title: titleFromName(source.name || name || project.name, assetId),
624
+ title: source.title || project.title || titleFromName(source.name || name || project.name, assetId),
612
625
  version: semverValue(project.release?.version || source.version, '1.0.0'),
613
626
  judgment_version: semverValue(project.release?.judgment_version || source.judgment_version || source.version, '1.0.0'),
614
627
  created_at: isoDateTime(source.created_at || source.created || project.created),
@@ -691,7 +704,13 @@ function cmdCreate(args) {
691
704
  if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
692
705
  if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
693
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;
694
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;
695
714
  if (kdnaData.source_manifest?.version) {
696
715
  if (!project.release) project.release = {};
697
716
  project.release.version = kdnaData.source_manifest.version;
@@ -700,9 +719,9 @@ function cmdCreate(args) {
700
719
  if (!project.release) project.release = {};
701
720
  project.release.judgment_version = kdnaData.source_manifest.judgment_version;
702
721
  }
703
- if (kdnaData.source_manifest?.description) {
722
+ if (kdnaData.source_manifest?.description || kdnaData.source_manifest?.summary) {
704
723
  if (!project.release) project.release = {};
705
- project.release.description = kdnaData.source_manifest.description;
724
+ project.release.description = kdnaData.source_manifest.description || kdnaData.source_manifest.summary;
706
725
  }
707
726
  if (project.stages?.judgment_cards) {
708
727
  project.stages.judgment_cards.total = kdnaData.cards.length;
@@ -756,19 +775,36 @@ function importFromKdna(kdnaPath) {
756
775
  if (stats.size > 50 * 1024 * 1024) {
757
776
  fail(`KDNA file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${absKdna}`);
758
777
  }
759
- let runtimeCore;
760
- try { runtimeCore = require('@aikdna/kdna-core'); } catch {
761
- fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
762
- }
763
- const validation = runtimeCore.validate(absKdna);
764
- if (!validation.overall_valid) {
765
- fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
766
- }
778
+
779
+ // ── read ZIP before Core validation so the profile can branch ──
767
780
  const zipBuf = fs.readFileSync(absKdna);
768
781
  const entries = readZipEntries(zipBuf);
782
+ validateZipContainer(entries);
769
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.');
770
785
 
771
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
+
772
808
  const lineage = {
773
809
  type: 'fork',
774
810
  parent_name: packageNameFromManifest(manifest),
@@ -777,17 +813,16 @@ function importFromKdna(kdnaPath) {
777
813
  parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
778
814
  };
779
815
 
780
- // Current assets carry strict-CBOR `payload.kdnab`. Legacy source folders
781
- // are migrated explicitly with --from-folder; they are not runtime assets.
782
816
  const importedCards = [];
783
817
  let judgmentCore = null;
784
- if (!entries.has('payload.kdnab')) {
785
- fail('Not a current .kdna asset: missing payload.kdnab. Use --from-folder for legacy JSON source migration.');
786
- }
787
818
  try {
788
- const payload = decodePayload(entries.get('payload.kdnab'), manifest);
789
- importedCards.push(...cardsFromPayload(payload));
790
- judgmentCore = judgmentCoreFromRuntimePayload(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
+ }
791
826
  } catch (e) {
792
827
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
793
828
  }
@@ -796,23 +831,239 @@ function importFromKdna(kdnaPath) {
796
831
  fail(`No current judgment cards could be extracted from ${absKdna}.`);
797
832
  }
798
833
 
799
- // Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
800
- // KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
801
- // them on the project. The next export then forwards them to
802
- // compileDomain via exportRuntimeAsset, which preserves
803
- // changelog / version_notes / reasoning_chains / banned_terms
804
- // through the round-trip.
805
834
  return {
806
835
  lineage,
807
836
  cards: importedCards,
808
837
  judgment_core: judgmentCore,
809
838
  source_manifest: manifest,
810
839
  source_patterns: null,
811
- source_reasoning: null,
812
- 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,
813
849
  };
814
850
  }
815
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
+
816
1067
  /**
817
1068
  * Import cards from a legacy source folder into a new Studio project.
818
1069
  * Reads KDNA_*.json files and converts entries to draft judgment cards.
@@ -847,6 +1098,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
847
1098
  if (manifest.default_language) manifestData.default_language = manifest.default_language;
848
1099
  if (manifest.description) manifestData.description = manifest.description;
849
1100
  if (manifest.name) manifestData.name = manifest.name;
1101
+ if (manifest.title) manifestData.title = manifest.title;
850
1102
  if (manifest.keywords) manifestData.keywords = manifest.keywords;
851
1103
  if (manifest.license) manifestData.license = manifest.license;
852
1104
  if (manifest.author) manifestData.author = manifest.author;
@@ -887,25 +1139,17 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
887
1139
 
888
1140
  if (core) {
889
1141
  for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
890
- const fields = {};
1142
+ const fields = importedFields(ax);
891
1143
  for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type']) {
892
- if (key in ax) fields[key] = ax[key];
893
- else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
1144
+ if (!(key in ax)) audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
894
1145
  }
895
1146
  importedCards.push(cardApi.createCard('axiom', fields, ax.id || null));
896
1147
  }
897
1148
  for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
898
- importedCards.push(cardApi.createCard('ontology', {
899
- one_sentence: ont.one_sentence || ont.essence || '',
900
- essence: ont.essence || '', boundary: ont.boundary || '',
901
- trigger_signal: ont.trigger_signal || '',
902
- }, ont.id || null));
1149
+ importedCards.push(cardApi.createCard('ontology', importedFields(ont), ont.id || null));
903
1150
  }
904
1151
  for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
905
- importedCards.push(cardApi.createCard('boundary', {
906
- scope: b.scope || '', out_of_scope: b.out_of_scope || '',
907
- acceptable_exceptions: b.acceptable_exceptions || [],
908
- }, b.id || null));
1152
+ importedCards.push(cardApi.createCard('boundary', importedFields(b), b.id || null));
909
1153
  }
910
1154
  // Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
911
1155
  // (either as `{risks: [...]}` or as an array itself).
@@ -916,23 +1160,16 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
916
1160
  else risksSource = [];
917
1161
  }
918
1162
  for (const r of risksSource) {
919
- importedCards.push(cardApi.createCard('risk', r.fields || r, r.id || null));
1163
+ importedCards.push(cardApi.createCard('risk', importedFields(r.fields || r), r.id || null));
920
1164
  }
921
1165
  // NEW: import stances
922
1166
  for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
923
- const text = typeof s === 'string' ? s : s.statement || s.stance || '';
924
- importedCards.push(cardApi.createCard('stance', {
925
- statement: text,
926
- applies_when: s.applies_when || [],
927
- does_not_apply_when: s.does_not_apply_when || [],
928
- }, 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));
929
1169
  }
930
1170
  // NEW: import frameworks
931
1171
  for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
932
- importedCards.push(cardApi.createCard('framework', {
933
- name: fw.name || '', when_to_use: fw.when_to_use || '',
934
- steps: fw.steps || [],
935
- }, fw.id || null));
1172
+ importedCards.push(cardApi.createCard('framework', importedFields(fw), fw.id || null));
936
1173
  }
937
1174
  }
938
1175
 
@@ -940,27 +1177,25 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
940
1177
  // NEW: import terminology
941
1178
  if (patterns.terminology) {
942
1179
  for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
943
- importedCards.push(cardApi.createCard('term', {
944
- term: t.term || '', definition: t.definition || '',
945
- }, t.id || null));
1180
+ importedCards.push(cardApi.createCard('term', importedFields(t), t.id || null));
946
1181
  }
947
1182
  for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
948
- importedCards.push(cardApi.createCard('banned_term', {
949
- term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
950
- }, bt.id || null));
1183
+ importedCards.push(cardApi.createCard('banned_term', importedFields(bt), bt.id || null));
951
1184
  }
952
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
+ }
953
1191
  for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
954
- importedCards.push(cardApi.createCard('misunderstanding', {
955
- wrong: ms.wrong || '', correct: ms.correct || '',
956
- key_distinction: ms.key_distinction || '', why: ms.why || '',
957
- }, ms.id || null));
1192
+ importedCards.push(cardApi.createCard('misunderstanding', importedFields(ms), ms.id || null));
958
1193
  }
959
1194
  for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
960
- const q = typeof sc === 'string' ? sc : sc.question || '';
1195
+ const fields = typeof sc === 'string' ? { question: sc } : importedFields(sc);
961
1196
  importedCards.push(cardApi.createCard(
962
1197
  'self_check',
963
- { question: q },
1198
+ fields,
964
1199
  typeof sc === 'object' ? sc.id || null : null,
965
1200
  ));
966
1201
  }
@@ -973,7 +1208,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
973
1208
  for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
974
1209
  importedCards.push(cardApi.createCard(
975
1210
  'aesthetic',
976
- aesthetic.fields || aesthetic,
1211
+ importedFields(aesthetic.fields || aesthetic),
977
1212
  aesthetic.id || null,
978
1213
  ));
979
1214
  }
@@ -982,20 +1217,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
982
1217
  // NEW: import scenarios
983
1218
  if (scenarios && Array.isArray(scenarios.scenes)) {
984
1219
  for (const scene of scenarios.scenes) {
985
- importedCards.push(cardApi.createCard('scenario', {
986
- name: scene.name || scene.id || '',
987
- trigger: scene.trigger || '', action: scene.action || '',
988
- }, scene.id || null));
1220
+ importedCards.push(cardApi.createCard('scenario', importedFields(scene), scene.id || null));
989
1221
  }
990
1222
  }
991
1223
 
992
1224
  // NEW: import cases
993
1225
  if (cases && Array.isArray(cases.cases)) {
994
1226
  for (const c of cases.cases) {
995
- importedCards.push(cardApi.createCard('case', {
996
- title: c.title || c.id || '',
997
- scenario: c.scenario || '', expected: c.expected || '',
998
- }, c.id || null));
1227
+ importedCards.push(cardApi.createCard('case', importedFields(c), c.id || null));
999
1228
  }
1000
1229
  }
1001
1230
 
@@ -1008,26 +1237,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1008
1237
  // identity.
1009
1238
  if (reasoning && Array.isArray(reasoning.reasoning_chains)) {
1010
1239
  for (const rc of reasoning.reasoning_chains) {
1011
- importedCards.push(cardApi.createCard('reasoning', {
1012
- axiom: rc.axiom || '',
1013
- one_sentence: rc.one_sentence || rc.conclusion || '',
1014
- chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
1015
- principle: rc.principle || rc.name || '',
1016
- concrete_action: rc.concrete_action || rc.so_what || '',
1017
- so_what: rc.so_what || rc.concrete_action || '',
1018
- conclusion: rc.conclusion || '',
1019
- }, rc.id || null));
1240
+ importedCards.push(cardApi.createCard('reasoning', importedFields(rc), rc.id || null));
1020
1241
  }
1021
1242
  }
1022
1243
 
1023
1244
  // NEW: import evolution stages
1024
1245
  if (evolution && Array.isArray(evolution.stages)) {
1025
1246
  for (const stage of evolution.stages) {
1026
- pushImportedCard(importedCards, 'evolution_stage', {
1027
- name: stage.name || stage.id || '',
1028
- level: stage.level != null ? stage.level : '',
1029
- description: stage.description || '',
1030
- }, stage.id || null);
1247
+ pushImportedCard(importedCards, 'evolution_stage', importedFields(stage), stage.id || null);
1031
1248
  }
1032
1249
  }
1033
1250
 
@@ -1038,6 +1255,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1038
1255
  sourcePath: absSource,
1039
1256
  creatorIdentity: creatorIdentity || null,
1040
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 || {} }),
1041
1259
  });
1042
1260
  // Preserve manifest metadata
1043
1261
  if (manifestData.asset_uid) project.asset_uid = manifestData.asset_uid;
@@ -1053,11 +1271,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1053
1271
  if (!project.release) project.release = {};
1054
1272
  project.release.description = manifestData.description;
1055
1273
  }
1274
+ if (manifestData.title) project.title = manifestData.title;
1056
1275
  if (manifestData.license) project.license = manifestData.license;
1057
1276
  if (manifestData.author) project.author = manifestData.author;
1058
1277
  if (manifestData.languages) project.languages = manifestData.languages;
1059
1278
  if (manifestData.default_language) project.default_language = manifestData.default_language;
1060
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
+ }
1061
1286
 
1062
1287
  project.cards = importedCards;
1063
1288
  if (project.stages?.judgment_cards) {
@@ -1074,10 +1299,12 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1074
1299
  }
1075
1300
 
1076
1301
  /**
1077
- * Minimal ZIP central directory parser — reads entries into a Map.
1302
+ * Minimal ZIP central directory parser — validates every entry before decompressing.
1078
1303
  */
1079
1304
  function readZipEntries(buf) {
1080
1305
  const entries = new Map();
1306
+ const metadata = [];
1307
+
1081
1308
  // Find end-of-central-directory record
1082
1309
  let eocdOffset = buf.length - 22;
1083
1310
  while (eocdOffset >= 0) {
@@ -1089,6 +1316,8 @@ function readZipEntries(buf) {
1089
1316
  const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
1090
1317
  let offset = centralDirOffset;
1091
1318
 
1319
+ // Pass 1: collect metadata, validate safety before any decompression
1320
+ let totalUncomp = 0;
1092
1321
  while (offset < eocdOffset) {
1093
1322
  const sig = buf.readUInt32LE(offset);
1094
1323
  if (sig !== 0x02014b50) break;
@@ -1101,20 +1330,47 @@ function readZipEntries(buf) {
1101
1330
  const localOffset = buf.readUInt32LE(offset + 42);
1102
1331
  const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
1103
1332
 
1104
- // Read local file header to get data
1105
- const localNameLen = buf.readUInt16LE(localOffset + 26);
1106
- const localExtraLen = buf.readUInt16LE(localOffset + 28);
1107
- const dataStart = localOffset + 30 + localNameLen + localExtraLen;
1108
-
1109
- if (compMethod === 0) {
1110
- entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
1111
- } else if (compMethod === 8) {
1112
- 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`);
1113
1336
  }
1114
-
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 });
1115
1353
  offset += 46 + nameLen + extraLen + commentLen;
1116
1354
  }
1117
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
+
1118
1374
  return entries;
1119
1375
  }
1120
1376
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.10.1",
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": {
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@aikdna/kdna-core": "0.20.0",
52
- "@aikdna/kdna-studio-core": "2.0.1",
52
+ "@aikdna/kdna-studio-core": "2.0.2",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
55
55
  "cbor-x": "^1.6.4"