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.
Files changed (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. 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
+ };