From f6f04ff9edb7472e258c0782160789a2454419ee Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 8 Aug 2026 21:07:59 +0300 Subject: [PATCH] Replace the decoy plant rules with book-independent ones after the control silently vanished on a different book --- eval/role_topology/absjudge.py | 81 ++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/eval/role_topology/absjudge.py b/eval/role_topology/absjudge.py index 811dd567..e84ebc3b 100644 --- a/eval/role_topology/absjudge.py +++ b/eval/role_topology/absjudge.py @@ -67,7 +67,9 @@ def _dirs(): # ЗАДАНИИ. Пока соль была константой в коде, слепота держалась только запретом судье читать код. # Теперь она в файле рядом с ключом, куда судье ходить запрещено тем же правилом, что и к ключу. def _salt(name: str = "abs") -> str: - f = Path.home() / "books" / "role-topology" / "blind-keys" / f"SALT-{name}.txt" + # ⚠ Путь берётся из 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) @@ -88,12 +90,38 @@ ARMS = ("F3", "A", "A_law", "B", "D", "D_") 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 = "китайском" # как называется язык исходника в задании судье + + +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 означает «ПЕРВЫЙ арм лучше». @@ -132,11 +160,15 @@ def build_unit(u: dict) -> tuple[str, dict]: """Задание на одну единицу + ключ раскладки. Контроли подмешаны и неотличимы от боевых.""" # ⚠ Проход 4 узкий: только армы объявленных контрастов Ф2б плюс база сравнения. Судить # заново все одиннадцать меток незачем — числа Ф2а уже сняты дважды и воспроизвелись. - arms = ("F3", "A_law", "D_") + ARMS_2B if PASS == "abs4" else tuple(ARMS) + ARMS_2B + 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} - kinds: dict[str, str] = {a: "боевой" 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 = slots.get("A") or slots["A_law"] + clean = DECOY_HOOK(slots) if DECOY_HOOK is not None else (slots.get("A") or slots["A_law"]) planted, n = _plant(clean) if n >= 2: slots["CTRLdecoy"], kinds["CTRLdecoy"] = planted, "контроль-декой" @@ -144,11 +176,17 @@ def build_unit(u: dict) -> tuple[str, dict]: # куплен по `build_msgs` (черновик ИЗ КОРПУСА), а боевой `A` третьей редакции — по `build_msgs3` # (СОБСТВЕННЫЙ черновик). Их входы различаются, и пол мерил бы разницу черновиков, а не шум # вызова. Повтором `Aprime` является `bo2-A`, и именно он идёт второй половиной пары. - 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"] = "контроль-пол" + 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)} @@ -161,7 +199,7 @@ def build_unit(u: dict) -> tuple[str, dict]: # берутся из неё дословно — меняется только обрамление. axes_def = J.RUBRIC.split("Посчитай ОШИБКИ", 1)[1].split("Отвечай РОВНО")[0].rstrip() ANSW = _dirs()[1] - body = [f"Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на китайском и {len(order)} " + body = [f"Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на {SRC_LANG} и {len(order)} " f"независимых русских перевода этого же фрагмента под слепыми метками.", "", "Посчитай ОШИБКИ" + axes_def, "", FORMAT_HEAD.format(path=ANSW / f"{tok}.txt", skeleton=skel), @@ -180,13 +218,20 @@ def build_unit(u: dict) -> tuple[str, dict]: # конкурента, и ориентация снималась прогоном самой посадки на обоих кандидатах (10/10 верно). # Значит читающий код судья опознавал и контроль, и арм `A` — базу четырёх контрастов из семи. # Вынос соли закрывал один канал из двух, и не главный. Здесь закрывается второй. +# ⚠ ПОСАДКИ ПЕРЕПИСАНЫ ЭКСП-22 НА КНИГО-НЕЗАВИСИМЫЕ. Прежний список был написан под лексику +# 蛊真人 («не выдержат», «десять», «сто», «поразительна», «спокойно») и на ДРУГОЙ книге давал +# одну сработку из шести при пороге две — декой молча не создавался, и судейская сессия уходила +# БЕЗ контроля. Поймано чтением контролей перед боевыми числами (проход `p2en` эксп-22: 0 декоев +# на 6 единиц). Это ровно ревью-вопрос проекта «заработает ли на книге, которой в репо ещё нет». +# Ниже — правила, опирающиеся на грамматику и типографику русской прозы, а не на её словарь. _PLANTS_DEFAULT = [ - [r"\bне\s+(выдерж\w+)", r"легко \1", "инверсия полярности"], - [r"\bдесят(ь|и)\b", "сорок", "подмена величины"], - [r"\bсто\b", "триста", "подмена величины"], - [r"\bпоразительн(\w+)", r"взгрумительн\1", "выдуманное слово"], - [r"\bспокойн(\w+)", r"безмятежн\1 и невозмутительн\1", "выдуманное слово"], + [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", " ", "слипание абзацев"], ] @@ -215,7 +260,7 @@ def emit() -> None: KEYS.mkdir(parents=True, exist_ok=True) ANSW.mkdir(parents=True, exist_ok=True) keys, n_tx = {}, 0 - for u in BO.units(): + for u in _units(): body, key = build_unit(u) tok = utoken(u["uid"]) (TASKS / f"{tok}.txt").write_text(body, encoding="utf-8") @@ -230,7 +275,7 @@ def emit() -> None: _армы=list(ARMS), _проход=PASS, **keys), ensure_ascii=False, indent=1), encoding="utf-8") print(f"заданий {len(keys)} → {TASKS} · прочтений {n_tx} (парная схема требовала 320)") - print(f"ключ → {KEYS}/abs-KEY.json · судье НЕ давать") + print(f"ключ → {KEYS}/{PASS}-KEY.json · судье НЕ давать") def ingest() -> None: @@ -637,7 +682,7 @@ def selfcheck() -> int: # ОТЛИЧАЕТСЯ от входа пары пола: если бы совпадал, пол можно было бы брать против боевого. chk("вход боевого A ≠ вход пары пола (иначе пол мерил бы черновик)", BO.build_msgs3("A", u) != BO.build_msgs("A", u), True) - for u2 in BO.units(): + for u2 in _units(): k = build_unit(u2)[1] arms_in = {v["arm"] for v in k.values()} if "CTRLfloorA" in arms_in: