From 05e2d57fd7ef2f857b17544667ad57e165243dde Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 8 Aug 2026 17:59:20 +0300 Subject: [PATCH] Re-freeze phase 1 tail: raise its ceiling to the owner-approved figure and align the markdown gate with the production sanitizer --- eval/tenant_panel/judge.py | 55 +++++++++++++++++++++++++++++++++++-- eval/tenant_panel/money.py | 6 +++- eval/tenant_panel/panel.py | 51 +++++++++++++++++++++------------- eval/tenant_panel/screen.py | 13 ++++++++- 4 files changed, 102 insertions(+), 23 deletions(-) diff --git a/eval/tenant_panel/judge.py b/eval/tenant_panel/judge.py index 98be913e..d8dde862 100644 --- a/eval/tenant_panel/judge.py +++ b/eval/tenant_panel/judge.py @@ -113,8 +113,8 @@ def floor_of(pass_: str): def hook(u: dict): if pass_ not in ("p2zh", "p3"): return None - a = P2.edit_text(P2.ANCHOR, u["uid"]) - b = P2.edit_text(P2.ANCHOR, u["uid"], floor=True) + a = P2.edit_text(P2.ANCHOR, u["uid"], floor=1) + b = P2.edit_text(P2.ANCHOR, u["uid"], floor=2) return (a, b) if a.strip() and b.strip() else None return hook @@ -164,6 +164,57 @@ def cmd_plan(pass_: str) -> None: print(f"агент-запусков израсходовано {len(runs_used())} из {CAP}") +# Сколько единиц отдаётся ОДНОЙ судейской сессии. Кап промта — «пакет до ~20 текстов»; здесь +# считается по фактическому числу меток прохода, а не на глаз. +def per_run(pass_: str) -> int: + n_arms = len(arm_map(pass_)) + 3 # + декой и пара пола + return max(1, 20 // max(1, n_arms)) + + +def cmd_dispatch(pass_: str) -> None: + """Группы заданий на агент-запуски + дословный промт судьи. Ничего не запускает сам.""" + setup(pass_) + tasks = sorted((OUT / f"aj-{pass_}-tasks").glob("*.txt")) + if not tasks: + print("заданий нет — сначала --emit") + return + k = per_run(pass_) + groups = [tasks[i:i + k] for i in range(0, len(tasks), k)] + used = len(runs_used()) + print(f"проход {pass_}: заданий {len(tasks)} · по {k} на сессию · сессий {len(groups)}") + print(f"агент-запусков израсходовано {used}, останется {CAP - used - len(groups)} из {CAP}") + if used + len(groups) > CAP: + print("⛔ КАП НЕ ВМЕЩАЕТ ПРОХОД — стоп и пинг, не резать молча") + return + print("\nПРОМТ СУДЬИ (дословно, один на сессию; список файлов подставляется):\n") + print(JUDGE_PROMPT.format(files="<файлы сессии>", answers=OUT / f"aj-{pass_}")) + print("\nГРУППЫ:") + for i, g in enumerate(groups, 1): + print(f" сессия {i}: " + " ".join(str(p) for p in g)) + + +JUDGE_PROMPT = """Ты — литературный редактор-эксперт в роли СУДЬИ. Работай сам, не делегируй \ +задачу другим агентам: раздача пакетов разным исполнителям смешивает их пороги строгости и \ +делает числа несравнимыми. + +Файлы заданий: {files} + +Для КАЖДОГО файла: прочитай его целиком и выполни ровно то, что в нём написано — оцени каждый \ +помеченный вариант ОТДЕЛЬНО против исходника, посчитай ошибки по четырём осям и запиши ответ \ +файлом ровно по тому пути, который назван внутри задания (каталог {answers}). + +Жёсткие правила: +* обоснование ПОЧЕМУ обязательно для каждой НЕНУЛЕВОЙ оси и обязано содержать цитату в кавычках; \ +число без цитаты не принимается разбором и на диск не попадает; +* ноль по оси обоснования не требует; +* НЕ открывай ничего, кроме файлов заданий: ни ключей раскладки, ни кода, ни соседних каталогов. \ +Метки Т1..Тn слепые намеренно; +* порядок меток ничего не значит и перемешан; +* не сравнивай варианты между собой — только каждый против исходника. + +Верни одной строкой: сколько файлов заданий обработано и куда записаны ответы.""" + + def main() -> None: a = sys.argv[1:] if not a: diff --git a/eval/tenant_panel/money.py b/eval/tenant_panel/money.py index f28f609f..b8f2e35d 100644 --- a/eval/tenant_panel/money.py +++ b/eval/tenant_panel/money.py @@ -48,7 +48,11 @@ TAGS = { "Ф2": ("tp2-draft-*.json", "tp2-edit-*.json", "tp2-floor-*.json"), "Ф3": ("tp3-ed-*.json",), } -CEILINGS = {"Ф0": 0.40, "Ф1": 1.00, "Ф2": 2.00, "Ф3": 1.60} +# ⚠ ПОТОЛОК Ф1 ПОДНЯТ 08.08 С $1.00 ДО $1.05 СЛОВОМ ВЛАДЕЛЬЦА, объявлено ДО траты. Причина: +# проекционный гард остановил скрин перед `gemini-3.1-pro-preview` — ОБЯЗАТЕЛЬНЫМ кандидатом Ф3 +# по промту. Его цена — единственная неизвестная в смете Ф3 (оценка $0.80 из $1.60), и без замера +# половина фазы стояла бы на прайс-странице вместо провода. Пакетный потолок $5.00 не двигался. +CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 1.60} PACK_CEILING = 5.00 diff --git a/eval/tenant_panel/panel.py b/eval/tenant_panel/panel.py index a72cd346..24e037d6 100644 --- a/eval/tenant_panel/panel.py +++ b/eval/tenant_panel/panel.py @@ -98,16 +98,20 @@ def units_all() -> list[tuple[dict, bool]]: return [(u, False) for u in M.units_zh()] + [(u, True) for u in M.units_en()] -def tag_draft(model: str, uid: str, attempt: int = 1, floor: bool = False) -> str: - base = "tp2-floor" if floor else "tp2-draft" +# ⚠ ПОЛ КЛАДЁТСЯ ОТДЕЛЬНОЙ ПАРОЙ, а не «боевая клетка + повтор». Иначе одна половина пола +# побайтно совпала бы с армом `E0` в том же судейском пакете: судья увидел бы один текст дважды, +# а контроль «близнец» сработал бы на боевом арме. Эксп-21 обошёл это тем, что его пол лежал на +# ДРУГОМ входе (§build_unit), — здесь то же достигается двумя независимыми прогонами `p1`/`p2`. +def tag_draft(model: str, uid: str, attempt: int = 1, floor: int = 0) -> str: + base = f"tp2-floor-p{floor}" if floor else "tp2-draft" return f"{base}-D-{model}-{uid}" + ("" if attempt == 1 else f"-r{attempt}") -def tag_edit(model: str, uid: str, floor: bool = False) -> str: - return f"{'tp2-floor' if floor else 'tp2-edit'}-E-{model}-{uid}" +def tag_edit(model: str, uid: str, floor: int = 0) -> str: + return f"{f'tp2-floor-p{floor}' if floor else 'tp2-edit'}-E-{model}-{uid}" -def draft_text(model: str, uid: str, floor: bool = False) -> str: +def draft_text(model: str, uid: str, floor: int = 0) -> str: """ГОДНЫЙ черновик: последняя попытка, прошедшая гейт. Пусто — годного нет.""" pre = tag_draft(model, uid, floor=floor) best = "" @@ -118,12 +122,12 @@ def draft_text(model: str, uid: str, floor: bool = False) -> str: return best -def edit_text(model: str, uid: str, floor: bool = False) -> str: +def edit_text(model: str, uid: str, floor: int = 0) -> str: f = OUT / f"{tag_edit(model, uid, floor)}.json" return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else "" -def buy_draft(led, model: str, u: dict, en: bool, floor: bool = False) -> dict: +def buy_draft(led, model: str, u: dict, en: bool, floor: int = 0) -> dict: blk = None if en else PR.glossary_block(PR.terms_in(u["source"])) msgs = PR.translator_msgs(u["source"], blk, en=en) rec = {} @@ -137,7 +141,7 @@ def buy_draft(led, model: str, u: dict, en: bool, floor: bool = False) -> dict: return rec -def buy_edit(led, model: str, u: dict, en: bool, floor: bool = False) -> dict: +def buy_edit(led, model: str, u: dict, en: bool, floor: int = 0) -> dict: draft = draft_text(model, u["uid"], floor) if not draft: return dict(skipped=False, content="", no_draft=True) @@ -207,21 +211,30 @@ def cmd_edit() -> None: def cmd_floor() -> None: - """Вторая генерация ЯКОРНОГО арма end-to-end на тех же zh-единицах.""" + """ШУМОВОЙ ПОЛ: ДВА независимых прогона якорного арма end-to-end на тех же zh-единицах. + + Истинная разница между `p1` и `p2` — ноль по построению, поэтому измеренный разброс и есть + шум пайплайна (генерация черновика + генерация редактуры + судья). Он применяется к Ф2 И Ф3: + перевес ниже пола печатается вердиктом «ниже порога различимости». + """ led = MONEY.Guarded("Ф2", default_expect=0.01) - for u in M.units_zh(): - d = buy_draft(led, ANCHOR, u, False, floor=True) - if d.get("skipped"): - print("⛔ потолок Ф2 — стоп") - return - e = buy_edit(led, ANCHOR, u, False, floor=True) - print(f" пол {u['uid'][:6]} черновик {len(d.get('content', '')):5d} зн · " - f"редактура {len(e.get('content', '')):5d} зн") - time.sleep(0.15) + for p in (1, 2): + for u in M.units_zh(): + d = buy_draft(led, ANCHOR, u, False, floor=p) + if d.get("skipped"): + print("⛔ потолок Ф2 — стоп") + return + e = buy_edit(led, ANCHOR, u, False, floor=p) + if e.get("skipped"): + print("⛔ потолок Ф2 — стоп") + return + print(f" пол p{p} {u['uid'][:6]} черновик {len(d.get('content', '')):5d} зн · " + f"редактура {len(e.get('content', '')):5d} зн") + time.sleep(0.15) print(f"потрачено Ф2 ${led.spent():.6f}") -def draft_cost(model: str, uid: str, floor: bool = False) -> float: +def draft_cost(model: str, uid: str, floor: int = 0) -> float: """Цена черновика = ВСЕ попытки: ре-ген оплачен, значит он в цене роли.""" pre = tag_draft(model, uid, floor=floor) return sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) diff --git a/eval/tenant_panel/screen.py b/eval/tenant_panel/screen.py index 8556427f..c72bc2e6 100644 --- a/eval/tenant_panel/screen.py +++ b/eval/tenant_panel/screen.py @@ -55,7 +55,18 @@ CYR_MIN = 0.5 # Г4: доля ECHO_MAX = 0.15 # Г2: боевой порог disposition.go _RE_HAN = re.compile(r"[㐀-鿿]") -_RE_MD = re.compile(r"^\s{0,3}#{1,6}\s|\*\*", re.M) +# ⚠ ДЕТЕКТОР MARKDOWN ИСПРАВЛЕН ПО ВСКРЫТИЮ КЛЕТОК (правило D39.61: перед сравнительным выводом +# открыть единицы). Первая версия (унаследована из скрина эксп-21) ловила ЛЮБОЕ `**` и потому +# помечала как «протёкшую разметку» строку `***` — стандартный русский типографский разделитель +# сцены, которым модель верно передала смену сцены в исходнике. Из-за этого ПРОВАЛ получали +# `glm-5` (ратифицированный резервный редактор) и `gemini-3.1-flash-lite` — то есть дефект +# детектора менял состав панели. +# Здесь детектор приведён к БОЕВОМУ (`checks/sanitizer.go:320` + D33.2): ATX-заголовок = решётки, +# пробел и БУКВА/ЦИФРА (декоративный разделитель решётками боевой гейт тоже не считает), плюс +# настоящий жирный спан `**текст**`, который боевой санитайзер не ловит, но в прозе он мусор. +# Порог «нарушений более чем в одной клетке» НЕ менялся — менялось только то, что считается +# нарушением. +_RE_MD = re.compile(r"^\s{0,3}#{1,6}\s+[^\W\d_]|\*\*(?=\S)(?:(?!\*\*).)+\*\*", re.M) _RE_PREAMBLE = re.compile(r"^\s*(вот|ниже|here is|here's)\b[^\n:]{0,80}:", re.I) _RE_REFUSAL = re.compile( r"не могу|не буду|отказ|прости(те)?, но|cannot (assist|help|comply)|i'm sorry", re.I)