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:
parent
da5f4d0224
commit
488d1d0b8d
1 changed files with 277 additions and 0 deletions
277
eval/dovodka/contour.py
Normal file
277
eval/dovodka/contour.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue