textmachine/eval/dovodka/zakhod.py

684 lines
51 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""ЗАХОД Д17 — маленький честный повтор под ПОЧИНЕННУЮ рубрику. Платный.
⚠ ЗАЧЕМ ЭТОТ ЗАХОД ВООБЩЕ. Дуга 19→23 отвечала на вопрос «какая модель лучше в роли редактора» и
ответила устойчиво. Продуктовый вопрос владельца другой — живая русская проза без translationese, —
и под него прибора не было: слепое чтение 16.08 дало на английской оси Спирмена 0.10, то есть
счёт ошибок и читаемость там НЕ СВЯЗАНЫ. Владелец 16.08 постановил: «штрафовать за вольность
нельзя, штрафовать можно только за перевирания смысла исходника». Рубрика переписана, значит
старые судейские числа с новыми несравнимы — и панель обязана быть пере-куплена и пере-сужена
целиком, вместе со СВОИМ шумовым полом.
ЧТО ПОКУПАЕТСЯ (имена армов ОДНИ на обе пары, пара живёт в данных — норматив общности §0):
ZD голый черновик БЕСПЛАТНО, уже есть (D0 / якорный черновик)
Z0 боевая связка черновик → перепис БЕСПЛАТНО, уже есть (A0 / E0) — ЭТАЛОН
ZP однопроходка уравненного мандата БЕСПЛАТНО, уже есть (A2 / E2)
Z8 ОБОГАЩЁННЫЙ ЧЕРНОВИК ПОКУПАЕТСЯ (flash) — вопрос владельца 16.08
Z8R Z8 + боевой перепис ПОКУПАЕТСЯ (pro)
Z1 критик → ПОЛНЫЙ ПЕРЕПИС по нему ПОКУПАЕТСЯ (pro) — ставка владельца в сильнейшей форме
Z7 однопроходка + канон-фиксер ПОСЛЕ ПОКУПАЕТСЯ (pro) — поправка владельца 16.08 про банк
ZF ВТОРАЯ генерация боевой связки ПОКУПАЕТСЯ (pro) — шумовой пол НОВОЙ рубрики
⚠ ПОЧЕМУ ПОЛ ПОКУПАЕТСЯ ЗАНОВО, А НЕ БЕРЁТСЯ СТАРЫЙ. Существующий пол осей снят с ТОЧЕЧНОГО
фиксера — операции почти детерминированной, дающей побайтных близнецов (в эксп-23 контроль
`CTRLfloorA` совпал с боевым армом в 16/16 и оказался пуст). Панель этого захода — панель
ПЕРЕПИСЫВАТЕЛЕЙ, и честный её пол есть ДВЕ ГЕНЕРАЦИИ ОДНОГО И ТОГО ЖЕ переписывателя. Ровно так
сделана японская нога (`JFLO`), и там это дало самый чистый вывод фазы: разница двух редакторов
(0.44) не больше разницы двух прогонов одного (0.42).
⚠ ЧЕМ Z1 ОТЛИЧАЕТСЯ ОТ ПРОИГРАВШЕГО A3. У `A3` правщик был ТОЧЕЧНЫМ: ему запрещалось трогать всё,
кроме перечисленных мест, и он двигал 0.23.7% знаков. Здесь мандат ПОЛНЫЙ — это боевой редактор,
которому замечания критика лишь ДОБАВЛЕНЫ к его обычной работе. Разложение по осям (Д14.11)
показало, что точечные контуры проигрывают ПРОЗОЙ, а не смыслом; Z1 снимает именно это ограничение.
⚠ ЗАМЕЧАНИЯ КРИТИКА ФИЛЬТРУЮТСЯ, И ЭТО ПОЧИНКА, А НЕ ОПТИМИЗАЦИЯ. `contour.critic_places` берёт
ЛЮБУЮ строку, начинающуюся с дефиса, без разбора вердикта: на китайской оси 311 строк из 1696
(18.3%) были ПОДТВЕРЖДЕНИЯМИ критика («верно», «допустимо», «не ошибка»), и фиксер получал их как
задания на правку. Ставка владельца была испытана инструментом, который на пятой части работы
правил заведомо исправное. Здесь фильтр явный, его отсев печатается числом, а старые клетки `A3`
не трогаются — они куплены и остаются как есть, с объявленным дефектом.
⚠ ГЕЙТ ПЕРЕЛОМА ЦЕНЫ DeepSeek. Вендор переводит прайс на пик/офф-пик В 16:00 UTC 16.08.2026
(сноска прайс-страницы, факт-чек 15.08 в `backend/configs/models.yaml`). До этого мгновения
июльский пин `eval/tenant_panel/roster.py` ВЕРЕН и записанная цена клетки правдива; после —
занижает втрое, и касса начнёт печатать неправду. Поэтому покупка DeepSeek после перелома
ОТКАЗЫВАЕТСЯ, пока ростер не пере-пинен. Это не осторожность, а единственное, что отличает
леджер-измеритель от леджера-фантазёра.
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы…] | --echo
"""
from __future__ import annotations
import datetime as dt
import hashlib
import importlib.util
import json
import re
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
for d in ("dovodka", "editor_tier", "tenant_panel", "role_topology", "bank_arbitration"):
sys.path.insert(0, str(REPO / "eval" / d))
def _load(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
# ⚠ КОНТУР БЕРЁТСЯ У ДРАЙВЕРА АНГЛИЙСКОЙ ОСИ, А НЕ ГРУЗИТСЯ ВТОРЫМ ЭКЗЕМПЛЯРОМ, И ЭТО НЕ
# ПЕДАНТИЗМ. `en_axis` держит СВОЙ экземпляр `contour.py` (`_load("contour_en", …)`), и его
# `wire()` настраивает хуки баз ИМЕННО В НЁМ. Свой второй экземпляр остался бы с китайскими
# хуками: английские черновики читались бы китайским путём, возвращали пусто и объявлялись
# «БРАКОВАН гейтом проекта» — то есть английская ось молча стала бы пустой. Поймано `--dry`
# до единой покупки; класс дефекта тот же, что стоил фазе семи оплаченных клеток 10.08.
EN = _load("en_axis_z", ZONE / "en_axis.py")
C = EN.C
MONEY, PR, ROSTER, ED22, PARA = C.MONEY, C.PR, C.ROSTER, C.ED22, C.PARA
OUT = MONEY.OUT
SALT = "zakhod-d17-2026-08-16" # соль отбора единиц; впечатывается в пре-рег ДО покупок
N_UNITS = 16 # мощность объявлена пре-регом Д17, здесь она же
DRAFT_MODEL = "deepseek-v4-flash"
EDITOR = "deepseek-v4-pro"
# Перелом прайса DeepSeek: вендор-сноска, факт-чек 15.08 (`models.yaml` шапка).
PRICE_CUTOVER = dt.datetime(2026, 8, 16, 16, 0, tzinfo=dt.timezone.utc)
JULY_PIN_PRO = 0.435 # если в ростере всё ещё это — пин июльский
# ── ПАРА КАК ДАННЫЕ ───────────────────────────────────────────────────────────────────────────
# Ни одна ветка ниже не спрашивает «а это китайский?». Всё, что у пары своё, — здесь.
PAIRS = {
# ⚠ БЮДЖЕТ ВЫВОДА 32000 НА ОБЕИХ ПАРАХ (починка по ревью). Хендофф §5.4 предупреждал прямо:
# «бюджет вывода новых пар 32000, 16000 мало». Я оставил китайской 16000 по умолчанию контура —
# и получил ДВА обрыва `finish=length` на первых шести клетках `Z8R` ($0.0324 списано, текста
# нет). У DeepSeek размышление тарифицируется ВНУТРИ бюджета, а обогащённый черновик длиннее.
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 32000,
"onepass": "A2", "crit": "A3", "wire": lambda: C.configure(PARA.ZH),
"units": lambda: C.units_ok()},
"en": {"para": lambda: PARA.EN, "letter": "j", "phase": "ФД-J", "max_out": 32000,
"onepass": "E2", "crit": "E3", "wire": EN.wire, "units": EN.units},
}
ARMS = ("Z8", "Z8R", "Z1", "Z7", "ZF") # покупаемые; ZD/Z0/ZP бесплатны
FREE = ("ZD", "Z0", "ZP")
# ⚠ ПРАВИЛО ДВУСТОРОННЕЕ, И ЭТО ВЫВОД ЗАМЕРА, А НЕ ВКУС. Первая редакция требовала от строки
# разделитель «→» и роняла всё прочее: на выборке это выбросило 3 строки, из которых ДВЕ были
# настоящими находками («рассогласование реплик», «фраза суха — лучше так»). Ошибка в эту сторону
# дороже противоположной: у Z1 правщик — ПОЛНЫЙ редактор, и лишнее подтверждение в списке он
# просто перепишет как обычно, а потерянная находка не вернётся ничем.
# Поэтому строка выбрасывается, ТОЛЬКО если (а) принимающая формула стоит в ХВОСТЕ вердикта —
# то есть критик закончил согласием, — И (б) во всей строке нет ни одного маркера дефекта.
# ⚠⚠ ПОЧИНКА ПО АДВЕРСАРИАЛЬНОМУ РЕВЬЮ (другое модельное семейство, 16.08). Прежняя редакция
# матчила принимающие формулы ВНУТРИ отрицательных слов: «неправильно», «некорректно»,
# «недопустимо», «неуместно», «неадекватно», «неточно передан» опознавались как СОГЛАСИЕ, потому
# что `правильно`/`корректн`/`допустим`/`уместн`/`адекватн` — их подстроки, а в списке маркеров
# дефекта этих форм не было. Замер по живому корпусу (1669 строк критика обеих пар): **10 реальных
# находок ложно отсеяны**, среди них «недопустимое смешение языков» и «неправильное употребление
# предлога». Ставка владельца покупалась бы по спискам, из которых выпали дефекты.
# Лечение: отрицательная приставка ловится ЯВНО и стоит в маркерах дефекта, а принимающие формулы
# требуют границы слова слева, чтобы не цепляться к «не-».
CONFIRM_RX = re.compile(
r"(?<![а-яё])(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|"
r"ошибки\s+нет|претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|"
r"передано\s+верно|соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)",
re.IGNORECASE)
DEFECT_RX = re.compile(
r"(искаж|потер|неверн|ошибк|кальк|канцеляр|пропущ|инверт|рассоглас|подмен|выдум|отрицан|"
r"не\s+соответств|лучше\b|следует\b|надо\b|стоит\b|заменить|исправ|сух|тяжел|громозд|"
r"буквализм|подстрочник|не\s+переда|утрач|"
# отрицательные формы принимающих слов — они и есть находки, а не согласия
r"неправильн|некорректн|недопустим|неуместн|неадекватн|неточн|неверо|не\s+вполне|"
r"не\s+совсем|сомнительн|спорн)", re.IGNORECASE)
CONFIRM_TAIL = 70 # знаков хвоста вердикта, где принимающая формула что-то значит
def _is_confirmation(line: str) -> bool:
"""Строка — согласие критика, а не место. `ё` нормализуется: критик пишет и «приёмлемо»."""
s = line.replace("ё", "е").replace("Ё", "Е")
verdict = s.split("", 1)[1] if "" in s else s
return bool(CONFIRM_RX.search(verdict[-CONFIRM_TAIL:])) and not DEFECT_RX.search(s)
def _pair(p: str) -> dict:
if p not in PAIRS:
raise SystemExit(f"⛔ неизвестная пара {p!r}; знаю {', '.join(PAIRS)}")
return PAIRS[p]
def tag(p: str, arm: str, uid: str) -> str:
return f"dv-{_pair(p)['letter']}-{arm.lower()}-{uid}"
def cell(tg: str) -> str:
f = OUT / f"{tg}.json"
if not f.exists():
return ""
d = json.loads(f.read_text(encoding="utf-8"))
return (d.get("content") or "") if d.get("finish") == "stop" else ""
# ── бесплатные армы: тексты, уже оплаченные прошлыми осями ────────────────────────────────────
def free_text(p: str, arm: str, uid: str) -> str:
"""ZD/Z0/ZP берутся у рига оси — он знает, где лежат клетки прошлых паков."""
if arm == "ZD":
return C.base_draft(uid)
if arm == "Z0":
return C.base_a0(uid)
if arm == "ZP":
return C.text_of(_pair(p)["onepass"], uid)
raise SystemExit(f"{arm} не бесплатный арм")
# ── ЭХО-ГЕЙТ ОБОГАЩЁННОГО ЧЕРНОВИКА ───────────────────────────────────────────────────────────
# ⚠⚠ ЭРРАТА Э-17.1, ОБЪЯВЛЕНА ПОСЛЕ 10 КУПЛЕННЫХ КЛЕТОК И ДО ОСТАЛЬНЫХ ПОКУПОК. Первые китайские
# клетки `Z8` показали **3 эха из 10**: модель вернула ИСХОДНИК с косметически переставленными
# репликами вместо перевода (валидный HTTP 200, `finish=stop`, доля хань ~0.99).
#
# Это не случайность и не новость — это ДОКУМЕНТИРОВАННЫЙ класс, и я обязан был проверить его
# ДО покупки, а не после:
# · черновая роль ходит на `deepseek-v4-flash` с `reasoning_effort:"low"`, а квирк-бюллетень
# (`00-provider-quirks.md`, п.4) прямо говорит: «`effort:"low"` ПЕРЕ-ВООРУЖАЕТ эхо-мину»,
# защита деградирует НЕПРЕРЫВНО с эффортом, а не скачком на «выключено»;
# · запрет D21.6 там же: **не вносить языковые констрейнты в промты reasoning-ослабленных путей
# без per-model замера** — на grok одна формулировка констрейнта подняла эхо 3/10 → 9/10.
# Мандат перевёрстки — ровно такой констрейнт: он ОПИСЫВАЕТ синтаксис исходного языка.
#
# ⇒ Это не помеха замеру, а ЕГО ПЕРВЫЙ РЕЗУЛЬТАТ: обогащение промта черновика имеет цену, и цена
# эта — частота тихого отказа на дешёвой модели. Число печатается как результат арма, а не
# прячется. Обработка — штатная для проекта: гейт годности + ОДИН ре-ген (квирк-бюллетень п.6:
# эхо стохастично по вызову, ре-ген на flash в 7.6× дешевле эскалации). Клетки с эхом уходят в
# карантин под именем, которое глоб кассы по-прежнему видит: деньги за них списаны и обязаны
# остаться видимыми.
REGEN_MAX = 1
def quarantine(tg: str, why: str) -> None:
f = OUT / f"{tg}.json"
if not f.exists():
return
n = 1
while (OUT / f"{tg}.ЭХО{n}.json").exists():
n += 1
f.rename(OUT / f"{tg}.ЭХО{n}.json")
print(f" {tg}: {why} → карантин .ЭХО{n}.json (деньги остаются видимы кассе)")
def draft_ok(p: str, uid: str) -> str:
"""Обогащённый черновик, ПРОШЕДШИЙ штатный гейт годности проекта. Пусто = негоден.
⚠ Через эту функцию, а не через `cell`, обязан ходить КАЖДЫЙ потребитель `Z8`: эхо-клетка
имеет `finish=stop` и непустое содержимое, поэтому обычная проверка её пропускает — и боевой
редактор `Z8R` принялся бы «редактировать» китайский текст за полную цену.
"""
t = cell(tag(p, "Z8", uid))
return "" if (not t.strip() or C.draft_defect(t)) else t
def echo_report() -> None:
for p in PAIRS:
_pair(p)["wire"]()
us = chosen(p)
bad, good, quar = [], 0, 0
for u in us:
t = cell(tag(p, "Z8", u["uid"]))
quar += len(list(OUT.glob(f"{tag(p, 'Z8', u['uid'])}.ЭХО*.json")))
if not t.strip():
continue
why = C.draft_defect(t)
bad.append((u["uid"], why)) if why else None
good += not why
n = good + len(bad)
print(f"{p}: годных {good} · с дефектом {len(bad)} из {n} "
f"({len(bad) / max(n, 1):.0%}) · в карантине {quar}")
for uid, why in bad[:5]:
print(f" {uid}: {why}")
def crit_raw(p: str, uid: str) -> str:
"""Ответ смыслового критика этой единицы — КУПЛЕН прошлой осью, здесь $0."""
return cell(C.tag(_pair(p)["crit"], uid, "crit"))
def critic_notes(p: str, uid: str) -> tuple[list[str], int]:
"""(замечания критика, сколько строк отсеяно как согласия критика)."""
txt = crit_raw(p, uid)
if not txt.strip():
return [], 0
lines = [ln.strip("-•* \t") for ln in txt.splitlines() if ln.strip().startswith(("-", "", "*"))]
keep = [ln for ln in lines if not _is_confirmation(ln)]
return keep[:C.PLACES_CAP], len(lines) - len(keep)
# ── ОБОГАЩЁННЫЙ ЧЕРНОВИК (Z8): промт переводчика + мандат перевёрстки СВОЕЙ ПАРЫ ──────────────
# ⚠ Вопрос владельца 16.08 дословно: «может, промт черновика от редактора сильно отличается и
# черновик тут выступает в роли недоведённого до ума инструмента?» Текстом подтверждено: у
# переводчика вся вёрстка — одна строка «НЕ копируй построчную разбивку», у редактора — секция
# ДИСКУРС-ПЕРЕВЁРСТКА из четырёх шагов, обязательная операция передачи чужого синтаксиса русской
# прозой. Черновик её не получал НИ РАЗУ за пять экспериментов.
# ⚠ ПОПРАВКА К ПЛАНУ, найденная чтением кода: FEWSHOT в промт НЕ ПОПАДАЕТ НИ У КОГО —
# `prompts.load_template` его дропает, потому что в проде `stages[edit].few_shot: false`. Значит
# асимметрия «у редактора есть примеры» НЕВЕРНА, и Z8 переносит только то, что реально едет на
# провод: секцию перевёрстки. Иначе арм мерил бы то, чего в бою нет.
DISCOURSE_HEAD = "ДИСКУРС-ПЕРЕВЁРСТКА"
def discourse_block(p: str) -> str:
"""Секция перевёрстки из редакторского промта ЭТОЙ ЖЕ пары. Данные, не код."""
core, _few, _user = PR.load_template(PR.PAIR_DIR[_pair(p)["para"]().key] / "editor.md")
i = core.find(DISCOURSE_HEAD)
if i < 0:
raise SystemExit(f"⛔ в редакторском промте пары {p} нет секции {DISCOURSE_HEAD}"
"обогащать черновик нечем, арм Z8 не собирается")
return core[i:].strip()
def z8_msgs(p: str, src: str) -> list[dict]:
"""Черновик с мандатом перевёрстки. Отличие от боевого черновика РОВНО ОДНО — эта секция."""
key = _pair(p)["para"]().key
m = PR.translator_msgs(src, None, pair=key)
blk = PR.render(discourse_block(p), getattr(PR, PR.PAIR_BRIEF[key]), "", "")
m[0]["content"] = m[0]["content"].rstrip() + "\n\n" + blk
return m
# ── ПОЛНЫЙ ПЕРЕПИС ПО ЗАМЕЧАНИЯМ КРИТИКА (Z1) ─────────────────────────────────────────────────
Z1_HEAD = (
"\n\nЗАМЕЧАНИЯ СМЫСЛОВОГО КРИТИКА к этому черновику (он читал исходник и перевод рядом). "
"Устрани КАЖДОЕ из них. Это ДОБАВЛЕНИЕ к твоей обычной работе, а не замена: мандат "
"художественной редактуры остаётся полным, и текст за пределами этих мест ты правишь так же, "
"как правил бы без списка.\n")
def z1_msgs(p: str, src: str, draft: str, notes: list[str]) -> list[dict]:
"""⚠ ПОЧИНКА ПО РЕВЬЮ: замечания дописываются в ПОСЛЕДНЕЕ сообщение с ролью `user`, а не в
`m[1]`. При непустом банке `editor_msgs` вставляет ВТОРЫМ сообщением закон-блок (роль
`system`), и замечания критика уезжали в системный промт — то есть структура запроса Z1
различалась между единицами, где термы банка есть и где их нет. Селфтест этого не ловил,
потому что проверял ИНДЕКС, а не роль: `m[1]` существует всегда."""
key = _pair(p)["para"]().key
bf = None if key == "zh-ru" else _pair(p)["para"]().bank_file
m = PR.editor_msgs(src, draft, PR.law_block(PR.terms_in(src, bf), bf), pair=key)
if m[-1].get("role") != "user":
raise SystemExit(f"⛔ последнее сообщение редактора не `user` ({m[-1].get('role')}) — "
"замечания критика уехали бы в системный промт, СТОП")
m[-1]["content"] = m[-1]["content"] + Z1_HEAD + "\n".join(f"- {n}" for n in notes)
return m
def editor_msgs(p: str, src: str, draft: str) -> list[dict]:
"""Боевой редактор без изменений — им куплены Z8R и ZF."""
key = _pair(p)["para"]().key
bf = None if key == "zh-ru" else _pair(p)["para"]().bank_file
return PR.editor_msgs(src, draft, PR.law_block(PR.terms_in(src, bf), bf), pair=key)
# ── ОТБОР ЕДИНИЦ ──────────────────────────────────────────────────────────────────────────────
def pool(p: str) -> list[dict]:
"""Единицы, где ВСЕ бесплатные армы и ответ критика уже есть. Меньше — покупать не на чем."""
_pair(p)["wire"]()
out = []
for u in _pair(p)["units"]():
if all(free_text(p, a, u["uid"]).strip() for a in FREE) and crit_raw(p, u["uid"]).strip():
out.append(u)
return out
def chosen(p: str) -> list[dict]:
"""N_UNITS единиц, отобранных ДЕТЕРМИНИРОВАННО от соли захода.
⚠ Почему не «самые длинные» и не «первые по списку». Длина коррелирует с трудностью, а
трудность — с тем, какой арм выигрывает; отбор по ней покупает часть вывода заранее. Отбор по
хешу от объявленной ДО покупок соли не коррелирует ни с чем и воспроизводим кем угодно.
"""
us = pool(p)
if len(us) <= N_UNITS:
return us
return sorted(us, key=lambda u: hashlib.sha256(f"{SALT}|{p}|{u['uid']}".encode()).hexdigest()
)[:N_UNITS]
# ── ГЕЙТ ПЕРЕЛОМА ЦЕНЫ ────────────────────────────────────────────────────────────────────────
def price_gate(quiet: bool = False) -> None:
now = dt.datetime.now(dt.timezone.utc)
pin = ROSTER.CANDIDATES[EDITOR][2]
stale = abs(pin - JULY_PIN_PRO) < 1e-9
left = (PRICE_CUTOVER - now).total_seconds() / 3600
if now < PRICE_CUTOVER:
if not quiet:
print(f" цена DeepSeek: до перелома {left:.1f} ч, июльский пин ${pin}/1M ВЕРЕН")
return
if stale:
raise SystemExit(
"⛔ ПЕРЕЛОМ ЦЕНЫ DeepSeek УЖЕ НАСТУПИЛ (16:00 UTC 16.08.2026), а "
f"`eval/tenant_panel/roster.py` всё ещё несёт июльский пин ${pin}/1M.\n"
" Покупка ОТКАЗАНА: записанная цена клетки была бы втрое ниже списанной, касса\n"
" печатала бы неправду, а проекционный гард резервировал бы втрое меньше нужного.\n"
" Пере-пинить ростер (пик $1.32/$3.96 pro, $0.44/$1.32 flash) и повторить.")
print(f" цена DeepSeek: перелом пройден, ростер пере-пинен (${pin}/1M) — покупка разрешена")
# ── ПОКУПКА ───────────────────────────────────────────────────────────────────────────────────
def _buy(led, tg: str, model: str, msgs: list[dict], max_out: int, **extra) -> None:
rec = MONEY.purchase(led, tg, model, ED22.client(model),
ROSTER.call_kwargs(model, msgs, max_out=max_out), **extra)
if rec.get("skipped"):
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на клетке {tg} — прогон ОСТАНОВЛЕН.\n"
" Резать панель или поднимать потолок решением сессии ЗАПРЕЩЕНО.\n"
" Это СТОП и вопрос владельцу: назвать недостающую сумму.")
def preflight(models: list[str], phase: str, letter: str) -> None:
"""Пред-полётная проба: КАЖДАЯ модель прогона отвечает, ДО того как пойдёт цикл покупок.
⚠ ЗАВЕДЕНО ПО ИНЦИДЕНТУ 16.08, И ЭТО ДЫРА В ИНФРЕ, А НЕ ПЕДАНТИЗМ. Заход ушёл в цикл на 160
клеток, не проверив ничего; на 62-й клетке у провайдера кончился баланс, и оставшиеся 96
запросов ушли в `402 Insufficient Balance` — по 0.9 с каждый, с записью файла на диск. Денег
это не стоило (отказ бесплатен), но выглядело как «купили 80 клеток»: файлы есть, годного
содержимого нет. Ровно тот класс, о котором предупреждает хендофф — «файл клетки ≠ годная
клетка», — и он стоил мне ложного доклада владельцу.
⚠ ПРОБА ИДЁТ ЧЕРЕЗ КАССУ (починка по ревью): первая редакция звала клиента напрямую, то есть
делала ПЛАТНЫЙ вызов мимо леджера — прямое нарушение нормы «все платные вызовы через кассу,
два пути счёта». Копейки, но именно так и заводится расход, которого касса не видит. Тег
пробы несёт порядковый номер, иначе кэш `purchase` вернул бы прошлую запись и гейт перестал
бы что-либо проверять на втором прогоне.
"""
led = MONEY.Guarded(phase, default_expect=0.001)
for m in dict.fromkeys(models):
k = 1
while (OUT / f"dv-{letter}-probe-{m}-{k}.json").exists():
k += 1
tg = f"dv-{letter}-probe-{m}-{k}"
rec = MONEY.purchase(led, tg, m, ED22.client(m),
ROSTER.call_kwargs(m, [{"role": "user", "content": "ок"}],
max_out=8000),
role="preflight")
if rec.get("finish") != "stop":
raise SystemExit(
f"⛔ ПРЕД-ПОЛЁТНАЯ ПРОБА НЕ ПРОШЛА: {m} → finish={rec.get('finish')} "
f"{str(rec.get('error'))[:200]}\n"
" Покупка НЕ запускается. Цикл на сотню клеток против неотвечающего\n"
" провайдера пишет файлы без содержимого и выглядит как успешный прогон.")
print(f" проба {m}: отвечает, ${rec.get('cost_usd', 0):.6f} (учтено кассой)")
def refill(p: str, which: list[str]) -> int:
"""Увести оборванные клетки (`finish=length`, пусто) в карантин, чтобы их можно было ПЕРЕ-купить.
⚠ ЗАВЕДЕНО ПО РЕВЬЮ: без этого оборванная клетка — мёртвая зона. Кэш `purchase` смотрит на
СУЩЕСТВОВАНИЕ файла и возвращает старую запись, а `cell()` требует `finish=stop` и отдаёт
пусто; значит смета считает клетку в «надо», а покупка на ней — вечный бесшумный no-op.
Файл не удаляется, а переименовывается: деньги за него списаны и обязаны остаться видимы
кассе (глоб считает `dv-*.json`, карантинное имя под него попадает).
"""
n = 0
for arm in which:
for u in chosen(p):
f = OUT / f"{tag(p, arm, u['uid'])}.json"
if not f.exists():
continue
rec = json.loads(f.read_text(encoding="utf-8"))
if rec.get("finish") == "length" and not (rec.get("content") or "").strip():
k = 1
while (OUT / f"{f.stem}.LENGTH{k}.json").exists():
k += 1
f.rename(OUT / f"{f.stem}.LENGTH{k}.json")
print(f" {f.stem}: обрыв на потолке вывода → карантин, клетка пере-покупаема")
n += 1
return n
def buy(p: str, which: list[str]) -> None:
price_gate()
cfg = _pair(p)
cfg["wire"]()
preflight([DRAFT_MODEL if a == "Z8" else EDITOR for a in which],
cfg["phase"], cfg["letter"])
us = chosen(p)
led = MONEY.Guarded(cfg["phase"], default_expect=0.009)
mo = cfg["max_out"]
print(f"\nпара {p} · единиц {len(us)} · армы {' '.join(which)} · касса {cfg['phase']}")
print(f" оборванных клеток уведено в карантин: {refill(p, which)}")
for arm in which:
for u in us:
uid, src = u["uid"], u["source"]
tg = tag(p, arm, uid)
if cell(tg).strip():
continue
# ⚠ ГЕЙТ ПЕРЕЛОМА ЦЕНЫ — ПЕРЕД КАЖДОЙ КЛЕТКОЙ, А НЕ ОДИН РАЗ НА СТАРТЕ (починка по
# ревью): прогон длится часами, и пересёкший 16:00 UTC цикл писал бы пост-переломные
# вызовы по июльскому прайсу — леджер занижен втрое ровно на том куске, который куплен
# после перелома.
price_gate(quiet=True)
if arm == "Z8":
# штатный гейт годности + ОДИН ре-ген: эхо стохастично по вызову (квирки п.6)
for attempt in range(REGEN_MAX + 1):
_buy(led, tg, DRAFT_MODEL, z8_msgs(p, src), mo,
role="translator", uid=uid, arm=arm, attempt=attempt)
why = C.draft_defect(cell(tg))
if not why:
break
if attempt < REGEN_MAX:
quarantine(tg, f"черновик негоден ({why}), ре-ген")
else:
print(f" {uid}: {arm} негоден и после ре-гена ({why}) — "
"клетка остаётся, арм на этой единице объявляется несобранным")
elif arm == "Z8R":
d = draft_ok(p, uid)
if not d.strip():
print(f" {uid}: годного обогащённого черновика нет — {arm} пропущен")
continue
_buy(led, tg, EDITOR, editor_msgs(p, src, d), mo,
role="editor", uid=uid, arm=arm)
elif arm == "Z1":
notes, dropped = critic_notes(p, uid)
if not notes:
print(f" {uid}: замечаний критика нет — {arm} пропущен, объявить в отчёте")
continue
_buy(led, tg, EDITOR, z1_msgs(p, src, free_text(p, "ZD", uid), notes), mo,
role="editor", uid=uid, arm=arm, notes=len(notes), notes_dropped=dropped)
elif arm == "Z7":
op = free_text(p, "ZP", uid)
gaps = C.canon_gaps(src, op)
if not gaps:
C._free_cell(tg, op, role="fixer", uid=uid, arm=arm, places=0)
continue
places = [f"КАНОН: термин {g} передан не канонной формой либо пропущен"
for g in gaps]
_buy(led, tg, EDITOR, C.fix_msgs(src, op, places), mo,
role="fixer", uid=uid, arm=arm, places=len(places))
elif arm == "ZF":
_buy(led, tg, EDITOR, editor_msgs(p, src, free_text(p, "ZD", uid)), mo,
role="floor2", uid=uid, arm=arm)
else:
raise SystemExit(f"⛔ арм {arm} не покупается этим файлом")
MONEY.report()
# ── СМЕТА И ПРОВЕРКИ ──────────────────────────────────────────────────────────────────────────
# ⚠ СМЕТА СЧИТАЕТСЯ ПО ТЕКУЩЕМУ ПРАЙСУ, А НЕ ПО ЗАШИТЫМ МЕДИАНАМ (починка 16.08). Первая
# редакция несла медианы, снятые при ИЮЛЬСКОМ пине, и после пере-пина продолжала печатать те же
# числа со строкой «по действующему прайсу» — то есть смета врала вчетверо и врала уверенно.
# Теперь цена арма выводится из ЗАМЕРЕННЫХ ТОКЕНОВ уже купленных клеток той же роли и
# пере-оценивается ростером; зашитых цен в файле не остаётся вовсе.
ROLE_OF = {"Z8": "translator", "Z8R": "editor", "Z1": "editor", "Z7": "fixer", "ZF": "floor2"}
MODEL_OF = {"Z8": DRAFT_MODEL, "Z8R": EDITOR, "Z1": EDITOR, "Z7": EDITOR, "ZF": EDITOR}
def price_of(arm: str) -> float:
"""Ожидаемая цена клетки арма по ТЕКУЩЕМУ прайсу, из токенов уже купленных клеток той же роли."""
import statistics as _st
m, role = MODEL_OF[arm], ROLE_OF[arm]
toks = []
for f in OUT.glob("dv-*.json"):
if any(x in f.name for x in ("ERROR", "ЭХО", "LENGTH", "probe")):
continue
try:
r = json.loads(f.read_text(encoding="utf-8"))
except Exception: # noqa: BLE001
continue
if not isinstance(r, dict) or r.get("model") != m or not r.get("prompt_tokens"):
continue
if r.get("role") not in (role, "editor" if role == "floor2" else role):
continue
toks.append((r["prompt_tokens"],
r.get("cached_tokens") or r.get("prompt_cache_hit_tokens") or 0,
r.get("completion_tokens") or 0, r.get("reasoning_tokens") or 0,
r.get("total_tokens") or 0))
if not toks:
return 0.02
med = [_st.median([x[i] for x in toks]) for i in range(5)]
return ROSTER.cost(m, int(med[0]), int(med[1]), int(med[2]), int(med[3]), int(med[4]))
WHAT = {"Z8": "обогащённый черновик (flash + мандат перевёрстки)",
"Z8R": "Z8 + боевой перепис — «лучше ли связка на лучшем черновике»",
"Z1": "критик → ПОЛНЫЙ перепис по его замечаниям",
"Z7": "однопроходка + канон-фиксер — банк без черновика",
"ZF": "ВТОРАЯ генерация боевой связки — шумовой пол новой рубрики"}
def cmd_dry() -> None:
print(f"ЗАХОД Д17 · соль отбора {SALT} · единиц на ось {N_UNITS}\n")
price_gate()
tot = 0.0
for p in PAIRS:
us, pl = chosen(p), pool(p)
print(f"\n=== пара {p} · пул {len(pl)} · взято {len(us)} · касса {_pair(p)['phase']} ===")
nd = sum(len(critic_notes(p, u["uid"])[0]) for u in us)
dr = sum(critic_notes(p, u["uid"])[1] for u in us)
print(f" замечаний критика {nd}, отсеяно подтверждений и безадресных строк {dr} "
f"({dr / max(nd + dr, 1):.0%})")
sub = 0.0
for a in ARMS:
have = sum(1 for u in us if cell(tag(p, a, u["uid"])).strip())
need = len(us) - have
pr = price_of(a)
cost = need * pr
sub += cost
print(f" {a:4s} есть {have:2d} · надо {need:2d} · ${pr:.5f}/кл · "
f"смета ${cost:.4f} {WHAT[a]}")
ph = _pair(p)["phase"]
sp = MONEY.Guarded(ph).spent() if ph in MONEY.CEILINGS else 0.0
ceil = MONEY.CEILINGS.get(ph, 0.0)
mark = "ВЛЕЗАЕТ" if sub <= ceil - sp else f"НЕ ВЛЕЗАЕТ (не хватает ${sub - (ceil - sp):.4f})"
print(f" ИТОГО ${sub:.4f} · потолок {ph} ${ceil:.2f} · потрачено ${sp:.4f}{mark}")
tot += sub
now = dt.datetime.now(dt.timezone.utc)
# ⚠ ПОПРАВКА 30.08: у вендора пик — ЕЩЁ И ТОЛЬКО ПО БУДНЯМ, а у нас этой оговорки не было.
# Дословно (`api-docs.deepseek.com/quick_start/pricing`, живое чтение 30.08.2026): «Peak hours
# are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday (all other hours are off-peak)».
# Поймано разведкой ландшафта: Bloomberg 23.08 писал о снятии пиковой цены на выходных, вендорская
# страница это подтверждает формулировкой. Без `weekday() < 5` риг звал воскресное утро ПИКОМ.
# ⚠ Это правка ТОЛЬКО ПЕЧАТИ: `peak` нигде, кроме этой сводки, не используется — цена клеток
# считается из пина ростера, а он ПИКОВЫЙ сознательно (D39.136: потолок обязан срабатывать
# раньше, а не позже). Ледждер не трогается.
peak = now.weekday() < 5 and now.hour in tuple(range(1, 4)) + tuple(range(6, 10))
print(f"\nВЕСЬ ЗАХОД: ${tot:.4f} по ТЕКУЩЕМУ пину ростера (пик).")
print(f" Сейчас {now:%H:%M} UTC — {'ПИК' if peak else 'ОФФ-ПИК'}; вендорский офф-пик ровно "
f"вдвое дешевле, то есть фактически спишется около ${tot if peak else tot / 2:.4f}.")
print("Касса пишет ПИКОВУЮ цену осознанно (политика D39.136): потолок срабатывает раньше,\n"
" а не позже; переплата резерва в офф-пик — цена простоты плоской схемы.")
def cmd_selftest() -> int:
bad = 0
def ck(n: str, ok: bool, d: str = "") -> None:
nonlocal bad
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
for p in PAIRS:
us = chosen(p)
ck(f"{p}: единиц ровно {N_UNITS}", len(us) == N_UNITS, f"их {len(us)}")
ck(f"{p}: uid уникальны", len({u["uid"] for u in us}) == len(us))
ck(f"{p}: отбор ДЕТЕРМИНИРОВАН (повтор даёт тот же набор)",
[u["uid"] for u in chosen(p)] == [u["uid"] for u in us])
ck(f"{p}: все три бесплатных арма есть на каждой единице",
all(free_text(p, a, u["uid"]).strip() for a in FREE for u in us))
u = us[0]
# Z8 обязан отличаться от боевого черновика РОВНО мандатом перевёрстки — иначе арм пуст
key = _pair(p)["para"]().key
base = PR.translator_msgs(u["source"], None, pair=key)[0]["content"]
z8 = z8_msgs(p, u["source"])[0]["content"]
ck(f"{p}: Z8 = черновик + мандат перевёрстки", z8.startswith(base.rstrip())
and DISCOURSE_HEAD in z8, f"дельта {len(z8) - len(base)} знаков")
ck(f"{p}: в Z8 нет нераскрытых плейсхолдеров", "{{" not in z8)
ck(f"{p}: FEWSHOT в Z8 не уехал (в бою его нет ни у кого)", "FEWSHOT" not in z8)
# ⚠ Э-17.1: потребитель Z8 обязан ходить через гейт годности, иначе редактор будет
# «редактировать» эхо исходника за полную цену. Гейт проверяется НА ЭХЕ, а не на словах.
ck(f"{p}: гейт годности ловит эхо исходника",
bool(C.draft_defect(u["source"] * 3)), "штатный гейт проекта молчит на исходнике")
ck(f"{p}: гейт годности пропускает нормальный русский текст",
not C.draft_defect(free_text(p, "ZD", u["uid"])))
# Z1 обязан быть БОЕВЫМ редактором плюс замечания, а не новым промтом
notes, _ = critic_notes(p, u["uid"])
z1 = z1_msgs(p, u["source"], free_text(p, "ZD", u["uid"]), notes or ["тест"])
ed = editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"]))
ck(f"{p}: Z1 несёт системный промт боевого редактора ПОБАЙТНО",
z1[0]["content"] == ed[0]["content"])
# ⚠ ПРОВЕРКА ПО РОЛИ, А НЕ ПО ИНДЕКСУ (починка по ревью). Прежняя редакция смотрела в
# `m[1]`, который при непустом банке есть закон-блок с ролью `system`: замечания критика
# уезжали в системный промт, а гейт этого не видел, потому что индекс существует всегда.
usr = [m for m in z1 if m["role"] == "user"]
edu = [m for m in ed if m["role"] == "user"]
ck(f"{p}: у Z1 ровно одно пользовательское сообщение", len(usr) == 1, f"их {len(usr)}")
ck(f"{p}: замечания критика уходят ПОЛЬЗОВАТЕЛЬСКИМ сообщением",
bool(usr) and bool(edu) and usr[-1]["content"].startswith(edu[-1]["content"])
and len(usr[-1]["content"]) > len(edu[-1]["content"]))
ck(f"{p}: в системный промт Z1 замечания НЕ попали",
not any("ДОБАВЛЕНИЕ к твоей обычной работе" in m["content"]
for m in z1 if m["role"] == "system"))
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ",
bool(usr) and "ДОБАВЛЕНИЕ к твоей обычной работе" in usr[-1]["content"])
# ZF обязан быть побайтной копией запроса Z0 — иначе это не пол, а другой арм
ck(f"{p}: ZF = ТОТ ЖЕ запрос, что у боевой связки (пол, а не новый арм)",
editor_msgs(p, u["source"], free_text(p, "ZD", u["uid"])) == ed)
# фильтр критика обязан ловить подтверждения и не выедать находки целиком
allk = sum(len(critic_notes(p, x["uid"])[0]) for x in us)
alld = sum(critic_notes(p, x["uid"])[1] for x in us)
ck(f"{p}: фильтр критика оставляет находки", allk > 0, f"осталось {allk}, отсеяно {alld}")
ck(f"{p}: фильтр критика ЧТО-ТО отсеивает (дефект A3 был реален)", alld > 0)
# теги: покрыты глобом кассы своей фазы и не сталкиваются с чужими
tg = tag(p, "Z8", u["uid"])
ck(f"{p}: тег покрыт глобом кассы {_pair(p)['phase']}",
tg.startswith(f"dv-{_pair(p)['letter']}-"), tg)
ck(f"{p}: фаза заведена в кассе", _pair(p)["phase"] in MONEY.CEILINGS)
ck(f"{p}: теги армов различны", len({tag(p, a, u["uid"]) for a in ARMS}) == len(ARMS))
# общие
ck("пары не делят буквы тегов", len({v["letter"] for v in PAIRS.values()}) == len(PAIRS))
ck("касса покупок смотрит в каталог фазы", MONEY.M22.BUY.OUT == OUT)
ck("согласие критика опознаётся", _is_confirmation("цитата → передано верно"))
ck("согласие с «ё» опознаётся (критик пишет «приёмлемо»)",
_is_confirmation("цитата → приёмлемо"))
ck("находка НЕ опознаётся как согласие", not _is_confirmation("цитата → адресат инвертирован"))
ck("СМЕШАННАЯ строка остаётся находкой (дефект перевешивает согласие в хвосте)",
not _is_confirmation("цитата → адресат инвертирован, но в остальном верно"))
ck("строка без «→» не выбрасывается только за отсутствие стрелки",
not _is_confirmation("Рассогласование реплик: реплика приписана не тому говорящему"))
ck("гейт перелома цены знает дату вендора",
PRICE_CUTOVER.isoformat() == "2026-08-16T16:00:00+00:00")
print(f"\n{'ЗАХОД ГОДЕН К ПОКУПКЕ' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
if __name__ == "__main__":
a = sys.argv[1:] or ["--dry"]
if a[0] == "--selftest":
sys.exit(1 if cmd_selftest() else 0)
elif a[0] == "--dry":
cmd_dry()
elif a[0] == "--echo":
echo_report()
elif a[0] == "--buy":
pr = [x for x in a[1:] if x in PAIRS] or list(PAIRS)
ar = [x for x in a[1:] if x in ARMS] or list(ARMS)
for x in pr:
buy(x, ar)
else:
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; знаю --dry --selftest --buy --echo")