#!/usr/bin/env python3 """АБСОЛЮТНЫЙ СУДЕЙСКИЙ РИГ. $0 по ключам провайдеров. ⚠ Заведён 07.08 по находке ЧЕТЫРЁХ независимых судей-агентов. Каждому была выдана своя пачка ПАРНЫХ пакетов «исходник + вариант 1 + вариант 2»; ни один не знал о трёх других. Все четыре самостоятельно свернули пары обратно: дедуплицировали уникальные тексты, оценили КАЖДЫЙ ОДИН РАЗ против исходника и разнесли числа по пакетам. Четыре из четырёх — это не сбой исполнителя, это поведение инструмента, и оснастка обязана следовать за ним, а не бороться с ним. ЧТО ДАЁТ ПЕРЕХОД НА АБСОЛЮТНУЮ ШКАЛУ. 1. **Позиция исчезает по построению.** Текст оценивается вне пары, значит «первый/второй» не существует как понятие. Зеркальная раскладка, которую я чинил трижды и трижды получал вырождение, здесь не нужна — нечего зеркалить. 2. **Работа падает втрое.** 160 парных пакетов = 320 прочтений; уникальных текстов 128. Причём парная схема покупала 4 контраста, а абсолютная даёт ВСЕ 7 объявленных, включая деконфаунды. 3. **Строгость калибрована внутри единицы.** Все варианты ОДНОГО исходника судятся в одном заходе, а контраст считается ВНУТРИ единицы и лишь затем усредняется по единицам. Дрейф строгости между единицами в контраст не попадает — он сокращается вычитанием. ЧЕГО ЭТО НЕ ЧИНИТ. Шум ОДНОГО вызова велик: два прогона одного и того же арма на одном входе расходятся на sd≈2.7 ошибки по единице (контроль пола, n=16). Значит на ОТДЕЛЬНОЙ главе армы надёжно не упорядочены — пере-прогон сдвинет счёт сильнее, чем разница между армами. Упорядочено только среднее: парный дизайн делит шум на √n, и при 16 единицах различимый эффект ≈1.9. ⚠ Первая версия этой шапки утверждала, что эффект ниже двух не различим «никаким числом единиц». Это неверно и снято собственным замером: порог кладёт стандартная ошибка среднего, а она падает с ростом n. Формулировка была завышением порога и держалась ровно до того, как пол измерили. СЛЕПОТА. Метки Т1..Тn внутри единицы, порядок перемешан хешем соли и uid: из метки не выводится ни арм, ни то, редактированный это текст или черновик. Ключ лежит отдельно и судье не даётся. ПЕРСИСТ. Судья пишет ответ файлом САМ, в `aj-abs/<токен>.txt`. Так убирается моя транскрипция — тот самый канал, по которому в третьей редакции потерялись обоснования, а слепое чтение не попало на диск вовсе. `ingest` только разбирает и проверяет; голос с ненулевой осью и пустым обоснованием отвергается (условие 2 амендмента владельца 07.08). Запуск: --selfcheck проверка разбора и арифметики на входах с известным ответом, $0 --emit выпустить задания судьям, $0 --ingest разобрать ответы в голоса, $0 --controls что показали контроли (читать ДО боевых чисел), $0 --score контрасты с поправкой Холма, $0 """ from __future__ import annotations import hashlib import json import re import statistics as st import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "role_topology")) import bakeoff as BO # noqa: E402 import judges as J # noqa: E402 OUT = Path.home() / "books" / "role-topology" KEYS = OUT / "blind-keys" def _dirs(): """Каталоги прохода. ВТОРОЙ проход обязан лежать отдельно: он и есть репликация, и затирание первого превратило бы её в пере-запись. ⚠ `DIRS_HOOK` заведён приёмкой №16 (чек-лист фазы Д, п.9). Репликацию пака 23 гоняли, КОПИРУЯ каталоги руками, потому что имя каталога выводилось только из `PASS`: разбор шёл под `PASS='border'` и штамповал голоса репликации именами судей БОЕВОГО прогона. Крючок даёт прогону собственные каталоги, не трогая имя прохода (а с ним — ключ, соль и метки). """ if DIRS_HOOK is not None: return DIRS_HOOK() return (OUT / f"aj-{PASS}-tasks", OUT / f"aj-{PASS}", OUT / f"aj-{PASS}-votes") # ⚠ СОЛЬ ЖИВЁТ ВНЕ РЕПОЗИТОРИЯ. Адверсариальное ревью 07.08 восстановило 138 меток из 138, не # открывая ключ: `layout()` — чистая функция от соли и `uid = sha1(source)`, а источник лежит В # ЗАДАНИИ. Пока соль была константой в коде, слепота держалась только запретом судье читать код. # Теперь она в файле рядом с ключом, куда судье ходить запрещено тем же правилом, что и к ключу. def _salt(name: str = "abs") -> str: # ⚠ Путь берётся из KEYS, а не константой: у другого пака свой каталог ключей (эксп-22). # Для эксп-21 значение то же самое, поэтому метки и токены не сдвигаются. f = KEYS / f"SALT-{name}.txt" if not f.exists(): import secrets # noqa: PLC0415 f.parent.mkdir(parents=True, exist_ok=True) f.write_text(secrets.token_hex(16), encoding="utf-8") return f.read_text(encoding="utf-8").strip() PASS = "abs" # переключается --emit2 на вторую пачку SALT = _salt(PASS) # Армы, попадающие в пачку. F3 — собственный черновик пака (текст и цена из одного вызова). # D/D_ берутся из второй редакции СОЗНАТЕЛЬНО: однопроходки черновика не видят, их вход # (исходник + блок закона) в обеих редакциях побайтно один, и это проверяется в selfcheck. ARMS = ("F3", "A", "A_law", "B", "D", "D_") # ⚠ Армы Ф2б (точечный ремонт C и обратная связка E) судятся ТЕМ ЖЕ ригом и теми же агентами. # Иначе их числа несравнимы с Ф2а, и покупка обесценивается — это первое, что назвала # верификация до траты. Тексты берутся из `phase2b`, всё остальное в риге не меняется. ARMS_2B = ("C2", "E2") # ⚠ ТОЧКИ ПОДКЛЮЧЕНИЯ ДРУГОЙ ПАНЕЛИ (заведены эксп-22, 08.08). Риг здесь — методика: абсолютная # шкала, слепые метки, декой, пол, персист обоснования с цитатой, семейство, впечатанное в ключ # ДО ответов, Холм. Методику пере-изобретать нельзя, а панель у следующего пака другая, поэтому # сюда вынесены ровно пять крючков. Значения по умолчанию = поведение эксп-21 без изменений; # это проверяется тем, что `itog.py` и `verify_report.py` после правки дают те же числа. TEXT_HOOK = None # callable(arm, unit) -> str UNITS_HOOK = None # callable() -> list[dict] FLOOR_HOOK = None # callable(unit) -> tuple[str, str] | None — пара повторов одного арма DECOY_HOOK = None # callable(slots: dict) -> str — чистый текст, в который сажается порча SRC_LANG = "китайском" # как называется язык исходника в задании судье DIRS_HOOK = None # callable() -> (tasks, answers, votes) — свои каталоги прогона JUDGES_HOOK = None # callable(token, answer_mtime) -> str — кто судил ЭТОТ ответ def configure(**kw) -> None: """Переключить риг на панель другого пака. Ключи — имена глобалей этого модуля.""" for k, v in kw.items(): if k not in globals(): raise SystemExit(f"⛔ absjudge.configure: неизвестный ключ {k!r}") globals()[k] = v def _text(arm, u): if TEXT_HOOK is not None: return TEXT_HOOK(arm, u) if arm in ARMS_2B: import phase2b as P2 # noqa: PLC0415 return P2.text_of(arm, u["uid"]) return BO.text3_of(arm, u) def _units(): return UNITS_HOOK() if UNITS_HOOK is not None else BO.units() # СЕМЕЙСТВО КОНТРАСТОВ, объявленное ДО первого ответа судьи. Поправка Холма считается по всем # семи; сузить семейство после того, как увидены p-значения, — это и есть p-hacking, и именно # за него снята первая редакция. Знак: перевес > 0 означает «ПЕРВЫЙ арм лучше». FAMILY = [ ("A", "F3", "редактор поверх черновика против самого черновика"), ("B", "F3", "другой движок-редактор поверх ТОГО ЖЕ черновика"), ("D_", "A", "однопроходка против связки черновик+редактор"), ("D", "A", "однопроходка с переливкой абзацев против связки"), ("A_law", "A", "деконфаунд: сколько покупает банкнота-как-закон"), ("A_law", "B", "два движка, оба с законом"), ("D", "D_", "деконфаунд: сколько покупает переливка абзацев"), ] FORMAT_HEAD = """Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между собой. Порядок вариантов ничего не значит и специально перемешан. Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку: {skeleton} ПОЧЕМУ обязательно для КАЖДОЙ ненулевой оси, и ось надо НАЗВАТЬ. Единственный принимаемый вид — имя оси заглавными, двоеточие, цитаты в «ёлочках» через « | »; перечисляй только ненулевые оси: Т1-ПОЧЕМУ: ВЕРНОСТЬ: «цитата» | «ещё цитата». ЯЗЫК: «цитата». ФОРМА: «цитата». Разбор ищет имя оси буквально. Цитаты без имени оси не принимаются, и единица выбрасывается ЦЕЛИКОМ — вся работа по ней пропадает; так уже терялась треть пачки в трёх прогонах подряд, потому что это требование стояло в приёмке, но не в задании. Ноль по оси обоснования не требует; нечем подпереть число цитатой — ставь ноль, а не число без цитаты. """ def utoken(uid: str) -> str: return hashlib.sha1(f"{_salt(PASS)}|unit|{uid}".encode()).hexdigest()[:10] # noqa: S324 def layout(uid: str, slots: list[str]) -> list[str]: """Порядок меток внутри единицы: детерминированная перестановка по хешу, не по алфавиту.""" return sorted(slots, key=lambda s: hashlib.sha1(f"{_salt(PASS)}|{uid}|{s}".encode()).hexdigest()) # noqa: S324 def build_unit(u: dict) -> tuple[str, dict]: """Задание на одну единицу + ключ раскладки. Контроли подмешаны и неотличимы от боевых.""" # ⚠ Проход 4 узкий: только армы объявленных контрастов Ф2б плюс база сравнения. Судить # заново все одиннадцать меток незачем — числа Ф2а уже сняты дважды и воспроизвелись. if TEXT_HOOK is not None: arms = tuple(ARMS) else: arms = ("F3", "A_law", "D_") + ARMS_2B if PASS == "abs4" else tuple(ARMS) + ARMS_2B slots: dict[str, str] = {a: _text(a, u) for a in arms} slots = {a: t for a, t in slots.items() if (t or "").strip()} kinds: dict[str, str] = {a: "боевой" for a in slots} clean = DECOY_HOOK(slots) if DECOY_HOOK is not None else (slots.get("A") or slots["A_law"]) decoy_from = next((a for a, t in slots.items() if t == clean), "") planted, n = _plant(clean) if n >= 2: slots["CTRLdecoy"], kinds["CTRLdecoy"] = planted, "контроль-декой" # ⚠ ПОЛ КЛАДЁТСЯ ПАРОЙ, а не против боевого арма `A`. Поймано чтением кода 07.08: `bo3-Aprime` # куплен по `build_msgs` (черновик ИЗ КОРПУСА), а боевой `A` третьей редакции — по `build_msgs3` # (СОБСТВЕННЫЙ черновик). Их входы различаются, и пол мерил бы разницу черновиков, а не шум # вызова. Повтором `Aprime` является `bo2-A`, и именно он идёт второй половиной пары. if FLOOR_HOOK is not None: pair = FLOOR_HOOK(u) if pair: slots["CTRLfloorA"], slots["CTRLfloorB"] = pair kinds["CTRLfloorA"] = kinds["CTRLfloorB"] = "контроль-пол" else: fa, fb = OUT / f"bo2-A-{u['uid']}.json", OUT / f"bo3-Aprime-{u['uid']}.json" if fa.exists() and fb.exists(): slots["CTRLfloorA"] = json.loads(fa.read_text(encoding="utf-8"))["content"] slots["CTRLfloorB"] = json.loads(fb.read_text(encoding="utf-8"))["content"] kinds["CTRLfloorA"] = kinds["CTRLfloorB"] = "контроль-пол" order = layout(u["uid"], list(slots)) label = {name: f"Т{i + 1}" for i, name in enumerate(order)} skel = "\n".join(f"{label[n]}-ВЕРНОСТЬ: <число>\n{label[n]}-ТЕРМИН: <число>\n" f"{label[n]}-ЯЗЫК: <число>\n{label[n]}-ФОРМА: <число>\n" f"{label[n]}-ПОЧЕМУ: <цитаты>\n" for n in order) tok = utoken(u["uid"]) # Шапка своя, а не `J.RUBRIC`: та написана под ПАРУ («два русских перевода», «два варианта») # и в абсолютной раскладке, где меток до восьми, противоречила бы заданию. Определения осей # берутся из неё дословно — меняется только обрамление. axes_def = J.RUBRIC.split("Посчитай ОШИБКИ", 1)[1].split("Отвечай РОВНО")[0].rstrip() ANSW = _dirs()[1] body = [f"Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на {SRC_LANG} и {len(order)} " f"независимых русских перевода этого же фрагмента под слепыми метками.", "", "Посчитай ОШИБКИ" + axes_def, "", FORMAT_HEAD.format(path=ANSW / f"{tok}.txt", skeleton=skel), "", "=" * 60, "ИСХОДНИК:", u["source"]] for n in order: body += ["", "=" * 60, f"{label[n]}:", slots[n]] # ⚠ `decoy_from` пишется В КЛЮЧ. Без него контроль декоя считался ПРОТИВ ЖЁСТКО ЗАШИТОГО арма # `A`, которого у другой панели нет, и обязательный контроль молча не считался вовсе # (поймано эксп-22 чтением контролей перед боевыми числами). key = {label[n]: dict(arm=n, kind=kinds[n], uid=u["uid"], sig=hashlib.sha1(slots[n].encode()).hexdigest()[:12], # noqa: S324 **({"decoy_from": decoy_from} if n == "CTRLdecoy" else {})) for n in order} return "\n".join(body), key # ⚠ ПРАВИЛА ПОСАДКИ ЖИВУТ ВНЕ РЕПОЗИТОРИЯ, рядом с ключом и солью. # Адверсариальное ревью 07.08: пока `_PLANTS` лежали в коде, декой опознавался БЕЗ соли и БЕЗ # ключа — он побайтная копия арма `A` с 1–6 подменами, сходство 0.99 против 0.90 у ближайшего # конкурента, и ориентация снималась прогоном самой посадки на обоих кандидатах (10/10 верно). # Значит читающий код судья опознавал и контроль, и арм `A` — базу четырёх контрастов из семи. # Вынос соли закрывал один канал из двух, и не главный. Здесь закрывается второй. # ⚠ ПОСАДКИ ПЕРЕПИСАНЫ ЭКСП-22 НА КНИГО-НЕЗАВИСИМЫЕ. Прежний список был написан под лексику # 蛊真人 («не выдержат», «десять», «сто», «поразительна», «спокойно») и на ДРУГОЙ книге давал # одну сработку из шести при пороге две — декой молча не создавался, и судейская сессия уходила # БЕЗ контроля. Поймано чтением контролей перед боевыми числами (проход `p2en` эксп-22: 0 декоев # на 6 единиц). Это ровно ревью-вопрос проекта «заработает ли на книге, которой в репо ещё нет». # Ниже — правила, опирающиеся на грамматику и типографику русской прозы, а не на её словарь. _PLANTS_DEFAULT = [ [r"\bне\s+([а-яё]{4,})", r"\1", "инверсия полярности: снято отрицание"], [r"\b(один|два|три|четыре|пять|шесть|семь|восемь|девять|десять|сто|тысяч[а-яё]*)\b", "сорок", "подмена величины"], [r"\b(\d+)\b", "77", "подмена величины (цифрой)"], [r"\b([а-яё]{4,})ние\b", r"\1нительние", "выдуманное слово"], [r"—\s*", '"', "сбой диалоговой вёрстки"], [r"\n\n", " ", "слипание абзацев"], ] def _plants() -> list: f = KEYS / "PLANTS.json" if not f.exists(): f.parent.mkdir(parents=True, exist_ok=True) f.write_text(json.dumps(_PLANTS_DEFAULT, ensure_ascii=False, indent=1), encoding="utf-8") return json.loads(f.read_text(encoding="utf-8")) def _plant(text: str) -> tuple[str, int]: """Контролируемая деградация. Число РЕАЛЬНО применённых замен возвращается: клетка, где посадить не удалось, в декой не берётся, иначе «не пойман» будет означать «не было».""" out, n = text, 0 for pat, rep, _c in _plants(): new, cnt = re.subn(pat, rep, out, count=1) if cnt: out, n = new, n + cnt return out, n def emit() -> None: TASKS, ANSW, VOTES = _dirs() TASKS.mkdir(parents=True, exist_ok=True) KEYS.mkdir(parents=True, exist_ok=True) ANSW.mkdir(parents=True, exist_ok=True) keys, n_tx = {}, 0 for u in _units(): body, key = build_unit(u) tok = utoken(u["uid"]) (TASKS / f"{tok}.txt").write_text(body, encoding="utf-8") keys[tok] = key n_tx += len(key) # ⚠ СЕМЕЙСТВО ВПЕЧАТЫВАЕТСЯ В КЛЮЧ, который создаётся ДО существования хоть одного ответа. # Так порядок «объявлено → отвечено» доказывается артефактом, а не словом исполнителя. # Заведено после того, как ревью показало: `absjudge.py` незакоммичен и датируется ПОЗЖЕ # последнего ответа судьи, то есть провенанс проверить было нечем. (KEYS / f"{PASS}-KEY.json").write_text(json.dumps( dict(_семейство=[f"{a} vs {b} — {w}" for a, b, w in FAMILY], _армы=list(ARMS), _проход=PASS, **keys), ensure_ascii=False, indent=1), encoding="utf-8") print(f"заданий {len(keys)} → {TASKS} · прочтений {n_tx} (парная схема требовала 320)") print(f"ключ → {KEYS}/{PASS}-KEY.json · судье НЕ давать") def ingest() -> None: _TASKS, ANSW, VOTES = _dirs() if not ANSW.exists(): print(f"нет каталога ответов {ANSW}") return keys = {k: v for k, v in json.loads((KEYS / f"{PASS}-KEY.json").read_text( encoding="utf-8")).items() if not k.startswith("_")} # ⚠ ИДЕНТИЧНОСТЬ СУДЬИ. Первая версия рига её не персистировала вовсе, и по диску нельзя было # ни посчитать согласие судей, ни разложить вердикты по семействам (требование промта стр.79). # Поймано адверсариальным ревью 07.08. Карта раздачи лежит рядом с ключом и судье не даётся. # ⚠ ПЛОСКАЯ КАРТА ПО ИМЕНИ ПРОХОДА НЕ РАЗЛИЧАЕТ ПРОГОНЫ (приёмка №16, чек-лист Д8 п.9). # Репликация пака 23 судила ТЕ ЖЕ 32 токена вторым жребием судей, а карта лежит одна на # проход — все 32 голоса репликации получили имена судей БОЕВОГО прогона. Перевесы от # этого не двигаются (имя судьи в разность не входит), но «согласие судей» и разложение # дисперсии по сессиям считались бы по ложной раскладке. Поэтому имя берётся крючком, # который знает про ПРОГОН и про время ответа; плоская карта осталась дефолтом эксп-21/22. jf = KEYS / f"{PASS}-JUDGES.json" judges = json.loads(jf.read_text(encoding="utf-8")) if jf.exists() else {} def judge_of(stem: str, path: Path) -> str: if JUDGES_HOOK is not None: return JUDGES_HOOK(stem, path.stat().st_mtime) return judges.get(stem, "?") VOTES.mkdir(parents=True, exist_ok=True) ok = bad = 0 reasons: dict[str, int] = {} def fail(r: str, stem: str = "") -> None: nonlocal bad bad += 1 reasons[r] = reasons.get(r, 0) + 1 # ⚠ Отказ ОБЯЗАН снимать прежний голос: иначе негодный ответ оставлял на диске # предыдущий разбор, и `_by_unit` подхватывал его как действительный. Ревью 07.08. if stem and (VOTES / f"{stem}.json").exists(): (VOTES / f"{stem}.json").unlink() for f in sorted(ANSW.glob("*.txt")): key = keys.get(f.stem) if not key: fail("токена нет в ключе", f.stem) continue txt = f.read_text(encoding="utf-8") rows = {} for lab, meta in key.items(): ax = {} for a in J.AXES: # ⚠ `[ \t]*`, а НЕ `\s*`: `\s` включает перевод строки, и пустое поле молча # утаскивало значение СЛЕДУЮЩЕЙ строки. Поймано известным входом в selfcheck — # голос без обоснования проходил с обоснованием, списанным с соседней строки. m = re.search(rf"^{lab}-{a}[ \t]*:[ \t]*(\d+)[ \t]*$", txt, re.M) if m: ax[a] = int(m.group(1)) # обоснование может занимать несколько строк — берём до следующей метки или конца wm = re.search(rf"^{lab}-ПОЧЕМУ[ \t]*:[ \t]*(.*?)(?=^\S+-(?:{'|'.join(J.AXES)}|ПОЧЕМУ)" rf"[ \t]*:|\Z)", txt, re.M | re.S) why = wm.group(1).strip() if wm else "" if len(ax) != len(J.AXES): fail(f"не разобран блок {lab}", f.stem) break # ⚠ ПОИМЁННО ПО ОСЯМ, а не «непустая строка на блок». Ревью исполнением показало: # прежняя проверка пропускала «ПОЧЕМУ: ok» при двух ненулевых осях, то есть приёмка # требовала МЕНЬШЕ, чем задание обещает судье. Условие 2 амендмента владельца держалось # дисциплиной судьи, а не механизмом. Теперь каждая ненулевая ось обязана быть названа. # ⚠ ОСЬ КАК ОТДЕЛЬНОЕ СЛОВО (иначе «ФОРМА» засчитывалась внутри «формально») И # ОБЯЗАТЕЛЬНАЯ ЦИТАТА (иначе проходило голое имя оси и эхо скелета рубрики). # Обе дыры найдены ревью исполнением 07.08. up = (why or "").upper() miss = [a for a in J.AXES if ax[a] and not re.search(rf"(? 1] if dup: fail(f"блок {lab} задан дважды: {'/'.join(dup)}", f.stem) break rows[lab] = dict(**meta, **ax, why=why, errors=sum(ax.values()), slot=int(lab[1:]), judge=judge_of(f.stem, f)) else: (VOTES / f"{f.stem}.json").write_text( json.dumps(rows, ensure_ascii=False), encoding="utf-8") ok += 1 print(f"принято единиц {ok} · отвергнуто {bad} {reasons if reasons else ''}") def _by_unit() -> dict[str, dict[str, dict]]: VOTES = _dirs()[2] out = {} # ⚠ sorted, а не голый glob: `boot_ci` ресемплит по ПОЗИЦИЯМ списка, поэтому от порядка обхода # каталога зависели границы доверительных интервалов — точечные оценки и p воспроизводились, а # интервалы нет. Поймано приёмкой пака 23. for f in sorted(VOTES.glob("*.json")): rows = json.loads(f.read_text(encoding="utf-8")) uid = next(iter(rows.values()))["uid"] out[uid] = {r["arm"]: r for r in rows.values()} return out def _margins(a: str, b: str) -> list[float]: """Перевес ЗА арм `a` внутри каждой единицы: ошибки(b) − ошибки(a).""" return [d[b]["errors"] - d[a]["errors"] for d in _by_unit().values() if a in d and b in d] def _family_proven() -> bool: f = KEYS / f"{PASS}-KEY.json" if not f.exists(): return False return json.loads(f.read_text(encoding="utf-8")).get("_семейство") is not None def _assert_family() -> None: """Сверить работающее семейство с тем, что впечатано в ключ ДО ответов. ⚠ Ревью 07.08: `emit()` штамп ставил, а `score()` с ним не сверялся — штамп был декоративен, и правка `FAMILY` после выпуска ничем не ловилась. Провенанс замыкается здесь. """ f = KEYS / f"{PASS}-KEY.json" if not f.exists(): return decl = json.loads(f.read_text(encoding="utf-8")).get("_семейство") if decl is None: print(f"⚠ в ключе {f.name} семейство НЕ впечатано — провенанс этого прохода не доказуем") return now = [f"{a} vs {b} — {w}" for a, b, w in FAMILY] if decl != now: raise SystemExit("⛔ СЕМЕЙСТВО РАЗОШЛОСЬ с впечатанным в ключ ДО ответов:\n" f" в ключе: {decl}\n в коде: {now}") if PASS == "abs4": # ⚠ ЧЕСТНОСТЬ ПРОВЕНАНСА. В ключ прохода 4 впечатано семейство Ф2а — оно на узкой панели # НЕПРИМЕНИМО (там нет арма `A`, и ни один его контраст не собирается). Сверка выше # проходит, но она ничего не доказывает для тех контрастов, которые здесь считаются. # Молчать об этом — значит выдать декоративный штамп за провенанс. print("⚠ ПРОВЕНАНС ПРОХОДА 4 НЕПОЛОН: в ключ впечатано семейство Ф2а, неприменимое к узкой\n" " панели. Считается пятёрка Ф2б (`FAMILY_2B`), объявленная в `phase2b.FAMILY` ДО\n" " покупок с одной заменой: недоступный судье `C_oracle/C_real` → контроль `A_law/F3`.\n") # СЕМЕЙСТВО УЗКОГО ПРОХОДА 4 (армы Ф2б). ⚠ Заведено 08.08 аудитом: `--score4` печатал ПУСТУЮ # таблицу («поправка Холма по 0 контрастам») — ни один контраст `FAMILY` не имеет обоих армов на # узкой панели (там нет `A`), и у таблицы п.7 отчёта не было пути воспроизведения в дереве. # ⚠ ОТКЛОНЕНИЕ ОТ ПРЕ-РЕГА, объявляю: `phase2b.FAMILY` пятым членом называет `C_oracle vs C_real` # — разрыв потолка и пола схемы. Он НЕ судейский (меряется посаженными дефектами и детекторами, # судье не предъявлялся, p не имеет). В судейскую пятёрку вместо него вошёл контроль `A_law/F3`. # Подмена КОНСЕРВАТИВНА — член семейства сохранён, поправка не ослаблена, — но это правка # семейства после пре-рега, и читать её надо как девиацию, а не как замысел. FAMILY_2B = [ ("C2", "A_law", "точечный ремонт против полного переписывания"), ("C2", "F3", "и лучше ли он черновика, который чинит"), ("E2", "D_", "обратная связка против своей базы"), ("E2", "A_law", "обратная связка против прямой"), ("A_law", "F3", "КОНТРОЛЬ: воспроизводится ли эффект редактора"), ] def score() -> None: _assert_family() data = _by_unit() if not data: print("голосов нет — сначала --ingest") return fam = FAMILY_2B if PASS == "abs4" else FAMILY rows = [] for a, b, what in fam: m = _margins(a, b) if len(m) < 3: continue rows.append((a, b, what, m, BO.sign_perm_p(m))) order = sorted(range(len(rows)), key=lambda i: rows[i][4]) holm, k = {}, len(rows) run = 0.0 for rank, i in enumerate(order): run = max(run, min(1.0, rows[i][4] * (k - rank))) holm[i] = run prov = ("⚠ штамп в ключе относится к семейству Ф2а и к этим контрастам НЕприменим" if PASS == "abs4" else "семейство впечатано в ключ ДО ответов" if _family_proven() else "⚠ провенанс семейства НЕ доказуем: в ключе штампа нет") print(f"единиц с голосами: {len(data)} · {prov}, поправка Холма по {k} контрастам\n") print(f"{'контраст':14s}{'ед.':>4s}{'перевес':>9s}{'95% ДИ':>18s}{'p':>8s}{'p Холма':>9s} что меряет") for i, (a, b, what, m, p) in enumerate(rows): lo, hi = BO.boot_ci(m) mark = "✔" if holm[i] < 0.05 else " " print(f"{a + ' vs ' + b:14s}{len(m):4d}{st.mean(m):+9.2f} [{lo:+.2f}, {hi:+.2f}]" f"{p:8.4f}{holm[i]:9.4f}{mark} {what}") print("\n⚠ ПОРОГ КЛАДЁТ ПОЛ, А НЕ SD. Шум одного вызова sd≈2.7 на единицу — это значит, что на" "\n ОТДЕЛЬНОЙ главе армы надёжно не упорядочены: пере-прогон того же арма сдвинет счёт" "\n сильнее, чем разница между армами. Упорядочено только СРЕДНЕЕ по единицам.") def axes() -> None: """Разложение контрастов по осям. Заведено потому, что в третьей редакции ОДНА ось (ФОРМА) переворачивала знак вывода: она штрафует слипание абзацев, а слипание есть прямое исполнение мандата дискурс-перевёрстки боевого `editor.md`. Сумма это прячет, разложение — показывает.""" data = _by_unit() if not data: print("голосов нет — сначала --ingest") return print(f"{'контраст':14s}" + "".join(f"{a:>12s}" for a in J.AXES) + f"{'сумма':>10s}") for a, b, _w in FAMILY: row = [] for ax in J.AXES: m = [d[b][ax] - d[a][ax] for d in data.values() if a in d and b in d] # ⚠ Критерий значимости — ТОЧНЫЙ знаковый тест, а не «бутстрап-ДИ не накрывает ноль». # Первая версия метила звёздочку по ДИ; адверсариальное ревью исполнением показало, # что так помечались значимыми три клетки с p>0.05 (перцентильный бутстрап при n=16 # накрывает ~93%, а не 95%). ДИ отвечает «где лежит среднее», а не «отличимо ли от нуля». row.append(f"{st.mean(m):+7.2f}{'*' if BO.sign_perm_p(m) < 0.05 else ' '} ") tot = _margins(a, b) print(f"{a + ' vs ' + b:14s}" + "".join(row) + f"{st.mean(tot):+10.2f}") print("\n* = p<0.05 точного знакового теста, БЕЗ поправки на 28 клеток. Разложение —" "\n разведочное: `score` корректирует на 7 гипотез, здесь их 28. Читать как направление," "\n не как подтверждение. Перевес > 0 ⇒ ПЕРВЫЙ арм лучше по этой оси.") def position() -> None: """Остаточная позиционная фора: влияет ли НОМЕР МЕТКИ внутри задания на счёт. ⚠ Заведено 07.08 адверсариальным ревью. Шапка рига утверждала, что «позиция исчезает по построению», и контроля не ставила вовсе — регрессия против третьей редакции, где `bakeoff.position_bias()` был. Утверждение неверно: меток в задании 8–9, порядок один, и хеш-перестановка НЕ сбалансирована по армам. Здесь наклон измеряется, а контрасты печатаются с поправкой на него — чтобы видеть, какие выводы от раскладки зависят. """ data = _by_unit() if not data: print("голосов нет — сначала --ingest") return xs, ys = [], [] for d in data.values(): mx = st.mean([r["slot"] for r in d.values()]) my = st.mean([r["errors"] for r in d.values()]) for r in d.values(): xs.append(r["slot"] - mx) ys.append(r["errors"] - my) slope = sum(a * b for a, b in zip(xs, ys, strict=True)) / sum(a * a for a in xs) print(f"наклон {slope:+.3f} ошибки на шаг метки (внутри единицы, n={len(xs)})") print("средняя позиция арма: " + " · ".join( f"{a} {st.mean([d[a]['slot'] for d in data.values() if a in d]):.2f}" for a in ARMS)) print(f"\n{'контраст':14s}{'перевес':>9s}{'Δметки':>8s}{'вклад':>8s}{'с поправкой':>13s}{'p':>9s}") for a, b, _w in FAMILY: m = [d[b]["errors"] - d[a]["errors"] for d in data.values() if a in d and b in d] dl = [d[a]["slot"] - d[b]["slot"] for d in data.values() if a in d and b in d] corr = [mi + slope * x for mi, x in zip(m, dl, strict=True)] print(f"{a + ' vs ' + b:14s}{st.mean(m):+9.2f}{st.mean(dl):+8.2f}" f"{-slope * st.mean(dl):+8.2f}{st.mean(corr):+13.2f}{BO.sign_perm_p(corr):9.4f}") print("\nВывод, зависящий от раскладки, — тот, чей знак или значимость меняются поправкой.") def pool() -> None: """Свод ДВУХ проходов и разложение шума. $0. ⚠ Заведено адверсариальным ревью 07.08: этот анализ считался ВНЕ зафризенного дерева, то есть невоспроизводим из репозитория — тот же класс дефекта, за который снята первая редакция, только для чисел, а не для денег. Касса отказывает незамороженному коду в ПОКУПКАХ, но анализ она не сторожит; сторожит только нахождение кода здесь. ⚠ АЛГЕБРА РАЗЛОЖЕНИЯ, дважды исправленная. пол = две РАЗНЫЕ генерации одного арма, ОДИН судья, одна единица ⇒ var = 2σ²ген + 2σ²идиосинкр; УРОВЕНЬ судьи сокращается точно. кросс = ОДИН И ТОТ ЖЕ текст, ДВА разных судьи ⇒ var = 2σ²идиосинкр + var(разница УРОВНЕЙ). Если брать кросс СЫРЫМ, разница уровней вычитается из пола как если бы это был шум судьи, и σ²ген выходит заниженной. Уровень снимается центрированием внутри единицы и прохода. Первая публикация этого числа («судья 73%, генерация 27%») сделана на сыром кроссе и неверна. """ a1, a2 = _by_unit_pass("abs"), _by_unit_pass("abs2") common = sorted(set(a1) & set(a2)) if not common: print("нужны оба прохода") return sig_ok = sum(1 for u in common for k in a1[u] if k in a2[u] and a1[u][k]["sig"] == a2[u][k]["sig"]) n_cells = sum(1 for u in common for k in a1[u] if k in a2[u]) print(f"единиц в обоих проходах {len(common)} · клеток {n_cells} · " f"подписи текстов совпали {sig_ok}/{n_cells}" f"{' ⛔ СУДИЛИСЬ РАЗНЫЕ ТЕКСТЫ' if sig_ok != n_cells else ''}") E = lambda d, u, k: d[u][k]["errors"] # noqa: E731 flo = [E(d, u, "CTRLfloorA") - E(d, u, "CTRLfloorB") for d in (a1, a2) for u in common if "CTRLfloorA" in d[u]] cen = [] for u in common: m1 = st.mean([r["errors"] for r in a1[u].values()]) m2 = st.mean([r["errors"] for r in a2[u].values()]) cen += [(E(a1, u, k) - m1) - (E(a2, u, k) - m2) for k in a1[u] if k in a2[u]] # ⚠ ПОПРАВКА НА СТЕПЕНИ СВОБОДЫ. Кросс-разности центрированы ВНУТРИ единицы (8–9 клеток), # поэтому `pvariance` занижает σ судьи ~на 11%, и печатный вердикт о доминирующей компоненте # переворачивается. Категорического вердикта нет ни в какую сторону — разрыв меньше смещения. k = st.mean([len(v) for v in a1.values()]) vf, vc = st.pvariance(flo), st.pvariance(cen) * k / max(1.0, k - 1) s_j, s_g = (vc / 2) ** 0.5, max(0.0, (vf - vc) / 2) ** 0.5 print(f"\nпол var {vf:.2f} (n={len(flo)}) = 2σ²ген + 2σ²судья") print(f"кросс var {vc:.2f} (n={len(cen)}) = 2σ²судья, уровень снят центрированием") print(f" ⇒ σ судьи {s_j:.2f} · σ генерации {s_g:.2f} — " f"доминирует {'ГЕНЕРАЦИЯ' if s_g > s_j else 'СУДЬЯ'}") print("\nчто эффективнее для точности контраста:") for n, k, lbl in ((len(common), 1, "как есть"), (len(common), 2, "×2 судьи ($0)"), (2 * len(common), 1, "×2 единицы (платно)")): print(f" {lbl:22s} SE={((2 * s_g ** 2 + 2 * s_j ** 2 / k) / n) ** 0.5:.3f}") rows = [] for a, b, w in FAMILY: m = [st.mean([E(a1, u, b) - E(a1, u, a), E(a2, u, b) - E(a2, u, a)]) for u in common] rows.append((a, b, w, m, BO.sign_perm_p(m))) order = sorted(range(len(rows)), key=lambda i: rows[i][4]) holm, run = {}, 0.0 for rank, i in enumerate(order): run = max(run, min(1.0, rows[i][4] * (len(rows) - rank))) holm[i] = run print(f"\n{'контраст':14s}{'перевес':>9s}{'95% ДИ':>19s}{'p':>9s}{'p Холма':>10s}{'вылетов LOO':>13s}") for i, (a, b, _w, m, pv) in enumerate(rows): lo, hi = BO.boot_ci(m) loo = sum(1 for k in range(len(m)) if BO.sign_perm_p(m[:k] + m[k + 1:]) * len(rows) >= 0.05) if holm[i] < 0.05 else None print(f"{a + ' vs ' + b:14s}{st.mean(m):+9.2f} [{lo:+.2f}, {hi:+.2f}]{pv:9.4f}{holm[i]:10.4f}" f"{'✔' if holm[i] < 0.05 else ' '}{(f'{loo}/{len(m)}' if loo is not None else '—'):>12s}") def _by_unit_pass(name: str) -> dict: global PASS # noqa: PLW0603 sv = PASS PASS = name try: return _by_unit() finally: PASS = sv def judges_agree() -> None: """Согласие судей и разброс порогов. Возможно только после персиста идентичности судьи.""" data = _by_unit() per: dict[str, list[float]] = {} for d in data.values(): j = next(iter(d.values()))["judge"] per.setdefault(j, []).append(st.mean([r["errors"] for r in d.values()])) if len(per) < 2: print("идентичность судьи не персистирована — согласие не считается") return lv = {j: st.mean(v) for j, v in per.items()} print(f"судей {len(per)} · средний уровень строгости по судьям: " f"{min(lv.values()):.2f}…{max(lv.values()):.2f} (разброс {max(lv.values()) - min(lv.values()):.2f})") print(" " + " · ".join(f"{j} {v:.2f}" for j, v in sorted(lv.items()))) print("\n⚠ Разброс порогов между судьями — причина, по которой контраст считается ВНУТРИ") print(" единицы: единицу целиком судит ОДИН судья, и его порог сокращается вычитанием.") def controls() -> None: data = _by_unit() if not data: print("голосов нет — сначала --ingest") return print("КОНТРОЛИ САМОГО СУДЬИ — читать ДО боевых чисел\n") # База декоя берётся ИЗ КЛЮЧА (`decoy_from`), а не из имени арма: панель у каждого пака своя. dec = [d[d["CTRLdecoy"].get("decoy_from", "A")]["errors"] - d["CTRLdecoy"]["errors"] for d in data.values() if "CTRLdecoy" in d and d["CTRLdecoy"].get("decoy_from", "A") in d] if dec: print(f" ДЕКОЙ посаженная деградация против чистого: {st.mean(dec):+.2f} " f"· поймано {sum(1 for x in dec if x < 0)}/{len(dec)} (нужен отрицательный знак)") flo = [d["CTRLfloorA"]["errors"] - d["CTRLfloorB"]["errors"] for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d] if flo: lo, hi = BO.boot_ci(flo) print(f" ПОЛ два вызова ОДНОГО арма на одном входе: {st.mean(flo):+.2f} " f"[{lo:+.2f}, {hi:+.2f}] n={len(flo)} · истинная разница НОЛЬ") # ⚠ sd по единицам — это НЕ порог различимости. Порог кладёт стандартная ошибка # СРЕДНЕГО, то есть sd/√n, и парный дизайн именно этим и полезен. Первая печать этого # блока написала «перевес меньше sd неотличим» — завышение порога вдвое-втрое, # снятое до того, как попало в отчёт. sd = st.pstdev(flo) se = sd / len(flo) ** 0.5 print(f" шум одного вызова: sd по единицам {sd:.2f} · ст.ошибка среднего " f"{se:.2f} при n={len(flo)}") # ⚠ ВЕРДИКТ, а не только числа. Ревью исполнением: заведомо сломанный пол (сдвиг +6 на # каждой единице) печатался тем же видом, что честный ноль, и вдобавок объявлял # «различимый эффект ≈0.00», потому что sd при систематическом сдвиге равен нулю. # критерий — ТОЧНЫЙ знаковый тест, как в `axes`. Прежде здесь стоял бутстрап-ДИ, от # которого `axes` уже отказался: непоследовательность, названная ревью 07.08. ok = BO.sign_perm_p(flo) >= 0.05 print(f" ⇒ различимый средний эффект ≈ {2.8 * se:.2f} (80% мощности); " "sd сам по себе порогом НЕ является — парный дизайн делит его на √n") print(f" ВЕРДИКТ: {'пол ЧЕСТЕН — ноль внутри интервала' if ok else '⛔ ПОЛ СМЕЩЁН: ноль ВНЕ интервала, боевые числа снимаются'}") if sd < 1e-9: print(" ⛔ нулевой sd при ненулевом среднем = систематический сдвиг, не шум") twin = [(u[:8], x["arm"], y["arm"], x["errors"] - y["errors"]) for uid, d in data.items() for u in [uid] for x in d.values() for y in d.values() if x["arm"] < y["arm"] and x["sig"] == y["sig"]] if twin: bad = [t for t in twin if t[3] != 0] print(f" БЛИЗНЕЦ побайтно одинаковых пар: {len(twin)} · " f"перевес ровно ноль в {len(twin) - len(bad)}/{len(twin)}") for t in bad: print(f" ⚠ {t[0]} {t[1]}/{t[2]} разошлись на {t[3]}") else: print(" БЛИЗНЕЦ побайтно одинаковых текстов в пачке нет") # -------------------------------------------------------------------------------------------- def selfcheck() -> int: """Проверка ИСПОЛНЕНИЕМ: разбор, арифметика перевеса и Холм на входах с известным ответом.""" bad = 0 def chk(name: str, got, want) -> None: nonlocal bad ok = got == want bad += not ok print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f" получено {got!r}, ждали {want!r}")) # 1. Однопроходки: вход второй и третьей редакции обязан совпадать побайтно, иначе брать # их тексты из bo2 нельзя. u = BO.units()[0] for arm in ("D", "D_"): chk(f"вход {arm} одинаков в обеих редакциях", BO.build_msgs(arm, u) == BO.build_msgs3(arm, u), True) # Пол обязан быть ПАРОЙ ПОВТОРОВ. Проверяется тем, что вход боевого `A` третьей редакции # ОТЛИЧАЕТСЯ от входа пары пола: если бы совпадал, пол можно было бы брать против боевого. chk("вход боевого A ≠ вход пары пола (иначе пол мерил бы черновик)", BO.build_msgs3("A", u) != BO.build_msgs("A", u), True) for u2 in _units(): k = build_unit(u2)[1] arms_in = {v["arm"] for v in k.values()} if "CTRLfloorA" in arms_in: chk("пол лежит парой, а не одиночкой", "CTRLfloorB" in arms_in, True) break # 2. Раскладка: перестановка детерминирована и не алфавитна. sl = ["F3", "A", "A_law", "B", "D", "D_"] chk("раскладка детерминирована", layout("u1", sl), layout("u1", sl)) chk("раскладка зависит от единицы", layout("u1", sl) != layout("u2", sl), True) # 3. Разбор: ненулевая ось без обоснования отвергается, нулевая — принимается. import tempfile # noqa: PLC0415 global _dirs, KEYS # noqa: PLW0603 sv = (_dirs, KEYS) d = Path(tempfile.mkdtemp()) VOTES, ANSW, KEYS = d / "v", d / "a", d / "k" for q in (VOTES, ANSW, KEYS): q.mkdir(parents=True) _dirs = lambda: (d / "t", ANSW, VOTES) # noqa: E731 key = {"tok": {"Т1": dict(arm="A", kind="боевой", uid="u1", sig="aa"), "Т2": dict(arm="B", kind="боевой", uid="u1", sig="bb")}} (KEYS / f"{PASS}-KEY.json").write_text(json.dumps(key), encoding="utf-8") def answer(t1: tuple, w1: str, t2: tuple, w2: str) -> None: (ANSW / "tok.txt").write_text( "".join(f"Т{i}-{a}: {v}\n" for i, t in ((1, t1), (2, t2)) for a, v in zip(J.AXES, t, strict=True)) .replace("Т1-ФОРМА", "Т1-ФОРМА") + f"Т1-ПОЧЕМУ: {w1}\nТ2-ПОЧЕМУ: {w2}\n", encoding="utf-8") answer((1, 0, 0, 0), "", (0, 0, 0, 0), "") ingest() chk("ненулевая ось без цитаты отвергнута", list(VOTES.glob("*.json")), []) answer((1, 0, 0, 0), "ВЕРНОСТЬ: «кривая фраза»", (0, 0, 0, 0), "") ingest() chk("нулевая ось без цитаты принята", len(list(VOTES.glob("*.json"))), 1) chk("перевес за A при 1 ошибке у A и 0 у B", _margins("A", "B"), [-1.0]) chk("перевес за B зеркален", _margins("B", "A"), [1.0]) # 3а. РЕГРЕССИЯ на найденный дефект: `\s` включает перевод строки, и пустое поле утаскивало # значение соседней строки. Проверяется на обеих ролях — числовой и обосновании. for f in VOTES.glob("*.json"): f.unlink() (ANSW / "tok.txt").write_text( "Т1-ВЕРНОСТЬ: 2\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ:\n" "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() chk("пустое ПОЧЕМУ не утаскивает соседнюю строку", list(VOTES.glob("*.json")), []) (ANSW / "tok.txt").write_text( "Т1-ВЕРНОСТЬ:\nТ1-ТЕРМИН: 5\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ТЕРМИН «а»\n" "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() chk("пустое число не утаскивает соседнюю строку", list(VOTES.glob("*.json")), []) (ANSW / "tok.txt").write_text( "Т1-ВЕРНОСТЬ: 1\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\n" "Т1-ПОЧЕМУ: ВЕРНОСТЬ «первая строка»\n и «вторая строка»\n" "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() got = json.loads((VOTES / "tok.json").read_text(encoding="utf-8"))["Т1"]["why"] chk("многострочное обоснование взято целиком", "вторая строка" in got, True) # 3б. РЕГРЕССИЯ: обоснование обязано НАЗЫВАТЬ каждую ненулевую ось, а не быть просто непустым. for f in VOTES.glob("*.json"): f.unlink() (ANSW / "tok.txt").write_text( "Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 4\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ «а»\n" "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() chk("ненулевая ось, НЕ названная в обосновании, отвергнута", list(VOTES.glob("*.json")), []) (ANSW / "tok.txt").write_text( "Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 4\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\n" "Т1-ПОЧЕМУ: ВЕРНОСТЬ «а» | ТЕРМИН «б»\n" "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() chk("обе ненулевые оси названы — принято", len(list(VOTES.glob("*.json"))), 1) # 3в. РЕГРЕССИИ на дыры приёмки, найденные ревью исполнением 07.08. def one(body: str) -> None: for f in VOTES.glob("*.json"): f.unlink() (ANSW / "tok.txt").write_text( body + "Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8") ingest() one("Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ\n") chk("голое имя оси без цитаты отвергнуто", list(VOTES.glob("*.json")), []) one("Т1-ВЕРНОСТЬ: 0\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 4\n" "Т1-ПОЧЕМУ: формально «а»\n") chk("ось как подстрока слова не засчитана", list(VOTES.glob("*.json")), []) one("Т1-ВЕРНОСТЬ: 1\nТ1-ВЕРНОСТЬ: 7\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\n" "Т1-ПОЧЕМУ: ВЕРНОСТЬ «а»\n") chk("дубль блока пойман", list(VOTES.glob("*.json")), []) one("Т1-ВЕРНОСТЬ: 1\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ «а»\n") chk("годный ответ принят", len(list(VOTES.glob("*.json"))), 1) one("Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ\n") chk("отказ СНЯЛ прежний годный голос", list(VOTES.glob("*.json")), []) one("Т1-ВЕРНОСТЬ: 7\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ «а»\n") # 4. Близнец: одинаковая подпись при разном счёте обязана быть поймана. key["tok"]["Т2"]["sig"] = "aa" (KEYS / f"{PASS}-KEY.json").write_text(json.dumps(key), encoding="utf-8") ingest() import io # noqa: PLC0415 import contextlib # noqa: PLC0415 buf = io.StringIO() with contextlib.redirect_stdout(buf): controls() chk("расхождение близнецов поймано", "разошлись на 7" in buf.getvalue(), True) _dirs, KEYS = sv # 5. Холм: на семи одинаковых p поправка обязана умножать наибольший на 7. chk("Холм монотонен", min(1.0, 0.01 * 7) > min(1.0, 0.01 * 1), True) print(f"\n{'ОСНАСТКА ЧИСТА' if not bad else f'ПРОВАЛОВ: {bad}'}") return bad if __name__ == "__main__": a = sys.argv[1:] or ["--selfcheck"] if a[0][-1] in "34": globals()["PASS"] = "abs" + a[0][-1] a[0] = a[0][:-1] if a[0] in ("--emit2", "--ingest2", "--score2", "--controls2", "--position2", "--judges2", "--axes2", "--agree"): PASS = "abs2" # noqa: PLW0127 globals()["PASS"] = "abs2" a[0] = a[0].replace("2", "") if a[0] != "--agree" else a[0] fn = {"--selfcheck": lambda: sys.exit(1 if selfcheck() else 0), "--emit": emit, "--ingest": ingest, "--score": score, "--controls": controls, "--axes": axes, "--position": position, "--judges": judges_agree, "--pool": pool}.get(a[0]) fn() if fn else print(__doc__)