#!/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)