# Отчёт: батч 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** | | строк файла (`wc -l`) | 1444 | **2463** | | форма (структурные строки) | 760 | **1183** | | проза (строки внутри описаний) | 540 | **1055** | | доля прозы | 42 % | **47 %** | | причина отказа на проводе | английская фраза, показываемая «as-is» | машинный `code` (16 корневых) + `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` | Команды, которыми получены числа (пере-ран после ПОСЛЕДНЕЙ правки дофикса — требование ФБ-10): ``` grep -c 'operationId:' openapi.yaml # 16 → 20 grep -cE '^ /' openapi.yaml # 15 → 17 wc -l openapi.yaml # 1444 → 2463 awk-счёт значений ErrorCode # → 16 python3 /tmp/prose_form.py (скрипт приложен в §4.4) # проза/форма обеих версий git show 1022567:…openapi.yaml > /tmp/old.yaml # снимок «было» для всех сверок ``` **Форма выросла на 56 %, проза — на 96 %; доля прозы выросла с 42 % до 47 %.** Это ответ на вопрос §8 п.3 «не переусложнён ли контракт», и он честнее прежнего: батч добавил четыре операции и шестнадцать схем, а каждая находка трёх линз селф-ревью и десять пунктов дофикса закрываются ПРАВИЛОМ — а правило это проза. Прежний вывод «доля не сдвинулась» держался на мерке, где строка `description: |` считалась формой; скрипт теперь приложен (§4.4), число можно оспорить командой, и утверждать «вернул к уровню 0.2.3» я больше не могу. Что резалась вода, а не норма, доказано отдельно и от мерки не зависит: типами и мультимножеством модальных глаголов (§4.4). ## 1. Записка-план: каждый ID заказа с диспозицией Комплектность сверялась механически против носителя, не по памяти. ### 1.1. Ядро — research/28 §5, пункты 0–4 | 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` (16 корневых, закрытый) + `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, пункты 5–10 | 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, пункты 11–16 | 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` — спроецировано с полосы отказов движка 10–19 (`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` | | **Б-23 / §3 промта** | жанр — выкинуть из `BookIntake` и `Book` | **ИСПОЛНЕНО** (слово владельца 16.08). Поля нет ни в интейке, ни в карточке; остальные половины удаления — снятие `{{genre}}` из промптов пары и из канона брифа — строка 184 и НЕ моя зона. ⚠ Строка отсутствовала в записке-плане первой редакции и дописана дофиксом (ФБ-10) | `§BookIntake`, `§Book` | | **К-10 / D39.138 п.5** | пофазность у главы — НЕ строить | **ИСПОЛНЕНО КАК «НЕ СТРОИТЬ».** Фазы не выходят на провод ни у книги, ни у главы: `ChapterProgress.units_done` — один счётчик «в текущем сегменте». Колонки `units_draft_done`/`units_edit_done` read-модели остаются внутренним делом платформы. Строка дописана дофиксом (ФБ-10) | `§ChapterProgress` | | **В-6 / D39.138 п.2з** | `blocked: {code, book_id}` на run-options и 409 | **ИСПОЛНЕНО** обоими носителями: `RunOptions.blocked` (required, nullable) и расширение `Problem.blocked` у `ceiling_unavailable`; `cause.code: credit_held` называет причину, `book_id` — держателя. Строка дописана дофиксом (ФБ-10) | `§RunOptions.blocked`, `§Blocked`, `§Problem.blocked` | | **§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д промта, аргумент в §1.7(д) | | зеркальный дефект `/usage` построен и не читается | **ЗАПИСАНО** в таблицу компаньона §3 | ### 1.6. §5б шаги 3–4 и Б-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` на каждый | | **(д)** генезис-проза из спеки — в компаньон | **ОТСТУПЛЕНИЕ, аргумент.** Фактически проза УЖАТА НА МЕСТЕ до ссылки на источник (`semver §4`, одна строка про `Retry-After` на `200`), а не перенесена в компаньон: переносить в спутник абзац, от которого осталась ссылка, значит плодить третью копию. Проверяется грепом — этих абзацев в компаньоне нет. Ярлык «ИСПОЛНЕНО (27 блоков перенесено)» был неверен и исправлен дофиксом (ФБ-10); компаньон §6а поправлен там же | ## 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:81,243`). Плюс `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б шаги 1–2 («записать В КОНТРАКТ») не сталкиваются: в контракт записана СЕМАНТИКА условных чтений, в примечание — способ развернуть. ### 3.8. `type` остался `about:blank` RFC 9457 §3.1.1 требует использовать `type` как первичный идентификатор. Рассматривал `urn:textmachine:error:`. **Выбрал `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 /schema-probe.ts → EXIT=0; на заленденном каноне 2608 строк, после дофикса 16.08 — 2798 ``` Прогонялся трижды: после первой редакции, после де-утечки, после правок по селф-ревью. **Класс 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. Резка прозы — отдельный проход, проверенный типами ⚠ **Пере-счёт 16.08 (ФБ-10): исходный вывод «доля прозы не сдвинулась» держался на методе, который не был приложен, и другим методом не воспроизводится.** Скрипт приложен ниже, чтобы число можно было оспорить командой, а не поверить на слово: ```python # /tmp/prose_form.py — проза = строки блочных скаляров description/summary (и строка ключа), # форма = всё прочее непустое; конец блока определяется по возврату отступа на уровень ключа. import sys, re def measure(path): lines = open(path).read().split('\n'); prose = form = 0; in_block = False; key_indent = 0 for ln in lines: if not ln.strip(): continue indent = len(ln) - len(ln.lstrip()) if in_block: if indent > key_indent: prose += 1; continue in_block = False m = re.match(r'^(\s*)(description|summary):\s*\|', ln) if m: prose += 1; in_block = True; key_indent = len(m.group(1)); continue if re.match(r'^\s*(description|summary):\s+\S', ln): prose += 1; continue form += 1 return prose, form ``` ``` 0.2.3 : проза 540 · форма 760 · доля 42 % 0.3.0 заленденный: проза 930 · форма 1185 · доля 44 % 0.3.0 + дофикс : проза 1055 · форма 1183 · доля 47 % ``` Честный вывод вместо прежнего: **доля прозы выросла — с 42 % до 44 % при батче и до 47 % после дофикса**, потому что каждая находка холодного потребителя закрывается ПРАВИЛОМ, а правило это проза. Абсолютные числа (форма 760 → 1183) не спорны ни при каком методе; спорна именно доля, и утверждать «вернул к уровню 0.2.3» я больше не могу. Что резалась вода, а не норма, доказано отдельно — типами и мультимножеством модальных глаголов ниже, и эти два доказательства от метода мерки не зависят. Первая редакция батча дала 2586 строк при доле прозы 50 % (приложенным скриптом). То есть я исполнил букву §5а (вынести названный генезис в компаньон) и провалил её предмет: объяснял ПОЧЕМУ прямо в описании поля, хотя рядом лежит компаньон ровно под это. Проход по всему файлу: в описании остаётся правило и обязанность потребителя, «потому что» уходит в компаньон либо снимается. Итог прохода, ТЕМ ЖЕ скриптом: 2586 → 2315 строк, проза 1163 → 930 (−20 %), доля 50 % → 44 %. Дофикс потом вернул часть — 1055 и 47 %, — потому что каждый его пункт это новое ПРАВИЛО; ниже видно, что правил прибавилось, а не воды. **Что резалась именно вода, а не норма, проверено двумя способами исполнением:** ``` # 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 вхождение: строка 18 — сама формулировка запрета в шапке. (пере-ран после дофикса 16.08: по-прежнему ровно одно, там же) ``` Список составлен из слов, названных Б-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` (полоса 10–19) · `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 находок, ранжированных по цене реализатору. **Ноль находок не случился ни у одного, и это правильный исход.** Пере-считано по таблицам ниже (ФБ-10): двумя линзами найдено **57** позиций — 46 исправлено (§5.1) и 11 отклонено с причиной (§5.2); третьей, кросс-модельной, ещё **12** (§5.3). Итого **69 находок, из них 57 исправлено**; четыре из них — дефекты, которые прошли бы в код P7 и стоили бы там на порядок дороже (F1, F8/F9, F32, C2). Обе линзы независимо нашли одно и то же в трёх местах (структурная версия · `X-TM-Client` против bearer · `Book` без ревизии в ответе на запись) — это и есть подтверждение, что находки не шум. ### 5.1. Исправлено (46 позиций) | # | Линза | Находка | Что сделано | |---|---|---|---| | **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. Отклонено — с причиной (11 позиций) | # | Линза | Находка | Почему отклонено | |---|---|---|---| | A2 | а | Три строки таблицы §5а не вырезаны: `Note.unit_id`, `Bank.signed`, нагрузка кадров | **Диспозиция была записана ДО ревью** — §3.3, §3.5, §3.6 этого отчёта и §6а компаньона, с контраргументом на каждое. Опровергатель отчёта и прозы компаньона не видел по условию мандата; его вердикт «без прикрытия» основан на неполном входе. Аргументы стоят | | **A3** | а | Файл вырос на 63 %, доля прозы выросла | **ПРИНЯТО и ИСПРАВЛЕНО отдельным проходом** (§4.4), после того как владелец назвал ту же вещь прямо. Итог прохода: 2586 → 2315 строк, проза −20 %, доля 50 % → 44 %. ⚠ Прежняя приписка «вернул ровно к уровню 0.2.3 (36 %)» СНЯТА дофиксом: она держалась на мерке, где ключевая строка `description: |` считалась формой, и приложенным скриптом не воспроизводится — уровень 0.2.3 это 42 %, и до него проход не дошёл. Что резалась ВОДА, а не норма, доказано независимо от мерки: генерённые типы до и после резки идентичны с точностью до 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.1–5.2 — одного семейства со мной, и их согласие независимым подтверждением не считается (тот же долг записан у research/28 §10 и строкой 190). По слову владельца добавлен проход агентом ДРУГОЙ модели по диффу `0.2.3 → 0.3.0` и по заказу; входы те же артефакты, отчёт и промт не давались. Мандат отличался прицелом: (1) что будет отлито в код P7 неверно · (2) смысловые дефекты, проходящие линтер · (3) сверка с заказом · (4) **не потеряна ли норма при резке прозы** — отдельный риск, которого у первых двух линз не было, потому что резка случилась после них. Результат: **1 HIGH и 4 MEDIUM, все — дефекты моей новой формы; ни одного неисполненного пункта заказа.** В таблице ниже **12 строк: 11 приняты и исправлены, одна отклонена осознанно** (числа интейка — аргумент в самой строке). Прежняя формулировка «все девять» не сходилась с таблицей и исправлена дофиксом (ФБ-10). | # | Находка | Что сделано | |---|---|---| | **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. **Рантайм не поднимался.** Ни платформа, ни фронт не запускались: платформа 12 из 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):** 2586 → 2315 строк, проза −20 %; типы не изменились ни на поле. **Прочее, изменённое селф-ревью:** `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. --- ## 9. Дофикс-раунд 16.08 (заказ приёмки D39.142): диспозиция по каждому ФБ > ⚠ **Провенанс дерева.** Часть правок дофикса приехала в рабочее дерево от ПАРАЛЛЕЛЬНОЙ сессии, > которую владелец остановил. Я не принимал их на веру: каждую сверил с заказом и с кодом, три > дефекта в них нашёл и починил, один невыполненный пункт доделал. Ниже — состояние ПОСЛЕ этой > сверки; там, где я правил чужую правку, это названо. | ФБ | Что требовалось | Диспозиция | |---|---|---| | **1** (HIGH) | Wire-форма кадра неоднозначна: `EventEnvelope{event,id,data}` как объект против SSE-фрейминга | **ИСПОЛНЕНО.** На схеме стоит дословный пример кадра и сказано прямо: `EventEnvelope` — ОПИСАНИЕ, а не форма JSON на проводе; `event` и `id` — поля SSE (`event.type`/`event.lastEventId` у `EventSource`), телом кадра является `data` и только оно; heartbeat — комментарий, не кадр | | **2** (HIGH) | Книга в покое без истории: `hello` без id → клиент без `Last-Event-ID` → вечный цикл | **ИСПОЛНЕНО.** История нумеруется с `1`, служебные кадры пустой книги несут `0`; `Last-Event-ID: 0` попадает под уже существующее правило `204`. Последовательность конечна: `hello` → `end` → закрытие → одно переподключение → `204` → браузер останавливается | | **3** (HIGH) | «`note` не теряется» не переживает reconnect | **ИСПОЛНЕНО.** Обещание не расширено (буфер обещать нечем) — названа ОБЯЗАННОСТЬ клиента: после КАЖДОГО переподключения дельта-чтение `/notes?after_version=<последняя применённая ревизия>` до того, как доверять списку | | **4** (HIGH) | `Revision` противоречит себе; `BookDetail.revision` vs `book.revision` | **ИСПОЛНЕНО.** Одна модель: одно число на КОНВЕРТ; у многостраничного обхода за всё чтение отвечает НАИМЕНЬШАЯ увиденная ревизия — и как гард свежести, и как водяной знак следующей дельты (лишнее перечитывание бесплатно, пропуск строки — нет); равенство `BookDetail.revision` и `BookDetail.book.revision` заявлено явно | | **5** (MED) | Пустые `allOf`-члены → необитаемые типы | **ИСПОЛНЕНО и ПРОВЕРЕНО ГЕНЕРАТОРОМ.** `EventEnd` и `EventResyncRequired` теперь `allOf` из одного члена с описанием на самой схеме; в генерённых типах оба — `components["schemas"]["EventBase"]`, `Record` в наших схемах не осталось (два вхождения в файле — служебные `webhooks`/`$defs` генератора). Записано и то, что раньше подразумевалось: эти два кадра неразличимы по форме, диспетчеризация только по имени | | **6** (MED) | Что делает `resume` по каждому статусу; «новый прогон» легален при ЛЮБОМ `paused` | **ИСПОЛНЕНО, таблица ИСПРАВЛЕНА мной.** В пришедшей редакции таблица клала КОРНЕВОЙ код в слот причины (`cause.code: run_not_resumable`) и оставляла три строки вовсе без кода. Исправлено: корневой код `run_not_resumable` во всех 409, `cause.code` только там, где причина есть (`bank_decisions_incomplete`, `ceiling_reached`), а «это вообще не остановленный прогон» идёт БЕЗ `cause`. Расширен и сам `ceiling_reached`: он покрывает и купленные главы, и кредит за ними — иначе строка `credit_exhausted` таблицы указывала на код, который её не описывает. Значение enum не заводилось (D39.132 п.2а не двигается) | | **7** (MED) | Ответ НЕ `problem+json` — потерянный пункт Б-1 рек. 4 | **ИСПОЛНЕНО.** Записан пол под моделью ошибок: из такого ответа клиент не показывает ни байта и рисует свою нейтральную фразу — по классу статуса, если он есть, иначе «сервис недоступен» | | **8** (MED, 10 позиций) | Пачка недоопределённостей | **ИСПОЛНЕНЫ все десять:** `title: null` → 400 с объяснением сужения RFC 7386 · разводка `X-TM-Client` `required` против освобождения bearer записана в самом параметре · список отказов интейка помечен неисчерпывающим (авторитет — `code`) · правило резолюции `Location` отдельным разделом шапки · тождество `Idempotency-Key` на multipart по объявленным частям + `408` не считается состоявшейся попыткой · `min_chapters` при `max_chapters: 0` — не диапазон, максимум проверяется первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» СНЯТО как обещание без носителя, заменено на наблюдаемое (`BankPage.signed` против `total`) · `409` у `createExport` объяснён как ключевой, а не книжный · неизвестный `term_id` отклоняет весь батч решений с указателем на позицию | | **9** (LOW) | `parser_unavailable` по эффекту; вычистить внутренние ссылки из описаний | **ИСПОЛНЕНО, ДОДЕЛАНО мной.** Переименование в `processing_failed` на месте; ⚠ проводное имя теперь расходится с внутренним словарём платформы (`parse.go:66`) — проекция это вход P7, записано в компаньон. Ссылки: пришедшая редакция сняла три (`research/28 §2 Б-15`, `§8 п.4`, `§2 Б-4`, «companion K-6»), но пропустила две — цитату D-нот в `info.summary` и `companion §2.8` у `BankTerm`; обе сняты мной. Греп по спеке на `research/28|companion §|К-N|D39.` — ноль | | **10** (отчёт) | Пере-ран чисел ПОСЛЕ последней правки + хвосты | **ИСПОЛНЕНО, ДОПРАВЛЕНО мной.** Пришедшая редакция закрыла: `ErrorCode` 16 (сверено `awk`-счётом), якорь `mining.go:81,243` (сверено — цитата «for EACH term either promote … OR decline» стоит на `:243`), «строка 21» → 18, «12 из 20 не отвечает» (сверено: `contractRoutes` регистрирует 8 путей из 20 операций), строки записки-плана Б-23/К-10/В-6, README §6а «ужаты на месте», скрипт мерки прозы приложен. **Осталось и починено мной:** §0 противоречил сам себе (заголовок «проза выросла быстрее», а следующим предложением «выросла ровно пропорционально») · §0 держал старую долю 36 % против новой мерки в §4.4 · заголовки §5.1/§5.2 говорили «26 позиций»/«12 позиций» против фактических 46/11 в самих таблицах · ярлык §5.5 «ИСПОЛНЕНО ИНАЧЕ» не был заменён на «ОТСТУПЛЕНИЕ, аргумент» · все числа пере-раны после ПОСЛЕДНЕЙ правки, а не после предпоследней | ### 9а. Проверка дофикса (кросс-модельная, по слову владельца) Одна линза другой модели, узкий мандат: закрыт ли каждый ФБ · нет ли регрессий · правда ли то, что дофикс УТВЕРЖДАЕТ о коде. **Вердикт: ФБ-1..9 закрыты (ФБ-8 — 10/10 подпунктов), регрессий в каноне не найдено, все кодовые утверждения подтверждены, кроме одного якоря.** Неприкосновенное цело — проверено грепом на месте: прогонного потока нет, `Note` несёт `code`, `content_refused` в обоих enum, `page_size_default` единственный носитель размера страницы. Нормативных правил заленденный канон → нынешний: 44 → 45, потеряно ноль. | Находка | Диспозиция | |---|---| | **Битый якорь в компаньоне:** `Resume` приписана `runs/runs.go`, а живёт в `runs/reconcile.go:906` (ветка `case "paused"` — `:919`, дневной потолок — `:931`). Пакет верный, файл нет | **ИСПРАВЛЕНО.** Моя ошибка, не чужая: строку писал я. Прочие ссылки на `runs/runs.go:227-231` (`readyToTranslate`) проверены и ВЕРНЫ — там функция действительно живёт | | LOW: пример кадра давал `id: 1841` и `"revision":1841` — одно число на два счётчика, которые батч специально разводил; холодный потребитель мог склеить их обратно | **ИСПРАВЛЕНО:** в примере теперь `id: 57` при `revision: 1841`, плюс строка «эти два числа намеренно непохожи и одно не подменяет другое» | | LOW: код позиции `unknown` введён в `submitBankDecisions`, но отсутствует в иллюстративном списке `ErrorItem.code` — потребитель его там не найдёт | **ИСПРАВЛЕНО:** добавлен в список | | Наблюдение: версия осталась `0.3.0`, хотя после лендинга переименовано значение enum (`parser_unavailable` → `processing_failed`) — «0.3.0» теперь обозначает два разных содержания | **ПРИНЯТО КАК ЕСТЬ, называю оркестратору.** Дофикс заказан САМОЙ нотой приёмки как часть того же раунда (D39.142 п.3), потребителей у 0.3.0 нет ни одного (зеркало фронта на 0.2.3, платформа на 0.2.3), и бампать минор внутри приёмочного раунда значило бы плодить версию, которую никто не видел. Если оркестратор читает иначе — бамп стоит одной строки | | Требование «пере-ран чисел ПОСЛЕ последней правки» на момент проверки снова не сходилось (дерево правилось при ней) | **ЗАКРЫТО финальным прогоном** — числа §0 сняты после САМОЙ последней правки и сверены; проверка это и предсказывала | **Ратификации приёмки, которые дофикс НЕ двигал** (проверено грепом по канону): прогонный поток снят, канал один — книжный · `Note.code` вместо `message` · `content_refused` остаётся значением `RejectReason` · единый `page_size_default` вместо пер-коллекционных. **Что дофикс добавил в долг P7, сверх читающей поверхности:** `POST /runs/{runId}/resume` — построенный путь, и 0.3.0 меняет его ответ на паузе по потолку с `202`-без-изменений на `409` `cause.code: ceiling_reached`. Записано строкой в таблицу компаньона §3, потому что это единственное место дофикса, где правится УЖЕ РАБОТАЮЩИЙ код, а не проекция, которой ещё нет.