@aikdna/kdna-studio-cli 0.1.2 → 0.2.0
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/bin/kdna-studio.js +351 -15
- package/package.json +2 -2
- package/tests/cli.test.js +282 -0
package/bin/kdna-studio.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
evidence: evidenceApi,
|
|
12
12
|
compile: compileApi,
|
|
13
13
|
quality,
|
|
14
|
+
creator: creatorApi,
|
|
14
15
|
} = require('@aikdna/kdna-studio-core');
|
|
15
16
|
|
|
16
17
|
const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
@@ -18,12 +19,20 @@ const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
|
18
19
|
function usage() {
|
|
19
20
|
console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
kdna-studio
|
|
22
|
+
Identity:
|
|
23
|
+
kdna-studio identity init [--name <display-name>]
|
|
24
|
+
kdna-studio identity show
|
|
25
|
+
|
|
26
|
+
Create (three entry paths):
|
|
27
|
+
kdna-studio create <project-dir> --name <@scope/name> # blank
|
|
28
|
+
kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
|
|
29
|
+
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
30
|
+
|
|
31
|
+
Authoring:
|
|
23
32
|
kdna-studio import <project> <source-file>
|
|
24
33
|
kdna-studio card list <project>
|
|
25
34
|
kdna-studio card add <project> <type> --field key=value [--field key=value]
|
|
26
|
-
kdna-studio card approve <project> <card-id> --by <id> --statement <text>
|
|
35
|
+
kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]
|
|
27
36
|
kdna-studio lock <project>
|
|
28
37
|
kdna-studio compile <project> --out <dir>
|
|
29
38
|
kdna-studio export <project> --out <file.kdna> [--sign]
|
|
@@ -110,18 +119,267 @@ function parseFields(args) {
|
|
|
110
119
|
|
|
111
120
|
function cmdCreate(args) {
|
|
112
121
|
const dir = args[0];
|
|
113
|
-
if (!dir) fail('Usage: kdna-studio create <project-dir>
|
|
122
|
+
if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--from-kdna <file> | --from-folder <dir>]');
|
|
114
123
|
const abs = path.resolve(dir);
|
|
115
124
|
if (fs.existsSync(abs)) fail(`Directory already exists: ${abs}`);
|
|
125
|
+
|
|
116
126
|
const name = option(args, '--name', path.basename(abs));
|
|
127
|
+
const fromKdna = option(args, '--from-kdna');
|
|
128
|
+
const fromFolder = option(args, '--from-folder');
|
|
129
|
+
|
|
130
|
+
let sourceMode = 'blank';
|
|
131
|
+
let lineage = null;
|
|
132
|
+
let creatorIdentity = null;
|
|
133
|
+
|
|
134
|
+
// Load creator identity if available
|
|
135
|
+
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* no identity yet */ }
|
|
136
|
+
|
|
137
|
+
if (fromKdna) {
|
|
138
|
+
sourceMode = 'kdna_asset';
|
|
139
|
+
const kdnaData = importFromKdna(fromKdna);
|
|
140
|
+
lineage = kdnaData.lineage;
|
|
141
|
+
if (kdnaData.cards) {
|
|
142
|
+
const project = projectApi.createProject(name, 'domain', {
|
|
143
|
+
author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
|
|
144
|
+
sourceMode,
|
|
145
|
+
creatorIdentity,
|
|
146
|
+
lineage,
|
|
147
|
+
});
|
|
148
|
+
project.cards = kdnaData.cards;
|
|
149
|
+
if (project.stages?.judgment_cards) {
|
|
150
|
+
project.stages.judgment_cards.total = kdnaData.cards.length;
|
|
151
|
+
project.stages.judgment_cards.status = 'in_progress';
|
|
152
|
+
}
|
|
153
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
154
|
+
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
155
|
+
console.log(`Created Studio project (source_mode: ${sourceMode}, imported: ${kdnaData.cards.length} cards): ${abs}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
} else if (fromFolder) {
|
|
159
|
+
sourceMode = 'source_folder';
|
|
160
|
+
importFromFolder(fromFolder, abs, name);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
117
164
|
const project = projectApi.createProject(name, 'domain', {
|
|
118
165
|
author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
|
|
166
|
+
sourceMode,
|
|
167
|
+
creatorIdentity,
|
|
168
|
+
lineage,
|
|
169
|
+
sourcePath: fromFolder ? path.resolve(fromFolder) : null,
|
|
119
170
|
});
|
|
171
|
+
|
|
172
|
+
if (fromKdna || fromFolder) {
|
|
173
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
174
|
+
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
175
|
+
if (fromFolder) {
|
|
176
|
+
// Cards were already added by importFromFolder
|
|
177
|
+
}
|
|
178
|
+
console.log(`Created Studio project (source_mode: ${sourceMode}): ${abs}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
120
182
|
fs.mkdirSync(abs, { recursive: true });
|
|
121
183
|
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
122
184
|
console.log(`Created Studio project: ${abs}`);
|
|
123
185
|
}
|
|
124
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Import cards from an existing .kdna asset into a new Studio project.
|
|
189
|
+
* Cards are imported as draft — they do NOT inherit trust from the source.
|
|
190
|
+
*/
|
|
191
|
+
function importFromKdna(kdnaPath) {
|
|
192
|
+
const absKdna = path.resolve(kdnaPath);
|
|
193
|
+
if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
|
|
194
|
+
if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
|
|
195
|
+
|
|
196
|
+
const zipBuf = fs.readFileSync(absKdna);
|
|
197
|
+
const entries = readZipEntries(zipBuf);
|
|
198
|
+
if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
|
|
199
|
+
|
|
200
|
+
const manifest = JSON.parse(entries.get('kdna.json').toString());
|
|
201
|
+
const lineage = {
|
|
202
|
+
type: 'fork',
|
|
203
|
+
parent_name: manifest.name || null,
|
|
204
|
+
parent_asset_uid: manifest.asset_uid || null,
|
|
205
|
+
parent_version: manifest.version || null,
|
|
206
|
+
parent_asset_digest: manifest.content_digest || null,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Extract cards from KDNA files
|
|
210
|
+
const importedCards = [];
|
|
211
|
+
if (entries.has('KDNA_Core.json')) {
|
|
212
|
+
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
213
|
+
for (const ax of (core.axioms || [])) {
|
|
214
|
+
const fields = {};
|
|
215
|
+
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
216
|
+
if (k in ax) fields[k] = ax[k];
|
|
217
|
+
}
|
|
218
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
219
|
+
}
|
|
220
|
+
for (const ont of (core.ontology || [])) {
|
|
221
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
222
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
223
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
224
|
+
trigger_signal: ont.trigger_signal || '',
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
for (const b of (core.boundaries || [])) {
|
|
228
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
229
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
230
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (entries.has('KDNA_Patterns.json')) {
|
|
235
|
+
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
236
|
+
for (const ms of (pat.misunderstandings || [])) {
|
|
237
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
238
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
239
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
for (const sc of (pat.self_check || [])) {
|
|
243
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
244
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { lineage, cards: importedCards };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Import cards from a legacy source folder into a new Studio project.
|
|
253
|
+
* Reads KDNA_*.json files and converts entries to draft judgment cards.
|
|
254
|
+
* Outputs a schema audit report.
|
|
255
|
+
*/
|
|
256
|
+
function importFromFolder(sourceDir, projectDir, projectName) {
|
|
257
|
+
const absSource = path.resolve(sourceDir);
|
|
258
|
+
if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
|
|
259
|
+
if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
|
|
260
|
+
|
|
261
|
+
const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
|
|
262
|
+
const importedCards = [];
|
|
263
|
+
|
|
264
|
+
function loadJson(filename) {
|
|
265
|
+
const p = path.join(absSource, filename);
|
|
266
|
+
if (!fs.existsSync(p)) return null;
|
|
267
|
+
audit.filesFound.push(filename);
|
|
268
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
269
|
+
catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const core = loadJson('KDNA_Core.json');
|
|
273
|
+
const patterns = loadJson('KDNA_Patterns.json');
|
|
274
|
+
const scenarios = loadJson('KDNA_Scenarios.json');
|
|
275
|
+
const cases = loadJson('KDNA_Cases.json');
|
|
276
|
+
const reasoning = loadJson('KDNA_Reasoning.json');
|
|
277
|
+
const evolution = loadJson('KDNA_Evolution.json');
|
|
278
|
+
|
|
279
|
+
if (core) {
|
|
280
|
+
for (const ax of (core.axioms || [])) {
|
|
281
|
+
const fields = {};
|
|
282
|
+
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
283
|
+
if (key in ax) fields[key] = ax[key];
|
|
284
|
+
else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
|
|
285
|
+
}
|
|
286
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
287
|
+
}
|
|
288
|
+
for (const ont of (core.ontology || [])) {
|
|
289
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
290
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
291
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
292
|
+
trigger_signal: ont.trigger_signal || '',
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
for (const b of (core.boundaries || [])) {
|
|
296
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
297
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
298
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
for (const r of (core.risks || core.risk_model || [])) {
|
|
302
|
+
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (patterns) {
|
|
307
|
+
for (const ms of (patterns.misunderstandings || [])) {
|
|
308
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
309
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
310
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
for (const sc of (patterns.self_check || [])) {
|
|
314
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
315
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
audit.cardsImported = importedCards.length;
|
|
320
|
+
|
|
321
|
+
const project = projectApi.createProject(projectName, 'domain', {
|
|
322
|
+
sourceMode: 'source_folder',
|
|
323
|
+
sourcePath: absSource,
|
|
324
|
+
creatorIdentity: null,
|
|
325
|
+
lineage: { type: 'migrated', parent_name: null, parent_asset_uid: null, parent_version: null, parent_asset_digest: null },
|
|
326
|
+
});
|
|
327
|
+
project.cards = importedCards;
|
|
328
|
+
if (project.stages?.judgment_cards) {
|
|
329
|
+
project.stages.judgment_cards.total = importedCards.length;
|
|
330
|
+
project.stages.judgment_cards.status = 'in_progress';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
334
|
+
writeProject(path.join(projectDir, 'studio.project.json'), project);
|
|
335
|
+
console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Minimal ZIP central directory parser — reads entries into a Map.
|
|
340
|
+
*/
|
|
341
|
+
function readZipEntries(buf) {
|
|
342
|
+
const entries = new Map();
|
|
343
|
+
// Find end-of-central-directory record
|
|
344
|
+
let eocdOffset = buf.length - 22;
|
|
345
|
+
while (eocdOffset >= 0) {
|
|
346
|
+
if (buf.readUInt32LE(eocdOffset) === 0x06054b50) break;
|
|
347
|
+
eocdOffset--;
|
|
348
|
+
}
|
|
349
|
+
if (eocdOffset < 0) throw new Error('Not a valid ZIP file');
|
|
350
|
+
|
|
351
|
+
const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
352
|
+
let offset = centralDirOffset;
|
|
353
|
+
|
|
354
|
+
while (offset < eocdOffset) {
|
|
355
|
+
const sig = buf.readUInt32LE(offset);
|
|
356
|
+
if (sig !== 0x02014b50) break;
|
|
357
|
+
const nameLen = buf.readUInt16LE(offset + 28);
|
|
358
|
+
const extraLen = buf.readUInt16LE(offset + 30);
|
|
359
|
+
const commentLen = buf.readUInt16LE(offset + 32);
|
|
360
|
+
const compMethod = buf.readUInt16LE(offset + 10);
|
|
361
|
+
const compSize = buf.readUInt32LE(offset + 20);
|
|
362
|
+
const uncompSize = buf.readUInt32LE(offset + 24);
|
|
363
|
+
const localOffset = buf.readUInt32LE(offset + 42);
|
|
364
|
+
const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
|
|
365
|
+
|
|
366
|
+
// Read local file header to get data
|
|
367
|
+
const localNameLen = buf.readUInt16LE(localOffset + 26);
|
|
368
|
+
const localExtraLen = buf.readUInt16LE(localOffset + 28);
|
|
369
|
+
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
|
370
|
+
|
|
371
|
+
if (compMethod === 0) {
|
|
372
|
+
entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
|
|
373
|
+
} else if (compMethod === 8) {
|
|
374
|
+
entries.set(name, zlib.inflateRawSync(buf.subarray(dataStart, dataStart + compSize)));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
offset += 46 + nameLen + extraLen + commentLen;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return entries;
|
|
381
|
+
}
|
|
382
|
+
|
|
125
383
|
function cmdImport(args) {
|
|
126
384
|
const [projectInput, source] = args;
|
|
127
385
|
if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file>');
|
|
@@ -163,7 +421,7 @@ function cmdCard(args) {
|
|
|
163
421
|
const projectInput = args[1];
|
|
164
422
|
const cardId = args[2];
|
|
165
423
|
if (!projectInput || !cardId) {
|
|
166
|
-
fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text>');
|
|
424
|
+
fail('Usage: kdna-studio card approve <project> <card-id> --by <id> --statement <text> [--sign]');
|
|
167
425
|
}
|
|
168
426
|
const by = option(args, '--by');
|
|
169
427
|
const statement = option(args, '--statement');
|
|
@@ -173,17 +431,35 @@ function cmdCard(args) {
|
|
|
173
431
|
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
174
432
|
let card = project.cards[idx];
|
|
175
433
|
if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
|
|
176
|
-
|
|
434
|
+
|
|
435
|
+
const lockPayload = {
|
|
177
436
|
by,
|
|
178
437
|
statement,
|
|
179
438
|
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
|
|
180
|
-
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Sign Human Lock with creator identity if --sign is passed
|
|
442
|
+
if (args.includes('--sign')) {
|
|
443
|
+
const identity = creatorApi.loadIdentity();
|
|
444
|
+
if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
|
|
445
|
+
lockPayload.creator_id = identity.creator_id;
|
|
446
|
+
try {
|
|
447
|
+
lockPayload.signature = creatorApi.signHumanLock(
|
|
448
|
+
cardId, statement, cardApi.cardJudgmentFingerprint(card),
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
project.cards[idx] = cardApi.lockCard(card, lockPayload);
|
|
181
456
|
if (project.stages?.judgment_cards) {
|
|
182
457
|
project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
|
|
183
458
|
project.stages.judgment_cards.total = project.cards.length;
|
|
184
459
|
}
|
|
185
460
|
writeProject(projectPath, project);
|
|
186
|
-
|
|
461
|
+
const signed = lockPayload.signature ? ' (signed)' : '';
|
|
462
|
+
console.log(`Approved and Human Locked: ${cardId}${signed}`);
|
|
187
463
|
return;
|
|
188
464
|
}
|
|
189
465
|
fail('Usage: kdna-studio card <list|add|approve> ...');
|
|
@@ -320,6 +596,7 @@ function manifestForSigning(manifest) {
|
|
|
320
596
|
delete copy.signature;
|
|
321
597
|
delete copy.asset_digest;
|
|
322
598
|
delete copy.container_sha256;
|
|
599
|
+
delete copy.content_digest;
|
|
323
600
|
return copy;
|
|
324
601
|
}
|
|
325
602
|
|
|
@@ -372,21 +649,48 @@ function cmdExport(args) {
|
|
|
372
649
|
const files = { ...result.files };
|
|
373
650
|
files['README.md'] = compileApi.generateReadme(project);
|
|
374
651
|
files.LICENSE = project.license?.type || 'UNSPECIFIED';
|
|
652
|
+
files.mimetype = 'application/vnd.aikdna.kdna+zip';
|
|
653
|
+
|
|
654
|
+
// Recompute content_digest after README.md / LICENSE / mimetype are added
|
|
655
|
+
// so that manifest, receipt, and provenance report all agree with the final ZIP contents.
|
|
656
|
+
const finalDigest = compileApi.computeContentDigest(files);
|
|
657
|
+
result.identity.content_digest = finalDigest;
|
|
658
|
+
|
|
659
|
+
// Update kdna.json manifest with final digest
|
|
660
|
+
const manifest = JSON.parse(files['kdna.json']);
|
|
661
|
+
manifest.content_digest = finalDigest;
|
|
662
|
+
if (manifest.authoring) manifest.authoring.content_digest = finalDigest;
|
|
663
|
+
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
664
|
+
|
|
665
|
+
// Update build-receipt.json
|
|
666
|
+
let receiptData = JSON.parse(files['build-receipt.json']);
|
|
667
|
+
receiptData.content_digest = finalDigest;
|
|
668
|
+
files['build-receipt.json'] = JSON.stringify(receiptData, null, 2);
|
|
669
|
+
|
|
670
|
+
// Update provenance report
|
|
671
|
+
const provenance = JSON.parse(files['reports/provenance-report.json']);
|
|
672
|
+
provenance.content_digest = finalDigest;
|
|
673
|
+
provenance.content_fingerprint = finalDigest;
|
|
674
|
+
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
675
|
+
|
|
375
676
|
if (args.includes('--sign')) applySignature(files);
|
|
376
677
|
|
|
377
|
-
const entries = [['mimetype',
|
|
378
|
-
for (const name of Object.keys(files).sort())
|
|
678
|
+
const entries = [['mimetype', files.mimetype]];
|
|
679
|
+
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
680
|
+
entries.push([name, files[name]]);
|
|
681
|
+
}
|
|
379
682
|
const zip = buildZip(entries);
|
|
380
683
|
const outPath = path.resolve(out);
|
|
381
684
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
382
685
|
fs.writeFileSync(outPath, zip);
|
|
383
686
|
|
|
384
687
|
const assetDigest = `sha256:${crypto.createHash('sha256').update(zip).digest('hex')}`;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(
|
|
688
|
+
receiptData = JSON.parse(files['build-receipt.json']);
|
|
689
|
+
receiptData.asset_path = outPath;
|
|
690
|
+
receiptData.asset_digest = assetDigest;
|
|
691
|
+
receiptData.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
|
|
692
|
+
fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receiptData, null, 2));
|
|
693
|
+
fs.writeFileSync(path.join(path.dirname(outPath), 'kdna.json'), files['kdna.json']);
|
|
390
694
|
fs.writeFileSync(path.join(path.dirname(outPath), 'provenance-report.json'), files['reports/provenance-report.json']);
|
|
391
695
|
fs.writeFileSync(path.join(path.dirname(outPath), 'quality-gate-report.json'), files['reports/quality-gate-report.json']);
|
|
392
696
|
fs.writeFileSync(path.join(path.dirname(outPath), 'human-lock-report.json'), files['reports/human-lock-report.json']);
|
|
@@ -396,6 +700,37 @@ function cmdExport(args) {
|
|
|
396
700
|
console.log(`Build ID: ${result.identity.build_id}`);
|
|
397
701
|
}
|
|
398
702
|
|
|
703
|
+
function cmdIdentity(args) {
|
|
704
|
+
const sub = args[0];
|
|
705
|
+
if (sub === 'init') {
|
|
706
|
+
const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
|
|
707
|
+
try {
|
|
708
|
+
const identity = creatorApi.initIdentity(name);
|
|
709
|
+
console.log(`Creator identity initialized:`);
|
|
710
|
+
console.log(` creator_id: ${identity.creator_id}`);
|
|
711
|
+
console.log(` display_name: ${identity.display_name}`);
|
|
712
|
+
console.log(` identity_dir: ${identity.identity_dir}`);
|
|
713
|
+
console.log(` public_key saved: ${identity.public_key_path}`);
|
|
714
|
+
console.log(`\nBack up your private key at: ${creatorApi.privateKeyPath()}`);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
fail(`Identity already exists. Use 'kdna-studio identity show' to view. ${err.message}`);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (sub === 'show') {
|
|
721
|
+
const identity = creatorApi.loadIdentity();
|
|
722
|
+
if (!identity) fail('No identity found. Run: kdna-studio identity init --name "Your Name"');
|
|
723
|
+
console.log(JSON.stringify({
|
|
724
|
+
creator_id: identity.creator_id,
|
|
725
|
+
display_name: identity.display_name,
|
|
726
|
+
verified: identity.verified || false,
|
|
727
|
+
created_at: identity.created_at,
|
|
728
|
+
}, null, 2));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
fail('Usage: kdna-studio identity <init|show>');
|
|
732
|
+
}
|
|
733
|
+
|
|
399
734
|
function cmdReport(args) {
|
|
400
735
|
const { project } = readProject(args[0]);
|
|
401
736
|
const readiness = quality.computeReadiness(project);
|
|
@@ -418,6 +753,7 @@ try {
|
|
|
418
753
|
else if (cmd === 'lock') cmdLock(args.slice(1));
|
|
419
754
|
else if (cmd === 'compile') cmdCompile(args.slice(1));
|
|
420
755
|
else if (cmd === 'export') cmdExport(args.slice(1));
|
|
756
|
+
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
421
757
|
else if (cmd === 'report') cmdReport(args.slice(1));
|
|
422
758
|
else {
|
|
423
759
|
usage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Official KDNA Studio command-line authoring entry for creating, locking, compiling, and exporting trusted .kdna assets.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"node": ">=18"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aikdna/kdna-studio-core": "^1.
|
|
40
|
+
"@aikdna/kdna-studio-core": "^1.4.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/tests/cli.test.js
CHANGED
|
@@ -14,6 +14,7 @@ function run(args, options = {}) {
|
|
|
14
14
|
env: {
|
|
15
15
|
...process.env,
|
|
16
16
|
KDNA_IDENTITY_DIR: path.join(options.tmp || os.tmpdir(), 'no-identity'),
|
|
17
|
+
...(options.env || {}),
|
|
17
18
|
},
|
|
18
19
|
encoding: 'utf8',
|
|
19
20
|
});
|
|
@@ -43,6 +44,10 @@ function createLockedProject(t) {
|
|
|
43
44
|
'--field',
|
|
44
45
|
'one_sentence=Prefer specific evidence over broad claims',
|
|
45
46
|
'--field',
|
|
47
|
+
'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.',
|
|
48
|
+
'--field',
|
|
49
|
+
'why=Without this axiom the agent may accept broad plausible-sounding claims as equally credible as narrow evidence-backed ones leading to false balance.',
|
|
50
|
+
'--field',
|
|
46
51
|
'applies_when=["reviewing content"]',
|
|
47
52
|
'--field',
|
|
48
53
|
'does_not_apply_when=["pure formatting"]',
|
|
@@ -146,6 +151,11 @@ test('runs the trusted authoring workflow through compile and export', (t) => {
|
|
|
146
151
|
assert.equal(manifest.authoring.human_confirmed, true);
|
|
147
152
|
assert.ok(manifest.authoring.human_lock_count >= 1);
|
|
148
153
|
|
|
154
|
+
// Content digest must be present and non-trivial
|
|
155
|
+
assert.ok(manifest.content_digest, 'manifest must have content_digest');
|
|
156
|
+
assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
|
|
157
|
+
assert.ok(manifest.authoring.content_digest, 'authoring must have content_digest');
|
|
158
|
+
|
|
149
159
|
const outFile = path.join(tmp, 'dist', 'example.kdna');
|
|
150
160
|
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
151
161
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -155,6 +165,19 @@ test('runs the trusted authoring workflow through compile and export', (t) => {
|
|
|
155
165
|
const receipt = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'build-receipt.json'), 'utf8'));
|
|
156
166
|
assert.equal(receipt.signature_status, 'unsigned');
|
|
157
167
|
assert.match(receipt.asset_digest, /^sha256:/);
|
|
168
|
+
|
|
169
|
+
// ── Digest chain consistency ──────────────────────────────────
|
|
170
|
+
// All three sources MUST agree on content_digest.
|
|
171
|
+
const exportedManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'kdna.json'), 'utf8'));
|
|
172
|
+
const exportedProvenance = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'provenance-report.json'), 'utf8'));
|
|
173
|
+
assert.equal(exportedManifest.content_digest, receipt.content_digest,
|
|
174
|
+
'manifest.content_digest !== build-receipt.content_digest');
|
|
175
|
+
assert.equal(exportedProvenance.content_digest, receipt.content_digest,
|
|
176
|
+
'provenance-report.content_digest !== build-receipt.content_digest');
|
|
177
|
+
assert.ok(exportedManifest.content_digest.startsWith('sha256:'),
|
|
178
|
+
'content_digest must be sha256: prefix');
|
|
179
|
+
assert.equal(exportedManifest.content_digest.length, 71,
|
|
180
|
+
'content_digest must be sha256: + 64 hex chars');
|
|
158
181
|
});
|
|
159
182
|
|
|
160
183
|
test('refuses signing without runtime identity keys', (t) => {
|
|
@@ -164,3 +187,262 @@ test('refuses signing without runtime identity keys', (t) => {
|
|
|
164
187
|
assert.equal(result.status, 5);
|
|
165
188
|
assert.match(result.stderr, /Signing requires KDNA identity keys/);
|
|
166
189
|
});
|
|
190
|
+
|
|
191
|
+
// ── Identity ────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
test('identity init creates local creator identity', (t) => {
|
|
194
|
+
const tmp = tmpDir();
|
|
195
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
196
|
+
const identityDir = path.join(tmp, 'identity');
|
|
197
|
+
|
|
198
|
+
const result = run(['identity', 'init', '--name', 'Test Creator'], {
|
|
199
|
+
tmp,
|
|
200
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
201
|
+
});
|
|
202
|
+
assert.equal(result.status, 0, result.stderr);
|
|
203
|
+
assert.match(result.stdout, /Creator identity initialized/);
|
|
204
|
+
assert.match(result.stdout, /kdna:creator:ed25519:/);
|
|
205
|
+
|
|
206
|
+
// Verify files were created
|
|
207
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'kdna.key')));
|
|
208
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'kdna.pub')));
|
|
209
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'creator.json')));
|
|
210
|
+
const creatorJson = JSON.parse(fs.readFileSync(path.join(identityDir, 'creator.json'), 'utf8'));
|
|
211
|
+
assert.match(creatorJson.creator_id, /^kdna:creator:ed25519:/);
|
|
212
|
+
assert.equal(creatorJson.display_name, 'Test Creator');
|
|
213
|
+
assert.equal(creatorJson.verified, false);
|
|
214
|
+
|
|
215
|
+
// Double init should fail
|
|
216
|
+
const result2 = run(['identity', 'init', '--name', 'Duplicate'], {
|
|
217
|
+
tmp,
|
|
218
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
219
|
+
});
|
|
220
|
+
assert.equal(result2.status, 2);
|
|
221
|
+
assert.match(result2.stderr, /already exists/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('identity show displays current identity', (t) => {
|
|
225
|
+
const tmp = tmpDir();
|
|
226
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
227
|
+
const identityDir = path.join(tmp, 'identity');
|
|
228
|
+
|
|
229
|
+
// Init first
|
|
230
|
+
run(['identity', 'init', '--name', 'Test User'], {
|
|
231
|
+
tmp,
|
|
232
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result = run(['identity', 'show'], {
|
|
236
|
+
tmp,
|
|
237
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
238
|
+
});
|
|
239
|
+
assert.equal(result.status, 0, result.stderr);
|
|
240
|
+
const info = JSON.parse(result.stdout);
|
|
241
|
+
assert.match(info.creator_id, /^kdna:creator:ed25519:/);
|
|
242
|
+
assert.equal(info.display_name, 'Test User');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('identity show fails when not initialized', (t) => {
|
|
246
|
+
const tmp = tmpDir();
|
|
247
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
248
|
+
|
|
249
|
+
const result = run(['identity', 'show'], {
|
|
250
|
+
tmp,
|
|
251
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: path.join(tmp, 'no-such') },
|
|
252
|
+
});
|
|
253
|
+
assert.equal(result.status, 2);
|
|
254
|
+
assert.match(result.stderr, /No identity found/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ── Create from folder ──────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
test('create --from-folder imports legacy JSON source, outputs audit', (t) => {
|
|
260
|
+
const tmp = tmpDir();
|
|
261
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
262
|
+
|
|
263
|
+
const sourceDir = path.join(tmp, 'legacy-source');
|
|
264
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
265
|
+
|
|
266
|
+
// Create legacy KDNA files
|
|
267
|
+
const core = {
|
|
268
|
+
meta: { domain: 'test', version: '0.1.0', created: '2026-01-01', purpose: 'test', load_condition: 'always' },
|
|
269
|
+
axioms: [{
|
|
270
|
+
id: 'ax_legacy',
|
|
271
|
+
one_sentence: 'Test axiom from legacy folder.',
|
|
272
|
+
full_statement: 'A complete testable explanation from a legacy source folder.',
|
|
273
|
+
why: 'Without this legacy axiom, agent judgment defaults to untrained behavior.',
|
|
274
|
+
applies_when: ['testing legacy import'],
|
|
275
|
+
does_not_apply_when: ['production'],
|
|
276
|
+
failure_risk: 'May produce incorrect results from untrained judgment.',
|
|
277
|
+
}],
|
|
278
|
+
ontology: [{ id: 'ont_legacy', one_sentence: 'Legacy concept.', essence: 'Operational meaning.', boundary: 'Not the opposite.', trigger_signal: 'keyword' }],
|
|
279
|
+
boundaries: [{ id: 'bnd_legacy', scope: 'Legacy scope.', out_of_scope: 'Not covered by legacy.', acceptable_exceptions: [] }],
|
|
280
|
+
stances: [],
|
|
281
|
+
};
|
|
282
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify(core));
|
|
283
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
|
|
284
|
+
meta: { domain: 'test', version: '0.1.0', created: '2026-01-01', purpose: 'patterns', load_condition: 'always' },
|
|
285
|
+
misunderstandings: [{ id: 'ms_legacy', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key distinction between wrong and correct.' }],
|
|
286
|
+
self_check: ['Did I verify the legacy import?'],
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
const projectDir = path.join(tmp, 'project');
|
|
290
|
+
const result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/legacy-import'], { tmp });
|
|
291
|
+
assert.equal(result.status, 0, result.stderr);
|
|
292
|
+
|
|
293
|
+
const output = JSON.parse(result.stdout);
|
|
294
|
+
assert.ok(output.audit, 'must have audit info');
|
|
295
|
+
assert.ok(output.audit.filesFound.includes('KDNA_Core.json'));
|
|
296
|
+
assert.ok(output.audit.filesFound.includes('KDNA_Patterns.json'));
|
|
297
|
+
assert.ok(output.imported > 0, `imported cards: ${output.imported}`);
|
|
298
|
+
assert.equal(output.source_mode, 'source_folder');
|
|
299
|
+
|
|
300
|
+
// Read project and verify cards were imported
|
|
301
|
+
const projectPath = path.join(projectDir, 'studio.project.json');
|
|
302
|
+
assert.ok(fs.existsSync(projectPath));
|
|
303
|
+
const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
304
|
+
assert.equal(project.source_mode, 'source_folder');
|
|
305
|
+
assert.ok(project.cards.length > 0);
|
|
306
|
+
assert.equal(project.lineage.type, 'migrated');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ── Create from KDNA ────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
test('create --from-kdna forks an existing .kdna asset', (t) => {
|
|
312
|
+
const tmp = tmpDir();
|
|
313
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
314
|
+
|
|
315
|
+
// First create and export a "parent" project
|
|
316
|
+
const parentTmp = tmpDir();
|
|
317
|
+
t.after(() => fs.rmSync(parentTmp, { recursive: true, force: true }));
|
|
318
|
+
const { projectDir: parentDir } = createLockedProject(t);
|
|
319
|
+
const parentDist = path.join(parentTmp, 'dist');
|
|
320
|
+
fs.mkdirSync(parentDist, { recursive: true });
|
|
321
|
+
const parentKdna = path.join(parentDist, 'parent.kdna');
|
|
322
|
+
run(['export', parentDir, '--out', parentKdna], { tmp: parentTmp });
|
|
323
|
+
|
|
324
|
+
// Now fork from that .kdna
|
|
325
|
+
const projectDir = path.join(tmp, 'forked-project');
|
|
326
|
+
const result = run(['create', projectDir, '--from-kdna', parentKdna, '--name', '@test/fork'], { tmp });
|
|
327
|
+
assert.equal(result.status, 0, result.stderr);
|
|
328
|
+
assert.match(result.stdout, /Created Studio project/);
|
|
329
|
+
assert.match(result.stdout, /kdna_asset/);
|
|
330
|
+
|
|
331
|
+
const projectPath = path.join(projectDir, 'studio.project.json');
|
|
332
|
+
assert.ok(fs.existsSync(projectPath));
|
|
333
|
+
const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
334
|
+
assert.equal(project.source_mode, 'kdna_asset');
|
|
335
|
+
assert.ok(project.lineage, 'must have lineage');
|
|
336
|
+
assert.equal(project.lineage.type, 'fork');
|
|
337
|
+
assert.ok(project.lineage.parent_name, 'must record parent name');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ── Card approve --sign ─────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
test('card approve --sign binds Human Lock to creator identity', (t) => {
|
|
343
|
+
const tmp = tmpDir();
|
|
344
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
345
|
+
const identityDir = path.join(tmp, 'identity');
|
|
346
|
+
|
|
347
|
+
// Initialize creator identity
|
|
348
|
+
const init = run(['identity', 'init', '--name', 'Signer'], {
|
|
349
|
+
tmp,
|
|
350
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
351
|
+
});
|
|
352
|
+
assert.equal(init.status, 0, init.stderr);
|
|
353
|
+
|
|
354
|
+
// Create project with an axiom
|
|
355
|
+
const projectDir = path.join(tmp, 'project');
|
|
356
|
+
let result = run(['create', projectDir, '--name', '@test/signed'], { tmp });
|
|
357
|
+
assert.equal(result.status, 0, result.stderr);
|
|
358
|
+
|
|
359
|
+
result = run([
|
|
360
|
+
'card', 'add', projectDir, 'axiom',
|
|
361
|
+
'--field', 'one_sentence=Prefer specific evidence over broad claims',
|
|
362
|
+
'--field', '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.',
|
|
363
|
+
'--field', 'why=Without this axiom the agent may accept broad plausible-sounding claims as equally credible as narrow evidence-backed ones.',
|
|
364
|
+
'--field', 'applies_when=["reviewing content"]',
|
|
365
|
+
'--field', 'does_not_apply_when=["pure formatting"]',
|
|
366
|
+
'--field', 'failure_risk=generic advice',
|
|
367
|
+
], { tmp });
|
|
368
|
+
assert.equal(result.status, 0, result.stderr);
|
|
369
|
+
const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
|
|
370
|
+
|
|
371
|
+
// Approve with --sign
|
|
372
|
+
result = run([
|
|
373
|
+
'card', 'approve', projectDir, cardId,
|
|
374
|
+
'--by', 'signer',
|
|
375
|
+
'--statement', 'I confirm this judgment with my creator identity.',
|
|
376
|
+
'--sign',
|
|
377
|
+
], {
|
|
378
|
+
tmp,
|
|
379
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
380
|
+
});
|
|
381
|
+
assert.equal(result.status, 0, result.stderr);
|
|
382
|
+
assert.match(result.stdout, /signed/);
|
|
383
|
+
|
|
384
|
+
// Verify human_lock has creator_id and signature
|
|
385
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
386
|
+
const lockedCard = project.cards.find(c => c.id === cardId);
|
|
387
|
+
assert.ok(lockedCard.human_lock, 'must have human_lock');
|
|
388
|
+
assert.ok(lockedCard.human_lock.creator_id, 'must have creator_id');
|
|
389
|
+
assert.match(lockedCard.human_lock.creator_id, /^kdna:creator:ed25519:/);
|
|
390
|
+
assert.ok(lockedCard.human_lock.signature, 'must have signature');
|
|
391
|
+
assert.match(lockedCard.human_lock.signature, /^ed25519:/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ── Blank create with default source_mode ───────────────────────────
|
|
395
|
+
|
|
396
|
+
test('create (blank) sets source_mode=blank by default', (t) => {
|
|
397
|
+
const tmp = tmpDir();
|
|
398
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
399
|
+
const projectDir = path.join(tmp, 'project');
|
|
400
|
+
|
|
401
|
+
const result = run(['create', projectDir, '--name', '@test/blank'], { tmp });
|
|
402
|
+
assert.equal(result.status, 0, result.stderr);
|
|
403
|
+
|
|
404
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
405
|
+
assert.equal(project.source_mode, 'blank');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── export includes creator/lineage in manifest ─────────────────────
|
|
409
|
+
|
|
410
|
+
test('export includes creator and lineage in kdna.json', (t) => {
|
|
411
|
+
const tmp = tmpDir();
|
|
412
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
413
|
+
const identityDir = path.join(tmp, 'identity');
|
|
414
|
+
|
|
415
|
+
// Init identity
|
|
416
|
+
run(['identity', 'init', '--name', 'Exporter'], {
|
|
417
|
+
tmp,
|
|
418
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Create, add axiom, approve
|
|
422
|
+
const projectDir = path.join(tmp, 'project');
|
|
423
|
+
run(['create', projectDir, '--name', '@test/exported'], { tmp });
|
|
424
|
+
run([
|
|
425
|
+
'card', 'add', projectDir, 'axiom',
|
|
426
|
+
'--field', 'one_sentence=Test axiom for lineage.',
|
|
427
|
+
'--field', 'full_statement=This is a complete testable explanation for the agent to apply in judgment scenarios.',
|
|
428
|
+
'--field', 'why=Without this axiom the agent would make incorrect judgment calls.',
|
|
429
|
+
'--field', 'applies_when=["testing"]',
|
|
430
|
+
'--field', 'does_not_apply_when=["not testing"]',
|
|
431
|
+
'--field', 'failure_risk=test may pass incorrectly',
|
|
432
|
+
], { tmp });
|
|
433
|
+
let result = run(['card', 'list', projectDir], { tmp });
|
|
434
|
+
const cardId = result.stdout.match(/(\S+)\s+axiom/)?.[1];
|
|
435
|
+
run(['card', 'approve', projectDir, cardId, '--by', 'tester', '--statement', 'Verified.'], { tmp });
|
|
436
|
+
run(['lock', projectDir], { tmp });
|
|
437
|
+
|
|
438
|
+
const distDir = path.join(tmp, 'dist');
|
|
439
|
+
const outFile = path.join(distDir, 'exported.kdna');
|
|
440
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
441
|
+
assert.equal(result.status, 0, result.stderr);
|
|
442
|
+
|
|
443
|
+
// Check exported kdna.json has new fields
|
|
444
|
+
const exportedManifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
445
|
+
assert.equal(exportedManifest.authoring.source_mode, 'blank');
|
|
446
|
+
assert.ok(exportedManifest.lineage, 'must have lineage');
|
|
447
|
+
assert.equal(exportedManifest.lineage.type, 'original');
|
|
448
|
+
});
|