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/recommend.js
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Method recommendation for a language pair — the routing evidence surface.
|
|
3
|
+
*
|
|
4
|
+
* JS port of arena/mt_eval_harness/recommend.py (the spec — keep the two in
|
|
5
|
+
* sync; its test contract lives in arena/tests/test_recommend.py and is
|
|
6
|
+
* mirrored in test/recommend.test.js). `champollion recommend SRC TGT`
|
|
7
|
+
* answers the practical question a translator-integrator actually has:
|
|
8
|
+
* "I need to translate SRC→TGT — what are my options, what does the published
|
|
9
|
+
* evidence say, and what can I actually run right now?" — while refusing to
|
|
10
|
+
* overclaim.
|
|
11
|
+
*
|
|
12
|
+
* Three evidence tiers are consulted, all offline (shared/ artifacts — no
|
|
13
|
+
* network, no credentials):
|
|
14
|
+
*
|
|
15
|
+
* 1. Dispatchable methods — shared/method-registry.json (the cross-runtime
|
|
16
|
+
* SSOT, via lib/method-manifest.js): every engine/provider with per-
|
|
17
|
+
* method AVAILABILITY resolved live (is its API key set? is it
|
|
18
|
+
* harness-only? is its license commercial-ready?). Key resolution reads
|
|
19
|
+
* the registry's credential_env metadata (key pairs require ALL vars;
|
|
20
|
+
* config vars like AWS_REGION never count) reconciled with
|
|
21
|
+
* lib/methods/provider-env.js so the verdict matches exactly what the
|
|
22
|
+
* method loaders read (canonical name + aliases, process.env AND .env
|
|
23
|
+
* files) — see resolveAvailability for the precedence.
|
|
24
|
+
* 2. Curated cited results — shared/catalogue/external-results.json
|
|
25
|
+
* (bundled into the npm package via sync:shared): hand-verified
|
|
26
|
+
* published datapoints (cited ≠ reproduced), direction-exact.
|
|
27
|
+
* 3. Bulk cited results — shared/catalogue/external-mt-index.json: the
|
|
28
|
+
* machine-imported best-published-score index (OPUS-MT leaderboard
|
|
29
|
+
* family), RELATIVE-ONLY lane by construction. NOT bundled into the
|
|
30
|
+
* npm package (1.6 MB) — available in monorepo checkouts; packaged
|
|
31
|
+
* installs degrade to an explicit note.
|
|
32
|
+
*
|
|
33
|
+
* HONESTY CONTRACT (the whole point):
|
|
34
|
+
* - Cited evidence orders methods relative to each other on a memorized
|
|
35
|
+
* public benchmark — never absolute quality, never a deployment ranking.
|
|
36
|
+
* - Evidence and dispatchability are DIFFERENT axes: the best-evidenced
|
|
37
|
+
* model for a pair may not be runnable in Champollion, and may be
|
|
38
|
+
* NC-licensed (excluded from commercial-lane recommendations). The two
|
|
39
|
+
* are joined only with explicit caveats.
|
|
40
|
+
* - No evidence → say exactly that, and point at the runnable corpora so
|
|
41
|
+
* the user can MEASURE rather than guess.
|
|
42
|
+
* - Missing artifacts (packaged installs) → each tier degrades to an
|
|
43
|
+
* explicit "not available" note, never silently.
|
|
44
|
+
*
|
|
45
|
+
* Pure logic over the shared artifacts; loaders take explicit fixtures so
|
|
46
|
+
* tests can inject them.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import fs from 'node:fs';
|
|
50
|
+
import path from 'node:path';
|
|
51
|
+
import { fileURLToPath } from 'node:url';
|
|
52
|
+
|
|
53
|
+
import { loadMethodManifest, cliNameFor } from './method-manifest.js';
|
|
54
|
+
import { providerEnvNames } from './methods/provider-env.js';
|
|
55
|
+
import { getEnvOrFileVar } from './api-key.js';
|
|
56
|
+
import { LANE_RELATIVE_ONLY, laneForGrade, normalizeGrade } from './contamination-lane.js';
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Shared-artifact resolution (mirrors method-manifest.js's loader)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find shared/catalogue/<filename>: package-bundled copy first
|
|
64
|
+
* (cli/shared/catalogue/, shipped in the npm package), then the
|
|
65
|
+
* monorepo-root SSOT. Returns null when neither exists — callers render an
|
|
66
|
+
* explicit "not available" note, never a silent skip.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} filename
|
|
69
|
+
* @returns {string|null}
|
|
70
|
+
*/
|
|
71
|
+
function cataloguePath(filename) {
|
|
72
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
const candidates = [
|
|
74
|
+
path.resolve(__dirname, '..', 'shared', 'catalogue', filename),
|
|
75
|
+
path.resolve(__dirname, '..', '..', 'shared', 'catalogue', filename),
|
|
76
|
+
];
|
|
77
|
+
for (const p of candidates) {
|
|
78
|
+
if (fs.existsSync(p)) return p;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @returns {object|null} parsed JSON, or null when the path is null/unreadable */
|
|
84
|
+
function loadCatalogueJson(filename) {
|
|
85
|
+
const p = cataloguePath(filename);
|
|
86
|
+
if (p === null) return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Tier 1 — dispatchable methods + live availability
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve one method-registry entry to a live availability verdict.
|
|
100
|
+
*
|
|
101
|
+
* Returns { status, detail } where status ∈:
|
|
102
|
+
* ready — invocable right now (needed env key present, or none needed)
|
|
103
|
+
* needs-key — adapter exists; set one of the named env vars
|
|
104
|
+
* local-setup — local engine; needs the optional install/extra
|
|
105
|
+
*
|
|
106
|
+
* Readiness is judged on the entry's CREDENTIAL vars only — the registry
|
|
107
|
+
* `env` list also names non-credential config vars (AWS_REGION, *_ENDPOINT)
|
|
108
|
+
* the adapter reads, and those must never make a method read "ready" (a
|
|
109
|
+
* region is not auth). The var list is picked in this order:
|
|
110
|
+
* 1. `credential_env` + `credential_env_all: true` (key-pair auth: EVERY
|
|
111
|
+
* var required — AWS id + secret, Lara id + secret). Beats PROVIDER_ENV,
|
|
112
|
+
* whose any-of semantics fit alias lists, not pairs.
|
|
113
|
+
* 2. PROVIDER_ENV (lib/methods/provider-env.js) when the provider is
|
|
114
|
+
* registered there: exactly the surface the method loaders read
|
|
115
|
+
* (canonical + aliases, incl. names the registry doesn't carry), any-of.
|
|
116
|
+
* 3. `credential_env` (any-of) — the registry's declared credential subset,
|
|
117
|
+
* for providers with no PROVIDER_ENV entry (harness-only engines).
|
|
118
|
+
* 4. The raw registry `env` list (every var is a credential).
|
|
119
|
+
*
|
|
120
|
+
* @param {string} name - Canonical registry name (e.g. 'google-translate')
|
|
121
|
+
* @param {object} entry - The method-registry entry
|
|
122
|
+
* @param {object} [opts]
|
|
123
|
+
* @param {Object<string,string>|null} [opts.env] - Fixture env for tests;
|
|
124
|
+
* null/omitted = live resolution via process.env AND .env files
|
|
125
|
+
* @param {string} [opts.cwd] - Project root for .env lookup (live mode)
|
|
126
|
+
* @returns {{status: string, detail: string}}
|
|
127
|
+
*/
|
|
128
|
+
function resolveAvailability(name, entry, { env = null, cwd = undefined } = {}) {
|
|
129
|
+
const credVars = Array.isArray(entry.credential_env) ? entry.credential_env : null;
|
|
130
|
+
const needAll = entry.credential_env_all === true && credVars !== null;
|
|
131
|
+
const providerNames = providerEnvNames(name);
|
|
132
|
+
const envVars = needAll ? credVars
|
|
133
|
+
: providerNames.length > 0 ? providerNames
|
|
134
|
+
: (credVars ?? entry.env ?? []);
|
|
135
|
+
const extra = entry.optional_extra;
|
|
136
|
+
const extraNote = extra ? ` (requires pip extra '${extra}')` : '';
|
|
137
|
+
|
|
138
|
+
if (entry.kind === 'local-model') {
|
|
139
|
+
return {
|
|
140
|
+
status: 'local-setup',
|
|
141
|
+
detail: extra
|
|
142
|
+
? `local engine — install extra '${extra}'`
|
|
143
|
+
: 'local engine — see homepage for setup',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (envVars.length === 0) {
|
|
147
|
+
return { status: 'ready', detail: `no credentials required${extraNote}` };
|
|
148
|
+
}
|
|
149
|
+
const get = env === null ? (n) => getEnvOrFileVar(n, cwd) : (n) => env[n];
|
|
150
|
+
const present = envVars.filter((n) => get(n));
|
|
151
|
+
if (needAll) {
|
|
152
|
+
const missing = envVars.filter((n) => !get(n));
|
|
153
|
+
if (missing.length === 0) {
|
|
154
|
+
return { status: 'ready', detail: `${envVars.join(' + ')} are set${extraNote}` };
|
|
155
|
+
}
|
|
156
|
+
const qualifier = present.length > 0
|
|
157
|
+
? ` (${present[0]} alone is not enough)`
|
|
158
|
+
: ' (all required)';
|
|
159
|
+
return { status: 'needs-key', detail: `set ${missing.join(' + ')}${qualifier}${extraNote}` };
|
|
160
|
+
}
|
|
161
|
+
if (present.length > 0) {
|
|
162
|
+
return { status: 'ready', detail: `${present[0]} is set${extraNote}` };
|
|
163
|
+
}
|
|
164
|
+
return { status: 'needs-key', detail: `set ${envVars.join(' or ')}${extraNote}` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Tier-1 rows: every registry method with availability + lane verdicts.
|
|
169
|
+
*
|
|
170
|
+
* useContext ∈ {'non-commercial', 'commercial'}: the commercial lane is
|
|
171
|
+
* STRICT (mirrors the harness's license lane) — a method whose
|
|
172
|
+
* commercialReady is not true is excluded-with-reason, never silently
|
|
173
|
+
* dropped. Entries whose runtimes exclude 'cli' are flagged harness-only
|
|
174
|
+
* and point at mt-eval (mirrors translate.js's findHarnessOnlyEntry).
|
|
175
|
+
*
|
|
176
|
+
* @param {string} useContext
|
|
177
|
+
* @param {object} [opts]
|
|
178
|
+
* @param {object|null} [opts.manifest] - Fixture manifest for tests
|
|
179
|
+
* @param {Object<string,string>|null} [opts.env] - Fixture env for tests
|
|
180
|
+
* @param {string} [opts.cwd]
|
|
181
|
+
* @returns {object[]}
|
|
182
|
+
*/
|
|
183
|
+
function dispatchableMethods(useContext, { manifest = undefined, env = null, cwd = undefined } = {}) {
|
|
184
|
+
const m = manifest !== undefined ? manifest : loadMethodManifest();
|
|
185
|
+
if (!m || !m.entries) return [];
|
|
186
|
+
const rows = [];
|
|
187
|
+
for (const [name, entry] of Object.entries(m.entries)) {
|
|
188
|
+
const avail = resolveAvailability(name, entry, { env, cwd });
|
|
189
|
+
const runtimes = entry.runtimes || ['harness', 'cli'];
|
|
190
|
+
const harnessOnly = Array.isArray(entry.runtimes) && !entry.runtimes.includes('cli');
|
|
191
|
+
let laneOk = true;
|
|
192
|
+
let laneNote = null;
|
|
193
|
+
if (useContext === 'commercial' && entry.commercialReady !== true) {
|
|
194
|
+
laneOk = false;
|
|
195
|
+
laneNote = `excluded from the commercial lane — license: ${entry.license || 'unknown'}`;
|
|
196
|
+
}
|
|
197
|
+
rows.push({
|
|
198
|
+
method: name,
|
|
199
|
+
cli_name: cliNameFor(name, entry),
|
|
200
|
+
kind: entry.kind ?? null,
|
|
201
|
+
paradigm: entry.paradigm ?? null,
|
|
202
|
+
license: entry.license ?? null,
|
|
203
|
+
commercial_ready: entry.commercialReady === true,
|
|
204
|
+
runtimes,
|
|
205
|
+
harness_only: harnessOnly,
|
|
206
|
+
runtime_note: harnessOnly
|
|
207
|
+
? `harness-only — no CLI adapter; run it with: mt-eval run --method ${name}`
|
|
208
|
+
: null,
|
|
209
|
+
availability: avail.status,
|
|
210
|
+
availability_detail: avail.detail,
|
|
211
|
+
lane_ok: laneOk,
|
|
212
|
+
lane_note: laneNote,
|
|
213
|
+
cost_note: entry.cost_note ?? null,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const order = { 'ready': 0, 'needs-key': 1, 'local-setup': 2 };
|
|
217
|
+
rows.sort((a, b) =>
|
|
218
|
+
(a.lane_ok === b.lane_ok ? 0 : a.lane_ok ? -1 : 1)
|
|
219
|
+
|| ((order[a.availability] ?? 9) - (order[b.availability] ?? 9))
|
|
220
|
+
|| a.method.localeCompare(b.method));
|
|
221
|
+
return rows;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Tier 2 — curated cited results (direction-exact)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Direction-exact curated datapoints + the cited-methods index.
|
|
230
|
+
*
|
|
231
|
+
* Rows carry per-datapoint provenance; ranking across different
|
|
232
|
+
* (benchmark, metric) buckets is deliberately NOT performed — metric
|
|
233
|
+
* variants are incomparable (metric_variant_flag).
|
|
234
|
+
*
|
|
235
|
+
* @param {string} src - ISO 639-3 source code
|
|
236
|
+
* @param {string} tgt - ISO 639-3 target code
|
|
237
|
+
* @param {object|null} [catalogue] - Fixture for tests; omit to load the
|
|
238
|
+
* shared artifact
|
|
239
|
+
* @returns {{rows: object[], methodsIndex: Object<string, object>}}
|
|
240
|
+
*/
|
|
241
|
+
function curatedEvidence(src, tgt, catalogue = undefined) {
|
|
242
|
+
const cat = catalogue !== undefined
|
|
243
|
+
? catalogue
|
|
244
|
+
: loadCatalogueJson('external-results.json');
|
|
245
|
+
if (!cat || Object.keys(cat).length === 0) return { rows: [], methodsIndex: {} };
|
|
246
|
+
const methodsIndex = {};
|
|
247
|
+
for (const m of cat.methods || []) {
|
|
248
|
+
if (m && m.id != null) methodsIndex[m.id] = m;
|
|
249
|
+
}
|
|
250
|
+
const rows = [];
|
|
251
|
+
for (const r of cat.results || []) {
|
|
252
|
+
const pair = r.pair || {};
|
|
253
|
+
if (pair.source !== src || pair.target !== tgt) continue;
|
|
254
|
+
const grade = normalizeGrade((r.signal_strength || {}).contamination);
|
|
255
|
+
rows.push({
|
|
256
|
+
tier: 'curated',
|
|
257
|
+
model: r.model ?? null,
|
|
258
|
+
benchmark: r.benchmark ?? null,
|
|
259
|
+
metric: r.metric ?? null,
|
|
260
|
+
value: r.value ?? null,
|
|
261
|
+
lower_is_better: r.lower_is_better === true,
|
|
262
|
+
grade: (r.signal_strength || {}).grade ?? null,
|
|
263
|
+
lane: grade ? laneForGrade(grade) : LANE_RELATIVE_ONLY,
|
|
264
|
+
verified: r.verified === true,
|
|
265
|
+
citation: r.citation ?? null,
|
|
266
|
+
source_url: r.source_url ?? null,
|
|
267
|
+
method_ref: r.method_ref ?? null,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return { rows, methodsIndex };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Tier 3 — bulk cited index (relative-only by construction)
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
/** Strip a FLORES-style script suffix: 'ace_Arab' → 'ace'. */
|
|
278
|
+
function baseCode(code) {
|
|
279
|
+
return code.split('_', 1)[0];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Best-published rows for the pair from the bulk index.
|
|
284
|
+
*
|
|
285
|
+
* Pair keys are upstream codes (may carry script suffixes); we match on the
|
|
286
|
+
* script-stripped base and surface the exact upstream key on each row so
|
|
287
|
+
* nothing is silently relabelled.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} src
|
|
290
|
+
* @param {string} tgt
|
|
291
|
+
* @param {object} [opts]
|
|
292
|
+
* @param {object|null} [opts.index] - Fixture for tests
|
|
293
|
+
* @param {number} [opts.maxRows]
|
|
294
|
+
* @returns {{rows: object[], meta: object}}
|
|
295
|
+
*/
|
|
296
|
+
function bulkEvidence(src, tgt, { index = undefined, maxRows = 8 } = {}) {
|
|
297
|
+
const idx = index !== undefined ? index : loadCatalogueJson('external-mt-index.json');
|
|
298
|
+
if (!idx || Object.keys(idx).length === 0) return { rows: [], meta: {} };
|
|
299
|
+
const models = idx.models || [];
|
|
300
|
+
const posture = idx.contamination_posture || {};
|
|
301
|
+
const rows = [];
|
|
302
|
+
for (const [key, cells] of Object.entries(idx.pairs || {})) {
|
|
303
|
+
const sep = key.indexOf('-');
|
|
304
|
+
const s = sep === -1 ? key : key.slice(0, sep);
|
|
305
|
+
const t = sep === -1 ? '' : key.slice(sep + 1);
|
|
306
|
+
if (baseCode(s) !== src || baseCode(t) !== tgt) continue;
|
|
307
|
+
for (const [testset, metrics] of Object.entries(cells)) {
|
|
308
|
+
for (const [metric, cell] of Object.entries(metrics)) {
|
|
309
|
+
const [modelIdx, value] = cell;
|
|
310
|
+
const model = (Number.isInteger(modelIdx) && modelIdx < models.length)
|
|
311
|
+
? models[modelIdx]
|
|
312
|
+
: String(modelIdx);
|
|
313
|
+
rows.push({
|
|
314
|
+
tier: 'bulk',
|
|
315
|
+
pair_key: key,
|
|
316
|
+
model,
|
|
317
|
+
benchmark: testset,
|
|
318
|
+
metric,
|
|
319
|
+
value,
|
|
320
|
+
lane: LANE_RELATIVE_ONLY,
|
|
321
|
+
posture: posture[testset] ?? null,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
rows.sort((a, b) =>
|
|
327
|
+
a.benchmark.localeCompare(b.benchmark)
|
|
328
|
+
|| a.metric.localeCompare(b.metric)
|
|
329
|
+
|| a.pair_key.localeCompare(b.pair_key));
|
|
330
|
+
const meta = {
|
|
331
|
+
provider: idx.provider ?? null,
|
|
332
|
+
provenance: idx.provenance ?? null,
|
|
333
|
+
truncated: rows.length > maxRows,
|
|
334
|
+
total_rows: rows.length,
|
|
335
|
+
};
|
|
336
|
+
return { rows: rows.slice(0, maxRows), meta };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Tier 4 — metric-reliability evidence (which metric to BELIEVE for the target)
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Per-target-family metric↔human correlation evidence (workstream B3).
|
|
345
|
+
*
|
|
346
|
+
* Answers a different question from tiers 1–3: not "which SYSTEM scores
|
|
347
|
+
* best" but "which METRIC can you trust to score output in this target
|
|
348
|
+
* language". Sourced from shared/catalogue/metric-reliability.json — the
|
|
349
|
+
* champollion-derived correlations between automatic metrics and the WMT
|
|
350
|
+
* Metrics-task human judgments (wmt19–wmt25); methodology spec:
|
|
351
|
+
* https://champollion.dev/docs/network/specifications/metric-reliability
|
|
352
|
+
*
|
|
353
|
+
* Fail-honest: an absent index, or a target no WMT campaign ever judged,
|
|
354
|
+
* yields an explicit note — never a silent skip, never borrowed numbers.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} tgt
|
|
357
|
+
* @param {object|null} [reliability] - Fixture for tests
|
|
358
|
+
* @returns {{section: object|null, notes: string[]}}
|
|
359
|
+
*/
|
|
360
|
+
function metricReliabilityEvidence(tgt, reliability = undefined) {
|
|
361
|
+
const rel = reliability !== undefined
|
|
362
|
+
? reliability
|
|
363
|
+
: loadCatalogueJson('metric-reliability.json');
|
|
364
|
+
if (!rel) {
|
|
365
|
+
return {
|
|
366
|
+
section: null,
|
|
367
|
+
notes: ['Metric-reliability index not bundled in the npm package — '
|
|
368
|
+
+ 'metric-trust tier skipped explicitly. See the methodology spec at '
|
|
369
|
+
+ 'https://champollion.dev/docs/network/specifications/metric-reliability, '
|
|
370
|
+
+ 'or run from a monorepo checkout (or `mt-eval recommend`) for the '
|
|
371
|
+
+ 'full evidence.'],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const languages = rel.languages || {};
|
|
375
|
+
let code = null;
|
|
376
|
+
let info = null;
|
|
377
|
+
for (const key of Object.keys(languages).sort()) {
|
|
378
|
+
const entry = languages[key] || {};
|
|
379
|
+
if (tgt === key || tgt === entry.iso639_3) {
|
|
380
|
+
code = key;
|
|
381
|
+
info = entry;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (info === null) {
|
|
386
|
+
return {
|
|
387
|
+
section: null,
|
|
388
|
+
notes: [`No WMT human-judgment meta-evaluation covers target '${tgt}' `
|
|
389
|
+
+ '(directly or via its family) — metric choice for this language is '
|
|
390
|
+
+ 'UNMEASURED. Treat every metric as unvalidated there; prefer '
|
|
391
|
+
+ 'metrics with morphology-robust behaviour and validate locally '
|
|
392
|
+
+ 'where possible.'],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const notes = [];
|
|
396
|
+
const family = info.family || 'Unclassified';
|
|
397
|
+
const famBlock = (rel.families || {})[family] || {};
|
|
398
|
+
const metricsOut = [];
|
|
399
|
+
for (const [metricId, levels] of Object.entries(famBlock.metrics || {}).sort()) {
|
|
400
|
+
const sysE = levels.sys || {};
|
|
401
|
+
const segE = levels.seg || {};
|
|
402
|
+
metricsOut.push({
|
|
403
|
+
metric: metricId,
|
|
404
|
+
sys_pearson: sysE.pearson_weighted_mean ?? null,
|
|
405
|
+
sys_pairwise_accuracy: sysE.pairwise_accuracy_weighted_mean ?? null,
|
|
406
|
+
sys_n_pairs: sysE.n_pairs ?? null,
|
|
407
|
+
seg_kendall: segE.kendall_tau_b_weighted_mean ?? null,
|
|
408
|
+
seg_n_pairs: segE.n_pairs ?? null,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
metricsOut.sort((a, b) =>
|
|
412
|
+
(b.sys_pearson ?? -2) - (a.sys_pearson ?? -2)
|
|
413
|
+
|| a.metric.localeCompare(b.metric));
|
|
414
|
+
const exactPairs = [...new Set((rel.cells || [])
|
|
415
|
+
.filter((c) => c.tgt === code && c.preferred)
|
|
416
|
+
.map((c) => c.pair))].sort();
|
|
417
|
+
if (exactPairs.length === 0) {
|
|
418
|
+
notes.push(`Family-level metric evidence only: the '${family}' `
|
|
419
|
+
+ `correlations come from other ${family} target languages, never from `
|
|
420
|
+
+ `'${tgt}' itself — transfer within a family is an assumption, not a `
|
|
421
|
+
+ 'measurement.');
|
|
422
|
+
}
|
|
423
|
+
if ((rel.license_lane || {}).commercial_ok === false) {
|
|
424
|
+
notes.push('Metric-reliability evidence rides a non-commercial hold (the '
|
|
425
|
+
+ 'upstream WMT judgment data license is unstated, founder review '
|
|
426
|
+
+ 'pending) — cite it in research lanes only.');
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
section: {
|
|
430
|
+
target_code: code,
|
|
431
|
+
target_iso639_3: info.iso639_3 ?? null,
|
|
432
|
+
target_family: family,
|
|
433
|
+
exact_pairs_measured: exactPairs,
|
|
434
|
+
family_metrics: metricsOut,
|
|
435
|
+
provenance: rel.provenance ?? null,
|
|
436
|
+
},
|
|
437
|
+
notes,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Assembly
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Assemble the full recommendation payload for one directed pair.
|
|
447
|
+
*
|
|
448
|
+
* The payload shape mirrors the harness's `mt-eval recommend --json` output
|
|
449
|
+
* (snake_case keys) so machine consumers see one cross-runtime contract.
|
|
450
|
+
*
|
|
451
|
+
* @param {string} src
|
|
452
|
+
* @param {string} tgt
|
|
453
|
+
* @param {object} [opts]
|
|
454
|
+
* @param {string} [opts.useContext]
|
|
455
|
+
* @param {object|null} [opts.manifest] - Fixture for tests
|
|
456
|
+
* @param {object|null} [opts.curated] - Fixture for tests
|
|
457
|
+
* @param {object|null} [opts.bulk] - Fixture for tests
|
|
458
|
+
* @param {object|null} [opts.reliability] - Fixture for tests
|
|
459
|
+
* @param {Object<string,string>|null} [opts.env] - Fixture env for tests
|
|
460
|
+
* @param {string} [opts.cwd]
|
|
461
|
+
* @returns {object}
|
|
462
|
+
*/
|
|
463
|
+
function recommend(src, tgt, {
|
|
464
|
+
useContext = 'non-commercial',
|
|
465
|
+
manifest = undefined,
|
|
466
|
+
curated = undefined,
|
|
467
|
+
bulk = undefined,
|
|
468
|
+
reliability = undefined,
|
|
469
|
+
env = null,
|
|
470
|
+
cwd = undefined,
|
|
471
|
+
} = {}) {
|
|
472
|
+
const methods = dispatchableMethods(useContext, { manifest, env, cwd });
|
|
473
|
+
const { rows: curatedRows, methodsIndex: citedMethods } = curatedEvidence(src, tgt, curated);
|
|
474
|
+
const { rows: bulkRows, meta: bulkMeta } = bulkEvidence(src, tgt, { index: bulk });
|
|
475
|
+
const { section: reliabilitySection, notes: reliabilityNotes } =
|
|
476
|
+
metricReliabilityEvidence(tgt, reliability);
|
|
477
|
+
|
|
478
|
+
// Join: which cited models are dispatchable / commercially deployable?
|
|
479
|
+
const evidencedModels = [];
|
|
480
|
+
const seen = new Set();
|
|
481
|
+
for (const row of curatedRows) {
|
|
482
|
+
const ref = row.method_ref;
|
|
483
|
+
if (!ref || seen.has(ref)) continue;
|
|
484
|
+
seen.add(ref);
|
|
485
|
+
const m = citedMethods[ref] || {};
|
|
486
|
+
evidencedModels.push({
|
|
487
|
+
id: ref,
|
|
488
|
+
name: m.name ?? null,
|
|
489
|
+
runnable_in_champollion: m.runnable_in_champollion === true,
|
|
490
|
+
commercial_use: m.commercial_use ?? null,
|
|
491
|
+
license: m.license ?? null,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const notes = [
|
|
496
|
+
'Cited evidence orders methods RELATIVE to each other on memorized '
|
|
497
|
+
+ 'public benchmarks — it is never absolute quality and never a '
|
|
498
|
+
+ 'deployment ranking (relative-comparison-only lane).',
|
|
499
|
+
'Evidence and runnability are different axes: the best-evidenced '
|
|
500
|
+
+ 'model may not be dispatchable here, and NC-licensed weights are '
|
|
501
|
+
+ 'excluded from commercial-lane recommendations.',
|
|
502
|
+
];
|
|
503
|
+
if (curated === undefined && cataloguePath('external-results.json') === null) {
|
|
504
|
+
notes.push('Curated cited-results index not available in this install — '
|
|
505
|
+
+ 'tier skipped explicitly.');
|
|
506
|
+
}
|
|
507
|
+
if (bulk === undefined && cataloguePath('external-mt-index.json') === null) {
|
|
508
|
+
notes.push('Bulk published-results index not bundled in the npm package '
|
|
509
|
+
+ '(1.6 MB) — tier skipped explicitly. Browse it at '
|
|
510
|
+
+ 'https://champollion.dev/catalogue, or run from a monorepo checkout '
|
|
511
|
+
+ '(or `mt-eval recommend`) for the full index.');
|
|
512
|
+
}
|
|
513
|
+
if (curatedRows.length === 0 && bulkRows.length === 0) {
|
|
514
|
+
notes.push(
|
|
515
|
+
`NO published evidence indexed for ${src}→${tgt}. That is the `
|
|
516
|
+
+ 'honest answer — measure instead of guessing: '
|
|
517
|
+
+ `\`mt-eval corpora --source ${src} --target ${tgt}\` lists runnable `
|
|
518
|
+
+ 'benchmarks, `mt-eval run` produces your own scored evidence.');
|
|
519
|
+
}
|
|
520
|
+
notes.push(...reliabilityNotes);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
pair: { source: src, target: tgt },
|
|
524
|
+
use_context: useContext,
|
|
525
|
+
runnable_methods: methods,
|
|
526
|
+
curated_evidence: curatedRows,
|
|
527
|
+
bulk_evidence: bulkRows,
|
|
528
|
+
bulk_meta: bulkMeta,
|
|
529
|
+
evidenced_models: evidencedModels,
|
|
530
|
+
metric_reliability: reliabilitySection,
|
|
531
|
+
notes,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// Rendering
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Human-readable rendering of a recommend() payload (mirrors the harness's
|
|
541
|
+
* render_text, plus CLI-name and harness-only annotations).
|
|
542
|
+
*
|
|
543
|
+
* @param {object} payload
|
|
544
|
+
* @returns {string}
|
|
545
|
+
*/
|
|
546
|
+
function renderText(payload) {
|
|
547
|
+
const p = payload.pair;
|
|
548
|
+
const out = [
|
|
549
|
+
`Method guidance — ${p.source} → ${p.target} (lane: ${payload.use_context})`,
|
|
550
|
+
'',
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
out.push('Runnable methods (shared/method-registry.json):');
|
|
554
|
+
if (payload.runnable_methods.length === 0) {
|
|
555
|
+
out.push(' (method registry not available in this install)');
|
|
556
|
+
}
|
|
557
|
+
const badges = {
|
|
558
|
+
'ready': 'READY ',
|
|
559
|
+
'needs-key': 'NEEDS KEY ',
|
|
560
|
+
'local-setup': 'LOCAL ',
|
|
561
|
+
};
|
|
562
|
+
for (const m of payload.runnable_methods) {
|
|
563
|
+
// Show the name a champollion.config.json actually uses when it differs
|
|
564
|
+
// from the canonical registry name (e.g. openrouter → llm).
|
|
565
|
+
const shown = m.cli_name && m.cli_name !== m.method
|
|
566
|
+
? `${m.method} (cli: ${m.cli_name})`
|
|
567
|
+
: m.method;
|
|
568
|
+
let line;
|
|
569
|
+
if (!m.lane_ok) {
|
|
570
|
+
line = ` EXCLUDED ${shown.padEnd(22)}${m.lane_note}`;
|
|
571
|
+
} else {
|
|
572
|
+
const badge = badges[m.availability] || '? ';
|
|
573
|
+
line = ` ${badge}${shown.padEnd(22)}${m.availability_detail}`;
|
|
574
|
+
if (m.license) line += ` [${m.license}]`;
|
|
575
|
+
}
|
|
576
|
+
out.push(line);
|
|
577
|
+
if (m.runtime_note) out.push(` ↳ ${m.runtime_note}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
out.push('');
|
|
581
|
+
out.push('Published evidence for this pair (cited — never reproduced '
|
|
582
|
+
+ 'by us; relative ordering only):');
|
|
583
|
+
if (payload.curated_evidence.length === 0 && payload.bulk_evidence.length === 0) {
|
|
584
|
+
out.push(' none indexed.');
|
|
585
|
+
}
|
|
586
|
+
for (const r of payload.curated_evidence) {
|
|
587
|
+
const v = r.verified ? 'verified' : 'UNVERIFIED';
|
|
588
|
+
out.push(` [curated/${v}] ${r.benchmark} ${r.metric}=${r.value} — `
|
|
589
|
+
+ `${r.model} (grade ${r.grade}; ${r.citation})`);
|
|
590
|
+
}
|
|
591
|
+
for (const r of payload.bulk_evidence) {
|
|
592
|
+
out.push(` [bulk] ${r.benchmark} ${r.metric}=${r.value} — ${r.model} `
|
|
593
|
+
+ `(pair key ${r.pair_key})`);
|
|
594
|
+
}
|
|
595
|
+
if (payload.bulk_meta.truncated) {
|
|
596
|
+
out.push(` … ${payload.bulk_meta.total_rows} bulk rows total `
|
|
597
|
+
+ '(showing best-per-bucket head).');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (payload.evidenced_models.length > 0) {
|
|
601
|
+
out.push('');
|
|
602
|
+
out.push('Evidenced models vs. dispatchability:');
|
|
603
|
+
for (const m of payload.evidenced_models) {
|
|
604
|
+
const bits = [];
|
|
605
|
+
bits.push(m.runnable_in_champollion
|
|
606
|
+
? 'runnable in Champollion'
|
|
607
|
+
: 'NOT dispatchable here yet');
|
|
608
|
+
if (m.commercial_use === false) bits.push('weights NC — non-commercial only');
|
|
609
|
+
out.push(` ${m.name || m.id}: ${bits.join('; ')}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const rel = payload.metric_reliability;
|
|
614
|
+
if (rel) {
|
|
615
|
+
const fmt = (v) => (v === null || v === undefined
|
|
616
|
+
? ' —'
|
|
617
|
+
: `${v >= 0 ? '+' : ''}${v.toFixed(2)}`);
|
|
618
|
+
out.push('');
|
|
619
|
+
out.push(`Metric trust for the target (family: ${rel.target_family} — `
|
|
620
|
+
+ 'WMT human-judgment correlations; which metric to believe):');
|
|
621
|
+
for (const m of rel.family_metrics) {
|
|
622
|
+
const n = m.sys_n_pairs ?? m.seg_n_pairs ?? 0;
|
|
623
|
+
out.push(` ${m.metric.padEnd(18)} sys-Pearson ${fmt(m.sys_pearson)} `
|
|
624
|
+
+ `seg-Kendall ${fmt(m.seg_kendall)} (${n} pair(s))`);
|
|
625
|
+
}
|
|
626
|
+
const measured = rel.exact_pairs_measured.length > 0
|
|
627
|
+
? rel.exact_pairs_measured.join(', ')
|
|
628
|
+
: 'none — family-level only';
|
|
629
|
+
out.push(` directly measured pairs for this target: ${measured}`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
out.push('');
|
|
633
|
+
for (const n of payload.notes) {
|
|
634
|
+
out.push(`⚠ ${n}`);
|
|
635
|
+
}
|
|
636
|
+
return out.join('\n');
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export {
|
|
640
|
+
cataloguePath,
|
|
641
|
+
resolveAvailability,
|
|
642
|
+
dispatchableMethods,
|
|
643
|
+
curatedEvidence,
|
|
644
|
+
bulkEvidence,
|
|
645
|
+
metricReliabilityEvidence,
|
|
646
|
+
recommend,
|
|
647
|
+
renderText,
|
|
648
|
+
};
|