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