18 KiB
Путь денег — от гранта до settle (топикальный вход, строка 167)
Статус: топикальный ВХОД, НЕ источник истины. Док собирает денежную тему, размазанную по D-нотам, реестру дефектов платформы, глоссарию и бэклогу, в один маршрут чтения. При конфликте с чем угодно побеждает
docs/architecture/05-decisions-log.md(правило чтения — диета D39.125: живой файл + слайсы). Каждое утверждение здесь снабжено указателем на первичный носитель; формулы — дословно оттуда. Словарь терминов (холд · прирост · аргумент потолка · baseline · settle · leftover-reserved · SpendBound) —docs/glossary.md§«Деньги платформы» (glossary.md:50).
1. Канон одним абзацем
Деньги видимы с первого вызова: телеметрия, потолки, явное согласие на пере-оплату (CLAUDE.md, цель 5). Леджер без потолка запрещён — Р7: книга обязана нести хотя бы один из book_usd/day_usd (backend/internal/config/book.go:250-251, текст валидатора: «a ledger with no ceiling is forbidden, Р7»). Суммы на пользовательский экран НЕ выходят — запрет D39.84, подтверждён D39.100/К-8 («страница лимитов = СТАТУС, не суммы», D-лог :196) и держится в контракте (14-api-contract/openapi.yaml:1152: «No window, no resets_at, no sums»). Защита от перерасхода двойная и не зависит от событий: холд платформы делает кредит недоступным следующему прогону, а движок сам останавливается на потолке — «an overspend is impossible even while the platform is blind. The event stream is freshness only» (док-коммент Hold, platform/internal/pgstore/credits.go:148-151).
2. Деньги ДВИЖКА (зона backend/)
- Леджер SQLite. Таблица
spend, две фигуры:committed_usd(потрачено, с сырым ответом провайдера) иreserved_usd(зарезервировано под летящий вызов).Reserveперед каждым платным вызовом сверяетSUM(committed_usd + reserved_usd)книги и дня с потолками (backend/internal/store/ledger.go:44,58,63; Р7 — коммент:12);Settleконвертирует резервацию в committed и персистит ответ (ledger.go:162,199-210). Суммарно по книге —SpentUSD(ledger.go:314-320). - Гейт потолков — ПЕР-ВЫЗОВНЫЙ, и был им до эмиттер-пака (
Reserveна каждый свежий attempt; прежняя формулировка «на границе юнита» была неверна по отношению к потолкам — опровергнута приёмкой D39.131). На границе юнита сидел РЕПЭЙР-суб-бюджет — ужесточён до пер-вызовного с ценой вызова (строка 135 закрыта D39.131); эскалационный кап хоп НЕ прицениваает — перелёт ≤1 хопа, задокументирован и запинен (диспозиция D39.131 п.2д, реопен — живой инцидент). - leftover-reserved зануляется write-open.
store.Open(путь записи, каждыйtranslate) выполняетrecoverReservations(backend/internal/store/store.go:88, функция:218:UPDATE spend SET reserved_usd = 0 WHERE reserved_usd > 0) — файл владеется одним процессом, значит любой reserved на открытии принадлежит несеттлённому прогону.OpenReadOnlyэтого прохода намеренно НЕ делает (store.go:95-104) ⇒ reserved из read-onlystatus— всегда остаток мёртвого процесса (несущий факт формулы PD-158, см. §3). --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:114; ⚠-комментmain.go:110). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри:Ceilings.BookUSDвReserveи именование сработавшего потолка в ошибке —backend/internal/pipeline/stagerun.go:479-480,505-511(стоп =errReserveCeiling).- Потолки — wiring, не семантика:
Ceilingsнамеренно исключены изBriefHash(«Wiring fields (paths, ceilings, db) deliberately excluded»,book.go:262-265, канон:267+) ⇒ смена потолка не двигает ни снапшот, ни ре-билл (D39.110 п.2б). status --jsonотдаёт фигуры платформе:committed_usd,reserved_usd,book_ceiling_usd,ceiling_pct= 100·(committed+reserved)/book_ceiling (backend/internal/pipeline/status.go:155-158). Дневной фигуры в status нет (PD-157).- Леджер = НИЖНЯЯ граница (строка 78 бэклога,
docs/PROGRESS.md:41): «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).
3. Деньги ПЛАТФОРМЫ по шагам (зона platform/)
- Грант.
Grant— строка леджераkind='grant', идемпотентная по(аккаунт, source, source_id):on conflict (user_id, source, source_id) do nothing(platform/internal/pgstore/credits.go:38-49,357-362). Фри-тир — тот же механизм в ТОЙ ЖЕ транзакции, что создание аккаунта (pgstore/identity.go:86-88,153-155; дефолтSignupGrantMicroUSD: 5 * 1_000_000—config/config.go:114, проведёнtmplatformd/main.go:99→login.go:305). ⚠ Продуктовая половина PD-104 открыта: предложение «на бете дефолт в НОЛЬ» (D39.110 п.3) ждёт слова владельца. - Выбор потолка — шкала в ГЛАВАХ (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в
book.yamlне пишется). Максимум = Balance КАК ЕСТЬ (D39.115 п.2а; эррата в шапке D-лога:4):Hold— дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация:pricing.Model.Scale—affordable = balance / PerChapter, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) —platform/internal/pricing/pricing.go:75-98. - Ставка $0.03/глава — конфиг-константа платформы:
DefaultPerChapter = 30_000micro-USD (pricing.go:33), переопределяетсяPerChapterMicroUSD(config.go:90-93,194,229). Провенанс сверен D39.123 п.2(г): exp08 v2 $0.0219/глава → ревизия D30.4 +15–25% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги;pricing.go:19-33). Временная мера беты — честная оценка движковая (строка 166). - Холд прироста ДО спавна.
holdTxпишет отрицательную строку('hold', -amount)и тем же знаком двигает кэш баланса (credits.go:163-206,375-385); нехватка —ErrInsufficientCredit. Холд + прогон + попытка + очередь — одна транзакция (pgstore/runs.go:104StartRun,:338рестарт; форма D39.123 п.2д). - Аргумент движку:
bookCap = committed + прирост×ставка, БЕЗ reserved — PD-158, дословно из кода:return m.committed + increment(platform/internal/runs/spawn.go:195-196). Обе фигуры читаются ОДНИМ вызовомstatus --jsonперед стартом (bookMeter,spawn.go:206-223); отсутствие любой из них = отказ спавна («absent is not zero», PD-40). Почему без reserved: write-open движка занулит leftover до первой резервации, значит цифра из read-only status к моменту сравнения уже мертва; ратифицированная ранее формула с reserved переплачивала запасом ровно на leftover сверх холда — исполнено обеими формулами против гейта (D39.123 п.2б, поправка к D39.122(в)). Аргумент ≠ холд: он больше холда на committed (глоссарий:50). Фактически ушедшее значение хранится вrun_attempts.ceiling_arg_micro_usd(PD-144, миграция 00011); потолок уходит argv-шаблоном{{usd}}(config.goCeilingArg,spawn.go:132-150); строка 145 закрыта D39.122. - Baseline и списание.
baseline = committedкниги на старте попытки (spawn.go:74); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84;reconcile.go:308-316). - Settle по фигуре движка.
reconcile.settle(platform/internal/runs/reconcile.go:277+) читает committed из status;credits.Settleодной транзакцией закрывает резервацию, возвращает холд и списывает spent; spent > холда — капится холдом с явной пометкой «capped at the hold; the engine reported …» (credits.go:214-234), перерасход движка счётом не абсорбируется. - SpendBound (PD-159). Отложенный settle читает пожизненный счётчик КНИГИ и без границы оплатил бы работу прогона-преемника (замер: $2.10 за прогон ценой $0.10, и преемник платил снова). Граница = наименьший baseline попыток той же книги, стартовавших ПОЗЖЕ:
committed = min(счётчик сейчас, SpendBound)(reconcile.go:293-307;pgstore/runs.go:268). - Открытое — эскроу/intent (строка 136,
docs/PROGRESS.md:85): write-ahead intent + состояниеuncertain-эскроу +closing; холд НЕ освобождается по выходу юнита (сигнал обгоняет денежные события); spend не постится без свидетельства провайдера; расход сверх холда — полной суммой, превышение явной строкой убытка. Стройка по research/25, «скоро». До эмиттера (строка 103) платформа между спавном и settle слепа — защита держится холдом и потолком движка (§1).
4. Фронт (зона frontend/)
Максимум шкалы — Balance КАК ЕСТЬ (D39.115 п.2а); Reserved — справочная величина, ReadAccount отдаёт обе раздельно (pgstore/credits.go:99). Шкала — в ГЛАВАХ: CeilingBounds — «in CHAPTERS. The chapters → money conversion lives on the platform and is not exposed here in any form»; клиенту запрещено клампить повторно; max_chapters: 0 = состояние «исчерпано» вместо шкалы (openapi.yaml:1116-1147). Суммы на экран не идут: Usage = state (ok/low/exhausted) + remaining_percent, без окон и сумм (openapi.yaml:1149-1165); продуктовый носитель — ПТ-35 (docs/product-requirements.md:62): стоп по потолку = paused + оповещение «перевод остановлен: лимиты исчерпаны»; остаток — страница лимитов (S4+), слово владельца В-3, К-13 (paused_reason не различает «свой потолок» и «нет кредита»).
5. Открытые дыры и их носители
| Дыра | Носитель |
|---|---|
ceiling{scope} + exit 4 (движок, D39.131) и потребитель (платформа, П-15): потолочный стоп = paused двумя независимыми каналами, причина пишется одним оператором со статусом и перечитывается после drain; открытых major в регистре платформы 0 |
platform/docs/DEFECT_REGISTER.md (эра P6), D39.132 |
schema_mismatch) едет паком migrate, ратификация направления — D39.132 |
D39.131, D39.132 |
| Строка 166 — движковая поверхность оценки $/глава; $0.03 — временная мера беты (П-10 зоны) | docs/PROGRESS.md:114 |
| PD-104 — фри-тир: док↔код закрыто, продуктовая половина (ноль на бете · агрегатный потолок · счётчик) на владельце | DEFECT_REGISTER.md:115, D39.110 п.3 |
| Строка 78 — леджер = нижняя граница (биллящийся decode-fail мимо request_log, ≤13% захода на ре-пробе) | docs/PROGRESS.md:41 |
| Строка 136 — эскроу/intent (см. §3 п.9) | docs/PROGRESS.md:85 |
(добавка сборщика) PD-157 (minor) — day_usd книги флагом не перекрывается, платформой не виден и не задаётся; в status --json дневной фигуры нет |
DEFECT_REGISTER.md:168 |
6. Карта носителей темы
| Вопрос | Где живёт |
|---|---|
| Словарь терминов | docs/glossary.md:50 §«Деньги платформы» |
| Управляемый потолок: решение и форма | D39.110 (+поправка D39.112 п.3: п.1 — пересказ, не цитата) |
| Максимум шкалы = Balance как есть | D39.115 п.2а; эррата — шапка D-лога :4 |
Семантика --ceiling-usd |
D39.122 п.2в; tmctl/invocation.go:114, main.go:110 |
| Формула аргумента потолка (без 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 |
| Код денег движка | backend/internal/store/ledger.go · store/store.go · pipeline/stagerun.go · pipeline/status.go · config/book.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 |