textmachine/eval/tenant_panel/money.py

229 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""КАССА ЭКСП-22: потолки фаз + потолок пака + механический фриз-гейт. $0.
Три урока эксп-21, каждый стоил денег, и все три чинятся ЗДЕСЬ, а не дисциплиной:
1. новый тег покупок не попадал в глоб кассы, и фаза «стоила» ноль — трижды, последний раз
гейт ЭТО СЕРТИФИЦИРОВАЛ (§17.4). ⇒ теги объявлены в одном месте, и `selftest` проверяет,
что КАЖДЫЙ тег виден своей кассой и кассой пака;
2. потолки фаз промта пробивались, потому что касса кода охраняла другие границы. ⇒ каждая
покупка проходит ДВА гарда: фазовый и пакетный, оба проекционные;
3. $2.57 из $2.62 потрачены кодом вне зафризенного дерева. ⇒ покупка отказывает, если файл
вызывающего не отслеживается git или отличается от закоммиченного: «фриз ДО покупок фазы»
становится механизмом, а не обещанием.
Обход — только `TM_ALLOW_UNFROZEN=1` в окружении: громко, осознанно и видно в истории.
"""
from __future__ import annotations
import json
import os
import subprocess
import time
import sys
from functools import lru_cache
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
ZONE = Path(__file__).resolve().parent
sys.path.insert(0, str(REPO / "eval" / "role_topology"))
import buy as BUY # noqa: E402
import roster as ROSTER # noqa: E402
OUT = Path.home() / "books" / "tenant-panel"
OUT.mkdir(parents=True, exist_ok=True)
BUY.OUT = OUT # сырьё пака — своё, не эксп-21
# ⚠ ПРАЙС-ТАБЛИЦА ТОЖЕ СВОЯ. Поймано исполнением на середине скрина: `buy.priced` звал
# `role_topology/prices.cost`, где нет ни одной новой модели ростера — прогон упал на `kimi-k2.6`
# c KeyError. Хуже тихого падения было бы его отсутствие: у пересекающихся моделей таблица
# эксп-21 ставит кэш-цену Gemini равной входной, то есть цена считалась бы по устаревшему прайсу
# и расходилась с объявленным в пре-реге. Подмена делается ЗДЕСЬ, один раз и явно.
BUY.cost = ROSTER.cost
# Теги покупок по фазам. ⚠ Заводишь новый тег — вписываешь СЮДА тем же коммитом: ровно этого
# не сделали трижды в эксп-21, и расход фазы печатался заниженным.
TAGS = {
"Ф0": ("tp0-cont-*.json", "tp0-bank-*.json"),
"Ф1": ("tp1-scr-*.json",),
"Ф2": ("tp2-draft-*.json", "tp2-edit-*.json", "tp2-floor-*.json"),
"Ф3": ("tp3-ed-*.json",),
}
# ⚠ ПОТОЛОК Ф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
class Pack:
"""Пакетный потолок поверх фазового. Дублирует утиный интерфейс `buy.Ledger`."""
def __init__(self) -> None:
self.name, self.ceiling = "ПАК", PACK_CEILING
self.prefixes = tuple(p for ps in TAGS.values() for p in ps)
self._led = BUY.Ledger("ПАК", PACK_CEILING, self.prefixes, default_expect=0.05)
def spent(self) -> float:
return self._led.spent()
def expect(self) -> float:
self._led.spent()
return self._led.expect()
def afford(self) -> tuple[bool, str]:
return self._led.afford()
class Guarded:
"""Фазовая касса, которая не пускает покупку, пробивающую ЛЮБОЙ из двух потолков."""
def __init__(self, phase: str, default_expect: float = 0.02) -> None:
self.phase = phase
self.name = f"{phase} эксп-22"
self.ceiling = CEILINGS[phase]
self._led = BUY.Ledger(self.name, self.ceiling, TAGS[phase], default_expect=default_expect)
self._pack = Pack()
def spent(self) -> float:
return self._led.spent()
def expect(self) -> float:
self._led.spent()
return self._led.expect()
def afford(self) -> tuple[bool, str]:
ok, why = self._led.afford()
if not ok:
return ok, why
return self._pack.afford()
@lru_cache(maxsize=64)
def _git_clean(path: str) -> tuple[bool, str]:
"""Файл отслеживается git и совпадает с закоммиченным? Это и есть проверка фриза."""
rel = str(Path(path).resolve().relative_to(REPO))
r = subprocess.run(["git", "-C", str(REPO), "ls-files", "--error-unmatch", rel], # noqa: S603, S607
capture_output=True, text=True)
if r.returncode:
return False, f"файл не в git: {rel}"
d = subprocess.run(["git", "-C", str(REPO), "diff", "--quiet", "HEAD", "--", rel]) # noqa: S603, S607
if d.returncode:
return False, f"файл отличается от закоммиченного: {rel}"
return True, ""
def _refuse_unfrozen() -> None:
if os.environ.get("TM_ALLOW_UNFROZEN") == "1":
print("⚠ ФРИЗ-ГЕЙТ ОТКЛЮЧЁН (TM_ALLOW_UNFROZEN=1) — покупка вне фриза")
return
import inspect # noqa: PLC0415
for fr in inspect.stack()[1:]:
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",):
continue
if p.parent != ZONE:
raise SystemExit(f"⛔ покупка из кода вне зоны эксп-22 запрещена: {p}")
ok, why = _git_clean(str(p))
if not ok:
raise SystemExit(f"⛔ ФРИЗ НЕ ВЗЯТ: {why}\n"
" зафризь фазу коммитом ДО её покупок (промт, §ЗАКОН)")
return
raise SystemExit("⛔ вызывающий файл не определён")
# Транзиентные отказы провода, которые лечатся ожиданием, а не диагнозом. Поймано исполнением
# 08.08: `gemini-3.1-pro-preview` отдал HTTP 503 «This model is currently experiencing high
# demand» на ЧЕТЫРЁХ вызовах подряд ($0, клетки потеряны), а через минуту тот же слаг ответил с
# первой попытки; страница статуса Google при этом инцидентов не показывала. Без отступа
# обязательный кандидат Ф3 выпал бы из замера по причине, которая замером не является.
_TRANSIENT = ("503", "502", "504", "overloaded", "high demand", "timeout", "Timeout",
"RemoteProtocolError", "APIConnectionError")
def purchase(led: Guarded, tag: str, model: str, client, kw: dict, **extra) -> dict:
"""Один платный вызов. Кэш → фриз-гейт → проекционный гард → покупка (+ отступ на 5xx)."""
f = OUT / f"{tag}.json"
if f.exists():
return json.loads(f.read_text(encoding="utf-8"))
if not any(tag.startswith(p.split("*")[0]) for p in TAGS[led.phase]):
raise SystemExit(f"⛔ тег {tag!r} не покрыт глобом кассы {led.phase} — впиши в TAGS")
_refuse_unfrozen()
rec = {}
for k in range(4):
rec = BUY.purchase(led, tag, model, client, kw, **extra)
err = rec.get("error", "") if rec.get("finish") == "error" else ""
if not err or not any(t in err for t in _TRANSIENT):
return rec
wait = 15 * (k + 1)
print(f" транзиентный отказ ({err[:60]}) — отступ {wait} с")
time.sleep(wait)
return rec
def report() -> None:
pack = Pack()
print(f"{'фаза':6s}{'потрачено':>12s}{'потолок':>10s} теги")
for ph in ("Ф0", "Ф1", "Ф2", "Ф3"):
g = Guarded(ph)
print(f"{ph:6s}{g.spent():12.6f}{g.ceiling:10.2f} {' '.join(TAGS[ph])}")
print(f"{'ПАК':6s}{pack.spent():12.6f}{PACK_CEILING:10.2f}")
def selftest() -> int:
"""Проверка кассы НА СВОИХ ТЕГАХ до покупок: каждый тег виден и фазой, и паком."""
bad = 0
# ⚠ ПО ОДНОМУ ТЕГУ ЗА РАЗ. Если писать все пробы сразу, `spent() > 0` у последней кассы
# доказывается ЧУЖИМ тегом — ровно та слепота глоба, которая в эксп-21 трижды дала
# заниженный расход фазы.
base_pack = Pack().spent()
for ph, globs in TAGS.items():
base_ph = Guarded(ph).spent()
for g in globs:
stem = g.split("*")[0] + "SELFTEST"
f = OUT / f"{stem}.json"
f.write_text(json.dumps(dict(tag=stem, cost_usd=0.011)), encoding="utf-8")
try:
d_ph = Guarded(ph).spent() - base_ph
d_pack = Pack().spent() - base_pack
ok = abs(d_ph - 0.011) < 1e-9 and abs(d_pack - 0.011) < 1e-9
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] тег {stem} даёт +{d_ph:.3f} кассе {ph} "
f"и +{d_pack:.3f} кассе пака")
finally:
f.unlink()
# Проекционный гард обязан ОТКАЗЫВАТЬ, когда ожидание пробивает потолок.
g = Guarded("Ф0")
g._led.ceiling = 0.0 # noqa: SLF001
ok, why = g.afford()
bad += ok
print(f"[{'OK ' if not ok else 'ПРОВАЛ'}] проекционный гард отказывает при нулевом потолке")
# Прайс: КАЖДАЯ модель ростера обязана считаться кассой, иначе покупка падает на середине.
for m in ROSTER.CANDIDATES:
try:
BUY.cost(m, 1000, 0, 1000, 0, 2000)
except Exception as e: # noqa: BLE001
bad += 1
print(f"[ПРОВАЛ] {m}: касса не умеет считать цену ({type(e).__name__})")
print(f"[OK ] цена считается для всех {len(ROSTER.CANDIDATES)} моделей ростера"
if not bad else "")
# Фриз-гейт: НЕ-файл (встроенный питон) обязан быть отвергнут.
try:
exec(compile("import money; money._refuse_unfrozen()", "<string>", "exec"), # noqa: S102
{"__name__": "x"})
print("[ПРОВАЛ] фриз-гейт пропустил покупку из НЕ-файла")
bad += 1
except SystemExit:
print("[OK ] фриз-гейт отвергает покупку из НЕ-файла")
print(f"\n{'КАССА ГОДНА' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
if __name__ == "__main__":
a = sys.argv[1:] or ["--report"]
if a[0] == "--selftest":
sys.exit(1 if selftest() else 0)
report()