@aikdna/kdna-studio-cli 0.5.1 → 0.5.3

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
@@ -58,12 +58,14 @@ Candidate promotion is scope-gated: only candidates with `status == accepted` an
58
58
  After export, use the runtime CLI:
59
59
 
60
60
  ```bash
61
- kdna verify dist/my_domain.kdna --judgment
62
- kdna publish dist/my_domain.kdna
61
+ kdna validate dist/my_domain.kdna
62
+ kdna load dist/my_domain.kdna --profile=compact --as=prompt
63
63
  ```
64
64
 
65
- `kdna-studio` is the CLI entry for trusted KDNA creation. `kdna` is the runtime
66
- control plane for existing `.kdna` assets.
65
+ `kdna-studio` is the CLI entry for confirmed KDNA authoring. `kdna` is the
66
+ 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.
67
69
 
68
70
  ## Identity
69
71
 
@@ -85,6 +87,6 @@ kdna-studio create migrated --from-folder ./old-domain-json --name @scope/migrat
85
87
  ## Related
86
88
 
87
89
  - [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
90
+ - [kdna-cli](https://github.com/aikdna/kdna-cli) — runtime CLI for inspect, validate, pack, unpack, and load
89
91
  - [kdna-studio-core](https://github.com/aikdna/kdna-studio-core) — authoring SDK used by this CLI
90
92
  - [kdna-studio-swift](https://github.com/aikdna/kdna-studio-swift) — native Swift counterpart
@@ -4,6 +4,7 @@ const os = require('os');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
6
  const zlib = require('zlib');
7
+ const cbor = require('cbor-x');
7
8
 
8
9
  const {
9
10
  project: projectApi,
@@ -14,6 +15,7 @@ const {
14
15
  creator: creatorApi,
15
16
  distillation: distillationApi,
16
17
  } = require('@aikdna/kdna-studio-core');
18
+ const { buildChecksumsV1 } = require('@aikdna/kdna-core/v1');
17
19
 
18
20
  const llm = require('../src/llm');
19
21
  const ai = require('../src/ai');
@@ -37,7 +39,7 @@ Create (three entry paths):
37
39
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
38
40
 
39
41
  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]
42
+ kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign]
41
43
 
42
44
  Authoring:
43
45
  kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
@@ -48,7 +50,8 @@ Authoring:
48
50
  kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign] [--passphrase <pass>]
49
51
  kdna-studio lock <project>
50
52
  kdna-studio compile <project> --out <dir>
51
- kdna-studio export <project> --out <file.kdna> [--sign]
53
+ kdna-studio export <project> --format v1 --out <file.kdna>
54
+ kdna-studio export <project> --out <file.kdna> [--sign] # legacy v2
52
55
 
53
56
  AI Authoring (requires LLM config: kdna-studio llm config):
54
57
  kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
@@ -149,6 +152,391 @@ function parseFields(args) {
149
152
  return fields;
150
153
  }
151
154
 
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
+ function semverValue(value, fallback = '1.0.0') {
162
+ const raw = String(value || '').trim();
163
+ if (/^[0-9]+\.[0-9]+\.[0-9]+([+-].+)?$/.test(raw)) return raw;
164
+ const twoPart = raw.match(/^([0-9]+)\.([0-9]+)$/);
165
+ if (twoPart) return `${twoPart[1]}.${twoPart[2]}.0`;
166
+ return fallback;
167
+ }
168
+
169
+ function isoDateTime(value) {
170
+ if (!value) return new Date().toISOString();
171
+ const raw = String(value);
172
+ if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return `${raw}T00:00:00.000Z`;
173
+ const d = new Date(raw);
174
+ return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString();
175
+ }
176
+
177
+ function slugSegment(value, fallback = 'asset') {
178
+ const s = String(value || fallback)
179
+ .trim()
180
+ .replace(/^@/, '')
181
+ .replace(/[^a-zA-Z0-9_.-]+/g, '_')
182
+ .replace(/^_+|_+$/g, '');
183
+ return s || fallback;
184
+ }
185
+
186
+ function assetIdFromName(name) {
187
+ const raw = String(name || '').trim();
188
+ if (/^[a-zA-Z][a-zA-Z0-9_-]*(:[a-zA-Z0-9_.-]+)+$/.test(raw)) return raw;
189
+ const scoped = raw.match(/^@?([^/:\s]+)\/([^/:\s]+)$/);
190
+ if (scoped) return `kdna:${slugSegment(scoped[1], 'scope')}:${slugSegment(scoped[2], 'asset')}`;
191
+ const parts = raw.split(':').filter(Boolean);
192
+ if (parts.length >= 2) return `kdna:${parts.map((p) => slugSegment(p)).join(':')}`;
193
+ return `kdna:studio:${slugSegment(raw, 'asset')}`;
194
+ }
195
+
196
+ function titleFromName(name, assetId) {
197
+ const raw = String(name || '').trim();
198
+ if (raw) return raw.replace(/^@/, '').replace('/', ' / ');
199
+ return assetId;
200
+ }
201
+
202
+ function publicManifestMetadata(raw = {}) {
203
+ const copy = { ...raw };
204
+ delete copy.registry;
205
+ delete copy.quality_badge;
206
+ delete copy.signature;
207
+ delete copy.media_type;
208
+ return copy;
209
+ }
210
+
211
+ function cardPayload(card) {
212
+ return { id: card.id, ...(card.fields || {}) };
213
+ }
214
+
215
+ function cleanFields(fields = {}) {
216
+ const cleaned = {};
217
+ for (const [key, value] of Object.entries(fields || {})) {
218
+ if (value !== undefined && value !== null) cleaned[key] = value;
219
+ }
220
+ return cleaned;
221
+ }
222
+
223
+ function importedCard(type, fields = {}, id = null) {
224
+ try {
225
+ return cardApi.createCard(type, cleanFields(fields), id || null);
226
+ } catch (_) {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ function pushImportedCard(cards, type, fields = {}, id = null) {
232
+ const card = importedCard(type, fields, id);
233
+ if (card) cards.push(card);
234
+ }
235
+
236
+ function decodePayload(bytes, manifest = {}) {
237
+ const raw = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
238
+ const encoding = manifest.payload?.encoding || manifest.container?.payload_encoding || null;
239
+ if (encoding === 'json' || raw[0] === 0x7b || raw[0] === 0x5b) {
240
+ return JSON.parse(raw.toString('utf8'));
241
+ }
242
+ return cbor.decode(raw);
243
+ }
244
+
245
+ function cardsFromV1Payload(payload) {
246
+ const cards = [];
247
+ for (const sourceCard of (payload.source_cards || [])) {
248
+ pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
249
+ }
250
+ if (cards.length > 0) return cards;
251
+
252
+ for (const ax of (payload.core?.axioms || [])) {
253
+ pushImportedCard(cards, 'axiom', {
254
+ one_sentence: ax.one_sentence || '',
255
+ full_statement: ax.full_statement || '',
256
+ why: ax.why || '',
257
+ applies_when: ax.applies_when || [],
258
+ does_not_apply_when: ax.does_not_apply_when || [],
259
+ failure_risk: ax.failure_risk || '',
260
+ }, ax.id || null);
261
+ }
262
+ for (const boundary of (payload.core?.boundaries || [])) {
263
+ pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
264
+ }
265
+ for (const risk of (payload.core?.risk_model?.risks || [])) {
266
+ pushImportedCard(cards, 'risk', risk, risk.id || null);
267
+ }
268
+ for (const pattern of (payload.patterns || [])) {
269
+ if (!pattern.type) continue;
270
+ const { type, ...fields } = pattern;
271
+ pushImportedCard(cards, type, fields, pattern.id || null);
272
+ }
273
+ for (const scenario of (payload.scenarios || [])) {
274
+ pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
275
+ }
276
+ for (const item of (payload.cases || [])) {
277
+ pushImportedCard(cards, 'case', item, item.id || null);
278
+ }
279
+ for (const selfCheck of (payload.reasoning?.self_checks || [])) {
280
+ const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
281
+ pushImportedCard(cards, 'self_check', fields, fields.id || null);
282
+ }
283
+ for (const failureMode of (payload.reasoning?.failure_modes || [])) {
284
+ pushImportedCard(cards, 'misunderstanding', {
285
+ wrong: failureMode.mode || failureMode.wrong || '',
286
+ correct: failureMode.correct || '',
287
+ key_distinction: failureMode.key_distinction || '',
288
+ why: failureMode.why || '',
289
+ }, failureMode.id || null);
290
+ }
291
+ for (const chain of (payload.reasoning?.reasoning_chains || [])) {
292
+ pushImportedCard(cards, 'reasoning', chain, chain.id || null);
293
+ }
294
+ for (const stage of (payload.evolution?.stages || [])) {
295
+ pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
296
+ }
297
+ return cards;
298
+ }
299
+
300
+ function cardsFromLegacyPayload(payload) {
301
+ const cards = [];
302
+ const judgment = payload.judgment || {};
303
+ const core = judgment.core || {};
304
+ const patterns = judgment.patterns || {};
305
+
306
+ for (const ax of (core.axioms || [])) {
307
+ pushImportedCard(cards, 'axiom', {
308
+ one_sentence: ax.one_sentence || '',
309
+ full_statement: ax.full_statement || '',
310
+ why: ax.why || '',
311
+ applies_when: ax.applies_when || [],
312
+ does_not_apply_when: ax.does_not_apply_when || [],
313
+ failure_risk: ax.failure_risk || '',
314
+ }, ax.id || null);
315
+ }
316
+ for (const ont of (core.ontology || [])) {
317
+ pushImportedCard(cards, 'ontology', {
318
+ one_sentence: ont.one_sentence || ont.essence || '',
319
+ essence: ont.essence || '',
320
+ boundary: ont.boundary || '',
321
+ trigger_signal: ont.trigger_signal || '',
322
+ }, ont.id || null);
323
+ }
324
+ for (const boundary of (core.boundaries || [])) {
325
+ pushImportedCard(cards, 'boundary', {
326
+ scope: boundary.scope || '',
327
+ out_of_scope: boundary.out_of_scope || '',
328
+ acceptable_exceptions: boundary.acceptable_exceptions || [],
329
+ }, boundary.id || null);
330
+ }
331
+ for (const risk of (core.risks || [])) {
332
+ pushImportedCard(cards, 'risk', risk, risk.id || null);
333
+ }
334
+ for (const term of (patterns.terminology?.standard_terms || [])) {
335
+ pushImportedCard(cards, 'term', term, term.id || null);
336
+ }
337
+ for (const banned of (patterns.terminology?.banned_terms || [])) {
338
+ pushImportedCard(cards, 'banned_term', banned, banned.id || null);
339
+ }
340
+ for (const ms of (patterns.misunderstandings || [])) {
341
+ pushImportedCard(cards, 'misunderstanding', {
342
+ wrong: ms.wrong || '',
343
+ correct: ms.correct || '',
344
+ key_distinction: ms.key_distinction || '',
345
+ why: ms.why || '',
346
+ applies_when: ms.applies_when || [],
347
+ does_not_apply_when: ms.does_not_apply_when || [],
348
+ failure_risk: ms.failure_risk || '',
349
+ }, ms.id || null);
350
+ }
351
+ for (const sc of (patterns.self_check || [])) {
352
+ const question = typeof sc === 'string' ? sc : sc.question || '';
353
+ pushImportedCard(cards, 'self_check', { question }, sc.id || null);
354
+ }
355
+ for (const aesthetic of (patterns.aesthetics || [])) {
356
+ pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
357
+ }
358
+ for (const scene of (judgment.scenarios?.scenes || [])) {
359
+ pushImportedCard(cards, 'scenario', scene, scene.id || null);
360
+ }
361
+ for (const item of (judgment.cases?.cases || [])) {
362
+ pushImportedCard(cards, 'case', item, item.id || null);
363
+ }
364
+ for (const chain of (judgment.reasoning?.reasoning_chains || [])) {
365
+ pushImportedCard(cards, 'reasoning', chain, chain.id || null);
366
+ }
367
+ for (const stage of (judgment.evolution?.stages || [])) {
368
+ pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
369
+ }
370
+ return cards;
371
+ }
372
+
373
+ function cardsFromPayload(payload) {
374
+ if (!payload || typeof payload !== 'object') return [];
375
+ if (payload.profile === 'judgment-profile-v1' || payload.core || payload.source_cards) {
376
+ return cardsFromV1Payload(payload);
377
+ }
378
+ if (payload.kind === 'kdna.payload' || payload.judgment) {
379
+ return cardsFromLegacyPayload(payload);
380
+ }
381
+ return [];
382
+ }
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
+ function buildV1Manifest(project, name) {
495
+ const source = project.source_manifest || {};
496
+ const assetId = assetIdFromName(name || source.name || project.name);
497
+ const author = source.author || project.author || {};
498
+ return {
499
+ kdna_version: '1.0',
500
+ asset_id: assetId,
501
+ asset_uid: project.asset_uid || 'urn:uuid:' + crypto.randomUUID(),
502
+ asset_type: project.type === 'cluster' ? 'cluster' : 'domain',
503
+ source_asset_type: source.asset_type || null,
504
+ title: titleFromName(source.name || name || project.name, assetId),
505
+ version: semverValue(project.release?.version || source.version, '1.0.0'),
506
+ judgment_version: semverValue(project.release?.judgment_version || source.judgment_version || source.version, '1.0.0'),
507
+ created_at: isoDateTime(source.created || project.created),
508
+ updated_at: isoDateTime(source.updated || project.updated),
509
+ creator: {
510
+ name: author.name || project.author?.name || 'Studio Export',
511
+ id: author.id || project.author?.id || undefined,
512
+ },
513
+ compatibility: { min_loader_version: '1.0.0', profile: 'judgment-profile-v1' },
514
+ payload: { path: 'payload.kdnab', encoding: 'json', encrypted: false },
515
+ summary: source.core_insight || project.release?.description || source.description || '',
516
+ description: source.description || project.release?.description || '',
517
+ language: source.default_language || project.default_language || (Array.isArray(source.languages) ? source.languages[0] : undefined),
518
+ languages: source.languages || project.languages || undefined,
519
+ license: source.license || project.license || undefined,
520
+ keywords: source.keywords || [],
521
+ lineage: {
522
+ type: project.lineage?.type === 'migrated' ? 'adaptation' : (project.lineage?.type || 'original'),
523
+ fork_of: null,
524
+ derived_from: source.name || project.lineage?.parent_name || null,
525
+ source_lineage_type: project.lineage?.type || null,
526
+ },
527
+ load_contract: {
528
+ default_profile: 'compact',
529
+ profiles: {
530
+ index: { requires_decryption: false, max_tokens_hint: 500, selection: 'manifest metadata', intended_for: ['discovery'] },
531
+ compact: { requires_decryption: false, max_tokens_hint: 2000, selection: 'core judgment summary', intended_for: ['agent prompt'] },
532
+ scenario: { requires_decryption: false, max_tokens_hint: 3000, selection: 'scenario cards', intended_for: ['situational loading'] },
533
+ full: { requires_decryption: false, max_tokens_hint: 12000, selection: 'full manifest and payload', intended_for: ['audit', 'migration'] },
534
+ },
535
+ },
536
+ source_manifest: publicManifestMetadata(source),
537
+ };
538
+ }
539
+
152
540
  function cmdCreate(args) {
153
541
  const dir = args[0];
154
542
  if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--from-kdna <file> | --from-folder <dir>]');
@@ -171,13 +559,30 @@ function cmdCreate(args) {
171
559
  const kdnaData = importFromKdna(fromKdna);
172
560
  lineage = kdnaData.lineage;
173
561
  if (kdnaData.cards) {
562
+ const sourceAuthor = kdnaData.source_manifest?.author || {};
174
563
  const project = projectApi.createProject(name, 'domain', {
175
- author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
564
+ author: {
565
+ name: option(args, '--author-name', sourceAuthor.name || ''),
566
+ id: option(args, '--author-id', sourceAuthor.id || ''),
567
+ },
176
568
  sourceMode,
177
569
  creatorIdentity,
178
570
  lineage,
179
571
  });
180
572
  project.cards = kdnaData.cards;
573
+ if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
574
+ if (kdnaData.source_manifest?.version) {
575
+ if (!project.release) project.release = {};
576
+ project.release.version = kdnaData.source_manifest.version;
577
+ }
578
+ if (kdnaData.source_manifest?.judgment_version) {
579
+ if (!project.release) project.release = {};
580
+ project.release.judgment_version = kdnaData.source_manifest.judgment_version;
581
+ }
582
+ if (kdnaData.source_manifest?.description) {
583
+ if (!project.release) project.release = {};
584
+ project.release.description = kdnaData.source_manifest.description;
585
+ }
181
586
  if (project.stages?.judgment_cards) {
182
587
  project.stages.judgment_cards.total = kdnaData.cards.length;
183
588
  project.stages.judgment_cards.status = 'in_progress';
@@ -232,10 +637,10 @@ function importFromKdna(kdnaPath) {
232
637
  const manifest = JSON.parse(entries.get('kdna.json').toString());
233
638
  const lineage = {
234
639
  type: 'fork',
235
- parent_name: manifest.name || null,
640
+ parent_name: manifest.name || manifest.title || manifest.asset_id || null,
236
641
  parent_asset_uid: manifest.asset_uid || null,
237
642
  parent_version: manifest.version || null,
238
- parent_asset_digest: manifest.content_digest || null,
643
+ parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
239
644
  };
240
645
 
241
646
  // Extract cards from KDNA files
@@ -277,7 +682,16 @@ function importFromKdna(kdnaPath) {
277
682
  }
278
683
  }
279
684
 
280
- return { lineage, cards: importedCards };
685
+ if (importedCards.length === 0 && entries.has('payload.kdnab')) {
686
+ try {
687
+ const payload = decodePayload(entries.get('payload.kdnab'), manifest);
688
+ importedCards.push(...cardsFromPayload(payload));
689
+ } catch (e) {
690
+ fail(`Could not import cards from payload.kdnab: ${e.message}`);
691
+ }
692
+ }
693
+
694
+ return { lineage, cards: importedCards, source_manifest: manifest };
281
695
  }
282
696
 
283
697
  /**
@@ -305,12 +719,22 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
305
719
  // Load manifest for version/languages/description
306
720
  const manifest = loadJson('kdna.json');
307
721
  if (manifest) {
722
+ manifestData.raw = manifest;
308
723
  if (manifest.version) manifestData.version = manifest.version;
309
724
  if (manifest.judgment_version) manifestData.judgment_version = manifest.judgment_version;
310
725
  if (manifest.languages) manifestData.languages = manifest.languages;
311
726
  if (manifest.default_language) manifestData.default_language = manifest.default_language;
312
727
  if (manifest.description) manifestData.description = manifest.description;
313
728
  if (manifest.name) manifestData.name = manifest.name;
729
+ if (manifest.keywords) manifestData.keywords = manifest.keywords;
730
+ if (manifest.license) manifestData.license = manifest.license;
731
+ if (manifest.author) manifestData.author = manifest.author;
732
+ if (manifest.asset_type) manifestData.asset_type = manifest.asset_type;
733
+ if (manifest.core_insight) manifestData.core_insight = manifest.core_insight;
734
+ if (manifest.created) manifestData.created = manifest.created;
735
+ if (manifest.updated) manifestData.updated = manifest.updated;
736
+ if (manifest.risk_level) manifestData.risk_level = manifest.risk_level;
737
+ if (manifest.fitness_for_purpose) manifestData.fitness_for_purpose = manifest.fitness_for_purpose;
314
738
  }
315
739
 
316
740
  const core = loadJson('KDNA_Core.json');
@@ -450,8 +874,11 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
450
874
  if (!project.release) project.release = {};
451
875
  project.release.description = manifestData.description;
452
876
  }
877
+ if (manifestData.license) project.license = manifestData.license;
878
+ if (manifestData.author) project.author = manifestData.author;
453
879
  if (manifestData.languages) project.languages = manifestData.languages;
454
880
  if (manifestData.default_language) project.default_language = manifestData.default_language;
881
+ if (manifestData.raw) project.source_manifest = publicManifestMetadata(manifestData.raw);
455
882
 
456
883
  project.cards = importedCards;
457
884
  if (project.stages?.judgment_cards) {
@@ -699,16 +1126,33 @@ function cmdMigrate(args) {
699
1126
  const out = option(args, '--out');
700
1127
  const by = option(args, '--by');
701
1128
  const statement = option(args, '--statement');
702
- const name = option(args, '--name') || path.basename(path.resolve(sourceDir || '.'));
1129
+ const requestedName = option(args, '--name');
1130
+ let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
703
1131
  if (!sourceDir || !out || !by || !statement) {
704
1132
  fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pass>]');
705
1133
  }
706
1134
 
707
- // Step 1: create temp project and import everything from dev source
708
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
1135
+ const sourcePath = path.resolve(sourceDir);
1136
+ const format = option(args, '--format');
1137
+ const isStudioProject = fs.existsSync(path.join(sourcePath, 'studio.project.json'));
1138
+ let tmpDir = null;
1139
+ let projectPath = null;
1140
+ let project = null;
1141
+
1142
+ // Step 1: import dev source, or use an existing Studio project directly.
709
1143
  let creatorIdentity = null;
710
1144
  try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
711
- const { project } = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1145
+ if (format === 'v1' && isStudioProject) {
1146
+ const loaded = readProject(sourcePath);
1147
+ project = loaded.project;
1148
+ projectPath = loaded.projectPath;
1149
+ if (!requestedName && project.name) name = project.name;
1150
+ } else {
1151
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
1152
+ const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
1153
+ project = imported.project;
1154
+ projectPath = path.join(tmpDir, 'studio.project.json');
1155
+ }
712
1156
 
713
1157
  // Reject if critical judgment fields are missing from axioms
714
1158
  const criticalMissing = [];
@@ -762,7 +1206,7 @@ function cmdMigrate(args) {
762
1206
  project.stages.judgment_cards.locked = locked;
763
1207
  project.stages.judgment_cards.total = project.cards.length;
764
1208
  }
765
- writeProject(path.join(tmpDir, 'studio.project.json'), project);
1209
+ writeProject(projectPath, project);
766
1210
  console.log(`Approved and Human Locked: ${locked} cards`);
767
1211
 
768
1212
  // Step 3: verify Human Lock gate
@@ -774,37 +1218,11 @@ function cmdMigrate(args) {
774
1218
 
775
1219
  const absOut = path.resolve(out);
776
1220
  // Step 4: v1 export or v2 compile + export
777
- if (args.includes('--format') && option(args, '--format') === 'v1') {
778
- let core;
779
- try { core = require('@aikdna/kdna-core'); } catch (e) {
780
- fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
1221
+ if (format === 'v1') {
1222
+ exportProjectV1(project, name, absOut);
1223
+ if (tmpDir) {
1224
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
781
1225
  }
782
- const lockedAxioms = (project.cards || []).filter(c => c.type === 'axiom' && c.locked);
783
- const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
784
- fs.writeFileSync(path.join(v1Dir, 'mimetype'), 'application/vnd.kdna.asset');
785
- fs.writeFileSync(path.join(v1Dir, 'kdna.json'), JSON.stringify({
786
- kdna_version: '1.0', asset_id: name || 'studio:v1-export',
787
- asset_uid: 'urn:uuid:' + crypto.randomUUID(), asset_type: 'domain',
788
- title: name || 'Studio v1 Export', version: '1.0.0',
789
- judgment_version: '1.0.0',
790
- created_at: new Date().toISOString(), updated_at: new Date().toISOString(),
791
- creator: { name: 'Studio Export' },
792
- compatibility: { min_loader_version: '1.0.0', profile: 'judgment-profile-v1' },
793
- payload: { path: 'payload.kdnab', encoding: 'json', encrypted: false },
794
- lineage: { type: 'original', fork_of: null, derived_from: null },
795
- }, null, 2));
796
- fs.writeFileSync(path.join(v1Dir, 'payload.kdnab'), JSON.stringify({
797
- profile: 'judgment-profile-v1',
798
- core: { highest_question: '', axioms: lockedAxioms.map(c => ({ one_sentence: c.fields?.one_sentence || c.summary || '' })), boundaries: [], risk_model: {} },
799
- patterns: [], scenarios: [], cases: [], reasoning: { self_checks: [], failure_modes: [] },
800
- }, null, 2));
801
- core.pack(v1Dir, absOut);
802
- const vr = core.validate(absOut);
803
- if (!vr.overall_valid) fail('v1 export validation failed: ' + (vr.problems || []).join('; '));
804
- console.log('Exported (v1): ' + absOut);
805
- console.log(' Name: ' + name + ' Axioms: ' + lockedAxioms.length + ' Validated: all gates pass');
806
- try { fs.rmSync(v1Dir, { recursive: true }); } catch { /* cleanup */ }
807
- try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
808
1226
  return;
809
1227
  }
810
1228
  const { result } = compileProject(tmpDir);
@@ -827,7 +1245,37 @@ function cmdMigrate(args) {
827
1245
  console.log(` Build ID: ${result.identity.build_id}`);
828
1246
 
829
1247
  // Cleanup temp
830
- try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
1248
+ if (tmpDir) {
1249
+ try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
1250
+ }
1251
+ }
1252
+
1253
+ function exportProjectV1(project, name, outPath) {
1254
+ let core;
1255
+ try { core = require('@aikdna/kdna-core'); } catch (e) {
1256
+ fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
1257
+ }
1258
+ const gate = projectApi.checkHumanLockGate(project);
1259
+ if (gate.blocked) {
1260
+ const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
1261
+ fail(`Human Lock Gate blocked v1 export:\n - ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
1262
+ }
1263
+ const lockedCards = (project.cards || []).filter(c => c.locked);
1264
+ const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
1265
+ 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));
1270
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
1271
+ core.pack(v1Dir, outPath);
1272
+ const vr = core.validate(outPath);
1273
+ if (!vr.overall_valid) fail('v1 export validation failed: ' + (vr.problems || []).join('; '));
1274
+ console.log('Exported (v1): ' + outPath);
1275
+ console.log(' Name: ' + name + ' Cards: ' + lockedCards.length + ' Validated: all gates pass');
1276
+ } finally {
1277
+ try { fs.rmSync(v1Dir, { recursive: true }); } catch { /* cleanup */ }
1278
+ }
831
1279
  }
832
1280
 
833
1281
  function compileProject(projectInput) {
@@ -1004,7 +1452,12 @@ function applySignature(files, passphrase = null) {
1004
1452
  function cmdExport(args) {
1005
1453
  const projectInput = args[0];
1006
1454
  const out = option(args, '--out');
1007
- if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--sign]');
1455
+ if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign]');
1456
+ if (option(args, '--format') === 'v1') {
1457
+ const { project } = readProject(projectInput);
1458
+ exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out));
1459
+ return;
1460
+ }
1008
1461
  const { project, result } = compileProject(projectInput);
1009
1462
  const files = { ...result.files };
1010
1463
  files['README.md'] = compileApi.generateReadme(project);
@@ -1430,6 +1883,10 @@ if (!cmd || cmd === '--help' || cmd === '-h') {
1430
1883
  usage();
1431
1884
  process.exit(EXIT.OK);
1432
1885
  }
1886
+ if (cmd === '--version' || cmd === '-v') {
1887
+ console.log(require('../package.json').version);
1888
+ process.exit(EXIT.OK);
1889
+ }
1433
1890
 
1434
1891
  (async () => {
1435
1892
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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": {
@@ -38,10 +38,13 @@
38
38
  "node": ">=18"
39
39
  },
40
40
  "dependencies": {
41
- "@aikdna/kdna-studio-core": "^1.5.0",
42
- "@aikdna/kdna-core": "^0.11.0"
41
+ "@aikdna/kdna-core": "^0.11.1",
42
+ "@aikdna/kdna-studio-core": "^1.5.3",
43
+ "ajv": "^8.20.0",
44
+ "ajv-formats": "^3.0.1",
45
+ "cbor-x": "^1.6.4"
43
46
  },
44
47
  "devDependencies": {
45
- "@aikdna/kdna-core": "^0.11.0"
48
+ "@aikdna/kdna-core": "^0.11.1"
46
49
  }
47
50
  }
package/tests/cli.test.js CHANGED
@@ -309,6 +309,133 @@ test('create --from-folder imports legacy JSON source, outputs audit', (t) => {
309
309
  assert.equal(project.lineage.type, 'migrated');
310
310
  });
311
311
 
312
+ test('migrate --format v1 maps scoped name, writes checksums, and preserves source cards', (t) => {
313
+ assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
314
+ const tmp = tmpDir();
315
+ t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
316
+
317
+ const sourceDir = path.join(tmp, 'legacy-source');
318
+ fs.mkdirSync(sourceDir, { recursive: true });
319
+ fs.writeFileSync(path.join(sourceDir, 'kdna.json'), JSON.stringify({
320
+ name: '@test/legacy-import',
321
+ version: '0.7.3',
322
+ judgment_version: '2026.05',
323
+ asset_type: 'domain_judgment',
324
+ description: 'Legacy import fixture.',
325
+ core_insight: 'Preserve real judgment structure.',
326
+ author: { name: 'Test Author', id: 'test-author' },
327
+ license: { type: 'CC-BY-4.0' },
328
+ keywords: ['legacy', 'v1'],
329
+ languages: ['en'],
330
+ default_language: 'en',
331
+ quality_badge: 'tested',
332
+ registry: { repo: 'https://example.invalid/legacy' },
333
+ }));
334
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify({
335
+ axioms: [{
336
+ id: 'ax_legacy',
337
+ one_sentence: 'Prefer specific evidence over broad claims.',
338
+ full_statement: 'When evaluating a claim the agent must preserve specific evidence and not collapse it into generic advice.',
339
+ why: 'Without this judgment the agent produces plausible but weak summaries.',
340
+ applies_when: ['reviewing content'],
341
+ does_not_apply_when: ['pure formatting'],
342
+ failure_risk: 'May over-diagnose simple copy edits.',
343
+ }],
344
+ ontology: [{ id: 'on_legacy', essence: 'Specificity anchor', boundary: 'Not generic polish', trigger_signal: 'abstract claim' }],
345
+ frameworks: [{ id: 'fw_legacy', name: 'Diagnose before rewrite', when_to_use: 'reviewing drafts', steps: ['diagnose', 'rewrite'] }],
346
+ stances: [{ id: 'st_legacy', stance: 'Structure first', applies_when: ['draft review'], does_not_apply_when: ['grammar only'] }],
347
+ }));
348
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
349
+ terminology: {
350
+ standard_terms: [{ term: 'specificity anchor', definition: 'A concrete detail.' }],
351
+ banned_terms: [{ term: 'make it better', why: 'Non-diagnostic.', replace_with: 'name the structural issue' }],
352
+ },
353
+ misunderstandings: [{ wrong: 'Smooth means good.', correct: 'Smooth can hide weak thinking.', key_distinction: 'surface vs structure', why: 'Prevents false praise.' }],
354
+ self_check: ['Did I preserve the structural judgment?'],
355
+ }));
356
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Scenarios.json'), JSON.stringify({ scenes: [{ id: 'sc_legacy', name: 'Draft review', trigger: 'user asks for review', action: 'diagnose first' }] }));
357
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Cases.json'), JSON.stringify({ cases: [{ id: 'ca_legacy', title: 'Smooth but empty', scenario: 'draft reads well', expected: 'flag missing claim' }] }));
358
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Reasoning.json'), JSON.stringify({ reasoning_chains: [{ id: 're_legacy', name: 'structure-first', conclusion: 'diagnose structure first', so_what: 'avoid surface polish' }] }));
359
+ fs.writeFileSync(path.join(sourceDir, 'KDNA_Evolution.json'), JSON.stringify({ stages: [{ id: 'ev_legacy', name: 'Structural reviewer', description: 'Moves beyond surface editing.' }] }));
360
+
361
+ const outFile = path.join(tmp, 'legacy.kdna');
362
+ const result = run([
363
+ 'migrate', sourceDir,
364
+ '--out', outFile,
365
+ '--name', '@test/legacy-import',
366
+ '--by', 'tester',
367
+ '--statement', 'I confirm this v1 migration fixture.',
368
+ '--format', 'v1',
369
+ ], { tmp });
370
+ assert.equal(result.status, 0, result.stderr);
371
+
372
+ const validation = kdnaCore.validate(outFile);
373
+ assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
374
+ const inspected = kdnaCore.inspect(outFile);
375
+ assert.equal(inspected.asset_id, 'kdna:test:legacy-import');
376
+ assert.equal(inspected.checksums_present, true);
377
+
378
+ const full = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
379
+ assert.equal(full.manifest.creator.name, 'Test Author');
380
+ assert.equal(full.manifest.version, '0.7.3');
381
+ assert.equal(full.manifest.judgment_version, '2026.05.0');
382
+ 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
+ assert.equal(full.payload.scenarios.length, 1);
390
+ assert.equal(full.payload.cases.length, 1);
391
+ assert.equal(full.payload.reasoning.self_checks.length, 1);
392
+ 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);
396
+ assert.equal(JSON.stringify(full).includes('quality_badge'), false);
397
+ assert.equal(JSON.stringify(full).includes('registry'), false);
398
+ });
399
+
400
+ test('export --format v1 exports a Studio project as a valid non-empty v1 asset', (t) => {
401
+ assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
402
+ const { tmp, projectDir } = createLockedProject(t);
403
+ const outFile = path.join(tmp, 'project-v1.kdna');
404
+
405
+ const result = run(['export', projectDir, '--format', 'v1', '--out', outFile], { tmp });
406
+ assert.equal(result.status, 0, result.stderr);
407
+ assert.match(result.stdout, /Exported \(v1\)/);
408
+
409
+ const validation = kdnaCore.validate(outFile);
410
+ assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
411
+ const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
412
+ 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'));
415
+ });
416
+
417
+ test('migrate --format v1 accepts an existing Studio project without dropping cards', (t) => {
418
+ assert.ok(kdnaCore, '@aikdna/kdna-core is required for v1 export verification');
419
+ const { tmp, projectDir } = createLockedProject(t);
420
+ const outFile = path.join(tmp, 'project-migrated-v1.kdna');
421
+
422
+ const result = run([
423
+ 'migrate', projectDir,
424
+ '--format', 'v1',
425
+ '--out', outFile,
426
+ '--by', 'tester',
427
+ '--statement', 'I confirm this project v1 migration.',
428
+ ], { tmp });
429
+ assert.equal(result.status, 0, result.stderr);
430
+ assert.match(result.stdout, /Cards: 1/);
431
+
432
+ const validation = kdnaCore.validate(outFile);
433
+ assert.equal(validation.overall_valid, true, JSON.stringify(validation.problems));
434
+ const loaded = kdnaCore.loadV1(outFile, { profile: 'full', as: 'json' }).content;
435
+ assert.equal(loaded.payload.core.axioms.length, 1);
436
+ assert.equal(loaded.payload.source_cards.length, 1);
437
+ });
438
+
312
439
  // ── Create from KDNA ────────────────────────────────────────────────
313
440
 
314
441
  test('create --from-kdna forks an existing .kdna asset', (t) => {