From 5a700e03b48ed0badd9af9df9cd44ae6233d9e0b Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 22 Aug 2026 18:24:31 +0300 Subject: [PATCH] Slice the P8 era and the P7 leftovers out of the zone journal after moving what only it carried into durable homes --- platform/docs/DEFECT_REGISTER.md | 2 +- platform/docs/archive/platform-PROGRESS-P7.md | 193 +++++ platform/docs/archive/platform-PROGRESS-P8.md | 472 ++++++++++++ platform/docs/platform-PROGRESS.md | 718 +----------------- 4 files changed, 696 insertions(+), 689 deletions(-) create mode 100644 platform/docs/archive/platform-PROGRESS-P8.md diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 2dcb4b7f..263f2a8d 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -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, 5–10 взаимоблокировок на 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) | diff --git a/platform/docs/archive/platform-PROGRESS-P7.md b/platform/docs/archive/platform-PROGRESS-P7.md index b68cc4e1..5b32e277 100644 --- a/platform/docs/archive/platform-PROGRESS-P7.md +++ b/platform/docs/archive/platform-PROGRESS-P7.md @@ -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 нет**. Дерево не закоммичено и не + застейджено. Миграции пака: **00019–00024**. + + ### Что построено и зачем + + 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, вопросы оркестратору. + diff --git a/platform/docs/archive/platform-PROGRESS-P8.md b/platform/docs/archive/platform-PROGRESS-P8.md new file mode 100644 index 00000000..c77ebedb --- /dev/null +++ b/platform/docs/archive/platform-PROGRESS-P8.md @@ -0,0 +1,472 @@ +# Слайс зонного журнала: эра пака P8-FIX (21–22.08.2026) + +> ⚠ **Архив. Инструкции отсюда НЕ исполнять, при онбординге НЕ читать** — только по конкретной ссылке. +> Вынесено оркестратором №18 22.08 при актуализации доков. Состояние зоны — шапка живого +> `../platform-PROGRESS.md`; дефекты — `../DEFECT_REGISTER.md`; правила, пережившие пак, — +> `../STACK_DECISIONS.md`; ратификация — **D39.154** в `docs/architecture/05-decisions-log.md`. +> +> **Что этот пак сделал:** закрыл фикс-лист приёмки P7 (пункты 1–7 и 9–13) и врезанный первым +> блокер свипа `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 (1–2 падения на ~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 --reason [--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), + тогда как 80–83% — вакуумированный. Носители мерили разное и все честны. + + ### Строка 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 и починки их находок — воркфлоу пошире, 10–30 + агентов, один 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 + свои посадки | «в песочнице создан симлинк `/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()` построен как зеркало движкового + правила), поэтому инвентарь сдаётся таблицей, а не словами «я прочитал код движка». + + | Канал | Писатель в движке | Атомарность | Читатель/писатель платформы | Согласовано? | + |---|---|---|---|---| + | `.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'а не имеет намеренно — платформа счётчиков чанков не берёт | + | `.bank.json` | `pipeline/bankexport.go:154` `writeFileAtomic` | **атомарно** | `internal/runner/artifacts.go:37` → `ingest.DecodeBank` → `pgstore.SaveBank` | ✅ | + | `.bank-stop.json` | `pipeline/mining.go:393` `writeFileAtomic` | **атомарно** | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ канал существует и не потребляется; полная таблица подписи платформе сегодня не нужна, она проецирует банк | + | `.bank-stop.txt` | `pipeline/mining.go:195` `os.WriteFile` | **НЕ атомарно** (усечение первым делом) | читателя нет | ✅ и это важно: брать его на живом прогоне нельзя — исправление пинга №15 оркестратором подтверждается кодом | + | `.mined-signature.yaml` | `pipeline/mining.go:188` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же | + | `.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(а) выше | + diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index 06848ab6..005659b7 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -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 (1–2 падения на ~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 --reason [--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), - тогда как 80–83% — вакуумированный. Носители мерили разное и все честны. - - ### Строка 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 и починки их находок — воркфлоу пошире, 10–30 - агентов, один 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 + свои посадки | «в песочнице создан симлинк `/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()` построен как зеркало движкового - правила), поэтому инвентарь сдаётся таблицей, а не словами «я прочитал код движка». - - | Канал | Писатель в движке | Атомарность | Читатель/писатель платформы | Согласовано? | - |---|---|---|---|---| - | `.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'а не имеет намеренно — платформа счётчиков чанков не берёт | - | `.bank.json` | `pipeline/bankexport.go:154` `writeFileAtomic` | **атомарно** | `internal/runner/artifacts.go:37` → `ingest.DecodeBank` → `pgstore.SaveBank` | ✅ | - | `.bank-stop.json` | `pipeline/mining.go:393` `writeFileAtomic` | **атомарно** | **читателя НЕТ** (грепом по зоне — ноль вхождений) | ⚠ канал существует и не потребляется; полная таблица подписи платформе сегодня не нужна, она проецирует банк | - | `.bank-stop.txt` | `pipeline/mining.go:195` `os.WriteFile` | **НЕ атомарно** (усечение первым делом) | читателя нет | ✅ и это важно: брать его на живом прогоне нельзя — исправление пинга №15 оркестратором подтверждается кодом | - | `.mined-signature.yaml` | `pipeline/mining.go:188` `os.WriteFile` | **НЕ атомарно** | читателя нет | ✅ то же | - | `.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 нет**. Дерево не закоммичено и не - застейджено. Миграции пака: **00019–00024**. - - ### Что построено и зачем - - 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 (21–22.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 (две линзы — другой моделью; исполнением,