@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 ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@aikdna/studio-core",
3
+ "version": "0.1.0",
4
+ "description": "KDNA Studio Core — pure logic library for authoring, validating, and compiling KDNA domain judgment packages.",
5
+ "type": "commonjs",
6
+ "main": "src/index.js",
7
+ "license": "Apache-2.0",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/knowledge-dna/kdna-studio.git",
11
+ "directory": "packages/studio-core"
12
+ },
13
+ "dependencies": {
14
+ "@aikdna/kdna-core": "^0.3.0"
15
+ },
16
+ "peerDependencies": {
17
+ "@aikdna/kdna-cli": ">=0.16.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "test": "node --test tests/*.test.js"
24
+ }
25
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Feynman Restatement — Verify understanding, not just agreement.
3
+ *
4
+ * The Feynman technique: explain a concept in simple terms a non-expert
5
+ * would understand. This proves the expert truly owns the judgment,
6
+ * rather than just nodding at an AI proposal.
7
+ */
8
+
9
+ function createFeynmanRestatement(card, text) {
10
+ if (!text || typeof text !== 'string') throw new Error('Feynman restatement text is required');
11
+ if (text.length < 20) throw new Error('Feynman restatement too short (minimum 20 chars)');
12
+
13
+ const restatement = {
14
+ text,
15
+ evaluated_at: new Date().toISOString(),
16
+ score: evaluateRestatementQuality(card, text),
17
+ };
18
+
19
+ return restatement;
20
+ }
21
+
22
+ function evaluateRestatementQuality(card, text) {
23
+ const original = card.fields?.one_sentence || card.fields?.essence || card.fields?.wrong || '';
24
+ const originalLower = original.toLowerCase();
25
+ const textLower = text.toLowerCase();
26
+
27
+ // 1. Not just a repeat — check word overlap ratio
28
+ const originalWords = new Set(originalLower.split(/\s+/).filter(w => w.length > 3));
29
+ const textWords = textLower.split(/\s+/).filter(w => w.length > 3);
30
+ const overlapCount = textWords.filter(w => originalWords.has(w)).length;
31
+ const overlapRatio = textWords.length > 0 ? overlapCount / textWords.length : 1;
32
+ const not_just_repeat = overlapRatio < 0.5;
33
+
34
+ // 2. Not too abstract — check for concrete words
35
+ const concreteSignals = ['example', 'instance', 'case', 'scenario', 'when', 'if', 'customer', 'user', 'client', 'team', 'manager', 'project', 'code', 'product', 'meeting', 'email'];
36
+ const hasConcrete = concreteSignals.some(w => textLower.includes(w));
37
+ const not_too_abstract = hasConcrete;
38
+
39
+ // 3. Has concrete example — check for story-like patterns
40
+ const storyPatterns = ['when', 'if', 'because', 'so', 'then', 'example', 'imagine', 'suppose', 'consider'];
41
+ const hasStory = storyPatterns.filter(w => textLower.includes(w)).length >= 2;
42
+ const has_concrete_example = hasStory;
43
+
44
+ // 4. Clarifies boundary — mentions what it's NOT
45
+ const boundaryWords = ['not', "don't", 'does not', 'cannot', 'unless', 'except', 'only if', 'but not', 'however'];
46
+ const clarifies_boundary = boundaryWords.some(w => textLower.includes(w));
47
+
48
+ // 5. Ordinary person understands — Flesch-like readability check
49
+ const sentences = text.split(/[.!?]+/).filter(s => s.trim());
50
+ const avgWordsPerSentence = sentences.length > 0
51
+ ? sentences.reduce((s, sent) => s + sent.trim().split(/\s+/).length, 0) / sentences.length
52
+ : 99;
53
+ const ordinary_person_understands = avgWordsPerSentence < 25;
54
+
55
+ const scores = { not_just_repeat, not_too_abstract, has_concrete_example, clarifies_boundary, ordinary_person_understands };
56
+ const totalScore = Object.values(scores).filter(Boolean).length;
57
+
58
+ return {
59
+ ...scores,
60
+ total: totalScore,
61
+ quality: totalScore >= 4 ? 'good' : totalScore >= 3 ? 'acceptable' : 'needs_improvement',
62
+ detail: {
63
+ overlap_ratio: Math.round(overlapRatio * 100) + '%',
64
+ avg_words_per_sentence: Math.round(avgWordsPerSentence),
65
+ },
66
+ };
67
+ }
68
+
69
+ function attachRestatementToLock(card, restatement) {
70
+ if (!card.human_lock) throw new Error('Card must be locked before attaching Feynman restatement');
71
+ card.feynman_restatement = restatement;
72
+ card.audit_log.push({
73
+ at: new Date().toISOString(),
74
+ event: 'feynman_restatement',
75
+ by: card.human_lock.by,
76
+ });
77
+ return card;
78
+ }
79
+
80
+ function validateRestatementCard(card) {
81
+ const issues = [];
82
+ if (!card.feynman_restatement) {
83
+ issues.push({ type: 'missing_feynman', severity: 'warning', message: `${card.id}: missing Feynman restatement (lock is stronger with it)` });
84
+ return issues;
85
+ }
86
+ const fr = card.feynman_restatement;
87
+ if (!fr.score?.not_just_repeat) issues.push({ type: 'repeat', severity: 'warning', message: `${card.id}: Feynman may just repeat the original text` });
88
+ if (!fr.score?.not_too_abstract) issues.push({ type: 'abstract', severity: 'warning', message: `${card.id}: Feynman may be too abstract` });
89
+ if (!fr.score?.clarifies_boundary) issues.push({ type: 'no_boundary', severity: 'warning', message: `${card.id}: Feynman does not explain when this does NOT apply` });
90
+ if (!fr.score?.ordinary_person_understands) issues.push({ type: 'complex', severity: 'warning', message: `${card.id}: Feynman may be too complex for a non-expert` });
91
+ return issues;
92
+ }
93
+
94
+ module.exports = { createFeynmanRestatement, evaluateRestatementQuality, attachRestatementToLock, validateRestatementCard };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Judgment Card state machine and lifecycle.
3
+ *
4
+ * Responsibilities:
5
+ * - Card CRUD operations
6
+ * - State machine enforcement (Draft → Revised → Locked → Tested → Published → Deprecated)
7
+ * - Human Lock protocol
8
+ * - Feynman Restatement
9
+ * - Audit trail management
10
+ */
11
+
12
+ const VALID_STATES = ['draft', 'revised', 'locked', 'tested', 'published', 'deprecated'];
13
+ const CARD_TYPES = ['axiom', 'ontology', 'misunderstanding', 'boundary', 'self_check', 'risk', 'aesthetic', 'scenario', 'case'];
14
+
15
+ const TRANSITIONS = {
16
+ draft: ['revised', 'deprecated'],
17
+ revised: ['locked', 'draft', 'deprecated'],
18
+ locked: ['tested', 'revised', 'deprecated'],
19
+ tested: ['published', 'locked', 'deprecated'],
20
+ published: ['deprecated'],
21
+ deprecated: [],
22
+ };
23
+
24
+ function createCard(type, fields = {}, id = null) {
25
+ if (!CARD_TYPES.includes(type)) throw new Error(`Invalid card type: ${type}`);
26
+ const card = {
27
+ id: id || `${type.slice(0, 2)}_${Date.now().toString(36)}`,
28
+ type,
29
+ status: 'draft',
30
+ locked: false,
31
+ fields,
32
+ evidence_refs: [],
33
+ test_refs: [],
34
+ human_lock: null,
35
+ feynman_restatement: null,
36
+ audit_log: [
37
+ { at: new Date().toISOString(), event: 'created', by: 'ai' }
38
+ ],
39
+ };
40
+ return card;
41
+ }
42
+
43
+ function transitionCard(card, toState, transitionContext = {}) {
44
+ if (!VALID_STATES.includes(toState)) throw new Error(`Invalid state: ${toState}`);
45
+ if (!TRANSITIONS[card.status].includes(toState)) {
46
+ throw new Error(`Invalid transition: ${card.status} → ${toState}`);
47
+ }
48
+ const from = card.status;
49
+ card.status = toState;
50
+ card.locked = ['locked', 'tested', 'published'].includes(toState);
51
+ card.audit_log.push({
52
+ at: new Date().toISOString(),
53
+ event: toState,
54
+ by: transitionContext.by || 'system',
55
+ ...(transitionContext.reason && { reason: transitionContext.reason }),
56
+ });
57
+ return card;
58
+ }
59
+
60
+ function lockCard(card, lockPayload) {
61
+ if (!lockPayload.by) throw new Error('lockPayload.by is required');
62
+ if (!lockPayload.statement) throw new Error('lockPayload.statement is required (expert confirmation in own words)');
63
+ if (!lockPayload.checked?.applies_when) throw new Error('Must confirm applies_when reviewed');
64
+ if (!lockPayload.checked?.does_not_apply_when) throw new Error('Must confirm does_not_apply_when reviewed');
65
+ if (!lockPayload.checked?.failure_risk) throw new Error('Must confirm failure_risk reviewed');
66
+
67
+ card.human_lock = {
68
+ by: lockPayload.by,
69
+ at: new Date().toISOString(),
70
+ statement: lockPayload.statement,
71
+ checked: lockPayload.checked,
72
+ };
73
+
74
+ return transitionCard(card, 'locked', { by: lockPayload.by });
75
+ }
76
+
77
+ function unlockCard(card, reason, by) {
78
+ if (!reason) throw new Error('Unlock requires a reason');
79
+ card.human_lock = null;
80
+ return transitionCard(card, 'revised', {
81
+ by,
82
+ reason: `unlocked: ${reason}`,
83
+ });
84
+ }
85
+
86
+ function getLockedCards(project) {
87
+ return project.cards.filter(c => ['locked', 'tested', 'published'].includes(c.status));
88
+ }
89
+
90
+ function getPublishableCards(project) {
91
+ return project.cards.filter(c => c.status === 'tested' || c.status === 'locked');
92
+ }
93
+
94
+ module.exports = {
95
+ CARD_TYPES,
96
+ VALID_STATES,
97
+ TRANSITIONS,
98
+ createCard,
99
+ transitionCard,
100
+ lockCard,
101
+ unlockCard,
102
+ getLockedCards,
103
+ getPublishableCards,
104
+ // Feynman restatement (re-exported from feynman.js)
105
+ createFeynmanRestatement: require('./feynman').createFeynmanRestatement,
106
+ evaluateRestatementQuality: require('./feynman').evaluateRestatementQuality,
107
+ attachRestatementToLock: require('./feynman').attachRestatementToLock,
108
+ validateRestatementCard: require('./feynman').validateRestatementCard,
109
+ };
@@ -0,0 +1 @@
1
+ module.exports = {};
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Compile locked cards into KDNA domain JSON files.
3
+ *
4
+ * Only locked cards enter compilation output.
5
+ * Draft/Revised cards are silently excluded.
6
+ */
7
+
8
+ function compileCore(cards) {
9
+ const axioms = cards.filter(c => c.type === 'axiom' && c.locked).map(c => ({
10
+ id: c.id,
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
+ }));
17
+ const frameworks = [];
18
+ const stances = [];
19
+ const boundaries = cards.filter(c => c.type === 'boundary' && c.locked).map(c => ({
20
+ id: c.id,
21
+ scope: c.fields.scope,
22
+ out_of_scope: c.fields.out_of_scope,
23
+ acceptable_exceptions: c.fields.acceptable_exceptions || [],
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,
31
+ }));
32
+
33
+ return { axioms, ontology, frameworks, stances, boundaries, risks };
34
+ }
35
+
36
+ function compilePatterns(cards) {
37
+ const misunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked).map(c => ({
38
+ id: c.id,
39
+ wrong: c.fields.wrong,
40
+ correct: c.fields.correct,
41
+ key_distinction: c.fields.key_distinction,
42
+ failure_risk: c.fields.failure_risk,
43
+ }));
44
+ const selfChecks = cards.filter(c => c.type === 'self_check' && c.locked).map(c => c.fields.question);
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,
50
+ }));
51
+
52
+ const terminology = {
53
+ standard_terms: [],
54
+ banned_terms: bannedTerms,
55
+ };
56
+
57
+ return { terminology, misunderstandings, self_check: selfChecks, aesthetics };
58
+ }
59
+
60
+ function compileScenarios(cards) {
61
+ return cards.filter(c => c.type === 'scenario' && c.locked).map(c => ({
62
+ id: c.id,
63
+ ...c.fields,
64
+ }));
65
+ }
66
+
67
+ function compileCases(cards) {
68
+ return cards.filter(c => c.type === 'case' && c.locked).map(c => ({
69
+ id: c.id,
70
+ ...c.fields,
71
+ }));
72
+ }
73
+
74
+ function compileManifest(project) {
75
+ const lockedCount = project.cards.filter(c => c.locked).length;
76
+ return {
77
+ kdna_spec: '1.0-rc',
78
+ name: project.name,
79
+ version: project.release?.version || '0.1.0',
80
+ status: 'experimental',
81
+ access: project.release?.access || 'open',
82
+ author: project.author,
83
+ description: project.release?.description || project.name,
84
+ file_count: 2, // Core + Patterns minimum
85
+ created: project.created,
86
+ updated: project.updated,
87
+ };
88
+ }
89
+
90
+ function compileDomain(project) {
91
+ const cards = project.cards || [];
92
+ const core = compileCore(cards);
93
+ const patterns = compilePatterns(cards);
94
+ const scenarios = compileScenarios(cards);
95
+ const cases = compileCases(cards);
96
+ const manifest = compileManifest(project);
97
+
98
+ const files = {};
99
+ files['KDNA_Core.json'] = JSON.stringify(core, null, 2);
100
+ 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);
103
+ files['kdna.json'] = JSON.stringify(manifest, null, 2);
104
+
105
+ const excludedCount = (project.cards || []).filter(c => !c.locked && !['deprecated'].includes(c.status)).length;
106
+
107
+ return {
108
+ files,
109
+ stats: {
110
+ total_cards: (project.cards || []).length,
111
+ locked_cards: (project.cards || []).filter(c => c.locked).length,
112
+ excluded_cards: excludedCount,
113
+ deprecated_cards: (project.cards || []).filter(c => c.status === 'deprecated').length,
114
+ },
115
+ };
116
+ }
117
+
118
+ module.exports = { compileDomain, compileCore, compilePatterns, compileScenarios, compileCases, compileManifest };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Evidence management — import, annotate, and link raw material to judgment cards.
3
+ *
4
+ * Evidence proves "there is material here." It does NOT prove "this is judgment."
5
+ * Spans are extracted text segments that MAY indicate a judgment pattern.
6
+ */
7
+
8
+ const crypto = require('crypto');
9
+
10
+ function createEvidenceEntry(type, title, content, source = 'manual') {
11
+ return {
12
+ id: `ev_${Date.now().toString(36)}`,
13
+ type,
14
+ title,
15
+ content_hash: `sha256:${crypto.createHash('sha256').update(content || '').digest('hex')}`,
16
+ source,
17
+ imported_at: new Date().toISOString(),
18
+ spans: [],
19
+ content: type === 'text' || type === 'chat' ? content : undefined,
20
+ };
21
+ }
22
+
23
+ function addEvidence(project, evidence) {
24
+ project.evidence = project.evidence || [];
25
+ project.evidence.push(evidence);
26
+ if (project.stages?.evidence_room) {
27
+ project.stages.evidence_room.evidence_count = project.evidence.length;
28
+ project.stages.evidence_room.status = 'in_progress';
29
+ }
30
+ return project;
31
+ }
32
+
33
+ function extractSpan(evidence, start, end, candidatePattern = null) {
34
+ const text = evidence.content ? evidence.content.slice(start, end) : '';
35
+ const span = {
36
+ id: `span_${evidence.id}_${evidence.spans.length}`,
37
+ text: text.slice(0, 200), // cap at 200 chars
38
+ start,
39
+ end,
40
+ candidate_pattern: candidatePattern,
41
+ extracted_at: new Date().toISOString(),
42
+ };
43
+ evidence.spans.push(span);
44
+ return span;
45
+ }
46
+
47
+ function linkEvidenceToCard(evidence, spanId, card) {
48
+ if (!card.evidence_refs) card.evidence_refs = [];
49
+ const ref = `${evidence.id}:${spanId}`;
50
+ if (!card.evidence_refs.includes(ref)) {
51
+ card.evidence_refs.push(ref);
52
+ }
53
+ return card;
54
+ }
55
+
56
+ function getEvidenceForCard(evidenceEntries, card) {
57
+ if (!card.evidence_refs) return [];
58
+ return card.evidence_refs.map(ref => {
59
+ const [evId, spanId] = ref.split(':');
60
+ const ev = evidenceEntries.find(e => e.id === evId);
61
+ if (!ev) return null;
62
+ const span = spanId ? ev.spans.find(s => s.id === spanId) : null;
63
+ return { evidence: ev, span };
64
+ }).filter(Boolean);
65
+ }
66
+
67
+ function markEvidenceRoomComplete(project) {
68
+ if (project.stages?.evidence_room) {
69
+ project.stages.evidence_room.status = 'complete';
70
+ }
71
+ return project;
72
+ }
73
+
74
+ module.exports = {
75
+ createEvidenceEntry,
76
+ addEvidence,
77
+ extractSpan,
78
+ linkEvidenceToCard,
79
+ getEvidenceForCard,
80
+ markEvidenceRoomComplete,
81
+ };
package/src/index.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * KDNA Studio Core — Pure logic for authoring KDNA domain judgment.
3
+ *
4
+ * This is the canonical open-source implementation of the Studio authoring
5
+ * workflow. Every exported function is pure logic — no UI dependencies.
6
+ *
7
+ * Modules:
8
+ * project/ Studio Project CRUD, validation, lifecycle
9
+ * evidence/ Evidence import, span extraction, card linkage
10
+ * cards/ Card state machine, human lock, Feynman restatement
11
+ * quality/ Quality gates, readiness scoring
12
+ * compile/ Locked cards → KDNA JSON files
13
+ * testlab/ Test case model, comparison runner
14
+ * provenance/ Build metadata, content fingerprinting
15
+ * packaging/ .kdna / .kdnae pack adapters
16
+ * versioning/ Judgment diff, changelog generation
17
+ * cli-bridge/ Adapter to kdna-cli subprocess calls
18
+ */
19
+
20
+ const cards = require('./cards');
21
+ const compile = require('./compile');
22
+ const evidence = require('./evidence');
23
+ const packaging = require('./packaging');
24
+ const project = require('./project');
25
+ const provenance = require('./provenance');
26
+ const quality = require('./quality');
27
+ const testlab = require('./testlab');
28
+ const versioning = require('./versioning');
29
+ const feynman = require('./cards/feynman');
30
+ const contradiction = require('./quality/contradiction');
31
+
32
+ module.exports = {
33
+ cards,
34
+ compile,
35
+ evidence,
36
+ packaging,
37
+ project,
38
+ provenance,
39
+ quality,
40
+ testlab,
41
+ versioning,
42
+ feynman,
43
+ contradiction,
44
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Packaging adapter — call kdna-cli for pack, verify, sign, publish operations.
3
+ *
4
+ * Studio Core does not re-implement these. It provides structured interfaces
5
+ * that delegate to kdna-cli subprocess calls.
6
+ */
7
+
8
+ const { execSync } = require('child_process');
9
+ const path = require('path');
10
+
11
+ function packDomain(domainDir, outputDir = null) {
12
+ const args = ['pack', domainDir];
13
+ if (outputDir) args.push('--output', outputDir);
14
+ const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
15
+ return { success: true, output: result.trim() };
16
+ }
17
+
18
+ function packEncryptedDomain(domainDir, licensePath, outputDir = null) {
19
+ const args = ['pack', domainDir, '--encrypt', '--license', licensePath];
20
+ if (outputDir) args.push('--output', outputDir);
21
+ const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
22
+ return { success: true, output: result.trim() };
23
+ }
24
+
25
+ function verifyDomain(domainPath) {
26
+ try {
27
+ const result = execSync(`kdna verify ${domainPath} --json`, { encoding: 'utf8' });
28
+ return JSON.parse(result);
29
+ } catch (e) {
30
+ const stdout = (e.stdout || '').toString();
31
+ try { return JSON.parse(stdout); } catch { return { error: e.message, stdout }; }
32
+ }
33
+ }
34
+
35
+ function inspectContainer(filePath) {
36
+ try {
37
+ const result = execSync(`kdna inspect ${filePath} --json`, { encoding: 'utf8' });
38
+ return JSON.parse(result);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function signDomain(domainDir, identityDir = null) {
45
+ // Uses kdna publish --check for signing
46
+ const args = ['publish', '--check', domainDir];
47
+ try {
48
+ const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8', env: { ...process.env } });
49
+ return { success: true, output: result.trim() };
50
+ } catch (e) {
51
+ return { success: false, error: e.stderr?.toString() || e.message };
52
+ }
53
+ }
54
+
55
+ function generateLicense(domain, issuedTo, savePath = null) {
56
+ const args = ['license', 'generate', domain, '--to', issuedTo];
57
+ if (savePath) args.push('--save', savePath);
58
+ try {
59
+ const result = execSync(`kdna ${args.join(' ')}`, { encoding: 'utf8' });
60
+ return { success: true, output: result.trim() };
61
+ } catch (e) {
62
+ return { success: false, error: e.stderr?.toString() || e.message };
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ packDomain,
68
+ packEncryptedDomain,
69
+ verifyDomain,
70
+ inspectContainer,
71
+ signDomain,
72
+ generateLicense,
73
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Studio Project lifecycle.
3
+ *
4
+ * Responsibilities:
5
+ * - Create, load, save, validate Studio Project manifests
6
+ * - Manage project-level state transitions
7
+ * - Schema validation against studio.project.schema.json
8
+ */
9
+
10
+ const projectSchema = require('../../../studio-schemas/studio.project.schema.json');
11
+
12
+ function createProject(name, type = 'domain', options = {}) {
13
+ const project = {
14
+ studio_version: '0.1.0',
15
+ project_id: `studio_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
16
+ name,
17
+ type,
18
+ created: new Date().toISOString().slice(0, 10),
19
+ updated: new Date().toISOString().slice(0, 10),
20
+ author: options.author || { name: '', id: '' },
21
+ status: 'drafting',
22
+ cards: [],
23
+ evidence: [],
24
+ tests: [],
25
+ stages: {
26
+ evidence_room: { status: 'pending', evidence_count: 0 },
27
+ interview_room: { status: 'pending', questions_asked: 0 },
28
+ judgment_cards: { status: 'pending', locked: 0, total: 0 },
29
+ test_lab: { status: 'pending', evals_passed: 0, evals_total: 0 },
30
+ export: { status: 'pending' },
31
+ },
32
+ };
33
+ return project;
34
+ }
35
+
36
+ function loadProject(json) {
37
+ const project = typeof json === 'string' ? JSON.parse(json) : json;
38
+ // TODO: validate against schema
39
+ return project;
40
+ }
41
+
42
+ function saveProject(project) {
43
+ project.updated = new Date().toISOString().slice(0, 10);
44
+ return JSON.stringify(project, null, 2);
45
+ }
46
+
47
+ function validateProject(project) {
48
+ // TODO: full schema validation using ajv + studio.project.schema.json
49
+ const issues = [];
50
+ if (!project.name) issues.push('Missing project name');
51
+ if (!project.type || !['domain', 'cluster'].includes(project.type)) issues.push('Invalid project type');
52
+ if (!Array.isArray(project.cards)) issues.push('cards must be an array');
53
+ return { valid: issues.length === 0, issues };
54
+ }
55
+
56
+ function upgradeProject(project, fromVersion, toVersion) {
57
+ // TODO: migration logic across studio_version changes
58
+ project.studio_version = toVersion;
59
+ return project;
60
+ }
61
+
62
+ module.exports = { createProject, loadProject, saveProject, validateProject, upgradeProject };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Provenance tracking — build metadata and content fingerprinting.
3
+ *
4
+ * Every compiled KDNA domain carries provenance proving:
5
+ * - Which Studio Core version created it
6
+ * - Which project it came from
7
+ * - Who authored the locked cards
8
+ * - Content tree fingerprint
9
+ */
10
+ const crypto = require('crypto');
11
+
12
+ function buildProvenance(project, compiledFiles) {
13
+ const lockedCards = (project.cards || []).filter(c => c.locked);
14
+ const tests = project.tests || [];
15
+
16
+ // Content fingerprint: hash of all locked card content
17
+ const cardHashes = lockedCards
18
+ .sort((a, b) => a.id.localeCompare(b.id))
19
+ .map(c => `${c.id}:${c.fields?.one_sentence || c.fields?.concept || ''}`);
20
+ const contentFingerprint = crypto.createHash('sha256').update(cardHashes.join('\n')).digest('hex');
21
+
22
+ return {
23
+ studio_core: 'knowledge-dna/kdna-studio',
24
+ studio_core_version: project.studio_version || '0.1.0',
25
+ build_id: `build_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
26
+ project_id: project.project_id,
27
+ author_id: project.author?.id || '',
28
+ locked_card_count: lockedCards.length,
29
+ test_case_count: tests.length,
30
+ built_at: new Date().toISOString(),
31
+ content_fingerprint: `sha256:${contentFingerprint}`,
32
+ };
33
+ }
34
+
35
+ module.exports = { buildProvenance };