Land contract batch 0.3.0: canon and companion accepted by a three-lens panel, session report archived, dofix round FB-1..10 ordered as a prompt addendum

This commit is contained in:
heaven 2026-08-16 20:07:12 +03:00
parent 10225677db
commit 8d8209611c
4 changed files with 2852 additions and 948 deletions

View file

@ -184,3 +184,20 @@ D39.138): ядро контракта здорово, дефекты сосре
дифф-сводка «было → стало» по операциям. Без саги. Дерево оставить
незакоммиченным. Вопросы и конфликты промта с кодом/доками — пинг оркестратору через владельца,
НЕ интерпретация.
---
## ДОФИКС-РАУНД (заказ приёмки D39.142, 16.08; аддендум — доезжает релеем владельца, подтверди эхом)
Батч ПРИНЯТ и заленден (панель: слепая сверка — потерь 0/искажений 0; аудит отчёта — 25+ клеймов сошлись). Дофикс — находки ХОЛОДНОГО ПОТРЕБИТЕЛЯ (линза, которой у твоего селф-ревью не было: только финальная спека + tsc-проба) и хвосты аудита. Зона и гейты прежние. Ратификации приёмки, которые НЕ двигать: прогонный поток снят · `Note.code` вместо `message` · `content_refused` остаётся · единый `page_size_default` принят. По каждому ФБ — диспозиция в дополнении к отчёту.
**ФБ-1 (HIGH).** Wire-форма SSE-кадра неоднозначна: `EventEnvelope{event,id,data}` как объект против `event:`/`id:` как SSE-фрейминг — два верных чтения (улика: A3 панели). Записать однозначно.
**ФБ-2 (HIGH).** Книга в покое без единого кадра истории: `hello` не имеет id последнего кадра → клиент без `Last-Event-ID` → «всегда новый поток» → hello→end→close→reconnect навечно (A4). Определить поведение.
**ФБ-3 (HIGH).** «`note` не теряется» не переживает reconnect: буфер не гарантирован, resync не шлётся (A5). Записать обязанность клиента: дельта-чтение `/notes?after_version=` после КАЖДОГО переподключения (механизм уже есть — назвать долг).
**ФБ-4 (HIGH).** `Revision` противоречит себе: «каждое книжное чтение несёт одно число» vs «рваное чтение = ревизия СТАРЕЙШЕЙ страницы» vs водяной знак «ревизия только что полученного конверта» (C1/A6); `BookDetail.revision` vs `book.revision` — равенство не заявлено (C2). Свести в одну модель; правило водяного знака при много-страничной дельте — явно.
**ФБ-5 (MED).** `EventEnd`/`EventResyncRequired` — пустые `allOf`-члены дают необитаемые типы (`Record<string,never>`, B1); дать им явные `properties` (хотя бы `{}` честной формой) и подсказку маппинга event→payload машинно-дружелюбнее (B2 — можно прозой-таблицей + непересекающимися формами).
**ФБ-6 (MED).** Спека приказывает предлагать «новый прогон» как лечение стопа по потолку, но resumable-статусы не перечислены, а `paused_reason:null` описан «нейтрально, continuable» (A1/A2). БЕЗ нового значения enum (ратификацию D39.132 п.2а не двигать): записать — «новый прогон с бОльшим потолком» легален для ЛЮБОГО paused; перечислить, что делает `resume` по каждому останавливающему статусу.
**ФБ-7 (MED).** Правило «ответ НЕ `problem+json`» (прокси 502, HTML, обрыв) — потерянный пункт заказа (Б-1 рек. п.4): клиенту нельзя показывать серверный текст и нечем взять код — записать нейтральный fallback (E1).
**ФБ-8 (MED, пачка одной строкой каждая).** merge-patch `title:null`→400 против RFC 7386 — объяснить в описании (C6) · `X-TM-Client` required против освобождения bearer — записать разводку в самом параметре (C8) · список «Refusals:» пометить неисчерпывающим (C5) · правило резолюции `Location` (A7) · тождество `Idempotency-Key` на multipart + путь ретрая 408-под-тем-же-ключом (A9) · `min_chapters` при `max=0` (A11) · приоритет пяти носителей «нет кредита» (A12) · «carried forward marked as unverified» — носителя на проводе нет: снять или дать (A13) · 409 у `createExport` объяснить (A14) · код для неизвестного `term_id` в decisions (A15).
**ФБ-9 (LOW).** `parser_unavailable` — имя компонента в enum, переименовать по эффекту (L1) · внутренние ссылки `research/28 §…`/«companion К-6» уезжают в исходники клиента — вычистить из описаний, оставить в компаньоне (L3).
**ФБ-10 (отчёт).** Пере-ран чисел ПОСЛЕ последней правки: строки файла (2315, не 2290) · `ErrorCode` 16, не 15 · генератор 2608 · «26 находок» → фактические 57/67 · «все девять» §5.3 → 11 строк · «8 из 20 не отвечает» → 12 из 20 · якорь `mining.go:201` → :81/:243 · «строка 21» → 18; скрипт мерки прозы/формы приложить в отчёт или снять вывод о доле; дописать строки записки-плана Б-23/К-10/В-6; ярлык §5.5 «исполнено иначе» → «отступление, аргумент»; README §6а — фраза «апологии перенесены» → «ужаты на месте».

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,594 @@
# Отчёт: батч 0.3.0 — ломающая правка контракта API v0
> Сессия по промту `docs/CONTRACT_BATCH_SESSION_PROMPT.md` (выдан оркестратором №17 16.08.2026 по
> ратификации D39.138 п.2/п.3, строка 183). Носитель заказа — `docs/research/28-contract-review.md`
> §5 · §5а · §5б · §8 · §8а + ревью-шапка приёмки; при расхождении с ней побеждала нота D39.138.
>
> **Зона записи — три файла, других не тронуто:** `docs/architecture/14-api-contract/openapi.yaml` ·
> `docs/architecture/14-api-contract/README.md` · этот отчёт. Зеркало
> `frontend/docs/api-contract/openapi.yaml` НЕ тронуто (зона фронта заморожена, D39.136 п.2) —
> расхождение канона и зеркала на момент сдачи известно и ратифицируется лендингом. Git не трогался:
> ни `add`, ни `commit`, ни `reset`, ни `checkout`. Дерево оставлено незакоммиченным.
## 0. Что сделано, одним экраном
| | было (0.2.3) | стало (0.3.0) |
|---|---|---|
| операций / путей | 16 / 15 | **20 / 17** |
| строк файла | 1445 | **2290** |
| форма (структурные строки) | 828 | **1336** (+61 %) |
| проза (строки внутри описаний) | 472 | **757** (+60 %) |
| доля прозы | 36 % | **36 %** |
| причина отказа на проводе | английская фраза, показываемая «as-is» | машинный `code` (15 корневых) + `cause` + `request_id` + `errors[]` |
| прогресс | `{draft{done,total}, edit{done,total}}` по всей книге, в юнитах | ОДНА полоса до ближайшей остановки, в главах, знаменатель — купленный объём |
| поток | `GET /runs/{runId}/events` | `GET /books/{bookId}/events` + кадр `end` + `204` |
| условные чтения | нет ни одного | `ETag`/`If-None-Match`/`304` на 7 чтениях + дельта `?after_version=` |
| «что умеет деплой» | нигде | `GET /capabilities` |
Команды, которыми получены числа:
```
grep -c 'operationId:' openapi.yaml # 16 → 20
grep -cE '^ /' openapi.yaml # 15 → 17
wc -l openapi.yaml # 1445 → 2290
git show HEAD:...openapi.yaml > /tmp/old.yaml # снимок «было» для всех сверок
```
Проза/форма меряны одним скриптом по обоим файлам (лист-скан: непустая строка внутри блочного
скаляра `description:|`/`summary:|` — проза, остальные — форма; пустые не считаются). Число «43 %» из
research/28 §5а получено другой меркой и с этими двумя не сравнивается — сравнимы только они между
собой.
**Форма выросла на 61 %, проза — на 60 %, доля прозы не сдвинулась.** Это и есть ответ на вопрос §8
п.3 «не переусложнён ли контракт»: батч добавил четыре операции и шестнадцать схем, и текст вырос
ровно пропорционально поверхности, а не быстрее неё. Как этого добились — §4.4; первая редакция
батча давала 43 % и 2587 строк, то есть проза росла быстрее формы, и это было исправлено отдельным
проходом.
## 1. Записка-план: каждый ID заказа с диспозицией
Комплектность сверялась механически против носителя, не по памяти.
### 1.1. Ядро — research/28 §5, пункты 04
| ID | Заказ | Диспозиция | Где |
|---|---|---|---|
| **§5.0 / Б-0** | `Progress` в один счётчик до остановки | **ИСПОЛНЕНО.** `Progress{done,total,eta_seconds}` в ГЛАВАХ, сегментная логика, знаменатель — купленный объём; `Counter` снят; `Progress` переехал с `Book` на `Run` | `§Progress`, `§Run.progress` |
| | `finalizing` свернуть | **ИСПОЛНЕНО.** Значение снято из `BookStatus`; в новый `RunStatus` не попало | `§BookStatus`, `§RunStatus` |
| | `verify_bank``stop_for_signing` без «before the final pass» | **ИСПОЛНЕНО** на `RunRequest` и на `Run` | `§RunRequest`, `§Run.stop_for_signing` |
| | `TermOrigin` снять/переименовать | **ПЕРЕИМЕНОВАНО, ось сохранена:** `seed·ruby·mined``given·annotated·found` | `§TermOrigin` |
| | `TermStatus` ОСТАВИТЬ осью, переименовать движковые значения (эррата 16.08-г) | **ИСПОЛНЕНО.** Ось на месте; `auto·draft·approved``proposed·in_progress·approved` (переименован и `draft` — он читается как имя волны и ловится анти-утечка-грепом) | `§TermStatus` |
| | `Unit` описать без «edit unit» и «1.9 на главу» | **ИСПОЛНЕНО** | `§Unit` |
| | из ВСЕХ описаний вычистить конвейерные слова | **ИСПОЛНЕНО**, проверено грепом (§4.2) | весь файл |
| | счётчик ГЛАВЫ той же сегментной логикой | **ИСПОЛНЕНО.** `ChapterProgress.units_done` — «в текущем сегменте», с явным «иначе дерево читало бы ноль всю первую волну» | `§ChapterProgress.units_done` |
| | завести гейт на утечку | **ЧАСТИЧНО — по границе зон, как заказано промтом §3.** Правило записано в компаньон как ревью-вопрос каждой правки со списком грепа; ТЕСТ — половина фронта, носитель — пинг фронту 16.08, исполнение при разморозке | компаньон §5 |
| **§5.1 / Б-1** | модель ошибок, вариант B | **ИСПОЛНЕНО ПЕРВОЙ** (порядок §3 промта соблюдён: словарь кодов определён до формулировок Б-2/Б-3/Б-8/Б-14а). `code` (15 корневых, закрытый) + `cause{code}` (второй уровень, НЕ закрытый) + `request_id` + `errors[]{pointer,code}` + `localized` + `blocked`; `title`/`detail` объявлены developer-facing и неотображаемыми, `title` получил `minLength:1` | `§Problem`, `§ErrorCode`, `§ErrorCause`, `§ErrorItem` |
| | два класса конкретности §8а | **ИСПОЛНЕНО.** Класс 1 — максимальная конкретика (`errors[]` + `cause`); класс 2 — ОДИН код `content_refused` без `cause`, без `errors`, без вариации между попытками; тем же правилом заведено значение `RejectReason.content_refused` (К-9) | `§ErrorCode.content_refused`, `§RejectReason` |
| **§5.2 / Б-15** | `X-TM-Client` машинно · 403 · `WWW-Authenticate` · множество методов · `servers.url` · судьба bearer | **ИСПОЛНЕНО ПОЛНОСТЬЮ.** Заголовок — параметр на 9 небезопасных операциях; `403` — компонент-ответ; `401` несёт обязательный `WWW-Authenticate`; методы приведены к коду и RFC (`OPTIONS` освобождён); `servers[0].url: /v0`; bearer ОСТАВЛЕН с явной записью «сервер принимает, выдать нечем» + носитель | `§ClientHeader`, `§responses.Forbidden`, `§responses.Unauthorized`, `§sessionCookie`, `servers`, `§bearerToken` |
| **§5.3 / Б-2** | `GET /capabilities` | **ИСПОЛНЕНО.** `contract_version` · `language_pairs[]` со статусом · `intake_max_bytes` · `export_formats[]` · `page_size_default`. Один плоский ответ на деплой (предупреждение судьи о соразмерности учтено: ни ETag-мультитенантности, ни пер-аккаунтных оверрайдов) | `/capabilities`, `§Capabilities` |
| **§5.4 / Б-7а** | три устаревших предупреждения снять; resume-абзац переписать | **ИСПОЛНЕНО.** «канала банка нет», «движок не эмитит note», «зависит от эмиттера» — сняты; `resume` после потолка → 409 `cause.code: ceiling_reached` + названо работающее лечение (новый прогон с бОльшим потолком) в `startRun` И в `resumeRun` | `/runs/{runId}/resume`, `/books/{bookId}/runs`, компаньон §3 |
### 1.2. Пока дёшево (Ц0) — §5, пункты 510
| ID | Заказ | Диспозиция | Где |
|---|---|---|---|
| **§5.5 / Б-6** | перевесить поток на книгу | **ИСПОЛНЕНО**`GET /books/{bookId}/events`, `streamBookEvents` | `/books/{bookId}/events` |
| | кадры конца разбора и конца прогона | **ИСПОЛНЕНО ИНАЧЕ, чем буквально:** конец разбора — обычная смена статуса на книжном потоке (кадр не нужен, и Б-6(а) сам это говорит: «чинить не кадром, а перевеской»); конец ПОТОКА — новый кадр `end` | `§EventEnd` |
| | `204` при переподключении к завершённому | **ИСПОЛНЕНО** с точной границей: `204` только на запрос С `Last-Event-ID`; без него всегда открывается новый поток | `/books/{bookId}/events``204` |
| | развести `id` кадра и ревизию | **ИСПОЛНЕНО.** `id` — позиция потока, форма зафиксирована `^[0-9]+$`; `revision` — в `data` каждого кадра через `EventBase` | `§EventEnvelope.id`, `§EventBase` |
| | правило докачки — одно | **ИСПОЛНЕНО:** короткий живой буфер разрешён, реплей истории запрещён, размер не объявляется | `/books/{bookId}/events` → Reconnect |
| | запрет склейки `note` | **ИСПОЛНЕНО** | `/books/{bookId}/events` → Coalescing |
| | *(отступление)* прогонный поток «оставить узким видом» | **ОТКАЗ, аргументирован** — §3.1 ниже | — |
| **§5.6 / Б-4** | экспорт: `state`, `failure_code`, `expires_at`, эхо формата, правило доступа | **ИСПОЛНЕНО ПОЛНОСТЬЮ** + `Export.revision` (одиночная правка §5) | `§Export` |
| **§5.7 / Б-9+Б-14а** | `id`, `created_at`, обязательная адресация; `pending_decisions`/`complete` в чтение банка | **ИСПОЛНЕНО.** `Note.id`/`created_at`/`chapter_id` обязательны; `unit_id` оставлен необязательным (ровно то, что просил Б-9); банк отвечает `pending_decisions`+`complete`+`total`+`signed` | `§Note`, `§BankPage` |
| **§5.8 / Б-19** | снять запрет клиентской служебной метки СЕЙЧАС | **ИСПОЛНЕНО** | `§Chapter.heading` |
| | `title_raw`, `kind` — формой ЛИБО передачей паку 161 | **ПЕРЕДАНО ПАКУ 161**, аргумент — §3.2 ниже; запись в компаньоне §2.3 | компаньон §2.3, §3 |
| | структурная версия | **ИСПОЛНЕНО (обязательна батчу)**`structure_version` в `hello`, `ChapterPage`, `UnitPage`, `Book` | `§StructureVersion` и др. |
| **§5.9 / Б-7** | версия в ответах и `hello`; `410`; `If-None-Match`/`304` | **ИСПОЛНЕНО.** `410 Gone` на `listUnits`; условные чтения на 7 операциях | `§responses.Gone`, info §Conditional reads |
| **§5.10 / Б-12** | правило согласования агрегатов со страницами | **ИСПОЛНЕНО:** агрегаты — на ПЕРВОЙ странице; правило согласовано с ревизией обхода (минимум по страницам), которая тоже записана впервые | `§BankPage`, `§Revision` |
### 1.3. Дёшево и на построенном (Ц1) — §5, пункты 1116
| ID | Заказ | Диспозиция | Где |
|---|---|---|---|
| **§5.11 / Б-3** | запрет молчаливой потери | **ИСПОЛНЕНО:** любая часть после файла → 400 + `errors[]`, «Nothing is lost silently» | `/books` POST |
| | порядок свойств `BookIntake` | **ИСПОЛНЕНО и ПРОВЕРЕНО ГЕНЕРАТОРОМ** (§4.1): `title, source_lang, target_lang, file` | `§BookIntake` |
| | `Idempotency-Key` (семантика — моя) | **ИСПОЛНЕНО**, шесть правил: область (принципал+метод+путь) · повтор → исходный ответ · другие параметры → 409 `key_reused` · параллельный → 409 `key_in_flight` · окно ≥24 ч · длина ≤255 | `§IdempotencyKey` |
| | `Location` на 201 | **ИСПОЛНЕНО** | `/books` POST → 201 |
| **§5.12 / Б-8+Б-13а** | `failure_reason` у прогона | **ИСПОЛНЕНО.** `RunFailureReason: source_unreadable · service_error · interrupted` — спроецировано с полосы отказов движка 1019 (`config_invalid`/`source_unreadable`/`project_locked`/`schema_mismatch`, `backend/internal/pipeline/refusal.go:29-46`) и ветки `outcome()` платформы; таксономия НЕ выдумана | `§RunFailureReason` |
| | знаменатель прогона в главах | **ИСПОЛНЕНО**`Progress.total` = купленный объём; плюс `Book.chapters_done` (величина «глав сделано», которой не было вовсе) | `§Progress`, `§Book.chapters_done` |
| **§5.13 / Б-13** | отдельный `RunStatus` | **ИСПОЛНЕНО** (6 значений) | `§RunStatus` |
| | правило старшинства | **ИСПОЛНЕНО** — записано в `BookStatus` | `§BookStatus` |
| | `book_id` в `Run` | **ИСПОЛНЕНО** | `§Run.book_id` |
| | судьба `finalizing` | **СНЯТ** | — |
| **§5.14 / Б-5** | `PATCH /books/{id}` только `title` | **ИСПОЛНЕНО** (merge-patch). ⚠ БЕЗ 409 при живом прогоне, вопреки скобке Б-5 — §3.9 | `/books/{bookId}` PATCH |
| | `DELETE /books/{id}` | **ИСПОЛНЕНО** (204, 409 при живом прогоне) | `/books/{bookId}` DELETE |
| | `GET /runs/{runId}` | **ИСПОЛНЕНО** | `/runs/{runId}` |
| | записать явно: правка метаданных — ОТОБРАЖЕНИЕ | **ИСПОЛНЕНО** дословно, с причиной («конфигурация пишется один раз и не перезаписывается») | `/books/{bookId}` PATCH |
| **§5.15 / Б-10+Б-11** | максимум `limit` и подрезание | **ИСПОЛНЕНО:** `maximum: 1000` (= `maxPage`, `pgstore/books.go:587`) + «CLAMPED DOWN, never answered with the default instead» | `§Limit` |
| | объявленный порядок коллекций | **ИСПОЛНЕНО для всех пяти** (библиотека · главы · юниты · замечания · банк) | описания пяти list-операций |
| | `maxItems` у `decisions` | **ИСПОЛНЕНО** (1000) | `§BankDecisionsRequest` |
| **§5.16 / Б-14** | ОДНО правило кодирования отсутствия | **ИСПОЛНЕНО: правило ЗАПИСАНО** отдельным разделом шапки и применено к `Usage.halt_reason`, `Run.finished_at`/`paused_reason`/`failure_reason`, `Book.character_count`/`reject_reason`, `Progress.eta_seconds`, `Unit.note`, `RunOptions.blocked`, `Export.*` | info §Absence of a value |
| | условная обязательность `Unit.target` | **ИСПОЛНЕНО**`if/then` + слова (К-11: генератор `if/then` игнорирует) | `§Unit.target` |
| | имена конвертов | **ИСПОЛНЕНО:** `Library/ChapterList/UnitList/NoteList/Bank``BookPage/ChapterPage/UnitPage/NotePage/BankPage` на общем `Page` через `allOf` | `§Page` и др. |
### 1.4. Правки одной строкой — §5, хвост
| ID | Диспозиция | Где |
|---|---|---|
| Б-17 `TermOrigin` → продуктовый словарь | ИСПОЛНЕНО | `§TermOrigin` |
| Б-18 окно термина | ИСПОЛНЕНО: координаты `structure_version` записаны явно; два смысла нуля разведены на `null` (`minimum: 1`) | `§BankTerm.since_chapter` |
| Б-16 требования деплоя — в примечание | ИСПОЛНЕНО: `HTTP/2 at the edge` и `X-Accel-Buffering` вынесены в компаньон §7; в спеке осталась НАБЛЮДАЕМАЯ норма «поток не буферизуется» + форма heartbeat | `/books/{bookId}/events`, компаньон §7 |
| Б-20 классы стабильности `Id` | ИСПОЛНЕНО (три класса) | `§Id` |
| Б-22 роль имени файла | ИСПОЛНЕНО | `§BookIntake.file` |
| описание `paused_reason` при `null` (К-13-остаток, D39.138 п.4) | ИСПОЛНЕНО: «`null` in every other state» заменено на форму, допускающую `paused` + `null` | `§Run.paused_reason` |
| `Usage.paused_reason` (§4 №18) | ИСПОЛНЕНО: → `Usage.halt_reason` со СВОИМ словарём `AccountHaltReason` (PD-203 развёл уровни), required+nullable | `§AccountHaltReason`, `§Usage.halt_reason` |
| `Export.revision` | ИСПОЛНЕНО | `§Export.revision` |
| кэш-директивы в схему | ИСПОЛНЕНО: `no-store` объявлен на ВСЕХ ответах, как его и ставит `middleware.go:38` | info §Responses common to the whole surface |
### 1.5. §5а — резка
| Кандидат §5а | Диспозиция |
|---|---|
| экспорт (2 операции) | **ОТОЗВАНО владельцем** («экспорт будет») — сделан Б-4 |
| `Problem.instance` | **СНЯТО** |
| `Note.unit_id` | **ОТКАЗ, аргументирован** (§3.3) |
| параметр `limit` | **ОТКАЗ, аргументирован** (§3.4) |
| `EventCeiling` | **СНЯТО целиком** |
| нагрузка 4 кадров из 8 | **ЧАСТИЧНО:** сняты `EventHello.run_id` и `EventResyncRequired.reason`; нагрузка `EventNote.note` и счётчики `EventBank` СОХРАНЕНЫ — аргумент §3.5 |
| `Bank.signed` | **ОТКАЗ, аргументирован** (§3.6) |
| слить формально пять конвертов на `allOf` | **ИСПОЛНЕНО** |
| `EventChapter` = подмножество `Chapter` | **ИСПОЛНЕНО:** выделен `ChapterProgress`, `Chapter` и `EventChapter` собраны из него через `allOf` — переименования `id``chapter_id` больше нет, клиент не разбирает его руками |
| не доводить до финала `listBankTerms`/`listNotes` | **УЧТЕНО:** словарь ступеней не проектировался (К-6), поля `BankTerm` не добавлялись (D39.136 п.4б), `Note.code` НЕ сделан enum до написания фраз, число страницы замечаний не выдумывалось |
| генезис-проза (semver-история, две апологии RFC 9110, объяснение имени `source`) | **ПЕРЕНЕСЕНО в компаньон**, не удалено (промт §5д) |
| зеркальный дефект `/usage` построен и не читается | **ЗАПИСАНО** в таблицу компаньона §3 |
### 1.6. §5б шаги 34 и Б-11а (заказ ПОВЕРХ §5)
| Что | Диспозиция | Где |
|---|---|---|
| правило кадра: применимая дельта ЛИБО счётчик + скоуп; не заставлять перечитывать коллекцию | **ИСПОЛНЕНО**, заменило необязательное «сервер МОЖЕТ склеивать» | `/books/{bookId}/events` → What a frame carries |
| `?after_version=` контрактной формой | **ИСПОЛНЕНО** на `/notes` и `/bank`; грунт — колонки `revision` и индексы `notes_book_revision_idx`/`bank_terms_book_revision_idx`, заведённые с комментарием «Delta reads on reconnect» | `§AfterVersion` |
| разводка «норма vs эксплуатационное примечание» (не упереться в Б-16) | **ИСПОЛНЕНО ПОСЛЕ ИСПРАВЛЕНИЯ.** Первая редакция вынесла сжатие ЦЕЛИКОМ в примечание — нарушение D39.138 п.2(д); поймано опровергателем (A1), исправлено: обязанность честить `Accept-Encoding` и запрет сжимать SSE — норма спеки, слой исполнения — примечание. §3.7 | info §Compression, компаньон §7 |
### 1.7. §5(а)(д) промта — компаньон
| Подпункт | Диспозиция |
|---|---|
| **(а)** снять три опровергнутых утверждения | **ИСПОЛНЕНО.** Все три сняты и названы в ревью-шапке компаньона поимённо, с указанием, чем именно каждое было ложно. Ответ §5 не «поправлен», а ПЕРЕПИСАН: старый ответ приведён как то, что реально давал 0.2.3 (проверено исполнением), новый — как следствие снятия фаз с провода |
| **(б)** таблица «чтение → источник → строка бэклога» + правило «предупреждение несёт номер строки» | **ИСПОЛНЕНО** — компаньон §3, 19 строк; правило поставлено над таблицей |
| **(в)** провенанс-классы ✓/◆/○ под 0.3.0 | **ИСПОЛНЕНО** — компаньон §0, «Пере-разметка 0.3.0»: четыре класса сдвинуты, `Progress` ✓→◆ с объяснением, почему прежняя ✓ и была дефектом |
| **(г)** приложение А на словарь кодов Б-1, слова не заполнять | **ИСПОЛНЕНО.** Карта переведена на «причина движка → КОД контракта → фраза ⬜»; класс 2 схлопнут в один код (нормативно, §8а); коды помечены ◆ и в спеку как enum НЕ занесены. Добавлено приложение А-2 — карта кодов ОШИБОК, заполненная целиком с грунтом `file:line` на каждый |
| **(д)** генезис-проза из спеки — в компаньон | **ИСПОЛНЕНО** (27 блоков перенесено/сжато) |
## 2. Решения, которые я принял сам (промт §4)
### 2.1. Двухуровневый код: `cause` вложенным объектом
Ратифицировано «стабильный корневой + расширяемый ВЛОЖЕННЫЙ». Рассматривал плоский `subcode`
(проще на один объект) — выбрал вложенный: (а) буквально соответствует ратификации, не требуя пинга;
(б) граница «внутри версии / вне версии» видна структурно — всё, что в `cause`, по определению вне
закрытого словаря. Глубина ровно одна: рекурсии Microsoft-овского `innererror` нет и не нужна.
### 2.2. Счёт полосы — в ГЛАВАХ
Три довода в компаньоне §2.5. Коротко: «один счётчик» + «знаменатель — купленный объём» вместе
требуют одной единицы, а купленный объём объявлен в главах и другой единицы у него нет (пересчёт в
деньги на провод не идёт — D39.84; в юниты до разбора неизвестен). Возражение «ноль всю первую волну»
снимает сегментная логика, а не единица счёта, и обе величины у платформы уже есть
(`00002_readmodel.sql:102-105`). Новых колонок батч не требует.
### 2.3. `Note.message` снят, введён `Note.code`
Вариант B ратифицирован для `Problem`. Замечание — то же самое явление: сервер рисовал бы русскую
фразу, не имея ни `Accept-Language`, ни локалей (грепом ноль), а карта «причина → фраза» объявлена
принадлежащей контракту. Промт §5(г) требует карту «КОД → фраза» — у карты кодов должен быть носитель
кода на проводе. Поэтому `code` обязателен, `message` снят, а enum кодов в спеку НЕ занесён, пока
фразы не написаны (строка 148): иначе схема стала бы второй копией незаписанной карты.
**Спорно; вариант «оставить `message`» отвергнут как сохранение ровно того дефекта, который Ф-61 и
называет.**
### 2.4. `promote``approve`
Б-0 называет `promote`/`decline` глаголами оператора майнера (они дословно из
`pipeline/mining.go:201`). Плюс `promote` порождал `status: approved` — два слова на один акт.
`decline` оставлен: обычное слово, к именам файлов майнера не привязано.
### 2.5. Порядок работ
Б-1 исполнен ПЕРВЫМ (требование §3 промта и §8б): словарь кодов класса 1 выведен перечислением
реальных ветвей платформы ДО того, как писались Б-2 (`unsupported_pair`), Б-3 (`missing_or_late`),
Б-8 (`RunFailureReason`), Б-14а (`bank_decisions_incomplete`) и §8а (`content_refused`).
## 3. Спорные формы: варианты и выбор
### 3.1. Прогонный поток НЕ оставлен узким видом
Б-6 рекомендовал «перевесить на книгу, прогонный оставить узким видом». §5 п.5 (ратифицированный
состав) говорит только «перевесить на книгу». **Выбрал: одна операция.** Второй канал с теми же
кадрами — вторая реализация, второй источник расхождения и вторая поверхность там, где §5а требует
резать. Адресация «кадры этого прогона» выводится клиентом, у которого id прогона уже есть из карточки.
⚠ Это единственное место, где я разошёлся с текстом буллета (не с ратификацией). Если оркестратор
считает иначе — возврат стоит одной операции.
### 3.2. `title_raw`/`kind` — паку 161, а не формой сейчас
Промт даёт выбор с требованием аргумента. **Выбрал передачу**, три довода: производителя настоящих
названий нет (строка 160, Этап 0 не построен) · форма узла «глава ↔ фрагмент» и вердикт детекции —
ровно предмет дизайн-пака, заложенная до него форма заморозила бы догадку · добавление аддитивно
(минор), и смены СМЫСЛА `heading` не будет — он и сегодня, и после 160 означает метку из данных книги.
Контр-риск («смена смысла = мажор», контраргумент Б-19) закрыт нормой в спеке: деплою, чей парсер
названий не извлекает, ПРЕДПИСАНО отвечать `null` и ЗАПРЕЩЕНО класть сюда рендеренный порядковый.
### 3.3. `Note.unit_id` оставлен (отказ от резки §5а)
Основание резки — «только в фикстуре мока; экраны берут `unit.note` вложенно». Это дословно тот довод,
который **эррата 16.08-г уже опрокинула** на `TermStatus`: «ни один экран не рисует» подменяет «экрана
ещё нет» (замечания — S6). Собственная фраза операции: «A note addresses a unit or a whole chapter»;
из плоского списка перейти к МЕСТУ замечания больше нечем. Б-9 просил ровно «`chapter_id` обязателен,
`unit_id` опционален» — исполнено буквально, дефект «замечание, не адресующее ничего» закрыт.
### 3.4. Параметр `limit` оставлен (отказ от резки §5а)
§5 п.15 того же заказа требует объявить у `limit` `maximum` и подрезание вместо понижения — у
удалённого параметра максимума не объявишь; два пункта одного носителя противоречат друг другу, и
ратифицированный СОСТАВ (§5) весит больше кандидата на резку (§5а). Плюс «референсный клиент не шлёт
ни разу» — свойство одного клиента, а контракт пишется для второго (`openapi.yaml:564-570` прежней
редакции декларировала именно портируемость).
### 3.5. Нагрузка кадров `note` и `bank` сохранена (частичный отказ)
§5а называет её мёртвой, потому что она «передаётся и игнорируется». Причину игнорирования батч
устранил: у замечания появился `id`, и кадр `note` стал ПРИМЕНИМОЙ ДЕЛЬТОЙ — первая ветка правила
Б-11а. Снятие нагрузки вернуло бы перечитывание всего списка, то есть ровно ту болезнь, которую
Б-11а лечит. Счётчики `bank` — вторая ветка того же правила («счётчик + скоуп»), строки читаются
дельтой. Сняты те две нагрузки, что мертвы и после батча: `EventHello.run_id` (поток стал книжным) и
`EventResyncRequired.reason`.
### 3.6. `Bank.signed` оставлен (отказ от резки §5а)
Тот же класс довода «нет экрана» (подпись — S5). `total`, `signed`, `pending_decisions` — три
НЕЗАВИСИМЫХ факта: строку можно решить и не подписать (отклонить), поэтому `signed` не выводится из
двух других.
### 3.9. `PATCH` без 409 при живом прогоне
Б-5 в скобке рекомендует «409 при живом прогоне». Холодный потребитель показал, что это противоречит
собственному обоснованию операции: если переименование не доезжает до движка и не трогает ничего, что
читает прогон, то конфликтовать не с чем, а клиенту нечего написать в подсказке к задизейбленной на
несколько часов кнопке. **409 снят с `PATCH`.** У `DELETE` он остаётся — там конфликт настоящий:
удалить книгу, против которой идут траты, значит потерять их привязку. Отступление от скобки буллета,
не от ратификации; при несогласии возврат — одна строка.
### 3.7. Разводка «норма / примечание» по НАБЛЮДАЕМОСТИ
Норма в спеке — только то, что клиент видит и обязан уметь: `ETag`/`If-None-Match`/`304`, `Vary` на
согласованном представлении, `no-store` на всех ответах, форма heartbeat, форма `id` кадра, запрет
буферизации потока (наблюдаемое требование вместо вендорного `X-Accel-Buffering`). В примечание
компаньона ушло то, что клиент не наблюдает: HTTP/2 на edge, конкретный заголовок прокси, включение
gzip. Так Б-16 (требования деплоя вон из контракта) и §5б шаги 12 («записать В КОНТРАКТ») не
сталкиваются: в контракт записана СЕМАНТИКА условных чтений, в примечание — способ развернуть.
### 3.8. `type` остался `about:blank`
RFC 9457 §3.1.1 требует использовать `type` как первичный идентификатор. Рассматривал
`urn:textmachine:error:<code>`. **Выбрал `about:blank` + `code`**: URN был бы второй копией `code`, а
URL — обещанием документа, которого мы не серверим. Отклонение названо в спеке вслух. Зато отклонение
0.2.3 от §3.1.3 (`title` показывался пользователю вопреки «advisory … for offline log analysis»)
ИСПРАВЛЕНО.
## 4. Самопроверка исполнением (промт §6)
### 4.1. Линт и «второй клиент»
```
cd frontend && npx spectral lint ../docs/architecture/14-api-contract/openapi.yaml \
--ruleset .spectral.yaml --fail-severity=warn
→ "No results with a severity of 'warn' or higher found!" EXIT=0
```
Прогонялся 6 раз: базовый (на 0.2.3, тоже EXIT=0 — форма команды рабочая и на «до»), после первой
редакции, после де-утечки, после добавления правила отсутствия, после правок по селф-ревью и после
починки дубля ключа, который эти правки внесли (`BankPage.allOf[1].properties` — единственная
поломка, которую линт поймал за всю сессию, и он её поймал).
```
cd frontend && npx openapi-typescript ../docs/architecture/14-api-contract/openapi.yaml \
-o <scratchpad>/schema-probe.ts
→ EXIT=0, 2916 строк
```
Прогонялся трижды: после первой редакции, после де-утечки, после правок по селф-ревью.
**Класс PD-172 проверен генератором, а не мысленно, и проверен ДВАЖДЫ.** Первый прогон: в генерённом
`BookIntake` порядок стал `title? → source_lang → target_lang → file`, файл ПОСЛЕДНИМ (в редакции
0.2.3 генератор ставил `file` вторым, впереди обоих обязательных языков, и произведённая им форма
получала 400). Второй прогон, после находки F1: `title` сделан обязательным, необязательных членов в
схеме не осталось — `title · source_lang · target_lang · file`, и теперь порядок не зависит от того,
эмитит генератор по `properties` или «сначала required». Проверил также, что `$ref`+`description` не
теряют описание (`Run.book_id`, `Problem.cause`, `Blocked.book_id` — на месте); что `Unit.target`
вышел `target: string` (не опциональным), то есть условная обязательность действительно НЕ доезжает
через `if/then`К-11 подтверждён на новой схеме и потому продублирован словами; и что три
восстановленных `enum` (`UnitState`, `RejectReason`, `EventEnvelope.event`) вышли закрытыми юнионами.
### 4.4. Резка прозы — отдельный проход, проверенный типами
Первая редакция батча дала 2587 строк при доле прозы 43 % (у 0.2.3 — 36 %). То есть я исполнил букву
§5а (вынести названный генезис в компаньон) и провалил её предмет: объяснял ПОЧЕМУ прямо в описании
поля, хотя рядом лежит компаньон ровно под это.
Проход по всему файлу: в описании остаётся правило и обязанность потребителя, «потому что» уходит в
компаньон либо снимается. Итог — 2290 строк, проза 1011 → 757 (25 %), доля 43 % → 36 %.
**Что резалась именно вода, а не норма, проверено двумя способами исполнением:**
```
# 1. Генерённые типы до и после резки — с вычеркнутыми JSDoc-комментариями
npx openapi-typescript … -o schema-probe.ts # до
npx openapi-typescript … -o schema-after-cut.ts # после
python: re.sub(r'/\*\*.*?\*/','',t,flags=re.S) на обоих → сравнить
→ TYPE SHAPE IDENTICAL: True
```
Ни одно поле, тип, `enum`, `required` или `oneOf` не сдвинулись — резка коснулась ТОЛЬКО текста.
```
# 2. Множество нормативных утверждений до и после
python: для каждого MUST / MUST NOT / MAY / SHOULD взять модал + 4 следующих слова, сравнить мультимножества
→ 42 → 41
```
Единственная потеря — цитата semver §4 («Major version zero … anything MAY change at any time») из
раздела Versioning. Это ВНЕШНЯЯ цитата, а не наше правило; само правило («пока версия 0.x, минор —
полоса ломающих правок») на месте. Правил не потеряно ни одного.
Что осталось прозой и почему это не вода: самые крупные блоки после резки — словарь `ErrorCode`
(42 строки), протокол потока (30), шапка `info` с четырьмя правилами документа (49), `Problem`,
`Revision`, схема безопасности, значения `RejectReason`/`RunFailureReason`/`BookStatus`. Это и есть
контракт: смысл значения словаря выразить схемой нечем.
### 4.2. Анти-утечка
```
grep -niE "draft|wave|stage names|mined|miner|ruby|finaliz|chunk|langpack|sanitiz|flagged|verdict|prompt|engine|pipeline|glossar|promote|snapshot|CJK" openapi.yaml
→ ровно 1 вхождение: строка 21 — сама формулировка запрета в шапке.
```
Список составлен из слов, названных Б-0, плюс найденные мной самостоятельно: `glossar` (два слова на
одну сущность — «bank» и «glossary»), `snapshot` (у движка это конкретный артефакт ре-биллинга),
`engine`, `pipeline`, `promote`. Пример `«CJK leak in the ru output: 第一节»`, который в 0.2.3 уезжал в
`frontend/src/api/schema.ts:1047`, снят.
### 4.3. Код первичен: что перепроверял руками
Не доверял якорям research/28 — открывал места. Подтвердилось: `fail()` и прямые `WriteProblem`
(`v0.go:238-250,333-345,356,409-423,531,542-574`) · `problem.go:14-28` (`instance` в структуре нет) ·
`reqid.go:16,27` (`X-Request-Id` на каждом ответе) · `middleware.go:38` (`no-store` на всех) ·
`server.go:99,134` (403 и охраняемый 404) · `csrf.go:51-62` (`OPTIONS` освобождён + карваут bearer) ·
`pgstore/books.go:510-511,586-587` (`defaultPage=100`, `maxPage=1000`, понижение до дефолта) ·
`pgstore/books.go:677-681` («глав сделано» уже считается в SQL) · `00002_readmodel.sql:102-105,127-142,
158-199` (обе фазовые колонки главы · чеки `target` · `notes.id`/`created_at`/`unit_id` · `exports.
failed_reason`/`ready_at`) · `runs/runs.go:187-231` (`paused` в allowlist старта, `HasLiveRun` по
своей книге) · `reconcile.go:466-527` (`outcome()`: полоса отказов → `failed`) ·
`pipeline/refusal.go:29-46` + `cmd/tmctl/main.go:50-59` (полоса 1019) · `books/books.go:118-121,141,
355-391` (форма кода, имя файла → титул, расширение → читатель) ·
`frontend/src/api/vocabulary.ts:159-163` (шов ПОТРЕБЛЯЕТ `TermStatus` — эррата 16.08-г верна) ·
`frontend/src/api/client.ts:99-106` (ревизия обхода = минимум).
**Одно расхождение с якорями отчёта:** research/28 Б-7а п.4 указывает `runs.go:229` для «`paused`
входит в допустимые для старта» — сегодня это `readyToTranslate` на `runs/runs.go:227-231`. Смысл
подтверждён, номер уплыл; в компаньоне записан диапазон.
## 5. Адверсариальное селф-ревью (промт §6.5)
Два независимых субагента, author≠reviewer, видели только артефакты, не мой ход мысли:
- **(а) опровергатель полноты** — получил research/28 (§5 · §5а · §5б · §8 · §8а · §6 + ревью-шапку),
D39.138 п.2, канон 0.2.3 и канон 0.3.0. Мандат: «пункты заказа, исполненные неверно или неполно, и
правки СВЕРХ заказа». Этого отчёта и прозы компаньона не получал (сам назвал, что грепал только
заголовки компаньона — его существование несущее для двух вердиктов). Вернул таблицу покрытия по
всем ID заказа + 4 находки класса (A) «не исполнено», 6 класса (B) «сверх заказа», 10 класса (C)
«внутренние противоречия».
- **(б) холодный потребитель** — получил ТОЛЬКО новую спеку, с запретом открывать любой другой файл
репозитория (запрет соблюдён: «хочется контекста, которого в файле нет» он записывал находкой, а
не шёл искать). Мандат: «ты пишешь клиента с нуля». Вернул 47 находок, ранжированных по цене
реализатору.
**Ноль находок не случился ни у одного, и это правильный исход.** Принято и исправлено 26 находок;
четыре из них — дефекты, которые прошли бы в код P7 и стоили бы там на порядок дороже (F1, F8/F9,
F32, C2). Обе линзы независимо нашли одно и то же в трёх местах (структурная версия · `X-TM-Client`
против bearer · `Book` без ревизии в ответе на запись) — это и есть подтверждение, что находки не
шум.
### 5.1. Исправлено (26 позиций)
| # | Линза | Находка | Что сделано |
|---|---|---|---|
| **F1** | б | **`BookIntake.required` ставит `file` перед необязательным `title`.** Генератор «сначала required, потом optional» — обычный дефолт там, где язык не даёт положить параметр с дефолтом первым, — кладёт `title` ПОСЛЕ файла, и форма получает 400. Тот же класс PD-172 в новой маскировке: моя перестановка `properties` защищала одно семейство генераторов и была опрокинута массивом `required` тремя строками ниже | **Класс убит структурно, а не прозой:** `title` сделан ОБЯЗАТЕЛЬНЫМ, пустая строка = «назови по файлу». В схеме не осталось НИ ОДНОГО необязательного члена — значит нет порядка, который генератор мог бы выбрать и нарушить правило. Перепроверено генератором: `title · source_lang · target_lang · file` |
| **F2** | б | **У `UnitState` пропал `enum`** — регрессия, внесённая моей правкой описания | Восстановлен |
| **F3** | б | **У `RejectReason` пропал `enum`**, при том что проза называет словарь закрытым | Восстановлен, 4 значения |
| F4 | б | `EventEnvelope.event` — восемь имён кадров живут только в markdown-таблице | `enum` из 8 имён |
| F5 | б | `limit`: схема «выше 1000 невалидно», проза «подрезается», словарь ошибок несёт `out_of_range` | Записано: подрезается, НЕ отвергается; «максимум, объявленный здесь» |
| F6 / C9 | б, а | `X-TM-Client` `required: true` против прозы «предъявитель bearer освобождён» | Записано: required — потому что браузерный клиент обязан всегда; bearer освобождён схемой безопасности |
| **F8 / F9** | б | **Дельта-чтение строго `>`, а `Revision` требует catch-up `>=`; кадр `bank` предписывает читать строки «ревизией этого кадра» — что при строгом сравнении ВСЕГДА возвращает пустоту.** Основной поток всей банковской фичи | `?after_version=` сделан ВКЛЮЧИТЕЛЬНЫМ (та же причина, что у catch-up: одна транзакция — одна ревизия, но НЕСКОЛЬКО строк); кадр `bank` переписан на «ревизию, которую клиент применил последней»; записано правило водяного знака |
| F10 | б | Рукопожатие проверяет МАЖОР, а 0.x ломает миноры — два правила документа исключают друг друга | Проверка версии сделана минор-чувствительной, пока мажор `0`; то же на `Capabilities.contract_version` |
| F11 | б | Переименование объявлено бесплатным («не доезжает до движка») и тут же отвечает 409 при живом прогоне | **409 снят с `PATCH`** — конфликтовать не с чем (§3.9). У `DELETE` остаётся: там конфликт настоящий |
| F12 | б | «Клиент никогда не опрашивает read-модель» — неправда: ни один кадр не несёт книжных величин библиотеки, а поток книжный | Формулировка приведена к правде: read-модель не опрашивается, чтобы ОБНАРУЖИТЬ изменение; читается по кадру/навигации/фокусу, и условные чтения делают это дёшево. Отсутствие ленты на библиотеку записано направлением в компаньон |
| F13 | б | `BankTerm.id` объявлен переживающим пересборку и выводится из окна, которое ПЕРЕ-РАЗБОР двигает | Записано прямо: пере-разбор он НЕ переживает; при смене `structure_version` клиент перечитывает банк и не считает решения перенесёнными |
| F14 / C8 | б, а | «CORS-слоя нет вовсе» против 403 по origin; плюс 403 по origin срабатывает БЕЗ куки, а описание требует сессии | Обе формулировки уточнены по коду (`csrf.go:52,62,65-66`) |
| F15 | б | `stopped` одновременно «выводится контрактом» и значение `RunStatus` | Записано: выводит ПЛАТФОРМА, клиент читает поле и никогда не вычисляет |
| F16 | б | Правило «дропать устаревшее чтение», прочитанное буквально, ломает опрос экспорта | Правило сужено: сравнение ПО СКОУПУ и ПО КОЛЛЕКЦИИ; опрос экспорта не дропается |
| F17 / C10 | б, а | Ответы `POST`/`PATCH` возвращают `Book`, у которого нет `revision` — упорядочить их против кадра нечем | Добавлен `Book.revision` (ревизия КНИГИ, не библиотеки) |
| F18 | б | Меняется ли `ETag` от `limit`/`cursor`/`after_version` | Записано: валидатор привязан ко всему запросу, строку запроса включая |
| F19 | б | Нет карты «код → статус» | Каждое значение `ErrorCode` получило свой HTTP-статус |
| F20 | б | Нельзя узнать ДО загрузки, принимает ли деплой книги (404 неотличим от прочих 404) | `Capabilities.intake_enabled` |
| F21 | б | Можно ли экспортировать недопереведённую книгу | Записано: можно; что окажется внутри — работа S7 |
| F22 | б | `{"title": null}` в merge-patch — удаление или 400 | Записано: 400 |
| F23 | б | Едут ли агрегаты банка на ДЕЛЬТА-чтении | Записано: да — «любой ответ на запрос без `cursor`, дельта включая»; и всегда описывают весь банк |
| F24 | б | `Export.url` — свой origin? под кукой? `fetch` или навигация? | Записано: свой origin, браузер НАВИГИРУЕТ; это загрузка, а не вызов |
| F25 / F26 | б | Что с потоком при удалении книги; сколько потоков держать | Оба записаны нормой: удаление закрывает поток и реконнект → `404`; один поток на книгу, ленты на библиотеку нет и открывать поток на строку списка клиент не должен |
| F27 / F39 | б | `Retry-After` обещан на `key_in_flight` и объявлен только на опросе экспорта | Объявлен на ответе `Conflict` |
| F28 | б | Показывать ли `request_id` пользователю | Записано: MAY; идентифицирует запрос, не человека |
| F29 | б | `number` и `heading` оба `null` | Записано: метка — из позиции в порядке чтения |
| **F30** | б | `Unit.note` единственное, а у пары легально несколько замечаний | `Unit.note``Unit.notes` массивом; это же согласуется с моим собственным правилом «пустая коллекция — пустой массив» |
| **F31** | б | Кадр `status` несёт только `paused_reason`: о `rejected` и `failed` объявляет, а причину не даёт — при том что обоснование самого кадра «это ровно тот момент, когда экран обязан сказать почему» | `EventStatus` получил `reject_reason` и `failure_reason` |
| **F32 / F33 / C7** | б, а | **Ничто не сообщает, что `structure_version` сдвинулась** — версия была только в `hello` и двух конвертах; `BankPage`/`NotePage` не говорят, к какой структуре относится их содержимое. Две обязанности документа («перечитать банк», «уронить якоря на пары») остались без триггера | `structure_version` — на КАЖДОМ кадре (`EventBase`) и в `BankPage`/`NotePage` |
| F34 / F35 | б | Дельта фильтрует по «версии строки», которой у строк нет; и нечем ответить на устаревший водяной знак | Водяной знак = `revision` конверта (записано); устаревший → `400` `cause.code: version_too_old` — тот же ответ и тот же смысл, что у мёртвого курсора |
| F36 / C3 | б, а | `500` не объявлен нигде, а `internal_error` — в закрытом словаре кодов | Записано: `500` сознательно не перечисляется по операциям (§4 №1, Zalando «do not document»), но тело у него то же и код тот же |
| F37 | б | Нет восстановления экспорта после перезагрузки; нет `Idempotency-Key` на создании | `Idempotency-Key` добавлен на `POST /exports`; обоснование `format` переписано (списка экспортов нет) |
| F38 | б | `Last-Event-ID` и `X-Request-Id` живут только в прозе — генерённый клиент их не увидит | `Last-Event-ID` объявлен параметром `streamBookEvents`; `X-Request-Id` — компонент-заголовок на всех девяти ответах-ошибках |
| F41 | б | `TermStatus.in_progress` недостижим | Записано: это состояние СТРОКИ; `POST /bank/decisions` его не ставит |
| **F42** | б | **Замечания объявлены «в порядке чтения», а ключа порядка на проводе нет:** кадр `note` — единственный, который нельзя потерять, — вставить некуда, и дельта-чтение слить не с чем | Порядок переобъявлен: `created_at`, затем `id` — единственный тотальный порядок, который считают обе стороны; порядок чтения строит клиент по дереву, которое у него уже есть |
| F43 | б | Нет чтения одной главы; кадр `chapter` про неподгруженную главу некуда деть | Записано: такой кадр ИГНОРИРУЕТСЯ — кадр не повод пейджить коллекцию. Операция — направлением к паку 161 |
| F45 | б | Нет состояния «останавливается»: `202` возвращает живой статус, кнопка включается обратно | Записано: `202` не значит «остановлен», значения `stopping` нет, клиент держит своё pending до кадра `status` |
| F46 | б | `Blocked` называет книгу id, а не именем | Записано: клиент читает карточку той книги |
| F0 | б | Из спеки нельзя сделать НИ ОДНОГО успешного вызова: оба способа аутентификации недостижимы из неё | Названо место входа (`GET /auth/login`, вне версионного префикса) прямо в разделе Transport |
| **A1** | а | **Сжатие ратифицировано «В КОНТРАКТ» (D39.138 п.2д), а спека вынесла его ЦЕЛИКОМ в примечание** — я перестарался с разводкой по Б-16 | Обязанность честить `Accept-Encoding` на JSON и НЕ сжимать `text/event-stream` — норма спеки; в примечании остался только слой исполнения, которого клиент не наблюдает |
| A4 | а | §8а требует класс 2 «с лимитом попыток на аккаунт» — носителя не было нигде | Записано на `content_refused`: сервер ограничивает число попыток одного аккаунта, граница его и на провод не идёт |
| C1 | а | Правило отсутствия объявляет ОДНО исключение, а их два (`Problem` и агрегаты `BankPage`) | Названы оба |
| **C2** | а | **`410 Gone` возвращает `Problem`, у которого `code` — закрытый словарь без значения `gone`:** конформный сервер не может ответить конформным 410 | `gone` добавлен в `ErrorCode` |
| C5 | а | `page_size_default` без максимума может превысить максимум, который клиент вправе ЗАПРОСИТЬ | `maximum: 1000` |
| **C6** | а | Правила докачки опираются на тождество, которого `id` кадра не несёт: «позиция в потоке» не уникальна между потоками одной книги, а обе нормы (буфер докачки и `204`) решают именно «тот ли это поток» | `id` переопределён как позиция в истории событий КНИГИ; `204` переформулирован без «поток, названный `Last-Event-ID`» |
| C4 / B1 | а | `Chapter.units_done` как сегментный счётчик противоречит «`Book.chapters_done` никогда не идёт назад» и не определён для главы ВНЕ текущего сегмента | Формулировка уточнена: счётчик ПРОХОДА (`0` — не дошли, `units_total` — прошли, глава вне прохода держит прежнее) + явная разводка с `Book.chapters_done`. Сама сегментная логика — прямой заказ промта батча §3, а не самодеятельность: опровергатель промта не видел |
### 5.2. Отклонено — с причиной (12 позиций)
| # | Линза | Находка | Почему отклонено |
|---|---|---|---|
| A2 | а | Три строки таблицы §5а не вырезаны: `Note.unit_id`, `Bank.signed`, нагрузка кадров | **Диспозиция была записана ДО ревью** — §3.3, §3.5, §3.6 этого отчёта и §6а компаньона, с контраргументом на каждое. Опровергатель отчёта и прозы компаньона не видел по условию мандата; его вердикт «без прикрытия» основан на неполном входе. Аргументы стоят |
| **A3** | а | Файл вырос на 63 %, доля прозы выросла | **ПРИНЯТО и ИСПРАВЛЕНО отдельным проходом** (§4.4), после того как владелец назвал ту же вещь прямо. Итог: 2587 → 2290 строк, проза 25 %, доля 43 % → 36 % — ровно уровень 0.2.3, при форме, выросшей на 61 %. Проверено, что резалась вода: генерённые типы до и после резки идентичны с точностью до JSDoc, нормативных правил потеряно ноль. Заказанное содержание НЕ сокращалось |
| B2 | а | `Book.reject_reason` сделан обязательным вопреки §4 №22 | Сам опровергатель называет правку justified: единое правило отсутствия (§5 п.16 того же заказа) не держится с неперепроверенным исключением, а прежний довод («деплой старше этого минора») внутри ЛОМАЮЩЕГО минора пуст |
| B3 | а | `Note.message` снят, введён `Note.code` | Признано justified самим опровергателем; аргумент — §2.3. Вынесено вопросом в §8 |
| B4 / B6 | а | Контентность `BankTerm.id` записана; дисциплина Б-21 применена в спеке | Обе — прямые «стоит записать» из research/28 §3 п.6 и Б-21 |
| B5 | а | Новый MUST NOT на `heading` при открытом К-2 | Кодифицирует собственное предупреждение движка (`manifest.go:80-86`). Без него P7 положил бы в поле рендеренный порядковый, и смысл поля сменился бы после строки 160 — то есть мажор |
| F7 | б | Четыре `code`-поля — свободные строки без словаря | По замыслу: `cause.code`/`ErrorItem.code` открыты специально (это и есть механизм расширяемости варианта B), `Note.code`/`Export.failure_code` не enum, пока нет источника — §6 obstacle |
| **F40** | б | **`TermStatus` не имеет значения для ОТКЛОНЁННОГО термина** — экран подписи не покажет, что уже отклонено | **НЕ ИСПРАВЛЕНО СОЗНАТЕЛЬНО, вынесено вопросом владельцу** (§8 п.4): лечение — одно поле `BankTerm.decision`, а «добавочные поля `BankTerm` НЕ заводить» — слово владельца 15.08 (D39.136 п.4б), повторённое промтом батча. Право сказать «этого делать не надо» есть, права молча сделать иначе — нет |
| F44 | б | `localized` — механизм, который ни один код не использует | Ратифицированный слот (§8 п.4, форма `google.rpc.LocalizedMessage`); помечен в спеке словами и носителем, чтобы предупреждение не пережило своё основание |
| — | б | «`Intl.DisplayNames` — браузерный API в контракте, а bearer заявлен для не-браузера» | Верно как замечание. `LangCode` отдаёт КОД; чем клиент рисует имя — его дело, и упоминание одного API в описании ни к чему не обязывает. Оставлено как есть |
| — | б | Три дизайн-возражения: двухшаговая загрузка · `X-TM-Client` параметром, а не обязанностью схемы · `Book` против `BookDetail` в ответах на запись | Первое — направление вне батча (§5 «не в батч»); второе — прямой заказ Б-15 («объявить машинно», иначе правило видно только человеку); третье — форма ответа на запись, менять её не заказано |
### 5.3. Третья линза — КРОСС-МОДЕЛЬНАЯ (fable), по слову владельца
Обе линзы §5.15.2 — одного семейства со мной, и их согласие независимым подтверждением не считается
(тот же долг записан у research/28 §10 и строкой 190). По слову владельца добавлен проход агентом
ДРУГОЙ модели по диффу `0.2.3 → 0.3.0` и по заказу; входы те же артефакты, отчёт и промт не давались.
Мандат отличался прицелом: (1) что будет отлито в код P7 неверно · (2) смысловые дефекты, проходящие
линтер · (3) сверка с заказом · (4) **не потеряна ли норма при резке прозы** — отдельный риск, которого
у первых двух линз не было, потому что резка случилась после них.
Результат: **1 HIGH и 4 MEDIUM, все — дефекты моей новой формы; ни одного неисполненного пункта
заказа.** Все девять находок приняты и исправлены.
| # | Находка | Что сделано |
|---|---|---|
| **HIGH-1** | **`id` кадра объявлен «позицией в истории КНИГИ, строго возрастающей, по одному на кадр» — а `hello` приходит первым на КАЖДОМ подключении, `end` и `resync_required` тоже принадлежат соединению.** У реализатора два пути, и оба против текста: минтить служебным кадрам книжные номера (два одновременных зрителя тратят номера друг друга, `Last-Event-ID` одного указывает на кадры, которых второй не видел) либо повторять последний номер (тогда «one per frame» — ложь). На этом же `id` стоят правило `204` и клиентский гард, то есть ошибка отливается в миграцию, воркер и шов клиента разом | Разведено: служебные кадры несут id последнего кадра ИСТОРИИ и своего номера НЕ тратят, повтор id на них легален; клиент хранит последний увиденный id и отправляет обратно, но им не считает |
| **MED-2** | **`limit`: схема (`maximum: 1000`) приказывает отвергнуть то, что проза приказывает подрезать.** Сервер, собранный со схемной валидацией запросов (oapi-codegen — кандидат P7), ответит 400 на `limit=5000` и будет конформен схеме и неконформен собственной прозе | Записано явно: `maximum` бьёт по тому, что вправе ЗАПРОСИТЬ КЛИЕНТ; сервер, получивший больше, MUST подрезать и MUST NOT отвергнуть, а деплою со схемной валидацией предписано вывести параметр из-под отказа |
| **MED-3** | `createExport` несёт `Idempotency-Key`, чья семантика без `409` неисполнима, а `409` в ответах нет (у `createBook` и `startRun` есть) | `409` добавлен |
| **MED-4** | Машина состояний экспорта недоопределена: `ready` объявлен терминальным ⇒ `expired` недостижим и значение мертво; `expires_at` описан как `null` вне `ready` ⇒ в состоянии `expired` поле «когда истекло» обязано быть пустым | Переписано: `pending → ready → expired` либо `pending → failed`; опрос останавливается на трёх, но терминальность опроса и терминальность состояния разведены; `expires_at` заполнен там, где артефакт существует — в `ready` И в `expired` |
| **MED-5** | Порядок замечаний, который я ввёл по находке F42, опирается на сравнение `id` как тай-брейка — а `Id` тем же документом ЗАПРЕЩАЕТ сортировать по id; вдобавок лексикографический порядок строк и серверный совпадать не обязаны, так что «единственный порядок, который считают обе стороны» фактически неверно | Тай-брейк снят с клиента: сервер разрешает ничьи по-своему, клиент этого НЕ воспроизводит и ставит пришедшее кадром замечание после всех, что держит с тем же `created_at`. Запрет сортировки по `Id` остаётся в силе |
| LOW | `page_size_default` несёт `maximum: 1000` внутри абзаца, отрицающего вторую копию числа | Формулировка переписана: бound — следствие единственного определения на `limit`, а не вторая копия |
| LOW | «MUST tolerate an unknown value under a minor bump» недостижимо весь 0.x: клиенту велено отказываться при любом расхождении минора, значит ветка толерантности физически не получит значения | Названа полоса, для которой правило написано: аддитивные миноры (с 1.0) и деградация внутри версии; в 0.x это пол, а не разрешение работать против чужого минора |
| LOW | `Book.chapters_done` «никогда не идёт назад» — необеспечимый абсолют: пере-разбор пересчитывает и его, и `chapter_count` | Сужено до «внутри одного `structure_version`» |
| **MED (резка)** | **Потеряно правило «клиент не читает пропуск номера как потерянный кадр»** (было в 0.2.3) — и потеряно ровно тогда, когда стало НУЖНЕЕ: в 0.2.3 `id` был ревизией и дыры были легальны по построению, в 0.3.0 `id` — «по одному на кадр», а склейка создаёт настоящие дыры. Клиент, дорожащий несклеиваемыми `note`, прочитал бы дыру как потерю и ушёл в лишний resync | Возвращено на `EventEnvelope.id` вместе с явным «дыра легальна» |
| LOW (резка) | Потеряно «книга легально не имеет глав вовсе» — единственное место, где пустое дерево объявлялось законным | Возвращено в `listChapters` |
| LOW (резка) | Потеряны числа интейка «16 частей / поле ≤ 1 КБ» без переезда в `Capabilities` | **Отклонено осознанно:** Б-3 показал, что эти два числа в спеке и в коде описывали РАЗНЫЕ правила (код считает части ДО файла, спека — все), а комментарий кода называет их «не делом контракта». Клиент узнаёт факт кодами `too_long`/`too_many_parts` в `errors[]`; превалидировать ему нечего — он шлёт четыре части |
| LOW | Компаньон (приложение А-2) давал `content_refused` как «400/409», спека — только 400 | Компаньон исправлен |
**Что третья линза подтвердила независимо** (ценно именно потому, что модель другая): исполнение Б-0
по ЭРРАТЕ, а не по опрокинутой рекомендации (`TermStatus` — ось, `TermOrigin` — переименование) ·
чистоту греп-листа анти-утечки, включая проверку СГЕНЕРЁННЫХ типов (`wave|draft` = 0) · что основание
переписанного resume-абзаца верно по коду (`pgstore/runs.go:672-676`, `runs/runs.go:227-231`,
`reconcile.go` ветка `stopped`) · что `Book.chapters_done` действительно Ц1 (величина уже в SQL) ·
что baseline диффа аутентичен (`old.yaml` = байт-зеркало фронта, ещё 0.2.3).
**Что она подняла на ратификацию, а не на исправление:** частичный отказ от резки §5а (четыре строки,
каждая с контраргументом) — «должен окнуть оркестратор, а не констатировать батч»; и `Note.message`
`Note.code` как решение класса «политика фраз замечаний», принятое батчем без слова владельца. Оба
уже стояли в §8 ниже; после этой линзы они там усилены, а не добавлены.
## 6. Obstacle reporting — что НЕ удалось и не сделано
1. **Гейт на утечку конвейера не построен** — и не должен был: тест по образцу `generality.test.ts`
живёт в зоне фронта, которая заморожена. Я записал ПРАВИЛО и список грепа в компаньон и прогнал
греп руками; автоматической защиты до разморозки нет. Носитель — пинг фронту 16.08.
2. **Число страницы замечаний не изменено и не могло быть.** Замера масштаба нет ни одного (research/28
§7, §10: «сколько замечаний даёт настоящая книга, не знает никто»). Я снял ЧИСЛО из спеки в
`Capabilities.page_size_default` (это правильное место для значения деплоя) и оставил оговорку в
спеке и в компаньоне. Новое число не выдумано.
3. **Дельта-чтение и структурная версия были дефектны в первой редакции батча** и починены только
селф-ревью (F8/F9, F32). Это не «удалось», а «поймано вторым рубежом»: механизм, который Б-11а
заводит ради экономии 372 МБ, в первой моей редакции возвращал пустое множество в своём главном
сценарии, а триггера «структура сдвинулась» не существовало. Обе ошибки — мои, обе были бы отлиты
в код P7.
4. **`Note.code` не стал enum.** Карта «код → фраза» пуста, фразы пишет владелец (строка 148).
Enum-словарь без фраз был бы формой без источника — тот самый класс, от которого предостерегает
§5а-оговорка. Коды предложены в компаньоне (◆), enum появится вместе с фразами.
5. **`Export.failure_code` не enum** — по той же причине: способы упасть зависят от форматов, которых
не существует (S7). Носитель записан в самой схеме.
6. **Рантайм не поднимался.** Ни платформа, ни фронт не запускались: платформа 8 из 20 операций не
отвечает вовсе, а фронт заморожен и живёт на моках. Всё, что я утверждаю о поведении сервера,
выведено чтением кода (§4.3), а не наблюдением. Соответственно НЕ проверено исполнением:
реальная форма `problem+json` при 413/408 посреди тела; поведение `Idempotency-Key` (не построен);
условные чтения (не построены).
7. **Зеркало фронта расходится с каноном на момент сдачи** — намеренно (зона заморожена, D39.136 п.2).
`cmp` при лендинге ОБЯЗАТЕЛЕН, порядок — канон первым, зеркало отдельным зонным коммитом
(D39.138 п.3).
8. **Кросс-СЕМЕЙНОЙ проверки по-прежнему нет, кросс-МОДЕЛЬНАЯ есть.** Третья линза (§5.3) — другая
модель того же семейства, по слову владельца; она нашла HIGH, который две предыдущие пропустили,
то есть разнос по модели окупился. Настоящий не-Claude проход при границе «$0 по внешним
провайдерам» остаётся невозможным — долг записан строкой 190.
9. **Платформе и фронту прибавилось работы, и это не оценено мной в часах.** Батч ломает
`wireProgress`, `projectBook`, `projectRun`, словари банка, форму `Problem` и путь потока; фронту —
`format.ts`, `About.tsx`, `Status.tsx`, `vocabulary.ts`, `contract.ts`, все моки. Оценка — за зонами.
## 7. Дифф-сводка «было → стало» по операциям
| Операция | Было | Стало |
|---|---|---|
| `getCapabilities` | — | **НОВАЯ**: версия · пары · порог · форматы · размер страницы |
| `listBooks` | `Library` | `BookPage` (на `Page`), `ETag`/`304`, объявлен порядок |
| `createBook` | `file` вторым в схеме; часть после файла молча терялась; идемпотентности нет; `Location` нет | `file` последним; часть после файла → 400 + `errors[]`; `Idempotency-Key`; `Location`; `409`; отказ по паре кодом |
| `getBook` | `BookDetail` | + `ETag`/`304`; `run` стал required+nullable |
| `updateBook` | — | **НОВАЯ**: merge-patch, только `title`; принимается и при живом прогоне |
| `deleteBook` | — | **НОВАЯ**: 204, 409 при живом прогоне |
| `listChapters` | «Default page size 5000» в прозе | `ChapterPage` + `structure_version` + `ETag`/`304`; число ушло в `capabilities` |
| `listUnits` | — | + `structure_version`, `ETag`/`304`, **`410 Gone`** |
| `listNotes` | `NoteList`; «в порядке чтения» без ключа порядка | `NotePage` + `structure_version` + `?after_version=` + `ETag`/`304`; порядок `created_at`,`id`; `Note` получил `id`/`created_at`/`code`, `chapter_id` стал обязательным, `message` снят |
| `listBankTerms` | `Bank`; предупреждение «канала нет» | `BankPage` + `?after_version=` + `ETag`/`304` + `pending_decisions`/`complete`; агрегаты на первой странице; предупреждение снято |
| `submitBankDecisions` | `promote`/`decline`, `decisions` без `maxItems` | `approve`/`decline`, `maxItems: 1000` |
| `getRunOptions` | `{ceiling}` | + `blocked: {code, book_id}` |
| `startRun` | `verify_bank`; 409 без причины | `stop_for_signing`; `Idempotency-Key`; 409 несёт `blocked`; названо лечение «новый прогон с бОльшим потолком» |
| `streamRunEvents`**`streamBookEvents`** | поток на ПРОГОНЕ; конца нет; склейка без ограничений; `id` = ревизия; 8 кадров | поток на КНИГЕ; кадр `end` + `204`; склейка только состояний; `id` = позиция, ревизия в `data`; `EventCeiling` снят, `EventEnd` добавлен |
| `getRun` | — | **НОВАЯ** |
| `stopRun` / `resumeRun` | `Run` без `book_id`; «MUST NOT offer resume» и никакого лечения | `Run` с `book_id`/`failure_reason`/`progress`; 409 c `cause.code`; лечение названо |
| `getUsage` | `paused_reason` необязателен, типизирован прогонным enum | `halt_reason` required+nullable, свой `AccountHaltReason` |
| `createExport` / `getExport` | `{id, ready, url?}`; опрос не завершался | `{id, revision, state, format, expires_at, failure_code, url}`; формат из `capabilities`; правило доступа к ссылке |
**Проход резки прозы (§4.4):** 2587 → 2290 строк, доля прозы 43 % → 36 %; типы не изменились ни на
поле.
**Прочее, изменённое селф-ревью:** `BookIntake.title` стал ОБЯЗАТЕЛЬНЫМ (пустая строка = «назови по
файлу») — в схеме не осталось необязательных членов, и порядок частей больше не зависит от повадки
генератора · `Unit.note``Unit.notes` массивом · `Book` получил `revision` · `EventBase` получил
`structure_version` · `EventStatus` получил `reject_reason` и `failure_reason` · `ErrorCode` получил
`gone` · `Capabilities` получил `intake_enabled` · `?after_version=` стал включительным.
**Снято с провода:** `Book.genre` · `BookIntake.genre` · `Book.progress` · схема `Counter` ·
`BookStatus.finalizing` · `Problem.instance` · `EventCeiling` · `EventHello.run_id` ·
`EventResyncRequired.reason` · `Note.message` · движковые значения `TermStatus`/`TermOrigin` ·
операция `streamRunEvents`.
## 8. Вопросы оркестратору (через владельца) — не интерпретировал
1. **§3.1** — прогонный поток снят вместо «оставить узким видом». Если это чтение §5 неверно,
возврат стоит одной операции.
2. **§2.3 / 3.3** — `Note.message` снят в пользу `Note.code`, `Note.unit_id` оставлен вопреки §5а.
Оба — мои чтения ратифицированного принципа, а не буквы носителя.
3. **`content_refused` в `RejectReason`** заведён под непостроенный прескрин (ПТ-16, строка 94). Это
ратифицировано К-9, но остаётся значением словаря без сегодняшнего писателя — если правило
«форма без источника» весит больше, значение снимается одной строкой.
4. **Экран подписи не может показать, ЧТО уже отклонено** (находка F40 холодного потребителя; не
исправлена сознательно). Чтение банка отвечает `pending_decisions`/`complete` — сколько осталось,
но не КАКИЕ строки решены: `TermStatus` это состояние строки банка, а решение живёт отдельной
таблицей `bank_decisions` (`00002_readmodel.sql`) и на провод не проецируется ни одним полем.
Экран, перезагруженный посреди стопа, знает «осталось 17 из 300» и не знает, какие семнадцать; на
сотнях строк это разница между «доделать» и «пройти заново». Лечение — ОДНО поле
`BankTerm.decision` (`approve`/`decline`/`null`), Ц0: чтение банка не построено.
**Не сделано, потому что «добавочные поля `BankTerm` НЕ заводить» — слово владельца 15.08
(D39.136 п.4б), повторённое промтом батча §3.** Моё чтение: тот запрет закрывал вопрос ПТ-33 о
полях СМЫСЛА («смысла хватает, но не слишком подробно»), а это поле — состояние подписи, то есть
предмет Б-14а, который в батч входит. Но чтение — не право сделать иначе. Вопрос владельцу; если
ответ «заводить», правка стоит четырёх строк схемы и одной строки P7.