Files
gridpilot.gg/docs/architecture/website/WEBSITE_GUARDRAILS.md
2026-01-12 01:01:49 +01:00

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.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.