@aikdna/studio-core 0.1.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 +25 -0
- package/src/cards/feynman.js +94 -0
- package/src/cards/index.js +109 -0
- package/src/cli-bridge/index.js +1 -0
- package/src/compile/index.js +118 -0
- package/src/evidence/index.js +81 -0
- package/src/index.js +44 -0
- package/src/packaging/index.js +73 -0
- package/src/project/index.js +62 -0
- package/src/provenance/index.js +35 -0
- package/src/quality/contradiction.js +172 -0
- package/src/quality/index.js +65 -0
- package/src/testlab/index.js +100 -0
- package/src/versioning/index.js +1 -0
- package/tests/core.test.js +132 -0
- package/tests/milestone1.test.js +327 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contradiction Check — Surface conflicts, gaps, and weak judgment.
|
|
3
|
+
*
|
|
4
|
+
* Detects:
|
|
5
|
+
* - Axiom contradictions (two axioms that cannot both be true)
|
|
6
|
+
* - Missing boundaries (axiom lacks does_not_apply_when)
|
|
7
|
+
* - Weak self-checks (not a yes/no question, too vague)
|
|
8
|
+
* - Over-generalized axioms (too broad to be testable)
|
|
9
|
+
* - Straw-man misunderstandings (describes something no one believes)
|
|
10
|
+
* - Missing counterexamples (misunderstanding lacks a real example)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function detectContradictions(cards) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
const axioms = cards.filter(c => c.type === 'axiom' && c.locked);
|
|
16
|
+
|
|
17
|
+
// Check for missing boundaries on locked axioms
|
|
18
|
+
for (const ax of axioms) {
|
|
19
|
+
if (!ax.fields?.does_not_apply_when || ax.fields.does_not_apply_when.length === 0) {
|
|
20
|
+
issues.push({
|
|
21
|
+
type: 'missing_boundary',
|
|
22
|
+
card_id: ax.id,
|
|
23
|
+
severity: 'blocking',
|
|
24
|
+
message: `${ax.id}: locked axiom lacks does_not_apply_when`,
|
|
25
|
+
fix: 'Define at least one situation where this axiom should NOT be applied.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!ax.fields?.applies_when || ax.fields.applies_when.length === 0) {
|
|
29
|
+
issues.push({
|
|
30
|
+
type: 'missing_applicability',
|
|
31
|
+
card_id: ax.id,
|
|
32
|
+
severity: 'blocking',
|
|
33
|
+
message: `${ax.id}: locked axiom lacks applies_when`,
|
|
34
|
+
fix: 'Define at least one situation where this axiom applies.',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (ax.fields?.full_statement && ax.fields.full_statement.length < 30) {
|
|
38
|
+
issues.push({
|
|
39
|
+
type: 'too_short',
|
|
40
|
+
card_id: ax.id,
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
message: `${ax.id}: full_statement is very short — may be too vague`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for over-generalization: axioms that use absolute language without boundaries
|
|
47
|
+
const oneLiner = (ax.fields?.one_sentence || '').toLowerCase();
|
|
48
|
+
if (/\b(always|never|every|all|none|must)\b/.test(oneLiner) && (!ax.fields.does_not_apply_when || ax.fields.does_not_apply_when.length === 0)) {
|
|
49
|
+
issues.push({
|
|
50
|
+
type: 'overgeneralized',
|
|
51
|
+
card_id: ax.id,
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
message: `${ax.id}: uses absolute language ("${oneLiner.match(/always|never|every|all|none|must/)[0]}") but has no does_not_apply_when`,
|
|
54
|
+
fix: 'Add does_not_apply_when to prevent this axiom from being applied universally.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check misunderstandings
|
|
60
|
+
const misunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked);
|
|
61
|
+
for (const ms of misunderstandings) {
|
|
62
|
+
const wrong = ms.fields?.wrong || '';
|
|
63
|
+
const correct = ms.fields?.correct || '';
|
|
64
|
+
|
|
65
|
+
// Check for straw-man: the "wrong" belief describes something no real person would believe
|
|
66
|
+
if (wrong.length < 15) {
|
|
67
|
+
issues.push({
|
|
68
|
+
type: 'vague_misunderstanding',
|
|
69
|
+
card_id: ms.id,
|
|
70
|
+
severity: 'warning',
|
|
71
|
+
message: `${ms.id}: wrong belief description is very short — may not describe a real mistake`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check the wrong and correct are actually different (not just negation)
|
|
76
|
+
const wrongWords = new Set(wrong.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
77
|
+
const correctWords = correct.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
78
|
+
const sharedWords = correctWords.filter(w => wrongWords.has(w)).length;
|
|
79
|
+
if (correctWords.length > 0 && sharedWords / correctWords.length > 0.7) {
|
|
80
|
+
issues.push({
|
|
81
|
+
type: 'weak_distinction',
|
|
82
|
+
card_id: ms.id,
|
|
83
|
+
severity: 'warning',
|
|
84
|
+
message: `${ms.id}: wrong and correct share ${Math.round(sharedWords / correctWords.length * 100)}% of words — distinction may be too weak`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Key distinction check
|
|
89
|
+
if (!ms.fields?.key_distinction || ms.fields.key_distinction.length < 20) {
|
|
90
|
+
issues.push({
|
|
91
|
+
type: 'missing_distinction',
|
|
92
|
+
card_id: ms.id,
|
|
93
|
+
severity: 'blocking',
|
|
94
|
+
message: `${ms.id}: key_distinction missing or too short`,
|
|
95
|
+
fix: 'Explain the conceptual boundary between the wrong belief and the correct one.',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check self-checks
|
|
101
|
+
const selfChecks = cards.filter(c => c.type === 'self_check' && c.locked);
|
|
102
|
+
for (const sc of selfChecks) {
|
|
103
|
+
const question = sc.fields?.question || '';
|
|
104
|
+
if (!question.trim().endsWith('?')) {
|
|
105
|
+
issues.push({
|
|
106
|
+
type: 'not_a_question',
|
|
107
|
+
card_id: sc.id,
|
|
108
|
+
severity: 'blocking',
|
|
109
|
+
message: `${sc.id}: self_check must be phrased as a question ending with ?`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (question.length < 15) {
|
|
113
|
+
issues.push({
|
|
114
|
+
type: 'vague_check',
|
|
115
|
+
card_id: sc.id,
|
|
116
|
+
severity: 'warning',
|
|
117
|
+
message: `${sc.id}: self_check question is very short — may be too vague to verify`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Check for generic self-checks
|
|
121
|
+
const genericPatterns = ['is this good', 'is this correct', 'is this helpful', 'is this clear', 'is this response', 'is the response', 'good enough', 'is it good'];
|
|
122
|
+
if (genericPatterns.some(p => question.toLowerCase().includes(p))) {
|
|
123
|
+
issues.push({
|
|
124
|
+
type: 'generic_check',
|
|
125
|
+
card_id: sc.id,
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
message: `${sc.id}: self_check is too generic — should be domain-specific`,
|
|
128
|
+
fix: 'Rephrase with domain-specific criteria, e.g. "Did the agent diagnose the type of uncertainty before suggesting action?"',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check boundaries
|
|
134
|
+
const boundaries = cards.filter(c => c.type === 'boundary' && c.locked);
|
|
135
|
+
for (const bd of boundaries) {
|
|
136
|
+
if (bd.fields?.out_of_scope && bd.fields.out_of_scope.length < 10) {
|
|
137
|
+
issues.push({
|
|
138
|
+
type: 'vague_boundary',
|
|
139
|
+
card_id: bd.id,
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
message: `${bd.id}: out_of_scope is very short — boundary may be unclear`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (!bd.fields?.acceptable_exceptions || bd.fields.acceptable_exceptions.length === 0) {
|
|
145
|
+
issues.push({
|
|
146
|
+
type: 'no_exceptions',
|
|
147
|
+
card_id: bd.id,
|
|
148
|
+
severity: 'warning',
|
|
149
|
+
message: `${bd.id}: no acceptable_exceptions declared — every boundary has justified exceptions`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return issues;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function summarizeContradictions(issues) {
|
|
158
|
+
const blocking = issues.filter(i => i.severity === 'blocking');
|
|
159
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
160
|
+
return {
|
|
161
|
+
total: issues.length,
|
|
162
|
+
blocking: blocking.length,
|
|
163
|
+
warnings: warnings.length,
|
|
164
|
+
clean: issues.length === 0,
|
|
165
|
+
by_type: issues.reduce((acc, i) => {
|
|
166
|
+
acc[i.type] = (acc[i.type] || 0) + 1;
|
|
167
|
+
return acc;
|
|
168
|
+
}, {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { detectContradictions, summarizeContradictions };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gates and readiness scoring.
|
|
3
|
+
*
|
|
4
|
+
* Four quality grades:
|
|
5
|
+
* draft_grade — structure exists, at least 3 human-reviewed cards
|
|
6
|
+
* human_controlled — all core axioms locked, each with applies_when/does_not_apply_when/failure_risk
|
|
7
|
+
* tested_grade — at least 5 eval cases, at least 3 comparison tests
|
|
8
|
+
* publishable_grade — at least 10 evals, README complete, kdna verify --judgment passes
|
|
9
|
+
*/
|
|
10
|
+
function computeReadiness(project) {
|
|
11
|
+
const cards = project.cards || [];
|
|
12
|
+
const locked = cards.filter(c => c.locked);
|
|
13
|
+
const tests = project.tests || [];
|
|
14
|
+
|
|
15
|
+
const blocking = [];
|
|
16
|
+
const warnings = [];
|
|
17
|
+
|
|
18
|
+
// Check minimum locked axioms
|
|
19
|
+
const lockedAxioms = locked.filter(c => c.type === 'axiom');
|
|
20
|
+
if (lockedAxioms.length < 1) blocking.push('At least 1 locked axiom required');
|
|
21
|
+
if (lockedAxioms.length < 3) warnings.push('Recommend at least 3 locked axioms');
|
|
22
|
+
|
|
23
|
+
// Check boundary completeness
|
|
24
|
+
for (const ax of lockedAxioms) {
|
|
25
|
+
if (!ax.fields?.does_not_apply_when?.length) {
|
|
26
|
+
blocking.push(`${ax.id}: missing does_not_apply_when`);
|
|
27
|
+
}
|
|
28
|
+
if (!ax.fields?.failure_risk) {
|
|
29
|
+
warnings.push(`${ax.id}: missing failure_risk`);
|
|
30
|
+
}
|
|
31
|
+
if (!ax.human_lock) {
|
|
32
|
+
blocking.push(`${ax.id}: missing human_lock`);
|
|
33
|
+
}
|
|
34
|
+
if (!ax.feynman_restatement) {
|
|
35
|
+
warnings.push(`${ax.id}: missing Feynman restatement`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check self-checks
|
|
40
|
+
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
41
|
+
for (const sc of lockedSelfChecks) {
|
|
42
|
+
if (sc.fields?.question && !sc.fields.question.trim().endsWith('?')) {
|
|
43
|
+
blocking.push(`${sc.id}: self_check question must end with ?`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Determine grade
|
|
48
|
+
let grade = 'draft_grade';
|
|
49
|
+
const axiomsComplete = lockedAxioms.every(ax =>
|
|
50
|
+
ax.fields?.applies_when?.length && ax.fields?.does_not_apply_when?.length && ax.human_lock
|
|
51
|
+
);
|
|
52
|
+
if (axiomsComplete && locked.length >= 3) grade = 'human_controlled';
|
|
53
|
+
if (axiomsComplete && tests.length >= 5) grade = 'tested_grade';
|
|
54
|
+
if (axiomsComplete && tests.length >= 10 && lockedSelfChecks.length >= 3) grade = 'publishable_grade';
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
grade,
|
|
58
|
+
publishable: grade === 'publishable_grade' && blocking.length === 0,
|
|
59
|
+
blocking,
|
|
60
|
+
warnings,
|
|
61
|
+
score: Math.max(0, 100 - blocking.length * 15 - warnings.length * 5),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { computeReadiness };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Lab — Validate that a KDNA domain actually changes agent judgment.
|
|
3
|
+
*
|
|
4
|
+
* Core operations:
|
|
5
|
+
* - Create test cases (input → expected_without_kdna → expected_with_kdna)
|
|
6
|
+
* - Run comparison through kdna-cli compare
|
|
7
|
+
* - Record human rating
|
|
8
|
+
* - Attach tests to cards
|
|
9
|
+
* - Export evals for KDNA domain
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function createTestCase(input, options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: `test_${Date.now().toString(36)}`,
|
|
15
|
+
input,
|
|
16
|
+
expected_without_kdna: options.expectedWithout || '',
|
|
17
|
+
expected_with_kdna: options.expectedWith || '',
|
|
18
|
+
domain: options.domain || null,
|
|
19
|
+
result: null, // 'with_kdna_better' | 'no_difference' | 'without_kdna_better'
|
|
20
|
+
human_rating: null,
|
|
21
|
+
rated_by: null,
|
|
22
|
+
rated_at: null,
|
|
23
|
+
notes: '',
|
|
24
|
+
linked_cards: [],
|
|
25
|
+
created_at: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recordHumanRating(testCase, result, ratedBy, notes = '') {
|
|
30
|
+
const validResults = ['with_kdna_better', 'no_difference', 'without_kdna_better'];
|
|
31
|
+
if (!validResults.includes(result)) throw new Error(`Invalid result: ${result}. Must be one of: ${validResults.join(', ')}`);
|
|
32
|
+
testCase.result = result;
|
|
33
|
+
testCase.human_rating = result;
|
|
34
|
+
testCase.rated_by = ratedBy;
|
|
35
|
+
testCase.rated_at = new Date().toISOString();
|
|
36
|
+
testCase.notes = notes;
|
|
37
|
+
return testCase;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function linkTestToCards(testCase, cardIds) {
|
|
41
|
+
testCase.linked_cards = [...new Set([...testCase.linked_cards, ...cardIds])];
|
|
42
|
+
return testCase;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generateTestSummary(project) {
|
|
46
|
+
const tests = project.tests || [];
|
|
47
|
+
const total = tests.length;
|
|
48
|
+
const rated = tests.filter(t => t.result).length;
|
|
49
|
+
const withKdnaBetter = tests.filter(t => t.result === 'with_kdna_better').length;
|
|
50
|
+
const noDiff = tests.filter(t => t.result === 'no_difference').length;
|
|
51
|
+
const withoutBetter = tests.filter(t => t.result === 'without_kdna_better').length;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
total,
|
|
55
|
+
rated,
|
|
56
|
+
unrated: total - rated,
|
|
57
|
+
with_kdna_better: withKdnaBetter,
|
|
58
|
+
with_kdna_better_pct: total > 0 ? Math.round((withKdnaBetter / rated) * 100) : 0,
|
|
59
|
+
no_difference: noDiff,
|
|
60
|
+
without_kdna_better: withoutBetter,
|
|
61
|
+
passing: withKdnaBetter >= Math.ceil(rated * 0.6), // at least 60% of rated tests should favor KDNA
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function exportEvals(project) {
|
|
66
|
+
const tests = (project.tests || []).filter(t => t.result);
|
|
67
|
+
return tests.map(t => ({
|
|
68
|
+
id: t.id,
|
|
69
|
+
input: t.input,
|
|
70
|
+
expected_without_kdna: t.expected_without_kdna || null,
|
|
71
|
+
expected_with_kdna: t.expected_with_kdna || null,
|
|
72
|
+
result: t.result,
|
|
73
|
+
linked_cards: t.linked_cards,
|
|
74
|
+
rated_by: t.rated_by,
|
|
75
|
+
rated_at: t.rated_at,
|
|
76
|
+
notes: t.notes,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compareAdapter(domainName, input, options = {}) {
|
|
81
|
+
// Returns the CLI command and args for kdna compare
|
|
82
|
+
const args = ['compare', domainName, '--input', input];
|
|
83
|
+
if (options.reportMd) args.push('--report-md');
|
|
84
|
+
if (options.reportJson) args.push('--report-json');
|
|
85
|
+
if (options.output) args.push('--output', options.output);
|
|
86
|
+
return {
|
|
87
|
+
command: 'kdna',
|
|
88
|
+
args,
|
|
89
|
+
description: 'Runs kdna compare to test judgment impact',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
createTestCase,
|
|
95
|
+
recordHumanRating,
|
|
96
|
+
linkTestToCards,
|
|
97
|
+
generateTestSummary,
|
|
98
|
+
exportEvals,
|
|
99
|
+
compareAdapter,
|
|
100
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const { createProject, validateProject } = require('../src/project');
|
|
5
|
+
const { createCard, lockCard, getLockedCards, transitionCard } = require('../src/cards');
|
|
6
|
+
const { compileDomain } = require('../src/compile');
|
|
7
|
+
const { computeReadiness } = require('../src/quality');
|
|
8
|
+
const { buildProvenance } = require('../src/provenance');
|
|
9
|
+
|
|
10
|
+
// ─── Project ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
test('createProject returns valid project', () => {
|
|
13
|
+
const p = createProject('test_domain');
|
|
14
|
+
assert.equal(p.name, 'test_domain');
|
|
15
|
+
assert.equal(p.type, 'domain');
|
|
16
|
+
assert.equal(p.status, 'drafting');
|
|
17
|
+
assert.ok(p.project_id);
|
|
18
|
+
assert.ok(Array.isArray(p.cards));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('validateProject catches missing name', () => {
|
|
22
|
+
const r = validateProject({ type: 'domain', cards: [] });
|
|
23
|
+
assert.equal(r.valid, false);
|
|
24
|
+
assert.ok(r.issues.length > 0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ─── Cards State Machine ──────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('createCard returns draft card', () => {
|
|
30
|
+
const card = createCard('axiom', { one_sentence: 'Test axiom' });
|
|
31
|
+
assert.equal(card.status, 'draft');
|
|
32
|
+
assert.equal(card.locked, false);
|
|
33
|
+
assert.equal(card.type, 'axiom');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('transitionCard: draft → revised', () => {
|
|
37
|
+
const card = createCard('axiom', {});
|
|
38
|
+
transitionCard(card, 'revised', { by: 'test_user' });
|
|
39
|
+
assert.equal(card.status, 'revised');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('transitionCard blocks draft → locked', () => {
|
|
43
|
+
const card = createCard('axiom', {});
|
|
44
|
+
assert.throws(() => transitionCard(card, 'locked'), /Invalid transition/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('lockCard sets human_lock and transitions to locked', () => {
|
|
48
|
+
const card = createCard('axiom', { one_sentence: 'Test' });
|
|
49
|
+
transitionCard(card, 'revised', { by: 'test_user' });
|
|
50
|
+
lockCard(card, {
|
|
51
|
+
by: 'test_user',
|
|
52
|
+
statement: 'I confirm this judgment.',
|
|
53
|
+
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true },
|
|
54
|
+
});
|
|
55
|
+
assert.equal(card.status, 'locked');
|
|
56
|
+
assert.equal(card.locked, true);
|
|
57
|
+
assert.ok(card.human_lock);
|
|
58
|
+
assert.equal(card.human_lock.by, 'test_user');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('lockCard rejects missing checked fields', () => {
|
|
62
|
+
const card = createCard('axiom', {});
|
|
63
|
+
transitionCard(card, 'revised', { by: 'test_user' });
|
|
64
|
+
assert.throws(() => lockCard(card, { by: 'u', statement: 'ok', checked: {} }), /applies_when/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('getLockedCards filters correctly', () => {
|
|
68
|
+
const project = { cards: [] };
|
|
69
|
+
const c1 = createCard('axiom', {});
|
|
70
|
+
transitionCard(c1, 'revised', { by: 'u' });
|
|
71
|
+
lockCard(c1, { by: 'u', statement: 'ok', checked: { applies_when: true, does_not_apply_when: true, failure_risk: true } });
|
|
72
|
+
project.cards.push(c1);
|
|
73
|
+
project.cards.push(createCard('misunderstanding', {})); // draft
|
|
74
|
+
assert.equal(getLockedCards(project).length, 1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── Compile ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
test('compileDomain only includes locked cards', () => {
|
|
80
|
+
const project = createProject('test');
|
|
81
|
+
const card = createCard('axiom', {
|
|
82
|
+
one_sentence: 'Test axiom.',
|
|
83
|
+
full_statement: 'Full statement.',
|
|
84
|
+
why: 'Because.',
|
|
85
|
+
applies_when: ['when testing'],
|
|
86
|
+
does_not_apply_when: ['when not testing'],
|
|
87
|
+
failure_risk: 'Test may fail.',
|
|
88
|
+
});
|
|
89
|
+
transitionCard(card, 'revised', { by: 'u' });
|
|
90
|
+
lockCard(card, { by: 'u', statement: 'ok', checked: { applies_when: true, does_not_apply_when: true, failure_risk: true } });
|
|
91
|
+
project.cards = [card, createCard('axiom', { one_sentence: 'Draft axiom' })];
|
|
92
|
+
|
|
93
|
+
const result = compileDomain(project);
|
|
94
|
+
assert.ok('KDNA_Core.json' in result.files);
|
|
95
|
+
assert.equal(result.stats.locked_cards, 1);
|
|
96
|
+
assert.equal(result.stats.excluded_cards, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── Quality ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
test('computeReadiness empty project → draft_grade', () => {
|
|
102
|
+
const r = computeReadiness({ cards: [], tests: [] });
|
|
103
|
+
assert.equal(r.grade, 'draft_grade');
|
|
104
|
+
assert.equal(r.publishable, false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('computeReadiness locked cards → human_controlled', () => {
|
|
108
|
+
const project = createProject('test');
|
|
109
|
+
const card = createCard('axiom', {
|
|
110
|
+
one_sentence: 'Test.',
|
|
111
|
+
applies_when: ['when test'],
|
|
112
|
+
does_not_apply_when: ['when not test'],
|
|
113
|
+
failure_risk: 'risk',
|
|
114
|
+
});
|
|
115
|
+
transitionCard(card, 'revised', { by: 'u' });
|
|
116
|
+
lockCard(card, { by: 'u', statement: 'ok', checked: { applies_when: true, does_not_apply_when: true, failure_risk: true } });
|
|
117
|
+
project.cards = [card];
|
|
118
|
+
const r = computeReadiness(project);
|
|
119
|
+
assert.equal(r.grade, 'draft_grade'); // still need 3 locked cards for human_controlled
|
|
120
|
+
assert.ok(r.warnings.length > 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ─── Provenance ───────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
test('buildProvenance returns metadata', () => {
|
|
126
|
+
const project = createProject('test', 'domain', { author: { name: 'Tester', id: 'tester' } });
|
|
127
|
+
project.cards = [];
|
|
128
|
+
const prov = buildProvenance(project, {});
|
|
129
|
+
assert.equal(prov.studio_core, 'knowledge-dna/kdna-studio');
|
|
130
|
+
assert.ok(prov.build_id);
|
|
131
|
+
assert.ok(prov.content_fingerprint);
|
|
132
|
+
});
|