#!/usr/bin/env python3 """СУДЕЙСТВО ЗАХОДА Д17 — слепой проход по ПОЧИНЕННОЙ рубрике. $0 (агент-сессии). Панель и правило решения зафиксированы пре-регом Д17 ДО покупок. Здесь только эмиссия заданий, разбор ответов и гейты. Ключ и соль свои: раскладка меток есть чистая функция соли, uid и НАБОРА армов, поэтому эмиссия поверх чужого ключа переписала бы раскладку уже отсуженной оси (авария фазы Д, восстановленная только потому, что соль лежит отдельным файлом). ПАНЕЛЬ (8 клеток на единицу, по 6 боевых армов и 2 контроля): p1: ZD · Z0 · ZP · Z8 · Z8R · Z1 + грубый декой + маржевый декой p2: ZD · ZF · ZP · Z8 · Z8R · Z1 + грубый декой + маржевый декой ⚠ ПОЛОВИНЫ ШУМОВОГО ПОЛА РАЗВЕДЕНЫ ПО НАБОРАМ: `Z0` живёт только в p1, `ZF` — только в p2. Это две генерации ОДНОГО И ТОГО ЖЕ боевого редактора, и разница их оценок есть чистый шум прибора, из которого строится порог различимости. Судья, увидевший обе, обесценивает порог целиком. Ширина панели в наборах ОДИНАКОВА (8 клеток) — иначе разница наборов мерила бы усталость судьи, а не шум: замер фазы Д показал, что строгость счёта зависит от ширины пачки. ⚠ `Z7` (однопроходка + канон-фиксер) СУДЕЙСТВОМ НЕ МЕРИТСЯ ВОВСЕ — так объявлено пре-регом. Его вопрос про БАНК, и отвечает на него детерминированный канон-гейт (`--canon`, $0). Судейский слот стоит внимания судьи, а внимание — тот ресурс, за который панель платит строгостью. ⚠ ДОНОР ДЕКОЯ ЧЕРЕДУЕТСЯ (`Z0`/`Z8R` по чётности единицы). В 62 пачках оси Д4 донором всегда был эталон, то есть чувствительность сертифицировалась в окрестности ОДНОГО арма. Запуск: zsud.py --emit | --score [zh|en] | --canon | --selftest """ from __future__ import annotations import collections import hashlib 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 Z = _load("zakhod_s", ZONE / "zakhod.py") import absjudge as AJ # noqa: E402 # ── ПОЧИНЕННАЯ РУБРИКА (решение владельца 16.08). Живёт здесь, потому что потребитель у неё # ОДИН — этот проход; закрытые проходы `tier2`/`ja6` судились старой шкалой и не трогаются. # Обвязки семейств несут то же правило, и совпадение сторожит существующий гейт `paritet.py` # (его список `must`), а не отдельный файл: второй гейт паритета был бы просто второй правдой. # # ⚠ ЧТО ИЗМЕНИЛОСЬ И ПОЧЕМУ — ЗАМЕР, А НЕ ВКУС. Слепое чтение 16.08 дало на английской оси # Спирмена −0.10 между порядком по счёту ошибок и порядком по читаемости: связи нет. Читатель # ВСЛЕПУЮ назвал механизм и попал — «у этого варианта локальных отклонений больше, а читается # заметно лучше». Прибор штрафовал ровно то, чем покупается читаемость. # ⚠ ЦЕНА: старые судейские числа дуги 19→23 с новыми НЕСРАВНИМЫ. # ⚠ РИСК, ЗАЯВЛЕННЫЙ ДО ДАННЫХ: правило «вольность — не ошибка» смещает счёт ВНИЗ, а пачка, # легшая на пол шкалы, перестаёт различать что-либо (проход `tier`: 38% нулей). Поэтому рубрика # идёт В КОМПЛЕКТЕ с двумя сторожами и по отдельности не применяется: гейт плотности и МАРЖЕВЫЙ # декой в каждой пачке. RUBRIC_TYPES = """ ВЕРНОСТЬ — сказано НЕ ТО, что в исходнике. Считается ТОЛЬКО искажение факта: · инверсия адресата или участника (кто кому говорит, кто кого бьёт, кто чей союзник); · снятое или, наоборот, добавленное отрицание; · подмена числа, разряда, ранга, меры, имени, времени, места; · выдуманный факт, которого в исходнике нет; · ПОТЕРЯННЫЙ факт: участник, действие, число, полярность, заголовок; · перевёрнутое состояние действия или идиомы (совершилось против ещё не совершилось). ЯЗЫК — то, чего НОСИТЕЛЬ РУССКОГО НЕ НАПИШЕТ: · калька с исходного языка — буквально переданный оборот, жест или идиома; · канцелярит и переводной суржик: «являлся», «осуществил», нанизывание родительных, отглагольное существительное там, где русский просит глагола; · несуществующее или неуместное слово, рассогласование, кривой порядок слов; · фраза грамматически верная, но звучащая ПЕРЕВЕДЁННОЙ, а не написанной по-русски. ТЕРМИН — имя или термин передан в РАЗНЫХ местах одного текста по-разному. ОПИСАТЕЛЬНАЯ ось. ФОРМА — нарушение РУССКОЙ типографики: реплика не с нового абзаца через тире, прямые кавычки вместо «ёлочек», дефис вместо тире. ОПИСАТЕЛЬНАЯ ось.""" RUBRIC_FREEDOM = """⛔ ЧТО ОШИБКОЙ НЕ ЯВЛЯЕТСЯ — ПРОЧТИ ДО ТОГО, КАК НАЧНЁШЬ СЧИТАТЬ. Живой русский язык — приоритет этого продукта, и переводчик ВПРАВЕ обращаться с буквой исходника свободно. НЕ штрафуй и не считай ошибкой: · перестановку предложений и частей предложения; · слияние или дробление абзацев; число абзацев и предложений совпадать с исходником НЕ обязано; · замену оборота на более живой русский, синонимическую замену, добавленную связку; · распутывание чужого синтаксиса в русскую фразу — это ОБЯЗАННОСТЬ переводчика, а не вольность; · убранную англоязычность или буквализм исходного языка там, где смысл сохранён; · вариант, который просто НРАВИТСЯ тебе меньше соседнего. Ошибка — только то, что ПЕРЕВИРАЕТ СМЫСЛ ИСХОДНИКА (ось ВЕРНОСТЬ) либо ЧЕГО НОСИТЕЛЬ РУССКОГО НЕ НАПИШЕТ (ось ЯЗЫК). Колеблешься между «вольность» и «искажение» — вернись к исходнику и проверь факт: не нашёл искажения факта, значит это вольность и она НЕ считается.""" RUBRIC_ZERO = """⚠ НОЛЬ — ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот вариант с исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ метки, в том числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено». Пустое ПОЧЕМУ не принимается и на диск не попадает. ⚠ И обратное так же важно: правило про вольность НЕ означает «ставь нули». Искажения факта бывают незаметны, потому что русский текст остаётся грамотным и связным, — именно их и надо ловить, сверяя с исходником, а не читая один перевод.""" def rubric() -> str: return (f"Посчитай ОШИБКИ в каждом варианте по типам:\n\n{RUBRIC_TYPES}\n\n" f"{RUBRIC_FREEDOM}\n\n{RUBRIC_ZERO}") OUT = Path.home() / "books" / "dovodka" WORK = Path.home() / "books" / "judging" / "z17" KEYD = OUT / "blind-keys-z17" SALT = "z17-2026-08-16" PER_SESSION = 4 SETS = ("p1", "p2") AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА") CARRY = ("ВЕРНОСТЬ", "ЯЗЫК") # несущая сумма (эррата П-5); ТЕРМИН отдан банк-гейту, ФОРМА описательна NL = chr(10) # ⚠ Посадки грубого декоя берутся ТЕ ЖЕ, что видели судьи фазы Д, — иначе гейт «грубый декой # грубее маржевого» сертифицировал бы не тот текст. Файл копируется, а не читается из чужого # каталога: чужой ключ трогать нельзя даже на чтение-по-ссылке. KEYD.mkdir(parents=True, exist_ok=True) _SRC_PLANTS = OUT / "blind-keys-d4" / "PLANTS.json" if _SRC_PLANTS.exists() and not (KEYD / "PLANTS.json").exists(): (KEYD / "PLANTS.json").write_text(_SRC_PLANTS.read_text(encoding="utf-8"), encoding="utf-8") AJ.KEYS = KEYD BATTLE = ("ZD", "ZP", "Z8", "Z8R", "Z1") # армы, стоящие в ОБОИХ наборах FLOOR = {"p1": "Z0", "p2": "ZF"} # половины пола, разведённые по наборам CTRL = ("CTRLdecoy", "CTRLmargin") SRC_LANG = {"zh": "китайском", "en": "английском"} MARGIN_MIN = 2 _axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE) _whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE) def _sud(pair: str): """`sud` разбирает ось из argv на уровне модуля — грузится ОДИН раз под нужную ось. ⚠ `--wide-plants` ОБЯЗАТЕЛЕН И ПЕРЕДАЁТСЯ ЯВНО (починка по ревью). Хендофф §5.8: классы маржевых посадок ПАР-ЗАВИСИМЫ, узкий (китайский) список на английском ложится 5 из 16, а рабочий класс «подмена принадлежности» даёт 11 из 16. Флаг разрешён «только для новых эмиссий» — заход и есть новая эмиссия. Без него гейт чувствительности П-2, то самое право сказать «не хуже», встал бы на пять точек вместо одиннадцати, а ширина пачек прыгала бы 7/8. """ axis = "d4" if pair == "zh" else "d3" key = f"sud_{axis}" if key in sys.modules: return sys.modules[key] saved = list(sys.argv) sys.argv = [saved[0], f"--axis={axis}", "--wide-plants"] try: m = _load(key, ZONE / "sud.py") finally: sys.argv = saved AJ.KEYS = KEYD # `sud` перенастраивает риг на СВОЙ ключ — вернуть return m def texts_of(pair: str) -> dict[str, dict[str, str]]: """{uid: {арм: текст}} по выбранным единицам пары. Пара включается ОДИН раз, здесь.""" us = Z.chosen(pair) # внутри вызывает wire() своей пары out = {} for u in us: uid = u["uid"] d = {a: Z.free_text(pair, a, uid) for a in ("ZD", "Z0", "ZP")} for a in ("Z8", "Z8R", "Z1", "ZF"): d[a] = Z.cell(Z.tag(pair, a, uid)) d["_src"] = u["source"] out[uid] = d return out def donor_arm(half: str, idx: int) -> str: """Арм-донор контролей ЭТОЙ пачки. ⚠⚠ ПОЧИНКА ПО РЕВЬЮ, И ЭТО БЫЛ САМЫЙ ДОРОГОЙ ИЗ ЕГО НАХОДОК. Прежняя редакция брала донором `Z0` по чётности единицы — включая пачки НАБОРА p2, где `Z0` стоять не должен вовсе: он половина шумового пола, живущая в p1. Проверено исполнением: декой в p2 совпадал с `Z0` на 0.9973 и с `ZF` на 0.138. То есть судья p2 читал `ZF` рядом с текстом, на 99.7% совпадающим со второй половиной пола, — ровно то, что этот же файл в своей шапке называет обесцениванием порога целиком. Порог, а на нём оба вердикта «не различимо», стояли бы на испорченном контроле. Лечение: донор всегда берётся ИЗ АРМОВ СВОЕЙ ПОЛОВИНЫ; чередование по чётности сохранено, но между `Z8R` и половиной пола ЭТОГО набора. """ return "Z8R" if idx % 2 else FLOOR[half] def panel(pair: str, uid: str, tx: dict[str, str], half: str, idx: int) -> dict[str, str]: S = _sud(pair) need = list(BATTLE) + [FLOOR[half]] if not all(tx.get(a, "").strip() for a in need): return {} out = {a: tx[a] for a in need} donor = tx[donor_arm(half, idx)] dec, n_d = AJ._plant(donor) if n_d >= MARGIN_MIN: out["CTRLdecoy"] = dec mg, n_m = S.margin_plant(donor) if n_m >= MARGIN_MIN: out["CTRLmargin"] = mg return out def layout(uid: str, half: str, arms) -> dict[str, str]: order = sorted(arms) seed = hashlib.sha256(f"{SALT}|{uid}|{half}|{'|'.join(order)}".encode()).hexdigest() rank = sorted(order, key=lambda a: hashlib.sha256(f"{seed}|{a}".encode()).hexdigest()) return {f"Т{i}": a for i, a in enumerate(rank, 1)} HEAD = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на {lang} и {n} независимых русских перевода этого же фрагмента под слепыми метками. {rubric} Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между собой. Порядок вариантов ничего не значит и специально перемешан. Числа — это СЧЁТ ошибок, а не оценка по шкале. Две одинаковые ошибки в одном варианте — это два. Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку: {skeleton} ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ». Число без цитаты не принимается и на диск не попадает. """ def emit() -> None: # ⚠ ЭМИССИЯ НЕ ПЕРЕПИСЫВАЕТ КЛЮЧ, ЗА КОТОРЫМ УЖЕ ЕСТЬ ОТВЕТЫ (починка по ревью). Раскладка # меток — чистая функция соли, uid и НАБОРА армов; повторная эмиссия при изменившемся составе # панели переписала бы ключ, и разбор сопоставил бы ответы с ЧУЖИМИ армами. Ровно так в фазе Д # вердикт `E0/D0` съехал с +1.56 на +0.38. Файл защищался от этого только словами в докстринге. got = [f for d in list(WORK.glob("*/p?-answers")) + list(WORK.glob("*/answers")) for f in d.glob("*.txt")] if got and (KEYD / "z17-KEY.json").exists(): raise SystemExit( f"⛔ ЭМИССИЯ ОТКАЗАНА: за ключом `{KEYD / 'z17-KEY.json'}` уже лежит {len(got)} " "ответов.\n Переэмиссия перепишет раскладку меток, и разбор сопоставит эти ответы\n" " с ДРУГИМИ армами. Новый состав панели = НОВАЯ соль и новый ключ.") # ⚠ ГЕЙТ БЛИЗНЕЦА ДЛЯ ПОЛА (починка по ревью). Половины `Z0`/`ZF` лежат в РАЗНЫХ пачках, # поэтому панельная проверка побайтных дублей их не видит по построению. Побайтно равная пара # даёт нулевую разность, ЗАНИЖАЕТ sd и с ним порог различимости — то есть делает вердикты # смелее, чем данные позволяют. Ровно этот класс («близнец») уже стоил фазе перекупки # японского пола. Такие единицы из пола исключаются и называются числом. twins: dict[str, list[str]] = {} key: dict[str, dict] = { "_проход": "z17", "_рубрика": "починенная 16.08: вольность не штрафуется", "_семейство": ["Z8/ZD", "Z8R/Z0", "Z1/Z0", "ZP/Z0"], "_контроль": ["Z0/ZD"], "_пол": ["Z0(p1) против ZF(p2)"]} made = collections.Counter() for pair in Z.PAIRS: tx_all = texts_of(pair) twins[pair] = [u for u, tx in tx_all.items() if tx.get("Z0", "").strip() and tx["Z0"] == tx.get("ZF", "")] if twins[pair]: print(f" ⚠ {pair}: половины пола ПОБАЙТНО РАВНЫ на {len(twins[pair])} единицах — " "они исключаются из пола (иначе порог занижен)") for half in SETS: (WORK / pair / f"{half}-tasks").mkdir(parents=True, exist_ok=True) (WORK / pair / f"{half}-answers").mkdir(parents=True, exist_ok=True) for idx, (uid, tx) in enumerate(sorted(tx_all.items())): for half in SETS: p = panel(pair, uid, tx, half, idx) if not p: miss = [a for a in list(BATTLE) + [FLOOR[half]] if not tx.get(a, "").strip()] print(f" ⚠ {pair}/{uid}/{half}: панель неполна ({', '.join(miss)}) — пропуск") continue lay = layout(uid, half, tuple(p)) tok = hashlib.sha256(f"{SALT}|{pair}|{uid}|{half}".encode()).hexdigest()[:10] skel = "\n\n".join("\n".join(f"{lab}-{a}: <число>" for a in AX) + f"\n{lab}-ПОЧЕМУ: <цитаты>" for lab in lay) body = [HEAD.format(lang=SRC_LANG[pair], n=len(lay), rubric=rubric(), path=WORK / pair / f"{half}-answers" / f"{tok}.txt", skeleton=skel), "", "=" * 60, "ИСХОДНИК:", tx["_src"]] for lab, arm in lay.items(): body += ["", "=" * 60, f"{lab}:", p[arm]] (WORK / pair / f"{half}-tasks" / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8") key[tok] = {lab: {"arm": a, "uid": uid, "half": half, "pair": pair, # донор нужен разбору: гейт декоя обязан сравнивать контроль # с ТЕМ, из чего он сделан, а не с эталоном (починка по ревью) "donor": donor_arm(half, idx) if a.startswith("CTRL") else "", "kind": "контроль" if a.startswith("CTRL") else "боевой", "sig": hashlib.sha256(p[a].encode()).hexdigest()[:12]} for lab, a in lay.items()} made[pair] += 1 key["_пол_близнецы"] = twins (KEYD / "z17-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8") (KEYD / "SALT-z17.txt").write_text(SALT + "\n", encoding="utf-8") print(f"заданий: " + " · ".join(f"{p} {n}" for p, n in made.items())) print(f"ключ → {KEYD} (судье НЕ давать)") for pair in made: toks = [t for t, v in key.items() if not t.startswith("_") and v and next(iter(v.values()))["pair"] == pair] for half in SETS: ht = [t for t in toks if next(iter(key[t].values()))["half"] == half] for i in range(0, len(ht), PER_SESSION): print(f" {pair}/{half} сессия {i // PER_SESSION + 1}: " f"{WORK / pair / f'{half}-tasks'} → " + " ".join(ht[i:i + PER_SESSION])) def read(root: Path | None = None) -> tuple[dict, list]: root = root or WORK key = json.loads((KEYD / "z17-KEY.json").read_text(encoding="utf-8")) out, bad = {}, [] dirs = list(root.glob("*/p?-answers")) + list(root.glob("*/answers")) + [root / "answers"] for d in dirs: if not d.exists(): continue for f in sorted(d.glob("*.txt")): km = key.get(f.stem) if not isinstance(km, dict): continue t = f.read_text(encoding="utf-8", errors="replace") cells: dict[str, dict[str, int]] = collections.defaultdict(dict) for m in _axre.finditer(t): cells["Т" + m.group(1)][m.group(2)] = int(m.group(3)) why = {"Т" + m.group(1): m.group(2).strip() for m in _whyre.finditer(t)} for lab, ax in cells.items(): meta = km.get(lab) if not isinstance(meta, dict): continue if len(ax) < len(AX): bad.append(f"{f.stem}/{lab}: не все четыре оси") continue if not why.get(lab): # ⚠ ПОЧИНКА ПО РЕВЬЮ: прежде замечание записывалось, а клетка ВСЁ РАВНО шла в # счёт. Задание говорит «не принимается и на диск не попадает», обвязка — # «единица целиком выбрасывается»; разбор обязан их исполнять, а не описывать. bad.append(f"{f.stem}/{lab}: ПУСТОЕ обоснование — клетка снята") continue # ⚠ КЛЮЧ НЕСЁТ НАБОР. Прежде ключом было `(пара, единица, арм)`, а боевые армы # стоят в ОБЕИХ пачках: вторая прочитанная молча затирала первую, и какая из двух # оценок выживет, решал порядок файловой системы. Проверено исполнением: из пары # оценок 7 и 1 выживала семёрка. Свой же риг фазы (`itog_d4`) ключует с набором. out[(meta["pair"], meta["uid"], meta["arm"], meta["half"])] = dict( ax, _donor=meta.get("donor", "")) return out, bad def _sign_p(diffs: list[float]) -> float: import math nz = [d for d in diffs if d != 0] n = len(nz) if not n: return 1.0 k = sum(1 for d in nz if d > 0) tail = sum(math.comb(n, i) for i in range(min(k, n - k) + 1)) / 2 ** n return min(1.0, 2 * tail) def _carry(ax: dict) -> int: return sum(ax.get(a, 0) for a in CARRY) def score(pairs: list[str] | None = None) -> int: """Свод захода. ⚠ ВСЕ КОНТРАСТЫ СЧИТАЮТСЯ ВНУТРИ ОДНОГО НАБОРА (починка по ревью). Боевые армы стоят в обеих пачках единицы, и раньше вторая прочитанная затирала первую — вердикт зависел от порядка файловой системы. Теперь для каждой единицы берётся разность армов в ТОЙ ЖЕ пачке (значит сдвиг уровня сессии сокращается — главный конфаундер фазы), и по единице усредняются те наборы, где есть оба арма. Пол — единственная величина, которая МЕЖсессионна намеренно: `Z0` живёт в p1, `ZF` в p2, и их разность как раз и несёт межсессионную компоненту. """ sc, bad = read() if not sc: print("ответов нет — сначала --emit и судейство") return 1 rc = 0 for pair in (pairs or list(Z.PAIRS)): cells = {(u, a, h): ax for (p, u, a, h), ax in sc.items() if p == pair} if not cells: print(f"\n=== {pair}: ответов нет ===") continue uids = sorted({u for u, _, _ in cells}) mine = [b for b in bad if True] print(f"\n=== ПАРА {pair} · единиц {len(uids)} · клеток {len(cells)} · " f"замечаний годности {len(mine)} ===") for b in mine[:6]: print(" ⚠", b) def val(u, a, h): return _carry(cells[(u, a, h)]) if (u, a, h) in cells else None def paired(a, b): """Разности по единицам, усреднённые по наборам, где присутствуют ОБА арма.""" out = [] for u in uids: d = [val(u, a, h) - val(u, b, h) for h in SETS if val(u, a, h) is not None and val(u, b, h) is not None] if d: out.append(st.mean(d)) return out vals = [_carry(ax) for (u, a, h), ax in cells.items() if not a.startswith("CTRL")] zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1) ok_d = zeros < 0.25 rc |= not ok_d print(f"ГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} " f"→ {'ГОДНО' if ok_d else '⛔ ПОЛ ШКАЛЫ — пачка не несёт «не различимо»'}") twin = set(json.loads((KEYD / "z17-KEY.json").read_text(encoding="utf-8")) .get("_пол_близнецы", {}).get(pair, [])) if twin: print(f" ⚠ из пола исключены {len(twin)} единиц: половины побайтно равны") fl = [val(u, "Z0", "p1") - val(u, "ZF", "p2") for u in uids if u not in twin and val(u, "Z0", "p1") is not None and val(u, "ZF", "p2") is not None] sd = st.pstdev(fl) if len(fl) > 1 else 0.0 thr = 2.8 * sd / max(len(fl), 1) ** 0.5 if fl else 0.0 print(f"СВОЙ ПОЛ (две генерации боевой связки, половины в РАЗНЫХ наборах): пар {len(fl)} · " f"sd {sd:.2f} → ПОРОГ {thr:.2f}") if not fl: print(" ⛔ ПОЛА НЕТ — порога различимости не существует, и ни один вердикт " "«различимо / не различимо» этой пачкой не выносится") rc = 1 # ⚠ ДЕКОЙ СРАВНИВАЕТСЯ СО СВОИМ ДОНОРОМ, А НЕ С ЭТАЛОНОМ (починка по ревью): донор # чередуется по чётности единицы, и разность «контроль минус эталон» подмешивала бы в # сертификат чувствительности разницу самих армов. for name, arm in (("ГРУБЫЙ ДЕКОЙ", "CTRLdecoy"), ("МАРЖЕВЫЙ ДЕКОЙ (гейт П-2)", "CTRLmargin")): d, skipped = [], 0 for u in uids: for h in SETS: if (u, arm, h) not in cells: continue dn = cells[(u, arm, h)].get("_donor") or "" if val(u, dn, h) is None: skipped += 1 continue d.append(_carry(cells[(u, arm, h)]) - val(u, dn, h)) if d: good = st.mean(d) > thr and thr > 0 rc |= (arm == "CTRLmargin") and not good print(f"{name}: n={len(d)} среднее над СВОИМ донором {st.mean(d):+.2f} против " f"порога {thr:.2f} → {'ПРОЙДЕН' if good else '⛔ НЕ ПРОЙДЕН'}" + (f" · пропущено без донора {skipped}" if skipped else "")) else: print(f"{name}: не сажался ни в одной пачке — гейт чувствительности ПУСТ") rc |= arm == "CTRLmargin" print(f"\n{'контраст':14s}{'ед.':>5s}{'перевес':>9s}{'p':>9s} вердикт") for a, b in (("Z8", "ZD"), ("Z8R", "Z0"), ("Z1", "Z0"), ("ZP", "Z0"), ("Z0", "ZD")): d = paired(b, a) # положительный перевес = арм `a` ЛУЧШЕ (ошибок меньше) if not d: continue m = st.mean(d) v = ("РАЗЛИЧИМО" if thr and abs(m) > thr and _sign_p(d) < 0.05 else "не различимо") print(f"{a + ' vs ' + b:14s}{len(d):5d}{m:+9.2f}{_sign_p(d):9.4f} {v}") print(f"\n{'арм':12s}{'несущая':>9s}" + "".join(f"{x:>11s}" for x in AX)) for arm in list(BATTLE) + ["Z0", "ZF", *CTRL]: v = [ax for (u, a, h), ax in cells.items() if a == arm] if not v: continue print(f"{arm:12s}{st.mean([_carry(x) for x in v]):9.2f}" + "".join(f"{st.mean([x.get(y, 0) for x in v]):11.2f}" for y in AX)) print("⚠ Вывод ОПИСАТЕЛЬНЫЙ: при n=16 MDE 2.41 против цели 1.50 (`power.py`), " "решение владельца 16.08 — не добирать. Числа НЕСРАВНИМЫ с дугой 19→23: рубрика другая.") return rc def canon() -> int: """Канон-гейт — им и только им меряется `Z7`. $0, детерминированно, без судьи.""" print("ПОКРЫТИЕ БАНКА (доля терминов исходника, переданных канонной формой)\n") for pair in Z.PAIRS: tx_all = texts_of(pair) rows = collections.defaultdict(list) for uid, tx in tx_all.items(): src = tx["_src"] for arm in ("ZD", "Z0", "ZP", "Z8", "Z8R", "Z1"): if tx.get(arm, "").strip(): rows[arm].append(len(Z.C.canon_gaps(src, tx[arm]))) z7 = Z.cell(Z.tag(pair, "Z7", uid)) if z7.strip(): rows["Z7"].append(len(Z.C.canon_gaps(src, z7))) print(f"=== {pair} ===") for arm in ("ZD", "Z0", "ZP", "Z7", "Z8", "Z8R", "Z1"): if rows[arm]: print(f" {arm:5s} потерянных терминов на единицу {st.mean(rows[arm]):5.2f} " f"(единиц {len(rows[arm])})") if rows["ZP"] and rows["Z7"]: print(f" ⇒ канон-фиксер поверх однопроходки: " f"{st.mean(rows['ZP']):.2f} → {st.mean(rows['Z7']):.2f} потерь на единицу") return 0 def selftest() -> int: fails = [] ok = lambda c, m: fails.append(m) if not c else None # noqa: E731 PAR = _load("paritet_s", ZONE / "paritet.py") ok(PAR.main() == 0, "гейт паритета обвязок красный — рубрика разъехалась между семействами") for pair in Z.PAIRS: tx_all = texts_of(pair) ok(len(tx_all) == Z.N_UNITS, f"{pair}: единиц {len(tx_all)}, ожидалось {Z.N_UNITS}") uid, tx = sorted(tx_all.items())[0] for half in SETS: p = panel(pair, uid, tx, half, 0) # ⚠ ПУСТАЯ ПАНЕЛЬ — ЭТО ПРОВАЛ, А НЕ ПРОПУСК. Прежняя редакция делала `continue`, и # пока клетки захода не куплены, ВСЕ проверки панели молча не исполнялись, а селфтест # печатал «провалов 0». Гейт, который не может упасть, ничего не сторожит. ok(bool(p), f"{pair}/{half}: панель не собирается — клетки захода ещё не куплены " f"(нет: {', '.join(a for a in list(BATTLE) + [FLOOR[half]] if not tx.get(a, '').strip())})") if not p: continue sigs = [hashlib.sha256(v.encode()).hexdigest() for v in p.values()] ok(len(sigs) == len(set(sigs)), f"{pair}/{half}: в панели побайтные дубли") ok(("Z0" in p) == (half == "p1"), f"{pair}/{half}: Z0 стоит не в своём наборе") ok(("ZF" in p) == (half == "p2"), f"{pair}/{half}: ZF стоит не в своём наборе") ok("Z7" not in p, f"{pair}/{half}: Z7 попал в судейство, а пре-рег это запрещает") lay = layout(uid, half, tuple(p)) ok(len(lay) == len(set(lay.values())), "раскладка не биективна") ok(layout(uid, half, tuple(sorted(lay.values()))) == lay, "раскладка не воспроизводима") p1 = panel(pair, uid, tx, "p1", 0) p2 = panel(pair, uid, tx, "p2", 0) if p1 and p2: ok(len(p1) == len(p2), f"{pair}: ширина наборов расходится ({len(p1)}/{len(p2)}) — " "разница наборов начнёт мерить усталость судьи, а не шум") # шапка задания обязана нести правило владельца — паритет П-4 требует его В ЗАДАНИИ head = HEAD.format(lang=SRC_LANG[pair], n=8, rubric=rubric(), path="x", skeleton="y") ok("ОШИБКОЙ НЕ ЯВЛЯЕТСЯ" in head and "перестановку предложений" in head, f"{pair}: правило владельца о вольности не в шапке ЗАДАНИЯ (норма паритета П-4: " "правила счёта в обвязке до одного из семейств в паке 23 не доехали)") ok("НЕ означает «ставь нули»" in head, f"{pair}: нет сторожа против сползания в нули") ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест сломан") ok("_пол_близнецы" in emit.__code__.co_consts or True, "") for pair in Z.PAIRS: # донор контролей обязан быть в СВОЕЙ половине for h in SETS: for idx in (0, 1): ok(donor_arm(h, idx) in (list(BATTLE) + [FLOOR[h]]), f"{pair}/{h}: донор декоя {donor_arm(h, idx)} не из армов своей половины") ok((KEYD / "PLANTS.json").exists(), "посадки грубого декоя не заведены") for m in fails: print("ПРОВАЛ:", m) print(f"селфтест z17: провалов {len(fails)}") return 1 if fails else 0 if __name__ == "__main__": a = sys.argv[1:] or ["--selftest"] if a[0] == "--emit": emit() elif a[0] == "--score": sys.exit(score([x for x in a[1:] if x in Z.PAIRS] or None)) elif a[0] == "--canon": sys.exit(canon()) elif a[0] == "--selftest": sys.exit(selftest()) else: raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")