#!/usr/bin/env python3 """ИТОГ ЭКСПА ОДНОЙ КОМАНДОЙ. Пере-считывает КАЖДОЕ число блока «ИТОГ» отчёта из сырья. $0. ЗАЧЕМ ЗАВЕДЁН. Два независимых аудита 08.08 (снизу вверх и сверху вниз) сошлись на одном дефекте оснастки: несущие числа отчёта — нога 2×2, контрасты Ф2б, покрытие канона, отборщики арма G — **в дереве пути воспроизведения не имели**. `absjudge --score` печатает семейство Ф2а и на узком проходе 4 выдаёт пустую таблицу (в `FAMILY` нет контрастов C2/E2); `verify_report` сторожил таблицы §2 второй редакции, деньги и батарею — и ни одного числа ИТОГа. То есть решающий слой отчёта стоял без стража, а часть чисел бралась из stdout прогонов, утраченных инцидентом 08.08. Это ровно тот класс дефекта, за который снята первая редакция, только для чисел, а не для денег: число, которое нельзя пере-снять командой из дерева, ничем не отличается от числа по памяти. ЧТО ЗДЕСЬ НЕ ДЕЛАЕТСЯ. Ничего не покупается и не судится: только счёт по персистированным голосам и артефактам. Каждый блок печатает провенанс — из какого прохода и по какой формуле взято, — и явно метит числа, которые в дереве не выводятся ни одним способом. Запуск: eval/.venv/bin/python eval/role_topology/itog.py """ from __future__ import annotations import json import re import statistics as st import sys from bisect import bisect_left, bisect_right from itertools import combinations from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "role_topology")) sys.path.insert(0, str(REPO / "eval" / "editor_harness")) import absjudge as AJ # noqa: E402 import bakeoff as BO # noqa: E402 import guarded as G # noqa: E402 import phase2b as P2 # noqa: E402 OUT = Path.home() / "books" / "role-topology" def _E(d: dict, u: str, k: str) -> float: return d[u][k]["errors"] def _passes() -> tuple[dict, dict, list[str]]: a1, a2 = AJ._by_unit_pass("abs"), AJ._by_unit_pass("abs2") return a1, a2, sorted(set(a1) & set(a2)) def _pooled(a1: dict, a2: dict, common: list[str], a: str, b: str) -> list[float]: """Перевес за арм `a` в пуле ДВУХ проходов: сначала среднее по проходам ВНУТРИ единицы. ⚠ Именно так, а не «средние по проходам вычесть»: пул усредняет перевес поединично, поэтому разница СТРОГОСТИ панелей сокращается вычитанием внутри единицы и в контраст не попадает. Уровни между панелями несопоставимы (abs 6.90 → abs2 7.92 → abs3 8.46 → abs4 9.22 ошибки на клетку), сопоставимы только разности — на этом стоит весь ИТОГ. """ return [st.mean([_E(a1, u, b) - _E(a1, u, a), _E(a2, u, b) - _E(a2, u, a)]) for u in common if a in a1[u] and b in a1[u] and a in a2[u] and b in a2[u]] def _holm(ps: list[float]) -> list[float]: order = sorted(range(len(ps)), key=lambda i: ps[i]) out, run = [0.0] * len(ps), 0.0 for rank, i in enumerate(order): run = max(run, min(1.0, ps[i] * (len(ps) - rank))) out[i] = run return out def p1_leg() -> None: print("═══ ИТОГ п.1 — НОГА 2×2 (пул проходов abs+abs2, перевес против черновика F3) ═══") a1, a2, common = _passes() print(f"единиц в обоих проходах: {len(common)}\n") print(f"{'арм':6s}{'n':>4s}{'перевес':>10s}{'95% ДИ':>19s}{'p':>10s}{'побед':>8s}") for a in ("A", "B", "D_", "D"): m = _pooled(a1, a2, common, a, "F3") lo, hi = BO.boot_ci(m) print(f"{a:6s}{len(m):4d}{st.mean(m):+10.2f} [{lo:+.2f}, {hi:+.2f}]" f"{BO.sign_perm_p(m):10.4f}{sum(1 for x in m if x > 0):5d}/{len(m)}") print("\n⚠ ПОСТ-ХОК. `D_ vs F3` и `D vs F3` в объявленном `absjudge.FAMILY` ОТСУТСТВУЮТ: нога") print(" собрана аудитом из того же сырья уже после чтения чисел. Поправку Холма они не несут") print(" и читаются как разведка; несущие контрасты семейства печатает `absjudge --pool`.") m = _pooled(a1, a2, common, "A_law", "F3") print(f"\nсправочно `A_law vs F3` (боевой конфиг редактора): {st.mean(m):+.2f}" f" · по проходам {st.mean([_E(a1, u, 'F3') - _E(a1, u, 'A_law') for u in common]):+.2f}" f" / {st.mean([_E(a2, u, 'F3') - _E(a2, u, 'A_law') for u in common]):+.2f}") def p1_robust() -> None: """Чем несущие контрасты пробовали убить: L2O, кластеризация по главе, строгость панелей. ⚠ Заведено 08.08. До этого клеймы отчёта «0/120 при удалении любых ДВУХ единиц» и «кластеризация по главе выживает» СЧИТАЛИСЬ ВНЕ ДЕРЕВА и в репозитории пути не имели — тот же класс дефекта, ради которого написан этот модуль. """ print("\n═══ ИТОГ п.1 — ЧЕМ НЕСУЩИЕ КОНТРАСТЫ ПРОБОВАЛИ УБИТЬ ═══") a1, a2, common = _passes() core = [("A", "F3"), ("B", "F3"), ("D", "A")] k_fam = len(AJ.FAMILY) print(" LEAVE-TWO-OUT (удаляются любые ДВЕ единицы, Холм по семи гипотезам):") for a, b in core: m = _pooled(a1, a2, common, a, b) bad = sum(1 for c in combinations(range(len(m)), 2) if BO.sign_perm_p([x for i, x in enumerate(m) if i not in c]) * k_fam >= 0.05) print(f" {a + ' vs ' + b:10s} вылетов {bad}/{len(list(combinations(range(len(m)), 2)))}") # КЛАСТЕРИЗАЦИЯ. 16 единиц происходят не из 16 разных глав: часть глав дала по два чанка, # и такие единицы НЕ независимы. Тест берёт средний перевес ПО ГЛАВЕ и работает на главах. us = {u["uid"]: u["chapter"] for u in BO.units()} chapters = sorted({us[u] for u in common}) print(f"\n КЛАСТЕРИЗАЦИЯ ПО ГЛАВЕ ИСТОЧНИКА: {len(common)} единиц из {len(chapters)} глав " f"({sum(1 for c in chapters if sum(1 for u in common if us[u] == c) > 1)} глав дали >1 чанка)") ps = [] for a, b in core: m = dict(zip(common, _pooled(a1, a2, common, a, b), strict=True)) by = [st.mean([m[u] for u in common if us[u] == c]) for c in chapters] ps.append(BO.sign_perm_p(by)) for (a, b), p, h in zip(core, ps, _holm(ps + [1.0] * (k_fam - len(ps))), strict=False): print(f" {a + ' vs ' + b:10s} на главах p={p:.4f} · Холм {h:.4f}" f"{' ✔ выживает' if h < 0.05 else ''}") print("\n СТРОГОСТЬ ПАНЕЛЕЙ (ошибок на клетку) — уровни между проходами НЕСОПОСТАВИМЫ:") for nm in ("abs", "abs2", "abs3", "abs4"): d = AJ._by_unit_pass(nm) cells = [r["errors"] for v in d.values() for r in v.values()] if cells: print(f" {nm:5s} {st.mean(cells):.2f} на {len(cells)} клетках") print(" ⇒ поэтому ИТОГ сравнивает ТОЛЬКО внутриединичные разности, а не уровни.") def p2_canon() -> None: print("\n═══ ИТОГ п.2 — ПОКРЫТИЕ КАНОНА (счётная метрика, окна) ═══") us = BO.units() print(f"{'арм':7s}{'канон/исходник':>16s}{'покрытие':>10s}") for a in ("F3", "A", "A_law", "B", "D", "D_", "C2", "E2"): s = k = 0 for u in us: x = P2.text_of(a, u["uid"]) if a in ("C2", "E2") else BO.text3_of(a, u) for zh, (_v1, _v2, r1, _r2) in BO.IP.TERMS.items(): n = u["source"].count(zh) if n: s += n k += min(len(re.findall(r1, x, re.I)), n) print(f"{a:7s}{f'{k}/{s}':>16s}{k / s if s else 0:>10.3f}") print(" доля упоминаний исходника, отражённых КАНОННОЙ формой; s — знаменатель на 16 единиц") def p3_draft() -> None: print("\n═══ ИТОГ п.3 — СВОЙ ЧЕРНОВИК ПРОТИВ КОРПУСНОГО (тот же редактор `A`) ═══") a1, a2, common = _passes() own = {u: st.mean([_E(a1, u, "A"), _E(a2, u, "A")]) for u in common} for lbl, cells in (("только CTRLfloorA (клетка ИТОГа)", ("CTRLfloorA",)), ("обе клетки пола CTRLfloorA+B", ("CTRLfloorA", "CTRLfloorB"))): corp, diff = {}, [] for u in common: vals = [_E(d, u, c) for d in (a1, a2) for c in cells if c in d[u]] if not vals: continue corp[u] = st.mean(vals) diff.append(corp[u] - own[u]) print(f" {lbl:34s} корпусный {st.mean(list(corp.values())):.2f} против своего " f"{st.mean([own[u] for u in corp]):.2f} · хуже " f"{sum(1 for x in diff if x > 0)}/{len(diff)} · p={BO.sign_perm_p(diff):.4f}") print(" клетки: CTRLfloorA=`bo2-A` · CTRLfloorB=`bo3-Aprime` (повтор того же корпусного входа)") print(" ⚠ ИТОГ печатает вариант ПО ОДНОЙ клетке — он в большую сторону; вердикт одинаков.") def p4_axes() -> None: print("\n═══ ИТОГ п.4 — ОДНОПРОХОДКА С МАНДАТОМ ПРОТИВ СВЯЗКИ, разложение по осям (пул) ═══") a1, a2, common = _passes() import judges as J # noqa: PLC0415 for a, b in (("D", "A"), ("D_", "A")): row = [] for ax in J.AXES: m = [st.mean([a1[u][b][ax] - a1[u][a][ax], a2[u][b][ax] - a2[u][a][ax]]) for u in common if a in a1[u] and b in a1[u]] row.append(f"{ax} {st.mean(m):+.2f}") tot = _pooled(a1, a2, common, a, b) lo, hi = BO.boot_ci(tot) print(f" {a} vs {b}: {st.mean(tot):+.2f} [{lo:+.2f}, {hi:+.2f}] " f"p={BO.sign_perm_p(tot):.4f} · " + " · ".join(row)) print(" ⚠ Оси разведочны: `absjudge --score` корректирует на 7 гипотез, здесь клеток 28.") def _qe() -> dict: f = OUT / "qe-arms.json" return json.loads(f.read_text(encoding="utf-8")) if f.exists() else {} def _pearson(xs: list[float], ys: list[float]) -> float: mx, my = st.mean(xs), st.mean(ys) num = sum((x - mx) * (y - my) for x, y in zip(xs, ys, strict=True)) den = (sum((x - mx) ** 2 for x in xs) * sum((y - my) ** 2 for y in ys)) ** 0.5 return num / den if den else float("nan") def _route() -> tuple[dict, list[str], list[str]]: """Кого арм G маршрутизировал и чью правку страж ОСТАВИЛ. Ровно логика `guarded.cmd_build`. ⚠ Выигрыш забирают ТОЛЬКО оставленные окна: там, где страж откатил правку к черновику, арм G получает счёт черновика, то есть ноль выигрыша. Первая версия этого счёта брала все маршрутизированные и завышала долю до 46% — поймано сверкой с `--build` 08.08. """ a1, a2, common = _passes() gain = dict(zip(common, _pooled(a1, a2, common, "A", "F3"), strict=True)) routed, kept = [], [] for u in BO.units(): uid = u["uid"] if uid not in gain: continue draft, edited = BO.text3_of("F3", u), BO.text3_of("A", u) if not G.flagged(draft): continue routed.append(uid) if sum(G.flags(edited).values()) <= sum(G.flags(draft).values()): kept.append(uid) return gain, routed, kept def _draft_cost(uid: str) -> float: return sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd") or 0.0 for f in OUT.glob(f"bo2-DRAFT-{uid}*.json")) def _cost_a(uid: str) -> float: return _draft_cost(uid) + G.cost_of("bo4-A", uid) def _cost_g(uid: str) -> float: """Цена арма G: черновик всегда, редактор — только флагнутым окнам.""" u = next(x for x in BO.units() if x["uid"] == uid) return _cost_a(uid) if G.flagged(BO.text3_of("F3", u)) else _draft_cost(uid) def p5_router() -> None: print("\n═══ ИТОГ п.5 — РОУТИНГ: ТРИ ОТБОРЩИКА ═══") a1, a2, common = _passes() gain, routed, picked = _route() tot = sum(gain.values()) got = sum(gain[u] for u in picked) print(f" МЕХАНИЧЕСКИЙ отборщик: маршрутизировал {len(routed)}/{len(gain)} окон, страж оставил" f" правку в {len(picked)} ⇒ забрано {got:.1f} из {tot:.1f} выигрыша = {got / tot:.0%}") sums = sorted(sum(gain[u] for u in c) for c in combinations(gain, len(picked))) lo, hi = bisect_left(sums, got) / len(sums), bisect_right(sums, got) / len(sums) cg = st.mean([_cost_g(u) for u in gain]) ca = st.mean([_cost_a(u) for u in gain]) print(f" цена: G ${cg:.5f} против связки ${ca:.5f} = {cg / ca:.0%} цены за {got / tot:.0%}" f" выигрыша ⇒ хуже случайного роутинга того же бюджета") print(f" среди всех C({len(gain)},{len(picked)})={len(sums)} подмножеств того же размера: " f"персентиль {lo:.1%}–{hi:.1%} (середина {(lo + hi) / 2:.1%}), связок {sums.count(got)}") print(" ⚠ ИТОГ печатает верхнюю границу (51-й); расхождение с 46-м — только конвенция связок.") q = _qe() if q: agree = tot_n = 0 for a, b, _w in AJ.FAMILY: for u in common: m = _pooled(a1, a2, [u], a, b) if not m or m[0] == 0: continue dq = q.get(f"{b}|{u}", 0) - q.get(f"{a}|{u}", 0) tot_n += 1 agree += (dq > 0) == (m[0] > 0) print(f" QE как СУДЬЯ: знак совпал {agree}/{tot_n} по 7 контрастам семейства × единицы") xs = [q.get(f"F3|{u}", 0) - q.get(f"A|{u}", 0) for u in common] ys = [gain[u] for u in common] print(f" QE против судьи на самом крупном контрасте `A/F3`: r={_pearson(xs, ys):+.2f}") print(" ⚠ Это АНТИкорреляция, а не слепота: чем сильнее QE «за» редактуру, тем меньше") print(" её выигрыш по судье. Довод против QE-роутинга сильнее, чем печатал ИТОГ.") print(" ⚠ Числа ИТОГа «43/91» и «r=+0.06» ни одним вариантом счёта не воспроизводятся —") print(" они из §15, утраченного инцидентом 08.08; в дереве кода пути к ним НЕТ.") def p6_guard() -> None: print("\n═══ ИТОГ п.6 — СТРАЖ guarded APE ═══") gain, routed, kept = _route() rej = [u for u in routed if u not in kept] print(f" страж ОТКЛОНИЛ правок: {len(rej)} — " + " · ".join(f"{u[:10]} выигрыш {gain[u]:+.1f}" for u in rej)) if rej: print(f" средний выигрыш ОТКЛОНЁННЫХ {st.mean([gain[u] for u in rej]):+.2f} против " f"{st.mean(list(gain.values())):+.2f} по ВСЕМ единицам ⇒ страж вырезал ЛУЧШИЕ") print(f" ⚠ n={len(rej)}. ИТОГ числа единиц не называл; вывод стоит на двух правках.") def p7_2b() -> None: print("\n═══ ИТОГ п.7 — АРМЫ Ф2б (проход abs4, узкая панель) ═══") sv = AJ.PASS AJ.PASS = "abs4" try: d = AJ._by_unit() finally: AJ.PASS = sv # СЕМЕЙСТВО ПРОХОДА 4 — пять СУДЕЙСКИХ контрастов. # ⚠ ОТКЛОНЕНИЕ ОТ ОБЪЯВЛЕННОГО, объявляю: `phase2b.FAMILY` пятым членом называет # `C_oracle vs C_real` — разрыв потолка и пола схемы. Он НЕ судейский: потолок меряется # посаженными дефектами и детекторами ($0, `phase2b --oracle`), судье не предъявлялся и # p-значения не имеет. В судейскую пятёрку вместо него вошёл контроль `A_law vs F3`. # Направление подмены КОНСЕРВАТИВНО (член семейства сохранён, поправка не ослаблена), но это # правка семейства после пре-рега, и читать её надо как девиацию, а не как замысел. fam = [("C2", "A_law", "точечный ремонт против полного переписывания"), ("C2", "F3", "и лучше ли он черновика, который чинит"), ("E2", "D_", "обратная связка против своей базы"), ("E2", "A_law", "обратная связка против прямой"), ("A_law", "F3", "КОНТРОЛЬ: воспроизводится ли эффект редактора")] rows = [] for a, b, w in fam: m = [d[u][b]["errors"] - d[u][a]["errors"] for u in d if a in d[u] and b in d[u]] rows.append((a, b, w, m, BO.sign_perm_p(m))) holm = _holm([r[4] for r in rows]) print(f"{'контраст':16s}{'n':>3s}{'перевес':>9s}{'95% ДИ':>19s}{'p':>9s}{'p Холма':>10s}") for (a, b, w, m, p), h in zip(rows, holm, strict=True): lo, hi = BO.boot_ci(m) print(f"{a + ' vs ' + b:16s}{len(m):3d}{st.mean(m):+9.2f} [{lo:+.2f}, {hi:+.2f}]" f"{p:9.4f}{h:10.4f}{'✔' if h < 0.05 else ' '} {w}") print(" ⚠ `E2 vs A_law` значим и в таблице ИТОГа НЕ напечатан — восстановлено ревью 08.08.") def money() -> None: print("\n═══ ИТОГ — ДЕНЬГИ ═══") def s(pat: str) -> float: out = 0.0 for f in OUT.glob(pat): try: x = json.loads(f.read_text(encoding="utf-8")) except (ValueError, OSError): continue if isinstance(x, dict): out += x.get("cost_usd") or 0.0 return out print(f" пак целиком ${s('*.json'):.5f}") print(f" фаза Ф2б: армы C/E `p2b-*` ${s('p2b-*.json'):.5f} + старые пробы `rp-*` " f"${s('rp-*.json'):.5f} = ${s('p2b-*.json') + s('rp-*.json'):.5f}") print(" ⚠ ИТОГ печатал как расход фазы только `p2b-*`; расход ФАЗЫ включает и `rp-*`.") empty = {} for f in OUT.glob("*.json"): try: x = json.loads(f.read_text(encoding="utf-8")) except (ValueError, OSError): continue if isinstance(x, dict) and (x.get("cost_usd") or 0) > 0 \ and not (x.get("content") or x.get("answer") or "").strip(): t = f.name.split("-")[0] e = empty.setdefault(t, [0, 0.0]) e[0] += 1 e[1] += x["cost_usd"] tot_n = sum(v[0] for v in empty.values()) tot_s = sum(v[1] for v in empty.values()) print(f" оплачено за ПУСТОЙ выход: {tot_n} вызовов ${tot_s:.5f} — " + " · ".join(f"{k} {v[0]} шт ${v[1]:.5f}" for k, v in sorted(empty.items()))) print(" ⚠ Счётчик смотрит ОБА поля выхода (`content` и `answer`): проба контаминации") print(" пишет в `answer`, и счётчик по одному полю давал ложные $0.19191 на 43 вызова.") def noise() -> None: print("\n═══ ИТОГ — ШУМ ═══") for nm in ("abs", "abs2", "abs4"): d = AJ._by_unit_pass(nm) m = [v["CTRLfloorA"]["errors"] - v["CTRLfloorB"]["errors"] for v in d.values() if "CTRLfloorA" in v and "CTRLfloorB" in v] if len(m) > 1: print(f" пол рига {nm:5s}: n={len(m):2d} среднее {st.mean(m):+.2f} · " f"sd {st.pstdev(m):.2f} — две генерации ОДНОГО арма, один судья") print(" ⇒ разброс повтора превышает разницу между армами: на ОДНОЙ главе армы не упорядочены.") print(" ⚠ Разложение шума на судейскую и генерационную компоненты НЕ УСТАНОВЛЕНО:") print(" `absjudge --pool` печатает один способ счёта, два других дают другой вердикт.") def main() -> None: p1_leg() p1_robust() p2_canon() p3_draft() p4_axes() p5_router() p6_guard() p7_2b() print("\n═══ ИТОГ п.8 — ДРЕЙФ ═══\n штатная команда: `drift.py --score` (9 общих глав)") money() noise() print("\nВСЁ ВЫШЕ ПЕРЕ-СНЯТО ИЗ СЫРЬЯ, $0. Числа, помеченные ⚠ «в дереве нет», не выводятся.") if __name__ == "__main__": main()