textmachine/eval/editor_tier/verify23.py

189 lines
11 KiB
Python
Raw Permalink 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
"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА ПАКА. $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)