champollion 0.3.3
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 +133 -0
- package/README.md +387 -0
- package/bin/cli.js +278 -0
- package/index.js +135 -0
- package/lib/api-key.js +127 -0
- package/lib/autofix.js +432 -0
- package/lib/bridge/method_bridge.py +430 -0
- package/lib/card-source-resolution.mjs +284 -0
- package/lib/cards/cache.js +169 -0
- package/lib/cards/env.js +82 -0
- package/lib/cards/fetch-card-child.js +38 -0
- package/lib/cards/reader.js +435 -0
- package/lib/cards/refresh.js +111 -0
- package/lib/cards/remote.js +387 -0
- package/lib/cldf-export.mjs +540 -0
- package/lib/cldf-terms.mjs +62 -0
- package/lib/command-help.js +790 -0
- package/lib/commands/audit.js +49 -0
- package/lib/commands/card.js +454 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/fonts.js +489 -0
- package/lib/commands/help.js +91 -0
- package/lib/commands/init.js +1259 -0
- package/lib/commands/integrity.js +148 -0
- package/lib/commands/leaderboard.js +478 -0
- package/lib/commands/lint.js +30 -0
- package/lib/commands/models.js +177 -0
- package/lib/commands/plugin.js +103 -0
- package/lib/commands/provenance.js +45 -0
- package/lib/commands/recommend.js +75 -0
- package/lib/commands/register-corpus.js +678 -0
- package/lib/commands/repair-script.js +42 -0
- package/lib/commands/seal-corpus.js +355 -0
- package/lib/commands/seo.js +72 -0
- package/lib/commands/serve.js +147 -0
- package/lib/commands/status.js +265 -0
- package/lib/commands/submit.js +332 -0
- package/lib/commands/sync.js +89 -0
- package/lib/commands/tm.js +573 -0
- package/lib/commands/verify.js +39 -0
- package/lib/commands/watch.js +20 -0
- package/lib/commands/wrap.js +138 -0
- package/lib/commands/xliff.js +327 -0
- package/lib/commercial-eligibility.js +235 -0
- package/lib/concurrent.js +87 -0
- package/lib/config.js +523 -0
- package/lib/contamination-lane.js +76 -0
- package/lib/content-sync.js +731 -0
- package/lib/content.js +733 -0
- package/lib/corpus-registration.mjs +608 -0
- package/lib/cost-report.js +346 -0
- package/lib/diff.js +155 -0
- package/lib/docusaurus-sync.js +1256 -0
- package/lib/flatten.js +55 -0
- package/lib/format.js +954 -0
- package/lib/hash.js +159 -0
- package/lib/icu.js +473 -0
- package/lib/integrity.js +689 -0
- package/lib/license-gate.mjs +478 -0
- package/lib/license-identify.mjs +229 -0
- package/lib/lint.js +629 -0
- package/lib/method-manifest.js +60 -0
- package/lib/methods/anthropic.js +140 -0
- package/lib/methods/apertium.js +163 -0
- package/lib/methods/api.js +316 -0
- package/lib/methods/base.js +184 -0
- package/lib/methods/content-separator.js +45 -0
- package/lib/methods/deepl.js +426 -0
- package/lib/methods/direct-llm.js +586 -0
- package/lib/methods/external.js +332 -0
- package/lib/methods/fetch-with-retry.js +124 -0
- package/lib/methods/gemini.js +147 -0
- package/lib/methods/google-translate.js +402 -0
- package/lib/methods/http-utils.js +122 -0
- package/lib/methods/libretranslate.js +314 -0
- package/lib/methods/llm-coached.js +670 -0
- package/lib/methods/llm.js +592 -0
- package/lib/methods/local.js +76 -0
- package/lib/methods/microsoft-translator.js +331 -0
- package/lib/methods/openai.js +131 -0
- package/lib/methods/openrouter-client.js +327 -0
- package/lib/methods/openrouter-pricing.js +156 -0
- package/lib/methods/provider-env.js +115 -0
- package/lib/methods/provider-pricing.js +310 -0
- package/lib/methods/tilde.js +150 -0
- package/lib/methods/translated.js +229 -0
- package/lib/methods/translation-error.js +80 -0
- package/lib/models.js +258 -0
- package/lib/no-translate.js +233 -0
- package/lib/output.js +238 -0
- package/lib/pairs.js +547 -0
- package/lib/plugins.js +447 -0
- package/lib/provenance.js +323 -0
- package/lib/recommend.js +648 -0
- package/lib/registers.js +1185 -0
- package/lib/repair-script.js +266 -0
- package/lib/scripts.js +994 -0
- package/lib/seal.mjs +464 -0
- package/lib/sealed-qualifier.mjs +211 -0
- package/lib/security.js +59 -0
- package/lib/segment.js +369 -0
- package/lib/seo.js +275 -0
- package/lib/serve.js +854 -0
- package/lib/string-classify.js +85 -0
- package/lib/submit.mjs +344 -0
- package/lib/sync.js +969 -0
- package/lib/tags/bcp47.js +202 -0
- package/lib/tags/resolve.js +314 -0
- package/lib/terminology.js +111 -0
- package/lib/tm-seed.js +294 -0
- package/lib/tm.js +515 -0
- package/lib/translate-pair.js +197 -0
- package/lib/translate.js +203 -0
- package/lib/types.js +230 -0
- package/lib/validate.js +510 -0
- package/lib/verify.js +451 -0
- package/lib/watch.js +145 -0
- package/lib/xliff.js +184 -0
- package/package.json +93 -0
- package/shared/ATTRIBUTION.md +145 -0
- package/shared/CORPORA-CARDS.md +288 -0
- package/shared/DATA-SOVEREIGNTY.md +500 -0
- package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
- package/shared/card-lint-baseline.json +3189 -0
- package/shared/cards-fallback.json +1 -0
- package/shared/catalogue/card-config.json +6091 -0
- package/shared/catalogue/external-results.json +3888 -0
- package/shared/catalogue/gender-guidance.json +1038 -0
- package/shared/catalogue/method-coverage.json +1751 -0
- package/shared/catalogue/metric-coverage.json +170 -0
- package/shared/catalogue/metric-reliability.json +1 -0
- package/shared/catalogue/register-presets.json +3180 -0
- package/shared/catalogue/vitality-scales.json +55 -0
- package/shared/cldr-index.json +1115 -0
- package/shared/code-bridge.json +253 -0
- package/shared/corpora-cards-v1-reference.md +281 -0
- package/shared/curated-dictionary-flags.json +35 -0
- package/shared/curated-endonyms.json +35 -0
- package/shared/curated-fsts.json +51 -0
- package/shared/curated-orthography-conventions.json +26 -0
- package/shared/curated-sil-resources.json +374 -0
- package/shared/curated-tools.json +41 -0
- package/shared/docent/corpus.json +11333 -0
- package/shared/docent/faq.en.json +564 -0
- package/shared/docent/register-blocks.json +60 -0
- package/shared/docent/system-prompt.md +144 -0
- package/shared/domain-taxonomy.json +35 -0
- package/shared/explainers/glossary.json +2975 -0
- package/shared/explainers/tc-features.json +20112 -0
- package/shared/explainers/term-watchlist.json +147 -0
- package/shared/human-services.json +59 -0
- package/shared/license-corrections.json +261 -0
- package/shared/license-evidence.json +13452 -0
- package/shared/licenses.json +6781 -0
- package/shared/method-registry.json +236 -0
- package/shared/metric-registry.json +620 -0
- package/shared/model-aliases.json +7 -0
- package/shared/schemas/champollion-plugin.schema.json +206 -0
- package/shared/schemas/corpora-card.schema.json +957 -0
- package/shared/schemas/domain-taxonomy.schema.json +64 -0
- package/shared/schemas/external-results.schema.json +314 -0
- package/shared/schemas/human-services.schema.json +90 -0
- package/shared/schemas/language-card.schema.json +1308 -0
- package/shared/schemas/licenses.schema.json +155 -0
- package/shared/schemas/method-card.schema.json +412 -0
- package/shared/schemas/method-registry.schema.json +85 -0
- package/shared/schemas/metric-registry.schema.json +96 -0
- package/shared/schemas/metric-reliability.schema.json +178 -0
- package/shared/schemas/model-aliases.schema.json +27 -0
- package/shared/schemas/source-snapshot.schema.json +96 -0
package/lib/plugins.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin system — loads, validates, installs, and manages method plugins.
|
|
3
|
+
*
|
|
4
|
+
* A plugin is a pre-packaged translation recipe for a specific language pair.
|
|
5
|
+
* It's a JSON manifest (method.json) that tells champollion which method to use,
|
|
6
|
+
* with what settings, and what quality has been verified.
|
|
7
|
+
*
|
|
8
|
+
* PLUGIN TYPES:
|
|
9
|
+
* - Open: type "llm-coached" — coaching data bundled in plugin dir
|
|
10
|
+
* - API: type "api" — translations run server-side (IP protected)
|
|
11
|
+
* - Custom: type "llm" — user's own method config (rare)
|
|
12
|
+
*
|
|
13
|
+
* PLUGIN STORAGE:
|
|
14
|
+
* .champollion/methods/
|
|
15
|
+
* crk-coached-v1/
|
|
16
|
+
* method.json # Manifest (config, benchmarks, provenance)
|
|
17
|
+
* coaching/ # Only for open plugins
|
|
18
|
+
* crk.json
|
|
19
|
+
*
|
|
20
|
+
* INSTALL SOURCES:
|
|
21
|
+
* 1. Remote registry (by name) — planned, not yet available
|
|
22
|
+
* 2. URL (HTTP) — downloads and extracts
|
|
23
|
+
* 3. Local directory — copies to .champollion/methods/
|
|
24
|
+
*
|
|
25
|
+
* NO OVERRIDE COMPLEXITY:
|
|
26
|
+
* Either you use a plugin for a pair, OR you configure manually.
|
|
27
|
+
* No layering, no merging of coaching data between plugins and user data.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
const METHODS_DIR = '.champollion/methods';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reference to the formal JSON Schema that defines the plugin contract.
|
|
37
|
+
* The schema is the source of truth for field definitions, types, and constraints.
|
|
38
|
+
* This runtime validator mirrors its rules without requiring ajv (zero-dependency).
|
|
39
|
+
*/
|
|
40
|
+
const SCHEMA_REF = 'shared/schemas/champollion-plugin.schema.json';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Required fields in a method.json manifest.
|
|
44
|
+
*/
|
|
45
|
+
const REQUIRED_FIELDS = ['name', 'type', 'version', 'locales'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Valid method types that plugins can declare.
|
|
49
|
+
*/
|
|
50
|
+
const VALID_TYPES = ['llm', 'llm-coached', 'api', 'google-translate', 'deepl', 'microsoft-translator', 'libretranslate', 'openai', 'anthropic', 'gemini'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load all installed plugins from the project's .champollion/methods/ directory.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
56
|
+
* @returns {Map<string, object>} Map of plugin name → parsed manifest
|
|
57
|
+
*/
|
|
58
|
+
function loadPlugins(projectRoot) {
|
|
59
|
+
const plugins = new Map();
|
|
60
|
+
const methodsDir = path.join(projectRoot, METHODS_DIR);
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(methodsDir)) {
|
|
63
|
+
return plugins;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = fs.readdirSync(methodsDir, { withFileTypes: true });
|
|
69
|
+
} catch {
|
|
70
|
+
return plugins;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.isDirectory()) continue;
|
|
75
|
+
|
|
76
|
+
const manifestPath = path.join(methodsDir, entry.name, 'method.json');
|
|
77
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
81
|
+
const manifest = JSON.parse(raw);
|
|
82
|
+
|
|
83
|
+
const validation = validateManifest(manifest);
|
|
84
|
+
if (!validation.valid) {
|
|
85
|
+
console.error(` [WARN] Skipping plugin "${entry.name}": ${validation.errors.join(', ')}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Attach the resolved directory path so methods can find coaching data
|
|
90
|
+
manifest._pluginDir = path.join(methodsDir, entry.name);
|
|
91
|
+
plugins.set(manifest.name, manifest);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(` [WARN] Skipping plugin "${entry.name}": ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return plugins;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate a method.json manifest.
|
|
102
|
+
*
|
|
103
|
+
* Runtime mirror of the constraints defined in the formal JSON Schema
|
|
104
|
+
* (see SCHEMA_REF). Kept hand-rolled to preserve zero-dependency.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} manifest - Parsed manifest object
|
|
107
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
108
|
+
*/
|
|
109
|
+
function validateManifest(manifest) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
|
|
112
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
113
|
+
return { valid: false, errors: ['Manifest is not a valid object'] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check required fields
|
|
117
|
+
for (const field of REQUIRED_FIELDS) {
|
|
118
|
+
if (!manifest[field]) {
|
|
119
|
+
errors.push(`Missing required field: "${field}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate type
|
|
124
|
+
if (manifest.type && !VALID_TYPES.includes(manifest.type)) {
|
|
125
|
+
errors.push(`Invalid type "${manifest.type}" — must be one of: ${VALID_TYPES.join(', ')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate locales is a non-empty array of strings
|
|
129
|
+
if (manifest.locales) {
|
|
130
|
+
if (!Array.isArray(manifest.locales)) {
|
|
131
|
+
errors.push('"locales" must be an array of locale codes');
|
|
132
|
+
} else if (manifest.locales.length === 0) {
|
|
133
|
+
errors.push('"locales" must contain at least one locale code');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate name format (kebab-case, matches schema pattern)
|
|
138
|
+
if (manifest.name && !/^[a-z0-9][a-z0-9-]*$/.test(manifest.name)) {
|
|
139
|
+
errors.push(`Invalid name "${manifest.name}" — use kebab-case (e.g., "french-formal-v1")`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate version format (semver-like: digits.digits.digits)
|
|
143
|
+
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
144
|
+
errors.push(`Invalid version "${manifest.version}" — use semver format (e.g., "1.0.0")`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// API plugins must have an endpoint
|
|
148
|
+
if (manifest.type === 'api' && !manifest.endpoint) {
|
|
149
|
+
errors.push('API plugins must include an "endpoint" URL');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Benchmarks: validate structure if present
|
|
153
|
+
if (manifest.benchmarks) {
|
|
154
|
+
if (typeof manifest.benchmarks !== 'object') {
|
|
155
|
+
errors.push('"benchmarks" must be an object keyed by locale code');
|
|
156
|
+
} else {
|
|
157
|
+
for (const [locale, entry] of Object.entries(manifest.benchmarks)) {
|
|
158
|
+
if (!entry || typeof entry !== 'object') {
|
|
159
|
+
errors.push(`benchmarks.${locale}: must be an object`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Required benchmark fields (per JSON Schema)
|
|
163
|
+
for (const reqField of ['date', 'corpus_size', 'exact_match_rate']) {
|
|
164
|
+
if (entry[reqField] === undefined || entry[reqField] === null) {
|
|
165
|
+
errors.push(`benchmarks.${locale}: missing required field "${reqField}"`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Range checks
|
|
169
|
+
if (typeof entry.exact_match_rate === 'number' && (entry.exact_match_rate < 0 || entry.exact_match_rate > 1)) {
|
|
170
|
+
errors.push(`benchmarks.${locale}: exact_match_rate must be 0.0–1.0`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Provenance: validate structure if present
|
|
177
|
+
if (manifest.provenance) {
|
|
178
|
+
if (typeof manifest.provenance !== 'object') {
|
|
179
|
+
errors.push('"provenance" must be an object');
|
|
180
|
+
} else {
|
|
181
|
+
if (manifest.provenance.resources && !Array.isArray(manifest.provenance.resources)) {
|
|
182
|
+
errors.push('"provenance.resources" must be an array');
|
|
183
|
+
}
|
|
184
|
+
if (manifest.provenance.flags && !Array.isArray(manifest.provenance.flags)) {
|
|
185
|
+
errors.push('"provenance.flags" must be an array');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Config: validate ranges if present
|
|
191
|
+
if (manifest.config && typeof manifest.config === 'object') {
|
|
192
|
+
if (manifest.config.batchSize !== undefined) {
|
|
193
|
+
if (typeof manifest.config.batchSize !== 'number' || manifest.config.batchSize < 1 || manifest.config.batchSize > 200) {
|
|
194
|
+
errors.push('"config.batchSize" must be 1–200');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (manifest.config.temperature !== undefined) {
|
|
198
|
+
if (typeof manifest.config.temperature !== 'number' || manifest.config.temperature < 0 || manifest.config.temperature > 2) {
|
|
199
|
+
errors.push('"config.temperature" must be 0–2');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { valid: errors.length === 0, errors };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Install a plugin from a local directory path.
|
|
209
|
+
*
|
|
210
|
+
* Copies the directory to .champollion/methods/<name>/ and validates the manifest.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} source - Local directory path containing method.json
|
|
213
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
214
|
+
* @returns {{ success: boolean, name: string|null, error: string|null }}
|
|
215
|
+
*/
|
|
216
|
+
function installPluginFromLocal(source, projectRoot) {
|
|
217
|
+
const resolvedSource = path.resolve(source);
|
|
218
|
+
|
|
219
|
+
// Validate source exists and has method.json
|
|
220
|
+
const manifestPath = path.join(resolvedSource, 'method.json');
|
|
221
|
+
if (!fs.existsSync(manifestPath)) {
|
|
222
|
+
return { success: false, name: null, error: `No method.json found in ${resolvedSource}` };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let manifest;
|
|
226
|
+
try {
|
|
227
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
228
|
+
} catch (err) {
|
|
229
|
+
return { success: false, name: null, error: `Invalid method.json: ${err.message}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const validation = validateManifest(manifest);
|
|
233
|
+
if (!validation.valid) {
|
|
234
|
+
return { success: false, name: null, error: `Invalid manifest: ${validation.errors.join(', ')}` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Copy to .champollion/methods/<name>/
|
|
238
|
+
const targetDir = path.join(projectRoot, METHODS_DIR, manifest.name);
|
|
239
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
240
|
+
|
|
241
|
+
copyDirRecursive(resolvedSource, targetDir);
|
|
242
|
+
|
|
243
|
+
return { success: true, name: manifest.name, error: null };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Install a plugin manifest directly (e.g., from a remote registry response).
|
|
248
|
+
*
|
|
249
|
+
* Creates the plugin directory and writes the manifest JSON.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} manifest - Parsed manifest object
|
|
252
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
253
|
+
* @returns {{ success: boolean, name: string|null, error: string|null }}
|
|
254
|
+
*/
|
|
255
|
+
function installPluginFromManifest(manifest, projectRoot) {
|
|
256
|
+
const validation = validateManifest(manifest);
|
|
257
|
+
if (!validation.valid) {
|
|
258
|
+
return { success: false, name: null, error: `Invalid manifest: ${validation.errors.join(', ')}` };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const targetDir = path.join(projectRoot, METHODS_DIR, manifest.name);
|
|
262
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
263
|
+
|
|
264
|
+
// Write the manifest
|
|
265
|
+
fs.writeFileSync(
|
|
266
|
+
path.join(targetDir, 'method.json'),
|
|
267
|
+
JSON.stringify(manifest, null, 2),
|
|
268
|
+
'utf-8',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return { success: true, name: manifest.name, error: null };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Remove an installed plugin.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} name - Plugin name
|
|
278
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
279
|
+
* @returns {{ success: boolean, error: string|null }}
|
|
280
|
+
*/
|
|
281
|
+
function removePlugin(name, projectRoot) {
|
|
282
|
+
const pluginDir = path.join(projectRoot, METHODS_DIR, name);
|
|
283
|
+
|
|
284
|
+
if (!fs.existsSync(pluginDir)) {
|
|
285
|
+
return { success: false, error: `Plugin "${name}" is not installed` };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
289
|
+
return { success: true, error: null };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* List all installed plugins with summary info.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
296
|
+
* @returns {Array<{ name, type, version, locales, qualityTier, hasBenchmarks }>}
|
|
297
|
+
*/
|
|
298
|
+
function listPlugins(projectRoot) {
|
|
299
|
+
const plugins = loadPlugins(projectRoot);
|
|
300
|
+
const summaries = [];
|
|
301
|
+
|
|
302
|
+
for (const [name, manifest] of plugins) {
|
|
303
|
+
summaries.push({
|
|
304
|
+
name,
|
|
305
|
+
type: manifest.type,
|
|
306
|
+
version: manifest.version,
|
|
307
|
+
description: manifest.description || '',
|
|
308
|
+
locales: manifest.locales || [],
|
|
309
|
+
qualityTier: manifest.config?.qualityTier || inferTierFromType(manifest.type),
|
|
310
|
+
hasBenchmarks: !!(manifest.benchmarks && Object.keys(manifest.benchmarks).length > 0),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return summaries;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Resolve plugin config for a specific pair.
|
|
319
|
+
*
|
|
320
|
+
* If the pair config references a methodPlugin, load the plugin and merge
|
|
321
|
+
* its configuration into the pair config. Plugin config fills gaps — it
|
|
322
|
+
* doesn't override explicit pair-level settings.
|
|
323
|
+
*
|
|
324
|
+
* @param {Map<string, object>} plugins - Loaded plugin registry
|
|
325
|
+
* @param {import('./types.js').PairConfig} pairConfig - Pair config from pairs.js
|
|
326
|
+
* @returns {import('./types.js').PairConfig} Merged pair config with plugin data
|
|
327
|
+
*/
|
|
328
|
+
function resolvePluginForPair(plugins, pairConfig) {
|
|
329
|
+
if (!pairConfig.methodPlugin) {
|
|
330
|
+
return pairConfig;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const plugin = plugins.get(pairConfig.methodPlugin);
|
|
334
|
+
if (!plugin) {
|
|
335
|
+
console.error(` [WARN] Plugin "${pairConfig.methodPlugin}" not found — using pair defaults`);
|
|
336
|
+
return pairConfig;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Plugin type wins for method — the user referenced this plugin because
|
|
340
|
+
// they want its translation strategy. Without this, the default 'llm'
|
|
341
|
+
// set by resolvePairs would silently override the plugin's declared type.
|
|
342
|
+
const method = plugin.type || pairConfig.method || 'llm';
|
|
343
|
+
|
|
344
|
+
// Precedence (per METHOD_PLUGIN_SPEC.md):
|
|
345
|
+
// explicit pair config > plugin config > system defaults
|
|
346
|
+
//
|
|
347
|
+
// pairConfig._defaults (a Set) tracks which fields were filled from
|
|
348
|
+
// system defaults by resolvePairs(). For those fields, plugin config
|
|
349
|
+
// should win. For explicitly set fields, the pair config wins.
|
|
350
|
+
const defaults = pairConfig._defaults || new Set();
|
|
351
|
+
|
|
352
|
+
// Helper: resolve a field value respecting the precedence chain.
|
|
353
|
+
// Two cases where the plugin value wins:
|
|
354
|
+
// 1. pairConfig[field] is nullish (gap — field was never set)
|
|
355
|
+
// 2. pairConfig[field] is truthy but came from system defaults
|
|
356
|
+
// (_defaults set tracks this from resolvePairs)
|
|
357
|
+
// If the pair explicitly set the field, it always wins.
|
|
358
|
+
const resolve = (field, pluginValue) => {
|
|
359
|
+
const pairValue = pairConfig[field];
|
|
360
|
+
if (pairValue == null && pluginValue != null) return pluginValue;
|
|
361
|
+
if (defaults.has(field) && pluginValue != null) return pluginValue;
|
|
362
|
+
return pairValue;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const merged = {
|
|
366
|
+
...pairConfig,
|
|
367
|
+
method,
|
|
368
|
+
model: resolve('model', plugin.config?.model),
|
|
369
|
+
register: resolve('register', plugin.config?.register),
|
|
370
|
+
batchSize: resolve('batchSize', plugin.config?.batchSize),
|
|
371
|
+
temperature: resolve('temperature', plugin.config?.temperature),
|
|
372
|
+
coachingFile: resolve('coachingFile', plugin.config?.coachingFile),
|
|
373
|
+
coachingPrompt: resolve('coachingPrompt', plugin.config?.coachingPrompt),
|
|
374
|
+
promptContext: resolve('promptContext', plugin.config?.promptContext),
|
|
375
|
+
|
|
376
|
+
// Plugin-specific fields
|
|
377
|
+
endpoint: plugin.endpoint || pairConfig.endpoint || null,
|
|
378
|
+
pluginName: plugin.name,
|
|
379
|
+
pluginVersion: plugin.version,
|
|
380
|
+
pluginDir: plugin._pluginDir,
|
|
381
|
+
pluginBenchmarks: plugin.benchmarks || null,
|
|
382
|
+
pluginProvenance: plugin.provenance || null,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Auto-set quality tier from plugin type if not explicitly set
|
|
386
|
+
if (pairConfig.qualityTier === 'standard' || !pairConfig.qualityTier) {
|
|
387
|
+
merged.qualityTier = inferTierFromType(method);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return merged;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// -----------------------------------------------------------------
|
|
394
|
+
// Helpers
|
|
395
|
+
// -----------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Infer quality tier from method type.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} type - Method type name (e.g., 'llm', 'api')
|
|
401
|
+
* @returns {string} Quality tier: 'standard', 'high', 'research', or 'verified'
|
|
402
|
+
*/
|
|
403
|
+
function inferTierFromType(type) {
|
|
404
|
+
const TIER_MAP = {
|
|
405
|
+
'llm': 'standard',
|
|
406
|
+
'google-translate': 'standard',
|
|
407
|
+
'llm-coached': 'high',
|
|
408
|
+
'api': 'high', // conservative default — server may be research-grade
|
|
409
|
+
};
|
|
410
|
+
return TIER_MAP[type] || 'standard';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Recursively copy a directory.
|
|
415
|
+
*
|
|
416
|
+
* @param {string} source - Absolute path to source directory
|
|
417
|
+
* @param {string} target - Absolute path to destination directory
|
|
418
|
+
*/
|
|
419
|
+
function copyDirRecursive(source, target) {
|
|
420
|
+
const entries = fs.readdirSync(source, { withFileTypes: true });
|
|
421
|
+
|
|
422
|
+
for (const entry of entries) {
|
|
423
|
+
const srcPath = path.join(source, entry.name);
|
|
424
|
+
const destPath = path.join(target, entry.name);
|
|
425
|
+
|
|
426
|
+
if (entry.isDirectory()) {
|
|
427
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
428
|
+
copyDirRecursive(srcPath, destPath);
|
|
429
|
+
} else {
|
|
430
|
+
fs.copyFileSync(srcPath, destPath);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export {
|
|
436
|
+
loadPlugins,
|
|
437
|
+
validateManifest,
|
|
438
|
+
installPluginFromLocal,
|
|
439
|
+
installPluginFromManifest,
|
|
440
|
+
removePlugin,
|
|
441
|
+
listPlugins,
|
|
442
|
+
resolvePluginForPair,
|
|
443
|
+
METHODS_DIR,
|
|
444
|
+
REQUIRED_FIELDS,
|
|
445
|
+
VALID_TYPES,
|
|
446
|
+
SCHEMA_REF,
|
|
447
|
+
};
|