@aikdna/studio-core 0.1.0 → 0.2.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/compile/index.js +190 -51
- package/src/index.js +2 -0
- package/src/quality/index.js +138 -28
- package/src/quality/validate-cards.js +163 -0
- package/tests/milestone2.test.js +285 -0
package/package.json
CHANGED
package/src/compile/index.js
CHANGED
|
@@ -3,32 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Only locked cards enter compilation output.
|
|
5
5
|
* Draft/Revised cards are silently excluded.
|
|
6
|
+
* Supports full 6-file output: Core / Patterns / Scenarios / Cases / Reasoning / Evolution.
|
|
6
7
|
*/
|
|
8
|
+
const provenance = require('../provenance');
|
|
7
9
|
|
|
8
10
|
function compileCore(cards) {
|
|
9
|
-
const axioms = cards.filter(c => c.type === 'axiom' && c.locked).map(c => ({
|
|
10
|
-
|
|
11
|
-
...c.fields,
|
|
12
|
-
}));
|
|
13
|
-
const ontology = cards.filter(c => c.type === 'ontology' && c.locked).map(c => ({
|
|
14
|
-
id: c.id,
|
|
15
|
-
...c.fields,
|
|
16
|
-
}));
|
|
11
|
+
const axioms = cards.filter(c => c.type === 'axiom' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
12
|
+
const ontology = cards.filter(c => c.type === 'ontology' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
17
13
|
const frameworks = [];
|
|
18
14
|
const stances = [];
|
|
19
15
|
const boundaries = cards.filter(c => c.type === 'boundary' && c.locked).map(c => ({
|
|
20
16
|
id: c.id,
|
|
21
|
-
scope: c.fields
|
|
22
|
-
out_of_scope: c.fields
|
|
23
|
-
acceptable_exceptions: c.fields
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
const risks = cards.filter(c => c.type === 'risk' && c.locked).map(c => ({
|
|
27
|
-
id: c.id,
|
|
28
|
-
failure_mode: c.fields.failure_mode,
|
|
29
|
-
likelihood: c.fields.likelihood,
|
|
30
|
-
mitigation: c.fields.mitigation,
|
|
17
|
+
scope: c.fields?.scope || '',
|
|
18
|
+
out_of_scope: c.fields?.out_of_scope || '',
|
|
19
|
+
acceptable_exceptions: c.fields?.acceptable_exceptions || [],
|
|
31
20
|
}));
|
|
21
|
+
const risks = cards.filter(c => c.type === 'risk' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
32
22
|
|
|
33
23
|
return { axioms, ontology, frameworks, stances, boundaries, risks };
|
|
34
24
|
}
|
|
@@ -36,52 +26,98 @@ function compileCore(cards) {
|
|
|
36
26
|
function compilePatterns(cards) {
|
|
37
27
|
const misunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked).map(c => ({
|
|
38
28
|
id: c.id,
|
|
39
|
-
wrong: c.fields
|
|
40
|
-
correct: c.fields
|
|
41
|
-
key_distinction: c.fields
|
|
42
|
-
failure_risk: c.fields
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const bannedTerms = [];
|
|
46
|
-
const aesthetics = cards.filter(c => c.type === 'aesthetic' && c.locked).map(c => ({
|
|
47
|
-
id: c.id,
|
|
48
|
-
preference: c.fields.preference,
|
|
49
|
-
rationale: c.fields.rationale,
|
|
29
|
+
wrong: c.fields?.wrong || '',
|
|
30
|
+
correct: c.fields?.correct || '',
|
|
31
|
+
key_distinction: c.fields?.key_distinction || '',
|
|
32
|
+
failure_risk: c.fields?.failure_risk || null,
|
|
33
|
+
applies_when: c.fields?.applies_when || [],
|
|
34
|
+
does_not_apply_when: c.fields?.does_not_apply_when || [],
|
|
50
35
|
}));
|
|
36
|
+
const selfCheckQuestions = cards.filter(c => c.type === 'self_check' && c.locked).map(c => c.fields?.question || '');
|
|
37
|
+
const aesthetics = cards.filter(c => c.type === 'aesthetic' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
51
38
|
|
|
52
39
|
const terminology = {
|
|
53
40
|
standard_terms: [],
|
|
54
|
-
banned_terms:
|
|
41
|
+
banned_terms: [],
|
|
55
42
|
};
|
|
56
43
|
|
|
57
|
-
return { terminology, misunderstandings, self_check:
|
|
44
|
+
return { terminology, misunderstandings, self_check: selfCheckQuestions, aesthetics };
|
|
58
45
|
}
|
|
59
46
|
|
|
60
47
|
function compileScenarios(cards) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}));
|
|
48
|
+
const locked = cards.filter(c => c.type === 'scenario' && c.locked);
|
|
49
|
+
if (locked.length === 0) return [];
|
|
50
|
+
return locked.map(c => ({ id: c.id, ...c.fields }));
|
|
65
51
|
}
|
|
66
52
|
|
|
67
53
|
function compileCases(cards) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
const locked = cards.filter(c => c.type === 'case' && c.locked);
|
|
55
|
+
if (locked.length === 0) return [];
|
|
56
|
+
return locked.map(c => ({ id: c.id, ...c.fields }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function compileReasoning(cards) {
|
|
60
|
+
// Reasoning chains from axiom implications
|
|
61
|
+
const lockedAxioms = cards.filter(c => c.type === 'axiom' && c.locked);
|
|
62
|
+
if (lockedAxioms.length === 0) return [];
|
|
63
|
+
return lockedAxioms.map(ax => ({
|
|
64
|
+
id: `chain_${ax.id}`,
|
|
65
|
+
from: ax.fields?.one_sentence || '',
|
|
66
|
+
logic: [ax.fields?.full_statement || ''],
|
|
67
|
+
so_what: ax.fields?.why || 'Agent judgment changes when this axiom is loaded.',
|
|
71
68
|
}));
|
|
72
69
|
}
|
|
73
70
|
|
|
71
|
+
function compileEvolution(cards) {
|
|
72
|
+
const lockedCards = cards.filter(c => c.locked);
|
|
73
|
+
if (lockedCards.length === 0) return { stages: [], capability_layers: [], measurements: [] };
|
|
74
|
+
|
|
75
|
+
// Build evolution from audit logs
|
|
76
|
+
const stages = [];
|
|
77
|
+
const seenAxioms = new Set();
|
|
78
|
+
for (const card of lockedCards) {
|
|
79
|
+
if (seenAxioms.has(card.id)) continue;
|
|
80
|
+
seenAxioms.add(card.id);
|
|
81
|
+
for (const entry of (card.audit_log || [])) {
|
|
82
|
+
if (entry.event === 'locked' || entry.event === 'published') {
|
|
83
|
+
stages.push({
|
|
84
|
+
card_id: card.id,
|
|
85
|
+
event: entry.event,
|
|
86
|
+
at: entry.at,
|
|
87
|
+
by: entry.by,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
stages: stages.sort((a, b) => a.at.localeCompare(b.at)),
|
|
95
|
+
capability_layers: [
|
|
96
|
+
{ layer: 1, name: 'Foundation', description: 'Core axioms and patterns established.' },
|
|
97
|
+
],
|
|
98
|
+
measurements: [
|
|
99
|
+
{ metric: 'locked_axioms', value: lockedCards.filter(c => c.type === 'axiom').length },
|
|
100
|
+
{ metric: 'locked_misunderstandings', value: lockedCards.filter(c => c.type === 'misunderstanding').length },
|
|
101
|
+
{ metric: 'self_checks', value: lockedCards.filter(c => c.type === 'self_check').length },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
74
106
|
function compileManifest(project) {
|
|
75
|
-
const lockedCount = project.cards.filter(c => c.locked).length;
|
|
107
|
+
const lockedCount = (project.cards || []).filter(c => c.locked).length;
|
|
108
|
+
const cards = project.cards || [];
|
|
109
|
+
const hasScenarios = cards.some(c => c.type === 'scenario' && c.locked);
|
|
110
|
+
const hasCases = cards.some(c => c.type === 'case' && c.locked);
|
|
111
|
+
const fileCount = 2 + (hasScenarios ? 1 : 0) + (hasCases ? 1 : 0) + 2; // Core+Patterns+Reasoning+Evolution (+ Scenarios + Cases)
|
|
76
112
|
return {
|
|
77
113
|
kdna_spec: '1.0-rc',
|
|
78
114
|
name: project.name,
|
|
79
|
-
version: project.release
|
|
115
|
+
version: (project.release && project.release.version) || '0.1.0',
|
|
80
116
|
status: 'experimental',
|
|
81
|
-
access: project.release
|
|
82
|
-
author: project.author,
|
|
83
|
-
description: project.
|
|
84
|
-
file_count: 2,
|
|
117
|
+
access: (project.release && project.release.access) || 'open',
|
|
118
|
+
author: project.author || { name: '', id: '' },
|
|
119
|
+
description: project.name,
|
|
120
|
+
file_count: 2,
|
|
85
121
|
created: project.created,
|
|
86
122
|
updated: project.updated,
|
|
87
123
|
};
|
|
@@ -93,26 +129,129 @@ function compileDomain(project) {
|
|
|
93
129
|
const patterns = compilePatterns(cards);
|
|
94
130
|
const scenarios = compileScenarios(cards);
|
|
95
131
|
const cases = compileCases(cards);
|
|
132
|
+
const reasoning = compileReasoning(cards);
|
|
133
|
+
const evolution = compileEvolution(cards);
|
|
96
134
|
const manifest = compileManifest(project);
|
|
97
135
|
|
|
98
136
|
const files = {};
|
|
99
137
|
files['KDNA_Core.json'] = JSON.stringify(core, null, 2);
|
|
100
138
|
files['KDNA_Patterns.json'] = JSON.stringify(patterns, null, 2);
|
|
101
|
-
if (scenarios.length > 0) files['KDNA_Scenarios.json'] = JSON.stringify(scenarios, null, 2);
|
|
102
|
-
if (cases.length > 0) files['KDNA_Cases.json'] = JSON.stringify(cases, null, 2);
|
|
139
|
+
if (scenarios.length > 0) files['KDNA_Scenarios.json'] = JSON.stringify({ scenes: scenarios }, null, 2);
|
|
140
|
+
if (cases.length > 0) files['KDNA_Cases.json'] = JSON.stringify({ cases }, null, 2);
|
|
141
|
+
if (reasoning.length > 0) files['KDNA_Reasoning.json'] = JSON.stringify({ chains: reasoning }, null, 2);
|
|
142
|
+
if (evolution.stages && evolution.stages.length > 0) files['KDNA_Evolution.json'] = JSON.stringify(evolution, null, 2);
|
|
103
143
|
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
104
144
|
|
|
105
|
-
const excludedCount =
|
|
145
|
+
const excludedCount = cards.filter(c => !c.locked && !['deprecated'].includes(c.status)).length;
|
|
106
146
|
|
|
107
147
|
return {
|
|
108
148
|
files,
|
|
109
149
|
stats: {
|
|
110
|
-
total_cards:
|
|
111
|
-
locked_cards:
|
|
150
|
+
total_cards: cards.length,
|
|
151
|
+
locked_cards: cards.filter(c => c.locked).length,
|
|
112
152
|
excluded_cards: excludedCount,
|
|
113
|
-
deprecated_cards:
|
|
153
|
+
deprecated_cards: cards.filter(c => c.status === 'deprecated').length,
|
|
154
|
+
files_output: Object.keys(files).length,
|
|
114
155
|
},
|
|
115
156
|
};
|
|
116
157
|
}
|
|
117
158
|
|
|
118
|
-
|
|
159
|
+
function generateReadme(project, options = {}) {
|
|
160
|
+
const cards = project.cards || [];
|
|
161
|
+
const locked = cards.filter(c => c.locked);
|
|
162
|
+
const lockedAxioms = locked.filter(c => c.type === 'axiom');
|
|
163
|
+
const lockedMisunderstandings = locked.filter(c => c.type === 'misunderstanding');
|
|
164
|
+
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
165
|
+
const lockedBoundaries = locked.filter(c => c.type === 'boundary');
|
|
166
|
+
const tests = project.tests || [];
|
|
167
|
+
|
|
168
|
+
const lines = [];
|
|
169
|
+
lines.push(`# ${project.name}`);
|
|
170
|
+
lines.push('');
|
|
171
|
+
if (options.description) {
|
|
172
|
+
lines.push(options.description);
|
|
173
|
+
lines.push('');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Four Questions
|
|
177
|
+
lines.push('## Where it comes from');
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push(options.origin || `Domain expertise encoded into ${locked.length} judgment cards through structured interview and human lock.`);
|
|
180
|
+
lines.push('');
|
|
181
|
+
|
|
182
|
+
lines.push('## Where it applies');
|
|
183
|
+
lines.push('');
|
|
184
|
+
const appliesWhen = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.applies_when || []))];
|
|
185
|
+
if (appliesWhen.length > 0) {
|
|
186
|
+
appliesWhen.forEach(w => lines.push(`- ${w}`));
|
|
187
|
+
} else {
|
|
188
|
+
lines.push('- As declared in each axiom\'s applies_when field.');
|
|
189
|
+
}
|
|
190
|
+
lines.push('');
|
|
191
|
+
|
|
192
|
+
lines.push('## How it is verified');
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push(`- ${tests.length} eval cases (${tests.filter(t => t.result).length} rated)`);
|
|
195
|
+
lines.push(`- ${lockedAxioms.length} locked axioms with applies_when / does_not_apply_when / failure_risk`);
|
|
196
|
+
lines.push(`- ${lockedSelfChecks.length} self-check questions`);
|
|
197
|
+
lines.push(`- ${lockedMisunderstandings.length} misunderstanding patterns`);
|
|
198
|
+
lines.push('');
|
|
199
|
+
|
|
200
|
+
lines.push('## When it does NOT apply');
|
|
201
|
+
lines.push('');
|
|
202
|
+
const notApply = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.does_not_apply_when || []))];
|
|
203
|
+
if (notApply.length > 0) {
|
|
204
|
+
notApply.forEach(w => lines.push(`- ${w}`));
|
|
205
|
+
}
|
|
206
|
+
const outOfScope = lockedBoundaries.flatMap(b => [b.fields?.out_of_scope || '']).filter(Boolean);
|
|
207
|
+
for (const oos of outOfScope) {
|
|
208
|
+
if (!notApply.includes(oos)) lines.push(`- ${oos}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push('');
|
|
211
|
+
|
|
212
|
+
// Top Axioms
|
|
213
|
+
if (lockedAxioms.length > 0) {
|
|
214
|
+
lines.push('## Top Axioms');
|
|
215
|
+
lines.push('');
|
|
216
|
+
lockedAxioms.forEach(ax => {
|
|
217
|
+
lines.push(`- **${ax.fields?.one_sentence || ax.id}**`);
|
|
218
|
+
if (ax.fields?.failure_risk) lines.push(` - Failure risk: ${ax.fields.failure_risk}`);
|
|
219
|
+
});
|
|
220
|
+
lines.push('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Top Misunderstandings
|
|
224
|
+
if (lockedMisunderstandings.length > 0) {
|
|
225
|
+
lines.push('## Top Misunderstandings');
|
|
226
|
+
lines.push('');
|
|
227
|
+
lockedMisunderstandings.forEach(ms => {
|
|
228
|
+
lines.push(`- WRONG: ${ms.fields?.wrong}`);
|
|
229
|
+
lines.push(` CORRECT: ${ms.fields?.correct}`);
|
|
230
|
+
});
|
|
231
|
+
lines.push('');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Self-checks
|
|
235
|
+
if (lockedSelfChecks.length > 0) {
|
|
236
|
+
lines.push('## Eval Score');
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(`- quality_badge: ${tests.filter(t => t.result === 'with_kdna_better').length >= 5 ? 'tested' : 'untested'}`);
|
|
239
|
+
lines.push(`- eval cases: ${tests.length}`);
|
|
240
|
+
lines.push('');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Files
|
|
244
|
+
lines.push('## Files');
|
|
245
|
+
lines.push('');
|
|
246
|
+
const fileCount = 2
|
|
247
|
+
+ (cards.filter(c => c.type === 'scenario' && c.locked).length > 0 ? 1 : 0)
|
|
248
|
+
+ (cards.filter(c => c.type === 'case' && c.locked).length > 0 ? 1 : 0)
|
|
249
|
+
+ (lockedAxioms.length > 0 ? 1 : 0)
|
|
250
|
+
+ (cards.filter(c => c.status === 'locked' || c.status === 'tested').length > 0 ? 1 : 0);
|
|
251
|
+
lines.push(`${fileCount} KDNA JSON files + evals/ + demo/`);
|
|
252
|
+
lines.push('');
|
|
253
|
+
|
|
254
|
+
return lines.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = { compileDomain, compileCore, compilePatterns, compileScenarios, compileCases, compileReasoning, compileEvolution, compileManifest, generateReadme };
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const testlab = require('./testlab');
|
|
|
28
28
|
const versioning = require('./versioning');
|
|
29
29
|
const feynman = require('./cards/feynman');
|
|
30
30
|
const contradiction = require('./quality/contradiction');
|
|
31
|
+
const validateCards = require('./quality/validate-cards');
|
|
31
32
|
|
|
32
33
|
module.exports = {
|
|
33
34
|
cards,
|
|
@@ -41,4 +42,5 @@ module.exports = {
|
|
|
41
42
|
versioning,
|
|
42
43
|
feynman,
|
|
43
44
|
contradiction,
|
|
45
|
+
validateCards,
|
|
44
46
|
};
|
package/src/quality/index.js
CHANGED
|
@@ -1,65 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quality
|
|
2
|
+
* Enhanced Quality Gates — 4-grade readiness scoring with detailed rules.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* draft_grade
|
|
6
|
-
* human_controlled
|
|
7
|
-
* tested_grade
|
|
8
|
-
* publishable_grade
|
|
4
|
+
* Grades:
|
|
5
|
+
* draft_grade — Core+Patterns exist, ≥3 human-reviewed cards
|
|
6
|
+
* human_controlled — All core axioms locked, each with applies_when/does_not_apply_when/failure_risk
|
|
7
|
+
* tested_grade — ≥5 eval cases, ≥3 comparison tests
|
|
8
|
+
* publishable_grade — ≥10 evals, README complete, known limitations, kdna verify passes
|
|
9
9
|
*/
|
|
10
|
+
const contradiction = require('./contradiction');
|
|
11
|
+
|
|
10
12
|
function computeReadiness(project) {
|
|
11
13
|
const cards = project.cards || [];
|
|
12
|
-
const locked = cards.filter(c => c.locked);
|
|
13
14
|
const tests = project.tests || [];
|
|
15
|
+
const locked = cards.filter(c => c.locked);
|
|
16
|
+
const lockedAxioms = locked.filter(c => c.type === 'axiom');
|
|
17
|
+
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
18
|
+
const ratedTests = tests.filter(t => t.result);
|
|
14
19
|
|
|
15
20
|
const blocking = [];
|
|
16
21
|
const warnings = [];
|
|
17
22
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// ── Minimum Structure ──────────────────────────────────────────
|
|
24
|
+
if (project.cards.length === 0) {
|
|
25
|
+
blocking.push('Project has no cards');
|
|
26
|
+
return buildResult('draft_grade', blocking, warnings, project);
|
|
27
|
+
}
|
|
28
|
+
if (locked.length === 0) {
|
|
29
|
+
blocking.push('No locked cards — nothing to compile');
|
|
30
|
+
return buildResult('draft_grade', blocking, warnings, project);
|
|
31
|
+
}
|
|
22
32
|
|
|
23
|
-
//
|
|
33
|
+
// ── Axiom Checks ──────────────────────────────────────────────
|
|
24
34
|
for (const ax of lockedAxioms) {
|
|
25
|
-
if (!ax.fields?.
|
|
35
|
+
if (!ax.fields?.one_sentence || ax.fields.one_sentence.length < 10) {
|
|
36
|
+
blocking.push(`${ax.id}: one_sentence too short or missing`);
|
|
37
|
+
}
|
|
38
|
+
if (!ax.fields?.full_statement || ax.fields.full_statement.length < 30) {
|
|
39
|
+
warnings.push(`${ax.id}: full_statement too short — may be vague`);
|
|
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) {
|
|
26
48
|
blocking.push(`${ax.id}: missing does_not_apply_when`);
|
|
27
49
|
}
|
|
28
50
|
if (!ax.fields?.failure_risk) {
|
|
29
|
-
|
|
51
|
+
blocking.push(`${ax.id}: missing failure_risk`);
|
|
30
52
|
}
|
|
31
53
|
if (!ax.human_lock) {
|
|
32
|
-
blocking.push(`${ax.id}:
|
|
54
|
+
blocking.push(`${ax.id}: not locked — must be locked before compile`);
|
|
33
55
|
}
|
|
34
56
|
if (!ax.feynman_restatement) {
|
|
35
57
|
warnings.push(`${ax.id}: missing Feynman restatement`);
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
60
|
|
|
39
|
-
//
|
|
40
|
-
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
61
|
+
// ── Self-check Checks ──────────────────────────────────────────
|
|
41
62
|
for (const sc of lockedSelfChecks) {
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Misunderstanding Checks ────────────────────────────────────
|
|
76
|
+
const lockedMisunderstandings = locked.filter(c => c.type === 'misunderstanding');
|
|
77
|
+
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`);
|
|
44
83
|
}
|
|
84
|
+
if (!ms.fields?.correct || ms.fields.correct.length < 10) {
|
|
85
|
+
warnings.push(`${ms.id}: correct belief very short`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Boundary Checks ────────────────────────────────────────────
|
|
90
|
+
const lockedBoundaries = locked.filter(c => c.type === 'boundary');
|
|
91
|
+
for (const bd of lockedBoundaries) {
|
|
92
|
+
if (bd.fields?.acceptable_exceptions && bd.fields.acceptable_exceptions.length === 0) {
|
|
93
|
+
warnings.push(`${bd.id}: no acceptable_exceptions — every boundary has justified exceptions`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Contradiction Check ────────────────────────────────────────
|
|
98
|
+
const contradictions = contradiction.detectContradictions(cards);
|
|
99
|
+
for (const c of contradictions) {
|
|
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');
|
|
45
107
|
}
|
|
108
|
+
if (ratedTests.length < 3 && ratedTests.length > 0) {
|
|
109
|
+
warnings.push(`Only ${ratedTests.length} rated tests — recommend at least 3 for confidence`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Determine Grade ────────────────────────────────────────────
|
|
113
|
+
const axiomsComplete = lockedAxioms.length >= 1 &&
|
|
114
|
+
lockedAxioms.every(ax =>
|
|
115
|
+
ax.fields?.applies_when?.length &&
|
|
116
|
+
ax.fields?.does_not_apply_when?.length &&
|
|
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);
|
|
46
123
|
|
|
47
|
-
// Determine grade
|
|
48
124
|
let grade = 'draft_grade';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
125
|
+
if (locked.length >= 3 && axiomsComplete) {
|
|
126
|
+
grade = 'human_controlled';
|
|
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) {
|
|
136
|
+
grade = 'publishable_grade';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return buildResult(grade, blocking, warnings, project);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildResult(grade, blocking, warnings, project) {
|
|
143
|
+
const lockedCount = (project.cards || []).filter(c => c.locked).length;
|
|
144
|
+
const ratedTests = (project.tests || []).filter(t => t.result).length;
|
|
55
145
|
|
|
56
146
|
return {
|
|
57
147
|
grade,
|
|
58
148
|
publishable: grade === 'publishable_grade' && blocking.length === 0,
|
|
59
149
|
blocking,
|
|
60
150
|
warnings,
|
|
61
|
-
score: Math.max(0, 100 - blocking.length * 15 - warnings.length *
|
|
151
|
+
score: Math.max(0, 100 - blocking.length * 15 - warnings.length * 3),
|
|
152
|
+
stats: {
|
|
153
|
+
total_cards: (project.cards || []).length,
|
|
154
|
+
locked_cards: lockedCount,
|
|
155
|
+
locked_axioms: (project.cards || []).filter(c => c.type === 'axiom' && c.locked).length,
|
|
156
|
+
locked_self_checks: (project.cards || []).filter(c => c.type === 'self_check' && c.locked).length,
|
|
157
|
+
total_tests: (project.tests || []).length,
|
|
158
|
+
rated_tests: ratedTests,
|
|
159
|
+
},
|
|
160
|
+
next_step: grade === 'draft_grade'
|
|
161
|
+
? 'Lock at least 3 axioms with applies_when, does_not_apply_when, and failure_risk.'
|
|
162
|
+
: grade === 'human_controlled'
|
|
163
|
+
? 'Add 5+ eval cases and run kdna compare to reach tested grade.'
|
|
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.',
|
|
62
167
|
};
|
|
63
168
|
}
|
|
64
169
|
|
|
65
|
-
|
|
170
|
+
function getBlockingIssues(project) {
|
|
171
|
+
const result = computeReadiness(project);
|
|
172
|
+
return result.blocking;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { computeReadiness, getBlockingIssues };
|
|
@@ -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,285 @@
|
|
|
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, createFeynmanRestatement, attachRestatementToLock } = require('../src/cards');
|
|
6
|
+
const { computeReadiness } = require('../src/quality');
|
|
7
|
+
const { detectContradictions } = require('../src/quality/contradiction');
|
|
8
|
+
const { validateCard, validateAllCards } = require('../src/quality/validate-cards');
|
|
9
|
+
const { compileDomain, generateReadme } = require('../src/compile');
|
|
10
|
+
const { createTestCase, recordHumanRating } = require('../src/testlab');
|
|
11
|
+
const { buildProvenance } = require('../src/provenance');
|
|
12
|
+
|
|
13
|
+
function makeLockedCard(type, fields = {}, id = null) {
|
|
14
|
+
const card = createCard(type, fields, id);
|
|
15
|
+
transitionCard(card, 'revised', { by: 'tester' });
|
|
16
|
+
lockCard(card, {
|
|
17
|
+
by: 'tester',
|
|
18
|
+
statement: 'I confirm this judgment.',
|
|
19
|
+
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
|
|
20
|
+
});
|
|
21
|
+
return card;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Quality Gates (4-grade scoring) ─────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe('Quality Gates', () => {
|
|
27
|
+
test('draft_grade: empty project', () => {
|
|
28
|
+
const r = computeReadiness({ cards: [], tests: [] });
|
|
29
|
+
assert.equal(r.grade, 'draft_grade');
|
|
30
|
+
assert.equal(r.publishable, false);
|
|
31
|
+
assert.ok(r.blocking.length > 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('draft_grade: no locked cards → nothing to compile', () => {
|
|
35
|
+
const project = createProject('test');
|
|
36
|
+
project.cards = [createCard('axiom', { one_sentence: 'Draft.' })];
|
|
37
|
+
const r = computeReadiness(project);
|
|
38
|
+
assert.equal(r.grade, 'draft_grade');
|
|
39
|
+
assert.ok(r.blocking.some(b => b.includes('nothing to compile') || b.includes('No locked cards')));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('human_controlled: 3+ locked axioms with boundaries ', () => {
|
|
43
|
+
const project = createProject('test');
|
|
44
|
+
for (let i = 0; i < 3; i++) {
|
|
45
|
+
const ax = makeLockedCard('axiom', {
|
|
46
|
+
one_sentence: `Axiom ${i}: a specific testable judgment principle.`,
|
|
47
|
+
full_statement: `Full statement for axiom ${i} explaining what the agent should do differently.`,
|
|
48
|
+
why: 'Without this, agents would get this specific thing wrong.',
|
|
49
|
+
applies_when: [`situation ${i}a`, `situation ${i}b`],
|
|
50
|
+
does_not_apply_when: [`not when ${i}`],
|
|
51
|
+
failure_risk: `Risk ${i}: misapplying this could cause X.`,
|
|
52
|
+
});
|
|
53
|
+
attachRestatementToLock(ax, createFeynmanRestatement(ax, `Simple explanation for axiom ${i}: when you see this situation, do this instead of that, but only if the conditions are right.`));
|
|
54
|
+
project.cards.push(ax);
|
|
55
|
+
}
|
|
56
|
+
const r = computeReadiness(project);
|
|
57
|
+
assert.equal(r.grade, 'human_controlled');
|
|
58
|
+
assert.equal(r.stats.locked_axioms, 3);
|
|
59
|
+
assert.ok(r.score >= 70);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('tested_grade: 5+ rated tests + 3+ self-checks', () => {
|
|
63
|
+
const project = createProject('test');
|
|
64
|
+
for (let i = 0; i < 3; i++) {
|
|
65
|
+
const ax = makeLockedCard('axiom', {
|
|
66
|
+
one_sentence: `Test axiom ${i}.`,
|
|
67
|
+
full_statement: `Full statement ${i} with enough detail for the agent.`,
|
|
68
|
+
why: 'Because.',
|
|
69
|
+
applies_when: ['when x'],
|
|
70
|
+
does_not_apply_when: ['when y'],
|
|
71
|
+
failure_risk: 'risk',
|
|
72
|
+
});
|
|
73
|
+
project.cards.push(ax);
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < 3; i++) {
|
|
76
|
+
const sc = makeLockedCard('self_check', { question: `Does the response satisfy condition ${i}?` });
|
|
77
|
+
project.cards.push(sc);
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < 6; i++) {
|
|
80
|
+
const tc = createTestCase(`input ${i}`);
|
|
81
|
+
recordHumanRating(tc, i < 4 ? 'with_kdna_better' : 'no_difference', 'tester');
|
|
82
|
+
project.tests.push(tc);
|
|
83
|
+
}
|
|
84
|
+
const r = computeReadiness(project);
|
|
85
|
+
assert.equal(r.grade, 'tested_grade');
|
|
86
|
+
assert.equal(r.stats.rated_tests, 6);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('publishable_grade: 10+ eval cases, 3+ axioms, 5+ self-checks', () => {
|
|
90
|
+
const project = createProject('test');
|
|
91
|
+
for (let i = 0; i < 4; i++) {
|
|
92
|
+
project.cards.push(makeLockedCard('axiom', {
|
|
93
|
+
one_sentence: `Pub axiom ${i} with judgment.`,
|
|
94
|
+
full_statement: `Full statement ${i} with enough detail for the agent to act on.`,
|
|
95
|
+
why: 'Without this, agents default to wrong behavior.',
|
|
96
|
+
applies_when: ['when x'],
|
|
97
|
+
does_not_apply_when: ['when y'],
|
|
98
|
+
failure_risk: 'risk of misapplication',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
for (let i = 0; i < 5; i++) {
|
|
102
|
+
project.cards.push(makeLockedCard('self_check', { question: `Does the output pass criterion ${i}?` }));
|
|
103
|
+
}
|
|
104
|
+
for (let i = 0; i < 12; i++) {
|
|
105
|
+
const tc = createTestCase(`eval case ${i}`);
|
|
106
|
+
recordHumanRating(tc, 'with_kdna_better', 'tester');
|
|
107
|
+
project.tests.push(tc);
|
|
108
|
+
}
|
|
109
|
+
const r = computeReadiness(project);
|
|
110
|
+
assert.equal(r.grade, 'publishable_grade');
|
|
111
|
+
assert.equal(r.publishable, true);
|
|
112
|
+
assert.equal(r.blocking.length, 0);
|
|
113
|
+
assert.ok(r.score >= 75);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ─── Anti-Vagueness Validation ──────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
describe('Card Validation', () => {
|
|
120
|
+
test('flags slogan-like axiom', () => {
|
|
121
|
+
const card = createCard('axiom', { one_sentence: 'Trust is important for teams.', full_statement: 'Trust matters.' });
|
|
122
|
+
const issues = validateCard(card);
|
|
123
|
+
assert.ok(issues.some(i => i.type === 'slogan' || i.type === 'too_short'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('flags SOP-like axiom', () => {
|
|
127
|
+
const card = createCard('axiom', {
|
|
128
|
+
one_sentence: 'First, you should always remember to follow these steps.',
|
|
129
|
+
full_statement: 'The process is to first identify the problem.',
|
|
130
|
+
});
|
|
131
|
+
const issues = validateCard(card);
|
|
132
|
+
assert.ok(issues.some(i => i.type === 'sop'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('flags straw-man misunderstanding', () => {
|
|
136
|
+
const card = createCard('misunderstanding', {
|
|
137
|
+
wrong: 'Some people say it is commonly thought that quality matters.',
|
|
138
|
+
correct: 'Actually it does matter.',
|
|
139
|
+
key_distinction: 'Quality is about getting things right the first time.',
|
|
140
|
+
});
|
|
141
|
+
const issues = validateCard(card);
|
|
142
|
+
assert.ok(issues.some(i => i.type === 'straw_man'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('flags generic self_check', () => {
|
|
146
|
+
const card = createCard('self_check', { question: 'Is this good?' });
|
|
147
|
+
const issues = validateCard(card);
|
|
148
|
+
assert.ok(issues.some(i => i.type === 'generic' || i.type === 'vague'));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('validateAllCards aggregates per-card', () => {
|
|
152
|
+
const project = createProject('test');
|
|
153
|
+
project.cards = [
|
|
154
|
+
createCard('axiom', { one_sentence: 'Trust is key.', full_statement: 'Trust matters in everything.' }),
|
|
155
|
+
createCard('self_check', { question: 'Is it helpful?' }),
|
|
156
|
+
];
|
|
157
|
+
const results = validateAllCards(project);
|
|
158
|
+
assert.equal(results.length, 2);
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
assert.ok(r.card_id);
|
|
161
|
+
assert.ok(r.issues.length > 0);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─── Full 6-File Compile ────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
describe('Full Compile', () => {
|
|
169
|
+
test('produces Core + Patterns minimum', () => {
|
|
170
|
+
const project = createProject('test');
|
|
171
|
+
project.cards = [
|
|
172
|
+
makeLockedCard('axiom', { one_sentence: 'Test.', full_statement: 'Test full.', why: 'Because.', applies_when: ['x'], does_not_apply_when: ['y'], failure_risk: 'risk' }),
|
|
173
|
+
];
|
|
174
|
+
const result = compileDomain(project);
|
|
175
|
+
assert.ok('KDNA_Core.json' in result.files);
|
|
176
|
+
assert.ok('KDNA_Patterns.json' in result.files);
|
|
177
|
+
assert.ok('kdna.json' in result.files);
|
|
178
|
+
assert.ok(result.stats.files_output >= 4); // Core + Patterns + Reasoning + Evolution minimum
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('excludes draft cards from output', () => {
|
|
182
|
+
const project = createProject('test');
|
|
183
|
+
project.cards = [
|
|
184
|
+
makeLockedCard('axiom', { one_sentence: 'Locked.', full_statement: 'FS.', why: 'B.', applies_when: ['x'], does_not_apply_when: ['y'], failure_risk: 'r' }),
|
|
185
|
+
createCard('axiom', { one_sentence: 'Draft.' }),
|
|
186
|
+
];
|
|
187
|
+
const result = compileDomain(project);
|
|
188
|
+
assert.equal(result.stats.locked_cards, 1);
|
|
189
|
+
assert.equal(result.stats.excluded_cards, 1);
|
|
190
|
+
const core = JSON.parse(result.files['KDNA_Core.json']);
|
|
191
|
+
assert.equal(core.axioms.length, 1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('produces Scenarios when scenario cards locked', () => {
|
|
195
|
+
const project = createProject('test');
|
|
196
|
+
project.cards = [
|
|
197
|
+
makeLockedCard('scenario', { id: 'scene_01', name: 'User reports bug', trigger_signal: 'bug report', sub_scenarios: [] }, 'sc_001'),
|
|
198
|
+
];
|
|
199
|
+
const result = compileDomain(project);
|
|
200
|
+
assert.ok('KDNA_Scenarios.json' in result.files);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('produces Reasoning from axiom implications', () => {
|
|
204
|
+
const project = createProject('test');
|
|
205
|
+
project.cards = [
|
|
206
|
+
makeLockedCard('axiom', { one_sentence: 'Price objections are certainty deficits.', full_statement: 'When a buyer says too expensive, first diagnose which type of uncertainty is blocking them.', why: 'Without this axiom, agents default to offering discounts instead of diagnosing.', applies_when: ['price objection'], does_not_apply_when: ['explicit discount request'], failure_risk: 'Agent may seem evasive.' }),
|
|
207
|
+
];
|
|
208
|
+
const result = compileDomain(project);
|
|
209
|
+
assert.ok('KDNA_Reasoning.json' in result.files);
|
|
210
|
+
const reasoning = JSON.parse(result.files['KDNA_Reasoning.json']);
|
|
211
|
+
assert.ok(reasoning.chains.length > 0);
|
|
212
|
+
assert.equal(reasoning.chains[0].so_what, 'Without this axiom, agents default to offering discounts instead of diagnosing.');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('produces Evolution from audit logs', () => {
|
|
216
|
+
const project = createProject('test');
|
|
217
|
+
const card = makeLockedCard('axiom', { one_sentence: 'Evolution test.', full_statement: 'FS.', why: 'B.', applies_when: ['x'], does_not_apply_when: ['y'], failure_risk: 'r' });
|
|
218
|
+
project.cards = [card];
|
|
219
|
+
const result = compileDomain(project);
|
|
220
|
+
const evo = JSON.parse(result.files['KDNA_Evolution.json']);
|
|
221
|
+
assert.ok(evo.stages.length > 0);
|
|
222
|
+
assert.equal(evo.measurements[0].metric, 'locked_axioms');
|
|
223
|
+
assert.equal(evo.measurements[0].value, 1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ─── README Generation ──────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe('README Generation', () => {
|
|
230
|
+
test('generates README with 4 questions', () => {
|
|
231
|
+
const project = createProject('leadership_decisions', 'domain', { author: { name: 'Expert' } });
|
|
232
|
+
project.cards = [
|
|
233
|
+
makeLockedCard('axiom', {
|
|
234
|
+
one_sentence: 'Execution failure is decision failure in disguise.',
|
|
235
|
+
full_statement: 'When a team fails to execute, first check whether a real decision was ever made.',
|
|
236
|
+
why: 'Without this axiom, managers address symptoms while missing the root cause.',
|
|
237
|
+
applies_when: ['Team reports being stuck'],
|
|
238
|
+
does_not_apply_when: ['Clear decision exists'],
|
|
239
|
+
failure_risk: 'May cause over-scrutiny of decision quality when issue is resources.',
|
|
240
|
+
}),
|
|
241
|
+
makeLockedCard('misunderstanding', {
|
|
242
|
+
wrong: 'If the team is not executing, they lack motivation.',
|
|
243
|
+
correct: 'If the team is not executing, first check whether a decision with owner+deadline+criteria was made.',
|
|
244
|
+
key_distinction: 'Motivation gaps produce gradual decline. Decision voids produce sudden stalls.',
|
|
245
|
+
}),
|
|
246
|
+
makeLockedCard('self_check', { question: 'Did I verify that a concrete decision exists before diagnosing the team?' }),
|
|
247
|
+
];
|
|
248
|
+
project.tests = [];
|
|
249
|
+
for (let i = 0; i < 6; i++) {
|
|
250
|
+
const tc = createTestCase(`test ${i}`);
|
|
251
|
+
recordHumanRating(tc, 'with_kdna_better', 'expert');
|
|
252
|
+
project.tests.push(tc);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const readme = generateReadme(project, {
|
|
256
|
+
description: 'Leadership decision-making judgment — diagnose whether execution failures are really decision voids.',
|
|
257
|
+
origin: '15 years of leadership coaching across 200+ teams.',
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
assert.ok(readme.includes('# leadership_decisions'));
|
|
261
|
+
assert.ok(readme.includes('## Where it comes from'));
|
|
262
|
+
assert.ok(readme.includes('## Where it applies'));
|
|
263
|
+
assert.ok(readme.includes('## How it is verified'));
|
|
264
|
+
assert.ok(readme.includes('## When it does NOT apply'));
|
|
265
|
+
assert.ok(readme.includes('15 years'));
|
|
266
|
+
assert.ok(readme.includes('quality_badge: tested'));
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ─── Provenance with Compile ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe('Provenance Integration', () => {
|
|
273
|
+
test('buildProvenance captures compile metadata', () => {
|
|
274
|
+
const project = createProject('test', 'domain', { author: { name: 'Author', id: 'auth_001' } });
|
|
275
|
+
project.cards = [
|
|
276
|
+
makeLockedCard('axiom', { one_sentence: 'T.', full_statement: 'FS.', why: 'B.', applies_when: ['x'], does_not_apply_when: ['y'], failure_risk: 'r' }),
|
|
277
|
+
];
|
|
278
|
+
const compiled = compileDomain(project);
|
|
279
|
+
const prov = buildProvenance(project, compiled.files);
|
|
280
|
+
assert.equal(prov.author_id, 'auth_001');
|
|
281
|
+
assert.equal(prov.locked_card_count, 1);
|
|
282
|
+
assert.ok(prov.build_id.startsWith('build_'));
|
|
283
|
+
assert.ok(prov.content_fingerprint.startsWith('sha256:'));
|
|
284
|
+
});
|
|
285
|
+
});
|