From 84bc73baa57eb841f0a5bdd09815c63dd852a0c6 Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 8 Aug 2026 02:42:48 +0300 Subject: [PATCH] =?UTF-8?q?Add=20the=20missing=20new=20file=20of=20the=20p?= =?UTF-8?q?revious=20commit:=20the=20command=20that=20recomputes=20every?= =?UTF-8?q?=20=D0=B8=D1=82=D0=BE=D0=B3=20number=20from=20raw?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eval/role_topology/itog.py | 384 +++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 eval/role_topology/itog.py diff --git a/eval/role_topology/itog.py b/eval/role_topology/itog.py new file mode 100644 index 00000000..2af05afb --- /dev/null +++ b/eval/role_topology/itog.py @@ -0,0 +1,384 @@ +#!/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()