19 KiB
Передача оркестратору: контракт 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/loginGET начинает вход, редиректит к провайдеру; принимает ?return_to=<путь на этом сайте>/auth/callbackGET завершает вход, ставит сессионную куку, редиректит на return_toлибо на дефолт/auth/logoutPOST завершает ЭТУ сессию /auth/logout-allPOST завершает ВСЕ сессии пользователя Клиенту нужно знать три вещи, и только три. Куда вести на вход. Что
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-бис. Три правки спеки СВЕРХ списка промта — с основанием на каждую
Все три сняты адверсариальным ревью диффа как утверждения, переставшие быть правдой; ни одна не меняет форму, все три меняют то, что файл о себе говорит.
Problem.titleполучил то же ограничение, что иdetail. Было: запрет движкового текста стоял только наdetail. Но клиенту при отказе нечего показать, кроме этих двух полей, иtitle, написанный для разработчика, становится фразой, которую читает человек. Теперь оба поля объявлены продуктовым языком:title— класс отказа,detail— конкретная фраза.- «vocabulary of version 0.1» → «of THIS version». Предложение осталось от 0.1.0 и после бампа стало враньём о собственном файле.
servers.descriptionперестал называть базовый путь непроверенным предложением фронта. Платформа заехала, и её собственная ручка в промте записана какGET /v0/usage, то есть префикс подтверждён. Заодно сказано главное: фиксирован только префикс версии, хост — тот origin, что отдал приложение, и клиент, зашивший конкретный хост, никуда не разворачивается.
⚠ Пункт 3 — единственное место, где я опираюсь на промт, а не на код платформы: в её зону мне ходить не велено. Если префикс на самом деле другой, это правится одной строкой.
6. Зависимости, которые эта правка НЕ закрыла
Прежний §3 компаньона в силе целиком, плюс одно уточнение:
- Механизма поднятия потолка по-прежнему нет, и теперь это записано в спеке жёстче:
ceiling_chaptersедет со СТАРТОМ прогона, изменить его контракт не умеет вовсе. Значитresumeпосле стопа по потолку возвращает прогон в то же состояние, и спека прямо запрещает клиенту предлагатьresumeкак лекарство отpaused. Строка единого бэклога (126) остаётся открытой и становится блокером экрана S4.