champollion 0.5.1 → 0.5.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/lib/command-help.js +2 -1
- package/lib/commands/models.js +50 -0
- package/lib/config.js +3 -1
- package/lib/content-refusals.js +54 -0
- package/lib/content-sync.js +18 -2
- package/lib/docusaurus-sync.js +21 -2
- package/lib/fallback.js +53 -21
- package/lib/methods/anthropic.js +2 -1
- package/lib/methods/gemini.js +2 -1
- package/lib/methods/openai.js +4 -3
- package/lib/model-defaults.js +113 -0
- package/lib/validate.js +65 -6
- package/package.json +1 -1
- package/shared/docent/corpus.json +13573 -0
- package/shared/model-defaults.json +47 -0
package/lib/validate.js
CHANGED
|
@@ -451,8 +451,11 @@ function isProtectedTermValue(value, protectedTerms = []) {
|
|
|
451
451
|
* until someone names it for a redo. Bump it whenever a check is loosened.
|
|
452
452
|
* 2 — 2026-10-05: names with citations/anchors, reference entries, tables,
|
|
453
453
|
* short titles and source-shown fullwidth letters stopped being refused.
|
|
454
|
+
* 3 — 2026-10-06: inline code is not prose (an ICU sample in backticks no
|
|
455
|
+
* longer makes a translated paragraph "ASCII-only"); a phrase said
|
|
456
|
+
* twice is not a loop; "keep it" twice is accepted give or take a stop.
|
|
454
457
|
*/
|
|
455
|
-
const GATE_VERSION =
|
|
458
|
+
const GATE_VERSION = 3;
|
|
456
459
|
|
|
457
460
|
/**
|
|
458
461
|
* Refusals that may be the model keeping text that is correct as written —
|
|
@@ -519,7 +522,7 @@ function isBibliographicEntry(text) {
|
|
|
519
522
|
const lines = t.split('\n').map(l => l.trim()).filter(Boolean);
|
|
520
523
|
return lines.length > 1 && lines.every(l => !l.includes('\n') && isBibliographicEntry(l));
|
|
521
524
|
}
|
|
522
|
-
return /^(?:\[?[A-Z]?\d{1,3}
|
|
525
|
+
return /^(?:\[?[A-Z]?\d{1,3}[a-z]?\]?[.)]?|[-*+])\s+\S/.test(t)
|
|
523
526
|
&& (/\(\d{4}[a-z]?\)/.test(t) || /\]\(https?:\/\//.test(t))
|
|
524
527
|
&& (/"[^"]{8,}"|“[^”]{8,}”/.test(t) || /\*[^*]{8,}\*/.test(t));
|
|
525
528
|
}
|
|
@@ -570,6 +573,43 @@ function contentGateFault(source, value, pairConfig = {}) {
|
|
|
570
573
|
return failures.length > 0 ? failures[0].reason : null;
|
|
571
574
|
}
|
|
572
575
|
|
|
576
|
+
/**
|
|
577
|
+
* Does `translated` show the shape of a degeneration loop, beyond a high
|
|
578
|
+
* repeated-n-gram rate? A phrase a language says twice where English
|
|
579
|
+
* elides it is not a loop: "From most to least trustworthy:" is correctly
|
|
580
|
+
* "Từ đáng tin cậy nhất đến ít đáng tin cậy nhất:" in Vietnamese, and was
|
|
581
|
+
* refused as a repetition hallucination (dogfood 2026-10-06). A loop does
|
|
582
|
+
* one of three things: repeats some span three times or more, repeats a whole
|
|
583
|
+
* line, or hands the source back verbatim beside a translation of it (the
|
|
584
|
+
* doubled Arabic headings, "### METEOR (…)\n### METEOR (…)").
|
|
585
|
+
*
|
|
586
|
+
* @param {string} translated
|
|
587
|
+
* @param {string} source
|
|
588
|
+
* @returns {boolean}
|
|
589
|
+
*/
|
|
590
|
+
function hasLoopEvidence(translated, source) {
|
|
591
|
+
const t = String(translated);
|
|
592
|
+
const grams = new Map();
|
|
593
|
+
for (let i = 0; i + REPETITION_LONG_N <= t.length; i++) {
|
|
594
|
+
const g = t.slice(i, i + REPETITION_LONG_N);
|
|
595
|
+
const n = (grams.get(g) || 0) + 1;
|
|
596
|
+
if (n >= 3 && g.trim().length >= 4) return true;
|
|
597
|
+
grams.set(g, n);
|
|
598
|
+
}
|
|
599
|
+
// A word said three times or more ("Qo' Qo' Qo' Qo'" — too short for an
|
|
600
|
+
// 8-character span to recur three times).
|
|
601
|
+
const words = new Map();
|
|
602
|
+
for (const w of t.toLowerCase().split(/\s+/u).filter(x => /\p{L}/u.test(x))) {
|
|
603
|
+
const n = (words.get(w) || 0) + 1;
|
|
604
|
+
if (n >= 3) return true;
|
|
605
|
+
words.set(w, n);
|
|
606
|
+
}
|
|
607
|
+
const lines = t.split('\n').map(l => l.trim()).filter(l => l.length >= 8);
|
|
608
|
+
if (new Set(lines).size < lines.length) return true;
|
|
609
|
+
const src = String(source).trim();
|
|
610
|
+
return src.length >= 8 && t.trim() !== src && t.includes(src);
|
|
611
|
+
}
|
|
612
|
+
|
|
573
613
|
/**
|
|
574
614
|
* Validate a batch of translations and return only passing keys.
|
|
575
615
|
*
|
|
@@ -775,7 +815,7 @@ function validateTranslations(translations, sourceFlat, pairConfig, options = {}
|
|
|
775
815
|
longGramRate: measureRepetition(seg, REPETITION_LONG_N),
|
|
776
816
|
}))
|
|
777
817
|
.find(m => m.trigramRate > trigramCap && m.longGramRate > longGramCap);
|
|
778
|
-
if (degenerateSegment) {
|
|
818
|
+
if (degenerateSegment && hasLoopEvidence(translated, source)) {
|
|
779
819
|
failures.push({
|
|
780
820
|
key,
|
|
781
821
|
reason: `repetition hallucination (${(degenerateSegment.trigramRate * 100).toFixed(0)}% repeated trigrams, ${(degenerateSegment.longGramRate * 100).toFixed(0)}% repeated ${REPETITION_LONG_N}-grams)`,
|
|
@@ -1300,12 +1340,30 @@ const FULLWIDTH_LATIN = /[A-Za-z]/u;
|
|
|
1300
1340
|
* URL value is a wrong-script answer unless the key is declared
|
|
1301
1341
|
* no-translate — which never reaches this check.)
|
|
1302
1342
|
*/
|
|
1343
|
+
/** `text` with every balanced {…} group removed (nested groups included). */
|
|
1344
|
+
function withoutBraceGroups(text) {
|
|
1345
|
+
let out = '';
|
|
1346
|
+
let depth = 0;
|
|
1347
|
+
for (const ch of text) {
|
|
1348
|
+
if (ch === '{') { depth++; continue; }
|
|
1349
|
+
if (ch === '}' && depth > 0) { depth--; continue; }
|
|
1350
|
+
if (depth === 0) out += ch;
|
|
1351
|
+
}
|
|
1352
|
+
return out;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1303
1355
|
function proseOf(text) {
|
|
1356
|
+
// Inline code is never prose: a Markdown paragraph quoting an ICU message
|
|
1357
|
+
// in backticks (`{n, plural, one {One file} other {…}}`) was read as that
|
|
1358
|
+
// message, its English branches were taken for the whole value, and 470
|
|
1359
|
+
// characters of correct Japanese were refused as "ASCII-only" — twice, in
|
|
1360
|
+
// five languages (dogfood 2026-10-06, docs/getting-started/configuration.md).
|
|
1361
|
+
const str = String(text).replace(/(`+)[^`]*?\1/g, ' ');
|
|
1304
1362
|
// A plural/select message: its prose is the text of its branches (the
|
|
1305
|
-
// keywords and selectors — "plural", "one", "other" — are code)
|
|
1306
|
-
|
|
1363
|
+
// keywords and selectors — "plural", "one", "other" — are code) PLUS the
|
|
1364
|
+
// text around it ("You have {count, plural, …}" — "You have" is prose).
|
|
1307
1365
|
const branches = str.includes('{') ? pluralBranchTexts(str) : null;
|
|
1308
|
-
return (branches ? branches.join(' ') : str)
|
|
1366
|
+
return (branches ? `${branches.join(' ')} ${withoutBraceGroups(str)}` : str)
|
|
1309
1367
|
// Simple placeholders only: {name}, {count, number}. Branch text such as
|
|
1310
1368
|
// "{Один файл}" is prose, never stripped.
|
|
1311
1369
|
.replace(/\{\s*[\w.$-]+\s*(?:,[^{}]*)?\}/g, ' ')
|
|
@@ -1467,6 +1525,7 @@ export {
|
|
|
1467
1525
|
nameCore,
|
|
1468
1526
|
GATE_VERSION,
|
|
1469
1527
|
isKeepAsWrittenFault,
|
|
1528
|
+
hasLoopEvidence,
|
|
1470
1529
|
isBibliographicEntry,
|
|
1471
1530
|
tableProse,
|
|
1472
1531
|
SHORT_SOURCE_LENGTH_SLACK,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "champollion",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Research-grade translation engine for i18n projects. Pluggable methods, per-pair quality tiers, and deterministic script converters. Supports JSON (next-intl, i18next), TOML, and YAML (Hugo).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|