12 KiB
12 KiB
Website Architecture Violations Report
Scope: apps/website/** aligned against docs/architecture/website/**, with the authoritative contract at WEBSITE_CONTRACT.md.
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, WEBSITE_DI_RULES.md) |
ContainerManager.getInstance().getContainer() |
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) |
PageDataFetcher.fetch() calls ContainerManager.getInstance().getContainer() |
This helper enables forbidden server access to the singleton container. | Ban server use of PageDataFetcher.fetch(). Use explicit construction in PageQueries. |
PageQueries may use DI only if request-scoped and stateless (WEBSITE_DI_RULES.md) |
DashboardPageQuery.execute() uses ContainerManager.getInstance().getContainer() |
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, WEBSITE_RSC_PRESENTATION.md) |
Data fetch + categorization + inline template in a server route module: fetchProfileLeaguesData() and ProfileLeaguesTemplate() |
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) |
Local type differs from spec: PageQueryResult<TData> uses data and destination |
Breaks the documented contract (ok with dto, redirect with to). |
Standardize a single PageQueryResult type per WEBSITE_PAGE_QUERIES.md 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, VIEW_MODELS.md) |
Server route data types include ViewModels: TeamDetailData.team: TeamDetailsViewModel and are passed into Template wrapper: TeamDetailTemplateWrapper() |
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, VIEW_DATA.md) |
Template prop uses ViewModel instances: DriverRankingsTemplateProps.drivers: DriverLeaderboardItemViewModel[] |
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) |
PageQuery imports from lib/view-models/**: DashboardOverviewViewModelData |
lib/view-models/** is the client-only boundary (also where Presenters live per PRESENTERS.md). |
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) |
Example: DriverLeaderboardItemViewModel |
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) |
Example: LeagueRoleDisplay |
Display Objects are forbidden inside Templates. | Use Presenters/ViewModels to emit primitive strings into ViewData. |
Templates MUST NOT compute derived values (VIEW_DATA.md) |
Filtering/sorting occurs inside Template: drivers.filter() and sort(...) |
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) |
Hooks in templates: useMemo() 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, WEBSITE_GUARDRAILS.md) |
Example: toLocaleDateString() |
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, VIEW_DATA.md) |
Example: toLocaleString() |
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) |
formatDate() calls toLocaleDateString() |
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) |
Server route computes derived values via reduce/filter/sort: computeDerivedData() |
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) |
Example: TeamSummaryViewModel |
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, FORM_SUBMISSION.md) |
Client component performs POST: fetch('/api/auth/logout', { method: 'POST' }) |
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, CLIENT_STATE.md) |
Stateful blockers inside a service: submitBlocker and throttle |
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.tsxLeagueDetailTemplate.tsxLeagueStandingsTemplate.tsxTeamDetailTemplate.tsx
Server routes importing ViewModels and or doing formatting/filtering (forbidden/discouraged)
Structural drift vs canonical lib/* layout
- API client exists at root
lib/instead oflib/api/:apiClient.ts - Non-canonical
lib/page/exists:PageDataFetcher.ts