@aikdna/kdna-studio-cli 0.5.2 → 0.5.4
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 +32 -7
- package/bin/kdna-studio.js +93 -150
- package/package.json +1 -1
- package/tests/cli.test.js +105 -10
package/README.md
CHANGED
|
@@ -45,12 +45,14 @@ kdna-studio candidate accept my_domain <candidate-id>
|
|
|
45
45
|
kdna-studio candidate promote my_domain
|
|
46
46
|
kdna-studio card add my_domain axiom \
|
|
47
47
|
--field one_sentence="Prefer specific evidence over broad claims" \
|
|
48
|
+
--field full_statement="When reviewing content, prefer specific evidence over broad claims because unsupported generalizations make the judgment impossible to verify or improve." \
|
|
49
|
+
--field why="Broad claims hide the actual reason for a judgment, so reviewers cannot tell whether the conclusion is evidence based, reusable, or merely plausible sounding." \
|
|
48
50
|
--field applies_when='["reviewing content"]' \
|
|
49
51
|
--field does_not_apply_when='["pure formatting"]' \
|
|
50
52
|
--field failure_risk="generic advice"
|
|
51
|
-
kdna-studio card approve my_domain
|
|
53
|
+
kdna-studio card approve my_domain --all --by expert --statement "I confirm this judgment."
|
|
52
54
|
kdna-studio lock my_domain
|
|
53
|
-
kdna-studio export my_domain --out dist/my_domain.kdna
|
|
55
|
+
kdna-studio export my_domain --format v1 --out dist/my_domain.kdna
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
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.
|
|
@@ -58,12 +60,35 @@ Candidate promotion is scope-gated: only candidates with `status == accepted` an
|
|
|
58
60
|
After export, use the runtime CLI:
|
|
59
61
|
|
|
60
62
|
```bash
|
|
61
|
-
kdna
|
|
62
|
-
kdna
|
|
63
|
+
kdna validate dist/my_domain.kdna --runtime
|
|
64
|
+
kdna plan-load dist/my_domain.kdna --json
|
|
65
|
+
kdna load dist/my_domain.kdna --profile=compact --as=prompt
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
`kdna-studio` is the CLI entry for
|
|
66
|
-
control plane for
|
|
68
|
+
`kdna-studio` is the CLI entry for confirmed KDNA authoring. `kdna` is the
|
|
69
|
+
runtime control plane for inspecting, validating, packing, unpacking, and
|
|
70
|
+
loading existing `.kdna` assets.
|
|
71
|
+
|
|
72
|
+
## Runtime Export Contract
|
|
73
|
+
|
|
74
|
+
`kdna-studio export --format v1` is the canonical runtime export path. It uses
|
|
75
|
+
`@aikdna/kdna-studio-core` to compile the Studio project into a KDNA Core v1
|
|
76
|
+
runtime asset and then packs it with `@aikdna/kdna-core`.
|
|
77
|
+
|
|
78
|
+
A v1 runtime export contains only these top-level entries:
|
|
79
|
+
|
|
80
|
+
- `mimetype`
|
|
81
|
+
- `kdna.json`
|
|
82
|
+
- `payload.kdnab`
|
|
83
|
+
- `checksums.json`
|
|
84
|
+
|
|
85
|
+
Authoring/source entries such as `KDNA_Core.json`, `KDNA_Patterns.json`, and
|
|
86
|
+
`source_cards` are not runtime distribution entries. They may exist in Studio
|
|
87
|
+
compile output or legacy imports, but they must not be emitted by the runtime
|
|
88
|
+
export path.
|
|
89
|
+
|
|
90
|
+
Signature, encryption, registry publishing, paid distribution, and private
|
|
91
|
+
assets are future/gated phases, not the current Core v1 baseline.
|
|
67
92
|
|
|
68
93
|
## Identity
|
|
69
94
|
|
|
@@ -85,6 +110,6 @@ kdna-studio create migrated --from-folder ./old-domain-json --name @scope/migrat
|
|
|
85
110
|
## Related
|
|
86
111
|
|
|
87
112
|
- [KDNA Core](https://github.com/aikdna/kdna) — Official format specification
|
|
88
|
-
- [kdna-cli](https://github.com/aikdna/kdna-cli) — runtime CLI for
|
|
113
|
+
- [kdna-cli](https://github.com/aikdna/kdna-cli) — runtime CLI for inspect, validate, pack, unpack, and load
|
|
89
114
|
- [kdna-studio-core](https://github.com/aikdna/kdna-studio-core) — authoring SDK used by this CLI
|
|
90
115
|
- [kdna-studio-swift](https://github.com/aikdna/kdna-studio-swift) — native Swift counterpart
|
package/bin/kdna-studio.js
CHANGED
|
@@ -6,6 +6,16 @@ const crypto = require('crypto');
|
|
|
6
6
|
const zlib = require('zlib');
|
|
7
7
|
const cbor = require('cbor-x');
|
|
8
8
|
|
|
9
|
+
function loadStudioCore() {
|
|
10
|
+
const publishedCore = require('@aikdna/kdna-studio-core');
|
|
11
|
+
if (publishedCore.exportRuntime) return publishedCore;
|
|
12
|
+
try {
|
|
13
|
+
return require('../../kdna-studio-core/src');
|
|
14
|
+
} catch (_) {
|
|
15
|
+
return publishedCore;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
const {
|
|
10
20
|
project: projectApi,
|
|
11
21
|
cards: cardApi,
|
|
@@ -14,8 +24,8 @@ const {
|
|
|
14
24
|
quality,
|
|
15
25
|
creator: creatorApi,
|
|
16
26
|
distillation: distillationApi,
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
exportRuntime,
|
|
28
|
+
} = loadStudioCore();
|
|
19
29
|
|
|
20
30
|
const llm = require('../src/llm');
|
|
21
31
|
const ai = require('../src/ai');
|
|
@@ -39,7 +49,7 @@ Create (three entry paths):
|
|
|
39
49
|
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
40
50
|
|
|
41
51
|
Migrate (dev source → trusted .kdna in one command):
|
|
42
|
-
kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign]
|
|
52
|
+
kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign]
|
|
43
53
|
|
|
44
54
|
Authoring:
|
|
45
55
|
kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
|
|
@@ -50,7 +60,8 @@ Authoring:
|
|
|
50
60
|
kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
|
|
51
61
|
kdna-studio lock <project>
|
|
52
62
|
kdna-studio compile <project> --out <dir>
|
|
53
|
-
kdna-studio export <project> --out <file.kdna>
|
|
63
|
+
kdna-studio export <project> --format v1 --out <file.kdna>
|
|
64
|
+
kdna-studio export <project> --out <file.kdna> [--sign] # legacy v2
|
|
54
65
|
|
|
55
66
|
AI Authoring (requires LLM config: kdna-studio llm config):
|
|
56
67
|
kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
|
|
@@ -151,12 +162,6 @@ function parseFields(args) {
|
|
|
151
162
|
return fields;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
function arrayValue(value) {
|
|
155
|
-
if (Array.isArray(value)) return value;
|
|
156
|
-
if (value === undefined || value === null || value === '') return [];
|
|
157
|
-
return [value];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
165
|
function semverValue(value, fallback = '1.0.0') {
|
|
161
166
|
const raw = String(value || '').trim();
|
|
162
167
|
if (/^[0-9]+\.[0-9]+\.[0-9]+([+-].+)?$/.test(raw)) return raw;
|
|
@@ -207,10 +212,6 @@ function publicManifestMetadata(raw = {}) {
|
|
|
207
212
|
return copy;
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
function cardPayload(card) {
|
|
211
|
-
return { id: card.id, ...(card.fields || {}) };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
215
|
function cleanFields(fields = {}) {
|
|
215
216
|
const cleaned = {};
|
|
216
217
|
for (const [key, value] of Object.entries(fields || {})) {
|
|
@@ -380,116 +381,6 @@ function cardsFromPayload(payload) {
|
|
|
380
381
|
return [];
|
|
381
382
|
}
|
|
382
383
|
|
|
383
|
-
function buildV1Payload(project) {
|
|
384
|
-
const locked = (project.cards || []).filter((c) => c.locked);
|
|
385
|
-
const byType = (type) => locked.filter((c) => c.type === type);
|
|
386
|
-
const source = project.source_manifest || {};
|
|
387
|
-
|
|
388
|
-
const axioms = byType('axiom').map((c) => ({
|
|
389
|
-
id: c.id,
|
|
390
|
-
one_sentence: c.fields?.one_sentence || '',
|
|
391
|
-
full_statement: c.fields?.full_statement || '',
|
|
392
|
-
why: c.fields?.why || '',
|
|
393
|
-
applies_when: arrayValue(c.fields?.applies_when),
|
|
394
|
-
does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
|
|
395
|
-
failure_risk: c.fields?.failure_risk || '',
|
|
396
|
-
status: c.status || null,
|
|
397
|
-
human_lock: c.human_lock || null,
|
|
398
|
-
}));
|
|
399
|
-
|
|
400
|
-
const boundaries = [
|
|
401
|
-
...byType('boundary').map((c) => ({ type: 'explicit_boundary', ...cardPayload(c) })),
|
|
402
|
-
...byType('axiom').map((c) => ({
|
|
403
|
-
id: `${c.id}_applicability`,
|
|
404
|
-
type: 'axiom_applicability',
|
|
405
|
-
axiom_id: c.id,
|
|
406
|
-
one_sentence: c.fields?.one_sentence || '',
|
|
407
|
-
applies_when: arrayValue(c.fields?.applies_when),
|
|
408
|
-
does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
|
|
409
|
-
failure_risk: c.fields?.failure_risk || '',
|
|
410
|
-
})),
|
|
411
|
-
...byType('ontology')
|
|
412
|
-
.filter((c) => c.fields?.boundary || c.fields?.applies_when || c.fields?.does_not_apply_when)
|
|
413
|
-
.map((c) => ({
|
|
414
|
-
id: `${c.id}_boundary`,
|
|
415
|
-
type: 'ontology_boundary',
|
|
416
|
-
concept_id: c.id,
|
|
417
|
-
one_sentence: c.fields?.one_sentence || '',
|
|
418
|
-
boundary: c.fields?.boundary || '',
|
|
419
|
-
applies_when: arrayValue(c.fields?.applies_when),
|
|
420
|
-
does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
|
|
421
|
-
trigger_signal: c.fields?.trigger_signal || '',
|
|
422
|
-
})),
|
|
423
|
-
...byType('stance')
|
|
424
|
-
.filter((c) => c.fields?.applies_when || c.fields?.does_not_apply_when)
|
|
425
|
-
.map((c) => ({
|
|
426
|
-
id: `${c.id}_boundary`,
|
|
427
|
-
type: 'stance_boundary',
|
|
428
|
-
stance_id: c.id,
|
|
429
|
-
stance: c.fields?.stance || c.fields?.statement || c.fields?.one_sentence || '',
|
|
430
|
-
applies_when: arrayValue(c.fields?.applies_when),
|
|
431
|
-
does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
|
|
432
|
-
})),
|
|
433
|
-
];
|
|
434
|
-
|
|
435
|
-
const patterns = [
|
|
436
|
-
...byType('ontology').map((c) => ({ type: 'ontology', ...cardPayload(c) })),
|
|
437
|
-
...byType('misunderstanding').map((c) => ({ type: 'misunderstanding', ...cardPayload(c) })),
|
|
438
|
-
...byType('term').map((c) => ({ type: 'term', ...cardPayload(c) })),
|
|
439
|
-
...byType('banned_term').map((c) => ({ type: 'banned_term', ...cardPayload(c) })),
|
|
440
|
-
...byType('framework').map((c) => ({ type: 'framework', ...cardPayload(c) })),
|
|
441
|
-
...byType('stance').map((c) => ({ type: 'stance', ...cardPayload(c) })),
|
|
442
|
-
];
|
|
443
|
-
|
|
444
|
-
const failureModes = byType('misunderstanding').map((c) => ({
|
|
445
|
-
id: c.id,
|
|
446
|
-
mode: c.fields?.wrong || c.fields?.key_distinction || c.id,
|
|
447
|
-
correct: c.fields?.correct || '',
|
|
448
|
-
key_distinction: c.fields?.key_distinction || '',
|
|
449
|
-
why: c.fields?.why || '',
|
|
450
|
-
}));
|
|
451
|
-
|
|
452
|
-
return {
|
|
453
|
-
profile: 'judgment-profile-v1',
|
|
454
|
-
core: {
|
|
455
|
-
highest_question: source.core_insight || project.release?.description || '',
|
|
456
|
-
axioms,
|
|
457
|
-
boundaries,
|
|
458
|
-
risk_model: {
|
|
459
|
-
risks: byType('risk').map((c) => cardPayload(c)),
|
|
460
|
-
source_risk_level: source.risk_level || null,
|
|
461
|
-
fitness_for_purpose: source.fitness_for_purpose || null,
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
patterns,
|
|
465
|
-
scenarios: byType('scenario').map((c) => cardPayload(c)),
|
|
466
|
-
cases: byType('case').map((c) => cardPayload(c)),
|
|
467
|
-
reasoning: {
|
|
468
|
-
self_checks: byType('self_check').map((c) => c.fields?.question || c.fields?.one_sentence || c.id),
|
|
469
|
-
failure_modes: failureModes,
|
|
470
|
-
reasoning_chains: byType('reasoning').map((c) => cardPayload(c)),
|
|
471
|
-
},
|
|
472
|
-
evolution: {
|
|
473
|
-
stages: byType('evolution_stage').map((c) => cardPayload(c)),
|
|
474
|
-
changelog: locked.map((c) => ({
|
|
475
|
-
id: `locked_${c.id}`,
|
|
476
|
-
card_id: c.id,
|
|
477
|
-
type: c.type,
|
|
478
|
-
event: 'locked',
|
|
479
|
-
at: c.human_lock?.at || null,
|
|
480
|
-
})),
|
|
481
|
-
version_notes: source.judgment_version ? [`Migrated from source judgment_version ${source.judgment_version}`] : [],
|
|
482
|
-
},
|
|
483
|
-
source_cards: locked.map((c) => ({
|
|
484
|
-
id: c.id,
|
|
485
|
-
type: c.type,
|
|
486
|
-
status: c.status,
|
|
487
|
-
fields: c.fields || {},
|
|
488
|
-
human_lock: c.human_lock || null,
|
|
489
|
-
})),
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
384
|
function buildV1Manifest(project, name) {
|
|
494
385
|
const source = project.source_manifest || {};
|
|
495
386
|
const assetId = assetIdFromName(name || source.name || project.name);
|
|
@@ -1043,10 +934,10 @@ function cmdCard(args) {
|
|
|
1043
934
|
}
|
|
1044
935
|
if (sub === 'approve') {
|
|
1045
936
|
const projectInput = args[1];
|
|
1046
|
-
const cardId = args[2];
|
|
1047
937
|
const approveAll = args.includes('--all');
|
|
938
|
+
const cardId = approveAll ? null : args[2];
|
|
1048
939
|
if (!projectInput || (!cardId && !approveAll)) {
|
|
1049
|
-
fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
|
|
940
|
+
fail('Usage: kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign]');
|
|
1050
941
|
}
|
|
1051
942
|
const by = option(args, '--by');
|
|
1052
943
|
const statement = option(args, '--statement');
|
|
@@ -1125,16 +1016,33 @@ function cmdMigrate(args) {
|
|
|
1125
1016
|
const out = option(args, '--out');
|
|
1126
1017
|
const by = option(args, '--by');
|
|
1127
1018
|
const statement = option(args, '--statement');
|
|
1128
|
-
const
|
|
1019
|
+
const requestedName = option(args, '--name');
|
|
1020
|
+
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1129
1021
|
if (!sourceDir || !out || !by || !statement) {
|
|
1130
1022
|
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
|
|
1131
1023
|
}
|
|
1132
1024
|
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1025
|
+
const sourcePath = path.resolve(sourceDir);
|
|
1026
|
+
const format = option(args, '--format');
|
|
1027
|
+
const isStudioProject = fs.existsSync(path.join(sourcePath, 'studio.project.json'));
|
|
1028
|
+
let tmpDir = null;
|
|
1029
|
+
let projectPath = null;
|
|
1030
|
+
let project = null;
|
|
1031
|
+
|
|
1032
|
+
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1135
1033
|
let creatorIdentity = null;
|
|
1136
1034
|
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
|
|
1137
|
-
|
|
1035
|
+
if (format === 'v1' && isStudioProject) {
|
|
1036
|
+
const loaded = readProject(sourcePath);
|
|
1037
|
+
project = loaded.project;
|
|
1038
|
+
projectPath = loaded.projectPath;
|
|
1039
|
+
if (!requestedName && project.name) name = project.name;
|
|
1040
|
+
} else {
|
|
1041
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
|
|
1042
|
+
const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
|
|
1043
|
+
project = imported.project;
|
|
1044
|
+
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
1045
|
+
}
|
|
1138
1046
|
|
|
1139
1047
|
// Reject if critical judgment fields are missing from axioms
|
|
1140
1048
|
const criticalMissing = [];
|
|
@@ -1188,7 +1096,7 @@ function cmdMigrate(args) {
|
|
|
1188
1096
|
project.stages.judgment_cards.locked = locked;
|
|
1189
1097
|
project.stages.judgment_cards.total = project.cards.length;
|
|
1190
1098
|
}
|
|
1191
|
-
writeProject(
|
|
1099
|
+
writeProject(projectPath, project);
|
|
1192
1100
|
console.log(`Approved and Human Locked: ${locked} cards`);
|
|
1193
1101
|
|
|
1194
1102
|
// Step 3: verify Human Lock gate
|
|
@@ -1200,24 +1108,11 @@ function cmdMigrate(args) {
|
|
|
1200
1108
|
|
|
1201
1109
|
const absOut = path.resolve(out);
|
|
1202
1110
|
// Step 4: v1 export or v2 compile + export
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1111
|
+
if (format === 'v1') {
|
|
1112
|
+
exportProjectV1(project, name, absOut);
|
|
1113
|
+
if (tmpDir) {
|
|
1114
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
|
|
1207
1115
|
}
|
|
1208
|
-
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
1209
|
-
const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
|
|
1210
|
-
fs.writeFileSync(path.join(v1Dir, 'mimetype'), 'application/vnd.kdna.asset');
|
|
1211
|
-
fs.writeFileSync(path.join(v1Dir, 'kdna.json'), JSON.stringify(buildV1Manifest(project, name), null, 2));
|
|
1212
|
-
fs.writeFileSync(path.join(v1Dir, 'payload.kdnab'), JSON.stringify(buildV1Payload(project), null, 2));
|
|
1213
|
-
fs.writeFileSync(path.join(v1Dir, 'checksums.json'), JSON.stringify(buildChecksumsV1(v1Dir), null, 2));
|
|
1214
|
-
core.pack(v1Dir, absOut);
|
|
1215
|
-
const vr = core.validate(absOut);
|
|
1216
|
-
if (!vr.overall_valid) fail('v1 export validation failed: ' + (vr.problems || []).join('; '));
|
|
1217
|
-
console.log('Exported (v1): ' + absOut);
|
|
1218
|
-
console.log(' Name: ' + name + ' Cards: ' + lockedCards.length + ' Validated: all gates pass');
|
|
1219
|
-
try { fs.rmSync(v1Dir, { recursive: true }); } catch { /* cleanup */ }
|
|
1220
|
-
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
|
|
1221
1116
|
return;
|
|
1222
1117
|
}
|
|
1223
1118
|
const { result } = compileProject(tmpDir);
|
|
@@ -1240,7 +1135,46 @@ function cmdMigrate(args) {
|
|
|
1240
1135
|
console.log(` Build ID: ${result.identity.build_id}`);
|
|
1241
1136
|
|
|
1242
1137
|
// Cleanup temp
|
|
1243
|
-
|
|
1138
|
+
if (tmpDir) {
|
|
1139
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function exportProjectV1(project, name, outPath) {
|
|
1144
|
+
let core;
|
|
1145
|
+
try { core = require('@aikdna/kdna-core'); } catch (e) {
|
|
1146
|
+
fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
|
|
1147
|
+
}
|
|
1148
|
+
if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
|
|
1149
|
+
fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for v1 export.', 2);
|
|
1150
|
+
}
|
|
1151
|
+
const gate = projectApi.checkHumanLockGate(project);
|
|
1152
|
+
if (gate.blocked) {
|
|
1153
|
+
const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
|
|
1154
|
+
fail(`Human Lock Gate blocked v1 export:\n - ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
|
|
1155
|
+
}
|
|
1156
|
+
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
1157
|
+
const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
|
|
1158
|
+
try {
|
|
1159
|
+
const manifestDefaults = buildV1Manifest(project, name);
|
|
1160
|
+
const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
|
|
1161
|
+
asset_id: manifestDefaults.asset_id,
|
|
1162
|
+
asset_uid: manifestDefaults.asset_uid,
|
|
1163
|
+
title: manifestDefaults.title,
|
|
1164
|
+
created_at: manifestDefaults.created_at,
|
|
1165
|
+
updated_at: manifestDefaults.updated_at,
|
|
1166
|
+
access: project.release?.access || project.source_manifest?.access || 'public',
|
|
1167
|
+
});
|
|
1168
|
+
writeFiles(v1Dir, runtimeAsset.files);
|
|
1169
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1170
|
+
core.pack(v1Dir, outPath);
|
|
1171
|
+
const vr = core.validate(outPath);
|
|
1172
|
+
if (!vr.overall_valid) fail('v1 export validation failed: ' + (vr.problems || []).join('; '));
|
|
1173
|
+
console.log('Exported (v1): ' + outPath);
|
|
1174
|
+
console.log(' Name: ' + name + ' Cards: ' + lockedCards.length + ' Validated: all gates pass');
|
|
1175
|
+
} finally {
|
|
1176
|
+
try { fs.rmSync(v1Dir, { recursive: true }); } catch { /* cleanup */ }
|
|
1177
|
+
}
|
|
1244
1178
|
}
|
|
1245
1179
|
|
|
1246
1180
|
function compileProject(projectInput) {
|
|
@@ -1417,7 +1351,12 @@ function applySignature(files, passphrase = null) {
|
|
|
1417
1351
|
function cmdExport(args) {
|
|
1418
1352
|
const projectInput = args[0];
|
|
1419
1353
|
const out = option(args, '--out');
|
|
1420
|
-
if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--sign]');
|
|
1354
|
+
if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign]');
|
|
1355
|
+
if (option(args, '--format') === 'v1') {
|
|
1356
|
+
const { project } = readProject(projectInput);
|
|
1357
|
+
exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out));
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1421
1360
|
const { project, result } = compileProject(projectInput);
|
|
1422
1361
|
const files = { ...result.files };
|
|
1423
1362
|
files['README.md'] = compileApi.generateReadme(project);
|
|
@@ -1843,6 +1782,10 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
|
1843
1782
|
usage();
|
|
1844
1783
|
process.exit(EXIT.OK);
|
|
1845
1784
|
}
|
|
1785
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
1786
|
+
console.log(require('../package.json').version);
|
|
1787
|
+
process.exit(EXIT.OK);
|
|
1788
|
+
}
|
|
1846
1789
|
|
|
1847
1790
|
(async () => {
|
|
1848
1791
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Official KDNA Studio command-line authoring entry for creating, locking, compiling, and exporting .kdna judgment assets through the KDNA toolchain.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/tests/cli.test.js
CHANGED
|
@@ -27,6 +27,15 @@ function tmpDir() {
|
|
|
27
27
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-studio-cli-'));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function assertCanonicalRuntimeContainer(outFile) {
|
|
31
|
+
const layout = kdnaCore.readV1Layout(outFile);
|
|
32
|
+
const names = (layout.entries || []).map((entry) => entry.name).sort();
|
|
33
|
+
assert.deepEqual(names, ['checksums.json', 'kdna.json', 'mimetype', 'payload.kdnab']);
|
|
34
|
+
assert.equal(names.includes('KDNA_Core.json'), false);
|
|
35
|
+
assert.equal(names.includes('KDNA_Patterns.json'), false);
|
|
36
|
+
return layout;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
function createLockedProject(t) {
|
|
31
40
|
const tmp = tmpDir();
|
|
32
41
|
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -89,6 +98,53 @@ test('rejects unknown commands with input error', () => {
|
|
|
89
98
|
assert.match(result.stderr, /Unknown command/);
|
|
90
99
|
});
|
|
91
100
|
|
|
101
|
+
test('card approve --all locks every unlocked card', (t) => {
|
|
102
|
+
const tmp = tmpDir();
|
|
103
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
104
|
+
const projectDir = path.join(tmp, 'project');
|
|
105
|
+
|
|
106
|
+
let result = run(['create', projectDir, '--name', '@test/approve-all'], { tmp });
|
|
107
|
+
assert.equal(result.status, 0, result.stderr);
|
|
108
|
+
|
|
109
|
+
result = run([
|
|
110
|
+
'card',
|
|
111
|
+
'add',
|
|
112
|
+
projectDir,
|
|
113
|
+
'axiom',
|
|
114
|
+
'--field',
|
|
115
|
+
'one_sentence=Prefer specific evidence over broad claims',
|
|
116
|
+
'--field',
|
|
117
|
+
'full_statement=When evaluating a claim the agent must weigh the specificity of the supporting evidence. A narrow claim backed by concrete data should be preferred over a broad claim supported only by general principles.',
|
|
118
|
+
'--field',
|
|
119
|
+
'why=Without this axiom the agent may accept broad plausible-sounding claims as equally credible as narrow evidence-backed ones leading to false balance.',
|
|
120
|
+
'--field',
|
|
121
|
+
'applies_when=["reviewing content"]',
|
|
122
|
+
'--field',
|
|
123
|
+
'does_not_apply_when=["pure formatting"]',
|
|
124
|
+
'--field',
|
|
125
|
+
'failure_risk=generic advice',
|
|
126
|
+
], { tmp });
|
|
127
|
+
assert.equal(result.status, 0, result.stderr);
|
|
128
|
+
|
|
129
|
+
result = run([
|
|
130
|
+
'card',
|
|
131
|
+
'approve',
|
|
132
|
+
projectDir,
|
|
133
|
+
'--all',
|
|
134
|
+
'--by',
|
|
135
|
+
'expert',
|
|
136
|
+
'--statement',
|
|
137
|
+
'I confirm every unlocked card in this project.',
|
|
138
|
+
], { tmp });
|
|
139
|
+
assert.equal(result.status, 0, result.stderr);
|
|
140
|
+
assert.match(result.stdout, /Approved and Human Locked: 1 cards/);
|
|
141
|
+
|
|
142
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
143
|
+
assert.equal(project.cards.length, 1);
|
|
144
|
+
assert.equal(project.cards[0].locked, true);
|
|
145
|
+
assert.ok(project.cards[0].human_lock);
|
|
146
|
+
});
|
|
147
|
+
|
|
92
148
|
test('creates a Studio project and rejects accidental overwrite', (t) => {
|
|
93
149
|
const tmp = tmpDir();
|
|
94
150
|
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -309,7 +365,7 @@ test('create --from-folder imports legacy JSON source, outputs audit', (t) => {
|
|
|
309
365
|
assert.equal(project.lineage.type, 'migrated');
|
|
310
366
|
});
|
|
311
367
|
|
|
312
|
-
test('migrate --format v1
|
|
368
|
+
test('migrate --format v1 exports canonical runtime payload without source entries', (t) => {
|
|
313
369
|
assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
|
|
314
370
|
const tmp = tmpDir();
|
|
315
371
|
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
@@ -374,29 +430,68 @@ test('migrate --format v1 maps scoped name, writes checksums, and preserves sour
|
|
|
374
430
|
const inspected = kdnaCore.inspect(outFile);
|
|
375
431
|
assert.equal(inspected.asset_id, 'kdna:test:legacy-import');
|
|
376
432
|
assert.equal(inspected.checksums_present, true);
|
|
433
|
+
const layout = assertCanonicalRuntimeContainer(outFile);
|
|
434
|
+
assert.equal(layout.manifest.lineage.type, 'adaptation');
|
|
435
|
+
assert.equal(layout.manifest.lineage.source_lineage_type, 'migrated');
|
|
377
436
|
|
|
378
437
|
const full = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
|
|
379
438
|
assert.equal(full.manifest.creator.name, 'Test Author');
|
|
380
439
|
assert.equal(full.manifest.version, '0.7.3');
|
|
381
440
|
assert.equal(full.manifest.judgment_version, '2026.05.0');
|
|
441
|
+
assert.equal(full.payload.profile, 'judgment-profile-v1');
|
|
382
442
|
assert.equal(full.payload.core.axioms.length, 1);
|
|
383
|
-
assert.equal(full.payload.core.boundaries.length, 3);
|
|
384
|
-
assert.ok(full.payload.core.boundaries.some((b) => b.type === 'axiom_applicability'));
|
|
385
|
-
assert.ok(full.payload.core.boundaries.some((b) => b.type === 'ontology_boundary'));
|
|
386
|
-
assert.ok(full.payload.core.boundaries.some((b) => b.type === 'stance_boundary'));
|
|
387
|
-
assert.equal(full.payload.patterns.filter((p) => p.type === 'term').length, 1);
|
|
388
|
-
assert.equal(full.payload.patterns.filter((p) => p.type === 'banned_term').length, 1);
|
|
389
443
|
assert.equal(full.payload.scenarios.length, 1);
|
|
390
444
|
assert.equal(full.payload.cases.length, 1);
|
|
391
445
|
assert.equal(full.payload.reasoning.self_checks.length, 1);
|
|
392
446
|
assert.equal(full.payload.reasoning.failure_modes.length, 1);
|
|
393
|
-
assert.
|
|
394
|
-
assert.equal(full.payload
|
|
395
|
-
assert.equal(full.payload.source_cards.length, 12);
|
|
447
|
+
assert.ok(full.payload.evolution.stages.length >= 1);
|
|
448
|
+
assert.equal(Object.hasOwn(full.payload, 'source_cards'), false);
|
|
396
449
|
assert.equal(JSON.stringify(full).includes('quality_badge'), false);
|
|
397
450
|
assert.equal(JSON.stringify(full).includes('registry'), false);
|
|
398
451
|
});
|
|
399
452
|
|
|
453
|
+
test('export --format v1 exports a Studio project as a valid non-empty v1 asset', (t) => {
|
|
454
|
+
assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
|
|
455
|
+
const { tmp, projectDir } = createLockedProject(t);
|
|
456
|
+
const outFile = path.join(tmp, 'project-v1.kdna');
|
|
457
|
+
|
|
458
|
+
const result = run(['export', projectDir, '--format', 'v1', '--out', outFile], { tmp });
|
|
459
|
+
assert.equal(result.status, 0, result.stderr);
|
|
460
|
+
assert.match(result.stdout, /Exported \(v1\)/);
|
|
461
|
+
|
|
462
|
+
const validation = kdnaCore.validate(outFile);
|
|
463
|
+
assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
|
|
464
|
+
assertCanonicalRuntimeContainer(outFile);
|
|
465
|
+
const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
|
|
466
|
+
assert.equal(loaded.payload.core.axioms.length, 1);
|
|
467
|
+
assert.equal(Object.hasOwn(loaded.payload, 'source_cards'), false);
|
|
468
|
+
assert.equal(loaded.manifest.payload.path, 'payload.kdnab');
|
|
469
|
+
assert.equal(loaded.manifest.payload.encrypted, false);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('migrate --format v1 accepts an existing Studio project without dropping cards', (t) => {
|
|
473
|
+
assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
|
|
474
|
+
const { tmp, projectDir } = createLockedProject(t);
|
|
475
|
+
const outFile = path.join(tmp, 'project-migrated-v1.kdna');
|
|
476
|
+
|
|
477
|
+
const result = run([
|
|
478
|
+
'migrate', projectDir,
|
|
479
|
+
'--format', 'v1',
|
|
480
|
+
'--out', outFile,
|
|
481
|
+
'--by', 'tester',
|
|
482
|
+
'--statement', 'I confirm this project v1 migration.',
|
|
483
|
+
], { tmp });
|
|
484
|
+
assert.equal(result.status, 0, result.stderr);
|
|
485
|
+
assert.match(result.stdout, /Cards: 1/);
|
|
486
|
+
|
|
487
|
+
const validation = kdnaCore.validate(outFile);
|
|
488
|
+
assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
|
|
489
|
+
assertCanonicalRuntimeContainer(outFile);
|
|
490
|
+
const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
|
|
491
|
+
assert.equal(loaded.payload.core.axioms.length, 1);
|
|
492
|
+
assert.equal(Object.hasOwn(loaded.payload, 'source_cards'), false);
|
|
493
|
+
});
|
|
494
|
+
|
|
400
495
|
// ── Create from KDNA ────────────────────────────────────────────────
|
|
401
496
|
|
|
402
497
|
test('create --from-kdna forks an existing .kdna asset', (t) => {
|