220 lines
12 KiB
Python
220 lines
12 KiB
Python
#!/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)
|