champollion 0.3.4 → 0.4.0

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 (132) hide show
  1. package/README.md +41 -26
  2. package/bin/cli.js +53 -5
  3. package/index.js +63 -2
  4. package/lib/api-key.js +17 -4
  5. package/lib/autofix.js +83 -36
  6. package/lib/bridge/method_bridge.py +15 -3
  7. package/lib/cards/reader.js +34 -0
  8. package/lib/cards/remote.js +15 -0
  9. package/lib/cards/search-names.js +178 -0
  10. package/lib/command-help.js +286 -85
  11. package/lib/commands/audit.js +10 -3
  12. package/lib/commands/card.js +583 -226
  13. package/lib/commands/doctor.js +54 -18
  14. package/lib/commands/help.js +37 -32
  15. package/lib/commands/init.js +1689 -87
  16. package/lib/commands/integrity.js +127 -40
  17. package/lib/commands/leaderboard.js +187 -67
  18. package/lib/commands/models.js +9 -2
  19. package/lib/commands/provenance.js +7 -2
  20. package/lib/commands/recommend.js +43 -14
  21. package/lib/commands/register-corpus.js +632 -125
  22. package/lib/commands/seal-corpus.js +1 -1
  23. package/lib/commands/status.js +564 -27
  24. package/lib/commands/submit.js +17 -12
  25. package/lib/commands/sync.js +31 -7
  26. package/lib/commands/tm.js +15 -9
  27. package/lib/commands/verify.js +27 -3
  28. package/lib/commands/wrap.js +63 -5
  29. package/lib/commands/xliff.js +135 -64
  30. package/lib/commercial-eligibility.js +1 -1
  31. package/lib/config.js +196 -14
  32. package/lib/content-estimate.js +96 -0
  33. package/lib/content-refusals.js +270 -0
  34. package/lib/content-review.js +372 -0
  35. package/lib/content-sync.js +1127 -344
  36. package/lib/content.js +94 -7
  37. package/lib/corpus-registration.mjs +194 -35
  38. package/lib/cost-label.js +29 -0
  39. package/lib/cost-report.js +726 -78
  40. package/lib/diff.js +38 -4
  41. package/lib/docusaurus-sync.js +965 -253
  42. package/lib/edit-distance.js +31 -0
  43. package/lib/fallback.js +964 -0
  44. package/lib/file-scope.js +106 -0
  45. package/lib/flatten.js +80 -3
  46. package/lib/flutter-locales.js +124 -0
  47. package/lib/format.js +266 -12
  48. package/lib/hash.js +146 -21
  49. package/lib/icu-structure.js +929 -0
  50. package/lib/integrity.js +223 -75
  51. package/lib/language-pair.js +157 -0
  52. package/lib/lint.js +78 -16
  53. package/lib/local-only-marks.js +106 -0
  54. package/lib/locale-layout.js +1103 -0
  55. package/lib/locale-state.js +571 -0
  56. package/lib/methods/anthropic.js +5 -0
  57. package/lib/methods/apertium.js +6 -3
  58. package/lib/methods/api.js +138 -25
  59. package/lib/methods/base.js +17 -0
  60. package/lib/methods/coaching-data.js +153 -0
  61. package/lib/methods/content-separator.js +43 -0
  62. package/lib/methods/deepl.js +1 -1
  63. package/lib/methods/direct-llm.js +252 -103
  64. package/lib/methods/external.js +146 -63
  65. package/lib/methods/gemini.js +1 -0
  66. package/lib/methods/google-translate.js +1 -0
  67. package/lib/methods/http-utils.js +41 -0
  68. package/lib/methods/libretranslate.js +7 -2
  69. package/lib/methods/llm-coached.js +68 -128
  70. package/lib/methods/llm.js +80 -31
  71. package/lib/methods/local.js +93 -10
  72. package/lib/methods/microsoft-translator.js +1 -2
  73. package/lib/methods/openai.js +4 -2
  74. package/lib/methods/openrouter-client.js +20 -19
  75. package/lib/methods/openrouter-pricing.js +150 -13
  76. package/lib/methods/prompt-methods.js +20 -0
  77. package/lib/methods/provider-pricing.js +42 -1
  78. package/lib/methods/request-capture.js +104 -0
  79. package/lib/methods/tilde.js +1 -1
  80. package/lib/methods/translated.js +1 -2
  81. package/lib/missing-key.js +93 -0
  82. package/lib/models.js +11 -0
  83. package/lib/name-rules.js +32 -0
  84. package/lib/named-keys.js +172 -0
  85. package/lib/no-translate.js +4 -3
  86. package/lib/output.js +160 -19
  87. package/lib/pairs.js +586 -30
  88. package/lib/placeholders.js +394 -0
  89. package/lib/plugins.js +8 -0
  90. package/lib/plural-gap-redo.js +109 -0
  91. package/lib/plurals.js +323 -0
  92. package/lib/po.js +1187 -0
  93. package/lib/public-catalogue.js +74 -0
  94. package/lib/recommend.js +527 -32
  95. package/lib/redo.js +95 -0
  96. package/lib/refusal-category.js +44 -0
  97. package/lib/registers.js +255 -11
  98. package/lib/repair-script.js +20 -13
  99. package/lib/scripts.js +6 -1
  100. package/lib/seal.mjs +4 -3
  101. package/lib/sealed-qualifier.mjs +1 -1
  102. package/lib/segment.js +2 -1
  103. package/lib/seo.js +19 -9
  104. package/lib/serve.js +43 -6
  105. package/lib/shared-output-seed.js +164 -0
  106. package/lib/source-contexts.js +39 -0
  107. package/lib/submit.mjs +57 -5
  108. package/lib/sync.js +2923 -474
  109. package/lib/terminology.js +13 -4
  110. package/lib/tm-evict.js +179 -0
  111. package/lib/tm-seed.js +5 -2
  112. package/lib/tm.js +818 -36
  113. package/lib/translate-pair.js +639 -34
  114. package/lib/translate.js +78 -5
  115. package/lib/types.js +22 -3
  116. package/lib/validate.js +880 -17
  117. package/lib/verify.js +1296 -104
  118. package/lib/watch.js +32 -13
  119. package/lib/xliff.js +44 -3
  120. package/package.json +1 -1
  121. package/shared/CORPORA-CARDS.md +2 -0
  122. package/shared/cards-fallback.json +1 -1
  123. package/shared/curated-orthography-conventions.json +26 -8
  124. package/shared/gettext-plural-forms.json +45 -0
  125. package/shared/method-registry.json +2 -0
  126. package/shared/metric-registry.json +96 -18
  127. package/shared/schemas/champollion-plugin.schema.json +4 -0
  128. package/shared/schemas/corpora-card.schema.json +8 -2
  129. package/shared/schemas/method-index-record.schema.json +67 -0
  130. package/shared/schemas/method-registry.schema.json +4 -0
  131. package/shared/schemas/metric-registry.schema.json +55 -1
  132. package/shared/docent/corpus.json +0 -11739
@@ -11,6 +11,11 @@
11
11
  */
12
12
 
13
13
  import { DEFAULT_OPENROUTER_MODEL, DEFAULT_TEMPERATURE, DEFAULT_BATCH_SIZE } from './config.js';
14
+ import { LOCALE_FILE_FORMATS } from './format.js';
15
+
16
+ // The --format values a config accepts — the reader's own list, so the help
17
+ // can never fall behind a format the CLI learns (lib/format.js).
18
+ const FORMAT_CHOICES = ['auto', ...LOCALE_FILE_FORMATS].join(', ');
14
19
 
15
20
  const COMMAND_HELP = {
16
21
  init: {
@@ -22,24 +27,40 @@ const COMMAND_HELP = {
22
27
  'In non-interactive environments (CI/piped stdin), or with --yes,',
23
28
  'generates a default config without prompting. In interactive mode',
24
29
  'the same flags prefill the wizard, so each step is Enter to accept.',
30
+ 'Finds your locale files first: Flutter lib/l10n/app_<lang>.arb (or',
31
+ 'the arb-dir/template-arb-file in l10n.yaml), gettext catalogs',
32
+ '(Django locale/<lang>/LC_MESSAGES/<domain>.po, Babel translations/,',
33
+ 'GNU po/), next-intl messages/, i18next public/locales/<lang>/ (one',
34
+ 'folder per language), vue-i18n src/locales/, Hugo i18n/, then common',
35
+ 'folders. --langs also creates the empty target files in that layout.',
25
36
  ],
26
37
  options: [
27
38
  ['--yes', 'Skip wizard, use defaults'],
28
- ['--force', 'Regenerate config even if one already exists (overwrites)'],
39
+ ['--force', 'Re-run over an existing config: rewrites only what the flags name (and re-detects the locale layout when the config no longer finds the source files); every other setting is kept. Prints what changed, and backs the file up first (champollion.config.json.bak; an older backup is never overwritten — .bak.2, .bak.3 …). To change one setting, edit it in the file instead'],
29
40
  ['--langs <codes>', 'Target languages, comma-separated (e.g., fr,de,ja); presets: european, asian, global, nordic'],
41
+ ['--script <choice>', 'Writing system for a language with more than one real orthography (Plains Cree: Latn = SRO, Cans = Syllabics; Serbian: Latn, Cyrl): crk=Cans, or several as crk=Cans,sr=Latn. Without it init says which languages need a choice'],
42
+ ['--name <code=name>', 'Display name for a language with no card — a private-use code (qaa–qtz) for a variety not yet confirmed: qaa="Ayta (variety not yet confirmed)"; several separated by ";". Written as "languages": { "qaa": { "name": … } }; it is what the model is told'],
30
43
  ['--source <code>', 'Source locale (default: en)'],
31
- ['--dir <path>', 'Locales directory (default: ./locales)'],
32
- ['--method <name>', 'Translation method: llm, openai, anthropic, gemini, deepl, microsoft-translator, libretranslate, google-translate (default: llm = OpenRouter)'],
44
+ ['--dir <path>', 'Locales directory (default: detected from your project, else ./locales)'],
45
+ ['--content-dir <dir>', 'A folder of Markdown/MDX files to translate too (writes "contentDir"; the folder must exist)'],
46
+ ['--method <name>', 'Translation method: llm, openai, anthropic, gemini, local (your own model: Ollama/vLLM/LM Studio/nmt-forge), deepl, microsoft-translator, libretranslate, google-translate, api (a champollion API endpoint — needs --endpoint) (default: llm = OpenRouter; local when a file in the project is marked local-only — a <file>.champollion.json sidecar with "transmission": "local-only")'],
47
+ ['--endpoint <url>', 'With --method api: the endpoint URL (e.g. http://127.0.0.1:8378/translate from `nmt-forge serve`). Writes one "pairs" entry per target, as the model\'s DEPLOY.md shows'],
48
+ ['--accepts-instructions <true|false>', 'With --method api: whether the endpoint follows per-key instructions (a model trained with nmt-forge does not: false). Default: what an installed plugin manifest for that endpoint says, else not stated'],
33
49
  ['--model <model>', `Translation model (default: ${DEFAULT_OPENROUTER_MODEL})`],
34
50
  ['--temperature <n>', `Sampling temperature, 0.0-1.0 (default: ${DEFAULT_TEMPERATURE})`],
35
- ['--format <fmt>', 'File format: auto, json, toml, yaml (default: auto)'],
51
+ ['--format <fmt>', `File format: ${FORMAT_CHOICES} (default: auto — detected from the file extension)`],
36
52
  ],
37
53
  examples: [
38
54
  'champollion init # Interactive wizard',
39
55
  'champollion init --langs fr,de,ja # Wizard with prefilled targets',
40
56
  'champollion init --yes --langs fr,de,ja # Non-interactive, quick setup',
41
57
  'champollion init --yes --langs fr,de --method deepl --temperature 0.2',
58
+ 'champollion init --yes --langs fr,de --method local --model llama3.1 # a model on this machine (Ollama, LM Studio…), no key',
59
+ 'champollion init --yes --langs abc --method api --endpoint http://127.0.0.1:8378/translate --accepts-instructions false # nmt-forge serve',
60
+ 'champollion init --yes --langs crk --script crk=Latn # Plains Cree in SRO (Cans = Syllabics)',
61
+ 'champollion init --yes --langs qaa --name qaa="Ayta (variety not yet confirmed)" # a variety with no confirmed code',
42
62
  'champollion init --yes # Minimal config, auto-detect',
63
+ 'champollion init --force --langs fr,de,es # Over an existing config: the target list changes, every other setting stays',
43
64
  ],
44
65
  },
45
66
 
@@ -55,21 +76,28 @@ const COMMAND_HELP = {
55
76
  ['--dry', 'Preview changes without writing files'],
56
77
  ['--list-keys', 'With --dry: name every queued key, grouped by reason (missing / fallback / unstamped echo / changed / forced)'],
57
78
  ['--pair <src:tgt>', 'Only sync the named pair(s), comma-separated (e.g. en:fr). Unknown pairs fail loud'],
58
- ['--force', 'Re-queue EVERY source key — whole-locale rebuild. Scope with --pair; add --no-tm to also bypass the cache. TM hits are still served (and gate-checked), so an intact cache makes this cheap'],
59
- ['--force-keys <keys>', 'Comma-separated dot-notation keys to force re-translate'],
60
- ['--force-content', 'Ignore the content lock and re-process every champollion-managed content file (hand-translated files are still preserved). Content cached in the Translation Memory comes back as free hits; text the TM has never seen is re-billed'],
61
- ['--model <model>', 'Override translation model for this run'],
79
+ ['--redo <scope>', 'Translate again: all | keys:<k1,k2> | content | files:<glob> | gaps (repeatable). Anything the cache already holds is served free (and gate-checked), so a redo is cheap. all and content keep text a person edited (and say how much); keys:<k> and files:<glob> replace it — the key/file was named. gaps: every plural message on disk without a form its language uses for ordinary counts (the "other" form standing in, marked "# champollion:" in a catalog) — asked from the model, not the cache. A gettext context key can be typed as ctx\\x04msgid (or ctx␄msgid, as reports print it)'],
80
+ ['--fresh', 'Do not use the cache for what is queued — it is paid for again. With --redo files:<glob>, the files are re-translated from scratch (lock, keep-hand-translated rule and cache all bypassed)'],
81
+ ['--force', 'Same as --redo all: re-queue EVERY source key — whole-locale rebuild; values a person edited are kept (sync names them). Scope with --pair; add --fresh (or --no-tm) to also bypass the cache'],
82
+ ['--force-keys <keys>', 'Same as --redo keys:<keys>: comma-separated keys to translate again — replaced even when a person edited them (the edited wording is printed and kept in .champollion-replaced-edits.jsonl). `\\,` = a comma inside a key; a gettext context key: ctx\\x04msgid'],
83
+ ['--force-content', 'Same as --redo content: ignore the content lock and re-process every champollion-managed content file (hand-translated files are still preserved). Cached content is served free; text the TM has never seen is billed'],
84
+ ['--files <glob>', 'Only these content files this run (repeatable; paths as sync prints them, e.g. "docs/intro.md", "posts/**")'],
85
+ ['--retranslate <glob>', 'Same as --redo files:<glob> --fresh. Translate these content files fresh — bypasses the lock and the cache, so it is billed, and replaces paragraphs a person edited in them (said first; like --redo files:<glob>, a named file is replaced — every other redo keeps edits) (repeatable)'],
86
+ ['--model <model>', 'Translation model for this run only — champollion.config.json is not changed, and a plain sync uses its "model" again. To switch for good, edit "model" there'],
62
87
  ['--config <path>', 'Path to config file'],
63
88
  ['--dir <path>', 'Override locales directory'],
64
- ['--content-dir <p>', 'Hugo content directory for Markdown translation'],
89
+ ['--content-dir <p>', 'Folder of Markdown/MDX to translate (Hugo content/ or any folder)'],
65
90
  ['--source <code>', 'Override source locale (default: en)'],
66
- ['--format <fmt>', 'Locale file format: json, toml, yaml, auto'],
67
- ['--method <method>', 'Translation method: llm, llm-coached, google-translate, api, deepl, microsoft-translator, libretranslate, openai, anthropic, gemini (default: from config)'],
91
+ ['--format <fmt>', `Locale file format: ${FORMAT_CHOICES} (po = gettext, arb = Flutter)`],
92
+ ['--method <method>', 'Translation method: llm, llm-coached, local (your own model: Ollama/vLLM/LM Studio/forge), openai, anthropic, gemini, google-translate, deepl, microsoft-translator, libretranslate, api. Overrides the config for this run only — including a pair\'s own method (sync names the pairs); scope with --pair. To switch for good, edit "defaultMethod" (or the pair\'s "method") in champollion.config.json'],
68
93
  ['--temperature <n>', `Sampling temperature for LLM methods (default: ${DEFAULT_TEMPERATURE})`],
69
94
  ['--batch-size <n>', `Keys per translation API call (positive integer, default: ${DEFAULT_BATCH_SIZE})`],
70
- ['--max-cost <usd>', 'Abort before any API call if the pre-run cost estimate exceeds this USD cap (exit 2; unknown estimates abort too — unknown is not free)'],
95
+ ['--max-cost <usd>', 'Abort before any API call if the pre-run cost estimate exceeds this USD cap (exit 2; unknown estimates abort too — unknown is not free). With --dry: says whether the real run would stop there'],
96
+ ['--show-prompt [key]', 'With --dry: print the exact request the method would be sent (system/user messages or request body, keys redacted) — for one key, or the first batch each file would send. Shows whether a gettext msgctxt or #. comment reaches the model'],
71
97
  ['--no-verify', 'Skip post-sync verification pass'],
72
- ['--no-tm', 'Skip Translation Memory cache (force fresh API calls for all keys)'],
98
+ ['--no-tm', 'Same as --fresh: skip the Translation Memory cache (fresh API calls for everything queued)'],
99
+ ['--fresh-on-model-change', 'Don\'t reuse the previous model\'s cached translations for what this run translates (default: reuse them, at no cost). On its own it affects only new or changed keys; to have the new model translate what an earlier model wrote: --redo all --fresh-on-model-change'],
100
+ ['--prune plural-extras', 'Remove i18next plural keys for a form the language does not have (Spanish count_two from a source with count_one/count_other) — CLDR says which. Lists each key it removes and touches nothing else; never without this flag. With --dry: says what it would remove'],
73
101
  ['--json', 'Machine-readable NDJSON output (one JSON object per line)'],
74
102
  ['--quiet, -q', 'Suppress informational messages; show only warnings and errors'],
75
103
  ],
@@ -80,12 +108,16 @@ const COMMAND_HELP = {
80
108
  'champollion sync --force-keys hero.title # Re-translate specific keys',
81
109
  'champollion sync --pair "en:tlh" --force # Rebuild one whole locale',
82
110
  'champollion sync --pair "en:tlh" --force --no-tm # …bypassing a suspect cache',
83
- 'champollion sync --content-dir ./content # Include Hugo content',
111
+ 'champollion sync --content-dir ./newsletters # Include a folder of Markdown',
84
112
  'champollion sync --max-cost 0.50 # Refuse to spend more than $0.50',
113
+ 'champollion sync --redo gaps # Ask again for plural forms a model left out',
114
+ 'champollion sync --prune plural-extras # Remove plural keys for forms a language does not have',
115
+ 'champollion sync --dry --show-prompt \'verb␄Open\' # The request for one gettext entry (context included), not sent',
85
116
  ],
86
117
  },
87
118
 
88
119
  serve: {
120
+ summary: "Serves this project's own translation stack over HTTP, for another project's `api` method.",
89
121
  usage: 'champollion serve [options]',
90
122
  description: [
91
123
  'Serves this project\'s OWN configured translation stack (method,',
@@ -143,18 +175,22 @@ const COMMAND_HELP = {
143
175
  },
144
176
 
145
177
  audit: {
178
+ summary: 'Lists what is not translated, or out of date, across locale files — with the command that fixes it.',
146
179
  usage: 'champollion audit [options]',
147
180
  description: [
148
- 'Lists all untranslated [EN] fallback values across locale files.',
149
- 'Returns exit code 1 if any untranslated keys exist — usable as',
150
- 'a CI gate to block deploys with missing translations.',
181
+ 'Lists what is not translated across locale files — keys missing,',
182
+ 'empty, or still an [EN] fallback — and translations that are OUT OF',
183
+ 'DATE: made from an older source text than the current one (the record',
184
+ 'in .champollion.lock), with the command that re-translates them.',
185
+ 'Returns exit code 1 if any exist — usable as a CI gate to block',
186
+ 'deploys with missing or stale translations.',
151
187
  ],
152
188
  options: [
153
189
  ['--config <path>', 'Path to config file'],
154
190
  ['--dir <path>', 'Override locales directory'],
155
191
  ['--source <code>', 'Override source locale'],
156
192
  ['--format <fmt>', 'Locale file format: json, toml, yaml, auto'],
157
- ['--json', 'Machine-readable NDJSON output (one JSON object per line; summary carries the untranslated key list)'],
193
+ ['--json', 'Machine-readable NDJSON output (one JSON object per line; summary carries the untranslated and out-of-date key lists)'],
158
194
  ],
159
195
  examples: [
160
196
  'champollion audit # List untranslated keys',
@@ -163,13 +199,46 @@ const COMMAND_HELP = {
163
199
  ],
164
200
  },
165
201
 
202
+ network: {
203
+ usage: 'champollion network <command> [options]',
204
+ description: [
205
+ 'Commands for the shared index and leaderboard, not your project.',
206
+ 'Each also works without the "network" prefix.',
207
+ '',
208
+ ' card <code> What the index knows about a language, every value cited',
209
+ ' recommend <s> <t> Methods you can run for a pair, and the evidence for each',
210
+ ' leaderboard Published results (--pair, --sort, --json)',
211
+ ' register-corpus Register a test set without handing it over',
212
+ ' seal-corpus Sealed-tier crypto: keygen / seal / open',
213
+ ' submit Propose an index entry (review-gated)',
214
+ '',
215
+ 'A language pair is written source>target: "eng>crk" (quote it — an unquoted >',
216
+ 'sends the output to a file). The commands that take a pair also read eng-crk',
217
+ 'and eng:crk; a code with its own hyphen (pt-BR) needs >: "eng>pt-BR".',
218
+ ],
219
+ options: [],
220
+ examples: [
221
+ 'champollion network card crk',
222
+ 'champollion network recommend eng crk',
223
+ 'champollion network recommend eng-crk # the same pair, as one value',
224
+ 'champollion network leaderboard --pair "eng>crk"',
225
+ 'champollion network register-corpus --data data/test.tsv --name "Ward phrases" --pair "eng>xyz" --license proprietary --tier local-only --domain medical',
226
+ ],
227
+ },
228
+
166
229
  card: {
167
- usage: 'champollion card <code> [options]',
230
+ usage: 'champollion network card <code> [options]',
168
231
  description: [
169
232
  'Pretty-prints a language card from the shared language-cards directory.',
170
- 'Shows identification, classification, speaker estimates, typology,',
171
- 'corpus availability, eval datasets, pipeline readiness, method support,',
172
- 'and data sources in a color-formatted terminal layout.',
233
+ 'Shows identification, classification, speaker estimates, endangerment,',
234
+ 'typology, corpus availability, language resources, eval datasets,',
235
+ 'pipeline readiness, method support, and data sources in a',
236
+ 'color-formatted terminal layout.',
237
+ '',
238
+ 'Where sources disagree (names, families, speaker counts, endangerment, …)',
239
+ 'every value is printed with its source and none is elected. A section',
240
+ 'the card has nothing for says "not recorded on this card": absence is',
241
+ 'unknown, not "none".',
173
242
  '',
174
243
  'Resolves aliases automatically (e.g., "fr" → "fra", "es" → "spa").',
175
244
  ],
@@ -177,18 +246,18 @@ const COMMAND_HELP = {
177
246
  ['--json', 'Output the raw card JSON instead of the formatted display'],
178
247
  ],
179
248
  examples: [
180
- 'champollion card crk # Plains Cree',
181
- 'champollion card spa # Spanish',
182
- 'champollion card cmn --json # Raw JSON for Mandarin',
183
- 'champollion card fr # French (resolves alias)',
249
+ 'champollion network card crk # Plains Cree',
250
+ 'champollion network card spa # Spanish',
251
+ 'champollion network card cmn --json # Raw JSON for Mandarin',
252
+ 'champollion network card fr # French (resolves alias)',
184
253
  ],
185
254
  },
186
255
 
187
256
  'register-corpus': {
188
- usage: 'champollion register-corpus [options]',
257
+ usage: 'champollion network register-corpus [options]',
189
258
  description: [
190
259
  'Register a new evaluation corpus, choosing its license and exposure tier.',
191
- 'You control the license and how far the corpus travels — three exposure',
260
+ 'You control the license and how far the corpus travels — four exposure',
192
261
  'tiers, defaulting to the most private:',
193
262
  ' local-only — never registered or uploaded; card + text stay on your machine.',
194
263
  ' private — register METADATA ONLY (WMT-style sovereign held-out set);',
@@ -200,18 +269,36 @@ const COMMAND_HELP = {
200
269
  ' content-free card. Paired with a public qualifier a method',
201
270
  ' must clear before any sealed run can be proposed.',
202
271
  '',
203
- 'Champollion never reads, uploads, or hosts your corpus plaintext in ANY tier.',
272
+ 'Champollion never uploads or hosts your corpus text, in any tier. A file you',
273
+ 'name is read on this machine only: --data to count its entries and checksum',
274
+ 'it, --seal-input to encrypt it. None of it is sent anywhere.',
204
275
  'Interactive in a terminal; fully scriptable with flags (or --yes).',
276
+ '',
277
+ 'A test set a model may be trained against (--role test, or a local-only or',
278
+ 'private set with no role stated): the command prints the nmt-forge steps —',
279
+ 'register it, screen the training corpus, write down predictions — that must',
280
+ 'come before its first score, then the baseline run.',
281
+ '',
282
+ 'Card id: eval-<src>-<tgt>-<name>[-<role>]-v1, e.g. --name "Ward phrases"',
283
+ '--pair "eng>xyz" --role test → eval-eng-xyz-ward-phrases-test-v1.',
284
+ 'The name part comes from --name (from --publisher only when the name has no',
285
+ 'a-z/0-9 letters). The role part appears only when you pass --role; no role is',
286
+ 'ever guessed. A file registered with --data keeps the id it was registered',
287
+ 'under: re-running asks you to pass it with --id.',
205
288
  ],
206
289
  options: [
207
290
  ['--tier <tier>', 'local-only | private | public | sealed (default: local-only). Alias: --exposure'],
208
291
  ['--license <id>', 'License key / SPDX id / list number (see --list)'],
209
- ['--name <text>', 'Corpus name (required)'],
210
- ['--pair <src>tgt>', 'Language pair, e.g. eng>crk (or --source-lang/--target-lang)'],
211
- ['--size <n>', 'Number of sentence pairs (required)'],
292
+ ['--name <text>', 'Corpus name (required; also names the card id)'],
293
+ ['--role <role>', 'What the set is for: test | dev | train. Goes into the id; left out, the id names no role'],
294
+ // The pair text is lib/language-pair.js's PAIR_NOTATION_HELP (a test holds them equal).
295
+ ['--pair <pair>', 'Language pair, source>target, e.g. "eng>crk" (quote it: an unquoted > sends the output to a file). eng-crk, eng:crk and "eng crk" are read the same way; a code with its own hyphen (pt-BR) needs >, e.g. "eng>pt-BR". Or --source-lang/--target-lang'],
296
+ ['--data <file>', 'The local test-set file (TSV/JSONL/JSON): read on this machine only to count and checksum it, never uploaded; writes <file>.champollion.json so mt-eval run applies the licence (local-only: remote models refused)'],
297
+ ['--size <n>', 'Number of sentence pairs (required unless --data)'],
212
298
  ['--domain <text>', 'Domain: news, conversational, educational, … (required)'],
213
- ['--contamination <l>', 'NONE | LOW | MEDIUM | HIGH (default: NONE private / LOW public)'],
299
+ ['--contamination <l>', 'NONE | LOW | MEDIUM | HIGH. Default: LOW for public, NONE otherwise — UNCHECKED when a --data or --seal-input file could not be compared with the public corpora (a copy of a public corpus is never graded NONE)'],
214
300
  ['--publisher <text>', 'Publisher / your name or org'],
301
+ ['--description <text>', 'One-line description'],
215
302
  ['--repo-url <url>', 'Public tier: fetch-from-source archive/repo URL'],
216
303
  ['--source-url <url>', 'Public tier: canonical project/dataset URL'],
217
304
  ['--builder <id>', 'Public tier: builder adapter id (rebuilds from source)'],
@@ -221,28 +308,33 @@ const COMMAND_HELP = {
221
308
  ['--seal-out <path>', 'Sealed tier: where to write the ciphertext artifact'],
222
309
  ['--qualifier-id <id>', 'Sealed tier: paired public qualifier card id (vYYYY)'],
223
310
  ['--qualifier-threshold <n>', 'Sealed tier: score a method must clear on the qualifier'],
224
- ['--id <id>', 'Override the generated card id (eval-…)'],
225
- ['--out <dir>', 'local-only: where to write the card (default: cwd)'],
311
+ ['--key-scheme <s>', 'Sealed tier: custody scheme label (default: TSS-3-of-5)'],
312
+ ['--id <id>', 'Set the card id yourself (eval-…/ref-…, used as given; other text replaces the name part)'],
313
+ ['--out <dir>', 'Where to write the card (default: next to --data, else cwd)'],
226
314
  ['--list', 'Print the license + tier catalog (add --json for JSON)'],
227
315
  ['--json', 'Machine-readable output (for agents)'],
228
316
  ['--yes', 'Non-interactive; take values from flags'],
229
317
  ],
230
318
  examples: [
231
- 'champollion register-corpus # interactive wizard',
232
- 'champollion register-corpus --list # see licenses + tiers',
233
- 'champollion register-corpus --yes --name "My set" --pair "eng>crk" \\',
319
+ 'champollion network register-corpus # interactive wizard',
320
+ 'champollion network register-corpus --list # see licenses + tiers',
321
+ 'champollion network register-corpus --yes --name "My set" --pair "eng>crk" \\',
234
322
  ' --license cc-by-4.0 --tier local-only --size 200 --domain news',
235
- 'champollion register-corpus --yes --name "Holdout" --pair "eng>crk" \\',
236
- ' --license cc-by-nc-4.0 --tier private --size 500 --domain educational',
237
- 'champollion register-corpus --yes --name "Sealed" --pair "eng>crk" \\',
323
+ 'champollion network register-corpus --yes --name "Holdout" --pair "eng>crk" \\',
324
+ ' --license cc-by-nc-4.0 --tier private --role test --size 500 --domain educational',
325
+ 'champollion network register-corpus --yes --name "Tatoeba eng-crk" --pair eng-crk \\',
326
+ ' --license cc-by-4.0 --tier public --size 1000 --domain conversational \\',
327
+ ' --repo-url https://example.org/data.tar --builder tatoeba-challenge',
328
+ 'champollion network register-corpus --yes --name "Sealed" --pair "eng>crk" \\',
238
329
  ' --license proprietary --tier sealed --size 500 --domain educational \\',
239
330
  ' --seal-input ./secret.json --threshold-pubkey ./group.pub \\',
240
- ' --custodian-group nehiyawewin-trust --qualifier-id eval-eng-crk-…-qualifier-v2026',
331
+ ' --custodian-group example-community-trust --qualifier-id eval-eng-crk-…-qualifier-v2026',
241
332
  ],
242
333
  },
243
334
 
244
335
  'seal-corpus': {
245
- usage: 'champollion seal-corpus <keygen|seal|open|sign-keygen|sign|verify> [options]',
336
+ summary: 'Encrypts, decrypts and signs corpora for the sealed exposure tier (keygen, seal, open, sign, verify).',
337
+ usage: 'champollion network seal-corpus <keygen|seal|open|sign-keygen|sign|verify> [options]',
246
338
  description: [
247
339
  'The crypto verbs around the sealed exposure tier (lib/seal.mjs is the one',
248
340
  'cipher implementation: X25519-ECDH → HKDF-SHA256 → AES-256-GCM). Used by',
@@ -279,21 +371,21 @@ const COMMAND_HELP = {
279
371
  ['--pubkey <k>', 'verify: Ed25519 public key (file / b64 / PEM)'],
280
372
  ],
281
373
  examples: [
282
- 'champollion seal-corpus keygen --out ~/.contest-keys',
283
- 'champollion seal-corpus seal --seal-input ./refs.json --id eval-eng-xxx-blindtest-v1 \\',
374
+ 'champollion network seal-corpus keygen --out ~/.contest-keys',
375
+ 'champollion network seal-corpus seal --seal-input ./refs.json --id eval-eng-xxx-blindtest-v1 \\',
284
376
  ' --custodian-group org-a1b2 --threshold-pubkey ~/.contest-keys/threshold-….pub.json \\',
285
377
  ' --seal-out ~/contest/refs.sealed.json --card-block-out ~/contest/sealed-block.json',
286
- 'champollion seal-corpus open --artifact ~/contest/refs.sealed.json \\',
378
+ 'champollion network seal-corpus open --artifact ~/contest/refs.sealed.json \\',
287
379
  ' --privkey ~/.contest-keys/threshold-….key.json --out /tmp/scratch/refs.json',
288
- 'champollion seal-corpus sign-keygen --out ~/.contest-keys',
289
- 'champollion seal-corpus sign --payload score-bundle.json --privkey ~/.contest-keys/score-sign-….key.json',
290
- 'champollion seal-corpus verify --payload score-bundle.json --sig score-bundle.json.sig.json \\',
380
+ 'champollion network seal-corpus sign-keygen --out ~/.contest-keys',
381
+ 'champollion network seal-corpus sign --payload score-bundle.json --privkey ~/.contest-keys/score-sign-….key.json',
382
+ 'champollion network seal-corpus verify --payload score-bundle.json --sig score-bundle.json.sig.json \\',
291
383
  ' --pubkey ~/.contest-keys/score-sign-….pub.json',
292
384
  ],
293
385
  },
294
386
 
295
387
  submit: {
296
- usage: 'champollion submit [options]',
388
+ usage: 'champollion network submit [options]',
297
389
  description: [
298
390
  'Propose an entry for the Champollion index along a REVIEW-GATED path.',
299
391
  'Gathers the fields for a chosen submission type and prints a PRE-FILLED',
@@ -310,10 +402,13 @@ const COMMAND_HELP = {
310
402
  ' external-result — a published result from another system/paper (cited, never re-hosted).',
311
403
  ' card-correction — a fix to a language card (cited; applied at the data source).',
312
404
  '',
405
+ 'A "pairs" field takes one language pair per line, source>target ("eng>crk";',
406
+ 'eng-crk and eng:crk work too). The issue carries each one as eng>crk.',
407
+ '',
313
408
  'Interactive in a terminal; fully scriptable with flags (or --yes).',
314
409
  ],
315
410
  options: [
316
- ['--type <key>', 'dataset | resource | method | human-service | external-result'],
411
+ ['--type <key>', 'dataset | resource | method | human-service | external-result | card-correction'],
317
412
  ['--values <json>', 'JSON object of field id -> value'],
318
413
  ['--field <id=val>', 'Set one field (repeatable): --field source-url=https://…'],
319
414
  ['--attest', 'Confirm the required compliance attestation (and consent)'],
@@ -325,12 +420,12 @@ const COMMAND_HELP = {
325
420
  ['--yes', 'Non-interactive; take values from flags'],
326
421
  ],
327
422
  examples: [
328
- 'champollion submit # interactive wizard',
329
- 'champollion submit --list # see the submission types',
330
- 'champollion submit --yes --type dataset --attest \\',
423
+ 'champollion network submit # interactive wizard',
424
+ 'champollion network submit --list # see the submission types',
425
+ 'champollion network submit --yes --type dataset --attest \\',
331
426
  ' --field dataset-name="GlobalVoices eng-amh" --field pairs=eng-amh \\',
332
427
  ' --field license=CC-BY-4.0 --field source-url=https://globalvoices.org',
333
- 'champollion submit --yes --type external-result --attest --out ./submission.json \\',
428
+ 'champollion network submit --yes --type external-result --attest --out ./submission.json \\',
334
429
  ' --values \'{"system-name":"NLLB-200","pairs":"eng-crk","dataset":"FLORES-200","metric":"chrF++","score":"28.4","citation":"https://arxiv.org/abs/2207.04672"}\'',
335
430
  ],
336
431
  },
@@ -345,6 +440,7 @@ const COMMAND_HELP = {
345
440
  options: [
346
441
  ['--src <path>', 'Source directory to scan (auto-detected by default)'],
347
442
  ['--min-length <n>','Minimum string length to flag (default: 2)'],
443
+ ['--ignore <names>', 'Comma-separated directory/file names to skip (adds to config lint.ignore)'],
348
444
  ['--warn-only', 'Exit 0 even if issues found'],
349
445
  ['--json', 'Machine-readable JSON output (single document with findings)'],
350
446
  ['--config <path>', 'Path to config file'],
@@ -402,6 +498,7 @@ const COMMAND_HELP = {
402
498
  },
403
499
 
404
500
  integrity: {
501
+ summary: 'Audits locale files for structural issues.',
405
502
  usage: 'champollion integrity [options]',
406
503
  description: [
407
504
  'Audits locale files for structural issues:',
@@ -410,6 +507,12 @@ const COMMAND_HELP = {
410
507
  ' - Encoding problems (mojibake, BOM issues)',
411
508
  ' - Key structure drift between locales',
412
509
  ' - ICU MessageFormat plural category completeness',
510
+ ' - ICU MessageFormat structure damage (translated keywords, variables,',
511
+ ' selectors; lost # or printf placeholders)',
512
+ ' - Flutter .arb file damage (@@locale, placeholder metadata)',
513
+ '',
514
+ 'A damaged value the Translation Memory produced is removed from the',
515
+ 'cache, so `sync --force-keys <key>` translates it again.',
413
516
  '',
414
517
  'Returns exit code 1 if issues found (unless --warn-only).',
415
518
  ],
@@ -465,6 +568,7 @@ const COMMAND_HELP = {
465
568
  },
466
569
 
467
570
  status: {
571
+ summary: 'Shows the project configuration summary.',
468
572
  usage: 'champollion status [options]',
469
573
  description: [
470
574
  'Shows the project configuration summary:',
@@ -628,59 +732,119 @@ const COMMAND_HELP = {
628
732
  verify: {
629
733
  usage: 'champollion verify [options]',
630
734
  description: [
631
- 'Re-reads all locale files from disk and verifies translations are',
632
- 'actually present and correct. Catches the gap between sync reporting',
633
- 'success and keys being wrong in fact.',
735
+ 'Checks the locale files on disk: complete, and structurally intact.',
736
+ '',
737
+ 'Re-reads the files (not what sync remembers) and compares each locale',
738
+ 'with the source — the gap between sync reporting success and keys being',
739
+ 'wrong in fact. Structure only: the meaning is not checked.',
634
740
  '',
635
- 'Checks: key parity, [EN] fallback markers, empty values, script',
636
- 'compliance, placeholder preservation, encoding issues, source echoes.',
741
+ 'Errors (exit 1):',
742
+ ' - a key missing — i18next plural keys included: the locale needs',
743
+ ' a key for each of its CLDR plural forms (French count_one,',
744
+ ' count_many, count_other; English needs count_one, count_other)',
745
+ ' - an empty value, or a fallback marker ("[EN] " — fallbackPrefix)',
746
+ ' - wrong script: Latin-only text in a non-Latin locale, or fullwidth',
747
+ ' Latin letters',
748
+ ' - a placeholder lost, renamed or added, named by its syntax:',
749
+ ' ICU MessageFormat structure (a translated variable name,',
750
+ ' plural/select keyword or selector, a lost #), printf/python-format',
751
+ ' (%s, %d, %(name)s), i18next {{name}}, single-brace {name}',
752
+ ' - markup: a tag opened, closed or nested differently',
753
+ ' - a hollowed value (the source with its letters deleted), or a',
754
+ ' no-translate key that differs from the source',
755
+ ' - one text written for several different source strings',
756
+ ' - Flutter .arb: a wrong @@locale or changed placeholder metadata',
757
+ ' - a configured locale with no file, or nothing found to check',
637
758
  '',
638
- 'Exits with code 1 if errors found — use as a CI gate.',
639
- 'This is the same verification that runs automatically after sync.',
759
+ 'Warnings (exit 0 unless --strict):',
760
+ ' - plurals: an i18next key for a form the locale does not have',
761
+ ' (Spanish count_two — `sync --prune plural-extras` removes those',
762
+ ' keys and nothing else); an ICU plural message without a form the',
763
+ ' language uses for ordinary counts (Russian few, many); gettext',
764
+ ' msgstr[] forms repeating "other" (marked # champollion:), or more',
765
+ ' of them than the catalog\'s nplurals',
766
+ ' - source echoes, encoding issues (invisible characters, U+FFFD),',
767
+ ' a lost closing ? or !',
768
+ ' - two locales with identical text',
769
+ ' - translations made from an older source text (out of date —',
770
+ ' `audit` fails on those)',
771
+ ' - Markdown blocks or front-matter fields the quality gate refuses',
772
+ 'Said, never counted: an i18next plural form holding the text of the',
773
+ 'form it was translated from, with no record of the model writing it.',
774
+ '',
775
+ 'Each locale also gets a line per kind of plural it carries — the',
776
+ 'forms its CLDR rules (or its gettext Plural-Forms) expect, and how',
777
+ 'many plurals have all of them: "Plural forms (CLDR fr): one, many,',
778
+ 'other ✓". --json writes one {"level":"event","event":"verify",…}',
779
+ 'record per locale (findings, placeholders by syntax, plural coverage)',
780
+ 'before the closing line, which carries the error and warning counts.',
781
+ '',
782
+ 'A plain sync keeps a value already on disk, damaged or not: each',
783
+ 'damage finding (placeholders, ICU structure, hollowed text) names the',
784
+ 'command that repairs it — `champollion sync --pair en:fr --redo',
785
+ 'keys:<key>` (`<ns>::<key>` when a locale spans several files, `\\,` for',
786
+ 'a comma inside a key). No --fresh is needed: a damaged value the',
787
+ 'Translation Memory produced is removed from the cache here, so the redo',
788
+ 'translates it again instead of serving the same text for free.',
789
+ 'Hand-written values and the files themselves are not touched.',
790
+ '',
791
+ 'This is the same verification that runs automatically after sync',
792
+ '(where `sync --pair` verifies only the pairs that ran).',
640
793
  ],
641
794
  options: [
642
- ['--warn-only', 'Exit 0 even if errors found'],
643
- ['--config <path>', 'Path to config file'],
644
- ['--dir <path>', 'Override locales directory'],
645
- ['--source <code>', 'Override source locale'],
795
+ ['--pair <src:tgt>', 'Only verify the named pair(s), comma-separated (e.g. en:fr). Unknown pairs fail loud'],
796
+ ['--strict', 'Exit 1 on warnings too (CI that must not pass a plural gap or an out-of-date value)'],
797
+ ['--warn-only', 'Exit 0 even if errors found'],
798
+ ['--json', 'NDJSON: one "verify" event per locale on stdout, errors and warnings on stderr, then the closing line with the counts'],
799
+ ['--config <path>', 'Path to config file'],
800
+ ['--dir <path>', 'Override locales directory'],
801
+ ['--source <code>', 'Override source locale'],
646
802
  ],
647
803
  examples: [
648
804
  'champollion verify # Verify all locale files',
805
+ 'champollion verify --pair en:fr # Only French',
649
806
  'champollion verify --warn-only # Non-blocking verification',
807
+ 'champollion verify --strict # Warnings fail too (CI)',
650
808
  'champollion verify && echo "All good" # CI gate',
809
+ 'champollion verify --json 2>/dev/null | jq -c \'select(.event == "verify") | {locale, plurals}\'',
651
810
  ],
652
811
  },
653
812
 
654
813
  leaderboard: {
655
- usage: 'champollion leaderboard [options]',
814
+ usage: 'champollion network leaderboard [options]',
656
815
  description: [
657
816
  'Fetches and displays MT evaluation leaderboard data from Supabase.',
658
- 'Shows ranked results with composite scores, quality tiers, and metrics.',
817
+ 'Ranks by chrF++ with its 95% confidence interval (scoring standard/1,',
818
+ 'as in WMT and FLORES-200), shows BLEU, TER and COMET beside it, and',
819
+ 'diagnostics (exact match, FST acceptance) apart. No quality labels.',
659
820
  'Supports filtering by language pair, sorting by any metric, and',
660
821
  'NDJSON output for CI/CD integration.',
661
822
  ],
662
823
  options: [
663
- ['--pair <pair>', 'Filter by language pair (e.g., en>crk)'],
664
- ['--sort <key>', 'Sort by metric: composite (default), chrf, exact, fst, equivalent, semantic, cost, date'],
824
+ ['--pair <pair>', 'Filter by language pair, as the board writes it: "eng>crk" (ISO 639-3; quote the >). eng-crk and eng:crk work too, and a 2-letter code is resolved (en → eng)'],
825
+ ['--sort <key>', 'Sort by: chrf (default), bleu, ter, comet; diagnostics exact, fst, equivalent, semantic; cost, date; composite (the retired legacy composite, old cards only)'],
665
826
  ['--top <n>', 'Show only the top N results'],
666
827
  ['--json', 'Machine-readable NDJSON output (one JSON object per line)'],
667
828
  ],
668
829
  examples: [
669
- 'champollion leaderboard # All results, sorted by composite',
670
- 'champollion leaderboard --pair "en>crk" # Filter to English→Cree (quote the >)',
671
- 'champollion leaderboard --sort chrf --top 10 # Top 10 by chrF++',
672
- 'champollion leaderboard --json | jq .composite # Pipe to jq for processing',
830
+ 'champollion network leaderboard # All results, ranked by chrF++',
831
+ 'champollion network leaderboard --pair "eng>crk" # Filter to English→Cree (quote the >)',
832
+ 'champollion network leaderboard --sort chrf --top 10 # Top 10 by chrF++',
833
+ 'champollion network leaderboard --json | jq .chrF # Pipe to jq for processing',
673
834
  ],
674
835
  },
675
836
 
676
837
  recommend: {
677
- usage: 'champollion recommend <src> <tgt> [options]',
838
+ summary: 'Method guidance for one source→target pair: the engines and open models that can translate it, and the published evidence.',
839
+ usage: 'champollion network recommend <src> <tgt> [options] (or one pair: eng-crk, "eng>crk", --pair)',
678
840
  description: [
679
841
  'Method guidance for one source→target pair (ISO 639-3 codes): every',
680
842
  'dispatchable engine with its live availability (API key present?',
681
- 'license lane?), the published evidence indexed for the pair (cited,',
682
- 'never reproduced by us; relative-comparison-only), and which evidenced',
683
- 'models are actually runnable here.',
843
+ 'license lane?), the open models whose own model card declares the',
844
+ 'target and that local-model can load (runnable, no published evidence —',
845
+ 'a claim to benchmark, not a measurement), the published evidence indexed',
846
+ 'for the pair (cited, never reproduced by us; relative-comparison-only),',
847
+ 'and which evidenced models are actually runnable here.',
684
848
  '',
685
849
  'Honest by construction: no evidence means it says so and points at',
686
850
  'runnable corpora instead of guessing. The commercial lane is STRICT —',
@@ -688,13 +852,15 @@ const COMMAND_HELP = {
688
852
  'excluded with reasons, never silently dropped.',
689
853
  ],
690
854
  options: [
855
+ ['--pair <pair>', 'The pair as one value instead of two codes: "eng>yor" (quote the >), eng-yor or eng:yor'],
691
856
  ['--use <lane>', 'License lane: non-commercial (default) or commercial (STRICT)'],
692
- ['--json', 'Machine-readable payload (mirrors `mt-eval recommend --json`)'],
857
+ ['--json', 'Machine-readable payload (the `mt-eval recommend --json` shape, plus declared_models)'],
693
858
  ],
694
859
  examples: [
695
- 'champollion recommend eng yor # Guidance for English→Yoruba',
696
- 'champollion recommend eng yor --use commercial # Commercial lane (AGPL excluded)',
697
- 'champollion recommend eng quy --json | jq .notes # Honest no-evidence state',
860
+ 'champollion network recommend eng yor # Guidance for English→Yoruba',
861
+ 'champollion network recommend eng-yor # The same pair, as one value',
862
+ 'champollion network recommend eng yor --use commercial # Commercial lane (AGPL excluded)',
863
+ 'champollion network recommend eng quy --json | jq .notes # Honest no-evidence state',
698
864
  ],
699
865
  },
700
866
 
@@ -711,7 +877,7 @@ const COMMAND_HELP = {
711
877
  ['cards', 'Verify language cards load correctly, report coverage stats'],
712
878
  ['config', 'Validate project config file and language code resolution'],
713
879
  ['fst [code]', 'Check FST installation (all or specific language)'],
714
- ['methods', 'Check API keys, server reachability, Python packages'],
880
+ ['methods', 'Check API keys (that each is set; an OpenRouter key is also sent to OpenRouter\'s free key endpoint, which rejects a bad one), server reachability, Python packages'],
715
881
  ],
716
882
  options: [
717
883
  ['--json', 'Machine-readable JSON output (single document with all check results)'],
@@ -733,12 +899,47 @@ const COMMAND_HELP = {
733
899
  * @param {string} commandName - The command to show help for
734
900
  * @returns {boolean} true if help was displayed, false if command not found
735
901
  */
902
+ /** Abbreviations whose period does not end a sentence ("e.g. AGPL"). */
903
+ const NOT_A_SENTENCE_END = /\b(?:e\.g|i\.e|etc|vs|cf)$/i;
904
+
905
+ /**
906
+ * The one-line heading of a command's help: its `summary`, else the first
907
+ * sentence of its description — never the first LINE, which cut nine
908
+ * headings off mid-sentence ("Serves this project's OWN configured
909
+ * translation stack (method," — Round 12). A sentence ends at . ! or ?
910
+ * followed by a space or the end, outside parentheses, and not after an
911
+ * abbreviation; the description's first paragraph is read as one text.
912
+ *
913
+ * @param {{ summary?: string, description: string[] }} help
914
+ * @returns {string}
915
+ */
916
+ function helpHeading(help) {
917
+ if (typeof help.summary === 'string' && help.summary.trim()) return help.summary.trim();
918
+ const para = [];
919
+ for (const line of help.description || []) {
920
+ if (!line.trim()) break;
921
+ para.push(line.trim());
922
+ }
923
+ const text = para.join(' ');
924
+ let depth = 0;
925
+ for (let i = 0; i < text.length; i++) {
926
+ const ch = text[i];
927
+ if (ch === '(') depth++;
928
+ else if (ch === ')') depth = Math.max(0, depth - 1);
929
+ else if (depth === 0 && '.!?'.includes(ch) && (i === text.length - 1 || /\s/.test(text[i + 1]))
930
+ && !NOT_A_SENTENCE_END.test(text.slice(0, i))) {
931
+ return text.slice(0, i + 1);
932
+ }
933
+ }
934
+ return text;
935
+ }
936
+
736
937
  function showCommandHelp(commandName) {
737
938
  const help = COMMAND_HELP[commandName];
738
939
  if (!help) return false;
739
940
 
740
941
  console.log('');
741
- console.log(` champollion ${commandName} — ${help.description[0]}`);
942
+ console.log(` champollion ${commandName} — ${helpHeading(help)}`);
742
943
  console.log('');
743
944
 
744
945
  // Usage
@@ -787,4 +988,4 @@ function showCommandHelp(commandName) {
787
988
  return true;
788
989
  }
789
990
 
790
- export { COMMAND_HELP, showCommandHelp };
991
+ export { COMMAND_HELP, showCommandHelp, helpHeading };