8.8 KiB
Website Guardrails (Mandatory)
This document defines architecture guardrails that must be enforced via tests + ESLint.
Authoritative contract: WEBSITE_CONTRACT.md.
Purpose:
- Encode the architecture as enforceable rules.
- Remove ambiguity and prevent drift.
- Make it impossible for
page.tsxand 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.*ortoLocale* - 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.tsare allowed underapps/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.tsmay 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.*ortoLocale*
Also fail CI if any Template:
- contains
useMemo,useEffect,useState,useReducer(state belongs in*PageClient.tsxand 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*ViewDatashapes). - Templates must not export helper functions.
3) Display Object guardrails
Fail CI if any apps/website/lib/display-objects/**:
- calls
Intl.*ortoLocale*
Also fail CI if any Display Object:
- imports from
apps/website/lib/api/**,apps/website/lib/services/**, orapps/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 referencesContainerManager - calls
Intl.*ortoLocale* - calls
.sort,.filter,.reduce(sorting/filtering belongs in API if canonical; otherwise client ViewModel) - returns
null(must returnPageQueryResultunion)
Filename rules:
- PageQueries must be named
*PageQuery.ts. - Page DTO types must be named
*PageDtoand 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/**orapps/website/templates/** - imports from
apps/website/lib/display-objects/** - stores state on
thisother 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/**orapps/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:
fetchwith methodPOST|PUT|PATCH|DELETE
Fail CI if any server action (apps/website/app/**/actions.ts):
- imports from
apps/website/lib/view-models/**orapps/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):
-
Any PageQuery output type MUST end with
PageDto.- Applies to types defined in
apps/website/lib/page-queries/**.
- Applies to types defined in
-
Any Template prop type MUST end with
ViewData.- Applies to types used by
apps/website/templates/**.
- Applies to types used by
-
API Transport DTO types may end with
DTO(existing generated convention) orApiDto(preferred for hand-written).
Module boundary reinforcement:
apps/website/templates/**MUST NOT import API Transport DTO types directly.- Prefer: PageQuery emits
pageDto→ Presenter emitsviewData.
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:
-
PageQuery contract:
apps/website/lib/contracts/page-queries/PageQuery.ts- Requires
execute(...) -> PageQueryResult<PageDto>.
- Requires
-
Service contract(s):
apps/website/lib/contracts/services/*- Services return
ApiDto/PageDtoonly. - No ViewModels.
- Services return
-
Presenter contract:
apps/website/lib/contracts/presenters/Presenter.tspresent(input) -> output(pure, deterministic).
-
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 aclass *PageQuerythat does NOT implementPageQuery. - Any file under
apps/website/lib/services/**defines aclass *Servicethat does NOT implement a Service contract. - Any file under
apps/website/lib/view-models/**defines a*Presenterthat does NOT implementPresenter.
Additionally:
- Fail if a PageQuery returns a shape that is not
PageQueryResult. - Fail if a service method returns a
*ViewModeltype.
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/**andapps/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.