@aikdna/kdna-studio-cli 0.10.1 → 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 +151 -36
- package/bin/kdna-studio.js +809 -430
- 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
|
|
|
@@ -366,12 +473,23 @@ function cleanFields(fields = {}) {
|
|
|
366
473
|
return cleaned;
|
|
367
474
|
}
|
|
368
475
|
|
|
476
|
+
function importedFields(entry = {}, options = {}) {
|
|
477
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return {};
|
|
478
|
+
const fields = JSON.parse(JSON.stringify(entry));
|
|
479
|
+
for (const key of ['id', 'status', 'locked', 'human_lock', 'audit_log', 'source_authored']) {
|
|
480
|
+
delete fields[key];
|
|
481
|
+
}
|
|
482
|
+
if (options.keepType !== true) delete fields.type;
|
|
483
|
+
return cleanFields(fields);
|
|
484
|
+
}
|
|
485
|
+
|
|
369
486
|
function importedCard(type, fields = {}, id = null) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
487
|
+
const card = cardApi.createCard(type, cleanFields(fields), id || null);
|
|
488
|
+
if (card === null) {
|
|
489
|
+
throw new Error(`failed to create card type="${type}" id="${id || '(auto)'}" — ` +
|
|
490
|
+
`the card type may require additional required fields or the fields may be malformed`);
|
|
374
491
|
}
|
|
492
|
+
return card;
|
|
375
493
|
}
|
|
376
494
|
|
|
377
495
|
// Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
|
|
@@ -401,8 +519,10 @@ function pushImportedCard(cards, type, fields = {}, id = null) {
|
|
|
401
519
|
}
|
|
402
520
|
return;
|
|
403
521
|
}
|
|
404
|
-
if (id) _importedIds.add(id);
|
|
405
522
|
const card = importedCard(type, fields, id);
|
|
523
|
+
// Register the id only after card creation succeeds; a failed creation
|
|
524
|
+
// must not consume the id and prevent retry.
|
|
525
|
+
if (card && id) _importedIds.add(id);
|
|
406
526
|
if (card) cards.push(card);
|
|
407
527
|
}
|
|
408
528
|
|
|
@@ -438,31 +558,22 @@ function cardsFromRuntimePayload(payload) {
|
|
|
438
558
|
if (cards.length > 0) return cards;
|
|
439
559
|
|
|
440
560
|
for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
|
|
441
|
-
pushImportedCard(cards, 'axiom',
|
|
442
|
-
one_sentence: ax.one_sentence || '',
|
|
443
|
-
full_statement: ax.full_statement || '',
|
|
444
|
-
why: ax.why || '',
|
|
445
|
-
applies_when: ax.applies_when || [],
|
|
446
|
-
does_not_apply_when: ax.does_not_apply_when || [],
|
|
447
|
-
failure_risk: ax.failure_risk || '',
|
|
448
|
-
confidence: ax.confidence || '',
|
|
449
|
-
evidence_type: ax.evidence_type || '',
|
|
450
|
-
}, ax.id || null);
|
|
561
|
+
pushImportedCard(cards, 'axiom', importedFields(ax), ax.id || null);
|
|
451
562
|
}
|
|
452
563
|
for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
|
|
453
|
-
pushImportedCard(cards, 'ontology', ont, ont.id || null);
|
|
564
|
+
pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
|
|
454
565
|
}
|
|
455
566
|
for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
|
|
456
|
-
pushImportedCard(cards, 'framework', fw, fw.id || null);
|
|
567
|
+
pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
|
|
457
568
|
}
|
|
458
569
|
for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
|
|
459
|
-
pushImportedCard(cards, 'stance', stance, stance.id || null);
|
|
570
|
+
pushImportedCard(cards, 'stance', importedFields(stance), stance.id || null);
|
|
460
571
|
}
|
|
461
572
|
for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
|
|
462
|
-
pushImportedCard(cards, 'aesthetic', aesthetic, aesthetic.id || null);
|
|
573
|
+
pushImportedCard(cards, 'aesthetic', importedFields(aesthetic), aesthetic.id || null);
|
|
463
574
|
}
|
|
464
575
|
for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
|
|
465
|
-
pushImportedCard(cards, 'boundary', boundary, boundary.id || null);
|
|
576
|
+
pushImportedCard(cards, 'boundary', importedFields(boundary), boundary.id || null);
|
|
466
577
|
}
|
|
467
578
|
// risk_model may be either an array of risk items (legacy) or
|
|
468
579
|
// `{risks: [...]}` (current). Treat both shapes; ignore everything else
|
|
@@ -470,7 +581,7 @@ function cardsFromRuntimePayload(payload) {
|
|
|
470
581
|
const riskModel = payload.core?.risk_model;
|
|
471
582
|
const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
|
|
472
583
|
for (const risk of riskItems) {
|
|
473
|
-
pushImportedCard(cards, 'risk', risk, risk.id || null);
|
|
584
|
+
pushImportedCard(cards, 'risk', importedFields(risk), risk.id || null);
|
|
474
585
|
}
|
|
475
586
|
for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
|
|
476
587
|
// Bug (#146): prior version dropped any payload.patterns entry
|
|
@@ -500,19 +611,25 @@ function cardsFromRuntimePayload(payload) {
|
|
|
500
611
|
);
|
|
501
612
|
continue;
|
|
502
613
|
}
|
|
503
|
-
fields =
|
|
504
|
-
delete fields.type;
|
|
614
|
+
fields = importedFields(pattern);
|
|
505
615
|
} else {
|
|
506
|
-
|
|
507
|
-
|
|
616
|
+
fields = importedFields(pattern, { keepType: type === 'pattern' || LEGACY_PATTERN_SUBTYPES.has(type) });
|
|
617
|
+
}
|
|
618
|
+
if (LEGACY_PATTERN_SUBTYPES.has(type)) {
|
|
619
|
+
fields = {
|
|
620
|
+
...fields,
|
|
621
|
+
type,
|
|
622
|
+
legacy_subtype: fields.legacy_subtype || type,
|
|
623
|
+
};
|
|
624
|
+
type = 'pattern';
|
|
508
625
|
}
|
|
509
626
|
pushImportedCard(cards, type, fields, pattern.id || null);
|
|
510
627
|
}
|
|
511
628
|
for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
|
|
512
|
-
pushImportedCard(cards, 'scenario', scenario, scenario.id || null);
|
|
629
|
+
pushImportedCard(cards, 'scenario', importedFields(scenario), scenario.id || null);
|
|
513
630
|
}
|
|
514
631
|
for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
|
|
515
|
-
pushImportedCard(cards, 'case', item, item.id || null);
|
|
632
|
+
pushImportedCard(cards, 'case', importedFields(item), item.id || null);
|
|
516
633
|
}
|
|
517
634
|
// The current payload contract has one self-check field. Refuse to infer a
|
|
518
635
|
// parallel field name: Core validation owns format migration, while Studio
|
|
@@ -520,8 +637,8 @@ function cardsFromRuntimePayload(payload) {
|
|
|
520
637
|
const rawSelfCheck = payload.reasoning?.self_check;
|
|
521
638
|
const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
|
|
522
639
|
for (const selfCheck of selfCheckArr) {
|
|
523
|
-
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : selfCheck;
|
|
524
|
-
pushImportedCard(cards, 'self_check', fields,
|
|
640
|
+
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : importedFields(selfCheck);
|
|
641
|
+
pushImportedCard(cards, 'self_check', fields, typeof selfCheck === 'object' ? selfCheck.id || null : null);
|
|
525
642
|
}
|
|
526
643
|
for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
|
|
527
644
|
// Bug (#145 follow-up): the prior version dropped three
|
|
@@ -530,7 +647,8 @@ function cardsFromRuntimePayload(payload) {
|
|
|
530
647
|
// project that round-tripped through `migrate` lost
|
|
531
648
|
// these fields every time. The fix reads them through, matching
|
|
532
649
|
// the export-side map in kdna-studio-core's buildPayload.
|
|
533
|
-
|
|
650
|
+
const fields = {
|
|
651
|
+
...importedFields(failureMode),
|
|
534
652
|
wrong: failureMode.mode || failureMode.wrong || '',
|
|
535
653
|
correct: failureMode.correct || '',
|
|
536
654
|
key_distinction: failureMode.key_distinction || '',
|
|
@@ -538,7 +656,9 @@ function cardsFromRuntimePayload(payload) {
|
|
|
538
656
|
failure_risk: failureMode.failure_risk || '',
|
|
539
657
|
applies_when: Array.isArray(failureMode.applies_when) ? failureMode.applies_when : [],
|
|
540
658
|
does_not_apply_when: Array.isArray(failureMode.does_not_apply_when) ? failureMode.does_not_apply_when : [],
|
|
541
|
-
}
|
|
659
|
+
};
|
|
660
|
+
delete fields.mode;
|
|
661
|
+
pushImportedCard(cards, 'misunderstanding', fields, failureMode.id || null);
|
|
542
662
|
}
|
|
543
663
|
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
544
664
|
// Bug (#4 UX): compile/index.js synthesises one reasoning chain
|
|
@@ -547,7 +667,7 @@ function cardsFromRuntimePayload(payload) {
|
|
|
547
667
|
// doubled the card count on round-trip. The fix skips chains
|
|
548
668
|
// that the producer explicitly marked as synthesised.
|
|
549
669
|
if (chain.source_authored === false) continue;
|
|
550
|
-
pushImportedCard(cards, 'reasoning', chain, chain.id || null);
|
|
670
|
+
pushImportedCard(cards, 'reasoning', importedFields(chain), chain.id || null);
|
|
551
671
|
}
|
|
552
672
|
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
553
673
|
// Distinguish source-authored stages (preserved identity) from
|
|
@@ -556,7 +676,7 @@ function cardsFromRuntimePayload(payload) {
|
|
|
556
676
|
// so the importer doesn't double-count what was already in the
|
|
557
677
|
// locked judgment cards.
|
|
558
678
|
if (stage.source_authored === true) {
|
|
559
|
-
pushImportedCard(cards, 'evolution_stage', stage, stage.id || null);
|
|
679
|
+
pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
|
|
560
680
|
}
|
|
561
681
|
}
|
|
562
682
|
return cards;
|
|
@@ -608,7 +728,7 @@ function buildManifest(project, name) {
|
|
|
608
728
|
asset_uid: project.asset_uid || source.asset_uid || 'urn:uuid:' + crypto.randomUUID(),
|
|
609
729
|
asset_type: project.type === 'cluster' ? 'cluster' : 'domain',
|
|
610
730
|
source_asset_type: source.asset_type || null,
|
|
611
|
-
title: titleFromName(source.name || name || project.name, assetId),
|
|
731
|
+
title: source.title || project.title || titleFromName(source.name || name || project.name, assetId),
|
|
612
732
|
version: semverValue(project.release?.version || source.version, '1.0.0'),
|
|
613
733
|
judgment_version: semverValue(project.release?.judgment_version || source.judgment_version || source.version, '1.0.0'),
|
|
614
734
|
created_at: isoDateTime(source.created_at || source.created || project.created),
|
|
@@ -663,8 +783,19 @@ function cmdCreate(args) {
|
|
|
663
783
|
let lineage = null;
|
|
664
784
|
let creatorIdentity = null;
|
|
665
785
|
|
|
666
|
-
//
|
|
667
|
-
|
|
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
|
+
}
|
|
668
799
|
|
|
669
800
|
if (fromKdna) {
|
|
670
801
|
sourceMode = 'kdna_asset';
|
|
@@ -691,7 +822,13 @@ function cmdCreate(args) {
|
|
|
691
822
|
if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
|
|
692
823
|
if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
|
|
693
824
|
if (kdnaData.source_evolution) project.source_evolution = kdnaData.source_evolution;
|
|
825
|
+
if (kdnaData.source_core_structure) project.source_core_structure = kdnaData.source_core_structure;
|
|
694
826
|
if (kdnaData.source_manifest) project.source_manifest = publicManifestMetadata(kdnaData.source_manifest);
|
|
827
|
+
if (kdnaData.source_manifest?.asset_uid) project.asset_uid = kdnaData.source_manifest.asset_uid;
|
|
828
|
+
if (kdnaData.source_manifest?.title) project.title = kdnaData.source_manifest.title;
|
|
829
|
+
if (kdnaData.source_manifest?.license) project.license = kdnaData.source_manifest.license;
|
|
830
|
+
if (kdnaData.source_manifest?.languages) project.languages = kdnaData.source_manifest.languages;
|
|
831
|
+
if (kdnaData.source_manifest?.language) project.default_language = kdnaData.source_manifest.language;
|
|
695
832
|
if (kdnaData.source_manifest?.version) {
|
|
696
833
|
if (!project.release) project.release = {};
|
|
697
834
|
project.release.version = kdnaData.source_manifest.version;
|
|
@@ -700,9 +837,9 @@ function cmdCreate(args) {
|
|
|
700
837
|
if (!project.release) project.release = {};
|
|
701
838
|
project.release.judgment_version = kdnaData.source_manifest.judgment_version;
|
|
702
839
|
}
|
|
703
|
-
if (kdnaData.source_manifest?.description) {
|
|
840
|
+
if (kdnaData.source_manifest?.description || kdnaData.source_manifest?.summary) {
|
|
704
841
|
if (!project.release) project.release = {};
|
|
705
|
-
project.release.description = kdnaData.source_manifest.description;
|
|
842
|
+
project.release.description = kdnaData.source_manifest.description || kdnaData.source_manifest.summary;
|
|
706
843
|
}
|
|
707
844
|
if (project.stages?.judgment_cards) {
|
|
708
845
|
project.stages.judgment_cards.total = kdnaData.cards.length;
|
|
@@ -756,6 +893,23 @@ function importFromKdna(kdnaPath) {
|
|
|
756
893
|
if (stats.size > 50 * 1024 * 1024) {
|
|
757
894
|
fail(`KDNA file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${absKdna}`);
|
|
758
895
|
}
|
|
896
|
+
|
|
897
|
+
// Read the container before Core validation only to produce a stable
|
|
898
|
+
// profile error. Studio does not retain a second Runtime contract.
|
|
899
|
+
const zipBuf = fs.readFileSync(absKdna);
|
|
900
|
+
const entries = readZipEntries(zipBuf);
|
|
901
|
+
validateZipContainer(entries);
|
|
902
|
+
if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
|
|
903
|
+
if (!entries.has('payload.kdnab')) fail('Not a valid .kdna asset: missing payload.kdnab. Use --from-folder for legacy JSON source migration.');
|
|
904
|
+
|
|
905
|
+
const manifest = JSON.parse(entries.get('kdna.json').toString());
|
|
906
|
+
const payload = decodePayload(entries.get('payload.kdnab'), manifest);
|
|
907
|
+
const profile = payload?.profile;
|
|
908
|
+
|
|
909
|
+
if (profile !== 'kdna.payload.judgment') {
|
|
910
|
+
fail(`Unsupported payload profile "${profile || '(none)'}". Only kdna.payload.judgment is accepted for --from-kdna.`);
|
|
911
|
+
}
|
|
912
|
+
|
|
759
913
|
let runtimeCore;
|
|
760
914
|
try { runtimeCore = require('@aikdna/kdna-core'); } catch {
|
|
761
915
|
fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
|
|
@@ -764,11 +918,7 @@ function importFromKdna(kdnaPath) {
|
|
|
764
918
|
if (!validation.overall_valid) {
|
|
765
919
|
fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
|
|
766
920
|
}
|
|
767
|
-
const zipBuf = fs.readFileSync(absKdna);
|
|
768
|
-
const entries = readZipEntries(zipBuf);
|
|
769
|
-
if (!entries.has('kdna.json')) fail('Not a valid .kdna asset: missing kdna.json');
|
|
770
921
|
|
|
771
|
-
const manifest = JSON.parse(entries.get('kdna.json').toString());
|
|
772
922
|
const lineage = {
|
|
773
923
|
type: 'fork',
|
|
774
924
|
parent_name: packageNameFromManifest(manifest),
|
|
@@ -777,42 +927,275 @@ function importFromKdna(kdnaPath) {
|
|
|
777
927
|
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
778
928
|
};
|
|
779
929
|
|
|
780
|
-
|
|
781
|
-
// are migrated explicitly with --from-folder; they are not runtime assets.
|
|
782
|
-
const importedCards = [];
|
|
783
|
-
let judgmentCore = null;
|
|
784
|
-
if (!entries.has('payload.kdnab')) {
|
|
785
|
-
fail('Not a current .kdna asset: missing payload.kdnab. Use --from-folder for legacy JSON source migration.');
|
|
786
|
-
}
|
|
930
|
+
let imported;
|
|
787
931
|
try {
|
|
788
|
-
|
|
789
|
-
importedCards.push(...cardsFromPayload(payload));
|
|
790
|
-
judgmentCore = judgmentCoreFromRuntimePayload(payload);
|
|
932
|
+
imported = importFromRuntimeContent(manifest, payload);
|
|
791
933
|
} catch (e) {
|
|
792
934
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
793
935
|
}
|
|
794
936
|
|
|
795
|
-
if (
|
|
937
|
+
if (imported.cards.length === 0) {
|
|
796
938
|
fail(`No current judgment cards could be extracted from ${absKdna}.`);
|
|
797
939
|
}
|
|
798
940
|
|
|
799
|
-
// Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
|
|
800
|
-
// KDNA_Reasoning / KDNA_Evolution entries so cmdCreate can store
|
|
801
|
-
// them on the project. The next export then forwards them to
|
|
802
|
-
// compileDomain via exportRuntimeAsset, which preserves
|
|
803
|
-
// changelog / version_notes / reasoning_chains / banned_terms
|
|
804
|
-
// through the round-trip.
|
|
805
941
|
return {
|
|
806
942
|
lineage,
|
|
807
|
-
|
|
808
|
-
|
|
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
|
+
},
|
|
809
978
|
source_manifest: manifest,
|
|
810
979
|
source_patterns: null,
|
|
811
|
-
source_reasoning:
|
|
812
|
-
|
|
980
|
+
source_reasoning: payload.reasoning && typeof payload.reasoning === 'object'
|
|
981
|
+
? JSON.parse(JSON.stringify(payload.reasoning))
|
|
982
|
+
: null,
|
|
983
|
+
source_evolution: payload.evolution && typeof payload.evolution === 'object'
|
|
984
|
+
? JSON.parse(JSON.stringify(payload.evolution))
|
|
985
|
+
: null,
|
|
986
|
+
source_core_structure: Array.isArray(payload.core?.core_structure)
|
|
987
|
+
? JSON.parse(JSON.stringify(payload.core.core_structure))
|
|
988
|
+
: null,
|
|
813
989
|
};
|
|
814
990
|
}
|
|
815
991
|
|
|
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
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const LEGACY_PATTERN_TYPE_MAP = {
|
|
1011
|
+
term: 'term',
|
|
1012
|
+
banned_term: 'banned_term',
|
|
1013
|
+
misunderstanding: 'misunderstanding',
|
|
1014
|
+
self_check: 'self_check',
|
|
1015
|
+
scenario: 'scenario',
|
|
1016
|
+
case: 'case',
|
|
1017
|
+
reasoning: 'reasoning',
|
|
1018
|
+
risk: 'risk',
|
|
1019
|
+
axiom: 'axiom',
|
|
1020
|
+
ontology: 'ontology',
|
|
1021
|
+
framework: 'framework',
|
|
1022
|
+
stance: 'stance',
|
|
1023
|
+
aesthetic: 'aesthetic',
|
|
1024
|
+
boundary: 'boundary',
|
|
1025
|
+
evolution_stage: 'evolution_stage',
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// Pattern subtypes that map to the generic 'pattern' card; the original
|
|
1029
|
+
// subtype is preserved in fields.legacy_subtype for audit and re-export.
|
|
1030
|
+
const LEGACY_PATTERN_SUBTYPES = new Set([
|
|
1031
|
+
'failure_pattern', 'design_pattern', 'response_pattern',
|
|
1032
|
+
'stopping_pattern', 'completion_pattern', 'decision_pattern',
|
|
1033
|
+
'reasoning_pattern', 'action_pattern', 'recovery_pattern',
|
|
1034
|
+
'responsibility_pattern',
|
|
1035
|
+
]);
|
|
1036
|
+
|
|
1037
|
+
const MAX_ZIP_ENTRIES = 100;
|
|
1038
|
+
const MAX_SINGLE_ENTRY_UNCOMPRESSED = 10 * 1024 * 1024;
|
|
1039
|
+
const MAX_TOTAL_UNCOMPRESSED = 50 * 1024 * 1024;
|
|
1040
|
+
const REQUIRED_ZIP_ENTRIES = ['kdna.json', 'payload.kdnab', 'checksums.json', 'mimetype'];
|
|
1041
|
+
|
|
1042
|
+
function validateZipContainer(entries) {
|
|
1043
|
+
if (entries.size > MAX_ZIP_ENTRIES) {
|
|
1044
|
+
fail(`ZIP container has too many entries (${entries.size}, max ${MAX_ZIP_ENTRIES})`);
|
|
1045
|
+
}
|
|
1046
|
+
for (const name of REQUIRED_ZIP_ENTRIES) {
|
|
1047
|
+
if (!entries.has(name)) fail(`Not a valid .kdna container: missing required entry "${name}"`);
|
|
1048
|
+
}
|
|
1049
|
+
let totalSize = 0;
|
|
1050
|
+
for (const [name, buf] of entries) {
|
|
1051
|
+
if (path.isAbsolute(name) || name.includes('..') || name.includes('\\')) {
|
|
1052
|
+
fail(`ZIP entry "${name}" has an unsafe path`);
|
|
1053
|
+
}
|
|
1054
|
+
if (Buffer.isBuffer(buf) && buf.length > MAX_SINGLE_ENTRY_UNCOMPRESSED) {
|
|
1055
|
+
fail(`ZIP entry "${name}" is too large (${buf.length} bytes)`);
|
|
1056
|
+
}
|
|
1057
|
+
totalSize += Buffer.isBuffer(buf) ? buf.length : 0;
|
|
1058
|
+
}
|
|
1059
|
+
if (totalSize > MAX_TOTAL_UNCOMPRESSED) {
|
|
1060
|
+
fail(`ZIP container total uncompressed size exceeds limit (${totalSize} bytes)`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function judgmentCoreFromLegacyPayload(payload) {
|
|
1065
|
+
const core = payload.core || {};
|
|
1066
|
+
const result = {};
|
|
1067
|
+
if (typeof core.highest_question === 'string' && core.highest_question) result.highest_question = core.highest_question;
|
|
1068
|
+
if (Array.isArray(core.worldview) && core.worldview.length) result.worldview = core.worldview;
|
|
1069
|
+
if (Array.isArray(core.value_order) && core.value_order.length) result.value_order = core.value_order;
|
|
1070
|
+
if (core.judgment_role && typeof core.judgment_role === 'object' && Object.keys(core.judgment_role).length) result.judgment_role = core.judgment_role;
|
|
1071
|
+
return Object.keys(result).length ? result : null;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function legacyStableId(type, fields, prefix) {
|
|
1075
|
+
// Deterministic ID for legacy items that lack a declared id.
|
|
1076
|
+
// Prefix is the parent scope (e.g. the asset name or domain id).
|
|
1077
|
+
const sorted = JSON.stringify(Object.entries(fields).sort());
|
|
1078
|
+
const h = require('crypto').createHash('sha256').update(`${type}:${sorted}`).digest('hex');
|
|
1079
|
+
return `${prefix}_${h.slice(0, 12)}`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const _legacyDedupIds = new Set();
|
|
1083
|
+
|
|
1084
|
+
function _legacyDedupPush(cards, type, fields, id) {
|
|
1085
|
+
// Deduplicate by id IF an id is declared. Items without ids are unique
|
|
1086
|
+
// (deterministic by content). A second push with the same id merges.
|
|
1087
|
+
if (!id) { pushImportedCard(cards, type, fields, null); return; }
|
|
1088
|
+
if (_legacyDedupIds.has(id)) { pushImportedCard(cards, type, fields, id); return; }
|
|
1089
|
+
_legacyDedupIds.add(id);
|
|
1090
|
+
pushImportedCard(cards, type, fields, id);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function cardsFromLegacyPayload(payload) {
|
|
1094
|
+
_legacyDedupIds.clear();
|
|
1095
|
+
const cards = [];
|
|
1096
|
+
const core = payload.core || {};
|
|
1097
|
+
|
|
1098
|
+
// axioms
|
|
1099
|
+
for (const ax of (core.axioms || [])) {
|
|
1100
|
+
_legacyDedupPush(cards, 'axiom', importedFields(ax), ax.id || null);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// ontology
|
|
1104
|
+
for (const ont of (core.ontology || [])) {
|
|
1105
|
+
pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// frameworks
|
|
1109
|
+
for (const fw of (core.frameworks || [])) {
|
|
1110
|
+
pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// stances
|
|
1114
|
+
for (const st of (core.stances || [])) {
|
|
1115
|
+
const fields = typeof st === 'string' ? { statement: st } : importedFields(st);
|
|
1116
|
+
pushImportedCard(cards, 'stance', fields, typeof st === 'object' ? st.id || null : null);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// boundaries
|
|
1120
|
+
for (const bd of (core.boundaries || [])) {
|
|
1121
|
+
pushImportedCard(cards, 'boundary', importedFields(bd), bd.id || null);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// risks
|
|
1125
|
+
const legacyRisks = Array.isArray(core.risk_model)
|
|
1126
|
+
? core.risk_model
|
|
1127
|
+
: (Array.isArray(core.risk_model?.risks) ? core.risk_model.risks : []);
|
|
1128
|
+
for (const risk of legacyRisks) {
|
|
1129
|
+
pushImportedCard(cards, 'risk', importedFields(risk.fields || risk), risk.id || null);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// patterns with explicit types (including legacy subtypes)
|
|
1133
|
+
const patterns = payload.patterns || [];
|
|
1134
|
+
for (const p of patterns) {
|
|
1135
|
+
if (!p || typeof p !== 'object') continue;
|
|
1136
|
+
const rawType = p.type;
|
|
1137
|
+
if (!rawType) continue; // type-less items handled separately (misunderstandings below)
|
|
1138
|
+
|
|
1139
|
+
if (LEGACY_PATTERN_SUBTYPES.has(rawType)) {
|
|
1140
|
+
pushImportedCard(cards, 'pattern', {
|
|
1141
|
+
...importedFields(p, { keepType: true }),
|
|
1142
|
+
legacy_subtype: rawType,
|
|
1143
|
+
}, p.id || null);
|
|
1144
|
+
} else if (rawType === 'term') {
|
|
1145
|
+
pushImportedCard(cards, 'term', importedFields(p), p.id || null);
|
|
1146
|
+
} else if (rawType === 'banned_term') {
|
|
1147
|
+
pushImportedCard(cards, 'banned_term', importedFields(p), p.id || null);
|
|
1148
|
+
} else if (LEGACY_PATTERN_TYPE_MAP.hasOwnProperty(rawType)) {
|
|
1149
|
+
pushImportedCard(cards, LEGACY_PATTERN_TYPE_MAP[rawType], importedFields(p), p.id || null);
|
|
1150
|
+
} else {
|
|
1151
|
+
fail(`Unknown legacy pattern type "${rawType}" for card "${p.id || '(no id)'}"`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// misunderstandings: appear in patterns[] with no type and in
|
|
1156
|
+
// reasoning.failure_modes[] with slightly different field names.
|
|
1157
|
+
// Import from patterns[] (richer fields). The pushImportedCard
|
|
1158
|
+
// dedup handles the failure_modes view via id matching.
|
|
1159
|
+
for (const p of patterns) {
|
|
1160
|
+
if (!p || typeof p !== 'object') continue;
|
|
1161
|
+
if (p.type) continue; // already handled above
|
|
1162
|
+
if (!p.wrong && !p.correct) continue;
|
|
1163
|
+
pushImportedCard(cards, 'misunderstanding', importedFields(p), p.id || null);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// reasoning chains
|
|
1167
|
+
for (const chain of (payload.reasoning?.reasoning_chains || [])) {
|
|
1168
|
+
const fields = importedFields(chain);
|
|
1169
|
+
pushImportedCard(cards, 'reasoning', fields, chain.id || null);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// self checks: array of plain strings in legacy payload
|
|
1173
|
+
for (const check of (payload.reasoning?.self_check || [])) {
|
|
1174
|
+
if (typeof check === 'string' && check.trim()) {
|
|
1175
|
+
const fields = { question: check.trim() };
|
|
1176
|
+
const id = legacyStableId('self_check', fields, 'legacy_sc');
|
|
1177
|
+
pushImportedCard(cards, 'self_check', fields, id);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// scenarios
|
|
1182
|
+
for (const sc of (payload.scenarios || [])) {
|
|
1183
|
+
pushImportedCard(cards, 'scenario', importedFields(sc), sc.id || null);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// cases
|
|
1187
|
+
for (const cs of (payload.cases || [])) {
|
|
1188
|
+
pushImportedCard(cards, 'case', importedFields(cs), cs.id || null);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// evolution stages
|
|
1192
|
+
for (const stage of (payload.evolution?.stages || [])) {
|
|
1193
|
+
pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return cards;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
816
1199
|
/**
|
|
817
1200
|
* Import cards from a legacy source folder into a new Studio project.
|
|
818
1201
|
* Reads KDNA_*.json files and converts entries to draft judgment cards.
|
|
@@ -847,6 +1230,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
847
1230
|
if (manifest.default_language) manifestData.default_language = manifest.default_language;
|
|
848
1231
|
if (manifest.description) manifestData.description = manifest.description;
|
|
849
1232
|
if (manifest.name) manifestData.name = manifest.name;
|
|
1233
|
+
if (manifest.title) manifestData.title = manifest.title;
|
|
850
1234
|
if (manifest.keywords) manifestData.keywords = manifest.keywords;
|
|
851
1235
|
if (manifest.license) manifestData.license = manifest.license;
|
|
852
1236
|
if (manifest.author) manifestData.author = manifest.author;
|
|
@@ -887,25 +1271,17 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
887
1271
|
|
|
888
1272
|
if (core) {
|
|
889
1273
|
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
890
|
-
const fields =
|
|
1274
|
+
const fields = importedFields(ax);
|
|
891
1275
|
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type']) {
|
|
892
|
-
if (key in ax)
|
|
893
|
-
else audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
|
|
1276
|
+
if (!(key in ax)) audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
|
|
894
1277
|
}
|
|
895
1278
|
importedCards.push(cardApi.createCard('axiom', fields, ax.id || null));
|
|
896
1279
|
}
|
|
897
1280
|
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
898
|
-
importedCards.push(cardApi.createCard('ontology',
|
|
899
|
-
one_sentence: ont.one_sentence || ont.essence || '',
|
|
900
|
-
essence: ont.essence || '', boundary: ont.boundary || '',
|
|
901
|
-
trigger_signal: ont.trigger_signal || '',
|
|
902
|
-
}, ont.id || null));
|
|
1281
|
+
importedCards.push(cardApi.createCard('ontology', importedFields(ont), ont.id || null));
|
|
903
1282
|
}
|
|
904
1283
|
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
905
|
-
importedCards.push(cardApi.createCard('boundary',
|
|
906
|
-
scope: b.scope || '', out_of_scope: b.out_of_scope || '',
|
|
907
|
-
acceptable_exceptions: b.acceptable_exceptions || [],
|
|
908
|
-
}, b.id || null));
|
|
1284
|
+
importedCards.push(cardApi.createCard('boundary', importedFields(b), b.id || null));
|
|
909
1285
|
}
|
|
910
1286
|
// Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
|
|
911
1287
|
// (either as `{risks: [...]}` or as an array itself).
|
|
@@ -916,23 +1292,16 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
916
1292
|
else risksSource = [];
|
|
917
1293
|
}
|
|
918
1294
|
for (const r of risksSource) {
|
|
919
|
-
importedCards.push(cardApi.createCard('risk', r.fields || r, r.id || null));
|
|
1295
|
+
importedCards.push(cardApi.createCard('risk', importedFields(r.fields || r), r.id || null));
|
|
920
1296
|
}
|
|
921
1297
|
// NEW: import stances
|
|
922
1298
|
for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
923
|
-
const
|
|
924
|
-
importedCards.push(cardApi.createCard('stance',
|
|
925
|
-
statement: text,
|
|
926
|
-
applies_when: s.applies_when || [],
|
|
927
|
-
does_not_apply_when: s.does_not_apply_when || [],
|
|
928
|
-
}, typeof s === 'object' ? s.id || null : null));
|
|
1299
|
+
const fields = typeof s === 'string' ? { statement: s } : importedFields(s);
|
|
1300
|
+
importedCards.push(cardApi.createCard('stance', fields, typeof s === 'object' ? s.id || null : null));
|
|
929
1301
|
}
|
|
930
1302
|
// NEW: import frameworks
|
|
931
1303
|
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
932
|
-
importedCards.push(cardApi.createCard('framework',
|
|
933
|
-
name: fw.name || '', when_to_use: fw.when_to_use || '',
|
|
934
|
-
steps: fw.steps || [],
|
|
935
|
-
}, fw.id || null));
|
|
1304
|
+
importedCards.push(cardApi.createCard('framework', importedFields(fw), fw.id || null));
|
|
936
1305
|
}
|
|
937
1306
|
}
|
|
938
1307
|
|
|
@@ -940,27 +1309,25 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
940
1309
|
// NEW: import terminology
|
|
941
1310
|
if (patterns.terminology) {
|
|
942
1311
|
for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
|
|
943
|
-
importedCards.push(cardApi.createCard('term',
|
|
944
|
-
term: t.term || '', definition: t.definition || '',
|
|
945
|
-
}, t.id || null));
|
|
1312
|
+
importedCards.push(cardApi.createCard('term', importedFields(t), t.id || null));
|
|
946
1313
|
}
|
|
947
1314
|
for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
|
|
948
|
-
importedCards.push(cardApi.createCard('banned_term',
|
|
949
|
-
term: bt.term || '', why: bt.why || '', replace_with: bt.replace_with || '',
|
|
950
|
-
}, bt.id || null));
|
|
1315
|
+
importedCards.push(cardApi.createCard('banned_term', importedFields(bt), bt.id || null));
|
|
951
1316
|
}
|
|
952
1317
|
}
|
|
1318
|
+
for (const pattern of (Array.isArray(patterns.patterns) ? patterns.patterns : [])) {
|
|
1319
|
+
const fields = importedFields(pattern, { keepType: true });
|
|
1320
|
+
if (!fields.type) fields.type = 'pattern';
|
|
1321
|
+
importedCards.push(cardApi.createCard('pattern', fields, pattern.id || null));
|
|
1322
|
+
}
|
|
953
1323
|
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
954
|
-
importedCards.push(cardApi.createCard('misunderstanding',
|
|
955
|
-
wrong: ms.wrong || '', correct: ms.correct || '',
|
|
956
|
-
key_distinction: ms.key_distinction || '', why: ms.why || '',
|
|
957
|
-
}, ms.id || null));
|
|
1324
|
+
importedCards.push(cardApi.createCard('misunderstanding', importedFields(ms), ms.id || null));
|
|
958
1325
|
}
|
|
959
1326
|
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
960
|
-
const
|
|
1327
|
+
const fields = typeof sc === 'string' ? { question: sc } : importedFields(sc);
|
|
961
1328
|
importedCards.push(cardApi.createCard(
|
|
962
1329
|
'self_check',
|
|
963
|
-
|
|
1330
|
+
fields,
|
|
964
1331
|
typeof sc === 'object' ? sc.id || null : null,
|
|
965
1332
|
));
|
|
966
1333
|
}
|
|
@@ -973,7 +1340,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
973
1340
|
for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
|
|
974
1341
|
importedCards.push(cardApi.createCard(
|
|
975
1342
|
'aesthetic',
|
|
976
|
-
aesthetic.fields || aesthetic,
|
|
1343
|
+
importedFields(aesthetic.fields || aesthetic),
|
|
977
1344
|
aesthetic.id || null,
|
|
978
1345
|
));
|
|
979
1346
|
}
|
|
@@ -982,20 +1349,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
982
1349
|
// NEW: import scenarios
|
|
983
1350
|
if (scenarios && Array.isArray(scenarios.scenes)) {
|
|
984
1351
|
for (const scene of scenarios.scenes) {
|
|
985
|
-
importedCards.push(cardApi.createCard('scenario',
|
|
986
|
-
name: scene.name || scene.id || '',
|
|
987
|
-
trigger: scene.trigger || '', action: scene.action || '',
|
|
988
|
-
}, scene.id || null));
|
|
1352
|
+
importedCards.push(cardApi.createCard('scenario', importedFields(scene), scene.id || null));
|
|
989
1353
|
}
|
|
990
1354
|
}
|
|
991
1355
|
|
|
992
1356
|
// NEW: import cases
|
|
993
1357
|
if (cases && Array.isArray(cases.cases)) {
|
|
994
1358
|
for (const c of cases.cases) {
|
|
995
|
-
importedCards.push(cardApi.createCard('case',
|
|
996
|
-
title: c.title || c.id || '',
|
|
997
|
-
scenario: c.scenario || '', expected: c.expected || '',
|
|
998
|
-
}, c.id || null));
|
|
1359
|
+
importedCards.push(cardApi.createCard('case', importedFields(c), c.id || null));
|
|
999
1360
|
}
|
|
1000
1361
|
}
|
|
1001
1362
|
|
|
@@ -1008,26 +1369,14 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
1008
1369
|
// identity.
|
|
1009
1370
|
if (reasoning && Array.isArray(reasoning.reasoning_chains)) {
|
|
1010
1371
|
for (const rc of reasoning.reasoning_chains) {
|
|
1011
|
-
importedCards.push(cardApi.createCard('reasoning',
|
|
1012
|
-
axiom: rc.axiom || '',
|
|
1013
|
-
one_sentence: rc.one_sentence || rc.conclusion || '',
|
|
1014
|
-
chain: Array.isArray(rc.chain) ? rc.chain : (rc.logic || []),
|
|
1015
|
-
principle: rc.principle || rc.name || '',
|
|
1016
|
-
concrete_action: rc.concrete_action || rc.so_what || '',
|
|
1017
|
-
so_what: rc.so_what || rc.concrete_action || '',
|
|
1018
|
-
conclusion: rc.conclusion || '',
|
|
1019
|
-
}, rc.id || null));
|
|
1372
|
+
importedCards.push(cardApi.createCard('reasoning', importedFields(rc), rc.id || null));
|
|
1020
1373
|
}
|
|
1021
1374
|
}
|
|
1022
1375
|
|
|
1023
1376
|
// NEW: import evolution stages
|
|
1024
1377
|
if (evolution && Array.isArray(evolution.stages)) {
|
|
1025
1378
|
for (const stage of evolution.stages) {
|
|
1026
|
-
pushImportedCard(importedCards, 'evolution_stage',
|
|
1027
|
-
name: stage.name || stage.id || '',
|
|
1028
|
-
level: stage.level != null ? stage.level : '',
|
|
1029
|
-
description: stage.description || '',
|
|
1030
|
-
}, stage.id || null);
|
|
1379
|
+
pushImportedCard(importedCards, 'evolution_stage', importedFields(stage), stage.id || null);
|
|
1031
1380
|
}
|
|
1032
1381
|
}
|
|
1033
1382
|
|
|
@@ -1038,6 +1387,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
1038
1387
|
sourcePath: absSource,
|
|
1039
1388
|
creatorIdentity: creatorIdentity || null,
|
|
1040
1389
|
lineage: { type: 'migrated', parent_name: manifestData.name || null, parent_asset_uid: null, parent_version: manifestData.version || null, parent_asset_digest: null },
|
|
1390
|
+
judgmentCore: judgmentCoreFromLegacyPayload({ core: core || {} }),
|
|
1041
1391
|
});
|
|
1042
1392
|
// Preserve manifest metadata
|
|
1043
1393
|
if (manifestData.asset_uid) project.asset_uid = manifestData.asset_uid;
|
|
@@ -1053,11 +1403,18 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
1053
1403
|
if (!project.release) project.release = {};
|
|
1054
1404
|
project.release.description = manifestData.description;
|
|
1055
1405
|
}
|
|
1406
|
+
if (manifestData.title) project.title = manifestData.title;
|
|
1056
1407
|
if (manifestData.license) project.license = manifestData.license;
|
|
1057
1408
|
if (manifestData.author) project.author = manifestData.author;
|
|
1058
1409
|
if (manifestData.languages) project.languages = manifestData.languages;
|
|
1059
1410
|
if (manifestData.default_language) project.default_language = manifestData.default_language;
|
|
1060
1411
|
if (manifestData.raw) project.source_manifest = publicManifestMetadata(manifestData.raw);
|
|
1412
|
+
if (patterns) project.source_patterns = JSON.parse(JSON.stringify(patterns));
|
|
1413
|
+
if (reasoning) project.source_reasoning = JSON.parse(JSON.stringify(reasoning));
|
|
1414
|
+
if (evolution) project.source_evolution = JSON.parse(JSON.stringify(evolution));
|
|
1415
|
+
if (Array.isArray(core?.core_structure)) {
|
|
1416
|
+
project.source_core_structure = JSON.parse(JSON.stringify(core.core_structure));
|
|
1417
|
+
}
|
|
1061
1418
|
|
|
1062
1419
|
project.cards = importedCards;
|
|
1063
1420
|
if (project.stages?.judgment_cards) {
|
|
@@ -1074,10 +1431,12 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
1074
1431
|
}
|
|
1075
1432
|
|
|
1076
1433
|
/**
|
|
1077
|
-
* Minimal ZIP central directory parser —
|
|
1434
|
+
* Minimal ZIP central directory parser — validates every entry before decompressing.
|
|
1078
1435
|
*/
|
|
1079
1436
|
function readZipEntries(buf) {
|
|
1080
1437
|
const entries = new Map();
|
|
1438
|
+
const metadata = [];
|
|
1439
|
+
|
|
1081
1440
|
// Find end-of-central-directory record
|
|
1082
1441
|
let eocdOffset = buf.length - 22;
|
|
1083
1442
|
while (eocdOffset >= 0) {
|
|
@@ -1089,6 +1448,8 @@ function readZipEntries(buf) {
|
|
|
1089
1448
|
const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
1090
1449
|
let offset = centralDirOffset;
|
|
1091
1450
|
|
|
1451
|
+
// Pass 1: collect metadata, validate safety before any decompression
|
|
1452
|
+
let totalUncomp = 0;
|
|
1092
1453
|
while (offset < eocdOffset) {
|
|
1093
1454
|
const sig = buf.readUInt32LE(offset);
|
|
1094
1455
|
if (sig !== 0x02014b50) break;
|
|
@@ -1101,20 +1462,47 @@ function readZipEntries(buf) {
|
|
|
1101
1462
|
const localOffset = buf.readUInt32LE(offset + 42);
|
|
1102
1463
|
const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
|
|
1103
1464
|
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
|
1108
|
-
|
|
1109
|
-
if (compMethod === 0) {
|
|
1110
|
-
entries.set(name, buf.subarray(dataStart, dataStart + uncompSize));
|
|
1111
|
-
} else if (compMethod === 8) {
|
|
1112
|
-
entries.set(name, zlib.inflateRawSync(buf.subarray(dataStart, dataStart + compSize)));
|
|
1465
|
+
// Security: path must not be absolute, must not escape
|
|
1466
|
+
if (!name || name.startsWith('/') || name.includes('..') || name.includes('\\')) {
|
|
1467
|
+
throw new Error(`ZIP entry "${name}" has an unsafe path`);
|
|
1113
1468
|
}
|
|
1114
|
-
|
|
1469
|
+
// Duplicate detection
|
|
1470
|
+
if (entries.has(name) || metadata.some(m => m.name === name)) {
|
|
1471
|
+
throw new Error(`Duplicate ZIP entry: "${name}"`);
|
|
1472
|
+
}
|
|
1473
|
+
if (compMethod !== 0 && compMethod !== 8) {
|
|
1474
|
+
throw new Error(`ZIP entry "${name}" uses unsupported compression method ${compMethod}`);
|
|
1475
|
+
}
|
|
1476
|
+
totalUncomp += uncompSize;
|
|
1477
|
+
if (totalUncomp > MAX_TOTAL_UNCOMPRESSED) {
|
|
1478
|
+
throw new Error(`ZIP total uncompressed size ${totalUncomp} exceeds limit`);
|
|
1479
|
+
}
|
|
1480
|
+
if (uncompSize > MAX_SINGLE_ENTRY_UNCOMPRESSED) {
|
|
1481
|
+
throw new Error(`ZIP entry "${name}" declared size ${uncompSize} exceeds limit`);
|
|
1482
|
+
}
|
|
1483
|
+
const dataStart = localOffset + 30 + buf.readUInt16LE(localOffset + 26) + buf.readUInt16LE(localOffset + 28);
|
|
1484
|
+
metadata.push({ name, compMethod, compSize, uncompSize, dataStart });
|
|
1115
1485
|
offset += 46 + nameLen + extraLen + commentLen;
|
|
1116
1486
|
}
|
|
1117
1487
|
|
|
1488
|
+
if (metadata.length > MAX_ZIP_ENTRIES) {
|
|
1489
|
+
throw new Error(`ZIP container has too many entries (${metadata.length}, max ${MAX_ZIP_ENTRIES})`);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Pass 2: decompress into entries
|
|
1493
|
+
for (const m of metadata) {
|
|
1494
|
+
if (m.compMethod === 0) {
|
|
1495
|
+
entries.set(m.name, buf.subarray(m.dataStart, m.dataStart + m.uncompSize));
|
|
1496
|
+
} else {
|
|
1497
|
+
const raw = buf.subarray(m.dataStart, m.dataStart + m.compSize);
|
|
1498
|
+
const inflated = zlib.inflateRawSync(raw, { maxOutputLength: m.uncompSize });
|
|
1499
|
+
if (inflated.length !== m.uncompSize) {
|
|
1500
|
+
throw new Error(`ZIP entry "${m.name}" decompressed length ${inflated.length} != declared ${m.uncompSize}`);
|
|
1501
|
+
}
|
|
1502
|
+
entries.set(m.name, inflated);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1118
1506
|
return entries;
|
|
1119
1507
|
}
|
|
1120
1508
|
|
|
@@ -1535,7 +1923,7 @@ function cmdCard(args) {
|
|
|
1535
1923
|
const passphrase = resolvePassphrase(args);
|
|
1536
1924
|
try {
|
|
1537
1925
|
if (identity.encrypted && !passphrase) {
|
|
1538
|
-
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin
|
|
1926
|
+
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin`);
|
|
1539
1927
|
return null;
|
|
1540
1928
|
}
|
|
1541
1929
|
lockPayload.signature = creatorApi.signHumanLock(
|
|
@@ -1656,7 +2044,7 @@ function cmdMigrate(args) {
|
|
|
1656
2044
|
const requestedName = option(args, '--name');
|
|
1657
2045
|
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
1658
2046
|
if (!sourceDir || (!out && !checkOnly) || (!by && !checkOnly) || (!statement && !checkOnly)) {
|
|
1659
|
-
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]');
|
|
1660
2048
|
}
|
|
1661
2049
|
|
|
1662
2050
|
const sourcePath = path.resolve(sourceDir);
|
|
@@ -1676,7 +2064,16 @@ function cmdMigrate(args) {
|
|
|
1676
2064
|
try {
|
|
1677
2065
|
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1678
2066
|
let creatorIdentity = null;
|
|
1679
|
-
|
|
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
|
+
}
|
|
1680
2077
|
if (isStudioProject) {
|
|
1681
2078
|
const loaded = readProject(sourcePath);
|
|
1682
2079
|
project = loaded.project;
|
|
@@ -1887,20 +2284,21 @@ function cmdExport(args) {
|
|
|
1887
2284
|
fail('The --format option is not supported. KDNA has one current asset format.');
|
|
1888
2285
|
}
|
|
1889
2286
|
if (args.includes('--sign')) {
|
|
1890
|
-
fail('Asset
|
|
2287
|
+
fail('Asset signatures are outside the current Preview contract.');
|
|
1891
2288
|
}
|
|
1892
2289
|
{
|
|
1893
2290
|
const { project } = readProject(projectInput);
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
+
}
|
|
1904
2302
|
const useStdin = args.includes('--password-stdin');
|
|
1905
2303
|
let password;
|
|
1906
2304
|
if (useStdin) {
|
|
@@ -1910,25 +2308,15 @@ function cmdExport(args) {
|
|
|
1910
2308
|
if (process.stdin.isTTY) {
|
|
1911
2309
|
fail(
|
|
1912
2310
|
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
1913
|
-
'Example:
|
|
2311
|
+
'Example: printf "%s\\n" "$KDNA_PASSWORD" | kdna-studio export <project> --out <file.kdna> --password-stdin\n' +
|
|
1914
2312
|
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
1915
2313
|
);
|
|
1916
2314
|
}
|
|
1917
2315
|
try {
|
|
1918
|
-
password =
|
|
2316
|
+
password = readBoundedSecretStdin('Password');
|
|
1919
2317
|
} catch (e) {
|
|
1920
2318
|
fail(`Could not read password from stdin: ${e.message}`);
|
|
1921
2319
|
}
|
|
1922
|
-
} else {
|
|
1923
|
-
// Bug (#55): prior version only consulted --password / --passphrase
|
|
1924
|
-
// here, so a caller who set KDNA_PASSPHRASE for the signing path
|
|
1925
|
-
// (works via resolvePassphrase) found that the runtime encryption
|
|
1926
|
-
// password was silently null. Resolve from the same sources as
|
|
1927
|
-
// the signing path so the two stay in sync.
|
|
1928
|
-
password = process.env.KDNA_PASSPHRASE
|
|
1929
|
-
|| process.env.KDNA_PASSWORD
|
|
1930
|
-
|| option(args, '--password')
|
|
1931
|
-
|| option(args, '--passphrase');
|
|
1932
2320
|
}
|
|
1933
2321
|
exportProject(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
1934
2322
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
@@ -1944,7 +2332,9 @@ function cmdStudioInstall(args) {
|
|
|
1944
2332
|
if (!target) fail('Usage: kdna-studio install <@scope/name|file.kdna>');
|
|
1945
2333
|
try {
|
|
1946
2334
|
const kdnaArgs = ['install', target, '--yes'];
|
|
1947
|
-
if (args.includes('--trusted'))
|
|
2335
|
+
if (args.includes('--trusted')) {
|
|
2336
|
+
fail('Asset signatures are outside the current Preview contract.');
|
|
2337
|
+
}
|
|
1948
2338
|
const result = require('child_process').spawnSync('kdna', kdnaArgs, {
|
|
1949
2339
|
stdio: 'inherit', encoding: 'utf8',
|
|
1950
2340
|
});
|
|
@@ -1998,6 +2388,77 @@ function cmdLlm(args) {
|
|
|
1998
2388
|
fail('Usage: kdna-studio llm <config|show>');
|
|
1999
2389
|
}
|
|
2000
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
|
+
|
|
2001
2462
|
function cmdIdentity(args) {
|
|
2002
2463
|
const sub = args[0];
|
|
2003
2464
|
if (sub === 'init') {
|
|
@@ -2016,7 +2477,7 @@ function cmdIdentity(args) {
|
|
|
2016
2477
|
console.log(` Private key is encrypted — keep your passphrase safe.`);
|
|
2017
2478
|
}
|
|
2018
2479
|
} catch (err) {
|
|
2019
|
-
|
|
2480
|
+
failIdentityInit(err);
|
|
2020
2481
|
}
|
|
2021
2482
|
return;
|
|
2022
2483
|
}
|
|
@@ -2034,22 +2495,14 @@ function cmdIdentity(args) {
|
|
|
2034
2495
|
fail('Usage: kdna-studio identity <init|show>');
|
|
2035
2496
|
}
|
|
2036
2497
|
|
|
2037
|
-
function cmdReport(args) {
|
|
2038
|
-
const { project } = readProject(args[0]);
|
|
2039
|
-
const readiness = quality.computeReadiness(project);
|
|
2040
|
-
const gate = projectApi.checkHumanLockGate(project);
|
|
2041
|
-
console.log(JSON.stringify({ readiness, human_lock_gate: gate }, null, 2));
|
|
2042
|
-
process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
2498
|
// Audit-locks: list every card that would fail the current Human Lock
|
|
2046
2499
|
// gate because of missing or empty fields. Used during the
|
|
2047
2500
|
// pre-migration review of source-tree assets (e.g. pro-20) so an
|
|
2048
2501
|
// author can fill in the missing confidence / evidence_type / risk
|
|
2049
2502
|
// description / stance statement BEFORE running `migrate`.
|
|
2050
2503
|
//
|
|
2051
|
-
//
|
|
2052
|
-
//
|
|
2504
|
+
// The earlier readiness output returned a flat human_lock_gate.issues list,
|
|
2505
|
+
// which mixed card
|
|
2053
2506
|
// types and field names. An author migrating 20 source-tree
|
|
2054
2507
|
// assets with 99 axioms + 140 risk cards + 70 stance cards had no
|
|
2055
2508
|
// way to see, for a single asset, "which axioms are missing
|
|
@@ -2311,6 +2764,16 @@ async function cmdDistill(args) {
|
|
|
2311
2764
|
suggested_card_type: c.type || c.suggested_card_type || 'axiom',
|
|
2312
2765
|
supporting_evidence_ids: (c.evidence_ids || c.evidenceIds || c.supporting_evidence_ids || []).map(String),
|
|
2313
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
|
+
: {},
|
|
2314
2777
|
candidate_status: 'proposed',
|
|
2315
2778
|
scope_fit: c.scope_fit ?? c.scopeFit ?? true,
|
|
2316
2779
|
};
|
|
@@ -2369,36 +2832,68 @@ function cmdCandidate(args) {
|
|
|
2369
2832
|
}
|
|
2370
2833
|
|
|
2371
2834
|
if (sub === 'promote') {
|
|
2372
|
-
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
|
+
);
|
|
2373
2841
|
if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
|
|
2374
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
|
+
|
|
2375
2871
|
for (const c of accepted) {
|
|
2376
|
-
//
|
|
2377
|
-
//
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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
|
+
};
|
|
2384
2886
|
if (c.suggested_card_type === 'axiom') {
|
|
2385
|
-
fields.
|
|
2386
|
-
fields.
|
|
2387
|
-
fields.why = c.why || '<TBD: why>';
|
|
2388
|
-
// applies_when / does_not_apply_when are arrays, not strings.
|
|
2389
|
-
// Empty string is invalid; emit an empty array so checkRequiredFields
|
|
2390
|
-
// (which treats `length === 0` as missing) correctly reports them.
|
|
2391
|
-
fields.applies_when = Array.isArray(c.applies_when) ? c.applies_when : [];
|
|
2392
|
-
fields.does_not_apply_when = Array.isArray(c.does_not_apply_when) ? c.does_not_apply_when : [];
|
|
2393
|
-
fields.failure_risk = c.failure_risk || '<TBD: failure_risk>';
|
|
2394
|
-
} else {
|
|
2395
|
-
fields.one_sentence = c.one_sentence || '';
|
|
2396
|
-
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;
|
|
2397
2889
|
}
|
|
2398
2890
|
const card = cardApi.createCard(c.suggested_card_type, fields);
|
|
2399
|
-
card.evidence_refs = c.supporting_evidence_ids
|
|
2891
|
+
card.evidence_refs = Array.isArray(c.supporting_evidence_ids)
|
|
2892
|
+
? c.supporting_evidence_ids
|
|
2893
|
+
: [];
|
|
2400
2894
|
project.cards = project.cards || [];
|
|
2401
2895
|
project.cards.push(card);
|
|
2896
|
+
c.promoted_card_id = card.id;
|
|
2402
2897
|
}
|
|
2403
2898
|
if (project.stages?.judgment_cards) {
|
|
2404
2899
|
project.stages.judgment_cards.total = project.cards.length;
|
|
@@ -2416,175 +2911,46 @@ async function cmdInterview(args) {
|
|
|
2416
2911
|
const projectInput = args[0];
|
|
2417
2912
|
const stage = option(args, '--stage');
|
|
2418
2913
|
if (!projectInput) fail('Usage: kdna-studio interview <project> [--stage distill|clarify|correct|replay]');
|
|
2419
|
-
const { project } = readProject(projectInput);
|
|
2914
|
+
const { projectPath, project } = readProject(projectInput);
|
|
2915
|
+
let results;
|
|
2420
2916
|
|
|
2421
2917
|
if (stage) {
|
|
2422
|
-
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];
|
|
2423
2927
|
console.log(`\n=== ${result.label} ===\n${result.content}`);
|
|
2424
2928
|
} else {
|
|
2425
|
-
await ai.interview.runInterviewInteractive(llm.config(), project, {});
|
|
2929
|
+
results = await ai.interview.runInterviewInteractive(llm.config(), project, {});
|
|
2426
2930
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
// prepared to auto-synthesise one. A caller without a configured
|
|
2447
|
-
// LLM (no `KDNA_API_KEY`, no `kdna-studio llm config`) would
|
|
2448
|
-
// get a `fail` instead of a structured note. The fix removes the
|
|
2449
|
-
// guard: a missing restatement is now handed off to the evaluator
|
|
2450
|
-
// (which either asks the LLM to evaluate a synthesised one or,
|
|
2451
|
-
// without an LLM, returns a structured `synthesised_restatement`
|
|
2452
|
-
// for the caller to use as a starting point). The error message
|
|
2453
|
-
// is kept as a secondary path: only if the evaluator itself fails
|
|
2454
|
-
// does the command exit non-zero, and only then with a clear
|
|
2455
|
-
// pointer to the manual `card update` path.
|
|
2456
|
-
//
|
|
2457
|
-
// We do still need a restatement-shaped object to feed the
|
|
2458
|
-
// evaluator, so we synthesise one in-memory when the card has
|
|
2459
|
-
// none. This does NOT persist the synthesised restatement to the
|
|
2460
|
-
// project (so the user still has to opt in via `card update` to
|
|
2461
|
-
// keep one across runs), but it lets the no-LLM path return a
|
|
2462
|
-
// useful result instead of a hard fail.
|
|
2463
|
-
if (!card.feynman_restatement) {
|
|
2464
|
-
const synthesised = ai.feynman.synthFeynmanRestatement(card.fields || {});
|
|
2465
|
-
card.feynman_restatement = { text: synthesised, synthesised: true };
|
|
2466
|
-
console.warn(`No feynman_restatement on ${cardId}; using synthesised:\n "${synthesised}"`);
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
if (!useJson) console.log(`Evaluating Feynman restatement for card: ${cardId}`);
|
|
2470
|
-
|
|
2471
|
-
// Bug (#1 UX follow-up): if no LLM is configured and the caller
|
|
2472
|
-
// did not pass --no-llm, route through cmdNeedsLlm which either
|
|
2473
|
-
// gives a clear "configure LLM" error or accepts the --no-llm
|
|
2474
|
-
// path. Without this branch, the no-LLM caller would fall
|
|
2475
|
-
// through to the LLM call below and get a stack trace.
|
|
2476
|
-
const llmGate = cmdNeedsLlm(args, 'feynmann');
|
|
2477
|
-
|
|
2478
|
-
let result;
|
|
2479
|
-
if (llmGate.noLlm) {
|
|
2480
|
-
// Static evaluation: every criterion is "unsure" (✗), score 0,
|
|
2481
|
-
// but the command still produces a useful structured result and
|
|
2482
|
-
// saves the synthesised restatement to the card.
|
|
2483
|
-
result = {
|
|
2484
|
-
score: 0,
|
|
2485
|
-
criteria: Object.fromEntries(Object.keys(ai.feynman.CRITERIA).map(k => [k, false])),
|
|
2486
|
-
explanations: { noLlm: llmGate.message },
|
|
2487
|
-
suggestions: [
|
|
2488
|
-
'No LLM configured — every criterion shows ✗. Run `kdna-studio llm config` and re-evaluate for a real score.',
|
|
2489
|
-
'Or set the synthesised restatement via: kdna-studio card update <project> ' + cardId + ' --field feynman_restatement=\'{"text":"<your own restatement>"}\'',
|
|
2490
|
-
],
|
|
2491
|
-
};
|
|
2492
|
-
} else {
|
|
2493
|
-
result = await ai.feynman.evaluate(llmGate.cfg, card, {});
|
|
2494
|
-
}
|
|
2495
|
-
const score = result.score || 0;
|
|
2496
|
-
const passed = score >= 4;
|
|
2497
|
-
|
|
2498
|
-
if (useJson) {
|
|
2499
|
-
console.log(JSON.stringify({
|
|
2500
|
-
card_id: cardId,
|
|
2501
|
-
score,
|
|
2502
|
-
passed,
|
|
2503
|
-
no_llm: !!llmGate.noLlm,
|
|
2504
|
-
criteria: result.criteria || {},
|
|
2505
|
-
suggestions: result.suggestions || [],
|
|
2506
|
-
feynman_restatement: card.feynman_restatement,
|
|
2507
|
-
}, null, 2));
|
|
2508
|
-
} else {
|
|
2509
|
-
console.log(`Score: ${score}/5 ${passed ? '✓ publishable' : '✗ below threshold (need 4/5)'}${llmGate.noLlm ? ' (--no-llm: static result)' : ''}`);
|
|
2510
|
-
for (const [criterion, desc] of Object.entries(ai.feynman.CRITERIA)) {
|
|
2511
|
-
const r = (result.criteria || {})[criterion] ? '✓' : '✗';
|
|
2512
|
-
console.log(` ${r} ${criterion}`);
|
|
2513
|
-
}
|
|
2514
|
-
if (result.suggestions && result.suggestions.length > 0) {
|
|
2515
|
-
console.log('\nSuggestions:');
|
|
2516
|
-
result.suggestions.forEach(s => console.log(` - ${s}`));
|
|
2517
|
-
}
|
|
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
|
+
);
|
|
2518
2950
|
}
|
|
2519
|
-
|
|
2520
|
-
card.feynman_evaluation = { score, criteria: result.criteria, suggestions: result.suggestions, evaluated_at: new Date().toISOString() };
|
|
2521
|
-
project.cards[cardIdx] = card;
|
|
2522
2951
|
writeProject(projectPath, project);
|
|
2523
2952
|
}
|
|
2524
2953
|
|
|
2525
|
-
async function cmdTest(args) {
|
|
2526
|
-
const projectInput = args[0];
|
|
2527
|
-
const input = option(args, '--input');
|
|
2528
|
-
const preset = option(args, '--preset', 'baseline');
|
|
2529
|
-
if (!projectInput || !input) fail('Usage: kdna-studio test <project> --input "<text>" [--preset baseline|edge|contradiction]');
|
|
2530
|
-
const { project } = readProject(projectInput);
|
|
2531
|
-
const domainName = project.name || 'Untitled Project';
|
|
2532
|
-
const domainPrompt = `Domain: ${domainName}\n${project.description || ''}`;
|
|
2533
|
-
|
|
2534
|
-
// Bug (#50): prior version hand-rolled the prompt for the 3 preset
|
|
2535
|
-
// names and bypassed ai.testlab.testPreset entirely. The exported
|
|
2536
|
-
// testPreset function was dead code, and the help text advertised
|
|
2537
|
-
// names (baseline / edge / contradiction) that did not match the
|
|
2538
|
-
// actual preset keys in testlab.js (baseline / edge_case /
|
|
2539
|
-
// contradiction). Result: --preset edge silently ran the
|
|
2540
|
-
// contradiction branch, and any caller that asked for "edge_case"
|
|
2541
|
-
// fell through to the contradiction branch too.
|
|
2542
|
-
//
|
|
2543
|
-
// The fix routes the call through testPreset when a known preset is
|
|
2544
|
-
// given, and accepts both help-text names (baseline / edge /
|
|
2545
|
-
// contradiction) and the canonical testlab names (baseline / edge_case
|
|
2546
|
-
// / contradiction).
|
|
2547
|
-
const PRESET_ALIASES = {
|
|
2548
|
-
baseline: 'baseline',
|
|
2549
|
-
edge: 'edge_case',
|
|
2550
|
-
edge_case: 'edge_case',
|
|
2551
|
-
contradiction: 'contradiction',
|
|
2552
|
-
};
|
|
2553
|
-
const canonical = PRESET_ALIASES[preset] || 'baseline';
|
|
2554
|
-
console.log(`Testing: ${domainName} [${preset}]`);
|
|
2555
|
-
|
|
2556
|
-
// Bug (#1 UX): gate LLM-requiring paths through cmdNeedsLlm.
|
|
2557
|
-
// The test command always needs an LLM (it compares without/with
|
|
2558
|
-
// the loaded domain). --no-llm gives a static "no-op" comparison
|
|
2559
|
-
// that lists the cards that *would* be applied.
|
|
2560
|
-
const llmGate = cmdNeedsLlm(args, 'test');
|
|
2561
|
-
const cfg = llmGate.noLlm ? null : llmGate.cfg;
|
|
2562
|
-
|
|
2563
|
-
if (canonical === 'baseline') {
|
|
2564
|
-
// Baseline is a single comparison: keep the prior single-result
|
|
2565
|
-
// shape so existing parsers / consumers see the same output.
|
|
2566
|
-
const prompt = `Test the core judgment: ${input}`;
|
|
2567
|
-
if (cfg === null) {
|
|
2568
|
-
// No-LLM static result
|
|
2569
|
-
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 || '' }));
|
|
2570
|
-
console.log(JSON.stringify({ no_llm: true, message: llmGate.message, would_apply: cards }, null, 2));
|
|
2571
|
-
return;
|
|
2572
|
-
}
|
|
2573
|
-
const result = await ai.testlab.compare(cfg, domainName, prompt, domainPrompt, {});
|
|
2574
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
// For non-baseline presets, run the preset + the baseline so the user
|
|
2578
|
-
// can see both side by side. The preset prompt is the one in
|
|
2579
|
-
// ai/testlab.js — that's the code path that was dead before.
|
|
2580
|
-
if (cfg === null) {
|
|
2581
|
-
console.log(JSON.stringify({ no_llm: true, message: llmGate.message }, null, 2));
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
const result = await ai.testlab.testPreset(cfg, domainName, input, domainPrompt, {});
|
|
2585
|
-
console.log(JSON.stringify(result[canonical] || result, null, 2));
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
2954
|
const args = process.argv.slice(2);
|
|
2589
2955
|
const cmd = args[0];
|
|
2590
2956
|
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
@@ -2598,7 +2964,23 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
2598
2964
|
|
|
2599
2965
|
(async () => {
|
|
2600
2966
|
try {
|
|
2601
|
-
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));
|
|
2602
2984
|
else if (cmd === 'migrate') cmdMigrate(args.slice(1));
|
|
2603
2985
|
else if (cmd === 'import') cmdImport(args.slice(1));
|
|
2604
2986
|
else if (cmd === 'filter') cmdFilter(args.slice(1));
|
|
@@ -2607,15 +2989,12 @@ try {
|
|
|
2607
2989
|
else if (cmd === 'distill') await cmdDistill(args.slice(1));
|
|
2608
2990
|
else if (cmd === 'candidate') cmdCandidate(args.slice(1));
|
|
2609
2991
|
else if (cmd === 'interview') await cmdInterview(args.slice(1));
|
|
2610
|
-
else if (cmd === 'feynman') await cmdFeynman(args.slice(1));
|
|
2611
|
-
else if (cmd === 'test') await cmdTest(args.slice(1));
|
|
2612
2992
|
else if (cmd === 'card') cmdCard(args.slice(1));
|
|
2613
2993
|
else if (cmd === 'lock') cmdLock(args.slice(1));
|
|
2614
2994
|
else if (cmd === 'compile') cmdCompile(args.slice(1));
|
|
2615
2995
|
else if (cmd === 'export') cmdExport(args.slice(1));
|
|
2616
2996
|
else if (cmd === 'llm') cmdLlm(args.slice(1));
|
|
2617
2997
|
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
2618
|
-
else if (cmd === 'report') cmdReport(args.slice(1));
|
|
2619
2998
|
else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
|
|
2620
2999
|
else if (cmd === 'install') cmdStudioInstall(args.slice(1));
|
|
2621
3000
|
else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
|