| .. | ||
| openapi.yaml | ||
| README.md | ||
Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
Нормативная поверхность контракта —
openapi.yaml(файл рядом, в этой же папке). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято каждое решение, чем оно обосновано, что осталось открытым, и ГЕНЕЗИС форм (историю ратификаций, сверку со стандартами, разобранные альтернативы). При расхождении по ФОРМЕ побеждает YAML; при вопросе «почему так» — этот файл.Статус: РАТИФИЦИРОВАН по 0.9.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. Дом канона — этот каталог;frontend/docs/api-contract/openapi.yaml— байт-зеркало ⚠ НА ФРИЗЕ ЗОНЫ РАВЕНСТВО ПРИОСТАНОВЛЕНО (D39.142 п.5): зеркало 0.2.3, канон 0.9.0.⚠ 0.5.0 ломающий по построению (мажор
0): снесены путьPOST …/bank/decisionsи три его схемы, изBankPageиEventBankснятыpending_decisions/complete(§2.19-бис), вCapabilities.requiredдобавленbank_corrections_enabled.⚠ Оговорка «дверь объявлена И НЕ ОБСЛУЖИВАЕТСЯ» СНЯТА лендингом P9 (D39.162). Платформа смонтировала
POST …/bank/corrections; флагbank_corrections_enabledтеперь следует включённости прогонов (platform/cmd/tmplatformd/runner.go:195—cfg.RunsEnabled()), потому что дверь спавнит тот же глагол движка и без прогонов ей нечем работать. Деплой без прогонов по-прежнему честно отвечает404, и это тот же флаг, а не новый.⚠ Расхождение «точное по ПУТЯМ, не по полям» — ЗАКРЫТО лендингом P9 (D39.162). Было: путь платформа снесла 22.08, а проекция
GET /bankещё клала на провод снятые 0.5.0 счётчики (wireBankPageвplatform/internal/httpapi/reading.go—PendingDecisions/Complete, носительPD-399). Полей в проекции больше нет, аллоулист-норма восстановлена.⚠ Урок оставлен НАМЕРЕННО, он переживает свой дефект: гейт версии этого класса не ловит.
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): пофазно и в юнитах, ✓ выведено. Основание было верное по факту:
«A unit is DONE when every member draft AND the unit's edit resolved ok» (pipeline/status.go:328-331),
редактура не стартует до стопа банка ⇒ сквозной счётчик стоял бы на нуле всю первую волну.
Почему это всё равно был дефект (research/28 Б-0, вопрос владельца 16.08). Пара draft/edit
была не абстракцией, а сквозным пробросом внутренней структуры движка до React-компонента —
runevents.go:195-196 → ingest/events.go:121,130-135 → SQL-констрейнт
check (wave in ('draft','edit')) (00015_seam_ceiling_and_units.sql:56) → wireProgress
(v0.go:92-96) → спека → генерённые типы → format.ts:109-110. То есть архитектура движка была
пришпилена к контракту в шести местах, а её изменение — ломающим для фронта.
Решение владельца 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 — и его проекция (было в 0.2.3, при резке прозы уцелело в каноне и
пропало здесь; возвращено синком 20.08). 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.- Снимок стопа переехал в ЧТЕНИЕ банка (Б-14а). ⚠ Пере-писано D39.144 (модель владельца 16.08:
подпись — ОДИН акт над всем банком):
complete/pending_decisions— ИНФОРМАЦИОННЫЕ,resumeRunснимает стоп при ЛЮБОМ состоянии решений, 409-полноты НЕТ. Прежняя формулировка — «поле, по которому решается, можно ли предлагать „продолжить“» — существовало только в квитанции POST. Экран, перезагруженный посреди стопа, мог прочитать ВЕСЬ банк и не узнать, сколько решений осталось; единственная реализация выводила признак какleft === 0(frontend/src/mock/handlers.ts:203-204) — инвариант, которого контракт не объявлял. Теперь ⚠ (0.5.0: клауза счётчиков этого пункта ПЕРЕКРЫТА — прежде чем читать дальше.) Сняты со всех трёх носителей; честный счёт едет квитанцией двери правок (signature), разбор — §2.19-бис. Исторический текст 0.3.0:pending_decisionsиcompleteотвечает иGET /bank, и квитанция, и кадрbank.
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 тоже принадлежат соединению, не книге. Первая редакция батча этого не развела, и у
реализатора оставалось два пути, оба против текста: минтить служебным кадрам книжные номера (тогда два
одновременных зрителя тратят номера друг друга, и Last-Event-ID одного указывает на кадры, которых
второй не видел) либо повторять последний номер (тогда «one per frame» ложь). Поймано кросс-модельной
линзой; исправлено: служебные кадры несут id последнего кадра ИСТОРИИ и своего номера не тратят,
повтор id на них легален, а дыра в нумерации легальна из-за склейки — и клиенту прямо запрещено читать
пропуск как потерянный кадр. Последнее правило было в 0.2.3, потерялось при резке прозы и возвращено.
Дельта-чтения ВКЛЮЧИТЕЛЬНЫ (>=), а не строго больше. Первая редакция батча сделала
?after_version= строгим — и тем сломала собственное правило 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; к смене версии привязаны
курсор, якоря на пары, окно термина и идентичность строки банка. ⚠ Первая редакция батча положила
версию только в hello и два конверта — и тем оставила ДВЕ свои же обязанности («перечитать банк,
когда версия сдвинулась», «уронить якоря на пары») без единого триггера, а BankPage/NotePage —
без указания, к какой структуре относится их содержимое. Поймано обеими линзами селф-ревью,
исправлено.
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— платформа обязана мерить оба на ПРОВОДНОЙ форме до спавна; остаточный случай (HTTP-тело < 1 МиБ, отрендеренный документ шва — больше) падает в409bank_corrections_refused, чьё описание несёт «a set larger than the service applies in one act». ⚠ Здесь монтаж и ошибся: он мерил ПРОВОДНОЕ тело, а движку отдавал документ, отрендеренныйjson.Marshalс HTML-экранированием (&/</>— один байт в шесть), так что законное тело под проводным потолком перепрыгивало движковый и возвращалось409«пере-решите» вместо канонного413. Найдено воркфлоу-ревью P9 замером (не рассуждением: два агента ошибочно положили ось в «не опровергнуто», рассуждая отomitempty), вылечено выключением экранирования — документ читает движок, не браузер; (б) канонный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) стоит именно на этом различении, и он честно записал его как СВОЁ суждение, а не как факт.
⚠ И на приёмке это суждение чуть не было пробито ПРОЗОЙ. Первая редакция минора описывала событие границы словами «the shape of the pipeline … an editing pass added or removed on the deployment» — то есть называла фазу конвейера открытым текстом, дважды. Граница запрещает это прямо и своими же словами: «no phase or stage names… The rule binds the PROSE too — every description here is compiled into the generated client's source» (§Boundaries). Поймано линзой контракта при приёмке P12, переформулировано формо-нейтрально («когда деплой меняет, что для главы значит „готово“») оркестратором №21 ТЕМ ЖЕ лендингом. Урок для следующего минора: непрозрачность поля не спасает, если проза рядом прозрачна — граница судит и описания, потому что они компилируются в исходник клиента.
⚠ Попутно тем же проходом снята битая ссылка ратифицированного канона: проза ShapeEpoch ссылалась на
схему BookStage, которой в контракте НЕТ ВОВСЕ (grep -c BookStage = 0 после правки; до неё
единственным вхождением была сама ссылка). Заменена на указатель формы, уже принятой в файле — «см.
Boundaries». И три примера contract_version, оставшиеся на 0.8.0, подняты до 0.9.0.
Механика — в зоне платформы: пара колонок books.shape_epoch + books.epoch_editor (миграция
00030_shape_epoch.sql, бэкфилл epoch_editor = edit_wave, поэтому первая граница — только реальная
смена формы). ⚠ Полоса ПРОГОНА на эпоху НЕ переведена: зона попробовала и откатила своим же
адверсариальным проходом — эпоха присваивается и ходит в обе стороны, отчего полоса переставала быть
монотонной (замерено: один прогон читал 4/4, потом 2/2 на своих же двух попытках при неизменной
structure_version). Через эпоху считается ТОЛЬКО пожизненный счёт книги; остаток — открытая строка
регистра платформы PD-435.
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 на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op, а на awaiting_bank это ещё и не вызывает ReleaseBankStop. Полная таблица по статусам — в описании resumeRun |
вход P7 ⚠ ИЗРАСХОДОВАН (D39.153) (правка построенного пути, не только читающей поверхности) |
GET /usage |
кредиты | ПОСТРОЕНО, не читается ни одним экраном | зона фронта |
⚠ ТАБЛИЦА ПЕРЕ-СНЯТА ПРОТИВ КОДА 22.08 (оркестратор №18, аудит доков): шесть строк объявляли НЕ ПОСТРОЕННЫМ то, что пак P7 построил, а P8 сохранил. Правило §Б-21 требует вести здесь статус построенного — оно нарушалось пятый раз подряд, потому что носитель «вход P7» никто не гасил при лендинге. Носитель израсходован: пак P7 принят D39.153, пак P8-FIX — D39.154.
| GET /capabilities | конфигурация деплоя | ПОСТРОЕНО P7 — маршрут смонтирован безусловно (platform/internal/httpapi/v0.go, таблица contractSurface), хендлер capabilities.go; версия контракта запинена ГЕЙТОМ против ЭТОГО канона (platform/internal/gates/contract_test.go) | закрыто D39.153 |
| PATCH/DELETE /books/{id}, GET /runs/{id} | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) — факт верен: роутер монтирует 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; прежняя запись «не хватает проекции» устарела при израсходованном носителе «вход P7 ⚠ ИЗРАСХОДОВАН (D39.153)», D39.153); форма синхронна канону с лендингом P9: счётчики pending_decisions/complete сняты с проекции (PD-399) | закрыто D39.162 |
| | стоп-механика майнера | ⚠ НЕ «не построено», а ОТМЕНЕНО: было построено P7 и СНЯТО 22.08 вместе с пер-термной моделью подписи (D39.144, слово владельца). 0.5.0 снёс и канон-половину — POST /bank/decisionsPD-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, на двух создающих вызовах; ⚠ живой дефект под конкуренцией — 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 (тот же факт этот док уже несёт ниже, в §6в); живой носитель — строки бэклога 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 доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ Остаток ДВИЖКОВЫЙ, две штуки, обе найдены воркфлоу-ревью P9: decline не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, mining.go:548), и повтор ТОГО ЖЕ документа на одном пути не сходится (membank/decisions.go:374) — второе ломает обещание сходимости, на котором стоит синхронная дверь | заказано бэкенд-паку «тихая порча», ../../archive/prompts/BACKEND_SILENT_HARM_SESSION_PROMPT_2026-08-28.md §3.3 | ⚠ пере-снято 30.08: из двух движковых остатков ВТОРОЙ ЗАКРЫТ — порядок записи документов решений сменён на rejects-first, повтор принятого документа СХОДИТСЯ (D39.164 §3.3). Живым остаётся ОДИН: decline не энтити-широк на обратном пути — отклонённая сущность возвращается через свой АЛИАС, тогда как канон обещает «declining a surface removes EVERY window of that surface». Носитель — строка 228 единого бэклога.
| Снятие замечания (переход «флаг снят») | движок | НЕДОСТИЖИМО сегодня, проверено чтением движка — п. H §6в | PD-298 регистра платформы (platform/docs/DEFECT_REGISTER.md) — там строка и живёт; механизма не строим |
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | вход P7 ИЗРАСХОДОВАН (D39.153) (PD-202) |
⚠ Три прежних предупреждения СНЯТЫ как устаревшие (Б-7а), и это причина, по которой заведена таблица выше:
- «
GET /bank— канала нет вообще» — неверно с D39.122: сайдкар пишется. Файл при этом противоречил сам себе (строка таблицы против абзаца ниже неё), а зона фронта до сих пор учится по старой версии (Ф-43). - «
EventNote— движок не эмитит пер-юнитных замечаний» — эмиттер приземлился 14.08. - «
EventCeiling— зависит от эмиттера» — то же (кадр при этом снят по §5а, см. §6).
⚠ Четвёртое было ХУЖЕ устаревшего — оно было НОРМАТИВНЫМ и отнимало работающее лечение. Спека
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) и здесь не
сохраняется даже как история — он учил зону неправде. Что на самом деле давала редакция 0.2.3, если
в движок добавляли волну (реальный format.ts собран через vite и вызван):
- третья волна приезжала как новое поле, клиент «игнорирует неизвестные поля» — и
translatedPercentотдавал 100 %, когда треть работы не сделана. Молча; - переименование фаз или снятие редактуры → TypeError в рендере (
Status.tsx:39,About.tsx:96), аErrorBoundary/errorElement/componentDidCatchво фронте нет ни одного (грепом пусто); - незнакомую волну платформа тихо дропала (
pgstore/sink.go:206-208) — прогресс занижался без ошибки; - «правок ноль» на деле означало миграцию БД + шов + спеку + регенерацию типов (её принуждает
дрифт-тест
frontend/src/api/contract.test.ts:30-37).
Ответ редакции 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 |
| Сжатие и условные чтения НА ПЛАТФОРМЕ | контракт их объявил; включение — работа платформы | строка 186 |
| Сворачивание замечаний в тексте под кнопку | зона фронта, при разморозке | пинг фронту 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а и что резать отказались
Снято: Problem.instance (структуры на платформе нет; функцию несёт request_id) · кадр
EventCeiling целиком (единственное поле halted всегда true, дубль кадра status, который уже
несёт paused_reason) · EventHello.run_id (поток теперь книжный) · EventResyncRequired.reason ·
схема Counter (одна полоса вместо двух) · Book.genre и BookIntake.genre (Б-23) · значение
finalizing · прогонный поток GET /runs/{id}/events (перевешен на книгу) · зашитые числа размеров
страниц из прозы операций · генезис-проза (история ратификации semver, две апологии RFC 9110,
объяснение имени source) — ужата НА МЕСТЕ до ссылки на источник, а не перенесена сюда: в спеке
остались semver §4 и одна строка про Retry-After на 200, целиком выброшенного текста нет.
Формулировка «перенесена в этот файл» держалась ровно один раунд и исправлена дофиксом (ФБ-10):
проверяется грепом — этих абзацев в компаньоне нет.
Резать отказались, с контраргументом на каждое:
- параметр
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 (заказ приёмки D39.142): что изменилось в каноне и почему
Десять находок ХОЛОДНОГО ПОТРЕБИТЕЛЯ — линзы, которой у селф-ревью батча не было: только финальная спека и проба генератором, без ревью, без компаньона, без диффа. Ниже — не пересказ правок, а их основание; сами правки в каноне.
Форма кадра была двусмысленна (ФБ-1). EventEnvelope{event,id,data} читался и как объект на
проводе, и как описание SSE-фрейминга — оба чтения соответствовали тексту, и клиент по второму
прочтению искал бы JSON с полем data внутри. Теперь на схеме стоит дословный пример кадра и
сказано прямо: event и id — ПОЛЯ SSE, телом кадра является data и только оно. Это тот класс
дефекта, который не ловится ни линтером, ни генератором: документ валиден в обоих прочтениях.
Книга в покое могла крутить переподключение вечно (ФБ-2). Служебные кадры несут id последнего
кадра истории — а у книги, которая ещё ничего не производила, такого кадра нет. Клиент оставался без
Last-Event-ID, каждый его запрос был «новым потоком», сервер отвечал hello+end, браузер
переподключался — и так по кругу. Закрыто самой дешёвой из возможных мер: история нумеруется с 1,
служебные кадры пустой книги несут 0, а Last-Event-ID: 0 на книге в покое попадает под уже
существующее правило 204. Последовательность стала конечной: hello → end → закрытие → одно
переподключение → 204 → браузер останавливается.
Обещание «note не теряется» не переживало разрыв (ФБ-3). Внутри одного соединения кадр-добавление
защищён запретом склейки; между двумя соединениями — ничем: буфер не обещан, resync_required за
обычный реконнект не полагается. Обещание не расширено (буфер обещать нечем), а названа обязанность
клиента: после КАЖДОГО переподключения — дельта-чтение /notes?after_version=…. Механизм для этого
уже был; не хватало записи, что он обязателен, а не удобен.
Пустые члены allOf давали необитаемые типы (ФБ-5). EventEnd и EventResyncRequired
описывались как EventBase плюс пустой объект — генератор выводил Record<string, never>, и
пересечение становилось типом, значение которого построить нельзя. Заменено на чистый allOf из
одного члена с описанием на самой схеме; проверено генератором — оба теперь EventBase. Попутно
записано то, что раньше подразумевалось: эти два кадра НЕРАЗЛИЧИМЫ по форме, диспетчеризация только
по имени события.
«Новый прогон» предписывался, а условия — нет (ФБ-6). Спека велела предлагать новый прогон как
лечение стопа по потолку, но нигде не перечисляла, что делает resume в каждом останавливающем
статусе, а paused_reason: null описывался как «нейтрально, продолжаемо» — из чего клиент мог
заключить, что при null надо звать resume. Добавлена таблица по всем статусам и сказано прямо:
новый прогон легален при ЛЮБОМ paused, включая null; причина паузы — подсказка о прошлом, а не
разрешение на будущее. Значение enum при этом не заводилось — ратификация D39.132 п.2а не двигается.
Пол под моделью ошибок (ФБ-7). Весь §Errors описывал ответы, которые СООТВЕТСТВУЮТ контракту;
что делать с ответом прокси, HTML-страницей или оборванным телом — не говорил никто, а именно там у
клиента нет ни code, ни request_id. Записано правило того же вида, что и для известных кодов:
не показывать из такого ответа ни байта, рисовать свою нейтральную фразу.
Пачка мелких (ФБ-8). title: null в merge-patch — объяснено, почему это 400, а не удаление
члена по RFC 7386 (книги без названия у этой поверхности не бывает) · X-TM-Client — записано, что
required: true стоит при живом исключении для bearer, потому что условной обязательности по схеме
безопасности OpenAPI не выражает, и валидатор не должен читать законное отсутствие как нарушение ·
список отказов интейка помечен неисчерпывающим (авторитет — code ответа) · правило резолюции
Location · ⚠ (ПЕРЕКРЫТО 0.4.0, см. §2.18) тождество Idempotency-Key на multipart считается по объявленным частям, а 408 не
считается состоявшейся попыткой · min_chapters при max_chapters: 0 — не диапазон, клиент проверяет
максимум первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» — снято
как обещание без носителя, заменено на наблюдаемое (BankPage.signed против total) · 409 у
экспорта объяснён как ключевой, а не книжный · неизвестный term_id в подписи отклоняет весь батч, а
не молча пропускает строку.
Слова, которые называли не то (ФБ-9). parser_unavailable называл наш компонент — переименован в
processing_failed, по эффекту: имя значения это то, на что клиент вешает фразу. ⚠ Проводное имя
теперь отличается от внутреннего словаря платформы — там значение зовётся parser_unavailable
(platform/internal/books/parse.go:66), и проекция обязана отобразить одно на другое; рядом там же
живут schema_mismatch и storage_unavailable, которые на провод не идут вовсе. Вход P7. Из описаний вычищены внутренние ссылки (research/28 §…,
«companion К-6»): описания компилируются в исходники клиента, и ссылка на ревью в чужом репозитории
там — мусор; носители остались здесь.
6в. Синк с платформой 20.08 → канон 0.4.0: что решено и почему
Вход — platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md, четырнадцать мест, где провод сообщает
состояние и не даёт клиенту действия. Записка честно разделяет ФАКТ / ⚠ МНЕНИЕ ПЛАТФОРМЫ / ГРАНИЦА,
и разбор шёл по этой границе: ФАКТы перепроверены исполнением, МНЕНИЯ проверялись на ПОСЫЛКУ, а не
только на предложенную форму. Два мнения посылку не выдержали (B, часть A), одно оказалось
слабее собственного факта (L о метках глав).
⚠ База ссылок этого раздела — РАБОЧЕЕ ДЕРЕВО 20.08, а не HEAD. В platform/ на этот момент 81
незакоммиченный файл (P7 в работе), поэтому одна и та же строка в HEAD и в дереве разная, и ссылка
без базы проверяема только случайно. backend/ чист, его ссылки годятся в обоих. Там, где
конструкция переживёт любую перенумерацию, адрес дан ИМЕНЕМ функции или ветки — это дешевле и
надёжнее номера. Поймано ревью диффа: первая редакция §6в смешивала обе базы в одной строке таблицы.
п.0. Сверка на потери прозы 0.2.3 → 0.3.0 — исполнена; найдена ОДНА потеря, она и была заявлена
Метод: полный дифф лендинга 8d82096 по обоим файлам (300 удалённых строк компаньона, 597 канона),
чтение ВСЕХ удалённых строк, выделение из них утверждений-правил и проверка каждого на наличие
смыслового двойника в живых файлах.
⚠ Отдельно про метод, потому что он чуть не дал ложный результат. Первый проход греп-проверки
шёл построчно и дал шесть «пропаж», которых нет: правило переносится через перенос строки, и
grep "fewer rows than asked" не находит текст, где fewer заканчивает строку. Сверка пере-ранена
на файлах, склеенных в одну строку (tr '\n' ' '), — и все шесть нашлись на месте. Тот же класс
ошибки в этой сессии уже ловился однажды; ставлю его сюда как метод, а не как случай.
Итог: единственная потерянная НОРМА — фраза §2.14 («различать причины отказа клиент не может по замыслу»), восстановлена. Остальное удалённое — три класса, и ни один не является потерей правила:
- правило уцелело в КАНОНЕ, а канон нормативен —
kind/null(«MUST NOT drop it or invent a kind»), «MUST NOT clamp it again», «stale read is the CLIENT's duty», «catch-up reads>=», «assembling a book's text on the client is forbidden», «pairs are read PER CHAPTER», уникальность термина по пятёрке, «a phrase MUST exist for a reason we do not know yet»,no-store, CSRF, запрет реплея истории, «max_chapters: 0= экран исчерпанности». Один из них — правило пустогоkind— уцелел в каноне, но потерял здесь ПРОЕКЦИЮ (""→null— работа платформы); клауза возвращена в §2.8; - утверждение снято ОСОЗНАННО и с записанным основанием — «carried forward marked as
unverified» (ФБ-8, обещание без носителя), «замена в
failedзапрещена» (ужато до «неfailed»), «Required: HTTP/2 at the edge» (Б-16, RFC 9205), пофазные счётчики,EventCeiling,genre,finalizing, пример«CJK leak … 第一节»(он и был утечкой, §2.12); - пропала МОТИВИРОВКА, а не норма — «1.9 юнита на главу», «иначе строки выглядят дубликатами и
удаляются», «portable to the desktop client»,
Intl.DisplayNames. Это цена резки прозы, принятая осознанно; ни одно не меняет того, что обязана делать сторона.
Вывод, который стоит записать отдельно. Приёмка батча сверяла мультимножество модальных глаголов
и этот класс структурно не ловила — но пере-сверка показала, что улов класса равен одному. Отсюда
дешёвый гейт вместо дорогого: при следующей резке прозы сверять не модальность, а список
собственных имён формы (полей, значений, кодов, заголовков) — норма почти всегда стоит рядом с
именем, а фраза §2.14 пропала именно там, где имени не было (/auth/* — не поле). Носитель: эта
запись.
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(и)).
⚠ Номера строк в чужой зоне в этом разделе сняты намеренно (находка ревью): дерево платформы на 20.08 держит незакоммиченные правки, поэтому одна и та же строка в HEAD и в рабочем дереве разная, и половина ссылок первой редакции указывала в одно дерево, половина — в другое. То же лечение, что у приложения А: адресуемся именем функции и ветки.
Правка канона (§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), и «денег не
хватает» из них не следует ни для одного. То есть класс закрыт, а дыра — только в четвёртом.
C. Вход /auth/* — ИСПОЛНЕНО (§2.14)
Фраза восстановлена, вариант A уточнённый записан целиком, триггер пересмотра зафиксирован, словарь в компаньоне НЕ объявлен. Разбор потери — п.0 выше. Возражение зоны про «нормативность с чёрного хода» принято дословно и стало основанием.
D. Note.code против пустой клетки — ПРАВЛЮ ОБА ФАЙЛА
Дыра реальна и была острее, чем в записке: канон не просто требует поле, он называет Note.code
«a closed vocabulary of this version», а карта в приложении А оставляла клетку пустой — то есть
сервер обязан прислать значение, которого словарь не содержит. unspecified ратифицирован как
ОБЯЗАННОСТЬ сервера (не строка словаря причин), записан и в канон, и в приложение А, п.5. Довод
записки про независимый выпуск принят. Прецедент D39.144 опорой не служит — согласен, его довод
(«спека ещё никем не потреблена») больше не верен.
Родственный вопрос про ступени — НЕ решаю: К-6 открыт, слово владельца 16.08 — «решим потом».
Но провизорность построенного зафиксирована в приложении А, п.6, чтобы ingest/notes.go не
читался как ратифицированная карта.
E. Отпечаток интейка (PD-262) — ПРАВЛЮ КАНОН, форму НЕ трогаю
ФАКТ подтверждён дословно: intakeFingerprint берёт r.ContentLength (httpapi/v0.go:511-517), и
комментарий рядом (:503-510) сам называет оба остатка. Из двух исходов один безопасный (ложный
key_reused на другой границе multipart — честный ретрай отвергнут), второй нет: chunked-клиент
даёт "unknown" обоим запросам, и две разные книги под одним ключом реплеят первую — то есть
ключ возвращает ответ, принадлежащий чужому запросу. Это нарушение самой гарантии, ради которой
ключ существует.
Выбрано: привести фразу канона к тому, что сервер может сравнить, БЕЗ поля размера в форме.
Записано: «тот же запрос» = те же метаданные + то же ИМЯ файла + то же СОДЕРЖИМОЕ; чем сервер
устанавливает последнее — его дело (дайджест на лету стоит ноль, книгу в памяти держать не надо);
обрамление multipart и длина тела запросом НЕ являются; сервер, который не может установить
тождество, не реплеит, а отвечает 409 idempotency_conflict. Почему не поле размера: оно
дало бы клиенту ещё одно обязательное поле, не закрыв случай «две разные книги одной длины», и
всё равно потребовало бы сверки с фактически прочитанным. Механизм отката для этого уже построен
и назван зоной верно (trailingParts v0.go:536-563 → books.go:190-204); регистр зоны сам
называет полное лечение «сверка принятого дайджеста на завершении с откатом» — контракт теперь это
разрешает вместо того, чтобы описывать деталь, которой в форме нет.
F. ETag/304 на любом безопасном чтении — ПРИНЯТО, канон правится одной фразой
Зона права дважды: RFC 9110 валидатор на любом GET разрешает, и список исключений в слое записи
протух бы за пак. Записано в шапке канона: валидатор на любом безопасном чтении легален и
объявления не требует; операции, которые его объявляют, — те, где клиенту есть смысл им
пользоваться, а не исчерпывающий список. Плюс две границы, чтобы фраза не стала лицензией:
клиент НИКОГДА не обязан слать If-None-Match, а 304 бывает ответом только на присланный.
getUsage/getRunOptions в объявленные НЕ добавлены сознательно — оба читаются ровно перед
действием, и условное чтение там не даёт ничего, кроме риска показать устаревшую шкалу.
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> не несёт ни снапшота, ни прогона
(events.go:399-401), EnqueueOnce вторую строку не пишет (store/outbox.go:74-106), леджер
переживает прогоны (outbox.go:205-212) и запинен тестом
(runevents_test.go:242-246). Единственное окно — падение между записью строки в журнал и
пометкой (events.go:154-164,200-202); плюс redrive платформа не вызывает вовсе
(runner/engine.go:83-124).
Поэтому: строка «недостижимо» в регистр, а не механизм — ровно тот исход, который зона назвала
правильным. Но в каноне закрыт РАЗРЫВ ПРАВИЛА, который эта проверка обнажила: §AfterVersion говорил
«удаление так не выразить» только про ОПТОВУЮ замену, а про исчезновение ОДНОЙ строки не говорил
ничего. Записано: строка, once delivered, поодиночке не отзывается; коллекция, которая обязана
потерять строку, теряет её единственным выразимым способом — заменой целиком с resync_required.
Это обязанность сервера, а не пожелание, и она делает поведение платформы выводимым: сегодня при
flagged=false строка обновляется и ревизия двигается (pgstore/sink.go:237-244), дельта её
прячет (readmodel.go:725-735), а note_count в кадре главы уже УМЕНЬШАЕТСЯ (sink.go:310-328) —
то есть клиент увидел бы счётчик, противоречащий списку. Теперь ясно, что должно произойти вместо
этого.
I. content_refused — forward-looking, НО с носителем; правлю канон и §3
Ответ на вопрос зоны: forward-looking, долг зона не заводит. Значение ратифицировано К-9
(владелец 16.08) до того, как появился производитель, и это сознательно: форму отказа лучше решить
не под давлением. Проверено — производителя нет ни на одной стороне (в платформе только константа и
строка таблицы; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ и уезжает как
Note.code: content_withheld, ingest/notes.go:45-48, — другой словарь, моста нет).
Правки: (а) оба значения помечены в каноне как объявленные ДО производителя, с причиной; (б) обязанность ограничивать число попыток аккаунта пере-привязана: она падает ВМЕСТЕ с производителем, а не висит на сервере, который отказа не выдаёт, — иначе это обязательство без адресата, ровно тот класс, из-за которого заведено правило Б-21; (в) §3 получил строку с носителем (строка 94, ПТ-16). Клиент обрабатывает код с сегодня — старый клиент, встретивший его впервые, и есть та беда, ради которой значение объявлено заранее.
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 на прогоне, который
успел доработать, потому что это и есть случай, который экран обязан объяснить.
K. --verify-bank — НЕ КОНТРАКТ, релей в зоны; но контрактно видимая половина закрыта
Расхождение «D39.144 (подпись = один акт) против пер-термного гейта движка» — ратификация и
поведение движка, обе вне этого файла: контрактная половина уже исполнена (D39.144/145 сняли гейт
полноты из канона и компаньона, 409 bank_decisions_incomplete удалён). Обход зоны (не передавать
флаг на resume) — её решение и её же декларация.
Контрактно видимая половина — вторая, и она молчала. Канон обещал decline — «leave it out»,
а решение до движка не доезжает (mined_rejects платформа не пишет), то есть отклонённый термин
возвращается предложенным. Это обещание пользователю, которого система не держит. Записано
предупреждением на BankDecision.action + строкой §3 с носителем — строка 192, отложенная
владельцем. Форму не меняю: обещание верное, не выполнена реализация.
⚠ (0.5.0: предупреждение умерло вместе со своим носителем — схема BankDecision снесена, а дверь-
преемник пишет файлы движка сама, так что «не доезжает» перестало быть свойством формы; остаток —
монтаж, пункт (2в) очереди D39.156. Абзац сохранён как история синка 20.08.)
L. Мелкое
- Ссылка приложения А протухла — ИСПРАВЛЕНО, и не новым номером, а именем функции
(
flagReasonSeverity): номер строки в чужой зоне протухает за пак, что здесь и произошло. headingвсегдаnull— посылка «у читателя НЕТ меток глав вообще» неверна, и это тот случай, где сказалась объявленная зоной граница (фронт не смотрели). 0.3.0 снял запрет клиентской служебной метки: «Глава N» рисует КЛИЕНТ, в локали интерфейса, за $0 (§2.3, решение владельца 09.08). Без меток остаётся только книга, у которой их нет в данных, — а это легальная книга. Продуктовый пробел «настоящих названий нет» держится видимым строкой §3 (строка 160) и никуда не делся; менять нечего.410на опечатку в id главы (PD-253) — канон изменён в сторону зоны, а не наоборот. Требование различать «была и исчезла» от «такой не было» невыполнимо: id непрозрачны, после пере-разбора не хранятся, и доказать, что id никогда не минтился, можно только кладбищем всех выданных. Записано:410— ответ на id, которого нет в ТЕКУЩЕЙ структуре, был он когда-то или нет;404остаётся за книгой. Лечение у клиента в обоих случаях одно — перечитать дерево. Расхождение перестаёт быть расхождением.
M. Что зона сделала сама — сверено, возражений нет
Все восемь пунктов сверены с каноном: область Idempotency-Key (принципал, метод, путь),
подрезание limit, resync_required вместо тихого старта, blocked только когда холд реально
укорачивает шкалу, heading: null, finalizing снят, полоса и её база на одном проходе, снятие
codeForStatus. Совпадает.
N. Границы этой сессии
Закрыто из того, что зона объявила своей границей: пункт H сверен с движком (чтением), пункт G
сверен с обеими зонами, совместимость с генерённым фронтом проверена исполнением
(openapi-typescript@7 — чисто; единственная ломающая правка названа). Пункт B живьём тоже не
воспроизводился — и не требовался: он закрыт ратификацией, а не замером. Не проверялось: как
экраны фронта рисуют новые ответы (зона заморожена), и поведение построенного пути resume после
правки таблицы — это работа P7.
Что забирает зона (P7): 409 ceiling_reached на исчерпанном прогоне вместо 202 (A) ·
unspecified как обязанность, а не самодеятельность (D) · тождество интейка по содержимому с
откатом вместо Content-Length (E) · «сделано» = последний проход этого деплоя (G) ·
Run.stop_requested в projectRun (J) · при снятии флага — resync_required, а не тихое
исчезновение (H). Что закрыто одной фразой в тексте и работы зоны не требует: C, F, I, L.
Ревью диффа этой зоны (исполнено этой же сессией, 20.08) — что оно изменило
Четыре независимые линзы по диффу: ХОЛОДНЫЙ ПОТРЕБИТЕЛЬ (модель другого семейства — fable; читал ТОЛЬКО финальный канон, без диффа, без компаньона, без записки платформы, и пытался реализовать шесть изменённых мест) · опровергатель ПОСЫЛОК диспозиций · регрессионная линза (умерло ли правило вместе с удалённой строкой + независимая пере-сверка 0.2.3 → 0.3.0 ДРУГИМ срезом: не по удалённым строкам, а по перечислению всех схем/полей/операций 0.2.3) · линза стандартов (RFC по первоисточникам). Каждая находка потом адверсариально верифицировалась отдельным агентом с установкой «по умолчанию опровергнуто». 31 находка, 5 подтверждено, 26 опровергнуто; 35 агентов, 0 упавших (важно: упавший агент — это НЕ «опровергнуто»).
Ни одна из пяти не опрокинула диспозицию — все пять о точности МОЕГО текста, и это правильный результат для ревью, которое ищет не «согласен ли я», а «проверяемо ли написанное»:
- Денежный довод в пункте G был сильнее фактов — и уже стоял НОРМОЙ в каноне. «Предлагает уже сделанное и берёт за них деньги»: вторая половина не проверена и для обычного случая неверна (потолок, а не цена; расчёт по замеренной трате; неизменённая книга переигрывается за $0). Снято и из канона, и отсюда; довод «предлагает уже сделанное» стоит сам.
- «Платформа физически не пишет
day_usd» — неверный глагол: не ЗАВОДИТ, но шаблон деплоя пере-маршалится целиком, и оператор может внести поле без единой правки Go. Поправка усиливает триггер пересмотра, а не отменяет отказ. - «
Runедет в кадре потока» — ложный факт: ни один payloadRunне несёт. Цена реальна, но её носитель другой — предписанная кадром перечитка карточки. - Носитель строки H («регистр движка») — документ, которого нет. Живой носитель — PD-298 регистра платформы. Ровно то нарушение правила Б-21, которое этот же файл и вводит.
- Ссылки в чужую зону мешали ДВЕ базы (HEAD и рабочее дерево с 81 незакоммиченным файлом платформы) в одной строке таблицы. Лечение — база названа явно, адресация именами функций.
⚠ Три опровержения я отклонил и правку внёс. Три линзы независимо споткнулись об один шов —
что отвечает resume, когда у прогона лимит ещё есть, а СЧЁТ не тянет холд. Каждую формулировку
верификатор опроверг по отдельности («тексты можно прочесть согласованно»), но холодный потребитель
прямо написал, что решить не может, а платформа этот случай уже схлопывает в тот же вердикт.
Сходимость трёх независимых линз на одном месте — сигнал сильнее трёх поштучных опровержений.
Заведено cause.code: credit_unavailable (второй уровень не закрыт по замыслу — типы не двигаются),
ceiling_reached пере-сформулирован как «ЭТОТ прогон закончен», и явно сказано, что подменять друг
друга они не могут. Тем же ходом закрыт вопрос холодного потребителя про stop_requested после
resume (снимается) и возвращена клауза «предел попыток не на проводе», умершая с моей же правкой.
Второе ревью: велосипеды и объём прозы (заказ владельца 20.08)
Первое ревью проверяло текст на соответствие RFC. Владелец задал ДРУГОЙ вопрос — «а так вообще делают, или ты изобрёл своё», плюс «не раздул ли ты прозу». Отдельная проверка: линза охоты за велосипедами (модель другого семейства, с обязательным чтением первоисточников через WebFetch) + линза размещения прозы, которой прямо сказали, что автор диффа склонен переобъяснять. 20 находок, подтверждена 1.
Велосипедов не подтверждено ни одного. Проверялись против настоящих спек: тождество интейка
против draft-ietf-httpapi-idempotency-key-header и RFC 9530 (Digest Fields) — формулировка
«чем сервер устанавливает тождество, его дело» черновику СООТВЕТСТВУЕТ, а Repr-Digest предписать
клиенту мы не можем и не должны · 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 % этого — обоснование, которому место здесь»
пере-проверку не прошла: из тринадцати заявленных блоков выжил ОДИН — довод в Progress («контракт
не говорит, сколько проходов бывает…»), который (а) дословно уже записан здесь, в §6в G, и (б)
локально пере-выводит глобальное правило шапки канона. Срезано; правило осталось одной фразой.
Своим решением, а не по находке, срезан ещё денежный хвост у Book.chapters_done — он ничего не
меняет в поведении клиента. Итог трима: 152 758 → 152 002 байта. Урок для следующей правки канона:
норма — в канон, довод — сюда; проверять не глазом, а диффом генерённого клиента.
Ратификация этой редакции — D39.152 (20.08, оркестратор №18)
Текст ноты, сданный контрактной сессией, исполнен и заменён живым: ратификация записана в
docs/architecture/05-decisions-log.md, нота D39.152, строка реестра —05-decisions-index.md. Черновик ноты, живший здесь, снят при лендинге (испр. оркестратором №18): он расходился с итоговым текстом в двух местах, и две копии одной ноты — ровно тот распад «один носитель на факт», против которого написана норма D39.112. Что в живой ноте отличается от черновика: (1) внесены ЧЕТЫРЕ текстовые правки при ратификации (сеттерstop_requestedвstopRun· стухшая фраза §6в A против собственного аддендума · «cleared by a resume that SUCCEEDS» · приписка третьего случаяresumeRunк таблице); (2) клейм «ломающая правка ровно одна» уточнён: он верен для диффа генерённых ТИПОВ и неточен поведенчески — против 0.3.0 расходятся ЧЕТЫРЕ места (см. D39.152 п.5).
7. Эксплуатационные примечания — НЕ норма контракта
Вынесено из спеки в 0.3.0 (Б-16). RFC 9205 §4.1 прямо про наш случай: «Requiring a particular version of HTTP … harms interoperability. Therefore, it is NOT RECOMMENDED that applications using HTTP specify a minimum version … However, if an application's deployment benefits from the use of a particular version of HTTP (for example, HTTP/2's multiplexing), this ought be noted». Отметить, а не потребовать. Спека 0.2.3 требовала («Required: HTTP/2 at the edge») и держала в норме вендорный заголовок конкретного прокси.
Норма в спеке — то, что клиент НАБЛЮДАЕТ и на что вправе рассчитывать: ETag/If-None-Match/304
· обязанность честить Accept-Encoding на JSON-ответах и НЕ сжимать text/event-stream · Vary
на согласованном представлении · Cache-Control: no-store · запрет буферизации потока · форма
heartbeat · форма id кадра. Сжатие как ТРЕБОВАНИЕ живёт в контракте — это прямое указание D39.138
п.2(д) («сжатие и условные чтения записываются В КОНТРАКТ, не в зонный док»), и первая редакция
батча его нарушила, вынеся сжатие целиком в примечание; поймано опровергателем полноты и исправлено.
В примечании остаётся ТОЛЬКО слой исполнения — где именно сжимать, — потому что вот этого клиент
действительно не наблюдает, и вот это Б-16 из контракта и выносит.
Ниже — то, что клиент не наблюдает и что деплой обязан себе устроить сам:
- Чем и на каком слое сжимать (middleware, обратный прокси, CDN). ⚠ ПРОТУХЛО (испр. 02.09):
платформа сжимает САМА, в Go —
platform/internal/httpapi/conditional.go, грепgzipFloor(порог 1<<10 байт) иacceptsGzip; edge-слой для этого не нужен. - HTTP/2 на edge. Клиент держит одно SSE-соединение на книгу; на HTTP/1.1 шесть соединений на origin — потолок вкладок. Сервис на HTTP/1.1 обязан работать ХУЖЕ, а не не работать.
X-Accel-Buffering: no(или эквивалент прокси) — механизм для нормы «поток не буферизуется».- Сколько это даёт. Замер ревью на настоящей книге (
~/books/gu-zhenren, 2283 главы): глава 26,1 КБ → 10,4 КБ; дерево глав 250 КБ → 39 КБ; банк 1000 строк 167 КБ → 17 КБ. - Порядок работ по эффекту на килобайт усилия (research/28 §5б, строка 186): сжатие →
ETag/304 → скоуп в кадре → дельта-чтения?after_version=→staleTimeу клиента. Шаги 3–4 — уже в контракте (0.3.0), шаги 1–2 — ⚠ сжатие УЖЕ ПОСТРОЕНО в Go платформой (conditional.go, грепgzipFloor), конфигурацией остаётся только edge, шаг 5 — зона фронта. - Транспорт НЕ меняется (проверено независимым агентом другого тира + сверка первоисточников):
наша нагрузка — художественный текст, и после gzip разница между JSON и protobuf единицы процентов;
gRPC-web требует прокси и теряет
If-None-Match/304; Connect возвращает то, что у нас уже есть. Наш паттерн «кадр без данных → клиент перечитывает» легитимен и называется poke/pull.
Приложение А. Карта «причина → КОД контракта → продуктовая фраза» — ЗАГОТОВКА
Что изменилось в 0.3.0. Прежняя карта вела «причина движка → фраза», то есть предполагала, что
ФРАЗУ рисует сервер. Это ровно та политика, которую вариант B отменил для ошибок (§2.17), и держать
её для замечаний значило бы оставить на проводе серверную локализованную строку — второй русский
текст, приходящий из зоны, у которой нет ни Accept-Language, ни локалей. Поэтому Note несёт
code, а фразу рисует клиент; Note.message с провода снят.
Правила заполнения.
- Фраза пишется по ДОККОММЕНТУ
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 |
⬜ | ⬜ |
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; клиент рисует ту же нейтральную фразу, что и на незнакомый код) |
⬜ нейтральная, НЕ «ошибка» | ⬜ |
Причин пятнадцать; 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 (server.go:99),
500 (v0.go:518,572), 431 от net/http (serve.go:79), 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 глотает
метод.