@aikdna/kdna-studio-cli 0.2.0 → 0.3.1
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 +43 -1
- package/bin/kdna-studio.js +680 -40
- package/package.json +6 -2
- package/src/ai/distill.js +77 -0
- package/src/ai/feynman.js +57 -0
- package/src/ai/filter.js +70 -0
- package/src/ai/index.js +6 -0
- package/src/ai/interview.js +144 -0
- package/src/ai/testlab.js +73 -0
- package/src/llm/config.js +72 -0
- package/src/llm/index.js +49 -0
- package/src/llm/providers/anthropic.js +40 -0
- package/src/llm/providers/openai.js +41 -0
- package/tests/cli.test.js +220 -0
package/bin/kdna-studio.js
CHANGED
|
@@ -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
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
436
|
-
by
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
+
})();
|