#!/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 не двигался. # ⚠ ПОТОЛКИ Ф3 И ПАКА ПОДНЯТЫ 08.08 СЛОВОМ ВЛАДЕЛЬЦА, объявлено ДО покупок Ф3: $1.60 → $3.00 и # $5.00 → $6.50. Причина — замер, а не желание: обязательный кандидат промта # `gemini-3.1-pro-preview` тарифицирует 11 144–12 010 выходных токенов на ответ в 2 508 (скрытое # размышление видно ТОЛЬКО в `total_tokens`), то есть $0.152 за редакторскую единицу и $2.44 за # 16 единиц — больше всего прежнего потолка фазы. Альтернатива «подмножество единиц» владельцем # отклонена в пользу полной мощности панели. 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: """Пакетный потолок поверх фазового. Дублирует утиный интерфейс `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() def model_expect(model: str) -> float: """Ожидаемая цена следующего вызова ИМЕННО ЭТОЙ модели. ⚠ Заведено после того, как потолок Ф1 был пробит ($1.087758 против $1.05). Механизм пробоя: проекционный гард брал p95 УЖЕ КУПЛЕННЫХ вызовов — то есть прошлое, в котором новой модели ещё не было. `gemini-3.1-pro-preview` оказался в семь раз дороже любого предшественника, и оценка «как раньше» его не удержала. Здесь ожидание берётся по СВОИМ записям этой модели, а при их отсутствии считается из прайса по медианным токенам пака — консервативно, с множителем на скрытое размышление у провайдеров класса `additive_total`. """ mine = [json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0) 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(GLOB_ALL)) if m == model and c > 0] if len(prices) >= 2: return max(prices) if model not in ROSTER.CANDIDATES: return max(mine or [0.02]) _, _, pin, _pc, pout, _t, _s = ROSTER.CANDIDATES[model] hidden = 5.0 if ROSTER.REASONING_BILLING.get(model) in ("additive_total", "additive") else 1.5 return 4500 / 1e6 * pin + 3000 * hidden / 1e6 * pout 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() self.next_model: str | None = None def spent(self) -> float: return self._led.spent() def expect(self) -> float: self._led.spent() base = self._led.expect() return max(base, model_expect(self.next_model)) if self.next_model else base def afford(self) -> tuple[bool, str]: sp = self.spent() exp = self.expect() if sp + exp > self.ceiling: return False, (f"[СТОП {self.name}] потрачено ${sp:.6f} + ожидание ${exp:.6f} " f"= ${sp + exp:.6f} > потолок ${self.ceiling:.2f}") psp, pexp = self._pack.spent(), max(self._pack.expect(), exp) if psp + pexp > PACK_CEILING: return False, (f"[СТОП ПАК] потрачено ${psp:.6f} + ожидание ${pexp:.6f} " f"> потолок пака ${PACK_CEILING:.2f}") return True, "" @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() # кадры САМОЙ кассы пропускаются по файлу, а не по каталогу: настроенная под другой пак # касса лежит в зоне эксп-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}") 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 _claim(tag: str) -> bool: """Атомарно занять клетку. False — её уже покупает другой процесс. ⚠ Заведено исполнением: панель покупалась НЕСКОЛЬКИМИ процессами, и проверка «файла ещё нет» у `buy.purchase` не атомарна — два воркера могли одновременно пройти её и купить одну клетку ДВАЖДЫ. На диске остаётся одна запись, поэтому вторая оплата в леджер не попадает вовсе: деньги потрачены, а касса их не видит. Это тот же класс, что «расход фазы мимо глоба», только хуже — незаметный по построению. """ try: fd = os.open(str(OUT / f"{tag}.lock"), os.O_CREAT | os.O_EXCL | os.O_WRONLY) except FileExistsError: return False os.close(fd) return True 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() if not _claim(tag): # клетку уже покупает другой процесс f = OUT / f"{tag}.json" for _ in range(240): if f.exists(): return json.loads(f.read_text(encoding="utf-8")) time.sleep(5) print(f" ⚠ замок {tag} протух — покупаю сам") (OUT / f"{tag}.lock").unlink(missing_ok=True) _claim(tag) led.next_model = model # гард считает ожидание по ЭТОЙ модели rec = {} try: 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 finally: (OUT / f"{tag}.lock").unlink(missing_ok=True) 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.ceiling = 0.0 ok, _why = g.afford() bad += ok print(f"[{'OK ' if not ok else 'ПРОВАЛ'}] проекционный гард отказывает при нулевом потолке") # ⚠ РЕГРЕССИЯ НА ПРОБОЙ ПОТОЛКА Ф1: ожидание обязано считаться по ЭТОЙ модели, а не по # p95 прошлого. Дорогая новая модель не должна проходить гард только потому, что раньше # таких дорогих вызовов не было. g2 = Guarded("Ф0") g2.ceiling = g2.spent() + 0.05 g2.next_model = "gemini-3.1-pro-preview" ok2, _ = g2.afford() bad += ok2 print(f"[{'OK ' if not ok2 else 'ПРОВАЛ'}] гард видит цену дорогой НОВОЙ модели " f"(${model_expect('gemini-3.1-pro-preview'):.4f}), а не p95 прошлого") # Прайс: КАЖДАЯ модель ростера обязана считаться кассой, иначе покупка падает на середине. 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()", "", "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()