249 lines
8.8 KiB
Markdown
249 lines
8.8 KiB
Markdown
# Website Guardrails (Mandatory)
|
|
|
|
This document defines architecture guardrails that must be enforced via tests + ESLint.
|
|
|
|
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
|
|
|
|
Purpose:
|
|
|
|
- Encode the architecture as *enforceable* rules.
|
|
- Remove ambiguity and prevent drift.
|
|
- Make it impossible for `page.tsx` and Templates to accumulate business logic.
|
|
|
|
## 1) RSC boundary guardrails
|
|
|
|
Fail CI if any `apps/website/app/**/page.tsx`:
|
|
|
|
- imports from `apps/website/lib/view-models/*`
|
|
- imports from Presenter code (presenters live colocated with ViewModels)
|
|
- calls `Intl.*` or `toLocale*`
|
|
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
|
|
|
|
Also fail CI if any `apps/website/app/**/page.tsx`:
|
|
|
|
- imports from `apps/website/lib/display-objects/**`
|
|
- imports from `apps/website/lib/services/**` **that are not explicitly server-safe**
|
|
- imports from `apps/website/lib/di/**` (server DI ban)
|
|
- defines local helper functions other than trivial `assert*`/`invariant*` guards
|
|
- contains `new SomeClass()` (object graph construction belongs in PageQueries)
|
|
- contains any of these calls (directly or indirectly):
|
|
- `ContainerManager.getInstance()`
|
|
- `ContainerManager.getContainer()`
|
|
|
|
Filename rules (route module clarity):
|
|
|
|
- Only `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `actions.ts` are allowed under `apps/website/app/**`.
|
|
- Fail CI if any file under `apps/website/app/**` matches:
|
|
- `*Template.tsx`
|
|
- `*ViewModel.ts`
|
|
- `*Presenter.ts`
|
|
|
|
Allowed exception:
|
|
|
|
- `apps/website/app/<route>/actions.ts` may call services and API clients (server-side), but it must not import ViewModels or Presenters.
|
|
|
|
## 2) Template purity guardrails
|
|
|
|
Fail CI if any `apps/website/templates/**`:
|
|
|
|
- imports from `apps/website/lib/view-models/*`
|
|
- imports from presenter code (presenters live colocated with ViewModels)
|
|
- imports from `apps/website/lib/display-objects/*`
|
|
- calls `Intl.*` or `toLocale*`
|
|
|
|
Also fail CI if any Template:
|
|
|
|
- contains `useMemo`, `useEffect`, `useState`, `useReducer` (state belongs in `*PageClient.tsx` and components)
|
|
- calls `.filter`, `.sort`, `.reduce` (derived computations must happen before ViewData reaches Templates)
|
|
- imports from:
|
|
- `apps/website/lib/page-queries/**`
|
|
- `apps/website/lib/services/**`
|
|
- `apps/website/lib/api/**`
|
|
- `apps/website/lib/di/**`
|
|
- `apps/website/lib/contracts/**`
|
|
|
|
Templates accept ViewData only.
|
|
|
|
Filename + signature rules:
|
|
|
|
- Template filenames must end with `Template.tsx`.
|
|
- The first parameter type of a Template component must be `*ViewData` (or an object containing only `*ViewData` shapes).
|
|
- Templates must not export helper functions.
|
|
|
|
## 3) Display Object guardrails
|
|
|
|
Fail CI if any `apps/website/lib/display-objects/**`:
|
|
|
|
- calls `Intl.*` or `toLocale*`
|
|
|
|
Also fail CI if any Display Object:
|
|
|
|
- imports from `apps/website/lib/api/**`, `apps/website/lib/services/**`, or `apps/website/lib/page-queries/**` (no IO)
|
|
- imports from `apps/website/lib/view-models/**` (direction must be Presenter/ViewModel -> DisplayObject, not vice versa)
|
|
- exports non-class members (Display Objects must be class-based)
|
|
|
|
Display Objects must be deterministic.
|
|
|
|
## 4) Page Query guardrails (server composition only)
|
|
|
|
Fail CI if any `apps/website/lib/page-queries/**`:
|
|
|
|
- imports from `apps/website/lib/view-models/**`
|
|
- imports from `apps/website/lib/display-objects/**`
|
|
- imports from `apps/website/lib/di/**` or references `ContainerManager`
|
|
- calls `Intl.*` or `toLocale*`
|
|
- calls `.sort`, `.filter`, `.reduce` (sorting/filtering belongs in API if canonical; otherwise client ViewModel)
|
|
- returns `null` (must return `PageQueryResult` union)
|
|
|
|
Filename rules:
|
|
|
|
- PageQueries must be named `*PageQuery.ts`.
|
|
- Page DTO types must be named `*PageDto` and live next to their PageQuery.
|
|
|
|
## 5) Services guardrails (DTO-only, server-safe)
|
|
|
|
Fail CI if any `apps/website/lib/services/**`:
|
|
|
|
- imports from `apps/website/lib/view-models/**` or `apps/website/templates/**`
|
|
- imports from `apps/website/lib/display-objects/**`
|
|
- stores state on `this` other than injected dependencies (services must be stateless)
|
|
- uses blockers (blockers are client-only UX helpers)
|
|
|
|
Naming rules:
|
|
|
|
- Service methods returning API responses should use variable name `apiDto`.
|
|
- Service methods returning Page DTO should use variable name `pageDto`.
|
|
|
|
## 6) Client-only guardrails (ViewModels, Presenters)
|
|
|
|
Fail CI if any file under `apps/website/lib/view-models/**`:
|
|
|
|
- lacks `'use client'` at top-level when it exports a ViewModel class intended for instantiation
|
|
- imports from `apps/website/lib/page-queries/**` or `apps/website/app/**` (dependency direction violation)
|
|
|
|
Fail CI if any Presenter/ViewModel uses:
|
|
|
|
- HTTP calls (`fetch`, axios, API clients)
|
|
|
|
## 7) Write boundary guardrails (Server Actions only)
|
|
|
|
Fail CI if any client module (`'use client'` file or `apps/website/components/**`) performs HTTP writes:
|
|
|
|
- `fetch` with method `POST|PUT|PATCH|DELETE`
|
|
|
|
Fail CI if any server action (`apps/website/app/**/actions.ts`):
|
|
|
|
- imports from `apps/website/lib/view-models/**` or `apps/website/templates/**`
|
|
- returns ViewModels (must return primitives / redirect / revalidate)
|
|
|
|
## 8) Model taxonomy guardrails (naming + type suffixes)
|
|
|
|
Problem being prevented:
|
|
|
|
- Calling everything “dto” collapses API Transport DTO, Page DTO, and ViewData.
|
|
- This causes wrong-layer dependencies and makes reviews error-prone.
|
|
|
|
Fail CI if any file under `apps/website/**` contains a variable named exactly:
|
|
|
|
- `dto`
|
|
|
|
Allowed variable names (pick the right one):
|
|
|
|
- `apiDto` (API Transport DTO from OpenAPI / backend HTTP)
|
|
- `pageDto` (Page DTO assembled by PageQueries)
|
|
- `viewData` (Template input)
|
|
- `commandDto` (write intent)
|
|
|
|
Type naming rules (CI should fail if violated):
|
|
|
|
1. Any PageQuery output type MUST end with `PageDto`.
|
|
- Applies to types defined in `apps/website/lib/page-queries/**`.
|
|
|
|
2. Any Template prop type MUST end with `ViewData`.
|
|
- Applies to types used by `apps/website/templates/**`.
|
|
|
|
3. API Transport DTO types may end with `DTO` (existing generated convention) or `ApiDto` (preferred for hand-written).
|
|
|
|
Module boundary reinforcement:
|
|
|
|
- `apps/website/templates/**` MUST NOT import API Transport DTO types directly.
|
|
- Prefer: PageQuery emits `pageDto` → Presenter emits `viewData`.
|
|
|
|
## 9) Contracts enforcement (mandatory interfaces)
|
|
|
|
Purpose:
|
|
|
|
- Guardrails that rely on regex alone will always have loopholes.
|
|
- Contracts make the compiler enforce architecture: code must implement the right shapes.
|
|
|
|
These contracts live under:
|
|
|
|
- `apps/website/lib/contracts/**`
|
|
|
|
### 9.1 Required contracts
|
|
|
|
Fail CI if any of these are missing:
|
|
|
|
1. PageQuery contract: `apps/website/lib/contracts/page-queries/PageQuery.ts`
|
|
- Requires `execute(...) -> PageQueryResult<PageDto>`.
|
|
|
|
2. Service contract(s): `apps/website/lib/contracts/services/*`
|
|
- Services return `ApiDto`/`PageDto` only.
|
|
- No ViewModels.
|
|
|
|
3. Presenter contract: `apps/website/lib/contracts/presenters/Presenter.ts`
|
|
- `present(input) -> output` (pure, deterministic).
|
|
|
|
4. ViewModel base: `apps/website/lib/contracts/view-models/ViewModel.ts`
|
|
- ViewModels are client-only.
|
|
- Must not expose a method that returns Page DTO or API DTO.
|
|
|
|
### 9.2 Enforcement rules
|
|
|
|
Fail CI if:
|
|
|
|
- Any file under `apps/website/lib/page-queries/**` defines a `class *PageQuery` that does NOT implement `PageQuery`.
|
|
- Any file under `apps/website/lib/services/**` defines a `class *Service` that does NOT implement a Service contract.
|
|
- Any file under `apps/website/lib/view-models/**` defines a `*Presenter` that does NOT implement `Presenter`.
|
|
|
|
Additionally:
|
|
|
|
- Fail if a PageQuery returns a shape that is not `PageQueryResult`.
|
|
- Fail if a service method returns a `*ViewModel` type.
|
|
|
|
Note:
|
|
|
|
- Enforcement can be implemented as a boundary test that parses TypeScript files (or a regex-based approximation as a first step), but the source of truth is: contracts must exist and be implemented.
|
|
|
|
## 10) Generated DTO isolation (OpenAPI transport types do not reach UI)
|
|
|
|
Purpose:
|
|
|
|
- Generated OpenAPI DTOs are transport contracts.
|
|
- UI must not depend on transport contracts directly.
|
|
- Prevents “DTO soup” and forces the PageDto/ViewData boundary.
|
|
|
|
Fail CI if any of these import from `apps/website/lib/types/generated/**`:
|
|
|
|
- `apps/website/templates/**`
|
|
- `apps/website/components/**`
|
|
- `apps/website/hooks/**` and `apps/website/lib/hooks/**`
|
|
|
|
Fail CI if any Template imports from `apps/website/lib/types/**`.
|
|
|
|
Allowed locations for generated DTO imports:
|
|
|
|
- `apps/website/lib/api/**` (API clients)
|
|
- `apps/website/lib/services/**` (transport orchestration)
|
|
- `apps/website/lib/page-queries/**` (Page DTO assembly)
|
|
|
|
Enforced flow:
|
|
|
|
- Generated `*DTO` -> `apiDto` (API client/service)
|
|
- `apiDto` -> `pageDto` (PageQuery)
|
|
- `pageDto` -> `viewData` (Presenter)
|
|
|
|
Rationale:
|
|
|
|
- If the API contract changes, the blast radius stays in infrastructure + server composition, not in Templates.
|