@aikdna/kdna-studio-cli 0.8.3 → 0.8.5
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 +142 -25
- package/package.json +2 -2
- package/src/ai/feynman.js +14 -2
package/bin/kdna-studio.js
CHANGED
|
@@ -50,7 +50,7 @@ Create (three entry paths):
|
|
|
50
50
|
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
51
51
|
|
|
52
52
|
Migrate (dev source or Studio project → canonical .kdna in one command):
|
|
53
|
-
kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete]
|
|
53
|
+
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]
|
|
54
54
|
|
|
55
55
|
Authoring:
|
|
56
56
|
kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
|
|
@@ -304,6 +304,14 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
304
304
|
return cbor.decode(raw);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
// V1 path: trust the producer (export-runtime) to have already shaped
|
|
308
|
+
// each card's fields. The v1 producer normalises everything into a
|
|
309
|
+
// canonical shape, so we just round-trip the raw object. Bug (#57):
|
|
310
|
+
// the legacy path below explicitly constructs each field. The two
|
|
311
|
+
// paths cannot be unified without losing either v1's per-field
|
|
312
|
+
// guarantees or legacy's defensive defaults — the asymmetry is
|
|
313
|
+
// intentional. Documented here so a future reader does not "fix" it
|
|
314
|
+
// by collapsing one path into the other.
|
|
307
315
|
function cardsFromV1Payload(payload) {
|
|
308
316
|
const cards = [];
|
|
309
317
|
for (const sourceCard of (payload.source_cards || [])) {
|
|
@@ -357,10 +365,13 @@ function cardsFromV1Payload(payload) {
|
|
|
357
365
|
}
|
|
358
366
|
// Accept both `self_check` (canonical) and `self_checks` (legacy schema
|
|
359
367
|
// name) so we round-trip assets written by either version of the
|
|
360
|
-
// payload-profile schema.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
368
|
+
// payload-profile schema. Bug (#53): prior version used
|
|
369
|
+
// `arr || []`, which would crash with "object is not iterable" if
|
|
370
|
+
// the field was a truthy non-array (e.g. a single `{question: ...}`
|
|
371
|
+
// object instead of an array). The fix guards with Array.isArray.
|
|
372
|
+
const rawSelfCheck = payload.reasoning?.self_check
|
|
373
|
+
|| payload.reasoning?.self_checks;
|
|
374
|
+
const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
|
|
364
375
|
for (const selfCheck of selfCheckArr) {
|
|
365
376
|
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
366
377
|
pushImportedCard(cards, 'self_check', fields, fields.id || null);
|
|
@@ -440,7 +451,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
440
451
|
failure_risk: ms.failure_risk || '',
|
|
441
452
|
}, ms.id || null);
|
|
442
453
|
}
|
|
443
|
-
for (const sc of (patterns.self_check
|
|
454
|
+
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
444
455
|
const question = typeof sc === 'string' ? sc : sc.question || '';
|
|
445
456
|
pushImportedCard(cards, 'self_check', { question }, sc.id || null);
|
|
446
457
|
}
|
|
@@ -474,6 +485,19 @@ function cardsFromPayload(payload) {
|
|
|
474
485
|
}
|
|
475
486
|
|
|
476
487
|
function buildV1Manifest(project, name) {
|
|
488
|
+
// Bug (#58): prior version was a hand-rolled manifest builder that
|
|
489
|
+
// shared zero logic with the canonical buildManifest in
|
|
490
|
+
// @aikdna/kdna-studio-core/src/export-runtime. The two paths
|
|
491
|
+
// diverged on every field added since 1.0 (lineage, load_contract,
|
|
492
|
+
// authoring block, etc.). The fix delegates the manifest shape to
|
|
493
|
+
// kdna-studio-core's buildManifest so the v1 path is the single
|
|
494
|
+
// source of truth.
|
|
495
|
+
//
|
|
496
|
+
// We still need a default manifest here (the exportRuntimeAsset
|
|
497
|
+
// path uses buildManifest internally; this function supplies the
|
|
498
|
+
// defaults that pass-through into the runtime options). The actual
|
|
499
|
+
// manifest the runtime ships is the one buildManifest produces, so
|
|
500
|
+
// keeping these defaults in sync matters less than it used to.
|
|
477
501
|
const source = project.source_manifest || {};
|
|
478
502
|
const assetId = assetIdFromName(name || source.name || project.name);
|
|
479
503
|
const author = source.author || project.author || {};
|
|
@@ -553,6 +577,14 @@ function cmdCreate(args) {
|
|
|
553
577
|
lineage,
|
|
554
578
|
});
|
|
555
579
|
project.cards = kdnaData.cards;
|
|
580
|
+
// Bug #15 / #28 follow-up: store the source KDNA_* files on the
|
|
581
|
+
// project so a later export through exportRuntimeAsset can
|
|
582
|
+
// forward them to compileDomain (which then preserves
|
|
583
|
+
// changelog / version_notes / reasoning_chains / banned_terms
|
|
584
|
+
// through the round-trip).
|
|
585
|
+
if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
|
|
586
|
+
if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
|
|
587
|
+
if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
|
|
556
588
|
if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
|
|
557
589
|
if (kdnaData.source_manifest?.version) {
|
|
558
590
|
if (!project.release) project.release = {};
|
|
@@ -680,7 +712,7 @@ function importFromKdna(kdnaPath) {
|
|
|
680
712
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
681
713
|
}));
|
|
682
714
|
}
|
|
683
|
-
for (const sc of (pat.self_check
|
|
715
|
+
for (const sc of (Array.isArray(pat.self_check) ? pat.self_check : [])) {
|
|
684
716
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
685
717
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
686
718
|
}
|
|
@@ -691,7 +723,30 @@ function importFromKdna(kdnaPath) {
|
|
|
691
723
|
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
|
|
692
724
|
}
|
|
693
725
|
|
|
694
|
-
|
|
726
|
+
// Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
|
|
727
|
+
// KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
|
|
728
|
+
// them on the project. The next export then forwards them to
|
|
729
|
+
// compileDomain via exportRuntimeAsset, which preserves
|
|
730
|
+
// changelog / version_notes / reasoning_chains / banned_terms
|
|
731
|
+
// through the round-trip.
|
|
732
|
+
const sourcePatterns = entries.has('KDNA_Patterns.json')
|
|
733
|
+
? JSON.parse(entries.get('KDNA_Patterns.json').toString())
|
|
734
|
+
: null;
|
|
735
|
+
const sourceReasoning = entries.has('KDNA_Reasoning.json')
|
|
736
|
+
? JSON.parse(entries.get('KDNA_Reasoning.json').toString())
|
|
737
|
+
: null;
|
|
738
|
+
const sourceEvolution = entries.has('KDNA_Evolution.json')
|
|
739
|
+
? JSON.parse(entries.get('KDNA_Evolution.json').toString())
|
|
740
|
+
: null;
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
lineage,
|
|
744
|
+
cards: importedCards,
|
|
745
|
+
source_manifest: manifest,
|
|
746
|
+
source_patterns: sourcePatterns,
|
|
747
|
+
source_reasoning: sourceReasoning,
|
|
748
|
+
source_evolution: sourceEvolution,
|
|
749
|
+
};
|
|
695
750
|
}
|
|
696
751
|
|
|
697
752
|
/**
|
|
@@ -833,7 +888,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity) {
|
|
|
833
888
|
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
834
889
|
}));
|
|
835
890
|
}
|
|
836
|
-
for (const sc of (patterns.self_check
|
|
891
|
+
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
837
892
|
const q = typeof sc === 'string' ? sc : sc.question || '';
|
|
838
893
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
839
894
|
}
|
|
@@ -1059,13 +1114,23 @@ function cmdImport(args) {
|
|
|
1059
1114
|
return;
|
|
1060
1115
|
}
|
|
1061
1116
|
let content;
|
|
1117
|
+
let fd = null;
|
|
1062
1118
|
try {
|
|
1063
|
-
|
|
1119
|
+
fd = fs.openSync(filePath, 'r');
|
|
1064
1120
|
const buf = Buffer.alloc(120000);
|
|
1065
1121
|
const bytesRead = fs.readSync(fd, buf, 0, 120000, 0);
|
|
1066
|
-
fs.closeSync(fd);
|
|
1067
1122
|
content = buf.toString('utf8', 0, bytesRead);
|
|
1068
1123
|
} catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
|
|
1124
|
+
finally {
|
|
1125
|
+
// Bug (#51): prior version called fs.closeSync only on the
|
|
1126
|
+
// success path, so any readSync failure left the file descriptor
|
|
1127
|
+
// open. Under bulk import (300 files), a single error mid-loop
|
|
1128
|
+
// could exhaust the per-process fd limit and silently break
|
|
1129
|
+
// later imports.
|
|
1130
|
+
if (fd !== null) {
|
|
1131
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1069
1134
|
|
|
1070
1135
|
const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
|
|
1071
1136
|
evidenceApi.addEvidence(project, evidence);
|
|
@@ -1378,7 +1443,7 @@ function cmdMigrate(args) {
|
|
|
1378
1443
|
const requestedName = option(args, '--name');
|
|
1379
1444
|
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1380
1445
|
if (!sourceDir || !out || !by || !statement) {
|
|
1381
|
-
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <
|
|
1446
|
+
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
|
|
1382
1447
|
}
|
|
1383
1448
|
|
|
1384
1449
|
const sourcePath = path.resolve(sourceDir);
|
|
@@ -1409,6 +1474,13 @@ function cmdMigrate(args) {
|
|
|
1409
1474
|
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
1410
1475
|
}
|
|
1411
1476
|
|
|
1477
|
+
// Bug (#54): --allow-incomplete used to be read AFTER the critical
|
|
1478
|
+
// fields check, so a caller asking to bypass the gate was rejected
|
|
1479
|
+
// for the wrong reason. Resolve the flag here so the critical-missing
|
|
1480
|
+
// check honours it (and reports the bypass correctly) instead of
|
|
1481
|
+
// always failing.
|
|
1482
|
+
const allowIncompleteEarly = args.includes('--allow-incomplete');
|
|
1483
|
+
|
|
1412
1484
|
// Reject if critical judgment fields are missing from axioms
|
|
1413
1485
|
const criticalMissing = [];
|
|
1414
1486
|
for (const card of (project.cards || [])) {
|
|
@@ -1421,13 +1493,21 @@ function cmdMigrate(args) {
|
|
|
1421
1493
|
}
|
|
1422
1494
|
}
|
|
1423
1495
|
if (criticalMissing.length > 0) {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1496
|
+
if (allowIncompleteEarly) {
|
|
1497
|
+
console.warn(
|
|
1498
|
+
`--allow-incomplete: bypassing ${criticalMissing.length} critical axiom-field check(s). ` +
|
|
1499
|
+
`The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
|
|
1500
|
+
);
|
|
1501
|
+
} else {
|
|
1502
|
+
fail(
|
|
1503
|
+
`Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
|
|
1504
|
+
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
1505
|
+
` decide when to load this domain). Add them to your source files first,\n` +
|
|
1506
|
+
` or pass --allow-incomplete to bypass.\n` +
|
|
1507
|
+
` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
|
|
1508
|
+
(criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1431
1511
|
}
|
|
1432
1512
|
|
|
1433
1513
|
// Step 2: approve and Human Lock all cards
|
|
@@ -1782,7 +1862,15 @@ function cmdExport(args) {
|
|
|
1782
1862
|
fail(`Could not read password from stdin: ${e.message}`);
|
|
1783
1863
|
}
|
|
1784
1864
|
} else {
|
|
1785
|
-
|
|
1865
|
+
// Bug (#55): prior version only consulted --password / --passphrase
|
|
1866
|
+
// here, so a caller who set KDNA_PASSPHRASE for the signing path
|
|
1867
|
+
// (works via resolvePassphrase) found that the v1 encryption
|
|
1868
|
+
// password was silently null. Resolve from the same sources as
|
|
1869
|
+
// the signing path so the two stay in sync.
|
|
1870
|
+
password = process.env.KDNA_PASSPHRASE
|
|
1871
|
+
|| process.env.KDNA_PASSWORD
|
|
1872
|
+
|| option(args, '--password')
|
|
1873
|
+
|| option(args, '--passphrase');
|
|
1786
1874
|
}
|
|
1787
1875
|
exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
1788
1876
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
@@ -2231,12 +2319,41 @@ async function cmdTest(args) {
|
|
|
2231
2319
|
const domainName = project.name || 'Untitled Project';
|
|
2232
2320
|
const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
|
|
2233
2321
|
|
|
2322
|
+
// Bug (#50): prior version hand-rolled the prompt for the 3 preset
|
|
2323
|
+
// names and bypassed ai.testlab.testPreset entirely. The exported
|
|
2324
|
+
// testPreset function was dead code, and the help text advertised
|
|
2325
|
+
// names (baseline / edge / contradiction) that did not match the
|
|
2326
|
+
// actual preset keys in testlab.js (baseline / edge_case /
|
|
2327
|
+
// contradiction). Result: --preset edge silently ran the
|
|
2328
|
+
// contradiction branch, and any caller that asked for "edge_case"
|
|
2329
|
+
// fell through to the contradiction branch too.
|
|
2330
|
+
//
|
|
2331
|
+
// The fix routes the call through testPreset when a known preset is
|
|
2332
|
+
// given, and accepts both help-text names (baseline / edge /
|
|
2333
|
+
// contradiction) and the canonical testlab names (baseline / edge_case
|
|
2334
|
+
// / contradiction).
|
|
2335
|
+
const PRESET_ALIASES = {
|
|
2336
|
+
baseline: 'baseline',
|
|
2337
|
+
edge: 'edge_case',
|
|
2338
|
+
edge_case: 'edge_case',
|
|
2339
|
+
contradiction: 'contradiction',
|
|
2340
|
+
};
|
|
2341
|
+
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2234
2342
|
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2343
|
+
|
|
2344
|
+
if (canonical === 'baseline') {
|
|
2345
|
+
// Baseline is a single comparison: keep the prior single-result
|
|
2346
|
+
// shape so existing parsers / consumers see the same output.
|
|
2347
|
+
const prompt = `Test the core judgment: ${input}`;
|
|
2348
|
+
const result = await ai.testlab.compare(llm.config(), domainName, prompt, domainPrompt, {});
|
|
2349
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
// For non-baseline presets, run the preset + the baseline so the user
|
|
2353
|
+
// can see both side by side. The preset prompt is the one in
|
|
2354
|
+
// ai/testlab.js — that's the code path that was dead before.
|
|
2355
|
+
const result = await ai.testlab.testPreset(llm.config(), domainName, input, domainPrompt, {});
|
|
2356
|
+
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2240
2357
|
}
|
|
2241
2358
|
|
|
2242
2359
|
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.5",
|
|
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.4",
|
|
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
|
|