@aikdna/kdna-studio-cli 0.11.0 → 0.13.0-rc.components.2

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.
@@ -1,3075 +1,7 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
- const os = require('os');
4
- const path = require('path');
5
- const crypto = require('crypto');
6
-
7
- const zlib = require('zlib');
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
+ });