From ac729bf54477cc155b6aa72da91db9bde6c86bce Mon Sep 17 00:00:00 2001 From: heaven Date: Wed, 2 Sep 2026 00:38:10 +0300 Subject: [PATCH] Land batch B5: the contract companion taught a compression gap the platform closed in Go, and its provenance column had rotted by 240 lines --- docs/architecture/14-api-contract/README.md | 46 +++++++++---------- .../architecture/14-api-contract/openapi.yaml | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md index 9aff803f..3fca6b12 100644 --- a/docs/architecture/14-api-contract/README.md +++ b/docs/architecture/14-api-contract/README.md @@ -57,7 +57,7 @@ cmp-сверка обязательна (D39.138 п.3). > целостному ревью research/28** · **0.4.0 (D39.152, 20.08) — синк с платформой, §6в** · > **0.5.0 (D39.161, 27.08) — снос отменённой пер-термной модели подписи (PD-370) + дверь правок > банка, выведенная из построенного глагола движка (D39.158), + предупреждение о конверте вне -> `/v0`; вывод и провенанс — §2.19, отчёт сессии — `docs/CONTRACT_MINOR_REPORT.md`** · +> `/v0`; вывод и провенанс — §2.19, отчёт сессии — `../../archive/reports/CONTRACT_MINOR_REPORT_2026-08-27.md`** · > **0.6.0 (D39.162, 28.08) — сквозная полоса прогресса и подпись стадии, приехавшие ОДНИМ лендингом > с платформенным паком P9; вывод и провенанс — §2.20** · > **0.7.0 (D39.166, 28.08) — покупка ПЕРЕ-ПРОХОДА: правка банка доезжает в уже переведённый текст; @@ -735,7 +735,7 @@ the original request was never applied». Каждый `POST /books` созда | занятый арбитр (живой прогон держит флок) — «подожди», не «сломано» | класс 12 (`main.go:71`), `bankdecisions.go:159-163` | **Решения этой сессии (◆), каждое с доводом; подробный разбор и отвергнутые альтернативы — отчёт -`docs/CONTRACT_MINOR_REPORT.md`:** +`../../archive/reports/CONTRACT_MINOR_REPORT_2026-08-27.md`:** - **Один `POST /books/{bookId}/bank/corrections` с документом целиком.** Глагол принимает документ и отвечает отчётом атомарно; ресурсная модель (PATCH строк) врала бы про атомарность и про то, что @@ -880,7 +880,7 @@ the original request was never applied». Каждый `POST /books` созда (`runStage` — SQL над теми же колонками, а не проброс движковой строки), и словарь ОТКРЫТ — клиент обязан рисовать незнакомое значение нейтрально. Следствие, ради которого условия и поставлены: **пара или конвейер с ИНОЙ формой работы не требуют нового клиента и не поднимают версию.** -Сегодняшние значения — `drafting` и `editing`. +Сегодняшние значения — `drafting`, `editing` и `re_pass` (⚠ испр. 02.09: третье значение выдаёт `runStage` для всего пере-прохода — `platform/internal/pgstore/readmodel.go`, греп `re_pass` — и оно доезжает на провод). ⚠ **Ложная перекрёстная ссылка, снятая этим же минором.** `ChapterProgress.units_done` обещал «то же счетоводство, что у `Progress`, уровнем ниже». После сквозной полосы это неправда: глава по-прежнему @@ -978,12 +978,12 @@ into the generated client's source**» (§Boundaries). Поймано линзо | `GET /books`, `GET /books/{id}` | read-модель платформы | ПОСТРОЕНО | — | | `GET /books/{id}/run-options`, `POST /runs` | шкала + холды | ПОСТРОЕНО | — | | `POST /runs/{id}/stop` | реконсилятор | ПОСТРОЕНО (кнопки на экране нет) | зона фронта | -| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **0.3.0 сменил поведение на паузе по потолку, а 0.4.0 — на ИСЧЕРПАННОМ прогоне**. Пауза: платформа уже отвечает `409` `ceiling_reached` (`runs/reconcile.go`, ветка `case "paused"` в `Resume`) — совпало. Исчерпанность: платформа отвечает `202` и возвращает прогон в то же состояние (`reconcile.go`, оба выхода `exhausted` из `reopen` → ветка `exhausted` в `Resume` → `httpapi/v0.go`, хендлер `resumeRun`), а контракт теперь требует `409` `run_not_resumable` · `cause.code: ceiling_reached`. Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op, а на `awaiting_bank` это ещё и не вызывает `ReleaseBankStop`. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) | +| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **0.3.0 сменил поведение на паузе по потолку, а 0.4.0 — на ИСЧЕРПАННОМ прогоне**. Пауза: платформа уже отвечает `409` `ceiling_reached` (`runs/reconcile.go`, ветка `case "paused"` в `Resume`) — совпало. Исчерпанность: ⚠ **РАСХОЖДЕНИЕ ЗАКРЫТО (испр. 02.09)** — `Resume` больше не отвечает `202` на исчерпанный прогон. Реконсилятор различает ДВА вердикта, и оба доезжают до клиента кодом 409 `run_not_resumable`: `ceilingSpent` → `cause.code: ceiling_reached` и `creditUnavailable` → `cause.code: credit_unavailable` (`platform/internal/runs/reconcile.go`, греп `ceilingSpent`; `platform/internal/httpapi/v0.go`, греп `run_not_resumable`). Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op, а на `awaiting_bank` это ещё и не вызывает `ReleaseBankStop`. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) | | `GET /usage` | кредиты | ПОСТРОЕНО, **не читается ни одним экраном** | зона фронта | > ⚠ **ТАБЛИЦА ПЕРЕ-СНЯТА ПРОТИВ КОДА 22.08 (оркестратор №18, аудит доков): шесть строк объявляли НЕ ПОСТРОЕННЫМ то, что пак P7 построил, а P8 сохранил.** Правило §Б-21 требует вести здесь статус построенного — оно нарушалось пятый раз подряд, потому что носитель «вход P7» никто не гасил при лендинге. Носитель израсходован: пак P7 принят D39.153, пак P8-FIX — D39.154. | `GET /capabilities` | конфигурация деплоя | **ПОСТРОЕНО P7** — маршрут смонтирован безусловно (`platform/internal/httpapi/v0.go`, таблица `contractSurface`), хендлер `capabilities.go`; версия контракта запинена ГЕЙТОМ против ЭТОГО канона (`platform/internal/gates/contract_test.go`) | закрыто D39.153 | -| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 | +| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) — факт верен: роутер монтирует 15 операций из 20 | ⚠ носитель «вход P7» ИЗРАСХОДОВАН (пак P7 принят D39.153); живой носитель — `platform/BACKLOG.md` П-17 и очередь CURRENT-STATE | | `GET /books/{id}/chapters`, `/units` | материализация манифеста | **ПОСТРОЕНО P7** — `httpapi/reading.go`, материализатор `internal/readmodel` | закрыто D39.153 | | `GET /books/{id}/notes` | `unit_done` несёт флаг и причину (`runevents.go:126-135`), платформа хранит (`sink.go:227-233`), колонка `notes.reason` заведена под это | канал ЕСТЬ; не хватает карты «причина → код → фраза» (приложение А) и проекции | приложение А + вход P7 | | `GET /books/{id}/bank` | движок пишет сайдкар всего банка (`pipeline/bankexport.go:16-33,72`, D39.122) | ⚠ пере-снято 0.5.0: проекция платформы ПОСТРОЕНА (P7 — маршрут в `contractSurface`, `wireBankPage` в `httpapi/reading.go`, `SaveBank` в `pgstore`; прежняя запись «не хватает проекции» устарела при израсходованном носителе «вход P7», D39.153); форма синхронна канону с лендингом P9: счётчики `pending_decisions`/`complete` сняты с проекции (`PD-399`) | закрыто D39.162 | @@ -995,11 +995,11 @@ into the generated client's source**» (§Boundaries). Поймано линзо | `Idempotency-Key` | — | **ПОСТРОЕНО P7** — `httpapi/idempotency.go` + `pgstore/idempotency.go`, на двух создающих вызовах; ⚠ живой дефект под конкуренцией — `PD-369` | закрыто D39.153 | | `bearerToken` — чем ВЫДАЁТСЯ токен | вход только ставит HttpOnly-куку (`login.go:338`) | сервер токен ПРИНИМАЕТ, выдать его нечем | носитель: research/28 §2 (Б-15); строки нет | | `Problem.localized` | — | объявлено, ни одним кодом не используется | носитель: research/28 §8 п.4 | -| `Note.code` как enum спеки | карта приложения А | не enum, пока не написаны фразы | строка 148 (фразы владельца) | +| `Note.code` как enum спеки | карта приложения А | не enum, пока не написаны фразы | ⚠ **прежний носитель «строка 148» МЁРТВ** — строку удалил `a023e39` решением владельца 21.08 (тот же факт этот док уже несёт ниже, в §6в); живой носитель — строка бэклога **204** (словарь фраз) | | Настоящие названия глав (`Chapter.heading` ≠ null) | парсер структуры | НЕ ПОСТРОЕНО | строка 160 (Этап 0) | | `title_raw` / `kind` (глава ↔ фрагмент) | дизайн-пак структуры глав | передано паку, аддитивно | строка 161 | | `ErrorCode.content_refused` (400) **и** `RejectReason.content_refused` | прескрин злоупотреблений | НЕ ПОСТРОЕН ни на одной стороне: в платформе только объявление константы (`httpapi/problem.go:42,94`), `ContractRejectReason` (`ingest/vocabulary.go:59-69`) его не отображает; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ (`disposition.go:60-63` → `Note.code: content_withheld`) и в exit-контракт не выходит — мостá между двумя словарями нет | **строка 94 (ПТ-16)**; там же ограничение числа попыток аккаунта — обязанность падает ВМЕСТЕ с производителем, не раньше | -| `decline` в подписи банка доезжает до работы | ⚠ пере-снято 0.5.0: старый носитель («таблица `bank_decisions`, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на `BankDecision.action` снесено вместе со схемой | дверь смонтирована (D39.162), и decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ **Остаток ДВИЖКОВЫЙ, две штуки, обе найдены воркфлоу-ревью P9:** decline не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, `mining.go:548`), и повтор ТОГО ЖЕ документа на одном пути не сходится (`membank/decisions.go:374`) — второе ломает обещание сходимости, на котором стоит синхронная дверь | заказано бэкенд-паку «тихая порча», `docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md` §3.3 | ⚠ пере-снято 30.08: из двух движковых остатков ВТОРОЙ ЗАКРЫТ — порядок записи документов решений сменён на rejects-first, повтор принятого документа СХОДИТСЯ (D39.164 §3.3). Живым остаётся ОДИН: `decline` не энтити-широк на обратном пути — отклонённая сущность возвращается через свой АЛИАС, тогда как канон обещает «declining a surface removes EVERY window of that surface». Носитель — строка 228 единого бэклога. +| `decline` в подписи банка доезжает до работы | ⚠ пере-снято 0.5.0: старый носитель («таблица `bank_decisions`, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на `BankDecision.action` снесено вместе со схемой | дверь смонтирована (D39.162), и decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ **Остаток ДВИЖКОВЫЙ, две штуки, обе найдены воркфлоу-ревью P9:** decline не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, `mining.go:548`), и повтор ТОГО ЖЕ документа на одном пути не сходится (`membank/decisions.go:374`) — второе ломает обещание сходимости, на котором стоит синхронная дверь | заказано бэкенд-паку «тихая порча», `../../archive/prompts/BACKEND_SILENT_HARM_SESSION_PROMPT_2026-08-28.md` §3.3 | ⚠ пере-снято 30.08: из двух движковых остатков ВТОРОЙ ЗАКРЫТ — порядок записи документов решений сменён на rejects-first, повтор принятого документа СХОДИТСЯ (D39.164 §3.3). Живым остаётся ОДИН: `decline` не энтити-широк на обратном пути — отклонённая сущность возвращается через свой АЛИАС, тогда как канон обещает «declining a surface removes EVERY window of that surface». Носитель — строка 228 единого бэклога. | Снятие замечания (переход «флаг снят») | движок | **НЕДОСТИЖИМО сегодня, проверено чтением движка** — п. H §6в | **PD-298** регистра платформы (`platform/docs/DEFECT_REGISTER.md`) — там строка и живёт; механизма не строим | | Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | **вход P7** (PD-202) | @@ -1634,8 +1634,8 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ Ниже — то, что клиент не наблюдает и что деплой обязан себе устроить сам: -- **Чем и на каком слое сжимать** (middleware, обратный прокси, CDN). Go stdlib не сжимает, - edge-конфига в репозитории нет, `grep -rn "gzip|Content-Encoding" platform --include=*.go` → ноль. +- **Чем и на каком слое сжимать** (middleware, обратный прокси, CDN). ⚠ **ПРОТУХЛО (испр. 02.09):** + платформа сжимает САМА, в Go — `platform/internal/httpapi/conditional.go`, греп `gzipFloor` (порог 1<<10 байт) и `acceptsGzip`; edge-слой для этого не нужен. - **HTTP/2 на edge.** Клиент держит одно SSE-соединение на книгу; на HTTP/1.1 шесть соединений на origin — потолок вкладок. Сервис на HTTP/1.1 обязан работать ХУЖЕ, а не не работать. - **`X-Accel-Buffering: no`** (или эквивалент прокси) — механизм для нормы «поток не буферизуется». @@ -1707,28 +1707,28 @@ heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИ ### Приложение А-2. Карта кодов ОШИБОК — заполнена (0.3.0) В отличие от карты выше, эта заполнена целиком: словарь выведен из реальных ветвей платформы, а фраз -она не содержит по замыслу — их рисует клиент. +она не содержит по замыслу — их рисует клиент. ⚠ **Колонка «Откуда взят» переведена 02.09 с НОМЕРОВ СТРОК на СИМВОЛЫ:** вся она съехала на 240–270 строк (производители уехали, на прежних номерах стояли комментарии и скобки), и это её второе протухание. Адресация символом (`pgstore.ErrNoBook`, `readField`, имя файла) переживает любой сдвиг; номеров сюда больше не ставить. | Корневой `code` | HTTP | Откуда взят (платформа) | |---|---|---| -| `invalid_request` | 400 | `v0.go:238,250,333,339,531` · `pgstore.ErrBadCursor` · `books.ErrBadIntake` (`books.go:118-121`, `readField` `v0.go:394-396`) · `v0.go:242` (неполное тело) · `v0.go:345,356,419,423` (интейк) | -| `unauthenticated` | 401 | гард сессии (`server.go:109-110`) | -| `forbidden` | 403 | `server.go:99` + `auth/csrf.go` (отсутствие `X-TM-Client` / чужой origin) | -| `not_found` | 404 | `pgstore.ErrNoBook`/`ErrNoAccount`/`ErrNoRun` · охраняемый catch-all `server.go:134` | -| `gone` | 410 | `pgstore.ErrNoChapter` → `v0.go:761` — глава, которой нет в ТЕКУЩЕЙ структуре книги (была она когда-то или нет — не различается по замыслу, `openapi.yaml` §`gone`; лечение клиента одно: перечитать дерево). ⚠ Строка дописана оркестратором №18 при ратификации 0.4.0: карта объявляла себя «заполненной», а несла 15 значений из 16 корневых — производитель у `gone` живой с P7 | -| `request_timeout` | 408 | `os.ErrDeadlineExceeded` → `v0.go:417` | -| `payload_too_large` | 413 | `*http.MaxBytesError` → `v0.go:409` | -| `run_in_flight` | 409 | `pgstore.ErrRunInFlight` (`v0.go:548`) | -| `book_not_ready` | 409 | `runs.ErrBookNotReady` (`v0.go:550`) | -| `run_not_stoppable` | 409 | `runs.ErrNotStoppable` (`v0.go:554`) | +| `invalid_request` | 400 | `v0.go` · `pgstore.ErrBadCursor` · `books.ErrBadIntake` (`books.go`, `readField` `v0.go`) · `v0.go` (неполное тело) · `v0.go` (интейк) | +| `unauthenticated` | 401 | гард сессии (`server.go`) | +| `forbidden` | 403 | `server.go` + `auth/csrf.go` (отсутствие `X-TM-Client` / чужой origin) | +| `not_found` | 404 | `pgstore.ErrNoBook`/`ErrNoAccount`/`ErrNoRun` · охраняемый catch-all `server.go` | +| `gone` | 410 | `pgstore.ErrNoChapter` → `v0.go` — глава, которой нет в ТЕКУЩЕЙ структуре книги (была она когда-то или нет — не различается по замыслу, `openapi.yaml` §`gone`; лечение клиента одно: перечитать дерево). ⚠ Строка дописана оркестратором №18 при ратификации 0.4.0: карта объявляла себя «заполненной», а несла 15 значений из 16 корневых — производитель у `gone` живой с P7 | +| `request_timeout` | 408 | `os.ErrDeadlineExceeded` → `v0.go` | +| `payload_too_large` | 413 | `*http.MaxBytesError` → `v0.go` | +| `run_in_flight` | 409 | `pgstore.ErrRunInFlight` (`v0.go`) | +| `book_not_ready` | 409 | `runs.ErrBookNotReady` (`v0.go`) | +| `run_not_stoppable` | 409 | `runs.ErrNotStoppable` (`v0.go`) | | `run_not_resumable` | 409 | `runs.ErrNotResumable`; `cause`: `ceiling_reached` — **с 0.4.0 это же ответ на ИСЧЕРПАННЫЙ прогон в `stopped`/`awaiting_bank`, где платформа сегодня молча отвечает `202`** (A, PD-282: `runs/reconcile.go`, ветка `exhausted` функции `reopen` и её чтение в `Resume`). ⚠ `bank_decisions_incomplete` удалён D39.144 — гейта полноты нет; построенный гейт полноты ДЕМОНТИРОВАН (0.5.0, сверено с кодом: `reconcile.go`, ветка `case "awaiting_bank"` снимает стоп при ЛЮБОМ состоянии решений, комментарий «this is the built half being dismantled with it») | -| `ceiling_unavailable` | 409 | `runs.ErrCeilingOutOfBounds` + `pgstore.ErrInsufficientCredit` (`v0.go:558`); `cause`: `bounds_moved` · `credit_held`; несёт `blocked` | +| `ceiling_unavailable` | 409 | `runs.ErrCeilingOutOfBounds` + `pgstore.ErrInsufficientCredit` (`v0.go`); `cause`: `bounds_moved` · `credit_held`; несёт `blocked` | | `idempotency_conflict` | 409 | форма заведена батчем; реализация — P7 | | `bank_corrections_refused` | 409 | объявлен 0.5.0 ДО производителя; производитель — монтаж двери правок (пак (2в) очереди D39.156): раскладка отказа класса 14 движка (`tmctl bank-apply`, документ прочитан и отклонён целиком — пользователь пере-решает) | | `bank_corrections_incomplete` | 503 | объявлен 0.5.0 ДО производителя; тот же монтаж: раскладка класса 15 (документ принят, запись не довершена — слать ТОТ ЖЕ документ, ретрай сходится) | | `content_refused` | 400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием `rejected` + `reject_reason` | -| `service_unavailable` | 503 | `runner.ErrCeilingNotWired` + `runs.ErrRunnerIncomplete` (`v0.go:562`) | -| `internal_error` | 500 | `v0.go:518,572` · `middleware.go:68` | +| `service_unavailable` | 503 | `runner.ErrCeilingNotWired` + `runs.ErrRunnerIncomplete` (`v0.go`) | +| `internal_error` | 500 | `v0.go` · `middleware.go` | ⚠ **Коды, которых платформа сегодня достигает, а контракт до 0.3.0 не объявлял:** 403 (`server.go:99`), 500 (`v0.go:518,572`), 431 от `net/http` (`serve.go:79`), 429 на `/auth` (`login.go:173,234`). Из них diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml index a47c3c06..a6593202 100644 --- a/docs/architecture/14-api-contract/openapi.yaml +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -1411,7 +1411,7 @@ components: **The vocabulary is OPEN, and a client MUST render a value it does not know neutrally** — the bar alone, or a generic phrase — rather than failing, hiding the run, or guessing. - Today's deployments answer `drafting` or `editing`; other values are not a breaking + Today's deployments answer `drafting`, `editing` or `re_pass`; other values are not a breaking change and do not raise this version. ⚠ Derived by the platform from the same counters as `done` and `total`, so the caption