Fix eight defects found by the cross-family adversarial review before any judging answer exists

This commit is contained in:
Claude (backend session) 2026-08-16 03:53:13 +03:00
parent 2579cb82ae
commit 2087237877
5 changed files with 1114 additions and 36 deletions

374
eval/dovodka/chtenie.py Normal file
View file

@ -0,0 +1,374 @@
#!/usr/bin/env python3
"""ПРИБОР ТКАНИ — слепое ранжирование читаемости. $0.
ЗАЧЕМ. Весь риг 1923 считает ЛОКАЛЬНЫЕ ошибки внутри чанка и по построению не видит того, что
владелец назвал целью 1: победы над translationese. Пре-рег П-6 завёл под это слепое ранжирование
издательской пригодности, владелец 11.08 сказал «берём» и оно не было построено.
Файл прошёл две редакции, и обе живут здесь, а не в двух файлах:
1. КАЛИБРОВКА 16.08 одна единица на ось, без контролей. Читал Fable по поручению владельца.
Дала результат, ради которого всё дальнейшее и делается: на английской оси Спирмен между
счётом ошибок и читаемостью **0.10**, связи нет. Улики: `~/books/chtenie-vladeltsa/ОТВЕТ-*.md`,
ключ `~/books/dovodka/blind-keys-chtenie/`. Пере-считать: `--score-calib`.
2. ПОЛНАЯ ПАНЕЛЬ захода Д17 выборка единиц, ДВА независимых читателя и контроли, которых у
калибровки не было:
· КОНТРОЛЬ ДЕКОЯ: намеренно испорченный вариант обязан ранжироваться последним;
· КОНТРОЛЬ ПОЛА: две генерации ОДНОГО редактора (`Z0`/`ZF`) обязаны оказаться СОСЕДЯМИ
развёл далеко, значит порядок читателя есть шум, и «победа» внутри этого расстояния
ничего не значит. Это для чтения то же, чем шумовой пол служит счёту.
ЧЕГО ПРИБОР НЕ МОЖЕТ. Абсолютной оценки качества он не даёт и читателя-человека не заменяет:
читают модели. Он отвечает на ОДИН вопрос совпадает ли порядок по читаемости с порядком по
счёту ошибок, и потому решает судьбу СЧЁТА как дешёвого прокси, а не судьбу армов.
Запуск: chtenie.py --emit | --score | --selftest | --score-calib
"""
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
ZS = _load("zsud_c", ZONE / "zsud.py")
Z = ZS.Z
WORK = Path.home() / "books" / "chtenie-z17"
KEYD = Path.home() / "books" / "dovodka" / "blind-keys-chtenie-z17"
CALIB = Path.home() / "books" / "chtenie-vladeltsa"
CALIB_KEY = Path.home() / "books" / "dovodka" / "blind-keys-chtenie" / "chtenie-KEY.json"
SALT = "chtenie-z17-2026-08-16"
N_UNITS = 8 # на ось; больше читатель за раз честно не осилит
READERS = ("A", "B") # два независимых читателя РАЗНЫХ семейств
# Единиц в задании: китайская глава ~9.7 тыс. знаков × 7 вариантов = 68 тыс. на единицу,
# английская ~1.9 тыс. × 7 = 13 тыс. Поэтому китайские читаются по одной, английские — по четыре.
PER_TASK = {"zh": 1, "en": 4}
PANEL = ("ZD", "Z0", "ZF", "ZP", "Z8R", "Z1")
DECOY = "CTRLdecoy"
HEAD = """# Слепое чтение — {pair}, задание {k}
## Что это и зачем
Наш измерительный аппарат считает ЛОКАЛЬНЫЕ ОШИБКИ: искажения смысла, кальки, сбитую вёрстку
штука за штукой, внутри отрывка. Это дёшево и воспроизводимо, но у такого прибора есть встроенная
слепота: **текст может быть без единой ошибки и при этом мёртвый**. Серая гладкая проза получит
«претензий нет», а читатель закроет книгу.
Поэтому здесь спрашивают ЧИТАТЕЛЯ. Ниже один и тот же отрывок в нескольких вариантах перевода,
сделанных разными способами. Способы не названы намеренно: метки перемешаны, соответствие
«метка способ» лежит в отдельном файле и до твоего ответа не открывается.
Твой ответ решает не «какой способ лучше» он решает, **совпадает ли человеческий порядок с тем,
что печатает наш счётчик ошибок**. Совпал счёт можно и дальше использовать как дешёвую замену
чтению. Разошёлся значит все выводы мерили не то.
## Как читать
Читай как ЧИТАТЕЛЬ, а не как корректор. Вопрос один: **какой текст ты взял бы в книгу**.
Главный критерий живая русская проза против переводного канцелярита. Смысловые ошибки, если
заметишь, называй, но ранжируй по читаемости.
**Три вещи, которые могут тебя сбить, говорю заранее.**
1. **Вёрстка выдаёт метод.** Часть вариантов прошла полное переписывание, а оно обязано сливать
построчный исходник в русские абзацы; остальные вёрстку исходника сохраняют. Разницу видно ДО
чтения не выводи из неё, «какой это способ», и не давай ей решать за тебя.
2. **Не угадывай гипотезы.** Не пытайся понять, каким способом сделан вариант, и не проверяй по
ходу, совпало ли с ожиданием. Запиши порядок ДО любых рассуждений о методах.
3. **Похожие варианты это нормально.** Часть вариантов может оказаться очень близка друг к
другу. Не различаешь так и напиши через «=», это законный и ценный ответ.
## Что записать
Ответ в файл `{answer}`. На КАЖДЫЙ отрывок обязательно две вещи:
1. Строка `ПОРЯДОК <номер отрывка>: Т3 > Т1 > Т5 > Т2 > Т4 > Т6 > Т7` от лучшего к худшему.
Неразличимые варианты соединяй знаком `=`.
2. По одной-две фразы на вариант: почему. Годится и «серо, но гладко», и «живой ритм, но во
втором абзаце потерян смысл».
Если какой-то вариант покажется откровенно машинным скажи прямо, это самая ценная информация.
## Границы
* Не открывай на диске ничего, кроме этого задания, и не пиши никуда, кроме указанного файла.
* Не выясняй происхождение вариантов; не применяй `diff`, `difflib`, `cmp` и механическое сличение.
* Не запускай своих агентов: сессия это ты один.
* Не переводи сам и не предлагай свою версию.
"""
def pick(pair: str) -> list[str]:
"""N_UNITS единиц из ОТСУЖЕННОГО набора — чтобы порядки читателя и счётчика сравнивались
на одних и тех же текстах. Отбор детерминирован своей солью."""
uids = sorted(u["uid"] for u in Z.chosen(pair))
return sorted(uids, key=lambda u: hashlib.sha256(f"{SALT}|{pair}|{u}".encode()).hexdigest()
)[:N_UNITS]
def variants(pair: str, tx: dict[str, str]) -> dict[str, str]:
"""⚠ ПОЛНАЯ ПАНЕЛЬ ИЛИ НИЧЕГО (починка по ревью). Прежняя редакция отдавала читателю столько
вариантов, сколько куплено, а средние ранги, снятые на пятёрке и на восьмёрке вариантов,
несравнимы между собой (ранг 1..5 против 1..8). Неполная единица просто не эмитируется."""
if not all(tx.get(a, "").strip() for a in PANEL):
return {}
out = {a: tx[a] for a in PANEL}
dec, n = ZS.AJ._plant(tx.get("Z0", ""))
if n >= ZS.MARGIN_MIN:
out[DECOY] = dec
return out
def layout(pair: str, uid: str, reader: str, arms) -> dict[str, str]:
"""Своя раскладка КАЖДОМУ читателю: одинаковая давала бы им общий позиционный наклон."""
order = sorted(arms)
seed = hashlib.sha256(f"{SALT}|{pair}|{uid}|{reader}|{'|'.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)}
def emit() -> None:
key: dict = {"_что": "слепое чтение по полной панели захода Д17 (П-6)", "_соль": SALT,
"онтроли": ["декой обязан быть последним", "Z0 и ZF обязаны быть соседями"]}
made = 0
for pair in Z.PAIRS:
tx_all = ZS.texts_of(pair)
uids = [u for u in pick(pair) if variants(pair, tx_all[u])]
for reader in READERS:
d = WORK / pair / reader
(d / "tasks").mkdir(parents=True, exist_ok=True)
(d / "answers").mkdir(parents=True, exist_ok=True)
step = PER_TASK[pair]
for k in range(0, len(uids), step):
grp = uids[k:k + step]
tok = hashlib.sha256(f"{SALT}|{pair}|{reader}|{grp[0]}".encode()).hexdigest()[:10]
ans = d / "answers" / f"{tok}.md"
body = [HEAD.format(pair=("китайская ось" if pair == "zh" else "английская ось"),
k=k // step + 1, answer=ans)]
for j, uid in enumerate(grp, 1):
vs = variants(pair, tx_all[uid])
lay = layout(pair, uid, reader, tuple(vs))
body += ["", "=" * 70, f"## ОТРЫВОК {j} — ИСХОДНИК", "",
tx_all[uid]["_src"], ""]
for lab, arm in lay.items():
body += ["-" * 70, f"### ОТРЫВОК {j}, вариант {lab}", "", vs[arm], ""]
key.setdefault(tok, {})[str(j)] = {
"uid": uid, "pair": pair, "reader": reader, "метки": lay,
"знаков": {a: len(t) for a, t in vs.items()}}
(d / "tasks" / f"{tok}.md").write_text("\n".join(body), encoding="utf-8")
made += 1
KEYD.mkdir(parents=True, exist_ok=True)
(KEYD / "chtenie-z17-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
encoding="utf-8")
print(f"заданий {made}{WORK}/<пара>/<читатель>/tasks")
print(f"ключ → {KEYD} (читателю НЕ давать)")
# ⚠ Латинская `T` принимается наравне с кириллической `Т` (починка по ревью): судьи уже писали
# латиницей, а `zsud`/`ja6` это учитывают. Молча выпавший ответ читателя стоил бы целой сессии.
_ORD = re.compile(r"ПОРЯДОК\s*(\d+)\s*:\s*((?:[ТT]\d+\s*[>=]\s*)+[ТT]\d+)", re.IGNORECASE)
def _ranks(order: str) -> dict[str, float]:
"""Метка → ранг (1 = лучший). Связанные через `=` получают средний ранг."""
out, pos = {}, 1
for g in [x.strip() for x in re.split(r">", order)]:
labs = [x.replace("T", "Т") for x in re.findall(r"[ТT]\d+", g)]
for x in labs:
out[x] = pos + (len(labs) - 1) / 2
pos += len(labs)
return out
def _rank(v: list[float]) -> list[float]:
"""Ранги со средним для связок — как `scipy.rankdata`, сверено с ним."""
order = sorted(range(len(v)), key=lambda i: v[i])
out = [0.0] * len(v)
i = 0
while i < len(order):
j = i
while j + 1 < len(order) and v[order[j + 1]] == v[order[i]]:
j += 1
r = (i + j) / 2 + 1
for k in range(i, j + 1):
out[order[k]] = r
i = j + 1
return out
def _spearman(a: list[float], b: list[float]) -> float:
"""⚠ ПОЧИНКА ПО РЕВЬЮ: прежняя редакция называлась Спирменом, а считала ПИРСОНА по входам.
Там, где на вход шли СРЕДНИЕ ранги и СРЕДНИЕ счёты ошибок (решающий гейт «следит ли счёт за
читаемостью»), это давало другое число: на демо-входах своё +0.79 против настоящего +1.00.
Гейт `rho > 0.5` по пре-регу Д17 решающий эндпойнт, и он не имеет права быть приблизительным.
"""
if len(a) < 3:
return 0.0
a, b = _rank(a), _rank(b)
ma, mb = st.mean(a), st.mean(b)
num = sum((x - ma) * (y - mb) for x, y in zip(a, b))
da = sum((x - ma) ** 2 for x in a) ** 0.5
db = sum((y - mb) ** 2 for y in b) ** 0.5
return num / (da * db) if da and db else 0.0
def score() -> int:
kf = KEYD / "chtenie-z17-KEY.json"
if not kf.exists():
print("ключа нет — сначала --emit")
return 1
key = json.loads(kf.read_text(encoding="utf-8"))
sc, _bad = ZS.read()
ranks: dict[tuple, dict[str, float]] = {}
for tok, blocks in key.items():
if tok.startswith("_"):
continue
meta0 = next(iter(blocks.values()))
f = WORK / meta0["pair"] / meta0["reader"] / "answers" / f"{tok}.md"
if not f.exists():
continue
got = {m.group(1): m.group(2) for m in _ORD.finditer(
f.read_text(encoding="utf-8", errors="replace"))}
for j, meta in blocks.items():
if j not in got:
print(f"{tok}/отрывок {j}: порядка нет")
continue
r = _ranks(got[j])
ranks[(meta["pair"], meta["uid"], meta["reader"])] = {
arm: r[lab] for lab, arm in meta["метки"].items() if lab in r}
if not ranks:
print("ответов читателей нет")
return 1
rc = 0
for pair in Z.PAIRS:
rows = {k: v for k, v in ranks.items() if k[0] == pair}
if not rows:
continue
print(f"\n=== ПАРА {pair} · прочитано единиц-читателей {len(rows)} ===")
bad_d = [k for k, v in rows.items() if DECOY in v and v[DECOY] < max(v.values())]
have_d = sum(1 for v in rows.values() if DECOY in v)
print(f"КОНТРОЛЬ ДЕКОЯ: не последним в {len(bad_d)} чтениях из {have_d}"
f"{'ГОДНО' if not bad_d else '⛔ читатель невнимателен'}")
rc |= bool(bad_d)
gaps = [abs(v["Z0"] - v["ZF"]) for v in rows.values() if "Z0" in v and "ZF" in v]
if gaps:
far = sum(1 for g in gaps if g > 2)
print(f"КОНТРОЛЬ ПОЛА (две генерации одного редактора): медиана расстояния "
f"{st.median(gaps):.1f} позиции · развёл дальше двух в {far} из {len(gaps)}"
f"{'ГОДНО' if far <= len(gaps) // 4 else '⛔ порядок читателя = шум'}")
rc |= far > len(gaps) // 4
per_reader = collections.defaultdict(dict)
for (_p, uid, rd), v in rows.items():
per_reader[rd][uid] = v
if len(per_reader) == 2:
(ra, va), (rb, vb) = sorted(per_reader.items())
rr = [_spearman([va[u][x] for x in va[u] if x in vb[u]],
[vb[u][x] for x in va[u] if x in vb[u]]) for u in va if u in vb]
if rr:
print(f"СОГЛАСИЕ ЧИТАТЕЛЕЙ {ra}/{rb}: Спирмен по единицам {st.mean(rr):+.2f} "
f"(единиц {len(rr)})")
avg = {a: st.mean([v[a] for v in rows.values() if a in v])
for a in PANEL + (DECOY,) if any(a in v for v in rows.values())}
print(f"\n{'арм':12s}{'ранг чтения':>13s}{'ошибок/ед.':>12s}")
xy = []
for a in sorted(avg, key=lambda x: avg[x]):
errs = [ZS._carry(ax) for (pp, u, aa, _h), ax in sc.items()
if pp == pair and aa == a and u in {k[1] for k in rows}]
print(f"{a:12s}{avg[a]:13.2f}"
+ (f"{st.mean(errs):12.2f}" if errs else f"{'':>12s}"))
if errs and not a.startswith("CTRL"):
xy.append((avg[a], st.mean(errs)))
if len(xy) >= 3:
rho = _spearman([x for x, _ in xy], [y for _, y in xy])
print(f"\nСПИРМЕН «ранг чтения против счёта ошибок»: {rho:+.2f}"
+ ("счёт следит за читаемостью, прокси годен" if rho > 0.5 else
"⛔ СЧЁТ НЕ СЛЕДИТ ЗА ЧИТАЕМОСТЬЮ: по пре-регу Д17 побеждает ЧТЕНИЕ"))
print("\n⚠ Читают модели, не человек. Прибор отвечает на вопрос «годен ли счёт как прокси», "
"а не «какой арм лучше вообще».")
return rc
def score_calib() -> int:
"""Де-слепление КАЛИБРОВКИ 16.08: печатает, какой арм стоял за какой меткой в ответе
читателя.
ЧЕСТНАЯ ГРАНИЦА (правка по ревью): режим НЕ пере-считывает корреляцию 0.10 он
печатает только порядок читателя. Прежний докстринг обещал воспроизводимость этого
числа, и обещание было ложным. Само число проверено независимо (адверсариальное ревью
16.08 пере-считало его по рангам из ключа и панели Д3 вышло ровно 0.10), но КОМАНДЫ,
делающей это одним вызовом, нет. Долг назван, а не замаскирован."""
if not CALIB_KEY.exists():
print("ключа калибровки нет")
return 1
key = json.loads(CALIB_KEY.read_text(encoding="utf-8"))
for axis in ("zh", "en"):
f = CALIB / f"ОТВЕТ-{axis}.md"
if not f.exists() or axis not in key:
print(f"{axis}: ответа нет")
continue
m = re.search(r"((?:Т\d\s*[>=]\s*)+Т\d)", f.read_text(encoding="utf-8"))
if not m:
print(f"{axis}: порядок не найден")
continue
lay = key[axis]["метки"]
print(f"\n{axis}: единица {key[axis]['uid']} · порядок читателя (лучший → худший):")
for i, lab in enumerate(re.findall(r"Т\d", m.group(1)), 1):
print(f" {i}. {lab} = {lay.get(lab, '?')}")
return 0
def selftest() -> int:
fails = []
ok = lambda c, m: fails.append(m) if not c else None # noqa: E731
r = _ranks("Т3 > Т1 = Т5 > Т2")
ok(r == {"Т3": 1.0, "Т1": 2.5, "Т5": 2.5, "Т2": 4.0}, f"разбор порядка сломан: {r}")
ok(abs(_spearman([1, 2, 3], [1, 2, 3]) - 1.0) < 1e-9, "Спирмен не даёт +1 на совпадении")
ok(abs(_spearman([1, 2, 3], [3, 2, 1]) + 1.0) < 1e-9, "Спирмен не даёт 1 на инверсии")
m = _ORD.search("ПОРЯДОК 2: Т3 > Т1 > Т5")
ok(bool(m) and m.group(1) == "2", "строка порядка не находится")
for pair in Z.PAIRS:
u = pick(pair)
ok(len(u) == N_UNITS, f"{pair}: единиц {len(u)}, ожидалось {N_UNITS}")
ok(set(u) <= {x["uid"] for x in Z.chosen(pair)},
f"{pair}: единицы чтения не подмножество отсуженных — Спирмен считать не на чем")
la, lb = layout(pair, u[0], "A", PANEL), layout(pair, u[0], "B", PANEL)
ok(la != lb, f"{pair}: у читателей ОДНА раскладка — общий позиционный наклон")
ok(layout(pair, u[0], "A", PANEL) == la, f"{pair}: раскладка не воспроизводима")
for pair in Z.PAIRS: # панель чтения обязана СОБИРАТЬСЯ, иначе гейт пуст
tx_all = ZS.texts_of(pair)
n = sum(1 for u in pick(pair) if variants(pair, tx_all[u]))
ok(n == N_UNITS, f"{pair}: панель чтения собирается на {n} единицах из {N_UNITS}"
"клетки захода не куплены, эмиссия дала бы неполные пачки")
for m in fails:
print("ПРОВАЛ:", m)
print(f"селфтест чтения: провалов {len(fails)}")
return 1 if fails else 0
if __name__ == "__main__":
a = sys.argv[1:] or ["--selftest"]
fn = {"--emit": emit, "--score": lambda: sys.exit(score()),
"--score-calib": lambda: sys.exit(score_calib()),
"--selftest": lambda: sys.exit(selftest())}.get(a[0])
if not fn:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")
fn()

View file

@ -53,7 +53,9 @@ def main() -> int:
norm(c) in norm(f.read_text(encoding="utf-8")))
# ядро обязано нести правила, купленные дорого прошлыми паками
must = ["ОТДЕЛЬНО против исходника", "требует обоснования ЦИТАТОЙ",
"Ноль — это утверждение", "потерянное отрицание", "Не ищи и не открывай ничего другого"]
"Ноль — это утверждение", "потерянное отрицание", "Не ищи и не открывай ничего другого",
# решение владельца 16.08, меняющее шкалу: см. `zsud.RUBRIC_*` и эрратy Д18
"переврать смысл исходника — ошибка; обойтись с его буквой вольно — нет"]
for m in must:
ck(f"ядро несёт правило: «{m[:44]}»", norm(m) in norm(c))
print(f"\n{'ПАРИТЕТ ОБВЯЗОК СОБЛЮДЁН' if not bad else f'ПРОВАЛОВ: {bad}'}")

View file

@ -63,8 +63,36 @@ AXES = [
"и продуктового смысла при цене 28× не имеет. ⚠ Именно этот порог решил спор о числе "
"клеток: при n=12 (минимум промта) MDE 2.11 БОЛЬШЕ цели и ось была бы ОПИСАТЕЛЬНОЙ; "
"владелец 10.08 выбрал полные 16, и только на них ось несущая"),
# ⚠ ЗАХОД Д17 ВПИСАН 16.08, УЖЕ ПОСЛЕ НАЧАЛА ПОКУПКИ — ЭТО ДЕВИАЦИЯ, И ОНА ОБЪЯВЛЕНА ЗДЕСЬ,
# А НЕ ПРИМЕЧАНИЕМ ПОСТ-ФАКТУМ. Норма заказа (:168) требует НАПЕЧАТАТЬ MDE при плановом n до
# денег; пре-рег Д17.4 объявил ПРАВИЛО («MDE считается по своему полу, не проходит цель — ось
# описательная»), но числа не напечатал, хотя инструмент для этого стоит в зоне. Покупка к
# моменту записи прошла ~40 клеток из 160. Числа ниже посчитаны ДО первого судейского ответа,
# то есть право пере-классифицировать ось до вскрытия вердиктов сохранено.
# ⚠ И ГЛАВНАЯ ОГОВОРКА ПРАЙОРА: все полы выше сняты СТАРОЙ рубрикой, а заход судится
# ПОЧИНЕННОЙ («вольность не штрафуется»), которая по построению убирает часть счёта. Перенос
# прайора между шкалами — допущение, а не замер, и потому статус оси решает НЕ он, а СВОЙ пол
# захода (`ZF` против `Z0`, две генерации боевой связки), снятый на новой шкале.
("Д17 заход zh (новая рубрика)", 16, 4, 2.12, 1.50,
"армы правят ОДНУ базу — структура `tier`/`border` (sd 1.45/2.12), берётся худшая; "
"⚠ прайор снят СТАРОЙ рубрикой и на новую шкалу переносится как допущение",
"та же цель, что у Д4: решение «менять архитектуру» стоит принимать при разрыве ≥ трети "
"собственного эффекта редактора (+4.44 эксп-22 Ф3) — это 1.50"),
("Д17 заход en (новая рубрика)", 16, 4, 2.12, 1.50,
"своего пола на en в новой шкале нет; прайор с zh той же структуры, свой пол докупается",
"цель снижена с 2.00 до 1.50 против оси Д3 сознательно: там вопрос был «переносится ли "
"эффект редактора вообще», здесь — «какая из четырёх архитектур лучше», и решение о смене "
"архитектуры принимается при меньшем разрыве"),
]
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка"} # объявлено промтом, арифметикой не отменяется
# ⚠ РЕШЕНИЕ ВЛАДЕЛЬЦА 16.08, ПРИНЯТО ДО ЕДИНОГО СУДЕЙСКОГО ОТВЕТА. Мощность захода посчитана уже
# после начала покупки (девиация объявлена выше), и она показала: при n=16 и цели 1.50 несущий
# вывод недостижим — нужен свой пол sd ≤ 1.32, а все замеренные полы фазы 1.45…2.63. Владельцу
# предложено добрать китайскую ось до 31 главы за $0.48 (тогда порог требований падает до 1.83,
# то есть достижим); он выбрал НЕ добирать. Значит обе оси захода ОПИСАТЕЛЬНЫЕ, и решающим
# эндпойнтом остаётся слепое чтение — ровно так, как пре-рег Д17 и объявил заранее.
# Пере-классификации задним числом не будет ни при каком исходе арифметики.
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка",
"Д17 заход zh (новая рубрика)", "Д17 заход en (новая рубрика)"}
# ⚠ ПЕРВИЧНОЕ И ВТОРИЧНОЕ СЕМЕЙСТВА — объявляются ДО покупок, и это НЕ сужение задним числом.
# Поправка Холма делится на РАЗМЕР семейства, поэтому «взять все контрасты несущими» = сделать
@ -78,6 +106,18 @@ PRIMARY = {
"A6/A0 — качественный черновик + контур (H-1)"],
"Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"],
"Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"],
# Заход Д17: по одному контрасту на КАЖДЫЙ вопрос, ради которого куплен арм. `Z7` в семействе
# нет намеренно — он меряется детерминированным канон-гейтом и судейского слота не занимает.
"Д17 заход zh (новая рубрика)": [
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
"Д17 заход en (новая рубрика)": [
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
}
# ⚠⚠ n У КАЖДОГО КОНТРАСТА СВОЙ, И ЭТО РЕШАЕТ. Прежняя редакция печатала MDE и статус ПО ОСИ при
# плановом n=32 — а члены первичного семейства живут на разных выборках, потому что база у них
@ -92,6 +132,14 @@ CONTRAST_N = {
"A6/A0 — качественный черновик + контур (H-1)": 16,
"E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)": 16,
"R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)": 16,
# ⚠ У захода n=16 у ВСЕХ четырёх контрастов, и это не совпадение, а условие отбора единиц:
# в пул берутся только те, где ВСЕ бесплатные армы и ответ критика уже есть (`zakhod.pool`).
# Именно поконтрастная проверка n погубила носителя H-1 (`A6/A0` жил на 16 из 32) — здесь
# разъехаться выборкам структурно не на чем.
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)": 16,
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)": 16,
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)": 16,
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале": 16,
}
SECONDARY = {
@ -199,16 +247,19 @@ def main() -> int:
m = mde(sd, n_c, k)
ok = m <= target
if not ok:
weak.append((c, n_c, m, target))
weak.append((c, n_c, m, target, k))
print(f" {c[:56]:56s}{n_c:4d}{m:7.2f}{target:7.2f} "
f"{'НЕСУЩИЙ' if ok else '⛔ НЕДОМОЩЕН'}")
if weak:
print("\n ⛔ НЕДОМОЩНЫЕ КОНТРАСТЫ ПЕРВИЧНОГО СЕМЕЙСТВА — объявляются ОПИСАТЕЛЬНЫМИ:")
for c, n_c, m, t in weak:
need_sd = t * math.sqrt(n_c) / ((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
* BETWEEN)
need_n = math.ceil((((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
* 2.12 * BETWEEN) / t) ** 2)
# ⚠ ПОЧИНКА ПО РЕВЬЮ: множитель Холма был ЗАШИТ как k=3 (`0.05 / 6`) для всех осей, тогда
# как сам `mde()` считает по настоящему k. Печатались ремонтные числа мягче честных: для
# контрастов захода (k=4) выходило «sd ≤ 1.37 либо n ≥ 39» вместо честных 1.32 и 42.
# Купив по этой строке пол с sd 1.35, ось объявили бы несущей ошибочно.
for c, n_c, m, t, k_c in weak:
mult = (_t(1 - 0.05 / (2 * max(1, k_c)), PRIOR_DF) + _t(0.80, PRIOR_DF)) * BETWEEN
need_sd = t * math.sqrt(n_c) / mult
need_n = math.ceil((mult * 2.12 / t) ** 2)
print(f"{c}\n при n={n_c} MDE {m:.2f} > цели {t:.2f}; несущим станет "
f"либо при своём поле sd ≤ {need_sd:.2f}, либо при n ≥ {need_n}")
print("\nСЕМЕЙСТВА, объявленные ДО покупок:")

View file

@ -46,7 +46,7 @@
ОТКАЗЫВАЕТСЯ, пока ростер не пере-пинен. Это не осторожность, а единственное, что отличает
леджер-измеритель от леджера-фантазёра.
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы] | --plan
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы] | --echo
"""
from __future__ import annotations
@ -95,7 +95,11 @@ JULY_PIN_PRO = 0.435 # если в ростере всё ещё
# ── ПАРА КАК ДАННЫЕ ───────────────────────────────────────────────────────────────────────────
# Ни одна ветка ниже не спрашивает «а это китайский?». Всё, что у пары своё, — здесь.
PAIRS = {
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 16000,
# ⚠ БЮДЖЕТ ВЫВОДА 32000 НА ОБЕИХ ПАРАХ (починка по ревью). Хендофф §5.4 предупреждал прямо:
# «бюджет вывода новых пар 32000, 16000 мало». Я оставил китайской 16000 по умолчанию контура —
# и получил ДВА обрыва `finish=length` на первых шести клетках `Z8R` ($0.0324 списано, текста
# нет). У DeepSeek размышление тарифицируется ВНУТРИ бюджета, а обогащённый черновик длиннее.
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 32000,
"onepass": "A2", "crit": "A3", "wire": lambda: C.configure(PARA.ZH),
"units": lambda: C.units_ok()},
"en": {"para": lambda: PARA.EN, "letter": "j", "phase": "ФД-J", "max_out": 32000,
@ -111,14 +115,27 @@ FREE = ("ZD", "Z0", "ZP")
# просто перепишет как обычно, а потерянная находка не вернётся ничем.
# Поэтому строка выбрасывается, ТОЛЬКО если (а) принимающая формула стоит в ХВОСТЕ вердикта —
# то есть критик закончил согласием, — И (б) во всей строке нет ни одного маркера дефекта.
# ⚠⚠ ПОЧИНКА ПО АДВЕРСАРИАЛЬНОМУ РЕВЬЮ (другое модельное семейство, 16.08). Прежняя редакция
# матчила принимающие формулы ВНУТРИ отрицательных слов: «неправильно», «некорректно»,
# «недопустимо», «неуместно», «неадекватно», «неточно передан» опознавались как СОГЛАСИЕ, потому
# что `правильно`/`корректн`/`допустим`/`уместн`/`адекватн` — их подстроки, а в списке маркеров
# дефекта этих форм не было. Замер по живому корпусу (1669 строк критика обеих пар): **10 реальных
# находок ложно отсеяны**, среди них «недопустимое смешение языков» и «неправильное употребление
# предлога». Ставка владельца покупалась бы по спискам, из которых выпали дефекты.
# Лечение: отрицательная приставка ловится ЯВНО и стоит в маркерах дефекта, а принимающие формулы
# требуют границы слова слева, чтобы не цепляться к «не-».
CONFIRM_RX = re.compile(
r"(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|ошибки\s+нет|"
r"претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|передано\s+верно|"
r"соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)", re.IGNORECASE)
r"(?<![а-яё])(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|"
r"ошибки\s+нет|претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|"
r"передано\s+верно|соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)",
re.IGNORECASE)
DEFECT_RX = re.compile(
r"(искаж|потер|неверн|ошибк|кальк|канцеляр|пропущ|инверт|рассоглас|подмен|выдум|отрицан|"
r"не\s+соответств|лучше\b|следует\b|надо\b|стоит\b|заменить|исправ|сух|тяжел|громозд|"
r"буквализм|подстрочник|не\s+переда|утрач)", re.IGNORECASE)
r"буквализм|подстрочник|не\s+переда|утрач|"
# отрицательные формы принимающих слов — они и есть находки, а не согласия
r"неправильн|некорректн|недопустим|неуместн|неадекватн|неточн|неверо|не\s+вполне|"
r"не\s+совсем|сомнительн|спорн)", re.IGNORECASE)
CONFIRM_TAIL = 70 # знаков хвоста вердикта, где принимающая формула что-то значит
@ -280,10 +297,18 @@ Z1_HEAD = (
def z1_msgs(p: str, src: str, draft: str, notes: list[str]) -> list[dict]:
"""⚠ ПОЧИНКА ПО РЕВЬЮ: замечания дописываются в ПОСЛЕДНЕЕ сообщение с ролью `user`, а не в
`m[1]`. При непустом банке `editor_msgs` вставляет ВТОРЫМ сообщением закон-блок (роль
`system`), и замечания критика уезжали в системный промт то есть структура запроса Z1
различалась между единицами, где термы банка есть и где их нет. Селфтест этого не ловил,
потому что проверял ИНДЕКС, а не роль: `m[1]` существует всегда."""
key = _pair(p)["para"]().key
bf = None if key == "zh-ru" else _pair(p)["para"]().bank_file
m = PR.editor_msgs(src, draft, PR.law_block(PR.terms_in(src, bf), bf), pair=key)
m[1]["content"] = m[1]["content"] + Z1_HEAD + "\n".join(f"- {n}" for n in notes)
if m[-1].get("role") != "user":
raise SystemExit(f"⛔ последнее сообщение редактора не `user` ({m[-1].get('role')}) — "
"замечания критика уехали бы в системный промт, СТОП")
m[-1]["content"] = m[-1]["content"] + Z1_HEAD + "\n".join(f"- {n}" for n in notes)
return m
@ -320,13 +345,14 @@ def chosen(p: str) -> list[dict]:
# ── ГЕЙТ ПЕРЕЛОМА ЦЕНЫ ────────────────────────────────────────────────────────────────────────
def price_gate() -> None:
def price_gate(quiet: bool = False) -> None:
now = dt.datetime.now(dt.timezone.utc)
pin = ROSTER.CANDIDATES[EDITOR][2]
stale = abs(pin - JULY_PIN_PRO) < 1e-9
left = (PRICE_CUTOVER - now).total_seconds() / 3600
if now < PRICE_CUTOVER:
print(f" цена DeepSeek: до перелома {left:.1f} ч, июльский пин ${pin}/1M ВЕРЕН")
if not quiet:
print(f" цена DeepSeek: до перелома {left:.1f} ч, июльский пин ${pin}/1M ВЕРЕН")
return
if stale:
raise SystemExit(
@ -348,20 +374,88 @@ def _buy(led, tg: str, model: str, msgs: list[dict], max_out: int, **extra) -> N
" Это СТОП и вопрос владельцу: назвать недостающую сумму.")
def preflight(models: list[str], phase: str, letter: str) -> None:
"""Пред-полётная проба: КАЖДАЯ модель прогона отвечает, ДО того как пойдёт цикл покупок.
ЗАВЕДЕНО ПО ИНЦИДЕНТУ 16.08, И ЭТО ДЫРА В ИНФРЕ, А НЕ ПЕДАНТИЗМ. Заход ушёл в цикл на 160
клеток, не проверив ничего; на 62-й клетке у провайдера кончился баланс, и оставшиеся 96
запросов ушли в `402 Insufficient Balance` по 0.9 с каждый, с записью файла на диск. Денег
это не стоило (отказ бесплатен), но выглядело как «купили 80 клеток»: файлы есть, годного
содержимого нет. Ровно тот класс, о котором предупреждает хендофф «файл клетки годная
клетка», и он стоил мне ложного доклада владельцу.
ПРОБА ИДЁТ ЧЕРЕЗ КАССУ (починка по ревью): первая редакция звала клиента напрямую, то есть
делала ПЛАТНЫЙ вызов мимо леджера прямое нарушение нормы «все платные вызовы через кассу,
два пути счёта». Копейки, но именно так и заводится расход, которого касса не видит. Тег
пробы несёт порядковый номер, иначе кэш `purchase` вернул бы прошлую запись и гейт перестал
бы что-либо проверять на втором прогоне.
"""
led = MONEY.Guarded(phase, default_expect=0.001)
for m in dict.fromkeys(models):
k = 1
while (OUT / f"dv-{letter}-probe-{m}-{k}.json").exists():
k += 1
tg = f"dv-{letter}-probe-{m}-{k}"
rec = MONEY.purchase(led, tg, m, ED22.client(m),
ROSTER.call_kwargs(m, [{"role": "user", "content": "ок"}],
max_out=8000),
role="preflight")
if rec.get("finish") != "stop":
raise SystemExit(
f"⛔ ПРЕД-ПОЛЁТНАЯ ПРОБА НЕ ПРОШЛА: {m} → finish={rec.get('finish')} "
f"{str(rec.get('error'))[:200]}\n"
" Покупка НЕ запускается. Цикл на сотню клеток против неотвечающего\n"
" провайдера пишет файлы без содержимого и выглядит как успешный прогон.")
print(f" проба {m}: отвечает, ${rec.get('cost_usd', 0):.6f} (учтено кассой)")
def refill(p: str, which: list[str]) -> int:
"""Увести оборванные клетки (`finish=length`, пусто) в карантин, чтобы их можно было ПЕРЕ-купить.
ЗАВЕДЕНО ПО РЕВЬЮ: без этого оборванная клетка мёртвая зона. Кэш `purchase` смотрит на
СУЩЕСТВОВАНИЕ файла и возвращает старую запись, а `cell()` требует `finish=stop` и отдаёт
пусто; значит смета считает клетку в «надо», а покупка на ней вечный бесшумный no-op.
Файл не удаляется, а переименовывается: деньги за него списаны и обязаны остаться видимы
кассе (глоб считает `dv-*.json`, карантинное имя под него попадает).
"""
n = 0
for arm in which:
for u in chosen(p):
f = OUT / f"{tag(p, arm, u['uid'])}.json"
if not f.exists():
continue
rec = json.loads(f.read_text(encoding="utf-8"))
if rec.get("finish") == "length" and not (rec.get("content") or "").strip():
k = 1
while (OUT / f"{f.stem}.LENGTH{k}.json").exists():
k += 1
f.rename(OUT / f"{f.stem}.LENGTH{k}.json")
print(f" {f.stem}: обрыв на потолке вывода → карантин, клетка пере-покупаема")
n += 1
return n
def buy(p: str, which: list[str]) -> None:
price_gate()
cfg = _pair(p)
cfg["wire"]()
preflight([DRAFT_MODEL if a == "Z8" else EDITOR for a in which],
cfg["phase"], cfg["letter"])
us = chosen(p)
led = MONEY.Guarded(cfg["phase"], default_expect=0.009)
mo = cfg["max_out"]
print(f"\nпара {p} · единиц {len(us)} · армы {' '.join(which)} · касса {cfg['phase']}")
print(f" оборванных клеток уведено в карантин: {refill(p, which)}")
for arm in which:
for u in us:
uid, src = u["uid"], u["source"]
tg = tag(p, arm, uid)
if cell(tg).strip():
continue
# ⚠ ГЕЙТ ПЕРЕЛОМА ЦЕНЫ — ПЕРЕД КАЖДОЙ КЛЕТКОЙ, А НЕ ОДИН РАЗ НА СТАРТЕ (починка по
# ревью): прогон длится часами, и пересёкший 16:00 UTC цикл писал бы пост-переломные
# вызовы по июльскому прайсу — леджер занижен втрое ровно на том куске, который куплен
# после перелома.
price_gate(quiet=True)
if arm == "Z8":
# штатный гейт годности + ОДИН ре-ген: эхо стохастично по вызову (квирки п.6)
for attempt in range(REGEN_MAX + 1):
@ -482,10 +576,20 @@ def cmd_selftest() -> int:
ed = editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"]))
ck(f"{p}: Z1 несёт системный промт боевого редактора ПОБАЙТНО",
z1[0]["content"] == ed[0]["content"])
ck(f"{p}: замечания критика уходят Z1 пользовательским сообщением",
z1[1]["content"].startswith(ed[1]["content"]) and len(z1[1]["content"]) > len(ed[1]["content"]))
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ", "ДОБАВЛЕНИЕ к твоей обычной работе"
in z1[1]["content"])
# ⚠ ПРОВЕРКА ПО РОЛИ, А НЕ ПО ИНДЕКСУ (починка по ревью). Прежняя редакция смотрела в
# `m[1]`, который при непустом банке есть закон-блок с ролью `system`: замечания критика
# уезжали в системный промт, а гейт этого не видел, потому что индекс существует всегда.
usr = [m for m in z1 if m["role"] == "user"]
edu = [m for m in ed if m["role"] == "user"]
ck(f"{p}: у Z1 ровно одно пользовательское сообщение", len(usr) == 1, f"их {len(usr)}")
ck(f"{p}: замечания критика уходят ПОЛЬЗОВАТЕЛЬСКИМ сообщением",
bool(usr) and bool(edu) and usr[-1]["content"].startswith(edu[-1]["content"])
and len(usr[-1]["content"]) > len(edu[-1]["content"]))
ck(f"{p}: в системный промт Z1 замечания НЕ попали",
not any("ДОБАВЛЕНИЕ к твоей обычной работе" in m["content"]
for m in z1 if m["role"] == "system"))
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ",
bool(usr) and "ДОБАВЛЕНИЕ к твоей обычной работе" in usr[-1]["content"])
# ZF обязан быть побайтной копией запроса Z0 — иначе это не пол, а другой арм
ck(f"{p}: ZF = ТОТ ЖЕ запрос, что у боевой связки (пол, а не новый арм)",
editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"])) == ed)
@ -517,26 +621,12 @@ def cmd_selftest() -> int:
return bad
def cmd_plan() -> None:
for p in PAIRS:
us = chosen(p)
print(f"\n=== {p}: {len(us)} единиц ===")
for a in FREE + ARMS:
have = sum(1 for u in us
if (free_text(p, a, u["uid"]) if a in FREE else cell(tag(p, a, u["uid"])))
.strip())
print(f" {a:4s} {have:2d}/{len(us)}" + (" (бесплатно)" if a in FREE else ""))
MONEY.report()
if __name__ == "__main__":
a = sys.argv[1:] or ["--dry"]
if a[0] == "--selftest":
sys.exit(1 if cmd_selftest() else 0)
elif a[0] == "--dry":
cmd_dry()
elif a[0] == "--plan":
cmd_plan()
elif a[0] == "--echo":
echo_report()
elif a[0] == "--buy":
@ -545,4 +635,4 @@ if __name__ == "__main__":
for x in pr:
buy(x, ar)
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --dry --selftest --plan --buy")
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --dry --selftest --buy --echo")

561
eval/dovodka/zsud.py Normal file
View file

@ -0,0 +1,561 @@
#!/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}")