#!/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() / "sud-d4", "sol": Path.home() / "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())