Files
gridpilot.gg/plans/website-architecture-violations.md
2026-01-11 15:06:26 +01:00

104 lines
12 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.
# 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 repos 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)