From 488d1d0b8d74e75db66b30b3c7c5ee11bb1a9ca6 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Mon, 10 Aug 2026 12:19:31 +0300 Subject: [PATCH] Freeze phase D edit-contour harness: fixer sees only flagged places, bank law on every arm including the critic, isolation selftest --- eval/dovodka/contour.py | 277 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 eval/dovodka/contour.py diff --git a/eval/dovodka/contour.py b/eval/dovodka/contour.py new file mode 100644 index 00000000..4192c2aa --- /dev/null +++ b/eval/dovodka/contour.py @@ -0,0 +1,277 @@ +#!/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") + +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)) + 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) + found = json.loads((OUT / f"{ct}.json").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()