textmachine/platform/docs/platform-PROGRESS.md

640 lines
77 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Журнал зоны «Платформа»
> Весь прогресс платформы — ЗДЕСЬ (решение владельца 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 его цена
выросла — потолок срабатывает ровно на исчерпании холда).
- **Промт P5 ВЫДАН оркестратором 10.08** (`PLATFORM_P5_SESSION_PROMPT.md`, D39.128): П-9
`POST /books` (+PD-72 потолок тела; развилка «кто пишет book.yaml при интейке» — вопросом в этот
журнал ДО стройки той половины) · стоп/резюм-ручки PD-140 (различение «потолок vs авария» —
за эмиттером движка, строки 103/165, его промт выдан параллельно) · наблюдаемость П-11 +
печать конфигурации PD-114 · DEFECT_REGISTER секциями (пинг №16). Запуск — по слову владельца.
Эскроу/uncertain (строка 136) — следующим денежным промтом, НЕ в P5.
- Эры P0P3 (вход 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) → фикс-лист F1F8 → ре-чек 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 (0809.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/<uid>` | проба под юнитом | `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.41.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 (+1525% → $0.02520.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-<id>-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 получает по эталону на ось; ратификация — оркестратора.
## Закрытые эры P0P3 — в архиве
Разделы сессий P0P3 и их ратификаций (0408.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` — пять поверхностей для платформы существуют. ФИНАЛЬНЫЕ формы (менялись трижды за приёмку — старые в переписке игнорировать):**
- **Манифест глав:** `<project_db>.manifest.json`, `manifest_version: "tm-manifest-v2"` (v1 движок сам отклоняет); id главы = 16 hex (стабилен через пере-нарезку); **`unit.id = <chapterID>:<cutTag>:<firstChunkIdx>`** — тег разреза 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`; процент не отгружается — собирает клиент.
- **Банк:** `<project_db>.bank.json` (весь банк тремя статусами; id термов — длино-префиксированный хеш ключа уникальности, стабилен через пересборку) и `<project_db>.bank-stop.json` (полная таблица подписи; `conf: null` ≠ 0). Все сайдкары пишутся атомарно (temp+rename) — читать можно во время прогона.
- **Потолок (строка 145): `tmctl translate|redrive --ceiling-usd <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, исполнение — попутно следующим паком, НЕ срочно. Вопросы — через владельца.