champollion-mcp-server 0.3.1 → 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.
- package/CHANGELOG.md +73 -0
- package/LICENSE +1 -1
- package/README.md +34 -3
- package/bin/server.js +9 -3
- package/instructions-rails.md +129 -0
- package/instructions.md +1 -1
- package/package.json +7 -3
- package/src/index.js +32 -2
- package/src/rails.js +821 -0
- package/src/tools/forge.js +6 -4
- package/src/tools/harness.js +13 -6
- package/src/tools/language-card.js +1 -1
- package/src/tools/languages.js +46 -24
- package/src/tools/training.js +4 -4
- package/src/tools/translate.js +2 -2
package/src/tools/forge.js
CHANGED
|
@@ -335,6 +335,7 @@ export function forgeFlagArg(flag) {
|
|
|
335
335
|
*/
|
|
336
336
|
export const FORGE_FLAGS_NEVER_TOOLS = {
|
|
337
337
|
'--show-text': 'prints a withheld (local-only / sealed) corpus\'s sentences — for a person at the terminal, never an agent',
|
|
338
|
+
'--holders-agreed-by': 'the data\'s holders\' own answer (a local-only model served off this machine) — an agent never gives it',
|
|
338
339
|
'--json': 'every tool adds it',
|
|
339
340
|
'--workspace': 'every tool takes `workspace`',
|
|
340
341
|
'--help': 'not a step',
|
|
@@ -922,10 +923,11 @@ export function statusNextHint(r) {
|
|
|
922
923
|
const terminal = tools.filter((t) => t.startsWith('terminal:')).map((t) => t.replace(/^terminal: nmt-forge /, ''));
|
|
923
924
|
if (a.state === 'serving') {
|
|
924
925
|
return 'the chosen model is served and answering (result.advice.ready names its URL) — forge has nothing '
|
|
925
|
-
+ 'left to run. What is left is the app\'s:
|
|
926
|
-
+ '
|
|
927
|
-
+ '
|
|
928
|
-
+ '
|
|
926
|
+
+ 'left to run. What is left is the app\'s: `npx champollion connect <model dir> --app <the app folder>` '
|
|
927
|
+
+ '(ask the person which app; for a model trained on local-only sentences it needs --holders-agreed-by, '
|
|
928
|
+
+ 'the holders\' own answer), then `npx champollion sync` and `npx champollion verify` in the app, in a '
|
|
929
|
+
+ 'terminal (or the translate tool, method api, endpoint <that URL>/translate). A speaker reviews the '
|
|
930
|
+
+ 'strings before anyone relies on them.';
|
|
929
931
|
}
|
|
930
932
|
if (a.state === 'no-dev-set' && /get_training_guardrails/.test(cmd)) {
|
|
931
933
|
// forge names the guardrails ONCE, here, before the split (Round 13):
|
package/src/tools/harness.js
CHANGED
|
@@ -647,11 +647,18 @@ export function buildCorpusArgv(params, {
|
|
|
647
647
|
}
|
|
648
648
|
const localModel = method === 'local-model'
|
|
649
649
|
|| (method != null && methodEntries?.[method]?.kind === 'local-model');
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
650
|
+
// the dictionary floor: word-for-word lookup in the file given as `model`
|
|
651
|
+
const lexicon = method === 'lexicon'
|
|
652
|
+
|| (method != null && methodEntries?.[method]?.kind === 'lexicon');
|
|
653
|
+
if (attest_local_transport === true && (localModel || lexicon)) {
|
|
654
|
+
throw new Error(`attest_local_transport does not apply to method "${method}": the harness runs it `
|
|
655
|
+
+ 'in this process, so nothing leaves the machine and no attestation is needed (or recorded). '
|
|
653
656
|
+ 'Drop attest_local_transport.');
|
|
654
657
|
}
|
|
658
|
+
if (lexicon && !model) {
|
|
659
|
+
throw new Error('method "lexicon" needs model: the dictionary file to look words up in (a .tsv, '
|
|
660
|
+
+ 'source<TAB>target per line) — the harness refuses a run without one (-m)');
|
|
661
|
+
}
|
|
655
662
|
if (model != null) {
|
|
656
663
|
// A method's model is a name in ITS world (a Hugging Face id, a local
|
|
657
664
|
// model directory, a plugin's own model name); an LLM provider's is a
|
|
@@ -669,9 +676,9 @@ export function buildCorpusArgv(params, {
|
|
|
669
676
|
// An OPUS-MT pair model names its own pair (opus-mt-en-fi writes Finnish):
|
|
670
677
|
// the harness refuses one whose pair visibly differs from the run's unless
|
|
671
678
|
// the user asks for it (a related-language baseline).
|
|
672
|
-
if (allow_model_pair_mismatch === true && !localModel) {
|
|
673
|
-
throw new Error('allow_model_pair_mismatch applies to
|
|
674
|
-
+ '
|
|
679
|
+
if (allow_model_pair_mismatch === true && !localModel && !lexicon) {
|
|
680
|
+
throw new Error('allow_model_pair_mismatch applies to methods "local-model" (an OPUS-MT pair model run '
|
|
681
|
+
+ 'against another pair) and "lexicon" (a dictionary whose sidecar names another pair) only — drop it.');
|
|
675
682
|
}
|
|
676
683
|
if (base_url != null) {
|
|
677
684
|
if (!['local', 'openai'].includes(provider)) throw new Error('base_url applies only to provider "local" or "openai"');
|
|
@@ -691,7 +691,7 @@ export function formatLanguage(r) {
|
|
|
691
691
|
p.publishedAt && `published ${String(p.publishedAt).slice(0, 10)}`,
|
|
692
692
|
p.sourceCount != null && `${p.sourceCount} cited sources`,
|
|
693
693
|
p.coverage && p.coverage.total && `${p.coverage.present}/${p.coverage.total} card components present`,
|
|
694
|
-
].filter(Boolean).join(' · ') || 'see the card'}. Card spec: https://champollion.dev/docs/
|
|
694
|
+
].filter(Boolean).join(' · ') || 'see the card'}. Card spec: https://champollion.dev/docs/reference/language-card-spec`);
|
|
695
695
|
out.push(`Next: language_overview { "code": "${s.code}" } — benchmarks, results, methods, constraints and the next steps for building a model.`);
|
|
696
696
|
return out.join('\n');
|
|
697
697
|
}
|
package/src/tools/languages.js
CHANGED
|
@@ -318,28 +318,35 @@ export function uncitedReason(lang, it = 'it', { brief = false } = {}) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
* The card's alternate names (ISO 639-3's
|
|
322
|
-
* capped, with the overflow counted.
|
|
323
|
-
*
|
|
321
|
+
* The card's alternate names (ISO 639-3's name index, SIL langtags), as
|
|
322
|
+
* attributed facts — capped, with the overflow counted. A spelling several
|
|
323
|
+
* registries record is one fact citing every one of them (Montagnais: ISO
|
|
324
|
+
* 639-3 and SIL langtags), and the names more registries record come first:
|
|
325
|
+
* with dozens of names on some cards and four shown, the corroborated ones
|
|
326
|
+
* are the ones a reader should see. Codes and the language's own name are not
|
|
327
|
+
* alternate names.
|
|
324
328
|
*
|
|
325
329
|
* @returns {{facts: Array<{text: string, sources: string[]}>, more: number}}
|
|
326
330
|
*/
|
|
327
331
|
function alternateNamesOf(card, ctx) {
|
|
328
|
-
const
|
|
329
|
-
const seen = new Set([String(card?.name ?? '').toLowerCase()]);
|
|
330
|
-
let total = 0;
|
|
332
|
+
const byKey = new Map([[String(card?.name ?? '').toLowerCase(), null]]);
|
|
331
333
|
for (const c of claimsFor(card, 'alternateNames', ['alternateNames'], ctx)) {
|
|
332
334
|
for (const n of (Array.isArray(c.value) ? c.value : [c.value])) {
|
|
333
335
|
// A bare lowercase 2–3 letter string is a code, not a name ("Ata" is a name).
|
|
334
336
|
if (typeof n !== 'string' || !n.trim() || /^[a-z]{2,3}$/.test(n.trim())) continue;
|
|
335
337
|
const key = n.trim().toLowerCase();
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
if (!byKey.has(key)) byKey.set(key, { text: n.trim(), sources: [] });
|
|
339
|
+
const fact = byKey.get(key);
|
|
340
|
+
if (!fact) continue; // the card's own name
|
|
341
|
+
for (const s of c.sources) if (!fact.sources.includes(s)) fact.sources.push(s);
|
|
340
342
|
}
|
|
341
343
|
}
|
|
342
|
-
|
|
344
|
+
const all = [...byKey.values()].filter(Boolean)
|
|
345
|
+
// stable: equally attested names keep the card's (alphabetical) order
|
|
346
|
+
.map((f, i) => ({ f, i }))
|
|
347
|
+
.sort((a, b) => b.f.sources.length - a.f.sources.length || a.i - b.i)
|
|
348
|
+
.map(({ f }) => f);
|
|
349
|
+
return { facts: all.slice(0, ALT_NAME_CAP), more: all.length - Math.min(all.length, ALT_NAME_CAP) };
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
/**
|
|
@@ -368,11 +375,15 @@ export function renderAttributed(facts, { unstamped = 'source not stated on this
|
|
|
368
375
|
* Every name a card records for a language, each with its source: the
|
|
369
376
|
* registries' names (the `name` envelope — normalizeCard keeps only the
|
|
370
377
|
* displayed one, so the claims are taken from the raw card BEFORE it is
|
|
371
|
-
* normalized: `nameClaims`)
|
|
378
|
+
* normalized: `nameClaims`), every endonym in the `endonym` envelope (a
|
|
372
379
|
* community's own name is often not the one displayed: the Cree
|
|
373
|
-
* macrolanguage's card shows ᓀᐦᐃᔭᐍᐏᐣ and also records nēhiyawēwin)
|
|
380
|
+
* macrolanguage's card shows ᓀᐦᐃᔭᐍᐏᐣ and also records nēhiyawēwin), and
|
|
381
|
+
* every alternate name (ISO 639-3's name index, SIL langtags) — one entry per
|
|
382
|
+
* spelling, citing every registry that records it, the shape the bundle's
|
|
383
|
+
* name-only entries carry (manifestSearchNames), so a match on one is said
|
|
384
|
+
* and cited the same way whichever index found it.
|
|
374
385
|
*
|
|
375
|
-
* @returns {Array<{text: string, field: 'name'|'endonym', source: string|null}>}
|
|
386
|
+
* @returns {Array<{text: string, field: 'name'|'endonym'|'alternate', source: string|null}>}
|
|
376
387
|
*/
|
|
377
388
|
function recordedNames(card, ctx, nameClaims = []) {
|
|
378
389
|
const out = [];
|
|
@@ -387,6 +398,17 @@ function recordedNames(card, ctx, nameClaims = []) {
|
|
|
387
398
|
for (const c of nameClaims) add(c?.value, 'name', c?.source);
|
|
388
399
|
for (const c of ctx.attributions?.(card?.endonym) ?? []) add(c?.value, 'endonym', c?.source);
|
|
389
400
|
if (card?.nativeName) add(card.nativeName, 'endonym', null);
|
|
401
|
+
const alternate = new Map();
|
|
402
|
+
for (const c of claimsFor(card, 'alternateNames', ['alternateNames'], ctx)) {
|
|
403
|
+
for (const n of (Array.isArray(c.value) ? c.value : [c.value])) {
|
|
404
|
+
// A bare lowercase 2–3 letter string is a code, not a name.
|
|
405
|
+
if (typeof n !== 'string' || !n.trim() || /^[a-z]{2,3}$/.test(n.trim())) continue;
|
|
406
|
+
const sources = alternate.get(n.trim()) ?? [];
|
|
407
|
+
for (const s of c.sources) if (!sources.includes(s)) sources.push(s);
|
|
408
|
+
alternate.set(n.trim(), sources);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const [text, sources] of alternate) add(text, 'alternate', sources.join(', ') || null);
|
|
390
412
|
return out;
|
|
391
413
|
}
|
|
392
414
|
|
|
@@ -516,10 +538,7 @@ function entryFromCard(card, ctx, { nameClaims = [] } = {}) {
|
|
|
516
538
|
: counts.length > 1 ? `${counts.join(' / ')} (sources differ)` : '');
|
|
517
539
|
const fam = card.classification?.family;
|
|
518
540
|
const code = card.code || card.iso639_3 || card.bcp47 || '';
|
|
519
|
-
const aliases = [
|
|
520
|
-
...(Array.isArray(card.alternateNames) ? card.alternateNames : []),
|
|
521
|
-
...(Array.isArray(card.aliases) ? card.aliases : []),
|
|
522
|
-
];
|
|
541
|
+
const aliases = Array.isArray(card.aliases) ? card.aliases : [];
|
|
523
542
|
const names = recordedNames(card, ctx, nameClaims);
|
|
524
543
|
return {
|
|
525
544
|
code,
|
|
@@ -534,13 +553,13 @@ function entryFromCard(card, ctx, { nameClaims = [] } = {}) {
|
|
|
534
553
|
region: card.macroarea || '',
|
|
535
554
|
typology: card.typologicalProfile?.verbSynthesis
|
|
536
555
|
|| card.typologicalProfile?.morphologicalSynthesis || '',
|
|
537
|
-
//
|
|
538
|
-
//
|
|
556
|
+
// Other CODES for the language. Its alternate NAMES are in `names`, each
|
|
557
|
+
// with its source — in `aliases` a match on one could not be cited.
|
|
539
558
|
aliases,
|
|
540
559
|
isolate: card.isIsolate === true,
|
|
541
|
-
// Every registry name and
|
|
542
|
-
// result found through one of them says which (Round 5
|
|
543
|
-
// nêhiyawêwin found nothing).
|
|
560
|
+
// Every registry name, endonym and alternate name, with its source:
|
|
561
|
+
// searchable, and a result found through one of them says which (Round 5
|
|
562
|
+
// school persona: nêhiyawêwin found nothing).
|
|
544
563
|
names,
|
|
545
564
|
// Those names read in the working script of the language's registered
|
|
546
565
|
// converter (Round 8: nêhiyawêwin found cre but never crk, whose card has
|
|
@@ -1147,8 +1166,11 @@ function matchTerms(index, qTokens, limit) {
|
|
|
1147
1166
|
used.add(at);
|
|
1148
1167
|
}
|
|
1149
1168
|
if (used.size === words.length) {
|
|
1169
|
+
// A name whose every word an earlier name already matched explains
|
|
1170
|
+
// nothing new ("Cree, Plains" after "Plains Cree"), so it is not listed.
|
|
1171
|
+
const adds = [...used].some((i) => !whole.has(i));
|
|
1150
1172
|
for (const i of used) { whole.add(i); covered.add(i); }
|
|
1151
|
-
if (!matched.some((m) => normalizeForMatch(m.text) === normalizeForMatch(text))) {
|
|
1173
|
+
if (adds && !matched.some((m) => normalizeForMatch(m.text) === normalizeForMatch(text))) {
|
|
1152
1174
|
matched.push({ text, field, ...(source ? { source } : {}), ...(derived ? { derived } : {}) });
|
|
1153
1175
|
}
|
|
1154
1176
|
if (field === 'name') ownName = 0;
|
package/src/tools/training.js
CHANGED
|
@@ -323,8 +323,8 @@ const COMMAND_ORDER = [
|
|
|
323
323
|
// user cannot reach. The public page below is the same map, symptom first.
|
|
324
324
|
const TAXONOMY_POINTER =
|
|
325
325
|
'The failure map (what you see → the likely cause → the forge guard or lever '
|
|
326
|
-
+ 'that fixes it): https://champollion.dev/docs/
|
|
327
|
-
+ '
|
|
326
|
+
+ 'that fixes it): https://champollion.dev/docs/agents/training'
|
|
327
|
+
+ '. Every forge refusal also carries its own what/why/fix. '
|
|
328
328
|
+ 'The forge_* MCP tools drive each guard; get_training_guardrails (this tool) '
|
|
329
329
|
+ 'is the rulebook behind them.';
|
|
330
330
|
|
|
@@ -399,9 +399,9 @@ export function formatTrainingGuardrails(answer) {
|
|
|
399
399
|
+ 'https://champollion.dev/docs/network/context/mt-training-concepts');
|
|
400
400
|
parts.push(
|
|
401
401
|
'- Step-by-step, agent-forward walkthrough: '
|
|
402
|
-
+ 'https://champollion.dev/docs/
|
|
402
|
+
+ 'https://champollion.dev/docs/your-first-translator');
|
|
403
403
|
parts.push(
|
|
404
404
|
'- The guardrails on one page: '
|
|
405
|
-
+ 'https://champollion.dev/docs/
|
|
405
|
+
+ 'https://champollion.dev/docs/agents/training');
|
|
406
406
|
return parts.join('\n').trim();
|
|
407
407
|
}
|
package/src/tools/translate.js
CHANGED
|
@@ -984,12 +984,12 @@ export async function translateTexts(args, deps = {}) {
|
|
|
984
984
|
method,
|
|
985
985
|
// The OpenRouter default slug only makes sense for the OpenRouter lane;
|
|
986
986
|
// every other engine (direct APIs, a local server) applies its own
|
|
987
|
-
// default when model is unset —
|
|
987
|
+
// default when model is unset — the OpenRouter default sent to Ollama
|
|
988
988
|
// or to Anthropic's API is a guaranteed failure. A method that came from
|
|
989
989
|
// the project brings the model sync would pair it with.
|
|
990
990
|
model: model
|
|
991
991
|
|| (methodSource === 'project' ? projectWouldBe?.model : null)
|
|
992
|
-
|| (method === 'llm' ?
|
|
992
|
+
|| (method === 'llm' ? champollion.DEFAULT_OPENROUTER_MODEL : undefined),
|
|
993
993
|
batchSize: champollion.DEFAULT_BATCH_SIZE ?? 20,
|
|
994
994
|
maxRetries: 2,
|
|
995
995
|
...reg,
|