@aikdna/studio-core 1.0.0 → 1.2.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 +1 -1
- package/src/i18n/index.js +138 -0
- package/src/index.js +2 -0
- package/src/quality/index.js +13 -1
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
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, item.id);
|
|
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
|
+
let level = 'L0';
|
|
130
|
+
if (totalFields > 0) level = 'L1'; // Assume card/readme localization
|
|
131
|
+
if (totalFields > 5) level = 'L2'; // Key fields covered
|
|
132
|
+
if (totalFields > 15) level = 'L3'; // Full coverage
|
|
133
|
+
if ((project.tests || []).length >= 5) level = 'L4'; // Has evals
|
|
134
|
+
|
|
135
|
+
return { level, coverage: Math.min(100, Math.round(locked.length * 10)), translatable_fields: totalFields };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { createLocaleOverlay, validateLocaleOverlay, applyLocaleOverlay, computeI18nCoverage, VALID_LOCALES };
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const cards = require('./cards');
|
|
|
21
21
|
const compile = require('./compile');
|
|
22
22
|
const evidence = require('./evidence');
|
|
23
23
|
const governance = require('./governance');
|
|
24
|
+
const i18n = require('./i18n');
|
|
24
25
|
const packaging = require('./packaging');
|
|
25
26
|
const pipeline = require('./pipeline');
|
|
26
27
|
const project = require('./project');
|
|
@@ -42,6 +43,7 @@ module.exports = {
|
|
|
42
43
|
provenance,
|
|
43
44
|
pipeline,
|
|
44
45
|
governance,
|
|
46
|
+
i18n,
|
|
45
47
|
|
|
46
48
|
// Experimental
|
|
47
49
|
evidence,
|
package/src/quality/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const contradiction = require('./contradiction');
|
|
14
14
|
const { validateAllCards } = require('./validate-cards');
|
|
15
15
|
const { validateGovernance } = require('../governance');
|
|
16
|
+
const { computeI18nCoverage } = require('../i18n');
|
|
16
17
|
|
|
17
18
|
function computeReadiness(project) {
|
|
18
19
|
const cards = project.cards || [];
|
|
@@ -28,6 +29,15 @@ function computeReadiness(project) {
|
|
|
28
29
|
|
|
29
30
|
// ── Governance check (v0.6.1) ───────────────────────────────────
|
|
30
31
|
const govResult = validateGovernance(project);
|
|
32
|
+
|
|
33
|
+
// ── I18N check (v1.2.0) ─────────────────────────────────────────
|
|
34
|
+
const isOfficial = project.name?.startsWith('@aikdna/') || project.release?.official === true;
|
|
35
|
+
const i18nCoverage = computeI18nCoverage(project);
|
|
36
|
+
if (isOfficial && i18nCoverage.level === 'L0') {
|
|
37
|
+
blocking.push('I18N: official domains require at least L1 (KDNA_CARD.json + README in locales/zh-CN/)');
|
|
38
|
+
} else if (isOfficial && i18nCoverage.level === 'L1') {
|
|
39
|
+
warnings.push('I18N: L1 achieved (card + readme). Recommended: L2 overlay for publishable grade.');
|
|
40
|
+
}
|
|
31
41
|
for (const issue of govResult.issues) {
|
|
32
42
|
(issue.severity === 'blocking' ? blocking : warnings).push(`Governance: ${issue.message}`);
|
|
33
43
|
}
|
|
@@ -114,7 +124,7 @@ function computeReadiness(project) {
|
|
|
114
124
|
warnings.push('Governance checks not passed — publishable downgraded to tested');
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
return buildResult(grade, blocking, warnings, project, { feynmanRatio, allFeynman, governance: govResult });
|
|
127
|
+
return buildResult(grade, blocking, warnings, project, { feynmanRatio, allFeynman, governance: govResult, i18n: i18nCoverage });
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
function buildResult(grade, blocking, warnings, project, detail = {}) {
|
|
@@ -128,6 +138,7 @@ function buildResult(grade, blocking, warnings, project, detail = {}) {
|
|
|
128
138
|
warnings,
|
|
129
139
|
score: Math.max(0, 100 - blocking.length * 15 - warnings.length * 3),
|
|
130
140
|
governance: detail.governance || null,
|
|
141
|
+
i18n: detail.i18n || null,
|
|
131
142
|
stats: {
|
|
132
143
|
total_cards: (project.cards || []).length,
|
|
133
144
|
locked_cards: lockedCount,
|
|
@@ -136,6 +147,7 @@ function buildResult(grade, blocking, warnings, project, detail = {}) {
|
|
|
136
147
|
total_tests: (project.tests || []).length,
|
|
137
148
|
rated_tests: ratedTests,
|
|
138
149
|
feynman_ratio: detail.feynmanRatio !== undefined ? Math.round(detail.feynmanRatio * 100) + '%' : 'N/A',
|
|
150
|
+
i18n_level: detail.i18n?.level || 'L0',
|
|
139
151
|
},
|
|
140
152
|
next_step: grade === 'draft_grade' ? 'Lock at least 3 axioms with boundaries and 50% Feynman.' :
|
|
141
153
|
grade === 'human_controlled' ? 'Add 5+ rated evals and 3+ self-checks.' :
|