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