Merge the two overlapping rule lists into one axis list, add the restored-paragraph arm and harden the coverage gate

This commit is contained in:
Claude (backend session) 2026-08-20 16:16:45 +03:00
parent 824bd209eb
commit 273d2df41f
2 changed files with 129 additions and 48 deletions

View file

@ -36,47 +36,49 @@
разворачивать идиому и сворачивать описание · выбирать слово, которого в словаре напротив
исходного не стоит. Буквальность здесь не достоинство. Если выбор стоит между точной калькой и
живой русской фразой — бери живую фразу.
⚠ Живой не значит нарядный. Простую фразу исходника не украшай: если герой идёт — он идёт, а не
шествует. Сворачивая описание, ты сжимаешь СЛОВА, а не факты: участники, действия, числа и
полярность остаются все до одного.
У этой свободы одна граница, и она важнее всего списка выше: ПОВТОР, СТОЯЩИЙ В ПАРАЛЛЕЛЬНЫХ
ПОЗИЦИЯХ САМОГО ИСХОДНИКА, — авторский рефрен, а не бедность словаря. Такой повтор сохраняй и НЕ
«разнообразь» лексику там, где автор повторяется намеренно.
«разнообразь» лексику там, где автор повторяется намеренно. Повтор, которого в исходнике нет, —
наоборот, дефект: немотивированные повторы убирай.
ЧЕГО НЕЛЬЗЯ — пять запретов, и они жёсткие:
1. Перевирать факт: участника действия, отрицание, число, ранг, меру, родство, кто кому что
сказал. Полярность и адресат неприкосновенны.
2. Терять смысловые ходы. Дробить и сливать можно; выбрасывать нельзя. Ничего не добавляй от себя
как факт — связка и уточнение допустимы, новое событие нет; сцены не пересочиняй.
3. Переводить буквально жест, идиому и реалию. Это три места, где подстрочник врёт чаще всего:
поклон — не «ударил головой»; идиому передавай смыслом, не сворачивая её в общее место и не
калькируя дословно; реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся
ошибкой смысла, даже если фраза звучит гладко.
4. Менять канон. Имена, термины и реалии — строго в тех формах, что даны в глоссарии ниже,
склоняя по контексту. Своих вариантов не придумывай.
5. Оставлять в выводе исходный язык, служебные преамбулы, комментарии, пояснения, заметки о
решениях и markdown-заголовки.
ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается ошибкой —
других запретов нет:
РЕГИСТР — держи тот же градус, что в исходнике:
грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Брань не смягчай и не
заменяй эвфемизмом: если персонаж ругается, он ругается. Не поднимай слог — там, где в исходнике
простое резкое слово, книжное слово будет ошибкой. Вежливость есть свойство персонажа, а не
переводчика: не добавляй вежливых форм и смягчающих оборотов, которых в реплике нет. Речевой
портрет героя (просторечие, архаика, чужой акцент) передавай лексикой и синтаксисом.
* СМЫСЛ. Перевран факт: участник действия, отрицание, число, ранг, мера, родство, кто кому что
сказал. Полярность и адресат неприкосновенны. Сюда же буквально переведённые жест, идиома и
реалия — три места, где подстрочник врёт чаще всего: обиходный жест называй тем, что он значит,
а не тем, из чего состоит; идиому передавай смыслом, не сворачивая в общее место и не калькируя;
реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся ошибкой смысла, даже
если фраза звучит гладко.
* ПОЛНОТА. Пропал смысловой ход исходника — или появилось событие, которого там не было. Дробить
и сливать можно; выбрасывать и досочинять нельзя, сцены не пересочиняй. Связка и уточнение
допустимы, новый факт — нет.
* КАНОН. Имя, термин или реалия стоит не в той форме, что дана в глоссарии (если он приложен).
Склоняй по контексту, своих вариантов не придумывай: книга выходит на тысячу с лишним глав, и
форма имени обязана совпасть во всех.
* ЯЗЫК. Фраза, которую носитель русского не написал бы сам: калька, канцелярит, переводной суржик,
немотивированный повтор.
* РЕГИСТР. Градус не тот, что в исходнике. Грубость передавай грубостью, ярость — яростью,
спокойствие — спокойствием. Брань не смягчай и не заменяй эвфемизмом: если персонаж ругается,
он ругается. Не поднимай слог: там, где в исходнике простое резкое слово, книжное будет ошибкой.
Вежливость есть свойство персонажа, а не переводчика — не добавляй вежливых форм и смягчающих
оборотов, которых в реплике нет. Речевой портрет героя (просторечие, архаика, чужой акцент)
передавай лексикой и синтаксисом.
ПО КАКИМ КРИТЕРИЯМ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ — проверь себя по ним сам, молча, перед выводом:
* СМЫСЛ — ни один факт не перевран (см. запрет 1);
* ПОЛНОТА — каждый смысловой ход исходника присутствует в переводе;
* КАНОН — каждый термин глоссария стоит в канонной форме;
* ЯЗЫК — в тексте нет ни одной фразы, которую носитель русского не написал бы сам: ни кальки,
ни канцелярита, ни переводного суржика;
* РЕГИСТР — градус тот же, что в исходнике.
Вольность в пределах этих критериев ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше и не
нарушает ни одного из пяти, лучше текста, который следует за буквой.
Вольность в пределах этих пяти ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше и не нарушает
ни одной из осей, лучше текста, который следует за буквой.
{{ПАР_ЕДИНИЦЫ}}
{{ПАРЕРЕВЁРСТКА}}
Максимума длины абзаца нет.
Выведи ТОЛЬКО перевод.
Выведи ТОЛЬКО перевод — без преамбул, комментариев, пояснений, заметок о решениях и
markdown-заголовков. Иноязычные вставки, которые велят сохранить правила выше, — не нарушение.
---USER---
Переведи следующий фрагмент:

View file

@ -82,10 +82,45 @@ PAIRS = {
"en": {"letter": "n", "phase": "ФД-N", "onepass": "E2",
"wire": EN.wire, "units": EN.units, "para": lambda: PARA.EN},
}
# арм → (модель, номер генерации). Генерация нужна, чтобы два прогона ОДНОЙ модели не схлопнулись
# в один тег: кэш `purchase` смотрит на существование файла и вернул бы первую клетку второй раз.
# ── ВОЗВРАТ АВТОРСКИХ АБЗАЦЕВ ИСХОДНИКУ ──────────────────────────────────────────────────────
# ⚠⚠ ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ АРМ, А НЕ ПОЧИНКА ЭКСТРАКТОРА. Наш английский экстрактор
# (`pair_en.raw_text`) сносит из epub ВСЕ теги регексом, включая `</p>`: в книге 11 447 абзацев
# медианой по 62 знака, а модели едет один сплошной блок. Это наш баг, и чинить его надо.
# НО uid единицы есть `sha1(source.strip())` (`bakeoff.uid_of`), то есть ЛЮБАЯ правка пробелов
# переименовывает все единицы. Цена такой правки замерена: **289 оплаченных клеток на $2.64**
# теряют адресата, плюс семь файлов слепых ключей и все судейские ответы английской пары.
# Манифест это поймает и остановит покупку — гейт работает, — но история оси всё равно умрёт.
#
# ⇒ Починка делается НА ПОДАЧЕ: тот же текст той же единицы, с тем же uid, но с возвращёнными
# границами абзацев. Функция взята у `podacha.py`, а не написана второй раз.
# ⇒ И это ОТДЕЛЬНЫЙ АРМ, потому что иначе замер стал бы некорректен: если `RN` получит
# восстановленный исходник, а его база `ZP` — плоский, контраст смешает эффект ПРОМТА с эффектом
# ПОДАЧИ, и разделить их будет нечем. Поэтому:
# RN / ZP — эффект ПРОМТА (вход у обоих ОДИНАКОВЫЙ, плоский)
# RA / RN — эффект ПОДАЧИ (промт у обоих ОДИНАКОВЫЙ, новый)
# ⚠ У китайской пары арма `RA` нет и быть не может: её исходник абзацы не терял (78 строк по
# 24 знака), возвращать нечего, и клетка была бы побайтным близнецом `RN`.
POD = _load("podacha_r", ZONE / "podacha.py")
def _src_en(src: str) -> str:
r = POD.restore_paragraphs(src, POD.EPUB_EN)
if not r.strip():
raise SystemExit("⛔ авторские абзацы для единицы не нашлись — арм RA стал бы близнецом "
"RN, покупка отказана")
return r
PAIRS["en"]["restore"] = _src_en
PAIRS["zh"]["restore"] = None
# арм → (модель, номер генерации, подача исходника). Генерация нужна, чтобы два прогона ОДНОЙ
# модели не схлопнулись в один тег: кэш `purchase` смотрит на существование файла и вернул бы
# первую клетку второй раз.
ARMS = {"RN": ("deepseek-v4-pro", 1), "RN2": ("deepseek-v4-pro", 2),
"RG": ("glm-5", 1), "RK": ("grok-4.3", 1)}
"RG": ("glm-5", 1), "RK": ("grok-4.3", 1),
"RA": ("deepseek-v4-pro", 1)} # тот же промт, ВОССТАНОВЛЕННЫЕ абзацы исходника
RESTORE_ARMS = ("RA",)
FREE = ("ZD", "Z0", "ZP")
PRIMARY = ("RN", "ZP") # единственный несущий контраст, см. шапку
@ -169,32 +204,38 @@ COVERAGE: tuple[tuple[str, str, str, str], ...] = (
("translator", "Хонорифики:", "промт", "Хонорифики"),
("translator", "Баланс форенизация", "промт", "форенизация/доместикация"),
("translator", "Сноски:", "промт", "Сноски"),
("translator", "ничего не пропускай", "промт", "выбрасывать нельзя"),
("translator", "не добавляй от себя", "промт", "не добавляй от себя"),
("translator", "ничего не пропускай", "промт", "выбрасывать и досочинять нельзя"),
("translator", "не добавляй от себя", "промт", "появилось событие, которого там не было"),
# ⚠ «смысл» — правило покрыто ПО СОДЕРЖАНИЮ другим, более полным местом промта, но не
# дословно. Диспозиция заведена отдельно от «блок» намеренно: смешивать «перенесено буквой» и
# «покрыто смыслом» значит потерять различие, на котором и погорела первая редакция.
("translator", "НЕ копируй построчную разбивку", "смысл",
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
("translator", "избегай канцелярита", "промт", "канцелярита"),
("translator", "Единицы и приёмы", "блок", "Единицы и приёмы"),
("translator", "избегай канцелярита", "промт", "канцеляр"),
("translator", "Выведи ТОЛЬКО перевод", "промт", "Выведи ТОЛЬКО перевод"),
("editor", "искажения смысла", "промт", "Перевирать факт"),
("editor", "искажения смысла", "промт", "Перевран факт"),
("editor", "неверно понятые реалии", "промт", "реалию сперва пойми"),
("editor", "кальки жестов и идиом", "промт", "жест, идиому и реалию"),
("editor", "кальки жестов и идиом", "промт", "подстрочник врёт"),
("editor", "а не подстрочником", "промт", "подстрочник врёт"),
("editor", "лишние повторы", "промт", "разнообразь"),
# ⚠ СЕРТИФИЦИРУЮЩЕЕ СЛОВО БЫЛО ВЗЯТО ИЗ ПРАВИЛА ПРОТИВОПОЛОЖНОГО СМЫСЛА. Строка
# «убирай лишние повторы» покрывалась словом «разнообразь», которое стоит в
# рефрен-границе — то есть там, где повторы убирать ЗАПРЕЩЕНО. Гейт печатал
# «покрыто», а требования убирать немотивированный повтор в промте не было вовсе.
# Поймано ревью Fable-5, воспроизведено исполнением. Сертификат пере-привязан.
("editor", "лишние повторы", "промт", "немотивированные повторы убирай"),
("editor", "авторский рефрен", "промт", "авторский рефрен"),
("editor", "СТРОГО по приведённому глоссарию", "промт", "Менять канон"),
("editor", "СТРОГО по приведённому глоссарию", "промт", "КАНОН"),
("editor", "склоняя по контексту", "промт", "склоняя по контексту"),
("editor", "не придумывай иных вариантов", "промт", "Своих вариантов не придумывай"),
("editor", "не придумывай иных вариантов", "промт", "вариантов не придумывай"),
("editor", "естественные русские абзацы", "смысл",
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
("editor", "Реплики прямой речи", "смысл",
"Каждую новую реплику начинай с нового абзаца через тире"),
("editor", "Сохраняй ПОЛНОТУ", "промт", "Терять смысловые ходы"),
("editor", "Сохраняй ПОЛНОТУ", "промт", "Пропал смысловой ход"),
("editor", "не пересочиняй сцены", "промт", "сцены не пересочиняй"),
("editor", "ДИСКУРС-ПЕРЕВЁРСТКА", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
# ⚠ Строк-ЗАГОЛОВКОВ обоих блоков в таблице нет намеренно: они не правила и не
# сканируются, а как строки таблицы были мёртвыми — то есть создавали видимость
# покрытия. Присутствие самих блоков сторожат отдельные проверки селфтеста.
# строки, которые приезжают ПАР-БЛОКАМИ дословно (их тело — данные пары, не наше)
("translator", "МЕРЫ", "блок", "Единицы и приёмы"),
("translator", "СТИХИ и названия классики", "блок", "Единицы и приёмы"),
@ -230,6 +271,12 @@ COVERAGE: tuple[tuple[str, str, str, str], ...] = (
_RULE_MARK = ("- ", "1.", "2.", "3.", "4.", "Ты —", "Жанр", "Твой мандат", "Выведи")
# ⚠ МЁРТВАЯ СТРОКА ТАБЛИЦЫ НЕОТЛИЧИМА ОТ ЖИВОЙ — и это дефект гейта, а не косметика:
# строка, не срабатывающая ни разу, создаёт видимость покрытия там, где его никто не проверял.
# Поймано ревью Fable-5. `fired` наполняется при разборе и сверяется селфтестом.
fired: set = set()
def coverage_report(p: str) -> list[tuple[str, str, str]]:
"""[(файл, строка боевого промта, вердикт)] — что покрыто, чем и что не покрыто вовсе."""
cfg = PAIRS[p]
@ -239,13 +286,17 @@ def coverage_report(p: str) -> list[tuple[str, str, str]]:
sysall = "\n".join(x["content"] for x in rol_msgs(p, u["source"]) if x["role"] == "system")
ub, rb = pair_blocks(p)
out = []
fired.clear()
for fname in ("translator", "editor"):
core, _f, _us = PR.load_template(d / f"{fname}.md")
for ln in core.splitlines():
s = ln.strip()
if len(s) < 25 or not s.startswith(_RULE_MARK):
# ⚠ ПОРОГ ДЛИНЫ БЫЛ СЛЕПЫМ ПЯТНОМ: правило «- Сноски: {{footnotes}}» короче
# 25 знаков и не сканировалось ВООБЩЕ, то есть могло пропасть незаметно.
if len(s) < 10 or not s.startswith(_RULE_MARK):
continue
hits = [c for c in COVERAGE if c[0] == fname and c[1] in s]
fired.update(c[:3] for c in hits)
if not hits:
out.append((fname, s[:90], "НЕ ОБЪЯВЛЕНО НИ ОДНОЙ ДИСПОЗИЦИЕЙ"))
continue
@ -268,6 +319,17 @@ def coverage_report(p: str) -> list[tuple[str, str, str]]:
return out
def src_of(p: str, arm: str, u: dict) -> str:
"""Исходник, который видит арм. ОДНА точка правды — иначе смета и покупка разъедутся.
Все армы, кроме `RA`, видят ПЛОСКИЙ исходник тот же самый, что видела база `ZP`. Иначе
несущий контраст `RN/ZP` смешал бы эффект промта с эффектом подачи.
"""
if arm in RESTORE_ARMS and PAIRS[p]["restore"]:
return PAIRS[p]["restore"](u["source"])
return u["source"]
def tag(p: str, arm: str, uid: str) -> str:
return f"dv-{PAIRS[p]['letter']}-{arm.lower()}-{uid}"
@ -307,6 +369,9 @@ def buy(p: str, which: list[str]) -> None:
print(f"\nпара {p} · глав {len(us)} · армы {' '.join(which)} · касса {cfg['phase']}")
for arm in which:
model, gen = ARMS[arm]
if arm in RESTORE_ARMS and not cfg["restore"]:
print(f" {arm} на паре {p} невозможен: исходник абзацев не терял — пропуск")
continue
for u in us:
tg = tag(p, arm, u["uid"])
if cell(tg).strip():
@ -315,8 +380,9 @@ def buy(p: str, which: list[str]) -> None:
# часами, и пересёкший 16:00 UTC цикл писал бы вызовы по вчерашнему прайсу.
ZK.price_gate(quiet=True)
rec = MONEY.purchase(led, tg, model, ED22.client(model),
call_kwargs(model, rol_msgs(p, u["source"])),
role="onepass", uid=u["uid"], arm=arm, gen=gen, pair=p)
call_kwargs(model, rol_msgs(p, src_of(p, arm, u))),
role="onepass", uid=u["uid"], arm=arm, gen=gen, pair=p,
podacha="абзацы" if arm in RESTORE_ARMS else "плоско")
if rec.get("skipped"):
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на клетке {tg} — прогон ОСТАНОВЛЕН.\n"
" Резать панель или поднимать потолок решением сессии ЗАПРЕЩЕНО.\n"
@ -405,6 +471,9 @@ def cmd_dry() -> None:
f"({_sys_len(p) / max(_sys_len_old(p), 1):.2f}×)")
sub = 0.0
for arm, (model, _g) in ARMS.items():
if arm in RESTORE_ARMS and not cfg["restore"]:
print(f" {arm:4s} {'':18s} на этой паре невозможен (исходник абзацев не терял)")
continue
need = sum(1 for u in us if not cell(tag(p, arm, u["uid"])).strip())
pr = price_of(p, model)
sub += need * pr
@ -447,7 +516,7 @@ def cmd_selftest() -> int:
ck(f"{p}: вольность РАЗРЕШЕНА явно", "ТЫ ВПРАВЕ" in sysall)
ck(f"{p}: регистр назван отдельной рамкой", "РЕГИСТР" in sysall)
ck(f"{p}: критерии проверки напечатаны в промте",
"ПО КАКИМ КРИТЕРИЯМ" in sysall and "ошибкой НЕ СЧИТАЕТСЯ" in sysall)
"ПО ЧЕМУ БУДЕТ ПРОВЕРЕН" in sysall and "ошибкой НЕ СЧИТАЕТСЯ" in sysall)
ck(f"{p}: черновик в промт НЕ попадает (однопроходка)",
"ЧЕРНОВ" not in sysall.upper() and "черновик" not in sysall.lower())
ck(f"{p}: банк-закон на месте либо термов в главе нет",
@ -479,6 +548,16 @@ def cmd_selftest() -> int:
miss = [r for r in rep if r[2].startswith("")]
ck(f"{p}: КАЖДОЕ правило боевых промтов имеет диспозицию и она исполнена",
not miss, f"не покрыто {len(miss)} из {len(rep)}: " + " | ".join(r[1][:50] for r in miss[:3]))
# ⚠ И ОБРАТНАЯ СТОРОНА: строка таблицы, не сработавшая НИ РАЗУ, создаёт видимость покрытия
# там, где никто ничего не проверял. Считается по ОБЕИМ парам сразу: часть правил живёт
# только у английской (французский слой, рамочный рассказ).
seen: set = set()
for p in PAIRS:
coverage_report(p)
seen |= fired
dead = [c for c in COVERAGE if c[:3] not in seen]
ck("в таблице COVERAGE нет МЁРТВЫХ строк (каждая срабатывает хотя бы раз)",
not dead, " | ".join(f"{c[0]}{c[1]}»" for c in dead[:4]))
ck("пары не делят буквы тегов", len({v["letter"] for v in PAIRS.values()}) == len(PAIRS))
ck("несущий контраст объявлен один", len(PRIMARY) == 2 and PRIMARY == ("RN", "ZP"))
for a, (mo, _g) in ARMS.items():