Ratify D39.99: API contract v0 lands in architecture home, row 95 closed with contract blockers reweighed, ping channels and prompt-review checkpoints written into canon
This commit is contained in:
parent
16e20e5884
commit
480333ded7
7 changed files with 1356 additions and 13 deletions
|
|
@ -18,8 +18,8 @@ Go-бэкенд издательского художественного пер
|
|||
| **Бэкенд** | `backend/` | нет — лендит оркестратор | читать можно; расхождения — пингом оркестратору в `docs/PROGRESS.md` |
|
||||
| **Полигон (eval)** | `eval/` + `docs/experiments/` | только пре-рег фризы | то же |
|
||||
| **Оркестратор** | `docs/` (architecture/research/PROGRESS) + корневые онбординг-доки, ревью чужого кода read-only | да | ратифицирует решения, пишет хендофф-промты |
|
||||
| **Фронт** | `frontend/` | нет — лендит оркестратор | то же; продуктовые требования — `docs/product-requirements.md`, НЕ из памяти |
|
||||
| **Платформа** | `platform/` (SaaS control plane: пользователи/квоты/очередь/HTTP; движок дёргает процессами, D39.81) | нет — лендит оркестратор | то же |
|
||||
| **Фронт** | `frontend/` | нет — лендит оркестратор | читать можно; пинги, итоги и вопросы — ЗОННЫЙ журнал `frontend/docs/frontend-PROGRESS.md` (решение владельца 02.08/04.08; в `docs/PROGRESS.md` фронт НЕ пишет), туда же пишет и оркестратор; продуктовые требования — `docs/product-requirements.md`, НЕ из памяти |
|
||||
| **Платформа** | `platform/` (SaaS control plane: пользователи/квоты/очередь/HTTP; движок дёргает процессами, D39.81) | нет — лендит оркестратор | читать можно; пинги и итоги — зонные доки `platform/`, в `docs/PROGRESS.md` не пишет |
|
||||
|
||||
Координация — журнал `docs/PROGRESS.md` (секции «Бэкенд»/«Полигон»/«Память»/«Голос и состояние»/«Ридер-IDE»; сверху CURRENT-STATE). Записывай туда краткие итоги своей сессии. Закрытые хроники вынесены в слайсы `docs/archive/PROGRESS-*.md` — НЕ читать при онбординге, только по конкретной ссылке. Параллельные сессии — норма: чужие незакоммиченные файлы в дереве не трогать; сессия считается ЖИВОЙ, пока владелец не сказал обратное.
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@
|
|||
## Методология (продолжай как предшественники — она себя оплатила)
|
||||
|
||||
- **Двухступенчатая верификация:** промты сессий несут мандат самопроверки ИСПОЛНЕНИЕМ (код+запросы+результаты); твоя пост-хок адверсариальная верификация при лендинге — второй рубеж, author≠reviewer, воркфлоу-инструментом, по СЫРЬЮ с пере-выводом чисел. Эра №6: рубеж-2 поймал CRITICAL-артефакт (окно судьи HEAD_CHARS) и спас контракт от трёх ложных заголовков — не ослабляй.
|
||||
- **Ревью хендофф-промтов — ПОСТРОЧНОЕ, не структурное (урок D39.99 п.4):** каждая инструкция промта с путём или каналом ВНЕ зоны сессии сверяется с зонной таблицей CLAUDE.md (куда пишет · кому пингует · кто коммитит); совпадение с общим каноном не оправдание — зонные решения владельца его перевешивают.
|
||||
- **Свип зонных решений владельца (урок D39.99 п.4):** при каждом лендинге зоны вычитать из зонного журнала НОВЫЕ «решения владельца» и вынести на confirm списком; решение, живущее только в зонном доке при противоречащем каноне, — дефект синхронизации, чинится ратификацией или опровержением, не молчанием.
|
||||
- **Анти-паттерны приёмки:** приёмка проверяет НАПРАВЛЕНИЕ и решения пака, не только клеймы отчёта. Шесть ловушек: след отчёта (проверяешь лишь названные автором места) · связность вместо истинности (форма отчёта убеждает сама) · соглашательство с чёткой позицией (распространенная ошибка ллм моделей склонных соглашаться с человеком) · рационализация задним числом · слепой участок общих моделей обученных на одинх и тех же данных (выводы одной модели усиливают уверенность в правильности другой) · экономия усилия на неудобном. Механика-минимум: ≥1 верификатор выносит мнение по промту+диффу ДО чтения отчёта · ≥1 ищет дефекты ВНЕ карты отчёта · экспериментальный клейм без ре-рана (хотя бы редуцированного) в D-ноте помечается «со слов сессии», не «исполнением» · «решение владельца», известное только со слов сессии, НЕ ратифицируется — список confirm/deny владельцу ДО лендинга, если решение несущее · к утверждениям самой приёмки та же дисциплина «заявление=команда» (догадку фактом не подавать) · оценочные эпитеты в ноты не писать.
|
||||
- **Пре-рег дисциплина полигона:** фриз коммитом ДО платных вызовов (единственный коммит сессии); изменения после = новый experiment-ID; девиации — явно в отчёте; стоп-гейты по бюджету легитимны; «если не влезает — стоп и пинг, не резать молча».
|
||||
- **Лендинг:** микро-дефекты доков чинишь сам с пометкой «испр. оркестратором»; отчёты получают ревью-шапку; код не правишь — находки в фикс-лист; коммиты скоуп-раздельные, стейджинг пофайловый; `git status` перед каждым коммитом (в дереве бывают ≥2 живые сессии) + **`git diff --cached` перед `commit`** — коммит уносит ВЕСЬ индекс: чужой staged `git mv` уедет в твой коммит (инцидент 77dd9b8); `add` общего файла сметает чужую секцию — diff-контент/`add -p`. **Лендинг, двигающий файлы или состав активных промтов, обновляет `docs/README.md` тем же коммитом** (норма D39.80; урок 02.08: README объявлял заленденный пак «можно запускать» с мёртвой ссылкой — поймал не процесс, а новая сессия). Статус/голову README не несёт вовсе — единственный носитель состояния = PROGRESS CURRENT-STATE. **Перед коммитом ратификации — механический чек головы:** `grep "голова D" docs/PROGRESS.md` обязан показать номер ТОЛЬКО ЧТО ратифицированной ноты (голова отставала дважды: D39.81 и D39.83 — оба раза «Текущее» обновлено, число головы забыто).
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -10,7 +10,7 @@
|
|||
- [product-requirements.md](product-requirements.md) — реестр «что продукт обязан уметь»: START_PROMT V0–V5 + находки H6–H18, статусы сверены кодом.
|
||||
- `architecture/` — синтез и контракты:
|
||||
- [05-decisions-log.md](architecture/05-decisions-log.md) — **источник истины по решениям**. Читать: карта актуальности (шапка) + живая голова (с хвоста); корпус D1–D38 — справочник по ссылкам/grep, целиком не читать.
|
||||
- [09-target-architecture.md](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (статус стройки — шапка-таблица; §3 — карта находок H/L) · [10-prompt-architecture.md](architecture/10-prompt-architecture.md) — промпт-слой · [12-go-style-notes.md](architecture/12-go-style-notes.md) — норматив общности §0 · [13-tech-debt-anchors.md](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер).
|
||||
- [09-target-architecture.md](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (статус стройки — шапка-таблица; §3 — карта находок H/L) · [10-prompt-architecture.md](architecture/10-prompt-architecture.md) — промпт-слой · [12-go-style-notes.md](architecture/12-go-style-notes.md) — норматив общности §0 · [13-tech-debt-anchors.md](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер) · [14-api-contract/](architecture/14-api-contract/) — **контракт API v0 фронт↔платформа (D39.99)**: нормативная спека OpenAPI 3.1 (типы фронта генерятся из неё) + компаньон-README (провенанс ✓/◆/○, К-вопросы; зонная копия `frontend/docs/api-contract/` — байт-зеркало, сверять при лендинге).
|
||||
- Исторические, читать через ⚠-баннеры: [01-decisions.md](architecture/01-decisions.md) (Р1–Р10) · [02-mvp-plan.md](architecture/02-mvp-plan.md) · [03-implementation-notes.md](architecture/03-implementation-notes.md) · [04-unhappy-paths.md](architecture/04-unhappy-paths.md) · [06-memory-risk-registry.md](architecture/06-memory-risk-registry.md).
|
||||
- `experiments/` — эмпирика полигона: [00-provider-quirks.md](experiments/00-provider-quirks.md) — **читать перед любым вызовом провайдера**; [08-cost-model-v2.md](experiments/08-cost-model-v2.md) — денежная модель; [09-pilot-protocol.md](experiments/09-pilot-protocol.md) — пилот Ф2.5; остальные 01–16 — отчёты закрытых экспериментов (судьба — в баннерах/D-логе).
|
||||
- `research/` — фактура ресёрчей 01–22; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (читать перед любым кодом стыка).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Журнал решений оркестратора — контракт D1–D39.98 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
# Журнал решений оркестратора — контракт D1–D39.99 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
|
||||
> **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто:
|
||||
> ⚠ **Навигация (актуализация 01.08):** карта ниже детально покрывает D1–D39.28; решения D39.29+ живут хронологически в теле файла, **свежая голова — С ХВОСТА** (новые ноты аппендятся вниз). Сводка текущей головы и очередь — CURRENT-STATE в `../PROGRESS.md`.
|
||||
|
|
@ -1432,3 +1432,15 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
|
|||
**3. Git-канон v2 заленден по носителям (решение владельца 04.08, правка `CLAUDE.md` его рукой): коммитит ТОЛЬКО оркестратор; право самокоммита фронта/платформы (D39.88) ОТОЗВАНО — supersede-запись = этот блок.** Носители: `CLAUDE.md` (таблица зон + гардрейлы) · промты фронта (переписаны фронт-сессией в своей зоне) · `platform/README.md` · подчистки D-лога/PROGRESS. ⚠ **Процедурная нота (D23.3):** синхронизация переписала тела D39.84 п.8 и D39.88 НА МЕСТЕ — снято в т.ч. дословное решение владельца 02.08; это против правила «исторические записи не переписываются». История цела в git (`2b166b7`, pre-rewrite HEAD); авторство подчистки — на подтверждении владельца; впредь supersede — только пометкой в карте + новым блоком.
|
||||
|
||||
**4. Открыто на владельце (+2 к списку D39.97):** confirm/deny двух «решений владельца 04.08» из журнала фронта («+» в левой панели закрывает половину Ф-7 · референсы возвращены на диск) · авторство подчистки D-лога (п.3). **В дереве появились нетрекнутые артефакты банк-арбитража** (`docs/research/24-bank-arbitration.md` · `eval/bank_arbitration/`) — заход строки 5, судя по всему, пере-исполнен; НЕ залендены, ждут отдельной приёмки. (04.08.2026, оркестратор №12). ✅
|
||||
|
||||
## D39.99 — КОНТРАКТ API v0 РАТИФИЦИРОВАН: строка 95 закрыта; форма = OpenAPI 3.1; три пасса приёмки, 19/19 находок вправлены; пост-мортем промт-ревью в канон (04.08). ✅
|
||||
|
||||
**1. Артефакт и форма.** S3 упёрлась в замок (контракта не существовало, строка 95 без промта) — по слову владельца написала вход Ф-14 (`frontend/docs/API_CONTRACT_INPUT.md`) и черновик контракта. **Решение владельца о форме: нормативная поверхность = OpenAPI 3.1** (`openapi.yaml`: JSON Schema моделей, spectral-линт шестым шагом `npm run check`, типы фронта ГЕНЕРЯТСЯ `openapi-typescript` с дрифт-тестом по голден-паттерну, MSW типизируется от них) — класс «фикстура выдумала форму», случившийся ЧЕТЫРЕЖДЫ (org/fullname · титульная политика · «Раздел N» · девять статусов), закрыт механически; прозаический md остался компаньоном (провенанс ✓/◆/○ · обоснования · К-вопросы). AsyncAPI отложен с записью (Ф-24). **Дом ратифицированной копии — `docs/architecture/14-api-contract/`** (спека байт-копией + компаньон-README с ревью-шапкой; байт-сверка копий — обязанность каждого лендинга; генерация типов далее из ратифицированной копии — контракт первичен).
|
||||
|
||||
**2. Приёмка — три адверсариальных воркфлоу-пасса, 0 опровергнутых из 19 подтверждённых** (черновик: 4 линзы×скептики, 2 high — «withheld=текст не выдан» ложно о движке (sanitizer_stripped и c-lite drop отгружают текст под флагом, `disposition.go:96-104`/`export.go:203-215`) и чтения без канала шва (текст юнитов посреди прогона · полный банк — строка 101 это таблица подписи, не экспорт) + 8 medium — сквозной паттерн «инфляция провенанса ✓ на ◆-предложениях»; round-1 дофикс: 6/6 применён, перепроверено кодом, генерённая цепочка подтверждена пере-генерацией байт-в-байт; round-2 свежий взгляд на новые схемы: 5 medium — экспорт ненаблюдаем · promote без dst (движок hard-reject, `memseed.go:84-86`) · закрытый required kind против легального пустого (`memseed.go:323-326`) · самопротиворечие §2.7 · resume после потолка без канала поднятия; все вправлены, включая if/then-констрейнт с честным замером «openapi-typescript игнорирует 3.1-условия» → К-11). Сессия сама завела К-10 (несплитованный счётчик главы) и К-12 (poll vs push экспорта). State выведен из ПАРЫ (вердикт, наличие текста); фразы приложения А — плейсхолдеры (В-3 за владельцем); ПТ-34-инвариант вписан (noindex · no-store · выгрузка только владельцу).
|
||||
|
||||
**3. Очередь.** Строка 95 ЗАКРЫТА. **99/100/101/103 перевешены «к подключению фронта» → «скоро, БЛОКЕРЫ контракта»** (аргументы сессии подтверждены кодом: без 100 у главы нет стабильного id — нумерация плотная, `chunker.go:99-105`; без 99 индикатор 0% всю черновую волну — `status.go:328-331`; без 101 подписной экран читает сайдкар с капом 20 — `cmd/tmctl/render.go:98`). НОВЫЕ строки: **125** банк-экспорт артефакт (канал `/bank`) · **126** механизм поднятия потолка (платформа; продуктовое слово — К-8) · **127** DDL-коммент `migrate.go:193` устарел (`auto` не пишет никто — фантом утёк во фронт-доки дважды). Счёт скриптом: 103 строки · скоро 21 · бэкенд 58/66.
|
||||
|
||||
**4. Пост-мортем процесса (вопрос владельца «где мы ошиблись на берегу») — три слоя, фиксы в канон.** (а) Ошибка последовательности оркестратора: ратифицирован S3-промт с мёртвым основным сценарием (замок закрыт, промта строки 95 не существовало) — фронт три сессии упирался в один вход. (б) Ревью промта было структурным, не построчным: инструкция «пинг в docs/PROGRESS.md» дословно следовала таблице ролей CLAUDE.md, но противоречила решению владельца 02.08 из шапки зонного журнала. (в) Системная дыра: решения владельца, принятые в зонных сессиях, не доезжают до канона. **Фиксы:** CLAUDE.md — пинг-канал фронта/платформы = ЗОННЫЙ журнал (`frontend/docs/frontend-PROGRESS.md` — переименован по слову владельца, писать туда всем включая оркестратора), не docs/PROGRESS.md; промт оркестратора — два чек-пункта (хендофф-промты сверяются ПОСТРОЧНО: каждая инструкция с путём/каналом вне зоны — против зонной таблицы; при каждом лендинге зоны — свип новых «решений владельца» из зонного журнала на confirm списком); шапка `frontend/docs/BACKLOG.md` разведена «предлагать обязан / ратифицирует оркестратор» (просьба №3 сессии — прежняя формулировка спровоцировала остановку первого захода).
|
||||
|
||||
**5. Открыто на владельце:** продуктовые К контракта — К-1 (десять статусов) · К-3 (метка главы в дереве) · К-5 (ETA в MVP; ПТ-19 существует) · К-6 (ось ступеней замечаний — операторская унаследована) · К-8 (слово и статус стопа по потолку) + заполнение приложения А (В-3) + прежний список D39.98 п.4 (confirm двух решений · авторство подчистки · приёмка банк-арбитража). **Следующий разговор на берегу — бутстрап платформы П-1: у второй стороны контракта ноль строк кода, К-4/К-7/К-12 адресованы зоне, которой нет** (урок строки 95 — не ждать, пока три сессии упрутся). (04.08.2026, оркестратор №12). ✅
|
||||
|
|
|
|||
339
docs/architecture/14-api-contract/README.md
Normal file
339
docs/architecture/14-api-contract/README.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
<!-- ======================================================================
|
||||
РЕВЬЮ-ШАПКА ОРКЕСТРАТОРА (ратификация 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 —
|
||||
платформа (П-1, кода ещё нет); К-2/К-9/К-10/К-11 — автор контракта/бэкенд.
|
||||
Зависимости §3 покрыты строками единого бэклога: 99/100/101/103 (перевешены в
|
||||
БЛОКЕРЫ контракта) + новые 125 (банк-экспорт) · 126 (поднятие потолка) · 127
|
||||
(DDL-коммент). Приложение А — заготовка: фразы и ступени заполняет владелец (В-3).
|
||||
|
||||
Тело ниже — авторский текст фронт-сессии S3 (два раунда дофикса), не переписано.
|
||||
====================================================================== -->
|
||||
|
||||
# Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
|
||||
|
||||
> **Нормативная поверхность контракта — [`api-contract/openapi.yaml`](api-contract/openapi.yaml)**
|
||||
> (OpenAPI 3.1). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято
|
||||
> каждое решение, чем оно обосновано, что осталось открытым. При расхождении по ФОРМЕ
|
||||
> побеждает YAML; при вопросе «почему так» — этот файл.
|
||||
>
|
||||
> **Статус: черновик фронт-сессии S3 на ратификацию.** Дом ратифицированной копии —
|
||||
> `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 |
|
||||
|
||||
Единственное безусловное «да» — то, которое и должно быть «да».
|
||||
|
||||
---
|
||||
|
||||
## Приложение А. Карта «вердикт → продуктовая фраза» — ЗАГОТОВКА
|
||||
|
||||
Заполняет автор контракта вместе с бэкендом и владельцем. **Правило: фраза пишется по
|
||||
доккомменту `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`),
|
||||
как и любая будущая причина. Последняя строка — не формальность: контракт обязан иметь фразу
|
||||
для причины, которой ещё не существует.
|
||||
983
docs/architecture/14-api-contract/openapi.yaml
Normal file
983
docs/architecture/14-api-contract/openapi.yaml
Normal file
|
|
@ -0,0 +1,983 @@
|
|||
openapi: 3.1.0
|
||||
|
||||
info:
|
||||
title: TextMachine API
|
||||
version: 0.1.0-draft
|
||||
summary: Contract between the frontend and the TextMachine platform (draft, awaiting ratification).
|
||||
description: |
|
||||
**DRAFT of frontend session S3 — not a ratified contract.** The ratified copy belongs in
|
||||
`docs/architecture/14-api-contract/` (orchestrator's zone).
|
||||
|
||||
Companion document: `../API_CONTRACT_DRAFT.md` — provenance of every decision (derived from
|
||||
engine code / proposed by the frontend / open), rationale, dependencies and open questions.
|
||||
This file is normative for the FORM; the companion explains where the form comes from.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The frontend reads the platform read-model only. The engine is never addressed by any path
|
||||
below (D39.85).
|
||||
|
||||
Pipeline vocabulary does not cross this boundary: no model names, no stage names, no money.
|
||||
The read-model → frontend projection is an allowlist — a field not named here never reaches
|
||||
the browser.
|
||||
|
||||
No response is served from an indexable URL. The app lives on `app.<domain>` under
|
||||
`X-Robots-Tag: noindex`; responses carrying translated text MUST be sent with
|
||||
`Cache-Control: no-store`.
|
||||
|
||||
## Versioning
|
||||
|
||||
Semver. **Minor** — backwards-compatible additions: a client MUST ignore unknown fields and
|
||||
MUST tolerate unknown enum values without failing. **Major** — a client MUST refuse an
|
||||
unsupported version and tell the user.
|
||||
|
||||
Every `enum` below is the vocabulary of version 0.1, not a closed world. Generated types are
|
||||
closed unions and do NOT protect against an unknown value, so the unknown-value branch
|
||||
belongs on the client seam (`src/api/`), where values enter, not in every component.
|
||||
|
||||
license:
|
||||
name: UNLICENSED
|
||||
identifier: LicenseRef-proprietary
|
||||
|
||||
servers:
|
||||
- url: https://app.example.org/v0
|
||||
description: Platform. The base path is a frontend proposal; the platform confirms it.
|
||||
|
||||
security:
|
||||
- sessionCookie: []
|
||||
- bearerToken: []
|
||||
|
||||
tags:
|
||||
- name: library
|
||||
description: Book library and book card.
|
||||
- name: reading
|
||||
description: Chapters, source/translation pairs, notes.
|
||||
- name: bank
|
||||
description: Memory bank and term signing.
|
||||
- name: runs
|
||||
description: Translation runs, live progress, control.
|
||||
- name: export
|
||||
description: Export of a finished book.
|
||||
|
||||
paths:
|
||||
/books:
|
||||
get:
|
||||
tags: [library]
|
||||
operationId: listBooks
|
||||
summary: Book library.
|
||||
description: |
|
||||
Flat list of the user's books. `revision` is the revision of the LIBRARY itself
|
||||
(membership and statuses), not of any run. Revision scope is open — companion §4 (K-4).
|
||||
responses:
|
||||
'200':
|
||||
description: Library.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Library' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [library]
|
||||
operationId: createBook
|
||||
summary: Add a book.
|
||||
description: |
|
||||
Accepts the file and the properties declared by the user. Responds immediately; the book
|
||||
enters `uploading` and parsing is a separate visible step.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema: { $ref: '#/components/schemas/BookIntake' }
|
||||
responses:
|
||||
'201':
|
||||
description: Book accepted.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Book' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'413': { $ref: '#/components/responses/TooLarge' }
|
||||
|
||||
/books/{bookId}:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
get:
|
||||
tags: [library]
|
||||
operationId: getBook
|
||||
summary: Book card.
|
||||
description: Book metadata plus the current or last run.
|
||||
responses:
|
||||
'200':
|
||||
description: Book card.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BookDetail' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/chapters:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
get:
|
||||
tags: [reading]
|
||||
operationId: listChapters
|
||||
summary: Chapter tree.
|
||||
description: |
|
||||
Chapters in reading order. A chapter has NO status, only unit progress: bank signing is a
|
||||
single book-wide stop, so "one chapter awaits signing while its neighbour finalizes"
|
||||
cannot happen.
|
||||
responses:
|
||||
'200':
|
||||
description: Chapters of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ChapterList' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/chapters/{chapterId}/units:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
- $ref: '#/components/parameters/ChapterId'
|
||||
get:
|
||||
tags: [reading]
|
||||
operationId: listUnits
|
||||
summary: Source/translation pairs of a chapter.
|
||||
description: |
|
||||
The unit of shipping is the EDIT UNIT, not a paragraph and not a chunk: roughly 1.9 units
|
||||
per chapter, and a whole chapter is sometimes a single block. Alignment is coarse and
|
||||
accepted as such.
|
||||
responses:
|
||||
'200':
|
||||
description: Pairs of the chapter.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/UnitList' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/notes:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
get:
|
||||
tags: [reading]
|
||||
operationId: listNotes
|
||||
summary: Notes of a book.
|
||||
description: |
|
||||
A note addresses a unit or a whole chapter. Byte offsets do not exist in the engine's
|
||||
checks and are not planned.
|
||||
responses:
|
||||
'200':
|
||||
description: Notes of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/NoteList' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/bank:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
get:
|
||||
tags: [bank]
|
||||
operationId: listBankTerms
|
||||
summary: Memory bank of a book.
|
||||
description: |
|
||||
⚠ **No backing channel exists for this read today.** The engine ships a signing table,
|
||||
not a bank export, and its private store must not be read by the platform. The export
|
||||
artifact is a dependency the frontend cannot create — companion §3.
|
||||
responses:
|
||||
'200':
|
||||
description: Bank of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Bank' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/bank/decisions:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
post:
|
||||
tags: [bank]
|
||||
operationId: submitBankDecisions
|
||||
summary: Submit term decisions.
|
||||
description: |
|
||||
**Signing is not a row edit.** The pipeline replaces a book's whole glossary from its
|
||||
deterministic inputs, so a direct write would be erased by the next run. A decision is
|
||||
`promote` (with a translation) or `decline`, following the stop mechanics exactly.
|
||||
|
||||
Submission is PARTIAL and accumulates on the server: there are hundreds of terms, and a
|
||||
closed tab must not cost an hour of work.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BankDecisionsRequest' }
|
||||
responses:
|
||||
'200':
|
||||
description: Decisions accepted; the response carries what is left.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BankDecisionsResult' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/runs:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
post:
|
||||
tags: [runs]
|
||||
operationId: startRun
|
||||
summary: Start a translation run.
|
||||
description: |
|
||||
`verify_bank` is a parameter of the RUN, not a global setting. With it the run stops at
|
||||
the bank boundary; without it the unsigned bank is carried forward marked as unverified.
|
||||
It is the user's choice between "I will sign" and "translate as is".
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/RunRequest' }
|
||||
responses:
|
||||
'202':
|
||||
description: Run accepted.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Run' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
|
||||
/runs/{runId}/events:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RunId'
|
||||
get:
|
||||
tags: [runs]
|
||||
operationId: streamRunEvents
|
||||
summary: Live run events (SSE).
|
||||
description: |
|
||||
`text/event-stream`. Required: HTTP/2 at the edge, `Cache-Control: no-store`,
|
||||
`X-Accel-Buffering: no`, a heartbeat every ~20 s, a monotonic `id` and `Last-Event-ID`
|
||||
support. Events are PUSHED by the platform worker; the frontend never polls the
|
||||
read-model.
|
||||
|
||||
**The first event is always `hello`** — the version handshake. A client that does not
|
||||
support the major version closes the stream and tells the user.
|
||||
|
||||
**Reconnect.** The client sends `Last-Event-ID`. If the server cannot resume from it, it
|
||||
MUST answer with `resync_required` instead of silently starting from the present moment;
|
||||
the client then re-reads snapshots. Replaying history is forbidden — a one-shot event
|
||||
such as `note` would otherwise be lost silently.
|
||||
|
||||
OpenAPI does not type SSE frames, so the event name → payload schema mapping is a table
|
||||
in the `EventEnvelope` description.
|
||||
responses:
|
||||
'200':
|
||||
description: Event stream.
|
||||
content:
|
||||
text/event-stream:
|
||||
schema: { $ref: '#/components/schemas/EventEnvelope' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/runs/{runId}/stop:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RunId'
|
||||
post:
|
||||
tags: [runs]
|
||||
operationId: stopRun
|
||||
summary: Stop a run.
|
||||
description: |
|
||||
The product "stop" action. The engine stops gracefully on a signal; deciding who pressed
|
||||
it belongs to the platform.
|
||||
responses:
|
||||
'202':
|
||||
description: Stop accepted.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Run' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
|
||||
/runs/{runId}/resume:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/RunId'
|
||||
post:
|
||||
tags: [runs]
|
||||
operationId: resumeRun
|
||||
summary: Resume a stopped run.
|
||||
description: |
|
||||
Clears the bank-signing stop and continues after a user stop.
|
||||
|
||||
⚠ **After a ceiling stop this call alone does not move the run:** the engine continues
|
||||
only once the ceiling has been raised, and no channel for raising it exists here — money
|
||||
is absent from the MVP interface by decision. The mechanism belongs to the platform
|
||||
(companion §3), and what the user sees meanwhile is companion §4 (K-8).
|
||||
|
||||
**Answers 409 while the set of bank decisions is incomplete** — the stop clears only on a
|
||||
complete set.
|
||||
responses:
|
||||
'202':
|
||||
description: Resume accepted.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Run' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
|
||||
/books/{bookId}/exports:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
post:
|
||||
tags: [export]
|
||||
operationId: createExport
|
||||
summary: Build a book export.
|
||||
description: Formats and their contents are stage S7 work; only the call shape is fixed here.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ExportRequest' }
|
||||
responses:
|
||||
'202':
|
||||
description: Export is being prepared.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Export' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/exports/{exportId}:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
- $ref: '#/components/parameters/ExportId'
|
||||
get:
|
||||
tags: [export]
|
||||
operationId: getExport
|
||||
summary: State of an export.
|
||||
description: |
|
||||
Without this read the creating call is a dead end: it answers `ready: false` and nothing
|
||||
ever says otherwise. Whether completion is ALSO pushed as a stream event, making polling
|
||||
unnecessary, is the platform's call — companion §4 (K-12).
|
||||
responses:
|
||||
'200':
|
||||
description: State of the export.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Export' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
sessionCookie:
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: __Host-tm_session
|
||||
description: |
|
||||
Browser presentation of one server-side session: HttpOnly, Secure, SameSite=Lax. CSRF
|
||||
protection is mandatory on the cookie path. The form belongs to the platform.
|
||||
bearerToken:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: |
|
||||
Desktop and CLI present the same server-side session as an opaque token. The principal is
|
||||
established in middleware only; no endpoint may assume a cookie — that is what keeps the
|
||||
API portable to the desktop client.
|
||||
|
||||
parameters:
|
||||
BookId:
|
||||
name: bookId
|
||||
in: path
|
||||
required: true
|
||||
description: Opaque book identifier.
|
||||
schema: { $ref: '#/components/schemas/Id' }
|
||||
ChapterId:
|
||||
name: chapterId
|
||||
in: path
|
||||
required: true
|
||||
description: Opaque chapter identifier.
|
||||
schema: { $ref: '#/components/schemas/Id' }
|
||||
RunId:
|
||||
name: runId
|
||||
in: path
|
||||
required: true
|
||||
description: Opaque run identifier.
|
||||
schema: { $ref: '#/components/schemas/Id' }
|
||||
ExportId:
|
||||
name: exportId
|
||||
in: path
|
||||
required: true
|
||||
description: Opaque export identifier.
|
||||
schema: { $ref: '#/components/schemas/Id' }
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Request rejected.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema: { $ref: '#/components/schemas/Problem' }
|
||||
Unauthorized:
|
||||
description: Session missing or invalid.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema: { $ref: '#/components/schemas/Problem' }
|
||||
NotFound:
|
||||
description: Object not found.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema: { $ref: '#/components/schemas/Problem' }
|
||||
Conflict:
|
||||
description: Action impossible in the current state.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema: { $ref: '#/components/schemas/Problem' }
|
||||
TooLarge:
|
||||
description: File exceeds the intake size limit.
|
||||
content:
|
||||
application/problem+json:
|
||||
schema: { $ref: '#/components/schemas/Problem' }
|
||||
|
||||
schemas:
|
||||
Id:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: |
|
||||
Opaque identifier. The client MUST NOT parse, sort by or construct it. Stability across
|
||||
runs is the platform's job when it persists the manifest.
|
||||
examples: ['bk_7c1']
|
||||
|
||||
Revision:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: |
|
||||
Monotonic revision of a resource. A read whose revision is LOWER than an event the client
|
||||
already applied MUST be dropped rather than rendered, otherwise the interface rolls
|
||||
progress backwards on every refetch.
|
||||
examples: [1841]
|
||||
|
||||
LangCode:
|
||||
type: string
|
||||
pattern: '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$'
|
||||
description: |
|
||||
Language code, never a name. The display name is computed by the screen via
|
||||
`Intl.DisplayNames`.
|
||||
examples: ['zh']
|
||||
|
||||
Counter:
|
||||
type: object
|
||||
description: Wave counter, in UNITS.
|
||||
required: [done, total]
|
||||
properties:
|
||||
done: { type: integer, minimum: 0 }
|
||||
total: { type: integer, minimum: 0 }
|
||||
|
||||
Progress:
|
||||
type: object
|
||||
description: |
|
||||
Progress PER PHASE, in units: a unit is done only once its edit resolved, and editing does
|
||||
not start before the bank stop, so a single end-to-end counter reads zero for the whole
|
||||
draft wave.
|
||||
|
||||
The phases exist for the DATA, not for the screen — the user sees one fraction with no
|
||||
phase names. No ready-made percentage is shipped: the formula is a product decision.
|
||||
required: [draft, edit]
|
||||
properties:
|
||||
draft: { $ref: '#/components/schemas/Counter' }
|
||||
edit: { $ref: '#/components/schemas/Counter' }
|
||||
|
||||
BookStatus:
|
||||
type: string
|
||||
description: |
|
||||
Product status of a book: `uploading` file is being accepted · `parsing` split into
|
||||
chapters · `not_started` parsed, never run · `translating` translation in progress ·
|
||||
`awaiting_bank` waiting for the glossary to be signed · `finalizing` final pass ·
|
||||
`ready` done · `stopped` stopped by the user · `rejected` file could not be parsed ·
|
||||
`failed` run aborted by an error.
|
||||
|
||||
The engine has no run-state vocabulary at all, so `not_started`, `stopped` and `rejected`
|
||||
are DERIVED by the contract rather than received as a field.
|
||||
|
||||
**A ceiling stop is NOT `failed`.** It is a resumable book-wide stop; mapping it to
|
||||
`failed` is forbidden, as that would lie about resumability. Which status and which word
|
||||
the user sees is an open product question — companion §4 (K-8).
|
||||
|
||||
A prescreen refusal maps to none of these values either — companion §4 (K-9).
|
||||
enum:
|
||||
- uploading
|
||||
- parsing
|
||||
- not_started
|
||||
- translating
|
||||
- awaiting_bank
|
||||
- finalizing
|
||||
- ready
|
||||
- stopped
|
||||
- rejected
|
||||
- failed
|
||||
|
||||
Book:
|
||||
type: object
|
||||
description: A book in the library.
|
||||
required:
|
||||
[id, title, source_lang, target_lang, status, chapter_count, added_at, progress, note_count]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
title: { type: string }
|
||||
source_lang: { $ref: '#/components/schemas/LangCode' }
|
||||
target_lang: { $ref: '#/components/schemas/LangCode' }
|
||||
genre: { type: string, description: Genre as declared by the user. }
|
||||
chapter_count: { type: integer, minimum: 0 }
|
||||
character_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Size in characters. Not an engine field; the platform knows it from intake.
|
||||
added_at: { type: string, format: date-time }
|
||||
status: { $ref: '#/components/schemas/BookStatus' }
|
||||
progress: { $ref: '#/components/schemas/Progress' }
|
||||
note_count: { type: integer, minimum: 0 }
|
||||
|
||||
Library:
|
||||
type: object
|
||||
required: [revision, books]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
books:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Book' }
|
||||
|
||||
Run:
|
||||
type: object
|
||||
description: |
|
||||
A run over a book. `status` reuses the book vocabulary, but the book-level values
|
||||
(`uploading`, `parsing`, `not_started`, `rejected`) never appear on a run.
|
||||
required: [id, revision, status, verify_bank, started_at]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
status: { $ref: '#/components/schemas/BookStatus' }
|
||||
verify_bank:
|
||||
type: boolean
|
||||
description: The run was requested with a stop for bank signing.
|
||||
started_at: { type: string, format: date-time }
|
||||
finished_at:
|
||||
type: [string, 'null']
|
||||
format: date-time
|
||||
|
||||
BookDetail:
|
||||
type: object
|
||||
required: [revision, book]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
book: { $ref: '#/components/schemas/Book' }
|
||||
run:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Run'
|
||||
- type: 'null'
|
||||
description: Current or last run; `null` if the book was never run.
|
||||
|
||||
BookIntake:
|
||||
type: object
|
||||
description: Add-a-book form.
|
||||
required: [file, source_lang, target_lang]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
description: Book file.
|
||||
source_lang: { $ref: '#/components/schemas/LangCode' }
|
||||
target_lang: { $ref: '#/components/schemas/LangCode' }
|
||||
genre: { type: string }
|
||||
|
||||
Chapter:
|
||||
type: object
|
||||
required: [id, number, units_total, units_done, note_count]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
number:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: |
|
||||
Displayed ordinal. **Not a key:** numbering is dense — chapters that yield no text do
|
||||
not consume a number — so editing the source shifts every later chapter.
|
||||
heading:
|
||||
type: string
|
||||
description: |
|
||||
Chapter title as a field is a frontend PROPOSAL: today the engine glues the rendered
|
||||
title into the text of the first unit and leaves the source column without it.
|
||||
Unresolved — companion §4 (K-2).
|
||||
units_total: { type: integer, minimum: 0 }
|
||||
units_done:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: |
|
||||
Units finished in this chapter. Whether "finished" needs the same draft/edit split as
|
||||
`Progress` — without it a chapter reads zero for the whole draft wave — is open,
|
||||
companion §4 (K-10).
|
||||
note_count: { type: integer, minimum: 0 }
|
||||
|
||||
ChapterList:
|
||||
type: object
|
||||
required: [revision, chapters]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
chapters:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Chapter' }
|
||||
|
||||
UnitState:
|
||||
type: string
|
||||
description: |
|
||||
State of a pair, derived from the PAIR (chunk verdict plus presence of final text), not
|
||||
from the verdict alone: a flagged unit legally arrives WITH text.
|
||||
|
||||
- `translated` — text shipped. This includes a flagged unit whose text shipped anyway,
|
||||
such as a cosmetic sanitizer cleanup; such a unit carries a `note`;
|
||||
- `withheld` — verdict flagged AND no text;
|
||||
- `pending` — not translated yet.
|
||||
|
||||
The word "flagged" never goes on the wire: it is pipeline vocabulary.
|
||||
enum: [translated, withheld, pending]
|
||||
|
||||
Unit:
|
||||
type: object
|
||||
description: |
|
||||
A source/translation pair, one edit unit wide.
|
||||
|
||||
**Freshness.** `target` is updated at stage boundaries and at stops, not continuously —
|
||||
mid-run there is no read channel at all. The live "something changed" signal arrives as an
|
||||
event; the text arrives with a read after the boundary.
|
||||
required: [id, source, state]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
source:
|
||||
type: string
|
||||
description: Source text, aligned to the edit unit.
|
||||
target:
|
||||
type: string
|
||||
description: Translated text. Empty for `withheld` and `pending`.
|
||||
state: { $ref: '#/components/schemas/UnitState' }
|
||||
note:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/Note'
|
||||
- type: 'null'
|
||||
|
||||
UnitList:
|
||||
type: object
|
||||
required: [revision, units]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
units:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Unit' }
|
||||
|
||||
NoteSeverity:
|
||||
type: string
|
||||
description: |
|
||||
Severity step. Two steps are a frontend PROPOSAL projected from the engine's operator
|
||||
severity ranks, and that axis need not match the product one. How many steps there are and
|
||||
where the boundary lies is an open product question — companion §4 (K-6).
|
||||
enum: [attention, glance]
|
||||
|
||||
Note:
|
||||
type: object
|
||||
description: |
|
||||
A note in product terms. Neither the engine's flag reason nor its detail text crosses the
|
||||
boundary; the reason → phrase map belongs to the contract and is not filled in this draft
|
||||
(companion, appendix A).
|
||||
required: [severity, message]
|
||||
properties:
|
||||
severity: { $ref: '#/components/schemas/NoteSeverity' }
|
||||
message:
|
||||
type: string
|
||||
description: |
|
||||
Ready human phrase; the wording is the owner's call. A phrase MUST exist for every
|
||||
reason, including one this contract does not know yet, and it MUST read neutrally
|
||||
rather than as an error.
|
||||
chapter_id: { $ref: '#/components/schemas/Id' }
|
||||
unit_id: { $ref: '#/components/schemas/Id' }
|
||||
|
||||
NoteList:
|
||||
type: object
|
||||
required: [revision, notes]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
notes:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Note' }
|
||||
|
||||
TermKind:
|
||||
type: string
|
||||
description: |
|
||||
Kind of term. Not cosmetic: `name` and `place` ROUTE a term into transliteration, so
|
||||
signing a term without seeing its kind means signing blind.
|
||||
enum: [name, place, title, term, nickname]
|
||||
|
||||
TermStatus:
|
||||
type: string
|
||||
description: |
|
||||
Signing status, THREE-VALUED; only `approved` is injected as canon. A boolean `signed`
|
||||
would merge "proposed by the engine, nobody looked" with "a human started and did not
|
||||
finish" — on a screen of hundreds of rows that is the main filter of work.
|
||||
enum: [auto, draft, approved]
|
||||
|
||||
TermOrigin:
|
||||
type: string
|
||||
description: Provenance of a bank row — who created it. An axis independent of `status`.
|
||||
enum: [seed, ruby, mined]
|
||||
|
||||
BankTerm:
|
||||
type: object
|
||||
description: |
|
||||
A memory bank row.
|
||||
|
||||
⚠ **The name `source` is deliberately unused here.** In the engine that column means
|
||||
PROVENANCE; this contract calls provenance `origin` and the term's surfaces `src`/`dst`.
|
||||
Naming the term's text `source` would create a false friend between the two schemas.
|
||||
required: [id, src, dst, kind, status, origin, since_chapter, until_chapter]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
src:
|
||||
type: string
|
||||
description: Source surface of the term.
|
||||
dst:
|
||||
type: string
|
||||
description: Translation; empty for a candidate with no proposed form.
|
||||
kind:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/TermKind'
|
||||
- type: 'null'
|
||||
description: |
|
||||
`null` when the engine could not decide the kind: ruby candidates that are neither a
|
||||
name nor a place legally carry none. A client MUST show such a row as "kind not
|
||||
decided" and MUST NOT drop it or invent a kind — the row still needs signing.
|
||||
status: { $ref: '#/components/schemas/TermStatus' }
|
||||
origin: { $ref: '#/components/schemas/TermOrigin' }
|
||||
sense:
|
||||
type: string
|
||||
description: Polysemy disambiguator; part of the uniqueness key.
|
||||
since_chapter:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: |
|
||||
Start of the spoiler window; `0` means from the beginning of the book. A term is
|
||||
unique by `(book, src, sense, since, until)`, so the same `src` legally arrives as
|
||||
several rows — without the window they look like duplicates and get deleted.
|
||||
until_chapter:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: End of the spoiler window; `0` means open-ended.
|
||||
|
||||
Bank:
|
||||
type: object
|
||||
required: [revision, total, signed, terms]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
total: { type: integer, minimum: 0 }
|
||||
signed:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: How many rows are in status `approved`.
|
||||
terms:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/BankTerm' }
|
||||
|
||||
BankDecision:
|
||||
type: object
|
||||
description: |
|
||||
A decision on one proposed term. `dst` is mandatory and non-empty for `promote`: the
|
||||
engine refuses a signed term with an empty translation on the next run, because such a
|
||||
term matches nothing yet reads as an intended rendering.
|
||||
required: [term_id, action]
|
||||
if:
|
||||
properties:
|
||||
action: { const: promote }
|
||||
required: [action]
|
||||
then:
|
||||
required: [dst]
|
||||
properties:
|
||||
dst: { minLength: 1 }
|
||||
properties:
|
||||
term_id: { $ref: '#/components/schemas/Id' }
|
||||
action:
|
||||
type: string
|
||||
enum: [promote, decline]
|
||||
description: '`promote` — accept the term (with a translation in `dst`); `decline` — reject it.'
|
||||
dst:
|
||||
type: string
|
||||
description: Translation. Required and non-empty when `action` is `promote`.
|
||||
|
||||
BankDecisionsRequest:
|
||||
type: object
|
||||
required: [decisions]
|
||||
properties:
|
||||
decisions:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { $ref: '#/components/schemas/BankDecision' }
|
||||
|
||||
BankDecisionsResult:
|
||||
type: object
|
||||
required: [revision, pending_decisions, complete]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
pending_decisions:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: How many proposed terms still have no decision.
|
||||
complete:
|
||||
type: boolean
|
||||
description: |
|
||||
The set is complete. The stop clears ONLY on a complete set, so the screen must show
|
||||
"N of M decided" and must not offer to resume while this is `false`.
|
||||
|
||||
RunRequest:
|
||||
type: object
|
||||
required: [verify_bank]
|
||||
properties:
|
||||
verify_bank:
|
||||
type: boolean
|
||||
description: Stop for bank signing before the final pass.
|
||||
|
||||
ExportRequest:
|
||||
type: object
|
||||
required: [format]
|
||||
properties:
|
||||
format:
|
||||
type: string
|
||||
description: Export format; the set of formats is stage S7 work.
|
||||
|
||||
Export:
|
||||
type: object
|
||||
required: [id, ready]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
ready: { type: boolean }
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
description: |
|
||||
Link to the finished export. Served to the authenticated owner only and never
|
||||
indexed.
|
||||
|
||||
EventEnvelope:
|
||||
type: object
|
||||
description: |
|
||||
An SSE frame. OpenAPI does not type stream frames, so the mapping is fixed here:
|
||||
|
||||
| `event` | `data` schema | When |
|
||||
|---|---|---|
|
||||
| `hello` | `EventHello` | always the first frame |
|
||||
| `status` | `EventStatus` | product status changed |
|
||||
| `progress` | `EventProgress` | counters moved |
|
||||
| `chapter` | `EventChapter` | a chapter's progress changed |
|
||||
| `note` | `EventNote` | a note appeared |
|
||||
| `bank` | `EventBank` | the bank changed or a signing stop occurred |
|
||||
| `ceiling` | `EventCeiling` | the run was halted by a ceiling |
|
||||
| `resync_required` | `EventResyncRequired` | resuming the stream is impossible |
|
||||
|
||||
The frame `id` carries a monotonic revision. Whether it is the same counter that reads
|
||||
carry is open — companion §4 (K-4).
|
||||
required: [event, data]
|
||||
properties:
|
||||
event: { type: string }
|
||||
data:
|
||||
description: |
|
||||
Frame payload. Schemas are listed as a union rather than tied by a discriminator:
|
||||
`event` lives in the SSE frame, not inside `data`, so an OpenAPI discriminator does
|
||||
not apply. Dispatch by event name, per the table above.
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/EventHello'
|
||||
- $ref: '#/components/schemas/EventStatus'
|
||||
- $ref: '#/components/schemas/EventProgress'
|
||||
- $ref: '#/components/schemas/EventChapter'
|
||||
- $ref: '#/components/schemas/EventNote'
|
||||
- $ref: '#/components/schemas/EventBank'
|
||||
- $ref: '#/components/schemas/EventCeiling'
|
||||
- $ref: '#/components/schemas/EventResyncRequired'
|
||||
|
||||
EventHello:
|
||||
type: object
|
||||
description: Version handshake. A client that does not support the major version closes the stream.
|
||||
required: [contract, run_id, revision]
|
||||
properties:
|
||||
contract:
|
||||
type: string
|
||||
description: Contract version, e.g. `0.1.0`.
|
||||
run_id: { $ref: '#/components/schemas/Id' }
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
|
||||
EventStatus:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status: { $ref: '#/components/schemas/BookStatus' }
|
||||
|
||||
EventProgress:
|
||||
type: object
|
||||
required: [progress]
|
||||
properties:
|
||||
progress: { $ref: '#/components/schemas/Progress' }
|
||||
|
||||
EventChapter:
|
||||
type: object
|
||||
required: [chapter_id, units_done, note_count]
|
||||
properties:
|
||||
chapter_id: { $ref: '#/components/schemas/Id' }
|
||||
units_done: { type: integer, minimum: 0 }
|
||||
note_count: { type: integer, minimum: 0 }
|
||||
|
||||
EventNote:
|
||||
type: object
|
||||
description: |
|
||||
A note appeared. Depends on the event emitter — the engine does not emit per-unit notes
|
||||
mid-run today (companion §3).
|
||||
required: [note]
|
||||
properties:
|
||||
note: { $ref: '#/components/schemas/Note' }
|
||||
|
||||
EventBank:
|
||||
type: object
|
||||
required: [total, signed, pending_decisions]
|
||||
properties:
|
||||
total: { type: integer, minimum: 0 }
|
||||
signed: { type: integer, minimum: 0 }
|
||||
pending_decisions: { type: integer, minimum: 0 }
|
||||
|
||||
EventCeiling:
|
||||
type: object
|
||||
description: |
|
||||
The run hit a ceiling and halted. **Carries no figures** — the fact of the stop, not a
|
||||
sum: money does not appear in the MVP interface at all. The stop is resumable, so it does
|
||||
not yield `failed`. Depends on the event emitter (companion §3).
|
||||
required: [halted]
|
||||
properties:
|
||||
halted: { type: boolean }
|
||||
|
||||
EventResyncRequired:
|
||||
type: object
|
||||
description: |
|
||||
The server cannot resume the stream from the presented `Last-Event-ID`. The client MUST
|
||||
re-read snapshots. Replaying history is forbidden — a one-shot event such as `note` would
|
||||
be lost silently.
|
||||
required: [reason]
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
description: Product-level reason; carries no internals.
|
||||
|
||||
Problem:
|
||||
type: object
|
||||
description: |
|
||||
Error per RFC 9457. ⚠ `detail` NEVER carries engine text: the engine's own detail strings
|
||||
read like "CJK leak in the ru output: 第一节", which exposes how the pipeline works. Here
|
||||
`detail` is either empty or already a product phrase.
|
||||
required: [type, title, status]
|
||||
properties:
|
||||
type: { type: string, format: uri }
|
||||
title: { type: string }
|
||||
status: { type: integer }
|
||||
detail: { type: string }
|
||||
instance: { type: string, format: uri-reference }
|
||||
Loading…
Add table
Reference in a new issue