# Журнал зоны «Платформа» > Весь прогресс платформы — ЗДЕСЬ (решение владельца 04.08): пинги, итоги сессий, открытые > вопросы, предложения на ратификацию. В `docs/PROGRESS.md` платформа не пишет; оркестратор > читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4). ## Текущее состояние - **P4 «раннер» + дофикс + V2 ПРИНЯТЫ и ЗАЛЕНДЕНЫ — D39.123, код `d29e30c`** (ревью-шапка ниже): транзиентный systemd-юнит на прогон · очередь River с холдом-до-спавна в одной транзакции · реконсилятор с базовой линией расчёта · тейлер `events.jsonl` с карантином проекции · пять ручек `/v0` контракта **0.2.1** · дев-интейк. Батарея с живым PG 18.4 под `-race` зелёная, скипов 0, тестов зоны **261**. Формула аргумента потолка — `committed + прирост` (PD-158, ратифицирована ПОПРАВКОЙ к пингу оркестратора). - **Регистр — 171 строка** (`python3 docs/scripts/counts.py`): 124 закрыто · 1 закрыт ратификацией (PD-59) · 3 риском · 43 открыто (**1 major — PD-113**: стоп по потолку отдаётся `failed` до эмиттера; после PD-158 его цена выросла — потолок срабатывает ровно на исчерпании холда). - **АКТИВНОГО промта зоны НЕТ.** Следующий — по слову владельца; кандидаты: стоп/резюм-ручки (PD-140, закроют и половину «failed»-лжи вместе со строкой 165 движка) · эскроу/uncertain (строка 136 единого бэклога) · POST /books (П-9). - Эры P0–P3 (вход OIDC · кредиты · админ-CLI · деплой · фикс-паки) — исполнены и залендены (D39.107/109/112/114); разделы — в [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md). Стек и рецепт стенда — `STACK_DECISIONS.md`; критерии приёмки — `ENGINEERING_STANDARDS.md`. ## Ратификация приёмкой P4 (оркестратор №15, 09.08) **Вердикт: P4 + дофикс + V2 ПРИНЯТЫ и ЗАЛЕНДЕНЫ — D39.123, код `d29e30c` (53 файла, +9867/−170, только `platform/`).** Три раунда, 13 агентов приёмки; каждое заявление пере-ранено исполнением. **Метод и что подтвердилось.** 7-агентный воркфлоу (слепой дифф · опровергатель-Opus D39.120 · пере-ран батареи · 10 моих посадок · 47 wire-проверок контракта · охотник-деньги · охотник-systemd с РЕАЛЬНЫМ tmctl из HEAD) → фикс-лист F1–F8 → ре-чек 4 контурами (обе пробы приёмки, пере-мутации, PD-158/159 адверсариально на Opus, охота по дельте) → V2 → финальный ре-чек (5/5 пере-мутаций убиты поимённо). Реальный движок принял платформенный argv целиком, имена полей `status --json` совпали с парсером побайтно, exit-контракт и маркер сошлись; четыре systemd-клейма пере-мерены; батарея EXIT=0 с живым PG 18.4 под `-race`, скипов 0; тестов 105→261 (+156/−0); дерево на лендинге байт-равно проверенному. **Пять MAJOR приёмки и PD-159 селф-ревью — все закрыты с пинами**; разборы в разделах «Дофикс P4» и «Ре-чек V2» ниже. Несущие: аргумент потолка против кумулятивной семантики движка (F1; вскрыт тем, что сквозная проба шла на фейке без кумулятива) · дедлок books↔run_attempts 258/300 с вечным карантином проекции на транзиентном 40P01 (F2; ложно закрытая PD-129 пере-открыта и закрыта честно) · settle по устаревшему снапшоту (F5) · двойная оплата при отложенном расчёте (PD-159, найдена селф-ревью сессии — воспроизведена приёмкой на до-фиксном дереве). **Ратифицировано (D39.123 п.2):** linger-privilege-модель · **ПОПРАВКА формулы потолка: `committed + прирост`, БЕЗ reserved (PD-158)** — сессия была права, отказавшись молча исполнять формулу моего пинга: исполнение обеих против гейта движка показало, что пинг переплачивал запасом ровно на leftover-reserved · карантин ПРОЕКЦИИ (PD-105-уточнение) · ставка $0.03 · `default_chapters` = верх шкалы · 503 в контракт (0.2.1; PD-112 закрыт) · PD-114 → задача «печать эффективной конфигурации» · PD-115 — направление принято. PD-113 — единственный открытый major, до эмиттера; его цена после PD-158 выросла (стоп по потолку = ровно исчерпание холда). Движку заведены строки 165 (различимые exit-коды потолка/стопа) и 166 (оценка $/глава). **Урок приёмки в обе стороны:** (а) фейк шва обязан моделировать СЕМАНТИКУ чужой стороны, не только форму — обе жёсткие находки (F1, N1-stop→failed) прошли бы фейком; класс закрыт кумулятивным ceilingJudge в пинах; (б) моя ошибка нумерации (PD-167 = коммент 00009, не 00011) поймана сессией — исправлена в строке; (в) две посадки сессии, пережившие первую редакцию пина, задекларированы, не подчищены — норма «заявление=команда» работает в обе стороны. ## Дофикс P4 (09.08): фикс-лист приёмки — F1…F8, N1…N4 Пак принят УСЛОВНО с пятью MAJOR. Ниже — что сделано по каждому пункту, чем это проверено и что осталось. Каждое число здесь снято исполнением; команда стоит рядом с числом. ### F1 [MAJOR, деньги/шов] Аргумент потолка — по ратифицированной формуле Приёмка права, и её посылка перепроверена мной на HEAD ОБЕИХ зон, не по памяти: `backend/cmd/tmctl/invocation.go` объявляет флаг словами «It caps the book's CUMULATIVE committed+reserved spend, not this run's increment», а `backend/internal/store/ledger.go` `Reserve` сравнивает `SUM(committed_usd + reserved_usd)` книги с `c.BookUSD` на КАЖДОЙ резервации. Движковая проба приёмки пере-прогнана в КОПИИ бэкенда (в чужую зону не писал): `TestAcceptSecondRunDeniedWhenCeilingIsPassedAsIncrement` — при committed $3.00 потолок `3.0` даёт `ReserveDeniedBook`, `6.0` — `ReserveOK`. Сделано: `runs.meter` (`committed`, `reserved`) читается ОДНИМ вызовом `status --json` перед стартом (`bookMeter`), аргумент = `committed + прирост` (`meter.bookCap`). ⚠ Слагаемое `reserved` из ратифицированной формулы НЕ добавляется — это названное отклонение в консервативную сторону, разбор и вопрос оркестратору ниже, PD-158. `reserved_usd` внесён в аллоулист `ingest.StatusReport` **указателем** — отсутствие ≠ ноль ровно по той же причине, что и у committed, только ошибка зеркальная: ноль вместо реального резерва даёт потолок НИЖЕ того, на чём книга уже стоит. Рестарт идёт тем же путём и получает свежий отсчёт (спавн один на все случаи). Фактически ушедшее значение хранится: `run_attempts.ceiling_arg_micro_usd` (миграция 00011) — оно не равно `ceiling_micro_usd`, и после того как счётчик книги сдвинулся, восстановить его нечем. Пины (посадки — ниже): `TestTheSecondRunOfABookIsGivenTheCumulativeCapAndNotItsOwnIncrement`, `TestAResumeIsGivenACapComputedFromTheMeterAsItStandsNow`. Оба гоняют фейк `ceilingJudge`, который судит `--ceiling-usd` **по правилу движка**, включая проход восстановления: открытие книги на запись обнуляет остаточный `reserved_usd`, после чего потолок на уровне `committed` или ниже отвергается первой же резервацией. Это и есть ответ на «почему батарея пака этого не поймала»: её фейк лишь записывал аргумент, а фейк, который не может отказать, не проверяет ничего. Резюмный пин отделён от арифметики специально: в нём смоделирован ОСТАВЛЕННЫЙ резерв убитого процесса ($0.20), и он утверждает ДВА факта — что потолок пересчитан по свежему отсчёту и что остаток его не раздул (PD-158). Проба приёмки на этом дереве (её файл, только `Reserved` дописан в отчёты фейка): `TestAcceptSecondRunCeilingArgument` — **PASS**, `run2 args: … --ceiling-usd 6.000000` при committed книги $3. Устаревшие посылки, названные приёмкой, сняты: - `runner/engine.go` больше не пишет, что флага нет и что копировать форму из чужого дерева нельзя: флаг заленден (`0e69bc1`), форма ратифицирована, поэтому `--ceiling-usd {{usd}}` стал **дефолтом** конфигурации (`TM_PLATFORM_ENGINE_CEILING_ARG` остаётся переопределением для сборки с другим написанием). `ErrCeilingNotWired` оставлен: тип по-прежнему допускает пустое значение, а пустой шаблон не должен значить «без потолка». ⚠ Уточнено ревью доков: он не «недостижим» — переменная из одних пробелов проходит подстановку дефолта и даёт пустой шаблон, то есть отказ в рантайме при одном WARN на буте (PD-165 — тот же класс, что относительный `TM_PLATFORM_CTL_BIN`). - `ingest/resync.go` больше не пишет, что у статуса нет фазового сплита: `progress{draft,edit}` внесён в аллоулист и материализуется `ApplyStatus` теми же четырьмя счётчиками, что и поток. Этим снято ⚠ затирания фаз резюнком — не гейтом, а тем, что затирать стало нечем. Гейт «поток идёт → ре-синк не зовём» оставлен, но его обоснование переписано честно: он теперь про ЦЕНУ (каждый вызов статуса — секунды CPU на пере-нарезку) и про то, что поток свежее. ### F2 [MAJOR] Инверсия блокировок была жива; транзиентный сбой уходил в карантин Порядок написан в ОДНОМ месте и стал глобальным — `pgstore.lockBook`: `books → runs → run_attempts → account_balances → reservations`. Книга блокируется первой в `RunSink.Apply`, `RestartRun` и `StartRun` (последний добавлен не «за компанию»: он берёт баланс до `update books`, и если бы книгу первым брал только рестарт, пара `StartRun ∥ RestartRun` дала бы ту же инверсию на другой паре строк). `closeReservation` уже брал баланс раньше резервации — проверено, не тронуто. Собственной сверкой всех транзакций пакета найден ещё один нарушитель — `DeleteBook` (резервации, потом книга); цикла для него сегодня нет, но именно такое рассуждение записанный инвариант и должен заменять, поэтому книга взята первой и там. Классификация ошибки материализации вынесена в `runs.quarantines` и стала явной: транзиентные SQLSTATE (`40P01`/`40001`, `pgstore.IsTransient`) и собственная отмена — повтор следующим свипом; карантин остаётся только логическим ошибкам (пропасть, конфликт payload, битая строка, строка длиннее буфера). Пины. Конкурентный оставлен (`TestAMaterializerAndAReconcilerOnOneRunDoNotDeadlock`, 60×3 через реальные API), но он **пробует**, а не доказывает: посадка «снять `lockBook` из `RestartRun`» его ПЕРЕЖИЛА — рестарт по-настоящему берёт замок один раз за прогон, и окно слишком узкое. Поэтому написаны два пина, утверждающие порядок ПРЯМО: `TestTheMaterializerTakesTheBookBeforeTheAttempt` и `TestARestartTakesTheBookBeforeTheAttempt` — тест держит замок книги, ждёт, пока операция реально заблокируется, и спрашивает строку попытки через `for update nowait`. Свободна — значит операция ждёт книгу, ничего не держа. Занята — значит она взяла попытку по дороге, то есть в том порядке, который и дедлочит. ⚠ Ожидание блокировки читается из `pg_stat_activity`, а не из `pg_locks`: ждущий строку ждёт на `transactionid` держателя, а у таких строк `pg_locks.database` пуст, поэтому запрос по своей базе их не видит (проверено — первая редакция теста падала именно так). ### F3, F4 [MAJOR, пины] Два свойства, построенных и не запиненных - **Нечитаемый отсчёт ОТКАЗЫВАЕТ спавну** (сердце PD-124): `TestAnAttemptWhoseMeterCannotBeReadIsNotStartedAtAll` — три формы нечитаемости (вызов упал · нет `committed_usd` · нет `reserved_usd`), в каждой утверждается, что юнит не создан И право на спавн не заклеймлено (`unit_name` пуст), значит свип повторит; хвост показывает, что после починки движка та же попытка стартует. - **Устаревший exit-маркер стирается ДО старта**: `TestAStaleExitMarkerIsClearedBeforeTheUnitStarts` — маркер создаётся до спавна, после спавна обязан отсутствовать, свип обязан оставить прогон живым и холд открытым. ### F5 [MAJOR, деньги] Расчёт по устаревшему снапшоту `pgstore.ReleaseUnspawned` перечитывает `run_attempts.unit_name` `for update` в ТОЙ ЖЕ транзакции, что и деньги, и отказывает (`ErrAttemptSpawned`), если юнит есть. Реконсилятор тогда откладывает расчёт: следующий проход видит юнит в снапшоте и считает по базовой линии попытки. Выбран SQL-гард, а не перечитывание в Go, потому что гард атомарен с самим возвратом холда. Пин: `TestAStaleSnapshotDoesNotGiveBackTheHoldOfAnAttemptThatSpent` — в проходе со старым снапшотом холд не возвращается целиком, следующий свип списывает ровно потраченные $0.50. ⚠ Проба приёмки `TestAcceptStaleSnapshotReleasesTheHoldOfARunThatActuallySpent` на этом дереве **не проходит как написана** и это не регрессия: она требует, чтобы деньги разрешились в ТОМ ЖЕ вызове `reconcile`, а исправление откладывает их на следующий проход. Замеренная разница: было — баланс `10.000000` (весь холд назад, списано ноль), стало — `7.000000` при `3.000000` в резерве (деньги на месте и видимы), после следующего свипа — `9.500000`. ### F6, F7 [MINOR] - Относительный `TM_PLATFORM_STATE_DIR` отвергается на буте (`filepath.IsAbs`), пин `TestARelativeStateDirectoryIsRefusedAtBoot`. - «Метка давности» ре-синка построена: `runs.last_resync_at` (миграция 00012) пишется КАЖДЫМ ре-синком, пин `TestAResyncRecordsWhenItWasTaken` (нет метки до первого · метка равна времени вызова · вторая переписывает первую). ⚠ Названная девиация: **на провод метка не выходит** — в контракте v0 у прогона поля свежести нет, и заводить его самовольно не право зоны (PD-150). ### F8 [MINOR, отчёт/регистр] Все пять исправлений внесены: числа тестов пересчитаны исполнением и приведены с командой (105 → 261, удалённых 0); арифметика «36 новых» исправлена на 27 + 9 (после ратификации PD-112) и подтверждена скриптом по таблице; заявление «каждый из 22 пинов проверен посадкой» снято как непроверяемое (PD-142); строка PD-105 приведена к коду — карантинится ПОПЫТКА (её проекция), жизненный цикл прогона продолжается; PD-122 дополнен: ревизия не двигается и на ДОБАВЛЕНИИ книги (`AddBook` пишет `revision = 0`), класс тот же. ### Новые строки регистра (N1…N4) и ратификации Заведены четыре по фикс-листу плюс две собственные (PD-156 — ниже, PD-151 — числа отчёта): **PD-152** (стоп → `failed`: реальный `tmctl` ловит SIGTERM и выходит кодом 1, так что ветка `stopped` достижима только для смерти ОТ сигнала — проба пака показала `stopped` на фейке, который именно так и умирал) · **PD-153** (грация спавна от `started_at`, а не от времени заявки) · **PD-154** (`settled_at` может остаться NULL между `Settle` и `MarkSettled`; потребителей нет, деньги целы) · **PD-155** (смена `StateDir` осиротляет маркеры — строка внесена в `deploy/README.md`). Ратификации оркестратора внесены в регистр: **PD-112** закрыт ратификацией (`503` вносится в спеку правкой владельца контракта) · **PD-114** переформулирован в задачу «печать эффективной конфигурации на старте с редакцией секретов», env-only остаётся · **PD-115** — направление «по внешнему эталону на ось» ратифицировано, носитель работы — строка · **PD-129** пере-диспозиция: строка была закрыта ЛОЖНО, действительно закрыта дофиксом (PD-145) · `default_chapters` = верх шкалы остаётся, кода не касается · PD-113 остаётся открытым до эмиттера. ### Батарея и посадки дофикса - `make check` с живым PostgreSQL 18.4 под `-race`: **13 пакетов, 0 FAIL**, линтер **0 issues**. - `go test ./... -count=1 -v | grep -c -- '--- SKIP'` = **0**. - `make vuln`: **No vulnerabilities found**. - Тесты: **105 → 261**, удалённых **0** (команда — раздел «Батарея и самопроверка» ниже). - **Посадки: 24 из 24 пойманы** (скрипт восстанавливает файл после каждой): `bookCap` возвращает прирост · записывается прирост вместо аргумента · отсутствующий `reserved_usd` читается нулём · материализатор берёт попытку первой · рестарт берёт попытку первой · транзиентный сбой карантинится · нечитаемый отсчёт читается нулём · очистка маркера удалена · расчёт доверяет снапшоту (`Release` вместо `ReleaseUnspawned`) · относительный `StateDir` принимается · метка ре-синка не пишется · метка пишется только однажды · ре-синк снова кладёт один агрегат · шаблон потолка теряет дефолт · отказ деплоя проверяется ПОСЛЕ вызова движка · остаточная резервация раздувает потолок · `DeleteBook` берёт резервации раньше книги · отложенный расчёт не ограничен верхней границей · транзиентный сбой карантинится (через РЕАЛЬНЫЙ путь) · повторная заявка перезаписывает базовую линию · рестарт Postgres карантинит проекцию · сброшенное соединение карантинит проекцию · ретрай пересчитывает переданный потолок · счётчик назад списывает молча. ⚠ Две посадки в первой редакции **ПЕРЕЖИЛИ** и это записано, а не подчищено: «рестарт берёт попытку первой» (конкурентный пин её не ловил — написаны прямые пины порядка) и «метка пишется только однажды» (тест ставил метку один раз — добавлена вторая проверка). - Контрактная проба приёмки (47 wire-проверок, `-overlay` поверх пакета `httpapi`) пере-прогнана на этом дереве целиком: **PASS**, формы не сломаны. - Гоночная проба приёмки `TestAcceptConcurrentStartRunTakesExactlyOneHold`: **PASS** (8 конкурентных `StartRun` → 1 прогон, 1 холд) — она проверяет то, что `lockBook` в `StartRun` мог бы изменить. ### Что нашла собственная сверка диффа (после фиксов, до сдачи) Две правки, обе внесены и обе — про то, что дофикс мог испортить рядом: 1. **`DeleteBook` брал резервации раньше книги** — единственный оставшийся нарушитель порядка, который я записал как глобальный. Цикла для него сегодня нет (проверено перебором пар: удаление трогает только ЗАКРЫТЫЕ резервации, а `StartRun` вставляет новую), но инвариант с необъявленным исключением перестаёт быть инвариантом — книга взята первой и там, и это запинено (`TestDeletingABookTakesItBeforeItsReservations`; вспомогательный `assertBookFirst` обобщён на любую вторую строку — попытку или резервацию). 2. **Отказ ДЕПЛОЯ проверялся после вызова движка.** Перенеся чтение отсчёта в начало `spawnAttempt`, я поставил его ПЕРЕД дешёвыми отказами («нечем записать конец юнита», «нечем передать потолок»), и инстанс с неполной конфигурацией платил бы секундами CPU движка за каждый прогон на каждом свипе, чтобы прийти к ответу, зависящему только от конфигурации. Вынесено в `runs.runnable()`, вызывается первым; пин `TestAMisconfiguredDeploymentIsRefusedWithoutAskingTheEngine` (посадка «убрать ранний отказ» падает — 15-я в списке выше). ### Формула потолка: почему `reserved` в неё не входит (PD-158, ратифицировано 09.08) Сдавалось как ВОПРОС с консервативным кодом; ре-чек V2 ратифицировал формулу зоны, пере-мерив обе против гейта движка — та, что с `reserved`, переплачивала запасом ровно на leftover-reserved. Первая формула — `committed + reserved + прирост` — читает `reserved_usd` из `status --json`. Но `store.Open`, путь ЗАПИСИ, которым идёт каждый `translate`, выполняет `recoverReservations` и обнуляет `reserved_usd` книги ДО того, как будет судима первая резервация (`backend/internal/store/store.go:88`, тело — `:214`); `tmctl status` идёт read-only и этот проход намеренно не делает — движок пишет об этом сам (`store.go:108`: «after a run crashes, reserved_usd stays non-zero until the next WRITE command … status will show this leftover as reserved»). Значит цифра, которую платформа кладёт в потолок, к моменту сравнения уже стёрта, и прогон получает на её величину БОЛЬШЕ, чем его холд: движок останавливается позже, расчёт упирается в потолок холда, леджер пишет «capped at the hold» — и это читается как перерасход движка, хотя это наша арифметика. Путь достижим на каждом резюме после падения, потому что падение и оставляет резервацию. **Что сделано:** `bookCap` считает `committed + прирост`. Отклонение в КОНСЕРВАТИВНУЮ сторону — более узкий потолок может только остановить прогон раньше, перерасхода не даёт, — названо в коде и запинено, и сдано ВОПРОСОМ, а не решением: отклонять ратифицированное молча не право зоны. Ответ пришёл ре-чеком V2 — формула принята. Цифра `reserved` по-прежнему читается и обязательна: она и есть доказательство, что отклонение безопасно — на спавне другого писателя нет (эксклюзивный лок плюс один живой прогон на книгу), значит любой `reserved` по построению остаток. ### Найдено при F1 и заведено, а не построено **PD-157: дневной потолок книги `--ceiling-usd` не перекрывает.** Ратификация D39.122 говорит это прямо, и движок требует хотя бы один из `book_usd`/`day_usd` (`backend/internal/config/book.go:250`), а `book.yaml` пишет ОПЕРАТОР — платформа его не правит (D39.110 §2b) и в `book add` лишь проверяет наличие. Значит книга с низким `day_usd` останавливает прогон на лимите, которого платформа не выбирала: движок выходит кодом 1, прогон приезжает `failed`, деньги пользователя целы, причина ему не видна. В `status --json` дневной фигуры нет вовсе, так что и диагностировать это платформа сегодня не может. Не строил: фикс-лист этого не просил, а закрывается оно либо проверкой при заведении книги, либо словом контракта о том, кто владеет потолками `book.yaml` у книг под платформой — второе не право зоны. ### Селф-ревью дофикса: три верификатора, что нашли Прогнаны три независимых ревьюера по разным линзам — один по фикс-листу и диффу, один НАМЕРЕННО без отчёта («вне карты»), один по claim-fidelity доков против кода. Все трое работали read-only, мутации гоняли в копиях дерева. **Найдено и ПОЧИНЕНО в этом же дереве:** 1. **PD-159, деньги, major.** Отложенный расчёт прогона оплачивал работу СЛЕДУЮЩЕГО прогона той же книги, а тот платил за неё ещё раз. Нашли ДВА верификатора независимо, каждый воспроизвёл исполнением; я воспроизвёл третьим ($2.10 за прогон, стоивший $0.10) ПЕРЕД починкой. Причина — расчёт читает пожизненный счётчик КНИГИ в момент повтора, а завершённый-но-нерассчитанный прогон не мешает начать новый. Починено верхней границей `SpendBound` (наименьшая базовая линия среди попыток книги, стартовавших позже — она снята после остановки этой попытки и до того, как та что-либо добавила, то есть ТОЧНАЯ граница). 2. **PD-160, пин.** «Транзиентный сбой не карантинит» пинилось на чистой функции; удаление ветки, которая её ВЫЗЫВАЕТ, переживало батарею. Написан пин, который гонит НАСТОЯЩИЙ дедлок через весь путь. 3. **PD-161, деньги.** Повторная заявка права на спавн перезаписывала базовую линию — при `systemd-run`, убитом ПОСЛЕ подачи запроса, попытка платила бы разницу от цифры, включающей её же работу. **Найдено, заведено и НЕ построено** (каждое — строкой с названным лечением): PD-152 расширен живым замером — штатная перезагрузка закрывает все прогоны как `failed`, потому что `ExecStopPost` отрабатывает и движок выходит кодом 1 (путь строки 138 покрывает только потерю питания) · PD-162 удалённый каталог книги заклинивает прогон навсегда с открытым холдом · PD-168 бюджет перезапуска считается по текущей ставке (замерено: $5.50 вместо $2.50 при удвоении) · PD-163 ревизия области читается вторым запросом · PD-164 неразбираемый маркер вечно валит реконсиляцию · PD-165 относительный `TM_PLATFORM_CTL_BIN` и `$` в `quoteArgv` · PD-166 `chunker_version` теряется · PD-167 расхождение док↔код в комментарии миграции · PD-169 бюджет свипа один на все прогоны · PD-170 асимметрия `Settle` с PD-97. **Найдено в ДОКАХ и исправлено:** тело F1 противоречило собственному разделу PD-158 · описание фейка `ceilingJudge` отстало от кода · §21 `STACK_DECISIONS` не догнал PD-158 · старый раздел «Сессия P4» утверждал, что флага потолка в HEAD нет · числа тестов и посадок разъезжались между журналом и регистром · PD-158 приписывал D39.122 буквальную формулу, которой в решении нет (она из пинга) · PD-122 требовал «свою миграцию», хотя колонка `users.library_revision` существует с 00001 и не используется ни одним путём · «`ErrCeilingNotWired` недостижим через конфигурацию» — переменная из пробелов до него доходит. ### Ре-чек V2 (оркестратор №15, 09.08): четыре хвоста, три — в новом коде дофикса **V2-1 [MAJOR] — моя же классификация была неполна.** `IsTransient` знала только про дедлок и сериализацию, поэтому ШТАТНЫЙ рестарт Postgres (57P01/57P02/57P03, класс 08, сетевой сброс) всё ещё карантинил проекцию живого платного прогона НАВСЕГДА — пути снятия карантина в дереве нет. Клейм PD-145 «карантин остаётся только логическим ошибкам» был в этой части неверен и пере-сформулирован. Теперь покрыты класс 08, 57P0x, `pgconn.SafeToRetry` и любой `net.Error`; ошибки чтения ФАЙЛА при этом остаются логическими — `*fs.PathError` не удовлетворяет `net.Error`, поэтому битый журнал по-прежнему останавливает проекцию, как и должен. Шесть новых кейсов в таблице пина, две посадки. **V2-2 [MINOR, деньги] — PD-161 был закрыт наполовину.** В БД значения сохранялись, а движку на ретрае уходил потолок, пересчитанный по СВЕЖЕМУ счётчику, то есть включающий трату собственного «призрака»; форензик-колонка 00011 на этом пути лгала (замер приёмки: handed 3.400000 против stored 3.000000 — воспроизведён посадкой на моём дереве до буквы). Теперь ретрай ПЕРЕДАЁТ сохранённый аргумент. Пин расширен до `handed == stored`, плюс отдельный пин на гард в БД (`TestASecondClaimOnOneAttemptKeepsWhatTheFirstRecorded`) — после правки в Go посадка на `coalesce` переставала ловиться, и свойство без собственного пина не считается закрытым. **V2-3 [MINOR, доки у денег]** — три текста утверждали отменённую формулу (`reconcile.go` про рестарт, комментарий поля `Reserved`, строка PD-144) и два — несуществующий отказ загрузчика на пустое переопределение. Всё приведено к факту. Туда же: **комментарий миграции 00011 нёс отменённую формулу, а 00009 обещал перечитывание файла, которого тейлер не делает.** Обе исправлены, отпечатки в `migrations.sha256` обновлены — с явной причиной в шапке файла: обе миграции написаны этим паком, нигде не применялись, а миграция с лгущим комментарием хуже сдвинутого отпечатка до релиза. ⚠ Ре-чек назвал застывшим комментарий 00011 под номером PD-167, тогда как предмет PD-167 — 00009; исправлены обе, расхождение в нумерации названо в строке. **V2-4 [NOTE]** — счётчик книги ниже собственной базовой линии попытки списывал $0 молча. Клампить в ноль правильно (платить за подмену БД проекта код решать не вправе), молчать — нет: добавлен WARN, не INFO, с фактом и без цифр. ### Что дофикс НЕ трогал (осознанно) Ручки стопа/резюма (PD-140) · эскроу и `uncertain` (строка 136) · `POST /books` (П-9) · собственный счётчик ревизии области (PD-122) · грация от времени заявки (PD-153) — заведены строками, не построены: фикс-лист их не просил, а расширять пак на приёмке — это то, за что заведён PD-83. --- ## Сессия P4 (08–09.08): раннер — юнит-на-прогон, очередь, реконсилятор, тейлер, первые ручки /v0 **Каждое число ниже — с командой, которой получено.** Дерево не коммичено; в индексе ничего не держу. ### Работа 1 — транзиентный systemd-юнит на прогон `internal/runner/runner.go` (юнит), `marker.go` (маркер выхода), `engine.go` (инвокация движка). **Привилегия-модель решена ДО постройки, и второй вариант отвергнут по СВОЙСТВУ, а не по вкусу.** Выбран linger-пользователь: юниты создаются в СОБСТВЕННОМ менеджере платформы, новых привилегий в рантайме ноль. Polkit-вариант research/25 непригоден: **polkit авторизует ГЛАГОЛ, а не свойства** — `StartTransientUnit` отдаёт выбор `ExecStart=`/`User=` вызывающему, а транзиентный СИСТЕМНЫЙ юнит без `User=` идёт от root, то есть правило выдаёт сервису root и сузить это нечем. Плюс до systemd v257 в запрос авторизации не попадает даже ИМЯ юнита (systemd issue #17224 → PR #34651, влит 09.10.2024), так что на стенде (systemd 255, `systemctl --version`) «узкое правило» не узкое вовсе. Разбор — `STACK_DECISIONS` §15. **Замерено на стенде, не выведено из доки** (все команды — `systemd-run --user`, стенд без root): | Свойство | Как замерено | Результат | |---|---|---| | Юнит переживает того, кто его создал | спавнер-скрипт стартует юнит и выходит | `ActiveState=active`, маркер `success` | | `--collect` ⇒ состояние systemd не истина | `systemctl --user show` после выхода | `LoadState=not-found`, `ExecMainStatus=0` для прогона, вышедшего с кодом **3** | | Маркер несёт исход | `ExecStopPost` на трёх исходах | `exit-code/exited/3` · `success/killed/TERM` · `oom-kill/killed/TERM` | | **Лимиты в `app.slice` НЕ применяются** | `cat app.slice/cgroup.subtree_control` | **пусто**; лист без `memory.max`; процесс, потрогавший 400 МиБ, пережил `MemoryMax=64M` | | Лимиты в своём срезе применяются | то же в `tm-runs.slice` | `memory.max=67108864`, `pids.max=32`, тот же процесс — `oom-kill`, `Memory peak: 64.0M` | | `%`-спецификаторы в `--property=` не раскрываются | `ExecStopPost` с `100%_done`/`%n` | доехали буквально ⇒ экранировать `%` не нужно и было бы неверно | | `ProtectHome=yes` прячет `/run/user/` | проба под юнитом | `Permission denied` ⇒ своя же шина недостижима; `ProtectHome=tmpfs`+`BindPaths` ⇒ сокет виден | | Шине хватает `DBUS_SESSION_BUS_ADDRESS` | `env -u XDG_RUNTIME_DIR` | работает; без обоих — `Failed to connect to bus` | **Ответ PD-13 живёт в cgroup ПРОГОНА и измерен.** ⚠ И там же найдено собственным флейком: `MemoryMax` БЕЗ `MemorySwapMax=0` не ограничивает прогон — ядро выдавливает страницы в своп (у среза `swap peak: 692.5M`), процесс выживает и тормозит до скорости диска. Для перевода это хуже отказа: тормозящий часами прогон продолжает платить за каждый прошедший вызов. Снято: `MemorySwapMax=0` идёт вместе с `MemoryMax` (`runner.go`), три прогона пина подряд зелёные, три посадки «убрать своп-лимит» подряд красные. Пиннинг версии (строка 139): `run_attempts.engine_binary` пишется ДО создания юнита, новая попытка его НАСЛЕДУЕТ, резюм исполняет именно его, а переход на другую сборку требует явного `TM_PLATFORM_RESUME_MAY_CHANGE_ENGINE`. ⚠ Первая редакция закрывала эту строку НАПОЛОВИНУ — путь читался каналом ремонта и не исполнялся резюмом; поймано моей же пост-сверкой диффа с промтом, не батареей (обе половины компилировались и всё было зелёным). PD-143. ### Работа 2 — очередь River + воркер `go.mod`: `github.com/riverqueue/river v0.42.0` + `riverdriver/riverpgxv5` (пин из `STACK_DECISIONS` подтверждён). `internal/runs/queue.go`. **Холд берётся ДО спавна и в ОДНОЙ транзакции с созданием прогона, попытки и записи очереди** (`pgstore.StartRun`, `runs.go:66`): холд без прогона — деньги, зарезервированные ни за чем; прогон без холда — трата незарезервированного; запись очереди без обоих — воркер, спавнящий движок против книги, у которой нет бухгалтерии. Очередь вставляет своё задание через колбэк с той же `pgx.Tx`. **Гард «один живой прогон на книгу» — исполнением:** частичный уникальный индекс `runs_one_live_per_book` мапится в доменную `ErrRunInFlight` (не сырой SQLSTATE), ручка отдаёт 409. Живая проба: второй `POST .../runs` на ту же книгу → **409**, `Reserved` не сдвинулся. `MaxAttempts: 1` у задания — намеренно против рефлекса очередей: повтор здесь не доделывает работу, а спавнит ВТОРОЙ движок; восстановление — работа реконсилятора. Живая проба: повторная доставка задания на живой прогон не создала второго юнита (`len(started)` = 1). ### Работа 3 — реконсилятор `internal/runs/reconcile.go`. На КАЖДЫЙ выход юнита — через **ExecStopPost-маркер**, не D-Bus: сигнал, посланный в момент, когда платформа лежит (а она лежит несколько раз в неделю по замыслу), теряется; файл — нет. На БУТЕ тот же свип перезапускает прерванные прогоны (строка 138): истина — каталоги книг и Postgres, systemd спрашивается только «жив ли юнит», и это улика, не вердикт. Разграничитель «не стартовал ещё» / «умер без слова» — время (`spawnGrace` 60 с). Перезапуск даёт новой попытке ОСТАТОК бюджета (`budget − RunSpent`), иначе один прогон потратил бы потолок дважды; если остатка нет — `paused`/`credit_exhausted`, не `failed`. **Расчёт — из фигуры движка, прочитанной ПОСЛЕ выхода процесса** (`status --json`, ратифицированный канал ремонта; лока нет, гадать не о чем). Не прочиталась — резервация остаётся ОТКРЫТОЙ и попадает в `UnsettledRuns` для следующего свипа. ⚠ Эскроу-половина research/25 (write-ahead intent, `uncertain`, `closing`) НЕ строилась: это строка 136 и её собственный промт. **Резюнк — честно редкий и честно грубый:** по умолчанию 5 минут (`TM_PLATFORM_RESYNC_EVERY`), и только там, где чинить нечего — курсор не двигался ЛИБО материализация в карантине. Причина в цене: каждый вызов заново ингестит и режет исходник (1.4–1.5 с CPU на книге 23 МБ, строка 100). ### Работа 4 — тейлер `events.jsonl` `internal/ingest/tail.go`. Курсор `(engine_run_id, seq)` коммитится в ОДНОЙ транзакции с эффектом (`pgstore.RunSink.Apply`), байтовое смещение — хинт. Файла ещё нет (эмиттер = строка 103) — тейлер спокойно ждёт (`ErrNoJournal` не ошибка свипа). **PD-105 ратифицирован этим паком и реализован:** дубль `seq` — НОРМА, пропускается идемпотентно и без фатала; тот же `seq` с ДРУГИМ payload — `ErrPayloadConflict` → карантин ПРОЕКЦИИ (не прогона: движок тратит уже зарезервированные деньги, и наша неспособность читать его журнал — не повод это выбросить). Пропасть остаётся ошибкой. Строка PD-105 реестра обновлена. Частичная последняя строка не читается как событие (писатель ещё пишет). Журнал пер-книжный и append-only, поэтому резюм дописывает ВТОРОЙ `hello`: читатель ведёт область и не судит чужие строки. ### Работа 5 — ручки `/v0` по контракту 0.2.0 `internal/httpapi/v0.go`: `GET /books` · `GET /books/{bookId}` · `GET /books/{bookId}/run-options` · `POST /books/{bookId}/runs` · `GET /usage`. Прогресс/статус прогона отдаётся внутри карточки книги — отдельной ручки прогона в спеке НЕТ, и выдумывать её я не стал. `CeilingBounds`: максимум = `Balance` КАК ЕСТЬ (холд — дебет в момент взятия), подрезан И остатком книги; `max_chapters: 0` легален. `default_chapters` = верх шкалы — продуктовая политика платформы (D39.115 §4), компромисс назван вслух в коде и вынесен вопросом ниже. **Интейк:** дев-инструмент `tmplatformctl book add` (Go-подкоманда, а не шелл-скрипт: тестируется). `POST /books` (multipart) НЕ взят — обоснование строкой **П-9** зонного бэклога. **Пересчёт «главы→доллары»** — константа платформы `$0.03`, провенанс: exp08 v2 ($10.94 на 500 глав = $0.0219) через ревизию D30.4 (+15–25% → $0.0252–0.0274), округление ВВЕРХ. Движковой поверхности оценки не выдумывал; потребность — строка **П-10**. ### Работа 6 — регистр Закрыты: **PD-43** (денежный контур получил вызывающих) · **PD-81** · **PD-82** · **PD-97** · **PD-99** · **PD-105**. Заведены **PD-108…PD-116** (из них PD-108/110/111/116 — дефекты, найденные и закрытые внутри этого же пака; PD-112…115 открыты вопросами). Строки эмиттер-стороны (PD-60/61) не тронуты. ### Сквозная проба на боевом бинаре (не тестами) Поднят `tmplatformd` против живого PG, с фейковым `tmctl` ($0, говорит ровно две поверхности: инвокацию с потолком и `status --json`). | Шаг | Наблюдение | |---|---| | `book add` + `GET /v0/books` | книга в библиотеке, `status: not_started` | | `GET run-options` | `min 1 / max 333 / default 333` — $10 / $0.03 = 333, подрезано 500 главами книги | | `POST runs` (100 глав) | `202`, форма `Run` по спеке; юнит `tm-run--1.service` **active running** | | потолок, дошедший до движка | `--ceiling-usd 3.000000` = 100 × $0.03 | | второй `POST` на ту же книгу | **409** | | `/v0/usage` при открытом холде | `remaining_percent: 70` (холд — дебет) | | прогресс из журнала | `draft 4/10, edit 0/10` — пофазно, без затирания резюнком | | выход юнита | книга и прогон → `ready`, `finished_at` проставлен | | расчёт | грант $10 → движок отчитался $0.42 → баланс **9.580000** (не потолок $3) | | гигиена логов | `grep -c 'ceiling-usd\|3.000000\|bk_' daemon.log` = **0** | | systemd после `--collect` | юнитов **0** | **Ратифицированное свойство D39.106 — прогон переживает деплой — проверено убийством платформы:** `pkill -9` по имени процесса → API отвечает `000`, юнит `active running`, журнал за 6 с вырос `10 → 13` строк. Платформа поднята заново → карточка показала `draft 21/300` (догнала всё, что было записано, пока её не было), через 6 с — `24/300`, юнит ОДИН, в логе новой платформы `grep -c 'run unit started'` = **0** (второго движка не создано). Остановка через systemd → маркер `success/killed/TERM` → статус **`stopped`** (не `failed`). ### Батарея и самопроверка - `make check` с `TM_PLATFORM_TEST_DSN` (живой PostgreSQL 18.4, `-race`): все 13 пакетов зелёные, линтер **0 issues**. Прогнана дважды подряд. - Скипы: `go test ./... -count=1 -v | grep -c -- '--- SKIP'` = **0**. - `make vuln`: **No vulnerabilities found**. - Дифф `^func Test` ИСПОЛНЕНИЕМ: **105 → 261**, удалённых **0**, добавленных **156** (число после дофикса 09.08). Команда, которой считано: `H=$(git grep -h '^func Test' HEAD -- 'platform/**/*.go' | sed 's/(.*//' | sort -u)` · `T=$(grep -rh '^func Test' platform --include=*_test.go | sed 's/(.*//' | sort -u)` · `comm -23 <(echo "$H") <(echo "$T")` — пусто, то есть удалённых ноль. ⚠ **Исправлено приёмкой (PD-151):** здесь стояло «105 → 203, +98», и это было неверно на момент написания — приёмка пересчитала 242/+137/−0, дофикс с ре-чеком довели до 261/+156/−0. Шапка журнала при этом была права. ⚠ Две мои промежуточные редакции этого счёта были неверны, и это записано, а не подчищено: сначала pathspec не тот, потом грep без `*.go` — второй ловил КОПИЮ теста, вставленную в этот же журнал приёмкой P1 (`platform-PROGRESS.md:683`), и рисовал ложное «удалён `TestHoldAndSettleOnTheSameAttemptDoNotDeadlock`». Тест на месте: `credits_test.go:308`. - **Посадки (мутации): 12 из 12** в самопроверке; **139 в аудите ревью** — 107 поймано, 32 пережили (8 не ослабления), по ним написано **22 новых пина**. ⚠ **Заявление «каждый проверен своей посадкой» СНЯТО приёмкой (PD-142/PD-151):** прогонов посадок было девять (9/10, затем 9/9), и поимённого соответствия пин↔посадка сессия не вела — 22 ими не покрываются. Итого посадок, проверенных поимённо: **33 моих в паке** (ниже) и **24 в дофиксе** (раздел «Дофикс P4»), все пойманы. Каждая восстанавливалась после прогона: холд вне транзакции допуска · мапинг `runs_one_live_per_book` · high-water mark в синке · `greatest` у spend · payload-конфликт · детект пропасти · частичная строка как событие · область чужого потока (обе стороны: `mine := true` и `mine := false`) · срез прогона · половина шкалы (та самая ошибка «минус Reserved») · грация спавна · потолок как `failed` · argv в INFO-логе · `MemorySwapMax` · compare-and-set права на спавн. ⚠ **Одна посадка ПЕРЕЖИЛА первую редакцию пина** и это записано, а не подчищено: high-water mark проверялся событием `progress`, а `progress` — присваивание, то есть идемпотентен сам по себе. Настоящий пин — считающий эффект (`unit_done`): `TestARedeliveredCountingEventDoesNotCountTwice`. ### Девиации и то, чего не сделал 1. **`503` на старте прогона не входит в перечисленные спекой статусы операции** — PD-112, вопрос владельцу контракта. Каждый разрешённый код в этой ситуации соврал бы. 2. **Стоп по потолку сегодня отдаётся как `failed`** — PD-113, контрактно видимо. Движок возвращает потолок ошибкой (`errReserveCeiling`, сверено в HEAD), события потолка нет (строка 103). Ветка под событие построена и запинена; выдумывать порог «сколько процентов потолка = стоп» я не стал. 3. **`POST /books` не взят** — П-9, с обоснованием. PD-72 закрывается вместе с ним. 4. **Эскроу/`uncertain`/`closing` не строились** — строка 136, следующий денежный промт. 5. **Стоп/резюм прогона как ручки контракта** не в списке работ промта — не строились; банк-стоп заканчивает попытку и прогон, резюм = новый прогон. 6. **`Supervisor.Status` дев-пути починен** (PD-108) — он в моей зоне и это мой канал ремонта. ### Адверсариальное ревью (author≠reviewer), и почему это главный результат пака Четыре независимых верификатора, ни один не читал отчёт (его на тот момент не существовало): (1) промт+дифф вслепую · (2) охота за дефектами ВНЕ карты · (3) сверка провода с openapi 0.2.0 · (4) сила пинов посадками. Каждому запрещено писать в репозиторий; эксперименты — в копиях. **Нашли три ДЕНЕЖНЫЕ ошибки и три запирающие прогон. Все закрыты в этом дереве, каждая с пином.** | Находка | Чем это было | Как закрыто | |---|---|---| | **PD-124** (major) | Расчёт брал `committed_usd` — ПОЖИЗНЕННУЮ сумму КНИГИ (`SUM(committed_usd) … WHERE book_id`) — как трату прогона. Второй прогон книги оплачивал первый заново; после того как сумма книги перерастает потолок, каждый прогон стоит ровно свой потолок. **Воспроизвели ДВА верификатора независимо** | Попытка пишет базовую линию книги ДО старта (миграция 00010) и платит разницу; линию не прочитать = не стартуем | | **PD-125** (major) | Перезапуск при отложенном расчёте брал ВТОРОЙ холд и терял первый навсегда: списки его не находили | Перезапуск спрашивает `AttemptReservationOpen`; список несведённых ключуется на завершённости ПОПЫТКИ | | **PD-126** (major) | Юнит, который не удалось создать, оставлял «право на спавн» — и каждый свип перезапускал прогон, съедая потолок по свипу за раз | Неудавшийся `Start` снимает право; повторяется та же попытка | | **PD-127** (major) | Одна нечитаемая строка журнала возвращала ошибку ДО чтения маркера: прогон навсегда `translating` с висящим холдом | Любая ошибка журнала = карантин ПРОЕКЦИИ, жизненный цикл продолжается | | **PD-128** | Грация спавна мерилась от старта ПРОГОНА ⇒ у перезапущенной попытки её не было | Мерится от старта попытки | | **PD-129** | Инверсия порядка блокировок `books`↔`runs` между материализатором и финишером ⇒ взаимоблокировки | Книга блокируется первой везде | | **PD-130/131** | Любая ошибка чтения журнала выглядела как «догнали»; хендшейк не обязан был нести `seq 1` | Только настоящий EOF; `seq != 1` = плохой хендшейк | | **PD-132** | Холд прогона, который так и не стартовал, не возвращался | Нет линии и нет юнита ⇒ холд возвращается целиком | | **PD-133/134/135** | Инстанс без движка не отдавал даже библиотеку; пиннинг версии писался и не читался; прерванный прогон вставал `paused` без причины | Чтения монтируются отдельно; `EngineBinary` читается и используется; `PauseRun` вместо `FinishRun` | | **PD-117…121** | Потолок ниже минимума схемы отвечал 409 вместо 400 · `Usage.paused_reason` недостижим через путь реконсилятора · карточка несла ревизию ПРОГОНА (отстающую) · чужой курсор пагинации принимался · «exhausted» при остатке, на который прогон стартует | Все пять закрыты, каждая с пином | | **PD-136/138** | Деплой-юнит нёс обе диспозиции сразу; `go.mod` не тидинут | Переписано; `go mod tidy` | | **PD-142** | **Аудит силы пинов: 139 посадок, 107 поймано, 32 пережили** (8 из них — не ослабления: эквивалентный код или страховка DDL). Пережившие — не дефекты кода, а отсутствующие пины, часть на свойствах, объявленных закрытыми | 22 новых пина написаны; ⚠ «каждый проверен своей посадкой» СНЯТО приёмкой (PD-142/PD-151) — прогонов посадок было девять. Самые весомые: блокировка строки попытки под КОНКУРЕНЦИЕЙ (восемь горутин на одно событие — последовательная доставка посадку не ловила) и **CSRF на контрактной поверхности** (снятие переживало всё, а это старт платного прогона с амбиентной кукой) | **Оставлено открытым и названо:** PD-122 (`Library.revision` не монотонна при удалении книги — сегодня недостижимо, ручки удаления нет; правильное решение — свой счётчик области, это своя миграция и несколько путей записи, поэтому НЕ сделано, а объявлено гейтом) · PD-137 (упорядочение на пользовательский менеджер — UID site-specific, инструкция в шапке юнита) · PD-139 (пути книг в ERROR-логах — диспозицию надо принять осознанно) · PD-140 (`Service.Stop` не подключён: ручек стопа/резюма в скоупе промта не было) · PD-141 · PD-112/113/123. **Что ревью подтвердило, а не опровергло:** привилегия-модель (с одним уточнением, принятым: «никакое правило не сузит» верно для ТРАНЗИЕНТНЫХ юнитов; шаблонный системный юнит с фиксированным `User=`, запускаемый `StartUnit`, — вариант, которого мой разбор не касался) · арифметика `CeilingBounds` (баланс КАК ЕСТЬ, без второго вычитания холдов — проверено на живом PG пятью раскладами) · отсутствие кросс-аккаунтных чтений и стартов · инвариант `balance == SUM(ledger)` (сломать не удалось) · правила тейлера по областям и курсору. **Возражение ревью, которое я принял только частично.** Верификатор предложил различать стоп по потолку через `ceiling_pct` из `status --json`. Не взял: `ceiling_pct` считается против `book_ceiling_usd`, то есть против потолка ИЗ `book.yaml`, а наш потолок приходит аргументом прогона (строка 145), которого в HEAD НА ТОТ МОМЕНТ не было — значит смысл поля зависел от ещё не залендённой формы (флаг заленден 09.08, `0e69bc1`; альтернатива от этого не становится верной — `ceiling_pct` по-прежнему считается против потолка из `book.yaml`, а не против переданного аргумента). Взять его сейчас значило бы построить дискриминатор на поведении, которого я не могу проверить. Записано в PD-113 как названная и оценённая альтернатива. **Повторная сквозная проба на боевом бинаре после починок** (чистая база, фейковый движок с НАКОПИТЕЛЬНЫМ счётчиком книги, как у настоящего): грант $10.00 → прогон 1 (движок насчитал $0.40) → баланс **9.600000** → прогон 2 на ТОЙ ЖЕ книге (счётчик книги $0.40 → $0.80) → баланс **9.200000**, резервов ноль. До починки второй прогон списал бы $0.80. ### Вопросы (канал вопросов промта; НЕ тихая интерпретация) 1. **`503` вне перечня спеки** (PD-112) — вносить в спеку или назвать другой код? 2. **`default_chapters` = верх шкалы.** Когда книга дороже баланса, верх шкалы резервирует ВЕСЬ баланс под одну книгу, и вторую начать нельзя (D39.110 §2в). Альтернатива — доля от максимума; какая именно — вопрос продуктовый, не инженерный. 3. **Конфигурация (вопрос ВЛАДЕЛЬЦА 08.08, PD-114).** Ответ по существу: единого stdlib-пути в Go нет; мейнстрим — либо 12-factor «только окружение» (наш выбор, записан в доккомменте `config.go`, деплой уже использует systemd-нативные `EnvironmentFile=`+`LoadCredential=`), либо слоёная `флаги > env > файл > дефолты` (koanf/viper). Ниже нормы у нас другое: **27 переменных** (грепнуто), **ноль флагов** у демона, **нет печати эффективной конфигурации** при старте. Вопрос владельца при этом нашёл реальный дефект в этом же паке: дефолт «$/глава» лежал в двух местах — исправлено, резолвится только в `config`. 4. **Абстрактный вопрос владельца, и он подтверждается фактом (PD-115).** `ENGINEERING_STANDARDS` §2 называет внешнюю версионированную базовую линию ровно для ОДНОЙ оси — безопасности (ASVS 5.0 L2 + API Top-10 2023). Всё остальное — собственная проза зоны. Разница видна эмпирически: PD-57/PD-58 нашлись именно сверкой с RFC 9700/9207 и NIST SP 800-63B. Там, где эталона нет, сверять не с чем — грепнуто на 08.08: метрик и трейсинга **ноль**, процедуры бэкапа/восстановления в `deploy/` нет, SLO не заданы. Предложение: §2 получает по эталону на ось; ратификация — оркестратора. ## Закрытые эры P0–P3 — в архиве Разделы сессий P0–P3 и их ратификаций (04–08.08) вынесены в [archive/platform-PROGRESS-P0-P3.md](archive/platform-PROGRESS-P0-P3.md) (D39.124). Решения оттуда живут в D-логе и `DEFECT_REGISTER.md`. ## Пинги оркестратора (живые ссылки для следующих сессий) **Пинг оркестратора №15 (09.08, D39.122): движковый пак «блокеры контракта» ПРИНЯТ и заленден `0e69bc1` — пять поверхностей для платформы существуют. ФИНАЛЬНЫЕ формы (менялись трижды за приёмку — старые в переписке игнорировать):** - **Манифест глав:** `.manifest.json`, `manifest_version: "tm-manifest-v2"` (v1 движок сам отклоняет); id главы = 16 hex (стабилен через пере-нарезку); **`unit.id = ::`** — тег разреза 8 hex, при любой смене нарезки/данных пары unit.id УМИРАЮТ намеренно (id жив ⇒ якорь цел); поле `heading` — ВРЕМЕННЫЙ рендер движка «Глава N», НЕ метка книги (решение владельца 09.08; настоящие заголовки — строка 160 бэклога движка). $0-команда `tmctl manifest` строит дерево до первого прогона (первое касание создаёт БД проекта — то же поведение, что у status). - **Прогресс:** `status --json` → `progress: {draft:{done,total}, edit:{done,total}}` на книге и в каждом элементе `chapters`; `done` = «разрешено волной» (ok/flagged/skipped); волна, которой нет, — `total: 0`; процент не отгружается — собирает клиент. - **Банк:** `.bank.json` (весь банк тремя статусами; id термов — длино-префиксированный хеш ключа уникальности, стабилен через пересборку) и `.bank-stop.json` (полная таблица подписи; `conf: null` ≠ 0). Все сайдкары пишутся атомарно (temp+rename) — читать можно во время прогона. - **Потолок (строка 145): `tmctl translate|redrive --ceiling-usd ` — это КНИЖНЫЙ ПОТОЛОК В СИЛЕ, не бюджет прогона** (ратифицировано D39.122): леджер сравнивает значение с накопленным committed+reserved книги. Пересчёт пользовательского «прирост в главах» (D39.110) в абсолют — ОБЯЗАННОСТЬ платформы. **⚠ АМЕНДИРОВАНО D39.123 (PD-158): формула = `committed_usd + прирост×оценка`, БЕЗ reserved** — read-only `status` показывает leftover-reserved, который `store.Open` зануляет до первой судимой резервации; включение переплачивало бы запасом сверх холда (исполнено обеими формулами против гейта). `reserved_usd` читается обязательным полем как гард присутствия и улика leftover. Значение ниже уже потраченного откажет первой же резервации. День-потолок не перекрывается. Опция по желанию зоны: движок готов провести `--ceiling-usd` и в `status`, чтобы мониторинг видел действующий потолок capped-прогона (сегодня status показывает книжный) — скажите, заведём строку. ## Пинг оркестратора №16 — 09.08.2026 (форма DEFECT_REGISTER, строка 167) Реестр дефектов дорос до 171 строки одной таблицей; докс-аудит D39.125 (worksheet `docs/archive/reports/DOC_AUDIT_INVENTORY_2026-08-09.md`, жалоба «зонная изоляция прячет половины тем») просит форму: разложить таблицу на СЕКЦИИ (open по весу · accepted-risk · fixed по эрам паков), сохранив построчную форму `| PD-N | … |` — `docs/scripts/counts.py` ключуется формой строки, не позицией, секции его не ломают. Заведите П-строкой в свой BACKLOG, исполнение — попутно следующим паком, НЕ срочно. Вопросы — через владельца.