@aikdna/kdna-studio-cli 0.10.2 → 0.11.0
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 +146 -36
- package/bin/kdna-studio.js +474 -351
- package/docs/CREATION_COMMAND_CONTRACT.md +235 -0
- package/docs/TERMINAL_AGENT_CREATION.md +219 -0
- package/package.json +13 -5
- package/src/ai/distill.js +21 -2
- package/src/ai/index.js +1 -3
- package/src/ai/interview.js +28 -2
- package/src/application-host.js +561 -0
- package/src/creation-cli.js +7721 -0
- package/src/llm/config.js +16 -3
- package/src/llm/index.js +3 -3
- package/src/llm/providers/anthropic.js +8 -7
- package/src/llm/providers/openai.js +8 -7
- package/src/llm/transport.js +196 -0
- package/src/ai/feynman.js +0 -120
- package/src/ai/testlab.js +0 -73
package/bin/kdna-studio.js
CHANGED
|
@@ -3,18 +3,109 @@ const fs = require('fs');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
|
|
6
7
|
const zlib = require('zlib');
|
|
7
8
|
const cbor = require('cbor-x');
|
|
8
9
|
const { execFileSync } = require('child_process');
|
|
9
10
|
|
|
11
|
+
function deterministicSourceDigest(paths) {
|
|
12
|
+
const files = [];
|
|
13
|
+
function visit(target, root) {
|
|
14
|
+
const stat = fs.lstatSync(target);
|
|
15
|
+
if (stat.isSymbolicLink()) {
|
|
16
|
+
throw new Error('Studio Core source checkout contains a symbolic link.');
|
|
17
|
+
}
|
|
18
|
+
if (stat.isFile()) {
|
|
19
|
+
files.push({ target, relative: path.relative(root, target) });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!stat.isDirectory()) return;
|
|
23
|
+
for (const entry of fs.readdirSync(target).sort()) {
|
|
24
|
+
visit(path.join(target, entry), root);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const target of paths) {
|
|
28
|
+
const absolute = path.resolve(target);
|
|
29
|
+
visit(absolute, path.dirname(absolute));
|
|
30
|
+
}
|
|
31
|
+
files.sort((left, right) => left.relative.localeCompare(right.relative));
|
|
32
|
+
const hash = crypto.createHash('sha256');
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
hash.update(file.relative);
|
|
35
|
+
hash.update('\0');
|
|
36
|
+
hash.update(fs.readFileSync(file.target));
|
|
37
|
+
hash.update('\0');
|
|
38
|
+
}
|
|
39
|
+
return `sha256:${hash.digest('hex')}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
function loadStudioCore() {
|
|
11
43
|
const publishedCore = require('@aikdna/kdna-studio-core');
|
|
12
|
-
|
|
44
|
+
const publishedVersion = require('@aikdna/kdna-studio-core/package.json').version;
|
|
45
|
+
if (
|
|
46
|
+
publishedCore.exportRuntime &&
|
|
47
|
+
publishedCore.protocolContract &&
|
|
48
|
+
publishedCore.creationEngine &&
|
|
49
|
+
publishedCore.creationEngine.SCHEMA_VERSION === '0.2.0' &&
|
|
50
|
+
typeof publishedCore.creationEngine.readManagedCandidate === 'function'
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
...publishedCore,
|
|
54
|
+
creationExportRuntime: publishedCore.exportRuntime,
|
|
55
|
+
creationStudioCoreCoordinate: {
|
|
56
|
+
package: '@aikdna/kdna-studio-core',
|
|
57
|
+
version: publishedVersion,
|
|
58
|
+
distribution: 'installed-package',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
13
62
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
63
|
+
const sourcePaths = [
|
|
64
|
+
path.join(__dirname, '../../kdna-studio-core/src'),
|
|
65
|
+
path.join(
|
|
66
|
+
__dirname,
|
|
67
|
+
'../../kdna-studio-core/schemas/creation-workspace.schema.json',
|
|
68
|
+
),
|
|
69
|
+
path.join(__dirname, '../../kdna-studio-core/package.json'),
|
|
70
|
+
path.join(__dirname, '../../kdna-studio-core/package-lock.json'),
|
|
71
|
+
].filter((target) => fs.existsSync(target));
|
|
72
|
+
const sourceDigestBeforeLoad = deterministicSourceDigest(sourcePaths);
|
|
73
|
+
const sourceCore = require('../../kdna-studio-core/src');
|
|
74
|
+
const sourceDigestAfterLoad = deterministicSourceDigest(sourcePaths);
|
|
75
|
+
if (sourceDigestBeforeLoad !== sourceDigestAfterLoad) {
|
|
76
|
+
throw new Error('Studio Core source changed while it was being loaded.');
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
sourceCore.exportRuntime &&
|
|
80
|
+
sourceCore.protocolContract &&
|
|
81
|
+
sourceCore.creationEngine
|
|
82
|
+
) {
|
|
83
|
+
// Keep the installed package as the binding for established expert
|
|
84
|
+
// commands. A sibling source checkout supplies only the unreleased
|
|
85
|
+
// Creation Engine and its matching exporter during coordinated
|
|
86
|
+
// development; it must not silently replace expert APIs.
|
|
87
|
+
return {
|
|
88
|
+
...publishedCore,
|
|
89
|
+
creationEngine: sourceCore.creationEngine,
|
|
90
|
+
creationExportRuntime: sourceCore.exportRuntime,
|
|
91
|
+
creationStudioCoreCoordinate: {
|
|
92
|
+
package: '@aikdna/kdna-studio-core',
|
|
93
|
+
version: require('../../kdna-studio-core/package.json').version,
|
|
94
|
+
distribution: 'source-checkout',
|
|
95
|
+
source_tree_digest: sourceDigestAfterLoad,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (
|
|
101
|
+
error?.code !== 'MODULE_NOT_FOUND' &&
|
|
102
|
+
error?.code !== 'ENOENT'
|
|
103
|
+
) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
// Installed packages do not have a sibling source checkout.
|
|
17
107
|
}
|
|
108
|
+
return publishedCore;
|
|
18
109
|
}
|
|
19
110
|
|
|
20
111
|
const {
|
|
@@ -22,68 +113,93 @@ const {
|
|
|
22
113
|
cards: cardApi,
|
|
23
114
|
evidence: evidenceApi,
|
|
24
115
|
compile: compileApi,
|
|
25
|
-
quality,
|
|
26
116
|
creator: creatorApi,
|
|
27
117
|
distillation: distillationApi,
|
|
28
118
|
exportRuntime,
|
|
119
|
+
creationExportRuntime,
|
|
120
|
+
creationStudioCoreCoordinate,
|
|
29
121
|
protocolContract,
|
|
122
|
+
creationEngine,
|
|
30
123
|
} = loadStudioCore();
|
|
31
124
|
|
|
32
125
|
const llm = require('../src/llm');
|
|
33
126
|
|
|
34
|
-
// Bug (#1 UX): the LLM-requiring commands (distill, interview, feynman,
|
|
35
|
-
// test) silently fail with a stack trace when no LLM is configured.
|
|
36
|
-
// First-time users hit this and have no way to know what to do. The
|
|
37
|
-
// fix routes every LLM call through this helper, which:
|
|
38
|
-
// 1. Checks whether an LLM is configured at all
|
|
39
|
-
// 2. If not and --no-llm is given, returns a static / synthesised
|
|
40
|
-
// result so the command can still be useful (5-minute path)
|
|
41
|
-
// 3. If not and --no-llm is absent, exits with a clear
|
|
42
|
-
// "configure LLM with: kdna-studio llm config" message
|
|
43
|
-
function cmdNeedsLlm(args, cmdName) {
|
|
44
|
-
const cfg = llm.config();
|
|
45
|
-
const hasLlm = cfg && cfg.provider && cfg.apiKey && cfg.model;
|
|
46
|
-
if (hasLlm) return { hasLlm: true, cfg };
|
|
47
|
-
if (args.includes('--no-llm')) {
|
|
48
|
-
return {
|
|
49
|
-
hasLlm: false,
|
|
50
|
-
cfg,
|
|
51
|
-
noLlm: true,
|
|
52
|
-
message: `${cmdName} invoked with --no-llm. Output will be static / synthesised; configure an LLM with \`kdna-studio llm config\` to enable real evaluation.`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
fail(
|
|
56
|
-
`${cmdName} requires an LLM. Configure one with:\n` +
|
|
57
|
-
` kdna-studio llm config --provider openai --key <api-key> --model <model>\n` +
|
|
58
|
-
`Or set env vars: KDNA_LLM_PROVIDER, KDNA_LLM_API_KEY, KDNA_LLM_MODEL.\n` +
|
|
59
|
-
`Or run with --no-llm to get a static / synthesised result.`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
127
|
const ai = require('../src/ai');
|
|
128
|
+
const {
|
|
129
|
+
CREATION_COMMANDS,
|
|
130
|
+
decodeSecretTransport,
|
|
131
|
+
executeCreationCommand,
|
|
132
|
+
} = require('../src/creation-cli');
|
|
63
133
|
|
|
64
134
|
const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
135
|
+
const SECRET_STDIN_LIMIT_BYTES = 64 * 1024;
|
|
65
136
|
|
|
66
|
-
function
|
|
67
|
-
|
|
137
|
+
function readBoundedSecretStdin(label) {
|
|
138
|
+
const storage = Buffer.alloc(SECRET_STDIN_LIMIT_BYTES + 1);
|
|
139
|
+
let length = 0;
|
|
140
|
+
try {
|
|
141
|
+
while (length < storage.length) {
|
|
142
|
+
const count = fs.readSync(
|
|
143
|
+
0,
|
|
144
|
+
storage,
|
|
145
|
+
length,
|
|
146
|
+
storage.length - length,
|
|
147
|
+
null,
|
|
148
|
+
);
|
|
149
|
+
if (count === 0) break;
|
|
150
|
+
length += count;
|
|
151
|
+
}
|
|
152
|
+
if (length > SECRET_STDIN_LIMIT_BYTES) {
|
|
153
|
+
throw new Error(`${label} exceeds the bounded secret input limit`);
|
|
154
|
+
}
|
|
155
|
+
return decodeSecretTransport(storage.subarray(0, length));
|
|
156
|
+
} finally {
|
|
157
|
+
storage.fill(0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
68
160
|
|
|
69
|
-
|
|
161
|
+
function usage() {
|
|
162
|
+
console.log(`kdna-studio — Agent-guided KDNA Creation Engine
|
|
163
|
+
|
|
164
|
+
Create without schema editing:
|
|
165
|
+
kdna-studio guide-agent --action create|inventory --json
|
|
166
|
+
kdna-studio guide-agent <workspace> --json
|
|
167
|
+
kdna-studio create-agent <workspace> [--input-file <json> | --input-stdin] [--material <path>] [--password-stdin] [--operation-id <id>] [--json]
|
|
168
|
+
kdna-studio inventory-agent <material-file-or-directory> [--material <path>] [--workspace <existing-creation-workspace>] [--json]
|
|
169
|
+
kdna-studio deliver-material <workspace> (--private-output-fd 3 | --private-output-file <mode-0600-system-temp-file>) [--input-file <json> | --input-stdin] [--material <reauthorized-path>] [--json]
|
|
170
|
+
kdna-studio resume <workspace> [--input-file <json> | --input-stdin] [--material <path>] [--password-stdin] [--operation-id <id>] [--json]
|
|
171
|
+
kdna-studio status <workspace> [--json]
|
|
172
|
+
kdna-studio answer <workspace> [--input-file <json> | --input-stdin] [--operation-id <id>] [--json]
|
|
173
|
+
kdna-studio review <workspace> [--input-file <json> | --input-stdin] [--operation-id <id>] [--json]
|
|
174
|
+
kdna-studio try <workspace> [--input-file <json> | --input-stdin] [--operation-id <id>] [--json]
|
|
175
|
+
kdna-studio verify-application-agent <workspace> [--password-stdin] [--json]
|
|
176
|
+
kdna-studio repair <workspace> [--input-file <json> | --input-stdin] [--operation-id <id>] [--json]
|
|
177
|
+
kdna-studio export-agent <workspace> [--password-stdin] [--operation-id <id>] [--json]
|
|
178
|
+
kdna-studio finalize-agent <workspace> --out <file.kdna> [--operation-id <id>] [--json]
|
|
179
|
+
|
|
180
|
+
The default Creation Engine speaks in purpose, material, judgment, boundary,
|
|
181
|
+
example, confirmation, and export terms. Natural-language answers belong in
|
|
182
|
+
stdin or an input file, never process arguments.
|
|
183
|
+
|
|
184
|
+
Expert authoring:
|
|
185
|
+
|
|
186
|
+
LLM configuration for expert AI-assisted authoring:
|
|
70
187
|
kdna-studio llm config [--provider <name>] [--model <name>] [--key-pipe] [--url <base-url>]
|
|
71
188
|
kdna-studio llm show
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
still produce a structured but unsynthesised result.)
|
|
189
|
+
(pipe the key to 'kdna-studio llm config --provider openai --key-pipe --model gpt-4'
|
|
190
|
+
to enable distill --ai and interview.)
|
|
75
191
|
|
|
76
192
|
Identity:
|
|
77
|
-
kdna-studio identity init [--name <display-name>]
|
|
193
|
+
kdna-studio identity init [--name <display-name>] [--passphrase-stdin]
|
|
78
194
|
kdna-studio identity show
|
|
79
195
|
|
|
80
196
|
Create (three entry paths):
|
|
81
|
-
kdna-studio create <project-dir> --name <@scope/name> [--author <name>]
|
|
197
|
+
kdna-studio create <project-dir> --name <@scope/name> [--author <name>] [--use-local-identity] # blank
|
|
82
198
|
kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
|
|
83
199
|
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
84
200
|
|
|
85
201
|
Migrate (dev source or Studio project → canonical .kdna in one command):
|
|
86
|
-
kdna-studio migrate <source-dir|project> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--
|
|
202
|
+
kdna-studio migrate <source-dir|project> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--use-local-identity] [--passphrase-stdin]
|
|
87
203
|
kdna-studio migrate <source-dir> --check --name <@scope/name> # pre-flight: report which fields would block the export without writing the .kdna
|
|
88
204
|
kdna-studio audit-locks <project> [--type axiom|risk|stance|...] [--json] # list cards with missing Human Lock fields (per-card-type, per-field)
|
|
89
205
|
|
|
@@ -95,10 +211,10 @@ Authoring:
|
|
|
95
211
|
kdna-studio card add <project> <type> --field key=value [--field key=value] [--template <name>] [--no-strict]
|
|
96
212
|
kdna-studio card update <project> <card-id> --field key=value
|
|
97
213
|
kdna-studio card remove <project> <card-id>
|
|
98
|
-
kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign] [--passphrase
|
|
214
|
+
kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign] [--passphrase-stdin]
|
|
99
215
|
kdna-studio card unlock <project> <card-id> --by <id> --statement <text>
|
|
100
216
|
kdna-studio compile <project> --out <dir>
|
|
101
|
-
kdna-studio export <project> --out <file.kdna> [--allow-incomplete] [--password
|
|
217
|
+
kdna-studio export <project> --out <file.kdna> [--allow-incomplete] [--password-stdin]
|
|
102
218
|
kdna-studio export-route-card <domain-id> [--out=<path>] # Export route-card sidecar skeleton
|
|
103
219
|
kdna-studio export-consumer-index [--entries=<domain-ids>] [--out=<path>] # Export consumer-index skeleton
|
|
104
220
|
|
|
@@ -106,8 +222,6 @@ AI Authoring (requires LLM config: kdna-studio llm config):
|
|
|
106
222
|
kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
|
|
107
223
|
kdna-studio distill <project> --candidates <file.json> # load pre-generated AI candidates
|
|
108
224
|
kdna-studio interview <project> [--stage <name>] # 4-stage guided AI interview
|
|
109
|
-
kdna-studio feynman <project> <card-id> # AI Feynman evaluation (5-dimension)
|
|
110
|
-
kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]
|
|
111
225
|
|
|
112
226
|
Distillation:
|
|
113
227
|
kdna-studio target declare <project> # declare distillation target interactively
|
|
@@ -118,7 +232,6 @@ Distillation:
|
|
|
118
232
|
kdna-studio candidate reject <project> <candidate-id>
|
|
119
233
|
kdna-studio candidate override <project> <candidate-id> # override scope gate
|
|
120
234
|
kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
|
|
121
|
-
kdna-studio report <project>
|
|
122
235
|
kdna-studio audit-locks <project> # list all axioms/risk/stance missing Human Lock fields
|
|
123
236
|
|
|
124
237
|
Project may be a directory containing studio.project.json or a project JSON file.`);
|
|
@@ -159,64 +272,58 @@ function option(args, name, fallback = null) {
|
|
|
159
272
|
}
|
|
160
273
|
|
|
161
274
|
/**
|
|
162
|
-
* Resolve a passphrase from
|
|
163
|
-
*
|
|
164
|
-
* SECURITY: passing a passphrase as `--passphrase <value>` exposes it in
|
|
165
|
-
* `ps aux` output and shell history. The recommended path is one of:
|
|
166
|
-
* 1. --passphrase-stdin — read from stdin (pipe-friendly, no TTY hang)
|
|
167
|
-
* 2. KDNA_PASSPHRASE env var (less secure but no process-list leak)
|
|
168
|
-
* 3. --passphrase <pw> — fallback only; prints a warning
|
|
275
|
+
* Resolve a passphrase from stdin. Secrets in argv are rejected.
|
|
169
276
|
*
|
|
170
277
|
* Returns the passphrase string, or null if none was provided.
|
|
171
278
|
*/
|
|
172
279
|
function resolvePassphrase(args) {
|
|
280
|
+
if (
|
|
281
|
+
args.includes('--passphrase') ||
|
|
282
|
+
args.some((arg) => arg.startsWith('--passphrase='))
|
|
283
|
+
) {
|
|
284
|
+
fail(
|
|
285
|
+
'--passphrase is not supported because it exposes secrets in process arguments. ' +
|
|
286
|
+
'Use --passphrase-stdin.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
173
289
|
if (args.includes('--passphrase-stdin')) {
|
|
174
290
|
if (process.stdin.isTTY) {
|
|
175
291
|
fail(
|
|
176
292
|
'--passphrase-stdin requires the passphrase to be piped in on stdin.\n' +
|
|
177
|
-
'Example: read -s PW &&
|
|
293
|
+
'Example: read -r -s PW && printf "%s\\n" "$PW" | kdna-studio export ... --sign --passphrase-stdin'
|
|
178
294
|
);
|
|
179
295
|
}
|
|
180
296
|
try {
|
|
181
|
-
return
|
|
297
|
+
return readBoundedSecretStdin('Passphrase');
|
|
182
298
|
} catch (e) {
|
|
183
299
|
fail(`Could not read passphrase from stdin: ${e.message}`);
|
|
184
300
|
}
|
|
185
301
|
}
|
|
186
|
-
if (process.env.KDNA_PASSPHRASE) return process.env.KDNA_PASSPHRASE;
|
|
187
|
-
const fromFlag = option(args, '--passphrase');
|
|
188
|
-
if (fromFlag) {
|
|
189
|
-
console.error('Warning: --passphrase <value> exposes the secret in `ps aux` and shell history. Prefer --passphrase-stdin or KDNA_PASSPHRASE env var.');
|
|
190
|
-
return fromFlag;
|
|
191
|
-
}
|
|
192
302
|
return null;
|
|
193
303
|
}
|
|
194
304
|
|
|
195
305
|
function resolveApiKey(args) {
|
|
196
|
-
|
|
197
|
-
|
|
306
|
+
if (
|
|
307
|
+
args.includes('--key') ||
|
|
308
|
+
args.includes('-k') ||
|
|
309
|
+
args.some((arg) => arg.startsWith('--key='))
|
|
310
|
+
) {
|
|
311
|
+
fail('API keys in process arguments are not supported. Use --key-pipe.');
|
|
312
|
+
}
|
|
198
313
|
|
|
199
|
-
// 2. Read from stdin via --key-pipe flag
|
|
200
314
|
if (args.includes('--key-pipe')) {
|
|
201
315
|
if (process.stdin.isTTY) {
|
|
202
|
-
console.error('Error: --key-pipe requires
|
|
316
|
+
console.error('Error: --key-pipe requires the API key to be piped on stdin.');
|
|
203
317
|
return null;
|
|
204
318
|
}
|
|
205
319
|
try {
|
|
206
|
-
return
|
|
320
|
+
return readBoundedSecretStdin('API key');
|
|
207
321
|
} catch (e) {
|
|
208
322
|
console.error('Error reading API key from stdin:', e.message);
|
|
209
323
|
return null;
|
|
210
324
|
}
|
|
211
325
|
}
|
|
212
326
|
|
|
213
|
-
// 3. Deprecated --key / -k flag (backward compat)
|
|
214
|
-
const key = option(args, '--key', null) || option(args, '-k', null);
|
|
215
|
-
if (key) {
|
|
216
|
-
console.error('Warning: --key is deprecated and exposes secrets in shell history and ps output. Use KDNA_API_KEY env var or pipe via stdin (--key-pipe).');
|
|
217
|
-
return key;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
327
|
return null;
|
|
221
328
|
}
|
|
222
329
|
|
|
@@ -676,8 +783,19 @@ function cmdCreate(args) {
|
|
|
676
783
|
let lineage = null;
|
|
677
784
|
let creatorIdentity = null;
|
|
678
785
|
|
|
679
|
-
//
|
|
680
|
-
|
|
786
|
+
// A machine-local identity is never inherited implicitly. Creating for
|
|
787
|
+
// another person, an organization, or an interpretive source must not pick
|
|
788
|
+
// up whichever identity happens to be configured on this computer.
|
|
789
|
+
if (args.includes('--use-local-identity')) {
|
|
790
|
+
try {
|
|
791
|
+
creatorIdentity = creatorApi.loadIdentity();
|
|
792
|
+
} catch {
|
|
793
|
+
fail(
|
|
794
|
+
'--use-local-identity was requested, but no usable local identity is available.',
|
|
795
|
+
EXIT.TRUST_FAILED,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
681
799
|
|
|
682
800
|
if (fromKdna) {
|
|
683
801
|
sourceMode = 'kdna_asset';
|
|
@@ -776,7 +894,8 @@ function importFromKdna(kdnaPath) {
|
|
|
776
894
|
fail(`KDNA file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${absKdna}`);
|
|
777
895
|
}
|
|
778
896
|
|
|
779
|
-
//
|
|
897
|
+
// Read the container before Core validation only to produce a stable
|
|
898
|
+
// profile error. Studio does not retain a second Runtime contract.
|
|
780
899
|
const zipBuf = fs.readFileSync(absKdna);
|
|
781
900
|
const entries = readZipEntries(zipBuf);
|
|
782
901
|
validateZipContainer(entries);
|
|
@@ -787,22 +906,17 @@ function importFromKdna(kdnaPath) {
|
|
|
787
906
|
const payload = decodePayload(entries.get('payload.kdnab'), manifest);
|
|
788
907
|
const profile = payload?.profile;
|
|
789
908
|
|
|
790
|
-
|
|
791
|
-
|
|
909
|
+
if (profile !== 'kdna.payload.judgment') {
|
|
910
|
+
fail(`Unsupported payload profile "${profile || '(none)'}". Only kdna.payload.judgment is accepted for --from-kdna.`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let runtimeCore;
|
|
792
914
|
try { runtimeCore = require('@aikdna/kdna-core'); } catch {
|
|
793
915
|
fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
|
|
794
916
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
|
|
799
|
-
}
|
|
800
|
-
isLegacy = false;
|
|
801
|
-
} else if (profile === 'judgment-profile-v1') {
|
|
802
|
-
validateLegacyImportContract(manifest, payload);
|
|
803
|
-
isLegacy = true;
|
|
804
|
-
} else {
|
|
805
|
-
fail(`Unsupported payload profile "${profile || '(none)'}". Only kdna.payload.judgment and judgment-profile-v1 are accepted for --from-kdna.`);
|
|
917
|
+
const validation = runtimeCore.validate(absKdna);
|
|
918
|
+
if (!validation.overall_valid) {
|
|
919
|
+
fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
|
|
806
920
|
}
|
|
807
921
|
|
|
808
922
|
const lineage = {
|
|
@@ -813,28 +927,54 @@ function importFromKdna(kdnaPath) {
|
|
|
813
927
|
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
814
928
|
};
|
|
815
929
|
|
|
816
|
-
|
|
817
|
-
let judgmentCore = null;
|
|
930
|
+
let imported;
|
|
818
931
|
try {
|
|
819
|
-
|
|
820
|
-
importedCards.push(...cardsFromLegacyPayload(payload));
|
|
821
|
-
judgmentCore = judgmentCoreFromLegacyPayload(payload);
|
|
822
|
-
} else {
|
|
823
|
-
importedCards.push(...cardsFromPayload(payload));
|
|
824
|
-
judgmentCore = judgmentCoreFromRuntimePayload(payload);
|
|
825
|
-
}
|
|
932
|
+
imported = importFromRuntimeContent(manifest, payload);
|
|
826
933
|
} catch (e) {
|
|
827
934
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
828
935
|
}
|
|
829
936
|
|
|
830
|
-
if (
|
|
937
|
+
if (imported.cards.length === 0) {
|
|
831
938
|
fail(`No current judgment cards could be extracted from ${absKdna}.`);
|
|
832
939
|
}
|
|
833
940
|
|
|
834
941
|
return {
|
|
835
942
|
lineage,
|
|
836
|
-
|
|
837
|
-
|
|
943
|
+
...imported,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function importFromRuntimeContent(manifest, payload) {
|
|
948
|
+
if (payload?.profile !== 'kdna.payload.judgment') {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`unsupported payload profile "${payload?.profile || '(none)'}"`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
resetImportedIds();
|
|
954
|
+
const cards = cardsFromPayload(payload);
|
|
955
|
+
const runtimeBoundaries = Array.isArray(payload.core?.boundaries)
|
|
956
|
+
? payload.core.boundaries
|
|
957
|
+
: [];
|
|
958
|
+
const taskScopes = Array.from(new Set(
|
|
959
|
+
runtimeBoundaries
|
|
960
|
+
.map((boundary) => boundary?.scope)
|
|
961
|
+
.filter((scope) => typeof scope === 'string' && scope.trim().length > 0),
|
|
962
|
+
));
|
|
963
|
+
const excludedAreas = runtimeBoundaries
|
|
964
|
+
.map((boundary) => boundary?.out_of_scope)
|
|
965
|
+
.filter((area) => typeof area === 'string' && area.trim().length > 0);
|
|
966
|
+
return {
|
|
967
|
+
cards,
|
|
968
|
+
judgment_core: judgmentCoreFromRuntimePayload(payload),
|
|
969
|
+
distillation_target: {
|
|
970
|
+
task_scope: taskScopes.length === 1 ? taskScopes[0] : null,
|
|
971
|
+
include_areas: taskScopes,
|
|
972
|
+
exclude_areas: excludedAreas,
|
|
973
|
+
load_condition:
|
|
974
|
+
typeof payload.core?.load_condition === 'string'
|
|
975
|
+
? payload.core.load_condition
|
|
976
|
+
: null,
|
|
977
|
+
},
|
|
838
978
|
source_manifest: manifest,
|
|
839
979
|
source_patterns: null,
|
|
840
980
|
source_reasoning: payload.reasoning && typeof payload.reasoning === 'object'
|
|
@@ -849,9 +989,23 @@ function importFromKdna(kdnaPath) {
|
|
|
849
989
|
};
|
|
850
990
|
}
|
|
851
991
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
992
|
+
function importFromRuntimeCapsule(capsule) {
|
|
993
|
+
if (capsule?.type !== 'kdna.runtime-capsule' || capsule.profile !== 'full') {
|
|
994
|
+
throw new Error('A full Runtime Capsule is required for Studio re-import.');
|
|
995
|
+
}
|
|
996
|
+
const manifest = capsule.context?.manifest;
|
|
997
|
+
const payload = capsule.context?.payload;
|
|
998
|
+
if (!manifest || !payload) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
'The Runtime Capsule does not contain current manifest and payload semantics.',
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const imported = importFromRuntimeContent(manifest, payload);
|
|
1004
|
+
if (imported.cards.length === 0) {
|
|
1005
|
+
throw new Error('No current judgment cards could be re-imported.');
|
|
1006
|
+
}
|
|
1007
|
+
return imported;
|
|
1008
|
+
}
|
|
855
1009
|
|
|
856
1010
|
const LEGACY_PATTERN_TYPE_MAP = {
|
|
857
1011
|
term: 'term',
|
|
@@ -907,28 +1061,6 @@ function validateZipContainer(entries) {
|
|
|
907
1061
|
}
|
|
908
1062
|
}
|
|
909
1063
|
|
|
910
|
-
function validateLegacyImportContract(manifest, payload) {
|
|
911
|
-
// Frozen validation for judgment-profile-v1 assets.
|
|
912
|
-
if (!manifest || typeof manifest !== 'object') fail('Legacy asset manifest is missing or invalid');
|
|
913
|
-
if (manifest.kdna_version !== '1.0') fail(`Legacy asset must declare kdna_version "1.0", got "${manifest.kdna_version}"`);
|
|
914
|
-
if (!manifest.asset_id || typeof manifest.asset_id !== 'string') fail('Legacy asset_id is missing');
|
|
915
|
-
if (!manifest.compatibility || manifest.compatibility.profile !== LEGACY_PROFILE) {
|
|
916
|
-
fail(`Legacy asset compatibility.profile must be "${LEGACY_PROFILE}"`);
|
|
917
|
-
}
|
|
918
|
-
if (!manifest.payload || manifest.payload.path !== 'payload.kdnab') {
|
|
919
|
-
fail('Legacy asset payload.path must be "payload.kdnab"');
|
|
920
|
-
}
|
|
921
|
-
if (manifest.payload.encoding !== 'cbor') {
|
|
922
|
-
fail('Legacy asset payload.encoding must be "cbor"');
|
|
923
|
-
}
|
|
924
|
-
if (manifest.payload.encrypted) fail('Legacy asset must not be encrypted');
|
|
925
|
-
if (!payload || typeof payload !== 'object') fail('Legacy asset payload is missing or invalid');
|
|
926
|
-
if (payload.profile !== LEGACY_PROFILE) fail('Legacy payload must declare profile "judgment-profile-v1"');
|
|
927
|
-
if (!payload.core || typeof payload.core !== 'object') fail('Legacy payload must contain a "core" object');
|
|
928
|
-
if (!Array.isArray(payload.core.axioms)) fail('Legacy payload must contain axioms');
|
|
929
|
-
if (!payload.patterns || !Array.isArray(payload.patterns)) fail('Legacy payload must contain patterns');
|
|
930
|
-
}
|
|
931
|
-
|
|
932
1064
|
function judgmentCoreFromLegacyPayload(payload) {
|
|
933
1065
|
const core = payload.core || {};
|
|
934
1066
|
const result = {};
|
|
@@ -1791,7 +1923,7 @@ function cmdCard(args) {
|
|
|
1791
1923
|
const passphrase = resolvePassphrase(args);
|
|
1792
1924
|
try {
|
|
1793
1925
|
if (identity.encrypted && !passphrase) {
|
|
1794
|
-
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin
|
|
1926
|
+
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin`);
|
|
1795
1927
|
return null;
|
|
1796
1928
|
}
|
|
1797
1929
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
@@ -1912,7 +2044,7 @@ function cmdMigrate(args) {
|
|
|
1912
2044
|
const requestedName = option(args, '--name');
|
|
1913
2045
|
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1914
2046
|
if (!sourceDir || (!out && !checkOnly) || (!by && !checkOnly) || (!statement && !checkOnly)) {
|
|
1915
|
-
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--check] [--sign] [--passphrase
|
|
2047
|
+
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--check] [--sign] [--passphrase-stdin]');
|
|
1916
2048
|
}
|
|
1917
2049
|
|
|
1918
2050
|
const sourcePath = path.resolve(sourceDir);
|
|
@@ -1932,7 +2064,16 @@ function cmdMigrate(args) {
|
|
|
1932
2064
|
try {
|
|
1933
2065
|
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1934
2066
|
let creatorIdentity = null;
|
|
1935
|
-
|
|
2067
|
+
if (args.includes('--use-local-identity')) {
|
|
2068
|
+
try {
|
|
2069
|
+
creatorIdentity = creatorApi.loadIdentity();
|
|
2070
|
+
} catch {
|
|
2071
|
+
fail(
|
|
2072
|
+
'--use-local-identity was requested, but no usable local identity is available.',
|
|
2073
|
+
EXIT.TRUST_FAILED,
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1936
2077
|
if (isStudioProject) {
|
|
1937
2078
|
const loaded = readProject(sourcePath);
|
|
1938
2079
|
project = loaded.project;
|
|
@@ -2143,20 +2284,21 @@ function cmdExport(args) {
|
|
|
2143
2284
|
fail('The --format option is not supported. KDNA has one current asset format.');
|
|
2144
2285
|
}
|
|
2145
2286
|
if (args.includes('--sign')) {
|
|
2146
|
-
fail('Asset
|
|
2287
|
+
fail('Asset signatures are outside the current Preview contract.');
|
|
2147
2288
|
}
|
|
2148
2289
|
{
|
|
2149
2290
|
const { project } = readProject(projectInput);
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2291
|
+
if (
|
|
2292
|
+
args.includes('--password') ||
|
|
2293
|
+
args.some((arg) => arg.startsWith('--password=')) ||
|
|
2294
|
+
args.includes('--passphrase') ||
|
|
2295
|
+
args.some((arg) => arg.startsWith('--passphrase='))
|
|
2296
|
+
) {
|
|
2297
|
+
fail(
|
|
2298
|
+
'Password and passphrase values in process arguments are not supported. ' +
|
|
2299
|
+
'Use --password-stdin.'
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2160
2302
|
const useStdin = args.includes('--password-stdin');
|
|
2161
2303
|
let password;
|
|
2162
2304
|
if (useStdin) {
|
|
@@ -2166,25 +2308,15 @@ function cmdExport(args) {
|
|
|
2166
2308
|
if (process.stdin.isTTY) {
|
|
2167
2309
|
fail(
|
|
2168
2310
|
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
2169
|
-
'Example:
|
|
2311
|
+
'Example: printf "%s\\n" "$KDNA_PASSWORD" | kdna-studio export <project> --out <file.kdna> --password-stdin\n' +
|
|
2170
2312
|
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
2171
2313
|
);
|
|
2172
2314
|
}
|
|
2173
2315
|
try {
|
|
2174
|
-
password =
|
|
2316
|
+
password = readBoundedSecretStdin('Password');
|
|
2175
2317
|
} catch (e) {
|
|
2176
2318
|
fail(`Could not read password from stdin: ${e.message}`);
|
|
2177
2319
|
}
|
|
2178
|
-
} else {
|
|
2179
|
-
// Bug (#55): prior version only consulted --password / --passphrase
|
|
2180
|
-
// here, so a caller who set KDNA_PASSPHRASE for the signing path
|
|
2181
|
-
// (works via resolvePassphrase) found that the runtime encryption
|
|
2182
|
-
// password was silently null. Resolve from the same sources as
|
|
2183
|
-
// the signing path so the two stay in sync.
|
|
2184
|
-
password = process.env.KDNA_PASSPHRASE
|
|
2185
|
-
|| process.env.KDNA_PASSWORD
|
|
2186
|
-
|| option(args, '--password')
|
|
2187
|
-
|| option(args, '--passphrase');
|
|
2188
2320
|
}
|
|
2189
2321
|
exportProject(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
2190
2322
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
@@ -2200,7 +2332,9 @@ function cmdStudioInstall(args) {
|
|
|
2200
2332
|
if (!target) fail('Usage: kdna-studio install <@scope/name|file.kdna>');
|
|
2201
2333
|
try {
|
|
2202
2334
|
const kdnaArgs = ['install', target, '--yes'];
|
|
2203
|
-
if (args.includes('--trusted'))
|
|
2335
|
+
if (args.includes('--trusted')) {
|
|
2336
|
+
fail('Asset signatures are outside the current Preview contract.');
|
|
2337
|
+
}
|
|
2204
2338
|
const result = require('child_process').spawnSync('kdna', kdnaArgs, {
|
|
2205
2339
|
stdio: 'inherit', encoding: 'utf8',
|
|
2206
2340
|
});
|
|
@@ -2254,6 +2388,77 @@ function cmdLlm(args) {
|
|
|
2254
2388
|
fail('Usage: kdna-studio llm <config|show>');
|
|
2255
2389
|
}
|
|
2256
2390
|
|
|
2391
|
+
// initIdentity fails for several distinct reasons and each needs a different
|
|
2392
|
+
// user message. A catch-all "Identity already exists." would lie about path
|
|
2393
|
+
// (ENOTDIR/ENOENT), permission (EACCES/EPERM), write, and KDF failures and
|
|
2394
|
+
// would send the user to `identity show`, which then reports no identity.
|
|
2395
|
+
function failIdentityInit(err) {
|
|
2396
|
+
const code = err && err.code;
|
|
2397
|
+
// Identity state is classified only by Studio Core's stable machine codes
|
|
2398
|
+
// (or by stable OS error codes). Human-readable error text is diagnostic
|
|
2399
|
+
// material, never a state oracle.
|
|
2400
|
+
if (code === 'IDENTITY_ALREADY_EXISTS') {
|
|
2401
|
+
fail(
|
|
2402
|
+
`Identity already exists [${code}] and passed consistency verification. `
|
|
2403
|
+
+ "Use 'kdna-studio identity show' to view it."
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
if (code === 'IDENTITY_INCOMPLETE') {
|
|
2407
|
+
fail(
|
|
2408
|
+
`Identity is incomplete [${code}]: only part of the canonical three-file identity exists. `
|
|
2409
|
+
+ 'No existing file was changed or removed. Preserve the files and recover the identity manually.'
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
if (code === 'IDENTITY_CORRUPT') {
|
|
2413
|
+
fail(
|
|
2414
|
+
`Identity is corrupt [${code}]: all canonical files exist but failed consistency verification. `
|
|
2415
|
+
+ 'Do not sign with or use this identity. Preserve the files and recover from a trusted backup.'
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
if (code === 'IDENTITY_COMMITTED_DURABILITY_UNCONFIRMED'
|
|
2419
|
+
&& err.committed === true && err.identityVerified === true) {
|
|
2420
|
+
fail(
|
|
2421
|
+
`Identity committed, durability unconfirmed [${code}]: your identity was created, `
|
|
2422
|
+
+ 'but confirming it is fully durable on disk failed.\n'
|
|
2423
|
+
+ 'Do NOT re-run `kdna-studio identity init`.\n'
|
|
2424
|
+
+ ' 1. Verify the identity with: kdna-studio identity show\n'
|
|
2425
|
+
+ ' 2. Back up your private key promptly.'
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (code === 'IDENTITY_COMMITTED_INCONSISTENT') {
|
|
2429
|
+
fail(
|
|
2430
|
+
`Identity committed but inconsistent [${code}]: files reached the canonical identity directory, `
|
|
2431
|
+
+ 'but they did not pass post-commit consistency verification.\n'
|
|
2432
|
+
+ 'Do NOT sign with, show as valid, delete, or automatically retry this identity.\n'
|
|
2433
|
+
+ 'Preserve and restrict access to the directory, then recover from a trusted backup or have an '
|
|
2434
|
+
+ 'administrator inspect all three files.'
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
if (code === 'IDENTITY_COMMITTED_DURABILITY_UNCONFIRMED') {
|
|
2438
|
+
fail(
|
|
2439
|
+
'Identity init returned an invalid committed-state result '
|
|
2440
|
+
+ `[${code}]: verified identity evidence is missing. Do not retry or use the identity; recover manually.`
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
if (code === 'ENOTDIR' || code === 'ENOENT') {
|
|
2444
|
+
fail(`Identity init failed — the identity path is not usable [${code}].`);
|
|
2445
|
+
}
|
|
2446
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
2447
|
+
fail(`Identity init failed — permission denied [${code}].`);
|
|
2448
|
+
}
|
|
2449
|
+
if (code === 'ENOSPC') {
|
|
2450
|
+
fail(`Identity init failed — storage is full [${code}]. No identity was committed.`);
|
|
2451
|
+
}
|
|
2452
|
+
if (code === 'EIO') {
|
|
2453
|
+
fail(`Identity init failed — storage I/O failed [${code}]. Do not assume an identity was created.`);
|
|
2454
|
+
}
|
|
2455
|
+
if (code === 'IDENTITY_KDF_FAILED') {
|
|
2456
|
+
fail(`Identity init failed — key derivation failed [${code}]. No identity was committed.`);
|
|
2457
|
+
}
|
|
2458
|
+
const displayCode = typeof code === 'string' && /^[A-Z0-9_]+$/.test(code) ? code : 'UNKNOWN';
|
|
2459
|
+
fail(`Identity init failed [${displayCode}]. No recognized identity state was reported.`);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2257
2462
|
function cmdIdentity(args) {
|
|
2258
2463
|
const sub = args[0];
|
|
2259
2464
|
if (sub === 'init') {
|
|
@@ -2272,7 +2477,7 @@ function cmdIdentity(args) {
|
|
|
2272
2477
|
console.log(` Private key is encrypted — keep your passphrase safe.`);
|
|
2273
2478
|
}
|
|
2274
2479
|
} catch (err) {
|
|
2275
|
-
|
|
2480
|
+
failIdentityInit(err);
|
|
2276
2481
|
}
|
|
2277
2482
|
return;
|
|
2278
2483
|
}
|
|
@@ -2290,22 +2495,14 @@ function cmdIdentity(args) {
|
|
|
2290
2495
|
fail('Usage: kdna-studio identity <init|show>');
|
|
2291
2496
|
}
|
|
2292
2497
|
|
|
2293
|
-
function cmdReport(args) {
|
|
2294
|
-
const { project } = readProject(args[0]);
|
|
2295
|
-
const readiness = quality.computeReadiness(project);
|
|
2296
|
-
const gate = projectApi.checkHumanLockGate(project);
|
|
2297
|
-
console.log(JSON.stringify({ readiness, human_lock_gate: gate }, null, 2));
|
|
2298
|
-
process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
2498
|
// Audit-locks: list every card that would fail the current Human Lock
|
|
2302
2499
|
// gate because of missing or empty fields. Used during the
|
|
2303
2500
|
// pre-migration review of source-tree assets (e.g. pro-20) so an
|
|
2304
2501
|
// author can fill in the missing confidence / evidence_type / risk
|
|
2305
2502
|
// description / stance statement BEFORE running `migrate`.
|
|
2306
2503
|
//
|
|
2307
|
-
//
|
|
2308
|
-
//
|
|
2504
|
+
// The earlier readiness output returned a flat human_lock_gate.issues list,
|
|
2505
|
+
// which mixed card
|
|
2309
2506
|
// types and field names. An author migrating 20 source-tree
|
|
2310
2507
|
// assets with 99 axioms + 140 risk cards + 70 stance cards had no
|
|
2311
2508
|
// way to see, for a single asset, "which axioms are missing
|
|
@@ -2567,6 +2764,16 @@ async function cmdDistill(args) {
|
|
|
2567
2764
|
suggested_card_type: c.type || c.suggested_card_type || 'axiom',
|
|
2568
2765
|
supporting_evidence_ids: (c.evidence_ids || c.evidenceIds || c.supporting_evidence_ids || []).map(String),
|
|
2569
2766
|
confidence: c.confidence || 'medium',
|
|
2767
|
+
rationale: c.rationale || c.why || '',
|
|
2768
|
+
applies_when: Array.isArray(c.applies_when) ? c.applies_when.map(String) : [],
|
|
2769
|
+
does_not_apply_when: Array.isArray(c.does_not_apply_when)
|
|
2770
|
+
? c.does_not_apply_when.map(String)
|
|
2771
|
+
: [],
|
|
2772
|
+
misuse_risk: c.misuse_risk || c.failure_risk || '',
|
|
2773
|
+
agent_inference: c.agent_inference === true,
|
|
2774
|
+
fields: c.fields && typeof c.fields === 'object' && !Array.isArray(c.fields)
|
|
2775
|
+
? JSON.parse(JSON.stringify(c.fields))
|
|
2776
|
+
: {},
|
|
2570
2777
|
candidate_status: 'proposed',
|
|
2571
2778
|
scope_fit: c.scope_fit ?? c.scopeFit ?? true,
|
|
2572
2779
|
};
|
|
@@ -2625,36 +2832,68 @@ function cmdCandidate(args) {
|
|
|
2625
2832
|
}
|
|
2626
2833
|
|
|
2627
2834
|
if (sub === 'promote') {
|
|
2628
|
-
const accepted = candidates.filter(
|
|
2835
|
+
const accepted = candidates.filter(
|
|
2836
|
+
c =>
|
|
2837
|
+
c.candidate_status === 'accepted' &&
|
|
2838
|
+
c.scope_fit !== false &&
|
|
2839
|
+
!c.promoted_card_id,
|
|
2840
|
+
);
|
|
2629
2841
|
if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
|
|
2630
2842
|
|
|
2843
|
+
const incomplete = [];
|
|
2844
|
+
for (const candidate of accepted) {
|
|
2845
|
+
const missing = [];
|
|
2846
|
+
if (!String(candidate.one_sentence || '').trim()) missing.push('one_sentence');
|
|
2847
|
+
if (!String(candidate.full_statement || '').trim()) missing.push('full_statement');
|
|
2848
|
+
if (!String(candidate.rationale || candidate.why || '').trim()) missing.push('rationale');
|
|
2849
|
+
if (!Array.isArray(candidate.applies_when) || candidate.applies_when.length === 0) {
|
|
2850
|
+
missing.push('applies_when');
|
|
2851
|
+
}
|
|
2852
|
+
if (
|
|
2853
|
+
!Array.isArray(candidate.does_not_apply_when) ||
|
|
2854
|
+
candidate.does_not_apply_when.length === 0
|
|
2855
|
+
) {
|
|
2856
|
+
missing.push('does_not_apply_when');
|
|
2857
|
+
}
|
|
2858
|
+
if (!String(candidate.misuse_risk || candidate.failure_risk || '').trim()) {
|
|
2859
|
+
missing.push('misuse_risk');
|
|
2860
|
+
}
|
|
2861
|
+
if (missing.length > 0) incomplete.push(`${candidate.id}: ${missing.join(', ')}`);
|
|
2862
|
+
}
|
|
2863
|
+
if (incomplete.length > 0) {
|
|
2864
|
+
fail(
|
|
2865
|
+
`Accepted candidates are incomplete and were not promoted:\n - ${incomplete.join(
|
|
2866
|
+
'\n - ',
|
|
2867
|
+
)}\nRefine or re-distill them before promotion.`,
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2631
2871
|
for (const c of accepted) {
|
|
2632
|
-
//
|
|
2633
|
-
//
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2872
|
+
// Candidate review is the place to resolve missing semantics. Promotion
|
|
2873
|
+
// preserves the reviewed fields and never fabricates placeholder text.
|
|
2874
|
+
const fields = {
|
|
2875
|
+
...(c.fields && typeof c.fields === 'object' ? c.fields : {}),
|
|
2876
|
+
one_sentence: c.one_sentence,
|
|
2877
|
+
full_statement: c.full_statement,
|
|
2878
|
+
rationale: c.rationale || c.why,
|
|
2879
|
+
applies_when: c.applies_when,
|
|
2880
|
+
does_not_apply_when: c.does_not_apply_when,
|
|
2881
|
+
misuse_risk: c.misuse_risk || c.failure_risk,
|
|
2882
|
+
confidence: c.confidence || 'unknown',
|
|
2883
|
+
evidence_type: c.agent_inference === true ? 'analogy' : 'practice',
|
|
2884
|
+
agent_inference: c.agent_inference === true,
|
|
2885
|
+
};
|
|
2640
2886
|
if (c.suggested_card_type === 'axiom') {
|
|
2641
|
-
fields.
|
|
2642
|
-
fields.
|
|
2643
|
-
fields.why = c.why || '<TBD: why>';
|
|
2644
|
-
// applies_when / does_not_apply_when are arrays, not strings.
|
|
2645
|
-
// Empty string is invalid; emit an empty array so checkRequiredFields
|
|
2646
|
-
// (which treats `length === 0` as missing) correctly reports them.
|
|
2647
|
-
fields.applies_when = Array.isArray(c.applies_when) ? c.applies_when : [];
|
|
2648
|
-
fields.does_not_apply_when = Array.isArray(c.does_not_apply_when) ? c.does_not_apply_when : [];
|
|
2649
|
-
fields.failure_risk = c.failure_risk || '<TBD: failure_risk>';
|
|
2650
|
-
} else {
|
|
2651
|
-
fields.one_sentence = c.one_sentence || '';
|
|
2652
|
-
fields.full_statement = c.full_statement || c.one_sentence || '';
|
|
2887
|
+
fields.why = c.rationale || c.why;
|
|
2888
|
+
fields.failure_risk = c.misuse_risk || c.failure_risk;
|
|
2653
2889
|
}
|
|
2654
2890
|
const card = cardApi.createCard(c.suggested_card_type, fields);
|
|
2655
|
-
card.evidence_refs = c.supporting_evidence_ids
|
|
2891
|
+
card.evidence_refs = Array.isArray(c.supporting_evidence_ids)
|
|
2892
|
+
? c.supporting_evidence_ids
|
|
2893
|
+
: [];
|
|
2656
2894
|
project.cards = project.cards || [];
|
|
2657
2895
|
project.cards.push(card);
|
|
2896
|
+
c.promoted_card_id = card.id;
|
|
2658
2897
|
}
|
|
2659
2898
|
if (project.stages?.judgment_cards) {
|
|
2660
2899
|
project.stages.judgment_cards.total = project.cards.length;
|
|
@@ -2672,175 +2911,46 @@ async function cmdInterview(args) {
|
|
|
2672
2911
|
const projectInput = args[0];
|
|
2673
2912
|
const stage = option(args, '--stage');
|
|
2674
2913
|
if (!projectInput) fail('Usage: kdna-studio interview <project> [--stage distill|clarify|correct|replay]');
|
|
2675
|
-
const { project } = readProject(projectInput);
|
|
2914
|
+
const { projectPath, project } = readProject(projectInput);
|
|
2915
|
+
let results;
|
|
2676
2916
|
|
|
2677
2917
|
if (stage) {
|
|
2678
|
-
const
|
|
2918
|
+
const normalizedStage = ai.interview.normalizeInterviewStage(stage);
|
|
2919
|
+
const result = await ai.interview.runInterview(
|
|
2920
|
+
llm.config(),
|
|
2921
|
+
project,
|
|
2922
|
+
normalizedStage,
|
|
2923
|
+
'',
|
|
2924
|
+
{},
|
|
2925
|
+
);
|
|
2926
|
+
results = [result];
|
|
2679
2927
|
console.log(`\n=== ${result.label} ===\n${result.content}`);
|
|
2680
2928
|
} else {
|
|
2681
|
-
await ai.interview.runInterviewInteractive(llm.config(), project, {});
|
|
2929
|
+
results = await ai.interview.runInterviewInteractive(llm.config(), project, {});
|
|
2682
2930
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
// prepared to auto-synthesise one. A caller without a configured
|
|
2703
|
-
// LLM (no `KDNA_API_KEY`, no `kdna-studio llm config`) would
|
|
2704
|
-
// get a `fail` instead of a structured note. The fix removes the
|
|
2705
|
-
// guard: a missing restatement is now handed off to the evaluator
|
|
2706
|
-
// (which either asks the LLM to evaluate a synthesised one or,
|
|
2707
|
-
// without an LLM, returns a structured `synthesised_restatement`
|
|
2708
|
-
// for the caller to use as a starting point). The error message
|
|
2709
|
-
// is kept as a secondary path: only if the evaluator itself fails
|
|
2710
|
-
// does the command exit non-zero, and only then with a clear
|
|
2711
|
-
// pointer to the manual `card update` path.
|
|
2712
|
-
//
|
|
2713
|
-
// We do still need a restatement-shaped object to feed the
|
|
2714
|
-
// evaluator, so we synthesise one in-memory when the card has
|
|
2715
|
-
// none. This does NOT persist the synthesised restatement to the
|
|
2716
|
-
// project (so the user still has to opt in via `card update` to
|
|
2717
|
-
// keep one across runs), but it lets the no-LLM path return a
|
|
2718
|
-
// useful result instead of a hard fail.
|
|
2719
|
-
if (!card.feynman_restatement) {
|
|
2720
|
-
const synthesised = ai.feynman.synthFeynmanRestatement(card.fields || {});
|
|
2721
|
-
card.feynman_restatement = { text: synthesised, synthesised: true };
|
|
2722
|
-
console.warn(`No feynman_restatement on ${cardId}; using synthesised:\n "${synthesised}"`);
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
if (!useJson) console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2726
|
-
|
|
2727
|
-
// Bug (#1 UX follow-up): if no LLM is configured and the caller
|
|
2728
|
-
// did not pass --no-llm, route through cmdNeedsLlm which either
|
|
2729
|
-
// gives a clear "configure LLM" error or accepts the --no-llm
|
|
2730
|
-
// path. Without this branch, the no-LLM caller would fall
|
|
2731
|
-
// through to the LLM call below and get a stack trace.
|
|
2732
|
-
const llmGate = cmdNeedsLlm(args, 'feynmann');
|
|
2733
|
-
|
|
2734
|
-
let result;
|
|
2735
|
-
if (llmGate.noLlm) {
|
|
2736
|
-
// Static evaluation: every criterion is "unsure" (✗), score 0,
|
|
2737
|
-
// but the command still produces a useful structured result and
|
|
2738
|
-
// saves the synthesised restatement to the card.
|
|
2739
|
-
result = {
|
|
2740
|
-
score: 0,
|
|
2741
|
-
criteria: Object.fromEntries(Object.keys(ai.feynman.CRITERIA).map(k => [k, false])),
|
|
2742
|
-
explanations: { noLlm: llmGate.message },
|
|
2743
|
-
suggestions: [
|
|
2744
|
-
'No LLM configured — every criterion shows ✗. Run `kdna-studio llm config` and re-evaluate for a real score.',
|
|
2745
|
-
'Or set the synthesised restatement via: kdna-studio card update <project> ' + cardId + ' --field feynman_restatement=\'{"text":"<your own restatement>"}\'',
|
|
2746
|
-
],
|
|
2747
|
-
};
|
|
2748
|
-
} else {
|
|
2749
|
-
result = await ai.feynman.evaluate(llmGate.cfg, card, {});
|
|
2750
|
-
}
|
|
2751
|
-
const score = result.score || 0;
|
|
2752
|
-
const passed = score >= 4;
|
|
2753
|
-
|
|
2754
|
-
if (useJson) {
|
|
2755
|
-
console.log(JSON.stringify({
|
|
2756
|
-
card_id: cardId,
|
|
2757
|
-
score,
|
|
2758
|
-
passed,
|
|
2759
|
-
no_llm: !!llmGate.noLlm,
|
|
2760
|
-
criteria: result.criteria || {},
|
|
2761
|
-
suggestions: result.suggestions || [],
|
|
2762
|
-
feynman_restatement: card.feynman_restatement,
|
|
2763
|
-
}, null, 2));
|
|
2764
|
-
} else {
|
|
2765
|
-
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
|
|
2766
|
-
for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
|
|
2767
|
-
const r = (result.criteria || {})[criterion] ? '✓' : '✗';
|
|
2768
|
-
console.log(` ${r} ${criterion}`);
|
|
2769
|
-
}
|
|
2770
|
-
if (result.suggestions && result.suggestions.length > 0) {
|
|
2771
|
-
console.log('\nSuggestions:');
|
|
2772
|
-
result.suggestions.forEach(s => console.log(` - ${s}`));
|
|
2773
|
-
}
|
|
2931
|
+
project.interview_sessions = Array.isArray(project.interview_sessions)
|
|
2932
|
+
? project.interview_sessions
|
|
2933
|
+
: [];
|
|
2934
|
+
project.interview_sessions.push({
|
|
2935
|
+
recorded_at: new Date().toISOString(),
|
|
2936
|
+
results,
|
|
2937
|
+
});
|
|
2938
|
+
if (project.stages?.interview_room) {
|
|
2939
|
+
project.stages.interview_room.status = 'in_progress';
|
|
2940
|
+
project.stages.interview_room.questions_asked =
|
|
2941
|
+
Number(project.stages.interview_room.questions_asked || 0) +
|
|
2942
|
+
results.reduce(
|
|
2943
|
+
(total, result) =>
|
|
2944
|
+
total +
|
|
2945
|
+
(Array.isArray(result.conversation)
|
|
2946
|
+
? result.conversation.filter(message => message.role === 'user').length
|
|
2947
|
+
: 1),
|
|
2948
|
+
0,
|
|
2949
|
+
);
|
|
2774
2950
|
}
|
|
2775
|
-
|
|
2776
|
-
card.feynman_evaluation = { score, criteria: result.criteria, suggestions: result.suggestions, evaluated_at: new Date().toISOString() };
|
|
2777
|
-
project.cards[cardIdx] = card;
|
|
2778
2951
|
writeProject(projectPath, project);
|
|
2779
2952
|
}
|
|
2780
2953
|
|
|
2781
|
-
async function cmdTest(args) {
|
|
2782
|
-
const projectInput = args[0];
|
|
2783
|
-
const input = option(args, '--input');
|
|
2784
|
-
const preset = option(args, '--preset', 'baseline');
|
|
2785
|
-
if (!projectInput || !input) fail('Usage: kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]');
|
|
2786
|
-
const { project } = readProject(projectInput);
|
|
2787
|
-
const domainName = project.name || 'Untitled Project';
|
|
2788
|
-
const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
|
|
2789
|
-
|
|
2790
|
-
// Bug (#50): prior version hand-rolled the prompt for the 3 preset
|
|
2791
|
-
// names and bypassed ai.testlab.testPreset entirely. The exported
|
|
2792
|
-
// testPreset function was dead code, and the help text advertised
|
|
2793
|
-
// names (baseline / edge / contradiction) that did not match the
|
|
2794
|
-
// actual preset keys in testlab.js (baseline / edge_case /
|
|
2795
|
-
// contradiction). Result: --preset edge silently ran the
|
|
2796
|
-
// contradiction branch, and any caller that asked for "edge_case"
|
|
2797
|
-
// fell through to the contradiction branch too.
|
|
2798
|
-
//
|
|
2799
|
-
// The fix routes the call through testPreset when a known preset is
|
|
2800
|
-
// given, and accepts both help-text names (baseline / edge /
|
|
2801
|
-
// contradiction) and the canonical testlab names (baseline / edge_case
|
|
2802
|
-
// / contradiction).
|
|
2803
|
-
const PRESET_ALIASES = {
|
|
2804
|
-
baseline: 'baseline',
|
|
2805
|
-
edge: 'edge_case',
|
|
2806
|
-
edge_case: 'edge_case',
|
|
2807
|
-
contradiction: 'contradiction',
|
|
2808
|
-
};
|
|
2809
|
-
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2810
|
-
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2811
|
-
|
|
2812
|
-
// Bug (#1 UX): gate LLM-requiring paths through cmdNeedsLlm.
|
|
2813
|
-
// The test command always needs an LLM (it compares without/with
|
|
2814
|
-
// the loaded domain). --no-llm gives a static "no-op" comparison
|
|
2815
|
-
// that lists the cards that *would* be applied.
|
|
2816
|
-
const llmGate = cmdNeedsLlm(args, 'test');
|
|
2817
|
-
const cfg = llmGate.noLlm ? null : llmGate.cfg;
|
|
2818
|
-
|
|
2819
|
-
if (canonical === 'baseline') {
|
|
2820
|
-
// Baseline is a single comparison: keep the prior single-result
|
|
2821
|
-
// shape so existing parsers / consumers see the same output.
|
|
2822
|
-
const prompt = `Test the core judgment: ${input}`;
|
|
2823
|
-
if (cfg === null) {
|
|
2824
|
-
// No-LLM static result
|
|
2825
|
-
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 || '' }));
|
|
2826
|
-
console.log(JSON.stringify({ no_llm: true, message: llmGate.message, would_apply: cards }, null, 2));
|
|
2827
|
-
return;
|
|
2828
|
-
}
|
|
2829
|
-
const result = await ai.testlab.compare(cfg, domainName, prompt, domainPrompt, {});
|
|
2830
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
// For non-baseline presets, run the preset + the baseline so the user
|
|
2834
|
-
// can see both side by side. The preset prompt is the one in
|
|
2835
|
-
// ai/testlab.js — that's the code path that was dead before.
|
|
2836
|
-
if (cfg === null) {
|
|
2837
|
-
console.log(JSON.stringify({ no_llm: true, message: llmGate.message }, null, 2));
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
const result = await ai.testlab.testPreset(cfg, domainName, input, domainPrompt, {});
|
|
2841
|
-
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
2954
|
const args = process.argv.slice(2);
|
|
2845
2955
|
const cmd = args[0];
|
|
2846
2956
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
@@ -2854,7 +2964,23 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
2854
2964
|
|
|
2855
2965
|
(async () => {
|
|
2856
2966
|
try {
|
|
2857
|
-
if (
|
|
2967
|
+
if (CREATION_COMMANDS.has(cmd)) {
|
|
2968
|
+
const runtimeCore = require('@aikdna/kdna-core');
|
|
2969
|
+
await executeCreationCommand(cmd, args.slice(1), {
|
|
2970
|
+
creationEngine,
|
|
2971
|
+
runtimeCore,
|
|
2972
|
+
exportRuntime: creationExportRuntime || exportRuntime,
|
|
2973
|
+
reimportCapsule: importFromRuntimeCapsule,
|
|
2974
|
+
cliVersion:
|
|
2975
|
+
`@aikdna/kdna-studio-cli@${require('../package.json').version}`,
|
|
2976
|
+
studioCoreVersion:
|
|
2977
|
+
creationStudioCoreCoordinate ||
|
|
2978
|
+
`@aikdna/kdna-studio-core@${require('@aikdna/kdna-studio-core/package.json').version}`,
|
|
2979
|
+
runtimeCoreVersion:
|
|
2980
|
+
`@aikdna/kdna-core@${require('@aikdna/kdna-core/package.json').version}`,
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
else if (cmd === 'create') cmdCreate(args.slice(1));
|
|
2858
2984
|
else if (cmd === 'migrate') cmdMigrate(args.slice(1));
|
|
2859
2985
|
else if (cmd === 'import') cmdImport(args.slice(1));
|
|
2860
2986
|
else if (cmd === 'filter') cmdFilter(args.slice(1));
|
|
@@ -2863,15 +2989,12 @@ try {
|
|
|
2863
2989
|
else if (cmd === 'distill') await cmdDistill(args.slice(1));
|
|
2864
2990
|
else if (cmd === 'candidate') cmdCandidate(args.slice(1));
|
|
2865
2991
|
else if (cmd === 'interview') await cmdInterview(args.slice(1));
|
|
2866
|
-
else if (cmd === 'feynman') await cmdFeynman(args.slice(1));
|
|
2867
|
-
else if (cmd === 'test') await cmdTest(args.slice(1));
|
|
2868
2992
|
else if (cmd === 'card') cmdCard(args.slice(1));
|
|
2869
2993
|
else if (cmd === 'lock') cmdLock(args.slice(1));
|
|
2870
2994
|
else if (cmd === 'compile') cmdCompile(args.slice(1));
|
|
2871
2995
|
else if (cmd === 'export') cmdExport(args.slice(1));
|
|
2872
2996
|
else if (cmd === 'llm') cmdLlm(args.slice(1));
|
|
2873
2997
|
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
2874
|
-
else if (cmd === 'report') cmdReport(args.slice(1));
|
|
2875
2998
|
else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
|
|
2876
2999
|
else if (cmd === 'install') cmdStudioInstall(args.slice(1));
|
|
2877
3000
|
else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
|