#!/usr/bin/env python3 """Д4 — EDIT-КОНТУР НА zh. Покупает армы A1–A4 и A6 на 32 единицах. Платный. Вопрос фазы (ставка владельца H-2б): нужен ли ПОЛНЫЙ ПЕРЕПИС, или дешевле и лучше найти проблемные места и починить точечно. Армы объявлены в пре-реге Д0.5 и здесь не меняются: A0 боевой full-regen (deepseek-v4-pro поверх якорного черновика) КУПЛЕН обоими паками, $0 A1 черновик + ДЕТЕРМИНИРОВАННЫЕ флаги (батарея + канон-гейт) → точечный фиксер A2 dspro-однопроходка УРАВНЕННОГО мандата (строка 153 бэклога) — редактора нет вовсе A3 черновик + СМЫСЛОВОЙ LLM-критик («прочитай, пойми, найди всё проблемное») → фиксер A4 A0 + канон-фиксер ПОСЛЕ (полировка: чинит потери канона боевого редактора) A6 ДРУГАЯ БАЗА: черновик dspro эксп-22 + оба контура — прямой носитель H-1 ⚠ ПОЧЕМУ ФИКСЕР ВИДИТ ТОЛЬКО МЕСТА, А НЕ ВЕСЬ ТЕКСТ. Если дать фиксеру полный мандат, он станет редактором, и контраст «точечно против переписа» исчезнет. Поэтому вход фиксера — черновик плюс СПИСОК МЕСТ, и в промте прямо запрещено трогать что-либо ещё. Это и есть измеряемая позиция контура; нарушение её — не оптимизация, а подмена арма. ⚠ ЧТО ДЕЛАЕТ КАНОН-ГЕЙТ. Детерминированная сверка покрытия банка после КАЖДОГО арма: он и флагует потери для фиксера (вход A1/A4), и печатается отдельной осью результата. Политика владельца 10.08: потеря канона = дефект безусловно; художественность — отдельная ось. Запуск: contour.py --plan | --selftest | --run [A1 A2 A3 A4 A6] | --floor """ from __future__ import annotations import importlib.util import json 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 MONEY = _load("money_d", ZONE / "money_d.py") PAN23 = _load("panel23", REPO / "eval" / "editor_tier" / "panel.py") P22, MAT = PAN23.P22, PAN23.MAT PR = _load("prompts22", REPO / "eval" / "tenant_panel" / "prompts.py") BANK = _load("bank22", REPO / "eval" / "tenant_panel" / "bank.py") ROSTER = _load("roster22", REPO / "eval" / "tenant_panel" / "roster.py") ED22 = _load("editors22", REPO / "eval" / "tenant_panel" / "editors.py") # ⚠⚠ КАССУ ПЕРЕ-НАСТРАИВАЕМ ПОСЛЕ ВСЕХ ЧУЖИХ ЗАГРУЗОК, И ЭТО НЕ ПЕДАНТИЗМ. # Модуль `buy` — СИНГЛТОН в `sys.modules`, и побеждает тот, кто настроил его последним. `money_d` # ставил `BUY.OUT` на каталог фазы, а грузившийся следом `panel.py` пака 23 тянул его кассу и # перебивал обратно на `~/books/editor-tier`. Итог был бесшумным и дорогим: кэш-чек, замок и # `finally` смотрели в каталог фазы, а РЕЗУЛЬТАТ писался в каталог чужого пака. Семь оплаченных # клеток ушли туда, касса фазы показывала $0, а цикл покупки 20 минут ждал появления файла, # который в её каталоге не появился бы никогда. Найдено многоагентным разбирательством 10.08. MONEY.M22.configure(ZONE=ZONE.parent / "dovodka", OUT=MONEY.OUT, TAGS=MONEY.TAGS, CEILINGS=MONEY.CEILINGS, PACK_CEILING=MONEY.PACK_CEILING, GLOB_ALL="dv-*.json", ROSTER=ROSTER) if MONEY.M22.BUY.OUT != MONEY.OUT: # ассерт, а не надежда raise SystemExit(f"⛔ buy.OUT={MONEY.M22.BUY.OUT} против кассы {MONEY.OUT} — " "покупки уедут в чужой каталог, СТОП") OUT = MONEY.OUT ANCHOR = "deepseek-v4-flash" # якорный черновик FIXER = "deepseek-v4-pro" # фиксер и критик — боевой редакторский движок DRAFT2 = "deepseek-v4-pro" # «качественный черновик» H-1 (арм D2 эксп-22) ARMS = ("A1", "A2", "A3", "A4", "A6") # ── единицы: 16 эксп-22 + 16 эксп-23, база у каждой своя и уже куплена ──────────────────────── def units() -> list[dict]: return P22.M.units_zh() + MAT.units_new() def base_draft(uid: str) -> str: """Якорный черновик единицы. У старых — из сырья эксп-22, у новых — из фазы B эксп-23.""" t = P22.draft_text(ANCHOR, uid) return t if t.strip() else PAN23.draft_b(uid) # новые единицы — фаза B пака 23 def base_a0(uid: str) -> str: """Боевой full-regen над якорной базой — арм A0, куплен, $0.""" t = P22.edit_text(ANCHOR, uid) return t if t.strip() else PAN23.text_b("R0", uid) def base_draft2(uid: str) -> str: """«Качественный черновик» H-1: черновик dspro эксп-22. Есть только у 16 старых единиц.""" return P22.draft_text(DRAFT2, uid) # ── флаги: детерминированные места, которые фиксер обязан починить ──────────────────────────── def canon_gaps(src: str, out: str) -> list[str]: """Термины банка, чья канонная форма В ВЫХОДЕ встречается реже, чем в исходнике.""" import re # noqa: PLC0415 gaps = [] for t, v in BANK.PR.bank().items(): n = src.count(t) if not n: continue if min(len(re.findall(v["rx"], out, re.I)), n) < n: gaps.append(f"«{v['dst']}» (в исходнике {t} встречается {n} раз)") return gaps def flags_of(u: dict, draft: str) -> list[str]: """Список мест для A1. Только ДЕТЕРМИНИРОВАННОЕ: канон-гейт + батарея, без судьи и без LLM.""" import battery as B # noqa: PLC0415 out = [] for g in canon_gaps(u["source"], draft): out.append(f"КАНОН: термин {g} передан не канонной формой либо пропущен") try: r = B.run_unit(dict(uid=u["uid"], source=u["source"], text=draft)) for k, v in (r or {}).items(): if isinstance(v, (int, float)) and v and k not in ("uid", "len"): out.append(f"БАТАРЕЯ: проверка «{k}» дала {v}") except Exception: # noqa: BLE001, S110 pass # батарея не обязана знать наш вход return out # ── промты армов: данные, не код ────────────────────────────────────────────────────────────── FIX_HEAD = ( "Ты — редактор-корректор. Тебе дан ИСХОДНИК, его ЧЕРНОВОЙ ПЕРЕВОД и СПИСОК МЕСТ, где перевод " "неверен.\n\n" "Почини ТОЛЬКО перечисленные места. Всё остальное оставь ПОБАЙТНО как есть: не переписывай " "фразы, не улучшай стиль, не меняй вёрстку, не трогай пунктуацию там, где о ней не сказано. " "Если место починить нельзя, не трогая соседнего текста, — почини минимально возможной " "правкой.\n\nВыведи ТОЛЬКО полный текст перевода после правок, без преамбул и комментариев.") CRIT_HEAD = ( "Ты — литературный редактор-эксперт. Тебе дан ИСХОДНИК и его ЧЕРНОВОЙ ПЕРЕВОД.\n\n" "ПРОЧИТАЙ оба текста и найди ВСЁ проблемное в переводе: искажения и потери смысла, неверно " "понятые реалии, кальки, канцелярит, несуществующие слова, рассогласование, сбитую вёрстку " "реплик, непоследовательную передачу имён и терминов.\n\n" "Выведи СПИСОК МЕСТ, по одному в строке, в формате:\n" "- <цитата из перевода> → <в чём ошибка>\n\n" "Только список. Не переписывай текст, не предлагай готовый перевод целиком.") def fix_msgs(src: str, draft: str, places: list[str]) -> list[dict]: law = PR.law_block(PR.terms_in(src)) body = (f"{FIX_HEAD}\n\n{law}\n\n---USER---\nИСХОДНИК:\n{src}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}\n\n" "МЕСТА, КОТОРЫЕ НАДО ПОЧИНИТЬ:\n" + "\n".join(f"- {p}" for p in places)) s, u = body.split("---USER---", 1) return [{"role": "system", "content": s.strip()}, {"role": "user", "content": u.strip()}] def crit_msgs(src: str, draft: str) -> list[dict]: # Критику банк-закон нужен не меньше фиксера: без него он назовёт «непоследовательным» то, # что канону как раз отвечает, и наоборот. Банк-закон D39.104 — на ВСЕХ армах, без изъятий. law = PR.law_block(PR.terms_in(src)) return [{"role": "system", "content": f"{CRIT_HEAD}\n\n{law}"}, {"role": "user", "content": f"ИСХОДНИК:\n{src}\n\nЧЕРНОВОЙ ПЕРЕВОД:\n{draft}"}] def tag(arm: str, uid: str, part: str = "") -> str: ph = "f" if arm == "A6" else "a" return f"dv-{ph}-{arm.lower()}{('-' + part) if part else ''}-{uid}" def text_of(arm: str, uid: str) -> str: f = OUT / f"{tag(arm, uid)}.json" if f.exists(): return json.loads(f.read_text(encoding="utf-8")).get("content") or "" return "" def cmd_plan() -> None: us = units() old = {u["uid"] for u in P22.M.units_zh()} print(f"единиц {len(us)} (старых {len(old)} + новых {len(us) - len(old)})") have = {a: sum(1 for u in us if text_of(a, u["uid"]).strip()) for a in ARMS} for a in ARMS: n = len(old) if a == "A6" else len(us) print(f" {a}: куплено {have[a]:2d} из {n}") base = sum(1 for u in us if base_draft(u["uid"]).strip()) a0 = sum(1 for u in us if base_a0(u["uid"]).strip()) d2 = sum(1 for u in us if base_draft2(u["uid"]).strip()) print(f"базы (уже куплены, $0): черновик {base}/{len(us)} · A0 {a0}/{len(us)} · " f"черновик-dspro {d2}/{len(old)}") MONEY.report() 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 "")) us = units() ck("единиц ровно 32", len(us) == 32, str(len(us))) ck("uid уникальны", len({u["uid"] for u in us}) == len(us)) miss = [u["uid"] for u in us if not base_draft(u["uid"]).strip()] ck("якорный черновик есть у всех", not miss, f"нет у {miss[:3]}") miss0 = [u["uid"] for u in us if not base_a0(u["uid"]).strip()] ck("арм A0 (боевой перепис) есть у всех", not miss0, f"нет у {miss0[:3]}") old = P22.M.units_zh() miss2 = [u["uid"] for u in old if not base_draft2(u["uid"]).strip()] ck("черновик-dspro для H-1 есть у всех 16 старых", not miss2, f"нет у {miss2[:3]}") # ИЗОЛЯЦИЯ: у A1 и A3 вход ОДИН И ТОТ ЖЕ черновик; различаться обязан только список мест u = us[0] d = base_draft(u["uid"]) m1 = fix_msgs(u["source"], d, ["КАНОН: тест"]) m3 = fix_msgs(u["source"], d, ["СМЫСЛ: тест"]) ck("A1 и A3 делят вход, различаясь только списком мест", m1[0]["content"] == m3[0]["content"] and m1[1]["content"] != m3[1]["content"]) ck("в промте фиксера стоит запрет трогать остальное", "ПОБАЙТНО как есть" in m1[0]["content"]) # ⚠ Банк-закон D39.104 обязателен на ВСЕХ армах с переводом/редактурой. Проверяется ДОСЛОВНО # по заголовку блока, а не по слову «закон»: эксп-22 отступил от него на 72 en-клетках. ck("банк-закон D39.104 в промте фиксера есть", "КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in m1[0]["content"], m1[0]["content"][-90:].replace("\n", " ")) ck("банк-закон есть и в промте КРИТИКА — иначе он флагует канон не по банку", "КАНОНИЧЕСКИЕ ПЕРЕВОДЫ" in crit_msgs(u["source"], d)[0]["content"]) fl = flags_of(u, d) ck("канон-гейт выдаёт места (иначе A1 нечего чинить)", isinstance(fl, list), f"мест {len(fl)}") # теги армов не пересекаются между собой и с чужими паками tags = {tag(a, u["uid"]) for a in ARMS} ck("теги армов различны", len(tags) == len(ARMS)) ck("теги покрыты глобом кассы", all(t.startswith(("dv-a-", "dv-f-")) for t in tags)) # ⚠ Главный ассерт этого харнесса: кэш-чек и ЗАПИСЬ результата смотрят в ОДИН каталог. ck("buy.OUT совпадает с кассой фазы", MONEY.M22.BUY.OUT == MONEY.OUT, f"{MONEY.M22.BUY.OUT} против {MONEY.OUT}") print(f"\n{'ХАРНЕСС ГОДЕН' if not bad else f'ПРОВАЛОВ: {bad}'}") return bad def cmd_run(which: list[str]) -> None: us = units() old = {u["uid"] for u in P22.M.units_zh()} for arm in which: led = MONEY.Guarded("ФД-F" if arm == "A6" else "ФД-A") for u in us: uid, src = u["uid"], u["source"] if arm == "A6" and uid not in old: continue if text_of(arm, uid).strip(): continue draft = base_draft2(uid) if arm == "A6" else base_draft(uid) if not draft.strip(): print(f" {uid}: базы нет — пропуск") continue if arm == "A2": # однопроходка: редактора нет msgs = PR.translator_msgs(src) if hasattr(PR, "translator_msgs") else None if msgs is None: print(" A2: сборки однопроходки нет — арм пропущен, объявить в отчёте") break MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER), ROSTER.call_kwargs(FIXER, msgs), role="onepass", uid=uid, arm=arm) continue if arm in ("A1", "A4"): target = base_a0(uid) if arm == "A4" else draft places = [f"КАНОН: термин {g} передан не канонной формой либо пропущен" for g in canon_gaps(src, target)] if arm == "A4" else flags_of(u, draft) if not places: print(f" {uid}/{arm}: мест нет — чинить нечего, клетка не покупается") continue MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER), ROSTER.call_kwargs(FIXER, fix_msgs(src, target, places)), role="fixer", uid=uid, arm=arm, places=len(places)) continue # A3 и A6: критик, затем фиксер по его списку ct = tag(arm, uid, "crit") if not (OUT / f"{ct}.json").exists(): MONEY.purchase(led, ct, FIXER, ED22.client(FIXER), ROSTER.call_kwargs(FIXER, crit_msgs(src, draft)), role="critic", uid=uid, arm=arm) cf = OUT / f"{ct}.json" if not cf.exists(): # оплачено, но файла нет — не падать print(f" {uid}/{arm}: критик не отдал файл — арм пропущен, объявить в отчёте") continue found = json.loads(cf.read_text(encoding="utf-8")).get("content") or "" places = [ln.strip("- ").strip() for ln in found.splitlines() if ln.strip().startswith("-")] if not places: print(f" {uid}/{arm}: критик не нашёл мест — фиксер не зовётся") continue MONEY.purchase(led, tag(arm, uid), FIXER, ED22.client(FIXER), ROSTER.call_kwargs(FIXER, fix_msgs(src, draft, places[:40])), role="fixer", uid=uid, arm=arm, places=len(places)) MONEY.report() if __name__ == "__main__": a = sys.argv[1:] or ["--plan"] if a[0] == "--selftest": sys.exit(1 if cmd_selftest() else 0) if a[0] == "--run": cmd_run([x for x in a[1:] if x in ARMS] or list(ARMS)) else: cmd_plan()