@aikdna/kdna-studio-cli 0.8.9 → 0.8.11

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.
@@ -258,23 +258,41 @@ function writeFiles(outDir, files) {
258
258
  }
259
259
  }
260
260
 
261
+ const ARRAY_FIELD_NAMES = new Set([
262
+ 'applies_when',
263
+ 'does_not_apply_when',
264
+ 'acceptable_exceptions',
265
+ ]);
266
+
261
267
  function parseFields(args) {
262
268
  const fields = {};
263
269
  for (const pair of optionsAll(args, '--field')) {
264
270
  const eq = pair.indexOf('=');
265
271
  if (eq < 1) fail(`Invalid --field "${pair}". Use key=value.`);
266
- const key = pair.slice(0, eq);
272
+ let key = pair.slice(0, eq);
273
+ const appendArray = key.endsWith('[]');
274
+ if (appendArray) key = key.slice(0, -2);
267
275
  const raw = pair.slice(eq + 1);
276
+ let value;
268
277
  if (raw.startsWith('[') || raw.startsWith('{')) {
269
278
  try {
270
- fields[key] = JSON.parse(raw);
279
+ value = JSON.parse(raw);
271
280
  } catch (_) {
272
- fields[key] = raw;
281
+ value = raw;
273
282
  }
274
283
  } else if (raw.includes('|')) {
275
- fields[key] = raw.split('|').map((s) => s.trim()).filter(Boolean);
284
+ value = raw.split('|').map((s) => s.trim()).filter(Boolean);
276
285
  } else {
277
- fields[key] = raw;
286
+ value = raw;
287
+ }
288
+
289
+ if (appendArray) {
290
+ const values = Array.isArray(value) ? value : [value];
291
+ fields[key] = [...(Array.isArray(fields[key]) ? fields[key] : []), ...values];
292
+ } else if (ARRAY_FIELD_NAMES.has(key) && typeof value === 'string') {
293
+ fields[key] = value.trim() ? [value.trim()] : [];
294
+ } else {
295
+ fields[key] = value;
278
296
  }
279
297
  }
280
298
  return fields;
@@ -413,6 +431,8 @@ function cardsFromV1Payload(payload) {
413
431
  applies_when: ax.applies_when || [],
414
432
  does_not_apply_when: ax.does_not_apply_when || [],
415
433
  failure_risk: ax.failure_risk || '',
434
+ confidence: ax.confidence || '',
435
+ evidence_type: ax.evidence_type || '',
416
436
  }, ax.id || null);
417
437
  }
418
438
  for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
@@ -451,7 +471,8 @@ function cardsFromV1Payload(payload) {
451
471
  let fields = pattern;
452
472
  if (!type) {
453
473
  // Heuristics, in priority order — most specific to most general.
454
- if ('wrong' in pattern && 'correct' in pattern) type = 'misunderstanding';
474
+ if ('logic' in pattern && ('so_what' in pattern || 'conclusion' in pattern)) type = 'reasoning';
475
+ else if ('wrong' in pattern && 'correct' in pattern) type = 'misunderstanding';
455
476
  else if ('term' in pattern && 'definition' in pattern) type = 'term';
456
477
  else if ('term' in pattern && 'why' in pattern && 'replace_with' in pattern) type = 'banned_term';
457
478
  else if ('what_it_looks_like' in pattern && 'how_to_fix' in pattern) type = 'pattern';
@@ -833,7 +854,7 @@ function importFromKdna(kdnaPath) {
833
854
  const core = JSON.parse(entries.get('KDNA_Core.json').toString());
834
855
  for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
835
856
  const fields = {};
836
- for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
857
+ for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk','confidence','evidence_type']) {
837
858
  if (k in ax) fields[k] = ax[k];
838
859
  }
839
860
  importedCards.push(cardApi.createCard('axiom', fields));
@@ -851,7 +872,16 @@ function importFromKdna(kdnaPath) {
851
872
  acceptable_exceptions: b.acceptable_exceptions || [],
852
873
  }));
853
874
  }
854
- for (const risk of (core.risks || core.risk_model || [])) {
875
+ // Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
876
+ // (either as `{risks: [...]}` or as an array itself). Uses the same
877
+ // Array.isArray ladder pattern as the safe path at lines 1033-1040.
878
+ let risksSource = core.risks;
879
+ if (!Array.isArray(risksSource)) {
880
+ if (Array.isArray(core.risk_model)) risksSource = core.risk_model;
881
+ else if (Array.isArray(core.risk_model?.risks)) risksSource = core.risk_model.risks;
882
+ else risksSource = [];
883
+ }
884
+ for (const risk of risksSource) {
855
885
  importedCards.push(cardApi.createCard('risk', risk.fields || risk));
856
886
  }
857
887
  for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
@@ -944,7 +974,7 @@ function importFromKdna(kdnaPath) {
944
974
  * Reads KDNA_*.json files and converts entries to draft judgment cards.
945
975
  * Outputs a schema audit report.
946
976
  */
947
- function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
977
+ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, opts = {}) {
948
978
  resetImportedIds();
949
979
  const absSource = path.resolve(sourceDir);
950
980
  if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
@@ -1011,7 +1041,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1011
1041
  if (core) {
1012
1042
  for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
1013
1043
  const fields = {};
1014
- for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
1044
+ for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type']) {
1015
1045
  if (key in ax) fields[key] = ax[key];
1016
1046
  else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
1017
1047
  }
@@ -1043,7 +1073,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1043
1073
  }
1044
1074
  // NEW: import stances
1045
1075
  for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
1046
- const text = typeof s === 'string' ? s : s.stance || '';
1076
+ const text = typeof s === 'string' ? s : s.statement || s.stance || '';
1047
1077
  importedCards.push(cardApi.createCard('stance', {
1048
1078
  statement: text,
1049
1079
  applies_when: s.applies_when || [],
@@ -1095,7 +1125,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1095
1125
  }
1096
1126
 
1097
1127
  // NEW: import scenarios
1098
- if (scenarios && scenarios.scenes) {
1128
+ if (scenarios && Array.isArray(scenarios.scenes)) {
1099
1129
  for (const scene of scenarios.scenes) {
1100
1130
  importedCards.push(cardApi.createCard('scenario', {
1101
1131
  name: scene.name || scene.id || '',
@@ -1105,7 +1135,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1105
1135
  }
1106
1136
 
1107
1137
  // NEW: import cases
1108
- if (cases && cases.cases) {
1138
+ if (cases && Array.isArray(cases.cases)) {
1109
1139
  for (const c of cases.cases) {
1110
1140
  importedCards.push(cardApi.createCard('case', {
1111
1141
  title: c.title || c.id || '',
@@ -1121,7 +1151,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1121
1151
  // so the cards were silently rewritten as axiom-derived fallback chains
1122
1152
  // — looking identical to misunderstanding cards and losing the source's
1123
1153
  // identity.
1124
- if (reasoning && reasoning.reasoning_chains) {
1154
+ if (reasoning && Array.isArray(reasoning.reasoning_chains)) {
1125
1155
  for (const rc of reasoning.reasoning_chains) {
1126
1156
  importedCards.push(cardApi.createCard('reasoning', {
1127
1157
  axiom: rc.axiom || '',
@@ -1136,12 +1166,13 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1136
1166
  }
1137
1167
 
1138
1168
  // NEW: import evolution stages
1139
- if (evolution && evolution.stages) {
1169
+ if (evolution && Array.isArray(evolution.stages)) {
1140
1170
  for (const stage of evolution.stages) {
1141
- importedCards.push(cardApi.createCard('evolution_stage', {
1171
+ pushImportedCard(importedCards, 'evolution_stage', {
1142
1172
  name: stage.name || stage.id || '',
1143
- level: stage.level || '', description: stage.description || '',
1144
- }));
1173
+ level: stage.level != null ? stage.level : '',
1174
+ description: stage.description || '',
1175
+ }, stage.id || null);
1145
1176
  }
1146
1177
  }
1147
1178
 
@@ -1180,7 +1211,9 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1180
1211
 
1181
1212
  fs.mkdirSync(projectDir, { recursive: true });
1182
1213
  writeProject(path.join(projectDir, 'studio.project.json'), project);
1183
- console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
1214
+ if (!opts.silent) {
1215
+ console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
1216
+ }
1184
1217
  return { project, projectPath: path.join(projectDir, 'studio.project.json'), audit };
1185
1218
  }
1186
1219
 
@@ -1395,18 +1428,74 @@ const CARD_REQUIRED_FIELDS = {
1395
1428
  pattern: ['type', 'name', 'one_sentence', 'what_it_looks_like', 'how_to_fix', 'failure_risk'],
1396
1429
  };
1397
1430
 
1431
+ const CARD_FIELD_RULES = {
1432
+ axiom: {
1433
+ applies_when: { type: 'array' },
1434
+ does_not_apply_when: { type: 'array' },
1435
+ confidence: { allowed: ['low', 'medium', 'high'] },
1436
+ evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
1437
+ },
1438
+ boundary: {
1439
+ acceptable_exceptions: { type: 'array', optional: true },
1440
+ },
1441
+ stance: {
1442
+ applies_when: { type: 'array', optional: true },
1443
+ },
1444
+ misunderstanding: {
1445
+ applies_when: { type: 'array', optional: true },
1446
+ does_not_apply_when: { type: 'array', optional: true },
1447
+ },
1448
+ };
1449
+
1450
+ function fieldIssue(fields, name, rule = {}) {
1451
+ const val = fields[name];
1452
+ if (val === undefined || val === null || val === '' ||
1453
+ (Array.isArray(val) && val.length === 0)) {
1454
+ return rule.optional ? null : { field: name, issue: 'missing' };
1455
+ }
1456
+ if (rule.type === 'array' && !Array.isArray(val)) {
1457
+ return { field: name, issue: 'wrong_type', have_type: typeof val, need_type: 'array' };
1458
+ }
1459
+ if (rule.allowed && !rule.allowed.includes(val)) {
1460
+ return { field: name, issue: 'invalid_value', have: val, allowed: rule.allowed };
1461
+ }
1462
+ return null;
1463
+ }
1464
+
1465
+ function formatFieldIssue(issue) {
1466
+ if (issue.issue === 'missing') return issue.field;
1467
+ if (issue.issue === 'wrong_type') return `${issue.field} (must be ${issue.need_type})`;
1468
+ if (issue.issue === 'invalid_value') return `${issue.field} (must be one of: ${issue.allowed.join('|')})`;
1469
+ return `${issue.field} (${issue.issue})`;
1470
+ }
1471
+
1398
1472
  function checkRequiredFields(type, fields) {
1399
1473
  const required = CARD_REQUIRED_FIELDS[type];
1400
1474
  if (!required) return [];
1401
- const missing = [];
1475
+ const issues = [];
1402
1476
  for (const f of required) {
1403
- const val = fields[f];
1404
- if (val === undefined || val === null || val === '' ||
1405
- (Array.isArray(val) && val.length === 0)) {
1406
- missing.push(f);
1477
+ const issue = fieldIssue(fields, f, CARD_FIELD_RULES[type]?.[f] || {});
1478
+ if (issue) issues.push(formatFieldIssue(issue));
1479
+ }
1480
+ for (const [name, rule] of Object.entries(CARD_FIELD_RULES[type] || {})) {
1481
+ if (required.includes(name)) continue;
1482
+ const issue = fieldIssue(fields, name, rule);
1483
+ if (issue) issues.push(formatFieldIssue(issue));
1484
+ }
1485
+ return issues;
1486
+ }
1487
+
1488
+ function criticalAxiomIssues(project) {
1489
+ const issues = [];
1490
+ for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
1491
+ if (card.type !== 'axiom') continue;
1492
+ const fields = card.fields || {};
1493
+ for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
1494
+ const issue = fieldIssue(fields, field, CARD_FIELD_RULES.axiom[field] || {});
1495
+ if (issue) issues.push(`${card.id}: ${formatFieldIssue(issue)}`);
1407
1496
  }
1408
1497
  }
1409
- return missing;
1498
+ return issues;
1410
1499
  }
1411
1500
 
1412
1501
  function cardStrictTemplate(type, fields) {
@@ -1490,7 +1579,11 @@ function cmdCard(args) {
1490
1579
  project.stages.judgment_cards.status = 'in_progress';
1491
1580
  }
1492
1581
  writeProject(projectPath, project);
1493
- console.log(`Added card: ${card.id}`);
1582
+ if (args.includes('--json')) {
1583
+ console.log(JSON.stringify({ added: true, card }, null, 2));
1584
+ } else {
1585
+ console.log(`Added card: ${card.id}`);
1586
+ }
1494
1587
  return;
1495
1588
  }
1496
1589
  if (sub === 'update') {
@@ -1733,7 +1826,7 @@ function cmdMigrate(args) {
1733
1826
  if (!requestedName && project.name) name = project.name;
1734
1827
  } else {
1735
1828
  tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
1736
- const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1829
+ const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity, { silent: true });
1737
1830
  project = imported.project;
1738
1831
  projectPath = path.join(tmpDir, 'studio.project.json');
1739
1832
  }
@@ -1745,17 +1838,8 @@ function cmdMigrate(args) {
1745
1838
  // always failing.
1746
1839
  const allowIncompleteEarly = args.includes('--allow-incomplete');
1747
1840
 
1748
- // Reject if critical judgment fields are missing from axioms
1749
- const criticalMissing = [];
1750
- for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
1751
- if (card.type !== 'axiom') continue;
1752
- for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
1753
- const val = card.fields && card.fields[field];
1754
- if (!val || (Array.isArray(val) && val.length === 0) || val === '') {
1755
- criticalMissing.push(`${card.id}: missing ${field}`);
1756
- }
1757
- }
1758
- }
1841
+ // Reject if critical judgment fields are missing or malformed on axioms.
1842
+ const criticalMissing = criticalAxiomIssues(project);
1759
1843
  if (criticalMissing.length > 0) {
1760
1844
  if (allowIncompleteEarly) {
1761
1845
  console.warn(
@@ -1796,9 +1880,8 @@ function cmdMigrate(args) {
1796
1880
  if (!req) continue;
1797
1881
  const missing = [];
1798
1882
  for (const f of req) {
1799
- const v = card.fields?.[f];
1800
- if (v === undefined || v === null || v === '' ||
1801
- (Array.isArray(v) && v.length === 0)) missing.push(f);
1883
+ const issue = fieldIssue(card.fields || {}, f, CARD_FIELD_RULES[card.type]?.[f] || {});
1884
+ if (issue) missing.push(formatFieldIssue(issue));
1802
1885
  }
1803
1886
  if (missing.length > 0) {
1804
1887
  if (!summary.by_type[card.type]) summary.by_type[card.type] = [];
@@ -1905,6 +1988,25 @@ function exportProjectV1(project, name, outPath, opts = {}) {
1905
1988
  if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
1906
1989
  fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for v1 export.', 2);
1907
1990
  }
1991
+ const criticalIssues = criticalAxiomIssues(project);
1992
+ if (criticalIssues.length > 0) {
1993
+ if (opts.allowIncomplete) {
1994
+ console.warn(
1995
+ `--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
1996
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
1997
+ );
1998
+ } else {
1999
+ fail(
2000
+ `Cannot export v1: ${criticalIssues.length} critical axiom field issue(s).\n` +
2001
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
2002
+ ` decide when to load this domain). Fix them first, or pass\n` +
2003
+ ` --allow-incomplete to bypass.\n` +
2004
+ ` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
2005
+ (criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
2006
+ EXIT.HUMAN_LOCK_REQUIRED,
2007
+ );
2008
+ }
2009
+ }
1908
2010
  const gate = projectApi.checkHumanLockGate(project);
1909
2011
  if (gate.blocked) {
1910
2012
  if (opts.allowIncomplete) {
@@ -2082,7 +2184,7 @@ function manifestForSigning(manifest) {
2082
2184
 
2083
2185
  function canonicalPayload(files) {
2084
2186
  return ['mimetype', ...Object.keys(files).filter((k) => k !== 'mimetype').sort()]
2085
- .filter((name) => name !== 'signature.json' && name !== '.DS_Store')
2187
+ .filter((name) => name !== 'signature.json' && name !== '.DS_Store' && name !== 'build-receipt.json' && !name.startsWith('reports/'))
2086
2188
  .map((name) => {
2087
2189
  let content = name === 'mimetype' ? 'application/vnd.aikdna.kdna+zip' : files[name];
2088
2190
  if (name.endsWith('.json')) {
@@ -2179,6 +2281,25 @@ function cmdExport(args) {
2179
2281
  return;
2180
2282
  }
2181
2283
  const { project, result } = compileProject(projectInput);
2284
+ const criticalIssues = criticalAxiomIssues(project);
2285
+ if (criticalIssues.length > 0) {
2286
+ if (args.includes('--allow-incomplete')) {
2287
+ console.warn(
2288
+ `--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
2289
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
2290
+ );
2291
+ } else {
2292
+ fail(
2293
+ `Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
2294
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
2295
+ ` decide when to load this domain). Fix them first, or pass\n` +
2296
+ ` --allow-incomplete to bypass.\n` +
2297
+ ` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
2298
+ (criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
2299
+ EXIT.HUMAN_LOCK_REQUIRED,
2300
+ );
2301
+ }
2302
+ }
2182
2303
  const files = { ...result.files };
2183
2304
  files['README.md'] = compileApi.generateReadme(project);
2184
2305
  files.LICENSE = project.license?.type || 'UNSPECIFIED';
@@ -2253,15 +2374,16 @@ function cmdStudioInstall(args) {
2253
2374
  function cmdStudioUpdate(args) {
2254
2375
  const target = args[0];
2255
2376
  if (!target) fail('Usage: kdna-studio update <@scope/name>');
2256
- try {
2257
- const result = require('child_process').spawnSync('kdna', ['update', target], {
2258
- stdio: 'inherit', encoding: 'utf8',
2259
- });
2260
- if (result.status !== 0) process.exit(result.status);
2261
- } catch (e) {
2262
- console.error('Error spawning kdna CLI:', e.message);
2263
- fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
2264
- }
2377
+ // kdna update does not exist in kdna-cli v0.28.x. The correct
2378
+ // path is to re-install via npm or use kdna install to pull
2379
+ // the latest published version of the asset.
2380
+ console.log(`To update ${target}, run:`);
2381
+ console.log(` npm install -g @aikdna/kdna-cli@latest`);
2382
+ console.log(` kdna install ${target}`);
2383
+ console.log(``);
2384
+ console.log(`Or for a project-local install:`);
2385
+ console.log(` cd <project-dir> && npm install @aikdna/kdna-cli@latest`);
2386
+ process.exit(0);
2265
2387
  }
2266
2388
 
2267
2389
  function cmdLlm(args) {
@@ -2379,9 +2501,11 @@ function cmdAuditLocks(args) {
2379
2501
  one_sentence: { min: 5, type: 'string' },
2380
2502
  full_statement: { min: 20, type: 'string' },
2381
2503
  why: { min: 20, type: 'string' },
2504
+ applies_when: { type: 'array', minLength: 1 },
2505
+ does_not_apply_when: { type: 'array', minLength: 1 },
2382
2506
  failure_risk: { min: 1, type: 'string' },
2383
2507
  confidence: { allowed: ['low', 'medium', 'high'] },
2384
- evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle'] },
2508
+ evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
2385
2509
  },
2386
2510
  risk: {
2387
2511
  name: { min: 1, type: 'string' },
@@ -2408,6 +2532,9 @@ function cmdAuditLocks(args) {
2408
2532
  if (rule.type === 'array' && !Array.isArray(value)) {
2409
2533
  return { field: name, issue: 'wrong_type', have_type: typeof value, need_type: 'array' };
2410
2534
  }
2535
+ if (rule.type === 'array' && rule.minLength && value.length < rule.minLength) {
2536
+ return { field: name, issue: 'too_short', have_length: value.length, need_min: rule.minLength };
2537
+ }
2411
2538
  return null;
2412
2539
  }
2413
2540
 
@@ -2719,6 +2846,7 @@ async function cmdFeynman(args) {
2719
2846
  const projectInput = args[0];
2720
2847
  const cardId = args[1];
2721
2848
  if (!projectInput || !cardId) fail('Usage: kdna-studio feynman <project> <card-id>');
2849
+ const useJson = args.includes('--json');
2722
2850
  const { projectPath, project } = readProject(projectInput);
2723
2851
  const cardIdx = (project.cards || []).findIndex(c => c.id === cardId);
2724
2852
  if (cardIdx < 0) fail(`Card not found: ${cardId}`);
@@ -2754,7 +2882,7 @@ async function cmdFeynman(args) {
2754
2882
  console.warn(`No feynman_restatement on ${cardId}; using synthesised:\n "${synthesised}"`);
2755
2883
  }
2756
2884
 
2757
- console.log(`Evaluating Feynman restatement for card: ${cardId}`);
2885
+ if (!useJson) console.log(`Evaluating Feynman restatement for card: ${cardId}`);
2758
2886
 
2759
2887
  // Bug (#1 UX follow-up): if no LLM is configured and the caller
2760
2888
  // did not pass --no-llm, route through cmdNeedsLlm which either
@@ -2783,14 +2911,26 @@ async function cmdFeynman(args) {
2783
2911
  const score = result.score || 0;
2784
2912
  const passed = score >= 4;
2785
2913
 
2786
- console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
2787
- for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
2788
- const r = (result.criteria || {})[criterion] ? '✓' : '✗';
2789
- console.log(` ${r} ${criterion}`);
2790
- }
2791
- if (result.suggestions && result.suggestions.length > 0) {
2792
- console.log('\nSuggestions:');
2793
- result.suggestions.forEach(s => console.log(` - ${s}`));
2914
+ if (useJson) {
2915
+ console.log(JSON.stringify({
2916
+ card_id: cardId,
2917
+ score,
2918
+ passed,
2919
+ no_llm: !!llmGate.noLlm,
2920
+ criteria: result.criteria || {},
2921
+ suggestions: result.suggestions || [],
2922
+ feynman_restatement: card.feynman_restatement,
2923
+ }, null, 2));
2924
+ } else {
2925
+ console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
2926
+ for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
2927
+ const r = (result.criteria || {})[criterion] ? '✓' : '✗';
2928
+ console.log(` ${r} ${criterion}`);
2929
+ }
2930
+ if (result.suggestions && result.suggestions.length > 0) {
2931
+ console.log('\nSuggestions:');
2932
+ result.suggestions.forEach(s => console.log(` - ${s}`));
2933
+ }
2794
2934
  }
2795
2935
 
2796
2936
  card.feynman_evaluation = { score, criteria: result.criteria, suggestions: result.suggestions, evaluated_at: new Date().toISOString() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
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,12 +39,12 @@
39
39
  "node": ">=18"
40
40
  },
41
41
  "dependencies": {
42
- "@aikdna/kdna-studio-core": "^1.7.6",
42
+ "@aikdna/kdna-studio-core": "^1.7.7",
43
43
  "ajv": "^8.20.0",
44
44
  "ajv-formats": "^3.0.1",
45
45
  "cbor-x": "^1.6.4"
46
46
  },
47
47
  "devDependencies": {
48
- "@aikdna/kdna-core": "^0.15.5"
48
+ "@aikdna/kdna-core": "^0.15.9"
49
49
  }
50
50
  }
package/src/ai/feynman.js CHANGED
@@ -43,7 +43,9 @@ async function evaluate(config, card, options = {}) {
43
43
  const axiomText = fields.one_sentence || fields.full_statement || fields.question || '';
44
44
  const restatement = card.feynman_restatement
45
45
  || fields.feynman_restatement
46
- || fields.feynman_text
46
+ // NOTE: fields.feynman_text was previously in this fallback chain
47
+ // but is intentionally excluded — no code path in @aikdna writes
48
+ // feynman_text. The canonical field is feynman_restatement.
47
49
  || '';
48
50
 
49
51
  if (!restatement) {