@aikdna/studio-core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/studio-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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",
@@ -51,7 +51,8 @@ function compilePatterns(cards, project) {
51
51
  wrong: c.fields?.wrong || '',
52
52
  correct: c.fields?.correct || '',
53
53
  key_distinction: c.fields?.key_distinction || '',
54
- 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',
55
56
  applies_when: c.fields?.applies_when || [],
56
57
  does_not_apply_when: c.fields?.does_not_apply_when || [],
57
58
  }));
@@ -95,7 +96,7 @@ function compileReasoning(cards, project) {
95
96
  meta: makeMeta(project),
96
97
  reasoning_chains: lockedAxioms.map(ax => ({
97
98
  id: `chain_${ax.id}`,
98
- from: ax.fields?.one_sentence || '',
99
+ one_sentence: ax.fields?.one_sentence || '',
99
100
  logic: [ax.fields?.full_statement || ''],
100
101
  so_what: ax.fields?.why || 'Agent judgment changes when this axiom is loaded.',
101
102
  })),
@@ -112,22 +113,27 @@ function compileEvolution(cards, project) {
112
113
  if (seenAxioms.has(card.id)) continue;
113
114
  seenAxioms.add(card.id);
114
115
  for (const entry of (card.audit_log || [])) {
115
- if (entry.event === 'locked' || entry.event === 'published') {
116
- stages.push({ card_id: card.id, event: entry.event, at: entry.at, by: entry.by });
116
+ if (entry.event === 'locked') {
117
+ stages.push({
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'],
122
+ });
117
123
  }
118
124
  }
119
125
  }
120
126
 
121
127
  return {
122
128
  meta: makeMeta(project),
123
- stages: stages.sort((a, b) => a.at.localeCompare(b.at)),
124
- capability_layers: [
125
- { layer: 1, name: 'Foundation', description: 'Core axioms and patterns established.' },
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' },
126
132
  ],
127
- measurements: [
128
- { metric: 'locked_axioms', value: lockedCards.filter(c => c.type === 'axiom').length },
129
- { metric: 'locked_misunderstandings', value: lockedCards.filter(c => c.type === 'misunderstanding').length },
130
- { 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}` },
131
137
  ],
132
138
  };
133
139
  }
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ const cards = require('./cards');
21
21
  const compile = require('./compile');
22
22
  const evidence = require('./evidence');
23
23
  const packaging = require('./packaging');
24
+ const pipeline = require('./pipeline');
24
25
  const project = require('./project');
25
26
  const provenance = require('./provenance');
26
27
  const quality = require('./quality');
@@ -32,17 +33,23 @@ const validateCards = require('./quality/validate-cards');
32
33
  const delta = require('./testlab/delta');
33
34
 
34
35
  module.exports = {
36
+ // Stable
37
+ project,
35
38
  cards,
36
39
  compile,
37
- evidence,
38
- packaging,
39
- project,
40
- provenance,
41
40
  quality,
41
+ provenance,
42
+ pipeline,
43
+
44
+ // Experimental
45
+ evidence,
42
46
  testlab,
43
- versioning,
47
+ delta,
44
48
  feynman,
45
49
  contradiction,
46
50
  validateCards,
47
- delta,
51
+ versioning,
52
+
53
+ // Internal
54
+ packaging,
48
55
  };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * createStudioPipeline(project, options) — Official convenience API.
3
+ *
4
+ * Provides the recommended Studio workflow as a single callable pipeline.
5
+ * Third-party apps should use this instead of manually calling individual modules.
6
+ *
7
+ * Stable API (semver guaranteed).
8
+ */
9
+
10
+ const { validateProject } = require('./project');
11
+ const { computeReadiness } = require('./quality');
12
+ const { compileDomain, generateReadme } = require('./compile');
13
+ const { buildProvenance } = require('./provenance');
14
+ const { validateAllCards } = require('./quality/validate-cards');
15
+
16
+ function createStudioPipeline(project, options = {}) {
17
+ return new StudioPipeline(project, options);
18
+ }
19
+
20
+ class StudioPipeline {
21
+ constructor(project, options = {}) {
22
+ this.project = project;
23
+ this.options = options;
24
+ this.results = {};
25
+ }
26
+
27
+ validateProject() {
28
+ const r = validateProject(this.project);
29
+ this.results.project_valid = r;
30
+ return this;
31
+ }
32
+
33
+ validateCards() {
34
+ const issues = validateAllCards(this.project);
35
+ this.results.card_validation = { total: issues.length, issues };
36
+ return this;
37
+ }
38
+
39
+ computeReadiness() {
40
+ const r = computeReadiness(this.project);
41
+ this.results.readiness = r;
42
+ return this;
43
+ }
44
+
45
+ compile() {
46
+ const r = compileDomain(this.project);
47
+ this.results.compile = r;
48
+ return this;
49
+ }
50
+
51
+ generateReadme(readmeOptions = {}) {
52
+ const r = generateReadme(this.project, readmeOptions);
53
+ this.results.readme = r;
54
+ return this;
55
+ }
56
+
57
+ buildProvenance() {
58
+ if (!this.results.compile) throw new Error('Must call compile() before buildProvenance()');
59
+ const r = buildProvenance(this.project, this.results.compile.files);
60
+ this.results.provenance = r;
61
+ return this;
62
+ }
63
+
64
+ runAll(options = {}) {
65
+ this.validateProject();
66
+ this.validateCards();
67
+ this.computeReadiness();
68
+ this.compile();
69
+ if (options.generateReadme !== false) this.generateReadme(options.readmeOptions);
70
+ if (options.buildProvenance !== false) this.buildProvenance();
71
+ return this.toResult();
72
+ }
73
+
74
+ get readyness() { return this.results.readiness; }
75
+ get compiled() { return this.results.compile; }
76
+ get kdnaFiles() { return this.results.compile?.files || {}; }
77
+ get isPublishable() { return this.results.readiness?.publishable === true; }
78
+
79
+ toResult() {
80
+ return {
81
+ project_valid: this.results.project_valid?.valid === true,
82
+ card_issues: this.results.card_validation?.total || 0,
83
+ readiness: this.results.readiness?.grade || 'unknown',
84
+ publishable: this.results.readiness?.publishable || false,
85
+ score: this.results.readiness?.score || 0,
86
+ kdna_files: this.results.compile?.stats?.kdna_files || 0,
87
+ locked_cards: this.results.compile?.stats?.locked_cards || 0,
88
+ excluded_cards: this.results.compile?.stats?.excluded_cards || 0,
89
+ build_id: this.results.provenance?.build_id || null,
90
+ fingerprint: this.results.provenance?.content_fingerprint || null,
91
+ blocking: this.results.readiness?.blocking || [],
92
+ warnings: this.results.readiness?.warnings || [],
93
+ next_step: this.results.readiness?.next_step || '',
94
+ };
95
+ }
96
+ }
97
+
98
+ module.exports = { createStudioPipeline };
@@ -0,0 +1,276 @@
1
+ /**
2
+ * End-to-end validation: prove Studio Core output passes KDNA SPEC validation.
3
+ *
4
+ * Tests:
5
+ * 1. compile → write files → kdna validate passes
6
+ * 2. compile → kdna pack → kdna inspect returns valid container
7
+ * 3. compile output matches KDNA SPEC reference structure
8
+ * 4. kdna-core schema validation on compiled output
9
+ */
10
+
11
+ const { test, describe } = require('node:test');
12
+ const assert = require('node:assert/strict');
13
+ const { execFileSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const { createProject } = require('../src/project');
19
+ const { createCard, lockCard, transitionCard, createFeynmanRestatement, attachRestatementToLock } = require('../src/cards');
20
+ const { compileDomain } = require('../src/compile');
21
+ const { buildProvenance } = require('../src/provenance');
22
+
23
+ function makeLockedCard(type, fields, id) {
24
+ const card = createCard(type, fields, id);
25
+ transitionCard(card, 'revised', { by: 'expert' });
26
+ lockCard(card, { by: 'expert', statement: 'I confirm this judgment.', checked: { applies_when: true, does_not_apply_when: true, failure_risk: true } });
27
+ return card;
28
+ }
29
+
30
+ function createFullProject() {
31
+ const project = createProject('leadership_decisions', 'domain', {
32
+ author: { name: 'Leadership Expert', id: 'expert_001' },
33
+ });
34
+ project.release = { version: '0.1.0', description: 'Leadership decision-making judgment — diagnose whether execution failures are really decision voids.' };
35
+
36
+ // Axioms
37
+ const ax1 = makeLockedCard('axiom', {
38
+ one_sentence: 'Execution failure is often decision failure in disguise.',
39
+ full_statement: 'When a team fails to execute, first check whether a real decision (with named owner, deadline, and criteria) was ever made. Most "execution problems" are decision voids.',
40
+ why: 'Without this axiom, managers address symptoms (motivation, process) while missing the root cause.',
41
+ applies_when: ['Team reports being stuck', 'Deadline was missed', 'Project not progressing'],
42
+ does_not_apply_when: ['Clear decision exists with owner', 'External blocker (vendor, regulation)'],
43
+ failure_risk: 'May cause over-scrutiny of decision quality when issue is resource availability.',
44
+ }, 'ax_001');
45
+ project.cards.push(ax1);
46
+
47
+ const ax2 = makeLockedCard('axiom', {
48
+ one_sentence: 'Broad agreement is not commitment. Only named ownership is commitment.',
49
+ full_statement: 'A decision without a single named owner with a deadline has no commitment. Multiple owners = no owner.',
50
+ why: 'Teams confuse "everyone nodding" with actual commitment. Without a named owner, no one wakes up responsible.',
51
+ applies_when: ['Meeting ends with "sounds good"', 'Decision without deadline', 'Multiple people assigned'],
52
+ does_not_apply_when: ['Solo work with clear self-accountability', 'Informal team alignment'],
53
+ failure_risk: 'May create unnecessary formality for trivial decisions.',
54
+ }, 'ax_002');
55
+ project.cards.push(ax2);
56
+
57
+ const ax3 = makeLockedCard('axiom', {
58
+ one_sentence: 'The cost of a slow decision is usually higher than the cost of an imperfect decision.',
59
+ full_statement: 'In leadership, decision speed compounds. A two-week delay for a perfect decision often costs more than an 80% decision made today.',
60
+ why: 'Leaders who wait for perfect information create organizational bottlenecks.',
61
+ applies_when: ['Decision is reversible', 'Stakes are below team/org level', 'More information unlikely to change outcome'],
62
+ does_not_apply_when: ['Safety-critical decisions', 'Irreversible resource commitments', 'Legal/compliance required decisions'],
63
+ failure_risk: 'May encourage premature decisions in high-stakes, irreversible situations.',
64
+ }, 'ax_003');
65
+ project.cards.push(ax3);
66
+
67
+ // Misunderstandings
68
+ const ms1 = makeLockedCard('misunderstanding', {
69
+ wrong: 'If the team is not executing, they lack motivation or skills.',
70
+ correct: 'If the team is not executing, first check whether a real decision was ever made with owner, deadline, and criteria.',
71
+ key_distinction: 'Motivation gaps produce gradual decline over weeks. Decision voids produce sudden stalls within days. The pattern is fundamentally different.',
72
+ }, 'ms_001');
73
+ project.cards.push(ms1);
74
+
75
+ const ms2 = makeLockedCard('misunderstanding', {
76
+ wrong: 'Consensus means everyone agrees with the decision.',
77
+ correct: 'Consensus means everyone understands the decision, knows their role in executing it, and commits to not blocking it — even if they disagree.',
78
+ key_distinction: 'Agreement is an emotional state. Commitment to execute is a behavioral contract. You need the latter, not the former.',
79
+ }, 'ms_002');
80
+ project.cards.push(ms2);
81
+
82
+ // Self-checks
83
+ for (let i = 0; i < 5; i++) {
84
+ const checks = [
85
+ 'Before concluding execution is the problem, did I verify that a named owner with a deadline exists?',
86
+ 'Does this decision have exactly one person who will wake up responsible for it tomorrow?',
87
+ 'Is this decision reversible enough that speed matters more than perfection?',
88
+ 'Did I check whether this is a decision void disguised as an execution gap?',
89
+ 'If I asked the team "who owns this?", would everyone point to the same person?',
90
+ ];
91
+ const sc = makeLockedCard('self_check', { question: checks[i] }, `sc_00${i + 1}`);
92
+ project.cards.push(sc);
93
+ }
94
+
95
+ // Feynman for axioms
96
+ attachRestatementToLock(ax1, createFeynmanRestatement(ax1,
97
+ 'When your team is stuck and nothing is moving forward, do not immediately assume they lack skills or motivation. First ask: was there a clear decision? Meaning: someone specific was named, a date was given, and everyone knows what "done" looks like. If any of these is missing, you have a decision problem pretending to be an execution problem.'));
98
+ attachRestatementToLock(ax2, createFeynmanRestatement(ax2,
99
+ 'After a meeting where everyone seems to agree, do not assume commitment happened. If you cannot name one specific person who knows they are accountable by a specific date, you do not have a decision — you have a discussion. Multiple owners means zero owners.'));
100
+ attachRestatementToLock(ax3, createFeynmanRestatement(ax3,
101
+ 'Waiting three weeks for the perfect answer is usually worse than making a good-enough decision today. The only exception is when the decision is irreversible — like hiring someone you cannot fire, or spending money you cannot get back. But most daily leadership decisions are reversible, so speed beats perfection.'));
102
+
103
+ return project;
104
+ }
105
+
106
+ const TMPDIR = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-studio-e2e-'));
107
+
108
+ // ─── E2E: compile → kdna validate ────────────────────────────────────
109
+
110
+ describe('E2E: compile → validate', () => {
111
+ test('compiled output passes kdna validate', () => {
112
+ const project = createFullProject();
113
+ const result = compileDomain(project);
114
+ const domainDir = path.join(TMPDIR, 'test-domain');
115
+
116
+ fs.mkdirSync(domainDir, { recursive: true });
117
+ for (const [filename, content] of Object.entries(result.files)) {
118
+ fs.writeFileSync(path.join(domainDir, filename), content);
119
+ }
120
+
121
+ try {
122
+ const validateResult = execFileSync('kdna', ['validate', domainDir], {
123
+ encoding: 'utf8', timeout: 30000,
124
+ });
125
+ assert.match(validateResult, /valid|✓/i);
126
+ } catch (e) {
127
+ const stderr = (e.stderr || '').toString();
128
+ // If kdna CLI not installed, skip gracefully
129
+ if (stderr.includes('kdna') || stderr.includes('not found')) {
130
+ console.log(' (kdna CLI not available — skipping validate test)');
131
+ return;
132
+ }
133
+ throw e;
134
+ }
135
+ });
136
+
137
+ test('compiled output structure matches KDNA SPEC', () => {
138
+ const project = createFullProject();
139
+ const result = compileDomain(project);
140
+
141
+ // Verify KDNA_Core.json has all SPEC-required fields
142
+ const core = JSON.parse(result.files['KDNA_Core.json']);
143
+ assert.ok(core.meta, 'meta required');
144
+ assert.equal(core.meta.domain, 'leadership_decisions');
145
+ assert.equal(core.meta.version, '0.1.0');
146
+ assert.ok(core.meta.purpose);
147
+ assert.ok(core.meta.load_condition);
148
+ assert.ok(Array.isArray(core.axioms), 'axioms array required');
149
+ assert.equal(core.axioms.length, 3);
150
+ assert.ok(Array.isArray(core.ontology), 'ontology array required');
151
+ assert.ok(Array.isArray(core.frameworks), 'frameworks array required');
152
+ assert.ok(Array.isArray(core.stances), 'stances array required');
153
+ assert.ok(Array.isArray(core.core_structure), 'core_structure array required');
154
+
155
+ // Verify KDNA_Patterns.json
156
+ const patterns = JSON.parse(result.files['KDNA_Patterns.json']);
157
+ assert.ok(patterns.meta, 'Patterns meta required');
158
+ assert.ok(Array.isArray(patterns.misunderstandings));
159
+ assert.equal(patterns.misunderstandings.length, 2);
160
+ assert.ok(Array.isArray(patterns.self_check));
161
+ assert.equal(patterns.self_check.length, 5);
162
+
163
+ // Verify kdna.json manifest
164
+ const manifest = JSON.parse(result.files['kdna.json']);
165
+ assert.equal(manifest.name, 'leadership_decisions');
166
+ assert.equal(manifest.kdna_spec, '1.0-rc');
167
+ assert.ok(manifest.file_count >= 2, `file_count should be >= 2, got ${manifest.file_count}`);
168
+
169
+ // Verify KDNA_Reasoning.json (should exist since we have axioms)
170
+ assert.ok('KDNA_Reasoning.json' in result.files, 'Reasoning file should exist');
171
+ const reasoning = JSON.parse(result.files['KDNA_Reasoning.json']);
172
+ assert.ok(reasoning.meta);
173
+ assert.ok(Array.isArray(reasoning.reasoning_chains));
174
+ assert.ok(reasoning.reasoning_chains.every(c => 'so_what' in c), 'Every chain must have so_what');
175
+ assert.ok(reasoning.reasoning_chains.every(c => 'one_sentence' in c), 'Every chain must have one_sentence');
176
+
177
+ // Verify KDNA_Evolution.json
178
+ assert.ok('KDNA_Evolution.json' in result.files, 'Evolution file should exist');
179
+ const evolution = JSON.parse(result.files['KDNA_Evolution.json']);
180
+ assert.ok(evolution.meta);
181
+ assert.ok(Array.isArray(evolution.measurement));
182
+ });
183
+
184
+ test('compiled axioms have required fields', () => {
185
+ const project = createFullProject();
186
+ const result = compileDomain(project);
187
+ const core = JSON.parse(result.files['KDNA_Core.json']);
188
+
189
+ for (const ax of core.axioms) {
190
+ assert.ok(ax.one_sentence, `${ax.id}: missing one_sentence`);
191
+ assert.ok(ax.full_statement, `${ax.id}: missing full_statement`);
192
+ assert.ok(ax.why, `${ax.id}: missing why`);
193
+ assert.ok(Array.isArray(ax.applies_when), `${ax.id}: applies_when must be array`);
194
+ assert.ok(Array.isArray(ax.does_not_apply_when), `${ax.id}: does_not_apply_when must be array`);
195
+ assert.ok(ax.failure_risk, `${ax.id}: missing failure_risk`);
196
+ }
197
+ });
198
+
199
+ test('compiled misunderstandings have key_distinction', () => {
200
+ const project = createFullProject();
201
+ const result = compileDomain(project);
202
+ const patterns = JSON.parse(result.files['KDNA_Patterns.json']);
203
+
204
+ for (const ms of patterns.misunderstandings) {
205
+ assert.ok(ms.key_distinction, `${ms.id}: missing key_distinction`);
206
+ assert.ok(ms.key_distinction.length >= 20, `${ms.id}: key_distinction too short`);
207
+ }
208
+ });
209
+ });
210
+
211
+ // ─── E2E: compile → pack → inspect ───────────────────────────────────
212
+
213
+ describe('E2E: compile → pack → inspect', () => {
214
+ test('compiled output survives pack→inspect round-trip', () => {
215
+ const project = createFullProject();
216
+ const result = compileDomain(project);
217
+ const domainDir = path.join(TMPDIR, 'pack-test');
218
+
219
+ fs.mkdirSync(domainDir, { recursive: true });
220
+ for (const [filename, content] of Object.entries(result.files)) {
221
+ fs.writeFileSync(path.join(domainDir, filename), content);
222
+ }
223
+
224
+ try {
225
+ // Pack
226
+ const packResult = execFileSync('kdna', ['pack', domainDir, '--output', TMPDIR], {
227
+ encoding: 'utf8', timeout: 60000,
228
+ });
229
+ assert.match(packResult, /Packed|✓/);
230
+
231
+ // Find the .kdna file
232
+ const kdnaFile = fs.readdirSync(TMPDIR).find(f => f.endsWith('.kdna'));
233
+ assert.ok(kdnaFile, 'Should produce a .kdna file');
234
+
235
+ // Inspect
236
+ const inspectResult = execFileSync('kdna', ['inspect', path.join(TMPDIR, kdnaFile), '--json'], {
237
+ encoding: 'utf8', timeout: 15000,
238
+ });
239
+ const inspected = JSON.parse(inspectResult);
240
+ assert.equal(inspected.name, 'leadership_decisions');
241
+ assert.equal(inspected.version, '0.1.0');
242
+ } catch (e) {
243
+ const stderr = (e.stderr || '').toString();
244
+ if (stderr.includes('kdna') || stderr.includes('not found')) {
245
+ console.log(' (kdna CLI not available — skipping pack/inspect test)');
246
+ return;
247
+ }
248
+ throw e;
249
+ }
250
+ });
251
+ });
252
+
253
+ // ─── E2E: provenance ─────────────────────────────────────────────────
254
+
255
+ describe('E2E: provenance completeness', () => {
256
+ test('provenance covers all required metadata', () => {
257
+ const project = createFullProject();
258
+ const result = compileDomain(project);
259
+ const prov = buildProvenance(project, result.files);
260
+
261
+ assert.equal(prov.studio_core, 'knowledge-dna/kdna-studio');
262
+ assert.ok(prov.studio_core_version);
263
+ assert.ok(prov.build_id);
264
+ assert.ok(prov.project_id);
265
+ assert.equal(prov.author_id, 'expert_001');
266
+ assert.equal(prov.locked_card_count, 10); // 3 axioms + 2 misunderstandings + 5 self-checks
267
+ assert.equal(prov.test_case_count, 0);
268
+ assert.ok(prov.built_at);
269
+ assert.ok(prov.content_fingerprint.startsWith('sha256:'));
270
+ });
271
+ });
272
+
273
+ // Cleanup
274
+ process.on('exit', () => {
275
+ try { fs.rmSync(TMPDIR, { recursive: true, force: true }); } catch {}
276
+ });
@@ -221,8 +221,8 @@ describe('Full Compile', () => {
221
221
  const result = compileDomain(project);
222
222
  const evo = JSON.parse(result.files['KDNA_Evolution.json']);
223
223
  assert.ok(evo.stages.length > 0);
224
- assert.equal(evo.measurements[0].metric, 'locked_axioms');
225
- assert.equal(evo.measurements[0].value, 1);
224
+ assert.equal(evo.measurement[0].what, 'locked_axioms');
225
+ assert.equal(evo.measurement[0].threshold, '1');
226
226
  });
227
227
  });
228
228