@aikdna/kdna-studio-core 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +168 -0
- package/package.json +27 -0
- package/schemas/studio.project.schema.json +194 -0
- package/src/cards/feynman.js +105 -0
- package/src/cards/index.js +114 -0
- package/src/cli-bridge/index.js +2 -0
- package/src/compile/index.js +511 -0
- package/src/evidence/index.js +81 -0
- package/src/governance/index.js +140 -0
- package/src/i18n/index.js +145 -0
- package/src/index.js +59 -0
- package/src/judgment-fields.js +28 -0
- package/src/packaging/index.js +88 -0
- package/src/pipeline.js +101 -0
- package/src/project/index.js +359 -0
- package/src/provenance/index.js +44 -0
- package/src/quality/contradiction.js +183 -0
- package/src/quality/index.js +161 -0
- package/src/quality/validate-cards.js +164 -0
- package/src/testlab/delta.js +193 -0
- package/src/testlab/index.js +116 -0
- package/src/versioning/index.js +155 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile locked cards into KDNA domain JSON files — SPEC-compatible output.
|
|
3
|
+
*
|
|
4
|
+
* KDNA SPEC v1.0-rc requirements:
|
|
5
|
+
* - Every file MUST have meta: { version, domain, created, purpose, load_condition }
|
|
6
|
+
* - Minimum output: KDNA_Core.json + KDNA_Patterns.json
|
|
7
|
+
* - Maximum 6 KDNA JSON files per domain
|
|
8
|
+
* - KDNA_Scenarios.json: { meta, scenes[] }
|
|
9
|
+
* - KDNA_Reasoning.json: { meta, reasoning_chains[] }
|
|
10
|
+
* - KDNA_Evolution.json: { meta, stages[], capability_layers[], measurements[] }
|
|
11
|
+
*
|
|
12
|
+
* Only locked cards enter compilation. Draft/Revised excluded silently.
|
|
13
|
+
*/
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
function uuidv7() {
|
|
17
|
+
const ts = BigInt(Date.now());
|
|
18
|
+
const rand = crypto.randomBytes(10);
|
|
19
|
+
const bytes = Buffer.alloc(16);
|
|
20
|
+
bytes.writeUIntBE(Number(ts), 0, 6);
|
|
21
|
+
bytes[6] = 0x70 | (rand[0] & 0x0f);
|
|
22
|
+
bytes[7] = rand[1];
|
|
23
|
+
bytes[8] = 0x80 | (rand[2] & 0x3f);
|
|
24
|
+
rand.copy(bytes, 9, 3);
|
|
25
|
+
const hex = bytes.toString('hex');
|
|
26
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stableStringify(value) {
|
|
30
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
31
|
+
if (value && typeof value === 'object') {
|
|
32
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canonicalEntryBytes(fileName, content) {
|
|
38
|
+
if (fileName.endsWith('.json')) {
|
|
39
|
+
try {
|
|
40
|
+
return stableStringify(JSON.parse(content));
|
|
41
|
+
} catch (_) {
|
|
42
|
+
return content;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function computeContentDigest(files) {
|
|
49
|
+
const excluded = new Set(['kdna.json', 'signature.json', '.DS_Store']);
|
|
50
|
+
const payload = Object.keys(files)
|
|
51
|
+
.filter(name => !excluded.has(name))
|
|
52
|
+
.filter(name => !name.startsWith('reports/') && name !== 'build-receipt.json')
|
|
53
|
+
.sort()
|
|
54
|
+
.map(name => `${name}\n${canonicalEntryBytes(name, files[name])}`)
|
|
55
|
+
.join('\n---entry---\n');
|
|
56
|
+
return `sha256:${crypto.createHash('sha256').update(payload).digest('hex')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function domainIdFromName(name = 'domain') {
|
|
60
|
+
const base = String(name).includes('/') ? String(name).split('/').pop() : String(name);
|
|
61
|
+
const normalized = base.toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
62
|
+
return /^[a-z]/.test(normalized) ? normalized : `domain_${normalized || 'untitled'}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildAssetIdentity(project, files, options = {}) {
|
|
66
|
+
const domainId = project.domain_id || domainIdFromName(project.name);
|
|
67
|
+
const registryName = project.registry_name || (String(project.name || '').startsWith('@') ? project.name : null);
|
|
68
|
+
return {
|
|
69
|
+
asset_uid: options.asset_uid || project.asset_uid || uuidv7(),
|
|
70
|
+
project_uid: options.project_uid || project.project_uid || project.project_id || uuidv7(),
|
|
71
|
+
build_id: options.build_id || `build_${uuidv7()}`,
|
|
72
|
+
domain_id: domainId,
|
|
73
|
+
registry_name: registryName,
|
|
74
|
+
version: (project.release && project.release.version) || '0.1.0',
|
|
75
|
+
judgment_version: (project.release && project.release.judgment_version) || (project.release && project.release.version) || '0.1.0',
|
|
76
|
+
content_digest: options.content_digest || computeContentDigest(files),
|
|
77
|
+
compiled_at: options.compiled_at || new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeMeta(project) {
|
|
82
|
+
return {
|
|
83
|
+
version: (project.release && project.release.version) || '0.1.0',
|
|
84
|
+
domain: project.name,
|
|
85
|
+
created: project.created || new Date().toISOString().slice(0, 10),
|
|
86
|
+
purpose: project.release?.description || `Domain judgment for ${project.name}`,
|
|
87
|
+
load_condition: 'Load when the task matches applies_when on domain axioms.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compileCore(cards, project) {
|
|
92
|
+
const lockedAxioms = cards
|
|
93
|
+
.filter(c => c.type === 'axiom' && c.locked)
|
|
94
|
+
.map(c => ({ id: c.id, ...c.fields, status: c.status, human_lock: c.human_lock }));
|
|
95
|
+
const lockedOntology = cards.filter(c => c.type === 'ontology' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
96
|
+
const lockedBoundaries = cards.filter(c => c.type === 'boundary' && c.locked);
|
|
97
|
+
const lockedRisks = cards.filter(c => c.type === 'risk' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
meta: makeMeta(project),
|
|
101
|
+
axioms: lockedAxioms,
|
|
102
|
+
ontology: lockedOntology,
|
|
103
|
+
frameworks: [],
|
|
104
|
+
stances: [],
|
|
105
|
+
core_structure: [],
|
|
106
|
+
boundaries: lockedBoundaries.map(c => ({
|
|
107
|
+
id: c.id,
|
|
108
|
+
scope: c.fields?.scope || '',
|
|
109
|
+
out_of_scope: c.fields?.out_of_scope || '',
|
|
110
|
+
acceptable_exceptions: c.fields?.acceptable_exceptions || [],
|
|
111
|
+
})),
|
|
112
|
+
risks: lockedRisks,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compilePatterns(cards, project) {
|
|
117
|
+
const lockedMisunderstandings = cards.filter(c => c.type === 'misunderstanding' && c.locked).map(c => ({
|
|
118
|
+
id: c.id,
|
|
119
|
+
wrong: c.fields?.wrong || '',
|
|
120
|
+
correct: c.fields?.correct || '',
|
|
121
|
+
key_distinction: c.fields?.key_distinction || '',
|
|
122
|
+
why: c.fields?.why || `What bad judgment results from believing "${(c.fields?.wrong || '').slice(0, 40)}"`,
|
|
123
|
+
failure_risk: c.fields?.failure_risk || 'No specific failure risk declared',
|
|
124
|
+
applies_when: c.fields?.applies_when || [],
|
|
125
|
+
does_not_apply_when: c.fields?.does_not_apply_when || [],
|
|
126
|
+
}));
|
|
127
|
+
const lockedSelfChecks = cards.filter(c => c.type === 'self_check' && c.locked).map(c => c.fields?.question || '');
|
|
128
|
+
const lockedAesthetics = cards.filter(c => c.type === 'aesthetic' && c.locked).map(c => ({ id: c.id, ...c.fields }));
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
meta: makeMeta(project),
|
|
132
|
+
terminology: {
|
|
133
|
+
standard_terms: [],
|
|
134
|
+
banned_terms: [],
|
|
135
|
+
},
|
|
136
|
+
misunderstandings: lockedMisunderstandings,
|
|
137
|
+
self_check: lockedSelfChecks,
|
|
138
|
+
aesthetics: lockedAesthetics,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function compileScenarios(cards, project) {
|
|
143
|
+
const locked = cards.filter(c => c.type === 'scenario' && c.locked);
|
|
144
|
+
if (locked.length === 0) return null;
|
|
145
|
+
return {
|
|
146
|
+
meta: makeMeta(project),
|
|
147
|
+
scenes: locked.map(c => ({ id: c.id, ...c.fields })),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function compileCases(cards, project) {
|
|
152
|
+
const locked = cards.filter(c => c.type === 'case' && c.locked);
|
|
153
|
+
if (locked.length === 0) return null;
|
|
154
|
+
return {
|
|
155
|
+
meta: makeMeta(project),
|
|
156
|
+
cases: locked.map(c => ({ id: c.id, ...c.fields })),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function compileReasoning(cards, project) {
|
|
161
|
+
const lockedAxioms = cards.filter(c => c.type === 'axiom' && c.locked);
|
|
162
|
+
if (lockedAxioms.length === 0) return null;
|
|
163
|
+
return {
|
|
164
|
+
meta: makeMeta(project),
|
|
165
|
+
reasoning_chains: lockedAxioms.map(ax => ({
|
|
166
|
+
id: `chain_${ax.id}`,
|
|
167
|
+
one_sentence: ax.fields?.one_sentence || '',
|
|
168
|
+
logic: [ax.fields?.full_statement || ''],
|
|
169
|
+
so_what: ax.fields?.why || 'Agent judgment changes when this axiom is loaded.',
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function compileEvolution(cards, project) {
|
|
175
|
+
const lockedCards = cards.filter(c => c.locked);
|
|
176
|
+
if (lockedCards.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
const stages = [];
|
|
179
|
+
const seenAxioms = new Set();
|
|
180
|
+
for (const card of lockedCards) {
|
|
181
|
+
if (seenAxioms.has(card.id)) continue;
|
|
182
|
+
seenAxioms.add(card.id);
|
|
183
|
+
for (const entry of (card.audit_log || [])) {
|
|
184
|
+
if (entry.event === 'locked') {
|
|
185
|
+
stages.push({
|
|
186
|
+
id: `stage_${card.id}`,
|
|
187
|
+
name: card.fields?.one_sentence || card.fields?.question || card.id,
|
|
188
|
+
description: `Card ${card.id} was locked by ${entry.by} at ${entry.at}. Type: ${card.type}.`,
|
|
189
|
+
indicators: [`${card.type} card locked`, 'Human judgment confirmed'],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
meta: makeMeta(project),
|
|
197
|
+
stages: stages.sort((a, b) => a.id.localeCompare(b.id)),
|
|
198
|
+
evolution_layers: [
|
|
199
|
+
{ id: 'layer_1', name: 'Foundation', capability: 'Core axioms and patterns established.', from_stage: stages[0]?.id || 'none', to_stage: stages[stages.length - 1]?.id || 'none' },
|
|
200
|
+
],
|
|
201
|
+
measurement: [
|
|
202
|
+
{ id: 'meas_axioms', what: 'locked_axioms', how: 'Count of locked axiom cards', threshold: `${lockedCards.filter(c => c.type === 'axiom').length}` },
|
|
203
|
+
{ id: 'meas_misunderstandings', what: 'locked_misunderstandings', how: 'Count of locked misunderstanding cards', threshold: `${lockedCards.filter(c => c.type === 'misunderstanding').length}` },
|
|
204
|
+
{ id: 'meas_self_checks', what: 'self_checks', how: 'Count of locked self-check cards', threshold: `${lockedCards.filter(c => c.type === 'self_check').length}` },
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function compileManifest(project, files, identity = null) {
|
|
210
|
+
const kdnaFileCount = Object.keys(files).filter(f => f.startsWith('KDNA_')).length;
|
|
211
|
+
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
212
|
+
const tests = project.tests || [];
|
|
213
|
+
const version = require('../../package.json').version;
|
|
214
|
+
const assetIdentity = identity || buildAssetIdentity(project, files);
|
|
215
|
+
const projectDigest = crypto
|
|
216
|
+
.createHash('sha256')
|
|
217
|
+
.update(JSON.stringify({
|
|
218
|
+
project_id: project.project_id,
|
|
219
|
+
name: project.name,
|
|
220
|
+
cards: lockedCards.map(c => ({ id: c.id, type: c.type, fields: c.fields, human_lock: c.human_lock })),
|
|
221
|
+
}))
|
|
222
|
+
.digest('hex');
|
|
223
|
+
const manifest = {
|
|
224
|
+
format: 'kdna',
|
|
225
|
+
format_version: '1.0',
|
|
226
|
+
spec_version: '1.0-rc',
|
|
227
|
+
name: project.name,
|
|
228
|
+
domain_id: assetIdentity.domain_id,
|
|
229
|
+
asset_uid: assetIdentity.asset_uid,
|
|
230
|
+
project_uid: assetIdentity.project_uid,
|
|
231
|
+
build_id: assetIdentity.build_id,
|
|
232
|
+
version: assetIdentity.version,
|
|
233
|
+
judgment_version: assetIdentity.judgment_version,
|
|
234
|
+
content_digest: assetIdentity.content_digest,
|
|
235
|
+
status: (project.release && project.release.status) || 'experimental',
|
|
236
|
+
quality_badge: tests.filter(t => t.result === 'with_kdna_better').length >= 10 ? 'tested' : 'untested',
|
|
237
|
+
access: (project.release && project.release.access) || 'open',
|
|
238
|
+
languages: project.languages || ['en'],
|
|
239
|
+
default_language: project.default_language || 'en',
|
|
240
|
+
author: project.author || { name: '', id: '' },
|
|
241
|
+
license: project.license || { type: 'CC-BY-4.0' },
|
|
242
|
+
description: project.release?.description || project.name,
|
|
243
|
+
file_count: kdnaFileCount,
|
|
244
|
+
authoring: {
|
|
245
|
+
created_by: 'kdna-studio-sdk',
|
|
246
|
+
authoring_tool: 'KDNA Studio Core',
|
|
247
|
+
authoring_tool_version: version,
|
|
248
|
+
compiler: '@aikdna/kdna-studio-core',
|
|
249
|
+
compiler_version: version,
|
|
250
|
+
asset_uid: assetIdentity.asset_uid,
|
|
251
|
+
project_uid: assetIdentity.project_uid,
|
|
252
|
+
build_id: assetIdentity.build_id,
|
|
253
|
+
domain_id: assetIdentity.domain_id,
|
|
254
|
+
content_digest: assetIdentity.content_digest,
|
|
255
|
+
studio_project_digest: `sha256:${projectDigest}`,
|
|
256
|
+
human_lock_required: true,
|
|
257
|
+
human_lock_count: lockedCards.length,
|
|
258
|
+
ai_assisted: (project.cards || []).some(c => c.history?.some(h => h.by === 'ai')),
|
|
259
|
+
human_confirmed: lockedCards.length > 0,
|
|
260
|
+
compiled_at: assetIdentity.compiled_at,
|
|
261
|
+
},
|
|
262
|
+
created: project.created || new Date().toISOString().slice(0, 10),
|
|
263
|
+
updated: project.updated || new Date().toISOString().slice(0, 10),
|
|
264
|
+
};
|
|
265
|
+
if (assetIdentity.registry_name) {
|
|
266
|
+
manifest.registry_name = assetIdentity.registry_name;
|
|
267
|
+
manifest.authoring.registry_name = assetIdentity.registry_name;
|
|
268
|
+
}
|
|
269
|
+
return manifest;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildReports(project, files, identity, provenance, stats) {
|
|
273
|
+
const cards = project.cards || [];
|
|
274
|
+
const lockedCards = cards.filter(c => c.locked);
|
|
275
|
+
const judgmentCards = cards.filter(c => ['axiom', 'ontology', 'misunderstanding', 'self_check', 'boundary', 'risk', 'aesthetic', 'scenario', 'case'].includes(c.type));
|
|
276
|
+
const tests = project.tests || [];
|
|
277
|
+
const ratedTests = tests.filter(t => t.result);
|
|
278
|
+
const qualityBadge = tests.filter(t => t.result === 'with_kdna_better').length >= 10 ? 'tested' : 'untested';
|
|
279
|
+
const packageVersion = require('../../package.json').version;
|
|
280
|
+
|
|
281
|
+
const buildReport = {
|
|
282
|
+
schema_version: 'studio-build-report-v1',
|
|
283
|
+
build_id: identity.build_id,
|
|
284
|
+
asset_uid: identity.asset_uid,
|
|
285
|
+
project_uid: identity.project_uid,
|
|
286
|
+
domain_id: identity.domain_id,
|
|
287
|
+
registry_name: identity.registry_name,
|
|
288
|
+
compiler: '@aikdna/kdna-studio-core',
|
|
289
|
+
compiler_version: packageVersion,
|
|
290
|
+
compiled_at: identity.compiled_at,
|
|
291
|
+
content_digest: identity.content_digest,
|
|
292
|
+
stats,
|
|
293
|
+
validations: {
|
|
294
|
+
schema_validation: 'required_before export',
|
|
295
|
+
cross_file_validation: 'required_before export',
|
|
296
|
+
id_uniqueness: 'required_before export',
|
|
297
|
+
language_version_consistency: 'required_before export',
|
|
298
|
+
},
|
|
299
|
+
outputs: Object.keys(files).sort(),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const humanLockReport = {
|
|
303
|
+
schema_version: 'human-lock-report-v1',
|
|
304
|
+
build_id: identity.build_id,
|
|
305
|
+
human_lock_required: true,
|
|
306
|
+
human_lock_count: lockedCards.length,
|
|
307
|
+
judgment_card_count: judgmentCards.length,
|
|
308
|
+
unlocked_judgment_card_count: judgmentCards.filter(c => !c.locked).length,
|
|
309
|
+
cards: lockedCards.map(c => ({
|
|
310
|
+
id: c.id,
|
|
311
|
+
type: c.type,
|
|
312
|
+
locked: true,
|
|
313
|
+
locked_by: c.human_lock?.by || null,
|
|
314
|
+
locked_at: c.human_lock?.at || null,
|
|
315
|
+
judgment_fingerprint: c.human_lock?.judgment_fingerprint || null,
|
|
316
|
+
})),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const qualityGateReport = {
|
|
320
|
+
schema_version: 'quality-gate-report-v1',
|
|
321
|
+
build_id: identity.build_id,
|
|
322
|
+
quality_badge: qualityBadge,
|
|
323
|
+
eval_count: tests.length,
|
|
324
|
+
rated_eval_count: ratedTests.length,
|
|
325
|
+
gates: {
|
|
326
|
+
untested: {
|
|
327
|
+
passed: true,
|
|
328
|
+
evidence: ['schema-compatible compile output', 'provenance report', 'human-lock report'],
|
|
329
|
+
},
|
|
330
|
+
tested: {
|
|
331
|
+
passed: qualityBadge === 'tested',
|
|
332
|
+
required: '>=10 eval cases where KDNA improves judgment with manual verification',
|
|
333
|
+
},
|
|
334
|
+
validated: {
|
|
335
|
+
passed: false,
|
|
336
|
+
required: 'automated scoring, raw outputs, and registry validation',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const evalReport = {
|
|
342
|
+
schema_version: 'eval-report-v1',
|
|
343
|
+
build_id: identity.build_id,
|
|
344
|
+
total: tests.length,
|
|
345
|
+
rated: ratedTests.length,
|
|
346
|
+
cases: tests.map(t => ({
|
|
347
|
+
id: t.id || null,
|
|
348
|
+
title: t.title || t.name || null,
|
|
349
|
+
result: t.result || null,
|
|
350
|
+
linked_cards: t.linked_cards || [],
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const buildReceipt = {
|
|
355
|
+
schema_version: 'studio-build-receipt-v1',
|
|
356
|
+
asset_uid: identity.asset_uid,
|
|
357
|
+
project_uid: identity.project_uid,
|
|
358
|
+
build_id: identity.build_id,
|
|
359
|
+
domain_id: identity.domain_id,
|
|
360
|
+
registry_name: identity.registry_name,
|
|
361
|
+
version: identity.version,
|
|
362
|
+
judgment_version: identity.judgment_version,
|
|
363
|
+
content_digest: identity.content_digest,
|
|
364
|
+
asset_digest: null,
|
|
365
|
+
compiler: '@aikdna/kdna-studio-core',
|
|
366
|
+
compiler_version: packageVersion,
|
|
367
|
+
signature_status: 'pending_export_sign',
|
|
368
|
+
encryption_profile: project.release?.access === 'licensed' ? 'kdna-licensed-entry-v1' : null,
|
|
369
|
+
built_at: identity.compiled_at,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
'reports/build-report.json': JSON.stringify(buildReport, null, 2),
|
|
374
|
+
'reports/provenance-report.json': JSON.stringify(provenance, null, 2),
|
|
375
|
+
'reports/human-lock-report.json': JSON.stringify(humanLockReport, null, 2),
|
|
376
|
+
'reports/quality-gate-report.json': JSON.stringify(qualityGateReport, null, 2),
|
|
377
|
+
'reports/eval-report.json': JSON.stringify(evalReport, null, 2),
|
|
378
|
+
'build-receipt.json': JSON.stringify(buildReceipt, null, 2),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function compileDomain(project) {
|
|
383
|
+
const cards = project.cards || [];
|
|
384
|
+
const core = compileCore(cards, project);
|
|
385
|
+
const patterns = compilePatterns(cards, project);
|
|
386
|
+
const scenarios = compileScenarios(cards, project);
|
|
387
|
+
const cases = compileCases(cards, project);
|
|
388
|
+
const reasoning = compileReasoning(cards, project);
|
|
389
|
+
const evolution = compileEvolution(cards, project);
|
|
390
|
+
|
|
391
|
+
const files = {};
|
|
392
|
+
files['KDNA_Core.json'] = JSON.stringify(core, null, 2);
|
|
393
|
+
files['KDNA_Patterns.json'] = JSON.stringify(patterns, null, 2);
|
|
394
|
+
if (scenarios) files['KDNA_Scenarios.json'] = JSON.stringify(scenarios, null, 2);
|
|
395
|
+
if (cases) files['KDNA_Cases.json'] = JSON.stringify(cases, null, 2);
|
|
396
|
+
if (reasoning) files['KDNA_Reasoning.json'] = JSON.stringify(reasoning, null, 2);
|
|
397
|
+
if (evolution) files['KDNA_Evolution.json'] = JSON.stringify(evolution, null, 2);
|
|
398
|
+
const identity = buildAssetIdentity(project, files);
|
|
399
|
+
|
|
400
|
+
// ── KDNA Card (governance metadata) ─────────────────────────────
|
|
401
|
+
const provenance = require('../provenance').buildProvenance(project, files, identity);
|
|
402
|
+
if (project.governance) {
|
|
403
|
+
const { generateKdnaCard } = require('../governance');
|
|
404
|
+
const kdnaCard = generateKdnaCard(project, {}, provenance);
|
|
405
|
+
files['KDNA_CARD.json'] = JSON.stringify(kdnaCard, null, 2);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const excludedCount = cards.filter(c => !c.locked && !['deprecated'].includes(c.status)).length;
|
|
409
|
+
const stats = {
|
|
410
|
+
total_cards: cards.length,
|
|
411
|
+
locked_cards: cards.filter(c => c.locked).length,
|
|
412
|
+
excluded_cards: excludedCount,
|
|
413
|
+
deprecated_cards: cards.filter(c => c.status === 'deprecated').length,
|
|
414
|
+
kdna_files: Object.keys(files).filter(f => f.startsWith('KDNA_')).length,
|
|
415
|
+
total_files: Object.keys(files).length,
|
|
416
|
+
};
|
|
417
|
+
Object.assign(files, buildReports(project, files, identity, provenance, stats));
|
|
418
|
+
identity.content_digest = computeContentDigest(files);
|
|
419
|
+
provenance.content_digest = identity.content_digest;
|
|
420
|
+
provenance.content_fingerprint = identity.content_digest;
|
|
421
|
+
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
422
|
+
files['kdna.json'] = JSON.stringify(compileManifest(project, files, identity), null, 2);
|
|
423
|
+
stats.total_files = Object.keys(files).length;
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
files,
|
|
427
|
+
stats,
|
|
428
|
+
identity,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function generateReadme(project, options = {}) {
|
|
433
|
+
const cards = project.cards || [];
|
|
434
|
+
const locked = cards.filter(c => c.locked);
|
|
435
|
+
const lockedAxioms = locked.filter(c => c.type === 'axiom');
|
|
436
|
+
const lockedMisunderstandings = locked.filter(c => c.type === 'misunderstanding');
|
|
437
|
+
const lockedSelfChecks = locked.filter(c => c.type === 'self_check');
|
|
438
|
+
const lockedBoundaries = locked.filter(c => c.type === 'boundary');
|
|
439
|
+
const tests = project.tests || [];
|
|
440
|
+
|
|
441
|
+
const lines = [];
|
|
442
|
+
lines.push(`# ${project.name}`);
|
|
443
|
+
lines.push('');
|
|
444
|
+
if (options.description) { lines.push(options.description); lines.push(''); }
|
|
445
|
+
|
|
446
|
+
lines.push('## Where it comes from');
|
|
447
|
+
lines.push('');
|
|
448
|
+
lines.push(options.origin || `Domain expertise encoded into ${locked.length} judgment cards through structured interview and human lock.`);
|
|
449
|
+
lines.push('');
|
|
450
|
+
|
|
451
|
+
lines.push('## Where it applies');
|
|
452
|
+
lines.push('');
|
|
453
|
+
const appliesWhen = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.applies_when || []))];
|
|
454
|
+
appliesWhen.length ? appliesWhen.forEach(w => lines.push(`- ${w}`)) : lines.push('- As declared in each axiom\'s applies_when field.');
|
|
455
|
+
lines.push('');
|
|
456
|
+
|
|
457
|
+
lines.push('## How it is verified');
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(`- ${tests.length} eval cases (${tests.filter(t => t.result).length} rated)`);
|
|
460
|
+
lines.push(`- ${lockedAxioms.length} locked axioms with applies_when / does_not_apply_when / failure_risk`);
|
|
461
|
+
lines.push(`- ${lockedSelfChecks.length} self-check questions`);
|
|
462
|
+
lines.push(`- ${lockedMisunderstandings.length} misunderstanding patterns`);
|
|
463
|
+
lines.push('');
|
|
464
|
+
|
|
465
|
+
lines.push('## When it does NOT apply');
|
|
466
|
+
lines.push('');
|
|
467
|
+
const notApply = [...new Set(lockedAxioms.flatMap(ax => ax.fields?.does_not_apply_when || []))];
|
|
468
|
+
notApply.forEach(w => lines.push(`- ${w}`));
|
|
469
|
+
for (const oos of lockedBoundaries.flatMap(b => [b.fields?.out_of_scope || '']).filter(Boolean)) {
|
|
470
|
+
if (!notApply.includes(oos)) lines.push(`- ${oos}`);
|
|
471
|
+
}
|
|
472
|
+
lines.push('');
|
|
473
|
+
|
|
474
|
+
if (lockedAxioms.length > 0) {
|
|
475
|
+
lines.push('## Top Axioms'); lines.push('');
|
|
476
|
+
lockedAxioms.forEach(ax => {
|
|
477
|
+
lines.push(`- **${ax.fields?.one_sentence || ax.id}**`);
|
|
478
|
+
if (ax.fields?.failure_risk) lines.push(` - Failure risk: ${ax.fields.failure_risk}`);
|
|
479
|
+
});
|
|
480
|
+
lines.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (lockedMisunderstandings.length > 0) {
|
|
484
|
+
lines.push('## Top Misunderstandings'); lines.push('');
|
|
485
|
+
lockedMisunderstandings.forEach(ms => {
|
|
486
|
+
lines.push(`- WRONG: ${ms.fields?.wrong}`);
|
|
487
|
+
lines.push(` CORRECT: ${ms.fields?.correct}`);
|
|
488
|
+
});
|
|
489
|
+
lines.push('');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (lockedSelfChecks.length > 0) {
|
|
493
|
+
lines.push('## Eval Score'); lines.push('');
|
|
494
|
+
lines.push(`- quality_badge: ${tests.filter(t => t.result === 'with_kdna_better').length >= 5 ? 'tested' : 'untested'}`);
|
|
495
|
+
lines.push(`- eval cases: ${tests.length}`);
|
|
496
|
+
lines.push('');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
lines.push('## Files'); lines.push('');
|
|
500
|
+
const kdnaFileCount = 2
|
|
501
|
+
+ (cards.filter(c => c.type === 'scenario' && c.locked).length > 0 ? 1 : 0)
|
|
502
|
+
+ (cards.filter(c => c.type === 'case' && c.locked).length > 0 ? 1 : 0)
|
|
503
|
+
+ (lockedAxioms.length > 0 ? 1 : 0)
|
|
504
|
+
+ (locked.length > 0 ? 1 : 0);
|
|
505
|
+
lines.push(`${kdnaFileCount} KDNA JSON files + evals/ + demo/`);
|
|
506
|
+
lines.push('');
|
|
507
|
+
|
|
508
|
+
return lines.join('\n');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
module.exports = { compileDomain, compileCore, compilePatterns, compileScenarios, compileCases, compileReasoning, compileEvolution, compileManifest, generateReadme, buildAssetIdentity, computeContentDigest };
|
|
@@ -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_${crypto.randomUUID()}`,
|
|
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
|
+
};
|