From aca06cdd1dec70ad6424523e06c08c0cc76fd6e1 Mon Sep 17 00:00:00 2001 From: heaven Date: Wed, 2 Sep 2026 15:47:05 +0300 Subject: [PATCH] Apply the controller's dofix in the platform zone: restore what the cut lost and repair what it broke, including places where the text asserted the wrong thing about the code --- platform/BACKLOG.md | 5 +- platform/README.md | 3 +- platform/docs/DEFECT_REGISTER.md | 4 +- platform/docs/PLATFORM_DIRECTION.md | 9 ++- platform/docs/STACK_DECISIONS.md | 6 +- platform/docs/platform-PROGRESS.md | 87 +++++++++++++++++++++++++---- 6 files changed, 94 insertions(+), 20 deletions(-) diff --git a/platform/BACKLOG.md b/platform/BACKLOG.md index 04535cfc..81900135 100644 --- a/platform/BACKLOG.md +++ b/platform/BACKLOG.md @@ -7,6 +7,9 @@ > (постановка холда воркером — часть П-1), П-4 отменён и поглощён П-7, П-5 переопределён под > кредитную модель и ждёт правки спеки, П-1 продолжена, П-2/П-3 не трогали. > Дублировать их здесь не стали — у строки один источник истины. +> ⚠ Сводка датирована 05.08 и задним числом не переписывается, но по П-5 она УСТАРЕЛА: спека с тех +> пор правлена, ручка построена — актуальное состояние в самой строке П-5 ниже. Адрес слайса +> file-относительный: `platform/docs/archive/platform-PROGRESS-P0-P3.md:1086`=`## Диспозиции бэклога зоны`. | ID | Хвост | Вес | Источник | |---|---|---|---| @@ -17,7 +20,7 @@ | П-6 | **OAuth-вход популярных провайдеров (Google первым) + модель аккаунта** (направление владельца 04.08) — ⚠ **ЗАКРЫТ P1:** authorization code + PKCE, собственная серверная сессия сохраняется (D39.84 — OAuth даёт только СОБЫТИЕ входа), ключ личности `(provider, subject)`; код — `internal/login/`, состав и живой остаток — диспозиция из шапки. Писатель куки с `__Host`-атрибутами и dev-профиль — PD-8 | P1, вместе с П-1 | владелец 04.08 | | П-7 | **Кредитный баланс и фри-тир** (решения владельца 05.08; ПРОДАЖИ НЕТ — бета на предоплаченных ключах владельца): append-only леджер в целых микро-долларах, грант фри-тира из админки (**дефолт НОЛЬ**, начисление руками — `tmplatformctl grant`), холд ДО спавна прогона + пер-книжный потолок движку (жёсткий стоп исполняет движок), расчёт на границе попытки. ⚠ Прежний «дефолт $5» ОТМЕНЁН словом владельца 16.08 (D39.138 п.2л, PD-104); возврат $5 идёт вместе с суточным агрегатным потолком. ⚠ Окон с обнулением НЕТ — баланс, а не подписка: черновик `usage_windows` в подписочной форме заменяется. Платёжный провайдер не проектируется до решения продавать. Разбор — `docs/PLATFORM_DIRECTION.md` §2 | P1+, после П-6 | владелец 04–05.08 | | П-8 | **Минимальная админ-поверхность**: начислить/списать кредиты аккаунту (одна запись леджера), посмотреть баланс и состояние прогонов — ⚠ **ЗАКРЫТ P1 формой CLI:** `tmplatformctl grant/adjust/balance/logins/revoke` (`cmd/tmplatformctl/`) | вместе с П-7 | владелец 05.08 | -| П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны». ⚠ **ПЕРЕОПРЕДЕЛЁН под кредитную модель (диспозиция P1, шапка выше):** окон и `resets_at` нет, форма ответа там же предложена, **ручка НЕ построена — ждёт правки спеки** | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 | +| П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны». ⚠ **ПЕРЕОПРЕДЕЛЁН под кредитную модель (диспозиция P1, шапка выше):** окон и `resets_at` нет, **ручка ПОСТРОЕНА, спека правлена** — канон несёт `docs/architecture/14-api-contract/openapi.yaml:815`=`operationId: getUsage` с прямым «no period, no `resets_at`», маршрут смонтирован (`platform/internal/httpapi/v0.go:89`=`path: "/usage"`, греп `"/usage"`; хендлер — символ `func (h *v0) usage`). Диспозиция П-5 с тремя поправками (колонка `Run.paused_reason` · скоуп `revision` · трактовка D39.84) — `platform/docs/archive/platform-PROGRESS-P0-P3.md:861`=`потолок поднимает платформа) — ПРИНЯТ` | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 | | П-9 | **Загрузка книги `POST /books` (multipart)** — **ИСПОЛНЕНО P5 (11.08)**: состав в `platform/README.md` §«Что здесь будет»; PD-72 закрыт своим тестом. Развилка «кто пишет стартовый `book.yaml` при интейке» (D39.110 §2b, шов `books.ErrNotProvisioned`) **РЕШЕНА D39.130:** бета = форма Б (стройка — П-14); движковая форма В (`tmctl init`) = строка 170 единого | Ф3, следующий пак | сессия P4 (границей промта D39.119 §5а), исполнено P5 | | П-10 | **Честная оценка «$/глава» от ДВИЖКА.** Сейчас ставка — константа платформы ($0.03, провенанс exp08 v2 через D30.4, `STACK_DECISIONS` §20), и это осознанная бета-мера: движковой поверхности оценки не существует, а выдумывать её запрещено. Ставка решает только ДЛИНУ шкалы (деньги защищены холдом и потолком движка), но на книге, которая заметно дороже или дешевле средней, шкала врёт пользователю о том, сколько глав он покупает. Нужна оценка от движка по конкретной книге — запрос уходит строкой ЕДИНОГО бэклога через оркестратора, не сюда | когда-нибудь (до первого платящего) | сессия P4 | | П-11 | **Наблюдаемость раннера** — **ИСПОЛНЕНО P5 (11.08):** `prometheus/client_golang` v1.24.1 на отдельном слушателе `TM_PLATFORM_METRICS_ADDR`; глубина очереди · возраст самого старого открытого холда · карантины · отставание тейлера · книги в интейке · длительность свипа и счётчик недоведённых проходов · запросы и задержки по паттерну маршрута. Ось наблюдаемости получила внешний эталон (практики именования Prometheus + золотые сигналы, `ENGINEERING_STANDARDS` §2) — половина PD-115, вторая половина строки остаётся открытой | Ф3 | сессия P4, исполнено P5 | diff --git a/platform/README.md b/platform/README.md index de259650..929f14e4 100644 --- a/platform/README.md +++ b/platform/README.md @@ -157,7 +157,8 @@ live-сверка 04–05.08); общая записка по обоим нов **подключена и работает** (P4) · вход `x/oauth2` + `go-oidc/v3` · `x/time` для лимита на `/auth/login` · `govulncheck` отдельной целью. ⚠ Номера версий здесь намеренно не дублируются: их единственные носители — `go.mod` и таблица пинов строкой выше. -**Redis не заводим нигде** — зафиксировано как архитектурное «нет». +**Redis не заводим нигде** — зафиксировано как архитектурное «нет», **иначе он приползёт по частям**: +очередь, лизы и рейт-лимиты живут в том же Postgres. Прогресс наружу — SSE, события **пушит воркер**, а не фронт опрашивает read-model. Аутентификация — одна серверная сессия в Postgres, два способа предъявления: `__Host`-кука для diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index 993a91f1..bdff7784 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -93,7 +93,7 @@ | PD-96 | hardening | info | `internal/httpapi/server.go:39-43`, `internal/auth/csrf.go:28` | **`TrustedOrigins` обещает отдельно развёрнутый фронт, но CORS-слоя нет вовсе.** Живая проба: preflight `OPTIONS` с `Origin: https://app.example.org` получает 401 от гарда (браузерный preflight креденшелов не носит и не должен), заголовков `Access-Control-*` нет ни на одном ответе. Сценарий «фронт на другом origin» браузером сегодня неисполним: либо CORS приезжает вместе с контрактными ручками (П-1), либо фронт живёт на том же origin, и тогда `TrustedOrigins` — мёртвая ручка | open | приёмка P2 (панель + живая проба) | | PD-98 | doc | info | `internal/pgstore/store.go:75-79` | Случай «схема НОВЕЕ бинаря» в `Ready` беззвучен — признано ⚠-комментарием на месте, но ни одной строки лога: оператор, запустивший старый бинарь на новой схеме, сигнала не получит | open | приёмка P2 (панель) | | PD-107 | hardening | info | `internal/pgstore/migrations/00007_credits.sql:85`, `00002_readmodel.sql:13` | **Удаление аккаунта обходит защиту PD-25:** составной FK `reservations → books(id, owner_id) on delete restrict` блокирует `DeleteBook`, но `users` каскадит в `reservations` НАПРЯМУЮ, поэтому `delete from users` уносит и ОТКРЫТУЮ резервацию. Замерено приёмкой: аккаунт с открытым холдом удаляется. Учётной дыры нет — леджер и кэш баланса каскадятся тем же удалением, — но прогон, идущий против этого холда, останется без того, кто его закроет. Кода удаления аккаунта в дереве нет вовсе (грепнуто) ⇒ строка = гейт перед появлением такой операции (и перед ASVS 7.4.2 в полной форме). ⚠ Заодно ОПРОВЕРГНУТА обратная версия этой находки от панели («удаление падает на композитном FK даже при закрытых резервациях») — мой прогон: удаляется и с закрытой резервацией, и без неё | open | приёмка P2 (замер оркестратора №15; версия панели опровергнута) | -| PD-122 | bug | info | `internal/pgstore/books.go` `ListBooks` | **`Library.revision` НЕ монотонна: она выведена как `max(books.revision)` по книгам аккаунта и падает, когда удаляется книга, державшая максимум.** Контракт требует монотонности внутри области и предписывает клиенту ОТБРАСЫВАТЬ чтение с меньшей ревизией — то есть после такого удаления библиотека замирает, пока чей-нибудь книжный счётчик не перерастёт старый максимум. Замерено ревью: 10 → 0 после удаления книги. ⚠ Сегодня недостижимо через API: ручки удаления книги нет вовсе (`DeleteBook` есть в сторе, маршрута нет). Правильное решение — собственный счётчик области у аккаунта, который двигается на изменение состава и статусов. ⚠ Уточнено ревью доков 09.08: КОЛОНКА уже есть — `users.library_revision` из `00001_identity.sql:13`, и её не читает и не пишет ни один Go-путь (грепнуто), так что нужна не миграция, а пути записи и чтения; правка нескольких мест, поэтому она НЕ сделана в этом паке, а названа. ⚠ Сужено паком P5: ДОБАВЛЕНИЕ книги ревизию области двигает — новая книга входит в библиотеку с `revision = max(revision по владельцу) + 1` (`pgstore.nextLibraryRevision`, обе точки входа: дев-интейк и загрузка), и каждая смена статуса интейка её тоже двигает; пин `books.TestABookThatJoinsTheLibraryMovesItsRevision`, посадка «входить с нулём» падает. Открытым остаётся исходный случай: УДАЛЕНИЕ книги может уронить максимум, и лечится это собственным счётчиком области. Ручки удаления по-прежнему нет ⚠ ПЕРЕ-ФОРМУЛИРОВАНА дофиксом приёмки (FP5-1): клейм «добавление закрыто» был ЛОЖЕН. Пол области поднимался при удалении, а вставка читала только максимум — книга, загруженная после отменённой загрузки, входила НА или НИЖЕ пола, и одна ревизия отвечала за три разных состояния библиотеки. Теперь вставка берёт `greatest(max, пол) + 1`, пин `books.TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision` гоняет именно сценарий «отмена → повторная загрузка». ОТКРЫТЫМ остаётся: смена статуса книги, которая НЕ самая новая, ревизию области не двигает — ни одна из половин не растёт; лечится тем же счётчиком области, который двигают все писатели состава и статусов ⚠ Дополнено ре-чеком (FP5-11): правило «брать следующий номер БИБЛИОТЕКИ» применено и к пяти писателям жизненного цикла прогона (старт, реоткрытие, пауза, два закрытия) — без них staleness оставалась на статусах прогона: книга не-максимума аккаунта уходила `translating`, а библиотека не двигалась. Пин `pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary`. Прогресс-путь синка НАМЕРЕННО не тронут (главная шкала считается от книжной до инкремента; прогресс бывает только у книги с идущим прогоном, а её старт уже поднял номер) | open (добавление закрыто P5; удаление — нет) | адверсариальное ревью (исполнением) | +| PD-122 | bug | info | `internal/pgstore/books.go` `ListBooks` | **`Library.revision` НЕ монотонна: она выведена как `max(books.revision)` по книгам аккаунта и падает, когда удаляется книга, державшая максимум.** Контракт требует монотонности внутри области и предписывает клиенту ОТБРАСЫВАТЬ чтение с меньшей ревизией — то есть после такого удаления библиотека замирает, пока чей-нибудь книжный счётчик не перерастёт старый максимум. Замерено ревью: 10 → 0 после удаления книги. ⚠ Сегодня недостижимо через API: ручки удаления книги нет вовсе (`DeleteBook` есть в сторе, маршрута нет). Правильное решение — собственный счётчик области у аккаунта, который двигается на изменение состава и статусов. ⚠ Уточнено ревью доков 09.08: КОЛОНКА уже есть — `users.library_revision` из `00001_identity.sql:13`, и её не читает и не пишет ни один Go-путь (грепнуто), так что нужна не миграция, а пути записи и чтения; правка нескольких мест, поэтому она НЕ сделана в этом паке, а названа. ⚠ Сужено паком P5: ДОБАВЛЕНИЕ книги ревизию области двигает — новая книга входит в библиотеку с `revision = max(revision по владельцу) + 1` (`pgstore.nextLibraryRevision`, обе точки входа: дев-интейк и загрузка), и каждая смена статуса интейка её тоже двигает; пин `books.TestABookThatJoinsTheLibraryMovesItsRevision`, посадка «входить с нулём» падает. Открытым остаётся исходный случай: УДАЛЕНИЕ книги может уронить максимум, и лечится это собственным счётчиком области. Ручки удаления по-прежнему нет. **Гейт строки: закрыть ДО появления ручки удаления книги** — обратная сторона этой же пары стоит в `PD-175` (греп `гейт PD-122`) ⚠ ПЕРЕ-ФОРМУЛИРОВАНА дофиксом приёмки (FP5-1): клейм «добавление закрыто» был ЛОЖЕН. Пол области поднимался при удалении, а вставка читала только максимум — книга, загруженная после отменённой загрузки, входила НА или НИЖЕ пола, и одна ревизия отвечала за три разных состояния библиотеки. Теперь вставка берёт `greatest(max, пол) + 1`, пин `books.TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision` гоняет именно сценарий «отмена → повторная загрузка». ОТКРЫТЫМ остаётся: смена статуса книги, которая НЕ самая новая, ревизию области не двигает — ни одна из половин не растёт; лечится тем же счётчиком области, который двигают все писатели состава и статусов ⚠ Дополнено ре-чеком (FP5-11): правило «брать следующий номер БИБЛИОТЕКИ» применено и к пяти писателям жизненного цикла прогона (старт, реоткрытие, пауза, два закрытия) — без них staleness оставалась на статусах прогона: книга не-максимума аккаунта уходила `translating`, а библиотека не двигалась. Пин `pgstore.TestARunTransitionOnAnOlderBookStillMovesTheLibrary`. Прогресс-путь синка НАМЕРЕННО не тронут (главная шкала считается от книжной до инкремента; прогресс бывает только у книги с идущим прогоном, а её старт уже поднял номер) | open (добавление закрыто P5; удаление — нет) | адверсариальное ревью (исполнением) | | PD-123 | doc | info | `internal/pgstore/migrations/00009_runner.sql:11` | `runs.ceiling_chapters` имеет `default 0`, а контракт объявляет `Run.ceiling_chapters` `minimum: 1`. Сегодня недостижимо: единственный путь вставки — `StartRun`, и он отказывает на неположительном значении. Строка заведена как гейт: строка прогона, записанная мимо `StartRun` (миграция данных, правка оператором), спроецируется на провод нулём, которого схема клиента не допускает | open | адверсариальное ревью (чтение схемы) | | PD-137 | hardening | info | `deploy/tmplatformd.service` `[Unit]` | `BindPaths=/run/user/%U` требует существования каталога на старте юнита, а создаёт его logind вместе с пользовательским менеджером; в `[Unit]` упорядочения на него нет. На первом бутe это гонка, которую лечит `Restart=on-failure` (сервис поднимается со второй попытки). Строка не закрыта кодом намеренно: UID сервисного пользователя site-specific, поэтому `After=user@.service` добавляется установкой — инструкция вписана в шапку юнита | open | адверсариальное ревью (чтение) | | 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 | адверсариальное ревью (чтение) | @@ -586,6 +586,6 @@ | PD-367 | bug | minor | `internal/books/parse.go:129`, `internal/ingest/manifest.go` `Whole` | **Пол на самосогласованность манифеста стоит только у материализатора, а интейк тот же документ ПРИНИМАЕТ.** Манифест `{ChaptersTotal: 120, UnitsTotal: 400}` с пустым списком глав `Whole()` отвергает, а `books.Parse` заводит книгу `not_started` с `chapter_count=120` и пустым деревом — по такой книге можно СТАРТОВАТЬ и ОПЛАТИТЬ прогон (потолок считается от `chapter_count`). Воспроизведено ревью на живом Postgres. ⚠ **ЗАКРЫТА пак P12 (30–31.08) по решению оркестратора: пол самосогласованности СИММЕТРИЧЕН, интейк отказывает.** `ingest.Manifest.Readable()` — ВТОРОЙ предикат рядом с `Whole()`, не расширение `DecodeManifest` (у того четыре пина намеренно декодируют частичные и чужие документы, и его собственная дока объявляет правило «значение не гейтим» решением). Стоит ПЕРВЫМ в `books.Parse`, до ветвей по `ChaptersTotal`: ниже этой черты документ, прочитанный неверно, неотличим от книги, в которой ничего нет, а ЭТО чтение удаляет аплоад. Класс — `parser_unavailable`: бюджет попыток тратится, файл остаётся. ⚠ **ПРАВКА ЗАПИНЕННОГО КОНТРАКТА, заказанная промтом P12 §3.8** — не подгонка под зелень: батарея интейка ездила на документах без списка глав, и фикстуры РАСШИРЕНЫ (`wholeManifest`), а не обойдены; поимённо пере-подписаны фикстуры `books_test.newFixture` и два манифеста `render_test`. Пин — `books.TestTheIntakeRefusesTheDocumentItsOwnMaterialiserWouldReject` (ровно документ строки: 120 глав, 400 пар, пустой список; проверяет и что `chapter_count` НЕ записан, и что файл цел); посадка M15 КРАСНАЯ адресно. | fixed(пак P12) | воркфлоу-ревью волны 2 (P8-FIX); рефутер подтвердил механику и опроверг предложенное лекарство | | PD-213 | hardening | info | `internal/ingest/manifest.go`, `internal/books/parse.go` | **Форма манифеста не версионируется на стороне платформы — латентная мина на УДАЛЕНИЕ файла.** `DecodeManifest` не сверяет `manifest_version` ни с чем; `json.Unmarshal` тихо игнорирует незнакомые поля и оставляет отсутствующие нулями, поэтому смена формы движком (`tm-manifest-v2` → v3, переименование `chapters_total`) даст валидный разбор с `ChaptersTotal = 0`. А ноль глав интейк трактует как «источник прочли, книги нет» — тот же терминал и тот же бюджет, что exit 11, то есть после пяти попыток файл пользователя УДАЛЯЕТСЯ, хотя движок книгу прекрасно разобрал. Сегодня формы совпадают поле-в-поле, так что не эксплуатируется; в отличие от потока (мажор отвергается) и от полосы кодов (незнакомый номер безопасен), у манифеста аналога нет. Лечить сверкой `manifest_version` с известной, где незнакомая версия даёт НЕ-деструктивный класс ⚠ **ЗАКРЫТА пак P12 (30–31.08) тем же классом «незнакомое → не-деструктивно»,** как и предписывала строка. `ingest.KnownManifestVersion` (`tm-manifest-v2`, зеркало `backend/internal/pipeline/manifest.go:46`) сверяется в `Readable()` перед всем остальным, и незнакомая версия даёт `parser_unavailable` — файл цел. Это ФОРМЕННЫЙ гейт, а не пин значения: сама строка и дока поля правы, что пиннинг значения везде сделал бы каждый релиз движка релизом платформы, поэтому версия сверяется РОВНО в одном месте — там, где неверное чтение разрушительно. Пин — `books.TestAManifestShapeThisBuildDoesNotKnowNeverDeletesTheUpload`: v3-манифест переживает весь бюджет попыток с целым файлом, а ЗНАКОМАЯ форма с честно нулевыми счётчиками по-прежнему удаляет каталог (иначе гейт съел бы настоящий вердикт о тексте пользователя). Посадка M14 (снять сверку версии) КРАСНАЯ адресно. | fixed(пак P12) | адверсариальное ревью P6 (линза шва) | | 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 единого бэклога ⚠ **ЗАКРЫТА пак P12 (30–31.08) — акт закрытия, не работа:** комментарий `internal/ingest/resync.go` уже исправлен 29.08 аудитом доков, снятая посылка из него ушла, предсказание про «новый движковый глагол» тоже. Проверено чтением обеих сторон. Проводку полей строка не открывала и не открывает — гейт `--max-units` в силе. | fixed(пак P12, акт закрытия) | оркестратор №19 при лендинге движкового пака (`6ec9f8a`), проверено чтением обеих сторон сессией P11 | -| PD-203 | bug | info | `internal/pgstore/books.go` `ReadUsage` | **Аккаунт объявляется исчерпанным по паузе ОДНОГО прогона.** `/usage` ставит `paused_reason` аккаунта, если у какой-нибудь книги последний прогон стоит `paused/credit_exhausted` — а это потолок ПРОГОНА (сколько глав купил пользователь), а не баланс: на аккаунте может лежать сколько угодно денег, и другой прогон стартует. Контракт про это поле говорит «Set when the account itself is in a halted state». Существовало до этого пака и не им создано; отдельной строкой, потому что различение потолков (PD-199) сделало вопрос «чей это потолок» отвечаемым ⚠ **ЗАКРЫТА пак P12 (30–31.08): предикат пере-проверен и предложение P8-REVIEW подтверждено.** `internal/pgstore/books.go` `ReadUsage` читает БАЛАНС аккаунта и ставит аккаунтную причину только при `balance <= 0`; книги не сканируются. Канон на той же стороне: `AccountHaltReason` — «a state of the account, not of a run», отдельный словарь ровно затем, чтобы прогонная причина не зажигала аккаунтный флаг. То есть дефект строки не воспроизводится, и это закрытие, а не пере-открытие. | fixed(пак P12) | сессия P6 (самопроверка вокруг PD-199) | +| PD-203 | bug | info | `internal/pgstore/books.go` `ReadUsage` | **Аккаунт объявляется исчерпанным по паузе ОДНОГО прогона.** `/usage` ставит `paused_reason` аккаунта, если у какой-нибудь книги последний прогон стоит `paused/credit_exhausted` — а это потолок ПРОГОНА (сколько глав купил пользователь), а не баланс: на аккаунте может лежать сколько угодно денег, и другой прогон стартует. Контракт про это поле говорит «Set when the account itself is in a halted state». Существовало до этого пака и не им создано; отдельной строкой, потому что различение потолков (PD-199) сделало вопрос «чей это потолок» отвечаемым ⚠ **СУЖЕНО P7:** `Usage.halt_reason` получил СВОЙ словарь (`AccountHaltReason`), и `PD-241` убрал самый частый ложный источник — стоп пользователя, приезжавший `credit_exhausted` ⚠ **ПАК P8-REVIEW 24.08 ПРЕДЛОЖИЛ ЗАКРЫТЬ, проверив предикат по коду:** `internal/pgstore/books.go` `ReadUsage` читает БАЛАНС аккаунта, а не сканирует книги (`platform/internal/pgstore/books.go:1116`=`case balance <= 0:`), а ⚠-комментарий рядом прямо описывает замену (`platform/internal/pgstore/books.go:1085`=`It used to be read off the`); единственный писатель `Usage.PausedReason` — эта же строка (греп `PausedCreditExhausted` по `internal/pgstore/books.go`), приехало `9b23e8c` ⚠ **ЗАКРЫТА пак P12 (30–31.08): предикат пере-проверен и предложение P8-REVIEW подтверждено.** `internal/pgstore/books.go` `ReadUsage` читает БАЛАНС аккаунта и ставит аккаунтную причину только при `balance <= 0`; книги не сканируются. Канон на той же стороне: `AccountHaltReason` — «a state of the account, not of a run», отдельный словарь ровно затем, чтобы прогонная причина не зажигала аккаунтный флаг. То есть дефект строки не воспроизводится, и это закрытие, а не пере-открытие. | fixed(пак P12) | сессия P6 (самопроверка вокруг PD-199) | | PD-380 | hardening | minor | `internal/pgstore/sessions.go:96`=`AbsoluteExpiresAt: now.Add(maxAge),`, `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 пакетах. ⚠ **ВЕС ПОДНЯТ info → minor закрывающим ревью, довод — симметрия с `PD-375`:** форма идентична (единственная точка принуждения объявленной границы не исполняется ни одним тестом, мутация переживает ПОЛНУЮ батарею, транзитивной страховки нет — `Touch` зажимает по значению ИЗ ТОЙ ЖЕ испорченной строки), а вес расходился только по валюте: там деньги и minor, здесь механизм ASVS 7.3.2 уровня 2 при объявленной зоной базовой линии L2 и info. Асимметрия была отпечатком того самого храповика «вреда сегодня нет», который это ревью и нашло ⚠ **Якорь пере-нацелен паком `sqlc` (29.08):** прежний токен — `s.pool.Exec` в `CreateSession` — исчез, потому что SQL этого запроса уехал в `internal/pgstore/queries/sessions.sql` и исполняется генерённым кодом. Новая цель — строка, где `maxAge` СТАНОВИТСЯ значением (`AbsoluteExpiresAt: now.Add(maxAge)`): именно она несёт факт, о котором строка, и она переживёт следующую генерацию. Сам дефект не тронут — потолок по-прежнему не запинен. ⚠ **ЗАКРЫТО паком `sqlc` (`63fcee5`, D39.172), пере-проверено ПОСАДКОЙ, а не рассуждением.** Пин — `pgstore.TestTheTwoSessionDeadlinesAreNotInterchangeable` (`sessions_test.go`): он создаёт сессию с РАЗНЕСЁННЫМИ сроками (`idleTTL` 1 ч против `maxAge` 24 ч — фикстура, где они совпадают, здесь ничего не доказывает, потому что `Touch` зажимает idle к absolute) и утверждает `AbsoluteExpiresAt == now.Add(maxAge)` после `CreateSession`, то есть ровно в единственном месте, где потолок становится фактом в базе. Именно та мутация, которой строка заведена — M12, `now.Add(maxAge)` → `now.Add(100*maxAge)` — теперь КРАСНАЯ адресно (замер 29.08: `AbsoluteExpiresAt = 2026-12-07…, want 2026-08-30…`). Пак строку не искал: тест писался против перестановки двух сроков, и потолок оказался запинен тем же утверждением — поэтому закрытие подтверждено пере-прогоном ИМЕННО M12, а не сходством формулировок | fixed | ревью-пак P8-REVIEW, ось 2 (посадка мутации, пере-посажена рефутером на полном пакете) | | PD-44 | hardening | info | `internal/pgstore/` | `sqlc` не взят, хотя направление §3 предписывает взять его ДО появления денежных таблиц. Весь денежный SQL — сырые строки pgx ⚠ **P8-FIX: половина, которая ЛЕЧИТ класс, построена; сам инструмент — вопрос владельцу.** Рантайм-ошибки «нет такой колонки» (`r.stop_for_signing`, `chapters_before`) случились в СКЛЕЕННОМ SQL read-модели, куда sqlc по построению не доходит, поэтому тем же пунктом заведён постоянный гейт, который доходит: `pgstore.TestEverySQLStatementParsesAgainstTheMigratedSchema` сворачивает КАЖДЫЙ SQL пакета из исходника (литералы, конкатенации, именованные константы) и планирует его Postgres'ом (`explain (generic_plan)`) против мигрированной схемы — **162 оператора, все планируются**. Посадка мутации в СКЛЕЕННЫЙ фрагмент (`c.units_edit_done` → несуществующая колонка) гейтом ловится; несворачиваемый SQL — ОШИБКА гейта, а не пропуск (единственное исключение — `store.go` `Ready`, где имя таблицы принадлежит goose, и оно выписано таблицей в самом гейте). Тем же гейтом закрыт открытый вопрос фикс-листа «есть ли в read-модели запрос, которого не касается ни один тест»: теперь его касаются все, на каждом прогоне батареи. **ГРАНИЦА sqlc:** read-модель для него недостижима по построению — склеек в пакете **25 мест из 147**; конвертируем только блок из пяти файлов без склейки (`credits`·`identity`·`idempotency`·`sessions`·`observe`). ⚠ **РЕШЕНО владельцем 22.08 (D39.154): sqlc берётся ОТДЕЛЬНОЙ СЕССИЕЙ, не внутри пака** — генерённый код в дереве, пин версии инструмента, гейт актуальности, `WithTx` для денежных запросов; мешать это с содержательной работой нельзя. ⚠ **ЗАКРЫТО: инструмент взят и заленджен** — `63fcee5`, ратификация `D39.172`, отдельным паком, как решил владелец 22.08 (`D39.154`). Конвертировано 40 запросов из этого самого блока; носители — `platform/sqlc.yaml`, `internal/pgstore/queries/*.sql`, генерённые `*.sql.go` в том же пакете. Актуальность генерации гейчена дважды: `sqlc diff` пререквизитом `make check` и `pgstore.TestEveryGeneratedQueryMatchesItsSourceFile` в батарее (работает без установленного sqlc). ⚠ **Замер набора на 29.08** (прежний счёт «41 запрос в 5 файлах» снят — он был сделан до пака P11, добавившего `StillLive`): в наборе **42 места вызова / 41 различный текст SQL** (константа `read` в `idempotency.go` исполнялась из двух мест), из них **конвертируемых 40**. Сороковой не `observe.go`: `Observe` спрашивает `river_job` через `to_regclass`, а эту таблицу мигрирует River сам, вне goose-миграций, поэтому sqlc отвергает запрос — и добавить схему River в конфиг значило бы завести ВТОРОЙ носитель чужой схемы. Гейт `sqlgate` после конверсии видит **172** оператора против пола 140 | fixed | ревью «вне карты»; гейт и граница — P8-FIX | diff --git a/platform/docs/PLATFORM_DIRECTION.md b/platform/docs/PLATFORM_DIRECTION.md index fb09576f..4aaed7e4 100644 --- a/platform/docs/PLATFORM_DIRECTION.md +++ b/platform/docs/PLATFORM_DIRECTION.md @@ -35,8 +35,9 @@ hosted IdP (связка PII + доступность, а свою сессию связывания и только при `email_verified` (Google прямо предупреждает: почта меняется, primary identifier из неё делать нельзя). 2. Коллизия почты решена ДО первого входа (миграция `00005`): `users.email` — NULLABLE и БЕЗ - уникального индекса, неизвестная пара всегда создаёт НОВЫЙ аккаунт. Цена и разбор — - `STACK_DECISIONS.md` §9. + уникального индекса, неизвестная пара всегда создаёт НОВЫЙ аккаунт. Цена и разбор — архив-слайс + журнала зоны, `platform/docs/archive/platform-PROGRESS-P0-P3.md:496`=`### Политика коллизии почты` + (в ЖИВОМ журнале раздела нет — он закрыт вместе с эрой P0–P3); строка пина — `STACK_DECISIONS.md` §9. 3. Ротация идентификатора сессии на границе входа (защита от фиксации) — обязательна. 4. ⚠ **Журнал входов** (время, провайдер, класс устройства/IP) и ручка «отозвать все мои сессии» — день-один для сервиса, продающего токены: таблица сессий чистится свипом и аудитом не является. @@ -130,6 +131,10 @@ hosted IdP (связка PII + доступность, а свою сессию | `sqlc` на свободный от склейки блок `pgstore` (включая денежный) | v1.31.1 | **ВЗЯТ и заленджен 29.08** (`D39.172`, PD-44 закрыт) — устройство и границы в `STACK_DECISIONS.md`, строка «Кодоген SQL» | | `golang.org/x/time/rate` | v0.15.0 | лимиты в процессе; долговечные пер-пользовательские — в Postgres | +⚠ **Границы той пробы 05.08:** sqlc читал ВСЕ goose-миграции зоны на тот момент — их было СЕМЬ; +`00008` приехала позже и пробу не проходила (`platform/internal/pgstore/migrations/`, греп +`00008_auth_state_issuer_and_start_id.sql`). + ⚠ **Две трения `sqlc`, увиденные исполнением на пробе 05.08 (тот же пин 1.31.1, что взят):** его анализатор отвергает запрос, который Postgres принимает (неквалифицированный `user_id` в коррелированных подзапросах) — переход означает правку существующего SQL, а не обёртку; и колонки типизуются как diff --git a/platform/docs/STACK_DECISIONS.md b/platform/docs/STACK_DECISIONS.md index bf39c2ef..1d9e8d45 100644 --- a/platform/docs/STACK_DECISIONS.md +++ b/platform/docs/STACK_DECISIONS.md @@ -25,7 +25,8 @@ | Кодоген SQL | `sqlc` **v1.31.1** | 22.04.2026 | **Инструмент разработчика, НЕ зависимость модуля** — в `go.mod` не входит, рантайм-граф не растёт ни на один пакет; пин держит `make tools-check`, как у линтера, и по более острой причине: генерённый код лежит В ДЕРЕВЕ, поэтому другая версия молча даёт другой диф и `sqlc diff` краснеет на чистом клоне. **Зачем:** генерирует слой запросов свободного от склейки блока `internal/pgstore` (40 запросов из `queries/*.sql`) — SQL и его `Scan` перестают быть двумя списками, которые сверяет человек ПО ПОЗИЦИИ. Взят по решению владельца 20.08 (`D39.153` п.6а, уточнение `D39.154`), носитель — `BACKLOG.md` П-19. ⚠ Довод «гейт `sqlgate_test.go` это уже закрыл» ПРОВЕРЕН и не подтвердился: гейт получает только СТРОКУ SQL и Go-сторону вызова не видит — шесть посаженных перестановок целей `Scan` и сломанных арностей ВЫЖИЛИ на полной батарее (замер 29.08, `platform-PROGRESS.md`). Схему читает ТОТ ЖЕ каталог `migrations/`, что гейтит манифест (второго носителя нет); деньги держит подстановочный override `*.*_micro_usd` → `money.MicroUSD` — без него генератор выдаёт голый `int64`, с ним снятие override становится ОШИБКОЙ СБОРКИ. Актуальность генерации гейчена дважды: `sqlc diff` в `make check` и `pgstore.TestEveryGeneratedQueryMatchesItsSourceFile` в батарее, который работает и без установленного sqlc | | Уязвимости | `govulncheck` **v1.6.0** | 09.07.2026 | Отдельная цель `make vuln`, не часть `check`: ей нужна сеть, а батарея обязана быть зелёной на голом клоне офлайн | -**Redis нет** — архитектурное «нет» из §5 в силе: очередь, лизы и рейт-лимиты живут в том же Postgres. +**Redis нет** — архитектурное «нет» в силе, с доводом: иначе он приползёт по частям. Дом решения — +`platform/README.md` (греп `Redis не заводим нигде`); очередь, лизы и рейт-лимиты живут в том же Postgres. ## Что решено этой сессией (сверх §5) @@ -70,7 +71,8 @@ > `users_email_key`, SQLSTATE 23505). Принято как цена правила. 9. **Ключ личности — `(provider, subject)`; почта не ключ.** `users.email` стала NULLABLE и БЕЗ уникального индекса; неизвестная пара всегда создаёт НОВЫЙ аккаунт. Разбор и цена решения — - в журнале зоны, раздел «Политика коллизии почты». + `platform/docs/archive/platform-PROGRESS-P0-P3.md:496`=`### Политика коллизии почты` (раздел закрыт + вместе с эрой P0–P3, в живом журнале его нет). 10. **Админ-поверхность — CLI (`tmplatformctl`), не HTTP-ручка.** Ручке понадобилась бы вторая модель авторизации (роли, эскалация, отзыв админской куки) ради пяти операций (`grant` · `adjust` · `balance` · `logins` · `revoke`), тогда как diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index db3fe7d5..7dc09e65 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -4,6 +4,17 @@ > вопросы, предложения на ратификацию. В `docs/PROGRESS.md` платформа не пишет; оркестратор > читает этот журнал при каждом лендинге зоны (свип «решений владельца» — норма D39.99 п.4). +> ⚠ **ДОФИКС ДОКС-РЕЗА, 02.09 — правка ЧУЖОЙ зоны по прямой санкции владельца 01.09** (сессия +> дофикса по вердикту независимого контролёра пакета `bd2077b..HEAD`). **Тронуто, только текст:** +> этот журнал · `DEFECT_REGISTER.md` (PD-203, PD-122) · `STACK_DECISIONS.md` (§«Redis нет», §9) · +> `PLATFORM_DIRECTION.md` (§1 п.2, границы пробы sqlc) · `BACKLOG.md` (строка П-5) · `README.md` +> (довод «Redis не заводим нигде»). Существо: возвращены два решения пака `sqlc`, снятые резом +> (шов `lockBook` и граница набора на `events.go`), и починены места, где текст после реза +> УТВЕРЖДАЛ про код неверное — главное из них строка П-5 бэклога, звавшая `GET /usage` непостроенной +> при смонтированном маршруте. **НЕ тронуто:** код зоны, миграции, тесты, `ENGINEERING_STANDARDS.md`, +> архив-слайсы и `platform/docs/p8-review/`, `platform/docs/p9/`. Гейты после правки: +> `python3 docs/scripts/counts.py --lint` — без ✗ по зоне, `--check` — литералы сходятся. + ## ПИНГ оркестратора №21 → зоне (31.08, СРОЧНО): ваша копия полосы отказов обещает то, что движок ОТОЗВАЛ @@ -228,7 +239,8 @@ reads as coverage» — и сам её не исполняет до конца: продовые пути: один прогон читал **4/4, затем 2/2** на своих же двух попытках при неизменной `structure_version`. Канон держит полосу одной монотонной дробью (строка 200). **Откачено:** полоса вернулась на монотонный `books.edit_wave`, на эпоху уехал только пожизненный счёт книги — ровно то, что я -писал тебе в пинге и в записке-плане и от чего отступил по ходу. Цена названа: вторая половина `PD-403` +писал тебе в пинге и от чего отступил по ходу (записка-план пака в этом файле не живёт — снята как +исполненная, греп `Записка-план пака`). Цена названа: вторая половина `PD-403` (полоса не доходит до единицы на деплое без редактора) НЕ закрыта, заведена `PD-435` с разбором, почему это не однострочник. Обратная сторона запинена: `TestTheRunsBarIsMonotoneAcrossAShapeBoundaryItSpans`. @@ -412,6 +424,9 @@ cgroup. Сам `tm-runs.slice` лежит глубже, чем его ищут: ### Три поправки к собственной записке-плану пака — обнаружились ПОСЛЕ неё (сама записка снята как исполненная; тело — D39.172 и коммит лендинга) +⚠ Два решения записки D39.172 НЕ несёт, и они живут здесь же: шов `lockBook` (§3.3, ниже под «Что +построено») и граница набора на `events.go` (§3.1, сразу под поправкой 3). + 1. **Опасных операторов не три, а ЧЕТЫРЕ.** Мой метод — покрытие — структурно не видит четвёртый, и нашёл его адверсариальный агент МУТАЦИЕЙ: `Touch` (`sessions.go:53`) выбрасывает `RowsAffected`, поэтому «оператор исполнился» — это всё, что покрытие о нём когда-либо докажет. Замер агента: `where @@ -428,12 +443,28 @@ cgroup. Сам `tm-runs.slice` лежит глубже, чем его ищут: носитель чужой схемы, чего пак обещал не делать. Резать запрос надвое нельзя: комментарий над `Observations` требует читать все восемь чисел ОДНИМ оператором. **Следствие честное: худший случай позиционного дрейфа во всём наборе — семь `int64` подряд — остаётся неконвертированным**, и посадка - M1 на нём выживет и после пака. + M1 (`platform/internal/pgstore/observe.go:73`=`Scan(&o.QueueDepth`: переставлены соседние + `QuarantinedAttempts` ↔ `LiveRuns`, два `int64`; батарея её НЕ ловит) на нём выживет и после пака. 3. **Конвертировано 40 запросов, не 42.** 41 = столько РАЗНЫХ текстов SQL в наборе (`read` в `idempotency.go` исполнялся из ДВУХ мест), минус `observe.go` = 40. Гейт `sqlgate_test.go` считает МЕСТА вызова, поэтому его число 173 → **172**: два места одного текста стали одним генерённым методом. Ни один оператор из-под гейта не выпал. +**Метод §3.1 — покрытие живым прогоном, затем МУТАЦИИ; команда названа:** полная батарея зоны с +`-coverpkg=./internal/pgstore/` при всех четырёх поднятых гейтах (18 пакетов, exit 0, скипов 0), +профиль слит по правилу «блок покрыт, если покрыт хоть в одной секции» — без слияния счёт врёт. Шесть +посадок поверх этого покрытия выжили все шесть; их предмет и адреса — вход промта пака, +`docs/archive/prompts/PLATFORM_SQLC_PACK_SESSION_PROMPT_2026-08-29.md:58-59`=`DeleteOldLoginEvents` +(там же `OpenReservations`, `ReleaseUnspawned`, `UserByIdentity`). Урок метода: покрытая строка `Scan` +ещё не значит проверенная цель `Scan`. + +⚠ **Ещё одно расхождение, которого промт не знал:** `events.go` (5 операторов) в набор не входит и +входить не должен — `ReadStream` собирается из общей константы-фрагмента `nothingIsRunning`, то есть +это склейка (`platform/internal/pgstore/events.go:126`=`nothingIsRunning`; символы `ReadStream`, +`nothingIsRunning`). Но 4 из 5 его операторов — цельные литералы. Файл приехал +ПОСЛЕ того, как пак P8-FIX назвал границу набора. Границу САМ не расширяю (это чужое решение), но +называю: **носитель границы устарел на один файл.** + ### Что построено `sqlc.yaml` · `internal/pgstore/queries/*.sql` (4 файла, 40 запросов) · генерённые `*.sql.go` + `db.go` + @@ -444,6 +475,17 @@ cgroup. Сам `tm-runs.slice` лежит глубже, чем его ищут: Публичные сигнатуры `Store` и контракты ошибок не менялись — генерируются SQL и `Scan`, поверх них рукописная проекция в доменные типы. +**`lockBook` — шов §3.3, решение: КОНВЕРТИРУЮ** (промт требовал решить ЯВНО; в D39.172 этого довода +нет, поэтому он живёт здесь). Довод: `lockBook` стоит на границе ⛔-зоны только по СПИСКУ ЗВАВШИХ +(символ `lockBook`, среди звавших — `internal/pgstore/readmodel.go` и `internal/pgstore/books.go`), +а сам по себе — цельный литерал `select id from books where id = $1 for update` внутри чужой +транзакции. Запрет ⛔ адресован СБОРКЕ запросов read-модели, а не всякому запросу, до которого +read-модель дотягивается: конвертация меняет способ исполнения одного оператора и не трогает ни одну +склейку. Транзакцию он продолжит ПРИНИМАТЬ, а не открывать, что и проверяется отдельно — сегодня это +видно грепом: `platform/internal/pgstore/credits.go:468`=`func lockBook(ctx context.Context, tx pgx.Tx` +берёт чужую `tx` и зовёт `New(tx)`, а сам оператор уехал в +`platform/internal/pgstore/queries/credits.sql:115`=`select id from books where id = sqlc.arg(id) for update;`. + ### Развилки §3.4 — обе решены прогоном - **Каталог тот же.** С генерённым файлом гейт напечатал 174 против 173 и СПЛАНИРОВАЛ генерённый @@ -649,7 +691,8 @@ $0.005460). Накладные масштабируются КНИГОЙ, а н сняты как исполненные: кадр `session_ended` стоит в каноне (`openapi.yaml`, греп `session_ended`), абзац политики отзыва — в `STACK_DECISIONS.md` §13 с обоими замеренными числами, пинг фронту — в `frontend/docs/frontend-PROGRESS.md` (греп `session_ended`), а тринадцать предложенных строк заведены -в `DEFECT_REGISTER.md` номерами `PD-414`…`PD-428`. +в `DEFECT_REGISTER.md` номерами `PD-414`…`PD-426` (`PD-427`/`PD-428` — не этого пака, они приехали +с лендинга `6ec9f8a`). ### Числа сдачи (§4.1) @@ -838,6 +881,10 @@ $0.005460). Накладные масштабируются КНИГОЙ, а н 4. **`abandonSettlement` закрывал только ОДИН осиротевший холд**, а `settled_at` штамповал за весь прогон и CLI печатал «холд возвращён целиком». Теперь цикл по всем, `settled_at` — только когда не осталось ни одного; гонку со свипом (`ErrNoReservation`) терпит, как терпит живая ветка. + ⚠ «Чужой холд» при этом невозможен ПО ПОСТРОЕНИЮ: всё ключуется `runID#attemptNo` + (`platform/internal/pgstore/runs.go:173`=`fmt.Sprintf("%s#%d", runID, attempt)`) и идёт через + `closeReservation`+`releaseHold` (символы в `platform/internal/pgstore/credits.go`), которые + сверяют владельца. 5. **Дефект, который пак внёс в ЧУЖОЙ гейт и который поймала собственная мутационная обвязка:** `usedException` в `sqlgate_test.go` был пакетного уровня, а мой новый гейт зовёт `collectSQL` вторым — счётчик, общий на два вызова, читал первый визит второго как второй визит первого. @@ -1007,7 +1054,8 @@ HTTP-старт → настоящий спавн → настоящий вых 10. **⛔ Я убила чужие процессы** `pkill -f 'go test'` / `pkill -9 -f '/exe/'`, гася свою мутационную кампанию: шесть ревью-агентов сессии `textmachine-e4` в окне **01:36:20–01:37:45**, включая линзу покрытия посадками — для неё убитый прогон читается как «мутация выжила», то есть я могла - подсунуть ей ложную НАХОДКУ. Сообщила ей сама с точными границами, она пере-прогнала; PID заданий + подсунуть ей ложную НАХОДКУ. Сообщила ей сама с точными границами, она пере-прогоняет (подтверждения, что пере-прогон + закончился, у меня нет — так и записано); PID заданий теперь пишутся в файл и гасятся построчно. Общий урок — в списке уроков `docs/PROGRESS.md` (греп «`pkill` по имени процесса») и в `STACK_DECISIONS` (греп «Демон нельзя убивать»). 11. **Не проверено вообще:** поведение при нескольких одновременных потоках одного пользователя под @@ -1099,6 +1147,12 @@ FAIL/DATA RACE — 0; `golangci-lint` → **0 issues**; деплоя (byte no-op проставит факт по already_applied-ветке). - Консент-гейт движка живьём деньгами по-прежнему не пробит ($0-цены; кандидат строки 202) — из прежнего Obstacle, не изменилось. +- **Идемпотентный ключ повторного `POST /runs {re_pass}`** — не строился (как и у обычного Start + вне идемпотентности ключа запроса); повтор после успеха отвечает `run_in_flight` либо + `ErrRePassUnavailable` — факт погашен финишем (символ `ErrRePassUnavailable`: + `platform/internal/runs/runs.go:142`=`var ErrRePassUnavailable = errors.New(`; ветка провода — + `platform/internal/httpapi/v0.go:824`=`case errors.Is(err, runs.ErrRePassUnavailable):`). + Вырожденных дублей не нашёл, но специального пина нет. - ⚠ **Для оркестратора — находка опровергателя P10, носителя ни в регистре, ни в бэклоге у неё нет:** якоря §2.12 компаньона контракта (`docs/architecture/14-api-contract/README.md`, греп `pipeline/status.go`) ПРОТУХЛИ — волновая машинерия **D39.122** увезла деньги в `ChapterPassport` и @@ -1226,7 +1280,10 @@ D39.162, держать её как находку нельзя · Р5 → F5 · строку **186** приёмка закрыть ОТКАЗАЛАСЬ — замер подтверждает шаги 1-2, шаги 3-5 живы (D39.162), и сам замер стоит в строке. -- cosmetic: `MkdirAll(StateDir)` на каждый вызов двери — место одному разу в конструкторе Service. +### Ось «стоимость» — остаток без своей строки + +- cosmetic: `MkdirAll(StateDir)` на каждый вызов двери — место одному разу в конструкторе Service + (`platform/internal/runs/bank.go:361`=`os.MkdirAll(s.Cfg.StateDir`). **Две движковые находки БЕЗ носителя — это и есть живой остаток секции** (чужая зона, лечить не мне; пинг оркестратору): @@ -1468,8 +1525,10 @@ mutation-catch (спутать пары «числитель×база»); ко **Числа сдачи зоны — провенанс, другого носителя у них нет** (ни в `D39.159`, ни в `platform/docs/p8-review/`, где лежит только сырьё): посадок мутаций **16** своих, все воспроизводимы (`plant.py` знает `M1`–`M16` поимённо, логи рядом) — **7 поймано, 9 выжило**; агентских **42** по их -таблицам (`agent-mutations.md`) — **21/21**. Уникальных переживших инвариантов **15**, каждый несёт -строку регистра. +таблицам (`agent-mutations.md`) — **21/21**. ⚠ Оговорка провенанса, снимать её нельзя: сырые логи 42 +агентских посадок не сохранены, и **из 21 «пойман» конкретный тест называют 19, а 2 — нет**; их +вердикты пере-ранить нельзя, только пере-посадить по описанию. Уникальных переживших инвариантов +**15**, каждый несёт строку регистра. ⚠ **Метод-урок той же сдачи, и место ему не здесь:** обрезанный по ширине вывод грепа читается как ОТСУТСТВИЕ совпадения — так зона объявила «грепа в промте ноль» на строке, которую греп нашёл. Дом урока — `platform/docs/ENGINEERING_STANDARDS.md` (дисциплина доказательства грепом); перенос за тем, @@ -1481,7 +1540,7 @@ mutation-catch (спутать пары «числитель×база»); ко ## Текущее состояние -**⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ ПЛАТФОРМЫ.** Из трёх находок пинга живой осталась ОДНА, она ниже. Две закрыты паком P9 и проверяются грепом, а не памятью: блокер провайдерских ключей (строка **211** единого бэклога) — ключи едут аргументом `--keys-file`, греп `TM_PLATFORM_ENGINE_KEYS_PATH`; дубль движковой конвенции пути (строка **213**) — `runner.projectDB` снесён, путь берётся из `artifacts.bank_export` манифеста. Четвёртая находка пинга (мёртвое поле `ingest.StatusReport.UnsignedBankTerms`) жива и несёт свой якорь строкой `PD-396`. +**⛔ ПИНГ ОРКЕСТРАТОРА №19 — 23.08, ЗОНЕ ПЛАТФОРМЫ.** ⚠ Его собственная шапка звала находки «тремя», а нумеровала ЧЕТЫРЕ — считать по нумерации. Живых из четырёх ДВЕ: находка 1 ниже дословно, находка 4 — строкой регистра. Две закрыты паком P9 и проверяются грепом, а не памятью: блокер провайдерских ключей (строка **211** единого бэклога) — ключи едут аргументом `--keys-file`, греп `TM_PLATFORM_ENGINE_KEYS_PATH`; дубль движковой конвенции пути (строка **213**) — `runner.projectDB` снесён, путь берётся из `artifacts.bank_export` манифеста. Находка 4 (мёртвое поле `ingest.StatusReport.UnsignedBankTerms`) жива и несёт свой якорь строкой `PD-396` (греп `UnsignedBankTerms` в регистре и в `platform/internal/ingest/resync.go`). 1. **Пере-нарезка книги сносит ВСЕ решения юнитов и пересчитывает прогресс из пустоты.** `internal/pgstore/readmodel.go:127-137`=`a re-cut drops the resolutions of the previous cut` — @@ -1682,7 +1741,7 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц ИСПОЛНЕН: реестр разложен на секции (открытые по весу · принятый риск · закрытые по эрам паков) при сохранённой построчной форме `| PD-N | … |`, на которой ключуется `docs/scripts/counts.py`. Заказ и -его причина — D39.125. +его причина — **D39.126 §3** («строка 167 пере-скоуплена в остаток: … DEFECT_REGISTER секциями»), не D39.125. ## Пинг оркестратора №17 — 15.08.2026 (migrate принят, D39.134 — движковая половина самолечения ФИНАЛЬНА) @@ -1691,8 +1750,12 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц бинаря — то есть движок под ногами больше не сдвинется, и `PD-201` («поймал 13 → migrate → повтор») можно строить; строка регистра открыта и несёт предмет. Три хвоста зоны, найденные той же приёмкой (мёртвая цитата текста ошибки схемы в деплой-рантбуке и в строке П-1 бэклога; образец в -`tmplatformctl`, зовущий голый `tmctl` из PATH), с тех пор закрыты правками — рантбук цитату сам -называет мёртвой, образец несёт версионированный путь. +`tmplatformctl`, зовущий голый `tmctl` из PATH), с тех пор закрыты правками, и это проверяется грепом: +мёртвой цитаты текста ошибки схемы («schema vN … expects vM») нет ни в рантбуке, ни в бэклоге зоны +(`grep -rn 'expects vM' platform/deploy platform/BACKLOG.md` — ноль строк), рантбук несёт +живой токен (`platform/deploy/README.md:187`=`schema_mismatch found=N expected=M`), строка П-1 бэклога +закрыта и адресует рантбук, а образец несёт версионированный путь +(`platform/cmd/tmplatformctl/runs.go:52`=`The VERSIONED path and never a bare`). ## Пинг оркестратора №17 (второй) — 15.08.2026 (S4 принят, контракт 0.2.3 в каноне — пять строк вашего регистра протухли; аудит корпуса) @@ -1737,7 +1800,7 @@ D39.179 п.1 ратифицировал: DeepSeek пере-пинен под ц Пропуски в нумерации — снятые пункты, чьё исполнение проверяемо и чей предмет живёт по адресу: **п.1** `ContractVersion` (поднята F13 до 0.6.0, канон сегодня 0.9.0) · **п.2** пины на `structure_version`/`revision` в кадре (стоят с названной пойманной мутацией в `pgstore/events_test.go`) · -**п.3** бюджет попыток материализации (`PD-357`, закрыт эрой P8-FIX) · **п.5** round-trip в +**п.3** бюджет попыток материализации (`PD-330`, закрыт эрой P8-FIX) · **п.5** round-trip в `SaveStructure` (`PD-297`, открытая строка несёт предмет) · **п.7** мусор `platform/ru` (снесён оркестратором тем же заходом) · **п.8** ревью-пак четырёх осей (отработан паком P8-REVIEW, D39.159) · **п.10** пол на пустой манифест (`readmodel.refreshStructure` сверяет `ChaptersTotal`, тест есть) ·