#!/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] [армы…] | --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")