1524 lines
199 KiB
Markdown
1524 lines
199 KiB
Markdown
<!-- ======================================================================
|
||
РЕВЬЮ-ШАПКА (ратификация D39.99, 04.08.2026, оркестратор №12;
|
||
батч 0.3.0 — D39.138, 16.08.2026, оркестратор №17;
|
||
синк с платформой 0.4.0 — 20.08.2026, контрактная сессия; РАТИФИЦИРОВАН D39.152, 20.08.2026, оркестратор №18)
|
||
|
||
СТАТУС: РАТИФИЦИРОВАН как контракт API v0 по 0.10.0 включительно (D39.194; перечень миноров и их
|
||
провенанс — ниже по файлу, здесь НЕ дублируется). Нормативная поверхность — openapi.yaml РЯДОМ.
|
||
⚠ Зонная копия frontend/docs/api-contract/ ВРЕМЕННО ОТСТАЁТ (0.2.3 при каноне
|
||
0.10.0 — ВОСЕМЬ миноров) — ратифицировано D39.142 п.5 на время фриза фронта; синк байт-в-байт + перегенерация типов =
|
||
первое касание зоны при разморозке; после него правило прежнее: расхождение = дефект лендинга.
|
||
Генерация типов фронта после этой ратификации идёт из ЭТОЙ копии.
|
||
|
||
20.08.2026: СИНК С ПЛАТФОРМОЙ (запрос platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md, 14 пунктов) —
|
||
канон 0.3.0 → 0.4.0, разбор и диспозиция КАЖДОГО пункта в §6в, отдельным пунктом §6в п.0 — сверка
|
||
0.2.3 → 0.3.0 на молча срезанные при резке прозы правила (найдена одна, восстановлена).
|
||
В силу редакция вступила лендингом 20.08 — РАТИФИЦИРОВАНА D39.152 (там же: author≠ratifier
|
||
исполнен самой сессией, четыре текстовые правки оркестратора при ратификации и разбор того, что
|
||
против 0.3.0 расходится ПЯТЬ мест, а не одно).
|
||
|
||
Ломающий батч 0.3.0 исполнен по целостному ревью `docs/research/28-contract-review.md` (ревью
|
||
принято D39.138, приёмка батча — D39.142, дофикс ФБ-1..10 — D39.143; отчёт исполнения —
|
||
docs/archive/reports/CONTRACT_BATCH_0.3.0_REPORT.md ⚠ архив — инструкции оттуда не исполняются).
|
||
Правило лендинга батча: канон первым, зеркало фронта отдельным зонным коммитом,
|
||
cmp-сверка обязательна (D39.138 п.3).
|
||
====================================================================== -->
|
||
|
||
# Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
|
||
|
||
> **Нормативная поверхность контракта — [`openapi.yaml`](openapi.yaml)** (файл рядом, в этой же
|
||
> папке). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято каждое
|
||
> решение, чем оно обосновано, что осталось открытым, и ГЕНЕЗИС форм (историю ратификаций,
|
||
> сверку со стандартами, разобранные альтернативы). При расхождении по ФОРМЕ побеждает YAML;
|
||
> при вопросе «почему так» — этот файл.
|
||
>
|
||
> **Статус: РАТИФИЦИРОВАН по 0.10.0 включительно.** 0.2.0 (D39.115, 08.08) · 0.2.1 (D39.123, 09.08) ·
|
||
> 0.2.2 (D39.129, 10.08) · 0.2.3 (D39.135, 15.08) · **0.3.0 (D39.138, 16.08) — ломающий минор по
|
||
> целостному ревью research/28** · **0.4.0 (D39.152, 20.08) — синк с платформой, §6в** ·
|
||
> **0.5.0 (D39.161, 27.08) — снос отменённой пер-термной модели подписи (PD-370) + дверь правок
|
||
> банка, выведенная из построенного глагола движка (D39.158), + предупреждение о конверте вне
|
||
> `/v0`; вывод и провенанс — §2.19, отчёт сессии — `../../archive/reports/CONTRACT_MINOR_REPORT_2026-08-27.md` ⚠ архив — инструкции оттуда не исполнять** ·
|
||
> **0.6.0 (D39.162, 28.08) — сквозная полоса прогресса и подпись стадии, приехавшие ОДНИМ лендингом
|
||
> с платформенным паком P9; вывод и провенанс — §2.20** ·
|
||
> **0.7.0 (D39.166, 28.08) — покупка ПЕРЕ-ПРОХОДА: правка банка доезжает в уже переведённый текст;
|
||
> провенанс — §2.21** ·
|
||
> **0.8.0 (D39.169, 29.08) — кадр `session_ended`: отзыв сессии гасит открытый поток и НАЗЫВАЕТ
|
||
> причину, вместо молчаливого обрыва, который клиент отвечает переподключением в `401`. Кадр
|
||
> СОЕДИНЕНИЯ: несёт id последнего исторического кадра и своего номера не потребляет, поэтому вход
|
||
> заново продолжает ровно с места остановки. Приехал ОДНИМ лендингом с платформенным паком P11** ·
|
||
> **0.9.0 (D39.180; ратифицирован №20 31.08 по пингу зоны ДО стройки, заленджен №21 с паком P12) — ВТОРАЯ
|
||
> граница пересчёта `chapters_done` и непрозрачный `Book.shape_epoch`, которым она себя называет;
|
||
> провенанс — §2.22** ·
|
||
> **0.10.0 (D39.194, 04.09; ратифицирован ПОСЛЕ стройки, по составу, вынесенному платформенной
|
||
> сессией из пака «закрыть цикл») — дверь выдачи, описанная тем, что построено: `failure_code`
|
||
> становится enum из ЧЕТЫРЁХ кодов, которые эта реализация ПРОИЗВОДИТ (условие «becomes an enum with
|
||
> the first built format» наступило); `Export.url` объявлен `uri-reference`, потому что отдаётся
|
||
> ОТНОСИТЕЛЬНЫЙ путь, а прежняя формулировка «minted for THIS response» описывала подписанную
|
||
> капабилити, которой никто не строил; третий адрес двери получает `operationId` `downloadExport` —
|
||
> до этого единственная поверхность, по которой браузер НАВИГИРУЕТ, жила в каноне только прозой;
|
||
> шестнадцатая причина замечания `off_target_lang` получает код `wrong_language` (Приложение А).
|
||
> Провенанс — §2.23**.
|
||
> Дом канона — этот каталог;
|
||
> `frontend/docs/api-contract/openapi.yaml` — байт-зеркало ⚠ НА ФРИЗЕ ЗОНЫ РАВЕНСТВО ПРИОСТАНОВЛЕНО (D39.142 п.5): зеркало 0.2.3, канон 0.10.0.
|
||
>
|
||
> ⚠ **0.5.0 ломающий по построению (мажор `0`): снесены путь `POST …/bank/decisions` и три его
|
||
> схемы, из `BankPage` и `EventBank` сняты `pending_decisions`/`complete` (§2.19-бис), в
|
||
> `Capabilities.required` добавлен `bank_corrections_enabled`.**
|
||
>
|
||
> ⚠ **Урок оставлен НАМЕРЕННО, он переживает свой дефект** (канон и проекция деплоя разошлись по
|
||
> ПОЛЯМ при совпавшем НОМЕРЕ версии — носитель `PD-399`, закрыт лендингом P9, D39.162; разбор —
|
||
> §2.19-бис)**: гейт версии этого класса не ловит.**
|
||
> `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` сверяет НОМЕР, а не поля — совпадение
|
||
> версий не значит совпадения форм, и следующее расхождение поля найдёт снова не он.
|
||
>
|
||
> ⚠ **0.4.0 ломающий ровно по одному месту, и это ЗАМЕРЕНО, а не объявлено.** Дифф генерённых
|
||
> типов 0.3.0 → 0.4.0 (`openapi-typescript@7`, комментарии отброшены) — **одна строка:**
|
||
> `Run.stop_requested: boolean`. Всё остальное в этой редакции — проза канона, одна запись в
|
||
> таблицу `resumeRun` и одно значение ВТОРОГО уровня (`cause.code: credit_unavailable`, а он по
|
||
> замыслу не закрыт и типов не двигает): корневой enum не сужен и не расширен, обязательных полей
|
||
> больше не появилось. Линтер зоны (`spectral --ruleset frontend/.spectral.yaml`) — 0 ошибок.
|
||
>
|
||
> ⚠ **Две границы этого замера, названные ревью диффа и обе честные.** (1) База — 0.3.0, а зонное
|
||
> зеркало фронта стоит на 0.2.3: при разморозке фронт платит переход 0.2.3 → 0.4.0 целиком, и это
|
||
> совсем другой объём (шапка ниже про отставание зеркала). Замер отвечает «что добавил ЭТОТ синк»,
|
||
> не «что стоит фронту разморозка». (2) Гейт §5 на утечку конвейерного словаря пере-ран и чист по
|
||
> сути, но «ноль вхождений» сказать нельзя: слово `edit` встречается дважды как обычный английский
|
||
> глагол («not an edit», «not a row edit»), `stage` — один раз в самой формулировке запрета. Ни
|
||
> одного нового вхождения этот синк не внёс.
|
||
>
|
||
> **0.x ломает миноры и будет ломать весь бета-период** (решение владельца 16.08, §8 п.17
|
||
> research/28): 0.3.0 — обычный ломающий минор, а НЕ последний перед 1.0. Окно 1.0 — после беты.
|
||
>
|
||
> **Язык.** Спека английская: из неё генерятся типы, а исходники фронта по конвенции английские
|
||
> (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской `K-N` — это те же
|
||
> вопросы §4. Спутник и остальные доки зоны — русские.
|
||
>
|
||
> **Зона строки 95 — «оркестратор/бэкенд/фронт».** Фронт авторитетен в одной трети: форма
|
||
> read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и работы
|
||
> движка здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют.
|
||
|
||
## 0. Пометки провенанса
|
||
|
||
| Пометка | Что значит |
|
||
|---|---|
|
||
| **✓ выведено** | следует из кода движка/платформы или ратифицированного решения; грунт `file:line` рядом |
|
||
| **◆ предложено** | решение автора контракта, разумное по его сведениям; подтверждает названная зона |
|
||
| **○ открыто** | развилка, на которую ответа нет; перечень — §4 |
|
||
|
||
⚠ Пометка ставится **на утверждение, а не на раздел**: у одного пункта половина бывает
|
||
выведенной, а половина предложенной.
|
||
|
||
**Пере-разметка 0.3.0.** Батч сдвинул четыре класса:
|
||
|
||
- `Progress` был ✓ (выведен из пофазной механики движка) — стал **◆**: одна полоса до ближайшей
|
||
остановки со знаменателем «купленный объём» это ПРОДУКТОВОЕ решение владельца (§8 п.1/п.8
|
||
research/28), а не проекция устройства движка. Это и было дефектом: форма ✓ означала, что
|
||
устройство движка обязано пролезать на провод;
|
||
- словари банка (`TermStatus`, `TermOrigin`) были ✓ по значениям — стали **✓ по различению, ◆ по
|
||
словам**: различение выведено из кода, слова назначены продуктом (движковые `auto`/`draft`/
|
||
`ruby`/`mined` на провод больше не идут);
|
||
- модель ошибок была ◆ (форма RFC 9457 как её понимал автор) — стала **✓ по словарю причин**
|
||
(класс 1 выведен перечислением реальных ветвей `platform/internal/httpapi/v0.go` (греп по именам хендлеров))
|
||
**и ✓ по политике** (вариант B ратифицирован владельцем, §8 п.4);
|
||
- `GET /capabilities`, условные чтения, `Idempotency-Key`, `blocked` — **◆ целиком**: формы
|
||
назначены этим батчем, подтверждает платформа при P7.
|
||
|
||
---
|
||
|
||
## 1. Почему YAML, а не проза
|
||
|
||
Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им типизируются
|
||
моки. Прозаический контракт расходится с кодом ровно тем способом, ради предотвращения которого
|
||
заведена строка 95.
|
||
|
||
⚠ **Но машинного мало, и 0.3.0 это подтвердил дважды.** Ревью прогнало линтер по канону 0.2.3 и
|
||
получило «No results» — то есть **все 29 находок ревью были смысловыми, ни одной синтаксической**.
|
||
А К-11 замерил, что `if`/`then` OpenAPI 3.1 генератор типов ИГНОРИРУЕТ. Отсюда правило,
|
||
действующее с 0.3.0: **любое межполевое или условное правило обязано быть записано И схемой, И
|
||
словами в описании поля** — схема защищает сервер, слова доезжают до клиента. Мест таких три
|
||
(0.5.0 сменил первое: схема `BankDecision` снесена вместе с моделью): `BankCorrection` (`dst` при
|
||
`approve`/`decline` и взаимоисключение `id`/кортежа), `Unit.target`, агрегаты `BankPage`.
|
||
|
||
Инструменты и пины — `STACK_DECISIONS.md` §3 и бэклог зоны.
|
||
|
||
---
|
||
|
||
## 2. Решения и их происхождение
|
||
|
||
### 2.1. Язык — код, никогда не имя — ✓ выведено
|
||
|
||
Движок держит коды (`backend/internal/config/book.go:26-27`), ключ пары — `zh-ru`
|
||
(`configs/langpacks/zh-ru/`). Имя языка в данных — пар-специфика в общем слое, запрещённая §2
|
||
канона. Вторая цена, дороже: `lang` элемента берётся из данных книги, и пара ja→ru с именем вместо
|
||
кода отрисует кандзи китайскими начертаниями молча.
|
||
|
||
**0.3.0 дополнил:** форма кода — не то же, что умение пары. Платформа проверяет только ФОРМУ
|
||
(`platform/internal/books/books.go:118-121`) и равенство `source == target` не проверяет никто, а
|
||
пар-промпты в репозитории есть ТОЛЬКО для `zh-ru`; отсутствие промпта пары — жёсткая ошибка
|
||
конфигурации, не деградация («no prompt for pair %q role %q … never silently substitute another
|
||
pair's conventions», `backend/internal/config/pipeline.go:952-956`). До 0.3.0 книга в
|
||
неподдерживаемой паре принималась, ложилась на диск, разбиралась, проводила пользователя через
|
||
денежный экран — и умирала на старте прогона без причины. Отсюда `Capabilities.language_pairs[]`
|
||
со статусом и код отказа на интейке (`errors[].code: unsupported_pair`).
|
||
|
||
### 2.2. Идентификаторы непрозрачны — ✓ выведено, классы стабильности разведены в 0.3.0
|
||
|
||
`glossary.id` — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется
|
||
(`store/migrate.go:173-174`); номер главы плотный, «Chapters that yield no text … do NOT consume a
|
||
chapter number» (`chunk/chunker.go:99-105`), поэтому правка исходника сдвигает номера последующих
|
||
глав.
|
||
|
||
**0.3.0:** одно предложение «Stability across runs is the platform's job» покрывало пять сущностей
|
||
с разной по природе стабильностью и для юнита обещало заведомо больше возможного: движковый id
|
||
юнита несёт cut-tag, и «any chunker/budget/pipeline-shape change mints a new id for EVERY unit in
|
||
the book, while chapter ids survive» (`backend/internal/pipeline/manifest.go:102`). Теперь в схеме
|
||
`Id` три класса явно: книга/прогон/экспорт — навсегда; глава — переживает пере-разбор; **пара —
|
||
только внутри одного `structure_version`**, и клиент это НАБЛЮДАЕТ (Б-7), а не узнаёт по разъехавшимся
|
||
вкладкам. Стабильность по-прежнему обеспечивает платформа при персисте манифеста (строка 100).
|
||
|
||
### 2.3. Заголовок главы отдельным полем — ◆ предложено
|
||
|
||
**Движок сегодня делает ОБРАТНОЕ**, и это надо назвать прямо: титул рендерится детерминистически из
|
||
шаблона пары (`configs/langpacks/zh-ru/heading.txt`), исходный маркер вырезается из текста для модели
|
||
(`chunk/chunker.go:110-114`), а на экспорте титул вклеивается внутрь текста первого юнита
|
||
(`pipeline/export.go:215`). Комментарий движка прямо запрещает подавать этот рендер как метку книги
|
||
(`pipeline/manifest.go:80-86`). Развилка — К-2.
|
||
|
||
**0.3.0 снял противоречие, которое существовало независимо от К-2.** Спека 0.2.3 запрещала клиенту
|
||
синтезировать метку «Глава N» — а решение владельца 09.08 (`research/27` §33-36) требует ДВУХ меток:
|
||
оригинальной из данных книги плюс служебного рендера «Глава N» за $0 в локали ИНТЕРФЕЙСА. Сервер
|
||
локали интерфейса не знает (`Accept-Language` в платформе нет грепом), значит служебный рендер обязан
|
||
быть клиентским. Запрет снят; `Chapter.heading` = только метка ИЗ ДАННЫХ книги, `null` — обычный
|
||
ответ, и деплою прямо запрещено класть сюда рендеренный порядковый.
|
||
|
||
**`title_raw` и `kind` (глава/фрагмент) в 0.3.0 НЕ заводятся — передано дизайн-паку этапа 161**
|
||
(строка 161, очередь D39.136 п.3), и вот почему это не откладывание: (а) производителя настоящих
|
||
названий не существует — это Этап 0, строка 160; (б) форма узла «глава ↔ технический фрагмент» и
|
||
вердикт детекции — ровно тот предмет, который дизайн-пак и решает, а заложенная до него форма
|
||
заморозила бы догадку; (в) добавление обоих полей — АДДИТИВНОЕ расширение (минор), а смены СМЫСЛА
|
||
существующего `heading` не будет: он и сегодня, и после 160 означает одно — метку из данных книги.
|
||
**Запись для пака 161: контракт 14 расширяется этим паком аддитивно; `Chapter` получает `title_raw`
|
||
(если решится, что рендер и оригинал — разные поля), `kind`, вердикт структуры; ломать 0.3.0 для
|
||
этого не требуется.**
|
||
|
||
### 2.4. Состояние — у прогона; у главы выполнение — ✓ выведено
|
||
|
||
Подпись банка это один стоп на всю книгу (`pipeline/mining.go:81,243`), поэтому «глава ждёт подписи,
|
||
пока соседняя финализируется» — невозможная картина. У главы движок держит `ChapterPassport`
|
||
(`pipeline/status.go:37-55`).
|
||
|
||
### 2.5. Прогресс — ОДНА полоса до ближайшей остановки, в главах (0.3.0) — ◆ решение владельца
|
||
|
||
**Прежняя форма (0.2.x — пофазно и в юнитах) снята как дефект** (research/28 Б-0, вопрос владельца
|
||
16.08): пара `draft`/`edit` была не абстракцией, а сквозным пробросом внутренней структуры движка до
|
||
React-компонента — движок оказывался пришпилен к контракту в шести местах, а его изменение —
|
||
ломающим для фронта.
|
||
|
||
**Решение владельца 16.08 («Согласен, переделываем» + В-5 «Ок, делаем так»):** одна полоса до
|
||
ближайшей остановки, знаменатель — КУПЛЕННЫЙ объём, после подписи банка полоса начинается заново.
|
||
|
||
**Форма, выбранная батчем: счёт в ГЛАВАХ, а не в юнитах.** Три довода, и второй — денежный:
|
||
|
||
1. **Одна величина, а не две.** «Один счётчик» и «знаменатель — купленный объём» вместе значат, что
|
||
числитель и знаменатель обязаны быть в одной единице. Купленный объём объявлен в ГЛАВАХ
|
||
(`ceiling_chapters`), и другой единицы у него нет: пересчёт «главы → деньги» на провод не идёт
|
||
(D39.84), пересчёт «главы → юниты» до разбора неизвестен.
|
||
2. **Дробь наконец означает то, что человек купил** (Б-13а). До 0.3.0 потолок был в главах, а
|
||
прогресс — в юнитах ПО ВСЕЙ КНИГЕ (`v0.go:473-477`), поэтому прогон, купленный на 10 глав из
|
||
2284, показывал дробь, которая не могла дойти до единицы, — и книга уходила в `paused` на 0,4 %.
|
||
А потолок стоит на КАЖДОМ прогоне: поле обязательное.
|
||
3. **Ноль всю первую волну не возвращается — его снимает СЕГМЕНТНАЯ логика, а не единица счёта.**
|
||
Сегмент = работа между двумя остановками; в первом сегменте глава засчитывается, когда её работа
|
||
ЭТОГО сегмента закончена, а не когда она пройдена от начала до конца. Обе величины у платформы
|
||
уже есть: `chapters.units_draft_done` / `units_edit_done` заведены именно под это
|
||
(`00002_readmodel.sql:102-105`, комментарий «K-10 is open… answering it is a projection change
|
||
rather than a migration»). Новых колонок батч не требует.
|
||
|
||
**Тем же ходом закрыт К-10 — вердикт «НЕ строить»** (D39.138, поправка приёмки research/28 №1):
|
||
пофазные счётчики на главу строить НЕ надо, потому что фаз на проводе больше нет вовсе.
|
||
`Chapter.units_done` считается той же сегментной логикой, что и книжная полоса, — иначе дерево глав
|
||
читало бы ноль всю первую волну, а это и была исходная жалоба К-10, и снятие фаз само по себе её не
|
||
лечит.
|
||
|
||
⚠ **(0.6.0: клауза «той же логикой, что и полоса» ПЕРЕКРЫТА — прежде чем читать дальше.)** Полоса
|
||
ПРОГОНА сегментной быть перестала: она одна монотонная доля на всю работу прогона и через стоп
|
||
подписи не обнуляется (§2.20). Глава — по-прежнему состояние ОДНОГО прохода и по-прежнему возвращается
|
||
к нулю, когда начинается следующий. То есть две величины теперь законно РАСХОДЯТСЯ, и это записано
|
||
предупреждением в самом каноне (`ChapterProgress.units_done`): клиент, зеркалящий одну в другую,
|
||
нарисует прыгающий бар. Абзац сохранён как провенанс батча 0.3.0 — вердикт К-10 «пофазные счётчики на
|
||
главу НЕ строить» им не затронут и в силе.
|
||
|
||
**Книжная полоса — отдельная величина.** `Book.chapters_done` против `chapter_count` — прогресс
|
||
КНИГИ (строка библиотеки), он не откатывается при старте нового прогона. Величина уже считается в
|
||
SQL и до 0.3.0 не отдавалась: `b.chapter_count - (select count(*) from chapters c where … units_done
|
||
>= units_total)` (`pgstore/books.go:677-681`). `Progress` на `Book` больше нет.
|
||
|
||
### 2.6. Словарь статусов — ✓ по механике, ◆ по составу; `finalizing` снят в 0.3.0
|
||
|
||
Лестница «загрузка → разбор → перевод → подпись банка → финал → готово» пришла из строки 95.
|
||
`stopped`, `rejected`, `not_started` контракт ВЫВОДИТ из поведения процесса, а не получает полем:
|
||
механика стопа у движка есть (`cmd/tmctl/main.go:63`), но «кто нажал» знает платформа.
|
||
|
||
**Стоп по потолку — не `failed`** ✓ выведено: «Ceiling is a hard, book-wide stop … the job stays
|
||
'pending' and resume continues once the ceiling is raised» (`pipeline/stagerun.go:488-489`).
|
||
|
||
**0.3.0 — три правки:**
|
||
|
||
- **`finalizing` снят.** У движка такой фазы нет вовсе (`grep -ri finaliz backend/internal` — пусто),
|
||
в словаре ingest её нет (`ingest/events.go:53-67`), писателя у значения нет нигде. Держалась она
|
||
на лестнице владельца — а владелец 16.08 сказал про лестницу: «Ну да, это чисто моя фраза была»
|
||
(§8 п.15). Устная формулировка нормой продукта не является, статус ею не связан. Появится фаза у
|
||
движка — значение вернётся минором так же дёшево.
|
||
- **Заведён отдельный `RunStatus` (6 значений).** `Run.status` был типизирован книжным словарём из
|
||
11 значений, из которых на прогоне легальны не все, и спека говорила это ПРОЗОЙ — то есть
|
||
генерённый union был шире правды, а клиент обязан был писать недостижимые ветки. Узкий словарь у
|
||
платформы в DDL уже записан (`00002_readmodel.sql:47-49`).
|
||
- **Записано правило старшинства** «книга производна от прогона, кроме `uploading`/`parsing`/
|
||
`not_started`/`rejected`». До 0.3.0 правила не было, и фронт уже разошёлся сам с собой: полоса
|
||
состояния решала «paused» по прогону (`showcase/Status.tsx:21`), карточка — по книге.
|
||
|
||
### 2.7. Состояние пары выводится из ПАРЫ — ✓ выведено
|
||
|
||
Флагнутый юнит легально приходит С ТЕКСТОМ в двух случаях: косметическая зачистка санитайзера («the
|
||
chunk is NOT lost — the cleaned text is committed as the export», `pipeline/disposition.go:96-104`) и
|
||
c-lite member-drop (`pipeline/export.go:203-207`). Поэтому состояние выводится из ПАРЫ (вердикт +
|
||
наличие финального текста): флаг+текст → `translated` с замечанием; флаг+пусто → `withheld`.
|
||
|
||
Причина флага при этом ПРОИЗВОЛЬНА («единственный легальный случай» снято ревью round-2): при c-lite
|
||
drop юнит несёт причину ПЕРВОГО выпавшего члена, какой бы она ни была. Значит карта причин обязана
|
||
иметь фразу для каждой, а не для двух. Но пара «текст + промах словаря» невозможна ни одним каналом:
|
||
`memberDrops` берёт причину из ЧЕРНОВОЙ строки члена (`status.go:242-257`), а `glossary_miss`
|
||
ставится пост-чеком только там, где отгружается финал (`waverun.go:373-381`, `:494`).
|
||
|
||
**0.3.0:** инвариант «`translated` ⇒ текст непуст, иначе пуст» перестал быть только чеком БД
|
||
(`00002_readmodel.sql:127-128`) и стал условной обязательностью в схеме плюс словами в описании
|
||
(К-11: генератор `if`/`then` игнорирует). Слова «flagged», «chunk verdict», «sanitizer» с провода
|
||
сняты — они компилировались в JSDoc генерённых типов клиента.
|
||
|
||
**Свежесть** ✓ выведено: `target` обновляется на границах работы и на стопах, а не непрерывно —
|
||
посреди прогона канала чтения не существует (эксклюзивный лок движка; санкционированное чтение —
|
||
завершённый либо остановленный прогон, `research/23` §0, §4).
|
||
|
||
### 2.8. Банк: различение ✓, слова ◆ (пере-назначены в 0.3.0)
|
||
|
||
| Поле | Словарь 0.3.0 | Что было у движка | Грунт |
|
||
|---|---|---|---|
|
||
| `status` | `proposed · in_progress · approved` | `auto · draft · approved` | `store/migrate.go:191`; только `approved` — канон |
|
||
| `kind` ◆ | `name · place · title · term · nickname` **плюс `null`** | то же | `terminology/classify.go:15` + `banknote.go:74`; пустое — `membank/memseed.go:323-326` |
|
||
| `origin` | `given · annotated · found` | `seed · ruby · mined` | пути записи, см. ниже |
|
||
| `sense` | свободный текст, пустая строка = «нет различителя» | то же | `store/migrate.go:182` |
|
||
| окно | `since_chapter`/`until_chapter`, **`null` = без границы** | целые, `0` = без границы | `store/migrate.go:189-190` |
|
||
|
||
**Почему слова пере-назначены (0.3.0, Б-0/Б-17).** `ruby` — японская фуригана, то есть
|
||
паро-специфика в общем слое; `mined` — имя стадии конвейера; `draft` — имя волны; `auto` читается
|
||
как «движок сам». Канон проекта: книжный/паровой термин в общем слое = утечка (CLAUDE.md,
|
||
гардрейлы), и шапка самой спеки объявляет «no stage names». Клиент был обязан нарисовать слово для
|
||
`ruby` в паре, где рубя не существует.
|
||
|
||
**`TermStatus` — ось СОХРАНЕНА, переименованы только значения** (эррата 16.08-г D-лога). Исходная
|
||
рекомендация Б-0 «снять с провода» опиралась на «ни один экран их не рисует» — а это подмена: экрана
|
||
подписи ещё нет (S5). Ось продуктовая и несущая — «на экране в сотни строк это главный фильтр
|
||
работы», и шов клиента её уже потребляет (`frontend/src/api/vocabulary.ts:159-163`, `termStatus` с
|
||
безопасным дефолтом `canon: false`). **`TermOrigin` тем же разбором ОСТАВЛЕН как различение**
|
||
(провенанс нужен подписывающему, чтобы понимать доверие к строке) и переименован по значениям: `seed`
|
||
→ `given` (пришло с книгой), `ruby` → `annotated` (сам текст книги сказал, как читать), `mined` →
|
||
`found` (сервис нашёл в тексте). Проекция трёх пар — работа платформы.
|
||
|
||
**Правило пустого `kind` — и его проекция** (провенанс возврата — §6в п.0). `kind` присутствует всегда и допускает `null`; `null`
|
||
значит «сервис не решил», строка при этом остаётся подписываемой, и клиенту запрещено и выбрасывать
|
||
её, и додумывать тип за движок (канон, схема `BankTerm.kind`). У движка та же строка несёт `Type:
|
||
""` (`membank/memseed.go:323-326`), поэтому **проекция `""` → `null` — работа платформы**; это
|
||
единственное место, где пустая строка и `null` означают одно, и потому оно записано.
|
||
|
||
**Фантом `auto` в провенансе убран ещё в 0.2.0:** комментарий схемы движка (`migrate.go:193`:
|
||
`seed|ruby|auto`) устарел — `"auto"` пишет СТАТУС, не провенанс (`membank/memseed.go:328`:
|
||
`Status:"auto", Source:"ruby"`), майнинг ставит `mined` (`pipeline/mining.go:424`).
|
||
|
||
**Ложный друг `source`.** У движка колонка `source` — это ПРОВЕНАНС. Поэтому в контракте провенанс
|
||
зовётся `origin`, формы термина — `src`/`dst`, а имя `source` в схеме банка не используется вовсе.
|
||
Переименование ради стиля здесь — самый дорогой класс правки, и ревью 0.3.0 его отвергло (§4 №25).
|
||
|
||
**Окно термина (0.3.0, Б-18).** `since_chapter`/`until_chapter` стоят на НОМЕРЕ главы, а номер сам
|
||
контракт называет не-ключом: нумерация плотная, правка исходника сдвигает хвост, и для книги без
|
||
нумерации номера легально нет. Перевести окно на `chapter_id` нельзя — оно входит в ключ уникальности
|
||
термина у движка (`store/migrate.go:202`), и смена ключа это работа движка, не контракта. Поэтому
|
||
записано ЯВНО: окно живёт в координатах текущего `structure_version` и пересчитывается при его
|
||
смене; два смысла нуля разведены на `null` («без границы»), потому что `Chapter.number` начинается с
|
||
единицы и `0` был сентинелом с двумя значениями в двух полях.
|
||
|
||
### 2.9. Подпись — набор решений — ✓ выведено
|
||
|
||
Конвейер заменяет банк целиком (`store/migrate.go:169-170`), поэтому `PATCH /term/{id}` молча не
|
||
работает. Механика дословно: «for EACH term either promote it into the mined-delta file OR decline it
|
||
in the mined-rejects file, then resume» (`pipeline/mining.go:81,243`; ⚠ фраза «the stop clears once
|
||
every proposed term is promoted or rejected» ОПРОКИНУТА D39.144 — движок авто-продолжает с
|
||
неподписанным, D39.42 п.3). Отсюда живое: решение на термин · информационный счётчик «решено N из M»
|
||
· частичное сохранение ◆. Запрет «продолжить» при неполном наборе — СНЯТ (подпись = один resume).
|
||
|
||
**0.3.0 — две правки:**
|
||
|
||
- **`promote` → `approve`.** `promote`/`decline` — дословно глаголы оператора майнера из строки выше,
|
||
и `promote` порождал `status: approved` — два слова на один акт. Теперь `approve` → `approved`.
|
||
- **Счётчики стопа (`pending_decisions`/`complete`) СНЕСЕНЫ 0.5.0 — разбор §2.19-бис.** Ратифицировано
|
||
D39.144 (модель владельца 16.08: подпись — ОДИН акт над всем банком): `resumeRun` снимает стоп при
|
||
ЛЮБОМ состоянии решений, 409-полноты НЕТ; честный счёт нерешённости едет квитанцией двери правок
|
||
(`signature`), а не чтением.
|
||
|
||
`POST /runs/{id}/resume` — нормативная операция, а не резерв.
|
||
|
||
⚠ **Открытый остаток (заведён батчем 0.3.0, пере-привязан 0.5.0 к живым носителям).** Чтение банка
|
||
не отвечает, КАКИЕ строки уже решены: `TermStatus` — состояние строки, решения живут файлами
|
||
движка (дельта и список отказов — `18-bank-ontology.md`, роли ИСТОЧНИКОВ), и на провод пер-строчная
|
||
решённость не проецируется ни одним полем; счётчик «сколько осталось» с 0.5.0 отвечает только
|
||
квитанция двери правок (`signature`), не чтение. Экран подписи, перезагруженный посреди стопа,
|
||
знает суммарный счёт лишь после первого своего вызова двери и не знает, какие строки решены.
|
||
Лечение — одно поле `BankTerm.decision`, и оно НЕ добавлено: слово владельца 15.08 «добавочные поля
|
||
`BankTerm` НЕ заводить» (D39.136 п.4б) прямо это запрещает; публикация решённости движком — заказ
|
||
на день, когда экран подписи закажут (`18-bank-ontology.md`, «Чего эта форма НЕ несёт»). Решение —
|
||
за владельцем.
|
||
|
||
### 2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
|
||
|
||
**✓ выведено:** правило ре-синка — идемпотентный апсерт по `(run_id, seq)`, канал согласования —
|
||
`status --json` (`research/23` §2, §8; D39.85).
|
||
|
||
**◆ предложено:** что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН. Обоснование —
|
||
гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом, а рефетч по возврату
|
||
фокуса окна у ратифицированного `@tanstack/react-query` включён по умолчанию.
|
||
|
||
**0.3.0 — два уточнения, оба выведены из построенного клиента:**
|
||
|
||
- **Ревизия страничного обхода — МИНИМУМ по страницам, не максимум.** Правило жило только
|
||
комментарием в `frontend/src/api/client.ts:99-106` («The revision of a torn list is its OLDEST
|
||
page»), то есть второй клиент его бы не узнал. Теперь оно в схеме `Revision`.
|
||
- **Агрегаты банка — на ПЕРВОЙ странице** (Б-12). `Bank` обязывал нести `total`/`signed` «в целом по
|
||
банку» на КАЖДОЙ странице, при этом у каждой страницы своя `revision`, а правила согласования не
|
||
было — и референсный клиент неизбежно смешивал два момента времени: счётчики брал с последней
|
||
страницы (`queries.ts:144-148`), ревизию — с первой. Обе половины написаны осознанно и обе верны по
|
||
отдельности. Первая страница — единственный выбор, согласованный с правилом выше, и он не требует
|
||
от сервера держать снимок между запросами.
|
||
|
||
**Кадры соединения не тратят номеров истории.** `id` кадра объявлен позицией в истории событий
|
||
КНИГИ, строго возрастающей, — но `hello` приходит первым на КАЖДОМ подключении, а `end` и
|
||
`resync_required` тоже принадлежат соединению, не книге. Разведено 0.3.0: служебные кадры несут id
|
||
последнего кадра ИСТОРИИ и своего номера не тратят, повтор id на них легален, а дыра в нумерации
|
||
легальна из-за склейки — и клиенту прямо запрещено читать пропуск как потерянный кадр. Обе
|
||
альтернативы отвергнуты собственным текстом канона: книжные номера служебным кадрам заставили бы двух
|
||
одновременных зрителей тратить номера друг друга, а повтор последнего номера сделал бы «one per frame»
|
||
ложью.
|
||
|
||
**Дельта-чтения ВКЛЮЧИТЕЛЬНЫ (`>=`), а не строго больше.** Строгое сравнение ломает собственное
|
||
правило `Revision` («catch-up reads `>= R`, not `> R`»): одна транзакция — это одна ревизия, но
|
||
НЕСКОЛЬКО строк, и `>` теряет соседей последней применённой; а кадр `bank` предписывает читать строки
|
||
«ревизией этого кадра», что при `>` всегда пустота. Повторно пришедшая строка безвредна (заменяется
|
||
по `id`), а водяной знак следующего чтения берётся из КОНВЕРТА, а не выводится из строк.
|
||
|
||
⚠ **Дофикс 16.08 (ФБ-4): у многостраничного обхода конверт не один.** Формулировка «водяной знак —
|
||
`revision` конверта» была однозначна ровно до тех пор, пока чтение умещалось в одну страницу; на
|
||
рваном чтении она сталкивалась со вторым правилом того же раздела — «ревизия рваного списка это
|
||
ревизия СТАРЕЙШЕЙ страницы». Два правила давали два разных числа, и клиент, взявший новейшее, молча
|
||
терял строки, изменившиеся между первой страницей и последней. Сведено в одно: **и гард свежести, и
|
||
водяной знак — это НАИМЕНЬШАЯ ревизия, увиденная за обход**; на одностраничном ответе это его
|
||
собственная. Направление выбора — безопасное: лишнее перечитывание бесплатно (строка заменяется по
|
||
`id`), пропуск строки — нет. Там же заявлено равенство `BookDetail.revision` и
|
||
`BookDetail.book.revision`: карточка собирается одной транзакцией с книгой, и клиент вправе брать
|
||
любое. Устаревший водяной знак
|
||
(коллекцию заменили целиком) отвечает `400` c `cause.code: version_too_old` — тем же ответом и с тем
|
||
же смыслом, что мёртвый курсор.
|
||
|
||
**Структурная версия — новая ось (0.3.0, Б-7).** Курсор был привязан к «STRUCTURAL epoch of the
|
||
collection», серверу вменялся MUST-отказ по мёртвому курсору, — а слово «epoch» встречалось в
|
||
документе РОВНО ОДИН раз: ни один ответ эпохи не нёс, наблюдать её было нечем. При этом структура
|
||
живая: «число глав может измениться против эвристики» (`research/27:35`, решение владельца 09.08).
|
||
Теперь `structure_version` едет **на КАЖДОМ кадре** (`EventBase`) и в `Book`, `ChapterPage`,
|
||
`UnitPage`, `NotePage`, `BankPage`; на исчезнувшую главу отвечает `410`; к смене версии привязаны
|
||
курсор, якоря на пары, окно термина и идентичность строки банка.
|
||
|
||
### 2.11. Разрыв потока — ◆ предложено; канал перевешен на КНИГУ в 0.3.0
|
||
|
||
При переподключении клиент шлёт `Last-Event-ID`. Если сервер докачать не может — обязан ответить
|
||
`resync_required`, а не молча начать с текущего момента: реплей истории запрещён, иначе разовое
|
||
событие вроде `note` теряется молча.
|
||
|
||
**0.3.0 — четыре правки одного канала (Б-6), все Ц0: канала нет ни строкой** (`grep
|
||
text/event-stream` по не-тестовому Go платформы — только комментарии).
|
||
|
||
- **Поток перевешен с прогона на книгу.** Книга в `uploading`/`parsing` прогона не имеет по
|
||
построению: строка прогона создаётся только в `StartRun` (`pgstore/runs.go:66,80`), весь разбор
|
||
живёт на строке книги (`books.go:180,237,283`). Прогонный поток не мог сообщить конец разбора в
|
||
принципе, и клиент лечился опросом раз в 3 с (`queries.ts:64-75`) плюс вторым хуком
|
||
(`useIntakeEnd.ts:31`) — а разбор настоящей книги это минуты. Это Ф-56, и чинится он не новым
|
||
кадром, а перевеской канала: конец разбора становится обычной сменой статуса.
|
||
⚠ **Прогонный поток узким видом НЕ оставлен** (Б-6 предлагал оставить). Довод: второй канал с теми
|
||
же кадрами — вторая реализация и второй источник расхождения, а адресация «кадры этого прогона»
|
||
выводится из книжного потока клиентом, у которого id прогона уже есть. §5а того же ревью требует
|
||
резать, а не добавлять поверхность.
|
||
- **Конец потока объявлен.** Терминального кадра не было, `204` в ответах не объявлен — при том что
|
||
SSE именно им останавливает переподключение («a client can be told to stop reconnecting using the
|
||
HTTP 204 No Content response code», WHATWG). После завершённого прогона браузер переподключался бы
|
||
вечно. Теперь: кадр `end` + `204` на переподключение с `Last-Event-ID` от завершённого потока;
|
||
запрос БЕЗ `Last-Event-ID` всегда открывает новый поток — иначе клиент не смог бы начать смотреть
|
||
снова после старта прогона.
|
||
- **`id` кадра и ревизия книги разведены.** Спека просила «a monotonic `id`», говорила, что он несёт
|
||
ревизию, и допускала несколько кадров с одним 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`): контракт требовал его только для ответов с переводом, то есть был беднее кода.
|
||
(б) ⚠ **испр. 05.09: перекрыто минором 0.10.0 (§2.23) — ссылка теперь ОТНОСИТЕЛЬНАЯ (`uri-reference`) и ведёт на `downloadExport`.** Ниже — редакция 0.3.0. Ссылка экспорта получила НОРМУ доступа вместо прозы: минтится под этот ответ и под
|
||
аутентифицированного владельца, не индексируется, истекает в `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 → `409` `bank_corrections_refused` (+`refusals[]`),
|
||
15 → `503` `bank_corrections_incomplete` («слать тот же документ»), 12 → `409` `run_in_flight`,
|
||
10/11/13 → не пер-операционные (деплой сломан — `5xx` общего вида); **16 `book_incomplete`**
|
||
(движковый `tmctl build` без `--partial`, D39.175) на ЭТОМ проводе не встречается — его раскладка
|
||
появится с дверью выдачи `createExport`; движковый исход `stopped`
|
||
(SIGTERM до первого байта: ничего не записано) пер-операционного кода тоже не имеет — снаружи
|
||
это рестарт деплоя, `5xx`, ретрай сходится; потолок 1 МиБ → `413`, потолок 5000 и вся валидация
|
||
формы → `400` `invalid_request`. 14 и 15 получили СВОИ корневые коды: разные адресаты ремеди
|
||
(пере-решить человек / повторить машина), совет промта принят.
|
||
⚠ **Три обязательства монтажа — ИСПОЛНЕНЫ пакетом P9 (D39.162); список сохранён, потому что он
|
||
нормативен для всякого, кто эту дверь пере-монтирует:** (а) оба потолка движок классифицирует
|
||
КЛАССОМ 14 («the document is well-formed and the deployment is fine», `bankdecisions.go:656-673`),
|
||
а канон раскладывает их в `400`/`413` — платформа обязана мерить оба на ПРОВОДНОЙ форме до
|
||
спавна, И на той же форме, какую отдаёт движку (рендер с HTML-экранированием раздувает `&`/`<`/`>`
|
||
вшестеро, и законное тело под проводным потолком перепрыгивало движковый); остаточный случай
|
||
(HTTP-тело < 1 МиБ, отрендеренный документ шва — больше) падает в `409` `bank_corrections_refused`,
|
||
чьё описание несёт «a set larger than the service applies in
|
||
one act»; (б) канонный `400` с `errors[/book_id]` на несовпадение тела с путём производит
|
||
ПЛАТФОРМА до спавна — движковая сверка того же факта (`bankdecisions.go:267-269`) есть класс 10,
|
||
«конфиг вызывающего», и в `400` сама не раскладывается; (в) свои вызовы двери по одной книге
|
||
монтаж СЕРИАЛИЗУЕТ сам: флок движка держит любой глагол, включая второй `bank-apply` и превью
|
||
(класс 12 = «another tmctl owns this project», не «книга переводится»), и без сериализации
|
||
транзиентный держатель отвечал бы `run_in_flight` — словом про прогон, которого нет. С
|
||
сериализацией снаружи остаётся ровно живой прогон, и слово точное.
|
||
- **Причины пер-решенческих отказов едут `refusals[].detail` developer-facing и НЕ показываются**
|
||
(как `Problem.detail`): у движка причины — свободный текст (`RejectedDecision.Reason`), машинного
|
||
словаря причин нет, а замораживать в каноне пересказ — вторая копия растущего словаря. Названная
|
||
щель: продуктовые фразы отказов появятся, когда движок даст причинам машинные имена.
|
||
- **Что НЕ пошло на провод из отчёта:** пути файлов (`files`) и пофайловая правда записи
|
||
(`written_delta`/`written_rejects`) — серверная топология, ремеди клиента от неё не зависит ·
|
||
`canonical_rewrite*` — предупреждение оператору файлов, а не пользователю продукта ·
|
||
тексты `preexisting_problems` — свободный текст движка; на провод идёт СЧЁТ
|
||
(`preexisting_faults`), потому что «книга уже больна, следующий прогон умрёт у банка»
|
||
пользователю нужен, а формулировки — нет · `replaced[]` — свободный текст; на провод идёт булев
|
||
`displaced` (перекрыл ли ты чьё-то раннее слово — свойство, ради видимости которого поле и
|
||
существует) · `mode` — раскладывается на `preview`/`changed`/HTTP-коды; остаток `stopped`
|
||
разобран в пункте раскладки выше · `decisions_version` / `report_version` — версии ДОКУМЕНТОВ
|
||
ШВА, на проводе их место занимает `contract_version`. ⚠ Отдельно названное УСЕЧЕНИЕ: на отказе
|
||
(`409`) движок печатает ПОЛНЫЙ отчёт (с `preexisting` и `signature` — «its report of reasons IS
|
||
its product»), а провод несёт только `refusals[]`: конверт `Problem` расширяется членами про
|
||
отказ, не квитанцией. Книга, каждый документ которой отказан, своё «уже больна» через дверь не
|
||
покажет — названная цена v1, не забывчивость.
|
||
- **`depth: edit_wave → refinement`:** движковая константа несёт имя волны — на провод идёт
|
||
продуктовое слово с тем же смыслом, открытым словарём; обе половины смысла (следующий прогон;
|
||
черновик не пере-формируется) продублированы словами в описании — это первое, о чём экран соврал
|
||
бы.
|
||
- **`signature` опубликован ВМЕСТЕ с запретительным контрактом** (обе половины D39.144): без запрета
|
||
экран подписи при разморозке выучил бы из канона «доведи число до нуля» — отменённую модель через
|
||
чёрный ход; без положительной половины поле выглядело бы бесполезным и его бы не строили. `map`
|
||
(путь карты) на провод не идёт; «карты ещё нет» выражено `signature: null`.
|
||
- **Признак «не построено» — `Capabilities.bank_corrections_enabled`** (прецеденты:
|
||
`intake_enabled` — булев с объявленным `404`, `export_formats` — «пусто = не построено»).
|
||
`Capabilities` годится: один плоский документ деплоя, одинаковый для всех аккаунтов, читается до
|
||
предложения UI. Без признака минор воспроизвёл бы PD-370 тем же коммитом, которым закрывает.
|
||
- **Квитанция — НЕ чтение:** ни `revision`, ни `structure_version` не едут — вызов не двигает
|
||
read-модель (вид банка пересобирается на границе прогона, `18-bank-ontology.md`), кадр `bank` не
|
||
испускается, и канон говорит это прямо, чтобы «исправил, а банк не изменился» читалось как
|
||
корректность, а не как баг.
|
||
|
||
### 2.19-бис. Счётчики `pending_decisions`/`complete` — СНЕСЕНЫ (0.5.0)
|
||
|
||
Оба поля кормились платформенной таблицей `bank_decisions`, чей write-путь снесён 22.08 вместе с
|
||
пер-термной моделью: новая дверь пишет файлы движка, не эту таблицу. ⚠ **«Поля навсегда нули»
|
||
(буква промта пака) опровергнута кодом при вычитке опровергателем — на деле ХУЖЕ нулей:**
|
||
`pending_decisions` считается как «proposed-строки, которых не коснулось ни одно решение», а
|
||
касаться нечем — то есть это число ВСЕХ proposed-строк банка, живое на каждой пересборке, и
|
||
`complete` вырождается в «предложений нет вовсе» (`platform/internal/pgstore/readmodel.go`,
|
||
`bankCountsTx` — комментарий признаёт это прямо). Замороженно-правдоподобное число учит
|
||
отменённой модели живым счётчиком. Из трёх исходов (снести · пере-определить на новую дверь ·
|
||
пометить) выбран СНОС со всех трёх носителей (`BankPage`, `EventBank`, квитанция — вместе со
|
||
схемой):
|
||
|
||
- **пере-определить нельзя**: честный счёт нерешённости — движковый (`SignatureState`: карта
|
||
последнего стопа против файлов решений); read-модель платформы его НЕ вычислит, не пере-реализовав
|
||
движковый закон у себя, что запрещено (17-seam-inbound-law п.6; `18-bank-ontology.md`, «Чего эта
|
||
форма НЕ несёт»);
|
||
- **пометить («пока нули») нельзя**: поле, обязательное в схеме и вечно лгущее нулём, — это ровно
|
||
класс PD-370 («канон объявляет — деплой не обслуживает»), только в поле вместо пути;
|
||
- ратифицированная проза «Signing the bank is ONE act over the whole of it» при сносе СОХРАНЕНА в
|
||
описании `listBankTerms`; фраза «marked unverified inside the service» из того же абзаца снята —
|
||
это обещание без носителя, снятое ещё ФБ-8 (§6б) и уцелевшее в одном месте.
|
||
|
||
Цена, названная честно — ТРЕМЯ половинами, и две из трёх ПОГАШЕНЫ лендингом P9 (D39.162):
|
||
(1) чтение банка больше не отвечает «сколько осталось» — экран узнаёт счёт из квитанции двери правок
|
||
(`signature`), то есть только имея что послать или что превьюировать: **эта половина В СИЛЕ**;
|
||
(2) ~~до монтажа пака (2в) счёт недоступен НИГДЕ~~ — окно между минорами закрыто, дверь смонтирована
|
||
и отвечает; (3) ~~проекция `GET /bank` деплоя ещё шлёт снятые поля~~ — сняты, носитель `PD-399`
|
||
закрыт. Возврат счёта в чтение — день, когда экран подписи закажут
|
||
и движок опубликует нерешённость проекцией (`18-bank-ontology.md`, «Чего эта форма НЕ несёт»);
|
||
сегодняшние носители лгать не будут.
|
||
|
||
---
|
||
|
||
### 2.20. Сквозная полоса прогресса и подпись стадии (0.6.0) — ✓ выведено из построенного сервера
|
||
|
||
**Что изменилось.** `Progress` перестал быть счётчиком СЕГМЕНТА между двумя стопами и стал ОДНОЙ
|
||
монотонной долей на всю работу прогона через все его проходы. Оговорка «при снятии стопа счётчик
|
||
начинается заново с нуля» снесена; добавлен обязательный член `stage`.
|
||
|
||
**Почему минор приехал ПОСЛЕ сервера, а не до него.** Требование владельца (строка 200) — одна доля
|
||
на всю работу прогона, считает СЕРВЕР (иначе клиент снова начнёт знать про фазы). D39.160 изъял его
|
||
из минора 0.5.0 ровно потому, что гейт версии требует совпадения канона и деплоя В МОМЕНТ лендинга:
|
||
объявить сквозную полосу раньше, чем сервер её считает, значило бы завести класс `PD-370` в поле
|
||
вместо пути. Канон догнал сервер тем же коммитом, которым сервер приехал.
|
||
|
||
**Провенанс формы — код, а не рассуждение** (`platform/internal/pgstore/readmodel.go`): числитель
|
||
складывает вклад каждого прохода (`runDone`), знаменатель — работу, которую прогон РЕАЛЬНО должен
|
||
(`runTotal`), и каждая половина мерится от СВОЕЙ базовой линии прогона (`chapters_before` для
|
||
последнего прохода, `draft_before` для чернового), после чего каппится покупкой. Отсюда обе новые
|
||
фразы канона — «счётчики только растут и ничто под ними не движется» и «прогон, кончившийся рано, до
|
||
`total` не доходит»: первая описывает базовые линии, вторая — то, что доля до единицы обязана
|
||
доходить лишь у ЗАВЕРШЁННОЙ покупки.
|
||
|
||
⚠ **`stage` — ВТОРОЕ исключение из границы «ничего о том, КАК переводится книга».** Первое — банк:
|
||
стоп, который снимает пользователь, спрятать нельзя. Второе завёл этот минор, потому что бар,
|
||
который движется молча, отвечает хуже, чем бар с подписью. Исключение ОГРАНИЧЕНО двумя условиями, и
|
||
оба выполняются кодом уже сейчас: значение ВЫВОДИТСЯ платформой из тех же счётчиков, что и бар
|
||
(`runStage` — SQL над теми же колонками, а не проброс движковой строки), и словарь ОТКРЫТ — клиент
|
||
обязан рисовать незнакомое значение нейтрально. Следствие, ради которого условия и поставлены:
|
||
**пара или конвейер с ИНОЙ формой работы не требуют нового клиента и не поднимают версию.**
|
||
Сегодняшние значения — `drafting`, `editing` и `re_pass` (⚠ испр. 02.09: третье значение выдаёт `runStage` для всего пере-прохода — `platform/internal/pgstore/readmodel.go`, греп `re_pass` — и оно доезжает на провод).
|
||
|
||
⚠ **Ложная перекрёстная ссылка, снятая этим же минором.** `ChapterProgress.units_done` обещал «то же
|
||
счетоводство, что у `Progress`, уровнем ниже». После сквозной полосы это неправда: глава по-прежнему
|
||
обнуляется каждым новым проходом, прогон — больше никогда. Оставленная, фраза научила бы клиента
|
||
зеркалить одно в другое и рисовать прыгающий бар.
|
||
|
||
---
|
||
|
||
### 2.21. Покупка пере-прохода (0.7.0) — ✓ выведено из построенного механизма движка
|
||
|
||
**Что появилось.** `RunRequest.re_pass` — булев член; `ceiling_chapters` вышел из `required` и
|
||
объявлен взаимоисключающим с ним. Плюс объявленная форма полосы такого прогона и ограничение фразы
|
||
«finished work is not bought twice».
|
||
|
||
**Зачем.** Главный пользовательский цикл продукта — «поправил термин → правка доехала в уже
|
||
переведённый текст» — до 0.7.0 был НЕВЫРАЗИМ на проводе: у дочитанной книги остаток нулевой, шкала
|
||
даёт максимум 0, валидного `ceiling_chapters` не существует физически. Канон при этом обещал, что
|
||
правка «takes effect on the NEXT run», а следующего прогона купить было нечем — **две фразы канона
|
||
складывались в дедлок**, и 0.7.0 его снимает.
|
||
|
||
**Провенанс — движок, а не проектирование.** Механизм построен и ратифицирован задолго до этого
|
||
минора: `translate --resnapshot` определяет затронутые правкой юниты байт-сверкой отрендеренного
|
||
запроса, незатронутые пере-привязывает за $0, а согласие на пере-плату берётся флагом
|
||
`--accept-rebill`. Минор не изобретает механику — он открывает к ней дверь. ⚠ Проектирование самого
|
||
продуктового ЦИКЛА пост-ридинга (отдельная ручка «перегенерировать», dry-run, доезд правки до
|
||
черновика) остаётся гейченным полигоном; область D39.144 сужена явно D39.165.
|
||
|
||
⚠ **Чего минор НЕ объявляет, и это названная цена, а не забывчивость.** Смету «затронуто N юнитов»
|
||
ДО покупки провод не несёт. Причина установлена исполнением и стоила пересборки пака: `bank-apply`
|
||
пишет только ФАЙЛЫ решений, а движковый `status` считает ре-билл от СОХРАНЁННОГО глоссария
|
||
(`backend/internal/pipeline/status.go` (греп `orphanStageRows`) — его собственный комментарий предупреждает, что
|
||
правка файла, ещё не прогнанная, здесь не отражается). Свёртка происходит внутри СЛЕДУЮЩЕГО
|
||
`translate`, поэтому сразу после правки движок честно отвечает «ничего не двигалось». Смета вернётся
|
||
на провод вместе с движковым глаголом «свернуть банк и оценить ВНЕ прогона» — до тех пор согласие
|
||
даётся ДЕНЬГАМИ: потолок пере-прохода равен холду, который пользователь уже внёс.
|
||
|
||
⚠ **Форма полосы объявлена, а не выведена, и причина названа в самом каноне.** `total` = 1, `done` =
|
||
0 → 1 на чистом финише. Первое решение оркестратора («полоса в главах») ОТМЕНЕНО эрратой 28.08-к:
|
||
движок анонсирует работу ОДИН РАЗ за жизнь книги, поэтому пере-проход, который переделывает уже
|
||
анонсированное, новых анонсов не производит — глава-полоса стояла бы на нуле навсегда. Более дробная
|
||
полоса была бы полосой, которая не движется.
|
||
|
||
---
|
||
|
||
|
||
### 2.22. Вторая граница пересчёта и `shape_epoch` (0.9.0) — ✓ выведено из построенного сервера
|
||
|
||
**Что.** У `chapters_done` было ОДНО событие, легитимно пересчитывающее счёт, — пере-нарезка книги, и
|
||
она называла себя `structure_version`. Событий оказалось ДВА: счёт пересчитывается и когда деплой
|
||
меняет, что для главы значит «готово». Второе событие получило свою координату — `Book.shape_epoch`,
|
||
целое, растущее.
|
||
|
||
**Почему координата, а не молчание.** Без неё клиент видит скачок `chapters_done` в обе стороны и не
|
||
может отличить легитимный пересчёт от ошибки сервера: «никогда не движется назад» переставало быть
|
||
правдой, а замены ему не было. Инвариант переписан точно: счёт не движется назад **внутри одной пары**
|
||
(`structure_version`, `shape_epoch`); через любую из двух границ — пересчитывается, и клиент
|
||
пере-читает, а не считает это ошибкой.
|
||
|
||
**Почему это НЕ третье исключение границы (D39.163).** Поле НЕПРОЗРАЧНО по построению: целое, которое
|
||
сравнивают с предыдущим, и оно сообщает только ЧТО поколение сменилось. Оно не отвечает на вопрос
|
||
«как переводится книга» — ни имени фазы, ни стадии, ни модели. Ратификация №20 (31.08) стоит именно на
|
||
этом различении, и он честно записал его как СВОЁ суждение, а не как факт.
|
||
|
||
⚠ **Норма, которую приёмка этого минора вывела и которая держится на следующий:** непрозрачность
|
||
ПОЛЯ не спасает, если проза рядом прозрачна — граница судит и описания, потому что они
|
||
компилируются в исходник клиента (D39.180 п.б).
|
||
|
||
**Механика — в зоне платформы:** пара колонок `books.shape_epoch` + `books.epoch_editor` (миграция
|
||
`00030_shape_epoch.sql`, бэкфилл `epoch_editor = edit_wave`, поэтому первая граница — только реальная
|
||
смена формы). ⚠ **Полоса ПРОГОНА на эпоху НЕ переведена:** зона попробовала и откатила своим же
|
||
адверсариальным проходом — эпоха присваивается и ходит в обе стороны, отчего полоса переставала быть
|
||
монотонной (замерено: один прогон читал 4/4, потом 2/2 на своих же двух попытках при неизменной
|
||
`structure_version`). Через эпоху считается ТОЛЬКО пожизненный счёт книги; остаток — открытая строка
|
||
регистра платформы `PD-435`.
|
||
|
||
### 2.23. Дверь выдачи, описанная построенным (0.10.0) — ✓ выведено из работающей двери
|
||
|
||
**Что.** Первый минор, целиком собранный из расхождений «канон обещает — код делает», найденных
|
||
живым ПЛАТНЫМ прогоном и адверсариальным проходом зоны. Четыре позиции, каждая правит КАНОН, а не
|
||
код: код здесь оказался прав.
|
||
|
||
**`failure_code` — enum из четырёх.** Поле само поставило себе условие («becomes an enum with the
|
||
first built format»), и условие наступило. Набор — то, что деплой ПРОИЗВОДИТ, а не то, что можно
|
||
вообразить: `book_empty` · `deployment_error` · `build_interrupted` · `build_failed`
|
||
(`platform/internal/exports/exports.go`, греп `the Export.failure_code vocabulary`). ⚠ `book_empty`
|
||
намеренно ОДИН код на две ситуации — книга нарезана и пуста, и книга, которую ещё никто не нарезал:
|
||
факт читателя один («в этой книге нечего отдавать») и лечение одно. ⚠ `deployment_error` не назван по
|
||
формату: тот же класс покрывает книгу без языкового тега, нечитаемый langpack и недоступный каталог,
|
||
и фраза «формат недоступен» была бы ложной для большинства причин — это находка адверсариального
|
||
прохода, а не переименование по вкусу.
|
||
|
||
**`Export.url` — `uri-reference`, и честный механизм.** Отдаётся ОТНОСИТЕЛЬНЫЙ путь; абсолютный
|
||
заставил бы сервис знать свой публичный ориджин, которого за edge-прокси он не знает. Прежняя
|
||
формулировка «minted for THIS response» читалась как подписанная капабилити — механизма такого никто
|
||
не строил, ссылка АУТЕНТИФИЦИРОВАННАЯ и защищена сессией. Это строго сильнее: утёкшая ссылка чужому
|
||
бесполезна, и деплойного секрета, который никто не ротирует, она не требует. ⚠ **Вхождение ОДНО.**
|
||
Оркестратор №22 при ратификации объявил их два, сверив грепом `format: uri` и не прочитав второе
|
||
место: там поле `type` проблемы RFC 9457 со значением `about:blank`, абсолютный URI по стандарту, и
|
||
трогать его нельзя. Ошибка исправлена ДО правки канона; класс — тот же, что в D39.193 (ратификация,
|
||
выданная без чтения предмета).
|
||
|
||
**Третий адрес двери получает `operationId`.** `…/exports/{exportId}/content` жил в каноне только
|
||
прозой, и генерируемый клиент про него не знал — при том что браузер по нему НАВИГИРУЕТ, и это
|
||
единственный способ существования поверхности. Теперь `downloadExport`, с объявленными `206` на Range
|
||
и `410` на протухшую ссылку.
|
||
|
||
**Шестнадцатая причина замечания.** `off_target_lang` → `wrong_language`; разбор и ⚠ о ранге — в
|
||
Приложении А.
|
||
|
||
### 2.24. ⛔ ЗАПРЕТ «ДЕНЕГ В ИНТЕРФЕЙСЕ НЕТ» ОТОЗВАН ВЛАДЕЛЬЦЕМ (05.09, прозой — не минор)
|
||
|
||
**Что случилось.** Норма «оплат в MVP нет, денежных полей в UI нет вовсе» месяц определяла форму
|
||
продукта. Владелец 05.09: **«я такого правила не ставил, надо от него избавляться»**. Форма —
|
||
**отзыв собственного решения**: текст записан в теле **D39.84** (адресовать НОМЕРОМ: греп `^## D39.84`; ⚠ испр. 05.09 — здесь стояла строка `:222`, которая за сутки уехала на `:225` от правок шапки того же файла) в перечне
|
||
«(1) Восемь решений владельца». ⚠ Нота пришла РЕЛЕЕМ отчёта фронт-сессии, и что в ней прямая речь, а
|
||
что пересказ релея, по тексту не различимо — та же неразличимость, которую D39.112 п.3 пометил у
|
||
соседней ноты.
|
||
|
||
⚠ **Первая редакция этого раздела утверждала «текста правила в журнале НЕТ» — ложное отрицание,
|
||
выведенное из СЛОМАННОЙ команды** (`grep` с `|` без `-E`: в basic-regex черта — литерал, находилась
|
||
только собственная цитата паттерна). Поймал внешний рецензент, пере-запустив ту же команду. Норма,
|
||
купленная этим: **команда, на которой стоит отрицание, приводится в тексте и обязана быть
|
||
воспроизведена другим исполнителем.**
|
||
|
||
**Что теперь верно.** Баланс аккаунта — деньги и показывается деньгами; потолок заказа и холд — тоже.
|
||
Остаётся запрещённым и НЕ отменялось: цена модели, стоимость стадии, стоимость вызова, структура
|
||
НАШИХ расходов (ПТ-33). Линия: **деньги, которыми владеет аккаунт, — его; деньги, которые тратим мы,
|
||
— наши.**
|
||
|
||
**Форма показа — пара, а не точка.** Проекция имеет дисперсию (редакторские вызовы 25–90 % от
|
||
worst-case, хвост ретраев 1.5–23 % между прогонами, D39.165 §1), поэтому точечное «$1.14» через прогон
|
||
превращается в «вы сказали 1.14, списали 1.31». Верная форма: «ожидаемо ≈ $1.1, зарезервируем до
|
||
$1.6, спишем по факту», и закрытие после прогона фактом. Главный ответ — фраза «хватит на всю книгу»;
|
||
цифра рядом и мельче.
|
||
|
||
**Почему прозой, а не минором.** Схема `Credit` НЕ тронута: поле суммы приезжает с паком, который
|
||
строит форму заказа. Двигать канон впереди кода — дефект, за который в тот же день написана эррата
|
||
04.09-в. Проза перестаёт лгать сегодня, поле появляется со своим кодом.
|
||
|
||
**Носители запрета, которые обязан закрыть тот же пак** *(⚠ список пере-снят 05.09 — первая редакция
|
||
была НЕПОЛНА и вдобавок называла несуществующую секцию `§Credit`; полный список ведётся эрратой 05.09-а
|
||
в шапке журнала решений, здесь — его копия)***:** тело **D39.84** · **D39.100** К-8 («запрет в силе и
|
||
НЕ superseded») · `product-requirements.md` ПТ-35 · `15-money-path.md` §1 · §4 · таблица §6 ·
|
||
`openapi.yaml` **§Boundaries** (не §Transport — испр. 05.09) · схема **`Usage`** · **`CeilingBounds`** · **`docs/glossary.md`** ·
|
||
**`13-tech-debt-anchors.md` §Б-126** · зеркало фронта · `API_CONTRACT_INPUT.md` §4.8 ·
|
||
`platform/docs/ENGINEERING_STANDARDS.md` и **`platform/README.md`** (зонные — чинит зона).
|
||
⚠ **Прозаические указатели на отзыв проставлены 05.09 везде, где носитель мой**; ПОЛЯ приезжают паком.
|
||
|
||
## 3. Зависимости: чтение → источник → строка бэклога
|
||
|
||
**Правило, введённое 0.3.0 (Б-21): предупреждение о недостроенном ОБЯЗАНО нести номер строки
|
||
бэклога** (а где строки нет — явного носителя-документ). Без него абзац переживает своё основание и
|
||
начинает лгать — что уже произошло четырежды, и одно из четырёх отняло у пользователя работающее
|
||
действие. Таблица ниже — единственное место, где это ведётся.
|
||
|
||
| Чтение / механизм | Источник данных | Состояние | Строка / носитель |
|
||
|---|---|---|---|
|
||
| `GET /books`, `GET /books/{id}` | read-модель платформы | ПОСТРОЕНО | — |
|
||
| `GET /books/{id}/run-options`, `POST /runs` | шкала + холды | ПОСТРОЕНО | — |
|
||
| `POST /runs/{id}/stop` | реконсилятор | ПОСТРОЕНО (кнопки на экране нет) | зона фронта |
|
||
| `POST /runs/{id}/resume` | реконсилятор | ПОСТРОЕНО, но **0.3.0 сменил поведение на паузе по потолку, а 0.4.0 — на ИСЧЕРПАННОМ прогоне**. Пауза: платформа уже отвечает `409` `ceiling_reached` (`runs/reconcile.go`, ветка `case "paused"` в `Resume`) — совпало. Исчерпанность: ⚠ **РАСХОЖДЕНИЕ ЗАКРЫТО (испр. 02.09)** — `Resume` больше не отвечает `202` на исчерпанный прогон. Реконсилятор различает ДВА вердикта, и оба доезжают до клиента кодом 409 `run_not_resumable`: `ceilingSpent` → `cause.code: ceiling_reached` и `creditUnavailable` → `cause.code: credit_unavailable` (`platform/internal/runs/reconcile.go`, греп `ceilingSpent`; `platform/internal/httpapi/v0.go`, греп `run_not_resumable`). Почему молчаливый `202` был дефектом — §6в A. Полная таблица по статусам — в описании `resumeRun` | **вход P7** ⚠ ИЗРАСХОДОВАН (D39.153) (правка построенного пути, не только читающей поверхности) |
|
||
| `GET /usage` | кредиты | ПОСТРОЕНО, **не читается ни одним экраном** | зона фронта |
|
||
| `GET /capabilities` | конфигурация деплоя | **ПОСТРОЕНО P7** — маршрут смонтирован безусловно (`platform/internal/httpapi/v0.go`, таблица `contractSurface`), хендлер `capabilities.go`; версия контракта запинена ГЕЙТОМ против ЭТОГО канона (`platform/internal/gates/contract_test.go`) | закрыто D39.153 |
|
||
| `DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) — роутер монтирует **19 операций из 21** ⚠ (испр. 05.09: `PATCH /books/{id}` СМОНТИРОВАН платформенным паком `7e2226a`, акт D39.201; счёт 18→19) (испр. 05.09: дверь выдачи построена `adf5e53`, прежнее «15 из 20» протухло) | ⚠ носитель «вход P7» ИЗРАСХОДОВАН (пак P7 принят D39.153); живой носитель — `platform/BACKLOG.md` П-17 и очередь CURRENT-STATE |
|
||
| `GET /books/{id}/chapters`, `/units` | материализация манифеста | **ПОСТРОЕНО P7** — `httpapi/reading.go`, материализатор `internal/readmodel` | закрыто D39.153 |
|
||
| `GET /books/{id}/notes` | `unit_done` несёт флаг и причину (`runevents.go:126-135`), платформа хранит (`sink.go:227-233`), колонка `notes.reason` заведена под это | канал ЕСТЬ; не хватает карты «причина → код → фраза» (приложение А) и проекции | приложение А + ⚠ носитель «вход P7» ИЗРАСХОДОВАН (D39.153) |
|
||
| `GET /books/{id}/bank` | движок пишет сайдкар всего банка (`pipeline/bankexport.go:16-33,72`, D39.122) | ⚠ пере-снято 0.5.0: проекция платформы ПОСТРОЕНА (P7 — маршрут в `contractSurface`, `wireBankPage` в `httpapi/reading.go`, `SaveBank` в `pgstore`); форма синхронна канону с лендингом P9: счётчики `pending_decisions`/`complete` сняты с проекции (`PD-399`) | закрыто D39.162 |
|
||
| ~~`POST /bank/decisions`~~ | стоп-механика майнера | ⚠ **НЕ «не построено», а ОТМЕНЕНО**: было построено P7 и СНЯТО 22.08 вместе с пер-термной моделью подписи (D39.144, слово владельца). **0.5.0 снёс и канон-половину — `PD-370` закрыт этим минором**; преемник — строка `POST …/bank/corrections` ниже | отменено D39.144; снесено 0.5.0 |
|
||
| `POST /books/{bookId}/bank/corrections` | `tmctl bank-apply` — движковая половина ПОСТРОЕНА (D39.158, лендинг `d1eb8a9`) | **ПОСТРОЕНО пакетом P9**: перевод словаря (`platform/internal/ingest/bankdecisions.go`), спавн глагола (`runner/bankapply.go`), раскладка отказов и пер-книжная сериализация (`runs/bank.go`), дверь синхронная. Флаг `bank_corrections_enabled` следует включённости прогонов (`cmd/tmplatformd/runner.go:195`) — дверь спавнит тот же глагол | закрыто D39.162 |
|
||
| `GET /books/{id}/events` (SSE) | эмиттер шва построен (D39.131) | **ПОСТРОЕНО P7** — `httpapi/stream.go`, поток регистрируется ВНЕ слоя сжатия (сжатие буферизует поток — единственное, что канон запрещает этому маршруту) | закрыто D39.153 |
|
||
| `POST`/`GET /exports` | движок: `tmctl build [--format epub,txt] [--out path] [--partial]` — EPUB 3 + чистый txt (`backend/internal/bookfile`), конверт `tm-build-v1`, пути в `StatusArtifacts.book_files`, отказ exit 16 `book_incomplete` | ⚠ **ПЕРЕ-СНЯТО 05.09: ОБЕ ПОЛОВИНЫ ПОСТРОЕНЫ.** Движковая — D39.175 (`8adcb86`); ПЛАТФОРМЕННАЯ дверь заленджена `adf5e53` (D39.194) и предъявлена живьём платным EPUB через API. Прежнее «открыта только дверь платформы» протухло на сутки. Дверь СТРОИТ через `tmctl build` (не читать файл у БД: там копия прежней сборки) и сверять `BuildReport` (`config_drift`/`stale_unknown`). ⚠ Это про ФАЙЛ КНИГИ; редакторский `tmctl export`/annot-v1 (D29.1а) — ОТДЕЛЬНАЯ, не закрытая работа | движковая половина — D39.175; дверь — пинг зоне платформы (её журнал, греп `Движок отдаёт книгу файлом`); annot-v1 — строка 49 / D29.1 |
|
||
| Условные чтения (`ETag`/304), сжатие | — | **ПОСТРОЕНО P7** — `httpapi/conditional.go`; валидатор считается от БАЙТ ответа. Остаток строки 186 — шаги 3–5 (скоуп кадра, дельта-чтение, `staleTime`) | шаги 1–2 закрыты D39.153 |
|
||
| `Idempotency-Key` | — | **ПОСТРОЕНО P7** — `httpapi/idempotency.go` + `pgstore/idempotency.go`, на ТРЁХ создающих вызовах (третий — дверь выдачи, `httpapi/exports.go`; было два до 04.09); ⚠ живой дефект под конкуренцией — `PD-369` | закрыто D39.153 |
|
||
| `bearerToken` — чем ВЫДАЁТСЯ токен | вход ставит HttpOnly-куку (`login.go:338`); токен минтится ВНЕ ПОЛОСЫ оператором | ✅ **ЗАКРЫТО 05.09 платформенным паком:** `tmplatformctl token issue --user … --client …`, сессия обычная — видна в журнале входов под операторским провайдером и гасится `POST /auth/logout`. ⚠ Эндпоинта для выдачи НЕТ и по умолчанию не будет: дверь, выдающая себе учётные данные по HTTP, — другая модель угроз и ратифицируется отдельно. Прежняя редакция («выдать его нечем») стала ложной в момент лендинга и сутки противоречила самому канону (`openapi.yaml`, греп `minted OUT OF BAND`) | носитель: research/28 §2 (Б-15) + **строка бэклога 270** (⚠ испр. 05.09: прежнее «строки нет» ложно); развилка «выдать либо снять обещание из канона» заказана платформенным паком 05.09 |
|
||
| `Problem.localized` | — | объявлено, ни одним кодом не используется | носитель: research/28 §8 п.4 |
|
||
| `Note.code` как enum спеки | карта приложения А | не enum, пока не написаны фразы | ⚠ **прежний носитель «строка 148» МЁРТВ** (`a023e39`, 21.08; разбор — приложение А, п.3); живой носитель — строки бэклога **203 + 204** (словарь фраз) |
|
||
| Настоящие названия глав (`Chapter.heading` ≠ null) | парсер структуры | НЕ ПОСТРОЕНО | строка 160 (Этап 0) |
|
||
| `title_raw` / `kind` (глава ↔ фрагмент) | дизайн-пак структуры глав | передано паку, аддитивно | строка 161 |
|
||
| `ErrorCode.content_refused` (400) **и** `RejectReason.content_refused` | прескрин злоупотреблений | НЕ ПОСТРОЕН ни на одной стороне: в платформе только объявление константы (`httpapi/problem.go:42,94`), `ContractRejectReason` (`ingest/vocabulary.go:59-69`) его не отображает; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ (`disposition.go:60-63` → `Note.code: content_withheld`) и в exit-контракт не выходит — мостá между двумя словарями нет | **строка 94 (ПТ-16)**; там же ограничение числа попыток аккаунта — обязанность падает ВМЕСТЕ с производителем, не раньше |
|
||
| `decline` в подписи банка доезжает до работы | ⚠ пере-снято 0.5.0: старый носитель («таблица `bank_decisions`, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на `BankDecision.action` снесено вместе со схемой | дверь смонтирована (D39.162), и decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ **Живой остаток — ДВИЖКОВЫЙ, один:** `decline` не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, `mining.go:548`), тогда как канон обещает «declining a surface removes EVERY window of that surface» | строка 228 единого бэклога; сходимость повтора закрыта D39.164 §3.3 |
|
||
| Снятие замечания (переход «флаг снят») | движок | **НЕДОСТИЖИМО сегодня, проверено чтением движка** — п. H §6в | **PD-298** регистра платформы (`platform/docs/DEFECT_REGISTER.md`) — там строка и живёт; механизма не строим |
|
||
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | **вход P7** ИЗРАСХОДОВАН (D39.153) (PD-202) |
|
||
|
||
⚠ Три из четырёх протухших предупреждений (Б-7а) сняты молча — они лишь устарели; живой хвост
|
||
одного: зона фронта до сих пор учится по старой версии «`GET /bank` — канала нет вообще» (**Ф-43**).
|
||
|
||
⚠ **Четвёртое было ХУЖЕ устаревшего — оно было НОРМАТИВНЫМ и отнимало работающее лечение.** Спека
|
||
0.2.3 писала: механизма поднятия потолка нет, «so the client MUST NOT offer resume as the remedy for
|
||
`paused`», — и не называла НИКАКОГО другого действия. Первая половина верна: `resume` действительно
|
||
не двигает такой прогон. Вторая — нет: стоп по потолку ЗАКРЫВАЕТ прогон, `finished_at` пишется тем же
|
||
оператором (`pgstore/runs.go:672-676`), `paused` входит в допустимые для старта состояния
|
||
(`runs/runs.go` (греп по имени функции), allowlist `readyToTranslate`), `HasLiveRun` при этом ложь — то есть **новый
|
||
прогон с бОльшим потолком запускается и является лечением уже сегодня**. Пользователь видел тупик там,
|
||
где его нет, на самом частом остановочном состоянии. В 0.3.0 лечение записано в `startRun` и в
|
||
`resumeRun`, а `resume` после потолка отвечает 409 с `cause.code: ceiling_reached`.
|
||
|
||
---
|
||
|
||
## 4. Открытые вопросы
|
||
|
||
| # | Вопрос | Статус |
|
||
|---|---|---|
|
||
| К-1 | Словарь статусов | **✅ ЗАКРЫТ D39.100**; 0.3.0 снял `finalizing` и развёл `RunStatus` — §2.6 |
|
||
| К-2 | Титул главы: поле `heading` или вклейка в текст | **○ ОТКРЫТ**, автор контракта + бэкенд. 0.3.0 закрыл ОТДЕЛЬНОЕ противоречие (запрет клиентской служебной метки снят), но кто производит настоящую метку — строка 160 |
|
||
| К-3 | Метка «Глава N» как единственная форма | **✅ ЗАКРЫТ D39.100** (метка — из ДАННЫХ книги). 0.3.0: служебный рендер «Глава N» разрешён КЛИЕНТУ, в локали интерфейса |
|
||
| К-4 | Ревизия: сквозная или пер-ресурсная; несут ли её чтения | **✅ ОТВЕЧЕН P0**, подтверждён ревью с поправкой: ревизия и позиция потока — РАЗНЫЕ величины, совмещать в `id` нельзя (0.3.0, §2.11) |
|
||
| К-5 | Показывать ли ETA | **✅ ЗАКРЫТ D39.100** (`eta_seconds` в спеке; 0.3.0 сделал поле required+nullable) |
|
||
| К-6 | Ступени замечания: сколько и где граница | **◐ ДЕЛЕГИРОВАН ПРОЕКТУ (испр. 05.09): подписи владельца больше НЕ ждём** — D39.176 п.4 (30.08) отдал фразы В-3 · К-6 · Приложение А проекту, требование ИНЖЕНЕРНОЕ (фраза не литерал, а данные по коду причины и локали); живые носители — строки 203/204. Прежний статус «ОТКРЫТ, владелец» держал вопрос на человеке, который его уже отдал. Ответ 16.08: «показывать ВСЕ; в тексте сворачивать и раскрывать по кнопке; как именно — решим потом». **Словарь ступеней проектировать заранее НЕ надо** — батч его и не проектировал. Зависимость закрыта: `Note.id` заведён (0.3.0), без него конкретное замечание нельзя свернуть и запомнить |
|
||
| К-7 | Пагинация: курсор или один ответ | **✅ ОТВЕЧЕН P0.** 0.3.0 добавил недостающее: максимум `limit`, подрезание вместо понижения, объявленный порядок каждой коллекции, `maxItems` у `decisions` |
|
||
| К-8 | Стоп по потолку — каким статусом | **✅ ЗАКРЫТ D39.100** (`paused` + оповещение) |
|
||
| К-9 | Отказ прескрина не выразим статусами | **✅ ПРИНЯТО ВЛАДЕЛЬЦЕМ 16.08:** прескрин — ещё одна ПРИЧИНА, а не двенадцатый статус: `rejected` + ОДИН грубый код (`content_refused`), максимально абстрактно, без вариации между попытками (§8а). Заведено 0.3.0 |
|
||
| К-10 | Пофазность у главы | **✅ ЗАКРЫТ — вердикт «НЕ строить»** (D39.138, поправка приёмки research/28 №1). Пофазных счётчиков на главу не будет: фаз на проводе нет. Исходная жалоба («дерево читает ноль всю первую волну») лечится СЕГМЕНТНОЙ логикой `Chapter.units_done` — §2.5 |
|
||
| К-11 | Условная обязательность полей | **○ ОСТАТОК ИНСТРУМЕНТАЛЬНЫЙ.** 0.3.0 применил правило «схема + слова» к трём местам (`BankDecision.dst`, `Unit.target`, агрегаты `BankPage`; 0.5.0: место `BankDecision.dst` унаследовала `BankCorrection` — там теперь `if`/`then`/`else` и `oneOf` идентичности, все продублированы словами) и сделал адресацию `Note` обязательной. Остаток — генератор игнорирует `if`/`then`; лечится сужением на шве клиента (S5) |
|
||
| К-12 | Завершение выгрузки: опрос или событие | **✅ ОТВЕЧЕН P0 (опрос)**, подтверждён AIP-151 («The response must not be a streaming response»). 0.3.0 добавил то, без чего опрос не завершался: `state` вместо булева `ready`, `failure_code`, `expires_at` |
|
||
| К-13 | `paused_reason` не различает две беды | **✅ ЗАКРЫТ D39.132 п.2а** (`null` на проводе ратифицирован). 0.3.0 добил остаток: описание требовало «`null` in every other state», что противоречило ратифицированному «`paused` + `null`» |
|
||
|
||
---
|
||
|
||
## 5. Ревью-вопрос: **«Сменится устройство пайплайна — придётся ли править фронт?»**
|
||
|
||
⚠ **Прежний ответ этой таблицы был ОПРОВЕРГНУТ ИСПОЛНЕНИЕМ** (research/28 Б-0, разбор — D39.138) и
|
||
здесь не сохраняется даже как история: он учил зону неправде — «фронт не правится» было ЛОЖНО для
|
||
0.2.3, где добавленная волна давала молчаливые 100 % и TypeError в рендере.
|
||
|
||
**Ответ редакции 0.3.0 — и теперь он верен, потому что чинили ПРИЧИНУ, а не формулировку:**
|
||
|
||
| Изменение в движке | Правит ли фронт |
|
||
|---|---|
|
||
| переименована стадия / добавлена/снята волна | **нет** — числа волн на проводе больше нет: одна полоса до ближайшей остановки, знаменатель — купленный объём (§2.5). Пофазный сплит остаётся ВНУТРИ платформы, колонки не трогаются |
|
||
| сменилась модель, маршрутизация, температура, промпт | **нет** — в allowlist не входят |
|
||
| добавлена новая причина флага | **нет** — на провод идёт КОД, карта живёт в контракте (приложение А); незнакомый код → нейтральная фраза, правило записано в схеме |
|
||
| добавлена новая причина отказа запроса | **нет** — второй уровень `cause.code` не закрыт по замыслу; клиент матчит корневой `code` |
|
||
| добавлен новый тип термина / новый провенанс | **нет** — словарь расширяется минором, ветка неизвестного стоит на шве |
|
||
| сменился чанкер, главы пере-разобраны | **нет по коду, ДА по данным — и теперь это ВИДНО:** `structure_version` двигается, курсоры и якоря на пары объявлены недействительными, на исчезнувшую главу отвечает `410`, полная замена — `resync_required`. До 0.3.0 клиент молча рисовал старое дерево |
|
||
| добавлено новое ПРОДУКТОВОЕ состояние | **да, один файл** — карта «статус → вид» на шве `src/api/`; это и есть контрольный вопрос владельца |
|
||
|
||
**Правило, которое отсюда следует и действует на КАЖДУЮ правку контракта (0.3.0):**
|
||
|
||
> **На проводе нет ни имён стадий/волн, ни движковых словарей — ни в полях, ни в ЗНАЧЕНИЯХ, ни в
|
||
> описаниях.** Описания компилируются в исходники клиента как JSDoc генерённых типов, поэтому
|
||
> объяснительная проза подпадает под тот же запрет, что и поля. Проверять грепом финальной спеки по
|
||
> списку: `draft · edit · wave · stage · mined · miner · ruby · finalizing · chunk · langpack ·
|
||
> sanitiz · flagged · verdict · prompt · engine · pipeline · glossar · escalat · snapshot` (список
|
||
> открытый — дополнять по мере находок). Единственное легальное вхождение — сама формулировка этого
|
||
> запрета в шапке спеки.
|
||
>
|
||
> **Гейт под это правило — тестом, по образцу языкового `generality.test.ts`** — половина ФРОНТА:
|
||
> `.spectral.yaml` держит только `spectral:oas`, а `generality.test.ts` ловит лишь языковую
|
||
> специфику, то есть утечку конвейера не стережёт ничто. Носитель: пинг фронту 16.08 в
|
||
> `frontend/docs/frontend-PROGRESS.md`, исполнение при разморозке зоны (D39.136 п.2).
|
||
|
||
---
|
||
|
||
## 6. Направления: что решено НЕ делать в 0.3.0
|
||
|
||
Записано направлениями, чтобы не превратиться в молчаливые дыры. **Номеров бэклога здесь не
|
||
выдумывается:** где строки нет, носителем назван документ.
|
||
|
||
| Что | Почему не в батч | Носитель |
|
||
|---|---|---|
|
||
| `POST /books/{id}/parts` — дописать главы в существующую книгу | форма и движковый гейт против МОЛЧАЛИВОЙ перекупки хвоста при вставке не в конец — отдельная работа вместе с этапом структуры глав | **строка 185** |
|
||
| Снятие жанра из брифа и промптов | двигает `BriefHash` ⇒ только в общее resnapshot-окно | **строка 184** |
|
||
| Сворачивание замечаний в тексте под кнопку | зона фронта, при разморозке | пинг фронту 16.08 |
|
||
| История прогонов книги | **снята решением владельца 16.08: «в МВП не нужно».** Данные уже в Postgres и не удаляются, индекс `runs (book_id, started_at desc)` стоит — если поддержка попросит, это один read-путь | research/28 §8 п.10 |
|
||
| Двухшаговая загрузка (метаданные JSON → `PUT` байтов) | три из четырёх greenfield-дизайнов выбрали её, и при ней проблема порядка частей не существует. Ц3: переписывается построенный путь с обеих сторон. Направление на после-беты | research/28 §5 «не в батч» |
|
||
| Лента изменений структуры (`added\|removed\|moved\|split\|merged`) с наследниками | дизайн-пак структуры глав | **строка 161** |
|
||
| Поиск и фильтр по банку и дереву | нужны индексы (Ц2); сегодня клиент ищет в браузере, и это осознанно | research/28 §5 «не в батч» |
|
||
| «Грубая группа статуса» для эволюции словаря | эволюционный механизм без сегодняшней боли | research/28 §5 «не в батч» |
|
||
| Словарь ступеней замечаний | К-6 ДЕЛЕГИРОВАН проекту (D39.176 п.4); инженерная задача, носители — строки 203/204 | research/28 §8 п.11 |
|
||
| Добавочные поля `BankTerm` (evidence/variants/conf, aliases) | слово владельца 15.08: «НЕ заводить … смысла хватает» | D39.136 п.4б |
|
||
| **Поток на БИБЛИОТЕКУ** (одна лента на все книги аккаунта) | поток книжный (§5 п.5 заказа — «перевесить на книгу»); экран библиотеки обновляется чтением, которое под условным чтением стоит заголовков. Ленты на библиотеку нет, и открывать поток на строку списка клиент НЕ должен — это записано нормой в спеке. Найдено холодным потребителем как реальный перф-вопрос | research/28 §5б · строка 186 |
|
||
| **Чтение ОДНОЙ главы** `GET /books/{id}/chapters/{chapterId}` | сегодня кадр `chapter` про главу, которой клиент не держит, игнорируется (норма записана), а `410` лечится перечитыванием дерева. Операции нет — она не в заказе; вопрос дизайн-пака структуры глав | **строка 161** |
|
||
| **Список экспортов** `GET /books/{id}/exports` | Б-4 просил СОСТОЯНИЕ экспорта, не список. Восстановление после перезагрузки закрыто иначе — `Idempotency-Key` на создании возвращает исходный `202` с тем же `Location` | research/28 §2 (Б-4) |
|
||
| Новое число страницы замечаний | **замера нет ни одного**: сколько замечаний даёт настоящая книга, не знает никто. 500 выбрано без числа, и выдумывать второе число вместо первого — та же ошибка. Число ушло из спеки в `Capabilities.page_size_default`; калибровка — после первого настоящего прогона | research/28 §7, §10 |
|
||
|
||
### 6а. Резать отказались (§5а) — контраргумент на каждое
|
||
|
||
Перечень СНЯТОГО батчем 0.3.0 здесь не дублируется: он в диффе лендинга `8d82096` (`git log -S`) и в
|
||
research/28 §5а, а живые следствия каждого снятия стоят в своих §2.
|
||
|
||
- **параметр `limit`** («клиент не отправил его ни разу»). Тот же §5 требует объявить у него
|
||
`maximum` и подрезание вместо понижения (Б-10) — у удалённого параметра максимума не объявишь.
|
||
Плюс: «референсный клиент не шлёт» — свойство ОДНОГО клиента, а контракт пишется для второго.
|
||
- **`Note.unit_id`** («только в фикстуре мока; экраны берут `unit.note` вложенно»). Основание — «ни
|
||
один экран не читает», ровно тот довод, который эррата 16.08-г уже опрокинула на `TermStatus`:
|
||
экранов замечаний (S6) ещё нет. Адресация на пару — единственный способ перейти к МЕСТУ замечания
|
||
из плоского списка, а собственная фраза операции говорит «A note addresses a unit or a whole
|
||
chapter». Поле оставлено НЕОБЯЗАТЕЛЬНЫМ (Б-9 просил ровно это), а обязательным сделан `chapter_id`
|
||
— то есть дефект «замечание, не адресующее ничего» закрыт.
|
||
- **`Bank.signed`** («доезжает до клиента и не рисуется»). Тот же довод «нет экрана» — экран подписи
|
||
это S5. `total`, `signed` и `pending_decisions` — три НЕЗАВИСИМЫХ факта: строку можно решить и не
|
||
подписать (отклонить), поэтому `signed` не выводится из двух других. *(0.5.0: `pending_decisions`
|
||
снесён — §2.19-бис; довод о независимости `signed` стоит и без него.)*
|
||
- **нагрузка кадров `note` и `bank`** («передаётся и игнорируется»). Игнорировалась она по причине,
|
||
которую батч устранил: у замечания не было id, поэтому кадр нельзя было сопоставить со списком.
|
||
С `Note.id` кадр `note` несёт ПРИМЕНИМУЮ ДЕЛЬТУ — это ровно первая ветка правила Б-11а, и снятие
|
||
нагрузки вернуло бы перечитывание всего списка. Счётчики `bank` — вторая ветка того же правила
|
||
(«счётчик + скоуп»), а строки читаются дельтой `?after_version=`.
|
||
|
||
---
|
||
|
||
## 6б. Дофикс-раунд 16.08 (ФБ-1…ФБ-10): что он изменил в каноне и почему
|
||
|
||
Все десять правок живут нормами в `openapi.yaml`, поимённый перечень — D39.143 п.1 (заказ раунда —
|
||
D39.142 п.3). Здесь остаётся только то, чего нет ни там, ни в нотах:
|
||
|
||
- **ФБ-9, шов имён.** `parser_unavailable` переименован на проводе в `processing_failed`. ⚠ **Живое
|
||
следствие для шва:** проводное имя отличается от внутреннего словаря платформы, где значение
|
||
по-прежнему зовётся `parser_unavailable` (`platform/internal/books/parse.go:66`), и проекция
|
||
обязана отобразить одно на другое; рядом там же живут `schema_mismatch` и `storage_unavailable`,
|
||
которые на провод не идут вовсе.
|
||
- **ФБ-8.** ⚠ Тождество `Idempotency-Key` на multipart считалось «по объявленным частям» —
|
||
ПЕРЕКРЫТО 0.4.0, читать по §2.18.
|
||
|
||
## 6в. Синк с платформой 20.08 → канон 0.4.0: что решено и почему
|
||
|
||
Вход — `platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md`, четырнадцать мест, где провод сообщает
|
||
состояние и не даёт клиенту действия. Метод разбора и его итог по всем четырнадцати — D39.152;
|
||
вердикт каждого пункта — в таблице ниже.
|
||
|
||
⚠ **База ссылок этого раздела — РАБОЧЕЕ ДЕРЕВО 20.08, а не HEAD.** В `platform/` на этот момент 81
|
||
незакоммиченный файл (P7 в работе), поэтому одна и та же строка в HEAD и в дереве разная, и ссылка
|
||
без базы проверяема только случайно. `backend/` чист, его ссылки годятся в обоих. Там, где
|
||
конструкция переживёт любую перенумерацию, адрес дан ИМЕНЕМ функции или ветки — это дешевле и
|
||
надёжнее номера. Поймано ревью диффа: первая редакция §6в смешивала обе базы в одной строке таблицы.
|
||
|
||
### п.0. Сверка на потери прозы 0.2.3 → 0.3.0 — исполнена; найдена ОДНА потеря, она и была заявлена
|
||
|
||
Метод (полный дифф лендинга `8d82096` по обоим файлам, чтение ВСЕХ удалённых строк) и разбор трёх
|
||
классов удалённого — D39.152; сами строки держит тот же дифф (`git log -S`).
|
||
|
||
Итог: **единственная потерянная НОРМА — фраза §2.14** («различать причины отказа клиент не может по
|
||
замыслу»), восстановлена. Второе — правило пустого `kind` уцелело в каноне, но потеряло здесь
|
||
ПРОЕКЦИЮ (`""` → `null` — работа платформы); клауза возвращена в §2.8. Остальное удалённое правил не
|
||
несло.
|
||
|
||
**Гейт на следующую резку прозы, и он дешевле того, что был.** Приёмка батча сверяла мультимножество
|
||
модальных глаголов и этот класс структурно не ловила. Сверять надо не модальность, а **список
|
||
собственных имён формы** (полей, значений, кодов, заголовков): норма почти всегда стоит рядом с
|
||
именем, а фраза §2.14 пропала именно там, где имени не было (`/auth/*` — не поле). ⚠ Греп по
|
||
исходному файлу даёт ЛОЖНЫЕ пропажи — правило переносится через перенос строки; сверять на копии,
|
||
склеенной в одну строку (`tr '\n' ' '`). Носитель: эта запись.
|
||
|
||
**Диспозиции пунктов, исполненных без остатка: тела сняты, содержимое живёт по адресу.** Пункты
|
||
A · B · G · H · J · L разобраны ниже полностью — их предмет либо открыт, либо несёт провенанс,
|
||
которого больше нигде нет.
|
||
|
||
| Пункт записки | Вердикт | Где живёт сегодня |
|
||
|---|---|---|
|
||
| **C.** Вход `/auth/*` | ИСПОЛНЕНО: срезанная фраза восстановлена, уточнённый вариант A записан, триггер пересмотра зафиксирован; проводной словарь в компаньоне НЕ объявлен — возражение зоны про «нормативность с чёрного хода» принято дословно и стало основанием | §2.14 целиком |
|
||
| **D.** `Note.code` против пустой клетки | ПРАВЛЮ ОБА ФАЙЛА: `unspecified` ратифицирован как ОБЯЗАННОСТЬ сервера, а не строка словаря причин. Родственный вопрос ступеней НЕ решается — К-6 открыт, слово владельца «решим потом», провизорность построенного зафиксирована | приложение А, п.5 и п.6; канон |
|
||
| **E.** Отпечаток интейка (PD-262) | ПРАВЛЮ КАНОН, форму НЕ трогаю: «тот же запрос» = метаданные + имя файла + СОДЕРЖИМОЕ; чем сервер устанавливает последнее — его дело; сервер, не установивший тождества, не реплеит, а отвечает `409`. Поля размера в форме нет: оно не закрыло бы «две разные книги одной длины» | §2.18 (правка 0.4.0) |
|
||
| **F.** `ETag`/`304` на любом безопасном чтении | ПРИНЯТО одной фразой: валидатор на любом GET легален и объявления не требует; объявленные операции — те, где клиенту есть смысл им пользоваться, а не исчерпывающий список. Границы: клиент НИКОГДА не обязан слать `If-None-Match`, `304` бывает только на присланный. `getUsage`/`getRunOptions` НЕ добавлены сознательно — оба читаются ровно перед действием | шапка канона; §3, строка условных чтений |
|
||
| **I.** `content_refused` | forward-looking, долг зона не заводит: значение ратифицировано К-9 ДО производителя сознательно. Обязанность ограничивать число попыток аккаунта пере-привязана — падает ВМЕСТЕ с производителем, а не висит на сервере, который отказа не выдаёт | §3, строка `content_refused` (строка 94, ПТ-16); приложение А-2 |
|
||
| **K.** `--verify-bank` | НЕ контракт, релей в зоны. Контрактно видимая половина закрыта D39.144/145. ⚠ Вторая половина умерла вместе с носителем: схема `BankDecision` снесена 0.5.0, дверь-преемник пишет файлы движка сама, так что «решение не доезжает» перестало быть свойством формы | §2.19-бис; §3, строка `decline`; D39.144 |
|
||
| **M.** Что зона сделала сама | сверено, возражений нет (восемь пунктов, включая снятие `codeForStatus`) | D39.152 п.2 |
|
||
| **N.** Границы сессии; что забирает P7 | шесть релеев зоне (A · D · E · G · J · H) приняты и построены паком P7. ⚠ **Граница автора 0.4.0** (его собственное объявление, восстановлено): НЕ проверялось, как экраны фронта рисуют новые ответы (зона заморожена) и как ведёт себя построенный путь `resume` после правки таблицы — это работа P7 | D39.153; статусы — таблица §3 |
|
||
|
||
### A. Прогон с исчерпанным бюджетом (PD-282 ⚠ ЗАКРЫТО (PD-282 `fixed`, акт 5; разбор — §3)) — ПРАВЛЮ КАНОН; половину мнения отклоняю
|
||
|
||
**Факт подтверждён и оказался шире записки.** `reopen` даёт `exhausted` в ДВУХ местах, а не одном
|
||
(`runs/reconcile.go`): `remaining <= 0` и `ErrInsufficientCredit` от `RestartRun` — то есть
|
||
«кончился потолок прогона» и «нечем взять холд на счёте» уже сегодня схлопнуты в один вердикт, и
|
||
`Resume` отвечает на оба одинаково: `202` + неизменённый `Run` (`httpapi/v0.go`, хендлер
|
||
`resumeRun`). `paused` до этой ветки не доходит — он отказан выше, поэтому молчаливый `202` живёт
|
||
ровно на `stopped` и `awaiting_bank`. Худший из двух — `awaiting_bank`: человек подписал банк,
|
||
нажал «продолжить», получил «принято», а `ReleaseBankStop` не вызывался (зона это записала сама,
|
||
`archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7(и)).
|
||
|
||
**Правка канона (§resumeRun).** Записана ВТОРАЯ ОСЬ: исчерпанность лимита прогона перебивает обе
|
||
строки `202`; ответ — `409` `run_not_resumable`, `cause.code: ceiling_reached`, лечение — новый
|
||
прогон. Новых значений enum не заводится: `ceiling_reached` уже есть и его собственный текст этот
|
||
случай описывает дословно. Тем же ходом канон получил обратную гарантию, которой не имел:
|
||
**`202` теперь означает, что работа действительно переоткрыта**, и клиенту не нужно второе чтение,
|
||
чтобы отличить успех от no-op. И записано разделение, о котором предупреждает сам канон у
|
||
`AccountHaltReason`: пустота СЧЁТА не сворачивается в `ceiling_reached` — у неё своя причина
|
||
`credit_unavailable` (аддендум ниже), а состояние аккаунта ЦЕЛИКОМ по-прежнему говорит `Usage`, не
|
||
этот вызов.
|
||
|
||
**Признак «этот прогон ещё можно продолжить» на `Run` — ОТКЛОНЁН, с оговоркой.** Посылка «платформа
|
||
знает его точно» верна, посылка «дёшево» — нет: `ReadRun` — один запрос по `books join runs`
|
||
(`pgstore`, функция `ReadRun`), а флагу нужны ТРИ чтения, которых в нём нет — `RunSpent`,
|
||
`AttemptReservationOpen` и баланс счёта. `Run` сериализуется в карточке книги, а карточку канон
|
||
велит перечитывать НА КАЖДЫЙ кадр потока («it is still read on a frame, on navigation and on
|
||
focus»), то есть цена платится на горячем пути ради факта, который
|
||
(а) устаревает к моменту клика и (б) уже отвечен ратифицированным порядком чтения пяти мест
|
||
(§RunOptions): `max_chapters == 0` + `blocked` — это ровно «стоит ли предлагать старт сейчас и что
|
||
мешает», и рисует его тот же экран. Авторитет по-прежнему у мутации, как канон и объявляет.
|
||
⚠ Поправка ревью к предыдущему абзацу: сам `Run` в КАДРЕ не едет — ни один payload его не несёт;
|
||
горячий путь создаёт не кадр, а предписанная им перечитка карточки. Вывод не меняется, носитель цены — другой.
|
||
**Триггер пересмотра:** замер, показывающий, что мёртвый клик частый, ЛИБО сворачивание трёх чтений
|
||
в коррелированные подзапросы того же запроса (агент проверил — возможно). Тогда форма — `Run`,
|
||
поле, минор.
|
||
|
||
### B. Суточный потолок движка — ОТКЛОНЯЮ ОБЕ ФОРМЫ; посылка не выдержала проверки
|
||
|
||
Записка просит слово или признак для паузы, которую деньги не лечат. Проверка посылки:
|
||
|
||
1. **Владелец уже решил этот вопрос — 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.
|
||
2. **Предложенная форма неверна по СМЫСЛУ, даже если бы случай был.** «Лечится покупкой / не
|
||
лечится» — булево, а суточный потолок это ОКНО: он снимается сам в полночь UTC
|
||
(`store/ledger.go:60-72`, `todayUTC()`). Клиент, прочитавший «не лечится», спрячет кнопку
|
||
навсегда там, где честный ответ — «не сейчас». Булево, у которого один из двух исходов врёт,
|
||
хуже, чем отсутствие поля.
|
||
3. **Второе значение enum — та же проблема плюс своя:** оно называет настройку чужого файла на
|
||
проводе, что запрещает правило §5 («на проводе нет движковых словарей»), и зона сама этого не
|
||
хочет.
|
||
|
||
**Что записано вместо этого — одна фраза в каноне (§resumeRun).** «Новый прогон легален при любом
|
||
`paused`» осталось нормой, но перестало читаться как обещание прогресса: деплой может держать
|
||
СВОИ лимиты, которых этот контракт словом не называет, и прогон под таким лимитом останавливается
|
||
так же; клиент всё равно предлагает новый прогон — лучшего действия у него нет, — но не подаёт его
|
||
как гарантию, а повторный `paused` это законный исход, а не сбой. Цена ошибки при этом наблюдаемая
|
||
и самокорректирующаяся: холд возвращается, провайдерских денег не теряется (ФАКТ записки).
|
||
|
||
**Триггер пересмотра, записанный явно:** день, когда `day_usd` вернётся в платформенный шаблон
|
||
книги. Тогда это настоящая дыра и контракт открывается заново.
|
||
|
||
⚠ **Попутно найдено и стоит знать зоне:** суточный потолок — НЕ единственный стоп, который деньги
|
||
не лечат. Их четыре класса (`cmd/tmctl/main.go:52-79`): подпись банка (exit 3), graceful stop
|
||
(exit 5), полоса отказов 10–19 и суточный потолок (exit 4, scope `day`). Просто три из четырёх уже
|
||
имеют свою проекцию (`awaiting_bank`, `stopped`, `failed` + `RunFailureReason`), и «денег не
|
||
хватает» из них не следует ни для одного. То есть класс закрыт, а дыра — только в четвёртом.
|
||
|
||
### G. Draft-only конвейер (PD-202) — ОТВЕЧАЮ: ДА, начерновленная глава «сделана»; правлю канон
|
||
|
||
Проверено чтением обеих зон, и цена выше заявленной. У движка деплой без второго прохода — не
|
||
экзотика: разбиение выводится из РОЛЕЙ стадий (`pipeline/waverun.go:43-53`), конфиг требует лишь
|
||
«хотя бы одна стадия» (`config/pipeline.go:841`), ветка живая и покрыта тестом
|
||
(`contractblockers_test.go:115-136`); в репозитории такого конфига пока нет, но выбирает его
|
||
оператор, и платформа шаблон не читает. У платформы `units_done` — жёстко второй проход
|
||
(`pgstore/sink.go:252-263`, `readmodel.go:200-208`).
|
||
|
||
**Следствие, которого в записке нет:** ноль навсегда — это не только короткая полоса.
|
||
`Book.chapters_done` и `ChaptersLeft` (`pgstore`, функции `chaptersDone` и `BookRunContext`) читают
|
||
ту же колонку, а `ChaptersLeft` подрезает ШКАЛУ покупки. То есть на таком деплое сервис бесконечно
|
||
предлагает купить главы, которые уже переведены. Это выводит вопрос за рамки «косметика полосы» и
|
||
делает ответ обязательным.
|
||
|
||
⚠ **Первая редакция этого абзаца дописывала «и берёт за них деньги» — снято ревью как неверенное,
|
||
и снято ИЗ КАНОНА тоже** (там оно уже стало нормативным текстом, компилирующимся в JSDoc клиента).
|
||
Что проверено: платформа держит ПОТОЛОК, а не цену, и рассчитывается по ЗАМЕРЕННОЙ трате, а
|
||
неизменённая уже начерновленная книга переигрывается с чекпойнтов за $0 — то есть в обычном случае
|
||
холд возвращается целиком и не берётся ничего. Плата возникает только если между прогонами сдвинулся
|
||
снапшот (слаг модели, промпт, langpack, флип гейта), и тогда пере-покупка действительно оплачивает
|
||
переведённое заново. Довод «предлагают уже сделанное» стоит сам по себе и в поправке не нуждается.
|
||
|
||
**Ответ контракта.** «Сделано» = последний проход, который эта книга на ЭТОМ деплое реально
|
||
получает. Записано в `Progress` и в `Book.chapters_done` — вместе с доводом: контракт не говорит,
|
||
сколько проходов бывает, значит и назвать один из них для счёта не может, а обещание «дробь
|
||
доходит до единицы» — это обещание, а не проходы. Имён волн в тексте нет (гейт §5 пере-ран, чисто).
|
||
Форма не меняется, генерённые типы не двигаются; работа — проекция платформы, вход P7 ⚠ (носитель израсходован, D39.153).
|
||
|
||
### H. Снятие замечания (PD-298) — СВЕРЕНО С ДВИЖКОМ ПЕРВЫМ, как просила зона; механизма НЕ строим
|
||
|
||
Порядок соблюдён: сначала чтение движка. Результат — **переход достижим у драйвера и НЕ достижим на
|
||
проводе**. `flagged` вычисляется на каждой эмиссии, а не хранится (`pipeline/events.go:351,377-379`,
|
||
`waverun.go:139-140,205-206`), и `redrive`/`--resnapshot`/правка исходника пере-атакуют юнит, так
|
||
что второе разрешение законно может выйти `flagged=false`. Но эмиссия идёт через
|
||
announce-once-леджер: ключ `unit:<book>:<wave>:<chapter>:<unit>` не несёт ни снапшота, ни прогона
|
||
(`backend/internal/pipeline/events.go`, греп `unit:%s:%s:%d:%d`), `EnqueueOnce` вторую строку не
|
||
пишет, а леджер переживает прогоны — «announced ONCE for the life of the book, whatever crashes and
|
||
resumes happen in between» (`backend/internal/store/outbox.go`, символы `EnqueueOnce` и
|
||
`onceKeyLookup`); запинено `TestAResumedRunOpensItsOwnStreamAndDoesNotRecountFinishedUnits`.
|
||
Единственное окно — падение между записью строки в журнал и пометкой
|
||
(`events.go:154-164,200-202`); плюс `redrive` платформа не вызывает вовсе: сборщики argv знают
|
||
`translate`/`status`/`manifest`/`export`/`bank-apply`, глагола `redrive` среди них нет
|
||
(`platform/internal/runner`, греп `Args(workdir`).
|
||
⚠ Адреса пере-нацелены 02.09 с номеров на символы: три из шести съехали за паки, вывод не изменился.
|
||
|
||
**Поэтому: строка «недостижимо» в регистр, а не механизм** — ровно тот исход, который зона назвала
|
||
правильным. Но в каноне закрыт РАЗРЫВ ПРАВИЛА, который эта проверка обнажила: §AfterVersion говорил
|
||
«удаление так не выразить» только про ОПТОВУЮ замену, а про исчезновение ОДНОЙ строки не говорил
|
||
ничего. Записано: строка, once delivered, поодиночке не отзывается; коллекция, которая обязана
|
||
потерять строку, теряет её единственным выразимым способом — заменой целиком с `resync_required`.
|
||
Это обязанность сервера, а не пожелание, и она делает поведение платформы выводимым: сегодня при
|
||
`flagged=false` строка обновляется и ревизия двигается (`pgstore/sink.go:237-244`), дельта её
|
||
прячет (`readmodel.go:725-735`), а `note_count` в кадре главы уже УМЕНЬШАЕТСЯ (`sink.go:310-328`) —
|
||
то есть клиент увидел бы счётчик, противоречащий списку. Теперь ясно, что должно произойти вместо
|
||
этого.
|
||
|
||
### J. «Остановлено по вашей просьбе» — ПОЛЕ ЗАВЕДЕНО; форма отличается от предложенной зоной
|
||
|
||
Решение владельца 17.08 принято, форму делегировали контрактной сессии
|
||
(`archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md:462`). Заведено `Run.stop_requested` — **обязательное булево, а не
|
||
необязательное.** Довод: функция тотальна — прогон, который никто не просил остановить, это `false`,
|
||
а не «поля нет»; необязательность дала бы два написания одного факта, и это ровно тот дефект, который
|
||
контракт уже чинил у `sense` (0.2.2), у `reject_reason` и у `kind`. Ценой стал ломающий минор —
|
||
`Run` получил обязательное поле, — и он объявлен честно в шапке компаньона, вместо того чтобы
|
||
называть правку «аддитивной» и оставить генератор доказывать обратное. В описании записано и то,
|
||
чего в заказе не было: флаг — запись ПРОСЬБЫ, а не исхода, и остаётся `true` на прогоне, который
|
||
успел доработать, потому что это и есть случай, который экран обязан объяснить.
|
||
|
||
### L. Мелкое
|
||
|
||
- **`410` на опечатку в id главы (PD-253)** — **канон изменён в сторону зоны, а не наоборот.**
|
||
Требование различать «была и исчезла» от «такой не было» невыполнимо: id непрозрачны, после
|
||
пере-разбора не хранятся, и доказать, что id никогда не минтился, можно только кладбищем всех
|
||
выданных. Записано: `410` — ответ на id, которого нет в ТЕКУЩЕЙ структуре, был он когда-то или
|
||
нет; `404` остаётся за книгой. Лечение у клиента в обоих случаях одно — перечитать дерево.
|
||
- Два других пункта исполнены и живут на своих местах: указатель приложения А переведён с номера
|
||
строки на имя функции (`flagReasonSeverity` — номер в чужой зоне протухает за пак), а посылка
|
||
«у читателя НЕТ меток глав вообще» опровергнута — служебную «Главу N» рисует КЛИЕНТ в локали
|
||
интерфейса (§2.3); продуктовый пробел «настоящих названий нет» держится строкой 160 в §3.
|
||
|
||
### Ревью диффа и второе ревью 20.08 — что от них осталось живого
|
||
|
||
Веер линз, счёт находок и их адверсариальная верификация — D39.152. Ни одна не опрокинула
|
||
диспозицию; все подтверждённые внесены НА МЕСТЕ и живут сегодняшними оговорками в пунктах A, B, G и
|
||
в строке PD-298 таблицы §3. Живого здесь — два провенанса ниже.
|
||
|
||
⚠ **Провенанс `cause.code: credit_unavailable`.** Три линзы независимо споткнулись об один шов —
|
||
что отвечает `resume`, когда у прогона лимит ещё есть, а СЧЁТ не тянет холд. Каждую формулировку
|
||
верификатор опроверг по отдельности («тексты можно прочесть согласованно»), но холодный потребитель
|
||
прямо написал, что решить не может, а платформа этот случай уже схлопывала в тот же вердикт:
|
||
сходимость трёх независимых линз на одном месте принята сигналом сильнее трёх поштучных
|
||
опровержений. Заведено второе значение уровня `cause` (он не закрыт по замыслу — типы не двигает),
|
||
`ceiling_reached` пере-сформулирован как «ЭТОТ прогон закончен», подменять друг друга им запрещено.
|
||
|
||
**Второе ревью (заказ владельца 20.08): «а так вообще делают, или ты изобрёл своё» + «не раздул ли
|
||
прозу». 20 находок, подтверждена 1; велосипедов не подтверждено ни одного** — проверялись против
|
||
настоящих спек: тождество интейка против `draft-ietf-httpapi-idempotency-key-header` и RFC 9530
|
||
(Digest Fields) · `credit_unavailable` под `409` (402 зарезервирован; двухуровневый код
|
||
конвенционален — Microsoft `innererror`, `google.rpc.ErrorInfo`) · `stop_requested` булевым против
|
||
конвенции таймстемпа (k8s `deletionTimestamp`, AIP-165) · `410` на никогда не существовавший id
|
||
против RFC 9110 §15.5.11 · «валидатор легален на любом безопасном чтении» против OAS 3.1 ·
|
||
пер-строчные tombstone по образцу JMAP (RFC 8620) — не наш случай.
|
||
|
||
**Замер объёма прозы** (пин зоны `openapi-typescript@7.13.0`, воспроизведён верификатором до байта):
|
||
канон дал **+10 546 байт и +144 строк комментариев** в исходники фронта. Гипотеза «62 % этого —
|
||
обоснование, которому место в компаньоне» пере-проверку НЕ прошла: из тринадцати заявленных блоков
|
||
выжил один. **Своим решением, а не по находке, срезан ещё денежный хвост у `Book.chapters_done`** — он ничего не меняет в поведении клиента. Итог трима: 152 758 → 152 002 байта. **Урок для следующей правки канона: норма — в
|
||
канон, довод — сюда; проверять не глазом, а диффом генерённого клиента.**
|
||
|
||
---
|
||
|
||
## 7. Эксплуатационные примечания — НЕ норма контракта
|
||
|
||
Вынесено из спеки в 0.3.0 (Б-16) по RFC 9205 §4.1: требовать минимальную версию HTTP —
|
||
**NOT RECOMMENDED**, а выигрыш деплоя от конкретной версии «**ought be noted**». Отметить, а не
|
||
потребовать.
|
||
|
||
Норма в спеке — то, что клиент НАБЛЮДАЕТ и на что вправе рассчитывать: `ETag`/`If-None-Match`/`304`
|
||
· **обязанность честить `Accept-Encoding` на JSON-ответах и НЕ сжимать `text/event-stream`** · `Vary`
|
||
на согласованном представлении · `Cache-Control: no-store` · запрет буферизации потока · форма
|
||
heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИЕ живёт в контракте — это прямое указание D39.138
|
||
п.2(д) («сжатие и условные чтения записываются В КОНТРАКТ, не в зонный док»).
|
||
В примечании остаётся ТОЛЬКО слой исполнения — где именно сжимать, — потому что вот этого клиент
|
||
действительно не наблюдает, и вот это Б-16 из контракта и выносит.
|
||
|
||
Ниже — то, что клиент не наблюдает и что деплой обязан себе устроить сам:
|
||
|
||
- **Сжатие — в приложении, а не на edge** (испр. 02.09; прежний текст требовал устроить его деплою):
|
||
платформа жмёт САМА, в Go — `platform/internal/httpapi/conditional.go`, греп `gzipFloor` (порог
|
||
1<<10 байт) и `acceptsGzip`. Деплою остаётся одно — НЕ сжимать ВТОРОЙ раз на прокси/CDN и не
|
||
сжимать `text/event-stream`.
|
||
- **HTTP/2 на edge.** Клиент держит одно SSE-соединение на книгу; на HTTP/1.1 шесть соединений на
|
||
origin — потолок вкладок. Сервис на HTTP/1.1 обязан работать ХУЖЕ, а не не работать.
|
||
- **`X-Accel-Buffering: no`** (или эквивалент прокси) — механизм для нормы «поток не буферизуется».
|
||
- **Сколько это даёт.** Замер ревью на настоящей книге (`~/books/gu-zhenren`, 2283 главы): глава
|
||
26,1 КБ → 10,4 КБ; дерево глав 250 КБ → 39 КБ; банк 1000 строк 167 КБ → 17 КБ.
|
||
- **Порядок работ по эффекту на килобайт усилия** (research/28 §5б, строка 186): сжатие → `ETag`/304
|
||
→ скоуп в кадре → дельта-чтения `?after_version=` → `staleTime` у клиента. Шаги 1–2 ПОСТРОЕНЫ
|
||
паком P7 (D39.153), шаги 3–4 — в контракте с 0.3.0, шаг 5 — зона фронта.
|
||
- **Транспорт НЕ меняется** (проверено независимым агентом другого тира + сверка первоисточников):
|
||
наша нагрузка — художественный текст, и после gzip разница между JSON и protobuf единицы процентов;
|
||
gRPC-web требует прокси и теряет `If-None-Match`/304; Connect возвращает то, что у нас уже есть.
|
||
Наш паттерн «кадр без данных → клиент перечитывает» легитимен и называется poke/pull.
|
||
|
||
---
|
||
|
||
## Приложение А. Карта «причина → КОД контракта → продуктовая фраза» — ЗАГОТОВКА
|
||
|
||
**Форма карты (0.3.0).** `Note` несёт **`code`**, а фразу рисует КЛИЕНТ; `Note.message` с провода
|
||
снят. Серверная локализованная фраза — ровно та политика, которую вариант B отменил для ошибок
|
||
(§2.17), и держать её для замечаний значило бы слать второй русский текст из зоны без локалей.
|
||
|
||
**Правила заполнения.**
|
||
|
||
1. Фраза пишется по ДОККОММЕНТУ `disposition.go`, а не по имени константы, и рядом кладётся цитата —
|
||
иначе повторяется инверсия, стоившая двух фраз (`glossary_miss` подан как «термин не подписан»,
|
||
хотя термин ПОДПИСАН и его проигнорировали, `disposition.go:78-79`; `sanitizer_stripped` подан как
|
||
потеря текста, хотя «the chunk is NOT lost», `disposition.go:99`).
|
||
2. **Класс 2 схлопывается в ОДИН код** (§8а, нормативно): четыре причины ранга 0 — модельный отказ —
|
||
на проводе неразличимы, потому что каждый различимый код здесь бит обратной связи подбирающему.
|
||
3. Слова — ЗА ПРОЕКТОМ (D39.176 п.4, 30.08): владелец делегировал фразы В-3 · К-6 · Приложение А, подписи его больше НЕ ждём. ⚠ Требование владельца — ИНТЕРНАЦИОНАЛЬНОСТЬ: фраза не литерал ни в Go платформы, ни в TSX фронта, а ДАННЫЕ по машинному коду причины и локали; словарь — один на продукт. Карта «причина движка → КОД контракта» НЕ загейчена и поручена контрактной сессии (строка бэклога 203); гейт доставки словаря данными — строка 204, вместе со словарём закрывается `PD-246`. ⚠ Прежний указатель «строка 148» мёртв: строку удалил `a023e39` решением владельца. **Коды ниже — ◆ ПРЕДЛОЖЕНИЕ**, ратифицируются вместе с
|
||
фразами; до тех пор `Note.code` в спеке НЕ enum, чтобы схема не стала второй копией незаписанной
|
||
карты.
|
||
4. Последняя строка — не формальность: контракт обязан иметь фразу для причины, которой ещё не
|
||
существует, и она обязана читаться нейтрально, а не как «ошибка».
|
||
5. **Клетка кода последней строки БОЛЬШЕ НЕ ПУСТА — `unspecified`** (синк 20.08, п. D записки).
|
||
Пустая клетка была невыполнима: `Note.code` в каноне обязателен и `minLength: 1`, движок и
|
||
платформа выпускаются независимо, поэтому окно «пришла причина, которой этот билд не знает» —
|
||
штатное. Платформа уже отдавала стабильный плейсхолдер `unspecified` (`ingest/notes.go`), не
|
||
имея на него ратификации; синк её ратифицировал и записал в канон обязанностью сервера, а не
|
||
строкой словаря. Бампа схемы не требует — `Note.code` объявлен `type: string`.
|
||
6. **Столбец «Ступень» пуст, а платформа уже провела границу.** `ingest/notes.go` раскладывает
|
||
девять движковых рангов на два проводных значения по правилу «потерял ли читатель текст». Это
|
||
ПРОВИЗОРНО и ратификацией не является: К-6 ДЕЛЕГИРОВАН проекту (D39.176 п.4) и закрывается инженерно, а не подписью. Строка
|
||
записана здесь, чтобы построенное не читалось как решённое.
|
||
|
||
| Причина движка | Ранг | Код контракта ◆ | Продуктовая фраза | Ступень |
|
||
|---|---|---|---|---|
|
||
| `hard_refusal` · `soft_refusal` · `content_filter` · `hard_block` | 0 | `content_withheld` (ОДИН на все четыре — класс 2) | ⬜ | ⬜ |
|
||
| `cjk_artifact` | 1 | `source_residue` | ⬜ | ⬜ |
|
||
| `off_target_lang` | 1 | `wrong_language` | ⬜ | ⬜ |
|
||
| `excision_suspect` | 1 | `text_possibly_dropped` | ⬜ | ⬜ |
|
||
| `coverage_fail` | 1 | `incomplete_coverage` | ⬜ | ⬜ |
|
||
| `sanitizer_defect` | 2 | `markup_defect` | ⬜ | ⬜ |
|
||
| `loop_degenerate` | 3 | `repetition` | ⬜ | ⬜ |
|
||
| `decode_error` | 4 | `unreadable_answer` | ⬜ | ⬜ |
|
||
| `glossary_miss` | 5 | `term_not_applied` | плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ |
|
||
| `length` | 6 | `length_mismatch` | ⬜ | ⬜ |
|
||
| `empty` | 6 | `empty_answer` | ⬜ | ⬜ |
|
||
| `sanitizer_stripped` | 7 | `markup_cleaned` | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
|
||
| `upstream_not_ok` | 6 | `unavailable` | ⬜ | ⬜ |
|
||
| незнакомая причина | 8 (по умолчанию) | **`unspecified`** (обязанность сервера, п.5; клиент рисует ту же нейтральную фразу, что и на незнакомый код) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
|
||
|
||
⚠ **Ряд `off_target_lang` (минор 0.10.0) — шестнадцатая причина, и его ранг ПРИЕХАЛ — испр. 05.09.**
|
||
Причина заведена движковым вайр-батчем (`backend/internal/pipeline/disposition.go`, греп
|
||
`FlagOffTargetLang`) и ранг **1** стоит в коде с лендингом движкового пака (`backend/internal/pipeline/status.go`, греп `FlagOffTargetLang`), и функция больше не цепочка `case`, а карта. Клетка теперь ФАКТ, а не обещание. Код `wrong_language` ратифицирован по ДОККОММЕНТУ, а не
|
||
по имени константы (правило 1): движок формулирует «the answer is not in the language that was asked
|
||
for», и это ровно то, что видит читатель, — не «артефакт исходного письма» и не «недоступно».
|
||
⚠ Имя не переиспользует `source_residue` намеренно: доккоммент прямо разбирает, почему повторное
|
||
использование эхо-флага было бы ЛОЖЬЮ о причине (D39.93 п.2), — беглый английский черновик
|
||
демонстративно не является артефактом исходного письма.
|
||
|
||
Причин шестнадцать. Ранги задаёт КАРТА `flagSeverity` (`backend/internal/pipeline/status.go`, греп
|
||
`flagReasonSeverity`), и она ИСЧЕРПЫВАЮЩАЯ по тесту `TestEveryFlagReasonIsRanked`: причина без ранга
|
||
валит батарею, а не тихо падает в 8. ⇒ ранг 8 (`severityUnknown`) остаётся ровно за одним случаем —
|
||
строка, которой ЭТОТ билд не знает. ⚠ **испр. 05.09 в две руки:** прежний текст говорил, что
|
||
`upstream_not_ok` своей ветки не имеет и падает в 8 — ложно (греп `FlagUpstreamNotOK`, ранг **6**);
|
||
поправка 05.09 дошла до абзаца, но НЕ до строки таблицы выше, и та ещё сутки держала «8 (по
|
||
умолчанию)». Класс известный — «поправил читателя, оставил писателя»; клетка и проза чинятся ОДНИМ
|
||
касанием. ⚠ Номер строки здесь намеренно не ставится: прежняя ссылка `: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 | ⚠ **испр. 05.09: ПОСТРОЕНО P7** — `Idempotency-Key` смонтирован и покрыт (см. §«ПОСТРОЕНО P7» выше); прежняя редакция «реализация — P7» читалась как ожидание |
|
||
| `bank_corrections_refused` | 409 | ⚠ **испр. 05.09: производитель ПОСТРОЕН** — дверь правок банка смонтирована паком P9 (D39.162; `platform/internal/runs/bank.go`), прежнее «объявлен ДО производителя» протухло. Раскладка отказа класса 14 движка (`tmctl bank-apply`, документ прочитан и отклонён целиком — пользователь пере-решает) |
|
||
| `bank_corrections_incomplete` | 503 | ⚠ **испр. 05.09: производитель ПОСТРОЕН** (тот же P9/D39.162). Раскладка класса 15 (документ принят, запись не довершена — слать ТОТ ЖЕ документ, ретрай сходится) |
|
||
| `content_refused` | 400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием `rejected` + `reject_reason` |
|
||
| `service_unavailable` | 503 | `runner.ErrCeilingNotWired` + `runs.ErrRunnerIncomplete` (`v0.go`) |
|
||
| `internal_error` | 500 | `v0.go` · `middleware.go` |
|
||
|
||
⚠ **Коды, которых платформа сегодня достигает, а контракт до 0.3.0 не объявлял:** 403
|
||
(`platform/internal/httpapi/server.go`, греп `CauseHandler(CodeForbidden`), 500
|
||
(`platform/internal/httpapi`, греп `Fail(w, r, CodeInternalError)` — `v0.go`, `middleware.go`,
|
||
`conditional.go`, `idempotency.go`, `bank.go`), 431 от `net/http` (`serve.go:79`, `MaxHeaderBytes`),
|
||
429 на `/auth` (`login.go:173,234`). Из них
|
||
контракт объявляет 403 (это нарушение правила, которое ИЗОБРЁЛ сам документ) и не объявляет 500
|
||
(Zalando: «500 Internal Server Error · use · **do not document**»), 431 (уровень stdlib) и 429 (на
|
||
`/v0` лимитера нет вовсе — форвард-вопрос платформы). `405` на `/v0` не бывает: catch-all глотает
|
||
метод.
|