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