From af6d88504742dab39d05884a274e49b4887320ef Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 8 Aug 2026 20:38:18 +0300 Subject: [PATCH] Re-freeze phase 2 mid-stage: split units across processes to raise throughput, and read the battle editor cell from phase 2 in the phase 3 rig --- eval/tenant_panel/editors.py | 5 ++ eval/tenant_panel/judge.py | 23 ++++-- eval/tenant_panel/panel.py | 24 +++++-- eval/tenant_panel/verify22.py | 128 ++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 eval/tenant_panel/verify22.py diff --git a/eval/tenant_panel/editors.py b/eval/tenant_panel/editors.py index 676c379b..f8e3b90e 100644 --- a/eval/tenant_panel/editors.py +++ b/eval/tenant_panel/editors.py @@ -94,9 +94,14 @@ def tag(model: str, uid: str) -> str: def text_of(arm: str, uid: str) -> str: + """Текст клетки Ф3. ⚠ R0 (боевой редактор) НЕ покупается заново: его клетка над якорным + черновиком уже куплена в Ф2 (`tp2-edit`), и повторная покупка была бы вторым замером той же + вещи. Промт это и предписывает: «клетка dspro-над-flash покупается в Ф2 ОДИН раз».""" if arm == "F": return base_text(uid) model = panel()[int(arm[1:])] + if model == BATTLE: + return P2.edit_text(P2.ANCHOR, uid) f = OUT / f"{tag(model, uid)}.json" return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else "" diff --git a/eval/tenant_panel/judge.py b/eval/tenant_panel/judge.py index d8dde862..f3cb3f66 100644 --- a/eval/tenant_panel/judge.py +++ b/eval/tenant_panel/judge.py @@ -141,12 +141,24 @@ def runs_used() -> list[dict]: def log_run(pass_: str, tokens: list[str], note: str = "") -> int: + """Запись агент-запуска + карта «токен → судья». + + Карта нужна не для отчётности: без идентичности судьи нельзя ни посчитать согласие судей, ни + разложить вердикты по популяциям — эксп-21 не персистировал её и потому не мог этого сделать + (найдено его же ревью). Карта лежит рядом с ключом и судье не выдаётся. + """ rs = runs_used() - rs.append(dict(n=len(rs) + 1, pass_=pass_, tokens=tokens, note=note)) + n = len(rs) + 1 + rs.append(dict(n=n, pass_=pass_, tokens=tokens, note=note)) RUNS.write_text(json.dumps(rs, ensure_ascii=False, indent=1), encoding="utf-8") - if len(rs) > CAP: - raise SystemExit(f"⛔ КАП АГЕНТ-ЗАПУСКОВ ПРОБИТ: {len(rs)} > {CAP} — стоп и пинг") - return len(rs) + jf = KEYS / f"{pass_}-JUDGES.json" + m = json.loads(jf.read_text(encoding="utf-8")) if jf.exists() else {} + for t in tokens: + m[t] = f"agent-{n:02d}" + jf.write_text(json.dumps(m, ensure_ascii=False, indent=1), encoding="utf-8") + if n > CAP: + raise SystemExit(f"⛔ КАП АГЕНТ-ЗАПУСКОВ ПРОБИТ: {n} > {CAP} — стоп и пинг") + return n def cmd_plan(pass_: str) -> None: @@ -154,6 +166,9 @@ def cmd_plan(pass_: str) -> None: us = units(pass_) amap = arm_map(pass_) print(f"проход {pass_} · единиц {len(us)} · армов {len(amap)}") + if pass_.startswith("p2"): + print(" (D_i — черновик жильца i; E_i — БОЕВОЙ редактор deepseek-v4-pro поверх ЭТОГО " + "черновика,\n то есть буква справа называет жильца ЧЕРНОВОЙ роли, а не редактора)") for a, m in amap.items(): print(f" {a:4s} → {m}") print("\nсемейство (впечатывается в ключ ДО ответов):") diff --git a/eval/tenant_panel/panel.py b/eval/tenant_panel/panel.py index 02bc04e1..c602ba7b 100644 --- a/eval/tenant_panel/panel.py +++ b/eval/tenant_panel/panel.py @@ -94,9 +94,18 @@ def tenants() -> list[str]: return [ANCHOR, *sorted(keep, key=lambda m: res[m]["price_translator"])] -def units_all() -> list[tuple[dict, bool]]: - """(единица, en?) — обе оси обязательны.""" - return [(u, False) for u in M.units_zh()] + [(u, True) for u in M.units_en()] +def units_all(part: str = "") -> list[tuple[dict, bool]]: + """(единица, en?) — обе оси обязательны. + + `part="i/n"` режет СПИСОК единиц на n равных долей для параллельных процессов. Деление + детерминированное (по позиции в уже отсортированном списке), поэтому доли не пересекаются и + ни одна единица не теряется: это проверяется `--plan`, который печатает сумму долей. + """ + us = [(u, False) for u in M.units_zh()] + [(u, True) for u in M.units_en()] + if not part: + return us + i, n = (int(x) for x in part.split("/")) + return [x for k, x in enumerate(us) if k % n == i] # ⚠ ПОЛ КЛАДЁТСЯ ОТДЕЛЬНОЙ ПАРОЙ, а не «боевая клетка + повтор». Иначе одна половина пола @@ -187,10 +196,15 @@ def _selected(argv: list[str]) -> list[str]: return [m for m in ts if m in only] if only else ts +def _part(argv: list[str]) -> str: + p = [a.split("=", 1)[1] for a in argv if a.startswith("--part=")] + return p[0] if p else "" + + def cmd_drafts(argv: list[str] | None = None) -> None: led = MONEY.Guarded("Ф2", default_expect=0.01) for m in _selected(argv or []): - for u, en in units_all(): + for u, en in units_all(_part(argv or [])): rec = buy_draft(led, m, u, en) if rec.get("skipped"): print("⛔ потолок Ф2 — стоп") @@ -205,7 +219,7 @@ def cmd_drafts(argv: list[str] | None = None) -> None: def cmd_edit(argv: list[str] | None = None) -> None: led = MONEY.Guarded("Ф2", default_expect=0.01) for m in _selected(argv or []): - for u, en in units_all(): + for u, en in units_all(_part(argv or [])): rec = buy_edit(led, m, u, en) if rec.get("no_draft"): print(f" {m:24s} {u['uid'][:6]} — годного черновика нет, редактура пропущена") diff --git a/eval/tenant_panel/verify22.py b/eval/tenant_panel/verify22.py new file mode 100644 index 00000000..4ee0f80f --- /dev/null +++ b/eval/tenant_panel/verify22.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""ВЕРИФИКАТОР ЧИСЕЛ ОТЧЁТА ЭКСП-22. $0. Ненулевой код возврата = отчёт не сдаётся. + +`itog22.py` пере-считывает числа из сырья; этот файл сверяет их с ТЕКСТОМ отчёта. Разделение +нужно потому, что «число посчиталось» и «в отчёте напечатано именно оно» — разные утверждения, и +эксп-21 разошёлся с сырьём ровно на этом стыке (§18: ИТОГ печатал расход фазы без одного тега, +а гейт это сертифицировал). + +Правило: гейт сторожит НЕСУЩИЕ числа — те, на которых стоит решение владельца. Число, которого +гейт не проверяет, в ИТОГе стоять не должно. +""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +ZONE = Path(__file__).resolve().parent +sys.path.insert(0, str(ZONE)) +sys.path.insert(0, str(REPO / "eval" / "role_topology")) + +import bank as BANK # noqa: E402 +import material as M # noqa: E402 +import money as MONEY # noqa: E402 +import panel as P2 # noqa: E402 + +REPORT = REPO / "docs" / "experiments" / "22-tenant-panel.md" +BAD = 0 + + +def ck(name: str, ok: bool, detail: str = "") -> None: + global BAD # noqa: PLW0603 + BAD += not ok + print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f" {detail}" if detail else "")) + + +def has(text: str, s: str) -> bool: + """Число ищется по НОРМАЛИЗОВАННОМУ тексту: пробелы и переносы строк не значимы.""" + return re.sub(r"\s+", "", s) in re.sub(r"\s+", "", text) + + +def main() -> int: + if not REPORT.exists(): + print("отчёта нет") + return 1 + t = REPORT.read_text(encoding="utf-8") + + print("=== МАТЕРИАЛ ===") + zs = M.units_zh() + ck("книга и дата первой публикации названы", + has(t, M.BOOK["title"]) and has(t, M.BOOK["first_pub"])) + ck(f"число zh-единиц {len(zs)}", has(t, f"{len(zs)} единиц") or has(t, f"{len(zs)} zh")) + ck("гейт прав объявлен числом снятых глав", + has(t, "гейт прав") or has(t, "ГЕЙТ ПРАВ")) + b = BANK.PR.bank() + tot = sum(sum(u["source"].count(x) for u in zs) for x in b) + ck(f"банк {len(b)} терминов", has(t, f"{len(b)} термин")) + ck(f"знаменатель покрытия {tot}", has(t, str(tot))) + + print("\n=== КОНТАМИНАЦИЯ ===") + f = MONEY.OUT / "contamination.json" + if f.exists(): + for r in json.loads(f.read_text(encoding="utf-8")): + ck(f"{r['book']}/{r['model'].split('-')[0]} узнано {r['recognized']}/{r['n']}", + has(t, f"{r['recognized']}/{r['n']}")) + + print("\n=== СКРИН Ф1 ===") + res = P2.screen_result() + for m, r in res.items(): + if r.get("price_translator") is None: + continue + ck(f"{m}: цена переводчика {r['price_translator']:.5f}", + has(t, f"{r['price_translator']:.5f}")) + ck(f"{m}: вердикт {r['verdict']}", has(t, m)) + + print("\n=== ПАНЕЛЬ Ф2 ===") + for m in P2.tenants(): + ck(f"жилец {m} назван в отчёте", has(t, m)) + + print("\n=== ДЕНЬГИ ===") + for ph in ("Ф0", "Ф1", "Ф2", "Ф3"): + sp = MONEY.Guarded(ph).spent() + if sp <= 0: + continue + ck(f"{ph}: расход ${sp:.6f} напечатан", has(t, f"{sp:.6f}")) + if sp > MONEY.CEILINGS[ph] + 1e-9: + ck(f"{ph}: ПРОБОЙ потолка объявлен", + has(t, "ПРОБОЙ") or has(t, "пробит") or has(t, "пробой"), + f"${sp:.6f} против ${MONEY.CEILINGS[ph]:.2f}") + pack = MONEY.Pack().spent() + ck(f"расход пака ${pack:.6f} напечатан", has(t, f"{pack:.6f}")) + + print("\n=== СУДЕЙСТВО ===") + import judge as JU # noqa: PLC0415 + rs = JU.runs_used() + if rs: + ck(f"агент-запусков {len(rs)} из {JU.CAP} напечатано", + has(t, f"{len(rs)} из {JU.CAP}") or has(t, f"{len(rs)}/{JU.CAP}")) + for pass_ in ("p2zh", "p2en", "p3", "dva"): + votes = MONEY.OUT / f"aj-{pass_}-votes" + if not votes.exists() or not list(votes.glob("*.json")): + continue + JU.setup(pass_) + import absjudge as AJ # noqa: PLC0415 + data = AJ._by_unit() # noqa: SLF001 + for a, bb, _w in JU.family(pass_): + m = AJ._margins(a, bb) # noqa: SLF001 + if len(m) < 3: + continue + import statistics as sst # noqa: PLC0415 + ck(f"{pass_}: контраст {a} vs {bb} = {sst.mean(m):+.2f} напечатан", + has(t, f"{sst.mean(m):+.2f}") or has(t, f"{sst.mean(m):.2f}")) + # контроли обязаны быть напечатаны ДО боевых чисел + dec = [d for d in data.values() if "CTRLdecoy" in d] + if dec: + ck(f"{pass_}: декой упомянут в отчёте", has(t, "декой") or has(t, "ДЕКОЙ")) + flo = [d for d in data.values() if "CTRLfloorA" in d and "CTRLfloorB" in d] + if flo: + ck(f"{pass_}: шумовой пол упомянут", has(t, "пол") or has(t, "ПОЛ")) + + print(f"\n{'ВСЕ ЧИСЛА ОТЧЁТА СХОДЯТСЯ' if not BAD else f'РАСХОЖДЕНИЙ: {BAD} — ОТЧЁТ НЕ СДАЁТСЯ'}") + return BAD + + +if __name__ == "__main__": + sys.exit(1 if main() else 0)