| .. | ||
| openapi.yaml | ||
| README.md | ||
Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы
Нормативная поверхность контракта —
openapi.yaml(файл рядом, в этой же папке) (OpenAPI 3.1). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято каждое решение, чем оно обосновано, что осталось открытым. При расхождении по ФОРМЕ побеждает YAML; при вопросе «почему так» — этот файл.Статус: РАТИФИЦИРОВАН — D39.99 (04.08), контракт 0.2.0 (D39.115, 08.08); 0.2.1 (D39.123, 09.08): +=
503наPOST /books/{bookId}/runs— старт прогона на неукомплектованном деплое (шов движка не сконфигурирован) не выражается ни одним прежним кодом; закрывает PD-112 платформы; 0.2.2 (D39.129, 10.08):BankTerm.senseобязателен (Ф-47); 0.2.3 (D39.135, 15.08): PD-172 потоковое правило формыcreateBook(уточнено ПО КОДУ платформы против текста заказа: чтение останавливается НА файле — обязательное поле после файла = 400 «как не слали», необязательное молча теряется) · PD-173RejectReason(enum версии: source_unreadable · not_configured · parser_unavailable;Book.reject_reasonнеобязательное) · PD-174 404 интейка / 503resume· PD-180 честный 201=parsing+ отказы 400/408/413 ·BookIntake.titleнеобязательное. ⚠ Форвард-половина 0.2.3: платформа сегодняtitleне читает иreject_reasonне проецирует — реализация обоих = явный вход P7 (D39.135 п.2в). Дом канона — этот каталог;frontend/docs/api-contract/openapi.yaml— байт-зеркало. ⚠ Испр. оркестратором №15 08.08: файл называл себя черновиком на ратификацию четверо суток ПОСЛЕ ратификации — тот же класс, что шапкаinfoспеки.docs/architecture/14-api-contract/, зона оркестратора; перенос делает он, генерация типов после ратификации идёт из перенесённой копии — контракт первичен, код вторичен.Язык. Спека английская: из неё генерятся типы, а исходники фронта по конвенции английские (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской
K-N— это те же вопросы §4. Спутник и остальные доки зоны — русские.Зона строки 95 — «оркестратор/бэкенд/фронт». Фронт авторитетен в одной трети: форма read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и работы движка (99–103) здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют.
0. Пометки провенанса
| Пометка | Что значит |
|---|---|
| ✓ выведено | следует из кода движка или ратифицированного решения; грунт file:line рядом |
| ◆ предложено | решение фронта, разумное по его сведениям; подтверждает названная зона |
| ○ открыто | развилка, на которую у фронта ответа нет; перечень — §4 |
⚠ Пометка ставится на утверждение, а не на раздел: у одного пункта половина бывает выведенной, а половина предложенной. Первая редакция черновика этим и грешила — восемь мест несли ✓ там, где верно было ◆; ниже разведено.
1. Почему YAML, а не проза
Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им типизируются моки. Прозаический контракт расходится с кодом ровно тем способом, ради предотвращения которого заведена строка 95.
Инструменты, пины и отклонение по пиру TS не дублирую: они в STACK_DECISIONS.md §3 и в
бэклоге зоны — Ф-23 (overrides вместо --legacy-peer-deps), Ф-24 (AsyncAPI отложен с
причиной). Четыре формы нарушения гейта, каждая проверена живьём, — FRONTEND_PLAN.md §5.4.2.
2. Решения и их происхождение
2.1. Язык — код, никогда не имя — ✓ выведено
Движок держит коды (backend/internal/config/book.go:26-27), ключ пары — zh-ru
(configs/langpacks/zh-ru/). Имя языка в данных — пар-специфика в общем слое, запрещённая
§2 канона. Вторая цена, дороже: lang элемента берётся из данных книги, и пара ja→ru с именем
вместо кода отрисует кандзи китайскими начертаниями молча.
2.2. Идентификаторы непрозрачны — ✓ выведено
glossary.id — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется
(store/migrate.go:173-174); номер главы плотный, «Chapters that yield no text … do NOT
consume a chapter number» (chunk/chunker.go:99-105), поэтому правка исходника сдвигает
номера последующих глав. Стабильность обеспечивает платформа при персисте манифеста (100).
2.3. Заголовок главы отдельным полем — ◆ предложено
Движок сегодня делает ОБРАТНОЕ, и это надо назвать прямо: титул рендерится
детерминистически из шаблона пары (configs/langpacks/zh-ru/heading.txt: template Глава {n}),
исходный маркер вырезается из текста для модели (chunk/chunker.go:110-114), а на экспорте
титул вклеивается внутрь текста первого юнита (pipeline/export.go:215), причём колонка
исходника остаётся без него.
Выведена здесь только МЕХАНИКА. Само поле heading — предложение фронта, и у него есть цена
на другой стороне: движку придётся отдавать титул отдельно. Альтернатива (оставить вклейку,
фронт отрезает строку) хуже: отрезание титула из текста — это парсинг прозы, и он сломается
на первой главе без заголовка. Развилка — К-2.
Расхождение фикстуры, найденное разбором: дерево витрины показывает «Раздел 2. …», колонка оригинала — неснятый «第二节:». Для zh→ru движок не порождает ни одной из форм. Не чинится до ответа на К-2.
2.4. Состояние — у прогона; у главы выполнение — ✓ выведено
Подпись банка это один стоп на всю книгу (pipeline/mining.go:201), поэтому «глава ждёт
подписи, пока соседняя финализируется» — невозможная картина. У главы движок держит
ChapterPassport (pipeline/status.go:37-55).
2.5. Прогресс пофазно и в юнитах — ✓ выведено
«A unit is DONE when every member draft AND the unit's edit resolved ok»
(pipeline/status.go:328-331), редактура не стартует до стопа банка ⇒ сквозной счётчик стоит
на нуле всю черновую волну. Зависимость — строка 99.
2.6. Словарь статусов — ✓ лестница, ◆ ненормальные исходы
Лестница дословно из строки 95: «загрузка → разбор → перевод → подпись банка → финал →
готово». not_started — дыра, найденная самопроверкой черновика: лестница описывает
идущий прогон, а библиотека обязана показывать разобранную книгу, которую не запускали.
stopped, rejected, not_started контракт ВЫВОДИТ из поведения процесса, а не получает
полем: механика стопа у движка есть (cmd/tmctl/main.go:63), но «кто нажал» знает платформа.
Стоп по потолку — не failed ✓ выведено: «Ceiling is a hard, book-wide stop (not a
per-chunk flag): the job stays 'pending' and resume continues once the ceiling is raised»
(pipeline/stagerun.go:488-489). Мапить его в failed запрещено — это соврало бы про
резюмируемость. Каким статусом и словом он показывается — К-8, вопрос владельцу.
2.7. Состояние пары выводится из ПАРЫ — ✓ выведено (исправление первой редакции)
Первая редакция утверждала «withheld = текст не выдан» как факт о движке. Это было
ложно: флагнутый юнит легально приходит С ТЕКСТОМ в двух случаях —
- косметическая зачистка санитайзера: «the chunk is NOT lost — the cleaned text is committed
as the export» (
pipeline/disposition.go:96-104); - c-lite member-drop: редактор отгружает отредактированный чистый остаток, а юнит флагнут
из-за выпавшего члена (
pipeline/export.go:203-207).
Поэтому состояние выводится из ПАРЫ (вердикт, наличие финального текста): флаг+текст →
translated с замечанием; флаг+пусто → withheld.
Причина флага при этом ПРОИЗВОЛЬНА, и «единственный легальный случай» — снято (ревью
оркестратора, round-2, пункт 4; утверждение противоречило выводу строкой выше). При c-lite drop
юнит несёт FlagReason ПЕРВОГО выпавшего члена, каким бы он ни был (export.go:203-207:
ce.FlagReason = drops[0].Reason, и тут же ce.FinalText … still ships). Значит «текст +
замечание» — это класс, а не один случай, и карта вердиктов обязана иметь фразу для каждой
причины, а не для двух.
Но glossary_miss в этот класс НЕ входит, и это проверено отдельно (иначе правка выше
воскресила бы невоспроизводимый пример). memberDrops берёт причину из ЧЕРНОВОЙ строки члена
(status.go:242-257: draftStages[cs.Stage] && flagged), а glossary_miss ставится
пост-чеком только там, где отгружается финал: в черновой волне — лишь когда она сама финальная
(waverun.go:373-381, draft-only), в c-lite — на строке РЕДАКТУРЫ (waverun.go:494). В
пайплайне, где текст отгружает редактура (то есть где c-lite drop вообще возможен), черновая
строка glossary_miss нести не может. При включённом гейте текст удерживается целиком
(export.go:295-300), при выключенном мисса нет вовсе. Итог: пара «текст + промах словаря»
невозможна ни одним каналом — фикстура витрины, показывавшая её, переведена на c-lite drop.
Свежесть ✓ выведено: target обновляется на границах стадий и на стопах, а не
непрерывно — посреди прогона канала чтения не существует (эксклюзивный лок движка;
санкционированное чтение — завершённый либо остановленный прогон, research/23 §0, §4).
2.8. Банк: словари ✓, имена ◆, kind ◆ с дырой
Значения выведены из схемы и гейтов Go; имена полей контракта — предложение фронта.
| Поле | Словарь | Грунт |
|---|---|---|
status |
auto · draft · approved |
store/migrate.go:191; только approved — канон |
kind ◆ |
name · place · title · term · nickname плюс отсутствие значения |
terminology/classify.go:15 + pipeline/banknote.go:74; пустое — membank/memseed.go:323-326 |
origin |
seed · ruby · mined |
пути записи, см. ниже |
sense |
свободный текст | store/migrate.go:182 |
since_chapter/until_chapter |
целые, 0 = без границы |
store/migrate.go:189-190 |
Фантом auto в провенансе убран. Первая редакция взяла словарь из комментария схемы
(migrate.go:193: seed|ruby|auto) — комментарий устарел. По путям записи "auto" пишет
статус, не провенанс (membank/memseed.go:328: Status:"auto", Source:"ruby"), а майнинг
ставит mined (pipeline/mining.go:424). Правка комментария в движке — за оркестратором.
kind пере-размечен ✓→◆, и вот почему это не косметика (ревью round-2, пункт 3). Словарь
из пяти значений выведен верно, но ЗАКРЫТЫМ и обязательным он делает нелегальной легальную
строку: ruby-кандидат получает Type: "", если его класс не name — то есть gloss и
ambiguous живут без типа по построению (membank/memseed.go:323-326: typ := "", и только
class == rubyClassName даёт "name"). Материализатору read-модели такую строку было
физически нечем заполнить. Правило пустого: kind присутствует всегда и допускает null;
null значит «движок не решил», строка при этом остаётся подписываемой, и клиенту запрещено
и выбрасывать её, и додумывать тип за движок. Проекция "" → null — работа платформы.
Ложный друг устранён. У движка колонка source — это ПРОВЕНАНС. Первая редакция назвала
провенанс origin, а имя source отдала ДРУГОЙ колонке (тексту термина) — то есть завела
между схемами ложного друга. Теперь: провенанс origin, формы термина src/dst, как их
зовёт сам движок; имя source в схеме банка не используется вовсе.
2.9. Подпись — набор решений — ✓ выведено
Конвейер заменяет банк целиком (store/migrate.go:169-170), поэтому PATCH /term/{id} молча
не работает. Механика дословно: «for EACH term either promote it into the mined-delta file OR
decline it in the mined-rejects file, then resume — the stop clears once every proposed term
is promoted or rejected» (pipeline/mining.go:201). Отсюда: решение promote|decline ·
счётчик «решено N из M» · запрет «продолжить» при неполном наборе · частичное сохранение ◆.
POST /runs/{id}/resume — нормативная операция, а не резерв (первая редакция помечала её
«○ резерв строки 94», хотя тут же делала её носителем снятия стопа банка). Резерв строки 94 —
это stop, продуктовая кнопка.
2.10. Ревизия — ✓ у ре-синка, ◆ у чтений
✓ выведено: правило ре-синка — идемпотентный апсерт по (run_id, seq), канал согласования
— status --json (research/23 §2, §8; D39.85).
◆ предложено фронтом: что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН.
Обоснование — гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом,
а рефетч по возврату фокуса окна у ратифицированного @tanstack/react-query включён по
умолчанию, то есть гонка на каждое переключение вкладки. Но это просьба, не вывод; выбор —
К-4.
Скоуп ревизии (дыра первой редакции: она отдавала revision на межкнижной библиотеке при
пер-прогонном определении): в спеке ревизия объявлена НА РЕСУРС — у библиотеки своя, у
прогона своя. Единая сквозная или пер-ресурсная — часть К-4.
2.11. Разрыв потока — ◆ предложено
При переподключении клиент шлёт Last-Event-ID. Если сервер докачать не может, он обязан
ответить событием resync_required, а не молча начать с текущего момента: реплей истории
запрещён, иначе разовое событие вроде note теряется молча и замечание не появится
до перезагрузки. Клиент по этому событию перечитывает снимки.
2.12. Разрешающий список — ✓ инвариант, ◆ форма
Проекция «read-модель → фронт» строится как allowlist. Что лежит в операторских структурах
(pipeline/status.go:58-130, :37-55) и не может доехать: пять денежных полей плюс
cost_usd главы (§4.8 — денег в MVP-интерфейсе нет вовсе) · routing вида «stage=model»,
content_labels, content_routing_problems (ПТ-33) · снапшот, дрифт, ре-билл ·
escalations, postcheck_misses, style_flags, glossary_miss_flagged, stages_skipped,
repair_applied, worst_flag_reason · flag_reason и detail — последний несёт сырой текст
движка вида «CJK leak in the ru output: 第一节»; строку собирает checks/sanitizer.go:658,
а pipeline/export.go:34 — лишь объявление поля, куда она доезжает.
2.13. ПТ-34 — перевод не индексируется — ✓ инвариант
Реестр требований назначает носителем ПТ-34 в том числе контракт, а первая редакция пункта
не имела вовсе. В спеке: приложение живёт под X-Robots-Tag: noindex, ответы с текстом
перевода несут Cache-Control: no-store, ссылка на выгрузку выдаётся только владельцу.
3. Зависимости: без чего контракт не заработает
| Что | Строка | Без чего именно |
|---|---|---|
Пофазный прогресс draft ∥ edit |
99 | прогресс (§2.5) |
Персист манифеста + chunker_version |
100 | стабильный id главы (§2.2) |
| Машиночитаемая таблица ПОДПИСИ | 101 | экран подписи (§2.9) |
| Событийный эмиттер + событие потолка | 103 | весь поток (§2.11), событие note, событие ceiling |
| Артефакт экспорта БАНКА | 169 — движковая половина ПОСТРОЕНА (internal/pipeline/bankexport.go, сайдкар <db>.bank.json, D39.122/127; остаток — платформенная проекция, вход P7); |
чтение GET /books/{id}/bank |
| Механизм поднятия потолка | 126 — ИСПОЛНЕНА (--ceiling-usd D39.122 + run-options/шкала на экране S4, D39.135); |
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. Открытые вопросы
Статусы «✅ ЗАКРЫТ» аннотированы оркестратором №16 (строка 167; авторские формулировки вопросов сохранены под аннотацией).
| # | Вопрос | Кому |
|---|---|---|
| К-1 | ✅ ЗАКРЫТ D39.100 (десять статусов приняты). Было: десять статусов (§2.6) — принять или поправить? Три контракт выводит, а не получает | владелец / автор контракта |
| К-2 | Титул главы: отдать полем heading (предложено) — или оставить вклейку в текст, и фронт отрезает строкой? ⚠ Контекст 09.08: heading манифеста движка = ВРЕМЕННЫЙ рендер (D39.122 п.2д), настоящие заголовки — строка 160 (titleRaw) |
автор контракта + бэкенд |
| К-3 | ✅ ЗАКРЫТ D39.100 (метка главы — из ДАННЫХ книги; зашитой формы «Глава N» не существует, легальна книга без номеров; глава без заголовка на экране — вопрос В-4/Ф-30). Было: титул это ровно «Глава N», узлов 2284 — дерево одинаковых по форме строк | владелец (продуктовое) |
| К-4 | Ревизия: одна сквозная на прогон или своя на ресурс? И несут ли её чтения вообще (§2.10) | платформа |
| К-5 | ✅ ЗАКРЫТ D39.100 (ETA показывать: eta_seconds в спеку). Было: показывать ли оценку времени. ПТ-19 существует (docs/product-requirements.md: «видимый прогресс/ETA»), посылка «не запрошено» неверна |
владелец (продуктовое) |
| К-6 | Ступени замечания: сколько их и где граница. Сегодняшние две — проекция ОПЕРАТОРСКОЙ лестницы рангов, а она не обязана совпадать с продуктовой осью. Статус D39.100: принцип принят (две ступени по читательскому эффекту), карта — с В-3 | владелец (продуктовое) |
| К-7 | Пагинация: 2284 главы и 1200 терминов одним ответом или курсором? Фронт виртуализует, ему годится любой | платформа |
| К-8 | ✅ ЗАКРЫТ D39.100 (BookStatus получает 11-е значение paused + оповещение «лимиты исчерпаны» — ПТ-35; точное продуктовое слово — В-3 на владельце). Было: стоп по потолку — каким статусом и словом? В 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 и ни слова дальше). Но пушить ли завершение ещё и кадром потока, чтобы не опрашивать, решает платформа: у неё воркер и её цена |
платформа |
| К-13 | paused_reason не различает две разные беды (заведён D39.115 п.6б = Ф-31): единственное значение credit_exhausted, а упор в СВОЙ потолок прогона и исчерпание кредита — разные состояния с разным следующим действием; первому из двух фраза врёт. После PD-158 цена выросла: консервативный потолок останавливает ровно на исчерпании холда (D39.123 п.2ж) |
владелец (продуктовое) |
5. Проверка ревью-вопросом строки 95
«Сменится стадия конвейера — придётся ли править фронт?»
| Изменение в движке | Правит ли фронт |
|---|---|
| переименована стадия / добавлена волна | нет — имена стадий не пересекают шов, прогресс пофазный, а не постадийный |
| сменилась модель или маршрутизация | нет — routing/content_labels в allowlist не входят |
| добавлена новая причина флага | нет — на провод идёт продуктовая фраза, карта живёт в контракте |
| добавлен новый тип термина | нет — словарь расширяется минором, ветка неизвестного стоит на шве |
| добавлено новое продуктовое состояние | да, один файл — карта «статус → вид» на шве src/api/; это и есть контрольный вопрос владельца |
| сменился чанкер, главы пере-разобраны | частично — код фронта не правится (ключ непрозрачный, номер отображаемый), но сохранность соответствия старых id новым главам контрактом не гарантируется: это работа персиста манифеста (строка 100). Если соответствие потеряно, у пользователя разъезжаются открытые вкладки и закладки — не правка кода, но видимый ущерб, и решать его строке 100 |
Единственное безусловное «да» — то, которое и должно быть «да».
2.14. Поверхность входа /auth/* — ✓ построено платформой (внесено оркестратором №15 при лендинге S3)
Четыре ручки живут ВНЕ версионного префикса, как /healthz: это механика сессии, а не контрактная
поверхность, поэтому в openapi.yaml они не тащатся (решение оркестратора как владельца контракта,
подтверждено платформой).
| Ручка | Метод | Что делает |
|---|---|---|
/auth/login |
GET | начинает вход, редиректит к провайдеру; принимает ?return_to=<путь этого сайта> |
/auth/callback |
GET | завершает вход, ставит сессионную куку, редиректит на return_to либо на дефолт |
/auth/logout |
POST | завершает ЭТУ сессию |
/auth/logout-all |
POST | завершает ВСЕ сессии пользователя («выйти везде») |
Клиенту нужно знать три вещи. return_to принимает ТОЛЬКО путь этого сайта, и чужой путь сервер
молча заменяет дефолтом — открытого редиректа нет, но и ошибки клиент не получит (сверено с
login.go:safeReturnTo). Обе POST-ручки лежат на cookie-пути, то есть требуют X-TM-Client.
Отказ входа — problem+json, как везде; различать причины отказа клиент не может по замыслу.
2.15. Потолок прогона — ◆ форма предложена фронтом, РАТИФИЦИРОВАНА оркестратором №15 (08.08)
Решение владельца 07.08: шкала в интерфейсе от минимума до максимума, ноль выбрать нельзя, единица —
ГЛАВЫ, потолок принадлежит ПРОГОНУ. Ручки, отдающей границы шкалы, в контракте не было — объявлена
правкой 0.2.0 как GET /books/{bookId}/run-options → CeilingBounds.
Отдельный ресурс, а не поле карточки книги. Максимум зависит от АККАУНТА и двигается, когда книга не меняется: холд под другую книгу опускает остаток. Карточка книги кэшируется библиотекой, то есть назвала бы максимум, которого уже нет, ровно когда человек двигает ползунок. Второй довод дешевле, но настоящий: граница нужна один раз перед стартом, а поле на карточке заставило бы КАЖДОЕ чтение библиотеки нести состояние счёта.
Три числа, а не два. min_chapters объясняет себя единицей — одна глава. max_chapters приходит
УЖЕ подрезанным и по остатку, и по непереведённому хвосту книги; клиенту подрезать второй раз
ЗАПРЕЩЕНО, иначе правило живёт в двух местах и расходится. default_chapters отдаёт платформа, потому
что предустановленное значение — продуктовая политика («потратить всё» ↔ «одна глава»), а не
презентация. max_chapters: 0 — легальный ответ, значит «прогон начать нельзя вовсе»; тогда и
default_chapters равен нулю, а клиент показывает исчерпанное состояние вместо шкалы.
⚠ max_chapters — величина, а не арифметика. Ратификация D39.110 в первой редакции требовала
«баланс МИНУС открытые холды»: это была ОШИБКА оркестратора — вычитание дважды. Холд есть дебет в
момент взятия (pgstore/credits.go:179 пишет отрицательную строку и тем же знаком двигает кэш
баланса), поэтому баланс уже не содержит открытых холдов. Замерено при приёмке на живом PostgreSQL:
грант $10 и холд $1 дают Balance 9 и Reserved 1, а «баланс минус Reserved» дало бы 8, то есть
вдвое урезанную шкалу. Ошибку нашла фронт-сессия S3 чтением Go-кода платформы.
Пересчёта «главы → деньги» на проводе нет ни в каком виде (D39.84) — он живёт на платформе по
оценке движка. ceiling_chapters обязателен и в запросе старта, и на Run: прогон без
объявленного потолка тратит мимо границы, которую человек вправе поставить ДО, а не узнавать после, а
поле на Run позволяет перезагруженному экрану назвать выбранный колпак. 409 на старте отвечает
и на «потолок больше не помещается»: границы читаются отдельным вызовом и могут сдвинуться.
2.16. Транспорт: тот же origin — ФАКТ, а не выбор (внесено оркестратором №15)
CORS-слоя в платформе нет вовсе: preflight OPTIONS с чужим Origin получает 401 от гарда сессии,
заголовков Access-Control-* нет ни на одном ответе (замер приёмки на живом бинаре, PD-96 регистра
платформы). Браузерный клиент с другого origin неработоспособен как класс. В деве фронт ходит через
прокси dev-сервера; кросс-origin не проектируется. X-TM-Client обязателен и на same-origin — он не
про CORS.
Приложение А. Карта «вердикт → продуктовая фраза» — ЗАГОТОВКА
Заполняет автор контракта вместе с бэкендом и владельцем. Правило: фраза пишется по
доккомменту disposition.go, а не по имени константы, и рядом кладётся цитата — иначе
повторяется инверсия, стоившая двух фраз (glossary_miss подан как «термин не подписан»,
хотя термин ПОДПИСАН и его проигнорировали, disposition.go:78-79; sanitizer_stripped подан
как потеря текста, хотя «the chunk is NOT lost», disposition.go:99).
⚠ Русские фразы ниже — плейсхолдеры, а не предложение фронта. Словарь продуктовый, его
слова выбирает владелец (ПТ-33, В-3). Ступень — тоже: сегодняшние attention/glance
унаследовали ОПЕРАТОРСКУЮ ось рангов, а она не обязана совпадать с продуктовой (К-6).
| Причина движка | Ранг | Продуктовая фраза | Ступень |
|---|---|---|---|
hard_refusal · soft_refusal · content_filter · hard_block |
0 | ⬜ | ⬜ |
cjk_artifact · excision_suspect · coverage_fail |
1 | ⬜ | ⬜ |
sanitizer_defect |
2 | ⬜ | ⬜ |
loop_degenerate |
3 | ⬜ | ⬜ |
decode_error |
4 | ⬜ | ⬜ |
glossary_miss |
5 | плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ |
length · empty |
6 | ⬜ | ⬜ |
sanitizer_stripped |
7 | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ |
upstream_not_ok |
8 (по умолчанию) | ⬜ | ⬜ |
| незнакомая причина | 8 (по умолчанию) | ⬜ нейтральная, НЕ «ошибка» | ⬜ |
Причин пятнадцать; upstream_not_ok в первой редакции отсутствовал — у него нет своей ветки
в flagReasonSeverity, поэтому он падает в ранг по умолчанию (pipeline/status.go:174),
как и любая будущая причина. Последняя строка — не формальность: контракт обязан иметь фразу
для причины, которой ещё не существует.