textmachine/eval/dovodka/spanjoin.py

130 lines
6.8 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
"""СПАН-ДЖОЙН: судейская ось против детерминированной. $0.
Единственная форма, в которой обе оси говорят об ОДНОМ. У детерминированных флагов есть МЕСТА
(термин банка, слово-нарушитель, потерянная величина), у судейских находок — ЦИТАТЫ. Пересечение
даёт число, прямо решающее спор H-2а против H-2б:
доля подтверждённых ошибок, лежащих ВНЕ покрытия детерминированных флагов.
Близко к нулю — «флагами всё отслеживается», флагового контура достаточно и смысловой критик
не нужен. Велика — нужен критик, и известно НАСКОЛЬКО. Прибор заведён приёмкой другого модельного
семейства (Д0.13 П-8) и стоит $0: цитаты уже обязательны, места уже вычисляются.
⚠ ЧТО ЭТО НЕ МЕРЯЕТ. Не «правильно ли судья счёл это ошибкой» — для этого адъюдикация. Здесь
берётся то, что судья НАЗВАЛ ошибкой и что подтверждено присутствием цитаты в своём варианте,
и спрашивается только одно: попадала ли эта ошибка в поле зрения детерминированного гейта.
⚠ ГРАНИЦА ИЗМЕРЕНИЯ. Флаг называет ТЕРМИН или СЛОВО, а цитата судьи — фразу. Совпадением
считается вхождение флагованного слова в цитату. Это НИЖНЯЯ оценка покрытия: флаг мог указать на
место, которое судья процитировал другими словами. Значит истинная доля «вне флагов» не больше
печатаемой, и вывод «флагов не хватает» от этой границы только консервативнее.
Запуск: eval/.venv/bin/python eval/dovodka/spanjoin.py
"""
from __future__ import annotations
import importlib.util
import json
import re
import statistics as st
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
sys.path.insert(0, str(REPO / "eval" / d))
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
S = _load("sud", ZONE / "sud.py")
C = S.C
KEYS = Path.home() / "books" / "dovodka" / "blind-keys-d4"
FAMILIES = {"claude": Path.home() / "books" / "judging" / "sud-d4",
"sol": Path.home() / "books" / "judging" / "sol-d4-work"}
_norm = re.compile(r"[\s«»„“”\"'`.,!?;:—–-]+")
def norm(t: str) -> str:
return _norm.sub(" ", (t or "").lower()).strip()
def flag_words(u: dict, text: str) -> set[str]:
"""Слова, на которые указывает детерминированный гейт: канон-термины + адреса батареи."""
out: set[str] = set()
for g in C.canon_gaps(u["source"], text):
m = re.search(r"«([^»]+)»", g)
if m:
out.add(norm(m.group(1)))
for f in C.battery_flags(u, text):
for m in re.finditer(r"«([^»]+)»", f):
out.add(norm(m.group(1)))
return {w for w in out if len(w) >= 3}
def main() -> int:
us = {x["uid"]: x for x in S.units()}
print("СПАН-ДЖОЙН — попадают ли судейские находки в поле зрения флагов\n")
for fam, root in FAMILIES.items():
if not root.exists():
continue
per_arm: dict[str, list[float]] = {}
for half, _hi in (("p1", 1), ("p2", 2)):
key_all = json.loads((KEYS / f"d4{half}-KEY.json").read_text(encoding="utf-8"))
for f in sorted((root / f"{half}-answers").glob("*.txt")):
if ".ANNULLED" in f.name:
continue
key = key_all.get(f.stem)
if not key:
continue
txt = f.read_text(encoding="utf-8", errors="replace")
for lab, meta in key.items():
arm, uid = meta["arm"], meta["uid"]
if arm.startswith("CTRL"):
continue
u = us.get(uid)
if not u:
continue
var = S.text_of(arm, u) if arm != "A0" else C.base_a0(uid)
if not var.strip():
continue
# флаги считаются на ВХОДЕ того арма, чей контур мы проверяем
base = (C.base_a0(uid) if arm == "A4"
else C.base_draft2(uid) if arm.startswith("A6")
else C.base_draft(uid))
if not base.strip():
continue
fw = flag_words(u, base)
if not fw:
continue
w = re.search(rf"^{lab}-ПОЧЕМУ:\s*(.*)$", txt, re.M)
if not w:
continue
qs = [norm(q) for q in re.findall(r"«([^»]{6,})»", w.group(1))]
qs = [q for q in qs if q and q in norm(var)] # только подтверждённые
if not qs:
continue
outside = sum(1 for q in qs if not any(x in q for x in fw))
per_arm.setdefault(arm, []).append(outside / len(qs))
print(f"### {fam.upper()}")
print(f" {'арм':6s}{'единиц':>8s}{'ошибок ВНЕ покрытия флагов':>30s}")
for arm in ("A0", "A1", "A1B", "A2", "A3", "A4", "A6", "A6F"):
v = per_arm.get(arm)
if v:
print(f" {arm:6s}{len(v):8d}{st.mean(v):29.0%}")
print()
print("⚠ Это НИЖНЯЯ оценка доли «вне флагов»: флаг называет слово, судья цитирует фразу,")
print(" и совпадение засчитывается по вхождению слова в цитату. Истинная доля не меньше.")
return 0
if __name__ == "__main__":
sys.exit(main())