1692 lines
212 KiB
Markdown
1692 lines
212 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.8.0 включительно (D39.169; перечень миноров и их
|
||
провенанс — ниже по файлу, здесь НЕ дублируется). Нормативная поверхность — openapi.yaml РЯДОМ.
|
||
⚠ Зонная копия frontend/docs/api-contract/ ВРЕМЕННО ОТСТАЁТ (0.2.3 при каноне
|
||
0.8.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 на молча срезанные при резке прозы правила (найдена одна, восстановлена).
|
||
Ратификацию этой редакции контрактная сессия за собой НЕ записывает — это акт оркестратора:
|
||
автор и ратификатор одного и того же текста совпадать не должны. Файлы уже несли 0.4.0 к моменту сдачи; в силу
|
||
редакция вступила лендингом 20.08 — РАТИФИЦИРОВАНА D39.152 (там же: четыре текстовые правки,
|
||
внесённые оркестратором при ратификации, и разбор того, что против 0.3.0 расходится ПЯТЬ мест,
|
||
а не одно). Строку в 05-decisions-log сессия тоже не писала по той же причине — готовый текст ноты
|
||
был сдан оркестратору вместе с этой редакцией.
|
||
|
||
Приёмка 0.2.0: три адверсариальных воркфлоу-пасса (черновик · дофикс round-1 · round-2),
|
||
19 подтверждённых находок, 0 опровергнутых, все вправлены; батарея фронта с
|
||
контракт-шагом (spectral + дрифт-тест генерённых типов) зелёная исполнением.
|
||
|
||
16.08.2026 (№17): ЦЕЛОСТНОЕ РЕВЬЮ КОНТРАКТА ПРИНЯТО (docs/research/28-contract-review.md,
|
||
D39.138) — исполнен ЛОМАЮЩИЙ батч 0.3.0 (состав — research/28 §5; строка 183; отчёт
|
||
исполнения — docs/archive/reports/CONTRACT_BATCH_0.3.0_REPORT.md). Тело этого файла
|
||
ПЕРЕПИСАНО под 0.3.0 вместе со спекой: держать его на 0.2.3 значило бы учить зоны неправде
|
||
(ровно то, что ревью и поймало — §5 ниже). Авторские формулировки фронт-сессии S3 сохранены
|
||
там, где их предмет не двигался; всё, что двигал батч, помечено «0.3.0».
|
||
Правило лендинга батча: канон первым, зеркало фронта отдельным зонным коммитом,
|
||
cmp-сверка обязательна (D39.138 п.3).
|
||
|
||
ТРИ ОПРОВЕРГНУТЫХ УТВЕРЖДЕНИЯ ПРЕЖНЕЙ РЕДАКЦИИ СНЯТЫ ЭТОЙ (не «поправлены» — сняты,
|
||
потому что каждое учило зону неправде):
|
||
1. §5 «переименована стадия / добавлена волна → фронт НЕ правится» — было ЛОЖНО для 0.2.3
|
||
(опровергнуто исполнением, research/28 Б-0). Ответ переписан, см. §5.
|
||
2. §3 «у чтения /bank сегодня нет канала вообще» — неверно с D39.122 (движок пишет сайдкар
|
||
всего банка); файл противоречил собственной строке. Снято, см. §3.
|
||
3. §3 «resume после стопа по потолку возвращает прогон в то же состояние, и лечения нет» —
|
||
вторая половина неверна: лечение существует и работает (новый прогон с бОльшим потолком).
|
||
Снято, см. §3 и описание `resumeRun` в спеке.
|
||
====================================================================== -->
|
||
|
||
# Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
|
||
|
||
> **Нормативная поверхность контракта — [`openapi.yaml`](openapi.yaml)** (файл рядом, в этой же
|
||
> папке). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято каждое
|
||
> решение, чем оно обосновано, что осталось открытым, и ГЕНЕЗИС форм (историю ратификаций,
|
||
> сверку со стандартами, разобранные альтернативы). При расхождении по ФОРМЕ побеждает YAML;
|
||
> при вопросе «почему так» — этот файл.
|
||
>
|
||
> **Статус: РАТИФИЦИРОВАН по 0.8.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, отчёт сессии — `docs/CONTRACT_MINOR_REPORT.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**.
|
||
> Дом канона — этот каталог;
|
||
> `frontend/docs/api-contract/openapi.yaml` — байт-зеркало.
|
||
>
|
||
> ⚠ **0.5.0 ломающий по построению (мажор `0`): снесены путь `POST …/bank/decisions` и три его
|
||
> схемы, из `BankPage` и `EventBank` сняты `pending_decisions`/`complete` (§2.19-бис), в
|
||
> `Capabilities.required` добавлен `bank_corrections_enabled`.**
|
||
>
|
||
> ⚠ **Оговорка «дверь объявлена И НЕ ОБСЛУЖИВАЕТСЯ» СНЯТА лендингом P9 (D39.162).** Платформа
|
||
> смонтировала `POST …/bank/corrections`; флаг `bank_corrections_enabled` теперь следует
|
||
> включённости прогонов (`platform/cmd/tmplatformd/runner.go:195` — `cfg.RunsEnabled()`), потому что
|
||
> дверь спавнит тот же глагол движка и без прогонов ей нечем работать. Деплой без прогонов
|
||
> по-прежнему честно отвечает `404`, и это тот же флаг, а не новый.
|
||
>
|
||
> ⚠ **Расхождение «точное по ПУТЯМ, не по полям» — ЗАКРЫТО лендингом P9 (D39.162).** Было: путь
|
||
> платформа снесла 22.08, а проекция `GET /bank` ещё клала на провод снятые 0.5.0 счётчики
|
||
> (`wireBankPage` в `platform/internal/httpapi/reading.go` — `PendingDecisions`/`Complete`, носитель
|
||
> `PD-399`). Полей в проекции больше нет, аллоулист-норма восстановлена.
|
||
>
|
||
> ⚠ **Урок оставлен НАМЕРЕННО, он переживает свой дефект: гейт версии этого класса не ловит.**
|
||
> `TestTheAnnouncedContractVersionIsTheOneTheCanonRatified` сверяет НОМЕР, а не поля — совпадение
|
||
> версий не значит совпадения форм, и следующее расхождение поля найдёт снова не он.
|
||
>
|
||
> ⚠ **0.4.0 ломающий ровно по одному месту, и это ЗАМЕРЕНО, а не объявлено.** Дифф генерённых
|
||
> типов 0.3.0 → 0.4.0 (`openapi-typescript@7`, комментарии отброшены) — **одна строка:**
|
||
> `Run.stop_requested: boolean`. Всё остальное в этой редакции — проза канона, одна запись в
|
||
> таблицу `resumeRun` и одно значение ВТОРОГО уровня (`cause.code: credit_unavailable`, а он по
|
||
> замыслу не закрыт и типов не двигает): корневой enum не сужен и не расширен, обязательных полей
|
||
> больше не появилось. Линтер зоны (`spectral --ruleset frontend/.spectral.yaml`) — 0 ошибок.
|
||
>
|
||
> ⚠ **Две границы этого замера, названные ревью диффа и обе честные.** (1) База — 0.3.0, а зонное
|
||
> зеркало фронта стоит на 0.2.3: при разморозке фронт платит переход 0.2.3 → 0.4.0 целиком, и это
|
||
> совсем другой объём (шапка ниже про отставание зеркала). Замер отвечает «что добавил ЭТОТ синк»,
|
||
> не «что стоит фронту разморозка». (2) Гейт §5 на утечку конвейерного словаря пере-ран и чист по
|
||
> сути, но «ноль вхождений» сказать нельзя: слово `edit` встречается дважды как обычный английский
|
||
> глагол («not an edit», «not a row edit»), `stage` — один раз в самой формулировке запрета. Ни
|
||
> одного нового вхождения этот синк не внёс.
|
||
>
|
||
> **0.x ломает миноры и будет ломать весь бета-период** (решение владельца 16.08, §8 п.17
|
||
> research/28): 0.3.0 — обычный ломающий минор, а НЕ последний перед 1.0. Окно 1.0 — после беты.
|
||
>
|
||
> **Язык.** Спека английская: из неё генерятся типы, а исходники фронта по конвенции английские
|
||
> (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской `K-N` — это те же
|
||
> вопросы §4. Спутник и остальные доки зоны — русские.
|
||
>
|
||
> **Зона строки 95 — «оркестратор/бэкенд/фронт».** Фронт авторитетен в одной трети: форма
|
||
> read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и работы
|
||
> движка здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют.
|
||
|
||
## 0. Пометки провенанса
|
||
|
||
| Пометка | Что значит |
|
||
|---|---|
|
||
| **✓ выведено** | следует из кода движка/платформы или ратифицированного решения; грунт `file:line` рядом |
|
||
| **◆ предложено** | решение автора контракта, разумное по его сведениям; подтверждает названная зона |
|
||
| **○ открыто** | развилка, на которую ответа нет; перечень — §4 |
|
||
|
||
⚠ Пометка ставится **на утверждение, а не на раздел**: у одного пункта половина бывает
|
||
выведенной, а половина предложенной.
|
||
|
||
**Пере-разметка 0.3.0.** Батч сдвинул четыре класса:
|
||
|
||
- `Progress` был ✓ (выведен из пофазной механики движка) — стал **◆**: одна полоса до ближайшей
|
||
остановки со знаменателем «купленный объём» это ПРОДУКТОВОЕ решение владельца (§8 п.1/п.8
|
||
research/28), а не проекция устройства движка. Это и было дефектом: форма ✓ означала, что
|
||
устройство движка обязано пролезать на провод;
|
||
- словари банка (`TermStatus`, `TermOrigin`) были ✓ по значениям — стали **✓ по различению, ◆ по
|
||
словам**: различение выведено из кода, слова назначены продуктом (движковые `auto`/`draft`/
|
||
`ruby`/`mined` на провод больше не идут);
|
||
- модель ошибок была ◆ (форма RFC 9457 как её понимал автор) — стала **✓ по словарю причин**
|
||
(класс 1 выведен перечислением реальных ветвей `platform/internal/httpapi/v0.go:333-427,542-574`)
|
||
**и ✓ по политике** (вариант B ратифицирован владельцем, §8 п.4);
|
||
- `GET /capabilities`, условные чтения, `Idempotency-Key`, `blocked` — **◆ целиком**: формы
|
||
назначены этим батчем, подтверждает платформа при P7.
|
||
|
||
---
|
||
|
||
## 1. Почему YAML, а не проза
|
||
|
||
Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им типизируются
|
||
моки. Прозаический контракт расходится с кодом ровно тем способом, ради предотвращения которого
|
||
заведена строка 95.
|
||
|
||
⚠ **Но машинного мало, и 0.3.0 это подтвердил дважды.** Ревью прогнало линтер по канону 0.2.3 и
|
||
получило «No results» — то есть **все 29 находок ревью были смысловыми, ни одной синтаксической**.
|
||
А К-11 замерил, что `if`/`then` OpenAPI 3.1 генератор типов ИГНОРИРУЕТ. Отсюда правило,
|
||
действующее с 0.3.0: **любое межполевое или условное правило обязано быть записано И схемой, И
|
||
словами в описании поля** — схема защищает сервер, слова доезжают до клиента. Мест таких три
|
||
(0.5.0 сменил первое: схема `BankDecision` снесена вместе с моделью): `BankCorrection` (`dst` при
|
||
`approve`/`decline` и взаимоисключение `id`/кортежа), `Unit.target`, агрегаты `BankPage`.
|
||
|
||
Инструменты и пины — `STACK_DECISIONS.md` §3 и бэклог зоны.
|
||
|
||
---
|
||
|
||
## 2. Решения и их происхождение
|
||
|
||
### 2.1. Язык — код, никогда не имя — ✓ выведено
|
||
|
||
Движок держит коды (`backend/internal/config/book.go:26-27`), ключ пары — `zh-ru`
|
||
(`configs/langpacks/zh-ru/`). Имя языка в данных — пар-специфика в общем слое, запрещённая §2
|
||
канона. Вторая цена, дороже: `lang` элемента берётся из данных книги, и пара ja→ru с именем вместо
|
||
кода отрисует кандзи китайскими начертаниями молча.
|
||
|
||
**0.3.0 дополнил:** форма кода — не то же, что умение пары. Платформа проверяет только ФОРМУ
|
||
(`platform/internal/books/books.go:118-121`) и равенство `source == target` не проверяет никто, а
|
||
пар-промпты в репозитории есть ТОЛЬКО для `zh-ru`; отсутствие промпта пары — жёсткая ошибка
|
||
конфигурации, не деградация («no prompt for pair %q role %q … never silently substitute another
|
||
pair's conventions», `backend/internal/config/pipeline.go:952-956`). До 0.3.0 книга в
|
||
неподдерживаемой паре принималась, ложилась на диск, разбиралась, проводила пользователя через
|
||
денежный экран — и умирала на старте прогона без причины. Отсюда `Capabilities.language_pairs[]`
|
||
со статусом и код отказа на интейке (`errors[].code: unsupported_pair`).
|
||
|
||
### 2.2. Идентификаторы непрозрачны — ✓ выведено, классы стабильности разведены в 0.3.0
|
||
|
||
`glossary.id` — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется
|
||
(`store/migrate.go:173-174`); номер главы плотный, «Chapters that yield no text … do NOT consume a
|
||
chapter number» (`chunk/chunker.go:99-105`), поэтому правка исходника сдвигает номера последующих
|
||
глав.
|
||
|
||
**0.3.0:** одно предложение «Stability across runs is the platform's job» покрывало пять сущностей
|
||
с разной по природе стабильностью и для юнита обещало заведомо больше возможного: движковый id
|
||
юнита несёт cut-tag, и «any chunker/budget/pipeline-shape change mints a new id for EVERY unit in
|
||
the book, while chapter ids survive» (`backend/internal/pipeline/manifest.go:102`). Теперь в схеме
|
||
`Id` три класса явно: книга/прогон/экспорт — навсегда; глава — переживает пере-разбор; **пара —
|
||
только внутри одного `structure_version`**, и клиент это НАБЛЮДАЕТ (Б-7), а не узнаёт по разъехавшимся
|
||
вкладкам. Стабильность по-прежнему обеспечивает платформа при персисте манифеста (строка 100).
|
||
|
||
### 2.3. Заголовок главы отдельным полем — ◆ предложено
|
||
|
||
**Движок сегодня делает ОБРАТНОЕ**, и это надо назвать прямо: титул рендерится детерминистически из
|
||
шаблона пары (`configs/langpacks/zh-ru/heading.txt`), исходный маркер вырезается из текста для модели
|
||
(`chunk/chunker.go:110-114`), а на экспорте титул вклеивается внутрь текста первого юнита
|
||
(`pipeline/export.go:215`). Комментарий движка прямо запрещает подавать этот рендер как метку книги
|
||
(`pipeline/manifest.go:80-86`). Развилка — К-2.
|
||
|
||
**0.3.0 снял противоречие, которое существовало независимо от К-2.** Спека 0.2.3 запрещала клиенту
|
||
синтезировать метку «Глава N» — а решение владельца 09.08 (`research/27` §33-36) требует ДВУХ меток:
|
||
оригинальной из данных книги плюс служебного рендера «Глава N» за $0 в локали ИНТЕРФЕЙСА. Сервер
|
||
локали интерфейса не знает (`Accept-Language` в платформе нет грепом), значит служебный рендер обязан
|
||
быть клиентским. Запрет снят; `Chapter.heading` = только метка ИЗ ДАННЫХ книги, `null` — обычный
|
||
ответ, и деплою прямо запрещено класть сюда рендеренный порядковый.
|
||
|
||
**`title_raw` и `kind` (глава/фрагмент) в 0.3.0 НЕ заводятся — передано дизайн-паку этапа 161**
|
||
(строка 161, очередь D39.136 п.3), и вот почему это не откладывание: (а) производителя настоящих
|
||
названий не существует — это Этап 0, строка 160; (б) форма узла «глава ↔ технический фрагмент» и
|
||
вердикт детекции — ровно тот предмет, который дизайн-пак и решает, а заложенная до него форма
|
||
заморозила бы догадку; (в) добавление обоих полей — АДДИТИВНОЕ расширение (минор), а смены СМЫСЛА
|
||
существующего `heading` не будет: он и сегодня, и после 160 означает одно — метку из данных книги.
|
||
**Запись для пака 161: контракт 14 расширяется этим паком аддитивно; `Chapter` получает `title_raw`
|
||
(если решится, что рендер и оригинал — разные поля), `kind`, вердикт структуры; ломать 0.3.0 для
|
||
этого не требуется.**
|
||
|
||
### 2.4. Состояние — у прогона; у главы выполнение — ✓ выведено
|
||
|
||
Подпись банка это один стоп на всю книгу (`pipeline/mining.go:81,243`), поэтому «глава ждёт подписи,
|
||
пока соседняя финализируется» — невозможная картина. У главы движок держит `ChapterPassport`
|
||
(`pipeline/status.go:37-55`).
|
||
|
||
### 2.5. Прогресс — ОДНА полоса до ближайшей остановки, в главах (0.3.0) — ◆ решение владельца
|
||
|
||
**Прежняя редакция (0.2.x): пофазно и в юнитах, ✓ выведено.** Основание было верное по факту:
|
||
«A unit is DONE when every member draft AND the unit's edit resolved ok» (`pipeline/status.go:328-331`),
|
||
редактура не стартует до стопа банка ⇒ сквозной счётчик стоял бы на нуле всю первую волну.
|
||
|
||
**Почему это всё равно был дефект (research/28 Б-0, вопрос владельца 16.08).** Пара `draft`/`edit`
|
||
была не абстракцией, а сквозным пробросом внутренней структуры движка до React-компонента —
|
||
`runevents.go:195-196` → `ingest/events.go:121,130-135` → SQL-констрейнт
|
||
`check (wave in ('draft','edit'))` (`00015_seam_ceiling_and_units.sql:56`) → `wireProgress`
|
||
(`v0.go:92-96`) → спека → генерённые типы → `format.ts:109-110`. То есть архитектура движка была
|
||
пришпилена к контракту в шести местах, а её изменение — ломающим для фронта.
|
||
|
||
**Решение владельца 16.08 («Согласен, переделываем» + В-5 «Ок, делаем так»):** одна полоса до
|
||
ближайшей остановки, знаменатель — КУПЛЕННЫЙ объём, после подписи банка полоса начинается заново.
|
||
|
||
**Форма, выбранная батчем: счёт в ГЛАВАХ, а не в юнитах.** Три довода, и второй — денежный:
|
||
|
||
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` — и его проекция** (было в 0.2.3, при резке прозы уцелело в каноне и
|
||
пропало здесь; возвращено синком 20.08). `kind` присутствует всегда и допускает `null`; `null`
|
||
значит «сервис не решил», строка при этом остаётся подписываемой, и клиенту запрещено и выбрасывать
|
||
её, и додумывать тип за движок (канон, схема `BankTerm.kind`). У движка та же строка несёт `Type:
|
||
""` (`membank/memseed.go:323-326`), поэтому **проекция `""` → `null` — работа платформы**; это
|
||
единственное место, где пустая строка и `null` означают одно, и потому оно записано.
|
||
|
||
**Фантом `auto` в провенансе убран ещё в 0.2.0:** комментарий схемы движка (`migrate.go:193`:
|
||
`seed|ruby|auto`) устарел — `"auto"` пишет СТАТУС, не провенанс (`membank/memseed.go:328`:
|
||
`Status:"auto", Source:"ruby"`), майнинг ставит `mined` (`pipeline/mining.go:424`).
|
||
|
||
**Ложный друг `source`.** У движка колонка `source` — это ПРОВЕНАНС. Поэтому в контракте провенанс
|
||
зовётся `origin`, формы термина — `src`/`dst`, а имя `source` в схеме банка не используется вовсе.
|
||
Переименование ради стиля здесь — самый дорогой класс правки, и ревью 0.3.0 его отвергло (§4 №25).
|
||
|
||
**Окно термина (0.3.0, Б-18).** `since_chapter`/`until_chapter` стоят на НОМЕРЕ главы, а номер сам
|
||
контракт называет не-ключом: нумерация плотная, правка исходника сдвигает хвост, и для книги без
|
||
нумерации номера легально нет. Перевести окно на `chapter_id` нельзя — оно входит в ключ уникальности
|
||
термина у движка (`store/migrate.go:202`), и смена ключа это работа движка, не контракта. Поэтому
|
||
записано ЯВНО: окно живёт в координатах текущего `structure_version` и пересчитывается при его
|
||
смене; два смысла нуля разведены на `null` («без границы»), потому что `Chapter.number` начинается с
|
||
единицы и `0` был сентинелом с двумя значениями в двух полях.
|
||
|
||
### 2.9. Подпись — набор решений — ✓ выведено
|
||
|
||
Конвейер заменяет банк целиком (`store/migrate.go:169-170`), поэтому `PATCH /term/{id}` молча не
|
||
работает. Механика дословно: «for EACH term either promote it into the mined-delta file OR decline it
|
||
in the mined-rejects file, then resume» (`pipeline/mining.go:81,243`; ⚠ фраза «the stop clears once
|
||
every proposed term is promoted or rejected» ОПРОКИНУТА D39.144 — движок авто-продолжает с
|
||
неподписанным, D39.42 п.3). Отсюда живое: решение на термин · информационный счётчик «решено N из M»
|
||
· частичное сохранение ◆. Запрет «продолжить» при неполном наборе — СНЯТ (подпись = один resume).
|
||
|
||
**0.3.0 — две правки:**
|
||
|
||
- **`promote` → `approve`.** `promote`/`decline` — дословно глаголы оператора майнера из строки выше,
|
||
и `promote` порождал `status: approved` — два слова на один акт. Теперь `approve` → `approved`.
|
||
- **Снимок стопа переехал в ЧТЕНИЕ банка** (Б-14а). ⚠ Пере-писано D39.144 (модель владельца 16.08:
|
||
подпись — ОДИН акт над всем банком): `complete`/`pending_decisions` — ИНФОРМАЦИОННЫЕ, `resumeRun`
|
||
снимает стоп при ЛЮБОМ состоянии решений, 409-полноты НЕТ. Прежняя формулировка — «поле, по
|
||
которому решается, можно ли предлагать „продолжить“» — существовало только в
|
||
квитанции POST. Экран, перезагруженный посреди стопа, мог прочитать ВЕСЬ банк и не узнать, сколько
|
||
решений осталось; единственная реализация выводила признак как `left === 0`
|
||
(`frontend/src/mock/handlers.ts:203-204`) — инвариант, которого контракт не объявлял. Теперь
|
||
⚠ **(0.5.0: клауза счётчиков этого пункта ПЕРЕКРЫТА — прежде чем читать дальше.)** Сняты со
|
||
всех трёх носителей; честный счёт едет квитанцией двери правок (`signature`), разбор —
|
||
§2.19-бис. Исторический текст 0.3.0: `pending_decisions` и `complete` отвечает и `GET /bank`,
|
||
и квитанция, и кадр `bank`.
|
||
|
||
`POST /runs/{id}/resume` — нормативная операция, а не резерв.
|
||
|
||
⚠ **Открытый остаток (заведён батчем 0.3.0, пере-привязан 0.5.0 к живым носителям).** Чтение банка
|
||
не отвечает, КАКИЕ строки уже решены: `TermStatus` — состояние строки, решения живут файлами
|
||
движка (дельта и список отказов — `18-bank-ontology.md`, роли ИСТОЧНИКОВ), и на провод пер-строчная
|
||
решённость не проецируется ни одним полем; счётчик «сколько осталось» с 0.5.0 отвечает только
|
||
квитанция двери правок (`signature`), не чтение. Экран подписи, перезагруженный посреди стопа,
|
||
знает суммарный счёт лишь после первого своего вызова двери и не знает, какие строки решены.
|
||
Лечение — одно поле `BankTerm.decision`, и оно НЕ добавлено: слово владельца 15.08 «добавочные поля
|
||
`BankTerm` НЕ заводить» (D39.136 п.4б) прямо это запрещает; публикация решённости движком — заказ
|
||
на день, когда экран подписи закажут (`18-bank-ontology.md`, «Чего эта форма НЕ несёт»). Решение —
|
||
за владельцем.
|
||
|
||
### 2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
|
||
|
||
**✓ выведено:** правило ре-синка — идемпотентный апсерт по `(run_id, seq)`, канал согласования —
|
||
`status --json` (`research/23` §2, §8; D39.85).
|
||
|
||
**◆ предложено:** что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН. Обоснование —
|
||
гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом, а рефетч по возврату
|
||
фокуса окна у ратифицированного `@tanstack/react-query` включён по умолчанию.
|
||
|
||
**0.3.0 — два уточнения, оба выведены из построенного клиента:**
|
||
|
||
- **Ревизия страничного обхода — МИНИМУМ по страницам, не максимум.** Правило жило только
|
||
комментарием в `frontend/src/api/client.ts:99-106` («The revision of a torn list is its OLDEST
|
||
page»), то есть второй клиент его бы не узнал. Теперь оно в схеме `Revision`.
|
||
- **Агрегаты банка — на ПЕРВОЙ странице** (Б-12). `Bank` обязывал нести `total`/`signed` «в целом по
|
||
банку» на КАЖДОЙ странице, при этом у каждой страницы своя `revision`, а правила согласования не
|
||
было — и референсный клиент неизбежно смешивал два момента времени: счётчики брал с последней
|
||
страницы (`queries.ts:144-148`), ревизию — с первой. Обе половины написаны осознанно и обе верны по
|
||
отдельности. Первая страница — единственный выбор, согласованный с правилом выше, и он не требует
|
||
от сервера держать снимок между запросами.
|
||
|
||
**Кадры соединения не тратят номеров истории.** `id` кадра объявлен позицией в истории событий
|
||
КНИГИ, строго возрастающей, — но `hello` приходит первым на КАЖДОМ подключении, а `end` и
|
||
`resync_required` тоже принадлежат соединению, не книге. Первая редакция батча этого не развела, и у
|
||
реализатора оставалось два пути, оба против текста: минтить служебным кадрам книжные номера (тогда два
|
||
одновременных зрителя тратят номера друг друга, и `Last-Event-ID` одного указывает на кадры, которых
|
||
второй не видел) либо повторять последний номер (тогда «one per frame» ложь). Поймано кросс-модельной
|
||
линзой; исправлено: служебные кадры несут id последнего кадра ИСТОРИИ и своего номера не тратят,
|
||
повтор id на них легален, а дыра в нумерации легальна из-за склейки — и клиенту прямо запрещено читать
|
||
пропуск как потерянный кадр. Последнее правило было в 0.2.3, потерялось при резке прозы и возвращено.
|
||
|
||
**Дельта-чтения ВКЛЮЧИТЕЛЬНЫ (`>=`), а не строго больше.** Первая редакция батча сделала
|
||
`?after_version=` строгим — и тем сломала собственное правило `Revision` («catch-up reads `>= R`, not
|
||
`> R`»): одна транзакция это одна ревизия, но НЕСКОЛЬКО строк, и строгое сравнение теряет соседей
|
||
последней применённой. Хуже: кадр `bank` предписывал читать строки «ревизией этого кадра», что при
|
||
строгом сравнении всегда возвращало пустоту. Поймано холодным потребителем, исправлено: чтение
|
||
включительно, повторно пришедшая строка безвредна (строка заменяется по `id`), а водяной знак
|
||
следующего чтения берётся из КОНВЕРТА, а не выводится из строк.
|
||
|
||
⚠ **Дофикс 16.08 (ФБ-4): у многостраничного обхода конверт не один.** Формулировка «водяной знак —
|
||
`revision` конверта» была однозначна ровно до тех пор, пока чтение умещалось в одну страницу; на
|
||
рваном чтении она сталкивалась со вторым правилом того же раздела — «ревизия рваного списка это
|
||
ревизия СТАРЕЙШЕЙ страницы». Два правила давали два разных числа, и клиент, взявший новейшее, молча
|
||
терял строки, изменившиеся между первой страницей и последней. Сведено в одно: **и гард свежести, и
|
||
водяной знак — это НАИМЕНЬШАЯ ревизия, увиденная за обход**; на одностраничном ответе это его
|
||
собственная. Направление выбора — безопасное: лишнее перечитывание бесплатно (строка заменяется по
|
||
`id`), пропуск строки — нет. Там же заявлено равенство `BookDetail.revision` и
|
||
`BookDetail.book.revision`: карточка собирается одной транзакцией с книгой, и клиент вправе брать
|
||
любое. Устаревший водяной знак
|
||
(коллекцию заменили целиком) отвечает `400` c `cause.code: version_too_old` — тем же ответом и с тем
|
||
же смыслом, что мёртвый курсор.
|
||
|
||
**Структурная версия — новая ось (0.3.0, Б-7).** Курсор был привязан к «STRUCTURAL epoch of the
|
||
collection», серверу вменялся MUST-отказ по мёртвому курсору, — а слово «epoch» встречалось в
|
||
документе РОВНО ОДИН раз: ни один ответ эпохи не нёс, наблюдать её было нечем. При этом структура
|
||
живая: «число глав может измениться против эвристики» (`research/27:35`, решение владельца 09.08).
|
||
Теперь `structure_version` едет **на КАЖДОМ кадре** (`EventBase`) и в `Book`, `ChapterPage`,
|
||
`UnitPage`, `NotePage`, `BankPage`; на исчезнувшую главу отвечает `410`; к смене версии привязаны
|
||
курсор, якоря на пары, окно термина и идентичность строки банка. ⚠ Первая редакция батча положила
|
||
версию только в `hello` и два конверта — и тем оставила ДВЕ свои же обязанности («перечитать банк,
|
||
когда версия сдвинулась», «уронить якоря на пары») без единого триггера, а `BankPage`/`NotePage` —
|
||
без указания, к какой структуре относится их содержимое. Поймано обеими линзами селф-ревью,
|
||
исправлено.
|
||
|
||
### 2.11. Разрыв потока — ◆ предложено; канал перевешен на КНИГУ в 0.3.0
|
||
|
||
При переподключении клиент шлёт `Last-Event-ID`. Если сервер докачать не может — обязан ответить
|
||
`resync_required`, а не молча начать с текущего момента: реплей истории запрещён, иначе разовое
|
||
событие вроде `note` теряется молча.
|
||
|
||
**0.3.0 — четыре правки одного канала (Б-6), все Ц0: канала нет ни строкой** (`grep
|
||
text/event-stream` по не-тестовому Go платформы — только комментарии).
|
||
|
||
- **Поток перевешен с прогона на книгу.** Книга в `uploading`/`parsing` прогона не имеет по
|
||
построению: строка прогона создаётся только в `StartRun` (`pgstore/runs.go:66,80`), весь разбор
|
||
живёт на строке книги (`books.go:180,237,283`). Прогонный поток не мог сообщить конец разбора в
|
||
принципе, и клиент лечился опросом раз в 3 с (`queries.ts:64-75`) плюс вторым хуком
|
||
(`useIntakeEnd.ts:31`) — а разбор настоящей книги это минуты. Это Ф-56, и чинится он не новым
|
||
кадром, а перевеской канала: конец разбора становится обычной сменой статуса.
|
||
⚠ **Прогонный поток узким видом НЕ оставлен** (Б-6 предлагал оставить). Довод: второй канал с теми
|
||
же кадрами — вторая реализация и второй источник расхождения, а адресация «кадры этого прогона»
|
||
выводится из книжного потока клиентом, у которого id прогона уже есть. §5а того же ревью требует
|
||
резать, а не добавлять поверхность.
|
||
- **Конец потока объявлен.** Терминального кадра не было, `204` в ответах не объявлен — при том что
|
||
SSE именно им останавливает переподключение («a client can be told to stop reconnecting using the
|
||
HTTP 204 No Content response code», WHATWG). После завершённого прогона браузер переподключался бы
|
||
вечно. Теперь: кадр `end` + `204` на переподключение с `Last-Event-ID` от завершённого потока;
|
||
запрос БЕЗ `Last-Event-ID` всегда открывает новый поток — иначе клиент не смог бы начать смотреть
|
||
снова после старта прогона.
|
||
- **`id` кадра и ревизия книги разведены.** Спека просила «a 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`): контракт требовал его только для ответов с переводом, то есть был беднее кода.
|
||
(б) Ссылка экспорта получила НОРМУ доступа вместо прозы: минтится под этот ответ и под
|
||
аутентифицированного владельца, не индексируется, истекает в `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:187-188`) и вторую книгу не
|
||
блокирует.
|
||
|
||
### 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` |
|
||
|
||
**Решения этой сессии (◆), каждое с доводом; подробный разбор и отвергнутые альтернативы — отчёт
|
||
`docs/CONTRACT_MINOR_REPORT.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` общего вида); движковый исход `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` — платформа обязана мерить оба на ПРОВОДНОЙ форме до
|
||
спавна; остаточный случай (HTTP-тело < 1 МиБ, отрендеренный документ шва — больше) падает в
|
||
`409` `bank_corrections_refused`, чьё описание несёт «a set larger than the service applies in
|
||
one act». ⚠ **Здесь монтаж и ошибся:** он мерил ПРОВОДНОЕ тело, а движку отдавал документ,
|
||
отрендеренный `json.Marshal` с HTML-экранированием (`&`/`<`/`>` — один байт в шесть), так что
|
||
законное тело под проводным потолком перепрыгивало движковый и возвращалось `409` «пере-решите»
|
||
вместо канонного `413`. Найдено воркфлоу-ревью P9 замером (не рассуждением: два агента ошибочно
|
||
положили ось в «не опровергнуто», рассуждая от `omitempty`), вылечено выключением экранирования —
|
||
документ читает движок, не браузер; (б) канонный `400` с `errors[/book_id]` на несовпадение тела с путём производит
|
||
ПЛАТФОРМА до спавна — движковая сверка того же факта (`bankdecisions.go:267-269`) есть класс 10,
|
||
«конфиг вызывающего», и в `400` сама не раскладывается; (в) свои вызовы двери по одной книге
|
||
монтаж СЕРИАЛИЗУЕТ сам: флок движка держит любой глагол, включая второй `bank-apply` и превью
|
||
(класс 12 = «another tmctl owns this project», не «книга переводится»), и без сериализации
|
||
транзиентный держатель отвечал бы `run_in_flight` — словом про прогон, которого нет. С
|
||
сериализацией снаружи остаётся ровно живой прогон, и слово точное.
|
||
- **Причины пер-решенческих отказов едут `refusals[].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`.
|
||
|
||
⚠ **Ложная перекрёстная ссылка, снятая этим же минором.** `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:733-744` — его собственный комментарий предупреждает, что
|
||
правка файла, ещё не прогнанная, здесь не отражается). Свёртка происходит внутри СЛЕДУЮЩЕГО
|
||
`translate`, поэтому сразу после правки движок честно отвечает «ничего не двигалось». Смета вернётся
|
||
на провод вместе с движковым глаголом «свернуть банк и оценить ВНЕ прогона» — до тех пор согласие
|
||
даётся ДЕНЬГАМИ: потолок пере-прохода равен холду, который пользователь уже внёс.
|
||
|
||
⚠ **Форма полосы объявлена, а не выведена, и причина названа в самом каноне.** `total` = 1, `done` =
|
||
0 → 1 на чистом финише. Первое решение оркестратора («полоса в главах») ОТМЕНЕНО эрратой 28.08-к:
|
||
движок анонсирует работу ОДИН РАЗ за жизнь книги, поэтому пере-проход, который переделывает уже
|
||
анонсированное, новых анонсов не производит — глава-полоса стояла бы на нуле навсегда. Более дробная
|
||
полоса была бы полосой, которая не движется.
|
||
|
||
---
|
||
|
||
## 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`) — совпало. Исчерпанность: платформа отвечает `202` и возвращает прогон в то же состояние (`reconcile.go`, оба выхода `exhausted` из `reopen` → ветка `exhausted` в `Resume` → `httpapi/v0.go`, хендлер `resumeRun`), а контракт теперь требует `409` `run_not_resumable` · `cause.code: ceiling_reached`. Молчаливый `202` на действие, которое ничего не сделало, — ровно то, от чего предупреждает собственный комментарий платформы; клиенту нечем отличить успех от no-op, а на `awaiting_bank` это ещё и не вызывает `ReleaseBankStop`. Полная таблица по статусам — в описании `resumeRun` | **вход P7** (правка построенного пути, не только читающей поверхности) |
|
||
| `GET /usage` | кредиты | ПОСТРОЕНО, **не читается ни одним экраном** | зона фронта |
|
||
> ⚠ **ТАБЛИЦА ПЕРЕ-СНЯТА ПРОТИВ КОДА 22.08 (оркестратор №18, аудит доков): шесть строк объявляли НЕ ПОСТРОЕННЫМ то, что пак P7 построил, а P8 сохранил.** Правило §Б-21 требует вести здесь статус построенного — оно нарушалось пятый раз подряд, потому что носитель «вход P7» никто не гасил при лендинге. Носитель израсходован: пак P7 принят D39.153, пак P8-FIX — D39.154.
|
||
|
||
| `GET /capabilities` | конфигурация деплоя | **ПОСТРОЕНО P7** — маршрут смонтирован безусловно (`platform/internal/httpapi/v0.go`, таблица `contractSurface`), хендлер `capabilities.go`; версия контракта запинена ГЕЙТОМ против ЭТОГО канона (`platform/internal/gates/contract_test.go`) | закрыто D39.153 |
|
||
| `PATCH`/`DELETE /books/{id}`, `GET /runs/{id}` | колонки есть | НЕ ПОСТРОЕНО (заведено 0.3.0) | вход P7 |
|
||
| `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 |
|
||
| `GET /books/{id}/bank` | движок пишет сайдкар всего банка (`pipeline/bankexport.go:16-33,72`, D39.122) | ⚠ пере-снято 0.5.0: проекция платформы ПОСТРОЕНА (P7 — маршрут в `contractSurface`, `wireBankPage` в `httpapi/reading.go`, `SaveBank` в `pgstore`; прежняя запись «не хватает проекции» устарела при израсходованном носителе «вход P7», D39.153); форма синхронна канону с лендингом 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` | у движка только stdout-JSON и `--plaintext` (`cmd/tmctl/invocation.go:107`) | НЕ ПОСТРОЕНО с обеих сторон | строка 49 / D29.1 «tmctl export-контракт» |
|
||
| Условные чтения (`ETag`/304), сжатие | — | **ПОСТРОЕНО P7** — `httpapi/conditional.go`; валидатор считается от БАЙТ ответа. Остаток строки 186 — шаги 3–5 (скоуп кадра, дельта-чтение, `staleTime`) | шаги 1–2 закрыты D39.153 |
|
||
| `Idempotency-Key` | — | **ПОСТРОЕНО P7** — `httpapi/idempotency.go` + `pgstore/idempotency.go`, на двух создающих вызовах; ⚠ живой дефект под конкуренцией — `PD-369` | закрыто D39.153 |
|
||
| `bearerToken` — чем ВЫДАЁТСЯ токен | вход только ставит HttpOnly-куку (`login.go:338`) | сервер токен ПРИНИМАЕТ, выдать его нечем | носитель: research/28 §2 (Б-15); строки нет |
|
||
| `Problem.localized` | — | объявлено, ни одним кодом не используется | носитель: research/28 §8 п.4 |
|
||
| `Note.code` как enum спеки | карта приложения А | не enum, пока не написаны фразы | строка 148 (фразы владельца) |
|
||
| Настоящие названия глав (`Chapter.heading` ≠ null) | парсер структуры | НЕ ПОСТРОЕНО | строка 160 (Этап 0) |
|
||
| `title_raw` / `kind` (глава ↔ фрагмент) | дизайн-пак структуры глав | передано паку, аддитивно | строка 161 |
|
||
| `ErrorCode.content_refused` (400) **и** `RejectReason.content_refused` | прескрин злоупотреблений | НЕ ПОСТРОЕН ни на одной стороне: в платформе только объявление константы (`httpapi/problem.go:42,94`), `ContractRejectReason` (`ingest/vocabulary.go:59-69`) его не отображает; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ (`disposition.go:60-63` → `Note.code: content_withheld`) и в exit-контракт не выходит — мостá между двумя словарями нет | **строка 94 (ПТ-16)**; там же ограничение числа попыток аккаунта — обязанность падает ВМЕСТЕ с производителем, не раньше |
|
||
| `decline` в подписи банка доезжает до работы | ⚠ пере-снято 0.5.0: старый носитель («таблица `bank_decisions`, движок её не читает») умер вместе с моделью — таблица снесена 22.08, предупреждение на `BankDecision.action` снесено вместе со схемой | дверь смонтирована (D39.162), и decline доезжает до следующего прогона ПО ПОСТРОЕНИЮ. ⚠ **Остаток ДВИЖКОВЫЙ, две штуки, обе найдены воркфлоу-ревью P9:** decline не энтити-широк на обратном пути (отклонённая сущность возвращается через свой АЛИАС, `mining.go:548`), и повтор ТОГО ЖЕ документа на одном пути не сходится (`membank/decisions.go:374`) — второе ломает обещание сходимости, на котором стоит синхронная дверь | заказано бэкенд-паку «тихая порча», `docs/BACKEND_SILENT_HARM_SESSION_PROMPT.md` §3.3 |
|
||
| Снятие замечания (переход «флаг снят») | движок | **НЕДОСТИЖИМО сегодня, проверено чтением движка** — п. H §6в | **PD-298** регистра платформы (`platform/docs/DEFECT_REGISTER.md`) — там строка и живёт; механизма не строим |
|
||
| Счёт «сделанного» на деплое без второго прохода | платформа | канон 0.4.0 определил «сделано» = последний проход ЭТОГО деплоя; проекция платформы считает жёстко второй проход | **вход P7** (PD-202) |
|
||
|
||
⚠ **Три прежних предупреждения СНЯТЫ как устаревшие** (Б-7а), и это причина, по которой заведена
|
||
таблица выше:
|
||
|
||
1. «`GET /bank` — канала нет вообще» — неверно с D39.122: сайдкар пишется. Файл при этом противоречил
|
||
сам себе (строка таблицы против абзаца ниже неё), а зона фронта до сих пор учится по старой
|
||
версии (Ф-43).
|
||
2. «`EventNote` — движок не эмитит пер-юнитных замечаний» — эмиттер приземлился 14.08.
|
||
3. «`EventCeiling` — зависит от эмиттера» — то же (кадр при этом снят по §5а, см. §6).
|
||
|
||
⚠ **Четвёртое было ХУЖЕ устаревшего — оно было НОРМАТИВНЫМ и отнимало работающее лечение.** Спека
|
||
0.2.3 писала: механизма поднятия потолка нет, «so the client MUST NOT offer resume as the remedy for
|
||
`paused`», — и не называла НИКАКОГО другого действия. Первая половина верна: `resume` действительно
|
||
не двигает такой прогон. Вторая — нет: стоп по потолку ЗАКРЫВАЕТ прогон, `finished_at` пишется тем же
|
||
оператором (`pgstore/runs.go:672-676`), `paused` входит в допустимые для старта состояния
|
||
(`runs/runs.go:227-231`, allowlist `readyToTranslate`), `HasLiveRun` при этом ложь — то есть **новый
|
||
прогон с бОльшим потолком запускается и является лечением уже сегодня**. Пользователь видел тупик там,
|
||
где его нет, на самом частом остановочном состоянии. В 0.3.0 лечение записано в `startRun` и в
|
||
`resumeRun`, а `resume` после потолка отвечает 409 с `cause.code: ceiling_reached`.
|
||
|
||
---
|
||
|
||
## 4. Открытые вопросы
|
||
|
||
| # | Вопрос | Статус |
|
||
|---|---|---|
|
||
| К-1 | Словарь статусов | **✅ ЗАКРЫТ D39.100**; 0.3.0 снял `finalizing` и развёл `RunStatus` — §2.6 |
|
||
| К-2 | Титул главы: поле `heading` или вклейка в текст | **○ ОТКРЫТ**, автор контракта + бэкенд. 0.3.0 закрыл ОТДЕЛЬНОЕ противоречие (запрет клиентской служебной метки снят), но кто производит настоящую метку — строка 160 |
|
||
| К-3 | Метка «Глава N» как единственная форма | **✅ ЗАКРЫТ D39.100** (метка — из ДАННЫХ книги). 0.3.0: служебный рендер «Глава N» разрешён КЛИЕНТУ, в локали интерфейса |
|
||
| К-4 | Ревизия: сквозная или пер-ресурсная; несут ли её чтения | **✅ ОТВЕЧЕН P0**, подтверждён ревью с поправкой: ревизия и позиция потока — РАЗНЫЕ величины, совмещать в `id` нельзя (0.3.0, §2.11) |
|
||
| К-5 | Показывать ли ETA | **✅ ЗАКРЫТ D39.100** (`eta_seconds` в спеке; 0.3.0 сделал поле required+nullable) |
|
||
| К-6 | Ступени замечания: сколько и где граница | **○ ОТКРЫТ, владелец.** Ответ 16.08: «показывать ВСЕ; в тексте сворачивать и раскрывать по кнопке; как именно — решим потом». **Словарь ступеней проектировать заранее НЕ надо** — батч его и не проектировал. Зависимость закрыта: `Note.id` заведён (0.3.0), без него конкретное замечание нельзя свернуть и запомнить |
|
||
| К-7 | Пагинация: курсор или один ответ | **✅ ОТВЕЧЕН P0.** 0.3.0 добавил недостающее: максимум `limit`, подрезание вместо понижения, объявленный порядок каждой коллекции, `maxItems` у `decisions` |
|
||
| К-8 | Стоп по потолку — каким статусом | **✅ ЗАКРЫТ D39.100** (`paused` + оповещение) |
|
||
| К-9 | Отказ прескрина не выразим статусами | **✅ ПРИНЯТО ВЛАДЕЛЬЦЕМ 16.08:** прескрин — ещё одна ПРИЧИНА, а не двенадцатый статус: `rejected` + ОДИН грубый код (`content_refused`), максимально абстрактно, без вариации между попытками (§8а). Заведено 0.3.0 |
|
||
| К-10 | Пофазность у главы | **✅ ЗАКРЫТ — вердикт «НЕ строить»** (D39.138, поправка приёмки research/28 №1). Пофазных счётчиков на главу не будет: фаз на проводе нет. Исходная жалоба («дерево читает ноль всю первую волну») лечится СЕГМЕНТНОЙ логикой `Chapter.units_done` — §2.5 |
|
||
| К-11 | Условная обязательность полей | **○ ОСТАТОК ИНСТРУМЕНТАЛЬНЫЙ.** 0.3.0 применил правило «схема + слова» к трём местам (`BankDecision.dst`, `Unit.target`, агрегаты `BankPage`; 0.5.0: место `BankDecision.dst` унаследовала `BankCorrection` — там теперь `if`/`then`/`else` и `oneOf` идентичности, все продублированы словами) и сделал адресацию `Note` обязательной. Остаток — генератор игнорирует `if`/`then`; лечится сужением на шве клиента (S5) |
|
||
| К-12 | Завершение выгрузки: опрос или событие | **✅ ОТВЕЧЕН P0 (опрос)**, подтверждён AIP-151 («The response must not be a streaming response»). 0.3.0 добавил то, без чего опрос не завершался: `state` вместо булева `ready`, `failure_code`, `expires_at` |
|
||
| К-13 | `paused_reason` не различает две беды | **✅ ЗАКРЫТ D39.132 п.2а** (`null` на проводе ратифицирован). 0.3.0 добил остаток: описание требовало «`null` in every other state», что противоречило ратифицированному «`paused` + `null`» |
|
||
|
||
---
|
||
|
||
## 5. Ревью-вопрос: **«Сменится устройство пайплайна — придётся ли править фронт?»**
|
||
|
||
⚠ **Прежний ответ этой таблицы был ОПРОВЕРГНУТ ИСПОЛНЕНИЕМ** (research/28 Б-0) и здесь не
|
||
сохраняется даже как история — он учил зону неправде. Что на самом деле давала редакция 0.2.3, если
|
||
в движок добавляли волну (реальный `format.ts` собран через vite и вызван):
|
||
|
||
- третья волна приезжала как новое поле, клиент «игнорирует неизвестные поля» — и
|
||
`translatedPercent` отдавал **100 %**, когда треть работы не сделана. Молча;
|
||
- переименование фаз или снятие редактуры → **TypeError в рендере** (`Status.tsx:39`, `About.tsx:96`),
|
||
а `ErrorBoundary`/`errorElement`/`componentDidCatch` во фронте нет ни одного (грепом пусто);
|
||
- незнакомую волну платформа тихо дропала (`pgstore/sink.go:206-208`) — прогресс занижался без ошибки;
|
||
- «правок ноль» на деле означало миграцию БД + шов + спеку + регенерацию типов (её принуждает
|
||
дрифт-тест `frontend/src/api/contract.test.ts:30-37`).
|
||
|
||
**Ответ редакции 0.3.0 — и теперь он верен, потому что чинили ПРИЧИНУ, а не формулировку:**
|
||
|
||
| Изменение в движке | Правит ли фронт |
|
||
|---|---|
|
||
| переименована стадия / добавлена/снята волна | **нет** — числа волн на проводе больше нет: одна полоса до ближайшей остановки, знаменатель — купленный объём (§2.5). Пофазный сплит остаётся ВНУТРИ платформы, колонки не трогаются |
|
||
| сменилась модель, маршрутизация, температура, промпт | **нет** — в allowlist не входят |
|
||
| добавлена новая причина флага | **нет** — на провод идёт КОД, карта живёт в контракте (приложение А); незнакомый код → нейтральная фраза, правило записано в схеме |
|
||
| добавлена новая причина отказа запроса | **нет** — второй уровень `cause.code` не закрыт по замыслу; клиент матчит корневой `code` |
|
||
| добавлен новый тип термина / новый провенанс | **нет** — словарь расширяется минором, ветка неизвестного стоит на шве |
|
||
| сменился чанкер, главы пере-разобраны | **нет по коду, ДА по данным — и теперь это ВИДНО:** `structure_version` двигается, курсоры и якоря на пары объявлены недействительными, на исчезнувшую главу отвечает `410`, полная замена — `resync_required`. До 0.3.0 клиент молча рисовал старое дерево |
|
||
| добавлено новое ПРОДУКТОВОЕ состояние | **да, один файл** — карта «статус → вид» на шве `src/api/`; это и есть контрольный вопрос владельца |
|
||
|
||
**Правило, которое отсюда следует и действует на КАЖДУЮ правку контракта (0.3.0):**
|
||
|
||
> **На проводе нет ни имён стадий/волн, ни движковых словарей — ни в полях, ни в ЗНАЧЕНИЯХ, ни в
|
||
> описаниях.** Описания компилируются в исходники клиента как JSDoc генерённых типов, поэтому
|
||
> объяснительная проза подпадает под тот же запрет, что и поля. Проверять грепом финальной спеки по
|
||
> списку: `draft · edit · wave · stage · mined · miner · ruby · finalizing · chunk · langpack ·
|
||
> sanitiz · flagged · verdict · prompt · engine · pipeline · glossar · escalat · snapshot` (список
|
||
> открытый — дополнять по мере находок). Единственное легальное вхождение — сама формулировка этого
|
||
> запрета в шапке спеки.
|
||
>
|
||
> **Гейт под это правило — тестом, по образцу языкового `generality.test.ts`** — половина ФРОНТА:
|
||
> `.spectral.yaml` держит только `spectral:oas`, а `generality.test.ts` ловит лишь языковую
|
||
> специфику, то есть утечку конвейера не стережёт ничто. Носитель: пинг фронту 16.08 в
|
||
> `frontend/docs/frontend-PROGRESS.md`, исполнение при разморозке зоны (D39.136 п.2).
|
||
|
||
---
|
||
|
||
## 6. Направления: что решено НЕ делать в 0.3.0
|
||
|
||
Записано направлениями, чтобы не превратиться в молчаливые дыры. **Номеров бэклога здесь не
|
||
выдумывается:** где строки нет, носителем назван документ.
|
||
|
||
| Что | Почему не в батч | Носитель |
|
||
|---|---|---|
|
||
| `POST /books/{id}/parts` — дописать главы в существующую книгу | форма и движковый гейт против МОЛЧАЛИВОЙ перекупки хвоста при вставке не в конец — отдельная работа вместе с этапом структуры глав | **строка 185** |
|
||
| Снятие жанра из брифа и промптов | двигает `BriefHash` ⇒ только в общее resnapshot-окно | **строка 184** |
|
||
| Сжатие и условные чтения НА ПЛАТФОРМЕ | контракт их объявил; включение — работа платформы | **строка 186** |
|
||
| Сворачивание замечаний в тексте под кнопку | зона фронта, при разморозке | пинг фронту 16.08 |
|
||
| История прогонов книги | **снята решением владельца 16.08: «в МВП не нужно».** Данные уже в Postgres и не удаляются, индекс `runs (book_id, started_at desc)` стоит — если поддержка попросит, это один read-путь | research/28 §8 п.10 |
|
||
| Двухшаговая загрузка (метаданные JSON → `PUT` байтов) | три из четырёх greenfield-дизайнов выбрали её, и при ней проблема порядка частей не существует. Ц3: переписывается построенный путь с обеих сторон. Направление на после-беты | research/28 §5 «не в батч» |
|
||
| Лента изменений структуры (`added\|removed\|moved\|split\|merged`) с наследниками | дизайн-пак структуры глав | **строка 161** |
|
||
| Поиск и фильтр по банку и дереву | нужны индексы (Ц2); сегодня клиент ищет в браузере, и это осознанно | research/28 §5 «не в батч» |
|
||
| «Грубая группа статуса» для эволюции словаря | эволюционный механизм без сегодняшней боли | research/28 §5 «не в батч» |
|
||
| Словарь ступеней замечаний | К-6: владелец — «решим потом» | research/28 §8 п.11 |
|
||
| Добавочные поля `BankTerm` (evidence/variants/conf, aliases) | слово владельца 15.08: «НЕ заводить … смысла хватает» | D39.136 п.4б |
|
||
| **Поток на БИБЛИОТЕКУ** (одна лента на все книги аккаунта) | поток книжный (§5 п.5 заказа — «перевесить на книгу»); экран библиотеки обновляется чтением, которое под условным чтением стоит заголовков. Ленты на библиотеку нет, и открывать поток на строку списка клиент НЕ должен — это записано нормой в спеке. Найдено холодным потребителем как реальный перф-вопрос | research/28 §5б · строка 186 |
|
||
| **Чтение ОДНОЙ главы** `GET /books/{id}/chapters/{chapterId}` | сегодня кадр `chapter` про главу, которой клиент не держит, игнорируется (норма записана), а `410` лечится перечитыванием дерева. Операции нет — она не в заказе; вопрос дизайн-пака структуры глав | **строка 161** |
|
||
| **Список экспортов** `GET /books/{id}/exports` | Б-4 просил СОСТОЯНИЕ экспорта, не список. Восстановление после перезагрузки закрыто иначе — `Idempotency-Key` на создании возвращает исходный `202` с тем же `Location` | research/28 §2 (Б-4) |
|
||
| Новое число страницы замечаний | **замера нет ни одного**: сколько замечаний даёт настоящая книга, не знает никто. 500 выбрано без числа, и выдумывать второе число вместо первого — та же ошибка. Число ушло из спеки в `Capabilities.page_size_default`; калибровка — после первого настоящего прогона | research/28 §7, §10 |
|
||
|
||
### 6а. Что РЕЗАЛОСЬ по §5а и что резать отказались
|
||
|
||
Снято: `Problem.instance` (структуры на платформе нет; функцию несёт `request_id`) · кадр
|
||
`EventCeiling` целиком (единственное поле `halted` всегда `true`, дубль кадра `status`, который уже
|
||
несёт `paused_reason`) · `EventHello.run_id` (поток теперь книжный) · `EventResyncRequired.reason` ·
|
||
схема `Counter` (одна полоса вместо двух) · `Book.genre` и `BookIntake.genre` (Б-23) · значение
|
||
`finalizing` · прогонный поток `GET /runs/{id}/events` (перевешен на книгу) · зашитые числа размеров
|
||
страниц из прозы операций · генезис-проза (история ратификации semver, две апологии RFC 9110,
|
||
объяснение имени `source`) — **ужата НА МЕСТЕ до ссылки на источник, а не перенесена сюда**: в спеке
|
||
остались `semver §4` и одна строка про `Retry-After` на `200`, целиком выброшенного текста нет.
|
||
Формулировка «перенесена в этот файл» держалась ровно один раунд и исправлена дофиксом (ФБ-10):
|
||
проверяется грепом — этих абзацев в компаньоне нет.
|
||
|
||
**Резать отказались, с контраргументом на каждое:**
|
||
|
||
- **параметр `limit`** («клиент не отправил его ни разу»). Тот же §5 требует объявить у него
|
||
`maximum` и подрезание вместо понижения (Б-10) — у удалённого параметра максимума не объявишь.
|
||
Плюс: «референсный клиент не шлёт» — свойство ОДНОГО клиента, а контракт пишется для второго.
|
||
- **`Note.unit_id`** («только в фикстуре мока; экраны берут `unit.note` вложенно»). Основание — «ни
|
||
один экран не читает», ровно тот довод, который эррата 16.08-г уже опрокинула на `TermStatus`:
|
||
экранов замечаний (S6) ещё нет. Адресация на пару — единственный способ перейти к МЕСТУ замечания
|
||
из плоского списка, а собственная фраза операции говорит «A note addresses a unit or a whole
|
||
chapter». Поле оставлено НЕОБЯЗАТЕЛЬНЫМ (Б-9 просил ровно это), а обязательным сделан `chapter_id`
|
||
— то есть дефект «замечание, не адресующее ничего» закрыт.
|
||
- **`Bank.signed`** («доезжает до клиента и не рисуется»). Тот же довод «нет экрана» — экран подписи
|
||
это S5. `total`, `signed` и `pending_decisions` — три НЕЗАВИСИМЫХ факта: строку можно решить и не
|
||
подписать (отклонить), поэтому `signed` не выводится из двух других. *(0.5.0: `pending_decisions`
|
||
снесён — §2.19-бис; довод о независимости `signed` стоит и без него.)*
|
||
- **нагрузка кадров `note` и `bank`** («передаётся и игнорируется»). Игнорировалась она по причине,
|
||
которую батч устранил: у замечания не было id, поэтому кадр нельзя было сопоставить со списком.
|
||
С `Note.id` кадр `note` несёт ПРИМЕНИМУЮ ДЕЛЬТУ — это ровно первая ветка правила Б-11а, и снятие
|
||
нагрузки вернуло бы перечитывание всего списка. Счётчики `bank` — вторая ветка того же правила
|
||
(«счётчик + скоуп»), а строки читаются дельтой `?after_version=`.
|
||
|
||
---
|
||
|
||
## 6б. Дофикс-раунд 16.08 (заказ приёмки D39.142): что изменилось в каноне и почему
|
||
|
||
Десять находок ХОЛОДНОГО ПОТРЕБИТЕЛЯ — линзы, которой у селф-ревью батча не было: только финальная
|
||
спека и проба генератором, без ревью, без компаньона, без диффа. Ниже — не пересказ правок, а их
|
||
основание; сами правки в каноне.
|
||
|
||
**Форма кадра была двусмысленна (ФБ-1).** `EventEnvelope{event,id,data}` читался и как объект на
|
||
проводе, и как описание SSE-фрейминга — оба чтения соответствовали тексту, и клиент по второму
|
||
прочтению искал бы JSON с полем `data` внутри. Теперь на схеме стоит дословный пример кадра и
|
||
сказано прямо: `event` и `id` — ПОЛЯ SSE, телом кадра является `data` и только оно. Это тот класс
|
||
дефекта, который не ловится ни линтером, ни генератором: документ валиден в обоих прочтениях.
|
||
|
||
**Книга в покое могла крутить переподключение вечно (ФБ-2).** Служебные кадры несут id последнего
|
||
кадра истории — а у книги, которая ещё ничего не производила, такого кадра нет. Клиент оставался без
|
||
`Last-Event-ID`, каждый его запрос был «новым потоком», сервер отвечал `hello`+`end`, браузер
|
||
переподключался — и так по кругу. Закрыто самой дешёвой из возможных мер: история нумеруется с `1`,
|
||
служебные кадры пустой книги несут `0`, а `Last-Event-ID: 0` на книге в покое попадает под уже
|
||
существующее правило `204`. Последовательность стала конечной: `hello` → `end` → закрытие → одно
|
||
переподключение → `204` → браузер останавливается.
|
||
|
||
**Обещание «`note` не теряется» не переживало разрыв (ФБ-3).** Внутри одного соединения кадр-добавление
|
||
защищён запретом склейки; между двумя соединениями — ничем: буфер не обещан, `resync_required` за
|
||
обычный реконнект не полагается. Обещание не расширено (буфер обещать нечем), а названа обязанность
|
||
клиента: после КАЖДОГО переподключения — дельта-чтение `/notes?after_version=…`. Механизм для этого
|
||
уже был; не хватало записи, что он обязателен, а не удобен.
|
||
|
||
**Пустые члены `allOf` давали необитаемые типы (ФБ-5).** `EventEnd` и `EventResyncRequired`
|
||
описывались как `EventBase` плюс пустой объект — генератор выводил `Record<string, never>`, и
|
||
пересечение становилось типом, значение которого построить нельзя. Заменено на чистый `allOf` из
|
||
одного члена с описанием на самой схеме; проверено генератором — оба теперь `EventBase`. Попутно
|
||
записано то, что раньше подразумевалось: эти два кадра НЕРАЗЛИЧИМЫ по форме, диспетчеризация только
|
||
по имени события.
|
||
|
||
**«Новый прогон» предписывался, а условия — нет (ФБ-6).** Спека велела предлагать новый прогон как
|
||
лечение стопа по потолку, но нигде не перечисляла, что делает `resume` в каждом останавливающем
|
||
статусе, а `paused_reason: null` описывался как «нейтрально, продолжаемо» — из чего клиент мог
|
||
заключить, что при `null` надо звать `resume`. Добавлена таблица по всем статусам и сказано прямо:
|
||
новый прогон легален при ЛЮБОМ `paused`, включая `null`; причина паузы — подсказка о прошлом, а не
|
||
разрешение на будущее. Значение enum при этом не заводилось — ратификация D39.132 п.2а не двигается.
|
||
|
||
**Пол под моделью ошибок (ФБ-7).** Весь §Errors описывал ответы, которые СООТВЕТСТВУЮТ контракту;
|
||
что делать с ответом прокси, HTML-страницей или оборванным телом — не говорил никто, а именно там у
|
||
клиента нет ни `code`, ни `request_id`. Записано правило того же вида, что и для известных кодов:
|
||
не показывать из такого ответа ни байта, рисовать свою нейтральную фразу.
|
||
|
||
**Пачка мелких (ФБ-8).** `title: null` в merge-patch — объяснено, почему это `400`, а не удаление
|
||
члена по RFC 7386 (книги без названия у этой поверхности не бывает) · `X-TM-Client` — записано, что
|
||
`required: true` стоит при живом исключении для bearer, потому что условной обязательности по схеме
|
||
безопасности OpenAPI не выражает, и валидатор не должен читать законное отсутствие как нарушение ·
|
||
список отказов интейка помечен неисчерпывающим (авторитет — `code` ответа) · правило резолюции
|
||
`Location` · ⚠ (ПЕРЕКРЫТО 0.4.0, см. §2.18) тождество `Idempotency-Key` на multipart считается по объявленным частям, а `408` не
|
||
считается состоявшейся попыткой · `min_chapters` при `max_chapters: 0` — не диапазон, клиент проверяет
|
||
максимум первым · порядок пяти носителей «нет кредита» · «carried forward marked as unverified» — снято
|
||
как обещание без носителя, заменено на наблюдаемое (`BankPage.signed` против `total`) · `409` у
|
||
экспорта объяснён как ключевой, а не книжный · неизвестный `term_id` в подписи отклоняет весь батч, а
|
||
не молча пропускает строку.
|
||
|
||
**Слова, которые называли не то (ФБ-9).** `parser_unavailable` называл наш компонент — переименован в
|
||
`processing_failed`, по эффекту: имя значения это то, на что клиент вешает фразу. ⚠ Проводное имя
|
||
теперь отличается от внутреннего словаря платформы — там значение зовётся `parser_unavailable`
|
||
(`platform/internal/books/parse.go:66`), и проекция обязана отобразить одно на другое; рядом там же
|
||
живут `schema_mismatch` и `storage_unavailable`, которые на провод не идут вовсе. Вход P7. Из описаний вычищены внутренние ссылки (`research/28 §…`,
|
||
«companion К-6»): описания компилируются в исходники клиента, и ссылка на ревью в чужом репозитории
|
||
там — мусор; носители остались здесь.
|
||
|
||
## 6в. Синк с платформой 20.08 → канон 0.4.0: что решено и почему
|
||
|
||
Вход — `platform/docs/archive/CONTRACT_SYNC_FROM_PLATFORM_2026-08-20.md`, четырнадцать мест, где провод сообщает
|
||
состояние и не даёт клиенту действия. Записка честно разделяет ФАКТ / ⚠ МНЕНИЕ ПЛАТФОРМЫ / ГРАНИЦА,
|
||
и разбор шёл по этой границе: ФАКТы перепроверены исполнением, МНЕНИЯ проверялись на ПОСЫЛКУ, а не
|
||
только на предложенную форму. Два мнения посылку не выдержали (B, часть A), одно оказалось
|
||
слабее собственного факта (L о метках глав).
|
||
|
||
⚠ **База ссылок этого раздела — РАБОЧЕЕ ДЕРЕВО 20.08, а не HEAD.** В `platform/` на этот момент 81
|
||
незакоммиченный файл (P7 в работе), поэтому одна и та же строка в HEAD и в дереве разная, и ссылка
|
||
без базы проверяема только случайно. `backend/` чист, его ссылки годятся в обоих. Там, где
|
||
конструкция переживёт любую перенумерацию, адрес дан ИМЕНЕМ функции или ветки — это дешевле и
|
||
надёжнее номера. Поймано ревью диффа: первая редакция §6в смешивала обе базы в одной строке таблицы.
|
||
|
||
### п.0. Сверка на потери прозы 0.2.3 → 0.3.0 — исполнена; найдена ОДНА потеря, она и была заявлена
|
||
|
||
Метод: полный дифф лендинга `8d82096` по обоим файлам (300 удалённых строк компаньона, 597 канона),
|
||
чтение ВСЕХ удалённых строк, выделение из них утверждений-правил и проверка каждого на наличие
|
||
смыслового двойника в живых файлах.
|
||
|
||
⚠ **Отдельно про метод, потому что он чуть не дал ложный результат.** Первый проход греп-проверки
|
||
шёл построчно и дал шесть «пропаж», которых нет: правило переносится через перенос строки, и
|
||
`grep "fewer rows than asked"` не находит текст, где `fewer` заканчивает строку. Сверка пере-ранена
|
||
на файлах, склеенных в одну строку (`tr '\n' ' '`), — и все шесть нашлись на месте. Тот же класс
|
||
ошибки в этой сессии уже ловился однажды; ставлю его сюда как метод, а не как случай.
|
||
|
||
Итог: **единственная потерянная НОРМА — фраза §2.14** («различать причины отказа клиент не может по
|
||
замыслу»), восстановлена. Остальное удалённое — три класса, и ни один не является потерей правила:
|
||
|
||
- **правило уцелело в КАНОНЕ, а канон нормативен** — `kind`/`null` («MUST NOT drop it or invent a
|
||
kind»), «MUST NOT clamp it again», «stale read is the CLIENT's duty», «catch-up reads `>=`»,
|
||
«assembling a book's text on the client is forbidden», «pairs are read PER CHAPTER», уникальность
|
||
термина по пятёрке, «a phrase MUST exist for a reason we do not know yet», `no-store`, CSRF,
|
||
запрет реплея истории, «`max_chapters: 0` = экран исчерпанности». Один из них — правило пустого
|
||
`kind` — уцелел в каноне, но потерял здесь ПРОЕКЦИЮ (`""` → `null` — работа платформы); клауза
|
||
возвращена в §2.8;
|
||
- **утверждение снято ОСОЗНАННО и с записанным основанием** — «carried forward marked as
|
||
unverified» (ФБ-8, обещание без носителя), «замена в `failed` запрещена» (ужато до «не `failed`»),
|
||
«Required: HTTP/2 at the edge» (Б-16, RFC 9205), пофазные счётчики, `EventCeiling`, `genre`,
|
||
`finalizing`, пример `«CJK leak … 第一节»` (он и был утечкой, §2.12);
|
||
- **пропала МОТИВИРОВКА, а не норма** — «1.9 юнита на главу», «иначе строки выглядят дубликатами и
|
||
удаляются», «portable to the desktop client», `Intl.DisplayNames`. Это цена резки прозы, принятая
|
||
осознанно; ни одно не меняет того, что обязана делать сторона.
|
||
|
||
**Вывод, который стоит записать отдельно.** Приёмка батча сверяла мультимножество модальных глаголов
|
||
и этот класс структурно не ловила — но пере-сверка показала, что улов класса равен одному. Отсюда
|
||
дешёвый гейт вместо дорогого: при следующей резке прозы сверять не модальность, а **список
|
||
собственных имён формы** (полей, значений, кодов, заголовков) — норма почти всегда стоит рядом с
|
||
именем, а фраза §2.14 пропала именно там, где имени не было (`/auth/*` — не поле). Носитель: эта
|
||
запись.
|
||
|
||
### A. Прогон с исчерпанным бюджетом (PD-282) — ПРАВЛЮ КАНОН; половину мнения отклоняю
|
||
|
||
**Факт подтверждён и оказался шире записки.** `reopen` даёт `exhausted` в ДВУХ местах, а не одном
|
||
(`runs/reconcile.go`): `remaining <= 0` и `ErrInsufficientCredit` от `RestartRun` — то есть
|
||
«кончился потолок прогона» и «нечем взять холд на счёте» уже сегодня схлопнуты в один вердикт, и
|
||
`Resume` отвечает на оба одинаково: `202` + неизменённый `Run` (`httpapi/v0.go`, хендлер
|
||
`resumeRun`). `paused` до этой ветки не доходит — он отказан выше, поэтому молчаливый `202` живёт
|
||
ровно на `stopped` и `awaiting_bank`. Худший из двух — `awaiting_bank`: человек подписал банк,
|
||
нажал «продолжить», получил «принято», а `ReleaseBankStop` не вызывался (зона это записала сама,
|
||
`archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md` §7(и)).
|
||
|
||
⚠ **Номера строк в чужой зоне в этом разделе сняты намеренно** (находка ревью): дерево платформы
|
||
на 20.08 держит незакоммиченные правки, поэтому одна и та же строка в HEAD и в рабочем дереве
|
||
разная, и половина ссылок первой редакции указывала в одно дерево, половина — в другое. То же
|
||
лечение, что у приложения А: адресуемся именем функции и ветки.
|
||
|
||
**Правка канона (§resumeRun).** Записана ВТОРАЯ ОСЬ: исчерпанность лимита прогона перебивает обе
|
||
строки `202`; ответ — `409` `run_not_resumable`, `cause.code: ceiling_reached`, лечение — новый
|
||
прогон. Новых значений enum не заводится: `ceiling_reached` уже есть и его собственный текст этот
|
||
случай описывает дословно. Тем же ходом канон получил обратную гарантию, которой не имел:
|
||
**`202` теперь означает, что работа действительно переоткрыта**, и клиенту не нужно второе чтение,
|
||
чтобы отличить успех от no-op. И записано разделение, о котором предупреждает сам канон у
|
||
`AccountHaltReason`: пустота СЧЁТА не сворачивается в `ceiling_reached` — у неё своя причина
|
||
`credit_unavailable` (аддендум ниже), а состояние аккаунта ЦЕЛИКОМ по-прежнему говорит `Usage`, не
|
||
этот вызов. *(Первая редакция раздела писала здесь «пустой счёт — не ответ этого вызова», что
|
||
противоречит и аддендуму, и канону; испр. оркестратором при ратификации.)*
|
||
|
||
**Признак «этот прогон ещё можно продолжить» на `Run` — ОТКЛОНЁН, с оговоркой.** Посылка «платформа
|
||
знает его точно» верна, посылка «дёшево» — нет: `ReadRun` — один запрос по `books join runs`
|
||
(`pgstore`, функция `ReadRun`), а флагу нужны ТРИ чтения, которых в нём нет — `RunSpent`,
|
||
`AttemptReservationOpen` и баланс счёта. `Run` сериализуется в карточке книги, а карточку канон
|
||
велит перечитывать НА КАЖДЫЙ кадр потока («it is still read on a frame, on navigation and on
|
||
focus»), то есть цена платится на горячем пути ради факта, который
|
||
(а) устаревает к моменту клика и (б) уже отвечен ратифицированным порядком чтения пяти мест
|
||
(§RunOptions): `max_chapters == 0` + `blocked` — это ровно «стоит ли предлагать старт сейчас и что
|
||
мешает», и рисует его тот же экран. Авторитет по-прежнему у мутации, как канон и объявляет.
|
||
⚠ Поправка ревью к предыдущему абзацу: сам `Run` в КАДРЕ не едет — ни один payload его не несёт;
|
||
горячий путь создаёт не кадр, а предписанная им перечитка карточки. Вывод не меняется, носитель
|
||
цены — другой; первая редакция называла его неверно.
|
||
**Триггер пересмотра:** замер, показывающий, что мёртвый клик частый, ЛИБО сворачивание трёх чтений
|
||
в коррелированные подзапросы того же запроса (агент проверил — возможно). Тогда форма — `Run`,
|
||
поле, минор.
|
||
|
||
### B. Суточный потолок движка — ОТКЛОНЯЮ ОБЕ ФОРМЫ; посылка не выдержала проверки
|
||
|
||
Записка просит слово или признак для паузы, которую деньги не лечат. Проверка посылки:
|
||
|
||
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`), и «денег не
|
||
хватает» из них не следует ни для одного. То есть класс закрыт, а дыра — только в четвёртом.
|
||
|
||
### C. Вход `/auth/*` — ИСПОЛНЕНО (§2.14)
|
||
|
||
Фраза восстановлена, вариант A уточнённый записан целиком, триггер пересмотра зафиксирован, словарь
|
||
в компаньоне НЕ объявлен. Разбор потери — п.0 выше. Возражение зоны про «нормативность с чёрного
|
||
хода» принято дословно и стало основанием.
|
||
|
||
### D. `Note.code` против пустой клетки — ПРАВЛЮ ОБА ФАЙЛА
|
||
|
||
Дыра реальна и была острее, чем в записке: канон не просто требует поле, он называет `Note.code`
|
||
«a closed vocabulary of this version», а карта в приложении А оставляла клетку пустой — то есть
|
||
сервер обязан прислать значение, которого словарь не содержит. `unspecified` ратифицирован как
|
||
ОБЯЗАННОСТЬ сервера (не строка словаря причин), записан и в канон, и в приложение А, п.5. Довод
|
||
записки про независимый выпуск принят. Прецедент D39.144 опорой не служит — согласен, его довод
|
||
(«спека ещё никем не потреблена») больше не верен.
|
||
|
||
**Родственный вопрос про ступени — НЕ решаю:** К-6 открыт, слово владельца 16.08 — «решим потом».
|
||
Но провизорность построенного зафиксирована в приложении А, п.6, чтобы `ingest/notes.go` не
|
||
читался как ратифицированная карта.
|
||
|
||
### E. Отпечаток интейка (PD-262) — ПРАВЛЮ КАНОН, форму НЕ трогаю
|
||
|
||
ФАКТ подтверждён дословно: `intakeFingerprint` берёт `r.ContentLength` (`httpapi/v0.go:511-517`), и
|
||
комментарий рядом (`:503-510`) сам называет оба остатка. Из двух исходов один безопасный (ложный
|
||
`key_reused` на другой границе multipart — честный ретрай отвергнут), второй нет: chunked-клиент
|
||
даёт `"unknown"` обоим запросам, и **две разные книги под одним ключом реплеят первую** — то есть
|
||
ключ возвращает ответ, принадлежащий чужому запросу. Это нарушение самой гарантии, ради которой
|
||
ключ существует.
|
||
|
||
**Выбрано: привести фразу канона к тому, что сервер может сравнить, БЕЗ поля размера в форме.**
|
||
Записано: «тот же запрос» = те же метаданные + то же ИМЯ файла + то же СОДЕРЖИМОЕ; чем сервер
|
||
устанавливает последнее — его дело (дайджест на лету стоит ноль, книгу в памяти держать не надо);
|
||
обрамление multipart и длина тела запросом НЕ являются; **сервер, который не может установить
|
||
тождество, не реплеит, а отвечает `409` `idempotency_conflict`**. Почему не поле размера: оно
|
||
дало бы клиенту ещё одно обязательное поле, не закрыв случай «две разные книги одной длины», и
|
||
всё равно потребовало бы сверки с фактически прочитанным. Механизм отката для этого уже построен
|
||
и назван зоной верно (`trailingParts` `v0.go:536-563` → `books.go:190-204`); регистр зоны сам
|
||
называет полное лечение «сверка принятого дайджеста на завершении с откатом» — контракт теперь это
|
||
разрешает вместо того, чтобы описывать деталь, которой в форме нет.
|
||
|
||
### F. `ETag`/`304` на любом безопасном чтении — ПРИНЯТО, канон правится одной фразой
|
||
|
||
Зона права дважды: RFC 9110 валидатор на любом GET разрешает, и список исключений в слое записи
|
||
протух бы за пак. Записано в шапке канона: валидатор на любом безопасном чтении легален и
|
||
объявления не требует; операции, которые его объявляют, — те, где клиенту есть смысл им
|
||
пользоваться, а не исчерпывающий список. Плюс две границы, чтобы фраза не стала лицензией:
|
||
клиент НИКОГДА не обязан слать `If-None-Match`, а `304` бывает ответом только на присланный.
|
||
`getUsage`/`getRunOptions` в объявленные НЕ добавлены сознательно — оба читаются ровно перед
|
||
действием, и условное чтение там не даёт ничего, кроме риска показать устаревшую шкалу.
|
||
|
||
### G. Draft-only конвейер (PD-202) — ОТВЕЧАЮ: ДА, начерновленная глава «сделана»; правлю канон
|
||
|
||
Проверено чтением обеих зон, и цена выше заявленной. У движка деплой без второго прохода — не
|
||
экзотика: разбиение выводится из РОЛЕЙ стадий (`pipeline/waverun.go:43-53`), конфиг требует лишь
|
||
«хотя бы одна стадия» (`config/pipeline.go:841`), ветка живая и покрыта тестом
|
||
(`contractblockers_test.go:115-136`); в репозитории такого конфига пока нет, но выбирает его
|
||
оператор, и платформа шаблон не читает. У платформы `units_done` — жёстко второй проход
|
||
(`pgstore/sink.go:252-263`, `readmodel.go:200-208`).
|
||
|
||
**Следствие, которого в записке нет:** ноль навсегда — это не только короткая полоса.
|
||
`Book.chapters_done` и `ChaptersLeft` (`pgstore`, функции `chaptersDone` и `BookRunContext`) читают
|
||
ту же колонку, а `ChaptersLeft` подрезает ШКАЛУ покупки. То есть на таком деплое сервис бесконечно
|
||
предлагает купить главы, которые уже переведены. Это выводит вопрос за рамки «косметика полосы» и
|
||
делает ответ обязательным.
|
||
|
||
⚠ **Первая редакция этого абзаца дописывала «и берёт за них деньги» — снято ревью как неверенное,
|
||
и снято ИЗ КАНОНА тоже** (там оно уже стало нормативным текстом, компилирующимся в JSDoc клиента).
|
||
Что проверено: платформа держит ПОТОЛОК, а не цену, и рассчитывается по ЗАМЕРЕННОЙ трате, а
|
||
неизменённая уже начерновленная книга переигрывается с чекпойнтов за $0 — то есть в обычном случае
|
||
холд возвращается целиком и не берётся ничего. Плата возникает только если между прогонами сдвинулся
|
||
снапшот (слаг модели, промпт, langpack, флип гейта), и тогда пере-покупка действительно оплачивает
|
||
переведённое заново. Довод «предлагают уже сделанное» стоит сам по себе и в поправке не нуждается;
|
||
денежный довод был сильнее фактов, и это ровно тот класс ошибки, который у меня уже ловился.
|
||
|
||
**Ответ контракта.** «Сделано» = последний проход, который эта книга на ЭТОМ деплое реально
|
||
получает. Записано в `Progress` и в `Book.chapters_done` — вместе с доводом: контракт не говорит,
|
||
сколько проходов бывает, значит и назвать один из них для счёта не может, а обещание «дробь
|
||
доходит до единицы» — это обещание, а не проходы. Имён волн в тексте нет (гейт §5 пере-ран, чисто).
|
||
Форма не меняется, генерённые типы не двигаются; работа — проекция платформы, вход P7.
|
||
|
||
### H. Снятие замечания (PD-298) — СВЕРЕНО С ДВИЖКОМ ПЕРВЫМ, как просила зона; механизма НЕ строим
|
||
|
||
Порядок соблюдён: сначала чтение движка. Результат — **переход достижим у драйвера и НЕ достижим на
|
||
проводе**. `flagged` вычисляется на каждой эмиссии, а не хранится (`pipeline/events.go:351,377-379`,
|
||
`waverun.go:139-140,205-206`), и `redrive`/`--resnapshot`/правка исходника пере-атакуют юнит, так
|
||
что второе разрешение законно может выйти `flagged=false`. Но эмиссия идёт через
|
||
announce-once-леджер: ключ `unit:<book>:<wave>:<chapter>:<unit>` не несёт ни снапшота, ни прогона
|
||
(`events.go:399-401`), `EnqueueOnce` вторую строку не пишет (`store/outbox.go:74-106`), леджер
|
||
переживает прогоны (`outbox.go:205-212`) и запинен тестом
|
||
(`runevents_test.go:242-246`). Единственное окно — падение между записью строки в журнал и
|
||
пометкой (`events.go:154-164,200-202`); плюс `redrive` платформа не вызывает вовсе
|
||
(`runner/engine.go:83-124`).
|
||
|
||
**Поэтому: строка «недостижимо» в регистр, а не механизм** — ровно тот исход, который зона назвала
|
||
правильным. Но в каноне закрыт РАЗРЫВ ПРАВИЛА, который эта проверка обнажила: §AfterVersion говорил
|
||
«удаление так не выразить» только про ОПТОВУЮ замену, а про исчезновение ОДНОЙ строки не говорил
|
||
ничего. Записано: строка, once delivered, поодиночке не отзывается; коллекция, которая обязана
|
||
потерять строку, теряет её единственным выразимым способом — заменой целиком с `resync_required`.
|
||
Это обязанность сервера, а не пожелание, и она делает поведение платформы выводимым: сегодня при
|
||
`flagged=false` строка обновляется и ревизия двигается (`pgstore/sink.go:237-244`), дельта её
|
||
прячет (`readmodel.go:725-735`), а `note_count` в кадре главы уже УМЕНЬШАЕТСЯ (`sink.go:310-328`) —
|
||
то есть клиент увидел бы счётчик, противоречащий списку. Теперь ясно, что должно произойти вместо
|
||
этого.
|
||
|
||
### I. `content_refused` — forward-looking, НО с носителем; правлю канон и §3
|
||
|
||
Ответ на вопрос зоны: **forward-looking, долг зона не заводит.** Значение ратифицировано К-9
|
||
(владелец 16.08) до того, как появился производитель, и это сознательно: форму отказа лучше решить
|
||
не под давлением. Проверено — производителя нет ни на одной стороне (в платформе только константа и
|
||
строка таблицы; в движке отказ провайдера живёт как ПРИЧИНА ЗАМЕЧАНИЯ и уезжает как
|
||
`Note.code: content_withheld`, `ingest/notes.go:45-48`, — другой словарь, моста нет).
|
||
|
||
Правки: (а) оба значения помечены в каноне как объявленные ДО производителя, с причиной;
|
||
(б) **обязанность ограничивать число попыток аккаунта пере-привязана**: она падает ВМЕСТЕ с
|
||
производителем, а не висит на сервере, который отказа не выдаёт, — иначе это обязательство без
|
||
адресата, ровно тот класс, из-за которого заведено правило Б-21; (в) §3 получил строку с носителем
|
||
(строка 94, ПТ-16). Клиент обрабатывает код с сегодня — старый клиент, встретивший его впервые, и
|
||
есть та беда, ради которой значение объявлено заранее.
|
||
|
||
### J. «Остановлено по вашей просьбе» — ПОЛЕ ЗАВЕДЕНО; форма отличается от предложенной зоной
|
||
|
||
Решение владельца 17.08 принято, форму делегировали контрактной сессии
|
||
(`archive/P7_ACCEPTANCE_HANDOFF_2026-08-17.md:462`). Заведено `Run.stop_requested` — **обязательное булево, а не
|
||
необязательное.** Довод: функция тотальна — прогон, который никто не просил остановить, это `false`,
|
||
а не «поля нет»; необязательность дала бы два написания одного факта, и это ровно тот дефект, который
|
||
контракт уже чинил у `sense` (0.2.2), у `reject_reason` и у `kind`. Ценой стал ломающий минор —
|
||
`Run` получил обязательное поле, — и он объявлен честно в шапке компаньона, вместо того чтобы
|
||
называть правку «аддитивной» и оставить генератор доказывать обратное. В описании записано и то,
|
||
чего в заказе не было: флаг — запись ПРОСЬБЫ, а не исхода, и остаётся `true` на прогоне, который
|
||
успел доработать, потому что это и есть случай, который экран обязан объяснить.
|
||
|
||
### K. `--verify-bank` — НЕ КОНТРАКТ, релей в зоны; но контрактно видимая половина закрыта
|
||
|
||
Расхождение «D39.144 (подпись = один акт) против пер-термного гейта движка» — ратификация и
|
||
поведение движка, обе вне этого файла: контрактная половина уже исполнена (D39.144/145 сняли гейт
|
||
полноты из канона и компаньона, `409` `bank_decisions_incomplete` удалён). Обход зоны (не передавать
|
||
флаг на `resume`) — её решение и её же декларация.
|
||
|
||
**Контрактно видимая половина — вторая, и она молчала.** Канон обещал `decline` — «leave it out»,
|
||
а решение до движка не доезжает (`mined_rejects` платформа не пишет), то есть отклонённый термин
|
||
возвращается предложенным. Это обещание пользователю, которого система не держит. Записано
|
||
предупреждением на `BankDecision.action` + строкой §3 с носителем — строка 192, отложенная
|
||
владельцем. Форму не меняю: обещание верное, не выполнена реализация.
|
||
|
||
⚠ *(0.5.0: предупреждение умерло вместе со своим носителем — схема `BankDecision` снесена, а дверь-
|
||
преемник пишет файлы движка сама, так что «не доезжает» перестало быть свойством формы; остаток —
|
||
монтаж, пункт (2в) очереди D39.156. Абзац сохранён как история синка 20.08.)*
|
||
|
||
### L. Мелкое
|
||
|
||
- **Ссылка приложения А протухла** — ИСПРАВЛЕНО, и не новым номером, а именем функции
|
||
(`flagReasonSeverity`): номер строки в чужой зоне протухает за пак, что здесь и произошло.
|
||
- **`heading` всегда `null`** — посылка «у читателя НЕТ меток глав вообще» **неверна**, и это тот
|
||
случай, где сказалась объявленная зоной граница (фронт не смотрели). 0.3.0 снял запрет клиентской
|
||
служебной метки: «Глава N» рисует КЛИЕНТ, в локали интерфейса, за $0 (§2.3, решение владельца
|
||
09.08). Без меток остаётся только книга, у которой их нет в данных, — а это легальная книга.
|
||
Продуктовый пробел «настоящих названий нет» держится видимым строкой §3 (строка 160) и никуда не
|
||
делся; менять нечего.
|
||
- **`410` на опечатку в id главы (PD-253)** — **канон изменён в сторону зоны, а не наоборот.**
|
||
Требование различать «была и исчезла» от «такой не было» невыполнимо: id непрозрачны, после
|
||
пере-разбора не хранятся, и доказать, что id никогда не минтился, можно только кладбищем всех
|
||
выданных. Записано: `410` — ответ на id, которого нет в ТЕКУЩЕЙ структуре, был он когда-то или
|
||
нет; `404` остаётся за книгой. Лечение у клиента в обоих случаях одно — перечитать дерево.
|
||
Расхождение перестаёт быть расхождением.
|
||
|
||
### M. Что зона сделала сама — сверено, возражений нет
|
||
|
||
Все восемь пунктов сверены с каноном: область `Idempotency-Key` `(принципал, метод, путь)`,
|
||
подрезание `limit`, `resync_required` вместо тихого старта, `blocked` только когда холд реально
|
||
укорачивает шкалу, `heading: null`, `finalizing` снят, полоса и её база на одном проходе, снятие
|
||
`codeForStatus`. Совпадает.
|
||
|
||
### N. Границы этой сессии
|
||
|
||
Закрыто из того, что зона объявила своей границей: пункт H сверен с движком (чтением), пункт G
|
||
сверен с обеими зонами, совместимость с генерённым фронтом проверена исполнением
|
||
(`openapi-typescript@7` — чисто; единственная ломающая правка названа). Пункт B живьём тоже не
|
||
воспроизводился — и не требовался: он закрыт ратификацией, а не замером. Не проверялось: как
|
||
экраны фронта рисуют новые ответы (зона заморожена), и поведение построенного пути `resume` после
|
||
правки таблицы — это работа P7.
|
||
|
||
**Что забирает зона (P7):** `409` `ceiling_reached` на исчерпанном прогоне вместо `202` (A) ·
|
||
`unspecified` как обязанность, а не самодеятельность (D) · тождество интейка по содержимому с
|
||
откатом вместо `Content-Length` (E) · «сделано» = последний проход этого деплоя (G) ·
|
||
`Run.stop_requested` в `projectRun` (J) · при снятии флага — `resync_required`, а не тихое
|
||
исчезновение (H). **Что закрыто одной фразой в тексте и работы зоны не требует:** C, F, I, L.
|
||
|
||
### Ревью диффа этой зоны (исполнено этой же сессией, 20.08) — что оно изменило
|
||
|
||
Четыре независимые линзы по диффу: ХОЛОДНЫЙ ПОТРЕБИТЕЛЬ (модель другого семейства — fable; читал
|
||
ТОЛЬКО финальный канон, без диффа, без компаньона, без записки платформы, и пытался реализовать
|
||
шесть изменённых мест) · опровергатель ПОСЫЛОК диспозиций · регрессионная линза (умерло ли правило
|
||
вместе с удалённой строкой + независимая пере-сверка 0.2.3 → 0.3.0 ДРУГИМ срезом: не по удалённым
|
||
строкам, а по перечислению всех схем/полей/операций 0.2.3) · линза стандартов (RFC по
|
||
первоисточникам). Каждая находка потом адверсариально верифицировалась отдельным агентом с
|
||
установкой «по умолчанию опровергнуто». **31 находка, 5 подтверждено, 26 опровергнуто; 35 агентов,
|
||
0 упавших** (важно: упавший агент — это НЕ «опровергнуто»).
|
||
|
||
Ни одна из пяти не опрокинула диспозицию — все пять о точности МОЕГО текста, и это правильный
|
||
результат для ревью, которое ищет не «согласен ли я», а «проверяемо ли написанное»:
|
||
|
||
1. **Денежный довод в пункте G был сильнее фактов — и уже стоял НОРМОЙ в каноне.** «Предлагает
|
||
уже сделанное и берёт за них деньги»: вторая половина не проверена и для обычного случая неверна
|
||
(потолок, а не цена; расчёт по замеренной трате; неизменённая книга переигрывается за $0). Снято
|
||
и из канона, и отсюда; довод «предлагает уже сделанное» стоит сам.
|
||
2. **«Платформа физически не пишет `day_usd`»** — неверный глагол: не ЗАВОДИТ, но шаблон деплоя
|
||
пере-маршалится целиком, и оператор может внести поле без единой правки Go. Поправка усиливает
|
||
триггер пересмотра, а не отменяет отказ.
|
||
3. **«`Run` едет в кадре потока»** — ложный факт: ни один payload `Run` не несёт. Цена реальна, но
|
||
её носитель другой — предписанная кадром перечитка карточки.
|
||
4. **Носитель строки H («регистр движка») — документ, которого нет.** Живой носитель — PD-298
|
||
регистра платформы. Ровно то нарушение правила Б-21, которое этот же файл и вводит.
|
||
5. **Ссылки в чужую зону мешали ДВЕ базы** (HEAD и рабочее дерево с 81 незакоммиченным файлом
|
||
платформы) в одной строке таблицы. Лечение — база названа явно, адресация именами функций.
|
||
|
||
⚠ **Три опровержения я отклонил и правку внёс.** Три линзы независимо споткнулись об один шов —
|
||
что отвечает `resume`, когда у прогона лимит ещё есть, а СЧЁТ не тянет холд. Каждую формулировку
|
||
верификатор опроверг по отдельности («тексты можно прочесть согласованно»), но холодный потребитель
|
||
прямо написал, что решить не может, а платформа этот случай уже схлопывает в тот же вердикт.
|
||
Сходимость трёх независимых линз на одном месте — сигнал сильнее трёх поштучных опровержений.
|
||
Заведено `cause.code: credit_unavailable` (второй уровень не закрыт по замыслу — типы не двигаются),
|
||
`ceiling_reached` пере-сформулирован как «ЭТОТ прогон закончен», и явно сказано, что подменять друг
|
||
друга они не могут. Тем же ходом закрыт вопрос холодного потребителя про `stop_requested` после
|
||
`resume` (снимается) и возвращена клауза «предел попыток не на проводе», умершая с моей же правкой.
|
||
|
||
### Второе ревью: велосипеды и объём прозы (заказ владельца 20.08)
|
||
|
||
Первое ревью проверяло текст на соответствие RFC. Владелец задал ДРУГОЙ вопрос — «а так вообще делают,
|
||
или ты изобрёл своё», плюс «не раздул ли ты прозу». Отдельная проверка: линза охоты за велосипедами
|
||
(модель другого семейства, с обязательным чтением первоисточников через WebFetch) + линза размещения
|
||
прозы, которой прямо сказали, что автор диффа склонен переобъяснять. **20 находок, подтверждена 1.**
|
||
|
||
**Велосипедов не подтверждено ни одного.** Проверялись против настоящих спек: тождество интейка
|
||
против `draft-ietf-httpapi-idempotency-key-header` и **RFC 9530 (Digest Fields)** — формулировка
|
||
«чем сервер устанавливает тождество, его дело» черновику СООТВЕТСТВУЕТ, а `Repr-Digest` предписать
|
||
клиенту мы не можем и не должны · `credit_unavailable` под `409` — 402 зарезервирован, двухуровневый
|
||
код конвенционален (Microsoft `innererror`, `google.rpc.ErrorInfo`) · `stop_requested` булевым против
|
||
конвенции таймстемпа (k8s `deletionTimestamp`, AIP-165) — отклонено · `410` на никогда не
|
||
существовавший id против RFC 9110 §15.5.11 — отклонено · «валидатор легален на любом безопасном
|
||
чтении» против OAS 3.1 — отклонено · пер-строчные tombstone по образцу JMAP (RFC 8620) — не наш
|
||
случай.
|
||
|
||
**По объёму — замер стоит, классификация не устояла.** Верификатор пере-ран замер пином зоны
|
||
(`openapi-typescript@7.13.0`) и воспроизвёл его до байта: канон дал **+10 546 байт и +144 строк
|
||
комментариев** в исходники фронта. Но гипотеза «62 % этого — обоснование, которому место здесь»
|
||
пере-проверку не прошла: из тринадцати заявленных блоков выжил ОДИН — довод в `Progress` («контракт
|
||
не говорит, сколько проходов бывает…»), который (а) дословно уже записан здесь, в §6в G, и (б)
|
||
локально пере-выводит глобальное правило шапки канона. Срезано; правило осталось одной фразой.
|
||
|
||
Своим решением, а не по находке, срезан ещё денежный хвост у `Book.chapters_done` — он ничего не
|
||
меняет в поведении клиента. Итог трима: 152 758 → 152 002 байта. **Урок для следующей правки канона:
|
||
норма — в канон, довод — сюда; проверять не глазом, а диффом генерённого клиента.**
|
||
|
||
### Ратификация этой редакции — D39.152 (20.08, оркестратор №18)
|
||
|
||
> Текст ноты, сданный контрактной сессией, **исполнен и заменён живым**: ратификация записана в
|
||
> `docs/architecture/05-decisions-log.md`, нота **D39.152**, строка реестра — `05-decisions-index.md`.
|
||
> Черновик ноты, живший здесь, снят при лендинге (испр. оркестратором №18): он расходился с
|
||
> итоговым текстом в двух местах, и две копии одной ноты — ровно тот распад «один носитель на
|
||
> факт», против которого написана норма D39.112. **Что в живой ноте отличается от черновика:**
|
||
> (1) внесены ЧЕТЫРЕ текстовые правки при ратификации (сеттер `stop_requested` в `stopRun` ·
|
||
> стухшая фраза §6в A против собственного аддендума · «cleared by a resume that SUCCEEDS» ·
|
||
> приписка третьего случая `resumeRun` к таблице); (2) клейм «ломающая правка ровно одна» уточнён:
|
||
> он верен для диффа генерённых ТИПОВ и неточен поведенчески — против 0.3.0 расходятся ЧЕТЫРЕ
|
||
> места (см. D39.152 п.5).
|
||
---
|
||
|
||
## 7. Эксплуатационные примечания — НЕ норма контракта
|
||
|
||
Вынесено из спеки в 0.3.0 (Б-16). RFC 9205 §4.1 прямо про наш случай: «Requiring a particular version
|
||
of HTTP … harms interoperability. Therefore, it is **NOT RECOMMENDED** that applications using HTTP
|
||
specify a minimum version … However, if an application's deployment benefits from the use of a
|
||
particular version of HTTP (for example, HTTP/2's multiplexing), **this ought be noted**». Отметить, а
|
||
не потребовать. Спека 0.2.3 требовала («Required: HTTP/2 at the edge») и держала в норме вендорный
|
||
заголовок конкретного прокси.
|
||
|
||
Норма в спеке — то, что клиент НАБЛЮДАЕТ и на что вправе рассчитывать: `ETag`/`If-None-Match`/`304`
|
||
· **обязанность честить `Accept-Encoding` на JSON-ответах и НЕ сжимать `text/event-stream`** · `Vary`
|
||
на согласованном представлении · `Cache-Control: no-store` · запрет буферизации потока · форма
|
||
heartbeat · форма `id` кадра. Сжатие как ТРЕБОВАНИЕ живёт в контракте — это прямое указание D39.138
|
||
п.2(д) («сжатие и условные чтения записываются В КОНТРАКТ, не в зонный док»), и первая редакция
|
||
батча его нарушила, вынеся сжатие целиком в примечание; поймано опровергателем полноты и исправлено.
|
||
В примечании остаётся ТОЛЬКО слой исполнения — где именно сжимать, — потому что вот этого клиент
|
||
действительно не наблюдает, и вот это Б-16 из контракта и выносит.
|
||
|
||
Ниже — то, что клиент не наблюдает и что деплой обязан себе устроить сам:
|
||
|
||
- **Чем и на каком слое сжимать** (middleware, обратный прокси, CDN). Go stdlib не сжимает,
|
||
edge-конфига в репозитории нет, `grep -rn "gzip|Content-Encoding" platform --include=*.go` → ноль.
|
||
- **HTTP/2 на edge.** Клиент держит одно SSE-соединение на книгу; на HTTP/1.1 шесть соединений на
|
||
origin — потолок вкладок. Сервис на HTTP/1.1 обязан работать ХУЖЕ, а не не работать.
|
||
- **`X-Accel-Buffering: no`** (или эквивалент прокси) — механизм для нормы «поток не буферизуется».
|
||
- **Сколько это даёт.** Замер ревью на настоящей книге (`~/books/gu-zhenren`, 2283 главы): глава
|
||
26,1 КБ → 10,4 КБ; дерево глав 250 КБ → 39 КБ; банк 1000 строк 167 КБ → 17 КБ.
|
||
- **Порядок работ по эффекту на килобайт усилия** (research/28 §5б, строка 186): сжатие → `ETag`/304
|
||
→ скоуп в кадре → дельта-чтения `?after_version=` → `staleTime` у клиента. Шаги 3–4 — уже в
|
||
контракте (0.3.0), шаги 1–2 — конфигурация, шаг 5 — зона фронта.
|
||
- **Транспорт НЕ меняется** (проверено независимым агентом другого тира + сверка первоисточников):
|
||
наша нагрузка — художественный текст, и после gzip разница между JSON и protobuf единицы процентов;
|
||
gRPC-web требует прокси и теряет `If-None-Match`/304; Connect возвращает то, что у нас уже есть.
|
||
Наш паттерн «кадр без данных → клиент перечитывает» легитимен и называется poke/pull.
|
||
|
||
---
|
||
|
||
## Приложение А. Карта «причина → КОД контракта → продуктовая фраза» — ЗАГОТОВКА
|
||
|
||
**Что изменилось в 0.3.0.** Прежняя карта вела «причина движка → фраза», то есть предполагала, что
|
||
ФРАЗУ рисует сервер. Это ровно та политика, которую вариант B отменил для ошибок (§2.17), и держать
|
||
её для замечаний значило бы оставить на проводе серверную локализованную строку — второй русский
|
||
текст, приходящий из зоны, у которой нет ни `Accept-Language`, ни локалей. Поэтому `Note` несёт
|
||
**`code`**, а фразу рисует клиент; `Note.message` с провода снят.
|
||
|
||
**Правила заполнения.**
|
||
|
||
1. Фраза пишется по ДОККОММЕНТУ `disposition.go`, а не по имени константы, и рядом кладётся цитата —
|
||
иначе повторяется инверсия, стоившая двух фраз (`glossary_miss` подан как «термин не подписан»,
|
||
хотя термин ПОДПИСАН и его проигнорировали, `disposition.go:78-79`; `sanitizer_stripped` подан как
|
||
потеря текста, хотя «the chunk is NOT lost», `disposition.go:99`).
|
||
2. **Класс 2 схлопывается в ОДИН код** (§8а, нормативно): четыре причины ранга 0 — модельный отказ —
|
||
на проводе неразличимы, потому что каждый различимый код здесь бит обратной связи подбирающему.
|
||
3. Слова — владельца (ПТ-33, В-3, строка 148). **Коды ниже — ◆ ПРЕДЛОЖЕНИЕ**, ратифицируются вместе с
|
||
фразами; до тех пор `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 открыт, ответ владельца 16.08 — «решим потом». Строка
|
||
записана здесь, чтобы построенное не читалось как решённое.
|
||
|
||
| Причина движка | Ранг | Код контракта ◆ | Продуктовая фраза | Ступень |
|
||
|---|---|---|---|---|
|
||
| `hard_refusal` · `soft_refusal` · `content_filter` · `hard_block` | 0 | `content_withheld` (ОДИН на все четыре — класс 2) | ⬜ | ⬜ |
|
||
| `cjk_artifact` | 1 | `source_residue` | ⬜ | ⬜ |
|
||
| `excision_suspect` | 1 | `text_possibly_dropped` | ⬜ | ⬜ |
|
||
| `coverage_fail` | 1 | `incomplete_coverage` | ⬜ | ⬜ |
|
||
| `sanitizer_defect` | 2 | `markup_defect` | ⬜ | ⬜ |
|
||
| `loop_degenerate` | 3 | `repetition` | ⬜ | ⬜ |
|
||
| `decode_error` | 4 | `unreadable_answer` | ⬜ | ⬜ |
|
||
| `glossary_miss` | 5 | `term_not_applied` | плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ |
|
||
| `length` | 6 | `length_mismatch` | ⬜ | ⬜ |
|
||
| `empty` | 6 | `empty_answer` | ⬜ | ⬜ |
|
||
| `sanitizer_stripped` | 7 | `markup_cleaned` | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
|
||
| `upstream_not_ok` | 8 (по умолчанию) | `unavailable` | ⬜ | ⬜ |
|
||
| незнакомая причина | 8 (по умолчанию) | **`unspecified`** (обязанность сервера, п.5; клиент рисует ту же нейтральную фразу, что и на незнакомый код) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
|
||
|
||
Причин пятнадцать; `upstream_not_ok` не имеет своей ветки в `flagReasonSeverity`
|
||
(`backend/internal/pipeline/status.go`, функция `flagReasonSeverity`) и падает в ранг по умолчанию,
|
||
как и любая будущая причина. ⚠ Номер строки здесь намеренно не ставится: прежняя ссылка `:174`
|
||
протухла за один пак (там теперь `GlossaryMissFlagged`) — поймано синком 20.08, п. L записки.
|
||
|
||
### Приложение А-2. Карта кодов ОШИБОК — заполнена (0.3.0)
|
||
|
||
В отличие от карты выше, эта заполнена целиком: словарь выведен из реальных ветвей платформы, а фраз
|
||
она не содержит по замыслу — их рисует клиент.
|
||
|
||
| Корневой `code` | HTTP | Откуда взят (платформа) |
|
||
|---|---|---|
|
||
| `invalid_request` | 400 | `v0.go:238,250,333,339,531` · `pgstore.ErrBadCursor` · `books.ErrBadIntake` (`books.go:118-121`, `readField` `v0.go:394-396`) · `v0.go:242` (неполное тело) · `v0.go:345,356,419,423` (интейк) |
|
||
| `unauthenticated` | 401 | гард сессии (`server.go:109-110`) |
|
||
| `forbidden` | 403 | `server.go:99` + `auth/csrf.go` (отсутствие `X-TM-Client` / чужой origin) |
|
||
| `not_found` | 404 | `pgstore.ErrNoBook`/`ErrNoAccount`/`ErrNoRun` · охраняемый catch-all `server.go:134` |
|
||
| `gone` | 410 | `pgstore.ErrNoChapter` → `v0.go:761` — глава, которой нет в ТЕКУЩЕЙ структуре книги (была она когда-то или нет — не различается по замыслу, `openapi.yaml` §`gone`; лечение клиента одно: перечитать дерево). ⚠ Строка дописана оркестратором №18 при ратификации 0.4.0: карта объявляла себя «заполненной», а несла 15 значений из 16 корневых — производитель у `gone` живой с P7 |
|
||
| `request_timeout` | 408 | `os.ErrDeadlineExceeded` → `v0.go:417` |
|
||
| `payload_too_large` | 413 | `*http.MaxBytesError` → `v0.go:409` |
|
||
| `run_in_flight` | 409 | `pgstore.ErrRunInFlight` (`v0.go:548`) |
|
||
| `book_not_ready` | 409 | `runs.ErrBookNotReady` (`v0.go:550`) |
|
||
| `run_not_stoppable` | 409 | `runs.ErrNotStoppable` (`v0.go:554`) |
|
||
| `run_not_resumable` | 409 | `runs.ErrNotResumable`; `cause`: `ceiling_reached` — **с 0.4.0 это же ответ на ИСЧЕРПАННЫЙ прогон в `stopped`/`awaiting_bank`, где платформа сегодня молча отвечает `202`** (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:558`); `cause`: `bounds_moved` · `credit_held`; несёт `blocked` |
|
||
| `idempotency_conflict` | 409 | форма заведена батчем; реализация — P7 |
|
||
| `bank_corrections_refused` | 409 | объявлен 0.5.0 ДО производителя; производитель — монтаж двери правок (пак (2в) очереди D39.156): раскладка отказа класса 14 движка (`tmctl bank-apply`, документ прочитан и отклонён целиком — пользователь пере-решает) |
|
||
| `bank_corrections_incomplete` | 503 | объявлен 0.5.0 ДО производителя; тот же монтаж: раскладка класса 15 (документ принят, запись не довершена — слать ТОТ ЖЕ документ, ретрай сходится) |
|
||
| `content_refused` | 400 | К-9; прескрин не построен (ПТ-16, строка 94). Отказ целой КНИГИ приходит не сюда, а состоянием `rejected` + `reject_reason` |
|
||
| `service_unavailable` | 503 | `runner.ErrCeilingNotWired` + `runs.ErrRunnerIncomplete` (`v0.go:562`) |
|
||
| `internal_error` | 500 | `v0.go:518,572` · `middleware.go:68` |
|
||
|
||
⚠ **Коды, которых платформа сегодня достигает, а контракт до 0.3.0 не объявлял:** 403 (`server.go:99`),
|
||
500 (`v0.go:518,572`), 431 от `net/http` (`serve.go:79`), 429 на `/auth` (`login.go:173,234`). Из них
|
||
контракт объявляет 403 (это нарушение правила, которое ИЗОБРЁЛ сам документ) и не объявляет 500
|
||
(Zalando: «500 Internal Server Error · use · **do not document**»), 431 (уровень stdlib) и 429 (на
|
||
`/v0` лимитера нет вовсе — форвард-вопрос платформы). `405` на `/v0` не бывает: catch-all глотает
|
||
метод.
|