@aikdna/kdna-studio-cli 0.5.3 → 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,14 +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 validate dist/my_domain.kdna
63
+ kdna validate dist/my_domain.kdna --runtime
64
+ kdna plan-load dist/my_domain.kdna --json
62
65
  kdna load dist/my_domain.kdna --profile=compact --as=prompt
63
66
  ```
64
67
 
65
68
  `kdna-studio` is the CLI entry for confirmed KDNA authoring. `kdna` is the
66
69
  runtime control plane for inspecting, validating, packing, unpacking, and
67
- loading existing `.kdna` assets. Signature, encryption, registry publishing,
68
- and private assets are future/gated phases, not the current Core v1 baseline.
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.
69
92
 
70
93
  ## Identity
71
94
 
@@ -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');
@@ -152,12 +162,6 @@ function parseFields(args) {
152
162
  return fields;
153
163
  }
154
164
 
155
- function arrayValue(value) {
156
- if (Array.isArray(value)) return value;
157
- if (value === undefined || value === null || value === '') return [];
158
- return [value];
159
- }
160
-
161
165
  function semverValue(value, fallback = '1.0.0') {
162
166
  const raw = String(value || '').trim();
163
167
  if (/^[0-9]+\.[0-9]+\.[0-9]+([+-].+)?$/.test(raw)) return raw;
@@ -208,10 +212,6 @@ function publicManifestMetadata(raw = {}) {
208
212
  return copy;
209
213
  }
210
214
 
211
- function cardPayload(card) {
212
- return { id: card.id, ...(card.fields || {}) };
213
- }
214
-
215
215
  function cleanFields(fields = {}) {
216
216
  const cleaned = {};
217
217
  for (const [key, value] of Object.entries(fields || {})) {
@@ -381,116 +381,6 @@ function cardsFromPayload(payload) {
381
381
  return [];
382
382
  }
383
383
 
384
- function buildV1Payload(project) {
385
- const locked = (project.cards || []).filter((c) => c.locked);
386
- const byType = (type) => locked.filter((c) => c.type === type);
387
- const source = project.source_manifest || {};
388
-
389
- const axioms = byType('axiom').map((c) => ({
390
- id: c.id,
391
- one_sentence: c.fields?.one_sentence || '',
392
- full_statement: c.fields?.full_statement || '',
393
- why: c.fields?.why || '',
394
- applies_when: arrayValue(c.fields?.applies_when),
395
- does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
396
- failure_risk: c.fields?.failure_risk || '',
397
- status: c.status || null,
398
- human_lock: c.human_lock || null,
399
- }));
400
-
401
- const boundaries = [
402
- ...byType('boundary').map((c) => ({ type: 'explicit_boundary', ...cardPayload(c) })),
403
- ...byType('axiom').map((c) => ({
404
- id: `${c.id}_applicability`,
405
- type: 'axiom_applicability',
406
- axiom_id: c.id,
407
- one_sentence: c.fields?.one_sentence || '',
408
- applies_when: arrayValue(c.fields?.applies_when),
409
- does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
410
- failure_risk: c.fields?.failure_risk || '',
411
- })),
412
- ...byType('ontology')
413
- .filter((c) => c.fields?.boundary || c.fields?.applies_when || c.fields?.does_not_apply_when)
414
- .map((c) => ({
415
- id: `${c.id}_boundary`,
416
- type: 'ontology_boundary',
417
- concept_id: c.id,
418
- one_sentence: c.fields?.one_sentence || '',
419
- boundary: c.fields?.boundary || '',
420
- applies_when: arrayValue(c.fields?.applies_when),
421
- does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
422
- trigger_signal: c.fields?.trigger_signal || '',
423
- })),
424
- ...byType('stance')
425
- .filter((c) => c.fields?.applies_when || c.fields?.does_not_apply_when)
426
- .map((c) => ({
427
- id: `${c.id}_boundary`,
428
- type: 'stance_boundary',
429
- stance_id: c.id,
430
- stance: c.fields?.stance || c.fields?.statement || c.fields?.one_sentence || '',
431
- applies_when: arrayValue(c.fields?.applies_when),
432
- does_not_apply_when: arrayValue(c.fields?.does_not_apply_when),
433
- })),
434
- ];
435
-
436
- const patterns = [
437
- ...byType('ontology').map((c) => ({ type: 'ontology', ...cardPayload(c) })),
438
- ...byType('misunderstanding').map((c) => ({ type: 'misunderstanding', ...cardPayload(c) })),
439
- ...byType('term').map((c) => ({ type: 'term', ...cardPayload(c) })),
440
- ...byType('banned_term').map((c) => ({ type: 'banned_term', ...cardPayload(c) })),
441
- ...byType('framework').map((c) => ({ type: 'framework', ...cardPayload(c) })),
442
- ...byType('stance').map((c) => ({ type: 'stance', ...cardPayload(c) })),
443
- ];
444
-
445
- const failureModes = byType('misunderstanding').map((c) => ({
446
- id: c.id,
447
- mode: c.fields?.wrong || c.fields?.key_distinction || c.id,
448
- correct: c.fields?.correct || '',
449
- key_distinction: c.fields?.key_distinction || '',
450
- why: c.fields?.why || '',
451
- }));
452
-
453
- return {
454
- profile: 'judgment-profile-v1',
455
- core: {
456
- highest_question: source.core_insight || project.release?.description || '',
457
- axioms,
458
- boundaries,
459
- risk_model: {
460
- risks: byType('risk').map((c) => cardPayload(c)),
461
- source_risk_level: source.risk_level || null,
462
- fitness_for_purpose: source.fitness_for_purpose || null,
463
- },
464
- },
465
- patterns,
466
- scenarios: byType('scenario').map((c) => cardPayload(c)),
467
- cases: byType('case').map((c) => cardPayload(c)),
468
- reasoning: {
469
- self_checks: byType('self_check').map((c) => c.fields?.question || c.fields?.one_sentence || c.id),
470
- failure_modes: failureModes,
471
- reasoning_chains: byType('reasoning').map((c) => cardPayload(c)),
472
- },
473
- evolution: {
474
- stages: byType('evolution_stage').map((c) => cardPayload(c)),
475
- changelog: locked.map((c) => ({
476
- id: `locked_${c.id}`,
477
- card_id: c.id,
478
- type: c.type,
479
- event: 'locked',
480
- at: c.human_lock?.at || null,
481
- })),
482
- version_notes: source.judgment_version ? [`Migrated from source judgment_version ${source.judgment_version}`] : [],
483
- },
484
- source_cards: locked.map((c) => ({
485
- id: c.id,
486
- type: c.type,
487
- status: c.status,
488
- fields: c.fields || {},
489
- human_lock: c.human_lock || null,
490
- })),
491
- };
492
- }
493
-
494
384
  function buildV1Manifest(project, name) {
495
385
  const source = project.source_manifest || {};
496
386
  const assetId = assetIdFromName(name || source.name || project.name);
@@ -1044,10 +934,10 @@ function cmdCard(args) {
1044
934
  }
1045
935
  if (sub === 'approve') {
1046
936
  const projectInput = args[1];
1047
- const cardId = args[2];
1048
937
  const approveAll = args.includes('--all');
938
+ const cardId = approveAll ? null : args[2];
1049
939
  if (!projectInput || (!cardId && !approveAll)) {
1050
- 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]');
1051
941
  }
1052
942
  const by = option(args, '--by');
1053
943
  const statement = option(args, '--statement');
@@ -1255,6 +1145,9 @@ function exportProjectV1(project, name, outPath) {
1255
1145
  try { core = require('@aikdna/kdna-core'); } catch (e) {
1256
1146
  fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
1257
1147
  }
1148
+ if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
1149
+ fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for v1 export.', 2);
1150
+ }
1258
1151
  const gate = projectApi.checkHumanLockGate(project);
1259
1152
  if (gate.blocked) {
1260
1153
  const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
@@ -1263,10 +1156,16 @@ function exportProjectV1(project, name, outPath) {
1263
1156
  const lockedCards = (project.cards || []).filter(c => c.locked);
1264
1157
  const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
1265
1158
  try {
1266
- fs.writeFileSync(path.join(v1Dir, 'mimetype'), 'application/vnd.kdna.asset');
1267
- fs.writeFileSync(path.join(v1Dir, 'kdna.json'), JSON.stringify(buildV1Manifest(project, name), null, 2));
1268
- fs.writeFileSync(path.join(v1Dir, 'payload.kdnab'), JSON.stringify(buildV1Payload(project), null, 2));
1269
- fs.writeFileSync(path.join(v1Dir, 'checksums.json'), JSON.stringify(buildChecksumsV1(v1Dir), null, 2));
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);
1270
1169
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
1271
1170
  core.pack(v1Dir, outPath);
1272
1171
  const vr = core.validate(outPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.5.3",
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,25 +430,22 @@ 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
  });
@@ -408,10 +461,12 @@ test('export --format v1 exports a Studio project as a valid non-empty v1 asset'
408
461
 
409
462
  const validation = kdnaCore.validate(outFile);
410
463
  assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
464
+ assertCanonicalRuntimeContainer(outFile);
411
465
  const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
412
466
  assert.equal(loaded.payload.core.axioms.length, 1);
413
- assert.equal(loaded.payload.source_cards.length, 1);
414
- assert.ok(loaded.payload.core.boundaries.some((b) => b.type === 'axiom_applicability'));
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);
415
470
  });
416
471
 
417
472
  test('migrate --format v1 accepts an existing Studio project without dropping cards', (t) => {
@@ -431,9 +486,10 @@ test('migrate --format v1 accepts an existing Studio project without dropping ca
431
486
 
432
487
  const validation = kdnaCore.validate(outFile);
433
488
  assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
489
+ assertCanonicalRuntimeContainer(outFile);
434
490
  const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
435
491
  assert.equal(loaded.payload.core.axioms.length, 1);
436
- assert.equal(loaded.payload.source_cards.length, 1);
492
+ assert.equal(Object.hasOwn(loaded.payload, 'source_cards'), false);
437
493
  });
438
494
 
439
495
  // ── Create from KDNA ────────────────────────────────────────────────