@aikdna/kdna-studio-cli 0.8.2 → 0.8.4
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 +335 -82
- package/package.json +2 -2
- package/src/ai/feynman.js +14 -2
package/bin/kdna-studio.js
CHANGED
|
@@ -99,6 +99,40 @@ function option(args, name, fallback = null) {
|
|
|
99
99
|
return value;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a passphrase from CLI flags, environment, or stdin.
|
|
104
|
+
*
|
|
105
|
+
* SECURITY: passing a passphrase as `--passphrase <value>` exposes it in
|
|
106
|
+
* `ps aux` output and shell history. The recommended path is one of:
|
|
107
|
+
* 1. --passphrase-stdin — read from stdin (pipe-friendly, no TTY hang)
|
|
108
|
+
* 2. KDNA_PASSPHRASE env var (less secure but no process-list leak)
|
|
109
|
+
* 3. --passphrase <pw> — fallback only; prints a warning
|
|
110
|
+
*
|
|
111
|
+
* Returns the passphrase string, or null if none was provided.
|
|
112
|
+
*/
|
|
113
|
+
function resolvePassphrase(args) {
|
|
114
|
+
if (args.includes('--passphrase-stdin')) {
|
|
115
|
+
if (process.stdin.isTTY) {
|
|
116
|
+
fail(
|
|
117
|
+
'--passphrase-stdin requires the passphrase to be piped in on stdin.\n' +
|
|
118
|
+
'Example: read -s PW && echo "$PW" | kdna-studio export ... --sign --passphrase-stdin'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return fs.readFileSync(0, 'utf8').trim();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
fail(`Could not read passphrase from stdin: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (process.env.KDNA_PASSPHRASE) return process.env.KDNA_PASSPHRASE;
|
|
128
|
+
const fromFlag = option(args, '--passphrase');
|
|
129
|
+
if (fromFlag) {
|
|
130
|
+
console.error('Warning: --passphrase <value> exposes the secret in `ps aux` and shell history. Prefer --passphrase-stdin or KDNA_PASSPHRASE env var.');
|
|
131
|
+
return fromFlag;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
102
136
|
function resolveApiKey(args) {
|
|
103
137
|
// 1. KDNA_API_KEY environment variable (preferred)
|
|
104
138
|
if (process.env.KDNA_API_KEY) return process.env.KDNA_API_KEY;
|
|
@@ -287,10 +321,27 @@ function cardsFromV1Payload(payload) {
|
|
|
287
321
|
failure_risk: ax.failure_risk || '',
|
|
288
322
|
}, ax.id || null);
|
|
289
323
|
}
|
|
324
|
+
for (const ont of (payload.core?.ontology || [])) {
|
|
325
|
+
pushImportedCard(cards, 'ontology', ont, ont.id || null);
|
|
326
|
+
}
|
|
327
|
+
for (const fw of (payload.core?.frameworks || [])) {
|
|
328
|
+
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
329
|
+
}
|
|
330
|
+
for (const stance of (payload.core?.stances || [])) {
|
|
331
|
+
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
332
|
+
}
|
|
333
|
+
for (const aesthetic of (payload.core?.aesthetics || [])) {
|
|
334
|
+
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
335
|
+
}
|
|
290
336
|
for (const boundary of (payload.core?.boundaries || [])) {
|
|
291
337
|
pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
|
|
292
338
|
}
|
|
293
|
-
|
|
339
|
+
// risk_model may be either an array of risk items (legacy) or
|
|
340
|
+
// `{risks: [...]}` (current). Treat both shapes; ignore everything else
|
|
341
|
+
// rather than crashing with "object is not iterable".
|
|
342
|
+
const riskModel = payload.core?.risk_model;
|
|
343
|
+
const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
|
|
344
|
+
for (const risk of riskItems) {
|
|
294
345
|
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
295
346
|
}
|
|
296
347
|
for (const pattern of (payload.patterns || [])) {
|
|
@@ -304,7 +355,16 @@ function cardsFromV1Payload(payload) {
|
|
|
304
355
|
for (const item of (payload.cases || [])) {
|
|
305
356
|
pushImportedCard(cards, 'case', item, item.id || null);
|
|
306
357
|
}
|
|
307
|
-
|
|
358
|
+
// Accept both `self_check` (canonical) and `self_checks` (legacy schema
|
|
359
|
+
// name) so we round-trip assets written by either version of the
|
|
360
|
+
// payload-profile schema. Bug (#53): prior version used
|
|
361
|
+
// `arr || []`, which would crash with "object is not iterable" if
|
|
362
|
+
// the field was a truthy non-array (e.g. a single `{question: ...}`
|
|
363
|
+
// object instead of an array). The fix guards with Array.isArray.
|
|
364
|
+
const rawSelfCheck = payload.reasoning?.self_check
|
|
365
|
+
|| payload.reasoning?.self_checks;
|
|
366
|
+
const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
|
|
367
|
+
for (const selfCheck of selfCheckArr) {
|
|
308
368
|
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
309
369
|
pushImportedCard(cards, 'self_check', fields, fields.id || null);
|
|
310
370
|
}
|
|
@@ -320,7 +380,14 @@ function cardsFromV1Payload(payload) {
|
|
|
320
380
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
321
381
|
}
|
|
322
382
|
for (const stage of (payload.evolution?.stages || [])) {
|
|
323
|
-
|
|
383
|
+
// Distinguish source-authored stages (preserved identity) from
|
|
384
|
+
// audit-log-derived stages (synthesised). The compile path tags
|
|
385
|
+
// source_authored: true; import those verbatim and skip the rest
|
|
386
|
+
// so the importer doesn't double-count what was already in the
|
|
387
|
+
// locked judgment cards.
|
|
388
|
+
if (stage.source_authored === true) {
|
|
389
|
+
pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
|
|
390
|
+
}
|
|
324
391
|
}
|
|
325
392
|
return cards;
|
|
326
393
|
}
|
|
@@ -376,7 +443,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
376
443
|
failure_risk: ms.failure_risk || '',
|
|
377
444
|
}, ms.id || null);
|
|
378
445
|
}
|
|
379
|
-
for (const sc of (patterns.self_check
|
|
446
|
+
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
380
447
|
const question = typeof sc === 'string' ? sc : sc.question || '';
|
|
381
448
|
pushImportedCard(cards, 'self_check', { question }, sc.id || null);
|
|
382
449
|
}
|
|
@@ -566,52 +633,65 @@ function importFromKdna(kdnaPath) {
|
|
|
566
633
|
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
567
634
|
};
|
|
568
635
|
|
|
569
|
-
// Extract cards
|
|
636
|
+
// Extract cards. Modern v1 packages carry `kdna.json` + `payload.kdnab`
|
|
637
|
+
// and use the unified judgment profile. Legacy packages split content
|
|
638
|
+
// across KDNA_*.json files. Prefer payload.kdnab when present — it
|
|
639
|
+
// covers all 8+ card types (axiom / boundary / risk / aesthetic /
|
|
640
|
+
// ontology / misunderstanding / self_check / scenario / case /
|
|
641
|
+
// reasoning / evolution_stage / stance / framework / term / banned_term)
|
|
642
|
+
// via cardsFromPayload / cardsFromV1Payload.
|
|
570
643
|
const importedCards = [];
|
|
571
|
-
if (entries.has('
|
|
572
|
-
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
573
|
-
for (const ax of (core.axioms || [])) {
|
|
574
|
-
const fields = {};
|
|
575
|
-
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
576
|
-
if (k in ax) fields[k] = ax[k];
|
|
577
|
-
}
|
|
578
|
-
importedCards.push(cardApi.createCard('axiom', fields));
|
|
579
|
-
}
|
|
580
|
-
for (const ont of (core.ontology || [])) {
|
|
581
|
-
importedCards.push(cardApi.createCard('ontology', {
|
|
582
|
-
one_sentence: ont.one_sentence || ont.essence || '',
|
|
583
|
-
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
584
|
-
trigger_signal: ont.trigger_signal || '',
|
|
585
|
-
}));
|
|
586
|
-
}
|
|
587
|
-
for (const b of (core.boundaries || [])) {
|
|
588
|
-
importedCards.push(cardApi.createCard('boundary', {
|
|
589
|
-
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
590
|
-
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
591
|
-
}));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (entries.has('KDNA_Patterns.json')) {
|
|
595
|
-
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
596
|
-
for (const ms of (pat.misunderstandings || [])) {
|
|
597
|
-
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
598
|
-
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
599
|
-
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
600
|
-
}));
|
|
601
|
-
}
|
|
602
|
-
for (const sc of (pat.self_check || [])) {
|
|
603
|
-
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
604
|
-
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (importedCards.length === 0 && entries.has('payload.kdnab')) {
|
|
644
|
+
if (entries.has('payload.kdnab')) {
|
|
609
645
|
try {
|
|
610
646
|
const payload = decodePayload(entries.get('payload.kdnab'), manifest);
|
|
611
647
|
importedCards.push(...cardsFromPayload(payload));
|
|
612
648
|
} catch (e) {
|
|
613
649
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
614
650
|
}
|
|
651
|
+
} else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
|
|
652
|
+
// Legacy fallback — only used when payload.kdnab is absent. This path
|
|
653
|
+
// intentionally covers a strict subset of card types and is not the
|
|
654
|
+
// recommended import path.
|
|
655
|
+
if (entries.has('KDNA_Core.json')) {
|
|
656
|
+
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
657
|
+
for (const ax of (core.axioms || [])) {
|
|
658
|
+
const fields = {};
|
|
659
|
+
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
660
|
+
if (k in ax) fields[k] = ax[k];
|
|
661
|
+
}
|
|
662
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
663
|
+
}
|
|
664
|
+
for (const ont of (core.ontology || [])) {
|
|
665
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
666
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
667
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
668
|
+
trigger_signal: ont.trigger_signal || '',
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
for (const b of (core.boundaries || [])) {
|
|
672
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
673
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
674
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (entries.has('KDNA_Patterns.json')) {
|
|
679
|
+
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
680
|
+
for (const ms of (pat.misunderstandings || [])) {
|
|
681
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
682
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
683
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
for (const sc of (Array.isArray(pat.self_check) ? pat.self_check : [])) {
|
|
687
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
688
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (importedCards.length === 0) {
|
|
694
|
+
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
|
|
615
695
|
}
|
|
616
696
|
|
|
617
697
|
return { lineage, cards: importedCards, source_manifest: manifest };
|
|
@@ -667,6 +747,24 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
667
747
|
const reasoning = loadJson('KDNA_Reasoning.json');
|
|
668
748
|
const evolution = loadJson('KDNA_Evolution.json');
|
|
669
749
|
|
|
750
|
+
// If none of the standard KDNA_*.json files were found, that almost
|
|
751
|
+
// always means the user pointed at the wrong directory or the source uses
|
|
752
|
+
// a non-standard filename. Report what was *actually* present so they can
|
|
753
|
+
// rename or move their files.
|
|
754
|
+
const expectedFiles = ['KDNA_Core.json', 'KDNA_Patterns.json', 'KDNA_Scenarios.json',
|
|
755
|
+
'KDNA_Cases.json', 'KDNA_Reasoning.json', 'KDNA_Evolution.json', 'kdna.json'];
|
|
756
|
+
const loadedCount = audit.filesFound.length;
|
|
757
|
+
if (loadedCount === 0) {
|
|
758
|
+
let dirContents = [];
|
|
759
|
+
try { dirContents = fs.readdirSync(absSource); } catch { /* ignore */ }
|
|
760
|
+
fail(
|
|
761
|
+
`No KDNA source files found in ${absSource}.\n` +
|
|
762
|
+
`Expected one of: ${expectedFiles.join(', ')}\n` +
|
|
763
|
+
`Directory contains ${dirContents.length} entr${dirContents.length === 1 ? 'y' : 'ies'}: ` +
|
|
764
|
+
(dirContents.length > 0 ? dirContents.slice(0, 20).join(', ') + (dirContents.length > 20 ? ', ...' : '') : '(empty)')
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
670
768
|
if (core) {
|
|
671
769
|
for (const ax of (core.axioms || [])) {
|
|
672
770
|
const fields = {};
|
|
@@ -689,7 +787,15 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
689
787
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
690
788
|
}));
|
|
691
789
|
}
|
|
692
|
-
|
|
790
|
+
// Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
|
|
791
|
+
// (either as `{risks: [...]}` or as an array itself).
|
|
792
|
+
let risksSource = core.risks;
|
|
793
|
+
if (!Array.isArray(risksSource)) {
|
|
794
|
+
if (Array.isArray(core.risk_model)) risksSource = core.risk_model;
|
|
795
|
+
else if (Array.isArray(core.risk_model?.risks)) risksSource = core.risk_model.risks;
|
|
796
|
+
else risksSource = [];
|
|
797
|
+
}
|
|
798
|
+
for (const r of risksSource) {
|
|
693
799
|
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
694
800
|
}
|
|
695
801
|
// NEW: import stances
|
|
@@ -730,7 +836,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
730
836
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
731
837
|
}));
|
|
732
838
|
}
|
|
733
|
-
for (const sc of (patterns.self_check
|
|
839
|
+
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
734
840
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
735
841
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
736
842
|
}
|
|
@@ -756,12 +862,23 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
756
862
|
}
|
|
757
863
|
}
|
|
758
864
|
|
|
759
|
-
// NEW: import reasoning chains
|
|
865
|
+
// NEW: import reasoning chains. Field names must match what compileReasoning
|
|
866
|
+
// reads (chain/principle/concrete_action/axiom/one_sentence) so the
|
|
867
|
+
// round-trip preserves the source's structure. Bug: prior code stored
|
|
868
|
+
// {name, conclusion, so_what}, none of which the compile path recognised,
|
|
869
|
+
// so the cards were silently rewritten as axiom-derived fallback chains
|
|
870
|
+
// — looking identical to misunderstanding cards and losing the source's
|
|
871
|
+
// identity.
|
|
760
872
|
if (reasoning && reasoning.reasoning_chains) {
|
|
761
873
|
for (const rc of reasoning.reasoning_chains) {
|
|
762
874
|
importedCards.push(cardApi.createCard('reasoning', {
|
|
763
|
-
|
|
764
|
-
|
|
875
|
+
axiom: rc.axiom || '',
|
|
876
|
+
one_sentence: rc.one_sentence || rc.conclusion || '',
|
|
877
|
+
chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
|
|
878
|
+
principle: rc.principle || rc.name || '',
|
|
879
|
+
concrete_action: rc.concrete_action || rc.so_what || '',
|
|
880
|
+
so_what: rc.so_what || rc.concrete_action || '',
|
|
881
|
+
conclusion: rc.conclusion || '',
|
|
765
882
|
}));
|
|
766
883
|
}
|
|
767
884
|
}
|
|
@@ -945,13 +1062,23 @@ function cmdImport(args) {
|
|
|
945
1062
|
return;
|
|
946
1063
|
}
|
|
947
1064
|
let content;
|
|
1065
|
+
let fd = null;
|
|
948
1066
|
try {
|
|
949
|
-
|
|
1067
|
+
fd = fs.openSync(filePath, 'r');
|
|
950
1068
|
const buf = Buffer.alloc(120000);
|
|
951
1069
|
const bytesRead = fs.readSync(fd, buf, 0, 120000, 0);
|
|
952
|
-
fs.closeSync(fd);
|
|
953
1070
|
content = buf.toString('utf8', 0, bytesRead);
|
|
954
1071
|
} catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
|
|
1072
|
+
finally {
|
|
1073
|
+
// Bug (#51): prior version called fs.closeSync only on the
|
|
1074
|
+
// success path, so any readSync failure left the file descriptor
|
|
1075
|
+
// open. Under bulk import (300 files), a single error mid-loop
|
|
1076
|
+
// could exhaust the per-process fd limit and silently break
|
|
1077
|
+
// later imports.
|
|
1078
|
+
if (fd !== null) {
|
|
1079
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
955
1082
|
|
|
956
1083
|
const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
|
|
957
1084
|
evidenceApi.addEvidence(project, evidence);
|
|
@@ -1158,10 +1285,10 @@ function cmdCard(args) {
|
|
|
1158
1285
|
return null;
|
|
1159
1286
|
}
|
|
1160
1287
|
lockPayload.creator_id = identity.creator_id;
|
|
1161
|
-
const passphrase =
|
|
1288
|
+
const passphrase = resolvePassphrase(args);
|
|
1162
1289
|
try {
|
|
1163
1290
|
if (identity.encrypted && !passphrase) {
|
|
1164
|
-
errors.push(`${card.id}: Private key is encrypted — provide --passphrase`);
|
|
1291
|
+
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin or KDNA_PASSPHRASE`);
|
|
1165
1292
|
return null;
|
|
1166
1293
|
}
|
|
1167
1294
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
@@ -1274,6 +1401,12 @@ function cmdMigrate(args) {
|
|
|
1274
1401
|
let projectPath = null;
|
|
1275
1402
|
let project = null;
|
|
1276
1403
|
|
|
1404
|
+
// Wrap everything in try / finally so a mid-migrate failure (e.g. missing
|
|
1405
|
+
// critical fields, gate blocked, signing error) still cleans up tmpDir.
|
|
1406
|
+
// Bug: prior code only cleaned up on the v1 and v2 success paths; any
|
|
1407
|
+
// non-v1 export path that threw leaked `/tmp/kdna-migrate-*` entries
|
|
1408
|
+
// indefinitely.
|
|
1409
|
+
try {
|
|
1277
1410
|
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1278
1411
|
let creatorIdentity = null;
|
|
1279
1412
|
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
|
|
@@ -1289,6 +1422,13 @@ function cmdMigrate(args) {
|
|
|
1289
1422
|
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
1290
1423
|
}
|
|
1291
1424
|
|
|
1425
|
+
// Bug (#54): --allow-incomplete used to be read AFTER the critical
|
|
1426
|
+
// fields check, so a caller asking to bypass the gate was rejected
|
|
1427
|
+
// for the wrong reason. Resolve the flag here so the critical-missing
|
|
1428
|
+
// check honours it (and reports the bypass correctly) instead of
|
|
1429
|
+
// always failing.
|
|
1430
|
+
const allowIncompleteEarly = args.includes('--allow-incomplete');
|
|
1431
|
+
|
|
1292
1432
|
// Reject if critical judgment fields are missing from axioms
|
|
1293
1433
|
const criticalMissing = [];
|
|
1294
1434
|
for (const card of (project.cards || [])) {
|
|
@@ -1301,13 +1441,21 @@ function cmdMigrate(args) {
|
|
|
1301
1441
|
}
|
|
1302
1442
|
}
|
|
1303
1443
|
if (criticalMissing.length > 0) {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1444
|
+
if (allowIncompleteEarly) {
|
|
1445
|
+
console.warn(
|
|
1446
|
+
`--allow-incomplete: bypassing ${criticalMissing.length} critical axiom-field check(s). ` +
|
|
1447
|
+
`The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
|
|
1448
|
+
);
|
|
1449
|
+
} else {
|
|
1450
|
+
fail(
|
|
1451
|
+
`Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
|
|
1452
|
+
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
1453
|
+
` decide when to load this domain). Add them to your source files first,\n` +
|
|
1454
|
+
` or pass --allow-incomplete to bypass.\n` +
|
|
1455
|
+
` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
|
|
1456
|
+
(criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1311
1459
|
}
|
|
1312
1460
|
|
|
1313
1461
|
// Step 2: approve and Human Lock all cards
|
|
@@ -1325,7 +1473,7 @@ function cmdMigrate(args) {
|
|
|
1325
1473
|
const identity = creatorApi.loadIdentity();
|
|
1326
1474
|
if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
|
|
1327
1475
|
lockPayload.creator_id = identity.creator_id;
|
|
1328
|
-
const passphrase =
|
|
1476
|
+
const passphrase = resolvePassphrase(args);
|
|
1329
1477
|
try {
|
|
1330
1478
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
1331
1479
|
card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
|
|
@@ -1383,6 +1531,17 @@ function cmdMigrate(args) {
|
|
|
1383
1531
|
// Cleanup temp
|
|
1384
1532
|
if (tmpDir) {
|
|
1385
1533
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1534
|
+
tmpDir = null;
|
|
1535
|
+
}
|
|
1536
|
+
} finally {
|
|
1537
|
+
// Single, guaranteed cleanup point for tmpDir even when an earlier
|
|
1538
|
+
// step (critical-missing check, gate block, signing failure) bails
|
|
1539
|
+
// out via `fail()`. `fail()` ultimately calls process.exit, but
|
|
1540
|
+
// if a future caller swallows that or the runtime traps the throw,
|
|
1541
|
+
// we still leave the filesystem clean.
|
|
1542
|
+
if (tmpDir) {
|
|
1543
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1544
|
+
}
|
|
1386
1545
|
}
|
|
1387
1546
|
}
|
|
1388
1547
|
|
|
@@ -1545,11 +1704,27 @@ function stableStringify(value) {
|
|
|
1545
1704
|
}
|
|
1546
1705
|
|
|
1547
1706
|
function manifestForSigning(manifest) {
|
|
1548
|
-
|
|
1707
|
+
// Mirrors the kdna-core implementation in
|
|
1708
|
+
// packages/kdna-core/src/asset-reader.js#manifestForSignature /
|
|
1709
|
+
// manifestForDigest. Anything not stripped here will end up in the
|
|
1710
|
+
// signing payload, and a mismatch between producer (this CLI) and
|
|
1711
|
+
// verifier (kdna-core) will cause the signature check to fail.
|
|
1712
|
+
//
|
|
1713
|
+
// Bug: prior version did not strip `_source` (only set by eval/loader
|
|
1714
|
+
// helpers) and did not recursively strip `authoring.content_digest`,
|
|
1715
|
+
// so a payload with either field would diverge from kdna-core's
|
|
1716
|
+
// canonicalisation and report a false-positive verification failure.
|
|
1717
|
+
const copy = { ...(manifest || {}) };
|
|
1549
1718
|
delete copy.signature;
|
|
1550
1719
|
delete copy.asset_digest;
|
|
1551
1720
|
delete copy.container_sha256;
|
|
1552
1721
|
delete copy.content_digest;
|
|
1722
|
+
delete copy._source;
|
|
1723
|
+
if (copy.authoring && typeof copy.authoring === 'object') {
|
|
1724
|
+
const auth = { ...copy.authoring };
|
|
1725
|
+
delete auth.content_digest;
|
|
1726
|
+
copy.authoring = auth;
|
|
1727
|
+
}
|
|
1553
1728
|
return copy;
|
|
1554
1729
|
}
|
|
1555
1730
|
|
|
@@ -1579,7 +1754,7 @@ function publicKeyFingerprint(publicKeyPem) {
|
|
|
1579
1754
|
function applySignature(files, passphrase = null) {
|
|
1580
1755
|
const paths = identityPaths();
|
|
1581
1756
|
if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
|
|
1582
|
-
fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
|
|
1757
|
+
fail('Signing requires KDNA identity keys. Run: kdna-studio identity init', EXIT.TRUST_FAILED);
|
|
1583
1758
|
}
|
|
1584
1759
|
const manifest = JSON.parse(files['kdna.json']);
|
|
1585
1760
|
const publicKeyPem = fs.readFileSync(paths.publicKey, 'utf8');
|
|
@@ -1617,9 +1792,34 @@ function cmdExport(args) {
|
|
|
1617
1792
|
// Use args.includes() for --password-stdin since it is a boolean flag
|
|
1618
1793
|
// (option() rejects it for "missing value").
|
|
1619
1794
|
const useStdin = args.includes('--password-stdin');
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1795
|
+
let password;
|
|
1796
|
+
if (useStdin) {
|
|
1797
|
+
// fs.readFileSync(0) on a TTY waits forever for input and never
|
|
1798
|
+
// returns — the export appears to hang. Refuse up front with a
|
|
1799
|
+
// clear pointer to the right invocation.
|
|
1800
|
+
if (process.stdin.isTTY) {
|
|
1801
|
+
fail(
|
|
1802
|
+
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
1803
|
+
'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --format v1 --out <file.kdna> --password-stdin\n' +
|
|
1804
|
+
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
password = fs.readFileSync(0, 'utf8').trim();
|
|
1809
|
+
} catch (e) {
|
|
1810
|
+
fail(`Could not read password from stdin: ${e.message}`);
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
// Bug (#55): prior version only consulted --password / --passphrase
|
|
1814
|
+
// here, so a caller who set KDNA_PASSPHRASE for the signing path
|
|
1815
|
+
// (works via resolvePassphrase) found that the v1 encryption
|
|
1816
|
+
// password was silently null. Resolve from the same sources as
|
|
1817
|
+
// the signing path so the two stay in sync.
|
|
1818
|
+
password = process.env.KDNA_PASSPHRASE
|
|
1819
|
+
|| process.env.KDNA_PASSWORD
|
|
1820
|
+
|| option(args, '--password')
|
|
1821
|
+
|| option(args, '--passphrase');
|
|
1822
|
+
}
|
|
1623
1823
|
exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
1624
1824
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
1625
1825
|
password,
|
|
@@ -1654,7 +1854,7 @@ function cmdExport(args) {
|
|
|
1654
1854
|
provenance.content_fingerprint = finalDigest;
|
|
1655
1855
|
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
1656
1856
|
|
|
1657
|
-
if (args.includes('--sign')) applySignature(files,
|
|
1857
|
+
if (args.includes('--sign')) applySignature(files, resolvePassphrase(args));
|
|
1658
1858
|
|
|
1659
1859
|
const entries = [['mimetype', files.mimetype]];
|
|
1660
1860
|
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
@@ -1744,7 +1944,7 @@ function cmdIdentity(args) {
|
|
|
1744
1944
|
const sub = args[0];
|
|
1745
1945
|
if (sub === 'init') {
|
|
1746
1946
|
const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
|
|
1747
|
-
const passphrase =
|
|
1947
|
+
const passphrase = resolvePassphrase(args);
|
|
1748
1948
|
try {
|
|
1749
1949
|
const identity = creatorApi.initIdentity(name, null, passphrase);
|
|
1750
1950
|
console.log(`Creator identity initialized:`);
|
|
@@ -1837,7 +2037,11 @@ function cmdSourceClassify(args) {
|
|
|
1837
2037
|
const target = project.distillation_target;
|
|
1838
2038
|
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
1839
2039
|
|
|
1840
|
-
|
|
2040
|
+
// Canonical evidence field is `evidence_materials` (set by cmdImport via
|
|
2041
|
+
// evidenceApi.addEvidence). The legacy `project.evidence` field never
|
|
2042
|
+
// receives writes, so reading it always produced an empty list and the
|
|
2043
|
+
// classifier reported "no evidence" even after `kdna-studio import`.
|
|
2044
|
+
const evidence = project.evidence_materials || project.evidence || [];
|
|
1841
2045
|
const results = evidence.map(e => {
|
|
1842
2046
|
const text = (e.content || e.title || '').toLowerCase();
|
|
1843
2047
|
const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
|
|
@@ -1965,14 +2169,29 @@ function cmdCandidate(args) {
|
|
|
1965
2169
|
if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
|
|
1966
2170
|
|
|
1967
2171
|
for (const c of accepted) {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
2172
|
+
// Promote candidates to draft cards. For axioms we must populate every
|
|
2173
|
+
// field the Human Lock gate and migrate path require — empty strings
|
|
2174
|
+
// cause migrate to fail ("critical fields missing from axioms"). When
|
|
2175
|
+
// a candidate lacks the field, fall back to the candidate's own
|
|
2176
|
+
// one_sentence/full_statement so the lock gate at least has substance
|
|
2177
|
+
// to verify. Authors are expected to enrich these fields before
|
|
2178
|
+
// locking.
|
|
2179
|
+
const fields = {};
|
|
2180
|
+
if (c.suggested_card_type === 'axiom') {
|
|
2181
|
+
fields.one_sentence = c.one_sentence || '';
|
|
2182
|
+
fields.full_statement = c.full_statement || c.one_sentence || '<TBD: full_statement>';
|
|
2183
|
+
fields.why = c.why || '<TBD: why>';
|
|
2184
|
+
// applies_when / does_not_apply_when are arrays, not strings.
|
|
2185
|
+
// Empty string is invalid; emit an empty array so checkRequiredFields
|
|
2186
|
+
// (which treats `length === 0` as missing) correctly reports them.
|
|
2187
|
+
fields.applies_when = Array.isArray(c.applies_when) ? c.applies_when : [];
|
|
2188
|
+
fields.does_not_apply_when = Array.isArray(c.does_not_apply_when) ? c.does_not_apply_when : [];
|
|
2189
|
+
fields.failure_risk = c.failure_risk || '<TBD: failure_risk>';
|
|
2190
|
+
} else {
|
|
2191
|
+
fields.one_sentence = c.one_sentence || '';
|
|
2192
|
+
fields.full_statement = c.full_statement || c.one_sentence || '';
|
|
2193
|
+
}
|
|
2194
|
+
const card = cardApi.createCard(c.suggested_card_type, fields);
|
|
1976
2195
|
card.evidence_refs = c.supporting_evidence_ids;
|
|
1977
2196
|
project.cards = project.cards || [];
|
|
1978
2197
|
project.cards.push(card);
|
|
@@ -2012,7 +2231,12 @@ async function cmdFeynman(args) {
|
|
|
2012
2231
|
if (cardIdx < 0) fail(`Card not found: ${cardId}`);
|
|
2013
2232
|
const card = project.cards[cardIdx];
|
|
2014
2233
|
|
|
2015
|
-
|
|
2234
|
+
// Canonical field name in the card schema is `feynman_restatement`, not
|
|
2235
|
+
// `feynman_text`. The old check was dead code (nothing writes the wrong
|
|
2236
|
+
// field) and made this command fail on every card.
|
|
2237
|
+
if (!card.feynman_restatement) {
|
|
2238
|
+
fail(`Card ${cardId} has no Feynman restatement. Generate one with the feynman authoring command, or set it via card update --field feynman_restatement='<json>'.`);
|
|
2239
|
+
}
|
|
2016
2240
|
|
|
2017
2241
|
console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2018
2242
|
const result = await ai.feynman.evaluate(llm.config(), card, {});
|
|
@@ -2043,12 +2267,41 @@ async function cmdTest(args) {
|
|
|
2043
2267
|
const domainName = project.name || 'Untitled Project';
|
|
2044
2268
|
const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
|
|
2045
2269
|
|
|
2270
|
+
// Bug (#50): prior version hand-rolled the prompt for the 3 preset
|
|
2271
|
+
// names and bypassed ai.testlab.testPreset entirely. The exported
|
|
2272
|
+
// testPreset function was dead code, and the help text advertised
|
|
2273
|
+
// names (baseline / edge / contradiction) that did not match the
|
|
2274
|
+
// actual preset keys in testlab.js (baseline / edge_case /
|
|
2275
|
+
// contradiction). Result: --preset edge silently ran the
|
|
2276
|
+
// contradiction branch, and any caller that asked for "edge_case"
|
|
2277
|
+
// fell through to the contradiction branch too.
|
|
2278
|
+
//
|
|
2279
|
+
// The fix routes the call through testPreset when a known preset is
|
|
2280
|
+
// given, and accepts both help-text names (baseline / edge /
|
|
2281
|
+
// contradiction) and the canonical testlab names (baseline / edge_case
|
|
2282
|
+
// / contradiction).
|
|
2283
|
+
const PRESET_ALIASES = {
|
|
2284
|
+
baseline: 'baseline',
|
|
2285
|
+
edge: 'edge_case',
|
|
2286
|
+
edge_case: 'edge_case',
|
|
2287
|
+
contradiction: 'contradiction',
|
|
2288
|
+
};
|
|
2289
|
+
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2046
2290
|
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2291
|
+
|
|
2292
|
+
if (canonical === 'baseline') {
|
|
2293
|
+
// Baseline is a single comparison: keep the prior single-result
|
|
2294
|
+
// shape so existing parsers / consumers see the same output.
|
|
2295
|
+
const prompt = `Test the core judgment: ${input}`;
|
|
2296
|
+
const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
|
|
2297
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
// For non-baseline presets, run the preset + the baseline so the user
|
|
2301
|
+
// can see both side by side. The preset prompt is the one in
|
|
2302
|
+
// ai/testlab.js — that's the code path that was dead before.
|
|
2303
|
+
const result = await ai.testlab.testPreset(llm.config(), domainName, input, domainPrompt, {});
|
|
2304
|
+
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2052
2305
|
}
|
|
2053
2306
|
|
|
2054
2307
|
const args = process.argv.slice(2);
|
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.4",
|
|
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.3",
|
|
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
|
@@ -31,8 +31,20 @@ Return ONLY valid JSON:
|
|
|
31
31
|
}`;
|
|
32
32
|
|
|
33
33
|
async function evaluate(config, card, options = {}) {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Bug: prior version read `card.feynman_text` (a field nothing in the
|
|
35
|
+
// codebase ever wrote) and flattened `card.one_sentence` /
|
|
36
|
+
// `card.full_statement` — the schema actually nests those under
|
|
37
|
+
// `card.fields`. The AI evaluator therefore always received an empty
|
|
38
|
+
// restatement and a blank axiom, no matter what was in the project.
|
|
39
|
+
// The fix reads the canonical schema field names and the nested
|
|
40
|
+
// `fields` object, with a fallback for callers that pass the flattened
|
|
41
|
+
// shape directly.
|
|
42
|
+
const fields = (card && card.fields) || card || {};
|
|
43
|
+
const axiomText = fields.one_sentence || fields.full_statement || fields.question || '';
|
|
44
|
+
const restatement = card.feynman_restatement
|
|
45
|
+
|| fields.feynman_restatement
|
|
46
|
+
|| fields.feynman_text
|
|
47
|
+
|| '';
|
|
36
48
|
|
|
37
49
|
if (!restatement) return { score: 0, criteria: {}, explanations: { notJustRepeat: 'No Feynman restatement provided.' }, suggestions: ['Write a Feynman restatement before evaluation.'] };
|
|
38
50
|
|