@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 +7 -5
- package/bin/kdna-studio.js +500 -43
- package/package.json +7 -4
- package/tests/cli.test.js +127 -0
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
|
|
62
|
-
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
|
|
66
|
-
control plane for
|
|
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
|
|
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
|
package/bin/kdna-studio.js
CHANGED
|
@@ -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>
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
708
|
-
const
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
42
|
-
"@aikdna/kdna-core": "^
|
|
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.
|
|
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) => {
|