722 lines
52 KiB
Python
722 lines
52 KiB
Python
#!/usr/bin/env python3
|
||
"""ПОДАЧА ВХОДА КРИТИКУ — что решает цену стадии: СЛОВА инструкции или ФОРМА подачи. Платный.
|
||
|
||
⚠ ОТКУДА ВОПРОС. Разбор токенов по ролям (журнал §14.2) дал разрыв, который никто не заказывал:
|
||
китайский смысловой критик думает 1 084 токена, английский — 9 576 при вдвое КОРОТКОМ входе.
|
||
Объяснение нашлось замером структуры (§14.3): китайская вебновелла набрана ПО ПРЕДЛОЖЕНИЮ НА
|
||
СТРОКУ, и черновик её разбивку скопировал (77 строк исходника → 78 абзацев перевода, 0.96:1);
|
||
английский приходит сплошным блоком (1 строка → 8 абзацев, 0.12:1). То есть дорого не «сличать
|
||
два текста», а сличать два НЕВЫРОВНЕННЫХ текста. У DeepSeek размышление тарифицируется по цене
|
||
ВЫХОДА, поэтому 85% цены дорогой клетки — то, что модель надумала про себя.
|
||
|
||
⚠⚠ ЭТО ОДНО НАБЛЮДЕНИЕ, И ОБЪЯСНИЛ ЕГО Я ЖЕ. Норма, заведённая разговором 17.08: прежде чем
|
||
строить схему поверх наблюдения, проверить НАБЛЮДЕНИЕ отдельным дешёвым замером. Здесь ничего не
|
||
выравнивается и не размечается — меняется только ПОДАЧА одного и того же материала.
|
||
|
||
⛔⛔ НАХОДКА, СДЕЛАННАЯ ПРИ ПОСТРОЙКЕ ЭТОГО ЗАМЕРА И МЕНЯЮЩАЯ ЕГО ЦЕЛЬ. Английский исходник
|
||
приходит критику одной строкой НЕ ПОТОМУ, что так устроен английский или книга, а потому, что так
|
||
устроен НАШ ЭКСТРАКТОР: `pair_en.raw_text()` сносит ВСЕ теги регексом `<[^>]+>` → пробел, вместе
|
||
с `</p>`. Замер по самому epub: в книге **11 447 абзацев, медиана 62 знака**; наш плоский текст
|
||
несёт 1 720 строк вместо 13 243. На единицу в 1642 знака приходится ~26 авторских абзацев, а мы
|
||
подаём их одним блоком. То есть невыровненность английской пары — АРТЕФАКТ НАШЕЙ НАРЕЗКИ, и
|
||
чинится он бесплатно, у нас же. Проверено: текст всех 16 английских единиц находится в абзацной
|
||
версии побайтно после нормализации пробелов, значит структуру можно вернуть, НЕ трогая
|
||
зафризенный экстрактор закрытой оси (менять его нельзя — uid единиц есть функция его текста).
|
||
|
||
ЧЕТЫРЕ ВАРИАНТА. Исходник и черновик во всех четырёх несут ОДИН И ТОТ ЖЕ текст; меняется ПОДАЧА:
|
||
|
||
P0 БАЗА нынешний промт критика, исходник как есть (одним блоком)
|
||
PF ФРАЗА + правдивое описание параллельности текстов (идея владельца 17.08)
|
||
PA АБЗАЦЫ база + исходнику ВОЗВРАЩЕНЫ авторские абзацы из epub (перевод не трогаем)
|
||
PB ОБА фраза + абзацы
|
||
|
||
⚠ ПОЧЕМУ НЕ «РАЗБИТЬ ПО ПРЕДЛОЖЕНИЮ НА СТРОКУ», КАК ПЛАНИРОВАЛОСЬ. Замечание владельца 20.08:
|
||
осмысленная разметка — это ГРУППИРОВКА по смыслу, а не более мелкое дробление; дробить мельче
|
||
значит увеличить число кусков, которые модель обязана сопоставить. Плюс замер: медиана авторского
|
||
абзаца у Кристоффа 62 знака, то есть примерно одно предложение, и синтетическая разбивка была бы
|
||
почти побайтным близнецом `PA` — второй оплаченной копией того же условия. Авторские абзацы и
|
||
честнее (это структура, которую написал автор), и продуктово осмысленнее (это то, что можно
|
||
починить в проде одной правкой экстрактора).
|
||
|
||
⚠ У КИТАЙСКОЙ ПАРЫ ВАРИАНТОВ ДВА, И ЭТО НЕ ЭКОНОМИЯ. Её исходник УЖЕ разбит по предложению на
|
||
строку — свойство материала, замеренное: 78 строк по 24 знака на главу. Возвращать абзацы нечему,
|
||
`PA`/`PB` там были бы побайтной копией `P0`/`PF`, то есть двумя оплаченными близнецами.
|
||
Селфтест это ПРОВЕРЯЕТ на живом сырье, а не верит комментарию.
|
||
⚠ Японская пара сюда не входит, но замерена тем же счётом и ложится к китайской: 19 строк по
|
||
43 знака, черновик 19 абзацев — отношение 1.00:1, самое выровненное из трёх книг. Значит
|
||
«дорогой режим» есть у ОДНОЙ книги из трёх, и именно у той, которую испортили мы.
|
||
|
||
⚠ ФОРМУЛИРОВКА ФРАЗЫ — ТОЛЬКО ПРАВДА, И ЭТО НЕ ЭТИКА, А МЕХАНИКА. «Предложение за предложением»
|
||
было бы ЛОЖЬЮ: наш же промт редактора обязывает сливать абзацы (замер: 77 строк исходника → 43
|
||
абзаца отредактированного). Соврём — модель будет искать соответствие, не найдёт и потратит
|
||
БОЛЬШЕ, а не меньше.
|
||
|
||
⚠⚠ РИСК, РАДИ КОТОРОГО ЗАВЕДЕНЫ ПОСАДКИ. Фраза про полное покрытие исходника может заставить
|
||
критика ПЕРЕСТАТЬ ИСКАТЬ ПРОПУСКИ — а потерянный кусок один из главных классов дефекта. Выигрыш
|
||
в токенах, купленный слепотой к целому классу, не выигрыш. Поэтому в каждый черновик садятся ДВА
|
||
дефекта с ИЗВЕСТНЫМ АДРЕСОМ (`plants.json`, пишется ДО первой покупки):
|
||
|
||
ПРОПУСК из черновика удаляется целый абзац — тот самый класс, что под угрозой
|
||
ИНВЕРСИЯ снимается отрицание — смысл переворачивается, текст остаётся грамотным
|
||
|
||
⇒ НИКТО НИЧЕГО НЕ РАЗМЕЧАЕТ РУКАМИ. Правильный ответ известен ДО прогона, потому что порчу внесли
|
||
мы сами; «нашёл или нет» считается детерминированно. Вопрос владельца 20.08 («а кто данные
|
||
разметит в таблицу? ты сам?») закрыт устройством замера, а не обещанием.
|
||
|
||
⚠ И У ДЕТЕКТОРА ЕСТЬ НУЛЕВАЯ МОДЕЛЬ — норма Д0.6 требует от всякого классификатора печатать
|
||
частоту срабатывания на СЛУЧАЙНОМ входе. Здесь она бесплатна: те же главы уже отсужены критиком
|
||
по НЕиспорченному черновику (клетки `A3`/`E3`), и детектор гоняется по ним. Сколько раз он
|
||
«находит» посадку там, где её не было, — и есть его шансовый пол.
|
||
|
||
⚠ ВАРИАНТЫ СТРЕЛЯЮТСЯ ВПЕРЕМЕШКУ ВНУТРИ ГЛАВЫ, И ЭТО НЕ АККУРАТНОСТЬ. Квирк-бюллетень (:110)
|
||
несёт замер: шесть дефолтных розыгрышей ОДНОГО запроса, разнесённых по времени, дали строго
|
||
растущий ряд размышления 1952 → 9104 → 10818 → 11157 → 13421 → 15720 знаков. Купи мы сначала все
|
||
`P0`, потом все `PF` — этот дрейф и стал бы «эффектом». Порядок вариантов внутри главы к тому же
|
||
перетасован детерминированно от соли, чтобы и позиция в очереди не доставалась одному варианту.
|
||
|
||
⚠ ДВЕ МОДЕЛИ — вопрос владельца 20.08 «это только дипсика касается?». `deepseek-v4-pro` (жилец) и
|
||
`glm-5` (настоящая альтернатива редакторского тира: другой вендор, цены не поднимал).
|
||
⚠ `glm-5` идёт с ВКЛЮЧЁННЫМ размышлением, и это ОБЪЯВЛЕННОЕ отступление от боевой конфигурации:
|
||
`roster.THINK` гасит ему thinking (`extra_body {"thinking": {"type": "disabled"}}`), а мы меряем
|
||
ровно размышление — с выключенным мерить было бы нечего. Цена отступления: у glm thinking-ON
|
||
раньше не влезал в таймаут (квирки :52), и это может выстрелить. Выстрелит — печатается как
|
||
результат, а не прячется.
|
||
⚠ ГРОК И GEMINI ОТВЕРГНУТЫ ДО ПОКУПКИ, с основанием: у grok размышление гуляет ×163 на побайтно
|
||
одном входе (квирки :69 — 98 · 8000 · 14014 · 16000 на одном куске), при четырёх главах этот шум
|
||
съест любой эффект; Gemini на нашем материале массово падает в контент-фильтр (эксп-21 §1/§8).
|
||
|
||
Запуск: podacha.py --dry | --selftest | --plants | --buy [zh|en] | --score
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
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()` настраивает хуки именно в нём. Свой второй экземпляр остался бы с
|
||
# китайскими хуками, английские черновики читались бы китайским путём и возвращали пусто —
|
||
# английская ось молча стала бы пустой. Класс уже стоил фазе оплаченных клеток, поймано `--dry`.
|
||
EN = _load("en_axis_p", 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_p", ZONE / "zakhod.py") # гейт перелома цены и пред-полётная проба
|
||
|
||
EPUB_EN = Path.home() / "books" / "Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub"
|
||
|
||
SALT = "podacha-2026-08-20" # соль отбора глав и порядка вариантов; впечатана ДО покупок
|
||
N_UNITS = 4 # решение владельца 20.08: «восемь глав слишком, сократи до 4»
|
||
MODELS = ("deepseek-v4-pro", "glm-5")
|
||
MAX_OUT = 32000 # как в заходе: обрыв на потолке стоит денег и не даёт данных
|
||
TRUTH = OUT / "podacha-plants.json" # правильный ответ; пишется ДО первой покупки
|
||
|
||
# ⚠ ПАРА ЖИВЁТ В ДАННЫХ. Ни одна ветка ниже не спрашивает «а это английский?»: возврат абзацев —
|
||
# такой же параметр пары, как её буква тега или касса. Пара, которой в репозитории ещё нет,
|
||
# заводится строкой здесь и не требует правки ни одной функции.
|
||
PAIRS = {
|
||
"zh": {"letter": "k", "phase": "ФД-K", "crit": "A3", "vars": ("P0", "PF"),
|
||
"wire": lambda: C.configure(PARA.ZH), "units": lambda: C.units_ok(),
|
||
"reflow": None}, # исходник уже по предложению на строку — возвращать нечего
|
||
"en": {"letter": "l", "phase": "ФД-L", "crit": "E3", "vars": ("P0", "PF", "PA", "PB"),
|
||
"wire": EN.wire, "units": EN.units,
|
||
"reflow": lambda src: restore_paragraphs(src, EPUB_EN)},
|
||
}
|
||
ALL_VARS = ("P0", "PF", "PA", "PB")
|
||
WHAT = {"P0": "база — нынешний промт критика",
|
||
"PF": "+ ФРАЗА о параллельности текстов (идея владельца)",
|
||
"PA": "+ исходнику ВОЗВРАЩЕНЫ авторские абзацы",
|
||
"PB": "фраза И абзацы вместе"}
|
||
|
||
# ── ФРАЗА. Каждое утверждение проверяемо, ни одного обещания сверх правды ─────────────────────
|
||
# «должен покрывать», а не «покрывает»: второе было бы обещанием, которого мы не даём, и оно же
|
||
# снимало бы с критика обязанность искать пропуски. Первое называет ТРЕБОВАНИЕ — то самое, из-за
|
||
# которого пропуск и есть ошибка.
|
||
PARALLEL_NOTE = (
|
||
"\n\nКАК СООТНОСЯТСЯ ЭТИ ДВА ТЕКСТА. Они идут ПАРАЛЛЕЛЬНО: порядок изложения в переводе тот "
|
||
"же, что в исходнике, и перевод ДОЛЖЕН покрывать исходник целиком. При этом членение на "
|
||
"абзацы и предложения может отличаться — переводчику разрешено сливать и дробить, и это не "
|
||
"ошибка. Читай оба текста параллельно, сверяя ход за ходом, и НЕ ищи точных пар предложений.")
|
||
|
||
# ── ВОЗВРАТ АВТОРСКИХ АБЗАЦЕВ ────────────────────────────────────────────────────────────────
|
||
# ⚠ ЗАФРИЗЕННЫЙ ЭКСТРАКТОР ЗАКРЫТОЙ ОСИ НЕ ТРОГАЕТСЯ. `pair_en.raw_text()` определяет uid единиц
|
||
# (`pair_en.py:144` предупреждает прямо), и правка его пере-именовала бы всю английскую ось —
|
||
# то есть обнулила бы отсуженное. Поэтому абзацы восстанавливаются ЗДЕСЬ и только для подачи:
|
||
# книга читается второй раз, с сохранением границ `</p>`, `</div>`, `<br>`; текст единицы ищется
|
||
# в ней по нормализованным пробелам, и возвращается тот же самый текст — но с переводами строк.
|
||
# ⚠ Ни знака не добавляется и не теряется: селфтест сверяет обе версии побайтно после снятия
|
||
# пробельных различий. Если единица не нашлась — возвращается исходная подача, и число ненайденных
|
||
# печатается; молча подменять вариант нельзя, иначе смета оплатит близнецов.
|
||
_WS = re.compile(r"\s+")
|
||
_para_cache: dict[str, tuple[str, str, list[int]]] = {}
|
||
|
||
|
||
def _epub_paragraphed(path: Path) -> tuple[str, str, list[int]]:
|
||
"""(текст с абзацами, его нормализованная копия, карта позиций). Считается один раз."""
|
||
key = str(path)
|
||
if key in _para_cache:
|
||
return _para_cache[key]
|
||
import html as _html # noqa: PLC0415
|
||
import zipfile # noqa: PLC0415
|
||
z = zipfile.ZipFile(path)
|
||
parts = []
|
||
for n in sorted(x for x in z.namelist() if x.endswith((".xhtml", ".html", ".htm"))):
|
||
raw = z.read(n).decode("utf-8", "ignore")
|
||
raw = re.sub(r"(?i)</(p|div|h[1-6])\s*>", "\n", raw)
|
||
raw = re.sub(r"(?i)<br\s*/?>", "\n", raw)
|
||
parts.append(re.sub(r"[ \t]+", " ", _html.unescape(re.sub(r"<[^>]+>", " ", raw))))
|
||
text = "\n".join(parts)
|
||
norm, idx = [], []
|
||
prev_ws = True
|
||
for i, ch in enumerate(text):
|
||
if ch.isspace():
|
||
if not prev_ws:
|
||
norm.append(" ")
|
||
idx.append(i)
|
||
prev_ws = True
|
||
else:
|
||
norm.append(ch)
|
||
idx.append(i)
|
||
prev_ws = False
|
||
_para_cache[key] = (text, "".join(norm).strip(), idx)
|
||
return _para_cache[key]
|
||
|
||
|
||
def restore_paragraphs(src: str, epub: Path) -> str:
|
||
"""Тот же текст единицы, но с авторскими границами абзацев. Пусто — если не нашёлся."""
|
||
text, norm, idx = _epub_paragraphed(epub)
|
||
want = _WS.sub(" ", src).strip()
|
||
i = norm.find(want)
|
||
if i < 0:
|
||
return ""
|
||
a, b = idx[i], idx[min(i + len(want) - 1, len(idx) - 1)] + 1
|
||
return "\n".join(x.strip() for x in text[a:b].split("\n") if x.strip())
|
||
|
||
|
||
# ── ПОСАДКИ: ДВА ДЕФЕКТА С ИЗВЕСТНЫМ АДРЕСОМ ─────────────────────────────────────────────────
|
||
# ⚠ Классы выбраны не воображением. «Снятое отрицание» замерено на этом же сырье как ложащееся на
|
||
# 31 единицу из 31 (норма маржевого декоя, `sud.MARGIN_PLANTS`): меняется одно слово, текст
|
||
# остаётся связным и грамотным, ошибка ТОЛЬКО смысловая. «Удалённый абзац» — тот самый класс,
|
||
# которому угрожает фраза про полное покрытие; он и есть предмет страховки.
|
||
_NEG = re.compile(r"\bне\s+([а-яё]{3,}(?:л|ла|ло|ли|ет|ит|ют|ат|ят))\b")
|
||
MIN_PARA = 120 # абзац короче не удаляем: пропажу двух слов не заметит и человек
|
||
# ⚠ ВЕРХНЯЯ ГРАНИЦА ЗАВЕДЕНА ВЫЧИТКОЙ ПОСАДОК ПЕРЕД ПОКУПКОЙ, И ЭТО НЕ ПЕДАНТИЗМ. Первая редакция
|
||
# знала только нижний порог, и на английской единице `100b088f71` под удаление попал абзац в
|
||
# 1602 знака при черновике в 1642 — то есть посадка сносила ПОЧТИ ВЕСЬ текст. Критик увидел бы
|
||
# пустую клетку, «нашёл пропуск» стало бы тавтологией, а замер токенов на этой единице мерил бы
|
||
# другую задачу. Пропажа обязана быть НЕЗАМЕТНОЙ на глаз в объёме — иначе это не тонкая посадка,
|
||
# а второй грубый декой (ровно та ошибка, за которую в паке 23 контроль оказался пуст).
|
||
MAX_DROP_SHARE = 0.25 # удаляемый абзац — не больше четверти черновика
|
||
|
||
|
||
def plant(draft: str, uid: str) -> tuple[str, dict]:
|
||
"""Испорченный черновик и ПРАВИЛЬНЫЙ ОТВЕТ к нему. Детерминированно от соли и uid.
|
||
|
||
⚠ Возвращает не только адреса, но и УЛИКИ — слова из удалённого абзаца и оба варианта фразы с
|
||
отрицанием. По ним детектор потом решает «нашёл или нет», и решает без единой ручной разметки.
|
||
"""
|
||
paras = [p for p in draft.split("\n") if p.strip()]
|
||
cand = [i for i, p in enumerate(paras)
|
||
if MIN_PARA <= len(p) <= len(draft) * MAX_DROP_SHARE]
|
||
truth: dict = {"uid": uid}
|
||
if cand:
|
||
# середина списка кандидатов, сдвинутая хешем: ни первый абзац (там имена и зачин), ни
|
||
# последний (там развязка) — оба слишком заметны и сделали бы посадку грубой
|
||
h = int(hashlib.sha256(f"{SALT}|{uid}|drop".encode()).hexdigest(), 16)
|
||
i = cand[len(cand) // 4 + h % max(1, len(cand) // 2)]
|
||
truth["dropped"] = paras[i]
|
||
paras = paras[:i] + paras[i + 1:]
|
||
out = "\n\n".join(paras)
|
||
m = _NEG.search(out)
|
||
if m:
|
||
truth["negation_before"] = m.group(0)
|
||
truth["negation_after"] = m.group(1)
|
||
out = out[:m.start()] + m.group(1) + out[m.end():]
|
||
return out, truth
|
||
|
||
|
||
def truth_all() -> dict:
|
||
return json.loads(TRUTH.read_text(encoding="utf-8")) if TRUTH.exists() else {}
|
||
|
||
|
||
def write_truth() -> dict:
|
||
"""Правильный ответ на диск ДО первой покупки. Существующий НЕ переписывается.
|
||
|
||
⚠ Пере-запись после ответов означала бы, что «правильный ответ» стал функцией того, что модель
|
||
ответила. Ровно этот класс — раскладка меток, пере-эмитированная поверх отсуженной оси, — уже
|
||
стоил фазе аварии, и починка была в том, что ключ пишется один раз и до ответов.
|
||
"""
|
||
if TRUTH.exists():
|
||
return truth_all()
|
||
key: dict = {}
|
||
for p, cfg in PAIRS.items():
|
||
cfg["wire"]()
|
||
for u in chosen(p):
|
||
_t, tr = plant(C.base_draft(u["uid"]), u["uid"])
|
||
key[f"{p}|{u['uid']}"] = tr
|
||
TRUTH.write_text(json.dumps(key, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
print(f"правильный ответ записан ДО покупок: {TRUTH} ({len(key)} глав)")
|
||
return key
|
||
|
||
|
||
# ── ЗАПРОС ───────────────────────────────────────────────────────────────────────────────────
|
||
def crit_msgs(p: str, src: str, draft: str, var: str) -> list[dict]:
|
||
"""Запрос критика в заданном варианте подачи.
|
||
|
||
⚠ Системный промт берётся у БОЕВОГО критика (`contour.crit_msgs`) и только дополняется: если
|
||
собрать его здесь заново, замер сравнивал бы два разных промта, а не две подачи одного.
|
||
"""
|
||
if var not in ALL_VARS:
|
||
raise SystemExit(f"⛔ вариант {var!r} неизвестен; знаю {' '.join(ALL_VARS)}")
|
||
cfg = PAIRS[p]
|
||
cfg["wire"]()
|
||
shown = src
|
||
if var in ("PA", "PB"):
|
||
if not cfg["reflow"]:
|
||
raise SystemExit(f"⛔ у пары {p} возврат абзацев не заведён — вариант {var} невозможен")
|
||
shown = cfg["reflow"](src)
|
||
if not shown.strip():
|
||
raise SystemExit(f"⛔ авторские абзацы для этой единицы не нашлись — вариант {var} "
|
||
"стал бы побайтным близнецом базы, покупка отказана")
|
||
m = C.crit_msgs(shown, draft)
|
||
if var in ("PF", "PB"):
|
||
m[0]["content"] = m[0]["content"].rstrip() + PARALLEL_NOTE
|
||
return m
|
||
|
||
|
||
def tag(p: str, var: str, model: str, uid: str) -> str:
|
||
short = {"deepseek-v4-pro": "ds", "glm-5": "gl"}[model]
|
||
return f"dv-{PAIRS[p]['letter']}-{var.lower()}{short}-{uid}"
|
||
|
||
|
||
def cell(tg: str) -> dict | None:
|
||
f = OUT / f"{tg}.json"
|
||
if not f.exists():
|
||
return None
|
||
d = json.loads(f.read_text(encoding="utf-8"))
|
||
return d if d.get("finish") == "stop" and (d.get("content") or "").strip() else None
|
||
|
||
|
||
def call_kwargs(model: str, msgs: list[dict]) -> dict:
|
||
"""Боевая конфигурация модели — КРОМЕ явно объявленного отступления по размышлению glm-5."""
|
||
kw = ROSTER.call_kwargs(model, msgs, max_out=MAX_OUT)
|
||
if model == "glm-5":
|
||
kw.pop("extra_body", None) # вендор-дефолт = thinking ON; см. шапку файла
|
||
return kw
|
||
|
||
|
||
# ── ОТБОР ГЛАВ ───────────────────────────────────────────────────────────────────────────────
|
||
def pool(p: str) -> list[dict]:
|
||
"""Главы, где есть черновик, боевой перевод И ответ критика по НЕиспорченному входу.
|
||
|
||
Последнее обязательно: эта клетка — нулевая модель детектора, и без неё замер не может
|
||
отличить «детектор сработал на посадке» от «детектор срабатывает всегда».
|
||
"""
|
||
cfg = PAIRS[p]
|
||
cfg["wire"]()
|
||
out = []
|
||
for u in cfg["units"]():
|
||
d = C.base_draft(u["uid"])
|
||
if not d.strip():
|
||
continue
|
||
base = OUT / f"{C.tag(cfg['crit'], u['uid'], 'crit')}.json"
|
||
if not (base.exists()
|
||
and (json.loads(base.read_text(encoding="utf-8")).get("content") or "").strip()):
|
||
continue
|
||
# ⚠ ОБЕ ПОСАДКИ ОБЯЗАНЫ ЛОЖИТЬСЯ, И ЭТО УСЛОВИЕ ОТБОРА, А НЕ ПОСТ-ФИЛЬТР. Глава, куда
|
||
# тонкая пропажа не помещается (все абзацы либо короче порога, либо больше четверти
|
||
# текста), даёт клетку без правильного ответа — и «не нашёл» на ней означает «нечего было
|
||
# находить». Такие главы в замер не идут вовсе, а не молча портят долю.
|
||
after, tr = plant(d, u["uid"])
|
||
if not tr.get("dropped") or not tr.get("negation_before"):
|
||
continue
|
||
# авторские абзацы обязаны находиться — иначе вариант станет близнецом базы
|
||
if cfg["reflow"] and not cfg["reflow"](u["source"]).strip():
|
||
continue
|
||
out.append(u)
|
||
return out
|
||
|
||
|
||
def chosen(p: str) -> list[dict]:
|
||
"""N_UNITS глав, отобранных ДЕТЕРМИНИРОВАННО от объявленной соли, а не по длине.
|
||
|
||
Длина коррелирует с трудностью, трудность — с тем, сколько модель думает; отбор по длине
|
||
покупал бы часть ответа заранее.
|
||
"""
|
||
us = pool(p)
|
||
return sorted(us, key=lambda u: hashlib.sha256(
|
||
f"{SALT}|{p}|{u['uid']}".encode()).hexdigest())[:N_UNITS]
|
||
|
||
|
||
def var_order(p: str, uid: str, model: str) -> list[str]:
|
||
"""Порядок вариантов ВНУТРИ главы — перетасован от соли. См. шапку про дрейф размышления."""
|
||
vs = list(PAIRS[p]["vars"])
|
||
return sorted(vs, key=lambda v: hashlib.sha256(
|
||
f"{SALT}|{p}|{uid}|{model}|{v}".encode()).hexdigest())
|
||
|
||
|
||
# ── ПОКУПКА ──────────────────────────────────────────────────────────────────────────────────
|
||
def buy(p: str) -> None:
|
||
ZK.price_gate()
|
||
cfg = PAIRS[p]
|
||
cfg["wire"]()
|
||
write_truth()
|
||
ZK.preflight(list(MODELS), cfg["phase"], cfg["letter"])
|
||
us = chosen(p)
|
||
key = truth_all()
|
||
led = MONEY.Guarded(cfg["phase"], default_expect=0.045)
|
||
print(f"\nпара {p} · глав {len(us)} · варианты {' '.join(cfg['vars'])} · "
|
||
f"модели {' '.join(MODELS)} · касса {cfg['phase']}")
|
||
for u in us:
|
||
uid, src = u["uid"], u["source"]
|
||
if f"{p}|{uid}" not in key:
|
||
raise SystemExit(f"⛔ у главы {uid} нет записанного правильного ответа — "
|
||
"покупка остановлена, посадки должны быть зафиксированы ДО неё")
|
||
draft, _tr = plant(C.base_draft(uid), uid)
|
||
for model in MODELS:
|
||
for var in var_order(p, uid, model):
|
||
tg = tag(p, var, model, uid)
|
||
if cell(tg):
|
||
continue
|
||
# ⚠ Гейт перелома цены — перед КАЖДОЙ клеткой, а не один раз на старте: прогон
|
||
# длится часами, и пересёкший 16:00 UTC цикл писал бы пост-переломные вызовы по
|
||
# июльскому прайсу.
|
||
ZK.price_gate(quiet=True)
|
||
rec = MONEY.purchase(led, tg, model, ED22.client(model),
|
||
call_kwargs(model, crit_msgs(p, src, draft, var)),
|
||
role="critic", uid=uid, arm=var, 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]} — клетка объявляется несобранной")
|
||
MONEY.report()
|
||
|
||
|
||
# ── РАЗБОР ───────────────────────────────────────────────────────────────────────────────────
|
||
def findings(txt: str) -> list[str]:
|
||
"""Строки-находки критика. Тот же разбор, что у боевого контура, плюс фильтр согласий."""
|
||
lines = [ln.strip("-•* \t") for ln in txt.splitlines() if ln.strip().startswith(("-", "•", "*"))]
|
||
return [ln for ln in lines if not ZK._is_confirmation(ln)] # noqa: SLF001
|
||
|
||
|
||
_WORD = re.compile(r"[А-Яа-яЁёA-Za-z]{4,}")
|
||
|
||
|
||
def _caught_drop(txt: str, tr: dict) -> bool:
|
||
"""Заметил ли критик УДАЛЁННЫЙ абзац. Детерминированно, без ручной разметки.
|
||
|
||
Правило: критик обязан сослаться на содержимое пропавшего куска — назвать хотя бы два редких
|
||
слова из него. Редкость меряется по самому тексту главы: слова, встречающиеся в удалённом
|
||
абзаце и НЕ встречающиеся в остальном черновике, — единственная улика, которая не могла
|
||
приехать из соседних абзацев.
|
||
"""
|
||
dropped = tr.get("dropped") or ""
|
||
uniq = tr.get("_uniq") or []
|
||
if not dropped or not uniq:
|
||
return False
|
||
return sum(1 for w in uniq if w in txt.lower()) >= 2
|
||
|
||
|
||
def _caught_neg(txt: str, tr: dict) -> bool:
|
||
"""Заметил ли критик снятое отрицание: процитировал испорченную форму или назвал отрицание."""
|
||
after = (tr.get("negation_after") or "").lower()
|
||
if not after:
|
||
return False
|
||
low = txt.lower()
|
||
return (after in low and ("отриц" in low or "не " + after in low)) or f"«{after}" in low
|
||
|
||
|
||
def _uniq_words(tr: dict, draft_after: str) -> list[str]:
|
||
"""Редкие слова удалённого абзаца — считаются ЗДЕСЬ, а не хранятся: правильный ответ не должен
|
||
зависеть от того, как мы сегодня решили считать редкость."""
|
||
d = (tr.get("dropped") or "").lower()
|
||
rest = draft_after.lower()
|
||
return sorted({w for w in _WORD.findall(d) if len(w) >= 5 and w not in rest})[:12]
|
||
|
||
|
||
def score() -> None:
|
||
key = truth_all()
|
||
if not key:
|
||
raise SystemExit("⛔ правильного ответа на диске нет — замер не разбирается")
|
||
for p, cfg in PAIRS.items():
|
||
cfg["wire"]()
|
||
us = chosen(p)
|
||
print("\n" + "=" * 78)
|
||
print(f"ПАРА {p.upper()} · глав {len(us)} · варианты {' '.join(cfg['vars'])}")
|
||
print("=" * 78)
|
||
for model in MODELS:
|
||
rows: dict = {}
|
||
for u in us:
|
||
uid = u["uid"]
|
||
tr = dict(key.get(f"{p}|{uid}") or {})
|
||
after, _ = plant(C.base_draft(uid), uid)
|
||
tr["_uniq"] = _uniq_words(tr, after)
|
||
for var in cfg["vars"]:
|
||
d = cell(tag(p, var, model, uid))
|
||
if not d:
|
||
continue
|
||
txt = d.get("content") or ""
|
||
fs = findings(txt)
|
||
rows.setdefault(var, {})[uid] = dict(
|
||
think=d.get("reasoning_tokens") or 0,
|
||
billed=ROSTER.billed_output(model, d.get("completion_tokens") or 0,
|
||
d.get("reasoning_tokens") or 0,
|
||
d.get("total_tokens") or 0,
|
||
d.get("prompt_tokens") or 0),
|
||
inp=d.get("prompt_tokens") or 0, cost=d.get("cost_usd") or 0,
|
||
n=len(fs), drop=_caught_drop(txt, tr), neg=_caught_neg(txt, tr))
|
||
if not rows:
|
||
print(f"\n {model}: клеток нет")
|
||
continue
|
||
print(f"\n === {model} ===")
|
||
print(f" {'вар':4s}{'кл':>3s}{'вход':>7s}{'РАЗМЫШЛ':>9s}{'оплач.вых':>10s}"
|
||
f"{'$/кл':>9s}{'находок':>9s}{'пропуск':>9s}{'отриц':>7s} что это")
|
||
for var in cfg["vars"]:
|
||
r = rows.get(var) or {}
|
||
if not r:
|
||
print(f" {var:4s} — клеток нет")
|
||
continue
|
||
med = lambda k: st.median([v[k] for v in r.values()]) # noqa: E731
|
||
print(f" {var:4s}{len(r):3d}{med('inp'):7.0f}{med('think'):9.0f}"
|
||
f"{med('billed'):10.0f}{med('cost'):9.5f}{med('n'):9.1f}"
|
||
f"{sum(v['drop'] for v in r.values()):5d}/{len(r):<3d}"
|
||
f"{sum(v['neg'] for v in r.values()):4d}/{len(r):<2d} {WHAT[var]}")
|
||
base = rows.get("P0") or {}
|
||
if base:
|
||
print("\n ПАРНАЯ РАЗНИЦА ПРОТИВ БАЗЫ (одна и та же глава, одна и та же модель):")
|
||
for var in cfg["vars"]:
|
||
if var == "P0" or var not in rows:
|
||
continue
|
||
ds = [rows[var][u]["billed"] - base[u]["billed"]
|
||
for u in rows[var] if u in base]
|
||
dn = [rows[var][u]["n"] - base[u]["n"] for u in rows[var] if u in base]
|
||
if not ds:
|
||
continue
|
||
print(f" {var:4s} оплаченный выход {st.mean(ds):+8.0f} токенов "
|
||
f"({st.mean(ds) / max(1, st.mean([base[u]['billed'] for u in base])):+.0%}) "
|
||
f"· находок {st.mean(dn):+.1f} · единиц {len(ds)}")
|
||
# НУЛЕВАЯ МОДЕЛЬ ДЕТЕКТОРА — по клеткам критика, где посадки НЕ БЫЛО
|
||
print("\n НУЛЕВАЯ МОДЕЛЬ ДЕТЕКТОРА (клетки критика по НЕиспорченному черновику):")
|
||
fp_d = fp_n = tot = 0
|
||
for u in us:
|
||
uid = u["uid"]
|
||
f = OUT / f"{C.tag(cfg['crit'], uid, 'crit')}.json"
|
||
if not f.exists():
|
||
continue
|
||
txt = json.loads(f.read_text(encoding="utf-8")).get("content") or ""
|
||
tr = dict(key.get(f"{p}|{uid}") or {})
|
||
after, _ = plant(C.base_draft(uid), uid)
|
||
tr["_uniq"] = _uniq_words(tr, after)
|
||
tot += 1
|
||
fp_d += _caught_drop(txt, tr)
|
||
fp_n += _caught_neg(txt, tr)
|
||
print(f" ложных «нашёл пропуск» {fp_d}/{tot} · ложных «нашёл отрицание» {fp_n}/{tot}")
|
||
print(" ⇒ выше этого пола детектор ничего не доказывает")
|
||
|
||
|
||
# ── СМЕТА И ПРОВЕРКИ ─────────────────────────────────────────────────────────────────────────
|
||
def price_of(p: str, model: str) -> float:
|
||
"""Ожидаемая цена клетки — из ЗАМЕРЕННЫХ токенов уже купленных клеток критика ЭТОЙ пары,
|
||
пере-оценённых ТЕКУЩИМ пином. Зашитых цен в файле нет: они врали вчетверо после пере-пина."""
|
||
toks = []
|
||
for f in OUT.glob(f"{C.tag(PAIRS[p]['crit'], '', 'crit')}*.json"):
|
||
if any(x in f.name for x in ("ERROR", "LENGTH", "ЭХО")):
|
||
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.045
|
||
med = [st.median([x[i] for x in toks]) for i in range(5)]
|
||
return ROSTER.cost(model, int(med[0]), int(med[1]), int(med[2]), int(med[3]), int(med[4]))
|
||
|
||
|
||
def cmd_dry() -> None:
|
||
print(f"ПОДАЧА ВХОДА КРИТИКУ · соль {SALT} · глав на пару {N_UNITS}\n")
|
||
ZK.price_gate()
|
||
tot = 0.0
|
||
for p, cfg in PAIRS.items():
|
||
cfg["wire"]()
|
||
us, pl = chosen(p), pool(p)
|
||
print(f"\n=== пара {p} · пул {len(pl)} · взято {len(us)} · касса {cfg['phase']} ===")
|
||
sub = 0.0
|
||
for model in MODELS:
|
||
pr = price_of(p, model)
|
||
need = sum(1 for u in us for v in cfg["vars"]
|
||
if not cell(tag(p, v, model, u["uid"])))
|
||
sub += need * pr
|
||
print(f" {model:18s} надо {need:2d} клеток × ${pr:.5f} = ${need * pr:.4f}")
|
||
sp = MONEY.Guarded(cfg["phase"]).spent()
|
||
ceil = MONEY.CEILINGS[cfg["phase"]]
|
||
mark = "ВЛЕЗАЕТ" if sub <= ceil - sp else f"НЕ ВЛЕЗАЕТ (не хватает ${sub - (ceil - sp):.4f})"
|
||
print(f" ИТОГО ${sub:.4f} · потолок {cfg['phase']} ${ceil:.2f} · "
|
||
f"потрачено ${sp:.4f} → {mark}")
|
||
tot += sub
|
||
print(f"\nВЕСЬ ЗАМЕР: ${tot:.4f} по текущему (пиковому) пину ростера.")
|
||
|
||
|
||
def cmd_plants() -> None:
|
||
"""Показать посадки глазами — обязательная вычитка ПЕРЕД покупкой."""
|
||
key = write_truth()
|
||
for p, cfg in PAIRS.items():
|
||
cfg["wire"]()
|
||
for u in chosen(p):
|
||
tr = key[f"{p}|{u['uid']}"]
|
||
after, _ = plant(C.base_draft(u["uid"]), u["uid"])
|
||
print(f"\n--- {p}/{u['uid']} ---")
|
||
print(f" УДАЛЁН абзац ({len(tr.get('dropped') or '')} знаков): "
|
||
f"{(tr.get('dropped') or '—')[:160]}…")
|
||
print(f" редкие слова-улики: {' '.join(_uniq_words(tr, after)) or '⛔ НЕТ'}")
|
||
print(f" ОТРИЦАНИЕ: «{tr.get('negation_before', '—')}» → "
|
||
f"«{tr.get('negation_after', '—')}»")
|
||
|
||
|
||
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}: глав ровно {N_UNITS}", len(us) == N_UNITS, f"их {len(us)}")
|
||
ck(f"{p}: отбор ДЕТЕРМИНИРОВАН", [u["uid"] for u in chosen(p)] == [u["uid"] for u in us])
|
||
u = us[0]
|
||
src, draft = u["source"], C.base_draft(u["uid"])
|
||
# ⚠ ГЛАВНОЕ ОБОСНОВАНИЕ ПАНЕЛИ — ПРОВЕРЯЕТСЯ НА ЖИВОМ СЫРЬЕ, А НЕ ВЕРИТСЯ КОММЕНТАРИЮ.
|
||
# «У китайской пары исходник уже разбит» — если это перестанет быть правдой, панель из
|
||
# двух вариантов молча станет неполной.
|
||
per_line = st.median([len(x) for x in src.split("\n") if x.strip()])
|
||
already = per_line < 60
|
||
ck(f"{p}: исходник {'УЖЕ разбит' if already else 'сплошной'} — "
|
||
f"возврат абзацев {'НЕ нужен' if already else 'нужен'}, вариантов {2 if already else 4}",
|
||
len(cfg["vars"]) == (2 if already else 4) and bool(cfg["reflow"]) == (not already),
|
||
f"медиана строки {per_line:.0f} знаков")
|
||
if cfg["reflow"]:
|
||
# ⚠ ВОЗВРАТ АБЗАЦЕВ ПРОВЕРЯЕТСЯ НА ВСЕХ ВЗЯТЫХ ГЛАВАХ, а не на первой: единица,
|
||
# которая не нашлась в книге, сделала бы вариант близнецом базы — оплаченным дважды.
|
||
got = [cfg["reflow"](x["source"]) for x in us]
|
||
ck(f"{p}: авторские абзацы нашлись для ВСЕХ глав", all(g.strip() for g in got),
|
||
f"нашлось {sum(1 for g in got if g.strip())} из {len(got)}")
|
||
sp = got[0]
|
||
ck(f"{p}: возврат абзацев реально дробит исходник",
|
||
sp.count("\n") >= 5, f"строк {src.count(chr(10)) + 1} → {sp.count(chr(10)) + 1}")
|
||
ck(f"{p}: возврат абзацев НЕ теряет и НЕ добавляет знаков",
|
||
re.sub(r"\s+", "", sp) == re.sub(r"\s+", "", src),
|
||
f"{len(re.sub(r'[^А-Яа-яA-Za-z]', '', sp))} против "
|
||
f"{len(re.sub(r'[^А-Яа-яA-Za-z]', '', src))} букв")
|
||
# четыре варианта обязаны быть ПОПАРНО РАЗНЫМИ запросами, иначе часть сметы — близнецы
|
||
msgs = {v: json.dumps(crit_msgs(p, src, draft, v), ensure_ascii=False)
|
||
for v in cfg["vars"]}
|
||
ck(f"{p}: варианты попарно различны", len(set(msgs.values())) == len(msgs))
|
||
# база обязана быть ПОБАЙТНО боевым критиком — иначе меряем два разных промта
|
||
ck(f"{p}: P0 — побайтно боевой критик",
|
||
msgs["P0"] == json.dumps(C.crit_msgs(src, draft), ensure_ascii=False))
|
||
ck(f"{p}: ФРАЗА уходит в системный промт и только туда",
|
||
PARALLEL_NOTE.strip()[:40] in json.loads(msgs["PF"])[0]["content"]
|
||
and PARALLEL_NOTE.strip()[:40] not in json.loads(msgs["PF"])[1]["content"])
|
||
ck(f"{p}: ФРАЗА не обещает построчного соответствия",
|
||
"предложение за предложением" not in PARALLEL_NOTE.lower()
|
||
and "НЕ ищи точных пар" in PARALLEL_NOTE)
|
||
ck(f"{p}: черновик во всех вариантах ОДИН И ТОТ ЖЕ",
|
||
len({json.loads(m)[1]["content"].split("ЧЕРНОВОЙ ПЕРЕВОД:")[-1]
|
||
for m in msgs.values()}) == 1)
|
||
# посадки: обе садятся, обе детерминированы, обе оставляют текст читаемым
|
||
after, tr = plant(draft, u["uid"])
|
||
ck(f"{p}: посадка ДЕТЕРМИНИРОВАНА", plant(draft, u["uid"])[0] == after)
|
||
# ⚠ ПО ВСЕМ ВЗЯТЫМ ГЛАВАМ, А НЕ ПО ПЕРВОЙ. Вычитка перед покупкой поймала посадку в 1602
|
||
# знака на четвёртой английской главе, тогда как на первой всё было в порядке: гейт,
|
||
# смотрящий на `us[0]`, сертифицировал бы негодный замер.
|
||
alld = [plant(C.base_draft(x["uid"]), x["uid"]) for x in us]
|
||
ck(f"{p}: абзац удалён на КАЖДОЙ главе",
|
||
all(t.get("dropped") and t["dropped"] not in a for a, t in alld),
|
||
" · ".join(str(len(t.get("dropped") or "")) for _a, t in alld) + " знаков")
|
||
ck(f"{p}: отрицание снято на КАЖДОЙ главе",
|
||
all(t.get("negation_before") and t["negation_before"] not in a for a, t in alld),
|
||
" · ".join(f"«{t.get('negation_before')}»" for _a, t in alld))
|
||
ck(f"{p}: у пропажи есть редкие слова-улики на КАЖДОЙ главе",
|
||
all(len(_uniq_words(t, a)) >= 2 for a, t in alld),
|
||
" · ".join(str(len(_uniq_words(t, a))) for a, t in alld))
|
||
ck(f"{p}: пропажа ТОНКАЯ — не больше четверти черновика НИ НА ОДНОЙ главе",
|
||
all(0 < len(C.base_draft(x["uid"])) - len(a)
|
||
<= len(C.base_draft(x["uid"])) * MAX_DROP_SHARE
|
||
for x, (a, _t) in zip(us, alld, strict=True)),
|
||
" · ".join(f"{(len(C.base_draft(x['uid'])) - len(a)) / max(1, len(C.base_draft(x['uid']))):.0%}"
|
||
for x, (a, _t) in zip(us, alld, strict=True)))
|
||
# ⚠ ДЕТЕКТОР ОБЯЗАН УМЕТЬ ПАДАТЬ. Гейт, который не может сказать «не нашёл», не сторожит
|
||
# ничего — норма, заведённая пустым контролем пака 23.
|
||
tr2 = dict(tr, _uniq=_uniq_words(tr, after))
|
||
ck(f"{p}: детектор пропажи МОЛЧИТ на пустом ответе", not _caught_drop("", tr2))
|
||
ck(f"{p}: детектор пропажи СРАБАТЫВАЕТ на тексте самого абзаца",
|
||
_caught_drop((tr.get("dropped") or "").lower(), tr2))
|
||
ck(f"{p}: детектор отрицания МОЛЧИТ на пустом ответе", not _caught_neg("", tr))
|
||
ck(f"{p}: детектор отрицания СРАБАТЫВАЕТ на прямой формулировке",
|
||
_caught_neg(f"«{tr.get('negation_after')} …» → снято отрицание", tr))
|
||
# теги: покрыты глобом своей кассы, различны по варианту и по модели
|
||
tgs = {tag(p, v, m, u["uid"]) for v in cfg["vars"] for m in MODELS}
|
||
ck(f"{p}: теги различны по варианту и модели",
|
||
len(tgs) == len(cfg["vars"]) * len(MODELS))
|
||
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}: нулевая модель детектора обеспечена (клетки критика по чистому входу есть)",
|
||
all((OUT / f"{C.tag(cfg['crit'], x['uid'], 'crit')}.json").exists() for x in us))
|
||
ck(f"{p}: порядок вариантов внутри главы перетасован, а не алфавитный",
|
||
len(cfg["vars"]) < 3 or any(var_order(p, x["uid"], MODELS[0]) != list(cfg["vars"])
|
||
for x in us))
|
||
ck("пары не делят буквы тегов", len({v["letter"] for v in PAIRS.values()}) == len(PAIRS))
|
||
ck("glm-5 идёт с ВКЛЮЧЁННЫМ размышлением (объявленное отступление)",
|
||
"extra_body" not in call_kwargs("glm-5", [{"role": "user", "content": "x"}]))
|
||
ck("deepseek идёт в БОЕВОЙ конфигурации, без отступлений",
|
||
call_kwargs("deepseek-v4-pro", [{"role": "user", "content": "x"}])
|
||
== ROSTER.call_kwargs("deepseek-v4-pro", [{"role": "user", "content": "x"}],
|
||
max_out=MAX_OUT))
|
||
ck("правильный ответ пишется ДО покупок и не перезаписывается",
|
||
"if TRUTH.exists()" in Path(__file__).read_text(encoding="utf-8"))
|
||
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] == "--plants":
|
||
cmd_plants()
|
||
elif a[0] == "--score":
|
||
score()
|
||
elif a[0] == "--buy":
|
||
for x in ([y for y in a[1:] if y in PAIRS] or list(PAIRS)):
|
||
buy(x)
|
||
else:
|
||
raise SystemExit(f"⛔ неизвестный режим {a[0]!r}; "
|
||
"знаю --dry --selftest --plants --buy --score")
|