@aikdna/kdna-studio-core 1.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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Versioning — Judgment-aware semver with refined bump rules (v0.3.3).
3
+ *
4
+ * PATCH: typo, description, Feynman restatement, evidence_refs, examples
5
+ * MINOR: new axiom/misunderstanding/self_check, narrowed applies_when, new does_not_apply_when, new evals
6
+ * MAJOR: removed axiom, changed core meaning, expanded applies_when, removed does_not_apply_when, scope change, access change
7
+ */
8
+
9
+ function diffProjects(oldProject, newProject) {
10
+ const oldCards = oldProject.cards || [];
11
+ const newCards = newProject.cards || [];
12
+ const oldById = new Map(oldCards.map(c => [c.id, c]));
13
+ const newById = new Map(newCards.map(c => [c.id, c]));
14
+
15
+ const added = []; const removed = []; const changed = [];
16
+ for (const [id, nc] of newById) {
17
+ if (!oldById.has(id)) { added.push(cardSummary(nc)); }
18
+ else {
19
+ const oc = oldById.get(id);
20
+ const fieldChanges = diffFields(oc.fields || {}, nc.fields || {});
21
+ if (Object.keys(fieldChanges).length > 0) {
22
+ changed.push(cardSummary(nc, fieldChanges));
23
+ } else if (oc.status !== nc.status) {
24
+ changed.push({ ...cardSummary(nc), status_change: { from: oc.status, to: nc.status } });
25
+ }
26
+ }
27
+ }
28
+ for (const [id, oc] of oldById) {
29
+ if (!newById.has(id)) removed.push(cardSummary(oc));
30
+ }
31
+
32
+ return { added, removed, changed, unchanged: oldCards.length - removed.length - changed.length,
33
+ summary: { added_count: added.length, removed_count: removed.length, changed_count: changed.length } };
34
+ }
35
+
36
+ function cardSummary(card, changes) {
37
+ return { id: card.id, type: card.type, one_sentence: card.fields?.one_sentence || card.fields?.question || '',
38
+ changes: changes || null };
39
+ }
40
+
41
+ function stableStringify(obj) {
42
+ if (typeof obj !== 'object' || obj === null) return JSON.stringify(obj);
43
+ if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
44
+ const keys = Object.keys(obj).sort();
45
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
46
+ }
47
+
48
+ function diffFields(oldFields, newFields) {
49
+ const changes = {};
50
+ for (const key of new Set([...Object.keys(oldFields), ...Object.keys(newFields)])) {
51
+ const ov = stableStringify(oldFields[key] || null), nv = stableStringify(newFields[key] || null);
52
+ if (ov !== nv) changes[key] = { before: oldFields[key] || null, after: newFields[key] || null };
53
+ }
54
+ return changes;
55
+ }
56
+
57
+ function recommendVersionBump(diff) {
58
+ const { added, removed, changed } = diff;
59
+ const removedAxioms = removed.filter(c => c.type === 'axiom');
60
+ const removedMisunderstandings = removed.filter(c => c.type === 'misunderstanding');
61
+
62
+ // MAJOR checks
63
+ if (removedAxioms.length > 0 || removedMisunderstandings.length > 0) return 'major';
64
+ for (const c of changed) {
65
+ if (!c.changes) continue;
66
+ // Core meaning change on axiom → major
67
+ if (c.type === 'axiom' && ('one_sentence' in c.changes || 'full_statement' in c.changes)) return 'major';
68
+ // Expanded scope → major
69
+ if ('applies_when' in c.changes) {
70
+ const bef = c.changes.applies_when.before || [], aft = c.changes.applies_when.after || [];
71
+ if (aft.length > bef.length) return 'major';
72
+ }
73
+ // Removed boundary → major
74
+ if ('does_not_apply_when' in c.changes) {
75
+ const bef = c.changes.does_not_apply_when.before || [], aft = c.changes.does_not_apply_when.after || [];
76
+ if (aft.length < bef.length) return 'major';
77
+ }
78
+ }
79
+
80
+ // MINOR checks
81
+ const addedAxioms = added.filter(c => c.type === 'axiom');
82
+ const addedMisunderstandings = added.filter(c => c.type === 'misunderstanding');
83
+ const addedSelfChecks = added.filter(c => c.type === 'self_check');
84
+ if (addedAxioms.length > 0 || addedMisunderstandings.length > 0 || addedSelfChecks.length > 0) return 'minor';
85
+ for (const c of changed) {
86
+ if (!c.changes) continue;
87
+ // Narrowed scope → minor
88
+ if ('does_not_apply_when' in c.changes) {
89
+ const bef = c.changes.does_not_apply_when.before || [], aft = c.changes.does_not_apply_when.after || [];
90
+ if (aft.length > bef.length) return 'minor';
91
+ }
92
+ // Changed why/key_distinction → minor
93
+ if (c.type === 'axiom' && 'why' in c.changes) return 'minor';
94
+ if (c.type === 'misunderstanding' && 'key_distinction' in c.changes) return 'minor';
95
+ }
96
+
97
+ // PATCH: wording-only changes
98
+ if (added.length > 0 || changed.length > 0) return 'patch';
99
+ return 'none';
100
+ }
101
+
102
+ function generateChangelog(diff, oldVersion, newVersion, options = {}) {
103
+ const lines = [];
104
+ const bump = recommendVersionBump(diff);
105
+ lines.push(`# ${options.domain || 'domain'} v${newVersion}`);
106
+ lines.push('');
107
+ lines.push(`**Previous:** v${oldVersion} **Bump:** ${bump.toUpperCase()}`);
108
+ lines.push('');
109
+
110
+ for (const [label, items] of [['Added', diff.added], ['Removed', diff.removed], ['Changed', diff.changed]]) {
111
+ if (items.length === 0) continue;
112
+ lines.push(`## ${label}`); lines.push('');
113
+ for (const c of items) {
114
+ lines.push(`- **${c.type}** \`${c.id}\`: ${c.one_sentence}`);
115
+ if (c.status_change) lines.push(` - Status: ${c.status_change.from} → ${c.status_change.to}`);
116
+ if (c.changes) for (const [f, v] of Object.entries(c.changes)) {
117
+ lines.push(` - ${f}: "${String(v.before || '').slice(0, 60)}" → "${String(v.after || '').slice(0, 60)}"`);
118
+ }
119
+ }
120
+ lines.push('');
121
+ }
122
+
123
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {
124
+ lines.push('No judgment changes detected.\n');
125
+ }
126
+
127
+ return lines.join('\n');
128
+ }
129
+
130
+ function bumpVersion(currentVersion, bumpType) {
131
+ const [maj, min, pat] = currentVersion.split('.').map(Number);
132
+ if (bumpType === 'major') return `${maj + 1}.0.0`;
133
+ if (bumpType === 'minor') return `${maj}.${min + 1}.0`;
134
+ if (bumpType === 'patch') return `${maj}.${min}.${pat + 1}`;
135
+ return currentVersion;
136
+ }
137
+
138
+ function markBreakingChange(diff) {
139
+ const recommended = recommendVersionBump(diff);
140
+ const removedAxioms = diff.removed.filter(c => c.type === 'axiom');
141
+ const scopeWidening = diff.changed.filter(c => c.changes && 'applies_when' in c.changes &&
142
+ (c.changes.applies_when.after || []).length > (c.changes.applies_when.before || []).length);
143
+ const coreMeaningChanges = diff.changed.filter(c => c.changes &&
144
+ ('one_sentence' in c.changes || 'full_statement' in c.changes));
145
+
146
+ return {
147
+ breaking: recommended === 'major',
148
+ reason: removedAxioms.length > 0 ? `${removedAxioms.length} axiom(s) removed — breaking change` :
149
+ coreMeaningChanges.length > 0 ? `${coreMeaningChanges.length} core meaning change(s) — breaking change` :
150
+ scopeWidening.length > 0 ? `${scopeWidening.length} scope widening(s) — may affect existing behavior` : null,
151
+ recommended_bump: recommended,
152
+ };
153
+ }
154
+
155
+ module.exports = { diffProjects, recommendVersionBump, generateChangelog, bumpVersion, markBreakingChange };