229 lines
12 KiB
Python
229 lines
12 KiB
Python
#!/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()
|