@aikdna/kdna-studio-cli 0.8.3 → 0.8.4

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.
@@ -357,10 +357,13 @@ function cardsFromV1Payload(payload) {
357
357
  }
358
358
  // Accept both `self_check` (canonical) and `self_checks` (legacy schema
359
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
- || [];
360
+ // payload-profile schema. Bug (#53): prior version used
361
+ // `arr || []`, which would crash with "object is not iterable" if
362
+ // the field was a truthy non-array (e.g. a single `{question: ...}`
363
+ // object instead of an array). The fix guards with Array.isArray.
364
+ const rawSelfCheck = payload.reasoning?.self_check
365
+ || payload.reasoning?.self_checks;
366
+ const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
364
367
  for (const selfCheck of selfCheckArr) {
365
368
  const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
366
369
  pushImportedCard(cards, 'self_check', fields, fields.id || null);
@@ -440,7 +443,7 @@ function cardsFromLegacyPayload(payload) {
440
443
  failure_risk: ms.failure_risk || '',
441
444
  }, ms.id || null);
442
445
  }
443
- for (const sc of (patterns.self_check || [])) {
446
+ for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
444
447
  const question = typeof sc === 'string' ? sc : sc.question || '';
445
448
  pushImportedCard(cards, 'self_check', { question }, sc.id || null);
446
449
  }
@@ -680,7 +683,7 @@ function importFromKdna(kdnaPath) {
680
683
  key_distinction: ms.key_distinction || '', why: ms.why || '',
681
684
  }));
682
685
  }
683
- for (const sc of (pat.self_check || [])) {
686
+ for (const sc of (Array.isArray(pat.self_check) ? pat.self_check : [])) {
684
687
  const q = typeof sc === 'string' ? sc : sc.question || '';
685
688
  importedCards.push(cardApi.createCard('self_check', { question: q }));
686
689
  }
@@ -833,7 +836,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
833
836
  key_distinction: ms.key_distinction || '', why: ms.why || '',
834
837
  }));
835
838
  }
836
- for (const sc of (patterns.self_check || [])) {
839
+ for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
837
840
  const q = typeof sc === 'string' ? sc : sc.question || '';
838
841
  importedCards.push(cardApi.createCard('self_check', { question: q }));
839
842
  }
@@ -1059,13 +1062,23 @@ function cmdImport(args) {
1059
1062
  return;
1060
1063
  }
1061
1064
  let content;
1065
+ let fd = null;
1062
1066
  try {
1063
- const fd = fs.openSync(filePath, 'r');
1067
+ fd = fs.openSync(filePath, 'r');
1064
1068
  const buf = Buffer.alloc(120000);
1065
1069
  const bytesRead = fs.readSync(fd, buf, 0, 120000, 0);
1066
- fs.closeSync(fd);
1067
1070
  content = buf.toString('utf8', 0, bytesRead);
1068
1071
  } catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
1072
+ finally {
1073
+ // Bug (#51): prior version called fs.closeSync only on the
1074
+ // success path, so any readSync failure left the file descriptor
1075
+ // open. Under bulk import (300 files), a single error mid-loop
1076
+ // could exhaust the per-process fd limit and silently break
1077
+ // later imports.
1078
+ if (fd !== null) {
1079
+ try { fs.closeSync(fd); } catch { /* best effort */ }
1080
+ }
1081
+ }
1069
1082
 
1070
1083
  const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
1071
1084
  evidenceApi.addEvidence(project, evidence);
@@ -1409,6 +1422,13 @@ function cmdMigrate(args) {
1409
1422
  projectPath = path.join(tmpDir, 'studio.project.json');
1410
1423
  }
1411
1424
 
1425
+ // Bug (#54): --allow-incomplete used to be read AFTER the critical
1426
+ // fields check, so a caller asking to bypass the gate was rejected
1427
+ // for the wrong reason. Resolve the flag here so the critical-missing
1428
+ // check honours it (and reports the bypass correctly) instead of
1429
+ // always failing.
1430
+ const allowIncompleteEarly = args.includes('--allow-incomplete');
1431
+
1412
1432
  // Reject if critical judgment fields are missing from axioms
1413
1433
  const criticalMissing = [];
1414
1434
  for (const card of (project.cards || [])) {
@@ -1421,13 +1441,21 @@ function cmdMigrate(args) {
1421
1441
  }
1422
1442
  }
1423
1443
  if (criticalMissing.length > 0) {
1424
- fail(
1425
- `Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
1426
- ` These fields are required for domain routing (kdna-loader uses them to\n` +
1427
- ` decide when to load this domain). Add them to your source files first.\n` +
1428
- ` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
1429
- (criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
1430
- );
1444
+ if (allowIncompleteEarly) {
1445
+ console.warn(
1446
+ `--allow-incomplete: bypassing ${criticalMissing.length} critical axiom-field check(s). ` +
1447
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
1448
+ );
1449
+ } else {
1450
+ fail(
1451
+ `Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
1452
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
1453
+ ` decide when to load this domain). Add them to your source files first,\n` +
1454
+ ` or pass --allow-incomplete to bypass.\n` +
1455
+ ` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
1456
+ (criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
1457
+ );
1458
+ }
1431
1459
  }
1432
1460
 
1433
1461
  // Step 2: approve and Human Lock all cards
@@ -1782,7 +1810,15 @@ function cmdExport(args) {
1782
1810
  fail(`Could not read password from stdin: ${e.message}`);
1783
1811
  }
1784
1812
  } else {
1785
- password = option(args, '--password') || option(args, '--passphrase');
1813
+ // Bug (#55): prior version only consulted --password / --passphrase
1814
+ // here, so a caller who set KDNA_PASSPHRASE for the signing path
1815
+ // (works via resolvePassphrase) found that the v1 encryption
1816
+ // password was silently null. Resolve from the same sources as
1817
+ // the signing path so the two stay in sync.
1818
+ password = process.env.KDNA_PASSPHRASE
1819
+ || process.env.KDNA_PASSWORD
1820
+ || option(args, '--password')
1821
+ || option(args, '--passphrase');
1786
1822
  }
1787
1823
  exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
1788
1824
  allowIncomplete: args.includes('--allow-incomplete'),
@@ -2231,12 +2267,41 @@ async function cmdTest(args) {
2231
2267
  const domainName = project.name || 'Untitled Project';
2232
2268
  const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
2233
2269
 
2270
+ // Bug (#50): prior version hand-rolled the prompt for the 3 preset
2271
+ // names and bypassed ai.testlab.testPreset entirely. The exported
2272
+ // testPreset function was dead code, and the help text advertised
2273
+ // names (baseline / edge / contradiction) that did not match the
2274
+ // actual preset keys in testlab.js (baseline / edge_case /
2275
+ // contradiction). Result: --preset edge silently ran the
2276
+ // contradiction branch, and any caller that asked for "edge_case"
2277
+ // fell through to the contradiction branch too.
2278
+ //
2279
+ // The fix routes the call through testPreset when a known preset is
2280
+ // given, and accepts both help-text names (baseline / edge /
2281
+ // contradiction) and the canonical testlab names (baseline / edge_case
2282
+ // / contradiction).
2283
+ const PRESET_ALIASES = {
2284
+ baseline: 'baseline',
2285
+ edge: 'edge_case',
2286
+ edge_case: 'edge_case',
2287
+ contradiction: 'contradiction',
2288
+ };
2289
+ const canonical = PRESET_ALIASES[preset] || 'baseline';
2234
2290
  console.log(`Testing: ${domainName} [${preset}]`);
2235
- const prompt = preset === 'baseline' ? `Test the core judgment: ${input}`
2236
- : preset === 'edge' ? `Test a boundary case: ${input}`
2237
- : `Test for contradiction: ${input}`;
2238
- const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
2239
- console.log(JSON.stringify(result, null, 2));
2291
+
2292
+ if (canonical === 'baseline') {
2293
+ // Baseline is a single comparison: keep the prior single-result
2294
+ // shape so existing parsers / consumers see the same output.
2295
+ const prompt = `Test the core judgment: ${input}`;
2296
+ const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
2297
+ console.log(JSON.stringify(result, null, 2));
2298
+ return;
2299
+ }
2300
+ // For non-baseline presets, run the preset + the baseline so the user
2301
+ // can see both side by side. The preset prompt is the one in
2302
+ // ai/testlab.js — that's the code path that was dead before.
2303
+ const result = await ai.testlab.testPreset(llm.config(), domainName, input, domainPrompt, {});
2304
+ console.log(JSON.stringify(result[canonical] || result, null, 2));
2240
2305
  }
2241
2306
 
2242
2307
  const args = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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.2",
42
+ "@aikdna/kdna-studio-core": "^1.7.3",
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
@@ -31,8 +31,20 @@ Return ONLY valid JSON:
31
31
  }`;
32
32
 
33
33
  async function evaluate(config, card, options = {}) {
34
- const axiomText = card.one_sentence || card.full_statement || '';
35
- const restatement = card.feynman_text || '';
34
+ // Bug: prior version read `card.feynman_text` (a field nothing in the
35
+ // codebase ever wrote) and flattened `card.one_sentence` /
36
+ // `card.full_statement` — the schema actually nests those under
37
+ // `card.fields`. The AI evaluator therefore always received an empty
38
+ // restatement and a blank axiom, no matter what was in the project.
39
+ // The fix reads the canonical schema field names and the nested
40
+ // `fields` object, with a fallback for callers that pass the flattened
41
+ // shape directly.
42
+ const fields = (card && card.fields) || card || {};
43
+ const axiomText = fields.one_sentence || fields.full_statement || fields.question || '';
44
+ const restatement = card.feynman_restatement
45
+ || fields.feynman_restatement
46
+ || fields.feynman_text
47
+ || '';
36
48
 
37
49
  if (!restatement) return { score: 0, criteria: {}, explanations: { notJustRepeat: 'No Feynman restatement provided.' }, suggestions: ['Write a Feynman restatement before evaluation.'] };
38
50