#!/usr/bin/env python3 """Ф2б, армы E и G — обратная связка и маршрутизация. Обе топологии считаются на ОДНОМ материале. E ОБРАТНАЯ СВЯЗКА: сильный черновик (однопроходка `deepseek-v4-pro`, арм D фазы 2а) → дешёвый фиксер по флагу. Проверяет прямо противоположную нынешней ставку: не «дешёвый черновик + дорогой редактор», а «дорогой черновик + дешёвая добивка». G МАРШРУТИЗАЦИЯ: дешёвый черновик → детерминированные гейты скорят КАЖДУЮ единицу → стиль-пасс уходит ТОЛЬКО флагнутым. Проверяет, можно ли не платить за переписывание там, где переписывать нечего. Почему обе считаются здесь, а не по отдельности. Ф2а показала, что связка «черновик + редактор» доминируется однопроходкой по цене (0.80×) и не выигрывает у неё по качеству. Значит вопрос сместился: не «нужен ли второй проход», а «можно ли платить за него ИЗБИРАТЕЛЬНО». E и G — два разных способа платить избирательно, и сравнивать их надо с уже намеренными A, D и F. ⚠ Форма арма G исправлена против буквы промта по результату калибровки §2.5, и это объявлено там же. Промт предписывал «принимать правку, только если внешний гейт видит улучшение»; замер на 91 реальной правке показал, что гейт улучшения НЕ ВИДИТ (47%, p=0.675) при том, что порчу видит решительно. Правило в исходной форме отсекало бы половину полезной работы по причине, которую гейт измерить не в состоянии. Рабочая форма обратная: правка принимается по умолчанию, отвергается только при УХУДШЕНИИ сверх порога. Запуск: eval/.venv/bin/python eval/role_topology/route_arm.py --plan # что флагается, $0 eval/.venv/bin/python eval/role_topology/route_arm.py """ from __future__ import annotations import json import os import statistics import sys import time from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "editor_contract")) sys.path.insert(0, str(REPO / "eval" / "bank_arbitration")) sys.path.insert(0, str(REPO / "eval" / "editor_harness")) sys.path.insert(0, str(REPO / "eval" / "role_topology")) from dotenv import load_dotenv # noqa: E402 from openai import OpenAI # noqa: E402 import bakeoff as BO # noqa: E402 import battery as BAT # noqa: E402 import detect_word as DW # noqa: E402 import repair_arm as RA # noqa: E402 from prices import CANDIDATES # noqa: E402 load_dotenv(REPO / "eval" / ".env") OUT = Path.home() / "books" / "role-topology" CEILING_USD_МЁРТВ = None # ⚠ объявлен и НИКЕМ не читается; # оставлен видимым маркером: охраны здесь нет, покупки идут мимо кассы (ревью 07.08) DRAFT_COST = 0.00195 # медиана flash(low) на переводческой роли, замер §2.7 EDITOR = "deepseek-v4-pro" def gate_flags(source: str, text: str, bank: frozenset) -> list[str]: """ДЕТЕРМИНИРОВАННЫЙ гейт единицы: что в ней объективно не так. Ровно те классы, которые батарея Ф0.5 умеет находить БЕЗ судьи и без модели — иначе маршрутизация стоила бы денег.""" why = [] r = BAT.run_unit(BAT.Unit(source=source, draft="", final=text)) if r["cjk_leak"]["han_chars"]: why.append(f"утечка иероглифов ({r['cjk_leak']['han_chars']})") if r["typography"]["violations"]: why.append(f"типографика ({r['typography']['violations']})") if r["numbers"]["lost_n"] or r["numbers"]["invented_n"]: why.append(f"величины (−{r['numbers']['lost_n']}/+{r['numbers']['invented_n']})") if r["palladius"]["nonconformant_text"]: why.append(f"не-палладиевские имена ({r['palladius']['nonconformant_text']})") bad = [w for w in BAT.words(text) if DW.verdict(w, bank)[0]] if bad: why.append(f"выдуманные слова ({len(bad)})") return why def main() -> None: plan_only = "--plan" in sys.argv bank = DW.load_bank() us = BO.units() print("ГЕЙТ-СКОРИНГ КАЖДОЙ ЕДИНИЦЫ (детерминированный, $0)\n") print(f"{'ед.':4s}{'F черновик':>42s}{'D однопроходка':>42s}") print("-" * 90) flagged_F, flagged_D = [], [] for ui, u in enumerate(us): f_why = gate_flags(u["source"], u["draft"], bank) d_txt = BO.text_of("D", u, ui) d_why = gate_flags(u["source"], d_txt, bank) if d_txt.strip() else ["нет выхода"] if f_why: flagged_F.append(ui) if d_why: flagged_D.append(ui) print(f"u{ui:<3d}{('; '.join(f_why) or 'чисто'):>42s}{('; '.join(d_why) or 'чисто'):>42s}") print(f"\nфлагнуто гейтом: черновик F {len(flagged_F)}/{len(us)} · " f"однопроходка D {len(flagged_D)}/{len(us)}") # ЭКОНОМИКА МАРШРУТИЗАЦИИ считается детерминированно, без единого вызова: цена единицы известна # из замеров Ф2а, а доля флагнутых — из гейта выше. ed = statistics.median(json.loads((OUT / f"bo-A-u{i}.json").read_text(encoding="utf-8")) ["cost_usd"] for i in range(len(us)) if (OUT / f"bo-A-u{i}.json").exists()) d_cost = statistics.median(json.loads((OUT / f"bo-D-u{i}.json").read_text(encoding="utf-8")) ["cost_usd"] for i in range(len(us)) if (OUT / f"bo-D-u{i}.json").exists()) share = len(flagged_F) / len(us) print(f"\n{'топология':46s}{'$/единицу':>11s}{'против A':>10s}") print("-" * 68) rows = [ ("A — черновик + редактор ВСЕГДА (боевая)", DRAFT_COST + ed), ("D — однопроходка сильной моделью", d_cost), (f"G — черновик + редактор ТОЛЬКО флагнутым ({share:.0%})", DRAFT_COST + ed * share), ("E — однопроходка + дешёвая починка по флагу", d_cost + 0.000275 * 2), ("F — черновик как есть", DRAFT_COST), ] base = rows[0][1] for name, v in rows: print(f"{name:46s}{v:11.5f}{v / base:9.2f}×") if plan_only: return # АРМ E: сильный черновик прогоняется через те же реальные детекторы и чинится дешёвым фиксером. cl = OpenAI(api_key=os.environ[CANDIDATES[RA.FIXER][1]], base_url=CANDIDATES[RA.FIXER][0], timeout=300) spent = 0.0 fixed = found = 0 for ui, u in enumerate(us): d_txt = BO.text_of("D", u, ui) if not d_txt.strip(): continue hits = RA.detect(d_txt, bank) found += len(hits) for hi, h in enumerate(hits): rec = RA.fix(cl, u["source"], h["sentence"], h["cls"], f"e-u{ui}-{hi}") spent += rec["cost_usd"] fixed += bool(rec["content"].strip()) time.sleep(0.15) print(f"\nАРМ E: реальные детекторы нашли в сильном черновике {found} дефектов, " f"починено {fixed}, потрачено ${spent:.6f}") print("⇒ сравнивать с армом C: там на ДЕШЁВОМ черновике те же детекторы находили 8 из 8 " "посаженных;\n здесь счёт идёт по ЕСТЕСТВЕННЫМ дефектам, и их число само есть " "мера качества базы.") (OUT / "route-arm.json").write_text(json.dumps( dict(flagged_F=flagged_F, flagged_D=flagged_D, e_found=found, e_fixed=fixed, e_cost=spent), ensure_ascii=False), encoding="utf-8") if __name__ == "__main__": main()