From 20295190ddb02cb279a6b404d40d944bde862c3d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 24 Aug 2026 13:07:25 +0300 Subject: [PATCH] docs(specs): archive frontend quality guardrails --- .../.openspec.yaml | 3 + .../README.md | 3 + .../design.md | 129 ++++++++++++++++++ .../proposal.md | 45 ++++++ .../specs/frontend-quality-guardrails/spec.md | 77 +++++++++++ .../tasks.md | 64 +++++++++ .../specs/frontend-quality-guardrails/spec.md | 79 +++++++++++ 7 files changed, 400 insertions(+) create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/.openspec.yaml create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/README.md create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/design.md create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/proposal.md create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/specs/frontend-quality-guardrails/spec.md create mode 100644 openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/tasks.md create mode 100644 openspec/specs/frontend-quality-guardrails/spec.md diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/.openspec.yaml b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/.openspec.yaml new file mode 100644 index 0000000..3cd8f77 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-08-24 +goal: Закрыть WL-P3-02, WL-P3-03, WL-P3-05 и WL-P3-06 без расширения MVP scope. diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/README.md b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/README.md new file mode 100644 index 0000000..97b08af --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/README.md @@ -0,0 +1,3 @@ +# tighten-frontend-quality-guardrails + +Закрыть согласованные frontend quality guardrails из аудита 20.08.2026. diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/design.md b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/design.md new file mode 100644 index 0000000..b5f160a --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/design.md @@ -0,0 +1,129 @@ +## 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. diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/proposal.md b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/proposal.md new file mode 100644 index 0000000..4631efc --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/proposal.md @@ -0,0 +1,45 @@ +## Why + +Аудит 20.08.2026 подтвердил, что frontend не принуждает две уже объявленные +структурные границы: i18n имеет import cycle, а cross-slice imports могут +обходить public `index.ts`. Одновременно production build содержит один +initial JavaScript chunk 628.21 kB minified, а coverage запускается без +минимальных порогов. Поэтому green `npm run verify` ещё не гарантирует +сохранение этих quality properties при следующем изменении. + +## What Changes + +- Закрепить отсутствие import cycles и обхода public API между любыми разными + FSD slices в `frontend/src`, включая `app` и tests; generated-code exception + допустим только как явно документированное вынужденное исключение. +- Устранить текущий i18n cycle и текущие deep imports без изменения + пользовательского API или HTTP-контракта. +- Lazy-load route page boundaries и добавить проверяемый budget: сумма entry + bundle и всех его eagerly loaded JavaScript dependencies не превышает + 500 kB minified. Lazy route chunks получают baseline только для последующих + регрессий; ручной vendor chunking не вводится. +- Закрепить coverage floors как anti-regression signal: statements 82%, + branches 75%, functions 75%, lines 83%. +- Перевести remediation registry в active для WL-P3-02, WL-P3-03, WL-P3-05 и + WL-P3-06. WL-P3-04 остаётся `planned`: accessibility semantics не входят в + текущий MVP scope. + +## Capabilities + +### New Capabilities + +- `frontend-quality-guardrails`: воспроизводимые structural, bundle и coverage + guards для delivered React client. + +### Modified Capabilities + +_Нет._ + +## Impact + +- Frontend: FSD dependency configuration, i18n and app public imports, + route loading, Vite/Vitest configuration, package scripts и targeted tests. +- Specs/docs: delta-spec, change design/tasks и audit remediation registry. + +Не изменяются HTTP/OpenAPI contract, domain terminology, backend, T-Bank +integration, Mantine accessibility semantics, CI/CD или deferred Prisma work. diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/specs/frontend-quality-guardrails/spec.md b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/specs/frontend-quality-guardrails/spec.md new file mode 100644 index 0000000..d788707 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/specs/frontend-quality-guardrails/spec.md @@ -0,0 +1,77 @@ +## Purpose + +Гарантирует, что frontend сохраняет проверяемые structural, startup-size и +test-coverage свойства при последующих изменениях React client. + +## ADDED Requirements + +### Requirement: Frontend сохраняет structural boundaries slice public API + +Frontend MUST не содержать cycles в import graph `src` и MUST обращаться к +другому FSD slice только через его public `index.ts`, независимо от слоя, +включая `app` и test source. Generated source MAY быть исключён только при +документированном вынужденном исключении в dependency configuration. + +#### Scenario: Добавлен import cycle + +- **WHEN** исходный или test module в `src` создаёт прямой либо косвенный + import cycle +- **THEN** documented frontend dependency gate завершается с ошибкой до + передачи изменения + +#### Scenario: Slice обходит public API другого slice + +- **WHEN** module одного slice импортирует внутренний file другого slice, + минуя его public `index.ts` +- **THEN** documented frontend dependency gate завершается с ошибкой и + сообщает boundary violation + +#### Scenario: Application использует public route API + +- **WHEN** application container использует route-level component +- **THEN** import проходит через public API routes slice и dependency gate + завершается успешно при отсутствии остальных violations + +### Requirement: Frontend ограничивает initial JavaScript payload + +Production frontend MUST lazy-load route page boundaries. Сумма minified +JavaScript entry bundle и всех его eagerly loaded JavaScript dependencies MUST +NOT превышать 500 kB. Lazy route chunks MUST иметь зафиксированный baseline +для последующих регрессий; bundle policy MUST NOT требовать ручного vendor +chunking. + +#### Scenario: Production build укладывается в initial budget + +- **WHEN** frontend production build создаёт entry bundle и eagerly loaded + JavaScript dependencies суммарным размером не более 500 kB minified +- **THEN** documented bundle gate завершается успешно + +#### Scenario: Production build превышает initial budget + +- **WHEN** frontend production build создаёт entry bundle и eagerly loaded + JavaScript dependencies суммарным размером более 500 kB minified +- **THEN** documented bundle gate завершается с ошибкой и сообщает measured + initial payload + +#### Scenario: Изменён lazy route chunk + +- **WHEN** lazy-loaded route chunk превышает свой зафиксированный baseline +- **THEN** documented bundle gate завершается с ошибкой до передачи изменения + +### Requirement: Frontend coverage является anti-regression gate + +Documented frontend coverage command MUST завершаться с ошибкой, если global +coverage committed source ниже 82% statements, 75% branches, 75% functions +или 83% lines. Эти пороги SHALL служить только сигналом регрессии и MUST NOT +заменять scenario-based tests. + +#### Scenario: Coverage сохраняет каждый baseline floor + +- **WHEN** frontend test suite достигает либо превышает все четыре coverage + floors +- **THEN** documented coverage command завершается успешно + +#### Scenario: Coverage опускается ниже floor + +- **WHEN** frontend test suite опускается ниже хотя бы одного coverage floor +- **THEN** documented coverage command завершается с ошибкой по этой метрике diff --git a/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/tasks.md b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/tasks.md new file mode 100644 index 0000000..b4cbb43 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-tighten-frontend-quality-guardrails/tasks.md @@ -0,0 +1,64 @@ +## 1. Зафиксировать remediation scope + +- [x] 1.1 В `docs/development/audit-remediation.md` перевести WL-P3-02, + WL-P3-03, WL-P3-05 и WL-P3-06 в `active` с change + `tighten-frontend-quality-guardrails`; WL-P3-04 оставить `planned` и не + добавлять terminal evidence до archive. +- [x] 1.2 Зафиксировать Node 22 baseline: выполнить frontend `npm run verify`, + записать observed tests, coverage и initial bundle size в change evidence. + +## 2. Enforce structural public API boundaries + +- [x] 2.1 Добавить fixture-based failing checks для dependency-cruiser: cycle + и import внутреннего file другого slice должны быть rejected, а same-slice и + public-index imports должны оставаться allowed. +- [x] 2.2 Запустить новые dependency fixture checks до implementation и + подтвердить expected red result для отсутствующих rules. +- [x] 2.3 Добавить `no-circular` и capture-group public-API rule в + `frontend/dependency-cruiser.cjs`; включить fixture checks в documented + frontend dependency gate. +- [x] 2.4 Вынести i18n initialization в leaf module, сохранить public exports + через `shared/i18n/index.ts` и перевести validation helper на leaf import, + чтобы устранить cycle. +- [x] 2.5 Реэкспортировать route error boundary из public routes API и + перевести application и auth imports на соответствующие public APIs. +- [x] 2.6 Выполнить targeted tests и `npm run deps`; подтвердить green result + для committed source и expected rejection обоих fixture violations. + +## 3. Сделать initial payload измеримым и ограниченным + +- [x] 3.1 Написать failing unit tests для manifest parser: он суммирует entry + asset с transitive eager imports, не включает dynamic imports, отклоняет + malformed manifest и сообщает initial-budget/lazy-baseline regressions. +- [x] 3.2 Запустить bundle parser tests и подтвердить expected red result до + создания parser/check command. +- [x] 3.3 Реализовать manifest-based bundle budget and explicit baseline + scripts без runtime dependencies; normal check не должен изменять baseline. +- [x] 3.4 Lazy-load route page boundaries через React lazy/Suspense с Mantine + fallback, не меняя route paths, auth redirects или application + accessibility policy. +- [x] 3.5 Включить Vite manifest output, bundle check и explicit reviewed lazy + route baseline в package scripts; добавить bundle check в `npm run verify`. +- [x] 3.6 Сформировать initial lazy-route baseline после code splitting и + подтвердить, что build проходит initial budget ≤ 500 * 1024 bytes. +- [x] 3.7 Выполнить bundle parser tests, route regression tests и bundle gate; + подтвердить green result и absence ручного vendor chunk policy. + +## 4. Закрепить coverage floors + +- [x] 4.1 Добавить global Vitest thresholds: statements 82, branches 75, + functions 75, lines 83, не меняя scenario tests ради процента. +- [x] 4.2 Запустить coverage command и подтвердить, что committed suite + проходит все четыре floors. + +## 5. Провести completion checks + +- [x] 5.1 Выполнить frontend `npm run verify` под Node 22, targeted tests, + `git diff --check` и зафиксировать observed results. +- [x] 5.2 Выполнить strict change validation, synced-spec validation и + OpenSpec authority verifier с `--store wealth-ledger-specs`. +- [x] 5.3 Провести independent read-only review implementation, tests, + generated/build artifacts и docs scope; устранить blocking findings до + archive. +- [x] 5.4 Выполнить applicable `opsx-verify` procedure и устранить every + blocking completeness, correctness или coherence result до archive. diff --git a/openspec/specs/frontend-quality-guardrails/spec.md b/openspec/specs/frontend-quality-guardrails/spec.md new file mode 100644 index 0000000..79bc95f --- /dev/null +++ b/openspec/specs/frontend-quality-guardrails/spec.md @@ -0,0 +1,79 @@ +# frontend-quality-guardrails Specification + +## Purpose + +Гарантирует, что frontend сохраняет проверяемые structural, startup-size и +test-coverage свойства при последующих изменениях React client. + +## Requirements + +### Requirement: Frontend сохраняет structural boundaries slice public API + +Frontend MUST не содержать cycles в import graph `src` и MUST обращаться к +другому FSD slice только через его public `index.ts`, независимо от слоя, +включая `app` и test source. Generated source MAY быть исключён только при +документированном вынужденном исключении в dependency configuration. + +#### Scenario: Добавлен import cycle + +- **WHEN** исходный или test module в `src` создаёт прямой либо косвенный + import cycle +- **THEN** documented frontend dependency gate завершается с ошибкой до + передачи изменения + +#### Scenario: Slice обходит public API другого slice + +- **WHEN** module одного slice импортирует внутренний file другого slice, + минуя его public `index.ts` +- **THEN** documented frontend dependency gate завершается с ошибкой и + сообщает boundary violation + +#### Scenario: Application использует public route API + +- **WHEN** application container использует route-level component +- **THEN** import проходит через public API routes slice и dependency gate + завершается успешно при отсутствии остальных violations + +### Requirement: Frontend ограничивает initial JavaScript payload + +Production frontend MUST lazy-load route page boundaries. Сумма minified +JavaScript entry bundle и всех его eagerly loaded JavaScript dependencies MUST +NOT превышать 500 kB. Lazy route chunks MUST иметь зафиксированный baseline +для последующих регрессий; bundle policy MUST NOT требовать ручного vendor +chunking. + +#### Scenario: Production build укладывается в initial budget + +- **WHEN** frontend production build создаёт entry bundle и eagerly loaded + JavaScript dependencies суммарным размером не более 500 kB minified +- **THEN** documented bundle gate завершается успешно + +#### Scenario: Production build превышает initial budget + +- **WHEN** frontend production build создаёт entry bundle и eagerly loaded + JavaScript dependencies суммарным размером более 500 kB minified +- **THEN** documented bundle gate завершается с ошибкой и сообщает measured + initial payload + +#### Scenario: Изменён lazy route chunk + +- **WHEN** lazy-loaded route chunk превышает свой зафиксированный baseline +- **THEN** documented bundle gate завершается с ошибкой до передачи изменения + +### Requirement: Frontend coverage является anti-regression gate + +Documented frontend coverage command MUST завершаться с ошибкой, если global +coverage committed source ниже 82% statements, 75% branches, 75% functions +или 83% lines. Эти пороги SHALL служить только сигналом регрессии и MUST NOT +заменять scenario-based tests. + +#### Scenario: Coverage сохраняет каждый baseline floor + +- **WHEN** frontend test suite достигает либо превышает все четыре coverage + floors +- **THEN** documented coverage command завершается успешно + +#### Scenario: Coverage опускается ниже floor + +- **WHEN** frontend test suite опускается ниже хотя бы одного coverage floor +- **THEN** documented coverage command завершается с ошибкой по этой метрике