textmachine/docs/archive/reports/CONTRACT_BATCH_0.3.0_REPORT.md

89 KiB
Raw Blame History

Отчёт: батч 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_bankstop_for_signing без «before the final pass» ИСПОЛНЕНО на RunRequest и на Run §RunRequest, §Run.stop_for_signing
TermOrigin снять/переименовать ПЕРЕИМЕНОВАНО, ось сохранена: seed·ruby·minedgiven·annotated·found §TermOrigin
TermStatus ОСТАВИТЬ осью, переименовать движковые значения (эррата 16.08-г) ИСПОЛНЕНО. Ось на месте; auto·draft·approvedproposed·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}/events204
развести 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/BankBookPage/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 — переименования idchapter_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. promoteapprove

Б-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.noteUnit.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.messageNote.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; названо лечение «новый прогон с бОльшим потолком»
streamRunEventsstreamBookEvents поток на ПРОГОНЕ; конца нет; склейка без ограничений; 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.noteUnit.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.3Note.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.