From 4a11df2d08a8fac9f4c98ca943de90617149de6a Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 29 Aug 2026 13:15:32 +0300 Subject: [PATCH] Land the platform documentation sweep: the seam comment no longer argues from a premise the engine pack retired, and the battery gate count now lives in one place instead of three --- platform/README.md | 27 +++++--- platform/deploy/README.md | 31 ++++++++-- platform/docs/DEFECT_REGISTER.md | 51 +++++++++------- platform/docs/ENGINEERING_STANDARDS.md | 7 ++- platform/docs/PLATFORM_DIRECTION.md | 2 +- platform/docs/STACK_DECISIONS.md | 4 +- platform/docs/platform-PROGRESS.md | 85 +++++++++++++++++++++++++- platform/internal/ingest/resync.go | 29 ++++++--- 8 files changed, 190 insertions(+), 46 deletions(-) diff --git a/platform/README.md b/platform/README.md index 2f93e2ea..8bf9ffa1 100644 --- a/platform/README.md +++ b/platform/README.md @@ -7,12 +7,17 @@ `docs/platform-PROGRESS.md` (весь прогресс зоны здесь, решение владельца 04.08), стек — `docs/STACK_DECISIONS.md`. -Батарея зоны: `make check` (build · vet · fmt · lint · test -race). Тесты со схемой требуют -`TM_PLATFORM_TEST_DSN` (как поднять Postgres без root — `docs/STACK_DECISIONS.md`); без него они -пропускаются, и `check` называет пропуски вслух. Живая проба рендера конфигурации требует второго -гейта — `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE`. `make vuln` и `make fuzz` — +Батарея зоны: `make check` (build · vet · fmt · lint · test -race). `make vuln` и `make fuzz` — отдельными целями. +⚠ **Сколько у батареи условий — НЕ ЗДЕСЬ, и это правка 29.08.** Число жило в трёх местах сразу +(здесь — два, `docs/ENGINEERING_STANDARDS.md` — три, `docs/STACK_DECISIONS.md` — четыре), и это самый +дешёвый способ получить ложную приёмку: сессия, честно исполнившая §3.1 по этому файлу, объявит +«скипов ноль» при красном тесте, о котором её версия списка не знала. **Единственный носитель — +`docs/STACK_DECISIONS.md`, раздел «Гейты батареи»**; здесь его сознательно не дублируем. Коротко и +без числа: без переменных окружения батарея МОЛЧА пропускает около трёхсот тестов и остаётся +зелёной, поэтому «зелено» без сверки со списком условий не значит ничего. + ⚠ **`TM_PLATFORM_LANGUAGE_PAIRS` — обязательная настройка деплоя** (форма `zh>ru,ja>ru:unavailable`): её отвечает `GET /capabilities`, по ней же интейк отклоняет неподдерживаемую пару кодом `unsupported_pair`. Какие пары существуют, решают ДАННЫЕ (пакет промптов оператора), а не список в Go. @@ -91,7 +96,11 @@ Prometheus; на контрактную поверхность они не вы реконсилятор, который читает мир (Postgres · журнал книги · маркер выхода) и не ждёт процесса; - читающая поверхность контракта — **есть (P7)**: дерево глав и пары с текстом, замечания, ЧТЕНИЕ банка (⚠ ЗАПИСЬ в банк снята 22.08 вместе с пер-термной моделью подписи — `PD-370`, D39.144: подпись - это `resume`, а ручки «поправить/добавить термин» нет ни в зоне, ни в каноне), `GET /capabilities`, машинная модель ошибок (`code` + `request_id`), условные чтения + это `resume`. ⚠⚠ **А вот «ручки поправить/добавить термин нет» — УЖЕ НЕВЕРНО, и это правка 29.08:** + дверь `POST /books/{bookId}/bank/corrections` построена паком P9 и смонтирована — `httpapi/v0.go` + в `contractSurface`, обработчик `httpapi/bank.go`, канон `14-api-contract/openapi.yaml`, акты + D39.161/162/166, монтаж по `Capabilities.bank_corrections_enabled`. Снята была ПЕР-ТЕРМНАЯ ПОДПИСЬ, + а не правка термина, и предложение выше склеивало две разные вещи), `GET /capabilities`, машинная модель ошибок (`code` + `request_id`), условные чтения (`ETag`/304) и сжатие JSON, `Idempotency-Key` на двух создающих вызовах; - SSE-поток прогресса во фронт — **есть (P7)**: поток на КНИГЕ (не на прогоне), кадры минтит писатель в свою транзакцию, клиент продолжает по `Last-Event-ID` (⚠ денежные суммы на провод и на @@ -117,9 +126,11 @@ Prometheus; на контрактную поверхность они не вы | `internal/pricing`, `internal/money` | шкала глав и целые микро-доллары | | `internal/metrics`, `internal/reqid`, `internal/jobs`, `internal/config`, `internal/gates` | телеметрия, id запроса, очередь, конфигурация, гейты тулчейна | -Три канала движка, и других нет: `tmctl manifest --json` (структура), `tmctl export --json --pairs` -(ТЕКСТ пар — единственный носитель), `.bank.json` (банк), плюс `tmctl status --json` как -канал ремонта и `events.jsonl` как поток. Живой SQLite движка не читается никогда (D39.85). +Каналов движка ПЯТЬ, и других нет (⚠ до 29.08 фраза начиналась «три канала, и других нет» и тут же +перечисляла пять — счёт правился, а слово нет): `tmctl manifest --json` (структура), +`tmctl export --json --pairs` (ТЕКСТ пар — единственный носитель), `.bank.json` (банк), +`tmctl status --json` (канал ремонта; с лендингом `6ec9f8a` он ещё и оценивает пере-проход ДО +покупки) и `events.jsonl` (поток). Живой SQLite движка не читается никогда (D39.85). ## Чего здесь НЕ будет diff --git a/platform/deploy/README.md b/platform/deploy/README.md index 655e2be2..c90e349d 100644 --- a/platform/deploy/README.md +++ b/platform/deploy/README.md @@ -81,8 +81,27 @@ TM_PLATFORM_LANGUAGE_PAIRS=zh>ru TM_PLATFORM_BOOKS_DIR=/srv/textmachine/books TM_PLATFORM_BOOK_TEMPLATE=/srv/textmachine/book-template.yaml TM_PLATFORM_METRICS_ADDR=127.0.0.1:9464 + +# ⚠⚠ ДОПИСАНО 29.08. БЕЗ ЭТИХ ЧЕТЫРЁХ ИНСТАНС НЕ ЗАПУСТИТ НИ ОДНОГО ПЕРЕВОДА, и это не +# «неполный пример», а окружение, при котором каждый ОПЛАЧЕННЫЙ прогон падает. +TM_PLATFORM_ENGINE_BIN=/opt/textmachine/engine/<версия>/tmctl # пусто = инстанс только читает +TM_PLATFORM_CTL_BIN=/opt/textmachine/bin/tmplatformctl # его зовёт юнит как ExecStopPost +TM_PLATFORM_STATE_DIR=/var/lib/tmplatform/state # маркеры выхода; ТОЛЬКО абсолютный +TM_PLATFORM_ENGINE_KEYS_PATH=/etc/tmplatform/engine-keys # KEY=VALUE, едет --keys-file ``` +⚠ **Почему каждая из четырёх обязательна, а не желательна:** +- **`ENGINE_BIN`** пусто — инстанс объявляет себя читающей репликой и не спавнит ничего; +- **`CTL_BIN`** — путь, который юнит зовёт в `ExecStopPost`, чтобы записать маркер выхода; без него + прогон завершается, а платформа об этом не узнаёт никогда; +- **`STATE_DIR`** — каталог маркеров. ⚠ Дефолт лежит ВНЕ `ReadWritePaths=` юнита, а `deploy/` + ставит `ProtectSystem=strict`, поэтому без явного значения запись маркера запрещена файловой + системой, а не логикой; +- **`ENGINE_KEYS_PATH`** — ЕДИНСТВЕННЫЙ канал провайдерских ключей в движок (едет аргументом + `--keys-file`, мимо процесса платформы и мимо окружения юнита). Без него движок ищет `.env` рядом + с `book.yaml`, которого SaaS-путь не пишет, и каждый платный прогон падает `exit 10` + («missing API keys»). На буте это WARN, а не отказ, — то есть тихо. + ⚠ **`TM_PLATFORM_SIGNUP_GRANT_USD` здесь НЕТ намеренно** (PD-104, слово владельца 16.08): на бете грант по умолчанию НОЛЬ и начисляется руками — `tmplatformctl grant`. Строка `=5` в этом блоке включала обратно ровно тот самообслуживаемый безлимитный грант, который дефолт выключает. @@ -169,11 +188,15 @@ Read-only команды движка отказывают файлу проек даёт `tmctl status --json` → exit 13 с этим токеном; `tmctl migrate --config ` делает пред-миграционный бэкап и переводит v14 → v15; тот же `status` после неё — exit 0. -⚠ **Жёсткое правило: эмиттер-бинарь не выкатывается, пока `tmctl migrate` не заленден** ($0-команда -движка, открывающая файл на запись без прогона). До того новую платформу проверяют против журналов, -писанных своей сборкой движка вне репозитория. +⚠ **Правило БЫЛО: эмиттер-бинарь не выкатывается, пока `tmctl migrate` не заленден** ($0-команда +движка, открывающая файл на запись без прогона). ⚠⚠ **УСЛОВИЕ ВЫПОЛНЕНО — правка 29.08.** Глагол +существует и заленджен: `backend/cmd/tmctl/migrate.go`, в диспетчере `main.go` он стоит в списке +команд, и у него есть свой код выхода (`exitSchemaMismatch = 13`, «run `tmctl migrate`»). Правило +блокировало выкат по причине, которой больше нет, — и это худший род блокировки, потому что снаружи +он неотличим от осторожности. Порядок ниже действует; читать его как ДЕЙСТВУЮЩИЙ, а не как «когда +приедет». -Порядок, когда `migrate` приедет. ⚠ Ключевое: **`migrate` гоняется НОВЫМ бинарём** — старый уводит +Порядок (действующий). ⚠ Ключевое: **`migrate` гоняется НОВЫМ бинарём** — старый уводит файл в свою же схему, то есть не делает ничего, и деадлок остаётся. Поэтому бинарь кладётся ДО миграции, а `TM_PLATFORM_ENGINE_BIN` переключается ПОСЛЕ. diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 40d1aff7..e91d1e06 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -25,14 +25,13 @@ | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | |---|---|---|---|---|---|---| -| PD-370 | bug | **major** | `internal/httpapi/reading.go`, `internal/httpapi/v0.go`, `internal/pgstore/readmodel.go`, канон `14-api-contract` §BankDecision | **Была построена и стоит в каноне модель, которую владелец отменил 16.08.** `D39.144` ратифицировал: подписывается ВЕСЬ банк ОДНИМ ОК, «пер-термная подпись = сотни кликов — НЕ модель продукта»; пер-термно существует не подпись, а ПРАВКА термина (до подписи) и правка с пере-генерацией задетых глав (после прочтения, гейчено 192). Гейт полноты нота сняла, а САМ ГЛАГОЛ остался — чистка дошла до resume и счётчиков и не дошла до словаря. Колонки были write-only: их не читал ни один SELECT, то есть реализации не существовало **— ЗОННАЯ ПОЛОВИНА ЗАКРЫТА 22.08 по слову владельца: снят write-путь целиком** (маршрут `POST /books/{bookId}/bank/decisions`, хендлер, wire-типы, метод интерфейса `Library`, `SubmitBankDecisions`, типы `BankDecision`/`BankReceipt`, `UnknownTermError`), на его месте — пометка для будущей сессии с ратифицированной моделью и с тем, что уже есть в схеме. Пол размера поверхности 15 → 14 сдвинут ЯВНО и с причиной в комментарии: гейт сработал, а не был подогнан. **КОНТРАКТНАЯ ПОЛОВИНА ЗАКРЫТА 27.08 минором 0.5.0 (D39.161):** путь `POST /books/{bookId}/bank/decisions`, глагол `submitBankDecisions` и три схемы снесены из канона — ноль вхождений, замер в `docs/CONTRACT_MINOR_REPORT.md` и пере-мер приёмки D39.161 п.2; счётчики `pending_decisions`/`complete` сняты со всех трёх носителей (`BankPage`, `EventBank`, квитанция). Преемник — дверь `POST …/bank/corrections` (выведена из `tmctl bank-apply`), СМОНТИРОВАНА паком P9 (D39.162); `Capabilities.bank_corrections_enabled` следует `cfg.RunsEnabled()`. Итог: зонная половина снята 22.08 (слово владельца), контрактная — 27.08 (D39.161); ⚠ НЕ наследовать этой строкой заказ на счётчики в `wireBankPage` — он жил отдельной строкой `PD-399` (закрыта паком P9 своим пином). Статус переведён по аудиту документации 28.08 — обязательство висело неисполненным (класс строки 225); текст закрытия — контрактной сессии, перенос в зону — платформенной (регистр в её праве записи и был грязен её WIP) | fixed(D39.161 — контрактная половина; 22.08 слово владельца — зонная; перевод по аудиту 28.08) | поправка владельца 22.08; ратификация D39.144; аудит 31 агента 22.08 · минор D39.161 · аудит документации 28.08 | | PD-402 | bug | **major** | `internal/runs/reconcile.go:1221` `Resume` (switch по статусу, «последний ли» не спрашивается), `internal/pgstore/readmodel.go:484` `lastRun` (`order by started_at desc`), `internal/pgstore/runs.go` `RestartRun` (`started_at` не пере-штампуется) | **Возобновление НЕ-последнего прогона книги ломает полосу и карточку сразу двумя путями.** (1) Полоса возобновлённого открывается на ЧУЖОЙ работе: базы не пере-снимаются (это правильно для последнего прогона), но между стопом и resume по книге легально прошёл ЦЕЛЫЙ другой прогон, и оба числителя уже накрутили его главы — на нажатие «продолжить» пользователь видит 100% при нуле собственной работы, клэмп режет только превышение единицы, не двойной счёт до неё. (2) `lastRun` выбирает строку по `started_at desc`, а RestartRun `started_at` не трогает — карточка книги, `derivedStatus` и КАЖДЫЙ progress-кадр живого прогона считаются по ЧУЖОМУ финишировавшему: экран показывает `ready` и замороженный бар, пока живой прогон тратит деньги невидимо. Найдено воркфлоу-ревью и подтверждено его overlay-прогонами (4 независимые линзы сошлись на одном корне); ответ на сам resume (`ReadRun` по id) при этом честный — клиент получает от одной ревизии книги два разных бара. Лечение — вопрос формы: запрет resume не-последнего ИЛИ live-приоритет в `lastRun`; решение следующего пака | open | воркфлоу-ревью P9 28.08 (линзы bar:monotonicity · bar:double-count · bar:baselines-x-migration · bar:stage-caption), диспозиция оркестратора 28.08: строкой | | PD-403 | bug | **major** | `internal/pgstore/sink.go:415` `recordWaveShape` (`edit_wave` только растёт), `internal/pgstore/readmodel.go:388` `finishedUnits`, `internal/pgstore/books.go` `ReadBookForRun.ChaptersLeft` | **Книга с уже сложившимся `edit_wave=true` на деплое, где оператор УБРАЛ редактора, продаёт уже переведённые главы повторно — и ни один прогон больше не доводит полосу до единицы.** Безредакторный движок шлёт `editTotal=0`, но флаг не возвращается (`or $2` пишет только false→true); прогон делает 100% купленного черновика и закрывается `ready` на 50% со stage `editing`; `chaptersDone` (edit-колонка) мёрзнет, `ChaptersLeft` не падает — шкала покупки предлагает те же главы снова (симптом `PD-202`, записанной fixed этим самым флагом), а follow-up прогон над этой покупкой читает 0/C с первой секунды. Комментарий `sink.go:406-409` о собственном коде неверен обеими фразами (запрещённое направление называет разрешённым, «shows up as more chapters finished» не случается). Подтверждено overlay-прогонами воркфлоу. Корень продуктовый — смена формы конвейера на живой книге; лечение за решением владельца/оркестратора | open | воркфлоу-ревью P9 28.08 (линзы bar:stage-caption · bar:double-count · bar:monotonicity), диспозиция оркестратора 28.08: строкой | | PD-404 | bug | **major** | `internal/pgstore/sink.go:415` `recordWaveShape`, `internal/pgstore/readmodel.go:388` `finishedUnits` (живой флаг), потребители `bookColumns.chaptersDone` и `ReadBookForRun.ChaptersLeft` | **Единственное разрешённое движение флага (false→true, оператор ДОБАВИЛ редактора) уводит `Book.chapters_done` НАЗАД в пределах одного `structure_version` — то, что канон запрещает прямо.** Книга с начерченным заделом: карточка 7/10; первое progress-событие прогона с редактором переворачивает флаг, `finishedUnits` переезжает на edit-колонку — 7 → 0 одной транзакцией при РАСТУЩЕЙ ревизии (клиент обязан отрисовать спад), `ChaptersLeft` раздувается обратно и шкала снова предлагает купить купленное. Класс `PD-316` (записана fixed) на непокрытом триггере: её пин ловит только admission, а не движение самого флага. Крайний случай той же арифметики: книга, начерченная ПОЛНОСТЬЮ под `false`, редактором НЕдочитываема ни одним действием API — `ChaptersLeft=0` не допускает прогон, а флип случается только в progress-событии допущенного прогона. Подтверждено воркфлоу на in-tree фикстуре (`sink_test.go:701`) | open | воркфлоу-ревью P9 28.08 (линзы race:flip-read-consistency · bar:stage-caption · bar:baselines-x-migration), диспозиция оркестратора 28.08: строкой | | PD-405 | bug | **major** | `internal/pgstore/sink.go:219` (ложный комментарий-обоснование), `internal/books/parse.go` (инлайн-материализация после коммита `not_started`), `internal/pgstore/books.go` `BooksOwedReadModel` (`nothingIsRunning`) | **Прогон над книгой без материализованного дерева читает 0/total ВСЮ ЖИЗНЬ при исправно доезжающих progress-событиях — а комментарий, которым этот случай объявлен закрытым, описывает архитектуру, которой больше нет.** Окно штатное: интейк коммитит `not_started` ДО материализации; если она упала/отложилась и пользователь успел стартовать прогон, долг дерева заморожен до конца прогона (`nothingIsRunning`), `unitDone` находит 0 строк глав и выходит, вся полоса выведена из `chapters` → 0/total до `ready`. Комментарий `sink.go:219` «the counters the screen reads today come from the progress event» ложен — эти колонки не читает никто (носитель — `PD-411`). Достижимость ЗАМЕРЕНА логом живого стенда: на здоровом пути окно «`not_started` без строк `chapters`» живёт **0.018 с** (обе загрузки пробоя P9: `book parsed` → `the reading surface was refreshed`, 0.0179 и 0.0189 с) — опасная форма только УПАВШАЯ/отложенная материализация, и тогда длина окна = вся жизнь прогона. Лечение — честная привязка полосы к бездеревному окну (отказ старта до дерева ИЛИ полоса из progress-событий как раньше); НЕ легло фикс-раундом — открыта целиком, замер достижимости ратифицирован актом D39.162 | open | воркфлоу-ревью P9 28.08 (линзы bar:stage-caption · bar:double-count), диспозиция оркестратора 28.08: строкой, достижимость замером (акт D39.162) | -| PD-406 | bug | **major** | `internal/httpapi/stream.go:137`=`helloID := state.Position` | **Служебный кадр `hello` на ПЕРЕПОДКЛЮЧЕНИИ несёт `id = state.Position` (голова истории книги) вместо предъявленного клиентом `last` — и переводит `Last-Event-ID` браузера ВПЕРЁД через кадры, которые это соединение ещё не отдало.** По WHATWG поле `id` фиксируется в last event ID buffer на диспатче; обрыв сразу после hello (прокси, вывод инстанса из ротации, ошибка первого `ReadFrames`) теряет кадры `last+1..Position` НАВСЕГДА — включая `note`, которые контракт запрещает терять («MUST NOT be coalesced or dropped»), без `resync_required`; усилитель — `revision` в data того же hello выше потерянных строк, так что предписанный контрактом дельта-догон `?after_version=` возвращает пусто, обезоружены обе починки сразу. Это ровно вариант, который `14-api-contract/README.md` (§Last-Event-ID) называет отвергнутым. **Лечение — одна строка:** при `resuming` слать hello с `last` (свежее подключение оставить как есть); код четырьмя строками ниже уже делает ровно этот выбор для `from` (`stream.go:122-126`). Свежий коннект не задет — там клиент читает коллекции целиком ⚠ **ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162):** при `resuming` hello несёт `last` клиента (свежий коннект — голову, как и `from` ниже); пин `TestAResumingHelloCarriesTheClientsOwnWatermark` (+ посадка «hello обратно на голову» поймана) | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линза race:flip-read-consistency), лечение по слову оркестратора 28.08 | | PD-410 | bug | **major** | `internal/pgstore/readmodel.go:460` `draftWork` (и вся модель «купленный диапазон в обеих волнах»), контраст: движку передаётся ТОЛЬКО `--ceiling-usd` (`internal/runs/spawn.go` `bookCap`), draft-волна фанится по ВСЕМ чанкам книги (`backend/internal/pipeline/waverun.go`) | **Полоса и платёжная модель считают, что прогон работает над диапазоном «C глав от edit-фронта» в обеих волнах, а движок гонит волны последовательно от СВОИХ фронтов до денег — и на continuation над недочерновленной книгой (`draft_before ≥ chapters_before + C`) формула даёт `draftWork=0`: движок тратит весь потолок на черновики, полоса стоит 0/C весь прогон со stage `editing` на прогоне, который только черновит.** Зеркало дефекта, который `draftWork` чинил (тот закрывал «ready на 50%», этот открывает «0% при сделанной работе»). Это НЕ локальная ошибка формулы: любая полоса из (баз, C, D, E) где-то соврёт, пока платёжная модель («довести до конца C глав от `e0`») и движковый план работ («деньги в порядке волн от фронтов») не согласованы — лечение требует либо передавать движку план (диапазон/волны), либо выводить total полосы из движкового плана; `ceiling_chapters` сегодня до движка не доезжает вовсе. Архитектурное, ОТДЕЛЬНЫМ паком по слову оркестратора 28.08 («остановись и скажи»); подтверждено воркфлоу арифметикой на существующем зелёном пине (`TestARunOverADraftedBacklogOwesOnlyTheLastPass` держит `draftWork=0` в родственной точке) | open | воркфлоу-ревью P9 28.08 (линза bar:double-count), стоп-решение сессии P9 28.08: тянет архитектуру, не чинится в паке | - +| PD-424 | bug | **major** | `internal/runs/reconcile.go` `reopen` (вердикт `deferred`) и `reconcileOne`, `internal/pgstore/runs.go` `StalledRuns`, `internal/pgstore/observe.go` | **ЖИВОЙ прогон с намертво заблокированной расплатой невидим на ВСЕХ поверхностях `PD-385` и не поддаётся `run abandon` — счётчик неудач не растёт НИКОГДА.** Цепь, воспроизведённая охотником приёмки на десяти проходах: юнит исчез без маркера → `restart` → `settle` возвращает `settlementBlocked, nil` (не ошибку) → `reopen` отказывается открыть новую попытку поверх незакрытого холда и возвращает вердикт `deferred` → `case deferred: return nil` → `reconcileOne` считает проход УСПЕШНЫМ и вдобавок зовёт `ClearRunDeferral`. Итог: `reconcile_failures` 0, `reconcile_after` NULL, `StalledRuns(5)` пуст, гейдж 0, холд заморожен, движок дёргается каждым проходом. Вывести из состояния может только пользователь, нажав Stop, — и никто ему об этом не говорит. ⚠ Это ТА ЖЕ болезнь, что `PD-384`/`PD-385`, но в ЖИВОЙ фазе, куда пак P11 не дошёл: он лечил вторую фазу (расплату) и её поверхности. ⚠ **Почему пак не взял её в себя (решение сессии, не пропуск):** лечение упирается в вопрос, которого нет ни в одной строке заказа, — чем должен считаться вердикт `deferred` для СЧЁТЧИКА: не-событием (как сейчас), успехом или неудачей. Это дизайн реконсиляционной фазы, а пак был про фазу расплаты; вмешаться в него по дороге значило бы «распухнуть вокруг самого срочного», против чего промт предупреждает прямо. Форма лечения, которую сессия предлагает: `reopen`, вернувший `deferred`, обязан сообщать `established=false` (тогда `ClearRunDeferral` хотя бы не стирает счёт), а сам блокированный расчёт — считаться неудачей ТОЙ фазы, которая им владеет | open | приёмка оркестратора №19 по паку P11 (охотник вне карты, воспроизведено 10 проходами) | +| PD-425 | bug | **major**, деньги | `internal/httpapi/bank.go:149` (`r.Context()`), `internal/runs/bank.go` `RecordBankMove` | **Дверь банковских коррекций теряет пост-verb факт НАВСЕГДА при обрыве клиента, и следующая покупка платит за это холдом.** Хендлер берёт `r.Context()`, и `RecordBankMove` пишется на нём же; клиент закрыл вкладку до ответа — контекст отменён, запись не легла, `bank_moved_at` остаётся NULL. Второго писателя у факта нет, свипа нет, то есть состояние вечное. Деньги дальше: `re_pass` отвечает 409, а ОБЫЧНЫЙ прогон допускается с `resnapshot=false` и убивается снапшот-гардом движка ПОСЛЕ взятия холда — холд взят, попытка потрачена впустую. ⚠ Средство, которое дверь называет, недостижимо по построению: она отвечает `503` со смыслом «клиент пере-шлёт», а запись падает именно потому, что клиента уже нет. ⚠ Шаблон лечения лежит В ЭТОЙ ЖЕ ЗОНЕ и здесь не применён: `internal/pgstore/idempotency.go:150` и `books.go:205,215` делают `WithTimeout(WithoutCancel(ctx), …)` ровно с этой мотивировкой. ⚠ Не предмет пака P11 (дверь построена паком P9/P10), поэтому заведено строкой, а не починено: правка денежного пути чужого пака требует своих пинов и своей приёмки | open | приёмка оркестратора №19 по паку P11 (охотник вне карты) | ## Открытые — minor | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | @@ -40,7 +39,7 @@ | PD-375 | bug | minor | `internal/runs/runs.go:323`=`if in.CeilingChapters < bounds.Min`, `internal/httpapi/v0_test.go:359`, `internal/runs/control_test.go` | **Верхнюю половину проверки `ceiling_chapters` не исполняет НИ ОДИН тест: снятие второго операнда `in.CeilingChapters > bounds.Max` проходит ВСЮ батарею.** Собственный доккомментарий называет проверку несущей — «число, решающее СКОЛЬКО ДЕНЕГ резервируется, не может быть выбрано вызывающим односторонне», — и канон требует того же от сервера (`max_chapters` уже прижат к остатку, «a client MUST NOT clamp it again»). Единственный тест, называющий `ErrCeilingOutOfBounds`, это таблица соответствия ошибки коду 409 в `httpapi/v0_test.go`, которая `Start` не зовёт, а самая тесная фикстура просит 10 глав у книги, где их 100. Код сегодня ВЕРЕН; дефект в том, что править эту строку можно безнаказанно. Замерено на живом стенде: чистая сборка отвечает `409 ceiling_unavailable/bounds_moved`, сборка с мутацией отдаёт 202 и открывает резервацию `24000000` микро на книге из ТРЁХ глав, а движку уходит `--ceiling-usd 24.200000`. Вес: рефутер сузил major → minor, потому что код верен и ни один сегодняшний клиент до вреда не доходит. Воспроизведение: `docs/p8-review/axis1-money/a1-mut5-ceiling-bound.sh`, готовый пин `docs/p8-review/axis1-money/r1_ceiling_bound_test.go.txt` | open | ревью-пак P8-REVIEW, ось 1 (посадка мутации + живая проба, подтверждено рефутером) | | PD-377 | doc | minor | `internal/pgstore/credits.go:163`=`The ceiling to hand the engine is the amount held`, `internal/runs/spawn.go` `bookCap`, `cmd/tmplatformctl/main.go` `balance` | **Доккомментарий `Hold` на входе в денежный путь неверен ОБЕИМИ половинами с миграции 00011.** Он обещает «the ceiling to hand the engine is the amount held; read it back with OpenReservations». Движку передаётся не сумма холда, а `run_attempts.ceiling_arg_micro_usd` = committed книги плюс прирост (`spawn.go` `bookCap`), и сама 00011 говорит это прямым текстом («It is NOT ceiling_micro_usd»); начиная со ВТОРОГО прогона книги числа расходятся тем сильнее, чем дороже книга — на стенде до 17 раз (`60000` холда против `1060000` в аргументе). Вторая половина не работает даже механически: `Reservation.Ceiling` — поле, которое только сканируется и не читается никем, единственный потребитель `OpenReservations` печатает `Amount`/`BookID`/`OpenedAt`/`EngineRunID`. ⚠ Рефутер снял два из трёх исходных якорей: доккомментарии в применённых миграциях `00007`/`00009` зона не правит после лендинга (у всех 25 файлов миграций ровно по одному коммиту), а исправление уже лежит в следующем файле того же каталога. Остаётся Go-доккомментарий. Воспроизведение: `docs/p8-review/axis1-money/a1-ceiling-column-doc.sql` | open | ревью-пак P8-REVIEW, ось 1 (живой стенд, сужено рефутером до одного якоря) | | PD-386 | bug | minor | `cmd/tmplatformd/runner.go:269`=`pass("runs", sweepBudget, s.runs.Sweep)` и четыре следующих прохода того же `one()`, `cmd/tmplatformd/runner_test.go` | **Такт свипа последователен, и бюджеты пяти проходов СКЛАДЫВАЮТСЯ: сумма объявленных — 37 минут при `TM_PLATFORM_SWEEP_EVERY` 15 секунд.** Пак P8-FIX дал каждому проходу свой бюджет и закрыл «один проход съедает дедлайн другого», но проходы по-прежнему идут подряд в одной горутине: runs 2м, readmodel 10м, intake 21м, idempotency 2м, observe 2м. Ничто эту сумму не ограничивает и ничто её не пинит — у функции `sweep` нет ни одного теста (в пакете два теста, оба про другое), и посадка «фаза runs уходит из головы такта в хвост» пережила ПОЛНУЮ батарею. Замерено пробой на реальной функции: за 4 секунды при такте 100 мс фаза прогонов отработала 40 раз сама по себе и 9 раз позади прохода материализации. Следствия по оси: калибровки самого лечения заданы в проходах и минутах и молча растягиваются (`StalledAfter` 5 «неудач подряд» и бэкофф, про который комментарий обещает «about a quarter of an hour», превращаются в часы); обещание `Stop` «the reconciler re-issues the stop on its next pass» задерживается на ту же величину; телеметрия стоит ПОСЛЕДНЕЙ. ⚠ Рефутер сузил: 37 минут — сумма ОБЪЯВЛЕННЫХ бюджетов, а не достижимая длительность (idempotency это один индексированный DELETE, а проход интейка сам себя режет). Воспроизведение: `docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt` | open | ревью-пак P8-REVIEW, ось 3 (проба на реальной функции + посадка мутации, сужено рефутером) | -| PD-387 | doc | minor | `deploy/README.md:288`=`сколько всего может занять один проход свипа`, `cmd/tmplatformd/runner.go` `one`, `internal/config/config.go` `SweepBudget` | **Рантбук называет `TM_PLATFORM_SWEEP_BUDGET` ручкой «одного прохода свипа» и не говорит, КАКОГО из пяти, — а два бюджета из пяти оператору недоступны вовсе.** Один такт прогоняет ПЯТЬ проходов подряд, и только три берут `sweepBudget`; проход материализации берёт `refreshSweepBudget` 10 минут, проход интейка — `intakeSweepBudget` = `jobs.JobTimeout` + `readmodel.MaterializeBudget` + минута = 21 минута, и обе константы оператору недоступны вовсе. Собственный доккомментарий кода при этом ТОЧЕН («SweepBudget is what ONE pass of the RUN sweep may take») — расходится именно операторская проза, и расходится в разделе «Застрявшая работа: что оператор делает, когда свип не справляется», то есть там, где по ней и будут действовать. Родня `PD-368`: та про то, что поднимать надо пару, эта про то, что ручка не покрывает такт ⚠ **Заголовок ИСПРАВЛЕН после интервальной самоверификации, и первая редакция была завышена:** она писала, что рантбук обещает ограничение ТАКТА целиком, а `deploy/README.md:288` буквально говорит «сколько всего может занять один проход свипа» — то есть ровно то, что ручка и делает. Дефект уже и точнее: строка не называет, какой из ПЯТИ проходов такта она ограничивает, `refreshSweepBudget` и `intakeSweepBudget` не являются ручками вовсе, и раздел, в котором эта таблица стоит, — «Застрявшая работа: что оператор делает, когда свип не справляется». Арифметика 37 минут пере-проверена и держится: 3×2 + 10 + 21. Воспроизведение — `docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt` (последовательность тактов) плюс `sed -n '264,272p' deploy/README.md` | open | ревью-пак P8-REVIEW, ось 3 (находка рефутера) | +| PD-387 | doc | minor | `deploy/README.md:311`=`сколько всего может занять один проход свипа`, `cmd/tmplatformd/runner.go` `one`, `internal/config/config.go` `SweepBudget` | **Рантбук называет `TM_PLATFORM_SWEEP_BUDGET` ручкой «одного прохода свипа» и не говорит, КАКОГО из пяти, — а два бюджета из пяти оператору недоступны вовсе.** Один такт прогоняет ПЯТЬ проходов подряд, и только три берут `sweepBudget`; проход материализации берёт `refreshSweepBudget` 10 минут, проход интейка — `intakeSweepBudget` = `jobs.JobTimeout` + `readmodel.MaterializeBudget` + минута = 21 минута, и обе константы оператору недоступны вовсе. Собственный доккомментарий кода при этом ТОЧЕН («SweepBudget is what ONE pass of the RUN sweep may take») — расходится именно операторская проза, и расходится в разделе «Застрявшая работа: что оператор делает, когда свип не справляется», то есть там, где по ней и будут действовать. Родня `PD-368`: та про то, что поднимать надо пару, эта про то, что ручка не покрывает такт ⚠ **Заголовок ИСПРАВЛЕН после интервальной самоверификации, и первая редакция была завышена:** она писала, что рантбук обещает ограничение ТАКТА целиком, а `deploy/README.md:311` буквально говорит «сколько всего может занять один проход свипа» — то есть ровно то, что ручка и делает. Дефект уже и точнее: строка не называет, какой из ПЯТИ проходов такта она ограничивает, `refreshSweepBudget` и `intakeSweepBudget` не являются ручками вовсе, и раздел, в котором эта таблица стоит, — «Застрявшая работа: что оператор делает, когда свип не справляется». Арифметика 37 минут пере-проверена и держится: 3×2 + 10 + 21. Воспроизведение — `docs/p8-review/axis3-queue/probe_sweep_serialisation_test.go.txt` (последовательность тактов) плюс `sed -n '264,272p' deploy/README.md` | open | ревью-пак P8-REVIEW, ось 3 (находка рефутера) | | PD-389 | hardening | minor | `cmd/tmplatformd/runner.go:320`=`s.metrics.ObserveRunner(metrics.Runner{`, `cmd/tmplatformd/runner.go` `pass`, `internal/metrics/metrics_test.go` `TestTheRunnersStateIsExposedWithItsUnits` | **Шов телеметрии не покрыт НИЧЕМ, и это доказуемо без прогона батареи: `sweep` и `observe` — неэкспортируемые функции пакета `main`, то есть из другого пакета их не может вызвать ни один тест в принципе,** а единственный тест-файл каталога несёт два теста, оба про другое. Проверено тремя посадками, пережившими полную батарею: `StalledRuns: o.StalledRuns` в ноль (наблюдаемая половина закрытого BLOCKER `PD-346`), `errors.Is(err, context.DeadlineExceeded)` в false (`sweep_unfinished_total` больше не может вырасти — `PD-351` со стороны ВЫЗЫВАЮЩЕГО, куда пин `TestAPassThatRanOutOfTimeSaysSo` по построению не достаёт), и перестановка `queue_depth` с `live_runs`. Дыра шире шва: пин формы, на который ссылается `STACK_DECISIONS` §24, задаёт литерал `metrics.Runner` из ШЕСТИ полей из восьми — `StalledRuns` и `AbandonedSurfaces` в него не входят, поэтому мутация внутри самого `ObserveRunner` тоже выживает. Пере-проверено координатором пака независимо: снятие `m.stalledRuns.Set(...)` и снятие `m.abandonedSurfaces.Set(...)` по отдельности проходят ПОЛНУЮ батарею (18 пакетов), при том что снятие соседнего инкремента `sweepUnfinished` тем же пином ловится. То есть операторская ручка, построенная паком P8-FIX в ответ на `PD-169`, не пиньётся ничем. Воспроизведение: `docs/p8-review/mutations-full.log` и `docs/p8-review/axis4-metrics/60-mutations.sh` | open | ревью-пак P8-REVIEW, ось 4 (посадки финдера, рефутера и координатора) | | PD-390 | hardening | minor | `cmd/tmplatformd/runner.go:317`=`log.Warn("the control plane's own state could not be read", "err", err)`, `internal/pgstore/observe.go` `Observe`, `internal/metrics/metrics.go` `ObserveRunner` | **Гейджи замирают при отказе телеметрического чтения, и признака устаревания в экспозиции нет.** `STACK_DECISIONS` §24 объявляет «значения снимает СВИП, а не скрейп», но у снятого значения нет ни отметки свежести, ни счётчика неудач: `observe()` при ошибке пишет один WARN и возвращается, НЕ тронув ни одного гейджа, а Prometheus такой ряд устаревшим не помечает — цель жива, ряд на месте, значение старое. Живой замер: при сломанном чтении и одновременно вылеченном мире экспозиция продолжала утверждать «1 застрявший прогон, 1 карантин, 1 живой прогон, холд возрастом 1201 с», тогда как в базе застрявших было 0; при этом `sweep_duration_seconds_count` рос по всем четырём проходам, то есть все «жив ли свип» сигналы оставались зелёными. `Observe` — ОДИН стейтмент на все восемь чисел, поэтому любая его поломка гасит все гейджи разом, а сам `observe()` вызывается ВНЕ `pass()`, поэтому своего ряда в `sweep_duration_seconds` у него нет и его отказ там не виден. Обратное направление хуже: процесс, у которого чтение не удалось НИ РАЗУ, отдаёт нули как здоровье, и `/readyz` с `/healthz` при этом зелёные. ⚠ Вторая половина того же корня, найденная рефутером: инстанс-ЧИТАТЕЛЬ (пустой `TM_PLATFORM_ENGINE_BIN` — объявленная форма деплоя) не запускает свип вовсе, `observe()` не зовётся ни разу, а метрики созданы раньше и регистрируют все восемь гейджей безусловно, поэтому реплика уверенно отвечает `runs_stalled 0`, `queue_depth 0`, `oldest_open_hold_seconds 0` про контрол-плейн, который она не измеряет — и тут нет даже WARN-строки. Лечится дёшево: `*_last_success_timestamp_seconds` либо счётчик неудач наблюдения плюс проведение `observe` через тот же `pass`. Воспроизведение: `docs/p8-review/axis4-metrics/30-stale-gauges.sh`, `r1-boot-with-blind-telemetry.sh`, `r6-read-replica-zeroes.sh` | open | ревью-пак P8-REVIEW, ось 4 (живой замер, расширено рефутером) | | PD-392 | hardening | minor | `internal/metrics/metrics.go:80`=`Namespace: namespace, Name: "quarantined_attempts"`, `internal/metrics/metrics.go` `oldest_open_hold_seconds`, `cmd/tmplatformctl/runs.go` `listRuns` | **Две метрики без ручки — тот самый класс, за который `PD-169` стоял BLOCKER'ом, в двух других местах.** Help гейджа карантина сам называет цену («Such a run keeps going and keeps spending»), но команды, называющей строку за этим числом, нет: `grep -rn quarantine cmd/` не даёт ни одного хита, и в таблице `runs` колонки карантина тоже нет. У возраста холда ручка формально есть — `balance --user`, — но она требует идентификатор аккаунта, которого гейдж не даёт, а документированный случай самого гейджа («A hold outlives its run only when a settlement could not be made») — это холд ЗАКОНЧЕННОГО прогона, которого список не показывает по построению. Живая проба на состоянии, произведённом ШТАТНЫМ операторским сценарием (abandon застрявшего прогона): при `tm_platform_oldest_open_hold_seconds 10813` команды отвечают «no run is live», «no run is failing to reconcile» и «no book has been given up on», а единственный путь к строке — psql, то есть ровно то, что эти числа заводились заменить. Глобального списка открытых холдов в CLI нет. Воспроизведение: `docs/p8-review/axis4-metrics/50-gauges-without-a-handle.sh` и `r5-hold-without-a-handle.sh` ⚠ **Общий корень с `PD-385`, и там же он взвешен:** сужение ЭТОЙ строки опирается на операторскую поверхность, несостоятельность которой доказывает соседняя строка того же пака — круговое сужение разобрано в `PD-385`, поднятой до major | open | ревью-пак P8-REVIEW, ось 4 (живая проба, подтверждено рефутером) | @@ -60,22 +59,21 @@ | PD-217 | bug | minor | `internal/pgstore/books.go` `BooksForMigration`, `deploy/README.md` | **Книга, застрявшая на `daily_ceiling` или на вечно незакрытом холде, блокирует апгрейд движка бессрочно, и выхода у оператора нет.** Оба состояния снимаются только тем, чего платформа сделать не может: дневной потолок живёт в `book.yaml` оператора, и резюм по нему отказан 409; холд закрывается расчётом, который читает движок ЗАПИНЕННЫМ бинарём. Форсирующего флага у `books --migratable` нет намеренно — он и был бы способом пере-оплатить уже купленные вызовы. Лечится либо ручным разрешением в БД, либо каналом «признать попытку невосстановимой», которого в контракте нет ⚠ **ПОЛОВИНА ЗАКРЫТА P7:** `paused` вышел из предиката `Resumable` — с 0.3.0 прогон, остановленный ЛЮБЫМ потолком, `resume` продолжить не может вовсе (409 `ceiling_reached`), значит к старой сборке ничего не пришпилено и мигрировать такую книгу безопасно; лечение пользователя — НОВЫЙ прогон, который спавнится ТЕКУЩИМ бинарём. Книга на дневном потолке апгрейд больше не запирает. Связь названа в коде: вернётся резюм паузы — вернётся и предикат. Пин `TestEachOfTheThreeBlockersAloneKeepsABookOutOfTheMigrationList` (перечень «безопасных» проверяется целиком, а не по одной книге). ⚠ ОСТАЁТСЯ вторая половина: книга с вечно незакрытым холдом блокирует по-прежнему, и это правильно — расчёт читает движок запиненным бинарём | open | приёмка P6 (дофикс, ФП-7) | | PD-219 | bug | minor | `internal/runs/reconcile.go` `drainJournal`, `reopen` | **Перезапуск после упавшего дрейна теряет непримененный хвост журнала навсегда.** Новая попытка получает курсор с РАЗМЕРА журнала на момент допуска, поэтому строки, которые прошлый дрейн не успел применить, не прочитает уже никто. Счётчики восстанавливает ре-синк, а `unit_resolutions` — нет: их единственный источник — поток. Сегодня невидимо (читающей поверхности единиц ещё нет), к P7 станет расхождением read-модели ⚠ **ДИСПОЗИЦИЯ P7 (взято на учёт, НЕ закрыто):** предсказание строки сбылось ровно наполовину, и половины теперь названы. Что САМОЛЕЧИТСЯ: текст и состояние пары приходят не из потока, а из `tmctl export` на границе работы (`readmodel.Refresh`), поэтому недодрейненный хвост их не искажает. Что НЕ лечится: `unit_resolutions` — единственный источник ЗАМЕЧАНИЙ и счётчиков главы, и потерянная строка это замечание, которого пользователь не увидит никогда, плюс `units_done`, занижённый навсегда. Лечение — перечитывание хвоста журнала при переоткрытии прогона (курсор новой попытки начинается с размера журнала на допуске); это работа того же класса, что эскроу (строка 136), и в P7 не бралась осознанно | open | приёмка P6 (дофикс, ФП-7) | | PD-380 | hardening | minor | `internal/pgstore/sessions.go:98`=`if _, err := s.pool.Exec(ctx, q, digest, userID, now, now.Add(idleTTL), now.Add(maxAge)); err != nil {`, `internal/config/config_test.go` | **Абсолютный потолок сессии не запинен в единственном месте, где он становится фактом в базе.** `CreateSession` — единственный писатель `sessions.absolute_expires_at`, и мутация этого выражения проходит ПОЛНЫЙ пакет `pgstore`: ни один сессионный тест не краснеет. Пин, который `STACK_DECISIONS` §13 называет носителем потолка, смотрит только на результат `config.Load()` (что значение конфигурации не выше ASVS-предела), то есть проверяет НАСТРОЙКУ, а не то, что она доезжает до строки. Родня `PD-86`, но на шаг раньше: там не запинены клаузы ЧТЕНИЯ и потолок держится транзитивно через `Touch`, здесь не запинена сама ЗАПИСЬ, а транзитивной страховки у неё нет. Воспроизведение: `docs/p8-review/axis2-auth/mutations-axis2.sh` ⚠ **Пере-проверено на ПОЛНОЙ батарее координатором пака (мутация M12):** `now.Add(maxAge)` → `now.Add(100*maxAge)` в `CreateSession`, дельта против чистой копии ПУСТА на всех 18 пакетах. ⚠ Первая редакция этой посадки НЕ СОБРАЛАСЬ — комментарий вставлялся в середину строки и съедал хвост `; err != nil {`; это ошибка харнесса, а не находка, и она названа в логе `docs/p8-review/mutations-round2.log`, чтобы приёмка не пере-открыла её как расхождение ⚠ **ВЕС ПОДНЯТ info → minor закрывающим ревью, довод — симметрия с `PD-375`:** форма идентична (единственная точка принуждения объявленной границы не исполняется ни одним тестом, мутация переживает ПОЛНУЮ батарею, транзитивной страховки нет — `Touch` зажимает по значению ИЗ ТОЙ ЖЕ испорченной строки), а вес расходился только по валюте: там деньги и minor, здесь механизм ASVS 7.3.2 уровня 2 при объявленной зоной базовой линии L2 и info. Асимметрия была отпечатком того самого храповика «вреда сегодня нет», который это ревью и нашло | open | ревью-пак P8-REVIEW, ось 2 (посадка мутации, пере-посажена рефутером на полном пакете) | -| PD-401 | bug | minor | `internal/pgstore/runs.go` `StartRun` (снятие баз), `internal/pgstore/sink.go` `recordWaveShape`, `internal/pgstore/readmodel.go` `runDone`/`runTotal` | **Прогон, стартовавший при `edit_wave = false` и получивший переворот флага ПОСЛЕ старта, делает всю купленную работу с полосой на `0/N` — и это навсегда, а не «окном».** Механика: движок объявляет форму волн первым progress-событием ПОСЛЕ старта (`recordWaveShape`; флаг только растёт, `STACK_DECISIONS` §35), а базы полосы снимались в `StartRun` через флаг-зависимый предикат МОМЕНТА СТАРТА и не пере-базируются никогда — сидят на черновой колонке, числитель после переворота уезжает на редакторскую, разрыв не закрывается. Достижимо штатно: книга прочерчена на пайплайне без редактора, оператор ставит редактора, куплено продолжение; воспроизведено ревьюером приёмки заменой одной строки в пине пака (`edit_wave = false` до старта, переворот после → `0/4` при всей сделанной работе). Тот же симптом «полоса не достигает единицы», что у `PD-281`, — на том «промежуточном классе», которым диспозиция `PD-281` и объясняется. ⚠ Первая редакция строки (заведена опровергателем пака) занижала вес (info) и формулировку: «скачок доли при done>0» и «окно секунды» — секунды живёт ПЕРЕВОРОТ, ущерб его переживает; пере-формулирована и пере-взвешена по слову оркестратора 27.08. ⚠ **Лечение ПОСТРОЕНО в этом же дереве и зелено:** базы снимаются на ФИКСИРОВАННЫХ колонках (`chapters_before` — редакторская, `draft_before` — черновая), пара «числитель+база» выбирается ЖИВЫМ флагом в момент чтения (`runDone`), миграция `00026` пере-снимает базы live-прогонов, порядок переворота запинен ровно как в проде — `TestAFlagThatFlipsAfterStartDoesNotStrandTheBar`; лендить ли лечение этим паком — решение владельца, состав собирает оркестратор, до лендинга строка open. Остаток и ПОСЛЕ лечения: на смешанном заделе (купленный диапазон шире начернённого) переворот двигает знаменатель вверх в окне первого progress-события — транзиентный провал доли, прежняя суть первой редакции ⚠ **ЗАКРЫТА лендингом P9 (58bae30, акт D39.162):** лечение (фиксированные базы + пара живым флагом + миграция 00026 + пин `TestAFlagThatFlipsAfterStartDoesNotStrandTheBar`) заленжено и зелено; тело само велело «до лендинга строка open» — лендинг прошёл, статус переведён по аудиту 28.08. Транзиентный остаток окна первого progress-события остаётся named-границей (PD-401-остаток в словаре зоны) | fixed(лендинг 58bae30, акт D39.162; перевод по аудиту 28.08) | пак P9: опровергатель формы полосы (первая редакция) · блокер ревьюера приёмки 27.08 (пере-формулировка) · аудит документации 28.08 | | PD-407 | bug | minor | `internal/runs/bank.go` (ветка `ctx.Err()` после `BankApply`), `internal/runner/bankapply.go:40` `bankStopGrace` | **Единственный путь двери правок, способный оставить полу-приземлённую пару файлов БЕЗ отчёта, отвечает общим `503 service_unavailable` вместо своего слова `bank_corrections_incomplete` — а комментарий ветки обосновывает её контрактом SIGTERM, который сюда не попадает.** Ветка `ctx.Err() != nil` достижима только при `res.Exited=false`, т.е. когда глагол НЕ вышел сам — SIGKILL по `WaitDelay` (грейс 10 с) или несостоявшийся старт; глагол, поймавший SIGTERM, выходит 5 и идёт через `bankVerdict`. SIGKILL между двумя rename — ровно класс 15, и адресат его слова другой («машина шлёт ТО ЖЕ, никто не пере-решает»). Смягчение: ре-сенд сходится байтовым no-op движка в обе стороны, цена — неточное слово. Рядом: `bankStopGrace` 10 с КОРОЧЕ самого длинного непрерываемого участка глагола (движковый замер: 5000 declines ≈ 11.3 с до пред-записной проверки ctx) — на документе-максимуме SIGKILL опережает штатный останов для большинства моментов отмены ⚠ **ГРЕЙС-ПОЛОВИНА ЗАКРЫТА фикс-раундом P9 (28.08, D39.162):** `bankStopGrace` поднят до 30 с, число обосновано движковым замером в комментарии константы (×2.5 запас на медленный хост); комментарий SIGKILL-ветки переписан честно (описывал соседний путь). ОТКРЫТЫМ остаётся слово: полу-приземлённая пара без отчёта по-прежнему отвечается общим `503` вместо `bank_corrections_incomplete` — смягчение (ре-сенд сходится) в силе | open (грейс-половина закрыта D39.162) | воркфлоу-ревью P9 28.08 (линзы door:interleave · door:crash-windows), диспозиция оркестратора 28.08: строкой; грейс — фикс-раундом | -| PD-409 | hardening | minor | `internal/runs/bank.go` `decisionsFile` (`bank-corrections-*.json` в StateDir, удаление только `defer cleanup()`) | **Отрендеренный документ решений переживает любую нечистую смерть демона и остаётся в StateDir навсегда: ничто в дереве каталог не подметает.** SIGKILL/OOM/паника/истёкший грейс деплоя посреди вызова двери — и файл до 1 МиБ с полными src/dst правленых терминов пользователя лежит под 0600 бессрочно в каталоге, где оператор считает маркеры прогонов; единственная существующая уборка (`os.Remove` exit-маркера в `spawn.go`) эти файлы не видит. Накапливается через деплои: N нечистых смертей = N сирот. Лечение — бут-подметание по маске или переезд под каталог с автоочисткой ⚠ **ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162):** `Service.SweepCorrectionScratch()` подметает по маске на буте (вызов в композиционном корне `tmplatformd` до подъёма HTTP, файлы вне маски не трогаются); пин `TestBootSweepsOrphanedCorrectionDocuments` (посадка «маска мимо» поймана). Честная оговорка: сам ВЫЗОВ из main юнитом не запинен — ловится чтением | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линзы door:crash-windows · door:lock-lifecycle), лечение по слову оркестратора 28.08 | | PD-411 | standards | minor | `internal/pgstore/sink.go:137` и `:447` (два писателя), потребителей НЕТ (греп: ни одного select) | **`runs.draft_done/draft_total/edit_done/edit_total` — носитель с ДВУМЯ писателями и НУЛЁМ читателей: класс A второго яруса онтологии банка (`18-bank-ontology.md`), сосед `PD-314` по форме.** Полоса целиком выведена из `chapters` (runDone/runTotal), эти колонки не читает ни один SELECT — а пишутся они на КАЖДОМ progress-событии внутри транзакции, держащей блокировку строки книги, двумя РАЗНЫМИ дисциплинами (поток присваивает, resync берёт `greatest`), и обоснование `greatest` на `sink.go:444-446` защищает полосу, которой не существует, — следующая сессия будет искать монотонность бара не там, где он живёт. Лечение: снос колонок миграцией + снос обоих писателей и лгущих комментариев; в паке P9 НЕ делается (слово оркестратора 28.08: миграцию на снос в этом паке не заводить) | open | воркфлоу-ревью P9 28.08 (линзы race:flip-read-consistency · bar:double-count · bar:stage-caption), диспозиция оркестратора 28.08: отдельной строкой класса A | | PD-412 | bug | minor | `internal/pgstore/readmodel.go:434-481` (draftChapters ×3 текстовых вхождения, editChapters, chaptersDone, noteCount), `internal/pgstore/books.go` `bookColumns`/`lastRunTx`/`listBooksTx` | **Карточка книги стоит 5 коррелированных сканов `chapters` на один GET, и 2 из них повторяются на КАЖДУЮ строку страницы библиотеки (×100).** PG повторные текстовые вхождения одного подзапроса НЕ дедуплицирует (доказано воркфлоу side-effect-последовательностью на живом PG 18.4), CASE-ветки честно short-circuit; замер на фикстуре в форме миграции 00002 и книге 2283 глав: выражение как написано — 1.359 мс, те же три числа одним `LEFT JOIN LATERAL (count(*) filter (...))` — 0.307 мс (×4.4); контрольный EXPLAIN сессии на реальной схеме tmp9stand: 6 SubPlan в плане, 5 исполняются. Лечение названо (один LATERAL-проход рядом с существующим lastRun); проект уже применял этот класс лекарства уровнем ниже (миграция 00022, `note_count`) | open | воркфлоу-ревью P9 28.08 (линза cost:per-request, замер) + контрольный EXPLAIN сессии P9, диспозиция оркестратора 28.08: строкой | | PD-413 | bug | minor | `internal/pgstore/sink.go:293-309` `emitProgress` (тот же 3-скан агрегат) под `lockBook` из `RunSink.Apply` (`sink.go:80-104`), источник событий: движок шлёт progress на КАЖДЫЙ разрешённый юнит каждой волны | **Каждое progress-событие пересобирает полосный агрегат по ВСЕЙ книге — под удержанной блокировкой строки книги: стоимость прогона растёт как O(глав × юнитов) вместо O(юнитов).** Изменение счётчиков ОДНОЙ главы (unitDone трогает одну строку) на следующей же строке потока триггерит 3 полных скана `chapters` (≈1 мс на книге 2283 глав, замер воркфлоу), сериализуя относительно себя любого другого писателя книги (RequestStop, дверь правок, следующее событие) — за прогон это десятки тысяч повторов, секунды суммарного удержания блокировки. Лечение — то же, что у `PD-412` (один LATERAL), плюс возможная инкрементальность; оркестратор 28.08: «вторая пахнет хуже первой» | open | воркфлоу-ревью P9 28.08 (линза cost:per-request, замер), диспозиция оркестратора 28.08: строкой | | PD-418 | bug | minor | `internal/pgstore/runs.go` `StalledRuns` (settling-ветвь), `internal/pgstore/observe.go`, `AbandonRun` (ветвление по `runs.finished_at`), `deploy/README.md` | **У settling-строки ЖИВОГО прогона нет ручки, а рантбук обещает оператору обратное.** `PD-385` называет ДВЕ популяции, и вторая — «прогон, который ЖИВ, но чья ПРЕДЫДУЩАЯ попытка не рассчиталась после рестарта». Пак P11 дал ей обе поверхности ВИДИМОСТИ (таблица и гейдж ключуются по концу ПОПЫТКИ, так что строка показывается), но `run abandon` ветвится по `runs.finished_at` и на живом прогоне уходит в живую ветку: осиротевший холд он не тронет, а ответит про процесс. Рантбук при этом описывает `PHASE=settling` как то, что лечится этой командой. ⚠ Сегодня состояние НЕДОСТИЖИМО и это часть строки, а не оговорка: единственный не-тестовый путь к второй открытой резервации — `reopen`, который отказывается стартовать следующую попытку, пока холд предыдущей открыт. То есть документ расходится с кодом на состоянии, которого код пока не производит, — и разойдётся заметно, если этот инвариант когда-нибудь ослабнет. Лечение — либо ветвление по НАЛИЧИЮ осиротевшей попытки вместо `finished_at`, либо оговорка в рантбуке | open | пак P11 (самопроход, линза соответствия заказу) + приёмка оркестратора №19 | | PD-419 | bug | minor | `internal/pgstore/migrations_test.go` `TestReleasedMigrationsAreUnchanged`, `internal/pgstore/migrations.sha256` | **Гейт выпущенных миграций слеп к ПЕРЕ-ПОДПИСИ и по построению не может отличить её от нарушения.** Он сверяет файлы против `migrations.sha256`, лежащего в ТОМ ЖЕ дереве, поэтому ловит ровно один сценарий: правку миграции тем, кто забыл про манифест. Автор, который правит ВЫПУЩЕННУЮ миграцию и пере-подписывает её строку одним движением, проходит молча. Оба отказа, ради которых гейт написан, остаются достижимыми через пере-подпись: файл, отредактированный после накатки, больше никогда не запускается (goose применяет по НОМЕРУ и хранит только его), а переиспользованный номер лишает базу отката. Комментарий гейта при этом заявляет «this is the check that makes that true rather than intended». Единственный носитель «что уже выпущено», не лежащий рядом с правкой, — git: гейт мог бы брать `git show HEAD:…migrations.sha256` и требовать побайтового совпадения строк СУЩЕСТВУЮЩИХ там миграций, свободно допуская новые; прогон в дереве без git обязан тогда ГРОМКО скипаться, иначе гейт возвращается туда же, откуда ушёл | open | приёмка оркестратора №19 по паку P11 (замечена на законной пере-подписи `00028`) | | PD-420 | bug | minor | `internal/pgstore/runs_test.go` `TestAClaimThatLostARaceToAReleaseIsRetriedAndNotAnError` | **Тест гонки холда против релиза краснеет под ПАРАЛЛЕЛЬНЫМИ батареями, а зона ратифицировала рецепт, который их требует.** `D39.159` §2 предписывает сажать мутации в КОПИЮ дерева, и всякая сессия, которая делает это всерьёз, гоняет несколько батарей разом. Замер пака P11: при трёх параллельных прогонах краснеет в ЧИСТЫХ копиях (2 раза из 15); в СЕРИЙНОМ прогоне зелен — пере-проверено трижды подряд отдельным прогоном, все три `ok`. Меряет ресурс, общий для копий на машине. ⚠ Цена не косметическая: **красная ЧИСТАЯ копия маскирует дельту**, а красный прогон посадки читается как «мутация поймана», когда она не поймана, — ровно та ложная улика, ради которой мутации и сажают. Обход пака — судить по ДЕЛЬТЕ множеств, а не по коду выхода; лечение — изоляция ресурса либо честный скип под нагрузкой ⚠ **ПОПРАВКА, внесённая до сдачи:** первая редакция этой строки называла вторым фигурантом `TestARunIsBoundedByItsOwnCgroup` — НЕВЕРНО, и это моя ошибка вывода из совпадения. Он краснеет не от нагрузки, а от состояния ХОСТА, и вынесен отдельной строкой `PD-423` | open | пак P11, мутационная кампания (15 прогонов) + пере-проверка серийными прогонами | - +| PD-423 | standards | minor | `internal/runner/systemd_test.go` `TestARunIsBoundedByItsOwnCgroup`, `docs/STACK_DECISIONS.md` «Гейты батареи» | **Батарея зоны требует ЧЕТВЁРТОГО условия хоста, которого рецепт не называет: пользовательский менеджер systemd должен РЕАЛЬНО применять `MemoryMax` к транзиентным юнитам.** Замерено на этом хосте 29.08: тест трижды подряд зелен в полных батареях (`baseline2`, `final2`, `final4`), затем **пять раз подряд красен в изоляции** — при неизменном коде пакета, которого пак не касался вовсе. Причина установлена ВНЕ батареи и вне Go: `systemd-run --user --scope -p MemoryMax=64M …` даёт процессу спокойно занять 400 МиБ и выйти с кодом 0. ⚠ **МЕХАНИЗМ уточнён приёмкой оркестратора №19, и уточнение решает, воспроизводимо ли это:** «systemd не применяет потолок» верно по симптому и мимо по причине. Свойство ДОЕХАЛО — `systemctl --user show -p MemoryMax` печатает `67108864`; делегирование в порядке — `memory pids` и в `cgroup.controllers`, и в `subtree_control`. Пропал не потолок, а cgroup-КАТАЛОГ: в `app.slice` нет ни одного scope-каталога, а `cut -d: -f3 /proc/self/cgroup` для оболочки даёт **`/init.scope`**. То есть вызывающий процесс живёт ВНЕ `user@.service`; `systemd-run --user` заводит юнит в модели менеджера, а процесс остаётся в исходном cgroup, и лимита не получает никто — молча. ⚠ Отсюда и наблюдение «условие отваливается между двумя прогонами одной сессии»: оно зависит от того, из какого cgroup стартовал прогон. Тест при этом ПРАВ и его сообщение точное («check that the leaf cgroup of tm-runs.slice has memory.max»): он ловит ровно то, ради чего написан, — что потолок памяти прогона на этом хосте иллюзорен. ⚠ Следствие для процесса, а не только для теста: рецепт `STACK_DECISIONS` называет три условия батареи, а их четыре, и четвёртое — свойство ХОСТА, которое может отвалиться между двумя прогонами в одной сессии, что здесь и произошло. Сессия, наступившая на это, потратит время на поиск дефекта в своём диффе. ⚠ **Условие для рецепта формулируется НЕ так, как оно там сейчас стоит.** «Достижимый пользовательский менеджер systemd» выполнено — менеджер отвечает, `tm-runs.slice` виден, — и всё равно лимит не применяется. Правильная формулировка: **вызывающий процесс обязан жить ВНУТРИ `user@.service`** (проверка: `cut -d: -f3 /proc/self/cgroup` не должен давать `/init.scope`). Оболочка, поднятая вне пользовательского входа — под WSL это обычный случай, — проходит первую проверку и валит вторую, и следующий читатель решит, что условие выполнено, и пойдёт искать дефект в Go. Лечение: назвать условие в рецепте именно так и дать тесту различать «хост не применяет лимит» (честный скип с причиной) и «раннер не передал лимит» (настоящий отказ) | open | пак P11 (финальная батарея; воспроизведено голым `systemd-run` вне Go) | +| PD-426 | bug | minor | `internal/pgstore/runs.go` `Quarantine`, `internal/ingest/tail.go` (четыре отказа выше ветки `default`) | **Карантин проекции не снимается НИЧЕМ, а попасть в него можно по чужому законному handshake'у.** Первое: `quarantine_reason` пишется, и во всём дереве нет ни одного места, которое его очищает, — то есть состояние терминально для проекции живого оплаченного прогона. Второе: в `ingest/tail.go` четыре отказа стоят ВЫШЕ ветки `default`, которая говорит «другой поток начинается здесь, нас не касается», при том что журнал ПЕР-КНИЖНЫЙ и append-only, так что чужие handshake'ы в нём законны. Вместе: чужой handshake в журнале книги карантинит проекцию прогона, за который заплачено, навсегда. ⚠ Не предмет пака P11 (тейлер и карантин — эры P4/P5), заведено строкой | open | приёмка оркестратора №19 по паку P11 (охотник вне карты) | +| PD-427 | doc | minor | `internal/ingest/resync.go:37-43` (аллоулист `StatusReport`), опровергнуто `backend/internal/pipeline/status.go` `projectStoredMemory` (лендинг `6ec9f8a`, D39.170) | **Комментарий несёт ПОСЫЛКУ, которую сняли, и читается как действующий довод.** Он объясняет, почему платформа сознательно НЕ берёт `rebill_units`/`rebill_usd` через шов: «status проецирует СОХРАНЁННУЮ память, и сразу после `bank-apply` — в единственный момент, когда согласие хотело бы цифру, — он честно читает ноль». Это было верно и ратифицировано (эррата 28.08-к). Движковый пак «деньги» починил ровно это: `foldMemoryForRead` стал ПЕРВЫМ ответом читающего пути, а `projectStoredMemory` понижена до фолбэка, и комментарий движка объявляет это дословно — «IT IS NO LONGER THE READ PATH'S FIRST ANSWER». Слепое окно закрыто, `status` отвечает «сколько будет стоить» ДО покупки, оставаясь $0-глаголом без записи. ⚠ **Комментарий неверен ДВАЖДЫ:** не только посылка, но и предсказанное лечение — он обещает, что «пара вернётся с движковым ГЛАГОЛОМ, который умеет свернуть и оценить коррекцию ВНЕ прогона», а нового глагола не появилось: починили существующий `status`. ⚠ **ПРОВОДКУ ПОЛЕЙ ЭТА СТРОКА НЕ ОТКРЫВАЕТ** (слово оркестратора при передаче): она гейчена вместе с `tmctl translate --max-units`, и тот гейт в силе — движковый потолок объёма на майнящей банк книге пробивался, лечение легло, но проводка ждёт отдельного решения. То есть предмет строки — ровно устаревший ДОВОД, а не отсутствие полей. Класс — «указатель пережил то, на что указывал», тот же, что `PD-310`/`PD-326`/`PD-366`, только в прозе шва. Зеркалит строку 234 единого бэклога | open | оркестратор №19 при лендинге движкового пака (`6ec9f8a`), проверено чтением обеих сторон сессией P11 | +| PD-250 | vuln | minor | `cmd/tmplatformd/main.go` (слушатель метрик) | **`/metrics` отдаётся БЕЗ аутентификации; вся защита — привязка к `127.0.0.1`.** Для одной VM это честная граница, и она записана (STACK §24). Но на хосте с несколькими пользователями любой локальный процесс читает оперативную картину сервиса, а на деплое, где слушатель однажды переедет на `0.0.0.0` «чтобы Prometheus дотянулся», защиты не останется вовсе. Денег в метриках нет (D39.84), поэтому это minor, а не major. Лечение — bearer-токен на слушателе или mTLS, решать при первом внешнем Prometheus ⚠ **ПАК P8-REVIEW 24.08: это ТОТ ЖЕ факт, что `PD-179`, и он стоит в регистре в ДВУХ статусах одновременно** (`accepted-risk(платформа P5, 11.08)` против `open`). Код и ручка одни: `cmd/tmplatformd/main.go` `serveMetrics` без аутентификации, дефолт `127.0.0.1:9464`; рантбук `deploy/README.md` называет строкой риска именно `PD-179`. Своя добавка у этой строки есть (многопользовательский хост), но статус один факт должен нести один. Предложение пака: свести ⚠⚠ **Условие сведения, найденное рефутером:** у `PD-179` довод про ОДНУ VM, а добавка этой строки — многопользовательский хост, где любой локальный непривилегированный процесс скрейпит экспозицию, — в `PD-179` ОТСУТСТВУЕТ. Плюс при сведении из выборок безопасности исчезает класс `vuln` (у `PD-179` он `hardening`). Сводить только ВМЕСТЕ с перенесённой фразой и с пометкой класса | open | research/28 §9 (пинг оркестратора №17), сверено P7 | ## Открытые — info | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | |---|---|---|---|---|---|---| -| PD-398 | standards | info | `docs/scripts/counts.py` `malformed`, строки `PD-99` и `PD-197` этого файла | **Гейт формы регистра ловит только НЕДОСТАЧУ ячеек, а не избыток — и в файле уже есть две строки, которые он пропускает.** `malformed()` сравнивает `if n < shape`, поэтому строка с лишним символом вертикальной черты внутри инлайн-кода проходит молча: счёт ячеек через `awk` с разделителем-чертой даёт `PD-99` (11 ячеек) и `PD-197` (10), а `counts.py --check` печатает `битая форма: []`. Обе строки допаковые, обе сломаны греп-альтернацией и регекспом в тексте. ⚠ Что СМЯГЧАЕТ и почему это `info`, а не выше: `col()` считает колонки С КОНЦА, поэтому статус и вес таких строк читаются ВЕРНО, и сегодняшние числа зоны не врут — пере-проверено, `PD-99` и `PD-197` попадают в свои корзины правильно. Дыра латентная: лишняя черта в одной из ТРЁХ последних колонок сдвинет уже их, и гейт снова промолчит. Пак наткнулся на это ДВАЖДЫ собственной рукой, и второй раз — этой же строкой: первая её редакция цитировала команду счёта ВМЕСТЕ с разделителем-чертой и сломала себя ровно тем, что описывает. моя же правка `PD-396` внесла конвейер в ячейку, счётчик весов выдал мусорный ключ вместо `info`, а `битая форма` осталась пустой — то есть симптом виден в СЧЁТЕ, а не в проверке формы, которая для этого и написана. Лечение однострочное и не моё: `if n != shape` вместо `if n < shape` (зона `docs/`, решение оркестратора). Родня по классу — весь этот пак: гейт, доказывающий меньше, чем читается ⚠ **ПЕРЕ-ФОРМУЛИРОВАНА приёмкой (оркестратор №19, 27.08, D39.159 п.8): предложенная правка ОТКЛОНЕНА замером.** `n != shape` краснит СЕМЬ законных строк (пять бэклога, две регистра), потому что колонки читаются С КОНЦА НАМЕРЕННО — `counts.py:132` называет это прямо и приводит ровно этот случай (строка бэклога 127 несёт три слова через вертикальную черту внутри инлайн-кода). Избыток в ПЕРВОЙ содержательной ячейке легален, и числа из-за него не врут: проверено, статус и вес читаются из хвоста, `PD-99` и `PD-197` разбираются верно. **Настоящий остаточный риск — избыток в ХВОСТОВОЙ ячейке:** там лишняя вертикальная черта сдвинула бы именно статус или вес, и тихо. Ловится не счётом ячеек, а сверкой хвостового словаря (статус ∈ {open, fixed, accepted-risk}); сегодня у такой сверки два известных доброкачественных исключения — `PD-59` и `PD-273` несут статус прозой. Строка остаётся ОТКРЫТОЙ под эту формулировку ⚠⚠ **ВТОРАЯ ПОЛОВИНА ПОСТРОЕНА 27.08 (оркестратор №19), смягчение строки ОПРОВЕРГНУТО.** Довод «читаем с конца, значит лишняя черта безвредна» неверен: он защищает СТАТУС (третья с конца), но не ВЕС — тот читался `col(l,6)`, то есть тоже с конца, а от конца он далеко, и любая лишняя черта в «сути» сдвигала его молча. Живой пример — сама `PD-197`: вес читался из ячейки «где». Эмпирика, принесённая зоной: три случая за двое суток, все у тех, кто в этот момент про этот класс ПИСАЛ (правка `PD-396` · первая редакция этой строки · пере-формулировка её приёмкой), и во всех трёх «битая форма» оставалась пустой. **Построено три вещи, каждая проверена исполнением:** (1) парсер уважает markdown-экранирование — `PD-99` несла корректно экранированные черты, и ломался парсер, а не строка; (2) ВЕС регистра читается с НАЧАЛА (`cells(l)[3]`), потому что ведущие ячейки коротки и черт не несут, хвостовые тоже, а свободный текст живёт в СЕРЕДИНЕ — оба конца безопасны, середина нет; (3) новый гейт `tail_vocab` судит СЛОВАРЬ хвоста (статус и вес), а не счёт ячеек, с одним грандфазерным исключением `PD-59`. Проверено подсадкой: черта в статусную ячейку краснит гейт немедленно; на чистом дереве EXIT=0, числа не сдвинулись (398/96, 3/27/66). ⚠ Правка `n != shape` по-прежнему ОТКЛОНЕНА и теперь на точном основании: после уважения экранирования в бэклоге остаются пять строк с законной сырой чертой внутри инлайн-кода, и их хвост чист. **Строка остаётся ОТКРЫТОЙ ровно на одно: у построенного гейта нет автоматического пина — он проверен подсадкой руками. Закрывать её без пина значило бы нарушить правило `PD-1` в строке, которая про это правило и есть** ⚠ **ЗАКРЫТА 27.08: пин появился.** `selftest_tail_vocab()` в `docs/scripts/counts.py` гоняется на КАЖДОМ `--check` и несёт четыре утверждения — чистая строка молчит · сырая черта в СТАТУСНОЙ ячейке краснит · экранирование законно и лишней ячейкой не считается · черта в СЕРЕДИНЕ не сдвигает вес. Проверен ПОСАДКОЙ, а не заявлением: три мутации гейта (снять уважение к экранированию · вернуть вес на чтение с конца · обезвредить словарь статуса) — пин ловит все три, базовая линия молчит. ⚠ Первая редакция пина МОЛЧАЛА на второй мутации: утверждение про вес проверяло `cells()`, а мутация меняет то, чем пользуется `register()`, — пин смотрел не туда. Переписано на настоящий путь; называю, потому что пин, проверенный одним прогоном вместо посадки, — это ровно тот дефект, который эта строка и описывает | fixed(приёмка 27.08, дерево сессии) | ревью-пак P8-REVIEW (наткнулся при правке собственной строки, подтверждено редакторским аудитом) | -| PD-399 | standards | minor | `internal/pgstore/readmodel.go:330` `bankCountsTx`, `internal/httpapi/reading_test.go:113` | **`GET /books/{bookId}/bank` шлёт два поля, которых канон 0.5.0 больше НЕ объявляет.** Минор снёс `pending_decisions` и `complete` из `BankPage` и `EventBank` вместе с пер-термной моделью, которую они обслуживали, а проекция платформы их по-прежнему кладёт на провод, и это НЕ нули: `bankCountsTx` считает `proposed`-строки, и её собственный комментарий это говорит. Клиент 0.5.0 лишние поля игнорирует по общему правилу канона, поэтому вес minor, но аллоулист-норма нарушена, а комментарий «the wire fields are the canon's» с этого минора ЛОЖЕН. ⚠ Присутствие полей ЗАПИНЕНО (`reading_test.go:113`), значит снятие — правка с пином, а не вычёркивание. Лечение — пункт **(2в)** очереди D39.156, тем же паком, что монтирует дверь `bank/corrections`. ⚠ Найдено САМОЙ контрактной сессией и принесено пингом: промт (§3.2-бис) утверждал «поля навсегда нули», а нота D39.160 п.2 — «после сноса канон и деплой совпадут точно»; верно по ПУТЯМ, неверно по ПОЛЯМ. Ошибка оркестратора, исправлена эрратой ⚠ **ЗАКРЫТА паком P9 (27.08): поля сняты со всех трёх носителей** — `BankCounts`/`payload()` (кадр `EventBank`), `wireBankPage`/`listBank` (провод), подзапрос к мёртвой `bank_decisions` ушёл из `bankCountsTx`; пин `reading_test.go` ПЕРЕПИСАН и теперь пинит ОТСУТСТВИЕ снятых полей на первой странице (`TestTheBankAggregatesRideOnTheFirstPageOnly`) | fixed(пак P9, дерево сессии) | пинг контрактной сессии при сдаче минора 27.08, пере-проверен приёмкой | | PD-396 | standards | info | `internal/pgstore/books.go:306`=`chunker_version = $4, parse_started_at = null,`, `internal/pgstore/sink.go:127`=`update books set chunker_version = $2 where id = $1`, `internal/ingest/resync.go:36`=`UnsignedBankTerms int `json:"unsigned_bank_terms"`` | **Мёртвые поля шва: у `books.chunker_version` ДВА писателя и НОЛЬ читателей, и это второй экземпляр класса, первый назвал оркестратор.** Колонку пишет интейк из манифеста движка (`FinishParse`) и пишет тейлер из хендшейка потока (`effect`); ни одного `select` по ней в зоне нет — грепом ноль. Сегодня это безвредно, но следствие названо ЗАРАНЕЕ, потому что оно семантическое, а не техническое: в день, когда читатель появится, РАСХОЖДЕНИЕ двух писателей (чанкер прогона против чанкера разбора) станет значением, и решать, какой из них правда, придётся задним числом — по колонке, у которой уже накоплена история из обоих источников. Родня — п.4 пинга оркестратора №19: `ingest.StatusReport.UnsignedBankTerms` разбирается из ответа движка и не используется НИ ОДНОЙ строкой продакшн-кода (грепом — только объявление и его доккомментарий, где поле описано как опора экрана подписи). Формулировка оркестратора применима дословно к обоим: мёртвое поле в структуре шва читается как контракт. ⚠ Заведено ОТДЕЛЬНОЙ строкой по прямому указанию закрывающего ревью старшей модели и с его же доводом: этой фразе НЕ место в `PD-166` — та строка про потерю значения между двумя автокоммитами, и её свойство построено; держать дефект-строку открытой как плейсхолдер несуществующей фичи есть ровно та патология «open, а лекарство построено», против которой пак завёл шестнадцать дописок. Диспозиция — вопрос владельца, а не зоны: либо назначить владельца колонки (один писатель), либо записать расхождение как ожидаемое до появления читателя Воспроизведение — две команды без конвейера (символ вертикальной черты в ячейку регистра не влезает): `grep -rn chunker_version platform/internal --include=*.go` даёт два `update` и ни одного `select`, `grep -rn UnsignedBankTerms platform/internal platform/cmd --include=*.go` даёт только объявление и его доккомментарий | open | ревью-пак P8-REVIEW (побочная находка сверки реестра, оформлена по указанию закрывающего ревью) | | PD-378 | bug | info | `internal/pgstore/books.go:1107`=`u.RemainingPercent = int(balance * 100 / granted)`, `internal/httpapi/v0.go` `usageState`, канон `14-api-contract` `remaining_percent` | **`/v0/usage` отдаёт `remaining_percent` вне контрактных 0..100 и зажигает предупреждение «low» на полном счёте: `balance * 100` переполняет int64.** Порог измерен точно: баланс 92 233 720 368 547 758 микро ещё даёт 99%, следующий микро-доллар даёт минус 99. Ответ нарушает схему (`minimum: 0`, `maximum: 100`), и хуже того `usageState` видит отрицательное значение ниже порога `lowCredit` и отдаёт `state: "low"` — «денег почти нет» счёту на сто миллиардов. Замерено на проводе: до гранта `{"state":"ok","remaining_percent":96}`, после `grant --usd 100000000000` → `{"state":"low","remaining_percent":-84}`; соседняя арифметика (`pricing.Scale`, `balance`) при том же балансе отвечает верно, то есть переполнение локально именно в этой строке. ⚠ Рефутер сузил minor → info: чтобы туда попасть, оператор должен добавить на счёт не меньше 92.23 млрд долларов, ни одна пользовательская ручка кредит не пишет; прецедент веса — `PD-39`. ⚠ Оговорка рефутера в другую сторону: более правдоподобный носитель — не разовая команда, а конфиг `TM_PLATFORM_SIGNUP_GRANT_USD`, у которого верхней границы нет и значение НАМЕРЕННО не печатается в стартовый лог, так что промах в нём сломал бы `/usage` каждому новому аккаунту невидимо. Воспроизведение: `docs/p8-review/axis1-money/a1-usage-overflow.sh` (сам откатывает грант) | open | ревью-пак P8-REVIEW, ось 1 (живой провод, сужено рефутером с измеренным порогом) | | PD-381 | hardening | info | `internal/auth/middleware.go:38`=`a.Deny.ServeHTTP(w, r)` и та же строка на `:52`, `internal/auth/cookie.go` `ClearSession` | **401 по мёртвой сессии не стирает куку: браузер продолжает слать отозванный токен до конца её `Max-Age` (по умолчанию 14 суток).** Обе ветки отказа зовут `a.Deny.ServeHTTP` и к `a.Cookies` не обращаются, перекрытия выше по стеку нет — живой 401 не несёт ни одной строки `Set-Cookie`. Норму формулирует сам код: комментарий `ClearLogin` говорит, что кука, пережившая свой круг, это «a replay waiting for an accident», а `PD-88` заведена ровно на тот исход, при котором кука переживает сессию. Дешёвое лечение — чистить куку на пути отказа, где она была предъявлена. Отдельно от `PD-88` (та про `Max-Age` меньше секунды) и от `PD-5`/`PD-70`/`PD-74`/`PD-103` Воспроизведение: `docs/p8-review/axis2-auth/csrf-matrix.sh` и `session-clocks-probe.sh` (обе пробы поднимают демон и печатают ПОЛНЫЕ заголовки ответа, включая отсутствие `Set-Cookie` на 401); проверка чтением — `grep -n 'Cookies' internal/auth/middleware.go`, ни одного вхождения на путях отказа | open | ревью-пак P8-REVIEW, ось 2 (чтение + живая проба, подтверждено рефутером) | @@ -88,8 +86,8 @@ | PD-297 | bug | info | `internal/pgstore/readmodel.go` `writeChapters`/`writeUnits` | **Материализация дерева делает один round-trip на СТРОКУ под эксклюзивной блокировкой книги** — на корпусной книге (2283 главы, ~7 тыс. пар) это ≈11 тыс. последовательных обращений, и всё это время за блокировкой стоят `emitFrame` потока, `StartRun` и фолд юнитов. Штатный инструмент — `tx.SendBatch` (pgx v5, уже драйвер модуля) или `CopyFrom` во временную таблицу. НЕ сделано осознанно: рефутеры первой приёмки понизили до DOUBT/LOW, цена не замерена на форме этого деплоя (unix-сокет против управляемого PG по TCP — разница на два порядка), а путь — самый опасный на запись. Мерить прежде правки: время удержания блокировки на 2283-главной книге до и после ⚠ **P8-FIX: НЕ ВЗЯТ, причина названа и она не «не успели».** Сама эта строка объявляет замер на здешнем стенде НЕпредставительным (unix-сокет против управляемого PG по TCP — разница на два порядка), а корпусной книги нет: она появляется на холодном прогоне движка, которым гейчена строка 202 единого бэклога (решение владельца 20.08). Мерить нечем и не на чем, а правка самого опасного на запись пути без замера — ровно то, что эта строка запрещает. Берётся вместе с холодным прогоном | open | доработка 20.08 (сверка находок против дерева) | | PD-298 | bug | info | `internal/pgstore/readmodel.go` `ListNotes`, `internal/pgstore/sink.go` `unitDone` | **Снятие флага с замечания дельта-чтение выразить не может.** Резолюция, пере-разрешённая как не-`flagged` (редрайв), обновляет строку и двигает `revision`, но дельта фильтруется предикатом `ur.flagged` — строка не возвращается, и клиент никогда не узнаёт, что замечание снято: оно остаётся на экране навсегда. Канон §AfterVersion: «A DELETION cannot be expressed this way», и требует одного из двух ответов — `resync_required` либо `400 version_too_old`; здесь не даётся ни один. ⚠ НЕ подтверждено, что движок вообще пере-издаёт `unit_done` для той же тройки (глава, юнит, волна) с `flagged=false` — комментарий `sink.go` это УТВЕРЖДАЕТ («a redrive re-attacks a flagged one»), но чтением движка не сверено. Порядок: сначала сверка у движка, потом либо счётчик замены для замечаний, либо строка «переход недостижим» ⚠ **ДИСПОЗИЦИЯ АКТА 5 (сверка с движком контрактной сессией 20.08): переход НЕДОСТИЖИМ и механизма не строим.** Движок объявляет вердикт юнита один раз на волну, его announce-once-леджер не пере-announce-ит, поэтому единственная пере-доставка — ТОТ ЖЕ вердикт. Инвариант записан в коде (`sink.go` `unitDone`): если что-то научится снимать флаг, фолд обязан выдать кадр — иначе счётчик разъедется со списком молча. Строка держится открытой этим долгом, а не живым дефектом | open | приёмка P7 → доработка 20.08 (сверка) | | PD-299 | standards | info | `internal/httpapi/conditional.go` `acceptsGzip` | **`Accept-Encoding: identity;q=0` не отвечает `406`.** Клиент, потребовавший ЛЮБОГО кодирования кроме identity, получает identity. Половина RFC 9110 §12.5.3, которую правка PD-268 не закрыла: gzip-сторона (именованное кодирование выигрывает у `*`, нулевой вес — отказ) закрыта и пиньётся, эта — нет. Достижимо только специально сконструированным клиентом; ни один генерённый по контракту клиент так не делает | open | доработка 20.08 (сверка находок против дерева) | -| PD-368 | hardening | info | `internal/config/config.go`, `internal/runs/reconcile.go` `phaseBudget` | **Пара `TM_PLATFORM_SWEEP_BUDGET`/`TM_PLATFORM_RUN_BUDGET` не проверяется на когерентность на буте, и поднять ОДИН из них — тихий no-op:** фазе достаётся половина прохода, поэтому любое значение `RunBudget` от половины прохода и выше наблюдаемо неотличимо от дефолта. Ревью заходило сюда как в major («поднять бюджет = объявить здоровый прогон застрявшим») и **это опровергнуто исполнением**: поднятие ручки не меняет вообще ничего, вердикт при дефолтах существует и без оператора, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами шиппящиеся дефолты (60 с против 60 с). Остаётся эргономика: поднимать надо `SweepBudget`, и об этом не сказано нигде, кроме доккоммента ⚠ **ПАК P8-REVIEW 24.08: остаток («поднимать надо `SweepBudget`, и об этом не сказано нигде, кроме доккомментария») УЖЕ ЗАКРЫТ, и закрыт тем же паком, чья приёмка эту строку написала.** `deploy/README.md` несёт таблицу обеих ручек и прямо под ней ⚠-абзац «Поднимать надо ПАРУ, а не одну… `RUN_BUDGET` выше половины `SWEEP_BUDGET` наблюдаемо ничего не меняет», со ссылкой на этот же номер. Верным остаётся только то, что когерентность пары не проверяется на буте. Предложение пака: сузить до этого или закрыть. ⚠ Связанное, но ДРУГОЕ: сама ручка не покрывает такт свипа целиком — `PD-387` ⚠⚠ **Уточнение рефутера, меняющее диспозицию: ЗАКРЫВАТЬ строку ЦЕЛИКОМ нельзя, только сузить.** Абзац рантбука приехал коммитом `31f1f82` — ТЕМ ЖЕ, который внёс саму строку, то есть остаток родился уже закрытым. А проверки когерентности пары на буте по-прежнему нет: `internal/config/config.go:460-463` грузит обе ручки без сверки. Сузить до этого и оставить `open`. ⚠ И соседство: абзац стоит под строкой `deploy/README.md:288`, которая сама предмет открытой `PD-387` | open | воркфлоу-ревью волны 2 (P8-FIX), находка опровергнута, остаток зафиксирован | -| PD-373 | doc | info | `internal/readmodel/readmodel.go:64`=`const maxAttempts = 5`, `deploy/README.md:273`=`После пяти неудач` | **У числа попыток материализации ДВА носителя и ничего между ними.** Код держит `const maxAttempts = 5`, рантбук оператора пишет «После пяти неудач долг списывается». **Посажена мутация оркестратором вне списка автора:** `maxAttempts` 5 → 500000, батарея зелёная — пин `readmodel_test.go` ездит `for attempts := range maxAttempts`, то есть доказывает МЕХАНИЗМ при любом значении константы, что само по себе правильно. Незакрытым остаётся другое: подняли константу — рантбук молча начал лгать оператору о том, когда платформа сдаётся. ⚠ Тот же ход у `runs.StalledAfter` проверен и НАРУШЕНИЯ НЕ ДАЛ: там носитель ровно один (рантбук пишет «сколько неудач подряд» без числа), мутация тоже выжила и это законно. Лечение — либо гейт на второй носитель ровно той формы, что пак построил для `ContractVersion` (`internal/gates/contract_test.go` читает канон, а не копию числа), либо число уходит из прозы | open | приёмка P8-FIX (посадка мутации оркестратором №18) | +| PD-368 | hardening | info | `internal/config/config.go`, `internal/runs/reconcile.go` `phaseBudget` | **Пара `TM_PLATFORM_SWEEP_BUDGET`/`TM_PLATFORM_RUN_BUDGET` не проверяется на когерентность на буте, и поднять ОДИН из них — тихий no-op:** фазе достаётся половина прохода, поэтому любое значение `RunBudget` от половины прохода и выше наблюдаемо неотличимо от дефолта. Ревью заходило сюда как в major («поднять бюджет = объявить здоровый прогон застрявшим») и **это опровергнуто исполнением**: поднятие ручки не меняет вообще ничего, вердикт при дефолтах существует и без оператора, а строгая проверка `RunBudget < SweepBudget/2` отвергла бы сами шиппящиеся дефолты (60 с против 60 с). Остаётся эргономика: поднимать надо `SweepBudget`, и об этом не сказано нигде, кроме доккоммента ⚠ **ПАК P8-REVIEW 24.08: остаток («поднимать надо `SweepBudget`, и об этом не сказано нигде, кроме доккомментария») УЖЕ ЗАКРЫТ, и закрыт тем же паком, чья приёмка эту строку написала.** `deploy/README.md` несёт таблицу обеих ручек и прямо под ней ⚠-абзац «Поднимать надо ПАРУ, а не одну… `RUN_BUDGET` выше половины `SWEEP_BUDGET` наблюдаемо ничего не меняет», со ссылкой на этот же номер. Верным остаётся только то, что когерентность пары не проверяется на буте. Предложение пака: сузить до этого или закрыть. ⚠ Связанное, но ДРУГОЕ: сама ручка не покрывает такт свипа целиком — `PD-387` ⚠⚠ **Уточнение рефутера, меняющее диспозицию: ЗАКРЫВАТЬ строку ЦЕЛИКОМ нельзя, только сузить.** Абзац рантбука приехал коммитом `31f1f82` — ТЕМ ЖЕ, который внёс саму строку, то есть остаток родился уже закрытым. А проверки когерентности пары на буте по-прежнему нет: `internal/config/config.go:460-463` грузит обе ручки без сверки. Сузить до этого и оставить `open`. ⚠ И соседство: абзац стоит под строкой `deploy/README.md:311`, которая сама предмет открытой `PD-387` | open | воркфлоу-ревью волны 2 (P8-FIX), находка опровергнута, остаток зафиксирован | +| PD-373 | doc | info | `internal/readmodel/readmodel.go:64`=`const maxAttempts = 5`, `deploy/README.md:296`=`После пяти неудач` | **У числа попыток материализации ДВА носителя и ничего между ними.** Код держит `const maxAttempts = 5`, рантбук оператора пишет «После пяти неудач долг списывается». **Посажена мутация оркестратором вне списка автора:** `maxAttempts` 5 → 500000, батарея зелёная — пин `readmodel_test.go` ездит `for attempts := range maxAttempts`, то есть доказывает МЕХАНИЗМ при любом значении константы, что само по себе правильно. Незакрытым остаётся другое: подняли константу — рантбук молча начал лгать оператору о том, когда платформа сдаётся. ⚠ Тот же ход у `runs.StalledAfter` проверен и НАРУШЕНИЯ НЕ ДАЛ: там носитель ровно один (рантбук пишет «сколько неудач подряд» без числа), мутация тоже выжила и это законно. Лечение — либо гейт на второй носитель ровно той формы, что пак построил для `ContractVersion` (`internal/gates/contract_test.go` читает канон, а не копию числа), либо число уходит из прозы | open | приёмка P8-FIX (посадка мутации оркестратором №18) | | PD-374 | doc | info | `docs/STACK_DECISIONS.md` «Гейты батареи», `internal/runner/systemd_test.go` `systemdOrSkip`, `Makefile` цель `check` | **Рецепт объявляет у батареи ДВА гейта и ждёт «скипов 0» — а условий три, и третье не названо.** Кроме `TM_PLATFORM_TEST_DSN` и пары `TM_PLATFORM_TEST_ENGINE_BIN`/`_BOOK_TEMPLATE` есть `systemdOrSkip`: без ДОСТИЖИМОГО пользовательского менеджера systemd три теста `internal/runner` скипаются. Поймано пере-прогоном батареи при приёмке: на этом хосте `/run/user/1000` не существует (сессия logind не поднята), поэтому «скипов 0» недостижимо в принципе — `make check` дал 18 пакетов, exit 0, линтер 0 issues и **3 скипа**. Мимо: сама цель `check` печатает над списком скипов «set TM_PLATFORM_TEST_DSN», отправляя читателя к ручке, которая тут ни при чём. Отчёт пака честен и это подтверждает — он мерил отдельно и получил 0, что верно на хосте с живым менеджером. Лечение: назвать третий гейт в рецепте вместе с двумя и не обещать «скипов 0» без него; заодно сделать сообщение цели `check` не называющим одну переменную из трёх ⚠ **ПАК P8-REVIEW 24.08: половина лечения УЖЕ в дереве.** `docs/ENGINEERING_STANDARDS.md` §3 п.1 переписан и называет ТРИ условия поимённо, включая достижимый пользовательский менеджер systemd, со ссылкой на этот номер. Названные строкой носители не тронуты: `docs/STACK_DECISIONS.md` по-прежнему пишет «Гейты батареи — их ДВА» и «скипов 0», а сообщение цели `check` в `Makefile` называет одну переменную из трёх. Предложение пака: сузить до этих двух. ⚠ На ЭТОМ хосте все три условия выполнимы (`/run/user/1000` жив), поэтому пак снял базовую линию со скипами 0 — см. `docs/p8-review/battery-final.log` | open | приёмка P8-FIX (пере-прогон батареи оркестратором №18) | | PD-6 | hardening | info | `internal/auth/csrf.go:51` | GET освобождён от CSRF (верно), но SSE-хендшейк — GET с амбиентной кукой: origin-чек хендшейка потока (STACK §5) не покрыт ничем. Закрыть при постройке SSE (P1) ⚠ **ПАК P8-REVIEW 24.08: условие закрытия НАСТУПИЛО, лекарство не приехало, а защита оказалась ТРАНЗИТИВНОЙ.** SSE построен (`internal/httpapi/v0.go` маршрут `/books/{bookId}/events`, `internal/httpapi/stream.go` `streamEvents`, коммит `9b23e8c`), origin-чека не появилось: GET освобождён в `internal/auth/csrf.go` `cookieUnsafe`, а `http.CrossOriginProtection` судит только unsafe-методы. Живая проба: хендшейк с `Origin: https://evil.example` и амбиентной кукой отвечает 200 и стримит, без куки 401, заголовок `X-TM-Client` не требуется (`docs/p8-review/sse-origin-probe.txt`). ⚠ Вес поднимать НЕ предлагается, и это измеренная поправка к предложению аудита: браузерный случай сегодня закрыт ТРЕМЯ механизмами, ни один из которых не является чеком хендшейка — кука `SameSite=Lax` не уходит на кросс-сайтовый подзапрос, префикс `__Host-` не отдаёт её соседнему поддомену, а CORS-слоя нет вовсе (`PD-96`), поэтому кросс-origin `EventSource` браузер странице не отдаст. Это ровно класс `PD-86`: свойство держится, и держат его посторонние механизмы, ни один из которых не запинен как защита хендшейка. Диспозиция — вопрос приёмке: чек хендшейка или явное принятие с записью трёх носителей | open | приёмка P0 (security-линза) | | PD-23 | hardening | info | `internal/pgstore/migrations/00001_identity.sql` | Журнал входов растёт без ретенции и чистится только каскадом при удалении аккаунта. Нужен свип по возрасту (год?) — вопрос политики, не кода ⚠ **ПРОВЕРЕНО ПАКОМ P8-REVIEW 24.08 И ПРОТИВ ДЕРЕВА ЛОЖНО: ретенция ПОСТРОЕНА и РАБОТАЕТ.** `internal/pgstore/identity.go` `DeleteOldLoginEvents` (`delete from login_events where at < $1`) зовётся из `cmd/tmplatformd/main.go` циклом `sweepLogins` каждые 15 минут с окном `loginJournalRetention = 180 * 24 * time.Hour`, и свип монтируется в ОБЕИХ ветках входа. Приехало коммитом `9b23e8c` (лендинг P7), строка не обновлялась с P1. Доказано не чтением: строка возрастом 200 суток исчезла на ближайшем тике, демон напечатал `"login sweep" events=1` (`docs/p8-review/pd23-result.txt`). Предложение пака: ЗАКРЫТЬ, срок 180 суток отметить как выбранную политику. Незапиненность самого механизма вынесена отдельной строкой `PD-383` | open | самопроверка P1 | @@ -111,7 +109,6 @@ | PD-139 | hardening | info | `internal/runs/reconcile.go` ERROR-строки | Путь каталога книги попадает в ERROR-логи внутри обёрнутых ошибок (`*fs.PathError` тейлера, обёртки спавна). PD-99 закрывал ДРУГОЕ — argv на INFO, — и та половина проверена (`grep` по логу сквозной пробы = 0). Здесь диспозиция иная и её надо принять осознанно: оператор чинит именно этот путь, а ERROR — не INFO. Заведено, чтобы это было решением, а не побочным эффектом; если норма зоны распространяется и на ERROR, путь придётся заменить на id прогона ⚠ Дополнено P5: у класса появилась ВТОРАЯ площадка — интейк. Путь книги уходит в ERROR только в двух местах и намеренно: терминальный отказ разбора (`err` движка несёт путь исходника) и неудавшееся удаление каталога. На INFO/WARN идентификатора книги нет вовсе, и это запинено `books.TestNoBookIdentifierReachesAnInfoLine`. Диспозиция по-прежнему нужна одна на класс | open | адверсариальное ревью (чтение) | | PD-153 | bug | info | `internal/runs/reconcile.go` `spawnGrace` | **Грация спавна мерится от `run_attempts.started_at`, а не от момента заявки права на спавн:** окно между `RecordSpawn` и возвратом `systemd-run` не покрыто, и второй инстанс платформы, у которого грация уже истекла, может решить, что попытка потеряна, и перезапустить прогон, который вот-вот стартует. Дёшево закрывается временем заявки, записываемым в `RecordSpawn`, и грацией от него ⚠ Сужено дофиксом приёмки: у СТОП-пути окно закрыто с обеих сторон — заявка на спавн не выдаётся прогону с висящим интентом, а закрытие «стопа до спавна» спрашивает systemd про детерминированное имя юнита, если у попытки есть базовая линия (то есть заявка когда-то бралась). Само окно PD-153 — грация от `started_at`, а не от момента заявки — не тронуто | open | приёмка P4 (N2) | | PD-154 | bug | info | `internal/runs/reconcile.go` `settle` | **`runs.settled_at` может остаться NULL между `Settle` и `MarkSettled`:** это два вызова, и падение между ними оставляет прогон с закрытой резервацией и без отметки. Потребителей у отметки сегодня нет (рабочий список расчёта построен на открытой резервации, а не на ней), деньги целы и второй расчёт отвергается самой резервацией. ⚠ Дофикс 09.08 добавил вторую половину той же строки: `settle` прерванной попытки ЖИВОГО прогона (путь `UnsettledRuns`) ставит `settled_at` прогону, который ещё идёт. Потребителей у колонки по-прежнему нет, дрейф только операторский. Заведено как известность, а не как долг: закрывается вместе с эскроу (строка 136) ⚠ **ПЕРЕ-ДИСПОЗИЦИЯ (P6): остаётся открытой в прежней формулировке.** Пак трогал `settle` (запиненный бинарь, аддендум оркестратора 14.08) и окно не закрывал: закрытие требует write-ahead intent и состояния `closing`, то есть эскроу строки 136, а строить половину эскроу рядом с проектируемым целым — это второй, более слабый ответ на тот же вопрос. Потребителей у колонки по-прежнему ноль | open | приёмка P4 (N3) | -| PD-166 | bug | info | `internal/ingest/tail.go`, `internal/pgstore/sink.go` `Begin` | **`chunker_version` из хендшейка теряется навсегда,** если краш пришёлся между двумя стейтментами `Begin` (привязка `engine_run_id` и запись версии — два отдельных автокоммита): при повторном чтении своего же `hello` тейлер видит, что поток уже привязан, и `Begin` больше не зовёт. Сегодня поле никем не читается (нужно для строки 100), поэтому info; закрывать — одной транзакцией в `Begin` ⚠ **ПАК P8-REVIEW 24.08: первая фраза сужена построенным.** У колонки `books.chunker_version` появился ВТОРОЙ независимый писатель — интейк (`internal/pgstore/books.go` `FinishParse` пишет её из манифеста движка). Значит крах между двумя стейтментами `Begin` оставляет не пустую колонку, а СТАРОЕ значение разбора: теряется дельта, а не значение. Читателя по-прежнему нет, вес `info` верен ⚠⚠ **МОЯ ДОПИСКА ВЫШЕ ОПРОВЕРГНУТА РЕФУТЕРОМ, и опровергнута верно — снимаю её.** Механизм, который строка описывает (крах между ДВУМЯ автокоммитами `Begin`), СЕГОДНЯ НЕДОСТИЖИМ: запись версии переехала из `Begin` в `effect` и идёт в ОДНОЙ транзакции с курсором (`internal/pgstore/sink.go:105-127`), а сам `Begin` объявлен legacy (`sink.go:33-38`) и для попыток этой сборки недостижим — `engine_run_id` присваивается в INSERT попытки и бэкфилится в `RecordSpawn`. Доказано исполнением: при уцелевшей привязке и нулевом курсоре хендшейк идёт в `Apply`, а не в `Begin` (`Begin calls=0 Apply calls=1`), то есть терять между двумя стейтментами нечего. Посадка мутации (вырезан `case ingest.TypeHello` из `effect`) роняет `TestTheChunkerVersionOfTheStreamReachesTheBook` — атомарный писатель есть и запинен. **Правильная диспозиция — не «сузить до потери дельты», а ЗАКРЫТЬ как построенное:** атомарность, которой строка требовала, существует. Читателя у колонки по-прежнему нет, и это отдельный факт, а не этот дефект ⚠ **ЗАКРЫТА актом лендинга P9 (D39.162):** акт объявил закрытие состоявшимся, статус переведён по аудиту документации 28.08 — обязательство акта висело неисполненным (класс «строка open при легшем лечении», носитель — строка бэклога 225) | fixed(акт D39.162, перевод по аудиту 28.08) | самопроверка дофикса (ревью вне карты) · аудит документации 28.08 | | PD-170 | hardening | info | `internal/pgstore/credits.go` `Settle` | `Settle` отбрасывает флаг `applied` строки расчёта, тогда как `releaseHold` на соседней строке из того же флага делает `ErrReleaseKeySpent` (PD-97). Недостижимо без правки леджера в обход кода — резервация должна быть открыта, чтобы дойти сюда, — но асимметрия в денежном пути стоит строки: либо симметричный отказ, либо явная причина, почему здесь он не нужен | open | самопроверка дофикса (ревью вне карты) | | PD-176 | hardening | info | `internal/httpapi/middleware.go` `LimitBody`, `internal/metrics` | **Сигнал `requestTooLarge` от `http.MaxBytesReader` до сервера не доходит через наши обёртки.** `MaxBytesReader` пытается сказать *`net/http`* «запрос слишком большой» приведением `ResponseWriter` к НЕЭКСПОРТИРУЕМОМУ интерфейсу пакета `net/http`; наши обёртки (`statusRecorder`, `metrics.recorder`) его удовлетворить не могут в принципе — метод неэкспортируемый, а значит квалифицирован чужим пакетом. Следствие мягкое и замерено рассуждением по коду `net/http`: 413 отдаётся штатно, а соединение закрывается не немедленным сигналом, а обычным путём — после хендлера сервер дренирует остаток тела и, не сумев дочитать, закрывает соединение, причём дренаж ограничен `ReadTimeout` (PD-2). Лечение (если понадобится): ставить лимит без обёрток над `ResponseWriter` либо закрывать соединение самим через `Connection: close` | open | сессия P5 (чтение stdlib при постройке маршрута) | | PD-177 | bug | info | `internal/books/books.go` `counter` | **`character_count` для не-UTF-8 источника — оценка, а не счёт.** Интейк считает символы потоково как байты, не являющиеся продолжением UTF-8 (`b&0xC0 != 0x80`), что для валидного UTF-8 ТОЧНО равно числу рун и не требует состояния между чанками. Движок принимает и GB18030, и UTF-16, декодируя их сам — на таком файле цифра неверна (для UTF-16 занижена примерно вдвое). Точный ответ есть на шаг позже: манифест несёт `source_bytes` и `encoding`, но не число символов. Лечится либо запросом числа символов у движка (строка единого бэклога), либо перерасчётом после разбора | open | сессия P5 (названо при постройке) | @@ -130,11 +127,8 @@ | PD-247 | bug | info | `internal/httpapi/stream.go` `pump` | **Живой поток ОПРАШИВАЕТ базу дважды в секунду на каждое открытое соединение** (кадры + состояние книги). Кадры минтят писатели в своих транзакциях и в других процессах, поэтому подписки у этой стороны нет; выбран опрос, а не `LISTEN/NOTIFY`, потому что кадр это ПОКА, и секунда задержки на поке не наблюдаема рядом с переводом. Цена названа числом: 12 вкладок на книгу = 24 запроса/с к Postgres, оба по индексу и по одной книге. Лечится `pg_notify` в `emitFrame` + один слушатель на процесс — работа на полдня, которая нужна не раньше второго десятка одновременных читателей | open | сессия P7 (собственная оценка) | | PD-248 | bug | info | `internal/readmodel/readmodel.go` `Refresh` | **Материализация читающей поверхности стоит ДВУХ полных ре-чанков исходника** — `tmctl manifest --json` и `tmctl export --json --pairs`, каждый из которых заново ингестит и режет книгу (~1,4–1,5 с CPU на 23 МБ, строка 100 единого бэклога). Зовётся на границах работы (конец интейка, конец прогона), то есть не на запрос пользователя, но на книге в 2283 главы это секунды CPU и десятки мегабайт JSON через пайп на каждый конец прогона. Дешевле было бы читать сайдкар манифеста напрямую (он уже лежит рядом с БД проекта) и просить у движка экспорт ТОЛЬКО изменившихся глав — второго канала у движка нет, это запрос строкой единого бэклога **Доработка 20.08:** интейк больше не платит за ТРЕТЬЮ ре-нарезку — `books.Parse` передаёт уже прочитанный манифест в `readmodel.RefreshCut`; остаются два (манифест + экспорт), и это цена самих каналов. | open | сессия P7 (собственная оценка) | | PD-249 | bug | info | `internal/pgstore/runs.go` `ReadRunForSpawn` | **Чтение прогона под спавн линейно по числу ЖИВЫХ прогонов:** читает их все и ищет нужный в цикле. Сегодня незаметно (живых прогонов единицы), но это O(живых) на КАЖДУЮ задачу спавна, и растёт ровно тогда, когда платформа становится нужной. Находка §9 контракт-ревью (research/28), проверена чтением кода: запрос действительно без предиката по id | open | research/28 §9 (пинг оркестратора №17), сверено P7 | -| PD-250 | vuln | minor | `cmd/tmplatformd/main.go` (слушатель метрик) | **`/metrics` отдаётся БЕЗ аутентификации; вся защита — привязка к `127.0.0.1`.** Для одной VM это честная граница, и она записана (STACK §24). Но на хосте с несколькими пользователями любой локальный процесс читает оперативную картину сервиса, а на деплое, где слушатель однажды переедет на `0.0.0.0` «чтобы Prometheus дотянулся», защиты не останется вовсе. Денег в метриках нет (D39.84), поэтому это minor, а не major. Лечение — bearer-токен на слушателе или mTLS, решать при первом внешнем Prometheus ⚠ **ПАК P8-REVIEW 24.08: это ТОТ ЖЕ факт, что `PD-179`, и он стоит в регистре в ДВУХ статусах одновременно** (`accepted-risk(платформа P5, 11.08)` против `open`). Код и ручка одни: `cmd/tmplatformd/main.go` `serveMetrics` без аутентификации, дефолт `127.0.0.1:9464`; рантбук `deploy/README.md` называет строкой риска именно `PD-179`. Своя добавка у этой строки есть (многопользовательский хост), но статус один факт должен нести один. Предложение пака: свести ⚠⚠ **Условие сведения, найденное рефутером:** у `PD-179` довод про ОДНУ VM, а добавка этой строки — многопользовательский хост, где любой локальный непривилегированный процесс скрейпит экспозицию, — в `PD-179` ОТСУТСТВУЕТ. Плюс при сведении из выборок безопасности исчезает класс `vuln` (у `PD-179` он `hardening`). Сводить только ВМЕСТЕ с перенесённой фразой и с пометкой класса | open | research/28 §9 (пинг оркестратора №17), сверено P7 | | PD-251 | bug | info | `internal/login/login.go` (лимитер входа) | **Лимитер входа один на ПРОЦЕСС, а не на адрес:** один клиент, долбящий `/auth/login`, расходует общее ведро и запирает вход всем. Выбор объяснён комментарием (за прокси адрес клиента без доверенного `X-Forwarded-For` — это адрес прокси, и пер-адресное ведро тогда защищает не то), но следствие не было записано. Лечение появляется вместе с доверенным заголовком прокси на деплое ⚠ **ПАК P8-REVIEW 24.08: это ТОТ ЖЕ факт, что `PD-42`, и он стоит в регистре в ДВУХ статусах одновременно.** `PD-42` (`accepted-risk(платформа P1, 05.08)`) описывает то же самое ведро с замером «8 отказов из 10 при фоне 5 rps», и код один — `internal/login/login.go` `startLimit` с комментарием «Deliberately GLOBAL rather than per-address». Клейм этой строки «следствие не было записано» неверен: оно записано раньше и принято. Предложение пака: свести — либо закрыть эту дублем, либо снять принятие риска с `PD-42` ⚠⚠ **Условие сведения, найденное рефутером и обязательное к переносу: НИ ОДНА из двух строк не называет ВТОРОЕ глобальное ведро.** Кроме `startLimit` на `/auth/login` есть `finishLimit` на `/auth/callback` (`internal/login/login.go:71,129,232`) — тот же дефект на второй половине потока входа, и он не покрыт ни одной строкой регистра. Сводить `PD-251` в `PD-42` можно только назвав в объединённой строке ОБА ведра и ПЕРЕ-подтвердив принятие риска явно, а не унаследовав его: иначе открытый баг молча превращается в принятый риск, а половина дефекта исчезает из регистра вовсе | open | research/28 §9 (пинг оркестратора №17), сверено P7 | | PD-252 | bug | info | `internal/runs/reconcile.go` (карантин проекции) | **Сырой Go-текст ошибки сохраняется причиной карантина в пользовательской строке БД.** На провод он не идёт (карантин не проецируется ни одним полем контракта), но это движковый и внутренний текст в таблице, которую читают дампами; тот же класс, что `Problem.detail`, только в базе. Лечение — закрытый словарь причин карантина, как у `books.reject_reason` | open | research/28 §9 (пинг оркестратора №17), сверено P7 | -| PD-254 | standards | info | `internal/httpapi/problem.go` `WriteStatusProblem`, `codeForStatus` | **Два писателя ошибок на одну форму тела.** `WriteProblem` выводит статус ИЗ кода (пара не может разойтись), а `WriteStatusProblem` идёт обратно — от статуса к коду — потому что вне версионного префикса (`/auth`, `/readyz`) есть статусы, которых в словаре контракта нет вовсе (405, 429). Свести в один писатель можно только назначив форму ответа для этих двух статусов, а это ратификация, не правка зоны. Пока — два пути и обратная функция рядом с прямой ⚠ **ЗАКРЫТО решением контрактной сессии 17.08 (релей владельца):** поверхность входа отвечает тем же конвертом и БЕЗ машинного кода — это ратифицированное решение, а не пробел («различать причины отказа клиент не может по замыслу», компаньон §2.14 с 0.2.3), а единственный осмысленный для пользователя случай `429` машинен без словаря, потому что лечение едет в `Retry-After`. Следствие для кода: писатель входа кода не эмитит, `codeForStatus` и `writeRaw` удалены — обратной функции, то есть второго источника истины, больше нет. Пин `httpapi.TestTheSignInSurfaceAnswersTheSameEnvelopeWithoutAVersionedCode` (шесть статусов). Отвергнуто с доводом: расширение `ErrorCode` (значения, недостижимые на описываемой им поверхности, ломают инвариант «код называет свой статус») и словарь в компаньоне (документ, не нормативный для формы, стал бы нормативным с чёрного хода) | fixed(P7, дерево сессии) | кросс-модельное ревью P7 (линза скоупа) | -| PD-255 | doc | info | `internal/pgstore/migrations/00016_read_surface.sql`, `internal/pgstore/readmodel.go` | **Плотность комментариев выше нормы зоны** («одна-две строки почему», владелец 26.07): миграция 00016 — 250 строк, из них около половины проза; у `readmodel.go` многие символы несут абзацы. Часть прозы несущая (порядок delete/insert в двух таблицах — ровно то, на чём пак и споткнулся), часть — эссе. Подрезано самое тяжёлое; остальное — предмет решения владельца о норме, а не тихой правки ⚠ **ЗАКРЫТО решением владельца 21.08: НОРМА СМЕНИЛАСЬ, и строка была открыта против снятой формулы.** Счёт строк снят как негодный гейт — комментарий на три строки может быть нужен, на одну достаточен; режется ВОДА (пересказ решений, провенанс, изложение исследования вместо ссылки), а всё, что из одной функции НЕ выводится — порядок блокировок, инварианты между таблицами, цена забывания, вендор-квирк — остаётся, сколько бы строк ни заняло. Формулировка — `docs/architecture/12-go-style-notes` §1. Обе названные здесь прозы под новой нормой законны: порядок delete/insert между двумя таблицами из функции не выводится, а сам дефект пака это доказал. Тихой правки не было и не будет | fixed(решение владельца 21.08) | кросс-модельное ревью P7 (линза энтропии) | | PD-256 | bug | info | `internal/ingest/export.go` `DecodeExport` | **Сигнал дрейфа конфигурации движка не читается.** `tmctl export` отдаёт `config_drift`/`current_snapshot` — «текущая конфигурация рендерит не тот снапшот, что несут сохранённые строки», то есть диспозиции могут не описывать то, что прогон делал. Платформа материализует текст без этого признака: на проводе места для него нет (и не должно быть — это операторский факт), но в логе материализатора он был бы уместен | open | кросс-модельное ревью P7 (линза шва) | | PD-345 | hardening | info | `internal/runner/engine.go` `readEngine`, `firstLine` | **stderr движка читается БЕЗ потолка, вплотную к намеренному потолку на stdout.** `doc` берётся через `io.LimitReader(out, limit+1)` и переполнение отдельно судится (`drain`, «a cap that is at the mercy of the thing it bounds is not a cap»), а `errOut` — простой `bytes.Buffer`, и `firstLine` отдаёт из него префикс любой длины. Асимметрия и есть находка: рядом стоит комментарий, объясняющий, почему потолок обязателен, и на соседнем канале его нет. Сегодня цена ограничена (процесс короткий, движок свой), но строка уезжает в ошибку, в лог и — до P8-FIX — в колонку БД (это половина `PD-340`, там закрыта на стороне хранения). ⚠ **Найдено ревью P8-FIX и НЕ чинится этим паком: вне его скоупа**, названо, чтобы не потеряться | open | ревью P8-FIX (контракт-конформность, соседство с PD-340) | | PD-245 | standards | info | `internal/ingest/supervisor.go` | **У дев-супервизора нет ни одного потребителя вне собственных тестов** (`grep` по зоне): это дев-путь D39.106 §3, который пережил постройку продового шва. Его чинят и держат в шаге с продовым (PD-224, PD-237) — но либо он должен быть подключён к дев-режиму демона, либо снят вместе со своими тестами; сейчас это код, который стоит сопровождения и ничего не обслуживает | open | кросс-семейное ревью дофикса P6 (линза шва) | @@ -142,12 +136,8 @@ | PD-220 | hardening | info | `internal/config/config.go` `Load` | **Резерв имени `dev` сравнивается байт-в-байт:** `TM_PLATFORM_OIDC_PROVIDER=Dev` проходит гейт. Сегодня инертно, и ровно по той же причине: Postgres сравнивает `identities.provider` тоже байт-в-байт, поэтому в пространство имён дев-входа такой издатель не попадает. Станет опасным в день, когда сравнение личности станет регистронезависимым | open | приёмка P6 (дофикс, ФП-7) | | PD-221 | bug | info | `internal/runs/spawn.go` `engineStreamID`, `internal/pgstore/runs.go` `EngineStreamID` | **Имя потока переиспользуется при повторном спавне ТОЙ ЖЕ попытки:** оно детерминировано по паре (прогон, номер попытки), а движок отвергает id, который уже писал события этой книги, и чеканит свой (`store.EventsUsed`, `pipeline/events.go`). Тогда платформа не узнаёт собственный поток и живёт на медленном ре-синке — свежесть, не деньги. Ре-спавн одной попытки бывает после отданной назад заявки на спавн | open | приёмка P6 (дофикс, ФП-7) | | PD-222 | standards | info | `cmd/tmplatformctl/seed.go` | **HTTP-променад сида не покрыт тестом:** вход, грант, загрузка и ожидание интейка проверены только живым прогоном на стенде (P6), автоматически — лишь разбор аргументов (`TestASubjectThatIsOnlyPaddingIsNoSubject`). Дев-инструмент, но именно он — единственный потребитель контракта в репозитории, и его поломка видна только тому, кто поднимет стенд | open | приёмка P6 (дофикс, ФП-7) | -| PD-400 | doc | info | `internal/runs/bank.go` `bankVerdict` (класс 12) и шапка файла, `internal/runner/engine.go` `Manifest`/`Export` | **Слово `run_in_flight` у двери правок покрывало больше, чем прогон; пер-книжный мьютекс внутрипроцессный.** Разобрана на две половины по слову владельца 27.08 («техдолг в паке не держим»), диспозиция каждой явная. **(1) ЗАКРЫТА этим же деревом:** раскладка слова сделана точной по построению — `run_in_flight` отвечается ТОЛЬКО из проверки собственной строки прогона под мьютексом, а класс 12 от самого глагола под тем же мьютексом прогоном быть НЕ МОЖЕТ (Start/Resume ждут этот мьютекс, реконсилер рестартует только живые строки, которые проверка видит) — это транзиентный держатель флока (границная материализация, ручной tmctl), и он едет `503 service_unavailable` «занято, повтори позже», а не словом про прогон, которого нет; пин — кейс «a held project = a transient holder» в `TestBankVerdictKeepsTheRemediesApart`. **(2) ГРАНИЦА v1, названная с условием и ценой** (на перевод в «Принятый риск» словом лендинга; ⚠ цена ПЕРЕ-ОПИСАНА по воркфлоу-ревью 28.08 и слову оркестратора — прежняя редакция говорила «ложного слова нет», и это неверно): мьютекс `runs.Service.books` — внутрипроцессный; вторая реплика платформы над одним хранилищем сужает сериализацию до пер-репличной. Цена ДВУСТОРОННЯЯ: (а) translate, заспавненный в флок глагола, умирает холостой попыткой — деньги и данные целы; (б) обратная сторона ТОЙ ЖЕ гонки: глагол проигрывает флок НАСТОЯЩЕМУ прогону, который соседняя реплика допустила своим мьютексом по чистой на тот миг таблице, — и класс 12 тогда отвечается `503` «транзиентный держатель, повтори позже» про живой многочасовой прогон, то есть ЛОЖНЫМ словом (канонно верное там — `409 run_in_flight`). Принятие риска включает эту ложь, а не только холостую попытку; оговорка внесена и в комментарий ветки класса 12 (`bank.go`). Условие: граница ПЕРЕСТАЁТ держать в день второй реплики; лечение тогда — арбитр в хранилище, не больший мьютекс. Сегодняшний деплой однорепличный по всей зоне (in-memory `resynced` в том же сервисе — тот же допуск) | accepted-risk(акт D39.162; половина 1 — fixed тем же актом; перевод по аудиту 28.08. Цена риска — по пере-описанию Р8 в теле: «ни денег, ни порчи» держится, «ни ложного слова» — НЕТ, мульти-репличный класс 12 может ответить 503 про живой прогон) | пак P9: опровергатель раскладки кодов (находка 2) · разбор по слову владельца 27.08 · воркфлоу-ревью 28.08 (линза code-layout): цена включает ложное слово · аудит документации 28.08 | | PD-408 | doc | info | `internal/runs/bank.go` (бюджет двери = `s.runBudget()`), `internal/runner/bankapply.go` (`errOut` без лимита; `Stderr: firstLine`) | **Две операционные оговорки двери правок, названные воркфлоу-ревью; обе — цена конфигурации, не дефект пути.** (1) Бюджет двери — та же ручка `TM_PLATFORM_RUN_BUDGET`, что у прохода свипа; движок выбирал потолок 5000 решений против ЖЁСТКИХ 60 с («пять раз внутри бюджета»), и оператор, понизивший ручку (к чему соседние комментарии подталкивают), делает легальный документ-максимум навсегда неприменимым — вечный `503` вместо «разбей документ»; связка ручки и капа нигде не названа. (2) stderr глагола читается в НЕограниченный `bytes.Buffer`, хотя потребляется только первая строка, — не-тот бинарь по сконфигурированному пути (полудеплой, обёртка) может раздуть демона до OOM за 60-секундный бюджет; stdout той же команды капнут 64 МиБ | open | воркфлоу-ревью P9 28.08 (линзы door:lock-lifecycle · door:crash-windows), диспозиция оркестратора 28.08: строкой | -| PD-423 | standards | minor | `internal/runner/systemd_test.go` `TestARunIsBoundedByItsOwnCgroup`, `docs/STACK_DECISIONS.md` «Гейты батареи» | **Батарея зоны требует ЧЕТВЁРТОГО условия хоста, которого рецепт не называет: пользовательский менеджер systemd должен РЕАЛЬНО применять `MemoryMax` к транзиентным юнитам.** Замерено на этом хосте 29.08: тест трижды подряд зелен в полных батареях (`baseline2`, `final2`, `final4`), затем **пять раз подряд красен в изоляции** — при неизменном коде пакета, которого пак не касался вовсе. Причина установлена ВНЕ батареи и вне Go: `systemd-run --user --scope -p MemoryMax=64M …` даёт процессу спокойно занять 400 МиБ и выйти с кодом 0. ⚠ **МЕХАНИЗМ уточнён приёмкой оркестратора №19, и уточнение решает, воспроизводимо ли это:** «systemd не применяет потолок» верно по симптому и мимо по причине. Свойство ДОЕХАЛО — `systemctl --user show -p MemoryMax` печатает `67108864`; делегирование в порядке — `memory pids` и в `cgroup.controllers`, и в `subtree_control`. Пропал не потолок, а cgroup-КАТАЛОГ: в `app.slice` нет ни одного scope-каталога, а `cut -d: -f3 /proc/self/cgroup` для оболочки даёт **`/init.scope`**. То есть вызывающий процесс живёт ВНЕ `user@.service`; `systemd-run --user` заводит юнит в модели менеджера, а процесс остаётся в исходном cgroup, и лимита не получает никто — молча. ⚠ Отсюда и наблюдение «условие отваливается между двумя прогонами одной сессии»: оно зависит от того, из какого cgroup стартовал прогон. Тест при этом ПРАВ и его сообщение точное («check that the leaf cgroup of tm-runs.slice has memory.max»): он ловит ровно то, ради чего написан, — что потолок памяти прогона на этом хосте иллюзорен. ⚠ Следствие для процесса, а не только для теста: рецепт `STACK_DECISIONS` называет три условия батареи, а их четыре, и четвёртое — свойство ХОСТА, которое может отвалиться между двумя прогонами в одной сессии, что здесь и произошло. Сессия, наступившая на это, потратит время на поиск дефекта в своём диффе. ⚠ **Условие для рецепта формулируется НЕ так, как оно там сейчас стоит.** «Достижимый пользовательский менеджер systemd» выполнено — менеджер отвечает, `tm-runs.slice` виден, — и всё равно лимит не применяется. Правильная формулировка: **вызывающий процесс обязан жить ВНУТРИ `user@.service`** (проверка: `cut -d: -f3 /proc/self/cgroup` не должен давать `/init.scope`). Оболочка, поднятая вне пользовательского входа — под WSL это обычный случай, — проходит первую проверку и валит вторую, и следующий читатель решит, что условие выполнено, и пойдёт искать дефект в Go. Лечение: назвать условие в рецепте именно так и дать тесту различать «хост не применяет лимит» (честный скип с причиной) и «раннер не передал лимит» (настоящий отказ) | open | пак P11 (финальная батарея; воспроизведено голым `systemd-run` вне Go) | -| PD-424 | bug | **major** | `internal/runs/reconcile.go` `reopen` (вердикт `deferred`) и `reconcileOne`, `internal/pgstore/runs.go` `StalledRuns`, `internal/pgstore/observe.go` | **ЖИВОЙ прогон с намертво заблокированной расплатой невидим на ВСЕХ поверхностях `PD-385` и не поддаётся `run abandon` — счётчик неудач не растёт НИКОГДА.** Цепь, воспроизведённая охотником приёмки на десяти проходах: юнит исчез без маркера → `restart` → `settle` возвращает `settlementBlocked, nil` (не ошибку) → `reopen` отказывается открыть новую попытку поверх незакрытого холда и возвращает вердикт `deferred` → `case deferred: return nil` → `reconcileOne` считает проход УСПЕШНЫМ и вдобавок зовёт `ClearRunDeferral`. Итог: `reconcile_failures` 0, `reconcile_after` NULL, `StalledRuns(5)` пуст, гейдж 0, холд заморожен, движок дёргается каждым проходом. Вывести из состояния может только пользователь, нажав Stop, — и никто ему об этом не говорит. ⚠ Это ТА ЖЕ болезнь, что `PD-384`/`PD-385`, но в ЖИВОЙ фазе, куда пак P11 не дошёл: он лечил вторую фазу (расплату) и её поверхности. ⚠ **Почему пак не взял её в себя (решение сессии, не пропуск):** лечение упирается в вопрос, которого нет ни в одной строке заказа, — чем должен считаться вердикт `deferred` для СЧЁТЧИКА: не-событием (как сейчас), успехом или неудачей. Это дизайн реконсиляционной фазы, а пак был про фазу расплаты; вмешаться в него по дороге значило бы «распухнуть вокруг самого срочного», против чего промт предупреждает прямо. Форма лечения, которую сессия предлагает: `reopen`, вернувший `deferred`, обязан сообщать `established=false` (тогда `ClearRunDeferral` хотя бы не стирает счёт), а сам блокированный расчёт — считаться неудачей ТОЙ фазы, которая им владеет | open | приёмка оркестратора №19 по паку P11 (охотник вне карты, воспроизведено 10 проходами) | -| PD-425 | bug | **major**, деньги | `internal/httpapi/bank.go:149` (`r.Context()`), `internal/runs/bank.go` `RecordBankMove` | **Дверь банковских коррекций теряет пост-verb факт НАВСЕГДА при обрыве клиента, и следующая покупка платит за это холдом.** Хендлер берёт `r.Context()`, и `RecordBankMove` пишется на нём же; клиент закрыл вкладку до ответа — контекст отменён, запись не легла, `bank_moved_at` остаётся NULL. Второго писателя у факта нет, свипа нет, то есть состояние вечное. Деньги дальше: `re_pass` отвечает 409, а ОБЫЧНЫЙ прогон допускается с `resnapshot=false` и убивается снапшот-гардом движка ПОСЛЕ взятия холда — холд взят, попытка потрачена впустую. ⚠ Средство, которое дверь называет, недостижимо по построению: она отвечает `503` со смыслом «клиент пере-шлёт», а запись падает именно потому, что клиента уже нет. ⚠ Шаблон лечения лежит В ЭТОЙ ЖЕ ЗОНЕ и здесь не применён: `internal/pgstore/idempotency.go:150` и `books.go:205,215` делают `WithTimeout(WithoutCancel(ctx), …)` ровно с этой мотивировкой. ⚠ Не предмет пака P11 (дверь построена паком P9/P10), поэтому заведено строкой, а не починено: правка денежного пути чужого пака требует своих пинов и своей приёмки | open | приёмка оркестратора №19 по паку P11 (охотник вне карты) | -| PD-426 | bug | minor | `internal/pgstore/runs.go` `Quarantine`, `internal/ingest/tail.go` (четыре отказа выше ветки `default`) | **Карантин проекции не снимается НИЧЕМ, а попасть в него можно по чужому законному handshake'у.** Первое: `quarantine_reason` пишется, и во всём дереве нет ни одного места, которое его очищает, — то есть состояние терминально для проекции живого оплаченного прогона. Второе: в `ingest/tail.go` четыре отказа стоят ВЫШЕ ветки `default`, которая говорит «другой поток начинается здесь, нас не касается», при том что журнал ПЕР-КНИЖНЫЙ и append-only, так что чужие handshake'ы в нём законны. Вместе: чужой handshake в журнале книги карантинит проекцию прогона, за который заплачено, навсегда. ⚠ Не предмет пака P11 (тейлер и карантин — эры P4/P5), заведено строкой | open | приёмка оркестратора №19 по паку P11 (охотник вне карты) | +| PD-428 | doc | info, деньги | `internal/pricing` (`TM_PLATFORM_USD_PER_CHAPTER`, `Pricing.Ceiling`) | **Цена продажи не знает о накладных, которые масштабируются КНИГОЙ, а не грантом.** Замер движкового охотника (лендинг `6ec9f8a`): терминолог переигрывается на КАЖДОЙ покупке ЦЕЛИКОМ по книге — три покупки по одному юниту дали три полнокнижных консолидации по $0.005460 каждая, при том что сам юнит дешевле. То есть книга на 500 юнитов, проданная по одному, оплатит 500 полнокнижных проходов. ⚠ **Сегодня это НЕ дефект платформы и заведено только как калибровка:** продажа идёт ГЛАВАМИ (`Ceiling(chapters)`), а не юнитами, так что нарезки, при которой накладные обгоняют полезную работу, в продукте нет. Строка существует, чтобы факт не потерялся к моменту, когда мелкая нарезка появится: любая будущая единица продажи мельче главы обязана нести в цене эту книжную составляющую, иначе COGS растёт быстрее выручки на самых дешёвых покупках. Носитель — константа цены, а не код движка | open | движковый пак «деньги» (охотник), передано оркестратором №19 сессии P11 | | PD-421 | hardening | info | `internal/pgstore/sessions.go` `StillLive` и `SweepSessions`, `docs/STACK_DECISIONS.md` §13 | **Открытый поток теряет свою сессию по ПОДМЕТАНИЮ строки, а не по клаузе бездействия, — и это остаток закрытия `PD-379`, названный прямо.** Проверка живости потока намеренно НЕ содержит клаузы `idle_expires_at`: окно бездействия скользит на ЗАПРОСЕ, а поток — один запрос на всю жизнь, поэтому гашение по idle рвало бы связь активному читателю. Но `SweepSessions` раз в час УДАЛЯЕТ строки и по бездействию тоже, а «строки нет» ОБЯЗАНО значить «мертва» — иначе отозванная сессия держала бы поток до свипа, то есть дыра ровно в час. Следствие: сессия, протухшая по бездействию и подметённая, теряет поток с опозданием до часа. Это не idle гасит поток, а отсутствие строки; к тому моменту любой другой запрос того же вызывающего — 401. Лечение, если сочтётся недопустимым, — скольжение окна бездействия ИЗ потока, но это правка ПОЛИТИКИ §13: открытая вкладка держала бы сессию до абсолютного потолка, а это слово владельца | open | пак P11 (назван при закрытии `PD-379`) | | PD-422 | bug | info | `internal/runs/runs.go:288` `if book.BankMoved`, `internal/pgstore/books.go:1030` | **`--resnapshot` платформа передаёт УСЛОВНО, а условие ставит только ПРАВКА банка — рост авто-банка от майнинга его не ставит.** Флаг выводится под `if book.BankMoved`, а единственный писатель `bank_moved_at` — дверь правок банка. На книге, которая МАЙНИТ банк, вторая покупка без правок банка идёт без флага, и движковый джоб-гард останавливает прогон (`exit 1` ⇒ `failed` на стороне платформы): авто-банк растёт от покупки к покупке, edit-снапшот съезжает, а гард банк-онли-движение от смены конфига не отличает. ⚠ Сегодня БЕСПРЕДМЕТНО: проводка `tmctl translate --max-units` на платформе гейчена оркестратором до лечения, а без неё вторая покупка этой формы не возникает. Строка заведена, чтобы условность не всплыла сюрпризом при снятии гейта. Найдено бэкенд-сессией `textmachine-e4` (пак «деньги»), проверено чтением платформенной стороны сессией P11 | open | бэкенд-пак «деньги» + пак P11 (сверка шва) | @@ -161,7 +151,7 @@ | PD-42 | hardening | info | `internal/login/login.go` | Лимит `/auth/login` глобальный: один хост держит ведро пустым и выключает вход всем (замерено: 8 отказов из 10 у «легитимного» пользователя при фоне 5 rps). Пер-адресный лимит здесь неверен, пока нет доверенного edge-прокси — за прокси RemoteAddr один на всех. Место лимита — edge | accepted-risk(платформа P1, 05.08) | ревью безопасности (исполнением) | | PD-71 | bug | info | `internal/pgstore/migrations/00005_identity_oauth.sql:72` | **Down-путь `00005` не исполним на данных, которые его же up-путь делает законными**, поэтому откат ниже версии 5 недоступен. Он восстанавливает `users_email_key` и `email NOT NULL`, а боевой код пишет `email = NULL` у неподтверждённой личности и кладёт один подтверждённый адрес на два аккаунта (следствие «почта не ключ»). **Перепроверено моим прогоном, не принято со слов ревью:** три реальных аккаунта (один с `email = NULL`, два с общим подтверждённым адресом) — `DownTo(5)` проходит, `DownTo(4)` падает с `could not create unique index "users_email_key" (SQLSTATE 23505)`; первым срабатывает индекс, до `NOT NULL` выполнение не доходит. Данные целы — down транзакционный, `Up()` вернул схему на версию 8 со всеми тремя аккаунтами, — но плана отката ниже 5 не существует. Править `00005` запрещает append-only, а чужой down-текст новая миграция не заменяет — **принято как ЦЕНА ПРАВИЛА:** записано в `STACK_DECISIONS §8` и в `deploy/README.md` разделом «Откат релиза: не ниже версии 5», чтобы оператор не узнал это в момент отката | accepted-risk(зона P2, 05.08) | ревью P2 (линза sql-money) | | PD-179 | hardening | info | `cmd/tmplatformd/main.go` `serveMetrics`, `deploy/` | **Эндпоинт `/metrics` не аутентифицирован и защищён только адресом привязки.** Дефолт `127.0.0.1:9464`, то есть снаружи недостижим; экспозиция несёт операционную форму деплоя (глубина очереди, число прогонов, возраст холдов), но не пользовательские данные и не деньги. Второй модели авторизации ради скрейпера зона не заводит — это ровно тот довод, по которому админ-поверхность стала CLI (§10). Риск принят: оператор, поднявший `TM_PLATFORM_METRICS_ADDR` на внешний адрес, открывает её сам, и об этом сказано в `deploy/README.md` | accepted-risk(платформа P5, 11.08) | сессия P5 | - +| PD-400 | doc | info | `internal/runs/bank.go` `bankVerdict` (класс 12) и шапка файла, `internal/runner/engine.go` `Manifest`/`Export` | **Слово `run_in_flight` у двери правок покрывало больше, чем прогон; пер-книжный мьютекс внутрипроцессный.** Разобрана на две половины по слову владельца 27.08 («техдолг в паке не держим»), диспозиция каждой явная. **(1) ЗАКРЫТА этим же деревом:** раскладка слова сделана точной по построению — `run_in_flight` отвечается ТОЛЬКО из проверки собственной строки прогона под мьютексом, а класс 12 от самого глагола под тем же мьютексом прогоном быть НЕ МОЖЕТ (Start/Resume ждут этот мьютекс, реконсилер рестартует только живые строки, которые проверка видит) — это транзиентный держатель флока (границная материализация, ручной tmctl), и он едет `503 service_unavailable` «занято, повтори позже», а не словом про прогон, которого нет; пин — кейс «a held project = a transient holder» в `TestBankVerdictKeepsTheRemediesApart`. **(2) ГРАНИЦА v1, названная с условием и ценой** (на перевод в «Принятый риск» словом лендинга; ⚠ цена ПЕРЕ-ОПИСАНА по воркфлоу-ревью 28.08 и слову оркестратора — прежняя редакция говорила «ложного слова нет», и это неверно): мьютекс `runs.Service.books` — внутрипроцессный; вторая реплика платформы над одним хранилищем сужает сериализацию до пер-репличной. Цена ДВУСТОРОННЯЯ: (а) translate, заспавненный в флок глагола, умирает холостой попыткой — деньги и данные целы; (б) обратная сторона ТОЙ ЖЕ гонки: глагол проигрывает флок НАСТОЯЩЕМУ прогону, который соседняя реплика допустила своим мьютексом по чистой на тот миг таблице, — и класс 12 тогда отвечается `503` «транзиентный держатель, повтори позже» про живой многочасовой прогон, то есть ЛОЖНЫМ словом (канонно верное там — `409 run_in_flight`). Принятие риска включает эту ложь, а не только холостую попытку; оговорка внесена и в комментарий ветки класса 12 (`bank.go`). Условие: граница ПЕРЕСТАЁТ держать в день второй реплики; лечение тогда — арбитр в хранилище, не больший мьютекс. Сегодняшний деплой однорепличный по всей зоне (in-memory `resynced` в том же сервисе — тот же допуск) | accepted-risk(акт D39.162; половина 1 — fixed тем же актом; перевод по аудиту 28.08. Цена риска — по пере-описанию Р8 в теле: «ни денег, ни порчи» держится, «ни ложного слова» — НЕТ, мульти-репличный класс 12 может ответить 503 про живой прогон) | пак P9: опровергатель раскладки кодов (находка 2) · разбор по слову владельца 27.08 · воркфлоу-ревью 28.08 (линза code-layout): цена включает ложное слово · аудит документации 28.08 | ## Закрытые ратификацией Строки, у которых лечением был не код, а решение владельца контракта или оркестратора. @@ -563,3 +553,22 @@ | PD-415 | doc | minor | `docs/STACK_DECISIONS.md` «Стенд разработчика», `internal/runner/translate_resnapshot_live_test.go` | **Рецепт стенда давал КРАСНУЮ батарею, а не скип, и красноту эту следующая сессия принимала за свою поломку.** Рецепт рендерит шаблон книги из `backend/example/book.yaml`, а тот указывает на `configs/pipeline-c1.yaml` — ПЛАТНЫЙ DeepSeek. Живой тест P10 `TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`, включаемый вторым гейтом, гоняет настоящий движок и падает `tmctl: missing API keys (fill in backend/.env)` — при том что его собственный комментарий обещает «free of provider keys and of paid calls». Тест прав: у стенда есть $0-пара (`local-qwen3-8b`, провайдер на `127.0.0.1:11434`, заглушку тест поднимает сам), но в репо НЕТ пайплайна, который бы её называл. ⚠ Цена не только во времени: гейт гоняет НАСТОЯЩИЙ движок, то есть платный пайплайн в шаблоне — ещё и риск оплаченных вызовов ⚠ **ЗАКРЫТО ПАКОМ P11 (29.08):** раздел переписан — три гейта вместо двух, замеренные числа (0 скипов с гейтами, 287 без на HEAD и 304 на дереве пака) и рецепт $0-шаблона, который обязан лежать РЯДОМ с `prompts/` (промпты резолвятся от каталога пайплайна). Пере-проверено чужими руками: оркестратор при приёмке наступил на ту же граблю и вышел по предупреждению за минуту | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | пак P11, сборка стенда | | PD-416 | bug | minor | `internal/pgstore/sqlgate_test.go` `usedException` | **Дефект, который пак P11 ВНЁС в чужой гейт, и который поймала его же мутационная обвязка.** Счётчик исполнения исключений `unresolvable` был ПАКЕТНОГО уровня, а `collectSQL` с приходом второго гейта (`TestTheLedgerIsAppendOnlyInTheCodeThatWritesIt`) стал вызываться дважды за прогон: общий счётчик читал первый визит ВТОРОГО вызывающего как второй визит ПЕРВОГО и падал с «`Ready` holds a second statement this gate cannot read» о функции, которая держит ровно одно. Проявлялось как КРАСНЫЕ ЧИСТЫЕ копии в мутационной кампании там, где копируемое дерево было зелёным, — то есть маскировало дельту, что для кампании худший исход. ⚠ **ЗАКРЫТО ПАКОМ P11 (29.08):** счётчик стал per-extraction (`newExceptionCounter`), инвариант «одно исключение — один сайт, каждое исключение исполнено» сохранён полностью | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | мутационная обвязка пака P11 | | PD-417 | bug | minor | `internal/pgstore/runs.go` `StalledRuns`, `internal/pgstore/observe.go`, миграция `00028_settling_attempts.sql` | **Расширение операторских выборок на вторую популяцию (`PD-385`) оставило их без индекса: последовательный проход по всем попыткам на запросе, который рантбук советует для cron, и на гейдже, который демон гоняет каждые 15 секунд вечно.** У живой половины индекс был с `00009` (`run_attempts_live_idx`, частичный по `ended_at is null`), у settling-половины — никакого. Замер на 200 000 попыток, `explain (analyze, buffers)`, таблица 2×2: **без индекса плохи ОБЕ формы** (3647 буферов дизъюнкцией, 3653 через `UNION ALL`), **с индексом хороши обе** (10 и 18). То есть катастрофу снимает ИНДЕКС, а не форма запроса — первая редакция комментария приписывала заслугу форме, и это исправлено в трёх носителях (код, текст миграции, отчёт). `UNION ALL` оставлен по другому доводу: каждая ветвь несёт путь доступа СТРУКТУРНО, тогда как `BitmapOr` — выбор планировщика по статистике, а статистика ЗДОРОВОГО деплоя (пустая застрявшая популяция) ровно обратна той, на которой мерилось. Класс — `PD-364` с другой стороны: там индекс не мог обслужить свой запрос, тут запрос перерос свои индексы ⚠ **ЗАКРЫТО ПАКОМ P11 (29.08)** миграцией `00028` (частичный индекс по `ended_at is not null and reconcile_failures >= 1`) плюс двумя ветвями | fixed(пак P11 — лендинг оркестратора; статус проставлен зоной) | самопроход пака P11 (линза цены в БД), пере-замерено по требованию артефакта | + +## Закрытые — перенос по секциям (аудит 29.08: статус был переведён раньше, а строка осталась под открытым заголовком) + +> Ни одна строка здесь не меняла СТАТУС этим переносом — каждая уже несла свой `fixed(...)` с актом, +> которым была закрыта. Двигалась только позиция: они лежали в секциях «Открытые», то есть человек, +> читающий открытый список по весу, видел закрытую работу и мог пойти чинить построенное — класс, +> который `ENGINEERING_STANDARDS` §3.8 называет стоившим зоне порядка десятой доли строк. Счёт по +> статусам от переноса не изменился (`counts.py` ключуется формой строки, а не секцией); изменилось +> то, что видит читатель. + +| PD-370 | bug | **major** | `internal/httpapi/reading.go`, `internal/httpapi/v0.go`, `internal/pgstore/readmodel.go`, канон `14-api-contract` §BankDecision | **Была построена и стоит в каноне модель, которую владелец отменил 16.08.** `D39.144` ратифицировал: подписывается ВЕСЬ банк ОДНИМ ОК, «пер-термная подпись = сотни кликов — НЕ модель продукта»; пер-термно существует не подпись, а ПРАВКА термина (до подписи) и правка с пере-генерацией задетых глав (после прочтения, гейчено 192). Гейт полноты нота сняла, а САМ ГЛАГОЛ остался — чистка дошла до resume и счётчиков и не дошла до словаря. Колонки были write-only: их не читал ни один SELECT, то есть реализации не существовало **— ЗОННАЯ ПОЛОВИНА ЗАКРЫТА 22.08 по слову владельца: снят write-путь целиком** (маршрут `POST /books/{bookId}/bank/decisions`, хендлер, wire-типы, метод интерфейса `Library`, `SubmitBankDecisions`, типы `BankDecision`/`BankReceipt`, `UnknownTermError`), на его месте — пометка для будущей сессии с ратифицированной моделью и с тем, что уже есть в схеме. Пол размера поверхности 15 → 14 сдвинут ЯВНО и с причиной в комментарии: гейт сработал, а не был подогнан. **КОНТРАКТНАЯ ПОЛОВИНА ЗАКРЫТА 27.08 минором 0.5.0 (D39.161):** путь `POST /books/{bookId}/bank/decisions`, глагол `submitBankDecisions` и три схемы снесены из канона — ноль вхождений, замер в `docs/CONTRACT_MINOR_REPORT.md` и пере-мер приёмки D39.161 п.2; счётчики `pending_decisions`/`complete` сняты со всех трёх носителей (`BankPage`, `EventBank`, квитанция). Преемник — дверь `POST …/bank/corrections` (выведена из `tmctl bank-apply`), СМОНТИРОВАНА паком P9 (D39.162); `Capabilities.bank_corrections_enabled` следует `cfg.RunsEnabled()`. Итог: зонная половина снята 22.08 (слово владельца), контрактная — 27.08 (D39.161); ⚠ НЕ наследовать этой строкой заказ на счётчики в `wireBankPage` — он жил отдельной строкой `PD-399` (закрыта паком P9 своим пином). Статус переведён по аудиту документации 28.08 — обязательство висело неисполненным (класс строки 225); текст закрытия — контрактной сессии, перенос в зону — платформенной (регистр в её праве записи и был грязен её WIP) | fixed(D39.161 — контрактная половина; 22.08 слово владельца — зонная; перевод по аудиту 28.08) | поправка владельца 22.08; ратификация D39.144; аудит 31 агента 22.08 · минор D39.161 · аудит документации 28.08 | +| PD-406 | bug | **major** | `internal/httpapi/stream.go:137`=`helloID := state.Position` | **Служебный кадр `hello` на ПЕРЕПОДКЛЮЧЕНИИ несёт `id = state.Position` (голова истории книги) вместо предъявленного клиентом `last` — и переводит `Last-Event-ID` браузера ВПЕРЁД через кадры, которые это соединение ещё не отдало.** По WHATWG поле `id` фиксируется в last event ID buffer на диспатче; обрыв сразу после hello (прокси, вывод инстанса из ротации, ошибка первого `ReadFrames`) теряет кадры `last+1..Position` НАВСЕГДА — включая `note`, которые контракт запрещает терять («MUST NOT be coalesced or dropped»), без `resync_required`; усилитель — `revision` в data того же hello выше потерянных строк, так что предписанный контрактом дельта-догон `?after_version=` возвращает пусто, обезоружены обе починки сразу. Это ровно вариант, который `14-api-contract/README.md` (§Last-Event-ID) называет отвергнутым. **Лечение — одна строка:** при `resuming` слать hello с `last` (свежее подключение оставить как есть); код четырьмя строками ниже уже делает ровно этот выбор для `from` (`stream.go:122-126`). Свежий коннект не задет — там клиент читает коллекции целиком ⚠ **ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162):** при `resuming` hello несёт `last` клиента (свежий коннект — голову, как и `from` ниже); пин `TestAResumingHelloCarriesTheClientsOwnWatermark` (+ посадка «hello обратно на голову» поймана) | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линза race:flip-read-consistency), лечение по слову оркестратора 28.08 | +| PD-401 | bug | minor | `internal/pgstore/runs.go` `StartRun` (снятие баз), `internal/pgstore/sink.go` `recordWaveShape`, `internal/pgstore/readmodel.go` `runDone`/`runTotal` | **Прогон, стартовавший при `edit_wave = false` и получивший переворот флага ПОСЛЕ старта, делает всю купленную работу с полосой на `0/N` — и это навсегда, а не «окном».** Механика: движок объявляет форму волн первым progress-событием ПОСЛЕ старта (`recordWaveShape`; флаг только растёт, `STACK_DECISIONS` §35), а базы полосы снимались в `StartRun` через флаг-зависимый предикат МОМЕНТА СТАРТА и не пере-базируются никогда — сидят на черновой колонке, числитель после переворота уезжает на редакторскую, разрыв не закрывается. Достижимо штатно: книга прочерчена на пайплайне без редактора, оператор ставит редактора, куплено продолжение; воспроизведено ревьюером приёмки заменой одной строки в пине пака (`edit_wave = false` до старта, переворот после → `0/4` при всей сделанной работе). Тот же симптом «полоса не достигает единицы», что у `PD-281`, — на том «промежуточном классе», которым диспозиция `PD-281` и объясняется. ⚠ Первая редакция строки (заведена опровергателем пака) занижала вес (info) и формулировку: «скачок доли при done>0» и «окно секунды» — секунды живёт ПЕРЕВОРОТ, ущерб его переживает; пере-формулирована и пере-взвешена по слову оркестратора 27.08. ⚠ **Лечение ПОСТРОЕНО в этом же дереве и зелено:** базы снимаются на ФИКСИРОВАННЫХ колонках (`chapters_before` — редакторская, `draft_before` — черновая), пара «числитель+база» выбирается ЖИВЫМ флагом в момент чтения (`runDone`), миграция `00026` пере-снимает базы live-прогонов, порядок переворота запинен ровно как в проде — `TestAFlagThatFlipsAfterStartDoesNotStrandTheBar`; лендить ли лечение этим паком — решение владельца, состав собирает оркестратор, до лендинга строка open. Остаток и ПОСЛЕ лечения: на смешанном заделе (купленный диапазон шире начернённого) переворот двигает знаменатель вверх в окне первого progress-события — транзиентный провал доли, прежняя суть первой редакции ⚠ **ЗАКРЫТА лендингом P9 (58bae30, акт D39.162):** лечение (фиксированные базы + пара живым флагом + миграция 00026 + пин `TestAFlagThatFlipsAfterStartDoesNotStrandTheBar`) заленжено и зелено; тело само велело «до лендинга строка open» — лендинг прошёл, статус переведён по аудиту 28.08. Транзиентный остаток окна первого progress-события остаётся named-границей (PD-401-остаток в словаре зоны) | fixed(лендинг 58bae30, акт D39.162; перевод по аудиту 28.08) | пак P9: опровергатель формы полосы (первая редакция) · блокер ревьюера приёмки 27.08 (пере-формулировка) · аудит документации 28.08 | +| PD-409 | hardening | minor | `internal/runs/bank.go` `decisionsFile` (`bank-corrections-*.json` в StateDir, удаление только `defer cleanup()`) | **Отрендеренный документ решений переживает любую нечистую смерть демона и остаётся в StateDir навсегда: ничто в дереве каталог не подметает.** SIGKILL/OOM/паника/истёкший грейс деплоя посреди вызова двери — и файл до 1 МиБ с полными src/dst правленых терминов пользователя лежит под 0600 бессрочно в каталоге, где оператор считает маркеры прогонов; единственная существующая уборка (`os.Remove` exit-маркера в `spawn.go`) эти файлы не видит. Накапливается через деплои: N нечистых смертей = N сирот. Лечение — бут-подметание по маске или переезд под каталог с автоочисткой ⚠ **ЗАКРЫТА фикс-раундом P9 (28.08, акт D39.162):** `Service.SweepCorrectionScratch()` подметает по маске на буте (вызов в композиционном корне `tmplatformd` до подъёма HTTP, файлы вне маски не трогаются); пин `TestBootSweepsOrphanedCorrectionDocuments` (посадка «маска мимо» поймана). Честная оговорка: сам ВЫЗОВ из main юнитом не запинен — ловится чтением | fixed(фикс-раунд P9, D39.162) | воркфлоу-ревью P9 28.08 (линзы door:crash-windows · door:lock-lifecycle), лечение по слову оркестратора 28.08 | +| PD-398 | standards | info | `docs/scripts/counts.py` `malformed`, строки `PD-99` и `PD-197` этого файла | **Гейт формы регистра ловит только НЕДОСТАЧУ ячеек, а не избыток — и в файле уже есть две строки, которые он пропускает.** `malformed()` сравнивает `if n < shape`, поэтому строка с лишним символом вертикальной черты внутри инлайн-кода проходит молча: счёт ячеек через `awk` с разделителем-чертой даёт `PD-99` (11 ячеек) и `PD-197` (10), а `counts.py --check` печатает `битая форма: []`. Обе строки допаковые, обе сломаны греп-альтернацией и регекспом в тексте. ⚠ Что СМЯГЧАЕТ и почему это `info`, а не выше: `col()` считает колонки С КОНЦА, поэтому статус и вес таких строк читаются ВЕРНО, и сегодняшние числа зоны не врут — пере-проверено, `PD-99` и `PD-197` попадают в свои корзины правильно. Дыра латентная: лишняя черта в одной из ТРЁХ последних колонок сдвинет уже их, и гейт снова промолчит. Пак наткнулся на это ДВАЖДЫ собственной рукой, и второй раз — этой же строкой: первая её редакция цитировала команду счёта ВМЕСТЕ с разделителем-чертой и сломала себя ровно тем, что описывает. моя же правка `PD-396` внесла конвейер в ячейку, счётчик весов выдал мусорный ключ вместо `info`, а `битая форма` осталась пустой — то есть симптом виден в СЧЁТЕ, а не в проверке формы, которая для этого и написана. Лечение однострочное и не моё: `if n != shape` вместо `if n < shape` (зона `docs/`, решение оркестратора). Родня по классу — весь этот пак: гейт, доказывающий меньше, чем читается ⚠ **ПЕРЕ-ФОРМУЛИРОВАНА приёмкой (оркестратор №19, 27.08, D39.159 п.8): предложенная правка ОТКЛОНЕНА замером.** `n != shape` краснит СЕМЬ законных строк (пять бэклога, две регистра), потому что колонки читаются С КОНЦА НАМЕРЕННО — `counts.py:132` называет это прямо и приводит ровно этот случай (строка бэклога 127 несёт три слова через вертикальную черту внутри инлайн-кода). Избыток в ПЕРВОЙ содержательной ячейке легален, и числа из-за него не врут: проверено, статус и вес читаются из хвоста, `PD-99` и `PD-197` разбираются верно. **Настоящий остаточный риск — избыток в ХВОСТОВОЙ ячейке:** там лишняя вертикальная черта сдвинула бы именно статус или вес, и тихо. Ловится не счётом ячеек, а сверкой хвостового словаря (статус ∈ {open, fixed, accepted-risk}); сегодня у такой сверки два известных доброкачественных исключения — `PD-59` и `PD-273` несут статус прозой. Строка остаётся ОТКРЫТОЙ под эту формулировку ⚠⚠ **ВТОРАЯ ПОЛОВИНА ПОСТРОЕНА 27.08 (оркестратор №19), смягчение строки ОПРОВЕРГНУТО.** Довод «читаем с конца, значит лишняя черта безвредна» неверен: он защищает СТАТУС (третья с конца), но не ВЕС — тот читался `col(l,6)`, то есть тоже с конца, а от конца он далеко, и любая лишняя черта в «сути» сдвигала его молча. Живой пример — сама `PD-197`: вес читался из ячейки «где». Эмпирика, принесённая зоной: три случая за двое суток, все у тех, кто в этот момент про этот класс ПИСАЛ (правка `PD-396` · первая редакция этой строки · пере-формулировка её приёмкой), и во всех трёх «битая форма» оставалась пустой. **Построено три вещи, каждая проверена исполнением:** (1) парсер уважает markdown-экранирование — `PD-99` несла корректно экранированные черты, и ломался парсер, а не строка; (2) ВЕС регистра читается с НАЧАЛА (`cells(l)[3]`), потому что ведущие ячейки коротки и черт не несут, хвостовые тоже, а свободный текст живёт в СЕРЕДИНЕ — оба конца безопасны, середина нет; (3) новый гейт `tail_vocab` судит СЛОВАРЬ хвоста (статус и вес), а не счёт ячеек, с одним грандфазерным исключением `PD-59`. Проверено подсадкой: черта в статусную ячейку краснит гейт немедленно; на чистом дереве EXIT=0, числа не сдвинулись (398/96, 3/27/66). ⚠ Правка `n != shape` по-прежнему ОТКЛОНЕНА и теперь на точном основании: после уважения экранирования в бэклоге остаются пять строк с законной сырой чертой внутри инлайн-кода, и их хвост чист. **Строка остаётся ОТКРЫТОЙ ровно на одно: у построенного гейта нет автоматического пина — он проверен подсадкой руками. Закрывать её без пина значило бы нарушить правило `PD-1` в строке, которая про это правило и есть** ⚠ **ЗАКРЫТА 27.08: пин появился.** `selftest_tail_vocab()` в `docs/scripts/counts.py` гоняется на КАЖДОМ `--check` и несёт четыре утверждения — чистая строка молчит · сырая черта в СТАТУСНОЙ ячейке краснит · экранирование законно и лишней ячейкой не считается · черта в СЕРЕДИНЕ не сдвигает вес. Проверен ПОСАДКОЙ, а не заявлением: три мутации гейта (снять уважение к экранированию · вернуть вес на чтение с конца · обезвредить словарь статуса) — пин ловит все три, базовая линия молчит. ⚠ Первая редакция пина МОЛЧАЛА на второй мутации: утверждение про вес проверяло `cells()`, а мутация меняет то, чем пользуется `register()`, — пин смотрел не туда. Переписано на настоящий путь; называю, потому что пин, проверенный одним прогоном вместо посадки, — это ровно тот дефект, который эта строка и описывает | fixed(приёмка 27.08, дерево сессии) | ревью-пак P8-REVIEW (наткнулся при правке собственной строки, подтверждено редакторским аудитом) | +| PD-399 | standards | minor | `internal/pgstore/readmodel.go:330` `bankCountsTx`, `internal/httpapi/reading_test.go:113` | **`GET /books/{bookId}/bank` шлёт два поля, которых канон 0.5.0 больше НЕ объявляет.** Минор снёс `pending_decisions` и `complete` из `BankPage` и `EventBank` вместе с пер-термной моделью, которую они обслуживали, а проекция платформы их по-прежнему кладёт на провод, и это НЕ нули: `bankCountsTx` считает `proposed`-строки, и её собственный комментарий это говорит. Клиент 0.5.0 лишние поля игнорирует по общему правилу канона, поэтому вес minor, но аллоулист-норма нарушена, а комментарий «the wire fields are the canon's» с этого минора ЛОЖЕН. ⚠ Присутствие полей ЗАПИНЕНО (`reading_test.go:113`), значит снятие — правка с пином, а не вычёркивание. Лечение — пункт **(2в)** очереди D39.156, тем же паком, что монтирует дверь `bank/corrections`. ⚠ Найдено САМОЙ контрактной сессией и принесено пингом: промт (§3.2-бис) утверждал «поля навсегда нули», а нота D39.160 п.2 — «после сноса канон и деплой совпадут точно»; верно по ПУТЯМ, неверно по ПОЛЯМ. Ошибка оркестратора, исправлена эрратой ⚠ **ЗАКРЫТА паком P9 (27.08): поля сняты со всех трёх носителей** — `BankCounts`/`payload()` (кадр `EventBank`), `wireBankPage`/`listBank` (провод), подзапрос к мёртвой `bank_decisions` ушёл из `bankCountsTx`; пин `reading_test.go` ПЕРЕПИСАН и теперь пинит ОТСУТСТВИЕ снятых полей на первой странице (`TestTheBankAggregatesRideOnTheFirstPageOnly`) | fixed(пак P9, дерево сессии) | пинг контрактной сессии при сдаче минора 27.08, пере-проверен приёмкой | +| PD-166 | bug | info | `internal/ingest/tail.go`, `internal/pgstore/sink.go` `Begin` | **`chunker_version` из хендшейка теряется навсегда,** если краш пришёлся между двумя стейтментами `Begin` (привязка `engine_run_id` и запись версии — два отдельных автокоммита): при повторном чтении своего же `hello` тейлер видит, что поток уже привязан, и `Begin` больше не зовёт. Сегодня поле никем не читается (нужно для строки 100), поэтому info; закрывать — одной транзакцией в `Begin` ⚠ **ПАК P8-REVIEW 24.08: первая фраза сужена построенным.** У колонки `books.chunker_version` появился ВТОРОЙ независимый писатель — интейк (`internal/pgstore/books.go` `FinishParse` пишет её из манифеста движка). Значит крах между двумя стейтментами `Begin` оставляет не пустую колонку, а СТАРОЕ значение разбора: теряется дельта, а не значение. Читателя по-прежнему нет, вес `info` верен ⚠⚠ **МОЯ ДОПИСКА ВЫШЕ ОПРОВЕРГНУТА РЕФУТЕРОМ, и опровергнута верно — снимаю её.** Механизм, который строка описывает (крах между ДВУМЯ автокоммитами `Begin`), СЕГОДНЯ НЕДОСТИЖИМ: запись версии переехала из `Begin` в `effect` и идёт в ОДНОЙ транзакции с курсором (`internal/pgstore/sink.go:105-127`), а сам `Begin` объявлен legacy (`sink.go:33-38`) и для попыток этой сборки недостижим — `engine_run_id` присваивается в INSERT попытки и бэкфилится в `RecordSpawn`. Доказано исполнением: при уцелевшей привязке и нулевом курсоре хендшейк идёт в `Apply`, а не в `Begin` (`Begin calls=0 Apply calls=1`), то есть терять между двумя стейтментами нечего. Посадка мутации (вырезан `case ingest.TypeHello` из `effect`) роняет `TestTheChunkerVersionOfTheStreamReachesTheBook` — атомарный писатель есть и запинен. **Правильная диспозиция — не «сузить до потери дельты», а ЗАКРЫТЬ как построенное:** атомарность, которой строка требовала, существует. Читателя у колонки по-прежнему нет, и это отдельный факт, а не этот дефект ⚠ **ЗАКРЫТА актом лендинга P9 (D39.162):** акт объявил закрытие состоявшимся, статус переведён по аудиту документации 28.08 — обязательство акта висело неисполненным (класс «строка open при легшем лечении», носитель — строка бэклога 225) | fixed(акт D39.162, перевод по аудиту 28.08) | самопроверка дофикса (ревью вне карты) · аудит документации 28.08 | +| PD-254 | standards | info | `internal/httpapi/problem.go` `WriteStatusProblem`, `codeForStatus` | **Два писателя ошибок на одну форму тела.** `WriteProblem` выводит статус ИЗ кода (пара не может разойтись), а `WriteStatusProblem` идёт обратно — от статуса к коду — потому что вне версионного префикса (`/auth`, `/readyz`) есть статусы, которых в словаре контракта нет вовсе (405, 429). Свести в один писатель можно только назначив форму ответа для этих двух статусов, а это ратификация, не правка зоны. Пока — два пути и обратная функция рядом с прямой ⚠ **ЗАКРЫТО решением контрактной сессии 17.08 (релей владельца):** поверхность входа отвечает тем же конвертом и БЕЗ машинного кода — это ратифицированное решение, а не пробел («различать причины отказа клиент не может по замыслу», компаньон §2.14 с 0.2.3), а единственный осмысленный для пользователя случай `429` машинен без словаря, потому что лечение едет в `Retry-After`. Следствие для кода: писатель входа кода не эмитит, `codeForStatus` и `writeRaw` удалены — обратной функции, то есть второго источника истины, больше нет. Пин `httpapi.TestTheSignInSurfaceAnswersTheSameEnvelopeWithoutAVersionedCode` (шесть статусов). Отвергнуто с доводом: расширение `ErrorCode` (значения, недостижимые на описываемой им поверхности, ломают инвариант «код называет свой статус») и словарь в компаньоне (документ, не нормативный для формы, стал бы нормативным с чёрного хода) | fixed(P7, дерево сессии) | кросс-модельное ревью P7 (линза скоупа) | +| PD-255 | doc | info | `internal/pgstore/migrations/00016_read_surface.sql`, `internal/pgstore/readmodel.go` | **Плотность комментариев выше нормы зоны** («одна-две строки почему», владелец 26.07): миграция 00016 — 250 строк, из них около половины проза; у `readmodel.go` многие символы несут абзацы. Часть прозы несущая (порядок delete/insert в двух таблицах — ровно то, на чём пак и споткнулся), часть — эссе. Подрезано самое тяжёлое; остальное — предмет решения владельца о норме, а не тихой правки ⚠ **ЗАКРЫТО решением владельца 21.08: НОРМА СМЕНИЛАСЬ, и строка была открыта против снятой формулы.** Счёт строк снят как негодный гейт — комментарий на три строки может быть нужен, на одну достаточен; режется ВОДА (пересказ решений, провенанс, изложение исследования вместо ссылки), а всё, что из одной функции НЕ выводится — порядок блокировок, инварианты между таблицами, цена забывания, вендор-квирк — остаётся, сколько бы строк ни заняло. Формулировка — `docs/architecture/12-go-style-notes` §1. Обе названные здесь прозы под новой нормой законны: порядок delete/insert между двумя таблицами из функции не выводится, а сам дефект пака это доказал. Тихой правки не было и не будет | fixed(решение владельца 21.08) | кросс-модельное ревью P7 (линза энтропии) | diff --git a/platform/docs/ENGINEERING_STANDARDS.md b/platform/docs/ENGINEERING_STANDARDS.md index 94377edb..e1d4ea6d 100644 --- a/platform/docs/ENGINEERING_STANDARDS.md +++ b/platform/docs/ENGINEERING_STANDARDS.md @@ -60,7 +60,12 @@ ## 3. Критерии приёмки сессии (Definition of Done) -1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **«Скипов ноль» требует ТРЁХ условий, а не +1. `make check` зелёный офлайн; скипы названы вслух. ⚠ **СКОЛЬКО условий — смотреть в + `docs/STACK_DECISIONS.md`, раздел «Гейты батареи», он единственный носитель** (правка 29.08: + число жило в трёх файлах и они расходились — два здесь, три там, четыре в третьем, и сессия, + честно исполнившая этот пункт по устаревшей копии, объявляла приёмку выполненной при красном + тесте). Список ниже оставлен как ИСТОРИЯ поправки 22.08, а не как действующий счёт. + ⚠ **«Скипов ноль» требует ТРЁХ условий, а не одного** (испр. 22.08 — прежняя редакция называла только DSN и позволяла объявить приёмку выполненной с молча пропущенной третью батареи): `TM_PLATFORM_TEST_DSN` (схема) · `TM_PLATFORM_TEST_ENGINE_BIN` + `TM_PLATFORM_TEST_BOOK_TEMPLATE` (живой рендер конфигурации) · diff --git a/platform/docs/PLATFORM_DIRECTION.md b/platform/docs/PLATFORM_DIRECTION.md index b2adedf1..cc0f6fab 100644 --- a/platform/docs/PLATFORM_DIRECTION.md +++ b/platform/docs/PLATFORM_DIRECTION.md @@ -155,7 +155,7 @@ hosted IdP (связка PII + доступность, а свою сессию | Добавление | Пин | Статус | |---|---|---| | OIDC-вход | x/oauth2 v0.36.0 + go-oidc/v3 v3.20.0 | ратифицировано (§1) | -| Кодоген сервера из ратифицированной спеки OpenAPI 3.1 | `oapi-codegen/v2` v2.8.0 (17.07.2026) | **ВЗЯТЬ — доказано исполнением 05.08** (ниже); фолбэк overlay 3.1→3.0 не понадобился. ⚠ **НЕ ИСПОЛНЕНО на 14.08** — см. баннер статуса выше | +| Кодоген сервера из ратифицированной спеки OpenAPI 3.1 | `oapi-codegen/v2` v2.8.0 (17.07.2026) | **НЕ БРАТЬ — решение P7, пере-подписано `D39.132` п.2б** (баннер выше несёт довод). ⚠ **Правка 29.08:** в этой клетке стояло «**ВЗЯТЬ** — доказано исполнением 05.08», и это ПРОТИВОРЕЧИЛО баннеру того же файла, который говорит «Решение P7 по обоим — НЕ БРАТЬ». Замер 05.08 остаётся верным и лежит ниже — он доказал, что инструмент РАБОТАЕТ, а не что его берут; слово вердикта было прочитано из замера. Сегодняшний статус: КАНДИДАТ, решение за паком, который его возьмёт | | ~~`sqlc` для денежных/квотных таблиц~~ → `sqlc` для поверхности контрактных ручек и read-model | v1.31.1 | **ПЕРЕСМОТРЕНО приёмкой P1 (05.08), доказано исполнением** — см. абзац ниже. Денежный пакет остаётся рукописным. ⚠ **НЕ ИСПОЛНЕНО на 14.08**, срок «до первого хендлера» пройден — см. баннер статуса выше (PD-44) | | `golang.org/x/time/rate` | v0.15.0 | лимиты в процессе; долговечные пер-пользовательские — в Postgres | diff --git a/platform/docs/STACK_DECISIONS.md b/platform/docs/STACK_DECISIONS.md index 0076a71f..58a175b7 100644 --- a/platform/docs/STACK_DECISIONS.md +++ b/platform/docs/STACK_DECISIONS.md @@ -552,9 +552,9 @@ export TM_PLATFORM_TEST_DSN='postgres://postgres@/postgres?host=/tmp&port=55432& - **Правил миграцию — пере-создай базу стенда** (`dropdb tmstand && createdb tmstand`): goose ключуется НОМЕРОМ и правку уже применённого файла не видит (то же правило, что в `deploy/README.md`). После пере-создания базы старый cookie-jar мёртв — повторить `/auth/dev-login`. -- **Красный `runner.TestARunIsBoundedByItsOwnCgroup` — обычно НЕ регрессия кода:** у `tm.slice` +- **Красный `runner.TestARunIsBoundedByItsOwnCgroup` — обычно НЕ регрессия кода:** у `tm-runs.slice` опустел `cgroup.subtree_control`. Лечение без root: `echo "+memory +pids" > - /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/tm.slice/cgroup.subtree_control`. + /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/tm-runs.slice/cgroup.subtree_control`. - **Демон нельзя убивать `pkill -f <путь к бинарю>`** — шаблон совпадает с собственной командной строкой оболочки, и она убивает сама себя (exit 144). Убивать по PID. - **Postgres стенда переживает не всё:** сокет живёт в `/tmp`, и уборка `/tmp` (или smart shutdown) diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index f68c8035..ada334ca 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -4,6 +4,88 @@ > вопросы, предложения на ратификацию. В `docs/PROGRESS.md` платформа не пишет; оркестратор > читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4). +## ПОСЛЕ ЛЕНДИНГА (2): аудит документации зоны — семь позиций разобраны, комментарий шва исправлен (сессия `textmachine-c0`, 29.08) + +Оркестратор передал список аудита доков по моей зоне («не заказ, а список»). Разобрала все семь, +каждую сперва ПРОВЕРИЛА в дереве. Ни одна не отвергнута — все подтвердились. + +**Комментарий шва (`PD-427`) — ИСПРАВЛЕН, оркестратор дал «правь».** В `internal/ingest/resync.go` +теперь сказано, что снята и ПОСЫЛКА (слепое окно закрыто лендингом `6ec9f8a`), и ПРЕДСКАЗАНИЕ (нового +движкового глагола не будет, починили существующий `status`), и названа причина, по которой пара не +берётся СЕГОДНЯ — гейт проводки вместе с `--max-units`. Вторая половина важнее первой, и довод его: +неверный ДОВОД сессия перепроверит, неверное ОЖИДАНИЕ она примет как карту. + +**⛔ Что было ложью в доках зоны:** +1. `README.md` объявлял, что «ручки поправить/добавить термин нет ни в зоне, ни в каноне» — дверь + `POST /books/{bookId}/bank/corrections` построена паком P9 и смонтирована. Предложение склеивало + снятую ПЕР-ТЕРМНУЮ ПОДПИСЬ с правкой термина. Исправлено. +2. Там же «Три канала движка, и других нет» — и тут же перечислялись ПЯТЬ. Счёт правился, слово нет. +3. **Число условий батареи жило в ТРЁХ файлах и они расходились** (README — два, `ENGINEERING_STANDARDS` — три, + `STACK_DECISIONS` — четыре). Это самый дешёвый способ получить ложную приёмку: сессия, честно + исполнившая §3.1 по устаревшей копии, объявит «скипов ноль» при красном тесте. **Носитель теперь + ОДИН** — `STACK_DECISIONS`, «Гейты батареи»; две другие копии заменены ссылкой на него. +4. `STACK_DECISIONS` в лечебной команде самой частой грабли стенда указывал на несуществующий срез + `tm.slice` (в коде `runner.go` — `tm-runs.slice`), причём двумя абзацами выше тот же файл писал + правильное имя. Оператор получал «No such file or directory». +5. `deploy/README.md` держал жёсткое правило «эмиттер не выкатывается, пока `tmctl migrate` не + заленден». Глагол существует и заленджен (`backend/cmd/tmctl/migrate.go`, свой код выхода 13). + Правило блокировало выкат по несуществующей причине — худший род блокировки, снаружи неотличимый + от осторожности. +6. **`deploy/README.md` показывал боевой env, при котором инстанс не запустит НИ ОДНОГО перевода:** + нет `ENGINE_BIN`, `CTL_BIN`, `STATE_DIR` (дефолт вне `ReadWritePaths=` при `ProtectSystem=strict`) + и `ENGINE_KEYS_PATH` — единственного канала ключей. Каждый ОПЛАЧЕННЫЙ прогон падал бы `exit 10`, а + на буте это WARN, то есть тихо. Дописаны все четыре, с объяснением, почему каждая обязательна. +7. `PLATFORM_DIRECTION.md` нёс в таблице «кодоген — **ВЗЯТЬ**, доказано 05.08» при баннере того же + файла «решение P7 по обоим — НЕ БРАТЬ». Замер 05.08 верен и остаётся: он доказал, что инструмент + РАБОТАЕТ, а не что его берут. Слово вердикта было прочитано из замера. + +**⛔ И самое неприятное — в РЕГИСТРЕ, и половина этого моя.** Секция и статус разошлись у десяти +строк: девять `fixed` и один `accepted-risk` лежали под заголовками «Открытые», то есть человек, +читающий открытый список, видел закрытую работу. Встречно и хуже — **`PD-424` и `PD-425`, оба +`major`, лежали в секции «Открытые — info»**, и `PD-425` денежный. Это МОЯ ошибка: я вставляла все +новые строки к одному якорю, не сверяя вес. Перенесены все; закрытые — в новую секцию «перенос по +секциям», где сказано, что статуса они не меняли. ⚠ Счёт по статусам от переноса не изменился +(`counts.py` ключуется формой строки, а не секцией) — изменилось то, что видит читатель. + +⚠ **Своя ошибка счёта, в третий раз за сутки:** мой разбор дал 19 «неуместных» строк против десяти у +аудита. Прав был аудит: три строки (`PD-115`, `PD-407`, `PD-122`) несут статус `open (…)` с +оговоркой в скобках, и мой парсер прочёл его как не-`open`. Тот же класс, что и раньше: вывод из +формы вместо чтения. + +**Слайсы регистра (444 КБ) НЕ трогала** и предупреждаю о том же, о чём предупредил аудит: +`docs/scripts/counts.py` держит регистр ОДНИМ путём и слайсы не глобит, поэтому вынос молча уронит +счёт 428 → 284. Скрипт в зоне оркестратора; браться за слайсы — только после того, как он научится +глобить. + +Гейты после всего: форма регистра зелёная (**428 строк, 107 open, 7 major**), битых якорей в зоне 0, +`go build` и `go vet` чисты. + +## ПОСЛЕ ЛЕНДИНГА: движковый пак снял посылку одного из решений зоны — заведено двумя строками (сессия `textmachine-c0`, 29.08) + +P11 залендён (`e548e5a`, канон 0.8.0), следом лёг движковый пак «деньги» (`6ec9f8a`, D39.170) — и он +**опроверг довод, на котором стоит комментарий шва в моей зоне**. Пришло пингом оркестратора, +проверено мной чтением ОБЕИХ сторон, а не принято на слово. + +**`PD-427` — комментарий несёт снятую посылку.** `internal/ingest/resync.go:37-43` объясняет, почему +платформа сознательно НЕ берёт `rebill_units`/`rebill_usd`: «status проецирует СОХРАНЁННУЮ память, и +сразу после `bank-apply` честно читает ноль». Движковый пак починил ровно это — `foldMemoryForRead` +стал ПЕРВЫМ ответом читающего пути, `projectStoredMemory` понижена до фолбэка, и комментарий движка +это объявляет дословно («IT IS NO LONGER THE READ PATH'S FIRST ANSWER»). ⚠ Комментарий неверен +ДВАЖДЫ: он ещё и предсказывает, что пара вернётся с НОВЫМ движковым глаголом, — а нового глагола не +появилось, починили существующий `status`. +⚠ **Проводку полей строка НЕ открывает** — она гейчена вместе с `--max-units`, и тот гейт в силе +(слово оркестратора). Предмет строки — ровно устаревший ДОВОД. +⚠ Правку самого комментария я НЕ делаю: оркестратор при передаче сказал «чинить прямо сейчас не +надо», а дерево только что залендено. Текст правки предложен ему пингом — решение его. + +**`PD-428` — калибровка цены, не дефект.** Замер движкового охотника: терминолог переигрывается на +КАЖДОЙ покупке ЦЕЛИКОМ по книге (три покупки по одному юниту — три полнокнижных консолидации по +$0.005460). Накладные масштабируются КНИГОЙ, а не грантом. Сегодня беспредметно, потому что продажа +идёт главами; строка существует, чтобы факт не потерялся к появлению мелкой нарезки, при которой +накладные обгонят полезную работу на самых дешёвых покупках. + +Регистр после: **428 строк, 107 open, 7 major**, форма зелёная, битых якорей в зоне 0. + ## ПАК P11 ОТРАБОТАН — отзыв сессии стал действием, застрявший расчёт стал виден и управляем; 7 строк из 7 (сессия платформы `textmachine-c0`, 29.08, промт `docs/PLATFORM_P11_SESSION_PROMPT.md`) Записка-план — следующей шапкой ниже. Здесь: числа С КОМАНДАМИ · что доказано живьём · таблица @@ -365,8 +447,7 @@ P11», каждая с телом (чем закрыта · чем доказа дефект в `sqlgate_test`), `PD-417` (цена широких выборок) — все четыре сразу `fixed` этим паком; и `PD-418`…`PD-422` — открытые (settling-строка живого прогона без ручки · слепота гейта миграций к пере-подписи · два теста, не выдерживающих параллельных батарей · остаток `PD-379` по подметанию · -условный `--resnapshot`). Гейт формы: `python3 docs/scripts/counts.py --check` → **422 строки, -`open` 101, `major` 5** (было 103 и 7), битая форма пустая, хвост вне словаря пустой. +условный `--resnapshot`). Гейт формы: `python3 docs/scripts/counts.py --check` → **428 строк, `open` 107, `major` 7** — счёт на момент сдачи был 422/101/5, и вырос от строк, заведённых уже ПОСЛЕ лендинга (`PD-423`…`PD-428`), битая форма пустая, хвост вне словаря пустой. ⚠ Отдельно: `PD-159` этот пак НЕ пере-открывает. Она стоит `fixed` с токеном `ОСПОРЕНО(PD-376)` (D39.159 §5), и теперь пробел, который несла `PD-376`, закрыт пином — то есть двусторонняя ссылка осталась целой, а спор разрешён в пользу строки: пин был нужен, мутация проходила батарею. diff --git a/platform/internal/ingest/resync.go b/platform/internal/ingest/resync.go index ca4eace3..b7378c9b 100644 --- a/platform/internal/ingest/resync.go +++ b/platform/internal/ingest/resync.go @@ -34,13 +34,28 @@ type StatusReport struct { ETASeconds float64 `json:"eta_seconds"` // UnsignedBankTerms backs the signing screen's "N of M decided" while a stop is standing. UnsignedBankTerms int `json:"unsigned_bank_terms"` - // The engine's re-bill projection (rebill_units/rebill_usd) is deliberately NOT taken, and the - // reason is not vocabulary but TIMING: status projects the STORED memory, and a correction - // reaches it only when the next translate folds the bank in — so right after an apply, the one - // moment a consent would want the figure, it honestly reads zero (P10 errata 28.08-к). The - // consents are funded from the run's own hold instead, and a field without a consumer is the - // defect class this project names — the pair returns with the engine verb that can fold and - // price a correction OUTSIDE a run (orchestrator's backlog row). + // The engine's re-bill projection (rebill_units/rebill_usd) is deliberately NOT taken — but the + // REASON changed under this comment, and both halves of what it used to say are now retired + // (register row PD-427). + // + // ⚠ The old reason was TIMING, and it was true when it was written: status projected the STORED + // memory, so right after a `bank-apply` — the one moment a consent would want the figure — it + // honestly read zero (P10 errata 28.08-к). That blind window is CLOSED: the engine's read path + // now folds the bank first and falls back to the stored glossary only if it cannot + // (`pipeline.projectStoredMemory`, "IT IS NO LONGER THE READ PATH'S FIRST ANSWER", landing + // 6ec9f8a / D39.170). `status` will price a re-pass BEFORE it is bought, and still writes nothing. + // + // ⚠ The old comment also predicted the SHAPE of the fix — "the pair returns with the engine verb + // that can fold and price a correction outside a run" — and that is the half worth correcting + // loudest, because a wrong REASON gets re-checked while a wrong EXPECTATION gets used as a map. + // No such verb was built and none is planned: the existing `status` was fixed instead. + // + // Why the pair is still not taken TODAY: its wiring is gated together with the engine's + // `translate --max-units`, which waits on a decision of its own (the volume ceiling was broken on + // a book that mines its bank; the cure landed, the wiring did not). So the figure exists and the + // seam simply does not carry it yet — which is a different sentence from the one this comment + // used to make, and the difference is what a later session would otherwise design against. + // The consents are meanwhile funded from the run's own hold. // Spend is the engine's committed spend, converted to integer micro-USD AT THE SEAM. The wire // value is a JSON decimal; binding it to a float64 would put drift one step before the integer // column that exists to prevent drift (PD-15). It is stored in the credit tables and NEVER