@aikdna/kdna-studio-cli 0.2.0 → 0.3.0

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.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @aikdna/kdna-studio-cli
2
2
 
3
- Official Studio command-line authoring entry for KDNA.
3
+ Official Studio command-line entry for KDNA judgment asset creation — turns your notes, documents, works, and feedback into loadable .kdna domains.
4
+
5
+ Two authoring paths: interview-first (articulate your judgment directly) and distillation-first (provide content, find the patterns, confirm what's really you).
6
+
7
+ Distillation-first authoring is domain-first: declare the target domain, owner scope, granularity, task scope, include areas, exclude areas, and load condition before extracting candidates. A single `.kdna` should stay scoped; complex work should compose multiple domain assets through a KDNA Cluster.
4
8
 
5
9
  This package provides the `kdna-studio` command. It creates Studio projects,
6
10
  imports evidence, manages judgment cards, checks Human Lock, compiles locked
@@ -25,6 +29,18 @@ npm install -g @aikdna/kdna-studio-cli
25
29
  ```bash
26
30
  kdna-studio create my_domain --name @yourscope/my_domain
27
31
  kdna-studio import my_domain ./notes.md
32
+ kdna-studio target declare my_domain \
33
+ --category expression_writing \
34
+ --scope personal \
35
+ --granularity core_principles \
36
+ --task "longform article review" \
37
+ --include "argument structure,tone,revision" \
38
+ --exclude "life habits,food preference"
39
+ kdna-studio source classify my_domain
40
+ kdna-studio distill my_domain --candidates candidates.json
41
+ kdna-studio candidate list my_domain
42
+ kdna-studio candidate accept my_domain <candidate-id>
43
+ kdna-studio candidate promote my_domain
28
44
  kdna-studio card add my_domain axiom \
29
45
  --field one_sentence="Prefer specific evidence over broad claims" \
30
46
  --field applies_when='["reviewing content"]' \
@@ -35,6 +51,8 @@ kdna-studio lock my_domain
35
51
  kdna-studio export my_domain --out dist/my_domain.kdna --sign
36
52
  ```
37
53
 
54
+ Candidate promotion is scope-gated: only candidates with `status == accepted` and `scope_fit == true` are promoted to cards by default. Use `kdna-studio candidate override <project> <candidate-id>` only when a human intentionally overrides the scope gate.
55
+
38
56
  After export, use the runtime CLI:
39
57
 
40
58
  ```bash
@@ -44,3 +62,27 @@ kdna publish dist/my_domain.kdna
44
62
 
45
63
  `kdna-studio` is the CLI entry for trusted KDNA creation. `kdna` is the runtime
46
64
  control plane for existing `.kdna` assets.
65
+
66
+ ## Identity
67
+
68
+ ```bash
69
+ kdna-studio identity init --name "Your Name"
70
+ kdna-studio identity show
71
+ ```
72
+
73
+ ## Import from existing KDNA or legacy folders
74
+
75
+ ```bash
76
+ # Fork an existing .kdna asset (cards imported as draft — re-lock required)
77
+ kdna-studio create forked --from-kdna ./parent.kdna --name @scope/forked
78
+
79
+ # Migrate a legacy JSON source folder
80
+ kdna-studio create migrated --from-folder ./old-domain-json --name @scope/migrated
81
+ ```
82
+
83
+ ## Related
84
+
85
+ - [KDNA Protocol](https://github.com/aikdna/kdna) — specification and schemas
86
+ - [kdna-cli](https://github.com/aikdna/kdna-cli) — runtime CLI for verify, install, load, publish
87
+ - [kdna-studio-core](https://github.com/aikdna/kdna-studio-core) — authoring SDK used by this CLI
88
+ - [kdna-studio-swift](https://github.com/aikdna/kdna-studio-swift) — native Swift counterpart
@@ -12,13 +12,21 @@ const {
12
12
  compile: compileApi,
13
13
  quality,
14
14
  creator: creatorApi,
15
+ distillation: distillationApi,
15
16
  } = require('@aikdna/kdna-studio-core');
16
17
 
18
+ const llm = require('../src/llm');
19
+ const ai = require('../src/ai');
20
+
17
21
  const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
18
22
 
19
23
  function usage() {
20
24
  console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
21
25
 
26
+ LLM (AI-powered authoring):
27
+ kdna-studio llm config [--provider <name>] [--model <name>] [--key <api-key>] [--url <base-url>]
28
+ kdna-studio llm show
29
+
22
30
  Identity:
23
31
  kdna-studio identity init [--name <display-name>]
24
32
  kdna-studio identity show
@@ -28,15 +36,39 @@ Create (three entry paths):
28
36
  kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
29
37
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
30
38
 
39
+ Migrate (dev source → trusted .kdna in one command):
40
+ kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign]
41
+
31
42
  Authoring:
32
- kdna-studio import <project> <source-file>
43
+ kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
44
+ kdna-studio filter <project> # check evidence for sensitive content
45
+ kdna-studio source classify <project> # classify evidence against declared target
33
46
  kdna-studio card list <project>
34
47
  kdna-studio card add <project> <type> --field key=value [--field key=value]
35
- kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]
48
+ kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
36
49
  kdna-studio lock <project>
37
50
  kdna-studio compile <project> --out <dir>
38
51
  kdna-studio export <project> --out <file.kdna> [--sign]
52
+
53
+ AI Authoring (requires LLM config: kdna-studio llm config):
54
+ kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
55
+ kdna-studio distill <project> --candidates <file.json> # load pre-generated AI candidates
56
+ kdna-studio interview <project> [--stage <name>] # 4-stage guided AI interview
57
+ kdna-studio feynman <project> <card-id> # AI Feynman evaluation (5-dimension)
58
+ kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]
59
+
60
+ Distillation:
61
+ kdna-studio target declare <project> # declare distillation target interactively
62
+ kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task> [--include <area,area>] [--exclude <area,area>] [--load-condition <condition>]
63
+ kdna-studio target show <project> # show current distillation target
64
+ kdna-studio candidate list <project> # list candidates with scope and status
65
+ kdna-studio candidate accept <project> <candidate-id>
66
+ kdna-studio candidate reject <project> <candidate-id>
67
+ kdna-studio candidate override <project> <candidate-id> # override scope gate
68
+ kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
39
69
  kdna-studio report <project>
70
+ kdna-studio install <@scope/name|file.kdna> [--trusted]
71
+ kdna-studio update <@scope/name>
40
72
 
41
73
  Project may be a directory containing studio.project.json or a project JSON file.`);
42
74
  }
@@ -157,7 +189,7 @@ function cmdCreate(args) {
157
189
  }
158
190
  } else if (fromFolder) {
159
191
  sourceMode = 'source_folder';
160
- importFromFolder(fromFolder, abs, name);
192
+ importFromFolder(fromFolder, abs, name, creatorIdentity);
161
193
  return;
162
194
  }
163
195
 
@@ -253,13 +285,14 @@ function importFromKdna(kdnaPath) {
253
285
  * Reads KDNA_*.json files and converts entries to draft judgment cards.
254
286
  * Outputs a schema audit report.
255
287
  */
256
- function importFromFolder(sourceDir, projectDir, projectName) {
288
+ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
257
289
  const absSource = path.resolve(sourceDir);
258
290
  if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
259
291
  if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
260
292
 
261
293
  const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
262
294
  const importedCards = [];
295
+ const manifestData = {};
263
296
 
264
297
  function loadJson(filename) {
265
298
  const p = path.join(absSource, filename);
@@ -269,6 +302,17 @@ function importFromFolder(sourceDir, projectDir, projectName) {
269
302
  catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
270
303
  }
271
304
 
305
+ // Load manifest for version/languages/description
306
+ const manifest = loadJson('kdna.json');
307
+ if (manifest) {
308
+ if (manifest.version) manifestData.version = manifest.version;
309
+ if (manifest.judgment_version) manifestData.judgment_version = manifest.judgment_version;
310
+ if (manifest.languages) manifestData.languages = manifest.languages;
311
+ if (manifest.default_language) manifestData.default_language = manifest.default_language;
312
+ if (manifest.description) manifestData.description = manifest.description;
313
+ if (manifest.name) manifestData.name = manifest.name;
314
+ }
315
+
272
316
  const core = loadJson('KDNA_Core.json');
273
317
  const patterns = loadJson('KDNA_Patterns.json');
274
318
  const scenarios = loadJson('KDNA_Scenarios.json');
@@ -301,9 +345,38 @@ function importFromFolder(sourceDir, projectDir, projectName) {
301
345
  for (const r of (core.risks || core.risk_model || [])) {
302
346
  importedCards.push(cardApi.createCard('risk', r.fields || r));
303
347
  }
348
+ // NEW: import stances
349
+ for (const s of (core.stances || [])) {
350
+ const text = typeof s === 'string' ? s : s.stance || '';
351
+ importedCards.push(cardApi.createCard('stance', {
352
+ statement: text,
353
+ applies_when: s.applies_when || [],
354
+ does_not_apply_when: s.does_not_apply_when || [],
355
+ }));
356
+ }
357
+ // NEW: import frameworks
358
+ for (const fw of (core.frameworks || [])) {
359
+ importedCards.push(cardApi.createCard('framework', {
360
+ name: fw.name || '', when_to_use: fw.when_to_use || '',
361
+ steps: fw.steps || [],
362
+ }));
363
+ }
304
364
  }
305
365
 
306
366
  if (patterns) {
367
+ // NEW: import terminology
368
+ if (patterns.terminology) {
369
+ for (const t of (patterns.terminology.standard_terms || [])) {
370
+ importedCards.push(cardApi.createCard('term', {
371
+ term: t.term || '', definition: t.definition || '',
372
+ }));
373
+ }
374
+ for (const bt of (patterns.terminology.banned_terms || [])) {
375
+ importedCards.push(cardApi.createCard('banned_term', {
376
+ term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
377
+ }));
378
+ }
379
+ }
307
380
  for (const ms of (patterns.misunderstandings || [])) {
308
381
  importedCards.push(cardApi.createCard('misunderstanding', {
309
382
  wrong: ms.wrong || '', correct: ms.correct || '',
@@ -316,14 +389,70 @@ function importFromFolder(sourceDir, projectDir, projectName) {
316
389
  }
317
390
  }
318
391
 
392
+ // NEW: import scenarios
393
+ if (scenarios && scenarios.scenes) {
394
+ for (const scene of scenarios.scenes) {
395
+ importedCards.push(cardApi.createCard('scenario', {
396
+ name: scene.name || scene.id || '',
397
+ trigger: scene.trigger || '', action: scene.action || '',
398
+ }));
399
+ }
400
+ }
401
+
402
+ // NEW: import cases
403
+ if (cases && cases.cases) {
404
+ for (const c of cases.cases) {
405
+ importedCards.push(cardApi.createCard('case', {
406
+ title: c.title || c.id || '',
407
+ scenario: c.scenario || '', expected: c.expected || '',
408
+ }));
409
+ }
410
+ }
411
+
412
+ // NEW: import reasoning chains
413
+ if (reasoning && reasoning.reasoning_chains) {
414
+ for (const rc of reasoning.reasoning_chains) {
415
+ importedCards.push(cardApi.createCard('reasoning', {
416
+ name: rc.name || rc.id || '', conclusion: rc.conclusion || '',
417
+ so_what: rc.so_what || '',
418
+ }));
419
+ }
420
+ }
421
+
422
+ // NEW: import evolution stages
423
+ if (evolution && evolution.stages) {
424
+ for (const stage of evolution.stages) {
425
+ importedCards.push(cardApi.createCard('evolution_stage', {
426
+ name: stage.name || stage.id || '',
427
+ level: stage.level || '', description: stage.description || '',
428
+ }));
429
+ }
430
+ }
431
+
319
432
  audit.cardsImported = importedCards.length;
320
433
 
321
434
  const project = projectApi.createProject(projectName, 'domain', {
322
435
  sourceMode: 'source_folder',
323
436
  sourcePath: absSource,
324
- creatorIdentity: null,
325
- lineage: { type: 'migrated', parent_name: null, parent_asset_uid: null, parent_version: null, parent_asset_digest: null },
437
+ creatorIdentity: creatorIdentity || null,
438
+ lineage: { type: 'migrated', parent_name: manifestData.name || null, parent_asset_uid: null, parent_version: manifestData.version || null, parent_asset_digest: null },
326
439
  });
440
+ // Preserve manifest metadata
441
+ if (manifestData.version) {
442
+ if (!project.release) project.release = {};
443
+ project.release.version = manifestData.version;
444
+ }
445
+ if (manifestData.judgment_version) {
446
+ if (!project.release) project.release = {};
447
+ project.release.judgment_version = manifestData.judgment_version;
448
+ }
449
+ if (manifestData.description) {
450
+ if (!project.release) project.release = {};
451
+ project.release.description = manifestData.description;
452
+ }
453
+ if (manifestData.languages) project.languages = manifestData.languages;
454
+ if (manifestData.default_language) project.default_language = manifestData.default_language;
455
+
327
456
  project.cards = importedCards;
328
457
  if (project.stages?.judgment_cards) {
329
458
  project.stages.judgment_cards.total = importedCards.length;
@@ -333,6 +462,7 @@ function importFromFolder(sourceDir, projectDir, projectName) {
333
462
  fs.mkdirSync(projectDir, { recursive: true });
334
463
  writeProject(path.join(projectDir, 'studio.project.json'), project);
335
464
  console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
465
+ return { project, projectPath: path.join(projectDir, 'studio.project.json'), audit };
336
466
  }
337
467
 
338
468
  /**
@@ -380,17 +510,85 @@ function readZipEntries(buf) {
380
510
  return entries;
381
511
  }
382
512
 
513
+ const aiFilter = require('../src/ai/filter');
514
+
383
515
  function cmdImport(args) {
384
516
  const [projectInput, source] = args;
385
- if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file>');
517
+ if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file-or-dir>');
386
518
  const { projectPath, project } = readProject(projectInput);
519
+
387
520
  const sourcePath = path.resolve(source);
388
- if (!fs.existsSync(sourcePath)) fail(`Source file not found: ${sourcePath}`);
389
- const content = fs.readFileSync(sourcePath, 'utf8');
390
- const evidence = evidenceApi.createEvidenceEntry('text', path.basename(sourcePath), content, sourcePath);
391
- evidenceApi.addEvidence(project, evidence);
521
+ if (!fs.existsSync(sourcePath)) fail(`Source not found: ${sourcePath}`);
522
+
523
+ const TEXT_EXTS = new Set(['.txt', '.md', '.json', '.jsonl', '.yaml', '.yml', '.csv', '.log', '.srt', '.vtt', '.html', '.xml', '.rst']);
524
+ const BINARY_EXTS = new Set(['.pdf', '.docx', '.rtf']);
525
+ let imported = 0;
526
+ let skipped = 0;
527
+
528
+ function importFile(filePath) {
529
+ const ext = path.extname(filePath).toLowerCase();
530
+ const name = path.basename(filePath);
531
+
532
+ if (BINARY_EXTS.has(ext)) {
533
+ console.warn(`Skipping binary format (not yet supported in CLI): ${name}`);
534
+ skipped++;
535
+ return;
536
+ }
537
+
538
+ let content;
539
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
540
+
541
+ if (content.length > 120000) {
542
+ console.warn(`File too large (${content.length} chars, max 120000): ${name}`);
543
+ skipped++;
544
+ return;
545
+ }
546
+
547
+ const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
548
+ evidenceApi.addEvidence(project, evidence);
549
+ imported++;
550
+ }
551
+
552
+ if (fs.statSync(sourcePath).isDirectory()) {
553
+ const files = fs.readdirSync(sourcePath).map(f => path.join(sourcePath, f)).filter(f => fs.statSync(f).isFile());
554
+ for (const f of files) {
555
+ if (imported >= 300) { console.warn('Reached 300 file limit.'); break; }
556
+ importFile(f);
557
+ }
558
+ } else {
559
+ importFile(sourcePath);
560
+ }
561
+
392
562
  writeProject(projectPath, project);
393
- console.log(`Imported evidence: ${evidence.id}`);
563
+
564
+ // Check for sensitive content
565
+ const evidenceList = project.evidence_materials || [];
566
+ const flagged = aiFilter.checkEvidence(evidenceList);
567
+ if (flagged.length > 0) {
568
+ console.warn(`\nSensitive content detected in ${flagged.length} file(s):`);
569
+ for (const f of flagged) console.warn(` ${f.filename}: ${f.flagged.join(', ')}`);
570
+ }
571
+
572
+ console.log(`Imported: ${imported} file(s)${skipped > 0 ? `, ${skipped} skipped` : ''}`);
573
+ }
574
+
575
+ function cmdFilter(args) {
576
+ const projectInput = args[0];
577
+ if (!projectInput) fail('Usage: kdna-studio filter <project>');
578
+ const { project } = readProject(projectInput);
579
+ const evidenceList = project.evidence_materials || [];
580
+ if (evidenceList.length === 0) { console.log('No evidence to filter.'); return; }
581
+
582
+ const flagged = aiFilter.checkEvidence(evidenceList);
583
+ if (flagged.length === 0) {
584
+ console.log(`All ${evidenceList.length} evidence file(s) passed sensitive content filter.`);
585
+ } else {
586
+ console.log(`Sensitive content detected in ${flagged.length}/${evidenceList.length} file(s):`);
587
+ for (const f of flagged) {
588
+ console.log(` ${f.filename}: ${f.flagged.join(', ')}`);
589
+ for (const [domain, match] of Object.entries(f.matches)) console.log(` ${domain}: "${match}"`);
590
+ }
591
+ }
394
592
  }
395
593
 
396
594
  function cmdCard(args) {
@@ -420,46 +618,66 @@ function cmdCard(args) {
420
618
  if (sub === 'approve') {
421
619
  const projectInput = args[1];
422
620
  const cardId = args[2];
423
- if (!projectInput || !cardId) {
621
+ const approveAll = args.includes('--all');
622
+ if (!projectInput || (!cardId && !approveAll)) {
424
623
  fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
425
624
  }
426
625
  const by = option(args, '--by');
427
626
  const statement = option(args, '--statement');
428
627
  if (!by || !statement) fail('card approve requires --by and --statement');
429
628
  const { projectPath, project } = readProject(projectInput);
430
- const idx = (project.cards || []).findIndex((c) => c.id === cardId);
431
- if (idx < 0) fail(`Card not found: ${cardId}`);
432
- let card = project.cards[idx];
433
- if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
434
629
 
435
- const lockPayload = {
436
- by,
437
- statement,
438
- checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
630
+ const lockOneCard = (card) => {
631
+ if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
632
+ const lockPayload = {
633
+ by,
634
+ statement,
635
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
636
+ };
637
+ if (args.includes('--sign')) {
638
+ const identity = creatorApi.loadIdentity();
639
+ if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
640
+ lockPayload.creator_id = identity.creator_id;
641
+ const passphrase = option(args, '--passphrase');
642
+ try {
643
+ if (identity.encrypted && !passphrase) {
644
+ fail('Private key is encrypted — provide --passphrase to sign.', EXIT.TRUST_FAILED);
645
+ }
646
+ lockPayload.signature = creatorApi.signHumanLock(
647
+ card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
648
+ );
649
+ } catch (e) {
650
+ fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
651
+ }
652
+ }
653
+ return [card, cardApi.lockCard(card, lockPayload)];
439
654
  };
440
655
 
441
- // Sign Human Lock with creator identity if --sign is passed
442
- if (args.includes('--sign')) {
443
- const identity = creatorApi.loadIdentity();
444
- if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
445
- lockPayload.creator_id = identity.creator_id;
446
- try {
447
- lockPayload.signature = creatorApi.signHumanLock(
448
- cardId, statement, cardApi.cardJudgmentFingerprint(card),
449
- );
450
- } catch (e) {
451
- fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
656
+ if (cardId) {
657
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
658
+ if (idx < 0) fail(`Card not found: ${cardId}`);
659
+ const [, locked] = lockOneCard(project.cards[idx]);
660
+ project.cards[idx] = locked;
661
+ const signed = args.includes('--sign') ? ' (signed)' : '';
662
+ console.log(`Approved and Human Locked: ${cardId}${signed}`);
663
+ } else {
664
+ let approved = 0;
665
+ for (let i = 0; i < (project.cards || []).length; i++) {
666
+ const card = project.cards[i];
667
+ if (card.locked) continue;
668
+ const [, locked] = lockOneCard(card);
669
+ project.cards[i] = locked;
670
+ approved++;
452
671
  }
672
+ if (approved === 0) fail('No unlocked cards to approve.');
673
+ const signed = args.includes('--sign') ? ' (signed)' : '';
674
+ console.log(`Approved and Human Locked: ${approved} cards${signed}`);
453
675
  }
454
-
455
- project.cards[idx] = cardApi.lockCard(card, lockPayload);
456
676
  if (project.stages?.judgment_cards) {
457
677
  project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
458
678
  project.stages.judgment_cards.total = project.cards.length;
459
679
  }
460
680
  writeProject(projectPath, project);
461
- const signed = lockPayload.signature ? ' (signed)' : '';
462
- console.log(`Approved and Human Locked: ${cardId}${signed}`);
463
681
  return;
464
682
  }
465
683
  fail('Usage: kdna-studio card <list|add|approve> ...');
@@ -476,6 +694,83 @@ function cmdLock(args) {
476
694
  console.log(`Human Lock Gate passed: ${gate.lockedJudgmentCards} locked judgment cards`);
477
695
  }
478
696
 
697
+ function cmdMigrate(args) {
698
+ const sourceDir = args[0];
699
+ const out = option(args, '--out');
700
+ const by = option(args, '--by');
701
+ const statement = option(args, '--statement');
702
+ const name = option(args, '--name') || path.basename(path.resolve(sourceDir || '.'));
703
+ if (!sourceDir || !out || !by || !statement) {
704
+ fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
705
+ }
706
+
707
+ // Step 1: create temp project and import everything from dev source
708
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
709
+ let creatorIdentity = null;
710
+ try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
711
+ const { project } = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
712
+
713
+ // Step 2: approve and Human Lock all cards
714
+ let locked = 0;
715
+ for (let i = 0; i < (project.cards || []).length; i++) {
716
+ let card = project.cards[i];
717
+ if (card.locked) { locked++; continue; }
718
+ if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
719
+ const lockPayload = {
720
+ by,
721
+ statement,
722
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
723
+ };
724
+ if (args.includes('--sign')) {
725
+ const identity = creatorApi.loadIdentity();
726
+ if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
727
+ lockPayload.creator_id = identity.creator_id;
728
+ const passphrase = option(args, '--passphrase');
729
+ try {
730
+ lockPayload.signature = creatorApi.signHumanLock(
731
+ card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
732
+ );
733
+ } catch (e) {
734
+ fail(`Failed to sign Human Lock for ${card.id}: ${e.message}`, EXIT.TRUST_FAILED);
735
+ }
736
+ }
737
+ project.cards[i] = cardApi.lockCard(card, lockPayload);
738
+ locked++;
739
+ }
740
+ if (project.stages?.judgment_cards) {
741
+ project.stages.judgment_cards.locked = locked;
742
+ project.stages.judgment_cards.total = project.cards.length;
743
+ }
744
+ writeProject(path.join(tmpDir, 'studio.project.json'), project);
745
+ console.log(`Approved and Human Locked: ${locked} cards`);
746
+
747
+ // Step 3: verify Human Lock gate
748
+ const gate = projectApi.checkHumanLockGate(project);
749
+ if (gate.blocked) {
750
+ const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
751
+ fail(`Human Lock Gate blocked: ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
752
+ }
753
+
754
+ // Step 4: compile and export as .kdna
755
+ const { result } = compileProject(tmpDir);
756
+ const files = { ...result.files };
757
+ files['README.md'] = compileApi.generateReadme(project);
758
+ files.LICENSE = project.license?.type || 'UNSPECIFIED';
759
+ files.mimetype = 'application/vnd.aikdna.kdna+zip';
760
+
761
+ if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
762
+ const absOut = path.resolve(out);
763
+ buildZip(files, absOut);
764
+ console.log(`Exported: ${absOut}`);
765
+ console.log(` Name: ${name}`);
766
+ console.log(` Cards: ${project.cards.length} (${locked} locked)`);
767
+ console.log(` Files: ${Object.keys(files).length}`);
768
+ console.log(` Build ID: ${result.identity.build_id}`);
769
+
770
+ // Cleanup temp
771
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
772
+ }
773
+
479
774
  function compileProject(projectInput) {
480
775
  const { project } = readProject(projectInput);
481
776
  const gate = projectApi.checkHumanLockGate(project);
@@ -623,7 +918,7 @@ function publicKeyFingerprint(publicKeyPem) {
623
918
  return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
624
919
  }
625
920
 
626
- function applySignature(files) {
921
+ function applySignature(files, passphrase = null) {
627
922
  const paths = identityPaths();
628
923
  if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
629
924
  fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
@@ -636,7 +931,13 @@ function applySignature(files) {
636
931
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
637
932
 
638
933
  const payload = canonicalPayload(files);
639
- const privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
934
+ let privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
935
+ if (creatorApi.isEncryptedKey(privateKeyPem)) {
936
+ if (!passphrase) {
937
+ fail('Private key is encrypted. Export with: kdna-studio export ... --sign --passphrase <your-passphrase>', EXIT.TRUST_FAILED);
938
+ }
939
+ privateKeyPem = creatorApi.decryptPrivateKey(privateKeyPem, passphrase);
940
+ }
640
941
  manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
641
942
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
642
943
  }
@@ -673,7 +974,7 @@ function cmdExport(args) {
673
974
  provenance.content_fingerprint = finalDigest;
674
975
  files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
675
976
 
676
- if (args.includes('--sign')) applySignature(files);
977
+ if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
677
978
 
678
979
  const entries = [['mimetype', files.mimetype]];
679
980
  for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
@@ -700,18 +1001,80 @@ function cmdExport(args) {
700
1001
  console.log(`Build ID: ${result.identity.build_id}`);
701
1002
  }
702
1003
 
1004
+ function cmdStudioInstall(args) {
1005
+ // Delegate to kdna CLI for runtime install operations
1006
+ const target = args[0];
1007
+ if (!target) fail('Usage: kdna-studio install <@scope/name|file.kdna>');
1008
+ try {
1009
+ const kdnaArgs = ['install', target, '--yes'];
1010
+ if (args.includes('--trusted')) kdnaArgs.push('--trusted');
1011
+ const result = require('child_process').spawnSync('kdna', kdnaArgs, {
1012
+ stdio: 'inherit', encoding: 'utf8',
1013
+ });
1014
+ if (result.status !== 0) process.exit(result.status);
1015
+ } catch {
1016
+ fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
1017
+ }
1018
+ }
1019
+
1020
+ function cmdStudioUpdate(args) {
1021
+ const target = args[0];
1022
+ if (!target) fail('Usage: kdna-studio update <@scope/name>');
1023
+ try {
1024
+ const result = require('child_process').spawnSync('kdna', ['update', target], {
1025
+ stdio: 'inherit', encoding: 'utf8',
1026
+ });
1027
+ if (result.status !== 0) process.exit(result.status);
1028
+ } catch {
1029
+ fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
1030
+ }
1031
+ }
1032
+
1033
+ function cmdLlm(args) {
1034
+ const sub = args[0];
1035
+ if (sub === 'show') {
1036
+ const cfg = llm.config();
1037
+ console.log(`Provider : ${cfg.provider || '(not set)'}`);
1038
+ console.log(`Model : ${cfg.model || '(not set)'}`);
1039
+ console.log(`Base URL : ${cfg.baseURL || '(not set)'}`);
1040
+ console.log(`API Key : ${cfg.apiKey ? '********' + cfg.apiKey.slice(-4) : '(not set)'}`);
1041
+ return;
1042
+ }
1043
+ if (sub === 'config') {
1044
+ const provider = option(args, '--provider') || option(args, '-p');
1045
+ const model = option(args, '--model') || option(args, '-m');
1046
+ const apiKey = option(args, '--key') || option(args, '-k');
1047
+ const baseURL = option(args, '--url') || option(args, '-u');
1048
+ const updates = {};
1049
+ if (provider) updates.provider = provider;
1050
+ if (model) updates.model = model;
1051
+ if (apiKey) updates.apiKey = apiKey;
1052
+ if (baseURL) updates.baseURL = baseURL;
1053
+ if (Object.keys(updates).length === 0) fail('Usage: kdna-studio llm config --provider <name> [--model <name>] [--key <api-key>] [--url <base-url>]');
1054
+ const cfg = llm.configure(updates);
1055
+ console.log(JSON.stringify({ provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL }, null, 2));
1056
+ return;
1057
+ }
1058
+ fail('Usage: kdna-studio llm <config|show>');
1059
+ }
1060
+
703
1061
  function cmdIdentity(args) {
704
1062
  const sub = args[0];
705
1063
  if (sub === 'init') {
706
1064
  const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
1065
+ const passphrase = option(args, '--passphrase');
707
1066
  try {
708
- const identity = creatorApi.initIdentity(name);
1067
+ const identity = creatorApi.initIdentity(name, null, passphrase);
709
1068
  console.log(`Creator identity initialized:`);
710
1069
  console.log(` creator_id: ${identity.creator_id}`);
711
1070
  console.log(` display_name: ${identity.display_name}`);
712
1071
  console.log(` identity_dir: ${identity.identity_dir}`);
713
1072
  console.log(` public_key saved: ${identity.public_key_path}`);
1073
+ console.log(` encrypted: ${identity.encrypted}`);
714
1074
  console.log(`\nBack up your private key at: ${creatorApi.privateKeyPath()}`);
1075
+ if (identity.encrypted) {
1076
+ console.log(` Private key is encrypted — keep your passphrase safe.`);
1077
+ }
715
1078
  } catch (err) {
716
1079
  fail(`Identity already exists. Use 'kdna-studio identity show' to view. ${err.message}`);
717
1080
  }
@@ -739,6 +1102,269 @@ function cmdReport(args) {
739
1102
  process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
740
1103
  }
741
1104
 
1105
+ // ─── Distillation Commands ──────────────────────────────────────────
1106
+
1107
+ function cmdTarget(args) {
1108
+ const sub = args[0];
1109
+ if (sub === 'declare') {
1110
+ const projectInput = args[1];
1111
+ if (!projectInput) fail('Usage: kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task>');
1112
+ const { projectPath, project } = readProject(projectInput);
1113
+ const category = option(args, '--category', 'expression_writing');
1114
+ const scope = option(args, '--scope', 'personal');
1115
+ const granularity = option(args, '--granularity', 'core_principles');
1116
+ const taskScope = option(args, '--task', 'general content creation');
1117
+ const include = option(args, '--include');
1118
+ const exclude = option(args, '--exclude');
1119
+ const loadCondition = option(args, '--load-condition');
1120
+ const domainName = project.name || path.basename(path.dirname(projectPath), '.json');
1121
+
1122
+ const target = distillationApi.createDistillationTarget({
1123
+ domainName,
1124
+ domainCategory: category,
1125
+ ownerScope: scope,
1126
+ granularity,
1127
+ taskScope,
1128
+ includeAreas: include ? include.split(',').map(s => s.trim()).filter(Boolean) : [],
1129
+ excludeAreas: exclude ? exclude.split(',').map(s => s.trim()).filter(Boolean) : [],
1130
+ loadCondition: loadCondition || '',
1131
+ });
1132
+
1133
+ const validation = distillationApi.validateDistillationTarget(target);
1134
+ if (!validation.valid) fail(`Invalid target: ${validation.errors.join('; ')}`);
1135
+
1136
+ project.distillation_target = target;
1137
+ writeProject(projectPath, project);
1138
+ console.log(JSON.stringify({ declared: true, target }, null, 2));
1139
+ return;
1140
+ }
1141
+ if (sub === 'show') {
1142
+ const { project } = readProject(args[1]);
1143
+ const target = project.distillation_target;
1144
+ if (!target) fail('No distillation target declared. Run: kdna-studio target declare <project>');
1145
+ console.log(JSON.stringify(target, null, 2));
1146
+ return;
1147
+ }
1148
+ fail('Usage: kdna-studio target <declare|show>');
1149
+ }
1150
+
1151
+ function cmdSourceClassify(args) {
1152
+ const projectInput = args[0];
1153
+ if (!projectInput) fail('Usage: kdna-studio source classify <project>');
1154
+ const { projectPath, project } = readProject(projectInput);
1155
+ const target = project.distillation_target;
1156
+ if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
1157
+
1158
+ const evidence = project.evidence || [];
1159
+ const results = evidence.map(e => {
1160
+ const text = (e.content || e.title || '').toLowerCase();
1161
+ const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
1162
+ const hitCount = domainWords.filter(w => text.includes(w.toLowerCase())).length;
1163
+ let relevance;
1164
+ if (hitCount >= 2) relevance = 'relevant';
1165
+ else if (hitCount === 1) relevance = 'weakly_relevant';
1166
+ else {
1167
+ const otherHits = Object.keys(distillationApi.DOMAIN_CATEGORIES)
1168
+ .filter(k => k !== target.domain_category)
1169
+ .filter(k => text.includes(k)).length;
1170
+ relevance = otherHits > 0 ? 'split_domain' : 'weakly_relevant';
1171
+ }
1172
+ return { id: e.id, title: e.title, relevance };
1173
+ });
1174
+
1175
+ const summary = {};
1176
+ for (const r of results) { summary[r.relevance] = (summary[r.relevance] || 0) + 1; }
1177
+
1178
+ project.distillation_evidence_relevance = results;
1179
+ writeProject(projectPath, project);
1180
+ console.log(JSON.stringify({ summary, results }, null, 2));
1181
+ }
1182
+
1183
+ async function cmdDistill(args) {
1184
+ const projectInput = args[0];
1185
+ const candidatesFile = option(args, '--candidates');
1186
+ const useAI = args.includes('--ai');
1187
+ const provider = option(args, '--provider');
1188
+ const model = option(args, '--model');
1189
+ const apiKey = option(args, '--key');
1190
+ if (!projectInput) fail('Usage: kdna-studio distill <project> [--ai] [--candidates <file.json>]');
1191
+ if (!candidatesFile && !useAI) fail('Either --ai or --candidates <file.json> required.');
1192
+
1193
+ const { projectPath, project } = readProject(projectInput);
1194
+ const target = project.distillation_target;
1195
+ if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
1196
+
1197
+ let rawCandidates;
1198
+ if (useAI) {
1199
+ const evidence = (project.evidence_materials || []).map(e => ({ filename: e.filename || e.name, content: e.raw_text || e.content || '' }));
1200
+ if (evidence.length === 0) console.warn('No evidence materials found. AI distillation works best with imported evidence. Run: kdna-studio import <project> <file>');
1201
+
1202
+ console.log('Running AI distillation...');
1203
+ rawCandidates = await ai.distill(llm.config(), evidence, target, { provider, model, apiKey });
1204
+ console.log(`AI extracted ${rawCandidates.length} candidates.`);
1205
+ } else {
1206
+ const rawPath = path.resolve(candidatesFile);
1207
+ if (!fs.existsSync(rawPath)) fail(`Candidates file not found: ${rawPath}`);
1208
+ rawCandidates = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
1209
+ if (!Array.isArray(rawCandidates)) fail('Candidates file must contain a JSON array');
1210
+ }
1211
+
1212
+ const candidates = rawCandidates.map(c => {
1213
+ const base = {
1214
+ id: c.candidate_id || c.id || `cand_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1215
+ one_sentence: c.one_sentence || c.oneSentence || '',
1216
+ full_statement: c.full_statement || c.fullStatement || '',
1217
+ suggested_card_type: c.type || c.suggested_card_type || 'axiom',
1218
+ supporting_evidence_ids: (c.evidence_ids || c.evidenceIds || c.supporting_evidence_ids || []).map(String),
1219
+ confidence: c.confidence || 'medium',
1220
+ candidate_status: 'proposed',
1221
+ scope_fit: c.scope_fit ?? c.scopeFit ?? true,
1222
+ };
1223
+ return distillationApi.applyScopeGate(base, target);
1224
+ });
1225
+
1226
+ const summary = distillationApi.candidateStatusSummary(candidates);
1227
+ project.distillation_candidates = candidates;
1228
+ writeProject(projectPath, project);
1229
+ console.log(JSON.stringify({ summary, count: candidates.length }, null, 2));
1230
+ }
1231
+
1232
+ function cmdCandidate(args) {
1233
+ const sub = args[0];
1234
+ const projectInput = args[1];
1235
+ if (!sub || !projectInput) fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
1236
+
1237
+ const { projectPath, project } = readProject(projectInput);
1238
+ const candidates = project.distillation_candidates || [];
1239
+
1240
+ if (sub === 'list') {
1241
+ for (const c of candidates) {
1242
+ const scopeMarker = c.scope_fit ? '' : ' [OUT_OF_SCOPE]';
1243
+ const split = c.suggested_split_domain ? ` → ${c.suggested_split_domain}` : '';
1244
+ console.log(`${c.id}\t${c.candidate_status}\t${c.suggested_card_type}\t${c.confidence}\t${c.one_sentence.substring(0,60)}${scopeMarker}${split}`);
1245
+ }
1246
+ console.log(JSON.stringify(distillationApi.candidateStatusSummary(candidates), null, 2));
1247
+ return;
1248
+ }
1249
+
1250
+ if (sub === 'accept' || sub === 'reject') {
1251
+ const candId = args[2];
1252
+ if (!candId) fail(`Usage: kdna-studio candidate ${sub} <project> <candidate-id>`);
1253
+ const idx = candidates.findIndex(c => c.id === candId);
1254
+ if (idx < 0) fail(`Candidate not found: ${candId}`);
1255
+ candidates[idx].candidate_status = sub === 'accept' ? 'accepted' : 'rejected';
1256
+ project.distillation_candidates = candidates;
1257
+ writeProject(projectPath, project);
1258
+ console.log(`${sub === 'accept' ? 'Accepted' : 'Rejected'}: ${candId}`);
1259
+ return;
1260
+ }
1261
+
1262
+ if (sub === 'override') {
1263
+ const candId = args[2];
1264
+ if (!candId) fail('Usage: kdna-studio candidate override <project> <candidate-id>');
1265
+ const idx = candidates.findIndex(c => c.id === candId);
1266
+ if (idx < 0) fail(`Candidate not found: ${candId}`);
1267
+ candidates[idx].scope_fit = true;
1268
+ candidates[idx].domain_relevance_score = 50;
1269
+ candidates[idx].relevance_evidence = 'User explicitly overrode scope gate via CLI';
1270
+ candidates[idx].suggested_split_domain = null;
1271
+ project.distillation_candidates = candidates;
1272
+ writeProject(projectPath, project);
1273
+ console.log(`Scope gate overridden: ${candId}`);
1274
+ return;
1275
+ }
1276
+
1277
+ if (sub === 'promote') {
1278
+ const accepted = candidates.filter(c => c.candidate_status === 'accepted' && c.scope_fit !== false);
1279
+ if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
1280
+
1281
+ for (const c of accepted) {
1282
+ const card = cardApi.createCard(c.suggested_card_type, {
1283
+ one_sentence: c.one_sentence,
1284
+ full_statement: c.full_statement,
1285
+ why: '',
1286
+ applies_when: '',
1287
+ does_not_apply_when: '',
1288
+ failure_risk: '',
1289
+ });
1290
+ card.evidence_refs = c.supporting_evidence_ids;
1291
+ project.cards = project.cards || [];
1292
+ project.cards.push(card);
1293
+ }
1294
+ if (project.stages?.judgment_cards) {
1295
+ project.stages.judgment_cards.total = project.cards.length;
1296
+ project.stages.judgment_cards.status = 'in_progress';
1297
+ }
1298
+ writeProject(projectPath, project);
1299
+ console.log(`Promoted ${accepted.length} candidates → ${accepted.length} Studio cards`);
1300
+ return;
1301
+ }
1302
+
1303
+ fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
1304
+ }
1305
+
1306
+ async function cmdInterview(args) {
1307
+ const projectInput = args[0];
1308
+ const stage = option(args, '--stage');
1309
+ if (!projectInput) fail('Usage: kdna-studio interview <project> [--stage distill|clarify|correct|replay]');
1310
+ const { project } = readProject(projectInput);
1311
+
1312
+ if (stage) {
1313
+ const result = await ai.interview.runInterview(llm.config(), project, stage, '', {});
1314
+ console.log(`\n=== ${result.label} ===\n${result.content}`);
1315
+ } else {
1316
+ await ai.interview.runInterviewInteractive(llm.config(), project, {});
1317
+ }
1318
+ }
1319
+
1320
+ async function cmdFeynman(args) {
1321
+ const projectInput = args[0];
1322
+ const cardId = args[1];
1323
+ if (!projectInput || !cardId) fail('Usage: kdna-studio feynman <project> <card-id>');
1324
+ const { projectPath, project } = readProject(projectInput);
1325
+ const cardIdx = (project.cards || []).findIndex(c => c.id === cardId);
1326
+ if (cardIdx < 0) fail(`Card not found: ${cardId}`);
1327
+ const card = project.cards[cardIdx];
1328
+
1329
+ if (!card.feynman_text) fail(`Card ${cardId} has no Feynman restatement. Use card approve first with --statement to set it.`);
1330
+
1331
+ console.log(`Evaluating Feynman restatement for card: ${cardId}`);
1332
+ const result = await ai.feynman.evaluate(llm.config(), card, {});
1333
+ const score = result.score || 0;
1334
+ const passed = score >= 4;
1335
+
1336
+ console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}`);
1337
+ for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
1338
+ const r = (result.criteria || {})[criterion] ? '✓' : '✗';
1339
+ console.log(` ${r} ${criterion}`);
1340
+ }
1341
+ if (result.suggestions && result.suggestions.length > 0) {
1342
+ console.log('\nSuggestions:');
1343
+ result.suggestions.forEach(s => console.log(` - ${s}`));
1344
+ }
1345
+
1346
+ card.feynman_evaluation = { score, criteria: result.criteria, suggestions: result.suggestions, evaluated_at: new Date().toISOString() };
1347
+ project.cards[cardIdx] = card;
1348
+ writeProject(projectPath, project);
1349
+ }
1350
+
1351
+ async function cmdTest(args) {
1352
+ const projectInput = args[0];
1353
+ const input = option(args, '--input');
1354
+ const preset = option(args, '--preset', 'baseline');
1355
+ if (!projectInput || !input) fail('Usage: kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]');
1356
+ const { project } = readProject(projectInput);
1357
+ const domainName = project.name || 'Untitled Project';
1358
+ const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
1359
+
1360
+ console.log(`Testing: ${domainName} [${preset}]`);
1361
+ const prompt = preset === 'baseline' ? `Test the core judgment: ${input}`
1362
+ : preset === 'edge' ? `Test a boundary case: ${input}`
1363
+ : `Test for contradiction: ${input}`;
1364
+ const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
1365
+ console.log(JSON.stringify(result, null, 2));
1366
+ }
1367
+
742
1368
  const args = process.argv.slice(2);
743
1369
  const cmd = args[0];
744
1370
  if (!cmd || cmd === '--help' || cmd === '-h') {
@@ -746,15 +1372,28 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
746
1372
  process.exit(EXIT.OK);
747
1373
  }
748
1374
 
1375
+ (async () => {
749
1376
  try {
750
1377
  if (cmd === 'create') cmdCreate(args.slice(1));
1378
+ else if (cmd === 'migrate') cmdMigrate(args.slice(1));
751
1379
  else if (cmd === 'import') cmdImport(args.slice(1));
1380
+ else if (cmd === 'filter') cmdFilter(args.slice(1));
1381
+ else if (cmd === 'target') cmdTarget(args.slice(1));
1382
+ else if (cmd === 'source') cmdSourceClassify(args.slice(1));
1383
+ else if (cmd === 'distill') await cmdDistill(args.slice(1));
1384
+ else if (cmd === 'candidate') cmdCandidate(args.slice(1));
1385
+ else if (cmd === 'interview') await cmdInterview(args.slice(1));
1386
+ else if (cmd === 'feynman') await cmdFeynman(args.slice(1));
1387
+ else if (cmd === 'test') await cmdTest(args.slice(1));
752
1388
  else if (cmd === 'card') cmdCard(args.slice(1));
753
1389
  else if (cmd === 'lock') cmdLock(args.slice(1));
754
1390
  else if (cmd === 'compile') cmdCompile(args.slice(1));
755
1391
  else if (cmd === 'export') cmdExport(args.slice(1));
1392
+ else if (cmd === 'llm') cmdLlm(args.slice(1));
756
1393
  else if (cmd === 'identity') cmdIdentity(args.slice(1));
757
1394
  else if (cmd === 'report') cmdReport(args.slice(1));
1395
+ else if (cmd === 'install') cmdStudioInstall(args.slice(1));
1396
+ else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
758
1397
  else {
759
1398
  usage();
760
1399
  fail(`Unknown command: ${cmd}`);
@@ -762,3 +1401,4 @@ try {
762
1401
  } catch (err) {
763
1402
  fail(err.message || String(err));
764
1403
  }
1404
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official KDNA Studio command-line authoring entry for creating, locking, compiling, and exporting trusted .kdna assets.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -37,6 +37,9 @@
37
37
  "node": ">=18"
38
38
  },
39
39
  "dependencies": {
40
- "@aikdna/kdna-studio-core": "^1.4.0"
40
+ "@aikdna/kdna-studio-core": "^1.5.0"
41
+ },
42
+ "devDependencies": {
43
+ "@aikdna/kdna-core": "^0.7.1"
41
44
  }
42
45
  }
package/tests/cli.test.js CHANGED
@@ -5,6 +5,9 @@ const path = require('node:path');
5
5
  const { spawnSync } = require('node:child_process');
6
6
  const test = require('node:test');
7
7
 
8
+ let kdnaCore = null;
9
+ try { kdnaCore = require('@aikdna/kdna-core'); } catch { /* optional — runtime digest cross-verification */ }
10
+
8
11
  const root = path.resolve(__dirname, '..');
9
12
  const cli = path.join(root, 'bin', 'kdna-studio.js');
10
13
 
@@ -446,3 +449,220 @@ test('export includes creator and lineage in kdna.json', (t) => {
446
449
  assert.ok(exportedManifest.lineage, 'must have lineage');
447
450
  assert.equal(exportedManifest.lineage.type, 'original');
448
451
  });
452
+
453
+ // ── E2E: Blank workflow with runtime content_digest check ──────────
454
+
455
+ test('E2E blank: create → approve → lock → export → runtime digest match', (t) => {
456
+ const tmp = tmpDir();
457
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
458
+
459
+ const projectDir = path.join(tmp, 'project');
460
+ let result = run(['create', projectDir, '--name', '@test/e2e-blank'], { tmp });
461
+ assert.equal(result.status, 0, result.stderr);
462
+
463
+ // Add axiom with full fields
464
+ result = run([
465
+ 'card', 'add', projectDir, 'axiom',
466
+ '--field', 'one_sentence=E2E blank axiom.',
467
+ '--field', 'full_statement=A complete testable explanation for the end-to-end blank workflow test.',
468
+ '--field', 'why=Without this axiom the end-to-end test would not validate blank creation.',
469
+ '--field', 'applies_when=["e2e testing"]',
470
+ '--field', 'does_not_apply_when=["production"]',
471
+ '--field', 'failure_risk=e2e test may incorrectly pass',
472
+ ], { tmp });
473
+ assert.equal(result.status, 0, result.stderr);
474
+ const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
475
+
476
+ // Add a misunderstanding
477
+ result = run([
478
+ 'card', 'add', projectDir, 'misunderstanding',
479
+ '--field', 'wrong=E2E tests are unnecessary.',
480
+ '--field', 'correct=E2E tests catch integration issues not visible in unit tests.',
481
+ '--field', 'key_distinction=E2E tests validate the full pipeline, not individual functions.',
482
+ ], { tmp });
483
+ assert.equal(result.status, 0, result.stderr);
484
+
485
+ // Approve all cards
486
+ let cards = run(['card', 'list', projectDir], { tmp });
487
+ for (const line of cards.stdout.trim().split('\n')) {
488
+ const id = line.split(/\s+/)[0];
489
+ result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Confirmed.'], { tmp });
490
+ assert.equal(result.status, 0, result.stderr);
491
+ }
492
+
493
+ // Lock and export
494
+ result = run(['lock', projectDir], { tmp });
495
+ assert.equal(result.status, 0, result.stderr);
496
+
497
+ const outFile = path.join(tmp, 'blank.kdna');
498
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
499
+ result = run(['export', projectDir, '--out', outFile], { tmp });
500
+ assert.equal(result.status, 0, result.stderr);
501
+
502
+ // ── Verify: source_mode, lineage, digest chain ────────────
503
+ const distDir = path.dirname(outFile);
504
+ const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
505
+ const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
506
+ const provenance = JSON.parse(fs.readFileSync(path.join(distDir, 'provenance-report.json'), 'utf8'));
507
+
508
+ assert.equal(manifest.authoring.source_mode, 'blank');
509
+ assert.equal(manifest.lineage.type, 'original');
510
+ assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
511
+ assert.equal(manifest.content_digest, receipt.content_digest, 'manifest vs receipt digest mismatch');
512
+ assert.equal(manifest.content_digest, provenance.content_digest, 'manifest vs provenance digest mismatch');
513
+ assert.equal(manifest.authoring.compiler, '@aikdna/kdna-studio-core');
514
+ assert.equal(manifest.authoring.human_confirmed, true);
515
+
516
+ // ── Runtime cross-verification: content digest consistency ──
517
+ // Compute content digest from the .kdna ZIP entries and verify it
518
+ // matches the manifest, receipt, and provenance report.
519
+ if (kdnaCore) {
520
+ const reader = kdnaCore.createKdnaAssetReader();
521
+ const asset = reader.openSync(outFile);
522
+ const runtimeResult = reader.verifySync(asset);
523
+ const runtimeDigest = runtimeResult.content_digest;
524
+
525
+ assert.ok(runtimeDigest, 'runtime content_digest must be present');
526
+ assert.match(runtimeDigest, /^sha256:[0-9a-f]{64}$/, 'runtime content_digest must be valid sha256');
527
+
528
+ // Read ZIP-internal manifest (avoid sidecar timing issues)
529
+ const zipManifest = JSON.parse(asset.readEntry('kdna.json').toString());
530
+ assert.equal(zipManifest.content_digest, receipt.content_digest,
531
+ `ZIP manifest digest != receipt digest`);
532
+ assert.equal(zipManifest.content_digest, provenance.content_digest,
533
+ `ZIP manifest digest != provenance digest`);
534
+
535
+ // Verify: runtime computed digest matches ZIP manifest
536
+ assert.equal(zipManifest.content_digest, runtimeDigest,
537
+ `ZIP manifest content_digest (${zipManifest.content_digest.slice(0, 20)}...) != runtime computed (${runtimeDigest.slice(0, 20)}...) — trust chain broken`);
538
+ } else {
539
+ console.log(' (kdna-core not available, skipping runtime assetReader cross-verification)');
540
+ }
541
+ });
542
+
543
+ // ── E2E: Fork workflow ────────────────────────────────────────────
544
+
545
+ test('E2E fork: parent → fork → approve → export → lineage check', (t) => {
546
+ const tmp = tmpDir();
547
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
548
+
549
+ // Step 1: Create and export a parent domain
550
+ const parentDir = path.join(tmp, 'parent');
551
+ let result = run(['create', parentDir, '--name', '@test/parent-domain'], { tmp });
552
+ result = run([
553
+ 'card', 'add', parentDir, 'axiom',
554
+ '--field', 'one_sentence=Parent axiom for forking test.',
555
+ '--field', 'full_statement=A complete testable explanation from the parent domain for fork lineage verification.',
556
+ '--field', 'why=Without this parent axiom the fork test would have no source to verify lineage against.',
557
+ '--field', 'applies_when=["fork testing"]',
558
+ '--field', 'does_not_apply_when=["not forking"]',
559
+ '--field', 'failure_risk=forked content may lose lineage',
560
+ ], { tmp });
561
+ const pCards = run(['card', 'list', parentDir], { tmp });
562
+ for (const line of pCards.stdout.trim().split('\n')) {
563
+ const id = line.split(/\s+/)[0];
564
+ run(['card', 'approve', parentDir, id, '--by', 'tester', '--statement', 'Parent OK.'], { tmp });
565
+ }
566
+ run(['lock', parentDir], { tmp });
567
+ const parentKdna = path.join(tmp, 'parent.kdna');
568
+ fs.mkdirSync(path.dirname(parentKdna), { recursive: true });
569
+ result = run(['export', parentDir, '--out', parentKdna], { tmp });
570
+ assert.equal(result.status, 0, result.stderr);
571
+
572
+ // Step 2: Fork from parent
573
+ const forkDir = path.join(tmp, 'forked');
574
+ result = run(['create', forkDir, '--from-kdna', parentKdna, '--name', '@test/forked-domain'], { tmp });
575
+ assert.equal(result.status, 0, result.stderr);
576
+
577
+ const fProject = JSON.parse(fs.readFileSync(path.join(forkDir, 'studio.project.json'), 'utf8'));
578
+ assert.equal(fProject.source_mode, 'kdna_asset');
579
+ assert.equal(fProject.lineage.type, 'fork');
580
+ assert.equal(fProject.lineage.parent_name, '@test/parent-domain');
581
+
582
+ // Step 3: Approve and export fork
583
+ const fCards = run(['card', 'list', forkDir], { tmp });
584
+ for (const line of fCards.stdout.trim().split('\n')) {
585
+ const id = line.split(/\s+/)[0];
586
+ run(['card', 'approve', forkDir, id, '--by', 'tester', '--statement', 'Fork confirmed.'], { tmp });
587
+ }
588
+ run(['lock', forkDir], { tmp });
589
+ const forkKdna = path.join(tmp, 'forked.kdna');
590
+ fs.mkdirSync(path.dirname(forkKdna), { recursive: true });
591
+ result = run(['export', forkDir, '--out', forkKdna], { tmp });
592
+ assert.equal(result.status, 0, result.stderr);
593
+
594
+ // Step 4: Verify lineage in exported fork
595
+ const fManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'kdna.json'), 'utf8'));
596
+ assert.equal(fManifest.authoring.source_mode, 'kdna_asset');
597
+ assert.equal(fManifest.lineage.type, 'fork');
598
+ assert.equal(fManifest.lineage.parent_name, '@test/parent-domain');
599
+ assert.match(fManifest.content_digest, /^sha256:/);
600
+ });
601
+
602
+ // ── E2E: Migrate workflow ─────────────────────────────────────────
603
+
604
+ test('E2E migrate: source folder → import → approve → export → verify', (t) => {
605
+ const tmp = tmpDir();
606
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
607
+
608
+ // Step 1: Create legacy source folder
609
+ const sourceDir = path.join(tmp, 'legacy');
610
+ fs.mkdirSync(sourceDir, { recursive: true });
611
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify({
612
+ meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'Legacy migrate test', load_condition: 'always' },
613
+ axioms: [{
614
+ id: 'ax_mig', one_sentence: 'Migrated axiom for E2E test.',
615
+ full_statement: 'A complete testable explanation for the migrated legacy axiom in the end-to-end workflow.',
616
+ why: 'Without this migrated axiom the e2e test cannot verify legacy folder migration.',
617
+ applies_when: ['legacy migration testing'], does_not_apply_when: ['not migrating'],
618
+ failure_risk: 'migrated content may not be re-locked',
619
+ }],
620
+ ontology: [],
621
+ stances: [],
622
+ }));
623
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
624
+ meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'legacy patterns', load_condition: 'always' },
625
+ misunderstandings: [{ id: 'ms_mig', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key difference between legacy wrong and correct behavior.' }],
626
+ self_check: ['Is the migrated content properly re-locked?'],
627
+ }));
628
+
629
+ // Step 2: Import via --from-folder
630
+ const projectDir = path.join(tmp, 'imported');
631
+ let result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/e2e-migrate'], { tmp });
632
+ assert.equal(result.status, 0, result.stderr);
633
+ const audit = JSON.parse(result.stdout);
634
+ assert.ok(audit.imported > 0, `No cards imported: ${result.stdout}`);
635
+ assert.equal(audit.source_mode, 'source_folder');
636
+
637
+ // Verify project has source_folder mode
638
+ const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
639
+ assert.equal(project.source_mode, 'source_folder');
640
+ assert.equal(project.lineage.type, 'migrated');
641
+
642
+ // Step 3: Approve and export
643
+ const cards = run(['card', 'list', projectDir], { tmp });
644
+ for (const line of cards.stdout.trim().split('\n')) {
645
+ const id = line.split(/\s+/)[0];
646
+ result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Migrated content confirmed.'], { tmp });
647
+ assert.equal(result.status, 0, result.stderr);
648
+ }
649
+ result = run(['lock', projectDir], { tmp });
650
+ assert.equal(result.status, 0, result.stderr);
651
+
652
+ const outFile = path.join(tmp, 'migrated.kdna');
653
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
654
+ result = run(['export', projectDir, '--out', outFile], { tmp });
655
+ assert.equal(result.status, 0, result.stderr);
656
+
657
+ // Verify exported manifest
658
+ const distDir = path.dirname(outFile);
659
+ const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
660
+ assert.equal(manifest.authoring.source_mode, 'source_folder');
661
+ assert.equal(manifest.lineage.type, 'migrated');
662
+ assert.match(manifest.content_digest, /^sha256:/);
663
+ assert.equal(manifest.authoring.human_confirmed, true);
664
+
665
+ // Digest chain
666
+ const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
667
+ assert.equal(manifest.content_digest, receipt.content_digest);
668
+ });