@aikdna/studio-core 0.2.0 → 0.3.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.2.0",
3
+ "version": "0.3.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",
package/src/index.js CHANGED
@@ -29,6 +29,7 @@ const versioning = require('./versioning');
29
29
  const feynman = require('./cards/feynman');
30
30
  const contradiction = require('./quality/contradiction');
31
31
  const validateCards = require('./quality/validate-cards');
32
+ const delta = require('./testlab/delta');
32
33
 
33
34
  module.exports = {
34
35
  cards,
@@ -43,4 +44,5 @@ module.exports = {
43
44
  feynman,
44
45
  contradiction,
45
46
  validateCards,
47
+ delta,
46
48
  };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Judgment Delta — Structured comparison of agent response with vs without KDNA.
3
+ *
4
+ * Parses kdna compare output (text or JSON) into structured axes:
5
+ * 1. CLASSIFICATION — how the task was classified
6
+ * 2. DIAGNOSIS — root cause identified
7
+ * 3. ACTIONS — what the response suggests
8
+ * 4. BOUNDARY — scope awareness
9
+ * 5. TERMINOLOGY — domain-specific terms used
10
+ *
11
+ * Also supports scoring along the D1-D7 dimensions defined in the
12
+ * KDNA Compare Report specification.
13
+ */
14
+
15
+ function parseCompareOutput(diffText) {
16
+ const axes = {};
17
+ const matches = diffText.matchAll(/^(\d)\.\s*(\w+(?:\s+\w+)*):\s*(.+)$/gim);
18
+ for (const m of matches) {
19
+ const name = m[2].toLowerCase().replace(/\s+/g, '_');
20
+ const value = m[3].trim();
21
+ if (value.toUpperCase() !== 'SAME') {
22
+ axes[name] = value;
23
+ }
24
+ }
25
+
26
+ // Legacy format: "<axis>: <value>"
27
+ if (Object.keys(axes).length === 0) {
28
+ const legacyMatch = diffText.matchAll(/^(\w+):\s*(.+)$/gim);
29
+ for (const m of legacyMatch) {
30
+ const name = m[1].toLowerCase();
31
+ const value = m[2].trim();
32
+ if (name === 'verdict') continue;
33
+ if (value.toUpperCase() !== 'SAME') {
34
+ axes[name] = value;
35
+ }
36
+ }
37
+ }
38
+
39
+ const verdictMatch = diffText.match(/VERDICT:\s*(.+)/i);
40
+ const verdict = verdictMatch ? verdictMatch[1].trim().toLowerCase() : 'trajectory_unchanged';
41
+
42
+ return { axes, verdict };
43
+ }
44
+
45
+ function scoreDelta(axes) {
46
+ let score = 5;
47
+ const changed = [];
48
+ for (const [axis, value] of Object.entries(axes)) {
49
+ changed.push({ axis, value: value.slice(0, 100) });
50
+ score = Math.min(10, score + 1);
51
+ }
52
+ return { score: Math.min(10, score), changed };
53
+ }
54
+
55
+ function createJudgmentDelta(domain, input, responseA, responseB, diffText, options = {}) {
56
+ const { axes, verdict } = parseCompareOutput(diffText);
57
+ const domainScore = scoreDelta(axes);
58
+ const triggeredAxioms = options.triggeredAxioms || [];
59
+ const avoidedMisunderstandings = options.avoidedMisunderstandings || [];
60
+ const selfChecksPassed = options.selfChecksPassed || null;
61
+
62
+ return {
63
+ meta: {
64
+ domain,
65
+ input: input.slice(0, 200),
66
+ model: options.model || 'unknown',
67
+ timestamp: new Date().toISOString(),
68
+ },
69
+ classification: {
70
+ without_kdna: axes.classification || 'generic',
71
+ with_kdna: axes.classification ? 'domain_specific' : 'unchanged',
72
+ changed: !!axes.classification,
73
+ },
74
+ axes,
75
+ verdict,
76
+ score: domainScore.score,
77
+ changed_dimensions: domainScore.changed,
78
+ triggered_axioms: triggeredAxioms,
79
+ avoided_misunderstandings: avoidedMisunderstandings,
80
+ self_checks_passed: selfChecksPassed,
81
+ scoring: buildScoring(axes, domainScore, selfChecksPassed),
82
+ summary: buildSummary(domain, domainScore, verdict),
83
+ };
84
+ }
85
+
86
+ function buildScoring(axes, domainScore, selfChecksPassed) {
87
+ return {
88
+ D1_diagnostic_depth: axes.diagnosis ? 8 : 5,
89
+ D2_terminology_precision: axes.terminology ? 8 : 5,
90
+ D3_misunderstanding_detection: 5,
91
+ D4_axiom_alignment: domainScore.score,
92
+ D5_self_check_pass_rate: selfChecksPassed !== null
93
+ ? `${selfChecksPassed}%`
94
+ : 'N/A',
95
+ D6_boundary_respect: axes.boundary_awareness || axes.boundary ? 'Pass' : 'N/A',
96
+ D7_risk_avoidance: 'N/A',
97
+ };
98
+ }
99
+
100
+ function buildSummary(domain, domainScore, verdict) {
101
+ const changed = domainScore.changed.map(c => `**${c.axis}**`).join(', ');
102
+ if (changed.length === 0) {
103
+ return `Loading \`${domain}\` did not significantly alter the judgment trajectory for this input.`;
104
+ }
105
+ if (verdict.includes('changed')) {
106
+ return `Loading \`${domain}\` changed the agent's response across ${domainScore.changed.length} dimensions: ${changed}. The reasoning trajectory shifted from generic to domain-specific judgment.`;
107
+ }
108
+ return `Loading \`${domain}\` produced changes in ${domainScore.changed.length} dimensions: ${changed}.`;
109
+ }
110
+
111
+ function compareDeltas(delta1, delta2) {
112
+ const diffs = [];
113
+ for (const axis of ['classification', 'diagnosis', 'actions', 'boundary_awareness', 'terminology']) {
114
+ const v1 = delta1.axes[axis] || 'SAME';
115
+ const v2 = delta2.axes[axis] || 'SAME';
116
+ if (v1 !== v2) {
117
+ diffs.push({ axis, before: v1, after: v2 });
118
+ }
119
+ }
120
+ return {
121
+ score_change: delta2.score - delta1.score,
122
+ verdict_before: delta1.verdict,
123
+ verdict_after: delta2.verdict,
124
+ axis_diffs: diffs,
125
+ improved: delta2.score > delta1.score,
126
+ };
127
+ }
128
+
129
+ function formatDeltaMarkdown(delta) {
130
+ const lines = [];
131
+ lines.push('# KDNA Judgment Comparison Report');
132
+ lines.push('');
133
+ lines.push(`**Domain:** ${delta.meta.domain}`);
134
+ lines.push(`**Model:** ${delta.meta.model}`);
135
+ lines.push(`**Date:** ${delta.meta.timestamp}`);
136
+ lines.push('');
137
+ lines.push('## Judgment Diff');
138
+ lines.push('');
139
+ lines.push('| Dimension | Change |');
140
+ lines.push('|-----------|--------|');
141
+ for (const d of delta.changed_dimensions) {
142
+ lines.push(`| ${d.axis} | **Changed**: ${d.value} |`);
143
+ }
144
+ if (delta.changed_dimensions.length === 0) {
145
+ lines.push('| (none) | No significant change |');
146
+ }
147
+ lines.push('');
148
+ lines.push('## Scoring');
149
+ lines.push('');
150
+ for (const [dim, value] of Object.entries(delta.scoring)) {
151
+ lines.push(`- **${dim}:** ${value}`);
152
+ }
153
+ lines.push('');
154
+ lines.push(`**Verdict:** ${delta.verdict.replace(/_/g, ' ')}`);
155
+ lines.push('');
156
+ lines.push(delta.summary);
157
+ return lines.join('\n');
158
+ }
159
+
160
+ module.exports = { parseCompareOutput, scoreDelta, createJudgmentDelta, compareDeltas, formatDeltaMarkdown };
@@ -1 +1,181 @@
1
- module.exports = {};
1
+ /**
2
+ * Versioning — Judgment-aware version management.
3
+ *
4
+ * KDNA versioning tracks judgment changes, not just text diffs.
5
+ * A version bump is based on:
6
+ * - PATCH: wording fixes, clarifications (no judgment change)
7
+ * - MINOR: new axioms, misunderstandings, self-checks added
8
+ * - MAJOR: axioms removed, domain scope changed, access mode changed
9
+ *
10
+ * Provides:
11
+ * - Judgment diff between two project snapshots
12
+ * - Changelog generation from audit logs
13
+ * - Version bump recommendation
14
+ * - Semantic version tracking
15
+ */
16
+
17
+ function diffProjects(oldProject, newProject) {
18
+ const oldCards = oldProject.cards || [];
19
+ const newCards = newProject.cards || [];
20
+
21
+ const oldById = new Map(oldCards.map(c => [c.id, c]));
22
+ const newById = new Map(newCards.map(c => [c.id, c]));
23
+
24
+ const added = [];
25
+ const removed = [];
26
+ const changed = [];
27
+ const unchanged = [];
28
+
29
+ for (const [id, newCard] of newById) {
30
+ if (!oldById.has(id)) {
31
+ added.push({ id, type: newCard.type, one_sentence: newCard.fields?.one_sentence || newCard.fields?.question || '' });
32
+ } else {
33
+ const oldCard = oldById.get(id);
34
+ if (JSON.stringify(oldCard.fields) !== JSON.stringify(newCard.fields)) {
35
+ const fieldChanges = diffFields(oldCard.fields || {}, newCard.fields || {});
36
+ changed.push({ id, type: newCard.type, changes: fieldChanges });
37
+ } else if (oldCard.status !== newCard.status) {
38
+ changed.push({ id, type: newCard.type, status_change: { from: oldCard.status, to: newCard.status } });
39
+ } else {
40
+ unchanged.push(id);
41
+ }
42
+ }
43
+ }
44
+
45
+ for (const [id, oldCard] of oldById) {
46
+ if (!newById.has(id)) {
47
+ removed.push({ id, type: oldCard.type, one_sentence: oldCard.fields?.one_sentence || oldCard.fields?.question || '' });
48
+ }
49
+ }
50
+
51
+ return {
52
+ added,
53
+ removed,
54
+ changed,
55
+ unchanged: unchanged.length,
56
+ summary: {
57
+ added_count: added.length,
58
+ removed_count: removed.length,
59
+ changed_count: changed.length,
60
+ unchanged_count: unchanged.length,
61
+ },
62
+ };
63
+ }
64
+
65
+ function diffFields(oldFields, newFields) {
66
+ const changes = {};
67
+ for (const key of new Set([...Object.keys(oldFields), ...Object.keys(newFields)])) {
68
+ const oldVal = JSON.stringify(oldFields[key] || null);
69
+ const newVal = JSON.stringify(newFields[key] || null);
70
+ if (oldVal !== newVal) {
71
+ changes[key] = { before: oldFields[key] || null, after: newFields[key] || null };
72
+ }
73
+ }
74
+ return changes;
75
+ }
76
+
77
+ function recommendVersionBump(diff) {
78
+ const { added, removed, changed } = diff;
79
+
80
+ // MAJOR: axioms removed or domain structure changed
81
+ const axiomsRemoved = removed.filter(c => c.type === 'axiom').length;
82
+ const misunderstandingsRemoved = removed.filter(c => c.type === 'misunderstanding').length;
83
+ if (axiomsRemoved > 0 || misunderstandingsRemoved > 0) return 'major';
84
+
85
+ // MINOR: new axioms, misunderstandings, or field changes on existing cards
86
+ const axiomsAdded = added.filter(c => c.type === 'axiom').length;
87
+ const misunderstandingsAdded = added.filter(c => c.type === 'misunderstanding').length;
88
+ if (axiomsAdded > 0 || misunderstandingsAdded > 0 || changed.length > 0) return 'minor';
89
+
90
+ // PATCH: wording-only changes (status changes, new self-checks, new boundaries)
91
+ if (added.length > 0 || changed.length > 0) return 'patch';
92
+
93
+ return 'none';
94
+ }
95
+
96
+ function generateChangelog(diff, oldVersion, newVersion, options = {}) {
97
+ const lines = [];
98
+ lines.push(`# ${options.domain || 'domain'} v${newVersion}`);
99
+ lines.push('');
100
+ lines.push(`**Previous:** v${oldVersion}`);
101
+ lines.push(`**Bump:** ${recommendVersionBump(diff).toUpperCase()}`);
102
+ lines.push('');
103
+
104
+ if (diff.summary.added_count > 0) {
105
+ lines.push('## Added');
106
+ lines.push('');
107
+ for (const card of diff.added) {
108
+ lines.push(`- **${card.type}** \`${card.id}\`: ${card.one_sentence}`);
109
+ }
110
+ lines.push('');
111
+ }
112
+
113
+ if (diff.summary.removed_count > 0) {
114
+ lines.push('## Removed');
115
+ lines.push('');
116
+ for (const card of diff.removed) {
117
+ lines.push(`- **${card.type}** \`${card.id}\`: ${card.one_sentence}`);
118
+ }
119
+ lines.push('');
120
+ }
121
+
122
+ if (diff.summary.changed_count > 0) {
123
+ lines.push('## Changed');
124
+ lines.push('');
125
+ for (const card of diff.changed) {
126
+ lines.push(`- **${card.type}** \`${card.id}\``);
127
+ if (card.status_change) {
128
+ lines.push(` - Status: ${card.status_change.from} → ${card.status_change.to}`);
129
+ }
130
+ if (card.changes && Object.keys(card.changes).length > 0) {
131
+ for (const [field, change] of Object.entries(card.changes)) {
132
+ const before = typeof change.before === 'string' ? change.before.slice(0, 80) : JSON.stringify(change.before).slice(0, 80);
133
+ const after = typeof change.after === 'string' ? change.after.slice(0, 80) : JSON.stringify(change.after).slice(0, 80);
134
+ lines.push(` - ${field}: "${before}" → "${after}"`);
135
+ }
136
+ }
137
+ }
138
+ lines.push('');
139
+ }
140
+
141
+ if (diff.summary.added_count === 0 && diff.summary.removed_count === 0 && diff.summary.changed_count === 0) {
142
+ lines.push('No judgment changes detected.');
143
+ lines.push('');
144
+ }
145
+
146
+ return lines.join('\n');
147
+ }
148
+
149
+ function bumpVersion(currentVersion, bumpType) {
150
+ const parts = currentVersion.split('.').map(Number);
151
+ switch (bumpType) {
152
+ case 'major': return `${parts[0] + 1}.0.0`;
153
+ case 'minor': return `${parts[0]}.${parts[1] + 1}.0`;
154
+ case 'patch': return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
155
+ default: return currentVersion;
156
+ }
157
+ }
158
+
159
+ function markBreakingChange(diff) {
160
+ const axiomsRemoved = diff.removed.filter(c => c.type === 'axiom').length;
161
+ const scopeChanges = diff.changed.filter(c =>
162
+ c.changes && ('applies_when' in c.changes || 'does_not_apply_when' in c.changes)
163
+ ).length;
164
+ return {
165
+ breaking: axiomsRemoved > 0,
166
+ reason: axiomsRemoved > 0
167
+ ? `${axiomsRemoved} axiom(s) removed — breaking change`
168
+ : scopeChanges > 0
169
+ ? `${scopeChanges} scope change(s) — may affect existing agent behavior`
170
+ : null,
171
+ recommended_bump: recommendVersionBump(diff),
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ diffProjects,
177
+ recommendVersionBump,
178
+ generateChangelog,
179
+ bumpVersion,
180
+ markBreakingChange,
181
+ };
@@ -0,0 +1,156 @@
1
+ const { test, describe } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const { createProject } = require('../src/project');
5
+ const { createCard, lockCard, transitionCard } = require('../src/cards');
6
+ const { parseCompareOutput, createJudgmentDelta, compareDeltas, formatDeltaMarkdown, scoreDelta } = require('../src/testlab/delta');
7
+ const { diffProjects, recommendVersionBump, generateChangelog, bumpVersion, markBreakingChange } = require('../src/versioning');
8
+
9
+ function makeLockedCard(type, fields, id) {
10
+ const card = createCard(type, fields, id);
11
+ transitionCard(card, 'revised', { by: 'tester' });
12
+ lockCard(card, { by: 'tester', statement: 'ok', checked: { applies_when: true, does_not_apply_when: true, failure_risk: true } });
13
+ return card;
14
+ }
15
+
16
+ // ─── Judgment Delta ──────────────────────────────────────────────────
17
+
18
+ describe('Judgment Delta', () => {
19
+ const DIFF_TEXT = `1. CLASSIFICATION: language_polishing → structural_diagnosis
20
+ 2. DIAGNOSIS: The root cause was identified as a missing argument rather than poor wording.
21
+ 3. ACTIONS: Suggested deletion more than rewriting — structural fix, not surface polish.
22
+ 4. BOUNDARY AWARENESS: SAME
23
+ 5. TERMINOLOGY: Used domain-specific terms like judgment_pressure and cognitive_hook.
24
+ VERDICT: trajectory_changed`;
25
+
26
+ test('parseCompareOutput extracts axes', () => {
27
+ const result = parseCompareOutput(DIFF_TEXT);
28
+ assert.equal(result.verdict, 'trajectory_changed');
29
+ assert.ok(result.axes.classification);
30
+ assert.ok(result.axes.diagnosis);
31
+ assert.ok(result.axes.actions);
32
+ assert.ok(!result.axes.boundary_awareness); // SAME → omitted
33
+ });
34
+
35
+ test('scoreDelta counts changed axes', () => {
36
+ const axes = { classification: 'changed', diagnosis: 'changed', terminology: 'changed' };
37
+ const result = scoreDelta(axes);
38
+ assert.equal(result.score, 8); // 5 + 3
39
+ assert.equal(result.changed.length, 3);
40
+ });
41
+
42
+ test('createJudgmentDelta builds full report', () => {
43
+ const delta = createJudgmentDelta('@aikdna/writing', 'Help me improve this post',
44
+ 'Generic response...', 'Domain-specific response...', DIFF_TEXT,
45
+ { model: 'claude-sonnet-4-5', triggeredAxioms: ['ax_001'], selfChecksPassed: 5 }
46
+ );
47
+ assert.equal(delta.meta.domain, '@aikdna/writing');
48
+ assert.equal(delta.verdict, 'trajectory_changed');
49
+ assert.ok(delta.score >= 8);
50
+ assert.ok(delta.summary.includes('dimensions'));
51
+ assert.ok(delta.scoring.D1_diagnostic_depth >= 5);
52
+ });
53
+
54
+ test('createJudgmentDelta handles no-change verdict', () => {
55
+ const noChangeText = '1. CLASSIFICATION: SAME\n2. DIAGNOSIS: SAME\nVERDICT: trajectory_unchanged';
56
+ const delta = createJudgmentDelta('test', 'input', 'a', 'b', noChangeText);
57
+ assert.equal(delta.verdict, 'trajectory_unchanged');
58
+ assert.equal(delta.score, 5);
59
+ assert.ok(delta.summary.includes('did not significantly alter'));
60
+ });
61
+
62
+ test('compareDeltas shows improvement', () => {
63
+ const d1 = createJudgmentDelta('test', 'input', 'a', 'b', DIFF_TEXT);
64
+ const betterText = DIFF_TEXT.replace('SAME', 'Improved boundary awareness with explicit scope limits');
65
+ const d2 = createJudgmentDelta('test', 'input', 'a', 'b', betterText);
66
+ const cmp = compareDeltas(d1, d2);
67
+ assert.ok(cmp.improved);
68
+ });
69
+
70
+ test('formatDeltaMarkdown produces readable report', () => {
71
+ const delta = createJudgmentDelta('@aikdna/writing', 'test input', 'a', 'b', DIFF_TEXT);
72
+ const md = formatDeltaMarkdown(delta);
73
+ assert.ok(md.includes('# KDNA Judgment Comparison Report'));
74
+ assert.ok(md.includes('## Judgment Diff'));
75
+ assert.ok(md.includes('## Scoring'));
76
+ assert.ok(md.includes('Verdict'));
77
+ });
78
+
79
+ test('parseCompareOutput handles legacy format', () => {
80
+ const legacyText = 'classification: changed\nterminology: domain_specific\nVERDICT: trajectory_changed';
81
+ const result = parseCompareOutput(legacyText);
82
+ assert.equal(result.verdict, 'trajectory_changed');
83
+ assert.ok(result.axes.classification);
84
+ });
85
+ });
86
+
87
+ // ─── Versioning ──────────────────────────────────────────────────────
88
+
89
+ describe('Versioning', () => {
90
+ test('diffProjects detects added cards', () => {
91
+ const oldProject = createProject('test');
92
+ const newProject = createProject('test');
93
+ newProject.cards = [makeLockedCard('axiom', { one_sentence: 'New axiom.' }, 'ax_001')];
94
+ const diff = diffProjects(oldProject, newProject);
95
+ assert.equal(diff.added.length, 1);
96
+ assert.equal(diff.removed.length, 0);
97
+ });
98
+
99
+ test('diffProjects detects removed cards', () => {
100
+ const oldProject = createProject('test');
101
+ oldProject.cards = [makeLockedCard('axiom', { one_sentence: 'Old.' }, 'ax_001')];
102
+ const newProject = createProject('test');
103
+ const diff = diffProjects(oldProject, newProject);
104
+ assert.equal(diff.removed.length, 1);
105
+ });
106
+
107
+ test('diffProjects detects changed fields', () => {
108
+ const oldProject = createProject('test');
109
+ oldProject.cards = [makeLockedCard('axiom', { one_sentence: 'Old text.', full_statement: 'Old full.' }, 'ax_001')];
110
+ const newProject = createProject('test');
111
+ newProject.cards = [makeLockedCard('axiom', { one_sentence: 'New text.', full_statement: 'New full.' }, 'ax_001')];
112
+ const diff = diffProjects(oldProject, newProject);
113
+ assert.equal(diff.changed.length, 1);
114
+ assert.ok(diff.changed[0].changes.one_sentence);
115
+ });
116
+
117
+ test('recommendVersionBump: MAJOR for removed axiom', () => {
118
+ const diff = { added: [], removed: [{ type: 'axiom' }], changed: [], summary: { added_count: 0, removed_count: 1, changed_count: 0 } };
119
+ assert.equal(recommendVersionBump(diff), 'major');
120
+ });
121
+
122
+ test('recommendVersionBump: MINOR for added axiom', () => {
123
+ const diff = { added: [{ type: 'axiom' }], removed: [], changed: [], summary: { added_count: 1, removed_count: 0, changed_count: 0 } };
124
+ assert.equal(recommendVersionBump(diff), 'minor');
125
+ });
126
+
127
+ test('recommendVersionBump: PATCH for field changes', () => {
128
+ const diff = { added: [], removed: [], changed: [{}], summary: { added_count: 0, removed_count: 0, changed_count: 1 } };
129
+ assert.equal(recommendVersionBump(diff), 'minor');
130
+ });
131
+
132
+ test('bumpVersion increments correctly', () => {
133
+ assert.equal(bumpVersion('0.1.0', 'patch'), '0.1.1');
134
+ assert.equal(bumpVersion('0.1.0', 'minor'), '0.2.0');
135
+ assert.equal(bumpVersion('0.1.0', 'major'), '1.0.0');
136
+ assert.equal(bumpVersion('2.3.5', 'none'), '2.3.5');
137
+ });
138
+
139
+ test('generateChangelog produces markdown', () => {
140
+ const oldProject = createProject('test');
141
+ const newProject = createProject('test');
142
+ newProject.cards = [makeLockedCard('axiom', { one_sentence: 'New axiom for agent judgment.' }, 'ax_001')];
143
+ const diff = diffProjects(oldProject, newProject);
144
+ const changelog = generateChangelog(diff, '0.1.0', '0.2.0', { domain: 'test' });
145
+ assert.ok(changelog.includes('# test v0.2.0'));
146
+ assert.ok(changelog.includes('MINOR'));
147
+ assert.ok(changelog.includes('ax_001'));
148
+ });
149
+
150
+ test('markBreakingChange detects axiom removal', () => {
151
+ const diff = { added: [], removed: [{ type: 'axiom' }], changed: [], summary: { added_count: 0, removed_count: 1, changed_count: 0 } };
152
+ const result = markBreakingChange(diff);
153
+ assert.equal(result.breaking, true);
154
+ assert.ok(result.reason.includes('breaking change'));
155
+ });
156
+ });