@aikdna/kdna-studio-cli 0.1.3 → 0.3.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/README.md +43 -1
- package/bin/kdna-studio.js +1009 -33
- package/package.json +5 -2
- package/tests/cli.test.js +502 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,9 @@
|
|
|
37
37
|
"node": ">=18"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aikdna/kdna-studio-core": "^1.
|
|
40
|
+
"@aikdna/kdna-studio-core": "^1.5.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@aikdna/kdna-core": "^0.7.1"
|
|
41
44
|
}
|
|
42
45
|
}
|
package/tests/cli.test.js
CHANGED
|
@@ -5,6 +5,9 @@ const path = require('node:path');
|
|
|
5
5
|
const { spawnSync } = require('node:child_process');
|
|
6
6
|
const test = require('node:test');
|
|
7
7
|
|
|
8
|
+
let kdnaCore = null;
|
|
9
|
+
try { kdnaCore = require('@aikdna/kdna-core'); } catch { /* optional — runtime digest cross-verification */ }
|
|
10
|
+
|
|
8
11
|
const root = path.resolve(__dirname, '..');
|
|
9
12
|
const cli = path.join(root, 'bin', 'kdna-studio.js');
|
|
10
13
|
|
|
@@ -14,6 +17,7 @@ function run(args, options = {}) {
|
|
|
14
17
|
env: {
|
|
15
18
|
...process.env,
|
|
16
19
|
KDNA_IDENTITY_DIR: path.join(options.tmp || os.tmpdir(), 'no-identity'),
|
|
20
|
+
...(options.env || {}),
|
|
17
21
|
},
|
|
18
22
|
encoding: 'utf8',
|
|
19
23
|
});
|
|
@@ -43,6 +47,10 @@ function createLockedProject(t) {
|
|
|
43
47
|
'--field',
|
|
44
48
|
'one_sentence=Prefer specific evidence over broad claims',
|
|
45
49
|
'--field',
|
|
50
|
+
'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.',
|
|
51
|
+
'--field',
|
|
52
|
+
'why=Without this axiom the agent may accept broad plausible-sounding claims as equally credible as narrow evidence-backed ones leading to false balance.',
|
|
53
|
+
'--field',
|
|
46
54
|
'applies_when=["reviewing content"]',
|
|
47
55
|
'--field',
|
|
48
56
|
'does_not_apply_when=["pure formatting"]',
|
|
@@ -146,6 +154,11 @@ test('runs the trusted authoring workflow through compile and export', (t) => {
|
|
|
146
154
|
assert.equal(manifest.authoring.human_confirmed, true);
|
|
147
155
|
assert.ok(manifest.authoring.human_lock_count >= 1);
|
|
148
156
|
|
|
157
|
+
// Content digest must be present and non-trivial
|
|
158
|
+
assert.ok(manifest.content_digest, 'manifest must have content_digest');
|
|
159
|
+
assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
|
|
160
|
+
assert.ok(manifest.authoring.content_digest, 'authoring must have content_digest');
|
|
161
|
+
|
|
149
162
|
const outFile = path.join(tmp, 'dist', 'example.kdna');
|
|
150
163
|
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
151
164
|
assert.equal(result.status, 0, result.stderr);
|
|
@@ -155,6 +168,19 @@ test('runs the trusted authoring workflow through compile and export', (t) => {
|
|
|
155
168
|
const receipt = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'build-receipt.json'), 'utf8'));
|
|
156
169
|
assert.equal(receipt.signature_status, 'unsigned');
|
|
157
170
|
assert.match(receipt.asset_digest, /^sha256:/);
|
|
171
|
+
|
|
172
|
+
// ── Digest chain consistency ──────────────────────────────────
|
|
173
|
+
// All three sources MUST agree on content_digest.
|
|
174
|
+
const exportedManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'kdna.json'), 'utf8'));
|
|
175
|
+
const exportedProvenance = JSON.parse(fs.readFileSync(path.join(tmp, 'dist', 'provenance-report.json'), 'utf8'));
|
|
176
|
+
assert.equal(exportedManifest.content_digest, receipt.content_digest,
|
|
177
|
+
'manifest.content_digest !== build-receipt.content_digest');
|
|
178
|
+
assert.equal(exportedProvenance.content_digest, receipt.content_digest,
|
|
179
|
+
'provenance-report.content_digest !== build-receipt.content_digest');
|
|
180
|
+
assert.ok(exportedManifest.content_digest.startsWith('sha256:'),
|
|
181
|
+
'content_digest must be sha256: prefix');
|
|
182
|
+
assert.equal(exportedManifest.content_digest.length, 71,
|
|
183
|
+
'content_digest must be sha256: + 64 hex chars');
|
|
158
184
|
});
|
|
159
185
|
|
|
160
186
|
test('refuses signing without runtime identity keys', (t) => {
|
|
@@ -164,3 +190,479 @@ test('refuses signing without runtime identity keys', (t) => {
|
|
|
164
190
|
assert.equal(result.status, 5);
|
|
165
191
|
assert.match(result.stderr, /Signing requires KDNA identity keys/);
|
|
166
192
|
});
|
|
193
|
+
|
|
194
|
+
// ── Identity ────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
test('identity init creates local creator identity', (t) => {
|
|
197
|
+
const tmp = tmpDir();
|
|
198
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
199
|
+
const identityDir = path.join(tmp, 'identity');
|
|
200
|
+
|
|
201
|
+
const result = run(['identity', 'init', '--name', 'Test Creator'], {
|
|
202
|
+
tmp,
|
|
203
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
204
|
+
});
|
|
205
|
+
assert.equal(result.status, 0, result.stderr);
|
|
206
|
+
assert.match(result.stdout, /Creator identity initialized/);
|
|
207
|
+
assert.match(result.stdout, /kdna:creator:ed25519:/);
|
|
208
|
+
|
|
209
|
+
// Verify files were created
|
|
210
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'kdna.key')));
|
|
211
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'kdna.pub')));
|
|
212
|
+
assert.ok(fs.existsSync(path.join(identityDir, 'creator.json')));
|
|
213
|
+
const creatorJson = JSON.parse(fs.readFileSync(path.join(identityDir, 'creator.json'), 'utf8'));
|
|
214
|
+
assert.match(creatorJson.creator_id, /^kdna:creator:ed25519:/);
|
|
215
|
+
assert.equal(creatorJson.display_name, 'Test Creator');
|
|
216
|
+
assert.equal(creatorJson.verified, false);
|
|
217
|
+
|
|
218
|
+
// Double init should fail
|
|
219
|
+
const result2 = run(['identity', 'init', '--name', 'Duplicate'], {
|
|
220
|
+
tmp,
|
|
221
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
222
|
+
});
|
|
223
|
+
assert.equal(result2.status, 2);
|
|
224
|
+
assert.match(result2.stderr, /already exists/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('identity show displays current identity', (t) => {
|
|
228
|
+
const tmp = tmpDir();
|
|
229
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
230
|
+
const identityDir = path.join(tmp, 'identity');
|
|
231
|
+
|
|
232
|
+
// Init first
|
|
233
|
+
run(['identity', 'init', '--name', 'Test User'], {
|
|
234
|
+
tmp,
|
|
235
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = run(['identity', 'show'], {
|
|
239
|
+
tmp,
|
|
240
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
241
|
+
});
|
|
242
|
+
assert.equal(result.status, 0, result.stderr);
|
|
243
|
+
const info = JSON.parse(result.stdout);
|
|
244
|
+
assert.match(info.creator_id, /^kdna:creator:ed25519:/);
|
|
245
|
+
assert.equal(info.display_name, 'Test User');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('identity show fails when not initialized', (t) => {
|
|
249
|
+
const tmp = tmpDir();
|
|
250
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
251
|
+
|
|
252
|
+
const result = run(['identity', 'show'], {
|
|
253
|
+
tmp,
|
|
254
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: path.join(tmp, 'no-such') },
|
|
255
|
+
});
|
|
256
|
+
assert.equal(result.status, 2);
|
|
257
|
+
assert.match(result.stderr, /No identity found/);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ── Create from folder ──────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
test('create --from-folder imports legacy JSON source, outputs audit', (t) => {
|
|
263
|
+
const tmp = tmpDir();
|
|
264
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
265
|
+
|
|
266
|
+
const sourceDir = path.join(tmp, 'legacy-source');
|
|
267
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
268
|
+
|
|
269
|
+
// Create legacy KDNA files
|
|
270
|
+
const core = {
|
|
271
|
+
meta: { domain: 'test', version: '0.1.0', created: '2026-01-01', purpose: 'test', load_condition: 'always' },
|
|
272
|
+
axioms: [{
|
|
273
|
+
id: 'ax_legacy',
|
|
274
|
+
one_sentence: 'Test axiom from legacy folder.',
|
|
275
|
+
full_statement: 'A complete testable explanation from a legacy source folder.',
|
|
276
|
+
why: 'Without this legacy axiom, agent judgment defaults to untrained behavior.',
|
|
277
|
+
applies_when: ['testing legacy import'],
|
|
278
|
+
does_not_apply_when: ['production'],
|
|
279
|
+
failure_risk: 'May produce incorrect results from untrained judgment.',
|
|
280
|
+
}],
|
|
281
|
+
ontology: [{ id: 'ont_legacy', one_sentence: 'Legacy concept.', essence: 'Operational meaning.', boundary: 'Not the opposite.', trigger_signal: 'keyword' }],
|
|
282
|
+
boundaries: [{ id: 'bnd_legacy', scope: 'Legacy scope.', out_of_scope: 'Not covered by legacy.', acceptable_exceptions: [] }],
|
|
283
|
+
stances: [],
|
|
284
|
+
};
|
|
285
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify(core));
|
|
286
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
|
|
287
|
+
meta: { domain: 'test', version: '0.1.0', created: '2026-01-01', purpose: 'patterns', load_condition: 'always' },
|
|
288
|
+
misunderstandings: [{ id: 'ms_legacy', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key distinction between wrong and correct.' }],
|
|
289
|
+
self_check: ['Did I verify the legacy import?'],
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
const projectDir = path.join(tmp, 'project');
|
|
293
|
+
const result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/legacy-import'], { tmp });
|
|
294
|
+
assert.equal(result.status, 0, result.stderr);
|
|
295
|
+
|
|
296
|
+
const output = JSON.parse(result.stdout);
|
|
297
|
+
assert.ok(output.audit, 'must have audit info');
|
|
298
|
+
assert.ok(output.audit.filesFound.includes('KDNA_Core.json'));
|
|
299
|
+
assert.ok(output.audit.filesFound.includes('KDNA_Patterns.json'));
|
|
300
|
+
assert.ok(output.imported > 0, `imported cards: ${output.imported}`);
|
|
301
|
+
assert.equal(output.source_mode, 'source_folder');
|
|
302
|
+
|
|
303
|
+
// Read project and verify cards were imported
|
|
304
|
+
const projectPath = path.join(projectDir, 'studio.project.json');
|
|
305
|
+
assert.ok(fs.existsSync(projectPath));
|
|
306
|
+
const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
307
|
+
assert.equal(project.source_mode, 'source_folder');
|
|
308
|
+
assert.ok(project.cards.length > 0);
|
|
309
|
+
assert.equal(project.lineage.type, 'migrated');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── Create from KDNA ────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
test('create --from-kdna forks an existing .kdna asset', (t) => {
|
|
315
|
+
const tmp = tmpDir();
|
|
316
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
317
|
+
|
|
318
|
+
// First create and export a "parent" project
|
|
319
|
+
const parentTmp = tmpDir();
|
|
320
|
+
t.after(() => fs.rmSync(parentTmp, { recursive: true, force: true }));
|
|
321
|
+
const { projectDir: parentDir } = createLockedProject(t);
|
|
322
|
+
const parentDist = path.join(parentTmp, 'dist');
|
|
323
|
+
fs.mkdirSync(parentDist, { recursive: true });
|
|
324
|
+
const parentKdna = path.join(parentDist, 'parent.kdna');
|
|
325
|
+
run(['export', parentDir, '--out', parentKdna], { tmp: parentTmp });
|
|
326
|
+
|
|
327
|
+
// Now fork from that .kdna
|
|
328
|
+
const projectDir = path.join(tmp, 'forked-project');
|
|
329
|
+
const result = run(['create', projectDir, '--from-kdna', parentKdna, '--name', '@test/fork'], { tmp });
|
|
330
|
+
assert.equal(result.status, 0, result.stderr);
|
|
331
|
+
assert.match(result.stdout, /Created Studio project/);
|
|
332
|
+
assert.match(result.stdout, /kdna_asset/);
|
|
333
|
+
|
|
334
|
+
const projectPath = path.join(projectDir, 'studio.project.json');
|
|
335
|
+
assert.ok(fs.existsSync(projectPath));
|
|
336
|
+
const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
|
|
337
|
+
assert.equal(project.source_mode, 'kdna_asset');
|
|
338
|
+
assert.ok(project.lineage, 'must have lineage');
|
|
339
|
+
assert.equal(project.lineage.type, 'fork');
|
|
340
|
+
assert.ok(project.lineage.parent_name, 'must record parent name');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ── Card approve --sign ─────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
test('card approve --sign binds Human Lock to creator identity', (t) => {
|
|
346
|
+
const tmp = tmpDir();
|
|
347
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
348
|
+
const identityDir = path.join(tmp, 'identity');
|
|
349
|
+
|
|
350
|
+
// Initialize creator identity
|
|
351
|
+
const init = run(['identity', 'init', '--name', 'Signer'], {
|
|
352
|
+
tmp,
|
|
353
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
354
|
+
});
|
|
355
|
+
assert.equal(init.status, 0, init.stderr);
|
|
356
|
+
|
|
357
|
+
// Create project with an axiom
|
|
358
|
+
const projectDir = path.join(tmp, 'project');
|
|
359
|
+
let result = run(['create', projectDir, '--name', '@test/signed'], { tmp });
|
|
360
|
+
assert.equal(result.status, 0, result.stderr);
|
|
361
|
+
|
|
362
|
+
result = run([
|
|
363
|
+
'card', 'add', projectDir, 'axiom',
|
|
364
|
+
'--field', 'one_sentence=Prefer specific evidence over broad claims',
|
|
365
|
+
'--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.',
|
|
366
|
+
'--field', 'why=Without this axiom the agent may accept broad plausible-sounding claims as equally credible as narrow evidence-backed ones.',
|
|
367
|
+
'--field', 'applies_when=["reviewing content"]',
|
|
368
|
+
'--field', 'does_not_apply_when=["pure formatting"]',
|
|
369
|
+
'--field', 'failure_risk=generic advice',
|
|
370
|
+
], { tmp });
|
|
371
|
+
assert.equal(result.status, 0, result.stderr);
|
|
372
|
+
const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
|
|
373
|
+
|
|
374
|
+
// Approve with --sign
|
|
375
|
+
result = run([
|
|
376
|
+
'card', 'approve', projectDir, cardId,
|
|
377
|
+
'--by', 'signer',
|
|
378
|
+
'--statement', 'I confirm this judgment with my creator identity.',
|
|
379
|
+
'--sign',
|
|
380
|
+
], {
|
|
381
|
+
tmp,
|
|
382
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
383
|
+
});
|
|
384
|
+
assert.equal(result.status, 0, result.stderr);
|
|
385
|
+
assert.match(result.stdout, /signed/);
|
|
386
|
+
|
|
387
|
+
// Verify human_lock has creator_id and signature
|
|
388
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
389
|
+
const lockedCard = project.cards.find(c => c.id === cardId);
|
|
390
|
+
assert.ok(lockedCard.human_lock, 'must have human_lock');
|
|
391
|
+
assert.ok(lockedCard.human_lock.creator_id, 'must have creator_id');
|
|
392
|
+
assert.match(lockedCard.human_lock.creator_id, /^kdna:creator:ed25519:/);
|
|
393
|
+
assert.ok(lockedCard.human_lock.signature, 'must have signature');
|
|
394
|
+
assert.match(lockedCard.human_lock.signature, /^ed25519:/);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ── Blank create with default source_mode ───────────────────────────
|
|
398
|
+
|
|
399
|
+
test('create (blank) sets source_mode=blank by default', (t) => {
|
|
400
|
+
const tmp = tmpDir();
|
|
401
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
402
|
+
const projectDir = path.join(tmp, 'project');
|
|
403
|
+
|
|
404
|
+
const result = run(['create', projectDir, '--name', '@test/blank'], { tmp });
|
|
405
|
+
assert.equal(result.status, 0, result.stderr);
|
|
406
|
+
|
|
407
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
408
|
+
assert.equal(project.source_mode, 'blank');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ── export includes creator/lineage in manifest ─────────────────────
|
|
412
|
+
|
|
413
|
+
test('export includes creator and lineage in kdna.json', (t) => {
|
|
414
|
+
const tmp = tmpDir();
|
|
415
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
416
|
+
const identityDir = path.join(tmp, 'identity');
|
|
417
|
+
|
|
418
|
+
// Init identity
|
|
419
|
+
run(['identity', 'init', '--name', 'Exporter'], {
|
|
420
|
+
tmp,
|
|
421
|
+
env: { ...process.env, KDNA_IDENTITY_DIR: identityDir },
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Create, add axiom, approve
|
|
425
|
+
const projectDir = path.join(tmp, 'project');
|
|
426
|
+
run(['create', projectDir, '--name', '@test/exported'], { tmp });
|
|
427
|
+
run([
|
|
428
|
+
'card', 'add', projectDir, 'axiom',
|
|
429
|
+
'--field', 'one_sentence=Test axiom for lineage.',
|
|
430
|
+
'--field', 'full_statement=This is a complete testable explanation for the agent to apply in judgment scenarios.',
|
|
431
|
+
'--field', 'why=Without this axiom the agent would make incorrect judgment calls.',
|
|
432
|
+
'--field', 'applies_when=["testing"]',
|
|
433
|
+
'--field', 'does_not_apply_when=["not testing"]',
|
|
434
|
+
'--field', 'failure_risk=test may pass incorrectly',
|
|
435
|
+
], { tmp });
|
|
436
|
+
let result = run(['card', 'list', projectDir], { tmp });
|
|
437
|
+
const cardId = result.stdout.match(/(\S+)\s+axiom/)?.[1];
|
|
438
|
+
run(['card', 'approve', projectDir, cardId, '--by', 'tester', '--statement', 'Verified.'], { tmp });
|
|
439
|
+
run(['lock', projectDir], { tmp });
|
|
440
|
+
|
|
441
|
+
const distDir = path.join(tmp, 'dist');
|
|
442
|
+
const outFile = path.join(distDir, 'exported.kdna');
|
|
443
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
444
|
+
assert.equal(result.status, 0, result.stderr);
|
|
445
|
+
|
|
446
|
+
// Check exported kdna.json has new fields
|
|
447
|
+
const exportedManifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
448
|
+
assert.equal(exportedManifest.authoring.source_mode, 'blank');
|
|
449
|
+
assert.ok(exportedManifest.lineage, 'must have lineage');
|
|
450
|
+
assert.equal(exportedManifest.lineage.type, 'original');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ── E2E: Blank workflow with runtime content_digest check ──────────
|
|
454
|
+
|
|
455
|
+
test('E2E blank: create → approve → lock → export → runtime digest match', (t) => {
|
|
456
|
+
const tmp = tmpDir();
|
|
457
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
458
|
+
|
|
459
|
+
const projectDir = path.join(tmp, 'project');
|
|
460
|
+
let result = run(['create', projectDir, '--name', '@test/e2e-blank'], { tmp });
|
|
461
|
+
assert.equal(result.status, 0, result.stderr);
|
|
462
|
+
|
|
463
|
+
// Add axiom with full fields
|
|
464
|
+
result = run([
|
|
465
|
+
'card', 'add', projectDir, 'axiom',
|
|
466
|
+
'--field', 'one_sentence=E2E blank axiom.',
|
|
467
|
+
'--field', 'full_statement=A complete testable explanation for the end-to-end blank workflow test.',
|
|
468
|
+
'--field', 'why=Without this axiom the end-to-end test would not validate blank creation.',
|
|
469
|
+
'--field', 'applies_when=["e2e testing"]',
|
|
470
|
+
'--field', 'does_not_apply_when=["production"]',
|
|
471
|
+
'--field', 'failure_risk=e2e test may incorrectly pass',
|
|
472
|
+
], { tmp });
|
|
473
|
+
assert.equal(result.status, 0, result.stderr);
|
|
474
|
+
const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
|
|
475
|
+
|
|
476
|
+
// Add a misunderstanding
|
|
477
|
+
result = run([
|
|
478
|
+
'card', 'add', projectDir, 'misunderstanding',
|
|
479
|
+
'--field', 'wrong=E2E tests are unnecessary.',
|
|
480
|
+
'--field', 'correct=E2E tests catch integration issues not visible in unit tests.',
|
|
481
|
+
'--field', 'key_distinction=E2E tests validate the full pipeline, not individual functions.',
|
|
482
|
+
], { tmp });
|
|
483
|
+
assert.equal(result.status, 0, result.stderr);
|
|
484
|
+
|
|
485
|
+
// Approve all cards
|
|
486
|
+
let cards = run(['card', 'list', projectDir], { tmp });
|
|
487
|
+
for (const line of cards.stdout.trim().split('\n')) {
|
|
488
|
+
const id = line.split(/\s+/)[0];
|
|
489
|
+
result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Confirmed.'], { tmp });
|
|
490
|
+
assert.equal(result.status, 0, result.stderr);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Lock and export
|
|
494
|
+
result = run(['lock', projectDir], { tmp });
|
|
495
|
+
assert.equal(result.status, 0, result.stderr);
|
|
496
|
+
|
|
497
|
+
const outFile = path.join(tmp, 'blank.kdna');
|
|
498
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
499
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
500
|
+
assert.equal(result.status, 0, result.stderr);
|
|
501
|
+
|
|
502
|
+
// ── Verify: source_mode, lineage, digest chain ────────────
|
|
503
|
+
const distDir = path.dirname(outFile);
|
|
504
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
505
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
|
|
506
|
+
const provenance = JSON.parse(fs.readFileSync(path.join(distDir, 'provenance-report.json'), 'utf8'));
|
|
507
|
+
|
|
508
|
+
assert.equal(manifest.authoring.source_mode, 'blank');
|
|
509
|
+
assert.equal(manifest.lineage.type, 'original');
|
|
510
|
+
assert.match(manifest.content_digest, /^sha256:[0-9a-f]{64}$/);
|
|
511
|
+
assert.equal(manifest.content_digest, receipt.content_digest, 'manifest vs receipt digest mismatch');
|
|
512
|
+
assert.equal(manifest.content_digest, provenance.content_digest, 'manifest vs provenance digest mismatch');
|
|
513
|
+
assert.equal(manifest.authoring.compiler, '@aikdna/kdna-studio-core');
|
|
514
|
+
assert.equal(manifest.authoring.human_confirmed, true);
|
|
515
|
+
|
|
516
|
+
// ── Runtime cross-verification: content digest consistency ──
|
|
517
|
+
// Compute content digest from the .kdna ZIP entries and verify it
|
|
518
|
+
// matches the manifest, receipt, and provenance report.
|
|
519
|
+
if (kdnaCore) {
|
|
520
|
+
const reader = kdnaCore.createKdnaAssetReader();
|
|
521
|
+
const asset = reader.openSync(outFile);
|
|
522
|
+
const runtimeResult = reader.verifySync(asset);
|
|
523
|
+
const runtimeDigest = runtimeResult.content_digest;
|
|
524
|
+
|
|
525
|
+
assert.ok(runtimeDigest, 'runtime content_digest must be present');
|
|
526
|
+
assert.match(runtimeDigest, /^sha256:[0-9a-f]{64}$/, 'runtime content_digest must be valid sha256');
|
|
527
|
+
|
|
528
|
+
// Read ZIP-internal manifest (avoid sidecar timing issues)
|
|
529
|
+
const zipManifest = JSON.parse(asset.readEntry('kdna.json').toString());
|
|
530
|
+
assert.equal(zipManifest.content_digest, receipt.content_digest,
|
|
531
|
+
`ZIP manifest digest != receipt digest`);
|
|
532
|
+
assert.equal(zipManifest.content_digest, provenance.content_digest,
|
|
533
|
+
`ZIP manifest digest != provenance digest`);
|
|
534
|
+
|
|
535
|
+
// Verify: runtime computed digest matches ZIP manifest
|
|
536
|
+
assert.equal(zipManifest.content_digest, runtimeDigest,
|
|
537
|
+
`ZIP manifest content_digest (${zipManifest.content_digest.slice(0, 20)}...) != runtime computed (${runtimeDigest.slice(0, 20)}...) — trust chain broken`);
|
|
538
|
+
} else {
|
|
539
|
+
console.log(' (kdna-core not available, skipping runtime assetReader cross-verification)');
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// ── E2E: Fork workflow ────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
test('E2E fork: parent → fork → approve → export → lineage check', (t) => {
|
|
546
|
+
const tmp = tmpDir();
|
|
547
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
548
|
+
|
|
549
|
+
// Step 1: Create and export a parent domain
|
|
550
|
+
const parentDir = path.join(tmp, 'parent');
|
|
551
|
+
let result = run(['create', parentDir, '--name', '@test/parent-domain'], { tmp });
|
|
552
|
+
result = run([
|
|
553
|
+
'card', 'add', parentDir, 'axiom',
|
|
554
|
+
'--field', 'one_sentence=Parent axiom for forking test.',
|
|
555
|
+
'--field', 'full_statement=A complete testable explanation from the parent domain for fork lineage verification.',
|
|
556
|
+
'--field', 'why=Without this parent axiom the fork test would have no source to verify lineage against.',
|
|
557
|
+
'--field', 'applies_when=["fork testing"]',
|
|
558
|
+
'--field', 'does_not_apply_when=["not forking"]',
|
|
559
|
+
'--field', 'failure_risk=forked content may lose lineage',
|
|
560
|
+
], { tmp });
|
|
561
|
+
const pCards = run(['card', 'list', parentDir], { tmp });
|
|
562
|
+
for (const line of pCards.stdout.trim().split('\n')) {
|
|
563
|
+
const id = line.split(/\s+/)[0];
|
|
564
|
+
run(['card', 'approve', parentDir, id, '--by', 'tester', '--statement', 'Parent OK.'], { tmp });
|
|
565
|
+
}
|
|
566
|
+
run(['lock', parentDir], { tmp });
|
|
567
|
+
const parentKdna = path.join(tmp, 'parent.kdna');
|
|
568
|
+
fs.mkdirSync(path.dirname(parentKdna), { recursive: true });
|
|
569
|
+
result = run(['export', parentDir, '--out', parentKdna], { tmp });
|
|
570
|
+
assert.equal(result.status, 0, result.stderr);
|
|
571
|
+
|
|
572
|
+
// Step 2: Fork from parent
|
|
573
|
+
const forkDir = path.join(tmp, 'forked');
|
|
574
|
+
result = run(['create', forkDir, '--from-kdna', parentKdna, '--name', '@test/forked-domain'], { tmp });
|
|
575
|
+
assert.equal(result.status, 0, result.stderr);
|
|
576
|
+
|
|
577
|
+
const fProject = JSON.parse(fs.readFileSync(path.join(forkDir, 'studio.project.json'), 'utf8'));
|
|
578
|
+
assert.equal(fProject.source_mode, 'kdna_asset');
|
|
579
|
+
assert.equal(fProject.lineage.type, 'fork');
|
|
580
|
+
assert.equal(fProject.lineage.parent_name, '@test/parent-domain');
|
|
581
|
+
|
|
582
|
+
// Step 3: Approve and export fork
|
|
583
|
+
const fCards = run(['card', 'list', forkDir], { tmp });
|
|
584
|
+
for (const line of fCards.stdout.trim().split('\n')) {
|
|
585
|
+
const id = line.split(/\s+/)[0];
|
|
586
|
+
run(['card', 'approve', forkDir, id, '--by', 'tester', '--statement', 'Fork confirmed.'], { tmp });
|
|
587
|
+
}
|
|
588
|
+
run(['lock', forkDir], { tmp });
|
|
589
|
+
const forkKdna = path.join(tmp, 'forked.kdna');
|
|
590
|
+
fs.mkdirSync(path.dirname(forkKdna), { recursive: true });
|
|
591
|
+
result = run(['export', forkDir, '--out', forkKdna], { tmp });
|
|
592
|
+
assert.equal(result.status, 0, result.stderr);
|
|
593
|
+
|
|
594
|
+
// Step 4: Verify lineage in exported fork
|
|
595
|
+
const fManifest = JSON.parse(fs.readFileSync(path.join(tmp, 'kdna.json'), 'utf8'));
|
|
596
|
+
assert.equal(fManifest.authoring.source_mode, 'kdna_asset');
|
|
597
|
+
assert.equal(fManifest.lineage.type, 'fork');
|
|
598
|
+
assert.equal(fManifest.lineage.parent_name, '@test/parent-domain');
|
|
599
|
+
assert.match(fManifest.content_digest, /^sha256:/);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// ── E2E: Migrate workflow ─────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
test('E2E migrate: source folder → import → approve → export → verify', (t) => {
|
|
605
|
+
const tmp = tmpDir();
|
|
606
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
607
|
+
|
|
608
|
+
// Step 1: Create legacy source folder
|
|
609
|
+
const sourceDir = path.join(tmp, 'legacy');
|
|
610
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
611
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Core.json'), JSON.stringify({
|
|
612
|
+
meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'Legacy migrate test', load_condition: 'always' },
|
|
613
|
+
axioms: [{
|
|
614
|
+
id: 'ax_mig', one_sentence: 'Migrated axiom for E2E test.',
|
|
615
|
+
full_statement: 'A complete testable explanation for the migrated legacy axiom in the end-to-end workflow.',
|
|
616
|
+
why: 'Without this migrated axiom the e2e test cannot verify legacy folder migration.',
|
|
617
|
+
applies_when: ['legacy migration testing'], does_not_apply_when: ['not migrating'],
|
|
618
|
+
failure_risk: 'migrated content may not be re-locked',
|
|
619
|
+
}],
|
|
620
|
+
ontology: [],
|
|
621
|
+
stances: [],
|
|
622
|
+
}));
|
|
623
|
+
fs.writeFileSync(path.join(sourceDir, 'KDNA_Patterns.json'), JSON.stringify({
|
|
624
|
+
meta: { domain: 'legacy_test', version: '0.3.0', created: '2026-01-01', purpose: 'legacy patterns', load_condition: 'always' },
|
|
625
|
+
misunderstandings: [{ id: 'ms_mig', wrong: 'Legacy wrong belief.', correct: 'Correct understanding.', key_distinction: 'The key difference between legacy wrong and correct behavior.' }],
|
|
626
|
+
self_check: ['Is the migrated content properly re-locked?'],
|
|
627
|
+
}));
|
|
628
|
+
|
|
629
|
+
// Step 2: Import via --from-folder
|
|
630
|
+
const projectDir = path.join(tmp, 'imported');
|
|
631
|
+
let result = run(['create', projectDir, '--from-folder', sourceDir, '--name', '@test/e2e-migrate'], { tmp });
|
|
632
|
+
assert.equal(result.status, 0, result.stderr);
|
|
633
|
+
const audit = JSON.parse(result.stdout);
|
|
634
|
+
assert.ok(audit.imported > 0, `No cards imported: ${result.stdout}`);
|
|
635
|
+
assert.equal(audit.source_mode, 'source_folder');
|
|
636
|
+
|
|
637
|
+
// Verify project has source_folder mode
|
|
638
|
+
const project = JSON.parse(fs.readFileSync(path.join(projectDir, 'studio.project.json'), 'utf8'));
|
|
639
|
+
assert.equal(project.source_mode, 'source_folder');
|
|
640
|
+
assert.equal(project.lineage.type, 'migrated');
|
|
641
|
+
|
|
642
|
+
// Step 3: Approve and export
|
|
643
|
+
const cards = run(['card', 'list', projectDir], { tmp });
|
|
644
|
+
for (const line of cards.stdout.trim().split('\n')) {
|
|
645
|
+
const id = line.split(/\s+/)[0];
|
|
646
|
+
result = run(['card', 'approve', projectDir, id, '--by', 'tester', '--statement', 'Migrated content confirmed.'], { tmp });
|
|
647
|
+
assert.equal(result.status, 0, result.stderr);
|
|
648
|
+
}
|
|
649
|
+
result = run(['lock', projectDir], { tmp });
|
|
650
|
+
assert.equal(result.status, 0, result.stderr);
|
|
651
|
+
|
|
652
|
+
const outFile = path.join(tmp, 'migrated.kdna');
|
|
653
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
654
|
+
result = run(['export', projectDir, '--out', outFile], { tmp });
|
|
655
|
+
assert.equal(result.status, 0, result.stderr);
|
|
656
|
+
|
|
657
|
+
// Verify exported manifest
|
|
658
|
+
const distDir = path.dirname(outFile);
|
|
659
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(distDir, 'kdna.json'), 'utf8'));
|
|
660
|
+
assert.equal(manifest.authoring.source_mode, 'source_folder');
|
|
661
|
+
assert.equal(manifest.lineage.type, 'migrated');
|
|
662
|
+
assert.match(manifest.content_digest, /^sha256:/);
|
|
663
|
+
assert.equal(manifest.authoring.human_confirmed, true);
|
|
664
|
+
|
|
665
|
+
// Digest chain
|
|
666
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(distDir, 'build-receipt.json'), 'utf8'));
|
|
667
|
+
assert.equal(manifest.content_digest, receipt.content_digest);
|
|
668
|
+
});
|