| .. | ||
| openapi.yaml | ||
| README.md | ||
Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
Нормативная поверхность контракта —
openapi.yaml(файл рядом, в этой же папке). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято каждое решение, чем оно обосновано, что осталось открытым, и ГЕНЕЗИС форм (историю ратификаций, сверку со стандартами, разобранные альтернативы). При расхождении по ФОРМЕ побеждает YAML; при вопросе «почему так» — этот файл.Статус: РАТИФИЦИРОВАН по 0.10.0 включительно. 0.2.0 (D39.115, 08.08) · 0.2.1 (D39.123, 09.08) · 0.2.2 (D39.129, 10.08) · 0.2.3 (D39.135, 15.08) · 0.3.0 (D39.138, 16.08) — ломающий минор по целостному ревью research/28 · 0.4.0 (D39.152, 20.08) — синк с платформой, §6в · 0.5.0 (D39.161, 27.08) — снос отменённой пер-термной модели подписи (PD-370) + дверь правок банка, выведенная из построенного глагола движка (D39.158), + предупреждение о конверте вне
/v0; вывод и провенанс — §2.19, отчёт сессии —../../archive/reports/CONTRACT_MINOR_REPORT_2026-08-27.md⚠ архив — инструкции оттуда не исполнять · 0.6.0 (D39.162, 28.08) — сквозная полоса прогресса и подпись стадии, приехавшие ОДНИМ лендингом с платформенным паком P9; вывод и провенанс — §2.20 · 0.7.0 (D39.166, 28.08) — покупка ПЕРЕ-ПРОХОДА: правка банка доезжает в уже переведённый текст; провенанс — §2.21 · 0.8.0 (D39.169, 29.08) — кадрsession_ended: отзыв сессии гасит открытый поток и НАЗЫВАЕТ причину, вместо молчаливого обрыва, который клиент отвечает переподключением в401. Кадр СОЕДИНЕНИЯ: несёт id последнего исторического кадра и своего номера не потребляет, поэтому вход заново продолжает ровно с места остановки. Приехал ОДНИМ лендингом с платформенным паком P11 · 0.9.0 (D39.180; ратифицирован №20 31.08 по пингу зоны ДО стройки, заленджен №21 с паком P12) — ВТОРАЯ граница пересчётаchapters_doneи непрозрачныйBook.shape_epoch, которым она себя называет; провенанс — §2.22 · 0.10.0 (D39.194, 04.09; ратифицирован ПОСЛЕ стройки, по составу, вынесенному платформенной сессией из пака «закрыть цикл») — дверь выдачи, описанная тем, что построено:failure_codeстановится enum из ЧЕТЫРЁХ кодов, которые эта реализация ПРОИЗВОДИТ (условие «becomes an enum with the first built format» наступило);Export.urlобъявленuri-reference, потому что отдаётся ОТНОСИТЕЛЬНЫЙ путь, а прежняя формулировка «minted for THIS response» описывала подписанную капабилити, которой никто не строил; третий адрес двери получаетoperationIddownloadExport— до этого единственная поверхность, по которой браузер НАВИГИРУЕТ, жила в каноне только прозой; шестнадцатая причина замечанияoff_target_langполучает кодwrong_language(Приложение А). Провенанс — §2.23. Дом канона — этот каталог;frontend/docs/api-contract/openapi.yaml— байт-зеркало ⚠ НА ФРИЗЕ ЗОНЫ РАВЕНСТВО ПРИОСТАНОВЛЕНО (D39.142 п.5): зеркало 0.2.3, канон 0.10.0.⚠ 0.5.0 ломающий по построению (мажор
0): снесены путьPOST …/bank/decisionsи три его схемы, изBankPageиEventBankснятыpending_decisions/complete(§2.19-бис), вCapabilities.requiredдобавленbank_corrections_enabled.⚠ Урок оставлен НАМЕРЕННО, он переживает свой дефект (канон и проекция деплоя разошлись по ПОЛЯМ при совпавшем НОМЕРЕ версии — носитель
PD-399, закрыт лендингом P9, D39.162; разбор — §2.19-бис): гейт версии этого класса не ловит.TestTheAnnouncedContractVersionIsTheOneTheCanonRatifiedсверяет НОМЕР, а не поля — совпадение версий не значит совпадения форм, и следующее расхождение поля найдёт снова не он.⚠ 0.4.0 ломающий ровно по одному месту, и это ЗАМЕРЕНО, а не объявлено. Дифф генерённых типов 0.3.0 → 0.4.0 (
openapi-typescript@7, комментарии отброшены) — одна строка:Run.stop_requested: boolean. Всё остальное в этой редакции — проза канона, одна запись в таблицуresumeRunи одно значение ВТОРОГО уровня (cause.code: credit_unavailable, а он по замыслу не закрыт и типов не двигает): корневой enum не сужен и не расширен, обязательных полей больше не появилось. Линтер зоны (spectral --ruleset frontend/.spectral.yaml) — 0 ошибок.⚠ Две границы этого замера, названные ревью диффа и обе честные. (1) База — 0.3.0, а зонное зеркало фронта стоит на 0.2.3: при разморозке фронт платит переход 0.2.3 → 0.4.0 целиком, и это совсем другой объём (шапка ниже про отставание зеркала). Замер отвечает «что добавил ЭТОТ синк», не «что стоит фронту разморозка». (2) Гейт §5 на утечку конвейерного словаря пере-ран и чист по сути, но «ноль вхождений» сказать нельзя: слово
editвстречается дважды как обычный английский глагол («not an edit», «not a row edit»),stage— один раз в самой формулировке запрета. Ни одного нового вхождения этот синк не внёс.0.x ломает миноры и будет ломать весь бета-период (решение владельца 16.08, §8 п.17 research/28): 0.3.0 — обычный ломающий минор, а НЕ последний перед 1.0. Окно 1.0 — после беты.
Язык. Спека английская: из неё генерятся типы, а исходники фронта по конвенции английские (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской
K-N— это те же вопросы §4. Спутник и остальные доки зоны — русские.Зона строки 95 — «оркестратор/бэкенд/фронт». Фронт авторитетен в одной трети: форма read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и работы движка здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют.
0. Пометки провенанса
| Пометка | Что значит |
|---|---|
| ✓ выведено | следует из кода движка/платформы или ратифицированного решения; грунт file:line рядом |
| ◆ предложено | решение автора контракта, разумное по его сведениям; подтверждает названная зона |
| ○ открыто | развилка, на которую ответа нет; перечень — §4 |
⚠ Пометка ставится на утверждение, а не на раздел: у одного пункта половина бывает выведенной, а половина предложенной.
Пере-разметка 0.3.0. Батч сдвинул четыре класса:
Progressбыл ✓ (выведен из пофазной механики движка) — стал ◆: одна полоса до ближайшей остановки со знаменателем «купленный объём» это ПРОДУКТОВОЕ решение владельца (§8 п.1/п.8 research/28), а не проекция устройства движка. Это и было дефектом: форма ✓ означала, что устройство движка обязано пролезать на провод;- словари банка (
TermStatus,TermOrigin) были ✓ по значениям — стали ✓ по различению, ◆ по словам: различение выведено из кода, слова назначены продуктом (движковыеauto/draft/ruby/minedна провод больше не идут); - модель ошибок была ◆ (форма RFC 9457 как её понимал автор) — стала ✓ по словарю причин
(класс 1 выведен перечислением реальных ветвей
platform/internal/httpapi/v0.go(греп по именам хендлеров)) и ✓ по политике (вариант B ратифицирован владельцем, §8 п.4); GET /capabilities, условные чтения,Idempotency-Key,blocked— ◆ целиком: формы назначены этим батчем, подтверждает платформа при P7.
1. Почему YAML, а не проза
Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им типизируются моки. Прозаический контракт расходится с кодом ровно тем способом, ради предотвращения которого заведена строка 95.
⚠ Но машинного мало, и 0.3.0 это подтвердил дважды. Ревью прогнало линтер по канону 0.2.3 и
получило «No results» — то есть все 29 находок ревью были смысловыми, ни одной синтаксической.
А К-11 замерил, что if/then OpenAPI 3.1 генератор типов ИГНОРИРУЕТ. Отсюда правило,
действующее с 0.3.0: любое межполевое или условное правило обязано быть записано И схемой, И
словами в описании поля — схема защищает сервер, слова доезжают до клиента. Мест таких три
(0.5.0 сменил первое: схема BankDecision снесена вместе с моделью): BankCorrection (dst при
approve/decline и взаимоисключение id/кортежа), Unit.target, агрегаты BankPage.
Инструменты и пины — STACK_DECISIONS.md §3 и бэклог зоны.
2. Решения и их происхождение
2.1. Язык — код, никогда не имя — ✓ выведено
Движок держит коды (backend/internal/config/book.go:26-27), ключ пары — zh-ru
(configs/langpacks/zh-ru/). Имя языка в данных — пар-специфика в общем слое, запрещённая §2
канона. Вторая цена, дороже: lang элемента берётся из данных книги, и пара ja→ru с именем вместо
кода отрисует кандзи китайскими начертаниями молча.
0.3.0 дополнил: форма кода — не то же, что умение пары. Платформа проверяет только ФОРМУ
(platform/internal/books/books.go:118-121) и равенство source == target не проверяет никто, а
пар-промпты в репозитории есть ТОЛЬКО для zh-ru; отсутствие промпта пары — жёсткая ошибка
конфигурации, не деградация («no prompt for pair %q role %q … never silently substitute another
pair's conventions», backend/internal/config/pipeline.go:952-956). До 0.3.0 книга в
неподдерживаемой паре принималась, ложилась на диск, разбиралась, проводила пользователя через
денежный экран — и умирала на старте прогона без причины. Отсюда Capabilities.language_pairs[]
со статусом и код отказа на интейке (errors[].code: unsupported_pair).
2.2. Идентификаторы непрозрачны — ✓ выведено, классы стабильности разведены в 0.3.0
glossary.id — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется
(store/migrate.go:173-174); номер главы плотный, «Chapters that yield no text … do NOT consume a
chapter number» (chunk/chunker.go:99-105), поэтому правка исходника сдвигает номера последующих
глав.
0.3.0: одно предложение «Stability across runs is the platform's job» покрывало пять сущностей
с разной по природе стабильностью и для юнита обещало заведомо больше возможного: движковый id
юнита несёт cut-tag, и «any chunker/budget/pipeline-shape change mints a new id for EVERY unit in
the book, while chapter ids survive» (backend/internal/pipeline/manifest.go:102). Теперь в схеме
Id три класса явно: книга/прогон/экспорт — навсегда; глава — переживает пере-разбор; пара —
только внутри одного structure_version, и клиент это НАБЛЮДАЕТ (Б-7), а не узнаёт по разъехавшимся
вкладкам. Стабильность по-прежнему обеспечивает платформа при персисте манифеста (строка 100).
2.3. Заголовок главы отдельным полем — ◆ предложено
Движок сегодня делает ОБРАТНОЕ, и это надо назвать прямо: титул рендерится детерминистически из
шаблона пары (configs/langpacks/zh-ru/heading.txt), исходный маркер вырезается из текста для модели
(chunk/chunker.go:110-114), а на экспорте титул вклеивается внутрь текста первого юнита
(pipeline/export.go:215). Комментарий движка прямо запрещает подавать этот рендер как метку книги
(pipeline/manifest.go:80-86). Развилка — К-2.
0.3.0 снял противоречие, которое существовало независимо от К-2. Спека 0.2.3 запрещала клиенту
синтезировать метку «Глава N» — а решение владельца 09.08 (research/27 §33-36) требует ДВУХ меток:
оригинальной из данных книги плюс служебного рендера «Глава N» за $0 в локали ИНТЕРФЕЙСА. Сервер
локали интерфейса не знает (Accept-Language в платформе нет грепом), значит служебный рендер обязан
быть клиентским. Запрет снят; Chapter.heading = только метка ИЗ ДАННЫХ книги, null — обычный
ответ, и деплою прямо запрещено класть сюда рендеренный порядковый.
title_raw и kind (глава/фрагмент) в 0.3.0 НЕ заводятся — передано дизайн-паку этапа 161
(строка 161, очередь D39.136 п.3), и вот почему это не откладывание: (а) производителя настоящих
названий не существует — это Этап 0, строка 160; (б) форма узла «глава ↔ технический фрагмент» и
вердикт детекции — ровно тот предмет, который дизайн-пак и решает, а заложенная до него форма
заморозила бы догадку; (в) добавление обоих полей — АДДИТИВНОЕ расширение (минор), а смены СМЫСЛА
существующего heading не будет: он и сегодня, и после 160 означает одно — метку из данных книги.
Запись для пака 161: контракт 14 расширяется этим паком аддитивно; Chapter получает title_raw
(если решится, что рендер и оригинал — разные поля), kind, вердикт структуры; ломать 0.3.0 для
этого не требуется.
2.4. Состояние — у прогона; у главы выполнение — ✓ выведено
Подпись банка это один стоп на всю книгу (pipeline/mining.go:81,243), поэтому «глава ждёт подписи,
пока соседняя финализируется» — невозможная картина. У главы движок держит ChapterPassport
(pipeline/status.go:37-55).
2.5. Прогресс — ОДНА полоса до ближайшей остановки, в главах (0.3.0) — ◆ решение владельца
Прежняя форма (0.2.x — пофазно и в юнитах) снята как дефект (research/28 Б-0, вопрос владельца
16.08): пара draft/edit была не абстракцией, а сквозным пробросом внутренней структуры движка до
React-компонента — движок оказывался пришпилен к контракту в шести местах, а его изменение —
ломающим для фронта.
Решение владельца 16.08 («Согласен, переделываем» + В-5 «Ок, делаем так»): одна полоса до ближайшей остановки, знаменатель — КУПЛЕННЫЙ объём, после подписи банка полоса начинается заново.
Форма, выбранная батчем: счёт в ГЛАВАХ, а не в юнитах. Три довода, и второй — денежный:
- Одна величина, а не две. «Один счётчик» и «знаменатель — купленный объём» вместе значат, что
числитель и знаменатель обязаны быть в одной единице. Купленный объём объявлен в ГЛАВАХ
(
ceiling_chapters), и другой единицы у него нет: пересчёт «главы → деньги» на провод не идёт (D39.84), пересчёт «главы → юниты» до разбора неизвестен. - Дробь наконец означает то, что человек купил (Б-13а). До 0.3.0 потолок был в главах, а
прогресс — в юнитах ПО ВСЕЙ КНИГЕ (
v0.go:473-477), поэтому прогон, купленный на 10 глав из 2284, показывал дробь, которая не могла дойти до единицы, — и книга уходила вpausedна 0,4 %. А потолок стоит на КАЖДОМ прогоне: поле обязательное. - Ноль всю первую волну не возвращается — его снимает СЕГМЕНТНАЯ логика, а не единица счёта.
Сегмент = работа между двумя остановками; в первом сегменте глава засчитывается, когда её работа
ЭТОГО сегмента закончена, а не когда она пройдена от начала до конца. Обе величины у платформы
уже есть:
chapters.units_draft_done/units_edit_doneзаведены именно под это (00002_readmodel.sql:102-105, комментарий «K-10 is open… answering it is a projection change rather than a migration»). Новых колонок батч не требует.
Тем же ходом закрыт К-10 — вердикт «НЕ строить» (D39.138, поправка приёмки research/28 №1):
пофазные счётчики на главу строить НЕ надо, потому что фаз на проводе больше нет вовсе.
Chapter.units_done считается той же сегментной логикой, что и книжная полоса, — иначе дерево глав
читало бы ноль всю первую волну, а это и была исходная жалоба К-10, и снятие фаз само по себе её не
лечит.
⚠ (0.6.0: клауза «той же логикой, что и полоса» ПЕРЕКРЫТА — прежде чем читать дальше.) Полоса
ПРОГОНА сегментной быть перестала: она одна монотонная доля на всю работу прогона и через стоп
подписи не обнуляется (§2.20). Глава — по-прежнему состояние ОДНОГО прохода и по-прежнему возвращается
к нулю, когда начинается следующий. То есть две величины теперь законно РАСХОДЯТСЯ, и это записано
предупреждением в самом каноне (ChapterProgress.units_done): клиент, зеркалящий одну в другую,
нарисует прыгающий бар. Абзац сохранён как провенанс батча 0.3.0 — вердикт К-10 «пофазные счётчики на
главу НЕ строить» им не затронут и в силе.
Книжная полоса — отдельная величина. Book.chapters_done против chapter_count — прогресс
КНИГИ (строка библиотеки), он не откатывается при старте нового прогона. Величина уже считается в
SQL и до 0.3.0 не отдавалась: `b.chapter_count - (select count(*) from chapters c where … units_done
= units_total)
(pgstore/books.go:677-681).ProgressнаBook` больше нет.
2.6. Словарь статусов — ✓ по механике, ◆ по составу; finalizing снят в 0.3.0
Лестница «загрузка → разбор → перевод → подпись банка → финал → готово» пришла из строки 95.
stopped, rejected, not_started контракт ВЫВОДИТ из поведения процесса, а не получает полем:
механика стопа у движка есть (cmd/tmctl/main.go:63), но «кто нажал» знает платформа.
Стоп по потолку — не failed ✓ выведено: «Ceiling is a hard, book-wide stop … the job stays
'pending' and resume continues once the ceiling is raised» (pipeline/stagerun.go:488-489).
0.3.0 — три правки:
finalizingснят. У движка такой фазы нет вовсе (grep -ri finaliz backend/internal— пусто), в словаре ingest её нет (ingest/events.go:53-67), писателя у значения нет нигде. Держалась она на лестнице владельца — а владелец 16.08 сказал про лестницу: «Ну да, это чисто моя фраза была» (§8 п.15). Устная формулировка нормой продукта не является, статус ею не связан. Появится фаза у движка — значение вернётся минором так же дёшево.- Заведён отдельный
RunStatus(6 значений).Run.statusбыл типизирован книжным словарём из 11 значений, из которых на прогоне легальны не все, и спека говорила это ПРОЗОЙ — то есть генерённый union был шире правды, а клиент обязан был писать недостижимые ветки. Узкий словарь у платформы в DDL уже записан (00002_readmodel.sql:47-49). - Записано правило старшинства «книга производна от прогона, кроме
uploading/parsing/not_started/rejected». До 0.3.0 правила не было, и фронт уже разошёлся сам с собой: полоса состояния решала «paused» по прогону (showcase/Status.tsx:21), карточка — по книге.
2.7. Состояние пары выводится из ПАРЫ — ✓ выведено
Флагнутый юнит легально приходит С ТЕКСТОМ в двух случаях: косметическая зачистка санитайзера («the
chunk is NOT lost — the cleaned text is committed as the export», pipeline/disposition.go:96-104) и
c-lite member-drop (pipeline/export.go:203-207). Поэтому состояние выводится из ПАРЫ (вердикт +
наличие финального текста): флаг+текст → translated с замечанием; флаг+пусто → withheld.
Причина флага при этом ПРОИЗВОЛЬНА («единственный легальный случай» снято ревью round-2): при c-lite
drop юнит несёт причину ПЕРВОГО выпавшего члена, какой бы она ни была. Значит карта причин обязана
иметь фразу для каждой, а не для двух. Но пара «текст + промах словаря» невозможна ни одним каналом:
memberDrops берёт причину из ЧЕРНОВОЙ строки члена (status.go:242-257), а glossary_miss
ставится пост-чеком только там, где отгружается финал (waverun.go:373-381, :494).
0.3.0: инвариант «translated ⇒ текст непуст, иначе пуст» перестал быть только чеком БД
(00002_readmodel.sql:127-128) и стал условной обязательностью в схеме плюс словами в описании
(К-11: генератор if/then игнорирует). Слова «flagged», «chunk verdict», «sanitizer» с провода
сняты — они компилировались в JSDoc генерённых типов клиента.
Свежесть ✓ выведено: target обновляется на границах работы и на стопах, а не непрерывно —
посреди прогона канала чтения не существует (эксклюзивный лок движка; санкционированное чтение —
завершённый либо остановленный прогон, research/23 §0, §4).
2.8. Банк: различение ✓, слова ◆ (пере-назначены в 0.3.0)
| Поле | Словарь 0.3.0 | Что было у движка | Грунт |
|---|---|---|---|
status |
proposed · in_progress · approved |
auto · draft · approved |
store/migrate.go:191; только approved — канон |
kind ◆ |
name · place · title · term · nickname плюс null |
то же | terminology/classify.go:15 + banknote.go:74; пустое — membank/memseed.go:323-326 |
origin |
given · annotated · found |
seed · ruby · mined |
пути записи, см. ниже |
sense |
свободный текст, пустая строка = «нет различителя» | то же | store/migrate.go:182 |
| окно | since_chapter/until_chapter, null = без границы |
целые, 0 = без границы |
store/migrate.go:189-190 |
Почему слова пере-назначены (0.3.0, Б-0/Б-17). ruby — японская фуригана, то есть
паро-специфика в общем слое; mined — имя стадии конвейера; draft — имя волны; auto читается
как «движок сам». Канон проекта: книжный/паровой термин в общем слое = утечка (CLAUDE.md,
гардрейлы), и шапка самой спеки объявляет «no stage names». Клиент был обязан нарисовать слово для
ruby в паре, где рубя не существует.
TermStatus — ось СОХРАНЕНА, переименованы только значения (эррата 16.08-г D-лога). Исходная
рекомендация Б-0 «снять с провода» опиралась на «ни один экран их не рисует» — а это подмена: экрана
подписи ещё нет (S5). Ось продуктовая и несущая — «на экране в сотни строк это главный фильтр
работы», и шов клиента её уже потребляет (frontend/src/api/vocabulary.ts:159-163, termStatus с
безопасным дефолтом canon: false). TermOrigin тем же разбором ОСТАВЛЕН как различение
(провенанс нужен подписывающему, чтобы понимать доверие к строке) и переименован по значениям: seed
→ given (пришло с книгой), ruby → annotated (сам текст книги сказал, как читать), mined →
found (сервис нашёл в тексте). Проекция трёх пар — работа платформы.
Правило пустого kind — и его проекция (провенанс возврата — §6в п.0). kind присутствует всегда и допускает null; null
значит «сервис не решил», строка при этом остаётся подписываемой, и клиенту запрещено и выбрасывать
её, и додумывать тип за движок (канон, схема BankTerm.kind). У движка та же строка несёт Type: "" (membank/memseed.go:323-326), поэтому проекция "" → null — работа платформы; это
единственное место, где пустая строка и null означают одно, и потому оно записано.
Фантом auto в провенансе убран ещё в 0.2.0: комментарий схемы движка (migrate.go:193:
seed|ruby|auto) устарел — "auto" пишет СТАТУС, не провенанс (membank/memseed.go:328:
Status:"auto", Source:"ruby"), майнинг ставит mined (pipeline/mining.go:424).
Ложный друг source. У движка колонка source — это ПРОВЕНАНС. Поэтому в контракте провенанс
зовётся origin, формы термина — src/dst, а имя source в схеме банка не используется вовсе.
Переименование ради стиля здесь — самый дорогой класс правки, и ревью 0.3.0 его отвергло (§4 №25).
Окно термина (0.3.0, Б-18). since_chapter/until_chapter стоят на НОМЕРЕ главы, а номер сам
контракт называет не-ключом: нумерация плотная, правка исходника сдвигает хвост, и для книги без
нумерации номера легально нет. Перевести окно на chapter_id нельзя — оно входит в ключ уникальности
термина у движка (store/migrate.go:202), и смена ключа это работа движка, не контракта. Поэтому
записано ЯВНО: окно живёт в координатах текущего structure_version и пересчитывается при его
смене; два смысла нуля разведены на null («без границы»), потому что Chapter.number начинается с
единицы и 0 был сентинелом с двумя значениями в двух полях.
2.9. Подпись — набор решений — ✓ выведено
Конвейер заменяет банк целиком (store/migrate.go:169-170), поэтому PATCH /term/{id} молча не
работает. Механика дословно: «for EACH term either promote it into the mined-delta file OR decline it
in the mined-rejects file, then resume» (pipeline/mining.go:81,243; ⚠ фраза «the stop clears once
every proposed term is promoted or rejected» ОПРОКИНУТА D39.144 — движок авто-продолжает с
неподписанным, D39.42 п.3). Отсюда живое: решение на термин · информационный счётчик «решено N из M»
· частичное сохранение ◆. Запрет «продолжить» при неполном наборе — СНЯТ (подпись = один resume).
0.3.0 — две правки:
promote→approve.promote/decline— дословно глаголы оператора майнера из строки выше, иpromoteпорождалstatus: approved— два слова на один акт. Теперьapprove→approved.- Счётчики стопа (
pending_decisions/complete) СНЕСЕНЫ 0.5.0 — разбор §2.19-бис. Ратифицировано D39.144 (модель владельца 16.08: подпись — ОДИН акт над всем банком):resumeRunснимает стоп при ЛЮБОМ состоянии решений, 409-полноты НЕТ; честный счёт нерешённости едет квитанцией двери правок (signature), а не чтением.
POST /runs/{id}/resume — нормативная операция, а не резерв.
⚠ Открытый остаток (заведён батчем 0.3.0, пере-привязан 0.5.0 к живым носителям). Чтение банка
не отвечает, КАКИЕ строки уже решены: TermStatus — состояние строки, решения живут файлами
движка (дельта и список отказов — 18-bank-ontology.md, роли ИСТОЧНИКОВ), и на провод пер-строчная
решённость не проецируется ни одним полем; счётчик «сколько осталось» с 0.5.0 отвечает только
квитанция двери правок (signature), не чтение. Экран подписи, перезагруженный посреди стопа,
знает суммарный счёт лишь после первого своего вызова двери и не знает, какие строки решены.
Лечение — одно поле BankTerm.decision, и оно НЕ добавлено: слово владельца 15.08 «добавочные поля
BankTerm НЕ заводить» (D39.136 п.4б) прямо это запрещает; публикация решённости движком — заказ
на день, когда экран подписи закажут (18-bank-ontology.md, «Чего эта форма НЕ несёт»). Решение —
за владельцем.
2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
✓ выведено: правило ре-синка — идемпотентный апсерт по (run_id, seq), канал согласования —
status --json (research/23 §2, §8; D39.85).
◆ предложено: что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН. Обоснование —
гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом, а рефетч по возврату
фокуса окна у ратифицированного @tanstack/react-query включён по умолчанию.
0.3.0 — два уточнения, оба выведены из построенного клиента:
- Ревизия страничного обхода — МИНИМУМ по страницам, не максимум. Правило жило только
комментарием в
frontend/src/api/client.ts:99-106(«The revision of a torn list is its OLDEST page»), то есть второй клиент его бы не узнал. Теперь оно в схемеRevision. - Агрегаты банка — на ПЕРВОЙ странице (Б-12).
Bankобязывал нестиtotal/signed«в целом по банку» на КАЖДОЙ странице, при этом у каждой страницы свояrevision, а правила согласования не было — и референсный клиент неизбежно смешивал два момента времени: счётчики брал с последней страницы (queries.ts:144-148), ревизию — с первой. Обе половины написаны осознанно и обе верны по отдельности. Первая страница — единственный выбор, согласованный с правилом выше, и он не требует от сервера держать снимок между запросами.
Кадры соединения не тратят номеров истории. id кадра объявлен позицией в истории событий
КНИГИ, строго возрастающей, — но hello приходит первым на КАЖДОМ подключении, а end и
resync_required тоже принадлежат соединению, не книге. Разведено 0.3.0: служебные кадры несут id
последнего кадра ИСТОРИИ и своего номера не тратят, повтор id на них легален, а дыра в нумерации
легальна из-за склейки — и клиенту прямо запрещено читать пропуск как потерянный кадр. Обе
альтернативы отвергнуты собственным текстом канона: книжные номера служебным кадрам заставили бы двух
одновременных зрителей тратить номера друг друга, а повтор последнего номера сделал бы «one per frame»
ложью.
Дельта-чтения ВКЛЮЧИТЕЛЬНЫ (>=), а не строго больше. Строгое сравнение ломает собственное
правило Revision («catch-up reads >= R, not > R»): одна транзакция — это одна ревизия, но
НЕСКОЛЬКО строк, и > теряет соседей последней применённой; а кадр bank предписывает читать строки
«ревизией этого кадра», что при > всегда пустота. Повторно пришедшая строка безвредна (заменяется
по id), а водяной знак следующего чтения берётся из КОНВЕРТА, а не выводится из строк.
⚠ Дофикс 16.08 (ФБ-4): у многостраничного обхода конверт не один. Формулировка «водяной знак —
revision конверта» была однозначна ровно до тех пор, пока чтение умещалось в одну страницу; на
рваном чтении она сталкивалась со вторым правилом того же раздела — «ревизия рваного списка это
ревизия СТАРЕЙШЕЙ страницы». Два правила давали два разных числа, и клиент, взявший новейшее, молча
терял строки, изменившиеся между первой страницей и последней. Сведено в одно: и гард свежести, и
водяной знак — это НАИМЕНЬШАЯ ревизия, увиденная за обход; на одностраничном ответе это его
собственная. Направление выбора — безопасное: лишнее перечитывание бесплатно (строка заменяется по
id), пропуск строки — нет. Там же заявлено равенство BookDetail.revision и
BookDetail.book.revision: карточка собирается одной транзакцией с книгой, и клиент вправе брать
любое. Устаревший водяной знак
(коллекцию заменили целиком) отвечает 400 c cause.code: version_too_old — тем же ответом и с тем
же смыслом, что мёртвый курсор.
Структурная версия — новая ось (0.3.0, Б-7). Курсор был привязан к «STRUCTURAL epoch of the
collection», серверу вменялся MUST-отказ по мёртвому курсору, — а слово «epoch» встречалось в
документе РОВНО ОДИН раз: ни один ответ эпохи не нёс, наблюдать её было нечем. При этом структура
живая: «число глав может измениться против эвристики» (research/27:35, решение владельца 09.08).
Теперь structure_version едет на КАЖДОМ кадре (EventBase) и в Book, ChapterPage,
UnitPage, NotePage, BankPage; на исчезнувшую главу отвечает 410; к смене версии привязаны
курсор, якоря на пары, окно термина и идентичность строки банка.
2.11. Разрыв потока — ◆ предложено; канал перевешен на КНИГУ в 0.3.0
При переподключении клиент шлёт Last-Event-ID. Если сервер докачать не может — обязан ответить
resync_required, а не молча начать с текущего момента: реплей истории запрещён, иначе разовое
событие вроде note теряется молча.
0.3.0 — четыре правки одного канала (Б-6), все Ц0: канала нет ни строкой (grep text/event-stream по не-тестовому Go платформы — только комментарии).
- Поток перевешен с прогона на книгу. Книга в
uploading/parsingпрогона не имеет по построению: строка прогона создаётся только вStartRun(pgstore/runs.go:66,80), весь разбор живёт на строке книги (books.go:180,237,283). Прогонный поток не мог сообщить конец разбора в принципе, и клиент лечился опросом раз в 3 с (queries.ts:64-75) плюс вторым хуком (useIntakeEnd.ts:31) — а разбор настоящей книги это минуты. Это Ф-56, и чинится он не новым кадром, а перевеской канала: конец разбора становится обычной сменой статуса. ⚠ Прогонный поток узким видом НЕ оставлен (Б-6 предлагал оставить). Довод: второй канал с теми же кадрами — вторая реализация и второй источник расхождения, а адресация «кадры этого прогона» выводится из книжного потока клиентом, у которого id прогона уже есть. §5а того же ревью требует резать, а не добавлять поверхность. - Конец потока объявлен. Терминального кадра не было,
204в ответах не объявлен — при том что SSE именно им останавливает переподключение («a client can be told to stop reconnecting using the HTTP 204 No Content response code», WHATWG). После завершённого прогона браузер переподключался бы вечно. Теперь: кадрend+204на переподключение сLast-Event-IDот завершённого потока; запрос БЕЗLast-Event-IDвсегда открывает новый поток — иначе клиент не смог бы начать смотреть снова после старта прогона. idкадра и ревизия книги разведены. Спека просила «a monotonicid», говорила, что он несёт ревизию, и допускала несколько кадров с одним id; форма зафиксирована не была. Сервер, сделавший id уникальным на кадр (1841-2), не нарушил бы ни слова прозы и навсегда отключил бы клиентский гард:Number('1841-2')=NaN(frontend/src/api/stream.ts:131). Теперьid— позиция потока (десятичное целое, форма зафиксирована), ревизия — поле вdataна КАЖДОМ кадре.- Склейка ограничена по типу. «The server MAY COALESCE frames» стояло без ограничений — а склейка
двух
noteтеряет замечание навсегда, на живом соединении, без переподключения и потому безresync_required. Это ровно тот исход, которым та же спека двумя абзацами выше обосновывала запрет реплея. Теперь: склеивать можно кадры СОСТОЯНИЯ,note— нельзя. - Правило докачки выбрано одно (было два взаимоисключающих): короткий живой буфер после предъявленного id разрешён, реплей истории за его пределами запрещён, размер буфера сервер не объявляет и клиент на него не опирается.
2.12. Разрешающий список — ✓ инвариант, ◆ форма
Проекция «read-модель → фронт» строится как allowlist. Что лежит в операторских структурах
(pipeline/status.go:58-130, :37-55) и не может доехать: пять денежных полей плюс cost_usd главы
· routing вида «stage=model», content_labels, content_routing_problems (ПТ-33) · снапшот, дрифт,
ре-билл · escalations, postcheck_misses, style_flags, glossary_miss_flagged, stages_skipped,
repair_applied, worst_flag_reason · flag_reason и detail.
⚠ 0.3.0 расширил инвариант на ПРОЗУ. Аллоулист полей держался, а описания — нет: объяснительный
текст спеки компилируется в исходники фронта как JSDoc генерённых типов, и туда уехали «chunk
verdict», «flagged», «sanitizer cleanup», «stage boundaries», операторские ранги и пример
«CJK leak in the ru output: 第一节» (openapi.yaml:1430 → frontend/src/api/schema.ts:1047 в
редакции 0.2.3) — то есть ровно та строка, которую контракт объявлял запретной. Теперь правило
звучит так: на проводе нет ни имён стадий/волн, ни движковых словарей — ни в полях, ни в
описаниях. Ревью-вопрос каждой правки — §5.
2.13. ПТ-34 — перевод не индексируется — ✓ инвариант
Приложение живёт под X-Robots-Tag: noindex, ответы несут Cache-Control: no-store, ссылка на
выгрузку выдаётся только владельцу.
0.3.0 — две правки. (а) no-store объявлен на ВСЕХ ответах, как его и ставит платформа
(middleware.go:38): контракт требовал его только для ответов с переводом, то есть был беднее кода.
(б) Ссылка экспорта получила НОРМУ доступа вместо прозы: минтится под этот ответ и под
аутентифицированного владельца, не индексируется, истекает в expires_at. Грунт — сама платформа:
«The download URL is minted per request for the owner and is never indexable (PT-34), so it is not a
column» (00002_readmodel.sql:188-199).
2.14. Поверхность входа /auth/* — ✓ построено платформой
Четыре ручки живут ВНЕ версионного префикса, как /healthz: это механика сессии, а не контрактная
поверхность.
| Ручка | Метод | Что делает |
|---|---|---|
/auth/login |
GET | начинает вход, редиректит к провайдеру; принимает ?return_to=<путь этого сайта> |
/auth/callback |
GET | завершает вход, ставит сессионную куку, редиректит на return_to либо на дефолт |
/auth/logout |
POST | завершает ЭТУ сессию |
/auth/logout-all |
POST | завершает ВСЕ сессии пользователя |
return_to принимает ТОЛЬКО путь этого сайта, и чужой путь сервер молча заменяет дефолтом
(login.go:safeReturnTo). Обе POST-ручки лежат на cookie-пути, то есть требуют X-TM-Client.
Отказ входа — problem+json, как везде; различать причины отказа клиент не может по замыслу.
⚠ Эта фраза стояла здесь в 0.2.3, была молча срезана коммитом лендинга батча 8d82096 (ловится
git log -S) и восстановлена синком 20.08 — не как переоткрытый спор, а как правило, которое
никто не отменял. Разбор потери — §6в п.0.
Что это значит для клиента, дословно (синк 20.08, п. C записки платформы). /auth/* отвечает
тем же конвертом, что и /v0, но БЕЗ машинного code — и это решение, а не пробел: клиент
диспетчеризует по статусу и показывает одну нейтральную фразу. Особый случай ровно один — 429
с Retry-After, который вход действительно отдаёт (login.go:173,233, dev.go:138); ветвление по
нему это механизм HTTP по назначению, а не запашок.
Арифметика остатка, из-за которой словарь здесь не нужен: из шести статусов входа четыре уже имеют
точные соответствия в ErrorCode (401 → unauthenticated, 400 → invalid_request,
404 → not_found, 503 → service_unavailable), а из двух оставшихся 405 — баг клиента, не
пользовательский случай. Обсуждался по существу ОДИН код.
Проводного словаря в этом файле не объявляется, и это принципиально. Компаньон объявлен не
нормативным для формы; словарь, записанный здесь, сделал бы его нормативным с чёрного хода — форму
стало бы нечем ни сгенерировать, ни отвалидировать, ни удержать линтером. Механическая половина
разбора уже исполнена платформой без разрешения и правильно: codeForStatus снят
(grep -rn "codeForStatus" --include=*.go platform/ → 0).
Триггер пересмотра, записанный явно: в день, когда на входе появится ВТОРОЙ пользовательски
осмысленный случай — «аккаунт отключён» против «провайдер лёг», — эта поверхность получает
собственный нормативный документ (маленький OpenAPI на /auth/*), а не таблицу в компаньоне.
2.15. Потолок прогона — ◆ форма, РАТИФИЦИРОВАНА оркестратором №15 (08.08)
Решение владельца 07.08: шкала от минимума до максимума, ноль выбрать нельзя, единица — ГЛАВЫ, потолок принадлежит ПРОГОНУ.
Отдельный ресурс, а не поле карточки книги. Максимум зависит от АККАУНТА и двигается, когда книга
не меняется. Три числа, а не два: max_chapters приходит УЖЕ подрезанным и по остатку, и по
непереведённому хвосту; клиенту подрезать второй раз ЗАПРЕЩЕНО. default_chapters отдаёт платформа,
потому что предустановленное значение — продуктовая политика.
⚠ max_chapters — величина, а не арифметика. Ратификация D39.110 в первой редакции требовала
«баланс МИНУС открытые холды»: это была ОШИБКА — вычитание дважды. Холд есть дебет в момент взятия
(pgstore/credits.go:179), поэтому баланс уже не содержит открытых холдов. Замерено на живом
PostgreSQL: грант $10 и холд $1 дают Balance 9 и Reserved 1.
0.3.0 — blocked (В-6, «ОК» владельца 16.08). Шкала второй книги молча ужимается — или исчезает
— когда кредит держит холд ПЕРВОЙ книги, и узнать это из контракта было нечем. Теперь run-options и
409 на старте несут blocked: {code, book_id}. Грунт: платформа знает открытые холды с book_id
(таблица reservations). Один код credit_held — других причин ужать шкалу у платформы сегодня нет:
ErrRunInFlight привязан к СВОЕЙ книге (book.HasLiveRun, runs/runs.go (греп по имени функции)) и вторую книгу не
блокирует.
2.16. Транспорт: тот же origin — ФАКТ, а не выбор
CORS-слоя в платформе нет вовсе: preflight OPTIONS с чужим Origin получает 401 от гарда сессии,
заголовков Access-Control-* нет ни на одном ответе (PD-96). Браузерный клиент с другого origin
неработоспособен как класс. X-TM-Client обязателен и на same-origin — он не про CORS.
0.3.0 — безопасность стала машинной (Б-15). Правило X-TM-Client жило в ПРОЗЕ описания схемы
безопасности: генератор его не создавал, spectral не проверял, контрактный тест не ловил, и клиент
носил его двумя копиями руками (client.ts:13,40, upload.ts:81). Правки: заголовок объявлен
параметром на каждой небезопасной операции · 403 объявлен ответом (раньше его в контракте не было
вовсе, а платформа им отвечает — server.go:99) · 401 объявил обязательный WWW-Authenticate
(RFC 9110 §15.5.2 требует его MUST, ни WriteProblem, ни Deny-обработчик его не ставили) ·
множество небезопасных методов приведено к коду и к RFC (код освобождает и OPTIONS,
auth/csrf.go:51-54; спека говорила «anything other than GET and HEAD», и клиент следовал СПЕКЕ) ·
записан второй карваут (well-formed Bearer, csrf.go:55-62) · servers[0].url стал относительным
/v0 (был абсолютный плейсхолдер https://app.example.org/v0, в который целился бы сгенерированный
клиент) · записана связь «защита работает, ПОКА нет CORS», чтобы её снятие требовало правки контракта,
а не конфига.
2.17. Модель ошибок — вариант B (0.3.0) — ✓ словарь, ◆ форма
Что было. type — константа about:blank на каждом ответе (problem.go:25); detail пуст на
всех вызовах контрактной поверхности; instance объявлен спекой и структурой Problem в коде не
предусмотрен вовсе. Различитель — английская фраза, которую контракт предписывал показывать
пользователю «as-is» при русском интерфейсе (Loaded.tsx:66, RunStart.tsx:188, AddBook.tsx:289).
Фраз при этом меньше, чем причин: «Request could not be read» покрывала шесть разных условий,
«The upload is incomplete» — четыре. Accept-Language и локалей в платформе нет грепом.
Решение владельца 16.08 — «Идём в Б путём гугла» (§8 п.4): машинный code (двухуровневый:
стабильный корневой + расширяемый вложенный) + request_id; title/detail — developer-facing,
клиент их НЕ показывает; серверная локализованная фраза — отдельным полем и только для
неперечислимых причин; errors[] с указателем поля.
Форма, выбранная батчем.
code— корневой, закрытый, 16 значений (0.3.0; 0.5.0 добавил два банковских — §2.19);cause.code— второй уровень, НЕ закрытый. Это и есть механизм расширяемости: новый частный случай добавляется вcause, не ломая клиентов, — то, чем Microsoft закрывает «новый код = ломающее изменение». Второй уровень сделан ВЛОЖЕННЫМ объектом, а не соседним полем: так граница «стабильное / расширяемое» видна структурно, и всё, что внутриcause, по определению вне словаря версии. Глубина ровно одна — рекурсииinnererrorу нас нет.- Словарь класса 1 выведен, а не придуман: перечислением всех ветвей, где платформа сегодня
отвечает ошибкой, —
v0.go:238,242,250,333,339,345,356,409,417,419,423,531(прямые ответы) иv0.go:542-574(fail()), плюсserver.go:99,134. Каждый корневой код имеет там источник; ни одного кода «на будущее» не заведено, кроме двух названных ниже. - Два кода заведены под ратифицированные решения, а не под построенный код:
idempotency_conflict(формаIdempotency-Key— заказ батча, реализация P7) иcontent_refused(класс 2, К-9). - Класс 2 — ОДИН грубый код на весь класс (§8а): без причины, без
cause, безerrors, без вариации между попытками. Опасение владельца точное — чем конкретнее отказ, тем лучше он работает как оракул для подбора входа. Тот же код и тем же правилом заведён значениемRejectReason(content_refused), потому что К-9 ратифицировал «rejected+ грубый код, двенадцатый статус НЕ заводить». typeосталсяabout:blank. RFC 9457 §3.1.1 требует использоватьtypeкак первичный идентификатор — но URI, который никуда не резолвится, или URN, дублирующийcode, это либо обещание, которого мы не держим, либо вторая копия факта. Расширение членами объекта — то, что §3.2 разрешает прямо. Зато отклонение 0.2.3 от §3.1.3 («The "title" string is advisory and is included only for users who are unaware of … the semantics of the type URI») ИСПРАВЛЕНО:titleбольше не показывается пользователю.Problem.instanceСНЯТ (§5а): структуры на платформе нет вовсе, реальная форма ответа —{type, title, status, detail?}, а функцию корреляции несётrequest_id, значение которого уже существует и уже едет на каждом ответе (reqid.go:16,27).localizedобъявлен и сегодня не используется ни одним кодом. Это ратифицированный слот (§8 п.4, формаgoogle.rpc.LocalizedMessage) под класс «причина не перечислима заранее»; носитель — research/28 §8 п.4. Помечено в спеке словами, чтобы предупреждение не пережило своё основание (правило §3 ниже).
Цена на клиенте — меньше, чем кажется: таблица «состояние → русская фраза + совет» у клиента УЖЕ
есть (AddBook.tsx:282-288) и отбрасывается всякий раз, когда сервер прислал любую фразу (:289).
Перевод на машинные коды — снятие одной строки.
2.18. Idempotency-Key — семантика (0.3.0) — ◆ форма
Идемпотентности не было, а контракт сам советовал ретрай как лечение 408 — при том что RFC 9110 §9.2.2
говорит: «A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has
some means to know that the request semantics are actually idempotent … or some means to detect that
the original request was never applied». Каждый POST /books создаёт новую книгу (books.go:125), а
удалить дубликат до 0.3.0 было нечем вовсе.
Семантику определяет контракт, реализует P7 (решение промта батча). Выбрано: область ключа —
(принципал, метод, путь) · повтор того же запроса → ИСХОДНЫЙ ответ, без новой работы · повтор с
другими параметрами → 409 key_reused · повтор во время исполнения первого → 409 key_in_flight ·
окно хранения ≥ 24 часа · длина ≤ 255 · отсутствие заголовка легально и означает «без защиты от
повтора». Область по ПУТИ, а не по телу: ключ, поданный на другую операцию, — другой ключ, иначе
клиент, генерирующий ключ на попытку пользователя, получил бы взаимное влияние двух разных действий.
Окно 24 часа — не замер, а достаточная граница для клиентского ретрая; сокращать его дешевле, чем
удлинять, поэтому взята нижняя обещаемая граница («не менее»).
0.4.0 сменил ОДНУ половину — что такое «тот же запрос» на multipart (§6в E): не объявленные
части плюс длина, а метаданные + имя файла + СОДЕРЖИМОЕ, и сервер, который тождества установить не
может, не реплеит, а отвечает 409. Остальное выше в силе. ⚠ Формулировка §6б ниже («тождество
считается по объявленным частям», дофикс ФБ-8 16.08) этой правкой ПЕРЕКРЫТА и оставлена только как
история — читать по канону.
2.19. Дверь правок банка (0.5.0) — ✓ выведено из tmctl bank-apply; форма HTTP — ◆ этой сессии
Движковая половина ПОСТРОЕНА и заленджена (D39.158, коммит d1eb8a9): tmctl bank-apply — $0-глагол
со словарём (backend/internal/membank/decisions.go), отчётом (pipeline/bankdecisions.go,
BankDecisionsReport) и полосой отказов 10–19 (cmd/tmctl/main.go:69-75). Канон 0.5.0 не сочиняет
тело двери — он ПРОЕЦИРУЕТ этот словарь на провод; платформа при монтаже (пак 2в очереди D39.156)
реализует объявленное: контракт-JSON → документ движка → спавн глагола → отчёт → ответ. Перевод
словаря на шве — закон (17-seam-inbound-law п.6), поэтому ниже каждая форма провода названа с её
движковым источником.
Выведено (✓), с грунтом:
| Провод | Источник в движке |
|---|---|
approve/decline, третьего нет; «un-decide» не существует — решение ЗАМЕНЯЕТСЯ |
decisions.go:33-36,59-63 |
идентичность: id XOR полный кортеж; оба сразу — отказ («назвать два разных терма одним решением») |
decisions.go:88-95,469-472 |
неизвестный кортеж = ДОБАВЛЕНИЕ терма, не ошибка; неизвестный id — отказ с причиной «другая книга или пере-резка» |
decisions.go:94-96 («the form in which a term the bank does not have yet is added»), :473-477 |
dst обязателен и непуст при approve, запрещён при decline (вместе с kind) |
decisions.go:100-103,459-468 |
kind: отсутствие = «не решено», сбросить в null нельзя. ⚠ Провод типизирует его TermKind (5 значений — словарь ЧТЕНИЯ, terminology/classify.go), движок в этой двери принимает любую непустую строку: сужение провода — ◆, см. список ниже |
decisions.go:104-106,632-634 |
note: принимается, НЕ публикуется; отсутствие = «не решено»; опустошить нельзя, только заменить |
decisions.go:107-115 |
aliases НЕ принимаются; promotion несёт кластер минера дальше; decline алиаса ПОДПИСАННОГО терма — инертен и отказан, судится по РЕЗУЛЬТАТУ документа (алиас неподписанной строки деклайнится законно — aliasOwner пропускает status != approved; decline терма и его алиаса одним документом — оба принимаются). ⚠ На ПРОВОДЕ BankTerm алиасов НЕ публикует (снос — слово владельца, D39.136 п.4б): клиент не видит, что поверхность — алиас; отказ приходит refusals[].detail. Названная щель |
decisions.go:78-80, termFromBank, refuseInertDeclines (:399-423), aliasOwner (:641-659) |
gender/speech/decl — полей НЕТ: сид-онли, производителя нет |
decisions.go:81-83 (строка бэклога 210) |
| узость окна v1, ОБЕ половины: approve нового кортежа добавляет строку и оставляет старую; decline снимает ВСЕ окна поверхности | decisions.go:85-87 |
сид-терм не правится этой дверью: approve — клэш с подписанной базой, БЕЗУСЛОВНО; decline — отказ ТОЛЬКО ИНЕРТНОМУ (условие !deltaHoldsSurface: когда правки книги держат свою строку той же поверхности, decline принимается и снимает её — ремонт ливлока «сид-алиас и строка правок делят firing key») |
decisions.go:357-377 (условие — :374; перенос базы — строка 192) |
book_id в теле обязателен И отдельно сверяется с книгой — две разные проверки |
decisions.go:121-124,141-143 и bankdecisions.go:267-269 |
| неизвестное поле — громкий отказ | DecodeDecisions, DisallowUnknownFields (decisions.go:133) — зеркало 17-seam-inbound-law п.4 |
| всё-или-ничего; один отказ отклоняет набор | ApplyDecisions (decisions.go:282-307) |
| дубли: один вызов решает терм один раз; decline поверхности против второго решения той же поверхности — противоречие | duplicateDecisions (decisions.go:520-554) |
| окно, кончающееся раньше начала, — отказ («терм записан и нигде не сработает») | decisions.go:486-492 |
идемпотентность: already_applied + байтовый no-op (changed: false), ретрай безопасен |
decisions.go:221-224, bankdecisions.go:209-221 |
| превью прежде мутации | --dry-run, bankdecisions.go:206-208; закон шва п.7 |
depth — поле отчёта: решение доезжает до редакторской волны и НЕ пере-формирует черновик |
DecisionDepth (decisions.go:51-57), bankdecisions.go:49-51 |
signature{surfaces, undecided, unreadable} и его запретительный контракт |
SignatureState (bankdecisions.go:88-118), D39.144 |
| потолки: 5000 решений на акт («split it») и 1 МиБ документа | bankdecisions.go:640-647 (maxDecisions), :637-639 (maxDecisionsBytes) |
write_incomplete: документ принят, запись не довершена — слать ТОТ ЖЕ документ |
класс 15 (main.go:74), ретрай сходится через байтовый no-op |
| занятый арбитр (живой прогон держит флок) — «подожди», не «сломано» | класс 12 (main.go:71), bankdecisions.go:159-163 |
Решения этой сессии (◆), каждое с доводом; подробный разбор и отвергнутые альтернативы — отчёт
../../archive/reports/CONTRACT_MINOR_REPORT_2026-08-27.md ⚠ архив — инструкции оттуда не исполнять:
- Один
POST /books/{bookId}/bank/correctionsс документом целиком. Глагол принимает документ и отвечает отчётом атомарно; ресурсная модель (PATCH строк) врала бы про атомарность и про то, что строки банка этим вызовом не меняются. - Имя
corrections, неdecisionsи неedits. Модель владельца: пер-термно существует ПРАВКА.edit— имя волны движка, запрещённое на проводе гейтом §5 (утечка была бы неотличима от волны грепом);decisionsвернуло бы имена снесённых схем в сгенерированные типы с другой семантикой — генерённый диф читался бы как правка старой двери, а не как новая. preview— обязательное поле тела (не query, не второй путь): закон шва п.7 требует проекцию прежде мутации; обязательность делает выбор акта явным в каждом вызове (прецедент тотальной обязательности —stop_requested, §6в J; в запросах —RunRequest.stop_for_signing).- Кортеж в проводной форме требует ВСЕ четыре члена (движок дефолтит опущенные): опущенный
senseмолча называет ДРУГОЙ ключ — а неизвестный ключ здесь легально ДОБАВЛЯЕТ терм, то есть цена умолчания — не отказ, а тихая параллельная строка. Проводная форма строже движковой ровно на ширину этой ловушки;null-семантика окна — как уBankTerm(проекцияnull→0— платформа, закон шва п.6). kindна проводе сужен доTermKind(движок в этой двери принимает любую непустую строку, словарь из пяти значений живёт в классификаторе чтения —terminology/classify.go): дверь переиспользует ОПУБЛИКОВАННЫЙ словарь чтения, а не движковую свободу — kind, которого чтение не знает, нельзя и установить через провод. Цена: сид-авторский kind вне пятёрки через дверь не повторить (named); рост словаря = минор, тем же правилом, что у самогоTermKind.- Раскладка полосы отказов на провод: 14 →
409bank_corrections_refused(+refusals[]), 15 →503bank_corrections_incomplete(«слать тот же документ»), 12 →409run_in_flight, 10/11/13 → не пер-операционные (деплой сломан —5xxобщего вида); 16book_incomplete(движковыйtmctl buildбез--partial, D39.175) на ЭТОМ проводе не встречается — его раскладка появится с дверью выдачиcreateExport; движковый исходstopped(SIGTERM до первого байта: ничего не записано) пер-операционного кода тоже не имеет — снаружи это рестарт деплоя,5xx, ретрай сходится; потолок 1 МиБ →413, потолок 5000 и вся валидация формы →400invalid_request. 14 и 15 получили СВОИ корневые коды: разные адресаты ремеди (пере-решить человек / повторить машина), совет промта принят. ⚠ Три обязательства монтажа — ИСПОЛНЕНЫ пакетом P9 (D39.162); список сохранён, потому что он нормативен для всякого, кто эту дверь пере-монтирует: (а) оба потолка движок классифицирует КЛАССОМ 14 («the document is well-formed and the deployment is fine»,bankdecisions.go:656-673), а канон раскладывает их в400/413— платформа обязана мерить оба на ПРОВОДНОЙ форме до спавна, И на той же форме, какую отдаёт движку (рендер с HTML-экранированием раздувает&/</>вшестеро, и законное тело под проводным потолком перепрыгивало движковый); остаточный случай (HTTP-тело < 1 МиБ, отрендеренный документ шва — больше) падает в409bank_corrections_refused, чьё описание несёт «a set larger than the service applies in one act»; (б) канонный400сerrors[/book_id]на несовпадение тела с путём производит ПЛАТФОРМА до спавна — движковая сверка того же факта (bankdecisions.go:267-269) есть класс 10, «конфиг вызывающего», и в400сама не раскладывается; (в) свои вызовы двери по одной книге монтаж СЕРИАЛИЗУЕТ сам: флок движка держит любой глагол, включая второйbank-applyи превью (класс 12 = «another tmctl owns this project», не «книга переводится»), и без сериализации транзиентный держатель отвечал быrun_in_flight— словом про прогон, которого нет. С сериализацией снаружи остаётся ровно живой прогон, и слово точное. - Причины пер-решенческих отказов едут
refusals[].detaildeveloper-facing и НЕ показываются (какProblem.detail): у движка причины — свободный текст (RejectedDecision.Reason), машинного словаря причин нет, а замораживать в каноне пересказ — вторая копия растущего словаря. Названная щель: продуктовые фразы отказов появятся, когда движок даст причинам машинные имена. - Что НЕ пошло на провод из отчёта: пути файлов (
files) и пофайловая правда записи (written_delta/written_rejects) — серверная топология, ремеди клиента от неё не зависит ·canonical_rewrite*— предупреждение оператору файлов, а не пользователю продукта · текстыpreexisting_problems— свободный текст движка; на провод идёт СЧЁТ (preexisting_faults), потому что «книга уже больна, следующий прогон умрёт у банка» пользователю нужен, а формулировки — нет ·replaced[]— свободный текст; на провод идёт булевdisplaced(перекрыл ли ты чьё-то раннее слово — свойство, ради видимости которого поле и существует) ·mode— раскладывается наpreview/changed/HTTP-коды; остатокstoppedразобран в пункте раскладки выше ·decisions_version/report_version— версии ДОКУМЕНТОВ ШВА, на проводе их место занимаетcontract_version. ⚠ Отдельно названное УСЕЧЕНИЕ: на отказе (409) движок печатает ПОЛНЫЙ отчёт (сpreexistingиsignature— «its report of reasons IS its product»), а провод несёт толькоrefusals[]: конвертProblemрасширяется членами про отказ, не квитанцией. Книга, каждый документ которой отказан, своё «уже больна» через дверь не покажет — названная цена v1, не забывчивость. depth: edit_wave → refinement: движковая константа несёт имя волны — на провод идёт продуктовое слово с тем же смыслом, открытым словарём; обе половины смысла (следующий прогон; черновик не пере-формируется) продублированы словами в описании — это первое, о чём экран соврал бы.signatureопубликован ВМЕСТЕ с запретительным контрактом (обе половины D39.144): без запрета экран подписи при разморозке выучил бы из канона «доведи число до нуля» — отменённую модель через чёрный ход; без положительной половины поле выглядело бы бесполезным и его бы не строили.map(путь карты) на провод не идёт; «карты ещё нет» выраженоsignature: null.- Признак «не построено» —
Capabilities.bank_corrections_enabled(прецеденты:intake_enabled— булев с объявленным404,export_formats— «пусто = не построено»).Capabilitiesгодится: один плоский документ деплоя, одинаковый для всех аккаунтов, читается до предложения UI. Без признака минор воспроизвёл бы PD-370 тем же коммитом, которым закрывает. - Квитанция — НЕ чтение: ни
revision, ниstructure_versionне едут — вызов не двигает read-модель (вид банка пересобирается на границе прогона,18-bank-ontology.md), кадрbankне испускается, и канон говорит это прямо, чтобы «исправил, а банк не изменился» читалось как корректность, а не как баг.
2.19-бис. Счётчики pending_decisions/complete — СНЕСЕНЫ (0.5.0)
Оба поля кормились платформенной таблицей bank_decisions, чей write-путь снесён 22.08 вместе с
пер-термной моделью: новая дверь пишет файлы движка, не эту таблицу. ⚠ «Поля навсегда нули»
(буква промта пака) опровергнута кодом при вычитке опровергателем — на деле ХУЖЕ нулей:
pending_decisions считается как «proposed-строки, которых не коснулось ни одно решение», а
касаться нечем — то есть это число ВСЕХ proposed-строк банка, живое на каждой пересборке, и
complete вырождается в «предложений нет вовсе» (platform/internal/pgstore/readmodel.go,
bankCountsTx — комментарий признаёт это прямо). Замороженно-правдоподобное число учит
отменённой модели живым счётчиком. Из трёх исходов (снести · пере-определить на новую дверь ·
пометить) выбран СНОС со всех трёх носителей (BankPage, EventBank, квитанция — вместе со
схемой):
- пере-определить нельзя: честный счёт нерешённости — движковый (
SignatureState: карта последнего стопа против файлов решений); read-модель платформы его НЕ вычислит, не пере-реализовав движковый закон у себя, что запрещено (17-seam-inbound-law п.6;18-bank-ontology.md, «Чего эта форма НЕ несёт»); - пометить («пока нули») нельзя: поле, обязательное в схеме и вечно лгущее нулём, — это ровно класс PD-370 («канон объявляет — деплой не обслуживает»), только в поле вместо пути;
- ратифицированная проза «Signing the bank is ONE act over the whole of it» при сносе СОХРАНЕНА в
описании
listBankTerms; фраза «marked unverified inside the service» из того же абзаца снята — это обещание без носителя, снятое ещё ФБ-8 (§6б) и уцелевшее в одном месте.
Цена, названная честно — ТРЕМЯ половинами, и две из трёх ПОГАШЕНЫ лендингом P9 (D39.162):
(1) чтение банка больше не отвечает «сколько осталось» — экран узнаёт счёт из квитанции двери правок
(signature), то есть только имея что послать или что превьюировать: эта половина В СИЛЕ;
(2) до монтажа пака (2в) счёт недоступен НИГДЕ — окно между минорами закрыто, дверь смонтирована
и отвечает; (3) проекция — сняты, носитель GET /bank деплоя ещё шлёт снятые поляPD-399
закрыт. Возврат счёта в чтение — день, когда экран подписи закажут
и движок опубликует нерешённость проекцией (18-bank-ontology.md, «Чего эта форма НЕ несёт»);
сегодняшние носители лгать не будут.
2.20. Сквозная полоса прогресса и подпись стадии (0.6.0) — ✓ выведено из построенного сервера
Что изменилось. Progress перестал быть счётчиком СЕГМЕНТА между двумя стопами и стал ОДНОЙ
монотонной долей на всю работу прогона через все его проходы. Оговорка «при снятии стопа счётчик
начинается заново с нуля» снесена; добавлен обязательный член stage.
Почему минор приехал ПОСЛЕ сервера, а не до него. Требование владельца (строка 200) — одна доля
на всю работу прогона, считает СЕРВЕР (иначе клиент снова начнёт знать про фазы). D39.160 изъял его
из минора 0.5.0 ровно потому, что гейт версии требует совпадения канона и деплоя В МОМЕНТ лендинга:
объявить сквозную полосу раньше, чем сервер её считает, значило бы завести класс PD-370 в поле
вместо пути. Канон догнал сервер тем же коммитом, которым сервер приехал.
Провенанс формы — код, а не рассуждение (platform/internal/pgstore/readmodel.go): числитель
складывает вклад каждого прохода (runDone), знаменатель — работу, которую прогон РЕАЛЬНО должен
(runTotal), и каждая половина мерится от СВОЕЙ базовой линии прогона (chapters_before для
последнего прохода, draft_before для чернового), после чего каппится покупкой. Отсюда обе новые
фразы канона — «счётчики только растут и ничто под ними не движется» и «прогон, кончившийся рано, до
total не доходит»: первая описывает базовые линии, вторая — то, что доля до единицы обязана
доходить лишь у ЗАВЕРШЁННОЙ покупки.
⚠ stage — ВТОРОЕ исключение из границы «ничего о том, КАК переводится книга». Первое — банк:
стоп, который снимает пользователь, спрятать нельзя. Второе завёл этот минор, потому что бар,
который движется молча, отвечает хуже, чем бар с подписью. Исключение ОГРАНИЧЕНО двумя условиями, и
оба выполняются кодом уже сейчас: значение ВЫВОДИТСЯ платформой из тех же счётчиков, что и бар
(runStage — SQL над теми же колонками, а не проброс движковой строки), и словарь ОТКРЫТ — клиент
обязан рисовать незнакомое значение нейтрально. Следствие, ради которого условия и поставлены:
пара или конвейер с ИНОЙ формой работы не требуют нового клиента и не поднимают версию.
Сегодняшние значения — drafting, editing и re_pass (⚠ испр. 02.09: третье значение выдаёт runStage для всего пере-прохода — platform/internal/pgstore/readmodel.go, греп re_pass — и оно доезжает на провод).
⚠ Ложная перекрёстная ссылка, снятая этим же минором. ChapterProgress.units_done обещал «то же
счетоводство, что у Progress, уровнем ниже». После сквозной полосы это неправда: глава по-прежнему
обнуляется каждым новым проходом, прогон — больше никогда. Оставленная, фраза научила бы клиента
зеркалить одно в другое и рисовать прыгающий бар.
2.21. Покупка пере-прохода (0.7.0) — ✓ выведено из построенного механизма движка
Что появилось. RunRequest.re_pass — булев член; ceiling_chapters вышел из required и
объявлен взаимоисключающим с ним. Плюс объявленная форма полосы такого прогона и ограничение фразы
«finished work is not bought twice».
Зачем. Главный пользовательский цикл продукта — «поправил термин → правка доехала в уже
переведённый текст» — до 0.7.0 был НЕВЫРАЗИМ на проводе: у дочитанной книги остаток нулевой, шкала
даёт максимум 0, валидного ceiling_chapters не существует физически. Канон при этом обещал, что
правка «takes effect on the NEXT run», а следующего прогона купить было нечем — две фразы канона
складывались в дедлок, и 0.7.0 его снимает.
Провенанс — движок, а не проектирование. Механизм построен и ратифицирован задолго до этого
минора: translate --resnapshot определяет затронутые правкой юниты байт-сверкой отрендеренного
запроса, незатронутые пере-привязывает за $0, а согласие на пере-плату берётся флагом
--accept-rebill. Минор не изобретает механику — он открывает к ней дверь. ⚠ Проектирование самого
продуктового ЦИКЛА пост-ридинга (отдельная ручка «перегенерировать», dry-run, доезд правки до
черновика) остаётся гейченным полигоном; область D39.144 сужена явно D39.165.
⚠ Чего минор НЕ объявляет, и это названная цена, а не забывчивость. Смету «затронуто N юнитов»
ДО покупки провод не несёт. Причина установлена исполнением и стоила пересборки пака: bank-apply
пишет только ФАЙЛЫ решений, а движковый status считает ре-билл от СОХРАНЁННОГО глоссария
(backend/internal/pipeline/status.go (греп orphanStageRows) — его собственный комментарий предупреждает, что
правка файла, ещё не прогнанная, здесь не отражается). Свёртка происходит внутри СЛЕДУЮЩЕГО
translate, поэтому сразу после правки движок честно отвечает «ничего не двигалось». Смета вернётся
на провод вместе с движковым глаголом «свернуть банк и оценить ВНЕ прогона» — до тех пор согласие
даётся ДЕНЬГАМИ: потолок пере-прохода равен холду, который пользователь уже внёс.
⚠ Форма полосы объявлена, а не выведена, и причина названа в самом каноне. total = 1, done =
0 → 1 на чистом финише. Первое решение оркестратора («полоса в главах») ОТМЕНЕНО эрратой 28.08-к:
движок анонсирует работу ОДИН РАЗ за жизнь книги, поэтому пере-проход, который переделывает уже
анонсированное, новых анонсов не производит — глава-полоса стояла бы на нуле навсегда. Более дробная
полоса была бы полосой, которая не движется.
2.22. Вторая граница пересчёта и shape_epoch (0.9.0) — ✓ выведено из построенного сервера
Что. У chapters_done было ОДНО событие, легитимно пересчитывающее счёт, — пере-нарезка книги, и
она называла себя structure_version. Событий оказалось ДВА: счёт пересчитывается и когда деплой
меняет, что для главы значит «готово». Второе событие получило свою координату — Book.shape_epoch,
целое, растущее.
Почему координата, а не молчание. Без неё клиент видит скачок chapters_done в обе стороны и не
может отличить легитимный пересчёт от ошибки сервера: «никогда не движется назад» переставало быть
правдой, а замены ему не было. Инвариант переписан точно: счёт не движется назад внутри одной пары
(structure_version, shape_epoch); через любую из двух границ — пересчитывается, и клиент
пере-читает, а не считает это ошибкой.
Почему это НЕ третье исключение границы (D39.163). Поле НЕПРОЗРАЧНО по построению: целое, которое сравнивают с предыдущим, и оно сообщает только ЧТО поколение сменилось. Оно не отвечает на вопрос «как переводится книга» — ни имени фазы, ни стадии, ни модели. Ратификация №20 (31.08) стоит именно на этом различении, и он честно записал его как СВОЁ суждение, а не как факт.
⚠ Норма, которую приёмка этого минора вывела и которая держится на следующий: непрозрачность ПОЛЯ не спасает, если проза рядом прозрачна — граница судит и описания, потому что они компилируются в исходник клиента (D39.180 п.б).
Механика — в зоне платформы: пара колонок books.shape_epoch + books.epoch_editor (миграция
00030_shape_epoch.sql, бэкфилл epoch_editor = edit_wave, поэтому первая граница — только реальная
смена формы). ⚠ Полоса ПРОГОНА на эпоху НЕ переведена: зона попробовала и откатила своим же
адверсариальным проходом — эпоха присваивается и ходит в обе стороны, отчего полоса переставала быть
монотонной (замерено: один прогон читал 4/4, потом 2/2 на своих же двух попытках при неизменной
structure_version). Через эпоху считается ТОЛЬКО пожизненный счёт книги; остаток — открытая строка
регистра платформы PD-435.
2.23. Дверь выдачи, описанная построенным (0.10.0) — ✓ выведено из работающей двери
Что. Первый минор, целиком собранный из расхождений «канон обещает — код делает», найденных живым ПЛАТНЫМ прогоном и адверсариальным проходом зоны. Четыре позиции, каждая правит КАНОН, а не код: код здесь оказался прав.
failure_code — enum из четырёх. Поле само поставило себе условие («becomes an enum with the
first built format»), и условие наступило. Набор — то, что деплой ПРОИЗВОДИТ, а не то, что можно
вообразить: book_empty · deployment_error · build_interrupted · build_failed
(platform/internal/exports/exports.go, греп the Export.failure_code vocabulary). ⚠ book_empty
намеренно ОДИН код на две ситуации — книга нарезана и пуста, и книга, которую ещё никто не нарезал:
факт читателя один («в этой книге нечего отдавать») и лечение одно. ⚠ deployment_error не назван по
формату: тот же класс покрывает книгу без языкового тега, нечитаемый langpack и недоступный каталог,
и фраза «формат недоступен» была бы ложной для большинства причин — это находка адверсариального
прохода, а не переименование по вкусу.
Export.url — uri-reference, и честный механизм. Отдаётся ОТНОСИТЕЛЬНЫЙ путь; абсолютный
заставил бы сервис знать свой публичный ориджин, которого за edge-прокси он не знает. Прежняя
формулировка «minted for THIS response» читалась как подписанная капабилити — механизма такого никто
не строил, ссылка АУТЕНТИФИЦИРОВАННАЯ и защищена сессией. Это строго сильнее: утёкшая ссылка чужому
бесполезна, и деплойного секрета, который никто не ротирует, она не требует. ⚠ Вхождение ОДНО.
Оркестратор №22 при ратификации объявил их два, сверив грепом format: uri и не прочитав второе
место: там поле type проблемы RFC 9457 со значением about:blank, абсолютный URI по стандарту, и
трогать его нельзя. Ошибка исправлена ДО правки канона; класс — тот же, что в D39.193 (ратификация,
выданная без чтения предмета).
Третий адрес двери получает operationId. …/exports/{exportId}/content жил в каноне только
прозой, и генерируемый клиент про него не знал — при том что браузер по нему НАВИГИРУЕТ, и это
единственный способ существования поверхности. Теперь downloadExport, с объявленными 206 на Range
и 410 на протухшую ссылку.
Шестнадцатая причина замечания. off_target_lang → wrong_language; разбор и ⚠ о ранге — в
Приложении А.
2.24. ⛔ ЗАПРЕТ «ДЕНЕГ В ИНТЕРФЕЙСЕ НЕТ» ОТОЗВАН ВЛАДЕЛЬЦЕМ (05.09, прозой — не минор)
Что случилось. Норма «оплат в MVP нет, денежных полей в UI нет вовсе» месяц определяла форму
продукта. Владелец 05.09: «я такого правила не ставил, надо от него избавляться». Форма —
отзыв собственного решения: текст записан в теле D39.84 (05-decisions-log.md:222) в перечне
«(1) Восемь решений владельца». ⚠ Нота пришла РЕЛЕЕМ отчёта фронт-сессии, и что в ней прямая речь, а
что пересказ релея, по тексту не различимо — та же неразличимость, которую D39.112 п.3 пометил у
соседней ноты.
⚠ Первая редакция этого раздела утверждала «текста правила в журнале НЕТ» — ложное отрицание,
выведенное из СЛОМАННОЙ команды (grep с | без -E: в basic-regex черта — литерал, находилась
только собственная цитата паттерна). Поймал внешний рецензент, пере-запустив ту же команду. Норма,
купленная этим: команда, на которой стоит отрицание, приводится в тексте и обязана быть
воспроизведена другим исполнителем.
Что теперь верно. Баланс аккаунта — деньги и показывается деньгами; потолок заказа и холд — тоже. Остаётся запрещённым и НЕ отменялось: цена модели, стоимость стадии, стоимость вызова, структура НАШИХ расходов (ПТ-33). Линия: деньги, которыми владеет аккаунт, — его; деньги, которые тратим мы, — наши.
Форма показа — пара, а не точка. Проекция имеет дисперсию (редакторские вызовы 25–90 % от worst-case, хвост ретраев 1.5–23 % между прогонами, D39.165 §1), поэтому точечное «$1.14» через прогон превращается в «вы сказали 1.14, списали 1.31». Верная форма: «ожидаемо ≈ $1.1, зарезервируем до $1.6, спишем по факту», и закрытие после прогона фактом. Главный ответ — фраза «хватит на всю книгу»; цифра рядом и мельче.
Почему прозой, а не минором. Схема Credit НЕ тронута: поле суммы приезжает с паком, который
строит форму заказа. Двигать канон впереди кода — дефект, за который в тот же день написана эррата
04.09-в. Проза перестаёт лгать сегодня, поле появляется со своим кодом.
Носители запрета, которые обязан закрыть тот же пак: 05-decisions-log.md:222 · :246 (D39.100
К-8 «запрет в силе и НЕ superseded») · product-requirements.md ПТ-35 · 15-money-path.md §запрета ·
openapi.yaml §Transport и §Credit · зеркало фронта · API_CONTRACT_INPUT.md §4.8 ·
platform/docs/ENGINEERING_STANDARDS.md (зонный — чинит зона).
3. Зависимости: чтение → источник → строка бэклога
Правило, введённое 0.3.0 (Б-21): предупреждение о недостроенном ОБЯЗАНО нести номер строки бэклога (а где строки нет — явного носителя-документ). Без него абзац переживает своё основание и начинает лгать — что уже произошло четырежды, и одно из четырёх отняло у пользователя работающее действие. Таблица ниже — единственное место, где это ведётся.
| Чтение / механизм | Источник данных | Состояние | Строка / носитель |
|---|---|---|---|
GET /books, GET /books/{id} |
read-модель платформы | ПОСТРОЕНО | — |
GET /books/{id}/run-options, POST /runs |
шкала + холды | ПОСТРОЕНО | — |
POST /runs/{id}/stop |
реконсилятор | ПОСТРОЕНО (кнопки на экране нет) | зона фронта |
POST /runs/{id}/resume |
реконсилятор | ПОСТРОЕНО, но 0.3.0 сменил поведение на паузе по потолку, а 0.4.0 — на ИСЧЕРПАННОМ прогоне. Пауза: платформа уже отвечает 409 ceiling_reached (runs/reconcile.go, ветка case "paused" в Resume) — совпало. Исчерпанность: ⚠ РАСХОЖДЕНИЕ ЗАКРЫТО (испр. 02.09) — Resume больше не отвечает 202 на исчерпанный прогон. Реконсилятор различает ДВА вердикта, и оба доезжают до клиента кодом 409 run_not_resumable: ceilingSpent → cause.code: ceiling_reached и creditUnavailable → cause.code: credit_unavailable (platform/internal/runs/reconcile.go, греп ceilingSpent; platform/internal/httpapi/v0.go, греп run_not_resumable). Почему молчаливый 202 был дефектом — §6в A. Полная таблица по статусам — в описании resumeRun |
вход P7 ⚠ ИЗРАСХОДОВАН (D39.153) (правка построенного пути, не только читающей поверхности) |
GET /usage |
кредиты | ПОСТРОЕНО, не читается ни одним экраном | зона фронта |
GET /capabilities |
конфигурация деплоя | ПОСТРОЕНО P7 — маршрут смонтирован безусловно (platform/internal/httpapi/v0.go, таблица contractSurface), хендлер capabilities.go; версия контракта запинена ГЕЙТОМ против ЭТОГО канона (platform/internal/gates/contract_test.go) |
закрыто D39.153 |
PATCH/DELETE /books/{id}, GET /runs/{id} |
колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) — роутер монтирует 18 операций из 21 (испр. 05.09: дверь выдачи построена adf5e53, прежнее «15 из 20» протухло) |
⚠ носитель «вход P7» ИЗРАСХОДОВАН (пак P7 принят D39.153); живой носитель — platform/BACKLOG.md П-17 и очередь CURRENT-STATE |
GET /books/{id}/chapters, /units |
материализация манифеста | ПОСТРОЕНО P7 — httpapi/reading.go, материализатор internal/readmodel |
закрыто D39.153 |
GET /books/{id}/notes |
unit_done несёт флаг и причину (runevents.go:126-135), платформа хранит (sink.go:227-233), колонка notes.reason заведена под это |
канал ЕСТЬ; не хватает карты «причина → код → фраза» (приложение А) и проекции | приложение А + ⚠ носитель «вход P7» ИЗРАСХОДОВАН (D39.153) |
GET /books/{id}/bank |
движок пишет сайдкар всего банка (pipeline/bankexport.go:16-33,72, D39.122) |
⚠ пере-снято 0.5.0: проекция платформы ПОСТРОЕНА (P7 — маршрут в contractSurface, wireBankPage в httpapi/reading.go, SaveBank в pgstore); форма синхронна канону с лендингом P9: счётчики pending_decisions/complete сняты с проекции (PD-399) |
закрыто D39.162 |
POST /bank/decisions |
стоп-механика майнера | ⚠ НЕ «не построено», а ОТМЕНЕНО: было построено P7 и СНЯТО 22.08 вместе с пер-термной моделью подписи (D39.144, слово владельца). 0.5.0 снёс и канон-половину — PD-370 закрыт этим минором; преемник — строка POST …/bank/corrections ниже |
отменено D39.144; снесено 0.5.0 |
POST /books/{bookId}/bank/corrections |
tmctl bank-apply — движковая половина ПОСТРОЕНА (D39.158, лендинг d1eb8a9) |
ПОСТРОЕНО пакетом P9: перевод словаря (platform/internal/ingest/bankdecisions.go), спавн глагола (runner/bankapply.go), раскладка отказов и пер-книжная сериализация (runs/bank.go), дверь синхронная. Флаг bank_corrections_enabled следует включённости прогонов (cmd/tmplatformd/runner.go:195) — дверь спавнит тот же глагол |
закрыто D39.162 |
GET /books/{id}/events (SSE) |
эмиттер шва построен (D39.131) | ПОСТРОЕНО P7 — httpapi/stream.go, поток регистрируется ВНЕ слоя сжатия (сжатие буферизует поток — единственное, что канон запрещает этому маршруту) |
закрыто D39.153 |
POST/GET /exports |
движок: tmctl build [--format epub,txt] [--out path] [--partial] — EPUB 3 + чистый txt (backend/internal/bookfile), конверт tm-build-v1, пути в StatusArtifacts.book_files, отказ exit 16 book_incomplete |
⚠ пере-снято 30.08: ДВИЖКОВАЯ ПОЛОВИНА ПОСТРОЕНА (D39.175, 8adcb86); открыта только дверь платформы — она обязана СТРОИТЬ через tmctl build (не читать файл у БД: там копия прежней сборки) и сверять BuildReport (config_drift/stale_unknown). ⚠ Это про ФАЙЛ КНИГИ; редакторский tmctl export/annot-v1 (D29.1а) — ОТДЕЛЬНАЯ, не закрытая работа |
движковая половина — D39.175; дверь — пинг зоне платформы (её журнал, греп Движок отдаёт книгу файлом); annot-v1 — строка 49 / D29.1 |
Условные чтения (ETag/304), сжатие |
— | ПОСТРОЕНО P7 — httpapi/conditional.go; валидатор считается от БАЙТ ответа. Остаток строки 186 — шаги 3–5 (скоуп кадра, дельта-чтение, staleTime) |
шаги 1–2 закрыты D39.153 |
Idempotency-Key |
— | ПОСТРОЕНО P7 — httpapi/idempotency.go + pgstore/idempotency.go, на ТРЁХ создающих вызовах (третий — дверь выдачи, httpapi/exports.go; было два до 04.09); ⚠ живой дефект под конкуренцией — PD-369 |
закрыто D39.153 |
bearerToken — чем ВЫДАЁТСЯ токен |
вход только ставит HttpOnly-куку (login.go:338) |
сервер токен ПРИНИМАЕТ, выдать его нечем | носитель: research/28 §2 (Б-15); строки нет |
Problem.localized |
— | объявлено, ни одним кодом не используется | носитель: research/28 §8 п.4 |
Note.code как enum спеки |
карта приложения А | не enum, пока не написаны фразы | ⚠ прежний носитель «строка 148» МЁРТВ (a023e39, 21.08; разбор — приложение А, п.3); живой носитель — строки бэклога 203 + 204 (словарь фраз) |
Настоящие названия глав (Chapter.heading ≠ null) |
парсер структуры | НЕ ПОСТРОЕНО | строка 160 (Этап 0) |
title_raw / kind (глава ↔ фрагмент) |
дизайн-пак структуры глав | передано паку, аддитивно | строка 161 |
ErrorCode.content_refused (400) и RejectReason.content_refused |
прескрин злоупотреблений | НЕ ПОСТРОЕН ни на одной стороне: в платформе только объявление константы (httpapi/problem.go:42,94), ContractRejectReason (ingest/vocabulary.go:59-69) его не отображает; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ (disposition.go:60-63 → Note.code: content_withheld) и в exit-контракт не выходит — мостá между двумя словарями нет |
строка 94 (ПТ-16); там же ограничение числа попыток аккаунта — обязанность падает ВМЕСТЕ с производителем, не раньше |
decline в подписи банка доезжает до работы |
⚠ пере-снято 0.5.0: старый носитель («таблица bank_decisions, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на BankDecision.action снесено вместе со схемой |
дверь смонтирована (D39.162), и decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ Живой остаток — ДВИЖКОВЫЙ, один: decline не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, mining.go:548), тогда как канон обещает «declining a surface removes EVERY window of that surface» |
строка 228 единого бэклога; сходимость повтора закрыта D39.164 §3.3 |
| Снятие замечания (переход «флаг снят») | движок | НЕДОСТИЖИМО сегодня, проверено чтением движка — п. H §6в | PD-298 регистра платформы (platform/docs/DEFECT_REGISTER.md) — там строка и живёт; механизма не строим |
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | вход P7 ИЗРАСХОДОВАН (D39.153) (PD-202) |
⚠ Три из четырёх протухших предупреждений (Б-7а) сняты молча — они лишь устарели; живой хвост
одного: зона фронта до сих пор учится по старой версии «GET /bank — канала нет вообще» (Ф-43).
⚠ Четвёртое было ХУЖЕ устаревшего — оно было НОРМАТИВНЫМ и отнимало работающее лечение. Спека
0.2.3 писала: механизма поднятия потолка нет, «so the client MUST NOT offer resume as the remedy for
paused», — и не называла НИКАКОГО другого действия. Первая половина верна: resume действительно
не двигает такой прогон. Вторая — нет: стоп по потолку ЗАКРЫВАЕТ прогон, finished_at пишется тем же
оператором (pgstore/runs.go:672-676), paused входит в допустимые для старта состояния
(runs/runs.go (греп по имени функции), allowlist readyToTranslate), HasLiveRun при этом ложь — то есть новый
прогон с бОльшим потолком запускается и является лечением уже сегодня. Пользователь видел тупик там,
где его нет, на самом частом остановочном состоянии. В 0.3.0 лечение записано в startRun и в
resumeRun, а resume после потолка отвечает 409 с cause.code: ceiling_reached.
4. Открытые вопросы
| # | Вопрос | Статус |
|---|---|---|
| К-1 | Словарь статусов | ✅ ЗАКРЫТ D39.100; 0.3.0 снял finalizing и развёл RunStatus — §2.6 |
| К-2 | Титул главы: поле heading или вклейка в текст |
○ ОТКРЫТ, автор контракта + бэкенд. 0.3.0 закрыл ОТДЕЛЬНОЕ противоречие (запрет клиентской служебной метки снят), но кто производит настоящую метку — строка 160 |
| К-3 | Метка «Глава N» как единственная форма | ✅ ЗАКРЫТ D39.100 (метка — из ДАННЫХ книги). 0.3.0: служебный рендер «Глава N» разрешён КЛИЕНТУ, в локали интерфейса |
| К-4 | Ревизия: сквозная или пер-ресурсная; несут ли её чтения | ✅ ОТВЕЧЕН P0, подтверждён ревью с поправкой: ревизия и позиция потока — РАЗНЫЕ величины, совмещать в id нельзя (0.3.0, §2.11) |
| К-5 | Показывать ли ETA | ✅ ЗАКРЫТ D39.100 (eta_seconds в спеке; 0.3.0 сделал поле required+nullable) |
| К-6 | Ступени замечания: сколько и где граница | ○ ОТКРЫТ, владелец. Ответ 16.08: «показывать ВСЕ; в тексте сворачивать и раскрывать по кнопке; как именно — решим потом». Словарь ступеней проектировать заранее НЕ надо — батч его и не проектировал. Зависимость закрыта: Note.id заведён (0.3.0), без него конкретное замечание нельзя свернуть и запомнить |
| К-7 | Пагинация: курсор или один ответ | ✅ ОТВЕЧЕН P0. 0.3.0 добавил недостающее: максимум limit, подрезание вместо понижения, объявленный порядок каждой коллекции, maxItems у decisions |
| К-8 | Стоп по потолку — каким статусом | ✅ ЗАКРЫТ D39.100 (paused + оповещение) |
| К-9 | Отказ прескрина не выразим статусами | ✅ ПРИНЯТО ВЛАДЕЛЬЦЕМ 16.08: прескрин — ещё одна ПРИЧИНА, а не двенадцатый статус: rejected + ОДИН грубый код (content_refused), максимально абстрактно, без вариации между попытками (§8а). Заведено 0.3.0 |
| К-10 | Пофазность у главы | ✅ ЗАКРЫТ — вердикт «НЕ строить» (D39.138, поправка приёмки research/28 №1). Пофазных счётчиков на главу не будет: фаз на проводе нет. Исходная жалоба («дерево читает ноль всю первую волну») лечится СЕГМЕНТНОЙ логикой Chapter.units_done — §2.5 |
| К-11 | Условная обязательность полей | ○ ОСТАТОК ИНСТРУМЕНТАЛЬНЫЙ. 0.3.0 применил правило «схема + слова» к трём местам (BankDecision.dst, Unit.target, агрегаты BankPage; 0.5.0: место BankDecision.dst унаследовала BankCorrection — там теперь if/then/else и oneOf идентичности, все продублированы словами) и сделал адресацию Note обязательной. Остаток — генератор игнорирует if/then; лечится сужением на шве клиента (S5) |
| К-12 | Завершение выгрузки: опрос или событие | ✅ ОТВЕЧЕН P0 (опрос), подтверждён AIP-151 («The response must not be a streaming response»). 0.3.0 добавил то, без чего опрос не завершался: state вместо булева ready, failure_code, expires_at |
| К-13 | paused_reason не различает две беды |
✅ ЗАКРЫТ D39.132 п.2а (null на проводе ратифицирован). 0.3.0 добил остаток: описание требовало «null in every other state», что противоречило ратифицированному «paused + null» |
5. Ревью-вопрос: «Сменится устройство пайплайна — придётся ли править фронт?»
⚠ Прежний ответ этой таблицы был ОПРОВЕРГНУТ ИСПОЛНЕНИЕМ (research/28 Б-0, разбор — D39.138) и здесь не сохраняется даже как история: он учил зону неправде — «фронт не правится» было ЛОЖНО для 0.2.3, где добавленная волна давала молчаливые 100 % и TypeError в рендере.
Ответ редакции 0.3.0 — и теперь он верен, потому что чинили ПРИЧИНУ, а не формулировку:
| Изменение в движке | Правит ли фронт |
|---|---|
| переименована стадия / добавлена/снята волна | нет — числа волн на проводе больше нет: одна полоса до ближайшей остановки, знаменатель — купленный объём (§2.5). Пофазный сплит остаётся ВНУТРИ платформы, колонки не трогаются |
| сменилась модель, маршрутизация, температура, промпт | нет — в allowlist не входят |
| добавлена новая причина флага | нет — на провод идёт КОД, карта живёт в контракте (приложение А); незнакомый код → нейтральная фраза, правило записано в схеме |
| добавлена новая причина отказа запроса | нет — второй уровень cause.code не закрыт по замыслу; клиент матчит корневой code |
| добавлен новый тип термина / новый провенанс | нет — словарь расширяется минором, ветка неизвестного стоит на шве |
| сменился чанкер, главы пере-разобраны | нет по коду, ДА по данным — и теперь это ВИДНО: structure_version двигается, курсоры и якоря на пары объявлены недействительными, на исчезнувшую главу отвечает 410, полная замена — resync_required. До 0.3.0 клиент молча рисовал старое дерево |
| добавлено новое ПРОДУКТОВОЕ состояние | да, один файл — карта «статус → вид» на шве src/api/; это и есть контрольный вопрос владельца |
Правило, которое отсюда следует и действует на КАЖДУЮ правку контракта (0.3.0):
На проводе нет ни имён стадий/волн, ни движковых словарей — ни в полях, ни в ЗНАЧЕНИЯХ, ни в описаниях. Описания компилируются в исходники клиента как JSDoc генерённых типов, поэтому объяснительная проза подпадает под тот же запрет, что и поля. Проверять грепом финальной спеки по списку:
draft · edit · wave · stage · mined · miner · ruby · finalizing · chunk · langpack · sanitiz · flagged · verdict · prompt · engine · pipeline · glossar · escalat · snapshot(список открытый — дополнять по мере находок). Единственное легальное вхождение — сама формулировка этого запрета в шапке спеки.Гейт под это правило — тестом, по образцу языкового
generality.test.ts— половина ФРОНТА:.spectral.yamlдержит толькоspectral:oas, аgenerality.test.tsловит лишь языковую специфику, то есть утечку конвейера не стережёт ничто. Носитель: пинг фронту 16.08 вfrontend/docs/frontend-PROGRESS.md, исполнение при разморозке зоны (D39.136 п.2).
6. Направления: что решено НЕ делать в 0.3.0
Записано направлениями, чтобы не превратиться в молчаливые дыры. Номеров бэклога здесь не выдумывается: где строки нет, носителем назван документ.
| Что | Почему не в батч | Носитель |
|---|---|---|
POST /books/{id}/parts — дописать главы в существующую книгу |
форма и движковый гейт против МОЛЧАЛИВОЙ перекупки хвоста при вставке не в конец — отдельная работа вместе с этапом структуры глав | строка 185 |
| Снятие жанра из брифа и промптов | двигает BriefHash ⇒ только в общее resnapshot-окно |
строка 184 |
| Сворачивание замечаний в тексте под кнопку | зона фронта, при разморозке | пинг фронту 16.08 |
| История прогонов книги | снята решением владельца 16.08: «в МВП не нужно». Данные уже в Postgres и не удаляются, индекс runs (book_id, started_at desc) стоит — если поддержка попросит, это один read-путь |
research/28 §8 п.10 |
Двухшаговая загрузка (метаданные JSON → PUT байтов) |
три из четырёх greenfield-дизайнов выбрали её, и при ней проблема порядка частей не существует. Ц3: переписывается построенный путь с обеих сторон. Направление на после-беты | research/28 §5 «не в батч» |
Лента изменений структуры (added|removed|moved|split|merged) с наследниками |
дизайн-пак структуры глав | строка 161 |
| Поиск и фильтр по банку и дереву | нужны индексы (Ц2); сегодня клиент ищет в браузере, и это осознанно | research/28 §5 «не в батч» |
| «Грубая группа статуса» для эволюции словаря | эволюционный механизм без сегодняшней боли | research/28 §5 «не в батч» |
| Словарь ступеней замечаний | К-6: владелец — «решим потом» | research/28 §8 п.11 |
Добавочные поля BankTerm (evidence/variants/conf, aliases) |
слово владельца 15.08: «НЕ заводить … смысла хватает» | D39.136 п.4б |
| Поток на БИБЛИОТЕКУ (одна лента на все книги аккаунта) | поток книжный (§5 п.5 заказа — «перевесить на книгу»); экран библиотеки обновляется чтением, которое под условным чтением стоит заголовков. Ленты на библиотеку нет, и открывать поток на строку списка клиент НЕ должен — это записано нормой в спеке. Найдено холодным потребителем как реальный перф-вопрос | research/28 §5б · строка 186 |
Чтение ОДНОЙ главы GET /books/{id}/chapters/{chapterId} |
сегодня кадр chapter про главу, которой клиент не держит, игнорируется (норма записана), а 410 лечится перечитыванием дерева. Операции нет — она не в заказе; вопрос дизайн-пака структуры глав |
строка 161 |
Список экспортов GET /books/{id}/exports |
Б-4 просил СОСТОЯНИЕ экспорта, не список. Восстановление после перезагрузки закрыто иначе — Idempotency-Key на создании возвращает исходный 202 с тем же Location |
research/28 §2 (Б-4) |
| Новое число страницы замечаний | замера нет ни одного: сколько замечаний даёт настоящая книга, не знает никто. 500 выбрано без числа, и выдумывать второе число вместо первого — та же ошибка. Число ушло из спеки в Capabilities.page_size_default; калибровка — после первого настоящего прогона |
research/28 §7, §10 |
6а. Резать отказались (§5а) — контраргумент на каждое
Перечень СНЯТОГО батчем 0.3.0 здесь не дублируется: он в диффе лендинга 8d82096 (git log -S) и в
research/28 §5а, а живые следствия каждого снятия стоят в своих §2.
- параметр
limit(«клиент не отправил его ни разу»). Тот же §5 требует объявить у негоmaximumи подрезание вместо понижения (Б-10) — у удалённого параметра максимума не объявишь. Плюс: «референсный клиент не шлёт» — свойство ОДНОГО клиента, а контракт пишется для второго. Note.unit_id(«только в фикстуре мока; экраны берутunit.noteвложенно»). Основание — «ни один экран не читает», ровно тот довод, который эррата 16.08-г уже опрокинула наTermStatus: экранов замечаний (S6) ещё нет. Адресация на пару — единственный способ перейти к МЕСТУ замечания из плоского списка, а собственная фраза операции говорит «A note addresses a unit or a whole chapter». Поле оставлено НЕОБЯЗАТЕЛЬНЫМ (Б-9 просил ровно это), а обязательным сделанchapter_id— то есть дефект «замечание, не адресующее ничего» закрыт.Bank.signed(«доезжает до клиента и не рисуется»). Тот же довод «нет экрана» — экран подписи это S5.total,signedиpending_decisions— три НЕЗАВИСИМЫХ факта: строку можно решить и не подписать (отклонить), поэтомуsignedне выводится из двух других. (0.5.0:pending_decisionsснесён — §2.19-бис; довод о независимостиsignedстоит и без него.)- нагрузка кадров
noteиbank(«передаётся и игнорируется»). Игнорировалась она по причине, которую батч устранил: у замечания не было id, поэтому кадр нельзя было сопоставить со списком. СNote.idкадрnoteнесёт ПРИМЕНИМУЮ ДЕЛЬТУ — это ровно первая ветка правила Б-11а, и снятие нагрузки вернуло бы перечитывание всего списка. Счётчикиbank— вторая ветка того же правила («счётчик + скоуп»), а строки читаются дельтой?after_version=.
6б. Дофикс-раунд 16.08 (ФБ-1…ФБ-10): что он изменил в каноне и почему
Все десять правок живут нормами в openapi.yaml, поимённый перечень — D39.143 п.1 (заказ раунда —
D39.142 п.3). Здесь остаётся только то, чего нет ни там, ни в нотах:
- ФБ-9, шов имён.
parser_unavailableпереименован на проводе вprocessing_failed. ⚠ Живое следствие для шва: проводное имя отличается от внутреннего словаря платформы, где значение по-прежнему зовётсяparser_unavailable(platform/internal/books/parse.go:66), и проекция обязана отобразить одно на другое; рядом там же живутschema_mismatchиstorage_unavailable, которые на провод не идут вовсе. - ФБ-8. ⚠ Тождество
Idempotency-Keyна multipart считалось «по объявленным частям» — ПЕРЕКРЫТО 0.4.0, читать по §2.18.
6в. Синк с платформой 20.08 → канон 0.4.0: что решено и почему
Вход — platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md, четырнадцать мест, где провод сообщает
состояние и не даёт клиенту действия. Метод разбора и его итог по всем четырнадцати — D39.152;
вердикт каждого пункта — в таблице ниже.
⚠ База ссылок этого раздела — РАБОЧЕЕ ДЕРЕВО 20.08, а не HEAD. В platform/ на этот момент 81
незакоммиченный файл (P7 в работе), поэтому одна и та же строка в HEAD и в дереве разная, и ссылка
без базы проверяема только случайно. backend/ чист, его ссылки годятся в обоих. Там, где
конструкция переживёт любую перенумерацию, адрес дан ИМЕНЕМ функции или ветки — это дешевле и
надёжнее номера. Поймано ревью диффа: первая редакция §6в смешивала обе базы в одной строке таблицы.
п.0. Сверка на потери прозы 0.2.3 → 0.3.0 — исполнена; найдена ОДНА потеря, она и была заявлена
Метод (полный дифф лендинга 8d82096 по обоим файлам, чтение ВСЕХ удалённых строк) и разбор трёх
классов удалённого — D39.152; сами строки держит тот же дифф (git log -S).
Итог: единственная потерянная НОРМА — фраза §2.14 («различать причины отказа клиент не может по
замыслу»), восстановлена. Второе — правило пустого kind уцелело в каноне, но потеряло здесь
ПРОЕКЦИЮ ("" → null — работа платформы); клауза возвращена в §2.8. Остальное удалённое правил не
несло.
Гейт на следующую резку прозы, и он дешевле того, что был. Приёмка батча сверяла мультимножество
модальных глаголов и этот класс структурно не ловила. Сверять надо не модальность, а список
собственных имён формы (полей, значений, кодов, заголовков): норма почти всегда стоит рядом с
именем, а фраза §2.14 пропала именно там, где имени не было (/auth/* — не поле). ⚠ Греп по
исходному файлу даёт ЛОЖНЫЕ пропажи — правило переносится через перенос строки; сверять на копии,
склеенной в одну строку (tr '\n' ' '). Носитель: эта запись.
Диспозиции пунктов, исполненных без остатка: тела сняты, содержимое живёт по адресу. Пункты A · B · G · H · J · L разобраны ниже полностью — их предмет либо открыт, либо несёт провенанс, которого больше нигде нет.
| Пункт записки | Вердикт | Где живёт сегодня |
|---|---|---|
C. Вход /auth/* |
ИСПОЛНЕНО: срезанная фраза восстановлена, уточнённый вариант A записан, триггер пересмотра зафиксирован; проводной словарь в компаньоне НЕ объявлен — возражение зоны про «нормативность с чёрного хода» принято дословно и стало основанием | §2.14 целиком |
D. Note.code против пустой клетки |
ПРАВЛЮ ОБА ФАЙЛА: unspecified ратифицирован как ОБЯЗАННОСТЬ сервера, а не строка словаря причин. Родственный вопрос ступеней НЕ решается — К-6 открыт, слово владельца «решим потом», провизорность построенного зафиксирована |
приложение А, п.5 и п.6; канон |
| E. Отпечаток интейка (PD-262) | ПРАВЛЮ КАНОН, форму НЕ трогаю: «тот же запрос» = метаданные + имя файла + СОДЕРЖИМОЕ; чем сервер устанавливает последнее — его дело; сервер, не установивший тождества, не реплеит, а отвечает 409. Поля размера в форме нет: оно не закрыло бы «две разные книги одной длины» |
§2.18 (правка 0.4.0) |
F. ETag/304 на любом безопасном чтении |
ПРИНЯТО одной фразой: валидатор на любом GET легален и объявления не требует; объявленные операции — те, где клиенту есть смысл им пользоваться, а не исчерпывающий список. Границы: клиент НИКОГДА не обязан слать If-None-Match, 304 бывает только на присланный. getUsage/getRunOptions НЕ добавлены сознательно — оба читаются ровно перед действием |
шапка канона; §3, строка условных чтений |
I. content_refused |
forward-looking, долг зона не заводит: значение ратифицировано К-9 ДО производителя сознательно. Обязанность ограничивать число попыток аккаунта пере-привязана — падает ВМЕСТЕ с производителем, а не висит на сервере, который отказа не выдаёт | §3, строка content_refused (строка 94, ПТ-16); приложение А-2 |
K. --verify-bank |
НЕ контракт, релей в зоны. Контрактно видимая половина закрыта D39.144/145. ⚠ Вторая половина умерла вместе с носителем: схема BankDecision снесена 0.5.0, дверь-преемник пишет файлы движка сама, так что «решение не доезжает» перестало быть свойством формы |
§2.19-бис; §3, строка decline; D39.144 |
| M. Что зона сделала сама | сверено, возражений нет (восемь пунктов, включая снятие codeForStatus) |
D39.152 п.2 |
| N. Границы сессии; что забирает P7 | шесть релеев зоне (A · D · E · G · J · H) приняты и построены паком P7. ⚠ Граница автора 0.4.0 (его собственное объявление, восстановлено): НЕ проверялось, как экраны фронта рисуют новые ответы (зона заморожена) и как ведёт себя построенный путь resume после правки таблицы — это работа P7 |
D39.153; статусы — таблица §3 |
A. Прогон с исчерпанным бюджетом (PD-282 ⚠ ЗАКРЫТО (PD-282 fixed, акт 5; разбор — §3)) — ПРАВЛЮ КАНОН; половину мнения отклоняю
Факт подтверждён и оказался шире записки. reopen даёт exhausted в ДВУХ местах, а не одном
(runs/reconcile.go): remaining <= 0 и ErrInsufficientCredit от RestartRun — то есть
«кончился потолок прогона» и «нечем взять холд на счёте» уже сегодня схлопнуты в один вердикт, и
Resume отвечает на оба одинаково: 202 + неизменённый Run (httpapi/v0.go, хендлер
resumeRun). paused до этой ветки не доходит — он отказан выше, поэтому молчаливый 202 живёт
ровно на stopped и awaiting_bank. Худший из двух — awaiting_bank: человек подписал банк,
нажал «продолжить», получил «принято», а ReleaseBankStop не вызывался (зона это записала сама,
archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md §7(и)).
Правка канона (§resumeRun). Записана ВТОРАЯ ОСЬ: исчерпанность лимита прогона перебивает обе
строки 202; ответ — 409 run_not_resumable, cause.code: ceiling_reached, лечение — новый
прогон. Новых значений enum не заводится: ceiling_reached уже есть и его собственный текст этот
случай описывает дословно. Тем же ходом канон получил обратную гарантию, которой не имел:
202 теперь означает, что работа действительно переоткрыта, и клиенту не нужно второе чтение,
чтобы отличить успех от no-op. И записано разделение, о котором предупреждает сам канон у
AccountHaltReason: пустота СЧЁТА не сворачивается в ceiling_reached — у неё своя причина
credit_unavailable (аддендум ниже), а состояние аккаунта ЦЕЛИКОМ по-прежнему говорит Usage, не
этот вызов.
Признак «этот прогон ещё можно продолжить» на Run — ОТКЛОНЁН, с оговоркой. Посылка «платформа
знает его точно» верна, посылка «дёшево» — нет: ReadRun — один запрос по books join runs
(pgstore, функция ReadRun), а флагу нужны ТРИ чтения, которых в нём нет — RunSpent,
AttemptReservationOpen и баланс счёта. Run сериализуется в карточке книги, а карточку канон
велит перечитывать НА КАЖДЫЙ кадр потока («it is still read on a frame, on navigation and on
focus»), то есть цена платится на горячем пути ради факта, который
(а) устаревает к моменту клика и (б) уже отвечен ратифицированным порядком чтения пяти мест
(§RunOptions): max_chapters == 0 + blocked — это ровно «стоит ли предлагать старт сейчас и что
мешает», и рисует его тот же экран. Авторитет по-прежнему у мутации, как канон и объявляет.
⚠ Поправка ревью к предыдущему абзацу: сам Run в КАДРЕ не едет — ни один payload его не несёт;
горячий путь создаёт не кадр, а предписанная им перечитка карточки. Вывод не меняется, носитель цены — другой.
Триггер пересмотра: замер, показывающий, что мёртвый клик частый, ЛИБО сворачивание трёх чтений
в коррелированные подзапросы того же запроса (агент проверил — возможно). Тогда форма — Run,
поле, минор.
B. Суточный потолок движка — ОТКЛОНЯЮ ОБЕ ФОРМЫ; посылка не выдержала проверки
Записка просит слово или признак для паузы, которую деньги не лечат. Проверка посылки:
- Владелец уже решил этот вопрос — 15.08, D39.132 п.2а.
day_usdУБРАН из платформенного шаблона книги; трата прогона ограничена купленным объёмом (холд +--ceiling-usd), дневная ось на платформе объявлена дублирующей, в движке остаётся ОПЕРАТОРСКОЙ опцией, а обработкаdaily_ceiling/409 — предохранитель. Там же дословно: «PD-199 закрыт этим же решением:nullна проводе подтверждён, слово в контракт не заводится». Заводить его сейчас значило бы опрокинуть ратификацию пятидневной давности ради случая, который зона сама пометила ГРАНИЦЕЙ («живьём не воспроизводилось»). Проверено: платформа это поле не заводит — её рендерbook.yamlпроставляет ровно пять строковых ключей, «the whole of what this platform claims about the engine's schema» (books/render.go, функцияrender), и потолки в их число не входят. ⚠ Уточнение ревью, и оно важнее исходной формулировки: «не заводит» ≠ «не пишет». Шаблон деплоя пере-маршалится целиком, поэтому оператор, положившийceilings.day_usdв СВОЙ шаблон, получит это поле в каждой книге, которую платформа создаст (запиненоrender_test.go— «the operator's ceilings did not survive»). Именно поэтому триггер ниже сформулирован через шаблон, а не через код платформы: настройка живёт в артефакте деплоя, и вернуться она может без единой правки Go. - Предложенная форма неверна по СМЫСЛУ, даже если бы случай был. «Лечится покупкой / не
лечится» — булево, а суточный потолок это ОКНО: он снимается сам в полночь UTC
(
store/ledger.go:60-72,todayUTC()). Клиент, прочитавший «не лечится», спрячет кнопку навсегда там, где честный ответ — «не сейчас». Булево, у которого один из двух исходов врёт, хуже, чем отсутствие поля. - Второе значение enum — та же проблема плюс своя: оно называет настройку чужого файла на проводе, что запрещает правило §5 («на проводе нет движковых словарей»), и зона сама этого не хочет.
Что записано вместо этого — одна фраза в каноне (§resumeRun). «Новый прогон легален при любом
paused» осталось нормой, но перестало читаться как обещание прогресса: деплой может держать
СВОИ лимиты, которых этот контракт словом не называет, и прогон под таким лимитом останавливается
так же; клиент всё равно предлагает новый прогон — лучшего действия у него нет, — но не подаёт его
как гарантию, а повторный paused это законный исход, а не сбой. Цена ошибки при этом наблюдаемая
и самокорректирующаяся: холд возвращается, провайдерских денег не теряется (ФАКТ записки).
Триггер пересмотра, записанный явно: день, когда day_usd вернётся в платформенный шаблон
книги. Тогда это настоящая дыра и контракт открывается заново.
⚠ Попутно найдено и стоит знать зоне: суточный потолок — НЕ единственный стоп, который деньги
не лечат. Их четыре класса (cmd/tmctl/main.go:52-79): подпись банка (exit 3), graceful stop
(exit 5), полоса отказов 10–19 и суточный потолок (exit 4, scope day). Просто три из четырёх уже
имеют свою проекцию (awaiting_bank, stopped, failed + RunFailureReason), и «денег не
хватает» из них не следует ни для одного. То есть класс закрыт, а дыра — только в четвёртом.
G. Draft-only конвейер (PD-202) — ОТВЕЧАЮ: ДА, начерновленная глава «сделана»; правлю канон
Проверено чтением обеих зон, и цена выше заявленной. У движка деплой без второго прохода — не
экзотика: разбиение выводится из РОЛЕЙ стадий (pipeline/waverun.go:43-53), конфиг требует лишь
«хотя бы одна стадия» (config/pipeline.go:841), ветка живая и покрыта тестом
(contractblockers_test.go:115-136); в репозитории такого конфига пока нет, но выбирает его
оператор, и платформа шаблон не читает. У платформы units_done — жёстко второй проход
(pgstore/sink.go:252-263, readmodel.go:200-208).
Следствие, которого в записке нет: ноль навсегда — это не только короткая полоса.
Book.chapters_done и ChaptersLeft (pgstore, функции chaptersDone и BookRunContext) читают
ту же колонку, а ChaptersLeft подрезает ШКАЛУ покупки. То есть на таком деплое сервис бесконечно
предлагает купить главы, которые уже переведены. Это выводит вопрос за рамки «косметика полосы» и
делает ответ обязательным.
⚠ Первая редакция этого абзаца дописывала «и берёт за них деньги» — снято ревью как неверенное, и снято ИЗ КАНОНА тоже (там оно уже стало нормативным текстом, компилирующимся в JSDoc клиента). Что проверено: платформа держит ПОТОЛОК, а не цену, и рассчитывается по ЗАМЕРЕННОЙ трате, а неизменённая уже начерновленная книга переигрывается с чекпойнтов за $0 — то есть в обычном случае холд возвращается целиком и не берётся ничего. Плата возникает только если между прогонами сдвинулся снапшот (слаг модели, промпт, langpack, флип гейта), и тогда пере-покупка действительно оплачивает переведённое заново. Довод «предлагают уже сделанное» стоит сам по себе и в поправке не нуждается.
Ответ контракта. «Сделано» = последний проход, который эта книга на ЭТОМ деплое реально
получает. Записано в Progress и в Book.chapters_done — вместе с доводом: контракт не говорит,
сколько проходов бывает, значит и назвать один из них для счёта не может, а обещание «дробь
доходит до единицы» — это обещание, а не проходы. Имён волн в тексте нет (гейт §5 пере-ран, чисто).
Форма не меняется, генерённые типы не двигаются; работа — проекция платформы, вход P7 ⚠ (носитель израсходован, D39.153).
H. Снятие замечания (PD-298) — СВЕРЕНО С ДВИЖКОМ ПЕРВЫМ, как просила зона; механизма НЕ строим
Порядок соблюдён: сначала чтение движка. Результат — переход достижим у драйвера и НЕ достижим на
проводе. flagged вычисляется на каждой эмиссии, а не хранится (pipeline/events.go:351,377-379,
waverun.go:139-140,205-206), и redrive/--resnapshot/правка исходника пере-атакуют юнит, так
что второе разрешение законно может выйти flagged=false. Но эмиссия идёт через
announce-once-леджер: ключ unit:<book>:<wave>:<chapter>:<unit> не несёт ни снапшота, ни прогона
(backend/internal/pipeline/events.go, греп unit:%s:%s:%d:%d), EnqueueOnce вторую строку не
пишет, а леджер переживает прогоны — «announced ONCE for the life of the book, whatever crashes and
resumes happen in between» (backend/internal/store/outbox.go, символы EnqueueOnce и
onceKeyLookup); запинено TestAResumedRunOpensItsOwnStreamAndDoesNotRecountFinishedUnits.
Единственное окно — падение между записью строки в журнал и пометкой
(events.go:154-164,200-202); плюс redrive платформа не вызывает вовсе: сборщики argv знают
translate/status/manifest/export/bank-apply, глагола redrive среди них нет
(platform/internal/runner, греп Args(workdir).
⚠ Адреса пере-нацелены 02.09 с номеров на символы: три из шести съехали за паки, вывод не изменился.
Поэтому: строка «недостижимо» в регистр, а не механизм — ровно тот исход, который зона назвала
правильным. Но в каноне закрыт РАЗРЫВ ПРАВИЛА, который эта проверка обнажила: §AfterVersion говорил
«удаление так не выразить» только про ОПТОВУЮ замену, а про исчезновение ОДНОЙ строки не говорил
ничего. Записано: строка, once delivered, поодиночке не отзывается; коллекция, которая обязана
потерять строку, теряет её единственным выразимым способом — заменой целиком с resync_required.
Это обязанность сервера, а не пожелание, и она делает поведение платформы выводимым: сегодня при
flagged=false строка обновляется и ревизия двигается (pgstore/sink.go:237-244), дельта её
прячет (readmodel.go:725-735), а note_count в кадре главы уже УМЕНЬШАЕТСЯ (sink.go:310-328) —
то есть клиент увидел бы счётчик, противоречащий списку. Теперь ясно, что должно произойти вместо
этого.
J. «Остановлено по вашей просьбе» — ПОЛЕ ЗАВЕДЕНО; форма отличается от предложенной зоной
Решение владельца 17.08 принято, форму делегировали контрактной сессии
(archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md:462). Заведено Run.stop_requested — обязательное булево, а не
необязательное. Довод: функция тотальна — прогон, который никто не просил остановить, это false,
а не «поля нет»; необязательность дала бы два написания одного факта, и это ровно тот дефект, который
контракт уже чинил у sense (0.2.2), у reject_reason и у kind. Ценой стал ломающий минор —
Run получил обязательное поле, — и он объявлен честно в шапке компаньона, вместо того чтобы
называть правку «аддитивной» и оставить генератор доказывать обратное. В описании записано и то,
чего в заказе не было: флаг — запись ПРОСЬБЫ, а не исхода, и остаётся true на прогоне, который
успел доработать, потому что это и есть случай, который экран обязан объяснить.
L. Мелкое
410на опечатку в id главы (PD-253) — канон изменён в сторону зоны, а не наоборот. Требование различать «была и исчезла» от «такой не было» невыполнимо: id непрозрачны, после пере-разбора не хранятся, и доказать, что id никогда не минтился, можно только кладбищем всех выданных. Записано:410— ответ на id, которого нет в ТЕКУЩЕЙ структуре, был он когда-то или нет;404остаётся за книгой. Лечение у клиента в обоих случаях одно — перечитать дерево.- Два других пункта исполнены и живут на своих местах: указатель приложения А переведён с номера
строки на имя функции (
flagReasonSeverity— номер в чужой зоне протухает за пак), а посылка «у читателя НЕТ меток глав вообще» опровергнута — служебную «Главу N» рисует КЛИЕНТ в локали интерфейса (§2.3); продуктовый пробел «настоящих названий нет» держится строкой 160 в §3.
Ревью диффа и второе ревью 20.08 — что от них осталось живого
Веер линз, счёт находок и их адверсариальная верификация — D39.152. Ни одна не опрокинула диспозицию; все подтверждённые внесены НА МЕСТЕ и живут сегодняшними оговорками в пунктах A, B, G и в строке PD-298 таблицы §3. Живого здесь — два провенанса ниже.
⚠ Провенанс cause.code: credit_unavailable. Три линзы независимо споткнулись об один шов —
что отвечает resume, когда у прогона лимит ещё есть, а СЧЁТ не тянет холд. Каждую формулировку
верификатор опроверг по отдельности («тексты можно прочесть согласованно»), но холодный потребитель
прямо написал, что решить не может, а платформа этот случай уже схлопывала в тот же вердикт:
сходимость трёх независимых линз на одном месте принята сигналом сильнее трёх поштучных
опровержений. Заведено второе значение уровня cause (он не закрыт по замыслу — типы не двигает),
ceiling_reached пере-сформулирован как «ЭТОТ прогон закончен», подменять друг друга им запрещено.
Второе ревью (заказ владельца 20.08): «а так вообще делают, или ты изобрёл своё» + «не раздул ли
прозу». 20 находок, подтверждена 1; велосипедов не подтверждено ни одного — проверялись против
настоящих спек: тождество интейка против draft-ietf-httpapi-idempotency-key-header и RFC 9530
(Digest Fields) · credit_unavailable под 409 (402 зарезервирован; двухуровневый код
конвенционален — Microsoft innererror, google.rpc.ErrorInfo) · stop_requested булевым против
конвенции таймстемпа (k8s deletionTimestamp, AIP-165) · 410 на никогда не существовавший id
против RFC 9110 §15.5.11 · «валидатор легален на любом безопасном чтении» против OAS 3.1 ·
пер-строчные tombstone по образцу JMAP (RFC 8620) — не наш случай.
Замер объёма прозы (пин зоны openapi-typescript@7.13.0, воспроизведён верификатором до байта):
канон дал +10 546 байт и +144 строк комментариев в исходники фронта. Гипотеза «62 % этого —
обоснование, которому место в компаньоне» пере-проверку НЕ прошла: из тринадцати заявленных блоков
выжил один. Своим решением, а не по находке, срезан ещё денежный хвост у Book.chapters_done — он ничего не меняет в поведении клиента. Итог трима: 152 758 → 152 002 байта. Урок для следующей правки канона: норма — в
канон, довод — сюда; проверять не глазом, а диффом генерённого клиента.
7. Эксплуатационные примечания — НЕ норма контракта
Вынесено из спеки в 0.3.0 (Б-16) по RFC 9205 §4.1: требовать минимальную версию HTTP — NOT RECOMMENDED, а выигрыш деплоя от конкретной версии «ought be noted». Отметить, а не потребовать.
Норма в спеке — то, что клиент НАБЛЮДАЕТ и на что вправе рассчитывать: ETag/If-None-Match/304
· обязанность честить Accept-Encoding на JSON-ответах и НЕ сжимать text/event-stream · Vary
на согласованном представлении · Cache-Control: no-store · запрет буферизации потока · форма
heartbeat · форма id кадра. Сжатие как ТРЕБОВАНИЕ живёт в контракте — это прямое указание D39.138
п.2(д) («сжатие и условные чтения записываются В КОНТРАКТ, не в зонный док»).
В примечании остаётся ТОЛЬКО слой исполнения — где именно сжимать, — потому что вот этого клиент
действительно не наблюдает, и вот это Б-16 из контракта и выносит.
Ниже — то, что клиент не наблюдает и что деплой обязан себе устроить сам:
- Сжатие — в приложении, а не на edge (испр. 02.09; прежний текст требовал устроить его деплою):
платформа жмёт САМА, в Go —
platform/internal/httpapi/conditional.go, грепgzipFloor(порог 1<<10 байт) иacceptsGzip. Деплою остаётся одно — НЕ сжимать ВТОРОЙ раз на прокси/CDN и не сжиматьtext/event-stream. - HTTP/2 на edge. Клиент держит одно SSE-соединение на книгу; на HTTP/1.1 шесть соединений на origin — потолок вкладок. Сервис на HTTP/1.1 обязан работать ХУЖЕ, а не не работать.
X-Accel-Buffering: no(или эквивалент прокси) — механизм для нормы «поток не буферизуется».- Сколько это даёт. Замер ревью на настоящей книге (
~/books/gu-zhenren, 2283 главы): глава 26,1 КБ → 10,4 КБ; дерево глав 250 КБ → 39 КБ; банк 1000 строк 167 КБ → 17 КБ. - Порядок работ по эффекту на килобайт усилия (research/28 §5б, строка 186): сжатие →
ETag/304 → скоуп в кадре → дельта-чтения?after_version=→staleTimeу клиента. Шаги 1–2 ПОСТРОЕНЫ паком P7 (D39.153), шаги 3–4 — в контракте с 0.3.0, шаг 5 — зона фронта. - Транспорт НЕ меняется (проверено независимым агентом другого тира + сверка первоисточников):
наша нагрузка — художественный текст, и после gzip разница между JSON и protobuf единицы процентов;
gRPC-web требует прокси и теряет
If-None-Match/304; Connect возвращает то, что у нас уже есть. Наш паттерн «кадр без данных → клиент перечитывает» легитимен и называется poke/pull.
Приложение А. Карта «причина → КОД контракта → продуктовая фраза» — ЗАГОТОВКА
Форма карты (0.3.0). Note несёт code, а фразу рисует КЛИЕНТ; Note.message с провода
снят. Серверная локализованная фраза — ровно та политика, которую вариант B отменил для ошибок
(§2.17), и держать её для замечаний значило бы слать второй русский текст из зоны без локалей.
Правила заполнения.
- Фраза пишется по ДОККОММЕНТУ
disposition.go, а не по имени константы, и рядом кладётся цитата — иначе повторяется инверсия, стоившая двух фраз (glossary_missподан как «термин не подписан», хотя термин ПОДПИСАН и его проигнорировали,disposition.go:78-79;sanitizer_strippedподан как потеря текста, хотя «the chunk is NOT lost»,disposition.go:99). - Класс 2 схлопывается в ОДИН код (§8а, нормативно): четыре причины ранга 0 — модельный отказ — на проводе неразличимы, потому что каждый различимый код здесь бит обратной связи подбирающему.
- Слова — ЗА ПРОЕКТОМ (D39.176 п.4, 30.08): владелец делегировал фразы В-3 · К-6 · Приложение А, подписи его больше НЕ ждём. ⚠ Требование владельца — ИНТЕРНАЦИОНАЛЬНОСТЬ: фраза не литерал ни в Go платформы, ни в TSX фронта, а ДАННЫЕ по машинному коду причины и локали; словарь — один на продукт. Карта «причина движка → КОД контракта» НЕ загейчена и поручена контрактной сессии (строка бэклога 203); гейт доставки словаря данными — строка 204, вместе со словарём закрывается
PD-246. ⚠ Прежний указатель «строка 148» мёртв: строку удалилa023e39решением владельца. Коды ниже — ◆ ПРЕДЛОЖЕНИЕ, ратифицируются вместе с фразами; до тех порNote.codeв спеке НЕ enum, чтобы схема не стала второй копией незаписанной карты. - Последняя строка — не формальность: контракт обязан иметь фразу для причины, которой ещё не существует, и она обязана читаться нейтрально, а не как «ошибка».
- Клетка кода последней строки БОЛЬШЕ НЕ ПУСТА —
unspecified(синк 20.08, п. D записки). Пустая клетка была невыполнима:Note.codeв каноне обязателен иminLength: 1, движок и платформа выпускаются независимо, поэтому окно «пришла причина, которой этот билд не знает» — штатное. Платформа уже отдавала стабильный плейсхолдерunspecified(ingest/notes.go), не имея на него ратификации; синк её ратифицировал и записал в канон обязанностью сервера, а не строкой словаря. Бампа схемы не требует —Note.codeобъявленtype: string. - Столбец «Ступень» пуст, а платформа уже провела границу.
ingest/notes.goраскладывает девять движковых рангов на два проводных значения по правилу «потерял ли читатель текст». Это ПРОВИЗОРНО и ратификацией не является: К-6 открыт, ответ владельца 16.08 — «решим потом». Строка записана здесь, чтобы построенное не читалось как решённое.
| Причина движка | Ранг | Код контракта ◆ | Продуктовая фраза | Ступень |
|---|---|---|---|---|
hard_refusal · soft_refusal · content_filter · hard_block |
0 | content_withheld (ОДИН на все четыре — класс 2) |
⬜ | ⬜ |
cjk_artifact |
1 | source_residue |
⬜ | ⬜ |
off_target_lang |
1 ⚠ | wrong_language |
⬜ | ⬜ |
excision_suspect |
1 | text_possibly_dropped |
⬜ | ⬜ |
coverage_fail |
1 | incomplete_coverage |
⬜ | ⬜ |
sanitizer_defect |
2 | markup_defect |
⬜ | ⬜ |
loop_degenerate |
3 | repetition |
⬜ | ⬜ |
decode_error |
4 | unreadable_answer |
⬜ | ⬜ |
glossary_miss |
5 | term_not_applied |
плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ |
length |
6 | length_mismatch |
⬜ | ⬜ |
empty |
6 | empty_answer |
⬜ | ⬜ |
sanitizer_stripped |
7 | markup_cleaned |
плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
upstream_not_ok |
8 (по умолчанию) | unavailable |
⬜ | ⬜ |
| незнакомая причина | 8 (по умолчанию) | unspecified (обязанность сервера, п.5; клиент рисует ту же нейтральную фразу, что и на незнакомый код) |
⬜ нейтральная, НЕ «ошибка» | ⬜ |
⚠ Ряд off_target_lang (минор 0.10.0) — шестнадцатая причина, и его РАНГ приезжает не с этой правкой.
Причина заведена движковым вайр-батчем (backend/internal/pipeline/disposition.go, греп
FlagOffTargetLang) и на сегодняшнем HEAD своей ветки в flagReasonSeverity НЕ имеет, то есть падает
в ранг по умолчанию 8 — «недоступно», что для внятного ответа на чужом языке ложно. Ранг 1 (рядом с
эхом, source_residue) — то, чем это чинится, и он приезжает лендингом движкового пака; до него
клетка читается как обещание, а не как факт. Код wrong_language ратифицирован по ДОККОММЕНТУ, а не
по имени константы (правило 1): движок формулирует «the answer is not in the language that was asked
for», и это ровно то, что видит читатель, — не «артефакт исходного письма» и не «недоступно».
⚠ Имя не переиспользует source_residue намеренно: доккоммент прямо разбирает, почему повторное
использование эхо-флага было бы ЛОЖЬЮ о причине (D39.93 п.2), — беглый английский черновик
демонстративно не является артефактом исходного письма.
Причин шестнадцать; upstream_not_ok не имеет своей ветки в flagReasonSeverity
(backend/internal/pipeline/status.go, функция flagReasonSeverity) и падает в ранг по умолчанию,
как и любая будущая причина. ⚠ Номер строки здесь намеренно не ставится: прежняя ссылка :174
протухла за один пак (там теперь GlossaryMissFlagged) — поймано синком 20.08, п. L записки.
Приложение А-2. Карта кодов ОШИБОК — заполнена (0.3.0)
В отличие от карты выше, эта заполнена целиком: словарь выведен из реальных ветвей платформы, а фраз
она не содержит по замыслу — их рисует клиент. ⚠ Колонка «Откуда взят» переведена 02.09 с НОМЕРОВ СТРОК на СИМВОЛЫ: вся она съехала на 240–270 строк (производители уехали, на прежних номерах стояли комментарии и скобки), и это её второе протухание. Адресация символом (pgstore.ErrNoBook, readField, имя файла) переживает любой сдвиг; номеров сюда больше не ставить.
Корневой code |
HTTP | Откуда взят (платформа) |
|---|---|---|
invalid_request |
400 | v0.go · pgstore.ErrBadCursor · books.ErrBadIntake (books.go, readField v0.go) · v0.go (неполное тело) · v0.go (интейк) |
unauthenticated |
401 | гард сессии (server.go) |
forbidden |
403 | server.go + auth/csrf.go (отсутствие X-TM-Client / чужой origin) |
not_found |
404 | pgstore.ErrNoBook/ErrNoAccount/ErrNoRun · охраняемый catch-all server.go |
gone |
410 | pgstore.ErrNoChapter → v0.go — глава, которой нет в ТЕКУЩЕЙ структуре книги (была она когда-то или нет — не различается по замыслу, openapi.yaml §gone; лечение клиента одно: перечитать дерево). ⚠ Строка дописана оркестратором №18 при ратификации 0.4.0: карта объявляла себя «заполненной», а несла 15 значений из 16 корневых — производитель у gone живой с P7 |
request_timeout |
408 | os.ErrDeadlineExceeded → v0.go |
payload_too_large |
413 | *http.MaxBytesError → v0.go |
run_in_flight |
409 | pgstore.ErrRunInFlight (v0.go) |
book_not_ready |
409 | runs.ErrBookNotReady (v0.go) |
run_not_stoppable |
409 | runs.ErrNotStoppable (v0.go) |
run_not_resumable |
409 | runs.ErrNotResumable; cause: ceiling_reached — с 0.4.0 это же ответ на ИСЧЕРПАННЫЙ прогон в stopped/awaiting_bank, где платформа сегодня молча отвечает 202 ⚠ (ЗАКРЫТО 02.09: Resume больше не отвечает 202 — вердикты ceilingSpent/creditUnavailable едут 409 run_not_resumable, см. §3) (A, PD-282: runs/reconcile.go, ветка exhausted функции reopen и её чтение в Resume). ⚠ bank_decisions_incomplete удалён D39.144 — гейта полноты нет; построенный гейт полноты ДЕМОНТИРОВАН (0.5.0, сверено с кодом: reconcile.go, ветка case "awaiting_bank" снимает стоп при ЛЮБОМ состоянии решений, комментарий «this is the built half being dismantled with it») |
ceiling_unavailable |
409 | runs.ErrCeilingOutOfBounds + pgstore.ErrInsufficientCredit (v0.go); cause: bounds_moved · credit_held; несёт blocked |
idempotency_conflict |
409 | форма заведена батчем; реализация — P7 |
bank_corrections_refused |
409 | объявлен 0.5.0 ДО производителя; производитель — монтаж двери правок (пак (2в) очереди D39.156): раскладка отказа класса 14 движка (tmctl bank-apply, документ прочитан и отклонён целиком — пользователь пере-решает) |
bank_corrections_incomplete |
503 | объявлен 0.5.0 ДО производителя; тот же монтаж: раскладка класса 15 (документ принят, запись не довершена — слать ТОТ ЖЕ документ, ретрай сходится) |
content_refused |
400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием rejected + reject_reason |
service_unavailable |
503 | runner.ErrCeilingNotWired + runs.ErrRunnerIncomplete (v0.go) |
internal_error |
500 | v0.go · middleware.go |
⚠ Коды, которых платформа сегодня достигает, а контракт до 0.3.0 не объявлял: 403
(platform/internal/httpapi/server.go, греп CauseHandler(CodeForbidden), 500
(platform/internal/httpapi, греп Fail(w, r, CodeInternalError) — v0.go, middleware.go,
conditional.go, idempotency.go, bank.go), 431 от net/http (serve.go:79, MaxHeaderBytes),
429 на /auth (login.go:173,234). Из них
контракт объявляет 403 (это нарушение правила, которое ИЗОБРЁЛ сам документ) и не объявляет 500
(Zalando: «500 Internal Server Error · use · do not document»), 431 (уровень stdlib) и 429 (на
/v0 лимитера нет вовсе — форвард-вопрос платформы). 405 на /v0 не бывает: catch-all глотает
метод.