238 lines
22 KiB
Markdown
238 lines
22 KiB
Markdown
# Решения по стеку — фронт и платформа
|
||
|
||
> Источник: многоагентное исследование 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` (точный пин) |
|
||
| Контракт 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 не берём.**
|
||
Каждая публичная страница достижима обычным `<a href>` не дальше трёх кликов от главной
|
||
(это и есть лечение «страниц-сирот», о которых писал владелец). `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` требует точного
|
||
совпадения версии схемы. Это влияет на порядок деплоя платформы.
|