Freeze phase D edit-contour harness: fixer sees only flagged places, bank law on every arm including the critic, isolation selftest

This commit is contained in:
Claude (backend session) 2026-08-10 12:19:31 +03:00
parent da5f4d0224
commit 488d1d0b8d

277
eval/dovodka/contour.py Normal file
View file

@ -0,0 +1,277 @@
#!/usr/bin/env python3
"""Д4 — EDIT-КОНТУР НА zh. Покупает армы A1A4 и 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()