musubi-sdd 3.6.1 → 3.7.1
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/bin/musubi-analyze.js +275 -3
- package/bin/musubi-checkpoint.js +396 -0
- package/bin/musubi-convert.js +40 -1
- package/bin/musubi-costs.js +344 -0
- package/bin/musubi-init.js +44 -2
- package/package.json +1 -1
- package/src/converters/index.js +39 -0
- package/src/converters/parsers/openapi-parser.js +350 -0
- package/src/gui/public/index.html +670 -3
- package/src/gui/server.js +195 -0
- package/src/gui/services/index.js +2 -0
- package/src/gui/services/replanning-service.js +276 -0
- package/src/gui/services/traceability-service.js +29 -0
- package/src/llm-providers/index.js +20 -1
- package/src/llm-providers/ollama-provider.js +401 -0
- package/src/managers/checkpoint-manager.js +556 -0
- package/src/managers/index.js +30 -0
- package/src/monitoring/cost-tracker.js +510 -0
- package/src/monitoring/index.js +5 -1
- package/src/templates/index.js +15 -0
- package/src/templates/locale-manager.js +288 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Multi-language Template Manager
|
|
3
|
+
* @module templates/locale-manager
|
|
4
|
+
* @description Manages localized templates for MUSUBI SDD
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Supported locales
|
|
14
|
+
*/
|
|
15
|
+
const SUPPORTED_LOCALES = ['en', 'ja', 'zh', 'ko', 'es', 'de', 'fr'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Template category to directory mapping
|
|
19
|
+
*/
|
|
20
|
+
const TEMPLATE_CATEGORIES = {
|
|
21
|
+
requirements: 'requirements',
|
|
22
|
+
design: 'design',
|
|
23
|
+
tasks: 'tasks',
|
|
24
|
+
adr: 'adr-template',
|
|
25
|
+
research: 'research',
|
|
26
|
+
'workflow-guide': 'workflow-guide',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Locale display names
|
|
31
|
+
*/
|
|
32
|
+
const LOCALE_NAMES = {
|
|
33
|
+
en: 'English',
|
|
34
|
+
ja: '日本語',
|
|
35
|
+
zh: '中文',
|
|
36
|
+
ko: '한국어',
|
|
37
|
+
es: 'Español',
|
|
38
|
+
de: 'Deutsch',
|
|
39
|
+
fr: 'Français',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Multi-language Template Manager
|
|
44
|
+
*/
|
|
45
|
+
class LocaleManager {
|
|
46
|
+
/**
|
|
47
|
+
* Create a LocaleManager instance
|
|
48
|
+
* @param {string} projectPath - Project root path
|
|
49
|
+
* @param {Object} options - Configuration options
|
|
50
|
+
*/
|
|
51
|
+
constructor(projectPath, options = {}) {
|
|
52
|
+
this.projectPath = projectPath;
|
|
53
|
+
this.templatesPath = path.join(projectPath, 'steering', 'templates');
|
|
54
|
+
this.defaultLocale = options.defaultLocale || 'en';
|
|
55
|
+
this.fallbackLocale = options.fallbackLocale || 'en';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get supported locales
|
|
60
|
+
* @returns {string[]} Locale codes
|
|
61
|
+
*/
|
|
62
|
+
getSupportedLocales() {
|
|
63
|
+
return [...SUPPORTED_LOCALES];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get locale display name
|
|
68
|
+
* @param {string} locale - Locale code
|
|
69
|
+
* @returns {string} Display name
|
|
70
|
+
*/
|
|
71
|
+
getLocaleName(locale) {
|
|
72
|
+
return LOCALE_NAMES[locale] || locale;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get template for specific locale
|
|
77
|
+
* @param {string} category - Template category
|
|
78
|
+
* @param {string} locale - Locale code
|
|
79
|
+
* @returns {Promise<string|null>} Template content or null
|
|
80
|
+
*/
|
|
81
|
+
async getTemplate(category, locale = this.defaultLocale) {
|
|
82
|
+
const baseName = TEMPLATE_CATEGORIES[category] || category;
|
|
83
|
+
|
|
84
|
+
// Try exact locale match first
|
|
85
|
+
let templatePath = this.getTemplatePath(baseName, locale);
|
|
86
|
+
if (await fs.pathExists(templatePath)) {
|
|
87
|
+
return fs.readFile(templatePath, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try fallback locale
|
|
91
|
+
if (locale !== this.fallbackLocale) {
|
|
92
|
+
templatePath = this.getTemplatePath(baseName, this.fallbackLocale);
|
|
93
|
+
if (await fs.pathExists(templatePath)) {
|
|
94
|
+
return fs.readFile(templatePath, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Try base template (no locale suffix)
|
|
99
|
+
templatePath = path.join(this.templatesPath, `${baseName}.md`);
|
|
100
|
+
if (await fs.pathExists(templatePath)) {
|
|
101
|
+
return fs.readFile(templatePath, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get template path for locale
|
|
109
|
+
* @param {string} baseName - Template base name
|
|
110
|
+
* @param {string} locale - Locale code
|
|
111
|
+
* @returns {string} Template file path
|
|
112
|
+
*/
|
|
113
|
+
getTemplatePath(baseName, locale) {
|
|
114
|
+
if (locale === 'en') {
|
|
115
|
+
return path.join(this.templatesPath, `${baseName}.md`);
|
|
116
|
+
}
|
|
117
|
+
return path.join(this.templatesPath, `${baseName}.${locale}.md`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* List available templates
|
|
122
|
+
* @returns {Promise<Object>} Map of category -> available locales
|
|
123
|
+
*/
|
|
124
|
+
async listTemplates() {
|
|
125
|
+
const result = {};
|
|
126
|
+
|
|
127
|
+
if (!await fs.pathExists(this.templatesPath)) {
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const files = await fs.readdir(this.templatesPath);
|
|
132
|
+
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
if (!file.endsWith('.md')) continue;
|
|
135
|
+
|
|
136
|
+
const { category, locale } = this.parseTemplateFilename(file);
|
|
137
|
+
if (!category) continue;
|
|
138
|
+
|
|
139
|
+
if (!result[category]) {
|
|
140
|
+
result[category] = [];
|
|
141
|
+
}
|
|
142
|
+
if (!result[category].includes(locale)) {
|
|
143
|
+
result[category].push(locale);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse template filename to extract category and locale
|
|
152
|
+
* @param {string} filename - Template filename
|
|
153
|
+
* @returns {{category: string, locale: string}} Parsed info
|
|
154
|
+
*/
|
|
155
|
+
parseTemplateFilename(filename) {
|
|
156
|
+
const baseName = filename.replace('.md', '');
|
|
157
|
+
|
|
158
|
+
// Check for locale suffix
|
|
159
|
+
for (const locale of SUPPORTED_LOCALES) {
|
|
160
|
+
if (baseName.endsWith(`.${locale}`)) {
|
|
161
|
+
return {
|
|
162
|
+
category: baseName.slice(0, -(locale.length + 1)),
|
|
163
|
+
locale,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// No locale suffix means English
|
|
169
|
+
return {
|
|
170
|
+
category: baseName,
|
|
171
|
+
locale: 'en',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create template for new locale
|
|
177
|
+
* @param {string} category - Template category
|
|
178
|
+
* @param {string} locale - Target locale
|
|
179
|
+
* @param {string} content - Translated content
|
|
180
|
+
* @returns {Promise<string>} Created file path
|
|
181
|
+
*/
|
|
182
|
+
async createLocalizedTemplate(category, locale, content) {
|
|
183
|
+
const baseName = TEMPLATE_CATEGORIES[category] || category;
|
|
184
|
+
const templatePath = this.getTemplatePath(baseName, locale);
|
|
185
|
+
|
|
186
|
+
await fs.ensureDir(path.dirname(templatePath));
|
|
187
|
+
await fs.writeFile(templatePath, content, 'utf-8');
|
|
188
|
+
|
|
189
|
+
return templatePath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get translation suggestions for a template
|
|
194
|
+
* @param {string} content - Template content
|
|
195
|
+
* @param {string} targetLocale - Target locale
|
|
196
|
+
* @returns {Object} Translation metadata
|
|
197
|
+
*/
|
|
198
|
+
getTranslationMetadata(content, targetLocale) {
|
|
199
|
+
// Extract translatable sections
|
|
200
|
+
const sections = [];
|
|
201
|
+
const lines = content.split('\n');
|
|
202
|
+
|
|
203
|
+
let inCodeBlock = false;
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
const line = lines[i];
|
|
207
|
+
|
|
208
|
+
if (line.startsWith('```')) {
|
|
209
|
+
inCodeBlock = !inCodeBlock;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (inCodeBlock) continue;
|
|
214
|
+
|
|
215
|
+
// Headers
|
|
216
|
+
if (line.startsWith('#')) {
|
|
217
|
+
sections.push({
|
|
218
|
+
type: 'header',
|
|
219
|
+
line: i + 1,
|
|
220
|
+
content: line,
|
|
221
|
+
translate: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Paragraphs (non-empty, non-special lines)
|
|
226
|
+
if (line.trim() && !line.startsWith('-') && !line.startsWith('*') && !line.startsWith('|')) {
|
|
227
|
+
sections.push({
|
|
228
|
+
type: 'text',
|
|
229
|
+
line: i + 1,
|
|
230
|
+
content: line,
|
|
231
|
+
translate: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
locale: targetLocale,
|
|
238
|
+
localeName: LOCALE_NAMES[targetLocale] || targetLocale,
|
|
239
|
+
totalLines: lines.length,
|
|
240
|
+
translatableSections: sections.length,
|
|
241
|
+
sections,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Detect locale from project configuration
|
|
247
|
+
* @returns {Promise<string>} Detected locale
|
|
248
|
+
*/
|
|
249
|
+
async detectProjectLocale() {
|
|
250
|
+
// Check project.yml
|
|
251
|
+
const projectPath = path.join(this.projectPath, 'steering', 'project.yml');
|
|
252
|
+
if (await fs.pathExists(projectPath)) {
|
|
253
|
+
const yaml = require('js-yaml');
|
|
254
|
+
const content = await fs.readFile(projectPath, 'utf-8');
|
|
255
|
+
const config = yaml.load(content);
|
|
256
|
+
if (config?.locale) {
|
|
257
|
+
return config.locale;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check steering files for locale hints
|
|
262
|
+
const structurePath = path.join(this.projectPath, 'steering', 'structure.md');
|
|
263
|
+
if (await fs.pathExists(structurePath)) {
|
|
264
|
+
const content = await fs.readFile(structurePath, 'utf-8');
|
|
265
|
+
// Check for Japanese characters
|
|
266
|
+
if (/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]/.test(content)) {
|
|
267
|
+
return 'ja';
|
|
268
|
+
}
|
|
269
|
+
// Check for Chinese characters (without Japanese hiragana/katakana)
|
|
270
|
+
if (/[\u4E00-\u9FFF]/.test(content) && !/[\u3040-\u309F\u30A0-\u30FF]/.test(content)) {
|
|
271
|
+
return 'zh';
|
|
272
|
+
}
|
|
273
|
+
// Check for Korean
|
|
274
|
+
if (/[\uAC00-\uD7AF]/.test(content)) {
|
|
275
|
+
return 'ko';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return this.defaultLocale;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
LocaleManager,
|
|
285
|
+
SUPPORTED_LOCALES,
|
|
286
|
+
LOCALE_NAMES,
|
|
287
|
+
TEMPLATE_CATEGORIES,
|
|
288
|
+
};
|