@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 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 <card-id> --by expert --statement "I confirm this judgment."
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 --sign
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 verify dist/my_domain.kdna --judgment
62
- kdna publish dist/my_domain.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 trusted KDNA creation. `kdna` is the runtime
66
- control plane for existing `.kdna` assets.
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 verify, install, load, publish
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
@@ -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
- } = require('@aikdna/kdna-studio-core');
18
- const { buildChecksumsV1 } = require('@aikdna/kdna-core/v1');
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> [--sign]
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 name = option(args, '--name') || path.basename(path.resolve(sourceDir || '.'));
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
- // Step 1: create temp project and import everything from dev source
1134
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
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
- const { project } = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
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(path.join(tmpDir, 'studio.project.json'), project);
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 (args.includes('--format') && option(args, '--format') === 'v1') {
1204
- let core;
1205
- try { core = require('@aikdna/kdna-core'); } catch (e) {
1206
- fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
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
- try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
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.2",
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 maps scoped name, writes checksums, and preserves source cards', (t) => {
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.equal(full.payload.reasoning.reasoning_chains.length, 1);
394
- assert.equal(full.payload.evolution.stages.length, 1);
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) => {