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

17 KiB
Raw Blame History

Путь денег — от гранта до 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).
  • Гейт потолка сегодня — на границе юнита работы, не на каждый платный вызов; ужесточение — строка 135 бэклога (промтом эмиттера, research/25 «Деньги»).
  • 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-only status — всегда остаток мёртвого процесса (несущий факт формулы PD-158, см. §3).
  • --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: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/)

  1. Грант. 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_000config/config.go:114, проведён tmplatformd/main.go:99login.go:305). ⚠ Продуктовая половина PD-104 открыта: предложение «на бете дефолт в НОЛЬ» (D39.110 п.3) ждёт слова владельца.
  2. Выбор потолка — шкала в ГЛАВАХ (D39.110: от 1 до максимума, ноль не выбираем; потолок принадлежит ПРОГОНУ, в book.yaml не пишется). Максимум = Balance КАК ЕСТЬ (D39.115 п.2а; эррата в шапке D-лога :4): Hold — дебет в момент взятия, баланс уже не содержит открытых холдов, повторное вычитание вдвое укорачивает шкалу. Реализация: pricing.Model.Scaleaffordable = balance / PerChapter, кламп по остатку глав, дефолт = верх шкалы (ратифицировано D39.123 п.2е; компромисс «весь баланс под одну книгу» принят осознанно) — platform/internal/pricing/pricing.go:75-98.
  3. Ставка $0.03/глава — конфиг-константа платформы: DefaultPerChapter = 30_000 micro-USD (pricing.go:33), переопределяется PerChapterMicroUSD (config.go:90-93,194,229). Провенанс сверен D39.123 п.2(г): exp08 v2 $0.0219/глава → ревизия D30.4 +1525% → округление ВВЕРХ (низкая ставка опаснее высокой — прогон встанет на потолке посреди книги; pricing.go:19-33). Временная мера беты — честная оценка движковая (строка 166).
  4. Холд прироста ДО спавна. holdTx пишет отрицательную строку ('hold', -amount) и тем же знаком двигает кэш баланса (credits.go:163-206,375-385); нехватка — ErrInsufficientCredit. Холд + прогон + попытка + очередь — одна транзакция (pgstore/runs.go:104 StartRun, :338 рестарт; форма D39.123 п.2д).
  5. Аргумент движку: 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.go CeilingArg, spawn.go:132-150); строка 145 закрыта D39.122.
  6. Baseline и списание. baseline = committed книги на старте попытки (spawn.go:74); списание попытки = разница счётчика с baseline; счётчик ниже baseline = подменённая БД, сеттлится в ноль с WARN — сами цифры в лог не идут (D39.84; reconcile.go:308-316).
  7. 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), перерасход движка счётом не абсорбируется.
  8. SpendBound (PD-159). Отложенный settle читает пожизненный счётчик КНИГИ и без границы оплатил бы работу прогона-преемника (замер: $2.10 за прогон ценой $0.10, и преемник платил снова). Граница = наименьший baseline попыток той же книги, стартовавших ПОЗЖЕ: committed = min(счётчик сейчас, SpendBound) (reconcile.go:293-307; pgstore/runs.go:268).
  9. Открытое — эскроу/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. Открытые дыры и их носители

Дыра Носитель
PD-113 (major, единственный открытый) — стоп по потолку неотличим от аварии: движок отдаёт потолок ошибкой (errReserveCeiling), exitCode мапит всё в 1; платформа ставит failed, где контракт требует paused. После PD-158 цена выросла: консервативный потолок останавливает ровно на исчерпании холда (D39.123 п.2ж). Закрывается эмиттером (строка 103) platform/docs/DEFECT_REGISTER.md:124
Строка 165 — различимые exit-коды tmctl (потолок vs graceful-stop; сегодня оба = 1), «скоро, вместе с эмиттером» docs/PROGRESS.md:113
Строка 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 · 135 · 136 · 137 · 165 · 166
Контракт наружу 14-api-contract/openapi.yaml (CeilingBounds, Usage); ПТ-35
Запрет денег на экране D39.84; D39.100/К-8; ПТ-35