@aikdna/kdna-studio-cli 0.1.3 → 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.
@@ -11,23 +11,64 @@ const {
11
11
  evidence: evidenceApi,
12
12
  compile: compileApi,
13
13
  quality,
14
+ creator: creatorApi,
15
+ distillation: distillationApi,
14
16
  } = require('@aikdna/kdna-studio-core');
15
17
 
18
+ const llm = require('../src/llm');
19
+ const ai = require('../src/ai');
20
+
16
21
  const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
17
22
 
18
23
  function usage() {
19
24
  console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
20
25
 
21
- Usage:
22
- kdna-studio create <project-dir> [--name <@scope/name|name>]
23
- kdna-studio import <project> <source-file>
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
+
30
+ Identity:
31
+ kdna-studio identity init [--name <display-name>]
32
+ kdna-studio identity show
33
+
34
+ Create (three entry paths):
35
+ kdna-studio create <project-dir> --name <@scope/name> # blank
36
+ kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
37
+ kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
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
+
42
+ Authoring:
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
24
46
  kdna-studio card list <project>
25
47
  kdna-studio card add <project> <type> --field key=value [--field key=value]
26
- kdna-studio card approve <project> <card-id> --by <id> --statement <text>
48
+ kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
27
49
  kdna-studio lock <project>
28
50
  kdna-studio compile <project> --out <dir>
29
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
30
69
  kdna-studio report <project>
70
+ kdna-studio install <@scope/name|file.kdna> [--trusted]
71
+ kdna-studio update <@scope/name>
31
72
 
32
73
  Project may be a directory containing studio.project.json or a project JSON file.`);
33
74
  }
@@ -110,29 +151,444 @@ function parseFields(args) {
110
151
 
111
152
  function cmdCreate(args) {
112
153
  const dir = args[0];
113
- if (!dir) fail('Usage: kdna-studio create <project-dir> [--name <name>]');
154
+ if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--from-kdna <file> | --from-folder <dir>]');
114
155
  const abs = path.resolve(dir);
115
156
  if (fs.existsSync(abs)) fail(`Directory already exists: ${abs}`);
157
+
116
158
  const name = option(args, '--name', path.basename(abs));
159
+ const fromKdna = option(args, '--from-kdna');
160
+ const fromFolder = option(args, '--from-folder');
161
+
162
+ let sourceMode = 'blank';
163
+ let lineage = null;
164
+ let creatorIdentity = null;
165
+
166
+ // Load creator identity if available
167
+ try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* no identity yet */ }
168
+
169
+ if (fromKdna) {
170
+ sourceMode = 'kdna_asset';
171
+ const kdnaData = importFromKdna(fromKdna);
172
+ lineage = kdnaData.lineage;
173
+ if (kdnaData.cards) {
174
+ const project = projectApi.createProject(name, 'domain', {
175
+ author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
176
+ sourceMode,
177
+ creatorIdentity,
178
+ lineage,
179
+ });
180
+ project.cards = kdnaData.cards;
181
+ if (project.stages?.judgment_cards) {
182
+ project.stages.judgment_cards.total = kdnaData.cards.length;
183
+ project.stages.judgment_cards.status = 'in_progress';
184
+ }
185
+ fs.mkdirSync(abs, { recursive: true });
186
+ writeProject(path.join(abs, 'studio.project.json'), project);
187
+ console.log(`Created Studio project (source_mode: ${sourceMode}, imported: ${kdnaData.cards.length} cards): ${abs}`);
188
+ return;
189
+ }
190
+ } else if (fromFolder) {
191
+ sourceMode = 'source_folder';
192
+ importFromFolder(fromFolder, abs, name, creatorIdentity);
193
+ return;
194
+ }
195
+
117
196
  const project = projectApi.createProject(name, 'domain', {
118
197
  author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
198
+ sourceMode,
199
+ creatorIdentity,
200
+ lineage,
201
+ sourcePath: fromFolder ? path.resolve(fromFolder) : null,
119
202
  });
203
+
204
+ if (fromKdna || fromFolder) {
205
+ fs.mkdirSync(abs, { recursive: true });
206
+ writeProject(path.join(abs, 'studio.project.json'), project);
207
+ if (fromFolder) {
208
+ // Cards were already added by importFromFolder
209
+ }
210
+ console.log(`Created Studio project (source_mode: ${sourceMode}): ${abs}`);
211
+ return;
212
+ }
213
+
120
214
  fs.mkdirSync(abs, { recursive: true });
121
215
  writeProject(path.join(abs, 'studio.project.json'), project);
122
216
  console.log(`Created Studio project: ${abs}`);
123
217
  }
124
218
 
219
+ /**
220
+ * Import cards from an existing .kdna asset into a new Studio project.
221
+ * Cards are imported as draft — they do NOT inherit trust from the source.
222
+ */
223
+ function importFromKdna(kdnaPath) {
224
+ const absKdna = path.resolve(kdnaPath);
225
+ if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
226
+ if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
227
+
228
+ const zipBuf = fs.readFileSync(absKdna);
229
+ const entries = readZipEntries(zipBuf);
230
+ if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
231
+
232
+ const manifest = JSON.parse(entries.get('kdna.json').toString());
233
+ const lineage = {
234
+ type: 'fork',
235
+ parent_name: manifest.name || null,
236
+ parent_asset_uid: manifest.asset_uid || null,
237
+ parent_version: manifest.version || null,
238
+ parent_asset_digest: manifest.content_digest || null,
239
+ };
240
+
241
+ // Extract cards from KDNA files
242
+ const importedCards = [];
243
+ if (entries.has('KDNA_Core.json')) {
244
+ const core = JSON.parse(entries.get('KDNA_Core.json').toString());
245
+ for (const ax of (core.axioms || [])) {
246
+ const fields = {};
247
+ for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
248
+ if (k in ax) fields[k] = ax[k];
249
+ }
250
+ importedCards.push(cardApi.createCard('axiom', fields));
251
+ }
252
+ for (const ont of (core.ontology || [])) {
253
+ importedCards.push(cardApi.createCard('ontology', {
254
+ one_sentence: ont.one_sentence || ont.essence || '',
255
+ essence: ont.essence || '', boundary: ont.boundary || '',
256
+ trigger_signal: ont.trigger_signal || '',
257
+ }));
258
+ }
259
+ for (const b of (core.boundaries || [])) {
260
+ importedCards.push(cardApi.createCard('boundary', {
261
+ scope: b.scope || '', out_of_scope: b.out_of_scope || '',
262
+ acceptable_exceptions: b.acceptable_exceptions || [],
263
+ }));
264
+ }
265
+ }
266
+ if (entries.has('KDNA_Patterns.json')) {
267
+ const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
268
+ for (const ms of (pat.misunderstandings || [])) {
269
+ importedCards.push(cardApi.createCard('misunderstanding', {
270
+ wrong: ms.wrong || '', correct: ms.correct || '',
271
+ key_distinction: ms.key_distinction || '', why: ms.why || '',
272
+ }));
273
+ }
274
+ for (const sc of (pat.self_check || [])) {
275
+ const q = typeof sc === 'string' ? sc : sc.question || '';
276
+ importedCards.push(cardApi.createCard('self_check', { question: q }));
277
+ }
278
+ }
279
+
280
+ return { lineage, cards: importedCards };
281
+ }
282
+
283
+ /**
284
+ * Import cards from a legacy source folder into a new Studio project.
285
+ * Reads KDNA_*.json files and converts entries to draft judgment cards.
286
+ * Outputs a schema audit report.
287
+ */
288
+ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
289
+ const absSource = path.resolve(sourceDir);
290
+ if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
291
+ if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
292
+
293
+ const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
294
+ const importedCards = [];
295
+ const manifestData = {};
296
+
297
+ function loadJson(filename) {
298
+ const p = path.join(absSource, filename);
299
+ if (!fs.existsSync(p)) return null;
300
+ audit.filesFound.push(filename);
301
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
302
+ catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
303
+ }
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
+
316
+ const core = loadJson('KDNA_Core.json');
317
+ const patterns = loadJson('KDNA_Patterns.json');
318
+ const scenarios = loadJson('KDNA_Scenarios.json');
319
+ const cases = loadJson('KDNA_Cases.json');
320
+ const reasoning = loadJson('KDNA_Reasoning.json');
321
+ const evolution = loadJson('KDNA_Evolution.json');
322
+
323
+ if (core) {
324
+ for (const ax of (core.axioms || [])) {
325
+ const fields = {};
326
+ for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
327
+ if (key in ax) fields[key] = ax[key];
328
+ else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
329
+ }
330
+ importedCards.push(cardApi.createCard('axiom', fields));
331
+ }
332
+ for (const ont of (core.ontology || [])) {
333
+ importedCards.push(cardApi.createCard('ontology', {
334
+ one_sentence: ont.one_sentence || ont.essence || '',
335
+ essence: ont.essence || '', boundary: ont.boundary || '',
336
+ trigger_signal: ont.trigger_signal || '',
337
+ }));
338
+ }
339
+ for (const b of (core.boundaries || [])) {
340
+ importedCards.push(cardApi.createCard('boundary', {
341
+ scope: b.scope || '', out_of_scope: b.out_of_scope || '',
342
+ acceptable_exceptions: b.acceptable_exceptions || [],
343
+ }));
344
+ }
345
+ for (const r of (core.risks || core.risk_model || [])) {
346
+ importedCards.push(cardApi.createCard('risk', r.fields || r));
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
+ }
364
+ }
365
+
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
+ }
380
+ for (const ms of (patterns.misunderstandings || [])) {
381
+ importedCards.push(cardApi.createCard('misunderstanding', {
382
+ wrong: ms.wrong || '', correct: ms.correct || '',
383
+ key_distinction: ms.key_distinction || '', why: ms.why || '',
384
+ }));
385
+ }
386
+ for (const sc of (patterns.self_check || [])) {
387
+ const q = typeof sc === 'string' ? sc : sc.question || '';
388
+ importedCards.push(cardApi.createCard('self_check', { question: q }));
389
+ }
390
+ }
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
+
432
+ audit.cardsImported = importedCards.length;
433
+
434
+ const project = projectApi.createProject(projectName, 'domain', {
435
+ sourceMode: 'source_folder',
436
+ sourcePath: absSource,
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 },
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
+
456
+ project.cards = importedCards;
457
+ if (project.stages?.judgment_cards) {
458
+ project.stages.judgment_cards.total = importedCards.length;
459
+ project.stages.judgment_cards.status = 'in_progress';
460
+ }
461
+
462
+ fs.mkdirSync(projectDir, { recursive: true });
463
+ writeProject(path.join(projectDir, 'studio.project.json'), project);
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 };
466
+ }
467
+
468
+ /**
469
+ * Minimal ZIP central directory parser — reads entries into a Map.
470
+ */
471
+ function readZipEntries(buf) {
472
+ const entries = new Map();
473
+ // Find end-of-central-directory record
474
+ let eocdOffset = buf.length - 22;
475
+ while (eocdOffset >= 0) {
476
+ if (buf.readUInt32LE(eocdOffset) === 0x06054b50) break;
477
+ eocdOffset--;
478
+ }
479
+ if (eocdOffset < 0) throw new Error('Not a valid ZIP file');
480
+
481
+ const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
482
+ let offset = centralDirOffset;
483
+
484
+ while (offset < eocdOffset) {
485
+ const sig = buf.readUInt32LE(offset);
486
+ if (sig !== 0x02014b50) break;
487
+ const nameLen = buf.readUInt16LE(offset + 28);
488
+ const extraLen = buf.readUInt16LE(offset + 30);
489
+ const commentLen = buf.readUInt16LE(offset + 32);
490
+ const compMethod = buf.readUInt16LE(offset + 10);
491
+ const compSize = buf.readUInt32LE(offset + 20);
492
+ const uncompSize = buf.readUInt32LE(offset + 24);
493
+ const localOffset = buf.readUInt32LE(offset + 42);
494
+ const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
495
+
496
+ // Read local file header to get data
497
+ const localNameLen = buf.readUInt16LE(localOffset + 26);
498
+ const localExtraLen = buf.readUInt16LE(localOffset + 28);
499
+ const dataStart = localOffset + 30 + localNameLen + localExtraLen;
500
+
501
+ if (compMethod === 0) {
502
+ entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
503
+ } else if (compMethod === 8) {
504
+ entries.set(name, zlib.inflateRawSync(buf.subarray(dataStart, dataStart + compSize)));
505
+ }
506
+
507
+ offset += 46 + nameLen + extraLen + commentLen;
508
+ }
509
+
510
+ return entries;
511
+ }
512
+
513
+ const aiFilter = require('../src/ai/filter');
514
+
125
515
  function cmdImport(args) {
126
516
  const [projectInput, source] = args;
127
- 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>');
128
518
  const { projectPath, project } = readProject(projectInput);
519
+
129
520
  const sourcePath = path.resolve(source);
130
- if (!fs.existsSync(sourcePath)) fail(`Source file not found: ${sourcePath}`);
131
- const content = fs.readFileSync(sourcePath, 'utf8');
132
- const evidence = evidenceApi.createEvidenceEntry('text', path.basename(sourcePath), content, sourcePath);
133
- 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
+
134
562
  writeProject(projectPath, project);
135
- 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
+ }
136
592
  }
137
593
 
138
594
  function cmdCard(args) {
@@ -162,28 +618,66 @@ function cmdCard(args) {
162
618
  if (sub === 'approve') {
163
619
  const projectInput = args[1];
164
620
  const cardId = args[2];
165
- if (!projectInput || !cardId) {
166
- fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text>');
621
+ const approveAll = args.includes('--all');
622
+ if (!projectInput || (!cardId && !approveAll)) {
623
+ fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
167
624
  }
168
625
  const by = option(args, '--by');
169
626
  const statement = option(args, '--statement');
170
627
  if (!by || !statement) fail('card approve requires --by and --statement');
171
628
  const { projectPath, project } = readProject(projectInput);
172
- const idx = (project.cards || []).findIndex((c) => c.id === cardId);
173
- if (idx < 0) fail(`Card not found: ${cardId}`);
174
- let card = project.cards[idx];
175
- if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
176
- project.cards[idx] = cardApi.lockCard(card, {
177
- by,
178
- statement,
179
- checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
180
- });
629
+
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)];
654
+ };
655
+
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++;
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}`);
675
+ }
181
676
  if (project.stages?.judgment_cards) {
182
677
  project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
183
678
  project.stages.judgment_cards.total = project.cards.length;
184
679
  }
185
680
  writeProject(projectPath, project);
186
- console.log(`Approved and Human Locked: ${cardId}`);
187
681
  return;
188
682
  }
189
683
  fail('Usage: kdna-studio card <list|add|approve> ...');
@@ -200,6 +694,83 @@ function cmdLock(args) {
200
694
  console.log(`Human Lock Gate passed: ${gate.lockedJudgmentCards} locked judgment cards`);
201
695
  }
202
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
+
203
774
  function compileProject(projectInput) {
204
775
  const { project } = readProject(projectInput);
205
776
  const gate = projectApi.checkHumanLockGate(project);
@@ -320,6 +891,7 @@ function manifestForSigning(manifest) {
320
891
  delete copy.signature;
321
892
  delete copy.asset_digest;
322
893
  delete copy.container_sha256;
894
+ delete copy.content_digest;
323
895
  return copy;
324
896
  }
325
897
 
@@ -346,7 +918,7 @@ function publicKeyFingerprint(publicKeyPem) {
346
918
  return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
347
919
  }
348
920
 
349
- function applySignature(files) {
921
+ function applySignature(files, passphrase = null) {
350
922
  const paths = identityPaths();
351
923
  if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
352
924
  fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
@@ -359,7 +931,13 @@ function applySignature(files) {
359
931
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
360
932
 
361
933
  const payload = canonicalPayload(files);
362
- 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
+ }
363
941
  manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
364
942
  files['kdna.json'] = JSON.stringify(manifest, null, 2);
365
943
  }
@@ -372,21 +950,48 @@ function cmdExport(args) {
372
950
  const files = { ...result.files };
373
951
  files['README.md'] = compileApi.generateReadme(project);
374
952
  files.LICENSE = project.license?.type || 'UNSPECIFIED';
375
- if (args.includes('--sign')) applySignature(files);
953
+ files.mimetype = 'application/vnd.aikdna.kdna+zip';
954
+
955
+ // Recompute content_digest after README.md / LICENSE / mimetype are added
956
+ // so that manifest, receipt, and provenance report all agree with the final ZIP contents.
957
+ const finalDigest = compileApi.computeContentDigest(files);
958
+ result.identity.content_digest = finalDigest;
376
959
 
377
- const entries = [['mimetype', 'application/vnd.aikdna.kdna+zip']];
378
- for (const name of Object.keys(files).sort()) entries.push([name, files[name]]);
960
+ // Update kdna.json manifest with final digest
961
+ const manifest = JSON.parse(files['kdna.json']);
962
+ manifest.content_digest = finalDigest;
963
+ if (manifest.authoring) manifest.authoring.content_digest = finalDigest;
964
+ files['kdna.json'] = JSON.stringify(manifest, null, 2);
965
+
966
+ // Update build-receipt.json
967
+ let receiptData = JSON.parse(files['build-receipt.json']);
968
+ receiptData.content_digest = finalDigest;
969
+ files['build-receipt.json'] = JSON.stringify(receiptData, null, 2);
970
+
971
+ // Update provenance report
972
+ const provenance = JSON.parse(files['reports/provenance-report.json']);
973
+ provenance.content_digest = finalDigest;
974
+ provenance.content_fingerprint = finalDigest;
975
+ files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
976
+
977
+ if (args.includes('--sign')) applySignature(files, option(args, '--passphrase'));
978
+
979
+ const entries = [['mimetype', files.mimetype]];
980
+ for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
981
+ entries.push([name, files[name]]);
982
+ }
379
983
  const zip = buildZip(entries);
380
984
  const outPath = path.resolve(out);
381
985
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
382
986
  fs.writeFileSync(outPath, zip);
383
987
 
384
988
  const assetDigest = `sha256:${crypto.createHash('sha256').update(zip).digest('hex')}`;
385
- const receipt = JSON.parse(files['build-receipt.json']);
386
- receipt.asset_path = outPath;
387
- receipt.asset_digest = assetDigest;
388
- receipt.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
389
- fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receipt, null, 2));
989
+ receiptData = JSON.parse(files['build-receipt.json']);
990
+ receiptData.asset_path = outPath;
991
+ receiptData.asset_digest = assetDigest;
992
+ receiptData.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
993
+ fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receiptData, null, 2));
994
+ fs.writeFileSync(path.join(path.dirname(outPath), 'kdna.json'), files['kdna.json']);
390
995
  fs.writeFileSync(path.join(path.dirname(outPath), 'provenance-report.json'), files['reports/provenance-report.json']);
391
996
  fs.writeFileSync(path.join(path.dirname(outPath), 'quality-gate-report.json'), files['reports/quality-gate-report.json']);
392
997
  fs.writeFileSync(path.join(path.dirname(outPath), 'human-lock-report.json'), files['reports/human-lock-report.json']);
@@ -396,6 +1001,99 @@ function cmdExport(args) {
396
1001
  console.log(`Build ID: ${result.identity.build_id}`);
397
1002
  }
398
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
+
1061
+ function cmdIdentity(args) {
1062
+ const sub = args[0];
1063
+ if (sub === 'init') {
1064
+ const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
1065
+ const passphrase = option(args, '--passphrase');
1066
+ try {
1067
+ const identity = creatorApi.initIdentity(name, null, passphrase);
1068
+ console.log(`Creator identity initialized:`);
1069
+ console.log(` creator_id: ${identity.creator_id}`);
1070
+ console.log(` display_name: ${identity.display_name}`);
1071
+ console.log(` identity_dir: ${identity.identity_dir}`);
1072
+ console.log(` public_key saved: ${identity.public_key_path}`);
1073
+ console.log(` encrypted: ${identity.encrypted}`);
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
+ }
1078
+ } catch (err) {
1079
+ fail(`Identity already exists. Use 'kdna-studio identity show' to view. ${err.message}`);
1080
+ }
1081
+ return;
1082
+ }
1083
+ if (sub === 'show') {
1084
+ const identity = creatorApi.loadIdentity();
1085
+ if (!identity) fail('No identity found. Run: kdna-studio identity init --name "Your Name"');
1086
+ console.log(JSON.stringify({
1087
+ creator_id: identity.creator_id,
1088
+ display_name: identity.display_name,
1089
+ verified: identity.verified || false,
1090
+ created_at: identity.created_at,
1091
+ }, null, 2));
1092
+ return;
1093
+ }
1094
+ fail('Usage: kdna-studio identity <init|show>');
1095
+ }
1096
+
399
1097
  function cmdReport(args) {
400
1098
  const { project } = readProject(args[0]);
401
1099
  const readiness = quality.computeReadiness(project);
@@ -404,6 +1102,269 @@ function cmdReport(args) {
404
1102
  process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
405
1103
  }
406
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
+
407
1368
  const args = process.argv.slice(2);
408
1369
  const cmd = args[0];
409
1370
  if (!cmd || cmd === '--help' || cmd === '-h') {
@@ -411,14 +1372,28 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
411
1372
  process.exit(EXIT.OK);
412
1373
  }
413
1374
 
1375
+ (async () => {
414
1376
  try {
415
1377
  if (cmd === 'create') cmdCreate(args.slice(1));
1378
+ else if (cmd === 'migrate') cmdMigrate(args.slice(1));
416
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));
417
1388
  else if (cmd === 'card') cmdCard(args.slice(1));
418
1389
  else if (cmd === 'lock') cmdLock(args.slice(1));
419
1390
  else if (cmd === 'compile') cmdCompile(args.slice(1));
420
1391
  else if (cmd === 'export') cmdExport(args.slice(1));
1392
+ else if (cmd === 'llm') cmdLlm(args.slice(1));
1393
+ else if (cmd === 'identity') cmdIdentity(args.slice(1));
421
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));
422
1397
  else {
423
1398
  usage();
424
1399
  fail(`Unknown command: ${cmd}`);
@@ -426,3 +1401,4 @@ try {
426
1401
  } catch (err) {
427
1402
  fail(err.message || String(err));
428
1403
  }
1404
+ })();