#!/usr/bin/env python3 """ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА ПАКА. $0. Ненулевой код возврата = отчёт не сдаётся. `itog23.py` пере-считывает числа из сырья; этот файл сверяет их с ТЕКСТОМ отчёта. Разделение нужно потому, что «число посчиталось» и «в отчёте напечатано именно оно» — разные утверждения. ⚠ ЧЕГО ЭТОТ ГЕЙТ НЕ ДЕЛАЕТ. Он сторожит, что число ПРИСУТСТВУЕТ в тексте, а не что предложение вокруг числа истинно. Приёмка эксп-22 нашла ровно этот класс: «все перевесы выше порога 2.47» при всех сходящихся числах. Пороговые и сравнительные утверждения проверяются только чтением; за соответствие ЗАКАЗУ отвечает отдельный инструмент `eval/conformance.py`. """ from __future__ import annotations import json import re import statistics as sst import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent sys.path.insert(0, str(REPO / "eval" / "tenant_panel")) sys.path.insert(0, str(REPO / "eval" / "role_topology")) def _load(name: str, path: Path): import importlib.util # noqa: PLC0415 spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod IT = _load("itog23", ZONE / "itog23.py") JU, PAN, SCR, MONEY, MAT = IT.JU, IT.PAN, IT.SCR, IT.MONEY, IT.MAT REPORT = REPO / "docs" / "experiments" / "23-editor-tier.md" BAD = 0 def ck(name: str, ok: bool, detail: str = "") -> None: global BAD # noqa: PLW0603 BAD += not ok print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" {detail}" if detail else "")) def _norm(x: str) -> str: """Пробелы не значимы; типографский минус U+2212 и ASCII-дефис — один знак.""" return re.sub(r"\s+", "", x).replace("−", "-").replace("–", "-") def has(text: str, s: str) -> bool: return _norm(s) in _norm(text) def main() -> int: if not REPORT.exists(): print("отчёта нет") return 1 t = REPORT.read_text(encoding="utf-8") print("=== МАТЕРИАЛ ===") new = MAT.units_new() ck(f"новых единиц {len(new)}", has(t, f"{len(new)} нов") or has(t, f"{len(new)} НОВ")) ck("главы новых единиц напечатаны", all(has(t, str(c)) for c in sorted({u["chapter"] for u in new})[:3])) print("\n=== ФАЗА A ===") for m, r in SCR.result().items(): ck(f"{m}: вердикт {r['verdict']} назван", has(t, m)) if r["price_editor"]: ck(f"{m}: цена редактора {r['price_editor']:.5f}", has(t, f"{r['price_editor']:.5f}")) ck("оба вердикта скрина различены (пре-рег и полный критерий эксп-22)", has(t, "пре-рег") and has(t, "переводческ")) print("\n=== ЦЕНА ===") us = JU.P22.M.units_zh() T22 = JU.P22.OUT def cost(f: Path) -> float: return json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) if f.exists() else 0.0 base = [cost(T22 / f"{JU.P22.tag_draft(PAN.ANCHOR, u['uid'])}.json") for u in us] # ⚠ R0 сторожится наравне с панелью. Прежняя редакция шла по `survivors()` = T1..T3, и строка # боевого редактора — та, против которой читается ВЕСЬ вывод о цене, — не проверялась ничем. arms = [("R0", [cost(T22 / f"{JU.P22.tag_edit(PAN.ANCHOR, u['uid'])}.json") for u in us])] arms += [(f"T{i}", [cost(MONEY.OUT / f"{PAN.tag_c(m, u['uid'])}.json") for u in us]) for i, m in enumerate(PAN.survivors(), start=1)] for name, cs in arms: nz = [c for c in cs if c > 0] if not nz: continue ck(f"{name}: p50 редактуры {sst.median(nz):.5f}", has(t, f"{sst.median(nz):.5f}")) link = [c + b for c, b in zip(cs, base, strict=True) if c > 0 and b > 0] if link: ck(f"{name}: книга по СВЯЗКЕ ${sst.median(link) * 1500:.2f}", has(t, f"{sst.median(link) * 1500:.2f}")) print("\n=== СВЕЖЕСТЬ РАЗБОРА ===") # ⚠ ГОЛОС СТАРШЕ СВОЕГО ОТВЕТА = РАЗБОР ГНАЛСЯ В СЕРЕДИНЕ ГОНКИ. Поймано аудитом: два круга # досуживания писали в ОДНИ И ТЕ ЖЕ файлы ответов, `--ingest` прошёл между ними, и 5 единиц из # 32 остались с голосами от одного круга при ответах от другого. Числа при этом пере-считывались # из голосов и сходились с отчётом — то есть ни один прежний гейт этого не видел. for pass_ in JU.PASSES: votes, answ = MONEY.OUT / f"aj-{pass_}-votes", MONEY.OUT / f"aj-{pass_}" if not votes.exists() or not answ.exists(): continue stale = [a.stem for a in answ.glob("*.txt") if (votes / f"{a.stem}.json").exists() and a.stat().st_mtime > (votes / f"{a.stem}.json").stat().st_mtime + 1] ck(f"{pass_}: голоса не старше ответов судей", not stale, f"устарели {len(stale)}: {' '.join(sorted(stale)[:6])}" if stale else "") print("\n=== СУДЕЙСТВО ===") for pass_ in JU.PASSES: v = MONEY.OUT / f"aj-{pass_}-votes" # ⚠ ПРОХОД БЕЗ ГОЛОСОВ РОНЯЕТ ГЕЙТ. Прежняя редакция делала `continue`, и гейт печатал # «ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ» с кодом 0, не проверив по проходу НИ ОДНОГО числа, — при # том что отчёт печатал по нему несущий вывод. Ровно этим прошёл аннулированный border. if not v.exists() or not list(v.glob("*.json")): ck(f"{pass_}: голоса прохода на диске", False, "нет ни одного — числа не пере-снимаемы") continue JU.setup(pass_) import absjudge as AJ # noqa: PLC0415 data = AJ._by_unit() # noqa: SLF001 ck(f"{pass_}: единиц отсужено {len(data)}", len(data) >= 3, f"{len(data)}") for a, b, _w in JU.family(pass_): m = AJ._margins(a, b) # noqa: SLF001 if len(m) < 3: continue ck(f"{pass_}: контраст {a} vs {b} = {sst.mean(m):+.2f} напечатан", has(t, f"{sst.mean(m):+.2f}") or has(t, f"{sst.mean(m):.2f}")) lo, hi = AJ.BO.boot_ci(m) ck(f"{pass_}: ДИ {a} vs {b} [{lo:+.2f}, {hi:+.2f}] напечатан", has(t, f"[{lo:+.2f},{hi:+.2f}]")) dec = [d[d["CTRLdecoy"].get("decoy_from", "")]["errors"] - d["CTRLdecoy"]["errors"] for d in data.values() if "CTRLdecoy" in d and d["CTRLdecoy"].get("decoy_from", "") in d] # Декой — условие осмысленности пака по его же §0, поэтому сторожится ЧИСЛОМ, а не словом. ck(f"{pass_}: ДЕКОЙ есть в каждой единице", len(dec) == len(data), f"{len(dec)}/{len(data)}") if dec: ck(f"{pass_}: декой пойман {sum(1 for x in dec if x < 0)}/{len(dec)}", all(x < 0 for x in dec)) ck(f"{pass_}: величина декоя {sst.mean(dec):+.2f} напечатана", has(t, f"{sst.mean(dec):+.2f}")) flo = [d["CTRLfloorA"]["errors"] - d["CTRLfloorB"]["errors"] for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d] if flo: sd = sst.pstdev(flo) thr = 2.8 * sd / (len(flo) ** 0.5) # Порог различимости — решающее правило пака: печатать его надо ИМЕННО из своего пола. ck(f"{pass_}: ПОРОГ {thr:.2f} напечатан", has(t, f"{thr:.2f}")) ck(f"{pass_}: пол {sst.mean(flo):+.2f} напечатан", has(t, f"{sst.mean(flo):+.2f}")) # ⚠ НАКЛОН СТОРОЖИТСЯ ЧИСЛОМ (поправка фазы Д, чек-лист приёмки №16 п.6). В теле стояло # +0.646, а текущее сырьё даёт +0.708: величина, на которую делается ПОПРАВКА боевого # контраста, печаталась не из тех голосов, из которых считается сам контраст. Ни один # прежний гейт наклон не трогал — сторожились только перевес, ДИ, пол, порог и декой. xs, ys = [], [] for d in data.values(): mx = sst.mean([r["slot"] for r in d.values()]) my = sst.mean([r["errors"] for r in d.values()]) for r in d.values(): xs.append(r["slot"] - mx) ys.append(r["errors"] - my) if sum(x * x for x in xs) > 0: slope = sum(a * b for a, b in zip(xs, ys, strict=True)) / sum(x * x for x in xs) ck(f"{pass_}: НАКЛОН {slope:+.3f} напечатан", has(t, f"{slope:+.3f}"), f"n={len(xs)}") print("\n=== ДЕНЬГИ ===") tot = 0.0 for ph in MONEY.TAGS: sp = MONEY.Guarded(ph).spent() tot += sp if sp <= 0: continue ck(f"{ph}: расход ${sp:.6f} напечатан", has(t, f"{sp:.6f}")) if sp > MONEY.CEILINGS[ph] + 1e-9: ck(f"{ph}: ПРОБОЙ объявлен", has(t, "ПРОБОЙ") or has(t, "пробой")) ck(f"расход пака ${tot:.6f} напечатан", has(t, f"{tot:.6f}")) ck("подъём потолка фазы C из резерва объявлен", has(t, "2.30") and has(t, "резерв")) print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not BAD else f'РАСХОЖДЕНИЙ: {BAD} — НЕ СДАЁТСЯ'}") return BAD if __name__ == "__main__": sys.exit(1 if main() else 0)