diff --git a/eval/dovodka/contour.py b/eval/dovodka/contour.py index 8aefe243..480ee29e 100644 --- a/eval/dovodka/contour.py +++ b/eval/dovodka/contour.py @@ -73,6 +73,24 @@ FIXER = "deepseek-v4-pro" # фиксер и критик — бое DRAFT2 = "deepseek-v4-pro" # «качественный черновик» H-1 (арм D2 эксп-22) ARMS = ("A1", "A1B", "A2", "A3", "A4", "A6", "A6F") +# ⚠ ДИСПЕТЧЕР ПО ВИДУ КОНТУРА, а не по имени арма. Пре-регистрация английской оси называет армы +# E0…E6, а механизмы у них те же, что у китайских A-армов. Копировать цикл покупки ради других +# букв значит копировать и будущие расхождения; поэтому имя арма отображается в ВИД, и цикл один. +ARM_KIND = {"A1": "flags", "A1B": "flags", "A6F": "flags", "A4": "canonfix", + "A2": "onepass", "A3": "critic", "A6": "critic", + # английская ось Д3 (имена — из пре-регистрации, механизмы — те же) + "E1": "flags", "E2": "onepass", "E3": "critic", "E4": "canonfix"} + + +def phase_of(arm: str) -> str: + """Фаза кассы арма. Должна совпадать с буквой в `tag`, иначе трата уедет мимо потолка.""" + P = pair() + if P.key == "zh-ru": + return "ФД-F" if arm.startswith("A6") else "ФД-A" + if arm == "E6": + return "ФД-G" + return {"en-ru": "ФД-B", "ja-ru": "ФД-C"}[P.key] + # ⚠ ПОЧЕМУ A1 И A1B — ДВА АРМА, А НЕ ПЕРЕ-ПОКУПКА ОДНОГО. Куплённые 30 клеток `A1` получили список # мест ТОЛЬКО от канон-гейта: батарея падала на 32/32 и ошибку глотал голый except (эррата Э-4). # Переименовать их значило бы разойтись с полем `tag` внутри самих записей, а править оплаченное @@ -114,9 +132,10 @@ def pair(): return _PAIR -def configure(p, units_provider=None) -> None: +def configure(p, units_provider=None, base_draft_hook=None, base_a0_hook=None) -> None: """Перевести контур на пару `p`. Для zh — проверенный no-op (селфтест это стережёт).""" - global _PAIR, _UNITS + global _PAIR, _UNITS, BASE_DRAFT_HOOK, BASE_A0_HOOK + BASE_DRAFT_HOOK, BASE_A0_HOOK = base_draft_hook, base_a0_hook _PAIR, _UNITS = p, units_provider B.configure_pair(SOURCE_VALUES_HOOK=p.values_in_source, NAME_CONFORMANT_TEXT_HOOK=p.name_conformant_text, @@ -149,6 +168,12 @@ def draft_defect(text: str) -> str: return BO.draft_reject_reason(text or "") +# Источники баз: у zh — сырьё прошлых паков, у новой пары их даёт драйвер оси. Умолчания +# сохраняют китайское поведение побайтно (стережёт селфтест «проводка zh — no-op»). +BASE_DRAFT_HOOK = None +BASE_A0_HOOK = None + + def base_draft(uid: str) -> str: """Якорный черновик единицы. У старых — из сырья эксп-22, у новых — из фазы B эксп-23. @@ -158,6 +183,8 @@ def base_draft(uid: str) -> str: `63ab55ac5c`: на ней куплены клетки армов A1 и A3 поверх черновика, который штатный гейт проекта бракует. Здесь ветки уравнены; брак не возвращается молча, а называется вслух. """ + if BASE_DRAFT_HOOK is not None: + return BASE_DRAFT_HOOK(uid) t = P22.draft_text(ANCHOR, uid) if t.strip(): return t @@ -171,6 +198,8 @@ def base_draft(uid: str) -> str: def base_a0(uid: str) -> str: """Боевой full-regen над якорной базой — арм A0, куплен, $0.""" + if BASE_A0_HOOK is not None: + return BASE_A0_HOOK(uid) t = P22.edit_text(ANCHOR, uid) return t if t.strip() else PAN23.text_b("R0", uid) @@ -377,10 +406,15 @@ def tag(arm: str, uid: str, part: str = "") -> str: # ⚠ Тег несёт пару: без неё кэш `purchase` отдал бы оплаченную КИТАЙСКУЮ клетку под видом # английской, за $0 и без предупреждения. У zh префикс пустой — теги уже купленных клеток # обязаны остаться прежними. + # Буква в теге — ФАЗА КАССЫ, а не пара: глоб `dv-b-*` покрывает ФД-B, `dv-a-*` — ФД-A. + # Пар-префикс, стоявший здесь до 14.08, давал `dv-en-…`, который не покрыт ни одним глобом, + # и покупка отверглась бы гейтом кассы — лучше так, чем мимо потолка, но работать нельзя. P = pair() - pk = "" if P.key == "zh-ru" else P.key.split("-")[0] + "-" - ph = "f" if arm.startswith("A6") else "a" - return f"dv-{pk}{ph}-{arm.lower()}{('-' + part) if part else ''}-{uid}" + if P.key == "zh-ru": + ph = "f" if arm.startswith("A6") else "a" + else: + ph = "g" if arm == "E6" else {"en-ru": "b", "ja-ru": "c"}[P.key] + return f"dv-{ph}-{arm.lower()}{('-' + part) if part else ''}-{uid}" def text_of(arm: str, uid: str) -> str: @@ -572,7 +606,8 @@ def cmd_run(which: list[str]) -> None: us = units_ok() old = {u["uid"] for u in P22.M.units_zh()} for arm in which: - led = MONEY.Guarded("ФД-F" if arm.startswith("A6") else "ФД-A") + led = MONEY.Guarded(phase_of(arm)) + kind = ARM_KIND[arm] for u in us: uid, src = u["uid"], u["source"] if arm.startswith("A6") and uid not in old: @@ -583,13 +618,13 @@ def cmd_run(which: list[str]) -> None: if not draft.strip(): print(f" {uid}: базы нет — пропуск") continue - if arm == "A2": # однопроходка уравненного мандата + if kind == "onepass": # однопроходка уравненного мандата _buy(led, tag(arm, uid), a2_msgs(src), role="onepass", uid=uid, arm=arm) continue - if arm in ("A1", "A1B", "A4", "A6F"): # детерминированные контуры - target = base_a0(uid) if arm == "A4" else draft + if kind in ("flags", "canonfix"): # детерминированные контуры + target = base_a0(uid) if kind == "canonfix" else draft places = ([f"КАНОН: термин {g} передан не канонной формой либо пропущен" - for g in canon_gaps(src, target)] if arm == "A4" + for g in canon_gaps(src, target)] if kind == "canonfix" else flags_of(u, target)) if not places: _free_cell(tag(arm, uid), target, role="fixer", uid=uid, arm=arm, places=0) diff --git a/eval/dovodka/en_axis.py b/eval/dovodka/en_axis.py new file mode 100644 index 00000000..96beeecc --- /dev/null +++ b/eval/dovodka/en_axis.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Д3 — НЕСУЩАЯ ОСЬ en→ru на 16 единицах Кристоффа. Платный. + +Зачем отдельным файлом. Механизмы контуров не дублируются: покупку ведёт `contour.cmd_run`, +переведённый на пару вызовом `contour.configure`. Здесь живёт только то, что у английской оси +СВОЁ, — базы и их источник. + +⚠ ПОЧЕМУ БАЗЫ ПОКУПАЮТСЯ ЗАНОВО, А НЕ БЕРУТСЯ ИЗ ЭКСП-21. В `~/books/role-topology` лежат 75 +оплаченных английских клеток (15 единиц × DRAFT/A/B/D/D_), и соблазн взять их базой велик. Нельзя: +они куплены 10.08, а пар-пакет `prompts/en-ru/` переписан и заморожен 13–14.08 ДО покупок фазы Д. +Взяв старые клетки, мы получили бы арм `E0` на старых промтах и контуры поверх него на новых — +разница армов мерила бы смену промтов, а не топологию. Это конфаундер эксп-19 в чистом виде. +Старое сырьё остаётся сырьём эксп-21 и здесь не читается вообще. + +Имена армов — из пре-регистрации (`23-editor-tier.md`, Д0.5), механизмы — общие с китайской осью: + + D0 черновик (`deepseek-v4-flash`) база, не арм + E0 боевая связка: черновик + боевой редактор эталон оси + E1 черновик + детерминированные флаги → фиксер вид «flags» + E2 однопроходка уравненного мандата вид «onepass» + E3 черновик + смысловой критик → фиксер вид «critic» + E4 E0 + канон-фиксер ПОСЛЕ вид «canonfix» + E6 ВТОРОЙ редактор (`glm-5`) поверх той же базы касса ФД-G + +Первичный контраст — `E0/D0` (H-3: покупает ли редактор что-нибудь поверх черновика). Он и +покупается первым: если денег не хватит на всю ось, несущая гипотеза всё равно будет закрыта, +а описательные армы объявлены некупленными — вслух, а не молчанием. + +Запуск: en_axis.py --dry смета и что уже куплено, $0 + en_axis.py --bases купить D0 и E0 (первичный контраст) + en_axis.py --arms купить E1 E2 E3 E4 + en_axis.py --e6 купить E6 (касса ФД-G) + en_axis.py --selftest +""" +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 + + +C = _load("contour_en", ZONE / "contour.py") +PE = _load("pair_en_ax", REPO / "eval" / "role_topology" / "pair_en.py") +MONEY, PR, ROSTER, ED22, PARA = C.MONEY, C.PR, C.ROSTER, C.ED22, C.PARA + +N_PER_STRATUM = 8 # 16 единиц — ровно то, что объявила мощность оси +DRAFT_MODEL = "deepseek-v4-flash" # тот же жилец-черновик, что на китайской оси +EDITOR = "deepseek-v4-pro" # боевой редактор +EDITOR2 = "glm-5" # второй редактор, H-4 + + +def units() -> list[dict]: + """16 приколотых единиц. Манифест обязателен: чтение никогда не переотбирает.""" + return PE.units(N_PER_STRATUM) + + +def text_of(tg: str) -> str: + f = MONEY.OUT / f"{tg}.json" + if not f.exists(): + return "" + return json.loads(f.read_text(encoding="utf-8")).get("content") or "" + + +def base_draft(uid: str) -> str: + """D0 — якорный черновик оси. ⚠ Гейт годности тот же, что на китайской ветке.""" + t = text_of(f"dv-b-d0-{uid}") + if not t.strip(): + return "" + why = C.draft_defect(t) + if why: + print(f" ⚠ {uid}: черновик БРАКОВАН гейтом проекта ({why}) — единица непригодна") + return "" + return t + + +def base_e0(uid: str) -> str: + """E0 — боевая связка, эталон оси (роль A0 китайской).""" + return text_of(f"dv-b-e0-{uid}") + + +def wire() -> None: + """Единственная точка включения пары. Всё остальное — механизмы контура.""" + C.configure(PARA.EN, units_provider=units, + base_draft_hook=base_draft, base_a0_hook=base_e0) + + +def _law(src: str) -> str | None: + bf = PARA.EN.bank_file + return PR.law_block(PR.terms_in(src, bf), bf) + + +def cmd_bases() -> None: + """D0 и E0 — первичный контраст оси. Покупаются ПЕРВЫМИ.""" + wire() + led = MONEY.Guarded("ФД-B", default_expect=0.006) + for u in units(): + uid, src = u["uid"], u["source"] + tg = f"dv-b-d0-{uid}" + if not text_of(tg).strip(): + rec = MONEY.purchase(led, tg, DRAFT_MODEL, ED22.client(DRAFT_MODEL), + ROSTER.call_kwargs(DRAFT_MODEL, + PR.translator_msgs(src, None, pair="en-ru")), + role="translator", uid=uid, arm="D0") + if rec.get("skipped"): + raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу") + for u in units(): + uid, src = u["uid"], u["source"] + draft = base_draft(uid) + if not draft.strip(): + print(f" {uid}: черновика нет — E0 пропущен") + continue + tg = f"dv-b-e0-{uid}" + if text_of(tg).strip(): + continue + rec = MONEY.purchase(led, tg, EDITOR, ED22.client(EDITOR), + ROSTER.call_kwargs(EDITOR, + PR.editor_msgs(src, draft, _law(src), + pair="en-ru")), + role="editor", uid=uid, arm="E0") + if rec.get("skipped"): + raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу") + MONEY.report() + + +def cmd_e6() -> None: + """E6 — ВТОРОЙ редактор поверх той же базы. Касса ФД-G, а не ФД-B.""" + wire() + led = MONEY.Guarded("ФД-G", default_expect=0.013) + for u in units(): + uid, src = u["uid"], u["source"] + draft = base_draft(uid) + if not draft.strip(): + continue + tg = f"dv-g-e6-{uid}" + if text_of(tg).strip(): + continue + rec = MONEY.purchase(led, tg, EDITOR2, ED22.client(EDITOR2), + ROSTER.call_kwargs(EDITOR2, + PR.editor_msgs(src, draft, _law(src), + pair="en-ru")), + role="editor3", uid=uid, arm="E6") + if rec.get("skipped"): + raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу") + MONEY.report() + + +def cmd_arms() -> None: + wire() + C.cmd_run(["E1", "E2", "E3", "E4"]) + + +def cmd_dry() -> None: + """Смета и наличие. $0. Цены — измеренные медианы фазы, а не догадки.""" + wire() + us = units() + have = {a: sum(1 for u in us if text_of(C.tag(a, u["uid"])).strip()) + for a in ("E1", "E2", "E3", "E4", "E6")} + have["D0"] = sum(1 for u in us if text_of(f"dv-b-d0-{u['uid']}").strip()) + have["E0"] = sum(1 for u in us if text_of(f"dv-b-e0-{u['uid']}").strip()) + price = {"D0": 0.0020, "E0": 0.00523, "E1": 0.00314, "E2": 0.00523, + "E3": 0.01120 + 0.00314, "E4": 0.00314, "E6": 0.01249} + what = {"D0": "черновик (база, не арм)", "E0": "боевая связка — ЭТАЛОН оси", + "E1": "флаги → фиксер", "E2": "однопроходка уравненного мандата", + "E3": "смысловой критик → фиксер", "E4": "E0 + канон-фиксер", + "E6": "второй редактор glm-5 (касса ФД-G)"} + print(f"Д3 en→ru · единиц {len(us)} · банк {len(PR.bank(PARA.EN.bank_file))} терминов\n") + print(f" {'арм':4s}{'есть':>6s}{'надо':>6s}{'$/клетка':>10s}{'смета':>9s} что это") + tot_b = tot_g = 0.0 + for a in ("D0", "E0", "E1", "E2", "E3", "E4", "E6"): + need = len(us) - have[a] + cost = need * price[a] + if a == "E6": + tot_g += cost + else: + tot_b += cost + print(f" {a:4s}{have[a]:6d}{need:6d}{price[a]:10.5f}{cost:9.4f} {what[a]}") + for ph, need in (("ФД-B", tot_b), ("ФД-G", tot_g)): + sp = MONEY.Guarded(ph).spent() + ceil = MONEY.CEILINGS[ph] + left = ceil - sp + mark = "ВЛЕЗАЕТ" if need <= left else f"НЕ ВЛЕЗАЕТ, не хватает ${need - left:.4f}" + print(f"\n {ph}: потрачено ${sp:.4f} · потолок ${ceil:.2f} · остаток ${left:.4f} · " + f"нужно ${need:.4f} → {mark}") + print("\n⚠ Цены — медианы, снятые на китайских единицах (~1100 знаков). Английские длиннее " + "(~1600), поэтому фактическая цена выше сметы; порядок покупки это учитывает: " + "первичный контраст E0/D0 берётся ПЕРВЫМ.") + + +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 "")) + + wire() + us = units() + ck("единиц ровно 16 — как объявила мощность оси", len(us) == 16, f"их {len(us)}") + ck("страты равны (французский слой против без него)", + sum(1 for u in us if u["stratum"] == "fr") == 8) + ck("пара включена целиком", C.pair().key == "en-ru") + ck("теги оси попадают в глоб кассы ФД-B", + all(C.tag(a, "u").startswith("dv-b-") for a in ("E1", "E2", "E3", "E4"))) + ck("E6 идёт в ФД-G, а не в ФД-B", + C.tag("E6", "u").startswith("dv-g-") and C.phase_of("E6") == "ФД-G") + ck("банк пары непуст (иначе канон-ось пуста и E4 неотличим от E0)", + len(PR.bank(PARA.EN.bank_file)) > 0) + # ⚠ Старое сырьё эксп-21 НЕ должно подхватываться: оно на других промтах. + old = list((Path.home() / "books" / "role-topology").glob("en2-*.json")) + ck("сырьё эксп-21 существует, но осью НЕ читается", bool(old) + and not any(str(MONEY.OUT) in str(f) for f in old), f"клеток эксп-21 {len(old)}") + ck("базы берутся из клеток ФАЗЫ", C.BASE_DRAFT_HOOK is base_draft + and C.BASE_A0_HOOK is base_e0) + print(f"\n{'ОСЬ ГОТОВА К ПОКУПКЕ' if not bad else f'ПРОВАЛОВ: {bad}'}") + return bad + + +if __name__ == "__main__": + a = sys.argv[1] if len(sys.argv) > 1 else "--dry" + fn = {"--dry": cmd_dry, "--bases": cmd_bases, "--arms": cmd_arms, "--e6": cmd_e6, + "--selftest": lambda: sys.exit(1 if cmd_selftest() else 0)}.get(a) + fn() if fn else print(__doc__)