@aikdna/kdna-studio-cli 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/bin/kdna-studio.js +145 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,47 @@ kdna load dist/my_domain.kdna --profile=compact --as=prompt
|
|
|
72
72
|
runtime control plane for inspecting, validating, packing, unpacking, and
|
|
73
73
|
loading existing `.kdna` assets.
|
|
74
74
|
|
|
75
|
+
## 5-minute first asset
|
|
76
|
+
|
|
77
|
+
If you just installed and want to ship a v1 asset without an LLM:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# 1. Init identity (required for the export signature)
|
|
81
|
+
kdna-studio identity init --name "Your Name"
|
|
82
|
+
|
|
83
|
+
# 2. Create a project
|
|
84
|
+
kdna-studio create my_domain --name @yourscope/my_domain
|
|
85
|
+
|
|
86
|
+
# 3. Add at least one judgment card
|
|
87
|
+
kdna-studio card add . axiom \
|
|
88
|
+
--field one_sentence='specific evidence outranks broad claims' \
|
|
89
|
+
--field full_statement='Always cite the specific source that supports a judgment; broad claims without evidence are the most common cause of bad agent advice.' \
|
|
90
|
+
--field why='because vague advice fails in production' \
|
|
91
|
+
--field applies_when='["reviewing"]' \
|
|
92
|
+
--field does_not_apply_when='["formatting"]' \
|
|
93
|
+
--field failure_risk='praise without diagnosis' \
|
|
94
|
+
--field confidence='high' \
|
|
95
|
+
--field evidence_type='observation'
|
|
96
|
+
|
|
97
|
+
# 4. Approve and lock
|
|
98
|
+
kdna-studio card approve . --all --by me --statement "i confirm"
|
|
99
|
+
|
|
100
|
+
# 5. Export the v1 asset
|
|
101
|
+
kdna-studio export . --format v1 --out my_domain.kdna
|
|
102
|
+
|
|
103
|
+
# 6. Verify with the runtime CLI
|
|
104
|
+
kdna load my_domain.kdna --profile=compact
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The first three AI commands (feynman, distill --ai, test, interview) work
|
|
108
|
+
without `KDNA_LLM_PROVIDER` / `KDNA_LLM_API_KEY` by passing `--no-llm`. They
|
|
109
|
+
return a structured but unsynthesised result. To enable real evaluation:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
kdna-studio llm config --provider openai --model gpt-4 --key <your-key>
|
|
113
|
+
# or set KDNA_LLM_PROVIDER, KDNA_LLM_API_KEY, KDNA_LLM_MODEL
|
|
114
|
+
```
|
|
115
|
+
|
|
75
116
|
## Runtime Export Contract
|
|
76
117
|
|
|
77
118
|
`kdna-studio export --format v1` is the canonical runtime export path. It uses
|
package/bin/kdna-studio.js
CHANGED
|
@@ -29,6 +29,35 @@ const {
|
|
|
29
29
|
} = loadStudioCore();
|
|
30
30
|
|
|
31
31
|
const llm = require('../src/llm');
|
|
32
|
+
|
|
33
|
+
// Bug (#1 UX): the LLM-requiring commands (distill, interview, feynman,
|
|
34
|
+
// test) silently fail with a stack trace when no LLM is configured.
|
|
35
|
+
// First-time users hit this and have no way to know what to do. The
|
|
36
|
+
// fix routes every LLM call through this helper, which:
|
|
37
|
+
// 1. Checks whether an LLM is configured at all
|
|
38
|
+
// 2. If not and --no-llm is given, returns a static / synthesised
|
|
39
|
+
// result so the command can still be useful (5-minute path)
|
|
40
|
+
// 3. If not and --no-llm is absent, exits with a clear
|
|
41
|
+
// "configure LLM with: kdna-studio llm config" message
|
|
42
|
+
function cmdNeedsLlm(args, cmdName) {
|
|
43
|
+
const cfg = llm.config();
|
|
44
|
+
const hasLlm = cfg && cfg.provider && cfg.apiKey && cfg.model;
|
|
45
|
+
if (hasLlm) return { hasLlm: true, cfg };
|
|
46
|
+
if (args.includes('--no-llm')) {
|
|
47
|
+
return {
|
|
48
|
+
hasLlm: false,
|
|
49
|
+
cfg,
|
|
50
|
+
noLlm: true,
|
|
51
|
+
message: `${cmdName} invoked with --no-llm. Output will be static / synthesised; configure an LLM with \`kdna-studio llm config\` to enable real evaluation.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
fail(
|
|
55
|
+
`${cmdName} requires an LLM. Configure one with:\n` +
|
|
56
|
+
` kdna-studio llm config --provider openai --key <api-key> --model <model>\n` +
|
|
57
|
+
`Or set env vars: KDNA_LLM_PROVIDER, KDNA_LLM_API_KEY, KDNA_LLM_MODEL.\n` +
|
|
58
|
+
`Or run with --no-llm to get a static / synthesised result.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
32
61
|
const ai = require('../src/ai');
|
|
33
62
|
|
|
34
63
|
const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
@@ -36,9 +65,12 @@ const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
|
36
65
|
function usage() {
|
|
37
66
|
console.log(`kdna-studio — Studio-compatible KDNA authoring CLI
|
|
38
67
|
|
|
39
|
-
LLM (AI-powered authoring):
|
|
68
|
+
LLM (AI-powered authoring; every AI command accepts --no-llm for a static result):
|
|
40
69
|
kdna-studio llm config [--provider <name>] [--model <name>] [--key-pipe] [--url <base-url>]
|
|
41
70
|
kdna-studio llm show
|
|
71
|
+
(run 'kdna-studio llm config --provider openai --key <key> --model gpt-4' to enable
|
|
72
|
+
feynman, distill --ai, interview, and test. With --no-llm these commands
|
|
73
|
+
still produce a structured but unsynthesised result.)
|
|
42
74
|
|
|
43
75
|
Identity:
|
|
44
76
|
kdna-studio identity init [--name <display-name>]
|
|
@@ -311,11 +343,40 @@ function importedCard(type, fields = {}, id = null) {
|
|
|
311
343
|
}
|
|
312
344
|
}
|
|
313
345
|
|
|
346
|
+
// Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
|
|
347
|
+
// misunderstanding card in two places — `patterns[]` (the v1 schema's
|
|
348
|
+
// primary card list) and `reasoning.failure_modes[]` (the v1 schema's
|
|
349
|
+
// "what bad judgment looks like" view). Both share the same `id`.
|
|
350
|
+
// Prior version imported both, doubling every misunderstanding on
|
|
351
|
+
// re-import. The dedup is by id: when a card with the same id is
|
|
352
|
+
// pushed twice, the second push merges fields (the failure_modes
|
|
353
|
+
// entry carries a different field shape than patterns; merging
|
|
354
|
+
// keeps the richer failure_risk / applies_when / does_not_apply_when
|
|
355
|
+
// from the patterns entry).
|
|
356
|
+
const _importedIds = new Set();
|
|
314
357
|
function pushImportedCard(cards, type, fields = {}, id = null) {
|
|
358
|
+
if (id && _importedIds.has(id)) {
|
|
359
|
+
// Already imported — merge richer fields into the existing card.
|
|
360
|
+
const existing = cards.find(c => c.id === id);
|
|
361
|
+
if (existing) {
|
|
362
|
+
existing.fields = existing.fields || {};
|
|
363
|
+
for (const [k, v] of Object.entries(fields || {})) {
|
|
364
|
+
if (v !== undefined && v !== null && v !== '' &&
|
|
365
|
+
(existing.fields[k] === undefined || existing.fields[k] === '' ||
|
|
366
|
+
(Array.isArray(existing.fields[k]) && existing.fields[k].length === 0))) {
|
|
367
|
+
existing.fields[k] = v;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (id) _importedIds.add(id);
|
|
315
374
|
const card = importedCard(type, fields, id);
|
|
316
375
|
if (card) cards.push(card);
|
|
317
376
|
}
|
|
318
377
|
|
|
378
|
+
function resetImportedIds() { _importedIds.clear(); }
|
|
379
|
+
|
|
319
380
|
function decodePayload(bytes, manifest = {}) {
|
|
320
381
|
const raw = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
321
382
|
const encoding = manifest.payload?.encoding || manifest.container?.payload_encoding || null;
|
|
@@ -334,6 +395,7 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
334
395
|
// intentional. Documented here so a future reader does not "fix" it
|
|
335
396
|
// by collapsing one path into the other.
|
|
336
397
|
function cardsFromV1Payload(payload) {
|
|
398
|
+
resetImportedIds();
|
|
337
399
|
const cards = [];
|
|
338
400
|
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
339
401
|
pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
|
|
@@ -445,6 +507,12 @@ function cardsFromV1Payload(payload) {
|
|
|
445
507
|
}, failureMode.id || null);
|
|
446
508
|
}
|
|
447
509
|
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
510
|
+
// Bug (#4 UX): compile/index.js synthesises one reasoning chain
|
|
511
|
+
// per locked axiom (the legacy 1.0.0 path) and tags it
|
|
512
|
+
// `source_authored: false`. Importing those synthesised chains
|
|
513
|
+
// doubled the card count on round-trip. The fix skips chains
|
|
514
|
+
// that the producer explicitly marked as synthesised.
|
|
515
|
+
if (chain.source_authored === false) continue;
|
|
448
516
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
449
517
|
}
|
|
450
518
|
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
@@ -461,6 +529,7 @@ function cardsFromV1Payload(payload) {
|
|
|
461
529
|
}
|
|
462
530
|
|
|
463
531
|
function cardsFromLegacyPayload(payload) {
|
|
532
|
+
resetImportedIds();
|
|
464
533
|
const cards = [];
|
|
465
534
|
const judgment = payload.judgment || {};
|
|
466
535
|
const core = judgment.core || {};
|
|
@@ -710,6 +779,7 @@ function cmdCreate(args) {
|
|
|
710
779
|
* Cards are imported as draft — they do NOT inherit trust from the source.
|
|
711
780
|
*/
|
|
712
781
|
function importFromKdna(kdnaPath) {
|
|
782
|
+
resetImportedIds();
|
|
713
783
|
const absKdna = path.resolve(kdnaPath);
|
|
714
784
|
if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
|
|
715
785
|
if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
|
|
@@ -872,6 +942,7 @@ function importFromKdna(kdnaPath) {
|
|
|
872
942
|
* Outputs a schema audit report.
|
|
873
943
|
*/
|
|
874
944
|
function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
945
|
+
resetImportedIds();
|
|
875
946
|
const absSource = path.resolve(sourceDir);
|
|
876
947
|
if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
|
|
877
948
|
if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
|
|
@@ -1353,8 +1424,25 @@ function cmdCard(args) {
|
|
|
1353
1424
|
const sub = args[0];
|
|
1354
1425
|
if (sub === 'list') {
|
|
1355
1426
|
const { project } = readProject(args[1]);
|
|
1356
|
-
|
|
1357
|
-
|
|
1427
|
+
// Bug (#2 UX): the --json flag was accepted but ignored — the
|
|
1428
|
+
// command printed the same TSV either way. A script consumer
|
|
1429
|
+
// that wanted to pipe the result into `jq` or a downstream
|
|
1430
|
+
// tool got garbage. The fix respects --json and emits a real
|
|
1431
|
+
// JSON document.
|
|
1432
|
+
const useJson = args.includes('--json');
|
|
1433
|
+
const rows = (project.cards || []).map(card => ({
|
|
1434
|
+
id: card.id,
|
|
1435
|
+
type: card.type,
|
|
1436
|
+
status: card.status,
|
|
1437
|
+
locked: !!card.locked,
|
|
1438
|
+
fields: card.fields || {},
|
|
1439
|
+
}));
|
|
1440
|
+
if (useJson) {
|
|
1441
|
+
console.log(JSON.stringify({ count: rows.length, cards: rows }, null, 2));
|
|
1442
|
+
} else {
|
|
1443
|
+
for (const r of rows) {
|
|
1444
|
+
console.log(`${r.id}\t${r.type}\t${r.status}\t${r.locked ? 'locked' : 'unlocked'}`);
|
|
1445
|
+
}
|
|
1358
1446
|
}
|
|
1359
1447
|
return;
|
|
1360
1448
|
}
|
|
@@ -2254,9 +2342,15 @@ async function cmdDistill(args) {
|
|
|
2254
2342
|
const projectInput = args[0];
|
|
2255
2343
|
const candidatesFile = option(args, '--candidates');
|
|
2256
2344
|
const useAI = args.includes('--ai');
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2345
|
+
// Bug (#1 UX): prior version called resolveApiKey(args)
|
|
2346
|
+
// unconditionally, which is fine when --candidates is set (the
|
|
2347
|
+
// call is a no-op) but it also surfaced the env-var resolution
|
|
2348
|
+
// on every run, so a caller that never asked for the LLM was
|
|
2349
|
+
// still at the mercy of `resolveApiKey` warnings. Defer the
|
|
2350
|
+
// LLM-key resolution to the --ai branch.
|
|
2351
|
+
const provider = useAI ? option(args, '--provider') : null;
|
|
2352
|
+
const model = useAI ? option(args, '--model') : null;
|
|
2353
|
+
const apiKey = useAI ? resolveApiKey(args) : null;
|
|
2260
2354
|
if (!projectInput) fail('Usage: kdna-studio distill <project> [--ai] [--candidates <file.json>]');
|
|
2261
2355
|
if (!candidatesFile && !useAI) fail('Either --ai or --candidates <file.json> required.');
|
|
2262
2356
|
|
|
@@ -2446,11 +2540,35 @@ async function cmdFeynman(args) {
|
|
|
2446
2540
|
}
|
|
2447
2541
|
|
|
2448
2542
|
console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2449
|
-
|
|
2543
|
+
|
|
2544
|
+
// Bug (#1 UX follow-up): if no LLM is configured and the caller
|
|
2545
|
+
// did not pass --no-llm, route through cmdNeedsLlm which either
|
|
2546
|
+
// gives a clear "configure LLM" error or accepts the --no-llm
|
|
2547
|
+
// path. Without this branch, the no-LLM caller would fall
|
|
2548
|
+
// through to the LLM call below and get a stack trace.
|
|
2549
|
+
const llmGate = cmdNeedsLlm(args, 'feynmann');
|
|
2550
|
+
|
|
2551
|
+
let result;
|
|
2552
|
+
if (llmGate.noLlm) {
|
|
2553
|
+
// Static evaluation: every criterion is "unsure" (✗), score 0,
|
|
2554
|
+
// but the command still produces a useful structured result and
|
|
2555
|
+
// saves the synthesised restatement to the card.
|
|
2556
|
+
result = {
|
|
2557
|
+
score: 0,
|
|
2558
|
+
criteria: Object.fromEntries(Object.keys(ai.feynman.CRITERIA).map(k => [k, false])),
|
|
2559
|
+
explanations: { noLlm: llmGate.message },
|
|
2560
|
+
suggestions: [
|
|
2561
|
+
'No LLM configured — every criterion shows ✗. Run `kdna-studio llm config` and re-evaluate for a real score.',
|
|
2562
|
+
'Or set the synthesised restatement via: kdna-studio card update <project> ' + cardId + ' --field feynman_restatement=\'{"text":"<your own restatement>"}\'',
|
|
2563
|
+
],
|
|
2564
|
+
};
|
|
2565
|
+
} else {
|
|
2566
|
+
result = await ai.feynman.evaluate(llmGate.cfg, card, {});
|
|
2567
|
+
}
|
|
2450
2568
|
const score = result.score || 0;
|
|
2451
2569
|
const passed = score >= 4;
|
|
2452
2570
|
|
|
2453
|
-
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}`);
|
|
2571
|
+
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
|
|
2454
2572
|
for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
|
|
2455
2573
|
const r = (result.criteria || {})[criterion] ? '✓' : '✗';
|
|
2456
2574
|
console.log(` ${r} ${criterion}`);
|
|
@@ -2496,18 +2614,35 @@ async function cmdTest(args) {
|
|
|
2496
2614
|
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2497
2615
|
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2498
2616
|
|
|
2617
|
+
// Bug (#1 UX): gate LLM-requiring paths through cmdNeedsLlm.
|
|
2618
|
+
// The test command always needs an LLM (it compares without/with
|
|
2619
|
+
// the loaded domain). --no-llm gives a static "no-op" comparison
|
|
2620
|
+
// that lists the cards that *would* be applied.
|
|
2621
|
+
const llmGate = cmdNeedsLlm(args, 'test');
|
|
2622
|
+
const cfg = llmGate.noLlm ? null : llmGate.cfg;
|
|
2623
|
+
|
|
2499
2624
|
if (canonical === 'baseline') {
|
|
2500
2625
|
// Baseline is a single comparison: keep the prior single-result
|
|
2501
2626
|
// shape so existing parsers / consumers see the same output.
|
|
2502
2627
|
const prompt = `Test the core judgment: ${input}`;
|
|
2503
|
-
|
|
2628
|
+
if (cfg === null) {
|
|
2629
|
+
// No-LLM static result
|
|
2630
|
+
const cards = (project.cards || []).filter(c => c.locked).map(c => ({ type: c.type, id: c.id, one_sentence: c.fields?.one_sentence || c.fields?.question || '' }));
|
|
2631
|
+
console.log(JSON.stringify({ no_llm: true, message: llmGate.message, would_apply: cards }, null, 2));
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const result = await ai.testlab.compare(cfg, domainName, prompt, domainPrompt, {});
|
|
2504
2635
|
console.log(JSON.stringify(result, null, 2));
|
|
2505
2636
|
return;
|
|
2506
2637
|
}
|
|
2507
2638
|
// For non-baseline presets, run the preset + the baseline so the user
|
|
2508
2639
|
// can see both side by side. The preset prompt is the one in
|
|
2509
2640
|
// ai/testlab.js — that's the code path that was dead before.
|
|
2510
|
-
|
|
2641
|
+
if (cfg === null) {
|
|
2642
|
+
console.log(JSON.stringify({ no_llm: true, message: llmGate.message }, null, 2));
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const result = await ai.testlab.testPreset(cfg, domainName, input, domainPrompt, {});
|
|
2511
2646
|
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2512
2647
|
}
|
|
2513
2648
|
|
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.8",
|
|
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": {
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"cbor-x": "^1.6.4"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@aikdna/kdna-core": "^0.15.
|
|
48
|
+
"@aikdna/kdna-core": "^0.15.5"
|
|
49
49
|
}
|
|
50
50
|
}
|