textmachine/eval/dovodka/stupen.py

155 lines
8.9 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
"""ЭНДПОЙНТ ДИЗАЙНА Д (пре-рег Д47): РЕЖЕТ ЛИ `reasoning_effort:"low"` РАЗМЫШЛЕНИЕ У `pro`. $0.
⚠ ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ, а не счёт в уме отчёта. Правило решения Д47.3 написано ДО чисел, и оно
обязано применяться механически, иначе «правило до чисел» превращается в «правило под числа».
Файл написан ДО прихода клеток и коммитится вместе с ними: кто угодно пере-снимает вывод одной
командой и видит ровно то, что видел автор.
ЧТО ЗДЕСЬ ЕСТЬ И ЧЕГО НЕТ:
* есть парный знаковый тест по восьми главам (`reasoning_tokens` у `RNL` против `RN2`);
* есть по-клеточный гейт годности В ТОЙ ФОРМЕ, В КАКОЙ ОН ЗАМОРОЖЕН, включая проверку
провенанса `Δprompt_tokens ∈ [60,100]` — она ловит клетку, до которой параметр НЕ доехал;
* НЕТ ни одной ручки, которой можно подвинуть порог: α, полосы и список глав — константы файла,
и селфтест стережёт, что они те же, что в отчёте.
⛔ ОТНОШЕНИЯ (×N) ПЕЧАТАЮТСЯ КАК ОПИСАНИЕ, А НЕ КАК ВЫВОД. Дизайн Д стоит на восьми ДЕШЁВЫХ главах
(10.2% цены панели) — переносить их величины на дорогую моду запрещено там же, где выбраны главы.
Запуск: eval/.venv/bin/python eval/dovodka/stupen.py [--selftest]
"""
from __future__ import annotations
import json
import math
import sys
from pathlib import Path
ZONE = Path(__file__).resolve().parent
sys.path.insert(0, str(ZONE))
import rol # noqa: E402
OUT = Path.home() / "books" / "dovodka"
# ── КОНСТАНТЫ ПРЕ-РЕГА Д47. Меняются только вместе с отчётом и только ДО покупок ───────────────
CHAPTERS = ("b065a92dc0", "5a8f48d24a", "100b088f71", "49ca859c94",
"8e50448cea", "c3517980c7", "3bd6481182", "26c3bff1d4")
ARM_HIGH, ARM_LOW = "RN2", "RNL" # контроль (ручка не шлётся) и испытуемый (`low`)
ALPHA = 0.05 # двусторонний, Д47.3
DPT_BAND = (60, 100) # провенанс параметра: ожидается 79 (Д46.1)
CYR_MIN = 0.99 # доля кириллицы среди букв
LAT_MAX = 0 # латиница-эхо
def _letters(t: str) -> tuple[int, int, int]:
"""(букв всего, кириллических, латинских)."""
al = [c for c in t if c.isalpha()]
cyr = sum(1 for c in al if "а" <= c.lower() <= "я" or c.lower() == "ё")
lat = sum(1 for c in al if "a" <= c.lower() <= "z")
return len(al), cyr, lat
def _cell(arm: str, uid: str) -> dict | None:
f = OUT / f"{rol.tag('en', arm, uid)}.json"
return json.loads(f.read_text(encoding="utf-8")) if f.exists() else None
def sign_p(k: int, n: int) -> float:
"""Точный двусторонний знаковый тест при p=0.5. k — сколько пар легли в ожидаемую сторону."""
if n == 0:
return 1.0
tail = sum(math.comb(n, i) for i in range(k, n + 1)) / 2 ** n
return min(1.0, 2 * tail)
def main() -> int:
rows, missing = [], []
for uid in CHAPTERS:
hi, lo = _cell(ARM_HIGH, uid), _cell(ARM_LOW, uid)
if not hi or not lo:
missing.append(uid)
continue
n_al, cyr, lat = _letters(lo.get("content") or "")
share = cyr / n_al if n_al else 0.0
dpt = hi["prompt_tokens"] - lo["prompt_tokens"]
bad = []
if hi.get("finish") != "stop" or lo.get("finish") != "stop":
bad.append("finish≠stop")
if share < CYR_MIN:
bad.append(f"кириллица {share:.3f}")
if lat > LAT_MAX:
bad.append(f"латиница {lat}")
if not DPT_BAND[0] <= dpt <= DPT_BAND[1]:
bad.append(f"Δpt {dpt} вне [{DPT_BAND[0]},{DPT_BAND[1]}] — параметр не доехал")
rows.append(dict(uid=uid, hi=hi["reasoning_tokens"], lo=lo["reasoning_tokens"],
dpt=dpt, cyr=share, lat=lat, bad=bad,
cost=hi["cost_usd"] + lo["cost_usd"]))
if missing:
print(f"⛔ клеток нет у глав: {' '.join(missing)} — эндпойнт неполон")
if not rows:
return 1
print(f"{'глава':12}{'RN2 дум':>9}{'RNL дум':>9}{'отнош':>8}{'Δpt':>6}{'кирилл':>8}"
f"{'лат':>5} гейт")
for r in rows:
ratio = r["hi"] / r["lo"] if r["lo"] else float("inf")
print(f"{r['uid']:12}{r['hi']:>9}{r['lo']:>9}{ratio:>8.2f}{r['dpt']:>6}"
f"{r['cyr']:>8.3f}{r['lat']:>5} "
f"{'OK' if not r['bad'] else '' + ' · '.join(r['bad'])}")
good = [r for r in rows if not r["bad"]]
n = len(good)
k = sum(1 for r in good if r["lo"] < r["hi"])
p = sign_p(k, n)
ratios = sorted(r["hi"] / r["lo"] for r in good if r["lo"])
med = ratios[len(ratios) // 2] if ratios else float("nan")
print(f"\nгодных пар {n} из {len(CHAPTERS)} · размышление у `low` МЕНЬШЕ в {k} из {n}")
print(f"знаковый тест, двусторонний: p = {p:.4f} при пороге α = {ALPHA}")
print(f"цена прогона: ${sum(r['cost'] for r in rows):.6f}")
# ── ПРАВИЛО Д47.3. Ветки написаны до чисел; ни одна из них ничего не «дозакупает» ──────────
print()
if n < len(CHAPTERS):
print("⚠ ПАР МЕНЬШЕ ВОСЬМИ: правило Д47.3 писалось на n=8. Вывод печатается по факту "
"годных пар и объявляется НЕПОЛНЫМ — добор до восьми ПОСЛЕ взгляда на числа запрещён.")
if k == n and p < ALPHA:
print(f"⭐ ЕДИНОГЛАСНО И ЗНАЧИМО: ручка РЕЖЕТ размышление. Медиана отношения ×{med:.2f}"
"ОПИСАНИЕ этих восьми дешёвых глав, не перенос на дорогую моду и не на пару zh.")
elif p >= ALPHA:
print(f"НЕ ЗНАЧИМО (p = {p:.4f}{ALPHA}): «не установлено». Это РЕЗУЛЬТАТ, а не повод "
"докупать девятую главу — добор до значимости после чисел запрещён (Д47.3).")
else:
print(f"⛔ ЗНАЧИМО В ОБРАТНУЮ СТОРОНУ (k={k} из {n}): ручка размышление НЕ режет.")
return 0
def selftest() -> int:
bad = 0
def ck(ok: bool, what: str) -> None:
nonlocal bad
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {what}")
bad += 0 if ok else 1
ck(len(CHAPTERS) == 8 and len(set(CHAPTERS)) == 8, "восемь глав пре-рега, без повторов")
rep = (ZONE.parents[1] / "docs" / "experiments" / "23-editor-tier.md").read_text(encoding="utf-8")
ck(all(u in rep for u in CHAPTERS), "все восемь глав названы в отчёте (пре-рег Д47.2)")
ck(ARM_HIGH in rol.ARMS and ARM_LOW in rol.ARMS, "оба арма есть в реестре покупок")
ck(rol.THINK_LEVEL.get(ARM_HIGH) is None and rol.THINK_LEVEL.get(ARM_LOW) == "low",
"контроль без ручки, испытуемый на `low`")
ck(rol.ARMS[ARM_HIGH][0] == rol.ARMS[ARM_LOW][0] == "deepseek-v4-pro", "модель у армов одна")
ck(rol.tag("en", ARM_HIGH, "x") != rol.tag("en", ARM_LOW, "x"), "теги армов не схлопываются")
# правило решения обязано быть НЕ достижимо ничем, кроме единогласия — это и есть цена n=8
ck(sign_p(8, 8) < ALPHA <= sign_p(7, 8), "при n=8 значимость даёт ТОЛЬКО 8 из 8")
ck(abs(sign_p(8, 8) - 0.0078125) < 1e-9, "минимальный двусторонний p посчитан верно")
ck(sign_p(4, 8) == 1.0, "полный ноль эффекта даёт p = 1")
print(f"\n{'ПРИБОР ГОДЕН' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
if __name__ == "__main__":
sys.exit(selftest() if "--selftest" in sys.argv else main())