@aikdna/kdna-studio-cli 0.11.0 → 0.13.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 +44 -269
- package/bin/kdna-studio.js +6 -3074
- package/docs/CREATION_COMMAND_CONTRACT.md +18 -223
- package/docs/TERMINAL_AGENT_CREATION.md +23 -206
- package/package.json +32 -29
- package/src/component-operations.js +13 -0
- package/src/public-bindings.json +8806 -0
- package/src/terminal-workspace.js +507 -0
- package/src/ai/distill.js +0 -96
- package/src/ai/filter.js +0 -70
- package/src/ai/index.js +0 -4
- package/src/ai/interview.js +0 -170
- package/src/application-host.js +0 -561
- package/src/creation-cli.js +0 -7721
- package/src/llm/config.js +0 -98
- package/src/llm/index.js +0 -50
- package/src/llm/providers/anthropic.js +0 -41
- package/src/llm/providers/openai.js +0 -42
- package/src/llm/transport.js +0 -196
package/bin/kdna-studio.js
CHANGED
|
@@ -1,3075 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const cbor = require('cbor-x');
|
|
9
|
-
const { execFileSync } = require('child_process');
|
|
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
|
-
|
|
42
|
-
function loadStudioCore() {
|
|
43
|
-
const publishedCore = require('@aikdna/kdna-studio-core');
|
|
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
|
-
}
|
|
62
|
-
try {
|
|
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.
|
|
107
|
-
}
|
|
108
|
-
return publishedCore;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const {
|
|
112
|
-
project: projectApi,
|
|
113
|
-
cards: cardApi,
|
|
114
|
-
evidence: evidenceApi,
|
|
115
|
-
compile: compileApi,
|
|
116
|
-
creator: creatorApi,
|
|
117
|
-
distillation: distillationApi,
|
|
118
|
-
exportRuntime,
|
|
119
|
-
creationExportRuntime,
|
|
120
|
-
creationStudioCoreCoordinate,
|
|
121
|
-
protocolContract,
|
|
122
|
-
creationEngine,
|
|
123
|
-
} = loadStudioCore();
|
|
124
|
-
|
|
125
|
-
const llm = require('../src/llm');
|
|
126
|
-
|
|
127
|
-
const ai = require('../src/ai');
|
|
128
|
-
const {
|
|
129
|
-
CREATION_COMMANDS,
|
|
130
|
-
decodeSecretTransport,
|
|
131
|
-
executeCreationCommand,
|
|
132
|
-
} = require('../src/creation-cli');
|
|
133
|
-
|
|
134
|
-
const EXIT = { OK: 0, INPUT_ERROR: 2, HUMAN_LOCK_REQUIRED: 4, TRUST_FAILED: 5 };
|
|
135
|
-
const SECRET_STDIN_LIMIT_BYTES = 64 * 1024;
|
|
136
|
-
|
|
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
|
-
}
|
|
160
|
-
|
|
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:
|
|
187
|
-
kdna-studio llm config [--provider <name>] [--model <name>] [--key-pipe] [--url <base-url>]
|
|
188
|
-
kdna-studio llm show
|
|
189
|
-
(pipe the key to 'kdna-studio llm config --provider openai --key-pipe --model gpt-4'
|
|
190
|
-
to enable distill --ai and interview.)
|
|
191
|
-
|
|
192
|
-
Identity:
|
|
193
|
-
kdna-studio identity init [--name <display-name>] [--passphrase-stdin]
|
|
194
|
-
kdna-studio identity show
|
|
195
|
-
|
|
196
|
-
Create (three entry paths):
|
|
197
|
-
kdna-studio create <project-dir> --name <@scope/name> [--author <name>] [--use-local-identity] # blank
|
|
198
|
-
kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
|
|
199
|
-
kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
|
|
200
|
-
|
|
201
|
-
Migrate (dev source or Studio project → canonical .kdna in one command):
|
|
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]
|
|
203
|
-
kdna-studio migrate <source-dir> --check --name <@scope/name> # pre-flight: report which fields would block the export without writing the .kdna
|
|
204
|
-
kdna-studio audit-locks <project> [--type axiom|risk|stance|...] [--json] # list cards with missing Human Lock fields (per-card-type, per-field)
|
|
205
|
-
|
|
206
|
-
Authoring:
|
|
207
|
-
kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
|
|
208
|
-
kdna-studio filter <project> # check evidence for sensitive content
|
|
209
|
-
kdna-studio source <project> # classify imported evidence against declared target
|
|
210
|
-
kdna-studio card list <project>
|
|
211
|
-
kdna-studio card add <project> <type> --field key=value [--field key=value] [--template <name>] [--no-strict]
|
|
212
|
-
kdna-studio card update <project> <card-id> --field key=value
|
|
213
|
-
kdna-studio card remove <project> <card-id>
|
|
214
|
-
kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign] [--passphrase-stdin]
|
|
215
|
-
kdna-studio card unlock <project> <card-id> --by <id> --statement <text>
|
|
216
|
-
kdna-studio compile <project> --out <dir>
|
|
217
|
-
kdna-studio export <project> --out <file.kdna> [--allow-incomplete] [--password-stdin]
|
|
218
|
-
kdna-studio export-route-card <domain-id> [--out=<path>] # Export route-card sidecar skeleton
|
|
219
|
-
kdna-studio export-consumer-index [--entries=<domain-ids>] [--out=<path>] # Export consumer-index skeleton
|
|
220
|
-
|
|
221
|
-
AI Authoring (requires LLM config: kdna-studio llm config):
|
|
222
|
-
kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
|
|
223
|
-
kdna-studio distill <project> --candidates <file.json> # load pre-generated AI candidates
|
|
224
|
-
kdna-studio interview <project> [--stage <name>] # 4-stage guided AI interview
|
|
225
|
-
|
|
226
|
-
Distillation:
|
|
227
|
-
kdna-studio target declare <project> # declare distillation target interactively
|
|
228
|
-
kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task> [--include <area,area>] [--exclude <area,area>] [--load-condition <condition>]
|
|
229
|
-
kdna-studio target show <project> # show current distillation target
|
|
230
|
-
kdna-studio candidate list <project> # list candidates with scope and status
|
|
231
|
-
kdna-studio candidate accept <project> <candidate-id>
|
|
232
|
-
kdna-studio candidate reject <project> <candidate-id>
|
|
233
|
-
kdna-studio candidate override <project> <candidate-id> # override scope gate
|
|
234
|
-
kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
|
|
235
|
-
kdna-studio audit-locks <project> # list all axioms/risk/stance missing Human Lock fields
|
|
236
|
-
|
|
237
|
-
Project may be a directory containing studio.project.json or a project JSON file.`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function fail(message, code = EXIT.INPUT_ERROR) {
|
|
241
|
-
console.error(`Error: ${message}`);
|
|
242
|
-
process.exit(code);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Bug (#65): `fail()` calls `process.exit`, which skips `finally`
|
|
246
|
-
// blocks in the same call stack. Any tmpDir that a `try { ... }
|
|
247
|
-
// finally { fs.rmSync(tmpDir) }` block is in the middle of running
|
|
248
|
-
// will leak. The fix tracks every temporary directory the process
|
|
249
|
-
// creates and removes it on process exit (including the exit path
|
|
250
|
-
// triggered by `fail()`).
|
|
251
|
-
const _tempDirs = new Set();
|
|
252
|
-
function trackTempDir(dir) {
|
|
253
|
-
if (dir) _tempDirs.add(dir);
|
|
254
|
-
return dir;
|
|
255
|
-
}
|
|
256
|
-
function cleanupTempDirs() {
|
|
257
|
-
for (const dir of _tempDirs) {
|
|
258
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
259
|
-
}
|
|
260
|
-
_tempDirs.clear();
|
|
261
|
-
}
|
|
262
|
-
process.on('exit', cleanupTempDirs);
|
|
263
|
-
process.on('SIGINT', () => { cleanupTempDirs(); process.exit(130); });
|
|
264
|
-
process.on('SIGTERM', () => { cleanupTempDirs(); process.exit(143); });
|
|
265
|
-
|
|
266
|
-
function option(args, name, fallback = null) {
|
|
267
|
-
const idx = args.indexOf(name);
|
|
268
|
-
if (idx < 0) return fallback;
|
|
269
|
-
const value = args[idx + 1];
|
|
270
|
-
if (!value || value.startsWith('--')) fail(`Missing value for ${name}`);
|
|
271
|
-
return value;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Resolve a passphrase from stdin. Secrets in argv are rejected.
|
|
276
|
-
*
|
|
277
|
-
* Returns the passphrase string, or null if none was provided.
|
|
278
|
-
*/
|
|
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
|
-
}
|
|
289
|
-
if (args.includes('--passphrase-stdin')) {
|
|
290
|
-
if (process.stdin.isTTY) {
|
|
291
|
-
fail(
|
|
292
|
-
'--passphrase-stdin requires the passphrase to be piped in on stdin.\n' +
|
|
293
|
-
'Example: read -r -s PW && printf "%s\\n" "$PW" | kdna-studio export ... --sign --passphrase-stdin'
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
return readBoundedSecretStdin('Passphrase');
|
|
298
|
-
} catch (e) {
|
|
299
|
-
fail(`Could not read passphrase from stdin: ${e.message}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function resolveApiKey(args) {
|
|
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
|
-
}
|
|
313
|
-
|
|
314
|
-
if (args.includes('--key-pipe')) {
|
|
315
|
-
if (process.stdin.isTTY) {
|
|
316
|
-
console.error('Error: --key-pipe requires the API key to be piped on stdin.');
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
return readBoundedSecretStdin('API key');
|
|
321
|
-
} catch (e) {
|
|
322
|
-
console.error('Error reading API key from stdin:', e.message);
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function optionsAll(args, name) {
|
|
331
|
-
const values = [];
|
|
332
|
-
for (let i = 0; i < args.length; i++) {
|
|
333
|
-
if (args[i] === name) {
|
|
334
|
-
const value = args[i + 1];
|
|
335
|
-
if (!value || value.startsWith('--')) fail(`Missing value for ${name}`);
|
|
336
|
-
values.push(value);
|
|
337
|
-
i++;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return values;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function resolveProjectPath(input) {
|
|
344
|
-
if (!input) fail('Project path required');
|
|
345
|
-
const abs = path.resolve(input);
|
|
346
|
-
return fs.existsSync(abs) && fs.statSync(abs).isDirectory()
|
|
347
|
-
? path.join(abs, 'studio.project.json')
|
|
348
|
-
: abs;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function readProject(input) {
|
|
352
|
-
const projectPath = resolveProjectPath(input);
|
|
353
|
-
if (!fs.existsSync(projectPath)) fail(`Project not found: ${projectPath}`);
|
|
354
|
-
return { projectPath, project: projectApi.loadProject(fs.readFileSync(projectPath, 'utf8')) };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function writeProject(projectPath, project) {
|
|
358
|
-
fs.mkdirSync(path.dirname(projectPath), { recursive: true });
|
|
359
|
-
fs.writeFileSync(projectPath, projectApi.saveProject(project));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function writeFiles(outDir, files) {
|
|
363
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
364
|
-
for (const [name, content] of Object.entries(files)) {
|
|
365
|
-
const target = path.join(outDir, name);
|
|
366
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
367
|
-
fs.writeFileSync(target, content);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const ARRAY_FIELD_NAMES = new Set([
|
|
372
|
-
'applies_when',
|
|
373
|
-
'does_not_apply_when',
|
|
374
|
-
'acceptable_exceptions',
|
|
375
|
-
]);
|
|
376
|
-
|
|
377
|
-
function parseFields(args) {
|
|
378
|
-
const fields = {};
|
|
379
|
-
for (const pair of optionsAll(args, '--field')) {
|
|
380
|
-
const eq = pair.indexOf('=');
|
|
381
|
-
if (eq < 1) fail(`Invalid --field "${pair}". Use key=value.`);
|
|
382
|
-
let key = pair.slice(0, eq);
|
|
383
|
-
const appendArray = key.endsWith('[]');
|
|
384
|
-
if (appendArray) key = key.slice(0, -2);
|
|
385
|
-
const raw = pair.slice(eq + 1);
|
|
386
|
-
let value;
|
|
387
|
-
if (raw.startsWith('[') || raw.startsWith('{')) {
|
|
388
|
-
try {
|
|
389
|
-
value = JSON.parse(raw);
|
|
390
|
-
} catch (_) {
|
|
391
|
-
value = raw;
|
|
392
|
-
}
|
|
393
|
-
} else if (raw.includes('|')) {
|
|
394
|
-
value = raw.split('|').map((s) => s.trim()).filter(Boolean);
|
|
395
|
-
} else {
|
|
396
|
-
value = raw;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (appendArray) {
|
|
400
|
-
const values = Array.isArray(value) ? value : [value];
|
|
401
|
-
fields[key] = [...(Array.isArray(fields[key]) ? fields[key] : []), ...values];
|
|
402
|
-
} else if (ARRAY_FIELD_NAMES.has(key) && typeof value === 'string') {
|
|
403
|
-
fields[key] = value.trim() ? [value.trim()] : [];
|
|
404
|
-
} else {
|
|
405
|
-
fields[key] = value;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return fields;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function semverValue(value, fallback = '1.0.0') {
|
|
412
|
-
const raw = String(value || '').trim();
|
|
413
|
-
if (/^[0-9]+\.[0-9]+\.[0-9]+([+-].+)?$/.test(raw)) return raw;
|
|
414
|
-
const twoPart = raw.match(/^([0-9]+)\.([0-9]+)$/);
|
|
415
|
-
if (twoPart) return `${twoPart[1]}.${twoPart[2]}.0`;
|
|
416
|
-
return fallback;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function isoDateTime(value) {
|
|
420
|
-
if (!value) return new Date().toISOString();
|
|
421
|
-
const raw = String(value);
|
|
422
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return `${raw}T00:00:00.000Z`;
|
|
423
|
-
const d = new Date(raw);
|
|
424
|
-
return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString();
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function slugSegment(value, fallback = 'asset') {
|
|
428
|
-
const s = String(value || fallback)
|
|
429
|
-
.trim()
|
|
430
|
-
.replace(/^@/, '')
|
|
431
|
-
.replace(/[^a-zA-Z0-9_.-]+/g, '_')
|
|
432
|
-
.replace(/^_+|_+$/g, '');
|
|
433
|
-
return s || fallback;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function assetIdFromName(name) {
|
|
437
|
-
const raw = String(name || '').trim();
|
|
438
|
-
if (/^[a-zA-Z][a-zA-Z0-9_-]*(:[a-zA-Z0-9_.-]+)+$/.test(raw)) return raw;
|
|
439
|
-
const scoped = raw.match(/^@?([^/:\s]+)\/([^/:\s]+)$/);
|
|
440
|
-
if (scoped) return `kdna:${slugSegment(scoped[1], 'scope')}:${slugSegment(scoped[2], 'asset')}`;
|
|
441
|
-
const parts = raw.split(':').filter(Boolean);
|
|
442
|
-
if (parts.length >= 2) return `kdna:${parts.map((p) => slugSegment(p)).join(':')}`;
|
|
443
|
-
return `kdna:studio:${slugSegment(raw, 'asset')}`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function titleFromName(name, assetId) {
|
|
447
|
-
const raw = String(name || '').trim();
|
|
448
|
-
if (raw) return raw.replace(/^@/, '').replace('/', ' / ');
|
|
449
|
-
return assetId;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function packageNameFromManifest(manifest = {}) {
|
|
453
|
-
if (manifest.name) return manifest.name;
|
|
454
|
-
const match = String(manifest.asset_id || '').match(/^kdna:([^:]+):(.+)$/);
|
|
455
|
-
if (match) return `@${match[1]}/${match[2]}`;
|
|
456
|
-
return manifest.title || manifest.asset_id || null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function publicManifestMetadata(raw = {}) {
|
|
460
|
-
const copy = { ...raw };
|
|
461
|
-
delete copy.registry;
|
|
462
|
-
delete copy.quality_badge;
|
|
463
|
-
delete copy.signature;
|
|
464
|
-
delete copy.media_type;
|
|
465
|
-
return copy;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function cleanFields(fields = {}) {
|
|
469
|
-
const cleaned = {};
|
|
470
|
-
for (const [key, value] of Object.entries(fields || {})) {
|
|
471
|
-
if (value !== undefined && value !== null) cleaned[key] = value;
|
|
472
|
-
}
|
|
473
|
-
return cleaned;
|
|
474
|
-
}
|
|
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
|
-
|
|
486
|
-
function importedCard(type, fields = {}, id = null) {
|
|
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`);
|
|
491
|
-
}
|
|
492
|
-
return card;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
|
|
496
|
-
// misunderstanding card in two places — `patterns[]` (the runtime schema's
|
|
497
|
-
// primary card list) and `reasoning.failure_modes[]` (the runtime schema's
|
|
498
|
-
// "what bad judgment looks like" view). Both share the same `id`.
|
|
499
|
-
// Prior version imported both, doubling every misunderstanding on
|
|
500
|
-
// re-import. The dedup is by id: when a card with the same id is
|
|
501
|
-
// pushed twice, the second push merges fields (the failure_modes
|
|
502
|
-
// entry carries a different field shape than patterns; merging
|
|
503
|
-
// keeps the richer failure_risk / applies_when / does_not_apply_when
|
|
504
|
-
// from the patterns entry).
|
|
505
|
-
const _importedIds = new Set();
|
|
506
|
-
function pushImportedCard(cards, type, fields = {}, id = null) {
|
|
507
|
-
if (id && _importedIds.has(id)) {
|
|
508
|
-
// Already imported — merge richer fields into the existing card.
|
|
509
|
-
const existing = cards.find(c => c.id === id);
|
|
510
|
-
if (existing) {
|
|
511
|
-
existing.fields = existing.fields || {};
|
|
512
|
-
for (const [k, v] of Object.entries(fields || {})) {
|
|
513
|
-
if (v !== undefined && v !== null && v !== '' &&
|
|
514
|
-
(existing.fields[k] === undefined || existing.fields[k] === '' ||
|
|
515
|
-
(Array.isArray(existing.fields[k]) && existing.fields[k].length === 0))) {
|
|
516
|
-
existing.fields[k] = v;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
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);
|
|
526
|
-
if (card) cards.push(card);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function resetImportedIds() { _importedIds.clear(); }
|
|
530
|
-
|
|
531
|
-
function decodePayload(bytes, manifest = {}) {
|
|
532
|
-
const raw = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
533
|
-
const encoding = manifest.payload?.encoding || manifest.container?.payload_encoding || null;
|
|
534
|
-
if (encoding && encoding !== 'cbor') {
|
|
535
|
-
throw new Error(`unsupported payload encoding "${encoding}"; current .kdna assets require CBOR`);
|
|
536
|
-
}
|
|
537
|
-
try {
|
|
538
|
-
return cbor.decode(raw);
|
|
539
|
-
} catch (error) {
|
|
540
|
-
throw new Error(`payload.kdnab is not valid CBOR: ${error.message}`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Runtime path: trust the producer (export-runtime) to have already shaped
|
|
545
|
-
// each card's fields. The producer normalises everything into a
|
|
546
|
-
// canonical shape, so we just round-trip the raw object. Bug (#57):
|
|
547
|
-
// the legacy path below explicitly constructs each field. The two
|
|
548
|
-
// paths cannot be unified without losing either runtime per-field
|
|
549
|
-
// guarantees or legacy's defensive defaults — the asymmetry is
|
|
550
|
-
// intentional. Documented here so a future reader does not "fix" it
|
|
551
|
-
// by collapsing one path into the other.
|
|
552
|
-
function cardsFromRuntimePayload(payload) {
|
|
553
|
-
resetImportedIds();
|
|
554
|
-
const cards = [];
|
|
555
|
-
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
556
|
-
pushImportedCard(cards, sourceCard.type, sourceCard.fields || {}, sourceCard.id || null);
|
|
557
|
-
}
|
|
558
|
-
if (cards.length > 0) return cards;
|
|
559
|
-
|
|
560
|
-
for (const ax of (Array.isArray(payload.core?.axioms) ? payload.core?.axioms : [])) {
|
|
561
|
-
pushImportedCard(cards, 'axiom', importedFields(ax), ax.id || null);
|
|
562
|
-
}
|
|
563
|
-
for (const ont of (Array.isArray(payload.core?.ontology) ? payload.core?.ontology : [])) {
|
|
564
|
-
pushImportedCard(cards, 'ontology', importedFields(ont), ont.id || null);
|
|
565
|
-
}
|
|
566
|
-
for (const fw of (Array.isArray(payload.core?.frameworks) ? payload.core?.frameworks : [])) {
|
|
567
|
-
pushImportedCard(cards, 'framework', importedFields(fw), fw.id || null);
|
|
568
|
-
}
|
|
569
|
-
for (const stance of (Array.isArray(payload.core?.stances) ? payload.core?.stances : [])) {
|
|
570
|
-
pushImportedCard(cards, 'stance', importedFields(stance), stance.id || null);
|
|
571
|
-
}
|
|
572
|
-
for (const aesthetic of (Array.isArray(payload.core?.aesthetics) ? payload.core?.aesthetics : [])) {
|
|
573
|
-
pushImportedCard(cards, 'aesthetic', importedFields(aesthetic), aesthetic.id || null);
|
|
574
|
-
}
|
|
575
|
-
for (const boundary of (Array.isArray(payload.core?.boundaries) ? payload.core?.boundaries : [])) {
|
|
576
|
-
pushImportedCard(cards, 'boundary', importedFields(boundary), boundary.id || null);
|
|
577
|
-
}
|
|
578
|
-
// risk_model may be either an array of risk items (legacy) or
|
|
579
|
-
// `{risks: [...]}` (current). Treat both shapes; ignore everything else
|
|
580
|
-
// rather than crashing with "object is not iterable".
|
|
581
|
-
const riskModel = payload.core?.risk_model;
|
|
582
|
-
const riskItems = Array.isArray(riskModel) ? riskModel : (riskModel?.risks || []);
|
|
583
|
-
for (const risk of riskItems) {
|
|
584
|
-
pushImportedCard(cards, 'risk', importedFields(risk), risk.id || null);
|
|
585
|
-
}
|
|
586
|
-
for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
|
|
587
|
-
// Bug (#146): prior version dropped any payload.patterns entry
|
|
588
|
-
// that lacked an explicit `type` field. The runtime schema lists
|
|
589
|
-
// misunderstandings in patterns[] without a `type` discriminator
|
|
590
|
-
// (their shape is `{wrong, correct, key_distinction, why}`),
|
|
591
|
-
// and several exporters omit `type` to keep the on-the-wire
|
|
592
|
-
// shape compact. The fix infers a type from the field set when
|
|
593
|
-
// `type` is missing, and only falls through to "skip" when the
|
|
594
|
-
// entry truly cannot be classified.
|
|
595
|
-
let type = pattern.type;
|
|
596
|
-
let fields = pattern;
|
|
597
|
-
if (!type) {
|
|
598
|
-
// Heuristics, in priority order — most specific to most general.
|
|
599
|
-
if ('logic' in pattern && ('so_what' in pattern || 'conclusion' in pattern)) type = 'reasoning';
|
|
600
|
-
else if ('wrong' in pattern && 'correct' in pattern) type = 'misunderstanding';
|
|
601
|
-
else if ('term' in pattern && 'definition' in pattern) type = 'term';
|
|
602
|
-
else if ('term' in pattern && 'why' in pattern && 'replace_with' in pattern) type = 'banned_term';
|
|
603
|
-
else if ('what_it_looks_like' in pattern && 'how_to_fix' in pattern) type = 'pattern';
|
|
604
|
-
else if ('name' in pattern && ('description' in pattern || 'one_sentence' in pattern)) type = 'aesthetic';
|
|
605
|
-
else {
|
|
606
|
-
// Truly unclassifiable. Log a one-line warning and skip
|
|
607
|
-
// rather than crash, so the rest of the asset still imports.
|
|
608
|
-
console.warn(
|
|
609
|
-
`Skipping payload.patterns entry with no recognisable type ` +
|
|
610
|
-
`(id=${pattern.id || '?'}, keys=${Object.keys(pattern).join(',') || '<empty>'})`
|
|
611
|
-
);
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
fields = importedFields(pattern);
|
|
615
|
-
} else {
|
|
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';
|
|
625
|
-
}
|
|
626
|
-
pushImportedCard(cards, type, fields, pattern.id || null);
|
|
627
|
-
}
|
|
628
|
-
for (const scenario of (Array.isArray(payload.scenarios) ? payload.scenarios : [])) {
|
|
629
|
-
pushImportedCard(cards, 'scenario', importedFields(scenario), scenario.id || null);
|
|
630
|
-
}
|
|
631
|
-
for (const item of (Array.isArray(payload.cases) ? payload.cases : [])) {
|
|
632
|
-
pushImportedCard(cards, 'case', importedFields(item), item.id || null);
|
|
633
|
-
}
|
|
634
|
-
// The current payload contract has one self-check field. Refuse to infer a
|
|
635
|
-
// parallel field name: Core validation owns format migration, while Studio
|
|
636
|
-
// imports only the current validated payload shape.
|
|
637
|
-
const rawSelfCheck = payload.reasoning?.self_check;
|
|
638
|
-
const selfCheckArr = Array.isArray(rawSelfCheck) ? rawSelfCheck : [];
|
|
639
|
-
for (const selfCheck of selfCheckArr) {
|
|
640
|
-
const fields = typeof selfCheck === 'string' ? { question: selfCheck } : importedFields(selfCheck);
|
|
641
|
-
pushImportedCard(cards, 'self_check', fields, typeof selfCheck === 'object' ? selfCheck.id || null : null);
|
|
642
|
-
}
|
|
643
|
-
for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
|
|
644
|
-
// Bug (#145 follow-up): the prior version dropped three
|
|
645
|
-
// judgment fields (failure_risk, applies_when, does_not_apply_when)
|
|
646
|
-
// when reading failure_modes back out of an asset. A Studio
|
|
647
|
-
// project that round-tripped through `migrate` lost
|
|
648
|
-
// these fields every time. The fix reads them through, matching
|
|
649
|
-
// the export-side map in kdna-studio-core's buildPayload.
|
|
650
|
-
const fields = {
|
|
651
|
-
...importedFields(failureMode),
|
|
652
|
-
wrong: failureMode.mode || failureMode.wrong || '',
|
|
653
|
-
correct: failureMode.correct || '',
|
|
654
|
-
key_distinction: failureMode.key_distinction || '',
|
|
655
|
-
why: failureMode.why || '',
|
|
656
|
-
failure_risk: failureMode.failure_risk || '',
|
|
657
|
-
applies_when: Array.isArray(failureMode.applies_when) ? failureMode.applies_when : [],
|
|
658
|
-
does_not_apply_when: Array.isArray(failureMode.does_not_apply_when) ? failureMode.does_not_apply_when : [],
|
|
659
|
-
};
|
|
660
|
-
delete fields.mode;
|
|
661
|
-
pushImportedCard(cards, 'misunderstanding', fields, failureMode.id || null);
|
|
662
|
-
}
|
|
663
|
-
for (const chain of (Array.isArray(payload.reasoning?.reasoning_chains) ? payload.reasoning?.reasoning_chains : [])) {
|
|
664
|
-
// Bug (#4 UX): compile/index.js synthesises one reasoning chain
|
|
665
|
-
// per locked axiom (the legacy 1.0.0 path) and tags it
|
|
666
|
-
// `source_authored: false`. Importing those synthesised chains
|
|
667
|
-
// doubled the card count on round-trip. The fix skips chains
|
|
668
|
-
// that the producer explicitly marked as synthesised.
|
|
669
|
-
if (chain.source_authored === false) continue;
|
|
670
|
-
pushImportedCard(cards, 'reasoning', importedFields(chain), chain.id || null);
|
|
671
|
-
}
|
|
672
|
-
for (const stage of (Array.isArray(payload.evolution?.stages) ? payload.evolution?.stages : [])) {
|
|
673
|
-
// Distinguish source-authored stages (preserved identity) from
|
|
674
|
-
// audit-log-derived stages (synthesised). The compile path tags
|
|
675
|
-
// source_authored: true; import those verbatim and skip the rest
|
|
676
|
-
// so the importer doesn't double-count what was already in the
|
|
677
|
-
// locked judgment cards.
|
|
678
|
-
if (stage.source_authored === true) {
|
|
679
|
-
pushImportedCard(cards, 'evolution_stage', importedFields(stage), stage.id || null);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
return cards;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function cardsFromPayload(payload) {
|
|
686
|
-
if (!payload || typeof payload !== 'object') return [];
|
|
687
|
-
if (
|
|
688
|
-
payload.profile === protocolContract.PAYLOAD_PROFILE &&
|
|
689
|
-
payload.profile_version === protocolContract.PAYLOAD_PROFILE_VERSION
|
|
690
|
-
) {
|
|
691
|
-
return cardsFromRuntimePayload(payload);
|
|
692
|
-
}
|
|
693
|
-
return [];
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function judgmentCoreFromRuntimePayload(payload) {
|
|
697
|
-
const core = payload?.core;
|
|
698
|
-
if (!core || typeof core !== 'object' || Array.isArray(core)) return null;
|
|
699
|
-
const declared = {};
|
|
700
|
-
for (const field of ['highest_question', 'worldview', 'value_order', 'judgment_role']) {
|
|
701
|
-
if (Object.hasOwn(core, field)) {
|
|
702
|
-
declared[field] = JSON.parse(JSON.stringify(core[field]));
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
return Object.keys(declared).length > 0 ? declared : null;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function buildManifest(project, name) {
|
|
709
|
-
// Bug (#58): prior version was a hand-rolled manifest builder that
|
|
710
|
-
// shared zero logic with the canonical buildManifest in
|
|
711
|
-
// @aikdna/kdna-studio-core/src/export-runtime. The two paths
|
|
712
|
-
// diverged on every field added since 1.0 (lineage, load_contract,
|
|
713
|
-
// authoring block, etc.). The fix delegates the manifest shape to
|
|
714
|
-
// kdna-studio-core's buildManifest so runtime export has a single
|
|
715
|
-
// source of truth.
|
|
716
|
-
//
|
|
717
|
-
// We still need a default manifest here (the exportRuntimeAsset
|
|
718
|
-
// path uses buildManifest internally; this function supplies the
|
|
719
|
-
// defaults that pass-through into the runtime options). The actual
|
|
720
|
-
// manifest the runtime ships is the one buildManifest produces, so
|
|
721
|
-
// keeping these defaults in sync matters less than it used to.
|
|
722
|
-
const source = project.source_manifest || {};
|
|
723
|
-
const assetId = assetIdFromName(name || source.name || project.name);
|
|
724
|
-
const author = source.author || project.author || {};
|
|
725
|
-
return {
|
|
726
|
-
format_version: protocolContract.FORMAT_VERSION,
|
|
727
|
-
asset_id: assetId,
|
|
728
|
-
asset_uid: project.asset_uid || source.asset_uid || 'urn:uuid:' + crypto.randomUUID(),
|
|
729
|
-
asset_type: project.type === 'cluster' ? 'cluster' : 'domain',
|
|
730
|
-
source_asset_type: source.asset_type || null,
|
|
731
|
-
title: source.title || project.title || titleFromName(source.name || name || project.name, assetId),
|
|
732
|
-
version: semverValue(project.release?.version || source.version, '1.0.0'),
|
|
733
|
-
judgment_version: semverValue(project.release?.judgment_version || source.judgment_version || source.version, '1.0.0'),
|
|
734
|
-
created_at: isoDateTime(source.created_at || source.created || project.created),
|
|
735
|
-
updated_at: isoDateTime(source.updated_at || source.updated || project.updated),
|
|
736
|
-
creator: {
|
|
737
|
-
name: author.name || project.author?.name || 'Studio Export',
|
|
738
|
-
id: author.id || project.author?.id || undefined,
|
|
739
|
-
},
|
|
740
|
-
compatibility: {
|
|
741
|
-
min_loader_version: '0.19.0',
|
|
742
|
-
profile: protocolContract.PAYLOAD_PROFILE,
|
|
743
|
-
profile_version: protocolContract.PAYLOAD_PROFILE_VERSION,
|
|
744
|
-
},
|
|
745
|
-
payload: { path: 'payload.kdnab', encoding: 'cbor', encrypted: false },
|
|
746
|
-
summary: source.core_insight || project.release?.description || source.description || '',
|
|
747
|
-
description: source.description || project.release?.description || '',
|
|
748
|
-
language: source.default_language || project.default_language || (Array.isArray(source.languages) ? source.languages[0] : undefined),
|
|
749
|
-
languages: source.languages || project.languages || undefined,
|
|
750
|
-
license: source.license || project.license || undefined,
|
|
751
|
-
keywords: source.keywords || [],
|
|
752
|
-
lineage: {
|
|
753
|
-
type: project.lineage?.type === 'migrated' ? 'adaptation' : (project.lineage?.type || 'original'),
|
|
754
|
-
fork_of: null,
|
|
755
|
-
derived_from: source.name || project.lineage?.parent_name || null,
|
|
756
|
-
source_lineage_type: project.lineage?.type || null,
|
|
757
|
-
},
|
|
758
|
-
load_contract: {
|
|
759
|
-
default_profile: 'compact',
|
|
760
|
-
profiles: {
|
|
761
|
-
index: { requires_decryption: false, max_tokens_hint: 500, selection: 'manifest metadata', intended_for: ['discovery'] },
|
|
762
|
-
compact: { requires_decryption: false, max_tokens_hint: 2000, selection: 'core judgment summary', intended_for: ['agent prompt'] },
|
|
763
|
-
scenario: { requires_decryption: false, max_tokens_hint: 3000, selection: 'scenario cards', intended_for: ['situational loading'] },
|
|
764
|
-
full: { requires_decryption: false, max_tokens_hint: 12000, selection: 'full manifest and payload', intended_for: ['audit', 'migration'] },
|
|
765
|
-
},
|
|
766
|
-
},
|
|
767
|
-
source_manifest: publicManifestMetadata(source),
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function cmdCreate(args) {
|
|
772
|
-
const dir = args[0];
|
|
773
|
-
if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--author <name>] [--from-kdna <file> | --from-folder <dir>]');
|
|
774
|
-
const abs = path.resolve(dir);
|
|
775
|
-
if (fs.existsSync(abs)) fail(`Directory already exists: ${abs}`);
|
|
776
|
-
|
|
777
|
-
const name = option(args, '--name', path.basename(abs));
|
|
778
|
-
const authorName = option(args, '--author', '');
|
|
779
|
-
const fromKdna = option(args, '--from-kdna');
|
|
780
|
-
const fromFolder = option(args, '--from-folder');
|
|
781
|
-
|
|
782
|
-
let sourceMode = 'blank';
|
|
783
|
-
let lineage = null;
|
|
784
|
-
let creatorIdentity = null;
|
|
785
|
-
|
|
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
|
-
}
|
|
799
|
-
|
|
800
|
-
if (fromKdna) {
|
|
801
|
-
sourceMode = 'kdna_asset';
|
|
802
|
-
const kdnaData = importFromKdna(fromKdna);
|
|
803
|
-
lineage = kdnaData.lineage;
|
|
804
|
-
if (kdnaData.cards) {
|
|
805
|
-
const sourceAuthor = kdnaData.source_manifest?.author || {};
|
|
806
|
-
const project = projectApi.createProject(name, 'domain', {
|
|
807
|
-
author: {
|
|
808
|
-
name: authorName || option(args, '--author-name', sourceAuthor.name || ''),
|
|
809
|
-
id: option(args, '--author-id', sourceAuthor.id || ''),
|
|
810
|
-
},
|
|
811
|
-
sourceMode,
|
|
812
|
-
creatorIdentity,
|
|
813
|
-
lineage,
|
|
814
|
-
judgmentCore: kdnaData.judgment_core,
|
|
815
|
-
});
|
|
816
|
-
project.cards = kdnaData.cards;
|
|
817
|
-
// Bug #15 / #28 follow-up: store the source KDNA_* files on the
|
|
818
|
-
// project so a later export through exportRuntimeAsset can
|
|
819
|
-
// forward them to compileDomain (which then preserves
|
|
820
|
-
// changelog / version_notes / reasoning_chains / banned_terms
|
|
821
|
-
// through the round-trip).
|
|
822
|
-
if (kdnaData.source_patterns) project.source_patterns = kdnaData.source_patterns;
|
|
823
|
-
if (kdnaData.source_reasoning) project.source_reasoning = kdnaData.source_reasoning;
|
|
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;
|
|
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;
|
|
832
|
-
if (kdnaData.source_manifest?.version) {
|
|
833
|
-
if (!project.release) project.release = {};
|
|
834
|
-
project.release.version = kdnaData.source_manifest.version;
|
|
835
|
-
}
|
|
836
|
-
if (kdnaData.source_manifest?.judgment_version) {
|
|
837
|
-
if (!project.release) project.release = {};
|
|
838
|
-
project.release.judgment_version = kdnaData.source_manifest.judgment_version;
|
|
839
|
-
}
|
|
840
|
-
if (kdnaData.source_manifest?.description || kdnaData.source_manifest?.summary) {
|
|
841
|
-
if (!project.release) project.release = {};
|
|
842
|
-
project.release.description = kdnaData.source_manifest.description || kdnaData.source_manifest.summary;
|
|
843
|
-
}
|
|
844
|
-
if (project.stages?.judgment_cards) {
|
|
845
|
-
project.stages.judgment_cards.total = kdnaData.cards.length;
|
|
846
|
-
project.stages.judgment_cards.status = 'in_progress';
|
|
847
|
-
}
|
|
848
|
-
fs.mkdirSync(abs, { recursive: true });
|
|
849
|
-
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
850
|
-
console.log(`Created Studio project (source_mode: ${sourceMode}, imported: ${kdnaData.cards.length} cards): ${abs}`);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
} else if (fromFolder) {
|
|
854
|
-
sourceMode = 'source_folder';
|
|
855
|
-
importFromFolder(fromFolder, abs, name, creatorIdentity);
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const project = projectApi.createProject(name, 'domain', {
|
|
860
|
-
author: { name: authorName || option(args, '--author-name', ''), id: option(args, '--author-id', '') },
|
|
861
|
-
sourceMode,
|
|
862
|
-
creatorIdentity,
|
|
863
|
-
lineage,
|
|
864
|
-
sourcePath: fromFolder ? path.resolve(fromFolder) : null,
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
if (fromKdna || fromFolder) {
|
|
868
|
-
fs.mkdirSync(abs, { recursive: true });
|
|
869
|
-
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
870
|
-
if (fromFolder) {
|
|
871
|
-
// Cards were already added by importFromFolder
|
|
872
|
-
}
|
|
873
|
-
console.log(`Created Studio project (source_mode: ${sourceMode}): ${abs}`);
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
fs.mkdirSync(abs, { recursive: true });
|
|
878
|
-
writeProject(path.join(abs, 'studio.project.json'), project);
|
|
879
|
-
console.log(`Created Studio project: ${abs}`);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Import cards from an existing .kdna asset into a new Studio project.
|
|
884
|
-
* Cards are imported as draft — they do NOT inherit trust from the source.
|
|
885
|
-
*/
|
|
886
|
-
function importFromKdna(kdnaPath) {
|
|
887
|
-
resetImportedIds();
|
|
888
|
-
const absKdna = path.resolve(kdnaPath);
|
|
889
|
-
if (!fs.existsSync(absKdna)) fail(`KDNA asset not found: ${absKdna}`);
|
|
890
|
-
if (!absKdna.endsWith('.kdna')) fail('--from-kdna requires a .kdna file');
|
|
891
|
-
|
|
892
|
-
const stats = fs.statSync(absKdna);
|
|
893
|
-
if (stats.size > 50 * 1024 * 1024) {
|
|
894
|
-
fail(`KDNA file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${absKdna}`);
|
|
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
|
-
|
|
913
|
-
let runtimeCore;
|
|
914
|
-
try { runtimeCore = require('@aikdna/kdna-core'); } catch {
|
|
915
|
-
fail('@aikdna/kdna-core is required to validate --from-kdna imports.');
|
|
916
|
-
}
|
|
917
|
-
const validation = runtimeCore.validate(absKdna);
|
|
918
|
-
if (!validation.overall_valid) {
|
|
919
|
-
fail(`Not a current .kdna asset: ${(validation.problems || []).join('; ')}`);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const lineage = {
|
|
923
|
-
type: 'fork',
|
|
924
|
-
parent_name: packageNameFromManifest(manifest),
|
|
925
|
-
parent_asset_uid: manifest.asset_uid || null,
|
|
926
|
-
parent_version: manifest.version || null,
|
|
927
|
-
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
let imported;
|
|
931
|
-
try {
|
|
932
|
-
imported = importFromRuntimeContent(manifest, payload);
|
|
933
|
-
} catch (e) {
|
|
934
|
-
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (imported.cards.length === 0) {
|
|
938
|
-
fail(`No current judgment cards could be extracted from ${absKdna}.`);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
lineage,
|
|
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
|
-
},
|
|
978
|
-
source_manifest: manifest,
|
|
979
|
-
source_patterns: null,
|
|
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,
|
|
989
|
-
};
|
|
990
|
-
}
|
|
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
|
-
|
|
1199
|
-
/**
|
|
1200
|
-
* Import cards from a legacy source folder into a new Studio project.
|
|
1201
|
-
* Reads KDNA_*.json files and converts entries to draft judgment cards.
|
|
1202
|
-
* Outputs a schema audit report.
|
|
1203
|
-
*/
|
|
1204
|
-
function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, opts = {}) {
|
|
1205
|
-
resetImportedIds();
|
|
1206
|
-
const absSource = path.resolve(sourceDir);
|
|
1207
|
-
if (!fs.existsSync(absSource)) fail(`Source folder not found: ${absSource}`);
|
|
1208
|
-
if (!fs.statSync(absSource).isDirectory()) fail('--from-folder requires a directory');
|
|
1209
|
-
|
|
1210
|
-
const audit = { filesFound: [], cardsImported: 0, missingFields: [], schemaWarnings: [] };
|
|
1211
|
-
const importedCards = [];
|
|
1212
|
-
const manifestData = {};
|
|
1213
|
-
|
|
1214
|
-
function loadJson(filename) {
|
|
1215
|
-
const p = path.join(absSource, filename);
|
|
1216
|
-
if (!fs.existsSync(p)) return null;
|
|
1217
|
-
audit.filesFound.push(filename);
|
|
1218
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
1219
|
-
catch { audit.schemaWarnings.push(`${filename}: invalid JSON`); return null; }
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// Load manifest for version/languages/description
|
|
1223
|
-
const manifest = loadJson('kdna.json');
|
|
1224
|
-
if (manifest) {
|
|
1225
|
-
manifestData.raw = manifest;
|
|
1226
|
-
if (manifest.version) manifestData.version = manifest.version;
|
|
1227
|
-
if (manifest.asset_uid) manifestData.asset_uid = manifest.asset_uid;
|
|
1228
|
-
if (manifest.judgment_version) manifestData.judgment_version = manifest.judgment_version;
|
|
1229
|
-
if (manifest.languages) manifestData.languages = manifest.languages;
|
|
1230
|
-
if (manifest.default_language) manifestData.default_language = manifest.default_language;
|
|
1231
|
-
if (manifest.description) manifestData.description = manifest.description;
|
|
1232
|
-
if (manifest.name) manifestData.name = manifest.name;
|
|
1233
|
-
if (manifest.title) manifestData.title = manifest.title;
|
|
1234
|
-
if (manifest.keywords) manifestData.keywords = manifest.keywords;
|
|
1235
|
-
if (manifest.license) manifestData.license = manifest.license;
|
|
1236
|
-
if (manifest.author) manifestData.author = manifest.author;
|
|
1237
|
-
if (manifest.asset_type) manifestData.asset_type = manifest.asset_type;
|
|
1238
|
-
if (manifest.core_insight) manifestData.core_insight = manifest.core_insight;
|
|
1239
|
-
if (manifest.created_at || manifest.created) manifestData.created = manifest.created_at || manifest.created;
|
|
1240
|
-
if (manifest.updated_at || manifest.updated) manifestData.updated = manifest.updated_at || manifest.updated;
|
|
1241
|
-
// legacy-migration bookkeeping only: preserved in the Studio project for
|
|
1242
|
-
// issuer-scoped governance; the Core 0.20 export manifest never projects it
|
|
1243
|
-
if (manifest.risk_level) manifestData.risk_level = manifest.risk_level;
|
|
1244
|
-
if (manifest.fitness_for_purpose) manifestData.fitness_for_purpose = manifest.fitness_for_purpose;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
const core = loadJson('KDNA_Core.json');
|
|
1248
|
-
const patterns = loadJson('KDNA_Patterns.json');
|
|
1249
|
-
const scenarios = loadJson('KDNA_Scenarios.json');
|
|
1250
|
-
const cases = loadJson('KDNA_Cases.json');
|
|
1251
|
-
const reasoning = loadJson('KDNA_Reasoning.json');
|
|
1252
|
-
const evolution = loadJson('KDNA_Evolution.json');
|
|
1253
|
-
|
|
1254
|
-
// If none of the standard KDNA_*.json files were found, that almost
|
|
1255
|
-
// always means the user pointed at the wrong directory or the source uses
|
|
1256
|
-
// a non-standard filename. Report what was *actually* present so they can
|
|
1257
|
-
// rename or move their files.
|
|
1258
|
-
const expectedFiles = ['KDNA_Core.json', 'KDNA_Patterns.json', 'KDNA_Scenarios.json',
|
|
1259
|
-
'KDNA_Cases.json', 'KDNA_Reasoning.json', 'KDNA_Evolution.json', 'kdna.json'];
|
|
1260
|
-
const loadedCount = audit.filesFound.length;
|
|
1261
|
-
if (loadedCount === 0) {
|
|
1262
|
-
let dirContents = [];
|
|
1263
|
-
try { dirContents = fs.readdirSync(absSource); } catch { /* ignore */ }
|
|
1264
|
-
fail(
|
|
1265
|
-
`No KDNA source files found in ${absSource}.\n` +
|
|
1266
|
-
`Expected one of: ${expectedFiles.join(', ')}\n` +
|
|
1267
|
-
`Directory contains ${dirContents.length} entr${dirContents.length === 1 ? 'y' : 'ies'}: ` +
|
|
1268
|
-
(dirContents.length > 0 ? dirContents.slice(0, 20).join(', ') + (dirContents.length > 20 ? ', ...' : '') : '(empty)')
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
if (core) {
|
|
1273
|
-
for (const ax of (Array.isArray(core.axioms) ? core.axioms : [])) {
|
|
1274
|
-
const fields = importedFields(ax);
|
|
1275
|
-
for (const key of ['one_sentence', 'full_statement', 'why', 'applies_when', 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type']) {
|
|
1276
|
-
if (!(key in ax)) audit.missingFields.push(`axiom.${ax.id || '?'}.${key}`);
|
|
1277
|
-
}
|
|
1278
|
-
importedCards.push(cardApi.createCard('axiom', fields, ax.id || null));
|
|
1279
|
-
}
|
|
1280
|
-
for (const ont of (Array.isArray(core.ontology) ? core.ontology : [])) {
|
|
1281
|
-
importedCards.push(cardApi.createCard('ontology', importedFields(ont), ont.id || null));
|
|
1282
|
-
}
|
|
1283
|
-
for (const b of (Array.isArray(core.boundaries) ? core.boundaries : [])) {
|
|
1284
|
-
importedCards.push(cardApi.createCard('boundary', importedFields(b), b.id || null));
|
|
1285
|
-
}
|
|
1286
|
-
// Accept risks as a top-level array (`risks`) or wrapped in `risk_model`
|
|
1287
|
-
// (either as `{risks: [...]}` or as an array itself).
|
|
1288
|
-
let risksSource = core.risks;
|
|
1289
|
-
if (!Array.isArray(risksSource)) {
|
|
1290
|
-
if (Array.isArray(core.risk_model)) risksSource = core.risk_model;
|
|
1291
|
-
else if (Array.isArray(core.risk_model?.risks)) risksSource = core.risk_model.risks;
|
|
1292
|
-
else risksSource = [];
|
|
1293
|
-
}
|
|
1294
|
-
for (const r of risksSource) {
|
|
1295
|
-
importedCards.push(cardApi.createCard('risk', importedFields(r.fields || r), r.id || null));
|
|
1296
|
-
}
|
|
1297
|
-
// NEW: import stances
|
|
1298
|
-
for (const s of (Array.isArray(core.stances) ? core.stances : [])) {
|
|
1299
|
-
const fields = typeof s === 'string' ? { statement: s } : importedFields(s);
|
|
1300
|
-
importedCards.push(cardApi.createCard('stance', fields, typeof s === 'object' ? s.id || null : null));
|
|
1301
|
-
}
|
|
1302
|
-
// NEW: import frameworks
|
|
1303
|
-
for (const fw of (Array.isArray(core.frameworks) ? core.frameworks : [])) {
|
|
1304
|
-
importedCards.push(cardApi.createCard('framework', importedFields(fw), fw.id || null));
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (patterns) {
|
|
1309
|
-
// NEW: import terminology
|
|
1310
|
-
if (patterns.terminology) {
|
|
1311
|
-
for (const t of (Array.isArray(patterns.terminology.standard_terms) ? patterns.terminology.standard_terms : [])) {
|
|
1312
|
-
importedCards.push(cardApi.createCard('term', importedFields(t), t.id || null));
|
|
1313
|
-
}
|
|
1314
|
-
for (const bt of (Array.isArray(patterns.terminology.banned_terms) ? patterns.terminology.banned_terms : [])) {
|
|
1315
|
-
importedCards.push(cardApi.createCard('banned_term', importedFields(bt), bt.id || null));
|
|
1316
|
-
}
|
|
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
|
-
}
|
|
1323
|
-
for (const ms of (Array.isArray(patterns.misunderstandings) ? patterns.misunderstandings : [])) {
|
|
1324
|
-
importedCards.push(cardApi.createCard('misunderstanding', importedFields(ms), ms.id || null));
|
|
1325
|
-
}
|
|
1326
|
-
for (const sc of (Array.isArray(patterns.self_check) ? patterns.self_check : [])) {
|
|
1327
|
-
const fields = typeof sc === 'string' ? { question: sc } : importedFields(sc);
|
|
1328
|
-
importedCards.push(cardApi.createCard(
|
|
1329
|
-
'self_check',
|
|
1330
|
-
fields,
|
|
1331
|
-
typeof sc === 'object' ? sc.id || null : null,
|
|
1332
|
-
));
|
|
1333
|
-
}
|
|
1334
|
-
// Bug (aesthetic round-trip): prior version dropped
|
|
1335
|
-
// `patterns.aesthetics` on import. `cardsFromRuntimePayload` and
|
|
1336
|
-
// `cardsFromLegacyPayload` both read the field, so a domain
|
|
1337
|
-
// that authored aesthetic cards in either format would lose
|
|
1338
|
-
// them when imported through `create --from-folder`. The fix
|
|
1339
|
-
// adds the missing loop here.
|
|
1340
|
-
for (const aesthetic of (Array.isArray(patterns.aesthetics) ? patterns.aesthetics : [])) {
|
|
1341
|
-
importedCards.push(cardApi.createCard(
|
|
1342
|
-
'aesthetic',
|
|
1343
|
-
importedFields(aesthetic.fields || aesthetic),
|
|
1344
|
-
aesthetic.id || null,
|
|
1345
|
-
));
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// NEW: import scenarios
|
|
1350
|
-
if (scenarios && Array.isArray(scenarios.scenes)) {
|
|
1351
|
-
for (const scene of scenarios.scenes) {
|
|
1352
|
-
importedCards.push(cardApi.createCard('scenario', importedFields(scene), scene.id || null));
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
// NEW: import cases
|
|
1357
|
-
if (cases && Array.isArray(cases.cases)) {
|
|
1358
|
-
for (const c of cases.cases) {
|
|
1359
|
-
importedCards.push(cardApi.createCard('case', importedFields(c), c.id || null));
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// NEW: import reasoning chains. Field names must match what compileReasoning
|
|
1364
|
-
// reads (chain/principle/concrete_action/axiom/one_sentence) so the
|
|
1365
|
-
// round-trip preserves the source's structure. Bug: prior code stored
|
|
1366
|
-
// {name, conclusion, so_what}, none of which the compile path recognised,
|
|
1367
|
-
// so the cards were silently rewritten as axiom-derived fallback chains
|
|
1368
|
-
// — looking identical to misunderstanding cards and losing the source's
|
|
1369
|
-
// identity.
|
|
1370
|
-
if (reasoning && Array.isArray(reasoning.reasoning_chains)) {
|
|
1371
|
-
for (const rc of reasoning.reasoning_chains) {
|
|
1372
|
-
importedCards.push(cardApi.createCard('reasoning', importedFields(rc), rc.id || null));
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// NEW: import evolution stages
|
|
1377
|
-
if (evolution && Array.isArray(evolution.stages)) {
|
|
1378
|
-
for (const stage of evolution.stages) {
|
|
1379
|
-
pushImportedCard(importedCards, 'evolution_stage', importedFields(stage), stage.id || null);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
audit.cardsImported = importedCards.length;
|
|
1384
|
-
|
|
1385
|
-
const project = projectApi.createProject(projectName, 'domain', {
|
|
1386
|
-
sourceMode: 'source_folder',
|
|
1387
|
-
sourcePath: absSource,
|
|
1388
|
-
creatorIdentity: creatorIdentity || null,
|
|
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 || {} }),
|
|
1391
|
-
});
|
|
1392
|
-
// Preserve manifest metadata
|
|
1393
|
-
if (manifestData.asset_uid) project.asset_uid = manifestData.asset_uid;
|
|
1394
|
-
if (manifestData.version) {
|
|
1395
|
-
if (!project.release) project.release = {};
|
|
1396
|
-
project.release.version = manifestData.version;
|
|
1397
|
-
}
|
|
1398
|
-
if (manifestData.judgment_version) {
|
|
1399
|
-
if (!project.release) project.release = {};
|
|
1400
|
-
project.release.judgment_version = manifestData.judgment_version;
|
|
1401
|
-
}
|
|
1402
|
-
if (manifestData.description) {
|
|
1403
|
-
if (!project.release) project.release = {};
|
|
1404
|
-
project.release.description = manifestData.description;
|
|
1405
|
-
}
|
|
1406
|
-
if (manifestData.title) project.title = manifestData.title;
|
|
1407
|
-
if (manifestData.license) project.license = manifestData.license;
|
|
1408
|
-
if (manifestData.author) project.author = manifestData.author;
|
|
1409
|
-
if (manifestData.languages) project.languages = manifestData.languages;
|
|
1410
|
-
if (manifestData.default_language) project.default_language = manifestData.default_language;
|
|
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
|
-
}
|
|
1418
|
-
|
|
1419
|
-
project.cards = importedCards;
|
|
1420
|
-
if (project.stages?.judgment_cards) {
|
|
1421
|
-
project.stages.judgment_cards.total = importedCards.length;
|
|
1422
|
-
project.stages.judgment_cards.status = 'in_progress';
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
fs.mkdirSync(projectDir, { recursive: true });
|
|
1426
|
-
writeProject(path.join(projectDir, 'studio.project.json'), project);
|
|
1427
|
-
if (!opts.silent) {
|
|
1428
|
-
console.log(JSON.stringify({ audit, imported: importedCards.length, source_mode: 'source_folder' }, null, 2));
|
|
1429
|
-
}
|
|
1430
|
-
return { project, projectPath: path.join(projectDir, 'studio.project.json'), audit };
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
/**
|
|
1434
|
-
* Minimal ZIP central directory parser — validates every entry before decompressing.
|
|
1435
|
-
*/
|
|
1436
|
-
function readZipEntries(buf) {
|
|
1437
|
-
const entries = new Map();
|
|
1438
|
-
const metadata = [];
|
|
1439
|
-
|
|
1440
|
-
// Find end-of-central-directory record
|
|
1441
|
-
let eocdOffset = buf.length - 22;
|
|
1442
|
-
while (eocdOffset >= 0) {
|
|
1443
|
-
if (buf.readUInt32LE(eocdOffset) === 0x06054b50) break;
|
|
1444
|
-
eocdOffset--;
|
|
1445
|
-
}
|
|
1446
|
-
if (eocdOffset < 0) throw new Error('Not a valid ZIP file');
|
|
1447
|
-
|
|
1448
|
-
const centralDirOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
1449
|
-
let offset = centralDirOffset;
|
|
1450
|
-
|
|
1451
|
-
// Pass 1: collect metadata, validate safety before any decompression
|
|
1452
|
-
let totalUncomp = 0;
|
|
1453
|
-
while (offset < eocdOffset) {
|
|
1454
|
-
const sig = buf.readUInt32LE(offset);
|
|
1455
|
-
if (sig !== 0x02014b50) break;
|
|
1456
|
-
const nameLen = buf.readUInt16LE(offset + 28);
|
|
1457
|
-
const extraLen = buf.readUInt16LE(offset + 30);
|
|
1458
|
-
const commentLen = buf.readUInt16LE(offset + 32);
|
|
1459
|
-
const compMethod = buf.readUInt16LE(offset + 10);
|
|
1460
|
-
const compSize = buf.readUInt32LE(offset + 20);
|
|
1461
|
-
const uncompSize = buf.readUInt32LE(offset + 24);
|
|
1462
|
-
const localOffset = buf.readUInt32LE(offset + 42);
|
|
1463
|
-
const name = buf.toString('utf8', offset + 46, offset + 46 + nameLen);
|
|
1464
|
-
|
|
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`);
|
|
1468
|
-
}
|
|
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 });
|
|
1485
|
-
offset += 46 + nameLen + extraLen + commentLen;
|
|
1486
|
-
}
|
|
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
|
-
|
|
1506
|
-
return entries;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
const aiFilter = require('../src/ai/filter');
|
|
1510
|
-
|
|
1511
|
-
function cmdImport(args) {
|
|
1512
|
-
const [projectInput, source] = args;
|
|
1513
|
-
if (!projectInput || !source) fail('Usage: kdna-studio import <project> <source-file-or-dir>');
|
|
1514
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1515
|
-
|
|
1516
|
-
const sourcePath = path.resolve(source);
|
|
1517
|
-
if (!fs.existsSync(sourcePath)) fail(`Source not found: ${sourcePath}`);
|
|
1518
|
-
|
|
1519
|
-
const TEXT_EXTS = new Set(['.txt', '.md', '.json', '.jsonl', '.yaml', '.yml', '.csv', '.log', '.srt', '.vtt', '.html', '.xml', '.rst']);
|
|
1520
|
-
const BINARY_EXTS = new Set(['.pdf', '.docx', '.rtf']);
|
|
1521
|
-
let imported = 0;
|
|
1522
|
-
let skipped = 0;
|
|
1523
|
-
|
|
1524
|
-
function extractBinaryText(filePath, ext) {
|
|
1525
|
-
try {
|
|
1526
|
-
if (ext === '.pdf') {
|
|
1527
|
-
const result = execFileSync('pdftotext', ['-raw', filePath, '-'], {
|
|
1528
|
-
encoding: 'utf8',
|
|
1529
|
-
timeout: 30000,
|
|
1530
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1531
|
-
});
|
|
1532
|
-
return { text: result, error: null };
|
|
1533
|
-
}
|
|
1534
|
-
if (ext === '.docx' || ext === '.rtf') {
|
|
1535
|
-
const result = execFileSync('textutil', ['-convert', 'txt', '-stdout', filePath], {
|
|
1536
|
-
encoding: 'utf8',
|
|
1537
|
-
timeout: 30000,
|
|
1538
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1539
|
-
});
|
|
1540
|
-
return { text: result, error: null };
|
|
1541
|
-
}
|
|
1542
|
-
} catch (e) {
|
|
1543
|
-
// Tool not installed or extraction failed — report distinguishably
|
|
1544
|
-
if (e.code === 'ENOENT') {
|
|
1545
|
-
return { text: null, error: 'tool_missing' };
|
|
1546
|
-
}
|
|
1547
|
-
return { text: null, error: 'extraction_failed', detail: e.message };
|
|
1548
|
-
}
|
|
1549
|
-
return { text: null, error: 'unsupported_format' };
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
function importFile(filePath) {
|
|
1553
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
1554
|
-
const name = path.basename(filePath);
|
|
1555
|
-
|
|
1556
|
-
if (BINARY_EXTS.has(ext)) {
|
|
1557
|
-
const extracted = extractBinaryText(filePath, ext);
|
|
1558
|
-
if (!extracted.text || extracted.text.trim().length === 0) {
|
|
1559
|
-
const tool = ext === '.pdf' ? 'pdftotext (poppler-utils)' : 'textutil (macOS built-in)';
|
|
1560
|
-
if (extracted.error === 'tool_missing') {
|
|
1561
|
-
console.warn(`Skipping ${ext} file (${tool} not installed): ${name}`);
|
|
1562
|
-
} else if (extracted.error === 'extraction_failed') {
|
|
1563
|
-
console.warn(`Skipping ${ext} file (extraction failed: ${extracted.detail}): ${name}`);
|
|
1564
|
-
} else {
|
|
1565
|
-
console.warn(`Skipping ${ext} file (empty or unsupported): ${name}`);
|
|
1566
|
-
}
|
|
1567
|
-
skipped++;
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
if (extracted.text.length > 120000) {
|
|
1571
|
-
console.warn(`Extracted text too large (${extracted.text.length} chars, max 120000): ${name}`);
|
|
1572
|
-
skipped++;
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
const evidence = evidenceApi.createEvidenceEntry('text', name, extracted.text.substring(0, 120000), filePath);
|
|
1576
|
-
evidenceApi.addEvidence(project, evidence);
|
|
1577
|
-
imported++;
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Detect binary content: try reading first 4KB as utf8; if it contains
|
|
1582
|
-
// null bytes or too many non-printable chars, it's likely binary.
|
|
1583
|
-
const buf = fs.readFileSync(filePath);
|
|
1584
|
-
if (buf.length === 0) { console.warn(`Empty file: ${name}`); skipped++; return; }
|
|
1585
|
-
const sample = buf.subarray(0, Math.min(buf.length, 4096));
|
|
1586
|
-
const nulls = sample.filter(b => b === 0).length;
|
|
1587
|
-
const nonPrintable = sample.filter(b => b !== 0 && (b < 0x20 || b === 0x7F) && b !== 0x0A && b !== 0x0D && b !== 0x09).length;
|
|
1588
|
-
if (nulls > 0 || nonPrintable > sample.length * 0.1) {
|
|
1589
|
-
console.warn(`Binary or non-text file skipped: ${name}`);
|
|
1590
|
-
skipped++;
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
let content;
|
|
1594
|
-
let fd = null;
|
|
1595
|
-
try {
|
|
1596
|
-
fd = fs.openSync(filePath, 'r');
|
|
1597
|
-
const buf = Buffer.alloc(120000);
|
|
1598
|
-
const bytesRead = fs.readSync(fd, buf, 0, 120000, 0);
|
|
1599
|
-
content = buf.toString('utf8', 0, bytesRead);
|
|
1600
|
-
} catch { console.warn(`Cannot read: ${name}`); skipped++; return; }
|
|
1601
|
-
finally {
|
|
1602
|
-
// Bug (#51): prior version called fs.closeSync only on the
|
|
1603
|
-
// success path, so any readSync failure left the file descriptor
|
|
1604
|
-
// open. Under bulk import (300 files), a single error mid-loop
|
|
1605
|
-
// could exhaust the per-process fd limit and silently break
|
|
1606
|
-
// later imports.
|
|
1607
|
-
if (fd !== null) {
|
|
1608
|
-
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
const evidence = evidenceApi.createEvidenceEntry('text', name, content, filePath);
|
|
1613
|
-
evidenceApi.addEvidence(project, evidence);
|
|
1614
|
-
imported++;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
if (fs.statSync(sourcePath).isDirectory()) {
|
|
1618
|
-
const files = fs.readdirSync(sourcePath).map(f => path.join(sourcePath, f)).filter(f => fs.statSync(f).isFile());
|
|
1619
|
-
for (const f of files) {
|
|
1620
|
-
if (imported >= 300) { console.warn('Reached 300 file limit.'); break; }
|
|
1621
|
-
importFile(f);
|
|
1622
|
-
}
|
|
1623
|
-
} else {
|
|
1624
|
-
importFile(sourcePath);
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
writeProject(projectPath, project);
|
|
1628
|
-
|
|
1629
|
-
// Check for sensitive content
|
|
1630
|
-
const evidenceList = project.evidence_materials || [];
|
|
1631
|
-
const flagged = aiFilter.checkEvidence(evidenceList);
|
|
1632
|
-
if (flagged.length > 0) {
|
|
1633
|
-
console.warn(`\nSensitive content detected in ${flagged.length} file(s):`);
|
|
1634
|
-
for (const f of flagged) console.warn(` ${f.filename}: ${f.flagged.join(', ')}`);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
console.log(`Imported: ${imported} file(s)${skipped > 0 ? `, ${skipped} skipped` : ''}`);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
function cmdFilter(args) {
|
|
1641
|
-
const projectInput = args[0];
|
|
1642
|
-
if (!projectInput) fail('Usage: kdna-studio filter <project>');
|
|
1643
|
-
const { project } = readProject(projectInput);
|
|
1644
|
-
const evidenceList = project.evidence_materials || [];
|
|
1645
|
-
if (evidenceList.length === 0) { console.log('No evidence to filter.'); return; }
|
|
1646
|
-
|
|
1647
|
-
const flagged = aiFilter.checkEvidence(evidenceList);
|
|
1648
|
-
if (flagged.length === 0) {
|
|
1649
|
-
console.log(`All ${evidenceList.length} evidence file(s) passed sensitive content filter.`);
|
|
1650
|
-
} else {
|
|
1651
|
-
console.log(`Sensitive content detected in ${flagged.length}/${evidenceList.length} file(s):`);
|
|
1652
|
-
for (const f of flagged) {
|
|
1653
|
-
console.log(` ${f.filename}: ${f.flagged.join(', ')}`);
|
|
1654
|
-
for (const [domain, match] of Object.entries(f.matches)) console.log(` ${domain}: "${match}"`);
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
const CARD_REQUIRED_FIELDS = {
|
|
1660
|
-
axiom: [
|
|
1661
|
-
'one_sentence', 'full_statement', 'why',
|
|
1662
|
-
'applies_when', 'does_not_apply_when', 'failure_risk',
|
|
1663
|
-
'confidence', 'evidence_type',
|
|
1664
|
-
],
|
|
1665
|
-
boundary: ['scope', 'out_of_scope'],
|
|
1666
|
-
misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
|
|
1667
|
-
self_check: ['question'],
|
|
1668
|
-
scenario: ['name', 'trigger', 'action', 'expected'],
|
|
1669
|
-
case: ['title', 'scenario', 'input', 'expected'],
|
|
1670
|
-
risk: ['name', 'description', 'mitigation'],
|
|
1671
|
-
stance: ['statement'],
|
|
1672
|
-
pattern: ['type', 'name', 'one_sentence', 'what_it_looks_like', 'how_to_fix', 'failure_risk'],
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
const CARD_FIELD_RULES = {
|
|
1676
|
-
axiom: {
|
|
1677
|
-
applies_when: { type: 'array' },
|
|
1678
|
-
does_not_apply_when: { type: 'array' },
|
|
1679
|
-
confidence: { allowed: ['low', 'medium', 'high'] },
|
|
1680
|
-
evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
|
|
1681
|
-
},
|
|
1682
|
-
boundary: {
|
|
1683
|
-
acceptable_exceptions: { type: 'array', optional: true },
|
|
1684
|
-
},
|
|
1685
|
-
stance: {
|
|
1686
|
-
applies_when: { type: 'array', optional: true },
|
|
1687
|
-
},
|
|
1688
|
-
misunderstanding: {
|
|
1689
|
-
applies_when: { type: 'array', optional: true },
|
|
1690
|
-
does_not_apply_when: { type: 'array', optional: true },
|
|
1691
|
-
},
|
|
1692
|
-
};
|
|
1693
|
-
|
|
1694
|
-
function fieldIssue(fields, name, rule = {}) {
|
|
1695
|
-
const val = fields[name];
|
|
1696
|
-
if (val === undefined || val === null || val === '' ||
|
|
1697
|
-
(Array.isArray(val) && val.length === 0)) {
|
|
1698
|
-
return rule.optional ? null : { field: name, issue: 'missing' };
|
|
1699
|
-
}
|
|
1700
|
-
if (rule.type === 'array' && !Array.isArray(val)) {
|
|
1701
|
-
return { field: name, issue: 'wrong_type', have_type: typeof val, need_type: 'array' };
|
|
1702
|
-
}
|
|
1703
|
-
if (rule.allowed && !rule.allowed.includes(val)) {
|
|
1704
|
-
return { field: name, issue: 'invalid_value', have: val, allowed: rule.allowed };
|
|
1705
|
-
}
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function formatFieldIssue(issue) {
|
|
1710
|
-
if (issue.issue === 'missing') return issue.field;
|
|
1711
|
-
if (issue.issue === 'wrong_type') return `${issue.field} (must be ${issue.need_type})`;
|
|
1712
|
-
if (issue.issue === 'invalid_value') return `${issue.field} (must be one of: ${issue.allowed.join('|')})`;
|
|
1713
|
-
return `${issue.field} (${issue.issue})`;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
function checkRequiredFields(type, fields) {
|
|
1717
|
-
const required = CARD_REQUIRED_FIELDS[type];
|
|
1718
|
-
if (!required) return [];
|
|
1719
|
-
const issues = [];
|
|
1720
|
-
for (const f of required) {
|
|
1721
|
-
const issue = fieldIssue(fields, f, CARD_FIELD_RULES[type]?.[f] || {});
|
|
1722
|
-
if (issue) issues.push(formatFieldIssue(issue));
|
|
1723
|
-
}
|
|
1724
|
-
for (const [name, rule] of Object.entries(CARD_FIELD_RULES[type] || {})) {
|
|
1725
|
-
if (required.includes(name)) continue;
|
|
1726
|
-
const issue = fieldIssue(fields, name, rule);
|
|
1727
|
-
if (issue) issues.push(formatFieldIssue(issue));
|
|
1728
|
-
}
|
|
1729
|
-
return issues;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
function criticalAxiomIssues(project) {
|
|
1733
|
-
const issues = [];
|
|
1734
|
-
for (const card of (Array.isArray(project.cards) ? project.cards : [])) {
|
|
1735
|
-
if (card.type !== 'axiom') continue;
|
|
1736
|
-
const fields = card.fields || {};
|
|
1737
|
-
for (const field of ['applies_when', 'does_not_apply_when', 'failure_risk']) {
|
|
1738
|
-
const issue = fieldIssue(fields, field, CARD_FIELD_RULES.axiom[field] || {});
|
|
1739
|
-
if (issue) issues.push(`${card.id}: ${formatFieldIssue(issue)}`);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return issues;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
function cardStrictTemplate(type, fields) {
|
|
1746
|
-
const required = CARD_REQUIRED_FIELDS[type];
|
|
1747
|
-
if (!required) return fields;
|
|
1748
|
-
const filled = { ...fields };
|
|
1749
|
-
for (const f of required) {
|
|
1750
|
-
const val = filled[f];
|
|
1751
|
-
if (val === undefined || val === null || val === '' ||
|
|
1752
|
-
(Array.isArray(val) && val.length === 0)) {
|
|
1753
|
-
filled[f] = `<TBD: ${f}>`;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return filled;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
function cmdCard(args) {
|
|
1760
|
-
const sub = args[0];
|
|
1761
|
-
if (sub === 'list') {
|
|
1762
|
-
const { project } = readProject(args[1]);
|
|
1763
|
-
// Bug (#2 UX): the --json flag was accepted but ignored — the
|
|
1764
|
-
// command printed the same TSV either way. A script consumer
|
|
1765
|
-
// that wanted to pipe the result into `jq` or a downstream
|
|
1766
|
-
// tool got garbage. The fix respects --json and emits a real
|
|
1767
|
-
// JSON document.
|
|
1768
|
-
const useJson = args.includes('--json');
|
|
1769
|
-
const rows = (project.cards || []).map(card => ({
|
|
1770
|
-
id: card.id,
|
|
1771
|
-
type: card.type,
|
|
1772
|
-
status: card.status,
|
|
1773
|
-
locked: !!card.locked,
|
|
1774
|
-
fields: card.fields || {},
|
|
1775
|
-
}));
|
|
1776
|
-
if (useJson) {
|
|
1777
|
-
console.log(JSON.stringify({ count: rows.length, cards: rows }, null, 2));
|
|
1778
|
-
} else {
|
|
1779
|
-
for (const r of rows) {
|
|
1780
|
-
console.log(`${r.id}\t${r.type}\t${r.status}\t${r.locked ? 'locked' : 'unlocked'}`);
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
if (sub === 'add') {
|
|
1786
|
-
const projectInput = args[1];
|
|
1787
|
-
const type = args[2];
|
|
1788
|
-
if (!projectInput || !type) fail('Usage: kdna-studio card add <project> <type> --field key=value [--template <name>] [--no-strict]');
|
|
1789
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1790
|
-
let fields = parseFields(args.slice(3));
|
|
1791
|
-
|
|
1792
|
-
const useTemplate = option(args, '--template');
|
|
1793
|
-
if (useTemplate === 'axiom-strict') {
|
|
1794
|
-
if (type !== 'axiom') fail('--template axiom-strict only applies to axiom cards');
|
|
1795
|
-
fields = cardStrictTemplate('axiom', fields);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
const isNoStrict = args.includes('--no-strict');
|
|
1799
|
-
if (!isNoStrict) {
|
|
1800
|
-
const missing = checkRequiredFields(type, fields);
|
|
1801
|
-
if (missing.length > 0) {
|
|
1802
|
-
fail(
|
|
1803
|
-
`Missing required fields for ${type}: ${missing.join(', ')}\n` +
|
|
1804
|
-
`Use --no-strict to add a partial card.`
|
|
1805
|
-
);
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// Warn: one_sentence is the compact profile display field
|
|
1810
|
-
if (type === 'axiom') {
|
|
1811
|
-
const oneSentence = fields.one_sentence;
|
|
1812
|
-
if (!oneSentence || String(oneSentence).startsWith('<TBD')) {
|
|
1813
|
-
console.warn('Warning: one_sentence is used by kdna load --profile=compact.');
|
|
1814
|
-
console.warn('Without it, the compact output may show a placeholder or truncated full_statement.');
|
|
1815
|
-
console.warn('Add --field one_sentence="..." for best results.\n');
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
const card = cardApi.createCard(type, fields);
|
|
1820
|
-
project.cards.push(card);
|
|
1821
|
-
if (project.stages?.judgment_cards) {
|
|
1822
|
-
project.stages.judgment_cards.total = project.cards.length;
|
|
1823
|
-
project.stages.judgment_cards.status = 'in_progress';
|
|
1824
|
-
}
|
|
1825
|
-
writeProject(projectPath, project);
|
|
1826
|
-
if (args.includes('--json')) {
|
|
1827
|
-
console.log(JSON.stringify({ added: true, card }, null, 2));
|
|
1828
|
-
} else {
|
|
1829
|
-
console.log(`Added card: ${card.id}`);
|
|
1830
|
-
}
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
if (sub === 'update') {
|
|
1834
|
-
const projectInput = args[1];
|
|
1835
|
-
const cardId = args[2];
|
|
1836
|
-
if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value OR kdna-studio card update <project> <card-id> --from-file <path.json>');
|
|
1837
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1838
|
-
const idx = (project.cards || []).findIndex((c) => c.id === cardId);
|
|
1839
|
-
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
1840
|
-
const card = project.cards[idx];
|
|
1841
|
-
if (card.locked) fail(`Cannot update locked card: ${cardId}. Unlock first with card unlock.`);
|
|
1842
|
-
// Bug (UX pro-20 migration): prior version only accepted
|
|
1843
|
-
// --field key=value, so an author migrating 20 source-tree
|
|
1844
|
-
// assets had to run one CLI invocation per field. With
|
|
1845
|
-
// confidence + evidence_type as 8th / 9th required fields on
|
|
1846
|
-
// axiom, a 99-axiom asset became 99 * 2 = 198 invocations.
|
|
1847
|
-
// The fix accepts --from-file <path.json> so an author can
|
|
1848
|
-
// write a single JSON file per card (or per project) and
|
|
1849
|
-
// apply it in one call. JSON shape: {field: value, ...}.
|
|
1850
|
-
// kdna-studio card update . ax_xxx --from-file ./fills/ax_xxx.json
|
|
1851
|
-
// For batched updates across many cards, see the
|
|
1852
|
-
// --from-file-projects variant below, which reads a list
|
|
1853
|
-
// of {id, fields} records.
|
|
1854
|
-
const fromFileIdx = args.indexOf('--from-file');
|
|
1855
|
-
if (fromFileIdx >= 0) {
|
|
1856
|
-
const filePath = args[fromFileIdx + 1];
|
|
1857
|
-
if (!filePath) fail('--from-file requires a path');
|
|
1858
|
-
if (!fs.existsSync(filePath)) fail(`--from-file: file not found: ${filePath}`);
|
|
1859
|
-
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1860
|
-
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
|
1861
|
-
fail(`--from-file: expected a JSON object mapping field names to values`);
|
|
1862
|
-
}
|
|
1863
|
-
for (const [k, v] of Object.entries(data)) {
|
|
1864
|
-
if (k === 'id' || k === 'type' || k === 'status' || k === 'locked') {
|
|
1865
|
-
fail(`--from-file: cannot edit reserved field "${k}"`);
|
|
1866
|
-
}
|
|
1867
|
-
if (card.fields) card.fields[k] = v;
|
|
1868
|
-
}
|
|
1869
|
-
} else {
|
|
1870
|
-
const updates = parseFields(args.slice(3));
|
|
1871
|
-
for (const [k, v] of Object.entries(updates)) {
|
|
1872
|
-
if (card.fields) card.fields[k] = v;
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
project.cards[idx] = card;
|
|
1876
|
-
writeProject(projectPath, project);
|
|
1877
|
-
console.log(`Updated card: ${cardId}`);
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
if (sub === 'remove') {
|
|
1881
|
-
const projectInput = args[1];
|
|
1882
|
-
const cardId = args[2];
|
|
1883
|
-
if (!projectInput || !cardId) fail('Usage: kdna-studio card remove <project> <card-id>');
|
|
1884
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1885
|
-
const idx = (project.cards || []).findIndex((c) => c.id === cardId);
|
|
1886
|
-
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
1887
|
-
if (project.cards[idx].locked) fail(`Cannot remove locked card: ${cardId}. Unlock first with card unlock.`);
|
|
1888
|
-
project.cards.splice(idx, 1);
|
|
1889
|
-
if (project.stages?.judgment_cards) {
|
|
1890
|
-
project.stages.judgment_cards.total = project.cards.length;
|
|
1891
|
-
}
|
|
1892
|
-
writeProject(projectPath, project);
|
|
1893
|
-
console.log(`Removed card: ${cardId}`);
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
if (sub === 'approve') {
|
|
1897
|
-
const projectInput = args[1];
|
|
1898
|
-
const approveAll = args.includes('--all');
|
|
1899
|
-
const cardId = approveAll ? null : args[2];
|
|
1900
|
-
if (!projectInput || (!cardId && !approveAll)) {
|
|
1901
|
-
fail('Usage: kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign]');
|
|
1902
|
-
}
|
|
1903
|
-
const by = option(args, '--by');
|
|
1904
|
-
const statement = option(args, '--statement');
|
|
1905
|
-
if (!by || !statement) fail('card approve requires --by and --statement');
|
|
1906
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1907
|
-
const errors = [];
|
|
1908
|
-
|
|
1909
|
-
const lockOneCard = (card) => {
|
|
1910
|
-
if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
|
|
1911
|
-
const lockPayload = {
|
|
1912
|
-
by,
|
|
1913
|
-
statement,
|
|
1914
|
-
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
|
|
1915
|
-
};
|
|
1916
|
-
if (args.includes('--sign')) {
|
|
1917
|
-
const identity = creatorApi.loadIdentity();
|
|
1918
|
-
if (!identity) {
|
|
1919
|
-
errors.push(`${card.id}: No creator identity. Run: kdna-studio identity init`);
|
|
1920
|
-
return null;
|
|
1921
|
-
}
|
|
1922
|
-
lockPayload.creator_id = identity.creator_id;
|
|
1923
|
-
const passphrase = resolvePassphrase(args);
|
|
1924
|
-
try {
|
|
1925
|
-
if (identity.encrypted && !passphrase) {
|
|
1926
|
-
errors.push(`${card.id}: Private key is encrypted — provide --passphrase-stdin`);
|
|
1927
|
-
return null;
|
|
1928
|
-
}
|
|
1929
|
-
lockPayload.signature = creatorApi.signHumanLock(
|
|
1930
|
-
card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
|
|
1931
|
-
);
|
|
1932
|
-
} catch (e) {
|
|
1933
|
-
errors.push(`${card.id}: Signing failed: ${e.message}`);
|
|
1934
|
-
return null;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
return cardApi.lockCard(card, lockPayload);
|
|
1938
|
-
};
|
|
1939
|
-
|
|
1940
|
-
if (cardId) {
|
|
1941
|
-
const idx = (project.cards || []).findIndex((c) => c.id === cardId);
|
|
1942
|
-
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
1943
|
-
if (project.cards[idx].locked) fail(`Card already locked: ${cardId}`);
|
|
1944
|
-
const locked = lockOneCard(project.cards[idx]);
|
|
1945
|
-
if (!locked) fail(errors[0]);
|
|
1946
|
-
project.cards[idx] = locked;
|
|
1947
|
-
const signed = args.includes('--sign') ? ' (signed)' : '';
|
|
1948
|
-
console.log(`Approved and Human Locked: ${cardId}${signed}`);
|
|
1949
|
-
} else {
|
|
1950
|
-
let approved = 0;
|
|
1951
|
-
let skipped = 0;
|
|
1952
|
-
for (let i = 0; i < (project.cards || []).length; i++) {
|
|
1953
|
-
const card = project.cards[i];
|
|
1954
|
-
if (card.locked) { skipped++; continue; }
|
|
1955
|
-
const locked = lockOneCard(card);
|
|
1956
|
-
if (!locked) continue;
|
|
1957
|
-
project.cards[i] = locked;
|
|
1958
|
-
approved++;
|
|
1959
|
-
}
|
|
1960
|
-
if (errors.length > 0) {
|
|
1961
|
-
fail(`Failed to approve ${errors.length} card(s):\n ${errors.join('\n ')}`);
|
|
1962
|
-
}
|
|
1963
|
-
if (approved === 0) {
|
|
1964
|
-
if (skipped > 0) fail(`No unlocked cards to approve (${skipped} already locked).`);
|
|
1965
|
-
fail('No unlocked cards to approve.');
|
|
1966
|
-
}
|
|
1967
|
-
const signed = args.includes('--sign') ? ' (signed)' : '';
|
|
1968
|
-
console.log(`Approved and Human Locked: ${approved} cards${signed}${skipped > 0 ? ` (${skipped} already locked)` : ''}`);
|
|
1969
|
-
}
|
|
1970
|
-
if (project.stages?.judgment_cards) {
|
|
1971
|
-
project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
|
|
1972
|
-
project.stages.judgment_cards.total = project.cards.length;
|
|
1973
|
-
}
|
|
1974
|
-
writeProject(projectPath, project);
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
if (sub === 'unlock') {
|
|
1978
|
-
const projectInput = args[1];
|
|
1979
|
-
const cardId = args[2];
|
|
1980
|
-
if (!projectInput || !cardId) fail('Usage: kdna-studio card unlock <project> <card-id> --by <id> --statement <text>');
|
|
1981
|
-
const by = option(args, '--by');
|
|
1982
|
-
const statement = option(args, '--statement');
|
|
1983
|
-
if (!by || !statement) fail('card unlock requires --by and --statement');
|
|
1984
|
-
const { projectPath, project } = readProject(projectInput);
|
|
1985
|
-
const idx = (project.cards || []).findIndex((c) => c.id === cardId);
|
|
1986
|
-
if (idx < 0) fail(`Card not found: ${cardId}`);
|
|
1987
|
-
const card = project.cards[idx];
|
|
1988
|
-
if (!card.locked) fail(`Card is not locked: ${cardId}`);
|
|
1989
|
-
card.locked = false;
|
|
1990
|
-
card.status = 'draft';
|
|
1991
|
-
delete card.human_lock;
|
|
1992
|
-
if (!card.audit_log) card.audit_log = [];
|
|
1993
|
-
card.audit_log.push({
|
|
1994
|
-
action: 'unlocked',
|
|
1995
|
-
by,
|
|
1996
|
-
statement,
|
|
1997
|
-
timestamp: new Date().toISOString(),
|
|
1998
|
-
});
|
|
1999
|
-
project.cards[idx] = card;
|
|
2000
|
-
if (project.stages?.judgment_cards) {
|
|
2001
|
-
project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
|
|
2002
|
-
}
|
|
2003
|
-
writeProject(projectPath, project);
|
|
2004
|
-
console.log(`Unlocked card: ${cardId}`);
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
fail('Usage: kdna-studio card <list|add|update|remove|approve|unlock> ...');
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
function cmdLock(args) {
|
|
2011
|
-
const { project } = readProject(args[0]);
|
|
2012
|
-
const gate = projectApi.checkHumanLockGate(project);
|
|
2013
|
-
if (gate.blocked) {
|
|
2014
|
-
console.error('Human Lock Gate blocked export:');
|
|
2015
|
-
for (const issue of gate.issues) console.error(` - ${issue.cardId}: ${issue.reason}`);
|
|
2016
|
-
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
2017
|
-
}
|
|
2018
|
-
console.log(`Human Lock Gate passed: ${gate.lockedJudgmentCards} locked judgment cards`);
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
function cmdMigrate(args) {
|
|
2022
|
-
// Bug (UX pro-20 migration): prior version of `migrate` always
|
|
2023
|
-
// attempted the full pipeline — read source folder, import to
|
|
2024
|
-
// Studio project, lock all cards, then export. An author migrating
|
|
2025
|
-
// 20 source-tree assets had no way to know in advance whether
|
|
2026
|
-
// the assets would pass the current Human Lock gate. The fix
|
|
2027
|
-
// adds `--check` which runs the import + the critical-missing
|
|
2028
|
-
// check + the lock-gate check, then prints a report and exits
|
|
2029
|
-
// without writing the .kdna file. Pair with `audit-locks` for
|
|
2030
|
-
// the full pre-migration report.
|
|
2031
|
-
//
|
|
2032
|
-
// Usage:
|
|
2033
|
-
// kdna-studio migrate <source-dir> --check --name <@scope/name>
|
|
2034
|
-
//
|
|
2035
|
-
// Exit codes:
|
|
2036
|
-
// 0 — would succeed
|
|
2037
|
-
// 2 — INPUT_ERROR (missing args)
|
|
2038
|
-
// 4 — would fail (Human Lock gate or critical axiom field)
|
|
2039
|
-
const checkOnly = args.includes('--check');
|
|
2040
|
-
const sourceDir = args[0];
|
|
2041
|
-
const out = option(args, '--out');
|
|
2042
|
-
const by = option(args, '--by');
|
|
2043
|
-
const statement = option(args, '--statement');
|
|
2044
|
-
const requestedName = option(args, '--name');
|
|
2045
|
-
let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
|
|
2046
|
-
if (!sourceDir || (!out && !checkOnly) || (!by && !checkOnly) || (!statement && !checkOnly)) {
|
|
2047
|
-
fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--check] [--sign] [--passphrase-stdin]');
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
const sourcePath = path.resolve(sourceDir);
|
|
2051
|
-
if (option(args, '--format')) {
|
|
2052
|
-
fail('The --format option is not supported. KDNA has one current asset format.');
|
|
2053
|
-
}
|
|
2054
|
-
const isStudioProject = fs.existsSync(path.join(sourcePath, 'studio.project.json'));
|
|
2055
|
-
let tmpDir = null;
|
|
2056
|
-
let projectPath = null;
|
|
2057
|
-
let project = null;
|
|
2058
|
-
|
|
2059
|
-
// Wrap everything in try / finally so a mid-migrate failure (e.g. missing
|
|
2060
|
-
// critical fields, gate blocked, signing error) still cleans up tmpDir.
|
|
2061
|
-
// Bug: prior code only cleaned up on its success paths; any
|
|
2062
|
-
// older export path that threw leaked `/tmp/kdna-migrate-*` entries
|
|
2063
|
-
// indefinitely.
|
|
2064
|
-
try {
|
|
2065
|
-
// Step 1: import dev source, or use an existing Studio project directly.
|
|
2066
|
-
let creatorIdentity = null;
|
|
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
|
-
}
|
|
2077
|
-
if (isStudioProject) {
|
|
2078
|
-
const loaded = readProject(sourcePath);
|
|
2079
|
-
project = loaded.project;
|
|
2080
|
-
projectPath = loaded.projectPath;
|
|
2081
|
-
if (!requestedName && project.name) name = project.name;
|
|
2082
|
-
} else {
|
|
2083
|
-
tmpDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-migrate-')));
|
|
2084
|
-
const imported = importFromFolder(sourceDir, tmpDir, name, creatorIdentity, { silent: true });
|
|
2085
|
-
project = imported.project;
|
|
2086
|
-
projectPath = path.join(tmpDir, 'studio.project.json');
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
// Bug (#54): --allow-incomplete used to be read AFTER the critical
|
|
2090
|
-
// fields check, so a caller asking to bypass the gate was rejected
|
|
2091
|
-
// for the wrong reason. Resolve the flag here so the critical-missing
|
|
2092
|
-
// check honours it (and reports the bypass correctly) instead of
|
|
2093
|
-
// always failing.
|
|
2094
|
-
const allowIncompleteEarly = args.includes('--allow-incomplete');
|
|
2095
|
-
|
|
2096
|
-
// Reject if critical judgment fields are missing or malformed on axioms.
|
|
2097
|
-
const criticalMissing = criticalAxiomIssues(project);
|
|
2098
|
-
if (criticalMissing.length > 0) {
|
|
2099
|
-
if (allowIncompleteEarly) {
|
|
2100
|
-
console.warn(
|
|
2101
|
-
`--allow-incomplete: bypassing ${criticalMissing.length} critical axiom-field check(s). ` +
|
|
2102
|
-
`The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
|
|
2103
|
-
);
|
|
2104
|
-
} else {
|
|
2105
|
-
fail(
|
|
2106
|
-
`Cannot migrate: ${criticalMissing.length} critical fields missing from axioms.\n` +
|
|
2107
|
-
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
2108
|
-
` decide when to load this domain). Add them to your source files first,\n` +
|
|
2109
|
-
` or pass --allow-incomplete to bypass.\n` +
|
|
2110
|
-
` Missing:\n ` + criticalMissing.slice(0, 10).join('\n ') +
|
|
2111
|
-
(criticalMissing.length > 10 ? `\n ... and ${criticalMissing.length - 10} more` : '')
|
|
2112
|
-
);
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// --check branch: print a pre-migration report and exit. The
|
|
2117
|
-
// project object already has every card from the source tree
|
|
2118
|
-
// imported, so we can run the same audit-locks logic here
|
|
2119
|
-
// without re-reading the source. This is the single-call
|
|
2120
|
-
// equivalent of `kdna-studio create --from-folder <src> &&
|
|
2121
|
-
// kdna-studio audit-locks .` — but it requires no
|
|
2122
|
-
// intermediate write.
|
|
2123
|
-
if (checkOnly) {
|
|
2124
|
-
const REQUIRED = {
|
|
2125
|
-
axiom: ['confidence', 'evidence_type', 'one_sentence', 'full_statement', 'why',
|
|
2126
|
-
'applies_when', 'does_not_apply_when', 'failure_risk'],
|
|
2127
|
-
risk: ['name', 'description', 'mitigation'],
|
|
2128
|
-
stance: ['statement', 'applies_when'],
|
|
2129
|
-
misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
|
|
2130
|
-
boundary: ['scope', 'out_of_scope'],
|
|
2131
|
-
};
|
|
2132
|
-
const summary = { project: name, source: sourceDir, total: project.cards.length, by_type: {}, would_block: false };
|
|
2133
|
-
for (const card of (project.cards || [])) {
|
|
2134
|
-
const req = REQUIRED[card.type];
|
|
2135
|
-
if (!req) continue;
|
|
2136
|
-
const missing = [];
|
|
2137
|
-
for (const f of req) {
|
|
2138
|
-
const issue = fieldIssue(card.fields || {}, f, CARD_FIELD_RULES[card.type]?.[f] || {});
|
|
2139
|
-
if (issue) missing.push(formatFieldIssue(issue));
|
|
2140
|
-
}
|
|
2141
|
-
if (missing.length > 0) {
|
|
2142
|
-
if (!summary.by_type[card.type]) summary.by_type[card.type] = [];
|
|
2143
|
-
summary.by_type[card.type].push({ id: card.id, missing });
|
|
2144
|
-
summary.would_block = true;
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
2148
|
-
process.exit(summary.would_block ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// Step 2: approve and Human Lock all cards
|
|
2152
|
-
let locked = 0;
|
|
2153
|
-
for (let i = 0; i < (project.cards || []).length; i++) {
|
|
2154
|
-
let card = project.cards[i];
|
|
2155
|
-
if (card.locked) { locked++; continue; }
|
|
2156
|
-
if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
|
|
2157
|
-
const lockPayload = {
|
|
2158
|
-
by,
|
|
2159
|
-
statement,
|
|
2160
|
-
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
|
|
2161
|
-
};
|
|
2162
|
-
if (args.includes('--sign')) {
|
|
2163
|
-
const identity = creatorApi.loadIdentity();
|
|
2164
|
-
if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
|
|
2165
|
-
lockPayload.creator_id = identity.creator_id;
|
|
2166
|
-
const passphrase = resolvePassphrase(args);
|
|
2167
|
-
try {
|
|
2168
|
-
lockPayload.signature = creatorApi.signHumanLock(
|
|
2169
|
-
card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
|
|
2170
|
-
);
|
|
2171
|
-
} catch (e) {
|
|
2172
|
-
fail(`Failed to sign Human Lock for ${card.id}: ${e.message}`, EXIT.TRUST_FAILED);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
project.cards[i] = cardApi.lockCard(card, lockPayload);
|
|
2176
|
-
locked++;
|
|
2177
|
-
}
|
|
2178
|
-
if (project.stages?.judgment_cards) {
|
|
2179
|
-
project.stages.judgment_cards.locked = locked;
|
|
2180
|
-
project.stages.judgment_cards.total = project.cards.length;
|
|
2181
|
-
}
|
|
2182
|
-
writeProject(projectPath, project);
|
|
2183
|
-
console.log(`Approved and Human Locked: ${locked} cards`);
|
|
2184
|
-
|
|
2185
|
-
// Step 3: verify Human Lock gate
|
|
2186
|
-
const gate = projectApi.checkHumanLockGate(project);
|
|
2187
|
-
if (gate.blocked) {
|
|
2188
|
-
const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
|
|
2189
|
-
fail(`Human Lock Gate blocked: ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
const absOut = path.resolve(out);
|
|
2193
|
-
const allowIncomplete = args.includes('--allow-incomplete');
|
|
2194
|
-
// Step 4: export the canonical runtime asset.
|
|
2195
|
-
exportProject(project, name, absOut, { allowIncomplete });
|
|
2196
|
-
if (tmpDir) {
|
|
2197
|
-
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
2198
|
-
tmpDir = null;
|
|
2199
|
-
}
|
|
2200
|
-
} finally {
|
|
2201
|
-
// Single, guaranteed cleanup point for tmpDir even when an earlier
|
|
2202
|
-
// step (critical-missing check, gate block, signing failure) bails
|
|
2203
|
-
// out via `fail()`. `fail()` ultimately calls process.exit, but
|
|
2204
|
-
// if a future caller swallows that or the runtime traps the throw,
|
|
2205
|
-
// we still leave the filesystem clean.
|
|
2206
|
-
if (tmpDir) {
|
|
2207
|
-
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
function exportProject(project, name, outPath, opts = {}) {
|
|
2213
|
-
let core;
|
|
2214
|
-
try { core = require('@aikdna/kdna-core'); } catch (e) {
|
|
2215
|
-
fail('@aikdna/kdna-core is required for runtime export.', 2);
|
|
2216
|
-
}
|
|
2217
|
-
if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
|
|
2218
|
-
fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for runtime export.', 2);
|
|
2219
|
-
}
|
|
2220
|
-
const criticalIssues = criticalAxiomIssues(project);
|
|
2221
|
-
if (criticalIssues.length > 0) {
|
|
2222
|
-
if (opts.allowIncomplete) {
|
|
2223
|
-
console.warn(
|
|
2224
|
-
`--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
|
|
2225
|
-
`The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
|
|
2226
|
-
);
|
|
2227
|
-
} else {
|
|
2228
|
-
fail(
|
|
2229
|
-
`Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
|
|
2230
|
-
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
2231
|
-
` decide when to load this domain). Fix them first, or pass\n` +
|
|
2232
|
-
` --allow-incomplete to bypass.\n` +
|
|
2233
|
-
` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
|
|
2234
|
-
(criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
|
|
2235
|
-
EXIT.HUMAN_LOCK_REQUIRED,
|
|
2236
|
-
);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
const exportedCards = (project.cards || []).filter(c => c.status !== 'deprecated');
|
|
2240
|
-
const runtimeDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-runtime-')));
|
|
2241
|
-
try {
|
|
2242
|
-
const manifestDefaults = buildManifest(project, name);
|
|
2243
|
-
const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
|
|
2244
|
-
asset_id: manifestDefaults.asset_id,
|
|
2245
|
-
asset_uid: manifestDefaults.asset_uid,
|
|
2246
|
-
title: manifestDefaults.title,
|
|
2247
|
-
created_at: manifestDefaults.created_at,
|
|
2248
|
-
updated_at: manifestDefaults.updated_at,
|
|
2249
|
-
access: project.release?.access || project.source_manifest?.access || 'public',
|
|
2250
|
-
password: opts.password || undefined,
|
|
2251
|
-
});
|
|
2252
|
-
writeFiles(runtimeDir, runtimeAsset.files);
|
|
2253
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2254
|
-
core.pack(runtimeDir, outPath);
|
|
2255
|
-
const vr = core.validate(outPath);
|
|
2256
|
-
if (!vr.overall_valid) fail('runtime export validation failed: ' + (vr.problems || []).join('; '));
|
|
2257
|
-
console.log('Exported: ' + outPath);
|
|
2258
|
-
console.log(' Name: ' + name + ' Cards: ' + exportedCards.length + ' Validated: runtime contract passed');
|
|
2259
|
-
} finally {
|
|
2260
|
-
try { fs.rmSync(runtimeDir, { recursive: true }); } catch { /* cleanup */ }
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
function compileProject(projectInput) {
|
|
2265
|
-
const { project } = readProject(projectInput);
|
|
2266
|
-
return { project, result: compileApi.compileDomain(project) };
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
function cmdCompile(args) {
|
|
2270
|
-
const projectInput = args[0];
|
|
2271
|
-
const out = option(args, '--out', './dist/studio-build');
|
|
2272
|
-
if (!projectInput) fail('Usage: kdna-studio compile <project> --out <dir>');
|
|
2273
|
-
const { result } = compileProject(projectInput);
|
|
2274
|
-
writeFiles(path.resolve(out), result.files);
|
|
2275
|
-
console.log(`Compiled Studio build output: ${path.resolve(out)}`);
|
|
2276
|
-
console.log(`Build ID: ${result.identity.build_id}`);
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
function cmdExport(args) {
|
|
2280
|
-
const projectInput = args[0];
|
|
2281
|
-
const out = option(args, '--out');
|
|
2282
|
-
if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--allow-incomplete]');
|
|
2283
|
-
if (option(args, '--format')) {
|
|
2284
|
-
fail('The --format option is not supported. KDNA has one current asset format.');
|
|
2285
|
-
}
|
|
2286
|
-
if (args.includes('--sign')) {
|
|
2287
|
-
fail('Asset signatures are outside the current Preview contract.');
|
|
2288
|
-
}
|
|
2289
|
-
{
|
|
2290
|
-
const { project } = readProject(projectInput);
|
|
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
|
-
}
|
|
2302
|
-
const useStdin = args.includes('--password-stdin');
|
|
2303
|
-
let password;
|
|
2304
|
-
if (useStdin) {
|
|
2305
|
-
// fs.readFileSync(0) on a TTY waits forever for input and never
|
|
2306
|
-
// returns — the export appears to hang. Refuse up front with a
|
|
2307
|
-
// clear pointer to the right invocation.
|
|
2308
|
-
if (process.stdin.isTTY) {
|
|
2309
|
-
fail(
|
|
2310
|
-
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
2311
|
-
'Example: printf "%s\\n" "$KDNA_PASSWORD" | kdna-studio export <project> --out <file.kdna> --password-stdin\n' +
|
|
2312
|
-
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
2313
|
-
);
|
|
2314
|
-
}
|
|
2315
|
-
try {
|
|
2316
|
-
password = readBoundedSecretStdin('Password');
|
|
2317
|
-
} catch (e) {
|
|
2318
|
-
fail(`Could not read password from stdin: ${e.message}`);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
exportProject(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
2322
|
-
allowIncomplete: args.includes('--allow-incomplete'),
|
|
2323
|
-
password,
|
|
2324
|
-
});
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
function cmdStudioInstall(args) {
|
|
2330
|
-
// Delegate to kdna CLI for runtime install operations
|
|
2331
|
-
const target = args[0];
|
|
2332
|
-
if (!target) fail('Usage: kdna-studio install <@scope/name|file.kdna>');
|
|
2333
|
-
try {
|
|
2334
|
-
const kdnaArgs = ['install', target, '--yes'];
|
|
2335
|
-
if (args.includes('--trusted')) {
|
|
2336
|
-
fail('Asset signatures are outside the current Preview contract.');
|
|
2337
|
-
}
|
|
2338
|
-
const result = require('child_process').spawnSync('kdna', kdnaArgs, {
|
|
2339
|
-
stdio: 'inherit', encoding: 'utf8',
|
|
2340
|
-
});
|
|
2341
|
-
if (result.status !== 0) process.exit(result.status);
|
|
2342
|
-
} catch (e) {
|
|
2343
|
-
console.error('Error spawning kdna CLI:', e.message);
|
|
2344
|
-
fail('kdna CLI not found. Install with: npm install -g @aikdna/kdna-cli');
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
function cmdStudioUpdate(args) {
|
|
2349
|
-
const target = args[0];
|
|
2350
|
-
if (!target) fail('Usage: kdna-studio update <@scope/name>');
|
|
2351
|
-
// kdna update does not exist in kdna-cli 0.28.x. The correct
|
|
2352
|
-
// path is to re-install via npm or use kdna install to pull
|
|
2353
|
-
// the latest published version of the asset.
|
|
2354
|
-
console.log(`To update ${target}, run:`);
|
|
2355
|
-
console.log(` npm install -g @aikdna/kdna-cli@latest`);
|
|
2356
|
-
console.log(` kdna install ${target}`);
|
|
2357
|
-
console.log(``);
|
|
2358
|
-
console.log(`Or for a project-local install:`);
|
|
2359
|
-
console.log(` cd <project-dir> && npm install @aikdna/kdna-cli@latest`);
|
|
2360
|
-
process.exit(0);
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
function cmdLlm(args) {
|
|
2364
|
-
const sub = args[0];
|
|
2365
|
-
if (sub === 'show') {
|
|
2366
|
-
const cfg = llm.config();
|
|
2367
|
-
console.log(`Provider : ${cfg.provider || '(not set)'}`);
|
|
2368
|
-
console.log(`Model : ${cfg.model || '(not set)'}`);
|
|
2369
|
-
console.log(`Base URL : ${cfg.baseURL || '(not set)'}`);
|
|
2370
|
-
console.log(`API Key : ${cfg.apiKey ? '(set)' : '(not set)'}`);
|
|
2371
|
-
return;
|
|
2372
|
-
}
|
|
2373
|
-
if (sub === 'config') {
|
|
2374
|
-
const provider = option(args, '--provider') || option(args, '-p');
|
|
2375
|
-
const model = option(args, '--model') || option(args, '-m');
|
|
2376
|
-
const apiKey = resolveApiKey(args);
|
|
2377
|
-
const baseURL = option(args, '--url') || option(args, '-u');
|
|
2378
|
-
const updates = {};
|
|
2379
|
-
if (provider) updates.provider = provider;
|
|
2380
|
-
if (model) updates.model = model;
|
|
2381
|
-
if (apiKey) updates.apiKey = apiKey;
|
|
2382
|
-
if (baseURL) updates.baseURL = baseURL;
|
|
2383
|
-
if (Object.keys(updates).length === 0) fail('Usage: kdna-studio llm config --provider <name> [--model <name>] [--key-pipe] [--url <base-url>]');
|
|
2384
|
-
const cfg = llm.configure(updates);
|
|
2385
|
-
console.log(JSON.stringify({ provider: cfg.provider, model: cfg.model, baseURL: cfg.baseURL }, null, 2));
|
|
2386
|
-
return;
|
|
2387
|
-
}
|
|
2388
|
-
fail('Usage: kdna-studio llm <config|show>');
|
|
2389
|
-
}
|
|
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
|
-
|
|
2462
|
-
function cmdIdentity(args) {
|
|
2463
|
-
const sub = args[0];
|
|
2464
|
-
if (sub === 'init') {
|
|
2465
|
-
const name = option(args, '--name', process.env.USER || process.env.USERNAME || '');
|
|
2466
|
-
const passphrase = resolvePassphrase(args);
|
|
2467
|
-
try {
|
|
2468
|
-
const identity = creatorApi.initIdentity(name, null, passphrase);
|
|
2469
|
-
console.log(`Creator identity initialized:`);
|
|
2470
|
-
console.log(` creator_id: ${identity.creator_id}`);
|
|
2471
|
-
console.log(` display_name: ${identity.display_name}`);
|
|
2472
|
-
console.log(` identity_dir: ${identity.identity_dir}`);
|
|
2473
|
-
console.log(` public_key saved: ${identity.public_key_path}`);
|
|
2474
|
-
console.log(` encrypted: ${identity.encrypted}`);
|
|
2475
|
-
console.log(`\nBack up your private key at: ${creatorApi.privateKeyPath()}`);
|
|
2476
|
-
if (identity.encrypted) {
|
|
2477
|
-
console.log(` Private key is encrypted — keep your passphrase safe.`);
|
|
2478
|
-
}
|
|
2479
|
-
} catch (err) {
|
|
2480
|
-
failIdentityInit(err);
|
|
2481
|
-
}
|
|
2482
|
-
return;
|
|
2483
|
-
}
|
|
2484
|
-
if (sub === 'show') {
|
|
2485
|
-
const identity = creatorApi.loadIdentity();
|
|
2486
|
-
if (!identity) fail('No identity found. Run: kdna-studio identity init --name "Your Name"');
|
|
2487
|
-
console.log(JSON.stringify({
|
|
2488
|
-
creator_id: identity.creator_id,
|
|
2489
|
-
display_name: identity.display_name,
|
|
2490
|
-
verified: identity.verified || false,
|
|
2491
|
-
created_at: identity.created_at,
|
|
2492
|
-
}, null, 2));
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
fail('Usage: kdna-studio identity <init|show>');
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
// Audit-locks: list every card that would fail the current Human Lock
|
|
2499
|
-
// gate because of missing or empty fields. Used during the
|
|
2500
|
-
// pre-migration review of source-tree assets (e.g. pro-20) so an
|
|
2501
|
-
// author can fill in the missing confidence / evidence_type / risk
|
|
2502
|
-
// description / stance statement BEFORE running `migrate`.
|
|
2503
|
-
//
|
|
2504
|
-
// The earlier readiness output returned a flat human_lock_gate.issues list,
|
|
2505
|
-
// which mixed card
|
|
2506
|
-
// types and field names. An author migrating 20 source-tree
|
|
2507
|
-
// assets with 99 axioms + 140 risk cards + 70 stance cards had no
|
|
2508
|
-
// way to see, for a single asset, "which axioms are missing
|
|
2509
|
-
// confidence?" — they had to grep the project JSON. The fix adds
|
|
2510
|
-
// a per-type, per-field breakdown.
|
|
2511
|
-
function cmdAuditLocks(args) {
|
|
2512
|
-
const { project } = readProject(args[0]);
|
|
2513
|
-
const useJson = args.includes('--json');
|
|
2514
|
-
const onlyType = option(args, '--type'); // 'axiom' | 'risk' | 'stance' | 'misunderstanding' | null
|
|
2515
|
-
|
|
2516
|
-
const cards = project.cards || [];
|
|
2517
|
-
const findings = {
|
|
2518
|
-
project: project.name,
|
|
2519
|
-
card_count: cards.length,
|
|
2520
|
-
locked_count: cards.filter(c => c.locked).length,
|
|
2521
|
-
by_type: {},
|
|
2522
|
-
};
|
|
2523
|
-
|
|
2524
|
-
// Per-card-type required field lists. Mirrors the current lockCard
|
|
2525
|
-
// gate (kdna-studio-core/src/cards/index.js) so the output is
|
|
2526
|
-
// exactly the field set the gate will reject.
|
|
2527
|
-
const REQUIRED = {
|
|
2528
|
-
axiom: ['one_sentence', 'full_statement', 'why', 'applies_when',
|
|
2529
|
-
'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type'],
|
|
2530
|
-
risk: ['name', 'description', 'mitigation'],
|
|
2531
|
-
stance: ['statement', 'applies_when'],
|
|
2532
|
-
misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
|
|
2533
|
-
boundary: ['scope', 'out_of_scope'],
|
|
2534
|
-
self_check: ['question'],
|
|
2535
|
-
};
|
|
2536
|
-
const THRESHOLDS = {
|
|
2537
|
-
axiom: {
|
|
2538
|
-
one_sentence: { min: 5, type: 'string' },
|
|
2539
|
-
full_statement: { min: 20, type: 'string' },
|
|
2540
|
-
why: { min: 20, type: 'string' },
|
|
2541
|
-
applies_when: { type: 'array', minLength: 1 },
|
|
2542
|
-
does_not_apply_when: { type: 'array', minLength: 1 },
|
|
2543
|
-
failure_risk: { min: 1, type: 'string' },
|
|
2544
|
-
confidence: { allowed: ['low', 'medium', 'high'] },
|
|
2545
|
-
evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle', 'practice'] },
|
|
2546
|
-
},
|
|
2547
|
-
risk: {
|
|
2548
|
-
name: { min: 1, type: 'string' },
|
|
2549
|
-
description: { min: 20, type: 'string' },
|
|
2550
|
-
mitigation: { min: 1, type: 'string' },
|
|
2551
|
-
},
|
|
2552
|
-
stance: {
|
|
2553
|
-
statement: { min: 1, type: 'string' },
|
|
2554
|
-
applies_when: { type: 'array', minLength: 0 },
|
|
2555
|
-
},
|
|
2556
|
-
};
|
|
2557
|
-
|
|
2558
|
-
function checkField(type, fields, name, rule) {
|
|
2559
|
-
const value = fields[name];
|
|
2560
|
-
if (value === undefined || value === null || value === '') {
|
|
2561
|
-
return { field: name, issue: 'missing' };
|
|
2562
|
-
}
|
|
2563
|
-
if (rule.type === 'string' && rule.min && String(value).length < rule.min) {
|
|
2564
|
-
return { field: name, issue: 'too_short', have_length: String(value).length, need_min: rule.min };
|
|
2565
|
-
}
|
|
2566
|
-
if (rule.allowed && !rule.allowed.includes(value)) {
|
|
2567
|
-
return { field: name, issue: 'invalid_value', have: value, allowed: rule.allowed };
|
|
2568
|
-
}
|
|
2569
|
-
if (rule.type === 'array' && !Array.isArray(value)) {
|
|
2570
|
-
return { field: name, issue: 'wrong_type', have_type: typeof value, need_type: 'array' };
|
|
2571
|
-
}
|
|
2572
|
-
if (rule.type === 'array' && rule.minLength && value.length < rule.minLength) {
|
|
2573
|
-
return { field: name, issue: 'too_short', have_length: value.length, need_min: rule.minLength };
|
|
2574
|
-
}
|
|
2575
|
-
return null;
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
for (const card of cards) {
|
|
2579
|
-
const req = REQUIRED[card.type];
|
|
2580
|
-
if (!req) continue;
|
|
2581
|
-
if (onlyType && card.type !== onlyType) continue;
|
|
2582
|
-
const rule = THRESHOLDS[card.type] || {};
|
|
2583
|
-
const missing = [];
|
|
2584
|
-
for (const name of req) {
|
|
2585
|
-
const issue = checkField(card.type, card.fields || {}, name, rule[name] || {});
|
|
2586
|
-
if (issue) missing.push(issue);
|
|
2587
|
-
}
|
|
2588
|
-
if (missing.length > 0) {
|
|
2589
|
-
if (!findings.by_type[card.type]) {
|
|
2590
|
-
findings.by_type[card.type] = { missing_count: 0, cards: [] };
|
|
2591
|
-
}
|
|
2592
|
-
findings.by_type[card.type].missing_count += 1;
|
|
2593
|
-
findings.by_type[card.type].cards.push({
|
|
2594
|
-
id: card.id,
|
|
2595
|
-
one_sentence: card.fields?.one_sentence || card.fields?.statement || card.fields?.question || '',
|
|
2596
|
-
missing,
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
if (useJson) {
|
|
2602
|
-
console.log(JSON.stringify(findings, null, 2));
|
|
2603
|
-
} else {
|
|
2604
|
-
console.log(`Project: ${findings.project}`);
|
|
2605
|
-
console.log(`Cards: ${findings.card_count} (locked: ${findings.locked_count})`);
|
|
2606
|
-
const types = Object.keys(findings.by_type);
|
|
2607
|
-
if (types.length === 0) {
|
|
2608
|
-
console.log('\nAll cards pass the current Human Lock gate. Ready to migrate.');
|
|
2609
|
-
return;
|
|
2610
|
-
}
|
|
2611
|
-
for (const t of types) {
|
|
2612
|
-
const block = findings.by_type[t];
|
|
2613
|
-
console.log(`\n${t}: ${block.missing_count} card(s) with missing fields`);
|
|
2614
|
-
for (const card of block.cards) {
|
|
2615
|
-
console.log(` ${card.id} ${(card.one_sentence || '').slice(0, 60)}`);
|
|
2616
|
-
for (const m of card.missing) {
|
|
2617
|
-
if (m.issue === 'missing') {
|
|
2618
|
-
console.log(` - ${m.field}: missing`);
|
|
2619
|
-
} else if (m.issue === 'too_short') {
|
|
2620
|
-
console.log(` - ${m.field}: too short (have ${m.have_length}, need ≥ ${m.need_min})`);
|
|
2621
|
-
} else if (m.issue === 'invalid_value') {
|
|
2622
|
-
console.log(` - ${m.field}: invalid value "${m.have}", allowed: ${m.allowed.join('|')}`);
|
|
2623
|
-
} else {
|
|
2624
|
-
console.log(` - ${m.field}: ${m.issue}`);
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// ─── Distillation Commands ──────────────────────────────────────────
|
|
2633
|
-
|
|
2634
|
-
function cmdTarget(args) {
|
|
2635
|
-
const sub = args[0];
|
|
2636
|
-
if (sub === 'declare') {
|
|
2637
|
-
const projectInput = args[1];
|
|
2638
|
-
if (!projectInput) fail('Usage: kdna-studio target declare <project> --category <cat> --scope <scope> --granularity <gran> --task <task>');
|
|
2639
|
-
const { projectPath, project } = readProject(projectInput);
|
|
2640
|
-
const category = option(args, '--category', 'expression_writing');
|
|
2641
|
-
const scope = option(args, '--scope', 'personal');
|
|
2642
|
-
const granularity = option(args, '--granularity', 'core_principles');
|
|
2643
|
-
const taskScope = option(args, '--task', 'general content creation');
|
|
2644
|
-
const include = option(args, '--include');
|
|
2645
|
-
const exclude = option(args, '--exclude');
|
|
2646
|
-
const loadCondition = option(args, '--load-condition');
|
|
2647
|
-
const domainName = project.name || path.basename(path.dirname(projectPath), '.json');
|
|
2648
|
-
|
|
2649
|
-
const target = distillationApi.createDistillationTarget({
|
|
2650
|
-
domainName,
|
|
2651
|
-
domainCategory: category,
|
|
2652
|
-
ownerScope: scope,
|
|
2653
|
-
granularity,
|
|
2654
|
-
taskScope,
|
|
2655
|
-
includeAreas: include ? include.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
2656
|
-
excludeAreas: exclude ? exclude.split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
2657
|
-
loadCondition: loadCondition || '',
|
|
2658
|
-
});
|
|
2659
|
-
|
|
2660
|
-
const validation = distillationApi.validateDistillationTarget(target);
|
|
2661
|
-
if (!validation.valid) fail(`Invalid target: ${validation.errors.join('; ')}`);
|
|
2662
|
-
|
|
2663
|
-
project.distillation_target = target;
|
|
2664
|
-
writeProject(projectPath, project);
|
|
2665
|
-
console.log(JSON.stringify({ declared: true, target }, null, 2));
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
if (sub === 'show') {
|
|
2669
|
-
const { project } = readProject(args[1]);
|
|
2670
|
-
const target = project.distillation_target;
|
|
2671
|
-
if (!target) fail('No distillation target declared. Run: kdna-studio target declare <project>');
|
|
2672
|
-
console.log(JSON.stringify(target, null, 2));
|
|
2673
|
-
return;
|
|
2674
|
-
}
|
|
2675
|
-
fail('Usage: kdna-studio target <declare|show>');
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
function cmdSourceClassify(args) {
|
|
2679
|
-
const projectInput = args[0];
|
|
2680
|
-
if (!projectInput) fail('Usage: kdna-studio source <project>');
|
|
2681
|
-
const { projectPath, project } = readProject(projectInput);
|
|
2682
|
-
const target = project.distillation_target;
|
|
2683
|
-
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
2684
|
-
|
|
2685
|
-
// Bug (#66 + #69): the previous version of this comment claimed
|
|
2686
|
-
// `evidence_materials` was canonical and `evidence` was the legacy
|
|
2687
|
-
// fallback. The reality was the opposite — `addEvidence` in
|
|
2688
|
-
// @aikdna/kdna-studio-core wrote only to `project.evidence`, so
|
|
2689
|
-
// every consumer that read `project.evidence_materials` (this
|
|
2690
|
-
// command, cmdFilter, cmdDistill) saw an empty list. The fix in
|
|
2691
|
-
// studio-core 1.7.5 makes `addEvidence` write to BOTH fields; the
|
|
2692
|
-
// canonical name is now the one this comment names, not the one
|
|
2693
|
-
// it used to name. The fallback `|| project.evidence` stays so
|
|
2694
|
-
// legacy projects (pre-1.7.5) still classify correctly.
|
|
2695
|
-
const evidence = project.evidence_materials || project.evidence || [];
|
|
2696
|
-
const results = evidence.map(e => {
|
|
2697
|
-
const text = (e.content || e.title || '').toLowerCase();
|
|
2698
|
-
const domainWords = (target.include_areas || []).concat([target.domain_category, target.task_scope]);
|
|
2699
|
-
const hitCount = domainWords.filter(w => text.includes(w.toLowerCase())).length;
|
|
2700
|
-
let relevance;
|
|
2701
|
-
if (hitCount >= 2) relevance = 'relevant';
|
|
2702
|
-
else if (hitCount === 1) relevance = 'weakly_relevant';
|
|
2703
|
-
else {
|
|
2704
|
-
const otherHits = Object.keys(distillationApi.DOMAIN_CATEGORIES)
|
|
2705
|
-
.filter(k => k !== target.domain_category)
|
|
2706
|
-
.filter(k => text.includes(k)).length;
|
|
2707
|
-
relevance = otherHits > 0 ? 'split_domain' : 'weakly_relevant';
|
|
2708
|
-
}
|
|
2709
|
-
return { id: e.id, title: e.title, relevance };
|
|
2710
|
-
});
|
|
2711
|
-
|
|
2712
|
-
const summary = {};
|
|
2713
|
-
for (const r of results) { summary[r.relevance] = (summary[r.relevance] || 0) + 1; }
|
|
2714
|
-
|
|
2715
|
-
project.distillation_evidence_relevance = results;
|
|
2716
|
-
writeProject(projectPath, project);
|
|
2717
|
-
console.log(JSON.stringify({ summary, results }, null, 2));
|
|
2718
|
-
}
|
|
2719
|
-
|
|
2720
|
-
async function cmdDistill(args) {
|
|
2721
|
-
const projectInput = args[0];
|
|
2722
|
-
const candidatesFile = option(args, '--candidates');
|
|
2723
|
-
const useAI = args.includes('--ai');
|
|
2724
|
-
// Bug (#1 UX): prior version called resolveApiKey(args)
|
|
2725
|
-
// unconditionally, which is fine when --candidates is set (the
|
|
2726
|
-
// call is a no-op) but it also surfaced the env-var resolution
|
|
2727
|
-
// on every run, so a caller that never asked for the LLM was
|
|
2728
|
-
// still at the mercy of `resolveApiKey` warnings. Defer the
|
|
2729
|
-
// LLM-key resolution to the --ai branch.
|
|
2730
|
-
const provider = useAI ? option(args, '--provider') : null;
|
|
2731
|
-
const model = useAI ? option(args, '--model') : null;
|
|
2732
|
-
const apiKey = useAI ? resolveApiKey(args) : null;
|
|
2733
|
-
if (!projectInput) fail('Usage: kdna-studio distill <project> [--ai] [--candidates <file.json>]');
|
|
2734
|
-
if (!candidatesFile && !useAI) fail('Either --ai or --candidates <file.json> required.');
|
|
2735
|
-
|
|
2736
|
-
const { projectPath, project } = readProject(projectInput);
|
|
2737
|
-
const target = project.distillation_target;
|
|
2738
|
-
if (!target) fail('Declare a distillation target first: kdna-studio target declare <project>');
|
|
2739
|
-
|
|
2740
|
-
let rawCandidates;
|
|
2741
|
-
if (useAI) {
|
|
2742
|
-
const evidence = (project.evidence_materials || []).map(e => ({ filename: e.filename || e.name, content: e.raw_text || e.content || '' }));
|
|
2743
|
-
if (evidence.length === 0) console.warn('No evidence materials found. AI distillation works best with imported evidence. Run: kdna-studio import <project> <file>');
|
|
2744
|
-
|
|
2745
|
-
console.log('Running AI distillation...');
|
|
2746
|
-
rawCandidates = await ai.distill(llm.config(), evidence, target, { provider, model, apiKey });
|
|
2747
|
-
console.log(`AI extracted ${rawCandidates.length} candidates.`);
|
|
2748
|
-
} else {
|
|
2749
|
-
const rawPath = path.resolve(candidatesFile);
|
|
2750
|
-
if (!fs.existsSync(rawPath)) fail(`Candidates file not found: ${rawPath}`);
|
|
2751
|
-
const stats = fs.statSync(rawPath);
|
|
2752
|
-
if (stats.size > 50 * 1024 * 1024) {
|
|
2753
|
-
fail(`Candidates file too large (${(stats.size / (1024 * 1024)).toFixed(1)} MiB, max 50 MiB): ${rawPath}`);
|
|
2754
|
-
}
|
|
2755
|
-
rawCandidates = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
|
|
2756
|
-
if (!Array.isArray(rawCandidates)) fail('Candidates file must contain a JSON array');
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
const candidates = rawCandidates.map(c => {
|
|
2760
|
-
const base = {
|
|
2761
|
-
id: c.candidate_id || c.id || `cand_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2762
|
-
one_sentence: c.one_sentence || c.oneSentence || '',
|
|
2763
|
-
full_statement: c.full_statement || c.fullStatement || '',
|
|
2764
|
-
suggested_card_type: c.type || c.suggested_card_type || 'axiom',
|
|
2765
|
-
supporting_evidence_ids: (c.evidence_ids || c.evidenceIds || c.supporting_evidence_ids || []).map(String),
|
|
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
|
-
: {},
|
|
2777
|
-
candidate_status: 'proposed',
|
|
2778
|
-
scope_fit: c.scope_fit ?? c.scopeFit ?? true,
|
|
2779
|
-
};
|
|
2780
|
-
return distillationApi.applyScopeGate(base, target);
|
|
2781
|
-
});
|
|
2782
|
-
|
|
2783
|
-
const summary = distillationApi.candidateStatusSummary(candidates);
|
|
2784
|
-
project.distillation_candidates = candidates;
|
|
2785
|
-
writeProject(projectPath, project);
|
|
2786
|
-
console.log(JSON.stringify({ summary, count: candidates.length }, null, 2));
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
function cmdCandidate(args) {
|
|
2790
|
-
const sub = args[0];
|
|
2791
|
-
const projectInput = args[1];
|
|
2792
|
-
if (!sub || !projectInput) fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
|
|
2793
|
-
|
|
2794
|
-
const { projectPath, project } = readProject(projectInput);
|
|
2795
|
-
const candidates = project.distillation_candidates || [];
|
|
2796
|
-
|
|
2797
|
-
if (sub === 'list') {
|
|
2798
|
-
for (const c of candidates) {
|
|
2799
|
-
const scopeMarker = c.scope_fit ? '' : ' [OUT_OF_SCOPE]';
|
|
2800
|
-
const split = c.suggested_split_domain ? ` → ${c.suggested_split_domain}` : '';
|
|
2801
|
-
console.log(`${c.id}\t${c.candidate_status}\t${c.suggested_card_type}\t${c.confidence}\t${c.one_sentence.substring(0,60)}${scopeMarker}${split}`);
|
|
2802
|
-
}
|
|
2803
|
-
console.log(JSON.stringify(distillationApi.candidateStatusSummary(candidates), null, 2));
|
|
2804
|
-
return;
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
if (sub === 'accept' || sub === 'reject') {
|
|
2808
|
-
const candId = args[2];
|
|
2809
|
-
if (!candId) fail(`Usage: kdna-studio candidate ${sub} <project> <candidate-id>`);
|
|
2810
|
-
const idx = candidates.findIndex(c => c.id === candId);
|
|
2811
|
-
if (idx < 0) fail(`Candidate not found: ${candId}`);
|
|
2812
|
-
candidates[idx].candidate_status = sub === 'accept' ? 'accepted' : 'rejected';
|
|
2813
|
-
project.distillation_candidates = candidates;
|
|
2814
|
-
writeProject(projectPath, project);
|
|
2815
|
-
console.log(`${sub === 'accept' ? 'Accepted' : 'Rejected'}: ${candId}`);
|
|
2816
|
-
return;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
if (sub === 'override') {
|
|
2820
|
-
const candId = args[2];
|
|
2821
|
-
if (!candId) fail('Usage: kdna-studio candidate override <project> <candidate-id>');
|
|
2822
|
-
const idx = candidates.findIndex(c => c.id === candId);
|
|
2823
|
-
if (idx < 0) fail(`Candidate not found: ${candId}`);
|
|
2824
|
-
candidates[idx].scope_fit = true;
|
|
2825
|
-
candidates[idx].domain_relevance_score = 50;
|
|
2826
|
-
candidates[idx].relevance_evidence = 'User explicitly overrode scope gate via CLI';
|
|
2827
|
-
candidates[idx].suggested_split_domain = null;
|
|
2828
|
-
project.distillation_candidates = candidates;
|
|
2829
|
-
writeProject(projectPath, project);
|
|
2830
|
-
console.log(`Scope gate overridden: ${candId}`);
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
if (sub === 'promote') {
|
|
2835
|
-
const accepted = candidates.filter(
|
|
2836
|
-
c =>
|
|
2837
|
-
c.candidate_status === 'accepted' &&
|
|
2838
|
-
c.scope_fit !== false &&
|
|
2839
|
-
!c.promoted_card_id,
|
|
2840
|
-
);
|
|
2841
|
-
if (accepted.length === 0) fail('No candidates to promote. Accept some candidates first.');
|
|
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
|
-
|
|
2871
|
-
for (const c of accepted) {
|
|
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
|
-
};
|
|
2886
|
-
if (c.suggested_card_type === 'axiom') {
|
|
2887
|
-
fields.why = c.rationale || c.why;
|
|
2888
|
-
fields.failure_risk = c.misuse_risk || c.failure_risk;
|
|
2889
|
-
}
|
|
2890
|
-
const card = cardApi.createCard(c.suggested_card_type, fields);
|
|
2891
|
-
card.evidence_refs = Array.isArray(c.supporting_evidence_ids)
|
|
2892
|
-
? c.supporting_evidence_ids
|
|
2893
|
-
: [];
|
|
2894
|
-
project.cards = project.cards || [];
|
|
2895
|
-
project.cards.push(card);
|
|
2896
|
-
c.promoted_card_id = card.id;
|
|
2897
|
-
}
|
|
2898
|
-
if (project.stages?.judgment_cards) {
|
|
2899
|
-
project.stages.judgment_cards.total = project.cards.length;
|
|
2900
|
-
project.stages.judgment_cards.status = 'in_progress';
|
|
2901
|
-
}
|
|
2902
|
-
writeProject(projectPath, project);
|
|
2903
|
-
console.log(`Promoted ${accepted.length} candidates → ${accepted.length} Studio cards`);
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
fail('Usage: kdna-studio candidate <list|accept|reject|override|promote>');
|
|
2908
|
-
}
|
|
2909
|
-
|
|
2910
|
-
async function cmdInterview(args) {
|
|
2911
|
-
const projectInput = args[0];
|
|
2912
|
-
const stage = option(args, '--stage');
|
|
2913
|
-
if (!projectInput) fail('Usage: kdna-studio interview <project> [--stage distill|clarify|correct|replay]');
|
|
2914
|
-
const { projectPath, project } = readProject(projectInput);
|
|
2915
|
-
let results;
|
|
2916
|
-
|
|
2917
|
-
if (stage) {
|
|
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];
|
|
2927
|
-
console.log(`\n=== ${result.label} ===\n${result.content}`);
|
|
2928
|
-
} else {
|
|
2929
|
-
results = await ai.interview.runInterviewInteractive(llm.config(), project, {});
|
|
2930
|
-
}
|
|
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
|
-
);
|
|
2950
|
-
}
|
|
2951
|
-
writeProject(projectPath, project);
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
const args = process.argv.slice(2);
|
|
2955
|
-
const cmd = args[0];
|
|
2956
|
-
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
2957
|
-
usage();
|
|
2958
|
-
process.exit(EXIT.OK);
|
|
2959
|
-
}
|
|
2960
|
-
if (cmd === '--version' || cmd === '-v') {
|
|
2961
|
-
console.log(require('../package.json').version);
|
|
2962
|
-
process.exit(EXIT.OK);
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
(async () => {
|
|
2966
|
-
try {
|
|
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));
|
|
2984
|
-
else if (cmd === 'migrate') cmdMigrate(args.slice(1));
|
|
2985
|
-
else if (cmd === 'import') cmdImport(args.slice(1));
|
|
2986
|
-
else if (cmd === 'filter') cmdFilter(args.slice(1));
|
|
2987
|
-
else if (cmd === 'target') cmdTarget(args.slice(1));
|
|
2988
|
-
else if (cmd === 'source') cmdSourceClassify(args.slice(1));
|
|
2989
|
-
else if (cmd === 'distill') await cmdDistill(args.slice(1));
|
|
2990
|
-
else if (cmd === 'candidate') cmdCandidate(args.slice(1));
|
|
2991
|
-
else if (cmd === 'interview') await cmdInterview(args.slice(1));
|
|
2992
|
-
else if (cmd === 'card') cmdCard(args.slice(1));
|
|
2993
|
-
else if (cmd === 'lock') cmdLock(args.slice(1));
|
|
2994
|
-
else if (cmd === 'compile') cmdCompile(args.slice(1));
|
|
2995
|
-
else if (cmd === 'export') cmdExport(args.slice(1));
|
|
2996
|
-
else if (cmd === 'llm') cmdLlm(args.slice(1));
|
|
2997
|
-
else if (cmd === 'identity') cmdIdentity(args.slice(1));
|
|
2998
|
-
else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
|
|
2999
|
-
else if (cmd === 'install') cmdStudioInstall(args.slice(1));
|
|
3000
|
-
else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
|
|
3001
|
-
else if (cmd === 'export-route-card') cmdExportRouteCard(args.slice(1));
|
|
3002
|
-
else if (cmd === 'export-consumer-index') cmdExportConsumerIndex(args.slice(1));
|
|
3003
|
-
else {
|
|
3004
|
-
usage();
|
|
3005
|
-
fail(`Unknown command: ${cmd}`);
|
|
3006
|
-
}
|
|
3007
|
-
} catch (err) {
|
|
3008
|
-
fail(err.message || String(err));
|
|
3009
|
-
}
|
|
3010
|
-
})();
|
|
3011
|
-
|
|
3012
|
-
function cmdExportRouteCard(args) {
|
|
3013
|
-
const domainId = args[0];
|
|
3014
|
-
if (!domainId) fail('Usage: kdna-studio export-route-card <domain-id> [--out=<path>]');
|
|
3015
|
-
const outArg = option(args, '--out');
|
|
3016
|
-
const outPath = outArg || `${slugSegment(domainId, 'domain')}.route-card.json`;
|
|
3017
|
-
|
|
3018
|
-
const card = {
|
|
3019
|
-
route_card: "0.1.0",
|
|
3020
|
-
domain_id: domainId,
|
|
3021
|
-
role: "primary",
|
|
3022
|
-
boundaries: {
|
|
3023
|
-
applies_when: [],
|
|
3024
|
-
does_not_apply_when: [],
|
|
3025
|
-
},
|
|
3026
|
-
neighbors: [],
|
|
3027
|
-
advisor_edges: [],
|
|
3028
|
-
provenance: {
|
|
3029
|
-
generated_by: "kdna-studio",
|
|
3030
|
-
generated_at: new Date().toISOString(),
|
|
3031
|
-
input_hash: crypto.createHash("sha256").update(domainId).digest("hex").slice(0, 12),
|
|
3032
|
-
review_status: "draft_generated",
|
|
3033
|
-
},
|
|
3034
|
-
};
|
|
3035
|
-
|
|
3036
|
-
const absOut = path.resolve(outPath);
|
|
3037
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
3038
|
-
fs.writeFileSync(absOut, JSON.stringify(card, null, 2) + "\n");
|
|
3039
|
-
console.log(`Route card skeleton written: ${absOut}`);
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
function cmdExportConsumerIndex(args) {
|
|
3043
|
-
const entriesArg = option(args, '--entries');
|
|
3044
|
-
const outArg = option(args, '--out');
|
|
3045
|
-
const outPath = outArg || 'consumer-index.json';
|
|
3046
|
-
|
|
3047
|
-
const domainIds = entriesArg
|
|
3048
|
-
? entriesArg.split(",").map((s) => s.trim()).filter(Boolean)
|
|
3049
|
-
: [];
|
|
3050
|
-
|
|
3051
|
-
const index = {
|
|
3052
|
-
consumer_index: "0.1.0",
|
|
3053
|
-
entries: domainIds.map((domainId) => ({
|
|
3054
|
-
domain_id: domainId,
|
|
3055
|
-
status: "draft_generated",
|
|
3056
|
-
enabled: false,
|
|
3057
|
-
route_preference: {
|
|
3058
|
-
primary_for: [],
|
|
3059
|
-
advisor_for: [],
|
|
3060
|
-
never_for: [],
|
|
3061
|
-
},
|
|
3062
|
-
provenance: {
|
|
3063
|
-
generated_by: "kdna-studio",
|
|
3064
|
-
generated_at: new Date().toISOString(),
|
|
3065
|
-
input_hash: crypto.createHash("sha256").update(domainId).digest("hex").slice(0, 12),
|
|
3066
|
-
source: "studio-export",
|
|
3067
|
-
},
|
|
3068
|
-
})),
|
|
3069
|
-
};
|
|
3070
|
-
|
|
3071
|
-
const absOut = path.resolve(outPath);
|
|
3072
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
3073
|
-
fs.writeFileSync(absOut, JSON.stringify(index, null, 2) + "\n");
|
|
3074
|
-
console.log(`Consumer index skeleton written: ${absOut}`);
|
|
3075
|
-
}
|
|
2
|
+
'use strict';
|
|
3
|
+
require('../src/terminal-workspace.js').main(process.argv.slice(2)).catch(error => {
|
|
4
|
+
const code = typeof error.code === 'string' && /^[A-Z][A-Z0-9_]+$/.test(error.code) ? error.code : 'CLI_FAILURE';
|
|
5
|
+
process.stderr.write(JSON.stringify({ status: 'rejected', code }) + '\n');
|
|
6
|
+
process.exitCode = 2;
|
|
7
|
+
});
|