> ⚠⚠ **ПРОМТ ОТРАБОТАН — исторический, НЕ задание.** Исполнен восьмой фронт-сессией (S3.7, 10–12.08), > **ПРИНЯТ и заленден D39.129** (`3e465bd` + доукомплектация `b3405ff`): контракт **0.2.2** в каноне > (Ф-47 sense-required), i18n-механизм построен (задача 9, ПТ-36), комментарии кода переведены на > английский; спорная находка 8.1 разрешена браузерным замером в пользу сессии. Живое состояние > зоны — `frontend-PROGRESS.md`; находки приёмки S3.7 — вход S4-промта. # Промт: фронт-сессия S3.7 — долги слоя данных и мелкие хвосты (до S4) Ты — фронтенд-сессия TextMachine, восьмая по счёту. Зона записи — **только `frontend/`**; сессия НЕ коммитит — дерево готовит и передаёт на лендинг оркестратору. Протокол зоны — стоящий промт `FRONTEND_SESSION_PROMPT.md` §«Зона и git», он в силе целиком; итоги, пинги и вопросы владельцу — ТОЛЬКО зонный журнал `frontend-PROGRESS.md` (в `docs/PROGRESS.md` фронт не пишет). В дереве живут незакоммиченные файлы ДРУГИХ зон (полигон: `eval/*`, `docs/*`) — не трогать и не «прибирать»; у полигона прямо сейчас идёт ЖИВОЙ платный прогон — ничего в `eval/` и `~/books/` не запускать, не читать замки, не убивать процессы. > Промт написан оркестратором (10.08.2026) по приёмке S3.6. Контекст холодному исполнителю: > оболочка и витрина построены сессиями S0–S3.6 и приняты владельцем (три круга замечаний); > слой данных стоит с S3 — `src/api/` разговаривает настоящим HTTP через MSW-мок сети, > живой поток идёт по SSE (`src/showcase/useRunStream.ts`), чтения — TanStack Query > (`src/api/queries.ts`), контракт — зонная копия `frontend/docs/api-contract/openapi.yaml` > (0.2.1), типы генерятся из неё. Эта сессия НЕ строит новых экранов: она закрывает названный > техдолг слоя данных (Ф-49…Ф-51), одну ратифицированную правку контракта (Ф-47), > интернационализацию строк интерфейса (Ф-35: словарь + гейт, без хардкода русского) > и мелкие хвосты. S4 стартует после неё отдельным промтом. > По всем задачам — прогон в отчёте; > **молча пропустить задачу нельзя**: сделано / диспозиция с обоснованием / вопрос В-строкой. ## Обязательное чтение до кода (порядок) 1. `FRONTEND_SESSION_PROMPT.md` — стоящий промт: зона, git, жёсткие ограничения. 2. `frontend-PROGRESS.md`: шапка «Текущее состояние», затем **приёмочная запись оркестратора 10.08 (первая в хронике)** — в записи S3.6 есть число-дрейф против финального дерева, и поправки перечислены именно там; затем сама запись S3.6 (⚠ её разделы 12–15 стоят в ОБРАТНОМ порядке после §11). `BACKLOG.md` строки Ф-40, Ф-47, Ф-49, Ф-50, Ф-51 — формулировки не переизобретать, но и не принимать на веру: КАЖДУЮ проверить по коду до правки. 3. Контракт: зонная копия `frontend/docs/api-contract/openapi.yaml` — §`Unit`, `EventEnvelope`, `Revision`, `resync_required`, `BankTerm`; поведение ревизий и правило `>= R` — норматив. 4. `FRONTEND_PLAN.md` §5 — гейты и замеры: что уже стоит и как расширять. ## Задачи ### 1. Ф-49 — текст открытой главы не подписан ни на один сигнал изменения `keys.units` не инвалидируется нигде: `useRunStream.ts` по кадрам инвалидирует notes/bank/book, кадр `chapter` патчит лишь СПИСОК глав, а открытые главы живут на keep-alive вкладках и не размонтируются — читатель, не уводивший фокус из окна, нового текста не увидит вообще. Контракт прямо предписывает обратное (`openapi.yaml`, `Unit`: «the live "something changed" signal arrives as an event; the text arrives with a read after the boundary»). Сделать: инвалидация чтений юнитов открытой главы на кадре `chapter` этой главы И на смене `status` книги (границы стадий — за всю черновую волну кадр `chapter` может не прийти ни разу, пока К-10 открыт). Безопасная сторона: лишняя инвалидация дешевле невидимого текста, но НЕ инвалидировать все главы на каждый кадр — скоуп по ключу главы из кадра. ### 2. Ф-50 — кадр потока, пришедший до появления чтения в кэше, теряется молча `useRunStream.ts`: обработчики делают `list === undefined ? list : {...}` — в пустой кэш кадр не пишется, а `acceptFrame` уже сдвинул отметку ревизии; `freshest(undefined, next)` затем принимает снимок, сделанный ДО кадра. Окно — старт экрана и resync. Рядом второе того же класса: многостраничное чтение штампует «рваный» список ревизией ПОСЛЕДНЕЙ страницы (`queries.ts`), хотя контракт предупреждает, что ревизия движется во время пагинации. Сделать: кадр, не нашедший кэша, обязан оставить след — либо инвалидация ключа, либо отметка ревизии не сдвигается до успешной записи; для пагинации — ревизия списка не выше МИНИМАЛЬНОЙ из страниц либо перечитывание при сдвиге ревизии между страницами. Выбор решения обосновать в журнале ЗАМЕРОМ/тестом, не рассуждением. ### 3. Ф-51 — `resync` сбрасывает один гард из двух На `resync_required` зовётся `invalidateQueries()` без ключа: данные сохраняются, гард `dropStaleReads` продолжает сравнивать новый снапшот со СТАРОЙ отметкой — а по контракту resync выдаётся именно после полной замены, где монотонность ревизии не обещана. Вторым слоем: вызов без ключа бьёт по чужим скоупам (библиотека, usage, run-options, другие книги). Сделать: `resetQueries`/`removeQueries` по скоупу КНИГИ + сброс отметок ревизий этой книги (стриминговая сторона уже сбрасывает — довести симметрию до кэша). Чужие скоупы не трогать. ### 4. Ф-47 — правка контракта: `sense` становится обязательным (РАТИФИЦИРОВАНО) Решение оркестратора (автор контракта), исполняется этой сессией в зонной копии: `BankTerm.sense` — в `required`; пустая строка = «различителя нет». Дифф в канон `docs/architecture/14-api-contract/` переносит оркестратор при лендинге — канон сам не трогать. Сделать: правка `frontend/docs/api-contract/openapi.yaml` (в `required` + описание правила пустого), бамп `info.version` 0.2.1 → 0.2.2, `npm run contract` зелёный, `npm run contract:types` — перегенерить `src/api/schema.ts`, убрать костыли `?? ''` по `sense` (проверить грепом ВСЕ места, не по счёту из строки бэклога — он там завышен), фикстуры мока привести (каждый терм несёт `sense`, хотя бы пустой строкой). ### 5. Ф-40 — общий модуль стилей поля ввода `src/ui/TextField.module.css` и `src/ui/FilterField.module.css` держат байт-одинаковые `.input`, `::placeholder`, `:focus`; отличие — цвет границы в покое. Роли примитивов разные, сливать примитивы нельзя — общий CSS-модуль поля на оба, отличия остаются у ролей. ### 6. Ф-25 — свод языка комментариев (конвенция владельца 04.08) Комментарии S1/S2 в ~40 файлах (`api/index.ts`, `showcase/*`, `ui/*`, `tokens/*`, `scripts/*`) остались русскими; приёмка нашла русские комментарии и в **~14 НОВЫХ файлах S3.6** (например, `src/showcase/About.tsx`) — вопреки конвенции и уточнению владельца по ходу самой S3.6. Перевести на английский ОДНИМ механическим проходом, покрыв ОБА множества. Граница жёсткая: переводятся комментарии, имена тестов и служебные строки; **UI-текст и фикстурная проза остаются русскими — это продукт, а не код.** Смыслы не менять; комментарий, который при переводе оказался враньём против кода, — чинить по коду и назвать в журнале. ### 7. Сторожа на то, что мерилось руками Из таблицы долгов S3.6: зум 125/150/200% и узкая панель замерены руками, сторожа нет. Дёшево и без новых механизмов: в существующие сценарии добавить прогон одной-двух ключевых проверок на втором вьюпорте/зуме (выбор — по замеру, что реально ловит регресс раскладки). Если дёшево не выходит — честная диспозиция в журнале, не молчание. Отдельно: референс fleet_2.png, на котором стоит палитра S3.6 и категория `toned` в `src/tokens/tokens.test.ts`, докинут владельцем в `frontend/references/` при приёмке 10.08 (быструю пиксельную пробу якорей оркестратор сделал — совпало; запись в журнале). Прогнать полный `measure.py` по палитре и вкладкам, записать числа в `FRONTEND_PLAN.md` §5 — замер становится воспроизводимым; ⚠ PIL для `measure.py` не запинен (см. 8.5) — решается тем же пунктом. ### 8. Находки приёмки S3.6 (закрыть здесь же) Приёмочное ревью оркестратора (12 агентов: шесть линз + скептики-верификаторы, author≠reviewer) подтвердило 12 находок. Ниже те, что закрывает эта сессия; каждая грунтована `file:line` — проверяй по коду, не по списку (строки могли съехать). **8.1 (HIGH) Замер ширин таблицы банка молча проваливается в скрытой keep-alive вкладке и не повторяется при показе.** `src/ui/Table.tsx:232` — зависимости эффекта замера `[frame, named, sample, marks, hasRows]` не знают о видимости вкладки; под `content-visibility: hidden` (`src/ui/Tabs.module.css`) геометрия нулевая, `read()` не набирает столбцов, 10 кадров ретрая и молчаливый `return`. Сценарий: открыть книгу, уйти с вкладки «Банк» ДО прихода `/bank`, вернуться — столбцы падают в `1fr` без потолков (ровно «баг 4» владельца, объявленный починенным). Лечение выбрать по замеру (триггер на возврат видимости: `contentvisibilityautostatechange` / ResizeObserver / зависимость от выбранной вкладки); приёмка — playwright-сценарий, воспроизводящий уход-возврат, и падающий на до-фиксном коде тест. **8.2 Вторичный текст и индикаторы теряют контраст на новом синем `--color-selected`.** Выбранная строка дерева — ПОСТОЯННОЕ состояние (открытый раздел всегда выбран), а содержимое не перекрашивается: на #1c4478 лежат текст 12px #9b9b9b (3.52:1 при пороге 4.5), иконка #707070 (1.98:1), дорожка прогресса #262626 (1.55:1) — `src/ui/Tree.module.css:46` красит только фон, `src/showcase/Library.module.css` содержимое не трогает. Починить пары на selected-фоне, не уезжая от референса; axe этого НЕ ловит (non-text не проверяет, выбранную строку со счётчиком не видел) — добавить сторож в сцену. **8.3 Путь из «Замечаний» не загарден: `open` по id, отсутствующему в `chapters`.** `src/showcase/Showcase.tsx:186` + `src/showcase/Notes.tsx:53-54` — строка сводки с определённым, но отсутствующим id рисуется кликабельной; `Tabs` контролируемый (авто-фолбэка react-stately на первую вкладку НЕТ), итог — пустой центр без empty-состояния. Гардить как Library-путь (`positionOf`), сироту помечать и в строке сводки (сейчас `data-orphan` только при `chapter_id === undefined`). **8.4 Съёмка нестабильна — два источника гонки, лечить оба.** (а) `src/showcase/useScreenshotFlag.ts:22` — на первом рендере `useIsFetching() === 0` и экран объявляется `ready` ДО старта первого запроса (нотификация батчится в отдельную задачу); гейт `scripts/shot.mjs` ждёт ровно `dataset.screen`. (б) Поймано исполнением на приёмке: axe стартует во время 220мс-анимации карточки ожидания — один прогон `npm run shot` напечатал «контраст: 4 узла» на `/loading`, два повторных — ноль. Стабилизировать цикл: убрать транзиент `ready`, дожидаться устаканивания анимаций до прогона axe. **8.5 Минорные — одним проходом** (правки на строки, не механизмы): - `src/ui/Table.tsx:257` — кэш канвас-замера не инвалидируется, если шрифт так и не загрузился (отравление ширинами fallback); - `src/showcase/Document.tsx:33` — пин вкладки срабатывает на ЛЮБОЙ `mouseup` при непустом глобальном выделении (правый клик, выделение в соседней панели); - `src/showcase/Showcase.tsx:59` — `awaiting()` на tracked-результатах + `useIsFetching` даёт полный ре-рендер экрана на каждом префетче; чинить ТОЛЬКО с замером до/после, иначе не трогать; - `src/api/vocabulary.ts:112` — `termStatus.label` («предложен»/«черновик»/«подписан») не рендерится ни одним экраном: снять либо применить; продуктовые слова не выдумывать; - `src/tokens/tokens.css:27` — в комментарии `--color-text-muted` число 3.9:1, фактически 3.58:1; `tokens.css:33` — `--color-note` 2.98:1 на панели при пороге 3:1 для non-text (добрать); `tokens.css:68` — комментарий `--row-height-term` ссылается на удалённый двухстрочный список; - `src/showcase/Status.module.css:8` — `.paused`/`.offline` стали no-op (цвет совпал с базой): вернуть отличие либо снять классы; - `src/ui/Button.module.css:36` — подпись disabled-кнопки ТЕКСТОМ `--color-text-muted` против контракта токена «non-text only» (WCAG disabled освобождает — противоречие контракту токена, привести одно к другому); - `scripts/scenes.mjs:170` — сторож обрезки вкладок молча зеленеет, если заголовок вкладки перестанет быть `span`; - `scripts/measure.py:13` — PIL нигде не запинен: манифест окружения для `scripts/` либо честная шапка «чем ставить». **8.6 Доки зоны — привести к коду** (правится СВОЁ; записи журнала S3.6 не переписывать — поправки к ним уже аннотированы приёмочной записью оркестратора в журнале): - `FRONTEND_PLAN.md` §5.6 описывает СНЯТУЮ модель вкладок (вырез в хроме) как текущую — в коде пилюля по fleet.png (возврат разделом 13 записи S3.6); §4 говорит «сценариев пять» — их семь; - `frontend/README.md` — вписать S3.6 (и эту сессию) в карту сессий; - `BACKLOG.md` Ф-47 — «`?? ''` в трёх местах» завышено: вхождение одно (`src/showcase/Bank.tsx:166`); поправить при закрытии строки. ### 9. Ф-35 — строки интерфейса без хардкода: словарь + гейт Сегодня ~90 русских литералов живут прямо в разметке (Ф-35); локаль примитивов уже сведена в одно место (`src/ui/Locale.tsx`, `interfaceLocale`). Сделать интерфейс переводимым МЕХАНИЗМОМ, не переводом: - все строки интерфейса уезжают в КАТАЛОГ сообщений, механизм — ОТРАСЛЕВОЙ i18n-инструмент, не самописная поделка: компонент берёт строку по ключу, локаль = данные, новый язык добавляется файлом перевода без захода в компоненты. Кандидатов сравнить и выбор обосновать в журнале (у react-aria уже есть свой слой `@internationalized/string`; react-intl / lingui — альтернативы); новая зависимость — с точным пином и записью в `STACK_DECISIONS.md` §3, как того требует политика пинов; цену бандла замерить; - **гейт против нового хардкода:** кириллический литерал в JSX/разметке = ошибка линта (исключения — модуль словаря и фикстуры); гейт проверить ЖИВЫМ нарушением, как Ф-4/Ф-9; - хранилище выбора языка подключить к словарю; каркасный пункт настроек «язык интерфейса» уже есть — честное состояние «сейчас интерфейс только русский» остаётся, пока локаль содержимого одна (ru); - границы прежние: фикстурная проза (тексты мок-книг, названия глав, термины банка) — ДАННЫЕ, не интерфейс, в словарь не уезжает; продуктовый список языков — решение владельца (если нужен ответ сейчас — В-строкой); переводы строк на другие языки НЕ выдумывать. Приёмка: гейт ловит подсунутый литерал (показать нарушением); кадры семи маршрутов не изменились — это переезд строк, не перевод. ## Что НЕ делать - Не строить экраны S4–S7 (загрузка книги, механизм подписи, содержимое настроек, экспорт). - Не трогать канон контракта `docs/architecture/14-api-contract/` — только зонную копию (задача 4). - Поля банка из черновика S3.5/S3.6 (`note`, `gender`, `contexts`…) НЕ заводить ни в спеку, ни в фикстуры — черновик ждёт ратификации (граница ПТ-33 — на владельце). - Продуктовые слова не выдумывать: В-3 (ступень замечания), В-4 (глава без заголовка), В-6 (фраза паузы), В-8 (слово состояния в дереве) — на владельце. - Тесты и гейты не подгонять под зелень (D39.121): красный гейт — вопрос оркестратору через журнал, не правка гейта. - Никакого `git add`/`git commit` — дерево передаётся на лендинг как есть. - Ф-48: если по ходу покажется, что пора вернуть ресайз столбцов — НЕ возвращать; строка требует свежего замера от новой базы, это не задача пака. ## Отчёт и приёмка - Каждая задача 1–3: ТЕСТ, падающий на до-фиксном коде (vitest поверх MSW; поток мокается событиями), и зелёный после; в журнал — что именно ловит тест и как воспроизводился дефект. - `npm run check` и `npm run check:full` зелёные на сдаче; счётчик тестов вырос, ни одна существующая проверка не ослаблена. - **Мандат самопроверки:** ревью ИСПОЛНЕНИЕМ своего кода, своих кадров и своих сценариев + адверсариальное ревью диффа (author≠reviewer) в конце; находки с диспозициями — в журнал. - Прогон по всем задачам таблицей в `frontend-PROGRESS.md`: задача → что сделано → тест/замер → либо диспозиция. Хвосты — строками Ф-N в `BACKLOG.md` (Ф-49/50/51/40/47 закрыть или переформулировать честно; Ф-35 и Ф-25 закрываются в своей код-половине, продуктовая половина Ф-35 «какие языки» остаётся на владельце), вопросы владельцу — В-строками. - Промт S4 не писать — его выдаёт оркестратор после приёмки этого пака.