Freeze editor-tier phase A: pack roster with vendor-verified prices, configurable ledger, unit pinning by manifest, and screen reusing exp-22 thresholds

This commit is contained in:
Claude (backend session) 2026-08-09 02:44:28 +03:00
parent 210b38ec98
commit c0dd228d64
7 changed files with 746 additions and 3 deletions

119
eval/conformance.py Normal file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""СВЕРКА С БУКВОЙ ЗАКАЗА. $0. Инструмент зоны `eval/`, не одного пака.
Зачем он есть. Эксп-22 сдал отчёт, у которого ВСЕ числа сходились с сырьём (два гейта, 85
проверок, пере-счёт двумя путями) и при этом ШЕСТЬ требований промта не были выполнены. Числа и
заказ разные классы отказа, и прибор для первого не ловит второй. Все шесть пропусков стояли не
в нумерованных блоках фаз, а в придаточных, и «критик полноты», написанный тем же, кто вёл пак,
их не поймал: он проверял по МОДЕЛИ заказа, а не по заказу.
Что делает. Читает из markdown-документа таблицу требований (маркер `<!-- ТРЕБОВАНИЯ -->`) и по
КАЖДОЙ строке проверяет улику исполнения. Улика бывает двух видов:
`$ <команда>` команда должна завершиться кодом 0 (только $0-команды; сеть и покупки здесь
неуместны и не предусмотрены);
`<путь>` артефакт должен существовать и быть непустым.
Улика `` означает «улики нет»: в режиме плана это законно (требование ещё не исполнено), в
режиме сдачи отказ.
Режимы:
--plan требование без улики = ОЖИДАЕТ (код 0, если ни одна улика не ПАДАЕТ);
--final требование без улики = ПРОВАЛ (код 0). Гонять перед сдачей пака.
Запуск: eval/.venv/bin/python eval/conformance.py <файл.md> [--plan|--final]
Границы инструмента, чтобы клейм не был шире прибора. Он проверяет, что у требования ЕСТЬ
проходящая улика, а не что улика доказывает именно это требование: связь «требование улика»
устанавливает автор таблицы, и подменить её ничего не мешает. Это сдвигает работу с «вспомнить всё
из прозы» на «объявить улику заранее» но не отменяет чтения.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
MARK = "<!-- ТРЕБОВАНИЯ -->"
def rows(md: str) -> list[tuple[str, str, str]]:
"""Строки таблицы требований: (ID, требование, улика)."""
if MARK not in md:
raise SystemExit(f"в документе нет маркера {MARK} — реестр требований не объявлен")
tail = md.split(MARK, 1)[1]
out = []
for line in tail.splitlines():
s = line.strip()
if not s.startswith("|"):
if out: # таблица кончилась
break
continue
cells = [c.strip() for c in s.strip("|").split("|")]
if len(cells) < 3 or set(cells[0]) <= set("-: "):
continue
if cells[0].upper() in ("ID", ""):
continue
out.append((cells[0], cells[1], cells[2]))
if not out:
raise SystemExit("реестр требований пуст")
return out
def check(ev: str) -> tuple[str, str]:
"""Вернуть (статус, деталь). Статусы: OK · ПРОВАЛ · ОЖИДАЕТ."""
ev = ev.strip().strip("`")
if ev in ("", "-", ""):
return "ОЖИДАЕТ", "улика не объявлена"
if ev.startswith("$"):
cmd = ev[1:].strip()
# ⚠ Улика не имеет права звать этот же инструмент в режиме сверки: реестр, чьей уликой
# служит проверка самого реестра, уходит в бесконечную рекурсию. Поймано первым прогоном.
if "conformance.py" in cmd and ("--plan" in cmd or "--final" in cmd):
return "ПРОВАЛ", "улика рекурсивна: зовёт conformance.py в режиме сверки"
try:
r = subprocess.run(cmd, shell=True, cwd=REPO, capture_output=True, # noqa: S602
text=True, timeout=1800)
except subprocess.TimeoutExpired:
return "ПРОВАЛ", "таймаут"
if r.returncode == 0:
return "OK", ""
return "ПРОВАЛ", (r.stderr or r.stdout).strip().splitlines()[-1][:120] if (
r.stderr or r.stdout).strip() else f"код {r.returncode}"
p = (REPO / ev) if not ev.startswith("/") else Path(ev)
if p.is_dir():
return ("OK", "") if any(p.iterdir()) else ("ПРОВАЛ", "каталог пуст")
if p.exists() and p.stat().st_size > 0:
return "OK", ""
return "ПРОВАЛ", "артефакта нет или он пуст"
def main() -> int:
args = [a for a in sys.argv[1:] if a]
if not args:
print(__doc__)
return 1
mode = "--final" if "--final" in args else "--plan"
src = next((a for a in args if not a.startswith("--")), None)
if not src:
print("не указан файл")
return 1
md = (REPO / src if not src.startswith("/") else Path(src)).read_text(encoding="utf-8")
bad = pend = 0
print(f"СВЕРКА С БУКВОЙ ЗАКАЗА: {src} [{mode[2:]}]\n")
for rid, req, ev in rows(md):
st, detail = check(ev)
if st == "ОЖИДАЕТ" and mode == "--final":
st = "ПРОВАЛ"
detail = "улики нет, а пак сдаётся"
bad += st == "ПРОВАЛ"
pend += st == "ОЖИДАЕТ"
print(f"[{st:8s}] {rid:6s} {req[:78]}" + (f"{detail}" if detail else ""))
total = len(rows(md))
print(f"\nтребований {total} · ожидают {pend} · провалов {bad}")
if bad:
print("СВЕРКА НЕ ПРОЙДЕНА")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""МАТЕРИАЛ ПАКА: 16 НОВЫХ единиц той же книги. $0 — глава уже скачана эксп-22.
Зачем новые единицы. Контраст `R2 glm-5` против боевого редактора дал 2.12 при шуме одного вызова
sd3.5: это вопрос МОЩНОСТИ, а не судьи. Пред-объявленный порог эксп-22 (2.03) он проходит,
собственный порог своего прохода (2.47) нет, и вердикт зависит от того, какой взять. Удвоение
n с 16 до 32 либо разводит интервал от порога, либо честно говорит «эффект меньше, чем стоит
различать». Оба ответа годны; зависимость вердикта от выбора порога нет.
Методика отбора та же, что в эксп-21 §0 и эксп-22: абзацные чанки, дедуп по подписи без заголовка
главы, ключ = хеш источника, отбор из СЕРЕДИНЫ распределения длин. Отличие ровно одно: главы,
из которых эксп-22 уже брал единицы, исключаются целиком иначе «новые» единицы были бы соседними
абзацами тех же глав, и прирост n оказался бы мнимым (соседние куски одной главы делят персонажей,
сцену и лексику, то есть коррелируют сильнее случайных).
Гейт прав эксп-22 (снятые главы верхнего дециля эротических маркеров) продолжает действовать: он
применяется в `chapters()`, который здесь переиспользуется как есть.
"""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
sys.path.insert(0, str(REPO / "eval" / "tenant_panel"))
import material as M22 # noqa: E402
N_NEW = 16
def used_chapters() -> set[int]:
"""Главы, из которых эксп-22 уже взял единицы."""
return {u["chapter"] for u in M22.units_zh()}
def used_uids() -> set[str]:
return {u["uid"] for u in M22.units_zh()}
def rights_dropped(pool: set[int]) -> set[int]:
"""Гейт прав по СВОЕМУ пулу: верхний дециль по плотности эротических маркеров.
Число снятых глав НЕ переносится из эксп-22. Там гейт снял две главы это был верхний
дециль пула из 18. Пул этого пака другой, и перенос константы «2» пропустил бы в замер главы,
которые объявленное правило снимает. Правило одно и то же, число считается заново.
"""
allch = {c["idx"]: c for c in M22.chapters(all_=True)}
ranked = sorted(pool, key=lambda i: (-M22.se_density(allch[i]["text"]), i))
k = max(1, round(len(pool) * 0.10))
return set(ranked[:k])
def units_new(n: int = N_NEW) -> list[dict]:
"""N новых единиц из глав, которых эксп-22 не касался и которые прошли гейт прав."""
allidx = {c["idx"] for c in M22.chapters(all_=True)}
free = allidx - used_chapters() - M22.se_dropped(M22.chapters(all_=True))
dropped = rights_dropped(free)
busy = used_chapters() | dropped | M22.se_dropped(M22.chapters(all_=True))
globals()["RIGHTS_DROPPED"] = sorted(dropped)
uniq: dict[str, dict] = {}
seen: set[str] = set()
for c in M22.chunks():
if c["chapter"] in busy:
continue
sig = M22._dedup_sig(c["text"]) # noqa: SLF001
if sig in seen:
continue
seen.add(sig)
uid = M22.uid_of(c["text"])
uniq.setdefault(uid, dict(source=c["text"], uid=uid, chapter=c["chapter"]))
keys = sorted(uniq, key=lambda k: (len(uniq[k]["source"]), k))
if len(keys) < n:
raise SystemExit(f"свободных единиц {len(keys)}, нужно {n} — материал не набирается")
lo = (len(keys) - n) // 2
picked = [uniq[k] for k in keys[lo:lo + n]]
# одна глава — не более одной единицы: иначе внутри «новых» появится та же корреляция,
# ради снятия которой исключались главы эксп-22
out, taken = [], set()
for u in picked:
if u["chapter"] in taken:
continue
taken.add(u["chapter"])
out.append(u)
if len(out) < n: # добираем по тому же порядку, соблюдая правило главы
for k in keys[lo + n:] + keys[:lo][::-1]:
u = uniq[k]
if u["chapter"] in taken:
continue
taken.add(u["chapter"])
out.append(u)
if len(out) == n:
break
return out[:n]
def selfcheck() -> int:
bad = 0
us = units_new()
if len(us) != N_NEW:
print(f"[ПРОВАЛ] единиц {len(us)}, нужно {N_NEW}")
bad += 1
if set(u["uid"] for u in us) & used_uids():
print("[ПРОВАЛ] единица повторяет эксп-22")
bad += 1
if set(u["chapter"] for u in us) & used_chapters():
print("[ПРОВАЛ] глава повторяет эксп-22")
bad += 1
if len({u["chapter"] for u in us}) != len(us):
print("[ПРОВАЛ] две единицы из одной главы")
bad += 1
sim = M22._pairwise_max_sim(us) # noqa: SLF001
if sim > 0.5:
print(f"[ПРОВАЛ] единицы слишком похожи: макс. близость {sim:.3f}")
bad += 1
ln = sorted(len(u["source"]) for u in us)
print(f"новых единиц {len(us)} из {len({u['chapter'] for u in us})} глав · "
f"знаков {ln[0]}{ln[-1]} медиана {ln[len(ln) // 2]} · макс. близость {sim:.3f}")
print(f"главы: {sorted(u['chapter'] for u in us)}")
print(f"снято гейтом прав из своего пула: {globals().get('RIGHTS_DROPPED')}")
print(f"провалов {bad}")
return bad
if __name__ == "__main__":
sys.exit(1 if selfcheck() else 0)

122
eval/editor_tier/money.py Normal file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""КАССА ПАКА «СИЛЬНЫЙ ТИР». $0 сама по себе; через неё идут ВСЕ платные вызовы пака.
Не копия кассы эксп-22, а её НАСТРОЙКА. Механизм там проверенный и дорого доставшийся фриз-гейт,
атомарный замок клетки, проекционный сторож перед каждым вызовом, два независимых пути счёта, и
копирование размножило бы будущие расхождения вместо того, чтобы их ловить. Пак-специфика (зона
кода, каталог сырья, теги фаз, потолки, прайс-таблица) подставляется через `money.configure`.
Потолки санкция владельца 09.08: пак $4.00. Фазовые суммируются в $3.35, разница объявленный
резерв на ре-гены эхо-мины и ретраи, а не запас «на всякий случай»: тратится он только через те же
теги и виден в отчёте.
"""
from __future__ import annotations
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" / "tenant_panel"))
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
def _load(name: str, path: Path):
"""⚠ Ростер пака грузится ПО ПУТИ. `import roster` находил ростер эксп-22: три
`sys.path.insert(0, )` подряд кладут ПОСЛЕДНИЙ вставленный первым, и своя папка оказывалась
в хвосте. Поймано печатью селфтеста «моделей 11» вместо 15, то есть проверка «цена считается
для ВСЕХ моделей» молча проверяла только старые."""
import importlib.util # noqa: PLC0415
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
# ⚠ И касса эксп-22 тоже грузится ПО ПУТИ. `import money` здесь работал лишь по счастливому
# порядку sys.path: стоило вызвать модуль из файла, который кладёт свою папку первой, как он
# находил САМ СЕБЯ и падал циклическим импортом. Правило без исключений: чужое — только _load.
M22 = _load("money22", REPO / "eval" / "tenant_panel" / "money.py")
ROSTER_ET = _load("roster_et", ZONE / "roster.py")
OUT = Path.home() / "books" / "editor-tier"
# Теги покупок по фазам. ⚠ Новый тег вписывается СЮДА тем же коммитом, что и код, который его
# заводит: расход мимо глоба невидим для кассы, и эксп-21 напоролся на это трижды.
TAGS = {
"ФA": ("et-scr-*.json",), # скрин сильного тира
"ФB": ("et-unit-*.json",), # 16 новых единиц для границы glm-5 (черновик + R0 + R2)
"ФC": ("et-ed-*.json",), # панель редакторов поверх якорной базы эксп-22
}
CEILINGS = {"ФA": 1.10, "ФB": 0.45, "ФC": 1.80}
PACK_CEILING = 4.00
M22.configure(ZONE=ZONE, OUT=OUT, TAGS=TAGS, CEILINGS=CEILINGS,
PACK_CEILING=PACK_CEILING, GLOB_ALL="et-*.json", ROSTER=ROSTER_ET)
# Всё остальное берётся у настроенной кассы — своих реализаций тут нет намеренно.
Guarded, Pack, purchase, model_expect = M22.Guarded, M22.Pack, M22.purchase, M22.model_expect
def report() -> None:
print(f"{'фаза':6s}{'потрачено':>12s}{'потолок':>10s}")
tot = 0.0
for ph in TAGS:
sp = Guarded(ph).spent()
tot += sp
mark = " ⛔ ПРОБОЙ" if sp > CEILINGS[ph] + 1e-9 else ""
print(f"{ph:6s}{sp:12.6f}{CEILINGS[ph]:10.2f}{mark}")
print(f"{'ПАК':6s}{tot:12.6f}{PACK_CEILING:10.2f}")
def selftest() -> int:
"""Обязан пройти ДО первой покупки пака."""
bad = 0
# 1. касса смотрит в СВОЁ сырьё, а не в сырьё эксп-22
if M22.OUT != OUT:
print("[ПРОВАЛ] касса не пере-настроена на своё сырьё")
bad += 1
if M22.BUY.OUT != OUT:
print("[ПРОВАЛ] buy.OUT указывает мимо каталога пака — покупки уедут к соседу")
bad += 1
# 2. цена считается для ВСЕХ моделей, включая новые (эксп-22 упал тут на середине скрина)
for m in ROSTER_ET.CANDIDATES:
try:
if not M22.BUY.cost(m, 4500, 0, 2500, 1000, 8000) > 0:
raise ValueError(m)
except Exception as e: # noqa: BLE001
print(f"[ПРОВАЛ] цена не считается: {m} ({type(e).__name__}: {e})")
bad += 1
# 3. в таблице кассы ДОЛЖНЫ быть новые кандидаты — иначе подставился чужой ростер
missing = [m for m in ("gpt-5.6-terra", "kimi-k3", "grok-4.5", "glm-5.2")
if m not in ROSTER_ET.CANDIDATES]
if missing:
print(f"[ПРОВАЛ] в таблице кассы нет новых кандидатов: {missing}")
bad += 1
# 4. потолки фаз не превышают пакетный
if sum(CEILINGS.values()) > PACK_CEILING + 1e-9:
print("[ПРОВАЛ] сумма фазовых потолков выше пакетного")
bad += 1
# 5. КАЖДЫЙ тег покрыт глобом GLOB_ALL — иначе покупка невидима для `model_expect`
for globs in TAGS.values():
for g in globs:
if not g.startswith("et-"):
print(f"[ПРОВАЛ] тег вне шаблона пака: {g}")
bad += 1
# 6. фриз-гейт обязан отказывать коду вне зоны пака
import inspect # noqa: PLC0415
if M22.ZONE != ZONE:
print("[ПРОВАЛ] фриз-гейт сторожит чужую зону")
bad += 1
_ = inspect
print(f"касса: тегов {sum(len(v) for v in TAGS.values())} · моделей "
f"{len(ROSTER_ET.CANDIDATES)} · провалов {bad}")
return bad
if __name__ == "__main__":
if "--selftest" in sys.argv:
sys.exit(1 if selftest() else 0)
report()

160
eval/editor_tier/roster.py Normal file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""РОСТЕР ПАКА «СИЛЬНЫЙ ТИР РЕДАКТОРСКОЙ РОЛИ». $0.
Пак закрывает дыру эксп-22: промт разрешал до 16 кандидатов и требовал у КАЖДОГО провайдера два
тира дешёвый и сильный; сессия срезала до 11 и снесла сильный тир четырёх провайдеров. Отсюда
вывод «`deepseek-v4-pro` побеждает» верен только ВНУТРИ панели. Здесь берутся ровно те четверо.
Ростер НЕ дублирует эксп-22, а расширяет его: `tenant_panel.roster` остаётся источником для армов,
которые пак переиспользует (якорь `deepseek-v4-flash`, боевой редактор `deepseek-v4-pro`,
пограничный `glm-5`). Дублирование прайс-таблицы уже стоило эксп-22 падения прогона на середине
скрина, и повторять его незачем.
Цены с ВЕНДОР-страниц, снятых 09.08.2026, а не из памяти и не из оценки. Слаги живой листинг
`/models` того же дня (`role_topology/list_models.py`): все четыре присутствуют.
"""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "tenant_panel"))
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
# ⚠ Ростер эксп-22 грузится ПО ПУТИ, а не `import roster`. Причина механическая и поймана первым
# же прогоном: файл называется так же, и при прямом запуске Python находит сам себя — циклический
# импорт с пустым модулем. Явная загрузка под своим именем снимает зависимость от порядка путей.
def _load(name: str, path: Path):
import importlib.util # noqa: PLC0415
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
R22 = _load("roster22", REPO / "eval" / "tenant_panel" / "roster.py")
# (base_url, env-ключ, $/M вход, $/M кэш, $/M выход, роль, провенанс цены)
NEW: dict[str, tuple] = {
"gpt-5.6-terra": ("https://api.openai.com/v1", "OPENAI_API_KEY",
2.00, 0.20, 12.00, "editor",
"developers.openai.com/api/docs/pricing, live 09.08 (standard tier)"),
"kimi-k3": ("https://api.moonshot.ai/v1", "KIMI_API_KEY",
3.00, 0.30, 15.00, "editor",
"platform.moonshot.ai (сохранено prices/kimi_k3.html 08.08): "
"cache-miss 3.00 / cache-hit 0.30 / out 15.00"),
"grok-4.5": ("https://api.x.ai/v1", "XAI_API_KEY",
2.00, 0.30, 6.00, "editor",
"docs.x.ai/docs/models, live 09.08; тариф <200k контекста — наш случай (вход ~4.5k)"),
"glm-5.2": ("https://api.z.ai/api/paas/v4", "ZAI_API_KEY",
1.40, 0.26, 4.40, "editor",
"docs.z.ai/guides/overview/pricing (сохранено prices/zai.html 08.08)"),
}
# Полная таблица = переиспользуемые армы эксп-22 + новые кандидаты. Пересечений быть не должно:
# если вендор завёл слаг, уже известный эксп-22, это надо заметить, а не тихо перекрыть.
_dup = set(NEW) & set(R22.CANDIDATES)
if _dup:
raise SystemExit(f"слаг есть и в ростере эксп-22, и в новом: {_dup}")
CANDIDATES: dict[str, tuple] = {**R22.CANDIDATES, **NEW}
# Режим биллинга размышления наследуется; у новых моделей он НЕ ОБЪЯВЛЕН вендором и будет снят
# замером в фазе A. До замера считаем консервативно — как аддитивный, то есть дороже.
REASONING_BILLING = {**R22.REASONING_BILLING,
"grok-4.5": "additive"} # grok-4.3 аддитивен (quirks); семейство то же
# ⚠ ОЖИДАНИЕ ЦЕНЫ РЕДАКТОРСКОЙ КЛЕТКИ — из ЗАМЕРОВ эксп-22, не из прайса. Считается как
# отношение «замеренная цена клетки / цена выхода» внутри ТОГО ЖЕ семейства, потому что отношение
# определяется поведением размышления, а оно у вендора своё. У Kimi отношение вчетверо хуже всех:
# `k2.6` жёг бюджет на размышление и отдавал пустой выход.
EXPECT_PER_UNIT = {
"gpt-5.6-terra": 12.00 * 0.00364, # отношение luna: 0.0043745 / 1.20
"kimi-k3": 15.00 * 0.01690, # отношение k2.6: 0.0677265 / 4.00 ⚠ ~$0.25
"grok-4.5": 6.00 * 0.00581, # отношение grok-4.3: 0.0145165 / 2.50
"glm-5.2": 4.40 * 0.00407, # отношение glm-5: 0.013033 / 3.20
}
def cost(model: str, prompt_tokens: int, cached: int, completion_tokens: int,
reasoning_tokens: int = 0, total_tokens: int = 0) -> float:
"""Цена вызова по СВОЕЙ таблице. Считает `tenant_panel.roster`, но по расширенным CANDIDATES —
иначе новые слаги дают KeyError на середине прогона (тот самый отказ эксп-22 §2.2). Сигнатура
повторяет исходную побайтно: касса зовёт её позиционно."""
saved_c, saved_b = R22.CANDIDATES, R22.REASONING_BILLING
R22.CANDIDATES, R22.REASONING_BILLING = CANDIDATES, REASONING_BILLING
try:
return R22.cost(model, prompt_tokens, cached, completion_tokens,
reasoning_tokens, total_tokens)
finally:
R22.CANDIDATES, R22.REASONING_BILLING = saved_c, saved_b
# БОЕВАЯ КОНФИГУРАЦИЯ РАЗМЫШЛЕНИЯ новых кандидатов — из quirks и поведения СОСЕДА ПО СЕМЕЙСТВУ,
# не из вкуса. Гардрейл DeepSeek (не гасить) новых моделей не касается: их тут нет.
THINK_ET = {
"gpt-5.6-terra": ("effort", "low"), # как luna того же семейства (эксп-22 §0.5)
"kimi-k3": ("none", ""), # ручки эффорта у Kimi в quirks не описано (эксп-22 §2.3)
"grok-4.5": ("none", ""), # grok-4.3: reasoning-off только явным none; дефолт low
"glm-5.2": ("extra_body_disable", ""), # как glm-5/glm-4.7 — так шёл арм B эксп-21
}
OPENAI_FAMILY_ET = R22.OPENAI_FAMILY | {"gpt-5.6-terra"}
FIXED_TEMP_ET = {**R22.FIXED_TEMP, "kimi-k3": 1.0} # у k2.6 иное → HTTP 400; семейство то же
def by_new_price() -> list[str]:
"""Порядок замера — по ВОЗРАСТАНИЮ цены выхода: кончится потолок — непокрытыми останутся
дорогие, и это будет объявлено числом, а не скрыто."""
return sorted(NEW, key=lambda m: NEW[m][4])
def call_kwargs(model: str, msgs: list[dict], max_out: int = 16000,
temperature: float = 0.3) -> dict:
"""Тело вызова в боевой конфигурации. Считает `tenant_panel.roster`, но по РАСШИРЕННЫМ
таблицам иначе новый слаг падает с KeyError в `THINK`."""
sv = (R22.CANDIDATES, R22.THINK, R22.OPENAI_FAMILY, R22.FIXED_TEMP)
R22.CANDIDATES = CANDIDATES
R22.THINK = {**R22.THINK, **THINK_ET}
R22.OPENAI_FAMILY, R22.FIXED_TEMP = OPENAI_FAMILY_ET, FIXED_TEMP_ET
try:
return R22.call_kwargs(model, msgs, max_out, temperature)
finally:
R22.CANDIDATES, R22.THINK, R22.OPENAI_FAMILY, R22.FIXED_TEMP = sv
def selfcheck() -> int:
"""Проверки, которые обязаны пройти ДО первой покупки."""
bad = 0
U = (4500, 0, 2500, 1000, 8000)
for m in NEW:
c = cost(m, *U)
if not c > 0:
print(f"[ПРОВАЛ] цена не считается: {m}")
bad += 1
for m in ("deepseek-v4-pro", "glm-5", "deepseek-v4-flash"):
if m not in CANDIDATES:
print(f"[ПРОВАЛ] переиспользуемый арм пропал из таблицы: {m}")
bad += 1
# цена унаследованных моделей не должна поехать от расширения таблицы
for m in ("deepseek-v4-pro", "glm-5", "grok-4.3"):
if abs(cost(m, *U) - R22.cost(m, *U)) > 1e-12:
print(f"[ПРОВАЛ] цена унаследованной модели поехала: {m}")
bad += 1
for m in NEW:
if m not in THINK_ET:
print(f"[ПРОВАЛ] боевая конфигурация размышления не объявлена: {m}")
bad += 1
kw = call_kwargs(m, [{"role": "user", "content": "x"}])
if "model" not in kw:
print(f"[ПРОВАЛ] тело вызова не строится: {m}")
bad += 1
if m in OPENAI_FAMILY_ET and "temperature" in kw:
print(f"[ПРОВАЛ] OpenAI-семейству шлётся temperature: {m}")
bad += 1
print(f"ростер: кандидатов {len(CANDIDATES)} (новых {len(NEW)}) · провалов {bad}")
return bad
if __name__ == "__main__":
sys.exit(1 if selfcheck() else 0)

164
eval/editor_tier/screen.py Normal file
View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""ФАЗА A — СКРИН СИЛЬНОГО ТИРА. Платный (потолок $1.10), идёт через кассу пака.
Пороги НЕ свои: берутся у эксп-22 побайтно (`screen.PRICE_CAP/CYR_MIN/ECHO_MAX`, `gates`,
`verdict`). Порог, взятый из прошлого пака без правки, единственная защита от подгонки состава
под желаемый результат, а состав здесь и есть предмет спора: пак существует потому, что эксп-22
срезал сильный тир.
ОСОБЫЙ РЕЖИМ `kimi-k3`, объявлен ДО покупок. По отношению «цена клетки / цена выхода» внутри
Kimi он ожидается ~$0.25/ед вчетверо выше порога роли, и `k2.6` уже отдал пустой выход на трёх
клетках из четырёх при 100% доли размышления. Полный скрин был бы покупкой предсказуемого провала
примерно за доллар. Берётся ОДНА клетка редакторской роли с жёстким потолком выхода: этого хватает
и на ценовой вердикт, и на второй замер отказного режима. Отдай он против ожидания годный дешёвый
выход досняли бы полным скрином отдельным решением.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
sys.path.insert(0, str(REPO / "eval" / "tenant_panel"))
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
def _load(name: str, path: Path):
"""⚠ Модули соседнего пака грузятся ПО ПУТИ, а не `import <имя>`. При прямом запуске своя
папка стоит в sys.path первой, и `import screen` находит ЭТОТ файл, а не скрин эксп-22
молчаливый само-импорт. Ловилось трижды подряд (ростер, касса, скрин), поэтому правило одно:
всё чужое только явной загрузкой."""
import importlib.util # noqa: PLC0415
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
TP = REPO / "eval" / "tenant_panel"
S22 = _load("screen22", TP / "screen.py")
MONEY = _load("money_et", ZONE / "money.py")
ROSTER = MONEY.ROSTER_ET
PHASE = "ФA"
NEW = list(ROSTER.NEW) # gpt-5.6-terra · kimi-k3 · grok-4.5 · glm-5.2
SPECIAL = {"kimi-k3": dict(units=1, roles=("editor",), max_out=4000)}
ROLES = ("translator", "editor")
def tag(model: str, role: str, uid: str) -> str:
return f"et-scr-{model}-{role}-{uid[:6]}"
def plan() -> list[tuple[str, str, dict]]:
"""Что будет куплено. Печатается ДО покупок и лежит во фризе."""
us = S22.units() # те же две единицы, что скринил эксп-22
out = []
for m in ROSTER.by_new_price():
sp = SPECIAL.get(m, {})
for role in sp.get("roles", ROLES):
for u in us[:sp.get("units", len(us))]:
out.append((m, role, u))
return out
def cmd_plan() -> None:
tot = 0.0
print(f"{'модель':16s}{'роль':12s}{'ед.':>4s}{'ожидание $':>12s}")
for m in ROSTER.by_new_price():
sp = SPECIAL.get(m, {})
n = len(sp.get("roles", ROLES)) * sp.get("units", len(S22.units()))
exp = ROSTER.EXPECT_PER_UNIT[m] * n
tot += exp
print(f"{m:16s}{'/'.join(sp.get('roles', ROLES)):12s}{n:4d}{exp:12.3f}"
+ (" ← особый режим" if m in SPECIAL else ""))
print(f"{'ИТОГО':16s}{'':12s}{'':4s}{tot:12.3f} потолок {MONEY.CEILINGS[PHASE]:.2f}")
if tot > MONEY.CEILINGS[PHASE]:
print("⛔ смета выше потолка фазы — СТОП, не резать молча")
raise SystemExit(1)
def cmd_run() -> None:
led = MONEY.Guarded(PHASE)
for m, role, u in plan():
t = tag(m, role, u["uid"])
if (MONEY.OUT / f"{t}.json").exists():
continue
msgs = S22.msgs_for(role, u)
kw = ROSTER.call_kwargs(m, msgs, max_out=SPECIAL.get(m, {}).get("max_out", 16000))
rec = MONEY.purchase(led, t, m, S22.client(m), kw, role=role, uid=u["uid"])
if rec is None:
print(f" {t}: не куплено")
continue
print(f" {t}: ${rec.get('cost_usd', 0):.5f} finish={rec.get('finish')} "
f"знаков={len(rec.get('content') or '')}")
MONEY.report()
def result() -> dict:
"""Вердикт по каждому кандидату — порогами эксп-22, без единой правки."""
res = {}
for m in NEW:
rows = []
for f in MONEY.OUT.glob(f"et-scr-{m}-*.json"):
rec = json.loads(f.read_text(encoding="utf-8"))
u = next((x for x in S22.units() if x["uid"] == rec.get("uid")), None)
rows.append(S22.gates(rec, u["source"] if u else ""))
v, why = S22.verdict(rows)
pt = [r["cost"] for r in rows if r["role"] == "translator"]
pe = [r["cost"] for r in rows if r["role"] == "editor"]
import statistics as st # noqa: PLC0415
res[m] = dict(verdict=v, why=why, n=len(rows),
price_translator=(st.median(pt) if pt else None),
price_editor=(st.median(pe) if pe else None),
reasoning_share=(max((r["reasoning_share"] for r in rows), default=0.0)),
han=sum(r["han_chars"] for r in rows))
return res
def cmd_score() -> None:
print(f"{'модель':16s}{'вердикт':>10s}{'ед.':>4s}{'$/ед transl':>13s}{'$/ед edit':>11s}"
f"{'размышл':>9s} причины")
for m, r in result().items():
print(f"{m:16s}{r['verdict']:>10s}{r['n']:4d}"
f"{(r['price_translator'] or 0):13.5f}{(r['price_editor'] or 0):11.5f}"
f"{r['reasoning_share']:9.1%} {'; '.join(r['why'])}")
ok = [m for m, r in result().items() if r["verdict"] == "прошёл"]
print(f"\nпрошли фазу A: {ok or ''}")
def selfcheck() -> int:
"""До покупок: пороги те же, план в потолке, теги уникальны."""
bad = 0
if (S22.PRICE_CAP, S22.CYR_MIN, S22.ECHO_MAX) != ({"translator": 0.02, "editor": 0.06},
0.5, 0.15):
print("[ПРОВАЛ] пороги эксп-22 изменились — состав пака стал бы подгонкой")
bad += 1
tags = [tag(m, r, u["uid"]) for m, r, u in plan()]
if len(tags) != len(set(tags)):
print("[ПРОВАЛ] теги покупок не уникальны — клетки перетрут друг друга")
bad += 1
if not all(t.startswith("et-scr-") for t in tags):
print("[ПРОВАЛ] тег вне глоба фазы — расход будет невидим кассе")
bad += 1
for m in NEW:
if m not in ROSTER.THINK_ET:
print(f"[ПРОВАЛ] боевая конфигурация размышления не объявлена: {m}")
bad += 1
if "kimi-k3" not in SPECIAL:
print("[ПРОВАЛ] особый режим kimi-k3 потерян")
bad += 1
print(f"скрин: клеток в плане {len(tags)} · провалов {bad}")
return bad
if __name__ == "__main__":
a = sys.argv[1:] or ["--plan"]
fn = {"--plan": cmd_plan, "--run": cmd_run, "--score": cmd_score,
"--selfcheck": lambda: sys.exit(1 if selfcheck() else 0)}.get(a[0])
if fn:
fn()
else:
print(__doc__)

View file

@ -196,12 +196,40 @@ def chunks() -> list[dict]:
return [c for c in out if MIN_CHARS <= len(c["text"]) <= MAX_CHARS] return [c for c in out if MIN_CHARS <= len(c["text"]) <= MAX_CHARS]
def _pinned_uids() -> list[str]:
"""uid единиц, ЗАФИКСИРОВАННЫЕ манифестом пака при отборе."""
f = OUT / "manifest.json"
if not f.exists():
return []
try:
return list(json.loads(f.read_text(encoding="utf-8")).get("zh", {}).get("uids", []))
except (ValueError, AttributeError):
return []
def units_zh(n: int = N_ZH) -> list[dict]: def units_zh(n: int = N_ZH) -> list[dict]:
"""N единиц: дедуп по подписи, ключ — хеш источника, отбор из СЕРЕДИНЫ длин (эксп-21 §0). """N единиц: дедуп по подписи, ключ — хеш источника, отбор из СЕРЕДИНЫ длин (эксп-21 §0).
Из середины чтобы единицы были боевого размера и при этом не выбросами; ключ сортировки Из середины чтобы единицы были боевого размера и при этом не выбросами; ключ сортировки
(длина, uid), а не одна длина: при равной длине порядок иначе решает обход множества. (длина, uid), а не одна длина: при равной длине порядок иначе решает обход множества.
ПРИКОЛОТО К МАНИФЕСТУ. Отбор «из середины длин» считается от того, что лежит на диске, и
поймано это исполнением: доскачка глав для следующего пака СДВИНУЛА единицы уже сданного
эксп-22 (главы 318 превратились в 2,5,941), то есть его гейты стали считать по другому
материалу, чем куплен. Детерминизм прогона цель 6 канона владельца, и «воспроизводимо, пока
никто не трогал каталог» ей не отвечает. Поэтому если манифест отбора существует, единицы
берутся ИМЕННО ЕГО uid; правило отбора применяется только когда манифеста ещё нет.
""" """
pins = _pinned_uids()
if pins:
byid = {}
for c in chunks():
byid.setdefault(uid_of(c["text"]),
dict(source=c["text"], uid=uid_of(c["text"]), chapter=c["chapter"]))
miss = [u for u in pins if u not in byid]
if miss:
raise SystemExit(f"единицы манифеста не находятся в материале: {miss}")
return [byid[u] for u in pins]
uniq: dict[str, dict] = {} uniq: dict[str, dict] = {}
seen: set[str] = set() seen: set[str] = set()
for c in chunks(): for c in chunks():

View file

@ -61,6 +61,26 @@ TAGS = {
# отклонена в пользу полной мощности панели. # отклонена в пользу полной мощности панели.
CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 3.00} CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 3.00}
PACK_CEILING = 6.50 PACK_CEILING = 6.50
GLOB_ALL = "tp*.json" # по какому шаблону касса видит СВОИ покупки
def configure(**kw) -> None:
"""Пере-настроить кассу под ДРУГОЙ пак, не копируя её.
Касса проверенный механизм: фриз-гейт, атомарный замок, проекционный сторож, два пути счёта.
Копировать её ради нового пака значит копировать и будущие расхождения; поэтому пак-специфика
(зона кода, каталог сырья, теги, потолки, прайс-таблица) вынесена в параметры. Дефолты
оставлены эксп-22 побайтно вызова `configure` в его коде нет, и поведение не меняется.
Тот же приём уже ратифицирован в судейском риге (`absjudge.configure`).
"""
g = globals()
for k, v in kw.items():
if k not in g:
raise SystemExit(f"касса: неизвестный параметр configure: {k}")
g[k] = v
OUT.mkdir(parents=True, exist_ok=True)
BUY.OUT = OUT # сырьё пака
BUY.cost = ROSTER.cost # прайс-таблица пака
class Pack: class Pack:
@ -93,11 +113,11 @@ def model_expect(model: str) -> float:
множителем на скрытое размышление у провайдеров класса `additive_total`. множителем на скрытое размышление у провайдеров класса `additive_total`.
""" """
mine = [json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) mine = [json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)
for f in OUT.glob("tp*.json") for f in OUT.glob(GLOB_ALL)
if f.name.count("SELFTEST") == 0] if f.name.count("SELFTEST") == 0]
prices = [c for c, m in ((json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0), prices = [c for c, m in ((json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0),
json.loads(f.read_text(encoding="utf-8")).get("model")) json.loads(f.read_text(encoding="utf-8")).get("model"))
for f in OUT.glob("tp*.json")) if m == model and c > 0] for f in OUT.glob(GLOB_ALL)) if m == model and c > 0]
if len(prices) >= 2: if len(prices) >= 2:
return max(prices) return max(prices)
if model not in ROSTER.CANDIDATES: if model not in ROSTER.CANDIDATES:
@ -162,7 +182,10 @@ def _refuse_unfrozen() -> None:
if fr.filename.startswith("<") or not Path(fr.filename).is_file(): if fr.filename.startswith("<") or not Path(fr.filename).is_file():
raise SystemExit(f"⛔ покупка из НЕ-ФАЙЛА ({fr.filename}) запрещена") raise SystemExit(f"⛔ покупка из НЕ-ФАЙЛА ({fr.filename}) запрещена")
p = Path(fr.filename).resolve() p = Path(fr.filename).resolve()
if p.parent == ZONE and p.name in ("money.py",): # кадры САМОЙ кассы пропускаются по файлу, а не по каталогу: настроенная под другой пак
# касса лежит в зоне эксп-22, а покупает код из зоны того пака, и сверка по каталогу
# отвергала бы собственный же модуль (поймано до первой покупки нового пака)
if p == Path(__file__).resolve() or (p.parent == ZONE and p.name == "money.py"):
continue continue
if p.parent != ZONE: if p.parent != ZONE:
raise SystemExit(f"⛔ покупка из кода вне зоны эксп-22 запрещена: {p}") raise SystemExit(f"⛔ покупка из кода вне зоны эксп-22 запрещена: {p}")