@aikdna/studio-core 0.1.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.
@@ -0,0 +1,327 @@
1
+ const { test, describe } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const { createProject, validateProject } = require('../src/project');
5
+ const { createCard, lockCard, transitionCard, createFeynmanRestatement, attachRestatementToLock, validateRestatementCard } = require('../src/cards');
6
+ const { evaluateRestatementQuality } = require('../src/cards/feynman');
7
+ const { detectContradictions, summarizeContradictions } = require('../src/quality/contradiction');
8
+ const { computeReadiness } = require('../src/quality');
9
+ const { compileDomain } = require('../src/compile');
10
+ const { createEvidenceEntry, addEvidence, extractSpan, linkEvidenceToCard, getEvidenceForCard } = require('../src/evidence');
11
+ const { createTestCase, recordHumanRating, linkTestToCards, generateTestSummary, exportEvals } = require('../src/testlab');
12
+ const { buildProvenance } = require('../src/provenance');
13
+
14
+ function makeLockedCard(type, fields = {}, id = null) {
15
+ const card = createCard(type, fields, id);
16
+ transitionCard(card, 'revised', { by: 'tester' });
17
+ lockCard(card, {
18
+ by: 'tester',
19
+ statement: 'I confirm this judgment represents my understanding.',
20
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
21
+ });
22
+ return card;
23
+ }
24
+
25
+ // ─── Feynman Restatement ──────────────────────────────────────────────
26
+
27
+ describe('Feynman Restatement', () => {
28
+ test('createFeynmanRestatement scores a good restatement', () => {
29
+ const card = makeLockedCard('axiom', {
30
+ one_sentence: 'Price objections are certainty deficits, not price problems.',
31
+ });
32
+ const text = 'When a customer says the price is too high, the agent should not immediately offer a discount. Instead, figure out which type of uncertainty is blocking them — value, risk, responsibility, or process. For example, if a client hesitates, ask what they are worried about rather than lowering the price.';
33
+ const fr = createFeynmanRestatement(card, text);
34
+ assert.ok(fr.text);
35
+ assert.ok(fr.score);
36
+ assert.equal(fr.score.quality, 'good');
37
+ assert.equal(fr.score.not_just_repeat, true);
38
+ assert.equal(fr.score.has_concrete_example, true);
39
+ assert.equal(fr.score.clarifies_boundary, true);
40
+ });
41
+
42
+ test('createFeynmanRestatement detects just-repeating', () => {
43
+ const card = makeLockedCard('axiom', {
44
+ one_sentence: 'Clarity is the only thing a writer needs to worry about when creating content.',
45
+ });
46
+ const text = 'Clarity is the only thing a writer needs to worry about when creating content for readers.';
47
+ const fr = createFeynmanRestatement(card, text);
48
+ assert.equal(fr.score.not_just_repeat, false);
49
+ });
50
+
51
+ test('attachRestatementToLock links restatement to card', () => {
52
+ const card = makeLockedCard('axiom', { one_sentence: 'Test.' });
53
+ const fr = createFeynmanRestatement(card, 'A simple explanation with an example and a boundary: this only works when there is clear evidence, not when the situation is ambiguous.');
54
+ attachRestatementToLock(card, fr);
55
+ assert.ok(card.feynman_restatement);
56
+ assert.equal(card.feynman_restatement.score.quality, 'good');
57
+ });
58
+
59
+ test('validateRestatementCard flags missing Feynman', () => {
60
+ const card = makeLockedCard('axiom', { one_sentence: 'Test' });
61
+ const issues = validateRestatementCard(card);
62
+ assert.ok(issues.length > 0);
63
+ assert.ok(issues.some(i => i.type === 'missing_feynman'));
64
+ });
65
+ });
66
+
67
+ // ─── Contradiction Check ──────────────────────────────────────────────
68
+
69
+ describe('Contradiction Check', () => {
70
+ test('detects missing does_not_apply_when on locked axiom', () => {
71
+ const card = makeLockedCard('axiom', {
72
+ one_sentence: 'Always trust the data.',
73
+ full_statement: 'When making decisions, always defer to data.',
74
+ applies_when: ['when data is available'],
75
+ // missing does_not_apply_when
76
+ });
77
+ const issues = detectContradictions([card]);
78
+ const boundary = issues.find(i => i.type === 'missing_boundary');
79
+ assert.ok(boundary);
80
+ assert.equal(boundary.severity, 'blocking');
81
+ });
82
+
83
+ test('detects over-generalized language', () => {
84
+ const card = makeLockedCard('axiom', {
85
+ one_sentence: 'Never accept user input without validation.',
86
+ full_statement: 'All user input must always be validated before processing.',
87
+ applies_when: ['when receiving input'],
88
+ });
89
+ const issues = detectContradictions([card]);
90
+ const overGen = issues.find(i => i.type === 'overgeneralized');
91
+ assert.ok(overGen);
92
+ });
93
+
94
+ test('detects not-a-question self-check', () => {
95
+ const card = makeLockedCard('self_check', {
96
+ question: 'The response should be helpful',
97
+ });
98
+ const issues = detectContradictions([card]);
99
+ const notQ = issues.find(i => i.type === 'not_a_question');
100
+ assert.ok(notQ);
101
+ });
102
+
103
+ test('detects generic self-check wording', () => {
104
+ const card = makeLockedCard('self_check', {
105
+ question: 'Is this response good enough?',
106
+ });
107
+ const issues = detectContradictions([card]);
108
+ const generic = issues.find(i => i.type === 'generic_check');
109
+ assert.ok(generic);
110
+ });
111
+
112
+ test('summarizeContradictions provides counts', () => {
113
+ const card = makeLockedCard('axiom', {
114
+ one_sentence: 'Always test.',
115
+ full_statement: 'Test.',
116
+ applies_when: ['when testing'],
117
+ });
118
+ const issues = detectContradictions([card]);
119
+ const summary = summarizeContradictions(issues);
120
+ assert.ok(summary.total > 0);
121
+ assert.ok('blocking' in summary);
122
+ assert.ok('warnings' in summary);
123
+ assert.ok(summary.by_type);
124
+ });
125
+ });
126
+
127
+ // ─── Evidence ──────────────────────────────────────────────────────────
128
+
129
+ describe('Evidence', () => {
130
+ test('createEvidenceEntry generates ID and hash', () => {
131
+ const ev = createEvidenceEntry('text', 'Test Evidence', 'This is some test content for judgment extraction.');
132
+ assert.ok(ev.id.startsWith('ev_'));
133
+ assert.ok(ev.content_hash.startsWith('sha256:'));
134
+ });
135
+
136
+ test('addEvidence adds to project and updates stage', () => {
137
+ const project = createProject('test');
138
+ const ev = createEvidenceEntry('text', 'Title', 'Content');
139
+ addEvidence(project, ev);
140
+ assert.equal(project.evidence.length, 1);
141
+ assert.equal(project.stages.evidence_room.evidence_count, 1);
142
+ assert.equal(project.stages.evidence_room.status, 'in_progress');
143
+ });
144
+
145
+ test('extractSpan creates annotation', () => {
146
+ const ev = createEvidenceEntry('text', 'Test', 'The expert said that price objections are really about uncertainty, not actual price concerns.');
147
+ const span = extractSpan(ev, 18, 90, 'possible_judgment');
148
+ assert.ok(span.id.startsWith('span_'));
149
+ assert.equal(span.candidate_pattern, 'possible_judgment');
150
+ assert.equal(ev.spans.length, 1);
151
+ });
152
+
153
+ test('linkEvidenceToCard adds ref', () => {
154
+ const card = createCard('axiom', {});
155
+ const ev = createEvidenceEntry('text', 'T', 'content');
156
+ extractSpan(ev, 0, 10);
157
+ linkEvidenceToCard(ev, ev.spans[0].id, card);
158
+ assert.ok(card.evidence_refs.length > 0);
159
+ });
160
+ });
161
+
162
+ // ─── Test Lab ──────────────────────────────────────────────────────────
163
+
164
+ describe('Test Lab', () => {
165
+ test('createTestCase generates test with ID', () => {
166
+ const tc = createTestCase('Help me improve this blog post', {
167
+ expectedWithout: 'Generic writing advice',
168
+ expectedWith: 'Structural diagnosis with evidence density check',
169
+ });
170
+ assert.ok(tc.id.startsWith('test_'));
171
+ assert.ok(tc.input);
172
+ assert.equal(tc.result, null);
173
+ });
174
+
175
+ test('recordHumanRating sets result', () => {
176
+ const tc = createTestCase('input');
177
+ recordHumanRating(tc, 'with_kdna_better', 'tester', 'KDNA clearly improved the diagnosis.');
178
+ assert.equal(tc.result, 'with_kdna_better');
179
+ assert.equal(tc.rated_by, 'tester');
180
+ assert.ok(tc.rated_at);
181
+ });
182
+
183
+ test('recordHumanRating rejects invalid result', () => {
184
+ const tc = createTestCase('input');
185
+ assert.throws(() => recordHumanRating(tc, 'invalid'), /Invalid result/);
186
+ });
187
+
188
+ test('linkTestToCards connects test to cards', () => {
189
+ const tc = createTestCase('input');
190
+ linkTestToCards(tc, ['card_1', 'card_2']);
191
+ linkTestToCards(tc, ['card_2', 'card_3']); // deduplicated
192
+ assert.equal(tc.linked_cards.length, 3);
193
+ });
194
+
195
+ test('generateTestSummary calculates stats', () => {
196
+ const project = createProject('test');
197
+ project.tests = [];
198
+ for (let i = 0; i < 4; i++) {
199
+ const tc = createTestCase(`input ${i}`);
200
+ recordHumanRating(tc, i < 3 ? 'with_kdna_better' : 'no_difference', 'tester');
201
+ project.tests.push(tc);
202
+ }
203
+ const summary = generateTestSummary(project);
204
+ assert.equal(summary.total, 4);
205
+ assert.equal(summary.with_kdna_better, 3);
206
+ assert.equal(summary.no_difference, 1);
207
+ assert.equal(summary.with_kdna_better_pct, 75);
208
+ assert.equal(summary.passing, true);
209
+ });
210
+
211
+ test('exportEvals filters rated tests', () => {
212
+ const project = createProject('test');
213
+ project.tests = [];
214
+ const rated = createTestCase('rated');
215
+ recordHumanRating(rated, 'with_kdna_better', 'tester');
216
+ project.tests.push(rated);
217
+ project.tests.push(createTestCase('unrated'));
218
+ const evals = exportEvals(project);
219
+ assert.equal(evals.length, 1);
220
+ assert.equal(evals[0].result, 'with_kdna_better');
221
+ });
222
+ });
223
+
224
+ // ─── Full Workflow: create → evidence → cards → lock → Feynman → check → compile ──
225
+
226
+ describe('Full Authoring Workflow', () => {
227
+ test('end-to-end: evidence → card → lock → Feynman → contradiction → compile', () => {
228
+ const project = createProject('leadership_decisions', 'domain', {
229
+ author: { name: 'Expert', id: 'expert_001' },
230
+ });
231
+
232
+ // Stage 1: Add evidence
233
+ const ev1 = createEvidenceEntry('text', 'Expert Interview', 'In 15 years of leadership coaching, I have noticed that most "execution failures" are actually decisions that were never properly made in the first place.', 'manual');
234
+ addEvidence(project, ev1);
235
+
236
+ // Stage 2: Create judgment cards
237
+ const ax1 = createCard('axiom', {
238
+ one_sentence: 'Execution failure is often decision failure in disguise.',
239
+ 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.',
240
+ why: 'Without this axiom, managers address symptoms (motivation, training, process) while missing the root cause.',
241
+ applies_when: ['Team reports being stuck', 'Deadline was missed', 'Project not progressing'],
242
+ does_not_apply_when: ['Clear decision exists with owner', 'External blocker (vendor, regulation)'],
243
+ failure_risk: 'May cause managers to over-scrutinize decision quality when the real issue is resource availability.',
244
+ }, 'ax_001');
245
+ project.cards.push(ax1);
246
+
247
+ const ms1 = createCard('misunderstanding', {
248
+ wrong: 'If the team is not executing, they lack motivation or skills.',
249
+ correct: 'If the team is not executing, first check whether a real decision was ever made — with owner, deadline, and criteria.',
250
+ key_distinction: 'Motivation gaps produce gradual decline. Decision voids produce sudden stalls. The pattern is different.',
251
+ }, 'ms_001');
252
+ project.cards.push(ms1);
253
+
254
+ const sc1 = createCard('self_check', {
255
+ question: 'Before suggesting that the team lacks motivation, did I verify that a concrete decision with owner+deadline+criteria exists?',
256
+ }, 'sc_001');
257
+ project.cards.push(sc1);
258
+
259
+ // Stage 3: Lock cards
260
+ const locked = [];
261
+ for (const card of [ax1, ms1, sc1]) {
262
+ transitionCard(card, 'revised', { by: 'expert_001' });
263
+ lockCard(card, {
264
+ by: 'expert_001',
265
+ statement: 'This judgment represents my 15 years of leadership coaching experience.',
266
+ checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
267
+ });
268
+ locked.push(card);
269
+ }
270
+
271
+ // Stage 4: Feynman restatements
272
+ for (const card of locked) {
273
+ const fr = createFeynmanRestatement(card,
274
+ card.type === 'axiom'
275
+ ? 'When a team is stuck and not making progress, do not immediately assume they lack skills or motivation. First check if a clear decision was actually made — meaning someone was named as responsible, a deadline was set, and criteria for success were defined. If not, you have a decision void, not an execution problem.'
276
+ : card.type === 'misunderstanding'
277
+ ? 'The difference between motivation failure and decision failure is like the difference between a car running out of gas (gradual) versus a car that was never given a destination (immediate stall). One fades, the other stops dead.'
278
+ : 'Before concluding the team is the problem, do this quick check: was there a specific person who knew they were responsible? Was there a date? Were there concrete criteria for done? If any of these is missing, fix the decision first, then check execution.'
279
+ );
280
+ attachRestatementToLock(card, fr);
281
+ }
282
+
283
+ // Stage 5: Contradiction check
284
+ const issues = detectContradictions(project.cards);
285
+ const summary = summarizeContradictions(issues);
286
+
287
+ // Stage 6: Compile
288
+ const result = compileDomain(project);
289
+
290
+ // Stage 7: Quality
291
+ const readiness = computeReadiness(project);
292
+
293
+ // Stage 8: Provenance
294
+ const provenance = buildProvenance(project, result.files);
295
+
296
+ // Assertions
297
+ assert.equal(project.cards.length, 3);
298
+ assert.equal(project.evidence.length, 1);
299
+ // Verify Feynman quality (all cards should have at least 'acceptable')
300
+ for (const card of project.cards) {
301
+ assert.ok(card.feynman_restatement);
302
+ const quality = card.feynman_restatement.score.quality;
303
+ assert.ok(quality === 'good' || quality === 'acceptable', `Expected good or acceptable, got ${quality} for ${card.id}`);
304
+ }
305
+
306
+ // Verify contradiction check found no blocking issues
307
+ assert.equal(summary.blocking, 0, 'No blocking issues expected');
308
+
309
+ // Verify compile includes all 3 cards
310
+ assert.equal(result.stats.locked_cards, 3);
311
+ assert.equal(result.stats.excluded_cards, 0);
312
+
313
+ // Verify output structure
314
+ const core = JSON.parse(result.files['KDNA_Core.json']);
315
+ assert.equal(core.axioms.length, 1);
316
+ assert.equal(core.axioms[0].id, 'ax_001');
317
+
318
+ const patterns = JSON.parse(result.files['KDNA_Patterns.json']);
319
+ assert.equal(patterns.misunderstandings.length, 1);
320
+ assert.equal(patterns.self_check.length, 1);
321
+
322
+ // Verify provenance
323
+ assert.ok(provenance.build_id);
324
+ assert.ok(provenance.content_fingerprint);
325
+ assert.equal(provenance.locked_card_count, 3);
326
+ });
327
+ });