130 lines
8.2 KiB
Markdown
130 lines
8.2 KiB
Markdown
## 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.
|