Second cut pass in the platform zone: restore what the review found lost, and take the sediment the first pass left behind

This commit is contained in:
heaven 2026-09-02 11:49:08 +03:00
parent 1d84cff75e
commit 21de1779ba
9 changed files with 270 additions and 347 deletions

View file

@ -2,31 +2,31 @@
> Ведёт зона `platform/` (решение владельца 02.08, D39.84: фронт и платформа держат СВОИ бэклоги; единый бэклог `docs/PROGRESS.md` остаётся трекером движка/полигона/доков и фронт/платформа-строк не принимает). Нормы те же: ID стабилен навсегда, каждая петля получает диспозицию. Запросы к ДВИЖКУ сюда не пишутся — они заходят строками единого бэклога через оркестратора (пример: строки 99102). Засеян оркестратором при лендинге D39.84 — дальше правит платформа-сессия.
> **Диспозиции после P1 (05.08)** в журнале зоны, раздел «Диспозиции бэклога зоны»
> (`docs/platform-PROGRESS.md`). Коротко: П-6 и П-8 ЗАКРЫТЫ, П-7 закрыт по схеме и операциям
> **Диспозиции после P1 (05.08)** — раздел «Диспозиции бэклога зоны» архив-слайса зонного журнала
> (`docs/archive/platform-PROGRESS-P0-P3.md`). Коротко: П-6 и П-8 ЗАКРЫТЫ, П-7 закрыт по схеме и операциям
> (постановка холда воркером — часть П-1), П-4 отменён и поглощён П-7, П-5 переопределён под
> кредитную модель и ждёт правки спеки, П-1 продолжена, П-2/П-3 не трогали.
> Дублировать их здесь не стали — у строки один источник истины.
| ID | Хвост | Вес | Источник |
|---|---|---|---|
| П-1 | **HTTP/SSE-слой и сервисная обвязка** (экс-строка 96 единого бэклога) — ⚠ **ЗАКРЫТА, живого хвоста нет (диспозиция 22.08, аудит доков).** Аутентификация, сессии и CSRF — P1; тейлер `events.jsonl` с курсором и карантином проекции, реконсилятор и пять ручек `/v0` — P4/P5; потребительская половина шва (коды выхода, фолд присваиванием, потолки) — P6. Последний остаток, читающая поверхность, был вынесен отдельной строкой **П-17** и исполнен паком P7 (D39.153), после чего «часть исполнена» перестало что-либо значить. Форма потока и аутентификации ратифицирована D39.84/D39.85 (`docs/research/23-engine-platform-seam.md`); порядок деплоя против schema-mismatch движка — `deploy/README.md`, §«Апгрейд ДВИЖКА» | закрыта | D39.81, D39.84, D39.85, STACK_DECISIONS §5 |
| П-1 | **HTTP/SSE-слой и сервисная обвязка** (экс-строка 96 единого бэклога) — ⚠ **ЗАКРЫТА, живого хвоста нет (диспозиция 22.08, аудит доков).** Что именно построено — `platform/README.md` §«Что здесь будет»; последний остаток, читающая поверхность, вынесен строкой **П-17** и исполнен P7 (D39.153). Форма потока и аутентификации ратифицирована D39.84/D39.85 (`docs/research/23-engine-platform-seam.md`); порядок деплоя против schema-mismatch движка — `deploy/README.md`, §«Апгрейд ДВИЖКА» | закрыта | D39.81, D39.84, D39.85, STACK_DECISIONS §5 |
| П-2 | **Глобальный брокер рейт-лимитов провайдеров** (экс-строка 97): гарды движка per-процесс (`pipeline/ratelimit.go:11`, mistral ~48% отказов под параллелизмом), а лимит провайдера — на ВЕСЬ аккаунт: N прогонов = N независимых гардов против общего лимита; воркер отпрашивается у платформы перед вызовом | ДО второго параллельного пользователя | D39.81, D39.84 |
| П-3 | **Очередь и конкурентность по книге** — ⚠ **ИСПОЛНЕНО P4 в той форме, которая была нужна:** River на том же Postgres, `MaxAttempts: 1` (повтор не доделывает работу, а спавнит второй движок — работу восстанавливает реконсилятор), одновременность по книге закрыта не лизами, а частичным уникальным индексом `runs_one_live_per_book` в БД: двух живых прогонов на книге не бывает, потому что движок держит эксклюзивный лок на файле проекта. **ОСТАЮТСЯ** ровно те части, которые нужны только со ВТОРЫМ хостом: пиннинг книги к хосту и лизы `book_leases` с heartbeat — сегодня хост один, и лиза без второго хоста охраняет от самой себя | до второго хоста | STACK_DECISIONS §5 и §19, D39.84 |
| П-4 | **Учёт токенов/денег per-user + бюджет-гейт ДО старта задачи** (сырьё уже считает движок: `request_log` + `internal/ledger`; помнить: леджер = нижняя граница — строка 78 единого) | Ф3-пак платформы | platform/README, D39.84 |
| П-6 | **OAuth-вход популярных провайдеров (Google первым) + модель аккаунта** (направление владельца 04.08): authorization code + PKCE, валидация ID-токена, связывание аккаунта по ПОДТВЕРЖДЁННОЙ почте; собственная серверная сессия сохраняется (D39.84) — OAuth даёт только СОБЫТИЕ входа. Форма и библиотеки — по ресёрч-пакету направления (ратифицирует оркестратор); писатель куки с `__Host`-атрибутами и dev-профиль — здесь же (PD-8) | P1, вместе с П-1 | владелец 04.08 |
| П-7 | **Кредитный баланс и фри-тир** (решения владельца 05.08; ПРОДАЖИ НЕТ — бета на предоплаченных ключах владельца): append-only леджер в целых микро-долларах, грант фри-тира из админки (дефолт $5, настраиваемый), холд ДО спавна прогона + пер-книжный потолок движку (жёсткий стоп исполняет движок), расчёт на границе попытки. ⚠ «дефолт $5» ПРОТУХ: слово владельца 16.08 (D39.138 п.2л, PD-104) — грант по умолчанию НОЛЬ, начисление руками; возврат $5 идёт вместе с суточным агрегатным потолком. ⚠ Окон с обнулением НЕТ — баланс, а не подписка: черновик `usage_windows` в подписочной форме заменяется. Платёжный провайдер не проектируется до решения продавать. Разбор — `docs/PLATFORM_DIRECTION.md` §2 | P1+, после П-6 | владелец 0405.08 |
| П-8 | **Минимальная админ-поверхность**: начислить/списать кредиты аккаунту (одна запись леджера), посмотреть баланс и состояние прогонов. Защищённая ручка либо CLI — форму предлагает сессия. Без неё фри-тир неуправляем | вместе с П-7 | владелец 05.08 |
| П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; сырьё у движка есть (`request_log`/`ledger`, потолки конфига), форму API предлагает P0 | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 |
| П-9 | **Загрузка книги `POST /books` (multipart)****ИСПОЛНЕНО P5 (11.08):** ручка построена потоково (`r.MultipartReader`, пер-маршрутный потолок тела, свой дедлайн чтения), хранилище — каталог книги под `TM_PLATFORM_BOOKS_DIR`, статусы `uploading → parsing → not_started \| rejected` получили писателей, разбор зовёт `tmctl manifest`, PD-72 закрыт своим тестом. Развилка «кто пишет стартовый `book.yaml` при интейке» (D39.110 §2b, шов `books.ErrNotProvisioned`) **РЕШЕНА D39.130:** бета = форма Б (стройка — П-14); движковая форма В (`tmctl init`) = строка 170 единого | Ф3, следующий пак | сессия P4 (границей промта D39.119 §5а), исполнено P5 |
| П-3 | **Очередь и конкурентность по книге** — ⚠ **ИСПОЛНЕНО P4 в той форме, которая была нужна:** River на том же Postgres, `MaxAttempts: 1` (довод — `STACK_DECISIONS` §19), одновременность по книге закрыта не лизами, а частичным уникальным индексом `runs_one_live_per_book` в БД. **ОСТАЮТСЯ** ровно те части, которые нужны только со ВТОРЫМ хостом: пиннинг книги к хосту и лизы `book_leases` с heartbeat — сегодня хост один, и лиза без второго хоста охраняет от самой себя | до второго хоста | STACK_DECISIONS §5 и §19, D39.84 |
| П-4 | **Учёт токенов/денег per-user + бюджет-гейт ДО старта задачи** — ⚠ **ОТМЕНЁН и поглощён П-7** (диспозиция P1, шапка выше). ⚠ Оговорка жива и после поглощения: леджер = НИЖНЯЯ граница траты (строка 78 единого) | отменён | platform/README, D39.84 |
| П-6 | **OAuth-вход популярных провайдеров (Google первым) + модель аккаунта** (направление владельца 04.08) — ⚠ **ЗАКРЫТ P1:** authorization code + PKCE, собственная серверная сессия сохраняется (D39.84 — OAuth даёт только СОБЫТИЕ входа), ключ личности `(provider, subject)`; код — `internal/login/`, состав и живой остаток — диспозиция из шапки. Писатель куки с `__Host`-атрибутами и dev-профиль — PD-8 | P1, вместе с П-1 | владелец 04.08 |
| П-7 | **Кредитный баланс и фри-тир** (решения владельца 05.08; ПРОДАЖИ НЕТ — бета на предоплаченных ключах владельца): append-only леджер в целых микро-долларах, грант фри-тира из админки (**дефолт НОЛЬ**, начисление руками — `tmplatformctl grant`), холд ДО спавна прогона + пер-книжный потолок движку (жёсткий стоп исполняет движок), расчёт на границе попытки. ⚠ Прежний «дефолт $5» ОТМЕНЁН словом владельца 16.08 (D39.138 п.2л, PD-104); возврат $5 идёт вместе с суточным агрегатным потолком. ⚠ Окон с обнулением НЕТ — баланс, а не подписка: черновик `usage_windows` в подписочной форме заменяется. Платёжный провайдер не проектируется до решения продавать. Разбор — `docs/PLATFORM_DIRECTION.md` §2 | P1+, после П-6 | владелец 0405.08 |
| П-8 | **Минимальная админ-поверхность**: начислить/списать кредиты аккаунту (одна запись леджера), посмотреть баланс и состояние прогонов — ⚠ **ЗАКРЫТ P1 формой CLI:** `tmplatformctl grant/adjust/balance/logins/revoke` (`cmd/tmplatformctl/`) | вместе с П-7 | владелец 05.08 |
| П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны». ⚠ **ПЕРЕОПРЕДЕЛЁН под кредитную модель (диспозиция P1, шапка выше):** окон и `resets_at` нет, форма ответа там же предложена, **ручка НЕ построена — ждёт правки спеки** | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 |
| П-9 | **Загрузка книги `POST /books` (multipart)****ИСПОЛНЕНО P5 (11.08)**: состав в `platform/README.md` §«Что здесь будет»; PD-72 закрыт своим тестом. Развилка «кто пишет стартовый `book.yaml` при интейке» (D39.110 §2b, шов `books.ErrNotProvisioned`) **РЕШЕНА D39.130:** бета = форма Б (стройка — П-14); движковая форма В (`tmctl init`) = строка 170 единого | Ф3, следующий пак | сессия P4 (границей промта D39.119 §5а), исполнено P5 |
| П-10 | **Честная оценка «$/глава» от ДВИЖКА.** Сейчас ставка — константа платформы ($0.03, провенанс exp08 v2 через D30.4, `STACK_DECISIONS` §20), и это осознанная бета-мера: движковой поверхности оценки не существует, а выдумывать её запрещено. Ставка решает только ДЛИНУ шкалы (деньги защищены холдом и потолком движка), но на книге, которая заметно дороже или дешевле средней, шкала врёт пользователю о том, сколько глав он покупает. Нужна оценка от движка по конкретной книге — запрос уходит строкой ЕДИНОГО бэклога через оркестратора, не сюда | когда-нибудь (до первого платящего) | сессия P4 |
| П-11 | **Наблюдаемость раннера****ИСПОЛНЕНО P5 (11.08):** `prometheus/client_golang` v1.24.1 на отдельном слушателе `TM_PLATFORM_METRICS_ADDR`; глубина очереди · возраст самого старого открытого холда · карантины · отставание тейлера · книги в интейке · длительность свипа и счётчик недоведённых проходов · запросы и задержки по паттерну маршрута. Ось наблюдаемости получила внешний эталон (практики именования Prometheus + золотые сигналы, `ENGINEERING_STANDARDS` §2) — половина PD-115, вторая половина строки остаётся открытой | Ф3 | сессия P4, исполнено P5 |
| П-12 | **Квота интейка и ретеншен отклонённых книг.** `POST /books` даёт аутентифицированному аккаунту писать на диск оператора: один аплоад ограничен (64 МиБ), число аплоадов — ничем. Отклонённая по вине источника книга каталог теряет, отклонённая по вине деплоя — сохраняет намеренно, и не чистит их никто. Строка регистра — PD-175 ⚠ **ПРОДУКТОВАЯ ПОЛОВИНА СНЯТА ЦЕЛИКОМ — D39.176 п.1 (слово владельца 30.08), диспозиция записана паком P12 31.08:** продуктовых КВОТ НЕТ и не будет, фри-тир-лимиты не проектируются, живём на покупке API и зачислениях из админки. То есть «сколько книг входит во фри-тир» — больше НЕ развилка и НЕ ждёт владельца; вопроса нет. Остаётся ИНЖЕНЕРНАЯ гигиена, решаемая зоной без чьего-либо слова: ретеншен отклонённых (`rejected`) книг · свип каталогов-сирот · потолок диска. Гейт у неё один и он не продуктовый — открытая регистрация, которой в закрытой бете нет. | инженерная гигиена, до открытой регистрации | сессия P5; продуктовая половина снята D39.176 |
| П-12 | **Квота интейка и ретеншен отклонённых книг.** `POST /books` даёт аутентифицированному аккаунту писать на диск оператора: один аплоад ограничен (64 МиБ), число аплоадов — ничем. Отклонённая по вине источника книга каталог теряет, отклонённая по вине деплоя — сохраняет намеренно, и не чистит их никто. Строка регистра — PD-175 ⚠ **ПРОДУКТОВАЯ ПОЛОВИНА СНЯТА ЦЕЛИКОМ — D39.176 п.1 (слово владельца 30.08):** продуктовых КВОТ НЕТ и не будет, фри-тир-лимиты не проектируются, живём на покупке API и зачислениях из админки; «сколько книг входит во фри-тир» — больше НЕ развилка и НЕ ждёт владельца. Остаётся ИНЖЕНЕРНАЯ гигиена, решаемая зоной без чьего-либо слова: ретеншен отклонённых (`rejected`) книг · свип каталогов-сирот · потолок диска. Гейт у неё один и он не продуктовый — открытая регистрация, которой в закрытой бете нет. | инженерная гигиена, до открытой регистрации | сессия P5; продуктовая половина снята D39.176 |
| П-13 | **Оценка размера книги в символах — у ДВИЖКА.** `character_count` контракта платформа считает потоково на приёме (байты, не являющиеся продолжением UTF-8), что точно для UTF-8 и приблизительно для GB18030/UTF-16, которые движок принимает и декодирует сам. Манифест несёт `source_bytes` и `encoding`, но не число символов. Запрос уходит строкой ЕДИНОГО бэклога через оркестратора; строка регистра — PD-177 **→ строка 171 единого заведена 14.08** | когда-нибудь | сессия P5 |
| П-15 | **ИСПОЛНЕНО P6 (14.08).** Платформенная половина шва эмиттера (движковая залендена D39.131, `9cfe080`) | ИСПОЛНЕНО P6: коды выхода движка 4/5/1019 через `ingest.OutcomeOf`, потолок = `paused` двумя каналами (PD-113 закрыт), интейк судит по коду (PD-196 закрыт), exit 5 без намерения = прерывание и перезапуск (PD-152 закрыт); фолд `unit_done` ПРИСВАИВАНИЕМ по тройке (chapter, unit, wave) через `unit_resolutions` (миграция 00015; ратифицировано D39.131 п.2г, at-least-once); StreamVersion 1.1, `Ceiling.Scope` (book\|day), внутренняя причина `daily_ceiling`, резюм дневного потолка = 409 (PD-157 половина); dev-супервизор читает ту же таблицу; foreign-hello adoption воспроизведён и закрыт (PD-200); порядок деплоя — `deploy/README.md` + `tmplatformctl books --migratable`. ⚠ Граница присваивания (PD-219): оно самолечит ПОВТОРНУЮ доставку, а не пропущенную — недодрейненный хвост новая попытка не перечитывает, её курсор начинается с размера журнала на допуске | приёмка D39.131 |
| П-14 | **ИСПОЛНЕНО P6 (14.08).** Стройка интейка формы Б (ратификация D39.130) | ИСПОЛНЕНО P6: рендер в ОДНОМ месте (`books.provision`), `TM_PLATFORM_BOOK_TEMPLATE`, работа с YAML-узлом (комментарии и незнакомые ключи оператора живы, значения — строки), `O_EXCL` (никогда не перезаписываем), битый шаблон = класс, который ЖДЁТ. Требования к полям — `backend/internal/config/book.go` как справочник; шаблон — деплой-артефакт оператора, не код. При приходе формы В (строка 170 единого, `tmctl init`) рендер заменяется вызовом движка. Проверено настоящим `tmctl manifest` (3 главы) | ратификация D39.130 (приёмка P5) |
| П-16 | **ИСПОЛНЕНО P6 (14.08).** Дев-сид стенда: тестовый пользователь, чтобы фронт разрабатывался на живой платформе, а не на своих моках (слово владельца 14.08). Границы: фикстуры фронта остаются батарее его гейтов (детерминированные состояния — не работа стенда); полный снос `frontend/src/mock/` — триггер Ф-29, после читающей поверхности | ИСПОЛНЕНО P6: `tmplatformctl seed` (дев-вход → аккаунт → грант → загрузка через ЖИВОЙ `POST /v0/books` → ожидание конца интейка), дев-вход `TM_PLATFORM_DEV_LOGIN` с четырьмя гардами непроходимости в проде, рецепт страницей в `deploy/README.md`. Прогнано на стенде целиком | владелец 14.08 |
| П-17 | **ИСПОЛНЕНО P7 (1617.08).** Читающая поверхность — остаток П-1, названный отдельной строкой | ИСПОЛНЕНО P7: `listChapters`/`listUnits`/`listNotes`/`listBankTerms`/`streamBookEvents`/`getCapabilities` по канону 0.3.0 · модель ошибок с машинным `code` · условные чтения и сжатие · `Idempotency-Key` · `structure_version` · материализатор `internal/readmodel` (манифест + экспорт + сайдкар банка). ⚠ `submitBankDecisions` СНЯТ 22.08 вместе с пер-термной моделью подписи — D39.144, слово владельца; см. PD-370. **НЕ вошло и отложено в P8:** `updateBook`/`deleteBook`/`getRun`, экспорт (`createExport`/`getExport`), эскроу (П-18) | П-1, D39.84/85, контракт 14 |
| П-14 | **ИСПОЛНЕНО P6 (14.08).** Стройка интейка формы Б (ратификация D39.130) | ИСПОЛНЕНО P6: рендер в ОДНОМ месте (`books.provision`); правила шаблона и его отказов — `deploy/README.md` §«Шаблон книги», требования к полям — `backend/internal/config/book.go` как справочник. При приходе формы В (строка 170 единого, `tmctl init`) рендер заменяется вызовом движка. Проверено настоящим `tmctl manifest` (3 главы) | ратификация D39.130 (приёмка P5) |
| П-16 | **ИСПОЛНЕНО P6 (14.08).** Дев-сид стенда: тестовый пользователь, чтобы фронт разрабатывался на живой платформе, а не на своих моках (слово владельца 14.08). Границы: фикстуры фронта остаются батарее его гейтов (детерминированные состояния — не работа стенда); полный снос `frontend/src/mock/` — триггер Ф-29, после читающей поверхности | ИСПОЛНЕНО P6: `tmplatformctl seed` и дев-вход `TM_PLATFORM_DEV_LOGIN` с четырьмя гардами непроходимости в проде — рецепт и разбор гардов в `deploy/README.md` §«Дев-стенд». Прогнано на стенде целиком | владелец 14.08 |
| П-17 | **ИСПОЛНЕНО P7 (1617.08).** Читающая поверхность — остаток П-1, названный отдельной строкой | ИСПОЛНЕНО P7 по канону 0.3.0; состав поверхности — `platform/README.md` §«Что здесь будет» и §«Карта зоны» (`internal/readmodel`). ⚠ `submitBankDecisions` СНЯТ 22.08 вместе с пер-термной моделью подписи — D39.144, слово владельца; см. PD-370. **НЕ вошло и отложено в P8:** `updateBook`/`deleteBook`/`getRun`, экспорт (`createExport`/`getExport`), эскроу (П-18) | П-1, D39.84/85, контракт 14 |
| П-18 | **Эскроу денег шва: write-ahead intent · `uncertain` · `closing`** (строка 136 единого бэклога) — денежный промт, сознательно НЕ взятый ни P5, ни P6. Здесь же закрывается PD-154 (`settled_at` между `Settle` и `MarkSettled`): половина эскроу рядом с проектируемым целым — второй, более слабый ответ на тот же вопрос | денежный промт | строка 136, PD-154 |
| П-19 | **Конверсия свободного от склейки блока под `sqlc`** (решение владельца 20.08 «БЕРЁМ»; граница названа P8-FIX, PD-44). Пять файлов без единой склейки — `credits.go`(15) · `identity.go`(13) · `idempotency.go`(7) · `sessions.go`(5) · `observe.go`(1) = **42 места / 41 текст / 40 КОНВЕРТИРУЕМЫХ** (⚠ испр. D39.172: `observe.go` структурно не конвертируется — River мигрирует `river_job` сам; худший позиционный дрейф набора, семь `int64` подряд, остался рукописным): это весь кусок пакета, где инструмент силён и ничего не ломает. Остальные 25 склеенных мест недостижимы по построению (`lastRun` — девять потребителей, `nextRevisionOfThisBooksLibrary` — восемь), и это ровно та часть, где рантайм-ошибки и случались. Состав пака: `sqlc.yaml` + пин версии инструмента (как у `golangci-lint`) + генерённый код в дереве + гейт «сгенерённое актуально» + `WithTx` для одиннадцати денежных запросов, идущих внутри ЧУЖОЙ транзакции. ⚠ Класс «нет такой колонки» УЖЕ закрыт постоянным гейтом (`TestEverySQLStatementParsesAgainstTheMigratedSchema`, 162 оператора, включая склеенные), поэтому этот пак покупает типизированные скан-структуры и раннюю обратную связь, а не корректность. **РЕШЕНО владельцем 22.08: ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — изменений много, мешать их с чем-либо нельзя | отдельная сессия | PD-44, фикс-лист приёмки P7 п. 4, решение владельца 22.08 |
| П-20 | **Хвост PD-162 после P8-FIX: холд прогона, чей движок не ответит НИКОГДА, возвращается только рукой.** Пак дал оператору и диагноз (`tmplatformctl runs --stalled`, гейдж `tm_platform_runs_stalled`), и терминальный вердикт (`run abandon [--release-hold]`), и это сознательная граница: автоматически решать про деньги прогона, о котором нельзя спросить, — та же ошибка «я не смог спросить = его нет», только со счётчиком впереди. Автоматический ответ на этот вопрос и есть эскроу — write-ahead intent · `uncertain` · `closing` (строка 136 единого бэклога, П-18). Пока эскроу нет, ручка остаётся ручной, и это записано, а не подразумевается | вместе с П-18 | P8-FIX |

View file

@ -1,9 +1,9 @@
# platform — control plane (SaaS-слой)
Зона записи сессии «Платформа». Что построено и каким паком — секция «Что здесь будет» ниже и
шапка «Текущее состояние» зонного журнала. Направление зоны — `docs/PLATFORM_DIRECTION.md`, критерии приёмки —
`docs/ENGINEERING_STANDARDS.md`, дефекты — `docs/DEFECT_REGISTER.md`, зонный журнал —
`docs/platform-PROGRESS.md` (весь прогресс зоны здесь, решение владельца 04.08), стек —
шапка «Текущее состояние» зонного журнала. Направление зоны — `docs/PLATFORM_DIRECTION.md`,
критерии приёмки — `docs/ENGINEERING_STANDARDS.md`, дефекты — `docs/DEFECT_REGISTER.md`, зонный
журнал — `docs/platform-PROGRESS.md` (весь прогресс зоны здесь, решение владельца 04.08), стек —
`docs/STACK_DECISIONS.md`.
Батарея зоны: `make check` (build · vet · fmt · lint · test -race). `make vuln` и `make fuzz`
@ -19,9 +19,8 @@
её отвечает `GET /capabilities`, по ней же интейк отклоняет неподдерживаемую пару кодом
`unsupported_pair`. Какие пары существуют, решают ДАННЫЕ (пакет промптов оператора), а не список в Go.
**Пустой список отклоняет ВСЁ, а не пропускает всё** (акт 5 P7): «не объявлено ничего» и «объявлена
эта пара» — разные ответы, и прежнее послабление заставляло деплой принимать книги, которые он мог
только провалить. Инстанс, принимающий загрузки без единой ДОСТУПНОЙ пары, не стартует вовсе — это
второй конец того же правила, и он в силе и в бою, и в рецепте дев-стенда.
эта пара» — разные ответы. Инстанс, принимающий загрузки без единой ДОСТУПНОЙ пары, не стартует
вовсе — это второй конец того же правила, и он в силе и в бою, и в рецепте дев-стенда.
Бинари: `cmd/tmplatformd` (сервис) и `cmd/tmplatformctl` (админ: гранты, КОРРЕКТИРОВКИ (`adjust`), баланс, журнал входов,
отзыв сессий, дев-интейк `book add`, список книг для апгрейда движка `books` и список книг, чью
@ -122,20 +121,16 @@ Prometheus; на контрактную поверхность они не вы
| `internal/pricing`, `internal/money` | шкала глав и целые микро-доллары |
| `internal/metrics`, `internal/reqid`, `internal/jobs`, `internal/config`, `internal/gates` | телеметрия, id запроса, очередь, конфигурация, гейты тулчейна |
Каналы движка, которыми зона пользуется, — СЕМЬ. ⚠ Слов «и других нет» здесь нет намеренно:
исчерпывающий перечень с атомарностью живёт в `docs/STACK_DECISIONS.md`, «Инвентарь каналов
движка», и список ниже СЧИТАЕТСЯ при правке вместе с ним. `tmctl manifest --json` (структура),
`tmctl export --json --pairs`
(ТЕКСТ пар — единственный носитель), `<project_db>.bank.json` (банк), `tmctl status --json` (канал
ремонта; с лендингом `6ec9f8a` он ещё и оценивает пере-проход ДО покупки), `events.jsonl` (поток) и —
с лендингом пака «писатель книги» (D39.175) — `tmctl build` с файловым сайдкаром
`<project_db>.book.<fmt>`, чьи пути публикуются в `StatusArtifacts.book_files`. Шестой канал ещё никем
в зоне не потребляется: его будет читать дверь выдачи `createExport`/`getExport`, и она обязана
СТРОИТЬ (звать `tmctl build`), а не подбирать лежащий рядом файл — там копия прежней сборки
(D39.175 п.2). Седьмой — `tmctl bank-apply`: документ решений уходит движку файлом, отчёт
`tm-bank-report-v1` приходит обратно на stdout; это ЕДИНСТВЕННЫЙ канал, по которому зона ПИШЕТ в
проект движка, и потому у него своя дверь (`POST /books/{bookId}/bank/corrections`) и свой класс
отказов. Живой SQLite движка не читается никогда (D39.85).
Каналы движка, которыми зона пользуется, — СЕМЬ: `tmctl manifest --json` ·
`tmctl export --json --pairs` · `<project_db>.bank.json` · `tmctl status --json` · `events.jsonl` ·
`tmctl build` · `tmctl bank-apply`. ⚠ Слов «и других нет» здесь нет намеренно, и второй копии
перечня здесь тоже нет: атомарность каждого канала, колонка «потребляет ли платформа» и все
оговорки лежат ЕДИНСТВЕННЫМ носителем в `docs/STACK_DECISIONS.md`, «Инвентарь каналов движка». Оттуда же два правила, которые
дороже перечня: `tmctl build` (D39.175) в зоне ещё НИКЕМ не читается, и его будущая дверь выдачи
`createExport`/`getExport` обязана СТРОИТЬ, а не подбирать лежащий рядом файл — там копия прежней
сборки (D39.175 п.2); `tmctl bank-apply` — ЕДИНСТВЕННЫЙ канал, по которому зона ПИШЕТ в проект
движка, и потому у него своя дверь (`POST /books/{bookId}/bank/corrections`) и свой класс отказов.
Живой SQLite движка не читается никогда (D39.85).
## Чего здесь НЕ будет
@ -147,21 +142,21 @@ Prometheus; на контрактную поверхность они не вы
## Известное требование к движку (не забыть)
**Глобальный брокер конкурентности.** `pipeline/ratelimit.go` строит рейт-гарды НА ПРОГОН,
а лимит провайдера — на весь аккаунт. N параллельных пользователей = N независимых гардов
против общего лимита (в комментарии там зафиксировано: mistral валит ~48% вызовов под
параллелизмом). Воркер должен отпрашиваться у платформы перед вызовом. Это единственная
по-настоящему новая механика на стыке.
**Глобальный брокер конкурентности** — единственная по-настоящему новая механика на стыке:
`pipeline/ratelimit.go` строит рейт-гарды НА ПРОГОН, а лимит провайдера — на весь аккаунт.
Постановка, замер и вес — строка `BACKLOG.md` П-2.
## Стек
Пины, даты релизов и обоснования — [`docs/STACK_DECISIONS.md`](docs/STACK_DECISIONS.md) (зонный,
live-сверка 0405.08); общая записка по обоим новым сервисам — `../frontend/docs/STACK_DECISIONS.md` §5.
Коротко: Go 1.26.4 в `go.mod` — общий с движком floor ЯЗЫКА, при этом `toolchain go1.26.6` там же поднимает тулчейн (D39.130; `make version-check` СРАВНИВАЕТ версии, а не матчит) · стандартный `net/http` + `ServeMux` без
роутер-библиотеки · PostgreSQL 18 · pgx v5.10.0 · goose v3.27.3 · очередь River v0.42.0 на том же
Postgres — **подключена и работает** (P4) · вход `x/oauth2` v0.36.0 + `go-oidc/v3` v3.20.0 ·
`x/time` v0.15.0 для лимита на `/auth/login` · `govulncheck` отдельной целью.
Коротко: floor ЯЗЫКА в `go.mod` общий с движком, `toolchain` там же поднимает тулчейн выше
(D39.130; `make version-check` СРАВНИВАЕТ версии, а не матчит) · стандартный `net/http` + `ServeMux`
без роутер-библиотеки · PostgreSQL 18 + pgx · goose · очередь River на том же Postgres —
**подключена и работает** (P4) · вход `x/oauth2` + `go-oidc/v3` · `x/time` для лимита на
`/auth/login` · `govulncheck` отдельной целью. ⚠ Номера версий здесь намеренно не дублируются: их
единственные носители — `go.mod` и таблица пинов строкой выше.
**Redis не заводим нигде** — зафиксировано как архитектурное «нет».
Прогресс наружу — SSE, события **пушит воркер**, а не фронт опрашивает read-model.

View file

@ -183,34 +183,34 @@ ceilings:
## Апгрейд ДВИЖКА: порядок против деадлока (строка 174 единого бэклога)
Read-only команды движка отказывают файлу проекта СТАРЕЕ бинаря — **exit 13 и токен
`schema_mismatch found=N expected=M` на stderr** (форма финализирована D39.134; прежняя
цитата «schema vN … expects vM» в этом доке была МЁРТВОЙ — движок её не печатает), а
платформа зовёт `status` перед каждым спавном и на расчёте денег. Значит выкат нового
движка на хост с существующими книгами запирает их до миграции файла проекта.
Read-only команды движка отказывают файлу проекта СТАРЕЕ бинаря —
**exit 13 и токен `schema_mismatch found=N expected=M` на stderr**
(форма финализирована D39.134 п.4), а платформа зовёт `status` перед
каждым спавном и на расчёте денег. Значит выкат нового движка на
хост с существующими книгами запирает их до миграции файла проекта.
**Проверено исполнением на стенде (P7):** проектная БД, отведённая на схему v14 при
бинаре v15, даёт `tmctl status --json` → exit 13 с этим токеном; `tmctl migrate
--config <book.yaml>` делает пред-миграционный бэкап и переводит v14 → v15; тот же
`status` после неё — exit 0.
**`tmctl migrate` СУЩЕСТВУЕТ и заленден** — $0-команда движка, открывающая файл
проекта на запись без прогона: `backend/cmd/tmctl/migrate.go`, в диспетчере `main.go`,
свой код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»). Порядок ниже —
ДЕЙСТВУЮЩИЙ, а не «когда приедет».
**`tmctl migrate` СУЩЕСТВУЕТ и заленден** — $0-команда
движка, открывающая файл проекта на запись без прогона:
`backend/cmd/tmctl/migrate.go`, в диспетчере `main.go`, свой
код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»).
⚠⚠ **ВТОРАЯ, НЕЗАВИСИМАЯ причина не выкатывать движок раньше платформы: подъём ФОРМЫ
МАНИФЕСТА движка требует платформенного билда, иначе выбывает КАЖДАЯ новая книга.**
Платформа с P12 сверяет `manifest_version` с известной ей формой
(`ingest.KnownManifestVersion`, зеркало `manifestVersion` движка) и на незнакомую
отвечает НЕ-деструктивным классом `parser_unavailable` — файл пользователя цел, и это
осознанный выбор: без сверки переименованный ключ декодируется в нули, а ноль глав
интейк читает как «источник прочли, книги нет», то есть УДАЛЯЕТ аплоад (`PD-213`). Но
класс всё равно ТЕРМИНАЛЕН по бюджету попыток: движок впереди платформы ⇒ каждая
загруженная книга уходит в `rejected` после пяти попыток. Симптом — интейк массово
отклоняет при здоровом на вид движке; лечение — выкатить платформенный билд, знающий
новую форму. Правило то же, что у схемы хранилища: **сначала платформа, потом движок**,
и оба конца этого правила теперь записаны.
⚠⚠ **ВТОРАЯ, НЕЗАВИСИМАЯ причина не выкатывать движок раньше платформы: подъём
ФОРМЫ МАНИФЕСТА движка требует платформенного билда, иначе выбывает КАЖДАЯ
новая книга.** Платформа с P12 сверяет `manifest_version` с известной ей
формой (`ingest.KnownManifestVersion`, зеркало `manifestVersion` движка) и на
незнакомую отвечает НЕ-деструктивным классом `parser_unavailable` — файл
пользователя цел, и это осознанный выбор: без сверки переименованный ключ
декодируется в нули, а ноль глав интейк читает как «источник прочли, книги
нет», то есть УДАЛЯЕТ аплоад (`PD-213`). Но класс всё равно ТЕРМИНАЛЕН по
бюджету попыток: движок впереди платформы ⇒ каждая загруженная книга уходит в
`rejected` после пяти попыток. Симптом — интейк массово отклоняет при здоровом
на вид движке; лечение — выкатить платформенный билд, знающий новую форму.
Правило то же, что у схемы хранилища: **сначала платформа, потом движок**.
Порядок (действующий). ⚠ Ключевое: **`migrate` гоняется НОВЫМ бинарём** — старый уводит
файл в свою же схему, то есть не делает ничего, и деадлок остаётся. Поэтому бинарь
@ -359,9 +359,8 @@ export TM_PLATFORM_STATE_DIR=$HOME/.local/share/tmstand/state
export TM_PLATFORM_ENGINE_BIN=$HOME/.local/bin/tmctl
export TM_PLATFORM_CTL_BIN=$PWD/tmplatformctl # абсолютный: systemd отвергает иной
export TM_PLATFORM_BOOK_TEMPLATE=$HOME/.local/share/tmstand/book-template.yaml
# ⚠ ОБЯЗАТЕЛЬНО, как и в бою: инстанс, принимающий загрузки, обязан объявить хотя бы одну ДОСТУПНУЮ
# пару, иначе демон отказывается стартовать. Пустой список — это «не объявлено ничего», а не
# «пускать всё»: без гейта стенд принимал книги, которые мог только провалить.
# ⚠ ОБЯЗАТЕЛЬНО, как и в бою: без хотя бы одной ДОСТУПНОЙ пары демон, принимающий загрузки, не
# стартует. Правило и его причина — блок ⚠ `TM_PLATFORM_LANGUAGE_PAIRS` выше.
export TM_PLATFORM_LANGUAGE_PAIRS='zh>ru'
mkdir -p "$TM_PLATFORM_BOOKS_DIR" "$TM_PLATFORM_STATE_DIR"
@ -396,11 +395,9 @@ await fetch('/auth/dev-login', {method: 'POST'})
install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine/books
```
**`/metrics` не аутентифицирован** — его защищает только адрес привязки. Дефолт `127.0.0.1`, то
есть снаружи он недостижим; поднять его на внешний адрес значит открыть операционную форму деплоя
(глубина очереди, число прогонов, возраст холдов) всем, кто до него дотянется. Скрейпер живёт на том
же хосте либо ходит через тот же edge, что и API. Разбор — `docs/STACK_DECISIONS.md` §24, строка
риска — PD-179.
**`/metrics` не аутентифицирован** — его защищает только адрес привязки, и дефолт `127.0.0.1`
снаружи недостижим. Скрейпер живёт на том же хосте либо ходит через тот же edge, что и API; что
именно открывает внешний адрес и почему риск принят — `PD-179`, разбор `docs/STACK_DECISIONS.md` §24.
**Загрузка книги идёт минуты, и это касается edge-прокси.** Маршрут `POST /v0/books` принимает до
`TM_PLATFORM_MAX_UPLOAD_BYTES` (64 МиБ) и держит соединение до `TM_PLATFORM_UPLOAD_DEADLINE`
@ -420,11 +417,10 @@ install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine/books
## Где на сервере лежат книги
`/srv/textmachine` — корень библиотеки НА СЕРВЕРЕ. Решение владельца «книги живут в `~/books`»
относится к машине разработки: под этим юнитом домашние каталоги недоступны вовсе
(`ProtectHome=yes` подставляет пустой `/home` и детям-`tmctl` тоже), поэтому библиотека под
домашним каталогом на сервере просто не откроется — не «сработает медленнее», а не найдётся.
Оператору, которому это нужно, юнит называет ровно две строки замены (`ProtectHome=tmpfs` +
`BindPaths=`); других изменений не требуется.
относится к машине разработки: под этим юнитом домашние каталоги недоступны и детям-`tmctl` тоже
(`ProtectHome=yes`, проверено живым прогоном — §«Файлы»), поэтому библиотека под домашним каталогом
на сервере не «сработает медленнее», а НЕ НАЙДЁТСЯ. Кому это нужно, юнит называет ровно две строки
замены (`ProtectHome=tmpfs` + `BindPaths=`); других изменений не требуется.
## Чего здесь ещё нет

File diff suppressed because one or more lines are too long

View file

@ -42,8 +42,7 @@
**Наблюдаемость** — базовая линия **практики именования Prometheus** (базовые единицы: секунды и
байты; `_total` у счётчиков; единица в имени, не в лейбле) + **четыре золотых сигнала** SRE на
вопрос «что мерить» (внесено P5 по ратифицированному направлению PD-115: у оси появился внешний
эталон, а не только собственная проза зоны). Конкретика:
вопрос «что мерить» (`PD-115`). Конкретика:
- структурные логи (route-pattern, не сырой путь — id пользователя/книги в логи не текут),
`X-Request-Id` на каждом ответе, ошибки видимы на своём уровне;
- метрики отдаются в формате Prometheus на ОТДЕЛЬНОМ слушателе (`STACK_DECISIONS` §24); лейбл несёт

View file

@ -50,9 +50,7 @@ hosted IdP (связка PII + доступность, а свою сессию
п.2л, PD-104): дефолт `TM_PLATFORM_SIGNUP_GRANT_USD` = **0**, кредит начисляется руками
(`tmplatformctl grant`); возврат автоматического гранта идёт ВМЕСТЕ с суточным агрегатным потолком,
который его ограничивает, а тот принадлежит платежам. Платёжный провайдер не выбирается, не проектируется и в бэклог зоны как
работа не заходит; вернуться — когда появится решение продавать. ⚠ Прежняя редакция этого раздела
несла вывод «покупателям из России платить нечем» — он был ВЫВЕДЕН из целевого языка перевода, а не
установлен, и снят как необоснованный (владелец 05.08). Язык книги о географии плательщика не говорит.
работа не заходит; вернуться — когда появится решение продавать.
**Модель лимита — БАЛАНС кредитов, а не окна с обнулением** (владелец 05.08: «не подписки, а покупка
токенов как у OpenRouter»). Следствия, несущие для схемы и контракта:
@ -69,7 +67,7 @@ hosted IdP (связка PII + доступность, а свою сессию
фри-тир. Нужна минимальная админ-поверхность — защищённая ручка или CLI-команда, пишущая грант.
**Схема (строить с П-7):** `credit_ledger` (append-only, знаковые целые микро-доллары, типы
`grant|hold|hold_release|settlement|adjustment`; `UNIQUE(user_id, source, source_id)` — ключ идемпотентности; ⚠ первая редакция этого абзаца называла `UNIQUE(source, source_id)`, и это ошибка: без `user_id` ключ, потраченный на одном аккаунте, проглатывает тот же ключ на другом, и второму сообщают «начислено», не начислив ничего (миграция `00007` и её комментарий);
`grant|hold|hold_release|settlement|adjustment`; `UNIQUE(user_id, source, source_id)` — ключ идемпотентности, скоупленный АККАУНТОМ (почему в нём обязателен `user_id` — комментарий миграции `00007_credits.sql`);
`purchase` добавится, если появится продажа), `reservations` (одна открытая на `engine_run_id`),
`account_balances` (кэш в ТОЙ ЖЕ транзакции, что вставка в леджер, + тест-инвариант
`balance == SUM(ledger)`). Правки строк не существует: ошибка чинится новой записью. Только целые
@ -78,13 +76,12 @@ hosted IdP (связка PII + доступность, а свою сессию
**Защита и свежесть — ДВА РАЗНЫХ механизма, строим ОБА (решение владельца 05.08 «сделаем нормально»;
моё прочтение его слов — записано так, чтобы дешёво поправить, если прочитал не то):**
1. **Защита — холд + потолок движку (было «вариант B»).** В той же транзакции, что допускает задачу
в очередь, ДО спавна `tmctl` ставим холд и передаём движку пер-книжный потолок ≤ остатка баланса.
1. **Защита — холд + потолок движку.** В той же транзакции, что допускает задачу в очередь, ДО спавна `tmctl` ставим холд и передаём движку пер-книжный потолок ≤ остатка баланса.
Жёсткий стоп исполняет САМ движок, у которого механизм уже есть ⇒ перерасход физически невозможен,
даже если платформа во время прогона слепа. Это рубеж корректности, и он НЕ зависит от доставки
событий (поток at-least-once и на краше теряет хвост).
2. **Свежесть — версионированное денежное событие в словаре потока (было «вариант A»).** Движок
эмитит НАКОПИТЕЛЬНЫЙ счётчик потраченного на границах стадии/волны; платформа материализует его
2. **Свежесть — версионированное денежное событие в словаре потока.** Движок эмитит
НАКОПИТЕЛЬНЫЙ счётчик потраченного на границах стадии/волны; платформа материализует его
существующим идемпотентным апсертом `(engine_run_id, seq)`. Накопительная семантика делает
повтор и дубль безвредными (берём максимум по прогону). Индикатор остатка перестаёт отставать
на целую попытку.
@ -100,16 +97,11 @@ hosted IdP (связка PII + доступность, а свою сессию
## 3. Стандарты и стек: что взять, что не брать
> ⚠⚠ **ВЕРХНИЙ СЛОЙ — ЧИТАТЬ ПРЕЖДЕ ВСЕГО НИЖЕ. По `sqlc` действует НЕ то решение, что записано
> в этом разделе:** владелец 20.08 сказал **БЕРЁМ** (D39.153 п.6а), 22.08 уточнил — отдельной
> сессией (D39.154 п.10), и 29.08 пак **принят и заленджен** (`D39.172`): сорок запросов пяти файлов
> `pgstore` вынесены в `internal/pgstore/queries/`, пин 1.31.1, `sqlc diff` в `make check`. Граница
> узкая по построению, а не по вкусу: свободный от склейки блок; остальное (25 склеек из 147, 15
> фрагментов-констант, read-модель) для sqlc недостижимо. Устройство, оговорки и цена —
> `STACK_DECISIONS.md`, строка «Кодоген SQL». Отказ P7 ниже остаётся как история пака, а не как
> действующее решение. ⚠ Тем же паком построена ЗАМЕНА того, ради чего sqlc
> звали: гейт `pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema` планирует ЖИВЫМ Postgres
> каждый собранный запрос пакета против мигрированной схемы — то есть покрывает и склейки, которых
> sqlc не видит, и именно в них случились оба рантайм-падения зоны.
> в этом разделе:** владелец сказал **БЕРЁМ** (`D39.153` п.6а, уточнение `D39.154` п.10), и 29.08
> пак **принят и заленджен** (`D39.172`). Устройство, пин и цена — `STACK_DECISIONS.md`, строка
> «Кодоген SQL»; граница инструмента и построенная взамен замена (гейт планируемости КАЖДОГО SQL
> пакета, включая склейки) — `DEFECT_REGISTER.md`, `PD-44`. Отказ P7 ниже остаётся как история
> пака, а не как действующее решение.
>
> ⚠ **ПЕРЕ-ПОДПИСАНО D39.132 п.2б:** `oapi-codegen` — КАНДИДАТ, решение за паком, который его
> берёт. **Решение P7 по обоим — НЕ БРАТЬ, с доводом, а не молчанием (история пака P7, 20.08):**
@ -129,7 +121,7 @@ hosted IdP (связка PII + доступность, а свою сессию
> тестами (`httpapi.Test*CarriesEveryRequiredField`).
Стек менять не надо: правильные куски уже взяты (pgx · goose · River · stdlib ServeMux/CSRF · slog ·
опаковые сессии). Добавить четыре вещи, пока репозиторий — скелет:
опаковые сессии). Четыре добавления и их сегодняшний статус:
| Добавление | Пин | Статус |
|---|---|---|
@ -169,39 +161,30 @@ zonky-стенд Postgres без root, пины линтера/govulncheck, из
feature-flag-платформы · CodeQL (на приватном репо платный) · gosec отдельным гейтом · SBOM/SLSA —
церемония на этом размере.
**Базовые линии безопасности:** OWASP **ASVS 5.0.0**, целевой **L2** — ⚠ поправка скептика к карте
глав: в 5.0 сессии это **V7**, аутентификация **V6**, и есть НОВАЯ глава **V10 «OAuth and OIDC»**
именно она написана под наше направление (ресёрч цитировал нумерацию 4.0 и главу V10 пропустил).
Плюс OWASP API Security Top 10 (2023) — API1 BOLA это буквально проверка владения книгой на каждом
`/v0`-маршруте, API4 — наш PD-2. Плюс уже действующие govulncheck-гейт и пины Go-модулей с sumdb.
Dependabot — да (граф крошечный, шума нет).
**Базовые линии безопасности** — `ENGINEERING_STANDARDS.md` §2. ⚠ Поправка скептика, из-за которой
карта глав там именно такая: ресёрч цитировал нумерацию ASVS 4.0 и пропустил НОВУЮ главу **V10
«OAuth and OIDC»**, написанную ровно под наше направление. Сверх стандарта: govulncheck-гейт, пины
Go-модулей с sumdb, Dependabot — да (граф крошечный, шума нет).
**Тест-пол — тот, что поймал бы дефекты этой приёмки** (иначе стандарт бесполезен):
1. Тест на РЕАЛЬНО сконфигурированном `http.Server`, а не на mux под `httptest`: полу-кормленный POST
обязан получить закрытие по `ReadTimeout`; SIGTERM обязан дренировать запросы (сегодня падают оба
PD-2 и PD-9; ни один mux-тест этот класс не видит).
2. Инвариант сессии НЕЗАВИСИМЫМ оракулом: SHA-256 считается ВНЕ `auth.Digest`, плюс ассерт «плейнтекст
токена в БД не находится» (PD-1: свойство истинно, но моя посадка его не уронила).
3. Нативный фаззинг `go test -fuzz` на NDJSON-декодере, оракулы — инварианты PD-10.
4. Дисциплина посадок мутаций (`ENGINEERING_STANDARDS.md` §3.3) как постоянная проверка того, что
свойства ЗАПИНЕНЫ, а не просто истинны.
5. Контрактные тесты против OpenAPI-спеки и нагрузочные (k6) — при первых живых ручках, не раньше.
**Тест-пол.** Четыре его пункта исполнены и живут нормой, а не планом: тест на РЕАЛЬНО
сконфигурированном `http.Server` вместо mux под `httptest` (`PD-2`, `PD-9`), инвариант сессии
НЕЗАВИСИМЫМ оракулом (`PD-1`), фаззинг NDJSON-декодера (`PD-10`) — эти строки регистра `fixed`,
каждая со своим пином; дисциплина посадок мутаций — `ENGINEERING_STANDARDS.md` §3. Полом остаётся
только пятый пункт: контрактные тесты против OpenAPI-спеки и нагрузочные (k6) — при первых живых
ручках, не раньше.
**Деплой:** одна VM + systemd-юниты, бинари артефактами CI; Postgres сперва там же, управляемый — при
первой выручке. Не Kubernetes: дети-`tmctl` живут ЧАСАМИ, держат эксклюзивный лок на файлах книги на
локальном диске, и любой оркестратор, способный переселить под посреди прогона, нам враждебен.
Юниты писать сейчас — они же честный ответ на PD-13 (осиротевшие процессы движка): прогон в
cgroup-поддереве платформы, `KillMode=mixed`, `Restart=on-failure`, `LoadCredential=` для DSN и
клиентского секрета OAuth, `MemoryMax`, песочница `ProtectSystem`/`PrivateTmp` бесплатно.
Юниты написаны — `deploy/tmplatformd.service`, и они же ответ на `PD-13` (осиротевшие процессы
движка); cgroup-поддерево прогона — `STACK_DECISIONS.md` §1516.
## 4. Скорость: что это требует от платформы и контракта
Замер снял главный страх: **пер-главный объём крошечный** — ~3.4 тыс. символов исходника + ~10.2 тыс.
перевода ≈ 27 КБ строк UTF-16 на главу, а контракт уже читает юниты ПО ГЛАВЕ, читает банк ДЕЛЬТОЙ
(`?after_version=`) и отдаёт экспорт ссылкой. ⚠ Прежняя формулировка «подписывает банк частями»
ЛОЖНА с D39.144: подпись — ОДИН акт над всем банком, а частичным является накопление РЕШЕНИЙ, что
совсем другая вещь. Риск не в экране чтения, а в четырёх местах, где сегодняшняя форма
вынудила бы клиента держать книгу целиком. Требования, которые платформа обязана исполнить:
(`?after_version=`) и отдаёт экспорт ссылкой. Риск не в экране чтения, а в четырёх местах, где
сегодняшняя форма вынудила бы клиента держать книгу целиком. Требования, которые платформа обязана исполнить:
1. **Сжатие ответов — ИСПОЛНЕНО P7** и записано нормой в канон 0.3.0, а не в этот док: JSON сжимается
там же, где строится тело (`internal/httpapi/conditional.go`), `text/event-stream` — никогда, и

View file

@ -1,12 +1,11 @@
# Стек платформы — пины и обоснования
> Зонный документ `platform/`. Пины сверены ЖИВЬЁМ 04.08 и 05.08.2026 (Go-прокси `@latest`,
> postgresql.org, go.dev/dl) — версии по памяти не называются. Библиотеки сессия не ратифицирует:
> таблица уходит оркестратору вместе с деревом.
> postgresql.org, go.dev/dl) — версии по памяти не называются. Порядок ратификации новой
> зависимости — `ENGINEERING_STANDARDS.md` §1.
>
> Общая записка по обоим новым сервисам — `frontend/docs/STACK_DECISIONS.md` §5 (02.08). Здесь —
> платформенная часть с датами релизов и сверкой; floor `go.mod` оставлен общим с движком, тулчейн
> сборки поднят выше — см. таблицу.
> Общая записка по обоим новым сервисам — `frontend/docs/STACK_DECISIONS.md` §5 (02.08); здесь —
> платформенная часть с датами релизов и сверкой.
## Пины
@ -19,9 +18,9 @@
| CSRF | stdlib `http.CrossOriginProtection` | Go 1.25 | Ровно тот механизм, что описан в §5 (Sec-Fetch-Site → Origin), теперь в тулчейне — свой велосипед не пишем |
| Postgres-драйвер | `github.com/jackc/pgx/v5` **v5.10.0** | 03.06.2026 | Живой pool, `pgconn.PgError` для проверки констрейнтов, `stdlib` для goose |
| Миграции | `github.com/pressly/goose/v3` **v3.27.3** | 22.07.2026 | Библиотекой + `embed.FS`; `WithSessionLocker` = advisory-лок, две реплики выкатываются по очереди |
| Очередь | `github.com/riverqueue/river` **v0.42.0** | 31.07.2026 | В `go.mod` и в работе с P4 (§19): задание очереди выдаёт только РАЗРЕШЕНИЕ стартовать, а жизнь прогона ведёт реконсилятор. Мигрируется своим мигратором — две летописи в одной базе |
| OIDC-вход | `golang.org/x/oauth2` **v0.36.0** + `github.com/coreos/go-oidc/v3` **v3.20.0** | 11.02.2026 · 08.07.2026 | Ратифицировано `PLATFORM_DIRECTION.md` §1; сверено живьём 05.08. Протокольный риск (PKCE, JWKS с рефетчем по kid, проверка подписи/issuer/audience/exp) отдан библиотекам, интеграция и модель аккаунта — наши. Транзитивно приходит `go-jose/v4` v4.1.4 |
| Рейт-лимит в процессе | `golang.org/x/time` **v0.15.0** | 11.02.2026 | `rate.Limiter` на ОБЕИХ неаутентифицированных ручках, которые ПИШУТ: `/auth/login` (строка состояния) и `/auth/callback` (строка журнала на каждом отказе — замерено ~880 строк/с с одного хоста, пока лимита не было). Долговечные пер-пользовательские лимиты — в Postgres, когда появятся |
| Очередь | `github.com/riverqueue/river` **v0.42.0** | 31.07.2026 | В `go.mod` и в работе с P4; устройство и цена формы — §19 |
| OIDC-вход | `golang.org/x/oauth2` **v0.36.0** + `github.com/coreos/go-oidc/v3` **v3.20.0** | 11.02.2026 · 08.07.2026 | Ратифицировано `PLATFORM_DIRECTION.md` §1 (там же — что отдано библиотекам и что пишем сами); сверено живьём 05.08. Транзитивно приходит `go-jose/v4` v4.1.4 |
| Рейт-лимит в процессе | `golang.org/x/time` **v0.15.0** | 11.02.2026 | `rate.Limiter` на ОБЕИХ неаутентифицированных ручках, которые ПИШУТ: `/auth/login` (строка состояния) и `/auth/callback` (строка журнала на каждом отказе — замерено ~880 строк/с с одного хоста, пока лимита не было) |
| Линтер | `golangci-lint` **2.12.2** | 06.05.2026 | Тот же пин, что у движка: находки версионно-зависимы, разъезд пинов = разные гейты в одном репо |
| Кодоген SQL | `sqlc` **v1.31.1** | 22.04.2026 | **Инструмент разработчика, НЕ зависимость модуля** — в `go.mod` не входит, рантайм-граф не растёт ни на один пакет; пин держит `make tools-check`, как у линтера, и по более острой причине: генерённый код лежит В ДЕРЕВЕ, поэтому другая версия молча даёт другой диф и `sqlc diff` краснеет на чистом клоне. **Зачем:** генерирует слой запросов свободного от склейки блока `internal/pgstore` (40 запросов из `queries/*.sql`) — SQL и его `Scan` перестают быть двумя списками, которые сверяет человек ПО ПОЗИЦИИ. Взят по решению владельца 20.08 (`D39.153` п.6а, уточнение `D39.154`), носитель — `BACKLOG.md` П-19. ⚠ Довод «гейт `sqlgate_test.go` это уже закрыл» ПРОВЕРЕН и не подтвердился: гейт получает только СТРОКУ SQL и Go-сторону вызова не видит — шесть посаженных перестановок целей `Scan` и сломанных арностей ВЫЖИЛИ на полной батарее (замер 29.08, `platform-PROGRESS.md`). Схему читает ТОТ ЖЕ каталог `migrations/`, что гейтит манифест (второго носителя нет); деньги держит подстановочный override `*.*_micro_usd``money.MicroUSD` — без него генератор выдаёт голый `int64`, с ним снятие override становится ОШИБКОЙ СБОРКИ. Актуальность генерации гейчена дважды: `sqlc diff` в `make check` и `pgstore.TestEveryGeneratedQueryMatchesItsSourceFile` в батарее, который работает и без установленного sqlc |
| Уязвимости | `govulncheck` **v1.6.0** | 09.07.2026 | Отдельная цель `make vuln`, не часть `check`: ей нужна сеть, а батарея обязана быть зелёной на голом клоне офлайн |
@ -68,7 +67,7 @@
> Значит `DownTo(<5)` на живой базе падает. Править `00005` нельзя — это и есть append-only, —
> а новая миграция чужой down-текст не заменяет. Данные при этом целы: down транзакционный,
> `Up()` возвращает схему на текущую версию (проверено прогоном: `DownTo(4)` падает на
> `users_email_key`, SQLSTATE 23505). Найдено ревью P2, перепроверено зоной, принято как цена правила.
> `users_email_key`, SQLSTATE 23505). Принято как цена правила.
9. **Ключ личности — `(provider, subject)`; почта не ключ.** `users.email` стала NULLABLE и БЕЗ
уникального индекса; неизвестная пара всегда создаёт НОВЫЙ аккаунт. Разбор и цена решения —
в журнале зоны, раздел «Политика коллизии почты».
@ -81,7 +80,7 @@
Это первая половина ключа личности. Направить `TM_PLATFORM_OIDC_ISSUER` на другой IdP, оставив
имя прежним, — значит сложить `sub` нового провайдера в старое пространство имён, то есть тихо
связать чужие аккаунты. Переменная называется здесь, потому что в деплой-примере её не было и
оператору нечему было напомнить (найдено ревью P2).
оператору нечему было напомнить.
11. **Секреты — через `*_FILE`.** `TM_PLATFORM_DSN_FILE` и `TM_PLATFORM_OIDC_CLIENT_SECRET_FILE`
читаются раньше одноимённых переменных: переменная окружения видна в `/proc/<pid>/environ` и
наследуется каждым ребёнком-`tmctl`. Это же формат `LoadCredential=` systemd (`deploy/`).
@ -156,9 +155,8 @@
**7.6.2 выполнено:** сессия создаётся только в колбэке потока, который человек начал явным
действием, и провайдер показывает свой экран согласия. Без взаимодействия сессия не появляется.
**7.3.1 / 7.3.2 — прицел, которого этому разделу не хватало.** 7.1.x требуют ДОКУМЕНТА, и выше
он написан; сами механизмы требуют другие две строки, и на них до 08.08 не ссылался ни один
наш док. Дословно (ASVS 5.0 V7, обе — уровень **2**): 7.3.1 — «Verify that there is an inactivity
**7.3.1 / 7.3.2 — про МЕХАНИЗМ, а не про документ, которого требуют 7.1.x.** Дословно
(ASVS 5.0 V7, обе — уровень **2**): 7.3.1 — «Verify that there is an inactivity
timeout such that re-authentication is enforced according to risk analysis and documented
security decisions»; 7.3.2 — то же про «absolute maximum session lifetime». ⚠ Читать точно: они
требуют не КОНКРЕТНОЙ величины, а того, чтобы механизм СУЩЕСТВОВАЛ и принуждал к повторной
@ -236,7 +234,7 @@
24. **Метрики — `prometheus/client_golang` v1.24.1, на ОТДЕЛЬНОМ слушателе, и это выбор против stdlib.** Норма зоны требует stdlib прежде библиотеки (`ENGINEERING_STANDARDS` §1), поэтому первым рассмотрен `expvar` — и он этой работы не несёт: нет лейблов (⇒ «запросы по маршруту и коду» не выражаются вовсе), нет гистограмм (⇒ на вопрос о задержке остаётся среднее — единственная статистика, которая прячет хвост), а его JSON не читает ни один скрейпер без переводчика. Сэкономил бы он зависимость, а стоил бы написания недостающих трёх руками — то есть ровно того самописного пути, который та же норма и запрещает. OpenTelemetry для этого деплоя тяжелее: коллектор процессом, протокол экспорта настройкой, и всё равно scrape-эндпоинт на конце. Пин сверен живьём 11.08 (`proxy.golang.org/@latest`), релиз 24.07.2026.
**Внешний эталон оси (половина PD-115):** практики именования Prometheus (базовые единицы — секунды и байты; `_total` у счётчиков; единица не в лейбле) плюс «четыре золотых сигнала» на вопрос «что мерить». Пин на форму — `metrics.TestTheRunnersStateIsExposedWithItsUnits`, он же ловит единицу в имени.
**Внешний эталон оси (половина PD-115):** практики именования Prometheus плюс «четыре золотых сигнала» на вопрос «что мерить»обе нормой в `ENGINEERING_STANDARDS.md` §2. Пин на форму — `metrics.TestTheRunnersStateIsExposedWithItsUnits`, он же ловит единицу в имени.
**Кардинальность:** лейбл несёт ПАТТЕРН маршрута, никогда путь — сырой путь это библиотека пользователя в индексе оператора (PD-3) и неограниченное число рядов; неразобранный запрос сворачивается в один ряд `(unmatched)`, потому что там лейбл выбирает не сервер. Пин — `TestRequestsAreCountedByRoutePatternAndNeverByPath`.
@ -284,8 +282,7 @@
СТАРТУЕТ (отказ конфигурации, не предупреждение) · личность берётся из конфигурации, а не из
запроса, поэтому «войти кем-то другим» через него нельзя в принципе · провайдер называется `dev`,
и в это пространство ключа `(provider, subject)` не может попасть ни один настоящий издатель.
Плюс WARN на буте и на каждый выданный сеанс. Разбор — доккоммент `internal/login/dev.go`;
security-ось ревью атакует именно этот список.
Плюс WARN на буте и на каждый выданный сеанс. Разбор — доккоммент `internal/login/dev.go`.
**Второй гейт того же класса, из приёмки P6 (PD-228):** дев-окружение экспортирует ДВЕ опасные
переменные, и отказ по одной ловил «стенд уехал в прод» наполовину. Поэтому
@ -315,7 +312,7 @@
ничего не мешает. Цена забывания названа замером: книга уезжает `finished_at`-нутой и НЕ
должной, очередь дрейна ключуется этой колонкой и больше на закрытую книгу не смотрит — то есть
прогон, за который пользователь заплатил, не показывает свой текст никогда. Две из трёх и
забыли; поймало ревью, пин — `pgstore.TestEveryEndingOfARunLeavesTheBookOwingASurface`, по
забыли; пин — `pgstore.TestEveryEndingOfARunLeavesTheBookOwingASurface`, по
таблице из трёх концовок.
34. **`books.read_model_owed_at` — это СРОК, а не момент возникновения.** Тот, кто берётся платить
@ -393,7 +390,7 @@
| `tmctl export --json --pairs` | движок (`pipeline/export.go`) | — (одноразовая выдача на вызов) | `internal/runner/engine.go` `ExportArgs`/`Export``ingest.DecodeExport``internal/readmodel` | ✅ ⚠ **Единственный канал, несущий ТЕКСТ пары** — исходник и перевод; манифест несёт только структуру |
| `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`; расчёт денег | ✅ форму P8-FIX не менял; изменил лишь то, СКОЛЬКО раз его зовут при неудаче (бэкофф отсрочки) |
| `tmctl status --json` | движок | — | `internal/runner/engine.go``ingest.StatusReport`; расчёт денег | ✅ ⚠ при неудаче зовётся не подряд, а с бэкоффом отсрочки |
| `book.yaml` | оператор (шаблон) + **платформа ОДИН раз** (`internal/books/render.go`, `O_EXCL`) | — | платформа пишет пять фиксированных ключей: `book_id · title · source_lang · target_lang · source_file` | ⚠ **декодер движка СТРОГИЙ** (`config/book.go`, `dec.KnownFields(true)`): незнакомый ключ — жёсткая ошибка, не предупреждение. Значит любое расширение набора ключей платформой это РАТИФИКАЦИЯ, а не правка: сборка движка, которая ключа ещё или уже не знает, перестанет грузить КАЖДУЮ новую книгу. Прямо относится к развилке 199(а) |
**Два следствия, которые стоит держать в голове при любой работе со швом.** Первое: относительные
@ -438,8 +435,8 @@ sed -e 's#^pipeline: ../configs/#pipeline: <repo>/backend/configs/#' \
system_messages not found in type config.CapabilitiesConfig`. Диагноз стоит времени именно потому,
что выглядит как дефект зоны: падает платформенный тест, а лжёт бинарь движка, собранный до того,
как в конфиг движка приехало поле. То же правило и той же причины — для КОНФИГОВ стенда, если они
скопированы рядом с бинарём: пак P12 нашёл стендовую копию `backend/configs` без `langpacks/ru`,
появившегося позже.
скопированы рядом с бинарём: стендовая копия `backend/configs` без появившегося позже
`langpacks/ru` (`PD-432`).
⛔ **Команда-проверка четвёртого условия СНЯТА и не подлежит восстановлению без нового диагноза.**
Прежняя редакция предлагала одну строку: `cut -d: -f3 /proc/self/cgroup` не должен давать

View file

@ -11,9 +11,9 @@ Postgres 18.4 без root (`~/.local/pgsql`, сокет `/tmp`, порт 55433)
`127.0.0.1:8099`, метрики `127.0.0.1:9464` · по своей копии дерева, своей базе и своей паре портов
на каждого из восьми субагентов.
Базовая линия батареи на дереве репозитория, ТРИ гейта выполнены (DSN · пара
`TM_PLATFORM_TEST_ENGINE_BIN`+`_BOOK_TEMPLATE` · достижимый пользовательский менеджер systemd):
**18 пакетов, EXIT=0, скипов 0, линтер 0 issues** — `battery-final.log`.
Базовая линия снята на дереве репозитория при ТРЁХ выполненных гейтах (DSN · пара
`TM_PLATFORM_TEST_ENGINE_BIN`+`_BOOK_TEMPLATE` · достижимый менеджер systemd пользователя);
числа прогона — строка `battery-final.log` таблицы ниже.
## ⚠ Два правила, без которых мутации читаются неверно

View file

@ -5,7 +5,6 @@
> читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4).
## ПИНГ оркестратора №21 → зоне (31.08, СРОЧНО): ваша копия полосы отказов обещает то, что движок ОТОЗВАЛ
Правку вносит ЗОНА. Пишу пингом, а не правкой, потому что `platform/` не моя зона — но откладывать
@ -950,12 +949,12 @@ HTTP-старт → настоящий спавн → настоящий вых
книга, чтобы исполнялся фильтр `r.book_id`). Раунд первый доказал, что `min` исполняется как ВЫБОР,
но не что исполняется книжный фильтр. Пропуск, а не подтверждение.
**ДВА флейка под нагрузкой, оба НЕ мой дифф.** При трёх параллельных батареях краснеют в ЧИСТЫХ
копиях `TestARunIsBoundedByItsOwnCgroup` (4 раза из 15) и
`TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError` (2). В серийных прогонах — ни разу, ни у
меня, ни у оркестратора. Оба меряют ресурс, общий для копий на машине. Именно поэтому вердикт —
ДЕЛЬТА множеств, а не код выхода: флейк, попавший в чистую копию, вычитается из посаженной. Заведено
строкой `PD-420`.
**ДВА флейка под нагрузкой, оба НЕ мой дифф** — при трёх параллельных батареях краснеют в ЧИСТЫХ
копиях `TestARunIsBoundedByItsOwnCgroup` (**4 раза из 15**; строка `PD-423` несёт другой замер — красноту
в ИЗОЛЯЦИИ и её причину) и `TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError` (**2 из 15**;
строка `PD-420`). В серийных прогонах — ни разу, ни у меня, ни у оркестратора: оба меряют ресурс, общий
для копий на машине. Отсюда и вердикт кампании — **ДЕЛЬТА множеств, а не код выхода**: флейк, попавший в
чистую копию, вычитается из посаженной.
**Одна посадка научила чинить не код, а ПИН.** `r_nocheck` в первом прогоне не дала ни одного
названного красного — она ПОВЕСИЛА пакет `internal/httpapi` на десятиминутном таймауте Go, потому что
@ -1005,18 +1004,13 @@ HTTP-старт → настоящий спавн → настоящий вых
9. **Живые сценарии сняты на ДЕВ-профиле** (`INSECURE_COOKIES`, `DEV_LOGIN`), то есть на кукe без
`__Host-` и без OIDC. Механизм отзыва от этого не зависит — он в сторе, — но «проверено на
проде-подобном профиле» я сказать не могу.
10. **⛔ Я убила чужие процессы.** Гася свою мутационную кампанию, применила `pkill -f 'go test'` и
`pkill -9 -f '/exe/'` — на общей машине это шаблон «все, кто сейчас работает». Попала по шести
ревью-агентам сессии `textmachine-e4` в окне **01:36:2001:37:45**, включая линзу, которая судит
покрытие посадкой мутаций: для неё убитый прогон читается как «мутация выжила», то есть я могла
подсунуть ей ложную НАХОДКУ. Сообщила ей сама, с точными границами; она пере-прогоняет. Своё
поведение изменила: PID заданий пишутся в файл, гашу построчно. Это моя ошибка целиком, и она
стоила чужого времени.
11. **Кампания посадок ДОШЛА до конца уже после первой редакции этого отчёта**, и её таблица — то
место, где отчёт дольше всего был неполон. Числа батареи, на которые ссылается таблица сдачи,
сняты в 02:04, а дерево правилось после (комментарии, дедлайн в тесте, текст миграции и её
контрольная сумма) — поэтому финальный прогон снят заново и таблица указывает на него.
12. **Не проверено вообще:** поведение при нескольких одновременных потоках одного пользователя под
10. **⛔ Я убила чужие процессы** `pkill -f 'go test'` / `pkill -9 -f '/exe/'`, гася свою мутационную
кампанию: шесть ревью-агентов сессии `textmachine-e4` в окне **01:36:2001:37:45**, включая линзу
покрытия посадками — для неё убитый прогон читается как «мутация выжила», то есть я могла
подсунуть ей ложную НАХОДКУ. Сообщила ей сама с точными границами, она пере-прогнала; PID заданий
теперь пишутся в файл и гасятся построчно. Общий урок — в списке уроков `docs/PROGRESS.md` (греп
«`pkill` по имени процесса») и в `STACK_DECISIONS` (греп «Демон нельзя убивать»).
11. **Не проверено вообще:** поведение при нескольких одновременных потоках одного пользователя под
отзывом (логически покрыто — проверка у каждого своя, — но живьём не снято) · `logout-all` на
дев-профиле (`PD-379` мимоходом сообщает про 404; я его не пере-проверяла, это строка группы
`PD-380…383`, которую пак не берёт) · поведение при недоступном Postgres в момент проверки
@ -1105,6 +1099,13 @@ FAIL/DATA RACE — 0; `golangci-lint` → **0 issues**;
деплоя (byte no-op проставит факт по already_applied-ветке).
- Консент-гейт движка живьём деньгами по-прежнему не пробит ($0-цены; кандидат строки 202) — из
прежнего Obstacle, не изменилось.
- ⚠ **Для оркестратора — находка опровергателя P10, носителя ни в регистре, ни в бэклоге у неё нет:**
якоря §2.12 компаньона контракта (`docs/architecture/14-api-contract/README.md`, греп
`pipeline/status.go`) ПРОТУХЛИ — волновая машинерия **D39.122** увезла деньги в `ChapterPassport` и
`StatusReport` (греп `type ChapterPassport` в `backend/internal/pipeline/status.go`). Там же довод
опровергателя, ради которого правка §2.12 ОБЯЗАТЕЛЬНА: два денежных поля из пяти (`Spend`,
`Reserved`) уже легально пересекают шов в заленженном аллоулисте, поэтому безусловное «§2.12
запрещает шов» делало бы их нарушениями задним числом.
## ⚠ ПОЧЕМУ ФОРМА P10 СО СМЕТОЙ БЫЛА ОТВЕРГНУТА — 42 находки/6 линз широкого самопрохода, три корня валят ФОРМУ (сессия платформы, 28.08)
@ -1204,94 +1205,36 @@ FAIL/DATA RACE — 0; `golangci-lint` → **0 issues**;
## ВОРКФЛОУ-РЕВЬЮ ДЕРЕВА P9 ОТРАБОТАНО — 16 линз, оба отложенных MAJOR подтверждены замером, сводка находок для оркестратора (сессия платформы, 28.08)
Заказ владельца (релей 28.08): адверсариальная вычитка дерева воркфлоу-оркестрацией — полоса ×4,
дверь ×3, миграция, раскладка кодов (`wf_cac14b2f-e84`) + гонки данных по 4 траекториям владельца,
баг-хант нового кода, стоимость per-request (`wf_155de7c3-bb4`). Раскладка моделей — по слову
владельца: 1×Fable на воркфлоу (самая тяжёлая линза), остальным явный opus/sonnet. 16/16 агентов
дошли (0 ошибок), ~2.83M токенов. Мандат: «найди, где рассуждение неверно», каждая находка —
severity + file:line + траектория с числами; PD-281, PD-401-остаток и пере-нарезка исключены
заданием как названные границы. **По находкам НИЧЕГО не чинил — жду слова оркестратора.**
Адверсариальная вычитка дерева воркфлоу-оркестрацией (заказ владельца, релей 28.08; журналы
`wf_cac14b2f-e84` и `wf_155de7c3-bb4` вне репо): 16 линз — полоса ×4, дверь ×3, миграция, раскладка
кодов, гонки данных по 4 траекториям владельца, баг-хант, стоимость per-request; раскладка моделей по
слову владельца (1×Fable на самую тяжёлую линзу, остальным явный opus/sonnet); 16/16 агентов дошли,
0 ошибок, ~2.83M токенов. Мандат: «найди, где рассуждение неверно». ⚠ Пометка «подтверждено прогоном»
в траекториях — исполнение АГЕНТОВ, не сессии; сессия пере-исполнила два клейма своего кода и одним
своим EXPLAIN — ось стоимости. **Лечение — таблица F1F13 секцией выше, приёмка — D39.162.**
Дисциплина честности: пометка «подтверждено прогоном» в траекториях — исполнение АГЕНТОВ (их
overlay-тесты и /tmp-замеры в копиях дерева), не моё; я пере-исполнил чтением два клейма своего кода
(Р4, Р5 ниже — оба подтвердились) и одним своим EXPLAIN-замером ось стоимости (сошлось, ниже).
Полные результаты с траекториями сохранены вне репо: журналы воркфлоу в каталоге сессии
(`subagents/workflows/wf_*/journal.jsonl`).
**Куда уехала каждая находка** (33 штуки; здесь остаётся ровно то, чего нет ни в F-таблице, ни в
строках). MAJOR(а) «1 МиБ на двух документах» → **F1**, MAJOR(б) «мьютекс без контекста» → **F2**.
Свой код P9: Р1 → F3 · Р2 → F4 (и dispute про READ COMMITTED растворён — стейтмент один) · Р3 →
**`PD-410`** (архитектурное, отдельным паком) · **Р4 ОПРОВЕРГНУТА исполнением** — несогласие 1 выше и
D39.162, держать её как находку нельзя · Р5 → F5 · Р6 → F6-F8 и **`PD-407`** · Р7 → F9 и **`PD-409`**
· Р8 → редакция **`PD-400.2`**. Наследие платформы: Н1 → F12 и **`PD-402`** · Н2 → **`PD-403`** ·
Н3 → **`PD-404`** · Н4 → **`PD-405`**, мёртвые колонки — **`PD-411`** · Н5 → F11 и **`PD-406`**.
Движковые: Д1 → строка **228** единого бэклога (пере-сформулирована бэкенд-сессией и принята), Д4 →
строка **227** (якорь находки был неверен и исправлен приёмкой), Д5 → F10. Ось стоимости: карточка в
5 сканов → **`PD-412`**, `emitProgress` под блокировкой → **`PD-413`** (оба несут замеры целиком);
строку **186** приёмка закрыть ОТКАЗАЛАСЬ — замер подтверждает шаги 1-2, шаги 3-5 живы (D39.162), и
сам замер стоит в строке.
### Два отложенных MAJOR ревьюера приёмки — оба ПОДТВЕРЖДЕНЫ, чинить
- cosmetic: `MkdirAll(StateDir)` на каждый вызов двери — место одному разу в конструкторе Service.
- **MAJOR(а) «1 МиБ на двух документах» — ПОДТВЕРЖДЁН тремя агентами с независимыми замерами.**
Механизм: `json.Marshal` в `ingest.EncodeDecisions` HTML-экранирует `&`/`<`/`>` (1 байт → 6) и
меняет конверт (+~25 Б), так что тело под проводным капом 1 МиБ рендерится в документ НАД
движковым капом 1 МиБ. Замер агента: 5000 declines с `note` из `&`×160 → тело 1 030 050 Б →
рендер 5 030 075 Б → движковый exit 14 → **409 `bank_corrections_refused` «split it» вместо
канонного 413**. Полоса законных тел широкая (~550 КБ плотного спецсимволами тела уже
перескакивает), а существующий пин `TestAnOversizedCorrectionDocumentAnswers413` дыру не ловит —
набит кириллицей, которая НЕ экранируется. ⚠ Конфликт линз: два агента первого воркфлоу положили
эту ось в «не опровергнуто», рассуждая от omitempty («рендер меньше тела») и НЕ меряя; замер двух
других бьёт рассуждение (один из «опровергателей» сам оговорил условие плотности `&<>`).
Бонус-cosmetic туда же: скраб пути (`runs/bank.go:146`) на этом отказе даёт задвоенную фразу «the
decision document the decision document» и пропускает на провод движковый префикс `pipeline:`.
- **MAJOR(б) «мьютекс без контекста, бюджет после захвата» — ПОДТВЕРЖДЁН** (`runs/bank.go:94` vs
`:126`, `lockBook` без ctx `:270-291`): K одинаковых ретраев дают очередь K×60 с; отменённый
клиентом запрос ЖДЁТ в очереди и, дождавшись, делает работу (чтение книги, temp-файл) за мёртвый
запрос; в ту же очередь встают Start и Resume — заявление «ждать один вызов движка»
(`runs.go:242-246`) держится только для одного ожидающего. Дёшево лечится ожиданием с
наблюдением ctx (канал вместо `sync.Mutex`).
### Находки в МОЁМ коде P9 (сверх двух MAJOR; лечение — после слова оркестратора)
**Две движковые находки БЕЗ носителя — это и есть живой остаток секции** (чужая зона, лечить не мне;
пинг оркестратору):
| # | Вес | Что | Где |
|---|---|---|---|
| Р1 | breaks | **Окно стопа подписи**: `bank_stop` ставит `awaiting_bank` БЕЗ `finished_at`, а `HasLiveRun` читает `finished_at is null` → до ближайшего свипа (15 с штатно; до 30 мин на бэкоффе отсрочек) дверь отвечает **409 `run_in_flight` на экран подписи, который платформа сама только что объявила**; соседняя кнопка Resume в том же окне отказывает ДРУГИМ словом («being settled»). Мой довод в `runs/bank.go:110-112` («its run is closed») прямо противоречит `reconcile.go:1122`; состояние строится собственным тестом дерева (`control_test.go:598-603`). Нашли НЕЗАВИСИМО 3 агента | `runs/bank.go:109` |
| Р2 | breaks | **Миграция 00026 для прогонов в полёте**: пере-снятие баз посреди прогона, уже сделавшего p_e правок, оставляет его финал на `runDonerunTotal = 2·p_e` — полностью доделанный прогон закрывается, например, 5/7 навсегда. Двух-базовая схема корректна ровно потому, что базы берутся ДО работы прогона; миграция — единственное место, берущее их ПОСЛЕ. + dispute: READ COMMITTED между двумя UPDATE допускает двойное касание строки при конкурентном финише (при штатном деплое одной реплики окно закрыто остановкой старого демона — вес мал) | `migrations/00026:20-27` |
| Р3 | breaks | **`draftWork` не моделирует порядок волн движка**: continuation над недочерновленной книгой (`draft_before ≥ chapters_before+C`, напр. прогон-предшественник упёрся в потолок на черновике) даёт `draftWork=0`, движок продолжает ЧЕРНОВУЮ волну — полоса 0/C весь прогон, stage «editing» на прогоне, который только черновит. Зеркало дефекта, который draftWork чинил | `readmodel.go:460` |
| Р4 | breaks | **`limitedBuffer` НЕ убивает чайлда** при переполнении stdout — вопреки своему комментарию (контраст: `drain()` в `engine.go:262-272` при том же переполнении явно зовёт `Process.Kill`); чайлд виснет на записи в полный пайп до чужого дедлайна (~70 с под пер-книжным мьютексом), без дедлайна — навсегда. Подтвердил чтением. + dispute: stderr — НЕограниченный `bytes.Buffer` (не-тот бинарь по сконфигурированному пути может раздуть демона до OOM; читается всё равно только первая строка) | `runner/bankapply.go:98-111` |
| Р5 | breaks | **Presence-vs-null дыра валидации**: только окна (`nullableInt`) различают явный `null` от отсутствия; `src/sense/dst/kind/id` — голые `*string`, так что `{"id":"X","src":null}` и decline с `"dst":null,"kind":null` проходят гейты, которые канонный `oneOf`/`not-required` велит отбивать 400. Подтвердил чтением (сам вводил `nullableInt` только для окон). Порчи данных нет — лишнее значение не читается; дыра контрактная | `httpapi/bank.go:48-51, 221-224, 251-259` |
| Р6 | dispute | **Раскладка на ветке SIGKILL**: `ctx.Err()`-ветка (`runs/bank.go:132`) достижима ТОЛЬКО когда SIGTERM не отработал (процесс убит по `WaitDelay`) — а это единственный путь, оставляющий полу-приземлённую пару БЕЗ отчёта, т.е. адресат `bank_corrections_incomplete`, отвечаемый общим 503; мой комментарий описывает соседнюю ветку. + `bankStopGrace` 10 с КОРОЧЕ самого длинного непрерываемого участка движка (11.3 с на документе-максимуме) — грейс истекает до фазы записи. + `runBudget` — операторская ручка свипа: понижение (к чему подталкивают комментарии) делает легальный документ-максимум навсегда неприменимым (вечный 503 вместо «split it») | `runs/bank.go:132`, `runner/bankapply.go:40` |
| Р7 | cosmetic | Утечка `bank-corrections-*.json` в StateDir при нечистой смерти демона (SIGKILL/OOM/обрыв грейса деплоя): cleanup только на defer, никто каталог не подметает; до 1 МиБ пользовательских решений на файл, бессрочно | `runs/bank.go:250` |
| Р8 | — | **Формулировка PD-400.2 (accepted-risk) требует правки**: на мульти-реплике класс 12 может быть НАСТОЯЩИМ многочасовым прогоном соседней реплики (её Start видит свой мьютекс и чистый `runs`) — слово 503 «transient holder» и мой лог тогда ЛОЖНЫ, канонно верное слово — 409 `run_in_flight`. «Ни денег, ни порчи» держится; «ни ложного слова» — НЕТ. Риск остаётся принятым (v1 = одна реплика), но акт лендинга должен описать его честно | `runs/bank.go:27-39, 182-189` |
### Находки в НАСЛЕДИИ платформы, вскрытые ревью (не код P9; полоса и дверь на них стоят)
| # | Вес | Что | Где |
|---|---|---|---|
| Н1 | breaks | **Resume НЕ-последнего прогона** (Resume пропускает любой `stopped` без проверки последнести; `lastRun` = `started_at desc`; RestartRun не трогает `started_at`): (i) полоса возобновлённого открывается на ЧУЖОЙ работе — 2/2 при нуле своих пассов (клэмп режет только >1, не двойной счёт до 1); (ii) карточка, кадр статуса и КАЖДЫЙ progress-кадр живого прогона считаются по ЧУЖОМУ финишировавшему — «ready» и замороженный бар, пока живой прогон тратит деньги. 4 агента, overlay-прогоны | `reconcile.go:1216`, `readmodel.go:487`, `runs.go:843` |
| Н2 | breaks | **`edit_wave` sticky-true × оператор убрал редактора**: прогон делает 100% купленного и закрывается ready на 50% со stage «editing»; `chaptersDone` (edit-колонка) мёрзнет → `ChaptersLeft` не падает → **шкала повторно продаёт уже переведённые главы** (симптом PD-202, записанной fixed); follow-up прогон над этой покупкой читается 0/C с первой секунды. Обе фразы комментария `sink.go:406-409` о собственном коде ложны | `sink.go:415`, `readmodel.go:388` |
| Н3 | breaks | **Флип false→true уводит `Book.chapters_done` НАЗАД** (finishedUnits через ЖИВОЙ флаг): 40/100 → 0/100 одной транзакцией при росте ревизии — клиент обязан отрисовать спад; `ChaptersLeft` раздувается обратно → предлагает купить купленное. PD-316-класс на непокрытом триггере (пин ловит только admission). Крайний случай: книга, полностью начерченная под false, НЕдочитываема редактором никаким действием API (ChaptersLeft=0 → прогон не допускается → флип недостижим) | `sink.go:415`, `readmodel.go:388`, `books.go:981-983` |
| Н4 | breaks | **Книга без материализованного дерева**: полоса (вся из `chapters`) читает 0/total весь прогон при исправно доезжающих progress-событиях; комментарий `sink.go:219` («счётчики придут из progress-события») ложен уже в HEAD. + dispute: `runs.draft_done/draft_total/edit_done/edit_total` — 2 писателя, 0 читателей (PD-314-класс, лишняя запись на каждом событии под блокировкой книги; обоснование `greatest()` на `sink.go:444-446` защищает несуществующую полосу) | `sink.go:219, 137, 447` |
| Н5 | breaks | **SSE `hello` при переподключении несёт `id = state.Position` вместо предъявленного `last`** — WHATWG-клиент фиксирует новый Last-Event-ID ДО получения догона; обрыв сразу после hello теряет кадры навсегда (включая `note`, которые контракт запрещает терять) и обезоруживает дельта-ремонт (`revision` из hello выше потерянных строк). Лечение — одна строка: при resuming слать `last`. Код рядом (`stream.go:122-126`) сам знает правильное значение | `stream.go:99` |
### Находки зоны ДВИЖКА (чужая зона — пинг оркестратору, мне не лечить)
| # | Вес | Что | Где |
|---|---|---|---|
| Д1 | breaks | **Decline не энтити-широк на обратном пути**: отклонённая сущность возвращается в карту/auto-bank/редактору через свой АЛИАС (`reverseSectionTerms` фильтрует по одному ключу, а `mined_rejects` получает только `src`), и память предъявления глушит стоп об этом НАВСЕГДА — нарушен собственный контракт «a declined term never re-enters» (`miner_emit.go:59-63`); approve симметричной утечки не имеет (кластер через строку банка) | `mining.go:548`, `membank/decisions.go:598` |
| Д2 | breaks | **Неидемпотентный decline** поверхности подписанного сида, имеющей строку в дельте: первый вызов ПРИНЯТ, повтор ТОГО ЖЕ документа — 409 (гейт `decisions.go:374` судит ДО-состояние, которое свёртка сама стирает); при классе 15 предписанный ре-сенд отбивается ЦЕЛИКОМ — **обещание сходимости 503-ретрая, на котором стоит синхронная дверь, ломается**. Доказано исполнением через СОБСТВЕННЫЙ оракул репозитория (оракул 4 фаззера); фаззер структурно не достаёт (фикс-книга не пересекает сид с дельтой). Лечение: судить по ПОСТ-состоянию, как соседний `refuseInertDeclines` | `membank/decisions.go:374` |
| Д3 | breaks | **Потеря/порча маркера выхода после стопа** (совместная с платформой): рестарт/ретрай проходит границу банка НАСКВОЗЬ — память предъявления покрывает карту, движок «continuing» одним WARN себе в журнал — оплаченный `verify_bank` стоп исчезает молча и навсегда (память append-only). Гард `LiftBankStop` (`reconcile.go:1122-1126`) писан против движка ДО памяти v16 и этот путь не держит | `reconcile.go:424`, `mining.go:216-227` |
| Д4 | dispute | **`signature` в квитанции двери** считается от карты, которую переписывает ЛЮБАЯ граница майнинга (запись карты на `mining.go:191` — ВЫШЕ решения о стопе): `signature != null` не означает состоявшегося стопа; `surfaces`/`undecided` дрейфуют между двумя вызовами владельца; `undecided: 0` достижим при непредъявленных решениях (кап top-200 вытесняет). У карты ЕСТЬ идентификатор (`signaturemap.go:25-29`) — шов его не читает | `pipeline/bankdecisions.go:549-598` |
| Д5 | cosmetic | `spawn.go:159-165` (платформа, но про движок) цитирует упразднённый D39.144-контракт стопа («halts whenever undecided terms remain») — при памяти v16 фактически ложно и питает дырявый гард Д3 | `spawn.go:159-165` |
### Ось «стоимость» — мои живые замеры + вердикт агента (сошлись)
- **Строка 186 единого бэклога ПРОТУХЛА — рекомендация оркестратору: закрыть.** Механизм построен
и применён: агент прогрепал ВСЕ call sites `writeJSON` (коллекции, карточка, capabilities, POST
двери; SSE корректно НЕ сжимается). Мои замеры на стенде tmp9stand (команды и заголовки — сырьём):
банк 437 Б / 7.2 мс холодным, повтор с `If-None-Match`**304 / 0 Б / 2.6 мс**; юниты главы
3008 Б → **740 Б gzip (×4.1)**; gzip ниже порога 1024 Б честно не применяется; resume SSE за
головой → 204 / 1.3 мс. Взамен предлагаю ДВЕ новые стоимостные строки (ниже).
- **Карточка книги — 5 коррелированных сканов `chapters` на один GET** (3 в runRow: draftChapters
×2 + editChapters, +2 в bookColumns: chaptersDone + noteCount), и те же 2 — на КАЖДУЮ строку
страницы библиотеки (×100). Эмпирика агента на живом PG 18.4, фикстура в форме миграции 00002,
книга 2283 главы: как написано — 1.359 мс; те же числа одним LATERAL `count(*) filter(...)`
0.307 мс (×4.4). Мой контрольный EXPLAIN на реальной схеме tmp9stand: **6 SubPlan-ов в плане,
5 исполняются** (ELSE-ветка never executed) — форма подтверждена. PG повторные текстовые
вхождения подзапроса НЕ дедуплицирует (доказано side-effect-последовательностью), CASE-ветки
честно short-circuit.
- **`emitProgress` повторяет 3-скан агрегат по ВСЕЙ книге на каждое progress-событие** (движок шлёт
его на каждый разрешённый юнит каждой волны) — ПОД блокировкой строки книги: O(глав × юнитов)
вместо O(юнитов), на большой книге — секунды суммарного удержания блокировки за прогон.
- cosmetic: `MkdirAll(StateDir)` на каждый вызов двери — место одному разу в конструкторе Service.
### Чистые оси (проверено — не опровергнуто)
@ -1306,27 +1249,6 @@ bankCountsTx; проекции без утечек словаря/путей; bu
термов через пересборку банка (id из ключа уникальности); сериализация чеканки кадров и штамповка
ревизий чисты; чтения на одном снимке; идемпотентный ключ Start не клинит за очередью мьютекса.
### Финальная батарея по дереву (после всех приёмочных правок; дерево ревью НЕ меняло)
`go test ./... -race -count=1 -v` с тремя гейтами → **EXIT=0, 18 пакетов ok, SKIP=0,
`^=== RUN` = 781**; DATA RACE — нет. (На сдаче было 775 — рост на приёмочных доборах, число
командой.)
### Вопросы/предложения оркестратору
1. Диспозиция Р1Р8: что чиню в этом дереве до лендинга, что строками регистра. Готов завести
пакет строк PD-40x по всем группам после твоего вердикта (не завожу до слова — рядом лендинг).
2. PD-400.2: скорректировать формулировку accepted-risk по Р8 (снять «ни ложного слова», описать
мульти-репличный суб-кейс) ДО акта лендинга.
3. Строку 186 закрыть (замер выше), взамен — две стоимостные строки (5-скан карточка ×100 на
библиотеку; emitProgress O(глав×юнитов) под блокировкой) куда решишь.
4. Д1Д5 — пинг бэкенду твоим каналом; Д2 ломает обещание, которое МОЯ дверь даёт на проводе
(сходимость ретрая 503) — до его лечения в движке слова канона §applyBankCorrections о ретрае
верны не для всех документов.
5. Н1Н5 — наследие: Н1 (resume не-последнего) и Н5 (hello id, лечение в одну строку) выглядят
дешёвыми и болезненными; Н2/Н3 упираются в продуктовое решение о смене формы конвейера на
живой книге.
## ПАК P9 ОТРАБОТАН — дверь правок банка смонтирована, ключи едут, полоса сквозная; цепь живого прогона ПРОБИТА живьём (сессия платформы, 27.08)
Дерево передаётся на лендинг. Опись: `git status --short -- platform/` → 31 изменённый + 9 новых
@ -1543,6 +1465,16 @@ mutation-catch (спутать пары «числитель×база»); ко
«до 17 раз» в `PD-377`, живая проба `PD-96`, экономика `PD-215`, кардинальность лейблов. Они
остаются названными пробелами, а не тихими.
**Числа сдачи зоны — провенанс, другого носителя у них нет** (ни в `D39.159`, ни в
`platform/docs/p8-review/`, где лежит только сырьё): посадок мутаций **16** своих, все воспроизводимы
(`plant.py` знает `M1``M16` поимённо, логи рядом) — **7 поймано, 9 выжило**; агентских **42** по их
таблицам (`agent-mutations.md`) — **21/21**. Уникальных переживших инвариантов **15**, каждый несёт
строку регистра.
**Метод-урок той же сдачи, и место ему не здесь:** обрезанный по ширине вывод грепа читается как
ОТСУТСТВИЕ совпадения — так зона объявила «грепа в промте ноль» на строке, которую греп нашёл. Дом
урока — `platform/docs/ENGINEERING_STANDARDS.md` (дисциплина доказательства грепом); перенос за тем,
кто правит тот файл.
**Об ошибках, которые вы назвали сами** (пять, от овер-атрибуции `PLATFORM_DIRECTION` до неполной
описи): называть их — правильно, и опись, недобравшая три файла из пяти, действительно стоила бы
лендеру правки НОРМЫ. Продолжайте так же.
@ -1746,33 +1678,47 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц
- **Потолок (строка 145): `tmctl translate|redrive --ceiling-usd <usd>` — это КНИЖНЫЙ ПОТОЛОК В СИЛЕ, не бюджет прогона** (ратифицировано D39.122): леджер сравнивает значение с накопленным committed+reserved книги. Пересчёт пользовательского «прирост в главах» (D39.110) в абсолют — ОБЯЗАННОСТЬ платформы. **⚠ АМЕНДИРОВАНО D39.123 (PD-158): формула = `committed_usd + прирост×оценка`, БЕЗ reserved** — read-only `status` показывает leftover-reserved, который `store.Open` зануляет до первой судимой резервации; включение переплачивало бы запасом сверх холда (исполнено обеими формулами против гейта). `reserved_usd` читается обязательным полем как гард присутствия и улика leftover. Значение ниже уже потраченного откажет первой же резервации. День-потолок не перекрывается. Опция по желанию зоны: движок готов провести `--ceiling-usd` и в `status`, чтобы мониторинг видел действующий потолок capped-прогона (сегодня status показывает книжный) — скажите, заведём строку.
## Пинг оркестратора №16 — 09.08.2026 (форма DEFECT_REGISTER, строка 167)
Реестр дефектов дорос до 171 строки одной таблицей; докс-аудит D39.125 (worksheet `docs/archive/reports/DOC_AUDIT_INVENTORY_2026-08-09.md`, жалоба «зонная изоляция прячет половины тем») просит форму: разложить таблицу на СЕКЦИИ (open по весу · accepted-risk · fixed по эрам паков), сохранив построчную форму `| PD-N | … |``docs/scripts/counts.py` ключуется формой строки, не позицией, секции его не ломают. Заведите П-строкой в свой BACKLOG, исполнение — попутно следующим паком, НЕ срочно. Вопросы — через владельца.
ИСПОЛНЕН: реестр разложен на секции (открытые по весу · принятый риск · закрытые по эрам паков) при
сохранённой построчной форме `| PD-N | … |`, на которой ключуется `docs/scripts/counts.py`. Заказ и
его причина — D39.125.
## Пинг оркестратора №17 — 15.08.2026 (migrate принят, D39.134 — движковая половина самолечения ФИНАЛЬНА)
Бэкенд-пак `tmctl migrate` принят и заленден (`d55edd4`): exit 13 = `schema_mismatch` финализирован, токен `schema_mismatch found=N expected=M` на stderr стабилен, write-путь тоже отказывает БД новее бинаря. Ваш PD-201 (самолечение «поймал 13 → migrate → повтор») теперь можно строить — движок больше не сдвинется под ногами; строку реестра, ссылающуюся на «движкового migrate ещё нет», обновите. Хвосты вашей зоны, найденные приёмкой (вход промта P7, исполнение попутно): (1) `deploy/README.md:131` цитирует МЁРТВЫЙ текст ошибки схемы («schema vN … expects vM») — движок его больше не печатает; (2) та же мёртвая цитата и уплывший якорь `store.go:135-143` в строке П-1 вашего BACKLOG; (3) комментарий-образец в `tmplatformctl` (`runs.go:45`) зовёт голый `tmctl` из PATH — копипаст воспроизводит тихий no-op СТАРЫМ бинарём при живом деадлоке, ваш же README:139-141 требует версионированный путь. Вопросы — через владельца.
Бэкенд-пак `tmctl migrate` принят и заленден (`d55edd4`): exit 13 = `schema_mismatch` финализирован,
токен `schema_mismatch found=N expected=M` на stderr стабилен, write-путь тоже отказывает БД новее
бинаря — то есть движок под ногами больше не сдвинется, и `PD-201` («поймал 13 → migrate → повтор»)
можно строить; строка регистра открыта и несёт предмет. Три хвоста зоны, найденные той же приёмкой
(мёртвая цитата текста ошибки схемы в деплой-рантбуке и в строке П-1 бэклога; образец в
`tmplatformctl`, зовущий голый `tmctl` из PATH), с тех пор закрыты правками — рантбук цитату сам
называет мёртвой, образец несёт версионированный путь.
## Пинг оркестратора №17 (второй) — 15.08.2026 (S4 принят, контракт 0.2.3 в каноне — пять строк вашего регистра протухли; аудит корпуса)
Фронт S4 принят и заленден (D39.135), контракт **0.2.3** в каноне. Для вашего регистра: (1) **PD-172 · PD-173 · PD-174 · PD-180 — диспозиция «спек-правка 0.2.3 задачей S4» (D39.130 п.2в) ИСПОЛНЕНА** — правила стоят в `openapi.yaml` §createBook (PD-172 при этом уточнён ПО ВАШЕМУ коду: чтение останавливается на файле, обязательное после файла = 400 «как не слали», необязательное молча теряется — D39.135 п.2б); обновите статусы. (2) **PD-199 ратифицирован закрытым ещё D39.132 п.2а** («null на проводе подтверждён»), в регистре до сих пор open — привести. (3) **Ваше НОВОЕ обязательство в P7** (D39.135 п.2в): читать `BookIntake.title` (сегодня падает в «unknown field is IGNORED», `v0.go:369-376`) и проецировать `books.reject_reason` на провод (сегодня «kept for an operator and never projected», `v0.go:497`) — без этого 0.2.3 остаётся обещанием без носителя. (4) Кандидаты в P7 по аудиту корпуса 15.08: **PD-219** (упавший дрейн теряет хвост `unit_resolutions` навсегда — зона сама вешала его на читающую поверхность) · **PD-217** (книга на вечном холде блокирует апгрейд движка — вторая половина после D39.132) · **PD-162** (удалённый каталог книги = вечный прогон с открытым холдом). (5) ⚠ Ваш же журнал честно фиксирует: «`tmctl migrate` живьём не гонялся» — команда теперь СУЩЕСТВУЕТ и принята (D39.134); мнение оркестратора: прогнать деплой-рантбук end-to-end на дев-стенде настоящим migrate — обязательное предусловие первого выката, дешевле любого пака. Вопросы — через владельца.
Фронт S4 принят и заленден (**D39.135**), контракт 0.2.3 в каноне. Пять статусов регистра (`PD-172`,
`PD-173`, `PD-174`, `PD-180` по диспозиции D39.130 п.2в; `PD-199` по D39.132 п.2а) с тех пор приведены —
все пять стоят закрытыми эрой P7. Обязательство 0.2.3 (`BookIntake.title` и проекция
`books.reject_reason` на провод, D39.135 п.2в) исполнено — `reject_reason` живёт членом ответа книги.
Три кандидата аудита 15.08 (`PD-219` упавший дрейн теряет хвост `unit_resolutions` · `PD-217` книга на
вечном холде блокирует апгрейд движка · `PD-162` удалённый каталог книги = вечный прогон с открытым
холдом) ОСТАЮТСЯ ОТКРЫТЫМИ строками регистра — предмет там, не здесь.
**Живым остаётся одно:** ваш журнал фиксировал «`tmctl migrate` живьём не гонялся», команда с D39.134
существует, и мнение оркестратора — прогнать деплой-рантбук end-to-end на дев-стенде настоящим migrate
как обязательное предусловие первого выката.
## Пинг оркестратора №17 (третий) — 16.08.2026 (контракт-ревью принято D39.138: ваша половина батча 0.3.0, PD-104 закрыт, попутные находки)
Контракт-ревью API v0 отработало отдельной сессией и ПРИНЯТО (отчёт — `docs/research/28-contract-review.md`; решения владельца — его §8, ратификация D39.138). ⚠ Читать ОРИГИНАЛ отчёта, не этот пересказ — он и есть носитель.
1. **PD-104 ЗАКРЫТ словом владельца 16.08** (§8 п.14): `SignupGrantMicroUSD`**0** на бете, начисление руками; возврат $5 — вместе с суточным агрегатным потолком, когда появятся платежи. Исполнение — P7-однострочник конфига + тест (грант при неверифицированном email уже 0, `login.go:306-308`). Обновите регистр.
2. **Ваша половина батча 0.3.0 — ПОСЛЕ лендинга спеки; состав — §5 отчёта:** `wireProgress` → один счётчик (Б-0; ваши колонки и словарь шва НЕ трогаются — фазы остаются внутренним делом); словарь машинных `code` + `request_id` в `Problem` (Б-1/§8а: класс 1 — конкретика максимальная, класс 2 модельный — ОДИН грубый код без вариации); `GET /capabilities` (Б-2: пары/порог интейка/форматы/страницы/версия — всё уже есть в конфиге и константах); `WWW-Authenticate` на 401 и две причины 403 (Б-15); трейлинг-части интейка → 400 вместо тихой потери + `Location` на 201 + `Idempotency-Key` (Б-3); подрезание `limit` до максимума вместо сброса к дефолту (Б-10, `books.go:509-511`); `PATCH /books/{id}` title-only + `DELETE` + `GET /runs/{id}` + `Run.failure_reason` (Б-5/Б-8 — словарь исхода у движка уже есть); `blocked: {code, book_id}` на run-options/409 (В-6); отказ по неподдерживаемой паре кодом на интейке (сегодня ja/en-книга умирает `failed`-ом на валидации конфига движка в КОНЦЕ пути — денег не сгорает, но причина не доезжает).
3. **Сеть (§5б; вход до первой живой книги под фронтом, строка 186):** gzip на текстовых ответах (НЕ на SSE) и `ETag`/`If-None-Match`→304 на списочных GET — ваше же требование `PLATFORM_DIRECTION.md:202-205`, батчем оно уезжает В КОНТРАКТ. Числа: дерево 2283 глав = 250 КБ на кадр `status`, фокус-рефетч 12 вкладок = 562 КБ.
Контракт-ревью API v0 отработало отдельной сессией и ПРИНЯТО (носитель — `docs/research/28-contract-review.md`,
решения владельца — его §8, ратификация **D39.138**; читать ОРИГИНАЛ, не пересказ). Три пункта пинга с тех пор
ЗАКРЫТЫ и здесь остаются только именами: `PD-104` (грант 0 на бете, начисление руками — §8 п.14) стоит закрытым
эрой P7 · половина батча **0.3.0** (состав — §5 отчёта) построена, канон с тех пор ушёл на семь миноров вперёд ·
шаги 1-2 сети (gzip и `ETag`/`304`) заленджены P7 (D39.153) и пере-проверены замером — числа живут строкой **186**
единого бэклога. Живыми остаются два пункта:
4. **К-10 (пофазность у главы): НЕ СТРОИТЬ** — поправка приёмки: вердикт §6 отчёта («правка проекции») противоречит Б-0; фазы уходят с провода и у главы.
5. **Попутные находки §9** (сессия их адверсариально НЕ судила — проверьте у себя, заведите П/PD-строки по месту): (а) расчёт может висеть навсегда в двух легаси-путях (`reconcile.go:660-670` — попытка без базовой отметки; дев-путь без движка) — бюджета «сколько холд может висеть» не существует; (б) флаг «аккаунт остановлен» выводится сканом последних прогонов ВСЕХ книг (`pgstore/books.go:720-724`) — одна книга зажигает аккаунт; после PD-203 перечитать; (в) `ReadRunForSpawn` линеен по живым прогонам (`pgstore/runs.go:712`); (г) `/metrics` без аутентификации, защита — только 127.0.0.1 (`main.go:200-202`); (д) лимитер входа один на процесс (`login.go:128-129,165`) — один клиент упирает вход всем (выбор объяснён комментарием, следствие стоит записать); (е) сырой Go-текст ошибки лежит причиной карантина в БД (`reconcile.go:320`) — на провод не идёт. Вопросы — через владельца.
**Аддендум к пингу №17-третьему (16.08, свип планировочных доков D39.139):** (1) `docs/PLATFORM_DIRECTION.md` §2 и `BACKLOG.md` П-7 держат «фри-тир дефолт $5» — ПРОТУХЛО против PD-104 (слово владельца 16.08: грант 0 на бете, начисление руками; D39.138 п.2л) — поправить тексты при P7. (2) Баннер §3 PLATFORM_DIRECTION («ратификация за оркестратором») закрыт ещё D39.132 п.2б: oapi-codegen — кандидат при P7, sqlc — привязан к P7, River-факт поправлен; баннер снять/заменить ссылкой на ноту. (3) §4 п.5 («сервер ВПРАВЕ склеивать события») сузится батчем 0.3.0: склейка только снимкам, `note` не склеивать (research/28 Б-6в). Вопросы — через владельца.
**Дозакладка к §4.6 промта P7 (16.08, аудит готовности зоны; релей владельца — подтверди эхом):** к списку протухших текстов зоны добавь: `PLATFORM_DIRECTION.md` §4 п.8 («живого канала до-прогонных состояний нет») — решено книжным SSE 0.3.0, опрос запрещён нормой · §4 п.10 («персист манифеста — недостающий») — построен D39.122 · `:184` ссылка на несуществующий §3.3 стандартов · `platform/README.md:3-4` шапка «зона на P1» и «один тест-гейт» (гейта два) · `BACKLOG.md` П-17 — состав пака у́же промта (промт первичен). Расхождение ENGINEERING_STANDARDS:59 против DIRECTION:128 по oapi-codegen решаешь ты (промт §4.6). Всё — попутно, не отдельным заходом.
## Пинг оркестратора №18 — 20.08.2026 (P7 акт 5 ПРИНЯТ С ФИКС-ЛИСТОМ и ЗАЛЕНДЕН; контракт 0.4.0 ратифицирован; тело — D39.152/D39.153)
**Вердикт: ПРИНЯТЬ С ФИКС-ЛИСТОМ.** Панель шести линз в изолированных копиях (слепая · контракт-конформность · деньги · шов · вне карты · ревью канона 0.4.0 другой моделью) + пере-раны и собственные посадки оркестратора. Регрессов против HEAD нет, ни одной линзы с REJECT.
@ -1787,27 +1733,40 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц
### Фикс-лист приёмки — завести строками СВОЕГО бэклога и регистра (зона моя не пишет)
**ПЕРВЫМ — не из пака P7, но найдено вторым рубежом приёмки и проверено мной построчно: `Sweep` останавливается для ВСЕЙ инсталляции на двух медленных прогонах, и выхода нет.** `cmd/tmplatformd/runner.go:189``sweepBudget = 2 * time.Minute` на ВЕСЬ проход; `internal/runs/reconcile.go:70``defaultRunBudget = 60 * time.Second` на ОДИН прогон. 120/60 = 2: два прогона, выбравшие свой бюджет, съедают проход целиком, и тогда `ctx.Err()` (`reconcile.go:39,48`) обрывает цикл — **`UnsettledRuns`, единственный ретрай отложенного расчёта, не вызывается вообще**, а живые прогоны, стоящие ниже, не реконсилируются и не спавнятся. `ListLiveRuns` сортирует `order by r.started_at` (`pgstore/runs.go:249`), то есть заклиненный прогон — по построению самый старый — стоит в голове и голодит остальных КАЖДЫЙ проход, детерминированно. Ручки нет: `runs.Config.RunBudget` объявлен (`internal/runs/runs.go:62-65`), но `startRunner` его НЕ присваивает (`cmd/tmplatformd/runner.go:74-83`), переменной окружения нет ни для одного из двух чисел, а `tmplatformctl` не умеет ни закрыть прогон, ни вернуть холд. Цена: прогон вечно `finished_at is null` ⇒ холд не возвращается ⇒ `runs_one_live_per_book` не даёт запустить новый прогон этой книги. Деньги заморожены, книга заморожена, пользователю видно «идёт».
**Достижимо буднично, без экзотики:** `RunSink.Apply` берёт транзакцию с блокировкой книги на КАЖДУЮ строку журнала (`pgstore/sink.go:80-104`), поэтому после часа простоя демона две книги с бэклогом выбирают проход целиком — и всё время догона деньги всей инсталляции не считаются.
**`PD-169` в вашем регистре стоит `fixed(P5)` и этим ЛЖЁТ приёмке:** его пин гоняет `Sweep` вообще без дедлайна прохода, то есть доказывает пер-прогонный бюджет, а не выживание прохода. Пере-открыть. Код приехал в P6 и в дифф P7 не входил (`Sweep` против HEAD — identical), поэтому ни одна диффовая линза его увидеть не могла: тот же класс «композиция известных фактов», что и строка 198. ⚠ Наблюдаемость при этом ЕСТЬ и она ваша: `tm_platform_sweep_unfinished_total` растёт — то есть оператор увидит, что проход не дошёл до конца, и не сможет ничего сделать. Метрика без ручки — половина механизма.
⚠ Список ИСТОРИЧЕСКИЙ — запись о том, что было верно 20.08; задним числом он не переписывается.
Пропуски в нумерации — снятые пункты, чьё исполнение проверяемо и чей предмет живёт по адресу:
**п.1** `ContractVersion` (поднята F13 до 0.6.0, канон сегодня 0.9.0) · **п.2** пины на
`structure_version`/`revision` в кадре (стоят с названной пойманной мутацией в `pgstore/events_test.go`) ·
**п.3** бюджет попыток материализации (`PD-357`, закрыт эрой P8-FIX) · **п.5** round-trip в
`SaveStructure` (`PD-297`, открытая строка несёт предмет) · **п.7** мусор `platform/ru` (снесён
оркестратором тем же заходом) · **п.8** ревью-пак четырёх осей (отработан паком P8-REVIEW, D39.159) ·
**п.10** пол на пустой манифест (`readmodel.refreshStructure` сверяет `ChaptersTotal`, тест есть) ·
**п.11** `PD-327` (строка закрыта эрой «самопроверка акта 5») · **п.13** оговорка `CreditHeldBy`
(запинена с названной пойманной мутацией в `pgstore/credits_test.go`).
**п.12 (изоляция читающих чтений) НЕ закрыт и потому остаётся дословно:** пина на
`RepeatableRead`+`ReadOnly` у `inReadTx` в дереве по-прежнему нет.
⚠ **ПЕРВЫМ — не из пака P7, но найдено вторым рубежом приёмки: `Sweep` останавливается для ВСЕЙ
инсталляции на двух медленных прогонах, и выхода нет** (бюджет прохода 2 мин против 60 с на прогон ⇒
`UnsettledRuns`, единственный ретрай отложенного расчёта, не вызывается вообще; заклиненный прогон по
построению стоит в голове `order by started_at` и голодит остальных детерминированно; деньги и книга
заморожены, пользователю видно «идёт»; `tm_platform_sweep_unfinished_total` растёт — метрика без
ручки). Тело разбора — **D39.153 §8в**; складывающиеся бюджеты пяти проходов и лечение — строка
**`PD-386`**. ⚠ Попутно: `PD-169` стоял `fixed(P5)` и этим ЛГАЛ приёмке (его пин гонял `Sweep` вообще
без дедлайна прохода, то есть доказывал пер-прогонный бюджет, а не выживание прохода) — пере-открыт и
закрыт эрой P8-FIX.
4. **sqlc — БЕРЁМ, слово владельца 20.08 («я вообще за»).** ИСПОЛНЕНО: `PD-44` закрыт эрой P12, пак
`sqlc` (П-19) отработан секцией выше. Заказанный тем же пунктом ПОСТОЯННЫЙ гейт — прогон каждого
собранного запроса через разбор Postgres против мигрированной схемы — построен
(`TestEverySQLStatementParsesAgainstTheMigratedSchema`). Разовый ответ, который он тогда дал —
141 запрос, все планируются чисто — и поправка к нему («десять непокрытых» неверно, реально три)
живут телом **D39.153 §8г**.
1. **`ContractVersion` = "0.3.0" при формах 0.4.0** (`internal/httpapi/capabilities.go:13`, он же в каждом `hello``stream.go:100`). Ратификация 0.4.0 состоялась (D39.152) ⇒ константа обязана подняться. Комментарий над ней сам это требует: «raised in the same commit as the code that implements a new minor». Сегодня практического вреда нет (фронт заморожен на 0.2.3), но деплой объявляет версию, которую не отдаёт. **Первым пунктом.**
2. **Пины на два поля `EventBase`** (см. выше). ⚠ Попутно 21.08 усилен гейт якорей доков: `file:line` теперь может нести токен ожидания (`` `путь:12-14`=`подстрока` ``), и такой якорь сверяется ПО СОДЕРЖИМОМУ на каждом прогоне, включая коммиты, которые двигают ЦЕЛЬ. Ваши доки (`platform/docs/**`) вошли в область линта — раньше их не сканировал никто.
3. **Бюджет попыток материализации.** У долга нет предела и канала «признать безнадёжным»: claim → fail → defer(`now()`) → следующий свип через 15 с, вечно (`pgstore/books.go:538,557-564`). У интейка предел есть (`parseAttempts`=5). Три следствия: до 5 мин движковых процессов на проход бесконечно · поток такой книги НЕ заканчивается никогда (`AtRest` требует `read_model_owed_at is null`, `events.go:125`) · при живом манифесте и падающем экспорте `SaveStructure` коммитится каждый проход ⇒ `revision++` и кадр каждые 15 с.
4. **sqlc — БЕРЁМ, слово владельца 20.08 («я вообще за»).** Отступление P7 закрыто, PD-44 переоткрыть исполнением. ⚠ Ваше предложение «взять на однооператорных ручках P8» покупает инструмент туда, где не болит: рантайм-ошибки «нет такой колонки» случились в СКЛЕЕННОМ SQL read-модели (⚠ счёт: проверяемый носитель — ваш регистр PD-44 и журнал — несёт ДВЕ; ещё две вы назвали в ответе владельцу, в доки они не попали, поэтому опираемся на две — испр. 20.08) (`chapters_before`/`stop_for_signing``readmodel.go:441,446`, там же `note_count` и сломанный алиас `b`), куда sqlc по построению не дойдёт. Тем же паком — гейт, который туда дойдёт: прогонять КАЖДЫЙ собранный запрос через разбор Postgres (prepare/describe) против мигрированной схемы; склейка ему не мешает, он получает финальную строку. И ответить попутно на вопрос, которого мы не знаем: **есть ли в read-модели запрос, которого не касается ни один тест** — если есть, это не «медленная обратная связь», а дыра. ⚠ **Половина ответа УЖЕ получена вторым рубежом приёмки** (собранный SQL извлечён из пакета через `go/types` и прогнан `EXPLAIN (GENERIC_PLAN)` против мигрированной схемы): **141 запрос, все планируются чисто, остаточных «нет такой колонки» НЕТ**; тестом ЭТОГО пакета недостижимы десять, но по батарее ЦЕЛИКОМ реально не покрыты только **три** (`DeleteOldLoginEvents` · `UserByIdentity` · `Observe`; остальные семь покрыты на 5887% тестами соседних пакетов — пере-мерено покрытием при аудите 20.08, первая редакция называла десять и это было неверно) — `RefundParseAttempt` · `StuckIntake` · `OpenReservations` · `ReleaseUnspawned` · `DeleteOldLoginEvents` · `UserByIdentity` · `Observe` · `SpendBound` · `AttemptReservationOpen` · `RunPausedReason`, и ни один из них не трогает схему 0001600024. То есть дыры сегодня нет, а гейт нужен как ПОСТОЯННЫЙ — именно он и делает этот ответ воспроизводимым.
5. **PD-297** — round-trip на строку в `SaveStructure` под эксклюзивной блокировкой книги; мерить на корпусной книге до и после (корпусная книга появится на холодном прогоне движка).
6. **Труба доставки решений банка в движок** — единый бэклог, строка **199(а)**: перед resume писать решения в `mined_delta`/`mined_rejects`. Сегодня `action`/`dst` — write-only колонки (единственный SELECT `readmodel.go:336` проверяет лишь наличие строки), а воркер описан в комментарии вашей же миграции `00002_readmodel.sql:172-174` и не построен.
7. **Мусор:** пустой `platform/ru` (0 байт, обрубок редиректа) — в лендинг не взят, снесён оркестратором. `books.go:234-235` — задвоенная первая строка доккомментария, класс PD-310/326.
8. **Ревью-пак четырёх осей, которых не смотрел НИКТО** (ваш же obstacle): деньги и леджер целиком · вход/сессии/CSRF · очередь и джобы · метрики. Это не дофикс и не довесок — первый взгляд, отдельной работой.
9. **Один носитель на факт для замера 00016→00022 — но НЕ удалением числа.** Выигрыш назван ЧЕТЫРЬМЯ носителями: шапка этого журнала «83%» · `internal/pgstore/perf_test.go:13`=`96% of the page` «96%» · миграция 00022 «16.8 против 3.4, джойн сам 9 мс» · регистр PD-306 (повторяет 83% и 16.8/3.4). ⚠ Я сперва записал, что «96%» ни из чего не выводится — **это была моя ошибка, снята проверкой записей**: оно выводится точно из ВАШЕГО же замера в `archive/P7_ACT5_FIX_PLAN_2026-08-20.md:444-446`=`636 мс против` («636 мс против 24 мс», холодный корпус акта 4) = 96.2%, тогда как 83% и мой независимый ≈80% — с вакуумированного корпуса. То есть носители меряли РАЗНОЕ и все честны. Свести указанием УСЛОВИЙ замера при каждом числе, оставив нормативным один (миграция 00022 — она их и несёт); удалять «96%» как фантом НЕЛЬЗЯ.
10. **У материализатора нет ПОЛА на пустой манифест — латентная потеря всего текста книги.** `internal/readmodel/readmodel.go:215-232` строит `in.Chapters` только из `manifest.Chapters` и ни разу не сверяется со счётчиками того же документа (`ChaptersTotal`/`UnitsTotal` лежат рядом и печатаются в лог строкой ниже). Пустой список едет в `SaveStructure`, где `pgstore/readmodel.go:193-196` выполняет `delete from chapters where book_id = $1 and not (id = any($2))` — на пустом массиве предикат истинен для ВСЕХ глав, и каскад `chapters → units` сносит текст; при пустом `Key` вдобавок срабатывает `changed` и уходят все `unit_resolutions` (родня строки 198). Сегодняшним движком недостижимо (`buildManifest` всегда наполняет главы, файл пишется атомарно) ⇒ фикс-лист, не блокер. **Асимметрия и есть находка:** на ИНТЕЙКЕ ровно этот случай отловлен явно и прибит мутацией (`internal/books/parse.go:134-142`, `books_test.go:1296`) — «нет глав, но работа считается» там не признаётся правдой о книге. У материализатора такого пола нет, и теста на нулевой манифест в `readmodel_test.go` тоже нет. Лечится одной сверкой `len(manifest.Chapters)` против `manifest.ChaptersTotal`.
11. **`PD-327` в регистре стоит `open`, а канон 0.4.0 РАТИФИЦИРОВАН** (D39.152) — то есть условие её закрытия наступило. Закрыть строку регистра явно; сегодня расхождение видно скриптом (`counts.py --check` печатает PD-327 среди открытых), и это ровно тот класс, ради которого регистр объявлен источником истины по статусу.
12. **Изоляция читающих чтений НЕ ЗАПИНЕНА (аудит 21.08, посадка мутации).** Снятие `RepeatableRead`+`ReadOnly` у `inReadTx` (`internal/pgstore/credits.go:492-493`) проходит ВСЮ батарею — при том, что под этим инвариантом лежат шесть ручек выдачи, а носитель прямо объясняет цену («every frame in that window was lost for good», `books.go:684-688`, PD-163). По вашей же норме PD-1 свойство без пинящего теста считается НЕ закрытым — а это тот самый класс, которым я мерил найденную дыру с `emitFrame`. Код приехал НОВЫМ в P7, то есть лежал внутри диффа, который читали шесть линз приёмки и я сам.
13. **`CreditHeldBy`: оговорка `and book_id <> $2` не запинена (аудит 21.08, посадка мутации).** Мутант, снимающий её, проходит батарею: пин, который выглядит покрывающим, её ни разу не исполняет — в фикстуре у исключаемой книги холда НЕТ (`internal/pgstore/credits_test.go:443-465`). Сумма кормит контрактный `blocked`, и раздутая сумма отправляет пользователя гасить прогон, который ничего не освободит — ровно тот вред, против которого написан комментарий над самим запросом (`credits.go:393-397`).
### Что ушло в ЕДИНЫЙ бэклог (движок/шов/контракт — ваши строки туда не заходят, D39.84)
**198** апгрейд движка стирает замечания и счётчики безвозвратно (ваша зачистка при ре-кате × announce-once движка — композиция, гейт холодного прогона) · **199** канал доставки правок банка · **200** сквозная полоса прогресса вместо пофазной (слово владельца 20.08) · **201** «Глава N» внутри текста экспорта · **202** живой прогон насквозь через API — ПОСЛЕ холодного прогона движка (слово владельца 20.08) · **203** хвосты контракта · **204** движок публикует причины флагов данными (релей §7в, ваш PD-246 — строка заведена в бэклоге ДВИЖКА, как вы и просили). ⚠ По релеям §7 сверено грепом при лендинге, а не по вашему списку: **(б) и (д) уже ИСПОЛНЕНЫ** синком 0.4.0, **(г) наполовину** (`unspecified` ратифицирован, открыта граница ступеней), **(и)** закрыт полем `stop_requested`. Реально открыты только (а)-канон-половина, (з) и договорная часть (к).
@ -1818,7 +1777,5 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц
**Оговорка про пер-термные решения расширена на ВСЮ ручку** (была только на `decline`): инертны одинаково и `approve`, и `dst`. Оговорка временная — снимается исполнением строки 199(а).
**Исправление МОЕГО же пинга №15 (испр. оркестратором №18):** «Все сайдкары пишутся атомарно (temp+rename)» — НЕВЕРНО. Атомарны три из шести (`manifest.json`, `bank.json`, `bank-stop.json``backend/internal/pipeline/artifact.go:24-59`); `bank-stop.txt`, `mined-signature.yaml`, `auto-bank.yaml` пишутся `os.WriteFile` с усечением. Сегодня безвредно (читателей нет), но сессия, взявшая любой из них по той строке, получит усечённый документ на живом прогоне.
**Сессия P7 ЗАКРЫТА владельцем 20.08.** Промт отработан → `platform/docs/archive/`. Дальше — другие сессии.