Freeze the role-built one-pass prompt, its buying code and the sanctioned ceilings before the first cell

This commit is contained in:
Claude (backend session) 2026-08-20 15:39:46 +03:00
parent a335acc0af
commit 7f15323a8d
3 changed files with 469 additions and 2 deletions

View file

@ -81,6 +81,8 @@ TAGS = {
"ФД-J": ("dv-j-*.json",), # Д17: заход под починенную рубрику, английская пара
"ФД-K": ("dv-k-*.json",), # Д22: подача входа критику, китайская пара
"ФД-L": ("dv-l-*.json",), # Д22: подача входа критику, английская пара
"ФД-M": ("dv-m-*.json",), # Д23: однопроходка как РОЛЬ, китайская пара
"ФД-N": ("dv-n-*.json",), # Д23: однопроходка как РОЛЬ, английская пара
}
# ⚠ Потолки фаз = смета `plan.py` плюс МИНИМАЛЬНЫЙ запас, так чтобы их СУММА не превышала
# пакетный потолок. Первая редакция округляла каждый вверх до цента и дала сумму 4.54 > 4.50 —
@ -231,8 +233,9 @@ TAGS = {
# времени суток. В офф-пик спишется около $1.09 вместо $1.51. Касса букирует ПИК осознанно.
CEILINGS = {"ФД-A": 1.910, "ФД-B": 0.900, "ФД-C": 0.370, "ФД-D": 0.050,
"ФД-E": 2.290, "ФД-F": 0.500, "ФД-G": 0.060, "ФД-H": 0.090,
"ФД-I": 3.950, "ФД-J": 1.950, "ФД-K": 0.400, "ФД-L": 1.300}
PACK_CEILING = 13.80
"ФД-I": 3.950, "ФД-J": 1.950, "ФД-K": 0.400, "ФД-L": 1.300,
"ФД-M": 2.100, "ФД-N": 2.400}
PACK_CEILING = 18.30
# ⚠ ТРЕТИЙ ПОДЪЁМ ФД-I — ЭТО МОЯ ОШИБКА ОЦЕНКИ, А НЕ РАСШИРЕНИЕ ЗАКАЗАННОГО, И ЗАПИСЫВАЮ ЕЁ ТАК.
# Владелец санкционировал словом «Поднимай» ПЯТЬ клеток шумового пола. Три купились, две не
# влезли: я считал китайскую клетку пола по $0.059, а фактическая цена оказалась $0.098 —

View file

@ -0,0 +1,76 @@
<!-- ЯДРО ПРОМТА ОДНОПРОХОДКИ. Общее для всех пар; пар-специфика подставляется двумя блоками
из файлов САМОЙ ПАРЫ (см. `rol.py`): «Единицы и приёмы» из её translator.md и
«ДИСКУРС-ПЕРЕВЁРСТКА» из её editor.md. Второго источника правды не заводится: пар-блоки
остаются там, где они уже живут и где их сторожит гейт пар-промтов. Пара, которой в
репозитории ещё НЕТ, заводится своими двумя блоками и работает без правки кода.
ЧЕМ ЭТОТ ПРОМТ ОТЛИЧАЕТСЯ ОТ ПРЕЖНЕЙ ОДНОПРОХОДКИ И ПОЧЕМУ — каждый пункт грунтован замером,
а не вкусом:
· прежний промт был СКЛЕЙКОЙ (промт переводчика + мандатная часть промта редактора,
четыре строки выброшены, четыре подставлены) и нёс мета-фразу про наш конвейер
«отдельного редактора после тебя НЕ БУДЕТ». Склейка сделана ради честности контраста
(уравнять ОБЪЁМ инструкции), но продуктовым промтом она не была ни разу;
· ВОЛЬНОСТЬ ЯВНО РАЗРЕШЕНА. Решение владельца 16.08: «штрафовать за вольность нельзя, живой
язык — приоритет; штрафовать можно только за перевирания смысла». Ни один боевой промт
такого разрешения не давал — оба только ЗАПРЕЩАЛИ. Модель осторожничала законно;
· РЕГИСТР назван отдельной рамкой. Слепое чтение владельца 17.08 забраковало боевой перевод
именно по регистру: ярость передана словом «шествовала», мат смягчён там, где на мате
держится сцена, вежливое «Пойдёмте» в реплике, которую выплёвывают. Ни в одном промте
слова «регистр» нет вовсе;
· КРИТЕРИИ ПРОВЕРКИ напечатаны в самом промте — прямое требование владельца 20.08: «зажми
рамками и по каким критериям мы будем проверять результат». Это же снимает противоречие,
на котором модель раньше зажималась: она видит, что вольность в рамках не штрафуется;
· мета-фраз про устройство конвейера нет ни одной. -->
Ты — литературный переводчик. Твой текст идёт в книгу, которую будут читать по-русски ради
удовольствия. Читатель не должен чувствовать, что перед ним перевод.
Книга: «{{title}}». Жанр: {{genre}}. Аудитория: {{audience}}.
Язык исходника — «{{source_lang}}», язык перевода — «{{target_lang}}».
Хонорифики: {{honorifics}}. Транскрипция имён и реалий: {{transcription}}.
Баланс форенизация/доместикация (0 — полная адаптация, 1 — сохранение чужого): {{venuti}}.
Сноски: {{footnotes}}.
ТЫ ВПРАВЕ — и это не оговорка, а часть задания:
переставлять предложения · сливать и дробить абзацы · менять синтаксис до неузнаваемости ·
заменять оборот на более живой русский · добавлять связки, которых нет в исходнике ·
разворачивать идиому и сворачивать описание · выбирать слово, которого в словаре напротив
исходного не стоит. Буквальность здесь не достоинство. Если выбор стоит между точной калькой и
живой русской фразой — бери живую фразу.
ЧЕГО НЕЛЬЗЯ — четыре запрета, и они жёсткие:
1. Перевирать факт: участника действия, отрицание, число, ранг, меру, родство, кто кому что
сказал. Полярность и адресат неприкосновенны.
2. Терять смысловые ходы. Дробить и сливать можно; выбрасывать нельзя. Ничего не добавляй от себя
как факт — связка и уточнение допустимы, новое событие нет.
3. Менять канон. Имена, термины и реалии — строго в тех формах, что даны в глоссарии ниже,
склоняя по контексту. Своих вариантов не придумывай.
4. Оставлять в выводе исходный язык, служебные преамбулы, комментарии, пояснения, заметки о
решениях и markdown-заголовки.
РЕГИСТР — держи тот же градус, что в исходнике:
грубость передавай грубостью, ярость — яростью, спокойствие — спокойствием. Брань не смягчай и не
заменяй эвфемизмом: если персонаж ругается, он ругается. Не поднимай слог — там, где в исходнике
простое резкое слово, книжное слово будет ошибкой. Вежливость есть свойство персонажа, а не
переводчика: не добавляй вежливых форм и смягчающих оборотов, которых в реплике нет. Речевой
портрет героя (просторечие, архаика, чужой акцент) передавай лексикой и синтаксисом.
ПО КАКИМ КРИТЕРИЯМ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ — проверь себя по ним сам, молча, перед выводом:
* СМЫСЛ — ни один факт не перевран (см. запрет 1);
* ПОЛНОТА — каждый смысловой ход исходника присутствует в переводе;
* КАНОН — каждый термин глоссария стоит в канонной форме;
* ЯЗЫК — в тексте нет ни одной фразы, которую носитель русского не написал бы сам: ни кальки,
ни канцелярита, ни переводного суржика;
* РЕГИСТР — градус тот же, что в исходнике.
Вольность в пределах этих критериев ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше и не
нарушает ни одного из пяти, лучше текста, который следует за буквой.
{{ПАР_ЕДИНИЦЫ}}
{{ПАРЕРЕВЁРСТКА}}
Выведи ТОЛЬКО перевод.
---USER---
Переведи следующий фрагмент:
{{text}}

388
eval/dovodka/rol.py Normal file
View file

@ -0,0 +1,388 @@
#!/usr/bin/env python3
"""ОДНОПРОХОДКА КАК РОЛЬ — новый промт против прежней склейки, на нескольких моделях. Платный.
ЗАЧЕМ. Однопроходка единственный живой продуктовый кандидат из всего, что дуга 1923
измерила: по судейской оси от боевой связки не отличается, по банку терминов ЛУЧШЕ неё на обеих
парах (потерянных терминов на главу 1.38 против 2.88 на zh, 0.31 против 0.56 на en), стоит ОДИН
вызов вместо двух, и при слепом чтении владелец поставил её ПЕРВОЙ на английском.
И всё это она делает С ГАНДИКАПОМ. Её промт (`contour.a2_msgs`) не продуктовый, а
ИЗМЕРИТЕЛЬНЫЙ: боевой промт переводчика плюс мандатная часть боевого промта редактора,
механически склеенные (четыре строки выброшены, четыре подставлены), с прямой мета-фразой про наш
конвейер «отдельного редактора после тебя НЕ БУДЕТ». Склейка сделана по уважительной причине:
в эксп-21 у однопроходки системный промт был вдвое короче, и замеренная разница могла быть
разницей ОБЪЁМА ИНСТРУКЦИИ, а не топологии. Уравнивание было правильным ДЛЯ ЗАМЕРА но
следствие в том, что однопроходка ни разу не испытывалась как нормально написанный промт-роль.
ЧТО ПОКУПАЕТСЯ (имена армов ОДНИ на обе пары, пара живёт в данных):
ZP ПРЕЖНЯЯ СКЛЕЙКА, deepseek-v4-pro БЕСПЛАТНО, куплена заходом Д17 это база
Z0 боевая связка (черновик перепис) БЕСПЛАТНО эталон продакшена
ZF вторая генерация связки БЕСПЛАТНО пол СТАРОЙ панели
ZD голый черновик БЕСПЛАТНО якорь низа
RN НОВАЯ РОЛЬ, deepseek-v4-pro, генерация 1 покупается несущий арм
RN2 НОВАЯ РОЛЬ, deepseek-v4-pro, генерация 2 покупается СВОЙ пол новой панели
RG НОВАЯ РОЛЬ, glm-5 покупается переносимость, другой вендор
RK НОВАЯ РОЛЬ, grok-4.3 покупается переносимость, третья семья
ПЕРВИЧНЫЙ КОНТРАСТ ОДИН: `RN/ZP` новый промт против прежней склейки НА ТОЙ ЖЕ МОДЕЛИ и ТЕХ ЖЕ
главах. Всё остальное описательное. Одного контраста требует мощность: при n=16 и поправке Холма
по четырём контрастам MDE вырос бы выше цели, и ось не дала бы вывода ни при каком исходе ровно
та болезнь, что погубила английскую ось эксп-22.
ТРЕТЬЯ МОДЕЛЬ `grok-4.3`, и выбор не случаен. Главная претензия владельца при слепом чтении
была РЕГИСТР: «смягчённый мат там, где на мате держится сцена». xAI единственный вендор, у
которого слова `violen*` в правилах нет вовсе, и модель меньше прочих склонна смягчать.
`gemini` в панель НЕ берётся: на этом материале он массово падает в контент-фильтр (эксп-21:
10 клеток судейского прогона с `PROHIBITED_CONTENT`), и его пустые клетки мерили бы фильтр, а не
промт. `grok-4.3` идёт с ВКЛЮЧЁННЫМ размышлением (вендор-дефолт `low`): запрет D30.1 прямой
grok с reasoning-off в редакторской роли есть no-op-редактор.
Запуск: rol.py --show [zh|en] | --dry | --selftest | --buy [zh|en] [армы] | --canon
"""
from __future__ import annotations
import importlib.util
import json
import statistics as st
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`, и `wire()` настраивает хуки именно в нём. Свой второй экземпляр остался бы с
# китайскими хуками, и английская ось молча стала бы пустой. Класс уже стоил фазе оплаченных клеток.
EN = _load("en_axis_r", 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
ZK = _load("zakhod_r", ZONE / "zakhod.py") # гейт перелома цены, пред-полёт, отбор глав
CORE = ZONE / "prompts" / "onepass-core.md"
MAX_OUT = 32000 # как в заходе Д17: обрыв на потолке стоит денег и не даёт данных
# ── ПАРА КАК ДАННЫЕ. Ни одна ветка ниже не спрашивает «а это китайский?» ──────────────────────
PAIRS = {
"zh": {"letter": "m", "phase": "ФД-M", "onepass": "A2",
"wire": lambda: C.configure(PARA.ZH), "units": lambda: C.units_ok(),
"para": lambda: PARA.ZH},
"en": {"letter": "n", "phase": "ФД-N", "onepass": "E2",
"wire": EN.wire, "units": EN.units, "para": lambda: PARA.EN},
}
# арм → (модель, номер генерации). Генерация нужна, чтобы два прогона ОДНОЙ модели не схлопнулись
# в один тег: кэш `purchase` смотрит на существование файла и вернул бы первую клетку второй раз.
ARMS = {"RN": ("deepseek-v4-pro", 1), "RN2": ("deepseek-v4-pro", 2),
"RG": ("glm-5", 1), "RK": ("grok-4.3", 1)}
FREE = ("ZD", "Z0", "ZP")
PRIMARY = ("RN", "ZP") # единственный несущий контраст, см. шапку
# ── СБОРКА ПРОМТА ────────────────────────────────────────────────────────────────────────────
# ⚠ ПАР-БЛОКИ НЕ ДУБЛИРУЮТСЯ, А ПОДТЯГИВАЮТСЯ ИЗ ФАЙЛОВ САМОЙ ПАРЫ. Второй экземпляр «Единиц и
# приёмов» разъехался бы с боевым молча, и замер мерил бы промт, которого в бою нет. Гейт
# пар-промтов сторожит те файлы, а не мою копию, — значит копии быть не должно.
UNITS_HEAD = "Единицы и приёмы"
REFLOW_HEAD = "ДИСКУРС-ПЕРЕВЁРСТКА"
def _block(core: str, head: str) -> str:
"""Секция от заголовка до пустой строки перед следующим заголовком верхнего уровня."""
i = core.find(head)
if i < 0:
return ""
tail = core[i:]
# секция кончается там, где начинается строка «Выведи ТОЛЬКО» или следующий ЗАГОЛОВОК КАПСОМ
stop = len(tail)
for mark in ("\nВыведи ТОЛЬКО", "\nХонорифики:"):
j = tail.find(mark)
if 0 <= j < stop:
stop = j
return tail[:stop].strip()
def pair_blocks(p: str) -> tuple[str, str]:
"""(«Единицы и приёмы» пары, «ДИСКУРС-ПЕРЕВЁРСТКА» пары) — из её же боевых файлов."""
d = PR.PAIR_DIR[PAIRS[p]["para"]().key]
tr_core, _f, _u = PR.load_template(d / "translator.md")
ed_core, _f2, _u2 = PR.load_template(d / "editor.md")
return _block(tr_core, UNITS_HEAD), _block(ed_core, REFLOW_HEAD)
def rol_msgs(p: str, src: str) -> list[dict]:
"""Однопроходка НОВЫМ промтом: роль, рамки, регистр, критерии проверки, пар-блоки, глоссарий."""
cfg = PAIRS[p]
cfg["wire"]()
key = cfg["para"]().key
units_blk, reflow_blk = pair_blocks(p)
if not units_blk or not reflow_blk:
raise SystemExit(f"у пары {p} нет блока «{UNITS_HEAD}» либо «{REFLOW_HEAD}» — "
"промт собрать нечем, арм не строится")
core, _few, user = PR.load_template(CORE)
core = (core.replace("{{ПАР_ЕДИНИЦЫ}}", units_blk)
.replace("{{ПАРЕРЕВЁРСТКА}}", reflow_blk))
brief = getattr(PR, PR.PAIR_BRIEF[key])
sysmsg = PR.render(core, brief, src, "")
# ⚠ БАНК-ЗАКОН НА ВСЕХ АРМАХ БЕЗ ИЗЪЯТИЙ (норма D39.104). Прежнее отступление — 72 клетки без
# блока — владелец пометил как дефект. Термы берутся ТОЛЬКО релевантные этому фрагменту:
# прямое требование брифа V6 «не давать слишком много контекста».
bf = None if key == "zh-ru" else cfg["para"]().bank_file
law = PR.law_block(PR.terms_in(src, bf), bf)
out = [{"role": "system", "content": sysmsg}]
if law:
# ⚠ БАНК-ЗАКОН НАПИСАН ДЛЯ ДВУХСТАДИЙНОГО ПУТИ и говорит «в ЧЕРНОВИКЕ термин слева должен
# быть передан формой справа». У однопроходки черновика нет вовсе: модель получила бы
# указание про текст, которого не существует. Поймано селфтестом ДО покупки. Правится
# ровно одно слово и ровно здесь — трогать общий `law_block` нельзя, он едет в бою.
out.append({"role": "system", "content": law.replace("в черновике", "в переводе")})
out.append({"role": "user", "content": PR.render(user, brief, src, "")})
return out
def tag(p: str, arm: str, uid: str) -> str:
return f"dv-{PAIRS[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:
"""Бесплатные армы берутся у рига захода Д17 — те же главы, те же тексты, $0."""
return ZK.free_text(p, arm, uid)
def chosen(p: str) -> list[dict]:
"""ТЕ ЖЕ главы, что у захода Д17 — иначе бесплатные армы не переиспользуются и база уедет."""
return ZK.chosen(p)
def call_kwargs(model: str, msgs: list[dict]) -> dict:
"""Боевая конфигурация модели. Отступлений нет: у `grok-4.3` вендор-дефолт = размышление ON,
а запрет D30.1 прямой reasoning-off в редакторской роли даёт no-op-редактора."""
return ROSTER.call_kwargs(model, msgs, max_out=MAX_OUT)
# ── ПОКУПКА ──────────────────────────────────────────────────────────────────────────────────
def buy(p: str, which: list[str]) -> None:
ZK.price_gate()
cfg = PAIRS[p]
cfg["wire"]()
ZK.preflight([ARMS[a][0] for a in which], cfg["phase"], cfg["letter"])
us = chosen(p)
led = MONEY.Guarded(cfg["phase"], default_expect=0.03)
print(f"\nпара {p} · глав {len(us)} · армы {' '.join(which)} · касса {cfg['phase']}")
for arm in which:
model, gen = ARMS[arm]
for u in us:
tg = tag(p, arm, u["uid"])
if cell(tg).strip():
continue
# ⚠ Гейт перелома цены — перед КАЖДОЙ клеткой, а не раз на старте: прогон длится
# часами, и пересёкший 16:00 UTC цикл писал бы вызовы по вчерашнему прайсу.
ZK.price_gate(quiet=True)
rec = MONEY.purchase(led, tg, model, ED22.client(model),
call_kwargs(model, rol_msgs(p, u["source"])),
role="onepass", uid=u["uid"], arm=arm, gen=gen, pair=p)
if rec.get("skipped"):
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на клетке {tg} — прогон ОСТАНОВЛЕН.\n"
" Резать панель или поднимать потолок решением сессии ЗАПРЕЩЕНО.\n"
" Это СТОП и вопрос владельцу: назвать недостающую сумму.")
if rec.get("finish") != "stop":
print(f"{tg}: finish={rec.get('finish')} "
f"{str(rec.get('error'))[:120]} — клетка объявляется несобранной")
elif C.draft_defect(cell(tg)):
# штатный гейт годности проекта: эхо исходника вместо перевода ловится ЗДЕСЬ,
# иначе негодная клетка уедет в судейскую пачку за полную цену
print(f"{tg}: {C.draft_defect(cell(tg))} — клетка НЕГОДНА, объявить в отчёте")
MONEY.report()
# ── $0-ГЕЙТ КАНОНА: там, где однопроходка уже выигрывала ──────────────────────────────────────
def canon() -> None:
print("КАНОН-ГЕЙТ ($0, детерминированно): потерянных терминов банка на главу, МЕНЬШЕ лучше\n")
for p, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
print(f"=== пара {p} · глав {len(us)} ===")
for arm in FREE + tuple(ARMS):
vals = []
for u in us:
t = free_text(p, arm, u["uid"]) if arm in FREE else cell(tag(p, arm, u["uid"]))
if t.strip():
vals.append(len(C.canon_gaps(u["source"], t)))
if vals:
print(f" {arm:4s} n={len(vals):2d} потерь/глава {st.mean(vals):5.2f}")
# ── СМЕТА И ПРОВЕРКИ ─────────────────────────────────────────────────────────────────────────
def price_of(p: str, model: str) -> float:
"""Ожидаемая цена клетки — из ЗАМЕРЕННЫХ токенов уже купленных ОДНОПРОХОДОК ЭТОЙ пары,
пере-оценённых ТЕКУЩИМ пином. Зашитых цен в файле нет: после пере-пина они врали вчетверо."""
toks = []
for f in OUT.glob(f"{C.tag(PAIRS[p]['onepass'], '')}*.json"):
if any(x in f.name for x in ("ERROR", "LENGTH", "ЭХО", "STALE", "TWIN")):
continue
r = json.loads(f.read_text(encoding="utf-8"))
if r.get("finish") == "stop" and r.get("prompt_tokens"):
toks.append((r["prompt_tokens"], r.get("cached_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.03
med = [st.median([x[i] for x in toks]) for i in range(5)]
# ⚠ Новый промт ДЛИННЕЕ склейки на входе — берём фактическую длину системного промта, а не
# длину прежнего: смета, посчитанная по старому входу, занижала бы резерв гарда.
grow = _sys_len(p) / max(_sys_len_old(p), 1)
return ROSTER.cost(model, int(med[0] * grow), int(med[1]), int(med[2]), int(med[3]),
int(med[4]))
def _sys_len(p: str) -> int:
u = chosen(p)[0]
return sum(len(m["content"]) for m in rol_msgs(p, u["source"]))
def _sys_len_old(p: str) -> int:
PAIRS[p]["wire"]()
u = chosen(p)[0]
return sum(len(m["content"]) for m in C.a2_msgs(u["source"]))
def cmd_show(p: str) -> None:
"""Промт целиком, глазами. Обязательная вычитка ПЕРЕД покупкой."""
u = chosen(p)[0]
for m in rol_msgs(p, u["source"]):
body = m["content"]
if m["role"] == "user":
body = body.split("Переведи следующий фрагмент:")[0] + \
"Переведи следующий фрагмент:\n\n<текст главы, %d знаков>" % len(u["source"])
print(f"\n────────── {m['role'].upper()} ──────────\n{body}")
def cmd_dry() -> None:
print("ОДНОПРОХОДКА КАК РОЛЬ · главы те же, что у захода Д17\n")
ZK.price_gate()
tot = 0.0
for p, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
print(f"\n=== пара {p} · глав {len(us)} · касса {cfg['phase']} ===")
print(f" системный промт: новый {_sys_len(p)} знаков против склейки {_sys_len_old(p)} "
f"({_sys_len(p) / max(_sys_len_old(p), 1):.2f}×)")
sub = 0.0
for arm, (model, _g) in ARMS.items():
need = sum(1 for u in us if not cell(tag(p, arm, u["uid"])).strip())
pr = price_of(p, model)
sub += need * pr
print(f" {arm:4s} {model:18s} надо {need:2d} × ${pr:.5f} = ${need * pr:.4f}")
sp = MONEY.Guarded(cfg["phase"]).spent()
ceil = MONEY.CEILINGS.get(cfg["phase"], 0.0)
mark = "ВЛЕЗАЕТ" if sub <= ceil - sp else f"НЕ ВЛЕЗАЕТ (не хватает ${sub - (ceil - sp):.4f})"
print(f" ИТОГО ${sub:.4f} · потолок ${ceil:.2f} · потрачено ${sp:.4f}{mark}")
tot += sub
print(f"\nВЕСЬ ЗАМЕР: ${tot:.4f} по текущему (пиковому) пину. В офф-пик DeepSeek вдвое дешевле.")
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, cfg in PAIRS.items():
cfg["wire"]()
us = chosen(p)
ck(f"{p}: глав ровно 16", len(us) == 16, f"их {len(us)}")
ck(f"{p}: все три бесплатных арма есть на каждой главе",
all(free_text(p, a, u["uid"]).strip() for a in FREE for u in us))
u = us[0]
m = rol_msgs(p, u["source"])
sysall = "\n".join(x["content"] for x in m if x["role"] == "system")
# ⚠ ПАР-БЛОКИ ДОЕХАЛИ. Пустой блок сделал бы промт беднее боевого, и замер сравнивал бы
# не «роль против склейки», а «роль минус пар-специфика против склейки».
ub, rb = pair_blocks(p)
ck(f"{p}: блок «{UNITS_HEAD}» пары подтянут", len(ub) > 200, f"{len(ub)} знаков")
ck(f"{p}: блок «{REFLOW_HEAD}» пары подтянут", len(rb) > 400, f"{len(rb)} знаков")
ck(f"{p}: оба блока стоят В ПРОМТЕ", ub[:60] in sysall and rb[:60] in sysall)
ck(f"{p}: плейсхолдеры раскрыты все до одного", "{{" not in sysall)
# ⚠ ГЛАВНОЕ ОТЛИЧИЕ ОТ СКЛЕЙКИ — мета-фразы про конвейер быть не должно
ck(f"{p}: мета-фразы про отсутствующего редактора НЕТ",
"НЕ БУДЕТ" not in sysall and "после тебя" not in sysall)
ck(f"{p}: вольность РАЗРЕШЕНА явно", "ТЫ ВПРАВЕ" in sysall)
ck(f"{p}: регистр назван отдельной рамкой", "РЕГИСТР" in sysall)
ck(f"{p}: критерии проверки напечатаны в промте",
"ПО КАКИМ КРИТЕРИЯМ" in sysall and "ошибкой НЕ СЧИТАЕТСЯ" in sysall)
ck(f"{p}: черновик в промт НЕ попадает (однопроходка)",
"ЧЕРНОВ" not in sysall.upper() and "черновик" not in sysall.lower())
ck(f"{p}: банк-закон на месте либо термов в главе нет",
bool(PR.law_block(PR.terms_in(u["source"], None if cfg["para"]().key == "zh-ru"
else cfg["para"]().bank_file),
None if cfg["para"]().key == "zh-ru"
else cfg["para"]().bank_file)) == (len(m) == 3),
f"сообщений {len(m)}")
ck(f"{p}: пользовательское сообщение несёт ТЕКСТ ГЛАВЫ",
u["source"][:80] in m[-1]["content"])
ck(f"{p}: ровно одно пользовательское сообщение",
sum(1 for x in m if x["role"] == "user") == 1)
# промт обязан ОТЛИЧАТЬСЯ от склейки, иначе замер пуст
ck(f"{p}: новый промт не равен склейке",
sysall.strip() != "\n".join(x["content"] for x in C.a2_msgs(u["source"])
if x["role"] == "system").strip())
# теги и касса
tgs = {tag(p, a, u["uid"]) for a in ARMS}
ck(f"{p}: теги армов различны", len(tgs) == len(ARMS))
ck(f"{p}: теги покрыты глобом кассы {cfg['phase']}",
all(t.startswith(f"dv-{cfg['letter']}-") for t in tgs))
ck(f"{p}: фаза заведена в кассе", cfg["phase"] in MONEY.CEILINGS)
ck(f"{p}: две генерации несущей модели — РАЗНЫЕ теги",
tag(p, "RN", u["uid"]) != tag(p, "RN2", u["uid"]))
ck("пары не делят буквы тегов", len({v["letter"] for v in PAIRS.values()}) == len(PAIRS))
ck("несущий контраст объявлен один", len(PRIMARY) == 2 and PRIMARY == ("RN", "ZP"))
for a, (mo, _g) in ARMS.items():
try:
ck(f"цена считается для {a} ({mo})",
MONEY.M22.BUY.cost(mo, 3000, 0, 3600, 800, 7400) > 0)
except Exception as e: # noqa: BLE001
ck(f"цена считается для {a} ({mo})", False, f"{type(e).__name__}: {e}")
ck("grok идёт с размышлением (D30.1: reasoning-off = no-op-редактор)",
"reasoning_effort" not in json.dumps(call_kwargs("grok-4.3",
[{"role": "user", "content": "x"}])))
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] == "--show":
cmd_show(next((x for x in a[1:] if x in PAIRS), "zh"))
elif a[0] == "--canon":
canon()
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}; знаю --show --dry --selftest --buy --canon")