Replace the decoy plant rules with book-independent ones after the control silently vanished on a different book

This commit is contained in:
Claude (backend session) 2026-08-08 21:07:59 +03:00
parent bccf2d8842
commit f6f04ff9ed

View file

@ -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"\е\s+(выдерж\w+)", r"легко \1", "инверсия полярности"],
[r"\есят(ь|и)\b", "сорок", "подмена величины"],
[r"\bсто\b", "триста", "подмена величины"],
[r"\bпоразительн(\w+)", r"взгрумительн\1", "выдуманное слово"],
[r"\bспокойн(\w+)", r"безмятежн\1 и невозмутительн\1", "выдуманное слово"],
[r"\е\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: