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

55 KiB
Raw Blame History

Путь денег — от гранта до settle (топикальный вход, строка 167)

Статус: топикальный ВХОД, НЕ источник истины. Док собирает денежную тему, размазанную по D-нотам, реестру дефектов платформы, глоссарию и бэклогу, в один маршрут чтения. При конфликте с чем угодно побеждает docs/architecture/05-decisions-log.md (правило чтения — диета D39.125: живой файл + слайсы). Каждое утверждение здесь снабжено указателем на первичный носитель; формулы — дословно оттуда. Словарь терминов (холд · прирост · аргумент потолка · baseline · settle · leftover-reserved · SpendBound) — docs/glossary.md §«Деньги платформы» (docs/glossary.md:55=**Деньги платформы:**).

Форма указателей — норма этого дока (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»). Суммы на пользовательский экран НЕ выходят — запрет 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» (док-коммент Holdplatform/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:392=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:260=--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:496=BookUSD: r.bookCeilingUSD() и backend/internal/pipeline/stagerun.go:525=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:311=book_ceiling_usd,omitempty; арифметика ceiling_pctbackend/internal/pipeline/status.go:835=100 * (committed + reserved)). Дневной фигуры в status нет (PD-157 — platform/docs/DEFECT_REGISTER.md, греп PD-157).

  • Леджер = НИЖНЯЯ граница (строка 78 бэклога, docs/PROGRESS.md, греп Леджер денег = НИЖНЯЯ граница): «2xx body decode failed (call IS billed)» — провайдер списал, попытка в request_log не попадает; живой замер D39.86 — 3 вызова из 14, неизвестность $0.015111 при леджере $0.114378; движок сеттлит оценку и печатает estimated-cost rows: 3. Канал виден, фикс — money-паком (дизайн GENERALITY_PHASE2 §5.6).

  • Согласие на пере-оплату — ДВИЖКОВЫЙ гейт. Прогон, который пере-покупает уже оплаченные юниты дороже порога, ОСТАНАВЛИВАЕТСЯ и требует явного согласия. Порог по умолчанию ратифицирован 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. Выбор потолка — шкала в ГЛАВАХ (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в book.yaml не пишется). Максимум = Balance КАК ЕСТЬ (D39.115 п.2а; эррата в шапке D-лога — docs/architecture/05-decisions-log.md:4=Balance КАК ЕСТЬ): Hold — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: pricing.Model.Scaleaffordable = balance / PerChapter, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — platform/internal/pricing/pricing.go, греп func (m Model) Scale(.

  3. Ставка $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 г) — гейт строка 202 бэклога (docs/PROGRESS.md, греп Живой перевод книги НАСКВОЗЬ через API платформы). Исторический провенанс: exp08 v2 $0.0219/глава → ревизия D30.4 +1525% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; platform/internal/pricing/pricing.go, греп Provenance, so the number is auditable). Временная мера беты — честная оценка движковая (строка 166 бэклога, docs/PROGRESS.md, греп Движковая поверхность оценки).

  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. Аргумент движку: bookCap = committed + прирост×ставка, БЕЗ reserved — PD-158, дословно из кода: return m.committed + increment (platform/internal/runs/spawn.go, греп return m.committed + increment). Обе фигуры читаются ОДНИМ вызовом status --json перед стартом (bookMeterplatform/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:55=ЭТО НЕ ХОЛД — он больше холда). Фактически ушедшее значение хранится в 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/PROGRESS.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(). Шкала — в ГЛАВАХ: 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 = состояние «исчерпано» вместо шкалы. Суммы на экран не идут: 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 — движковая поверхность оценки $/глава; $0.03 — временная мера беты (П-10 зоны) docs/PROGRESS.md, греп Движковая поверхность оценки
Строка 78 — леджер = нижняя граница (биллящийся decode-fail мимо request_log, ≤13% захода на ре-пробе) docs/PROGRESS.md, греп Леджер денег = НИЖНЯЯ граница
Строка 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:55=**Деньги платформы:**
Управляемый потолок: решение и форма 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:260=--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
Ратификации, на которых стоит этот док 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/PROGRESS.md строки 78 · 136 · 137 · 166 (135 и 165 закрыты D39.131)
Контракт наружу 14-api-contract/openapi.yaml (CeilingBounds, Usage); ПТ-35
Запрет денег на экране D39.84; D39.100/К-8; ПТ-35

⚠ ОТКРЫТАЯ РАЗВИЛКА 04.09: КНИГУ НЕЛЬЗЯ ДОЧИТАТЬ — разбор трёх независимых чтений, решение за владельцем

СТАТУС ИЗМЕНИЛСЯ 05.09: ФОРМА ЗАКАЗА РАТИФИЦИРОВАНА — D39.196. Решение владельца: баланс аккаунта — ДЕНЬГАМИ и виден; ползунок потолка — деньгами; рядом ответ «хватает на полную книгу или нет»; второй ползунок объёмом — ГЛАВЫ когда книга распозналась, ЗНАКИ когда нет (о нераспознавании говорим ЯВНО); старт = холд с баланса; отдельная кнопка «книга целиком». Тем же решением ОТОЗВАН запрет «денежных полей в UI нет вовсе». Строки исполнения — 276283. Ниже — РАЗБОР, из которого решение выросло. Он не контракт: при конфликте с D39.196 побеждает нота. Носитель дефекта — PD-440/PD-441 в регистре платформы. Собрано по прямому указанию владельца «посоветуйся с зонами и со старшей моделью»: читали независимо зона движка, зона платформы и внешний рецензент другой модели; расхождения между ними названы, а не сглажены.

ЧТО НАБЛЮДАЛОСЬ. Первый живой ПЛАТНЫЙ прогон книги через API (04.09) остановился, не продвинув книгу ни на юнит, и воспроизвёлся дважды. Прогон умирает за 1015 секунд, списание не двигается.

ПОЧЕМУ — цепь, проверенная тремя чтениями независимо. Запас нового прогона на НОВЫЕ траты равен chaptersLeft × ставка (platform/internal/runs/spawn.go, греп func (m meter) bookCap; ставка DefaultPerChapter = 30_000 микро-USD). Резерв ОДНОГО редакторского вызова — worst case по maxTokens (backend/internal/pipeline/stagerun.go, греп func (r *Runner) callEstimateUSD), замеренный отказ в ряду PD-440denied estimate=$0.069828. Отказ резервации трактуется как авария всей книги: runWave зовёт cancel() производного контекста на ПЕРВОЙ ошибке любого рода, и уже допущенные параллельные вызовы гасятся.

ТРИ УТВЕРЖДЕНИЯ, СОШЕДШИЕСЯ У ВСЕХ ТРЁХ ЧТЕНИЙ:

  1. Форма потолка неверна, а не только его размер. Запас сжимается вместе с остатком книги, стоимость неделимого шага — нет. Ставку можно поднять, стену это отодвинет, но не уберёт. Правильная форма — max(главы × ставка, шаг), а не главы × ставка.
  2. Резервацию убирать нельзя. Она — единственный сериализованный контроль и единственное, что держит обещание «не потратить больше, чем дано». Отраслевой стандарт тот же (см. прайор-арт ниже).
  3. Менять надо ТРАКТОВКУ ОТКАЗА, а не наличие резервации: «не начинаем новое» вместо «гасим всё».

ЧИСЛА, В КОТОРЫХ ЧТЕНИЯ РАСХОДЯТСЯ — и оба верны на своём числе воркеров. Зона платформы: стена стоит везде, где 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. Формулировалось так: «потолок = баланс ⇒ холд берёт весь баланс ⇒ вторая книга не стартует, пока не рассчитается первая», и выносилось владельцу на решение. Владелец: это не решение, а то, как деньги работают — выделил книге сумму, она занята, на занятые вторую не стартуешь; управляется тем, сколько выделил. Механика, поданная как вопрос к владельцу, — дефект оркестратора, а не открытый пункт.

ЧТО ПОД НЕЙ ЛЕЖАЛО НА САМОМ ДЕЛЕ И ОКАЗАЛОСЬ ХУЖЕ: выделить деньги СЕГОДНЯ НЕЛЬЗЯ. Шкала прогона — в ГЛАВАХ, и канон говорит это прямо: 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 нашёл четыре сверх-утверждения — и ни одно не нашла посадка; обе сегодняшние денежные находки получены чтением под вопрос. Рабочая форма вопроса, выведенная зоной движка: не «правда ли то, что здесь написано», а «что должно было бы сломаться, если это правда, и ломается ли оно».