Files
specs/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/design.md
T

130 lines
8.2 KiB
Markdown
Raw 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.
## Context
См. motivation в `proposal.md` и behavioral contract в
`specs/frontend-quality-guardrails/spec.md`. Baseline под Node 22.21.0:
`npm run verify` проходит (17 test files, 55 tests), coverage составляет
82.46% statements, 75.68% branches, 75.47% functions и 83.41% lines, но
Vite создаёт один JS entry asset размером 628,212 bytes minified.
Текущий dependency-cruiser проверяет только направление FSD layers. Он не
включает `no-circular` и не различает public `index.ts` от внутренних files
другого slice. `shared/i18n/index.ts -> zod.ts -> index.ts` образует cycle;
`app/ui/App.tsx` и `features/auth/ui/AuthForm.tsx` обходят public API routes и
i18n. `AppRoutes` статически импортирует все page components. Vitest собирает
coverage, но не задаёт thresholds.
### Baseline bundle measurement
Vite manifest при текущем build содержит один `isEntry` JavaScript asset
`assets/index-DEUAdr-U.js` размером 628,212 bytes. Bundle gate будет читать
manifest и filesystem, а не парсить human-oriented Vite output.
## Goals / Non-Goals
**Goals:**
- Сделать существующий `npm run verify` источником enforced structural,
bundle и coverage guarantees.
- Дать routes lazy-loading boundary без ручного vendor chunk policy.
- Сделать изменение policy проверяемым через focused regression tests и
deterministic build artifacts.
**Non-Goals:**
- Не закрывать WL-P3-04: не менять `lang`, ARIA/live-region policy или
application accessibility semantics. Для loading fallback используется
Mantine component без нового собственного accessibility primitive.
- Не вводить CI/CD, new runtime dependencies, API/backend changes, route
contract changes или manual `manualChunks` policy.
- Не повышать coverage ради процента и не создавать general-purpose
architecture linter вне существующего dependency-cruiser gate.
## Decisions
### Dependency-cruiser остаётся единственным structural graph gate
Configuration задаёт `severity: error` для всех FSD rules (default
dependency-cruiser severity — `warn`) и добавляет `no-circular` и rule с
capture groups: она запрещает
dependency из одного slice во внутренний module другого slice, но разрешает
внутренние imports того же slice и target public `index.ts`. Это охватывает
application и colocated tests, не создавая allowlist для известных violations.
Текущий i18n initialisation переезжает в leaf module; public `index.ts`
сохраняет только consumer-facing validation helper, а `zod.ts` импортирует
leaf напрямую. Route boundary реэкспортируется из public routes API;
application потребляет его оттуда. Это устраняет текущие violations, не
оставляя неиспользуемый public symbol.
Альтернатива — отдельный regex linter для import strings. Она отвергнута,
поскольку duplicate resolver легко расходится с TypeScript alias и не даёт
cycle analysis. Альтернатива с постоянными exception paths отвергнута: она
сделает public API policy необязательной.
### Initial budget считается по Vite manifest import graph
Vite build пишет manifest. `bundle:check` находит `isEntry` JavaScript entry,
рекурсивно суммирует только его `imports`, читает sizes assets с диска и
завершается ошибкой выше 500 * 1024 bytes. `dynamicImports` намеренно не
попадают в initial payload.
Route pages загружаются через `React.lazy`; единственный route-level Suspense
fallback использует Mantine loader. После выделения страниц manifest показал,
что protected application shell всё ещё достижим из entry graph; поэтому
`ProtectedLayout` также становится lazy route boundary. Это снизило measured
initial graph с 549,665 до 509,165 bytes. Script отдельно сохраняет размер
каждого dynamic route boundary — page и `ProtectedLayout` — в committed
baseline JSON. При последующем build script падает, если существующий lazy
route chunk стал больше baseline; обновление baseline выполняется только
отдельной явной командой после review.
Альтернатива — опираться на Vite `chunkSizeWarningLimit`. Она отвергнута:
warning не является gate и измеряет отдельный asset, а не весь eagerly loaded
entry graph. Альтернатива с `manualChunks` отвергнута как premature vendor
optimization.
### Coverage thresholds являются floors, а не delivery metric
Vitest получает global thresholds, округлённые вниз от observed baseline:
82/75/75/83, и instrumentирует весь committed application source
`src/**/*.{ts,tsx}` (исключая test/support source). Любое уменьшение metrics
останавливает test:coverage stage; scenario tests по-прежнему определяют
достаточность поведения.
Альтернатива — exact current percentages. Она отвергнута, поскольку малое
изменение числа instrumented lines создаёт шум без полезного risk signal.
### Remediation lifecycle сохраняет фактический scope
До реализации registry помечает данный change active только для WL-P3-02,
WL-P3-03, WL-P3-05 и WL-P3-06. WL-P3-04 остаётся planned, так как owner
исключил application accessibility work из MVP scope. После успешного archive
registry фиксирует terminal evidence и называет `tighten-backend-request-feedback`
следующей единственной recommended task.
## Risks / Trade-offs
- [Lazy routes кратко показывают fallback] → использовать единственный Mantine
loader без изменения domain, route или application accessibility policy.
- [Vite manifest shape меняется при обновлении tooling] → bundle script
валидирует required manifest fields и сообщает format error вместо false
green result.
- [Новый lazy route не имеет baseline] → explicit baseline command создаёт его
только после review; normal verify не переписывает committed baseline.
- [Structural rule ловит корректный generated import] → exception добавляется
только с documented generator constraint и regression evidence.
- [Coverage floor блокирует полезный refactor] → добавить scenario coverage
либо принять отдельное согласованное изменение baseline, а не обходить gate.
## Migration Plan
1. Сделать dependency, public-API и route-loading behavior testable; затем
изменить минимальные source/config files, пока tests and structural gate
не станут green.
2. Добавить manifest-based bundle tooling, сформировать reviewed lazy baseline
и включить bundle check в `verify`.
3. Добавить coverage floors, выполнить full Node 22 frontend verify и
OpenSpec/docs gates.
4. Rollback — independent Git revert в frontend/specs/docs. Нет persisted data
migration, API migration или browser storage migration.