# Вход фронта в контракт API v0 — материал для строки 95 > ⚠ **ИСПОЛНЕНО И БОЛЬШЕ НЕ ПОДДЕРЖИВАЕТСЯ (04.08).** Этот файл — ВХОД, написанный до > контракта: просьба к его автору. Контракт с тех пор написан и ратифицирован, и все 18 пунктов > ниже стали либо полем канонической спеки `docs/architecture/14-api-contract/openapi.yaml`, либо > разделом §2 канонического спутника `docs/architecture/14-api-contract/README.md`, либо его > К-вопросом §4. **Читать для решения — канон; этот файл с ним не сверяется и уже расходится:** > его `file:line` в чужую зону протухли (проверено 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` роняет `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 стоит именно на слое данных: экраны S4–S7 пишутся поверх него, и каждый день без контракта — это не «фронт ждёт», а «фронт может начать строить экраны на формах, которые придётся переписать».