@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 +5 -0
- package/bin/kdna-studio.js +385 -129
- package/package.json +2 -2
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
|
package/bin/kdna-studio.js
CHANGED
|
@@ -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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 =
|
|
504
|
-
delete fields.type;
|
|
507
|
+
fields = importedFields(pattern);
|
|
505
508
|
} else {
|
|
506
|
-
|
|
507
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
760
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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:
|
|
812
|
-
|
|
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)
|
|
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
|
|
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
|
|
1195
|
+
const fields = typeof sc === 'string' ? { question: sc } : importedFields(sc);
|
|
961
1196
|
importedCards.push(cardApi.createCard(
|
|
962
1197
|
'self_check',
|
|
963
|
-
|
|
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 —
|
|
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
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
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.
|
|
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.
|
|
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"
|