# 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//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`. 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.