@aikdna/kdna-cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,33 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { CANONICAL_REGISTRY_URL, REGISTRY_CACHE, fetchRegistry } = require('../registry');
4
- const { error, readJson, loadRegistry, INSTALL_DIR } = require('./_common');
4
+ const { error, readJson, loadRegistry, INSTALL_DIR, EXIT } = require('./_common');
5
5
 
6
- function cmdList(showAvailable) {
6
+ function cmdList(showAvailable, jsonMode = false) {
7
7
  if (showAvailable) {
8
8
  const domains = loadRegistry({ allowNetwork: true });
9
9
  if (!domains || !domains.length) {
10
+ if (jsonMode) {
11
+ console.log(JSON.stringify([]));
12
+ process.exit(EXIT.OK);
13
+ }
10
14
  error('No registry found.');
11
15
  }
12
16
 
17
+ if (jsonMode) {
18
+ const result = domains.map((d) => ({
19
+ name: d.name || d.id || null,
20
+ version: d.version || null,
21
+ type: d.type || 'domain',
22
+ status: d.status || null,
23
+ description: d.description || null,
24
+ yanked: d.yanked || false,
25
+ deprecated: d.deprecated || false,
26
+ }));
27
+ console.log(JSON.stringify(result));
28
+ process.exit(EXIT.OK);
29
+ }
30
+
13
31
  console.log('Available KDNA domains:');
14
32
  console.log(`Registry: ${REGISTRY_CACHE}`);
15
33
  console.log('');
@@ -30,6 +48,10 @@ function cmdList(showAvailable) {
30
48
  }
31
49
 
32
50
  if (!fs.existsSync(INSTALL_DIR)) {
51
+ if (jsonMode) {
52
+ console.log(JSON.stringify([]));
53
+ process.exit(EXIT.OK);
54
+ }
33
55
  console.log('No domains installed.');
34
56
  console.log(`Installation directory: ${INSTALL_DIR}`);
35
57
  return;
@@ -61,38 +83,56 @@ function cmdList(showAvailable) {
61
83
  }
62
84
 
63
85
  // Detect and warn about legacy (un-scoped) installs
64
- const legacy = fs.readdirSync(INSTALL_DIR).filter((d) => {
65
- if (d.startsWith('@') || d.startsWith('.')) return false;
66
- try {
67
- return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
68
- } catch {
69
- return false;
86
+ if (!jsonMode) {
87
+ const legacy = fs.readdirSync(INSTALL_DIR).filter((d) => {
88
+ if (d.startsWith('@') || d.startsWith('.')) return false;
89
+ try {
90
+ return fs.statSync(path.join(INSTALL_DIR, d)).isDirectory();
91
+ } catch {
92
+ return false;
93
+ }
94
+ });
95
+ if (legacy.length) {
96
+ console.log('⚠ Legacy (un-scoped) directories detected — please remove + re-install:');
97
+ legacy.forEach((d) => console.log(` ~/.kdna/domains/${d}/`));
98
+ console.log('');
70
99
  }
71
- });
72
- if (legacy.length) {
73
- console.log('⚠ Legacy (un-scoped) directories detected — please remove + re-install:');
74
- legacy.forEach((d) => console.log(` ~/.kdna/domains/${d}/`));
75
- console.log('');
76
100
  }
77
101
 
78
102
  if (!installed.length) {
103
+ if (jsonMode) {
104
+ console.log(JSON.stringify([]));
105
+ process.exit(EXIT.OK);
106
+ }
79
107
  console.log('No v0.7 domains installed.');
80
108
  console.log(`Run: kdna install <name> # e.g. kdna install writing`);
81
109
  return;
82
110
  }
83
111
 
84
- console.log('Installed KDNA domains:');
85
- console.log('');
86
- for (const { scope, ident, full } of installed) {
112
+ // Build structured data for installed domains
113
+ const domains = installed.map(({ scope, ident, full }) => {
87
114
  const core = readJson(path.join(full, 'KDNA_Core.json'));
88
115
  const manifest = readJson(path.join(full, 'kdna.json'));
89
116
  const cluster = readJson(path.join(full, 'cluster.json'));
90
- const name = `${scope}/${ident}`;
91
- const version = manifest?.version || manifest?._source?.version || core?.meta?.version || '?';
92
- const kind = cluster ? '[cluster]' : '';
93
- const desc = manifest?.description || core?.meta?.purpose || '';
94
- console.log(` ${name.padEnd(36)} v${version} ${kind}`);
95
- if (desc) console.log(` ${desc}`);
117
+ return {
118
+ name: `${scope}/${ident}`,
119
+ version: manifest?.version || manifest?._source?.version || core?.meta?.version || '?',
120
+ type: cluster ? 'cluster' : 'domain',
121
+ description: manifest?.description || core?.meta?.purpose || '',
122
+ };
123
+ });
124
+
125
+ if (jsonMode) {
126
+ console.log(JSON.stringify(domains));
127
+ process.exit(EXIT.OK);
128
+ }
129
+
130
+ console.log('Installed KDNA domains:');
131
+ console.log('');
132
+ for (const d of domains) {
133
+ const kind = d.type === 'cluster' ? '[cluster]' : '';
134
+ console.log(` ${d.name.padEnd(36)} v${d.version} ${kind}`);
135
+ if (d.description) console.log(` ${d.description}`);
96
136
  }
97
137
  console.log('');
98
138
  console.log(`Location: ${INSTALL_DIR}`);
@@ -0,0 +1,577 @@
1
+ /**
2
+ * KDNA Studio CLI commands — Phase 1: Studio Beta 底层支撑.
3
+ *
4
+ * kdna studio scaffold <name> Create Studio project skeleton
5
+ * kdna cards validate <project.json> Validate Judgment Cards
6
+ * kdna lock verify <project.json> Verify Human Lock status
7
+ * kdna studio compile <project.json> Compile locked cards into KDNA domain
8
+ * kdna studio readiness <project.json> Generate Domain Readiness Card
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { error, readJson, writeJson, EXIT } = require('./_common');
14
+
15
+ // ─── Scaffold ─────────────────────────────────────────────────────────
16
+
17
+ const CARD_TEMPLATES = {
18
+ axioms: [
19
+ {
20
+ id: 'ax_001',
21
+ one_sentence: '[TODO: One-line judgment principle]',
22
+ full_statement: '[TODO: Full statement — what the agent should do differently]',
23
+ why: '[TODO: What the agent would get wrong without this axiom]',
24
+ applies_when: [],
25
+ does_not_apply_when: [],
26
+ failure_risk: null,
27
+ confidence: null,
28
+ evidence_type: [],
29
+ locked: false,
30
+ human_lock: null,
31
+ feynman_restatement: null,
32
+ },
33
+ ],
34
+ ontology: [
35
+ {
36
+ id: 'ont_001',
37
+ concept: '[TODO: Core concept name]',
38
+ essence: '[TODO: Operational essence — what the agent checks]',
39
+ boundary: '[TODO: What this is not, or is often confused with]',
40
+ trigger_signal: '[TODO: Observable words/patterns the agent detects]',
41
+ locked: false,
42
+ human_lock: null,
43
+ feynman_restatement: null,
44
+ },
45
+ ],
46
+ misunderstandings: [
47
+ {
48
+ id: 'ms_001',
49
+ wrong: '[TODO: A belief a real agent might hold]',
50
+ correct: '[TODO: The correct judgment]',
51
+ key_distinction: '[TODO: Conceptual boundary between wrong and correct]',
52
+ failure_risk: null,
53
+ locked: false,
54
+ human_lock: null,
55
+ feynman_restatement: null,
56
+ },
57
+ ],
58
+ boundaries: [
59
+ {
60
+ id: 'bd_001',
61
+ scope: '[TODO: What this domain covers]',
62
+ out_of_scope: '[TODO: What this domain explicitly does not cover]',
63
+ acceptable_exceptions: [],
64
+ locked: false,
65
+ human_lock: null,
66
+ feynman_restatement: null,
67
+ },
68
+ ],
69
+ self_checks: [
70
+ {
71
+ id: 'sc_001',
72
+ question: '[TODO: A yes/no question the agent asks before final output?]',
73
+ applies_to: [],
74
+ locked: false,
75
+ human_lock: null,
76
+ feynman_restatement: null,
77
+ },
78
+ ],
79
+ };
80
+
81
+ function cmdStudioScaffold(name, args = []) {
82
+ if (!name) error('Usage: kdna studio scaffold <name> [--type=domain|cluster] [--minimal]', EXIT.INPUT_ERROR);
83
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
84
+ error(`Invalid name "${name}". Use lowercase letters, numbers, hyphens, underscores. Start with letter.`, EXIT.INPUT_ERROR);
85
+ }
86
+
87
+ const type = args.includes('--type=cluster') ? 'cluster' : 'domain';
88
+ const minimal = args.includes('--minimal');
89
+ const targetDir = path.resolve(name);
90
+
91
+ if (fs.existsSync(targetDir)) {
92
+ error(`Directory already exists: ${targetDir}`, EXIT.INPUT_ERROR);
93
+ }
94
+
95
+ const today = new Date().toISOString().slice(0, 10);
96
+
97
+ // Create directory structure
98
+ fs.mkdirSync(targetDir, { recursive: true });
99
+ fs.mkdirSync(path.join(targetDir, 'cards'), { recursive: true });
100
+ fs.mkdirSync(path.join(targetDir, 'exports'), { recursive: true });
101
+
102
+ // Write studio.project.json
103
+ const project = {
104
+ kdna_studio: '1.0-beta',
105
+ name,
106
+ type,
107
+ created: today,
108
+ updated: today,
109
+ cards: {
110
+ axioms: 'cards/axioms.json',
111
+ ontology: 'cards/ontology.json',
112
+ misunderstandings: 'cards/misunderstandings.json',
113
+ boundaries: 'cards/boundaries.json',
114
+ self_checks: 'cards/self_checks.json',
115
+ },
116
+ exports: {
117
+ dir: 'exports/',
118
+ },
119
+ };
120
+ writeJson(path.join(targetDir, 'studio.project.json'), project);
121
+
122
+ // Write card templates (skip ontology/boundaries in minimal mode)
123
+ const cardTypes = minimal
124
+ ? ['axioms', 'self_checks']
125
+ : ['axioms', 'ontology', 'misunderstandings', 'boundaries', 'self_checks'];
126
+
127
+ for (const type of cardTypes) {
128
+ const file = project.cards[type];
129
+ writeJson(path.join(targetDir, file), CARD_TEMPLATES[type]);
130
+ }
131
+
132
+ // Write exports/README.md
133
+ fs.writeFileSync(
134
+ path.join(targetDir, 'exports', 'README.md'),
135
+ `# ${name}\n\nCompiled KDNA domain files will appear here after \`kdna studio compile\`.\n`,
136
+ );
137
+
138
+ console.log(`✓ Studio project created: ${targetDir}/`);
139
+ console.log(` Type: ${type}${minimal ? ' (minimal)' : ''}`);
140
+ console.log(` Cards: ${cardTypes.join(', ')}`);
141
+ console.log('');
142
+ console.log('Next steps:');
143
+ console.log(' 1. Edit cards/ — replace all [TODO] placeholders');
144
+ console.log(' 2. Run: kdna cards validate studio.project.json');
145
+ console.log(' 3. Run: kdna lock verify studio.project.json');
146
+ console.log(' 4. Run: kdna studio compile studio.project.json');
147
+ }
148
+
149
+ // ─── Cards Validate ───────────────────────────────────────────────────
150
+
151
+ function cmdCardsValidate(projectPath, args = []) {
152
+ const jsonMode = args.includes('--json');
153
+ const abs = path.resolve(projectPath);
154
+
155
+ if (!fs.existsSync(abs)) error(`Project file not found: ${abs}`, EXIT.INPUT_ERROR);
156
+ const project = readJson(abs);
157
+ if (!project || !project.kdna_studio) error(`Not a KDNA Studio project: ${abs}`, EXIT.INPUT_ERROR);
158
+
159
+ const errors = [];
160
+ const warnings = [];
161
+ const passed = [];
162
+
163
+ function fail(msg) { errors.push(msg); }
164
+ function warn(msg) { warnings.push(msg); }
165
+ function ok(msg) { passed.push(msg); }
166
+
167
+ // Validate each card set
168
+ for (const [cardType, cardFile] of Object.entries(project.cards || {})) {
169
+ const cardPath = path.join(path.dirname(abs), cardFile);
170
+ if (!fs.existsSync(cardPath)) {
171
+ warn(`Card file not found: ${cardFile}`);
172
+ continue;
173
+ }
174
+
175
+ const cards = readJson(cardPath);
176
+ if (!cards || !Array.isArray(cards)) {
177
+ fail(`Invalid card file (not a JSON array): ${cardFile}`);
178
+ continue;
179
+ }
180
+
181
+ switch (cardType) {
182
+ case 'axioms':
183
+ for (const ax of cards) {
184
+ const label = ax.id || '?';
185
+ if (!ax.one_sentence || ax.one_sentence.includes('[TODO]')) {
186
+ warn(`axiom ${label}: one_sentence is placeholder`);
187
+ } else {
188
+ ok(`axiom ${label}: one_sentence OK`);
189
+ }
190
+ if (!ax.applies_when || !Array.isArray(ax.applies_when) || ax.applies_when.length === 0) {
191
+ fail(`axiom ${label}: missing applies_when`);
192
+ } else {
193
+ ok(`axiom ${label}: applies_when has ${ax.applies_when.length} entries`);
194
+ }
195
+ if (!ax.does_not_apply_when || !Array.isArray(ax.does_not_apply_when) || ax.does_not_apply_when.length === 0) {
196
+ fail(`axiom ${label}: missing does_not_apply_when`);
197
+ } else {
198
+ ok(`axiom ${label}: does_not_apply_when has ${ax.does_not_apply_when.length} entries`);
199
+ }
200
+ if (!ax.failure_risk) {
201
+ fail(`axiom ${label}: missing failure_risk`);
202
+ } else {
203
+ ok(`axiom ${label}: failure_risk declared`);
204
+ }
205
+ }
206
+ break;
207
+
208
+ case 'misunderstandings':
209
+ for (const ms of cards) {
210
+ const label = ms.id || '?';
211
+ if (!ms.wrong || ms.wrong.includes('[TODO]')) warn(`misunderstanding ${label}: wrong is placeholder`);
212
+ else ok(`misunderstanding ${label}: wrong OK`);
213
+ if (!ms.correct || ms.correct.includes('[TODO]')) warn(`misunderstanding ${label}: correct is placeholder`);
214
+ else ok(`misunderstanding ${label}: correct OK`);
215
+ if (!ms.key_distinction || ms.key_distinction.length < 15) {
216
+ fail(`misunderstanding ${label}: key_distinction missing or too short`);
217
+ } else {
218
+ ok(`misunderstanding ${label}: key_distinction OK`);
219
+ }
220
+ }
221
+ break;
222
+
223
+ case 'boundaries':
224
+ for (const bd of cards) {
225
+ const label = bd.id || '?';
226
+ if (!bd.scope || bd.scope.includes('[TODO]')) warn(`boundary ${label}: scope is placeholder`);
227
+ else ok(`boundary ${label}: scope OK`);
228
+ if (!bd.out_of_scope || bd.out_of_scope.includes('[TODO]')) warn(`boundary ${label}: out_of_scope is placeholder`);
229
+ else ok(`boundary ${label}: out_of_scope OK`);
230
+ if (!bd.acceptable_exceptions || !Array.isArray(bd.acceptable_exceptions)) {
231
+ warn(`boundary ${label}: acceptable_exceptions not declared`);
232
+ } else {
233
+ ok(`boundary ${label}: acceptable_exceptions has ${bd.acceptable_exceptions.length} entries`);
234
+ }
235
+ }
236
+ break;
237
+
238
+ case 'self_checks':
239
+ for (const sc of cards) {
240
+ const label = sc.id || '?';
241
+ if (!sc.question || sc.question.includes('[TODO]')) {
242
+ warn(`self_check ${label}: question is placeholder`);
243
+ } else if (!sc.question.trim().endsWith('?')) {
244
+ fail(`self_check ${label}: question should end with "?"`);
245
+ } else {
246
+ ok(`self_check ${label}: question OK`);
247
+ }
248
+ }
249
+ break;
250
+
251
+ case 'ontology':
252
+ for (const ont of cards) {
253
+ const label = ont.id || '?';
254
+ if (!ont.essence || ont.essence.includes('[TODO]')) warn(`ontology ${label}: essence is placeholder`);
255
+ else ok(`ontology ${label}: essence OK`);
256
+ if (!ont.boundary || ont.boundary.includes('[TODO]')) warn(`ontology ${label}: boundary is placeholder`);
257
+ else ok(`ontology ${label}: boundary OK`);
258
+ if (!ont.trigger_signal || ont.trigger_signal.includes('[TODO]')) warn(`ontology ${label}: trigger_signal is placeholder`);
259
+ else ok(`ontology ${label}: trigger_signal OK`);
260
+ }
261
+ break;
262
+ }
263
+ }
264
+
265
+ if (jsonMode) {
266
+ console.log(JSON.stringify({
267
+ project: path.basename(abs),
268
+ valid: errors.length === 0,
269
+ errors,
270
+ warnings,
271
+ passed: passed.length,
272
+ total_checks: errors.length + warnings.length + passed.length,
273
+ }, null, 2));
274
+ process.exit(errors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
275
+ }
276
+
277
+ if (warnings.length) {
278
+ console.log('Warnings:');
279
+ warnings.forEach((w) => console.log(` ⚠ ${w}`));
280
+ }
281
+ if (errors.length) {
282
+ console.error('Errors:');
283
+ errors.forEach((e) => console.error(` ✗ ${e}`));
284
+ }
285
+ if (passed.length) {
286
+ console.log(`✓ ${passed.length} checks passed`);
287
+ }
288
+
289
+ if (errors.length) process.exit(EXIT.VALIDATION_FAILED);
290
+ console.log(`✓ All ${passed.length} checks passed (no errors)`);
291
+ }
292
+
293
+ // ─── Lock Verify ──────────────────────────────────────────────────────
294
+
295
+ function cmdLockVerify(projectPath, args = []) {
296
+ const jsonMode = args.includes('--json');
297
+ const abs = path.resolve(projectPath);
298
+
299
+ if (!fs.existsSync(abs)) error(`Project file not found: ${abs}`, EXIT.INPUT_ERROR);
300
+ const project = readJson(abs);
301
+ if (!project || !project.kdna_studio) error(`Not a KDNA Studio project: ${abs}`, EXIT.INPUT_ERROR);
302
+
303
+ const locked = [];
304
+ const unlocked = [];
305
+ const blocking = [];
306
+
307
+ for (const [cardType, cardFile] of Object.entries(project.cards || {})) {
308
+ const cardPath = path.join(path.dirname(abs), cardFile);
309
+ if (!fs.existsSync(cardPath)) {
310
+ blocking.push(`${cardType}: file not found (${cardFile})`);
311
+ continue;
312
+ }
313
+
314
+ const cards = readJson(cardPath);
315
+ if (!cards || !Array.isArray(cards)) {
316
+ blocking.push(`${cardType}: invalid file`);
317
+ continue;
318
+ }
319
+
320
+ for (const card of cards) {
321
+ const label = `${cardType}.${card.id || '?'}`;
322
+ if (card.locked === true) {
323
+ if (!card.human_lock || !card.human_lock.by || !card.human_lock.at) {
324
+ locked.push(label);
325
+ } else {
326
+ // Check Feynman restatement for axioms and misunderstandings
327
+ if ((cardType === 'axioms' || cardType === 'misunderstandings') && !card.feynman_restatement) {
328
+ unlocked.push(label);
329
+ blocking.push(`${label} missing Feynman restatement`);
330
+ } else {
331
+ locked.push(label);
332
+ }
333
+ }
334
+ } else {
335
+ unlocked.push(label);
336
+ blocking.push(`${label} requires human lock`);
337
+ }
338
+ }
339
+ }
340
+
341
+ const publishable = blocking.length === 0 && locked.length > 0;
342
+
343
+ if (jsonMode) {
344
+ console.log(JSON.stringify({
345
+ project: path.basename(abs),
346
+ locked_cards: locked.length,
347
+ unlocked_cards: unlocked.length,
348
+ publishable,
349
+ blocking,
350
+ locked: locked.sort(),
351
+ unlocked: unlocked.sort(),
352
+ }, null, 2));
353
+ process.exit(publishable ? EXIT.OK : EXIT.HUMAN_LOCK_REQUIRED);
354
+ }
355
+
356
+ console.log(`Lock status for: ${path.basename(abs)}`);
357
+ console.log('');
358
+ console.log(` Locked: ${locked.length}`);
359
+ console.log(` Unlocked: ${unlocked.length}`);
360
+ console.log(` Publishable: ${publishable ? '✓ yes' : '✗ no'}`);
361
+ console.log('');
362
+
363
+ if (blocking.length) {
364
+ console.log('Blocking issues:');
365
+ blocking.forEach((b) => console.log(` ✗ ${b}`));
366
+ console.log('');
367
+ }
368
+
369
+ if (locked.length) {
370
+ console.log('Locked cards:');
371
+ locked.forEach((l) => console.log(` ✓ ${l}`));
372
+ }
373
+
374
+ process.exit(publishable ? EXIT.OK : EXIT.HUMAN_LOCK_REQUIRED);
375
+ }
376
+
377
+ // ─── Studio Compile ───────────────────────────────────────────────────
378
+
379
+ function cmdStudioCompile(projectPath, args = []) {
380
+ const abs = path.resolve(projectPath);
381
+
382
+ if (!fs.existsSync(abs)) error(`Project file not found: ${abs}`, EXIT.INPUT_ERROR);
383
+ const project = readJson(abs);
384
+ if (!project || !project.kdna_studio) error(`Not a KDNA Studio project: ${abs}`, EXIT.INPUT_ERROR);
385
+
386
+ // Determine output directory
387
+ const outIdx = args.indexOf('--out');
388
+ const outDir = outIdx >= 0
389
+ ? path.resolve(args[outIdx + 1])
390
+ : path.join(path.dirname(abs), project.exports?.dir || 'exports');
391
+
392
+ fs.mkdirSync(outDir, { recursive: true });
393
+
394
+ const today = new Date().toISOString().slice(0, 10);
395
+ const excluded = [];
396
+ const included = [];
397
+
398
+ // Compile axioms → KDNA_Core.json
399
+ const axioms = loadCards(project, path.dirname(abs), 'axioms');
400
+ const ontology = loadCards(project, path.dirname(abs), 'ontology');
401
+ const _boundaries = loadCards(project, path.dirname(abs), 'boundaries');
402
+
403
+ const core = {
404
+ meta: {
405
+ domain: project.name,
406
+ purpose: '',
407
+ language: 'en',
408
+ created: project.created || today,
409
+ updated: today,
410
+ },
411
+ axioms: axioms.locked.map((ax) => ({
412
+ id: ax.id,
413
+ one_sentence: ax.one_sentence,
414
+ full_statement: ax.full_statement,
415
+ why: ax.why,
416
+ applies_when: ax.applies_when || [],
417
+ does_not_apply_when: ax.does_not_apply_when || [],
418
+ failure_risk: ax.failure_risk || null,
419
+ confidence: ax.confidence || null,
420
+ evidence_type: ax.evidence_type || [],
421
+ })),
422
+ ontology: ontology.locked.map((ont) => ({
423
+ id: ont.id,
424
+ concept: ont.concept,
425
+ essence: ont.essence,
426
+ boundary: ont.boundary,
427
+ trigger_signal: ont.trigger_signal,
428
+ })),
429
+ stances: [],
430
+ frameworks: [],
431
+ core_structure: [],
432
+ };
433
+ excluded.push(
434
+ ...axioms.unlocked.map((ax) => `axiom ${ax.id || '?'} not locked`),
435
+ ...ontology.unlocked.map((ont) => `ontology ${ont.id || '?'} not locked`),
436
+ );
437
+
438
+ // Compile misunderstandings + self_checks + banned_terms → KDNA_Patterns.json
439
+ const misunderstandings = loadCards(project, path.dirname(abs), 'misunderstandings');
440
+ const selfChecks = loadCards(project, path.dirname(abs), 'self_checks');
441
+
442
+ const patterns = {
443
+ misunderstandings: misunderstandings.locked.map((ms) => ({
444
+ id: ms.id,
445
+ wrong: ms.wrong,
446
+ correct: ms.correct,
447
+ key_distinction: ms.key_distinction,
448
+ failure_risk: ms.failure_risk || null,
449
+ })),
450
+ self_check: selfChecks.locked.map((sc) => sc.question).filter(Boolean),
451
+ terminology: {
452
+ banned_terms: [],
453
+ preferred_terms: [],
454
+ },
455
+ };
456
+ excluded.push(
457
+ ...misunderstandings.unlocked.map((ms) => `misunderstanding ${ms.id || '?'} not locked`),
458
+ ...selfChecks.unlocked.map((sc) => `self_check ${sc.id || '?'} not locked`),
459
+ );
460
+
461
+ // Compile manifest → kdna.json
462
+ const manifest = {
463
+ kdna_spec: '1.0-rc',
464
+ name: `@aikdna/${project.name}`,
465
+ version: '0.1.0',
466
+ status: 'experimental',
467
+ access: 'open',
468
+ language: 'en',
469
+ author: { name: '', id: '' },
470
+ license: { type: 'CC-BY-4.0' },
471
+ description: '',
472
+ created: project.created || today,
473
+ updated: today,
474
+ };
475
+
476
+ writeJson(path.join(outDir, 'KDNA_Core.json'), core);
477
+ writeJson(path.join(outDir, 'KDNA_Patterns.json'), patterns);
478
+ writeJson(path.join(outDir, 'kdna.json'), manifest);
479
+
480
+ included.push(
481
+ `axioms: ${axioms.locked.length} included`,
482
+ `ontology: ${ontology.locked.length} included`,
483
+ `misunderstandings: ${misunderstandings.locked.length} included`,
484
+ `self_checks: ${selfChecks.locked.length} included`,
485
+ );
486
+
487
+ console.log(`✓ Compiled: ${outDir}/`);
488
+ console.log(` Included:`);
489
+ included.forEach((i) => console.log(` + ${i}`));
490
+
491
+ if (excluded.length) {
492
+ console.log(` Excluded (not locked):`);
493
+ excluded.forEach((e) => console.log(` - ${e}`));
494
+ }
495
+
496
+ console.log('');
497
+ console.log('Next:');
498
+ console.log(` kdna validate ${outDir}`);
499
+ }
500
+
501
+ function loadCards(project, projectDir, cardType) {
502
+ const cardFile = project.cards?.[cardType];
503
+ if (!cardFile) return { locked: [], unlocked: [] };
504
+
505
+ const cardPath = path.join(projectDir, cardFile);
506
+ if (!fs.existsSync(cardPath)) return { locked: [], unlocked: [] };
507
+
508
+ const cards = readJson(cardPath) || [];
509
+ return {
510
+ locked: cards.filter((c) => c.locked === true),
511
+ unlocked: cards.filter((c) => c.locked !== true),
512
+ };
513
+ }
514
+
515
+ // ─── Studio Readiness ─────────────────────────────────────────────────
516
+
517
+ function cmdStudioReadiness(projectPath, args = []) {
518
+ const jsonMode = args.includes('--json');
519
+ const abs = path.resolve(projectPath);
520
+
521
+ if (!fs.existsSync(abs)) error(`Project file not found: ${abs}`, EXIT.INPUT_ERROR);
522
+ const project = readJson(abs);
523
+ if (!project || !project.kdna_studio) error(`Not a KDNA Studio project: ${abs}`, EXIT.INPUT_ERROR);
524
+
525
+ const readiness = {
526
+ axioms: loadCardStats(project, path.dirname(abs), 'axioms'),
527
+ ontology: loadCardStats(project, path.dirname(abs), 'ontology'),
528
+ misunderstandings: loadCardStats(project, path.dirname(abs), 'misunderstandings'),
529
+ boundaries: loadCardStats(project, path.dirname(abs), 'boundaries'),
530
+ self_checks: loadCardStats(project, path.dirname(abs), 'self_checks'),
531
+ test_cases: 0,
532
+ human_pass: '0/0',
533
+ publishable: false,
534
+ };
535
+
536
+ // Determine publishability
537
+ const allTypes = Object.values(readiness).filter((v) => v && typeof v === 'object' && 'total' in v);
538
+ let allLocked = allTypes.every((t) => t.total > 0 && t.total === t.locked);
539
+ if (allTypes.length === 0) allLocked = false;
540
+ readiness.publishable = allLocked;
541
+
542
+ if (jsonMode) {
543
+ console.log(JSON.stringify(readiness, null, 2));
544
+ process.exit(readiness.publishable ? EXIT.OK : EXIT.HUMAN_LOCK_REQUIRED);
545
+ }
546
+
547
+ console.log(`Domain Readiness: ${project.name}`);
548
+ console.log('');
549
+ for (const [type, stats] of Object.entries(readiness)) {
550
+ if (!stats || typeof stats !== 'object' || !('total' in stats)) {
551
+ console.log(` ${type}: ${stats}`);
552
+ continue;
553
+ }
554
+ const marker = stats.total > 0 && stats.total === stats.locked ? '✓' : '○';
555
+ console.log(` ${marker} ${type}: ${stats.locked}/${stats.total} locked`);
556
+ }
557
+
558
+ console.log('');
559
+ console.log(` Publishable: ${readiness.publishable ? '✓ yes' : '✗ no'}`);
560
+ process.exit(readiness.publishable ? EXIT.OK : EXIT.HUMAN_LOCK_REQUIRED);
561
+ }
562
+
563
+ function loadCardStats(project, projectDir, cardType) {
564
+ const result = loadCards(project, projectDir, cardType);
565
+ return {
566
+ total: result.locked.length + result.unlocked.length,
567
+ locked: result.locked.length,
568
+ };
569
+ }
570
+
571
+ module.exports = {
572
+ cmdStudioScaffold,
573
+ cmdCardsValidate,
574
+ cmdLockVerify,
575
+ cmdStudioCompile,
576
+ cmdStudioReadiness,
577
+ };