@aikdna/kdna-studio-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/bin/kdna-studio.js +680 -40
- package/package.json +5 -2
- package/tests/cli.test.js +220 -0
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @aikdna/kdna-studio-cli
|
|
2
2
|
|
|
3
|
-
Official Studio command-line
|
|
3
|
+
Official Studio command-line entry for KDNA judgment asset creation — turns your notes, documents, works, and feedback into loadable .kdna domains.
|
|
4
|
+
|
|
5
|
+
Two authoring paths: interview-first (articulate your judgment directly) and distillation-first (provide content, find the patterns, confirm what's really you).
|
|
6
|
+
|
|
7
|
+
Distillation-first authoring is domain-first: declare the target domain, owner scope, granularity, task scope, include areas, exclude areas, and load condition before extracting candidates. A single `.kdna` should stay scoped; complex work should compose multiple domain assets through a KDNA Cluster.
|
|
4
8
|
|
|
5
9
|
This package provides the `kdna-studio` command. It creates Studio projects,
|
|
6
10
|
imports evidence, manages judgment cards, checks Human Lock, compiles locked
|
|
@@ -25,6 +29,18 @@ npm install -g @aikdna/kdna-studio-cli
|
|
|
25
29
|
```bash
|
|
26
30
|
kdna-studio create my_domain --name @yourscope/my_domain
|
|
27
31
|
kdna-studio import my_domain ./notes.md
|
|
32
|
+
kdna-studio target declare my_domain \
|
|
33
|
+
--category expression_writing \
|
|
34
|
+
--scope personal \
|
|
35
|
+
--granularity core_principles \
|
|
36
|
+
--task "longform article review" \
|
|
37
|
+
--include "argument structure,tone,revision" \
|
|
38
|
+
--exclude "life habits,food preference"
|
|
39
|
+
kdna-studio source classify my_domain
|
|
40
|
+
kdna-studio distill my_domain --candidates candidates.json
|
|
41
|
+
kdna-studio candidate list my_domain
|
|
42
|
+
kdna-studio candidate accept my_domain <candidate-id>
|
|
43
|
+
kdna-studio candidate promote my_domain
|
|
28
44
|
kdna-studio card add my_domain axiom \
|
|
29
45
|
--field one_sentence="Prefer specific evidence over broad claims" \
|
|
30
46
|
--field applies_when='["reviewing content"]' \
|
|
@@ -35,6 +51,8 @@ kdna-studio lock my_domain
|
|
|
35
51
|
kdna-studio export my_domain --out dist/my_domain.kdna --sign
|
|
36
52
|
```
|
|
37
53
|
|
|
54
|
+
Candidate promotion is scope-gated: only candidates with `status == accepted` and `scope_fit == true` are promoted to cards by default. Use `kdna-studio candidate override <project> <candidate-id>` only when a human intentionally overrides the scope gate.
|
|
55
|
+
|
|
38
56
|
After export, use the runtime CLI:
|
|
39
57
|
|
|
40
58
|
```bash
|
|
@@ -44,3 +62,27 @@ kdna publish dist/my_domain.kdna
|
|
|
44
62
|
|
|
45
63
|
`kdna-studio` is the CLI entry for trusted KDNA creation. `kdna` is the runtime
|
|
46
64
|
control plane for existing `.kdna` assets.
|
|
65
|
+
|
|
66
|
+
## Identity
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
kdna-studio identity init --name "Your Name"
|
|
70
|
+
kdna-studio identity show
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Import from existing KDNA or legacy folders
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Fork an existing .kdna asset (cards imported as draft — re-lock required)
|
|
77
|
+
kdna-studio create forked --from-kdna ./parent.kdna --name @scope/forked
|
|
78
|
+
|
|
79
|
+
# Migrate a legacy JSON source folder
|
|
80
|
+
kdna-studio create migrated --from-folder ./old-domain-json --name @scope/migrated
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- [KDNA Protocol](https://github.com/aikdna/kdna) — specification and schemas
|
|
86
|
+
- [kdna-cli](https://github.com/aikdna/kdna-cli) — runtime CLI for verify, install, load, publish
|
|
87
|
+
- [kdna-studio-core](https://github.com/aikdna/kdna-studio-core) — authoring SDK used by this CLI
|
|
88
|
+
- [kdna-studio-swift](https://github.com/aikdna/kdna-studio-swift) — native Swift counterpart
|
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
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Official KDNA Studio command-line authoring entry for creating, locking, compiling, and exporting trusted .kdna assets.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"node": ">=18"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aikdna/kdna-studio-core": "^1.
|
|
40
|
+
"@aikdna/kdna-studio-core": "^1.5.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@aikdna/kdna-core": "^0.7.1"
|
|
41
44
|
}
|
|
42
45
|
}
|
package/tests/cli.test.js
CHANGED
|
@@ -5,6 +5,9 @@ const path = require('node:path');
|
|
|
5
5
|
const { spawnSync } = require('node:child_process');
|
|
6
6
|
const test = require('node:test');
|
|
7
7
|
|
|
8
|
+
let kdnaCore = null;
|
|
9
|
+
try { kdnaCore = require('@aikdna/kdna-core'); } catch { /* optional — runtime digest cross-verification */ }
|
|
10
|
+
|
|
8
11
|
const root = path.resolve(__dirname, '..');
|
|
9
12
|
const cli = path.join(root, 'bin', 'kdna-studio.js');
|
|
10
13
|
|
|
@@ -446,3 +449,220 @@ test('export includes creator and lineage in kdna.json', (t) => {
|
|
|
446
449
|
assert.ok(exportedManifest.lineage, 'must have lineage');
|
|
447
450
|
assert.equal(exportedManifest.lineage.type, 'original');
|
|
448
451
|
});
|
|
452
|
+
|
|
453
|
+
// ── E2E: Blank workflow with runtime content_digest check ──────────
|
|
454
|
+
|
|
455
|
+
test('E2E blank: create → approve → lock → export → runtime digest match', (t) => {
|
|
456
|
+
const tmp = tmpDir();
|
|
457
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
458
|
+
|
|
459
|
+
const projectDir = path.join(tmp, 'project');
|
|
460
|
+
let result = run(['create', projectDir, '--name', '@test/e2e-blank'], { tmp });
|
|
461
|
+
assert.equal(result.status, 0, result.stderr);
|
|
462
|
+
|
|
463
|
+
// Add axiom with full fields
|
|
464
|
+
result = run([
|
|
465
|
+
'card', 'add', projectDir, 'axiom',
|
|
466
|
+
'--field', 'one_sentence=E2E blank axiom.',
|
|
467
|
+
'--field', 'full_statement=A complete testable explanation for the end-to-end blank workflow test.',
|
|
468
|
+
'--field', 'why=Without this axiom the end-to-end test would not validate blank creation.',
|
|
469
|
+
'--field', 'applies_when=["e2e testing"]',
|
|
470
|
+
'--field', 'does_not_apply_when=["production"]',
|
|
471
|
+
'--field', 'failure_risk=e2e test may incorrectly pass',
|
|
472
|
+
], { tmp });
|
|
473
|
+
assert.equal(result.status, 0, result.stderr);
|
|
474
|
+
const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
|
|
475
|
+
|
|
476
|
+
// Add a misunderstanding
|
|
477
|
+
result = run([
|
|
478
|
+
'card', 'add', projectDir, 'misunderstanding',
|
|
479
|
+
'--field', 'wrong=E2E tests are unnecessary.',
|
|
480
|
+
'--field', 'correct=E2E tests catch integration issues not visible in unit tests.',
|
|
481
|
+
'--field', 'key_distinction=E2E tests validate the full pipeline, not individual functions.',
|
|
482
|
+
], { tmp });
|
|
483
|
+
assert.equal(result.status, 0, result.stderr);
|
|
484
|
+
|
|
485
|
+
// Approve all cards
|
|
486
|
+
let cards = run(['card', 'list', projectDir], { tmp });
|
|
487
|
+
for (const line of cards.stdout.trim().split('\n')) {
|
|
488
|
+
const id = line.split(/\s+/)[0];
|
|
489
|
+
result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Confirmed.'], { tmp });
|
|
490
|
+
assert.equal(result.status, 0, result.stderr);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Lock and export
|
|
494
|
+
result = run(['lock', projectDir], { tmp });
|
|
495
|
+
assert.equal(result.status, 0, result.stderr);
|
|
496
|
+
|
|
497
|
+
const outFile = path.join(tmp, 'blank.kdna');
|
|
498
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
499
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
500
|
+
assert.equal(result.status, 0, result.stderr);
|
|
501
|
+
|
|
502
|
+
// ── Verify: source_mode, lineage, digest chain ────────────
|
|
503
|
+
const distDir = path.dirname(outFile);
|
|
504
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
505
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
|
|
506
|
+
const provenance = JSON.parse(fs.readFileSync(path.join(distDir, 'provenance-report.json'), 'utf8'));
|
|
507
|
+
|
|
508
|
+
assert.equal(manifest.authoring.source_mode, 'blank');
|
|
509
|
+
assert.equal(manifest.lineage.type, 'original');
|
|
510
|
+
assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
|
|
511
|
+
assert.equal(manifest.content_digest, receipt.content_digest, 'manifest vs receipt digest mismatch');
|
|
512
|
+
assert.equal(manifest.content_digest, provenance.content_digest, 'manifest vs provenance digest mismatch');
|
|
513
|
+
assert.equal(manifest.authoring.compiler, '@aikdna/kdna-studio-core');
|
|
514
|
+
assert.equal(manifest.authoring.human_confirmed, true);
|
|
515
|
+
|
|
516
|
+
// ── Runtime cross-verification: content digest consistency ──
|
|
517
|
+
// Compute content digest from the .kdna ZIP entries and verify it
|
|
518
|
+
// matches the manifest, receipt, and provenance report.
|
|
519
|
+
if (kdnaCore) {
|
|
520
|
+
const reader = kdnaCore.createKdnaAssetReader();
|
|
521
|
+
const asset = reader.openSync(outFile);
|
|
522
|
+
const runtimeResult = reader.verifySync(asset);
|
|
523
|
+
const runtimeDigest = runtimeResult.content_digest;
|
|
524
|
+
|
|
525
|
+
assert.ok(runtimeDigest, 'runtime content_digest must be present');
|
|
526
|
+
assert.match(runtimeDigest, /^sha256:[0-9a-f]{64}$/, 'runtime content_digest must be valid sha256');
|
|
527
|
+
|
|
528
|
+
// Read ZIP-internal manifest (avoid sidecar timing issues)
|
|
529
|
+
const zipManifest = JSON.parse(asset.readEntry('kdna.json').toString());
|
|
530
|
+
assert.equal(zipManifest.content_digest, receipt.content_digest,
|
|
531
|
+
`ZIP manifest digest != receipt digest`);
|
|
532
|
+
assert.equal(zipManifest.content_digest, provenance.content_digest,
|
|
533
|
+
`ZIP manifest digest != provenance digest`);
|
|
534
|
+
|
|
535
|
+
// Verify: runtime computed digest matches ZIP manifest
|
|
536
|
+
assert.equal(zipManifest.content_digest, runtimeDigest,
|
|
537
|
+
`ZIP manifest content_digest (${zipManifest.content_digest.slice(0, 20)}...) != runtime computed (${runtimeDigest.slice(0, 20)}...) — trust chain broken`);
|
|
538
|
+
} else {
|
|
539
|
+
console.log(' (kdna-core not available, skipping runtime assetReader cross-verification)');
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ── E2E: Fork workflow ────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
test('E2E fork: parent → fork → approve → export → lineage check', (t) => {
|
|
546
|
+
const tmp = tmpDir();
|
|
547
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
548
|
+
|
|
549
|
+
// Step 1: Create and export a parent domain
|
|
550
|
+
const parentDir = path.join(tmp, 'parent');
|
|
551
|
+
let result = run(['create', parentDir, '--name', '@test/parent-domain'], { tmp });
|
|
552
|
+
result = run([
|
|
553
|
+
'card', 'add', parentDir, 'axiom',
|
|
554
|
+
'--field', 'one_sentence=Parent axiom for forking test.',
|
|
555
|
+
'--field', 'full_statement=A complete testable explanation from the parent domain for fork lineage verification.',
|
|
556
|
+
'--field', 'why=Without this parent axiom the fork test would have no source to verify lineage against.',
|
|
557
|
+
'--field', 'applies_when=["fork testing"]',
|
|
558
|
+
'--field', 'does_not_apply_when=["not forking"]',
|
|
559
|
+
'--field', 'failure_risk=forked content may lose lineage',
|
|
560
|
+
], { tmp });
|
|
561
|
+
const pCards = run(['card', 'list', parentDir], { tmp });
|
|
562
|
+
for (const line of pCards.stdout.trim().split('\n')) {
|
|
563
|
+
const id = line.split(/\s+/)[0];
|
|
564
|
+
run(['card', 'approve', parentDir, id, '--by', 'tester', '--statement', 'Parent OK.'], { tmp });
|
|
565
|
+
}
|
|
566
|
+
run(['lock', parentDir], { tmp });
|
|
567
|
+
const parentKdna = path.join(tmp, 'parent.kdna');
|
|
568
|
+
fs.mkdirSync(path.dirname(parentKdna), { recursive: true });
|
|
569
|
+
result = run(['export', parentDir, '--out', parentKdna], { tmp });
|
|
570
|
+
assert.equal(result.status, 0, result.stderr);
|
|
571
|
+
|
|
572
|
+
// Step 2: Fork from parent
|
|
573
|
+
const forkDir = path.join(tmp, 'forked');
|
|
574
|
+
result = run(['create', forkDir, '--from-kdna', parentKdna, '--name', '@test/forked-domain'], { tmp });
|
|
575
|
+
assert.equal(result.status, 0, result.stderr);
|
|
576
|
+
|
|
577
|
+
const fProject = JSON.parse(fs.readFileSync(path.join(forkDir, 'studio.project.json'), 'utf8'));
|
|
578
|
+
assert.equal(fProject.source_mode, 'kdna_asset');
|
|
579
|
+
assert.equal(fProject.lineage.type, 'fork');
|
|
580
|
+
assert.equal(fProject.lineage.parent_name, '@test/parent-domain');
|
|
581
|
+
|
|
582
|
+
// Step 3: Approve and export fork
|
|
583
|
+
const fCards = run(['card', 'list', forkDir], { tmp });
|
|
584
|
+
for (const line of fCards.stdout.trim().split('\n')) {
|
|
585
|
+
const id = line.split(/\s+/)[0];
|
|
586
|
+
run(['card', 'approve', forkDir, id, '--by', 'tester', '--statement', 'Fork confirmed.'], { tmp });
|
|
587
|
+
}
|
|
588
|
+
run(['lock', forkDir], { tmp });
|
|
589
|
+
const forkKdna = path.join(tmp, 'forked.kdna');
|
|
590
|
+
fs.mkdirSync(path.dirname(forkKdna), { recursive: true });
|
|
591
|
+
result = run(['export', forkDir, '--out', forkKdna], { tmp });
|
|
592
|
+
assert.equal(result.status, 0, result.stderr);
|
|
593
|
+
|
|
594
|
+
// Step 4: Verify lineage in exported fork
|
|
595
|
+
const fManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'kdna.json'), 'utf8'));
|
|
596
|
+
assert.equal(fManifest.authoring.source_mode, 'kdna_asset');
|
|
597
|
+
assert.equal(fManifest.lineage.type, 'fork');
|
|
598
|
+
assert.equal(fManifest.lineage.parent_name, '@test/parent-domain');
|
|
599
|
+
assert.match(fManifest.content_digest, /^sha256:/);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ── E2E: Migrate workflow ─────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
test('E2E migrate: source folder → import → approve → export → verify', (t) => {
|
|
605
|
+
const tmp = tmpDir();
|
|
606
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
607
|
+
|
|
608
|
+
// Step 1: Create legacy source folder
|
|
609
|
+
const sourceDir = path.join(tmp, 'legacy');
|
|
610
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
611
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify({
|
|
612
|
+
meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'Legacy migrate test', load_condition: 'always' },
|
|
613
|
+
axioms: [{
|
|
614
|
+
id: 'ax_mig', one_sentence: 'Migrated axiom for E2E test.',
|
|
615
|
+
full_statement: 'A complete testable explanation for the migrated legacy axiom in the end-to-end workflow.',
|
|
616
|
+
why: 'Without this migrated axiom the e2e test cannot verify legacy folder migration.',
|
|
617
|
+
applies_when: ['legacy migration testing'], does_not_apply_when: ['not migrating'],
|
|
618
|
+
failure_risk: 'migrated content may not be re-locked',
|
|
619
|
+
}],
|
|
620
|
+
ontology: [],
|
|
621
|
+
stances: [],
|
|
622
|
+
}));
|
|
623
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
|
|
624
|
+
meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'legacy patterns', load_condition: 'always' },
|
|
625
|
+
misunderstandings: [{ id: 'ms_mig', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key difference between legacy wrong and correct behavior.' }],
|
|
626
|
+
self_check: ['Is the migrated content properly re-locked?'],
|
|
627
|
+
}));
|
|
628
|
+
|
|
629
|
+
// Step 2: Import via --from-folder
|
|
630
|
+
const projectDir = path.join(tmp, 'imported');
|
|
631
|
+
let result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/e2e-migrate'], { tmp });
|
|
632
|
+
assert.equal(result.status, 0, result.stderr);
|
|
633
|
+
const audit = JSON.parse(result.stdout);
|
|
634
|
+
assert.ok(audit.imported > 0, `No cards imported: ${result.stdout}`);
|
|
635
|
+
assert.equal(audit.source_mode, 'source_folder');
|
|
636
|
+
|
|
637
|
+
// Verify project has source_folder mode
|
|
638
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
639
|
+
assert.equal(project.source_mode, 'source_folder');
|
|
640
|
+
assert.equal(project.lineage.type, 'migrated');
|
|
641
|
+
|
|
642
|
+
// Step 3: Approve and export
|
|
643
|
+
const cards = run(['card', 'list', projectDir], { tmp });
|
|
644
|
+
for (const line of cards.stdout.trim().split('\n')) {
|
|
645
|
+
const id = line.split(/\s+/)[0];
|
|
646
|
+
result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Migrated content confirmed.'], { tmp });
|
|
647
|
+
assert.equal(result.status, 0, result.stderr);
|
|
648
|
+
}
|
|
649
|
+
result = run(['lock', projectDir], { tmp });
|
|
650
|
+
assert.equal(result.status, 0, result.stderr);
|
|
651
|
+
|
|
652
|
+
const outFile = path.join(tmp, 'migrated.kdna');
|
|
653
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
654
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
655
|
+
assert.equal(result.status, 0, result.stderr);
|
|
656
|
+
|
|
657
|
+
// Verify exported manifest
|
|
658
|
+
const distDir = path.dirname(outFile);
|
|
659
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
660
|
+
assert.equal(manifest.authoring.source_mode, 'source_folder');
|
|
661
|
+
assert.equal(manifest.lineage.type, 'migrated');
|
|
662
|
+
assert.match(manifest.content_digest, /^sha256:/);
|
|
663
|
+
assert.equal(manifest.authoring.human_confirmed, true);
|
|
664
|
+
|
|
665
|
+
// Digest chain
|
|
666
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
|
|
667
|
+
assert.equal(manifest.content_digest, receipt.content_digest);
|
|
668
|
+
});
|