Accept and ratify platform P0 with corrections: defect and vulnerability register, zone engineering standards, backlog rows for OAuth and credits, stale doc pointers fixed

This commit is contained in:
heaven 2026-08-05 00:00:04 +03:00
parent eeeef897b9
commit 954c034f53
6 changed files with 180 additions and 4 deletions

View file

@ -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; при вопросе «почему так» — этот файл.

View file

@ -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 |

View file

@ -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).
## Чего здесь НЕ будет

View file

@ -0,0 +1,29 @@
# Регистр дефектов и уязвимостей платформы
> Заведён по решению владельца 04.08 («отдельно ведётся колонка багов и уязвимостей — тут опасно всё»).
> Правила: каждая находка любой сессии/приёмки/аудита — строкой сюда ДО закрытия; ID стабилен навсегда;
> закрытие — только с коммитом фикса и тестом, пинящим свойство (урок PD-1: свойство без пинящего теста
> считается НЕ закрытым). Класс: `vuln` — эксплуатируемо или ослабляет защиту · `bug` — неверное поведение ·
> `hardening` — защита в глубину / латентное. Статус: `open` · `fixed(<commit>)` · `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 757 и держались, пока не закрыл КЛИЕНТ (агент-скептик независимо пинил 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-линза) |

View file

@ -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. Дерево не коммитится сессией — лендит оркестратор после адверсариальной приёмки
(для несущих путей — с воркфлоу-верификаторами и посадками).

View file

@ -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 — ревизия: пер-ресурсная со скоупом КНИГА; чтения её несут