1265 lines
102 KiB
Python
1265 lines
102 KiB
Python
#!/usr/bin/env python3
|
||
"""ОДНОПРОХОДКА КАК РОЛЬ — новый промт против прежней склейки, на нескольких моделях. Платный.
|
||
|
||
⚠ ЗАЧЕМ. Однопроходка — живой продуктовый кандидат из того, что дуга 19→23 измерила: по судейской
|
||
оси от боевой связки не отличается и стоит ОДИН вызов вместо двух.
|
||
|
||
⛔⛔ ЭРРАТА Д30.3 (ревью 21.08). Прежняя редакция этой шапки утверждала «по банку терминов ЛУЧШЕ
|
||
связки на обеих парах — 1.38 против 2.88 на zh, 0.31 против 0.56 на en». **Числа 1.38 и 0.31
|
||
принадлежат арму `Z7`** — однопроходке ПЛЮС отдельно оплаченному канон-фиксеру. У голой
|
||
однопроходки zh 3.00 против 2.88 у связки, то есть ХУЖЕ; попарно по главам разница +2 термина за
|
||
16 глав на zh и −2 на en. **Правда: по банку голая однопроходка от связки НЕ отличается ни на одной
|
||
паре.** ⇒ у неё не остаётся объявленного преимущества по КАЧЕСТВУ — остаётся только цена.
|
||
⚠ И «владелец поставил её первой на английском» этим замером не воспроизводится: на 15 главах она
|
||
шестая из одиннадцати (Д24.3 п.7).
|
||
|
||
⚠⚠ И всё это она делает С ГАНДИКАПОМ. Её промт (`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 re
|
||
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},
|
||
}
|
||
# ── ВОЗВРАТ АВТОРСКИХ АБЗАЦЕВ ИСХОДНИКУ ──────────────────────────────────────────────────────
|
||
# ⚠⚠ ПОЧЕМУ ЭТО ОТДЕЛЬНЫЙ АРМ, А НЕ ПОЧИНКА ЭКСТРАКТОРА. Наш английский экстрактор
|
||
# (`pair_en.raw_text`) сносит из epub ВСЕ теги регексом, включая `</p>`: в книге 11 447 абзацев
|
||
# медианой по 62 знака, а модели едет один сплошной блок. Это наш баг, и чинить его надо.
|
||
# НО uid единицы есть `sha1(source.strip())` (`bakeoff.uid_of`), то есть ЛЮБАЯ правка пробелов
|
||
# переименовывает все единицы. Цена такой правки замерена: **289 оплаченных клеток на $2.64**
|
||
# теряют адресата, плюс семь файлов слепых ключей и все судейские ответы английской пары.
|
||
# Манифест это поймает и остановит покупку — гейт работает, — но история оси всё равно умрёт.
|
||
#
|
||
# ⇒ Починка делается НА ПОДАЧЕ: тот же текст той же единицы, с тем же uid, но с возвращёнными
|
||
# границами абзацев. Функция взята у `podacha.py`, а не написана второй раз.
|
||
# ⇒ И это ОТДЕЛЬНЫЙ АРМ, потому что иначе замер стал бы некорректен: если `RN` получит
|
||
# восстановленный исходник, а его база `ZP` — плоский, контраст смешает эффект ПРОМТА с эффектом
|
||
# ПОДАЧИ, и разделить их будет нечем. Поэтому:
|
||
# RN / ZP — эффект ПРОМТА (вход у обоих ОДИНАКОВЫЙ, плоский)
|
||
# RA / RN — эффект ПОДАЧИ (промт у обоих ОДИНАКОВЫЙ, новый)
|
||
# ⚠ У китайской пары арма `RA` нет и быть не может: её исходник абзацы не терял (78 строк по
|
||
# 24 знака), возвращать нечего, и клетка была бы побайтным близнецом `RN`.
|
||
POD = _load("podacha_r", ZONE / "podacha.py")
|
||
|
||
|
||
def _src_en(src: str) -> str:
|
||
r = POD.restore_paragraphs(src, POD.EPUB_EN)
|
||
if not r.strip():
|
||
raise SystemExit("⛔ авторские абзацы для единицы не нашлись — арм RA стал бы близнецом "
|
||
"RN, покупка отказана")
|
||
return r
|
||
|
||
|
||
PAIRS["en"]["restore"] = _src_en
|
||
PAIRS["zh"]["restore"] = None
|
||
|
||
# арм → (модель, номер генерации, подача исходника). Генерация нужна, чтобы два прогона ОДНОЙ
|
||
# модели не схлопнулись в один тег: кэш `purchase` смотрит на существование файла и вернул бы
|
||
# первую клетку второй раз.
|
||
ARMS = {"RN": ("deepseek-v4-pro", 1), "RN2": ("deepseek-v4-pro", 2),
|
||
"RG": ("glm-5", 1), "RGT": ("glm-5", 1),
|
||
"RK": ("grok-4.3", 1), "RKT": ("grok-4.3", 1), # RKT — клетка блокера Б6, не куплена
|
||
"RL": ("gpt-5.6-luna", 1), "RE": ("gemini-3.1-flash-lite", 1), "RET": ("gemini-3.1-flash-lite", 1),
|
||
"RA": ("deepseek-v4-pro", 1), # тот же промт, ВОССТАНОВЛЕННЫЕ абзацы исходника
|
||
"RC": ("deepseek-v4-pro", 1), # промт МИНУС рамка оценки, см. CUT
|
||
"RB": ("deepseek-v4-pro", 1)} # рамка оценки ПЕРЕВЁРНУТА: брак = чужой язык
|
||
RESTORE_ARMS = ("RA",)
|
||
|
||
# ⚠ РЕЗАНЫЕ ВАРИАНТЫ ПРОМТА. Замер `RN` дал ТРИ независимых показания одного сдвига: размышление
|
||
# ×28 против прежней склейки при входе ×1.17, слепое чтение — «самый точный по деталям, но проза
|
||
# самая серая: гладкопись подстрочника», перевёрстка 1.60 против 1.77–1.79 у обеих старых схем.
|
||
# Модель стала аккуратнее и трусливее по всем осям сразу.
|
||
# Гипотеза: пять напечатанных осей, «по которым проверят результат», заставляют оптимизировать
|
||
# ЧЕК-ЛИСТ, а не текст — держаться ближе к букве есть самый дешёвый способ не нарушить пять
|
||
# критериев разом. Ровно этот отказ предсказывали и приёмка Fable-5, и документация Anthropic
|
||
# (перенесённые инструкции самопроверки дают избыточную проверку).
|
||
# ⚠ РЕЖЕТСЯ РОВНО ОДИН БЛОК, остальное побайтно то же — иначе замер не скажет, что подействовало.
|
||
# Селфтест это ПРОВЕРЯЕТ: разница промтов обязана быть равна вырезанному куску и ничему больше.
|
||
# ⚠ ПЕРВАЯ РЕДАКЦИЯ РЕЗА БЫЛА НЕГОДНОЙ, и поймала её проверка исполнением ДО покупки: вырезание
|
||
# блока целиком уносило 1871 знак — вместе с ним исчезали ВСЕ запреты и рамка РЕГИСТРА, лучшая
|
||
# часть промта по мнению приёмки. Это был бы замер «промт без правил», а не «промт без чек-листа».
|
||
# ⇒ Режется РОВНО РАМКА ОЦЕНКИ, содержание остаётся побайтно: «по чему будет проверен результат,
|
||
# это же и есть список ошибок» → «чего нельзя». Одна переменная: знает ли модель, что её ОЦЕНЯТ
|
||
# по перечню. Убирается и мета-строка про «вольность ошибкой не считается» — она сама ссылается
|
||
# на оценку и без рамки повисает.
|
||
# ⚠ RB — ВТОРАЯ ФОРМУЛИРОВКА, И ОНА ОТ ВЛАДЕЛЬЦА. Разбор Fable-5 показал, ПОЧЕМУ экзамен
|
||
# переиграл свободы: все пять осей сформулированы как оси ИЗБЕГАНИЯ, и серая проза проходит все
|
||
# пять — «ни кальки, ни канцелярита» гладкий подстрочник проходит, факт/полноту/канон проходит
|
||
# лучше всех. Весь риск объявленного экзамена лежал на смелости и ни грамма на осторожности,
|
||
# значит держаться буквы было доминантной стратегией.
|
||
# Лечение — не убрать экзамен, а ПЕРЕВЕРНУТЬ его риск. Формулировка владельца 20.08 дословно:
|
||
# «не прям серость, я бы сказал присутствие одного языка в другом. То есть полное отсутствие
|
||
# художественности и родного звучания текста. Машинный перевод грубо говоря. Всё же перевод это
|
||
# отчасти какое-то но творчество, без перевирания смысла нужно написать текст, чтоб он звучал
|
||
# естественно». Это уточняет его же решение 16.08 («штрафовать только за перевирание смысла»):
|
||
# вторым браком объявляется не вольность, а ПРОСВЕЧИВАЮЩИЙ ИСХОДНЫЙ ЯЗЫК.
|
||
_BRAK = (
|
||
"БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый.\n"
|
||
"Первый — перевранный смысл: факт, смысловой ход, канон.\n"
|
||
"Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо\n"
|
||
"звучащая фраза, отсутствие живого русского. Машинный перевод бракуется наравне с враньём.\n"
|
||
"Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски\n"
|
||
"естественно. Вольность не проверяет никто.\n\n"
|
||
"Ниже — то, что относится к первому браку и к границам второго:")
|
||
# ⚠⚠ ХВОСТ РЕЖЕТСЯ В ОБОИХ АРМАХ, И ЭТО ПОЧИНКА ДИЗАЙНА, А НЕ ФОРМУЛИРОВКИ. Приёмка Fable-5
|
||
# поймала: после того как императив самопроверки оправдан замером (его не было на момент покупки),
|
||
# ведущим подозреваемым становится ХВОСТОВАЯ СТРОКА «не нарушает ни одной из осей» — единственное
|
||
# оставшееся место, где смелость ОБУСЛОВЛЕНА полной верификацией пяти осей. В первой редакции
|
||
# обоих резов она стояла НЕТРОНУТОЙ, то есть при исходе «оба не сработали» вывод «рамка ни при
|
||
# чём» был бы неинтерпретируем: ведущий подозреваемый не вырезан нигде.
|
||
_TAIL_OLD = ("Вольность в пределах этих пяти ошибкой НЕ СЧИТАЕТСЯ. Текст, который читается лучше "
|
||
"и не нарушает\nни одной из осей, лучше текста, который следует за буквой.")
|
||
# ⚠ Формулировка второго брака — СЛОВА ВЛАДЕЛЬЦА 20.08: «присутствие одного языка в другом …
|
||
# полное отсутствие художественности и родного звучания … машинный перевод грубо говоря … перевод
|
||
# это отчасти какое-то но творчество, без перевирания смысла нужно написать текст, чтоб он звучал
|
||
# естественно». Правки по приёмке: убран дубль «так же строго»/«наравне с враньём»; «отсутствие
|
||
# живого русского» → «мёртвый русский» (проверка СВОЙСТВА, а не отсутствия — последняя глобальна
|
||
# по построению); строка-мост исправлена (ЯЗЫК и РЕГИСТР — СУЩЕСТВО второго брака, а не «границы»).
|
||
_BRAK = (
|
||
"БРАКУЮТСЯ ДВА ИСХОДА, и второй так же строго, как первый.\n"
|
||
"Первый — перевранный смысл: факт, смысловой ход, канон.\n"
|
||
"Второй — текст, сквозь который просвечивает исходный язык: чужой синтаксис, переведённо\n"
|
||
"звучащая фраза, мёртвый русский. Машинный перевод — брак, даже когда в нём всё верно.\n"
|
||
"Перевод — отчасти творчество: не переврав смысла, напиши так, чтобы звучало по-русски\n"
|
||
"естественно. Вольность не проверяет никто.\n\n"
|
||
"Первому браку служат СМЫСЛ, ПОЛНОТА и КАНОН; второму — ЯЗЫК и РЕГИСТР:")
|
||
CUT = {
|
||
# RB: риск ПЕРЕВЁРНУТ. Хвост заменён не условием, а ПЕРЕОПРЕДЕЛЕНИЕМ: смелость перестаёт быть
|
||
# разрешённым исключением и становится проверяемым требованием.
|
||
"RB": [("ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается "
|
||
"ошибкой —\nдругих запретов нет:", _BRAK),
|
||
(_TAIL_OLD,
|
||
"Живая фраза вместо буквы — не вольность, а исполнение второго требования.")],
|
||
# RC: экзамена нет вовсе. Хвост тоже без условия верификации и без слова «оси», которое в RC
|
||
# повисало бы без референта — выше ни одна вещь осью не названа.
|
||
"RC": [("ПО ЧЕМУ БУДЕТ ПРОВЕРЕН РЕЗУЛЬТАТ. Это же и есть полный список того, что считается "
|
||
"ошибкой —\nдругих запретов нет:", "ЧЕГО НЕЛЬЗЯ:"),
|
||
(_TAIL_OLD,
|
||
"Всё, что за пределами этих пяти, — твоя воля; живая фраза вместо буквы — норма, "
|
||
"а не риск.")],
|
||
}
|
||
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, arm: str = "RN") -> 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)
|
||
for was, now in CUT.get(arm, []):
|
||
if was not in core:
|
||
raise SystemExit(f"⛔ у арма {arm} не найден заменяемый кусок «{was[:40]}…» — "
|
||
"промт изменился под замером, СТОП")
|
||
core = core.replace(was, now)
|
||
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
|
||
|
||
|
||
# ── ГЕЙТ ПОКРЫТИЯ БОЕВЫХ ПРОМТОВ ─────────────────────────────────────────────────────────────
|
||
# ⚠⚠ ЗАВЕДЁН ПО ЗАМЕЧАНИЮ ВЛАДЕЛЬЦА 20.08 («там же довольно много пропущено?»), И ЗАМЕЧАНИЕ БЫЛО
|
||
# ВЕРНЫМ. Первая редакция нового промта молча потеряла три правила боевых промтов, и одно из них
|
||
# потеряла ОПАСНО: «повтор в параллельных позициях исходника — авторский рефрен, сохраняй его» —
|
||
# при том что новый промт прямо говорит «буквальность не достоинство», то есть толкает в обратную
|
||
# сторону. Ещё два: «кальки жестов и идиом · неверно понятые реалии» и «не пересочиняй сцены».
|
||
#
|
||
# Обещание «я всё перенёс» проверяемо только механизмом, поэтому вот он. Каждое ПРАВИЛО боевых
|
||
# промтов перечислено ниже характерной подстрокой, и у каждого объявлена ДИСПОЗИЦИЯ:
|
||
# "промт" — правило стоит в ядре нового промта (проверяется вхождением ключевых слов);
|
||
# "блок" — правило приезжает пар-блоком дословно (проверяется вхождением самой строки);
|
||
# "снято" — правило снято СОЗНАТЕЛЬНО, с основанием прямо здесь.
|
||
# Селфтест роняет замер, если хоть одна строка боевого промта не покрыта ни одной диспозицией:
|
||
# правка боевого промта чужой зоной сломает мой гейт ГРОМКО, а не тихо изменит смысл арма.
|
||
COVERAGE: tuple[tuple[str, str, str, str], ...] = (
|
||
# (файл, характерная подстрока боевого правила, диспозиция, чем закрыто / основание)
|
||
("translator", "Хонорифики:", "промт", "Хонорифики"),
|
||
("translator", "Баланс форенизация", "промт", "форенизация/доместикация"),
|
||
("translator", "Сноски:", "промт", "Сноски"),
|
||
("translator", "ничего не пропускай", "промт", "выбрасывать и досочинять нельзя"),
|
||
("translator", "не добавляй от себя", "промт", "появилось событие, которого там не было"),
|
||
# ⚠ «смысл» — правило покрыто ПО СОДЕРЖАНИЮ другим, более полным местом промта, но не
|
||
# дословно. Диспозиция заведена отдельно от «блок» намеренно: смешивать «перенесено буквой» и
|
||
# «покрыто смыслом» значит потерять различие, на котором и погорела первая редакция.
|
||
("translator", "НЕ копируй построчную разбивку", "смысл",
|
||
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
|
||
("translator", "избегай канцелярита", "промт", "канцеляр"),
|
||
("translator", "Выведи ТОЛЬКО перевод", "промт", "Выведи ТОЛЬКО перевод"),
|
||
("editor", "искажения смысла", "промт", "Перевран факт"),
|
||
("editor", "неверно понятые реалии", "промт", "реалию сперва пойми"),
|
||
("editor", "кальки жестов и идиом", "промт", "подстрочник врёт"),
|
||
("editor", "а не подстрочником", "промт", "подстрочник врёт"),
|
||
# ⚠ СЕРТИФИЦИРУЮЩЕЕ СЛОВО БЫЛО ВЗЯТО ИЗ ПРАВИЛА ПРОТИВОПОЛОЖНОГО СМЫСЛА. Строка
|
||
# «убирай лишние повторы» покрывалась словом «разнообразь», которое стоит в
|
||
# рефрен-границе — то есть там, где повторы убирать ЗАПРЕЩЕНО. Гейт печатал
|
||
# «покрыто», а требования убирать немотивированный повтор в промте не было вовсе.
|
||
# Поймано ревью Fable-5, воспроизведено исполнением. Сертификат пере-привязан.
|
||
("editor", "лишние повторы", "промт", "немотивированные повторы убирай"),
|
||
("editor", "авторский рефрен", "промт", "авторский рефрен"),
|
||
("editor", "СТРОГО по приведённому глоссарию", "промт", "КАНОН"),
|
||
("editor", "склоняя по контексту", "промт", "склоняя по контексту"),
|
||
("editor", "не придумывай иных вариантов", "промт", "вариантов не придумывай"),
|
||
("editor", "естественные русские абзацы", "смысл",
|
||
"Один повествовательный ход оформляй ОДНИМ русским абзацем"),
|
||
("editor", "Реплики прямой речи", "смысл",
|
||
"Каждую новую реплику начинай с нового абзаца через тире"),
|
||
("editor", "Сохраняй ПОЛНОТУ", "промт", "Пропал смысловой ход"),
|
||
("editor", "не пересочиняй сцены", "промт", "сцены не пересочиняй"),
|
||
# ⚠ Строк-ЗАГОЛОВКОВ обоих блоков в таблице нет намеренно: они не правила и не
|
||
# сканируются, а как строки таблицы были мёртвыми — то есть создавали видимость
|
||
# покрытия. Присутствие самих блоков сторожат отдельные проверки селфтеста.
|
||
# строки, которые приезжают ПАР-БЛОКАМИ дословно (их тело — данные пары, не наше)
|
||
("translator", "МЕРЫ", "блок", "Единицы и приёмы"),
|
||
("translator", "СТИХИ и названия классики", "блок", "Единицы и приёмы"),
|
||
("translator", "Род и форму имён", "блок", "Единицы и приёмы"),
|
||
("translator", "РАМОЧНЫЙ РАССКАЗ", "блок", "Единицы и приёмы"),
|
||
("translator", "ФРАНЦУЗСКИЙ СЛОЙ", "блок", "Единицы и приёмы"),
|
||
("translator", "РЕЧЕВЫЕ ПОРТРЕТЫ", "блок", "Единицы и приёмы"),
|
||
("translator", "КАЛЬКИ en→ru", "блок", "Единицы и приёмы"),
|
||
("translator", "ЗАГЛАВНЫЕ РЕАЛИИ", "блок", "Единицы и приёмы"),
|
||
# у редактора СВОЯ сжатая копия единиц; в промт едет БОЛЕЕ ПОЛНАЯ версия переводчика
|
||
("editor", "Единицы и приёмы", "смысл", "Единицы и приёмы"),
|
||
("editor", "Молча определи смысловые ходы", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
|
||
("editor", "Один повествовательный ход", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
|
||
("editor", "Каждую новую реплику", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
|
||
("editor", "Разрешено объединять и делить", "блок", "ДИСКУРС-ПЕРЕВЁРСТКА"),
|
||
("editor", "Хонорифики:", "промт", "Хонорифики"),
|
||
# ⚠ ЕДИНСТВЕННОЕ СОЗНАТЕЛЬНОЕ СНЯТИЕ, И ОНО ОБЪЯВЛЕНО ЗДЕСЬ, А НЕ УМОЛЧАНО. Боевой промт
|
||
# переводчика требует «не сокращай»; решение владельца 16.08 («вольность не ошибка, живой
|
||
# язык — приоритет») это правило отменяет, и новый промт прямо разрешает «сворачивать
|
||
# описание». Полнота при этом охраняется НЕ запретом сокращать, а запретом терять смысловые
|
||
# ХОДЫ — то есть на уровне содержания, а не числа слов.
|
||
("translator", "не сокращай", "снято", "решение владельца 16.08: вольность не ошибка"),
|
||
# Роль редактора-правщика однопроходке не передаётся по построению: чинить нечего.
|
||
("editor", "Тебе даны ИСХОДНЫЙ текст и его черновой перевод", "снято", "черновика нет"),
|
||
("editor", "Сверяй смысл черновика", "снято", "черновика нет"),
|
||
("editor", "художественная редактура черновика", "снято", "черновика нет"),
|
||
("editor", "Ты — литературный редактор", "снято", "роль задана заново: переводчик"),
|
||
("editor", "Выведи ТОЛЬКО отредактированный", "снято", "своя строка вывода у переводчика"),
|
||
("translator", "Ты — профессиональный литературный переводчик", "снято", "роль задана заново"),
|
||
("translator", "Жанр книги:", "снято", "бриф перенесён своим блоком"),
|
||
("editor", "Жанр книги:", "снято", "бриф перенесён своим блоком"),
|
||
)
|
||
_RULE_MARK = ("- ", "1.", "2.", "3.", "4.", "Ты —", "Жанр", "Твой мандат", "Выведи")
|
||
|
||
|
||
# ⚠ МЁРТВАЯ СТРОКА ТАБЛИЦЫ НЕОТЛИЧИМА ОТ ЖИВОЙ — и это дефект гейта, а не косметика:
|
||
# строка, не срабатывающая ни разу, создаёт видимость покрытия там, где его никто не проверял.
|
||
# Поймано ревью Fable-5. `fired` наполняется при разборе и сверяется селфтестом.
|
||
fired: set = set()
|
||
|
||
|
||
def coverage_report(p: str) -> list[tuple[str, str, str]]:
|
||
"""[(файл, строка боевого промта, вердикт)] — что покрыто, чем и что не покрыто вовсе."""
|
||
cfg = PAIRS[p]
|
||
cfg["wire"]()
|
||
d = PR.PAIR_DIR[cfg["para"]().key]
|
||
u = chosen(p)[0]
|
||
sysall = "\n".join(x["content"] for x in rol_msgs(p, u["source"]) if x["role"] == "system")
|
||
ub, rb = pair_blocks(p)
|
||
out = []
|
||
fired.clear()
|
||
for fname in ("translator", "editor"):
|
||
core, _f, _us = PR.load_template(d / f"{fname}.md")
|
||
for ln in core.splitlines():
|
||
s = ln.strip()
|
||
# ⚠ ПОРОГ ДЛИНЫ БЫЛ СЛЕПЫМ ПЯТНОМ: правило «- Сноски: {{footnotes}}» короче
|
||
# 25 знаков и не сканировалось ВООБЩЕ, то есть могло пропасть незаметно.
|
||
if len(s) < 10 or not s.startswith(_RULE_MARK):
|
||
continue
|
||
hits = [c for c in COVERAGE if c[0] == fname and c[1] in s]
|
||
fired.update(c[:3] for c in hits)
|
||
if not hits:
|
||
out.append((fname, s[:90], "⛔ НЕ ОБЪЯВЛЕНО НИ ОДНОЙ ДИСПОЗИЦИЕЙ"))
|
||
continue
|
||
for _f2, _sub, disp, what in hits:
|
||
if disp == "снято":
|
||
out.append((fname, s[:90], f"снято: {what}"))
|
||
elif disp == "смысл":
|
||
out.append((fname, s[:90], "покрыто смыслом"
|
||
if what in sysall else f"⛔ «{what}» НЕТ в промте"))
|
||
elif disp == "блок":
|
||
# ⚠ Сверяется САМА СТРОКА боевого промта, а не начало блока: слабая проверка
|
||
# («блок непустой») пропустила бы усечённый блок, из которого выпала половина
|
||
# правил. Строка рендерится тем же брифом, иначе `{{venuti}}` не совпадёт.
|
||
line = PR.render(s, getattr(PR, PR.PAIR_BRIEF[cfg["para"]().key]), "", "")
|
||
out.append((fname, s[:90],
|
||
"блок пары ДОСЛОВНО" if line in sysall else "⛔ строка НЕ доехала"))
|
||
else:
|
||
out.append((fname, s[:90],
|
||
"в промте" if what in sysall else f"⛔ «{what}» НЕТ в промте"))
|
||
return out
|
||
|
||
|
||
def src_of(p: str, arm: str, u: dict) -> str:
|
||
"""Исходник, который видит арм. ОДНА точка правды — иначе смета и покупка разъедутся.
|
||
|
||
⚠ Все армы, кроме `RA`, видят ПЛОСКИЙ исходник — тот же самый, что видела база `ZP`. Иначе
|
||
несущий контраст `RN/ZP` смешал бы эффект промта с эффектом подачи.
|
||
"""
|
||
if arm in RESTORE_ARMS and PAIRS[p]["restore"]:
|
||
return PAIRS[p]["restore"](u["source"])
|
||
return u["source"]
|
||
|
||
|
||
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)
|
||
|
||
|
||
# ⛔⛔ БЛОКЕРЫ Б6/Б7 РЕВЬЮ 21.08. Ростер кодирует ОДНИМ режимом `none` («ручку не шлём») две
|
||
# ПРОТИВОПОЛОЖНЫЕ вещи: у `deepseek-v4-pro` вендор-дефолт — размышление HIGH, у `grok-4.3` — LOW
|
||
# (квирк-бюллетень :60/:80). Значит «одинаковая конфигурация по ростеру» означает РАЗНОЕ
|
||
# размышление, и это конфаундер, а не фон. Цена его замерена: у `glm-5` тот же промт на тех же
|
||
# главах даёт 2.54 места с размышлением против 5.15 без — различимо, оба прибора согласны (Д28.4).
|
||
#
|
||
# Что это уже стоило фазе: `RK` шёл на грок-дефолте `low` и думал 737 токенов против 6139 у якоря
|
||
# (×8), а вердикт опубликован как «промт не переносим на grok-4.3» — свойство КОНФИГУРАЦИИ подано
|
||
# свойством ВЕНДОРА. Тем же классом заражены армы `E6`/`J6` (носители H-4): 25 клеток из 25
|
||
# с нулём токенов размышления, потому что наш код гасил `glm-5` явно.
|
||
#
|
||
# ⇒ УРОВЕНЬ РАЗМЫШЛЕНИЯ ОБЪЯВЛЯЕТСЯ ПОАРМНО И ЯВНО. «Включено» словом больше не считается ответом:
|
||
# норма Д28.3 расширена ревью — уровень называется ЗНАЧЕНИЕМ, а не наличием.
|
||
THINK_LEVEL: dict = {
|
||
"RN": None, "RN2": None, "RC": None, "RB": None, "RA": None, # dspro: вендор-дефолт high
|
||
"RG": "off", # glm-5 с НАШИМ подавлением — оставлен намеренно, он и есть контраст
|
||
"RGT": "vendor", # тот же glm-5 без нашего подавления
|
||
"RK": "low", # grok на вендор-дефолте — ровно так и куплен, теперь это НАПИСАНО
|
||
"RKT": "high", # grok с настоящим размышлением — клетка Б6, не куплена
|
||
"RL": "vendor", # gpt-5.6-luna: ростер пинит `low`, снимаем (В22)
|
||
"RE": "vendor", # gemini-3.1-flash-lite: ВЕНДОР-ДЕФОЛТ. ⛔ НЕ «выключено»: у серии Gemini 3
|
||
# размышление вообще невыключаемо (вендор-дока 17.08.2026), а поле
|
||
# `reasoning_tokens` у Gemini ВСЕГДА 0 по построению слоя — считать надо
|
||
# `total − prompt − completion`. Прежний комментарий «замерено, 0 ток.»
|
||
# читал ровно то поле, которое наш же бюллетень объявил всегда нулевым.
|
||
"RET": "budget", # он же с размышлением: у Gemini своя ручка `thinking_budget`, не `effort`
|
||
}
|
||
|
||
|
||
# ⛔⛔ ВЕНДОР-КВИРК, ВСКРЫТ ЖИВОЙ ПРОБОЙ 22.08 (`proba_gemini.py`). Слой OpenAI у Gemini МОЛЧА
|
||
# ВЫБРАСЫВАЕТ системные сообщения, когда их больше одного. Один и тот же системный текст в 7970
|
||
# знаков: ОДНИМ сообщением → 2994 входных токена и инструкция исполнена; ДВУМЯ → **535 токенов и
|
||
# инструкция НЕ исполнена**. Ответ при этом приходит с `finish=stop` и выглядит правильным
|
||
# переводом — то есть отказа нет, есть тихая потеря промта.
|
||
# Наш арм шлёт ДВА системных (ядро роли + банк-закон), поэтому первые 16 клеток `RE` переведены
|
||
# ВООБЩЕ БЕЗ ИНСТРУКЦИИ. Вердикт «gemini не годится» из них был бы ложным — четвёртый случай того
|
||
# же класса за два дня (склейка · glm-5 · grok · этот).
|
||
# Лечение узкое и по вендору, а не общее: склеивать системные в ОДНО только там, где вендор иначе
|
||
# их теряет. Общая склейка сменила бы форму запроса у ВСЕХ моделей и рассогласовала бы уже
|
||
# купленные клетки с будущими.
|
||
ONE_SYSTEM = {"gemini-3.1-flash-lite", "gemini-3.6-flash", "gemini-3.1-pro-preview"}
|
||
|
||
|
||
def fit_msgs(model: str, msgs: list[dict]) -> list[dict]:
|
||
"""Приводит форму сообщений к тому, что вендор реально принимает (см. `ONE_SYSTEM`)."""
|
||
if model not in ONE_SYSTEM:
|
||
return msgs
|
||
sysm = [x["content"] for x in msgs if x["role"] == "system"]
|
||
if len(sysm) < 2:
|
||
return msgs
|
||
return ([{"role": "system", "content": "\n\n".join(sysm)}]
|
||
+ [x for x in msgs if x["role"] != "system"])
|
||
|
||
|
||
def call_kwargs(model: str, msgs: list[dict], arm: str = "") -> dict:
|
||
"""Тело вызова с ЯВНО объявленным уровнем размышления арма (см. `THINK_LEVEL`).
|
||
|
||
`None` — как отдаёт ростер · `off` — наше подавление остаётся · `vendor` — подавление снимается
|
||
и едет вендор-дефолт · `low`/`medium`/`high` — уровень задаётся явно, потому что вендор-дефолт
|
||
у этой модели не тот, который нужен арму.
|
||
"""
|
||
kw = ROSTER.call_kwargs(model, fit_msgs(model, msgs), max_out=MAX_OUT)
|
||
lvl = THINK_LEVEL.get(arm)
|
||
if lvl in (None, "off"):
|
||
return kw
|
||
eb = dict(kw.get("extra_body") or {})
|
||
if lvl == "budget":
|
||
# ⚠ У Gemini уровень задаётся БЮДЖЕТОМ, а не словом: `thinking_budget` в google-секции
|
||
# (квирк-бюллетень :54). `-1` — динамический бюджет, то есть «думай сколько нужно».
|
||
# ⛔ ПОПРАВКА 22.08 ПОСЛЕ СВЕРКИ С ВЕНДОР-ДОКОЙ. Прежний комментарий здесь утверждал, что
|
||
# вендор ОТВЕРГ google-секцию. Неверно: секция работает, а 400 «Unknown name "google"» был
|
||
# НАШЕЙ ошибкой вложенности — Python-SDK раскладывает `extra_body` в верхний уровень тела,
|
||
# поэтому вендорская форма требует ДВОЙНОГО ключа: `extra_body={'extra_body': {'google': …}}`.
|
||
# Рабочая форма лежала в репозитории (`eval/adaptive_probe.py:71`), но была объявлена багом
|
||
# в `eval/providers.json:59`, и ошибка размножилась оттуда.
|
||
# ⇒ Здесь оставлена ПЛОСКАЯ `reasoning_effort` — она документирована вендором для 3.x
|
||
# (маппится в `thinking_level`) и проверена живой пробой: 4068 токенов размышления в
|
||
# `total_tokens`. Слать её ВМЕСТЕ с `thinking_level`/`thinking_budget` вендор запрещает.
|
||
# ⚠ `high` — ВЕРХНЯЯ ступень. Для роли переводчика аналог того, что мы шлём другим вендорам,
|
||
# это `low`/`minimal`; контраст `RE`/`RET` читать как «вендор-дефолт против high», НЕ как
|
||
# «выключено против включено».
|
||
eb["reasoning_effort"] = "high"
|
||
elif lvl == "vendor":
|
||
# снимаем ЛЮБОЕ наше подавление: у Z.ai это `thinking.disabled`, у OpenAI — `effort: low`
|
||
eb.pop("thinking", None)
|
||
eb.pop("reasoning_effort", None)
|
||
else:
|
||
eb["reasoning_effort"] = lvl
|
||
if eb:
|
||
kw["extra_body"] = eb
|
||
else:
|
||
kw.pop("extra_body", None)
|
||
return kw
|
||
|
||
|
||
# ── ПОКУПКА ──────────────────────────────────────────────────────────────────────────────────
|
||
# ⚠ АБЛЯЦИИ НЕ НУЖНА ВСЯ ПАНЕЛЬ. Эффект, который ищется, огромен (×28 размышления), и виден он
|
||
# на трёх главах; тратить на него шестнадцать значит выбрать между двумя вариантами вместо того,
|
||
# чтобы купить оба. Ограничитель берёт ПЕРВЫЕ N глав отбора — тот же детерминированный порядок,
|
||
# значит абляция сравнима с уже купленным `RN` глава в главу.
|
||
N_LIMIT = next((int(a.split("=", 1)[1]) for a in sys.argv if a.startswith("--n=")), 0)
|
||
|
||
|
||
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)[:N_LIMIT] if N_LIMIT else 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]
|
||
if arm in RESTORE_ARMS and not cfg["restore"]:
|
||
print(f" {arm} на паре {p} невозможен: исходник абзацев не терял — пропуск")
|
||
continue
|
||
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, src_of(p, arm, u), arm), arm),
|
||
role="onepass", uid=u["uid"], arm=arm, gen=gen, pair=p,
|
||
podacha="абзацы" if arm in RESTORE_ARMS else "плоско")
|
||
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():
|
||
if arm in RESTORE_ARMS and not cfg["restore"]:
|
||
print(f" {arm:4s} {'—':18s} на этой паре невозможен (исходник абзацев не терял)")
|
||
continue
|
||
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"]))
|
||
# ⚠ ГЕЙТ ПОКРЫТИЯ — главная проверка этого файла. Обещание «я перенёс всё из боевых
|
||
# промтов» проверяемо только механизмом; замечание владельца 20.08 показало, что на слово
|
||
# его брать нельзя.
|
||
rep = coverage_report(p)
|
||
miss = [r for r in rep if r[2].startswith("⛔")]
|
||
ck(f"{p}: КАЖДОЕ правило боевых промтов имеет диспозицию и она исполнена",
|
||
not miss, f"не покрыто {len(miss)} из {len(rep)}: " + " | ".join(r[1][:50] for r in miss[:3]))
|
||
# ⚠ И ОБРАТНАЯ СТОРОНА: строка таблицы, не сработавшая НИ РАЗУ, создаёт видимость покрытия
|
||
# там, где никто ничего не проверял. Считается по ОБЕИМ парам сразу: часть правил живёт
|
||
# только у английской (французский слой, рамочный рассказ).
|
||
seen: set = set()
|
||
for p in PAIRS:
|
||
coverage_report(p)
|
||
seen |= fired
|
||
dead = [c for c in COVERAGE if c[:3] not in seen]
|
||
ck("в таблице COVERAGE нет МЁРТВЫХ строк (каждая срабатывает хотя бы раз)",
|
||
not dead, " | ".join(f"{c[0]}:«{c[1]}»" for c in dead[:4]))
|
||
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}")
|
||
# ── ГЕЙТЫ ПОЧИНОК РЕВЬЮ 21.08. Каждый обязан УМЕТЬ УПАСТЬ на своём дефекте ────────────────
|
||
# ⚠ Заведены после того, как пять дефектов класса «реализация не то, чем названа» прошли мимо
|
||
# всех существовавших гейтов. Здесь проверяется не «код изменён», а «поведение верное».
|
||
MARK = {"zh": ("时辰", "чэнъюй", "zh→ru"), "en": ("МЕРЫ английской традиции",),
|
||
"ja": ("ja→ru",)}
|
||
for p, cfg in PAIRS.items():
|
||
cfg["wire"]()
|
||
src = chosen(p)[0]["source"]
|
||
sysm = "\n\n".join(x["content"] for x in C.a2_msgs(src) if x["role"] == "system")
|
||
alien = [w for q, ws in MARK.items() if q != p for w in ws if w in sysm]
|
||
# Б1: мандат склейки берётся у СВОЕЙ пары. Падал бы на прежнем коде: на `en` в промт
|
||
# уезжали 时辰/чэнъюй/«китайский паратаксис» из боевого редактора китайской пары.
|
||
ck(f"Б1 {p}: склейка НЕ несёт маркеров чужой пары", not alien, f"нашлось: {alien}")
|
||
ck(f"Б1 {p}: склейка несёт маркеры СВОЕЙ пары",
|
||
any(w in sysm for w in MARK[p]), f"ни одного из {MARK[p]}")
|
||
|
||
# Б8: отбор глав НЕ ЗАВИСИТ от гейта годности. Первая редакция починки гасила текст в
|
||
# `base_a0`, единица выпадала из `pool()`, и `chosen()` подставлял на её место другую —
|
||
# «те же 16 глав» переставали быть теми же. Сверяем с главами УЖЕ СОБРАННЫХ панелей.
|
||
for p, tag_ in (("en", "arch2"), ("zh", "zhpanel")):
|
||
PAIRS[p]["wire"]()
|
||
now = {u["uid"][:8] for u in chosen(p)}
|
||
was = {k.stem.split(f"{tag_}-")[1]
|
||
for k in (OUT / "blind-keys-rol").glob(f"rol-KEY-{tag_}-*.json")}
|
||
ck(f"Б8 {p}: отбор глав не поехал после починок", was <= now,
|
||
f"пропали из отбора: {sorted(was - now)}")
|
||
|
||
# Б8: гейт годности `A0` ловит известную больную единицу и молчит на здоровых.
|
||
PAIRS["zh"]["wire"]()
|
||
sick = [u["uid"] for u in chosen("zh") if not C.a0_ok(u["uid"])]
|
||
ck("Б8: гейт A0 ловит оборванную клетку", sick == ["41599f30df"], f"поймал: {sick}")
|
||
|
||
# ⚠ ЛАТЕНТНАЯ МИНА, НАЙДЕНА 22.08 ПРИ ФАЛЬСИФИКАЦИИ ГЕЙТА Б8. В одном процессе живут НЕСКОЛЬКО
|
||
# независимых экземпляров `contour` (`rol` держит свой, `zakhod` — свой через `en_axis`), и
|
||
# `wire()` настраивает НЕ ВСЕ: после включения английской пары один экземпляр остаётся на
|
||
# китайской. Проверено, что до данных это НЕ добралось — горячие функции получают пару
|
||
# АРГУМЕНТОМ (`_pair(p)["para"]().key`), а не из глобального `pair()`, и настоящий путь покупки
|
||
# грузит верный банк (английские клетки `Z7` собраны по английскому банку — сверено).
|
||
# Поэтому здесь ПРЕДУПРЕЖДЕНИЕ, а не провал: ронять покупку из-за мины, которая не выстрелила,
|
||
# значит остановить работу по подозрению. Но любая НОВАЯ функция, читающая глобальный `pair()`,
|
||
# обязана быть проверена на этом месте — иначе арм соберётся на невключённой паре.
|
||
_inst = {id(C): ("rol.C", C), id(ZK.C): ("zakhod.C", ZK.C)}
|
||
if len(_inst) > 1:
|
||
PAIRS["en"]["wire"]()
|
||
_st = {n: c.pair().key for n, c in _inst.values()}
|
||
if len(set(_st.values())) > 1:
|
||
print(f"[⚠ МИНА] экземпляров contour {len(_inst)}, после wire(en) пары разошлись: {_st}"
|
||
" — глобальный `pair()` использовать НЕЛЬЗЯ, только пара аргументом")
|
||
|
||
# Б6/Б7: уровень размышления объявлен для КАЖДОГО арма и уходит на провод именно он.
|
||
ck("Б6: уровень размышления объявлен для всех армов",
|
||
set(ARMS) <= set(THINK_LEVEL), f"без объявления: {sorted(set(ARMS) - set(THINK_LEVEL))}")
|
||
for a, (mo, _g) in ARMS.items():
|
||
eb = call_kwargs(mo, [{"role": "user", "content": "x"}], a).get("extra_body") or {}
|
||
lvl = THINK_LEVEL.get(a)
|
||
if lvl in ("low", "medium", "high"):
|
||
ok, d = eb.get("reasoning_effort") == lvl, f"на проводе {eb}"
|
||
elif lvl == "vendor": # никакого НАШЕГО подавления на проводе быть не должно
|
||
ok, d = not ({"thinking", "reasoning_effort"} & set(eb)), f"на проводе {eb}"
|
||
else:
|
||
ok, d = True, ""
|
||
ck(f"Б6 {a}: на провод уходит объявленный уровень «{lvl}»", ok, d)
|
||
print(f"\n{'ЗАМЕР ГОДЕН К ПОКУПКЕ' if not bad else f'ПРОВАЛОВ: {bad}'}")
|
||
return bad
|
||
|
||
|
||
# ── СЛЕПОЕ ЧТЕНИЕ ПАНЕЛИ ЭТОГО ЗАМЕРА ────────────────────────────────────────────────────────
|
||
# ⚠ ПЕРВИЧНЫЙ ПРИБОР КОНТРАСТА — ЧТЕНИЕ, А НЕ СЧЁТ (решение владельца 20.08). Основание замерено:
|
||
# счётчик ошибок согласен с владельцем на китайской паре (+0.80) и АНТИ-согласен на английской
|
||
# (−0.80), а слепое чтение — +0.80 на обеих. Всё, что новый промт меняет (вольность, регистр),
|
||
# лежит ровно в осях, к которым счётчик слеп, поэтому судить им этот контраст значит гарантированно
|
||
# получить «неотличимо» и не понять, эффекта нет или прибора нет.
|
||
# ⚠ РАМКА ПАКЕТА НЕЙТРАЛЬНА. Норма, заведённая 20.08 после того, как я нашёл подсказку в СВОЁМ же
|
||
# пакете («вариант с наименьшим числом ошибок читался почти хуже всех»): пакет не имеет права
|
||
# называть читателю ни вердикт другого прибора, ни существование контроля. Замер показал, что
|
||
# подсказка стоила 0.20 Спирмена — верх и низ порядка устояли, но средние места переставились.
|
||
# ⚠ Перетасовка меток берётся у `chtenie.layout` — второй реализации того же не заводится.
|
||
CH = _load("chtenie_r", ZONE / "chtenie.py")
|
||
READ_SALT = "rol-2026-08-20"
|
||
READ_PANEL = ("ZP", "RN", "Z0", "ZF") # склейка · новая роль · боевая связка · её 2-я генерация
|
||
READ_DIR = Path.home() / "books" / "chtenie-rol"
|
||
READ_KEY = OUT / "blind-keys-rol" / "rol-KEY.json"
|
||
READ_HEAD = """
|
||
Ниже {n} переводов одного и того же отрывка, сделанных разными способами. Какой чем сделан — не
|
||
сказано намеренно, метки перемешаны, расшифровка лежит отдельно и до твоего ответа не открывается.
|
||
|
||
Исходник приложен. Оцени варианты по ТРЁМ вещам, в этом порядке важности:
|
||
|
||
1. **СМЫСЛ** — не переврано ли то, что сказано в исходнике: участник действия, отрицание, число,
|
||
ранг, родство, кто кому что говорит; не пропало ли и не добавилось ли событие.
|
||
2. **БРАК** — явные дефекты русского текста: незаконченная фраза, сбитая атрибуция реплики,
|
||
рассогласование, оставшийся кусок исходного языка, глосса в скобках посреди прозы, имя,
|
||
меняющее форму или род.
|
||
3. **ХУДОЖЕСТВЕННОСТЬ** — читается ли это как русская проза или как перевод: живой ритм и голос
|
||
против кальки, канцелярита и подстрочника.
|
||
|
||
## Как читать — это важнее скорости
|
||
|
||
Времени у тебя нет ограничения, и торопиться не надо. Прочти ВСЕ варианты целиком, прежде чем
|
||
выстраивать порядок: решение по первым двум, к которому потом подгоняются остальные, — самая
|
||
частая порча такого чтения.
|
||
|
||
Не выдумывай различий там, где их нет: если два варианта действительно неразличимы, соединяй их
|
||
знаком `=`, это законный ответ. И наоборот — не сваливай в кучу то, что различимо: если разница
|
||
мелкая, так и скажи, что мелкая, но порядок назови.
|
||
|
||
Думай столько, сколько нужно. Ответ на один абзац, выданный быстро, стоит меньше, чем разбор,
|
||
в котором видно, ЧЕМ один текст лучше другого.
|
||
|
||
## Что записать — в файл `{answer}`
|
||
|
||
1. **ПОРЯДОК** — строкой: `Т3 > Т1 > Т2 > Т4`, от лучшего к худшему. Неразличимые соединяй `=`.
|
||
Это ответ на вопрос «какой ты взял бы в книгу»: смысл и брак — пороги, художественность решает
|
||
между теми, кто пороги прошёл.
|
||
2. **По каждому варианту** — по фразе на каждую из трёх осей, если есть что сказать.
|
||
3. Если вариант читается откровенно машинным — скажи прямо.
|
||
|
||
---
|
||
"""
|
||
|
||
|
||
def read_texts(p: str, uid: str) -> dict:
|
||
"""Тексты панели чтения. Пусто у любого арма — глава в пакет не идёт целиком."""
|
||
out = {"_src": next(u["source"] for u in chosen(p) if u["uid"] == uid)}
|
||
for a in READ_PANEL:
|
||
t = free_text(p, a, uid) if a in FREE else (
|
||
cell(tag(p, a, uid)) if a in ARMS else ZK.cell(ZK.tag(p, a, uid)))
|
||
out[a] = t
|
||
return out
|
||
|
||
|
||
def emit_read(p: str, n_units: int = 3, panel: tuple = (), tag_: str = "",
|
||
only_uid: str = "") -> None:
|
||
"""Пакет слепого чтения: ключ пишется ДО ответов, раскладка ОДНА на пару.
|
||
|
||
⚠ ПАНЕЛЬ И ИМЯ ПАКЕТА — ПАРАМЕТРЫ, И ЭТО НЕ УДОБСТВО. Слепое чтение стало ШТАТНЫМ прибором
|
||
(решение владельца 20.08: «теперь судим так всё время»), значит панелей будет много. Ключ
|
||
каждой лежит в СВОЁМ файле: пере-эмиссия с другим составом армов переписала бы раскладку уже
|
||
прочитанной панели, и ответ читателя стал бы мусором молча — класс, уже стоивший фазе вердикта.
|
||
"""
|
||
global READ_PANEL, READ_KEY # noqa: PLW0603
|
||
if panel:
|
||
READ_PANEL = tuple(panel)
|
||
if tag_:
|
||
READ_KEY = OUT / "blind-keys-rol" / f"rol-KEY-{tag_}.json"
|
||
cfg = PAIRS[p]
|
||
cfg["wire"]()
|
||
full = []
|
||
for u in chosen(p):
|
||
# ⛔ Б8: единица с ОБОРВАННОЙ клеткой `A0`/`Z0` в панель не идёт. Гейт стоит ЗДЕСЬ, а не в
|
||
# источнике текста: гашение текста в `base_a0` выбрасывает единицу из `pool()`, и `chosen()`
|
||
# молча подставляет на её место другую — «те же 16 глав» перестают быть теми же.
|
||
if "Z0" in READ_PANEL and not C.a0_ok(u["uid"]):
|
||
print(f" ⚠ {u['uid'][:10]}: клетка Z0 оборвана — глава в панель НЕ идёт")
|
||
continue
|
||
tx = read_texts(p, u["uid"])
|
||
if all(tx[a].strip() for a in READ_PANEL):
|
||
full.append((u["uid"], tx))
|
||
if only_uid:
|
||
full = [x for x in full if x[0] == only_uid]
|
||
full.sort(key=lambda x: -sum(len(x[1][a]) for a in READ_PANEL))
|
||
take = full[:n_units]
|
||
if not take:
|
||
raise SystemExit(f"⛔ у пары {p} нет ни одной главы с ПОЛНОЙ панелью {READ_PANEL}")
|
||
READ_DIR.mkdir(parents=True, exist_ok=True)
|
||
READ_KEY.parent.mkdir(parents=True, exist_ok=True)
|
||
key = json.loads(READ_KEY.read_text(encoding="utf-8")) if READ_KEY.exists() else {
|
||
"_что": "слепое чтение панели замера «однопроходка как роль»",
|
||
"_соль": READ_SALT, "_панель": list(READ_PANEL),
|
||
"_контроль": "Z0 и ZF — две генерации ОДНОЙ боевой связки"}
|
||
ans = READ_DIR / f"ОТВЕТ-{p}{('-' + tag_) if tag_ else ''}.md"
|
||
lay = CH.layout(p, f"ROL-ALL|{READ_SALT}|{tag_}", "READER", READ_PANEL)
|
||
body = [f"# {'КИТАЙСКАЯ' if p == 'zh' else 'АНГЛИЙСКАЯ'} ПАРА — слепое чтение\n",
|
||
READ_HEAD.format(n=len(READ_PANEL), answer=ans)]
|
||
for j, (uid, tx) in enumerate(take, 1):
|
||
body += [f"\n# ОТРЫВОК {j}\n",
|
||
f"## Исходник ({'китайский' if p == 'zh' else 'английский'})\n", tx["_src"], ""]
|
||
for lab, arm in lay.items():
|
||
body += ["\n---\n", f"## Отрывок {j}, вариант {lab}\n", tx[arm], ""]
|
||
key.setdefault(p, {})[str(j)] = {"uid": uid, "метки": lay,
|
||
"знаков": {a: len(tx[a]) for a in READ_PANEL}}
|
||
(READ_DIR / f"ЧТЕНИЕ-{p}{('-' + tag_) if tag_ else ''}.md").write_text(
|
||
"\n".join(body), encoding="utf-8")
|
||
if not ans.exists():
|
||
ans.write_text(f"# Ответ — пара {p}\n\nПОРЯДОК: \n\nПо вариантам:\n", encoding="utf-8")
|
||
READ_KEY.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
print(f" {p}: глав {len(take)} · вариантов {len(READ_PANEL)} · "
|
||
f"{sum(len(tx[a]) for _u, tx in take for a in READ_PANEL) // 1000} тыс. знаков")
|
||
print(f" пакет → {READ_DIR}/ЧТЕНИЕ-{p}{('-' + tag_) if tag_ else ''}.md · "
|
||
f"ключ → {READ_KEY.name} (читателю НЕ давать)")
|
||
|
||
|
||
def score_read() -> None:
|
||
if not READ_KEY.exists():
|
||
raise SystemExit("⛔ ключа нет — сначала --emit-read")
|
||
key = json.loads(READ_KEY.read_text(encoding="utf-8"))
|
||
import re as _re # noqa: PLC0415
|
||
for p in PAIRS:
|
||
f = READ_DIR / f"ОТВЕТ-{p}.md"
|
||
if not f.exists() or p not in key:
|
||
continue
|
||
txt = f.read_text(encoding="utf-8", errors="replace")
|
||
seen: list[str] = []
|
||
for x in _re.findall(r"Т\d+", txt):
|
||
if x not in seen:
|
||
seen.append(x)
|
||
lay = key[p]["1"]["метки"]
|
||
order = [lay[x] for x in seen if x in lay]
|
||
if len(order) < len(READ_PANEL):
|
||
print(f" {p}: порядок не разобран ({seen})")
|
||
continue
|
||
print(f" {p}: {' > '.join(order)}")
|
||
print(f" контроль шума Z0/ZF на местах {order.index('Z0') + 1} и {order.index('ZF') + 1}")
|
||
|
||
|
||
# ── СВОД ПАНЕЛИ «ОДНА ГЛАВА — ОДИН ПАКЕТ» ────────────────────────────────────────────────────
|
||
# ⚠ `score_read` выше разбирает ОДИН пакет и один ключ; здесь пакетов 15, ключей 15 и раскладок 15.
|
||
# Свод обязан (а) отказываться от главы, чей порядок не покрывает панель ровно один раз, вслух, и
|
||
# (б) печатать контроли рядом со средним местом, а не под ним: без них порядок ничего не значит.
|
||
# ⚠ РАЗБОР И РАНГИ БЕРУТСЯ У `chtenie` — второй реализации того же не заводится: тамошний `_ranks`
|
||
# уже несёт правило средних мест для связок, а `orders` — две починки, купленные потерянным ответом
|
||
# владельца. Здесь своё ТОЛЬКО правило ВЫБОРА цепочки: `orders` берёт первую и нумерует остальные
|
||
# как разные отрывки, а в пакете «одна глава — один пакет» отрывок один, зато читатель называет
|
||
# порядок и в заголовке, и внутри разбора («внутри семьи Т8 > Т2 > Т7»). Берём САМУЮ ПОЛНУЮ.
|
||
def _order_of(txt: str, labels: set) -> str:
|
||
best, best_n = "", 0
|
||
for m in CH._ORD.findall(txt): # noqa: SLF001
|
||
n = len({x for x in re.findall(r"[ТT]\d+", m) if x.replace("T", "Т") in labels})
|
||
if n > best_n:
|
||
best, best_n = m, n
|
||
return best
|
||
|
||
|
||
def _variants(pack: Path) -> dict:
|
||
"""Тексты пакета по меткам — нужны своду, чтобы контроль тождества стоял на побайтном
|
||
сравнении, а не на равенстве длин.
|
||
|
||
⚠ Разделитель `---`, стоящий ПЕРЕД следующим вариантом, обязан отрезаться: он прилипает к
|
||
хвосту предыдущего тела и переживает `strip()` (6 знаков). Прежняя редакция его оставляла, и
|
||
это было невидимо ровно потому, что сравнивались два ТЕЛА — артефакт стоял у обоих. Сверка с
|
||
клеткой поймала сразу: 84 «расхождения» из 96, все ровно на 6 знаков.
|
||
"""
|
||
txt = pack.read_text(encoding="utf-8")
|
||
parts = re.split(r"(?m)^## Отрывок \d+, вариант (Т\d+)\s*$", txt)
|
||
return {lab: re.sub(r"(?m)\n-{3,}\s*$", "", body.strip()).strip()
|
||
for lab, body in zip(parts[1::2], parts[2::2])}
|
||
|
||
|
||
def wire_dump(p: str, only: str = "") -> int:
|
||
"""ГЕЙТ «ПРОМТ НА ПРОВОД»: печатает ПОЛНЫЙ текст того, что уходит в модель, по каждому арму.
|
||
|
||
⛔⛔ Заведён планом 21.08 §3 после того, как за один день нашлось ПЯТЬ дефектов класса
|
||
«реализация не то, чем названа», и НИ ОДНОГО не поймал ни один гейт:
|
||
· склейка несла мета-фразу «отдельного редактора после тебя НЕ БУДЕТ» — прямым текстом;
|
||
· она же тащила на английскую пару КИТАЙСКИЙ мандат с 时辰 и чэнъюй (блокер Б1);
|
||
· `glm-5` шёл с молча погашенным размышлением, `grok` — на вендор-дефолте `low` (Б6/Б7);
|
||
· опубликованное отношение объёмов промта было снято с ДРУГОЙ редакции файла (Б5).
|
||
Ни хеш, ни длина, ни «расхождение объявлено» этого не видят. **Видит человек, читающий текст.**
|
||
Поэтому гейт не даёт вердикта: он ПОКАЗЫВАЕТ. Обязателен перед каждой покупкой, и прочесть его
|
||
обязаны двое — сессия вслух в отчёте и приёмка другим модельным семейством.
|
||
"""
|
||
PAIRS[p]["wire"]()
|
||
u = chosen(p)[0]
|
||
src, d = u["source"], free_text(p, "ZD", u["uid"])
|
||
cases = [("ZD боевой черновик", ZK.PR.translator_msgs(src, None, pair=f"{p}-ru"), ARMS_MODEL["ZD"]),
|
||
("Z0/ZF боевой редактор", ZK.editor_msgs(p, src, d), ARMS_MODEL["Z0"]),
|
||
("Z8 обогащённый черновик", ZK.z8_msgs(p, src), ARMS_MODEL["ZD"]),
|
||
("Z1 редактор + список критика", ZK.z1_msgs(p, src, d, ["- «х» → калька"]), ARMS_MODEL["Z0"]),
|
||
("критик", C.crit_msgs(src, d), ARMS_MODEL["Z0"]),
|
||
("Z7 канон-фиксер", C.fix_msgs(src, d, ["КАНОН: термин X"]), ARMS_MODEL["Z0"]),
|
||
("ZP однопроходка-склейка", C.a2_msgs(src), ARMS_MODEL["Z0"])]
|
||
cases += [(f"{a} однопроходка-роль", rol_msgs(p, src, a), ARMS[a][0]) for a in ARMS]
|
||
base = None
|
||
for name, msgs, model in cases:
|
||
arm = name.split()[0]
|
||
if only and only != arm:
|
||
continue
|
||
sysm = "\n\n".join(x["content"] for x in msgs if x["role"] == "system")
|
||
base = base or len(sysm)
|
||
kw = call_kwargs(model, msgs, arm)
|
||
print("\n" + "=" * 100)
|
||
print(f"АРМ {name} · модель {model} · размышление: {THINK_LEVEL.get(arm, 'как у ростера')}")
|
||
print(f"инструкции {len(sysm)} знаков (×{len(sysm) / base:.2f} к первому арму) · "
|
||
f"сообщений {len(msgs)}")
|
||
print(f"НА ПРОВОД: {json.dumps({k: v for k, v in kw.items() if k != 'messages'}, ensure_ascii=False)}")
|
||
print("-" * 100)
|
||
print(sysm)
|
||
print("-" * 100)
|
||
print("USER:", "\n\n".join(x["content"] for x in msgs if x["role"] == "user")[:400], "…")
|
||
return 0
|
||
|
||
|
||
ARMS_MODEL = {"ZD": "deepseek-v4-flash", "Z0": "deepseek-v4-pro"}
|
||
|
||
|
||
def verify_read(tag_: str, p: str) -> int:
|
||
"""Сплошная сверка пакетов ПЕРЕД чтением. ⚠ Пункт «тексты РАЗЛИЧНЫ» заведён 21.08 после того,
|
||
как читатели нашли то, чего не видела моя же «сплошная» сверка: `Z7` был побайтной копией `ZP`
|
||
на 10 главах из 15 (Д23.6). Прежняя сверка сличала пакет с клетками, а клетки друг с другом —
|
||
нет, и панель из 11 армов показывала 10 текстов."""
|
||
global READ_PANEL # noqa: PLW0603
|
||
keys = sorted((OUT / "blind-keys-rol").glob(f"rol-KEY-{tag_}-*.json"))
|
||
bad, lays, dupes, ntx = [], set(), [], 0
|
||
PAIRS[p]["wire"]()
|
||
by_uid = {u["uid"][:8]: u["uid"] for u in chosen(p)}
|
||
for kf in keys:
|
||
uid8 = kf.stem.split(f"{tag_}-")[1]
|
||
key = json.loads(kf.read_text(encoding="utf-8"))
|
||
if p not in key:
|
||
continue
|
||
lay, pack = key[p]["1"]["метки"], READ_DIR / f"ЧТЕНИЕ-{p}-{tag_}-{uid8}.md"
|
||
# ⚠ ГЛАВНАЯ проверка: текст под меткой — ТОТ ЖЕ, что в клетке своего арма, побайтно.
|
||
READ_PANEL = tuple(key[p]["1"]["знаков"])
|
||
cells = read_texts(p, by_uid[uid8])
|
||
for lab, arm in lay.items():
|
||
ntx += 1
|
||
if _variants(pack).get(lab, "") != cells[arm].strip():
|
||
bad.append(f"{uid8}: текст метки {lab} не равен клетке арма {arm}")
|
||
if cells["_src"].strip() not in pack.read_text(encoding="utf-8"):
|
||
bad.append(f"{uid8}: исходник в пакете не тот, что у главы")
|
||
var = _variants(pack)
|
||
txt = pack.read_text(encoding="utf-8")
|
||
lays.add(tuple(sorted(lay.items())))
|
||
if set(var) != set(lay):
|
||
bad.append(f"{uid8}: в пакете {len(var)} вариантов, в ключе {len(lay)}")
|
||
for arm in key[p]["1"]["знаков"]: # имя арма в пакете = утечка
|
||
if re.search(rf"\b{arm}\b", txt):
|
||
bad.append(f"{uid8}: имя арма {arm} стоит в пакете")
|
||
if "торопиться не надо" not in txt:
|
||
bad.append(f"{uid8}: нет указания владельца «торопиться не надо»")
|
||
# ⚠ ОБОРВАННАЯ КЛЕТКА В ПАНЕЛИ. `contour.base_a0` (арм `Z0`) читает `content` БЕЗ проверки
|
||
# `finish`, в отличие от `base_draft`, у которого гейт есть на обеих ветках. Через эту щель
|
||
# в китайскую панель прошла клетка с `finish=length` (21.08), и читатель законно поставил
|
||
# продакшен последним за оборванную кульминацию. Гейт стоит ЗДЕСЬ, потому что ловит класс
|
||
# независимо от источника: текст, который короче медианы панели, панель ломает.
|
||
ln = {a: len(t) for a, t in var.items()}
|
||
med = st.median(ln.values())
|
||
for lab, v in ln.items():
|
||
if med and v / med < 0.85:
|
||
bad.append(f"{uid8}: {lay[lab]} короче медианы панели ({v} против {med:.0f}, "
|
||
f"{v / med:.2f}) — похоже на оборванную клетку")
|
||
seen: dict = {}
|
||
for lab, body in var.items():
|
||
if body in seen:
|
||
dupes.append((uid8, lay[seen[body]], lay[lab]))
|
||
seen[body] = lab
|
||
print(f"\n=== СВЕРКА {tag_}, пара {p} ===")
|
||
print(f" текстов сверено побайтно с клетками: {ntx}")
|
||
print(f" пакетов {len(keys)} · РАЗНЫХ раскладок {len(lays)}"
|
||
f"{'' if len(lays) == len(keys) else ' ⛔ раскладки повторяются'}")
|
||
if dupes:
|
||
print(f" ⚠ ПОБАЙТНО СОВПАДАЮЩИЕ армы в {len(dupes)} пакетах:")
|
||
for uid8, a, b in dupes[:6]:
|
||
print(f" {uid8}: {a} ≡ {b}")
|
||
print(" (не обязательно дефект — но панель показывает читателю МЕНЬШЕ текстов, "
|
||
"чем армов, и вывод по такому арму законен только там, где он отличается)")
|
||
for x in bad:
|
||
print(f" ⛔ {x}")
|
||
print(f" {'СБОРКА ГОДНА' if not bad else f'ПРОВАЛОВ {len(bad)}'}")
|
||
return 1 if bad else 0
|
||
|
||
|
||
def score_arch(tag_: str = "arch2", p: str = "en", drop: tuple = (), anchor: str = "") -> int:
|
||
"""`drop` — РЫЧАГ ЧУВСТВИТЕЛЬНОСТИ, не способ получить нужный ответ. Печатается в шапке свода,
|
||
и вывод, который живёт только при выброшенной главе, обязан называться вместе с ней."""
|
||
keys = sorted((OUT / "blind-keys-rol").glob(f"rol-KEY-{tag_}-*.json"))
|
||
keys = [k for k in keys if k.stem.split(f"{tag_}-")[1] not in drop]
|
||
if not keys:
|
||
raise SystemExit(f"⛔ ключей тега {tag_} нет")
|
||
ranks: dict = {}
|
||
twins, decoy, ident, bad = [], [], [], []
|
||
for kf in keys:
|
||
uid8 = kf.stem.split(f"{tag_}-")[1]
|
||
key = json.loads(kf.read_text(encoding="utf-8"))
|
||
if p not in key: # ключ другой пары под тем же тегом — не наш
|
||
continue
|
||
lay = key[p]["1"]["метки"]
|
||
af = READ_DIR / f"ОТВЕТ-{p}-{tag_}-{uid8}.md"
|
||
if not af.exists():
|
||
bad.append((uid8, "ответа нет"))
|
||
continue
|
||
txt = af.read_text(encoding="utf-8", errors="replace")
|
||
places = CH._ranks(_order_of(txt, set(lay))) # noqa: SLF001
|
||
if sorted(places) != sorted(lay):
|
||
bad.append((uid8, f"порядок покрывает {len(places)} из {len(lay)} меток"))
|
||
continue
|
||
r = {lay[lab]: pos for lab, pos in places.items()}
|
||
for arm, v in r.items():
|
||
ranks.setdefault(arm, []).append(v)
|
||
twins.append(abs(r["Z0"] - r["ZF"]))
|
||
if "ZD" in r: # грубый декой стоит не в каждой панели
|
||
decoy.append(r["ZD"])
|
||
# ⚠ КОНТРОЛЬ ТОЖДЕСТВА, найденный чтением: на главах без канон-щелей `Z7` — побайтная копия
|
||
# `ZP` (фиксер не звался). Читатель, разводящий два ОДИНАКОВЫХ текста, не имеет разрешения.
|
||
var = _variants(READ_DIR / f"ЧТЕНИЕ-{p}-{tag_}-{uid8}.md")
|
||
rev = {v: k for k, v in lay.items()}
|
||
if "Z7" in r and "ZP" in r and var.get(rev["Z7"]) == var.get(rev["ZP"]):
|
||
ident.append((uid8, abs(r["Z7"] - r["ZP"])))
|
||
n = len(twins)
|
||
print(f"\n=== СВОД {tag_}, пара {p}: глав отсужено {n} из {len(keys)} ==="
|
||
+ (f"\n ⚠ ВЫБРОШЕНЫ ГЛАВЫ: {', '.join(drop)} — свод УСЛОВНЫЙ" if drop else ""))
|
||
for uid8, why in bad:
|
||
print(f" ⚠ {uid8}: {why}")
|
||
if not n:
|
||
return 1
|
||
print(f"\n{'арм':5s} {'ср. место':>10s} {'глав':>5s} места")
|
||
for arm, v in sorted(ranks.items(), key=lambda x: sum(x[1]) / len(x[1])):
|
||
print(f"{arm:5s} {sum(v) / len(v):10.2f} {len(v):5d} "
|
||
f"{' '.join(f'{x:g}' for x in v)}")
|
||
print(f"\nКОНТРОЛЬ ШУМА |Z0−ZF| среднее {sum(twins) / n:.2f} из {len(ranks)} мест "
|
||
f"· помест-но {' '.join(f'{x:g}' for x in twins)}")
|
||
if decoy:
|
||
print(f"КОНТРОЛЬ ДЕКОЯ ZD среднее место {sum(decoy) / n:.2f} (обязан быть внизу; "
|
||
f"{sum(1 for x in decoy if x > len(ranks) / 2)} из {n} в нижней половине)")
|
||
else:
|
||
print("КОНТРОЛЬ ДЕКОЯ: грубого декоя в этой панели НЕТ — "
|
||
"разрешение прибора держится только на близнецах")
|
||
# ⚠ КОНТРАСТЫ ПАРНЫЕ И МЕРИЛО У НИХ — БЛИЗНЕЦЫ, А НЕ НОЛЬ. Два прогона ОДНОГО способа расходятся
|
||
# по главам; значит «арм A выше арма B» что-то значит, только если разрыв больше того, что тот
|
||
# же прибор показывает между `Z0` и `ZF`. Точный знаковый тест берётся у `zsud`, своего не пишем.
|
||
ZS = _load("zsud_r", ZONE / "zsud.py")
|
||
tw = [a - b for a, b in zip(ranks["Z0"], ranks["ZF"])]
|
||
print(f"\nПАРНЫЕ КОНТРАСТЫ (n={n}). Собственный пол: близнецы Z0/ZF расходятся на "
|
||
f"{st.mean(tw):+.2f} места при пороге {2.8 * st.pstdev(tw) / n ** 0.5:.2f} — "
|
||
f"{'НЕразличимы, как и обязаны' if abs(st.mean(tw)) <= 2.8 * st.pstdev(tw) / n ** 0.5 else '⛔ РАЗЛИЧИМЫ, пол негоден'}")
|
||
# ⚠ ЯКОРЬ КОНТРАСТА — ПАРАМЕТР. Для панели архитектур это боевая связка; для панели
|
||
# переносимости пре-рег Д25 назначил якорем `RN` — тот же промт на `deepseek-v4-pro`,
|
||
# иначе замер сравнивал бы вендоров с ДРУГОЙ архитектурой, а не промт с самим собой.
|
||
skip = (anchor,) if anchor in ranks else ("Z0", "ZF")
|
||
base = ranks[anchor] if anchor in ranks else [
|
||
(a + b) / 2 for a, b in zip(ranks["Z0"], ranks["ZF"])]
|
||
print(f"якорь контраста: {anchor if anchor in ranks else 'связка Z0/ZF'}")
|
||
for arm in sorted(ranks, key=lambda x: sum(ranks[x]) / n):
|
||
if arm in skip:
|
||
continue
|
||
d = [x - y for x, y in zip(ranks[arm], base)] # >0 — арм ХУЖЕ якоря
|
||
mde = 2.8 * st.pstdev(d) / n ** 0.5 # форма порога из пре-рега Д23.3
|
||
print(f" якорь ↔ {arm:4s} разрыв {st.mean(d):+5.2f} места · порог {mde:4.2f} · "
|
||
f"{'РАЗЛИЧИМ ' if abs(st.mean(d)) > mde else 'в пределах'} · "
|
||
f"знаковый p={ZS._sign_p(d):.4f}") # noqa: SLF001
|
||
if ident:
|
||
worst = max(x for _u, x in ident)
|
||
print(f"КОНТРОЛЬ ТОЖДЕСТВА Z7≡ZP на {len(ident)} главах · развод мест: "
|
||
f"среднее {sum(x for _u, x in ident) / len(ident):.2f}, худший {worst:g}"
|
||
f"{' ⛔ читатель развёл ОДИНАКОВЫЕ тексты' if worst else ' ✔ связаны знаком ='}")
|
||
return 0
|
||
|
||
|
||
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] == "--emit-read":
|
||
_pan = next((x.split("=", 1)[1].split(",") for x in a if x.startswith("--panel=")), [])
|
||
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "")
|
||
_n = next((int(x.split("=", 1)[1]) for x in a if x.startswith("--n=")), 3)
|
||
_each = "--each" in a
|
||
for x in ([y for y in a[1:] if y in PAIRS] or list(PAIRS)):
|
||
if not _each:
|
||
emit_read(x, _n, tuple(_pan), _tag)
|
||
continue
|
||
# ⚠ ОДНА ГЛАВА — ОДИН ПАКЕТ, решение владельца 20.08: главы большие, читателю с
|
||
# несколькими сразу тяжело, и он начинает экономить внимание на последних.
|
||
PAIRS[x]["wire"]()
|
||
skipped = []
|
||
for k, u in enumerate(chosen(x)[:_n], 1):
|
||
try:
|
||
emit_read(x, 1, tuple(_pan), f"{_tag}-{u['uid'][:8]}" if _tag else u["uid"][:8],
|
||
only_uid=u["uid"])
|
||
except SystemExit as e:
|
||
# ⚠ ГЛАВА С НЕПОЛНОЙ ПАНЕЛЬЮ ПРОПУСКАЕТСЯ, А НЕ РОНЯЕТ ВЕСЬ ЦИКЛ. Первая
|
||
# редакция падала на первой же такой главе, и из 16 пакетов собралось 5 —
|
||
# причём молча выглядело как «панель полна только на пяти». Пропуск
|
||
# ПЕЧАТАЕТСЯ числом: тихо потерянная глава хуже, чем громко пропущенная.
|
||
skipped.append((u["uid"][:10], str(e)[:60]))
|
||
if skipped:
|
||
print(f" ⚠ ПРОПУЩЕНО глав с неполной панелью: {len(skipped)}")
|
||
for uid, why in skipped:
|
||
print(f" {uid}: {why}")
|
||
elif a[0] == "--score-read":
|
||
score_read()
|
||
elif a[0] == "--wire":
|
||
_only = next((x.split("=", 1)[1] for x in a if x.startswith("--arm=")), "")
|
||
sys.exit(wire_dump(next((x for x in a[1:] if x in PAIRS), "en"), _only))
|
||
elif a[0] == "--verify-read":
|
||
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "arch2")
|
||
sys.exit(verify_read(_tag, next((x for x in a[1:] if x in PAIRS), "en")))
|
||
elif a[0] == "--score-arch":
|
||
_tag = next((x.split("=", 1)[1] for x in a if x.startswith("--tag=")), "arch2")
|
||
_drop = tuple(next((x.split("=", 1)[1].split(",") for x in a if x.startswith("--drop=")), []))
|
||
_anch = next((x.split("=", 1)[1] for x in a if x.startswith("--anchor=")), "")
|
||
sys.exit(score_arch(_tag, next((x for x in a[1:] if x in PAIRS), "en"), _drop, _anch))
|
||
elif a[0] == "--coverage":
|
||
for p in ([x for x in a[1:] if x in PAIRS] or list(PAIRS)):
|
||
print(f"\n=== ПОКРЫТИЕ БОЕВЫХ ПРОМТОВ, пара {p} ===")
|
||
for fname, line, verdict in coverage_report(p):
|
||
print(f" [{verdict:32s}] {fname}: {line}")
|
||
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")
|