#!/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"(? 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()