textmachine/eval/dovodka/zakhod.py

465 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

#!/usr/bin/env python3
"""ЗАХОД Д17 — маленький честный повтор под ПОЧИНЕННУЮ рубрику. Платный.
⚠ ЗАЧЕМ ЭТОТ ЗАХОД ВООБЩЕ. Дуга 19→23 отвечала на вопрос «какая модель лучше в роли редактора» и
ответила устойчиво. Продуктовый вопрос владельца другой — живая русская проза без translationese, —
и под него прибора не было: слепое чтение 16.08 дало на английской оси Спирмена 0.10, то есть
счёт ошибок и читаемость там НЕ СВЯЗАНЫ. Владелец 16.08 постановил: «штрафовать за вольность
нельзя, штрафовать можно только за перевирания смысла исходника». Рубрика переписана, значит
старые судейские числа с новыми несравнимы — и панель обязана быть пере-куплена и пере-сужена
целиком, вместе со СВОИМ шумовым полом.
ЧТО ПОКУПАЕТСЯ (имена армов ОДНИ на обе пары, пара живёт в данных — норматив общности §0):
ZD голый черновик БЕСПЛАТНО, уже есть (D0 / якорный черновик)
Z0 боевая связка черновик → перепис БЕСПЛАТНО, уже есть (A0 / E0) — ЭТАЛОН
ZP однопроходка уравненного мандата БЕСПЛАТНО, уже есть (A2 / E2)
Z8 ОБОГАЩЁННЫЙ ЧЕРНОВИК ПОКУПАЕТСЯ (flash) — вопрос владельца 16.08
Z8R Z8 + боевой перепис ПОКУПАЕТСЯ (pro)
Z1 критик → ПОЛНЫЙ ПЕРЕПИС по нему ПОКУПАЕТСЯ (pro) — ставка владельца в сильнейшей форме
Z7 однопроходка + канон-фиксер ПОСЛЕ ПОКУПАЕТСЯ (pro) — поправка владельца 16.08 про банк
ZF ВТОРАЯ генерация боевой связки ПОКУПАЕТСЯ (pro) — шумовой пол НОВОЙ рубрики
⚠ ПОЧЕМУ ПОЛ ПОКУПАЕТСЯ ЗАНОВО, А НЕ БЕРЁТСЯ СТАРЫЙ. Существующий пол осей снят с ТОЧЕЧНОГО
фиксера — операции почти детерминированной, дающей побайтных близнецов (в эксп-23 контроль
`CTRLfloorA` совпал с боевым армом в 16/16 и оказался пуст). Панель этого захода — панель
ПЕРЕПИСЫВАТЕЛЕЙ, и честный её пол есть ДВЕ ГЕНЕРАЦИИ ОДНОГО И ТОГО ЖЕ переписывателя. Ровно так
сделана японская нога (`JFLO`), и там это дало самый чистый вывод фазы: разница двух редакторов
(0.44) не больше разницы двух прогонов одного (0.42).
⚠ ЧЕМ Z1 ОТЛИЧАЕТСЯ ОТ ПРОИГРАВШЕГО A3. У `A3` правщик был ТОЧЕЧНЫМ: ему запрещалось трогать всё,
кроме перечисленных мест, и он двигал 0.23.7% знаков. Здесь мандат ПОЛНЫЙ — это боевой редактор,
которому замечания критика лишь ДОБАВЛЕНЫ к его обычной работе. Разложение по осям (Д14.11)
показало, что точечные контуры проигрывают ПРОЗОЙ, а не смыслом; Z1 снимает именно это ограничение.
⚠ ЗАМЕЧАНИЯ КРИТИКА ФИЛЬТРУЮТСЯ, И ЭТО ПОЧИНКА, А НЕ ОПТИМИЗАЦИЯ. `contour.critic_places` берёт
ЛЮБУЮ строку, начинающуюся с дефиса, без разбора вердикта: на китайской оси 311 строк из 1696
(18.3%) были ПОДТВЕРЖДЕНИЯМИ критика («верно», «допустимо», «не ошибка»), и фиксер получал их как
задания на правку. Ставка владельца была испытана инструментом, который на пятой части работы
правил заведомо исправное. Здесь фильтр явный, его отсев печатается числом, а старые клетки `A3`
не трогаются — они куплены и остаются как есть, с объявленным дефектом.
⚠ ГЕЙТ ПЕРЕЛОМА ЦЕНЫ DeepSeek. Вендор переводит прайс на пик/офф-пик В 16:00 UTC 16.08.2026
(сноска прайс-страницы, факт-чек 15.08 в `backend/configs/models.yaml`). До этого мгновения
июльский пин `eval/tenant_panel/roster.py` ВЕРЕН и записанная цена клетки правдива; после —
занижает втрое, и касса начнёт печатать неправду. Поэтому покупка DeepSeek после перелома
ОТКАЗЫВАЕТСЯ, пока ростер не пере-пинен. Это не осторожность, а единственное, что отличает
леджер-измеритель от леджера-фантазёра.
Запуск: zakhod.py --dry | --selftest | --buy [zh|en] [армы…] | --plan
"""
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 = {
"zh": {"para": lambda: PARA.ZH, "letter": "i", "phase": "ФД-I", "max_out": 16000,
"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 правщик — ПОЛНЫЙ редактор, и лишнее подтверждение в списке он
# просто перепишет как обычно, а потерянная находка не вернётся ничем.
# Поэтому строка выбрасывается, ТОЛЬКО если (а) принимающая формула стоит в ХВОСТЕ вердикта —
# то есть критик закончил согласием, — И (б) во всей строке нет ни одного маркера дефекта.
CONFIRM_RX = re.compile(
r"(?:вс[е]\s+верно|верно\b|правильно\b|корректн|допустим|уместн|не\s+ошибк|ошибки\s+нет|"
r"претензий\s+нет|нареканий\s+нет|приемлем|адекватн|точно\s+переда|передано\s+верно|"
r"соответствует\s+исходник|без\s+замечаний|(?:^|[\s,;—-])ок[.,;]?$)", re.IGNORECASE)
DEFECT_RX = re.compile(
r"(искаж|потер|неверн|ошибк|кальк|канцеляр|пропущ|инверт|рассоглас|подмен|выдум|отрицан|"
r"не\s+соответств|лучше\b|следует\b|надо\b|стоит\b|заменить|исправ|сух|тяжел|громозд|"
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} не бесплатный арм")
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]:
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)
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() -> 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:
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 buy(p: str, which: list[str]) -> None:
price_gate()
cfg = _pair(p)
cfg["wire"]()
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']}")
for arm in which:
for u in us:
uid, src = u["uid"], u["source"]
tg = tag(p, arm, uid)
if cell(tg).strip():
continue
if arm == "Z8":
_buy(led, tg, DRAFT_MODEL, z8_msgs(p, src), mo,
role="translator", uid=uid, arm=arm)
elif arm == "Z8R":
d = cell(tag(p, "Z8", 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()
# ── СМЕТА И ПРОВЕРКИ ──────────────────────────────────────────────────────────────────────────
PRICE = {"Z8": 0.00060, "Z8R": 0.00790, "Z1": 0.00900, "Z7": 0.00758, "ZF": 0.00790}
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
cost = need * PRICE[a]
sub += cost
print(f" {a:4s} есть {have:2d} · надо {need:2d} · ${PRICE[a]:.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
print(f"\nВЕСЬ ЗАХОД: ${tot:.4f} по действующему (июльскому) прайсу DeepSeek.")
print(f"После перелома 16:00 UTC 16.08 та же работа стоила бы ${tot * 3.0:.4f} (пик ×3).")
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)
# 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"])
ck(f"{p}: замечания критика уходят Z1 пользовательским сообщением",
z1[1]["content"].startswith(ed[1]["content"]) and len(z1[1]["content"]) > len(ed[1]["content"]))
ck(f"{p}: мандат редактуры в Z1 остался ПОЛНЫМ", "ДОБАВЛЕНИЕ к твоей обычной работе"
in z1[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
def cmd_plan() -> None:
for p in PAIRS:
us = chosen(p)
print(f"\n=== {p}: {len(us)} единиц ===")
for a in FREE + ARMS:
have = sum(1 for u in us
if (free_text(p, a, u["uid"]) if a in FREE else cell(tag(p, a, u["uid"])))
.strip())
print(f" {a:4s} {have:2d}/{len(us)}" + (" (бесплатно)" if a in FREE else ""))
MONEY.report()
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] == "--plan":
cmd_plan()
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 --plan --buy")