@aikdna/kdna-studio-cli 0.8.7 → 0.8.9
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 +367 -16
- 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>]
|
|
@@ -51,6 +83,8 @@ Create (three entry paths):
|
|
|
51
83
|
|
|
52
84
|
Migrate (dev source or Studio project → canonical .kdna in one command):
|
|
53
85
|
kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--passphrase <pw>|--passphrase-stdin]
|
|
86
|
+
kdna-studio migrate <source-dir> --check --name <@scope/name> # pre-flight: report which fields would block the export without writing the .kdna
|
|
87
|
+
kdna-studio audit-locks <project> [--type axiom|risk|stance|...] [--json] # list cards with missing Human Lock fields (per-card-type, per-field)
|
|
54
88
|
|
|
55
89
|
Authoring:
|
|
56
90
|
kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
|
|
@@ -82,6 +116,7 @@ Distillation:
|
|
|
82
116
|
kdna-studio candidate override <project> <candidate-id> # override scope gate
|
|
83
117
|
kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
|
|
84
118
|
kdna-studio report <project>
|
|
119
|
+
kdna-studio audit-locks <project> # list all axioms/risk/stance missing Human Lock fields
|
|
85
120
|
|
|
86
121
|
Project may be a directory containing studio.project.json or a project JSON file.`);
|
|
87
122
|
}
|
|
@@ -311,11 +346,40 @@ function importedCard(type, fields = {}, id = null) {
|
|
|
311
346
|
}
|
|
312
347
|
}
|
|
313
348
|
|
|
349
|
+
// Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
|
|
350
|
+
// misunderstanding card in two places — `patterns[]` (the v1 schema's
|
|
351
|
+
// primary card list) and `reasoning.failure_modes[]` (the v1 schema's
|
|
352
|
+
// "what bad judgment looks like" view). Both share the same `id`.
|
|
353
|
+
// Prior version imported both, doubling every misunderstanding on
|
|
354
|
+
// re-import. The dedup is by id: when a card with the same id is
|
|
355
|
+
// pushed twice, the second push merges fields (the failure_modes
|
|
356
|
+
// entry carries a different field shape than patterns; merging
|
|
357
|
+
// keeps the richer failure_risk / applies_when / does_not_apply_when
|
|
358
|
+
// from the patterns entry).
|
|
359
|
+
const _importedIds = new Set();
|
|
314
360
|
function pushImportedCard(cards, type, fields = {}, id = null) {
|
|
361
|
+
if (id && _importedIds.has(id)) {
|
|
362
|
+
// Already imported — merge richer fields into the existing card.
|
|
363
|
+
const existing = cards.find(c => c.id === id);
|
|
364
|
+
if (existing) {
|
|
365
|
+
existing.fields = existing.fields || {};
|
|
366
|
+
for (const [k, v] of Object.entries(fields || {})) {
|
|
367
|
+
if (v !== undefined && v !== null && v !== '' &&
|
|
368
|
+
(existing.fields[k] === undefined || existing.fields[k] === '' ||
|
|
369
|
+
(Array.isArray(existing.fields[k]) && existing.fields[k].length === 0))) {
|
|
370
|
+
existing.fields[k] = v;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (id) _importedIds.add(id);
|
|
315
377
|
const card = importedCard(type, fields, id);
|
|
316
378
|
if (card) cards.push(card);
|
|
317
379
|
}
|
|
318
380
|
|
|
381
|
+
function resetImportedIds() { _importedIds.clear(); }
|
|
382
|
+
|
|
319
383
|
function decodePayload(bytes, manifest = {}) {
|
|
320
384
|
const raw = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
321
385
|
const encoding = manifest.payload?.encoding || manifest.container?.payload_encoding || null;
|
|
@@ -334,6 +398,7 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
334
398
|
// intentional. Documented here so a future reader does not "fix" it
|
|
335
399
|
// by collapsing one path into the other.
|
|
336
400
|
function cardsFromV1Payload(payload) {
|
|
401
|
+
resetImportedIds();
|
|
337
402
|
const cards = [];
|
|
338
403
|
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
339
404
|
pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
|
|
@@ -445,6 +510,12 @@ function cardsFromV1Payload(payload) {
|
|
|
445
510
|
}, failureMode.id || null);
|
|
446
511
|
}
|
|
447
512
|
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
513
|
+
// Bug (#4 UX): compile/index.js synthesises one reasoning chain
|
|
514
|
+
// per locked axiom (the legacy 1.0.0 path) and tags it
|
|
515
|
+
// `source_authored: false`. Importing those synthesised chains
|
|
516
|
+
// doubled the card count on round-trip. The fix skips chains
|
|
517
|
+
// that the producer explicitly marked as synthesised.
|
|
518
|
+
if (chain.source_authored === false) continue;
|
|
448
519
|
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
449
520
|
}
|
|
450
521
|
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
@@ -461,6 +532,7 @@ function cardsFromV1Payload(payload) {
|
|
|
461
532
|
}
|
|
462
533
|
|
|
463
534
|
function cardsFromLegacyPayload(payload) {
|
|
535
|
+
resetImportedIds();
|
|
464
536
|
const cards = [];
|
|
465
537
|
const judgment = payload.judgment || {};
|
|
466
538
|
const core = judgment.core || {};
|
|
@@ -710,6 +782,7 @@ function cmdCreate(args) {
|
|
|
710
782
|
* Cards are imported as draft — they do NOT inherit trust from the source.
|
|
711
783
|
*/
|
|
712
784
|
function importFromKdna(kdnaPath) {
|
|
785
|
+
resetImportedIds();
|
|
713
786
|
const absKdna = path.resolve(kdnaPath);
|
|
714
787
|
if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
|
|
715
788
|
if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
|
|
@@ -872,6 +945,7 @@ function importFromKdna(kdnaPath) {
|
|
|
872
945
|
* Outputs a schema audit report.
|
|
873
946
|
*/
|
|
874
947
|
function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
948
|
+
resetImportedIds();
|
|
875
949
|
const absSource = path.resolve(sourceDir);
|
|
876
950
|
if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
|
|
877
951
|
if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
|
|
@@ -1353,8 +1427,25 @@ function cmdCard(args) {
|
|
|
1353
1427
|
const sub = args[0];
|
|
1354
1428
|
if (sub === 'list') {
|
|
1355
1429
|
const { project } = readProject(args[1]);
|
|
1356
|
-
|
|
1357
|
-
|
|
1430
|
+
// Bug (#2 UX): the --json flag was accepted but ignored — the
|
|
1431
|
+
// command printed the same TSV either way. A script consumer
|
|
1432
|
+
// that wanted to pipe the result into `jq` or a downstream
|
|
1433
|
+
// tool got garbage. The fix respects --json and emits a real
|
|
1434
|
+
// JSON document.
|
|
1435
|
+
const useJson = args.includes('--json');
|
|
1436
|
+
const rows = (project.cards || []).map(card => ({
|
|
1437
|
+
id: card.id,
|
|
1438
|
+
type: card.type,
|
|
1439
|
+
status: card.status,
|
|
1440
|
+
locked: !!card.locked,
|
|
1441
|
+
fields: card.fields || {},
|
|
1442
|
+
}));
|
|
1443
|
+
if (useJson) {
|
|
1444
|
+
console.log(JSON.stringify({ count: rows.length, cards: rows }, null, 2));
|
|
1445
|
+
} else {
|
|
1446
|
+
for (const r of rows) {
|
|
1447
|
+
console.log(`${r.id}\t${r.type}\t${r.status}\t${r.locked ? 'locked' : 'unlocked'}`);
|
|
1448
|
+
}
|
|
1358
1449
|
}
|
|
1359
1450
|
return;
|
|
1360
1451
|
}
|
|
@@ -1405,15 +1496,44 @@ function cmdCard(args) {
|
|
|
1405
1496
|
if (sub === 'update') {
|
|
1406
1497
|
const projectInput = args[1];
|
|
1407
1498
|
const cardId = args[2];
|
|
1408
|
-
if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value');
|
|
1499
|
+
if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value OR kdna-studio card update <project> <card-id> --from-file <path.json>');
|
|
1409
1500
|
const { projectPath, project } = readProject(projectInput);
|
|
1410
1501
|
const idx = (project.cards || []).findIndex((c) => c.id === cardId);
|
|
1411
1502
|
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
1412
1503
|
const card = project.cards[idx];
|
|
1413
1504
|
if (card.locked) fail(`Cannot update locked card: ${cardId}. Unlock first with card unlock.`);
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1505
|
+
// Bug (UX pro-20 migration): prior version only accepted
|
|
1506
|
+
// --field key=value, so an author migrating 20 source-tree
|
|
1507
|
+
// assets had to run one CLI invocation per field. With
|
|
1508
|
+
// confidence + evidence_type as 8th / 9th required fields on
|
|
1509
|
+
// axiom, a 99-axiom asset became 99 * 2 = 198 invocations.
|
|
1510
|
+
// The fix accepts --from-file <path.json> so an author can
|
|
1511
|
+
// write a single JSON file per card (or per project) and
|
|
1512
|
+
// apply it in one call. JSON shape: {field: value, ...}.
|
|
1513
|
+
// kdna-studio card update . ax_xxx --from-file ./fills/ax_xxx.json
|
|
1514
|
+
// For batched updates across many cards, see the
|
|
1515
|
+
// --from-file-projects variant below, which reads a list
|
|
1516
|
+
// of {id, fields} records.
|
|
1517
|
+
const fromFileIdx = args.indexOf('--from-file');
|
|
1518
|
+
if (fromFileIdx >= 0) {
|
|
1519
|
+
const filePath = args[fromFileIdx + 1];
|
|
1520
|
+
if (!filePath) fail('--from-file requires a path');
|
|
1521
|
+
if (!fs.existsSync(filePath)) fail(`--from-file: file not found: ${filePath}`);
|
|
1522
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1523
|
+
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
|
1524
|
+
fail(`--from-file: expected a JSON object mapping field names to values`);
|
|
1525
|
+
}
|
|
1526
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1527
|
+
if (k === 'id' || k === 'type' || k === 'status' || k === 'locked') {
|
|
1528
|
+
fail(`--from-file: cannot edit reserved field "${k}"`);
|
|
1529
|
+
}
|
|
1530
|
+
if (card.fields) card.fields[k] = v;
|
|
1531
|
+
}
|
|
1532
|
+
} else {
|
|
1533
|
+
const updates = parseFields(args.slice(3));
|
|
1534
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1535
|
+
if (card.fields) card.fields[k] = v;
|
|
1536
|
+
}
|
|
1417
1537
|
}
|
|
1418
1538
|
project.cards[idx] = card;
|
|
1419
1539
|
writeProject(projectPath, project);
|
|
@@ -1562,14 +1682,32 @@ function cmdLock(args) {
|
|
|
1562
1682
|
}
|
|
1563
1683
|
|
|
1564
1684
|
function cmdMigrate(args) {
|
|
1685
|
+
// Bug (UX pro-20 migration): prior version of `migrate` always
|
|
1686
|
+
// attempted the full pipeline — read source folder, import to
|
|
1687
|
+
// Studio project, lock all cards, then export. An author migrating
|
|
1688
|
+
// 20 source-tree assets had no way to know in advance whether
|
|
1689
|
+
// the assets would pass the v1.7.2+ Human Lock gate. The fix
|
|
1690
|
+
// adds `--check` which runs the import + the critical-missing
|
|
1691
|
+
// check + the lock-gate check, then prints a report and exits
|
|
1692
|
+
// without writing the .kdna file. Pair with `audit-locks` for
|
|
1693
|
+
// the full pre-migration report.
|
|
1694
|
+
//
|
|
1695
|
+
// Usage:
|
|
1696
|
+
// kdna-studio migrate <source-dir> --check --name <@scope/name>
|
|
1697
|
+
//
|
|
1698
|
+
// Exit codes:
|
|
1699
|
+
// 0 — would succeed
|
|
1700
|
+
// 2 — INPUT_ERROR (missing args)
|
|
1701
|
+
// 4 — would fail (Human Lock gate or critical axiom field)
|
|
1702
|
+
const checkOnly = args.includes('--check');
|
|
1565
1703
|
const sourceDir = args[0];
|
|
1566
1704
|
const out = option(args, '--out');
|
|
1567
1705
|
const by = option(args, '--by');
|
|
1568
1706
|
const statement = option(args, '--statement');
|
|
1569
1707
|
const requestedName = option(args, '--name');
|
|
1570
1708
|
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1571
|
-
if (!sourceDir || !out || !by || !statement) {
|
|
1572
|
-
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
|
|
1709
|
+
if (!sourceDir || (!out && !checkOnly) || (!by && !checkOnly) || (!statement && !checkOnly)) {
|
|
1710
|
+
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--check] [--sign] [--passphrase <pw>|--passphrase-stdin]');
|
|
1573
1711
|
}
|
|
1574
1712
|
|
|
1575
1713
|
const sourcePath = path.resolve(sourceDir);
|
|
@@ -1636,6 +1774,42 @@ function cmdMigrate(args) {
|
|
|
1636
1774
|
}
|
|
1637
1775
|
}
|
|
1638
1776
|
|
|
1777
|
+
// --check branch: print a pre-migration report and exit. The
|
|
1778
|
+
// project object already has every card from the source tree
|
|
1779
|
+
// imported, so we can run the same audit-locks logic here
|
|
1780
|
+
// without re-reading the source. This is the single-call
|
|
1781
|
+
// equivalent of `kdna-studio create --from-folder <src> &&
|
|
1782
|
+
// kdna-studio audit-locks .` — but it requires no
|
|
1783
|
+
// intermediate write.
|
|
1784
|
+
if (checkOnly) {
|
|
1785
|
+
const REQUIRED = {
|
|
1786
|
+
axiom: ['confidence', 'evidence_type', 'one_sentence', 'full_statement', 'why',
|
|
1787
|
+
'applies_when', 'does_not_apply_when', 'failure_risk'],
|
|
1788
|
+
risk: ['name', 'description', 'mitigation'],
|
|
1789
|
+
stance: ['statement', 'applies_when'],
|
|
1790
|
+
misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
|
|
1791
|
+
boundary: ['scope', 'out_of_scope'],
|
|
1792
|
+
};
|
|
1793
|
+
const summary = { project: name, source: sourceDir, total: project.cards.length, by_type: {}, would_block: false };
|
|
1794
|
+
for (const card of (project.cards || [])) {
|
|
1795
|
+
const req = REQUIRED[card.type];
|
|
1796
|
+
if (!req) continue;
|
|
1797
|
+
const missing = [];
|
|
1798
|
+
for (const f of req) {
|
|
1799
|
+
const v = card.fields?.[f];
|
|
1800
|
+
if (v === undefined || v === null || v === '' ||
|
|
1801
|
+
(Array.isArray(v) && v.length === 0)) missing.push(f);
|
|
1802
|
+
}
|
|
1803
|
+
if (missing.length > 0) {
|
|
1804
|
+
if (!summary.by_type[card.type]) summary.by_type[card.type] = [];
|
|
1805
|
+
summary.by_type[card.type].push({ id: card.id, missing });
|
|
1806
|
+
summary.would_block = true;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1810
|
+
process.exit(summary.would_block ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1639
1813
|
// Step 2: approve and Human Lock all cards
|
|
1640
1814
|
let locked = 0;
|
|
1641
1815
|
for (let i = 0; i < (project.cards || []).length; i++) {
|
|
@@ -2162,6 +2336,135 @@ function cmdReport(args) {
|
|
|
2162
2336
|
process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
2163
2337
|
}
|
|
2164
2338
|
|
|
2339
|
+
// Audit-locks: list every card that would fail the v1.7.2+ Human Lock
|
|
2340
|
+
// gate because of missing or empty fields. Used during the
|
|
2341
|
+
// pre-migration review of source-tree assets (e.g. pro-20) so an
|
|
2342
|
+
// author can fill in the missing confidence / evidence_type / risk
|
|
2343
|
+
// description / stance statement BEFORE running `migrate --format v1`.
|
|
2344
|
+
//
|
|
2345
|
+
// Bug (UX pro-20 migration): prior version of `kdna-studio report`
|
|
2346
|
+
// returned a flat human_lock_gate.issues list, which mixed card
|
|
2347
|
+
// types and field names. An author migrating 20 source-tree
|
|
2348
|
+
// assets with 99 axioms + 140 risk cards + 70 stance cards had no
|
|
2349
|
+
// way to see, for a single asset, "which axioms are missing
|
|
2350
|
+
// confidence?" — they had to grep the project JSON. The fix adds
|
|
2351
|
+
// a per-type, per-field breakdown.
|
|
2352
|
+
function cmdAuditLocks(args) {
|
|
2353
|
+
const { project } = readProject(args[0]);
|
|
2354
|
+
const useJson = args.includes('--json');
|
|
2355
|
+
const onlyType = option(args, '--type'); // 'axiom' | 'risk' | 'stance' | 'misunderstanding' | null
|
|
2356
|
+
|
|
2357
|
+
const cards = project.cards || [];
|
|
2358
|
+
const findings = {
|
|
2359
|
+
project: project.name,
|
|
2360
|
+
card_count: cards.length,
|
|
2361
|
+
locked_count: cards.filter(c => c.locked).length,
|
|
2362
|
+
by_type: {},
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
// Per-card-type required field lists. Mirrors the v1.7.2+ lockCard
|
|
2366
|
+
// gate (kdna-studio-core/src/cards/index.js) so the output is
|
|
2367
|
+
// exactly the field set the gate will reject.
|
|
2368
|
+
const REQUIRED = {
|
|
2369
|
+
axiom: ['one_sentence', 'full_statement', 'why', 'applies_when',
|
|
2370
|
+
'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type'],
|
|
2371
|
+
risk: ['name', 'description', 'mitigation'],
|
|
2372
|
+
stance: ['statement', 'applies_when'],
|
|
2373
|
+
misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
|
|
2374
|
+
boundary: ['scope', 'out_of_scope'],
|
|
2375
|
+
self_check: ['question'],
|
|
2376
|
+
};
|
|
2377
|
+
const THRESHOLDS = {
|
|
2378
|
+
axiom: {
|
|
2379
|
+
one_sentence: { min: 5, type: 'string' },
|
|
2380
|
+
full_statement: { min: 20, type: 'string' },
|
|
2381
|
+
why: { min: 20, type: 'string' },
|
|
2382
|
+
failure_risk: { min: 1, type: 'string' },
|
|
2383
|
+
confidence: { allowed: ['low', 'medium', 'high'] },
|
|
2384
|
+
evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle'] },
|
|
2385
|
+
},
|
|
2386
|
+
risk: {
|
|
2387
|
+
name: { min: 1, type: 'string' },
|
|
2388
|
+
description: { min: 20, type: 'string' },
|
|
2389
|
+
mitigation: { min: 1, type: 'string' },
|
|
2390
|
+
},
|
|
2391
|
+
stance: {
|
|
2392
|
+
statement: { min: 1, type: 'string' },
|
|
2393
|
+
applies_when: { type: 'array', minLength: 0 },
|
|
2394
|
+
},
|
|
2395
|
+
};
|
|
2396
|
+
|
|
2397
|
+
function checkField(type, fields, name, rule) {
|
|
2398
|
+
const value = fields[name];
|
|
2399
|
+
if (value === undefined || value === null || value === '') {
|
|
2400
|
+
return { field: name, issue: 'missing' };
|
|
2401
|
+
}
|
|
2402
|
+
if (rule.type === 'string' && rule.min && String(value).length < rule.min) {
|
|
2403
|
+
return { field: name, issue: 'too_short', have_length: String(value).length, need_min: rule.min };
|
|
2404
|
+
}
|
|
2405
|
+
if (rule.allowed && !rule.allowed.includes(value)) {
|
|
2406
|
+
return { field: name, issue: 'invalid_value', have: value, allowed: rule.allowed };
|
|
2407
|
+
}
|
|
2408
|
+
if (rule.type === 'array' && !Array.isArray(value)) {
|
|
2409
|
+
return { field: name, issue: 'wrong_type', have_type: typeof value, need_type: 'array' };
|
|
2410
|
+
}
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
for (const card of cards) {
|
|
2415
|
+
const req = REQUIRED[card.type];
|
|
2416
|
+
if (!req) continue;
|
|
2417
|
+
if (onlyType && card.type !== onlyType) continue;
|
|
2418
|
+
const rule = THRESHOLDS[card.type] || {};
|
|
2419
|
+
const missing = [];
|
|
2420
|
+
for (const name of req) {
|
|
2421
|
+
const issue = checkField(card.type, card.fields || {}, name, rule[name] || {});
|
|
2422
|
+
if (issue) missing.push(issue);
|
|
2423
|
+
}
|
|
2424
|
+
if (missing.length > 0) {
|
|
2425
|
+
if (!findings.by_type[card.type]) {
|
|
2426
|
+
findings.by_type[card.type] = { missing_count: 0, cards: [] };
|
|
2427
|
+
}
|
|
2428
|
+
findings.by_type[card.type].missing_count += 1;
|
|
2429
|
+
findings.by_type[card.type].cards.push({
|
|
2430
|
+
id: card.id,
|
|
2431
|
+
one_sentence: card.fields?.one_sentence || card.fields?.statement || card.fields?.question || '',
|
|
2432
|
+
missing,
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
if (useJson) {
|
|
2438
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
2439
|
+
} else {
|
|
2440
|
+
console.log(`Project: ${findings.project}`);
|
|
2441
|
+
console.log(`Cards: ${findings.card_count} (locked: ${findings.locked_count})`);
|
|
2442
|
+
const types = Object.keys(findings.by_type);
|
|
2443
|
+
if (types.length === 0) {
|
|
2444
|
+
console.log('\nAll cards pass the v1.7.2+ Human Lock gate. Ready to migrate.');
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
for (const t of types) {
|
|
2448
|
+
const block = findings.by_type[t];
|
|
2449
|
+
console.log(`\n${t}: ${block.missing_count} card(s) with missing fields`);
|
|
2450
|
+
for (const card of block.cards) {
|
|
2451
|
+
console.log(` ${card.id} ${(card.one_sentence || '').slice(0, 60)}`);
|
|
2452
|
+
for (const m of card.missing) {
|
|
2453
|
+
if (m.issue === 'missing') {
|
|
2454
|
+
console.log(` - ${m.field}: missing`);
|
|
2455
|
+
} else if (m.issue === 'too_short') {
|
|
2456
|
+
console.log(` - ${m.field}: too short (have ${m.have_length}, need ≥ ${m.need_min})`);
|
|
2457
|
+
} else if (m.issue === 'invalid_value') {
|
|
2458
|
+
console.log(` - ${m.field}: invalid value "${m.have}", allowed: ${m.allowed.join('|')}`);
|
|
2459
|
+
} else {
|
|
2460
|
+
console.log(` - ${m.field}: ${m.issue}`);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2165
2468
|
// ─── Distillation Commands ──────────────────────────────────────────
|
|
2166
2469
|
|
|
2167
2470
|
function cmdTarget(args) {
|
|
@@ -2254,9 +2557,15 @@ async function cmdDistill(args) {
|
|
|
2254
2557
|
const projectInput = args[0];
|
|
2255
2558
|
const candidatesFile = option(args, '--candidates');
|
|
2256
2559
|
const useAI = args.includes('--ai');
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2560
|
+
// Bug (#1 UX): prior version called resolveApiKey(args)
|
|
2561
|
+
// unconditionally, which is fine when --candidates is set (the
|
|
2562
|
+
// call is a no-op) but it also surfaced the env-var resolution
|
|
2563
|
+
// on every run, so a caller that never asked for the LLM was
|
|
2564
|
+
// still at the mercy of `resolveApiKey` warnings. Defer the
|
|
2565
|
+
// LLM-key resolution to the --ai branch.
|
|
2566
|
+
const provider = useAI ? option(args, '--provider') : null;
|
|
2567
|
+
const model = useAI ? option(args, '--model') : null;
|
|
2568
|
+
const apiKey = useAI ? resolveApiKey(args) : null;
|
|
2260
2569
|
if (!projectInput) fail('Usage: kdna-studio distill <project> [--ai] [--candidates <file.json>]');
|
|
2261
2570
|
if (!candidatesFile && !useAI) fail('Either --ai or --candidates <file.json> required.');
|
|
2262
2571
|
|
|
@@ -2446,11 +2755,35 @@ async function cmdFeynman(args) {
|
|
|
2446
2755
|
}
|
|
2447
2756
|
|
|
2448
2757
|
console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2449
|
-
|
|
2758
|
+
|
|
2759
|
+
// Bug (#1 UX follow-up): if no LLM is configured and the caller
|
|
2760
|
+
// did not pass --no-llm, route through cmdNeedsLlm which either
|
|
2761
|
+
// gives a clear "configure LLM" error or accepts the --no-llm
|
|
2762
|
+
// path. Without this branch, the no-LLM caller would fall
|
|
2763
|
+
// through to the LLM call below and get a stack trace.
|
|
2764
|
+
const llmGate = cmdNeedsLlm(args, 'feynmann');
|
|
2765
|
+
|
|
2766
|
+
let result;
|
|
2767
|
+
if (llmGate.noLlm) {
|
|
2768
|
+
// Static evaluation: every criterion is "unsure" (✗), score 0,
|
|
2769
|
+
// but the command still produces a useful structured result and
|
|
2770
|
+
// saves the synthesised restatement to the card.
|
|
2771
|
+
result = {
|
|
2772
|
+
score: 0,
|
|
2773
|
+
criteria: Object.fromEntries(Object.keys(ai.feynman.CRITERIA).map(k => [k, false])),
|
|
2774
|
+
explanations: { noLlm: llmGate.message },
|
|
2775
|
+
suggestions: [
|
|
2776
|
+
'No LLM configured — every criterion shows ✗. Run `kdna-studio llm config` and re-evaluate for a real score.',
|
|
2777
|
+
'Or set the synthesised restatement via: kdna-studio card update <project> ' + cardId + ' --field feynman_restatement=\'{"text":"<your own restatement>"}\'',
|
|
2778
|
+
],
|
|
2779
|
+
};
|
|
2780
|
+
} else {
|
|
2781
|
+
result = await ai.feynman.evaluate(llmGate.cfg, card, {});
|
|
2782
|
+
}
|
|
2450
2783
|
const score = result.score || 0;
|
|
2451
2784
|
const passed = score >= 4;
|
|
2452
2785
|
|
|
2453
|
-
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}`);
|
|
2786
|
+
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
|
|
2454
2787
|
for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
|
|
2455
2788
|
const r = (result.criteria || {})[criterion] ? '✓' : '✗';
|
|
2456
2789
|
console.log(` ${r} ${criterion}`);
|
|
@@ -2496,18 +2829,35 @@ async function cmdTest(args) {
|
|
|
2496
2829
|
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2497
2830
|
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2498
2831
|
|
|
2832
|
+
// Bug (#1 UX): gate LLM-requiring paths through cmdNeedsLlm.
|
|
2833
|
+
// The test command always needs an LLM (it compares without/with
|
|
2834
|
+
// the loaded domain). --no-llm gives a static "no-op" comparison
|
|
2835
|
+
// that lists the cards that *would* be applied.
|
|
2836
|
+
const llmGate = cmdNeedsLlm(args, 'test');
|
|
2837
|
+
const cfg = llmGate.noLlm ? null : llmGate.cfg;
|
|
2838
|
+
|
|
2499
2839
|
if (canonical === 'baseline') {
|
|
2500
2840
|
// Baseline is a single comparison: keep the prior single-result
|
|
2501
2841
|
// shape so existing parsers / consumers see the same output.
|
|
2502
2842
|
const prompt = `Test the core judgment: ${input}`;
|
|
2503
|
-
|
|
2843
|
+
if (cfg === null) {
|
|
2844
|
+
// No-LLM static result
|
|
2845
|
+
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 || '' }));
|
|
2846
|
+
console.log(JSON.stringify({ no_llm: true, message: llmGate.message, would_apply: cards }, null, 2));
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
const result = await ai.testlab.compare(cfg, domainName, prompt, domainPrompt, {});
|
|
2504
2850
|
console.log(JSON.stringify(result, null, 2));
|
|
2505
2851
|
return;
|
|
2506
2852
|
}
|
|
2507
2853
|
// For non-baseline presets, run the preset + the baseline so the user
|
|
2508
2854
|
// can see both side by side. The preset prompt is the one in
|
|
2509
2855
|
// ai/testlab.js — that's the code path that was dead before.
|
|
2510
|
-
|
|
2856
|
+
if (cfg === null) {
|
|
2857
|
+
console.log(JSON.stringify({ no_llm: true, message: llmGate.message }, null, 2));
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
const result = await ai.testlab.testPreset(cfg, domainName, input, domainPrompt, {});
|
|
2511
2861
|
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2512
2862
|
}
|
|
2513
2863
|
|
|
@@ -2542,6 +2892,7 @@ try {
|
|
|
2542
2892
|
else if (cmd === 'llm') cmdLlm(args.slice(1));
|
|
2543
2893
|
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
2544
2894
|
else if (cmd === 'report') cmdReport(args.slice(1));
|
|
2895
|
+
else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
|
|
2545
2896
|
else if (cmd === 'install') cmdStudioInstall(args.slice(1));
|
|
2546
2897
|
else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
|
|
2547
2898
|
else {
|
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.9",
|
|
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
|
}
|