@aikdna/kdna-studio-cli 0.8.9 → 0.8.10

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,7 +1166,7 @@ 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
1171
  importedCards.push(cardApi.createCard('evolution_stage', {
1142
1172
  name: stage.name || stage.id || '',
@@ -1180,7 +1210,9 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
1180
1210
 
1181
1211
  fs.mkdirSync(projectDir, { recursive: true });
1182
1212
  writeProject(path.join(projectDir, 'studio.project.json'), project);
1183
- console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
1213
+ if (!opts.silent) {
1214
+ console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
1215
+ }
1184
1216
  return { project, projectPath: path.join(projectDir, 'studio.project.json'), audit };
1185
1217
  }
1186
1218
 
@@ -1395,18 +1427,74 @@ const CARD_REQUIRED_FIELDS = {
1395
1427
  pattern: ['type', 'name', 'one_sentence', 'what_it_looks_like', 'how_to_fix', 'failure_risk'],
1396
1428
  };
1397
1429
 
1430
+ const CARD_FIELD_RULES = {
1431
+ axiom: {
1432
+ applies_when: { type: 'array' },
1433
+ does_not_apply_when: { type: 'array' },
1434
+ confidence: { allowed: ['low', 'medium', 'high'] },
1435
+ evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
1436
+ },
1437
+ boundary: {
1438
+ acceptable_exceptions: { type: 'array', optional: true },
1439
+ },
1440
+ stance: {
1441
+ applies_when: { type: 'array', optional: true },
1442
+ },
1443
+ misunderstanding: {
1444
+ applies_when: { type: 'array', optional: true },
1445
+ does_not_apply_when: { type: 'array', optional: true },
1446
+ },
1447
+ };
1448
+
1449
+ function fieldIssue(fields, name, rule = {}) {
1450
+ const val = fields[name];
1451
+ if (val === undefined || val === null || val === '' ||
1452
+ (Array.isArray(val) && val.length === 0)) {
1453
+ return rule.optional ? null : { field: name, issue: 'missing' };
1454
+ }
1455
+ if (rule.type === 'array' && !Array.isArray(val)) {
1456
+ return { field: name, issue: 'wrong_type', have_type: typeof val, need_type: 'array' };
1457
+ }
1458
+ if (rule.allowed && !rule.allowed.includes(val)) {
1459
+ return { field: name, issue: 'invalid_value', have: val, allowed: rule.allowed };
1460
+ }
1461
+ return null;
1462
+ }
1463
+
1464
+ function formatFieldIssue(issue) {
1465
+ if (issue.issue === 'missing') return issue.field;
1466
+ if (issue.issue === 'wrong_type') return `${issue.field} (must be ${issue.need_type})`;
1467
+ if (issue.issue === 'invalid_value') return `${issue.field} (must be one of: ${issue.allowed.join('|')})`;
1468
+ return `${issue.field} (${issue.issue})`;
1469
+ }
1470
+
1398
1471
  function checkRequiredFields(type, fields) {
1399
1472
  const required = CARD_REQUIRED_FIELDS[type];
1400
1473
  if (!required) return [];
1401
- const missing = [];
1474
+ const issues = [];
1402
1475
  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);
1476
+ const issue = fieldIssue(fields, f, CARD_FIELD_RULES[type]?.[f] || {});
1477
+ if (issue) issues.push(formatFieldIssue(issue));
1478
+ }
1479
+ for (const [name, rule] of Object.entries(CARD_FIELD_RULES[type] || {})) {
1480
+ if (required.includes(name)) continue;
1481
+ const issue = fieldIssue(fields, name, rule);
1482
+ if (issue) issues.push(formatFieldIssue(issue));
1483
+ }
1484
+ return issues;
1485
+ }
1486
+
1487
+ function criticalAxiomIssues(project) {
1488
+ const issues = [];
1489
+ for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
1490
+ if (card.type !== 'axiom') continue;
1491
+ const fields = card.fields || {};
1492
+ for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
1493
+ const issue = fieldIssue(fields, field, CARD_FIELD_RULES.axiom[field] || {});
1494
+ if (issue) issues.push(`${card.id}: ${formatFieldIssue(issue)}`);
1407
1495
  }
1408
1496
  }
1409
- return missing;
1497
+ return issues;
1410
1498
  }
1411
1499
 
1412
1500
  function cardStrictTemplate(type, fields) {
@@ -1490,7 +1578,11 @@ function cmdCard(args) {
1490
1578
  project.stages.judgment_cards.status = 'in_progress';
1491
1579
  }
1492
1580
  writeProject(projectPath, project);
1493
- console.log(`Added card: ${card.id}`);
1581
+ if (args.includes('--json')) {
1582
+ console.log(JSON.stringify({ added: true, card }, null, 2));
1583
+ } else {
1584
+ console.log(`Added card: ${card.id}`);
1585
+ }
1494
1586
  return;
1495
1587
  }
1496
1588
  if (sub === 'update') {
@@ -1733,7 +1825,7 @@ function cmdMigrate(args) {
1733
1825
  if (!requestedName && project.name) name = project.name;
1734
1826
  } else {
1735
1827
  tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
1736
- const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1828
+ const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity, { silent: true });
1737
1829
  project = imported.project;
1738
1830
  projectPath = path.join(tmpDir, 'studio.project.json');
1739
1831
  }
@@ -1745,17 +1837,8 @@ function cmdMigrate(args) {
1745
1837
  // always failing.
1746
1838
  const allowIncompleteEarly = args.includes('--allow-incomplete');
1747
1839
 
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
- }
1840
+ // Reject if critical judgment fields are missing or malformed on axioms.
1841
+ const criticalMissing = criticalAxiomIssues(project);
1759
1842
  if (criticalMissing.length > 0) {
1760
1843
  if (allowIncompleteEarly) {
1761
1844
  console.warn(
@@ -1796,9 +1879,8 @@ function cmdMigrate(args) {
1796
1879
  if (!req) continue;
1797
1880
  const missing = [];
1798
1881
  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);
1882
+ const issue = fieldIssue(card.fields || {}, f, CARD_FIELD_RULES[card.type]?.[f] || {});
1883
+ if (issue) missing.push(formatFieldIssue(issue));
1802
1884
  }
1803
1885
  if (missing.length > 0) {
1804
1886
  if (!summary.by_type[card.type]) summary.by_type[card.type] = [];
@@ -1905,6 +1987,25 @@ function exportProjectV1(project, name, outPath, opts = {}) {
1905
1987
  if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
1906
1988
  fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for v1 export.', 2);
1907
1989
  }
1990
+ const criticalIssues = criticalAxiomIssues(project);
1991
+ if (criticalIssues.length > 0) {
1992
+ if (opts.allowIncomplete) {
1993
+ console.warn(
1994
+ `--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
1995
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
1996
+ );
1997
+ } else {
1998
+ fail(
1999
+ `Cannot export v1: ${criticalIssues.length} critical axiom field issue(s).\n` +
2000
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
2001
+ ` decide when to load this domain). Fix them first, or pass\n` +
2002
+ ` --allow-incomplete to bypass.\n` +
2003
+ ` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
2004
+ (criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
2005
+ EXIT.HUMAN_LOCK_REQUIRED,
2006
+ );
2007
+ }
2008
+ }
1908
2009
  const gate = projectApi.checkHumanLockGate(project);
1909
2010
  if (gate.blocked) {
1910
2011
  if (opts.allowIncomplete) {
@@ -2082,7 +2183,7 @@ function manifestForSigning(manifest) {
2082
2183
 
2083
2184
  function canonicalPayload(files) {
2084
2185
  return ['mimetype', ...Object.keys(files).filter((k) => k !== 'mimetype').sort()]
2085
- .filter((name) => name !== 'signature.json' && name !== '.DS_Store')
2186
+ .filter((name) => name !== 'signature.json' && name !== '.DS_Store' && name !== 'build-receipt.json' && !name.startsWith('reports/'))
2086
2187
  .map((name) => {
2087
2188
  let content = name === 'mimetype' ? 'application/vnd.aikdna.kdna+zip' : files[name];
2088
2189
  if (name.endsWith('.json')) {
@@ -2179,6 +2280,25 @@ function cmdExport(args) {
2179
2280
  return;
2180
2281
  }
2181
2282
  const { project, result } = compileProject(projectInput);
2283
+ const criticalIssues = criticalAxiomIssues(project);
2284
+ if (criticalIssues.length > 0) {
2285
+ if (args.includes('--allow-incomplete')) {
2286
+ console.warn(
2287
+ `--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
2288
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
2289
+ );
2290
+ } else {
2291
+ fail(
2292
+ `Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
2293
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
2294
+ ` decide when to load this domain). Fix them first, or pass\n` +
2295
+ ` --allow-incomplete to bypass.\n` +
2296
+ ` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
2297
+ (criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
2298
+ EXIT.HUMAN_LOCK_REQUIRED,
2299
+ );
2300
+ }
2301
+ }
2182
2302
  const files = { ...result.files };
2183
2303
  files['README.md'] = compileApi.generateReadme(project);
2184
2304
  files.LICENSE = project.license?.type || 'UNSPECIFIED';
@@ -2253,15 +2373,16 @@ function cmdStudioInstall(args) {
2253
2373
  function cmdStudioUpdate(args) {
2254
2374
  const target = args[0];
2255
2375
  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
- }
2376
+ // kdna update does not exist in kdna-cli v0.28.x. The correct
2377
+ // path is to re-install via npm or use kdna install to pull
2378
+ // the latest published version of the asset.
2379
+ console.log(`To update ${target}, run:`);
2380
+ console.log(` npm install -g @aikdna/kdna-cli@latest`);
2381
+ console.log(` kdna install ${target}`);
2382
+ console.log(``);
2383
+ console.log(`Or for a project-local install:`);
2384
+ console.log(` cd <project-dir> && npm install @aikdna/kdna-cli@latest`);
2385
+ process.exit(0);
2265
2386
  }
2266
2387
 
2267
2388
  function cmdLlm(args) {
@@ -2379,9 +2500,11 @@ function cmdAuditLocks(args) {
2379
2500
  one_sentence: { min: 5, type: 'string' },
2380
2501
  full_statement: { min: 20, type: 'string' },
2381
2502
  why: { min: 20, type: 'string' },
2503
+ applies_when: { type: 'array', minLength: 1 },
2504
+ does_not_apply_when: { type: 'array', minLength: 1 },
2382
2505
  failure_risk: { min: 1, type: 'string' },
2383
2506
  confidence: { allowed: ['low', 'medium', 'high'] },
2384
- evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle'] },
2507
+ evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
2385
2508
  },
2386
2509
  risk: {
2387
2510
  name: { min: 1, type: 'string' },
@@ -2408,6 +2531,9 @@ function cmdAuditLocks(args) {
2408
2531
  if (rule.type === 'array' && !Array.isArray(value)) {
2409
2532
  return { field: name, issue: 'wrong_type', have_type: typeof value, need_type: 'array' };
2410
2533
  }
2534
+ if (rule.type === 'array' && rule.minLength && value.length < rule.minLength) {
2535
+ return { field: name, issue: 'too_short', have_length: value.length, need_min: rule.minLength };
2536
+ }
2411
2537
  return null;
2412
2538
  }
2413
2539
 
@@ -2719,6 +2845,7 @@ async function cmdFeynman(args) {
2719
2845
  const projectInput = args[0];
2720
2846
  const cardId = args[1];
2721
2847
  if (!projectInput || !cardId) fail('Usage: kdna-studio feynman <project> <card-id>');
2848
+ const useJson = args.includes('--json');
2722
2849
  const { projectPath, project } = readProject(projectInput);
2723
2850
  const cardIdx = (project.cards || []).findIndex(c => c.id === cardId);
2724
2851
  if (cardIdx < 0) fail(`Card not found: ${cardId}`);
@@ -2754,7 +2881,7 @@ async function cmdFeynman(args) {
2754
2881
  console.warn(`No feynman_restatement on ${cardId}; using synthesised:\n "${synthesised}"`);
2755
2882
  }
2756
2883
 
2757
- console.log(`Evaluating Feynman restatement for card: ${cardId}`);
2884
+ if (!useJson) console.log(`Evaluating Feynman restatement for card: ${cardId}`);
2758
2885
 
2759
2886
  // Bug (#1 UX follow-up): if no LLM is configured and the caller
2760
2887
  // did not pass --no-llm, route through cmdNeedsLlm which either
@@ -2783,14 +2910,26 @@ async function cmdFeynman(args) {
2783
2910
  const score = result.score || 0;
2784
2911
  const passed = score >= 4;
2785
2912
 
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}`));
2913
+ if (useJson) {
2914
+ console.log(JSON.stringify({
2915
+ card_id: cardId,
2916
+ score,
2917
+ passed,
2918
+ no_llm: !!llmGate.noLlm,
2919
+ criteria: result.criteria || {},
2920
+ suggestions: result.suggestions || [],
2921
+ feynman_restatement: card.feynman_restatement,
2922
+ }, null, 2));
2923
+ } else {
2924
+ console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
2925
+ for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
2926
+ const r = (result.criteria || {})[criterion] ? '✓' : '✗';
2927
+ console.log(` ${r} ${criterion}`);
2928
+ }
2929
+ if (result.suggestions && result.suggestions.length > 0) {
2930
+ console.log('\nSuggestions:');
2931
+ result.suggestions.forEach(s => console.log(` - ${s}`));
2932
+ }
2794
2933
  }
2795
2934
 
2796
2935
  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.10",
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) {