textmachine/docs/architecture/15-money-path.md

231 lines
70 KiB
Markdown
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.

# Путь денег — от гранта до settle (топикальный вход; строка 167 ЗАКРЫТА D39.200, файл живёт сам по себе)
> **Статус: топикальный ВХОД, НЕ источник истины.** Док собирает денежную тему, размазанную по D-нотам, реестру дефектов платформы, глоссарию и бэклогу, в один маршрут чтения. При конфликте с чем угодно побеждает `docs/architecture/05-decisions-log.md` (правило чтения — диета D39.125: живой файл + слайсы). Каждое утверждение здесь снабжено указателем на первичный носитель; формулы — дословно оттуда. Словарь терминов (холд · прирост · аргумент потолка · baseline · settle · leftover-reserved · SpendBound) — `docs/glossary.md` §«Деньги платформы» (`docs/glossary.md:58`=`**Деньги платформы:**`).
>
> ⚠ **Форма указателей — норма этого дока (31.08, пере-проверены ВСЕ указатели: 34 уехали, 1 мёртв, 16 верны).**
> **Стабильна не ЗОНА, а ЧИСТЫЙ ФАЙЛ.** Номер строки ставится только туда, где `git status <цель>` показывает файл чистым; файл с незакоммиченной правкой — чей угодно, включая `docs/` — получает греп-указатель без номера. Ось именно такая — ратифицировано **D39.179 п.4**, который уточняет D39.177 §2в и прямо объявляет зонную ось НЕВЕРНОЙ (там же: «Носитель нормы — шапка 15-money-path.md»), и зонная ось её НЕ заменяет: `docs/architecture/14-api-contract/openapi.yaml` ЛЕЖАЛ в «своей» зоне и при этом ДВИГАЛСЯ паком контракта 0.9.0 (заленджен 31.08, `0a680a3`, D39.180).
> Токен-форма — `` `путь:N`=`ожидаемая подстрока` ``; её гейт `docs/scripts/counts.py --lint` сверяет ПО СОДЕРЖИМОМУ и краснеет, когда цель уезжает.
> ⚠ **Что гейт берёт с якоря БЕЗ токена:** существование файла и что номер не больше длины файла; у голого имени журнала решений — ещё и структуру (владелец строки обязан совпасть с названной рядом нотой). НЕ проверяется только СОДЕРЖИМОЕ — вот почему 34 якоря уехали молча. Голое имя без «/» не судится вовсе.
> ⚠ **Греп-форма не проверяется гейтом никак** (регексп якоря требует `:номер`) — осознанный размен: переживает чужой WIP, но молчит.
> ⚠ **Токен НЕ должен содержать бэктик:** регексп берёт токен как «всё до следующего бэктика» и обрезает хвост, а необрезанная часть не проверяется вовсе.
## 1. Канон одним абзацем
Деньги видимы с первого вызова: телеметрия, потолки, явное согласие на пере-оплату (CLAUDE.md, цель 5). Леджер без потолка запрещён — Р7: книга обязана нести хотя бы один из `book_usd`/`day_usd` (`backend/internal/config/book.go:332`=`a ledger with no ceiling is forbidden`, текст валидатора: «a ledger with no ceiling is forbidden, Р7»). ⚠ **ИСПР. 05.09 — ЗАПРЕТ ОТОЗВАН ВЛАДЕЛЬЦЕМ (D39.196, эррата 05.09-а): баланс аккаунта, потолок заказа и холд ВЫХОДЯТ наружу деньгами;** запрещённым осталось другое и оно не отменялось — цены моделей, стоимость стадий и вызовов, структура НАШИХ расходов (ПТ-33). Ниже — прежняя редакция, она объясняет, ОТКУДА запрет взялся и почему его половина жива: суммы на пользовательский экран НЕ выходят — запрет D39.84, подтверждён D39.100/К-8 (дословно: «страница лимитов/использования в настройках (СТАТУС, не суммы — запрет денег на экране D39.84 в силе и НЕ superseded)» — `docs/architecture/05-decisions-log.md`, греп `^## D39.100` → пункт К-8, дословная подстрока `СТАТУС, не суммы — запрет денег на экране`; ⚠ адресуем НОМЕРОМ ноты, а не строкой журнала) и держится в контракте (`docs/architecture/14-api-contract/openapi.yaml`, греп `State of the credit balance. No window` — ⚠ номер не ставим: контракт переверстывается минорами, греп-форма дешевле поддержки). Защита от перерасхода двойная и не зависит от событий: холд платформы делает кредит недоступным следующему прогону, а движок сам останавливается на потолке — «an overspend is impossible even while the platform is blind. The event stream is freshness only» (док-коммент `Hold``platform/internal/pgstore/credits.go`, греп `an overspend is impossible even`).
## 2. Деньги ДВИЖКА (зона `backend/`)
- **Леджер SQLite.** Таблица `spend`, две фигуры: `committed_usd` (потрачено, с сырым ответом провайдера) и `reserved_usd` (зарезервировано под летящий вызов). `Reserve` перед каждым платным вызовом сверяет `SUM(committed_usd + reserved_usd)` книги и дня с потолками (`backend/internal/store/ledger.go:44,58,63`=`func (s *Store) Reserve(`; Р7 — коммент `backend/internal/store/ledger.go:12`=`per-day (Р7); checked against`); `SettleWithCheckpoint` одной транзакцией конвертирует резервацию в committed и персистит ответ (`backend/internal/store/ledger.go:178`=`func (s *Store) SettleWithCheckpoint(`). ⚠ У ДВИЖКОВОГО `store.Store` метода `Settle` нет — он называется `SettleWithCheckpoint` (словарь кода: `backend/internal/store/ledger.go:11`=`Reserve/Settle/ReleaseReservation semantics`). ⚠ Не спутать с ПЛАТФОРМЕННЫМ `credits.Settle` (§3 п.8) — это другой `Store`. Суммарно по книге — `SpentUSD` (`backend/internal/store/ledger.go:400`=`SpentUSD reports (committed, reserved)`).
- **Гейт потолков — ПЕР-ВЫЗОВНЫЙ** (`Reserve` на каждый свежий attempt). На границе юнита сидел РЕПЭЙР-суб-бюджет — ужесточён до пер-вызовного с ценой вызова (строка 135 закрыта D39.131); эскалационный кап хоп НЕ прицениваает — перелёт ≤1 хопа, задокументирован и запинен (диспозиция D39.131 п.2д, реопен — живой инцидент). ⚠ **ТРЕТЬЕ семейство, которого перечень не знал (доп. ревизией 02.09): банк-роли несут СОБСТВЕННЫЕ КНИГО-ШИРОКИЕ бюджеты**`gates.terminology.budget_usd` и `gates.terminology.classify_budget_usd`. Именно оно резало ОПЛАЧЕННУЮ работу на холодном прогоне 31.08 (D39.182 §4: инцидент был на классификаторе). С 31.08 (D39.182) план прохода режется ценой партии ДО первого вызова, а не обрывается посередине; усечение ВИДНО в отчёте — поля `BatchesDropped` и `ClassifyBatchesDropped` (`backend/internal/pipeline/terminologist.go`, греп `BatchesDropped`). ⚠⚠ **И сами цифры этих суб-бюджетов в книжных конфигах КАЛИБРОВАНЫ ПОД ИЮЛЬСКИЕ ЦЕНЫ** (тот же множитель ×4.47, D39.179 п.1): на холодном прогоне 31.08 `classify_budget_usd` 0.02 оборвал классификатор дважды, а поднятый до 0.08 `escalation.budget_usd` был пробит фактом до 0.103305. То есть суб-бюджеты режут ОПЛАЧЕННУЮ работу не по замыслу, а по протухшей калибровке.
- **leftover-reserved зануляется write-open.** `store.Open` (путь записи, каждый `translate`) выполняет `recoverReservations` (`backend/internal/store/store.go:110`=`s.recoverReservations(ctx)`; сама функция — `backend/internal/store/store.go:278-279`=`UPDATE spend SET reserved_usd = 0`) — файл владеется одним процессом, значит любой reserved на открытии принадлежит несеттлённому прогону. `OpenReadOnly` этого прохода намеренно НЕ делает (`backend/internal/store/store.go:124`=`does not run that pass`) ⇒ **reserved, увиденный read-only `status` В МОМЕНТ СПАВНА, — остаток мёртвого процесса** (несущий факт формулы PD-158, см. §3). ⚠ Но НЕ «всегда»: `OpenReadOnly` построен ровно затем, чтобы `status` работал ВО ВРЕМЯ живого прогона (`backend/internal/store/store.go:122`=`allows concurrent readers while a writer is live`), и конкурентный `status` покажет ЖИВУЮ резервацию между `Reserve` и settle. Узко формулирует и сам код: «after a run crashes, reserved_usd stays non-zero until the next WRITE command» (`backend/internal/store/store.go:130`=`after a run crashes, reserved_usd stays non-zero`), и платформа — «at spawn there is no other writer … so anything reserved is by construction a leftover, never a live promise» (`platform/internal/runs/spawn.go`, греп `never a live promise`).
- **`--max-units` — ОБЪЁМНЫЙ потолок прогона, ортогональный денежному** (D39.165 §1б, принят D39.170). Ограничивает не деньги, а РАБОТУ: не больше N выходных ЮНИТОВ (гранулярность `units_total` манифеста — та же, в которой платформа продаёт главы) возьмут СЛОТ гранта в этом прогоне; юниты, отданные за $0 (резюм, ре-пин), ретраи и эскалации внутри юнита потолок не тратят. ⚠ **«Слот» ≠ «оплата», и с 03.09 это РАЗНЫЕ числа** (пак «число согласия», строка бэклога 232): юнит, который прежний прогон НАЧАЛ и не отгрузил, дописывается ВНЕ гранта, поэтому грант N оплачивает ДО 2N выходных юнитов — замерено приёмкой на живом раннере (грант 2 → `Paid()=4`, шесть вызовов провайдера). Единственный денежный бонд здесь — `--ceiling-usd`. ⛔ **Семантика переноса НЕ ратифицирована — слово владельца 03.09 «подумаем на этот счёт»**, и до его решения читать это как замеренный факт, а не как норму. Принимает только `translate`. **Остановка по объёму — ЗАВЕРШЕНИЕ (exit 0), не пауза:** словарь кодов выхода не расширялся и нового значения `Finished.Outcome` тоже нет — ⚠ **но с 31.08 признак едет ЧИСЛАМИ в кадре `finished` шва, а не только прозой** (D39.181 п.2, закон раскрытия: прозаическая строка отчёта до потребителя потока не доезжала): носитель — `Finished.Volume`, леджер доставки. Различение при этом живёт и в отчёте прогона, и в логе; отчёт разводит ДОСТАВКУ и ПЕРЕ-ДЕЛКУ. Носитель — `backend/internal/pipeline/volume.go:13`=`the VOLUME ceiling — the run's second stop`; словарь флага дословно — `backend/cmd/tmctl/invocation.go:143`=`Stopping on it is a COMPLETION (exit 0), not a pause`. ⚠ Проводка в платформу ГЕЙЧЕНА (`PD-422`): единственный писатель признака движения банка — дверь правок, рост АВТО-банка от майнинга флага не ставит.
- **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment…» (`backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`; ⚠-коммент `backend/cmd/tmctl/main.go:261`=`--ceiling-usd is NOT a per-run budget`; многоточие закрывает обрыв цитаты — во флаге дальше стоит «, and must be > 0»). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:612`=`BookUSD: r.bookCeilingUSD()` и `backend/internal/pipeline/stagerun.go:699`=`overrides the book's ceilings.book_usd` (стоп = `errReserveCeiling`, `backend/internal/pipeline/escalation.go:49`=`var errReserveCeiling`).
- **Потолки — wiring, не семантика:** `Ceilings` намеренно исключены из `BriefHash` («Wiring fields (paths, ceilings, db) deliberately excluded», `backend/internal/config/book.go:345`=`Wiring fields (paths, ceilings, db) deliberately excluded`; канон — `backend/internal/config/book.go:369`=`canon := struct {`) ⇒ смена ДЕНЕЖНОГО потолка не двигает ни снапшот, ни ре-билл (D39.110 п.2б). ⚠ Верно ровно про ДЕНЬГИ: потолок СЕГМЕНТАЦИИ `edit_ceiling_out` в снапшот ВХОДИТ и меняет границы чанков (`backend/internal/pipeline/snapshot.go:34`=`EditCeilingOut int`).
- **`status --json` отдаёт фигуры платформе:** `committed_usd`, `reserved_usd`, `book_ceiling_usd`, `ceiling_pct` = 100·(committed+reserved)/book_ceiling (`backend/internal/pipeline/status.go:324`=`book_ceiling_usd,omitempty`; арифметика `ceiling_pct``backend/internal/pipeline/status.go:918`=`100 * (committed + reserved)`). Дневной фигуры в status нет (PD-157 — `platform/docs/DEFECT_REGISTER.md`, греп `PD-157`).
- **Леджер = НИЖНЯЯ граница; строка 78 снята с трекера 10.09, но её ЖИВОЙ ОСТАТОК несёт строка 377.** ⛔ **Испр. 10.09 — прежняя редакция этого пункта, написанная в тот же день, была НЕВЕРНА, и ошибка моя.** Она утверждала, будто оплаченный `2xx` с нечитаемым телом «прежде в `request_log` не попадал» и что это починил пак оборванных вызовов. Замер: ветвь `BilledDecodeError` писала свою строку с пометкой оценки ещё до пака — на `3f05fab^` это `stagerun.go:629`, введено коммитом `2f91b04` **24.07**, а пак этой ветви не касался вовсе (хитов `Estimated` в его диффе по `stagerun.go`**0**). И носитель я процитировал ЧУЖОЙ: `backend/internal/pipeline/cutcall.go:133` обслуживает `*llm.AttemptCutError` (`backend/internal/pipeline/stagerun.go:741`), а речь шла о `*llm.BilledDecodeError` (`stagerun.go:709`) — это разные классы, и второй до `cutcall.go` не доходит. **Что пак действительно купил:** строку `request_log` за вызов, который оборвали МЫ (прежде он падал в ветвь «ничего не куплено» и получал $0-строку) — вот это `cutcall.go:133`. **Что остаётся правдой:** леджер — нижняя граница, потому что обрыв, за которым ретрай УСПЕЛ, исчезает бесследно (строка **377**). Живой замер `D39.86` — 3 вызова из 14, неизвестность $0.015111 при леджере $0.114378.
- **ГРАНИЦА ДЕНЕГ ЗА ОБОРВАННЫЙ НАМИ ВЫЗОВ — ФАКТ ДОСТАВКИ, а не статус-строка** (пак строки 360, акт `D39.232`; слово владельца — `D39.230`). Деньги берутся, только когда провайдер подтвердил запрос ответным объектом: `backend/internal/llm/attemptcut.go:221`=`Billable: answered && tr.afterHeaders()`. Наш собственный дедлайн даёт свой класс `attempt_timeout` без ретрая; дедлайн вычисляется ИЗ БЮДЖЕТА вызова, а не назначается (`backend/internal/llm/attemptcut.go:269`=`func (p RetryProfile) deriveDeadline`), так что эскалация, удвоившая `max_tokens`, удваивает и время. Оплаченный обрыв получает чекпойнт, и резюм его НЕ перепокупает. ⛔ Пометка «это оценка» — УСЛОВИЕ слова владельца (`D39.230` п.1) — сегодня в движке живёт ТРЕМЯ носителями (колонка `Estimated` в `request_log``backend/internal/store/requestlog.go:41`; пара `estimated_rows`/`estimated_usd` в `status --json``backend/internal/pipeline/status.go:911`; печать оператору — `backend/cmd/tmctl/render.go:402`), но НАРУЖУ не выходит ни одним каналом шва: строка **382**. ⚠ Формулировка испр. 10.09 — прежняя («живёт только в `status --json`») звала следующего чинить пометку, которая уже есть.
- **Согласие на пере-оплату — ДВИЖКОВЫЙ гейт**. Прогон, который пере-покупает уже оплаченные юниты дороже порога, ОСТАНАВЛИВАЕТСЯ и требует явного согласия. Порог по умолчанию ратифицирован D20.2-Q2: `min($0.50, 5% × ProjectedBookUSD)`. Носитель — `backend/internal/pipeline/rebill.go`, греп `rebillConsentThreshold`; единственная ручка книги — `rebill_consent_usd` (валидатор `backend/internal/config/book.go`, греп `rebill_consent_usd`; 0 = ратифицированный дефолт). ⚠ Порог считается БЕЗ контура банк-ролей (D39.182 §3), то есть срабатывает позже, чем подсказывает полная проекция.
## 3. Деньги ПЛАТФОРМЫ по шагам (зона `platform/`)
1. **Грант.** `Grant` — строка леджера `kind='grant'`, идемпотентная по `(аккаунт, source, source_id)`: `on conflict (user_id, source, source_id) do nothing` (`platform/internal/pgstore/credits.go`, греп `func (s *Store) Grant(`; сама идемпотентность после пака sqlc уехала В ЗАПРОС — `platform/internal/pgstore/queries/credits.sql`, греп `on conflict (user_id, source, source_id) do nothing`). Фри-тир — тот же механизм в ТОЙ ЖЕ транзакции, что создание аккаунта (`platform/internal/pgstore/identity.go`, греп `appendLedger(ctx, tx, userID, "grant"`; ⚠ **дефолт `SignupGrantMicroUSD` — НОЛЬ** (`platform/internal/config/config.go`, греп `SignupGrantMicroUSD:`), а НЕ «5 × 1_000_000» — эта сумма ходила по докам и ложна: предложение «на бете дефолт в НОЛЬ» (D39.110 п.3) ИСПОЛНЕНО, `PD-104` в регистре стоит `fixed(P7, дерево сессии)`).
2. ⚠⚠ **ИСПР. 06.09 — ШКАЛЫ В ГЛАВАХ БОЛЬШЕ НЕТ (лендинг формы заказа, `D39.208`).** Объём заказывают
ТРЕМЯ формами: вся книга (дефолт) · до главы N · знаками; ползунок — ДЕНЕЖНЫЙ (`limit.min/max_micro_usd`,
минимум = неделимый шаг движка, и он может быть БОЛЬШЕ максимума, когда баланс не несёт даже одной
резервации). Ниже — прежняя редакция, историей: **Выбор потолка — шкала в ГЛАВАХ** (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в `book.yaml` не пишется). **Максимум = Balance КАК ЕСТЬ** (D39.115 п.2а; эррата в шапке D-лога — `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ`): `Hold` — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: `pricing.Model.Scale``affordable = balance / PerChapter`, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — `platform/internal/pricing/pricing.go`, греп `func (m Model) Scale(`.
3. ⛔⛔ **СТАВКИ ЗА ГЛАВУ БОЛЬШЕ НЕ СУЩЕСТВУЕТ — ни константы, ни ручки (испр. 06.09).** Цена берётся из
ПРОЕКЦИИ ДВИЖКА (`expected_usd` · `book_once_usd` · `step_max_usd` в манифесте), холд считается
`k × ожидаемое + шаг [+ книжный бонд]`. `PerChapterMicroUSD` удалён целиком; от `DefaultPerChapter`
в дереве остался ОДИН хит — надгробный комментарий `platform/internal/pricing/pricing.go` («Until
05.09 a chapter was worth a CONSTANT»), и грепать его как живой носитель нельзя. Прежняя редакция
историей: **Ставка $0.03/глава — конфиг-константа платформы:** `DefaultPerChapter = 30_000` micro-USD (`platform/internal/pricing/pricing.go`, греп `DefaultPerChapter`), переопределяется `PerChapterMicroUSD` (`platform/internal/config/config.go`, греп `PerChapterMicroUSD`). ⚠⚠ **ЧИСЛА НИЖЕ — ИЮЛЬСКИЕ ДЕНЬГИ И С 16.08 НЕ ДЕЙСТВУЮТ (испр. ревизией 02.09).** D39.179 п.1 ратифицировал: DeepSeek пере-пинен под цены, вступившие **16.08 16:00 UTC**, множитель к июльским деньгам **×4.47**. ⇒ по действующему прайсу та же выборка даёт среднюю главу **≈$0.051$0.085** и максимум **≈$0.168**, то есть константа $0.03 не завышена, а **ЗАНИЖЕНА**, и знак вывода перевернулся. ⚠ Иллюстрация живыми деньгами (НЕ калибровка): холодный прогон 31.08 дал полную цепь главы $0.093 и $0.151 при `projected_book_usd = 1.1377277707` — но это **n=2 главы из десяти**, прогон остановлен снапшот-гардом, промпт черновика был банкнотным вместо конвенционного, и сам отчёт запрещает такое употребление дословно (`archive/reports/COLDRUN_V16_REPORT_2026-08-31.md`, греп `калибровать ставку платформы`). **Провенанс ниже — ИСТОРИЯ (D39.165 §1, 28.08):** средняя **$0.0115$0.0190**, максимум одной главы $0.0375, «завышена в **1.62.6×**» — ⚠ но НЕ покрывает максимум: источник (D39.165 §1) добавляет «и отдельные главы её уже пробивают» ($0.0375 против $0.03 = ×1.25), и «купить 10 глав» отдаёт движку $0.30, покупающий ~1626 глав («денежная ручка с подписью „главы"», `PD-410`). РЕШЕНО: цена — от ОБЪЁМА ИСХОДНИКА (носитель `chapters.units_total`, пишется интейком за $0), настоящий стоп — объёмный потолок В ДВИЖКЕ (см. §2). ⛔ Константу по сегодняшним числам НЕ калибровать (D39.165 ⛔г) — ⚠ **испр. 05.09: гейт ПАЛ вместе со строкой 202** — живой платный прогон отработал 04.09 и показал, что ставка занижена ×4.47; строка 202 закрыта D39.200. Калибровка ставки идёт заказом строки **281** (⚠ испр.: строка 278 ЗАКРЫТА лендингом `81a89e9` и снята — проекция цены построена и опубликована) (проекция цены и замер стоимости тысячи знаков), а не запретом. Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +1525% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; `platform/internal/pricing/pricing.go`, греп `Provenance, so the number is auditable`). ⚠ **испр. 05.09: движковая половина ПОСТРОЕНА и опубликована**`expected_usd` по главам, `book_once_usd`, `step_max_usd` в `manifest --json`/`status --json` (лендинг `81a89e9`, акт D39.206). Непостроенной осталась ПЛАТФОРМЕННАЯ половина: `git grep -lE "StepMax|step_max" -- platform/` пусто, то есть числа публикуются и никем не читаются.
4. **Холд прироста ДО спавна.** `holdTx` пишет отрицательную строку `('hold', -amount)` и тем же знаком двигает кэш баланса (`platform/internal/pgstore/credits.go`, греп `func holdTx(` — строку леджера пишет `appendLedger(… "hold", -amount …)`, кэш баланса двигает `MoveBalance` внутри неё); нехватка — `ErrInsufficientCredit`. Холд + прогон + попытка + очередь — одна транзакция (`platform/internal/pgstore/runs.go`, греп `func (s *Store) StartRun(`; рестарт — греп `func (s *Store) RestartRun(`; форма D39.123 п.2д).
5.**ИСПР. 06.09: формула ЖИВА дословно, сменился ИСТОЧНИК прироста.** `return m.committed + increment`
стоит на месте (`platform/internal/runs/spawn.go`, греп `func (m meter) bookCap`), но `increment`
приходит теперь из проекции движка, а не из «глав × ставку». Прежняя редакция историей:
**Аргумент движку: `bookCap = committed + прирост×ставка`, БЕЗ reserved — PD-158, дословно из кода: `return m.committed + increment`** (`platform/internal/runs/spawn.go`, греп `return m.committed + increment`). Обе фигуры читаются ОДНИМ вызовом `status --json` перед стартом (`bookMeter``platform/internal/runs/spawn.go`, греп `func (s *Service) bookMeter(`; отказы — греп `Absent is not zero`); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (`docs/glossary.md:58`=`ЭТО НЕ ХОЛД — он больше холда`). Фактически ушедшее значение хранится в `run_attempts.ceiling_arg_micro_usd` (PD-144, миграция 00011); потолок уходит argv-шаблоном `{{usd}}` (шаблон — `platform/internal/config/config.go`, греп `CeilingArg`; подстановку делает `platform/internal/runner/engine.go`, греп `{{usd}}`; подстановку и рендер argv делает `platform/internal/runner/engine.go`, греп `func (t CeilingTemplate) Args(`; `platform/internal/runs/runs.go`, греп `func (s *Service) ceilingFor(` — однострочная делегация к нему; `spawn.go` потолок только передаёт); строка 145 закрыта D39.122.
6. **Baseline и списание.** `baseline = committed` книги на старте попытки (`platform/internal/runs/spawn.go`, греп `baseline, bookCap := m.committed`); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84; `platform/internal/runs/reconcile.go`, греп `the book's meter reads below`).
7.**Терминальный вердикт ОПЕРАТОРА — путь холда мимо settle (добавлено 22.08, P8-FIX/D39.154).** Прогон, чей движок не ответит уже никогда, закрывает человек: `tmplatformctl run abandon --run <id> --reason <текст> [--release-hold]`. Команда ОТКАЗЫВАЕТ, если попытка ещё называет юнит или несёт базовую линию траты — над живым движком прогон не закрывают. Холд возвращается ЦЕЛИКОМ в обоих случаях (к abandon допускается только попытка, не дошедшая до движка, значит она ничего не потратила), а флаг решает лишь КОГДА — сразу или на ближайшем свипе. ⚠ **С пака P11 (`PD-385`) ВЕТВЕЙ ДВЕ, и абзац выше — только ЖИВАЯ** (испр. ревизией 02.09; образец — секция «Строка PHASE = settling» в `platform/deploy/README.md`). У прогона, чья попытка уже КОНЧИЛАСЬ, команда идёт второй ветвью: отказ «юнит/базовая линия» к ней НЕ применяется, к abandon допускается попытка, которая ДОШЛА до движка и ПОТРАТИЛА, холд всё равно возвращается ЦЕЛИКОМ по каждой осиротевшей попытке, а флаг `--release-hold` там ИГНОРИРУЕТСЯ; единственная защита — порог `abandonAfter` неудач расчёта (отказ `ErrSettlementNotStuck`). ⚠ **Ветвление идёт по `runs.finished_at`, а НЕ по наличию осиротевшей попытки** — ратифицированная оговорка `PD-418`. ⚠ **И вторая ветвь НЕ лечит живую:** прогон с намертво заблокированной расплатой уходит в ЖИВУЮ ветвь и закрыт отказом по юниту до сих пор — `PD-424` стоит `open` именно на этой половине.
8. **Settle по фигуре движка.** `reconcile.settle` (`platform/internal/runs/reconcile.go`, греп `func (s *Service) settle(` — ⚠ номера строк в файлы живой зоны не ставим, пока её сессия работает) читает committed из status; `credits.Settle` одной транзакцией закрывает резервацию, возвращает холд и списывает spent; **spent > холда — капится холдом** с явной пометкой «capped at the hold; the engine reported …» (`platform/internal/pgstore/credits.go`, греп `capped at the hold; the engine reported`), перерасход движка счётом не абсорбируется.
9. **SpendBound (PD-159).** Отложенный settle читает пожизненный счётчик КНИГИ и без границы оплатил бы работу прогона-преемника (замер: $2.10 за прогон ценой $0.10, и преемник платил снова). Граница = наименьший baseline попыток той же книги, стартовавших ПОЗЖЕ: `committed = min(счётчик сейчас, SpendBound)` (`platform/internal/runs/reconcile.go`, греп `bound != nil`; `platform/internal/pgstore/runs.go`, греп `func (s *Store) SpendBound(`).
10. **Открытое — эскроу/intent (строка 136 бэклога, `docs/BACKLOG.md`, греп `Write-ahead intent`):** write-ahead intent + состояние `uncertain`-эскроу + `closing`; холд НЕ освобождается по выходу юнита (сигнал обгоняет денежные события); spend не постится без свидетельства провайдера; расход сверх холда — полной суммой, превышение явной строкой убытка. Стройка по research/25, «скоро». ⚠ **«Строка 103» — ФАНТОМ, снят 31.08:** такой строки в бэклоге нет (закрыта D39.131), а эмиттер шва ПОСТРОЕН с обеих сторон — платформа тейлит `events.jsonl` и принимает кадр `spend` (`platform/internal/ingest/events.go`, греп `TypeSpend Type`). Денежной слепоты это НЕ снимает по построению: поток событий — только свежесть. Платформа между спавном и settle слепа — защита держится холдом и потолком движка (§1).
## 4. Фронт (зона `frontend/`)
Максимум шкалы — `Balance` КАК ЕСТЬ (D39.115 п.2а); `Reserved` — справочная величина, `ReadAccount` отдаёт обе раздельно (`platform/internal/pgstore/credits.go`, греп `func (s *Store) ReadAccount(`). ⚠⚠ **ЭРРАТА 06.09 — ШКАЛЫ В ГЛАВАХ БОЛЬШЕ НЕТ.** Лендинг формы заказа похоронил `DefaultPerChapter`, `Scale` и потолок-в-главах целиком: объём заказывается тремя формами (вся книга · до главы N · знаками), холд считается от ПРОЕКЦИИ ЦЕНЫ ДВИЖКА (`k × ожидаемое + шаг [+ книжный бонд]`), а заказ хранится ИДЕНТИФИКАТОРОМ главы, не порядковым номером. Абзац ниже оставлен как история эры шкалы и НЕ описывает сегодняшний провод. Тело — `D39.208`. Прежняя редакция: Шкала — в ГЛАВАХ: `CeilingBounds` — «in CHAPTERS. The conversion to money lives on the platform and is not exposed here in any form» (`docs/architecture/14-api-contract/openapi.yaml`, греп `The conversion to money lives on the platform`); клиенту запрещено клампить повторно; `max_chapters: 0` = состояние «исчерпано» вместо шкалы. ⚠ **Испр. 05.09: запрет отозван (D39.196), но СЕГОДНЯШНИЙ `Usage` действительно без сумм — поле приезжает паком формы заказа, схема впереди кода не едет.** Ниже — что сервится сейчас: `Usage` несёт ТРИ обязательных члена — `state` (ok/low/exhausted), `remaining_percent` и `halt_reason` — и ни окон, ни сумм (`docs/architecture/14-api-contract/openapi.yaml`, греп `required: [state, remaining_percent, halt_reason]`); продуктовый носитель — ПТ-35 (`docs/product-requirements.md:63`=`ПТ-35 | Лимиты «как в Claude Code»`): стоп по потолку = `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; остаток — страница лимитов (S4+), слово владельца В-3. ⚠ **К-13 ЗАКРЫТ** (D39.132 п.2а: `null` на проводе; пере-суждён ревью 28 §4 №9 и НЕ опрокинут — с уходом дневной оси различие «свой потолок / нет кредита» перестало быть продуктовым).
## 5. Открытые дыры и их носители
| Дыра | Носитель |
|---|---|
| ⚠ **Строка 166 — ДВИЖКОВАЯ половина ПОСТРОЕНА 05.09** (`expected_usd` · `book_once_usd` · `step_max_usd` в `manifest --json`, лендинг `81a89e9`, акт D39.206); ⛔ **ПЛАТФОРМЕННАЯ половина ТОЖЕ ПОСТРОЕНА — пере-снято 07.09 ЭТОЙ ЖЕ командой, которой утверждение доказывалось:** `git grep -lE "StepMax|step_max" -- platform/` даёт **19 файлов** (контроль на несуществующем токене — 0), среди них боевые `internal/ingest/manifest.go`, `internal/pgstore/books.go`, миграция `00033_order_and_price.sql`; движковые числа не только читаются, но и СТРОЯТ холд — `platform/internal/pricing/pricing.go:198`=`func (m Model) Hold(expected, stepMax, bookOnce, balance`. ⚠ Прежние две редакции звали дырой сначала обе половины, потом платформенную — обе устарели, и вторая пережила лендинг `81a89e9`. Строка бэклога 166 — кандидат на закрытие при лендинге платформенного пака. Движковая поверхность оценки $/глава; $0.03 — временная мера беты (П-10 зоны) | `docs/PROGRESS.md`, греп `Движковая поверхность оценки` |
| ✅ **Строка 78 ЗАКРЫТА 10.09** (акт `D39.232`) — биллящийся decode-fail пишет свою строку и помечен оценкой; остаток другого знака — строка **377** | `docs/architecture/05-decisions-log.md`, греп `## D39.232` |
| **Строка 136** — эскроу/intent (см. §3 п.10) | `docs/PROGRESS.md`, греп `Write-ahead intent` |
| *(добавка сборщика)* **PD-157 (minor)**`day_usd` книги флагом не перекрывается, платформой не виден и не задаётся; в `status --json` дневной фигуры нет | `platform/docs/DEFECT_REGISTER.md`, греп `PD-157` |
## 6. Карта носителей темы
| Вопрос | Где живёт |
|---|---|
| Словарь терминов | `docs/glossary.md:58`=`**Деньги платформы:**` |
| Управляемый потолок: решение и форма | D39.110 (+поправка D39.112 п.3: п.1 — пересказ, не цитата) |
| Максимум шкалы = Balance как есть | D39.115 п.2а; эррата — шапка D-лога, `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ` |
| Семантика `--ceiling-usd` | D39.122 п.2в; `backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`, `backend/cmd/tmctl/main.go:261`=`--ceiling-usd is NOT a per-run budget` |
| Формула аргумента потолка (без reserved) · ставка · settle-формы | D39.123 п.2б/г/д; PD-158/PD-159/PD-144 в реестре |
| Код денег платформы | `platform/internal/pgstore/credits.go` · `pgstore/runs.go` · `runs/spawn.go` · `runs/reconcile.go` · `pricing/pricing.go` |
| Ратификации, на которых стоит этот док | ⚠ **добавлены 05.09: `D39.203`** (владелец: движку МОЖНО говорить «сколько добавить»; наружу идёт ДО-ВЫЗОВНАЯ ОЦЕНКА, не стоимость) · **`D39.204`** (отказ резервации на ретрае деградирует во ФЛАГ; решение принято, код НЕ тронут — строка 291) · **`D39.206`** (акт приёмки денежного стопа). ⚠ **И новые движковые денежные носители того же лендинга:** `backend/internal/pipeline/reservegate.go` (шлюз резерваций и `shortfallMicroUSD`) · `priceprojection.go` (проекция цены книги) · `internal/runevents/runevents.go` (`Ceiling.ShortfallMicroUSD`, `Finished.Money`, поток 1.3). · **D39.179** (пере-пин цен 16.08, ×4.47; норма «стабилен чистый ФАЙЛ» — п.4) · **D39.181** (закон раскрытия: признак объёмного стопа едет числом) · **D39.182** (пак «деньги и честность»: план банк-ролей режется до первого вызова; порог согласия без контура банк-ролей) |
| Код денег движка | `backend/internal/store/ledger.go` · `store/store.go` · `pipeline/stagerun.go` · `pipeline/status.go` · `config/book.go` · `pipeline/rebill.go` (согласие на пере-оплату) · `pipeline/terminologist.go` (книго-широкие бюджеты банк-ролей) · `pipeline/volume.go` (объёмный потолок) |
| Дефекты денег | `platform/docs/DEFECT_REGISTER.md` (PD-104·113·144·157·158·159) |
| Бэклог-строки | `docs/BACKLOG.md` строки 136 · 137 · **377** · **382** (⚠ 78 ЗАКРЫТА 10.09 актом `D39.232`) (⚠ испр. 05.09: **166 отсюда СНЯТА** — движковая половина оценки построена и опубликована лендингом `81a89e9`; открыта только платформенная) (135 и 165 закрыты D39.131) |
| Контракт наружу | `14-api-contract/openapi.yaml` (⚠ **`CeilingBounds` РЕТИРОВАН лендингом формы заказа 06.09** — его место занял `RunOptions` с `chapters_left`/`affordable_chapters`/`verdict` и денежными границами; `Usage`); ПТ-35 |
| ⚠ Запрет денег на экране — **ОТОЗВАН 05.09 (D39.196)**. Живым остаётся ПТ-35 в его четырёх пунктах: цены моделей · стоимость стадий · стоимость ВЫЗОВОВ · структура наших расходов. ⚠ И различение `D39.203`, без которого пункт про вызовы читается неверно: наружу идёт ДО-ВЫЗОВНАЯ ОЦЕНКА (резервация перед звонком), а запрет написан про СТОИМОСТЬ — что списано по факту; сеттлмент не выходит | D39.84; D39.100/К-8; ПТ-35; **D39.196**, эррата 05.09-а |
---
## РАЗБОР 04.09: КНИГУ НЕЛЬЗЯ ДОЧИТАТЬ — три независимых чтения; форма заказа РЕШЕНА владельцем 05.09 (`D39.196`)
> **⚠ СТАТУС ИЗМЕНИЛСЯ 05.09: ФОРМА ЗАКАЗА РАТИФИЦИРОВАНА — `D39.196`.** Решение владельца: баланс аккаунта — ДЕНЬГАМИ и виден;
> ползунок потолка — деньгами; рядом ответ «хватает на полную книгу или нет»; второй ползунок объёмом — ГЛАВЫ когда книга
> распозналась, ЗНАКИ когда нет (о нераспознавании говорим ЯВНО); старт = холд с баланса; отдельная кнопка «книга целиком».
> Тем же решением ОТОЗВАН запрет «денежных полей в UI нет вовсе». Строки исполнения — 276283.
> **Ниже — РАЗБОР, из которого решение выросло.** Он не контракт: при конфликте с `D39.196` побеждает нота. Носитель дефекта — `PD-440`/`PD-441` в регистре платформы. Собрано по
> прямому указанию владельца «посоветуйся с зонами и со старшей моделью»: читали независимо зона движка,
> зона платформы и внешний рецензент другой модели; расхождения между ними названы, а не сглажены.
**ЧТО НАБЛЮДАЛОСЬ.** Первый живой ПЛАТНЫЙ прогон книги через API (04.09) остановился, не продвинув книгу
ни на юнит, и воспроизвёлся дважды. Прогон умирает за 1015 секунд, списание не двигается.
**ПОЧЕМУ — цепь, проверенная тремя чтениями независимо.** Запас нового прогона на НОВЫЕ траты равен
**испр. 06.09** — не `chaptersLeft × ставка` (ставки нет), а **холд от проекции движка**:
`k × ожидаемое(заказ) + шаг`, где шаг — крупнейшая НЕДЕЛИМАЯ резервация
(`platform/internal/pricing/pricing.go`, греп `func (m Model) Hold(`). Резерв ОДНОГО редакторского вызова — worst case по `maxTokens`
(`backend/internal/pipeline/stagerun.go`, греп `func (r *Runner) callEstimateUSD`), замеренный отказ в
ряду `PD-440``denied estimate=$0.069828`. Отказ резервации трактуется как **авария всей книги**:
`runWave` зовёт `cancel()` производного контекста на ПЕРВОЙ ошибке любого рода, и уже допущенные
параллельные вызовы гасятся.
**ТРИ УТВЕРЖДЕНИЯ, СОШЕДШИЕСЯ У ВСЕХ ТРЁХ ЧТЕНИЙ:**
1. **Форма потолка неверна, а не только его размер.** Запас сжимается вместе с остатком книги, стоимость
неделимого шага — нет. Ставку можно поднять, стену это отодвинет, но не уберёт. Правильная форма —
`max(главы × ставка, шаг)`, а не `главы × ставка`.
2. **Резервацию убирать нельзя.** Она — единственный сериализованный контроль и единственное, что держит
обещание «не потратить больше, чем дано». Отраслевой стандарт тот же (см. прайор-арт ниже).
3. **Менять надо ТРАКТОВКУ ОТКАЗА**, а не наличие резервации: «не начинаем новое» вместо «гасим всё».
⛔⛔ **ЭРРАТА 05.09 — ВСЁ НИЖЕ ОПИСЫВАЕТ ДВИЖОК ДО ПАКА «ДЕНЕЖНЫЙ СТОП» (приёмочный `81a89e9` + дофикс `616a8e4`, акт приёмки **D39.206**; ⚠ испр.: раньше здесь стояла D39.203 — это нота ВЛАДЕЛЬЦА про «сколько добавить», а не акт, и назван был только дофикс-коммит). Порог «книга короче десяти глав не переводится» СНЯТ ЛЕНДИНГОМ.** Стена стояла на том, что отказ ОДНОГО воркера ронял ВСЮ волну ⇒ резервы всех четырёх обязаны были уместиться в потолок разом. Пак это и чинил (его промт `docs/archive/prompts/BACKEND_MONEYSTOP_SESSION_PROMPT_2026-09-05.md` называл порог целью): отказ ЗАЩЁЛКИВАЕТ волну — новое не стартует, допущенное дорабатывается, — а обязательный вызов ЖДЁТ settle, когда settle может помочь. Пины: `backend/internal/pipeline/moneystop_test.go`, греп `TestUnderACeilingTheWaveFinishesWhatItAdmitted` и `TestWaitingForHeadroomDeliversTheWholeBook` («a ceiling that admits one call AT A TIME must still deliver the book»).
**Честное состояние на 05.09:** движок больше НЕ требует, чтобы вся волна умещалась в потолок; короткая книга идёт по одному вызову. Остаточный пол — ОДНА резервация (`step_max_usd` ≈ $0.07 ⇒ ~3 главы по ставке $0.03; до ИСПОЛНЕНИЯ `D39.204` удвоение на ретрае поднимает это до ~5 — ⚠ решение владельца принято 05.09, но КОД ещё не тронут, работа заведена строкой 291). ⚠ **Новый порог ВЫВЕДЕН, НЕ ЗАМЕРЕН** — отчёт пака называет это честно (`backend/docs/MONEYSTOP_REPORT.md` §9: цена защёлки не замерена, числа проекции с фактом не сверены). Замер стоит одного $0.15-прогона пятиглавой книги и передан полигону. ⚠ **Испр. 10.09: платформенная половина (потолок ДЕНЬГАМИ, D39.196) ПОСТРОЕНА, и прежний отрицательный замер здесь протух.** Та же команда сегодня: `git grep -lE "StepMax|step_max" -- platform/`**20 файлов** (контроль: несуществующий токен — 0; файлов под `platform/` в индексе — 339), среди них боевые `platform/internal/pricing/pricing.go` (греп `func (m Model) Hold`), `platform/internal/ingest/manifest.go`, `platform/internal/pgstore/books.go`. Холд считается ИМЕННО на этих числах. ⚠ Строка **63** этого же файла предупредила об этом ещё 07.09 («вторая пережила лендинг `81a89e9`»), а фраза стояла — то есть предупреждение в доке само по себе не чинит дока.
**Класс ошибки, ради которого эррата написана так подробно: оркестратор заленджил пак, чей промт заказывал снятие этой стены, принял его — и в тот же час предупредил владельца о стене как о действующей, читая ЭТОТ абзац.** Канон, не обновлённый лендингом, врёт громче, чем отсутствующий.
**ЧИСЛА, В КОТОРЫХ ЧТЕНИЯ РАСХОДИЛИСЬ (ДО-ПАКОВАЯ АРХЕОЛОГИЯ) — и оба верны на своём числе воркеров.** Зона платформы: стена
стоит везде, где `chaptersLeft × ставка < workers × шаг`, при боевых четырёх воркерах это
`chaptersLeft ≤ 9` — недостижим не хвост, а **последние девять глав любой книги**. Внешний рецензент:
даже при ОДНОМ воркере `2 × $0.03 = $0.06 < $0.07` одного вызова, то есть **последние две главы
недостижимы в принципе**, независимо от параллельности. Второе — инвариантный пол, первое — сегодняшняя
конфигурация.
**ПРЕДСКАЗАНИЕ ПОДТВЕРЖДЕНО ЗАМЕРОМ 04.09 — это больше не предсказание, а ПОРОГ.** Замер за $0 (книга
`bk_P5UCXKHDO4HFWSH3` заведена настоящим интейком, баланс не двинулся): `run-options` отдаёт
`max_chapters: 5`, то есть максимум покупки пятиглавой книге — **$0.15 при стоимости волны $0.277633**.
Наблюдённая волна снята из отказа самого движка (`book USD ceiling reached ($0.548319)
(committed=$0.278319 reserved=$0.207805, denied estimate=$0.069828, ch5/chunk0/edit)`): три реальных
резерва $0.207805 + отказанный $0.069828. ⚠ Произведение `4 × $0.069828 = $0.279312` — ВЕРХНЯЯ оценка, а
не наблюдение: вызовы волны не равны (отношение 2.976, промпты чанков разной длины), и счёт «три»
ВЫВЕДЕН из `waves.workers: 4` минус отказанный, движок напечатал сумму, а не количество.
**Порог одинаков по обоим числам: 9.25 и 9.31 ⇒ книга проходит только с ДЕСЯТИ непереведённых глав.**
Последнее возражение («у короткой книги куски мельче») снято замером: разрезка совпала побайтно
(`2·1·1·2·1` против первых пяти глав длинной), то есть короткая книга содержит ровно тот чанк, чей
резерв движок и назвал. ⇒ **Сегодня сервис не может перевести книгу короче десяти глав — не «дорого», а
невозможно при любых деньгах на счету.** ⚠ Сама пятиглавая книга НЕ запускалась: заказ был на $0 и
исполнен за $0; живой прогон добавил бы лишь наблюдение, на какой волне она встанет.
**ЧЕТВЁРТОЕ, ЧЕГО НЕ БЫЛО В ИСХОДНОМ ДИАГНОЗЕ (зона движка).** Резерв завышен ПО ПОСТРОЕНИЮ: оценка
считается по потолку выдачи. Под нынешним поведением он никогда не превращается в факт — его отпускают,
и следующее решение о допуске принимается против тех же завышенных чисел. Под «не начинаем новое»
соседи доседают по ФАКТИЧЕСКОЙ цене и освобождают разницу. Это само по себе может быть разницей между
«встала» и «дошла».
**ПЯТОЕ — ПРЕЦЕДЕНТ ВНУТРИ ЭТОГО ЖЕ ПАКЕТА, и он решает спор о направлении.** Объёмный потолок уже
построен с нужной семантикой и САМ противопоставляет себя денежному:
`backend/internal/pipeline/volume.go`, греп `THE STOP IS A COMPLETION, NOT A HALT` — «денежный потолок
останавливает прогон ПОСРЕДИ работы, которую он собирался сделать». Там же названа причина, по которой
допуск не сделан счётчиком у воркеров: «воркер, которому отказали в слоте, был бы вынужден бросить свой
элемент навсегда» — то есть ровно тот дефект, который денежный путь и имеет. Плюс три места уже
деградируют по `errReserveCeiling` вместо аборта (`escalation.go`, `repair.go`, `terminologist.go`);
не деградирует только главная волна.
**ШЕСТОЕ — ДЕФЕКТ, ПЕРЕЖИВАЮЩИЙ ЛЮБУЮ ПОЧИНКУ РАЗМЕРА И ФОРМЫ (зона платформы).** Стена МОЛЧАЛИВА:
прогон, вставший мгновенно, неотличим на всех поверхностях от работавшего — одинаковый статус, одинаковая
причина паузы, одинаковая полоса (наблюдение H16 зонного журнала, два прогона: $0.081 и $0). Недобрать
можно при любом потолке; сказать об этом сегодня нечем.
**ВЕС — ЖИВУЧЕСТЬ, НЕ ДЕНЬГИ.** Мёртвые прогоны рассчитались в ноль, леджер сходится тремя источниками
до микродоллара. Пользователь теряет ВРЕМЯ. ⚠ Отдельно и с другим весом — `PD-441`: на пути отмены есть
расход, который, ЕСЛИ он был, невидим для леджера по построению. Доказуемая форма именно такая; «деньги
потрачены» НЕ измерено. Одна движковая попытка — до трёх запросов к провайдеру (`backend/internal/llm/httpllm.go`,
греп `for att := 0; att < profile.MaxAttempts`, дефолт 3) под ОДНИМ резервом и ОДНОЙ строкой `request_log`:
след не просто беден, он агрегирован и не различает «не ушло ничего» от «ушло трижды».
**ПРАЙОР-АРТ (внешний рецензент, ссылки — в отчёте сессии).** LiteLLM делает буквально наше:
worst-case-резерв → отказ до отправки → замена фактической ценой; их известные баги — про ОБХОД бюджета
при параллельных запросах, то есть отраслевая беда противоположна нашей. Ближайший по смыслу стандарт —
Diameter Credit-Control (RFC 4006/8506) с `Final-Unit-Indication`: «это последний грант, доработай его и
не начинай нового» — ровно та семантика, которой нам не хватает. Kubernetes `ResourceQuota`: отказ — это
недопуск НОВОГО, уже созданное квота не трогает. Slurm `GraceTime`: слив вместо убийства. Карточная
пре-авторизация: холд с запасом, списание по факту. ⚠ Про биллинг при обрыве: OpenRouter документирует,
что для непотоковых запросов отмена клиентом биллинг НЕ останавливает — это факт о ЕГО платформе, для
наших провайдеров вопрос обязан быть закрыт вендор-докой по правилу `experiments/00-provider-quirks.md`,
а не рассуждением.
**⚠ РАЗВИЛКА, КОТОРУЮ ОРКЕСТРАТОР НАЗВАЛ РАЗВИЛКОЙ, ЕЮ НЕ ЯВЛЯЕТСЯ — снято словом владельца 04.09.**
Формулировалось так: «потолок = баланс ⇒ холд берёт весь баланс ⇒ вторая книга не стартует, пока не
рассчитается первая», и выносилось владельцу на решение. Владелец: это не решение, а то, как деньги
работают — выделил книге сумму, она занята, на занятые вторую не стартуешь; управляется тем, сколько
выделил. **Механика, поданная как вопрос к владельцу, — дефект оркестратора, а не открытый пункт.**
**ЧТО ПОД НЕЙ ЛЕЖАЛО НА САМОМ ДЕЛЕ И ОКАЗАЛОСЬ ХУЖЕ: выделить деньги СЕГОДНЯ НЕЛЬЗЯ.** Шкала прогона —
в ГЛАВАХ (⚠ **ретировано 06.09, см. эрратy выше** — историческая запись эры шкалы), и канон говорил это прямо: `14-api-contract/openapi.yaml`, схема `CeilingBounds`, греп
`Bounds of the run-limit scale, in CHAPTERS` — «перевод в деньги живёт на платформе и наружу не
отдаётся ни в каком виде». Человек двигает главы; сумму он не видит и выбрать не может. Ограничение при
этом ДЕНЕЖНОЕ, перевод между единицами делает сломанная ставка — отсюда и наблюдаемая нелепость:
пятиглавой книге шкала честно предлагает `max_chapters: 5`, а купить это не может ничего.
**Единица выбора и единица ограничения не совпадают, и это корень, а не следствие.** Направление,
объявленное владельцу 04.09: человек выделяет книге ДЕНЬГИ (по умолчанию — на всю книгу, ограничено
балансом), рядом показывается оценка «хватит примерно на N глав» именно как оценка, прогон тратит
выделенное и ГОВОРИТ, когда кончилось, а «после этого на другие книги останется ноль» показывается в
момент выделения. Порог десяти глав при этом исчезает по построению: считать «остаток глав × ставку»
становится негде. Цена — контрактный минор в месте, где сегодня записано «деньги наружу не отдаются», и
пере-рисовка ручки фронтом (зона заморожена, первое касание — синк с каноном, момент удобен).
**ЧТО СЧИТАЕТСЯ ЗАКРЫТЫМ И БОЛЬШЕ НЕ ОБСУЖДАЕТСЯ:** уже проданные прогоны смену ставки переживают без
миграции — продолжение оценивается по тому, за что прогон был ПРОДАН, бюджет читается обратно из холда
первой попытки, пин `TestARestartHoldsWhatTheRunWasSoldForWhenTheRateHasMovedSince`.
**СЕДЬМОЕ — ЕДИНИЦА УЧЁТА НЕ СОВПАДАЕТ С ЕДИНИЦЕЙ БИЛЛИНГА, и это бьёт не в отчётность, а в ЦЕЛОСТНОСТЬ
потолка** (найдено зоной движка чтением под вопрос, проверено оркестратором по строкам). Класс
`BilledDecodeError` заведён ровно для того, чтобы оплаченный-но-нечитаемый вызов стал ВИДЕН: рантайм
консервативно доседает его по оценке. Комментарий рядом сам называет цену ретрая —
`backend/internal/llm/httpllm.go`, греп `each retry is a new billed`, — и по этой причине УСЕЧЁННЫЙ ответ
сделан терминальным. Но битое тело осталось ретраябельным, а `retryLoop` при успешном ретрае возвращает
ответ и предыдущую ошибку ВЫБРАСЫВАЕТ (греп `if err == nil {` в `retryLoop`). Тогда рантайм видит один
успешный ответ, доседает его `Usage`, и оплаченный 2xx перед ним не попадает НИКУДА — ни в списание, ни
отдельной строкой лога. **Механизм, построенный чтобы сделать оплаченный вызов видимым, обходится ровно
на том пути, где всё прошло хорошо.** Следствие для потолка: допуск выдан один раз под одну оценку; если
под ней прошло два оплаченных ответа, `Reserve` об этом не узнает никогда — он сравнивает с потолком
сумму, в которой невидимого запроса нет. Потолок не «пробивается громко», он считает не то.
**Границы:** структурность доказана чтением; ЧАСТОТА пути НЕ измерена и, вероятно, мала — владельцу
эти две вещи давать раздельно. Более слабый родственник — сетевой обрыв/таймаут (там же комментарий про
`mid-generation RST that re-bills the call`): вход тот же, но «оборвалось на середине ⇒ оплачено» уже
вывод о провайдере, а не утверждение кода.
**ВЕНДОРСКИЙ ВОПРОС — ДВОЙНОЙ, и вторая половина конкретнее первой:** (1) платит ли провайдер за запрос,
оборванный КЛИЕНТОМ после начала генерации; (2) сколько раз он берёт деньги, когда мы ретраим ПОСЛЕ
оплаченного 2xx. Второй случай не гипотетический — он уже назван оплаченным в нашем собственном коде.
Оба закрываются вендор-докой по правилу `experiments/00-provider-quirks.md`, а не рассуждением.
**ГРАНИЦА ИНСТРУМЕНТА, ЗАМЕРЕННАЯ ЭТИМ ЖЕ ЗАХОДОМ.** Каталог мутаций (141 запись, 136 красных, ноль
переживших) доказывает ПОВЕДЕНИЕ и молчит о том, что код УТВЕРЖДАЕТ о себе. Круг 3 нашёл четыре
сверх-утверждения — и **ни одно не нашла посадка**; обе сегодняшние денежные находки получены чтением под
вопрос. Рабочая форма вопроса, выведенная зоной движка: не «правда ли то, что здесь написано», а **«что
должно было бы сломаться, если это правда, и ломается ли оно»**.