# Бэклог фронта (зонный) > Ведёт зона `frontend/` (решение владельца 02.08, D39.84: фронт и платформа держат СВОИ бэклоги; единый бэклог `docs/PROGRESS.md` — трекер движка/полигона/доков, фронт-строк не принимает). Нормы те же: ID стабилен навсегда, каждая петля получает диспозицию. Запросы к ДВИЖКУ сюда не пишутся — они заходят строками единого бэклога через оркестратора (пример: строки 99–102 — пофазный прогресс · манифест глав · машиночитаемая таблица подписи · трасса). **Развод «предложить/ратифицировать» (D39.99 п.4, после инцидента остановки S3): ПРЕДЛАГАТЬ артефакты для чужих решений — черновики контрактов, входы дизайна — фронт не только может, но обязан; не может он их РАТИФИЦИРОВАТЬ. «Не заводит своим решением» значит «финальное слово не его», а не «не смей писать черновик».** Засеян оркестратором при лендинге D39.84 — дальше правят фронт-сессии. | ID | Хвост | Вес | Источник | |---|---|---|---| | Ф-1 | **Этапы S2–S7** (`FRONTEND_SESSION_PROMPT.md`): S2 оболочка + слой `src/ui/` · S3 слой данных/MSW/фикстуры всех состояний · S4 библиотека/загрузка/разбор/прогресс · S5 банк памяти и подпись (самый тяжёлый) · S6 читалка двух колонок и замечания · S7 метаданные/экспорт/настройки/сквозной прогон | по очереди сессий (S0/S1 — текущая) | FRONTEND_SESSION_PROMPT | | Ф-2 | **React Compiler** — включить ОТДЕЛЬНЫМ шагом после заморозки интерфейса, с CI-проверкой, что вставки реально попали в бандл (в MVP выключен; совместимость держит линт eslint-plugin-react-hooks) | после заморозки UI | STACK_DECISIONS §1 | | Ф-3 | **Пересмотр TS 6 → TS 7** после выхода TS 7.1 с программным API (~октябрь 2026); решение обратимо — тайпчек не участвует в сборке | триггер: релиз TS 7.1 | STACK_DECISIONS §1 | | ~~Ф-4~~ | ~~**Токен-гейт на отступы**~~ — **включён 04.08 в S2.** Значение теперь обязано быть последовательностью `var(--…)`; `0`, `auto` и `inherit` легальны (это не размеры), `calc()` намеренно запрещён: составное выражение из литералов проверить нечем, а если размер понадобился новый — его место в `tokens.css`. Свойства взяты регулярками, поэтому длинные формы (`border-top-left-radius`, `padding-inline`) закрыты тоже. Обкатка потребовала трёх токенов, которых не хватало: `--radius-round`, `--radius-bar`, `--row-inset`. Проверено шестью живыми нарушениями и тремя легальными формами (перечень — `FRONTEND_PLAN.md` §5.4) | закрыт | STACK_DECISIONS §3 | | ~~Ф-8~~ | ~~Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX~~ — **закрыт 02.08 без решения владельца.** Вопрос был поставлен как выбор из двух плохих вариантов, а у него есть отраслевой третий: `@eslint-community/eslint-plugin-eslint-comments` 4.7.2 (`no-unlimited-disable` + `require-description` + `disable-enable-pair`). Голое отключение падает тремя ошибками, точечное подавление с причиной проходит. Урок: не выносить владельцу технический вопрос, не поискав отраслевую практику | закрыт | адверсариальное ревью S1 | | ~~Ф-9~~ | ~~**Хвосты гейтов после адверсариального ревью S1**~~ — **все четыре закрыты 04.08 в S2**, каждая живым нарушением: именованные и системные цвета в TSX ловятся разрешающим списком на цветных атрибутах (`fill`/`stroke`/`color`/`stopColor`/`floodColor`/`lightingColor` — только `none`, `currentColor`, `inherit`, `transparent`, `var(--…)`) · значение CSS-переменной в `style={{…}}` обязано быть числом или токеном · носитель `style` внутри объектного литерала запрещён, и тем самым закрыт JSX-спред · системные цветовые ключевые слова закрыты `declaration-property-value-disallowed-list` по ВСЕМ свойствам, то есть и в сокращённых записях `outline`/`box-shadow`/`border`. Перечень проверенных форм (15 TSX + 14 CSS) — `FRONTEND_PLAN.md` §5.4.1. **Граница названа честно и там же:** гейт цветных атрибутов ловит литерал и шаблон, но не вычисленное значение (`const c = 'red'; `) — синтаксически это не берётся, и объявлять строку «закрытой без остатка» было бы ровно тем переобещанием, из-за которого она и заведена | закрыт для литеральных форм, граница названа | адверсариальное ревью S1 | | ~~Ф-10~~ | ~~**Хвосты структурных тестов**~~ — **закрыты 04.08 в S2.** `cssModules.test.ts` строит пары по ИМПОРТАМ, а не по совпадению имён файлов, видит деструктуризацию `const { name } = styles` и выкидывает `url(...)` и строковые литералы до поиска классов; проверка сиротства стала «модуль, который никто не импортирует». `tokens.test.ts` получил замок полноты: каждый токен обязан попасть либо в замеры, либо в явный список подобранных, и обратно — список не может ссылаться на исчезнувший токен. Плюс новое: числа для виртуализатора живут в `src/tokens/measures.ts` и сверяются с одноимёнными токенами. Семь живых нарушений, перечень — `FRONTEND_PLAN.md` §5.4 | закрыт | адверсариальное ревью S1 | | ~~Ф-11~~ | ~~**Контраст ниже порога WCAG на 4 узлах на маршрут**~~ — **ЗАКРЫТА 09.08 в S3.5 замером: axe печатает НОЛЬ узлов контраста на всех семи маршрутах.** Решение владельца тут не понадобилось, потому что вопрос был поставлен неверно: узлы давал не «весь приглушённый тон», а КОНКРЕТНАЯ роль — `--color-text-muted` (#707479 на панели = 3.85:1) стоял под текстом, несущим смысл. В S3.5 весь такой текст переведён на `--color-text-secondary` (5.36:1 до перекраски, 5.6:1 после), а `--color-text-muted` остался только под НЕтекстом — иконками и дорожками индикаторов, к которым порог для текста не предъявляется. Заодно палитра стала тёплой (замечание 16), и это подняло числа ещё немного. Верности референсу это не стоило ничего: приглушённая ступень как ступень осталась | закрыт | ревью S1 №2 → S3.5 | | Ф-12 | **Масштаб списков** — **главное закрыто 04.08 в S2 замером, а не обещанием.** Заведён маршрут `/scale`: та же оболочка на настоящем масштабе — 2284 раздела и банк на 1200 терминов (`src/mock/scale.ts` через `getScaleFixture()`). Дерево и банк виртуализованы по умолчанию (RAC `Virtualizer` + `ListLayout`), и это измерено в живом браузере: на 2293 узлах дерева в DOM держится **31 строка**, `scrollHeight` 59 618px, кадр при прокрутке — медиана 22 мс, p95 31 мс; банк на 1200 терминах — 30 строк в DOM, медиана 16,7 мс. Прокрутка до конца и обратно счётчик строк не растит. **Замечания намеренно НЕ виртуализованы:** их список пер-главный, а юнитов на главу ≈1,9 (ПТ-21) и флаг у юнита один — 10³ там не бывает по построению; сводный вид по книге, если он появится, придёт с S6 и получит тот же примитив `ui/List`. **Два хвоста строки НЕ сделаны и получают диспозицию, а не зачёркивание** (нашло адверсариальное ревью S2): дерево осталось плоским без группировки по сотням — это работа экрана глав (S4), у оболочки для неё нет ни данных, ни требования; банк без разделения «подписано / ждёт решения» — это работа подписного экрана (S5), справочному виду в 320px разделение только мешает | масштаб закрыт; группировка — S4, разделение банка — S5 | ревью S1 №2 | | Ф-14 | **Вход фронта в контракт API (единый бэклог, строка 95)** — **доведён до артефакта 04.08 в S3: [`API_CONTRACT_INPUT.md`](API_CONTRACT_INPUT.md)**, перечень с обоснованием на каждый пункт, каждое обоснование грунтовано `file:line` движка (прочитано, не пересказано). Внутри: язык кодами · идентификатор главы отдельно от номера (номер производный от чанкера, `chunker.go:101-118` — пустая глава не потребляет номер, правка исходника сдвигает нумерацию) · единица пары = ЮНИТ с ключом лидера `(chapter, firstChunkIdx)`, `export.go:88-93` · состояние у прогона, у главы выполнение · прогресс пофазно по юнитам · `run_id` · трёхзначный `auto\|draft\|approved` вместо булева `signed` · непрозрачный ключ строки банка (автоинкремент движка стабильным НЕ является — `migrate.go:172-174`, банк заменяется целиком каждый прогон) · поля `type`/`sense`/`since_ch`/`source` с точным словарём · подпись = набор решений promote/decline, стоп снимается ТОЛЬКО полным набором (`mining.go:200-202`) · продуктовый словарь вердиктов и запрет `detail` на стороне контракта · монотонный `id` + `Last-Event-ID` + ревизия на чтениях из того же счётчика · **разрешающий список полей вместо вычитания** (инвентарь протечек `StatusReport`/`ChapterPassport`: деньги · `routing`/модели · снапшот/дрифт/ре-билл) · правило эволюции словарей под Ф-22. Плюс 6 вопросов (К-1..К-6), на которые фронт ответа не имеет. **Найдено при сборке — четвёртый экземпляр класса «фикстура выдумала форму»:** титульная политика pack-13 рендерит заголовок ДЕТЕРМИНИСТИЧЕСКИ («Глава {n}» из `configs/langpacks/zh-ru/heading.txt`), вырезает исходный маркер из текста для модели (`chunker.go:110-118`) и ВКЛЕИВАЕТ титул внутрь финального текста первого юнита (`export.go:209-215`), оставляя колонку исходника без него — а фикстура рисует «Раздел N. …» в дереве и неснятый «第二节:» в оригинале. Не чинится до ответа на К-2: правка формы заголовка без контракта даст пятую выдумку. **По слову владельца тем же днём написан и ЧЕРНОВИК самого контракта — [`API_CONTRACT_DRAFT.md`](API_CONTRACT_DRAFT.md)** (12 разделов + приложение-карта вердиктов; каждое место помечено «выведено из кода» / «предложено фронтом» / «открыто»; двухфазный поток ВЫВЕДЕН из `--verify-bank`, а не спроектирован; в конце — прогон ревью-вопроса строки 95 по шести изменениям движка). Самопроверка черновика нашла дыру в самой лестнице статусов строки 95: между «разбор» и «перевод» нет состояния покоя, а библиотека обязана показывать разобранную книгу, которую не запускали ⇒ заведено `not_started`, и сегодняшние девять значений типов разводятся на десять. **Дофикс по ревью оркестратора (04.08) исполнен, форма сменена по решению владельца:** нормативная поверхность контракта — теперь **OpenAPI 3.1 `docs/api-contract/openapi.yaml`** (spectral шестым шагом `check`, openapi-typescript генерит `src/api/schema.ts`, тест дрифта + ветка неизвестного значения на шве `src/api/contract.ts`, MSW-хендлер как proof-of-form); md стал спутником — провенанс, обоснования, К-вопросы. Закрыты оба блокера ревью (состояние пары выводится из ПАРЫ «вердикт + наличие текста», а не из вердикта — флагнутый юнит легально приходит С текстом; чтение банка помечено зависимостью «канала нет»), восемь ✓-инфляций пере-размечены на ◆, внесены все фактические правки (фантом `auto` в провенансе убран по путям записи · `upstream_not_ok` добавлен · ПТ-34 заведён пунктом · стоп по потолку разведён с `failed` · правило разрыва `Last-Event-ID` · смягчён §12 про чанкер). Открытых вопросов стало девять: добавлен К-9 — **отказ прескрина (строка 94) не выразим ни одним из десяти статусов**. **Ратифицировать и перенести в `docs/architecture/14-api-contract/` — оркестратор; фронт этого не может** (шапка этого файла). **Подрезана 04.08 по слову владельца:** из описаний спеки убран провенанс и грунты `file:line` — они дублировали спутник и разошлись бы с ним при первой правке; осталось норматив плюс та оговорка «почему», что предотвращает конкретную ошибку. Спека переведена на английский, потому что её описания приезжают доккомментариями в генерённый `src/api/schema.ts` (Ф-25). Адверсариальная проверка подрезки нашла в спеке три расхождения с самим спутником (провенанс `mined` подан как «никем не подписано» — это ось `status`; ревизия кадра объявлена фактом при открытом К-4; `Run.status` берёт книжный словарь целиком) и два вопроса, которых не было: **К-10** пофазность выполнения главы и **К-11** условная обязательность полей ⇒ открытых стало одиннадцать. **Бумага сокращена тем же днём:** `API_CONTRACT_INPUT.md` под ⚠-баннером «исполнено, не поддерживается» — его 18 разделов лежат один-в-один в §2 спутника, и как живой документ он был третьим местом правки; не удалён (untracked, удаление невосстановимо), перенос в `docs/archive/` — оркестратору. Из спутника убран §1 (пины и отклонение по пиру дублировали `STACK_DECISIONS.md` §3 и Ф-23/Ф-24). **Второй пасс ревью (round-2) исполнен:** чтение выгрузки + К-12 · условие `if action=promote → dst` непустой (spectral валидирует, генератор ИГНОРИРУЕТ — записано в К-11 измерением) · `kind` nullable с правилом пустого, пометка ✓→◆ · `resume` после потолка помечен зависимостью на платформенный механизм поднятия · §2.7 переписан: «текст + замечание» это класс, но `glossary_miss` в него не входит (проверено по `memberDrops`/`waverun`), фикстура `r2-2` переведена на c-lite drop · ветка неизвестного добита до шести словарей. Открытых вопросов двенадцать. **08.08 (S3): РАТИФИЦИРОВАН и ПРАВЛЕН.** Контракт ратифицирован оркестратором как D39.99; S3 внесла в зонную копию весь список правок своего промта и бампнула версию до **0.2.0** — `paused`, `eta_seconds`, метка главы из данных, пер-книжная ревизия с правилом `>= R` и обязательным `resync_required` после полной замены, keyset-курсор на ВСЕХ списках, `X-TM-Client`, `Run.paused_reason`, `GET /usage`, `GET /books/{id}/run-options` с потолком прогона в ГЛАВАХ, same-origin. Байт-сверка с каноном намеренно даёт расхождение: канон правит оркестратор, диффом. Открытых вопросов стало **семь** (закрыты К-1/К-3/К-4/К-5/К-7/К-8/К-12, добавлен К-13 — Ф-31). Что переносить в канон — канон `docs/architecture/14-api-contract/` (патч ПРИМЕНЁН и удалён, D39.115) | правки внесены · ждут пере-ратификации диффом | ревью S1 №3 · сборка S3 04.08 · дофикс ревью 04.08 · подрезка 04.08 | | ~~Ф-15~~ | ~~**`src/api/` синхронен, состояний загрузки и ошибки в типах нет**~~ — **ЗАКРЫТА 08.08 в S3.** `src/api/` асинхронен и разговаривает HTTP: MSW как СЕТЕВОЙ мок (а не подменённый модуль), поэтому приложение делает настоящий `fetch` и настоящий `EventSource`, и ветки ожидания/ошибки/пустоты — это код, который работает в бою, а не второй его экземпляр. Ветки живут в одном месте (`showcase/Loaded.tsx`) и приезжают ВМЕСТЕ со слоем данных, до первого продуктового экрана, как строка и требовала. Типы сведены с контрактом: `types.ts` удалён, экраны живут на суженных типах `api/contract.ts`. **Что нашёл сам скриншот-цикл, а не рассуждение:** выключенный запрос TanStack Query остаётся `pending` НАВСЕГДА, и на пустой библиотеке правая панель вечно показывала «Идёт загрузка» — различение «ждём» и «выключено» (`fetchStatus === 'idle'`) добавлено после того, как это увидели на кадре. Фикстуры всех состояний — шесть миров, по маршруту на каждый | закрыт | ревью S1 №3 | | Ф-13 | **`eslint-plugin-jsx-a11y` не встаёт на ESLint 10** (peer `^3..^9`, проверено `--dry-run` → ERESOLVE). Статическую половину доступности держит axe в скриншот-цикле; плагин подключить, когда выйдет совместимый мажор | триггер: мажор плагина | ревью S1 №2 | | Ф-7 | **Хвосты сведения витрины с референсом** (`FRONTEND_PLAN.md` §5.2). **Часть S2 закрыта 04.08:** `+` стоит в ряду вкладок ЛЕВОЙ панели и запускает «Добавить книгу» (решение владельца 04.08); дублирующая строка «+ Добавить книгу» из дерева убрана — одна вещь, одно место. В центре и справа `+` нет и не будет: наборы вкладок там фиксированные, пользователь их не расширяет, и кнопка вышла бы декоративной — ровно то, что эта строка запрещает. Состояния наведения перестали быть «непроверенными»: они сняты не статичным кадром, а прогоном браузера (`data-hovered`/`data-selected`/`data-focus-visible` у кнопки, вкладки, строки дерева и списка). **Осталось на S6:** приглушённая колонка оригинала — решение, а не замер, пересмотреть на настоящей читалке | ~~S2~~ · S6 | S1, сверка с fleet.png | | Ф-16 | **Выгрузка книги (§3.11) ушла из верхней полосы.** В витрине S1 там висела иконка загрузки без действия; по правилу Ф-7 («кнопка только вместе с действием») она снята до S7, который строит экспорт. Вернуть иконку вместе с самим действием | S7 | S2 | | Ф-17 | **Закрытие вкладки с клавиатуры не объявлено скринридеру.** Крестик у вкладки-документа сознательно НЕ кнопка: у роли `tab` дети презентационные (ARIA), вложенный фокусируемый элемент выпал бы из дерева доступности, оставшись в порядке табуляции (это же ловит axe правилом `nested-interactive`). Мышь закрывает крестиком, клавиатура — `Delete` на выбранной вкладке. Объявить сочетание нечем: `aria-keyshortcuts` не входит в типизированный API `Tab`/`TabList` в react-aria-components 1.20.0. Пересмотреть при обновлении библиотеки либо при появлении палитры команд | триггер: мажор RAC или экран горячих клавиш | S2 | | ~~Ф-19~~ | ~~**Переключение вкладки размонтирует её содержимое, и прокрутка длинного списка теряется.**~~ — **ЗАКРЫТА 09.08 в S3.5, и механизмом.** Лекарство выбрано не вслепую: решение об устройстве оболочки принято тем же паком (модель вкладок VS Code, замечание владельца 2), и под него подошёл keep-alive, а не запоминание офсета. Реализация — проп `keepAlive` у примитива `ui/Tabs`: невыбранные `TabPanel` монтируются (`shouldForceMount`), стоят в одной ячейке грида и гасятся `visibility: hidden`. Именно `visibility`, а не `display: none`: последний уничтожает бокс и вместе с ним офсет прокрутки — то есть лечил бы половину болезни. Библиотека сама вешает на невыбранную панель `inert` и `data-inert`, поэтому из дерева доступности и порядка табуляции она вынута, а axe на дублях содержимого не срабатывает (проверено прогоном на семи маршрутах). **Цена, которую пришлось заплатить и назвать:** при keep-alive все открытые вкладки живут одновременно, поэтому общий запрос глав отдал бы каждой текст АКТИВНОЙ главы — запрос переехал в `showcase/Document.tsx`, по запросу на открытую вкладку. Приёмка: `npm run scenes scroll` — прокрутка банка на `/scale` до 9000 → уход на «О книге» → возврат → офсет 9000. **Хвост, которого раньше не было:** скрытые панели остаются в DOM, поэтому глобальный локатор по роли в тестах находит строку соседней вкладки — сценарии обязаны искать внутри своей области | закрыт | адверсариальное ревью S2 → S3.5 | | Ф-20 | **Мёртвый шрифт и мёртвые токены в поставке.** `@fontsource-variable/jetbrains-mono` грузится в бандл, но `--font-mono` не выбран ни одним правилом: художественную прозу моноширинным не набирают, а во Fleet на этом месте был код. Туда же несколько токенов, которые ни разу не используются (`--color-current-line`, `--color-hint`). Замок полноты токенов (S2) требует классифицировать токен, но не требует его использовать. Решать на S6, когда читалка станет настоящей: либо моноширинный находит работу, либо импорт и токены уходят | S6 | адверсариальное ревью S2 | | ~~Ф-22~~ | ~~**У закрытых юнионов нет ветки неизвестного значения**~~ — **ЗАКРЫТА 08.08 в S3, и механизмом, а не дисциплиной.** Ветка стоит НА ШВЕ (`src/api/vocabulary.ts` + нормализация в `contract.ts`): значение, пришедшее с провода, сужается один раз, там, где входит, и незнакомое становится `null`, а не роняет дерево на `undefined.tone`. Словарь объявлен как `Record<Значение, Смысл>`, поэтому список значений ВЫВОДИТСЯ из записей — разойтись с типом ему нечем, а новое значение контракта роняет `tsc` с именем пропавшего значения в тексте ошибки (проверено живым нарушением, `FRONTEND_PLAN.md` §5.4.3). Тем же ходом закрыт названный в строке второй дефект того же класса: незнакомая ступень замечания больше НЕ уходит в спокойную ветку — неизвестная ступень берёт ветку внимания, потому что единственное, чего ось важности делать не вправе, это прятать важное. Продуктовое слово ступени по-прежнему за владельцем (Ф-21), и оно тут не выдумано | закрыт | адверсариальное ревью S2 → ревью оркестратора 04.08 → S3 08.08 | | Ф-21 | **Ступень замечания различается только цветом полоски** (`attention` синяя, `glance` серая). Зрячему разница читается ещё и светлотой, но скринридеру приоритет не достаётся вовсе. Лечение — слово, как это уже сделано для состояния прогона книги; **какое именно слово, решает владелец**: словарь замечаний продуктовый (ПТ-33), а §3.8 требует, чтобы экран не выглядел тревожным, так что «важно/неважно» не годится | решение владельца | адверсариальное ревью S2 | | Ф-18 | **Бандл 545 КБ (168 КБ gzip) одним куском** — замер 04.08 после установки `react-aria-components`; Rolldown предупреждает про порог 500 КБ. Для десктопного SPA за логином это не авария (индексации нет, сеть локальная), но одним куском едут и экраны, которых пользователь ещё не открыл. Лечение — разбиение по маршрутам, и делать его осмысленно после S4–S7, когда экраны появятся: сейчас резать нечего | после S7 либо при жалобе на первый экран | S2, замер | | Ф-23 | **`openapi-typescript` держится на точечном `overrides`, а не на честном пире.** Генератор объявляет `typescript@^5.x` (7.13.0 вышла 11.02.2026, до релиза TS 6), у нас намеренный TS 6 — Ф-3. `--legacy-peer-deps` **отвергнут проверкой исполнением:** после него обычный `npm install` падает, то есть дерево ломается у каждой следующей сессии и у `prepare`-инсталлера pre-commit хука. Взят `overrides` на ОДИН пакет (`{"openapi-typescript":{"typescript":"$typescript"}}`) вместо глобального ослабления проверки пиров; работоспособность доказана: `npm ci` с нуля зелёный (0 уязвимостей), генератор запускается на TS 6, тест дрифта проходит. Снять, когда генератор расширит пир либо когда приедет TS 7.1 и Ф-3 закроется | триггер: пир генератора расширен ИЛИ TS 7.1 (Ф-3) | S3, дофикс контракта 04.08 | | Ф-24 | **AsyncAPI рассмотрен и отложен.** Он описал бы SSE-канал строже, чем нынешняя таблица «имя события → схема» в описании `EventEnvelope`. Отложен, потому что это второй нормативный артефакт и второй линтер ради ОДНОГО эндпоинта, при том что полезные нагрузки уже описаны схемами OpenAPI и попадают в генерённые типы. Пересмотреть, когда каналов станет больше одного либо когда понадобится генерировать клиент потока, а не разбирать кадры руками | триггер: второй канал ИЛИ генерация клиента потока | S3, дофикс контракта 04.08 | | Ф-25 | **`src/` смешанный по языку комментариев.** Конвенция владельца от 04.08 — исходники английские; мой дифф S3 ей приведён (`api/contract.ts`, `api/contract.test.ts`, `mock/handlers.ts`, `.spectral.yaml`, `.prettierignore`, хунк в `tokens/tokens.test.ts`), а комментарии S1/S2 в ~40 файлах остались русскими (`api/index.ts`, `api/types.ts`, `showcase/*`, `ui/*`, `tokens/*`, `scripts/*`). Свод — отдельный проход: он трогает уже отревьюенный и залэнденный код, то есть даёт дифф чистой косметики поверх чужой работы, и делать его надо одним движением, а не по файлу за сессию. **Границу держать явно:** переводятся комментарии, имена тестов и служебные строки; UI-текст и фикстурная проза остаются русскими — это продукт, а не код | отдельный проход, по слову владельца/оркестратора | слово владельца 04.08 | | Ф-26 | **Выгрузка и загрузка книги в слое данных не заведены — намеренно.** `POST /books` (multipart с прогрессом отправки) и пара `POST/GET /books/{id}/exports` (опрос по `Retry-After`) несут МЕХАНИЗМ, а не поверхность: прогресс загрузки — часть экрана S4, цикл опроса — часть экрана S7. Строить их сейчас значило бы построить механизм без задачи, что проектный норматив прямо запрещает, и выбросить его при первом же настоящем экране. Контракт обе поверхности объявляет, генерённые типы для них есть — недостающего кода ровно на десяток строк, когда появится, кому их звать | S4 (загрузка) · S7 (экспорт) | S3 08.08 | | Ф-27 | **`GET /usage` читается слоем данных, но ни один экран его не показывает.** Так и задумано: страница статуса использования живёт в настройках (ПТ-35/П-5), а настройки строит S7. Функция и её тест существуют, чтобы поверхность контракта была покрыта целиком и чтобы S7 не пришлось трогать шов; если S7 сдвинется далеко, строку пересмотреть — непрочитанный код тоже стоит денег | S7 | S3 08.08 | | Ф-28 | **Три high-уязвимости в дереве, все транзитивные и все в инструментах сборки** (замер 08.08): `js-yaml` через `@redocly/openapi-core` ← `openapi-typescript`, `js-yaml` через `cosmiconfig` ← `stylelint`, `nanoid` через `postcss` ← `stylelint`. **К новому пину S3 отношения не имеют** — проверено `npm ls`, `@tanstack/react-query` ни одну из них не тянет; это адвайзори, опубликованные после 04.08, когда журнал фиксировал «0 уязвимостей». В браузер не едет ничего из перечисленного. Пины ратифицированы (`STACK_DECISIONS` §3), и двигать их посреди сессии, которая на этих же инструментах держит гейт, было бы разменом безопасности сборки на риск красного дерева у всех следующих сессий — поэтому измерено и передано, а не пропатчено молча | решение оркестратора/владельца: бампнуть пины отдельным проходом | S3 08.08, `npm audit` | | Ф-29 | **Мок-сеть едет в продакшн-бандл.** `startMocking()` подтягивает `src/mock/` динамическим импортом, но скриншот-цикл гоняет `vite build` + `preview`, то есть продакшн-режим, и без мока там нечего показывать. Пока платформы на том же origin нет, это цена работоспособности; в день, когда фронт заговорит с настоящей платформой, папка `mock/` удаляется целиком и импорт уходит вместе с ней — ровно так, как обещал шов. Строка нужна, чтобы это удаление кто-то сделал ОСОЗНАННО, а не обнаружил мок в бою | триггер: живая платформа на том же origin | S3 08.08 | | Ф-30 | **Метка главы без заголовка — голый порядковый номер.** Контракт запретил синтез «Глава {n}» (К-3: метка из ДАННЫХ книги, легальна книга без номеров и глав вовсе), и фикстуры содержат такие главы намеренно. Когда у главы нет ни `heading`, ни `number`, в дереве остаётся позиция в порядке чтения — на кадре это строка «12» среди названий. Врать словом фронт не стал: словарь продуктовый, и это ровно вопрос В-4, который уже стоит владельцу. Строка держит хвост до его ответа | решение владельца (В-4) | S3 08.08, кадр `/scale` | | Ф-31 | **К-13 — новый вопрос контракта, заведённый этой сессией.** Достижение СОБСТВЕННОГО потолка прогона (`ceiling_chapters`) и исчерпание кредита аккаунта сегодня неразличимы: `paused_reason` имеет ровно одно значение, `credit_exhausted`, и оно про кредит. Механически они могут совпасть — платформа берёт холд на сумму потолка до спавна, — но человек, упершийся в свой колпак, и человек, у которого кончились деньги, находятся в разных положениях, и фраза у них разная. Передано оркестратору в канон `docs/architecture/14-api-contract/` (патч применён и удалён, D39.115) §4; в спеке помечено в `EventCeiling` | владелец + платформа | S3 08.08 | | Ф-32 | **Замечания читаются пер-КНИЖНО, а показываются пер-главно.** Контракт даёт `GET /books/{id}/notes` и не даёт пер-главной ручки; панель контекста фильтровала по открытой главе на клиенте — **с S3.5 фильтр снят, сводка пер-книжная (замечание владельца 8, строка Ф-38), то есть пер-книжное чтение теперь и показывается пер-книжно**. Пока это дёшево — дефолт страницы 500, а на книге с сотнями замечаний клиент тянет весь список ради трёх строк, и с курсором дотянет ВЕСЬ. Ф-12 обосновывал отказ от виртуализации замечаний тем, что список пер-главный и мал; после этой правки он пер-главный на ЭКРАНЕ, но пер-книжный в ПАМЯТИ. Решать не фронту: либо пер-главная ручка (вопрос автору контракта), либо явный потолок с честным «показаны не все». Обнаружено вторым адверсариальным проходом 08.08 при починке того, что панель «О читаемой главе» показывала замечания всей книги | S6 (читалка) либо раньше, если появится книга с тысячами замечаний | второй проход ревью S3 | | Ф-5 | **Tauri 2.x вторым шагом** — по явным триггерам (трей · глобальные горячие клавиши · распространяемый .exe · офлайн · хранилище учёток ОС); до того — установимое PWA, только браузерные API | триггеры названы | STACK_DECISIONS §6, D39.84 | | Ф-6 | **`@tanstack/react-virtual`** — в резерве, подключать только по замеру (дефолт виртуализации — RAC Virtualizer). **Замер сделан 04.08 (S2) и триггер НЕ наступил:** RAC `Virtualizer` держит 2284 раздела на 31 строке в DOM при медиане кадра 22 мс и 1200 терминов на 30 строках при 16,7 мс. Вторая библиотека виртуализации сейчас была бы платой без выигрыша; пересматривать — если появится список с переменной высотой строки (читалка S6) и `estimatedRowSize` не справится | по замеру; замер 04.08 отрицательный | STACK_DECISIONS §2 | | Ф-33 | **Поиска по ТЕКСТУ книги в контракте нет вовсе.** Замечание владельца 4 («две кнопки поиска, делают одно и то же») разведено так: лупа в правом верхнем углу стала настоящей палитрой быстрого перехода к разделу или термину, а вкладка-заглушка «Поиск» из левой панели СНЯТА. Снята потому, что построить её нечем: у контракта пятнадцать ручек и ни одной поисковой (`GET /books/{id}/…` даёт списки, не запросы), то есть вкладка была обещанием без адресата. Вопрос не фронта: полнотекстовый поиск по книге — это либо ручка платформы, либо индекс на клиенте поверх выгрузки, и то и другое решает автор контракта. До ответа поиск по книге живёт как хвост читалки | S6 либо ручка контракта | S3.5, замечание 4 | | Ф-34 | **Светлая тема: цена честно замерена, дёшево не выходит.** Проверено по коду, а не на глаз. Дёшево — только половина: значения цвета лежат в одном месте (`tokens.css`), второй набор под `:root[data-theme='light']` механичен. Дорого — вторая половина: подпись оболочки, снятая с Fleet, это «карточки на почти чёрном фоне», и она не инвертируется значениями — в светлой теме отношения поверхностей приходится ПЕРЕПРИДУМАТЬ (что светлее, панель или геп; чем отделять карточку без теней; что делать с вырезом активной вкладки, который держится на паре «хром ↔ полотно»). Плюс переключатель и его персист, плюс пере-прогон axe по семи маршрутам на второй теме, плюс замок токенов на второй набор. Оценка: отдельный проход масштаба S7, а не хвост фикс-пака. Пункт назван в каркасе настроек честно: «сейчас доступна только тёмная» | S7 либо отдельный пакет | S3.5, замечание 15 | | Ф-35 | **Язык интерфейса: словаря строк нет, есть ~90 литералов в разметке.** Дёшево сделана только та часть, что снижает цену завтра: локаль интерфейса сведена в ОДНО место (`ui/Locale.tsx`, `interfaceLocale`) — прежде её порознь зашивали провайдер примитивов, `Intl.DisplayNames`, `Intl.NumberFormat` и `toLocaleDateString`. Само переключение требует словаря строк, хранилища выбора и гейта против литералов в разметке (иначе следующая сессия заведёт строку мимо словаря — класс ошибки известный), а ещё продуктового решения «какие языки». Оценка: механический проход по ~15 файлам плюс гейт плюс решение владельца. Пункт назван в каркасе настроек: «сейчас интерфейс только русский» | S7 либо отдельный пакет | S3.5, замечание 15 | | Ф-36 | **Физический масштаб интерфейса на мониторе — ручки нет.** Замечание владельца 1 говорит «великоваты, особенно на 27″ 2K». Замер при вьюпорте референса 1280×764 расхождения не подтвердил: промежутки, ширины панелей, шаг строки и высота вкладки совпадают с Fleet (FRONTEND_PLAN §5.5). Значит речь про ФИЗИЧЕСКИЙ размер на конкретном экране, а он зависит от масштабирования ОС, и лечится он не сдвигом токенов вслепую (это разъедет витрину с референсом), а ручкой «плотность интерфейса» в настройках. Цена ручки названа честно: все размеры сегодня в px, для одного множителя их надо перевести в rem и задать `font-size` на корне — механически, но трогает КАЖДЫЙ размерный токен и все замеренные числа, поэтому отдельным проходом с пере-замером. ⚠ **Дополнено 10.08:** предел меры набора (`--reader-width`), заведённый S3.5 «пока нет ручки», владелец отверг словами «жёстко приклеен к центру, до этого было намного лучше» — токен удалён, набор снова во всю ширину панели. Значит длина строки на 2K остаётся открытой ровно до ручки плотности, и подпирать её геометрией больше нельзя | владелец (В-7) + отдельный проход | S3.5, замечание 1 | | Ф-37 | **Пометка черновика висит на статусе КНИГИ, а не главы.** Замечание 24 сделано данными контракта: пока прогон не довёл книгу до `ready`, текст помечен как черновой. Но у главы есть своё выполнение (`units_done/units_total`), и глава, уже прошедшая обе волны в незавершённой книге, получает ту же пометку, что и непереведённая. Точнее было бы по главе, и для этого в контракте не хватает одного: `units_done` не разведён по фазам (открытый вопрос К-10 спутника). До ответа взята безопасная сторона — предупредить лишний раз дешевле, чем не предупредить | контракт (К-10) → S6 | S3.5, замечание 24 | | Ф-38 | **Вкладка «Замечания» защищена сводкой по книге — слово за владельцем.** Замечание 8 («непонятно зачем, всё рисуется в тексте») по пер-ГЛАВНОМУ списку было верным: он дублировал читалку. Вкладка переделана в пер-КНИЖНУЮ сводку со ссылкой в раздел — сценарий «пройтись по всем местам, требующим внимания», который без неё не живёт и который не стоит ни одного лишнего запроса (контракт даёт замечания пер-книжно, Ф-32). Если владелец сценария не признает — вкладка снимается одной строкой в `Context.tsx`, вместе с ней уходит и `notesQuery` с экрана | решение владельца | S3.5, замечание 8 | | Ф-39 | **Банку не хватает полей для проверки термина, и выдумывать их фронт не стал.** Замечание 7/13 просит на термин род/пол, описание-обоснование и варианты перевода; в контракте (`BankTerm`) их нет — есть `src`, `dst`, `kind`, `status`, `origin`, `sense`, окно `since_chapter`/`until_chapter`. Показано всё, что есть; недостающее оформлено ЧЕРНОВИКОМ правки спеки с пометкой «предложено» (`frontend-PROGRESS.md`, запись S3.5) и не заведено ни в фикстуру, ни в типы — класс ошибки «фикстура выдумала форму» ловился четырежды (Ф-14). Ратифицирует оркестратор | оркестратор + владелец | S3.5, замечания 7 и 13 | | Ф-40 | **Вид поля ввода описан в двух модулях.** `ui/TextField.module.css` и `ui/FilterField.module.css` держат байт-в-байт одинаковые `.input`, `::placeholder` и `:focus`; отличие одно — цвет границы в покое. Роли примитивов при этом РАЗНЫЕ и слить их нельзя: `SearchField` очищается Escape и не требует подписи, `TextField` подпись требует — это форма. Лечение — общий модуль стилей поля на оба примитива, и оно требует переезда классов; делать его внутри фикс-пака оболочки значило бы менять то, что пак не трогает, ради семнадцати строк CSS. Нашло адверсариальное ревью S3.5 | ближайшая сессия, трогающая `src/ui/` | S3.5, ревью энтропии | | Ф-41 | **Спойлер банка раскрывается только наведением.** Клавиатура и тач его не раскроют, а перевод остаётся в DOM (скринридер прочитает). Кнопка раскрытия внутри строки списка невозможна: у роли `option` дети презентационные, и вложенный интерактив роняет гейт axe (та же ловушка, что Ф-17). Настоящее лечение — раскрытие на уровне СПИСКА (один переключатель «показывать термины из будущих разделов» над ним) либо экран подписи S5, где строка перестаёт быть строкой списка. ⚠ **Уточнено 10.08 (S3.6):** спойлер снят с ПЕРЕВОДА и поставлен на описание (`sense`) — закрытый перевод делал строку непроверяемой, а таблица существует ради проверки. Второй хвост строки («состояние замещается объяснением с N-го») ЗАКРЫТ: в таблице окно применимости и состояние — разные столбцы. Остался ровно один: раскрытие только наведением | S5 либо раньше по слову владельца | S3.5, ревью точности | | Ф-42 | **У keep-alive нет потолка.** Вкладки центра не размонтируются (в этом и лекарство Ф-19), поэтому каждая закреплённая глава держит смонтированную читалку и свой запрос: замерено адверсариальным ревью — семь вкладок дают семь запросов и семь читалок. На фикстурных главах это ничего не стоит, на настоящих (глава бывает одним блоком в тысячи знаков) — стоит. Лечение из отраслевой практики: вытеснять самые давние закреплённые вкладки сверх N смонтированных, оставляя их в ряду. Порог называть по замеру на настоящей книге, а не на глаз | S4 (экран глав) либо при первой жалобе на память | S3.5, ревью корректности | | Ф-43 | **У чтения банка нет канала — вся витрина банка стоит на моке.** Банк живёт в приватном SQLite движка, платформе читать его запрещено (D39.85), артефакта экспорта банка нет; компаньон контракта (`14-api-contract/README.md` §3) прямо пишет «строки нет — заводит оркестратор», и в живом `docs/PROGRESS.md` я её не нашёл. Пока канала нет, таблица банка (замечание владельца второго круга 7) работает только на фикстуре, а подписной экран S5 строить не на чем | **оркестратор: завести строку единого бэклога** → бэкенд (экспорт банка) → платформа (проекция) | S3.6 10.08, разбор движка | | Ф-44 | **Действие «подписать банк» не нарисовано — намеренно.** Модель владельца и движка совпадают: подписывается банк ЦЕЛИКОМ, стоп снимается только когда решён каждый термин (`pipeline/mining.go:201`), в контракте это `POST /runs/{id}/resume` с `409` при неполном наборе. Но за действием стоит механизм подписи (S5), а под ним — отсутствующий канал чтения (Ф-43). Кнопка без действия запрещена нормой зоны, поэтому в S3.6 построен ВИД и честное состояние. ⚠ **Уточнено 10.08 по слову владельца:** счётчики «подписано N / ждут решения N» с экрана СНЯТЫ вместе со столбцом состояния строки — они и были той самой путаницей «подписываем банк или термины» из замечания 20 первого круга. В подвале — размер банка, при сужении «показано N из M», и состояние ВСЕГО банка («банк ждёт подписи») из статуса книги. Рисовать действие — когда есть чем его исполнить | S5, после Ф-43 | S3.6 10.08 | | Ф-45 | **Править термин в таблице нельзя — только читать.** Владелец: «разумеется в банке можно подправить какой то термин». Контракт это выражает (`BankDecision`: `promote` с непустым `dst` либо `decline`, частичное накопление на сервере), то есть редактирование строки — это отправка решения, а не PATCH. Экран решений = S5; в S3.6 таблица читающая, и это названо честно, а не замаскировано неактивными полями | S5 | S3.6 10.08, замечание 7 | | Ф-46 | **У таблицы банка нет сортировки, и она не различает канон от предложенного.** Первое: сортировки по столбцу нет — примитив `ui/Table.tsx` на RAC `Table` её допускает (`sortDescriptor`), но проектировать навигацию по банку на 60 фикстурных терминах нельзя. Второе, найдено при снятии столбца подписи (10.08): в банке лежат и уже канонические термины, и свежепредложенные — подписывают, по сути, вторые, а таблица их не различает. Возвращать это как «состояние подписи строки» нельзя (владелец снял его прямо), значит нужен другой различитель, и он продуктовый. Сегодня путей два: поиск по трём полям и фильтр по типу со счётчиками. Оба вопроса упираются в настоящий банк книги (Ф-43) и в слово владельца | после Ф-43 + владелец | S3.6 10.08, замечание 7 | | Ф-47 | **Дефект контракта: `sense` не обязателен, хотя объявлен частью ключа уникальности.** `BankTerm.required` перечисляет восемь полей и `sense` в них нет, а его же описание говорит: термин уникален по `(book, src, sense, since, until)` (`openapi.yaml:983-1023`). Клиент вынужден писать `term.sense ?? ''` в трёх местах и не может отличить «различителя нет» от «различитель пуст» — при том что ровно по этому полю две легальные строки одного иероглифа отличаются друг от друга. Либо поле обязательно (пустая строка = нет различителя), либо ключ описан неверно | автор контракта (оркестратор) | S3.6 10.08, тайпчек | | Ф-48 | **Перетаскивание границ столбцов снято по замеру, а не по вкусу.** Отраслевая норма таблиц данных (MUI X `flex`+`minWidth`+resize, AG Grid) — колонки тянут рукой; в react-aria это `ResizableTableContainer` + `ColumnResizer`, и оно работает (проверено сценарием: столбец растянут 194→274px). Но A/B-замер драга ПАНЕЛИ с открытым банком на dev-сервере: без ресайза p90 33 мс и 7 длинных кадров, с ресайзом — **p90 83 мс и 44 длинных кадра**: контейнер пересчитывает ширины на каждый кадр изменения своей ширины. Владелец дважды жаловался именно на драг, поэтому выбрана плавность: остались текучие доли (`fr`) с минимумами на столбец. Возвращать — когда банк станет поверхностью РЕДАКТИРОВАНИЯ (S5, там ширины важнее) либо когда у библиотеки появится способ не пересчитывать ширины на ресайз контейнера. ⚠ **База замера УСТАРЕЛА (10.08, четвёртый круг):** «без ресайза p90 33 мс» на текущем дереве не воспроизводится — драг держит **p90 17 мс, 1 длинный кадр из 84**, и A/B по блюру и `contain` числа не двигает. Значит и цену ресайза (83 мс) надо мерить заново от новой базы, а не наследовать: возвращая ресайз, начинать с замера, а не со ссылки на эту строку | S5 либо новая версия библиотеки | S3.6 10.08, A/B-замер; база пере-замерена 10.08 | | Ф-49 | **Текст открытой главы не подписан ни на один сигнал изменения.** `keys.units` не инвалидируется нигде (`src/showcase/useRunStream.ts` — только notes/bank/book и общий resync); кадр `chapter` патчит лишь СПИСОК глав. Контракт при этом прямо предписывает обратное: `Unit` — «the live "something changed" signal arrives as an event; the text arrives with a read after the boundary» (`openapi.yaml:899-901`). Уточнение против первой формулировки: это не «устаревает на 15 секунд» — `staleTime` лишь разрешает рефетч, а триггеров у него три (фокус окна, реконнект, монтирование), и вкладки keep-alive не размонтируются. Читатель, не уводивший фокус из окна, нового текста НЕ УВИДИТ ВООБЩЕ. Инвалидации на `chapter` мало: если `units_done` останется единым счётчиком (открытый вопрос К-10 контракта), за всю черновую волну кадр `chapter` не придёт ни разу, а `target` на границе стадии уже наполнится — значит нужна и инвалидация открытых глав на смене `status` (те самые «stage boundaries and stops») | фронт, после ясности по К-10 | S3.6 10.08, ресёрч-агент + контракт | | Ф-50 | **Кадр потока, пришедший до того как чтение легло в кэш, теряется молча — а высокая вода уже сдвинута.** `useRunStream.ts`: обработчики `progress`/`status`/`chapter` делают `list === undefined ? list : {...}`, то есть в пустой кэш не пишут, а `acceptFrame` поднял отметку ревизии ДО этого. Дальше `freshest(undefined, next)` принимает первый ответ независимо от ревизии — в кэш ложится снимок, сделанный ДО кадра, а кадр уже «применён». Окно открывается на каждом старте экрана и при каждом resync. Рядом второе того же класса: многостраничное чтение штампует «рваный» список ревизией ПОСЛЕДНЕЙ страницы (`queries.ts`), хотя контракт предупреждает, что ревизия во время пагинации движется (`openapi.yaml:658-661`) | фронт | S3.6 10.08, ресёрч-агент | | Ф-51 | **`resync` сбрасывает один гард из двух.** `useRunStream.ts` на `resync_required` зовёт `invalidateQueries()` без ключа: данные при этом СОХРАНЯЮТСЯ, и гард `dropStaleReads` продолжает сравнивать пришедший снапшот со старым — а по контракту resync выдаётся именно после полной замены («the bank rebuilt from scratch, re-chunking replacing the chapters», `openapi.yaml:646-648`), где монотонность ревизии не обещана. На стриминговой стороне отметка честно сбрасывается, на стороне кэша — нет. Правильный примитив — `resetQueries`/`removeQueries`. Вторым слоем: тот же вызов без ключа бьёт по чужим скоупам (библиотека, usage, run-options, другие книги) | фронт | S3.6 10.08, ресёрч-агент |