textmachine/eval/dovodka/rol.py

1068 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""ОДНОПРОХОДКА КАК РОЛЬ — новый промт против прежней склейки, на нескольких моделях. Платный.
⚠ ЗАЧЕМ. Однопроходка — единственный живой продуктовый кандидат из всего, что дуга 19→23
измерила: по судейской оси от боевой связки не отличается, по банку терминов ЛУЧШЕ неё на обеих
парах (потерянных терминов на главу 1.38 против 2.88 на zh, 0.31 против 0.56 на en), стоит ОДИН
вызов вместо двух, и при слепом чтении владелец поставил её ПЕРВОЙ на английском.
⚠⚠ И всё это она делает С ГАНДИКАПОМ. Её промт (`contour.a2_msgs`) — не продуктовый, а
ИЗМЕРИТЕЛЬНЫЙ: боевой промт переводчика плюс мандатная часть боевого промта редактора,
механически склеенные (четыре строки выброшены, четыре подставлены), с прямой мета-фразой про наш
конвейер — «отдельного редактора после тебя НЕ БУДЕТ». Склейка сделана по уважительной причине:
в эксп-21 у однопроходки системный промт был вдвое короче, и замеренная разница могла быть
разницей ОБЪЁМА ИНСТРУКЦИИ, а не топологии. Уравнивание было правильным ДЛЯ ЗАМЕРА — но
следствие в том, что однопроходка ни разу не испытывалась как нормально написанный промт-роль.
ЧТО ПОКУПАЕТСЯ (имена армов ОДНИ на обе пары, пара живёт в данных):
ZP ПРЕЖНЯЯ СКЛЕЙКА, deepseek-v4-pro БЕСПЛАТНО, куплена заходом Д17 — это база
Z0 боевая связка (черновик → перепис) БЕСПЛАТНО — эталон продакшена
ZF вторая генерация связки БЕСПЛАТНО — пол СТАРОЙ панели
ZD голый черновик БЕСПЛАТНО — якорь низа
RN НОВАЯ РОЛЬ, deepseek-v4-pro, генерация 1 покупается — несущий арм
RN2 НОВАЯ РОЛЬ, deepseek-v4-pro, генерация 2 покупается — СВОЙ пол новой панели
RG НОВАЯ РОЛЬ, glm-5 покупается — переносимость, другой вендор
RK НОВАЯ РОЛЬ, grok-4.3 покупается — переносимость, третья семья
⚠ ПЕРВИЧНЫЙ КОНТРАСТ ОДИН: `RN/ZP` — новый промт против прежней склейки НА ТОЙ ЖЕ МОДЕЛИ и ТЕХ ЖЕ
главах. Всё остальное описательное. Одного контраста требует мощность: при n=16 и поправке Холма
по четырём контрастам MDE вырос бы выше цели, и ось не дала бы вывода ни при каком исходе — ровно
та болезнь, что погубила английскую ось эксп-22.
⚠ ТРЕТЬЯ МОДЕЛЬ — `grok-4.3`, и выбор не случаен. Главная претензия владельца при слепом чтении
была РЕГИСТР: «смягчённый мат там, где на мате держится сцена». xAI — единственный вендор, у
которого слова `violen*` в правилах нет вовсе, и модель меньше прочих склонна смягчать.
⚠ `gemini` в панель НЕ берётся: на этом материале он массово падает в контент-фильтр (эксп-21:
10 клеток судейского прогона с `PROHIBITED_CONTENT`), и его пустые клетки мерили бы фильтр, а не
промт. `grok-4.3` идёт с ВКЛЮЧЁННЫМ размышлением (вендор-дефолт `low`): запрет D30.1 прямой —
grok с reasoning-off в редакторской роли есть no-op-редактор.
Запуск: rol.py --show [zh|en] | --dry | --selftest | --buy [zh|en] [армы…] | --canon
"""
from __future__ import annotations
import importlib.util
import json
import re
import statistics as st
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
sys.path.insert(0, str(REPO / "eval" / d))
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
# ⚠ Контур берётся У ДРАЙВЕРА английской оси, а не грузится вторым экземпляром: `en_axis` держит
# СВОЙ `contour.py`, и `wire()` настраивает хуки именно в нём. Свой второй экземпляр остался бы с
# китайскими хуками, и английская ось молча стала бы пустой. Класс уже стоил фазе оплаченных клеток.
EN = _load("en_axis_r", ZONE / "en_axis.py")
C = EN.C
MONEY, PR, ROSTER, ED22, PARA = C.MONEY, C.PR, C.ROSTER, C.ED22, C.PARA
OUT = MONEY.OUT
ZK = _load("zakhod_r", ZONE / "zakhod.py") # гейт перелома цены, пред-полёт, отбор глав
CORE = ZONE / "prompts" / "onepass-core.md"
MAX_OUT = 32000 # как в заходе Д17: обрыв на потолке стоит денег и не даёт данных
# ── ПАРА КАК ДАННЫЕ. Ни одна ветка ниже не спрашивает «а это китайский?» ──────────────────────
PAIRS = {
"zh": {"letter": "m", "phase": "ФД-M", "onepass": "A2",
"wire": lambda: C.configure(PARA.ZH), "units": lambda: C.units_ok(),
"para": lambda: PARA.ZH},
"en": {"letter": "n", "phase": "ФД-N", "onepass": "E2",
"wire": EN.wire, "units": EN.units, "para": lambda: PARA.EN},
}
# ── ВОЗВРАТ АВТОРСКИХ АБЗАЦЕВ ИСХОДНИКУ ──────────────────────────────────────────────────────
# ⚠⚠ ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ АРМ, А НЕ ПОЧИНКА ЭКСТРАКТОРА. Наш английский экстрактор
# (`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), "RGT": ("glm-5", 1), "RK": ("grok-4.3", 1),
"RA": ("deepseek-v4-pro", 1), # тот же промт, ВОССТАНОВЛЕННЫЕ абзацы исходника
"RC": ("deepseek-v4-pro", 1), # промт МИНУС рамка оценки, см. CUT
"RB": ("deepseek-v4-pro", 1)} # рамка оценки ПЕРЕВЁРНУТА: брак = чужой язык
RESTORE_ARMS = ("RA",)
# ⚠ РЕЗАНЫЕ ВАРИАНТЫ ПРОМТА. Замер `RN` дал ТРИ независимых показания одного сдвига: размышление
# ×28 против прежней склейки при входе ×1.17, слепое чтение — «самый точный по деталям, но проза
# самая серая: гладкопись подстрочника», перевёрстка 1.60 против 1.771.79 у обеих старых схем.
# Модель стала аккуратнее и трусливее по всем осям сразу.
# Гипотеза: пять напечатанных осей, «по которым проверят результат», заставляют оптимизировать
# ЧЕК-ЛИСТ, а не текст — держаться ближе к букве есть самый дешёвый способ не нарушить пять
# критериев разом. Ровно этот отказ предсказывали и приёмка Fable-5, и документация Anthropic
# (перенесённые инструкции самопроверки дают избыточную проверку).
# ⚠ РЕЖЕТСЯ РОВНО ОДИН БЛОК, остальное побайтно то же — иначе замер не скажет, что подействовало.
# Селфтест это ПРОВЕРЯЕТ: разница промтов обязана быть равна вырезанному куску и ничему больше.
# ⚠ ПЕРВАЯ РЕДАКЦИЯ РЕЗА БЫЛА НЕГОДНОЙ, и поймала её проверка исполнением ДО покупки: вырезание
# блока целиком уносило 1871 знак — вместе с ним исчезали ВСЕ запреты и рамка РЕГИСТРА, лучшая
# часть промта по мнению приёмки. Это был бы замер «промт без правил», а не «промт без чек-листа».
# ⇒ Режется РОВНО РАМКА ОЦЕНКИ, содержание остаётся побайтно: «по чему будет проверен результат,
# это же и есть список ошибок» → «чего нельзя». Одна переменная: знает ли модель, что её ОЦЕНЯТ
# по перечню. Убирается и мета-строка про «вольность ошибкой не считается» — она сама ссылается
# на оценку и без рамки повисает.
# ⚠ RB — ВТОРАЯ ФОРМУЛИРОВКА, И ОНА ОТ ВЛАДЕЛЬЦА. Разбор Fable-5 показал, ПОЧЕМУ экзамен
# переиграл свободы: все пять осей сформулированы как оси ИЗБЕГАНИЯ, и серая проза проходит все
# пять — «ни кальки, ни канцелярита» гладкий подстрочник проходит, факт/полноту/канон проходит
# лучше всех. Весь риск объявленного экзамена лежал на смелости и ни грамма на осторожности,
# значит держаться буквы было доминантной стратегией.
# Лечение — не убрать экзамен, а ПЕРЕВЕРНУТЬ его риск. Формулировка владельца 20.08 дословно:
# «не прям серость, я бы сказал присутствие одного языка в другом. То есть полное отсутствие
# художественности и родного звучания текста. Машинный перевод грубо говоря. Всё же перевод это
# отчасти какое-то но творчество, без перевирания смысла нужно написать текст, чтоб он звучал
# естественно». Это уточняет его же решение 16.08 («штрафовать только за перевирание смысла»):
# вторым браком объявляется не вольность, а ПРОСВЕЧИВАЮЩИЙ ИСХОДНЫЙ ЯЗЫК.
_BRAK = (
"БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый.\n"
"Первый — перевранный смысл: факт, смысловой ход, канон.\n"
"Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо\n"
"звучащая фраза, отсутствие живого русского. Машинный перевод бракуется наравне с враньём.\n"
"Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски\n"
"естественно. Вольность не проверяет никто.\n\n"
"Ниже — то, что относится к первому браку и к границам второго:")
# ⚠⚠ ХВОСТ РЕЖЕТСЯ В ОБОИХ АРМАХ, И ЭТО ПОЧИНКА ДИЗАЙНА, А НЕ ФОРМУЛИРОВКИ. Приёмка Fable-5
# поймала: после того как императив самопроверки оправдан замером (его не было на момент покупки),
# ведущим подозреваемым становится ХВОСТОВАЯ СТРОКА «не нарушает ни одной из осей» — единственное
# оставшееся место, где смелость ОБУСЛОВЛЕНА полной верификацией пяти осей. В первой редакции
# обоих резов она стояла НЕТРОНУТОЙ, то есть при исходе «оба не сработали» вывод «рамка ни при
# чём» был бы неинтерпретируем: ведущий подозреваемый не вырезан нигде.
_TAIL_OLD = ("Вольность в пределах этих пяти ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше "
"и не нарушает\nни одной из осей, лучше текста, который следует за буквой.")
# ⚠ Формулировка второго брака — СЛОВА ВЛАДЕЛЬЦА 20.08: «присутствие одного языка в другом …
# полное отсутствие художественности и родного звучания … машинный перевод грубо говоря … перевод
# это отчасти какое-то но творчество, без перевирания смысла нужно написать текст, чтоб он звучал
# естественно». Правки по приёмке: убран дубль «так же строго»/«наравне с враньём»; «отсутствие
# живого русского» → «мёртвый русский» (проверка СВОЙСТВА, а не отсутствия — последняя глобальна
# по построению); строка-мост исправлена (ЯЗЫК и РЕГИСТР — СУЩЕСТВО второго брака, а не «границы»).
_BRAK = (
"БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый.\n"
"Первый — перевранный смысл: факт, смысловой ход, канон.\n"
"Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо\n"
"звучащая фраза, мёртвый русский. Машинный перевод — брак, даже когда в нём всё верно.\n"
"Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски\n"
"естественно. Вольность не проверяет никто.\n\n"
"Первому браку служат СМЫСЛ, ПОЛНОТА и КАНОН; второму — ЯЗЫК и РЕГИСТР:")
CUT = {
# RB: риск ПЕРЕВЁРНУТ. Хвост заменён не условием, а ПЕРЕОПРЕДЕЛЕНИЕМ: смелость перестаёт быть
# разрешённым исключением и становится проверяемым требованием.
"RB": [("ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается "
"ошибкой —\nдругих запретов нет:", _BRAK),
(_TAIL_OLD,
"Живая фраза вместо буквы — не вольность, а исполнение второго требования.")],
# RC: экзамена нет вовсе. Хвост тоже без условия верификации и без слова «оси», которое в RC
# повисало бы без референта — выше ни одна вещь осью не названа.
"RC": [("ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается "
"ошибкой —\nдругих запретов нет:", "ЧЕГО НЕЛЬЗЯ:"),
(_TAIL_OLD,
"Всё, что за пределами этих пяти, — твоя воля; живая фраза вместо буквы — норма, "
"а не риск.")],
}
FREE = ("ZD", "Z0", "ZP")
PRIMARY = ("RN", "ZP") # единственный несущий контраст, см. шапку
# ── СБОРКА ПРОМТА ────────────────────────────────────────────────────────────────────────────
# ⚠ ПАР-БЛОКИ НЕ ДУБЛИРУЮТСЯ, А ПОДТЯГИВАЮТСЯ ИЗ ФАЙЛОВ САМОЙ ПАРЫ. Второй экземпляр «Единиц и
# приёмов» разъехался бы с боевым молча, и замер мерил бы промт, которого в бою нет. Гейт
# пар-промтов сторожит те файлы, а не мою копию, — значит копии быть не должно.
UNITS_HEAD = "Единицы и приёмы"
REFLOW_HEAD = "ДИСКУРС-ПЕРЕВЁРСТКА"
def _block(core: str, head: str) -> str:
"""Секция от заголовка до пустой строки перед следующим заголовком верхнего уровня."""
i = core.find(head)
if i < 0:
return ""
tail = core[i:]
# секция кончается там, где начинается строка «Выведи ТОЛЬКО» или следующий ЗАГОЛОВОК КАПСОМ
stop = len(tail)
for mark in ("\nВыведи ТОЛЬКО", "\nХонорифики:"):
j = tail.find(mark)
if 0 <= j < stop:
stop = j
return tail[:stop].strip()
def pair_blocks(p: str) -> tuple[str, str]:
"""(«Единицы и приёмы» пары, «ДИСКУРС-ПЕРЕВЁРСТКА» пары) — из её же боевых файлов."""
d = PR.PAIR_DIR[PAIRS[p]["para"]().key]
tr_core, _f, _u = PR.load_template(d / "translator.md")
ed_core, _f2, _u2 = PR.load_template(d / "editor.md")
return _block(tr_core, UNITS_HEAD), _block(ed_core, REFLOW_HEAD)
def rol_msgs(p: str, src: str, arm: str = "RN") -> list[dict]:
"""Однопроходка НОВЫМ промтом: роль, рамки, регистр, критерии проверки, пар-блоки, глоссарий."""
cfg = PAIRS[p]
cfg["wire"]()
key = cfg["para"]().key
units_blk, reflow_blk = pair_blocks(p)
if not units_blk or not reflow_blk:
raise SystemExit(f"у пары {p} нет блока «{UNITS_HEAD}» либо «{REFLOW_HEAD}» — "
"промт собрать нечем, арм не строится")
core, _few, user = PR.load_template(CORE)
for was, now in CUT.get(arm, []):
if was not in core:
raise SystemExit(f"у арма {arm} не найден заменяемый кусок «{was[:40]}…» — "
"промт изменился под замером, СТОП")
core = core.replace(was, now)
core = (core.replace("{{ПАР_ЕДИНИЦЫ}}", units_blk)
.replace("{{ПАРЕРЕВЁРСТКА}}", reflow_blk))
brief = getattr(PR, PR.PAIR_BRIEF[key])
sysmsg = PR.render(core, brief, src, "")
# ⚠ БАНК-ЗАКОН НА ВСЕХ АРМАХ БЕЗ ИЗЪЯТИЙ (норма D39.104). Прежнее отступление — 72 клетки без
# блока — владелец пометил как дефект. Термы берутся ТОЛЬКО релевантные этому фрагменту:
# прямое требование брифа V6 «не давать слишком много контекста».
bf = None if key == "zh-ru" else cfg["para"]().bank_file
law = PR.law_block(PR.terms_in(src, bf), bf)
out = [{"role": "system", "content": sysmsg}]
if law:
# ⚠ БАНК-ЗАКОН НАПИСАН ДЛЯ ДВУХСТАДИЙНОГО ПУТИ и говорит «в ЧЕРНОВИКЕ термин слева должен
# быть передан формой справа». У однопроходки черновика нет вовсе: модель получила бы
# указание про текст, которого не существует. Поймано селфтестом ДО покупки. Правится
# ровно одно слово и ровно здесь — трогать общий `law_block` нельзя, он едет в бою.
out.append({"role": "system", "content": law.replace("в черновике", "в переводе")})
out.append({"role": "user", "content": PR.render(user, brief, src, "")})
return out
# ── ГЕЙТ ПОКРЫТИЯ БОЕВЫХ ПРОМТОВ ─────────────────────────────────────────────────────────────
# ⚠⚠ ЗАВЕДЁН ПО ЗАМЕЧАНИЮ ВЛАДЕЛЬЦА 20.08 («там же довольно много пропущено?»), И ЗАМЕЧАНИЕ БЫЛО
# ВЕРНЫМ. Первая редакция нового промта молча потеряла три правила боевых промтов, и одно из них
# потеряла ОПАСНО: «повтор в параллельных позициях исходника — авторский рефрен, сохраняй его» —
# при том что новый промт прямо говорит «буквальность не достоинство», то есть толкает в обратную
# сторону. Ещё два: «кальки жестов и идиом · неверно понятые реалии» и «не пересочиняй сцены».
#
# Обещание «я всё перенёс» проверяемо только механизмом, поэтому вот он. Каждое ПРАВИЛО боевых
# промтов перечислено ниже характерной подстрокой, и у каждого объявлена ДИСПОЗИЦИЯ:
# "промт" — правило стоит в ядре нового промта (проверяется вхождением ключевых слов);
# "блок" — правило приезжает пар-блоком дословно (проверяется вхождением самой строки);
# "снято" — правило снято СОЗНАТЕЛЬНО, с основанием прямо здесь.
# Селфтест роняет замер, если хоть одна строка боевого промта не покрыта ни одной диспозицией:
# правка боевого промта чужой зоной сломает мой гейт ГРОМКО, а не тихо изменит смысл арма.
COVERAGE: tuple[tuple[str, str, str, str], ...] = (
# (файл, характерная подстрока боевого правила, диспозиция, чем закрыто / основание)
("translator", "Хонорифики:", "промт", "Хонорифики"),
("translator", "Баланс форенизация", "промт", "форенизация/доместикация"),
("translator", "Сноски:", "промт", "Сноски"),
("translator", "ничего не пропускай", "промт", "выбрасывать и досочинять нельзя"),
("translator", "не добавляй от себя", "промт", "появилось событие, которого там не было"),
# ⚠ «смысл» — правило покрыто ПО СОДЕРЖАНИЮ другим, более полным местом промта, но не
# дословно. Диспозиция заведена отдельно от «блок» намеренно: смешивать «перенесено буквой» и
# «покрыто смыслом» значит потерять различие, на котором и погорела первая редакция.
("translator", "НЕ копируй построчную разбивку", "смысл",
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
("translator", "избегай канцелярита", "промт", "канцеляр"),
("translator", "Выведи ТОЛЬКО перевод", "промт", "Выведи ТОЛЬКО перевод"),
("editor", "искажения смысла", "промт", "Перевран факт"),
("editor", "неверно понятые реалии", "промт", "реалию сперва пойми"),
("editor", "кальки жестов и идиом", "промт", "подстрочник врёт"),
("editor", "а не подстрочником", "промт", "подстрочник врёт"),
# ⚠ СЕРТИФИЦИРУЮЩЕЕ СЛОВО БЫЛО ВЗЯТО ИЗ ПРАВИЛА ПРОТИВОПОЛОЖНОГО СМЫСЛА. Строка
# «убирай лишние повторы» покрывалась словом «разнообразь», которое стоит в
# рефрен-границе — то есть там, где повторы убирать ЗАПРЕЩЕНО. Гейт печатал
# «покрыто», а требования убирать немотивированный повтор в промте не было вовсе.
# Поймано ревью Fable-5, воспроизведено исполнением. Сертификат пере-привязан.
("editor", "лишние повторы", "промт", "немотивированные повторы убирай"),
("editor", "авторский рефрен", "промт", "авторский рефрен"),
("editor", "СТРОГО по приведённому глоссарию", "промт", "КАНОН"),
("editor", "склоняя по контексту", "промт", "склоняя по контексту"),
("editor", "не придумывай иных вариантов", "промт", "вариантов не придумывай"),
("editor", "естественные русские абзацы", "смысл",
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
("editor", "Реплики прямой речи", "смысл",
"Каждую новую реплику начинай с нового абзаца через тире"),
("editor", "Сохраняй ПОЛНОТУ", "промт", "Пропал смысловой ход"),
("editor", "не пересочиняй сцены", "промт", "сцены не пересочиняй"),
# ⚠ Строк-ЗАГОЛОВКОВ обоих блоков в таблице нет намеренно: они не правила и не
# сканируются, а как строки таблицы были мёртвыми — то есть создавали видимость
# покрытия. Присутствие самих блоков сторожат отдельные проверки селфтеста.
# строки, которые приезжают ПАР-БЛОКАМИ дословно (их тело — данные пары, не наше)
("translator", "МЕРЫ", "блок", "Единицы и приёмы"),
("translator", "СТИХИ и названия классики", "блок", "Единицы и приёмы"),
("translator", "Род и форму имён", "блок", "Единицы и приёмы"),
("translator", "РАМОЧНЫЙ РАССКАЗ", "блок", "Единицы и приёмы"),
("translator", "ФРАНЦУЗСКИЙ СЛОЙ", "блок", "Единицы и приёмы"),
("translator", "РЕЧЕВЫЕ ПОРТРЕТЫ", "блок", "Единицы и приёмы"),
("translator", "КАЛЬКИ en→ru", "блок", "Единицы и приёмы"),
("translator", "ЗАГЛАВНЫЕ РЕАЛИИ", "блок", "Единицы и приёмы"),
# у редактора СВОЯ сжатая копия единиц; в промт едет БОЛЕЕ ПОЛНАЯ версия переводчика
("editor", "Единицы и приёмы", "смысл", "Единицы и приёмы"),
("editor", "Молча определи смысловые ходы", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
("editor", "Один повествовательный ход", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
("editor", "Каждую новую реплику", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
("editor", "Разрешено объединять и делить", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
("editor", "Хонорифики:", "промт", "Хонорифики"),
# ⚠ ЕДИНСТВЕННОЕ СОЗНАТЕЛЬНОЕ СНЯТИЕ, И ОНО ОБЪЯВЛЕНО ЗДЕСЬ, А НЕ УМОЛЧАНО. Боевой промт
# переводчика требует «не сокращай»; решение владельца 16.08 («вольность не ошибка, живой
# язык — приоритет») это правило отменяет, и новый промт прямо разрешает «сворачивать
# описание». Полнота при этом охраняется НЕ запретом сокращать, а запретом терять смысловые
# ХОДЫ — то есть на уровне содержания, а не числа слов.
("translator", "не сокращай", "снято", "решение владельца 16.08: вольность не ошибка"),
# Роль редактора-правщика однопроходке не передаётся по построению: чинить нечего.
("editor", "Тебе даны ИСХОДНЫЙ текст и его черновой перевод", "снято", "черновика нет"),
("editor", "Сверяй смысл черновика", "снято", "черновика нет"),
("editor", "художественная редактура черновика", "снято", "черновика нет"),
("editor", "Ты — литературный редактор", "снято", "роль задана заново: переводчик"),
("editor", "Выведи ТОЛЬКО отредактированный", "снято", "своя строка вывода у переводчика"),
("translator", "Ты — профессиональный литературный переводчик", "снято", "роль задана заново"),
("translator", "Жанр книги:", "снято", "бриф перенесён своим блоком"),
("editor", "Жанр книги:", "снято", "бриф перенесён своим блоком"),
)
_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]
cfg["wire"]()
d = PR.PAIR_DIR[cfg["para"]().key]
u = chosen(p)[0]
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()
# ⚠ ПОРОГ ДЛИНЫ БЫЛ СЛЕПЫМ ПЯТНОМ: правило «- Сноски: {{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
for _f2, _sub, disp, what in hits:
if disp == "снято":
out.append((fname, s[:90], f"снято: {what}"))
elif disp == "смысл":
out.append((fname, s[:90], "покрыто смыслом"
if what in sysall else f"⛔ «{what}» НЕТ в промте"))
elif disp == "блок":
# ⚠ Сверяется САМА СТРОКА боевого промта, а не начало блока: слабая проверка
# («блок непустой») пропустила бы усечённый блок, из которого выпала половина
# правил. Строка рендерится тем же брифом, иначе `{{venuti}}` не совпадёт.
line = PR.render(s, getattr(PR, PR.PAIR_BRIEF[cfg["para"]().key]), "", "")
out.append((fname, s[:90],
"блок пары ДОСЛОВНО" if line in sysall else "⛔ строка НЕ доехала"))
else:
out.append((fname, s[:90],
"в промте" if what in sysall else f"⛔ «{what}» НЕТ в промте"))
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}"
def cell(tg: str) -> str:
f = OUT / f"{tg}.json"
if not f.exists():
return ""
d = json.loads(f.read_text(encoding="utf-8"))
return (d.get("content") or "") if d.get("finish") == "stop" else ""
def free_text(p: str, arm: str, uid: str) -> str:
"""Бесплатные армы берутся у рига захода Д17 — те же главы, те же тексты, $0."""
return ZK.free_text(p, arm, uid)
def chosen(p: str) -> list[dict]:
"""ТЕ ЖЕ главы, что у захода Д17 — иначе бесплатные армы не переиспользуются и база уедет."""
return ZK.chosen(p)
# ⚠ РАЗМЫШЛЕНИЕ У `glm-5` РОСТЕР ГАСИТ ПО УМОЛЧАНИЮ (`extra_body:{thinking:{type:disabled}}`,
# квирк-бюллетень :53). Для этого замера это КОНФАУНД, а не деталь: трейс 20.08 показал, что
# `dspro` пишет 8894% финального текста ВНУТРИ размышления, то есть промт-роль опирается ровно на
# тот механизм, который у `glm-5` выключен. Сравнивать «промт на dspro с думанием» с «тем же промтом
# на glm-5 без думания» значит мерить думание, а не переносимость промта.
# ⇒ арм `RGT` — тот же `glm-5` с ВЕНДОР-ДЕФОЛТНЫМ размышлением. `RG` остаётся: разность RG/RGT и
# есть цена размышления у этого вендора, замеренная попутно и бесплатно.
THINK_ON = ("RGT",)
def call_kwargs(model: str, msgs: list[dict], arm: str = "") -> dict:
"""Боевая конфигурация модели. Отступлений нет: у `grok-4.3` вендор-дефолт = размышление ON,
а запрет D30.1 прямой — reasoning-off в редакторской роли даёт no-op-редактора."""
kw = ROSTER.call_kwargs(model, msgs, max_out=MAX_OUT)
if arm in THINK_ON and isinstance(kw.get("extra_body"), dict):
eb = {k: v for k, v in kw["extra_body"].items() if k != "thinking"}
kw["extra_body"] = eb if eb else None # пустой extra_body вендору не шлём
if kw["extra_body"] is None:
del kw["extra_body"]
return kw
# ── ПОКУПКА ──────────────────────────────────────────────────────────────────────────────────
# ⚠ АБЛЯЦИИ НЕ НУЖНА ВСЯ ПАНЕЛЬ. Эффект, который ищется, огромен (×28 размышления), и виден он
# на трёх главах; тратить на него шестнадцать значит выбрать между двумя вариантами вместо того,
# чтобы купить оба. Ограничитель берёт ПЕРВЫЕ N глав отбора — тот же детерминированный порядок,
# значит абляция сравнима с уже купленным `RN` глава в главу.
N_LIMIT = next((int(a.split("=", 1)[1]) for a in sys.argv if a.startswith("--n=")), 0)
def buy(p: str, which: list[str]) -> None:
ZK.price_gate()
cfg = PAIRS[p]
cfg["wire"]()
ZK.preflight([ARMS[a][0] for a in which], cfg["phase"], cfg["letter"])
us = chosen(p)[:N_LIMIT] if N_LIMIT else chosen(p)
led = MONEY.Guarded(cfg["phase"], default_expect=0.03)
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():
continue
# ⚠ Гейт перелома цены — перед КАЖДОЙ клеткой, а не раз на старте: прогон длится
# часами, и пересёкший 16:00 UTC цикл писал бы вызовы по вчерашнему прайсу.
ZK.price_gate(quiet=True)
rec = MONEY.purchase(led, tg, model, ED22.client(model),
call_kwargs(model, rol_msgs(p, src_of(p, arm, u), arm), arm),
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"
" Это СТОП и вопрос владельцу: назвать недостающую сумму.")
if rec.get("finish") != "stop":
print(f"{tg}: finish={rec.get('finish')} "
f"{str(rec.get('error'))[:120]} — клетка объявляется несобранной")
elif C.draft_defect(cell(tg)):
# штатный гейт годности проекта: эхо исходника вместо перевода ловится ЗДЕСЬ,
# иначе негодная клетка уедет в судейскую пачку за полную цену
print(f"{tg}: {C.draft_defect(cell(tg))} — клетка НЕГОДНА, объявить в отчёте")
MONEY.report()
# ── $0-ГЕЙТ КАНОНА: там, где однопроходка уже выигрывала ──────────────────────────────────────
def canon() -> None:
print("КАНОН-ГЕЙТ ($0, детерминированно): потерянных терминов банка на главу, МЕНЬШЕ лучше\n")
for p, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
print(f"=== пара {p} · глав {len(us)} ===")
for arm in FREE + tuple(ARMS):
vals = []
for u in us:
t = free_text(p, arm, u["uid"]) if arm in FREE else cell(tag(p, arm, u["uid"]))
if t.strip():
vals.append(len(C.canon_gaps(u["source"], t)))
if vals:
print(f" {arm:4s} n={len(vals):2d} потерь/глава {st.mean(vals):5.2f}")
# ── СМЕТА И ПРОВЕРКИ ─────────────────────────────────────────────────────────────────────────
def price_of(p: str, model: str) -> float:
"""Ожидаемая цена клетки — из ЗАМЕРЕННЫХ токенов уже купленных ОДНОПРОХОДОК ЭТОЙ пары,
пере-оценённых ТЕКУЩИМ пином. Зашитых цен в файле нет: после пере-пина они врали вчетверо."""
toks = []
for f in OUT.glob(f"{C.tag(PAIRS[p]['onepass'], '')}*.json"):
if any(x in f.name for x in ("ERROR", "LENGTH", "ЭХО", "STALE", "TWIN")):
continue
r = json.loads(f.read_text(encoding="utf-8"))
if r.get("finish") == "stop" and r.get("prompt_tokens"):
toks.append((r["prompt_tokens"], r.get("cached_tokens") or 0,
r.get("completion_tokens") or 0, r.get("reasoning_tokens") or 0,
r.get("total_tokens") or 0))
if not toks:
return 0.03
med = [st.median([x[i] for x in toks]) for i in range(5)]
# ⚠ Новый промт ДЛИННЕЕ склейки на входе — берём фактическую длину системного промта, а не
# длину прежнего: смета, посчитанная по старому входу, занижала бы резерв гарда.
grow = _sys_len(p) / max(_sys_len_old(p), 1)
return ROSTER.cost(model, int(med[0] * grow), int(med[1]), int(med[2]), int(med[3]),
int(med[4]))
def _sys_len(p: str) -> int:
u = chosen(p)[0]
return sum(len(m["content"]) for m in rol_msgs(p, u["source"]))
def _sys_len_old(p: str) -> int:
PAIRS[p]["wire"]()
u = chosen(p)[0]
return sum(len(m["content"]) for m in C.a2_msgs(u["source"]))
def cmd_show(p: str) -> None:
"""Промт целиком, глазами. Обязательная вычитка ПЕРЕД покупкой."""
u = chosen(p)[0]
for m in rol_msgs(p, u["source"]):
body = m["content"]
if m["role"] == "user":
body = body.split("Переведи следующий фрагмент:")[0] + \
"Переведи следующий фрагмент:\n\n<текст главы, %d знаков>" % len(u["source"])
print(f"\n────────── {m['role'].upper()} ──────────\n{body}")
def cmd_dry() -> None:
print("ОДНОПРОХОДКА КАК РОЛЬ · главы те же, что у захода Д17\n")
ZK.price_gate()
tot = 0.0
for p, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
print(f"\n=== пара {p} · глав {len(us)} · касса {cfg['phase']} ===")
print(f" системный промт: новый {_sys_len(p)} знаков против склейки {_sys_len_old(p)} "
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
print(f" {arm:4s} {model:18s} надо {need:2d} × ${pr:.5f} = ${need * pr:.4f}")
sp = MONEY.Guarded(cfg["phase"]).spent()
ceil = MONEY.CEILINGS.get(cfg["phase"], 0.0)
mark = "ВЛЕЗАЕТ" if sub <= ceil - sp else f"НЕ ВЛЕЗАЕТ (не хватает ${sub - (ceil - sp):.4f})"
print(f" ИТОГО ${sub:.4f} · потолок ${ceil:.2f} · потрачено ${sp:.4f}{mark}")
tot += sub
print(f"\nВЕСЬ ЗАМЕР: ${tot:.4f} по текущему (пиковому) пину. В офф-пик DeepSeek вдвое дешевле.")
def cmd_selftest() -> int:
bad = 0
def ck(n: str, ok: bool, d: str = "") -> None:
nonlocal bad
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
for p, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
ck(f"{p}: глав ровно 16", len(us) == 16, f"их {len(us)}")
ck(f"{p}: все три бесплатных арма есть на каждой главе",
all(free_text(p, a, u["uid"]).strip() for a in FREE for u in us))
u = us[0]
m = rol_msgs(p, u["source"])
sysall = "\n".join(x["content"] for x in m if x["role"] == "system")
# ⚠ ПАР-БЛОКИ ДОЕХАЛИ. Пустой блок сделал бы промт беднее боевого, и замер сравнивал бы
# не «роль против склейки», а «роль минус пар-специфика против склейки».
ub, rb = pair_blocks(p)
ck(f"{p}: блок «{UNITS_HEAD}» пары подтянут", len(ub) > 200, f"{len(ub)} знаков")
ck(f"{p}: блок «{REFLOW_HEAD}» пары подтянут", len(rb) > 400, f"{len(rb)} знаков")
ck(f"{p}: оба блока стоят В ПРОМТЕ", ub[:60] in sysall and rb[:60] in sysall)
ck(f"{p}: плейсхолдеры раскрыты все до одного", "{{" not in sysall)
# ⚠ ГЛАВНОЕ ОТЛИЧИЕ ОТ СКЛЕЙКИ — мета-фразы про конвейер быть не должно
ck(f"{p}: мета-фразы про отсутствующего редактора НЕТ",
"НЕ БУДЕТ" not in sysall and "после тебя" not in sysall)
ck(f"{p}: вольность РАЗРЕШЕНА явно", "ТЫ ВПРАВЕ" in sysall)
ck(f"{p}: регистр назван отдельной рамкой", "РЕГИСТР" in sysall)
ck(f"{p}: критерии проверки напечатаны в промте",
"ПО ЧЕМУ БУДЕТ ПРОВЕРЕН" in sysall and "ошибкой НЕ СЧИТАЕТСЯ" in sysall)
ck(f"{p}: черновик в промт НЕ попадает (однопроходка)",
"ЧЕРНОВ" not in sysall.upper() and "черновик" not in sysall.lower())
ck(f"{p}: банк-закон на месте либо термов в главе нет",
bool(PR.law_block(PR.terms_in(u["source"], None if cfg["para"]().key == "zh-ru"
else cfg["para"]().bank_file),
None if cfg["para"]().key == "zh-ru"
else cfg["para"]().bank_file)) == (len(m) == 3),
f"сообщений {len(m)}")
ck(f"{p}: пользовательское сообщение несёт ТЕКСТ ГЛАВЫ",
u["source"][:80] in m[-1]["content"])
ck(f"{p}: ровно одно пользовательское сообщение",
sum(1 for x in m if x["role"] == "user") == 1)
# промт обязан ОТЛИЧАТЬСЯ от склейки, иначе замер пуст
ck(f"{p}: новый промт не равен склейке",
sysall.strip() != "\n".join(x["content"] for x in C.a2_msgs(u["source"])
if x["role"] == "system").strip())
# теги и касса
tgs = {tag(p, a, u["uid"]) for a in ARMS}
ck(f"{p}: теги армов различны", len(tgs) == len(ARMS))
ck(f"{p}: теги покрыты глобом кассы {cfg['phase']}",
all(t.startswith(f"dv-{cfg['letter']}-") for t in tgs))
ck(f"{p}: фаза заведена в кассе", cfg["phase"] in MONEY.CEILINGS)
ck(f"{p}: две генерации несущей модели — РАЗНЫЕ теги",
tag(p, "RN", u["uid"]) != tag(p, "RN2", u["uid"]))
# ⚠ ГЕЙТ ПОКРЫТИЯ — главная проверка этого файла. Обещание «я перенёс всё из боевых
# промтов» проверяемо только механизмом; замечание владельца 20.08 показало, что на слово
# его брать нельзя.
rep = coverage_report(p)
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():
try:
ck(f"цена считается для {a} ({mo})",
MONEY.M22.BUY.cost(mo, 3000, 0, 3600, 800, 7400) > 0)
except Exception as e: # noqa: BLE001
ck(f"цена считается для {a} ({mo})", False, f"{type(e).__name__}: {e}")
ck("grok идёт с размышлением (D30.1: reasoning-off = no-op-редактор)",
"reasoning_effort" not in json.dumps(call_kwargs("grok-4.3",
[{"role": "user", "content": "x"}])))
print(f"\n{'ЗАМЕР ГОДЕН К ПОКУПКЕ' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
# ── СЛЕПОЕ ЧТЕНИЕ ПАНЕЛИ ЭТОГО ЗАМЕРА ────────────────────────────────────────────────────────
# ⚠ ПЕРВИЧНЫЙ ПРИБОР КОНТРАСТА — ЧТЕНИЕ, А НЕ СЧЁТ (решение владельца 20.08). Основание замерено:
# счётчик ошибок согласен с владельцем на китайской паре (+0.80) и АНТИ-согласен на английской
# (0.80), а слепое чтение — +0.80 на обеих. Всё, что новый промт меняет (вольность, регистр),
# лежит ровно в осях, к которым счётчик слеп, поэтому судить им этот контраст значит гарантированно
# получить «неотличимо» и не понять, эффекта нет или прибора нет.
# ⚠ РАМКА ПАКЕТА НЕЙТРАЛЬНА. Норма, заведённая 20.08 после того, как я нашёл подсказку в СВОЁМ же
# пакете («вариант с наименьшим числом ошибок читался почти хуже всех»): пакет не имеет права
# называть читателю ни вердикт другого прибора, ни существование контроля. Замер показал, что
# подсказка стоила 0.20 Спирмена — верх и низ порядка устояли, но средние места переставились.
# ⚠ Перетасовка меток берётся у `chtenie.layout` — второй реализации того же не заводится.
CH = _load("chtenie_r", ZONE / "chtenie.py")
READ_SALT = "rol-2026-08-20"
READ_PANEL = ("ZP", "RN", "Z0", "ZF") # склейка · новая роль · боевая связка · её 2-я генерация
READ_DIR = Path.home() / "books" / "chtenie-rol"
READ_KEY = OUT / "blind-keys-rol" / "rol-KEY.json"
READ_HEAD = """
Ниже {n} переводов одного и того же отрывка, сделанных разными способами. Какой чем сделан — не
сказано намеренно, метки перемешаны, расшифровка лежит отдельно и до твоего ответа не открывается.
Исходник приложен. Оцени варианты по ТРЁМ вещам, в этом порядке важности:
1. **СМЫСЛ** — не переврано ли то, что сказано в исходнике: участник действия, отрицание, число,
ранг, родство, кто кому что говорит; не пропало ли и не добавилось ли событие.
2. **БРАК** — явные дефекты русского текста: незаконченная фраза, сбитая атрибуция реплики,
рассогласование, оставшийся кусок исходного языка, глосса в скобках посреди прозы, имя,
меняющее форму или род.
3. **ХУДОЖЕСТВЕННОСТЬ** — читается ли это как русская проза или как перевод: живой ритм и голос
против кальки, канцелярита и подстрочника.
## Как читать — это важнее скорости
Времени у тебя нет ограничения, и торопиться не надо. Прочти ВСЕ варианты целиком, прежде чем
выстраивать порядок: решение по первым двум, к которому потом подгоняются остальные, — самая
частая порча такого чтения.
Не выдумывай различий там, где их нет: если два варианта действительно неразличимы, соединяй их
знаком `=`, это законный ответ. И наоборот — не сваливай в кучу то, что различимо: если разница
мелкая, так и скажи, что мелкая, но порядок назови.
Думай столько, сколько нужно. Ответ на один абзац, выданный быстро, стоит меньше, чем разбор,
в котором видно, ЧЕМ один текст лучше другого.
## Что записать — в файл `{answer}`
1. **ПОРЯДОК** — строкой: `Т3 > Т1 > Т2 > Т4`, от лучшего к худшему. Неразличимые соединяй `=`.
Это ответ на вопрос «какой ты взял бы в книгу»: смысл и брак — пороги, художественность решает
между теми, кто пороги прошёл.
2. **По каждому варианту** — по фразе на каждую из трёх осей, если есть что сказать.
3. Если вариант читается откровенно машинным — скажи прямо.
---
"""
def read_texts(p: str, uid: str) -> dict:
"""Тексты панели чтения. Пусто у любого арма — глава в пакет не идёт целиком."""
out = {"_src": next(u["source"] for u in chosen(p) if u["uid"] == uid)}
for a in READ_PANEL:
t = free_text(p, a, uid) if a in FREE else (
cell(tag(p, a, uid)) if a in ARMS else ZK.cell(ZK.tag(p, a, uid)))
out[a] = t
return out
def emit_read(p: str, n_units: int = 3, panel: tuple = (), tag_: str = "",
only_uid: str = "") -> None:
"""Пакет слепого чтения: ключ пишется ДО ответов, раскладка ОДНА на пару.
⚠ ПАНЕЛЬ И ИМЯ ПАКЕТА — ПАРАМЕТРЫ, И ЭТО НЕ УДОБСТВО. Слепое чтение стало ШТАТНЫМ прибором
(решение владельца 20.08: «теперь судим так всё время»), значит панелей будет много. Ключ
каждой лежит в СВОЁМ файле: пере-эмиссия с другим составом армов переписала бы раскладку уже
прочитанной панели, и ответ читателя стал бы мусором молча — класс, уже стоивший фазе вердикта.
"""
global READ_PANEL, READ_KEY # noqa: PLW0603
if panel:
READ_PANEL = tuple(panel)
if tag_:
READ_KEY = OUT / "blind-keys-rol" / f"rol-KEY-{tag_}.json"
cfg = PAIRS[p]
cfg["wire"]()
full = []
for u in chosen(p):
tx = read_texts(p, u["uid"])
if all(tx[a].strip() for a in READ_PANEL):
full.append((u["uid"], tx))
if only_uid:
full = [x for x in full if x[0] == only_uid]
full.sort(key=lambda x: -sum(len(x[1][a]) for a in READ_PANEL))
take = full[:n_units]
if not take:
raise SystemExit(f"у пары {p} нет ни одной главы с ПОЛНОЙ панелью {READ_PANEL}")
READ_DIR.mkdir(parents=True, exist_ok=True)
READ_KEY.parent.mkdir(parents=True, exist_ok=True)
key = json.loads(READ_KEY.read_text(encoding="utf-8")) if READ_KEY.exists() else {
"_что": "слепое чтение панели замера «однопроходка как роль»",
"_соль": READ_SALT, "_панель": list(READ_PANEL),
"онтроль": "Z0 и ZF — две генерации ОДНОЙ боевой связки"}
ans = READ_DIR / f"ОТВЕТ-{p}{('-' + tag_) if tag_ else ''}.md"
lay = CH.layout(p, f"ROL-ALL|{READ_SALT}|{tag_}", "READER", READ_PANEL)
body = [f"# {'КИТАЙСКАЯ' if p == 'zh' else 'АНГЛИЙСКАЯ'} ПАРА — слепое чтение\n",
READ_HEAD.format(n=len(READ_PANEL), answer=ans)]
for j, (uid, tx) in enumerate(take, 1):
body += [f"\n# ОТРЫВОК {j}\n",
f"## Исходник ({'китайский' if p == 'zh' else 'английский'})\n", tx["_src"], ""]
for lab, arm in lay.items():
body += ["\n---\n", f"## Отрывок {j}, вариант {lab}\n", tx[arm], ""]
key.setdefault(p, {})[str(j)] = {"uid": uid, "метки": lay,
"знаков": {a: len(tx[a]) for a in READ_PANEL}}
(READ_DIR / f"ЧТЕНИЕ-{p}{('-' + tag_) if tag_ else ''}.md").write_text(
"\n".join(body), encoding="utf-8")
if not ans.exists():
ans.write_text(f"# Ответ — пара {p}\n\nПОРЯДОК: \n\nПо вариантам:\n", encoding="utf-8")
READ_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8")
print(f" {p}: глав {len(take)} · вариантов {len(READ_PANEL)} · "
f"{sum(len(tx[a]) for _u, tx in take for a in READ_PANEL) // 1000} тыс. знаков")
print(f" пакет → {READ_DIR}/ЧТЕНИЕ-{p}{('-' + tag_) if tag_ else ''}.md · "
f"ключ → {READ_KEY.name} (читателю НЕ давать)")
def score_read() -> None:
if not READ_KEY.exists():
raise SystemExit("⛔ ключа нет — сначала --emit-read")
key = json.loads(READ_KEY.read_text(encoding="utf-8"))
import re as _re # noqa: PLC0415
for p in PAIRS:
f = READ_DIR / f"ОТВЕТ-{p}.md"
if not f.exists() or p not in key:
continue
txt = f.read_text(encoding="utf-8", errors="replace")
seen: list[str] = []
for x in _re.findall(r"Т\d+", txt):
if x not in seen:
seen.append(x)
lay = key[p]["1"]["метки"]
order = [lay[x] for x in seen if x in lay]
if len(order) < len(READ_PANEL):
print(f" {p}: порядок не разобран ({seen})")
continue
print(f" {p}: {' > '.join(order)}")
print(f" контроль шума Z0/ZF на местах {order.index('Z0') + 1} и {order.index('ZF') + 1}")
# ── СВОД ПАНЕЛИ «ОДНА ГЛАВА — ОДИН ПАКЕТ» ────────────────────────────────────────────────────
# ⚠ `score_read` выше разбирает ОДИН пакет и один ключ; здесь пакетов 15, ключей 15 и раскладок 15.
# Свод обязан (а) отказываться от главы, чей порядок не покрывает панель ровно один раз, вслух, и
# (б) печатать контроли рядом со средним местом, а не под ним: без них порядок ничего не значит.
# ⚠ РАЗБОР И РАНГИ БЕРУТСЯ У `chtenie` — второй реализации того же не заводится: тамошний `_ranks`
# уже несёт правило средних мест для связок, а `orders` — две починки, купленные потерянным ответом
# владельца. Здесь своё ТОЛЬКО правило ВЫБОРА цепочки: `orders` берёт первую и нумерует остальные
# как разные отрывки, а в пакете «одна глава — один пакет» отрывок один, зато читатель называет
# порядок и в заголовке, и внутри разбора («внутри семьи Т8 > Т2 > Т7»). Берём САМУЮ ПОЛНУЮ.
def _order_of(txt: str, labels: set) -> str:
best, best_n = "", 0
for m in CH._ORD.findall(txt): # noqa: SLF001
n = len({x for x in re.findall(r"[ТT]\d+", m) if x.replace("T", "Т") in labels})
if n > best_n:
best, best_n = m, n
return best
def _variants(pack: Path) -> dict:
"""Тексты пакета по меткам — нужны своду, чтобы контроль тождества стоял на побайтном
сравнении, а не на равенстве длин.
⚠ Разделитель `---`, стоящий ПЕРЕД следующим вариантом, обязан отрезаться: он прилипает к
хвосту предыдущего тела и переживает `strip()` (6 знаков). Прежняя редакция его оставляла, и
это было невидимо ровно потому, что сравнивались два ТЕЛА — артефакт стоял у обоих. Сверка с
клеткой поймала сразу: 84 «расхождения» из 96, все ровно на 6 знаков.
"""
txt = pack.read_text(encoding="utf-8")
parts = re.split(r"(?m)^## Отрывок \d+, вариант (Т\d+)\s*$", txt)
return {lab: re.sub(r"(?m)\n-{3,}\s*$", "", body.strip()).strip()
for lab, body in zip(parts[1::2], parts[2::2])}
def verify_read(tag_: str, p: str) -> int:
"""Сплошная сверка пакетов ПЕРЕД чтением. ⚠ Пункт «тексты РАЗЛИЧНЫ» заведён 21.08 после того,
как читатели нашли то, чего не видела моя же «сплошная» сверка: `Z7` был побайтной копией `ZP`
на 10 главах из 15 (Д23.6). Прежняя сверка сличала пакет с клетками, а клетки друг с другом —
нет, и панель из 11 армов показывала 10 текстов."""
global READ_PANEL # noqa: PLW0603
keys = sorted((OUT / "blind-keys-rol").glob(f"rol-KEY-{tag_}-*.json"))
bad, lays, dupes, ntx = [], set(), [], 0
PAIRS[p]["wire"]()
by_uid = {u["uid"][:8]: u["uid"] for u in chosen(p)}
for kf in keys:
uid8 = kf.stem.split(f"{tag_}-")[1]
key = json.loads(kf.read_text(encoding="utf-8"))
if p not in key:
continue
lay, pack = key[p]["1"]["метки"], READ_DIR / f"ЧТЕНИЕ-{p}-{tag_}-{uid8}.md"
# ⚠ ГЛАВНАЯ проверка: текст под меткой — ТОТ ЖЕ, что в клетке своего арма, побайтно.
READ_PANEL = tuple(key[p]["1"]["знаков"])
cells = read_texts(p, by_uid[uid8])
for lab, arm in lay.items():
ntx += 1
if _variants(pack).get(lab, "") != cells[arm].strip():
bad.append(f"{uid8}: текст метки {lab} не равен клетке арма {arm}")
if cells["_src"].strip() not in pack.read_text(encoding="utf-8"):
bad.append(f"{uid8}: исходник в пакете не тот, что у главы")
var = _variants(pack)
txt = pack.read_text(encoding="utf-8")
lays.add(tuple(sorted(lay.items())))
if set(var) != set(lay):
bad.append(f"{uid8}: в пакете {len(var)} вариантов, в ключе {len(lay)}")
for arm in key[p]["1"]["знаков"]: # имя арма в пакете = утечка
if re.search(rf"\b{arm}\b", txt):
bad.append(f"{uid8}: имя арма {arm} стоит в пакете")
if "торопиться не надо" not in txt:
bad.append(f"{uid8}: нет указания владельца «торопиться не надо»")
# ⚠ ОБОРВАННАЯ КЛЕТКА В ПАНЕЛИ. `contour.base_a0` (арм `Z0`) читает `content` БЕЗ проверки
# `finish`, в отличие от `base_draft`, у которого гейт есть на обеих ветках. Через эту щель
# в китайскую панель прошла клетка с `finish=length` (21.08), и читатель законно поставил
# продакшен последним за оборванную кульминацию. Гейт стоит ЗДЕСЬ, потому что ловит класс
# независимо от источника: текст, который короче медианы панели, панель ломает.
ln = {a: len(t) for a, t in var.items()}
med = st.median(ln.values())
for lab, v in ln.items():
if med and v / med < 0.85:
bad.append(f"{uid8}: {lay[lab]} короче медианы панели ({v} против {med:.0f}, "
f"{v / med:.2f}) — похоже на оборванную клетку")
seen: dict = {}
for lab, body in var.items():
if body in seen:
dupes.append((uid8, lay[seen[body]], lay[lab]))
seen[body] = lab
print(f"\n=== СВЕРКА {tag_}, пара {p} ===")
print(f" текстов сверено побайтно с клетками: {ntx}")
print(f" пакетов {len(keys)} · РАЗНЫХ раскладок {len(lays)}"
f"{'' if len(lays) == len(keys) else ' ⛔ раскладки повторяются'}")
if dupes:
print(f" ⚠ ПОБАЙТНО СОВПАДАЮЩИЕ армы в {len(dupes)} пакетах:")
for uid8, a, b in dupes[:6]:
print(f" {uid8}: {a}{b}")
print(" (не обязательно дефект — но панель показывает читателю МЕНЬШЕ текстов, "
"чем армов, и вывод по такому арму законен только там, где он отличается)")
for x in bad:
print(f"{x}")
print(f" {'СБОРКА ГОДНА' if not bad else f'ПРОВАЛОВ {len(bad)}'}")
return 1 if bad else 0
def score_arch(tag_: str = "arch2", p: str = "en", drop: tuple = (), anchor: str = "") -> int:
"""`drop` — РЫЧАГ ЧУВСТВИТЕЛЬНОСТИ, не способ получить нужный ответ. Печатается в шапке свода,
и вывод, который живёт только при выброшенной главе, обязан называться вместе с ней."""
keys = sorted((OUT / "blind-keys-rol").glob(f"rol-KEY-{tag_}-*.json"))
keys = [k for k in keys if k.stem.split(f"{tag_}-")[1] not in drop]
if not keys:
raise SystemExit(f"⛔ ключей тега {tag_} нет")
ranks: dict = {}
twins, decoy, ident, bad = [], [], [], []
for kf in keys:
uid8 = kf.stem.split(f"{tag_}-")[1]
key = json.loads(kf.read_text(encoding="utf-8"))
if p not in key: # ключ другой пары под тем же тегом — не наш
continue
lay = key[p]["1"]["метки"]
af = READ_DIR / f"ОТВЕТ-{p}-{tag_}-{uid8}.md"
if not af.exists():
bad.append((uid8, "ответа нет"))
continue
txt = af.read_text(encoding="utf-8", errors="replace")
places = CH._ranks(_order_of(txt, set(lay))) # noqa: SLF001
if sorted(places) != sorted(lay):
bad.append((uid8, f"порядок покрывает {len(places)} из {len(lay)} меток"))
continue
r = {lay[lab]: pos for lab, pos in places.items()}
for arm, v in r.items():
ranks.setdefault(arm, []).append(v)
twins.append(abs(r["Z0"] - r["ZF"]))
if "ZD" in r: # грубый декой стоит не в каждой панели
decoy.append(r["ZD"])
# ⚠ КОНТРОЛЬ ТОЖДЕСТВА, найденный чтением: на главах без канон-щелей `Z7` — побайтная копия
# `ZP` (фиксер не звался). Читатель, разводящий два ОДИНАКОВЫХ текста, не имеет разрешения.
var = _variants(READ_DIR / f"ЧТЕНИЕ-{p}-{tag_}-{uid8}.md")
rev = {v: k for k, v in lay.items()}
if "Z7" in r and "ZP" in r and var.get(rev["Z7"]) == var.get(rev["ZP"]):
ident.append((uid8, abs(r["Z7"] - r["ZP"])))
n = len(twins)
print(f"\n=== СВОД {tag_}, пара {p}: глав отсужено {n} из {len(keys)} ==="
+ (f"\n ⚠ ВЫБРОШЕНЫ ГЛАВЫ: {', '.join(drop)} — свод УСЛОВНЫЙ" if drop else ""))
for uid8, why in bad:
print(f"{uid8}: {why}")
if not n:
return 1
print(f"\n{'арм':5s} {'ср. место':>10s} {'глав':>5s} места")
for arm, v in sorted(ranks.items(), key=lambda x: sum(x[1]) / len(x[1])):
print(f"{arm:5s} {sum(v) / len(v):10.2f} {len(v):5d} "
f"{' '.join(f'{x:g}' for x in v)}")
print(f"\nКОНТРОЛЬ ШУМА |Z0ZF| среднее {sum(twins) / n:.2f} из {len(ranks)} мест "
f"· помест-но {' '.join(f'{x:g}' for x in twins)}")
if decoy:
print(f"КОНТРОЛЬ ДЕКОЯ ZD среднее место {sum(decoy) / n:.2f} (обязан быть внизу; "
f"{sum(1 for x in decoy if x > len(ranks) / 2)} из {n} в нижней половине)")
else:
print("КОНТРОЛЬ ДЕКОЯ: грубого декоя в этой панели НЕТ"
"разрешение прибора держится только на близнецах")
# ⚠ КОНТРАСТЫ ПАРНЫЕ И МЕРИЛО У НИХ — БЛИЗНЕЦЫ, А НЕ НОЛЬ. Два прогона ОДНОГО способа расходятся
# по главам; значит «арм A выше арма B» что-то значит, только если разрыв больше того, что тот
# же прибор показывает между `Z0` и `ZF`. Точный знаковый тест берётся у `zsud`, своего не пишем.
ZS = _load("zsud_r", ZONE / "zsud.py")
tw = [a - b for a, b in zip(ranks["Z0"], ranks["ZF"])]
print(f"\nПАРНЫЕ КОНТРАСТЫ (n={n}). Собственный пол: близнецы Z0/ZF расходятся на "
f"{st.mean(tw):+.2f} места при пороге {2.8 * st.pstdev(tw) / n ** 0.5:.2f}"
f"{'НЕразличимы, как и обязаны' if abs(st.mean(tw)) <= 2.8 * st.pstdev(tw) / n ** 0.5 else '⛔ РАЗЛИЧИМЫ, пол негоден'}")
# ⚠ ЯКОРЬ КОНТРАСТА — ПАРАМЕТР. Для панели архитектур это боевая связка; для панели
# переносимости пре-рег Д25 назначил якорем `RN` — тот же промт на `deepseek-v4-pro`,
# иначе замер сравнивал бы вендоров с ДРУГОЙ архитектурой, а не промт с самим собой.
skip = (anchor,) if anchor in ranks else ("Z0", "ZF")
base = ranks[anchor] if anchor in ranks else [
(a + b) / 2 for a, b in zip(ranks["Z0"], ranks["ZF"])]
print(f"якорь контраста: {anchor if anchor in ranks else 'связка Z0/ZF'}")
for arm in sorted(ranks, key=lambda x: sum(ranks[x]) / n):
if arm in skip:
continue
d = [x - y for x, y in zip(ranks[arm], base)] # >0 — арм ХУЖЕ якоря
mde = 2.8 * st.pstdev(d) / n ** 0.5 # форма порога из пре-рега Д23.3
print(f" якорь ↔ {arm:4s} разрыв {st.mean(d):+5.2f} места · порог {mde:4.2f} · "
f"{'РАЗЛИЧИМ ' if abs(st.mean(d)) > mde else 'в пределах'} · "
f"знаковый p={ZS._sign_p(d):.4f}") # noqa: SLF001
if ident:
worst = max(x for _u, x in ident)
print(f"КОНТРОЛЬ ТОЖДЕСТВА Z7≡ZP на {len(ident)} главах · развод мест: "
f"среднее {sum(x for _u, x in ident) / len(ident):.2f}, худший {worst:g}"
f"{' ⛔ читатель развёл ОДИНАКОВЫЕ тексты' if worst else ' ✔ связаны знаком ='}")
return 0
if __name__ == "__main__":
a = sys.argv[1:] or ["--dry"]
if a[0] == "--selftest":
sys.exit(1 if cmd_selftest() else 0)
elif a[0] == "--dry":
cmd_dry()
elif a[0] == "--show":
cmd_show(next((x for x in a[1:] if x in PAIRS), "zh"))
elif a[0] == "--canon":
canon()
elif a[0] == "--emit-read":
_pan = next((x.split("=", 1)[1].split(",") for x in a if x.startswith("--panel=")), [])
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "")
_n = next((int(x.split("=", 1)[1]) for x in a if x.startswith("--n=")), 3)
_each = "--each" in a
for x in ([y for y in a[1:] if y in PAIRS] or list(PAIRS)):
if not _each:
emit_read(x, _n, tuple(_pan), _tag)
continue
# ⚠ ОДНА ГЛАВА — ОДИН ПАКЕТ, решение владельца 20.08: главы большие, читателю с
# несколькими сразу тяжело, и он начинает экономить внимание на последних.
PAIRS[x]["wire"]()
skipped = []
for k, u in enumerate(chosen(x)[:_n], 1):
try:
emit_read(x, 1, tuple(_pan), f"{_tag}-{u['uid'][:8]}" if _tag else u["uid"][:8],
only_uid=u["uid"])
except SystemExit as e:
# ⚠ ГЛАВА С НЕПОЛНОЙ ПАНЕЛЬЮ ПРОПУСКАЕТСЯ, А НЕ РОНЯЕТ ВЕСЬ ЦИКЛ. Первая
# редакция падала на первой же такой главе, и из 16 пакетов собралось 5 —
# причём молча выглядело как «панель полна только на пяти». Пропуск
# ПЕЧАТАЕТСЯ числом: тихо потерянная глава хуже, чем громко пропущенная.
skipped.append((u["uid"][:10], str(e)[:60]))
if skipped:
print(f" ⚠ ПРОПУЩЕНО глав с неполной панелью: {len(skipped)}")
for uid, why in skipped:
print(f" {uid}: {why}")
elif a[0] == "--score-read":
score_read()
elif a[0] == "--verify-read":
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "arch2")
sys.exit(verify_read(_tag, next((x for x in a[1:] if x in PAIRS), "en")))
elif a[0] == "--score-arch":
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "arch2")
_drop = tuple(next((x.split("=", 1)[1].split(",") for x in a if x.startswith("--drop=")), []))
_anch = next((x.split("=", 1)[1] for x in a if x.startswith("--anchor=")), "")
sys.exit(score_arch(_tag, next((x for x in a[1:] if x in PAIRS), "en"), _drop, _anch))
elif a[0] == "--coverage":
for p in ([x for x in a[1:] if x in PAIRS] or list(PAIRS)):
print(f"\n=== ПОКРЫТИЕ БОЕВЫХ ПРОМТОВ, пара {p} ===")
for fname, line, verdict in coverage_report(p):
print(f" [{verdict:32s}] {fname}: {line}")
elif a[0] == "--buy":
pr = [x for x in a[1:] if x in PAIRS] or list(PAIRS)
ar = [x for x in a[1:] if x in ARMS] or list(ARMS)
for x in pr:
buy(x, ar)
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --show --dry --selftest --buy --canon")