Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWeV2yiMWTjkeaQfSsYuL
915 lines
58 KiB
Python
915 lines
58 KiB
Python
#!/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:<sha>` — он ТОЖЕ лежит в 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())
|