#!/usr/bin/env python3 """ТРЕТИЙ КОНТУР — слепое ЧТЕНИЕ выходов. $0 по ключам провайдеров. Зачем нужен. Первая редакция пака мерила качество ровно одним инструментом — судьями по ключам (`gpt-5.6-luna`, `grok-4.3`), и когда у этого инструмента нашлась позиционная фора +4.65 ошибки, проверить его оказалось нечем: батарея говорит только о механических дефектах, а сам я выходы систематически не читал. Один инструмент без второго мнения — это не замер, а доверие. Чем этот контур ОТЛИЧАЕТСЯ от судей, а не дублирует их: * другое семейство моделей (читатели — не из семей, населяющих армы, и не из семей судей), поэтому общий слепой участок семейства не переносится; * другая задача: не «посчитай ошибки по осям», а «прочитай как редактор издательства и назови конкретные места цитатой». Счёт ошибок и чтение ловят разное; * ранжирование ВСЕХ армов сразу, а не парно — позиционная фора парного протокола сюда структурно не переносится, зато появляется своя (порядок в списке), и она гасится перестановкой на каждой единице. Слепота. Метки армов снимаются, варианты перемешиваются детерминированным ключом от uid, карта раскладки лежит ОТДЕЛЬНЫМ файлом и читателю не выдаётся. Расшифровка — только при сведении. Запуск: --emit разложить пакеты чтения (и карту) по файлам --map показать карту раскладки (после чтения) """ from __future__ import annotations import hashlib import json import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "role_topology")) OUT = Path.home() / "books" / "role-topology" PACKETS = OUT / "blind" # ⚠ Карта раскладки лежит ВНЕ каталога пакетов (починка 07.08 по ревью). Прежде `zh-MAP.json` # лежал рядом с 28 пакетами: читателю запрещали её открывать инструкцией, а не расположением. # Запрет, который держится только на послушании читателя, слепотой не является. KEYS = OUT / "blind-keys" INSTRUCTION = """Ты — редактор русского издательства, готовящий книгу к печати. Ниже ИСХОДНИК и несколько независимых переводов одного и того же фрагмента. Кто их сделал — не сообщается намеренно. Прочитай каждый вариант ЦЕЛИКОМ как читатель, а не как чекер. Затем по КАЖДОМУ варианту: 1. Назови конкретные места, которые не пойдут в печать — ЦИТАТОЙ из варианта (не пересказом). Классы, которые важны: искажение смысла исходника; выдуманное или несуществующее русское слово; translationese (калька синтаксиса, канцелярит, неестественный порядок слов); потеря или подмена фактуры (числа, имена, полярность утверждения); сбой вёрстки и диалога. 2. Отметь, чем вариант ЛУЧШЕ остальных, если есть чем. Затем расставь варианты в порядке пригодности к печати, от лучшего к худшему, и назови ОДИН решающий довод за первое место — тоже цитатой. Отвечай строго в таком виде: ВАРИАНТ <буква> ДЕФЕКТЫ: <класс> — «цитата» | <класс> — «цитата» | ... (или «нет») СИЛЬНОЕ: <одной строкой или «нет»> (повторить для каждого варианта) РАНГ: <буквы через > , например В > А > Б> ДОВОД: <одна фраза с цитатой> """ def shuffle_key(uid: str, arms: list[str]) -> list[str]: """Детерминированная перестановка от uid: воспроизводима и не зависит от порядка армов.""" h = hashlib.sha1(uid.encode()).digest() # noqa: S324 return [a for _, a in sorted(zip(h[:len(arms)], sorted(arms), strict=True))] def emit(pack: str) -> None: if pack == "zh": import bakeoff as M # noqa: PLC0415 us, arms, prefix = M.units(), ["F", *M.ARMS], "zh" else: import pair_en as M # noqa: PLC0415 us, arms, prefix = M.units(), ["F", *M.ARMS], "en" PACKETS.mkdir(parents=True, exist_ok=True) letters = "АБВГДЕЖЗ" written, mapping = 0, {} for u in us: texts = {a: M.text_of(a, u) for a in arms} have = [a for a in shuffle_key(u["uid"], arms) if texts[a].strip()] if len(have) < 2: continue body = [INSTRUCTION, "\n=== ИСХОДНИК ===\n", u["source"]] for i, a in enumerate(have): body.append(f"\n\n=== ВАРИАНТ {letters[i]} ===\n{texts[a]}") mapping.setdefault(u["uid"], {})[letters[i]] = a (PACKETS / f"{prefix}-{u['uid']}.txt").write_text("".join(body), encoding="utf-8") written += 1 KEYS.mkdir(parents=True, exist_ok=True) (KEYS / f"{prefix}-MAP.json").write_text(json.dumps(mapping, ensure_ascii=False, indent=1), encoding="utf-8") print(f"пакетов {written} → {PACKETS}/{prefix}-*.txt · карта в {KEYS} (вне каталога пакетов)") def score(pack: str) -> None: """Свод чтения по персистированным рангам. $0. ⚠ Заведён 07.08 по адверсариальному ревью второй редакции. Прежде модуль умел только ВЫПУСКАТЬ пакеты, а ответы читателей нигде не оседали — ранги переносились в свод руками. Это нарушение закона промта стр.32 («per-vote персист КАЖДОГО голоса»), рождённого ревью-шапкой эксп-20: замер без персиста невоспроизводим. Ранги прошлого прогона восстановлены в `READINGS.json`, полные разборы утрачены и требуют ре-рана читателей. """ import statistics # noqa: PLC0415 from collections import defaultdict # noqa: PLC0415 f = KEYS / "READINGS.json" if not f.exists(): print("нет READINGS.json — чтение не персистировано") return d = json.loads(f.read_text(encoding="utf-8")) m = json.loads((KEYS / f"{pack}-MAP.json").read_text(encoding="utf-8")) ranks = d[f"ranks_{pack}"] pos: dict[str, list[int]] = defaultdict(list) wins: dict[str, int] = defaultdict(int) for uid, order in ranks.items(): letters = order.split() for i, letter in enumerate(letters): pos[m[uid][letter]].append(i + 1) wins[m[uid][letters[0]]] += 1 print(f"СЛЕПОЕ ЧТЕНИЕ {pack} · единиц {len(ranks)}\n") print(f"{'арм':7s}{'ср.ранг':>9s}{'первых':>8s}{'медиана':>9s}") for arm in sorted(pos, key=lambda a: statistics.mean(pos[a])): print(f"{arm:7s}{statistics.mean(pos[arm]):9.2f}{wins[arm]:8d}" f"{statistics.median(pos[arm]):9.1f}") print(f"\n⚠ {d['provenance']}") def show_map(pack: str) -> None: f = KEYS / f"{pack}-MAP.json" print(f.read_text(encoding="utf-8") if f.exists() else "карты нет") if __name__ == "__main__": a = sys.argv[1:] or ["--emit", "zh"] if a[0] == "--emit": emit(a[1] if len(a) > 1 else "zh") elif a[0] == "--score": score(a[1] if len(a) > 1 else "zh") elif a[0] == "--map": show_map(a[1] if len(a) > 1 else "zh") else: print(__doc__)