textmachine/eval/editor_tier/solscore.py

118 lines
5.8 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
"""ПЕРЕ-СЧЁТ ГОЛОСОВ ВНЕШНЕГО СУДЬИ (не-Claude) ПО ПРОХОДУ border. $0.
Судья другой семьи отсуживал ТЕ ЖЕ файлы заданий, что и агенты-судьи пака (`aj-border-tasks/`), и
клал ответы в `sol-border/`. Поэтому его счёт сопоставим с моим текст-в-текст, а не «другой замер».
⚠ Разбор здесь СВОЙ, а не `absjudge.ingest`: тот пишет в боевой каталог голосов и затёр бы прогон
пака. Правила приёмки ответа те же — каждая ненулевая ось названа и подпёрта цитатой.
Запуск: eval/.venv/bin/python eval/editor_tier/solscore.py
"""
from __future__ import annotations
import json
import math
import random
import re
import statistics as st
import sys
from pathlib import Path
OUT = Path.home() / "books" / "editor-tier"
# Проход задаётся аргументом: `border` (исполнен 09.08) и `tier` (заказан фазой Д, п.Д2).
PASS = "border"
KEY = OUT / "blind-keys" / f"{PASS}-KEY.json"
ANSW = OUT / f"sol-{PASS}"
AXES = ("ВЕРНОСТЬ", "ТЕРМИН", "ЯЗЫК", "ФОРМА")
SEED = 20260809
def votes() -> tuple[dict, list]:
key = json.loads(KEY.read_text(encoding="utf-8"))
ok, bad = {}, []
for f in sorted(ANSW.glob("*.txt")):
k = key.get(f.stem)
if not k:
bad.append((f.stem, "токена нет в ключе"))
continue
txt, unit, fail = f.read_text(encoding="utf-8"), {}, None
for lab, meta in k.items():
ax = {}
for a in AXES:
m = re.search(rf"^{lab}-{a}[ \t]*:[ \t]*(\d+)[ \t]*$", txt, re.M)
if m:
ax[a] = int(m.group(1))
if len(ax) != len(AXES):
fail = f"не разобран блок {lab}"
break
wm = re.search(rf"^{lab}-ПОЧЕМУ[ \t]*:[ \t]*(.*?)(?=^\S+-(?:{'|'.join(AXES)}|ПОЧЕМУ)"
rf"[ \t]*:|\Z)", txt, re.M | re.S)
why = (wm.group(1) if wm else "").strip()
miss = [a for a in AXES if ax[a] and not re.search(rf"(?<!\w){a}(?!\w)", why.upper())]
if miss:
fail = f"ось без обоснования: {'/'.join(miss)}"
break
if any(ax.values()) and not re.search(r"\"“„].+?[»\"”]", why):
fail = "обоснование без цитаты"
break
unit[meta["arm"]] = dict(errors=sum(ax.values()), decoy_from=meta.get("decoy_from"))
if fail:
bad.append((f.stem, fail))
continue
ok[next(iter(k.values()))["uid"]] = unit
return ok, bad
def sign_p(m: list[float]) -> float:
"""Точный при малом n, иначе сид-Монте-Карло: воспроизводимость важнее последней цифры."""
rnd, s, hits, trials = random.Random(SEED), sum(m), 0, 200_000 # noqa: S311
for _ in range(trials):
if abs(sum(x * rnd.choice((1, -1)) for x in m)) >= abs(s) - 1e-12:
hits += 1
return (hits + 1) / (trials + 1)
def main() -> int:
global PASS, KEY, ANSW # noqa: PLW0603
args = [a for a in sys.argv[1:] if not a.startswith("-")]
if args:
PASS = args[0]
KEY, ANSW = OUT / "blind-keys" / f"{PASS}-KEY.json", OUT / f"sol-{PASS}"
print(f"проход {PASS} · ключ {KEY.name} · ответы {ANSW}")
if not ANSW.exists() or not list(ANSW.glob("*.txt")):
print(f"ответов внешнего судьи нет в {ANSW}")
return 1
ok, bad = votes()
print(f"принято единиц {len(ok)} · отвергнуто {len(bad)}")
for t, r in bad:
print(f"{t}: {r}")
dec = [u[u["CTRLdecoy"]["decoy_from"]]["errors"] - u["CTRLdecoy"]["errors"] for u in ok.values()
if "CTRLdecoy" in u and u["CTRLdecoy"].get("decoy_from") in u]
flo = [u["CTRLfloorA"]["errors"] - u["CTRLfloorB"]["errors"] for u in ok.values()
if "CTRLfloorA" in u and "CTRLfloorB" in u]
# Семейство прохода: у `border` один контраст, у `tier` — три плюс позитивный контроль.
fam = ([("R2", "R0")] if PASS == "border"
else [("T1", "R0"), ("T2", "R0"), ("T3", "R0"), ("R0", "F")])
pairs = [(a, b, [u[b]["errors"] - u[a]["errors"] for u in ok.values() if a in u and b in u])
for a, b in fam]
pairs = [(a, b, m) for a, b, m in pairs if len(m) >= 3]
if not (dec and flo and pairs):
print("контролей или боевых армов не хватает")
return 1
caught = sum(1 for x in dec if x < 0)
thr = 2.8 * st.pstdev(flo) / math.sqrt(len(flo))
print(f"\n ДЕКОЙ {st.mean(dec):+.2f} · поймано {caught}/{len(dec)}"
+ ("" if caught == len(dec) else " ⛔ сессия внешнего судьи аннулируется"))
print(f" ПОЛ {st.mean(flo):+.2f} · n={len(flo)} sd={st.pstdev(flo):.2f} → ПОРОГ {thr:.2f}")
for a, b, m in pairs:
print(f" {a} vs {b} n={len(m)} {st.mean(m):+.2f} p={sign_p(m):.4f} "
f"{'РАЗЛИЧИМ' if abs(st.mean(m)) > thr else 'ниже порога различимости'}")
battle = {x for a, b, _ in pairs for x in (a, b)}
lv = [r["errors"] for u in ok.values() for a, r in u.items() if a in battle]
print(f" уровень (медиана боевых клеток) {st.median(lv):.1f}")
return 0 if caught == len(dec) else 1
if __name__ == "__main__":
sys.exit(main())