From 954c034f53ac4c9548c1c55ed9725073ae02c86a Mon Sep 17 00:00:00 2001 From: heaven Date: Wed, 5 Aug 2026 00:00:04 +0300 Subject: [PATCH] Accept and ratify platform P0 with corrections: defect and vulnerability register, zone engineering standards, backlog rows for OAuth and credits, stale doc pointers fixed --- docs/architecture/14-api-contract/README.md | 5 +- platform/BACKLOG.md | 2 + platform/README.md | 4 +- platform/docs/DEFECT_REGISTER.md | 29 +++++++ platform/docs/ENGINEERING_STANDARDS.md | 60 +++++++++++++++ platform/docs/platform-PROGRESS.md | 84 +++++++++++++++++++++ 6 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 platform/docs/DEFECT_REGISTER.md create mode 100644 platform/docs/ENGINEERING_STANDARDS.md diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md index 6415eaa0..11d38fbf 100644 --- a/docs/architecture/14-api-contract/README.md +++ b/docs/architecture/14-api-contract/README.md @@ -12,7 +12,8 @@ openapi.yaml РЯДОМ (байт-копия зонной frontend/docs/api-cont Провенанс-классы ✓/◆/○ ниже выборочно перепроверены кодом движка — легенда честная. ОТКРЫТЫ (см. §4): продуктовые К-1/К-3/К-5/К-6/К-8 — владелец; К-4/К-7/К-12 — -платформа (П-1, кода ещё нет); К-2/К-9/К-10/К-11 — автор контракта/бэкенд. +платформа — ОТВЕЧЕНЫ P0 и ратифицированы приёмкой 04.08 с поправками (носитель — зонный журнал +`platform/docs/platform-PROGRESS.md`, раздел «Ратификация приёмкой»); К-2/К-9/К-10/К-11 — автор контракта/бэкенд. Зависимости §3 покрыты строками единого бэклога: 99/100/101/103 (перевешены в БЛОКЕРЫ контракта) + новые 125 (банк-экспорт) · 126 (поднятие потолка) · 127 (DDL-коммент). Приложение А — заготовка: фразы и ступени заполняет владелец (В-3). @@ -32,7 +33,7 @@ openapi.yaml РЯДОМ (байт-копия зонной frontend/docs/api-cont # Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы -> **Нормативная поверхность контракта — [`api-contract/openapi.yaml`](api-contract/openapi.yaml)** +> **Нормативная поверхность контракта — [`openapi.yaml`](openapi.yaml)** (файл рядом, в этой же папке) > (OpenAPI 3.1). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято > каждое решение, чем оно обосновано, что осталось открытым. При расхождении по ФОРМЕ > побеждает YAML; при вопросе «почему так» — этот файл. diff --git a/platform/BACKLOG.md b/platform/BACKLOG.md index a4bc09cb..44dbd007 100644 --- a/platform/BACKLOG.md +++ b/platform/BACKLOG.md @@ -12,4 +12,6 @@ | П-2 | **Глобальный брокер рейт-лимитов провайдеров** (экс-строка 97): гарды движка per-процесс (`pipeline/ratelimit.go:11`, mistral ~48% отказов под параллелизмом), а лимит провайдера — на ВЕСЬ аккаунт: N прогонов = N независимых гардов против общего лимита; воркер отпрашивается у платформы перед вызовом | ДО второго параллельного пользователя | D39.81, D39.84 | | П-3 | **Очередь и конкурентность по книге**: River на том же Postgres, сериализация по `book_id` + пиннинг книги к одному хосту на MVP, лизы `book_leases` с heartbeat — вежливое ожидание вместо аварии | Ф3-пак платформы | STACK_DECISIONS §5, D39.84 | | П-4 | **Учёт токенов/денег per-user + бюджет-гейт ДО старта задачи** (сырьё уже считает движок: `request_log` + `internal/ledger`; помнить: леджер = нижняя граница — строка 78 единого) | Ф3-пак платформы | platform/README, D39.84 | +| П-6 | **OAuth-вход популярных провайдеров (Google первым) + модель аккаунта** (направление владельца 04.08): authorization code + PKCE, валидация ID-токена, связывание аккаунта по ПОДТВЕРЖДЁННОЙ почте; собственная серверная сессия сохраняется (D39.84) — OAuth даёт только СОБЫТИЕ входа. Форма и библиотеки — по ресёрч-пакету направления (ратифицирует оркестратор); писатель куки с `__Host`-атрибутами и dev-профиль — здесь же (PD-8) | P1, вместе с П-1 | владелец 04.08 | +| П-7 | **Фри-тир + покупка токенов «как у OpenRouter»** (направление владельца 04.08): кредитный леджер в целых микро-долларах, идемпотентные списания, резервация ДО старта прогона, окна фри-тира; продажа кредитов — через провайдера с минимальной комплаенс-поверхностью (merchant-of-record vs Stripe — решение по ресёрчу). ⚠ Гейт: пер-пользовательский учёт отстаёт на попытку, пока не решён открытый канон-вопрос об источнике денег во время попытки (журнал зоны, «Ратификация приёмкой») | P1+, после П-6 | владелец 04.08 | | П-5 | **API лимитов/использования + оповещение стопа по потолку** (решение владельца 04.08, D39.100/ПТ-35, механизм «как Claude Code»): страница лимитов в настройках читает СТАТУС использования (не суммы — деньги на провод не идут, D39.84); стоп по потолку → статус `paused` + оповещение «перевод остановлен: лимиты исчерпаны»; сырьё у движка есть (`request_log`/`ledger`, потолки конфига), форму API предлагает P0 | Ф3, вместе с П-1 | D39.100, ПТ-35, контракт К-8 | diff --git a/platform/README.md b/platform/README.md index aab12912..0d5d8a84 100644 --- a/platform/README.md +++ b/platform/README.md @@ -10,7 +10,7 @@ ## ⚠ Git и зона (читать ДО первой строки кода) -**Платформенная сессия не коммитит — лендит оркестратор.** Канон — `../CLAUDE.md`, §Гардрейлы; сюда вынесено, потому что отдельного промта платформы ещё нет. +**Платформенная сессия не коммитит — лендит оркестратор.** Канон — `../CLAUDE.md`, §Гардрейлы; продублировано здесь, потому что зона живёт своим онбордингом. Стандарты и критерии приёмки зоны — `docs/ENGINEERING_STANDARDS.md`; дефекты и уязвимости — `docs/DEFECT_REGISTER.md` (каждая находка получает строку ДО закрытия). 1. **Писать только внутрь `platform/`.** Ничего за её пределами — ни `docs/`, ни `backend/`, ни `frontend/`, ни корневых файлов. Нужна правка вне зоны — пинг владельцу, её сделает оркестратор. 2. **Чужие незакоммиченные файлы в дереве не трогать**: параллельные сессии — норма. @@ -28,7 +28,7 @@ - учёт токенов и денег **на пользователя** (сырьё уже считает движок: `request_log` + `internal/ledger`), потолки и гейт бюджета ДО старта задачи; - очередь задач и запуск воркеров, статусы прогонов, ретраи; -- SSE-поток прогресса и живой стоимости во фронт. +- SSE-поток прогресса во фронт (⚠ денежные суммы на провод и на экран НЕ идут — D39.84; пользователь видит СТАТУС использования: процент и время сброса, П-5). ## Чего здесь НЕ будет diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md new file mode 100644 index 00000000..a059db3d --- /dev/null +++ b/platform/docs/DEFECT_REGISTER.md @@ -0,0 +1,29 @@ +# Регистр дефектов и уязвимостей платформы + +> Заведён по решению владельца 04.08 («отдельно ведётся колонка багов и уязвимостей — тут опасно всё»). +> Правила: каждая находка любой сессии/приёмки/аудита — строкой сюда ДО закрытия; ID стабилен навсегда; +> закрытие — только с коммитом фикса и тестом, пинящим свойство (урок PD-1: свойство без пинящего теста +> считается НЕ закрытым). Класс: `vuln` — эксплуатируемо или ослабляет защиту · `bug` — неверное поведение · +> `hardening` — защита в глубину / латентное. Статус: `open` · `fixed()` · `accepted-risk(<кем, когда>)`. + +| ID | Класс | Серьёзность | Где | Суть | Статус | Источник | +|---|---|---|---|---|---|---| +| PD-1 | hardening | minor | `internal/pgstore/pg_test.go:89` | Свойство «в БД только SHA-256, не токен» НЕ запинено тестом: посадка «`Digest` возвращает плейнтекст» выживает — тест сверяет хранимое через тот же `auth.Digest` (self-consistent). Нужен тест с НЕЗАВИСИМО вычисленным хешом либо ассерт «плейнтекст в БД не находится» | open | приёмка P0 (посадка №1) | +| PD-2 | vuln | **major, ЖИВАЯ (не латентная)** | `cmd/tmplatformd/main.go:73-82` | Нет `ReadTimeout` ⇒ соединения пиннятся уже СЕГОДНЯ, без единой body-принимающей ручки: `net/http` дренирует непрочитанное тело <256 КБ ВНУТРИ `chunkWriter.writeHeader` до отправки заголовка ответа (`net/http/server.go:1389-1435`), и этот чтение-шаг наследует отсутствующий дедлайн. **Репродуцировано оркестратором на собранном бинаре:** 50 полу-кормленных POST на охраняемый `/v0/*` → сервер отработал и залогировал 50×401 `ms:0`, клиенты получили НОЛЬ байт, fd 7→57 и держались, пока не закрыл КЛИЕНТ (агент-скептик независимо пинил 500 соединений). Ограничителя соединений и документированного edge-прокси в зоне нет. Фикс — одна строка (`ReadTimeout`; для будущего SSE — per-conn дедлайны через `ResponseController`). Вторая половина (`MaxBytesReader`) сегодня не эксплуатируема (ни один хендлер не читает body) — гейт P1: закрыть ДО первого POST-хендлера | open | приёмка P0 (security-линза + скептик + собственная репродукция) | +| PD-3 | bug | minor | `internal/httpapi/middleware.go:57` | `Recover` логирует сырой `r.URL.Path` на ERROR — id книг/прогонов утекают в лог, против собственной дисциплины AccessLog (route-pattern, не путь) | open | приёмка P0 (security-линза) | +| PD-4 | hardening | minor | `internal/pgstore/sessions.go:41` | WHERE у `Touch` слабее, чем у `Lookup` (нет `idle_expires_at > now`): прямой вызов воскресил бы idle-истёкшую сессию. Через `Require` недостижимо (Touch только после успешного Lookup) — одна строка защиты в глубину | open | приёмка P0 (security-линза) | +| PD-5 | bug | minor | `internal/auth/middleware.go:36,45` | Ошибки стора невидимы: сбойный `Lookup` → 401 без единой строки лога (аутентификационный DB-outage выглядит как шторм 401), `Touch` глотается `_ =`. На проводе различать нельзя (оракул) — но лог обязан различать | open | приёмка P0 (security+blind линзы) | +| PD-6 | hardening | info | `internal/auth/csrf.go:51` | GET освобождён от CSRF (верно), но SSE-хендшейк — GET с амбиентной кукой: origin-чек хендшейка потока (STACK §5) не покрыт ничем. Закрыть при постройке SSE (P1) | open | приёмка P0 (security-линза) | +| PD-7 | bug | info | `internal/pgstore/sessions.go:78` | `DeleteExpiredSessions` никем не вызывается — свип запланировать в P1 (периодическая джоба воркера) | open | приёмка P0 | +| PD-8 | hardening | info | `internal/auth/session.go:18` | Писателя куки ещё нет; `__Host-` требует Secure ⇒ локальный dev по HTTP куку не поставит. Решить формой в P1 (dev-профиль), префикс не ослаблять в проде | open | приёмка P0 | +| PD-9 | bug | minor | `cmd/tmplatformd/main.go:81` | `BaseContext` возвращает signal-контекст ⇒ SIGTERM мгновенно рубит контексты ВСЕХ in-flight запросов, и 15-секундный дренаж `Shutdown` мёртв для ctx-aware хендлеров. Fix: BaseContext без signal-ctx; сигнал ведёт только Shutdown | open | приёмка P0 (faults-линза) | +| PD-10 | bug | minor | `internal/ingest/decoder.go:42,61,67` | Три ужесточения декодера: (а) `hello` с пустым `engine_run_id` принимается — а это половина ключа идемпотентности; (б) seq самого hello не пинится к 1 — потеря пре-хендшейковых строк недетектируема; (в) mid-stream `hello` (любой версии, вкл. мажор 9.9) уходит в Sink как обычное событие — version-гейт держит только строку 1 | open | приёмка P0 (faults-линза) | +| PD-11 | bug | minor | `internal/pgstore/migrations/00002_readmodel.sql:137,177` | Неиндексированные FK-каскады: `notes.chapter_id` и `bank_decisions.term_id` — каскадное удаление сканирует таблицы | open | приёмка P0 (faults-линза) | +| PD-12 | bug | info | `internal/ingest/supervisor.go:82-84` | Сбой Sink в начале прогона ⇒ платформа дренирует ВЕСЬ оставшийся поток в `io.Discard` часами: ceiling/bank_stop-события выбрасываются, никто не оповещён. Нужна политика «БД платформы упала посреди прогона» (ретраи синка / деградация с алармом) — дизайн-вопрос P1 | open | приёмка P0 (faults-линза) | +| PD-13 | bug | info | `internal/ingest/supervisor.go:64` | Краш платформы осиротляет процесс движка: ни process-group, ни pidfile, ни пути реаттача (поток невосстановим, повторный спавн упрётся в EXCLUSIVE-лок). Дизайн супервизии P1 | open | приёмка P0 (faults-линза) | +| PD-14 | hardening | info | `internal/httpapi/server.go:79` | `readyz`: ping без собственного таймаута (WriteTimeout нет намеренно — SSE), эндпоинт неаутентифицирован и без rate-limit — задушить дешёво; таймаут на ping + прикрыть на ops-слое | open | приёмка P0 | +| PD-15 | bug | info | `internal/ingest/resync.go:32` | Деньги в ре-синке — float64, а `usage_windows` хранит micro-USD именно против дрейфа: дрейф входит шагом раньше (JSON-парс + суммирование дельт). Принять осознанно или считать в целых | open | приёмка P0 (faults-линза) | +| PD-16 | bug | minor | `internal/httpapi/server.go:81` | `readyz` глотает ошибку ping вопреки собственному комменту «the reason stays in the log» — лога нет | open | приёмка P0 (blind-линза) | +| PD-17 | bug | minor | `Makefile:41-42` | Баннер «did NOT run (no database)» печатается и при ПРОГНАННЫХ БД-тестах (безусловный); батарея гоняет сьют дважды ради имён скипов (второй прогон без -race) | open | приёмка P0 (blind-линза) | +| PD-18 | bug | info | `internal/pgstore/migrations/00002_readmodel.sql:9,139` | Коммент шапки «engine vocabulary never crosses this seam» противоречит `notes.reason` (движковая причина хранится, не проецируется); коммент переписать честно | open | приёмка P0 (canon-линза) | +| PD-19 | bug | info | `internal/ingest/resync.go:44` | `WorstFlagReason` задокументирован «stored», а колонки в `chapters` нет — доккоммент или схема, одно из двух | open | приёмка P0 (canon-линза) | diff --git a/platform/docs/ENGINEERING_STANDARDS.md b/platform/docs/ENGINEERING_STANDARDS.md new file mode 100644 index 00000000..8c50d191 --- /dev/null +++ b/platform/docs/ENGINEERING_STANDARDS.md @@ -0,0 +1,60 @@ +# Стандарты разработки и критерии приёмки платформы + +> Ратифицировано оркестратором №14 по решению владельца 04.08: платформа считает токены пользователей +> и держит их логины — «ошибки недопустимы»; качество и отказоустойчивость выше скорости фич. +> Каждый промт платформенной сессии обязан ссылаться на этот документ; отступление = пинг, не тихая девиация. + +## 1. Рамка + +- **Несущие пути** — auth/сессии/CSRF, всё денежное (usage, лимиты, биллинг), ингест/супервизия движка, + миграции. Изменение несущего пути = «тяжёлый» класс: полный цикл приёмки §3, без исключений. +- **Сторонние тулы прежде самописного** (решение владельца 04.08): проверенные библиотеки предпочитаются + своему коду везде, КРОМЕ похода в ядро переводов (tmctl зовётся процессом, обвязка своя) и мест, где + stdlib и есть индустриальный стандарт (net/http, database/sql-слой через pgx). Новая зависимость — + строка в `STACK_DECISIONS.md` с датой релиза и «зачем», ратифицирует оркестратор. +- Канон-инварианты зоны без изменений: D39.84 (никаких денежных сумм на проводе/UI/INFO-логах) · + D39.85 (шов: NDJSON → апсерт `(engine_run_id, seq)` → SSE из Postgres; живой SQLite движка не читать) · + ПТ-34 (noindex/no-store на всём) · языко-агностичность. + +## 2. Базовые стандарты (проверяются каждой приёмкой) + +**Безопасность** (направление — OWASP ASVS, без карго-культа): +- Сессии: одна серверная, opaque-токен ≥256 бит из crypto/rand, в БД только хеш, немедленный отзыв, + два срока (idle скользит, absolute нет). Плейнтекст-токен не персистится, не логируется и живёт + в памяти только окно обработки запроса (решение владельца 04.08). +- Секреты — только из окружения/секрет-менеджера; в репо и в логи не попадают НИ на каком уровне. +- Ввод: только параметризованные запросы; каждый body-принимающий хендлер несёт `http.MaxBytesReader` + и дедлайн (гейт PD-2 регистра); закрытые словари валидируются на входе И констрейнтами БД. +- Ответы: problem+json; внутренности и оракулы (unknown-vs-expired токен) на провод не текут; + заголовки ПТ-34+nosniff+no-referrer — мидлварью на всё, не дисциплиной хендлера. +- `govulncheck` (`make vuln`) — перед каждым лендингом; красный = блокер лендинга. + +**Отказоустойчивость:** +- Ингест идемпотентен (`(engine_run_id, seq)`, high-water mark в одной транзакции с эффектом); + at-least-once — норма, дубль — не ошибка. +- Graceful shutdown с реальным дренажем; пул с лимитами; транзиентные ошибки БД — ретраи с джиттером; + миграции идемпотентны, down-путь существует и гоняется тестом. +- Отказ зависимости — деградация с диагностикой, не тишина: проглоченная ошибка без лога и без + комментария-обоснования = дефект (класс PD-5/PD-16). + +**Наблюдаемость:** структурные логи (route-pattern, не сырой путь — id пользователя/книги в логи не +текут), `X-Request-Id` на каждом ответе, ошибки видимы на своём уровне. Money-числа — только в +приватных таблицах, ни в логах, ни в ответах (кроме P-5-статуса процентами). + +**Контракт-первичность:** поверхность = `docs/architecture/14-api-contract/openapi.yaml`; расхождение +кода со спекой = дефект чей-то один: либо спека правится через ратификацию, либо код. Генерация +типов/стабов из спеки (oapi-codegen — кандидат, ратификация при P1) предпочтительнее ручного дрифта. + +## 3. Критерии приёмки сессии (Definition of Done) + +1. `make check` зелёный офлайн; с `TM_PLATFORM_TEST_DSN` — та же батарея плюс живая схема + (порядок подъёма PG без root — `STACK_DECISIONS.md`); скипы названы вслух и их ноль при DSN. +2. Каждый деливерабл проверен ИСПОЛНЕНИЕМ (сервер поднят и опрошен, миграции применены дважды, + констрейнты сработали поимённо, супервизор гонял настоящий процесс) — не чтением. +3. Заявленное в отчёте свойство несущего пути ОБЯЗАНО быть запинено тестом, который ловит мутацию + этого свойства; сессия сама называет в отчёте, какой тест что пинит. Приёмка оркестратора сажает + СВОИ мутации (урок P0: «в БД только хеш» было истинно, но не запинено — посадка выжила). +4. Адверсариальная самопроверка (author≠reviewer) до сдачи; найденное — в отчёт, не молча. +5. Новые находки — строками в `DEFECT_REGISTER.md` тем же деревом; закрытие дефекта — коммит + пинящий тест. +6. Дерево не коммитится сессией — лендит оркестратор после адверсариальной приёмки + (для несущих путей — с воркфлоу-верификаторами и посадками). diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index 8961784f..de40e684 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -6,6 +6,11 @@ ## Текущее состояние +- **P0 ПРИНЯТ и ЗАЛЕНДЕН** (`eeeef89`, приёмка оркестратора №14 04.08 — раздел «Ратификация приёмкой» + ниже). Дизайн-ответы К-4/К-7/К-12/П-5 ратифицированы С ПОПРАВКАМИ. Найдено 19 дефектов, все + строками в `DEFECT_REGISTER.md`; один — ЖИВАЯ уязвимость (PD-2, пиннинг соединений), гейт P1. +- **Стандарты зоны заведены** (решение владельца 04.08): `ENGINEERING_STANDARDS.md` (критерии приёмки, + индустриальные базовые линии) + `DEFECT_REGISTER.md` (отдельная колонка багов и уязвимостей). - **P0 собран** (сессия 04.08): модуль компилируется, батарея зоны `make check` зелёная, `/healthz` и `/readyz` проверены живым запуском против живого Postgres 18.4. - Стек запинен и live-сверен — `STACK_DECISIONS.md` (зонный). @@ -25,6 +30,85 @@ заголовок `X-TM-Client` на небезопасных запросах cookie-пути. Это требование к ФРОНТУ, и его место — в описании `sessionCookie` в спеке. Реализовано и проверено тестами. +## Ратификация приёмкой (оркестратор №14, 04.08) + +**Вердикт: P0 ПРИНЯТ, заленден `eeeef89`.** Метод: батарея пере-прогнана мной (офлайн зелёная; с живым +PostgreSQL 18.4, поднятым без root, все три БД-гейченных теста зелёные — 6/6 констрейнтов сработали +поимённо) · живые пробы бинаря (healthz 200 без БД · readyz 503 честно · Bearer без стора → 401, не +паника · неизвестный путь под `/v0` → 401 раньше 404 · CSRF: cookie-POST без `X-TM-Client` 403, +cross-site 403, Bearer-POST не требует заголовка · SIGTERM → «shutting down» и чистый выход) · +**три СВОИ мутации** в несущие свойства (см. ниже) · адверсариальный воркфлоу из пяти линз +(слепое ревью до отчёта · security-охота вне карты · корректность/отказоустойчивость · канон-сверка · +опровержение дизайн-ответов) со скептик-пассом. + +**Что подтверждено исполнением:** зонная дисциплина (в коммите только `platform/*`, чужого нет) · +модуль-sibling, `backend/internal` не импортируется, `go.work` не заведён · живой SQLite движка нигде +не открывается · event-sourcing не построен (high-water mark) · деньги отсутствуют на проводе и в +INFO-логах (stderr движка уходит в ФАЙЛ попытки — верно) · ПТ-34-заголовки мидлварью на всём, не +дисциплиной хендлера · имена полей `status --json` сверены 1:1 с `pipeline/status.go:37-130` · +экзит-коды сверены с `cmd/tmctl/main.go:30-52` · языко-агностичность (пар-литералов нет) · +пин линтера идентичен движковому. + +**Мои мутации (author≠reviewer):** (1) `Digest` возвращает плейнтекст вместо SHA-256 — **тесты +ВЫЖИЛИ** ⇒ свойство «в БД только хеш» истинно, но НЕ запинено (тест сверяет через ту же функцию); +строка PD-1. (2) Снятие требования `X-TM-Client` — тест упал поимённо ✓. (3) Ослабление констрейнта +`units_translated_has_text` до `check (true)` — тест упал поимённо ✓. Дерево после мутаций +восстановлено байт-в-байт (sha256-сверка). + +**Главная находка приёмки — PD-2, ЖИВАЯ уязвимость, а не латентная.** Отсутствие `ReadTimeout` +позволяет пиннить соединения СЕГОДНЯ, без единой body-принимающей ручки: `net/http` дренирует +непрочитанное тело <256 КБ внутри `chunkWriter.writeHeader` ДО отправки заголовка ответа, и это +чтение наследует отсутствующий дедлайн. Репродуцировано мной на собранном бинаре (50 полу-кормленных +POST: сервер залогировал 50×401 `ms:0`, клиенты получили ноль байт, fd 7→57 до закрытия КЛИЕНТОМ); +скептик независимо пинил 500. Фикс — одна строка; **гейт: закрыть первым шагом P1**. + +**Ратификация дизайн-ответов — все четыре ПРИНЯТЫ, каждый с обязательной поправкой:** + +- **К-4 (ревизия пер-книжная, кадр SSE = `books.revision`) — ПРИНЯТ + три поправки.** (а) Посылка + «единственный писатель» неверна: HTTP-хендлеры тоже пишут книго-скоупное состояние (подпись банка + обязана вернуть бампнутую ревизию). Нормативный механизм — не «единственность писателя», а + **блокировка строки книги, удерживаемая до коммита** (`update books set revision = revision + 1` + сериализует и бамп, и порядок коммитов); материализатор и хендлеры обязаны ходить через неё. + (б) Одна транзакция = одна ревизия, но НЕСКОЛЬКО кадров: докачка по `Last-Event-ID` обязана читать + `revision >= R` (дельта-чтения идемпотентны), иначе теряются кадры-братья транзакции R. (в) Дельта- + чтение не выражает УДАЛЕНИЯ (банк переписывается целиком, пере-чанковка заменяет главы/юниты) ⇒ + после любой транзакции-замены сервер обязан выдать `resync_required` (событие в контракте есть). +- **К-7 (keyset-курсор на всех списках, `next_cursor` всегда) — ПРИНЯТ + две поправки.** (а) Курсор + НЕ привязывать к `books.revision`: тот бампается на каждой материализации, и на книге 5000+ глав + правило «сменилась ревизия — начни цикл заново» даёт вечный рестарт пагинации во время прогона. + Привязка — к СТРУКТУРНОЙ эпохе (поколение манифеста / `books.chunker_version`), которая меняется + только при (пере-)разборе. (б) Отклонение протухшего курсора — обязанность СЕРВЕРА (MUST), не + клиента: курсор непрозрачен, клиент не может её исполнить. Ключи сортировки для банка и замечаний + назвать при правке спеки (для глав — `(book_id, number)`). +- **К-12 (опрос с `Retry-After`, 202+`Location`) — ПРИНЯТ.** Аргумент структурный и верен: поток + привязан к прогону, экспорт делают с законченной книги. Поправка формы: `Retry-After` на 200 + стандартом не определён (RFC 9110 — 503 и 3xx), поэтому в спеке объявить его ЯВНЫМ заголовком + этого ответа, а не полагаться на общую семантику. +- **П-5 (`GET /v0/usage` статусом, `Run.paused_reason`, потолок поднимает платформа) — ПРИНЯТ + + три поправки.** Несущий клейм ПЕРЕПРОВЕРЕН мной по коду и подтверждён: `Ceilings` объявлены + (`backend/internal/config/book.go:106`), в канон `BriefHash` НЕ входят (`:280-297`, доккоммент + `:264` «wiring fields … deliberately excluded»), и ни один другой хеш их не сворачивает (снапшот + волны `pipeline/snapshot.go`, кортеж чекпойнта `stagerun.go:406-412`) ⇒ поднятие потолка не двигает + снапшот и не вызывает ре-билл. Поправки: (а) `Run.paused_reason` не имеет колонки — завести в схеме + (собственный мандат сессии: у каждого поля ответа есть источник); (б) `revision` в ответе usage не + имеет скоупа — назвать счётчик пользователя либо убрать поле; (в) формулировка «поток денег не + несёт и НЕ ДОЛЖЕН» подана как следствие D39.84 — это не так: D39.84 запрещает суммы на + ПОЛЬЗОВАТЕЛЬСКОМ проводе, экране и в INFO-логах, а внутренний поток движок→платформа в приватную + таблицу — другая поверхность. Вопрос «нести ли версионированное денежное событие в словаре строки + 103» ОТКРЫТ (см. ниже), а не закрыт. +- **Три находки сессии подтверждены и маршрутизированы:** движковый `run_id` в кадре `hello` + (иначе resume отбрасывается high-water mark'ом) — в строку 103 единого бэклога; статусы до прогона + и библиотека без живого канала — правка спеки (S3); `X-TM-Client` в спеку — туда же, с уточнением + «значение любое, несущей является ПРИСУТСТВИЕ заголовка». + +**Открытый канон-вопрос, поднятый приёмкой (нужно слово владельца/решение оркестратора): +у платформы нет санкционированного источника денег ВО ВРЕМЯ попытки.** Движок держит эксклюзивный +лок (`status --json` физически недоступен), его stderr-INFO с ценами парсить запрещено (анти-паттерн +research/23 §2 + запрет INFO-денег), а словарь строки 103 денег не несёт. Следствие: пер-пользовательские +окна отстают на целую попытку (часы), и единственный он-лайн-гард — пер-книжные потолки, которые +платформа обязана ставить консервативно. Варианты — версионированное денежное событие в словаре 103 · +консервативная политика потолков · нарезка попыток — в ресёрч-пакете направления «биллинг». + ## Дизайн-ответы на ратификацию ### К-4 — ревизия: пер-ресурсная со скоупом КНИГА; чтения её несут