66 KiB
Путь денег — от гранта до settle (топикальный вход; строка 167 ЗАКРЫТА D39.200, файл живёт сам по себе)
Статус: топикальный ВХОД, НЕ источник истины. Док собирает денежную тему, размазанную по 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»). ⚠ ИСПР. 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: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.08classify_budget_usd0.02 оборвал классификатор дважды, а поднятый до 0.08escalation.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-onlystatusВ МОМЕНТ СПАВНА, — остаток мёртвого процесса (несущий факт формулы 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.yamlceilings.book_usdand 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:495=BookUSD: r.bookCeilingUSD()иbackend/internal/pipeline/stagerun.go:559=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_pct—backend/internal/pipeline/status.go:869=100 * (committed + reserved)). Дневной фигуры в status нет (PD-157 —platform/docs/DEFECT_REGISTER.md, грепPD-157). -
Леджер = НИЖНЯЯ граница (строка 78 бэклога,
docs/BACKLOG.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/)
-
Грант.
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, дерево сессии)). -
⚠⚠ ИСПР. 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(. -
⛔⛔ СТАВКИ ЗА ГЛАВУ БОЛЬШЕ НЕ СУЩЕСТВУЕТ — ни константы, ни ручки (испр. 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_000micro-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.6–2.6×» — ⚠ но НЕ покрывает максимум: источник (D39.165 §1) добавляет «и отдельные главы её уже пробивают» ($0.0375 против $0.03 = ×1.25), и «купить 10 глав» отдаёт движку $0.30, покупающий ~16–26 глав («денежная ручка с подписью „главы"»,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 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги;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/пусто, то есть числа публикуются и никем не читаются. -
Холд прироста ДО спавна.
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д). -
⚠ ИСПР. 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: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. -
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). -
⚠ Терминальный вердикт ОПЕРАТОРА — путь холда мимо 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именно на этой половине. -
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), перерасход движка счётом не абсорбируется. -
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(). -
Открытое — эскроу/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 зоны) |
| Строка 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 |
| Ратификации, на которых стоит этот док | ⚠ добавлены 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 строки 78 · 136 · 137 (⚠ испр. 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 нет вовсе». Строки исполнения — 276–283. Ниже — РАЗБОР, из которого решение выросло. Он не контракт: при конфликте сD39.196побеждает нота. Носитель дефекта —PD-440/PD-441в регистре платформы. Собрано по прямому указанию владельца «посоветуйся с зонами и со старшей моделью»: читали независимо зона движка, зона платформы и внешний рецензент другой модели; расхождения между ними названы, а не сглажены.
ЧТО НАБЛЮДАЛОСЬ. Первый живой ПЛАТНЫЙ прогон книги через API (04.09) остановился, не продвинув книгу ни на юнит, и воспроизвёлся дважды. Прогон умирает за 10–15 секунд, списание не двигается.
ПОЧЕМУ — цепь, проверенная тремя чтениями независимо. Запас нового прогона на НОВЫЕ траты равен
⚠ испр. 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() производного контекста на ПЕРВОЙ ошибке любого рода, и уже допущенные
параллельные вызовы гасятся.
ТРИ УТВЕРЖДЕНИЯ, СОШЕДШИЕСЯ У ВСЕХ ТРЁХ ЧТЕНИЙ:
- Форма потолка неверна, а не только его размер. Запас сжимается вместе с остатком книги, стоимость
неделимого шага — нет. Ставку можно поднять, стену это отодвинет, но не уберёт. Правильная форма —
max(главы × ставка, шаг), а неглавы × ставка. - Резервацию убирать нельзя. Она — единственный сериализованный контроль и единственное, что держит обещание «не потратить больше, чем дано». Отраслевой стандарт тот же (см. прайор-арт ниже).
- Менять надо ТРАКТОВКУ ОТКАЗА, а не наличие резервации: «не начинаем новое» вместо «гасим всё».
⛔⛔ ЭРРАТА 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-прогона пятиглавой книги и передан полигону. ⚠ Платформенная половина (потолок ДЕНЬГАМИ, D39.196) снимает и этот пол по построению — но НЕ ПОСТРОЕНА: git grep -lE "StepMax|step_max" -- platform/ пусто.
⚠ Класс ошибки, ради которого эррата написана так подробно: оркестратор заленджил пак, чей промт заказывал снятие этой стены, принял его — и в тот же час предупредил владельца о стене как о действующей, читая ЭТОТ абзац. Канон, не обновлённый лендингом, врёт громче, чем отсутствующий.
ЧИСЛА, В КОТОРЫХ ЧТЕНИЯ РАСХОДИЛИСЬ (ДО-ПАКОВАЯ АРХЕОЛОГИЯ) — и оба верны на своём числе воркеров. Зона платформы: стена
стоит везде, где 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 нашёл четыре сверх-утверждения — и ни одно не нашла посадка; обе сегодняшние денежные находки получены чтением под вопрос. Рабочая форма вопроса, выведенная зоной движка: не «правда ли то, что здесь написано», а «что должно было бы сломаться, если это правда, и ломается ли оно».