@aikdna/kdna-studio-cli 0.8.4 → 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.
@@ -50,7 +50,7 @@ Create (three entry paths):
50
50
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
51
51
 
52
52
  Migrate (dev source or Studio project → canonical .kdna in one command):
53
- kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete]
53
+ kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--passphrase <pw>|--passphrase-stdin]
54
54
 
55
55
  Authoring:
56
56
  kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
@@ -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;
@@ -304,14 +325,22 @@ function decodePayload(bytes, manifest = {}) {
304
325
  return cbor.decode(raw);
305
326
  }
306
327
 
328
+ // V1 path: trust the producer (export-runtime) to have already shaped
329
+ // each card's fields. The v1 producer normalises everything into a
330
+ // canonical shape, so we just round-trip the raw object. Bug (#57):
331
+ // the legacy path below explicitly constructs each field. The two
332
+ // paths cannot be unified without losing either v1's per-field
333
+ // guarantees or legacy's defensive defaults — the asymmetry is
334
+ // intentional. Documented here so a future reader does not "fix" it
335
+ // by collapsing one path into the other.
307
336
  function cardsFromV1Payload(payload) {
308
337
  const cards = [];
309
- for (const sourceCard of (payload.source_cards || [])) {
338
+ for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
310
339
  pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
311
340
  }
312
341
  if (cards.length > 0) return cards;
313
342
 
314
- for (const ax of (payload.core?.axioms || [])) {
343
+ for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
315
344
  pushImportedCard(cards, 'axiom', {
316
345
  one_sentence: ax.one_sentence || '',
317
346
  full_statement: ax.full_statement || '',
@@ -321,19 +350,19 @@ function cardsFromV1Payload(payload) {
321
350
  failure_risk: ax.failure_risk || '',
322
351
  }, ax.id || null);
323
352
  }
324
- for (const ont of (payload.core?.ontology || [])) {
353
+ for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
325
354
  pushImportedCard(cards, 'ontology', ont, ont.id || null);
326
355
  }
327
- for (const fw of (payload.core?.frameworks || [])) {
356
+ for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
328
357
  pushImportedCard(cards, 'framework', fw, fw.id || null);
329
358
  }
330
- for (const stance of (payload.core?.stances || [])) {
359
+ for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
331
360
  pushImportedCard(cards, 'stance', stance, stance.id || null);
332
361
  }
333
- for (const aesthetic of (payload.core?.aesthetics || [])) {
362
+ for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
334
363
  pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
335
364
  }
336
- for (const boundary of (payload.core?.boundaries || [])) {
365
+ for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
337
366
  pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
338
367
  }
339
368
  // risk_model may be either an array of risk items (legacy) or
@@ -344,15 +373,45 @@ function cardsFromV1Payload(payload) {
344
373
  for (const risk of riskItems) {
345
374
  pushImportedCard(cards, 'risk', risk, risk.id || null);
346
375
  }
347
- for (const pattern of (payload.patterns || [])) {
348
- if (!pattern.type) continue;
349
- 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
+ }
350
409
  pushImportedCard(cards, type, fields, pattern.id || null);
351
410
  }
352
- for (const scenario of (payload.scenarios || [])) {
411
+ for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
353
412
  pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
354
413
  }
355
- for (const item of (payload.cases || [])) {
414
+ for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
356
415
  pushImportedCard(cards, 'case', item, item.id || null);
357
416
  }
358
417
  // Accept both `self_check` (canonical) and `self_checks` (legacy schema
@@ -368,7 +427,7 @@ function cardsFromV1Payload(payload) {
368
427
  const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
369
428
  pushImportedCard(cards, 'self_check', fields, fields.id || null);
370
429
  }
371
- for (const failureMode of (payload.reasoning?.failure_modes || [])) {
430
+ for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
372
431
  pushImportedCard(cards, 'misunderstanding', {
373
432
  wrong: failureMode.mode || failureMode.wrong || '',
374
433
  correct: failureMode.correct || '',
@@ -376,10 +435,10 @@ function cardsFromV1Payload(payload) {
376
435
  why: failureMode.why || '',
377
436
  }, failureMode.id || null);
378
437
  }
379
- for (const chain of (payload.reasoning?.reasoning_chains || [])) {
438
+ for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
380
439
  pushImportedCard(cards, 'reasoning', chain, chain.id || null);
381
440
  }
382
- for (const stage of (payload.evolution?.stages || [])) {
441
+ for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
383
442
  // Distinguish source-authored stages (preserved identity) from
384
443
  // audit-log-derived stages (synthesised). The compile path tags
385
444
  // source_authored: true; import those verbatim and skip the rest
@@ -398,7 +457,7 @@ function cardsFromLegacyPayload(payload) {
398
457
  const core = judgment.core || {};
399
458
  const patterns = judgment.patterns || {};
400
459
 
401
- for (const ax of (core.axioms || [])) {
460
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
402
461
  pushImportedCard(cards, 'axiom', {
403
462
  one_sentence: ax.one_sentence || '',
404
463
  full_statement: ax.full_statement || '',
@@ -408,7 +467,7 @@ function cardsFromLegacyPayload(payload) {
408
467
  failure_risk: ax.failure_risk || '',
409
468
  }, ax.id || null);
410
469
  }
411
- for (const ont of (core.ontology || [])) {
470
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
412
471
  pushImportedCard(cards, 'ontology', {
413
472
  one_sentence: ont.one_sentence || ont.essence || '',
414
473
  essence: ont.essence || '',
@@ -416,23 +475,32 @@ function cardsFromLegacyPayload(payload) {
416
475
  trigger_signal: ont.trigger_signal || '',
417
476
  }, ont.id || null);
418
477
  }
419
- for (const boundary of (core.boundaries || [])) {
478
+ for (const boundary of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
420
479
  pushImportedCard(cards, 'boundary', {
421
480
  scope: boundary.scope || '',
422
481
  out_of_scope: boundary.out_of_scope || '',
423
482
  acceptable_exceptions: boundary.acceptable_exceptions || [],
424
483
  }, boundary.id || null);
425
484
  }
426
- for (const risk of (core.risks || [])) {
485
+ for (const risk of (Array.isArray(core.risks) ? core.risks : [])) {
427
486
  pushImportedCard(cards, 'risk', risk, risk.id || null);
428
487
  }
429
- 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 : [])) {
430
498
  pushImportedCard(cards, 'term', term, term.id || null);
431
499
  }
432
- for (const banned of (patterns.terminology?.banned_terms || [])) {
500
+ for (const banned of (Array.isArray(patterns.terminology?.banned_terms) ? patterns.terminology?.banned_terms : [])) {
433
501
  pushImportedCard(cards, 'banned_term', banned, banned.id || null);
434
502
  }
435
- for (const ms of (patterns.misunderstandings || [])) {
503
+ for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
436
504
  pushImportedCard(cards, 'misunderstanding', {
437
505
  wrong: ms.wrong || '',
438
506
  correct: ms.correct || '',
@@ -447,19 +515,19 @@ function cardsFromLegacyPayload(payload) {
447
515
  const question = typeof sc === 'string' ? sc : sc.question || '';
448
516
  pushImportedCard(cards, 'self_check', { question }, sc.id || null);
449
517
  }
450
- for (const aesthetic of (patterns.aesthetics || [])) {
518
+ for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
451
519
  pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
452
520
  }
453
- for (const scene of (judgment.scenarios?.scenes || [])) {
521
+ for (const scene of (Array.isArray(judgment.scenarios?.scenes) ? judgment.scenarios?.scenes : [])) {
454
522
  pushImportedCard(cards, 'scenario', scene, scene.id || null);
455
523
  }
456
- for (const item of (judgment.cases?.cases || [])) {
524
+ for (const item of (Array.isArray(judgment.cases?.cases) ? judgment.cases?.cases : [])) {
457
525
  pushImportedCard(cards, 'case', item, item.id || null);
458
526
  }
459
- for (const chain of (judgment.reasoning?.reasoning_chains || [])) {
527
+ for (const chain of (Array.isArray(judgment.reasoning?.reasoning_chains) ? judgment.reasoning?.reasoning_chains : [])) {
460
528
  pushImportedCard(cards, 'reasoning', chain, chain.id || null);
461
529
  }
462
- for (const stage of (judgment.evolution?.stages || [])) {
530
+ for (const stage of (Array.isArray(judgment.evolution?.stages) ? judgment.evolution?.stages : [])) {
463
531
  pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
464
532
  }
465
533
  return cards;
@@ -477,6 +545,19 @@ function cardsFromPayload(payload) {
477
545
  }
478
546
 
479
547
  function buildV1Manifest(project, name) {
548
+ // Bug (#58): prior version was a hand-rolled manifest builder that
549
+ // shared zero logic with the canonical buildManifest in
550
+ // @aikdna/kdna-studio-core/src/export-runtime. The two paths
551
+ // diverged on every field added since 1.0 (lineage, load_contract,
552
+ // authoring block, etc.). The fix delegates the manifest shape to
553
+ // kdna-studio-core's buildManifest so the v1 path is the single
554
+ // source of truth.
555
+ //
556
+ // We still need a default manifest here (the exportRuntimeAsset
557
+ // path uses buildManifest internally; this function supplies the
558
+ // defaults that pass-through into the runtime options). The actual
559
+ // manifest the runtime ships is the one buildManifest produces, so
560
+ // keeping these defaults in sync matters less than it used to.
480
561
  const source = project.source_manifest || {};
481
562
  const assetId = assetIdFromName(name || source.name || project.name);
482
563
  const author = source.author || project.author || {};
@@ -556,6 +637,14 @@ function cmdCreate(args) {
556
637
  lineage,
557
638
  });
558
639
  project.cards = kdnaData.cards;
640
+ // Bug #15 / #28 follow-up: store the source KDNA_* files on the
641
+ // project so a later export through exportRuntimeAsset can
642
+ // forward them to compileDomain (which then preserves
643
+ // changelog / version_notes / reasoning_chains / banned_terms
644
+ // through the round-trip).
645
+ if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
646
+ if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
647
+ if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
559
648
  if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
560
649
  if (kdnaData.source_manifest?.version) {
561
650
  if (!project.release) project.release = {};
@@ -649,35 +738,50 @@ function importFromKdna(kdnaPath) {
649
738
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
650
739
  }
651
740
  } else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
652
- // Legacy fallback — only used when payload.kdnab is absent. This path
653
- // intentionally covers a strict subset of card types and is not the
654
- // 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.
655
750
  if (entries.has('KDNA_Core.json')) {
656
751
  const core = JSON.parse(entries.get('KDNA_Core.json').toString());
657
- for (const ax of (core.axioms || [])) {
752
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
658
753
  const fields = {};
659
754
  for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
660
755
  if (k in ax) fields[k] = ax[k];
661
756
  }
662
757
  importedCards.push(cardApi.createCard('axiom', fields));
663
758
  }
664
- for (const ont of (core.ontology || [])) {
759
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
665
760
  importedCards.push(cardApi.createCard('ontology', {
666
761
  one_sentence: ont.one_sentence || ont.essence || '',
667
762
  essence: ont.essence || '', boundary: ont.boundary || '',
668
763
  trigger_signal: ont.trigger_signal || '',
669
764
  }));
670
765
  }
671
- for (const b of (core.boundaries || [])) {
766
+ for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
672
767
  importedCards.push(cardApi.createCard('boundary', {
673
768
  scope: b.scope || '', out_of_scope: b.out_of_scope || '',
674
769
  acceptable_exceptions: b.acceptable_exceptions || [],
675
770
  }));
676
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
+ }
677
781
  }
678
782
  if (entries.has('KDNA_Patterns.json')) {
679
783
  const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
680
- for (const ms of (pat.misunderstandings || [])) {
784
+ for (const ms of (Array.isArray(pat.misunderstandings) ? pat.misunderstandings : [])) {
681
785
  importedCards.push(cardApi.createCard('misunderstanding', {
682
786
  wrong: ms.wrong || '', correct: ms.correct || '',
683
787
  key_distinction: ms.key_distinction || '', why: ms.why || '',
@@ -687,6 +791,39 @@ function importFromKdna(kdnaPath) {
687
791
  const q = typeof sc === 'string' ? sc : sc.question || '';
688
792
  importedCards.push(cardApi.createCard('self_check', { question: q }));
689
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
+ }
690
827
  }
691
828
  }
692
829
 
@@ -694,7 +831,30 @@ function importFromKdna(kdnaPath) {
694
831
  fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
695
832
  }
696
833
 
697
- return { lineage, cards: importedCards, source_manifest: manifest };
834
+ // Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
835
+ // KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
836
+ // them on the project. The next export then forwards them to
837
+ // compileDomain via exportRuntimeAsset, which preserves
838
+ // changelog / version_notes / reasoning_chains / banned_terms
839
+ // through the round-trip.
840
+ const sourcePatterns = entries.has('KDNA_Patterns.json')
841
+ ? JSON.parse(entries.get('KDNA_Patterns.json').toString())
842
+ : null;
843
+ const sourceReasoning = entries.has('KDNA_Reasoning.json')
844
+ ? JSON.parse(entries.get('KDNA_Reasoning.json').toString())
845
+ : null;
846
+ const sourceEvolution = entries.has('KDNA_Evolution.json')
847
+ ? JSON.parse(entries.get('KDNA_Evolution.json').toString())
848
+ : null;
849
+
850
+ return {
851
+ lineage,
852
+ cards: importedCards,
853
+ source_manifest: manifest,
854
+ source_patterns: sourcePatterns,
855
+ source_reasoning: sourceReasoning,
856
+ source_evolution: sourceEvolution,
857
+ };
698
858
  }
699
859
 
700
860
  /**
@@ -766,7 +926,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
766
926
  }
767
927
 
768
928
  if (core) {
769
- for (const ax of (core.axioms || [])) {
929
+ for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
770
930
  const fields = {};
771
931
  for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
772
932
  if (key in ax) fields[key] = ax[key];
@@ -774,14 +934,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
774
934
  }
775
935
  importedCards.push(cardApi.createCard('axiom', fields));
776
936
  }
777
- for (const ont of (core.ontology || [])) {
937
+ for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
778
938
  importedCards.push(cardApi.createCard('ontology', {
779
939
  one_sentence: ont.one_sentence || ont.essence || '',
780
940
  essence: ont.essence || '', boundary: ont.boundary || '',
781
941
  trigger_signal: ont.trigger_signal || '',
782
942
  }));
783
943
  }
784
- for (const b of (core.boundaries || [])) {
944
+ for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
785
945
  importedCards.push(cardApi.createCard('boundary', {
786
946
  scope: b.scope || '', out_of_scope: b.out_of_scope || '',
787
947
  acceptable_exceptions: b.acceptable_exceptions || [],
@@ -799,7 +959,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
799
959
  importedCards.push(cardApi.createCard('risk', r.fields || r));
800
960
  }
801
961
  // NEW: import stances
802
- for (const s of (core.stances || [])) {
962
+ for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
803
963
  const text = typeof s === 'string' ? s : s.stance || '';
804
964
  importedCards.push(cardApi.createCard('stance', {
805
965
  statement: text,
@@ -808,7 +968,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
808
968
  }));
809
969
  }
810
970
  // NEW: import frameworks
811
- for (const fw of (core.frameworks || [])) {
971
+ for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
812
972
  importedCards.push(cardApi.createCard('framework', {
813
973
  name: fw.name || '', when_to_use: fw.when_to_use || '',
814
974
  steps: fw.steps || [],
@@ -819,18 +979,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
819
979
  if (patterns) {
820
980
  // NEW: import terminology
821
981
  if (patterns.terminology) {
822
- for (const t of (patterns.terminology.standard_terms || [])) {
982
+ for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
823
983
  importedCards.push(cardApi.createCard('term', {
824
984
  term: t.term || '', definition: t.definition || '',
825
985
  }));
826
986
  }
827
- for (const bt of (patterns.terminology.banned_terms || [])) {
987
+ for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
828
988
  importedCards.push(cardApi.createCard('banned_term', {
829
989
  term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
830
990
  }));
831
991
  }
832
992
  }
833
- for (const ms of (patterns.misunderstandings || [])) {
993
+ for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
834
994
  importedCards.push(cardApi.createCard('misunderstanding', {
835
995
  wrong: ms.wrong || '', correct: ms.correct || '',
836
996
  key_distinction: ms.key_distinction || '', why: ms.why || '',
@@ -1391,7 +1551,7 @@ function cmdMigrate(args) {
1391
1551
  const requestedName = option(args, '--name');
1392
1552
  let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
1393
1553
  if (!sourceDir || !out || !by || !statement) {
1394
- fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
1554
+ fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
1395
1555
  }
1396
1556
 
1397
1557
  const sourcePath = path.resolve(sourceDir);
@@ -1416,7 +1576,7 @@ function cmdMigrate(args) {
1416
1576
  projectPath = loaded.projectPath;
1417
1577
  if (!requestedName && project.name) name = project.name;
1418
1578
  } else {
1419
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
1579
+ tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
1420
1580
  const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1421
1581
  project = imported.project;
1422
1582
  projectPath = path.join(tmpDir, 'studio.project.json');
@@ -1431,7 +1591,7 @@ function cmdMigrate(args) {
1431
1591
 
1432
1592
  // Reject if critical judgment fields are missing from axioms
1433
1593
  const criticalMissing = [];
1434
- for (const card of (project.cards || [])) {
1594
+ for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
1435
1595
  if (card.type !== 'axiom') continue;
1436
1596
  for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
1437
1597
  const val = card.fields && card.fields[field];
@@ -1564,7 +1724,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
1564
1724
  }
1565
1725
  }
1566
1726
  const lockedCards = (project.cards || []).filter(c => c.locked);
1567
- const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
1727
+ const v1Dir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-')));
1568
1728
  try {
1569
1729
  const manifestDefaults = buildV1Manifest(project, name);
1570
1730
  const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
@@ -2037,10 +2197,16 @@ function cmdSourceClassify(args) {
2037
2197
  const target = project.distillation_target;
2038
2198
  if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
2039
2199
 
2040
- // Canonical evidence field is `evidence_materials` (set by cmdImport via
2041
- // evidenceApi.addEvidence). The legacy `project.evidence` field never
2042
- // receives writes, so reading it always produced an empty list and the
2043
- // 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.
2044
2210
  const evidence = project.evidence_materials || project.evidence || [];
2045
2211
  const results = evidence.map(e => {
2046
2212
  const text = (e.content || e.title || '').toLowerCase();
@@ -2234,8 +2400,24 @@ async function cmdFeynman(args) {
2234
2400
  // Canonical field name in the card schema is `feynman_restatement`, not
2235
2401
  // `feynman_text`. The old check was dead code (nothing writes the wrong
2236
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.
2237
2416
  if (!card.feynman_restatement) {
2238
- 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
+ );
2239
2421
  }
2240
2422
 
2241
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.4",
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.3",
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 };