textmachine/eval/role_topology/verify_report.py

477 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА эксп-21 (вторая редакция). Ненулевой код возврата = отчёт не сдаётся.
Правило: каждое число отчёта пере-выводится ИЗ СЫРЬЯ и сверяется с напечатанным. Верификатор
не читает выводы — он читает артефакты и текст, и падает на расхождении.
Три вещи, которых у первой редакции не было и которые тут есть по её разбору:
* ДЕНЬГИ ВТОРЫМ НЕЗАВИСИМЫМ ПУТЁМ. `prices.cost` считает цену и при записи, и при сверке, поэтому
их совпадение доказывает целостность персистенции, а НЕ правильность правила тарификации:
ошибка внутри `cost` (как отсутствовавшая ветка Gemini) проходит обе стороны незамеченной.
`independent_price` выписывает правило заново, по строкам quirks 00.
* ИСПРАВНОСТЬ РАСКЛАДКИ проверяется равенством числа голосов по порядкам, а не малостью
позиционной форы: зеркало убирает позицию из перевеса АРМА, а не из среднего по всем голосам.
* АРМЫ СВЕРЯЮТСЯ С БУКВОЙ ПРОМТА исполнением — сборкой сообщений, а не чтением докстрингов.
Запуск: eval/.venv/bin/python eval/role_topology/verify_report.py
"""
from __future__ import annotations
import json
import re
import statistics
import sys
from itertools import combinations
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
RAW = Path.home() / "books" / "role-topology"
DOC = REPO / "docs" / "experiments" / "21-role-topology.md"
FAILS = 0
def num_in(text: str, s: str) -> bool:
"""Точное вхождение ЧИСЛА: не префикс более длинного и не хвост другого.
⚠ Свип приёмки 07.08 прогнал 18 мутантов отчёта, подменяя число на более длинное с тем же
префиксом (`0.469816`→`0.4698169`), и ВСЕ 18 прошли зелёными: предикат `num_in(text, f"{x:.5f}")`
слеп к префиксу. Плюс две проверки не могли покраснеть вовсе — иглой было голое короткое
число. Точная форма чинит весь класс разом. (Восстановлено после инцидента 08.08, D39.113.)
"""
return re.search(rf"(?<![\d.]){re.escape(s)}(?![\d.])", text) is not None
def num_near(text: str, label: str, s: str) -> bool:
"""Число обязано стоять В СТРОКЕ С МЕТКОЙ, а не где угодно в файле: иначе дубль в прозе
даёт клетке таблицы алиби, и порча одной клетки проходит зелёной."""
for line in text.splitlines():
if label in line and num_in(line, s):
return True
return False
def med(xs: list, default: float = 0.0) -> float:
"""Медиана, не роняющая гейт на пустом сырье."""
return statistics.median(xs) if xs else default
def ck(claim: str, ok: bool, got: str = "") -> None:
global FAILS # noqa: PLW0603
if not ok:
FAILS += 1
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {claim}" + ("" if ok else f" — сырьё даёт {got}"))
def independent_price(model: str, r: dict) -> float:
"""Цена по прайсу, посчитанная НЕ через `prices.cost` — второй независимый путь."""
from prices import CANDIDATES # noqa: PLC0415
_, _, pin, pcache, pout, _, _ = CANDIDATES[model]
pt, ca = r.get("prompt_tokens", 0), r.get("cached_tokens", 0)
ct, rt = r.get("completion_tokens", 0), r.get("reasoning_tokens", 0)
tt = r.get("total_tokens", 0)
if model.startswith("grok"):
out = ct + rt # xAI: размышление СВЕРХ completion
elif model.startswith("gemini"):
out = max(tt - pt, ct + rt) # Gemini: размышление только в total (D22.3)
else:
out = ct # DeepSeek/OpenAI/Z.AI: размышление ВНУТРИ completion
return (pt - ca) / 1e6 * pin + ca / 1e6 * pcache + out / 1e6 * pout
def money(pat: str) -> float:
# ⚠ часть файлов зоны — списки-сводки, а не записи покупок; `.get` на них падал.
out = 0.0
for f in RAW.glob(pat):
try:
d = json.loads(f.read_text(encoding="utf-8"))
except (ValueError, OSError):
continue
if isinstance(d, dict):
out += d.get("cost_usd") or 0.0
return out
def main() -> None: # noqa: PLR0915
if not DOC.exists():
ck("отчёт на месте", False, "нет файла")
sys.exit(1)
text = DOC.read_text(encoding="utf-8")
import bakeoff as BO # noqa: PLC0415
import pair_en as E # noqa: PLC0415
import judges as J # noqa: PLC0415
print("=== СТРУКТУРА ЗАМЕРА ===")
us_zh, us_en = BO.units(), E.units()
ck("zh: 16 единиц", len(us_zh) == 16, str(len(us_zh)))
ck("en: 12 единиц, страты 6/6", len(us_en) == 12
and sum(1 for u in us_en if u["stratum"] == "fr") == 6, str(len(us_en)))
ck("zh: единицы уникальны по нормализованной подписи",
len({BO._dedup_sig(u["source"]) for u in us_zh}) == 16)
ck("отбор единиц ДЕТЕРМИНИРОВАН (повторный вызов даёт тот же набор)",
[u["uid"] for u in BO.units()] == [u["uid"] for u in us_zh]
and [u["uid"] for u in E.units()] == [u["uid"] for u in us_en])
print("\n=== АРМЫ ПРОТИВ БУКВЫ ПРОМТА (проверка исполнением) ===")
a_msgs = BO.build_msgs("A", us_zh[0])
ck("арм A идёт БЕЗ банкноты (промт стр.29)",
not any("КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in m["content"] for m in a_msgs))
ck("закон пинится БЕЗ оговорки области (промт стр.29)",
not any("глоссарий обязателен для употреблений" in m["content"]
for arm in BO.ARMS for m in BO.build_msgs(arm, us_zh[0])))
ck("A и A_law различаются ТОЛЬКО банкнотой",
a_msgs[0]["content"] == BO.build_msgs("A_law", us_zh[0])[0]["content"]
and a_msgs[-1]["content"] == BO.build_msgs("A_law", us_zh[0])[-1]["content"])
d, d_ = BO.build_msgs("D", us_zh[0]), BO.build_msgs("D_", us_zh[0])
ck("D и D_ различаются ТОЛЬКО мандатом",
d[1]["content"] == d_[1]["content"] and d[0]["content"] != d_[0]["content"])
bs = [json.loads(f.read_text(encoding="utf-8")) for f in RAW.glob("bo2-B-*.json")]
ck("арм B: мышление ПОГАШЕНО (промт стр.62)",
bool(bs) and all(r.get("reasoning_tokens", 0) == 0 for r in bs),
f"максимум {max((r.get('reasoning_tokens', 0) for r in bs), default=-1)}")
print("\n=== РАСКЛАДКА СУДЕЙ ===")
# Считаются только голоса ЖИВЫХ единиц: на диске лежат ещё голоса единиц, отобранных до
# починки недетерминированного отбора (§3 п.9). Они не участвуют в своде и не должны
# участвовать в контроле баланса — иначе контроль мерит мусор, а не раскладку.
for pref, live in (("jv2-", {u["uid"] for u in us_zh}), ("jve-", {u["uid"] for u in us_en})):
by = {0: 0, 1: 0}
for f in RAW.glob(f"{pref}*.json"):
if f.name.endswith((".ERROR.json", ".NOMSG.json")):
continue
r = json.loads(f.read_text(encoding="utf-8"))
if r.get("uid") in live:
by[r.get("order", 0)] += 1
ck(f"{pref}: голосов порядка 0 и порядка 1 ПОРОВНУ ({by[0]}/{by[1]})",
by[0] == by[1] and by[0] > 0, f"o0={by[0]} o1={by[1]}")
ck("судьи вне семейств, населяющих армы",
not any(j.startswith(("deepseek", "glm")) for j in J.JUDGES), str(J.JUDGES))
bias_zh = BO.position_bias()
ck("позиционная фора zh напечатана в отчёте", f"{bias_zh:+.2f}".replace("+", "+") in text
or num_in(text, f"{abs(bias_zh):.2f}"), f"{bias_zh:+.2f}")
print("\n=== ТАБЛИЦЫ ПЕРЕВЕСОВ (пере-вывод из голосов) ===")
for a, b in BO.CONTRASTS:
per = []
for u in us_zh:
pj = []
for judge in J.JUDGES:
bo = BO.margins_by_order(a, b, u["uid"], judge)
if len(bo) < 2:
continue
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
if len(pj) == 4:
per.append(statistics.mean(pj))
if not per:
ck(f"zh {a}/{b}: голоса на месте", False, "нет")
continue
eff = statistics.mean(per)
lo, hi = BO.boot_ci(per)
zh_sec = text.split("### 2.1", 1)[-1].split("### 2.2", 1)[0]
printed = re.search(rf"^{re.escape(a)} против {re.escape(b)}\s+(\d+)\s+\d+\s+\d+\s+\d+\s+"
rf"([+-]?[\d.]+)\s+\[", zh_sec, re.M)
ck(f"zh {a}/{b}: перевес {eff:+.2f}, ДИ [{lo:+.2f}, {hi:+.2f}] — совпадает с отчётом",
bool(printed) and abs(float(printed.group(2).replace("", "-")) - eff) < 0.005
and int(printed.group(1)) == len(per),
f"n={len(per)} перевес {eff:+.2f}; в отчёте {printed.group(0)[:40] if printed else ''}")
for a, b in E.CONTRASTS:
per = []
for u in us_en:
pj = []
for judge in J.JUDGES:
bo = E._margins(a, b, u["uid"], judge)
if len(bo) < 2:
continue
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
if len(pj) == 4:
per.append(statistics.mean(pj))
if not per:
ck(f"en {a}/{b}: голоса на месте", False, "нет")
continue
eff = statistics.mean(per)
# ⚠ Искать в ГРАНИЦАХ РАЗДЕЛА: метки контрастов в таблицах zh и en одинаковы, и поиск по
# всему файлу возвращает китайскую строку. Поймано исполнением: сверка «совпадает» при
# совпадающих числах падала, потому что сравнивала en-перевес с zh-строкой.
en_sec = text.split("### 2.2", 1)[-1].split("### 2.3", 1)[0]
pr = re.search(rf"^{re.escape(a)} против {re.escape(b)}\s+(\d+)\s+\d+\s+\d+\s+\d+\s+"
rf"([+-]?[\d.]+)\s+\[", en_sec, re.M)
ck(f"en {a}/{b}: перевес {eff:+.2f} на n={len(per)} — совпадает с отчётом",
bool(pr) and abs(float(pr.group(2).replace("", "-")) - eff) < 0.005
and int(pr.group(1)) == len(per), f"n={len(per)} перевес {eff:+.2f}")
print("\n=== ДЕНЬГИ ===")
recs = [json.loads(f.read_text(encoding="utf-8"))
for f in list(RAW.glob("bo2-*.json")) + list(RAW.glob("en2-*.json"))
+ list(RAW.glob("jv2-*.json")) + list(RAW.glob("jve-*.json"))
+ list(RAW.glob("jc-*.json"))]
recs = [r for r in recs if not r.get("skipped") and "prompt_tokens" in r]
# Сверка ПОКЛЕТОЧНАЯ, а не по сумме: записанная цена округлена до 6 знаков, и на 500+ записях
# округление копится до величины, которую тугой допуск на сумму читает как расхождение правила.
worst = max((abs(r["cost_usd"] - independent_price(r["model"], r)) for r in recs), default=0.0)
a_sum = sum(r["cost_usd"] for r in recs)
b_sum = sum(independent_price(r["model"], r) for r in recs)
ck(f"цена сходится ДВУМЯ независимыми путями поклеточно ({len(recs)} записей, "
f"максимальное расхождение ${worst:.9f})",
worst < 1e-6, f"записано ${a_sum:.6f} · независимо ${b_sum:.6f} · худшая клетка ${worst:.9f}")
ck("total_tokens персистирован во ВСЕХ записях",
all("total_tokens" in r for r in recs),
f"{sum(1 for r in recs if 'total_tokens' not in r)} без него")
blocks = [("армы zh", "bo2-*.json", BO.CEIL_ARMS), ("судейство zh", "jv2-*.json", BO.CEIL_JUDGE),
("армы en", "en2-*.json", E.CEIL_ARMS), ("судейство en", "jve-*.json", E.CEIL_JUDGE),
("проба судьи-замены", "jc-*.json", 0.10)]
total = 0.0
for name, pat, ceil in blocks:
v = money(pat)
total += v
ck(f"{name}: ${v:.6f} ≤ потолок ${ceil:.2f} и напечатано в отчёте",
v <= ceil and num_in(text, f"{v:.6f}"), f"${v:.6f}")
ck(f"итог ${total:.6f} напечатан в отчёте", num_in(text, f"{total:.6f}"), f"${total:.6f}")
print("\n=== ЭКОНОМИКА ===")
# ⚠ Цена печатается ПО СОБСТВЕННОМУ ВЫЗОВУ арма, без ноги черновика. Основание: армы правят
# ЗАМОРОЖЕННЫЙ черновик корпуса пакета-6, чья цена не сохранена, а докупленные `bo2-DRAFT-*`
# с ним побайтно не совпадают — их сложение приписывало бы связке цену другого текста.
for arm in BO.ARMS:
costs = [json.loads((RAW / f"bo2-{arm}-{u['uid']}.json").read_text(encoding="utf-8"))["cost_usd"]
for u in us_zh if (RAW / f"bo2-{arm}-{u['uid']}.json").exists()]
m_arm = med(costs)
ck(f"{arm}: ${m_arm:.5f}/единицу напечатано в СТРОКЕ С МЕТКОЙ арма",
num_near(text, arm, f"{m_arm:.5f}"), f"{m_arm:.5f}")
same = sum(1 for u in us_zh
if BO.text_of("F", u).strip()
== json.loads((RAW / f"bo2-DRAFT-{u['uid']}.json").read_text(
encoding="utf-8"))["content"].strip())
ck("подмена ноги черновика ОБЪЯВЛЕНА: текст арма F не совпадает с докупленным черновиком",
same == 0 and "не совпадают" in text.replace("побайтно они с корпусными не совпадают",
"не совпадают"),
f"совпало {same}/{len(us_zh)}")
med_draft = statistics.median([BO.draft_cost(u["uid"]) for u in us_zh])
ck(f"пере-замер цены черновой роли ${med_draft:.5f} напечатан отдельным числом",
num_in(text, f"{med_draft:.5f}"), f"{med_draft:.5f}")
print("\n=== ПОПРАВКА НА МНОЖЕСТВЕННОСТЬ И РАЗБОР ПО СУДЬЯМ ===")
ps = []
for a, b in BO.CONTRASTS:
per = []
for u in us_zh:
pj = []
for judge in J.JUDGES:
bo = BO.margins_by_order(a, b, u["uid"], judge)
if len(bo) == 2:
pj += [statistics.mean(bo[0]), statistics.mean(bo[1])]
if len(pj) == 2 * len(J.JUDGES):
per.append(statistics.mean(pj))
ps.append(BO.sign_perm_p(per))
order_i = sorted(range(len(ps)), key=lambda i: ps[i])
holm, run = [0.0] * len(ps), 0.0
for rank, i in enumerate(order_i):
run = max(run, ps[i] * (len(ps) - rank))
holm[i] = min(1.0, run)
ck("отчёт объявляет, что НИ ОДИН контраст не проходит Holm 0.05",
all(h >= 0.05 for h in holm) and "Ни один контраст не проходит 0.05" in text,
f"минимальный Holm {min(holm):.4f}")
ck("отчёт печатает разбор ПО СУДЬЯМ (эффект одного судьи не есть свойство арма)",
"ПО СУДЬЯМ" in text or "по судьям" in text)
ck("отчёт печатает разложение ПО ОСЯМ",
"ВЕРНОСТЬ" in text and "разложение по осям" in text.lower())
print("\n=== ТРЕТИЙ КОНТУР ПЕРСИСТИРОВАН ===")
rd = RAW / "blind-keys" / "READINGS.json"
ck("ранги слепого чтения лежат на диске, ВНЕ каталога пакетов", rd.exists())
ck("карта раскладки вынесена из каталога пакетов (слепота держится расположением)",
not list((RAW / "blind").glob("*MAP*")) and bool(list((RAW / "blind-keys").glob("*MAP*"))))
if rd.exists():
d = json.loads(rd.read_text(encoding="utf-8"))
ck("персист несёт оговорку о том, что полные разборы утрачены",
"НЕ персистированы" in d.get("provenance", ""))
ck("рангов zh 16 и en 12", len(d["ranks_zh"]) == 16 and len(d["ranks_en"]) == 12)
print("\n=== БАТАРЕЯ ===")
import battery as B # noqa: PLC0415
ck("check_numbers симметричен: 两千三百 против «две тысячи триста» НЕ даёт дефекта",
not (B.check_numbers("他有两千三百块灵石。", "У него две тысячи триста камней.")["lost"]
or B.check_numbers("他有两千三百块灵石。", "У него две тысячи триста камней.")["invented"]))
ck("check_numbers ловит настоящую потерю: 三千五百 против «три тысячи»",
bool(B.check_numbers("三千五百人", "три тысячи человек")["lost"]))
cards = B.load_cards()
ck("карточки построены из подписанного сида", len(cards) == 51, str(len(cards)))
ck("check_gender отсекает косвенную форму имени",
B.check_gender("Воля Фан Юаня была тверда.", cards)["mismatched"] == 0)
ck("check_gender ловит настоящее рассогласование",
B.check_gender("Фан Юань была спокойна.", cards)["mismatched"] == 1)
print("\n=== ОТКАЗЫ ПРОВОДА ЗАПИСАНЫ, А НЕ ПОТЕРЯНЫ ===")
nomsg = list(RAW.glob("jv2-*.NOMSG.json")) + list(RAW.glob("jve-*.NOMSG.json"))
err = list(RAW.glob("jv2-*.ERROR.json")) + list(RAW.glob("jve-*.ERROR.json"))
ck(f"отказы фильтра ({len(nomsg)}) записаны и объявлены в отчёте",
bool(nomsg) and str(len(nomsg)) in text, str(len(nomsg)))
ck(f"отказы xAI ({len(err)}) записаны ОТДЕЛЬНЫМ тегом (кэш успеха не отравлен)",
all(not (RAW / f.name.replace(".ERROR.json", ".json")).exists()
or json.loads((RAW / f.name.replace(".ERROR.json", ".json")).read_text(
encoding="utf-8")).get("content") for f in err))
# ⚠ ПОТОЛКИ ПРОМТА, А НЕ ТОЛЬКО КАССЫ. Заведено адверсариальным ревью 07.08: гейт сверял
# отчёт с сырьём и возвращал 0, тогда как потолки ФАЗ промта пробиты, а кассы кода
# пере-объявляются каждой редакцией и в сумме выданы выше санкционированного. Проверка НЕ
# запрещает пробой — она требует, чтобы пробой был ОБЪЯВЛЕН в отчёте числом.
# ⚠ ПОКРЫТИЕ ТЕГОВ КАССАМИ. Ревью 07.08: $2.62646 куплено тегами, не входящими ни в один
# глоб ни одной `Ledger` — класс дефекта, который шапка `buy.py` объявляет починенным.
# Проверка не запрещает исторические траты, а требует, чтобы непокрытая сумма была НАЗВАНА.
# ⚠ ПРОВЕРКИ ДРЕЙФА, утраченные при восстановлении после инцидента 08.08 (было 63 проверки,
# стало 56). Числа берутся из того же модуля, которым считаются, и сверяются с ТЕКСТОМ отчёта.
print("\n=== §ДРЕЙФ ===")
try:
sys.path.insert(0, str(Path(__file__).parent))
import drift as DR # noqa: PLC0415
ch = DR.chapters()
common = [c for c in ch if all(DR.text(a, c["uid"]).strip() for a in DR.DRIFT_ARMS)]
ck(f"общих глав у всех армов: {len(common)}",
re.search(rf"Общих глав у всех армов: {len(common)}(?!\d)", text) is not None
or num_in(text, f"{len(common)} глав"), str(len(common)))
for a in DR.DRIFT_ARMS:
s = c_ = 0
for zh, (_1, _2, r1, _3) in DR.BO.IP.TERMS.items():
for c in common:
n = c["source"].count(zh)
if not n:
continue
s += n
c_ += min(len(re.findall(r1, DR.text(a, c["uid"]), re.I)), n)
ck(f"дрейф {a}: покрытие {c_}/{s} напечатано в отчёте",
num_in(text, f"{c_}/{s}") or num_in(text, f"{c_ / s:.2f}"), f"{c_}/{s}")
ck(f"касса дрейфа ${DR.LED_DRIFT.spent():.5f} ≤ потолка ${DR.CEIL_DRIFT:.2f}",
DR.LED_DRIFT.spent() <= DR.CEIL_DRIFT, f"{DR.LED_DRIFT.spent():.5f}")
except Exception as e: # noqa: BLE001
ck(f"дрейф проверяется гейтом ({type(e).__name__})", False, str(e)[:60])
# ══════════════════════════════════════════════════════════════════════════════════════
print("\n=== §ИТОГ — НЕСУЩИЕ ЧИСЛА БЛОКА РЕШЕНИЯ ===")
# ⚠ ЗАВЕДЕНО 08.08 ДВУМЯ АУДИТАМИ. До этого гейт сторожил таблицы §2 второй редакции, деньги
# и батарею — и НИ ОДНОГО числа ИТОГа: нога 2×2, контрасты Ф2б, покрытие канона и отборщики
# арма G стояли без стража, хотя именно на них принимается решение. Дефект того же класса,
# что глоб кассы Ф2б: охраняется не то, что решает.
try:
import absjudge as AJ # noqa: PLC0415
import itog as IT # noqa: PLC0415
st = statistics
a1, a2, common = IT._passes()
ck("ИТОГ: пул считается по 16 единицам обоих проходов", len(common) == 16, str(len(common)))
for arm, lbl in (("A", "A "), ("B", "B "), ("D_", "D_"), ("D", "D ")):
m = IT._pooled(a1, a2, common, arm, "F3")
ck(f"ИТОГ п.1 нога 2×2: {arm} против F3 = {st.mean(m):+.2f}",
num_near(text, lbl, f"{abs(st.mean(m)):.2f}"), f"{st.mean(m):+.2f}")
m = IT._pooled(a1, a2, common, "D", "A")
ck(f"ИТОГ п.4: D/A = {st.mean(m):+.2f}", num_in(text, f"{abs(st.mean(m)):.2f}"),
f"{st.mean(m):+.2f}")
us = BO.units()
for arm in ("F3", "A", "A_law", "B", "D", "D_"):
s = k = 0
for u in us:
x = BO.text3_of(arm, 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)
ck(f"ИТОГ п.2 покрытие канона {arm} = {k / s:.3f}", num_in(text, f"{k / s:.3f}"),
f"{k / s:.3f}")
gain, routed, kept = IT._route()
got = sum(gain[u] for u in kept) / sum(gain.values())
ck(f"ИТОГ п.5: арм G забирает {got:.0%} выигрыша", num_in(text, f"{got * 100:.0f}"),
f"{got:.0%}")
rej = [u for u in routed if u not in kept]
ck(f"ИТОГ п.6: страж отклонил {len(rej)} правки со средним "
f"{st.mean([gain[u] for u in rej]):+.2f}",
num_in(text, f"{st.mean([gain[u] for u in rej]):.2f}") and num_in(text, str(len(rej))),
f"n={len(rej)}")
# Ф2б: пятёрка судейских контрастов прохода 4 с поправкой Холма
sv, AJ.PASS = AJ.PASS, "abs4"
try:
d4 = AJ._by_unit()
finally:
AJ.PASS = sv
fam = [("C2", "A_law"), ("C2", "F3"), ("E2", "D_"), ("E2", "A_law"), ("A_law", "F3")]
ms = [[d4[u][b]["errors"] - d4[u][a]["errors"] for u in d4 if a in d4[u] and b in d4[u]]
for a, b in fam]
holm = IT._holm([BO.sign_perm_p(m) for m in ms])
for (a, b), m, h in zip(fam, ms, holm, strict=True):
ck(f"ИТОГ п.7: {a} vs {b} = {st.mean(m):+.2f} (Холм {h:.4f})",
num_in(text, f"{abs(st.mean(m)):.2f}"), f"{st.mean(m):+.2f}")
ck("ИТОГ п.7: значимый контраст E2/A_law НАПЕЧАТАН в таблице отчёта",
num_near(text, "E2 vs A_law", "2.06"), "строки нет")
# §12: чем несущие контрасты пробовали убить — до 08.08 считалось ВНЕ дерева
chap = {u["uid"]: u["chapter"] for u in us}
chapters = sorted({chap[u] for u in common})
ck(f"§12: {len(common)} единиц происходят из {len(chapters)} глав (единицы НЕ независимы)",
num_in(text, str(len(chapters))), str(len(chapters)))
# ⚠ Холм считается СОВМЕСТНО по объявленному семейству из семи (три посчитанных контраста
# плюс четыре недобранных как p=1), а не по каждому отдельно: раздельный счёт даёт p×7
# каждому и расходится с числами отчёта. Ровно та же формула, что в `itog.p1_robust`.
core = [("A", "F3"), ("B", "F3"), ("D", "A")]
ms = [dict(zip(common, IT._pooled(a1, a2, common, a, b), strict=True)) for a, b in core]
ps = [BO.sign_perm_p([st.mean([m[u] for u in common if chap[u] == c]) for c in chapters])
for m in ms]
for (a, b), m, h in zip(core, ms,
IT._holm(ps + [1.0] * (len(AJ.FAMILY) - len(ps))), strict=False):
ck(f"§12 кластеризация по главе: {a}/{b} Холм {h:.4f} — выживает",
h < 0.05 and num_in(text, f"{h:.4f}"), f"{h:.4f}")
n2 = sum(1 for c in combinations(range(len(m)), 2)
if BO.sign_perm_p([x for i, x in enumerate(m.values()) if i not in c])
* len(AJ.FAMILY) >= 0.05)
ck(f"§12 leave-two-out: {a}/{b} вылетов {n2}/120", n2 == 0, str(n2))
lv = []
for nm in ("abs", "abs2", "abs3", "abs4"):
d = AJ._by_unit_pass(nm)
lv.append(st.mean([r["errors"] for v in d.values() for r in v.values()]))
ck(f"§11 строгость панелей растёт монотонно {''.join(f'{x:.2f}' for x in lv)}",
all(x < y for x, y in zip(lv, lv[1:], strict=False))
and all(num_in(text, f"{x:.2f}") for x in lv), " ".join(f"{x:.2f}" for x in lv))
except Exception as e: # noqa: BLE001
ck(f"числа ИТОГа проверяются гейтом ({type(e).__name__})", False, str(e)[:80])
print("\n=== ПОКРЫТИЕ ТРАТ КАССАМИ ===")
covered = ("bo2-", "bo4-", "jv2-", "bo3-", "jf-", "jr-", "en2-", "jve-", "jc-", "dr-")
unc = 0.0
for f in RAW.glob("*.json"):
if any(f.name.startswith(c) for c in covered):
continue
try:
d = json.loads(f.read_text(encoding="utf-8"))
except (ValueError, OSError):
continue
if isinstance(d, dict):
unc += d.get("cost_usd") or 0.0
# ⚠ ТОЧНАЯ ФОРМА `$X.XXXXX` без цифры следом — та же коллизия подстроки, что уже ловилась
# в потолках фаз: «2.62283» совпадало с «$2.622830» (ценой снятой первой редакции).
ck(f"траты ВНЕ касс ${unc:.5f} названы в отчёте",
re.search(rf"\${unc:.5f}(?!\d)", text) is not None, f"{unc:.5f}")
print("\n=== ПОТОЛКИ ФАЗ ПРОМТА (не касс кода) ===")
phases = {"Ф1 скрин": (["scr-*.json"], 1.00),
"Ф2а бейк-офф": (["bo-*.json", "bo2-*.json", "bo3-*.json", "bo4-*.json",
"jv-*.json", "jv2-*.json", "jf-*.json", "jr-*.json",
"jc-*.json", "en2-*.json", "jve-*.json"], 2.50),
# ⚠ ТРЕТИЙ РАЗ ТОТ ЖЕ КЛАСС: глоб фазы не покрывал тег покупок. Было `rp-*` ($0.00904),
# а армы C/E куплены тегом `p2b-*` ($0.10916) — гейт СЕРТИФИЦИРОВАЛ ложное число.
# Ровно так же он молчал про `bo4-*` при глобе `bo2-*`. Правило: новый тег — в глоб
# фазы В ТОМ ЖЕ коммите, и это проверяется покрытием, а не памятью.
"Ф2б ремонт/маршрут": (["rp-*.json", "p2b-*.json"], 3.00)}
for name, (pats, ceil) in phases.items():
spent = sum(money(x) for x in pats)
over = spent > ceil
ck(f"{name}: ${spent:.5f} против потолка ${ceil:.2f}"
+ (" — ПРОБОЙ обязан быть объявлен числом в отчёте" if over else ""),
# ⚠ ТОЧНАЯ ФОРМА `$X.XXXXX`, а не подстрока: ревью исполнением показало три обхода —
# более длинное число с тем же префиксом, усечённая форма без доллара в постороннем
# контексте, и число рядом со словами «фаза уложилась».
(not over) or re.search(rf"\${spent:.5f}(?!\d)", text) is not None, f"{spent:.5f}")
total = money("*.json")
ck(f"полная сумма ${total:.5f} напечатана в отчёте", num_in(text, f"{total:.5f}"), f"{total:.5f}")
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not FAILS else f'РАСХОЖДЕНИЙ: {FAILS}'}")
sys.exit(1 if FAILS else 0)
if __name__ == "__main__":
main()