textmachine/frontend/docs/BACKLOG.md

69 lines
73 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# Бэклог фронта (зонный)
> Ведёт зона `frontend/` (решение владельца 02.08, D39.84: фронт и платформа держат СВОИ бэклоги; единый бэклог `docs/PROGRESS.md` — трекер движка/полигона/доков, фронт-строк не принимает). Нормы те же: ID стабилен навсегда, каждая петля получает диспозицию. Запросы к ДВИЖКУ сюда не пишутся — они заходят строками единого бэклога через оркестратора (пример: строки 99102 — пофазный прогресс · манифест глав · машиночитаемая таблица подписи · трасса; все четыре ЗАКРЫТЫ, тела в D39.122/D39.131, а сама таблица подписи с тех пор снесена D39.158). **Развод «предложить/ратифицировать» (D39.99 п.4, после инцидента остановки S3): ПРЕДЛАГАТЬ артефакты для чужих решений — черновики контрактов, входы дизайна — фронт не только может, но обязан; не может он их РАТИФИЦИРОВАТЬ. «Не заводит своим решением» значит «финальное слово не его», а не «не смей писать черновик».** Засеян оркестратором при лендинге D39.84 — дальше правят фронт-сессии.
| ID | Хвост | Вес | Источник |
|---|---|---|---|
| Ф-1 | **Этапы S2S7** (`FRONTEND_SESSION_PROMPT.md`): S2 оболочка + слой `src/ui/` · S3 слой данных/MSW/фикстуры всех состояний · S4 библиотека/загрузка/разбор/прогресс · S5 банк памяти и подпись (самый тяжёлый) · S6 читалка двух колонок и замечания · S7 метаданные/экспорт/настройки/сквозной прогон | S0S4 пройдены и приняты (D39.135); S5 — после разморозки зоны (D39.147) | 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`) закрыты тоже. Механизм — `stylelint.config.js`; проверено шестью живыми нарушениями и тремя легальными формами (перечень форм — `FRONTEND_PLAN.md` §5.4) | закрыт | STACK_DECISIONS §3 |
| ~~Ф-8~~ | ~~Вопрос владельцу: закрывать ли `/* eslint-disable */` в TSX~~**закрыт 02.08 без решения владельца.** Вопрос был поставлен как выбор из двух плохих вариантов, а у него есть отраслевой третий: `@eslint-community/eslint-plugin-eslint-comments` (`no-unlimited-disable` + `require-description` + `disable-enable-pair`) — голое отключение падает тремя ошибками, точечное подавление с причиной проходит. **Урок:** не выносить владельцу технический вопрос, не поискав отраслевую практику | закрыт | адверсариальное ревью S1 |
| ~~Ф-9~~ | ~~**Хвосты гейтов после адверсариального ревью S1**~~**все четыре закрыты 04.08 в S2**, каждая живым нарушением: цветные атрибуты TSX судятся разрешающим списком; значение CSS-переменной в `style={{…}}` обязано быть числом или токеном; носитель `style` внутри объектного литерала запрещён, и этим закрыт JSX-спред; системные цветовые ключевые слова закрыты по ВСЕМ свойствам, то есть и в сокращённых `outline`/`box-shadow`/`border`. Механизм — `eslint.config.js` и `stylelint.config.js`, перечень проверенных форм (15 TSX + 14 CSS) — `FRONTEND_PLAN.md` §5.4. **Граница названа честно:** гейт ловит литерал и шаблон, но НЕ вычисленное значение (`const c = 'red'; <svg fill={c}/>`) — синтаксически это не берётся, и объявлять строку «закрытой без остатка» было бы тем самым переобещанием, из-за которого она заведена | закрыт для литеральных форм, граница названа | адверсариальное ревью 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) стоял под текстом, несущим смысл. Весь такой текст переведён на `--color-text-secondary` (5.36:1 до перекраски, 5.6:1 после), а `--color-text-muted` остался только под НЕтекстом — иконками и дорожками индикаторов, к которым порог для текста не предъявляется. Верности референсу это не стоило ничего: приглушённая ступень как ступень осталась | закрыт | ревью 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` | ⚠ **Группировка ЗАКРЫТА 14.08 в S4:** дерево свыше 200 разделов складывается по СОТНЯМ (`Library.tsx`, `groupSize`/`groupFrom`), 2284 раздела дают 23 строки. Число не из вкуса: главу адресуют порядковым номером, и сотня — шаг, которым их считает человек; замер говорит то же с другой стороны — плоское дерево рисуется дёшево, а полоса прокрутки на 2284 строках даёт треть пикселя на строку. Ниже порога дерево остаётся ПЛОСКИМ; группа с выбранным разделом раскрывается сама и остаётся закрываемой рукой (`ui/Tree.tsx`). ⚠ Хвост «разделение банка подписано/ждёт решения» СНЯТ: счётчиков подписи нет с 10.08 (Ф-44 · D39.144), а различитель канона и предложенного — Ф-46 | ревью S1 №2 → S4 14.08 |
| Ф-14 | **Вход фронта в контракт API (единый бэклог, строка 95) — ИСПОЛНЕН.** Перечень входа и черновик контракта написаны S3 04.08 ([`API_CONTRACT_INPUT.md`](API_CONTRACT_INPUT.md), [`API_CONTRACT_DRAFT.md`](API_CONTRACT_DRAFT.md) — оба сегодня под ⚠-баннерами «исполнено, канон живёт не здесь»), контракт РАТИФИЦИРОВАН оркестратором (D39.99) и с тех пор ушёл на **0.9.0**. Где что живёт сегодня: норматив — `docs/architecture/14-api-contract/openapi.yaml`; провенанс ✓/◆/○, обоснования и К-вопросы — компаньон `README.md` рядом с ним; ход сборки, оба пасса ревью и дофиксы — [archive/frontend-PROGRESS-S0-S4.md](archive/frontend-PROGRESS-S0-S4.md) (записи 04.08 и 08.08). **Урок, который строка держит живым:** класс ошибки «фикстура выдумала форму» ловился в зоне ЧЕТЫРЕЖДЫ — форму берут из канона контракта, а не из движка и не из головы | закрыт (D39.99) | ревью S1 №3 · сборка S3 04.08 |
| ~~Ф-15~~ | ~~**`src/api/` синхронен, состояний загрузки и ошибки в типах нет**~~**ЗАКРЫТА 08.08 в S3.** `src/api/` асинхронен и разговаривает HTTP: MSW стоит СЕТЕВЫМ моком (а не подменённым модулем), поэтому приложение делает настоящий `fetch` и настоящий `EventSource`, и ветки ожидания/ошибки/пустоты — код, работающий в бою, а не второй его экземпляр. `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, и механизмом:** проп `keepAlive` у примитива `ui/Tabs` — невыбранные `TabPanel` монтируются (`shouldForceMount`), стоят в одной ячейке грида и гасятся `visibility: hidden`. Именно `visibility`, а не `display: none`: последний уничтожает бокс и вместе с ним офсет прокрутки, то есть лечил бы половину болезни. Библиотека сама вешает на невыбранную панель `inert`, поэтому из дерева доступности и порядка табуляции она вынута, а axe на дублях содержимого не срабатывает (проверено на семи маршрутах). **Цена, которую пришлось назвать:** при keep-alive все открытые вкладки живут одновременно, поэтому общий запрос глав отдал бы каждой текст АКТИВНОЙ главы — запрос переехал в `showcase/Document.tsx`, по запросу на открытую вкладку. Приёмка — сцена `npm run scenes scroll`. **Хвост, живой и сегодня:** скрытые панели остаются в 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). Тем же ходом: незнакомая ступень замечания берёт ветку ВНИМАНИЯ — прятать важное ось важности не вправе | закрыт | адверсариальное ревью S2 → S3 08.08 |
| Ф-21 | **Ступень замечания различается только цветом полоски** (`attention` синяя, `glance` серая). Зрячему разница читается ещё и светлотой, но скринридеру приоритет не достаётся вовсе. Лечение — слово, как это уже сделано для состояния прогона книги. ⚠ **Диспозиция сменилась 30.08: подписи владельца НЕ ждём** — фразы делегированы проекту (D39.176 п.4), и требование к ним инженерное: слово не литерал в TSX, а ДАННЫЕ по машинному коду причины и локали, словарь один на продукт. Границы выбора: словарь замечаний продуктовый (ПТ-33), а §3.8 требует, чтобы экран не выглядел тревожным, так что «важно/неважно» не годится | зона, по словарю фраз (D39.176 п.4) | адверсариальное ревью S2 → пинг №20 п.1 |
| Ф-18 | **Бандл едет одним куском, и кусок растёт.** Замеры главного чанка: 545 КБ (04.08, S2) → 724 130 байт (S3.7) → **800 219 байт, 243 446 gzip (S4 14.08, `npx vite build` + `gzip -c`)**. Прирост пака S4 — +76 089 байт: четыре примитива формы (`Select` тянет слой поповера, `Slider` — свой пакет react-aria), экраны интейка и запуска, мир фикстуры и текст каталога. Для десктопного SPA за логином это не авария (индексации нет, сеть локальная), но одним куском едут и экраны, которых пользователь ещё не открыл — включая мок-сеть (Ф-29), которая в бою не нужна вовсе. Лечение прежнее — разбиение по маршрутам после S7, когда экраны появятся; ⚠ добавилось второе, дешёвое и раннее: выкинуть `src/mock/` из продакшн-сборки в день живой платформы (это Ф-29, и она теперь ещё и про вес) | после S7 либо при жалобе на первый экран | S2, замер; пере-замерено S3.7 и S4 |
| Ф-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/` смешанный по языку комментариев.**~~**ЗАКРЫТА 10.08 в S3.7, и шире, чем строка просила.** Правило владельца ужесточено до «в КОДЕ никакого русского»: комментарии, имена тестов, сообщения ошибок, логи скриптов и служебные строки сценариев — английские, по ВСЕМУ дереву зоны (54 файла, включая корневые конфиги), а не по списку тронутых сессией. Держит это не дисциплина, а гейт: `src/i18n/catalogue.test.ts` подметает кириллицу построчно по ts/tsx/css/mjs/py (идентификаторы вида Ф-49 исключены явно — это имена строк бэклога, а не проза). **Граница машинная:** русский легален ровно в двух местах, и оба — данные (каталог `src/i18n/ru.ts` и фикстуры `src/mock/`) | закрыт | слово владельца 04.08 → S3.7 10.08 |
| Ф-26 | **Загрузка книги ПОСТРОЕНА (S4), выгрузка ждёт S7.** `POST /books` живёт в `src/api/upload.ts` — XHR, а не `fetch`, потому что стандарт Fetch не отдаёт прогресс ТЕЛА запроса вовсе (поток в запросе — только Chromium и только HTTP/2), а книга это минуты тишины. Порядок частей формы — правило провода (PD-172, `file` последним) и держится тестом; отказы 400/404/408/413 — состояния формы. Экспорт (`POST/GET /books/{id}/exports`) не строился намеренно: цикл опроса по `Retry-After` — часть экрана S7, а механизм без задачи проектный норматив прямо запрещает; контракт обе поверхности объявляет, типы сгенерены, недостающего кода ровно на десяток строк | S7 (экспорт) | S3 08.08 → S4 14.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 | **Метка главы без заголовка — голый порядковый номер.****Прежнее основание строки СНЯТО каноном 0.3.0:** запрет синтезировать «Глава {n}» на клиенте отменён — когда у главы нет ни `heading`, ни `number`, клиент ОБЯЗАН сам нарисовать порядковый в локали интерфейса (строка учила обратному по К-3). Сегодня в дереве на таком месте стоит голая позиция в порядке чтения — строка «12» среди названий; правка экрана — при разморозке. НАСТОЯЩИЕ названия глав — движковая половина (строка 160 единого бэклога), а продуктовое слово о том, чем подписывать 2284 одинаковых «Глава N», — по-прежнему В-4 на владельце | разморозка + владелец (В-4) | S3 08.08, кадр `/scale` → канон 0.3.0 |
| ~~Ф-31~~ | ~~**К-13: достижение СОБСТВЕННОГО потолка прогона и исчерпание кредита неразличимы** — `paused_reason` имел ровно одно значение, `credit_exhausted`, и оно про кредит~~**ЗАКРЫТА D39.132 п.2а** (`null` на проводе ратифицирован), а кадр `EventCeiling`, на который строка опиралась, снят каноном 0.3.0 целиком. Разведение живёт сегодня в самом каноне: `resume` отвечает разными `cause.code``ceiling_reached` («прогон закончен, нужен новый») против `credit_unavailable` («пополнить счёт») | закрыт | S3 08.08 → D39.132 |
| Ф-32 | **Замечания читаются пер-КНИЖНО, а показываются пер-главно.** Контракт даёт `GET /books/{id}/notes` и не даёт пер-главной ручки; панель контекста фильтровала по открытой главе на клиенте — **с S3.5 фильтр снят, сводка пер-книжная (замечание владельца 8, строка Ф-38), то есть пер-книжное чтение теперь и показывается пер-книжно**. Пока это дёшево — дефолт страницы 500, а на книге с сотнями замечаний клиент тянет весь список ради трёх строк, и с курсором дотянет ВЕСЬ. Ф-12 обосновывал отказ от виртуализации замечаний тем, что список пер-главный и мал; после этой правки он пер-главный на ЭКРАНЕ, но пер-книжный в ПАМЯТИ. Решать не фронту: либо пер-главная ручка (вопрос автору контракта), либо явный потолок с честным «показаны не все». | 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` держит и дерево, и банк на настоящем масштабе (числа — Ф-12), то есть вторая библиотека виртуализации была бы платой без выигрыша; пересматривать — если появится список с переменной высотой строки (читалка S6) и `estimatedRowSize` не справится | по замеру; замер 04.08 отрицательный | STACK_DECISIONS §2 |
| Ф-33 | **Поиска по ТЕКСТУ книги в контракте нет вовсе.** Замечание владельца 4 («две кнопки поиска, делают одно и то же») разведено так: лупа в правом верхнем углу стала настоящей палитрой быстрого перехода к разделу или термину, а вкладка-заглушка «Поиск» из левой панели СНЯТА. Снята потому, что построить её нечем: у контракта ни одной поисковой ручки (сверено по канону 0.9.0 02.09: 20 операций, `GET /books/{id}/…` даёт списки, не запросы; греп `operationId:` по `docs/architecture/14-api-contract/openapi.yaml` — ни одной поисковой), то есть вкладка была обещанием без адресата. Вопрос не фронта: полнотекстовый поиск по книге — это либо ручка платформы, либо индекс на клиенте поверх выгрузки, и то и другое решает автор контракта. До ответа поиск по книге живёт как хвост читалки | S6 либо ручка контракта | S3.5, замечание 4 |
| Ф-34 | **Светлая тема: цена честно замерена, дёшево не выходит.** Проверено по коду, а не на глаз. Дёшево — только половина: значения цвета лежат в одном месте (`tokens.css`), второй набор под `:root[data-theme='light']` механичен. Дорого — вторая половина: подпись оболочки, снятая с Fleet, это «карточки на почти чёрном фоне», и она не инвертируется значениями — в светлой теме отношения поверхностей приходится ПЕРЕПРИДУМАТЬ (что светлее, панель или геп; чем отделять карточку без теней; что делать с вырезом активной вкладки, который держится на паре «хром ↔ полотно»). Плюс переключатель и его персист, плюс пере-прогон axe по семи маршрутам на второй теме, плюс замок токенов на второй набор. Оценка: отдельный проход масштаба S7, а не хвост фикс-пака. Пункт назван в каркасе настроек честно: «сейчас доступна только тёмная» | S7 либо отдельный пакет | S3.5, замечание 15 |
| Ф-35 | **Язык интерфейса: код-половина ЗАКРЫТА 10.08 в S3.7, продуктовая — на владельце.** Сделано: словарь строк `src/i18n/ru.ts` — единственный дом слов интерфейса; механизм отраслевой, не самописка — `@internationalized/string` 3.2.10 (i18n-слой Adobe, на котором react-aria отдаёт свои строки; сравнение с react-intl и lingui — `STACK_DECISIONS.md` §1); ключ типизирован ИЗ каталога, поэтому опечатка роняет `tsc`; выбор языка — стор `src/i18n/language.ts` с персистом, от него же берут локаль `I18nProvider` и весь `Intl` (`showcase/format.ts`); второй язык = второй файл того же вида плюс строка в `language.ts`, ни один компонент не трогается. Гейт против нового хардкода стоит и проверен живым нарушением ДВАЖДЫ: ESLint запрещает текст в разметке по МЕСТУ (ловит и `title="Настройки"`, и `title="Settings"` — алфавит ни при чём), `catalogue.test.ts` держит «код зоны английский» по комментариям, CSS, python и корневым конфигам. ⚠ **Числа — ЗАМЕР 14.08 (S4), арбитраж двух носителей:** до пака каталог **157** ключей (прогон `node -e` по живому `ru.ts`), главный чанк **724130 байт** (`npx vite build`); ходившие рядом «147» и «723102» ошибочны. После пака S4: каталог **202 ключа**, главный чанк **800 219 байт**, 243 446 gzip (+76 089 к до-пакному — новые примитивы формы, интейк, фикстура мира и сам текст). **Осталось владельцу:** какие языки продукт обещает, и переводы на них — их фронт не выдумывает | решение владельца: список языков | S3.5 → S3.7 10.08 → числа сверены S4 14.08 |
| Ф-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`), и глава, уже прошедшая обе волны в незавершённой книге, получает ту же пометку, что и непереведённая. ⚠ **Открытый вопрос К-10, на который строка ждала ответа, ОТВЕЧЕН «НЕ строить» (D39.138):** фазы уходят с провода, и у главы тоже — пометка черновика остаётся КНИЖНОЙ. Хвост строки пере-диспозиционировать при разморозке: безопасная сторона (предупредить лишний раз) осталась взятой намеренно | пере-диспозиция при разморозке | S3.5, замечание 24 → D39.138 |
| Ф-38 | **Вкладка «Замечания» защищена сводкой по книге — слово за владельцем.** Замечание 8 («непонятно зачем, всё рисуется в тексте») по пер-ГЛАВНОМУ списку было верным: он дублировал читалку. Вкладка переделана в пер-КНИЖНУЮ сводку со ссылкой в раздел — сценарий «пройтись по всем местам, требующим внимания», который без неё не живёт и который не стоит ни одного лишнего запроса (контракт даёт замечания пер-книжно, Ф-32). Если владелец сценария не признает — вкладка снимается одной строкой в `Context.tsx`, вместе с ней уходит и `notesQuery` с экрана | решение владельца | S3.5, замечание 8 |
| ~~Ф-39~~ | ~~**Банку не хватает полей для проверки термина, и выдумывать их фронт не стал**~~ (замечания 7/13 просили род/пол, описание-обоснование, варианты перевода; в `BankTerm` их нет) — **ЗАКРЫТА словом владельца 15.08 (D39.136 п.4б): полей `BankTerm` не заводить, `sense` хватает**; его калибровка — строка 178 единого бэклога. Показано было всё, что даёт контракт, а недостающее оформлено черновиком правки спеки (`archive/frontend-PROGRESS-S0-S4.md`, запись S3.5 09.08, §5) и НЕ заведено ни в фикстуру, ни в типы — класс ошибки «фикстура выдумала форму» (Ф-14) | закрыт | S3.5, замечания 7 и 13 → D39.136 |
| ~~Ф-40~~ | ~~**Вид поля ввода описан в двух модулях.**~~**ЗАКРЫТА 10.08 в S3.7.** Общий модуль `src/ui/Field.module.css` несёт `.input`, `::placeholder`, `:focus` и модификатор `.framed` (граница в покое — отличие формы от фильтра); примитивы остались РАЗНЫМИ, у каждого свой модуль со своей ролью (`TextField` — подпись и подсказка, `FilterField` — раскладка фильтра). Обе половины лежат в ОДНОМ модуле намеренно: порядок между двумя CSS-модулями — это порядок их импортов, а модификатор обязан идти после базы | закрыт | S3.5 → S3.7 10.08 |
| Ф-41 | **Спойлер банка раскрывается только наведением.** Клавиатура и тач его не раскроют, а перевод остаётся в DOM (скринридер прочитает). Кнопка раскрытия внутри строки списка невозможна: у роли `option` дети презентационные, и вложенный интерактив роняет гейт axe (та же ловушка, что Ф-17). Настоящее лечение — раскрытие на уровне СПИСКА (один переключатель «показывать термины из будущих разделов» над ним) либо экран подписи S5, где строка перестаёт быть строкой списка. Сегодняшнее устройство: спойлер стоит на описании (`sense`), а не на переводе — закрытый перевод делал строку непроверяемой, а таблица существует ради проверки; окно применимости и состояние — разные столбцы | S5 либо раньше по слову владельца | S3.5, ревью точности |
| Ф-42 | **У keep-alive нет потолка.** Вкладки центра не размонтируются (в этом и лекарство Ф-19), поэтому каждая закреплённая глава держит смонтированную читалку и свой запрос: замерено адверсариальным ревью — семь вкладок дают семь запросов и семь читалок. На фикстурных главах это ничего не стоит, на настоящих (глава бывает одним блоком в тысячи знаков) — стоит. Лечение из отраслевой практики: вытеснять самые давние закреплённые вкладки сверх N смонтированных, оставляя их в ряду | ⚠ **ЗАМЕРЕНО 14.08 в S4, потолок НЕ ставится.** Прогон в браузере с принудительной сборкой мусора (`page.requestGC()`), `/scale`, 12 закреплённых вкладок: куча 34,4 → 36,7 МБ (**+2,3 МБ, ≈190 КБ на вкладку**), узлов в DOM 1351 → 1571, чтений юнитов 2 → 11. Первый замер без GC давал +62 МБ и был мусором аллокаций — назван, чтобы никто не повторил. Значит память не авария: на настоящей книге (юнит в сотни раз длиннее фикстурного) это сотни килобайт на вкладку. Настоящая цена — СЕТЕВАЯ: каждая открытая вкладка перечитывается на границе стадии (`useRunStream`, кадр `status`), то есть 20 вкладок это 20 чтений на кадр. Потолок ставить не стал: он меняет поведение модели вкладок VS Code (закреплённая вкладка молча теряет содержимое — новый класс сюрприза), а порог всё равно называть по замеру на НАСТОЯЩЕЙ книге, которого у фикстуры нет. Пересмотр — при первой жалобе на память или когда прогон пойдёт на живой платформе | замерено, отложено с числами | S3.5, ревью корректности → S4 14.08 |
| Ф-43 | ⚠ **Строка ПРОТУХЛА ОБЕИМИ половинами (сверено деревом 30.08, пинг №20 п.2).** Было: «у чтения банка нет канала, витрина стоит на моке, подписной экран S5 строить не на чем». Стало: движковый сайдкар банка ПОСТРОЕН (`backend/internal/pipeline/bankexport.go`), платформа его декодирует и отдаёт проекцией, у контракта есть `GET /books/{bookId}/bank`. Значит основание «S5 непостроим» и зависимости Ф-44/Ф-46 «после Ф-43» больше не держатся. Осталось: перевести витрину банка с фикстуры на живое чтение при разморозке | разморозка: снос мока банка | S3.6 10.08 → пинг №20 п.2 |
| Ф-44 | **Действие «подписать банк» не нарисовано — намеренно.** Модель ратифицирована D39.144 (слово владельца 16.08): подписывается ВЕСЬ банк ОДНИМ ОК (`POST /runs/{runId}/resume`), нерешённые термины едут как предложил сервис. ⚠ Гейт полноты СНЯТ: `awaiting_bank` с ЛЮБЫМ состоянием решений отвечает `202`, `pending_decisions`/`complete` информационные, пер-термная масс-подпись НЕ строится — движковый флаг стопа прямо об этом говорит (`pipeline/mining.go`, коммент «THE FLAG», D39.42 п.5 · D39.144). Счётчики «подписано N / ждут решения N» сняты с экрана словом владельца 10.08 — они и были путаницей «подписываем банк или термины»; в подвале остались размер банка, «показано N из M» при сужении и состояние ВСЕГО банка из статуса книги. Экран действия — при разморозке | S5 | S3.6 10.08 · пере-читано по D39.144 |
| Ф-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` не обязателен, хотя объявлен частью ключа уникальности.**~~**ЗАКРЫТА 10.08 в S3.7 по ратификации оркестратора:** в зонной копии `openapi.yaml` поле в `required`, правило пустой строки («различителя нет») записано в описании, версия 0.2.1 → 0.2.2, spectral зелёный, типы перегенерены (счёт прежней редакции строки был завышен: `?? ''` стояло в одном месте, а не в трёх) | закрыт | S3.6 10.08 → S3.7 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~~ | ~~**Текст открытой главы не подписан ни на один сигнал изменения.**~~**ЗАКРЫТА 10.08 в S3.7.** Кадр `chapter` инвалидирует чтение юнитов ИМЕННО названной главы (`keys.units`), кадр `status` — юниты всей книги (`keys.bookUnits`): границы стадий это единственный сигнал, пока `units_done` остаётся единым счётчиком. Тест — `showcase/useRunStream.test.tsx` | закрыт | S3.6 10.08 → S3.7 |
| ~~Ф-50~~ | ~~**Кадр потока, пришедший до появления чтения в кэше, теряется молча.**~~**ЗАКРЫТА 10.08 в S3.7, обе половины.** (1) Кадр, не нашедший снимка, зовёт `resetQueries` по своему ключу — именно reset, и это ЗАМЕР, а не вкус: `invalidateQueries` на запросе без данных не делает ничего (`Query.fetch`: `state.data !== undefined && cancelRefetch`) и возвращает промис того самого устаревшего чтения. (2) Ревизия многостраничного списка — МИНИМУМ из страниц (`client.ts`): рваный список свеж настолько, насколько стара его первая страница | закрыт | S3.6 10.08 → S3.7 |
| ~~Ф-51~~ | ~~**`resync` сбрасывает один гард из двух.**~~**ЗАКРЫТА 10.08 в S3.7.** На `resync_required` зовётся `resetQueries` по скоупу КНИГИ (`bookScope` — предикат по второму элементу ключа: id книги стоит там у каждого книжного чтения и больше нигде), то есть отметка ревизии уходит вместе со снимком, а библиотека, usage и другие книги не трогаются. Гарантии закрыты тестами: «новая эпоха с НИЗШИМ номером принимается» и «resync одной книги не трогает чужие скоупы» | закрыт | S3.6 10.08 → S3.7 |
| ~~Ф-52~~ | ~~**`--color-danger` не дотягивает до порога нетекста**~~**ЗАКРЫТА 14.08 в S4 по слову владельца, и не одним числом, а ПРАВИЛОМ.** Тон поднят минимальным сдвигом при сохранённых тоне и насыщенности (350°, S 0.60): `#b82e45`**`#bd2f47`**, на полотне панели `#181818` это **3.10:1** против прежних 2.97 (порог нетекста 3:1; для сравнения `--color-note` стоит на 3.07). Токен переехал из «замеренных» в «подобранные по референсу» с числом референса в комментарии. Сверх задания поставлен гейт: `tokens.test.ts` считает контраст КАЖДОГО тона состояния (`note`/`ok`/`warn`/`danger`) о полотно и требует 3:1 — axe этого не ловит вовсе (контраст нетекста он не проверяет), и обе просадки нашлись руками, с разницей в пакет. Проверено живым нарушением. ⚠ **Порог НЕтекста — потолок этих тонов:** тона состояния не проходят порог для ТЕКСТА, поэтому цвет живёт на точке, а смысл несёт слово (замер 02.08: попытка красить сам текст дала axe 5 → 14 узлов, откат вернул 5) | закрыт | S3.7 10.08, замер → S4 14.08 |
| Ф-53 | **Находка приёмки 8.1 (HIGH) НЕ ВОСПРОИЗВОДИТСЯ — оставлен сторож, а не механизм.** Утверждение «под `content-visibility: hidden` геометрия нулевая, замер ширин таблицы молча проваливается» проверено прямым замером в браузере: таблица, смонтированная в скрытой keep-alive вкладке `/scale`, держит 116 ячеек и ширины `121·122·133·232` — БАЙТ В БАЙТ те же, что у вкладки, с которой не уходили, и на до-фиксном коде тоже. Причина: запрос геометрии внутри пропущенного поддерева заставляет браузер посчитать раскладку, поэтому `read()` набирает столбцы. Добавленный было под находку `IntersectionObserver` снят — чинить нечего. Сценарий `npm run scenes hidden` (уход с вкладки ДО прихода банка и возврат) оставлен: сам путь реален и не был покрыт ничем. Если владелец увидит дефект руками — здесь лежит и замер, и готовый сценарий, куда его повторить | закрыто замером; пересмотр — по факту от владельца | S3.7 10.08, замер в браузере |
| Ф-54 | **При зуме от 125% центральная панель обрезает СВОЁ содержимое, а не сжимается.** Замер сцены `zoom` (вьюпорт в CSS-пикселях = зум браузера): при 1152×720 центр режет 55px своего содержимого, при 960×600 — 98px, при 720×450 — 170px; окно при этом не переливается, три панели стоят в ряд, боковые честно сжимаются (левая доходит до 141px при своём минимуме 220). То есть карточка центра держит ширину, которой у слота уже нет, и первой уходит вторая колонка читалки — перевод. Кадр `--size 720x450 --dpr 1` это показывает глазом. Не чинил и не считаю это своим решением: ширина центра — часть вопроса «плотность интерфейса» (Ф-36, В-7 на владельце), а менять раскладку сверх задания пака — самодеятельность. Сцена ЧИСЛО печатает, но не утверждает: гейт, красный по построению, учит пропускать команду | владелец (В-7) + Ф-36 | S3.7 10.08, замер сцены `zoom` |
| Ф-55 | **Стор языка есть, переключателя нет — и это не забывчивость.** `src/i18n/language.ts` держит выбор с персистом и отдаёт локаль каталогу, `I18nProvider` и `Intl`; `choose()` при этом не зовёт ни один экран, потому что содержимое настроек строит S7, а промт пака прямо запрещает его трогать. Пункт «Язык интерфейса» в каркасе настроек честно говорит «сейчас интерфейс только русский». Строка держит хвост, чтобы S7 не завёл ВТОРОЕ хранилище выбора рядом с этим | S7 | S3.7 10.08 |
| Ф-56 | **Конца интейка никто не объявляет — клиент опрашивает.** В контракте 0.2.3 живой канал принадлежал ПРОГОНУ (`GET /runs/{runId}/events`), а книга в `uploading`/`parsing` прогона не имеет вовсе: «разбор закончился» не приходило ничем. S4 закрыла дыру опросом (`src/api/queries.ts`, `intakePollMs = 3000`, только пока книга в интейке) плюс перечитыванием дерева на выходе (`useIntakeEnd.ts`). Цена опроса названа: чтение библиотеки раз в три секунды на всё время разбора — минуты на настоящей книге. ⚠ **ОТВЕТ ПРИШЁЛ (Б-6а, канон 0.3.0+):** поток перевешен на КНИГУ — `GET /books/{bookId}/events` с кадрами конца разбора и конца прогона и `204` на завершённом. Опрос снимается при разморозке | разморозка: снять опрос, перейти на книжный поток | S4 14.08 → канон |
| Ф-57 | **Какие пары языков продукт принимает — форма загрузки предлагает свой список.** `src/showcase/languages.ts` держит `zh·ja·en → ru` как ДАННЫЕ клиента, имена считает `Intl.DisplayNames`. Это честная слабость интейка: что деплой умеет переводить — свойство деплоя (движок читает пару из langpack), и список на клиенте соврёт в тот день, когда деплой поедет с другим набором. ⚠ **ОТВЕТ ПРИШЁЛ (Б-2, канон 0.3.0+):** `GET /capabilities` отдаёт пары со статусом, порог интейка, форматы, размеры страниц и версию контракта — `languages.ts` при разморозке становится ЧТЕНИЕМ, а не константой | разморозка: `languages.ts` на `/capabilities` | S4 14.08 → канон |
| Ф-58 | **RTL не проверен ни разу, а триггер наступил.** Условие, записанное S1 («когда появится `I18nProvider`»), исполнилось в S3.7: провайдер стоит, локаль берётся из стора, `<html lang>` следует выбору. Чего нет — ни одного прогона с языком письма справа налево: примитивы react-aria логическое направление поддерживают, а НАША раскладка (фиксированное место бейджа, обрезка вкладок, инсеты панелей, стрелка `zh → ru` в статус-полосе) написана в физических терминах местами. Цена вопроса неизвестна до замера; сам замер дешёвый — второй каталог-заглушка плюс `dir="rtl"` и прогон сцен. Делать, когда владелец назовёт второй язык (В-9): до того это проверка обещания, которого продукт не давал | владелец (В-9) → отдельный проход | аудит-свип 14.08 → S4 |
| ~~Ф-59~~ | ~~**Спойлер банка достаётся поиском по странице — ЗАМЕРЕНО.**~~**ЗАКРЫТА решением владельца 16.08 (D39.138 п.2к): спойлер это ВЕЖЛИВОСТЬ, сделанного достаточно, цена названа.** Сам замер (S4, браузер, `/showcase`, вкладка «Банк»): размытие глифов (`filter: blur`) прячет описание от глаза, но текст остаётся текстом страницы — закрытое описание лежит в `document.body.innerText`, `window.find()` его находит, то есть Ctrl+F подсвечивает закрытую ячейку; скринридер отрезан честно (`aria-hidden`), выделение мышью пусто, под `@media print` размытие СОХРАНЯЕТСЯ (`blur(3px)`), но текстовый слой PDF строится из DOM, и там строка остаётся | закрыт | замер S4 14.08 → D39.138 |
| Ф-60 | **Канвас-замер ширин не знает языка строки.** Ширины столбцов банка меряются канвасом одним шрифтом на весь набор (`ui/Table.tsx`), а при ДВУХ CJK-лицах в стеке один и тот же код-пойнт рисуется по-разному: японская и китайская гарнитуры дают разную метрику для общего иероглифа. Сегодня это спит: пара одна (`zh→ru`), лицо одно. Сработает в день, когда в наборе окажутся и `zh`, и `ja` — то есть ровно на второй паре, которую движок уже умеет. Лечение: мерить с тем же `lang`, что у ячейки (канвасу `lang` не передашь — придётся мерить в DOM либо держать по замерщику на язык), и это отдельный проход с пере-замером цены | триггер: вторая пара в одной библиотеке | аудит-свип 14.08 → S4 |
| Ф-61 | **Платформа отвечает по-английски, а интерфейс русский.** Контракт 0.2.3 клал фразу отказа на СЕРВЕР (`Problem.title`/`detail` — «product language», клиент показывает как пришло), и живая платформа слала `"The file is larger than this service accepts"` рядом с русским советом — два языка на одном экране. ⚠ **РЕШЕНО владельцем, вариант B (D39.138 §8 п.4; требование интернациональности подтверждено D39.176 п.4):** на проводе машинный `code` + `request_id`, `title`/`detail` developer-facing, а ФРАЗУ рисует клиент по словарю кодов и локали — таблица `AddBook.tsx` (греп по предмету) становится нормой, а фолбэки `AddBook.tsx` (греп по предмету) (`problem?.detail ?? problem?.title`), `Loaded.tsx` (греп по предмету) и `RunStart.tsx` (греп по предмету) при разморозке СНИМАЮТСЯ. Карта «код → фраза» — приложение А компаньона контракта | разморозка: словарь кодов вместо серверной фразы | S4 14.08 → D39.138 |
| Ф-62 | **Переименовать книгу нечем.** В 0.2.3 `POST /books` клал название один раз (рукой или из имени файла), дальше только чтения. Найдено адверсариальным ревью S4: подсказка формы обещала «название можно будет поправить позже» — обещание, которого контракт не давал; фраза исправлена, обещание снято. ⚠ **ОТВЕТ ПРИШЁЛ (Б-5, канон 0.3.0+):** `PATCH /books/{bookId}` ТОЛЬКО с `title` (409 при живом прогоне) и `DELETE`; правка — отображение, до движкового брифа не доезжает. Плюс слово владельца 16.08 (Б-23): жанр из продукта УХОДИТ совсем — поле формы снять при разморозке | разморозка: правка названия и удаление на живых ручках | S4 14.08 → канон |
| Ф-63 | **Рамка поля ввода на модальной поверхности — 1.20:1, ниже порога 1.4.11.** Замер S4 (найдено адверсариальным ревью на новом чекбоксе, у которого рамка — единственный носитель границы; там починено локально на `--color-text-muted`, 3.29:1): `--color-border` `#2e2e2e` о `--color-elevated` `#202020` даёт **1.20:1** при пороге 3:1 для границ элементов управления. Тот же токен несёт рамку ВСЕХ полей формы (`ui/Field.module.css`, модификатор `.framed`), рамку самого модального окна и разделители секций, поэтому одним значением это не чинится: поднять токен — перекрасить всю оболочку, принятую владельцем по референсу (S3.6). Правильный ход — отдельный токен границы для того, что является КОНТРОЛОМ (поле, чекбокс), и он у него уже есть в частном виде. Отложено сознательно: перекраска рамок — это вид, а вид принимает владелец | владелец + отдельный проход | S4 14.08, адверсариальное ревью |