@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 +1 -1
- package/src/index.js +2 -0
- package/src/testlab/delta.js +160 -0
- package/src/versioning/index.js +181 -1
- package/tests/milestone3.test.js +156 -0
package/package.json
CHANGED
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 };
|
package/src/versioning/index.js
CHANGED
|
@@ -1 +1,181 @@
|
|
|
1
|
-
|
|
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
|
+
});
|