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:
parent
210b38ec98
commit
c0dd228d64
7 changed files with 746 additions and 3 deletions
119
eval/conformance.py
Normal file
119
eval/conformance.py
Normal 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())
|
||||
127
eval/editor_tier/material.py
Normal file
127
eval/editor_tier/material.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python3
|
||||
"""МАТЕРИАЛ ПАКА: 16 НОВЫХ единиц той же книги. $0 — глава уже скачана эксп-22.
|
||||
|
||||
Зачем новые единицы. Контраст `R2 glm-5` против боевого редактора дал −2.12 при шуме одного вызова
|
||||
sd≈3.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
122
eval/editor_tier/money.py
Normal 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
160
eval/editor_tier/roster.py
Normal 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
164
eval/editor_tier/screen.py
Normal 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__)
|
||||
|
|
@ -196,12 +196,40 @@ def chunks() -> list[dict]:
|
|||
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]:
|
||||
"""N единиц: дедуп по подписи, ключ — хеш источника, отбор из СЕРЕДИНЫ длин (эксп-21 §0).
|
||||
|
||||
Из середины — чтобы единицы были боевого размера и при этом не выбросами; ключ сортировки
|
||||
(длина, uid), а не одна длина: при равной длине порядок иначе решает обход множества.
|
||||
|
||||
⚠ ПРИКОЛОТО К МАНИФЕСТУ. Отбор «из середины длин» считается от того, что лежит на диске, и
|
||||
поймано это исполнением: доскачка глав для следующего пака СДВИНУЛА единицы уже сданного
|
||||
эксп-22 (главы 3–18 превратились в 2,5,9…41), то есть его гейты стали считать по другому
|
||||
материалу, чем куплен. Детерминизм прогона — цель 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] = {}
|
||||
seen: set[str] = set()
|
||||
for c in chunks():
|
||||
|
|
|
|||
|
|
@ -61,6 +61,26 @@ TAGS = {
|
|||
# отклонена в пользу полной мощности панели.
|
||||
CEILINGS = {"Ф0": 0.40, "Ф1": 1.05, "Ф2": 2.00, "Ф3": 3.00}
|
||||
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:
|
||||
|
|
@ -93,11 +113,11 @@ def model_expect(model: str) -> float:
|
|||
множителем на скрытое размышление у провайдеров класса `additive_total`.
|
||||
"""
|
||||
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]
|
||||
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"))
|
||||
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:
|
||||
return max(prices)
|
||||
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():
|
||||
raise SystemExit(f"⛔ покупка из НЕ-ФАЙЛА ({fr.filename}) запрещена")
|
||||
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
|
||||
if p.parent != ZONE:
|
||||
raise SystemExit(f"⛔ покупка из кода вне зоны эксп-22 запрещена: {p}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue