Remove the applied S3 contract patch: its blocks are in the canon companion and the file declared itself temporary

This commit is contained in:
heaven 2026-08-08 02:33:02 +03:00
parent dde6d389a9
commit 483ea6eb12

View file

@ -1,165 +0,0 @@
# Передача оркестратору: контракт 0.2.0 — что применить в каноне
> **Это не носитель состояния, а патч.** Файл существует ровно до лендинга S3: оркестратор
> переносит правку спеки и вписывает блоки §2§4 в компаньон канона, после чего файл удаляется.
> Сессия фронта канон (`docs/architecture/14-api-contract/`) не правит — это чужая зона.
>
> Написано фронт-сессией S3, 08.08.2026.
## 1. Спека: зонная копия впереди канона
| Что | Где |
|---|---|
| Правленая копия | `frontend/docs/api-contract/openapi.yaml`**version `0.2.0`** |
| Канон | `docs/architecture/14-api-contract/openapi.yaml` — всё ещё `0.1.0-draft` |
Байт-сверка (пункт Д промта) **даёт расхождение, и это ожидаемо**: правки заказаны промтом S3,
а канон правит оркестратор. Диффом:
```
diff -u docs/architecture/14-api-contract/openapi.yaml frontend/docs/api-contract/openapi.yaml
```
После пере-ратификации копии обязаны снова стать байт-равными; шапка `info` в зонной копии это
прямо говорит, чтобы следующая сессия не приняла временное расхождение за дефект.
**Что сделано по списку промта** (каждый пункт — в спеке, а не в намерении):
| Пункт | Исполнено |
|---|---|
| А-нуль | `version: 0.2.0`. ⚠ **Отступление, называю вслух:** промт говорил «твоё там одно — бампнуть version», но в шапке стояла фраза «version всё ещё `-draft` — S3 бампнет первым действием», которая после бампа становилась ложной. Заменил её на правду о том, что копия сейчас впереди канона. Баннер «DRAFT … not a ratified contract» НЕ возвращён |
| А.1 К-1 | статусы не тронуты |
| А.2 К-8 | `BookStatus` += `paused`; в описании прямо: стоп по потолку это `paused`, не `failed` |
| А.3 К-5 | `Progress.eta_seconds`, опциональное и nullable |
| А.4 К-3 | `Chapter.number` и `Chapter.heading`оба обязательны и оба допускают `null`; запрет синтеза «Глава {n}» записан нормативом. Фикстуры проверены и правлены (см. §5) |
| Б.5 К-4 | ревизия пер-книжная; правила «монотонна в скоупе», «отбрасывание — обязанность клиента», «докачка `>= R`, не `>`», «полная замена ⇒ `resync_required`» — прозой в `Revision` |
| Б.6 К-7 | `?limit=&cursor=` и `next_cursor` на ВСЕХ пяти списочных ответах; дефолты 5000/1000/500; курсор привязан к структурной эпохе, отклонение протухшего — MUST сервера, ответ `400` |
| Б.7 К-12 | `202` + обязательный `Location`; `Retry-After` объявлен ЯВНО заголовком 200-ответа со ссылкой на то, почему RFC 9110 сам этого не даёт |
| Б.8 CSRF | `X-TM-Client` в описании `sessionCookie`: несёт ПРИСУТСТВИЕ, значение любое, обязателен и на same-origin |
| Б.9 | `PausedReason` = `credit_exhausted`, `Run.paused_reason` обязателен и nullable |
| В.1011 | `GET /usage` + схема `Usage`. ⚠ Форма из промта записана в 3.0-синтаксисе (`nullable: true`), в 3.1 он невалиден — выражено через `oneOf` с `type: 'null'`, как везде в этой спеке |
| В-бис | в `openapi.yaml` НЕ потащено, как и велено; текст для компаньона — §2 ниже |
| Г.1215 | пер-главное чтение юнитов с запретом пер-книжной ручки · экспорт-артефакт с явным запретом сборки текста на клиенте · право сервера склеивать кадры + обязанность клиента терпеть скачки · `maxLength` у `heading` |
| В-трис.1618 | `GET /books/{bookId}/run-options``CeilingBounds`; `RunRequest.ceiling_chapters`; `Run.ceiling_chapters`. Обоснование формы — §3 |
| В-кватер | раздел «Transport» в шапке: тот же origin как факт, CORS-слоя нет, кросс-origin не проектируется. В коде — прокси dev-сервера (`vite.config.ts`, включается `TM_PLATFORM`) |
## 2. Для компаньона: поверхность входа (пункт 15-бис)
Вставить в компаньон канона (`docs/architecture/14-api-contract/README.md`) новым разделом.
> ### 2.14. Поверхность входа — вне версионного префикса
>
> Платформа построила вход через OIDC (P1). Четыре ручки живут ВНЕ `/v0`, как `/healthz`, потому
> что это механика сессии, а не контрактная поверхность, и в `openapi.yaml` они намеренно не
> тащатся (решение оркестратора как владельца контракта).
>
> | Ручка | Метод | Что делает |
> |---|---|---|
> | `/auth/login` | GET | начинает вход, редиректит к провайдеру; принимает `?return_to=<путь на этом сайте>` |
> | `/auth/callback` | GET | завершает вход, ставит сессионную куку, редиректит на `return_to` либо на дефолт |
> | `/auth/logout` | POST | завершает ЭТУ сессию |
> | `/auth/logout-all` | POST | завершает ВСЕ сессии пользователя |
>
> Клиенту нужно знать три вещи, и только три. Куда вести на вход. Что `return_to` принимает
> **только путь этого сайта** — чужой сервер молча заменит на дефолт, открытый редирект закрыт.
> И что обе `POST`-ручки лежат на cookie-пути, то есть требуют `X-TM-Client` (§2.8 спеки).
> Отказ входа — `problem+json`, как везде; различать причины отказа клиент не может и не должен.
## 3. Для компаньона: управляемый потолок прогона — форма и обоснование
Единственное место списка правок, где решение было за сессией. Форма и почему именно она:
> ### 2.15. Потолок прогона — ◆ предложено фронтом
>
> Ратифицировано владельцем 07.08: шкала в интерфейсе от минимума до максимума, ноль выбрать
> нельзя, единица — ГЛАВЫ, максимум — доступный остаток (баланс минус холды), потолок принадлежит
> ПРОГОНУ. Ручки, отдающей границы шкалы, в контракте не было; она объявлена этой правкой.
>
> **Отдельный ресурс `GET /books/{bookId}/run-options`, а не поле карточки книги.** Два довода, и
> оба про правду на экране. Максимум зависит от АККАУНТА — баланс минус открытые холды — и
> двигается, когда книга не меняется: холд, взятый под другую книгу, опускает остаток. Карточка
> книги кэшируется и переиспользуется библиотекой, то есть назвала бы максимум, которого уже нет,
> ровно в тот момент, когда пользователь двигает ползунок. Второй довод дешевле, но тоже
> настоящий: граница нужна один раз, перед стартом, а поле на карточке заставило бы КАЖДОЕ чтение
> библиотеки нести состояние счёта.
>
> **Три числа, а не два.** `min_chapters` объясняет себя единицей — одна глава. `max_chapters`
> отдаётся уже подрезанным и по остатку, и по тому, сколько в книге осталось непереведённого:
> клиенту запрещено подрезать второй раз, иначе правило живёт в двух местах и расходится.
> `default_chapters` отдаёт платформа, потому что выбор предустановленного значения — это
> продуктовая политика («потратить всё» ↔ «одна глава»), а не презентация; клиент, выбравший его
> сам, принял бы это решение молча.
>
> **`max_chapters: 0` — легальный ответ** и значит «прогон начать нельзя вовсе». Клиент показывает
> исчерпанное состояние вместо шкалы. Это единственный случай, когда `default_chapters` тоже `0`.
>
> **Пересчёта «главы → деньги» на проводе нет ни в каком виде** (D39.84). Он живёт на платформе по
> оценке движка.
>
> **`ceiling_chapters` — обязательное поле старта и поле прогона.** Обязательное, потому что прогон
> без объявленного потолка тратит мимо той границы, которую человек вправе поставить ДО, а не
> узнавать после. Поле на `Run` — чтобы перезагруженный экран мог назвать выбранный колпак, а не
> забыть его.
>
> **409 на старте** отвечает и на «потолок больше не помещается»: границы читаются отдельным
> вызовом и могут сдвинуться между чтением и стартом.
## 4. К-таблица компаньона: статусы после этой правки
Заменить таблицу §4 компаньона на эту (изменения — колонка «Статус»):
| # | Кому | Статус после 0.2.0 |
|---|---|---|
| К-1 | владелец | **ЗАКРЫТ** (D39.100) — десять статусов приняты; одиннадцатым добавлен `paused` по К-8 |
| К-2 | автор контракта + бэкенд | **ОТКРЫТ** — кто ПОРОЖДАЕТ титул, по-прежнему не решено; К-3 закрыл продуктовую половину (метка из данных), механическая осталась |
| К-3 | владелец | **ЗАКРЫТ** (D39.100) — метка из данных книги; спека запрещает синтез «Глава {n}», `number` и `heading` оба nullable |
| К-4 | платформа | **ЗАКРЫТ** — счётчик пер-книжный, одно число на все книго-скоупные чтения и кадр потока; выражено прозой в `Revision` |
| К-5 | владелец | **ЗАКРЫТ** (D39.100) — `Progress.eta_seconds`, опциональное |
| К-6 | владелец | **ОТКРЫТ** — принцип принят (две ступени), карта «причина → фраза» ждёт В-3 |
| К-7 | платформа | **ЗАКРЫТ** — keyset-курсор на всех списках, дефолты 5000/1000/500, привязка к структурной эпохе |
| К-8 | владелец | **ЗАКРЫТ** (D39.100) — `paused` + `Run.paused_reason: credit_exhausted` |
| К-9 | владелец + автор контракта | **ОТКРЫТ** — отказ прескрина по-прежнему не выразим ни одним статусом |
| К-10 | автор контракта + бэкенд | **ОТКРЫТ**`Chapter.units_done` так и не разведён по фазам; дерево глав показывает ноль всю черновую волну |
| К-11 | автор контракта | **ОТКРЫТ**, и половина стала фактом: `Note` по-прежнему не требует ни `chapter_id`, ни `unit_id`; сужение `BankDecision` для экрана — работа S5 |
| К-12 | платформа | **ЗАКРЫТ** — опрос, `202` + `Location`, `Retry-After` объявлен явно |
| **К-13** | **владелец + платформа** | **НОВЫЙ.** Достижение СОБСТВЕННОГО потолка прогона (`ceiling_chapters`) и исчерпание кредита аккаунта — это одно `paused_reason` или два? Сегодня значение ровно одно, `credit_exhausted`, и оно про кредит. Платформа берёт холд на сумму потолка до спавна, поэтому механически они могут совпасть — но это надо СКАЗАТЬ, а не вывести: пользователь, упершийся в собственный колпак, и пользователь, у которого кончились деньги, находятся в разных положениях, и фраза у них разная. Записано пометкой в `EventCeiling` |
## 5. Что фронт исправил в фикстурах и почему это надо знать автору контракта
- **Снят неснятый маркер `第二节:` из колонки оригинала.** Компаньон §2.3 фиксирует замером: чанкер
ВЫРЕЗАЕТ маркер из текста, который видит модель. Формы, которую рисовала прежняя фикстура,
движок для zh→ru не порождает — это был четвёртый экземпляр класса «фикстура выдумала форму».
- **Титул больше не вклеен в текст первого юнита фикстуры.** Метка живёт в `Chapter.heading`.
Это НЕ решение К-2: фикстура просто перестала утверждать что-либо про открытый вопрос.
- **Ключ строки банка — непрозрачный `id` контракта.** Прежняя заплата из (`src`,`dst`,`kind`)
роняла вторую строку полисемичного термина; в фикстуре теперь есть настоящая пара таких строк
(青茅 в двух смыслах с разными окнами), и без `id` она видна как дефект сразу.
## 5-бис. Три правки спеки СВЕРХ списка промта — с основанием на каждую
Все три сняты адверсариальным ревью диффа как утверждения, переставшие быть правдой; ни одна не
меняет форму, все три меняют то, что файл о себе говорит.
1. **`Problem.title` получил то же ограничение, что и `detail`.** Было: запрет движкового текста
стоял только на `detail`. Но клиенту при отказе нечего показать, кроме этих двух полей, и
`title`, написанный для разработчика, становится фразой, которую читает человек. Теперь оба поля
объявлены продуктовым языком: `title` — класс отказа, `detail` — конкретная фраза.
2. **«vocabulary of version 0.1» → «of THIS version».** Предложение осталось от 0.1.0 и после бампа
стало враньём о собственном файле.
3. **`servers.description` перестал называть базовый путь непроверенным предложением фронта.**
Платформа заехала, и её собственная ручка в промте записана как `GET /v0/usage`, то есть префикс
подтверждён. Заодно сказано главное: фиксирован только префикс версии, хост — тот origin, что
отдал приложение, и клиент, зашивший конкретный хост, никуда не разворачивается.
⚠ Пункт 3 — единственное место, где я опираюсь на промт, а не на код платформы: в её зону мне
ходить не велено. Если префикс на самом деле другой, это правится одной строкой.
## 6. Зависимости, которые эта правка НЕ закрыла
Прежний §3 компаньона в силе целиком, плюс одно уточнение:
- **Механизма поднятия потолка по-прежнему нет,** и теперь это записано в спеке жёстче:
`ceiling_chapters` едет со СТАРТОМ прогона, изменить его контракт не умеет вовсе. Значит
`resume` после стопа по потолку возвращает прогон в то же состояние, и спека прямо запрещает
клиенту предлагать `resume` как лекарство от `paused`. Строка единого бэклога (126) остаётся
открытой и становится блокером экрана S4.