@llm-translate/cli 1.0.0-next.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.
Files changed (157) hide show
  1. package/.dockerignore +51 -0
  2. package/.env.example +33 -0
  3. package/.github/workflows/docs-pages.yml +57 -0
  4. package/.github/workflows/release.yml +49 -0
  5. package/.translaterc.json +44 -0
  6. package/CLAUDE.md +243 -0
  7. package/Dockerfile +55 -0
  8. package/README.md +371 -0
  9. package/RFC.md +1595 -0
  10. package/dist/cli/index.d.ts +2 -0
  11. package/dist/cli/index.js +4494 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/index.d.ts +1152 -0
  14. package/dist/index.js +3841 -0
  15. package/dist/index.js.map +1 -0
  16. package/docker-compose.yml +56 -0
  17. package/docs/.vitepress/config.ts +161 -0
  18. package/docs/api/agent.md +262 -0
  19. package/docs/api/engine.md +274 -0
  20. package/docs/api/index.md +171 -0
  21. package/docs/api/providers.md +304 -0
  22. package/docs/changelog.md +64 -0
  23. package/docs/cli/dir.md +243 -0
  24. package/docs/cli/file.md +213 -0
  25. package/docs/cli/glossary.md +273 -0
  26. package/docs/cli/index.md +129 -0
  27. package/docs/cli/init.md +158 -0
  28. package/docs/cli/serve.md +211 -0
  29. package/docs/glossary.json +235 -0
  30. package/docs/guide/chunking.md +272 -0
  31. package/docs/guide/configuration.md +139 -0
  32. package/docs/guide/cost-optimization.md +237 -0
  33. package/docs/guide/docker.md +371 -0
  34. package/docs/guide/getting-started.md +150 -0
  35. package/docs/guide/glossary.md +241 -0
  36. package/docs/guide/index.md +86 -0
  37. package/docs/guide/ollama.md +515 -0
  38. package/docs/guide/prompt-caching.md +221 -0
  39. package/docs/guide/providers.md +232 -0
  40. package/docs/guide/quality-control.md +206 -0
  41. package/docs/guide/vitepress-integration.md +265 -0
  42. package/docs/index.md +63 -0
  43. package/docs/ja/api/agent.md +262 -0
  44. package/docs/ja/api/engine.md +274 -0
  45. package/docs/ja/api/index.md +171 -0
  46. package/docs/ja/api/providers.md +304 -0
  47. package/docs/ja/changelog.md +64 -0
  48. package/docs/ja/cli/dir.md +243 -0
  49. package/docs/ja/cli/file.md +213 -0
  50. package/docs/ja/cli/glossary.md +273 -0
  51. package/docs/ja/cli/index.md +111 -0
  52. package/docs/ja/cli/init.md +158 -0
  53. package/docs/ja/guide/chunking.md +271 -0
  54. package/docs/ja/guide/configuration.md +139 -0
  55. package/docs/ja/guide/cost-optimization.md +30 -0
  56. package/docs/ja/guide/getting-started.md +150 -0
  57. package/docs/ja/guide/glossary.md +214 -0
  58. package/docs/ja/guide/index.md +32 -0
  59. package/docs/ja/guide/ollama.md +410 -0
  60. package/docs/ja/guide/prompt-caching.md +221 -0
  61. package/docs/ja/guide/providers.md +232 -0
  62. package/docs/ja/guide/quality-control.md +137 -0
  63. package/docs/ja/guide/vitepress-integration.md +265 -0
  64. package/docs/ja/index.md +58 -0
  65. package/docs/ko/api/agent.md +262 -0
  66. package/docs/ko/api/engine.md +274 -0
  67. package/docs/ko/api/index.md +171 -0
  68. package/docs/ko/api/providers.md +304 -0
  69. package/docs/ko/changelog.md +64 -0
  70. package/docs/ko/cli/dir.md +243 -0
  71. package/docs/ko/cli/file.md +213 -0
  72. package/docs/ko/cli/glossary.md +273 -0
  73. package/docs/ko/cli/index.md +111 -0
  74. package/docs/ko/cli/init.md +158 -0
  75. package/docs/ko/guide/chunking.md +271 -0
  76. package/docs/ko/guide/configuration.md +139 -0
  77. package/docs/ko/guide/cost-optimization.md +30 -0
  78. package/docs/ko/guide/getting-started.md +150 -0
  79. package/docs/ko/guide/glossary.md +214 -0
  80. package/docs/ko/guide/index.md +32 -0
  81. package/docs/ko/guide/ollama.md +410 -0
  82. package/docs/ko/guide/prompt-caching.md +221 -0
  83. package/docs/ko/guide/providers.md +232 -0
  84. package/docs/ko/guide/quality-control.md +137 -0
  85. package/docs/ko/guide/vitepress-integration.md +265 -0
  86. package/docs/ko/index.md +58 -0
  87. package/docs/zh/api/agent.md +262 -0
  88. package/docs/zh/api/engine.md +274 -0
  89. package/docs/zh/api/index.md +171 -0
  90. package/docs/zh/api/providers.md +304 -0
  91. package/docs/zh/changelog.md +64 -0
  92. package/docs/zh/cli/dir.md +243 -0
  93. package/docs/zh/cli/file.md +213 -0
  94. package/docs/zh/cli/glossary.md +273 -0
  95. package/docs/zh/cli/index.md +111 -0
  96. package/docs/zh/cli/init.md +158 -0
  97. package/docs/zh/guide/chunking.md +271 -0
  98. package/docs/zh/guide/configuration.md +139 -0
  99. package/docs/zh/guide/cost-optimization.md +30 -0
  100. package/docs/zh/guide/getting-started.md +150 -0
  101. package/docs/zh/guide/glossary.md +214 -0
  102. package/docs/zh/guide/index.md +32 -0
  103. package/docs/zh/guide/ollama.md +410 -0
  104. package/docs/zh/guide/prompt-caching.md +221 -0
  105. package/docs/zh/guide/providers.md +232 -0
  106. package/docs/zh/guide/quality-control.md +137 -0
  107. package/docs/zh/guide/vitepress-integration.md +265 -0
  108. package/docs/zh/index.md +58 -0
  109. package/package.json +91 -0
  110. package/release.config.mjs +15 -0
  111. package/schemas/glossary.schema.json +110 -0
  112. package/src/cli/commands/dir.ts +469 -0
  113. package/src/cli/commands/file.ts +291 -0
  114. package/src/cli/commands/glossary.ts +221 -0
  115. package/src/cli/commands/init.ts +68 -0
  116. package/src/cli/commands/serve.ts +60 -0
  117. package/src/cli/index.ts +64 -0
  118. package/src/cli/options.ts +59 -0
  119. package/src/core/agent.ts +1119 -0
  120. package/src/core/chunker.ts +391 -0
  121. package/src/core/engine.ts +634 -0
  122. package/src/errors.ts +188 -0
  123. package/src/index.ts +147 -0
  124. package/src/integrations/vitepress.ts +549 -0
  125. package/src/parsers/markdown.ts +383 -0
  126. package/src/providers/claude.ts +259 -0
  127. package/src/providers/interface.ts +109 -0
  128. package/src/providers/ollama.ts +379 -0
  129. package/src/providers/openai.ts +308 -0
  130. package/src/providers/registry.ts +153 -0
  131. package/src/server/index.ts +152 -0
  132. package/src/server/middleware/auth.ts +93 -0
  133. package/src/server/middleware/logger.ts +90 -0
  134. package/src/server/routes/health.ts +84 -0
  135. package/src/server/routes/translate.ts +210 -0
  136. package/src/server/types.ts +138 -0
  137. package/src/services/cache.ts +899 -0
  138. package/src/services/config.ts +217 -0
  139. package/src/services/glossary.ts +247 -0
  140. package/src/types/analysis.ts +164 -0
  141. package/src/types/index.ts +265 -0
  142. package/src/types/modes.ts +121 -0
  143. package/src/types/mqm.ts +157 -0
  144. package/src/utils/logger.ts +141 -0
  145. package/src/utils/tokens.ts +116 -0
  146. package/tests/fixtures/glossaries/ml-glossary.json +53 -0
  147. package/tests/fixtures/input/lynq-installation.ko.md +350 -0
  148. package/tests/fixtures/input/lynq-installation.md +350 -0
  149. package/tests/fixtures/input/simple.ko.md +27 -0
  150. package/tests/fixtures/input/simple.md +27 -0
  151. package/tests/unit/chunker.test.ts +229 -0
  152. package/tests/unit/glossary.test.ts +146 -0
  153. package/tests/unit/markdown.test.ts +205 -0
  154. package/tests/unit/tokens.test.ts +81 -0
  155. package/tsconfig.json +28 -0
  156. package/tsup.config.ts +34 -0
  157. package/vitest.config.ts +16 -0
@@ -0,0 +1,265 @@
1
+ // ============================================================================
2
+ // Configuration Types
3
+ // ============================================================================
4
+
5
+ export type ProviderName = 'claude' | 'openai' | 'ollama' | 'custom';
6
+
7
+ export interface TranslateConfig {
8
+ version: string;
9
+ project?: {
10
+ name: string;
11
+ description: string;
12
+ purpose: string;
13
+ };
14
+ languages: {
15
+ source: string;
16
+ targets: string[];
17
+ /** Per-language style instructions (e.g., { "ko": "경어체", "ja": "です・ます調" }) */
18
+ styles?: Record<string, string>;
19
+ };
20
+ provider: {
21
+ default: ProviderName;
22
+ model?: string;
23
+ fallback?: ProviderName[];
24
+ apiKeys?: Record<ProviderName, string>;
25
+ };
26
+ quality: {
27
+ threshold: number;
28
+ maxIterations: number;
29
+ evaluationMethod: 'llm' | 'embedding' | 'hybrid';
30
+ };
31
+ chunking: {
32
+ maxTokens: number;
33
+ overlapTokens: number;
34
+ preserveStructure: boolean;
35
+ };
36
+ glossary?: {
37
+ path: string;
38
+ strict: boolean;
39
+ };
40
+ paths: {
41
+ output: string;
42
+ cache?: string;
43
+ };
44
+ ignore?: string[];
45
+ }
46
+
47
+ // ============================================================================
48
+ // Glossary Types
49
+ // ============================================================================
50
+
51
+ export interface Glossary {
52
+ metadata: {
53
+ name: string;
54
+ sourceLang: string;
55
+ targetLangs: string[];
56
+ version: string;
57
+ domain?: string;
58
+ };
59
+ terms: GlossaryTerm[];
60
+ }
61
+
62
+ export interface GlossaryTerm {
63
+ source: string;
64
+ targets: Record<string, string>;
65
+ context?: string;
66
+ caseSensitive?: boolean;
67
+ doNotTranslate?: boolean;
68
+ doNotTranslateFor?: string[];
69
+ partOfSpeech?: 'noun' | 'verb' | 'adjective' | 'other';
70
+ notes?: string;
71
+ }
72
+
73
+ export interface ResolvedGlossary {
74
+ metadata: {
75
+ name: string;
76
+ sourceLang: string;
77
+ targetLang: string;
78
+ version: string;
79
+ domain?: string;
80
+ };
81
+ terms: ResolvedGlossaryTerm[];
82
+ }
83
+
84
+ export interface ResolvedGlossaryTerm {
85
+ source: string;
86
+ target: string;
87
+ context?: string;
88
+ caseSensitive: boolean;
89
+ doNotTranslate: boolean;
90
+ }
91
+
92
+ // ============================================================================
93
+ // Translation Types
94
+ // ============================================================================
95
+
96
+ export type DocumentFormat = 'markdown' | 'html' | 'text';
97
+
98
+ export interface TranslationRequest {
99
+ content: string;
100
+ sourceLang: string;
101
+ targetLang: string;
102
+ format: DocumentFormat;
103
+ glossary?: ResolvedGlossary;
104
+ context?: {
105
+ documentPurpose?: string;
106
+ /** Per-language style instruction (e.g., "경어체", "です・ます調") */
107
+ styleInstruction?: string;
108
+ previousChunks?: string[];
109
+ documentSummary?: string;
110
+ };
111
+ options?: {
112
+ qualityThreshold?: number;
113
+ maxIterations?: number;
114
+ preserveFormatting?: boolean;
115
+ };
116
+ }
117
+
118
+ export interface TranslationResult {
119
+ content: string;
120
+ metadata: {
121
+ qualityScore: number;
122
+ qualityThreshold: number;
123
+ thresholdMet: boolean;
124
+ iterations: number;
125
+ tokensUsed: {
126
+ input: number;
127
+ output: number;
128
+ /** Tokens read from cache (90% cost reduction) */
129
+ cacheRead?: number;
130
+ /** Tokens written to cache (25% cost increase for first write) */
131
+ cacheWrite?: number;
132
+ };
133
+ duration: number;
134
+ provider: string;
135
+ model: string;
136
+ };
137
+ glossaryCompliance?: {
138
+ applied: string[];
139
+ missed: string[];
140
+ };
141
+ }
142
+
143
+ export interface ChunkResult {
144
+ original: string;
145
+ translated: string;
146
+ startOffset: number;
147
+ endOffset: number;
148
+ qualityScore: number;
149
+ iterations?: number;
150
+ tokensUsed?: {
151
+ input: number;
152
+ output: number;
153
+ /** Number of cache hits for this chunk */
154
+ cacheRead?: number;
155
+ };
156
+ /** Whether this chunk was retrieved from cache */
157
+ cached?: boolean;
158
+ }
159
+
160
+ export interface DocumentResult {
161
+ content: string;
162
+ chunks: ChunkResult[];
163
+ metadata: {
164
+ totalTokensUsed: number;
165
+ totalDuration: number;
166
+ averageQuality: number;
167
+ provider: string;
168
+ model: string;
169
+ totalIterations: number;
170
+ tokensUsed: {
171
+ input: number;
172
+ output: number;
173
+ /** Tokens read from cache (90% cost reduction) */
174
+ cacheRead?: number;
175
+ /** Tokens written to cache (25% cost increase for first write) */
176
+ cacheWrite?: number;
177
+ };
178
+ /** Cache statistics */
179
+ cache?: {
180
+ hits: number;
181
+ misses: number;
182
+ };
183
+ };
184
+ glossaryCompliance?: {
185
+ applied: string[];
186
+ missed: string[];
187
+ compliant: boolean;
188
+ };
189
+ }
190
+
191
+ // ============================================================================
192
+ // Chunking Types
193
+ // ============================================================================
194
+
195
+ export interface Chunk {
196
+ id: string;
197
+ content: string;
198
+ type: 'translatable' | 'preserve';
199
+ startOffset: number;
200
+ endOffset: number;
201
+ metadata?: {
202
+ headerHierarchy?: string[];
203
+ previousContext?: string;
204
+ };
205
+ }
206
+
207
+ export interface ChunkingConfig {
208
+ maxTokens: number;
209
+ overlapTokens: number;
210
+ separators: string[];
211
+ preservePatterns: RegExp[];
212
+ }
213
+
214
+ // ============================================================================
215
+ // Quality Evaluation Types
216
+ // ============================================================================
217
+
218
+ /**
219
+ * Legacy simple quality evaluation (for fast mode or fallback)
220
+ */
221
+ export interface SimpleQualityEvaluation {
222
+ score: number;
223
+ breakdown: {
224
+ accuracy: number;
225
+ fluency: number;
226
+ glossary: number;
227
+ format: number;
228
+ };
229
+ issues: string[];
230
+ }
231
+
232
+ // Re-export MQM types
233
+ export * from './mqm.js';
234
+
235
+ // Re-export analysis types
236
+ export * from './analysis.js';
237
+
238
+ // Re-export mode types
239
+ export * from './modes.js';
240
+
241
+ /**
242
+ * Combined quality evaluation type (supports both MQM and simple)
243
+ */
244
+ export type QualityEvaluation = SimpleQualityEvaluation;
245
+
246
+ // ============================================================================
247
+ // Cache Types
248
+ // ============================================================================
249
+
250
+ export interface CacheEntry {
251
+ sourceHash: string;
252
+ sourceLang: string;
253
+ targetLang: string;
254
+ glossaryHash: string;
255
+ translation: string;
256
+ qualityScore: number;
257
+ createdAt: string;
258
+ provider: string;
259
+ model: string;
260
+ }
261
+
262
+ export interface CacheIndex {
263
+ version: string;
264
+ entries: Record<string, CacheEntry>;
265
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Translation Mode Configurations
3
+ *
4
+ * Defines preset configurations for different translation quality/speed tradeoffs
5
+ */
6
+
7
+ /**
8
+ * Available translation modes
9
+ */
10
+ export type TranslationMode = 'fast' | 'balanced' | 'quality';
11
+
12
+ /**
13
+ * Configuration for a translation mode
14
+ */
15
+ export interface ModeConfig {
16
+ /** Enable pre-translation analysis (MAPS-style) */
17
+ enableAnalysis: boolean;
18
+
19
+ /** Use MQM-based evaluation instead of simple scoring */
20
+ useMQMEvaluation: boolean;
21
+
22
+ /** Maximum refinement iterations */
23
+ maxIterations: number;
24
+
25
+ /** Quality threshold (0 = skip threshold check) */
26
+ qualityThreshold: number;
27
+ }
28
+
29
+ /**
30
+ * Mode preset configurations
31
+ */
32
+ export const MODE_PRESETS: Record<TranslationMode, ModeConfig> = {
33
+ /**
34
+ * Fast mode: Single pass, no evaluation
35
+ * Best for: Quick drafts, large batches, local models
36
+ * Speed: ~1x (fastest)
37
+ */
38
+ fast: {
39
+ enableAnalysis: false,
40
+ useMQMEvaluation: false,
41
+ maxIterations: 1,
42
+ qualityThreshold: 0, // Skip threshold check
43
+ },
44
+
45
+ /**
46
+ * Balanced mode: TEaR with MQM evaluation
47
+ * Best for: General use, good quality with reasonable speed
48
+ * Speed: ~2-3x
49
+ */
50
+ balanced: {
51
+ enableAnalysis: false,
52
+ useMQMEvaluation: true,
53
+ maxIterations: 2,
54
+ qualityThreshold: 75,
55
+ },
56
+
57
+ /**
58
+ * Quality mode: Full MAPS + TEaR pipeline
59
+ * Best for: Production content, critical documents
60
+ * Speed: ~4-5x
61
+ */
62
+ quality: {
63
+ enableAnalysis: true,
64
+ useMQMEvaluation: true,
65
+ maxIterations: 4,
66
+ qualityThreshold: 85,
67
+ },
68
+ };
69
+
70
+ /**
71
+ * Get mode configuration with optional overrides
72
+ */
73
+ export function getModeConfig(
74
+ mode: TranslationMode,
75
+ overrides?: Partial<ModeConfig>
76
+ ): ModeConfig {
77
+ const preset = MODE_PRESETS[mode];
78
+
79
+ if (!overrides) {
80
+ return preset;
81
+ }
82
+
83
+ return {
84
+ enableAnalysis: overrides.enableAnalysis ?? preset.enableAnalysis,
85
+ useMQMEvaluation: overrides.useMQMEvaluation ?? preset.useMQMEvaluation,
86
+ maxIterations: overrides.maxIterations ?? preset.maxIterations,
87
+ qualityThreshold: overrides.qualityThreshold ?? preset.qualityThreshold,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Determine effective mode from CLI options
93
+ */
94
+ export function resolveMode(options: {
95
+ mode?: TranslationMode;
96
+ quality?: number;
97
+ maxIterations?: number;
98
+ noAnalysis?: boolean;
99
+ noMqm?: boolean;
100
+ }): ModeConfig {
101
+ const baseMode = options.mode ?? 'balanced';
102
+ const preset = MODE_PRESETS[baseMode];
103
+
104
+ return {
105
+ enableAnalysis:
106
+ options.noAnalysis !== undefined
107
+ ? !options.noAnalysis
108
+ : preset.enableAnalysis,
109
+ useMQMEvaluation:
110
+ options.noMqm !== undefined ? !options.noMqm : preset.useMQMEvaluation,
111
+ maxIterations: options.maxIterations ?? preset.maxIterations,
112
+ qualityThreshold: options.quality ?? preset.qualityThreshold,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Check if mode allows skipping evaluation
118
+ */
119
+ export function shouldSkipEvaluation(config: ModeConfig): boolean {
120
+ return config.maxIterations <= 1 && config.qualityThreshold <= 0;
121
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * MQM (Multidimensional Quality Metrics) Types
3
+ * Based on https://themqm.org/ framework used in WMT evaluations
4
+ *
5
+ * Reference: TEaR (Translate, Estimate, Refine) - NAACL 2025
6
+ * https://arxiv.org/abs/2402.16379
7
+ */
8
+
9
+ /**
10
+ * MQM Error Categories
11
+ */
12
+ export type MQMErrorType =
13
+ // Accuracy errors - meaning/content issues
14
+ | 'accuracy/mistranslation' // Incorrect meaning
15
+ | 'accuracy/omission' // Missing content from source
16
+ | 'accuracy/addition' // Extra content not in source
17
+ | 'accuracy/untranslated' // Source text left unchanged
18
+ // Fluency errors - target language issues
19
+ | 'fluency/grammar' // Grammatical errors
20
+ | 'fluency/spelling' // Spelling/typos
21
+ | 'fluency/register' // Inappropriate formality level
22
+ | 'fluency/inconsistency' // Inconsistent terminology
23
+ // Style errors - quality/naturalness issues
24
+ | 'style/awkward' // Unnatural phrasing
25
+ | 'style/unidiomatic'; // Non-native expressions
26
+
27
+ /**
28
+ * MQM Severity Levels
29
+ */
30
+ export type MQMSeverity = 'minor' | 'major' | 'critical';
31
+
32
+ /**
33
+ * MQM Severity Weights for score calculation
34
+ */
35
+ export const MQM_SEVERITY_WEIGHTS: Record<MQMSeverity, number> = {
36
+ minor: 1, // Noticeable but doesn't affect understanding
37
+ major: 5, // Affects understanding or usability
38
+ critical: 25, // Completely wrong or unusable
39
+ };
40
+
41
+ /**
42
+ * Individual MQM error annotation
43
+ */
44
+ export interface MQMError {
45
+ /** Error category */
46
+ type: MQMErrorType;
47
+
48
+ /** Error severity */
49
+ severity: MQMSeverity;
50
+
51
+ /** The affected text in translation */
52
+ span: string;
53
+
54
+ /** Suggested correction */
55
+ suggestion: string;
56
+
57
+ /** Brief reason for the error */
58
+ explanation?: string;
59
+
60
+ /** Corresponding source text (if applicable) */
61
+ sourceSpan?: string;
62
+ }
63
+
64
+ /**
65
+ * MQM evaluation result
66
+ */
67
+ export interface MQMEvaluation {
68
+ /** List of identified errors */
69
+ errors: MQMError[];
70
+
71
+ /** Quality score: 100 - sum(error weights), min 0 */
72
+ score: number;
73
+
74
+ /** Brief overall assessment */
75
+ summary: string;
76
+
77
+ /** Error count breakdown by category */
78
+ breakdown: {
79
+ accuracy: number;
80
+ fluency: number;
81
+ style: number;
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Calculate MQM score from errors
87
+ * Score = max(0, 100 - Σ(error_weight))
88
+ */
89
+ export function calculateMQMScore(errors: MQMError[]): number {
90
+ const totalPenalty = errors.reduce(
91
+ (sum, err) => sum + MQM_SEVERITY_WEIGHTS[err.severity],
92
+ 0
93
+ );
94
+ return Math.max(0, 100 - totalPenalty);
95
+ }
96
+
97
+ /**
98
+ * Calculate error breakdown by category
99
+ */
100
+ export function calculateMQMBreakdown(
101
+ errors: MQMError[]
102
+ ): MQMEvaluation['breakdown'] {
103
+ return {
104
+ accuracy: errors.filter((e) => e.type.startsWith('accuracy/')).length,
105
+ fluency: errors.filter((e) => e.type.startsWith('fluency/')).length,
106
+ style: errors.filter((e) => e.type.startsWith('style/')).length,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Parse MQM evaluation JSON response from LLM
112
+ */
113
+ export function parseMQMResponse(response: string): MQMEvaluation | null {
114
+ try {
115
+ // Extract JSON from response
116
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
117
+ if (!jsonMatch) {
118
+ return null;
119
+ }
120
+
121
+ const parsed = JSON.parse(jsonMatch[0]) as {
122
+ errors?: MQMError[];
123
+ score?: number;
124
+ summary?: string;
125
+ };
126
+
127
+ const errors = parsed.errors ?? [];
128
+ const score = parsed.score ?? calculateMQMScore(errors);
129
+
130
+ return {
131
+ errors,
132
+ score,
133
+ summary: parsed.summary ?? '',
134
+ breakdown: calculateMQMBreakdown(errors),
135
+ };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Format MQM errors for refinement prompt
143
+ */
144
+ export function formatMQMErrorsForPrompt(errors: MQMError[]): string {
145
+ if (errors.length === 0) {
146
+ return 'No errors identified.';
147
+ }
148
+
149
+ return errors
150
+ .map((err, i) => {
151
+ const severity = err.severity.toUpperCase();
152
+ return `${i + 1}. [${severity}] ${err.type}
153
+ Text: "${err.span}"
154
+ Fix: "${err.suggestion}"${err.explanation ? `\n Reason: ${err.explanation}` : ''}`;
155
+ })
156
+ .join('\n\n');
157
+ }
@@ -0,0 +1,141 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ============================================================================
4
+ // Log Levels
5
+ // ============================================================================
6
+
7
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
8
+
9
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
10
+ debug: 0,
11
+ info: 1,
12
+ warn: 2,
13
+ error: 3,
14
+ };
15
+
16
+ // ============================================================================
17
+ // Logger Configuration
18
+ // ============================================================================
19
+
20
+ interface LoggerConfig {
21
+ level: LogLevel;
22
+ quiet: boolean;
23
+ json: boolean;
24
+ }
25
+
26
+ let config: LoggerConfig = {
27
+ level: 'info',
28
+ quiet: false,
29
+ json: false,
30
+ };
31
+
32
+ export function configureLogger(options: Partial<LoggerConfig>): void {
33
+ config = { ...config, ...options };
34
+ }
35
+
36
+ // ============================================================================
37
+ // Logger Implementation
38
+ // ============================================================================
39
+
40
+ function shouldLog(level: LogLevel): boolean {
41
+ if (config.quiet && level !== 'error') {
42
+ return false;
43
+ }
44
+ return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[config.level];
45
+ }
46
+
47
+ function formatMessage(
48
+ level: LogLevel,
49
+ message: string,
50
+ data?: Record<string, unknown>
51
+ ): string {
52
+ if (config.json) {
53
+ return JSON.stringify({
54
+ level,
55
+ message,
56
+ timestamp: new Date().toISOString(),
57
+ ...data,
58
+ });
59
+ }
60
+
61
+ const timestamp = new Date().toISOString().slice(11, 19);
62
+ const prefix = `[${timestamp}]`;
63
+
64
+ switch (level) {
65
+ case 'debug':
66
+ return chalk.gray(`${prefix} ${message}`);
67
+ case 'info':
68
+ return `${prefix} ${message}`;
69
+ case 'warn':
70
+ return chalk.yellow(`${prefix} ⚠ ${message}`);
71
+ case 'error':
72
+ return chalk.red(`${prefix} ✗ ${message}`);
73
+ }
74
+ }
75
+
76
+ export const logger = {
77
+ debug(message: string, data?: Record<string, unknown>): void {
78
+ if (shouldLog('debug')) {
79
+ console.log(formatMessage('debug', message, data));
80
+ }
81
+ },
82
+
83
+ info(message: string, data?: Record<string, unknown>): void {
84
+ if (shouldLog('info')) {
85
+ console.log(formatMessage('info', message, data));
86
+ }
87
+ },
88
+
89
+ warn(message: string, data?: Record<string, unknown>): void {
90
+ if (shouldLog('warn')) {
91
+ console.warn(formatMessage('warn', message, data));
92
+ }
93
+ },
94
+
95
+ error(message: string, data?: Record<string, unknown>): void {
96
+ if (shouldLog('error')) {
97
+ console.error(formatMessage('error', message, data));
98
+ }
99
+ },
100
+
101
+ success(message: string): void {
102
+ if (!config.quiet) {
103
+ console.log(chalk.green(`✓ ${message}`));
104
+ }
105
+ },
106
+
107
+ progress(current: number, total: number, message: string): void {
108
+ if (!config.quiet && !config.json) {
109
+ const percent = Math.round((current / total) * 100);
110
+ const bar = '█'.repeat(Math.round(percent / 5)) + '░'.repeat(20 - Math.round(percent / 5));
111
+ process.stdout.write(`\r[${bar}] ${percent}% ${message}`);
112
+ if (current === total) {
113
+ console.log();
114
+ }
115
+ }
116
+ },
117
+ };
118
+
119
+ // ============================================================================
120
+ // Timing Utilities
121
+ // ============================================================================
122
+
123
+ export function createTimer(): {
124
+ elapsed: () => number;
125
+ format: () => string;
126
+ } {
127
+ const start = performance.now();
128
+
129
+ return {
130
+ elapsed(): number {
131
+ return performance.now() - start;
132
+ },
133
+ format(): string {
134
+ const ms = this.elapsed();
135
+ if (ms < 1000) {
136
+ return `${ms.toFixed(0)}ms`;
137
+ }
138
+ return `${(ms / 1000).toFixed(1)}s`;
139
+ },
140
+ };
141
+ }