#!/usr/bin/env python3 """Арм SCOPED REPAIR: починка по УЖЕ НАЙДЕННОМУ спану — механизм, которого у нас нет. `research/22:176` фиксирует раскол дословно: «Checker→typed-error→scoped repair | AiNiee `ProofreadTask.py:250`; LinguaGacha retrans-by-keyword | у нас НЕТ (флагаем, не чиним)». Что проверяется. Мои же замеры дали, что цену редакторского вызова держит РЕШЕНИЕ, а не формат: арм `locate` (только найди, ничего не чини) стоил 2.14× от полного переписывания на deepseek-v4-pro. Если так — починка по указанному месту обязана быть на порядок дешевле: модели не нужно ни искать, ни держать в голове весь текст, ей дают одно предложение и тип ошибки. Материал — те же 12 посаженных дефектов пробы 18 (§0.5): 6 несуществующих слов (к1) и 6 смысловых инверсий (к2). Эталон известен посимвольно, поэтому починка скорится детерминированно, без судьи, теми же регексами `fix_rx`/`bad_rx`, что валидированы ревью посылки (12/12 корректны). ⚠ Объявляется заранее: арм меряет починку при ИДЕАЛЬНОЙ детекции. Он НЕ отвечает, найдут ли гейты эти дефекты сами — гейта на «несуществующее слово» и на смысловую инверсию у нас нет. Поэтому результат читается как ВЕРХНЯЯ ГРАНИЦА схемы, а не как её цена в проде. """ from __future__ import annotations import json import re import sys import time from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") HERE = Path(__file__).resolve().parent sys.path.insert(0, str(REPO / "eval" / "editor_contract")) sys.path.insert(0, str(REPO / "eval" / "bank_arbitration")) sys.path.insert(0, str(HERE)) import probe as Q # noqa: E402 import editor_wire_probe as P # noqa: E402 from inject_probe import pick_windows # noqa: E402 RAW = Q.RAW TYPE = {"k1": "несуществующее слово (модель выдумала слово, которого в русском языке нет)", "k2": "смысловое искажение против исходника (сказано обратное или иное, чем в оригинале)"} def sentence_with(text: str, needle: str) -> str | None: """Предложение, содержащее посаженный фрагмент. Границы — по .!?… и переносу строки.""" i = text.find(needle) if i < 0: return None starts = [m.end() for m in re.finditer(r"[.!?…]\s+|\n", text[:i])] s = starts[-1] if starts else 0 m = re.search(r"[.!?…](\s|$)|\n", text[i + len(needle):]) e = i + len(needle) + (m.end() if m else 0) return text[s:e].strip() def plan() -> list[tuple]: out = [] for k, (_, buf, _) in enumerate(pick_windows()): planted, _ = P.planted_draft(k, P.draft_of(k)) for cls, old, new, bad_rx, fix_rx in P.DEFECTS[k]: sent = sentence_with(planted, new) if not sent: print(f" ⚠ w{k}/{cls}: не выделил предложение вокруг посадки — пропуск") continue # ⚠ Селф-ревью 05.08: у части посадок дефект ДОБАВЛЯЕТ слово, не заменяя ключевое # («мреязное СОКРОВИЩЕ природы»), поэтому `fix_rx` совпадает с испорченным предложением # ЕЩЁ ДО починки. В таких клетках «восстановлен» не несёт информации — помечаем их и # держим вне строгого знаменателя, иначе бездействие модели засчиталось бы как успех. degenerate = bool(re.search(fix_rx, sent)) out.append((k, cls, sent, buf, bad_rx, fix_rx, old, degenerate)) return out def main(models: list[str]) -> None: pl = plan() print(f"починок в плане: {len(pl)} (ожидается 12)") tpl = HERE / "prompts" / "repair.md" for m in models: short = {"deepseek-v4-pro": "dp", "glm-5": "gl", "gpt-5.6-luna": "lu", "grok-4.3": "gr"}[m] nothink = Q.MODELS[m][5] in ("disable", "effort_none") rows, spent = [], 0.0 for k, cls, sent, buf, bad_rx, fix_rx, old, degen in pl: flagged = f"{TYPE[cls]}\n\nПроблемное предложение перевода:\n{sent}" sys_msg, user = Q.render(tpl, buf, flagged) rec = Q.call(m, [{"role": "system", "content": sys_msg}, {"role": "user", "content": user}], f"rp-{short}-w{k}-{cls}", nothink) spent += rec["cost_usd"] got = rec["content"].strip() rows.append(dict(window=k, cls=cls, degenerate=degen, still=bool(re.search(bad_rx, got)), restored=bool(re.search(fix_rx, got)), cost=rec["cost_usd"], out_tokens=rec["completion_tokens"], reasoning=rec["reasoning_chars"], chars=len(got))) time.sleep(0.2) inf = [r for r in rows if not r["degenerate"]] snyat = sum(1 for r in rows if not r["still"]) strict = sum(1 for r in inf if not r["still"] and r["restored"]) print(f"\n{m}: СНЯТ дефект {snyat}/{len(rows)} · " f"ВОССТАНОВЛЕН строго {strict}/{len(inf)} (клеток с информативным эталоном {len(inf)} " f"из {len(rows)}) · ${spent:.6f}, ${spent / max(len(rows), 1):.6f} за починку") for cls in ("k1", "k2"): sub = [r for r in rows if r["cls"] == cls] si = [r for r in sub if not r["degenerate"]] if sub: print(f" {cls}: снят {sum(1 for r in sub if not r['still'])}/{len(sub)}" + (f" · восстановлен {sum(1 for r in si if not r['still'] and r['restored'])}/{len(si)}" if si else "")) (RAW / f"repair-{short}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8") if __name__ == "__main__": main(sys.argv[1:] or ["deepseek-v4-pro"])