From 273d2df41f05a1f72c56c31c4091053e2bfbd024 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Thu, 20 Aug 2026 16:16:45 +0300 Subject: [PATCH] Merge the two overlapping rule lists into one axis list, add the restored-paragraph arm and harden the coverage gate --- eval/dovodka/prompts/onepass-core.md | 62 ++++++++------- eval/dovodka/rol.py | 115 ++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 48 deletions(-) diff --git a/eval/dovodka/prompts/onepass-core.md b/eval/dovodka/prompts/onepass-core.md index 9e3a41ca..edcd8af8 100644 --- a/eval/dovodka/prompts/onepass-core.md +++ b/eval/dovodka/prompts/onepass-core.md @@ -36,47 +36,49 @@ разворачивать идиому и сворачивать описание · выбирать слово, которого в словаре напротив исходного не стоит. Буквальность здесь не достоинство. Если выбор стоит между точной калькой и живой русской фразой — бери живую фразу. +⚠ Живой не значит нарядный. Простую фразу исходника не украшай: если герой идёт — он идёт, а не +шествует. Сворачивая описание, ты сжимаешь СЛОВА, а не факты: участники, действия, числа и +полярность остаются все до одного. У этой свободы одна граница, и она важнее всего списка выше: ПОВТОР, СТОЯЩИЙ В ПАРАЛЛЕЛЬНЫХ ПОЗИЦИЯХ САМОГО ИСХОДНИКА, — авторский рефрен, а не бедность словаря. Такой повтор сохраняй и НЕ -«разнообразь» лексику там, где автор повторяется намеренно. +«разнообразь» лексику там, где автор повторяется намеренно. Повтор, которого в исходнике нет, — +наоборот, дефект: немотивированные повторы убирай. -ЧЕГО НЕЛЬЗЯ — пять запретов, и они жёсткие: -1. Перевирать факт: участника действия, отрицание, число, ранг, меру, родство, кто кому что - сказал. Полярность и адресат неприкосновенны. -2. Терять смысловые ходы. Дробить и сливать можно; выбрасывать нельзя. Ничего не добавляй от себя - как факт — связка и уточнение допустимы, новое событие нет; сцены не пересочиняй. -3. Переводить буквально жест, идиому и реалию. Это три места, где подстрочник врёт чаще всего: - поклон — не «ударил головой»; идиому передавай смыслом, не сворачивая её в общее место и не - калькируя дословно; реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся - ошибкой смысла, даже если фраза звучит гладко. -4. Менять канон. Имена, термины и реалии — строго в тех формах, что даны в глоссарии ниже, - склоняя по контексту. Своих вариантов не придумывай. -5. Оставлять в выводе исходный язык, служебные преамбулы, комментарии, пояснения, заметки о - решениях и markdown-заголовки. +ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается ошибкой — +других запретов нет: -РЕГИСТР — держи тот же градус, что в исходнике: -грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Брань не смягчай и не -заменяй эвфемизмом: если персонаж ругается, он ругается. Не поднимай слог — там, где в исходнике -простое резкое слово, книжное слово будет ошибкой. Вежливость есть свойство персонажа, а не -переводчика: не добавляй вежливых форм и смягчающих оборотов, которых в реплике нет. Речевой -портрет героя (просторечие, архаика, чужой акцент) передавай лексикой и синтаксисом. +* СМЫСЛ. Перевран факт: участник действия, отрицание, число, ранг, мера, родство, кто кому что + сказал. Полярность и адресат неприкосновенны. Сюда же буквально переведённые жест, идиома и + реалия — три места, где подстрочник врёт чаще всего: обиходный жест называй тем, что он значит, + а не тем, из чего состоит; идиому передавай смыслом, не сворачивая в общее место и не калькируя; + реалию сперва пойми, а потом переводи — неверно понятая реалия остаётся ошибкой смысла, даже + если фраза звучит гладко. +* ПОЛНОТА. Пропал смысловой ход исходника — или появилось событие, которого там не было. Дробить + и сливать можно; выбрасывать и досочинять нельзя, сцены не пересочиняй. Связка и уточнение + допустимы, новый факт — нет. +* КАНОН. Имя, термин или реалия стоит не в той форме, что дана в глоссарии (если он приложен). + Склоняй по контексту, своих вариантов не придумывай: книга выходит на тысячу с лишним глав, и + форма имени обязана совпасть во всех. +* ЯЗЫК. Фраза, которую носитель русского не написал бы сам: калька, канцелярит, переводной суржик, + немотивированный повтор. +* РЕГИСТР. Градус не тот, что в исходнике. Грубость передавай грубостью, ярость — яростью, + спокойствие — спокойствием. Брань не смягчай и не заменяй эвфемизмом: если персонаж ругается, + он ругается. Не поднимай слог: там, где в исходнике простое резкое слово, книжное будет ошибкой. + Вежливость есть свойство персонажа, а не переводчика — не добавляй вежливых форм и смягчающих + оборотов, которых в реплике нет. Речевой портрет героя (просторечие, архаика, чужой акцент) + передавай лексикой и синтаксисом. -ПО КАКИМ КРИТЕРИЯМ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ — проверь себя по ним сам, молча, перед выводом: -* СМЫСЛ — ни один факт не перевран (см. запрет 1); -* ПОЛНОТА — каждый смысловой ход исходника присутствует в переводе; -* КАНОН — каждый термин глоссария стоит в канонной форме; -* ЯЗЫК — в тексте нет ни одной фразы, которую носитель русского не написал бы сам: ни кальки, - ни канцелярита, ни переводного суржика; -* РЕГИСТР — градус тот же, что в исходнике. -Вольность в пределах этих критериев ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше и не -нарушает ни одного из пяти, лучше текста, который следует за буквой. +Вольность в пределах этих пяти ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше и не нарушает +ни одной из осей, лучше текста, который следует за буквой. {{ПАР_ЕДИНИЦЫ}} {{ПАР_ПЕРЕВЁРСТКА}} +Максимума длины абзаца нет. -Выведи ТОЛЬКО перевод. +Выведи ТОЛЬКО перевод — без преамбул, комментариев, пояснений, заметок о решениях и +markdown-заголовков. Иноязычные вставки, которые велят сохранить правила выше, — не нарушение. ---USER--- Переведи следующий фрагмент: diff --git a/eval/dovodka/rol.py b/eval/dovodka/rol.py index 5b62978f..405bee14 100644 --- a/eval/dovodka/rol.py +++ b/eval/dovodka/rol.py @@ -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 ВСЕ теги регексом, включая `

`: в книге 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():