From b1cd8920403ac35fcc3e3fb723fd1d4b00b2a744 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 11 Aug 2026 14:53:34 +0300 Subject: [PATCH] Freeze judge wrappers built from one shared core with a mechanical parity gate, plus reproducible session layout keeping floor halves in different sessions --- eval/dovodka/contam_d.py | 96 ++++++++++++++++++++++++ eval/dovodka/judge-prompts/claude.md | 49 ++++++++++++ eval/dovodka/judge-prompts/sol.md | 49 ++++++++++++ eval/dovodka/judge-prompts/ОБЩЕЕ-ЯДРО.md | 37 +++++++++ eval/dovodka/paritet.py | 64 ++++++++++++++++ eval/dovodka/sessii.py | 73 ++++++++++++++++++ 6 files changed, 368 insertions(+) create mode 100644 eval/dovodka/contam_d.py create mode 100644 eval/dovodka/judge-prompts/claude.md create mode 100644 eval/dovodka/judge-prompts/sol.md create mode 100644 eval/dovodka/judge-prompts/ОБЩЕЕ-ЯДРО.md create mode 100644 eval/dovodka/paritet.py create mode 100644 eval/dovodka/sessii.py diff --git a/eval/dovodka/contam_d.py b/eval/dovodka/contam_d.py new file mode 100644 index 00000000..07cd11f8 --- /dev/null +++ b/eval/dovodka/contam_d.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""КОНТАМИНАЦИОННАЯ ПРОБА ФАЗЫ Д — ЖИЛЬЦАМИ и на книгах, которые фаза реально покупает. Платный. + +Заказ (Д3 и Д6) требует пробу «ЖИЛЬЦАМИ» — правило эксп-22 §1.1: гонять теми, кто работает, +а не удобной моделью. Непригоден по пробе → СТОП и пинг, замену без слова владельца не искать. + +Риг НЕ пере-изобретается: берётся `eval/role_topology/contamination.py` и настраивается через +свои глобали — тем же приёмом, каким `money_d` настраивает кассу, а `sud` — судейский риг. Метод +пробы остаётся его: УЗНАВАНИЕ («что за произведение и кто автор») плюс КЛОУЗ ПО ИМЕНИ (из отрывка +вырезано имя собственное, модель называет пропущенное). Пороги — его же, объявленные до данных: +узнавание ≥3/5 = «знает о книге», клоуз ≥2/5 = «знает конкретику, то есть текст был в обучении». + +⚠ ЧТО ЗДЕСЬ ДОБАВЛЕНО И ЗАЧЕМ. + +1. **Книги фазы.** Прошлый прогон мерил `gu-zhenren`, `jinpingmei`, `fifty_shades`, `kristoff` и + СТАРУЮ японскую книгу. Новая японская (`enkan_no_hate`, замена по слову владельца после + срабатывания гардрейла 18+) не мерена ВООБЩЕ, а Д6 покупается именно на ней. +2. **Жильцы вместо удобной модели.** Прошлый прогон гнался `gpt-5.6-luna`. Это не жилец: черновики + делает `deepseek-v4-flash`, редактуру `deepseek-v4-pro`. Знание книги ИМИ и есть то, что + завышает качество перевода, а знание посторонней модели не значит ничего. +3. **Бюджет вывода поднят.** Редакция 1 прошлой пробы звала flash с 16000 токенов и получала + 0–46 знаков: у DeepSeek размышление тарифицируется и считается ВНУТРИ бюджета, поэтому на + короткий ответ ничего не оставалось (стена quirks §10; ровно эта же стена сегодня съела 14 + клеток арма A1B). Ответ здесь нужен КОРОТКИЙ, а думать модель будет много — значит бюджет + должен вмещать размышление ЦЕЛИКОМ, иначе «НЕ ЗНАЮ» будет означать «не успел сказать». + ⚠ Если и при поднятом бюджете жилец возвращает пустоту — это НЕ «книга чиста», а непригодность + пробы на этом жильце, и она объявляется отдельно, а не растворяется в нуле. +4. **Положительный контроль обязателен на КАЖДОМ жильце.** Нулевой результат неинтерпретируем: + «модель не знает книгу» и «проба не работает на этой модели» дают одну картину. Контроль — + `jinpingmei` (минский канон, public domain, заведомо в претрейне). Не сработал контроль — + аннулируется весь прогон этого жильца, а не отдельная книга. + +Запуск: contam_d.py --dry # план, $0 + contam_d.py # платный +""" +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +ZONE = Path(__file__).resolve().parent +for d in ("dovodka", "role_topology", "tenant_panel"): + 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 + + +MONEY = _load("money_d", ZONE / "money_d.py") +CT = _load("contam21", REPO / "eval" / "role_topology" / "contamination.py") + +# Жильцы: те, кто реально работает на парах. Проба на каждом отдельно. +RESIDENTS = ("deepseek-v4-flash", "deepseek-v4-pro") + +# Книги, которые фаза Д покупает, плюс обязательный положительный контроль. +BOOKS_D = { + "enkan-ja": dict(path=Path.home() / "books" / "enkan_no_hate_ja.txt", lang="ja", + titles=["円環の果てに", "enkan", "円環"], + note="ja-ось Д6; первая публикация 2026-07-30, 18 глав — НЕ мерена ни разу"), + "kristoff-en": dict(path=Path.home() / "books" / + "Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub", + lang="en", titles=["empire of the dawn", "empire of the vampire", + "кристофф", "kristoff"], + note="en-ось Д3; luna узнала СЕРИЮ 1 раз из 5 — жильцами не мерена"), + "jinpingmei": dict(path=Path.home() / "books" / "jinpingmei" / "jinpingmei-ctext-zhs.txt", + lang="zh", titles=["金瓶梅", "золотой лотос", "jin ping mei"], + note="ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ — обязателен на каждом жильце"), +} + + +def main() -> int: + dry = "--dry" in sys.argv + print("КОНТАМИНАЦИОННАЯ ПРОБА ФАЗЫ Д — жильцами\n") + print(f"{'книга':14s}{'язык':>6s} что это") + for b, m in BOOKS_D.items(): + ok = "" if m["path"].exists() else " ⛔ ФАЙЛА НЕТ" + print(f"{b:14s}{m['lang']:>6s} {m['note']}{ok}") + print(f"\nжильцы: {' · '.join(RESIDENTS)}") + print("пороги (объявлены ДО данных, взяты из рига эксп-21): " + "узнавание ≥3/5 = знает о книге · клоуз ≥2/5 = знает конкретику") + if dry: + print("\n--dry: покупок нет") + return 0 + print("\n⚠ платный режим ещё не подключён к кассе фазы — см. TODO ниже") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/eval/dovodka/judge-prompts/claude.md b/eval/dovodka/judge-prompts/claude.md new file mode 100644 index 00000000..5805f347 --- /dev/null +++ b/eval/dovodka/judge-prompts/claude.md @@ -0,0 +1,49 @@ + + +Ты — литературный редактор-эксперт. Твоя работа — оценить качество русских художественных +переводов по заданиям, которые лежат готовыми файлами. + +Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом +задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми +метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан +ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой. + +Работай по заданиям последовательно: прочитал задание → записал ответ → перешёл к следующему. + +## Правила, которые важнее скорости + +* **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не + ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток ничего не значит + и перемешан намеренно. +* **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в + формате, который задан в задании. Оценка без цитаты не принимается, и единица целиком + выбрасывается из замера. +* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это + утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись, + что читал внимательно. +* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия, + подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают + незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их + и надо ловить. +* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия. + +## Границы + +* Работай ТОЛЬКО с назначенными файлами заданий и пиши ТОЛЬКО по путям, указанным внутри них. +* **Не ищи и не открывай ничего другого на диске.** В частности: не пытайся выяснить, откуда + взялся тот или иной вариант, какой моделью он сделан и что означают метки. Эта информация к + твоей задаче отношения не имеет, а её поиск делает оценку недействительной. +* Ничего не покупай, никаких сетевых вызовов. Файлы с ключами и секретами не читать. + +Когда все ответы записаны, верни коротко: сколько заданий выполнено и по какому пути легли +ответы. Сами оценки в ответ не переписывай, они уже в файлах. + +## Назначенные задания + +{СПИСОК ФАЙЛОВ} + +Каталог заданий: `~/sud-d4/p1-tasks/` и `~/sud-d4/p2-tasks/`. +Отвечать по путям, указанным внутри заданий (`~/sud-d4/p1-answers/`, `~/sud-d4/p2-answers/`). diff --git a/eval/dovodka/judge-prompts/sol.md b/eval/dovodka/judge-prompts/sol.md new file mode 100644 index 00000000..aa60abe4 --- /dev/null +++ b/eval/dovodka/judge-prompts/sol.md @@ -0,0 +1,49 @@ + + +Ты — литературный редактор-эксперт. Твоя работа — оценить качество русских художественных +переводов по заданиям, которые лежат готовыми файлами. + +Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом +задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми +метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан +ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой. + +Работай по заданиям последовательно: прочитал задание → записал ответ → перешёл к следующему. + +## Правила, которые важнее скорости + +* **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не + ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток ничего не значит + и перемешан намеренно. +* **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в + формате, который задан в задании. Оценка без цитаты не принимается, и единица целиком + выбрасывается из замера. +* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это + утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись, + что читал внимательно. +* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия, + подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают + незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их + и надо ловить. +* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия. + +## Границы + +* Работай ТОЛЬКО с назначенными файлами заданий и пиши ТОЛЬКО по путям, указанным внутри них. +* **Не ищи и не открывай ничего другого на диске.** В частности: не пытайся выяснить, откуда + взялся тот или иной вариант, какой моделью он сделан и что означают метки. Эта информация к + твоей задаче отношения не имеет, а её поиск делает оценку недействительной. +* Ничего не покупай, никаких сетевых вызовов. Файлы с ключами и секретами не читать. + +Когда все ответы записаны, верни коротко: сколько заданий выполнено и по какому пути легли +ответы. Сами оценки в ответ не переписывай, они уже в файлах. + +## Назначенные задания + +{СПИСОК ФАЙЛОВ} + +Каталог заданий: `~/sol-d4-work/p1-tasks/` и `~/sol-d4-work/p2-tasks/`. +Отвечать по путям, указанным внутри заданий (`~/sol-d4-work/p1-answers/`, `~/sol-d4-work/p2-answers/`). diff --git a/eval/dovodka/judge-prompts/ОБЩЕЕ-ЯДРО.md b/eval/dovodka/judge-prompts/ОБЩЕЕ-ЯДРО.md new file mode 100644 index 00000000..776755e6 --- /dev/null +++ b/eval/dovodka/judge-prompts/ОБЩЕЕ-ЯДРО.md @@ -0,0 +1,37 @@ +Ты — литературный редактор-эксперт. Твоя работа — оценить качество русских художественных +переводов по заданиям, которые лежат готовыми файлами. + +Прочитай каждое назначенное задание ЦЕЛИКОМ и выполни ровно то, что в нём написано. В каждом +задании есть исходник на китайском, несколько русских переводов этого же фрагмента под слепыми +метками и точная форма ответа. Ответ на каждое задание запиши ФАЙЛОМ по пути, который указан +ВНУТРИ самого задания, — путь там прописан явно, не придумывай свой. + +Работай по заданиям последовательно: прочитал задание → записал ответ → перешёл к следующему. + +## Правила, которые важнее скорости + +* **Оценивай каждый вариант ОТДЕЛЬНО против исходника.** Не сравнивай варианты между собой, не + ранжируй их и не подстраивай оценку одного под оценку другого. Порядок меток ничего не значит + и перемешан намеренно. +* **Каждая ненулевая оценка требует обоснования ЦИТАТОЙ** из соответствующего варианта, в + формате, который задан в задании. Оценка без цитаты не принимается, и единица целиком + выбрасывается из замера. +* **Ноль ставится только тогда, когда ошибок этого типа действительно нет.** Ноль — это + утверждение, а не способ пропустить вариант. Если ты не нашёл ошибок, значит их нет; убедись, + что читал внимательно. +* Читай текст ВНИМАТЕЛЬНО и сверяй смысл с китайским исходником. Смысловые искажения — инверсия, + подмена адресата реплики, потерянное отрицание, изменённое число, выдуманный факт — бывают + незаметны при беглом чтении, потому что русский текст остаётся грамотным и связным. Именно их + и надо ловить. +* Не жалей вариант и не ищи, за что его похвалить: твоя задача — СЧЁТ ОШИБОК, а не рецензия. + +## Границы + +* Работай ТОЛЬКО с назначенными файлами заданий и пиши ТОЛЬКО по путям, указанным внутри них. +* **Не ищи и не открывай ничего другого на диске.** В частности: не пытайся выяснить, откуда + взялся тот или иной вариант, какой моделью он сделан и что означают метки. Эта информация к + твоей задаче отношения не имеет, а её поиск делает оценку недействительной. +* Ничего не покупай, никаких сетевых вызовов. Файлы с ключами и секретами не читать. + +Когда все ответы записаны, верни коротко: сколько заданий выполнено и по какому пути легли +ответы. Сами оценки в ответ не переписывай, они уже в файлах. diff --git a/eval/dovodka/paritet.py b/eval/dovodka/paritet.py new file mode 100644 index 00000000..4789ac9d --- /dev/null +++ b/eval/dovodka/paritet.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""ГЕЙТ ПАРИТЕТА СУДЕЙСКИХ ОБВЯЗОК. $0. + +Норма Д0.13 П-4. Единственный неустранимый конфаунд прохода `tier` пака 23: промт харнесса Sol +нёс правила плотности счёта («две одинаковые ошибки — это два», «4 задания за сессию»), которых +у обвязки Claude не было. Значит часть расхождения семейств в РАЗЫ могла быть куплена +ИНСТРУКЦИЕЙ, а не моделью, и разделить это постфактум нечем. + +Отсюда норма: обвязки обоих семейств обязаны совпадать ВЕЗДЕ, кроме путей, и совпадение +проверяется механически ДО прогона, а не обещанием. Приём тот же, что у `promptdiff.py` для +пар-промтов: нормализуем объявленное различие и требуем побайтного совпадения остального. + +Запуск: eval/.venv/bin/python eval/dovodka/paritet.py +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +P = Path(__file__).resolve().parent / "judge-prompts" +# ЕДИНСТВЕННОЕ объявленное различие — каталоги. Всё прочее обязано совпасть. +PATHS = ("sud-d4", "sol-d4-work") + + +def body(f: Path) -> str: + t = re.sub(r"", "", f.read_text(encoding="utf-8"), flags=re.S) + for p in PATHS: + t = t.replace(p, "ПУТЬ") + return "\n".join(x.rstrip() for x in t.strip().splitlines()) + + +def main() -> 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 "")) + + core, cl, sol = P / "ОБЩЕЕ-ЯДРО.md", P / "claude.md", P / "sol.md" + for f in (core, cl, sol): + ck(f"файл обвязки есть: {f.name}", f.exists()) + if bad: + return bad + a, b = body(cl), body(sol) + ck("обвязки совпадают ВЕЗДЕ, кроме путей", a == b, + f"первое расхождение: {next((i for i, (x, y) in enumerate(zip(a.splitlines(), b.splitlines())) if x != y), '—')}") + c = core.read_text(encoding="utf-8").strip() + norm = lambda t: re.sub(r"\s+", " ", t) # noqa: E731 перенос строки не различие + for f in (cl, sol): + ck(f"{f.name} несёт общее ядро дословно", + norm(c) in norm(f.read_text(encoding="utf-8"))) + # ядро обязано нести правила, купленные дорого прошлыми паками + must = ["ОТДЕЛЬНО против исходника", "требует обоснования ЦИТАТОЙ", + "Ноль — это утверждение", "потерянное отрицание", "Не ищи и не открывай ничего другого"] + for m in must: + ck(f"ядро несёт правило: «{m[:44]}»", norm(m) in norm(c)) + print(f"\n{'ПАРИТЕТ ОБВЯЗОК СОБЛЮДЁН' if not bad else f'ПРОВАЛОВ: {bad}'}") + return bad + + +if __name__ == "__main__": + sys.exit(1 if main() else 0) diff --git a/eval/dovodka/sessii.py b/eval/dovodka/sessii.py new file mode 100644 index 00000000..8b06beb2 --- /dev/null +++ b/eval/dovodka/sessii.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""РАСКЛАДКА СУДЕЙСКИХ ПАЧЕК ПО СЕССИЯМ и печать готовых промтов. $0. + +Зачем скриптом, а не руками. Состав сессии — часть прибора, а не оформление: от него зависит, +(1) не попадут ли обе половины одной пары шумового пола к ОДНОМУ судье — тогда пол не поймает +межсессионную компоненту, и порог будет занижен, как в паке 23 на 19%; (2) одинаково ли +нагружены семейства — если Sol судит по 10 пачек за сессию, а Claude по 4, разница усталости +станет разницей семейств; (3) воспроизводима ли раскладка при пере-прогоне. + +Обвязка берётся из `judge-prompts/{claude,sol}.md`, которые собраны из ОДНОГО ядра и сверяются +гейтом `paritet.py` (норма Д0.13 П-4). Здесь подставляется только список файлов. + +⚠ Наборы `p1` и `p2` НИКОГДА не попадают в одну сессию: это разные половины пары пола. + +Запуск: sessii.py план раскладки + sessii.py --write записать готовые промты файлами +""" +from __future__ import annotations + +import sys +from pathlib import Path + +ZONE = Path(__file__).resolve().parent +PROMPTS = ZONE / "judge-prompts" +FAM = { + "claude": dict(root=Path.home() / "sud-d4", tpl=PROMPTS / "claude.md"), + "sol": dict(root=Path.home() / "sol-d4-work", tpl=PROMPTS / "sol.md"), +} +PER_SESSION = 4 # как у харнесса Sol в паке 23; одинаково для обоих семейств + + +def batches(root: Path, half: str) -> list[list[str]]: + """Пачки набора, разложенные по сессиям. Порядок — от имени файла, значит воспроизводим.""" + toks = sorted(p.stem for p in (root / f"{half}-tasks").glob("*.txt")) + return [toks[i:i + PER_SESSION] for i in range(0, len(toks), PER_SESSION)] + + +def render(fam: str, half: str, toks: list[str]) -> str: + m = FAM[fam] + body = m["tpl"].read_text(encoding="utf-8") + lst = "\n".join(f" ~/{m['root'].name}/{half}-tasks/{t}.txt" for t in toks) + return body.replace("{СПИСОК ФАЙЛОВ}", lst) + + +def main() -> int: + write = "--write" in sys.argv + total = 0 + for fam, m in FAM.items(): + root = m["root"] + if not root.exists(): + print(f"{fam}: каталога {root} нет — пропуск") + continue + out = root / "промты" + if write: + out.mkdir(exist_ok=True) + print(f"\n=== {fam.upper()} ({root}) ===") + n = 0 + for half in ("p1", "p2"): + for i, toks in enumerate(batches(root, half), 1): + n += 1 + name = f"{half}-сессия-{i:02d}.md" + print(f" {name:22s} пачек {len(toks)}: {' '.join(toks)}") + if write: + (out / name).write_text(render(fam, half, toks), encoding="utf-8") + total += n + print(f" сессий у семейства: {n}" + (f" · записано в {out}" if write else "")) + print(f"\nвсего сессий: {total} · по {PER_SESSION} пачки · кап агент-сессий 80") + print("⚠ наборы p1 и p2 — разные половины пары пола, одному судье их отдавать НЕЛЬЗЯ") + return 0 + + +if __name__ == "__main__": + sys.exit(main())