Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWeV2yiMWTjkeaQfSsYuL
249 lines
16 KiB
Python
249 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""predpolet_proverka.py — ассерты пред-полёта. Исполняемые, а не «посмотрела глазами».
|
||
|
||
Промт §5.3 требует от пред-полёта пять вещей: сигнатуру Δprompt_tokens, пять разных снапшотов,
|
||
кириллицу ≥0.99, эхо 0 и гейты референса. Гарды прибора при n=1 их ПЕЧАТАЮТ, но не ОСТАНАВЛИВАЮТ:
|
||
`chetyre.py` рассчитан на пачку. Поэтому стоп-критерии пачки исполняются здесь, и каждый — с кодом.
|
||
|
||
⛔ Ни одного платного вызова: только чтение баз и логов.
|
||
"""
|
||
|
||
import json
|
||
import math
|
||
import pathlib
|
||
import re
|
||
import sqlite3
|
||
import sys
|
||
|
||
B = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else pathlib.Path.home() / "books/gu-zhenren/probe-4axes")
|
||
PIN = json.loads((pathlib.Path(__file__).parent / "prereg-4axes/prereg.json").read_text())
|
||
COEF = PIN["коэффициент_восстановления_размышления"]
|
||
LOW_THRESHOLD = PIN["деньги"]["порог_цены_клетки_low"]
|
||
|
||
ARMS = ["p7-off", "p7-low", "p9-off", "p9-low", "glm-off"]
|
||
CROSS = ARMS[:4]
|
||
fails, warns = [], []
|
||
|
||
|
||
def ro(db, q, a=()):
|
||
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
|
||
con.row_factory = sqlite3.Row
|
||
r = con.execute(q, a).fetchall()
|
||
con.close()
|
||
return r
|
||
|
||
|
||
def cjk_count(t):
|
||
return sum(1 for c in (t or "") if 0x4E00 <= ord(c) <= 0x9FFF or 0x3400 <= ord(c) <= 0x4DBF)
|
||
|
||
|
||
def cyr_share(t):
|
||
import unicodedata
|
||
letters = [c for c in (t or "") if unicodedata.category(c).startswith("L")]
|
||
if not letters:
|
||
return 0.0
|
||
return sum(1 for c in letters if "CYRILLIC" in unicodedata.name(c, "")) / len(letters)
|
||
|
||
|
||
print("═" * 92)
|
||
print("АССЕРТЫ ПРЕД-ПОЛЁТА — стоп-критерии пачки, исполняемые")
|
||
print("═" * 92)
|
||
|
||
# ── 1. КЛЕТКИ: цена, попытка 0, доставка ────────────────────────────────────────────────────
|
||
print("\n## 1. КЛЕТКИ ЮНИТА")
|
||
cells = {}
|
||
for arm in ARMS:
|
||
db = B / "arms" / f"{arm}.db"
|
||
rows = ro(db, """
|
||
select rl.chapter, rl.chunk_idx, rl.prompt_tokens pt, rl.cached_tokens ct,
|
||
rl.completion_tokens cmpl, rl.cost_usd, rl.finish_reason fin, rl.model_requested model,
|
||
cp.attempt, cp.response_text resp
|
||
from request_log rl left join checkpoints cp on cp.request_hash = rl.request_hash
|
||
where rl.stage='edit' and rl.tm_hit=0 order by rl.id""")
|
||
if not rows:
|
||
fails.append(f"{arm}: НИ ОДНОЙ редакторской строки — клетка не куплена")
|
||
continue
|
||
a0 = next((r for r in rows if r["attempt"] == 0), None)
|
||
if a0 is None:
|
||
fails.append(f"{arm}: нет попытки 0 (реконструкция через checkpoints не нашла attempt=0)")
|
||
continue
|
||
st = ro(db, "select chapter, chunk_idx, disposition, flag_reason, cost_usd, final_hash, content_hash "
|
||
"from chunk_status where stage='edit'")
|
||
unit_cost = sum(r["cost_usd"] for r in st)
|
||
shipped = ""
|
||
if st and st[0]["final_hash"]:
|
||
sh = ro(db, "select response_text from checkpoints where request_hash=?", (st[0]["final_hash"],))
|
||
shipped = sh[0][0] if sh else ""
|
||
rh = None if arm == "glm-off" else a0["cmpl"] - COEF * len(a0["resp"] or "")
|
||
cells[arm] = {
|
||
"key": (a0["chapter"], a0["chunk_idx"]), "pt": a0["pt"], "ct": a0["ct"], "cmpl": a0["cmpl"],
|
||
"cost0": a0["cost_usd"], "fin": a0["fin"], "model": a0["model"], "resp": a0["resp"] or "",
|
||
"attempts": len(rows), "unit_cost": unit_cost, "shipped": shipped,
|
||
"content_hash": st[0]["content_hash"] if st else "", "disp": st[0]["disposition"] if st else "?",
|
||
"flag": st[0]["flag_reason"] if st else "", "rhat": rh,
|
||
}
|
||
c = cells[arm]
|
||
print(f" {arm:8s} юнит {c['key']} · попыток {c['attempts']} · pt {c['pt']} (кэш {c['ct']}) · "
|
||
f"cmpl {c['cmpl']} · finish {c['fin']} · клетка ${c['cost0']:.6f} · юнит ${c['unit_cost']:.6f}")
|
||
print(f" диспозиция {c['disp']}{'/' + c['flag'] if c['flag'] else ''} · сырой ответ "
|
||
f"{len(c['resp'])} зн · отгружено {len(c['shipped'])} зн · "
|
||
f"R̂ {('%.0f' % c['rhat']) if c['rhat'] is not None else '0 (по построению)'}")
|
||
|
||
if len(cells) != 5:
|
||
fails.append(f"клеток куплено {len(cells)} из 5")
|
||
|
||
# ── 2. ⛔ ОДИН И ТОТ ЖЕ ЮНИТ У ПЯТИ РУК ──────────────────────────────────────────────────────
|
||
print("\n## 2. БЛОКИРОВКА ТРУДНОСТИ: пять рук обязаны редактировать ОДИН юнит")
|
||
keys = {a: c["key"] for a, c in cells.items()}
|
||
print(f" ключи: {keys}")
|
||
if len(set(keys.values())) != 1:
|
||
fails.append(f"руки взяли РАЗНЫЕ юниты: {keys} — блокировка трудности сломана")
|
||
else:
|
||
print(" ✓ юнит один")
|
||
|
||
# ── 3. ⛔ ПОБАЙТНАЯ ИДЕНТИЧНОСТЬ ВХОДА ВНУТРИ ПРОМПТА ────────────────────────────────────────
|
||
print("\n## 3. content_hash — побайтная сигнатура отрендеренных сообщений")
|
||
ch = {a: c["content_hash"] for a, c in cells.items()}
|
||
for a, h in ch.items():
|
||
print(f" {a:8s} {h[:24] or '(пусто)'}")
|
||
# ⚠ только НЕПУСТЫЕ хеши: на пустом стенде оба множества были бы {None} и «совпадение»
|
||
# отрапортовалось бы ложно — ровно тот класс ложной находки, за который здесь ловят.
|
||
p7 = {h for h in (ch.get("p7-off"), ch.get("p7-low")) if h}
|
||
p9 = {h for h in (ch.get("p9-off"), ch.get("p9-low"), ch.get("glm-off")) if h}
|
||
if len(p7) != 1 and p7:
|
||
fails.append(f"content_hash РАЗНЫЙ внутри пары P7: {p7} — руки видят разный вход, а должны только разный ЭФФОРТ")
|
||
if len(p9) != 1 and p9:
|
||
fails.append(f"content_hash РАЗНЫЙ внутри группы P9: {p9}")
|
||
if p7 and p9 and (p7 & p9):
|
||
fails.append("content_hash СОВПАЛ между P7 и P9 — ось промпта не доехала до сообщений")
|
||
if len(p7) == 1 and len(p9) == 1 and not (p7 & p9):
|
||
print(" ✓ внутри P7 одинаков, внутри P9 одинаков, между P7 и P9 РАЗЛИЧЕН — ровно как должно быть")
|
||
|
||
# ── 4. СИГНАТУРА Δprompt_tokens ─────────────────────────────────────────────────────────────
|
||
print("\n## 4. СИГНАТУРА Δprompt_tokens (∅ − low) — её пинит ФРИЗ №2")
|
||
sig = {}
|
||
for label, off, low in (("P7", "p7-off", "p7-low"), ("P9", "p9-off", "p9-low")):
|
||
if off in cells and low in cells:
|
||
sig[label] = cells[off]["pt"] - cells[low]["pt"]
|
||
print(f" {label}: {cells[off]['pt']} − {cells[low]['pt']} = {sig[label]}")
|
||
if not sig:
|
||
fails.append("сигнатура не снята — нет пар попыток 0")
|
||
|
||
# ── 5. ⛔ КИРИЛЛИЦА И ЭХО — СТОП-КРИТЕРИИ ПАЧКИ ─────────────────────────────────────────────
|
||
print("\n## 5. КИРИЛЛИЦА ≥0.99 И ЭХО = 0 (по СЫРОМУ ответу: санитайзер эхо вырезает)")
|
||
for arm, c in cells.items():
|
||
raw_cjk, ship_cjk = cjk_count(c["resp"]), cjk_count(c["shipped"])
|
||
raw_cyr, ship_cyr = cyr_share(c["resp"]), cyr_share(c["shipped"])
|
||
print(f" {arm:8s} сырой: CJK {raw_cjk:4d} кириллица {raw_cyr:.4f} | отгружено: CJK {ship_cjk:4d} "
|
||
f"кириллица {ship_cyr:.4f}")
|
||
if arm in CROSS:
|
||
if raw_cjk > 0:
|
||
fails.append(f"{arm}: ЭХО в сыром ответе ({raw_cjk} иероглифов) — стоп-критерий пред-полёта")
|
||
if raw_cyr < 0.99:
|
||
fails.append(f"{arm}: кириллица {raw_cyr:.4f} < 0.99 — стоп-критерий пред-полёта")
|
||
else:
|
||
if raw_cjk > 0:
|
||
warns.append(f"референс glm-off: CJK {raw_cjk} в сыром ответе — ожидаемая зона эха "
|
||
f"(thinking выключен), в E5 идёт ОТДЕЛЬНОЙ строкой, пачку не блокирует")
|
||
|
||
# ── 6. ГЕЙТЫ РЕФЕРЕНСА ──────────────────────────────────────────────────────────────────────
|
||
print("\n## 6. ГЕЙТЫ РЕФЕРЕНСА glm-5")
|
||
if "glm-off" in cells:
|
||
g = cells["glm-off"]
|
||
resid = g["cmpl"] - COEF * len(g["resp"])
|
||
rel = abs(resid) / g["cmpl"] if g["cmpl"] else 1.0
|
||
print(f" остаток completion − {COEF}·len(resp) = {g['cmpl']} − {COEF * len(g['resp']):.0f} = {resid:+.0f}"
|
||
f" ({rel:.1%} от completion)")
|
||
print(" ⚠ «R̂ = 0 по построению» — определение. УЛИКА, что thinking действительно выключен, — "
|
||
"именно этот остаток.")
|
||
if rel > 0.15:
|
||
warns.append(f"референс: остаток {rel:.1%} > 15% — на модели с выключенным thinking ожидался "
|
||
f"близкий к нулю; либо тумблер не доехал, либо у glm другой токенизатор")
|
||
|
||
# ── 7. ⛔ ЦЕНА КЛЕТКИ low ПРОТИВ ПОРОГА, ОБЪЯВЛЕННОГО ДО ЧИСЕЛ ──────────────────────────────
|
||
print(f"\n## 7. ЦЕНА КЛЕТКИ low ПРОТИВ ПОРОГА ${LOW_THRESHOLD} (объявлен в пре-реге ДО покупки)")
|
||
lows = [cells[a]["cost0"] for a in ("p7-low", "p9-low") if a in cells]
|
||
offs = [cells[a]["cost0"] for a in ("p7-off", "p9-off") if a in cells]
|
||
if lows:
|
||
m = sum(lows) / len(lows)
|
||
print(f" low: {['$%.6f' % x for x in lows]} ⇒ среднее ${m:.6f}")
|
||
print(f" ∅ : {['$%.6f' % x for x in offs]}")
|
||
if m > LOW_THRESHOLD:
|
||
fails.append(f"СРЕДНЯЯ КЛЕТКА low ${m:.6f} > порога ${LOW_THRESHOLD} — СТОП И ВОПРОС ВЛАДЕЛЬЦУ "
|
||
f"до основной покупки (выше в замороженной смете сценария нет)")
|
||
else:
|
||
print(f" ✓ ${m:.6f} ≤ ${LOW_THRESHOLD} — иду дальше")
|
||
for a in ("p7-off", "p9-off"):
|
||
if a in cells and cells[a]["cost0"] > 0.20:
|
||
warns.append(f"{a}: клетка ${cells[a]['cost0']:.6f} > $0.20 — отметка (симметрия к порогу low), "
|
||
f"не остановка")
|
||
|
||
# ── 8. ПРОВОДНАЯ УЛИКА ИЗ СЫРЫХ ТЕЛ ─────────────────────────────────────────────────────────
|
||
print("\n## 8. ПРОВОДНАЯ УЛИКА — что реально уехало в теле запроса")
|
||
for arm in ARMS:
|
||
log = B / "logs" / f"pre-{arm}.log"
|
||
if not log.exists():
|
||
warns.append(f"{arm}: лога {log.name} нет — проводная улика не снята")
|
||
continue
|
||
txt = log.read_text(errors="replace")
|
||
n_ex = txt.count("llm exchange")
|
||
# ⚠ ТЕЛО ЛОГИРУЕТСЯ ЭКРАНИРОВАННЫМ: slog печатает request="{\"max_tokens\":16000,...}".
|
||
# Первая редакция грепа искала неэкранированные кавычки, ничего не находила и рапортовала
|
||
# «ключа нет» — то есть дефект ИНСТРУМЕНТА выглядел как находка о проводе. Поймано
|
||
# положительным контролем: `grep -c reasoning_effort` даёт 1 там, где регулярка давала 0.
|
||
def find(key, val=r'\\?"([\w.\-]+)\\?"'):
|
||
# экранированы ОБЕ пары кавычек: \"reasoning_effort\":\"low\"
|
||
return sorted(set(re.findall(r'\\?"' + key + r'\\?":\s*' + val, txt)))
|
||
eff = find("reasoning_effort")
|
||
think = sorted(set(re.findall(r'\\?"thinking\\?":\s*\{\\?"type\\?":\s*\\?"(\w+)', txt)))
|
||
mt = sorted(set(int(x) for x in find("max_tokens", r'(\d+)')))
|
||
tmp = sorted(set(find("temperature", r'([\d.]+)')))
|
||
mdl = find("model")
|
||
print(f" {arm:8s} обменов {n_ex} · reasoning_effort {eff or '—'} · thinking {think or '—'} · "
|
||
f"max_tokens {mt or '—'} · temperature {tmp or '—'} · model {mdl or '—'}")
|
||
# ⛔ ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ НА ГРЕП: «ключа нет» обязано отличаться от «лога нет».
|
||
if n_ex == 0:
|
||
warns.append(f"{arm}: в логе НОЛЬ строк «llm exchange» — «ключа нет» здесь ничего не доказывает")
|
||
continue
|
||
want_eff = "low" if arm.endswith("low") else None
|
||
if want_eff and eff != [want_eff]:
|
||
fails.append(f"{arm}: в теле reasoning_effort {eff}, а должно быть ['{want_eff}']")
|
||
if not want_eff and arm != "glm-off" and eff:
|
||
fails.append(f"{arm}: в теле ЕСТЬ reasoning_effort {eff}, а у ∅-руки ключа быть не должно")
|
||
if arm == "glm-off" and think != ["disabled"]:
|
||
fails.append(f"{arm}: thinking в теле {think}, а должно быть ['disabled']")
|
||
|
||
# ── 9. ПОТОЛОК ВЫВОДА ОДИНАКОВ ВО ВСЕХ ЧЕТЫРЁХ РУКАХ КРЕСТА ─────────────────────────────────
|
||
print("\n## 9. ПОТОЛОК ВЫВОДА (требование §14: одинаков во всех руках креста)")
|
||
mts = {}
|
||
for arm in ARMS:
|
||
log = B / "logs" / f"pre-{arm}.log"
|
||
if log.exists():
|
||
m = re.findall(r'msg="calling model".*?max_tokens=(\d+)', log.read_text(errors="replace"))
|
||
edits = sorted(set(int(x) for x in m))
|
||
mts[arm] = edits
|
||
print(f" {arm:8s} max_tokens в вызовах: {edits}")
|
||
cross_mt = {a: [x for x in v if x >= 16000] for a, v in mts.items() if a in CROSS}
|
||
flat = {tuple(v) for v in cross_mt.values() if v}
|
||
if len(flat) > 1:
|
||
fails.append(f"потолок вывода РАЗЛИЧЕН между руками креста: {cross_mt} — конфаунд, убивший прошлый замер")
|
||
elif flat:
|
||
print(f" ✓ у четырёх рук креста потолок один: {sorted(flat)[0]}")
|
||
if "glm-off" in mts:
|
||
print(f" ⚠ у референса свой потолок ({mts['glm-off']}): у glm-5 нет min_max_tokens, «равный потолок» "
|
||
f"на него НЕ распространяется — это печатается рядом с его числом")
|
||
|
||
# ── ИТОГ ────────────────────────────────────────────────────────────────────────────────────
|
||
print("\n" + "═" * 92)
|
||
for w in warns:
|
||
print(f"⚠ {w}")
|
||
if fails:
|
||
print("\n⛔ ПРЕД-ПОЛЁТ НЕ ПРОШЁЛ:")
|
||
for f in fails:
|
||
print(f" · {f}")
|
||
print("\nПАЧКУ НЕ НАЧИНАТЬ. Это стоп-критерии §5.1, а не рекомендации.")
|
||
sys.exit(1)
|
||
print("✓ ПРЕД-ПОЛЁТ ПРОЙДЕН — все стоп-критерии §5.1 исполнены командой, а не осмотрены глазами.")
|
||
print(f" сигнатуру {sig} пинит ФРИЗ №2 — до пачки.")
|
||
sys.exit(0)
|