20 KiB
Решения по стеку — фронт и платформа
Источник: многоагентное исследование 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 |
| Пакетный менеджер | 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, старый рецепт подключения удалён, а весь интернет
показывает именно его; при неверном порядке плагинов компилятор молча не запускается.
Почему 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 (точный пин) |
Почему 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 не берём.
Каждая публичная страница достижима обычным
<a href>не дальше трёх кликов от главной (это и есть лечение «страниц-сирот», о которых писал владелец).sitemap.xml— дубль, не основной канал.
Продуктовый запрет (жёсткий): ни один символ пользовательского перевода никогда не попадает на публично индексируемый URL. В MVP нет ни витрины примеров, ни «поделиться главой», ни публичных ссылок на прочтение. Причины две: авторские права на исходники и политика поисковиков в отношении машинно-сгенерированного контента.
5. Платформа
| Что | Пин | Заметка |
|---|---|---|
| Go | 1.26.4 |
|
| 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. Ловушки, на которые мы наступим
Отсортировано по вероятности укусить.
- Вся документация в сети описывает прошлые версии. Vite 8 сделал
build.commonjsOptionsмолчаливым no-op;react-resizable-panelsv4 сменил экспорты (PanelGroup/PanelResizeHandle→Group/Separator); рецепт React Compiler для Vite удалён. AI-сессия скопирует конфиг из статьи 2025 года и получит тихо неработающую опцию. Лечение: конфиги минимальные, каждая нестандартная опция с комментарием «зачем», версии проверять по npm перед использованием. typesв tsconfig теперь по умолчанию[], а не["*"]— забыть прописать"types": ["node", "vite/client"]значит внезапно потерять глобальные типы.- Дефолтные скроллбары Chromium на Windows толстые и светлые — ломают Fleet-вид сразу на трёх панелях и в читалке. Стилизовать с первого дня.
- Порядок каскада между стилями React Aria,
tokens.cssи CSS Modules — источник тихих расхождений. Задать@layerявно на этапе токенов. - Fleet закрыт (объявлено 08.12.2025, скачивание прекращено 22.12.2025). Референс невоспроизводим: всё, что не замерено (кегли, интерлиньяж, внутренние отступы), больше посмотреть негде. Скриншот снят на macOS при 2x, цель — Windows-Chromium с другим хинтингом. Растровое совпадение недостижимо и не должно быть критерием готовности.
- Node на стенде объявлен как 22, но react-router 8 требует ≥22.22, Vite 8 — ≥22.12. Проверить первым действием.
8. Что придётся достроить в движке
Через оркестратора, в зону backend/, в порядке критичности. Обосновано разбором кода.
- Пофазный прогресс
draft N/M∥edit N/Mповерхchunk_status. Сейчас unit объявляетсяdoneтолько когда есть и все draft-строки его членов, и edit-строка — то есть во время черновой волны индикатор показывал бы 0% почти всё время. - Персист манифеста глав и чанков (+
chunker_versionи хеш источника). Нужен под экран разбора и дерево глав, и снимает ре-ингест: сейчас каждый read-вызов заново читает и режет исходник — замерено 1.42–1.51 с процессорного времени на книге 23 МБ, и это умножается на число книг в библиотеке. - JSON-выход таблицы подписи банка — сейчас только человеческий текст с ограничением 20 строк.
- Приём внешнего
TRACEPARENTвcmd/tmctl, чтобы прогон был одной трассой.
Спаны для подсветки внутри текста — не делаем (владелец решил 02.08): у дешёвых гейтов нет ни одного байтового смещения, только счётчики, а подсветка на уровне блока признана достаточной.
Выравнивание колонок достраивать не нужно — tmctl export --pairs уже отдаёт колонку
исходника, выровненную по единицам редактора (export.go:189). Гранулярность грубая и владельцем
принята.
⚠ Подпись термина — это НЕ UPDATE строки. Банк пересобирается из файлов на каждом прогоне
(seedGlossary + ReplaceBank), поэтому прямая запись в таблицу glossary молча стирается
следующим прогоном. Контракт подписи обязан это учитывать.
⚠ После апгрейда бинарника движка все read-запросы по старым книгам падают
(«schema vN, expects vM»), пока по книге не пройдёт write-команда: OpenReadOnly требует точного
совпадения версии схемы. Это влияет на порядок деплоя платформы.