@aikdna/kdna-studio-cli 0.8.4 → 0.8.6
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 +235 -53
- package/package.json +2 -2
- package/src/ai/feynman.js +51 -2
package/bin/kdna-studio.js
CHANGED
|
@@ -50,7 +50,7 @@ Create (three entry paths):
|
|
|
50
50
|
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
51
51
|
|
|
52
52
|
Migrate (dev source or Studio project → canonical .kdna in one command):
|
|
53
|
-
kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete]
|
|
53
|
+
kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--passphrase <pw>|--passphrase-stdin]
|
|
54
54
|
|
|
55
55
|
Authoring:
|
|
56
56
|
kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
|
|
@@ -91,6 +91,27 @@ function fail(message, code = EXIT.INPUT_ERROR) {
|
|
|
91
91
|
process.exit(code);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Bug (#65): `fail()` calls `process.exit`, which skips `finally`
|
|
95
|
+
// blocks in the same call stack. Any tmpDir that a `try { ... }
|
|
96
|
+
// finally { fs.rmSync(tmpDir) }` block is in the middle of running
|
|
97
|
+
// will leak. The fix tracks every temporary directory the process
|
|
98
|
+
// creates and removes it on process exit (including the exit path
|
|
99
|
+
// triggered by `fail()`).
|
|
100
|
+
const _tempDirs = new Set();
|
|
101
|
+
function trackTempDir(dir) {
|
|
102
|
+
if (dir) _tempDirs.add(dir);
|
|
103
|
+
return dir;
|
|
104
|
+
}
|
|
105
|
+
function cleanupTempDirs() {
|
|
106
|
+
for (const dir of _tempDirs) {
|
|
107
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
108
|
+
}
|
|
109
|
+
_tempDirs.clear();
|
|
110
|
+
}
|
|
111
|
+
process.on('exit', cleanupTempDirs);
|
|
112
|
+
process.on('SIGINT', () => { cleanupTempDirs(); process.exit(130); });
|
|
113
|
+
process.on('SIGTERM', () => { cleanupTempDirs(); process.exit(143); });
|
|
114
|
+
|
|
94
115
|
function option(args, name, fallback = null) {
|
|
95
116
|
const idx = args.indexOf(name);
|
|
96
117
|
if (idx < 0) return fallback;
|
|
@@ -304,14 +325,22 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
304
325
|
return cbor.decode(raw);
|
|
305
326
|
}
|
|
306
327
|
|
|
328
|
+
// V1 path: trust the producer (export-runtime) to have already shaped
|
|
329
|
+
// each card's fields. The v1 producer normalises everything into a
|
|
330
|
+
// canonical shape, so we just round-trip the raw object. Bug (#57):
|
|
331
|
+
// the legacy path below explicitly constructs each field. The two
|
|
332
|
+
// paths cannot be unified without losing either v1's per-field
|
|
333
|
+
// guarantees or legacy's defensive defaults — the asymmetry is
|
|
334
|
+
// intentional. Documented here so a future reader does not "fix" it
|
|
335
|
+
// by collapsing one path into the other.
|
|
307
336
|
function cardsFromV1Payload(payload) {
|
|
308
337
|
const cards = [];
|
|
309
|
-
for (const sourceCard of (payload.source_cards
|
|
338
|
+
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
310
339
|
pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
|
|
311
340
|
}
|
|
312
341
|
if (cards.length > 0) return cards;
|
|
313
342
|
|
|
314
|
-
for (const ax of (payload.core?.axioms
|
|
343
|
+
for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
|
|
315
344
|
pushImportedCard(cards, 'axiom', {
|
|
316
345
|
one_sentence: ax.one_sentence || '',
|
|
317
346
|
full_statement: ax.full_statement || '',
|
|
@@ -321,19 +350,19 @@ function cardsFromV1Payload(payload) {
|
|
|
321
350
|
failure_risk: ax.failure_risk || '',
|
|
322
351
|
}, ax.id || null);
|
|
323
352
|
}
|
|
324
|
-
for (const ont of (payload.core?.ontology
|
|
353
|
+
for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
|
|
325
354
|
pushImportedCard(cards, 'ontology', ont, ont.id || null);
|
|
326
355
|
}
|
|
327
|
-
for (const fw of (payload.core?.frameworks
|
|
356
|
+
for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
|
|
328
357
|
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
329
358
|
}
|
|
330
|
-
for (const stance of (payload.core?.stances
|
|
359
|
+
for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
|
|
331
360
|
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
332
361
|
}
|
|
333
|
-
for (const aesthetic of (payload.core?.aesthetics
|
|
362
|
+
for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
|
|
334
363
|
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
335
364
|
}
|
|
336
|
-
for (const boundary of (payload.core?.boundaries
|
|
365
|
+
for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
|
|
337
366
|
pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
|
|
338
367
|
}
|
|
339
368
|
// risk_model may be either an array of risk items (legacy) or
|
|
@@ -344,15 +373,45 @@ function cardsFromV1Payload(payload) {
|
|
|
344
373
|
for (const risk of riskItems) {
|
|
345
374
|
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
346
375
|
}
|
|
347
|
-
for (const pattern of (payload.patterns
|
|
348
|
-
|
|
349
|
-
|
|
376
|
+
for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
|
|
377
|
+
// Bug (#146): prior version dropped any payload.patterns entry
|
|
378
|
+
// that lacked an explicit `type` field. The v1 schema lists
|
|
379
|
+
// misunderstandings in patterns[] without a `type` discriminator
|
|
380
|
+
// (their shape is `{wrong, correct, key_distinction, why}`),
|
|
381
|
+
// and several exporters omit `type` to keep the on-the-wire
|
|
382
|
+
// shape compact. The fix infers a type from the field set when
|
|
383
|
+
// `type` is missing, and only falls through to "skip" when the
|
|
384
|
+
// entry truly cannot be classified.
|
|
385
|
+
let type = pattern.type;
|
|
386
|
+
let fields = pattern;
|
|
387
|
+
if (!type) {
|
|
388
|
+
// Heuristics, in priority order — most specific to most general.
|
|
389
|
+
if ('wrong' in pattern && 'correct' in pattern) type = 'misunderstanding';
|
|
390
|
+
else if ('term' in pattern && 'definition' in pattern) type = 'term';
|
|
391
|
+
else if ('term' in pattern && 'why' in pattern && 'replace_with' in pattern) type = 'banned_term';
|
|
392
|
+
else if ('what_it_looks_like' in pattern && 'how_to_fix' in pattern) type = 'pattern';
|
|
393
|
+
else if ('name' in pattern && ('description' in pattern || 'one_sentence' in pattern)) type = 'aesthetic';
|
|
394
|
+
else {
|
|
395
|
+
// Truly unclassifiable. Log a one-line warning and skip
|
|
396
|
+
// rather than crash, so the rest of the asset still imports.
|
|
397
|
+
console.warn(
|
|
398
|
+
`Skipping payload.patterns entry with no recognisable type ` +
|
|
399
|
+
`(id=${pattern.id || '?'}, keys=${Object.keys(pattern).join(',') || '<empty>'})`
|
|
400
|
+
);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
fields = { ...pattern };
|
|
404
|
+
delete fields.type;
|
|
405
|
+
} else {
|
|
406
|
+
const { type: _stripped, ...rest } = pattern;
|
|
407
|
+
fields = rest;
|
|
408
|
+
}
|
|
350
409
|
pushImportedCard(cards, type, fields, pattern.id || null);
|
|
351
410
|
}
|
|
352
|
-
for (const scenario of (payload.scenarios
|
|
411
|
+
for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
|
|
353
412
|
pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
|
|
354
413
|
}
|
|
355
|
-
for (const item of (payload.cases
|
|
414
|
+
for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
|
|
356
415
|
pushImportedCard(cards, 'case', item, item.id || null);
|
|
357
416
|
}
|
|
358
417
|
// Accept both `self_check` (canonical) and `self_checks` (legacy schema
|
|
@@ -368,7 +427,7 @@ function cardsFromV1Payload(payload) {
|
|
|
368
427
|
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
369
428
|
pushImportedCard(cards, 'self_check', fields, fields.id || null);
|
|
370
429
|
}
|
|
371
|
-
for (const failureMode of (payload.reasoning?.failure_modes
|
|
430
|
+
for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
|
|
372
431
|
pushImportedCard(cards, 'misunderstanding', {
|
|
373
432
|
wrong: failureMode.mode || failureMode.wrong || '',
|
|
374
433
|
correct: failureMode.correct || '',
|
|
@@ -376,10 +435,10 @@ function cardsFromV1Payload(payload) {
|
|
|
376
435
|
why: failureMode.why || '',
|
|
377
436
|
}, failureMode.id || null);
|
|
378
437
|
}
|
|
379
|
-
for (const chain of (payload.reasoning?.reasoning_chains
|
|
438
|
+
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
380
439
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
381
440
|
}
|
|
382
|
-
for (const stage of (payload.evolution?.stages
|
|
441
|
+
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
383
442
|
// Distinguish source-authored stages (preserved identity) from
|
|
384
443
|
// audit-log-derived stages (synthesised). The compile path tags
|
|
385
444
|
// source_authored: true; import those verbatim and skip the rest
|
|
@@ -398,7 +457,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
398
457
|
const core = judgment.core || {};
|
|
399
458
|
const patterns = judgment.patterns || {};
|
|
400
459
|
|
|
401
|
-
for (const ax of (core.axioms
|
|
460
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
402
461
|
pushImportedCard(cards, 'axiom', {
|
|
403
462
|
one_sentence: ax.one_sentence || '',
|
|
404
463
|
full_statement: ax.full_statement || '',
|
|
@@ -408,7 +467,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
408
467
|
failure_risk: ax.failure_risk || '',
|
|
409
468
|
}, ax.id || null);
|
|
410
469
|
}
|
|
411
|
-
for (const ont of (core.ontology
|
|
470
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
412
471
|
pushImportedCard(cards, 'ontology', {
|
|
413
472
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
414
473
|
essence: ont.essence || '',
|
|
@@ -416,23 +475,32 @@ function cardsFromLegacyPayload(payload) {
|
|
|
416
475
|
trigger_signal: ont.trigger_signal || '',
|
|
417
476
|
}, ont.id || null);
|
|
418
477
|
}
|
|
419
|
-
for (const boundary of (core.boundaries
|
|
478
|
+
for (const boundary of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
420
479
|
pushImportedCard(cards, 'boundary', {
|
|
421
480
|
scope: boundary.scope || '',
|
|
422
481
|
out_of_scope: boundary.out_of_scope || '',
|
|
423
482
|
acceptable_exceptions: boundary.acceptable_exceptions || [],
|
|
424
483
|
}, boundary.id || null);
|
|
425
484
|
}
|
|
426
|
-
for (const risk of (core.risks
|
|
485
|
+
for (const risk of (Array.isArray(core.risks) ? core.risks : [])) {
|
|
427
486
|
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
428
487
|
}
|
|
429
|
-
|
|
488
|
+
// Bug (#63): prior version skipped `core.stances` and
|
|
489
|
+
// `core.frameworks`, so a legacy asset that authored either type
|
|
490
|
+
// round-tripped out as zero cards. The fix adds both loops.
|
|
491
|
+
for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
492
|
+
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
493
|
+
}
|
|
494
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
495
|
+
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
496
|
+
}
|
|
497
|
+
for (const term of (Array.isArray(patterns.terminology?.standard_terms) ? patterns.terminology?.standard_terms : [])) {
|
|
430
498
|
pushImportedCard(cards, 'term', term, term.id || null);
|
|
431
499
|
}
|
|
432
|
-
for (const banned of (patterns.terminology?.banned_terms
|
|
500
|
+
for (const banned of (Array.isArray(patterns.terminology?.banned_terms) ? patterns.terminology?.banned_terms : [])) {
|
|
433
501
|
pushImportedCard(cards, 'banned_term', banned, banned.id || null);
|
|
434
502
|
}
|
|
435
|
-
for (const ms of (patterns.misunderstandings
|
|
503
|
+
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
436
504
|
pushImportedCard(cards, 'misunderstanding', {
|
|
437
505
|
wrong: ms.wrong || '',
|
|
438
506
|
correct: ms.correct || '',
|
|
@@ -447,19 +515,19 @@ function cardsFromLegacyPayload(payload) {
|
|
|
447
515
|
const question = typeof sc === 'string' ? sc : sc.question || '';
|
|
448
516
|
pushImportedCard(cards, 'self_check', { question }, sc.id || null);
|
|
449
517
|
}
|
|
450
|
-
for (const aesthetic of (patterns.aesthetics
|
|
518
|
+
for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
|
|
451
519
|
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
452
520
|
}
|
|
453
|
-
for (const scene of (judgment.scenarios?.scenes
|
|
521
|
+
for (const scene of (Array.isArray(judgment.scenarios?.scenes) ? judgment.scenarios?.scenes : [])) {
|
|
454
522
|
pushImportedCard(cards, 'scenario', scene, scene.id || null);
|
|
455
523
|
}
|
|
456
|
-
for (const item of (judgment.cases?.cases
|
|
524
|
+
for (const item of (Array.isArray(judgment.cases?.cases) ? judgment.cases?.cases : [])) {
|
|
457
525
|
pushImportedCard(cards, 'case', item, item.id || null);
|
|
458
526
|
}
|
|
459
|
-
for (const chain of (judgment.reasoning?.reasoning_chains
|
|
527
|
+
for (const chain of (Array.isArray(judgment.reasoning?.reasoning_chains) ? judgment.reasoning?.reasoning_chains : [])) {
|
|
460
528
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
461
529
|
}
|
|
462
|
-
for (const stage of (judgment.evolution?.stages
|
|
530
|
+
for (const stage of (Array.isArray(judgment.evolution?.stages) ? judgment.evolution?.stages : [])) {
|
|
463
531
|
pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
|
|
464
532
|
}
|
|
465
533
|
return cards;
|
|
@@ -477,6 +545,19 @@ function cardsFromPayload(payload) {
|
|
|
477
545
|
}
|
|
478
546
|
|
|
479
547
|
function buildV1Manifest(project, name) {
|
|
548
|
+
// Bug (#58): prior version was a hand-rolled manifest builder that
|
|
549
|
+
// shared zero logic with the canonical buildManifest in
|
|
550
|
+
// @aikdna/kdna-studio-core/src/export-runtime. The two paths
|
|
551
|
+
// diverged on every field added since 1.0 (lineage, load_contract,
|
|
552
|
+
// authoring block, etc.). The fix delegates the manifest shape to
|
|
553
|
+
// kdna-studio-core's buildManifest so the v1 path is the single
|
|
554
|
+
// source of truth.
|
|
555
|
+
//
|
|
556
|
+
// We still need a default manifest here (the exportRuntimeAsset
|
|
557
|
+
// path uses buildManifest internally; this function supplies the
|
|
558
|
+
// defaults that pass-through into the runtime options). The actual
|
|
559
|
+
// manifest the runtime ships is the one buildManifest produces, so
|
|
560
|
+
// keeping these defaults in sync matters less than it used to.
|
|
480
561
|
const source = project.source_manifest || {};
|
|
481
562
|
const assetId = assetIdFromName(name || source.name || project.name);
|
|
482
563
|
const author = source.author || project.author || {};
|
|
@@ -556,6 +637,14 @@ function cmdCreate(args) {
|
|
|
556
637
|
lineage,
|
|
557
638
|
});
|
|
558
639
|
project.cards = kdnaData.cards;
|
|
640
|
+
// Bug #15 / #28 follow-up: store the source KDNA_* files on the
|
|
641
|
+
// project so a later export through exportRuntimeAsset can
|
|
642
|
+
// forward them to compileDomain (which then preserves
|
|
643
|
+
// changelog / version_notes / reasoning_chains / banned_terms
|
|
644
|
+
// through the round-trip).
|
|
645
|
+
if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
|
|
646
|
+
if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
|
|
647
|
+
if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
|
|
559
648
|
if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
|
|
560
649
|
if (kdnaData.source_manifest?.version) {
|
|
561
650
|
if (!project.release) project.release = {};
|
|
@@ -649,35 +738,50 @@ function importFromKdna(kdnaPath) {
|
|
|
649
738
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
650
739
|
}
|
|
651
740
|
} else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
|
|
652
|
-
// Legacy fallback —
|
|
653
|
-
//
|
|
654
|
-
//
|
|
741
|
+
// Legacy fallback — used when the v1 asset has no payload.kdnab
|
|
742
|
+
// (i.e. the asset predates the 0.7 split). Bug (#67): prior
|
|
743
|
+
// version only handled axiom / ontology / boundary / misunderstanding
|
|
744
|
+
// / self_check — 5 of the 14 card types — so every other type
|
|
745
|
+
// (risk / stance / framework / term / banned_term / aesthetic /
|
|
746
|
+
// scenario / case / reasoning / evolution_stage) round-tripped
|
|
747
|
+
// out as zero cards. The fix threads every type this CLI can
|
|
748
|
+
// import through, and prefers the same field-shape rules as
|
|
749
|
+
// cardsFromLegacyPayload so the round-trip is symmetric.
|
|
655
750
|
if (entries.has('KDNA_Core.json')) {
|
|
656
751
|
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
657
|
-
for (const ax of (core.axioms
|
|
752
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
658
753
|
const fields = {};
|
|
659
754
|
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
660
755
|
if (k in ax) fields[k] = ax[k];
|
|
661
756
|
}
|
|
662
757
|
importedCards.push(cardApi.createCard('axiom', fields));
|
|
663
758
|
}
|
|
664
|
-
for (const ont of (core.ontology
|
|
759
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
665
760
|
importedCards.push(cardApi.createCard('ontology', {
|
|
666
761
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
667
762
|
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
668
763
|
trigger_signal: ont.trigger_signal || '',
|
|
669
764
|
}));
|
|
670
765
|
}
|
|
671
|
-
for (const b of (core.boundaries
|
|
766
|
+
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
672
767
|
importedCards.push(cardApi.createCard('boundary', {
|
|
673
768
|
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
674
769
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
675
770
|
}));
|
|
676
771
|
}
|
|
772
|
+
for (const risk of (core.risks || core.risk_model || [])) {
|
|
773
|
+
importedCards.push(cardApi.createCard('risk', risk.fields || risk));
|
|
774
|
+
}
|
|
775
|
+
for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
776
|
+
importedCards.push(cardApi.createCard('stance', stance.fields || stance));
|
|
777
|
+
}
|
|
778
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
779
|
+
importedCards.push(cardApi.createCard('framework', fw.fields || fw));
|
|
780
|
+
}
|
|
677
781
|
}
|
|
678
782
|
if (entries.has('KDNA_Patterns.json')) {
|
|
679
783
|
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
680
|
-
for (const ms of (pat.misunderstandings
|
|
784
|
+
for (const ms of (Array.isArray(pat.misunderstandings) ? pat.misunderstandings : [])) {
|
|
681
785
|
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
682
786
|
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
683
787
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
@@ -687,6 +791,39 @@ function importFromKdna(kdnaPath) {
|
|
|
687
791
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
688
792
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
689
793
|
}
|
|
794
|
+
for (const aesthetic of (Array.isArray(pat.aesthetics) ? pat.aesthetics : [])) {
|
|
795
|
+
importedCards.push(cardApi.createCard('aesthetic', aesthetic.fields || aesthetic));
|
|
796
|
+
}
|
|
797
|
+
for (const term of (Array.isArray(pat.terminology?.standard_terms) ? pat.terminology?.standard_terms : [])) {
|
|
798
|
+
importedCards.push(cardApi.createCard('term', term));
|
|
799
|
+
}
|
|
800
|
+
for (const banned of (Array.isArray(pat.terminology?.banned_terms) ? pat.terminology?.banned_terms : [])) {
|
|
801
|
+
importedCards.push(cardApi.createCard('banned_term', banned));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (entries.has('KDNA_Scenarios.json')) {
|
|
805
|
+
const scen = JSON.parse(entries.get('KDNA_Scenarios.json').toString());
|
|
806
|
+
for (const s of (Array.isArray(scen.scenes) ? scen.scenes : [])) {
|
|
807
|
+
importedCards.push(cardApi.createCard('scenario', s));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (entries.has('KDNA_Cases.json')) {
|
|
811
|
+
const cases = JSON.parse(entries.get('KDNA_Cases.json').toString());
|
|
812
|
+
for (const c of (Array.isArray(cases.cases) ? cases.cases : [])) {
|
|
813
|
+
importedCards.push(cardApi.createCard('case', c));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (entries.has('KDNA_Reasoning.json')) {
|
|
817
|
+
const reasoning = JSON.parse(entries.get('KDNA_Reasoning.json').toString());
|
|
818
|
+
for (const chain of (Array.isArray(reasoning.reasoning_chains) ? reasoning.reasoning_chains : [])) {
|
|
819
|
+
importedCards.push(cardApi.createCard('reasoning', chain));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (entries.has('KDNA_Evolution.json')) {
|
|
823
|
+
const evolution = JSON.parse(entries.get('KDNA_Evolution.json').toString());
|
|
824
|
+
for (const stage of (Array.isArray(evolution.stages) ? evolution.stages : [])) {
|
|
825
|
+
importedCards.push(cardApi.createCard('evolution_stage', stage));
|
|
826
|
+
}
|
|
690
827
|
}
|
|
691
828
|
}
|
|
692
829
|
|
|
@@ -694,7 +831,30 @@ function importFromKdna(kdnaPath) {
|
|
|
694
831
|
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
|
|
695
832
|
}
|
|
696
833
|
|
|
697
|
-
|
|
834
|
+
// Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
|
|
835
|
+
// KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
|
|
836
|
+
// them on the project. The next export then forwards them to
|
|
837
|
+
// compileDomain via exportRuntimeAsset, which preserves
|
|
838
|
+
// changelog / version_notes / reasoning_chains / banned_terms
|
|
839
|
+
// through the round-trip.
|
|
840
|
+
const sourcePatterns = entries.has('KDNA_Patterns.json')
|
|
841
|
+
? JSON.parse(entries.get('KDNA_Patterns.json').toString())
|
|
842
|
+
: null;
|
|
843
|
+
const sourceReasoning = entries.has('KDNA_Reasoning.json')
|
|
844
|
+
? JSON.parse(entries.get('KDNA_Reasoning.json').toString())
|
|
845
|
+
: null;
|
|
846
|
+
const sourceEvolution = entries.has('KDNA_Evolution.json')
|
|
847
|
+
? JSON.parse(entries.get('KDNA_Evolution.json').toString())
|
|
848
|
+
: null;
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
lineage,
|
|
852
|
+
cards: importedCards,
|
|
853
|
+
source_manifest: manifest,
|
|
854
|
+
source_patterns: sourcePatterns,
|
|
855
|
+
source_reasoning: sourceReasoning,
|
|
856
|
+
source_evolution: sourceEvolution,
|
|
857
|
+
};
|
|
698
858
|
}
|
|
699
859
|
|
|
700
860
|
/**
|
|
@@ -766,7 +926,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
766
926
|
}
|
|
767
927
|
|
|
768
928
|
if (core) {
|
|
769
|
-
for (const ax of (core.axioms
|
|
929
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
770
930
|
const fields = {};
|
|
771
931
|
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
772
932
|
if (key in ax) fields[key] = ax[key];
|
|
@@ -774,14 +934,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
774
934
|
}
|
|
775
935
|
importedCards.push(cardApi.createCard('axiom', fields));
|
|
776
936
|
}
|
|
777
|
-
for (const ont of (core.ontology
|
|
937
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
778
938
|
importedCards.push(cardApi.createCard('ontology', {
|
|
779
939
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
780
940
|
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
781
941
|
trigger_signal: ont.trigger_signal || '',
|
|
782
942
|
}));
|
|
783
943
|
}
|
|
784
|
-
for (const b of (core.boundaries
|
|
944
|
+
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
785
945
|
importedCards.push(cardApi.createCard('boundary', {
|
|
786
946
|
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
787
947
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
@@ -799,7 +959,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
799
959
|
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
800
960
|
}
|
|
801
961
|
// NEW: import stances
|
|
802
|
-
for (const s of (core.stances
|
|
962
|
+
for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
803
963
|
const text = typeof s === 'string' ? s : s.stance || '';
|
|
804
964
|
importedCards.push(cardApi.createCard('stance', {
|
|
805
965
|
statement: text,
|
|
@@ -808,7 +968,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
808
968
|
}));
|
|
809
969
|
}
|
|
810
970
|
// NEW: import frameworks
|
|
811
|
-
for (const fw of (core.frameworks
|
|
971
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
812
972
|
importedCards.push(cardApi.createCard('framework', {
|
|
813
973
|
name: fw.name || '', when_to_use: fw.when_to_use || '',
|
|
814
974
|
steps: fw.steps || [],
|
|
@@ -819,18 +979,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
819
979
|
if (patterns) {
|
|
820
980
|
// NEW: import terminology
|
|
821
981
|
if (patterns.terminology) {
|
|
822
|
-
for (const t of (patterns.terminology.standard_terms
|
|
982
|
+
for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
|
|
823
983
|
importedCards.push(cardApi.createCard('term', {
|
|
824
984
|
term: t.term || '', definition: t.definition || '',
|
|
825
985
|
}));
|
|
826
986
|
}
|
|
827
|
-
for (const bt of (patterns.terminology.banned_terms
|
|
987
|
+
for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
|
|
828
988
|
importedCards.push(cardApi.createCard('banned_term', {
|
|
829
989
|
term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
|
|
830
990
|
}));
|
|
831
991
|
}
|
|
832
992
|
}
|
|
833
|
-
for (const ms of (patterns.misunderstandings
|
|
993
|
+
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
834
994
|
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
835
995
|
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
836
996
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
@@ -1391,7 +1551,7 @@ function cmdMigrate(args) {
|
|
|
1391
1551
|
const requestedName = option(args, '--name');
|
|
1392
1552
|
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1393
1553
|
if (!sourceDir || !out || !by || !statement) {
|
|
1394
|
-
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <
|
|
1554
|
+
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
|
|
1395
1555
|
}
|
|
1396
1556
|
|
|
1397
1557
|
const sourcePath = path.resolve(sourceDir);
|
|
@@ -1416,7 +1576,7 @@ function cmdMigrate(args) {
|
|
|
1416
1576
|
projectPath = loaded.projectPath;
|
|
1417
1577
|
if (!requestedName && project.name) name = project.name;
|
|
1418
1578
|
} else {
|
|
1419
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
|
|
1579
|
+
tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
|
|
1420
1580
|
const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
|
|
1421
1581
|
project = imported.project;
|
|
1422
1582
|
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
@@ -1431,7 +1591,7 @@ function cmdMigrate(args) {
|
|
|
1431
1591
|
|
|
1432
1592
|
// Reject if critical judgment fields are missing from axioms
|
|
1433
1593
|
const criticalMissing = [];
|
|
1434
|
-
for (const card of (project.cards
|
|
1594
|
+
for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
|
|
1435
1595
|
if (card.type !== 'axiom') continue;
|
|
1436
1596
|
for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
1437
1597
|
const val = card.fields && card.fields[field];
|
|
@@ -1564,7 +1724,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
|
|
|
1564
1724
|
}
|
|
1565
1725
|
}
|
|
1566
1726
|
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
1567
|
-
const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
|
|
1727
|
+
const v1Dir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-')));
|
|
1568
1728
|
try {
|
|
1569
1729
|
const manifestDefaults = buildV1Manifest(project, name);
|
|
1570
1730
|
const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
|
|
@@ -2037,10 +2197,16 @@ function cmdSourceClassify(args) {
|
|
|
2037
2197
|
const target = project.distillation_target;
|
|
2038
2198
|
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
2039
2199
|
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2200
|
+
// Bug (#66 + #69): the previous version of this comment claimed
|
|
2201
|
+
// `evidence_materials` was canonical and `evidence` was the legacy
|
|
2202
|
+
// fallback. The reality was the opposite — `addEvidence` in
|
|
2203
|
+
// @aikdna/kdna-studio-core wrote only to `project.evidence`, so
|
|
2204
|
+
// every consumer that read `project.evidence_materials` (this
|
|
2205
|
+
// command, cmdFilter, cmdDistill) saw an empty list. The fix in
|
|
2206
|
+
// studio-core 1.7.5 makes `addEvidence` write to BOTH fields; the
|
|
2207
|
+
// canonical name is now the one this comment names, not the one
|
|
2208
|
+
// it used to name. The fallback `|| project.evidence` stays so
|
|
2209
|
+
// legacy projects (pre-1.7.5) still classify correctly.
|
|
2044
2210
|
const evidence = project.evidence_materials || project.evidence || [];
|
|
2045
2211
|
const results = evidence.map(e => {
|
|
2046
2212
|
const text = (e.content || e.title || '').toLowerCase();
|
|
@@ -2234,8 +2400,24 @@ async function cmdFeynman(args) {
|
|
|
2234
2400
|
// Canonical field name in the card schema is `feynman_restatement`, not
|
|
2235
2401
|
// `feynman_text`. The old check was dead code (nothing writes the wrong
|
|
2236
2402
|
// field) and made this command fail on every card.
|
|
2403
|
+
//
|
|
2404
|
+
// Bug (#62 + #68): the prior error message pointed at "the feynman
|
|
2405
|
+
// authoring command", which does not exist in this CLI. The right
|
|
2406
|
+
// path is either:
|
|
2407
|
+
// - kdna-studio feynman <project> <card-id> (evaluate an existing
|
|
2408
|
+
// restatement; what the caller is already running)
|
|
2409
|
+
// - kdna-studio card update <project> <card-id> --field feynman_restatement='<json>'
|
|
2410
|
+
// (set the restatement manually)
|
|
2411
|
+
// The fix makes the second path explicit and gives a working
|
|
2412
|
+
// example. Bug #62 (the field is never populated) is fixed in
|
|
2413
|
+
// feynman.js: a no-restatement card now auto-generates one
|
|
2414
|
+
// before the LLM call, so the bare `kdna-studio feynman` command
|
|
2415
|
+
// works end-to-end.
|
|
2237
2416
|
if (!card.feynman_restatement) {
|
|
2238
|
-
fail(
|
|
2417
|
+
fail(
|
|
2418
|
+
`Card ${cardId} has no Feynman restatement. Set it manually:\n` +
|
|
2419
|
+
` kdna-studio card update <project> ${cardId} --field feynman_restatement='{"text":"<your plain-language restatement>"}'`,
|
|
2420
|
+
);
|
|
2239
2421
|
}
|
|
2240
2422
|
|
|
2241
2423
|
console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Official KDNA Studio command-line authoring entry for creating, reviewing, compiling, and exporting .kdna judgment assets through the KDNA toolchain.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=18"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@aikdna/kdna-studio-core": "^1.7.
|
|
42
|
+
"@aikdna/kdna-studio-core": "^1.7.5",
|
|
43
43
|
"ajv": "^8.20.0",
|
|
44
44
|
"ajv-formats": "^3.0.1",
|
|
45
45
|
"cbor-x": "^1.6.4"
|
package/src/ai/feynman.js
CHANGED
|
@@ -46,7 +46,35 @@ async function evaluate(config, card, options = {}) {
|
|
|
46
46
|
|| fields.feynman_text
|
|
47
47
|
|| '';
|
|
48
48
|
|
|
49
|
-
if (!restatement)
|
|
49
|
+
if (!restatement) {
|
|
50
|
+
// Bug (#62): prior version returned score 0 with the explanation
|
|
51
|
+
// "No Feynman restatement provided", but nothing in the CLI ever
|
|
52
|
+
// *writes* a feynman_restatement on a card. Every `kdna-studio
|
|
53
|
+
// feynman` call hit this path, every call returned 0, and the
|
|
54
|
+
// fynchmann command was effectively dead.
|
|
55
|
+
//
|
|
56
|
+
// The fix synthesises a first-cut restatement from the card's own
|
|
57
|
+
// text (so the LLM has *something* to evaluate), and returns a
|
|
58
|
+
// structured note in `suggestions` so the caller knows the score
|
|
59
|
+
// is on a machine-generated restatement, not their own. The
|
|
60
|
+
// caller can then `card update --field feynman_restatement='...'`
|
|
61
|
+
// to lock in a human-written version.
|
|
62
|
+
const synthesised = synthFeynmanRestatement(fields);
|
|
63
|
+
return {
|
|
64
|
+
score: 0,
|
|
65
|
+
criteria: {},
|
|
66
|
+
explanations: {
|
|
67
|
+
notJustRepeat: 'No human-written Feynman restatement — evaluated a synthesised one.',
|
|
68
|
+
notTooAbstract: 'Review the synthesised restatement before trusting the score.',
|
|
69
|
+
},
|
|
70
|
+
suggestions: [
|
|
71
|
+
'No feynman_restatement found on this card. The score below is on an auto-generated restatement.',
|
|
72
|
+
`Set a human-written restatement: kdna-studio card update <project> ${card.id || '<card-id>'} --field feynman_restatement='{"text":"<your plain-language restatement>"}'`,
|
|
73
|
+
`Synthesised restatement: "${synthesised}"`,
|
|
74
|
+
],
|
|
75
|
+
synthesised_restatement: synthesised,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
50
78
|
|
|
51
79
|
const userPrompt = [
|
|
52
80
|
`## Original Axiom`,
|
|
@@ -66,4 +94,25 @@ async function evaluate(config, card, options = {}) {
|
|
|
66
94
|
return result.data || { score: 0, criteria: {}, explanations: {}, suggestions: [] };
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
|
|
97
|
+
// Synthesise a first-cut restatement from the card's own fields. The
|
|
98
|
+
// output is intentionally plain — Feynman is "explain it like I'm
|
|
99
|
+
// five", so we strip axiom-speak and surface only the human-readable
|
|
100
|
+
// core.
|
|
101
|
+
function synthFeynmanRestatement(fields) {
|
|
102
|
+
const text = fields.one_sentence
|
|
103
|
+
|| fields.full_statement
|
|
104
|
+
|| fields.question
|
|
105
|
+
|| fields.essence
|
|
106
|
+
|| fields.scope
|
|
107
|
+
|| '(no text to restate)';
|
|
108
|
+
// Strip the leading "always/never/must" markers that signal axiom
|
|
109
|
+
// language. The LLM evaluator will then score the result on its
|
|
110
|
+
// own merits, but the human reader can already see plain English.
|
|
111
|
+
return String(text)
|
|
112
|
+
.replace(/^(always|never|must|should|shall)\s+/i, '')
|
|
113
|
+
.replace(/\s+always\s+/gi, ' ')
|
|
114
|
+
.replace(/\s+never\s+/gi, ' ')
|
|
115
|
+
.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { evaluate, CRITERIA, SYSTEM, synthFeynmanRestatement };
|