@aikdna/kdna-studio-cli 0.8.1 → 0.8.3
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 +254 -66
- package/package.json +2 -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,13 @@ 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.
|
|
361
|
+
const selfCheckArr = payload.reasoning?.self_check
|
|
362
|
+
|| payload.reasoning?.self_checks
|
|
363
|
+
|| [];
|
|
364
|
+
for (const selfCheck of selfCheckArr) {
|
|
308
365
|
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
309
366
|
pushImportedCard(cards, 'self_check', fields, fields.id || null);
|
|
310
367
|
}
|
|
@@ -320,7 +377,14 @@ function cardsFromV1Payload(payload) {
|
|
|
320
377
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
321
378
|
}
|
|
322
379
|
for (const stage of (payload.evolution?.stages || [])) {
|
|
323
|
-
|
|
380
|
+
// Distinguish source-authored stages (preserved identity) from
|
|
381
|
+
// audit-log-derived stages (synthesised). The compile path tags
|
|
382
|
+
// source_authored: true; import those verbatim and skip the rest
|
|
383
|
+
// so the importer doesn't double-count what was already in the
|
|
384
|
+
// locked judgment cards.
|
|
385
|
+
if (stage.source_authored === true) {
|
|
386
|
+
pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
|
|
387
|
+
}
|
|
324
388
|
}
|
|
325
389
|
return cards;
|
|
326
390
|
}
|
|
@@ -566,52 +630,65 @@ function importFromKdna(kdnaPath) {
|
|
|
566
630
|
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
567
631
|
};
|
|
568
632
|
|
|
569
|
-
// Extract cards
|
|
633
|
+
// Extract cards. Modern v1 packages carry `kdna.json` + `payload.kdnab`
|
|
634
|
+
// and use the unified judgment profile. Legacy packages split content
|
|
635
|
+
// across KDNA_*.json files. Prefer payload.kdnab when present — it
|
|
636
|
+
// covers all 8+ card types (axiom / boundary / risk / aesthetic /
|
|
637
|
+
// ontology / misunderstanding / self_check / scenario / case /
|
|
638
|
+
// reasoning / evolution_stage / stance / framework / term / banned_term)
|
|
639
|
+
// via cardsFromPayload / cardsFromV1Payload.
|
|
570
640
|
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')) {
|
|
641
|
+
if (entries.has('payload.kdnab')) {
|
|
609
642
|
try {
|
|
610
643
|
const payload = decodePayload(entries.get('payload.kdnab'), manifest);
|
|
611
644
|
importedCards.push(...cardsFromPayload(payload));
|
|
612
645
|
} catch (e) {
|
|
613
646
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
614
647
|
}
|
|
648
|
+
} else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
|
|
649
|
+
// Legacy fallback — only used when payload.kdnab is absent. This path
|
|
650
|
+
// intentionally covers a strict subset of card types and is not the
|
|
651
|
+
// recommended import path.
|
|
652
|
+
if (entries.has('KDNA_Core.json')) {
|
|
653
|
+
const core = JSON.parse(entries.get('KDNA_Core.json').toString());
|
|
654
|
+
for (const ax of (core.axioms || [])) {
|
|
655
|
+
const fields = {};
|
|
656
|
+
for (const k of ['one_sentence','full_statement','why','applies_when','does_not_apply_when','failure_risk']) {
|
|
657
|
+
if (k in ax) fields[k] = ax[k];
|
|
658
|
+
}
|
|
659
|
+
importedCards.push(cardApi.createCard('axiom', fields));
|
|
660
|
+
}
|
|
661
|
+
for (const ont of (core.ontology || [])) {
|
|
662
|
+
importedCards.push(cardApi.createCard('ontology', {
|
|
663
|
+
one_sentence: ont.one_sentence || ont.essence || '',
|
|
664
|
+
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
665
|
+
trigger_signal: ont.trigger_signal || '',
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
for (const b of (core.boundaries || [])) {
|
|
669
|
+
importedCards.push(cardApi.createCard('boundary', {
|
|
670
|
+
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
671
|
+
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
672
|
+
}));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (entries.has('KDNA_Patterns.json')) {
|
|
676
|
+
const pat = JSON.parse(entries.get('KDNA_Patterns.json').toString());
|
|
677
|
+
for (const ms of (pat.misunderstandings || [])) {
|
|
678
|
+
importedCards.push(cardApi.createCard('misunderstanding', {
|
|
679
|
+
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
680
|
+
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
for (const sc of (pat.self_check || [])) {
|
|
684
|
+
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
685
|
+
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (importedCards.length === 0) {
|
|
691
|
+
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
|
|
615
692
|
}
|
|
616
693
|
|
|
617
694
|
return { lineage, cards: importedCards, source_manifest: manifest };
|
|
@@ -667,6 +744,24 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
667
744
|
const reasoning = loadJson('KDNA_Reasoning.json');
|
|
668
745
|
const evolution = loadJson('KDNA_Evolution.json');
|
|
669
746
|
|
|
747
|
+
// If none of the standard KDNA_*.json files were found, that almost
|
|
748
|
+
// always means the user pointed at the wrong directory or the source uses
|
|
749
|
+
// a non-standard filename. Report what was *actually* present so they can
|
|
750
|
+
// rename or move their files.
|
|
751
|
+
const expectedFiles = ['KDNA_Core.json', 'KDNA_Patterns.json', 'KDNA_Scenarios.json',
|
|
752
|
+
'KDNA_Cases.json', 'KDNA_Reasoning.json', 'KDNA_Evolution.json', 'kdna.json'];
|
|
753
|
+
const loadedCount = audit.filesFound.length;
|
|
754
|
+
if (loadedCount === 0) {
|
|
755
|
+
let dirContents = [];
|
|
756
|
+
try { dirContents = fs.readdirSync(absSource); } catch { /* ignore */ }
|
|
757
|
+
fail(
|
|
758
|
+
`No KDNA source files found in ${absSource}.\n` +
|
|
759
|
+
`Expected one of: ${expectedFiles.join(', ')}\n` +
|
|
760
|
+
`Directory contains ${dirContents.length} entr${dirContents.length === 1 ? 'y' : 'ies'}: ` +
|
|
761
|
+
(dirContents.length > 0 ? dirContents.slice(0, 20).join(', ') + (dirContents.length > 20 ? ', ...' : '') : '(empty)')
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
670
765
|
if (core) {
|
|
671
766
|
for (const ax of (core.axioms || [])) {
|
|
672
767
|
const fields = {};
|
|
@@ -689,7 +784,15 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
689
784
|
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
690
785
|
}));
|
|
691
786
|
}
|
|
692
|
-
|
|
787
|
+
// Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
|
|
788
|
+
// (either as `{risks: [...]}` or as an array itself).
|
|
789
|
+
let risksSource = core.risks;
|
|
790
|
+
if (!Array.isArray(risksSource)) {
|
|
791
|
+
if (Array.isArray(core.risk_model)) risksSource = core.risk_model;
|
|
792
|
+
else if (Array.isArray(core.risk_model?.risks)) risksSource = core.risk_model.risks;
|
|
793
|
+
else risksSource = [];
|
|
794
|
+
}
|
|
795
|
+
for (const r of risksSource) {
|
|
693
796
|
importedCards.push(cardApi.createCard('risk', r.fields || r));
|
|
694
797
|
}
|
|
695
798
|
// NEW: import stances
|
|
@@ -756,12 +859,23 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
756
859
|
}
|
|
757
860
|
}
|
|
758
861
|
|
|
759
|
-
// NEW: import reasoning chains
|
|
862
|
+
// NEW: import reasoning chains. Field names must match what compileReasoning
|
|
863
|
+
// reads (chain/principle/concrete_action/axiom/one_sentence) so the
|
|
864
|
+
// round-trip preserves the source's structure. Bug: prior code stored
|
|
865
|
+
// {name, conclusion, so_what}, none of which the compile path recognised,
|
|
866
|
+
// so the cards were silently rewritten as axiom-derived fallback chains
|
|
867
|
+
// — looking identical to misunderstanding cards and losing the source's
|
|
868
|
+
// identity.
|
|
760
869
|
if (reasoning && reasoning.reasoning_chains) {
|
|
761
870
|
for (const rc of reasoning.reasoning_chains) {
|
|
762
871
|
importedCards.push(cardApi.createCard('reasoning', {
|
|
763
|
-
|
|
764
|
-
|
|
872
|
+
axiom: rc.axiom || '',
|
|
873
|
+
one_sentence: rc.one_sentence || rc.conclusion || '',
|
|
874
|
+
chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
|
|
875
|
+
principle: rc.principle || rc.name || '',
|
|
876
|
+
concrete_action: rc.concrete_action || rc.so_what || '',
|
|
877
|
+
so_what: rc.so_what || rc.concrete_action || '',
|
|
878
|
+
conclusion: rc.conclusion || '',
|
|
765
879
|
}));
|
|
766
880
|
}
|
|
767
881
|
}
|
|
@@ -1158,10 +1272,10 @@ function cmdCard(args) {
|
|
|
1158
1272
|
return null;
|
|
1159
1273
|
}
|
|
1160
1274
|
lockPayload.creator_id = identity.creator_id;
|
|
1161
|
-
const passphrase =
|
|
1275
|
+
const passphrase = resolvePassphrase(args);
|
|
1162
1276
|
try {
|
|
1163
1277
|
if (identity.encrypted && !passphrase) {
|
|
1164
|
-
errors.push(`${card.id}: Private key is encrypted — provide --passphrase`);
|
|
1278
|
+
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin or KDNA_PASSPHRASE`);
|
|
1165
1279
|
return null;
|
|
1166
1280
|
}
|
|
1167
1281
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
@@ -1274,6 +1388,12 @@ function cmdMigrate(args) {
|
|
|
1274
1388
|
let projectPath = null;
|
|
1275
1389
|
let project = null;
|
|
1276
1390
|
|
|
1391
|
+
// Wrap everything in try / finally so a mid-migrate failure (e.g. missing
|
|
1392
|
+
// critical fields, gate blocked, signing error) still cleans up tmpDir.
|
|
1393
|
+
// Bug: prior code only cleaned up on the v1 and v2 success paths; any
|
|
1394
|
+
// non-v1 export path that threw leaked `/tmp/kdna-migrate-*` entries
|
|
1395
|
+
// indefinitely.
|
|
1396
|
+
try {
|
|
1277
1397
|
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1278
1398
|
let creatorIdentity = null;
|
|
1279
1399
|
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
|
|
@@ -1325,7 +1445,7 @@ function cmdMigrate(args) {
|
|
|
1325
1445
|
const identity = creatorApi.loadIdentity();
|
|
1326
1446
|
if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
|
|
1327
1447
|
lockPayload.creator_id = identity.creator_id;
|
|
1328
|
-
const passphrase =
|
|
1448
|
+
const passphrase = resolvePassphrase(args);
|
|
1329
1449
|
try {
|
|
1330
1450
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
1331
1451
|
card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
|
|
@@ -1383,6 +1503,17 @@ function cmdMigrate(args) {
|
|
|
1383
1503
|
// Cleanup temp
|
|
1384
1504
|
if (tmpDir) {
|
|
1385
1505
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1506
|
+
tmpDir = null;
|
|
1507
|
+
}
|
|
1508
|
+
} finally {
|
|
1509
|
+
// Single, guaranteed cleanup point for tmpDir even when an earlier
|
|
1510
|
+
// step (critical-missing check, gate block, signing failure) bails
|
|
1511
|
+
// out via `fail()`. `fail()` ultimately calls process.exit, but
|
|
1512
|
+
// if a future caller swallows that or the runtime traps the throw,
|
|
1513
|
+
// we still leave the filesystem clean.
|
|
1514
|
+
if (tmpDir) {
|
|
1515
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1516
|
+
}
|
|
1386
1517
|
}
|
|
1387
1518
|
}
|
|
1388
1519
|
|
|
@@ -1545,11 +1676,27 @@ function stableStringify(value) {
|
|
|
1545
1676
|
}
|
|
1546
1677
|
|
|
1547
1678
|
function manifestForSigning(manifest) {
|
|
1548
|
-
|
|
1679
|
+
// Mirrors the kdna-core implementation in
|
|
1680
|
+
// packages/kdna-core/src/asset-reader.js#manifestForSignature /
|
|
1681
|
+
// manifestForDigest. Anything not stripped here will end up in the
|
|
1682
|
+
// signing payload, and a mismatch between producer (this CLI) and
|
|
1683
|
+
// verifier (kdna-core) will cause the signature check to fail.
|
|
1684
|
+
//
|
|
1685
|
+
// Bug: prior version did not strip `_source` (only set by eval/loader
|
|
1686
|
+
// helpers) and did not recursively strip `authoring.content_digest`,
|
|
1687
|
+
// so a payload with either field would diverge from kdna-core's
|
|
1688
|
+
// canonicalisation and report a false-positive verification failure.
|
|
1689
|
+
const copy = { ...(manifest || {}) };
|
|
1549
1690
|
delete copy.signature;
|
|
1550
1691
|
delete copy.asset_digest;
|
|
1551
1692
|
delete copy.container_sha256;
|
|
1552
1693
|
delete copy.content_digest;
|
|
1694
|
+
delete copy._source;
|
|
1695
|
+
if (copy.authoring && typeof copy.authoring === 'object') {
|
|
1696
|
+
const auth = { ...copy.authoring };
|
|
1697
|
+
delete auth.content_digest;
|
|
1698
|
+
copy.authoring = auth;
|
|
1699
|
+
}
|
|
1553
1700
|
return copy;
|
|
1554
1701
|
}
|
|
1555
1702
|
|
|
@@ -1579,7 +1726,7 @@ function publicKeyFingerprint(publicKeyPem) {
|
|
|
1579
1726
|
function applySignature(files, passphrase = null) {
|
|
1580
1727
|
const paths = identityPaths();
|
|
1581
1728
|
if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
|
|
1582
|
-
fail('Signing requires KDNA identity keys. Run: kdna identity init', EXIT.TRUST_FAILED);
|
|
1729
|
+
fail('Signing requires KDNA identity keys. Run: kdna-studio identity init', EXIT.TRUST_FAILED);
|
|
1583
1730
|
}
|
|
1584
1731
|
const manifest = JSON.parse(files['kdna.json']);
|
|
1585
1732
|
const publicKeyPem = fs.readFileSync(paths.publicKey, 'utf8');
|
|
@@ -1617,9 +1764,26 @@ function cmdExport(args) {
|
|
|
1617
1764
|
// Use args.includes() for --password-stdin since it is a boolean flag
|
|
1618
1765
|
// (option() rejects it for "missing value").
|
|
1619
1766
|
const useStdin = args.includes('--password-stdin');
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1767
|
+
let password;
|
|
1768
|
+
if (useStdin) {
|
|
1769
|
+
// fs.readFileSync(0) on a TTY waits forever for input and never
|
|
1770
|
+
// returns — the export appears to hang. Refuse up front with a
|
|
1771
|
+
// clear pointer to the right invocation.
|
|
1772
|
+
if (process.stdin.isTTY) {
|
|
1773
|
+
fail(
|
|
1774
|
+
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
1775
|
+
'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --format v1 --out <file.kdna> --password-stdin\n' +
|
|
1776
|
+
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
password = fs.readFileSync(0, 'utf8').trim();
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
fail(`Could not read password from stdin: ${e.message}`);
|
|
1783
|
+
}
|
|
1784
|
+
} else {
|
|
1785
|
+
password = option(args, '--password') || option(args, '--passphrase');
|
|
1786
|
+
}
|
|
1623
1787
|
exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
1624
1788
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
1625
1789
|
password,
|
|
@@ -1654,7 +1818,7 @@ function cmdExport(args) {
|
|
|
1654
1818
|
provenance.content_fingerprint = finalDigest;
|
|
1655
1819
|
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
1656
1820
|
|
|
1657
|
-
if (args.includes('--sign')) applySignature(files,
|
|
1821
|
+
if (args.includes('--sign')) applySignature(files, resolvePassphrase(args));
|
|
1658
1822
|
|
|
1659
1823
|
const entries = [['mimetype', files.mimetype]];
|
|
1660
1824
|
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
@@ -1744,7 +1908,7 @@ function cmdIdentity(args) {
|
|
|
1744
1908
|
const sub = args[0];
|
|
1745
1909
|
if (sub === 'init') {
|
|
1746
1910
|
const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
|
|
1747
|
-
const passphrase =
|
|
1911
|
+
const passphrase = resolvePassphrase(args);
|
|
1748
1912
|
try {
|
|
1749
1913
|
const identity = creatorApi.initIdentity(name, null, passphrase);
|
|
1750
1914
|
console.log(`Creator identity initialized:`);
|
|
@@ -1837,7 +2001,11 @@ function cmdSourceClassify(args) {
|
|
|
1837
2001
|
const target = project.distillation_target;
|
|
1838
2002
|
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
1839
2003
|
|
|
1840
|
-
|
|
2004
|
+
// Canonical evidence field is `evidence_materials` (set by cmdImport via
|
|
2005
|
+
// evidenceApi.addEvidence). The legacy `project.evidence` field never
|
|
2006
|
+
// receives writes, so reading it always produced an empty list and the
|
|
2007
|
+
// classifier reported "no evidence" even after `kdna-studio import`.
|
|
2008
|
+
const evidence = project.evidence_materials || project.evidence || [];
|
|
1841
2009
|
const results = evidence.map(e => {
|
|
1842
2010
|
const text = (e.content || e.title || '').toLowerCase();
|
|
1843
2011
|
const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
|
|
@@ -1965,14 +2133,29 @@ function cmdCandidate(args) {
|
|
|
1965
2133
|
if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
|
|
1966
2134
|
|
|
1967
2135
|
for (const c of accepted) {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
2136
|
+
// Promote candidates to draft cards. For axioms we must populate every
|
|
2137
|
+
// field the Human Lock gate and migrate path require — empty strings
|
|
2138
|
+
// cause migrate to fail ("critical fields missing from axioms"). When
|
|
2139
|
+
// a candidate lacks the field, fall back to the candidate's own
|
|
2140
|
+
// one_sentence/full_statement so the lock gate at least has substance
|
|
2141
|
+
// to verify. Authors are expected to enrich these fields before
|
|
2142
|
+
// locking.
|
|
2143
|
+
const fields = {};
|
|
2144
|
+
if (c.suggested_card_type === 'axiom') {
|
|
2145
|
+
fields.one_sentence = c.one_sentence || '';
|
|
2146
|
+
fields.full_statement = c.full_statement || c.one_sentence || '<TBD: full_statement>';
|
|
2147
|
+
fields.why = c.why || '<TBD: why>';
|
|
2148
|
+
// applies_when / does_not_apply_when are arrays, not strings.
|
|
2149
|
+
// Empty string is invalid; emit an empty array so checkRequiredFields
|
|
2150
|
+
// (which treats `length === 0` as missing) correctly reports them.
|
|
2151
|
+
fields.applies_when = Array.isArray(c.applies_when) ? c.applies_when : [];
|
|
2152
|
+
fields.does_not_apply_when = Array.isArray(c.does_not_apply_when) ? c.does_not_apply_when : [];
|
|
2153
|
+
fields.failure_risk = c.failure_risk || '<TBD: failure_risk>';
|
|
2154
|
+
} else {
|
|
2155
|
+
fields.one_sentence = c.one_sentence || '';
|
|
2156
|
+
fields.full_statement = c.full_statement || c.one_sentence || '';
|
|
2157
|
+
}
|
|
2158
|
+
const card = cardApi.createCard(c.suggested_card_type, fields);
|
|
1976
2159
|
card.evidence_refs = c.supporting_evidence_ids;
|
|
1977
2160
|
project.cards = project.cards || [];
|
|
1978
2161
|
project.cards.push(card);
|
|
@@ -2012,7 +2195,12 @@ async function cmdFeynman(args) {
|
|
|
2012
2195
|
if (cardIdx < 0) fail(`Card not found: ${cardId}`);
|
|
2013
2196
|
const card = project.cards[cardIdx];
|
|
2014
2197
|
|
|
2015
|
-
|
|
2198
|
+
// Canonical field name in the card schema is `feynman_restatement`, not
|
|
2199
|
+
// `feynman_text`. The old check was dead code (nothing writes the wrong
|
|
2200
|
+
// field) and made this command fail on every card.
|
|
2201
|
+
if (!card.feynman_restatement) {
|
|
2202
|
+
fail(`Card ${cardId} has no Feynman restatement. Generate one with the feynman authoring command, or set it via card update --field feynman_restatement='<json>'.`);
|
|
2203
|
+
}
|
|
2016
2204
|
|
|
2017
2205
|
console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2018
2206
|
const result = await ai.feynman.evaluate(llm.config(), card, {});
|
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.3",
|
|
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.2",
|
|
43
43
|
"ajv": "^8.20.0",
|
|
44
44
|
"ajv-formats": "^3.0.1",
|
|
45
45
|
"cbor-x": "^1.6.4"
|