410 lines
42 KiB
Markdown
410 lines
42 KiB
Markdown
<!-- ======================================================================
|
||
РЕВЬЮ-ШАПКА ОРКЕСТРАТОРА (ратификация D39.99, 04.08.2026, оркестратор №12)
|
||
|
||
СТАТУС: РАТИФИЦИРОВАН как контракт API v0. Нормативная поверхность —
|
||
openapi.yaml РЯДОМ (байт-копия зонной frontend/docs/api-contract/openapi.yaml;
|
||
байт-сверка копий — обязанность каждого лендинга, расхождение = дефект лендинга).
|
||
Генерация типов фронта после этой ратификации идёт из ЭТОЙ копии.
|
||
|
||
Приёмка: три адверсариальных воркфлоу-пасса (черновик · дофикс round-1 · round-2),
|
||
19 подтверждённых находок, 0 опровергнутых, все вправлены; батарея фронта с
|
||
контракт-шагом (spectral + дрифт-тест генерённых типов) зелёная исполнением.
|
||
Провенанс-классы ✓/◆/○ ниже выборочно перепроверены кодом движка — легенда честная.
|
||
|
||
ОТКРЫТЫ (см. §4): продуктовые К-1/К-3/К-5/К-6/К-8 — владелец; К-4/К-7/К-12 —
|
||
платформа — ОТВЕЧЕНЫ P0 и ратифицированы приёмкой 04.08 с поправками (носитель — зонный журнал
|
||
`platform/docs/platform-PROGRESS.md`, раздел «Ратификация приёмкой»); К-2/К-9/К-10/К-11 — автор контракта/бэкенд.
|
||
Зависимости §3 покрыты строками единого бэклога: 99/100/101/103 (перевешены в
|
||
БЛОКЕРЫ контракта) + новые 125 (банк-экспорт) · 126 (поднятие потолка) · 127
|
||
(DDL-коммент). Приложение А — заготовка: фразы и ступени заполняет владелец (В-3).
|
||
|
||
Тело ниже — авторский текст фронт-сессии S3 (два раунда дофикса), не переписано.
|
||
|
||
ПОСТ-РАТИФИКАЦИЯ (решения владельца 04.08 вечер, D39.100): К-1 ЗАКРЫТ (десять
|
||
статусов приняты) · К-8 ЗАКРЫТ (механизм «как Claude Code»: BookStatus получает
|
||
11-е значение `paused`, оповещение «лимиты исчерпаны», страница лимитов в
|
||
настройках — ПТ-35/П-5; суммы денег на провод по-прежнему не идут) · К-5 ЗАКРЫТ
|
||
(ETA показывать: `eta_seconds` в спеку) · К-3 ЗАКРЫТ (метка главы — из ДАННЫХ
|
||
книги; зашитой формы «Глава N» не существует — легальна книга без номеров глав) ·
|
||
К-6 — принцип принят (две ступени по читательскому эффекту; карта — с В-3 позже).
|
||
Правки спеки исполняет S3 (блок в её промте) с минорным бампом версии; эта копия
|
||
пере-ратифицируется диффом при лендинге.
|
||
====================================================================== -->
|
||
|
||
# Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
|
||
|
||
> **Нормативная поверхность контракта — [`openapi.yaml`](openapi.yaml)** (файл рядом, в этой же папке)
|
||
> (OpenAPI 3.1). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято
|
||
> каждое решение, чем оно обосновано, что осталось открытым. При расхождении по ФОРМЕ
|
||
> побеждает YAML; при вопросе «почему так» — этот файл.
|
||
>
|
||
> **Статус: РАТИФИЦИРОВАН — D39.99 (04.08), контракт 0.2.0 (D39.115, 08.08).** Дом канона — этот каталог; `frontend/docs/api-contract/openapi.yaml` — байт-зеркало. ⚠ Испр. оркестратором №15 08.08: файл называл себя черновиком на ратификацию четверо суток ПОСЛЕ ратификации — тот же класс, что шапка `info` спеки.
|
||
> `docs/architecture/14-api-contract/`, зона оркестратора; перенос делает он, генерация типов
|
||
> после ратификации идёт из перенесённой копии — контракт первичен, код вторичен.
|
||
>
|
||
> **Язык.** Спека английская: из неё генерятся типы, а исходники фронта по конвенции
|
||
> английские (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской
|
||
> `K-N` — это те же вопросы §4. Спутник и остальные доки зоны — русские.
|
||
>
|
||
> **Зона строки 95 — «оркестратор/бэкенд/фронт».** Фронт авторитетен в одной трети: форма
|
||
> read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и
|
||
> работы движка (99–103) здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют.
|
||
|
||
## 0. Пометки провенанса
|
||
|
||
| Пометка | Что значит |
|
||
|---|---|
|
||
| **✓ выведено** | следует из кода движка или ратифицированного решения; грунт `file:line` рядом |
|
||
| **◆ предложено** | решение фронта, разумное по его сведениям; подтверждает названная зона |
|
||
| **○ открыто** | развилка, на которую у фронта ответа нет; перечень — §4 |
|
||
|
||
⚠ Пометка ставится **на утверждение, а не на раздел**: у одного пункта половина бывает
|
||
выведенной, а половина предложенной. Первая редакция черновика этим и грешила — восемь мест
|
||
несли ✓ там, где верно было ◆; ниже разведено.
|
||
|
||
---
|
||
|
||
## 1. Почему YAML, а не проза
|
||
|
||
Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им
|
||
типизируются моки. Прозаический контракт расходится с кодом ровно тем способом, ради
|
||
предотвращения которого заведена строка 95.
|
||
|
||
Инструменты, пины и отклонение по пиру TS не дублирую: они в `STACK_DECISIONS.md` §3 и в
|
||
бэклоге зоны — Ф-23 (`overrides` вместо `--legacy-peer-deps`), Ф-24 (AsyncAPI отложен с
|
||
причиной). Четыре формы нарушения гейта, каждая проверена живьём, — `FRONTEND_PLAN.md` §5.4.2.
|
||
|
||
---
|
||
|
||
## 2. Решения и их происхождение
|
||
|
||
### 2.1. Язык — код, никогда не имя — ✓ выведено
|
||
|
||
Движок держит коды (`backend/internal/config/book.go:26-27`), ключ пары — `zh-ru`
|
||
(`configs/langpacks/zh-ru/`). Имя языка в данных — пар-специфика в общем слое, запрещённая
|
||
§2 канона. Вторая цена, дороже: `lang` элемента берётся из данных книги, и пара ja→ru с именем
|
||
вместо кода отрисует кандзи китайскими начертаниями молча.
|
||
|
||
### 2.2. Идентификаторы непрозрачны — ✓ выведено
|
||
|
||
`glossary.id` — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется
|
||
(`store/migrate.go:173-174`); номер главы плотный, «Chapters that yield no text … do NOT
|
||
consume a chapter number» (`chunk/chunker.go:99-105`), поэтому правка исходника сдвигает
|
||
номера последующих глав. Стабильность обеспечивает платформа при персисте манифеста (100).
|
||
|
||
### 2.3. Заголовок главы отдельным полем — ◆ предложено
|
||
|
||
**Движок сегодня делает ОБРАТНОЕ**, и это надо назвать прямо: титул рендерится
|
||
детерминистически из шаблона пары (`configs/langpacks/zh-ru/heading.txt`: `template Глава {n}`),
|
||
исходный маркер вырезается из текста для модели (`chunk/chunker.go:110-114`), а на экспорте
|
||
титул **вклеивается внутрь текста первого юнита** (`pipeline/export.go:215`), причём колонка
|
||
исходника остаётся без него.
|
||
|
||
Выведена здесь только МЕХАНИКА. Само поле `heading` — предложение фронта, и у него есть цена
|
||
на другой стороне: движку придётся отдавать титул отдельно. Альтернатива (оставить вклейку,
|
||
фронт отрезает строку) хуже: отрезание титула из текста — это парсинг прозы, и он сломается
|
||
на первой главе без заголовка. Развилка — К-2.
|
||
|
||
**Расхождение фикстуры, найденное разбором:** дерево витрины показывает «Раздел 2. …», колонка
|
||
оригинала — неснятый «第二节:». Для zh→ru движок не порождает ни одной из форм. Не чинится
|
||
до ответа на К-2.
|
||
|
||
### 2.4. Состояние — у прогона; у главы выполнение — ✓ выведено
|
||
|
||
Подпись банка это один стоп на всю книгу (`pipeline/mining.go:201`), поэтому «глава ждёт
|
||
подписи, пока соседняя финализируется» — невозможная картина. У главы движок держит
|
||
`ChapterPassport` (`pipeline/status.go:37-55`).
|
||
|
||
### 2.5. Прогресс пофазно и в юнитах — ✓ выведено
|
||
|
||
«A unit is DONE when every member draft AND the unit's edit resolved ok»
|
||
(`pipeline/status.go:328-331`), редактура не стартует до стопа банка ⇒ сквозной счётчик стоит
|
||
на нуле всю черновую волну. Зависимость — строка 99.
|
||
|
||
### 2.6. Словарь статусов — ✓ лестница, ◆ ненормальные исходы
|
||
|
||
Лестница дословно из строки 95: «загрузка → разбор → перевод → подпись банка → финал →
|
||
готово». **`not_started` — дыра, найденная самопроверкой черновика:** лестница описывает
|
||
идущий прогон, а библиотека обязана показывать разобранную книгу, которую не запускали.
|
||
`stopped`, `rejected`, `not_started` контракт ВЫВОДИТ из поведения процесса, а не получает
|
||
полем: механика стопа у движка есть (`cmd/tmctl/main.go:63`), но «кто нажал» знает платформа.
|
||
|
||
**Стоп по потолку — не `failed`** ✓ выведено: «Ceiling is a hard, book-wide stop (not a
|
||
per-chunk flag): the job stays 'pending' and resume continues once the ceiling is raised»
|
||
(`pipeline/stagerun.go:488-489`). Мапить его в `failed` запрещено — это соврало бы про
|
||
резюмируемость. Каким статусом и словом он показывается — К-8, вопрос владельцу.
|
||
|
||
### 2.7. Состояние пары выводится из ПАРЫ — ✓ выведено (исправление первой редакции)
|
||
|
||
Первая редакция утверждала «`withheld` = текст не выдан» как факт о движке. **Это было
|
||
ложно:** флагнутый юнит легально приходит С ТЕКСТОМ в двух случаях —
|
||
|
||
- косметическая зачистка санитайзера: «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, пункт 4; утверждение противоречило выводу строкой выше). При c-lite drop
|
||
юнит несёт `FlagReason` ПЕРВОГО выпавшего члена, каким бы он ни был (`export.go:203-207`:
|
||
`ce.FlagReason = drops[0].Reason`, и тут же `ce.FinalText … still ships`). Значит «текст +
|
||
замечание» — это класс, а не один случай, и карта вердиктов обязана иметь фразу для каждой
|
||
причины, а не для двух.
|
||
|
||
**Но `glossary_miss` в этот класс НЕ входит, и это проверено отдельно** (иначе правка выше
|
||
воскресила бы невоспроизводимый пример). `memberDrops` берёт причину из ЧЕРНОВОЙ строки члена
|
||
(`status.go:242-257`: `draftStages[cs.Stage] && flagged`), а `glossary_miss` ставится
|
||
пост-чеком только там, где отгружается финал: в черновой волне — лишь когда она сама финальная
|
||
(`waverun.go:373-381`, draft-only), в c-lite — на строке РЕДАКТУРЫ (`waverun.go:494`). В
|
||
пайплайне, где текст отгружает редактура (то есть где c-lite drop вообще возможен), черновая
|
||
строка `glossary_miss` нести не может. При включённом гейте текст удерживается целиком
|
||
(`export.go:295-300`), при выключенном мисса нет вовсе. **Итог: пара «текст + промах словаря»
|
||
невозможна ни одним каналом — фикстура витрины, показывавшая её, переведена на c-lite drop.**
|
||
|
||
**Свежесть** ✓ выведено: `target` обновляется на границах стадий и на стопах, а не
|
||
непрерывно — посреди прогона канала чтения не существует (эксклюзивный лок движка;
|
||
санкционированное чтение — завершённый либо остановленный прогон, `research/23` §0, §4).
|
||
|
||
### 2.8. Банк: словари ✓, имена ◆, `kind` ◆ с дырой
|
||
|
||
Значения выведены из схемы и гейтов Go; **имена полей контракта — предложение фронта.**
|
||
|
||
| Поле | Словарь | Грунт |
|
||
|---|---|---|
|
||
| `status` | `auto · draft · approved` | `store/migrate.go:191`; только `approved` — канон |
|
||
| `kind` ◆ | `name · place · title · term · nickname` **плюс отсутствие значения** | `terminology/classify.go:15` + `pipeline/banknote.go:74`; пустое — `membank/memseed.go:323-326` |
|
||
| `origin` | `seed · ruby · mined` | пути записи, см. ниже |
|
||
| `sense` | свободный текст | `store/migrate.go:182` |
|
||
| `since_chapter`/`until_chapter` | целые, `0` = без границы | `store/migrate.go:189-190` |
|
||
|
||
**Фантом `auto` в провенансе убран.** Первая редакция взяла словарь из комментария схемы
|
||
(`migrate.go:193`: `seed|ruby|auto`) — комментарий устарел. По путям записи `"auto"` пишет
|
||
**статус**, не провенанс (`membank/memseed.go:328`: `Status:"auto", Source:"ruby"`), а майнинг
|
||
ставит `mined` (`pipeline/mining.go:424`). Правка комментария в движке — за оркестратором.
|
||
|
||
**`kind` пере-размечен ✓→◆, и вот почему это не косметика** (ревью round-2, пункт 3). Словарь
|
||
из пяти значений выведен верно, но ЗАКРЫТЫМ и обязательным он делает нелегальной легальную
|
||
строку: ruby-кандидат получает `Type: ""`, если его класс не `name` — то есть gloss и
|
||
ambiguous живут без типа по построению (`membank/memseed.go:323-326`: `typ := ""`, и только
|
||
`class == rubyClassName` даёт `"name"`). Материализатору read-модели такую строку было
|
||
физически нечем заполнить. **Правило пустого:** `kind` присутствует всегда и допускает `null`;
|
||
`null` значит «движок не решил», строка при этом остаётся подписываемой, и клиенту запрещено
|
||
и выбрасывать её, и додумывать тип за движок. Проекция `""` → `null` — работа платформы.
|
||
|
||
**Ложный друг устранён.** У движка колонка `source` — это ПРОВЕНАНС. Первая редакция назвала
|
||
провенанс `origin`, а имя `source` отдала ДРУГОЙ колонке (тексту термина) — то есть завела
|
||
между схемами ложного друга. Теперь: провенанс `origin`, формы термина `src`/`dst`, как их
|
||
зовёт сам движок; имя `source` в схеме банка не используется вовсе.
|
||
|
||
### 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 — the stop clears once every proposed term
|
||
is promoted or rejected» (`pipeline/mining.go:201`). Отсюда: решение `promote|decline` ·
|
||
счётчик «решено N из M» · запрет «продолжить» при неполном наборе · частичное сохранение ◆.
|
||
|
||
`POST /runs/{id}/resume` — **нормативная операция, а не резерв** (первая редакция помечала её
|
||
«○ резерв строки 94», хотя тут же делала её носителем снятия стопа банка). Резерв строки 94 —
|
||
это `stop`, продуктовая кнопка.
|
||
|
||
### 2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
|
||
|
||
**✓ выведено:** правило ре-синка — идемпотентный апсерт по `(run_id, seq)`, канал согласования
|
||
— `status --json` (`research/23` §2, §8; D39.85).
|
||
|
||
**◆ предложено фронтом:** что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН.
|
||
Обоснование — гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом,
|
||
а рефетч по возврату фокуса окна у ратифицированного `@tanstack/react-query` включён по
|
||
умолчанию, то есть гонка на каждое переключение вкладки. Но это просьба, не вывод; выбор —
|
||
К-4.
|
||
|
||
**Скоуп ревизии** (дыра первой редакции: она отдавала `revision` на межкнижной библиотеке при
|
||
пер-прогонном определении): в спеке ревизия объявлена НА РЕСУРС — у библиотеки своя, у
|
||
прогона своя. Единая сквозная или пер-ресурсная — часть К-4.
|
||
|
||
### 2.11. Разрыв потока — ◆ предложено
|
||
|
||
При переподключении клиент шлёт `Last-Event-ID`. Если сервер докачать не может, он обязан
|
||
ответить событием `resync_required`, а не молча начать с текущего момента: **реплей истории
|
||
запрещён**, иначе разовое событие вроде `note` теряется молча и замечание не появится
|
||
до перезагрузки. Клиент по этому событию перечитывает снимки.
|
||
|
||
### 2.12. Разрешающий список — ✓ инвариант, ◆ форма
|
||
|
||
Проекция «read-модель → фронт» строится как allowlist. Что лежит в операторских структурах
|
||
(`pipeline/status.go:58-130`, `:37-55`) и не может доехать: пять денежных полей плюс
|
||
`cost_usd` главы (§4.8 — денег в MVP-интерфейсе нет вовсе) · `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` — последний несёт сырой текст
|
||
движка вида «CJK leak in the ru output: 第一节»; строку собирает `checks/sanitizer.go:658`,
|
||
а `pipeline/export.go:34` — лишь объявление поля, куда она доезжает.
|
||
|
||
### 2.13. ПТ-34 — перевод не индексируется — ✓ инвариант
|
||
|
||
Реестр требований назначает носителем ПТ-34 в том числе контракт, а первая редакция пункта
|
||
не имела вовсе. В спеке: приложение живёт под `X-Robots-Tag: noindex`, ответы с текстом
|
||
перевода несут `Cache-Control: no-store`, ссылка на выгрузку выдаётся только владельцу.
|
||
|
||
---
|
||
|
||
## 3. Зависимости: без чего контракт не заработает
|
||
|
||
| Что | Строка | Без чего именно |
|
||
|---|---|---|
|
||
| Пофазный прогресс `draft ∥ edit` | 99 | прогресс (§2.5) |
|
||
| Персист манифеста + `chunker_version` | 100 | стабильный `id` главы (§2.2) |
|
||
| Машиночитаемая таблица ПОДПИСИ | 101 | экран подписи (§2.9) |
|
||
| Событийный эмиттер + событие потолка | 103 | весь поток (§2.11), событие `note`, событие `ceiling` |
|
||
| **Артефакт экспорта БАНКА** | **строки нет — заводит оркестратор** | чтение `GET /books/{id}/bank` |
|
||
| **Механизм поднятия потолка** | **строки нет — заводит оркестратор** | `POST /runs/{id}/resume` после стопа по потолку (см. ниже) |
|
||
| HTTP/SSE, аутентификация, воркер | П-1 | всё; в `platform/` ноль строк кода |
|
||
|
||
⚠ **Отдельно про банк — дыра, найденная ревью оркестратора.** Строка 101 даёт таблицу
|
||
ПОДПИСИ (стоп-таблица, кап 20 на stdout — `cmd/tmctl/render.go:98`), а не экспорт всего банка;
|
||
сам банк живёт в приватном SQLite движка, читать который платформе запрещено (D39.85). То есть
|
||
у чтения `/bank` сегодня **нет канала вообще**. Фронт этого не решает — нужна строка единого
|
||
бэклога, и заводит её оркестратор.
|
||
|
||
Та же природа у события `note`: пер-юнитных замечаний посреди прогона движок не эмитит —
|
||
зависимость на словарь строки 103.
|
||
|
||
⚠ **Потолок: `resume` сам по себе не сдвинет прогон** (ревью round-2, пункт 5). Движок
|
||
продолжает «once the ceiling is raised» (`pipeline/stagerun.go:488-489`), а канала поднятия
|
||
в контракте нет — и в MVP-интерфейсе быть не может: денег на экране нет вовсе (D39.84). Значит
|
||
между стопом по потолку и продолжением обязан стоять механизм ПЛАТФОРМЫ (поднятие по политике,
|
||
или явное действие вне интерфейса книги), и до него `resume` после потолка возвращает прогон
|
||
в то же состояние. Что при этом видит пользователь — К-8, вопрос владельцу; чем поднимают —
|
||
строка единого бэклога, которой нет.
|
||
|
||
---
|
||
|
||
## 4. Открытые вопросы
|
||
|
||
| # | Вопрос | Кому |
|
||
|---|---|---|
|
||
| К-1 | Десять статусов (§2.6) — принять или поправить? Три контракт выводит, а не получает | владелец / автор контракта |
|
||
| К-2 | Титул главы: отдать полем `heading` (предложено) — или оставить вклейку в текст, и фронт отрезает строкой? | автор контракта + бэкенд |
|
||
| К-3 | Метка главы в дереве: титул это ровно «Глава N», узлов 2284 — дерево одинаковых по форме строк | владелец (продуктовое) |
|
||
| К-4 | Ревизия: одна сквозная на прогон или своя на ресурс? И несут ли её чтения вообще (§2.10) | платформа |
|
||
| К-5 | Показывать ли оценку времени. **ПТ-19 существует** (`docs/product-requirements.md:49`: «видимый прогресс/ETA — из Ф3-видения ридер-IDE»), то есть посылка «не запрошено» неверна; вопрос в том, показываем ли в MVP | владелец (продуктовое) |
|
||
| К-6 | Ступени замечания: сколько их и где граница. Сегодняшние две — проекция ОПЕРАТОРСКОЙ лестницы рангов, а она не обязана совпадать с продуктовой осью | владелец (продуктовое) |
|
||
| К-7 | Пагинация: 2284 главы и 1200 терминов одним ответом или курсором? Фронт виртуализует, ему годится любой | платформа |
|
||
| К-8 | Стоп по потолку: каким статусом и каким словом? В `failed` мапить нельзя — стоп резюмируемый | владелец (продуктовое) |
|
||
| К-9 | **Отказ прескрина не выразим ни одним из десяти статусов.** Абьюз/misuse-прескрин до трат токенов и UI-контракт отказа — строка 94 (ПТ-16); книга, отклонённая прескрином, это не `rejected` (тот про неразобранный файл) и не `failed` | владелец + автор контракта |
|
||
| К-10 | **Выполнение главы — тот самый несплитованный счётчик, который §2.5 объявляет негодным.** `Chapter.units_done` не разведён по фазам, значит дерево глав показывает ноль всю черновую волну — ровно то, из-за чего прогресс книги сделан пофазным. Развести и тут (цена — пофазные счётчики НА ГЛАВУ в строке 99) или показывать в дереве другое | автор контракта + бэкенд |
|
||
| К-11 | **Условная обязательность полей — выражена у банка, не выражена у чтений.** У `BankDecision` констрейнт поставлен (`if action=promote → dst` непустой), и вот что это стоило, измерено: spectral его валидирует, а **openapi-typescript его игнорирует** — в генерённых типах `dst?: string` как был. То есть 3.1-условие защищает сервер, но не экран; клиентское сужение (юнион `promote`-с-`dst` ↔ `decline`) — работа подписного экрана S5, писать его до экрана не на чем проверить. Остаётся решить то же для чтений: `Note` не требует ни `chapter_id`, ни `unit_id`, `Unit.target` не обязателен при `translated`; обе схемы служат и вложенно, и отдельно, поэтому простое `required` соврало бы | автор контракта |
|
||
| К-12 | **Завершение выгрузки: опрос или событие?** Чтение `GET /books/{id}/exports/{id}` заведено — без него создающий вызов был тупиком (`ready:false` и ни слова дальше). Но пушить ли завершение ещё и кадром потока, чтобы не опрашивать, решает платформа: у неё воркер и её цена | платформа |
|
||
|
||
---
|
||
|
||
## 5. Проверка ревью-вопросом строки 95
|
||
|
||
**«Сменится стадия конвейера — придётся ли править фронт?»**
|
||
|
||
| Изменение в движке | Правит ли фронт |
|
||
|---|---|
|
||
| переименована стадия / добавлена волна | **нет** — имена стадий не пересекают шов, прогресс пофазный, а не постадийный |
|
||
| сменилась модель или маршрутизация | **нет** — `routing`/`content_labels` в allowlist не входят |
|
||
| добавлена новая причина флага | **нет** — на провод идёт продуктовая фраза, карта живёт в контракте |
|
||
| добавлен новый тип термина | **нет** — словарь расширяется минором, ветка неизвестного стоит на шве |
|
||
| добавлено новое продуктовое состояние | **да, один файл** — карта «статус → вид» на шве `src/api/`; это и есть контрольный вопрос владельца |
|
||
| сменился чанкер, главы пере-разобраны | **частично** — код фронта не правится (ключ непрозрачный, номер отображаемый), но **сохранность соответствия старых `id` новым главам контрактом не гарантируется**: это работа персиста манифеста (строка 100). Если соответствие потеряно, у пользователя разъезжаются открытые вкладки и закладки — не правка кода, но видимый ущерб, и решать его строке 100 |
|
||
|
||
Единственное безусловное «да» — то, которое и должно быть «да».
|
||
|
||
## 2.14. Поверхность входа `/auth/*` — ✓ построено платформой (внесено оркестратором №15 при лендинге S3)
|
||
|
||
Четыре ручки живут ВНЕ версионного префикса, как `/healthz`: это механика сессии, а не контрактная
|
||
поверхность, поэтому в `openapi.yaml` они не тащатся (решение оркестратора как владельца контракта,
|
||
подтверждено платформой).
|
||
|
||
| Ручка | Метод | Что делает |
|
||
|---|---|---|
|
||
| `/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`, как везде; различать причины отказа клиент не может по замыслу.
|
||
|
||
## 2.15. Потолок прогона — ◆ форма предложена фронтом, РАТИФИЦИРОВАНА оркестратором №15 (08.08)
|
||
|
||
Решение владельца 07.08: шкала в интерфейсе от минимума до максимума, ноль выбрать нельзя, единица —
|
||
ГЛАВЫ, потолок принадлежит ПРОГОНУ. Ручки, отдающей границы шкалы, в контракте не было — объявлена
|
||
правкой 0.2.0 как `GET /books/{bookId}/run-options` → `CeilingBounds`.
|
||
|
||
**Отдельный ресурс, а не поле карточки книги.** Максимум зависит от АККАУНТА и двигается, когда книга
|
||
не меняется: холд под другую книгу опускает остаток. Карточка книги кэшируется библиотекой, то есть
|
||
назвала бы максимум, которого уже нет, ровно когда человек двигает ползунок. Второй довод дешевле, но
|
||
настоящий: граница нужна один раз перед стартом, а поле на карточке заставило бы КАЖДОЕ чтение
|
||
библиотеки нести состояние счёта.
|
||
|
||
**Три числа, а не два.** `min_chapters` объясняет себя единицей — одна глава. `max_chapters` приходит
|
||
УЖЕ подрезанным и по остатку, и по непереведённому хвосту книги; клиенту подрезать второй раз
|
||
ЗАПРЕЩЕНО, иначе правило живёт в двух местах и расходится. `default_chapters` отдаёт платформа, потому
|
||
что предустановленное значение — продуктовая политика («потратить всё» ↔ «одна глава»), а не
|
||
презентация. `max_chapters: 0` — легальный ответ, значит «прогон начать нельзя вовсе»; тогда и
|
||
`default_chapters` равен нулю, а клиент показывает исчерпанное состояние вместо шкалы.
|
||
|
||
⚠ **`max_chapters` — величина, а не арифметика.** Ратификация D39.110 в первой редакции требовала
|
||
«баланс МИНУС открытые холды»: это была ОШИБКА оркестратора — вычитание дважды. Холд есть дебет в
|
||
момент взятия (`pgstore/credits.go:179` пишет отрицательную строку и тем же знаком двигает кэш
|
||
баланса), поэтому баланс уже не содержит открытых холдов. Замерено при приёмке на живом PostgreSQL:
|
||
грант $10 и холд $1 дают `Balance` 9 и `Reserved` 1, а «баланс минус Reserved» дало бы 8, то есть
|
||
вдвое урезанную шкалу. Ошибку нашла фронт-сессия S3 чтением Go-кода платформы.
|
||
|
||
**Пересчёта «главы → деньги» на проводе нет ни в каком виде** (D39.84) — он живёт на платформе по
|
||
оценке движка. **`ceiling_chapters` обязателен** и в запросе старта, и на `Run`: прогон без
|
||
объявленного потолка тратит мимо границы, которую человек вправе поставить ДО, а не узнавать после, а
|
||
поле на `Run` позволяет перезагруженному экрану назвать выбранный колпак. **`409` на старте** отвечает
|
||
и на «потолок больше не помещается»: границы читаются отдельным вызовом и могут сдвинуться.
|
||
|
||
## 2.16. Транспорт: тот же origin — ФАКТ, а не выбор (внесено оркестратором №15)
|
||
|
||
CORS-слоя в платформе нет вовсе: preflight `OPTIONS` с чужим `Origin` получает 401 от гарда сессии,
|
||
заголовков `Access-Control-*` нет ни на одном ответе (замер приёмки на живом бинаре, PD-96 регистра
|
||
платформы). Браузерный клиент с другого origin неработоспособен как класс. В деве фронт ходит через
|
||
прокси dev-сервера; кросс-origin не проектируется. `X-TM-Client` обязателен и на same-origin — он не
|
||
про CORS.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## Приложение А. Карта «вердикт → продуктовая фраза» — ЗАГОТОВКА
|
||
|
||
Заполняет автор контракта вместе с бэкендом и владельцем. **Правило: фраза пишется по
|
||
доккомменту `disposition.go`, а не по имени константы, и рядом кладётся цитата** — иначе
|
||
повторяется инверсия, стоившая двух фраз (`glossary_miss` подан как «термин не подписан»,
|
||
хотя термин ПОДПИСАН и его проигнорировали, `disposition.go:78-79`; `sanitizer_stripped` подан
|
||
как потеря текста, хотя «the chunk is NOT lost», `disposition.go:99`).
|
||
|
||
⚠ **Русские фразы ниже — плейсхолдеры, а не предложение фронта.** Словарь продуктовый, его
|
||
слова выбирает владелец (ПТ-33, В-3). Ступень — тоже: сегодняшние `attention`/`glance`
|
||
унаследовали ОПЕРАТОРСКУЮ ось рангов, а она не обязана совпадать с продуктовой (К-6).
|
||
|
||
| Причина движка | Ранг | Продуктовая фраза | Ступень |
|
||
|---|---|---|---|
|
||
| `hard_refusal` · `soft_refusal` · `content_filter` · `hard_block` | 0 | ⬜ | ⬜ |
|
||
| `cjk_artifact` · `excision_suspect` · `coverage_fail` | 1 | ⬜ | ⬜ |
|
||
| `sanitizer_defect` | 2 | ⬜ | ⬜ |
|
||
| `loop_degenerate` | 3 | ⬜ | ⬜ |
|
||
| `decode_error` | 4 | ⬜ | ⬜ |
|
||
| `glossary_miss` | 5 | плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ |
|
||
| `length` · `empty` | 6 | ⬜ | ⬜ |
|
||
| `sanitizer_stripped` | 7 | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
|
||
| `upstream_not_ok` | 8 (по умолчанию) | ⬜ | ⬜ |
|
||
| незнакомая причина | 8 (по умолчанию) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
|
||
|
||
Причин пятнадцать; `upstream_not_ok` в первой редакции отсутствовал — у него нет своей ветки
|
||
в `flagReasonSeverity`, поэтому он падает в ранг по умолчанию (`pipeline/status.go:174`),
|
||
как и любая будущая причина. Последняя строка — не формальность: контракт обязан иметь фразу
|
||
для причины, которой ещё не существует.
|