textmachine/eval/pkg7/audit_phaseB.py

204 lines
13 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
"""Полигон, пакет-7: АДВЕРСАРИАЛЬНАЯ ревизия собственной фазы B (мандат самопроверки 12.07).
Зачем отдельным скриптом. Объективный слой отчёта B (§B1) считался разовыми heredoc-ами, то есть
именно тот слой, на который отчёт опёр ВСЕ выводы после отбраковки судей, не имел воспроизводимого
кодового пути. Здесь он есть, и здесь же — проверки, которых в фазе B не было и которые её выводы
частично опровергают.
Что проверяется (каждая проверка печатает и замер, и то, что заявлял отчёт):
1. §B1.1 совпадение с подписью владельца — пересчёт независимым кодом;
2. покрытие набора жанровым словарём — включая гипотетический ПОЛНЫЙ словарь §Z4-Т;
3. состав промпта КАЖДОГО арма по СЫРЬЮ — какой блок в каком арме реально был;
4. вырожденные айтемы: сколько термов, где все кандидаты — одна и та же строка;
5. измерим ли вообще контроль §A2.4 п.4 (уникальна ли строка GOLD среди кандидатов);
6. пере-счёт BWS ТЕМ ЖЕ скорером на невырожденных айтемах — F1/F1b переворачиваются;
7. катастрофы на GOLD: пометил ли судья одинаковые строки одинаково;
8. чем на самом деле объясняется разрыв прокси §B1.3 на ловушке S5;
9. деньги из сырья каждого вызова против леджера отчёта.
Запуск: audit_phaseB.py --dir <скретчпад/phaseB> [--z4t <файл со списком src полного словаря>]
Сети не требует, платных вызовов не делает.
"""
from __future__ import annotations
import argparse
import collections
import json
import re
import sys
from pathlib import Path
ARMS = ["P0b", "P0", "P1", "P2", "P3", "P4"]
# Полный список src таблицы §Z4-Т (жанровый словарь «как если бы подписали всё»).
Z4T_SRC = ["修炼", "修真", "修行", "修士", "修为", "真气", "灵气", "筑基", "金丹", "丹田", "元神",
"元婴", "经脉", "穴位", "心法", "秘籍", "功法", "法宝", "丹药", "符箓", "轻功", "内功",
"内力", "境界", "飞升", "天劫", "灵石", "长老", "掌门", "剑修", "", "", "", "江湖",
"走火入魔", "资质", "凡人"]
# Маркеры блоков промпта — по ним видно, что арм РЕАЛЬНО получил (а не что задумывалось).
BLOCKS = {
"правило-имени": "если термин — имя собственное, передавай его транскрипцией",
"Палладий": "система Палладия",
"лемма": "Русский — язык с богатой морфологией",
"словарь": "Жанровый словарь пары",
"рубрика": "K1 верность концепту (то ли это понятие)",
"справка": "СЛОВАРНАЯ СПРАВКА из китайско-русского словаря",
"KWIC": " контекст: ",
}
PRICE = {"deepseek-v4-flash": (0.14, 0.28, 0.0028)}
def norm(s: str | None) -> str:
return re.sub(r"\s+", " ", (s or "").strip().lower())
def bws(rows: list[dict], crit: str, subset: set[str] | None = None) -> dict[str, float]:
"""Нормировка ровно как в score_bws.py: (плюсы минусы) / число появлений арма."""
plus, minus, seen = collections.Counter(), collections.Counter(), collections.Counter()
for r in rows:
if subset is not None and r["id"] not in subset:
continue
v = (r.get("verdicts") or {}).get(crit)
if not isinstance(v, dict):
continue
l2a = r["letter2arm"]
for arm in l2a.values():
seen[arm] += 1
b, w = l2a.get(str(v.get("best"))), l2a.get(str(v.get("worst")))
if b:
plus[b] += 1
if w:
minus[w] += 1
return {a: (plus[a] - minus[a]) / n for a, n in seen.items() if n}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--dir", required=True, type=Path, help="каталог фазы B (arms_final.json, j*.json, raw/)")
a = ap.parse_args()
D = a.dir
res = json.loads((D / "arms_final.json").read_text(encoding="utf-8"))["results"]
by, gold = collections.defaultdict(dict), {}
for r in res:
by[r["id"]][r["arm"]] = (r["dst"] or "").strip()
if r.get("gold"):
gold[r["id"]] = r["gold"].strip()
def cands(tid: str) -> dict[str, str]:
m = dict(by[tid])
if tid in gold:
m["GOLD"] = gold[tid]
return m
print(f"=== 1. §B1.1 совпадение с подписью (термов с эталоном: {len(gold)}) ===")
hit, tot = collections.Counter(), collections.Counter()
per = collections.defaultdict(collections.Counter)
pert = collections.defaultdict(collections.Counter)
for r in res:
if not r.get("gold"):
continue
tot[r["arm"]] += 1
pert[r["sample"]][r["arm"]] += 1
if norm(r["dst"]) == norm(r["gold"]):
hit[r["arm"]] += 1
per[r["sample"]][r["arm"]] += 1
for arm in ARMS:
print(f" {arm:4s} {hit[arm]}/{tot[arm]} = {hit[arm]/tot[arm]:.3f}")
for s in sorted(pert):
print(f" {s}: " + " ".join(f"{x}={per[s][x]}/{pert[s][x]}" for x in ARMS))
print(" отчёт §B1.1: 12/19 · 19/19 · 18/19 · 18/19 · 17/19 · 17/19")
print("\n=== 2. покрытие набора жанровым словарём ===")
srcs = {r["src"] for r in res}
fed = sorted(srcs & {"修真", "修行", "丹田", "经脉", "灵气", "金丹"})
full = sorted(srcs & set(Z4T_SRC))
print(f" 6 строк, реально уехавших в промпт: {len(fed)}/80 {fed}")
print(f" ПОЛНЫЙ словарь §Z4-Т (37 строк): {len(full)}/80 {full}")
print(" ⇒ вывод §B1.2 (словарь инертен на этом наборе) от подачи НЕ зависит")
print("\n=== 3. состав промпта каждого арма ПО СЫРЬЮ ===")
first: dict[str, str] = {}
for p in sorted((D / "raw").glob("*.json")) + sorted((D / "raw_P4").glob("*.json")):
r = json.loads(p.read_text(encoding="utf-8"))
first.setdefault(r["arm"], r["prompt"])
for arm in ARMS:
pr = first.get(arm, "")
print(f" {arm:4s} " + " ".join(f"{k}={'ДА' if v in pr else '--'}" for k, v in BLOCKS.items()))
print(" ⇒ ПРАВИЛО ИМЕНИ СОБСТВЕННОГО есть во ВСЕХ шести армах: контрольного арма без него НЕТ")
print("\n=== 4-5. вырожденные айтемы и измеримость контроля §A2.4 п.4 ===")
deg = {t for t in by if len({norm(v) for v in cands(t).values()}) == 1}
nondeg = set(by) - deg
uniq = [t for t in gold if norm(gold[t]) not in {norm(v) for v in by[t].values()}]
print(f" термов всего {len(by)}; ВСЕ кандидаты идентичны у {len(deg)}; невырожденных {len(nondeg)}")
print(f" из {len(gold)} термов с GOLD полностью вырождены {len(deg & set(gold))}")
print(f" термов, где строка GOLD УНИКАЛЬНА среди кандидатов: {len(uniq)}/{len(gold)}")
print(" ⇒ контроль «отличает ли судья подпись от случайного варианта» на этом наборе НЕИЗМЕРИМ")
jf = [D / "j1.json", D / "j2.json"]
rows = [r for f in jf for r in json.loads(f.read_text(encoding="utf-8"))["rows"] if not r.get("reverse")]
print("\n=== 6. BWS тем же скорером: все айтемы против невырожденных ===")
for label, sub in [("все 80 (как в отчёте)", None), ("45 невырожденных", nondeg)]:
k1 = bws(rows, "K1", sub)
d1 = abs(k1.get("P1", 0) - k1.get("P0b", 0))
d1b = abs(k1.get("P2", 0) - k1.get("P0", 0))
print(f" [{label}] " + " · ".join(f"{a} {v:+.3f}" for a, v in sorted(k1.items(), key=lambda x: -x[1])))
print(f" F1 |P1P0b|={d1:.3f}{'ПРОВАЛ' if d1 < 0.10 else 'НЕ провал'} "
f"F1b |P2P0|={d1b:.3f}{'ПРОВАЛ' if d1b < 0.10 else 'НЕ провал'} (порог 0.10)")
print(" ⇒ на невырожденных айтемах оба критерия переворачиваются, а P0b встаёт ПОСЛЕДНИМ —")
print(" то есть судьи разрыв «контексты работают» ВИДЯТ; его прятали вырожденные термы")
print("\n=== 7. катастрофы на GOLD: согласованность судьи ===")
j1 = json.loads((D / "j1.json").read_text(encoding="utf-8"))["rows"]
ncat = 0
for row in j1:
l2a = row.get("letter2arm") or {}
inv = {v: k for k, v in l2a.items()}
if "GOLD" not in inv or inv["GOLD"] not in (row.get("catastrophic") or []):
continue
ncat += 1
c = cands(row["id"])
twin = sorted(L for L, arm in l2a.items() if norm(c.get(arm, "")) == norm(c["GOLD"]))
cat = sorted(row.get("catastrophic") or [])
print(f" {row['id']:22s} {c['GOLD']!r:26s} букв с тем же текстом={twin} помечено={cat}"
f" {'СОГЛАСОВАНО' if twin == cat else '← РАСХОЖДЕНИЕ'}")
print(f" всего катастроф на GOLD: {ncat} — и это несогласие судьи с каноном ПО СУЩЕСТВУ,")
print(" а не неспособность отличить подпись: одинаковый текст он метит одинаково")
print("\n=== 8. чем объясняется разрыв прокси §B1.3 на ловушке S5 ===")
s5 = collections.defaultdict(dict)
for r in res:
if r["sample"] == "S5":
s5[r["src"]][r["arm"]] = r["dst"]
for src, m in s5.items():
n = len({norm(v) for v in m.values()})
mark = " ← ЕДИНСТВЕННЫЙ источник разрыва P0 против P2/P3/P4" if src == "春秋蝉" else ""
print(f" {src:6s} различных вариантов={n}{mark}")
print(" ⇒ строка S5 прокси НЕ является уликой, независимой от наблюдения по 春秋蝉 (§B1.4)")
print("\n=== 9. деньги из сырья против леджера ===")
pi, po, pc = PRICE["deepseek-v4-flash"]
grand = 0.0
for sub, label, claim in [("raw", "армы, первый прогон", 0.07791), ("raw_P4", "P4 пере-прогон", 0.01413)]:
ti = to = tc = 0
for p in sorted((D / sub).glob("*.json")):
u = json.loads(p.read_text(encoding="utf-8"))["usage"]
ti += u["in"]; to += u["out"]; tc += u.get("cached") or 0
usd = (ti - tc) / 1e6 * pi + tc / 1e6 * pc + to / 1e6 * po
grand += usd
print(f" {label:22s} in={ti:7d} out={to:6d} cached={tc:6d} ${usd:.5f} (отчёт ${claim:.5f})")
for f in ["j1.json", "j2.json", "j3.json"]:
led = json.loads((D / f).read_text(encoding="utf-8"))["ledger"]
usd = sum(x["usd"] for x in led)
grand += usd
nofin = sum(1 for x in led if x.get("finish") != "stop")
print(f" {f:22s} вызовов={len(led)} ${usd:.5f} finish!=stop={nofin}")
print(f" ИТОГО ${grand:.5f} (отчёт $0.31030)")
print(" NB: сырьё СУДЕЙ содержит только id/prompt/response — usage у них живёт лишь в леджере,")
print(" вопреки формулировке §B0.4 «сырьё каждого вызова … usage … сохраняется целиком»")
return 0
if __name__ == "__main__":
raise SystemExit(main())