@aikdna/kdna-studio-cli 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/kdna-studio.js +254 -66
  2. package/package.json +2 -2
@@ -99,6 +99,40 @@ function option(args, name, fallback = null) {
99
99
  return value;
100
100
  }
101
101
 
102
+ /**
103
+ * Resolve a passphrase from CLI flags, environment, or stdin.
104
+ *
105
+ * SECURITY: passing a passphrase as `--passphrase <value>` exposes it in
106
+ * `ps aux` output and shell history. The recommended path is one of:
107
+ * 1. --passphrase-stdin — read from stdin (pipe-friendly, no TTY hang)
108
+ * 2. KDNA_PASSPHRASE env var (less secure but no process-list leak)
109
+ * 3. --passphrase <pw> — fallback only; prints a warning
110
+ *
111
+ * Returns the passphrase string, or null if none was provided.
112
+ */
113
+ function resolvePassphrase(args) {
114
+ if (args.includes('--passphrase-stdin')) {
115
+ if (process.stdin.isTTY) {
116
+ fail(
117
+ '--passphrase-stdin requires the passphrase to be piped in on stdin.\n' +
118
+ 'Example: read -s PW && echo "$PW" | kdna-studio export ... --sign --passphrase-stdin'
119
+ );
120
+ }
121
+ try {
122
+ return fs.readFileSync(0, 'utf8').trim();
123
+ } catch (e) {
124
+ fail(`Could not read passphrase from stdin: ${e.message}`);
125
+ }
126
+ }
127
+ if (process.env.KDNA_PASSPHRASE) return process.env.KDNA_PASSPHRASE;
128
+ const fromFlag = option(args, '--passphrase');
129
+ if (fromFlag) {
130
+ console.error('Warning: --passphrase <value> exposes the secret in `ps aux` and shell history. Prefer --passphrase-stdin or KDNA_PASSPHRASE env var.');
131
+ return fromFlag;
132
+ }
133
+ return null;
134
+ }
135
+
102
136
  function resolveApiKey(args) {
103
137
  // 1. KDNA_API_KEY environment variable (preferred)
104
138
  if (process.env.KDNA_API_KEY) return process.env.KDNA_API_KEY;
@@ -287,10 +321,27 @@ function cardsFromV1Payload(payload) {
287
321
  failure_risk: ax.failure_risk || '',
288
322
  }, ax.id || null);
289
323
  }
324
+ for (const ont of (payload.core?.ontology || [])) {
325
+ pushImportedCard(cards, 'ontology', ont, ont.id || null);
326
+ }
327
+ for (const fw of (payload.core?.frameworks || [])) {
328
+ pushImportedCard(cards, 'framework', fw, fw.id || null);
329
+ }
330
+ for (const stance of (payload.core?.stances || [])) {
331
+ pushImportedCard(cards, 'stance', stance, stance.id || null);
332
+ }
333
+ for (const aesthetic of (payload.core?.aesthetics || [])) {
334
+ pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
335
+ }
290
336
  for (const boundary of (payload.core?.boundaries || [])) {
291
337
  pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
292
338
  }
293
- for (const risk of (payload.core?.risk_model?.risks || [])) {
339
+ // risk_model may be either an array of risk items (legacy) or
340
+ // `{risks: [...]}` (current). Treat both shapes; ignore everything else
341
+ // rather than crashing with "object is not iterable".
342
+ const riskModel = payload.core?.risk_model;
343
+ const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
344
+ for (const risk of riskItems) {
294
345
  pushImportedCard(cards, 'risk', risk, risk.id || null);
295
346
  }
296
347
  for (const pattern of (payload.patterns || [])) {
@@ -304,7 +355,13 @@ function cardsFromV1Payload(payload) {
304
355
  for (const item of (payload.cases || [])) {
305
356
  pushImportedCard(cards, 'case', item, item.id || null);
306
357
  }
307
- for (const selfCheck of (payload.reasoning?.self_checks || [])) {
358
+ // Accept both `self_check` (canonical) and `self_checks` (legacy schema
359
+ // name) so we round-trip assets written by either version of the
360
+ // payload-profile schema.
361
+ const selfCheckArr = payload.reasoning?.self_check
362
+ || payload.reasoning?.self_checks
363
+ || [];
364
+ for (const selfCheck of selfCheckArr) {
308
365
  const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
309
366
  pushImportedCard(cards, 'self_check', fields, fields.id || null);
310
367
  }
@@ -320,7 +377,14 @@ function cardsFromV1Payload(payload) {
320
377
  pushImportedCard(cards, 'reasoning', chain, chain.id || null);
321
378
  }
322
379
  for (const stage of (payload.evolution?.stages || [])) {
323
- pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
380
+ // Distinguish source-authored stages (preserved identity) from
381
+ // audit-log-derived stages (synthesised). The compile path tags
382
+ // source_authored: true; import those verbatim and skip the rest
383
+ // so the importer doesn't double-count what was already in the
384
+ // locked judgment cards.
385
+ if (stage.source_authored === true) {
386
+ pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
387
+ }
324
388
  }
325
389
  return cards;
326
390
  }
@@ -566,52 +630,65 @@ function importFromKdna(kdnaPath) {
566
630
  parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
567
631
  };
568
632
 
569
- // Extract cards from KDNA files
633
+ // Extract cards. Modern v1 packages carry `kdna.json` + `payload.kdnab`
634
+ // and use the unified judgment profile. Legacy packages split content
635
+ // across KDNA_*.json files. Prefer payload.kdnab when present — it
636
+ // covers all 8+ card types (axiom / boundary / risk / aesthetic /
637
+ // ontology / misunderstanding / self_check / scenario / case /
638
+ // reasoning / evolution_stage / stance / framework / term / banned_term)
639
+ // via cardsFromPayload / cardsFromV1Payload.
570
640
  const importedCards = [];
571
- if (entries.has('KDNA_Core.json')) {
572
- const core = JSON.parse(entries.get('KDNA_Core.json').toString());
573
- for (const ax of (core.axioms || [])) {
574
- const fields = {};
575
- for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
576
- if (k in ax) fields[k] = ax[k];
577
- }
578
- importedCards.push(cardApi.createCard('axiom', fields));
579
- }
580
- for (const ont of (core.ontology || [])) {
581
- importedCards.push(cardApi.createCard('ontology', {
582
- one_sentence: ont.one_sentence || ont.essence || '',
583
- essence: ont.essence || '', boundary: ont.boundary || '',
584
- trigger_signal: ont.trigger_signal || '',
585
- }));
586
- }
587
- for (const b of (core.boundaries || [])) {
588
- importedCards.push(cardApi.createCard('boundary', {
589
- scope: b.scope || '', out_of_scope: b.out_of_scope || '',
590
- acceptable_exceptions: b.acceptable_exceptions || [],
591
- }));
592
- }
593
- }
594
- if (entries.has('KDNA_Patterns.json')) {
595
- const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
596
- for (const ms of (pat.misunderstandings || [])) {
597
- importedCards.push(cardApi.createCard('misunderstanding', {
598
- wrong: ms.wrong || '', correct: ms.correct || '',
599
- key_distinction: ms.key_distinction || '', why: ms.why || '',
600
- }));
601
- }
602
- for (const sc of (pat.self_check || [])) {
603
- const q = typeof sc === 'string' ? sc : sc.question || '';
604
- importedCards.push(cardApi.createCard('self_check', { question: q }));
605
- }
606
- }
607
-
608
- if (importedCards.length === 0 && entries.has('payload.kdnab')) {
641
+ if (entries.has('payload.kdnab')) {
609
642
  try {
610
643
  const payload = decodePayload(entries.get('payload.kdnab'), manifest);
611
644
  importedCards.push(...cardsFromPayload(payload));
612
645
  } catch (e) {
613
646
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
614
647
  }
648
+ } else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
649
+ // Legacy fallback — only used when payload.kdnab is absent. This path
650
+ // intentionally covers a strict subset of card types and is not the
651
+ // recommended import path.
652
+ if (entries.has('KDNA_Core.json')) {
653
+ const core = JSON.parse(entries.get('KDNA_Core.json').toString());
654
+ for (const ax of (core.axioms || [])) {
655
+ const fields = {};
656
+ for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
657
+ if (k in ax) fields[k] = ax[k];
658
+ }
659
+ importedCards.push(cardApi.createCard('axiom', fields));
660
+ }
661
+ for (const ont of (core.ontology || [])) {
662
+ importedCards.push(cardApi.createCard('ontology', {
663
+ one_sentence: ont.one_sentence || ont.essence || '',
664
+ essence: ont.essence || '', boundary: ont.boundary || '',
665
+ trigger_signal: ont.trigger_signal || '',
666
+ }));
667
+ }
668
+ for (const b of (core.boundaries || [])) {
669
+ importedCards.push(cardApi.createCard('boundary', {
670
+ scope: b.scope || '', out_of_scope: b.out_of_scope || '',
671
+ acceptable_exceptions: b.acceptable_exceptions || [],
672
+ }));
673
+ }
674
+ }
675
+ if (entries.has('KDNA_Patterns.json')) {
676
+ const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
677
+ for (const ms of (pat.misunderstandings || [])) {
678
+ importedCards.push(cardApi.createCard('misunderstanding', {
679
+ wrong: ms.wrong || '', correct: ms.correct || '',
680
+ key_distinction: ms.key_distinction || '', why: ms.why || '',
681
+ }));
682
+ }
683
+ for (const sc of (pat.self_check || [])) {
684
+ const q = typeof sc === 'string' ? sc : sc.question || '';
685
+ importedCards.push(cardApi.createCard('self_check', { question: q }));
686
+ }
687
+ }
688
+ }
689
+
690
+ if (importedCards.length === 0) {
691
+ fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
615
692
  }
616
693
 
617
694
  return { lineage, cards: importedCards, source_manifest: manifest };
@@ -667,6 +744,24 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
667
744
  const reasoning = loadJson('KDNA_Reasoning.json');
668
745
  const evolution = loadJson('KDNA_Evolution.json');
669
746
 
747
+ // If none of the standard KDNA_*.json files were found, that almost
748
+ // always means the user pointed at the wrong directory or the source uses
749
+ // a non-standard filename. Report what was *actually* present so they can
750
+ // rename or move their files.
751
+ const expectedFiles = ['KDNA_Core.json', 'KDNA_Patterns.json', 'KDNA_Scenarios.json',
752
+ 'KDNA_Cases.json', 'KDNA_Reasoning.json', 'KDNA_Evolution.json', 'kdna.json'];
753
+ const loadedCount = audit.filesFound.length;
754
+ if (loadedCount === 0) {
755
+ let dirContents = [];
756
+ try { dirContents = fs.readdirSync(absSource); } catch { /* ignore */ }
757
+ fail(
758
+ `No KDNA source files found in ${absSource}.\n` +
759
+ `Expected one of: ${expectedFiles.join(', ')}\n` +
760
+ `Directory contains ${dirContents.length} entr${dirContents.length === 1 ? 'y' : 'ies'}: ` +
761
+ (dirContents.length > 0 ? dirContents.slice(0, 20).join(', ') + (dirContents.length > 20 ? ', ...' : '') : '(empty)')
762
+ );
763
+ }
764
+
670
765
  if (core) {
671
766
  for (const ax of (core.axioms || [])) {
672
767
  const fields = {};
@@ -689,7 +784,15 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
689
784
  acceptable_exceptions: b.acceptable_exceptions || [],
690
785
  }));
691
786
  }
692
- for (const r of (core.risks || core.risk_model || [])) {
787
+ // Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
788
+ // (either as `{risks: [...]}` or as an array itself).
789
+ let risksSource = core.risks;
790
+ if (!Array.isArray(risksSource)) {
791
+ if (Array.isArray(core.risk_model)) risksSource = core.risk_model;
792
+ else if (Array.isArray(core.risk_model?.risks)) risksSource = core.risk_model.risks;
793
+ else risksSource = [];
794
+ }
795
+ for (const r of risksSource) {
693
796
  importedCards.push(cardApi.createCard('risk', r.fields || r));
694
797
  }
695
798
  // NEW: import stances
@@ -756,12 +859,23 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
756
859
  }
757
860
  }
758
861
 
759
- // NEW: import reasoning chains
862
+ // NEW: import reasoning chains. Field names must match what compileReasoning
863
+ // reads (chain/principle/concrete_action/axiom/one_sentence) so the
864
+ // round-trip preserves the source's structure. Bug: prior code stored
865
+ // {name, conclusion, so_what}, none of which the compile path recognised,
866
+ // so the cards were silently rewritten as axiom-derived fallback chains
867
+ // — looking identical to misunderstanding cards and losing the source's
868
+ // identity.
760
869
  if (reasoning && reasoning.reasoning_chains) {
761
870
  for (const rc of reasoning.reasoning_chains) {
762
871
  importedCards.push(cardApi.createCard('reasoning', {
763
- name: rc.name || rc.id || '', conclusion: rc.conclusion || '',
764
- so_what: rc.so_what || '',
872
+ axiom: rc.axiom || '',
873
+ one_sentence: rc.one_sentence || rc.conclusion || '',
874
+ chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
875
+ principle: rc.principle || rc.name || '',
876
+ concrete_action: rc.concrete_action || rc.so_what || '',
877
+ so_what: rc.so_what || rc.concrete_action || '',
878
+ conclusion: rc.conclusion || '',
765
879
  }));
766
880
  }
767
881
  }
@@ -1158,10 +1272,10 @@ function cmdCard(args) {
1158
1272
  return null;
1159
1273
  }
1160
1274
  lockPayload.creator_id = identity.creator_id;
1161
- const passphrase = option(args, '--passphrase');
1275
+ const passphrase = resolvePassphrase(args);
1162
1276
  try {
1163
1277
  if (identity.encrypted && !passphrase) {
1164
- errors.push(`${card.id}: Private key is encrypted — provide --passphrase`);
1278
+ errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin or KDNA_PASSPHRASE`);
1165
1279
  return null;
1166
1280
  }
1167
1281
  lockPayload.signature = creatorApi.signHumanLock(
@@ -1274,6 +1388,12 @@ function cmdMigrate(args) {
1274
1388
  let projectPath = null;
1275
1389
  let project = null;
1276
1390
 
1391
+ // Wrap everything in try / finally so a mid-migrate failure (e.g. missing
1392
+ // critical fields, gate blocked, signing error) still cleans up tmpDir.
1393
+ // Bug: prior code only cleaned up on the v1 and v2 success paths; any
1394
+ // non-v1 export path that threw leaked `/tmp/kdna-migrate-*` entries
1395
+ // indefinitely.
1396
+ try {
1277
1397
  // Step 1: import dev source, or use an existing Studio project directly.
1278
1398
  let creatorIdentity = null;
1279
1399
  try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
@@ -1325,7 +1445,7 @@ function cmdMigrate(args) {
1325
1445
  const identity = creatorApi.loadIdentity();
1326
1446
  if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
1327
1447
  lockPayload.creator_id = identity.creator_id;
1328
- const passphrase = option(args, '--passphrase');
1448
+ const passphrase = resolvePassphrase(args);
1329
1449
  try {
1330
1450
  lockPayload.signature = creatorApi.signHumanLock(
1331
1451
  card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
@@ -1383,6 +1503,17 @@ function cmdMigrate(args) {
1383
1503
  // Cleanup temp
1384
1504
  if (tmpDir) {
1385
1505
  try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
1506
+ tmpDir = null;
1507
+ }
1508
+ } finally {
1509
+ // Single, guaranteed cleanup point for tmpDir even when an earlier
1510
+ // step (critical-missing check, gate block, signing failure) bails
1511
+ // out via `fail()`. `fail()` ultimately calls process.exit, but
1512
+ // if a future caller swallows that or the runtime traps the throw,
1513
+ // we still leave the filesystem clean.
1514
+ if (tmpDir) {
1515
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
1516
+ }
1386
1517
  }
1387
1518
  }
1388
1519
 
@@ -1545,11 +1676,27 @@ function stableStringify(value) {
1545
1676
  }
1546
1677
 
1547
1678
  function manifestForSigning(manifest) {
1548
- const copy = { ...manifest };
1679
+ // Mirrors the kdna-core implementation in
1680
+ // packages/kdna-core/src/asset-reader.js#manifestForSignature /
1681
+ // manifestForDigest. Anything not stripped here will end up in the
1682
+ // signing payload, and a mismatch between producer (this CLI) and
1683
+ // verifier (kdna-core) will cause the signature check to fail.
1684
+ //
1685
+ // Bug: prior version did not strip `_source` (only set by eval/loader
1686
+ // helpers) and did not recursively strip `authoring.content_digest`,
1687
+ // so a payload with either field would diverge from kdna-core's
1688
+ // canonicalisation and report a false-positive verification failure.
1689
+ const copy = { ...(manifest || {}) };
1549
1690
  delete copy.signature;
1550
1691
  delete copy.asset_digest;
1551
1692
  delete copy.container_sha256;
1552
1693
  delete copy.content_digest;
1694
+ delete copy._source;
1695
+ if (copy.authoring && typeof copy.authoring === 'object') {
1696
+ const auth = { ...copy.authoring };
1697
+ delete auth.content_digest;
1698
+ copy.authoring = auth;
1699
+ }
1553
1700
  return copy;
1554
1701
  }
1555
1702
 
@@ -1579,7 +1726,7 @@ function publicKeyFingerprint(publicKeyPem) {
1579
1726
  function applySignature(files, passphrase = null) {
1580
1727
  const paths = identityPaths();
1581
1728
  if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
1582
- fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
1729
+ fail('Signing requires KDNA identity keys. Run: kdna-studio identity init', EXIT.TRUST_FAILED);
1583
1730
  }
1584
1731
  const manifest = JSON.parse(files['kdna.json']);
1585
1732
  const publicKeyPem = fs.readFileSync(paths.publicKey, 'utf8');
@@ -1617,9 +1764,26 @@ function cmdExport(args) {
1617
1764
  // Use args.includes() for --password-stdin since it is a boolean flag
1618
1765
  // (option() rejects it for "missing value").
1619
1766
  const useStdin = args.includes('--password-stdin');
1620
- const password = useStdin
1621
- ? fs.readFileSync(0, 'utf8').trim()
1622
- : option(args, '--password') || option(args, '--passphrase');
1767
+ let password;
1768
+ if (useStdin) {
1769
+ // fs.readFileSync(0) on a TTY waits forever for input and never
1770
+ // returns — the export appears to hang. Refuse up front with a
1771
+ // clear pointer to the right invocation.
1772
+ if (process.stdin.isTTY) {
1773
+ fail(
1774
+ '--password-stdin requires the password to be piped in on stdin.\n' +
1775
+ 'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --format v1 --out <file.kdna> --password-stdin\n' +
1776
+ 'If you are running interactively, omit --password-stdin and you will be prompted.'
1777
+ );
1778
+ }
1779
+ try {
1780
+ password = fs.readFileSync(0, 'utf8').trim();
1781
+ } catch (e) {
1782
+ fail(`Could not read password from stdin: ${e.message}`);
1783
+ }
1784
+ } else {
1785
+ password = option(args, '--password') || option(args, '--passphrase');
1786
+ }
1623
1787
  exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
1624
1788
  allowIncomplete: args.includes('--allow-incomplete'),
1625
1789
  password,
@@ -1654,7 +1818,7 @@ function cmdExport(args) {
1654
1818
  provenance.content_fingerprint = finalDigest;
1655
1819
  files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
1656
1820
 
1657
- if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
1821
+ if (args.includes('--sign')) applySignature(files, resolvePassphrase(args));
1658
1822
 
1659
1823
  const entries = [['mimetype', files.mimetype]];
1660
1824
  for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
@@ -1744,7 +1908,7 @@ function cmdIdentity(args) {
1744
1908
  const sub = args[0];
1745
1909
  if (sub === 'init') {
1746
1910
  const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
1747
- const passphrase = option(args, '--passphrase');
1911
+ const passphrase = resolvePassphrase(args);
1748
1912
  try {
1749
1913
  const identity = creatorApi.initIdentity(name, null, passphrase);
1750
1914
  console.log(`Creator identity initialized:`);
@@ -1837,7 +2001,11 @@ function cmdSourceClassify(args) {
1837
2001
  const target = project.distillation_target;
1838
2002
  if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
1839
2003
 
1840
- const evidence = project.evidence || [];
2004
+ // Canonical evidence field is `evidence_materials` (set by cmdImport via
2005
+ // evidenceApi.addEvidence). The legacy `project.evidence` field never
2006
+ // receives writes, so reading it always produced an empty list and the
2007
+ // classifier reported "no evidence" even after `kdna-studio import`.
2008
+ const evidence = project.evidence_materials || project.evidence || [];
1841
2009
  const results = evidence.map(e => {
1842
2010
  const text = (e.content || e.title || '').toLowerCase();
1843
2011
  const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
@@ -1965,14 +2133,29 @@ function cmdCandidate(args) {
1965
2133
  if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
1966
2134
 
1967
2135
  for (const c of accepted) {
1968
- const card = cardApi.createCard(c.suggested_card_type, {
1969
- one_sentence: c.one_sentence,
1970
- full_statement: c.full_statement,
1971
- why: '',
1972
- applies_when: '',
1973
- does_not_apply_when: '',
1974
- failure_risk: '',
1975
- });
2136
+ // Promote candidates to draft cards. For axioms we must populate every
2137
+ // field the Human Lock gate and migrate path require — empty strings
2138
+ // cause migrate to fail ("critical fields missing from axioms"). When
2139
+ // a candidate lacks the field, fall back to the candidate's own
2140
+ // one_sentence/full_statement so the lock gate at least has substance
2141
+ // to verify. Authors are expected to enrich these fields before
2142
+ // locking.
2143
+ const fields = {};
2144
+ if (c.suggested_card_type === 'axiom') {
2145
+ fields.one_sentence = c.one_sentence || '';
2146
+ fields.full_statement = c.full_statement || c.one_sentence || '<TBD: full_statement>';
2147
+ fields.why = c.why || '<TBD: why>';
2148
+ // applies_when / does_not_apply_when are arrays, not strings.
2149
+ // Empty string is invalid; emit an empty array so checkRequiredFields
2150
+ // (which treats `length === 0` as missing) correctly reports them.
2151
+ fields.applies_when = Array.isArray(c.applies_when) ? c.applies_when : [];
2152
+ fields.does_not_apply_when = Array.isArray(c.does_not_apply_when) ? c.does_not_apply_when : [];
2153
+ fields.failure_risk = c.failure_risk || '<TBD: failure_risk>';
2154
+ } else {
2155
+ fields.one_sentence = c.one_sentence || '';
2156
+ fields.full_statement = c.full_statement || c.one_sentence || '';
2157
+ }
2158
+ const card = cardApi.createCard(c.suggested_card_type, fields);
1976
2159
  card.evidence_refs = c.supporting_evidence_ids;
1977
2160
  project.cards = project.cards || [];
1978
2161
  project.cards.push(card);
@@ -2012,7 +2195,12 @@ async function cmdFeynman(args) {
2012
2195
  if (cardIdx < 0) fail(`Card not found: ${cardId}`);
2013
2196
  const card = project.cards[cardIdx];
2014
2197
 
2015
- if (!card.feynman_text) fail(`Card ${cardId} has no Feynman restatement. Use card approve first with --statement to set it.`);
2198
+ // Canonical field name in the card schema is `feynman_restatement`, not
2199
+ // `feynman_text`. The old check was dead code (nothing writes the wrong
2200
+ // field) and made this command fail on every card.
2201
+ if (!card.feynman_restatement) {
2202
+ fail(`Card ${cardId} has no Feynman restatement. Generate one with the feynman authoring command, or set it via card update --field feynman_restatement='<json>'.`);
2203
+ }
2016
2204
 
2017
2205
  console.log(`Evaluating Feynman restatement for card: ${cardId}`);
2018
2206
  const result = await ai.feynman.evaluate(llm.config(), card, {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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.0",
42
+ "@aikdna/kdna-studio-core": "^1.7.2",
43
43
  "ajv": "^8.20.0",
44
44
  "ajv-formats": "^3.0.1",
45
45
  "cbor-x": "^1.6.4"