@aikdna/studio-core 0.3.0 → 0.4.1
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/compile/index.js +123 -111
- package/src/packaging/index.js +26 -24
- package/src/quality/index.js +47 -111
- package/src/testlab/delta.js +55 -22
- package/src/versioning/index.js +86 -124
- package/tests/e2e.test.js +276 -0
- package/tests/milestone2.test.js +13 -11
- package/tests/milestone3.test.js +3 -3
package/src/quality/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enhanced Quality Gates — 4-grade readiness
|
|
2
|
+
* Enhanced Quality Gates — 4-grade readiness with integrated card validation.
|
|
3
3
|
*
|
|
4
4
|
* Grades:
|
|
5
5
|
* draft_grade — Core+Patterns exist, ≥3 human-reviewed cards
|
|
6
|
-
* human_controlled — All core axioms locked,
|
|
7
|
-
* tested_grade — ≥5
|
|
8
|
-
* publishable_grade — ≥10 evals,
|
|
6
|
+
* human_controlled — All core axioms locked with boundaries, ≥50% have Feynman
|
|
7
|
+
* tested_grade — ≥5 rated evals, ≥3 comparison tests
|
|
8
|
+
* publishable_grade — ≥10 evals, all axioms have Feynman, README 4 questions, no blocking
|
|
9
|
+
*
|
|
10
|
+
* v0.3.2: integrates validateAllCards, Feynman enforcement at publishable grade.
|
|
9
11
|
*/
|
|
12
|
+
|
|
10
13
|
const contradiction = require('./contradiction');
|
|
14
|
+
const { validateAllCards } = require('./validate-cards');
|
|
11
15
|
|
|
12
16
|
function computeReadiness(project) {
|
|
13
17
|
const cards = project.cards || [];
|
|
@@ -15,131 +19,68 @@ function computeReadiness(project) {
|
|
|
15
19
|
const locked = cards.filter(c => c.locked);
|
|
16
20
|
const lockedAxioms = locked.filter(c => c.type === 'axiom');
|
|
17
21
|
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
22
|
+
const lockedMisunderstandings = locked.filter(c => c.type === 'misunderstanding');
|
|
18
23
|
const ratedTests = tests.filter(t => t.result);
|
|
19
24
|
|
|
20
25
|
const blocking = [];
|
|
21
26
|
const warnings = [];
|
|
22
27
|
|
|
23
|
-
// ──
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return buildResult('draft_grade', blocking, warnings, project);
|
|
28
|
+
// ── Card validation integration (v0.3.2) ─────────────────────────
|
|
29
|
+
const cardResults = validateAllCards(project);
|
|
30
|
+
for (const { card_id, issues } of cardResults) {
|
|
31
|
+
for (const issue of issues) {
|
|
32
|
+
if (issue.severity === 'blocking') blocking.push(`${card_id}: ${issue.message}`);
|
|
33
|
+
else warnings.push(`${card_id}: ${issue.message}`);
|
|
34
|
+
}
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
// ── Minimum Structure ──────────────────────────────────────────
|
|
38
|
+
if (cards.length === 0) { blocking.push('Project has no cards'); return buildResult('draft_grade', blocking, warnings, project); }
|
|
39
|
+
if (locked.length === 0) { blocking.push('No locked cards — nothing to compile'); return buildResult('draft_grade', blocking, warnings, project); }
|
|
40
|
+
|
|
33
41
|
// ── Axiom Checks ──────────────────────────────────────────────
|
|
34
42
|
for (const ax of lockedAxioms) {
|
|
35
|
-
if (!ax.fields?.one_sentence || ax.fields.one_sentence.length < 10) {
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
if (!ax.fields?.
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
if (!ax.fields?.why || ax.fields.why.length < 10) {
|
|
42
|
-
warnings.push(`${ax.id}: missing "why" — explains what the agent gets wrong without this`);
|
|
43
|
-
}
|
|
44
|
-
if (!ax.fields?.applies_when || ax.fields.applies_when.length === 0) {
|
|
45
|
-
blocking.push(`${ax.id}: missing applies_when`);
|
|
46
|
-
}
|
|
47
|
-
if (!ax.fields?.does_not_apply_when || ax.fields.does_not_apply_when.length === 0) {
|
|
48
|
-
blocking.push(`${ax.id}: missing does_not_apply_when`);
|
|
49
|
-
}
|
|
50
|
-
if (!ax.fields?.failure_risk) {
|
|
51
|
-
blocking.push(`${ax.id}: missing failure_risk`);
|
|
52
|
-
}
|
|
53
|
-
if (!ax.human_lock) {
|
|
54
|
-
blocking.push(`${ax.id}: not locked — must be locked before compile`);
|
|
55
|
-
}
|
|
56
|
-
if (!ax.feynman_restatement) {
|
|
57
|
-
warnings.push(`${ax.id}: missing Feynman restatement`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Self-check Checks ──────────────────────────────────────────
|
|
62
|
-
for (const sc of lockedSelfChecks) {
|
|
63
|
-
const q = sc.fields?.question || '';
|
|
64
|
-
if (!q.endsWith('?')) {
|
|
65
|
-
blocking.push(`${sc.id}: self_check must be a question ending with ?`);
|
|
66
|
-
}
|
|
67
|
-
if (q.length < 15) {
|
|
68
|
-
warnings.push(`${sc.id}: self_check question too short — may be too vague`);
|
|
69
|
-
}
|
|
70
|
-
if (/\b(is this good|is this correct|is this helpful|is this clear|good enough)\b/i.test(q)) {
|
|
71
|
-
warnings.push(`${sc.id}: self_check is generic — should be domain-specific`);
|
|
72
|
-
}
|
|
43
|
+
if (!ax.fields?.one_sentence || ax.fields.one_sentence.length < 10) blocking.push(`${ax.id}: one_sentence too short`);
|
|
44
|
+
if (!ax.fields?.applies_when?.length) blocking.push(`${ax.id}: missing applies_when`);
|
|
45
|
+
if (!ax.fields?.does_not_apply_when?.length) blocking.push(`${ax.id}: missing does_not_apply_when`);
|
|
46
|
+
if (!ax.fields?.failure_risk) blocking.push(`${ax.id}: missing failure_risk`);
|
|
47
|
+
if (!ax.human_lock) blocking.push(`${ax.id}: not locked`);
|
|
48
|
+
if (!ax.feynman_restatement) warnings.push(`${ax.id}: missing Feynman restatement`);
|
|
73
49
|
}
|
|
74
50
|
|
|
75
51
|
// ── Misunderstanding Checks ────────────────────────────────────
|
|
76
|
-
const lockedMisunderstandings = locked.filter(c => c.type === 'misunderstanding');
|
|
77
52
|
for (const ms of lockedMisunderstandings) {
|
|
78
|
-
if (!ms.fields?.key_distinction || ms.fields.key_distinction.length < 20) {
|
|
79
|
-
blocking.push(`${ms.id}: key_distinction missing or too short`);
|
|
80
|
-
}
|
|
81
|
-
if (!ms.fields?.wrong || ms.fields.wrong.length < 10) {
|
|
82
|
-
warnings.push(`${ms.id}: wrong belief very short — may be a straw man`);
|
|
83
|
-
}
|
|
84
|
-
if (!ms.fields?.correct || ms.fields.correct.length < 10) {
|
|
85
|
-
warnings.push(`${ms.id}: correct belief very short`);
|
|
86
|
-
}
|
|
53
|
+
if (!ms.fields?.key_distinction || ms.fields.key_distinction.length < 20) blocking.push(`${ms.id}: key_distinction too short`);
|
|
87
54
|
}
|
|
88
55
|
|
|
89
|
-
// ──
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
warnings.push(`${bd.id}: no acceptable_exceptions — every boundary has justified exceptions`);
|
|
94
|
-
}
|
|
56
|
+
// ── Self-check Checks ──────────────────────────────────────────
|
|
57
|
+
for (const sc of lockedSelfChecks) {
|
|
58
|
+
const q = sc.fields?.question || '';
|
|
59
|
+
if (!q.endsWith('?')) blocking.push(`${sc.id}: self_check must end with ?`);
|
|
95
60
|
}
|
|
96
61
|
|
|
97
62
|
// ── Contradiction Check ────────────────────────────────────────
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
if (c.severity === 'blocking') blocking.push(c.message);
|
|
101
|
-
else warnings.push(c.message);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── Test Count Checks ──────────────────────────────────────────
|
|
105
|
-
if (ratedTests.length === 0 && locked.length >= 3) {
|
|
106
|
-
warnings.push('No rated tests — domain may not actually change agent behavior');
|
|
107
|
-
}
|
|
108
|
-
if (ratedTests.length < 3 && ratedTests.length > 0) {
|
|
109
|
-
warnings.push(`Only ${ratedTests.length} rated tests — recommend at least 3 for confidence`);
|
|
63
|
+
for (const c of contradiction.detectContradictions(cards)) {
|
|
64
|
+
(c.severity === 'blocking' ? blocking : warnings).push(c.message);
|
|
110
65
|
}
|
|
111
66
|
|
|
112
67
|
// ── Determine Grade ────────────────────────────────────────────
|
|
113
68
|
const axiomsComplete = lockedAxioms.length >= 1 &&
|
|
114
|
-
lockedAxioms.every(ax =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
ax.fields?.failure_risk &&
|
|
118
|
-
ax.human_lock
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const boundariesComplete = lockedBoundaries.length === 0 ||
|
|
122
|
-
lockedBoundaries.every(b => b.fields?.scope && b.fields?.out_of_scope);
|
|
69
|
+
lockedAxioms.every(ax => ax.fields?.applies_when?.length && ax.fields?.does_not_apply_when?.length && ax.fields?.failure_risk && ax.human_lock);
|
|
70
|
+
const feynmanRatio = lockedAxioms.length > 0 ? lockedAxioms.filter(ax => ax.feynman_restatement).length / lockedAxioms.length : 0;
|
|
71
|
+
const allFeynman = lockedAxioms.every(ax => ax.feynman_restatement) && lockedMisunderstandings.every(ms => !ms.locked || ms.feynman_restatement);
|
|
123
72
|
|
|
124
73
|
let grade = 'draft_grade';
|
|
125
|
-
if (locked.length >= 3 && axiomsComplete)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (grade === 'human_controlled' && ratedTests.length >= 5 && lockedSelfChecks.length >= 3 && boundariesComplete) {
|
|
129
|
-
grade = 'tested_grade';
|
|
130
|
-
}
|
|
131
|
-
if (grade === 'tested_grade' &&
|
|
132
|
-
ratedTests.length >= 10 &&
|
|
133
|
-
lockedAxioms.length >= 3 &&
|
|
134
|
-
lockedSelfChecks.length >= 5 &&
|
|
135
|
-
blocking.length === 0) {
|
|
74
|
+
if (locked.length >= 3 && axiomsComplete && feynmanRatio >= 0.5) grade = 'human_controlled';
|
|
75
|
+
if (grade === 'human_controlled' && ratedTests.length >= 5 && lockedSelfChecks.length >= 3) grade = 'tested_grade';
|
|
76
|
+
if (grade === 'tested_grade' && ratedTests.length >= 10 && lockedAxioms.length >= 3 && lockedSelfChecks.length >= 5 && blocking.length === 0 && allFeynman) {
|
|
136
77
|
grade = 'publishable_grade';
|
|
137
78
|
}
|
|
138
79
|
|
|
139
|
-
return buildResult(grade, blocking, warnings, project);
|
|
80
|
+
return buildResult(grade, blocking, warnings, project, { feynmanRatio, allFeynman });
|
|
140
81
|
}
|
|
141
82
|
|
|
142
|
-
function buildResult(grade, blocking, warnings, project) {
|
|
83
|
+
function buildResult(grade, blocking, warnings, project, detail = {}) {
|
|
143
84
|
const lockedCount = (project.cards || []).filter(c => c.locked).length;
|
|
144
85
|
const ratedTests = (project.tests || []).filter(t => t.result).length;
|
|
145
86
|
|
|
@@ -156,20 +97,15 @@ function buildResult(grade, blocking, warnings, project) {
|
|
|
156
97
|
locked_self_checks: (project.cards || []).filter(c => c.type === 'self_check' && c.locked).length,
|
|
157
98
|
total_tests: (project.tests || []).length,
|
|
158
99
|
rated_tests: ratedTests,
|
|
100
|
+
feynman_ratio: detail.feynmanRatio !== undefined ? Math.round(detail.feynmanRatio * 100) + '%' : 'N/A',
|
|
159
101
|
},
|
|
160
|
-
next_step: grade === 'draft_grade'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
: grade === 'tested_grade'
|
|
165
|
-
? 'Add 10+ evals, 3+ axioms, 5+ self-checks, and pass kdna verify --judgment to reach publishable.'
|
|
166
|
-
: 'Ready to publish. Run kdna pack and kdna publish.',
|
|
102
|
+
next_step: grade === 'draft_grade' ? 'Lock at least 3 axioms with boundaries and 50% Feynman.' :
|
|
103
|
+
grade === 'human_controlled' ? 'Add 5+ rated evals and 3+ self-checks.' :
|
|
104
|
+
grade === 'tested_grade' ? 'Add 10+ evals, complete Feynman on all axioms/misunderstandings, resolve all blocking issues.' :
|
|
105
|
+
'Ready to publish. Run kdna pack and kdna publish.',
|
|
167
106
|
};
|
|
168
107
|
}
|
|
169
108
|
|
|
170
|
-
function getBlockingIssues(project) {
|
|
171
|
-
const result = computeReadiness(project);
|
|
172
|
-
return result.blocking;
|
|
173
|
-
}
|
|
109
|
+
function getBlockingIssues(project) { return computeReadiness(project).blocking; }
|
|
174
110
|
|
|
175
111
|
module.exports = { computeReadiness, getBlockingIssues };
|
package/src/testlab/delta.js
CHANGED
|
@@ -128,33 +128,66 @@ function compareDeltas(delta1, delta2) {
|
|
|
128
128
|
|
|
129
129
|
function formatDeltaMarkdown(delta) {
|
|
130
130
|
const lines = [];
|
|
131
|
-
lines.push('# KDNA Judgment Comparison Report');
|
|
132
|
-
lines.push('');
|
|
131
|
+
lines.push('# KDNA Judgment Comparison Report'); lines.push('');
|
|
133
132
|
lines.push(`**Domain:** ${delta.meta.domain}`);
|
|
134
133
|
lines.push(`**Model:** ${delta.meta.model}`);
|
|
135
|
-
lines.push(`**Date:** ${delta.meta.timestamp}`);
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push('
|
|
138
|
-
lines.push(
|
|
139
|
-
lines.push('|
|
|
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
|
-
}
|
|
134
|
+
lines.push(`**Date:** ${delta.meta.timestamp}`); lines.push('');
|
|
135
|
+
lines.push('## Judgment Diff'); lines.push('');
|
|
136
|
+
lines.push('| Dimension | Change |'); lines.push('|-----------|--------|');
|
|
137
|
+
for (const d of delta.changed_dimensions) lines.push(`| ${d.axis} | **Changed**: ${d.value} |`);
|
|
138
|
+
if (!delta.changed_dimensions.length) lines.push('| (none) | No significant change |');
|
|
153
139
|
lines.push('');
|
|
154
|
-
lines.push(
|
|
140
|
+
lines.push('## Scoring'); lines.push('');
|
|
141
|
+
for (const [dim, value] of Object.entries(delta.scoring)) lines.push(`- **${dim}:** ${value}`);
|
|
155
142
|
lines.push('');
|
|
143
|
+
lines.push(`**Verdict:** ${delta.verdict.replace(/_/g, ' ')}`); lines.push('');
|
|
156
144
|
lines.push(delta.summary);
|
|
157
145
|
return lines.join('\n');
|
|
158
146
|
}
|
|
159
147
|
|
|
160
|
-
|
|
148
|
+
// ─── JSON report parsing (v0.3.3) ─────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function parseCompareReportJson(report) {
|
|
151
|
+
if (!report || !report.diff) return { axes: {}, verdict: 'trajectory_unchanged' };
|
|
152
|
+
|
|
153
|
+
const axes = {};
|
|
154
|
+
// Extract axes from structured report format
|
|
155
|
+
if (report.diff.axes) {
|
|
156
|
+
for (const [axis, value] of Object.entries(report.diff.axes)) {
|
|
157
|
+
if (value && String(value).toUpperCase() !== 'SAME') axes[axis] = String(value);
|
|
158
|
+
}
|
|
159
|
+
return { axes, verdict: report.diff.verdict || 'trajectory_unchanged' };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Legacy: raw baseline/kdna comparison
|
|
163
|
+
if (report.without_kdna && report.with_kdna) {
|
|
164
|
+
if (report.without_kdna.classification !== report.with_kdna.classification)
|
|
165
|
+
axes.classification = 'changed';
|
|
166
|
+
return { axes, verdict: Object.keys(axes).length > 0 ? 'trajectory_changed' : 'trajectory_unchanged' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { axes: {}, verdict: 'trajectory_unchanged' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createJudgmentDeltaFromReport(domain, input, report, options = {}) {
|
|
173
|
+
const { axes, verdict } = parseCompareReportJson(report);
|
|
174
|
+
const domainScore = scoreDelta(axes);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
meta: { domain, input: (input || '').slice(0, 200), model: report.meta?.model || options.model || 'unknown',
|
|
178
|
+
timestamp: new Date().toISOString() },
|
|
179
|
+
classification: { without_kdna: axes.classification || 'generic',
|
|
180
|
+
with_kdna: axes.classification ? 'domain_specific' : 'unchanged', changed: !!axes.classification },
|
|
181
|
+
axes, verdict,
|
|
182
|
+
score: domainScore.score,
|
|
183
|
+
changed_dimensions: domainScore.changed,
|
|
184
|
+
triggered_axioms: options.triggeredAxioms || [],
|
|
185
|
+
avoided_misunderstandings: options.avoidedMisunderstandings || [],
|
|
186
|
+
self_checks_passed: options.selfChecksPassed || null,
|
|
187
|
+
scoring: buildScoring(axes, domainScore, options.selfChecksPassed),
|
|
188
|
+
summary: buildSummary(domain, domainScore, verdict),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { parseCompareOutput, parseCompareReportJson, scoreDelta,
|
|
193
|
+
createJudgmentDelta, createJudgmentDeltaFromReport, compareDeltas, formatDeltaMarkdown };
|
package/src/versioning/index.js
CHANGED
|
@@ -1,181 +1,143 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Versioning — Judgment-aware
|
|
2
|
+
* Versioning — Judgment-aware semver with refined bump rules (v0.3.3).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
function diffProjects(oldProject, newProject) {
|
|
18
10
|
const oldCards = oldProject.cards || [];
|
|
19
11
|
const newCards = newProject.cards || [];
|
|
20
|
-
|
|
21
12
|
const oldById = new Map(oldCards.map(c => [c.id, c]));
|
|
22
13
|
const newById = new Map(newCards.map(c => [c.id, c]));
|
|
23
14
|
|
|
24
|
-
const added = [];
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
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 } });
|
|
41
25
|
}
|
|
42
26
|
}
|
|
43
27
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!newById.has(id)) {
|
|
47
|
-
removed.push({ id, type: oldCard.type, one_sentence: oldCard.fields?.one_sentence || oldCard.fields?.question || '' });
|
|
48
|
-
}
|
|
28
|
+
for (const [id, oc] of oldById) {
|
|
29
|
+
if (!newById.has(id)) removed.push(cardSummary(oc));
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
return {
|
|
52
|
-
added,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
removed_count: removed.length,
|
|
59
|
-
changed_count: changed.length,
|
|
60
|
-
unchanged_count: unchanged.length,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
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 };
|
|
63
39
|
}
|
|
64
40
|
|
|
65
41
|
function diffFields(oldFields, newFields) {
|
|
66
42
|
const changes = {};
|
|
67
43
|
for (const key of new Set([...Object.keys(oldFields), ...Object.keys(newFields)])) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (oldVal !== newVal) {
|
|
71
|
-
changes[key] = { before: oldFields[key] || null, after: newFields[key] || null };
|
|
72
|
-
}
|
|
44
|
+
const ov = JSON.stringify(oldFields[key] || null), nv = JSON.stringify(newFields[key] || null);
|
|
45
|
+
if (ov !== nv) changes[key] = { before: oldFields[key] || null, after: newFields[key] || null };
|
|
73
46
|
}
|
|
74
47
|
return changes;
|
|
75
48
|
}
|
|
76
49
|
|
|
77
50
|
function recommendVersionBump(diff) {
|
|
78
51
|
const { added, removed, changed } = diff;
|
|
52
|
+
const removedAxioms = removed.filter(c => c.type === 'axiom');
|
|
53
|
+
const removedMisunderstandings = removed.filter(c => c.type === 'misunderstanding');
|
|
54
|
+
|
|
55
|
+
// MAJOR checks
|
|
56
|
+
if (removedAxioms.length > 0 || removedMisunderstandings.length > 0) return 'major';
|
|
57
|
+
for (const c of changed) {
|
|
58
|
+
if (!c.changes) continue;
|
|
59
|
+
// Core meaning change on axiom → major
|
|
60
|
+
if (c.type === 'axiom' && ('one_sentence' in c.changes || 'full_statement' in c.changes)) return 'major';
|
|
61
|
+
// Expanded scope → major
|
|
62
|
+
if ('applies_when' in c.changes) {
|
|
63
|
+
const bef = c.changes.applies_when.before || [], aft = c.changes.applies_when.after || [];
|
|
64
|
+
if (aft.length > bef.length) return 'major';
|
|
65
|
+
}
|
|
66
|
+
// Removed boundary → major
|
|
67
|
+
if ('does_not_apply_when' in c.changes) {
|
|
68
|
+
const bef = c.changes.does_not_apply_when.before || [], aft = c.changes.does_not_apply_when.after || [];
|
|
69
|
+
if (aft.length < bef.length) return 'major';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
79
72
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
// MINOR checks
|
|
74
|
+
const addedAxioms = added.filter(c => c.type === 'axiom');
|
|
75
|
+
const addedMisunderstandings = added.filter(c => c.type === 'misunderstanding');
|
|
76
|
+
const addedSelfChecks = added.filter(c => c.type === 'self_check');
|
|
77
|
+
if (addedAxioms.length > 0 || addedMisunderstandings.length > 0 || addedSelfChecks.length > 0) return 'minor';
|
|
78
|
+
for (const c of changed) {
|
|
79
|
+
if (!c.changes) continue;
|
|
80
|
+
// Narrowed scope → minor
|
|
81
|
+
if ('does_not_apply_when' in c.changes) {
|
|
82
|
+
const bef = c.changes.does_not_apply_when.before || [], aft = c.changes.does_not_apply_when.after || [];
|
|
83
|
+
if (aft.length > bef.length) return 'minor';
|
|
84
|
+
}
|
|
85
|
+
// Changed why/key_distinction → minor
|
|
86
|
+
if (c.type === 'axiom' && 'why' in c.changes) return 'minor';
|
|
87
|
+
if (c.type === 'misunderstanding' && 'key_distinction' in c.changes) return 'minor';
|
|
88
|
+
}
|
|
89
89
|
|
|
90
|
-
// PATCH: wording-only changes
|
|
90
|
+
// PATCH: wording-only changes
|
|
91
91
|
if (added.length > 0 || changed.length > 0) return 'patch';
|
|
92
|
-
|
|
93
92
|
return 'none';
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
function generateChangelog(diff, oldVersion, newVersion, options = {}) {
|
|
97
96
|
const lines = [];
|
|
97
|
+
const bump = recommendVersionBump(diff);
|
|
98
98
|
lines.push(`# ${options.domain || 'domain'} v${newVersion}`);
|
|
99
99
|
lines.push('');
|
|
100
|
-
lines.push(`**Previous:** v${oldVersion}`);
|
|
101
|
-
lines.push(`**Bump:** ${recommendVersionBump(diff).toUpperCase()}`);
|
|
100
|
+
lines.push(`**Previous:** v${oldVersion} **Bump:** ${bump.toUpperCase()}`);
|
|
102
101
|
lines.push('');
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
lines.push('');
|
|
107
|
-
for (const
|
|
108
|
-
lines.push(`- **${
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
103
|
+
for (const [label, items] of [['Added', diff.added], ['Removed', diff.removed], ['Changed', diff.changed]]) {
|
|
104
|
+
if (items.length === 0) continue;
|
|
105
|
+
lines.push(`## ${label}`); lines.push('');
|
|
106
|
+
for (const c of items) {
|
|
107
|
+
lines.push(`- **${c.type}** \`${c.id}\`: ${c.one_sentence}`);
|
|
108
|
+
if (c.status_change) lines.push(` - Status: ${c.status_change.from} → ${c.status_change.to}`);
|
|
109
|
+
if (c.changes) for (const [f, v] of Object.entries(c.changes)) {
|
|
110
|
+
lines.push(` - ${f}: "${String(v.before || '').slice(0, 60)}" → "${String(v.after || '').slice(0, 60)}"`);
|
|
136
111
|
}
|
|
137
112
|
}
|
|
138
113
|
lines.push('');
|
|
139
114
|
}
|
|
140
115
|
|
|
141
|
-
if (diff.
|
|
142
|
-
lines.push('No judgment changes detected
|
|
143
|
-
lines.push('');
|
|
116
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {
|
|
117
|
+
lines.push('No judgment changes detected.\n');
|
|
144
118
|
}
|
|
145
119
|
|
|
146
120
|
return lines.join('\n');
|
|
147
121
|
}
|
|
148
122
|
|
|
149
123
|
function bumpVersion(currentVersion, bumpType) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
default: return currentVersion;
|
|
156
|
-
}
|
|
124
|
+
const [maj, min, pat] = currentVersion.split('.').map(Number);
|
|
125
|
+
if (bumpType === 'major') return `${maj + 1}.0.0`;
|
|
126
|
+
if (bumpType === 'minor') return `${maj}.${min + 1}.0`;
|
|
127
|
+
if (bumpType === 'patch') return `${maj}.${min}.${pat + 1}`;
|
|
128
|
+
return currentVersion;
|
|
157
129
|
}
|
|
158
130
|
|
|
159
131
|
function markBreakingChange(diff) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
c.changes
|
|
163
|
-
).length;
|
|
132
|
+
const removedAxioms = diff.removed.filter(c => c.type === 'axiom');
|
|
133
|
+
const scopeWidening = diff.changed.filter(c => c.changes && 'applies_when' in c.changes &&
|
|
134
|
+
(c.changes.applies_when.after || []).length > (c.changes.applies_when.before || []).length);
|
|
164
135
|
return {
|
|
165
|
-
breaking:
|
|
166
|
-
reason:
|
|
167
|
-
? `${
|
|
168
|
-
: scopeChanges > 0
|
|
169
|
-
? `${scopeChanges} scope change(s) — may affect existing agent behavior`
|
|
170
|
-
: null,
|
|
136
|
+
breaking: removedAxioms.length > 0,
|
|
137
|
+
reason: removedAxioms.length > 0 ? `${removedAxioms.length} axiom(s) removed — breaking change` :
|
|
138
|
+
scopeWidening.length > 0 ? `${scopeWidening.length} scope widening(s) — may affect existing behavior` : null,
|
|
171
139
|
recommended_bump: recommendVersionBump(diff),
|
|
172
140
|
};
|
|
173
141
|
}
|
|
174
142
|
|
|
175
|
-
module.exports = {
|
|
176
|
-
diffProjects,
|
|
177
|
-
recommendVersionBump,
|
|
178
|
-
generateChangelog,
|
|
179
|
-
bumpVersion,
|
|
180
|
-
markBreakingChange,
|
|
181
|
-
};
|
|
143
|
+
module.exports = { diffProjects, recommendVersionBump, generateChangelog, bumpVersion, markBreakingChange };
|