189 lines
11 KiB
Python
189 lines
11 KiB
Python
#!/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)
|