# Решения по стеку — фронт и платформа > Источник: многоагентное исследование 02.08.2026 (15 агентов: 7 направлений, у каждого независимый > скептик-опровергатель, затем синтез). Все версии сверены с вебом на 2026-08-02. Где скептик дал > поправку уровня blocker/major — записан ИСПРАВЛЕННЫЙ вариант, не исходный. > > **Правило пользования:** пины ниже — не пожелания. Ставить точными версиями, без `^`. > Экосистема сдвинулась за последний год сильно, и **весь корпус документации в сети описывает > прошлые версии** — это главная ловушка (см. §7). --- ## 1. Фронт — ядро | Что | Пин | Заметка | |---|---|---| | React | `19.2.8` | React 20 не существует. Ветка 19.2 живая | | TypeScript | `6.0.3` | **НЕ 7.x** — см. ниже | | Сборщик | `vite 8.2.0` + `@vitejs/plugin-react 6.0.5` | Vite 8 (12.03.2026) заменил Rollup+esbuild на Rolldown | | Маршрутизация | `react-router 8.3.0` | библиотечный (data) режим, без framework mode и SSR | | Серверное состояние | `@tanstack/react-query 5.101.4` | мажор 5 актуален; «v6» — это svelte-адаптер | | Состояние интерфейса | `zustand 5.0.14` | никакого `useStore()` без селектора или `useShallow` | | Строки интерфейса | `@internationalized/string 3.2.10` | каталог сообщений и подстановка; **уже в дереве** под react-aria-components | | Пакетный менеджер | npm из поставки Node | один `package.json` в `frontend/`, воркспейсы не нужны | | Node | **≥ 22.22** | нижняя граница react-router 8; проверить ПЕРВЫМ делом | **Почему TypeScript 6, а не 7.** TS 7 (нативный компилятор на Go, GA 08.07.2026) даёт 8–12× скорости, но **не поставляет программный API** — он обещан в 7.1. Следствие: не работают `typescript-eslint`, плагины языкового сервера, `typescript-plugin-css-modules`. Официальный обход — держать обе версии через npm-алиасы, то есть ровно та связность, которую владелец запретил. Выигрыш в скорости измерялся на миллионных кодовых базах; у нас один пакет, где `tsc` и на TS 6 занимает секунды. **Пересмотреть после выхода TS 7.1 (~октябрь 2026)** — решение обратимо, тайпчек не участвует в сборке. **React Compiler — НЕ включать в MVP.** Совместимость держим линтом (`eslint-plugin-react-hooks 7.1.1` с compiler-powered правилами), сам компилятор — отдельным шагом после заморозки интерфейса и только с проверкой в CI, что вставки реально попали в бандл. Причина: в Vite 8 плагин React выбросил Babel, старый рецепт подключения удалён, а весь интернет показывает именно его; при неверном порядке плагинов компилятор молча не запускается. **Почему `@internationalized/string`, а не react-intl и не lingui (S3.7, 10.08).** Требование — отраслевой инструмент, не самописка: компонент берёт строку по ключу, локаль это данные, новый язык добавляется файлом перевода. Кандидаты сравнены по трём осям — цена бандла, цена сборки, цена ошибки. | | `@internationalized/string` | react-intl (FormatJS) | lingui | |---|---|---|---| | Уже в дереве | **да**, транзитивом react-aria-components (`npm ls`: одна копия, deduped) | нет | нет | | Цена бандла | **0** новых байт, кроме самих слов | ~40 КБ min | ~10 КБ + рантайм ICU | | Шаг сборки | нет | нет (без экстрактора) | **нужен** плагин babel/swc | | Формат | ключ → строка с `{переменной}` | полный ICU | полный ICU + макросы | | Локаль | тот же провайдер, что у примитивов (`I18nProvider`) | свой провайдер | свой провайдер | Взят первый: это i18n-слой Adobe, на котором react-aria и React Spectrum отдают СВОИ строки, то есть он уже несёт локаль нашего интерфейса и уже проверен на нём. Полный ICU нам сегодня не нужен: единственная нетривиальная форма — склонение числительных, и его делает `Intl.PluralRules` в одном месте (`showcase/format.ts`, `counted()`), а не таблицей окончаний в коде. Пересмотреть — если понадобятся вложенные ICU-конструкции (select внутри plural) или экстрактор строк для переводчика. Механизм и его границы: каталог `src/i18n/ru.ts` — единственный дом слов интерфейса, `MessageKey` выводится ИЗ каталога (опечатка в ключе роняет `tsc`), выбор языка — стор `src/i18n/language.ts`, и от него же берут локаль `I18nProvider` и `Intl`. Второй язык = второй файл того же вида плюс строка в `language.ts`; ни один компонент при этом не трогается. Гейт против нового хардкода — две половины с РАЗНЫМИ правилами, и это не педантизм: правило «текста нет в разметке» держит ESLint по МЕСТУ (буква в `JSXText` или в читаемом атрибуте), поэтому оно ловит и русский литерал, и английский — алфавит тут ни при чём; правило «код зоны английский» держит `src/i18n/catalogue.test.ts`, потому что комментарии, CSS и python в AST линтера не входят. Первая редакция держала оба правила алфавитом и в двух носителях сразу — переделано по ревью. Легальных квартир русского ровно две, и обе — данные: сам каталог и фикстуры `src/mock/`. **Почему React Router, а не TanStack Router.** Оба выбора обоснованы: TanStack даёт типобезопасность маршрутов, React Router — то, что AI-сессии знают его кратно лучше и генерируют по его образцам корректный код. При требовании «поддерживаемость важнее» выигрывает узнаваемость. Типизация параметров запроса — тонкий хелпер на zod вокруг `useSearchParams`, а не типобезопасный роутер целиком. ## 2. Фронт — внешний вид и компоненты | Что | Пин | Заметка | |---|---|---| | Стилизация | CSS Modules (нативно в Vite) | Tailwind не берём | | Токены | единственный `src/tokens/tokens.css` | CSS-переменные; минификация — Lightning CSS 1.33.0 | | Примитивы | `react-aria-components 1.20.0` | **ОДНА библиотека на всё**, Apache-2.0 | | Панели | `react-resizable-panels 4.12.2` | API v4: `{ Group, Panel, Separator }` | | Виртуализация | RAC `Virtualizer` | `@tanstack/react-virtual 3.14.9` — в резерве, по замеру | | Иконки | `lucide-react 1.28.0` | `size 16`, `strokeWidth 1.5`, БЕЗ `absoluteStrokeWidth` | | Шрифты | `@fontsource-variable/inter 5.3.0` + `jetbrains-mono 5.3.0` | CJK — системный стек + обязательный `lang` из данных пары | **Одна библиотека примитивов, не две.** React Aria Components закрывает всё сразу: `Table` + `Virtualizer` под банк памяти, `Tree` под дерево глав, `Tabs/Menu/Dialog/Popover/Tooltip` под оболочку, `I18nProvider` под русские служебные строки. Две библиотеки означали бы два focus-scope и два портальных менеджера в одном приложении — это ломает поддерживаемость. Импортируется **только внутри `src/ui/`**; экраны ходят в наши обёртки. **⚠ Читалка двух колонок — синхронизации прокруток НЕТ.** Это исправление моей прежней рекомендации. Правильно: **один скролл-контейнер**, список строк-пар, у каждой строки внутри `display:grid; grid-template-columns:1fr 1fr`. Тогда колонки физически не могут разъехаться. Единица пары — edit-unit из экспорта движка (`--pairs`). Копирование текста ограничить колонкой. **Панели: персист раскладки** — через хук `useDefaultLayout({panelIds, storage})` плюс `Group defaultLayout` и `onLayoutChanged`, а **не** через проп `storage`. Докинг (dockview) в MVP нет. ## 3. Фронт — контроль качества | Что | Пин | |---|---| | Линтер | `eslint 10.8.0` + `typescript-eslint 8.65.0` + `eslint-plugin-react-hooks 7.1.1` | | CSS-линтер | `stylelint 17.14.1` + `stylelint-config-standard 40.0.0` | | Формат | `prettier 3.9.6` (точный пин) | | Тесты | `vitest 4.1.10` | | Моки | `msw 2.15.0` | | Браузер | `@playwright/test 1.62.1` (точный пин) | | Контракт API | `@stoplight/spectral-cli 6.16.3` + `openapi-typescript 7.13.0` | **Контракт API — машинный артефакт, а не проза (04.08, дофикс ревью оркестратора).** Нормативная поверхность — OpenAPI 3.1 в `docs/api-contract/openapi.yaml`: spectral линтует её шестым шагом `npm run check`, openapi-typescript генерит из неё `src/api/schema.ts`, тест дрифта не даёт спеке и типам разъехаться. Прозаический контракт расходится с кодом ровно тем способом, ради предотвращения которого заведена строка 95 единого бэклога. ⚠ `openapi-typescript` объявляет пир `typescript@^5.x` при нашем намеренном TS 6 — `--legacy-peer-deps` ОТВЕРГНУТ проверкой (после него ломается обычный `npm install`), взят точечный `overrides` на один пакет; `npm ci` с нуля проходит. Хвост — `BACKLOG.md` Ф-23. **Почему ESLint, а не oxlint.** oxlint быстрее и связнее, но его type-aware режим требует TS 7, а мы на TS 6. Плюс конкретно для нас у ESLint есть `no-restricted-syntax` — правила, которого в oxlint нет вовсе, и на котором строится главный гейт (ниже). ### Машинный гейт «одно место для цвета и размера» Прямой ответ на требование владельца о поддерживаемости. Двухсторонний, в одной команде: - **в CSS** — stylelint `declaration-property-value-allowed-list` со значением `/^var\(--/` на `color`, `background-color`, `border-color`, `fill`, `stroke`, `z-index`, `font-size`; исключение по пути только для `tokens.css` и сброса; - **в TSX** — ESLint `no-restricted-syntax` на литералы `#hex` / `rgb(` / `hsl(` / `oklch(` плюс запрет атрибута `style` (уровень warn); - **в CI** — stylelint с `reportDisables: true`, чтобы отключение правила комментарием тоже падало. Отступы (`padding`/`margin`/`gap`/`border-radius`) подключить **вторым шагом**, когда шкала токенов зафиксирована — иначе гейт будет мешать на этапе подбора. ### Одна команда проверки ``` npm run check # prettier --check → eslint → tsc --noEmit → vitest run npm run check:full # + vite build + e2e ``` CI вызывает **именно их**, а не дублирует список инструментов. Path-фильтры на уровне job'ов (правка CSS не должна гонять тесты Go) плюс агрегирующий job с явной проверкой `contains(needs.*.result,'failure')||contains(needs.*.result,'cancelled')`. ~~Git-хуков в MVP нет~~ — пересмотрено 02.08 запросом владельца: CI ещё не поднят, и до него pre-commit — единственный машинный рубеж. Хук зовёт те же `npm run check`-команды, не дубль списка (`scripts/githooks/`, детали — `FRONTEND_PLAN.md` §7). **Визуальный гейт с эталонными скриншотами — ОТЛОЖЕН.** Он флейкует между платформами, а наш референс снят на macOS при 2x, целевая платформа — Windows. Вместо него **контракт-тест токенов**: рендерим корень, сверяем вычисленные значения CSS-переменных с замеренными числами. ## 4. Форма продукта и SEO **Два независимых деплоя.** - **Приложение** — чистая SPA на `app.<домен>`, `X-Robots-Tag: noindex` на КАЖДОМ HTML-ответе. `robots.txt` обход HTML **не** запрещает (иначе Google не увидит noindex), `Disallow` только для `/api/*`. - **Публичный контур** — лендинг, цены, справка, политика куков, страницы входа и регистрации — статический HTML на апексе, отдаётся CDN или существующим Go-сервисом. **Astro в MVP не берём.** Каждая публичная страница достижима обычным `` не дальше трёх кликов от главной (это и есть лечение «страниц-сирот», о которых писал владелец). `sitemap.xml` — дубль, не основной канал. **Продуктовый запрет (жёсткий):** ни один символ пользовательского перевода никогда не попадает на публично индексируемый URL. В MVP нет ни витрины примеров, ни «поделиться главой», ни публичных ссылок на прочтение. Причины две: авторские права на исходники и политика поисковиков в отношении машинно-сгенерированного контента. ## 5. Платформа | Что | Пин | Заметка | |---|---|---| | Go | `1.26.4` в `go.mod`, ⚠ тулчейн сборки платформы **≥1.26.5** (`platform/Makefile` `GO_MIN_VERSION` — security-фиксы crypto/tls и os в сетевом модуле); пин живёт в Makefile, здесь только указатель | | | HTTP | стандартный `net/http` + `ServeMux` | роутер-библиотеку не тянуть | | Postgres | `pgx v5.10.0` | нижняя допустимая граница 5.9.2, **не** 5.9.0 | | Миграции | `goose v3.27.3` | как библиотека, `embed.FS`, `WithLocker` | | Очередь | `river v0.42.0` | на том же Postgres | | Безопасность | `govulncheck` | обязательный гейт CI | **Redis не заводим нигде** — зафиксировано как архитектурное «нет», иначе он приползёт по частям. **Конкурентность по книге.** Одна книга = один процесс-воркер; сериализация в очереди по `book_id` плюс пиннинг книги к одному хосту на MVP. Лизы `book_leases` с heartbeat — поверх, ради вежливого ожидания вместо аварии. Глобальный брокер исходящих LLM-вызовов — отдельный компонент, включаемый по гейту «до второго параллельного пользователя». **Прогресс — SSE, и события ПУШИТ воркер**, а не фронт опрашивает read-model (см. §7, риск про ре-ингест). Браузер — нативный `EventSource` на cookie; десктоп и CLI — обычный GET с `Authorization: Bearer` и построчным парсером. WebSocket не нужен. Обязательно: HTTP/2 на границе, `Cache-Control: no-store`, `X-Accel-Buffering: no`, heartbeat ~20 с, монотонный `id` + `Last-Event-ID` для докачки. **Аутентификация — одна серверная сессия, два способа предъявления.** Запись сессии в Postgres, непрозрачный токен 256 бит (в БД только SHA-256), и предъявляется он либо `__Host`-cookie (HttpOnly, Secure, SameSite=Lax) для браузера, либо `Authorization: Bearer` для десктопа и CLI. Principal создаётся **только** в middleware; ни один эндпоинт не имеет права предполагать cookie — именно это сохраняет портируемость на десктоп, ради которой владелец и требовал Bearer. CSRF только на cookie-пути: `Sec-Fetch-Site` → фолбэк на `Origin` по allowlist → обязательный кастомный заголовок; та же проверка `Origin` на любом стрим-handshake. JWT отвергнут. ## 6. Десктоп **Первый шаг — устанавливаемое PWA** на том же origin, что API: ноль второго режима аутентификации, ноль подписи кода, ноль сборочного конвейера. **Tauri 2.11.5 — вторым шагом**, по явным триггерам: нужен трей или глобальные горячие клавиши, нужен распространяемый `.exe`, появился офлайн-режим, нужен доступ к папке без диалога выбора, нужно хранилище учётных данных ОС. Electron 43.2.0 и Wails v3 (всё ещё alpha) отвергнуты. До этого решения в приложении — только браузерные API, вся связь с движком через HTTP-контракт платформы. ## 7. Ловушки, на которые мы наступим Отсортировано по вероятности укусить. 1. **Вся документация в сети описывает прошлые версии.** Vite 8 сделал `build.commonjsOptions` молчаливым no-op; `react-resizable-panels` v4 сменил экспорты (`PanelGroup`/`PanelResizeHandle` → `Group`/`Separator`); рецепт React Compiler для Vite удалён. AI-сессия скопирует конфиг из статьи 2025 года и получит тихо неработающую опцию. **Лечение:** конфиги минимальные, каждая нестандартная опция с комментарием «зачем», версии проверять по npm перед использованием. 2. **`types` в tsconfig теперь по умолчанию `[]`**, а не `["*"]` — забыть прописать `"types": ["node", "vite/client"]` значит внезапно потерять глобальные типы. 3. **Дефолтные скроллбары Chromium на Windows** толстые и светлые — ломают Fleet-вид сразу на трёх панелях и в читалке. Стилизовать с первого дня. 4. **Порядок каскада** между стилями React Aria, `tokens.css` и CSS Modules — источник тихих расхождений. Задать `@layer` явно на этапе токенов. 5. **Fleet закрыт** (объявлено 08.12.2025, скачивание прекращено 22.12.2025). Референс невоспроизводим: всё, что не замерено (кегли, интерлиньяж, внутренние отступы), больше посмотреть негде. Скриншот снят на macOS при 2x, цель — Windows-Chromium с другим хинтингом. **Растровое совпадение недостижимо и не должно быть критерием готовности.** 6. **Node на стенде** объявлен как 22, но react-router 8 требует ≥22.22, Vite 8 — ≥22.12. Проверить первым действием. ## 8. Что придётся достроить в движке Через оркестратора, в зону `backend/`, в порядке критичности. Обосновано разбором кода. 1. **Пофазный прогресс** `draft N/M` ∥ `edit N/M` поверх `chunk_status`. Сейчас unit объявляется `done` только когда есть и все draft-строки его членов, и edit-строка — то есть во время черновой волны индикатор показывал бы 0% почти всё время. 2. **Персист манифеста глав и чанков** (+ `chunker_version` и хеш источника). Нужен под экран разбора и дерево глав, и снимает ре-ингест: сейчас каждый read-вызов заново читает и режет исходник — замерено 1.42–1.51 с процессорного времени на книге 23 МБ, и это умножается на число книг в библиотеке. 3. **JSON-выход таблицы подписи банка** — сейчас только человеческий текст с ограничением 20 строк. 4. **Приём внешнего `TRACEPARENT`** в `cmd/tmctl`, чтобы прогон был одной трассой. Спаны для подсветки **внутри** текста — **не делаем** (владелец решил 02.08): у дешёвых гейтов нет ни одного байтового смещения, только счётчики, а подсветка на уровне блока признана достаточной. **Выравнивание колонок достраивать не нужно** — `tmctl export --pairs` уже отдаёт колонку исходника, выровненную по единицам редактора (`export.go:189`). Гранулярность грубая и владельцем принята. ⚠ **Подпись термина — это НЕ UPDATE строки.** Банк пересобирается из файлов на каждом прогоне (`seedGlossary` + `ReplaceBank`), поэтому прямая запись в таблицу `glossary` молча стирается следующим прогоном. Контракт подписи обязан это учитывать. ⚠ **После апгрейда бинарника движка** все read-запросы по старым книгам падают («schema vN, expects vM»), пока по книге не пройдёт write-команда: `OpenReadOnly` требует точного совпадения версии схемы. Это влияет на порядок деплоя платформы.