Ratify the S3 data layer, sync the canon contract to 0.2.0 with the companion blocks, and correct my own instruction that told the frontend to subtract holds twice
This commit is contained in:
parent
60595f644e
commit
f0d8fedc4f
4 changed files with 476 additions and 56 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
# Журнал решений оркестратора — контракт D1–D39.114 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
# Журнал решений оркестратора — контракт D1–D39.115 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
|
||||
> **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто:
|
||||
> ⚠ **Навигация (актуализация 07.08, эра D39.1xx):** append-only-дисциплина (D23.3) означает, что
|
||||
|
|
@ -1566,7 +1566,7 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
|
|||
|
||||
**1. Решение владельца 07.08 (дословно по смыслу):** потолок управляется двумя вещами — «выключить потолок» и «выставить вручную с фронта»; форма — шкала в интерфейсе от минимума (ноль выбрать НЕЛЬЗЯ, подсвечен серым) до максимума баланса аккаунта, конкретный виджет на выбор исполнителя. **Ратифицировано формой, и форма сама снимает конфликт с Р7:** максимум = баланс ⇒ движок всегда получает положительное число, состояния «потолка нет» не существует, валидатор `book.go:250-251` («a ledger with no ceiling is forbidden, Р7») не правится и Р7 не амендируется. Носитель — пере-поставленная строка **126**.
|
||||
|
||||
**2. Три следствия, без которых форма неполна (добор оркестратора, каждое проверено кодом).** **(а) Единица шкалы — НЕ доллары, а главы (либо % баланса).** Тогда минимум объясняет себя сам («одна глава», ниже прогон бессмыслен — владелец сам отметил, что непонятно, какое минимальное число ставить), а **D39.84 остаётся цел**: читатель не видит структуру наших расходов, платформа сама переводит главы в доллары по оценке движка. Доллары на ПОЛЬЗОВАТЕЛЬСКОМ экране = осознанный амендмент D39.84, и это отдельное решение владельца, а не побочный эффект виджета (П-5 показывает процент ровно по этой причине). **(б) Это потолок ПРОГОНА, значит он не пишется в `book.yaml`.** Дизайн платформы объявлял её владельцем `book.yaml` и намеревался поднимать `ceilings.book_usd` правкой файла — тогда пользовательское число становится постоянной записью в данных движка, что смешивает зоны против D39.81/D39.85. В коде этого НЕТ (проверено: платформа только ставит `cmd.Dir`), значит форма выбирается бесплатно ⇒ **НОВАЯ строка 145**: движок принимает потолок аргументом прогона; сегодня ни флага, ни ENV не существует (`tmctl` таких флагов не имеет, значение идёт только через `Ceilings.BookUSD`, `stagerun.go:480,505`). Цена нулевая: `Ceilings` в `BriefHash` не входит (`book.go:280-297`) — ни снапшот, ни ре-билл не двигаются. **(в) Максимум шкалы = баланс МИНУС открытые холды.** Платформа берёт холд на сумму потолка ДО спавна, поэтому ползунок в упоре запирает весь баланс на одну книгу и вторую начать нельзя; `ReadAccount` уже отдаёт `Balance` и `Reserved` раздельно, данных хватает.
|
||||
**2. Три следствия, без которых форма неполна (добор оркестратора, каждое проверено кодом).** **(а) Единица шкалы — НЕ доллары, а главы (либо % баланса).** Тогда минимум объясняет себя сам («одна глава», ниже прогон бессмыслен — владелец сам отметил, что непонятно, какое минимальное число ставить), а **D39.84 остаётся цел**: читатель не видит структуру наших расходов, платформа сама переводит главы в доллары по оценке движка. Доллары на ПОЛЬЗОВАТЕЛЬСКОМ экране = осознанный амендмент D39.84, и это отдельное решение владельца, а не побочный эффект виджета (П-5 показывает процент ровно по этой причине). **(б) Это потолок ПРОГОНА, значит он не пишется в `book.yaml`.** Дизайн платформы объявлял её владельцем `book.yaml` и намеревался поднимать `ceilings.book_usd` правкой файла — тогда пользовательское число становится постоянной записью в данных движка, что смешивает зоны против D39.81/D39.85. В коде этого НЕТ (проверено: платформа только ставит `cmd.Dir`), значит форма выбирается бесплатно ⇒ **НОВАЯ строка 145**: движок принимает потолок аргументом прогона; сегодня ни флага, ни ENV не существует (`tmctl` таких флагов не имеет, значение идёт только через `Ceilings.BookUSD`, `stagerun.go:480,505`). Цена нулевая: `Ceilings` в `BriefHash` не входит (`book.go:280-297`) — ни снапшот, ни ре-билл не двигаются. **(в) Максимум шкалы = доступный остаток аккаунта.** ⚠ **ИСПРАВЛЕНО 08.08 приёмкой S3 (D39.115): первая редакция этого подпункта говорила «баланс МИНУС открытые холды» — это ОШИБКА, вычитание дважды.** `Hold` пишет в леджер отрицательную строку и тем же знаком двигает кэш баланса (`credits.go:179` → `appendLedger` `:297-305`), поэтому баланс УЖЕ не содержит открытых холдов; `Reserved` — отдельная справочная сумма. Замерено приёмкой на живом PG: грант $10, холд $1 → `Balance=9.000000`, `Reserved=1.000000`, а «баланс минус Reserved» дало бы $8. Поймала фронт-сессия S3 чтением Go-кода платформы, а не доков — и ради этого нарушила мой же запрет ходить в чужую зону. Платформа берёт холд на сумму потолка ДО спавна, поэтому ползунок в упоре запирает весь баланс на одну книгу и вторую начать нельзя; `ReadAccount` уже отдаёт `Balance` и `Reserved` раздельно, данных хватает.
|
||||
|
||||
**3. Фри-тир — вопрос владельца («а как это обычно делается в бэкендах?»), ответ и ПРЕДЛОЖЕНИЕ, а не ратификация.** Разделяется на две половины, и стандартный ответ есть только у одной. **Где живёт начисление:** учебник — промо-кредит пишется строкой леджера в ТОЙ ЖЕ транзакции, что создание аккаунта, идемпотентно по `(аккаунт, source, source_id)`; код зоны это уже делает верно (`upsertIdentityTx`), менять нечего. **Стреляет ли само:** норма — «автоматически, но с ЯКОРЕМ, который стоит абьюзеру денег» (карта на файле — AWS/GCP; телефон — OpenAI добавил его к своим $5 после фарминга; инвайт), плюс бюджет кампании отдельной суммой и алерт при упоре. `email_verified` от Google якорем НЕ является — аккаунты бесплатны и создаются пачками, признак доказывает существование ящика, а не отдельного человека. У нас нет ни одного из трёх, а деньги — предоплаченные ключи владельца. **Предложение: на бете дефолт в НОЛЬ и начисление руками** (платёжного инструмента нет ⇒ между скриптом и ключами нет ничего; бета-тестеров столько, что одна команда в шелле на человека не работа); при появлении платежей дефолт возвращается к $5 ВМЕСТЕ с суточным агрегатным потолком — якорь и потолок приезжают по одному поводу. Ждёт слова владельца; носитель — PD-104.
|
||||
|
||||
|
|
@ -1621,3 +1621,19 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу
|
|||
**4. Ратифицировано отдельно: PD-104 оставлен открытым ПРАВИЛЬНО.** Зона починила половину док↔код (ячейка PD-30 больше не обещает нулевой аккаунт при автогранте) и строку не закрыла, потому что продуктовая половина — ноль на бете, суточный агрегатный потолок, счётчик аномалий — ждёт слова владельца. Это правило PD-83 («свойство без пина закрытым не считается»), применённое зоной к себе.
|
||||
|
||||
**5. Оговорка зоны принята как честная:** адверсариального ревью вторым читателем у P3 не было — сессия без права на субагентов. Второй читатель — эта приёмка. **Живой счёт регистра руками больше не переписывается** (норма D39.112): `python3 docs/scripts/counts.py`; исторические числа в CURRENT-STATE помечены историческими, а литерал веса снят с автопроверки, потому что легитимного живого носителя в зоне `docs` у него нет.
|
||||
|
||||
## D39.115 — S3 «СЛОЙ ДАННЫХ» ПРИНЯТ И ЗАЛЕНДЕН: контракт 0.2.0 в каноне, три ошибки ОРКЕСТРАТОРА поймала сама сессия, правило 0.x ратифицировано (08.08). ✅
|
||||
|
||||
**1. Вердикт:** ПРИНЯТ, заленден `60595f6` (46 файлов). Пере-прогнано мной: `npm run check` — 108 тестов, prettier/eslint/stylelint/tsc чисты, spectral по спеке без замечаний; `npm run check:full` — сборка плюс СЕМЬ кадров с axe, включая новый маршрут `/partial` (отказал один рид при рабочих остальных — ветка жила незамеченной именно потому, что её нельзя было открыть по ссылке). Кадр `/scale` прочитан глазами: пер-главный счётчик замечаний на месте, и честно оставленный Ф-30 виден прямо на экране — глава без заголовка показана голым номером «12», а не выдуманной «Главой N».
|
||||
|
||||
**2. Три моих ошибки, найденные сессией. Это главный результат пака.** **(а) «Максимум шкалы = баланс МИНУС открытые холды» (D39.110 п.2(в), строка 126, промт S3 п.17) — НЕВЕРНО, вычитание дважды.** `Hold` пишет отрицательную строку в леджер и тем же знаком двигает кэш баланса (`credits.go:179` → `appendLedger` `:297-305`), значит баланс УЖЕ не содержит открытых холдов, а `Reserved` — справочная сумма. Пере-мерено мной на живом PostgreSQL: грант $10 и холд $1 дают `Balance` 9.000000 при `Reserved` 1.000000, а моя формула дала бы 8 — шкала вдвое короче правды. Исправлено во всех трёх носителях. **(б) Форма `nullable: true`,** которую я вписал в промт для `GET /usage`, в OpenAPI 3.1 невалидна; сессия выразила через `oneOf` с `type: 'null'`, как во всей остальной спеке. **(в) Инструкция ВНУТРИ нормативного дока протухает от исполнения:** я оставил в шапке спеки фразу «version всё ещё `-draft` — S3 бампнет первым действием», и после бампа она стала ложной; сессия заменила её на правду и назвала это отступлением вслух. Тот же класс, что рукописные производные числа (D39.112): предписание, записанное в артефакт, живёт до первого действия.
|
||||
|
||||
**3. Норму «не ходить в чужую зону» АМЕНДИРУЮ, потому что она и была источником (а).** Сессия объявила вслух, что нарушила мой запрет и читала Go платформы (только чтение, ничего не тронуто) — и ровно этим нашла ошибку в моей ратификации. Новая формулировка: **в чужую зону НЕ ПИСАТЬ; ЧИТАТЬ код для сверки несущего утверждения — можно и НУЖНО**, потому что доки врут, и это норма проекта («не верь заголовкам, грунтуй `file:line`»). Запрет читать превращал сессию в заложника моего текста. Отдельно: раскрытие нарушения вслух — правильное поведение, и его надо поощрять, а не наказывать.
|
||||
|
||||
**4. Ратифицировано.** Форма управляемого потолка — `GET /books/{bookId}/run-options` → `CeilingBounds` отдельным ресурсом, а не полем карточки книги (максимум зависит от АККАУНТА и двигается, когда книга не менялась; карточка кэшируется библиотекой и назвала бы протухший максимум ровно в момент выбора) · три числа вместо двух, `max_chapters` приходит уже подрезанным и клиенту подрезать второй раз запрещено, `default_chapters` — продуктовая политика платформы, `max_chapters: 0` легален и означает «прогон начать нельзя» · `ceiling_chapters` обязателен в запросе старта и на `Run` · `409` покрывает «потолок больше не помещается». **Правило версий 0.x:** минорный бамп — законная полоса ломающих правок (semver §4), клиент пинит точную 0.x-версию; с 1.0.0 действует общее правило. Сессия НЕ стала править раздел Versioning своей рукой, а назвала противоречие — это верно, правило версий принадлежит владельцу контракта; вписано мной в обе копии. **Блоки компаньона 2.14 (поверхность входа) · 2.15 (потолок) · 2.16 (тот же origin — факт, CORS-слоя нет)** внесены в канон; патч-файл `S3_CONTRACT_HANDOFF.md` применён и удалён, как он сам себя и объявил.
|
||||
|
||||
**5. Канон и зонная копия снова байт-равны** (`ba7f3745…`), spectral по обеим чист.
|
||||
|
||||
**6. На владельце — три вопроса, сессия их назвала сама.** (а) Метка «остановлена: лимиты» в дереве — сжатие ратифицированной фразы «перевод остановлен: лимиты исчерпаны», сделанное сессией; продуктовые слова по В-3/Ф-21 не её. (б) **К-13 (Ф-31): упереться в СВОЙ потолок прогона и остаться без кредита сегодня неразличимо** — `paused_reason` имеет одно значение `credit_exhausted`, и первому из двух фраза врёт, а следующее действие у него другое. (в) Ф-30: глава без заголовка показана голым порядковым номером, потому что синтез «Глава N» запрещён К-3; это вопрос В-4, уже стоящий владельцу. Плюс три поля, придуманные сессией по её собственному признанию и ратифицированные мной выше: `default_chapters`, `maxLength: 200` у заголовка, `paused_reason` в кадре статуса.
|
||||
|
||||
**7. Открытые хвосты зоны, заведённые честно:** Ф-29 (мок едет в прод-бандл — цена того, что скриншот-цикл гоняет прод-сборку; папка удаляется в день живой платформы) · Ф-32 (замечания читаются пер-книжно, показываются пер-главно: экран починен, память нет — на книге с тысячами замечаний клиент тянет весь список ради трёх строк) · `ceiling_chapters` обязателен сегодня бесплатно, после первого реального прогона это станет ломающей правкой.
|
||||
|
|
|
|||
|
|
@ -317,6 +317,66 @@ is promoted or rejected» (`pipeline/mining.go:201`). Отсюда: решени
|
|||
|
||||
Единственное безусловное «да» — то, которое и должно быть «да».
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Приложение А. Карта «вердикт → продуктовая фраза» — ЗАГОТОВКА
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ openapi: 3.1.0
|
|||
|
||||
info:
|
||||
title: TextMachine API
|
||||
version: 0.1.0-draft
|
||||
version: 0.2.0
|
||||
summary: Ratified contract between the frontend and the TextMachine platform (D39.99).
|
||||
description: |
|
||||
**RATIFIED contract (D39.99, 04.08.2026).** The canonical copy lives in
|
||||
`docs/architecture/14-api-contract/` (orchestrator's zone); `frontend/docs/api-contract/` is a
|
||||
byte-mirror of it, and a divergence is a defect of one of the two, never a local edit.
|
||||
⚠ Text corrected by orchestrator #15 on 07.08: this file called itself an unratified draft of
|
||||
session S3 for three days after it was ratified, and pointed at a companion path that does not
|
||||
exist beside the canonical copy. The version string still reads `-draft` — session S3 bumps it
|
||||
as the first item of its spec work.
|
||||
⚠ **This copy is deliberately AHEAD of the canonical one right now.** Version 0.2.0 was written
|
||||
by session S3 from the owner's decisions of 04–07.08 and the platform's ratified answers; the
|
||||
orchestrator re-ratifies the canonical copy by diff, and the two are byte-equal again after
|
||||
that landing.
|
||||
|
||||
Companion document: the `README.md` beside the canonical copy — provenance of every decision
|
||||
(derived from engine code / proposed by the frontend / open), rationale, dependencies and open
|
||||
|
|
@ -30,13 +30,34 @@ info:
|
|||
`X-Robots-Tag: noindex`; responses carrying translated text MUST be sent with
|
||||
`Cache-Control: no-store`.
|
||||
|
||||
## Transport
|
||||
|
||||
The browser client is served from the **same origin** as this API. That is a fact of the
|
||||
platform, not a setting: it has no CORS layer at all — a preflight `OPTIONS` carrying a foreign
|
||||
`Origin` is answered `401` by the session guard, and no `Access-Control-*` header is sent on any
|
||||
response. A cross-origin browser client is therefore inoperable as a class rather than
|
||||
unconfigured, and nothing here is designed around cross-origin requests. A development server
|
||||
reaches this API through a proxy onto its own origin.
|
||||
|
||||
Session mechanics — starting a login, finishing it, ending one session, ending all of them —
|
||||
live OUTSIDE the version prefix, like `/healthz`, and are described in the companion rather
|
||||
than here: they are the mechanics of holding a session, not a contract surface.
|
||||
|
||||
## 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
|
||||
⚠ **While the version is 0.x, a MINOR bump is the lane for breaking changes** (semver §4:
|
||||
"Major version zero … anything MAY change at any time"), and 0.2.0 carries several — a required
|
||||
`next_cursor` on every list, a required `ceiling_chapters` on a run request. A client pins the
|
||||
exact 0.x version it was generated against and does not assume compatibility across minors.
|
||||
From 1.0.0 the rule above applies unqualified. Ratified by the orchestrator at the S3 landing:
|
||||
session S3 named the contradiction between this section and its own bump instead of resolving
|
||||
it by its own hand, which was correct — the versioning rule is the contract owner's.
|
||||
|
||||
Every `enum` below is the vocabulary of THIS version, 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.
|
||||
|
||||
|
|
@ -46,7 +67,10 @@ info:
|
|||
|
||||
servers:
|
||||
- url: https://app.example.org/v0
|
||||
description: Platform. The base path is a frontend proposal; the platform confirms it.
|
||||
description: |
|
||||
The platform, on the SAME ORIGIN as the browser client (see Transport). Only the version
|
||||
prefix is fixed here: the host is whatever origin served the application, and a client that
|
||||
hard-codes one is a client that cannot be deployed anywhere else.
|
||||
|
||||
security:
|
||||
- sessionCookie: []
|
||||
|
|
@ -63,6 +87,8 @@ tags:
|
|||
description: Translation runs, live progress, control.
|
||||
- name: export
|
||||
description: Export of a finished book.
|
||||
- name: account
|
||||
description: Credit balance of the account.
|
||||
|
||||
paths:
|
||||
/books:
|
||||
|
|
@ -72,13 +98,21 @@ paths:
|
|||
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).
|
||||
(membership and statuses) and belongs to the library's own scope: it is never compared
|
||||
with the revision of a book.
|
||||
|
||||
Page size default is the platform's choice here — the client MUST follow `next_cursor`
|
||||
until it is `null` rather than assume the library fits in one page.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Library.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Library' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [library]
|
||||
|
|
@ -130,12 +164,18 @@ paths:
|
|||
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.
|
||||
|
||||
Default page size **5000**.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Chapters of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ChapterList' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
|
|
@ -151,12 +191,21 @@ paths:
|
|||
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.
|
||||
|
||||
**Units are read PER CHAPTER and only per chapter.** A book-wide units endpoint is never
|
||||
introduced: the whole memory model of the client stands on this — the working set stays
|
||||
tens of kilobytes instead of tens of megabytes. Page size default is the platform's choice;
|
||||
the client follows `next_cursor`.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Pairs of the chapter.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/UnitList' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
|
|
@ -170,12 +219,18 @@ paths:
|
|||
description: |
|
||||
A note addresses a unit or a whole chapter. Byte offsets do not exist in the engine's
|
||||
checks and are not planned.
|
||||
|
||||
Default page size **500**.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Notes of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/NoteList' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
|
|
@ -190,12 +245,18 @@ paths:
|
|||
⚠ **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.
|
||||
|
||||
Default page size **1000**.
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Cursor'
|
||||
responses:
|
||||
'200':
|
||||
description: Bank of the book.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Bank' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
|
|
@ -228,6 +289,28 @@ paths:
|
|||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/run-options:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
get:
|
||||
tags: [runs]
|
||||
operationId: getRunOptions
|
||||
summary: Bounds for starting a run.
|
||||
description: |
|
||||
Bounds of the run-ceiling scale, read right before a run is started.
|
||||
|
||||
A resource of its own rather than a field of the book card: the maximum depends on the
|
||||
ACCOUNT and moves while the book does not, so a cached card would state a maximum that is no
|
||||
longer true — at the moment the user is dragging the scale.
|
||||
responses:
|
||||
'200':
|
||||
description: Bounds of the scale.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/RunOptions' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/books/{bookId}/runs:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
|
|
@ -239,6 +322,13 @@ paths:
|
|||
`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".
|
||||
|
||||
`ceiling_chapters` is a parameter of the RUN as well, and it is not a property of the
|
||||
book: it travels with the start and does not outlive the run.
|
||||
|
||||
**409** also answers a ceiling that no longer fits: the bounds are read by
|
||||
`GET /books/{bookId}/run-options` and may move between that read and this call, because a
|
||||
hold taken for another book lowers the remainder.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
@ -250,6 +340,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Run' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
|
|
@ -316,10 +407,12 @@ paths:
|
|||
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).
|
||||
⚠ **After a ceiling stop this call alone does not move the run.** The engine continues only
|
||||
once the ceiling has been raised, and no handle raises it: `ceiling_chapters` travels with
|
||||
the START of a run and this contract has no way to change it afterwards. The mechanism is
|
||||
the platform's and does not exist yet (companion §3); until it does, `resume` on a run
|
||||
paused by a ceiling returns it to the same state, so the client MUST NOT offer resume as
|
||||
the remedy for `paused`.
|
||||
|
||||
**Answers 409 while the set of bank decisions is incomplete** — the stop clears only on a
|
||||
complete set.
|
||||
|
|
@ -333,6 +426,25 @@ paths:
|
|||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
|
||||
/usage:
|
||||
get:
|
||||
tags: [account]
|
||||
operationId: getUsage
|
||||
summary: State of the credit balance.
|
||||
description: |
|
||||
Credits are a BALANCE, not a subscription with windows. There is no period, no `resets_at`
|
||||
and no "resets in": the screen shows what is LEFT. The percentage is computed against the
|
||||
sum of the account's grants, not against a limit of a period — periods do not exist.
|
||||
|
||||
Money SUMS never cross this boundary in any form: a percentage, never an amount.
|
||||
responses:
|
||||
'200':
|
||||
description: Balance state.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Usage' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/books/{bookId}/exports:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/BookId'
|
||||
|
|
@ -340,7 +452,15 @@ paths:
|
|||
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.
|
||||
description: |
|
||||
Formats and their contents are stage S7 work; only the call shape is fixed here.
|
||||
|
||||
The export is an ARTIFACT BEHIND A LINK. Assembling the text of a book on the client —
|
||||
reading every chapter and stitching it together — is forbidden explicitly: it would defeat
|
||||
the per-chapter working set that the read paths are built around.
|
||||
|
||||
Completion is POLLED, not pushed: the `202` names the status resource in `Location`, and
|
||||
the status read carries `Retry-After`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
|
@ -349,6 +469,11 @@ paths:
|
|||
responses:
|
||||
'202':
|
||||
description: Export is being prepared.
|
||||
headers:
|
||||
Location:
|
||||
required: true
|
||||
description: Address of the status resource for this export.
|
||||
schema: { type: string, format: uri-reference }
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Export' }
|
||||
|
|
@ -365,11 +490,19 @@ paths:
|
|||
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).
|
||||
ever says otherwise. **Completion is polled, not pushed** — no stream frame announces it.
|
||||
responses:
|
||||
'200':
|
||||
description: State of the export.
|
||||
headers:
|
||||
Retry-After:
|
||||
description: |
|
||||
Seconds to wait before polling again; sent while `ready` is `false`.
|
||||
|
||||
Declared here on purpose. RFC 9110 defines this header for `503` and for `3xx`,
|
||||
and its general semantics do not reach a `200`, so a contract that wants it on a
|
||||
`200` has to say so itself.
|
||||
schema: { type: integer, minimum: 0 }
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Export' }
|
||||
|
|
@ -383,8 +516,15 @@ components:
|
|||
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.
|
||||
Browser presentation of one server-side session: HttpOnly, Secure, SameSite=Lax.
|
||||
|
||||
**CSRF.** On the cookie path a browser client MUST send the header `X-TM-Client` on every
|
||||
UNSAFE request — anything other than GET and HEAD. What carries the protection is the
|
||||
PRESENCE of the header; the value is arbitrary and has no token semantics, so do not invent
|
||||
any. It is required on same-origin requests as well: it is not a CORS mechanism.
|
||||
|
||||
The same requirement holds for the session-mechanics endpoints that live outside the
|
||||
version prefix (companion).
|
||||
bearerToken:
|
||||
type: http
|
||||
scheme: bearer
|
||||
|
|
@ -418,6 +558,26 @@ components:
|
|||
required: true
|
||||
description: Opaque export identifier.
|
||||
schema: { $ref: '#/components/schemas/Id' }
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Page size. The default is stated per collection on the operation; a server MAY return
|
||||
fewer rows than asked for, and the client decides nothing from that — only from
|
||||
`next_cursor`.
|
||||
schema: { type: integer, minimum: 1 }
|
||||
Cursor:
|
||||
name: cursor
|
||||
in: query
|
||||
required: false
|
||||
description: |
|
||||
Keyset cursor taken from `next_cursor` of the previous page. Opaque: the client MUST NOT
|
||||
parse, compare or construct it. Omitted for the first page.
|
||||
|
||||
A cursor that no longer applies is rejected with `400`; see `NextCursor` for why that
|
||||
rejection is the server's duty and not the client's.
|
||||
schema: { type: string, minLength: 1 }
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
|
|
@ -459,11 +619,41 @@ components:
|
|||
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.
|
||||
Monotonic revision. **The counter is PER BOOK:** every book-scoped read and the `id` of
|
||||
every stream frame of that book's run carry the same number. The library has a scope of its
|
||||
own.
|
||||
|
||||
A revision is monotonic WITHIN its scope and is NEVER compared across scopes.
|
||||
|
||||
Discarding a stale read is the CLIENT's duty: a read whose revision is lower than what the
|
||||
client has already applied MUST be dropped rather than rendered, otherwise the interface
|
||||
rolls progress backwards on every refetch — and a refetch on window focus is the default
|
||||
behaviour of the client's query layer, so the race happens on every tab switch.
|
||||
|
||||
**Catch-up after a reconnect reads `revision >= R`, not `> R`.** One transaction is one
|
||||
revision but SEVERAL frames; strict "greater than" drops the sibling frames of the last
|
||||
one the client applied.
|
||||
|
||||
After a transaction of FULL REPLACEMENT — the bank rebuilt from scratch, re-chunking
|
||||
replacing the chapters — the server MUST answer `resync_required` rather than a delta: a
|
||||
delta read cannot express a deletion.
|
||||
examples: [1841]
|
||||
|
||||
NextCursor:
|
||||
type: [string, 'null']
|
||||
description: |
|
||||
Cursor of the NEXT page, or `null` on the last one. Present on EVERY list response,
|
||||
always — introducing it later would silently cut the tail off a client that does not read
|
||||
the field.
|
||||
|
||||
The cursor is bound to the STRUCTURAL epoch of the collection — the generation of the
|
||||
manifest, the chunker version — and **not to the revision of the book**: the revision bumps
|
||||
on every materialization, so binding to it would restart pagination forever while a
|
||||
5000-chapter book is running.
|
||||
|
||||
Rejecting a cursor from a dead epoch is the SERVER's duty (MUST), answered `400`. The
|
||||
client cannot perform it: the cursor is opaque to it by construction.
|
||||
|
||||
LangCode:
|
||||
type: string
|
||||
pattern: '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$'
|
||||
|
|
@ -493,6 +683,13 @@ components:
|
|||
properties:
|
||||
draft: { $ref: '#/components/schemas/Counter' }
|
||||
edit: { $ref: '#/components/schemas/Counter' }
|
||||
eta_seconds:
|
||||
type: [integer, 'null']
|
||||
minimum: 0
|
||||
description: |
|
||||
Estimated seconds to the end of the run. **Optional:** it is absent whenever there is
|
||||
nothing to estimate from — before the first calls of a wave there is no throughput yet
|
||||
— and the screen MUST render without it rather than show a zero.
|
||||
|
||||
BookStatus:
|
||||
type: string
|
||||
|
|
@ -500,15 +697,15 @@ components:
|
|||
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.
|
||||
`ready` done · `paused` halted and resumable · `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 ceiling stop is `paused`, never `failed`.** It is a resumable book-wide stop, and
|
||||
mapping it to `failed` is forbidden because that would lie about resumability. The machine
|
||||
reason travels as `Run.paused_reason`; the phrase the user reads is drawn by the client.
|
||||
|
||||
A prescreen refusal maps to none of these values either — companion §4 (K-9).
|
||||
enum:
|
||||
|
|
@ -519,6 +716,7 @@ components:
|
|||
- awaiting_bank
|
||||
- finalizing
|
||||
- ready
|
||||
- paused
|
||||
- stopped
|
||||
- rejected
|
||||
- failed
|
||||
|
|
@ -546,19 +744,30 @@ components:
|
|||
|
||||
Library:
|
||||
type: object
|
||||
required: [revision, books]
|
||||
required: [revision, next_cursor, books]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
next_cursor: { $ref: '#/components/schemas/NextCursor' }
|
||||
books:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Book' }
|
||||
|
||||
PausedReason:
|
||||
type: string
|
||||
description: |
|
||||
Machine reason a run is paused. The API carries STATE; the phrase the user reads is drawn
|
||||
by the client, so no wording appears here.
|
||||
|
||||
One value exists today. A client MUST tolerate an unknown one arriving under a minor bump
|
||||
and show the neutral "halted, resumable" state rather than failing or guessing.
|
||||
enum: [credit_exhausted]
|
||||
|
||||
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]
|
||||
required: [id, revision, status, verify_bank, ceiling_chapters, paused_reason, started_at]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
|
|
@ -566,6 +775,18 @@ components:
|
|||
verify_bank:
|
||||
type: boolean
|
||||
description: The run was requested with a stop for bank signing.
|
||||
ceiling_chapters:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: |
|
||||
The ceiling this run was started with, in CHAPTERS. A property of the RUN, not of the
|
||||
book: it travels with the start and does not outlive the run. Present so that a reloaded
|
||||
screen can still name the cap the user chose.
|
||||
paused_reason:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/PausedReason'
|
||||
- type: 'null'
|
||||
description: Reason when `status` is `paused`; `null` in every other state.
|
||||
started_at: { type: string, format: date-time }
|
||||
finished_at:
|
||||
type: [string, 'null']
|
||||
|
|
@ -598,21 +819,35 @@ components:
|
|||
|
||||
Chapter:
|
||||
type: object
|
||||
required: [id, number, units_total, units_done, note_count]
|
||||
required: [id, number, heading, units_total, units_done, note_count]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
number:
|
||||
type: integer
|
||||
type: [integer, 'null']
|
||||
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.
|
||||
Displayed ordinal, or `null` when the book has no numbering — a legal book. Always
|
||||
present, possibly null, so the client handles one shape rather than two.
|
||||
|
||||
**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
|
||||
type: [string, 'null']
|
||||
maxLength: 200
|
||||
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).
|
||||
The chapter's label **as it comes from the data of the book**, or `null` when the book
|
||||
carries none.
|
||||
|
||||
A client MUST NOT synthesize a label from a template such as "Chapter {n}": no such
|
||||
form exists, a book legally has no numbers, and a book legally has no chapters at all.
|
||||
An unlabelled chapter is shown without a name rather than given an invented one.
|
||||
|
||||
The server bounds the length — in a list of 5000 rows this is the only string that
|
||||
would otherwise be unbounded.
|
||||
|
||||
Who produces the label is still a backend question: today the engine glues a rendered
|
||||
title into the text of the first unit and leaves the source column without it —
|
||||
companion §4 (K-2).
|
||||
units_total: { type: integer, minimum: 0 }
|
||||
units_done:
|
||||
type: integer
|
||||
|
|
@ -625,9 +860,10 @@ components:
|
|||
|
||||
ChapterList:
|
||||
type: object
|
||||
required: [revision, chapters]
|
||||
required: [revision, next_cursor, chapters]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
next_cursor: { $ref: '#/components/schemas/NextCursor' }
|
||||
chapters:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Chapter' }
|
||||
|
|
@ -671,9 +907,10 @@ components:
|
|||
|
||||
UnitList:
|
||||
type: object
|
||||
required: [revision, units]
|
||||
required: [revision, next_cursor, units]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
next_cursor: { $ref: '#/components/schemas/NextCursor' }
|
||||
units:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Unit' }
|
||||
|
|
@ -706,9 +943,10 @@ components:
|
|||
|
||||
NoteList:
|
||||
type: object
|
||||
required: [revision, notes]
|
||||
required: [revision, next_cursor, notes]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
next_cursor: { $ref: '#/components/schemas/NextCursor' }
|
||||
notes:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Note' }
|
||||
|
|
@ -777,14 +1015,18 @@ components:
|
|||
|
||||
Bank:
|
||||
type: object
|
||||
required: [revision, total, signed, terms]
|
||||
required: [revision, next_cursor, total, signed, terms]
|
||||
properties:
|
||||
revision: { $ref: '#/components/schemas/Revision' }
|
||||
total: { type: integer, minimum: 0 }
|
||||
next_cursor: { $ref: '#/components/schemas/NextCursor' }
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Rows in the whole bank, not on this page.
|
||||
signed:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: How many rows are in status `approved`.
|
||||
description: Rows in status `approved` in the whole bank, not on this page.
|
||||
terms:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/BankTerm' }
|
||||
|
|
@ -840,11 +1082,86 @@ components:
|
|||
|
||||
RunRequest:
|
||||
type: object
|
||||
required: [verify_bank]
|
||||
required: [verify_bank, ceiling_chapters]
|
||||
properties:
|
||||
verify_bank:
|
||||
type: boolean
|
||||
description: Stop for bank signing before the final pass.
|
||||
ceiling_chapters:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: |
|
||||
Ceiling of THIS run, in chapters, within the bounds returned by
|
||||
`GET /books/{bookId}/run-options`.
|
||||
|
||||
Required: a run started without a declared ceiling would spend past the limit the user
|
||||
is entitled to set before it begins rather than learn about afterwards. `0` is not a
|
||||
legal value — a run with a zero ceiling does not start, so it is not offered.
|
||||
|
||||
RunOptions:
|
||||
type: object
|
||||
required: [ceiling]
|
||||
properties:
|
||||
ceiling: { $ref: '#/components/schemas/CeilingBounds' }
|
||||
|
||||
CeilingBounds:
|
||||
type: object
|
||||
description: |
|
||||
Bounds of the run-ceiling scale, in CHAPTERS. The chapters → money conversion lives on the
|
||||
platform and is not exposed here in any form.
|
||||
|
||||
`max_chapters` is what the account can still spend, already clamped to what is left of the
|
||||
book. A client MUST NOT clamp it again.
|
||||
|
||||
⚠ A quantity, not arithmetic: a hold is a debit when it is taken, so a running balance
|
||||
already excludes the holds open against it, and subtracting them a second time would halve
|
||||
the scale.
|
||||
|
||||
`max_chapters` of `0` means no run can start at all — the client shows the exhausted state
|
||||
instead of a scale. Zero is never selectable.
|
||||
required: [min_chapters, max_chapters, default_chapters]
|
||||
properties:
|
||||
min_chapters:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Smallest ceiling that can be started.
|
||||
max_chapters:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Largest ceiling that can be started; `0` when none can.
|
||||
default_chapters:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: |
|
||||
Pre-selected value. The platform owns it because the choice is product policy — a client
|
||||
picking it would decide "spend everything" or "one chapter" on its own. `0` only when
|
||||
`max_chapters` is `0`.
|
||||
|
||||
Usage:
|
||||
type: object
|
||||
description: |
|
||||
State of the credit balance. No window, no `resets_at`, no sums — see `GET /usage`.
|
||||
required: [state, remaining_percent]
|
||||
properties:
|
||||
state:
|
||||
type: string
|
||||
description: |
|
||||
`ok` · `low` the threshold at which the interface warns · `exhausted` nothing left. The
|
||||
threshold itself belongs to the platform and is not on the wire: a client that computed
|
||||
it from the percentage would carry a second copy of the policy.
|
||||
enum: [ok, low, exhausted]
|
||||
remaining_percent:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Share of the account's grants still available. A percentage, never an amount.
|
||||
paused_reason:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/PausedReason'
|
||||
- type: 'null'
|
||||
description: |
|
||||
Set when the account itself is in a halted state; `null` otherwise. The same value
|
||||
travels per-run as `Run.paused_reason`.
|
||||
|
||||
ExportRequest:
|
||||
type: object
|
||||
|
|
@ -883,8 +1200,15 @@ components:
|
|||
| `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).
|
||||
The frame `id` carries the book's revision — the same counter every book-scoped read
|
||||
carries, so a frame and a read can be ordered against each other. One transaction produces
|
||||
one revision but possibly SEVERAL frames, which is why catch-up reads `>=` and not `>`
|
||||
(see `Revision`).
|
||||
|
||||
**The server MAY COALESCE frames**, and a client MUST tolerate counters that jump: a run
|
||||
over 9500 units would otherwise be an unbounded source of renders. A client therefore must
|
||||
not animate from its previous value as though every step had arrived, and must not treat a
|
||||
skipped number as a lost frame.
|
||||
required: [event, data]
|
||||
properties:
|
||||
event: { type: string }
|
||||
|
|
@ -916,9 +1240,17 @@ components:
|
|||
|
||||
EventStatus:
|
||||
type: object
|
||||
required: [status]
|
||||
description: |
|
||||
Product status changed. `paused_reason` travels with it so that a pause is actionable
|
||||
without a second read — the frame that announces the stop is exactly the moment the screen
|
||||
has to say why.
|
||||
required: [status, paused_reason]
|
||||
properties:
|
||||
status: { $ref: '#/components/schemas/BookStatus' }
|
||||
paused_reason:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/PausedReason'
|
||||
- type: 'null'
|
||||
|
||||
EventProgress:
|
||||
type: object
|
||||
|
|
@ -955,8 +1287,12 @@ components:
|
|||
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).
|
||||
sum: money does not appear in the MVP interface at all. The resulting status is `paused`,
|
||||
never `failed`: the stop is resumable. Depends on the event emitter (companion §3).
|
||||
|
||||
Which ceiling — the account's credit or the run's own `ceiling_chapters` — is not
|
||||
distinguished by this frame; whether the two need separate `paused_reason` values is open,
|
||||
companion §4 (K-13).
|
||||
required: [halted]
|
||||
properties:
|
||||
halted: { type: boolean }
|
||||
|
|
@ -976,13 +1312,21 @@ components:
|
|||
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.
|
||||
Error per RFC 9457.
|
||||
|
||||
⚠ **Neither `title` nor `detail` ever carries engine text.** The engine's own detail strings
|
||||
read like "CJK leak in the ru output: 第一节", which exposes how the pipeline works.
|
||||
|
||||
The constraint applies to BOTH fields because both are shown: a client has nothing else to
|
||||
put on the screen when a call fails, so a `title` written for a developer becomes the
|
||||
sentence the reader gets. `title` is the CLASS of the failure, `detail` the specific
|
||||
sentence; both are product language, and either may be empty.
|
||||
required: [type, title, status]
|
||||
properties:
|
||||
type: { type: string, format: uri }
|
||||
title: { type: string }
|
||||
title:
|
||||
type: string
|
||||
description: Product phrase naming the class of failure. Shown to the user as-is.
|
||||
status: { type: integer }
|
||||
detail: { type: string }
|
||||
instance: { type: string, format: uri-reference }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue