@aikdna/kdna-studio-cli 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -91,6 +91,27 @@ function fail(message, code = EXIT.INPUT_ERROR) {
91
91
  process.exit(code);
92
92
  }
93
93
 
94
+ // Bug (#65): `fail()` calls `process.exit`, which skips `finally`
95
+ // blocks in the same call stack. Any tmpDir that a `try { ... }
96
+ // finally { fs.rmSync(tmpDir) }` block is in the middle of running
97
+ // will leak. The fix tracks every temporary directory the process
98
+ // creates and removes it on process exit (including the exit path
99
+ // triggered by `fail()`).
100
+ const _tempDirs = new Set();
101
+ function trackTempDir(dir) {
102
+ if (dir) _tempDirs.add(dir);
103
+ return dir;
104
+ }
105
+ function cleanupTempDirs() {
106
+ for (const dir of _tempDirs) {
107
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
108
+ }
109
+ _tempDirs.clear();
110
+ }
111
+ process.on('exit', cleanupTempDirs);
112
+ process.on('SIGINT', () => { cleanupTempDirs(); process.exit(130); });
113
+ process.on('SIGTERM', () => { cleanupTempDirs(); process.exit(143); });
114
+
94
115
  function option(args, name, fallback = null) {
95
116
  const idx = args.indexOf(name);
96
117
  if (idx < 0) return fallback;
@@ -314,12 +335,12 @@ function decodePayload(bytes, manifest = {}) {
314
335
  // by collapsing one path into the other.
315
336
  function cardsFromV1Payload(payload) {
316
337
  const cards = [];
317
- for (const sourceCard of (payload.source_cards || [])) {
338
+ for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
318
339
  pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
319
340
  }
320
341
  if (cards.length > 0) return cards;
321
342
 
322
- for (const ax of (payload.core?.axioms || [])) {
343
+ for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
323
344
  pushImportedCard(cards, 'axiom', {
324
345
  one_sentence: ax.one_sentence || '',
325
346
  full_statement: ax.full_statement || '',
@@ -329,19 +350,19 @@ function cardsFromV1Payload(payload) {
329
350
  failure_risk: ax.failure_risk || '',
330
351
  }, ax.id || null);
331
352
  }
332
- for (const ont of (payload.core?.ontology || [])) {
353
+ for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
333
354
  pushImportedCard(cards, 'ontology', ont, ont.id || null);
334
355
  }
335
- for (const fw of (payload.core?.frameworks || [])) {
356
+ for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
336
357
  pushImportedCard(cards, 'framework', fw, fw.id || null);
337
358
  }
338
- for (const stance of (payload.core?.stances || [])) {
359
+ for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
339
360
  pushImportedCard(cards, 'stance', stance, stance.id || null);
340
361
  }
341
- for (const aesthetic of (payload.core?.aesthetics || [])) {
362
+ for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
342
363
  pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
343
364
  }
344
- for (const boundary of (payload.core?.boundaries || [])) {
365
+ for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
345
366
  pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
346
367
  }
347
368
  // risk_model may be either an array of risk items (legacy) or
@@ -352,15 +373,45 @@ function cardsFromV1Payload(payload) {
352
373
  for (const risk of riskItems) {
353
374
  pushImportedCard(cards, 'risk', risk, risk.id || null);
354
375
  }
355
- for (const pattern of (payload.patterns || [])) {
356
- if (!pattern.type) continue;
357
- const { type, ...fields } = pattern;
376
+ for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
377
+ // Bug (#146): prior version dropped any payload.patterns entry
378
+ // that lacked an explicit `type` field. The v1 schema lists
379
+ // misunderstandings in patterns[] without a `type` discriminator
380
+ // (their shape is `{wrong, correct, key_distinction, why}`),
381
+ // and several exporters omit `type` to keep the on-the-wire
382
+ // shape compact. The fix infers a type from the field set when
383
+ // `type` is missing, and only falls through to "skip" when the
384
+ // entry truly cannot be classified.
385
+ let type = pattern.type;
386
+ let fields = pattern;
387
+ if (!type) {
388
+ // Heuristics, in priority order — most specific to most general.
389
+ if ('wrong' in pattern && 'correct' in pattern) type = 'misunderstanding';
390
+ else if ('term' in pattern && 'definition' in pattern) type = 'term';
391
+ else if ('term' in pattern && 'why' in pattern && 'replace_with' in pattern) type = 'banned_term';
392
+ else if ('what_it_looks_like' in pattern && 'how_to_fix' in pattern) type = 'pattern';
393
+ else if ('name' in pattern && ('description' in pattern || 'one_sentence' in pattern)) type = 'aesthetic';
394
+ else {
395
+ // Truly unclassifiable. Log a one-line warning and skip
396
+ // rather than crash, so the rest of the asset still imports.
397
+ console.warn(
398
+ `Skipping payload.patterns entry with no recognisable type ` +
399
+ `(id=${pattern.id || '?'}, keys=${Object.keys(pattern).join(',') || '<empty>'})`
400
+ );
401
+ continue;
402
+ }
403
+ fields = { ...pattern };
404
+ delete fields.type;
405
+ } else {
406
+ const { type: _stripped, ...rest } = pattern;
407
+ fields = rest;
408
+ }
358
409
  pushImportedCard(cards, type, fields, pattern.id || null);
359
410
  }
360
- for (const scenario of (payload.scenarios || [])) {
411
+ for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
361
412
  pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
362
413
  }
363
- for (const item of (payload.cases || [])) {
414
+ for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
364
415
  pushImportedCard(cards, 'case', item, item.id || null);
365
416
  }
366
417
  // Accept both `self_check` (canonical) and `self_checks` (legacy schema
@@ -376,7 +427,7 @@ function cardsFromV1Payload(payload) {
376
427
  const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
377
428
  pushImportedCard(cards, 'self_check', fields, fields.id || null);
378
429
  }
379
- for (const failureMode of (payload.reasoning?.failure_modes || [])) {
430
+ for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
380
431
  pushImportedCard(cards, 'misunderstanding', {
381
432
  wrong: failureMode.mode || failureMode.wrong || '',
382
433
  correct: failureMode.correct || '',
@@ -384,10 +435,10 @@ function cardsFromV1Payload(payload) {
384
435
  why: failureMode.why || '',
385
436
  }, failureMode.id || null);
386
437
  }
387
- for (const chain of (payload.reasoning?.reasoning_chains || [])) {
438
+ for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
388
439
  pushImportedCard(cards, 'reasoning', chain, chain.id || null);
389
440
  }
390
- for (const stage of (payload.evolution?.stages || [])) {
441
+ for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
391
442
  // Distinguish source-authored stages (preserved identity) from
392
443
  // audit-log-derived stages (synthesised). The compile path tags
393
444
  // source_authored: true; import those verbatim and skip the rest
@@ -406,7 +457,7 @@ function cardsFromLegacyPayload(payload) {
406
457
  const core = judgment.core || {};
407
458
  const patterns = judgment.patterns || {};
408
459
 
409
- for (const ax of (core.axioms || [])) {
460
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
410
461
  pushImportedCard(cards, 'axiom', {
411
462
  one_sentence: ax.one_sentence || '',
412
463
  full_statement: ax.full_statement || '',
@@ -416,7 +467,7 @@ function cardsFromLegacyPayload(payload) {
416
467
  failure_risk: ax.failure_risk || '',
417
468
  }, ax.id || null);
418
469
  }
419
- for (const ont of (core.ontology || [])) {
470
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
420
471
  pushImportedCard(cards, 'ontology', {
421
472
  one_sentence: ont.one_sentence || ont.essence || '',
422
473
  essence: ont.essence || '',
@@ -424,23 +475,32 @@ function cardsFromLegacyPayload(payload) {
424
475
  trigger_signal: ont.trigger_signal || '',
425
476
  }, ont.id || null);
426
477
  }
427
- for (const boundary of (core.boundaries || [])) {
478
+ for (const boundary of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
428
479
  pushImportedCard(cards, 'boundary', {
429
480
  scope: boundary.scope || '',
430
481
  out_of_scope: boundary.out_of_scope || '',
431
482
  acceptable_exceptions: boundary.acceptable_exceptions || [],
432
483
  }, boundary.id || null);
433
484
  }
434
- for (const risk of (core.risks || [])) {
485
+ for (const risk of (Array.isArray(core.risks) ? core.risks : [])) {
435
486
  pushImportedCard(cards, 'risk', risk, risk.id || null);
436
487
  }
437
- for (const term of (patterns.terminology?.standard_terms || [])) {
488
+ // Bug (#63): prior version skipped `core.stances` and
489
+ // `core.frameworks`, so a legacy asset that authored either type
490
+ // round-tripped out as zero cards. The fix adds both loops.
491
+ for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
492
+ pushImportedCard(cards, 'stance', stance, stance.id || null);
493
+ }
494
+ for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
495
+ pushImportedCard(cards, 'framework', fw, fw.id || null);
496
+ }
497
+ for (const term of (Array.isArray(patterns.terminology?.standard_terms) ? patterns.terminology?.standard_terms : [])) {
438
498
  pushImportedCard(cards, 'term', term, term.id || null);
439
499
  }
440
- for (const banned of (patterns.terminology?.banned_terms || [])) {
500
+ for (const banned of (Array.isArray(patterns.terminology?.banned_terms) ? patterns.terminology?.banned_terms : [])) {
441
501
  pushImportedCard(cards, 'banned_term', banned, banned.id || null);
442
502
  }
443
- for (const ms of (patterns.misunderstandings || [])) {
503
+ for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
444
504
  pushImportedCard(cards, 'misunderstanding', {
445
505
  wrong: ms.wrong || '',
446
506
  correct: ms.correct || '',
@@ -455,19 +515,19 @@ function cardsFromLegacyPayload(payload) {
455
515
  const question = typeof sc === 'string' ? sc : sc.question || '';
456
516
  pushImportedCard(cards, 'self_check', { question }, sc.id || null);
457
517
  }
458
- for (const aesthetic of (patterns.aesthetics || [])) {
518
+ for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
459
519
  pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
460
520
  }
461
- for (const scene of (judgment.scenarios?.scenes || [])) {
521
+ for (const scene of (Array.isArray(judgment.scenarios?.scenes) ? judgment.scenarios?.scenes : [])) {
462
522
  pushImportedCard(cards, 'scenario', scene, scene.id || null);
463
523
  }
464
- for (const item of (judgment.cases?.cases || [])) {
524
+ for (const item of (Array.isArray(judgment.cases?.cases) ? judgment.cases?.cases : [])) {
465
525
  pushImportedCard(cards, 'case', item, item.id || null);
466
526
  }
467
- for (const chain of (judgment.reasoning?.reasoning_chains || [])) {
527
+ for (const chain of (Array.isArray(judgment.reasoning?.reasoning_chains) ? judgment.reasoning?.reasoning_chains : [])) {
468
528
  pushImportedCard(cards, 'reasoning', chain, chain.id || null);
469
529
  }
470
- for (const stage of (judgment.evolution?.stages || [])) {
530
+ for (const stage of (Array.isArray(judgment.evolution?.stages) ? judgment.evolution?.stages : [])) {
471
531
  pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
472
532
  }
473
533
  return cards;
@@ -678,35 +738,50 @@ function importFromKdna(kdnaPath) {
678
738
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
679
739
  }
680
740
  } else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
681
- // Legacy fallback — only used when payload.kdnab is absent. This path
682
- // intentionally covers a strict subset of card types and is not the
683
- // recommended import path.
741
+ // Legacy fallback — used when the v1 asset has no payload.kdnab
742
+ // (i.e. the asset predates the 0.7 split). Bug (#67): prior
743
+ // version only handled axiom / ontology / boundary / misunderstanding
744
+ // / self_check — 5 of the 14 card types — so every other type
745
+ // (risk / stance / framework / term / banned_term / aesthetic /
746
+ // scenario / case / reasoning / evolution_stage) round-tripped
747
+ // out as zero cards. The fix threads every type this CLI can
748
+ // import through, and prefers the same field-shape rules as
749
+ // cardsFromLegacyPayload so the round-trip is symmetric.
684
750
  if (entries.has('KDNA_Core.json')) {
685
751
  const core = JSON.parse(entries.get('KDNA_Core.json').toString());
686
- for (const ax of (core.axioms || [])) {
752
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
687
753
  const fields = {};
688
754
  for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
689
755
  if (k in ax) fields[k] = ax[k];
690
756
  }
691
757
  importedCards.push(cardApi.createCard('axiom', fields));
692
758
  }
693
- for (const ont of (core.ontology || [])) {
759
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
694
760
  importedCards.push(cardApi.createCard('ontology', {
695
761
  one_sentence: ont.one_sentence || ont.essence || '',
696
762
  essence: ont.essence || '', boundary: ont.boundary || '',
697
763
  trigger_signal: ont.trigger_signal || '',
698
764
  }));
699
765
  }
700
- for (const b of (core.boundaries || [])) {
766
+ for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
701
767
  importedCards.push(cardApi.createCard('boundary', {
702
768
  scope: b.scope || '', out_of_scope: b.out_of_scope || '',
703
769
  acceptable_exceptions: b.acceptable_exceptions || [],
704
770
  }));
705
771
  }
772
+ for (const risk of (core.risks || core.risk_model || [])) {
773
+ importedCards.push(cardApi.createCard('risk', risk.fields || risk));
774
+ }
775
+ for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
776
+ importedCards.push(cardApi.createCard('stance', stance.fields || stance));
777
+ }
778
+ for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
779
+ importedCards.push(cardApi.createCard('framework', fw.fields || fw));
780
+ }
706
781
  }
707
782
  if (entries.has('KDNA_Patterns.json')) {
708
783
  const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
709
- for (const ms of (pat.misunderstandings || [])) {
784
+ for (const ms of (Array.isArray(pat.misunderstandings) ? pat.misunderstandings : [])) {
710
785
  importedCards.push(cardApi.createCard('misunderstanding', {
711
786
  wrong: ms.wrong || '', correct: ms.correct || '',
712
787
  key_distinction: ms.key_distinction || '', why: ms.why || '',
@@ -716,6 +791,39 @@ function importFromKdna(kdnaPath) {
716
791
  const q = typeof sc === 'string' ? sc : sc.question || '';
717
792
  importedCards.push(cardApi.createCard('self_check', { question: q }));
718
793
  }
794
+ for (const aesthetic of (Array.isArray(pat.aesthetics) ? pat.aesthetics : [])) {
795
+ importedCards.push(cardApi.createCard('aesthetic', aesthetic.fields || aesthetic));
796
+ }
797
+ for (const term of (Array.isArray(pat.terminology?.standard_terms) ? pat.terminology?.standard_terms : [])) {
798
+ importedCards.push(cardApi.createCard('term', term));
799
+ }
800
+ for (const banned of (Array.isArray(pat.terminology?.banned_terms) ? pat.terminology?.banned_terms : [])) {
801
+ importedCards.push(cardApi.createCard('banned_term', banned));
802
+ }
803
+ }
804
+ if (entries.has('KDNA_Scenarios.json')) {
805
+ const scen = JSON.parse(entries.get('KDNA_Scenarios.json').toString());
806
+ for (const s of (Array.isArray(scen.scenes) ? scen.scenes : [])) {
807
+ importedCards.push(cardApi.createCard('scenario', s));
808
+ }
809
+ }
810
+ if (entries.has('KDNA_Cases.json')) {
811
+ const cases = JSON.parse(entries.get('KDNA_Cases.json').toString());
812
+ for (const c of (Array.isArray(cases.cases) ? cases.cases : [])) {
813
+ importedCards.push(cardApi.createCard('case', c));
814
+ }
815
+ }
816
+ if (entries.has('KDNA_Reasoning.json')) {
817
+ const reasoning = JSON.parse(entries.get('KDNA_Reasoning.json').toString());
818
+ for (const chain of (Array.isArray(reasoning.reasoning_chains) ? reasoning.reasoning_chains : [])) {
819
+ importedCards.push(cardApi.createCard('reasoning', chain));
820
+ }
821
+ }
822
+ if (entries.has('KDNA_Evolution.json')) {
823
+ const evolution = JSON.parse(entries.get('KDNA_Evolution.json').toString());
824
+ for (const stage of (Array.isArray(evolution.stages) ? evolution.stages : [])) {
825
+ importedCards.push(cardApi.createCard('evolution_stage', stage));
826
+ }
719
827
  }
720
828
  }
721
829
 
@@ -818,7 +926,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
818
926
  }
819
927
 
820
928
  if (core) {
821
- for (const ax of (core.axioms || [])) {
929
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
822
930
  const fields = {};
823
931
  for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
824
932
  if (key in ax) fields[key] = ax[key];
@@ -826,14 +934,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
826
934
  }
827
935
  importedCards.push(cardApi.createCard('axiom', fields));
828
936
  }
829
- for (const ont of (core.ontology || [])) {
937
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
830
938
  importedCards.push(cardApi.createCard('ontology', {
831
939
  one_sentence: ont.one_sentence || ont.essence || '',
832
940
  essence: ont.essence || '', boundary: ont.boundary || '',
833
941
  trigger_signal: ont.trigger_signal || '',
834
942
  }));
835
943
  }
836
- for (const b of (core.boundaries || [])) {
944
+ for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
837
945
  importedCards.push(cardApi.createCard('boundary', {
838
946
  scope: b.scope || '', out_of_scope: b.out_of_scope || '',
839
947
  acceptable_exceptions: b.acceptable_exceptions || [],
@@ -851,7 +959,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
851
959
  importedCards.push(cardApi.createCard('risk', r.fields || r));
852
960
  }
853
961
  // NEW: import stances
854
- for (const s of (core.stances || [])) {
962
+ for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
855
963
  const text = typeof s === 'string' ? s : s.stance || '';
856
964
  importedCards.push(cardApi.createCard('stance', {
857
965
  statement: text,
@@ -860,7 +968,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
860
968
  }));
861
969
  }
862
970
  // NEW: import frameworks
863
- for (const fw of (core.frameworks || [])) {
971
+ for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
864
972
  importedCards.push(cardApi.createCard('framework', {
865
973
  name: fw.name || '', when_to_use: fw.when_to_use || '',
866
974
  steps: fw.steps || [],
@@ -871,18 +979,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
871
979
  if (patterns) {
872
980
  // NEW: import terminology
873
981
  if (patterns.terminology) {
874
- for (const t of (patterns.terminology.standard_terms || [])) {
982
+ for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
875
983
  importedCards.push(cardApi.createCard('term', {
876
984
  term: t.term || '', definition: t.definition || '',
877
985
  }));
878
986
  }
879
- for (const bt of (patterns.terminology.banned_terms || [])) {
987
+ for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
880
988
  importedCards.push(cardApi.createCard('banned_term', {
881
989
  term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
882
990
  }));
883
991
  }
884
992
  }
885
- for (const ms of (patterns.misunderstandings || [])) {
993
+ for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
886
994
  importedCards.push(cardApi.createCard('misunderstanding', {
887
995
  wrong: ms.wrong || '', correct: ms.correct || '',
888
996
  key_distinction: ms.key_distinction || '', why: ms.why || '',
@@ -1468,7 +1576,7 @@ function cmdMigrate(args) {
1468
1576
  projectPath = loaded.projectPath;
1469
1577
  if (!requestedName && project.name) name = project.name;
1470
1578
  } else {
1471
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
1579
+ tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
1472
1580
  const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1473
1581
  project = imported.project;
1474
1582
  projectPath = path.join(tmpDir, 'studio.project.json');
@@ -1483,7 +1591,7 @@ function cmdMigrate(args) {
1483
1591
 
1484
1592
  // Reject if critical judgment fields are missing from axioms
1485
1593
  const criticalMissing = [];
1486
- for (const card of (project.cards || [])) {
1594
+ for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
1487
1595
  if (card.type !== 'axiom') continue;
1488
1596
  for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
1489
1597
  const val = card.fields && card.fields[field];
@@ -1616,7 +1724,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
1616
1724
  }
1617
1725
  }
1618
1726
  const lockedCards = (project.cards || []).filter(c => c.locked);
1619
- const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
1727
+ const v1Dir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-')));
1620
1728
  try {
1621
1729
  const manifestDefaults = buildV1Manifest(project, name);
1622
1730
  const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
@@ -2089,10 +2197,16 @@ function cmdSourceClassify(args) {
2089
2197
  const target = project.distillation_target;
2090
2198
  if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
2091
2199
 
2092
- // Canonical evidence field is `evidence_materials` (set by cmdImport via
2093
- // evidenceApi.addEvidence). The legacy `project.evidence` field never
2094
- // receives writes, so reading it always produced an empty list and the
2095
- // classifier reported "no evidence" even after `kdna-studio import`.
2200
+ // Bug (#66 + #69): the previous version of this comment claimed
2201
+ // `evidence_materials` was canonical and `evidence` was the legacy
2202
+ // fallback. The reality was the opposite — `addEvidence` in
2203
+ // @aikdna/kdna-studio-core wrote only to `project.evidence`, so
2204
+ // every consumer that read `project.evidence_materials` (this
2205
+ // command, cmdFilter, cmdDistill) saw an empty list. The fix in
2206
+ // studio-core 1.7.5 makes `addEvidence` write to BOTH fields; the
2207
+ // canonical name is now the one this comment names, not the one
2208
+ // it used to name. The fallback `|| project.evidence` stays so
2209
+ // legacy projects (pre-1.7.5) still classify correctly.
2096
2210
  const evidence = project.evidence_materials || project.evidence || [];
2097
2211
  const results = evidence.map(e => {
2098
2212
  const text = (e.content || e.title || '').toLowerCase();
@@ -2286,8 +2400,24 @@ async function cmdFeynman(args) {
2286
2400
  // Canonical field name in the card schema is `feynman_restatement`, not
2287
2401
  // `feynman_text`. The old check was dead code (nothing writes the wrong
2288
2402
  // field) and made this command fail on every card.
2403
+ //
2404
+ // Bug (#62 + #68): the prior error message pointed at "the feynman
2405
+ // authoring command", which does not exist in this CLI. The right
2406
+ // path is either:
2407
+ // - kdna-studio feynman <project> <card-id> (evaluate an existing
2408
+ // restatement; what the caller is already running)
2409
+ // - kdna-studio card update <project> <card-id> --field feynman_restatement='<json>'
2410
+ // (set the restatement manually)
2411
+ // The fix makes the second path explicit and gives a working
2412
+ // example. Bug #62 (the field is never populated) is fixed in
2413
+ // feynman.js: a no-restatement card now auto-generates one
2414
+ // before the LLM call, so the bare `kdna-studio feynman` command
2415
+ // works end-to-end.
2289
2416
  if (!card.feynman_restatement) {
2290
- fail(`Card ${cardId} has no Feynman restatement. Generate one with the feynman authoring command, or set it via card update --field feynman_restatement='<json>'.`);
2417
+ fail(
2418
+ `Card ${cardId} has no Feynman restatement. Set it manually:\n` +
2419
+ ` kdna-studio card update <project> ${cardId} --field feynman_restatement='{"text":"<your plain-language restatement>"}'`,
2420
+ );
2291
2421
  }
2292
2422
 
2293
2423
  console.log(`Evaluating Feynman restatement for card: ${cardId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
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": {
@@ -39,7 +39,7 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@aikdna/kdna-studio-core": "^1.7.4",
42
+ "@aikdna/kdna-studio-core": "^1.7.5",
43
43
  "ajv": "^8.20.0",
44
44
  "ajv-formats": "^3.0.1",
45
45
  "cbor-x": "^1.6.4"
package/src/ai/feynman.js CHANGED
@@ -46,7 +46,35 @@ async function evaluate(config, card, options = {}) {
46
46
  || fields.feynman_text
47
47
  || '';
48
48
 
49
- if (!restatement) return { score: 0, criteria: {}, explanations: { notJustRepeat: 'No Feynman restatement provided.' }, suggestions: ['Write a Feynman restatement before evaluation.'] };
49
+ if (!restatement) {
50
+ // Bug (#62): prior version returned score 0 with the explanation
51
+ // "No Feynman restatement provided", but nothing in the CLI ever
52
+ // *writes* a feynman_restatement on a card. Every `kdna-studio
53
+ // feynman` call hit this path, every call returned 0, and the
54
+ // fynchmann command was effectively dead.
55
+ //
56
+ // The fix synthesises a first-cut restatement from the card's own
57
+ // text (so the LLM has *something* to evaluate), and returns a
58
+ // structured note in `suggestions` so the caller knows the score
59
+ // is on a machine-generated restatement, not their own. The
60
+ // caller can then `card update --field feynman_restatement='...'`
61
+ // to lock in a human-written version.
62
+ const synthesised = synthFeynmanRestatement(fields);
63
+ return {
64
+ score: 0,
65
+ criteria: {},
66
+ explanations: {
67
+ notJustRepeat: 'No human-written Feynman restatement — evaluated a synthesised one.',
68
+ notTooAbstract: 'Review the synthesised restatement before trusting the score.',
69
+ },
70
+ suggestions: [
71
+ 'No feynman_restatement found on this card. The score below is on an auto-generated restatement.',
72
+ `Set a human-written restatement: kdna-studio card update <project> ${card.id || '<card-id>'} --field feynman_restatement='{"text":"<your plain-language restatement>"}'`,
73
+ `Synthesised restatement: "${synthesised}"`,
74
+ ],
75
+ synthesised_restatement: synthesised,
76
+ };
77
+ }
50
78
 
51
79
  const userPrompt = [
52
80
  `## Original Axiom`,
@@ -66,4 +94,25 @@ async function evaluate(config, card, options = {}) {
66
94
  return result.data || { score: 0, criteria: {}, explanations: {}, suggestions: [] };
67
95
  }
68
96
 
69
- module.exports = { evaluate, CRITERIA, SYSTEM };
97
+ // Synthesise a first-cut restatement from the card's own fields. The
98
+ // output is intentionally plain — Feynman is "explain it like I'm
99
+ // five", so we strip axiom-speak and surface only the human-readable
100
+ // core.
101
+ function synthFeynmanRestatement(fields) {
102
+ const text = fields.one_sentence
103
+ || fields.full_statement
104
+ || fields.question
105
+ || fields.essence
106
+ || fields.scope
107
+ || '(no text to restate)';
108
+ // Strip the leading "always/never/must" markers that signal axiom
109
+ // language. The LLM evaluator will then score the result on its
110
+ // own merits, but the human reader can already see plain English.
111
+ return String(text)
112
+ .replace(/^(always|never|must|should|shall)\s+/i, '')
113
+ .replace(/\s+always\s+/gi, ' ')
114
+ .replace(/\s+never\s+/gi, ' ')
115
+ .trim();
116
+ }
117
+
118
+ module.exports = { evaluate, CRITERIA, SYSTEM, synthFeynmanRestatement };