#!/usr/bin/env python3 """ПЕРЕ-СУДЕЙСТВО ПРОХОДА `tier` ПОЧИНЕННЫМ ПРИБОРОМ. $0 (агент-сессии). Заказано владельцем 15.08: «Можно пересудить. Но не подгонять. Нужно понять, кто ошибается из судей». Пере-судятся ТЕ ЖЕ ТЕКСТЫ — ни одна клетка не покупается заново, поэтому разница между старым и новым замером есть разница ПРИБОРА, а не материала. ⚠ ЧТО ИМЕННО СЛОМАНО В СТАРОМ ЗАМЕРЕ (замерено, не предположено — `gain.py`): · пачка легла на пол шкалы: 38% клеток ровно ноль, из них 24% с ПУСТЫМ обоснованием; у арма `T2` нулей 75%, у боевого `R0` — 56%; в решающем контрасте 9 ничьих из 16, 7 из них на нуле; · тот же арм `R0` на тех же 16 единицах в проходе `border` дал 4.31 ошибки против 0.69 здесь (подписи текста совпадают 16/16) — прибор читал те же байты вшестеро глуше; · семейства не коррелируют по трудности единицы (r=−0.22) при +0.31…+0.70 на осях фазы Д; · разобранная руками единица `38c99e5efb`: боевой редактор превратил адресата угрозы в союзника («мы с тобой покараем его» против 我二人…除了你), судья поставил 0 с пустым обоснованием. ⚠ ТРИ ПРАВКИ ПАНЕЛИ — ПРЕ-РЕГИСТРИРУЮТСЯ ЗДЕСЬ, ДО ПЕРВОГО ОТВЕТА: 1. `CTRLfloorA` УБРАН как отдельная метка: он побайтно равен боевому арму `T1` в 16/16, то есть один текст лежал в пачке дважды. Пол считается как и раньше — по РАЗНОСТИ двух генераций того же лечения, но вторая метка теперь одна (`CTRLfloor` = прежний `CTRLfloorB`), а первой служит сам `T1`. Дублей в пачке не остаётся. 2. `CTRLmargin` ДОБАВЛЕН — маржевый декой (норма П-2), которого у прохода `tier` не было никогда. Без него «не различимо» неотличимо от слепоты прибора; именно этого контроля не хватило паку 23. 3. Ключ ВЫПУСКАЕТСЯ НОВЫЙ (`tier2-KEY.json`, своя соль), старый не трогается. Раскладка меток — функция соли, uid и НАБОРА армов; эмиссия поверх старого ключа переписала бы отсуженную раскладку (авария фазы Д, хендофф §5 п.1). ⚠ ТРИ ПРАВКИ ЗАДАНИЯ — ТОЖЕ ПРЕ-РЕГИСТРИРУЮТСЯ. Лечится ЗАДАНИЕМ, а не сменой судьи: прибор устойчив (побайтно один текст под двумя метками дал sd=0.000 по всем осям 16/16), у него высокий порог счёта, а не разболтанность. A. МОЛЧАЛИВЫЙ НОЛЬ ЗАПРЕЩЁН: клетка с нулём обязана нести непустое «почему». B. В рубрику ВЕРНОСТЬ внесены КЛАССЫ, которые старая пачка пропускала: инверсия адресата или участника реплики · потеря/подмена разряда, ранга, числа · состояние идиомы · снятое или добавленное отрицание. Классы арм-нейтральны и ни на один арм не указывают. C. Сказано прямо, что ноль — утверждение, а не отсутствие ответа. Всё прочее в задании побайтно то же, что судили агенты пака 23. ⚠ ПРАВИЛО РЕШЕНИЯ — ОБЪЯВЛЯЕТСЯ ДО ОТВЕТОВ: · пачка, не взявшая гейт плотности (`gain.py`: доля нулей ≥25%), НЕ несёт вывода «не различимо»; · семейство, чей перевес на маржевом декое не пересекает свой порог, теряет право говорить «не хуже» (П-2); · контрасты `T1/R0`, `T2/R0`, `T3/R0` — точный знаковый тест с поправкой Холма по трём; `R0/F` — позитивный контроль, в семейство не входит; · порог различимости = 2.8·sd/√n по разностям пар пола, как во всей фазе; · сравнение со старым замером печатается по-армно и является ГЛАВНЫМ результатом прогона: вопрос стоит не «кто лучше», а «что показывает прибор, когда ему запрещено молчать». Запуск: tier2.py --emit | --score | --selftest """ from __future__ import annotations 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)) ET = Path.home() / "books" / "editor-tier" OLD_KEY = ET / "blind-keys" / "tier-KEY.json" OLD_TASKS, OLD_ANSW = ET / "aj-tier-tasks", ET / "aj-tier" NEW_KEY = ET / "blind-keys" / "tier2-KEY.json" SALT_F = ET / "blind-keys" / "SALT-tier2.txt" TASKS, ANSW = ET / "tier2-tasks", ET / "tier2" AX = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА") CARRY = ("ВЕРНОСТЬ", "ЯЗЫК") ARMS = ("R0", "T1", "T2", "T3", "F") # боевые; контраст — T*/R0, контроль — R0/F CTRL = ("CTRLdecoy", "CTRLfloor", "CTRLmargin") PER_SESSION = 4 # заданий на сессию, как гонялся `border` SALT = "tier2-2026-08-15" # своя соль; старая не трогается _axre = re.compile(r"^[ТT](\d+)-(ВЕРНОСТЬ|ТЕРМИН|ЯЗЫК|ФОРМА):\s*(\d+)", re.MULTILINE) _whyre = re.compile(r"^[ТT](\d+)-ПОЧЕМУ:(.*)$", re.MULTILINE) 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 def _sud(): """`sud` грузится лениво: он разбирает флаги на уровне модуля и тянет сырьё своей оси.""" return sys.modules.get("sud") or _load("sud", ZONE / "sud.py") def old_texts() -> dict[str, dict[str, str]]: """(uid → {арм: текст}) из ОТСУЖЕННЫХ заданий пака 23. Тексты не покупаются заново.""" key = json.loads(OLD_KEY.read_text(encoding="utf-8")) out: dict[str, dict[str, str]] = {} for tok, km in key.items(): if not isinstance(km, dict): continue f = OLD_TASKS / f"{tok}.txt" if not f.exists(): continue body = f.read_text(encoding="utf-8") # варианты разделены строками вида «Т3:»; исходник идёт до первой такой строки parts = re.split(r"^(Т\d):\s*$", body, flags=re.MULTILINE) src = re.search(r"^ИСХОДНИК:\s*\n(.*?)(?=\n=+\n|\Z)", parts[0], re.DOTALL | re.MULTILINE) texts = {} for i in range(1, len(parts) - 1, 2): texts[parts[i]] = parts[i + 1].strip() for lab, meta in km.items(): if not isinstance(meta, dict) or lab not in texts: continue uid = meta["uid"] out.setdefault(uid, {})["_src"] = (src.group(1).strip() if src else "") out[uid][meta["arm"]] = texts[lab] return out def layout(uid: str, arms: tuple[str, ...]) -> dict[str, str]: """Метка → арм. Чистая функция от соли, uid и НАБОРА армов — как во всей дуге.""" order = sorted(arms) seed = hashlib.sha256(f"{SALT}|{uid}|{'|'.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 = """Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на китайском и {n} независимых русских перевода этого же фрагмента под слепыми метками. Посчитай ОШИБКИ в каждом варианте по типам: ВЕРНОСТЬ — сказано не то, что в исходнике: искажение, инверсия, выдумка, пропуск факта. Сюда же входят, и их пропускают чаще всего: · инверсия адресата или участника реплики (кто кому говорит, кто кого бьёт); · потеря или подмена разряда, ранга, числа, единицы измерения; · состояние идиомы или действия: совершилось против ещё не совершилось; · снятое или, наоборот, добавленное отрицание. ТЕРМИН — имя или термин передан не так, как в других местах того же текста. ЯЗЫК — то, чего носитель не напишет: калька, канцелярит, несуществующее слово, кривой порядок слов, рассогласование. ФОРМА — вёрстка и пунктуация: оформление реплик, кавычки, тире, абзацы. Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между собой. Порядок вариантов ничего не значит и специально перемешан. ⚠ НОЛЬ — ЭТО УТВЕРЖДЕНИЕ, А НЕ ОТСУТСТВИЕ ОТВЕТА. Ноль означает «я сверил этот вариант с исходником по этой оси и дефектов не нашёл». Поэтому ПОЧЕМУ обязательно для КАЖДОЙ метки, в том числе при нулях: если по всем осям ноль, напиши «прочитано, дефектов не найдено». Пустое ПОЧЕМУ не принимается и на диск не попадает. Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку: {skeleton} ПОЧЕМУ для НЕНУЛЕВОЙ оси: короткая ЦИТАТА из текста через « | ». Число без цитаты не принимается и на диск не попадает: именно из-за отсутствия цитат показания прошлых судей оказались непроверяемыми. """ def emit() -> None: S = _sud() src = old_texts() TASKS.mkdir(parents=True, exist_ok=True) ANSW.mkdir(parents=True, exist_ok=True) NEW_KEY.parent.mkdir(parents=True, exist_ok=True) SALT_F.write_text(SALT + "\n", encoding="utf-8") key: dict[str, dict] = { "_проход": "tier2", "_семейство": ["T1/R0", "T2/R0", "T3/R0"], "_контроль": ["R0/F"], "_армы": list(ARMS) + list(CTRL), } made = 0 for uid, arms in sorted(src.items()): if not all(arms.get(a, "").strip() for a in ARMS): print(f" ⚠ {uid}: не хватает армов, единица пропущена") continue texts = {a: arms[a] for a in ARMS} texts["CTRLdecoy"] = arms.get("CTRLdecoy", "") texts["CTRLfloor"] = arms.get("CTRLfloorB", "") planted, n_pl = S.margin_plant(arms["R0"]) texts["CTRLmargin"] = planted if n_pl >= 2 else "" panel = tuple(a for a in (*ARMS, *CTRL) if texts.get(a, "").strip()) lay = layout(uid, panel) tok = hashlib.sha256(f"{SALT}|{uid}".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(n=len(lay), path=ANSW / f"{tok}.txt", skeleton=skel), "", "=" * 60, "ИСХОДНИК:", arms.get("_src", "")] for lab, arm in lay.items(): body += ["", "=" * 60, f"{lab}:", texts[arm]] (TASKS / f"{tok}.txt").write_text("\n".join(body), encoding="utf-8") key[tok] = {lab: {"arm": a, "uid": uid, "kind": "контроль" if a.startswith("CTRL") else "боевой", "sig": hashlib.sha256(texts[a].encode()).hexdigest()[:12]} for lab, a in lay.items()} made += 1 NEW_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8") toks = [t for t in key if not t.startswith("_")] print(f"заданий {made} → {TASKS}") print(f"ключ → {NEW_KEY} (судье НЕ давать); соль → {SALT_F}") print(f"сессий по {PER_SESSION} задания: {-(-len(toks) // PER_SESSION)}") for i in range(0, len(toks), PER_SESSION): print(f" сессия {i // PER_SESSION + 1}: " + " · ".join(toks[i:i + PER_SESSION])) def read(dirpath: Path, keypath: Path) -> tuple[dict, list]: """{(uid, арм): сумма несущих осей} + список замечаний годности.""" key = json.loads(keypath.read_text(encoding="utf-8")) out, bad = {}, [] for f in sorted(dirpath.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]] = {} for m in _axre.finditer(t): cells.setdefault("Т" + 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}: ПУСТОЕ обоснование (запрещено заданием)") out[(meta["uid"], meta["arm"])] = sum(ax.get(a, 0) for a in CARRY) 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) c = lambda a, b: math.comb(a, b) tail = sum(c(n, i) for i in range(min(k, n - k) + 1)) / 2 ** n return min(1.0, 2 * tail) def score() -> int: if not NEW_KEY.exists(): print("ключа нет — сначала --emit") return 1 new, bad = read(ANSW, NEW_KEY) old, _ = read(OLD_ANSW, OLD_KEY) if not new: print(f"ответов в {ANSW} нет") return 1 uids = sorted({u for u, _ in new}) print(f"единиц отсужено: {len(uids)} · клеток {len(new)} · замечаний годности {len(bad)}") for b in bad[:8]: print(" ⚠", b) vals = [v for (u, a), v in new.items() if not a.startswith("CTRL")] zeros = sum(1 for v in vals if v == 0) / max(len(vals), 1) print(f"\nГЕЙТ ПЛОТНОСТИ: ошибок/клетку {st.mean(vals):.2f} · доля нулей {zeros:.0%} " f"→ {'ГОДНО' if zeros < 0.25 else '⛔ ПОЛ ШКАЛЫ, вывод «не различимо» не принимается'}") dec = [new[(u, "CTRLdecoy")] - new[(u, "R0")] for u in uids if (u, "CTRLdecoy") in new and (u, "R0") in new] print(f"ГРУБЫЙ ДЕКОЙ: пойман {sum(1 for d in dec if d > 0)}/{len(dec)} · медиана +{st.median(dec) if dec else 0:.1f}") fl = [new[(u, "T1")] - new[(u, "CTRLfloor")] for u in uids if (u, "T1") in new and (u, "CTRLfloor") in new] 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)} · sd {sd:.2f} → ПОРОГ РАЗЛИЧИМОСТИ {thr:.2f}") mg = [new[(u, "CTRLmargin")] - new[(u, "R0")] for u in uids if (u, "CTRLmargin") in new and (u, "R0") in new] mgm = st.mean(mg) if mg else 0.0 ok_m = mgm > thr print(f"МАРЖЕВЫЙ ДЕКОЙ (гейт чувствительности П-2): n={len(mg)} среднее {mgm:+.2f} " f"против порога {thr:.2f} → {'ПРОЙДЕН' if ok_m else '⛔ НЕ ПРОЙДЕН — права на «не хуже» нет'}") print(f"\n{'контраст':12s}{'ед.':>5s}{'перевес':>9s}{'p':>9s}{'p Холма':>10s} вердикт") ps = {} for a in ("T1", "T2", "T3"): d = [new[(u, "R0")] - new[(u, a)] for u in uids if (u, a) in new and (u, "R0") in new] if d: ps[a] = (_sign_p(d), st.mean(d), len(d)) for i, (a, (p, m, n)) in enumerate(sorted(ps.items(), key=lambda x: x[1][0])): holm = min(1.0, p * (len(ps) - i)) v = "ПЕРЕШЁЛ ПОРОГ" if abs(m) > thr and holm < 0.05 else "ниже порога" print(f"{a + ' vs R0':12s}{n:5d}{m:+9.2f}{p:9.4f}{holm:10.4f} {v}") d = [new[(u, "R0")] - new[(u, "F")] for u in uids if (u, "F") in new and (u, "R0") in new] if d: print(f"{'R0 vs F':12s}{len(d):5d}{st.mean(d):+9.2f}{_sign_p(d):9.4f}{'—':>10s} позитивный контроль") print(f"\n{'арм':12s}{'СТАРЫЙ замер':>14s}{'НОВЫЙ замер':>13s}{'сдвиг':>8s} ← главный результат") for a in (*ARMS, *CTRL): o = [old[(u, a)] for u in uids if (u, a) in old] n_ = [new[(u, a)] for u in uids if (u, a) in new] if not n_: continue om = st.mean(o) if o else float("nan") print(f"{a:12s}{om:14.2f}{st.mean(n_):13.2f}{st.mean(n_) - om:+8.2f}") print("\n⚠ Тексты не менялись ни в одном арме. Любой сдвиг здесь — свойство ПРИБОРА.") return 0 def selftest() -> int: fails = [] ok = lambda c, m: fails.append(m) if not c else None src = old_texts() ok(len(src) == 16, f"старых единиц разобрано {len(src)}, ожидалось 16") if src: u0 = next(iter(src)) ok(all(src[u0].get(a, "").strip() for a in ARMS), f"{u0}: не все боевые армы извлеклись") ok(len(src[u0].get("_src", "")) > 200, f"{u0}: исходник не извлёкся") # дублей в новой панели быть не должно S = _sud() t = {a: src[u0][a] for a in ARMS} t["CTRLfloor"] = src[u0].get("CTRLfloorB", "") t["CTRLmargin"] = S.margin_plant(src[u0]["R0"])[0] sigs = [hashlib.sha256(v.encode()).hexdigest() for v in t.values() if v.strip()] ok(len(sigs) == len(set(sigs)), f"{u0}: в новой панели остались побайтные дубли") # старая панель дубли содержала — проверка, что мы чиним реальную болезнь old_pair = src[u0].get("CTRLfloorA", "") == src[u0].get("T1", "") ok(old_pair, f"{u0}: ожидалось, что старый CTRLfloorA ≡ T1 (дефект пака 23)") lay = layout(u0, tuple(a for a in (*ARMS, *CTRL) if t.get(a, "").strip())) ok(len(lay) == len(set(lay.values())), "раскладка меток не биективна") ok(layout(u0, tuple(sorted(lay.values()))) == lay, "раскладка не воспроизводима") ok(_sign_p([1, 1, 1, 1, 1]) < 0.07, "знаковый тест: пять единиц в одну сторону должны дать p<0.07") ok(_sign_p([0, 0, 0]) == 1.0, "знаковый тест: одни ничьи должны дать p=1") for m in fails: print("ПРОВАЛ:", m) print(f"селфтест tier2: провалов {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()) elif a[0] == "--selftest": sys.exit(selftest()) else: raise SystemExit(f"⛔ неизвестный режим {a[0]!r}")