textmachine/eval/role_topology/verify_report.py

299 lines
19 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
"""Сверка НЕСУЩИХ ЧИСЕЛ отчёта `docs/experiments/21-role-topology.md` с сырьём.
Мандат промта: «Верификатор чисел отчёта отдельным скриптом; ненулевой код возврата = отчёт не
сдаётся». Смысл в том, что число попадает в текст через мою же аналитику, и опечатка, устаревшая
цифра после пере-съёмки и факт визуально неотличимы. Здесь каждое заявленное число выводится из
персистированных артефактов ЗАНОВО и сравнивается с тем, что написано в отчёте — включая сам текст
отчёта, чтобы расхождение ловилось, даже если я забуду поправить таблицу.
Запуск: eval/.venv/bin/python eval/role_topology/verify_report.py
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
RAW = Path.home() / "books" / "role-topology"
REPORT = REPO / "docs" / "experiments" / "21-role-topology.md"
FAILS = 0
def ck(claim: str, ok: bool, got: str = "") -> None:
global FAILS
if not ok:
FAILS += 1
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {claim}" + ("" if ok else f" — сырьё даёт {got}"))
def main() -> None:
text = REPORT.read_text(encoding="utf-8")
import detect_word as D # noqa: PLC0415
# §2.1 — таблица слоёв детектора выводится заново из земли, а не читается из json.
rows = [json.loads(l) for l in D.LABELS.read_text(encoding="utf-8").splitlines() if l.strip()]
bank = D.load_bank()
expect = {0: (9, 34, 1), 1: (9, 28, 1), 2: (8, 11, 2), 3: (7, 2, 3)}
for layer, want in expect.items():
s = D.score(rows, layer, bank)
got = (s["tp"], s["fp"], s["fn"])
ck(f"§2.1 слой С{layer} = tp/fp/fn {want}", got == want, str(got))
s3 = D.score(rows, 3, bank)
ck("§2.1 итоговые precision 0.778 / recall 0.700",
abs(s3["precision"] - 0.778) < 0.001 and abs(s3["recall"] - 0.700) < 0.001,
f"{s3['precision']:.3f}/{s3['recall']:.3f}")
ck("§2.1 таблица отчёта несёт те же 0.778 / 0.700",
"0.778" in text and "0.700" in text)
import editor_wire_probe as WP # noqa: PLC0415
plants = [w for rs in WP.DEFECTS.values() for cls, old, new, _, _ in rs if cls == "k1"
for w in new.split() if w not in old.split()]
hit = sum(1 for w in plants if D.verdict(w, bank)[0])
ck("§2.1 выделенная валидация 6/6", (hit, len(plants)) == (6, 6), f"{hit}/{len(plants)}")
ck("§2.1 ущерб: ложных 2 из 9 флагов (22%)",
s3["fp"] == 2 and s3["tp"] + s3["fp"] == 9, f"{s3['fp']}/{s3['tp'] + s3['fp']}")
# §2.2 — QE. Числа берутся из персистированного сырья прогона, а не пере-считываются моделью
# (пере-счёт стоил бы 20 минут CPU и не добавил бы проверки: артефакт и есть сырьё).
# ⚠ Артефакты РАЗВЕДЕНЫ ПО МОДЕЛЯМ. Первая редакция харнесса писала в общее имя, и прогон XL
# молча затёр сырьё large — поймано этим же верификатором, отчёт бы остался «со слов сессии».
import statistics # noqa: PLC0415
expect_qe = {
"large": dict(decoy=(69, 7.107, 65), k1=(6, 4), k2=(6, 3), clean=81),
"xl": dict(decoy=(69, 7.000, 59), k1=(6, 4), k2=(6, 5), clean=81),
}
for tag, want in expect_qe.items():
seg, dec = RAW / f"qe-segments-{tag}.json", RAW / f"qe-decoy-{tag}.json"
if not seg.exists() or not dec.exists():
ck(f"§2.2 сырьё QE ({tag}) на месте", False, f"нет qe-*-{tag}.json")
continue
d = json.loads(dec.read_text(encoding="utf-8"))
wn, wm, wp = want["decoy"]
ck(f"§2.2 {tag}: декой n={wn}, медиана {wm:+.3f}, направление {wp}/{wn}",
len(d) == wn and abs(statistics.median(d) - wm) < 0.01
and sum(1 for x in d if x > 0) == wp,
f"n={len(d)} медиана={statistics.median(d):.3f} "
f"плюсовых={sum(1 for x in d if x > 0)}")
srows = json.loads(seg.read_text(encoding="utf-8"))
by = {(r["window"], r["variant"], tuple(r["di"])): r["score"] for r in srows}
for cls in ("k1", "k2"):
want_n, want_pos = want[cls]
deltas = [sc - by[(w, "clean", di)] for (w, v, di), sc in by.items()
if v == cls and (w, "clean", di) in by
and abs(sc - by[(w, "clean", di)]) > 1e-9]
ck(f"§2.2 {tag}: парная Δ класса {cls}{want_n} сегментов, Δ>0 в {want_pos}",
len(deltas) == want_n and sum(1 for x in deltas if x > 0) == want_pos,
f"{len(deltas)} сегментов, Δ>0 в {sum(1 for x in deltas if x > 0)}")
clean = [r["score"] for r in srows if r["variant"] == "clean"]
ck(f"§2.2 {tag}: здоровых сегментов {want['clean']}", len(clean) == want["clean"],
str(len(clean)))
# §2.5 — калибровка гейта арма G. Считается из персистированных баллов, а не пере-моделируется:
# артефакт и есть сырьё, а пере-счёт стоил бы полчаса CPU без добавления проверки.
gu = RAW / "qe-guard-units-all.json"
if not gu.exists():
ck("§2.5 сырьё калибровки на месте", False, "нет qe-guard-units-all.json")
else:
import statistics # noqa: PLC0415
from math import comb # noqa: PLC0415
rows = json.loads(gu.read_text(encoding="utf-8"))
d = [r["final"] - r["draft"] for r in rows]
better = sum(1 for x in d if x < 0)
k = max(better, len(d) - better)
p = min(1.0, 2 * sum(comb(len(d), i) for i in range(k, len(d) + 1)) / (2 ** len(d)))
ck("§2.5 единичный уровень: n=91, улучшил 43, медиана Δ 0.0000",
len(d) == 91 and better == 43 and abs(statistics.median(d)) < 1e-9,
f"n={len(d)} улучшил={better} медиана={statistics.median(d):+.4f}")
ck("§2.5 знаковый тест p = 0.6752", abs(p - 0.6752) < 0.0005, f"{p:.4f}")
ck("§2.5 паритет |Δ|<0.25 в 55 единицах",
sum(1 for x in d if abs(x) < 0.25) == 55, str(sum(1 for x in d if abs(x) < 0.25)))
# §2.2 шаг 3 — мощность парного режима.
pw = RAW / "qe-power-large.json"
if not pw.exists():
ck("§2.2 сырьё мощности на месте", False, "нет qe-power-large.json")
else:
import statistics # noqa: PLC0415
rows = json.loads(pw.read_text(encoding="utf-8"))
d = [r["flipped"] - r["base"] for r in rows]
ck("§2.2 парная мощность: 72 пары, медиана +1.490, направление 66/72",
len(d) == 72 and abs(statistics.median(d) - 1.490) < 0.001
and sum(1 for x in d if x > 0) == 66,
f"n={len(d)} медиана={statistics.median(d):+.3f} "
f"плюсовых={sum(1 for x in d if x > 0)}")
# §2.3 — батарея. Пере-прогоняется по тому же корпусу целиком: это единственный способ
# заметить, что правило поехало после правки.
import battery as B # noqa: PLC0415
corp = Path.home() / "books" / "gu-zhenren" / "labels" / "raw" / "corpus.jsonl"
units = [json.loads(l) for l in corp.read_text(encoding="utf-8").splitlines() if l.strip()]
units = [u for u in units if (u.get("final") or "").strip()]
ck("§2.3 единиц с финалом 91", len(units) == 91, str(len(units)))
typo = han = lost = inv = cand = nonconf = calq = 0
for u in units:
f = u["final"]
typo += B.check_typography(f)["violations"]
han += B.check_cjk_leak(f)["han_chars"]
nm = B.check_numbers(u.get("source") or "", f)
lost += nm["lost_n"]
inv += nm["invented_n"]
p = B.check_palladius(f, set())
cand += p["name_candidates"]
nonconf += p["nonconformant_text"]
calq += bool(B.check_translationese(f)["calque_interjections"])
n = len(units)
for label, got, want in (("нарушений типографики/ед 0.18", typo / n, 0.18),
("ханьцзы всего 4", han, 4),
("потеряно величин/ед 0.27", lost / n, 0.275),
("появилось величин/ед 0.27", inv / n, 0.27),
("не-палладиевских срабатываний 6", nonconf, 6),
("единиц с кальками 0", calq, 0)):
ck(f"§2.3 {label}", abs(got - want) < 0.005, f"{got:.3f}" if isinstance(got, float)
else str(got))
ck("§2.3 кандидатов в имена 2184", cand == 2184, str(cand))
# §2.6 — вахта эффорта. Числа выводятся из сырья вызовов, а не из моей сводки.
ew = sorted(RAW.glob("ew-*.json"))
if not ew:
ck("§2.6 сырьё вахты на месте", False, "нет ew-*.json")
else:
recs = [json.loads(f.read_text(encoding="utf-8")) for f in ew]
by = {}
for r in recs:
by.setdefault(r["tag"].split("-")[1], []).append(r)
ck("§2.6 армы: дефолт 12 · low 12 · xhigh 1",
(len(by.get("default", [])), len(by.get("low", [])), len(by.get("xhigh", [])))
== (12, 12, 1),
str({k: len(v) for k, v in by.items()}))
ck("§2.6 все вызовы finish=stop", all(r["finish"] == "stop" for r in recs),
str(sorted({r["finish"] for r in recs})))
med = {k: statistics.median(r["reasoning_chars"] for r in v) for k, v in by.items()}
ck("§2.6 reasoning медианы: дефолт 2645.5 · low 2514 · xhigh 27185",
(med.get("default"), med.get("low"), med.get("xhigh")) == (2645.5, 2514, 27185),
str(med))
pt = {k: sorted({r["prompt_tokens"] for r in v}) for k, v in by.items()}
ck("§2.6 prompt_tokens: дефолт и low = 1945, xhigh = 2024 (серверная реакция)",
pt.get("default") == [1945] and pt.get("low") == [1945] and pt.get("xhigh") == [2024],
str(pt))
spent = sum(r["cost_usd"] for r in recs)
ck("§2.6 потрачено $0.047361 и потолок $0.05 НЕ пробит",
abs(spent - 0.047361) < 1e-6 and spent <= 0.05, f"${spent:.6f}")
ck("§2.6 запрос побайтно один во всех вызовах",
len({json.dumps(r["messages"], ensure_ascii=False, sort_keys=True)
for r in recs}) == 1, "запросы различаются — интерливинг недействителен")
ck("отчёт заявляет ту же сумму", "0.047361" in text)
# §2.7 — Ф1. Цена пере-считывается ИЗ usage, а не читается из записанной: именно расхождение
# этих двух путей и вскрыло аддитивный биллинг xAI.
from prices import cost as price_of # noqa: PLC0415
allscr = [json.loads(f.read_text(encoding="utf-8")) for f in sorted(RAW.glob("scr-*.json"))]
allscr = [r for r in allscr if not r.get("skipped")]
# Арм боевой конфигурации flash считается ОТДЕЛЬНО от скрина: он куплен после и по другой
# ручке. Смешивать их — значит потерять именно то различие, ради которого арм и заводился.
scr = [r for r in allscr if not r["tag"].startswith("flashlow-")]
flashlow = [r for r in allscr if r["tag"].startswith("flashlow-")]
if not scr:
ck("§2.7 сырьё скрина на месте", False, "нет scr-*.json")
else:
ck("§2.7 клеток скрина 48", len(scr) == 48, str(len(scr)))
ck("§2.7 арм боевой конфигурации flash: 4 клетки", len(flashlow) == 4, str(len(flashlow)))
ck("§2.7 арм flash(low): выход НЕ пуст ни в одной клетке",
all(r["content"].strip() for r in flashlow),
f"{sum(1 for r in flashlow if not r['content'].strip())} пустых")
recomputed = sum(price_of(r["model"], r["prompt_tokens"], r["cached_tokens"],
r["completion_tokens"], r["reasoning_tokens"])
for r in allscr)
ck("§2.7 пере-счёт цены Ф1 из usage даёт $1.074724",
abs(recomputed - 1.074724) < 1e-5, f"${recomputed:.6f}")
ck("§2.7 отчёт признаёт превышение потолка Ф1", "1.074724" in text and "7.5%" in text)
ck("§2.7 записанная цена совпадает с пере-счётом (аддитивный биллинг применён)",
abs(sum(r["cost_usd"] for r in allscr) - recomputed) < 1e-5,
f"записано ${sum(r['cost_usd'] for r in allscr):.6f}")
flash = [r for r in scr if r["model"] == "deepseek-v4-flash"]
ck("§2.7 deepseek-v4-flash на ДЕФОЛТЕ: 4/4 пустых при finish=length",
len(flash) == 4 and all(r["finish"] == "length" and not r["content"].strip()
for r in flash),
f"{sum(1 for r in flash if not r['content'].strip())}/{len(flash)} пустых")
grok = [r for r in scr if r["model"] == "grok-4.5"]
ck("§2.7 у grok-4.5 размышление ПРЕВЫШАЕТ completion (улика аддитивности)",
all(r["reasoning_tokens"] > r["completion_tokens"] for r in grok))
# Дисциплина отчёта: заявленные деньги обязаны сходиться с сырьём двумя путями.
ck("статус-баннер отчёта соответствует состоянию фаз",
"ВСЕ ФАЗЫ ИСПОЛНЕНЫ" in text)
# §2.9/§2.11 — бейк-офф и маршрутизация. Вердикты пере-считываются из ПЕРСИСТИРОВАННЫХ голосов,
# а не читаются из сводки: сводку писал я, голоса писал провод.
import judges as JJ # noqa: PLC0415
import bakeoff as BO # noqa: PLC0415
us = BO.units()
ck("§2.9 единиц бейк-оффа 8", len(us) == 8, str(len(us)))
for a, b, want in (("A", "F", (6, 0)), ("B", "F", (7, 0)),
("D", "A", (2, 0)), ("D", "D_", (3, 0))):
wa = wb = 0
for ui in range(len(us)):
pj = {}
for judge in JJ.JUDGES:
marg = []
for rep in (1, 2, 3):
o = (rep - 1) % 2
f = RAW / f"jv-bo-{a}v{b}-u{ui}-o{o}-{judge}-r{rep}.json"
if not f.exists():
continue
p = JJ.parse(json.loads(f.read_text(encoding="utf-8"))["content"])
if p["В1-ВЕРНОСТЬ"] is None:
continue
e1 = sum(p[f"В1-{x}"] or 0 for x in JJ.AXES)
e2 = sum(p[f"В2-{x}"] or 0 for x in JJ.AXES)
marg.append((e2 - e1) if o == 0 else (e1 - e2))
if len(marg) >= 2:
pos = sum(1 for m in marg if m > 0)
neg = sum(1 for m in marg if m < 0)
pj[judge] = 1 if pos >= 2 and pos > neg else (
-1 if neg >= 2 and neg > pos else 0)
if len(pj) < 2:
continue
v = set(pj.values())
wa += v == {1}
wb += v == {-1}
ck(f"§2.9 контраст {a} против {b} = {want[0]}:{want[1]}", (wa, wb) == want, f"{wa}:{wb}")
# §2.12 — карточки персонажей. Пин на то, что проверка рода НЕ инертна: пустые карточки
# молча возвращают нули, и именно так этот пробел прожил весь пак незамеченным.
cards = B.load_cards()
ck("§2.12 карточки построены из подписанного сида (51 ключ)", len(cards) == 51,
str(len(cards)))
ck("§2.12 проверка рода НЕ инертна на реальном тексте",
B.check_gender(units[0]["final"], cards)["checked"] > 0,
"0 сверок — карточки не доехали")
ra = RAW / "repair-arm.json"
if ra.exists():
r = json.loads(ra.read_text(encoding="utf-8"))
ck("§2.10 арм C: oracle 16/16, реальные 8/16",
r["oracle"] == [16, 16] and r["real"] == [8, 16], str(r))
else:
ck("§2.10 сырьё арма C на месте", False, "нет repair-arm.json")
rt = RAW / "route-arm.json"
if rt.exists():
r = json.loads(rt.read_text(encoding="utf-8"))
ck("§2.11 гейт флагает 5 черновиков из 8", len(r["flagged_F"]) == 5,
str(len(r["flagged_F"])))
ck("§2.11 арм E: найдено 6 естественных дефектов, починено 6",
(r["e_found"], r["e_fixed"]) == (6, 6), f"{r['e_found']}/{r['e_fixed']}")
else:
ck("§2.11 сырьё армов E/G на месте", False, "нет route-arm.json")
print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not FAILS else f'РАСХОЖДЕНИЙ: {FAILS}'}")
sys.exit(1 if FAILS else 0)
if __name__ == "__main__":
main()