@aikdna/studio-core 0.7.0 → 1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/studio-core",
3
- "version": "0.7.0",
3
+ "version": "1.1.0",
4
4
  "description": "KDNA Studio Core — pure logic library for authoring, validating, and compiling KDNA domain judgment packages.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -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,