textmachine/frontend/docs/API_CONTRACT_INPUT.md

456 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Вход фронта в контракт API v0 — материал для строки 95
> ⚠ **ИСПОЛНЕНО И БОЛЬШЕ НЕ ПОДДЕРЖИВАЕТСЯ (04.08).** Этот файл — ВХОД, написанный до
> контракта: просьба к его автору. Контракт написан и ратифицирован, и все 18 пунктов ниже стали
> либо полем канонической спеки `docs/architecture/14-api-contract/openapi.yaml`, либо разделом §2
> канонического спутника `docs/architecture/14-api-contract/README.md`, либо его К-вопросом §4.
> **Читать для решения — канон; этот файл с ним не сверяется и уже расходится:** часть просьб
> канон пере-решил, а грунт в движок снят 04.08 и протух ВЫБОРОЧНО — проверено 02.09: часть
> номеров строк ещё резолвится, а часть цитат из движка не грепается вовсе. Не удалён и не
> сокращён ради аудиторского следа; место такому — `docs/archive/`, перенос делает оркестратор.
>
> **Что это было.** Ф-14 бэклога зоны, доведённый до артефакта: перечень того, что интерфейсу
> нужно от read-модели платформы, с обоснованием на КАЖДЫЙ пункт. Адресат — автор контракта
> `docs/architecture/14-api-contract.md` (единый бэклог, строка 95); канал передачи —
> оркестратор.
>
> **Чем это НЕ является.** Это не контракт и не его проект. Заводить контракт своим решением
> фронт не может (шапка `BACKLOG.md`: запросы к движку идут строками единого бэклога через
> оркестратора). Ни один пункт ниже не реализован в коде и не будет реализован до ратификации.
>
> **Как собрано.** Каждый пункт грунтован либо кодом движка (`file:line`, прочитано в этой
> сессии, а не пересказано по доку), либо ратифицированным решением с номером. Где основания
> нет — пункт помечен как **гипотеза фронта** и вынесен в §9 вопросом, а не подан утверждением.
> Формы, которые фронт уже нарисовал, доказательством не считаются: половина из них — рабочая
> гипотеза `src/api/types.ts`, и там, где она разошлась с движком, расхождение названо здесь же.
## 0. Одна причина, по которой этот список существует
Строка 95 заведена ради класса дефектов «док↔код»: **моки, снятые не с того контракта,
разойдутся с API**. В этой зоне дефект уже случался трижды, и каждый раз стоил экрана:
| Когда | Что выдумано | Чем кончилось |
|---|---|---|
| S1 | поле `translatedChapters` | наивный счётчик глав — прогресс, запрещённый собственным планом |
| S1 | `kind: 'имя'`, `state: 'подписан'` | русские слова вместо словаря движка, переписан весь слой данных |
| S2 | `org` в типах банка | тип, которого в закрытом словаре движка нет вовсе (`classify.go:15`) |
Четвёртый экземпляр найден этой сессией и описан в §1.4 — фикстура рисует заголовок главы
в форме, которую движок для zh→ru не порождает. Все четыре — один и тот же класс: **фронт
догадывался там, где надо было прочитать или спросить.** Поэтому ниже — вопросы к контракту,
а не поля в типах.
---
## 1. Идентичность и адресация
### 1.1. Язык — код, никогда не имя
**Просим:** `source_lang` / `target_lang` строками-кодами (`zh`, `ja`, `en`, `ru`), плюс ключ
пары в форме `zh-ru`. Человеческое имя языка контракт не отдаёт вовсе.
**Основание:** движок держит именно коды (`backend/internal/config/book.go:26``SourceLang`
`yaml:"source_lang"` с комментарием `zh | ja | en`), и ключ langpack-директории — `zh-ru`
(`configs/langpacks/zh-ru/`).
**Что ломается без этого:** имя языка в данных — это пар-специфика в общем слое, ровно то,
что запрещает §2 канона («заработает ли пара, которой в репо ещё НЕТ, без правки Go?»).
На фронте у этого есть вторая цена, дороже: атрибут `lang` элемента берётся из данных книги,
а по нему браузер подбирает начертания CJK. Пара ja→ru с именем вместо кода отрисует кандзи
китайскими начертаниями молча — унификация Хань, и ни один тест этого не увидит. Ровно этот
дефект дважды ловили ревью S1 и оркестратора 04.08.
### 1.2. У главы нужен идентификатор, а номер им не является
**Просим:** у главы — непрозрачный стабильный `id`, отдельно от её порядкового номера.
**Основание:** сегодня глава адресуется целым числом (`chunk.Chunk.Chapter`,
`chunker.go:47-49`, 1-based), и это число **производное от чанкера**: «Chapters that yield no
text … do NOT consume a chapter number» (`chunker.go:99-105`). То есть правка исходника,
из-за которой пустая глава перестала быть пустой, сдвигает номера всех последующих глав.
Персиста манифеста нет — каждый read-вызов режет исходник заново (строка 100 единого
бэклога, `status.go` `bookChunks`).
**Что ломается без этого:** дерево на 2284 узла и открытые вкладки живут по ключу. Сдвиг
нумерации после пере-разбора переставляет содержимое под ключами: пользователь открыл главу
17, вернулся — там другой текст. Это не гипотетика, это прямое следствие процитированного
правила плотной нумерации.
**Вопрос контракту:** идентификатор минтит платформа при персисте манифеста (строка 100) —
или он выводится из чего-то, что уже стабильно?
### 1.3. Единица пары — юнит редактуры, и ключ у неё составной
**Просим:** у строки читалки — свой `id`; знать, что единица — юнит, а не чанк.
**Основание:** «The shipping granularity is the OUTPUT UNIT (edit unit for an edit pipeline,
draft chunk for a draft-only one)» (`status.go:328-331`). Экспорт итерирует именно юниты,
и ключ юнита — **лидер**: `(chapter, firstChunkIdx)` (`export.go:90-91`). Поэтому поле
`chunk_idx` в `ChunkExport` — индекс лидера, а не «номер чанка подряд»; читать его как
порядковый номер блока в главе неверно.
**Что ломается без этого:** ключи списка. Юнитов ≈1.9 на главу (ПТ-21), местами вся глава —
один блок; при пере-разборе состав юнитов меняется, и позиционный ключ переставляет пары
местами.
### 1.4. Заголовок главы — отдельное поле, а не первая строка перевода
**Просим:** заголовок главы отдельным полем (или явное решение «его нет»), и знать,
приезжает ли он уже вклеенным в текст.
**Основание — самое неожиданное из всего разбора.** Титульная политика (pack-13):
1. чанкер детектирует исходный структурный заголовок, **вырезает маркер+номер+единицу из
текста, который видит модель**, и рендерит заголовок ДЕТЕРМИНИСТИЧЕСКИ из шаблона пары
(`chunker.go:110-114`, `stripHeading`);
2. шаблон для zh→ru — `template Глава {n}` (`configs/langpacks/zh-ru/heading.txt`), то есть
заголовок это ровно «Глава N» и ничего больше; подзаголовок («逆光阴五百年觉悟») остаётся
ОБЫЧНЫМ ТЕЛОМ ТЕКСТА, а не названием;
3. на экспорте заголовок **вклеивается внутрь финального текста первого юнита главы**
(`export.go:209-215`, `chunk.ApplyHeading`), а колонка исходника под `--pairs` остаётся
БЕЗ него — «the DC FP-measure aligns src↔target on the body prose, not the deterministic
title».
**Следствие, которое обязан знать интерфейс:** у первой пары каждой главы колонки
асимметричны по построению — слева тела нет заголовка, справа он есть первой строкой.
Читалка двух колонок либо это учитывает, либо рисует ложное расхождение выравнивания
на каждой главе книги.
**Расхождение фикстуры, найденное этим разбором (четвёртый экземпляр класса из §0).**
На снимке `.shots/showcase.png` дерево показывает «Раздел 2. Прозрение пятисот лет», в колонке
оригинала стоит «第二节:逆光阴五百年觉悟», в колонке перевода — «Раздел 2: Прозрение пятисот
лет против течения времени». Для zh→ru движок не порождает ни одной из трёх форм: слово
заголовка — «Глава», маркер `第…节` из исходника вырезан, а подзаголовок титулом не является.
Диспозиция: **не чинить сейчас** — форма заголовка это вопрос контракта (нужно ли поле
`heading` отдельно), и правка фикстуры до ответа даст пятую выдумку вместо четвёртой.
Строка Ф-14 держит хвост.
### 1.5. Прогон и книга — разные сущности
**Просим:** развести идентичность книги и идентичность ПРОГОНА над ней; событиям потока
нужен `run_id`.
**Основание:** правило ре-синка ратифицировано как идемпотентный апсерт по `(run_id, seq)`
(`research/23` §2, D39.85). У движка сущности «прогон» нет вовсе — есть книга (`book_id`)
и trace вызова, причём внешний trace-контекст пока не принимается (строка 102).
**Что ломается без этого:** без `run_id` два последовательных прогона одной книги
неразличимы в потоке, и докачка после обрыва не может отличить «продолжение моего прогона»
от «начался новый».
---
## 2. Состояние и прогресс
### 2.1. Состояние — у прогона, у главы его нет
**Просим:** состояние (лестница «загрузка → разбор → перевод → подпись банка → финал →
готово») — поле ПРОГОНА над книгой. У главы вместо состояния — выполнение по юнитам.
**Основание:** подпись банка — один стоп на всю книгу между черновой и редакторской волнами
(`mining.go:201`: «run STOPPED before the edit wave»), поэтому картина «глава ждёт
подписи, пока соседняя финализируется» физически невозможна. У главы движок держит
`ChapterPassport` (`status.go:37-55`): `units_total/done/flagged/in_progress/pending` и
вердикт.
**Что ломается без этого:** ровно то, что уже было построено и переделано в S1 — девять
состояний, нарисованных на главах. Интерфейс обещал бы пользователю переходы, которых
конвейер породить не может.
### 2.2. Прогресс — пофазно и по ЮНИТАМ
**Просим:** две пары счётчиков — черновик `{done,total}` и редактура `{done,total}`,
в единицах ЮНИТОВ. Не «готово N глав из M».
**Основание:** «A unit is DONE when every member draft AND the unit's edit resolved ok»
(`status.go:328-331`). Редактура не стартует до стопа банка. Значит сквозной счётчик стоит
на нуле всю черновую волну — на книге в 2284 раздела это часы нуля. `StatusReport` сегодня
отдаёт единые `Done/Pending/InProgress/TotalUnits` без деления по волнам; вывод пофазных
счётчиков — строка 99 единого бэклога, без миграции схемы.
**Что ломается без этого:** индикатор врёт в самую заметную сторону — «ничего не происходит»
на работающем прогоне. Фикстура фронта намеренно поставлена в середину черновой волны
(`draft 214/4276`, `edit 0/4276`) именно чтобы скриншот-цикл смотрел на этот случай.
**Отдельно:** пофазность нужна ДАННЫМ, но не экрану. Пользователь видит одну долю без
названий фаз (§4.1 промта, ПТ-33) — счёт по двум волнам ведёт контракт, а не интерфейс.
### 2.3. Девять значений состояния — гипотеза фронта, а не находка в коде
**Честно:** словаря состояний прогона в движке НЕТ. Есть shell-контракт кодов возврата —
`0` чисто · `2` завершено с флагами · `3` стоп на подписи банка · `1` сбой
(`cmd/tmctl/main.go:30-52`) — и счётчики статуса. Девять значений в `src/api/types.ts`
(`not-started · parsing · translating · awaiting-bank · finalizing · ready · stopped ·
rejected · failed`) — проекция ПТ-33 на эту механику, сделанная фронтом.
Из них **`stopped` и `rejected` в терминах движка не выражаются.** Точнее, чем «движок так
не умеет»: механика остановки у него ЕСТЬ — graceful stop по сигналу
(`cmd/tmctl/main.go:63`, `signal.NotifyContext` на `SIGINT`/`SIGTERM`), а неразобранный файл
падает обычным сбоем (`1`). Но и то и другое движок сообщает как «процесс кончился так-то»,
а не как состояние прогона: **кто именно нажал стоп и почему прогон не начался — знает
платформа**, и продуктовые кнопки «стоп/продолжить» лежат строкой 94 единого бэклога, а не
в конвейере. То есть эти два значения контракт выводит, а не получает.
**Просим:** словарь минтит контракт (ПТ-33 прямо называет носителем строку 95), а девять
значений принять, поправить или отвергнуть — но явно. Фронт на них не настаивает.
---
## 3. Банк памяти и подпись
### 3.1. Статус подписи трёхзначный, а не булев
**Просим:** `auto | draft | approved` вместо сегодняшнего `signed: boolean`.
**Основание:** `glossary.status TEXT NOT NULL DEFAULT 'auto' -- auto|draft|approved (term
status machine; only approved is CONFIRMED-injected)` (`migrate.go:191`).
**Что ломается без этого:** булев склеивает две РАЗНЫЕ вещи. `auto` — предложение самого
движка, которого никто не смотрел (`StatusReport.UnsignedBankTerms` считает именно строки
«carrying a rendering NOBODY approved»); `draft` — термин, который человек начал и не
закончил. Подписной экран S5 — сотни строк подряд, и «не трогали» против «начали» это
главный фильтр работы. Булев его уничтожает.
### 3.2. Ключ строки банка составной, и `id` движка для него не годится
**Просим:** непрозрачный стабильный `id` у строки банка, минченный платформой.
**Основание, две половины:**
- уникальность термина — `UNIQUE (book_id, src, sense, since_ch, until_ch)`
(`migrate.go:202`). Один и тот же `src` законно приходит несколькими строками: полисемия
(`sense`) и спойлерное окно (`since_ch`/`until_ch`);
- **автоинкремент движка стабильным ключом НЕ является** — это сказано в самой схеме:
«glossary.id is a fresh autoincrement each replace and is NEVER hashed into memoryVersion»
(`migrate.go:173-174`), потому что конвейер ЗАМЕНЯЕТ весь банк книги на каждом прогоне.
**Что ломается без этого:** сегодня ключ ряда собран фронтом из (`src`, `dst`, `type`) — это
заплата, поставленная ревью оркестратора 04.08 взамен ещё худшей (`term.source`). На полисемии
она даёт коллизию ключей списка; после прогона, сменившего `dst`, — потерю позиции и выделения
в списке на 1200 строк.
### 3.3. Поля, без которых подпись не работает
**Просим:** `type` · `sense` · `since_ch`/`until_ch` · `source` (провенанс).
**Основание и точный словарь:**
| Поле | Значения | Грунт |
|---|---|---|
| `type` | `name · place · title · term · nickname` | классификатор знает четыре (`classify.go:15`), банкнота принимает их же плюс `nickname` (`banknote.go:74`). Колонка в схеме — свободный текст с ОТКРЫТЫМ комментарием (`migrate.go:181`), поэтому истина — гейты в Go, а не DDL |
| `sense` | свободный текст | «A3 polysemy disambiguator (part of the uniqueness key)» (`migrate.go:182`) |
| `since_ch` / `until_ch` | целые, `0` = без границы | спойлерное окно (`migrate.go:189-190`); один термин с разным переводом до и после раскрытия — это две законные строки, а не конфликт |
| `origin` (провенанс) | `seed · ruby · mined` | ⚠ **исправлено 04.08 по ревью оркестратора:** словарь снят с ПУТЕЙ ЗАПИСИ, а не с комментария схемы — комментарий `migrate.go:193` (`seed\|ruby\|auto`) устарел. Литералы `"auto"` в коде пишут СТАТУС, не провенанс (`membank/memseed.go:328`: `Status:"auto", Source:"ruby"`); майнинг ставит `mined` (`mining.go:424`). Имя `source` контракт этому полю НЕ отдаёт — у движка так зовётся сам провенанс, и переиспользование имени завело бы ложного друга |
**Что ломается без `type`:** он не косметика. `type ∈ {name, place}` МАРШРУТИЗИРУЕТ термин
в транслитерацию (`classify.go:6-7`), то есть решает, переводить слово или транскрибировать.
Подписывать термин, не видя типа, — подписывать вслепую.
**Что ломается без спойлерного окна:** две строки с одинаковым `src` выглядят на экране
дубликатом-ошибкой, и пользователь «наведёт порядок», удалив легитимную.
### 3.4. Подпись — это набор решений, а не правка строк
**Просим:** контракт подписи в форме «набор решений по предложенным терминам», с явным
признаком полноты набора.
**Основание, буквальное:** конвейер ЗАМЕНЯЕТ банк книги из детерминированных входов
(«the pipeline REPLACES a book's whole glossary from its deterministic inputs (seed file +
classified ruby)», `migrate.go:169-170`) — прямая запись в таблицу стирается следующим
прогоном. Механика стопа: «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**» (`mining.go:201`).
**Три следствия для экрана S5, каждое из этой цитаты:**
1. решение бинарное с нагрузкой: promote (с переводом) либо decline;
2. **стоп снимается только полным набором** — значит экрану нужен счётчик «решено N из M»
и запрет «продолжить», пока набор неполон;
3. сотни терминов в один присест не подписываются — значит частичное сохранение обязано быть
в контракте, иначе закрытая вкладка теряет час работы.
**Что ломается без этого:** контракт вида `PATCH /term/{id}` выглядит естественно и молча
не работает — правка исчезает на следующем прогоне. Это ловушка, названная в
`STACK_DECISIONS.md` §8 отдельным ⚠-блоком.
**Ещё нужна машиночитаемая таблица подписи** (строка 101): сегодня полная таблица — текстовый
сайдкар, stdout режется на 20 строках.
---
## 4. Замечания и продуктовый словарь вердиктов
### 4.1. Перевод «вердикт → человеческая фраза» живёт в контракте, не во фронте
**Просим:** продуктовый словарь замечаний — часть контракта; фронт получает уже продуктовое
понятие плюс ступень важности.
**Основание:** у движка 15 причин флага (пересчитано по файлу, не на глаз) — `length · empty ·
loop_degenerate · hard_refusal · soft_refusal · content_filter · cjk_artifact · decode_error ·
coverage_fail · excision_suspect · hard_block · upstream_not_ok · glossary_miss ·
sanitizer_defect · sanitizer_stripped` (`disposition.go:51-105`) — и ратифицированная лестница
важности, ранги 0..8 (`status.go:150-175`).
**Что ломается без этого — уже ломалось.** Фронт написал фразы по ИМЕНИ причины, и две из
пяти получились наоборот:
- `glossary_miss` → «Термин не подписан в банке» — **неверно**: «an **approved** term's src
fired … the model ignored the glossary» (`disposition.go:78-79`). Термин подписан, его
проигнорировал перевод; фраза посылала человека подписывать подписанное;
- `sanitizer_stripped` → «Часть блока не переведена» — **неверно**: «the chunk is NOT lost —
the cleaned text is committed as the export» (`disposition.go:99`), и это самый
безобидный флаг из всех (ранг 7 против 0 у отказов).
Имя причины — не её смысл. Пока карта живёт во фронте, она будет писаться по имени снова.
### 4.2. `detail` не пересекает шов никогда
**Просим:** поля `detail` в контракте нет вовсе — не «фронт его не показывает», а не отдаёт
платформа.
**Основание:** `ChunkExport.Detail` (`export.go:34`) несёт сырой текст движка вида
«CJK leak in the ru output: 第一节». Это прямое раскрытие устройства конвейера — §4.1 промта
и ПТ-33.
**Почему запретом на стороне контракта, а не дисциплиной фронта:** поле, доехавшее до
браузера, попадает в devtools, в баг-репорт со скриншотом и в первый же экран, который
напишут «временно, чтоб отладить». Инвариант, который держится только на внимательности
шести будущих сессий, — не инвариант.
### 4.3. Ступень важности — продуктовая, и её значение придёт от владельца
Сегодня фронт различает две ступени (`attention` / `glance`), выведенные из лестницы рангов.
Каким СЛОВОМ они называются на экране — вопрос владельца (Ф-21, В-3): словарь продуктовый
(ПТ-33), а §3.8 требует, чтобы экран не выглядел тревожным, поэтому «важно/неважно» и
«ошибка/предупреждение» не годятся. Контракт может отдавать ступень кодом; слово — экрана.
---
## 5. Живой поток: докачка и ре-синк
### 5.1. Монотонный `id` события + `Last-Event-ID`
**Просим:** у события SSE монотонный `id`; сервер обязан поддержать докачку по
`Last-Event-ID`.
**Основание:** ратифицированный транспорт до фронта — SSE, «монотонный `id` +
`Last-Event-ID` для докачки», heartbeat ~20 с, `EventSource` в браузере и
`Authorization: Bearer` с построчным разбором для десктопа (`STACK_DECISIONS.md` §5, D39.84).
**Что ломается без этого:** обрыв на минуту при живом прогоне — обычное дело. Без докачки
у фронта два выхода, оба плохие: перезапросить всё (2284 главы и банк на 1200 строк на
каждый чих сети) либо показывать устаревшее молча.
### 5.2. Ревизия на ЧТЕНИЯХ, из того же счётчика, что и `id` потока
**Просим:** каждое чтение read-модели несёт ревизию, сравнимую с `id` событий.
**Основание:** правило ре-синка — идемпотентный апсерт по `(run_id, seq)`, а `status --json`
объявлен каналом согласования (`research/23` §2, §8; D39.85).
**Что ломается без этого — гонка, которую нечем разрешить.** Фронт живёт на двух источниках
разом: снимок из чтения и поток событий. Без общего счётчика невозможно ответить на вопрос
«снимок, который приехал по HTTP, новее последнего события или старее?» — и интерфейс
показывает откат прогресса назад при каждом переподключении. Это не теория: у ратифицированного
`@tanstack/react-query` рефетч по возврату фокуса окна включён ПО УМОЛЧАНИЮ
(`refetchOnWindowFocus`, дефолт `true` — сверено с официальной докой 04.08:
[tanstack.com/query/latest/…/window-focus-refetching](https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching)),
то есть гонка случается на каждое переключение вкладки браузера, а не в редком краевом случае.
### 5.3. Что фронт про поток НЕ спрашивает
NDJSON движка фронту не принадлежит и в его коде появиться не может (`research/23` §0,
`FRONTEND_PLAN.md` §0.2): движок фронту не виден, между ними платформа. Словарь событий
эмиттера (строка 103) фронт не проектирует — просит лишь, чтобы продуктовый словарь статусов
и словарь событий проектировались ВМЕСТЕ, как и записано в строке 95.
---
## 6. Чего в read-модели фронта быть не должно (разрешающий список, а не вычитание)
**Просим:** проекция «read-модель → фронт» строится как **allowlist**. Это единственный
пункт списка, сформулированный как запрет, и он самый дешёвый в исполнении сейчас и самый
дорогой потом.
**Основание:** `StatusReport` (`status.go:58-130`) и `ChapterPassport` (`status.go:37-55`) —
операторские структуры, и в них лежит ровно то, что запрещено показывать:
| Что лежит в структуре движка | Почему не может доехать |
|---|---|
| `committed_usd` · `reserved_usd` · `book_ceiling_usd` · `ceiling_pct` · `projected_book_usd` · `ChapterPassport.cost_usd` | §4.8: **денег в интерфейсе MVP нет вообще** — ни сумм, ни потолков, ни оценки, ни остатка |
| `routing` («stage=model», плюс «→hop» на фолбэке) · `content_labels` · `content_routing_problems` | ПТ-33: имена моделей и стадий на экран не протекают |
| `snapshot_id` · `snapshot_drift` · `config_drift` · `rebill_units` · `rebill_usd` | внутренности детерминизма и пере-оплаты; это операторский инструмент, не продуктовый |
| `escalations` · `postcheck_misses` · `style_flags` · `glossary_miss_flagged` · `stages_skipped` · `repair_applied` · `worst_flag_reason` | лексика конвейера; продуктовая проекция — §4.1 |
| `eta_seconds` | не запрещено; ⚠ и НЕ «не запрошено» — посылка первой редакции неверна: ПТ-19 существует (`docs/product-requirements.md:49`, «видимый прогресс/ETA — из Ф3-видения ридер-IDE»). Вопрос только в том, показываем ли в MVP — К-6 |
**Почему allowlist, а не «фронт это не рисует»:** ревью-вопрос самого контракта — «сменится
стадия конвейера — придётся ли править фронт?». При вычитании ответ «нет» держится, пока
кто-то помнит список; при разрешающем списке новое поле движка физически не доезжает до
браузера, пока контракт его не назовёт. Плюс ПТ-34: чего не приехало, то не утечёт.
---
## 7. Правило эволюции словарей — и ветка неизвестного значения (Ф-22)
**Просим:** контракт явно объявляет правило эволюции закрытых словарей.
**Основание:** образец уже ратифицирован — terraform-подобный version-хендшейк:
«минорную версию инкрементируем для обратно-совместимых добавлений: **игнорируйте незнакомые
поля**; мажорную — для несовместимых: **отвергайте неподдерживаемую**» (`research/23` §2,
входы D39.85 к строке 95). Сам движок эту дисциплину уже держит: неизвестная причина флага
получает ранг по умолчанию (`status.go:174`), неизвестный тип термина падает в `term`
(«anything else falls back to "term" (a benign default)», `banknote.go:73`).
**Зачем это фронту именно как строка контракта.** Строгость тайпчека здесь НЕ страхует.
Проба пере-запущена в этой сессии (не взята со слов Ф-22) на нашем `tsconfig.json`, где
`noUncheckedIndexedAccess: true` стоит строкой 18: обращение `tone[s]` при `s: RunState`
(КОНЕЧНЫЙ юнион) компилируется **молча, без единой диагностики**, а `open[s]` при
`open: Record<string, …>` роняет `TS2532: Object is possibly 'undefined'`. То есть
`undefined` добавляется только индексным сигнатурам, и значение вне юниона — новое состояние
прогона из будущего контракта — даёт `undefined.tone`, то есть **падение дерева библиотеки**,
а не пустую метку; тем же классом незнакомая ступень замечания тихо уезжает в спокойную
ветку. Хвост — Ф-22.
**Что фронт сделает сам, когда контракт появится:** ветка неизвестного значения ставится
на ШВЕ `src/api/`, где значение входит, а не в каждом компоненте — иначе перевод «значение →
вид» расползётся по экранам, как уже расползался перевод вердиктов. Но выбор поведения ветки
(показать нейтрально ↔ отвергнуть ответ) зависит от того, минор это или мажор, — а это знание
контракта, не фронта.
---
## 8. Границы: чего фронт НЕ просит
Список коротких «нет» — чтобы автор контракта не проектировал лишнего:
- **спанов и байтовых смещений внутри текста** — у проверок движка их нет, только счётчики;
подсветка на уровне блока и главы, вопрос закрыт владельцем 02.08 (§4.9 промта);
- **синхронизации двух прокруток** в читалке — колонки живут одной строкой грида и разъехаться
не могут (`STACK_DECISIONS.md` §2);
- **денежных полей** — §6 выше;
- **доступа к движку в любой форме** — фронт читает только read-модель платформы (D39.85);
- **редактора текста** — в MVP тексты только читают и сравнивают;
- **выравнивания тоньше юнита** — гранулярность грубая и владельцем принята (ПТ-21).
---
## 9. Открытые вопросы — на них фронт ответа не имеет
| # | Вопрос | Кому |
|---|---|---|
| К-1 | Девять значений состояния прогона (§2.3) — принять, поправить или заменить? Словаря состояний в движке нет вовсе, а два значения (`stopped`, `rejected`) контракт ВЫВОДИТ из поведения процесса, а не получает полем: механика стопа у движка есть (`main.go:63`), но «кто нажал и почему не начался» знает платформа | автор контракта / владелец |
| К-2 | Заголовок главы (§1.4): отдельное поле `heading` — или он остаётся вклеенным в текст первого юнита? От ответа зависит и дерево, и первая строка читалки | автор контракта |
| К-3 | Метка главы в дереве при 2284 узлах: если заголовок это ровно «Глава N», дерево — 2284 одинаковых по форме строки. Оставить так или контракт отдаёт что-то ещё? | владелец (продуктовое) |
| К-4 | Частичное сохранение подписи (§3.4): контракт хранит незавершённый набор решений на сервере — или это состояние клиента до отправки целиком? | автор контракта |
| К-5 | Ревизия (§5.2) — сквозная на книгу/прогон или отдельная на каждый ресурс (главы · банк · замечания)? Второе дешевле серверу, первое проще клиенту | автор контракта |
| К-6 | `eta_seconds` (§6): показываем оценку времени в MVP? Требование ПТ-19 существует (`product-requirements.md:49`) — решать до формы прогресса | владелец (продуктовое) |
---
## 10. Что это стоит и что разблокирует
Артефакт нужен, потому что **этап S3 (слой данных) без контракта не начинается** — так
поставлен замок в промте S3, и причина в §0. Как только контракт ратифицирован, S3 делает:
асинхронный `src/api/` на MSW + TanStack Query · фикстуры всех состояний (ожидание · ошибка ·
пусто · частично · длинный хвост · обрыв стрима) · `EventSource` за швом `src/api/` ·
приведение типов к контракту · ветку неизвестного значения на шве (Ф-22) — и закрывает Ф-15.
Порядок этапов Ф-1 стоит именно на слое данных: экраны S4S7 пишутся поверх него, и каждый
день без контракта — это не «фронт ждёт», а «фронт может начать строить экраны на формах,
которые придётся переписать».