docs
This commit is contained in:
103
plans/website-architecture-violations.md
Normal file
103
plans/website-architecture-violations.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Website Architecture Violations Report
|
||||
|
||||
Scope: `apps/website/**` aligned against `docs/architecture/website/**`, with the authoritative contract at [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
||||
|
||||
This report lists violations as: rule ⇒ evidence ⇒ impact ⇒ fix direction.
|
||||
|
||||
---
|
||||
|
||||
## A) DI contract violations (server-side singleton container usage)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| Server `app/**/page.tsx` MUST NOT access the DI container ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:231), [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:15)) | [`ContainerManager.getInstance().getContainer()`](apps/website/app/profile/leagues/page.tsx:45) | Server route is directly resolving from a singleton container; violates the client-first DI contract and request-safety. | Replace with a PageQuery + manual per-request wiring, or a request-scoped container factory. |
|
||||
| `ContainerManager.getContainer()` is client-only ([`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:35)) | [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15) calls [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page/PageDataFetcher.ts:21) | This helper enables forbidden server access to the singleton container. | Ban server use of [`PageDataFetcher.fetch()`](apps/website/lib/page/PageDataFetcher.ts:15). Use explicit construction in PageQueries. |
|
||||
| PageQueries may use DI only if request-scoped and stateless ([`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:20)) | [`DashboardPageQuery.execute()`](apps/website/lib/page-queries/DashboardPageQuery.ts:108) uses [`ContainerManager.getInstance().getContainer()`](apps/website/lib/page-queries/DashboardPageQuery.ts:110) | PageQuery runs on the server; the singleton container is explicitly unsafe for concurrent server requests. | Convert to manual wiring (construct API client + service per call), or create a fresh container per request. |
|
||||
|
||||
---
|
||||
|
||||
## B) RSC route structure violations (page.tsx doing more than composition)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| `page.tsx` does composition only ([`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:26), [`WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:50)) | Data fetch + categorization + inline template in a server route module: [`fetchProfileLeaguesData()`](apps/website/app/profile/leagues/page.tsx:22) and [`ProfileLeaguesTemplate()`](apps/website/app/profile/leagues/page.tsx:82) | Responsibilities are mixed: server data fetching, decision logic, and rendering composition in one module. | Split into per-route shape: `page.tsx` calls a PageQuery and passes Page DTO; a `*PageClient.tsx` builds ViewData; Template renders ViewData only. |
|
||||
| PageQueries MUST return the documented discriminated union ([`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:17)) | Local type differs from spec: [`PageQueryResult<TData>`](apps/website/lib/page-queries/DashboardPageQuery.ts:11) uses `data` and `destination` | Breaks the documented contract (`ok` with `dto`, `redirect` with `to`). | Standardize a single `PageQueryResult` type per [`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:17) and enforce across queries. |
|
||||
|
||||
---
|
||||
|
||||
## C) ViewModel boundary violations (ViewModels used/created on server and/or passed into Templates)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| ViewModels are client-only and never serialized ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:70), [`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:71)) | Server route data types include ViewModels: [`TeamDetailData.team: TeamDetailsViewModel`](apps/website/app/teams/[id]/page.tsx:15) and are passed into Template wrapper: [`TeamDetailTemplateWrapper()`](apps/website/app/teams/[id]/page.tsx:20) | Implies ViewModels cross the server→client boundary, contradicting the “client-only class” rule and risking serialization/hydration issues. | Use Page DTOs for server-to-client, and instantiate ViewModels only in `'use client'` modules. |
|
||||
| Templates accept ViewData only ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:86), [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:3)) | Template prop uses ViewModel instances: [`DriverRankingsTemplateProps.drivers: DriverLeaderboardItemViewModel[]`](apps/website/templates/DriverRankingsTemplate.tsx:19) | Template is no longer receiving ViewData (JSON-serializable primitive shapes). | Introduce Presenters that map Page DTO → ViewData and pass ViewData into Templates. |
|
||||
| PageQueries must not instantiate client-only types / ViewModels ([`WEBSITE_PAGE_QUERIES.md`](docs/architecture/website/WEBSITE_PAGE_QUERIES.md:31)) | PageQuery imports from `lib/view-models/**`: [`DashboardOverviewViewModelData`](apps/website/lib/page-queries/DashboardPageQuery.ts:6) | `lib/view-models/**` is the client-only boundary (also where Presenters live per [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:50)). | Move Page DTO types under `lib/page-queries/**` and keep view-model types under `lib/view-models/**`. |
|
||||
|
||||
---
|
||||
|
||||
## D) Template purity violations (Templates importing forbidden layers and doing computation)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| Templates MUST NOT import ViewModels ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`DriverLeaderboardItemViewModel`](apps/website/templates/DriverRankingsTemplate.tsx:7) | Template depends on client-only classes, drifting into Presenter/ViewModel responsibilities. | Replace ViewModel props with ViewData props. |
|
||||
| Templates MUST NOT import Display Objects ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`LeagueRoleDisplay`](apps/website/templates/LeagueDetailTemplate.tsx:13) | Display Objects are forbidden inside Templates. | Use Presenters/ViewModels to emit primitive strings into ViewData. |
|
||||
| Templates MUST NOT compute derived values ([`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:33)) | Filtering/sorting occurs inside Template: [`drivers.filter()`](apps/website/templates/DriverRankingsTemplate.tsx:73) and [`sort(...)`](apps/website/templates/DriverRankingsTemplate.tsx:81) | Template performs orchestration and non-trivial computation rather than rendering prepared ViewData. | Move to ViewModels (client) and/or Presenters; Templates render only. |
|
||||
| “Templates are pure” ([`WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:27)) | Hooks in templates: [`useMemo()`](apps/website/templates/RacesTemplate.tsx:3) used for filtering/grouping | Pure-template contract is violated by stateful/reactive computations. | Create a `*PageClient.tsx` container for state/computation; keep template as pure function over ViewData. |
|
||||
|
||||
---
|
||||
|
||||
## E) Determinism violations: forbidden locale/time formatting paths (`Intl.*`, `toLocale*`)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| Templates must not call locale APIs ([`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:41), [`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:16)) | Example: [`toLocaleDateString()`](apps/website/templates/RacesTemplate.tsx:148) | Locale/timezone-dependent formatting can differ between SSR and browser, risking hydration mismatches and non-determinism. | Replace with deterministic formatting via Display Objects, or API-provided labels, passed through Presenters into ViewData. |
|
||||
| Formatting code paths must not use locale APIs ([`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:63), [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:37)) | Example: [`toLocaleString()`](apps/website/lib/view-models/BillingViewModel.ts:86) | Even though ViewModels may format, this repo’s strict determinism rules forbid runtime-locale APIs. | Replace with deterministic numeric/date formatting helpers (no `Intl.*`, no `toLocale*`). |
|
||||
| Utility helpers used in presentation must not use locale APIs ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:7)) | [`formatDate()`](apps/website/lib/utilities/time.ts:48) calls [`toLocaleDateString()`](apps/website/lib/utilities/time.ts:50) | Utilities become hidden sources of non-determinism across the app. | Replace these helpers with deterministic formatters and enforce via guardrail tests/ESLint. |
|
||||
|
||||
---
|
||||
|
||||
## F) Page.tsx guardrail violations (sorting/filtering in server routes and forbidden imports)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| RSC `page.tsx` must not perform sorting/filtering beyond trivial checks ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)) | Server route computes derived values via reduce/filter/sort: [`computeDerivedData()`](apps/website/app/teams/page.tsx:12) | Server route contains presentation shaping. | Move shaping into PageQuery output (as Page DTO only) and/or into client ViewModel/Presenter. |
|
||||
| RSC `page.tsx` must not import ViewModels ([`WEBSITE_GUARDRAILS.md`](docs/architecture/website/WEBSITE_GUARDRAILS.md:9)) | Example: [`TeamSummaryViewModel`](apps/website/app/teams/page.tsx:10) | ViewModels are client-only. | Replace with Page DTO types; construct ViewModels client-side. |
|
||||
|
||||
---
|
||||
|
||||
## G) Write-flow violations (writes not entering through Server Actions)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| All writes MUST enter through Server Actions ([`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:171), [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:18)) | Client component performs POST: [`fetch('/api/auth/logout', { method: 'POST' })`](apps/website/components/profile/UserPill.tsx:212) | Client-initiated writes are explicitly forbidden. | Replace with a Server Action invoked via `<form action={...}>` or button action, then revalidate/navigate. |
|
||||
|
||||
---
|
||||
|
||||
## H) UX-only blockers embedded in services (state leakage risk)
|
||||
|
||||
| Rule | Evidence | Why this is a violation | Fix direction |
|
||||
|---|---|---|---|
|
||||
| Blockers are UX-only and should be local + reversible ([`BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:7), [`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:7)) | Stateful blockers inside a service: [`submitBlocker`](apps/website/lib/services/leagues/LeagueService.ts:96) and [`throttle`](apps/website/lib/services/leagues/LeagueService.ts:97) | If the service is DI-singleton or reused across requests, this state can leak across users/requests; also mixes UI concerns into a service boundary. | Move blockers into client UI boundary (hook/component) or ensure strict client-only, per-instance usage. Services should remain stateless. |
|
||||
|
||||
---
|
||||
|
||||
## High-signal file sets (pattern-based indicators)
|
||||
|
||||
### Templates importing ViewModels and or Display Objects (forbidden)
|
||||
|
||||
- [`DriverRankingsTemplate.tsx`](apps/website/templates/DriverRankingsTemplate.tsx:7)
|
||||
- [`LeagueDetailTemplate.tsx`](apps/website/templates/LeagueDetailTemplate.tsx:13)
|
||||
- [`LeagueStandingsTemplate.tsx`](apps/website/templates/LeagueStandingsTemplate.tsx:6)
|
||||
- [`TeamDetailTemplate.tsx`](apps/website/templates/TeamDetailTemplate.tsx:14)
|
||||
|
||||
### Server routes importing ViewModels and or doing formatting/filtering (forbidden/discouraged)
|
||||
|
||||
- [`teams/page.tsx`](apps/website/app/teams/page.tsx:12)
|
||||
- [`teams/[id]/page.tsx`](apps/website/app/teams/[id]/page.tsx:9)
|
||||
- [`profile/leagues/page.tsx`](apps/website/app/profile/leagues/page.tsx:7)
|
||||
|
||||
### Structural drift vs canonical `lib/*` layout
|
||||
|
||||
- API client exists at root `lib/` instead of `lib/api/`: [`apiClient.ts`](apps/website/lib/apiClient.ts:1)
|
||||
- Non-canonical `lib/page/` exists: [`PageDataFetcher.ts`](apps/website/lib/page/PageDataFetcher.ts:1)
|
||||
|
||||
Reference in New Issue
Block a user