#!/usr/bin/env python3 # -*- coding: utf-8 -*- """chetyre.py — прибор контролируемой пробы редактора по четырём осям. ЗАКОН: eval/dovodka/PLAN-01-09.md (пре-рег, фриз документа dc602e9). Этот файл — ИНСТРУМЕНТ, он реализует правило решения §4, а НЕ выбирает его. Всякий порог здесь запинен ДО чисел. ⛔ ГЛАВНЫЙ ГАРД ФАЙЛА: прибор ОТКАЗЫВАЕТ (код 2), а не печатает «в пределах», когда знаменатель пуст. «Зелень на пустоте» — способ, которым замер молча превращается в ничто. ──────────────────────────────────────────────────────────────────────────────────────────────── РЕКОНСТРУКЦИИ, НАЗВАННЫЕ ЯВНО (каждая — место, где прибор мог бы соврать) 1. НОМЕР ПОПЫТКИ. В `request_log` колонки `attempt` НЕТ (проверено `.schema`; положительный контроль — она есть в `checkpoints`). Реконструкция: `request_log LEFT JOIN checkpoints ON request_hash`. Проверено на холодном прогоне: 4 из 5 edit-строк находят чекпойнт, пятая — `context canceled` с нулевой ценой и пустым request_hash. ⇒ строки БЕЗ чекпойнта считаются вызовами-без-ответа: они входят в деньги и в счётчик, но не несут попытки и не дают клетки. 2. «ДОСТАВЛЕНО» (`delivered`). Меряется на ОТГРУЖАЕМОМ тексте, а не по имени флага. Отгружаемый текст = `checkpoints.response_text` по `chunk_status.final_hash` (final_hash может быть синтетическим `tm-sanitized-v1:` — он ТОЖЕ лежит в checkpoints, проверено). delivered = 0 ⟺ диспозиция `skipped`, ИЛИ final_hash не резолвится, ИЛИ отгружаемый текст не проходит $0-предикат доставки: доля кириллицы среди букв < 0.99 либо в тексте есть CJK. Иначе delivered = длина отгружаемого текста в знаках. ⚠ ПОЧЕМУ НЕ «flagged ⇒ 0»: в холодном прогоне 1 из 3 редакторских юнитов — `sanitizer_stripped` (CJK-утечка, санитайзер её вырезал и текст ОТГРУЖЕН, 6048 знаков). По правилу «flagged ⇒ 0» гард «>20% пустых ⇒ прибор отказывает» сработал бы от санитайзера, а не от предмета замера. Флаги при этом не теряются — они целиком идут в E5, разделённые по механизму. ⚠ ОБЪЯВЛЕНО ДО ЧИСЕЛ: строгий вариант (`flagged ⇒ delivered = 0`) считается ВСЕГДА и печатается рядом как заранее объявленная проверка устойчивости. Выбор основного определения сделан до первой платной клетки и после просмотра чисел не меняется. 3. РАЗМЫШЛЕНИЕ. Движок его не видит (`provider_openai.go` — `ReasoningTokens stays 0`; проверено на базе холодного прогона: 0 строк из 68 с reasoning_tokens>0 при 38 строках с непустым completion). Восстановление, коэффициент запинен пре-регом: R̂ = completion_tokens − 0.3644 · len(сырой response_text попытки) ⚠ Границы, названные планом ДО покупки: (1) любой CJK в выходе ломает оценку (иероглиф ≈1 токен) ⇒ R̂ завышается; (2) при R̂ < ~700 относительная ошибка 20-70% — о величине таких клеток вывод не делается; (3) берётся СЫРАЯ строка попытки, не `sanitized_export`; (4) типичная ошибка ~3%, худшая клетка 5.3%. ⛔ У РЕФЕРЕНСА `glm-5` R̂ = 0 ПО ПОСТРОЕНИЮ, а не по формуле: у него другой токенизатор и thinking выключен; формула напечатала бы «думает ±300 токенов» на модели, которая не думает. 4. ЦЕНЗУРА (правило §4, СИММЕТРИЧНО, записано до чисел). Клетка с `finish_reason='length'` на попытке 0 цензурирована справа: её R̂ — НИЖНЯЯ граница. Знак пары (∅ против low): · цензурирована ∅ — знак достоверен, пока R̂(∅) ≥ R̂(low); иначе НИЧЬЯ; · цензурирована low — знак достоверен, только если R̂(∅) ≤ R̂(low); иначе НИЧЬЯ; · цензурированы обе — НИЧЬЯ. ⚠ НИЧЬЯ ≠ ВЫБРОШЕНО: ничья = разность 0, и она СОХРАНЯЕТСЯ. Поэтому `zero_method='pratt'` (умолчание `wilcox` нули ВЫБРАСЫВАЕТ — проверено исполнением на scipy 1.18.0). 5. КОНТРАСТ КРЕСТА. Главный эффект усилия на юните — среднее двух разностей: d_u = ½[logR̂(P₇low)+logR̂(P₉low)] − ½[logR̂(P₇∅)+logR̂(P₉∅)] Именно у него sd = σ, а не σ√2 (план §4 «Мощность»): var(d) = 4·(σ²/4) = σ². Ось промпта (E3) и взаимодействие (E4) — те же четыре клетки, другие знаки. 6. ДОВЕРИТЕЛЬНЫЙ ИНТЕРВАЛ ХОДЖЕСА-ЛЕМАНА — РУКАМИ. `scipy.stats.wilcoxon` интервала не отдаёт (проверено исполнением: у результата поля statistic/pvalue/count/index, метода confidence_interval нет). Реализовано по Уолшевым средним с рангово-точной отсечкой. """ import argparse import json import math import os import sqlite3 import sys import unicodedata from pathlib import Path import numpy as np from scipy.stats import wilcoxon, spearmanr, t as student_t # ── ЗАПИНЕННОЕ ПРЕ-РЕГОМ (менять здесь = менять пре-рег; после чисел запрещено) ─────────────── COEF_VISIBLE = 0.3644 # токенов на знак видимого выхода pro-редактора zh→ru ALPHA = 0.05 # двусторонне SIGNATURE_TOLERANCE = 2 # ±2 токена от снятого пред-полётом значения SIGNATURE_FAIL_SHARE = 0.20 # >20% юнитов вне допуска ⇒ проба аннулируется EMPTY_REFUSE_SHARE = 0.20 # >20% пустых в руке КРЕСТА ⇒ прибор отказывает SMALL_RHAT = 700 # ниже — о величине клетки вывод не делается BAND = (0.8, 1.25) # описательная полоса; решения НЕ несёт CROSS_ARMS = ["p7-off", "p7-low", "p9-off", "p9-low"] REFERENCE_ARM = "glm-off" ALL_ARMS = CROSS_ARMS + [REFERENCE_ARM] OFF_ARMS = ["p7-off", "p9-off"] LOW_ARMS = ["p7-low", "p9-low"] P7_ARMS = ["p7-off", "p7-low"] P9_ARMS = ["p9-off", "p9-low"] # Ожидаемая подпись edit-стадии каждой руки — ГАРД ПРОТИВ СХЛОПЫВАНИЯ ОСИ. # Хеши промптов запиниваются пре-регом; сюда они приходят из манифеста, а не из головы. EXPECTED = { "p7-off": {"prompt": "P7", "reasoning": "off", "model": "deepseek-v4-pro"}, "p7-low": {"prompt": "P7", "reasoning": "low", "model": "deepseek-v4-pro"}, "p9-off": {"prompt": "P9", "reasoning": "off", "model": "deepseek-v4-pro"}, "p9-low": {"prompt": "P9", "reasoning": "low", "model": "deepseek-v4-pro"}, "glm-off": {"prompt": "P9", "reasoning": "off", "model": "glm-5"}, } # Пиковые цены за 1M токенов — из backend/configs/models.yaml. ⚠ Леджер букирует ПИК всегда, # и число владельца тоже пиковое. Сверено с леджером холодного прогона до цента: # строка pt=4412 cmpl=12011 → 4412·1.32/1e6 + 12011·3.96/1e6 = $0.053388 против $0.053387. PRICES = { "deepseek-v4-pro": {"in": 1.32, "cached": 0.044, "out": 3.96}, # models.yaml:198 "deepseek-v4-flash": {"in": 0.44, "cached": 0.014, "out": 1.32}, # models.yaml:183 "glm-5": {"in": 1.00, "cached": 0.200, "out": 3.20}, # models.yaml:223 } REFUSAL = 2 # код выхода «прибор отказывает» class Refuse(Exception): """Прибор отказывает: знаменатель пуст или гард дизайна не прошёл.""" def is_cjk(ch): o = ord(ch) return (0x4E00 <= o <= 0x9FFF or 0x3400 <= o <= 0x4DBF or 0xF900 <= o <= 0xFAFF or 0x3040 <= o <= 0x30FF) def delivery_predicate(text): """$0-предикат доставки. Возвращает (годен, доля_кириллицы, число_CJK).""" if not text: return False, 0.0, 0 cjk = sum(1 for c in text if is_cjk(c)) letters = [c for c in text if unicodedata.category(c).startswith("L")] cyr = sum(1 for c in letters if "CYRILLIC" in unicodedata.name(c, "")) share = cyr / len(letters) if letters else 0.0 return (share >= 0.99 and cjk == 0), share, cjk # ── ЧТЕНИЕ БАЗ ─────────────────────────────────────────────────────────────────────────────── def open_db(path): if not Path(path).exists(): raise Refuse(f"базы нет: {path} (положительный контроль: ls каталога рук)") con = sqlite3.connect(f"file:{path}?mode=ro", uri=True) con.row_factory = sqlite3.Row return con def read_arm(path, arm): """Все edit-клетки одной руки. Ключ клетки — (chapter, chunk_idx).""" con = open_db(path) cells = {} # Реконструкция №1: попытка приходит из checkpoints через request_hash. rows = con.execute(""" select rl.id, rl.chapter, rl.chunk_idx, rl.model_requested, rl.model_actual, rl.prompt_tokens, rl.cached_tokens, rl.completion_tokens, rl.reasoning_tokens, rl.cost_usd, rl.finish_reason, rl.tm_hit, rl.ok, rl.err, rl.request_hash, cp.attempt as attempt, cp.response_text as resp from request_log rl left join checkpoints cp on cp.request_hash = rl.request_hash where rl.stage = 'edit' order by rl.id """).fetchall() for r in rows: key = (r["chapter"], r["chunk_idx"]) c = cells.setdefault(key, {"arm": arm, "unit": key, "calls": [], "orphan_calls": 0}) resp = r["resp"] call = { "id": r["id"], "attempt": r["attempt"], "model": r["model_requested"], "prompt_tokens": r["prompt_tokens"], "cached_tokens": r["cached_tokens"], "completion_tokens": r["completion_tokens"], "reasoning_tokens_engine": r["reasoning_tokens"], "cost_usd": r["cost_usd"], "finish": r["finish_reason"], "tm_hit": r["tm_hit"], "ok": r["ok"], "err": r["err"], "resp_len": len(resp) if resp is not None else None, "resp": resp, } if r["attempt"] is None: c["orphan_calls"] += 1 c["calls"].append(call) # Диспозиция и отгружаемый текст for r in con.execute(""" select cs.chapter, cs.chunk_idx, cs.disposition, cs.flag_reason, cs.first_flag_reason, cs.attempts, cs.cost_usd, cs.final_hash, cs.detail, cs.snapshot_id, (select c.response_text from checkpoints c where c.request_hash = cs.final_hash) as shipped from chunk_status cs where cs.stage = 'edit' """): key = (r["chapter"], r["chunk_idx"]) c = cells.setdefault(key, {"arm": arm, "unit": key, "calls": [], "orphan_calls": 0}) shipped = r["shipped"] good, cyr, cjk = delivery_predicate(shipped or "") c.update({ "disposition": r["disposition"], "flag_reason": r["flag_reason"], "first_flag_reason": r["first_flag_reason"], "attempts": r["attempts"], "unit_cost_usd": r["cost_usd"], "final_hash": r["final_hash"], "detail": r["detail"], "unit_snapshot": r["snapshot_id"], "shipped_len": len(shipped) if shipped else 0, "cyr_share": cyr, "cjk_in_shipped": cjk, "delivered": (len(shipped) if (good and r["disposition"] != "skipped") else 0), # строгий вариант, объявленный ДО чисел "delivered_strict": (len(shipped) if (good and r["disposition"] == "ok") else 0), }) # Длина черновика по ЧАНКАМ. ⚠ Это ещё НЕ длина входа редакторского юнита: юнит редактуры # охватывает несколько чанков черновика, и в этом стенде таких 15 из 20. Сборка юнита из чанков # делается по манифесту движка — см. unit_draft_lengths(). draft_len = {} for r in con.execute(""" select cs.chapter, cs.chunk_idx, (select length(c.response_text) from checkpoints c where c.request_hash = cs.final_hash) as n from chunk_status cs where cs.stage = 'draft' """): draft_len[(r["chapter"], r["chunk_idx"])] = r["n"] snaps = {r["snapshot_id"]: r["payload"] for r in con.execute("select snapshot_id, payload from snapshots")} jobs = [dict(r) for r in con.execute("select chapter, stage, status, snapshot_id from jobs")] spend = con.execute("select coalesce(sum(committed_usd+reserved_usd),0) from spend").fetchone()[0] con.close() return cells, draft_len, snaps, jobs, spend def unit_draft_lengths(manifest_path, chunk_len): """Длина ВХОДА редакторского юнита = сумма его чанков черновика. ⛔ ЗАЧЕМ ОТДЕЛЬНАЯ ФУНКЦИЯ, А НЕ chunk_len НАПРЯМУЮ: юнит редактуры и чанк черновика — РАЗНЫЕ единицы. Манифест этого стенда: 20 юнитов на 35 чанков, и у 15 юнитов из 20 `chunk_count > 1`. Взяв длину только первого чанка (ключ (глава, chunk_idx) совпадает у обоих!), ковариата длины занизилась бы у трёх четвертей выборки — и это НЕ дало бы ошибки, а тихо испортило бы проверку. Соответствие: юнит (глава, k) есть k-й элемент `chapters[глава−1].units`, а его чанки черновика — `first_chunk_idx … first_chunk_idx + chunk_count − 1` в той же главе. """ m = json.loads(Path(manifest_path).read_text()) out = {} for ch in m.get("chapters", []): num = ch["number"] for k, u in enumerate(ch.get("units", [])): total, missing = 0, 0 for ci in range(u["first_chunk_idx"], u["first_chunk_idx"] + u["chunk_count"]): n = chunk_len.get((num, ci)) if n is None: missing += 1 else: total += n out[(num, k)] = None if missing else total return out def rhat(call, arm): """Восстановленное размышление. У референса — 0 ПО ПОСТРОЕНИЮ (реконструкция №3).""" if arm == REFERENCE_ARM: return 0.0 if call["resp_len"] is None or call["completion_tokens"] is None: return None return call["completion_tokens"] - COEF_VISIBLE * call["resp_len"] def attempt0(cell): """Попытка 0 клетки. ⚠ ЗАЩИТА, КУПЛЕННАЯ ЧТЕНИЕМ ДАННЫХ, А НЕ ДОГАДКОЙ: интерливинг гоняет каждую руку по 20 раз с `--max-units 1`, и КАЖДЫЙ такой прогон дописывает в `request_log` строку-ПОВТОР уже оплаченной клетки с `tm_hit=1`. Проверено на базе холодного прогона: у 29 строк с `tm_hit=1` ровно нули — `prompt_tokens=0, completion_tokens=0, cost_usd=0` — а `request_hash` у них ТОТ ЖЕ, то есть они соединяются с тем же чекпойнтом и несут `attempt=0`. Взять такую строку за попытку 0 значило бы посчитать R̂ от нулей. Поэтому настоящий вызов (`tm_hit=0`) выбирается ЯВНО, а не по порядку id. """ hits = [c for c in cell.get("calls", []) if c["attempt"] == 0] for c in hits: if not c["tm_hit"]: return c return hits[0] if hits else None # ── ГАРДЫ ДИЗАЙНА ──────────────────────────────────────────────────────────────────────────── def stage_of(payload, name): p = json.loads(payload) for s in p.get("stages", []): if s.get("name") == name: return s, p return None, p def gate_axis(arm_data, pins, out): """⛔ Не схлопнулась ли ось. Сильнее, чем «пять разных snapshot_id»: сверяем ПОЛЯ.""" out.append("## ГАРД 1 — ОСЬ НЕ СХЛОПНУЛАСЬ (edit-снапшот каждой руки)") edit_ids, draft_ids, problems = {}, {}, [] for arm in ALL_ARMS: snaps, jobs = arm_data[arm]["snaps"], arm_data[arm]["jobs"] eids = {j["snapshot_id"] for j in jobs if j["stage"] == "edit"} dids = {j["snapshot_id"] for j in jobs if j["stage"] == "draft"} edit_ids[arm], draft_ids[arm] = eids, dids seen = set() for sid in eids: st, _ = stage_of(snaps[sid], "edit") if st is None: problems.append(f"{arm}: в снапшоте {sid[:12]} нет стадии edit") continue got = (st.get("prompt_sha256"), st.get("reasoning"), st.get("model")) seen.add(got) want = EXPECTED[arm] wsha = pins[want["prompt"]] if got[0] != wsha: problems.append(f"{arm}: prompt_sha256 {got[0][:12]}… ≠ запиненного {want['prompt']} {wsha[:12]}…") if got[1] != want["reasoning"]: problems.append(f"{arm}: reasoning '{got[1]}' ≠ '{want['reasoning']}' — ОСЬ УСИЛИЯ НЕ УЕХАЛА НА ПРОВОД") if got[2] != want["model"]: problems.append(f"{arm}: model '{got[2]}' ≠ '{want['model']}'") for sha, reas, mdl in sorted(seen): out.append(f" {arm:8s} edit: prompt_sha256 {sha[:16]}… reasoning '{reas}' model {mdl}") all_edit = set().union(*edit_ids.values()) if edit_ids else set() out.append(f" различных edit-снапшотов по пяти рукам: {len(all_edit)} (нужно ≥5)") if len(all_edit) < 5: problems.append(f"edit-снапшотов всего {len(all_edit)} — руки схлопнулись, платили за пять, мерили меньше") all_draft = set().union(*draft_ids.values()) if draft_ids else set() out.append(f" различных draft-снапшотов по пяти рукам: {len(all_draft)} (нужно РОВНО 1 — иначе черновики перекуплены)") if len(all_draft) != 1: problems.append(f"draft-снапшотов {len(all_draft)} ≠ 1: {sorted(s[:12] for s in all_draft)} — черновики НЕ общие") return problems def gate_signature(arm_data, pinned, out): """Сигнатура Δprompt_tokens(∅ − low) по каждому промпту. Косвенная улика провода.""" out.append("\n## ГАРД 2 — СИГНАТУРА Δprompt_tokens (∅ − low), попытка 0") res, bad, total = {}, 0, 0 for label, (a_off, a_low) in (("P7", ("p7-off", "p7-low")), ("P9", ("p9-off", "p9-low"))): deltas = {} for unit in sorted(set(arm_data[a_off]["cells"]) & set(arm_data[a_low]["cells"])): c0, c1 = attempt0(arm_data[a_off]["cells"][unit]), attempt0(arm_data[a_low]["cells"][unit]) if not c0 or not c1: continue deltas[unit] = c0["prompt_tokens"] - c1["prompt_tokens"] res[label] = deltas if not deltas: out.append(f" {label}: нет ни одной пары попыток 0 — сигнатура НЕ СНЯТА") continue vals = list(deltas.values()) uniq = sorted(set(vals)) out.append(f" {label}: n={len(vals)} различных значений {uniq} медиана {int(np.median(vals))}") pin = pinned.get(label) if pin is None: out.append(f" (значение НЕ ЗАПИНЕНО — это пред-полёт: оно и есть то, что пинится коммитом)") else: off = [u for u, v in deltas.items() if abs(v - pin) > SIGNATURE_TOLERANCE] total += len(vals) bad += len(off) out.append(f" запинено {pin} ±{SIGNATURE_TOLERANCE} ⇒ вне допуска {len(off)}/{len(vals)}") problems = [] if total and bad / total > SIGNATURE_FAIL_SHARE: problems.append(f"сигнатура не воспроизвелась у {bad}/{total} = {bad/total:.0%} юнитов (>{SIGNATURE_FAIL_SHARE:.0%}) ⇒ ПРОБА АННУЛИРУЕТСЯ") return res, problems def gate_empty(arm_data, out): """⛔ Зелень на пустоте. Правило действует ТОЛЬКО на четыре руки креста (план §5.2).""" out.append("\n## ГАРД 3 — ЗЕЛЕНЬ НА ПУСТОТЕ (delivered = 0)") problems = [] for arm in ALL_ARMS: cells = arm_data[arm]["cells"] done = [c for c in cells.values() if "delivered" in c] if not done: out.append(f" {arm:8s}: ни одной завершённой клетки") continue empty = [c for c in done if c["delivered"] == 0] empty_s = [c for c in done if c["delivered_strict"] == 0] share = len(empty) / len(done) mark = "" if arm in CROSS_ARMS and share > EMPTY_REFUSE_SHARE: problems.append(f"{arm}: пустых {len(empty)}/{len(done)} = {share:.0%} > {EMPTY_REFUSE_SHARE:.0%} ⇒ ПРИБОР ОТКАЗЫВАЕТ") mark = " ⛔" if arm == REFERENCE_ARM and share > EMPTY_REFUSE_SHARE: mark = " (референс: в E0 не печатается, доля идёт строкой в E5 — план §5.2)" out.append(f" {arm:8s}: пустых {len(empty)}/{len(done)} = {share:.0%}" f" | строгий вариант {len(empty_s)}/{len(done)} = {len(empty_s)/len(done):.0%}{mark}") return problems # ── СТАТИСТИКА ─────────────────────────────────────────────────────────────────────────────── def hodges_lehmann(d): """Медиана Ходжеса-Лемана = медиана Уолшевых средних (d_i+d_j)/2, i ≤ j.""" d = np.asarray(d, dtype=float) n = len(d) if n == 0: return None w = np.array([(d[i] + d[j]) / 2 for i in range(n) for j in range(i, n)]) return float(np.median(w)), w def hl_ci(d, alpha=ALPHA): """95% ДИ Ходжеса-Лемана. РУКАМИ: scipy.wilcoxon интервала не отдаёт (проверено). Отсечка k по нормальному приближению знаково-рангового распределения с поправкой на непрерывность. Для маленьких n это приближение — ЗАЯВЛЕННОЕ ограничение, а не скрытое. """ d = np.asarray(d, dtype=float) n = len(d) if n < 6: return None hl, w = hodges_lehmann(d) w = np.sort(w) m = len(w) from scipy.stats import norm z = norm.ppf(1 - alpha / 2) se = math.sqrt(n * (n + 1) * (2 * n + 1) / 24.0) k = int(math.floor(n * (n + 1) / 4.0 - z * se - 0.5)) if k < 0: k = 0 if k >= m // 2: return None return float(w[k]), float(w[m - 1 - k]) def paired_contrast(arm_data, unit, off_arm, low_arm): """Разность log R̂(low) − log R̂(∅) одной пары, с симметричным правилом цензуры §4. Возвращает (значение, статус) где статус ∈ {ok, tie-censored, missing, small}. """ co = arm_data[off_arm]["cells"].get(unit) cl = arm_data[low_arm]["cells"].get(unit) if not co or not cl: return None, "missing" a0o, a0l = attempt0(co), attempt0(cl) if not a0o or not a0l: return None, "missing" ro, rl = rhat(a0o, off_arm), rhat(a0l, low_arm) if ro is None or rl is None or ro <= 0 or rl <= 0: return None, "missing" cens_o = a0o["finish"] == "length" cens_l = a0l["finish"] == "length" if cens_o and cens_l: return 0.0, "tie-censored" if cens_o and not (ro >= rl): return 0.0, "tie-censored" if cens_l and not (ro <= rl): return 0.0, "tie-censored" return math.log(rl) - math.log(ro), ("ok-censored" if (cens_o or cens_l) else "ok") def endpoint_test(name, values, out, note=""): """Вилкоксон с pratt + величина Ходжеса-Лемана с ДИ. Печатает вывод при ЛЮБОМ исходе.""" out.append(f"\n### {name}") if note: out.append(f" {note}") vals = [v for v in values if v is not None] if len(vals) == 0: raise Refuse(f"{name}: годных юнитов НОЛЬ — знаменатель пуст, прибор ОТКАЗЫВАЕТ") out.append(f" юнитов в тесте: {len(vals)} (ничьих-цензур: {sum(1 for v in vals if v == 0)})") if len(vals) < 6: out.append(f" ⛔ n={len(vals)} — ниже разрешения любого рангового теста; величина не печатается") return {"n": len(vals), "p": None, "hl": None, "ci": None} res = wilcoxon(vals, zero_method="pratt") # ⛔ pratt пинится: умолчание wilcox нули ВЫБРАСЫВАЕТ hl, _ = hodges_lehmann(vals) ci = hl_ci(vals) ratio = math.exp(hl) out.append(f" Вилкоксон (zero_method='pratt'): W={res.statistic:.1f} p={res.pvalue:.4g}") out.append(f" Ходжес-Леман: медиана log = {hl:+.4f} ⇒ отношение ×{ratio:.3f}") if ci: out.append(f" 95% ДИ (руками, Уолшевы средние): log [{ci[0]:+.4f}; {ci[1]:+.4f}]" f" ⇒ ×[{math.exp(ci[0]):.3f}; {math.exp(ci[1]):.3f}]") inband = BAND[0] <= math.exp(ci[0]) and math.exp(ci[1]) <= BAND[1] out.append(f" описательная полоса [{BAND[0]};{BAND[1]}]: ДИ {'внутри' if inband else 'НЕ внутри'}" f" — решения НЕ несёт (план §4)") else: out.append(" 95% ДИ: не определён при этом n (отсечка ушла за край) — заявлено, не скрыто") if res.pvalue < ALPHA: out.append(f" ⇒ ВЫВОД: эффект ЕСТЬ (p<{ALPHA}). Величина — выше.") else: out.append(f" ⇒ ВЫВОД: НИЖЕ РАЗРЕШЕНИЯ ЗАМЕРА (p={res.pvalue:.3g} ≥ {ALPHA}). Это НЕ «эффекта нет».") return {"n": len(vals), "p": float(res.pvalue), "hl": hl, "ci": ci} # ── ЭНДПОЙНТЫ ──────────────────────────────────────────────────────────────────────────────── def e0_decomposition(arm_data, out): """E0 — ПОЧЕМУ дорого: цена по статьям, сложенная по ВСЕМ попыткам.""" out.append("\n## E0 — РАЗЛОЖЕНИЕ ЦЕНЫ ПО СТАТЬЯМ (все попытки, $0)") out.append(f" {'рука':8s} {'клеток':>6s} {'вход':>9s} {'кэш':>9s} {'вид.выход':>10s} {'размышл.':>10s} {'ретраи':>9s} {'ИТОГО':>9s}") table = {} for arm in ALL_ARMS: cells = arm_data[arm]["cells"] art = {"in": 0.0, "cache": 0.0, "vis": 0.0, "reas": 0.0, "retry": 0.0, "total": 0.0} n = 0 for cell in cells.values(): if not cell.get("calls"): continue n += 1 for c in cell["calls"]: if c["tm_hit"] or c["completion_tokens"] is None: continue pr = PRICES.get(c["model"]) if not pr: continue fresh_in = max(0, (c["prompt_tokens"] or 0) - (c["cached_tokens"] or 0)) art["in"] += fresh_in / 1e6 * pr["in"] art["cache"] += (c["cached_tokens"] or 0) / 1e6 * pr["cached"] r = rhat(c, arm) vis_tok = (c["completion_tokens"] - r) if r is not None else c["completion_tokens"] vis_tok = max(0.0, vis_tok) r = max(0.0, r or 0.0) art["vis"] += vis_tok / 1e6 * pr["out"] art["reas"] += r / 1e6 * pr["out"] art["total"] += c["cost_usd"] or 0.0 if (c["attempt"] or 0) >= 1: art["retry"] += c["cost_usd"] or 0.0 table[arm] = art if arm == REFERENCE_ARM: done = [c for c in cells.values() if "delivered" in c] empty = sum(1 for c in done if c["delivered"] == 0) if done and empty / len(done) > EMPTY_REFUSE_SHARE: out.append(f" {arm:8s} — НЕ ПЕЧАТАЕТСЯ: пустых {empty}/{len(done)} > {EMPTY_REFUSE_SHARE:.0%}" f" (план §5.2; доля идёт строкой в E5)") continue out.append(f" {arm:8s} {n:6d} {art['in']:9.5f} {art['cache']:9.5f} {art['vis']:10.5f}" f" {art['reas']:10.5f} {art['retry']:9.5f} {art['total']:9.5f}") out.append("\n ДОЛИ от суммы статей (вход+кэш+видимый+размышление):") for arm, art in table.items(): s = art["in"] + art["cache"] + art["vis"] + art["reas"] if s <= 0: continue out.append(f" {arm:8s} вход {art['in']/s:5.1%} кэш {art['cache']/s:5.1%}" f" видимый выход {art['vis']/s:5.1%} РАЗМЫШЛЕНИЕ {art['reas']/s:5.1%}" f" | ретраи {art['retry']/art['total']:5.1%} от цены руки" if art["total"] else "") out.append(" ⚠ Одна статья невосстановима никакой пробой и объявлена заранее: что из подорожания") out.append(" дали НОВЫЕ ВЕСА и смена вендор-дефолта — июльское состояние сервинга не вернуть.") return table def cell_cost_nocache(call, arm): """Цена клетки, посчитанная так, будто префикс-кэша НЕ БЫЛО (весь вход по полной ставке). ⛔ ЗАЧЕМ. Порядок клеток рандомизирован, но при ЗАПИНЕННОМ сиде выпавшая перестановка на n=20 несимметрична, и это посчитано ДО чисел: `p7-low` идёт раньше `p7-off` в 15 юнитах из 20 (у пары P9 — 9 из 20), первой клеткой юнита `p7-low` бывает 10 раз против 1 у `p7-off`. Первая клетка юнита кэш не бьёт и потому дороже примерно на 6000·($1.32−$0.044)/1e6 ≈ $0.0077. ⇒ E2 у пары P7 систематически наклонён ПРОТИВ `low`. Сид — часть пре-рега и НЕ трогается; вместо этого печатается вторая, кэш-независимая версия E2, объявленная до просмотра чисел. """ pr = PRICES.get(call["model"]) if not pr or call["completion_tokens"] is None: return None return ((call["prompt_tokens"] or 0) / 1e6 * pr["in"] + (call["completion_tokens"] or 0) / 1e6 * pr["out"]) def e2_cost_per_delivered(arm_data, units, out): """E2 — цена доставленного знака, в четырёх заранее объявленных вариантах.""" out.append("\n## E2 — ЦЕНА ДОСТАВЛЕННОГО ЗНАКА") out.append(" Четыре варианта объявлены ДО чисел: два по определению «доставлено» (мягкое ×") out.append(" строгое) и два по цене (фактическая × кэш-независимая). Основной — первый.") res = {} VARIANTS = [ ("попытка 0 · фактическая цена (ОСНОВНОЙ, равный потолок)", False, "delivered", False), ("итог юнита · фактическая цена (боевая цена, ретраи включены)", True, "delivered", False), ("попытка 0 · СТРОГОЕ delivered (flagged ⇒ 0)", False, "delivered_strict", False), ("попытка 0 · КЭШ-НЕЗАВИСИМАЯ цена (снимает перекос порядка)", False, "delivered", True), ] for label, use_unit_cost, dkey, nocache in VARIANTS: vals, skipped = [], 0 for u in units: per = {} for arm in CROSS_ARMS: c = arm_data[arm]["cells"].get(u) if not c or dkey not in c or c[dkey] == 0: per = None break a0 = attempt0(c) if use_unit_cost: cost = c["unit_cost_usd"] elif nocache: cost = cell_cost_nocache(a0, arm) if a0 else None else: cost = a0["cost_usd"] if a0 else None if not cost or cost <= 0: per = None break per[arm] = math.log(cost / c[dkey]) if per is None: skipped += 1 continue vals.append(0.5 * (per["p7-low"] + per["p9-low"]) - 0.5 * (per["p7-off"] + per["p9-off"])) primary = label.startswith("попытка 0 · фактическая") if primary: # ⛔ У ОСНОВНОГО варианта отказ прибора НЕ ПЕРЕХВАТЫВАЕТСЯ: пустой знаменатель здесь — # это и есть «зелень на пустоте», ради которой гард существует. res[label] = endpoint_test(f"E2 · {label}", vals, out, note=f"пропущено юнитов (пустая доставка или нет цены): {skipped}") else: # У ЧУВСТВИТЕЛЬНОСТЕЙ пустота — законный исход (строгое delivered может обнулить всё), # и она печатается как пустота, а не роняет весь отчёт. try: res[label] = endpoint_test(f"E2 · {label}", vals, out, note=f"пропущено юнитов: {skipped}") except Refuse as e: out.append(f"\n### E2 · {label}\n ⛔ ЧУВСТВИТЕЛЬНОСТЬ НЕ СЧИТАЕТСЯ: {e}") res[label] = None return res def e5_safety(arm_data, out): """E5 — безопасность. Блокирует любую рекомендацию, считается по механизму, не по имени. ⛔ ГЛАВНАЯ ПРАВКА ПОСЛЕ ВЕРИФИКАЦИИ: эхо считается по СЫРОМУ ответу попытки 0, а НЕ по отгружаемому тексту. Санитайзер CJK ВЫРЕЗАЕТ, и эндпойнт, блокирующий любую рекомендацию, был бы слеп ровно к своему предмету. Улика с холодного прогона, снята командой: юнит (1,0), flag=sanitizer_stripped: CJK в отгруженном = 0, CJK в СЫРОМ ответе = 10. Колонка по отгруженному остаётся ВТОРОЙ — она отвечает на другой вопрос («что увидел читатель»). """ out.append("\n## E5 — БЕЗОПАСНОСТЬ (эхо / CJK / обрыв по длине)") out.append(" ⚠ «CJK@сырой» — по СЫРОМУ ответу попытки 0 (санитайзер эхо вырезает, и по") out.append(" отгруженному тексту эхо не видно вовсе). «CJK@отгр» — что осталось читателю.") out.append(f" {'рука':8s} {'клеток':>6s} {'CJK@сырой':>9s} {'CJK@отгр':>8s} {'кир<0.99':>8s}" f" {'flagged':>7s} {'length@0':>8s} {'санитайз':>8s} {'малый R̂':>8s}") summary = {} for arm in ALL_ARMS: cells = [c for c in arm_data[arm]["cells"].values() if "delivered" in c] if not cells: continue cjk_ship = sum(1 for c in cells if c["cjk_in_shipped"] > 0) lowcyr = sum(1 for c in cells if c["cyr_share"] < 0.99) flagged = sum(1 for c in cells if c["disposition"] == "flagged") sanit = sum(1 for c in cells if "sanitiz" in (c["flag_reason"] or "")) length0 = cjk_raw = small = 0 for c in cells: a0 = attempt0(c) if not a0: continue if a0["finish"] == "length": length0 += 1 _, _, ncjk = delivery_predicate(a0["resp"] or "") if ncjk > 0: cjk_raw += 1 r = rhat(a0, arm) if arm != REFERENCE_ARM and r is not None and 0 < r < SMALL_RHAT: small += 1 summary[arm] = {"n": len(cells), "cjk_raw": cjk_raw, "cjk_shipped": cjk_ship, "lowcyr": lowcyr, "flagged": flagged, "length0": length0, "sanitizer": sanit, "small_rhat": small} out.append(f" {arm:8s} {len(cells):6d} {cjk_raw:9d} {cjk_ship:8d} {lowcyr:8d}" f" {flagged:7d} {length0:8d} {sanit:8d} {small:8d}") out.append(f" ⚠ «малый R̂» — клетки с R̂ < {SMALL_RHAT} токенов: у них относительная ошибка формулы") out.append(" восстановления 20-70% (план §3), и о ВЕЛИЧИНЕ таких клеток вывод не делается.") echo_off = sum(summary.get(a, {}).get("cjk_raw", 0) for a in OFF_ARMS) echo_low = sum(summary.get(a, {}).get("cjk_raw", 0) for a in LOW_ARMS) out.append(f"\n эхо-клеток: у ∅ {echo_off}, у low {echo_low}") if echo_low - echo_off >= 3: out.append(f" ⛔ РИСК ЭХА НА low: у low на {echo_low-echo_off} эхо-клеток больше — блокирует рекомендацию НЕЗАВИСИМО ОТ ЦЕНЫ") else: out.append(" риск эха на low по правилу §4 (≥3 сверх ∅) НЕ предъявлен") out.append(" ⚠ Референс glm-5 идёт СТРОКОЙ ОТДЕЛЬНО: у него thinking ВЫКЛЮЧЕН, а эхо-мина") out.append(" вооружается именно выключением — его доля эха к оси усилия не относится.") return summary def covariate_check(arm_data, units, contrasts, out, dl): """Ковариата длины входа — с ЗАЯВЛЕННОЙ находкой о том, почему модель плана вырождена.""" out.append("\n## КОВАРИАТА ДЛИНЫ ВХОДА") out.append(" ⛔ НАХОДКА (предъявляется, а не правится молча): модель плана") out.append(" `log R̂ ~ effort + prompt + log(len_draft) + (1|unit)` НЕ МОЖЕТ быть проверкой") out.append(" устойчивости E1/E3. Длина черновика — величина УНИТ-УРОВНЕВАЯ: все четыре руки") out.append(" редактируют ОДИН И ТОТ ЖЕ черновик, это и есть блокировка трудности. В") out.append(" сбалансированном внутри-юнитном дизайне юнит-уровневая ковариата НЕ МЕНЯЕТ") out.append(" оценок усилия и промпта ПО ПОСТРОЕНИЮ (её вклад целиком лежит в между-юнитной") out.append(" части, которую съедает случайный перехват). Это свойство дизайна, а не нехватка") out.append(" библиотеки; сам наклон длины при этом оценим — он ниже, пункт (2).") out.append(" ⇒ Вместо «проверки устойчивости» считается то, что оценимо и отвечает на ту же") out.append(" тревогу («не длиной ли входа объясняется эффект»): зависимость ЮНИТНОГО") out.append(" КОНТРАСТА от длины черновика.") xs, ys = [], [] for u, d in zip(units, contrasts): n = dl.get(u) if d is None or not n: continue xs.append(math.log(n)) ys.append(d) if len(xs) < 6: out.append(f" n={len(xs)} — недостаточно, проверка ОБЪЯВЛЕНА НЕИСПОЛНЕННОЙ") return None x, y = np.array(xs), np.array(ys) rho, prho = spearmanr(x, y) # OLS руками: наклон, стандартная ошибка, t-тест (statsmodels не нужен и не установлен) xm, ym = x.mean(), y.mean() sxx = ((x - xm) ** 2).sum() b = ((x - xm) * (y - ym)).sum() / sxx a = ym - b * xm resid = y - (a + b * x) dof = len(x) - 2 s2 = (resid ** 2).sum() / dof seb = math.sqrt(s2 / sxx) tstat = b / seb if seb > 0 else float("nan") p = 2 * (1 - student_t.cdf(abs(tstat), dof)) out.append(f" (1) ВНУТРИ-ЮНИТНАЯ УСТОЙЧИВОСТЬ: зависит ли САМ контраст от длины входа") out.append(f" n={len(x)} Спирмен ρ={rho:+.3f} (p={prho:.3g})") out.append(f" МНК контраст ~ log(len_draft): наклон {b:+.4f} ± {seb:.4f}, t={tstat:+.2f}, p={p:.3g}, df={dof}") out.append(f" ⇒ {'контраст зависит от длины входа' if p < ALPHA else 'зависимости контраста от длины входа не предъявлено'}") # (2) Между-юнитная часть — ЭТО И ЕСТЬ коэффициент длины из смешанной модели плана. # В сбалансированном внутри-юнитном дизайне он равен МНК-наклону по СРЕДНИМ ПО ЮНИТУ # значениям log R̂; считается точно и без statsmodels. ux, uy = [], [] for u in units: n = dl.get(u) if not n: continue rs = [] for arm in CROSS_ARMS: c = arm_data[arm]["cells"].get(u) a0 = attempt0(c) if c else None r = rhat(a0, arm) if a0 else None if r and r > 0: rs.append(math.log(r)) if len(rs) == len(CROSS_ARMS): ux.append(math.log(n)) uy.append(float(np.mean(rs))) out.append(f" (2) МЕЖДУ-ЮНИТНАЯ ЧАСТЬ — это и есть коэффициент длины из смешанной модели плана") lenfit = None if len(ux) >= 6: X, Y = np.array(ux), np.array(uy) Xm, Ym = X.mean(), Y.mean() sxx2 = ((X - Xm) ** 2).sum() b2 = ((X - Xm) * (Y - Ym)).sum() / sxx2 a2 = Ym - b2 * Xm r2d = len(X) - 2 s22 = ((Y - (a2 + b2 * X)) ** 2).sum() / r2d seb2 = math.sqrt(s22 / sxx2) t2 = b2 / seb2 if seb2 > 0 else float("nan") p2 = 2 * (1 - student_t.cdf(abs(t2), r2d)) out.append(f" n={len(X)} log R̂(среднее по юниту) ~ log(len_draft): наклон {b2:+.3f} ± {seb2:.3f}," f" t={t2:+.2f}, p={p2:.3g}") out.append(f" ⇒ при удвоении длины черновика размышление ×{2**b2:.2f}" f" (для сравнения: exp19 намерил ×2.78 при ×3.24 входа ⇒ наклон ≈{math.log(2.78)/math.log(3.24):.2f})") lenfit = {"slope": float(b2), "p": float(p2), "n": len(X)} else: out.append(f" n={len(ux)} — недостаточно, ОБЪЯВЛЕНО НЕИСПОЛНЕННЫМ") out.append(" ⚠ statsmodels в eval/.venv НЕ УСТАНОВЛЕН и НЕ УСТАНАВЛИВАЛСЯ. Причина — НЕ «нет сети»") out.append(" (сеть есть, колесо под cp314 существует — проверено): в сбалансированном внутри-юнитном") out.append(" дизайне ОБЕ величины, которые дала бы смешанная модель, вычисляются точно руками —") out.append(" (1) и (2) выше, — а правка общего venv при живых параллельных сессиях есть риск без") out.append(" выигрыша. Решение объявлено словом, а не умолчано.") return {"rho": float(rho), "slope": float(b), "p": float(p), "n": len(x), "length_effect": lenfit} def money(arm_data, base_spend, out, sanction, outside=0.0): """§9.5 — агрегат вручную: механизма нет, база считается ОДИН раз. ⚠ `outside` — деньги, ушедшие провайдеру из БД, которая потом была пере-создана. Из леджера копии они исчезают, из счёта — нет, и агрегат «база + Σ(копия − база)» их не видит. Слагаемое заведено исполнением: $0.018243 ушли в p7-low на терминолога до пере-копирования руки. """ out.append("\n## ДЕНЬГИ — АГРЕГАТ ВРУЧНУЮ (§9.5)") out.append(f" база : ${base_spend:.6f}") if outside: out.append(f" потрачено вне баз : ${outside:.6f} ⚠ из леджеров не видно, но провайдеру ушло") total = base_spend + outside for arm in ALL_ARMS: s = arm_data[arm]["spend"] delta = s - base_spend total += delta out.append(f" {arm:8s} леджер ${s:.6f} − база ⇒ ${delta:.6f}") out.append(f" ─────────────────────────────────────────") out.append(f" ИЗРАСХОДОВАНО (база + Σ(копия−база)) = ${total:.6f}") out.append(f" САНКЦИЯ ВЛАДЕЛЬЦА (пиковые термины) = ${sanction:.2f}") out.append(f" остаток = ${sanction-total:.6f}") if total > sanction: out.append(" ⛔ ПОТОЛОК ПРОБИТ — стоп и вопрос владельцу, добор запрещён") return total # ── MAIN ───────────────────────────────────────────────────────────────────────────────────── def main(): ap = argparse.ArgumentParser(description="прибор пробы редактора по четырём осям") ap.add_argument("--stand", default=str(Path.home() / "books/gu-zhenren/probe-4axes")) ap.add_argument("--manifest", help="манифест пре-рега (sha256 промптов и запиненная сигнатура)") ap.add_argument("--sanction", type=float, default=9.50) ap.add_argument("--outside", type=float, default=0.018243, help="потрачено вне текущих баз (пере-созданные БД); в агрегат входит") ap.add_argument("--json", help="куда сложить машинный результат") a = ap.parse_args() B = Path(a.stand) out, result = [], {} manifest = {} if a.manifest and Path(a.manifest).exists(): manifest = json.loads(Path(a.manifest).read_text()) pins = manifest.get("prompt_sha256", {}) pinned_sig = manifest.get("signature", {}) out.append("═" * 96) out.append("ПРОБА РЕДАКТОРА ПО ЧЕТЫРЁМ ОСЯМ — ПРИБОР (chetyre.py)") out.append(f"стенд: {B}") out.append("═" * 96) try: if not pins: raise Refuse("в манифесте нет запиненных prompt_sha256 — гард оси не может быть исполнен " "(положительный контроль: --manifest указывает на файл пре-рега)") arm_data = {} for arm in ALL_ARMS: path = B / "arms" / f"{arm}.db" cells, dlen, snaps, jobs, spend = read_arm(path, arm) arm_data[arm] = {"cells": cells, "draft_len": dlen, "snaps": snaps, "jobs": jobs, "spend": spend} # ⚠ ДЛИНА ВХОДА ЮНИТА — только через манифест движка: юнит редактуры охватывает несколько # чанков черновика (в этом стенде 15 юнитов из 20), и ключ (глава, индекс) у них СОВПАДАЕТ, # то есть ошибка была бы ТИХОЙ. mpath = B / "guzhenren-probe4.db.manifest.json" if not mpath.exists(): raise Refuse(f"нет манифеста движка {mpath} — длину входа юнита взять неоткуда " f"(положительный контроль: он пишется командой `tmctl manifest`, $0)") unit_len = unit_draft_lengths(mpath, arm_data[CROSS_ARMS[0]]["draft_len"]) base_con = open_db(B / "guzhenren-probe4.db") base_spend = base_con.execute("select coalesce(sum(committed_usd+reserved_usd),0) from spend").fetchone()[0] base_con.close() out.append("\n## ИНВЕНТАРЬ") for arm in ALL_ARMS: cells = arm_data[arm]["cells"] done = [c for c in cells.values() if "delivered" in c] orph = sum(c.get("orphan_calls", 0) for c in cells.values()) out.append(f" {arm:8s} клеток {len(cells):3d} завершённых {len(done):3d}" f" вызовов без чекпойнта {orph} леджер ${arm_data[arm]['spend']:.6f}") problems = gate_axis(arm_data, pins, out) sig, sp = gate_signature(arm_data, pinned_sig, out) problems += sp problems += gate_empty(arm_data, out) if problems: out.append("\n⛔ ГАРДЫ НЕ ПРОШЛИ:") for p in problems: out.append(f" · {p}") raise Refuse("гарды дизайна не прошли — эндпойнты не считаются, чтобы не выдать зелень на браке") units = sorted(set.intersection(*[set(arm_data[x]["cells"]) for x in CROSS_ARMS])) out.append(f"\n## ЮНИТЫ, ПОЛНЫЕ ПО ВСЕМ ЧЕТЫРЁМ РУКАМ КРЕСТА: {len(units)}") if not units: raise Refuse("полных юнитов НОЛЬ — знаменатель пуст") e0 = e0_decomposition(arm_data, out) # E1 — главный d1, statuses, dropped = [], [], [] for u in units: p7, s7 = paired_contrast(arm_data, u, "p7-off", "p7-low") p9, s9 = paired_contrast(arm_data, u, "p9-off", "p9-low") statuses += [s7, s9] if p7 is None or p9 is None: dropped.append((u, f"P7:{s7} P9:{s9}")) d1.append(None) else: d1.append(0.5 * (p7 + p9)) cens = sum(1 for s in statuses if "censored" in s) # ⛔ ВЫБРОШЕННЫЕ ЮНИТЫ НАЗЫВАЮТСЯ ПОИМЁННО И С ПРИЧИНОЙ: «юнитов в тесте: N» без этого # читается как «столько и было», а разница между 20 и N есть отдельный факт о приборе. note = f"пар с цензурой: {cens} из {len(statuses)}" if cens: note += " ⇒ ВЕЛИЧИНА ЕСТЬ НИЖНЯЯ ГРАНИЦА, читать со знаком «≥»" if dropped: note += f"\n ⛔ ВЫБРОШЕНО ЮНИТОВ: {len(dropped)} из {len(units)} — " + \ "; ".join(f"({a},{b}) {r}" for (a, b), r in dropped[:8]) if len(dropped) > 8: note += f" … и ещё {len(dropped)-8}" else: note += f"\n выброшенных юнитов нет: в тесте все {len(units)}" e1 = endpoint_test("E1 — ГЛАВНЫЙ: усилие low против ∅, log R̂, попытка 0", d1, out, note=note) # E3 — ось промпта d3 = [] for u in units: v = {} for arm in CROSS_ARMS: c = arm_data[arm]["cells"].get(u) a0 = attempt0(c) if c else None r = rhat(a0, arm) if a0 else None v[arm] = math.log(r) if (r and r > 0) else None d3.append(None if any(x is None for x in v.values()) else 0.5 * (v["p9-off"] + v["p9-low"]) - 0.5 * (v["p7-off"] + v["p7-low"])) e3 = endpoint_test("E3 — ОСЬ ПРОМПТА (сегодняшний против июльского), log R̂", d3, out, note="прогноз, калибрующий удивление (записан до чисел): |эффект| < ×1.25") # E4 — взаимодействие d4 = [] for u in units: v = {} for arm in CROSS_ARMS: c = arm_data[arm]["cells"].get(u) a0 = attempt0(c) if c else None r = rhat(a0, arm) if a0 else None v[arm] = math.log(r) if (r and r > 0) else None d4.append(None if any(x is None for x in v.values()) else (v["p9-low"] - v["p9-off"]) - (v["p7-low"] - v["p7-off"])) e4 = endpoint_test("E4 — ВЗАИМОДЕЙСТВИЕ промпт×усилие", d4, out, note="ОПИСАТЕЛЬНЫЙ: sd контраста вдвое больше, MDE ≈×3.3 при n=32 — выводов не несёт") e2 = e2_cost_per_delivered(arm_data, units, out) e5 = e5_safety(arm_data, out) cov = covariate_check(arm_data, units, d1, out, unit_len) total = money(arm_data, base_spend, out, a.sanction, a.outside) result = {"e0": {k: v for k, v in e0.items()}, "e1": e1, "e2": {k: v for k, v in e2.items()}, "e3": e3, "e4": e4, "e5": e5, "covariate": cov, "units": len(units), "spent_usd": total, "signature": {k: {str(i): j for i, j in v.items()} for k, v in sig.items()}} code = 0 except Refuse as e: out.append(f"\n⛔⛔ ПРИБОР ОТКАЗЫВАЕТ (код {REFUSAL}): {e}") out.append(" Это НЕ «в пределах» и НЕ «эффекта нет». Знаменатель пуст либо гард не прошёл.") code = REFUSAL text = "\n".join(out) print(text) if a.json: Path(a.json).write_text(json.dumps({"ok": code == 0, "report": text, **result}, ensure_ascii=False, indent=2, default=str)) return code if __name__ == "__main__": sys.exit(main())