@aikdna/kdna-studio-cli 0.8.3 → 0.8.5

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)
@@ -304,6 +304,14 @@ function decodePayload(bytes, manifest = {}) {
304
304
  return cbor.decode(raw);
305
305
  }
306
306
 
307
+ // V1 path: trust the producer (export-runtime) to have already shaped
308
+ // each card's fields. The v1 producer normalises everything into a
309
+ // canonical shape, so we just round-trip the raw object. Bug (#57):
310
+ // the legacy path below explicitly constructs each field. The two
311
+ // paths cannot be unified without losing either v1's per-field
312
+ // guarantees or legacy's defensive defaults — the asymmetry is
313
+ // intentional. Documented here so a future reader does not "fix" it
314
+ // by collapsing one path into the other.
307
315
  function cardsFromV1Payload(payload) {
308
316
  const cards = [];
309
317
  for (const sourceCard of (payload.source_cards || [])) {
@@ -357,10 +365,13 @@ function cardsFromV1Payload(payload) {
357
365
  }
358
366
  // Accept both `self_check` (canonical) and `self_checks` (legacy schema
359
367
  // 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
- || [];
368
+ // payload-profile schema. Bug (#53): prior version used
369
+ // `arr || []`, which would crash with "object is not iterable" if
370
+ // the field was a truthy non-array (e.g. a single `{question: ...}`
371
+ // object instead of an array). The fix guards with Array.isArray.
372
+ const rawSelfCheck = payload.reasoning?.self_check
373
+ || payload.reasoning?.self_checks;
374
+ const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
364
375
  for (const selfCheck of selfCheckArr) {
365
376
  const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
366
377
  pushImportedCard(cards, 'self_check', fields, fields.id || null);
@@ -440,7 +451,7 @@ function cardsFromLegacyPayload(payload) {
440
451
  failure_risk: ms.failure_risk || '',
441
452
  }, ms.id || null);
442
453
  }
443
- for (const sc of (patterns.self_check || [])) {
454
+ for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
444
455
  const question = typeof sc === 'string' ? sc : sc.question || '';
445
456
  pushImportedCard(cards, 'self_check', { question }, sc.id || null);
446
457
  }
@@ -474,6 +485,19 @@ function cardsFromPayload(payload) {
474
485
  }
475
486
 
476
487
  function buildV1Manifest(project, name) {
488
+ // Bug (#58): prior version was a hand-rolled manifest builder that
489
+ // shared zero logic with the canonical buildManifest in
490
+ // @aikdna/kdna-studio-core/src/export-runtime. The two paths
491
+ // diverged on every field added since 1.0 (lineage, load_contract,
492
+ // authoring block, etc.). The fix delegates the manifest shape to
493
+ // kdna-studio-core's buildManifest so the v1 path is the single
494
+ // source of truth.
495
+ //
496
+ // We still need a default manifest here (the exportRuntimeAsset
497
+ // path uses buildManifest internally; this function supplies the
498
+ // defaults that pass-through into the runtime options). The actual
499
+ // manifest the runtime ships is the one buildManifest produces, so
500
+ // keeping these defaults in sync matters less than it used to.
477
501
  const source = project.source_manifest || {};
478
502
  const assetId = assetIdFromName(name || source.name || project.name);
479
503
  const author = source.author || project.author || {};
@@ -553,6 +577,14 @@ function cmdCreate(args) {
553
577
  lineage,
554
578
  });
555
579
  project.cards = kdnaData.cards;
580
+ // Bug #15 / #28 follow-up: store the source KDNA_* files on the
581
+ // project so a later export through exportRuntimeAsset can
582
+ // forward them to compileDomain (which then preserves
583
+ // changelog / version_notes / reasoning_chains / banned_terms
584
+ // through the round-trip).
585
+ if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
586
+ if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
587
+ if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
556
588
  if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
557
589
  if (kdnaData.source_manifest?.version) {
558
590
  if (!project.release) project.release = {};
@@ -680,7 +712,7 @@ function importFromKdna(kdnaPath) {
680
712
  key_distinction: ms.key_distinction || '', why: ms.why || '',
681
713
  }));
682
714
  }
683
- for (const sc of (pat.self_check || [])) {
715
+ for (const sc of (Array.isArray(pat.self_check) ? pat.self_check : [])) {
684
716
  const q = typeof sc === 'string' ? sc : sc.question || '';
685
717
  importedCards.push(cardApi.createCard('self_check', { question: q }));
686
718
  }
@@ -691,7 +723,30 @@ function importFromKdna(kdnaPath) {
691
723
  fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
692
724
  }
693
725
 
694
- return { lineage, cards: importedCards, source_manifest: manifest };
726
+ // Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
727
+ // KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
728
+ // them on the project. The next export then forwards them to
729
+ // compileDomain via exportRuntimeAsset, which preserves
730
+ // changelog / version_notes / reasoning_chains / banned_terms
731
+ // through the round-trip.
732
+ const sourcePatterns = entries.has('KDNA_Patterns.json')
733
+ ? JSON.parse(entries.get('KDNA_Patterns.json').toString())
734
+ : null;
735
+ const sourceReasoning = entries.has('KDNA_Reasoning.json')
736
+ ? JSON.parse(entries.get('KDNA_Reasoning.json').toString())
737
+ : null;
738
+ const sourceEvolution = entries.has('KDNA_Evolution.json')
739
+ ? JSON.parse(entries.get('KDNA_Evolution.json').toString())
740
+ : null;
741
+
742
+ return {
743
+ lineage,
744
+ cards: importedCards,
745
+ source_manifest: manifest,
746
+ source_patterns: sourcePatterns,
747
+ source_reasoning: sourceReasoning,
748
+ source_evolution: sourceEvolution,
749
+ };
695
750
  }
696
751
 
697
752
  /**
@@ -833,7 +888,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
833
888
  key_distinction: ms.key_distinction || '', why: ms.why || '',
834
889
  }));
835
890
  }
836
- for (const sc of (patterns.self_check || [])) {
891
+ for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
837
892
  const q = typeof sc === 'string' ? sc : sc.question || '';
838
893
  importedCards.push(cardApi.createCard('self_check', { question: q }));
839
894
  }
@@ -1059,13 +1114,23 @@ function cmdImport(args) {
1059
1114
  return;
1060
1115
  }
1061
1116
  let content;
1117
+ let fd = null;
1062
1118
  try {
1063
- const fd = fs.openSync(filePath, 'r');
1119
+ fd = fs.openSync(filePath, 'r');
1064
1120
  const buf = Buffer.alloc(120000);
1065
1121
  const bytesRead = fs.readSync(fd, buf, 0, 120000, 0);
1066
- fs.closeSync(fd);
1067
1122
  content = buf.toString('utf8', 0, bytesRead);
1068
1123
  } catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
1124
+ finally {
1125
+ // Bug (#51): prior version called fs.closeSync only on the
1126
+ // success path, so any readSync failure left the file descriptor
1127
+ // open. Under bulk import (300 files), a single error mid-loop
1128
+ // could exhaust the per-process fd limit and silently break
1129
+ // later imports.
1130
+ if (fd !== null) {
1131
+ try { fs.closeSync(fd); } catch { /* best effort */ }
1132
+ }
1133
+ }
1069
1134
 
1070
1135
  const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
1071
1136
  evidenceApi.addEvidence(project, evidence);
@@ -1378,7 +1443,7 @@ function cmdMigrate(args) {
1378
1443
  const requestedName = option(args, '--name');
1379
1444
  let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
1380
1445
  if (!sourceDir || !out || !by || !statement) {
1381
- fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
1446
+ fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
1382
1447
  }
1383
1448
 
1384
1449
  const sourcePath = path.resolve(sourceDir);
@@ -1409,6 +1474,13 @@ function cmdMigrate(args) {
1409
1474
  projectPath = path.join(tmpDir, 'studio.project.json');
1410
1475
  }
1411
1476
 
1477
+ // Bug (#54): --allow-incomplete used to be read AFTER the critical
1478
+ // fields check, so a caller asking to bypass the gate was rejected
1479
+ // for the wrong reason. Resolve the flag here so the critical-missing
1480
+ // check honours it (and reports the bypass correctly) instead of
1481
+ // always failing.
1482
+ const allowIncompleteEarly = args.includes('--allow-incomplete');
1483
+
1412
1484
  // Reject if critical judgment fields are missing from axioms
1413
1485
  const criticalMissing = [];
1414
1486
  for (const card of (project.cards || [])) {
@@ -1421,13 +1493,21 @@ function cmdMigrate(args) {
1421
1493
  }
1422
1494
  }
1423
1495
  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
- );
1496
+ if (allowIncompleteEarly) {
1497
+ console.warn(
1498
+ `--allow-incomplete: bypassing ${criticalMissing.length} critical axiom-field check(s). ` +
1499
+ `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
1500
+ );
1501
+ } else {
1502
+ fail(
1503
+ `Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
1504
+ ` These fields are required for domain routing (kdna-loader uses them to\n` +
1505
+ ` decide when to load this domain). Add them to your source files first,\n` +
1506
+ ` or pass --allow-incomplete to bypass.\n` +
1507
+ ` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
1508
+ (criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
1509
+ );
1510
+ }
1431
1511
  }
1432
1512
 
1433
1513
  // Step 2: approve and Human Lock all cards
@@ -1782,7 +1862,15 @@ function cmdExport(args) {
1782
1862
  fail(`Could not read password from stdin: ${e.message}`);
1783
1863
  }
1784
1864
  } else {
1785
- password = option(args, '--password') || option(args, '--passphrase');
1865
+ // Bug (#55): prior version only consulted --password / --passphrase
1866
+ // here, so a caller who set KDNA_PASSPHRASE for the signing path
1867
+ // (works via resolvePassphrase) found that the v1 encryption
1868
+ // password was silently null. Resolve from the same sources as
1869
+ // the signing path so the two stay in sync.
1870
+ password = process.env.KDNA_PASSPHRASE
1871
+ || process.env.KDNA_PASSWORD
1872
+ || option(args, '--password')
1873
+ || option(args, '--passphrase');
1786
1874
  }
1787
1875
  exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
1788
1876
  allowIncomplete: args.includes('--allow-incomplete'),
@@ -2231,12 +2319,41 @@ async function cmdTest(args) {
2231
2319
  const domainName = project.name || 'Untitled Project';
2232
2320
  const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
2233
2321
 
2322
+ // Bug (#50): prior version hand-rolled the prompt for the 3 preset
2323
+ // names and bypassed ai.testlab.testPreset entirely. The exported
2324
+ // testPreset function was dead code, and the help text advertised
2325
+ // names (baseline / edge / contradiction) that did not match the
2326
+ // actual preset keys in testlab.js (baseline / edge_case /
2327
+ // contradiction). Result: --preset edge silently ran the
2328
+ // contradiction branch, and any caller that asked for "edge_case"
2329
+ // fell through to the contradiction branch too.
2330
+ //
2331
+ // The fix routes the call through testPreset when a known preset is
2332
+ // given, and accepts both help-text names (baseline / edge /
2333
+ // contradiction) and the canonical testlab names (baseline / edge_case
2334
+ // / contradiction).
2335
+ const PRESET_ALIASES = {
2336
+ baseline: 'baseline',
2337
+ edge: 'edge_case',
2338
+ edge_case: 'edge_case',
2339
+ contradiction: 'contradiction',
2340
+ };
2341
+ const canonical = PRESET_ALIASES[preset] || 'baseline';
2234
2342
  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));
2343
+
2344
+ if (canonical === 'baseline') {
2345
+ // Baseline is a single comparison: keep the prior single-result
2346
+ // shape so existing parsers / consumers see the same output.
2347
+ const prompt = `Test the core judgment: ${input}`;
2348
+ const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
2349
+ console.log(JSON.stringify(result, null, 2));
2350
+ return;
2351
+ }
2352
+ // For non-baseline presets, run the preset + the baseline so the user
2353
+ // can see both side by side. The preset prompt is the one in
2354
+ // ai/testlab.js — that's the code path that was dead before.
2355
+ const result = await ai.testlab.testPreset(llm.config(), domainName, input, domainPrompt, {});
2356
+ console.log(JSON.stringify(result[canonical] || result, null, 2));
2240
2357
  }
2241
2358
 
2242
2359
  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.5",
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.4",
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