textmachine/eval/tenant_panel/verify22.py

220 lines
12 KiB
Python
Raw 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
"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА ЭКСП-22. $0. Ненулевой код возврата = отчёт не сдаётся.
`itog22.py` пере-считывает числа из сырья; этот файл сверяет их с ТЕКСТОМ отчёта. Разделение
нужно потому, что «число посчиталось» и «в отчёте напечатано именно оно» — разные утверждения, и
эксп-21 разошёлся с сырьём ровно на этом стыке (§18: ИТОГ печатал расход фазы без одного тега,
а гейт это сертифицировал).
Правило: гейт сторожит НЕСУЩИЕ числа — те, на которых стоит решение владельца. Число, которого
гейт не проверяет, в ИТОГе стоять не должно.
⚠ ЧЕГО ЭТОТ ГЕЙТ НЕ ДЕЛАЕТ, чтобы клейм не был шире прибора. Он проверяет, что число ПРИСУТСТВУЕТ
в тексте отчёта, а не что оно стоит в нужном месте и не что предложение вокруг него истинно.
Приёмка нашла ровно этот класс: «все перевесы выше порога 2.47» — все числа напечатаны и сходятся,
а утверждение о них ложно. Пороговые и сравнительные предложения гейт не сторожит; их проверяет
только чтение.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
sys.path.insert(0, str(ZONE))
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
import bank as BANK # noqa: E402
import material as M # noqa: E402
import money as MONEY # noqa: E402
import panel as P2 # noqa: E402
REPORT = REPO / "docs" / "experiments" / "22-tenant-panel.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("\u2212", "-").replace("\u2013", "-")
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("=== МАТЕРИАЛ ===")
zs = M.units_zh()
ck("книга и дата первой публикации названы",
has(t, M.BOOK["title"]) and has(t, M.BOOK["first_pub"]))
ck(f"число zh-единиц {len(zs)}", has(t, f"{len(zs)} единиц") or has(t, f"{len(zs)} zh"))
ck("гейт прав объявлен числом снятых глав",
has(t, "гейт прав") or has(t, "ГЕЙТ ПРАВ"))
b = BANK.PR.bank()
tot = sum(sum(u["source"].count(x) for u in zs) for x in b)
ck(f"банк {len(b)} терминов", has(t, f"{len(b)} термин"))
ck(f"знаменатель покрытия {tot}", has(t, str(tot)))
print("\n=== КОНТАМИНАЦИЯ ===")
f = MONEY.OUT / "contamination.json"
if f.exists():
for r in json.loads(f.read_text(encoding="utf-8")):
ck(f"{r['book']}/{r['model'].split('-')[0]} узнано {r['recognized']}/{r['n']}",
has(t, f"{r['recognized']}/{r['n']}"))
print("\n=== СКРИН Ф1 ===")
res = P2.screen_result()
for m, r in res.items():
if r.get("price_translator") is None:
continue
ck(f"{m}: цена переводчика {r['price_translator']:.5f}",
has(t, f"{r['price_translator']:.5f}"))
ck(f"{m}: вердикт {r['verdict']}", has(t, m))
print("\n=== ПАНЕЛЬ Ф2 ===")
for m in P2.tenants():
ck(f"жилец {m} назван в отчёте", has(t, m))
# ⚠ ПОКРЫТИЕ КАНОНА СТОРОЖИТСЯ ЧИСЛОМ (поправка фазы Д). Таблицы (г) и §6.2 несут по 18
# чисел покрытия, и до сих пор их не проверял никто: правка регекса банка прошла бы молча.
# Поймано ревью, когда в `bank._rx` нашлась ловля корня В СЕРЕДИНЕ чужого слова.
print("\n=== ПОКРЫТИЕ КАНОНА ===")
us = M.units_zh()
for label, get in (("D0", lambda u: P2.draft_text(P2.ANCHOR, u["uid"])),
("E0", lambda u: P2.edit_text(P2.ANCHOR, u["uid"]))):
k = s = 0
for u in us:
o = get(u) or ""
if not o:
continue
a, b = BANK.coverage(u["source"], o)
k += a
s += b
if s:
ck(f"покрытие канона {label} = {k / s:.3f} напечатано", has(t, f"{k / s:.3f}"),
f"{k}/{s}")
print("\n=== ДЕНЬГИ ===")
for ph in ("Ф0", "Ф1", "Ф2", "Ф3"):
sp = MONEY.Guarded(ph).spent()
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, "пробит") or has(t, "пробой"),
f"${sp:.6f} против ${MONEY.CEILINGS[ph]:.2f}")
pack = MONEY.Pack().spent()
ck(f"расход пака ${pack:.6f} напечатан", has(t, f"{pack:.6f}"))
print("\n=== ОСЬ (а): ЧЕРНОВИК КАК ТЕКСТ ===")
import statistics as sst # noqa: PLC0415
for pass_ in ("p2zh", "p2en"):
votes = MONEY.OUT / f"aj-{pass_}-votes"
if not votes.exists() or not list(votes.glob("*.json")):
continue
import judge as JU0 # noqa: PLC0415
JU0.setup(pass_)
import absjudge as AJ0 # noqa: PLC0415
for a, bb, _w in JU0.family_desc(pass_):
m = AJ0._margins(a, bb) # noqa: SLF001
if not m:
continue
if pass_ == "p2zh": # решающая печать — zh; en описателен и в ИТОГ не вынесен
ck(f"{pass_}: {a} vs {bb} = {sst.mean(m):+.2f} напечатан",
has(t, f"{sst.mean(m):+.2f}"))
print("\n=== ПОЗИЦИОННЫЙ НАКЛОН (требование ЗАКОНа промта) ===")
ck("наклон объявлен замеренным по всем проходам", has(t, "0.078") and has(t, "0.469"))
ck("поправленное значение довеска напечатано", has(t, "-1.65") or has(t, "1.65"))
print("\n=== ЦЕНА p95 ===")
import itog22 as IT # noqa: PLC0415
zh = M.units_zh()
for m in P2.tenants():
dc = [P2.draft_cost(m, u["uid"]) for u in zh]
ec = []
for u in zh:
f2 = MONEY.OUT / f"{P2.tag_edit(m, u['uid'])}.json"
if f2.exists():
ec.append(json.loads(f2.read_text(encoding="utf-8")).get("cost_usd", 0.0))
if not dc:
continue
pair = [d + e for d, e in zip(dc, ec, strict=False)] if ec else dc
ck(f"{m}: связка p95 {IT._p95(pair):.5f}", has(t, f"{IT._p95(pair):.5f}")) # noqa: SLF001
print("\n=== ПУСТОЙ ВЫХОД И ГРАНИЦА ДВОЙНОЙ ОПЛАТЫ ===")
empty = []
for globs in MONEY.TAGS.values():
for g in globs:
for f3 in MONEY.OUT.glob(g):
r3 = json.loads(f3.read_text(encoding="utf-8"))
if r3.get("cost_usd", 0) > 0 and not (r3.get("content") or "").strip():
empty.append(r3["cost_usd"])
if empty:
ck(f"оплачено за пустой выход ${sum(empty):.5f} напечатано", has(t, f"{sum(empty):.5f}"))
ck(f"{len(empty)} пустых вызовов объявлено", has(t, f"{len(empty)} вызов"))
# граница двойной оплаты обязана быть ВЫВОДИМОЙ: расход Ф2 до атомарного замка
import subprocess # noqa: PLC0415
at = subprocess.run(["git", "log", "--format=%at", "-1", "fd3e522"],
capture_output=True, text=True, cwd=REPO).stdout.strip()
if at:
pre = 0.0
for g in MONEY.TAGS["Ф2"]:
for f4 in MONEY.OUT.glob(g):
if f4.stat().st_mtime < int(at):
pre += json.loads(f4.read_text(encoding="utf-8")).get("cost_usd", 0.0)
ck(f"верхняя граница двойной оплаты ${pre:.6f} напечатана", has(t, f"{pre:.6f}"),
"граница = расход Ф2 ДО атомарного замка, в худшем случае оплаченный дважды")
print("\n=== СУДЕЙСТВО ===")
import judge as JU # noqa: PLC0415
rs = JU.runs_used()
if rs:
ck(f"агент-запусков {len(rs)} из {JU.CAP} напечатано",
has(t, f"{len(rs)} из {JU.CAP}") or has(t, f"{len(rs)}/{JU.CAP}"))
for pass_ in ("p2zh", "p2en", "p3", "dva"):
votes = MONEY.OUT / f"aj-{pass_}-votes"
if not votes.exists() or not list(votes.glob("*.json")):
continue
JU.setup(pass_)
import absjudge as AJ # noqa: PLC0415
data = AJ._by_unit() # noqa: SLF001
for a, bb, _w in JU.family(pass_):
m = AJ._margins(a, bb) # noqa: SLF001
if len(m) < 3:
continue
import statistics as sst # noqa: PLC0415
ck(f"{pass_}: контраст {a} vs {bb} = {sst.mean(m):+.2f} напечатан",
has(t, f"{sst.mean(m):+.2f}") or has(t, f"{sst.mean(m):.2f}"))
# контроли обязаны быть напечатаны ДО боевых чисел
dec = [d for d in data.values() if "CTRLdecoy" in d]
if dec:
ck(f"{pass_}: декой упомянут в отчёте", has(t, "декой") or has(t, "ДЕКОЙ"))
flo = [d for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d]
if flo:
ck(f"{pass_}: шумовой пол упомянут", has(t, "пол") or has(t, "ПОЛ"))
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not BAD else f'РАСХОЖДЕНИЙ: {BAD} — ОТЧЁТ НЕ СДАЁТСЯ'}")
return BAD
if __name__ == "__main__":
sys.exit(1 if main() else 0)