299 lines
19 KiB
Python
299 lines
19 KiB
Python
#!/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()
|