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