@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,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
|
+
};
|
package/src/pipeline.js
ADDED
|
@@ -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 };
|