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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: status
|
|
3
|
+
*
|
|
4
|
+
* Shows the translation pair graph, config summary, installed plugins,
|
|
5
|
+
* TM cache stats, and benchmark scores. A diagnostic tool for understanding
|
|
6
|
+
* how the project is configured before running sync.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolveConfig, autoDetectLanguages } from '../config.js';
|
|
10
|
+
import { resolvePairs, QUALITY_TIERS } from '../pairs.js';
|
|
11
|
+
import { loadPlugins, resolvePluginForPair } from '../plugins.js';
|
|
12
|
+
import { DEFAULT_REGISTERS, getLanguageCard, getRegisterPresets, DEFAULT_REGISTER_FALLBACK } from '../registers.js';
|
|
13
|
+
import { loadTM, tmSize, TM_DIR, TM_FILENAME } from '../tm.js';
|
|
14
|
+
import { getMethod } from '../translate.js';
|
|
15
|
+
import { output } from '../output.js';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
// CLI version — read from package.json (the single source of truth), never a
|
|
20
|
+
// hardcoded literal. Mirrors bin/cli.js's --version read.
|
|
21
|
+
const { version: CLI_VERSION } = JSON.parse(
|
|
22
|
+
fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
|
|
27
|
+
* @param {string} cwd - Working directory
|
|
28
|
+
* @returns {Promise<number>} Exit code (0 = success, 1 = error)
|
|
29
|
+
*/
|
|
30
|
+
async function run(args, cwd) {
|
|
31
|
+
// --json: stdout carries exactly one JSON document. Quiet mode keeps the
|
|
32
|
+
// human status display off stdout.
|
|
33
|
+
const json = !!args.json;
|
|
34
|
+
if (json) output.setMode('quiet');
|
|
35
|
+
|
|
36
|
+
const config = resolveConfig(args, cwd);
|
|
37
|
+
|
|
38
|
+
// Auto-detect languages if not configured
|
|
39
|
+
let languages = config.resolvedLanguages;
|
|
40
|
+
if (Object.keys(languages).length === 0) {
|
|
41
|
+
languages = autoDetectLanguages(config);
|
|
42
|
+
}
|
|
43
|
+
config.resolvedLanguages = languages;
|
|
44
|
+
|
|
45
|
+
const pairs = resolvePairs(config);
|
|
46
|
+
|
|
47
|
+
// Load installed plugins to enrich status display
|
|
48
|
+
const plugins = loadPlugins(cwd);
|
|
49
|
+
|
|
50
|
+
if (json) {
|
|
51
|
+
return runJson(config, pairs, plugins, cwd);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
output.raw('\n champollion v3 — Translation Status\n');
|
|
55
|
+
output.raw(` Input locale: ${config.inputLocale}`);
|
|
56
|
+
output.raw(` Locales dir: ${config.localesDir}`);
|
|
57
|
+
output.raw(` Default model: ${config.model}`);
|
|
58
|
+
if (config.temperature != null) {
|
|
59
|
+
output.raw(` Temperature: ${config.temperature}`);
|
|
60
|
+
}
|
|
61
|
+
output.raw(` Config version: ${config.version || '2 (legacy)'}`);
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// Show installed plugins summary
|
|
65
|
+
if (plugins.size > 0) {
|
|
66
|
+
output.raw(` Plugins: ${plugins.size} installed`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Show Translation Memory status — gives users immediate feedback
|
|
70
|
+
// that caching is working and how much is cached.
|
|
71
|
+
const tmPath = path.join(cwd, TM_DIR, TM_FILENAME);
|
|
72
|
+
if (fs.existsSync(tmPath)) {
|
|
73
|
+
const tm = loadTM(cwd);
|
|
74
|
+
const count = tmSize(tm);
|
|
75
|
+
const stat = fs.statSync(tmPath);
|
|
76
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
77
|
+
output.raw(` TM cache: ${count.toLocaleString()} entries (${sizeKB} KB)`);
|
|
78
|
+
} else {
|
|
79
|
+
output.raw(' TM cache: empty (will populate on first sync)');
|
|
80
|
+
}
|
|
81
|
+
output.raw('');
|
|
82
|
+
|
|
83
|
+
if (pairs.size === 0) {
|
|
84
|
+
output.raw(' No translation pairs configured.');
|
|
85
|
+
output.raw(' Run `champollion init` or add languages to your config.\n');
|
|
86
|
+
} else {
|
|
87
|
+
output.raw(` Translation Pairs (${pairs.size}):\n`);
|
|
88
|
+
for (const [pairKey, pairConfig] of pairs) {
|
|
89
|
+
const tier = QUALITY_TIERS[pairConfig.qualityTier];
|
|
90
|
+
const tierLabel = tier ? tier.label : pairConfig.qualityTier;
|
|
91
|
+
const dirLabel = pairConfig.dir === 'rtl' ? ' [RTL]' : '';
|
|
92
|
+
// Show the RESOLVED script decision, not the converter's existence.
|
|
93
|
+
// The old label printed the registry key ("[crk]"), which read as if
|
|
94
|
+
// conversion were happening whether or not it was.
|
|
95
|
+
const res = pairConfig.scriptResolution;
|
|
96
|
+
let scriptLabel = '';
|
|
97
|
+
if (res?.converterKey) scriptLabel = ` [script: ${res.script ?? res.converterKey}]`;
|
|
98
|
+
else if (res?.source === 'choice-required') scriptLabel = ' [script: CHOICE REQUIRED — see `champollion sync`]';
|
|
99
|
+
else if (res?.source === 'default' && pairConfig.scripts) scriptLabel = ` [script: ${res.script} (converter available)]`;
|
|
100
|
+
// Display arrow is a UI-only semantic element, not a data separator
|
|
101
|
+
output.raw(` ${pairKey} → ${pairConfig.name}${dirLabel}${scriptLabel}`);
|
|
102
|
+
|
|
103
|
+
// Resolve display model — direct providers may have model=null in the
|
|
104
|
+
// pair config, meaning the method class picks its own default at runtime.
|
|
105
|
+
// Show the resolved model so users know what will actually be used.
|
|
106
|
+
let displayModel = pairConfig.model;
|
|
107
|
+
if (!displayModel && pairConfig.method !== 'api') {
|
|
108
|
+
try {
|
|
109
|
+
const method = getMethod(pairConfig.method);
|
|
110
|
+
displayModel = method._getDefaultModel?.() || 'auto';
|
|
111
|
+
} catch { displayModel = 'auto'; }
|
|
112
|
+
}
|
|
113
|
+
const modelStr = pairConfig.method === 'api' ? '' : ` | model: ${displayModel || 'auto'}`;
|
|
114
|
+
|
|
115
|
+
// Show benchmarks if plugin has them; otherwise show tier as self-reported
|
|
116
|
+
let qualityStr = '';
|
|
117
|
+
if (pairConfig.methodPlugin) {
|
|
118
|
+
const resolvedPair = resolvePluginForPair(plugins, pairConfig);
|
|
119
|
+
const benchmarks = resolvedPair.pluginBenchmarks;
|
|
120
|
+
if (benchmarks && benchmarks[pairConfig.target]) {
|
|
121
|
+
const bm = benchmarks[pairConfig.target];
|
|
122
|
+
const parts = [];
|
|
123
|
+
if (bm.corpus_chrf) parts.push(`chrF++ ${bm.corpus_chrf}`);
|
|
124
|
+
if (bm.exact_match_rate) parts.push(`exact ${Math.round(bm.exact_match_rate * 100)}%`);
|
|
125
|
+
qualityStr = ` | benchmarks: ${parts.join(', ')}`;
|
|
126
|
+
} else {
|
|
127
|
+
qualityStr = ` | quality: ${tierLabel} (self-reported, no benchmarks)`;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
qualityStr = ` | quality: ${tierLabel}`;
|
|
131
|
+
}
|
|
132
|
+
output.raw(` method: ${pairConfig.method}${modelStr}${qualityStr}`);
|
|
133
|
+
|
|
134
|
+
// Plugin badge — show name and version (benchmarks already shown above)
|
|
135
|
+
if (pairConfig.methodPlugin) {
|
|
136
|
+
const resolvedPair = resolvePluginForPair(plugins, pairConfig);
|
|
137
|
+
if (resolvedPair.pluginName) {
|
|
138
|
+
let pluginLine = ` [PLUGIN] ${resolvedPair.pluginName}`;
|
|
139
|
+
if (resolvedPair.pluginVersion) pluginLine += ` v${resolvedPair.pluginVersion}`;
|
|
140
|
+
output.raw(pluginLine);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// API badge
|
|
145
|
+
if (pairConfig.method === 'api') {
|
|
146
|
+
output.raw(' [API] Translation runs server-side (IP protected)');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Google Translate badge
|
|
150
|
+
if (pairConfig.method === 'google-translate') {
|
|
151
|
+
output.raw(' Google Cloud Translation API');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
output.raw('');
|
|
155
|
+
|
|
156
|
+
// Show active registers for each pair with structured card info
|
|
157
|
+
output.raw(' Registers:\n');
|
|
158
|
+
for (const [pairKey, pairConfig] of pairs) {
|
|
159
|
+
const card = getLanguageCard(pairConfig.target);
|
|
160
|
+
const presets = getRegisterPresets(pairConfig.target);
|
|
161
|
+
const systemLabel = card?.formality?.system ? ` (${card.formality.system})` : '';
|
|
162
|
+
|
|
163
|
+
// Use registerPreset for direct lookup — no fragile prompt text matching
|
|
164
|
+
const presetKey = pairConfig.registerPreset;
|
|
165
|
+
const matchedPreset = presetKey
|
|
166
|
+
? presets.find(p => p.key === presetKey)
|
|
167
|
+
: null;
|
|
168
|
+
|
|
169
|
+
if (matchedPreset) {
|
|
170
|
+
// Known preset — show key, label, and formality system
|
|
171
|
+
const marker = matchedPreset.isDefault ? ' ★' : '';
|
|
172
|
+
output.raw(` ${pairConfig.target} ${matchedPreset.key}${marker}${systemLabel} — ${matchedPreset.label}`);
|
|
173
|
+
} else {
|
|
174
|
+
// Custom register text — show truncated text
|
|
175
|
+
const regText = pairConfig.register || DEFAULT_REGISTER_FALLBACK;
|
|
176
|
+
const truncated = regText.length > 55 ? regText.slice(0, 52) + '...' : regText;
|
|
177
|
+
output.raw(` ${pairConfig.target} (custom)${systemLabel} — "${truncated}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
output.raw('');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* --json: assemble everything the human display computes into a single
|
|
188
|
+
* JSON document — config summary, TM cache stats, pair graph with resolved
|
|
189
|
+
* models/tiers/plugins, and per-pair register info.
|
|
190
|
+
*/
|
|
191
|
+
function runJson(config, pairs, plugins, cwd) {
|
|
192
|
+
const tmPath = path.join(cwd, TM_DIR, TM_FILENAME);
|
|
193
|
+
let tm = { exists: false, entries: 0, sizeBytes: 0 };
|
|
194
|
+
if (fs.existsSync(tmPath)) {
|
|
195
|
+
const stat = fs.statSync(tmPath);
|
|
196
|
+
tm = { exists: true, entries: tmSize(loadTM(cwd)), sizeBytes: stat.size };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const pairList = [];
|
|
200
|
+
for (const [pairKey, pairConfig] of pairs) {
|
|
201
|
+
const tier = QUALITY_TIERS[pairConfig.qualityTier];
|
|
202
|
+
const resolvedPair = resolvePluginForPair(plugins, pairConfig);
|
|
203
|
+
|
|
204
|
+
// Same display-model resolution as the human path: direct providers may
|
|
205
|
+
// have model=null (the method class picks its own default at runtime).
|
|
206
|
+
let displayModel = pairConfig.model;
|
|
207
|
+
if (!displayModel && pairConfig.method !== 'api') {
|
|
208
|
+
try {
|
|
209
|
+
const method = getMethod(pairConfig.method);
|
|
210
|
+
displayModel = method._getDefaultModel?.() || 'auto';
|
|
211
|
+
} catch { displayModel = 'auto'; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const presets = getRegisterPresets(pairConfig.target);
|
|
215
|
+
const matchedPreset = pairConfig.registerPreset
|
|
216
|
+
? presets.find(p => p.key === pairConfig.registerPreset)
|
|
217
|
+
: null;
|
|
218
|
+
|
|
219
|
+
pairList.push({
|
|
220
|
+
pair: pairKey,
|
|
221
|
+
target: pairConfig.target,
|
|
222
|
+
name: pairConfig.name,
|
|
223
|
+
method: pairConfig.method,
|
|
224
|
+
model: pairConfig.method === 'api' ? null : (displayModel || 'auto'),
|
|
225
|
+
qualityTier: pairConfig.qualityTier,
|
|
226
|
+
qualityTierLabel: tier ? tier.label : pairConfig.qualityTier,
|
|
227
|
+
dir: pairConfig.dir || 'ltr',
|
|
228
|
+
scripts: pairConfig.scripts || null,
|
|
229
|
+
// Resolved script decision (kept alongside legacy `scripts` for compat)
|
|
230
|
+
script: pairConfig.scriptResolution
|
|
231
|
+
? {
|
|
232
|
+
resolved: pairConfig.scriptResolution.script,
|
|
233
|
+
source: pairConfig.scriptResolution.source,
|
|
234
|
+
converts: !!pairConfig.scriptResolution.converterKey,
|
|
235
|
+
}
|
|
236
|
+
: null,
|
|
237
|
+
plugin: resolvedPair.pluginName
|
|
238
|
+
? { name: resolvedPair.pluginName, version: resolvedPair.pluginVersion || null }
|
|
239
|
+
: null,
|
|
240
|
+
benchmarks: resolvedPair.pluginBenchmarks?.[pairConfig.target] || null,
|
|
241
|
+
register: matchedPreset
|
|
242
|
+
? { preset: matchedPreset.key, label: matchedPreset.label, isDefault: !!matchedPreset.isDefault }
|
|
243
|
+
: { preset: null, custom: pairConfig.register || DEFAULT_REGISTER_FALLBACK },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log(JSON.stringify({
|
|
248
|
+
command: 'status',
|
|
249
|
+
version: CLI_VERSION,
|
|
250
|
+
inputLocale: config.inputLocale,
|
|
251
|
+
localesDir: config.localesDir,
|
|
252
|
+
defaultModel: config.model,
|
|
253
|
+
temperature: config.temperature ?? null,
|
|
254
|
+
configVersion: config.version || '2 (legacy)',
|
|
255
|
+
format: config.format,
|
|
256
|
+
contentDir: config.contentDir || null,
|
|
257
|
+
plugins: plugins.size,
|
|
258
|
+
tm,
|
|
259
|
+
pairs: pairList,
|
|
260
|
+
}, null, 2));
|
|
261
|
+
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { run };
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: submit
|
|
3
|
+
*
|
|
4
|
+
* The low-friction CLI front door to Champollion's REVIEW-GATED community
|
|
5
|
+
* submission path. It gathers the fields for a chosen submission type and
|
|
6
|
+
* prints a PRE-FILLED GitHub issue URL (optionally also writing a local
|
|
7
|
+
* submission JSON). The GitHub issue IS the human-review queue: a maintainer
|
|
8
|
+
* reviews each one against IP / license / sovereignty rules before adding the
|
|
9
|
+
* entry to the relevant source-of-truth. Nothing here is auto-approved, and
|
|
10
|
+
* this command never writes to a registry, a card, or a database.
|
|
11
|
+
*
|
|
12
|
+
* Six submission types (see lib/submit.mjs / .github/ISSUE_TEMPLATE/):
|
|
13
|
+
* dataset · resource · method · human-service · external-result · card-correction
|
|
14
|
+
*
|
|
15
|
+
* Interactive wizard when stdin is a TTY; fully scriptable via flags otherwise
|
|
16
|
+
* (--type + --values/--field + --attest, or --yes). See `champollion submit --help`.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes: 0 = URL produced (or cancelled); 1 = invalid/incomplete request.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import readline from 'node:readline';
|
|
24
|
+
import { output } from '../output.js';
|
|
25
|
+
import {
|
|
26
|
+
SUBMISSION_TYPES,
|
|
27
|
+
ATTESTATION_TEXT,
|
|
28
|
+
DEFAULT_REPO_BASE,
|
|
29
|
+
resolveType,
|
|
30
|
+
repoBaseFromRepository,
|
|
31
|
+
validateSubmission,
|
|
32
|
+
buildIssueUrl,
|
|
33
|
+
buildSubmissionRecord,
|
|
34
|
+
} from '../submit.mjs';
|
|
35
|
+
|
|
36
|
+
function isInteractive() {
|
|
37
|
+
return process.stdin.isTTY === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ask(rl, question, defaultValue) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
43
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
44
|
+
resolve(answer.trim() || defaultValue || '');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve the repo base from cli/package.json, falling back to the default. */
|
|
50
|
+
function resolveRepoBase(args) {
|
|
51
|
+
if (args.repo) return repoBaseFromRepository(args.repo);
|
|
52
|
+
try {
|
|
53
|
+
const pkgUrl = new URL('../../package.json', import.meta.url);
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgUrl, 'utf-8'));
|
|
55
|
+
const repoUrl = pkg?.repository?.url || pkg?.repository;
|
|
56
|
+
return repoBaseFromRepository(repoUrl);
|
|
57
|
+
} catch {
|
|
58
|
+
return DEFAULT_REPO_BASE;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const BOOL_RE = /^(true|yes|1|y|on)$/i;
|
|
63
|
+
|
|
64
|
+
/** Coerce any string-y bool field values (from --values/--field) to real booleans. */
|
|
65
|
+
function coerceBools(type, values) {
|
|
66
|
+
for (const f of type.fields) {
|
|
67
|
+
if (f.kind === 'bool' && typeof values[f.id] === 'string') {
|
|
68
|
+
values[f.id] = BOOL_RE.test(values[f.id].trim());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return values;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Interactive wizard ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async function pickType(rl, args) {
|
|
77
|
+
const pre = resolveType(args.type);
|
|
78
|
+
if (pre) return pre;
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(' What are you submitting?');
|
|
81
|
+
console.log('');
|
|
82
|
+
SUBMISSION_TYPES.forEach((t, i) => {
|
|
83
|
+
console.log(` ${i + 1}. ${t.label}`);
|
|
84
|
+
console.log(` ${t.blurb}`);
|
|
85
|
+
});
|
|
86
|
+
console.log('');
|
|
87
|
+
const choice = await ask(rl, 'Choose a type (number or name)', '1');
|
|
88
|
+
return resolveType(choice) || SUBMISSION_TYPES[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function runInteractive(rl, args) {
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(' champollion — Submit to the index');
|
|
94
|
+
console.log(' ════════════════════════════════════════════════');
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(' Every submission is HUMAN-REVIEWED for IP / license / sovereignty');
|
|
97
|
+
console.log(' compliance before anything is added — nothing is auto-approved. This');
|
|
98
|
+
console.log(' command just gathers the fields and hands you a pre-filled GitHub issue.');
|
|
99
|
+
|
|
100
|
+
const type = await pickType(rl, args);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(` ${type.label}`);
|
|
103
|
+
console.log(' ────────────────────────────────────────────────');
|
|
104
|
+
|
|
105
|
+
const values = {};
|
|
106
|
+
for (const f of type.fields) {
|
|
107
|
+
if (f.kind === 'bool') continue; // handled together at the end
|
|
108
|
+
if (f.kind === 'choice') {
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(` ${f.label}${f.required ? ' (required)' : ''}`);
|
|
111
|
+
f.choices.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
|
|
112
|
+
const pick = await ask(rl, 'Choose', '');
|
|
113
|
+
if (pick) {
|
|
114
|
+
const idx = /^\d+$/.test(pick) ? Number(pick) - 1 : f.choices.findIndex((c) => c.toLowerCase() === pick.toLowerCase());
|
|
115
|
+
values[f.id] = f.choices[idx] != null ? f.choices[idx] : pick;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
let v = await ask(rl, `${f.label}${f.required ? ' (required)' : ''}`, '');
|
|
119
|
+
if (!v && f.required) v = await ask(rl, `↳ ${f.id} is required`, '');
|
|
120
|
+
if (v) values[f.id] = v;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Confirmations (attestation + any per-type consent) — must be affirmative.
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(' Confirmation');
|
|
127
|
+
console.log(' ────────────────────────────────────────────────');
|
|
128
|
+
for (const f of type.fields) {
|
|
129
|
+
if (f.kind !== 'bool') continue;
|
|
130
|
+
const yn = await ask(rl, `${f.label}\n Confirm? (y/N)`, 'N');
|
|
131
|
+
values[f.id] = BOOL_RE.test(yn.trim());
|
|
132
|
+
}
|
|
133
|
+
return { type, values };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Non-interactive (flags) ─────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function gatherFromFlags(type, args) {
|
|
139
|
+
const values = {};
|
|
140
|
+
if (args.values) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(args.values);
|
|
143
|
+
if (parsed && typeof parsed === 'object') Object.assign(values, parsed);
|
|
144
|
+
} catch {
|
|
145
|
+
// Surface as a validation error path: leave values empty so required
|
|
146
|
+
// fields are reported missing rather than silently swallowing bad JSON.
|
|
147
|
+
output.warn('--values was not valid JSON; ignoring it.');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const fieldFlags = args.field == null ? [] : (Array.isArray(args.field) ? args.field : [args.field]);
|
|
151
|
+
for (const pair of fieldFlags) {
|
|
152
|
+
const s = String(pair);
|
|
153
|
+
const eq = s.indexOf('=');
|
|
154
|
+
if (eq === -1) continue;
|
|
155
|
+
values[s.slice(0, eq).trim()] = s.slice(eq + 1);
|
|
156
|
+
}
|
|
157
|
+
// --attest confirms every required confirmation at once; --consent is a
|
|
158
|
+
// narrower alias for the human-service provider-consent box.
|
|
159
|
+
if (args.attest) for (const f of type.fields) if (f.kind === 'bool') values[f.id] = true;
|
|
160
|
+
if (args.consent) values.consent = true;
|
|
161
|
+
return coerceBools(type, values);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function run(args, cwd) {
|
|
167
|
+
if (args.help) {
|
|
168
|
+
showHelp();
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
if (args.list) {
|
|
172
|
+
if (args.json) {
|
|
173
|
+
console.log(JSON.stringify({
|
|
174
|
+
types: SUBMISSION_TYPES.map((t) => ({
|
|
175
|
+
key: t.key, label: t.label, template: t.template,
|
|
176
|
+
target_ssot: t.ssot,
|
|
177
|
+
fields: t.fields.map((f) => ({ id: f.id, kind: f.kind, required: !!f.required })),
|
|
178
|
+
})),
|
|
179
|
+
attestation: ATTESTATION_TEXT,
|
|
180
|
+
}, null, 2));
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(' Submission types (champollion submit --type <key>):');
|
|
185
|
+
console.log('');
|
|
186
|
+
SUBMISSION_TYPES.forEach((t, i) => {
|
|
187
|
+
console.log(` ${i + 1}. ${t.key} — ${t.label}`);
|
|
188
|
+
console.log(` ${t.blurb}`);
|
|
189
|
+
console.log(` → reviewed, then added to: ${t.ssot}`);
|
|
190
|
+
});
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(' Every submission is human-reviewed; nothing is auto-approved.');
|
|
193
|
+
console.log('');
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const repoBase = resolveRepoBase(args);
|
|
198
|
+
|
|
199
|
+
// Gather the submission (interactive wizard or flags).
|
|
200
|
+
let type;
|
|
201
|
+
let values;
|
|
202
|
+
if (!args.yes && isInteractive()) {
|
|
203
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
204
|
+
try {
|
|
205
|
+
({ type, values } = await runInteractive(rl, args));
|
|
206
|
+
} finally {
|
|
207
|
+
rl.close();
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
type = resolveType(args.type);
|
|
211
|
+
if (!type) {
|
|
212
|
+
output.error('Choose a submission type with --type (run `champollion submit --list`).');
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
values = gatherFromFlags(type, args);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Validate — collect every problem, fail loud.
|
|
219
|
+
const v = validateSubmission(type, values);
|
|
220
|
+
if (!v.ok) {
|
|
221
|
+
console.error('[ERR] Cannot build this submission:');
|
|
222
|
+
for (const e of v.errors) console.error(` • ${e}`);
|
|
223
|
+
console.error('');
|
|
224
|
+
console.error(' The compliance attestation is required. Re-run and confirm it');
|
|
225
|
+
console.error(' (interactively, or pass --attest with the other fields).');
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const issueUrl = buildIssueUrl({ type, values, repoBase });
|
|
230
|
+
const generatedAt = new Date().toISOString();
|
|
231
|
+
const record = buildSubmissionRecord({ type, values, repoBase, generatedAt });
|
|
232
|
+
|
|
233
|
+
// Optionally save a local content-free copy.
|
|
234
|
+
let localPath = null;
|
|
235
|
+
if (args.out) {
|
|
236
|
+
localPath = path.resolve(cwd, args.out);
|
|
237
|
+
try {
|
|
238
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
239
|
+
fs.writeFileSync(localPath, JSON.stringify(record, null, 2) + '\n', 'utf-8');
|
|
240
|
+
} catch (e) {
|
|
241
|
+
output.error(`Could not write the local submission file to ${localPath}: ${e.message}`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Report ──
|
|
247
|
+
if (args.json) {
|
|
248
|
+
console.log(JSON.stringify({
|
|
249
|
+
ok: true,
|
|
250
|
+
type: type.key,
|
|
251
|
+
target_ssot: type.ssot,
|
|
252
|
+
title: `${type.titlePrefix}${values[type.fields.find((f) => f.primary)?.id] ?? ''}`.trimEnd(),
|
|
253
|
+
labels: type.labels,
|
|
254
|
+
attestationConfirmed: values.attestation === true,
|
|
255
|
+
issueUrl,
|
|
256
|
+
localPath,
|
|
257
|
+
review: 'human-reviewed — not auto-approved',
|
|
258
|
+
}, null, 2));
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log('');
|
|
263
|
+
output.ok(`Submission ready: ${type.label}`);
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(' This will be HUMAN-REVIEWED for IP / license / sovereignty compliance');
|
|
266
|
+
console.log(' before anything is added to the index — nothing is auto-approved.');
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(' Open this pre-filled GitHub issue to file it (review the attestation box');
|
|
269
|
+
console.log(' in the browser, then submit):');
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log(` ${issueUrl}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
if (localPath) {
|
|
274
|
+
console.log(` Saved a local copy: ${localPath}`);
|
|
275
|
+
console.log('');
|
|
276
|
+
}
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function showHelp() {
|
|
281
|
+
console.log(`
|
|
282
|
+
champollion submit — Propose an entry for the Champollion index (review-gated)
|
|
283
|
+
|
|
284
|
+
USAGE
|
|
285
|
+
champollion submit [options]
|
|
286
|
+
|
|
287
|
+
DESCRIPTION
|
|
288
|
+
Gather the fields for a submission and print a PRE-FILLED GitHub issue URL
|
|
289
|
+
(optionally writing a local submission JSON). The GitHub issue is the
|
|
290
|
+
human-review queue: a maintainer reviews every submission against IP /
|
|
291
|
+
license / sovereignty rules before adding it to a source-of-truth. Nothing
|
|
292
|
+
is auto-approved, and this command never writes to a registry or database.
|
|
293
|
+
|
|
294
|
+
Interactive when run in a terminal; fully scriptable with flags (or --yes).
|
|
295
|
+
|
|
296
|
+
SUBMISSION TYPES (--type)
|
|
297
|
+
dataset A benchmark / evaluation corpus (metadata + fetch pointer only).
|
|
298
|
+
resource A dictionary / archive / app / FST / tool (a pointer, not a copy).
|
|
299
|
+
method A translation method / MT engine / LLM provider.
|
|
300
|
+
human-service An opt-in human translation provider (contact PII stays out-of-band).
|
|
301
|
+
external-result A published result from another system/paper (cited, never re-hosted).
|
|
302
|
+
|
|
303
|
+
OPTIONS
|
|
304
|
+
--type <key> Submission type (see --list). Required when non-interactive.
|
|
305
|
+
--values <json> JSON object of field id → value (e.g. '{"dataset-name":"X"}').
|
|
306
|
+
--field <id=val> Set one field (repeatable): --field source-url=https://…
|
|
307
|
+
--attest Confirm the required compliance attestation (and consent).
|
|
308
|
+
--consent human-service: confirm the provider's listing consent.
|
|
309
|
+
--out <path> Also write a local content-free submission JSON here.
|
|
310
|
+
--repo <url> Override the GitHub repo for the issue URL (advanced/testing).
|
|
311
|
+
--list List the submission types (add --json for JSON).
|
|
312
|
+
--json Machine-readable output (for agents).
|
|
313
|
+
--yes Non-interactive; take values from flags.
|
|
314
|
+
|
|
315
|
+
ATTESTATION (required on every submission)
|
|
316
|
+
"${ATTESTATION_TEXT}"
|
|
317
|
+
|
|
318
|
+
EXAMPLES
|
|
319
|
+
champollion submit # interactive wizard
|
|
320
|
+
champollion submit --list # see the types
|
|
321
|
+
# A dataset submission, fully from flags:
|
|
322
|
+
champollion submit --yes --type dataset --attest \\
|
|
323
|
+
--field dataset-name="GlobalVoices eng-amh" \\
|
|
324
|
+
--field pairs=eng-amh --field license=CC-BY-4.0 \\
|
|
325
|
+
--field source-url=https://globalvoices.org
|
|
326
|
+
# An external result, with a saved local copy:
|
|
327
|
+
champollion submit --yes --type external-result --attest --out ./submission.json \\
|
|
328
|
+
--values '{"system-name":"NLLB-200","pairs":"eng-crk","dataset":"FLORES-200","metric":"chrF++","score":"28.4","citation":"https://arxiv.org/abs/2207.04672"}'
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { run };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: sync
|
|
3
|
+
*
|
|
4
|
+
* Translates & syncs all locale files based on the project config.
|
|
5
|
+
* Delegates entirely to lib/sync.runSync — this module just bridges
|
|
6
|
+
* CLI arguments to the sync engine's options object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runSync } from '../sync.js';
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map a runSync result to a process exit code.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes (documented contract for CI pipelines + agents):
|
|
16
|
+
* 0 — clean: everything translated, verification passed.
|
|
17
|
+
* 1 — catastrophic: failures occurred and NOTHING was translated.
|
|
18
|
+
* This is the bad-key / 401 case — every locale failed, so
|
|
19
|
+
* totalProcessed is 0. The old gate (`totalFailed > 0 &&
|
|
20
|
+
* totalProcessed > 0`) returned 0 here, so a `sync` wired into a
|
|
21
|
+
* pre-deploy hook went green while silently shipping English
|
|
22
|
+
* fallbacks. Any failure with zero progress must be loud.
|
|
23
|
+
* 2 — partial: some keys translated but others failed, OR verification
|
|
24
|
+
* found errors. The run did real work but isn't clean.
|
|
25
|
+
* ALSO used for a --max-cost abort: the pre-run estimate exceeded the
|
|
26
|
+
* cap (or was unknowable — unknown ≠ free), so the sync deliberately
|
|
27
|
+
* stopped BEFORE any API call. Not catastrophic (nothing broke,
|
|
28
|
+
* nothing was spent), but definitely not a clean pass either.
|
|
29
|
+
*
|
|
30
|
+
* Pure and total (never throws) so it can be unit-tested directly.
|
|
31
|
+
*
|
|
32
|
+
* @param {{ totalProcessed?: number, totalFailed?: number, verifyErrors?: number, maxCostAborted?: boolean } | null | undefined} result
|
|
33
|
+
* @returns {number} exit code
|
|
34
|
+
*/
|
|
35
|
+
function computeExitCode(result) {
|
|
36
|
+
if (!result) return 0;
|
|
37
|
+
|
|
38
|
+
// --max-cost abort: deliberate pre-run stop, exit 2 by documented contract.
|
|
39
|
+
if (result.maxCostAborted) return 2;
|
|
40
|
+
|
|
41
|
+
const processed = result.totalProcessed || 0;
|
|
42
|
+
const failed = result.totalFailed || 0;
|
|
43
|
+
const verifyErrors = result.verifyErrors || 0;
|
|
44
|
+
|
|
45
|
+
// Catastrophic: failures with no successful work. A fully-failed sync
|
|
46
|
+
// (bad key, every locale errored) must never exit 0.
|
|
47
|
+
if (failed > 0 && processed === 0) return 1;
|
|
48
|
+
|
|
49
|
+
// Partial: real work done but something failed, or files didn't verify.
|
|
50
|
+
if (failed > 0 || verifyErrors > 0) return 2;
|
|
51
|
+
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
|
|
57
|
+
* @param {string} cwd - Working directory
|
|
58
|
+
* @returns {Promise<number>} Exit code: 0 = success, 1 = error, 2 = partial failure
|
|
59
|
+
*/
|
|
60
|
+
async function run(args, cwd) {
|
|
61
|
+
// Set output mode before any sync work begins.
|
|
62
|
+
// --json: machine-readable NDJSON, one JSON object per line.
|
|
63
|
+
// --quiet: suppress info/ok messages, show only warnings and errors.
|
|
64
|
+
if (args.json) output.setMode('json');
|
|
65
|
+
else if (args.quiet) output.setMode('quiet');
|
|
66
|
+
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = await runSync({
|
|
70
|
+
dryRun: !!args.dry,
|
|
71
|
+
cwd,
|
|
72
|
+
cliArgs: args,
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// In --json mode every outcome must be machine-parseable — including a
|
|
76
|
+
// hard error (missing locales dir, preflight failure). Emit a structured
|
|
77
|
+
// summary object instead of letting the dispatcher print a bare [ERR]
|
|
78
|
+
// line that an agent would have to regex.
|
|
79
|
+
if (args.json) {
|
|
80
|
+
output.summary({ command: 'sync', ok: false, error: err.message });
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return computeExitCode(result);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { run, computeExitCode };
|