# 25 · Шов движок↔платформа: ратифицированная форма и почему не иначе > ⚠ **ЭРРАТА D39.156 (23.08): «поднятие потолка ФАЙЛОМ через atomic rename» (пункт ниже, дословно `потолка — файлом через atomic rename на границах юнитов`) НИКОГДА НЕ РАТИФИЦИРОВАЛОСЬ.** > Отчёт относит этот механизм к носителям решения D39.106 — в теле D39.106 его НЕТ вовсе (проверено грепом по слайсу > живого тела ноты **D39.106** («ШОВ: прогон переживает деплой платформы») в `../architecture/05-decisions-log.md` — адресуем НОМЕРОМ, а не строкой: line-якорь в журнал решений не переживает вставку эрраты, и он уже съезжал дважды (гейт якорей, 28.08); первый хит темы — только в D39.110. ⚠ Испр. 23.08: прежняя редакция ссылалась на слайс `archive/architecture/05-decisions-D39-106-123.md`, где тела D39.106 НЕТ — греп по нему подтвердил бы эррату вакуумно). Строка была КАНДИДАТОМ панели, > а живой канон решил ось иначе: **потолок едет со стартом прогона и после не меняется**, лекарство при упоре — НОВЫЙ прогон > (`platform/internal/runs/reconcile.go` (греп `202` в обработчике возобновления; номер снят 30.08 — файл правит живая сессия зоны)). ⚠ Испр. 23.08: прежняя редакция звала «строку 69», но вставка самого баннера сдвинула нумерацию — 69-я строка теперь несёт ЖИВОЙ денежный механизм («холд не освобождается по выходу юнита»), и исполнение прежней инструкции пометило бы отвергнутым НЕ ТО. Номера строк здесь больше не называем, только подстроку. Читать НАЗВАННЫЙ ВЫШЕ пункт как отвергнутого > кандидата, а не как обязательный механизм; закон входной двери шва (`../architecture/17-seam-inbound-law.md` п.2) это фиксирует: > посреди прогона внутрь идёт ТОЛЬКО сигнал. > **Что это.** Независимая верификация шва и топологии: задача в переодетом домене отдана четырём > чистым панелям (~18 агентов, Opus и Fable, без доступа к репозиторию; изоляция проверена > поагентно по журналам). Финальная тройка: атака на готовое решение + два абстрактных дизайна, > которым явно разрешили REST/gRPC/брокер. Пятое плечо — кросс-чек третьим семейством (GPT, > через владельца): слепой дизайн сошёлся по форме и списку отвергнутого, атака — build with > named changes без альтернативы; его уточнения помечены (GPT) ниже. Воркфлоу: `wf_6adb39d7-870` · `wf_2b45042d-115` · > `wf_4ac43daf-ec5` · `wf_b7eae996-b9c` (первый прогон `wf_b2b954de-e83` загрязнён чтением > репозитория и отброшен; правило: файловые инструменты панелям запрещать явно, проверять > постфактум). > > **Статус:** решение владельца 05.08 «прогон переживает деплой платформы» ратифицировано > **D39.106**. Оркестратор №14. ## Форма - **Два Go-модуля** (движок · платформа), граница — модульная, D39.85-гард в силе. - Движок — **транзиентный/шаблонный systemd-юнит на прогон** (`Restart=no`, стоп SIGTERM, щедрый `TimeoutStopSec`), платформа его **не родитель**: её деплой прогоны не трогает. - События — **`events.jsonl` в каталоге книги**, append-only, как **проекция** уже закоммиченных строк SQLite движка (та же транзакция, что чекпойнт chunk×stage; outbox, не вторая запись). - Платформа **тейлит** журнал; запись курсора — `(engine_run_id, seq)`, байтовое смещение — хинт; курсор коммитится в той же Postgres-транзакции, что эффект события. - Деньги: **холд + потолок движку = защита** (не зависит от живости платформы), события — свежесть; hold→capture→release. `tmctl status --json` — ремонт, только по остановленному прогону (лок). - **Реконсилятор** — обязательный компонент: на каждый выход юнита (D-Bus-сигнал + `ExecStopPost=`-маркер) и на буте, где он ПЕРЕЗАПУСКАЕТ прерванные прогоны (транзиентные юниты не переживают ребут VM). - Формат потока (NDJSON · hello · версия · seq), D39.84, запрет читать живой SQLite движка — без изменений; журнал им не является. ## Отвергнутые альтернативы (кем и почему — кратко) | Вариант | Счёт | Причина отказа | |---|---|---| | Монолит (движок внутри платформы) | 0 из ~18 | Радиус поражения (леджер+вход в одном адресном пространстве с часто меняющимся движком); «одна транзакция решает деньги» ложна — БД прогона + леджер + провайдер = три хранилища при любом числе процессов. Пороги пересмотра: без cgo ✅ · деплой реже месяца ИЛИ p99 ≤ 2 ч ❌ · кросс-версионный резюм в CI ❌ | | Платформа — родитель + пайп (stdout/fd) | 0 из 15 | Время жизни движка — подмножество платформы: деплой/рестарт убивает или осиротляет прогон; дешёвый резюм это НЕ лечит (проверено отдельным плечом: факт «прерывание стоит минуты» не сдвинул ни один голос) | | gRPC/HTTP-сервис поверх движка | 0, отвергли явно | Изобретает жизненный цикл, которого у движка нет (одна задача, эксклюзивный лок); поток всё равно умирает на рестарте платформы — долговечный буфер нужен, а он и есть журнал | | Push в платформу (unix-сокет/localhost HTTP) | 2 голоса в одном плече, затем 0 | Платформа лежит несколько раз в неделю ПО ЗАМЫСЛУ ⇒ push требует буфер+ретраи+дедуп = журнал; плюс неаутентифицированный локальный писатель в денежные таблицы | | Брокер (NATS/Redis/Kafka) | 0 | Третий stateful-демон на одной VM для 1 продюсера и 1 консюмера; redelivery сам создаёт риск двойного списания | | Чтение чужой БД (в обе стороны) | 1 голос из ~18 | Сварка с приватной схемой, меняющейся еженедельно; лок-конфликт с живым писателем | | Выделенный fd 3 вместо stdout | 0 | Никому не понадобился при свободном выборе; отдельно замерено: fd 1/2 дают SIGPIPE-смерть сироты, fd 3 — нет | | journald/stdout как источник событий | 0 | journald молча роняет под всплеском и ротирует сам; людям — да, контракту — нет | ## Подтверждено вхолодную (наши решения воспроизведены независимо) Потолок у движка вместо синхронной авторизации · hold→capture→release · идемпотентность `(run_id, seq)` · запрет читать приватную БД движка · standalone тем же путём кода (не режимом) · `flock` вместо pid-файлов · запись намерения до платного вызова (Р7) · целочисленные деньги · SSE, не WebSocket · `status` как ремонт, не поллинг. Атака на готовое решение: вердикт **build-with-changes**, доминирующая альтернатива — **none**. ## Обязательные механизмы при постройке (носители в D39.106) **Деньги:** - write-ahead intent + состояние **`uncertain`** для окна «платный вызов прошёл, коммит не успел»: деньги остаются НЕРЕШЁННЫМИ в эскроу — intent доказывает намерение, не факт; spend не постится без свидетельства провайдера; флаг человеку (GPT); молчаливой бесплатной работы не бывает; - холд НЕ освобождается по выходу юнита — сигнал обгоняет финальные денежные события: состояние `closing`, эскроу держится до терминального seq журнала (GPT); расход сверх холда записывать полной суммой, превышение — явной строкой убытка платформы, не примечанием (GPT, уточняет PD-27); - **ночная сверка с данными провайдера** — приоритетнее споров об округлении: в движок суммы входят float64 по ручной таблице цен с ненулевым якорем незнакомого слага (`backend/internal/ledger/pricing.go:27-52`), систематика процентная; - **гейт потолка на каждый платный вызов**, не на юнит работы; - state-machine строки прогона в Postgres (холд может утечь до создания каталога); поднятие потолка — файлом через atomic rename на границах юнитов; кандидат: холд траншами (~часы ожидаемого расхода), не на весь прогон. **Эмиттер журнала (строка 103):** - `events` (fsync, seq, деньги) ≠ `status` (atomic rename, косметика, не для биллинга); - **формат — развилка строки 103 (GPT):** одиночный JSONL с ftruncate-ремонтом имеет гонку живого читателя и слеп к битой записи с валидным `\n`; альтернативы — пофайловые события (`events/.json`, temp+rename) либо CRC-фрейминг; побайтовая идентичность не требуется; - материализатор принимает только `seq == last+1` под блокировкой строки прогона; тот же seq с другим payload = карантин прогона, не тихий no-op (перекрытие инстансов при деплое) (GPT); - `status --json` берёт тот же flock — проверка «остановлен» не есть взаимоисключение (GPT); - **пиннинг прогона к версионированному пути бинаря**; резюм другой версией — только по явному флагу (движок выкатывается чаще, чем идёт прогон); - сброс буфера на всех путях выхода (`os.Exit` пропускает defer — теряются финальные события); - живость = растущий `seq`, не удерживаемый лок; политика обратного давления выбирается явно. **Опс:** privilege-модель создания юнитов (linger-пользователь либо узкое polkit-правило) решить до постройки · состояние systemd — не истина (источник на буте = каталоги + Postgres) · диск ограничить (`ENOSPC` на ФС леджера ломает денежные инварианты; квота на прогон, проверка места при допуске) · ключи идемпотентности провайдеров протухают — replay это оптимизация, не гарантия · отдача SPA из бинаря платформы (один origin) — вопрос к П-1, не решено. ## Открытое - Строка 103 единого бэклога и денежные строки — завести при освобождении `docs/PROGRESS.md` от чужих незакоммиченных правок; задачи зонам — отдельными промтами. - Вторая машина (решение владельца: одна пока что): при переезде умирают файловая шина и `systemctl start` как глагол — заменяются хост-агентом, шлющим те же строки; outbox, деньги, идемпотентность, SSE переживают без правки. Пиннинг книги к хосту уже в П-3.