From 483ea6eb121f2abaf42e4450e547c16a3b33ff5d Mon Sep 17 00:00:00 2001 From: heaven Date: Sat, 8 Aug 2026 02:33:02 +0300 Subject: [PATCH] Remove the applied S3 contract patch: its blocks are in the canon companion and the file declared itself temporary --- frontend/docs/S3_CONTRACT_HANDOFF.md | 165 --------------------------- 1 file changed, 165 deletions(-) delete mode 100644 frontend/docs/S3_CONTRACT_HANDOFF.md diff --git a/frontend/docs/S3_CONTRACT_HANDOFF.md b/frontend/docs/S3_CONTRACT_HANDOFF.md deleted file mode 100644 index 73caf3ac..00000000 --- a/frontend/docs/S3_CONTRACT_HANDOFF.md +++ /dev/null @@ -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 | -| В.10–11 | `GET /usage` + схема `Usage`. ⚠ Форма из промта записана в 3.0-синтаксисе (`nullable: true`), в 3.1 он невалиден — выражено через `oneOf` с `type: 'null'`, как везде в этой спеке | -| В-бис | в `openapi.yaml` НЕ потащено, как и велено; текст для компаньона — §2 ниже | -| Г.12–15 | пер-главное чтение юнитов с запретом пер-книжной ручки · экспорт-артефакт с явным запретом сборки текста на клиенте · право сервера склеивать кадры + обязанность клиента терпеть скачки · `maxLength` у `heading` | -| В-трис.16–18 | `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.