@aikdna/studio-core 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/studio-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "KDNA Studio Core — pure logic library for authoring, validating, and compiling KDNA domain judgment packages.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -1,114 +1,145 @@
1
1
  /**
2
- * Compile locked cards into KDNA domain JSON files.
2
+ * Compile locked cards into KDNA domain JSON files — SPEC-compatible output.
3
3
  *
4
- * Only locked cards enter compilation output.
5
- * Draft/Revised cards are silently excluded.
6
- * Supports full 6-file output: Core / Patterns / Scenarios / Cases / Reasoning / Evolution.
4
+ * KDNA SPEC v1.0-rc requirements:
5
+ * - Every file MUST have meta: { version, domain, created, purpose, load_condition }
6
+ * - Minimum output: KDNA_Core.json + KDNA_Patterns.json
7
+ * - Maximum 6 KDNA JSON files per domain
8
+ * - KDNA_Scenarios.json: { meta, scenes[] }
9
+ * - KDNA_Reasoning.json: { meta, reasoning_chains[] }
10
+ * - KDNA_Evolution.json: { meta, stages[], capability_layers[], measurements[] }
11
+ *
12
+ * Only locked cards enter compilation. Draft/Revised excluded silently.
7
13
  */
8
- const provenance = require('../provenance');
9
14
 
10
- function compileCore(cards) {
11
- const axioms = cards.filter(c => c.type === 'axiom' && c.locked).map(c => ({ id: c.id, ...c.fields }));
12
- const ontology = cards.filter(c => c.type === 'ontology' && c.locked).map(c => ({ id: c.id, ...c.fields }));
13
- const frameworks = [];
14
- const stances = [];
15
- const boundaries = cards.filter(c => c.type === 'boundary' && c.locked).map(c => ({
16
- id: c.id,
17
- scope: c.fields?.scope || '',
18
- out_of_scope: c.fields?.out_of_scope || '',
19
- acceptable_exceptions: c.fields?.acceptable_exceptions || [],
20
- }));
21
- const risks = cards.filter(c => c.type === 'risk' && c.locked).map(c => ({ id: c.id, ...c.fields }));
15
+ function makeMeta(project) {
16
+ return {
17
+ version: (project.release && project.release.version) || '0.1.0',
18
+ domain: project.name,
19
+ created: project.created || new Date().toISOString().slice(0, 10),
20
+ purpose: project.release?.description || `Domain judgment for ${project.name}`,
21
+ load_condition: 'Load when the task matches applies_when on domain axioms.',
22
+ };
23
+ }
24
+
25
+ function compileCore(cards, project) {
26
+ const lockedAxioms = cards.filter(c => c.type === 'axiom' && c.locked).map(c => ({ id: c.id, ...c.fields }));
27
+ const lockedOntology = cards.filter(c => c.type === 'ontology' && c.locked).map(c => ({ id: c.id, ...c.fields }));
28
+ const lockedBoundaries = cards.filter(c => c.type === 'boundary' && c.locked);
29
+ const lockedRisks = cards.filter(c => c.type === 'risk' && c.locked).map(c => ({ id: c.id, ...c.fields }));
22
30
 
23
- return { axioms, ontology, frameworks, stances, boundaries, risks };
31
+ return {
32
+ meta: makeMeta(project),
33
+ axioms: lockedAxioms,
34
+ ontology: lockedOntology,
35
+ frameworks: [],
36
+ stances: [],
37
+ core_structure: [],
38
+ boundaries: lockedBoundaries.map(c => ({
39
+ id: c.id,
40
+ scope: c.fields?.scope || '',
41
+ out_of_scope: c.fields?.out_of_scope || '',
42
+ acceptable_exceptions: c.fields?.acceptable_exceptions || [],
43
+ })),
44
+ risks: lockedRisks,
45
+ };
24
46
  }
25
47
 
26
- function compilePatterns(cards) {
27
- const misunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked).map(c => ({
48
+ function compilePatterns(cards, project) {
49
+ const lockedMisunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked).map(c => ({
28
50
  id: c.id,
29
51
  wrong: c.fields?.wrong || '',
30
52
  correct: c.fields?.correct || '',
31
53
  key_distinction: c.fields?.key_distinction || '',
32
- failure_risk: c.fields?.failure_risk || null,
54
+ why: c.fields?.why || `What bad judgment results from believing "${(c.fields?.wrong || '').slice(0, 40)}"`,
55
+ failure_risk: c.fields?.failure_risk || 'No specific failure risk declared',
33
56
  applies_when: c.fields?.applies_when || [],
34
57
  does_not_apply_when: c.fields?.does_not_apply_when || [],
35
58
  }));
36
- const selfCheckQuestions = cards.filter(c => c.type === 'self_check' && c.locked).map(c => c.fields?.question || '');
37
- const aesthetics = cards.filter(c => c.type === 'aesthetic' && c.locked).map(c => ({ id: c.id, ...c.fields }));
59
+ const lockedSelfChecks = cards.filter(c => c.type === 'self_check' && c.locked).map(c => c.fields?.question || '');
60
+ const lockedAesthetics = cards.filter(c => c.type === 'aesthetic' && c.locked).map(c => ({ id: c.id, ...c.fields }));
38
61
 
39
- const terminology = {
40
- standard_terms: [],
41
- banned_terms: [],
62
+ return {
63
+ meta: makeMeta(project),
64
+ terminology: {
65
+ standard_terms: [],
66
+ banned_terms: [],
67
+ },
68
+ misunderstandings: lockedMisunderstandings,
69
+ self_check: lockedSelfChecks,
70
+ aesthetics: lockedAesthetics,
42
71
  };
43
-
44
- return { terminology, misunderstandings, self_check: selfCheckQuestions, aesthetics };
45
72
  }
46
73
 
47
- function compileScenarios(cards) {
74
+ function compileScenarios(cards, project) {
48
75
  const locked = cards.filter(c => c.type === 'scenario' && c.locked);
49
- if (locked.length === 0) return [];
50
- return locked.map(c => ({ id: c.id, ...c.fields }));
76
+ if (locked.length === 0) return null;
77
+ return {
78
+ meta: makeMeta(project),
79
+ scenes: locked.map(c => ({ id: c.id, ...c.fields })),
80
+ };
51
81
  }
52
82
 
53
- function compileCases(cards) {
83
+ function compileCases(cards, project) {
54
84
  const locked = cards.filter(c => c.type === 'case' && c.locked);
55
- if (locked.length === 0) return [];
56
- return locked.map(c => ({ id: c.id, ...c.fields }));
85
+ if (locked.length === 0) return null;
86
+ return {
87
+ meta: makeMeta(project),
88
+ cases: locked.map(c => ({ id: c.id, ...c.fields })),
89
+ };
57
90
  }
58
91
 
59
- function compileReasoning(cards) {
60
- // Reasoning chains from axiom implications
92
+ function compileReasoning(cards, project) {
61
93
  const lockedAxioms = cards.filter(c => c.type === 'axiom' && c.locked);
62
- if (lockedAxioms.length === 0) return [];
63
- return lockedAxioms.map(ax => ({
64
- id: `chain_${ax.id}`,
65
- from: ax.fields?.one_sentence || '',
66
- logic: [ax.fields?.full_statement || ''],
67
- so_what: ax.fields?.why || 'Agent judgment changes when this axiom is loaded.',
68
- }));
94
+ if (lockedAxioms.length === 0) return null;
95
+ return {
96
+ meta: makeMeta(project),
97
+ reasoning_chains: lockedAxioms.map(ax => ({
98
+ id: `chain_${ax.id}`,
99
+ one_sentence: ax.fields?.one_sentence || '',
100
+ logic: [ax.fields?.full_statement || ''],
101
+ so_what: ax.fields?.why || 'Agent judgment changes when this axiom is loaded.',
102
+ })),
103
+ };
69
104
  }
70
105
 
71
- function compileEvolution(cards) {
106
+ function compileEvolution(cards, project) {
72
107
  const lockedCards = cards.filter(c => c.locked);
73
- if (lockedCards.length === 0) return { stages: [], capability_layers: [], measurements: [] };
108
+ if (lockedCards.length === 0) return null;
74
109
 
75
- // Build evolution from audit logs
76
110
  const stages = [];
77
111
  const seenAxioms = new Set();
78
112
  for (const card of lockedCards) {
79
113
  if (seenAxioms.has(card.id)) continue;
80
114
  seenAxioms.add(card.id);
81
115
  for (const entry of (card.audit_log || [])) {
82
- if (entry.event === 'locked' || entry.event === 'published') {
116
+ if (entry.event === 'locked') {
83
117
  stages.push({
84
- card_id: card.id,
85
- event: entry.event,
86
- at: entry.at,
87
- by: entry.by,
118
+ id: `stage_${card.id}`,
119
+ name: card.fields?.one_sentence || card.fields?.question || card.id,
120
+ description: `Card ${card.id} was locked by ${entry.by} at ${entry.at}. Type: ${card.type}.`,
121
+ indicators: [`${card.type} card locked`, 'Human judgment confirmed'],
88
122
  });
89
123
  }
90
124
  }
91
125
  }
92
126
 
93
127
  return {
94
- stages: stages.sort((a, b) => a.at.localeCompare(b.at)),
95
- capability_layers: [
96
- { layer: 1, name: 'Foundation', description: 'Core axioms and patterns established.' },
128
+ meta: makeMeta(project),
129
+ stages: stages.sort((a, b) => a.id.localeCompare(b.id)),
130
+ evolution_layers: [
131
+ { id: 'layer_1', name: 'Foundation', capability: 'Core axioms and patterns established.', from_stage: stages[0]?.id || 'none', to_stage: stages[stages.length - 1]?.id || 'none' },
97
132
  ],
98
- measurements: [
99
- { metric: 'locked_axioms', value: lockedCards.filter(c => c.type === 'axiom').length },
100
- { metric: 'locked_misunderstandings', value: lockedCards.filter(c => c.type === 'misunderstanding').length },
101
- { metric: 'self_checks', value: lockedCards.filter(c => c.type === 'self_check').length },
133
+ measurement: [
134
+ { id: 'meas_axioms', what: 'locked_axioms', how: 'Count of locked axiom cards', threshold: `${lockedCards.filter(c => c.type === 'axiom').length}` },
135
+ { id: 'meas_misunderstandings', what: 'locked_misunderstandings', how: 'Count of locked misunderstanding cards', threshold: `${lockedCards.filter(c => c.type === 'misunderstanding').length}` },
136
+ { id: 'meas_self_checks', what: 'self_checks', how: 'Count of locked self-check cards', threshold: `${lockedCards.filter(c => c.type === 'self_check').length}` },
102
137
  ],
103
138
  };
104
139
  }
105
140
 
106
- function compileManifest(project) {
107
- const lockedCount = (project.cards || []).filter(c => c.locked).length;
108
- const cards = project.cards || [];
109
- const hasScenarios = cards.some(c => c.type === 'scenario' && c.locked);
110
- const hasCases = cards.some(c => c.type === 'case' && c.locked);
111
- const fileCount = 2 + (hasScenarios ? 1 : 0) + (hasCases ? 1 : 0) + 2; // Core+Patterns+Reasoning+Evolution (+ Scenarios + Cases)
141
+ function compileManifest(project, files) {
142
+ const kdnaFileCount = Object.keys(files).filter(f => f.startsWith('KDNA_')).length;
112
143
  return {
113
144
  kdna_spec: '1.0-rc',
114
145
  name: project.name,
@@ -116,31 +147,30 @@ function compileManifest(project) {
116
147
  status: 'experimental',
117
148
  access: (project.release && project.release.access) || 'open',
118
149
  author: project.author || { name: '', id: '' },
119
- description: project.name,
120
- file_count: 2,
121
- created: project.created,
122
- updated: project.updated,
150
+ description: project.release?.description || project.name,
151
+ file_count: kdnaFileCount,
152
+ created: project.created || new Date().toISOString().slice(0, 10),
153
+ updated: project.updated || new Date().toISOString().slice(0, 10),
123
154
  };
124
155
  }
125
156
 
126
157
  function compileDomain(project) {
127
158
  const cards = project.cards || [];
128
- const core = compileCore(cards);
129
- const patterns = compilePatterns(cards);
130
- const scenarios = compileScenarios(cards);
131
- const cases = compileCases(cards);
132
- const reasoning = compileReasoning(cards);
133
- const evolution = compileEvolution(cards);
134
- const manifest = compileManifest(project);
159
+ const core = compileCore(cards, project);
160
+ const patterns = compilePatterns(cards, project);
161
+ const scenarios = compileScenarios(cards, project);
162
+ const cases = compileCases(cards, project);
163
+ const reasoning = compileReasoning(cards, project);
164
+ const evolution = compileEvolution(cards, project);
135
165
 
136
166
  const files = {};
137
167
  files['KDNA_Core.json'] = JSON.stringify(core, null, 2);
138
168
  files['KDNA_Patterns.json'] = JSON.stringify(patterns, null, 2);
139
- if (scenarios.length > 0) files['KDNA_Scenarios.json'] = JSON.stringify({ scenes: scenarios }, null, 2);
140
- if (cases.length > 0) files['KDNA_Cases.json'] = JSON.stringify({ cases }, null, 2);
141
- if (reasoning.length > 0) files['KDNA_Reasoning.json'] = JSON.stringify({ chains: reasoning }, null, 2);
142
- if (evolution.stages && evolution.stages.length > 0) files['KDNA_Evolution.json'] = JSON.stringify(evolution, null, 2);
143
- files['kdna.json'] = JSON.stringify(manifest, null, 2);
169
+ if (scenarios) files['KDNA_Scenarios.json'] = JSON.stringify(scenarios, null, 2);
170
+ if (cases) files['KDNA_Cases.json'] = JSON.stringify(cases, null, 2);
171
+ if (reasoning) files['KDNA_Reasoning.json'] = JSON.stringify(reasoning, null, 2);
172
+ if (evolution) files['KDNA_Evolution.json'] = JSON.stringify(evolution, null, 2);
173
+ files['kdna.json'] = JSON.stringify(compileManifest(project, files), null, 2);
144
174
 
145
175
  const excludedCount = cards.filter(c => !c.locked && !['deprecated'].includes(c.status)).length;
146
176
 
@@ -151,7 +181,8 @@ function compileDomain(project) {
151
181
  locked_cards: cards.filter(c => c.locked).length,
152
182
  excluded_cards: excludedCount,
153
183
  deprecated_cards: cards.filter(c => c.status === 'deprecated').length,
154
- files_output: Object.keys(files).length,
184
+ kdna_files: Object.keys(files).filter(f => f.startsWith('KDNA_')).length,
185
+ total_files: Object.keys(files).length,
155
186
  },
156
187
  };
157
188
  }
@@ -168,12 +199,8 @@ function generateReadme(project, options = {}) {
168
199
  const lines = [];
169
200
  lines.push(`# ${project.name}`);
170
201
  lines.push('');
171
- if (options.description) {
172
- lines.push(options.description);
173
- lines.push('');
174
- }
202
+ if (options.description) { lines.push(options.description); lines.push(''); }
175
203
 
176
- // Four Questions
177
204
  lines.push('## Where it comes from');
178
205
  lines.push('');
179
206
  lines.push(options.origin || `Domain expertise encoded into ${locked.length} judgment cards through structured interview and human lock.`);
@@ -182,11 +209,7 @@ function generateReadme(project, options = {}) {
182
209
  lines.push('## Where it applies');
183
210
  lines.push('');
184
211
  const appliesWhen = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.applies_when || []))];
185
- if (appliesWhen.length > 0) {
186
- appliesWhen.forEach(w => lines.push(`- ${w}`));
187
- } else {
188
- lines.push('- As declared in each axiom\'s applies_when field.');
189
- }
212
+ appliesWhen.length ? appliesWhen.forEach(w => lines.push(`- ${w}`)) : lines.push('- As declared in each axiom\'s applies_when field.');
190
213
  lines.push('');
191
214
 
192
215
  lines.push('## How it is verified');
@@ -200,19 +223,14 @@ function generateReadme(project, options = {}) {
200
223
  lines.push('## When it does NOT apply');
201
224
  lines.push('');
202
225
  const notApply = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.does_not_apply_when || []))];
203
- if (notApply.length > 0) {
204
- notApply.forEach(w => lines.push(`- ${w}`));
205
- }
206
- const outOfScope = lockedBoundaries.flatMap(b => [b.fields?.out_of_scope || '']).filter(Boolean);
207
- for (const oos of outOfScope) {
226
+ notApply.forEach(w => lines.push(`- ${w}`));
227
+ for (const oos of lockedBoundaries.flatMap(b => [b.fields?.out_of_scope || '']).filter(Boolean)) {
208
228
  if (!notApply.includes(oos)) lines.push(`- ${oos}`);
209
229
  }
210
230
  lines.push('');
211
231
 
212
- // Top Axioms
213
232
  if (lockedAxioms.length > 0) {
214
- lines.push('## Top Axioms');
215
- lines.push('');
233
+ lines.push('## Top Axioms'); lines.push('');
216
234
  lockedAxioms.forEach(ax => {
217
235
  lines.push(`- **${ax.fields?.one_sentence || ax.id}**`);
218
236
  if (ax.fields?.failure_risk) lines.push(` - Failure risk: ${ax.fields.failure_risk}`);
@@ -220,10 +238,8 @@ function generateReadme(project, options = {}) {
220
238
  lines.push('');
221
239
  }
222
240
 
223
- // Top Misunderstandings
224
241
  if (lockedMisunderstandings.length > 0) {
225
- lines.push('## Top Misunderstandings');
226
- lines.push('');
242
+ lines.push('## Top Misunderstandings'); lines.push('');
227
243
  lockedMisunderstandings.forEach(ms => {
228
244
  lines.push(`- WRONG: ${ms.fields?.wrong}`);
229
245
  lines.push(` CORRECT: ${ms.fields?.correct}`);
@@ -231,24 +247,20 @@ function generateReadme(project, options = {}) {
231
247
  lines.push('');
232
248
  }
233
249
 
234
- // Self-checks
235
250
  if (lockedSelfChecks.length > 0) {
236
- lines.push('## Eval Score');
237
- lines.push('');
251
+ lines.push('## Eval Score'); lines.push('');
238
252
  lines.push(`- quality_badge: ${tests.filter(t => t.result === 'with_kdna_better').length >= 5 ? 'tested' : 'untested'}`);
239
253
  lines.push(`- eval cases: ${tests.length}`);
240
254
  lines.push('');
241
255
  }
242
256
 
243
- // Files
244
- lines.push('## Files');
245
- lines.push('');
246
- const fileCount = 2
257
+ lines.push('## Files'); lines.push('');
258
+ const kdnaFileCount = 2
247
259
  + (cards.filter(c => c.type === 'scenario' && c.locked).length > 0 ? 1 : 0)
248
260
  + (cards.filter(c => c.type === 'case' && c.locked).length > 0 ? 1 : 0)
249
261
  + (lockedAxioms.length > 0 ? 1 : 0)
250
- + (cards.filter(c => c.status === 'locked' || c.status === 'tested').length > 0 ? 1 : 0);
251
- lines.push(`${fileCount} KDNA JSON files + evals/ + demo/`);
262
+ + (locked.length > 0 ? 1 : 0);
263
+ lines.push(`${kdnaFileCount} KDNA JSON files + evals/ + demo/`);
252
264
  lines.push('');
253
265
 
254
266
  return lines.join('\n');
@@ -1,30 +1,31 @@
1
1
  /**
2
- * Packaging adapter — call kdna-cli for pack, verify, sign, publish operations.
2
+ * Packaging adapter — secure delegation to kdna-cli.
3
3
  *
4
- * Studio Core does not re-implement these. It provides structured interfaces
5
- * that delegate to kdna-cli subprocess calls.
4
+ * All subprocess calls use execFileSync (not execSync with string interpolation)
5
+ * to prevent command injection. Studio Core calls kdna-cli as the canonical
6
+ * implementation of pack/verify/sign/publish operations.
6
7
  */
7
8
 
8
- const { execSync } = require('child_process');
9
+ const { execFileSync } = require('child_process');
9
10
  const path = require('path');
10
11
 
11
12
  function packDomain(domainDir, outputDir = null) {
12
13
  const args = ['pack', domainDir];
13
14
  if (outputDir) args.push('--output', outputDir);
14
- const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
15
+ const result = execFileSync('kdna', args, { encoding: 'utf8', timeout: 60000 });
15
16
  return { success: true, output: result.trim() };
16
17
  }
17
18
 
18
19
  function packEncryptedDomain(domainDir, licensePath, outputDir = null) {
19
20
  const args = ['pack', domainDir, '--encrypt', '--license', licensePath];
20
21
  if (outputDir) args.push('--output', outputDir);
21
- const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
22
+ const result = execFileSync('kdna', args, { encoding: 'utf8', timeout: 60000 });
22
23
  return { success: true, output: result.trim() };
23
24
  }
24
25
 
25
26
  function verifyDomain(domainPath) {
26
27
  try {
27
- const result = execSync(`kdna verify ${domainPath} --json`, { encoding: 'utf8' });
28
+ const result = execFileSync('kdna', ['verify', domainPath, '--json'], { encoding: 'utf8', timeout: 30000 });
28
29
  return JSON.parse(result);
29
30
  } catch (e) {
30
31
  const stdout = (e.stdout || '').toString();
@@ -32,23 +33,28 @@ function verifyDomain(domainPath) {
32
33
  }
33
34
  }
34
35
 
36
+ function validateDomain(domainPath) {
37
+ try {
38
+ const result = execFileSync('kdna', ['validate', domainPath], { encoding: 'utf8', timeout: 30000 });
39
+ return { success: true, output: result.trim() };
40
+ } catch (e) {
41
+ return { success: false, error: e.message, stderr: (e.stderr || '').toString() };
42
+ }
43
+ }
44
+
35
45
  function inspectContainer(filePath) {
36
46
  try {
37
- const result = execSync(`kdna inspect ${filePath} --json`, { encoding: 'utf8' });
47
+ const result = execFileSync('kdna', ['inspect', filePath, '--json'], { encoding: 'utf8', timeout: 15000 });
38
48
  return JSON.parse(result);
39
- } catch {
40
- return null;
41
- }
49
+ } catch { return null; }
42
50
  }
43
51
 
44
- function signDomain(domainDir, identityDir = null) {
45
- // Uses kdna publish --check for signing
46
- const args = ['publish', '--check', domainDir];
52
+ function signDomain(domainDir) {
47
53
  try {
48
- const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8', env: { ...process.env } });
54
+ const result = execFileSync('kdna', ['publish', '--check', domainDir], { encoding: 'utf8', timeout: 30000 });
49
55
  return { success: true, output: result.trim() };
50
56
  } catch (e) {
51
- return { success: false, error: e.stderr?.toString() || e.message };
57
+ return { success: false, error: (e.stderr || '').toString() || e.message };
52
58
  }
53
59
  }
54
60
 
@@ -56,18 +62,14 @@ function generateLicense(domain, issuedTo, savePath = null) {
56
62
  const args = ['license', 'generate', domain, '--to', issuedTo];
57
63
  if (savePath) args.push('--save', savePath);
58
64
  try {
59
- const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
65
+ const result = execFileSync('kdna', args, { encoding: 'utf8', timeout: 15000 });
60
66
  return { success: true, output: result.trim() };
61
67
  } catch (e) {
62
- return { success: false, error: e.stderr?.toString() || e.message };
68
+ return { success: false, error: (e.stderr || '').toString() || e.message };
63
69
  }
64
70
  }
65
71
 
66
72
  module.exports = {
67
- packDomain,
68
- packEncryptedDomain,
69
- verifyDomain,
70
- inspectContainer,
71
- signDomain,
72
- generateLicense,
73
+ packDomain, packEncryptedDomain, verifyDomain, validateDomain,
74
+ inspectContainer, signDomain, generateLicense,
73
75
  };