@aikdna/kdna-studio-cli 0.8.5 → 0.8.7
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 +205 -50
- package/package.json +2 -2
- package/src/ai/feynman.js +51 -2
package/bin/kdna-studio.js
CHANGED
|
@@ -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;
|
|
@@ -314,12 +335,12 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
314
335
|
// by collapsing one path into the other.
|
|
315
336
|
function cardsFromV1Payload(payload) {
|
|
316
337
|
const cards = [];
|
|
317
|
-
for (const sourceCard of (payload.source_cards
|
|
338
|
+
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
318
339
|
pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
|
|
319
340
|
}
|
|
320
341
|
if (cards.length > 0) return cards;
|
|
321
342
|
|
|
322
|
-
for (const ax of (payload.core?.axioms
|
|
343
|
+
for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
|
|
323
344
|
pushImportedCard(cards, 'axiom', {
|
|
324
345
|
one_sentence: ax.one_sentence || '',
|
|
325
346
|
full_statement: ax.full_statement || '',
|
|
@@ -329,19 +350,19 @@ function cardsFromV1Payload(payload) {
|
|
|
329
350
|
failure_risk: ax.failure_risk || '',
|
|
330
351
|
}, ax.id || null);
|
|
331
352
|
}
|
|
332
|
-
for (const ont of (payload.core?.ontology
|
|
353
|
+
for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
|
|
333
354
|
pushImportedCard(cards, 'ontology', ont, ont.id || null);
|
|
334
355
|
}
|
|
335
|
-
for (const fw of (payload.core?.frameworks
|
|
356
|
+
for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
|
|
336
357
|
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
337
358
|
}
|
|
338
|
-
for (const stance of (payload.core?.stances
|
|
359
|
+
for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
|
|
339
360
|
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
340
361
|
}
|
|
341
|
-
for (const aesthetic of (payload.core?.aesthetics
|
|
362
|
+
for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
|
|
342
363
|
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
343
364
|
}
|
|
344
|
-
for (const boundary of (payload.core?.boundaries
|
|
365
|
+
for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
|
|
345
366
|
pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
|
|
346
367
|
}
|
|
347
368
|
// risk_model may be either an array of risk items (legacy) or
|
|
@@ -352,15 +373,45 @@ function cardsFromV1Payload(payload) {
|
|
|
352
373
|
for (const risk of riskItems) {
|
|
353
374
|
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
354
375
|
}
|
|
355
|
-
for (const pattern of (payload.patterns
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}
|
|
358
409
|
pushImportedCard(cards, type, fields, pattern.id || null);
|
|
359
410
|
}
|
|
360
|
-
for (const scenario of (payload.scenarios
|
|
411
|
+
for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
|
|
361
412
|
pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
|
|
362
413
|
}
|
|
363
|
-
for (const item of (payload.cases
|
|
414
|
+
for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
|
|
364
415
|
pushImportedCard(cards, 'case', item, item.id || null);
|
|
365
416
|
}
|
|
366
417
|
// Accept both `self_check` (canonical) and `self_checks` (legacy schema
|
|
@@ -376,18 +427,27 @@ function cardsFromV1Payload(payload) {
|
|
|
376
427
|
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
377
428
|
pushImportedCard(cards, 'self_check', fields, fields.id || null);
|
|
378
429
|
}
|
|
379
|
-
for (const failureMode of (payload.reasoning?.failure_modes
|
|
430
|
+
for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
|
|
431
|
+
// Bug (#145 follow-up): the prior version dropped three
|
|
432
|
+
// judgment fields (failure_risk, applies_when, does_not_apply_when)
|
|
433
|
+
// when reading failure_modes back out of a v1 asset. A Studio
|
|
434
|
+
// project that round-tripped through `migrate --format v1` lost
|
|
435
|
+
// these fields every time. The fix reads them through, matching
|
|
436
|
+
// the export-side map in kdna-studio-core's buildPayload.
|
|
380
437
|
pushImportedCard(cards, 'misunderstanding', {
|
|
381
438
|
wrong: failureMode.mode || failureMode.wrong || '',
|
|
382
439
|
correct: failureMode.correct || '',
|
|
383
440
|
key_distinction: failureMode.key_distinction || '',
|
|
384
441
|
why: failureMode.why || '',
|
|
442
|
+
failure_risk: failureMode.failure_risk || '',
|
|
443
|
+
applies_when: Array.isArray(failureMode.applies_when) ? failureMode.applies_when : [],
|
|
444
|
+
does_not_apply_when: Array.isArray(failureMode.does_not_apply_when) ? failureMode.does_not_apply_when : [],
|
|
385
445
|
}, failureMode.id || null);
|
|
386
446
|
}
|
|
387
|
-
for (const chain of (payload.reasoning?.reasoning_chains
|
|
447
|
+
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
388
448
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
389
449
|
}
|
|
390
|
-
for (const stage of (payload.evolution?.stages
|
|
450
|
+
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
391
451
|
// Distinguish source-authored stages (preserved identity) from
|
|
392
452
|
// audit-log-derived stages (synthesised). The compile path tags
|
|
393
453
|
// source_authored: true; import those verbatim and skip the rest
|
|
@@ -406,7 +466,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
406
466
|
const core = judgment.core || {};
|
|
407
467
|
const patterns = judgment.patterns || {};
|
|
408
468
|
|
|
409
|
-
for (const ax of (core.axioms
|
|
469
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
410
470
|
pushImportedCard(cards, 'axiom', {
|
|
411
471
|
one_sentence: ax.one_sentence || '',
|
|
412
472
|
full_statement: ax.full_statement || '',
|
|
@@ -416,7 +476,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
416
476
|
failure_risk: ax.failure_risk || '',
|
|
417
477
|
}, ax.id || null);
|
|
418
478
|
}
|
|
419
|
-
for (const ont of (core.ontology
|
|
479
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
420
480
|
pushImportedCard(cards, 'ontology', {
|
|
421
481
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
422
482
|
essence: ont.essence || '',
|
|
@@ -424,23 +484,32 @@ function cardsFromLegacyPayload(payload) {
|
|
|
424
484
|
trigger_signal: ont.trigger_signal || '',
|
|
425
485
|
}, ont.id || null);
|
|
426
486
|
}
|
|
427
|
-
for (const boundary of (core.boundaries
|
|
487
|
+
for (const boundary of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
428
488
|
pushImportedCard(cards, 'boundary', {
|
|
429
489
|
scope: boundary.scope || '',
|
|
430
490
|
out_of_scope: boundary.out_of_scope || '',
|
|
431
491
|
acceptable_exceptions: boundary.acceptable_exceptions || [],
|
|
432
492
|
}, boundary.id || null);
|
|
433
493
|
}
|
|
434
|
-
for (const risk of (core.risks
|
|
494
|
+
for (const risk of (Array.isArray(core.risks) ? core.risks : [])) {
|
|
435
495
|
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
436
496
|
}
|
|
437
|
-
|
|
497
|
+
// Bug (#63): prior version skipped `core.stances` and
|
|
498
|
+
// `core.frameworks`, so a legacy asset that authored either type
|
|
499
|
+
// round-tripped out as zero cards. The fix adds both loops.
|
|
500
|
+
for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
501
|
+
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
502
|
+
}
|
|
503
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
504
|
+
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
505
|
+
}
|
|
506
|
+
for (const term of (Array.isArray(patterns.terminology?.standard_terms) ? patterns.terminology?.standard_terms : [])) {
|
|
438
507
|
pushImportedCard(cards, 'term', term, term.id || null);
|
|
439
508
|
}
|
|
440
|
-
for (const banned of (patterns.terminology?.banned_terms
|
|
509
|
+
for (const banned of (Array.isArray(patterns.terminology?.banned_terms) ? patterns.terminology?.banned_terms : [])) {
|
|
441
510
|
pushImportedCard(cards, 'banned_term', banned, banned.id || null);
|
|
442
511
|
}
|
|
443
|
-
for (const ms of (patterns.misunderstandings
|
|
512
|
+
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
444
513
|
pushImportedCard(cards, 'misunderstanding', {
|
|
445
514
|
wrong: ms.wrong || '',
|
|
446
515
|
correct: ms.correct || '',
|
|
@@ -455,19 +524,19 @@ function cardsFromLegacyPayload(payload) {
|
|
|
455
524
|
const question = typeof sc === 'string' ? sc : sc.question || '';
|
|
456
525
|
pushImportedCard(cards, 'self_check', { question }, sc.id || null);
|
|
457
526
|
}
|
|
458
|
-
for (const aesthetic of (patterns.aesthetics
|
|
527
|
+
for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
|
|
459
528
|
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
460
529
|
}
|
|
461
|
-
for (const scene of (judgment.scenarios?.scenes
|
|
530
|
+
for (const scene of (Array.isArray(judgment.scenarios?.scenes) ? judgment.scenarios?.scenes : [])) {
|
|
462
531
|
pushImportedCard(cards, 'scenario', scene, scene.id || null);
|
|
463
532
|
}
|
|
464
|
-
for (const item of (judgment.cases?.cases
|
|
533
|
+
for (const item of (Array.isArray(judgment.cases?.cases) ? judgment.cases?.cases : [])) {
|
|
465
534
|
pushImportedCard(cards, 'case', item, item.id || null);
|
|
466
535
|
}
|
|
467
|
-
for (const chain of (judgment.reasoning?.reasoning_chains
|
|
536
|
+
for (const chain of (Array.isArray(judgment.reasoning?.reasoning_chains) ? judgment.reasoning?.reasoning_chains : [])) {
|
|
468
537
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
469
538
|
}
|
|
470
|
-
for (const stage of (judgment.evolution?.stages
|
|
539
|
+
for (const stage of (Array.isArray(judgment.evolution?.stages) ? judgment.evolution?.stages : [])) {
|
|
471
540
|
pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
|
|
472
541
|
}
|
|
473
542
|
return cards;
|
|
@@ -678,35 +747,50 @@ function importFromKdna(kdnaPath) {
|
|
|
678
747
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
679
748
|
}
|
|
680
749
|
} else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
|
|
681
|
-
// Legacy fallback —
|
|
682
|
-
//
|
|
683
|
-
//
|
|
750
|
+
// Legacy fallback — used when the v1 asset has no payload.kdnab
|
|
751
|
+
// (i.e. the asset predates the 0.7 split). Bug (#67): prior
|
|
752
|
+
// version only handled axiom / ontology / boundary / misunderstanding
|
|
753
|
+
// / self_check — 5 of the 14 card types — so every other type
|
|
754
|
+
// (risk / stance / framework / term / banned_term / aesthetic /
|
|
755
|
+
// scenario / case / reasoning / evolution_stage) round-tripped
|
|
756
|
+
// out as zero cards. The fix threads every type this CLI can
|
|
757
|
+
// import through, and prefers the same field-shape rules as
|
|
758
|
+
// cardsFromLegacyPayload so the round-trip is symmetric.
|
|
684
759
|
if (entries.has('KDNA_Core.json')) {
|
|
685
760
|
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
686
|
-
for (const ax of (core.axioms
|
|
761
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
687
762
|
const fields = {};
|
|
688
763
|
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
689
764
|
if (k in ax) fields[k] = ax[k];
|
|
690
765
|
}
|
|
691
766
|
importedCards.push(cardApi.createCard('axiom', fields));
|
|
692
767
|
}
|
|
693
|
-
for (const ont of (core.ontology
|
|
768
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
694
769
|
importedCards.push(cardApi.createCard('ontology', {
|
|
695
770
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
696
771
|
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
697
772
|
trigger_signal: ont.trigger_signal || '',
|
|
698
773
|
}));
|
|
699
774
|
}
|
|
700
|
-
for (const b of (core.boundaries
|
|
775
|
+
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
701
776
|
importedCards.push(cardApi.createCard('boundary', {
|
|
702
777
|
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
703
778
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
704
779
|
}));
|
|
705
780
|
}
|
|
781
|
+
for (const risk of (core.risks || core.risk_model || [])) {
|
|
782
|
+
importedCards.push(cardApi.createCard('risk', risk.fields || risk));
|
|
783
|
+
}
|
|
784
|
+
for (const stance of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
785
|
+
importedCards.push(cardApi.createCard('stance', stance.fields || stance));
|
|
786
|
+
}
|
|
787
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
788
|
+
importedCards.push(cardApi.createCard('framework', fw.fields || fw));
|
|
789
|
+
}
|
|
706
790
|
}
|
|
707
791
|
if (entries.has('KDNA_Patterns.json')) {
|
|
708
792
|
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
709
|
-
for (const ms of (pat.misunderstandings
|
|
793
|
+
for (const ms of (Array.isArray(pat.misunderstandings) ? pat.misunderstandings : [])) {
|
|
710
794
|
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
711
795
|
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
712
796
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
@@ -716,6 +800,39 @@ function importFromKdna(kdnaPath) {
|
|
|
716
800
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
717
801
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
718
802
|
}
|
|
803
|
+
for (const aesthetic of (Array.isArray(pat.aesthetics) ? pat.aesthetics : [])) {
|
|
804
|
+
importedCards.push(cardApi.createCard('aesthetic', aesthetic.fields || aesthetic));
|
|
805
|
+
}
|
|
806
|
+
for (const term of (Array.isArray(pat.terminology?.standard_terms) ? pat.terminology?.standard_terms : [])) {
|
|
807
|
+
importedCards.push(cardApi.createCard('term', term));
|
|
808
|
+
}
|
|
809
|
+
for (const banned of (Array.isArray(pat.terminology?.banned_terms) ? pat.terminology?.banned_terms : [])) {
|
|
810
|
+
importedCards.push(cardApi.createCard('banned_term', banned));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (entries.has('KDNA_Scenarios.json')) {
|
|
814
|
+
const scen = JSON.parse(entries.get('KDNA_Scenarios.json').toString());
|
|
815
|
+
for (const s of (Array.isArray(scen.scenes) ? scen.scenes : [])) {
|
|
816
|
+
importedCards.push(cardApi.createCard('scenario', s));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (entries.has('KDNA_Cases.json')) {
|
|
820
|
+
const cases = JSON.parse(entries.get('KDNA_Cases.json').toString());
|
|
821
|
+
for (const c of (Array.isArray(cases.cases) ? cases.cases : [])) {
|
|
822
|
+
importedCards.push(cardApi.createCard('case', c));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (entries.has('KDNA_Reasoning.json')) {
|
|
826
|
+
const reasoning = JSON.parse(entries.get('KDNA_Reasoning.json').toString());
|
|
827
|
+
for (const chain of (Array.isArray(reasoning.reasoning_chains) ? reasoning.reasoning_chains : [])) {
|
|
828
|
+
importedCards.push(cardApi.createCard('reasoning', chain));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (entries.has('KDNA_Evolution.json')) {
|
|
832
|
+
const evolution = JSON.parse(entries.get('KDNA_Evolution.json').toString());
|
|
833
|
+
for (const stage of (Array.isArray(evolution.stages) ? evolution.stages : [])) {
|
|
834
|
+
importedCards.push(cardApi.createCard('evolution_stage', stage));
|
|
835
|
+
}
|
|
719
836
|
}
|
|
720
837
|
}
|
|
721
838
|
|
|
@@ -818,7 +935,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
818
935
|
}
|
|
819
936
|
|
|
820
937
|
if (core) {
|
|
821
|
-
for (const ax of (core.axioms
|
|
938
|
+
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
822
939
|
const fields = {};
|
|
823
940
|
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
824
941
|
if (key in ax) fields[key] = ax[key];
|
|
@@ -826,14 +943,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
826
943
|
}
|
|
827
944
|
importedCards.push(cardApi.createCard('axiom', fields));
|
|
828
945
|
}
|
|
829
|
-
for (const ont of (core.ontology
|
|
946
|
+
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
830
947
|
importedCards.push(cardApi.createCard('ontology', {
|
|
831
948
|
one_sentence: ont.one_sentence || ont.essence || '',
|
|
832
949
|
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
833
950
|
trigger_signal: ont.trigger_signal || '',
|
|
834
951
|
}));
|
|
835
952
|
}
|
|
836
|
-
for (const b of (core.boundaries
|
|
953
|
+
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
837
954
|
importedCards.push(cardApi.createCard('boundary', {
|
|
838
955
|
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
839
956
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
@@ -851,7 +968,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
851
968
|
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
852
969
|
}
|
|
853
970
|
// NEW: import stances
|
|
854
|
-
for (const s of (core.stances
|
|
971
|
+
for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
855
972
|
const text = typeof s === 'string' ? s : s.stance || '';
|
|
856
973
|
importedCards.push(cardApi.createCard('stance', {
|
|
857
974
|
statement: text,
|
|
@@ -860,7 +977,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
860
977
|
}));
|
|
861
978
|
}
|
|
862
979
|
// NEW: import frameworks
|
|
863
|
-
for (const fw of (core.frameworks
|
|
980
|
+
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
864
981
|
importedCards.push(cardApi.createCard('framework', {
|
|
865
982
|
name: fw.name || '', when_to_use: fw.when_to_use || '',
|
|
866
983
|
steps: fw.steps || [],
|
|
@@ -871,18 +988,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
871
988
|
if (patterns) {
|
|
872
989
|
// NEW: import terminology
|
|
873
990
|
if (patterns.terminology) {
|
|
874
|
-
for (const t of (patterns.terminology.standard_terms
|
|
991
|
+
for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
|
|
875
992
|
importedCards.push(cardApi.createCard('term', {
|
|
876
993
|
term: t.term || '', definition: t.definition || '',
|
|
877
994
|
}));
|
|
878
995
|
}
|
|
879
|
-
for (const bt of (patterns.terminology.banned_terms
|
|
996
|
+
for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
|
|
880
997
|
importedCards.push(cardApi.createCard('banned_term', {
|
|
881
998
|
term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
|
|
882
999
|
}));
|
|
883
1000
|
}
|
|
884
1001
|
}
|
|
885
|
-
for (const ms of (patterns.misunderstandings
|
|
1002
|
+
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
886
1003
|
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
887
1004
|
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
888
1005
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
@@ -892,6 +1009,15 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
892
1009
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
893
1010
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
894
1011
|
}
|
|
1012
|
+
// Bug (aesthetic round-trip): prior version dropped
|
|
1013
|
+
// `patterns.aesthetics` on import. `cardsFromV1Payload` and
|
|
1014
|
+
// `cardsFromLegacyPayload` both read the field, so a domain
|
|
1015
|
+
// that authored aesthetic cards in either format would lose
|
|
1016
|
+
// them when imported through `create --from-folder`. The fix
|
|
1017
|
+
// adds the missing loop here.
|
|
1018
|
+
for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
|
|
1019
|
+
importedCards.push(cardApi.createCard('aesthetic', aesthetic.fields || aesthetic));
|
|
1020
|
+
}
|
|
895
1021
|
}
|
|
896
1022
|
|
|
897
1023
|
// NEW: import scenarios
|
|
@@ -1468,7 +1594,7 @@ function cmdMigrate(args) {
|
|
|
1468
1594
|
projectPath = loaded.projectPath;
|
|
1469
1595
|
if (!requestedName && project.name) name = project.name;
|
|
1470
1596
|
} else {
|
|
1471
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-'));
|
|
1597
|
+
tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
|
|
1472
1598
|
const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity);
|
|
1473
1599
|
project = imported.project;
|
|
1474
1600
|
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
@@ -1483,7 +1609,7 @@ function cmdMigrate(args) {
|
|
|
1483
1609
|
|
|
1484
1610
|
// Reject if critical judgment fields are missing from axioms
|
|
1485
1611
|
const criticalMissing = [];
|
|
1486
|
-
for (const card of (project.cards
|
|
1612
|
+
for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
|
|
1487
1613
|
if (card.type !== 'axiom') continue;
|
|
1488
1614
|
for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
1489
1615
|
const val = card.fields && card.fields[field];
|
|
@@ -1616,7 +1742,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
|
|
|
1616
1742
|
}
|
|
1617
1743
|
}
|
|
1618
1744
|
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
1619
|
-
const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
|
|
1745
|
+
const v1Dir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-')));
|
|
1620
1746
|
try {
|
|
1621
1747
|
const manifestDefaults = buildV1Manifest(project, name);
|
|
1622
1748
|
const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
|
|
@@ -2089,10 +2215,16 @@ function cmdSourceClassify(args) {
|
|
|
2089
2215
|
const target = project.distillation_target;
|
|
2090
2216
|
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
2091
2217
|
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2218
|
+
// Bug (#66 + #69): the previous version of this comment claimed
|
|
2219
|
+
// `evidence_materials` was canonical and `evidence` was the legacy
|
|
2220
|
+
// fallback. The reality was the opposite — `addEvidence` in
|
|
2221
|
+
// @aikdna/kdna-studio-core wrote only to `project.evidence`, so
|
|
2222
|
+
// every consumer that read `project.evidence_materials` (this
|
|
2223
|
+
// command, cmdFilter, cmdDistill) saw an empty list. The fix in
|
|
2224
|
+
// studio-core 1.7.5 makes `addEvidence` write to BOTH fields; the
|
|
2225
|
+
// canonical name is now the one this comment names, not the one
|
|
2226
|
+
// it used to name. The fallback `|| project.evidence` stays so
|
|
2227
|
+
// legacy projects (pre-1.7.5) still classify correctly.
|
|
2096
2228
|
const evidence = project.evidence_materials || project.evidence || [];
|
|
2097
2229
|
const results = evidence.map(e => {
|
|
2098
2230
|
const text = (e.content || e.title || '').toLowerCase();
|
|
@@ -2286,8 +2418,31 @@ async function cmdFeynman(args) {
|
|
|
2286
2418
|
// Canonical field name in the card schema is `feynman_restatement`, not
|
|
2287
2419
|
// `feynman_text`. The old check was dead code (nothing writes the wrong
|
|
2288
2420
|
// field) and made this command fail on every card.
|
|
2421
|
+
//
|
|
2422
|
+
// Bug (#62 + #68 follow-up): the prior `if (!card.feynman_restatement)
|
|
2423
|
+
// fail(...)` block rejected every card that had no human-written
|
|
2424
|
+
// restatement, even when the AI evaluator in feynman.js was
|
|
2425
|
+
// prepared to auto-synthesise one. A caller without a configured
|
|
2426
|
+
// LLM (no `KDNA_API_KEY`, no `kdna-studio llm config`) would
|
|
2427
|
+
// get a `fail` instead of a structured note. The fix removes the
|
|
2428
|
+
// guard: a missing restatement is now handed off to the evaluator
|
|
2429
|
+
// (which either asks the LLM to evaluate a synthesised one or,
|
|
2430
|
+
// without an LLM, returns a structured `synthesised_restatement`
|
|
2431
|
+
// for the caller to use as a starting point). The error message
|
|
2432
|
+
// is kept as a secondary path: only if the evaluator itself fails
|
|
2433
|
+
// does the command exit non-zero, and only then with a clear
|
|
2434
|
+
// pointer to the manual `card update` path.
|
|
2435
|
+
//
|
|
2436
|
+
// We do still need a restatement-shaped object to feed the
|
|
2437
|
+
// evaluator, so we synthesise one in-memory when the card has
|
|
2438
|
+
// none. This does NOT persist the synthesised restatement to the
|
|
2439
|
+
// project (so the user still has to opt in via `card update` to
|
|
2440
|
+
// keep one across runs), but it lets the no-LLM path return a
|
|
2441
|
+
// useful result instead of a hard fail.
|
|
2289
2442
|
if (!card.feynman_restatement) {
|
|
2290
|
-
|
|
2443
|
+
const synthesised = ai.feynman.synthFeynmanRestatement(card.fields || {});
|
|
2444
|
+
card.feynman_restatement = { text: synthesised, synthesised: true };
|
|
2445
|
+
console.warn(`No feynman_restatement on ${cardId}; using synthesised:\n "${synthesised}"`);
|
|
2291
2446
|
}
|
|
2292
2447
|
|
|
2293
2448
|
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.7",
|
|
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.6",
|
|
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 };
|