Slice the P8 era and the P7 leftovers out of the zone journal after moving what only it carried into durable homes

This commit is contained in:
Claude (backend session) 2026-08-22 18:24:31 +03:00
parent ca886c0f61
commit 5a700e03b4
4 changed files with 696 additions and 689 deletions

View file

@ -173,7 +173,7 @@
| PD-51 | bug | minor | `internal/httpapi/serve.go:118-127`, `STACK_DECISIONS §12` | **Механизм заявлен неверно.** Утверждение «`ReadTimeout` убил бы и поток, поэтому стриминговый хендлер ОБЯЗАН снять read-дедлайн» на Go 1.26.5 не подтверждается: `connReader.startBackgroundRead` сам делает `SetReadDeadline(time.Time{})` (`net/http/server.go:687-698`) и для запроса без тела вызывается ДО хендлера (`:2062`). Проверено исполнением на трёх формах запроса (GET без тела · POST с непрочитанным телом · POST с вычитанным телом) — поздний кадр доезжает во всех шести комбинациях, звали `ClearReadDeadline` или нет. Следствие: `TestStreamOutlivesReadTimeout` НЕ МОЖЕТ упасть от выхолащивания `ClearReadDeadline` (проверено); он пинит только `Unwrap` (эта посадка ловится). Код безвреден, ложны обоснование и строка в таблице пинов — **закрыто, и вывод приёмки уточнён исполнением:** механизм подтверждён (`startBackgroundRead` снимает дедлайн сам, `server.go:687-698`, для запроса без остатка тела — до хендлера, `:2059-2062`; по ходу хендлера не перевзводится — проверено по всем call sites). Но «код безвреден» неверно: см. PD-63. `ClearReadDeadline` УДАЛЁН, `STACK_DECISIONS §12` переписан, `TestStreamOutlivesReadTimeout` переписан на настоящее свойство (поток переживает `Read` БЕЗ действий хендлера) и пинит `Unwrap` через ошибку `Flush` | fixed(P2, дерево сессии) | приёмка P1 (посадка M24/M42 + отдельная проба) |
| PD-52 | hardening | minor | `internal/pgstore/credits.go:233-243` | Порядок блокировок (PD-26) не запинен ни одним тестом — снятие `lockBalance` из `closeReservation` батарею переживает. Дефект воспроизведён приёмкой НЕЗАВИСИМО, в форме, которая действительно даёт цикл: конкурентные `Settle(run-1)` и повторный `Hold(run-1)`**2 взаимоблокировки на 150 раундов с инверсией, 0 с фиксом**. Регрессионный тест написан приёмкой и лежит готовым к вставке в `docs/platform-PROGRESS.md`, раздел «Ратификация приёмкой P1». ⚠ Замер сессии «41 на 300» воспроизвести не удалось — их нагрузка не описана; принимается СО СЛОВ — **закрыто:** тест приёмки вставлен как `pgstore.TestHoldAndSettleOnTheSameAttemptDoNotDeadlock`. ⚠ Замер приёмки не копировался, а ПЕРЕПРОВЕРЕН на своём стенде (PostgreSQL 18.4): с инверсией падает 5 прогонов из 5, 510 взаимоблокировок на 150 раундов; с фиксом 5 прогонов из 5 зелёные. Замер сессии P1 «41 на 300» так и не воспроизведён и остаётся СО СЛОВ | fixed(P2, дерево сессии) | приёмка P1 (посадка M07 + собственная репродукция) |
| PD-53 | hardening | info | `internal/httpapi/server.go:73-75` | `DefaultMaxBody` не запинен: поднятие лимита поддерева до 1 ГиБ батарею переживает. Пер-маршрутность лимита (PD-35) — тоже только на ревью — **закрыто:** `httpapi.TestBodyCapIsPerRouteBecauseNestingOnlyTightens` фиксирует исполнением ПРИЧИНУ пер-маршрутности — вложенный БОЛЬШИЙ лимит не поднимает внешний, — поэтому возврат общего слоя молча урезал бы аплоуд-маршрут; `TestDefaultBodyCapStaysAContractSizedNumber` держит дефолт в полосе контрактного размера (посадка «1 ГиБ» падает), не превращаясь в change-detector на точное число | fixed(P2, дерево сессии) | приёмка P1 (посадка M26) |
| PD-54 | bug | minor | `docs/platform-PROGRESS.md:329-353` | В журнале ДВЕ несовместимые формы `GET /v0/usage`: новая кредитная (строка 172) и старая подписочная (строка 329) с `resets_at`, `windows[{period}]` и хранением в `usage_windows` — таблице, которую снесла миграция `00006`. Секция P0-эры не помечена superseded, а S3 идёт читать журнал именно за формой ручки — **закрыто:** подписочное тело ответа УДАЛЕНО из журнала, а не помечено баннером: S3 идёт туда за формой ручки и скопировал бы тело. Осталась одна форма — кредитная, в разделе «Что предлагаем в спеку (S3)»; из П-5 сохранены абзацы, не зависящие от модели денег, ссылка на хранение переведена на `credit_ledger` | fixed(P2, дерево сессии) | приёмка P1 (свип доков) |
| PD-54 | bug | minor | зонный журнал, секция эры P0 (⚠ якорь на строку снят 22.08: описанное тело было УДАЛЕНО при закрытии строки, а сам журнал с тех пор дважды срезан в слайсы — адресовать было нечего) | В журнале ДВЕ несовместимые формы `GET /v0/usage`: новая кредитная (строка 172) и старая подписочная (строка 329) с `resets_at`, `windows[{period}]` и хранением в `usage_windows` — таблице, которую снесла миграция `00006`. Секция P0-эры не помечена superseded, а S3 идёт читать журнал именно за формой ручки — **закрыто:** подписочное тело ответа УДАЛЕНО из журнала, а не помечено баннером: S3 идёт туда за формой ручки и скопировал бы тело. Осталась одна форма — кредитная, в разделе «Что предлагаем в спеку (S3)»; из П-5 сохранены абзацы, не зависящие от модели денег, ссылка на хранение переведена на `credit_ledger` | fixed(P2, дерево сессии) | приёмка P1 (свип доков) |
| PD-55 | bug | info | `deploy/tmplatformd.service` | `MemoryMax=2G` объявлен как «bounds the control plane», но ограничивает cgroup ЮНИТА — а по собственному аргументу этого же файла (закрытие PD-13) в этом cgroup живёт каждый ребёнок-`tmctl`. Значит потолок общий на платформу и все идущие прогоны, и OOM-killer выберет самый жирный процесс — движок, который держит ЭКСКЛЮЗИВНЫЙ лок на файле проекта: ровно тот исход, ради которого запрещён SIGKILL. То же про `TasksMax=512`. Латентно до появления воркера. ⚠ Под systemd не проверялось (нет sudo) — вывод из семантики `MemoryMax=`, не из замера — **закрыто:** семантика сверена по man 5 systemd.resource-control («absolute limit on memory usage of the executed processes in this unit… out-of-memory killer is invoked inside the unit»). `MemoryMax=2G` заменён на `MemoryMax=80%` — потолок машины, а не сервиса, как «last line of defense» и без знания о железе; `TasksMax=512` оставлен с честным комментарием, что покрывает платформу и прогоны вместе; ограничение ОДНОГО прогона названо работой воркера (transient scope). Побочно найдено и закрыто следствие, которого в этой строке не было, — PD-64. ⚠ Под systemd не запускалось (нет sudo); `systemd-analyze verify` (systemd 259) — exit 0 | fixed(P2, дерево сессии) | приёмка P1 (ревью деплой-юнита) |
| PD-56 | bug | info | `internal/pgstore/credits.go:35-63` | `Grant`/`Adjust` на несуществующий аккаунт отдают оператору сырую ошибку Postgres с именем констрейнта (`credit_ledger_user_id_fkey`), тогда как `Balance` на том же входе отдаёт `ErrNoAccount`. Живая проба CLI. Косметика админ-поверхности, но опечатка в id читается как поломка БД — **закрыто:** `appendLedger` мапит нарушение `credit_ledger_user_id_fkey` в `ErrNoAccount`; `pgstore.TestMoneyOperationsAgreeOnAMissingAccount` требует одного ответа от `Grant`/`Adjust`/`Balance`/`ReadAccount`. Посадка «убрать сверку констрейнта» падает | fixed(P2, дерево сессии) | приёмка P1 (живая проба CLI) |
| PD-57 | hardening | minor | `internal/login/login.go:239-242` | **Защита от IdP mix-up не та, что требует действующая норма.** RFC 9700 §2.1 (OAuth Security BCP, янв. 2025) — клиент SHOULD применять параметр `iss` из авторизационного ответа (RFC 9207) либо иной контрмер НА ОСНОВЕ `iss`; MAY — различные redirect URI на провайдера. Реализована собственная сверка `st.Provider` с `h.cfg.Provider`, а внутри одного хендлера это сравнение конфигурации с самой собой: `start` пишет туда то же значение. `iss` авторизационного ответа не читается вообще (`iss` ID-токена библиотека проверяет — это другой шаг и другой момент). Сегодня не эксплуатируемо: провайдер один, код всегда редимится у него же. Заведено потому, что регистр объявляет PD-32 закрытием «класса IdP mix-up», а против нормы это неверно, и при втором провайдере выбор (`iss` или раздельные redirect URI) должен быть ОСОЗНАННЫМ, а не побочным эффектом конфигурации — **закрыто реализацией нормы, а не обещанием.** Первоисточники сверены: RFC 9700 §4.4.2 («When an OAuth client can only interact with one authorization server, a mix-up defense is not required» — то есть СЕГОДНЯ несоответствия нет, требование включается со вторым сервером), §4.4.2.2 объявляет раздельные redirect URI фолбэком («SHOULD therefore only be used if other options are not available»); альтернатива «`iss` из ID-токена» нам не подходит — при чистом code flow токен приходит уже ПОСЛЕ отдачи кода. Выбран `iss` авторизационного ответа: **Google его шлёт** (`authorization_response_iss_parameter_supported: true`, сверено живьём). Сделано: `auth_states.issuer` (миграция 00008), сверка до обмена кода, отказ на СОРВАННОМ параметре у поддерживающего провайдера (RFC 9207 §2.4). Пин — `login.TestAuthorizationResponseIssuerIsChecked` (4 случая); посадки «убрать вызов», «убрать ветку несовпадения», «убрать ветку сорванного параметра», «потерять issuer в сторе» падают | fixed(P2, дерево сессии) | приёмка P1 (сверка с RFC 9700 §2.1 / RFC 9207) |

View file

@ -248,3 +248,196 @@ python3 docs/scripts/counts.py # регистр: 256
проверки резки прозы.
5. **Плотность комментариев** (PD-255): пак вышел за норму «одна-две строки почему». Часть прозы
несущая, часть — эссе; прошу решение владельца о норме, а не тихую подрезку в следующем паке.
## ⟶ Дослано 22.08 оркестратором №18: записи акта 5, оставшиеся в живом журнале
> Заголовок живого журнала «Эра пака P7 — в архиве» утверждал, что выселено всё, а ~185 строк
> остались на месте. Досылаю их сюда без правки тел.
- **P7, акт 5 — ЗАКРЫТ. Дерево готово к приёмке и лендингу.**
Батарея: 18 пакетов, exit 0, скипов 0, линтер 0 issues, тестовых функций **532** (⚠ было «533» — испр. оркестратором №18 20.08: канонической командой D39.121 `grep -rh '^func Test' platform --include='*_test.go' | wc -l` выходит 532; расхождение на единицу, ни на что не влияет, но число было заявлено без команды). Регистр — 327
строк, открытых 65 (13 minor, 52 info), **major и BLOCKER нет**. Дерево не закоммичено и не
застейджено. Миграции пака: **0001900024**.
### Что построено и зачем
1. **Долг на материализацию — колонка с арендой** (`books.read_model_owed_at`, 00019).
Ставится в ТОЙ ЖЕ транзакции, что закрывает границу работы (конец разбора и все три конца
прогона), гасится сверкой метки, берётся в аренду тем, кто платит. **Зачем:** очередь жила в
памяти процесса — упавший `Refresh` её терял, рестарт демона терял её всю, и оплаченный текст не
доезжал до читателя НИКОГДА. Плюс «в покое» для потока теперь значит «и материализация не
должна» — это Ф-56: книга, только что нарезанная, получала `end` и 204 «не переподключайся»
ровно в те секунды, пока дерево ещё не легло.
**Почему отошёл от прежней реализации:** удалены очередь в памяти (`runs.deferRefresh`/
`DrainRefresh`/`Reader`) и бэкстоп «дерева нет вовсе» (`books.materializeMissingTrees`,
`pgstore.BooksWithNoTree`) — оба покрывались одним механизмом, а бэкстоп к тому же не видел
книгу, у которой дерево есть, но на границу старше. Дрейн переехал в `readmodel.Drain` — в
пакет, чья это работа.
2. **«Глава сделана» = последний проход, который книга на ЭТОМ деплое реально получает**
(`books.edit_wave`, 00024). **Зачем:** счёт шёл по волне `edit`, и на деплое без редактора ни
одна глава не была сделана никогда — шкала покупки не подрезалась, сервис бесконечно предлагал
купить уже переведённое. Форму объявляет сам движок (пайплайн без редактора даёт волне `edit`
знаменатель ноль). **Почему на КНИГЕ, а не на прогоне:** прочитанная с последнего прогона, она
роняла `chapters_done` в ноль в момент допуска нового прогона — счётчик ходил назад, что канон
запрещает. Флаг монотонный: книга, прошедшая редактирующий пайплайн, остаётся такой.
3. **У клейма идемпотентности появился владелец** (`claim_token`, 00020) и **тождество интейка
решается дайджестом файла** (`content_sha256`, 00023). **Зачем:** строка ключа не несла, какая
попытка её держит — застрявшая попытка удаляла строку преемника (пользователь получал вторую
книгу) или писала свою квитанцию поверх чужой (клиент реплеил `Location` несуществующей книги);
а тождество по `Content-Length` не различало две РАЗНЫЕ книги с одинаковым объявлением.
4. **Остановка АККАУНТА читается с аккаунта**, а не с потолка одного прогона; `resume` различает
«прогон закончен, нужен новый» (`ceiling_reached`) и «пополните счёт» (`credit_unavailable`);
`blocked` называет холд, который реально освободит шкалу. **Зачем:** всё это — «у вас нет
денег» пользователю, у которого деньги есть, и отправка его делать не то.
5. **Счётчик замечаний вернулся на главу** (`chapters.note_count`, 00022), `chapters.units_done`
снесён (00021). **Почему это разворот решения 00016 того же пака:** 00016 снёс колонку с
доводом «её никто не пишет» — здесь она возвращается ВМЕСТЕ с писателем, которого ей не
хватало. Основание замерено: джойн, которым её заменяли, стоил 83% времени страницы библиотеки
(16.8 мс против 3.4 мс на корпусе 40 книг × 500 глав, ВАКУУМИРОВАННОМ); со счётчиком — 6.6 мс.
⚠ Условие замера названо здесь и при каждом другом числе про него (P8-FIX, PD-336): на ХОЛОДНОМ
корпусе тот же джойн даёт 96%, и это не противоречие, а другой замер. Нормативный носитель
вакуумированных цифр — миграция 00022, остальные на неё указывают. Бенчмарк оставлен
в дереве: `go test ./internal/pgstore/ -bench LibraryPage -run xxx`.
### Что форсировано и почему
- **Работа по канону 0.4.0 лежит в дереве до его ратификации.** Основание: ответ контрактной
сессии на записку зоны плюс слово владельца 20.08 про «кончились деньги → сообщить и дать
продолжить». Ломающая правка ровно одна — `Run.stop_requested` (0.3.0 такого поля не объявляет);
`cause.code: credit_unavailable` легален и по 0.3.0, второй уровень там открыт. Зависимость
записана строкой **PD-327** и закрывается лендингом канона, а не работой зоны. Если форма
сдвинется словом владельца — правки отделимы, карта в `archive/P7_ACT5_FIX_PLAN_2026-08-20.md` §5б.
- **Тесты на АТОМАРНОСТЬ сверяют `xmin`** (транзакцию, последней писавшей строку). Это не
экзотика ради экзотики: два пина акта проходили под мутацией «вынести метку из транзакции во
второй оператор» — конечное состояние она не меняет, меняет окно, и утверждать про окно иначе
тест не может.
### Что осталось, названо и не спрятано
- **PD-327** — ждёт лендинга канона 0.4.0 (не работа зоны).
- **PD-297** — round-trip на строку в `SaveStructure` под эксклюзивной блокировкой книги. Не взят
осознанно: путь самый опасный на запись, инструмент штатный (`tx.SendBatch`), но правку надо
мерить на корпусной книге до и после.
- **Четыре оси не смотрел никто и никогда:** деньги и леджер целиком · вход/сессии/CSRF · очередь
и джобы · метрики. Пак их не менял, а все приёмки смотрели дифф. Это отдельный пак.
- **Живого перевода на стенде не было ни разу** — движку нужны ключи провайдера и деньги. Всё
после «прогон стартовал и остановился» (подпись банка, halt на потолке, резюм после пополнения)
покрыто батареей против живого Postgres, а не живым движком.
### Как это проверялось
`make check` с гейтами (скипов 0) · каждая правка проверена ПОСАДКОЙ МУТАЦИИ: код испорчен именно
так, как называет комментарий пина, пин обязан упасть, код возвращён (40 проб, все поймались) ·
живые пробы по ручкам на стенде: интейк и реплей, `409 key_reused` на другой файл под тем же
ключом, часть после файла на реплее, `/chapters` `/units` `/notes` `/bank`, ETag→304 и разные теги
у страниц, 410 на чужой id главы, `after_version` из прошлой жизни, `unsupported_pair`, `/usage`
на пустом счету и после `adjust`, старт и стоп прогона со `stop_requested` на проводе, SSE в
покое и SSE с непогашенным долгом (Ф-56 на проводе), 400 вместо 413 на длинном теле.
### Самопроверка
Акт закрыт адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера + критик полноты, 84
агента, 0 ошибок): **37 находок, 18 пережили рефутеров**, и почти все — дефекты, внесённые
правками самого акта. Строки **PD-315…PD-327**, каждая с механизмом и пином. Два из них —
собственные пины акта, проходившие под мутацией, которую сами называют (PD-318): это третье
появление класса в паке, и он назван в плане §3 п.2 ещё до того, как повторился.
- **P7, акт 5 (начало) — исполнение фиксов по ревью акта 4 + ответ контрактной сессии.**
План и рабочий список — **`platform/docs/archive/P7_ACT5_FIX_PLAN_2026-08-20.md`** (читать первым; §5а — что пришло из
контракта).
⚠ **Контрактная сессия ответила 20.08 на записку зоны `archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md`: канон стал
0.4.0** (лежит в дереве контрактной зоны, НЕ ратифицирован — до лендинга в силе 0.3.0). Все три
вопроса, висевшие на владельце, закрыты. Зоне пришла работа: `resume` исчерпанного прогона — 409 с
двумя РАЗНЫМИ причинами (`ceiling_reached` против нового `credit_unavailable`); «глава сделана» =
последний реально пройденный проход, и это ДЕНЬГИ — `ChaptersLeft` на деплое без волны редактора не
подрезается вовсе, сервис бесконечно предлагает купить уже переведённое; отпечаток интейка — по
содержимому, `Content-Length` из нормы вышел, а невозможность установить тождество обязана давать
409, а не реплей; `Run.stop_requested` обязательным булевым (единственная ломающая правка).
Отклонены: слово под суточный потолок движка (решение владельца 15.08) и предложенный зоной признак
«прогон продолжаем» (три лишних чтения на карточке, которую перечитывают на каждый кадр).
Акт 4 закрылся адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера, 75 агентов):
**33 находки, 27 пережили рефутеров**, и часть из них внесли сами правки акта 4. Тяжелейшая —
четыре независимые линзы сошлись на том, что **реконсилятор снимал стоп подписи банка, которого
никто не подписывал**, и доспавнивал движок без `--verify-bank`: складывая снятие стопа внутрь
общего `reopen`, акт 4 не проверил, что `reopen` зовёт не только `resume`, но и свип. Нарушение
согласия пользователя; починено ключом на вызывающего, пин проверен посадкой мутации.
**Первый написанный на это пин ПРОШЁЛ под мутацией** — фикстура не доходила до кода, потому что
часы сервиса захвачены при конструировании и `f.now` их не двигает. Урок в плане, §3.
Закрыто также: загрузочная проверка пар — на ДОСТУПНОЙ половине (деплой с одними `:unavailable`
больше не стартует), интейк не трактует пустой список как «пускать всё», теги сравниваются
регистронезависимо. Батарея: 18 пакетов, exit 0, скипов 0, тестов **503**.
- **P7, акт 4 — доработка 20.08: сверка ВСЕХ находок против дерева + триаж §8.2.**
(Пакет релеев `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; три вопроса владельцу §8.1а; вопросы контракта
вынесены в `platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md` для отдельной контрактной сессии.)
Все 74 позиции обеих приёмок сверены поштучно с ТЕКУЩИМ деревом — каждая с цитатой живого кода,
исход «не проверено» отдельно от «опровергнуто».
⚠ **Три места числились закрытыми и закрыты не были — правка не закрывала собственный сценарий:**
(1) подрезка буфера потока сверялась ПОСЛЕ того, как водяной знак перепрыгнул дыру, поэтому
условие не срабатывало никогда и кадры `note` терялись молча (PD-283); (2) `CompleteIdempotency`
писала квитанцию в строку, которая попытке уже не принадлежит — воспроизведено живым PG, клиент
получал квитанцию ЧУЖОГО запроса (PD-284); (3) пин на снятие `--verify-bank` был ПУСТЫМ: фикстура
стартовала прогон без `stop_for_signing` (PD-285). Все три «подтверждались» зелёной батареей.
**Ещё закрыто:** HEAD на потоке больше не держит горутину и ошибка `Flush` не выдаётся за
отправленный кадр (PD-286) · `blocked` называет чужую книгу только когда её холд укорачивает шкалу
(PD-287, пина не было вовсе) · `note_count` и `GET /notes` описывают одно множество (PD-288) ·
устаревшая доставка не рождает кадр (PD-289) · чтения на одном снапшоте (PD-290) · флаг «текст
прочитан» переехал на ПАРУ (PD-291) · снятие стопа банка внутри транзакции переоткрытия, отдельного
писателя больше нет (PD-292) · проход материализации подрезается своим бюджетом (PD-293) ·
блокировка книги в решениях банка (PD-294) · дубликат индекса снят (PD-295) · маршруты и коды
ошибок — по ОДНОЙ таблице, тесты ходят по ней (PD-296) · бэкстоп-свип для книги без дерева
(PD-276 дозакрыт) · интейк не платит за третью ре-нарезку (PD-248 актуализирован).
**Триаж §8.2:** 17 сделано, 5 отклонено с причиной, 1 релей. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 501**; каждая правка проверена посадкой мутации, не рассуждением.
**Осознанно НЕ сделано, с причиной:** PD-297 (round-trip на строку под блокировкой книги —
мерить прежде правки) · PD-202 (draft-only, вопрос владельцу) · PD-282 (канон §resumeRun не
покрывает «денег не осталось») · PD-298 (сперва сверка у движка) · PD-299.
**Осей, которых не смотрел никто:** деньги и леджер · вход/сессии/CSRF · очередь и джобы · метрики.
- **P7: приёмка → правки → приёмка правок (17.08).**
⚠ Полный хендофф под сессию без контекста — **`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`** (читать §0§3,
дальше по нужде). Там же: принципы приёмки, сверка с исходным промтом, техдолг, план следующей сессии.
Категорийное ревью по 12 осям с адверсариальной верификацией: 80 находок, **54 подтверждено, 16
правдоподобно, 10 опровергнуто**; после дедупа **46 уникальных дефектов** — 3 блокирующих класса,
15 HIGH, 17 MEDIUM. Батарея была зелёная, потому что все три блокирующих дефекта живут в путях,
которые тесты проходили только в безопасной форме.
**Починено в зоне** (регистр PD-257…PD-281): пере-нарезка книги больше не валит читающую модель
навсегда (отложенный констрейнт, миграция 00018) · упавший `tmctl export` больше не затирает текст
пустотой · весь блок идемпотентности (гонка первых попыток 500→409, область ключа, отпечаток с
размером файла, закрытие ключа вне контекста запроса) · база полосы прогресса считает тот же
проход, что числитель · SSE (подрезанные кадры → `resync_required`, дедлайн записи, id из
будущего) · материализация не голодает свип · банк читается на штатном деплое · деплой-нота больше
не включает обратно грант, который PD-104 выключил. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 490**.
**Приёмка ПРАВОК (9 осей + fable-5) нашла, что восемь дефектов внесли сами правки** — батарея не
увидела ни одного, три были запинены под ошибку автора. Все исправлены; разбор — хендофф §0.
**Б-1 ПОЧИНЕН разбором с владельцем 17.08 (PD-277).** Рамка приёмки была неверна: это не «нужен
новый канал в движок», а невыполненная половина строки единого бэклога **191(в)** (слово владельца
16.08, D39.144) — «проводка `resume = снятие стопа` до движка». Движок при выключенном
`--verify-bank` уже делает модель владельца: неподписанное едет авто-строками с пометкой
⟨проверить⟩ (`mining.go:211-231`, D39.42 п.3), а платформа передавала флаг обратно и роняла прогон
в тот же стоп. Снято: `l.VerifyBank && !l.BankReleased`; пин `TestAResumedRunIsSpawnedWithoutTheSigningStop`.
⚠ Остаток: `decline` пользователя до движка не доезжает (движок читает `mined_rejects`) — строка
бэклога **192**, отложена владельцем.
**PD-273 закрыт решением владельца 17.08:** статус остаётся `awaiting_bank`, лечение — признак
«стоп был запрошен» в контракте; готовая строка для единого бэклога — хендофф §7(и), вставляет
оркестратор (зона `docs/` не наша).
**Разбор, план, обязательства через оркестратора и команды воспроизведения —
`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`.**
⚠ Заявки раздела «Сессия P7» ниже приёмку НЕ прошли — читать их вместе с хендоффом, не вместо.
- **P7 ПОСТРОЕН — заявка САМОЙ сессии написания (17.08).****Приёмку эта заявка НЕ прошла** — см.
верхнюю запись и `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`: 46 уникальных дефектов, три блокирующих класса, из них
ни один батарея не видела. Читать ниже как исходную позицию, а не как состояние дерева.
Читающая поверхность контракта **0.3.0**:
главы · пары с текстом · замечания · банк и подпись · SSE на КНИГЕ · `GET /capabilities` ·
машинная модель ошибок (`code` + `request_id`) · условные чтения и сжатие · `Idempotency-Key` ·
`structure_version`. Плюс форварды (`title`, `reject_reason`), PD-104 (грант → 0), демонтаж
построенного гейта полноты банка (D39.144), PD-241, конформность построенного по §5 промта и
рантбук деплоя, прогнанный ЖИВЫМ `migrate`. Батарея на стенде `~/.local/pgsql` (порт 55433) под
`-race` с ОБОИМИ гейтами: **EXIT=0, 18 пакетов, скипов 0, линтер 0 issues**, тестовых функций
**416 → 458**. Дифф: 46 файлов, +2329/646. Селф-ревью финалом — ТРИ независимых ревьюера, два из
них другой модели: **22 находки, 17 подтверждены и починены** (две HIGH нашли оба контрактных
ревьюера независимо), 5 — диспозиция с аргументом. Разделы ниже — записка-план, живые пробы,
таблица селф-ревью, obstacle, вопросы оркестратору.

View file

@ -0,0 +1,472 @@
# Слайс зонного журнала: эра пака P8-FIX (2122.08.2026)
> ⚠ **Архив. Инструкции отсюда НЕ исполнять, при онбординге НЕ читать** — только по конкретной ссылке.
> Вынесено оркестратором №18 22.08 при актуализации доков. Состояние зоны — шапка живого
> `../platform-PROGRESS.md`; дефекты — `../DEFECT_REGISTER.md`; правила, пережившие пак, —
> `../STACK_DECISIONS.md`; ратификация — **D39.154** в `docs/architecture/05-decisions-log.md`.
>
> **Что этот пак сделал:** закрыл фикс-лист приёмки P7 (пункты 17 и 913) и врезанный первым
> блокер свипа `PD-169` — голодание, из-за которого расчёт денег вставал для ВСЕЙ инсталляции.
> Плюс спил пер-термного write-пути банка по слову владельца (`PD-370`, ратификация D39.144).
> Лендинг — `31f1f82`.
>
> ⚠ **Что из этого отчёта ПЕРЕНЕСЕНО в долговечные носители и там первично:**
> инвентарь каналов шва с движком → `../STACK_DECISIONS.md` §«Инвентарь каналов движка» (и там он
> ПОЛНЕЕ: дописан канал текста `tmctl export --json --pairs`, которого в этой таблице не было) ·
> развилка владения `book.yaml` с четырьмя вариантами → строка **199(а)** единого бэклога ·
> поимённый разбор канона под минор 0.5.0 → строка **206** · граница sqlc (41 запрос) → `BACKLOG.md`
> **П-19** · ловушка `.check.log` и третий гейт батареи → `PD-374`.
- **P8-FIX — ФИКС-ЛИСТ ПРИЁМКИ P7 ЗАКРЫТ. Дерево готово к приёмке и лендингу (22.08).**
Батарея: 18 пакетов, exit 0, **скипов 0**, линтер 0 issues, скипов 0, тестов **561** (HEAD 532). Регистр —
**370** строк, открытых **68** (14 minor, 53 info, **1 major**) — сверено командой на момент сдачи
(`python3 docs/scripts/counts.py --check`, exit 0; `--lint` exit 0; ⚠ вес регистра этим скриптом НЕ
проверяется — числа сняты его печатью, а не гейтом). **Открытый major — `PD-370`**, контрактная
половина спила пер-термной подписи банка: она не работа зоны, закрывать её здесь было бы подгонкой
под критерий приёмки. Миграция пака: **00025**. Дерево не закоммичено и не застейджено.
**Удалённых имён тестов ТРИ, и все три — по решению владельца 22.08** (сверено исполнением, не
памятью: имена из `git show HEAD:<файл>` по всем `_test.go` против дерева):
`TestABadDecisionRefusesTheWholeBatchAndNamesTheItem` · `TestAnUnknownTermRefusesTheBatch` ·
`TestTheBankIsReplacedWhileTheDecisionsSurvive` — они описывали пер-термную подпись банка, которую
`D39.144` отменил, а этот пак снял из кода (`PD-370`). Тест снятого кода — не потеря покрытия;
третий переписан на СОХРАНИВШЕЕСЯ свойство (`TestTheBankIsReplacedFromTheEnginesReadOut`).
Плюс один ПЕРЕИМЕНОВАН по факту изменения смысла (`…GivesTheHoldBackOnlyWhenAsked`
`…AlwaysGivesTheHoldBack`, `PD-361`) — имя заведено этим же паком.
**Пинг оркестратору №1 — ЕГО зона, сверить при лендинге:** `docs/architecture/05-decisions-log.md:542`
держит якорь на `internal/pgstore/perf_test.go:13` с токеном «96%». Пак ЭТОТ комментарий правил
(`PD-366`: он лгал про свой же бенчмарк), токен я сохранил намеренно и `--lint` зелёный — но док
чужой, поэтому говорю, а не считаю вопрос закрытым.
**Пинг оркестратору №2 — попутная находка ВНЕ зоны пака:** `PD-369` / `П-21``ClaimIdempotency`
под конкуренцией отвечает 500 (12 падения на ~80 прогонов собственного теста). Файл в диффе пака
не участвует, не трогал.
**Пинг оркестратору №3 (`PD-370`, требует контрактной сессии):** канон 0.4.0 несёт пер-термный
`BankDecision{action: approve|decline}`, а `D39.144` эту модель отменил 16.08 — подписывается весь
банк одним ОК. Зона канон исполняет, снять в одностороннем порядке не может. Правка канона →
правка зоны, в этом порядке.
**Сдача идёт ПОСЛЕ второй волны ревью**, которая нашла в самом паке блокер (`PD-353`) и два
дефекта, делавших его собственные механизмы недостижимыми (`PD-354`, `PD-355`) — раздел «Волна 2».
### Записка-план по §4 промта: пункт → где исполнено / отказ с причиной
| Пункт | Исход | Где |
|---|---|---|
| **Блокер свипа** (врезан первым) | **исполнен четырьмя механизмами** | ниже, отдельным разделом; `PD-169` пере-открыт и закрыт заново |
| 1. `ContractVersion` 0.3.0 при формах 0.4.0 | исполнен | `internal/httpapi/capabilities.go` + гейт `internal/gates/contract_test.go` (`PD-328`) |
| 2. Пины на два поля `EventBase` | исполнен, посадка сперва воспроизведена | `internal/pgstore/events_test.go` (`PD-329`) |
| 3. Бюджет попыток материализации | исполнен | `internal/readmodel/readmodel.go`, миграция 00025 (`PD-330`) |
| 4. sqlc + гейт разбора SQL | **гейт построен; sqlc — граница названа, решение владельца** | `internal/pgstore/sqlgate_test.go`; разбор — `PD-44`, строка бэклога **П-19** |
| 5. `PD-297` (round-trip под блокировкой) | **отказ с причиной** | нечем и не на чем мерить — ниже |
| 6. Труба доставки решений банка (строка 199а) | **не строил, сдаю вопросом** — как промт и требует | ниже, отдельным разделом |
| 7. Мусор: задвоенная строка доккомментария | исполнен | `internal/books/books.go` `canTranslate` (`PD-335`) |
| 8. Ревью-пак четырёх осей | **НЕ мой** — заказ пака P8-REVIEW | — |
| 9. Один носитель на факт для замера 00016→00022 | исполнен, с названным расхождением с буквой пинга | `internal/pgstore/perf_test.go` (`PD-336`) |
| 10. Пол на пустой манифест у материализатора | исполнен | `internal/ingest/manifest.go` `Whole()`, `internal/readmodel/readmodel.go` (`PD-332`) |
| 11. `PD-327` стоит open при ратифицированном каноне | исполнен | регистр, `fixed(P8-FIX)` |
| 12. Изоляция читающих чтений не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/isolation_test.go` (`PD-333`) |
| 13. `CreditHeldBy`: оговорка не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/credits_test.go` (`PD-334`) |
| `PD-255` (норма комментариев сменилась) | закрыт решением владельца 21.08 | регистр |
### Блокер свипа: почему лечение — не «поднять константу»
Диагноз пинга подтверждён построчно и **воспроизведён исполнением**. Живое свойство: проход 2 мин
против 60 с на прогон ⇒ двух медленных прогонов хватает, чтобы съесть проход целиком, после чего
`ctx.Err()` обрывает цикл и `UnsettledRuns` — единственный ретрай отложенного расчёта — не
вызывается ВООБЩЕ, для всей инсталляции, каждый проход. Построено:
1. **Проход разделён на фазы.** Реконсиляция получает половину остатка, расчёт — свой кусок
(`runs.phaseBudget`). Выбранный бюджет фазы больше не ошибка свипа: очередь просто ждёт тика.
2. **Отсрочка с растущим бэкоффом на ПОПЫТКЕ** (`run_attempts.reconcile_after`, миграция 00025).
Свип читает `RunsToReconcile`, а не `ListLiveRuns`; порядок «старейший первым» не тронут — он
верен для всего, что просто медленно, — а из ГОЛОВЫ заклиненный прогон убирает именно отсрочка.
Телеметрия (`Lag`) по-прежнему видит всё.
3. **Терминальное состояние после N неудач** (`runs.StalledAfter` = 5, ≈ четверть часа попыток):
прогон считается ЗАСТРЯВШИМ, гейдж `tm_platform_runs_stalled`, одна ERROR-строка на пересечении
порога — не на каждом тике.
4. **Ручка у ОПЕРАТОРА, которой не было вовсе:** `tmplatformctl runs [--stalled]` называет строку,
число неудач, следующую попытку, сумму холда и его возраст, юнит и последнюю ошибку;
`tmplatformctl run abandon --run <id> --reason <text> [--release-hold]` — терминальный вердикт.
Плюс `books --abandoned` и `book refresh` для второй половины (материализация).
**Границу «дальше не иду» называю явно:** автоматически решать про деньги прогона, о котором нельзя
спросить, этот пак НЕ стал. Правило реконсилятора везде — «я не смог спросить» ≠ «прогона нет»;
прогон, упавший N раз, это ровно прогон, о котором никто не смог спросить, и счётчик перед той же
ошибкой её не чинит. Автоматический ответ на этот вопрос и есть эскроу (строка 136, П-18), который
промт §4.6 из пака исключает. Что счётчик покупает — что человека ИЗВЕЩАЮТ; что покупает ручка —
что извещение чего-то стоит. Хвост записан строкой **П-20**, а не подразумевается.
Оба числа стали ручками: `TM_PLATFORM_SWEEP_BUDGET` и `TM_PLATFORM_RUN_BUDGET`. Второе —
отдельная находка пака: поле `runs.Config.RunBudget` было объявлено, задокументировано и **никем не
присваивалось** (`PD-331`), то есть ручка существовала в структуре и не делала ничего.
### Пункт 4: что построено и где проходит граница sqlc
**Построен гейт, который доходит туда, куда sqlc не дойдёт:** `TestEverySQLStatementParsesAgainstTheMigratedSchema`
сворачивает КАЖДЫЙ SQL пакета прямо из исходника (литералы, конкатенации, именованные константы,
плюс оба внутренних помощника, принимающих SQL параметром) и планирует его живым Postgres'ом
(`explain (generic_plan)`) против мигрированной схемы. **162 оператора, все планируются.**
Несворачиваемый SQL — ОШИБКА гейта, а не пропуск: единственное исключение выписано таблицей в самом
гейте (`store.go` `Ready`, где имя таблицы принадлежит goose). Тем же гейтом закрыт вопрос
фикс-листа «есть ли в read-модели запрос, которого не касается ни один тест»: теперь его касаются
все, на каждом прогоне батареи.
**Граница sqlc — мой выбор, аргумент:** склеек в пакете 25 из 147, фрагментов-констант 15, у
`lastRun` девять потребителей — read-модель для sqlc недостижима по построению, и это ровно та
часть, где рантайм-ошибки и случались. Свободных от склейки файлов целиком пять: `credits.go`(15) ·
`identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) = **41 запрос**.
Это единственный кусок, где инструмент силён; конверсия — отдельный пак (одиннадцать денежных
запросов идут внутри чужой транзакции, генерённый код коммитится, нужен пин версии и гейт
«сгенерённое актуально»). Взять его здесь на две-три ручки — ровно то, от чего предостерёг
фикс-лист: купить инструмент туда, где не болит. Заведено строкой **П-19**.
**Вопрос закрыт владельцем 22.08: sqlc берётся ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — изменений
много и мешать их с содержательной работой нельзя. Граница остаётся названной: 41 запрос.
### Пункт 5, `PD-297`: отказ с причиной
Мерить нечем и не на чем. Сама строка регистра объявляет замер на здешнем стенде НЕпредставительным
(unix-сокет против управляемого PG по TCP — разница на два порядка), а корпусной книги нет: она
появляется на холодном прогоне движка, которым гейчена строка 202 (решение владельца 20.08). Правка
самого опасного на запись пути без замера — ровно то, что эта строка запрещает. Берётся вместе с
холодным прогоном.
### Пункт 9: расхождение с буквой пинга, названное вслух
Пинг просил свести условия В миграцию 00022. **Она РЕЛИЗНАЯ**, и гейт неизменности
(`migrations.sha256` + `TestReleasedMigrationsAreUnchanged`) её правку запрещает; правка гейта ради
этого была бы подгонкой под зелень, что промт и `CLAUDE.md` запрещают прямо. Поэтому 00022 остаётся
нормативным носителем вакуумированных цифр как есть, а условия дописаны в носители, которые править
законно. «96%» не удалено и получило своё условие: это ХОЛОДНЫЙ корпус акта 4 (636 мс против 24),
тогда как 8083% — вакуумированный. Носители мерили разное и все честны.
### Строка 199(а): развилку НЕ решал — сдаю вопросом, как требует §4.2 промта
**ПОПРАВКА ВЛАДЕЛЬЦА 22.08, и она сносит посылку, на которой я строил разбор.** Я описывал трубу
как доставку ПЕР-ТЕРМНЫХ решений («approve/decline на термин»). **Такой модели нет и не было
с 16.08:** D39.144 ратифицировал ОДИН ОК над ВСЕМ банком — «пер-термная подпись = сотни кликов —
НЕ модель продукта», «в модели одного ОК сценария „какие 17 из 300 решены“ не существует».
Пер-термно существует не подпись, а **правка**: поправить/добавить термин до подписи, и — после
прочтения — поправить термин с пере-генерацией задетых глав (вторая половина гейчена строкой 192).
Труба от этого не отменяется, но её ГРУЗ другой: не «решения», а **правки терминов** (`src` → новый
`dst`, плюс добавленные) в `mined_delta`. Развилка ниже (кто объявляет ключи и кто создаёт файлы)
от этого не меняется ни в одном пункте — она про владение `book.yaml`, а не про груз.
⚠ **Модель, которую D39.144 отменил, была у зоны ПОСТРОЕНА — 22.08 СНЯТА по слову владельца**
(`PD-370`). Что ушло: маршрут `POST /books/{bookId}/bank/decisions`, хендлер и wire-типы, метод
интерфейса `Library`, `SubmitBankDecisions`, типы `BankDecision`/`BankReceipt`, `UnknownTermError`
и их пины. На месте write-пути — пометка для будущей сессии (`internal/httpapi/reading.go`, блок
«THE WRITE HALF OF THE BANK IS GONE»): что отменено, что ратификация ОСТАВИЛА пер-термным
(правку), и что под неё уже есть в схеме. ⚠ **Контрактная половина остаётся открытой** — канон
0.4.0 несёт путь, три схемы и глагол, и это зона контрактной сессии.
Посылку промта пере-проверил САМ, чтением кода движка, а не по докам. Подтверждается вся:
| Факт | Улика |
|---|---|
| движок читает `mined_delta`, только если путь объявлен | `backend/internal/pipeline/mining.go` `loadMinedDelta`: `if r.Book.MinedDelta == "" { return nil, nil }` — ни одного чтения |
| то же для `mined_rejects` | там же, `loadMinedRejects` |
| объявленный ключ с НЕсуществующим файлом валит загрузку конфига целиком | `backend/internal/config/book.go:199-207`: `bad("mined_delta %s is not readable: %v")` |
| ключей нет в шаблоне оператора | `backend/example/book.yaml` — ноль вхождений `mined_delta`/`mined_rejects` |
| платформа пишет `book.yaml` ОДИН раз и фиксированным набором из пяти ключей | `platform/internal/books/render.go``O_EXCL`, «writes the file once and never again»; ключи `book_id · title · source_lang · target_lang · source_file` |
| **новое, чего в промте не было:** декодер движка СТРОГИЙ — незнакомый ключ это жёсткая ошибка | `backend/internal/config/book.go:142-144`, `dec.KnownFields(true)` — то есть платформа не может «на всякий случай» дописать ключ, которого сборка движка не знает |
| относительные пути резолвятся от каталога КНИГИ | `backend/internal/config/book.go:148-162`, `resolve()` — значит один шаблон на все книги даёт пер-книжные пути без подстановки |
| форматы файлов | `mined_delta` = `seed.File` (`terms:` со схемой `internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed` и пере-штампуется `Source:"mined"`; `mined_rejects` = `rejects: [{src, note}]` (`mining.go` `minedRejectFile`), это ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит |
| на стороне платформы решения write-only | `internal/pgstore/readmodel.go` `SubmitBankDecisions` пишет `action`/`dst`; ни один SELECT их не читает, воркер описан комментарием собственной миграции `00002_readmodel.sql:172-174` и не построен |
**Варианты с ценой (решает владелец, не я):**
- **A. Платформа объявляет два ключа при рождении книги и сразу создаёт два пустых файла.**
Работает без единой правки движка и без действий оператора; пустой файл законен (`terms:` пуст ⇒
ноль записей, `os.Stat` проходит). **Цена:** платформа начинает знать два ключа СХЕМЫ движка, а
форма Б ратифицирована ровно наоборот (D39.110 §2б: шаблон — деплой-артефакт оператора, платформа
подставляет только личность, языки и имя файла). Плюс строгий декодер: сборка движка, которая
этих ключей ещё/уже не знает, перестанет грузить КАЖДУЮ новую книгу.
- **B. Ключи несёт шаблон оператора.** Платформа схемы не узнаёт, пути пер-книжные достаются даром.
**Цена:** сам по себе вариант НЕ работает — движок отказывается грузить конфиг с объявленным, но
отсутствующим файлом, а создавать файлы всё равно некому, кроме платформы. То есть B требует
половины A или варианта D.
- **C. Двигается ратифицированная граница владения:** платформе разрешено писать в каталог книги эти
два файла (и только их), ключи несёт шаблон. **Цена:** правка ратификации D39.110; зато платформа
по-прежнему не знает схемы — она пишет файлы по путям, которые прочитала из уже существующего
`book.yaml`.
- **D. Движок терпит объявленный, но отсутствующий `mined_delta`/`mined_rejects` как пустой.**
Одна правка `backend/internal/config/book.go`, после чего работает чистый B: платформа пишет файлы
только когда решения есть, и ничего не обязано существовать заранее. **Цена:** ослабляется
намеренный гард — опечатка оператора в пути становится тихой. Смягчается тем, что послабление
только для двух ключей и с громким WARN. Это строка ЕДИНОГО бэклога (движок), не моя.
**Рекомендация зоны: C или D+B.** Обе оставляют платформу в неведении о схеме движка — а это и есть
то свойство, ради которого форма Б ратифицировалась. A решает быстрее всех и покупает это тем, что
платформа впервые начинает знать схему чужой стороны, причём под строгим декодером.
**Строить начну только после ратификации.** Границы, если разрешат, приняты как в §4.2: писать не
«перед resume», а только на том резюме, которое СНИМАЕТ стоп подписи (`l.VerifyBank &&
!l.BankReleased`); `--verify-bank` не трогать; пост-ридинговый цикл (строка 192) не проектировать.
`PD-277` в регистре относил эту работу к отложенной 192. ⚠ **22.08 он ПЕРЕ-ДИСПОЗИРОВАН** — не по
развилке, а по спилу глагола: остаток «честность `decline`» умер вместе с ним. Ниже — прежняя
формулировка обязательства, оставленная как след решения: правлю его только после ратификации, как
промт и велит.
### Как это проверялось
**Батарея:** `cd platform && make check` с ОБОИМИ гейтами (`TM_PLATFORM_TEST_DSN` на живом
PostgreSQL 18.4, `TM_PLATFORM_TEST_ENGINE_BIN` + `_BOOK_TEMPLATE`) — 18 пакетов, EXIT=0, скипов 0,
линтер «0 issues». Дифф имён тестов — исполнением: `grep -rho '^func Test[A-Za-z0-9_]*'` по дереву
против `git show HEAD:<файл>` по всем `_test.go`. ⚠ **Замер пере-снят при сдаче** и его исход
изменился: `comm -23` даёт ТРИ удалённых имени — все три описывали пер-термную подпись банка,
снятую 22.08 по слову владельца (`PD-370`, разбор в шапке), — а `comm -13` даёт 29 новых.
Прежняя формулировка «удалённых нет» была верна до спила и снята, а не оставлена рядом с новой.
**Посадки мутаций волны 1 — 20, поймано 20** (перечень ниже; счёт пере-снят при сдаче — прежняя
цифра «16» не сходилась с собственным списком в той же фразе). Каждая ставилась в код, пин обязан был упасть, код возвращался:
снятие `payload["revision"]` ✅ · снятие `payload["structure_version"]` ✅ · `ContractVersion` назад
на 0.3.0 ✅ · `inReadTx` без обеих опций ✅ · без изоляции ✅ · без read-only ✅ · `ListBooks` на
`inTx` ✅ · снятие `and book_id <> $2` ✅ · снятие пола самоописания манифеста ✅ · снятие пола «ноль
глав» ✅ · обе фазы свипа на ОДНОМ дедлайне ✅ · ранний возврат фазы из `Sweep` ✅ · свип читает
`ListLiveRuns` вместо списка «пора» ✅ · отсрочка только по ошибке ✅ · `DeferReadModelDebt` в
`now()` ✅ · долг никогда не списывается ✅ · новая граница наследует потраченный бюджет ✅ · колонка,
которой нет в схеме, в СКЛЕЕННОМ фрагменте (гейт SQL) ✅ · то же в локальной `const q` ✅ ·
несворачиваемый SQL ✅.
**Три посадки из этого списка сперва ПЕРЕЖИЛИ** мои же пины и вскрыли настоящие дефекты пака —
разбор в `PD-337`/`PD-338`/`PD-339`; пины переписаны, мутации пере-посажены.
**Воспроизведение ПЕРЕД пином — там, где промт этого требовал (пп. 2, 12, 13):** три мутации
посажены разом в HEAD-код и прогнана ПОЛНАЯ батарея — 18 пакетов, exit 0, скипов 0. То есть все три
дыры подтверждены исполнением, а не прочитаны в пинге.
**Живые пробы — настоящий `tmctl`, живой Postgres, дев-демон на 127.0.0.1:8098:**
- `GET /v0/capabilities``"contract_version": "0.4.0"` (пункт 1 на проводе);
- `tmplatformctl seed` — аккаунт, грант, книга через ЖИВОЙ `POST /v0/books`: интейк прошёл, движок
нарезал 3 главы, `GET /chapters` отдаёт 3, долг материализации погашен, `read_model_attempts = 0`.
**Это отрицательная проверка нового пола: законный манифест живого движка он НЕ отвергает;**
- **PD-162 воспроизведён живьём** — каталог книги унесён, прогон принят (202), спавн падает.
`tmplatformctl runs` печатает строку с числом неудач `1`, временем следующей попытки, суммой холда
`0.090000`, его возрастом, `(none)` вместо юнита и текстом ошибки движка. **До этого пака оператор
видел только растущий счётчик `sweep_unfinished_total` и не мог сделать ничего;**
- `run abandon` без флага: прогон `failed`/`service_error`, деньги не тронуты, книга РАЗМОРОЖЕНА —
следующий прогон принят (202). С флагом `--release-hold`: холд вернулся целиком;
- **деньги пере-считаны из сырого леджера двумя путями и сошлись до цента:** кэш
`account_balances.balance_micro_usd` = 5000000, `sum(amount_micro_usd)` по `credit_ledger` =
5000000, открытых холдов 0; строки леджера — `grant · hold · hold_release · hold · hold_release`;
- метрики на проводе: `tm_platform_runs_stalled 0`, `tm_platform_reading_surfaces_abandoned 0`;
- обе новые ручки конфигурации печатаются на буте с источником (`TM_PLATFORM_SWEEP_BUDGET` `2m0s`
`default`, `TM_PLATFORM_RUN_BUDGET` `1m0s` `default`) — норма PD-114.
**Гейты доков:** `python3 docs/scripts/counts.py --check` — exit 0, литералы сходятся;
`--lint` — exit 0.
**Раскрываю правку ЧУЖОГО документа:** пак убил два якоря в `PLATFORM_P8_REVIEW_SESSION_PROMPT.md`
(константы `sweepBudget`/`defaultRunBudget` переехали), и `--lint` был из-за этого КРАСНЫМ. Якоря
пере-нацелены на новые места, абзац оставлен как описание НАЙДЕННОГО, а не текущего кода. Правил
чужой промт сознательно и говорю об этом прямо: якоря умерли по моей вине, а красный гейт не
обходят. Оркестратор вправе откатить эту правку — содержательного текста она не трогает.
⚠ Первая моя проверка этого гейта дала EXIT=0 и была ЛОЖНОЙ: я мерил в момент, когда файла с
якорями в дереве не было, поэтому линт его не сканировал. Критик полноты был прав, я нет.
### Спил пер-термной подписи банка (слово владельца 22.08) и что осталось контракту
**Заказ:** модель отменена `D39.144` ещё 16.08, реализации нет — спилить в зоне, оставить пометку
будущим сессиям. Перед спилом — аудит воркфлоу: 5 линз (канон · код зоны · потребители ·
ратификация · будущая пометка) + адверсариальная проверка каждого рискованного вывода, **31 агент**.
**Снято из зоны:** маршрут `POST /books/{bookId}/bank/decisions` · хендлер `bankDecisions` и
wire-типы (`wireBankDecision`/`wireBankDecisions`/`wireBankReceipt`) · метод `SubmitBankDecisions`
из интерфейса `Library` и из стора · типы `BankDecision`/`BankReceipt` · `ErrUnknownTerm` /
`UnknownTermError` и их отображение в контрактный код · пины, которые их описывали.
**НЕ тронуто, и это проверено:** чтение банка (`GET /bank`, `BankTerm`, оси статуса и происхождения)
· вокабула ДВИЖКА `proposed|in_progress|approved` в сайдкар-инжесте — она чужая и не отменялась ·
сам стоп `awaiting_bank` и подпись как `resume` (D39.144 снимала ГЕЙТ, не СТОП) · таблица
`bank_decisions` и её констрейнты: миграции релизные, а колонки `term_id`/`dst` — ровно то, что
нужно будущей ПРАВКЕ термина. Данных под снос не было: на стенде ноль строк решений.
**Пометка на месте write-пути** (`internal/httpapi/reading.go`, «THE WRITE HALF OF THE BANK IS
GONE»): что именно отменено, что ратификация ОСТАВИЛА пер-термным (правку и добавление термина),
чего нет ни на одной поверхности, и что под это уже есть в схеме и в движке (`mined_delta`).
**Гейт размера поверхности сработал**`len(contractSurface) < 15` поймал снятие маршрута. Пол
сдвинут 15 → 14 **с причиной в комментарии**: пол, опущенный под ратифицированное сокращение, — не
подгонка; опущенный без неё — подгонка. Два теста, стоявшие на снятой ручке как на «любом POST»,
пере-нацелены на живой маршрут, а не удалены.
#### Насколько страдает контракт (главный вопрос владельца)
Канон **не лжёт про модель**`D39.144` в него внесена и записана в четырёх местах верно
(`openapi.yaml:443-448`, `:539-541`, `:707`, `:2498-2499`). Беда в другом: **рядом с новой моделью
в полном рабочем обмундировании стоит старая** — путь, три схемы, четыре счётчика, — и обе зовутся
словом «signing». Файл несёт ДВЕ модели подписи.
- **Прямая ложь — шесть мест** (четыре в каноне, два в компаньоне). Худшие: `:1830-1832`
(«статус подтянется на следующей пересборке» — не подтянется, решения до движка не доезжают, что
говорит сам же канон на `:1959-1963`) и компаньон `:672``POST /bank/decisions` НЕ ПОСТРОЕНО» —
было построено в P7 и снято сегодня).
- **Самопротиворечия — три.** Самое явное: `:443` «Signing the bank is ONE act over the whole of
it» против `:477` «Signing is not a row edit» — заголовок описания ручки называет подписью САМУ
РУЧКУ. Тридцать четыре строки между ними, и оба текста компилируются в сгенерированный клиент.
Плюс `:1918-1922` («строка может быть решена и НЕ подписана, потому что отказ — тоже решение») —
дословно отменённая модель — соседствует с `:1929-1932` («INFORMATIONAL, never a gate»).
- **Дыра, которую спил вскрывает и которую надо назвать вслух:** `approve` + непустой `dst` был
ЕДИНСТВЕННЫМ живым носителем того, что D39.144 оставила пер-термным — «поправить термин». А
«добавить свой термин» модель называет, и в контракте его **нет вообще**: на 17 путей канона
запись в банк была ровно одна. То есть после спила у собственной фразы канона `:539-541`
(«correct what they care to, then release it with ONE resume») **нет ручки**.
- **Счётчики вырождаются.** `pending_decisions` считается как «строки `proposed`, на которые НЕТ
записи в `bank_decisions`»; писателя больше нет ⇒ это «сколько строк предложено», а `complete`
истинно ровно тогда, когда read-out движка не несёт ни одной `proposed` (не константа: сайдкар
несёт и `in_progress`, и `approved`). Поля обязательные на проводе (`EventBank.required`), поэтому оставлены как есть:
менять их смысл — правка канона, а не зоны.
- **Объём и вес:** ~17 мест в `openapi.yaml` (~150 строк из 2592) + ~8 в компаньоне. Вес
**ломающий минор 0.4.0 → 0.5.0**; мажора нет ни по политике («0.x ломает миноры весь бета-период»),
ни по факту. Цена фронту нулевая ДОПОЛНИТЕЛЬНО: его зеркало ратифицированно стоит на 0.2.3.
**Пока канон не поправлен, деплой обслуживает на одну ручку меньше, чем объявляет.** Это записано
строкой `PD-370` (зонная половина закрыта, контрактная открыта), и это осознанная цена спила, а не
недосмотр.
### Волна 2: воркфлоу-ревью и что оно нашло в САМОМ паке
Заказ владельца: после возврата ревьюеров волны 1 и починки их находок — воркфлоу пошире, 1030
агентов, один Fable 5 на тяжелейший участок. Исполнено: **11 линз поиска + адверсариальный рефутер
на КАЖДУЮ находку + синтез, 47 агентов, 0 ошибок**. Fable 5 — один, на свипе и деньгах.
**36 находок, 21 пережила рефутера, 15 опровергнуто.** После склейки дублей (голодовку расчётной
фазы назвали пять линз, срез по байту — две) это **17 строк регистра**: `PD-353…PD-366` закрыты,
`PD-367`/`PD-368`/`PD-369` открыты — они не мои решения и переданы как вопросы.
⚠ **Итоговая строка самого воркфлоу («ни одна находка не пережила проверку») — БАГ МОЕГО скрипта
синтеза, а не вердикт.** Она противоречила сама себе (пустыми были и «выжившие», и «убитые»), и
таблица пере-собрана из `journal.jsonl` — по одной записи на агента. Говорю прямо: доверься я
сводке, пак уехал бы на приёмку с блокером внутри.
**Блокер, найденный ПЯТЬЮ независимыми линзами (включая Fable 5) и подтверждённый мной по коду
до их вердикта — `PD-353`.** Пак разделил проход на фазы, чтобы одна не могла съесть другую, и
оставил ВНУТРИ второй фазы то же самое голодание: `UnsettledRuns` упорядочен старейшим-первым,
каждый элемент стоит целый бюджет, а `settle` при нечитаемой трате возвращает nil — то есть клин
рапортует УСПЕХ, потратив проход. Одна недоступная книга замораживала холды ВСЕХ аккаунтов, каждый
проход. Это ровно та формулировка, которую шапка `Sweep` объявляла побеждённой.
**Ещё два, которые делали механизмы пака недостижимыми:**
- `PD-354` — счётчик неудач обнулялся ТИШИНОЙ: дорогой вопрос движку задаётся раз в `ResyncEvery`,
остальные проходы ничего не устанавливают, и `ClearRunDeferral` на них сбрасывал счёт. 1,0,1,0 —
`StalledAfter` недостижим, гейдж пуст, оператору не говорят никогда.
- `PD-355` — колонка SPENT в операторской таблице показывала пожизненную трату КНИГИ. Моя же
починка G8 волны 1 взяла не то число, на единственной таблице, заведённой ради решения о деньгах.
**Чему меня научили РЕФУТЕРЫ (15 опровержений, каждое куплено исполнением, а не рассуждением):**
- «Поднять `RunBudget` = объявить здоровый прогон застрявшим» — **опровергнуто замером**: поднятие
ручки не меняет вообще ничего, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами
шиппящиеся дефолты. Я уже держал эту «починку» в плане и не построил её. Остаток — `PD-368`.
- «Интейк обязан применить `Whole()`» — **опровергнуто контрактом и батареей**: вся батарея интейка
ездит на манифестах без списка глав, контракт интейка — только счётчики. Механика находки верна
(`PD-367` открыт), лекарство — нет.
- «Пропуск тела помощника в гейте SQL = дыра» — **опровергнуто исполнением**: мутация в теле `bump`
краснит десятки поведенческих тестов; спрятать там непроверяемый оператор нельзя.
- «Стойл-детектор слеп к форме PD-162» (находка Fable 5) — опровергнуто.
**Две линзы разошлись во ВЕСЕ одной находки** (`PD-358`, срез по байту в операторской таблице):
одна назвала микрополировкой вне рамки, вторая довела цепочку до многобайтового stderr движка.
Починил как дешёвое и бесспорное, расхождение раскрыл в строке.
### Самопроверка (артефакт §5: имена агентов и по одной команде или цитате из каждого)
Исходов три, «не проверено» ≠ «опровергнуто». Каждому агенту выдан СВОЙ каталог песочницы явным
путём, мандат читающий, state-changing git запрещён перечислением (D39.113). В рабочее дерево не
писал никто: оба ревьюера волны 1 ответили на прямой вопрос уликами.
**Волна 1 — три изолированных ревьюера по финальному диффу (author≠reviewer):**
| Агент | Ось | Цитата или команда ИЗ ЕГО отчёта | Находок | Исход |
|---|---|---|---|---|
| `agent-afec473415b228cdf` | контракт-конформность против канона 0.4.0 + свои посадки | «в песочнице создан симлинк `<scratch>/docs -> …/docs` — новый гейт `internal/gates/contract_test.go` тянется за канон ЗА пределы модуля, и без него копия дерева его не находит» | 5 + 3 пережившие мутации | все 8 починены |
| `agent-a394edded388676bc` | шов с движком и деньги, свой инвентарь каналов | «`repo*/platform/cmd/rev2probe\|rev2ctl\|rev2seam\|rev2ceiling\|rev2ids/main.go` и `repo*/platform/internal/runs/rev2_starve_test.go` — мои пробы (только в копиях)» | 8 + 3 пережившие мутации | все 11 починены; G1 — БЛОКЕР в моём коде |
| `agent-a1e4407d89e52128d` | полнота сдачи: пункт заказа → носитель в дереве | «Snapshot taken at 2026-08-22 00:49:24. ⚠ The tree was moving while I read it: `platform/BACKLOG.md` gained rows П-19/П-20 at 00:48, mid-review» | 6 | 5 починены, 1 (К-4) вскрыла МОЮ ошибку измерения |
Две разведки (read-only, не ревью): `agent-acf370d564663617e` — инвентарь SQL для границы sqlc;
`agent-a1cb3d1a5c59f2f2e` — инвентарь каналов движка.
**Волна 2 — воркфлоу `wf_f8d1f3e6-85a`, 11 линз (одна на Fable 5) + рефутер на каждую находку:**
| Линза (агент) | Предмет | Цитата или команда ИЗ ЕГО отчёта | Находок | Пережило |
|---|---|---|---|---|
| `a2fdcd13144058b57` **Fable 5** | свип и деньги — тяжелейший участок | «Стойл-детектор слеп к клину „юнит исчез, workdir недоступен“ (форма PD-162): реконсиляция каждый проход рапортует успех» | 3 | 2 |
| `ae8f856fe08ee9e61` | фазы и отсрочка свипа | «Счётчик отсрочек не может дорасти до StalledAfter для того самого клина, ради которого написан» | 4 | 3 |
| `a3c4dc82a7d62f8a6` | архитектурный стиль, велосипеды | «Фаза расчёта денег осталась без отсрочки: head-of-line starvation, от которой пак и построен, на списке `UnsettledRuns` не починена» | 3 | 1 |
| `a62786a00eb5bbae4` | каноны Go и каноны зоны | «RunBudget молча обрезан половиной SweepBudget» (опровергнуто) + голодовка расчётной фазы | 4 | 2 |
| `af8620a80b7e12b06` | материализация и пол манифеста | «Провалившийся экспорт на смене кадра затирает весь оплаченный текст книги» | 3 | 3 |
| `ada2420ac7c576856` | CLI, метрики, конфиг | «SPENT в операторской таблице — пожизненный счётчик КНИГИ, а не траты этой попытки» | 3 | 3 |
| `ac92e0a8acba148ec` | сам гейт SQL: даёт ли ложную зелень | «`unresolvable` ключёвана функцией, а не сайтом: второй несворачиваемый запрос в той же функции молча проверяется текстом первого» | 2 | 1 |
| `a21f43459f3f12f7e` | комментарии: вода против несущего | «perf_test.go копирует „нормативные“ цифры из миграции 00022 и в той же фразе утверждает, что не копирует» | 4 | 1 |
| `a8faf63f1dde50903` | скоуп: сверка построенного против БУКВЫ промта | «Пункт 9: новое условие замера в perf_test.go ложно для бенчмарка этого же файла» | 4 | 2 |
| `a4dad647c0eb8310f` | регрессии: что пак сломал в уже работавшем | «Отмена контекста сообщается как дедлайн: остановка демона посреди прохода поднимает счётчик голодания» | 3 | 2 |
| `a178aca863af19915` | контракт-конформность построенного против канона 0.4.0 | «Пункт 1: подъём константы оставил в коде утверждение, что деплой объявляет 0.3.0» (опровергнуто) | 3 | 1 |
Рефутеры (по одному на находку, 36 штук) считаются наравне с находками — три примера того, чем
куплены их вердикты:
- `ad12d9bcca05b883a`: «`WITH the index n_tup_upd=3942 n_tup_hot_upd=3909 (HOT 99.2%) idx size=8192`
против `WITHOUT the index n_tup_upd=1994 n_tup_hot_upd=1984 (HOT 99.5%)`» — из этого замера
выросла `PD-364` (индекс удалён);
- `a337318688be20d30`: «`finished_at = now()` → `books.go:313: column "finished_at" of relation
"books" does not exist (42703)` … на всех 5 сайтах»;
- `a7fadd3c541e9a87d`: «пять проходов подряд дают „the sweep settled 2 of 3 finished runs: context
deadline exceeded“, третий, чистый прогон не достигается никогда, reserved держится на $0.90» —
это подтверждение блокера `PD-353` исполнением.
**Посадки мутаций волны 2 — 6, поймано 6:** фаза расчёта не отсрочивает ✅ · `UnsettledRuns`
игнорирует отсрочку ✅ · тишина снова считается здоровьем ✅ · `RunBudget` снова не присваивается ✅ ·
срез по байту в операторской таблице ✅ · управляющие символы не вычищаются ✅.
⚠ Две первые попытки посадить последние две дали НЕ падение теста, а ошибку сборки (осиротевший
импорт) — это слабее, и они пере-посажены компилирующимися.
### Релей §4.7: не приходил
Аддендум пака P8-REVIEW должен был доехать **только релеем оркестратора с эхо-подтверждением**.
За сессию релея не поступало: ни находок по четырём осям, ни эхо. Отчёт читающего пака я не читал
и не гадал по нему — промт это прямо запрещает, состав пака определяет оркестратор.
Единственное касание того пака — техническое и раскрыто выше: два его якоря умерли от МОИХ правок
кода, `--lint` покраснел, якоря пере-нацелены (`docs/PLATFORM_P8_REVIEW_SESSION_PROMPT.md:62`,
плюс третье пере-нацеливание 22.08 после спила). Содержательного текста чужого промта не трогал.
### Obstacle: что не удалось и что не проверено
- **Сквозного живого перевода через платформу не было** и в этом паке быть не могло: строка 202
гейчена холодным прогоном движка (решение владельца 20.08). Всё после «прогон стартовал и
остановился» доказано батареей против живого Postgres и настоящего `tmctl`, а не переводом.
- **Труба 199(а) не построена и форматы сверены ЧТЕНИЕМ**, а не записью файла: `mined_delta` и
`mined_rejects` прочитаны в коде движка (`membank.LoadGlossarySeed`, `mining.go`), но ни одного
файла платформа не писала — писать было бы строительством нератифицированной развилки.
- **Замер `PD-297` не снят.** Отказ осознанный: путь самый опасный на запись, править его надо с
замером на корпусной книге до и после, а корпусной книги в паке не было.
- **Финальный дифф ПОСМОТРЕН** независимым ревьюером (author≠reviewer) уже после спила — он и
закрыл эту строку obstacle. Три его major были настоящими дырами в МОЁМ коде, и все три —
отсутствие пина, а не ошибка логики: пол `ErrTextUnknownForANewCut` (охраняет единственную
запись, способную стереть оплаченный текст) · «стоп перевешивает отсрочку» в `RunsToReconcile` ·
«пересборка банка не трогает `bank_decisions`» (свойство, на которое опирается пометка, ради
которой спил и делался). Каждая закрыта пином, каждый пин проверен посадкой мутации. Плюс семь
минорных: комментарии `SaveBank` и `bankCountsTx` описывали отменённую модель как живую,
пометка врала про `action NOT NULL`, снятие NUL не было запинено. Не найдено дефектов там, где
он искал отдельно: пол поверхности 15→14 равен реальному размеру таблицы, пере-нацеленные тесты
проверяют то же свойство, лишнего не срезано.
- **Число скипов батареи** снято рецептом `make check`, который пишет `.check.log` в дерево; при
параллельных прогонах агентов лог обрезался. Отдельная сверка двух пакетов с условными скипами
(`internal/runner`, `internal/gates`) дала `--- SKIP` = 0.
- **Контрактная половина `PD-370` открыта**: пока канон не поправлен, деплой обслуживает на одну
ручку меньше, чем объявляет. Это цена спила, названная заранее, а не обнаруженная при приёмке.
### Инвентарь каналов движка (артефакт §4.4 — собран ЧТЕНИЕМ кода движка, не по нашим докам)
Пак коснулся канала движка (пункт 10: `ingest.Manifest.Whole()` построен как зеркало движкового
правила), поэтому инвентарь сдаётся таблицей, а не словами «я прочитал код движка».
| Канал | Писатель в движке | Атомарность | Читатель/писатель платформы | Согласовано? |
|---|---|---|---|---|
| `<project_db>.manifest.json` | `pipeline/manifest.go:379` `writeFileAtomic` | **атомарно** (temp+`Sync`+rename, `pipeline/artifact.go:24-59`) | `internal/runner/engine.go` (`tmctl manifest`) → `ingest.DecodeManifest``internal/books/parse.go`, `internal/readmodel` | ✅ **и стало строже**: `Manifest.Whole()` теперь зеркалит `BookManifest.selfConsistent` (`pipeline/manifest.go:460-483`) поле в поле по тем правилам, которые платформа вообще читает (главы, юниты, нумерация). Верхняя граница чанков движка у нас counterpart'а не имеет намеренно — платформа счётчиков чанков не берёт |
| `<project_db>.bank.json` | `pipeline/bankexport.go:154` `writeFileAtomic` | **атомарно** | `internal/runner/artifacts.go:37``ingest.DecodeBank``pgstore.SaveBank` | ✅ |
| `<project_db>.bank-stop.json` | `pipeline/mining.go:393` `writeFileAtomic` | **атомарно** | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ канал существует и не потребляется; полная таблица подписи платформе сегодня не нужна, она проецирует банк |
| `<project_db>.bank-stop.txt` | `pipeline/mining.go:195` `os.WriteFile` | **НЕ атомарно** (усечение первым делом) | читателя нет | ✅ и это важно: брать его на живом прогоне нельзя — исправление пинга №15 оркестратором подтверждается кодом |
| `<project_db>.mined-signature.yaml` | `pipeline/mining.go:188` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `<project_db>.auto-bank.yaml` | `pipeline/mining.go:741` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `mined_delta` (путь из `book.yaml`) | **писателя в движке НЕТ** — только читатель `pipeline/mining.go` `loadMinedDelta`; формат `seed.File` (`internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed`, `Source` пере-штампуется на `"mined"` | — | **писателя НЕТ и у платформы** | ❌ **разрыв — это и есть строка 199(а)** |
| `mined_rejects` | читатель `pipeline/mining.go` `loadMinedRejects`; формат `rejects: [{src, note}]`; ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит | — | писателя нет | ❌ тот же разрыв |
| `events.jsonl` (NDJSON эмиттера) | движок, StreamVersion 1.1 | append-only | `internal/ingest/tail.go` + `pgstore.RunSink` | ✅ пак не трогал |
| exit-коды `tmctl` | контракт движка | — | `ingest.OutcomeOf`, `internal/runs/reconcile.go` `outcome` | ✅ пак не трогал |
| `tmctl status --json` | движок | — | `internal/runner/engine.go``ingest.StatusReport`; расчёт денег | ✅ пак не менял форму; изменил лишь то, СКОЛЬКО раз его зовут при неудаче (бэкофф) |
| `book.yaml` | оператор (шаблон) + **платформа один раз** (`internal/books/render.go`, `O_EXCL`) | — | платформа ПИШЕТ пять фиксированных ключей | ⚠ **декодер движка СТРОГИЙ** (`config/book.go:142-144`, `KnownFields(true)`): незнакомый ключ = жёсткая ошибка. Значит любое расширение набора ключей платформой — ратификация, а не правка. Прямо относится к развилке 199(а) выше |

View file

@ -6,694 +6,36 @@
## Текущее состояние
- **P8-FIX ПРИНЯТ И ЗАЛЕНДЕН оркестратором №18 (22.08).** Приёмка велась слепой линзой ДО чтения
отчёта: сперва пере-прогон, счёт тестов и собственные посадки, и только потом сверка с запиской
зоны. **Что сверено ИСПОЛНЕНИЕМ, а не принято на слово:**
- **Батарея на моём стенде:** 18 пакетов, exit 0, линтер «0 issues» — сходится. ⚠ **«Скипов 0» НЕ
воспроизведён: у меня три.** Это не регрессия пака и не ошибка отчёта — `systemdOrSkip` гейтит
три теста `internal/runner` достижимостью пользовательского менеджера systemd, а на моём хосте
`/run/user/1000` не существует. Отчёт мерил на хосте с живым менеджером и там 0. Незакрытым
остаётся то, что рецепт объявляет у батареи ДВА гейта, а их три — `PD-374`.
- **Тесты 561 против 532 на HEAD** — сходится, сверено `git show HEAD:<файл>` по всем `_test.go`
против дерева. Удалённых имён ровно **три**, все три описывали снятую пер-термную подпись.
- **Отпечатки миграций:** пере-считаны все 25, расхождений нет (⚠ испр. 22.08 аудитом доков: первая редакция этой строки писала «41» — число взято из головы, а не из команды; `grep -c '^[0-9a-f]\{64\}' migrations.sha256` = 25, файлов миграций тоже 25); в диффе `migrations.sha256` ровно
одна ДОБАВЛЕННАЯ строка — заявление «релизные миграции не тронуты» держится механически.
- **Пол поверхности 15 → 14** равен реальной длине таблицы маршрутов (14), то есть запаса не
оставлено и следующее сужение гейт поймает. Причина стоит в комментарии рядом.
- **Гейты доков:** `--check` exit 0 · `--lint` exit 0. **Пинг №1 зоны закрыт:** якорь
`05-decisions-log.md:542``perf_test.go:13` жив, токен «96% of the page» на месте. Правку
ЧУЖОГО промта (пере-нацеливание двух моих якорей после переезда констант) **утверждаю и не
откатываю**: якоря умерли по вине пака, пак их починил и раскрыл это сам — это ровно то
поведение, которого норма и требует.
- **Деньги — двумя путями из СЫРОГО леджера, своей живой пробой** (свой дев-демон на дереве пака,
своя БД): до `run abandon --release-hold` кэш `account_balances` = 24910000 и `sum(credit_ledger)`
= 24910000 при одном открытом холде 90000; после — 25000000 и 25000000, открытых холдов 0, строки
`grant · hold · hold_release`. Холд вернулся целиком, книга разморозилась (следующий прогон 202).
- **Вся цепь PD-169 воспроизведена мной живьём, а не по отчёту:** отсрочка считает неудачи между
проходами (`FAILS 6`), переходит `StalledAfter` → гейдж `tm_platform_runs_stalled 1` на проводе →
`tmplatformctl runs --stalled` называет прогон, счёт, что держит и НАСТОЯЩУЮ ошибку движка. Обе
новые ручки печатаются на буте с источником (`SWEEP_BUDGET 2m0s default`, `RUN_BUDGET 1m0s default`).
- **Одиннадцать собственных посадок мутаций вне списка автора.** Убиты восемь: разделение фазы ·
«бюджет выбран = отказ» · очистка счёта тишиной · бэкофф · пропуск фазы расчёта на исчерпанном
проходе · версия контракта · пол идентичностей манифеста · «стоп перевешивает отсрочку». ⚠ Две
первых редакции моих посадок дали ЛОЖНЫЕ «выжила» — я гонял не тот пакет (пин лежал в
`internal/runs`, а мутация была в `internal/pgstore`); пере-прогнано, обе убиты. Говорю об этом,
потому что необъявленная ошибка харнесса приёмки — это находка, которой нет.
- **Выжили три, и они разные.** `StalledAfter` — законно: носитель один, пин ездит на константе.
`maxAttempts`**`PD-373`**: второй носитель есть в рантбуке прозой. `truncateReason` в
`DeferRun`**`PD-372`**: пин ездит на самой функции, ни один из четырёх вызовов не проверен.
- **Находка вне карты пака — `PD-371`:** исключение «стоп перевешивает отсрочку» не имеет границы,
и это видно живьём: `NEXT TRY` стоял на 30 минут вперёд, а счётчик рос каждые 15 секунд.
**Регистр после приёмки: 374 строки, открытых 72 (1 major, 16 minor, 55 info)** — своих строк
четыре (`PD-371``PD-374`); зонные числа сдачи (370/68) остаются верны на момент сдачи и не
переписываются. **Открытым уезжает `PD-370`** (контрактная половина) — это НЕ работа зоны, и
закрывать её здесь было бы подгонкой под критерий приёмки; согласен с диспозицией.
**Порядок паков вышел ОБРАТНЫМ моей очереди, и это меняет рамку следующего:** промт P8-FIX
строкой 3 требует запуска «ПОСЛЕ пака P8-REVIEW», запущен он был раньше. Следствие в плюс: цена,
которую я назвал вслух при перестановке — «блокер свипа живёт всё время читающего пака», — НЕ
заплачена, блокер закрыт первым. Следствие в минус: §4.7 (аддендум релеем) в этом паке пустой и
честно назван таковым, а читающий пак теперь пойдёт по коду, который этот пак только что переписал —
его промт по оси 3 зона уже пере-нацелила на новые места, остальные три оси не затронуты.
- **P8-FIX — ФИКС-ЛИСТ ПРИЁМКИ P7 ЗАКРЫТ. Дерево готово к приёмке и лендингу (22.08).**
Батарея: 18 пакетов, exit 0, **скипов 0**, линтер 0 issues, скипов 0, тестов **561** (HEAD 532). Регистр —
**370** строк, открытых **68** (14 minor, 53 info, **1 major**) — сверено командой на момент сдачи
(`python3 docs/scripts/counts.py --check`, exit 0; `--lint` exit 0; ⚠ вес регистра этим скриптом НЕ
проверяется — числа сняты его печатью, а не гейтом). **Открытый major — `PD-370`**, контрактная
половина спила пер-термной подписи банка: она не работа зоны, закрывать её здесь было бы подгонкой
под критерий приёмки. Миграция пака: **00025**. Дерево не закоммичено и не застейджено.
**Удалённых имён тестов ТРИ, и все три — по решению владельца 22.08** (сверено исполнением, не
памятью: имена из `git show HEAD:<файл>` по всем `_test.go` против дерева):
`TestABadDecisionRefusesTheWholeBatchAndNamesTheItem` · `TestAnUnknownTermRefusesTheBatch` ·
`TestTheBankIsReplacedWhileTheDecisionsSurvive` — они описывали пер-термную подпись банка, которую
`D39.144` отменил, а этот пак снял из кода (`PD-370`). Тест снятого кода — не потеря покрытия;
третий переписан на СОХРАНИВШЕЕСЯ свойство (`TestTheBankIsReplacedFromTheEnginesReadOut`).
Плюс один ПЕРЕИМЕНОВАН по факту изменения смысла (`…GivesTheHoldBackOnlyWhenAsked`
`…AlwaysGivesTheHoldBack`, `PD-361`) — имя заведено этим же паком.
**Пинг оркестратору №1 — ЕГО зона, сверить при лендинге:** `docs/architecture/05-decisions-log.md:542`
держит якорь на `internal/pgstore/perf_test.go:13` с токеном «96%». Пак ЭТОТ комментарий правил
(`PD-366`: он лгал про свой же бенчмарк), токен я сохранил намеренно и `--lint` зелёный — но док
чужой, поэтому говорю, а не считаю вопрос закрытым.
**Пинг оркестратору №2 — попутная находка ВНЕ зоны пака:** `PD-369` / `П-21``ClaimIdempotency`
под конкуренцией отвечает 500 (12 падения на ~80 прогонов собственного теста). Файл в диффе пака
не участвует, не трогал.
**Пинг оркестратору №3 (`PD-370`, требует контрактной сессии):** канон 0.4.0 несёт пер-термный
`BankDecision{action: approve|decline}`, а `D39.144` эту модель отменил 16.08 — подписывается весь
банк одним ОК. Зона канон исполняет, снять в одностороннем порядке не может. Правка канона →
правка зоны, в этом порядке.
**Сдача идёт ПОСЛЕ второй волны ревью**, которая нашла в самом паке блокер (`PD-353`) и два
дефекта, делавших его собственные механизмы недостижимыми (`PD-354`, `PD-355`) — раздел «Волна 2».
### Записка-план по §4 промта: пункт → где исполнено / отказ с причиной
| Пункт | Исход | Где |
|---|---|---|
| **Блокер свипа** (врезан первым) | **исполнен четырьмя механизмами** | ниже, отдельным разделом; `PD-169` пере-открыт и закрыт заново |
| 1. `ContractVersion` 0.3.0 при формах 0.4.0 | исполнен | `internal/httpapi/capabilities.go` + гейт `internal/gates/contract_test.go` (`PD-328`) |
| 2. Пины на два поля `EventBase` | исполнен, посадка сперва воспроизведена | `internal/pgstore/events_test.go` (`PD-329`) |
| 3. Бюджет попыток материализации | исполнен | `internal/readmodel/readmodel.go`, миграция 00025 (`PD-330`) |
| 4. sqlc + гейт разбора SQL | **гейт построен; sqlc — граница названа, решение владельца** | `internal/pgstore/sqlgate_test.go`; разбор — `PD-44`, строка бэклога **П-19** |
| 5. `PD-297` (round-trip под блокировкой) | **отказ с причиной** | нечем и не на чем мерить — ниже |
| 6. Труба доставки решений банка (строка 199а) | **не строил, сдаю вопросом** — как промт и требует | ниже, отдельным разделом |
| 7. Мусор: задвоенная строка доккомментария | исполнен | `internal/books/books.go` `canTranslate` (`PD-335`) |
| 8. Ревью-пак четырёх осей | **НЕ мой** — заказ пака P8-REVIEW | — |
| 9. Один носитель на факт для замера 00016→00022 | исполнен, с названным расхождением с буквой пинга | `internal/pgstore/perf_test.go` (`PD-336`) |
| 10. Пол на пустой манифест у материализатора | исполнен | `internal/ingest/manifest.go` `Whole()`, `internal/readmodel/readmodel.go` (`PD-332`) |
| 11. `PD-327` стоит open при ратифицированном каноне | исполнен | регистр, `fixed(P8-FIX)` |
| 12. Изоляция читающих чтений не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/isolation_test.go` (`PD-333`) |
| 13. `CreditHeldBy`: оговорка не запинена | исполнен, посадка сперва воспроизведена | `internal/pgstore/credits_test.go` (`PD-334`) |
| `PD-255` (норма комментариев сменилась) | закрыт решением владельца 21.08 | регистр |
### Блокер свипа: почему лечение — не «поднять константу»
Диагноз пинга подтверждён построчно и **воспроизведён исполнением**. Живое свойство: проход 2 мин
против 60 с на прогон ⇒ двух медленных прогонов хватает, чтобы съесть проход целиком, после чего
`ctx.Err()` обрывает цикл и `UnsettledRuns` — единственный ретрай отложенного расчёта — не
вызывается ВООБЩЕ, для всей инсталляции, каждый проход. Построено:
1. **Проход разделён на фазы.** Реконсиляция получает половину остатка, расчёт — свой кусок
(`runs.phaseBudget`). Выбранный бюджет фазы больше не ошибка свипа: очередь просто ждёт тика.
2. **Отсрочка с растущим бэкоффом на ПОПЫТКЕ** (`run_attempts.reconcile_after`, миграция 00025).
Свип читает `RunsToReconcile`, а не `ListLiveRuns`; порядок «старейший первым» не тронут — он
верен для всего, что просто медленно, — а из ГОЛОВЫ заклиненный прогон убирает именно отсрочка.
Телеметрия (`Lag`) по-прежнему видит всё.
3. **Терминальное состояние после N неудач** (`runs.StalledAfter` = 5, ≈ четверть часа попыток):
прогон считается ЗАСТРЯВШИМ, гейдж `tm_platform_runs_stalled`, одна ERROR-строка на пересечении
порога — не на каждом тике.
4. **Ручка у ОПЕРАТОРА, которой не было вовсе:** `tmplatformctl runs [--stalled]` называет строку,
число неудач, следующую попытку, сумму холда и его возраст, юнит и последнюю ошибку;
`tmplatformctl run abandon --run <id> --reason <text> [--release-hold]` — терминальный вердикт.
Плюс `books --abandoned` и `book refresh` для второй половины (материализация).
**Границу «дальше не иду» называю явно:** автоматически решать про деньги прогона, о котором нельзя
спросить, этот пак НЕ стал. Правило реконсилятора везде — «я не смог спросить» ≠ «прогона нет»;
прогон, упавший N раз, это ровно прогон, о котором никто не смог спросить, и счётчик перед той же
ошибкой её не чинит. Автоматический ответ на этот вопрос и есть эскроу (строка 136, П-18), который
промт §4.6 из пака исключает. Что счётчик покупает — что человека ИЗВЕЩАЮТ; что покупает ручка —
что извещение чего-то стоит. Хвост записан строкой **П-20**, а не подразумевается.
Оба числа стали ручками: `TM_PLATFORM_SWEEP_BUDGET` и `TM_PLATFORM_RUN_BUDGET`. Второе —
отдельная находка пака: поле `runs.Config.RunBudget` было объявлено, задокументировано и **никем не
присваивалось** (`PD-331`), то есть ручка существовала в структуре и не делала ничего.
### Пункт 4: что построено и где проходит граница sqlc
**Построен гейт, который доходит туда, куда sqlc не дойдёт:** `TestEverySQLStatementParsesAgainstTheMigratedSchema`
сворачивает КАЖДЫЙ SQL пакета прямо из исходника (литералы, конкатенации, именованные константы,
плюс оба внутренних помощника, принимающих SQL параметром) и планирует его живым Postgres'ом
(`explain (generic_plan)`) против мигрированной схемы. **162 оператора, все планируются.**
Несворачиваемый SQL — ОШИБКА гейта, а не пропуск: единственное исключение выписано таблицей в самом
гейте (`store.go` `Ready`, где имя таблицы принадлежит goose). Тем же гейтом закрыт вопрос
фикс-листа «есть ли в read-модели запрос, которого не касается ни один тест»: теперь его касаются
все, на каждом прогоне батареи.
**Граница sqlc — мой выбор, аргумент:** склеек в пакете 25 из 147, фрагментов-констант 15, у
`lastRun` девять потребителей — read-модель для sqlc недостижима по построению, и это ровно та
часть, где рантайм-ошибки и случались. Свободных от склейки файлов целиком пять: `credits.go`(15) ·
`identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) = **41 запрос**.
Это единственный кусок, где инструмент силён; конверсия — отдельный пак (одиннадцать денежных
запросов идут внутри чужой транзакции, генерённый код коммитится, нужен пин версии и гейт
«сгенерённое актуально»). Взять его здесь на две-три ручки — ровно то, от чего предостерёг
фикс-лист: купить инструмент туда, где не болит. Заведено строкой **П-19**.
**Вопрос закрыт владельцем 22.08: sqlc берётся ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — изменений
много и мешать их с содержательной работой нельзя. Граница остаётся названной: 41 запрос.
### Пункт 5, `PD-297`: отказ с причиной
Мерить нечем и не на чем. Сама строка регистра объявляет замер на здешнем стенде НЕпредставительным
(unix-сокет против управляемого PG по TCP — разница на два порядка), а корпусной книги нет: она
появляется на холодном прогоне движка, которым гейчена строка 202 (решение владельца 20.08). Правка
самого опасного на запись пути без замера — ровно то, что эта строка запрещает. Берётся вместе с
холодным прогоном.
### Пункт 9: расхождение с буквой пинга, названное вслух
Пинг просил свести условия В миграцию 00022. **Она РЕЛИЗНАЯ**, и гейт неизменности
(`migrations.sha256` + `TestReleasedMigrationsAreUnchanged`) её правку запрещает; правка гейта ради
этого была бы подгонкой под зелень, что промт и `CLAUDE.md` запрещают прямо. Поэтому 00022 остаётся
нормативным носителем вакуумированных цифр как есть, а условия дописаны в носители, которые править
законно. «96%» не удалено и получило своё условие: это ХОЛОДНЫЙ корпус акта 4 (636 мс против 24),
тогда как 8083% — вакуумированный. Носители мерили разное и все честны.
### Строка 199(а): развилку НЕ решал — сдаю вопросом, как требует §4.2 промта
**ПОПРАВКА ВЛАДЕЛЬЦА 22.08, и она сносит посылку, на которой я строил разбор.** Я описывал трубу
как доставку ПЕР-ТЕРМНЫХ решений («approve/decline на термин»). **Такой модели нет и не было
с 16.08:** D39.144 ратифицировал ОДИН ОК над ВСЕМ банком — «пер-термная подпись = сотни кликов —
НЕ модель продукта», «в модели одного ОК сценария „какие 17 из 300 решены“ не существует».
Пер-термно существует не подпись, а **правка**: поправить/добавить термин до подписи, и — после
прочтения — поправить термин с пере-генерацией задетых глав (вторая половина гейчена строкой 192).
Труба от этого не отменяется, но её ГРУЗ другой: не «решения», а **правки терминов** (`src` → новый
`dst`, плюс добавленные) в `mined_delta`. Развилка ниже (кто объявляет ключи и кто создаёт файлы)
от этого не меняется ни в одном пункте — она про владение `book.yaml`, а не про груз.
⚠ **Модель, которую D39.144 отменил, была у зоны ПОСТРОЕНА — 22.08 СНЯТА по слову владельца**
(`PD-370`). Что ушло: маршрут `POST /books/{bookId}/bank/decisions`, хендлер и wire-типы, метод
интерфейса `Library`, `SubmitBankDecisions`, типы `BankDecision`/`BankReceipt`, `UnknownTermError`
и их пины. На месте write-пути — пометка для будущей сессии (`internal/httpapi/reading.go`, блок
«THE WRITE HALF OF THE BANK IS GONE»): что отменено, что ратификация ОСТАВИЛА пер-термным
(правку), и что под неё уже есть в схеме. ⚠ **Контрактная половина остаётся открытой** — канон
0.4.0 несёт путь, три схемы и глагол, и это зона контрактной сессии.
Посылку промта пере-проверил САМ, чтением кода движка, а не по докам. Подтверждается вся:
| Факт | Улика |
|---|---|
| движок читает `mined_delta`, только если путь объявлен | `backend/internal/pipeline/mining.go` `loadMinedDelta`: `if r.Book.MinedDelta == "" { return nil, nil }` — ни одного чтения |
| то же для `mined_rejects` | там же, `loadMinedRejects` |
| объявленный ключ с НЕсуществующим файлом валит загрузку конфига целиком | `backend/internal/config/book.go:199-207`: `bad("mined_delta %s is not readable: %v")` |
| ключей нет в шаблоне оператора | `backend/example/book.yaml` — ноль вхождений `mined_delta`/`mined_rejects` |
| платформа пишет `book.yaml` ОДИН раз и фиксированным набором из пяти ключей | `platform/internal/books/render.go``O_EXCL`, «writes the file once and never again»; ключи `book_id · title · source_lang · target_lang · source_file` |
| **новое, чего в промте не было:** декодер движка СТРОГИЙ — незнакомый ключ это жёсткая ошибка | `backend/internal/config/book.go:142-144`, `dec.KnownFields(true)` — то есть платформа не может «на всякий случай» дописать ключ, которого сборка движка не знает |
| относительные пути резолвятся от каталога КНИГИ | `backend/internal/config/book.go:148-162`, `resolve()` — значит один шаблон на все книги даёт пер-книжные пути без подстановки |
| форматы файлов | `mined_delta` = `seed.File` (`terms:` со схемой `internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed` и пере-штампуется `Source:"mined"`; `mined_rejects` = `rejects: [{src, note}]` (`mining.go` `minedRejectFile`), это ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит |
| на стороне платформы решения write-only | `internal/pgstore/readmodel.go` `SubmitBankDecisions` пишет `action`/`dst`; ни один SELECT их не читает, воркер описан комментарием собственной миграции `00002_readmodel.sql:172-174` и не построен |
**Варианты с ценой (решает владелец, не я):**
- **A. Платформа объявляет два ключа при рождении книги и сразу создаёт два пустых файла.**
Работает без единой правки движка и без действий оператора; пустой файл законен (`terms:` пуст ⇒
ноль записей, `os.Stat` проходит). **Цена:** платформа начинает знать два ключа СХЕМЫ движка, а
форма Б ратифицирована ровно наоборот (D39.110 §2б: шаблон — деплой-артефакт оператора, платформа
подставляет только личность, языки и имя файла). Плюс строгий декодер: сборка движка, которая
этих ключей ещё/уже не знает, перестанет грузить КАЖДУЮ новую книгу.
- **B. Ключи несёт шаблон оператора.** Платформа схемы не узнаёт, пути пер-книжные достаются даром.
**Цена:** сам по себе вариант НЕ работает — движок отказывается грузить конфиг с объявленным, но
отсутствующим файлом, а создавать файлы всё равно некому, кроме платформы. То есть B требует
половины A или варианта D.
- **C. Двигается ратифицированная граница владения:** платформе разрешено писать в каталог книги эти
два файла (и только их), ключи несёт шаблон. **Цена:** правка ратификации D39.110; зато платформа
по-прежнему не знает схемы — она пишет файлы по путям, которые прочитала из уже существующего
`book.yaml`.
- **D. Движок терпит объявленный, но отсутствующий `mined_delta`/`mined_rejects` как пустой.**
Одна правка `backend/internal/config/book.go`, после чего работает чистый B: платформа пишет файлы
только когда решения есть, и ничего не обязано существовать заранее. **Цена:** ослабляется
намеренный гард — опечатка оператора в пути становится тихой. Смягчается тем, что послабление
только для двух ключей и с громким WARN. Это строка ЕДИНОГО бэклога (движок), не моя.
**Рекомендация зоны: C или D+B.** Обе оставляют платформу в неведении о схеме движка — а это и есть
то свойство, ради которого форма Б ратифицировалась. A решает быстрее всех и покупает это тем, что
платформа впервые начинает знать схему чужой стороны, причём под строгим декодером.
**Строить начну только после ратификации.** Границы, если разрешат, приняты как в §4.2: писать не
«перед resume», а только на том резюме, которое СНИМАЕТ стоп подписи (`l.VerifyBank &&
!l.BankReleased`); `--verify-bank` не трогать; пост-ридинговый цикл (строка 192) не проектировать.
`PD-277` в регистре относил эту работу к отложенной 192. ⚠ **22.08 он ПЕРЕ-ДИСПОЗИРОВАН** — не по
развилке, а по спилу глагола: остаток «честность `decline`» умер вместе с ним. Ниже — прежняя
формулировка обязательства, оставленная как след решения: правлю его только после ратификации, как
промт и велит.
### Как это проверялось
**Батарея:** `cd platform && make check` с ОБОИМИ гейтами (`TM_PLATFORM_TEST_DSN` на живом
PostgreSQL 18.4, `TM_PLATFORM_TEST_ENGINE_BIN` + `_BOOK_TEMPLATE`) — 18 пакетов, EXIT=0, скипов 0,
линтер «0 issues». Дифф имён тестов — исполнением: `grep -rho '^func Test[A-Za-z0-9_]*'` по дереву
против `git show HEAD:<файл>` по всем `_test.go`. ⚠ **Замер пере-снят при сдаче** и его исход
изменился: `comm -23` даёт ТРИ удалённых имени — все три описывали пер-термную подпись банка,
снятую 22.08 по слову владельца (`PD-370`, разбор в шапке), — а `comm -13` даёт 29 новых.
Прежняя формулировка «удалённых нет» была верна до спила и снята, а не оставлена рядом с новой.
**Посадки мутаций волны 1 — 20, поймано 20** (перечень ниже; счёт пере-снят при сдаче — прежняя
цифра «16» не сходилась с собственным списком в той же фразе). Каждая ставилась в код, пин обязан был упасть, код возвращался:
снятие `payload["revision"]` ✅ · снятие `payload["structure_version"]` ✅ · `ContractVersion` назад
на 0.3.0 ✅ · `inReadTx` без обеих опций ✅ · без изоляции ✅ · без read-only ✅ · `ListBooks` на
`inTx` ✅ · снятие `and book_id <> $2` ✅ · снятие пола самоописания манифеста ✅ · снятие пола «ноль
глав» ✅ · обе фазы свипа на ОДНОМ дедлайне ✅ · ранний возврат фазы из `Sweep` ✅ · свип читает
`ListLiveRuns` вместо списка «пора» ✅ · отсрочка только по ошибке ✅ · `DeferReadModelDebt` в
`now()` ✅ · долг никогда не списывается ✅ · новая граница наследует потраченный бюджет ✅ · колонка,
которой нет в схеме, в СКЛЕЕННОМ фрагменте (гейт SQL) ✅ · то же в локальной `const q` ✅ ·
несворачиваемый SQL ✅.
**Три посадки из этого списка сперва ПЕРЕЖИЛИ** мои же пины и вскрыли настоящие дефекты пака —
разбор в `PD-337`/`PD-338`/`PD-339`; пины переписаны, мутации пере-посажены.
**Воспроизведение ПЕРЕД пином — там, где промт этого требовал (пп. 2, 12, 13):** три мутации
посажены разом в HEAD-код и прогнана ПОЛНАЯ батарея — 18 пакетов, exit 0, скипов 0. То есть все три
дыры подтверждены исполнением, а не прочитаны в пинге.
**Живые пробы — настоящий `tmctl`, живой Postgres, дев-демон на 127.0.0.1:8098:**
- `GET /v0/capabilities``"contract_version": "0.4.0"` (пункт 1 на проводе);
- `tmplatformctl seed` — аккаунт, грант, книга через ЖИВОЙ `POST /v0/books`: интейк прошёл, движок
нарезал 3 главы, `GET /chapters` отдаёт 3, долг материализации погашен, `read_model_attempts = 0`.
**Это отрицательная проверка нового пола: законный манифест живого движка он НЕ отвергает;**
- **PD-162 воспроизведён живьём** — каталог книги унесён, прогон принят (202), спавн падает.
`tmplatformctl runs` печатает строку с числом неудач `1`, временем следующей попытки, суммой холда
`0.090000`, его возрастом, `(none)` вместо юнита и текстом ошибки движка. **До этого пака оператор
видел только растущий счётчик `sweep_unfinished_total` и не мог сделать ничего;**
- `run abandon` без флага: прогон `failed`/`service_error`, деньги не тронуты, книга РАЗМОРОЖЕНА —
следующий прогон принят (202). С флагом `--release-hold`: холд вернулся целиком;
- **деньги пере-считаны из сырого леджера двумя путями и сошлись до цента:** кэш
`account_balances.balance_micro_usd` = 5000000, `sum(amount_micro_usd)` по `credit_ledger` =
5000000, открытых холдов 0; строки леджера — `grant · hold · hold_release · hold · hold_release`;
- метрики на проводе: `tm_platform_runs_stalled 0`, `tm_platform_reading_surfaces_abandoned 0`;
- обе новые ручки конфигурации печатаются на буте с источником (`TM_PLATFORM_SWEEP_BUDGET` `2m0s`
`default`, `TM_PLATFORM_RUN_BUDGET` `1m0s` `default`) — норма PD-114.
**Гейты доков:** `python3 docs/scripts/counts.py --check` — exit 0, литералы сходятся;
`--lint` — exit 0.
**Раскрываю правку ЧУЖОГО документа:** пак убил два якоря в `PLATFORM_P8_REVIEW_SESSION_PROMPT.md`
(константы `sweepBudget`/`defaultRunBudget` переехали), и `--lint` был из-за этого КРАСНЫМ. Якоря
пере-нацелены на новые места, абзац оставлен как описание НАЙДЕННОГО, а не текущего кода. Правил
чужой промт сознательно и говорю об этом прямо: якоря умерли по моей вине, а красный гейт не
обходят. Оркестратор вправе откатить эту правку — содержательного текста она не трогает.
⚠ Первая моя проверка этого гейта дала EXIT=0 и была ЛОЖНОЙ: я мерил в момент, когда файла с
якорями в дереве не было, поэтому линт его не сканировал. Критик полноты был прав, я нет.
### Спил пер-термной подписи банка (слово владельца 22.08) и что осталось контракту
**Заказ:** модель отменена `D39.144` ещё 16.08, реализации нет — спилить в зоне, оставить пометку
будущим сессиям. Перед спилом — аудит воркфлоу: 5 линз (канон · код зоны · потребители ·
ратификация · будущая пометка) + адверсариальная проверка каждого рискованного вывода, **31 агент**.
**Снято из зоны:** маршрут `POST /books/{bookId}/bank/decisions` · хендлер `bankDecisions` и
wire-типы (`wireBankDecision`/`wireBankDecisions`/`wireBankReceipt`) · метод `SubmitBankDecisions`
из интерфейса `Library` и из стора · типы `BankDecision`/`BankReceipt` · `ErrUnknownTerm` /
`UnknownTermError` и их отображение в контрактный код · пины, которые их описывали.
**НЕ тронуто, и это проверено:** чтение банка (`GET /bank`, `BankTerm`, оси статуса и происхождения)
· вокабула ДВИЖКА `proposed|in_progress|approved` в сайдкар-инжесте — она чужая и не отменялась ·
сам стоп `awaiting_bank` и подпись как `resume` (D39.144 снимала ГЕЙТ, не СТОП) · таблица
`bank_decisions` и её констрейнты: миграции релизные, а колонки `term_id`/`dst` — ровно то, что
нужно будущей ПРАВКЕ термина. Данных под снос не было: на стенде ноль строк решений.
**Пометка на месте write-пути** (`internal/httpapi/reading.go`, «THE WRITE HALF OF THE BANK IS
GONE»): что именно отменено, что ратификация ОСТАВИЛА пер-термным (правку и добавление термина),
чего нет ни на одной поверхности, и что под это уже есть в схеме и в движке (`mined_delta`).
**Гейт размера поверхности сработал**`len(contractSurface) < 15` поймал снятие маршрута. Пол
сдвинут 15 → 14 **с причиной в комментарии**: пол, опущенный под ратифицированное сокращение, — не
подгонка; опущенный без неё — подгонка. Два теста, стоявшие на снятой ручке как на «любом POST»,
пере-нацелены на живой маршрут, а не удалены.
#### Насколько страдает контракт (главный вопрос владельца)
Канон **не лжёт про модель**`D39.144` в него внесена и записана в четырёх местах верно
(`openapi.yaml:443-448`, `:539-541`, `:707`, `:2498-2499`). Беда в другом: **рядом с новой моделью
в полном рабочем обмундировании стоит старая** — путь, три схемы, четыре счётчика, — и обе зовутся
словом «signing». Файл несёт ДВЕ модели подписи.
- **Прямая ложь — шесть мест** (четыре в каноне, два в компаньоне). Худшие: `:1830-1832`
(«статус подтянется на следующей пересборке» — не подтянется, решения до движка не доезжают, что
говорит сам же канон на `:1959-1963`) и компаньон `:672``POST /bank/decisions` НЕ ПОСТРОЕНО» —
было построено в P7 и снято сегодня).
- **Самопротиворечия — три.** Самое явное: `:443` «Signing the bank is ONE act over the whole of
it» против `:477` «Signing is not a row edit» — заголовок описания ручки называет подписью САМУ
РУЧКУ. Тридцать четыре строки между ними, и оба текста компилируются в сгенерированный клиент.
Плюс `:1918-1922` («строка может быть решена и НЕ подписана, потому что отказ — тоже решение») —
дословно отменённая модель — соседствует с `:1929-1932` («INFORMATIONAL, never a gate»).
- **Дыра, которую спил вскрывает и которую надо назвать вслух:** `approve` + непустой `dst` был
ЕДИНСТВЕННЫМ живым носителем того, что D39.144 оставила пер-термным — «поправить термин». А
«добавить свой термин» модель называет, и в контракте его **нет вообще**: на 17 путей канона
запись в банк была ровно одна. То есть после спила у собственной фразы канона `:539-541`
(«correct what they care to, then release it with ONE resume») **нет ручки**.
- **Счётчики вырождаются.** `pending_decisions` считается как «строки `proposed`, на которые НЕТ
записи в `bank_decisions`»; писателя больше нет ⇒ это «сколько строк предложено», а `complete`
истинно ровно тогда, когда read-out движка не несёт ни одной `proposed` (не константа: сайдкар
несёт и `in_progress`, и `approved`). Поля обязательные на проводе (`EventBank.required`), поэтому оставлены как есть:
менять их смысл — правка канона, а не зоны.
- **Объём и вес:** ~17 мест в `openapi.yaml` (~150 строк из 2592) + ~8 в компаньоне. Вес
**ломающий минор 0.4.0 → 0.5.0**; мажора нет ни по политике («0.x ломает миноры весь бета-период»),
ни по факту. Цена фронту нулевая ДОПОЛНИТЕЛЬНО: его зеркало ратифицированно стоит на 0.2.3.
**Пока канон не поправлен, деплой обслуживает на одну ручку меньше, чем объявляет.** Это записано
строкой `PD-370` (зонная половина закрыта, контрактная открыта), и это осознанная цена спила, а не
недосмотр.
### Волна 2: воркфлоу-ревью и что оно нашло в САМОМ паке
Заказ владельца: после возврата ревьюеров волны 1 и починки их находок — воркфлоу пошире, 1030
агентов, один Fable 5 на тяжелейший участок. Исполнено: **11 линз поиска + адверсариальный рефутер
на КАЖДУЮ находку + синтез, 47 агентов, 0 ошибок**. Fable 5 — один, на свипе и деньгах.
**36 находок, 21 пережила рефутера, 15 опровергнуто.** После склейки дублей (голодовку расчётной
фазы назвали пять линз, срез по байту — две) это **17 строк регистра**: `PD-353…PD-366` закрыты,
`PD-367`/`PD-368`/`PD-369` открыты — они не мои решения и переданы как вопросы.
⚠ **Итоговая строка самого воркфлоу («ни одна находка не пережила проверку») — БАГ МОЕГО скрипта
синтеза, а не вердикт.** Она противоречила сама себе (пустыми были и «выжившие», и «убитые»), и
таблица пере-собрана из `journal.jsonl` — по одной записи на агента. Говорю прямо: доверься я
сводке, пак уехал бы на приёмку с блокером внутри.
**Блокер, найденный ПЯТЬЮ независимыми линзами (включая Fable 5) и подтверждённый мной по коду
до их вердикта — `PD-353`.** Пак разделил проход на фазы, чтобы одна не могла съесть другую, и
оставил ВНУТРИ второй фазы то же самое голодание: `UnsettledRuns` упорядочен старейшим-первым,
каждый элемент стоит целый бюджет, а `settle` при нечитаемой трате возвращает nil — то есть клин
рапортует УСПЕХ, потратив проход. Одна недоступная книга замораживала холды ВСЕХ аккаунтов, каждый
проход. Это ровно та формулировка, которую шапка `Sweep` объявляла побеждённой.
**Ещё два, которые делали механизмы пака недостижимыми:**
- `PD-354` — счётчик неудач обнулялся ТИШИНОЙ: дорогой вопрос движку задаётся раз в `ResyncEvery`,
остальные проходы ничего не устанавливают, и `ClearRunDeferral` на них сбрасывал счёт. 1,0,1,0 —
`StalledAfter` недостижим, гейдж пуст, оператору не говорят никогда.
- `PD-355` — колонка SPENT в операторской таблице показывала пожизненную трату КНИГИ. Моя же
починка G8 волны 1 взяла не то число, на единственной таблице, заведённой ради решения о деньгах.
**Чему меня научили РЕФУТЕРЫ (15 опровержений, каждое куплено исполнением, а не рассуждением):**
- «Поднять `RunBudget` = объявить здоровый прогон застрявшим» — **опровергнуто замером**: поднятие
ручки не меняет вообще ничего, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами
шиппящиеся дефолты. Я уже держал эту «починку» в плане и не построил её. Остаток — `PD-368`.
- «Интейк обязан применить `Whole()`» — **опровергнуто контрактом и батареей**: вся батарея интейка
ездит на манифестах без списка глав, контракт интейка — только счётчики. Механика находки верна
(`PD-367` открыт), лекарство — нет.
- «Пропуск тела помощника в гейте SQL = дыра» — **опровергнуто исполнением**: мутация в теле `bump`
краснит десятки поведенческих тестов; спрятать там непроверяемый оператор нельзя.
- «Стойл-детектор слеп к форме PD-162» (находка Fable 5) — опровергнуто.
**Две линзы разошлись во ВЕСЕ одной находки** (`PD-358`, срез по байту в операторской таблице):
одна назвала микрополировкой вне рамки, вторая довела цепочку до многобайтового stderr движка.
Починил как дешёвое и бесспорное, расхождение раскрыл в строке.
### Самопроверка (артефакт §5: имена агентов и по одной команде или цитате из каждого)
Исходов три, «не проверено» ≠ «опровергнуто». Каждому агенту выдан СВОЙ каталог песочницы явным
путём, мандат читающий, state-changing git запрещён перечислением (D39.113). В рабочее дерево не
писал никто: оба ревьюера волны 1 ответили на прямой вопрос уликами.
**Волна 1 — три изолированных ревьюера по финальному диффу (author≠reviewer):**
| Агент | Ось | Цитата или команда ИЗ ЕГО отчёта | Находок | Исход |
|---|---|---|---|---|
| `agent-afec473415b228cdf` | контракт-конформность против канона 0.4.0 + свои посадки | «в песочнице создан симлинк `<scratch>/docs -> …/docs` — новый гейт `internal/gates/contract_test.go` тянется за канон ЗА пределы модуля, и без него копия дерева его не находит» | 5 + 3 пережившие мутации | все 8 починены |
| `agent-a394edded388676bc` | шов с движком и деньги, свой инвентарь каналов | «`repo*/platform/cmd/rev2probe\|rev2ctl\|rev2seam\|rev2ceiling\|rev2ids/main.go` и `repo*/platform/internal/runs/rev2_starve_test.go` — мои пробы (только в копиях)» | 8 + 3 пережившие мутации | все 11 починены; G1 — БЛОКЕР в моём коде |
| `agent-a1e4407d89e52128d` | полнота сдачи: пункт заказа → носитель в дереве | «Snapshot taken at 2026-08-22 00:49:24. ⚠ The tree was moving while I read it: `platform/BACKLOG.md` gained rows П-19/П-20 at 00:48, mid-review» | 6 | 5 починены, 1 (К-4) вскрыла МОЮ ошибку измерения |
Две разведки (read-only, не ревью): `agent-acf370d564663617e` — инвентарь SQL для границы sqlc;
`agent-a1cb3d1a5c59f2f2e` — инвентарь каналов движка.
**Волна 2 — воркфлоу `wf_f8d1f3e6-85a`, 11 линз (одна на Fable 5) + рефутер на каждую находку:**
| Линза (агент) | Предмет | Цитата или команда ИЗ ЕГО отчёта | Находок | Пережило |
|---|---|---|---|---|
| `a2fdcd13144058b57` **Fable 5** | свип и деньги — тяжелейший участок | «Стойл-детектор слеп к клину „юнит исчез, workdir недоступен“ (форма PD-162): реконсиляция каждый проход рапортует успех» | 3 | 2 |
| `ae8f856fe08ee9e61` | фазы и отсрочка свипа | «Счётчик отсрочек не может дорасти до StalledAfter для того самого клина, ради которого написан» | 4 | 3 |
| `a3c4dc82a7d62f8a6` | архитектурный стиль, велосипеды | «Фаза расчёта денег осталась без отсрочки: head-of-line starvation, от которой пак и построен, на списке `UnsettledRuns` не починена» | 3 | 1 |
| `a62786a00eb5bbae4` | каноны Go и каноны зоны | «RunBudget молча обрезан половиной SweepBudget» (опровергнуто) + голодовка расчётной фазы | 4 | 2 |
| `af8620a80b7e12b06` | материализация и пол манифеста | «Провалившийся экспорт на смене кадра затирает весь оплаченный текст книги» | 3 | 3 |
| `ada2420ac7c576856` | CLI, метрики, конфиг | «SPENT в операторской таблице — пожизненный счётчик КНИГИ, а не траты этой попытки» | 3 | 3 |
| `ac92e0a8acba148ec` | сам гейт SQL: даёт ли ложную зелень | «`unresolvable` ключёвана функцией, а не сайтом: второй несворачиваемый запрос в той же функции молча проверяется текстом первого» | 2 | 1 |
| `a21f43459f3f12f7e` | комментарии: вода против несущего | «perf_test.go копирует „нормативные“ цифры из миграции 00022 и в той же фразе утверждает, что не копирует» | 4 | 1 |
| `a8faf63f1dde50903` | скоуп: сверка построенного против БУКВЫ промта | «Пункт 9: новое условие замера в perf_test.go ложно для бенчмарка этого же файла» | 4 | 2 |
| `a4dad647c0eb8310f` | регрессии: что пак сломал в уже работавшем | «Отмена контекста сообщается как дедлайн: остановка демона посреди прохода поднимает счётчик голодания» | 3 | 2 |
| `a178aca863af19915` | контракт-конформность построенного против канона 0.4.0 | «Пункт 1: подъём константы оставил в коде утверждение, что деплой объявляет 0.3.0» (опровергнуто) | 3 | 1 |
Рефутеры (по одному на находку, 36 штук) считаются наравне с находками — три примера того, чем
куплены их вердикты:
- `ad12d9bcca05b883a`: «`WITH the index n_tup_upd=3942 n_tup_hot_upd=3909 (HOT 99.2%) idx size=8192`
против `WITHOUT the index n_tup_upd=1994 n_tup_hot_upd=1984 (HOT 99.5%)`» — из этого замера
выросла `PD-364` (индекс удалён);
- `a337318688be20d30`: «`finished_at = now()` → `books.go:313: column "finished_at" of relation
"books" does not exist (42703)` … на всех 5 сайтах»;
- `a7fadd3c541e9a87d`: «пять проходов подряд дают „the sweep settled 2 of 3 finished runs: context
deadline exceeded“, третий, чистый прогон не достигается никогда, reserved держится на $0.90» —
это подтверждение блокера `PD-353` исполнением.
**Посадки мутаций волны 2 — 6, поймано 6:** фаза расчёта не отсрочивает ✅ · `UnsettledRuns`
игнорирует отсрочку ✅ · тишина снова считается здоровьем ✅ · `RunBudget` снова не присваивается ✅ ·
срез по байту в операторской таблице ✅ · управляющие символы не вычищаются ✅.
⚠ Две первые попытки посадить последние две дали НЕ падение теста, а ошибку сборки (осиротевший
импорт) — это слабее, и они пере-посажены компилирующимися.
### Релей §4.7: не приходил
Аддендум пака P8-REVIEW должен был доехать **только релеем оркестратора с эхо-подтверждением**.
За сессию релея не поступало: ни находок по четырём осям, ни эхо. Отчёт читающего пака я не читал
и не гадал по нему — промт это прямо запрещает, состав пака определяет оркестратор.
Единственное касание того пака — техническое и раскрыто выше: два его якоря умерли от МОИХ правок
кода, `--lint` покраснел, якоря пере-нацелены (`docs/PLATFORM_P8_REVIEW_SESSION_PROMPT.md:62`,
плюс третье пере-нацеливание 22.08 после спила). Содержательного текста чужого промта не трогал.
### Obstacle: что не удалось и что не проверено
- **Сквозного живого перевода через платформу не было** и в этом паке быть не могло: строка 202
гейчена холодным прогоном движка (решение владельца 20.08). Всё после «прогон стартовал и
остановился» доказано батареей против живого Postgres и настоящего `tmctl`, а не переводом.
- **Труба 199(а) не построена и форматы сверены ЧТЕНИЕМ**, а не записью файла: `mined_delta` и
`mined_rejects` прочитаны в коде движка (`membank.LoadGlossarySeed`, `mining.go`), но ни одного
файла платформа не писала — писать было бы строительством нератифицированной развилки.
- **Замер `PD-297` не снят.** Отказ осознанный: путь самый опасный на запись, править его надо с
замером на корпусной книге до и после, а корпусной книги в паке не было.
- **Финальный дифф ПОСМОТРЕН** независимым ревьюером (author≠reviewer) уже после спила — он и
закрыл эту строку obstacle. Три его major были настоящими дырами в МОЁМ коде, и все три —
отсутствие пина, а не ошибка логики: пол `ErrTextUnknownForANewCut` (охраняет единственную
запись, способную стереть оплаченный текст) · «стоп перевешивает отсрочку» в `RunsToReconcile` ·
«пересборка банка не трогает `bank_decisions`» (свойство, на которое опирается пометка, ради
которой спил и делался). Каждая закрыта пином, каждый пин проверен посадкой мутации. Плюс семь
минорных: комментарии `SaveBank` и `bankCountsTx` описывали отменённую модель как живую,
пометка врала про `action NOT NULL`, снятие NUL не было запинено. Не найдено дефектов там, где
он искал отдельно: пол поверхности 15→14 равен реальному размеру таблицы, пере-нацеленные тесты
проверяют то же свойство, лишнего не срезано.
- **Число скипов батареи** снято рецептом `make check`, который пишет `.check.log` в дерево; при
параллельных прогонах агентов лог обрезался. Отдельная сверка двух пакетов с условными скипами
(`internal/runner`, `internal/gates`) дала `--- SKIP` = 0.
- **Контрактная половина `PD-370` открыта**: пока канон не поправлен, деплой обслуживает на одну
ручку меньше, чем объявляет. Это цена спила, названная заранее, а не обнаруженная при приёмке.
### Инвентарь каналов движка (артефакт §4.4 — собран ЧТЕНИЕМ кода движка, не по нашим докам)
Пак коснулся канала движка (пункт 10: `ingest.Manifest.Whole()` построен как зеркало движкового
правила), поэтому инвентарь сдаётся таблицей, а не словами «я прочитал код движка».
| Канал | Писатель в движке | Атомарность | Читатель/писатель платформы | Согласовано? |
|---|---|---|---|---|
| `<project_db>.manifest.json` | `pipeline/manifest.go:379` `writeFileAtomic` | **атомарно** (temp+`Sync`+rename, `pipeline/artifact.go:24-59`) | `internal/runner/engine.go` (`tmctl manifest`) → `ingest.DecodeManifest``internal/books/parse.go`, `internal/readmodel` | ✅ **и стало строже**: `Manifest.Whole()` теперь зеркалит `BookManifest.selfConsistent` (`pipeline/manifest.go:460-483`) поле в поле по тем правилам, которые платформа вообще читает (главы, юниты, нумерация). Верхняя граница чанков движка у нас counterpart'а не имеет намеренно — платформа счётчиков чанков не берёт |
| `<project_db>.bank.json` | `pipeline/bankexport.go:154` `writeFileAtomic` | **атомарно** | `internal/runner/artifacts.go:37``ingest.DecodeBank``pgstore.SaveBank` | ✅ |
| `<project_db>.bank-stop.json` | `pipeline/mining.go:393` `writeFileAtomic` | **атомарно** | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ канал существует и не потребляется; полная таблица подписи платформе сегодня не нужна, она проецирует банк |
| `<project_db>.bank-stop.txt` | `pipeline/mining.go:195` `os.WriteFile` | **НЕ атомарно** (усечение первым делом) | читателя нет | ✅ и это важно: брать его на живом прогоне нельзя — исправление пинга №15 оркестратором подтверждается кодом |
| `<project_db>.mined-signature.yaml` | `pipeline/mining.go:188` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `<project_db>.auto-bank.yaml` | `pipeline/mining.go:741` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же |
| `mined_delta` (путь из `book.yaml`) | **писателя в движке НЕТ** — только читатель `pipeline/mining.go` `loadMinedDelta`; формат `seed.File` (`internal/seed/seed.go:63-80`), грузится `membank.LoadGlossarySeed`, `Source` пере-штампуется на `"mined"` | — | **писателя НЕТ и у платформы** | ❌ **разрыв — это и есть строка 199(а)** |
| `mined_rejects` | читатель `pipeline/mining.go` `loadMinedRejects`; формат `rejects: [{src, note}]`; ФИЛЬТР ПРЕДЛОЖЕНИЙ, в банк не входит | — | писателя нет | ❌ тот же разрыв |
| `events.jsonl` (NDJSON эмиттера) | движок, StreamVersion 1.1 | append-only | `internal/ingest/tail.go` + `pgstore.RunSink` | ✅ пак не трогал |
| exit-коды `tmctl` | контракт движка | — | `ingest.OutcomeOf`, `internal/runs/reconcile.go` `outcome` | ✅ пак не трогал |
| `tmctl status --json` | движок | — | `internal/runner/engine.go``ingest.StatusReport`; расчёт денег | ✅ пак не менял форму; изменил лишь то, СКОЛЬКО раз его зовут при неудаче (бэкофф) |
| `book.yaml` | оператор (шаблон) + **платформа один раз** (`internal/books/render.go`, `O_EXCL`) | — | платформа ПИШЕТ пять фиксированных ключей | ⚠ **декодер движка СТРОГИЙ** (`config/book.go:142-144`, `KnownFields(true)`): незнакомый ключ = жёсткая ошибка. Значит любое расширение набора ключей платформой — ратификация, а не правка. Прямо относится к развилке 199(а) выше |
- **P7, акт 5 — ЗАКРЫТ. Дерево готово к приёмке и лендингу.**
Батарея: 18 пакетов, exit 0, скипов 0, линтер 0 issues, тестовых функций **532** (⚠ было «533» — испр. оркестратором №18 20.08: канонической командой D39.121 `grep -rh '^func Test' platform --include='*_test.go' | wc -l` выходит 532; расхождение на единицу, ни на что не влияет, но число было заявлено без команды). Регистр — 327
строк, открытых 65 (13 minor, 52 info), **major и BLOCKER нет**. Дерево не закоммичено и не
застейджено. Миграции пака: **0001900024**.
### Что построено и зачем
1. **Долг на материализацию — колонка с арендой** (`books.read_model_owed_at`, 00019).
Ставится в ТОЙ ЖЕ транзакции, что закрывает границу работы (конец разбора и все три конца
прогона), гасится сверкой метки, берётся в аренду тем, кто платит. **Зачем:** очередь жила в
памяти процесса — упавший `Refresh` её терял, рестарт демона терял её всю, и оплаченный текст не
доезжал до читателя НИКОГДА. Плюс «в покое» для потока теперь значит «и материализация не
должна» — это Ф-56: книга, только что нарезанная, получала `end` и 204 «не переподключайся»
ровно в те секунды, пока дерево ещё не легло.
**Почему отошёл от прежней реализации:** удалены очередь в памяти (`runs.deferRefresh`/
`DrainRefresh`/`Reader`) и бэкстоп «дерева нет вовсе» (`books.materializeMissingTrees`,
`pgstore.BooksWithNoTree`) — оба покрывались одним механизмом, а бэкстоп к тому же не видел
книгу, у которой дерево есть, но на границу старше. Дрейн переехал в `readmodel.Drain` — в
пакет, чья это работа.
2. **«Глава сделана» = последний проход, который книга на ЭТОМ деплое реально получает**
(`books.edit_wave`, 00024). **Зачем:** счёт шёл по волне `edit`, и на деплое без редактора ни
одна глава не была сделана никогда — шкала покупки не подрезалась, сервис бесконечно предлагал
купить уже переведённое. Форму объявляет сам движок (пайплайн без редактора даёт волне `edit`
знаменатель ноль). **Почему на КНИГЕ, а не на прогоне:** прочитанная с последнего прогона, она
роняла `chapters_done` в ноль в момент допуска нового прогона — счётчик ходил назад, что канон
запрещает. Флаг монотонный: книга, прошедшая редактирующий пайплайн, остаётся такой.
3. **У клейма идемпотентности появился владелец** (`claim_token`, 00020) и **тождество интейка
решается дайджестом файла** (`content_sha256`, 00023). **Зачем:** строка ключа не несла, какая
попытка её держит — застрявшая попытка удаляла строку преемника (пользователь получал вторую
книгу) или писала свою квитанцию поверх чужой (клиент реплеил `Location` несуществующей книги);
а тождество по `Content-Length` не различало две РАЗНЫЕ книги с одинаковым объявлением.
4. **Остановка АККАУНТА читается с аккаунта**, а не с потолка одного прогона; `resume` различает
«прогон закончен, нужен новый» (`ceiling_reached`) и «пополните счёт» (`credit_unavailable`);
`blocked` называет холд, который реально освободит шкалу. **Зачем:** всё это — «у вас нет
денег» пользователю, у которого деньги есть, и отправка его делать не то.
5. **Счётчик замечаний вернулся на главу** (`chapters.note_count`, 00022), `chapters.units_done`
снесён (00021). **Почему это разворот решения 00016 того же пака:** 00016 снёс колонку с
доводом «её никто не пишет» — здесь она возвращается ВМЕСТЕ с писателем, которого ей не
хватало. Основание замерено: джойн, которым её заменяли, стоил 83% времени страницы библиотеки
(16.8 мс против 3.4 мс на корпусе 40 книг × 500 глав, ВАКУУМИРОВАННОМ); со счётчиком — 6.6 мс.
⚠ Условие замера названо здесь и при каждом другом числе про него (P8-FIX, PD-336): на ХОЛОДНОМ
корпусе тот же джойн даёт 96%, и это не противоречие, а другой замер. Нормативный носитель
вакуумированных цифр — миграция 00022, остальные на неё указывают. Бенчмарк оставлен
в дереве: `go test ./internal/pgstore/ -bench LibraryPage -run xxx`.
### Что форсировано и почему
- **Работа по канону 0.4.0 лежит в дереве до его ратификации.** Основание: ответ контрактной
сессии на записку зоны плюс слово владельца 20.08 про «кончились деньги → сообщить и дать
продолжить». Ломающая правка ровно одна — `Run.stop_requested` (0.3.0 такого поля не объявляет);
`cause.code: credit_unavailable` легален и по 0.3.0, второй уровень там открыт. Зависимость
записана строкой **PD-327** и закрывается лендингом канона, а не работой зоны. Если форма
сдвинется словом владельца — правки отделимы, карта в `archive/P7_ACT5_FIX_PLAN_2026-08-20.md` §5б.
- **Тесты на АТОМАРНОСТЬ сверяют `xmin`** (транзакцию, последней писавшей строку). Это не
экзотика ради экзотики: два пина акта проходили под мутацией «вынести метку из транзакции во
второй оператор» — конечное состояние она не меняет, меняет окно, и утверждать про окно иначе
тест не может.
### Что осталось, названо и не спрятано
- **PD-327** — ждёт лендинга канона 0.4.0 (не работа зоны).
- **PD-297** — round-trip на строку в `SaveStructure` под эксклюзивной блокировкой книги. Не взят
осознанно: путь самый опасный на запись, инструмент штатный (`tx.SendBatch`), но правку надо
мерить на корпусной книге до и после.
- **Четыре оси не смотрел никто и никогда:** деньги и леджер целиком · вход/сессии/CSRF · очередь
и джобы · метрики. Пак их не менял, а все приёмки смотрели дифф. Это отдельный пак.
- **Живого перевода на стенде не было ни разу** — движку нужны ключи провайдера и деньги. Всё
после «прогон стартовал и остановился» (подпись банка, halt на потолке, резюм после пополнения)
покрыто батареей против живого Postgres, а не живым движком.
### Как это проверялось
`make check` с гейтами (скипов 0) · каждая правка проверена ПОСАДКОЙ МУТАЦИИ: код испорчен именно
так, как называет комментарий пина, пин обязан упасть, код возвращён (40 проб, все поймались) ·
живые пробы по ручкам на стенде: интейк и реплей, `409 key_reused` на другой файл под тем же
ключом, часть после файла на реплее, `/chapters` `/units` `/notes` `/bank`, ETag→304 и разные теги
у страниц, 410 на чужой id главы, `after_version` из прошлой жизни, `unsupported_pair`, `/usage`
на пустом счету и после `adjust`, старт и стоп прогона со `stop_requested` на проводе, SSE в
покое и SSE с непогашенным долгом (Ф-56 на проводе), 400 вместо 413 на длинном теле.
### Самопроверка
Акт закрыт адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера + критик полноты, 84
агента, 0 ошибок): **37 находок, 18 пережили рефутеров**, и почти все — дефекты, внесённые
правками самого акта. Строки **PD-315…PD-327**, каждая с механизмом и пином. Два из них —
собственные пины акта, проходившие под мутацией, которую сами называют (PD-318): это третье
появление класса в паке, и он назван в плане §3 п.2 ещё до того, как повторился.
- **P7, акт 5 (начало) — исполнение фиксов по ревью акта 4 + ответ контрактной сессии.**
План и рабочий список — **`platform/docs/archive/P7_ACT5_FIX_PLAN_2026-08-20.md`** (читать первым; §5а — что пришло из
контракта).
⚠ **Контрактная сессия ответила 20.08 на записку зоны `archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md`: канон стал
0.4.0** (лежит в дереве контрактной зоны, НЕ ратифицирован — до лендинга в силе 0.3.0). Все три
вопроса, висевшие на владельце, закрыты. Зоне пришла работа: `resume` исчерпанного прогона — 409 с
двумя РАЗНЫМИ причинами (`ceiling_reached` против нового `credit_unavailable`); «глава сделана» =
последний реально пройденный проход, и это ДЕНЬГИ — `ChaptersLeft` на деплое без волны редактора не
подрезается вовсе, сервис бесконечно предлагает купить уже переведённое; отпечаток интейка — по
содержимому, `Content-Length` из нормы вышел, а невозможность установить тождество обязана давать
409, а не реплей; `Run.stop_requested` обязательным булевым (единственная ломающая правка).
Отклонены: слово под суточный потолок движка (решение владельца 15.08) и предложенный зоной признак
«прогон продолжаем» (три лишних чтения на карточке, которую перечитывают на каждый кадр).
Акт 4 закрылся адверсариальным ревью СОБСТВЕННОГО диффа (9 линз × 2 рефутера, 75 агентов):
**33 находки, 27 пережили рефутеров**, и часть из них внесли сами правки акта 4. Тяжелейшая —
четыре независимые линзы сошлись на том, что **реконсилятор снимал стоп подписи банка, которого
никто не подписывал**, и доспавнивал движок без `--verify-bank`: складывая снятие стопа внутрь
общего `reopen`, акт 4 не проверил, что `reopen` зовёт не только `resume`, но и свип. Нарушение
согласия пользователя; починено ключом на вызывающего, пин проверен посадкой мутации.
**Первый написанный на это пин ПРОШЁЛ под мутацией** — фикстура не доходила до кода, потому что
часы сервиса захвачены при конструировании и `f.now` их не двигает. Урок в плане, §3.
Закрыто также: загрузочная проверка пар — на ДОСТУПНОЙ половине (деплой с одними `:unavailable`
больше не стартует), интейк не трактует пустой список как «пускать всё», теги сравниваются
регистронезависимо. Батарея: 18 пакетов, exit 0, скипов 0, тестов **503**.
- **P7, акт 4 — доработка 20.08: сверка ВСЕХ находок против дерева + триаж §8.2.**
(Пакет релеев `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7; три вопроса владельцу §8.1а; вопросы контракта
вынесены в `platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md` для отдельной контрактной сессии.)
Все 74 позиции обеих приёмок сверены поштучно с ТЕКУЩИМ деревом — каждая с цитатой живого кода,
исход «не проверено» отдельно от «опровергнуто».
⚠ **Три места числились закрытыми и закрыты не были — правка не закрывала собственный сценарий:**
(1) подрезка буфера потока сверялась ПОСЛЕ того, как водяной знак перепрыгнул дыру, поэтому
условие не срабатывало никогда и кадры `note` терялись молча (PD-283); (2) `CompleteIdempotency`
писала квитанцию в строку, которая попытке уже не принадлежит — воспроизведено живым PG, клиент
получал квитанцию ЧУЖОГО запроса (PD-284); (3) пин на снятие `--verify-bank` был ПУСТЫМ: фикстура
стартовала прогон без `stop_for_signing` (PD-285). Все три «подтверждались» зелёной батареей.
**Ещё закрыто:** HEAD на потоке больше не держит горутину и ошибка `Flush` не выдаётся за
отправленный кадр (PD-286) · `blocked` называет чужую книгу только когда её холд укорачивает шкалу
(PD-287, пина не было вовсе) · `note_count` и `GET /notes` описывают одно множество (PD-288) ·
устаревшая доставка не рождает кадр (PD-289) · чтения на одном снапшоте (PD-290) · флаг «текст
прочитан» переехал на ПАРУ (PD-291) · снятие стопа банка внутри транзакции переоткрытия, отдельного
писателя больше нет (PD-292) · проход материализации подрезается своим бюджетом (PD-293) ·
блокировка книги в решениях банка (PD-294) · дубликат индекса снят (PD-295) · маршруты и коды
ошибок — по ОДНОЙ таблице, тесты ходят по ней (PD-296) · бэкстоп-свип для книги без дерева
(PD-276 дозакрыт) · интейк не платит за третью ре-нарезку (PD-248 актуализирован).
**Триаж §8.2:** 17 сделано, 5 отклонено с причиной, 1 релей. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 501**; каждая правка проверена посадкой мутации, не рассуждением.
**Осознанно НЕ сделано, с причиной:** PD-297 (round-trip на строку под блокировкой книги —
мерить прежде правки) · PD-202 (draft-only, вопрос владельцу) · PD-282 (канон §resumeRun не
покрывает «денег не осталось») · PD-298 (сперва сверка у движка) · PD-299.
**Осей, которых не смотрел никто:** деньги и леджер · вход/сессии/CSRF · очередь и джобы · метрики.
- **P7: приёмка → правки → приёмка правок (17.08).**
⚠ Полный хендофф под сессию без контекста — **`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`** (читать §0§3,
дальше по нужде). Там же: принципы приёмки, сверка с исходным промтом, техдолг, план следующей сессии.
Категорийное ревью по 12 осям с адверсариальной верификацией: 80 находок, **54 подтверждено, 16
правдоподобно, 10 опровергнуто**; после дедупа **46 уникальных дефектов** — 3 блокирующих класса,
15 HIGH, 17 MEDIUM. Батарея была зелёная, потому что все три блокирующих дефекта живут в путях,
которые тесты проходили только в безопасной форме.
**Починено в зоне** (регистр PD-257…PD-281): пере-нарезка книги больше не валит читающую модель
навсегда (отложенный констрейнт, миграция 00018) · упавший `tmctl export` больше не затирает текст
пустотой · весь блок идемпотентности (гонка первых попыток 500→409, область ключа, отпечаток с
размером файла, закрытие ключа вне контекста запроса) · база полосы прогресса считает тот же
проход, что числитель · SSE (подрезанные кадры → `resync_required`, дедлайн записи, id из
будущего) · материализация не голодает свип · банк читается на штатном деплое · деплой-нота больше
не включает обратно грант, который PD-104 выключил. Батарея: 18 пакетов, exit 0, скипов 0,
линтер 0 issues, тестов **458 → 490**.
**Приёмка ПРАВОК (9 осей + fable-5) нашла, что восемь дефектов внесли сами правки** — батарея не
увидела ни одного, три были запинены под ошибку автора. Все исправлены; разбор — хендофф §0.
**Б-1 ПОЧИНЕН разбором с владельцем 17.08 (PD-277).** Рамка приёмки была неверна: это не «нужен
новый канал в движок», а невыполненная половина строки единого бэклога **191(в)** (слово владельца
16.08, D39.144) — «проводка `resume = снятие стопа` до движка». Движок при выключенном
`--verify-bank` уже делает модель владельца: неподписанное едет авто-строками с пометкой
⟨проверить⟩ (`mining.go:211-231`, D39.42 п.3), а платформа передавала флаг обратно и роняла прогон
в тот же стоп. Снято: `l.VerifyBank && !l.BankReleased`; пин `TestAResumedRunIsSpawnedWithoutTheSigningStop`.
⚠ Остаток: `decline` пользователя до движка не доезжает (движок читает `mined_rejects`) — строка
бэклога **192**, отложена владельцем.
**PD-273 закрыт решением владельца 17.08:** статус остаётся `awaiting_bank`, лечение — признак
«стоп был запрошен» в контракте; готовая строка для единого бэклога — хендофф §7(и), вставляет
оркестратор (зона `docs/` не наша).
**Разбор, план, обязательства через оркестратора и команды воспроизведения —
`platform/docs/archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`.**
⚠ Заявки раздела «Сессия P7» ниже приёмку НЕ прошли — читать их вместе с хендоффом, не вместо.
- **P7 ПОСТРОЕН — заявка САМОЙ сессии написания (17.08).****Приёмку эта заявка НЕ прошла** — см.
верхнюю запись и `archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md`: 46 уникальных дефектов, три блокирующих класса, из них
ни один батарея не видела. Читать ниже как исходную позицию, а не как состояние дерева.
Читающая поверхность контракта **0.3.0**:
главы · пары с текстом · замечания · банк и подпись · SSE на КНИГЕ · `GET /capabilities` ·
машинная модель ошибок (`code` + `request_id`) · условные чтения и сжатие · `Idempotency-Key` ·
`structure_version`. Плюс форварды (`title`, `reject_reason`), PD-104 (грант → 0), демонтаж
построенного гейта полноты банка (D39.144), PD-241, конформность построенного по §5 промта и
рантбук деплоя, прогнанный ЖИВЫМ `migrate`. Батарея на стенде `~/.local/pgsql` (порт 55433) под
`-race` с ОБОИМИ гейтами: **EXIT=0, 18 пакетов, скипов 0, линтер 0 issues**, тестовых функций
**416 → 458**. Дифф: 46 файлов, +2329/646. Селф-ревью финалом — ТРИ независимых ревьюера, два из
них другой модели: **22 находки, 17 подтверждены и починены** (две HIGH нашли оба контрактных
ревьюера независимо), 5 — диспозиция с аргументом. Разделы ниже — записка-план, живые пробы,
таблица селф-ревью, obstacle, вопросы оркестратору.
- **P8-FIX ПРИНЯТ И ЗАЛЕНДЕН оркестратором №18 (22.08).** Тело приёмки — ратифицированная нота
**D39.154** (`docs/architecture/05-decisions-log.md`), здесь не пересказывается. Зоне важно ровно
следующее, и этого в ноте нет:
- **Пинг зоны №1 ЗАКРЫТ:** якорь `05-decisions-log.md:542``internal/pgstore/perf_test.go:13` жив,
токен «96% of the page» на месте — правку комментария бенчмарка приёмка приняла.
- **Правку зоной ЧУЖОГО документа** (пере-нацеливание двух моих якорей в промте читающего пака
после переезда констант) **утверждаю и не откатываю**: якоря умерли по вине пака, пак их починил
и раскрыл это сам, вместо того чтобы обойти красный гейт. Это ровно то поведение, которого норма
и требует, — так и делайте дальше.
- **«Скипов 0» у меня НЕ воспроизвелось: три скипа.** Не ваша регрессия и не ошибка отчёта —
`systemdOrSkip` гейтит три теста `internal/runner` достижимостью пользовательского менеджера
systemd, а на моём хосте `/run/user/1000` нет. Незакрытым осталось то, что рецепт и критерий
приёмки объявляли у батареи ДВА гейта, а их три: `PD-374`, оба носителя поправлены 22.08.
- **Мои одиннадцать посадок мутаций вне вашего списка:** убито восемь, выжили три — `StalledAfter`
законно (носитель один), `maxAttempts` и `truncateReason` дали `PD-373`/`PD-372`. Плюс находка
вне карты пака `PD-371`. ⚠ Две первых редакции МОИХ посадок дали ложное «выжила» — гонял не тот
пакет; пере-прогнано. Говорю, потому что необъявленная ошибка харнесса приёмки — находка, которой нет.
- **Числа сдачи в вашем отчёте держатся**, кроме одного: «отпечатков миграций 41» — их 25
(`grep -c '^[0-9a-f]\{64\}' migrations.sha256`); число было названо без команды. Регистр после
приёмки — **374 строки, открытых 72** (1 major, 16 minor, 55 info).
- **Открытым уезжает `PD-370`** (контрактная половина) — не работа зоны, закрывать её здесь было бы
подгонкой под критерий приёмки. Согласен с вашей диспозицией.
**Порядок паков вышел ОБРАТНЫМ очереди:** промт P8-FIX требовал запуска ПОСЛЕ читающего пака,
запущен был раньше. В плюс — названная цена перестановки («блокер живёт всё время читающего пака»)
НЕ заплачена. В минус — §4.7 релея пуст, и читающий пак пойдёт по только что переписанному коду;
его промт про это предупреждён баннером.
- **Эра пака P8-FIX (2122.08) — В АРХИВЕ.** Отчёт пака, обе волны ревью, спил пер-термной подписи, инвентарь каналов и obstacle — [archive/platform-PROGRESS-P8.md](archive/platform-PROGRESS-P8.md). Ратификация — D39.154, лендинг `31f1f82`. Живое из этой эры: четыре открытые строки регистра (`PD-370` мажор — контрактная половина · `PD-371` · `PD-372` · `PD-373`/`PD-374`), инвентарь каналов шва в `STACK_DECISIONS.md`, граница sqlc в `BACKLOG.md` П-19.
- **Записи акта 5 пака P7, остававшиеся в живом журнале, — ДОСЛАНЫ в [archive/platform-PROGRESS-P7.md](archive/platform-PROGRESS-P7.md)** 22.08: заголовок «эра P7 в архиве» стоял, а тела лежали здесь.
- **Запись оркестратора, 15.08 — P6 + ДОФИКС ПРИНЯТЫ И ЗАЛЕНДЕНЫ (D39.132).** Приёмка двумя
раундами: панель 5 адверсариальных верификаторов по P6 (две линзы — другой моделью; исполнением,