477 lines
32 KiB
Python
477 lines
32 KiB
Python
#!/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()
|