@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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Governance risk assessment — classify domain risk level and validate governance metadata.
3
+ *
4
+ * Risk levels:
5
+ * R0 — Low: inconvenience, not harm
6
+ * R1 — Medium: suboptimal outcomes
7
+ * R2 — High: significant harm possible
8
+ * R3 — Restricted: serious harm, not for public registry
9
+ */
10
+
11
+ const HIGH_RISK_KEYWORDS = {
12
+ medical: ['diagnosis', 'treatment', 'symptom', 'patient', 'clinical', 'therapy', 'medication', 'disease', 'prescription', 'surgery', 'medical'],
13
+ legal: ['lawsuit', 'liability', 'plaintiff', 'defendant', 'jurisdiction', 'statute', 'legal advice', 'attorney', 'court', 'litigation'],
14
+ financial: ['investment', 'portfolio', 'stock', 'bond', 'retirement', 'insurance', 'mortgage', 'loan', 'tax advice', 'credit score', 'financial advice'],
15
+ safety: ['weapon', 'surveillance', 'monitoring', 'tracking', 'child safety', 'emergency response', 'public safety', 'self-harm', 'suicide'],
16
+ decision: ['hiring', 'firing', 'termination', 'employment decision', 'performance review'],
17
+ };
18
+
19
+ function computeRiskLevel(project) {
20
+ const cards = project.cards || [];
21
+
22
+ // Check declared risk level first
23
+ const declared = (project.governance && project.governance.risk_level) || null;
24
+ if (declared === 'R3') return 'R3';
25
+
26
+ // Short-circuit: check each card individually and stop at first high-risk match
27
+ for (const card of cards) {
28
+ const fields = card.fields || {};
29
+ const cardText = [fields.one_sentence, fields.full_statement, fields.wrong, fields.correct, fields.question,
30
+ fields.essence, fields.scope, fields.out_of_scope,
31
+ ...(fields.applies_when || []), ...(fields.does_not_apply_when || [])]
32
+ .filter(Boolean).join(' ').toLowerCase();
33
+
34
+ for (const [category, keywords] of Object.entries(HIGH_RISK_KEYWORDS)) {
35
+ for (const kw of keywords) {
36
+ if (cardText.includes(kw)) {
37
+ if (['medical', 'safety'].includes(category)) return 'R3';
38
+ if (['legal', 'financial'].includes(category)) return 'R2';
39
+ if (category === 'decision') return 'R1';
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ // If no high-risk keywords found and R0-R2 declared, trust the declaration
46
+ if (declared) return declared;
47
+
48
+ return 'R1'; // Default: medium risk
49
+ }
50
+
51
+ function requiresExpertReview(riskLevel) {
52
+ return riskLevel === 'R2' || riskLevel === 'R3';
53
+ }
54
+
55
+ function validateGovernance(project) {
56
+ const issues = [];
57
+ const gov = project.governance || {};
58
+
59
+ // Required fields
60
+ if (!gov.risk_level) {
61
+ issues.push({ type: 'missing_risk_level', severity: 'blocking', message: 'Governance: risk_level must be declared (R0/R1/R2/R3)' });
62
+ }
63
+ if (!gov.intended_use || !gov.intended_use.length) {
64
+ issues.push({ type: 'missing_intended_use', severity: 'blocking', message: 'Governance: intended_use must be declared' });
65
+ }
66
+ if (!gov.out_of_scope || !gov.out_of_scope.length) {
67
+ issues.push({ type: 'missing_out_of_scope', severity: 'blocking', message: 'Governance: out_of_scope must be declared' });
68
+ }
69
+ if (!gov.known_limitations || !gov.known_limitations.length) {
70
+ issues.push({ type: 'missing_limitations', severity: 'blocking', message: 'Governance: known_limitations must be declared' });
71
+ }
72
+
73
+ // Compute risk level once, reuse for both riskLevel and detectedLevel
74
+ const computedLevel = computeRiskLevel(project);
75
+ const riskLevel = gov.risk_level || computedLevel;
76
+
77
+ // Risk level specific checks
78
+ if (requiresExpertReview(riskLevel)) {
79
+ if (!gov.reviewed_by) {
80
+ issues.push({ type: 'requires_expert_review', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires expert_review. reviewed_by must be set.` });
81
+ }
82
+ if (!gov.risk_warnings || !gov.risk_warnings.length) {
83
+ issues.push({ type: 'missing_risk_warnings', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires risk_warnings.` });
84
+ }
85
+ }
86
+
87
+ // Check for high-risk keywords in content that might not match declared level
88
+ const detectedLevel = computedLevel;
89
+ if (gov.risk_level && ['R0', 'R1'].includes(gov.risk_level) && ['R2', 'R3'].includes(detectedLevel)) {
90
+ issues.push({
91
+ type: 'risk_mismatch',
92
+ severity: 'blocking',
93
+ message: `Governance: declared risk_level ${gov.risk_level} but content analysis suggests ${detectedLevel}. Review required.`,
94
+ });
95
+ }
96
+
97
+ // Author responsibility required for R1+
98
+ if (['R1', 'R2', 'R3'].includes(riskLevel) && !gov.author_statement) {
99
+ issues.push({ type: 'missing_author_statement', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires author_statement.` });
100
+ }
101
+
102
+ return {
103
+ valid: issues.filter(i => i.severity === 'blocking').length === 0,
104
+ issues,
105
+ risk_level: riskLevel,
106
+ requires_expert_review: requiresExpertReview(riskLevel),
107
+ };
108
+ }
109
+
110
+ function generateKdnaCard(project, compiledStats, provenance) {
111
+ const gov = project.governance || {};
112
+ const cards = project.cards || [];
113
+ const lockedCards = cards.filter(c => c.locked);
114
+
115
+ return {
116
+ name: project.name,
117
+ version: (project.release && project.release.version) || '0.1.0',
118
+ risk_level: gov.risk_level || computeRiskLevel(project),
119
+ intended_use: gov.intended_use || [],
120
+ out_of_scope: gov.out_of_scope || [],
121
+ known_limitations: gov.known_limitations || [],
122
+ author_responsibility: gov.author_statement || '',
123
+ risk_warnings: gov.risk_warnings || [],
124
+ human_lock_summary: {
125
+ locked_cards: lockedCards.length,
126
+ locked_axioms: lockedCards.filter(c => c.type === 'axiom').length,
127
+ locked_misunderstandings: lockedCards.filter(c => c.type === 'misunderstanding').length,
128
+ locked_self_checks: lockedCards.filter(c => c.type === 'self_check').length,
129
+ feynman_restatements: lockedCards.filter(c => c.feynman_restatement).length,
130
+ locked_by: (project.author && project.author.id) || 'unknown',
131
+ },
132
+ quality_badge: (compiledStats && compiledStats.locked_cards > 0) ? 'tested' : 'untested',
133
+ review_status: gov.review_status || 'community',
134
+ requires_expert_review: requiresExpertReview(gov.risk_level || 'R1'),
135
+ provenance: provenance || {},
136
+ license: (project.release && project.release.license) || 'CC-BY-4.0',
137
+ };
138
+ }
139
+
140
+ module.exports = { computeRiskLevel, requiresExpertReview, validateGovernance, generateKdnaCard, HIGH_RISK_KEYWORDS };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * I18N — Locale overlay creation, validation, and application.
3
+ *
4
+ * KDNA domains encode judgment. Localization changes language, not logic.
5
+ * Overlays translate text fields by referencing canonical IDs.
6
+ * Structural fields MUST NOT be changed by localization.
7
+ */
8
+
9
+ const TEXT_FIELDS = ['one_sentence', 'full_statement', 'why', 'key_distinction', 'wrong', 'correct',
10
+ 'failure_risk', 'essence', 'boundary', 'trigger_signal', 'question', 'scope', 'out_of_scope'];
11
+ const ARRAY_TEXT_FIELDS = ['applies_when', 'does_not_apply_when', 'acceptable_exceptions'];
12
+
13
+ const VALID_LOCALES = ['en', 'zh-CN', 'zh-TW', 'ja', 'ko', 'fr', 'de'];
14
+
15
+ function createLocaleOverlay(project, locale) {
16
+ if (!VALID_LOCALES.includes(locale)) throw new Error(`Invalid locale: ${locale}`);
17
+
18
+ const overlay = { locale, base: 'en', spec_version: '1.0-rc', translations: {} };
19
+ const cards = project.cards || [];
20
+
21
+ for (const card of cards) {
22
+ if (!card.locked) continue;
23
+ const fields = card.fields || {};
24
+
25
+ for (const field of TEXT_FIELDS) {
26
+ if (fields[field] && typeof fields[field] === 'string' && fields[field].length > 3) {
27
+ overlay.translations[`${card.id}.${field}`] = `[TODO: ${locale}] ${fields[field]}`;
28
+ }
29
+ }
30
+ for (const field of ARRAY_TEXT_FIELDS) {
31
+ if (Array.isArray(fields[field])) {
32
+ fields[field].forEach((val, idx) => {
33
+ if (val && typeof val === 'string') {
34
+ overlay.translations[`${card.id}.${field}.${idx}`] = `[TODO: ${locale}] ${val}`;
35
+ }
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ return overlay;
42
+ }
43
+
44
+ function validateLocaleOverlay(project, overlay) {
45
+ const issues = [];
46
+ const cards = project.cards || [];
47
+ const cardIds = new Set(cards.map(c => c.id));
48
+
49
+ if (!overlay.locale) issues.push({ type: 'missing_locale', severity: 'blocking', message: 'Overlay must declare locale' });
50
+ if (!overlay.translations || Object.keys(overlay.translations).length === 0) {
51
+ issues.push({ type: 'empty', severity: 'warning', message: 'Overlay has no translations' });
52
+ return { valid: false, issues };
53
+ }
54
+
55
+ // Validate referenced IDs exist
56
+ for (const key of Object.keys(overlay.translations)) {
57
+ const cardId = key.split('.')[0];
58
+ if (!cardIds.has(cardId)) {
59
+ issues.push({ type: 'unknown_id', severity: 'blocking', message: `Overlay references unknown card: ${cardId}` });
60
+ }
61
+ }
62
+
63
+ // Check for TODO placeholders
64
+ const todoCount = Object.values(overlay.translations).filter(v => v.includes('[TODO:')).length;
65
+ if (todoCount > 0) {
66
+ issues.push({ type: 'incomplete', severity: 'warning', message: `${todoCount} translations still have TODO placeholders` });
67
+ }
68
+
69
+ return { valid: issues.filter(i => i.severity === 'blocking').length === 0, issues };
70
+ }
71
+
72
+ function applyLocaleOverlay(domainFiles, overlay) {
73
+ if (!overlay || !overlay.translations) return domainFiles;
74
+ const localized = { ...domainFiles };
75
+
76
+ for (const [filename, content] of Object.entries(localized)) {
77
+ if (!filename.startsWith('KDNA_')) continue;
78
+ try {
79
+ const data = JSON.parse(content);
80
+ applyOverlayToObject(data, overlay.translations);
81
+ localized[filename] = JSON.stringify(data, null, 2);
82
+ } catch { /* skip non-JSON */ }
83
+ }
84
+
85
+ return localized;
86
+ }
87
+
88
+ function applyOverlayToObject(obj, translations, prefix = '') {
89
+ for (const key of Object.keys(obj)) {
90
+ const fullKey = prefix ? `${prefix}.${key}` : key;
91
+ if (typeof obj[key] === 'string') {
92
+ if (translations[fullKey] && !translations[fullKey].includes('[TODO:')) {
93
+ obj[key] = translations[fullKey];
94
+ }
95
+ } else if (Array.isArray(obj[key])) {
96
+ if (obj[key].every(v => typeof v === 'string')) {
97
+ obj[key] = obj[key].map((v, i) => {
98
+ const arrayKey = `${fullKey}.${i}`;
99
+ return (translations[arrayKey] && !translations[arrayKey].includes('[TODO:')) ? translations[arrayKey] : v;
100
+ });
101
+ } else {
102
+ obj[key].forEach((item, i) => {
103
+ if (typeof item === 'object' && item !== null) {
104
+ applyOverlayToObject(item, translations, `${fullKey}.${i}`);
105
+ } else if (item && item.id) {
106
+ applyOverlayToObject(item, translations, `${fullKey}.${i}`);
107
+ }
108
+ });
109
+ }
110
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
111
+ applyOverlayToObject(obj[key], translations, fullKey);
112
+ }
113
+ }
114
+ }
115
+
116
+ function computeI18nCoverage(project) {
117
+ const cards = project.cards || [];
118
+ const locked = cards.filter(c => c.locked);
119
+ if (locked.length === 0) return { level: 'L0', coverage: 0, translatable_fields: 0 };
120
+
121
+ const totalFields = locked.reduce((sum, c) => {
122
+ const fields = c.fields || {};
123
+ let count = 0;
124
+ for (const f of TEXT_FIELDS) { if (fields[f] && typeof fields[f] === 'string') count++; }
125
+ for (const f of ARRAY_TEXT_FIELDS) { if (Array.isArray(fields[f])) count += fields[f].filter(v => typeof v === 'string').length; }
126
+ return sum + count;
127
+ }, 0);
128
+
129
+ // Check overlay for actual translation completion
130
+ const overlay = project.i18n_overlay || {};
131
+ const translations = overlay.translations || {};
132
+ const translatedCount = Object.values(translations).filter(v => typeof v === 'string' && !v.includes('[TODO:')).length;
133
+
134
+ const coverage = totalFields > 0 ? Math.round((translatedCount / totalFields) * 100) : 0;
135
+
136
+ let level = 'L0';
137
+ if (totalFields > 0) level = 'L1'; // Has translatable content
138
+ if (coverage >= 30) level = 'L2'; // Key fields covered
139
+ if (coverage >= 70) level = 'L3'; // Full coverage
140
+ if (coverage >= 90 && (project.tests || []).length >= 5) level = 'L4'; // Full coverage + evals
141
+
142
+ return { level, coverage: Math.min(100, coverage), translatable_fields: totalFields, translated_fields: translatedCount };
143
+ }
144
+
145
+ module.exports = { createLocaleOverlay, validateLocaleOverlay, applyLocaleOverlay, computeI18nCoverage, VALID_LOCALES };
package/src/index.js ADDED
@@ -0,0 +1,59 @@
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/ runtime CLI adapters for dev diagnostics and verification
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 governance = require('./governance');
24
+ const i18n = require('./i18n');
25
+ const packaging = require('./packaging');
26
+ const pipeline = require('./pipeline');
27
+ const project = require('./project');
28
+ const provenance = require('./provenance');
29
+ const quality = require('./quality');
30
+ const testlab = require('./testlab');
31
+ const versioning = require('./versioning');
32
+ const feynman = require('./cards/feynman');
33
+ const contradiction = require('./quality/contradiction');
34
+ const validateCards = require('./quality/validate-cards');
35
+ const delta = require('./testlab/delta');
36
+
37
+ module.exports = {
38
+ // Stable
39
+ project,
40
+ cards,
41
+ compile,
42
+ quality,
43
+ provenance,
44
+ pipeline,
45
+ governance,
46
+ i18n,
47
+
48
+ // Experimental
49
+ evidence,
50
+ testlab,
51
+ delta,
52
+ feynman,
53
+ contradiction,
54
+ validateCards,
55
+ versioning,
56
+
57
+ // Internal
58
+ packaging,
59
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared judgment field definitions for Human Lock gate and fingerprinting.
3
+ * Used by both cards/index.js and project/index.js to avoid circular deps.
4
+ */
5
+
6
+ const JUDGMENT_CARD_TYPES = new Set(['axiom', 'boundary', 'risk', 'aesthetic']);
7
+
8
+ const JUDGMENT_FIELDS = new Set([
9
+ 'one_sentence', 'full_statement', 'why', 'essence', 'boundary',
10
+ 'wrong', 'correct', 'key_distinction', 'question', 'scope',
11
+ 'out_of_scope', 'applies_when', 'does_not_apply_when', 'failure_risk',
12
+ 'acceptable_exceptions', 'trigger_signal', 'when_to_use', 'steps',
13
+ ]);
14
+
15
+ const crypto = require('crypto');
16
+
17
+ function cardJudgmentFingerprint(card) {
18
+ const fields = card.fields || {};
19
+ const relevant = {};
20
+ for (const key of JUDGMENT_FIELDS) {
21
+ if (key in fields) relevant[key] = fields[key];
22
+ }
23
+ return crypto.createHash('sha256')
24
+ .update(card.type + ':' + JSON.stringify(relevant, Object.keys(relevant).sort()))
25
+ .digest('hex');
26
+ }
27
+
28
+ module.exports = { JUDGMENT_CARD_TYPES, JUDGMENT_FIELDS, cardJudgmentFingerprint };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Runtime CLI adapter for dev-source diagnostics and asset verification.
3
+ *
4
+ * All subprocess calls use execFileSync (not execSync with string interpolation)
5
+ * to prevent command injection. Trusted compile/export is implemented by
6
+ * Studio Core itself and exposed through @aikdna/kdna-studio-cli. kdna-cli is
7
+ * only called here for dev-source diagnostics and runtime verification.
8
+ */
9
+
10
+ const { execFileSync } = require('child_process');
11
+ const path = require('path');
12
+
13
+ function packDomain(domainDir, outputDir = null) {
14
+ return devBundleSource(domainDir, outputDir);
15
+ }
16
+
17
+ function devBundleSource(domainDir, outputDir = null) {
18
+ const args = ['dev', 'pack', domainDir];
19
+ if (outputDir) args.push('--output', outputDir);
20
+ const result = execFileSync('kdna', args, { encoding: 'utf8', timeout: 60000 });
21
+ return {
22
+ success: true,
23
+ trusted: false,
24
+ canonical: false,
25
+ output: result.trim(),
26
+ };
27
+ }
28
+
29
+ function packEncryptedDomain(domainDir, licensePath, outputDir = null) {
30
+ void domainDir;
31
+ void licensePath;
32
+ void outputDir;
33
+ return {
34
+ success: false,
35
+ error: 'Encrypted-extension packaging has been removed. Build a licensed .kdna asset and activate it through the entitlement API.',
36
+ };
37
+ }
38
+
39
+ function verifyDomain(domainPath) {
40
+ try {
41
+ const result = execFileSync('kdna', ['verify', domainPath, '--json'], { encoding: 'utf8', timeout: 30000 });
42
+ return JSON.parse(result);
43
+ } catch (e) {
44
+ const stdout = (e.stdout || '').toString();
45
+ try { return JSON.parse(stdout); } catch { return { error: e.message, stdout }; }
46
+ }
47
+ }
48
+
49
+ function validateDomain(domainPath) {
50
+ try {
51
+ const result = execFileSync('kdna', ['dev', 'validate', domainPath], { encoding: 'utf8', timeout: 30000 });
52
+ return { success: true, output: result.trim() };
53
+ } catch (e) {
54
+ return { success: false, error: e.message, stderr: (e.stderr || '').toString() };
55
+ }
56
+ }
57
+
58
+ function inspectContainer(filePath) {
59
+ try {
60
+ const result = execFileSync('kdna', ['inspect', filePath, '--json'], { encoding: 'utf8', timeout: 15000 });
61
+ return JSON.parse(result);
62
+ } catch { return null; }
63
+ }
64
+
65
+ function signDomain(domainDir) {
66
+ try {
67
+ const result = execFileSync('kdna', ['publish', '--check', domainDir], { encoding: 'utf8', timeout: 30000 });
68
+ return { success: true, output: result.trim() };
69
+ } catch (e) {
70
+ return { success: false, error: (e.stderr || '').toString() || e.message };
71
+ }
72
+ }
73
+
74
+ function generateLicense(domain, issuedTo, savePath = null) {
75
+ const args = ['license', 'generate', domain, '--to', issuedTo];
76
+ if (savePath) args.push('--save', savePath);
77
+ try {
78
+ const result = execFileSync('kdna', args, { encoding: 'utf8', timeout: 15000 });
79
+ return { success: true, output: result.trim() };
80
+ } catch (e) {
81
+ return { success: false, error: (e.stderr || '').toString() || e.message };
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ packDomain, devBundleSource, packEncryptedDomain, verifyDomain, validateDomain,
87
+ inspectContainer, signDomain, generateLicense,
88
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * createStudioPipeline(project, options) — Official convenience API.
3
+ *
4
+ * Provides the recommended Studio workflow as a single callable pipeline.
5
+ * Third-party apps should use this instead of manually calling individual modules.
6
+ *
7
+ * Stable API (semver guaranteed).
8
+ */
9
+
10
+ const { validateProject } = require('./project');
11
+ const { computeReadiness } = require('./quality');
12
+ const { compileDomain, generateReadme } = require('./compile');
13
+ const { buildProvenance } = require('./provenance');
14
+ const { validateAllCards } = require('./quality/validate-cards');
15
+
16
+ function createStudioPipeline(project, options = {}) {
17
+ return new StudioPipeline(project, options);
18
+ }
19
+
20
+ class StudioPipeline {
21
+ constructor(project, options = {}) {
22
+ this.project = project;
23
+ this.options = options;
24
+ this.results = {};
25
+ }
26
+
27
+ validateProject() { this.results.project_valid = validateProject(this.project); return this; }
28
+ validateCards() { const cardIssues = validateAllCards(this.project); this.results.card_validation = { total: cardIssues.length, issues: cardIssues }; return this; }
29
+ computeReadiness() { this.results.readiness = computeReadiness(this.project); return this; }
30
+
31
+ compile() {
32
+ this.results.compile = compileDomain(this.project);
33
+ return this;
34
+ }
35
+
36
+ generateReadme(readmeOptions = {}) {
37
+ this.results.readme = generateReadme(this.project, readmeOptions);
38
+ return this;
39
+ }
40
+
41
+ buildProvenance() {
42
+ if (!this.results.compile) throw new Error('Must call compile() before buildProvenance()');
43
+ this.results.provenance = buildProvenance(this.project, this.results.compile.files);
44
+ return this;
45
+ }
46
+
47
+ runAll(options = {}) {
48
+ this.validateProject();
49
+ this.validateCards();
50
+ this.computeReadiness();
51
+ this.compile();
52
+ if (options.generateReadme !== false) this.generateReadme(options.readmeOptions);
53
+ if (options.buildProvenance !== false) this.buildProvenance();
54
+ return this;
55
+ }
56
+
57
+ // ── Getters ─────────────────────────────────────────────────────
58
+
59
+ /** @deprecated Use .readiness instead */
60
+ get readyness() { return this.results.readiness; }
61
+ get readiness() { return this.results.readiness; }
62
+ get compiled() { return this.results.compile; }
63
+ get kdnaFiles() { return this.results.compile?.files || {}; }
64
+ get isPublishable() { return this.results.readiness?.publishable === true; }
65
+
66
+ // ── Output methods ──────────────────────────────────────────────
67
+
68
+ /** Flat summary for UI display */
69
+ toResult() {
70
+ return {
71
+ project_valid: this.results.project_valid?.valid === true,
72
+ card_issues: this.results.card_validation?.total || 0,
73
+ readiness: this.results.readiness?.grade || 'unknown',
74
+ publishable: this.results.readiness?.publishable || false,
75
+ score: this.results.readiness?.score || 0,
76
+ kdna_files: this.results.compile?.stats?.kdna_files || 0,
77
+ locked_cards: this.results.compile?.stats?.locked_cards || 0,
78
+ excluded_cards: this.results.compile?.stats?.excluded_cards || 0,
79
+ build_id: this.results.provenance?.build_id || null,
80
+ fingerprint: this.results.provenance?.content_fingerprint || null,
81
+ blocking: this.results.readiness?.blocking || [],
82
+ warnings: this.results.readiness?.warnings || [],
83
+ next_step: this.results.readiness?.next_step || '',
84
+ };
85
+ }
86
+
87
+ /** Full artifacts: files + readme + provenance + summary */
88
+ toArtifacts() {
89
+ const result = this.toResult();
90
+ return {
91
+ ...result,
92
+ files: this.results.compile?.files || {},
93
+ readme: this.results.readme || '',
94
+ provenance: this.results.provenance || null,
95
+ readiness_raw: this.results.readiness || null,
96
+ card_validation_raw: this.results.card_validation || null,
97
+ };
98
+ }
99
+ }
100
+
101
+ module.exports = { createStudioPipeline };