@aikdna/studio-core 0.1.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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Card Validator — Anti-vagueness, anti-SOP, anti-slogan, anti-straw-man checks.
3
+ *
4
+ * Ensures every card meets minimum quality before it can be locked.
5
+ * These checks mirror the kdna-cli publish --check rules.
6
+ */
7
+
8
+ const ANTI_PATTERNS = {
9
+ axiom: {
10
+ slogans: ['is key', 'is important', 'matters', 'is critical', 'is essential', 'should be', 'must be'],
11
+ sops: ['first, you should', 'follow these steps', 'always remember to', 'the process is'],
12
+ },
13
+ misunderstanding: {
14
+ straw_men: ['some people say', 'many believe', 'it is commonly thought'],
15
+ },
16
+ self_check: {
17
+ generics: ['is this good', 'is this correct', 'is this helpful', 'is this clear', 'does this work', 'is it right'],
18
+ },
19
+ };
20
+
21
+ function validateCard(card) {
22
+ const issues = [];
23
+
24
+ switch (card.type) {
25
+ case 'axiom':
26
+ validateAxiom(card, issues);
27
+ break;
28
+ case 'misunderstanding':
29
+ validateMisunderstanding(card, issues);
30
+ break;
31
+ case 'self_check':
32
+ validateSelfCheck(card, issues);
33
+ break;
34
+ case 'ontology':
35
+ validateOntology(card, issues);
36
+ break;
37
+ case 'boundary':
38
+ validateBoundary(card, issues);
39
+ break;
40
+ }
41
+
42
+ return issues;
43
+ }
44
+
45
+ function validateAxiom(card, issues) {
46
+ const oneLiner = (card.fields?.one_sentence || '').toLowerCase();
47
+ const full = (card.fields?.full_statement || '').toLowerCase();
48
+
49
+ // Anti-slogan: reject axioms that are just motivational slogans
50
+ for (const slogan of ANTI_PATTERNS.axiom.slogans) {
51
+ if (oneLiner.includes(slogan) && oneLiner.length < 40) {
52
+ issues.push({
53
+ type: 'slogan',
54
+ severity: 'warning',
55
+ message: `${card.id}: one_sentence may be a slogan — "${oneLiner.slice(0, 60)}"`,
56
+ fix: 'Axioms must be specific, testable judgment principles. Replace vague slogans with concrete decision rules.',
57
+ });
58
+ break;
59
+ }
60
+ }
61
+
62
+ // Anti-SOP: axioms should not encode step-by-step procedures
63
+ for (const sop of ANTI_PATTERNS.axiom.sops) {
64
+ if (oneLiner.includes(sop) || full.includes(sop)) {
65
+ issues.push({
66
+ type: 'sop',
67
+ severity: 'warning',
68
+ message: `${card.id}: axiom reads like a procedure, not a judgment principle`,
69
+ fix: 'Axioms encode how to judge, not what steps to follow. Rephrase as a decision principle.',
70
+ });
71
+ break;
72
+ }
73
+ }
74
+
75
+ // Anti-vagueness: one_sentence must be specific enough
76
+ if (oneLiner.length < 15) {
77
+ issues.push({ type: 'too_short', severity: 'blocking', message: `${card.id}: one_sentence too short (${oneLiner.length} chars)`, fix: 'Make it a complete, specific judgment statement.' });
78
+ }
79
+
80
+ // Check for dictionary-definition style (axiom should not start with "X is")
81
+ if (/^\w+\s+is\s/.test(oneLiner) && oneLiner.length < 50) {
82
+ issues.push({ type: 'definition_like', severity: 'warning', message: `${card.id}: one_sentence reads like a definition, not a judgment — rephrase as a principle` });
83
+ }
84
+ }
85
+
86
+ function validateMisunderstanding(card, issues) {
87
+ const wrong = (card.fields?.wrong || '').toLowerCase();
88
+ const correct = (card.fields?.correct || '').toLowerCase();
89
+ const distinction = card.fields?.key_distinction || '';
90
+
91
+ // Anti-straw-man: the wrong belief should be something real people believe
92
+ if (wrong.length < 15) {
93
+ issues.push({ type: 'vague_wrong', severity: 'warning', message: `${card.id}: wrong belief too short — may describe a straw man no one believes` });
94
+ }
95
+ for (const straw of ANTI_PATTERNS.misunderstanding.straw_men) {
96
+ if (wrong.includes(straw)) {
97
+ issues.push({ type: 'straw_man', severity: 'warning', message: `${card.id}: wrong belief uses straw-man phrasing — describe what people actually get wrong` });
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (!distinction || distinction.length < 20) {
103
+ issues.push({ type: 'missing_distinction', severity: 'blocking', message: `${card.id}: key_distinction missing or too short (${distinction.length} chars)` });
104
+ }
105
+ }
106
+
107
+ function validateSelfCheck(card, issues) {
108
+ const question = card.fields?.question || '';
109
+
110
+ if (!question.endsWith('?')) {
111
+ issues.push({ type: 'not_question', severity: 'blocking', message: `${card.id}: must be a question ending with ?` });
112
+ }
113
+
114
+ if (question.length < 15) {
115
+ issues.push({ type: 'vague', severity: 'warning', message: `${card.id}: question too short — make it domain-specific` });
116
+ }
117
+
118
+ for (const gen of ANTI_PATTERNS.self_check.generics) {
119
+ if (question.toLowerCase().includes(gen)) {
120
+ issues.push({ type: 'generic', severity: 'warning', message: `${card.id}: question is generic — should reference domain-specific criteria` });
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ function validateOntology(card, issues) {
127
+ const essence = card.fields?.essence || '';
128
+ const boundary = card.fields?.boundary || '';
129
+ const trigger = card.fields?.trigger_signal || '';
130
+
131
+ if (essence.length < 15) {
132
+ issues.push({ type: 'vague_essence', severity: 'warning', message: `${card.id}: essence too short — explain operational meaning` });
133
+ }
134
+ if (boundary.length < 10) {
135
+ issues.push({ type: 'missing_boundary', severity: 'warning', message: `${card.id}: boundary missing — what is this concept NOT?` });
136
+ }
137
+ if (trigger.length < 10) {
138
+ issues.push({ type: 'missing_trigger', severity: 'warning', message: `${card.id}: trigger_signal missing — how does the agent detect this concept?` });
139
+ }
140
+ }
141
+
142
+ function validateBoundary(card, issues) {
143
+ const scope = card.fields?.scope || '';
144
+ const outOfScope = card.fields?.out_of_scope || '';
145
+
146
+ if (scope.length < 10) {
147
+ issues.push({ type: 'vague_scope', severity: 'warning', message: `${card.id}: scope too short` });
148
+ }
149
+ if (outOfScope.length < 10) {
150
+ issues.push({ type: 'vague_out_of_scope', severity: 'blocking', message: `${card.id}: out_of_scope missing or too short` });
151
+ }
152
+ }
153
+
154
+ function validateAllCards(project) {
155
+ const allIssues = [];
156
+ for (const card of (project.cards || [])) {
157
+ const cardIssues = validateCard(card);
158
+ allIssues.push({ card_id: card.id, issues: cardIssues });
159
+ }
160
+ return allIssues;
161
+ }
162
+
163
+ module.exports = { validateCard, validateAllCards, ANTI_PATTERNS };
@@ -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
+ };