Freeze the absolute judging rig before the second pass: family pinned into the key, salt out of the repo, per-axis justification, floor verdict, position control, judge identity, ledger covering the arm tags it pays
This commit is contained in:
parent
0eefc97df8
commit
6d79dfbd83
4 changed files with 656 additions and 1 deletions
566
eval/role_topology/absjudge.py
Normal file
566
eval/role_topology/absjudge.py
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""АБСОЛЮТНЫЙ СУДЕЙСКИЙ РИГ. $0 по ключам провайдеров.
|
||||||
|
|
||||||
|
⚠ Заведён 07.08 по находке ЧЕТЫРЁХ независимых судей-агентов. Каждому была выдана своя пачка
|
||||||
|
ПАРНЫХ пакетов «исходник + вариант 1 + вариант 2»; ни один не знал о трёх других. Все четыре
|
||||||
|
самостоятельно свернули пары обратно: дедуплицировали уникальные тексты, оценили КАЖДЫЙ ОДИН РАЗ
|
||||||
|
против исходника и разнесли числа по пакетам. Четыре из четырёх — это не сбой исполнителя, это
|
||||||
|
поведение инструмента, и оснастка обязана следовать за ним, а не бороться с ним.
|
||||||
|
|
||||||
|
ЧТО ДАЁТ ПЕРЕХОД НА АБСОЛЮТНУЮ ШКАЛУ.
|
||||||
|
|
||||||
|
1. **Позиция исчезает по построению.** Текст оценивается вне пары, значит «первый/второй» не
|
||||||
|
существует как понятие. Зеркальная раскладка, которую я чинил трижды и трижды получал
|
||||||
|
вырождение, здесь не нужна — нечего зеркалить.
|
||||||
|
2. **Работа падает втрое.** 160 парных пакетов = 320 прочтений; уникальных текстов 128. Причём
|
||||||
|
парная схема покупала 4 контраста, а абсолютная даёт ВСЕ 7 объявленных, включая деконфаунды.
|
||||||
|
3. **Строгость калибрована внутри единицы.** Все варианты ОДНОГО исходника судятся в одном
|
||||||
|
заходе, а контраст считается ВНУТРИ единицы и лишь затем усредняется по единицам. Дрейф
|
||||||
|
строгости между единицами в контраст не попадает — он сокращается вычитанием.
|
||||||
|
|
||||||
|
ЧЕГО ЭТО НЕ ЧИНИТ. Шум ОДНОГО вызова велик: два прогона одного и того же арма на одном входе
|
||||||
|
расходятся на sd≈2.7 ошибки по единице (контроль пола, n=16). Значит на ОТДЕЛЬНОЙ главе армы
|
||||||
|
надёжно не упорядочены — пере-прогон сдвинет счёт сильнее, чем разница между армами. Упорядочено
|
||||||
|
только среднее: парный дизайн делит шум на √n, и при 16 единицах различимый эффект ≈1.9.
|
||||||
|
⚠ Первая версия этой шапки утверждала, что эффект ниже двух не различим «никаким числом единиц».
|
||||||
|
Это неверно и снято собственным замером: порог кладёт стандартная ошибка среднего, а она падает
|
||||||
|
с ростом n. Формулировка была завышением порога и держалась ровно до того, как пол измерили.
|
||||||
|
|
||||||
|
СЛЕПОТА. Метки Т1..Тn внутри единицы, порядок перемешан хешем соли и uid: из метки не выводится
|
||||||
|
ни арм, ни то, редактированный это текст или черновик. Ключ лежит отдельно и судье не даётся.
|
||||||
|
|
||||||
|
ПЕРСИСТ. Судья пишет ответ файлом САМ, в `aj-abs/<токен>.txt`. Так убирается моя транскрипция —
|
||||||
|
тот самый канал, по которому в третьей редакции потерялись обоснования, а слепое чтение не
|
||||||
|
попало на диск вовсе. `ingest` только разбирает и проверяет; голос с ненулевой осью и пустым
|
||||||
|
обоснованием отвергается (условие 2 амендмента владельца 07.08).
|
||||||
|
|
||||||
|
Запуск: --selfcheck проверка разбора и арифметики на входах с известным ответом, $0
|
||||||
|
--emit выпустить задания судьям, $0
|
||||||
|
--ingest разобрать ответы в голоса, $0
|
||||||
|
--controls что показали контроли (читать ДО боевых чисел), $0
|
||||||
|
--score контрасты с поправкой Холма, $0
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import statistics as st
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path("/home/ubuntu/projects/textmachine")
|
||||||
|
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
|
||||||
|
|
||||||
|
import bakeoff as BO # noqa: E402
|
||||||
|
import judges as J # noqa: E402
|
||||||
|
|
||||||
|
OUT = Path.home() / "books" / "role-topology"
|
||||||
|
KEYS = OUT / "blind-keys"
|
||||||
|
|
||||||
|
|
||||||
|
def _dirs():
|
||||||
|
"""Каталоги прохода. ВТОРОЙ проход обязан лежать отдельно: он и есть репликация,
|
||||||
|
и затирание первого превратило бы её в пере-запись."""
|
||||||
|
return (OUT / f"aj-{PASS}-tasks", OUT / f"aj-{PASS}", OUT / f"aj-{PASS}-votes")
|
||||||
|
# ⚠ СОЛЬ ЖИВЁТ ВНЕ РЕПОЗИТОРИЯ. Адверсариальное ревью 07.08 восстановило 138 меток из 138, не
|
||||||
|
# открывая ключ: `layout()` — чистая функция от соли и `uid = sha1(source)`, а источник лежит В
|
||||||
|
# ЗАДАНИИ. Пока соль была константой в коде, слепота держалась только запретом судье читать код.
|
||||||
|
# Теперь она в файле рядом с ключом, куда судье ходить запрещено тем же правилом, что и к ключу.
|
||||||
|
def _salt(name: str = "abs") -> str:
|
||||||
|
f = Path.home() / "books" / "role-topology" / "blind-keys" / f"SALT-{name}.txt"
|
||||||
|
if not f.exists():
|
||||||
|
import secrets # noqa: PLC0415
|
||||||
|
f.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
f.write_text(secrets.token_hex(16), encoding="utf-8")
|
||||||
|
return f.read_text(encoding="utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
|
PASS = "abs" # переключается --emit2 на вторую пачку
|
||||||
|
SALT = _salt(PASS)
|
||||||
|
|
||||||
|
# Армы, попадающие в пачку. F3 — собственный черновик пака (текст и цена из одного вызова).
|
||||||
|
# D/D_ берутся из второй редакции СОЗНАТЕЛЬНО: однопроходки черновика не видят, их вход
|
||||||
|
# (исходник + блок закона) в обеих редакциях побайтно один, и это проверяется в selfcheck.
|
||||||
|
ARMS = ("F3", "A", "A_law", "B", "D", "D_")
|
||||||
|
|
||||||
|
# СЕМЕЙСТВО КОНТРАСТОВ, объявленное ДО первого ответа судьи. Поправка Холма считается по всем
|
||||||
|
# семи; сузить семейство после того, как увидены p-значения, — это и есть p-hacking, и именно
|
||||||
|
# за него снята первая редакция. Знак: перевес > 0 означает «ПЕРВЫЙ арм лучше».
|
||||||
|
FAMILY = [
|
||||||
|
("A", "F3", "редактор поверх черновика против самого черновика"),
|
||||||
|
("B", "F3", "другой движок-редактор поверх ТОГО ЖЕ черновика"),
|
||||||
|
("D_", "A", "однопроходка против связки черновик+редактор"),
|
||||||
|
("D", "A", "однопроходка с переливкой абзацев против связки"),
|
||||||
|
("A_law", "A", "деконфаунд: сколько покупает банкнота-как-закон"),
|
||||||
|
("A_law", "B", "два движка, оба с законом"),
|
||||||
|
("D", "D_", "деконфаунд: сколько покупает переливка абзацев"),
|
||||||
|
]
|
||||||
|
|
||||||
|
FORMAT_HEAD = """Оцени КАЖДЫЙ вариант ОТДЕЛЬНО против исходника — не сравнивая варианты между
|
||||||
|
собой. Порядок вариантов ничего не значит и специально перемешан.
|
||||||
|
|
||||||
|
Ответ ЗАПИШИ ФАЙЛОМ в {path} — ровно в таком виде и ничем больше, блок на каждую метку:
|
||||||
|
|
||||||
|
{skeleton}
|
||||||
|
ПОЧЕМУ обязательно для КАЖДОЙ ненулевой оси: короткая ЦИТАТА из текста через « | ».
|
||||||
|
Число без цитаты не принимается и на диск не попадает: именно из-за отсутствия цитат показания
|
||||||
|
прошлых судей оказались непроверяемыми. Ноль по оси обоснования не требует.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def utoken(uid: str) -> str:
|
||||||
|
return hashlib.sha1(f"{_salt(PASS)}|unit|{uid}".encode()).hexdigest()[:10] # noqa: S324
|
||||||
|
|
||||||
|
|
||||||
|
def layout(uid: str, slots: list[str]) -> list[str]:
|
||||||
|
"""Порядок меток внутри единицы: детерминированная перестановка по хешу, не по алфавиту."""
|
||||||
|
return sorted(slots, key=lambda s: hashlib.sha1(f"{_salt(PASS)}|{uid}|{s}".encode()).hexdigest()) # noqa: S324
|
||||||
|
|
||||||
|
|
||||||
|
def build_unit(u: dict) -> tuple[str, dict]:
|
||||||
|
"""Задание на одну единицу + ключ раскладки. Контроли подмешаны и неотличимы от боевых."""
|
||||||
|
slots: dict[str, str] = {a: BO.text3_of(a, u) for a in ARMS}
|
||||||
|
kinds: dict[str, str] = {a: "боевой" for a in ARMS}
|
||||||
|
|
||||||
|
clean = slots["A"]
|
||||||
|
planted, n = _plant(clean)
|
||||||
|
if n >= 2:
|
||||||
|
slots["CTRLdecoy"], kinds["CTRLdecoy"] = planted, "контроль-декой"
|
||||||
|
# ⚠ ПОЛ КЛАДЁТСЯ ПАРОЙ, а не против боевого арма `A`. Поймано чтением кода 07.08: `bo3-Aprime`
|
||||||
|
# куплен по `build_msgs` (черновик ИЗ КОРПУСА), а боевой `A` третьей редакции — по `build_msgs3`
|
||||||
|
# (СОБСТВЕННЫЙ черновик). Их входы различаются, и пол мерил бы разницу черновиков, а не шум
|
||||||
|
# вызова. Повтором `Aprime` является `bo2-A`, и именно он идёт второй половиной пары.
|
||||||
|
fa, fb = OUT / f"bo2-A-{u['uid']}.json", OUT / f"bo3-Aprime-{u['uid']}.json"
|
||||||
|
if fa.exists() and fb.exists():
|
||||||
|
slots["CTRLfloorA"] = json.loads(fa.read_text(encoding="utf-8"))["content"]
|
||||||
|
slots["CTRLfloorB"] = json.loads(fb.read_text(encoding="utf-8"))["content"]
|
||||||
|
kinds["CTRLfloorA"] = kinds["CTRLfloorB"] = "контроль-пол"
|
||||||
|
|
||||||
|
order = layout(u["uid"], list(slots))
|
||||||
|
label = {name: f"Т{i + 1}" for i, name in enumerate(order)}
|
||||||
|
skel = "\n".join(f"{label[n]}-ВЕРНОСТЬ: <число>\n{label[n]}-ТЕРМИН: <число>\n"
|
||||||
|
f"{label[n]}-ЯЗЫК: <число>\n{label[n]}-ФОРМА: <число>\n"
|
||||||
|
f"{label[n]}-ПОЧЕМУ: <цитаты>\n" for n in order)
|
||||||
|
tok = utoken(u["uid"])
|
||||||
|
# Шапка своя, а не `J.RUBRIC`: та написана под ПАРУ («два русских перевода», «два варианта»)
|
||||||
|
# и в абсолютной раскладке, где меток до восьми, противоречила бы заданию. Определения осей
|
||||||
|
# берутся из неё дословно — меняется только обрамление.
|
||||||
|
axes_def = J.RUBRIC.split("Посчитай ОШИБКИ", 1)[1].split("Отвечай РОВНО")[0].rstrip()
|
||||||
|
ANSW = _dirs()[1]
|
||||||
|
body = [f"Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК на китайском и {len(order)} "
|
||||||
|
f"независимых русских перевода этого же фрагмента под слепыми метками.",
|
||||||
|
"", "Посчитай ОШИБКИ" + axes_def,
|
||||||
|
"", FORMAT_HEAD.format(path=ANSW / f"{tok}.txt", skeleton=skel),
|
||||||
|
"", "=" * 60, "ИСХОДНИК:", u["source"]]
|
||||||
|
for n in order:
|
||||||
|
body += ["", "=" * 60, f"{label[n]}:", slots[n]]
|
||||||
|
key = {label[n]: dict(arm=n, kind=kinds[n], uid=u["uid"],
|
||||||
|
sig=hashlib.sha1(slots[n].encode()).hexdigest()[:12]) # noqa: S324
|
||||||
|
for n in order}
|
||||||
|
return "\n".join(body), key
|
||||||
|
|
||||||
|
|
||||||
|
_PLANTS = [
|
||||||
|
(r"\bне\s+(выдерж\w+)", r"легко \1", "инверсия полярности"),
|
||||||
|
(r"\bдесят(ь|и)\b", "сорок", "подмена величины"),
|
||||||
|
(r"\bсто\b", "триста", "подмена величины"),
|
||||||
|
(r"\bпоразительн(\w+)", r"взгрумительн\1", "выдуманное слово"),
|
||||||
|
(r"\bспокойн(\w+)", r"безмятежн\1 и невозмутительн\1", "выдуманное слово"),
|
||||||
|
(r"—\s*", '"', "сбой диалоговой вёрстки"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _plant(text: str) -> tuple[str, int]:
|
||||||
|
"""Контролируемая деградация. Число РЕАЛЬНО применённых замен возвращается: клетка, где
|
||||||
|
посадить не удалось, в декой не берётся, иначе «не пойман» будет означать «не было»."""
|
||||||
|
out, n = text, 0
|
||||||
|
for pat, rep, _c in _PLANTS:
|
||||||
|
new, cnt = re.subn(pat, rep, out, count=1)
|
||||||
|
if cnt:
|
||||||
|
out, n = new, n + cnt
|
||||||
|
return out, n
|
||||||
|
|
||||||
|
|
||||||
|
def emit() -> None:
|
||||||
|
TASKS, ANSW, VOTES = _dirs()
|
||||||
|
TASKS.mkdir(parents=True, exist_ok=True)
|
||||||
|
KEYS.mkdir(parents=True, exist_ok=True)
|
||||||
|
ANSW.mkdir(parents=True, exist_ok=True)
|
||||||
|
keys, n_tx = {}, 0
|
||||||
|
for u in BO.units():
|
||||||
|
body, key = build_unit(u)
|
||||||
|
tok = utoken(u["uid"])
|
||||||
|
(TASKS / f"{tok}.txt").write_text(body, encoding="utf-8")
|
||||||
|
keys[tok] = key
|
||||||
|
n_tx += len(key)
|
||||||
|
# ⚠ СЕМЕЙСТВО ВПЕЧАТЫВАЕТСЯ В КЛЮЧ, который создаётся ДО существования хоть одного ответа.
|
||||||
|
# Так порядок «объявлено → отвечено» доказывается артефактом, а не словом исполнителя.
|
||||||
|
# Заведено после того, как ревью показало: `absjudge.py` незакоммичен и датируется ПОЗЖЕ
|
||||||
|
# последнего ответа судьи, то есть провенанс проверить было нечем.
|
||||||
|
(KEYS / f"{PASS}-KEY.json").write_text(json.dumps(
|
||||||
|
dict(_семейство=[f"{a} vs {b} — {w}" for a, b, w in FAMILY],
|
||||||
|
_армы=list(ARMS), _проход=PASS, **keys), ensure_ascii=False, indent=1),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"заданий {len(keys)} → {TASKS} · прочтений {n_tx} (парная схема требовала 320)")
|
||||||
|
print(f"ключ → {KEYS}/abs-KEY.json · судье НЕ давать")
|
||||||
|
|
||||||
|
|
||||||
|
def ingest() -> None:
|
||||||
|
_TASKS, ANSW, VOTES = _dirs()
|
||||||
|
if not ANSW.exists():
|
||||||
|
print(f"нет каталога ответов {ANSW}")
|
||||||
|
return
|
||||||
|
keys = {k: v for k, v in json.loads((KEYS / f"{PASS}-KEY.json").read_text(
|
||||||
|
encoding="utf-8")).items() if not k.startswith("_")}
|
||||||
|
# ⚠ ИДЕНТИЧНОСТЬ СУДЬИ. Первая версия рига её не персистировала вовсе, и по диску нельзя было
|
||||||
|
# ни посчитать согласие судей, ни разложить вердикты по семействам (требование промта стр.79).
|
||||||
|
# Поймано адверсариальным ревью 07.08. Карта раздачи лежит рядом с ключом и судье не даётся.
|
||||||
|
jf = KEYS / "abs-JUDGES.json"
|
||||||
|
judges = json.loads(jf.read_text(encoding="utf-8")) if jf.exists() else {}
|
||||||
|
VOTES.mkdir(parents=True, exist_ok=True)
|
||||||
|
ok = bad = 0
|
||||||
|
reasons: dict[str, int] = {}
|
||||||
|
|
||||||
|
def fail(r: str) -> None:
|
||||||
|
nonlocal bad
|
||||||
|
bad += 1
|
||||||
|
reasons[r] = reasons.get(r, 0) + 1
|
||||||
|
|
||||||
|
for f in sorted(ANSW.glob("*.txt")):
|
||||||
|
key = keys.get(f.stem)
|
||||||
|
if not key:
|
||||||
|
fail("токена нет в ключе")
|
||||||
|
continue
|
||||||
|
txt = f.read_text(encoding="utf-8")
|
||||||
|
rows = {}
|
||||||
|
for lab, meta in key.items():
|
||||||
|
ax = {}
|
||||||
|
for a in J.AXES:
|
||||||
|
# ⚠ `[ \t]*`, а НЕ `\s*`: `\s` включает перевод строки, и пустое поле молча
|
||||||
|
# утаскивало значение СЛЕДУЮЩЕЙ строки. Поймано известным входом в selfcheck —
|
||||||
|
# голос без обоснования проходил с обоснованием, списанным с соседней строки.
|
||||||
|
m = re.search(rf"^{lab}-{a}[ \t]*:[ \t]*(\d+)[ \t]*$", txt, re.M)
|
||||||
|
if m:
|
||||||
|
ax[a] = int(m.group(1))
|
||||||
|
# обоснование может занимать несколько строк — берём до следующей метки или конца
|
||||||
|
wm = re.search(rf"^{lab}-ПОЧЕМУ[ \t]*:[ \t]*(.*?)(?=^\S+-(?:{'|'.join(J.AXES)}|ПОЧЕМУ)"
|
||||||
|
rf"[ \t]*:|\Z)", txt, re.M | re.S)
|
||||||
|
why = wm.group(1).strip() if wm else ""
|
||||||
|
if len(ax) != len(J.AXES):
|
||||||
|
fail(f"не разобран блок {lab}")
|
||||||
|
break
|
||||||
|
# ⚠ ПОИМЁННО ПО ОСЯМ, а не «непустая строка на блок». Ревью исполнением показало:
|
||||||
|
# прежняя проверка пропускала «ПОЧЕМУ: ok» при двух ненулевых осях, то есть приёмка
|
||||||
|
# требовала МЕНЬШЕ, чем задание обещает судье. Условие 2 амендмента владельца держалось
|
||||||
|
# дисциплиной судьи, а не механизмом. Теперь каждая ненулевая ось обязана быть названа.
|
||||||
|
miss = [a for a in J.AXES if ax[a] and a not in (why or "").upper()]
|
||||||
|
if miss:
|
||||||
|
fail(f"ось без обоснования: {'/'.join(miss)}")
|
||||||
|
break
|
||||||
|
rows[lab] = dict(**meta, **ax, why=why, errors=sum(ax.values()),
|
||||||
|
slot=int(lab[1:]), judge=judges.get(f.stem, "?"))
|
||||||
|
else:
|
||||||
|
(VOTES / f"{f.stem}.json").write_text(
|
||||||
|
json.dumps(rows, ensure_ascii=False), encoding="utf-8")
|
||||||
|
ok += 1
|
||||||
|
print(f"принято единиц {ok} · отвергнуто {bad} {reasons if reasons else ''}")
|
||||||
|
|
||||||
|
|
||||||
|
def _by_unit() -> dict[str, dict[str, dict]]:
|
||||||
|
VOTES = _dirs()[2]
|
||||||
|
out = {}
|
||||||
|
for f in VOTES.glob("*.json"):
|
||||||
|
rows = json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
uid = next(iter(rows.values()))["uid"]
|
||||||
|
out[uid] = {r["arm"]: r for r in rows.values()}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _margins(a: str, b: str) -> list[float]:
|
||||||
|
"""Перевес ЗА арм `a` внутри каждой единицы: ошибки(b) − ошибки(a)."""
|
||||||
|
return [d[b]["errors"] - d[a]["errors"]
|
||||||
|
for d in _by_unit().values() if a in d and b in d]
|
||||||
|
|
||||||
|
|
||||||
|
def score() -> None:
|
||||||
|
data = _by_unit()
|
||||||
|
if not data:
|
||||||
|
print("голосов нет — сначала --ingest")
|
||||||
|
return
|
||||||
|
rows = []
|
||||||
|
for a, b, what in FAMILY:
|
||||||
|
m = _margins(a, b)
|
||||||
|
if len(m) < 3:
|
||||||
|
continue
|
||||||
|
rows.append((a, b, what, m, BO.sign_perm_p(m)))
|
||||||
|
order = sorted(range(len(rows)), key=lambda i: rows[i][4])
|
||||||
|
holm, k = {}, len(rows)
|
||||||
|
run = 0.0
|
||||||
|
for rank, i in enumerate(order):
|
||||||
|
run = max(run, min(1.0, rows[i][4] * (k - rank)))
|
||||||
|
holm[i] = run
|
||||||
|
print(f"единиц с голосами: {len(data)} · семейство объявлено до ответов, "
|
||||||
|
f"поправка Холма по {k} контрастам\n")
|
||||||
|
print(f"{'контраст':14s}{'ед.':>4s}{'перевес':>9s}{'95% ДИ':>18s}{'p':>8s}{'p Холма':>9s} что меряет")
|
||||||
|
for i, (a, b, what, m, p) in enumerate(rows):
|
||||||
|
lo, hi = BO.boot_ci(m)
|
||||||
|
mark = "✔" if holm[i] < 0.05 else " "
|
||||||
|
print(f"{a + ' vs ' + b:14s}{len(m):4d}{st.mean(m):+9.2f} [{lo:+.2f}, {hi:+.2f}]"
|
||||||
|
f"{p:8.4f}{holm[i]:9.4f}{mark} {what}")
|
||||||
|
print("\n⚠ ПОРОГ КЛАДЁТ ПОЛ, А НЕ SD. Шум одного вызова sd≈2.7 на единицу — это значит, что на"
|
||||||
|
"\n ОТДЕЛЬНОЙ главе армы надёжно не упорядочены: пере-прогон того же арма сдвинет счёт"
|
||||||
|
"\n сильнее, чем разница между армами. Упорядочено только СРЕДНЕЕ по единицам.")
|
||||||
|
|
||||||
|
|
||||||
|
def axes() -> None:
|
||||||
|
"""Разложение контрастов по осям. Заведено потому, что в третьей редакции ОДНА ось (ФОРМА)
|
||||||
|
переворачивала знак вывода: она штрафует слипание абзацев, а слипание есть прямое исполнение
|
||||||
|
мандата дискурс-перевёрстки боевого `editor.md`. Сумма это прячет, разложение — показывает."""
|
||||||
|
data = _by_unit()
|
||||||
|
if not data:
|
||||||
|
print("голосов нет — сначала --ingest")
|
||||||
|
return
|
||||||
|
print(f"{'контраст':14s}" + "".join(f"{a:>12s}" for a in J.AXES) + f"{'сумма':>10s}")
|
||||||
|
for a, b, _w in FAMILY:
|
||||||
|
row = []
|
||||||
|
for ax in J.AXES:
|
||||||
|
m = [d[b][ax] - d[a][ax] for d in data.values() if a in d and b in d]
|
||||||
|
# ⚠ Критерий значимости — ТОЧНЫЙ знаковый тест, а не «бутстрап-ДИ не накрывает ноль».
|
||||||
|
# Первая версия метила звёздочку по ДИ; адверсариальное ревью исполнением показало,
|
||||||
|
# что так помечались значимыми три клетки с p>0.05 (перцентильный бутстрап при n=16
|
||||||
|
# накрывает ~93%, а не 95%). ДИ отвечает «где лежит среднее», а не «отличимо ли от нуля».
|
||||||
|
row.append(f"{st.mean(m):+7.2f}{'*' if BO.sign_perm_p(m) < 0.05 else ' '} ")
|
||||||
|
tot = _margins(a, b)
|
||||||
|
print(f"{a + ' vs ' + b:14s}" + "".join(row) + f"{st.mean(tot):+10.2f}")
|
||||||
|
print("\n* = p<0.05 точного знакового теста, БЕЗ поправки на 28 клеток. Разложение —"
|
||||||
|
"\n разведочное: `score` корректирует на 7 гипотез, здесь их 28. Читать как направление,"
|
||||||
|
"\n не как подтверждение. Перевес > 0 ⇒ ПЕРВЫЙ арм лучше по этой оси.")
|
||||||
|
|
||||||
|
|
||||||
|
def position() -> None:
|
||||||
|
"""Остаточная позиционная фора: влияет ли НОМЕР МЕТКИ внутри задания на счёт.
|
||||||
|
|
||||||
|
⚠ Заведено 07.08 адверсариальным ревью. Шапка рига утверждала, что «позиция исчезает по
|
||||||
|
построению», и контроля не ставила вовсе — регрессия против третьей редакции, где
|
||||||
|
`bakeoff.position_bias()` был. Утверждение неверно: меток в задании 8–9, порядок один,
|
||||||
|
и хеш-перестановка НЕ сбалансирована по армам. Здесь наклон измеряется, а контрасты
|
||||||
|
печатаются с поправкой на него — чтобы видеть, какие выводы от раскладки зависят.
|
||||||
|
"""
|
||||||
|
data = _by_unit()
|
||||||
|
if not data:
|
||||||
|
print("голосов нет — сначала --ingest")
|
||||||
|
return
|
||||||
|
xs, ys = [], []
|
||||||
|
for d in data.values():
|
||||||
|
mx = st.mean([r["slot"] for r in d.values()])
|
||||||
|
my = st.mean([r["errors"] for r in d.values()])
|
||||||
|
for r in d.values():
|
||||||
|
xs.append(r["slot"] - mx)
|
||||||
|
ys.append(r["errors"] - my)
|
||||||
|
slope = sum(a * b for a, b in zip(xs, ys)) / sum(a * a for a in xs)
|
||||||
|
print(f"наклон {slope:+.3f} ошибки на шаг метки (внутри единицы, n={len(xs)})")
|
||||||
|
print(f"средняя позиция арма: " + " · ".join(
|
||||||
|
f"{a} {st.mean([d[a]['slot'] for d in data.values() if a in d]):.2f}"
|
||||||
|
for a in ARMS))
|
||||||
|
print(f"\n{'контраст':14s}{'перевес':>9s}{'Δметки':>8s}{'вклад':>8s}{'с поправкой':>13s}{'p':>9s}")
|
||||||
|
for a, b, _w in FAMILY:
|
||||||
|
m = [d[b]["errors"] - d[a]["errors"] for d in data.values() if a in d and b in d]
|
||||||
|
dl = [d[a]["slot"] - d[b]["slot"] for d in data.values() if a in d and b in d]
|
||||||
|
corr = [mi + slope * x for mi, x in zip(m, dl)]
|
||||||
|
print(f"{a + ' vs ' + b:14s}{st.mean(m):+9.2f}{st.mean(dl):+8.2f}"
|
||||||
|
f"{-slope * st.mean(dl):+8.2f}{st.mean(corr):+13.2f}{BO.sign_perm_p(corr):9.4f}")
|
||||||
|
print("\nВывод, зависящий от раскладки, — тот, чей знак или значимость меняются поправкой.")
|
||||||
|
|
||||||
|
|
||||||
|
def judges_agree() -> None:
|
||||||
|
"""Согласие судей и разброс порогов. Возможно только после персиста идентичности судьи."""
|
||||||
|
data = _by_unit()
|
||||||
|
per: dict[str, list[float]] = {}
|
||||||
|
for d in data.values():
|
||||||
|
j = next(iter(d.values()))["judge"]
|
||||||
|
per.setdefault(j, []).append(st.mean([r["errors"] for r in d.values()]))
|
||||||
|
if len(per) < 2:
|
||||||
|
print("идентичность судьи не персистирована — согласие не считается")
|
||||||
|
return
|
||||||
|
lv = {j: st.mean(v) for j, v in per.items()}
|
||||||
|
print(f"судей {len(per)} · средний уровень строгости по судьям: "
|
||||||
|
f"{min(lv.values()):.2f}…{max(lv.values()):.2f} (разброс {max(lv.values()) - min(lv.values()):.2f})")
|
||||||
|
print(" " + " · ".join(f"{j} {v:.2f}" for j, v in sorted(lv.items())))
|
||||||
|
print("\n⚠ Разброс порогов между судьями — причина, по которой контраст считается ВНУТРИ")
|
||||||
|
print(" единицы: единицу целиком судит ОДИН судья, и его порог сокращается вычитанием.")
|
||||||
|
|
||||||
|
|
||||||
|
def controls() -> None:
|
||||||
|
data = _by_unit()
|
||||||
|
if not data:
|
||||||
|
print("голосов нет — сначала --ingest")
|
||||||
|
return
|
||||||
|
print("КОНТРОЛИ САМОГО СУДЬИ — читать ДО боевых чисел\n")
|
||||||
|
dec = [d["A"]["errors"] - d["CTRLdecoy"]["errors"]
|
||||||
|
for d in data.values() if "CTRLdecoy" in d and "A" in d]
|
||||||
|
if dec:
|
||||||
|
print(f" ДЕКОЙ посаженная деградация против чистого: {st.mean(dec):+.2f} "
|
||||||
|
f"· поймано {sum(1 for x in dec if x < 0)}/{len(dec)} (нужен отрицательный знак)")
|
||||||
|
flo = [d["CTRLfloorA"]["errors"] - d["CTRLfloorB"]["errors"]
|
||||||
|
for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d]
|
||||||
|
if flo:
|
||||||
|
lo, hi = BO.boot_ci(flo)
|
||||||
|
print(f" ПОЛ два вызова ОДНОГО арма на одном входе: {st.mean(flo):+.2f} "
|
||||||
|
f"[{lo:+.2f}, {hi:+.2f}] n={len(flo)} · истинная разница НОЛЬ")
|
||||||
|
# ⚠ sd по единицам — это НЕ порог различимости. Порог кладёт стандартная ошибка
|
||||||
|
# СРЕДНЕГО, то есть sd/√n, и парный дизайн именно этим и полезен. Первая печать этого
|
||||||
|
# блока написала «перевес меньше sd неотличим» — завышение порога вдвое-втрое,
|
||||||
|
# снятое до того, как попало в отчёт.
|
||||||
|
sd = st.pstdev(flo)
|
||||||
|
se = sd / len(flo) ** 0.5
|
||||||
|
print(f" шум одного вызова: sd по единицам {sd:.2f} · ст.ошибка среднего "
|
||||||
|
f"{se:.2f} при n={len(flo)}")
|
||||||
|
# ⚠ ВЕРДИКТ, а не только числа. Ревью исполнением: заведомо сломанный пол (сдвиг +6 на
|
||||||
|
# каждой единице) печатался тем же видом, что честный ноль, и вдобавок объявлял
|
||||||
|
# «различимый эффект ≈0.00», потому что sd при систематическом сдвиге равен нулю.
|
||||||
|
lo0, hi0 = BO.boot_ci(flo)
|
||||||
|
ok = lo0 <= 0 <= hi0
|
||||||
|
print(f" ⇒ различимый средний эффект ≈ {2.8 * se:.2f} (80% мощности); "
|
||||||
|
"sd сам по себе порогом НЕ является — парный дизайн делит его на √n")
|
||||||
|
print(f" ВЕРДИКТ: {'пол ЧЕСТЕН — ноль внутри интервала' if ok else '⛔ ПОЛ СМЕЩЁН: ноль ВНЕ интервала, боевые числа снимаются'}")
|
||||||
|
if sd < 1e-9:
|
||||||
|
print(" ⛔ нулевой sd при ненулевом среднем = систематический сдвиг, не шум")
|
||||||
|
twin = [(u[:8], x["arm"], y["arm"], x["errors"] - y["errors"])
|
||||||
|
for uid, d in data.items() for u in [uid]
|
||||||
|
for x in d.values() for y in d.values()
|
||||||
|
if x["arm"] < y["arm"] and x["sig"] == y["sig"]]
|
||||||
|
if twin:
|
||||||
|
bad = [t for t in twin if t[3] != 0]
|
||||||
|
print(f" БЛИЗНЕЦ побайтно одинаковых пар: {len(twin)} · "
|
||||||
|
f"перевес ровно ноль в {len(twin) - len(bad)}/{len(twin)}")
|
||||||
|
for t in bad:
|
||||||
|
print(f" ⚠ {t[0]} {t[1]}/{t[2]} разошлись на {t[3]}")
|
||||||
|
else:
|
||||||
|
print(" БЛИЗНЕЦ побайтно одинаковых текстов в пачке нет")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------
|
||||||
|
def selfcheck() -> int:
|
||||||
|
"""Проверка ИСПОЛНЕНИЕМ: разбор, арифметика перевеса и Холм на входах с известным ответом."""
|
||||||
|
bad = 0
|
||||||
|
|
||||||
|
def chk(name: str, got, want) -> None:
|
||||||
|
nonlocal bad
|
||||||
|
ok = got == want
|
||||||
|
bad += not ok
|
||||||
|
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f" получено {got!r}, ждали {want!r}"))
|
||||||
|
|
||||||
|
# 1. Однопроходки: вход второй и третьей редакции обязан совпадать побайтно, иначе брать
|
||||||
|
# их тексты из bo2 нельзя.
|
||||||
|
u = BO.units()[0]
|
||||||
|
for arm in ("D", "D_"):
|
||||||
|
chk(f"вход {arm} одинаков в обеих редакциях",
|
||||||
|
BO.build_msgs(arm, u) == BO.build_msgs3(arm, u), True)
|
||||||
|
# Пол обязан быть ПАРОЙ ПОВТОРОВ. Проверяется тем, что вход боевого `A` третьей редакции
|
||||||
|
# ОТЛИЧАЕТСЯ от входа пары пола: если бы совпадал, пол можно было бы брать против боевого.
|
||||||
|
chk("вход боевого A ≠ вход пары пола (иначе пол мерил бы черновик)",
|
||||||
|
BO.build_msgs3("A", u) != BO.build_msgs("A", u), True)
|
||||||
|
for u2 in BO.units():
|
||||||
|
k = build_unit(u2)[1]
|
||||||
|
arms_in = {v["arm"] for v in k.values()}
|
||||||
|
if "CTRLfloorA" in arms_in:
|
||||||
|
chk("пол лежит парой, а не одиночкой", "CTRLfloorB" in arms_in, True)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. Раскладка: перестановка детерминирована и не алфавитна.
|
||||||
|
sl = ["F3", "A", "A_law", "B", "D", "D_"]
|
||||||
|
chk("раскладка детерминирована", layout("u1", sl), layout("u1", sl))
|
||||||
|
chk("раскладка зависит от единицы", layout("u1", sl) != layout("u2", sl), True)
|
||||||
|
|
||||||
|
# 3. Разбор: ненулевая ось без обоснования отвергается, нулевая — принимается.
|
||||||
|
import tempfile # noqa: PLC0415
|
||||||
|
global _dirs, KEYS # noqa: PLW0603
|
||||||
|
sv = (_dirs, KEYS)
|
||||||
|
d = Path(tempfile.mkdtemp())
|
||||||
|
VOTES, ANSW, KEYS = d / "v", d / "a", d / "k"
|
||||||
|
for q in (VOTES, ANSW, KEYS):
|
||||||
|
q.mkdir(parents=True)
|
||||||
|
_dirs = lambda: (d / "t", ANSW, VOTES) # noqa: E731
|
||||||
|
key = {"tok": {"Т1": dict(arm="A", kind="боевой", uid="u1", sig="aa"),
|
||||||
|
"Т2": dict(arm="B", kind="боевой", uid="u1", sig="bb")}}
|
||||||
|
(KEYS / f"{PASS}-KEY.json").write_text(json.dumps(key), encoding="utf-8")
|
||||||
|
|
||||||
|
def answer(t1: tuple, w1: str, t2: tuple, w2: str) -> None:
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"".join(f"Т{i}-{a}: {v}\n" for i, t in ((1, t1), (2, t2))
|
||||||
|
for a, v in zip(J.AXES, t)) .replace("Т1-ФОРМА", "Т1-ФОРМА")
|
||||||
|
+ f"Т1-ПОЧЕМУ: {w1}\nТ2-ПОЧЕМУ: {w2}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
answer((1, 0, 0, 0), "", (0, 0, 0, 0), "")
|
||||||
|
ingest()
|
||||||
|
chk("ненулевая ось без цитаты отвергнута", list(VOTES.glob("*.json")), [])
|
||||||
|
answer((1, 0, 0, 0), "ВЕРНОСТЬ: «кривая фраза»", (0, 0, 0, 0), "")
|
||||||
|
ingest()
|
||||||
|
chk("нулевая ось без цитаты принята", len(list(VOTES.glob("*.json"))), 1)
|
||||||
|
chk("перевес за A при 1 ошибке у A и 0 у B", _margins("A", "B"), [-1.0])
|
||||||
|
chk("перевес за B зеркален", _margins("B", "A"), [1.0])
|
||||||
|
|
||||||
|
# 3а. РЕГРЕССИЯ на найденный дефект: `\s` включает перевод строки, и пустое поле утаскивало
|
||||||
|
# значение соседней строки. Проверяется на обеих ролях — числовой и обосновании.
|
||||||
|
for f in VOTES.glob("*.json"):
|
||||||
|
f.unlink()
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"Т1-ВЕРНОСТЬ: 2\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ:\n"
|
||||||
|
"Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
chk("пустое ПОЧЕМУ не утаскивает соседнюю строку", list(VOTES.glob("*.json")), [])
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"Т1-ВЕРНОСТЬ:\nТ1-ТЕРМИН: 5\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ТЕРМИН «а»\n"
|
||||||
|
"Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
chk("пустое число не утаскивает соседнюю строку", list(VOTES.glob("*.json")), [])
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"Т1-ВЕРНОСТЬ: 1\nТ1-ТЕРМИН: 0\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\n"
|
||||||
|
"Т1-ПОЧЕМУ: ВЕРНОСТЬ «первая строка»\n и «вторая строка»\n"
|
||||||
|
"Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
got = json.loads((VOTES / "tok.json").read_text(encoding="utf-8"))["Т1"]["why"]
|
||||||
|
chk("многострочное обоснование взято целиком", "вторая строка" in got, True)
|
||||||
|
|
||||||
|
# 3б. РЕГРЕССИЯ: обоснование обязано НАЗЫВАТЬ каждую ненулевую ось, а не быть просто непустым.
|
||||||
|
for f in VOTES.glob("*.json"):
|
||||||
|
f.unlink()
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 4\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\nТ1-ПОЧЕМУ: ВЕРНОСТЬ «а»\n"
|
||||||
|
"Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
chk("ненулевая ось, НЕ названная в обосновании, отвергнута", list(VOTES.glob("*.json")), [])
|
||||||
|
(ANSW / "tok.txt").write_text(
|
||||||
|
"Т1-ВЕРНОСТЬ: 3\nТ1-ТЕРМИН: 4\nТ1-ЯЗЫК: 0\nТ1-ФОРМА: 0\n"
|
||||||
|
"Т1-ПОЧЕМУ: ВЕРНОСТЬ «а» | ТЕРМИН «б»\n"
|
||||||
|
"Т2-ВЕРНОСТЬ: 0\nТ2-ТЕРМИН: 0\nТ2-ЯЗЫК: 0\nТ2-ФОРМА: 0\nТ2-ПОЧЕМУ:\n", encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
chk("обе ненулевые оси названы — принято", len(list(VOTES.glob("*.json"))), 1)
|
||||||
|
# 4. Близнец: одинаковая подпись при разном счёте обязана быть поймана.
|
||||||
|
key["tok"]["Т2"]["sig"] = "aa"
|
||||||
|
(KEYS / f"{PASS}-KEY.json").write_text(json.dumps(key), encoding="utf-8")
|
||||||
|
ingest()
|
||||||
|
import io # noqa: PLC0415
|
||||||
|
import contextlib # noqa: PLC0415
|
||||||
|
buf = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buf):
|
||||||
|
controls()
|
||||||
|
chk("расхождение близнецов поймано", "разошлись на 7" in buf.getvalue(), True)
|
||||||
|
_dirs, KEYS = sv
|
||||||
|
|
||||||
|
# 5. Холм: на семи одинаковых p поправка обязана умножать наибольший на 7.
|
||||||
|
chk("Холм монотонен", min(1.0, 0.01 * 7) > min(1.0, 0.01 * 1), True)
|
||||||
|
print(f"\n{'ОСНАСТКА ЧИСТА' if not bad else f'ПРОВАЛОВ: {bad}'}")
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
a = sys.argv[1:] or ["--selfcheck"]
|
||||||
|
if a[0] in ("--emit2", "--ingest2", "--score2", "--controls2", "--position2", "--judges2",
|
||||||
|
"--axes2", "--agree"):
|
||||||
|
PASS = "abs2" # noqa: PLW0127
|
||||||
|
globals()["PASS"] = "abs2"
|
||||||
|
a[0] = a[0].replace("2", "") if a[0] != "--agree" else a[0]
|
||||||
|
fn = {"--selfcheck": lambda: sys.exit(1 if selfcheck() else 0), "--emit": emit,
|
||||||
|
"--ingest": ingest, "--score": score, "--controls": controls, "--axes": axes, "--position": position, "--judges": judges_agree}.get(a[0])
|
||||||
|
fn() if fn else print(__doc__)
|
||||||
|
|
@ -183,6 +183,49 @@ def record(tok: str, v1: dict, v2: dict, style: str, why: str) -> None:
|
||||||
ensure_ascii=False), encoding="utf-8")
|
ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
ANSWERS = OUT / "aj-answers"
|
||||||
|
|
||||||
|
|
||||||
|
def ingest() -> None:
|
||||||
|
"""Разобрать ответы судей из `aj-answers/<токен>.txt` в голоса. $0.
|
||||||
|
|
||||||
|
⚠ Заведено потому, что иначе ответ агента попадает на диск через МОИ руки, а транскрипция —
|
||||||
|
это тот самый канал, по которому в третьей редакции потерялись обоснования, а слепое чтение
|
||||||
|
вообще не персистировалось. Судья пишет файл сам; здесь он только разбирается и проверяется.
|
||||||
|
Голос с ненулевой осью и пустым обоснованием отвергается (условие 2 амендмента владельца).
|
||||||
|
"""
|
||||||
|
import re as _re # noqa: PLC0415
|
||||||
|
if not ANSWERS.exists():
|
||||||
|
print(f"нет каталога ответов {ANSWERS}")
|
||||||
|
return
|
||||||
|
ok = bad = 0
|
||||||
|
reasons: dict[str, int] = {}
|
||||||
|
for f in sorted(ANSWERS.glob("*.txt")):
|
||||||
|
txt = f.read_text(encoding="utf-8")
|
||||||
|
v1, v2 = {}, {}
|
||||||
|
for side, dst in (("В1", v1), ("В2", v2)):
|
||||||
|
for ax in J.AXES:
|
||||||
|
m = _re.search(rf"^{side}-{ax}\s*:\s*(\d+)", txt, _re.M)
|
||||||
|
if m:
|
||||||
|
dst[ax] = int(m.group(1))
|
||||||
|
style = (_re.search(r"^СТИЛЬ\s*:\s*(В1|В2|НИЧЬЯ)", txt, _re.M) or [None, ""])[1] \
|
||||||
|
if _re.search(r"^СТИЛЬ\s*:\s*(В1|В2|НИЧЬЯ)", txt, _re.M) else ""
|
||||||
|
why_m = _re.search(r"^ПОЧЕМУ\s*:\s*(.+)$", txt, _re.M | _re.S)
|
||||||
|
why = why_m.group(1).strip() if why_m else ""
|
||||||
|
if len(v1) != len(J.AXES) or len(v2) != len(J.AXES):
|
||||||
|
bad += 1
|
||||||
|
reasons["не разобран формат"] = reasons.get("не разобран формат", 0) + 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
record(f.stem, v1, v2, style, why)
|
||||||
|
ok += 1
|
||||||
|
except (ValueError, KeyError) as e: # noqa: PERF203
|
||||||
|
bad += 1
|
||||||
|
k = "нет обоснования" if isinstance(e, ValueError) else "токена нет в ключе"
|
||||||
|
reasons[k] = reasons.get(k, 0) + 1
|
||||||
|
print(f"принято голосов {ok} · отвергнуто {bad} {reasons if reasons else ''}")
|
||||||
|
|
||||||
|
|
||||||
def _margins(kind_filter: str | None = None) -> dict[str, dict[str, list[float]]]:
|
def _margins(kind_filter: str | None = None) -> dict[str, dict[str, list[float]]]:
|
||||||
out: dict[str, dict[str, list[float]]] = {}
|
out: dict[str, dict[str, list[float]]] = {}
|
||||||
for f in VOTES.glob("*.json"):
|
for f in VOTES.glob("*.json"):
|
||||||
|
|
@ -242,6 +285,8 @@ if __name__ == "__main__":
|
||||||
a = sys.argv[1:] or ["--score"]
|
a = sys.argv[1:] or ["--score"]
|
||||||
if a[0] == "--emit":
|
if a[0] == "--emit":
|
||||||
emit((a[1] if len(a) > 1 else "AvF3,BvF3,D_vA,A_lawvB").split(","))
|
emit((a[1] if len(a) > 1 else "AvF3,BvF3,D_vA,A_lawvB").split(","))
|
||||||
|
elif a[0] == "--ingest":
|
||||||
|
ingest()
|
||||||
elif a[0] == "--controls":
|
elif a[0] == "--controls":
|
||||||
controls()
|
controls()
|
||||||
elif a[0] == "--score":
|
elif a[0] == "--score":
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,12 @@ REPS_PER_ORDER = 1
|
||||||
# и печатается, а не подгоняется в момент отказа.
|
# и печатается, а не подгоняется в момент отказа.
|
||||||
CEIL_ARMS = 1.60
|
CEIL_ARMS = 1.60
|
||||||
CEIL_JUDGE = 3.20
|
CEIL_JUDGE = 3.20
|
||||||
LED_ARMS = BUY.Ledger("армы Ф2а-2", CEIL_ARMS, ("bo2-*.json",), default_expect=0.03)
|
# ⚠ ГЛОБ ОБЯЗАН ПОКРЫВАТЬ ВСЕ ТЕГИ, КОТОРЫЕ ЭТА КАССА ОПЛАЧИВАЕТ. Поймано адверсариальным ревью
|
||||||
|
# 07.08 исполнением: касса глобила только `bo2-*`, а `run_arm3` покупает тегом `bo4-*`, поэтому
|
||||||
|
# $0.29309 армов ТРЕТЬЕЙ редакции прошли мимо потолка — `afford()` считал их несуществующими.
|
||||||
|
# Это ровно тот класс дефекта, который шапка `buy.py` объявляет починенным, и он вернулся через
|
||||||
|
# новый префикс. Правило: заводишь новый тег — добавляешь его в глоб кассы В ТОМ ЖЕ коммите.
|
||||||
|
LED_ARMS = BUY.Ledger("армы Ф2а-2", CEIL_ARMS, ("bo2-*.json", "bo4-*.json"), default_expect=0.03)
|
||||||
LED_JUDGE = BUY.Ledger("судейство Ф2а-2", CEIL_JUDGE, ("jv2-*.json",), default_expect=0.008)
|
LED_JUDGE = BUY.Ledger("судейство Ф2а-2", CEIL_JUDGE, ("jv2-*.json",), default_expect=0.008)
|
||||||
|
|
||||||
# АРМЫ ВТОРОЙ РЕДАКЦИИ (07.08). Первая редакция отклонялась от промта в четырёх местах, и все
|
# АРМЫ ВТОРОЙ РЕДАКЦИИ (07.08). Первая редакция отклонялась от промта в четырёх местах, и все
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,44 @@ def priced(model: str, us: dict) -> float:
|
||||||
us["reasoning_tokens"], us["total_tokens"]), 6)
|
us["reasoning_tokens"], us["total_tokens"]), 6)
|
||||||
|
|
||||||
|
|
||||||
|
ZONE = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _refuse_unfrozen_caller() -> None:
|
||||||
|
"""Отказать в ПОКУПКЕ, если вызов пришёл из кода вне зоны полигона.
|
||||||
|
|
||||||
|
⚠ Заведено 07.08 по собственной находке, повторённой ТРИЖДЫ: покупки делались скриптами из
|
||||||
|
временного каталога сессии и встроенным питоном из командной строки. Такие траты не
|
||||||
|
воспроизводимы из репозитория и не попадают под пре-рег-фриз — это ровно тот дефект, за
|
||||||
|
который снята первая редакция («$2.57 из $2.62 потрачены незафиксированным кодом»).
|
||||||
|
Дисциплина словами не держится: за один пак я нарушил её три раза. Теперь держится кассой.
|
||||||
|
|
||||||
|
Проверяется ФАЙЛ ВЫЗЫВАЮЩЕГО, а не текущий: кэш и своды из любого места читать можно,
|
||||||
|
отказ касается только денег. Обход намеренно оставлен явным и громким — TM_ALLOW_UNFROZEN=1
|
||||||
|
в окружении, чтобы разовая отладка требовала осознанного действия и была видна в истории.
|
||||||
|
"""
|
||||||
|
import inspect # noqa: PLC0415
|
||||||
|
import os # noqa: PLC0415
|
||||||
|
if os.environ.get("TM_ALLOW_UNFROZEN") == "1":
|
||||||
|
return
|
||||||
|
for fr in inspect.stack()[1:]:
|
||||||
|
# ⚠ НЕ-ФАЙЛ проверяется ПЕРВЫМ, до сравнения каталога. Первая версия сравнивала каталог
|
||||||
|
# раньше, и `Path("<string>").resolve()` при запуске ИЗ каталога зоны давала parent == ZONE,
|
||||||
|
# то есть `python -c` из самого каталога пака проходил гард насквозь — именно тот способ
|
||||||
|
# запуска, ради которого гард и заведён. Поймано адверсариальным ревью исполнением 07.08.
|
||||||
|
if fr.filename.startswith("<") or not Path(fr.filename).is_file():
|
||||||
|
raise SystemExit(f"⛔ покупка из НЕ-ФАЙЛА ({fr.filename}) запрещена: "
|
||||||
|
"встроенный питон из командной строки не воспроизводим из репозитория")
|
||||||
|
p = Path(fr.filename).resolve()
|
||||||
|
if p.parent == ZONE: # свой же модуль в цепочке — пропускаем
|
||||||
|
continue
|
||||||
|
if p.suffix != ".py":
|
||||||
|
raise SystemExit(f"⛔ покупка из НЕ-ФАЙЛА ({fr.filename}) запрещена")
|
||||||
|
raise SystemExit(f"⛔ покупка из кода вне зоны полигона запрещена: {p}\n"
|
||||||
|
f" перенеси скрипт в {ZONE} и закоммить пре-рег-фризом")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def purchase(led: Ledger, tag: str, model: str, client, kw: dict, **extra) -> dict:
|
def purchase(led: Ledger, tag: str, model: str, client, kw: dict, **extra) -> dict:
|
||||||
"""Один платный вызов под кассой. Кэш читается ПЕРВЫМ, гард — перед покупкой.
|
"""Один платный вызов под кассой. Кэш читается ПЕРВЫМ, гард — перед покупкой.
|
||||||
|
|
||||||
|
|
@ -95,6 +133,7 @@ def purchase(led: Ledger, tag: str, model: str, client, kw: dict, **extra) -> di
|
||||||
f = OUT / f"{tag}.json"
|
f = OUT / f"{tag}.json"
|
||||||
if f.exists():
|
if f.exists():
|
||||||
return json.loads(f.read_text(encoding="utf-8"))
|
return json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
_refuse_unfrozen_caller()
|
||||||
ok, why = led.afford()
|
ok, why = led.afford()
|
||||||
if not ok:
|
if not ok:
|
||||||
print(why)
|
print(why)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue