/types.ts`)
Adapté aux besoins d'un écran spécifique.
```typescript
// features/products-list/types.ts
export interface ProductListItemView {
id: string;
title: string;
formattedPrice: string; // Ex: "49,00 €"
badgeTone: "success" | "warning" | "danger";
isOutOfStock: boolean;
}
```
### Les Props de Composant (`Composant.tsx`)
Seul type autorisé directement dans un fichier `.tsx`, décrivant uniquement les paramètres d'entrée du composant.
```tsx
// features/products-list/ProductCard.tsx
import type { ProductListItemView } from "./types";
interface ProductCardProps {
product: ProductListItemView;
onSelect: (productId: string) => void;
className?: string;
}
export function ProductCard({ product, onSelect, className }: ProductCardProps) {
return (
onSelect(product.id)}>
{product.title}
{product.formattedPrice}
);
}
```
---
## Règle du Pragmatisme Typé Maedow Arch (Anti Over-Engineering)
1. **Cas Simple (1:1)** : Si la vue utilise exactement les mêmes champs que l'entité du domaine, utilisez un type alias direct ou `Pick` :
```typescript
// features/user-details/types.ts
import type { User } from "@/core/users/types";
// ✅ Pas de duplication inutile de champs
export type UserDetailsView = Pick;
```
2. **Inférence Zod Systématique** : Ne définissez jamais une interface manuelle en double d'un schéma Zod. Utilisez `z.infer`.
3. **Types de Formulaires** : Dérivez les types de formulaires depuis le schéma de validation du domaine (`type FormValues = z.infer`).
---
## Matrice de Localisation des Fichiers Maedow Arch
| Type de Donnée / Logique | Extension | Dossier Cible | Exemple de Fichier |
| :--- | :--- | :--- | :--- |
| **Entités du Domaine** | `.ts` | `core//` | `core/user/types.ts` |
| **Contrats de Services / Repositories** | `.ts` | `core//` | `core/user/repository.contract.ts` |
| **Schémas de Validation (Zod/Valibot)** | `.ts` | `core//` | `core/user/validation.ts` |
| **Machines d'États & Réducteurs Purs** | `.ts` | `core//` | `core/cart/reducer.ts` |
| **ViewModels Spécifiques à un Écran** | `.ts` | `features//` | `features/checkout/types.ts` |
| **Composants Métier Partagés** | `.tsx` | `features/_shared/` | `features/_shared/UserAvatar.tsx` |
| **Hooks React Métier** | `.ts` | `features//hooks/` | `features/checkout/hooks/useCheckout.ts` |
| **Composants d'Interface (Rendu)** | `.tsx` | `features//` | `features/checkout/CheckoutForm.tsx` |
---
## Exemple Comparatif Avant / Après
### ❌ Anti-Pattern : Modèle mélangé dans le JSX
```tsx
// features/profile/UserProfile.tsx
import React, { useState } from "react";
// ❌ Définition métier dans le composant d'UI
export interface UserAccount {
id: string;
email: string;
balance: number;
}
export function UserProfile() {
const [user, setUser] = useState(null);
return {user?.email}
;
}
```
### ✅ Clean Pattern Maedow Arch : Découplage complet
1. **Modèle de Domaine (`core/user/types.ts`)** :
```typescript
export interface UserAccount {
id: string;
email: string;
balanceCents: number;
}
```
2. **Hook & ViewModel (`features/profile/hooks/useUserProfile.ts`)** :
```typescript
import { useState } from "react";
import type { UserAccount } from "@/core/user/types";
export interface UserProfileView {
email: string;
formattedBalance: string;
isVip: boolean;
}
export function useUserProfile(userId: string) {
const [profile, setProfile] = useState(null);
// Logique de chargement et transformation du domaine vers la vue
return { profile, isLoading: false };
}
```
3. **Composant Visuel Pur (`features/profile/UserProfile.tsx`)** :
```tsx
import type { UserProfileView } from "./hooks/useUserProfile";
interface UserProfileProps {
profile: UserProfileView;
}
export function UserProfile({ profile }: UserProfileProps) {
return (
{profile.email}
Solde : {profile.formattedBalance}
{profile.isVip && VIP}
);
}
```
======================================================================
# Conventions, typage strict et Result Pattern
======================================================================
> **Standards de Développement et Règles de Qualité Logicielle sous Maedow Arch**
> Ce document définit les conventions applicables à tout projet suivant les principes de Maedow Arch (*maedow-arch*).
---
## Règle d'Or Maedow Arch : Isolation des Responsabilités
1. **Les fichiers `.tsx` ne contiennent que du code de présentation** (JSX, layout, binding d'événements UI légers).
2. **Les fichiers `.ts` encapsulent toute la logique** (types, transformations, appels de services, validateurs, hooks).
3. **Les composants ne font pas d'appels directs à des bases de données ou API tierces** : ils consomment un hook ou un service d'abstraction.
---
## Conventions de Nommage et Extensions Maedow Arch
| Type d'Élément | Format | Extension | Exemple |
| :--- | :--- | :--- | :--- |
| **Composant React d'Écran** | PascalCase | `.tsx` | `OrderSummaryCard.tsx` |
| **Composant Métier Partagé** | PascalCase | `.tsx` | `features/_shared/AddressPicker.tsx` |
| **Hook React** | camelCase (`use*`) | `.ts` | `useOrderPayment.ts` |
| **Entité / Types Métier** | camelCase | `.ts` | `types.ts`, `entities.ts` |
| **Service / Use Case** | camelCase | `.ts` | `calculateDiscount.ts`, `orderService.ts` |
| **Schéma de Validation** | camelCase | `.ts` | `order.schema.ts`, `validation.ts` |
| **Contrat d'Interface (Port)** | camelCase | `.ts` | `payment.contract.ts` |
| **Adaptateur d'Infrastructure** | camelCase (`*.adapter.ts` ou `*.repository.ts`) | `.ts` | `stripePayment.adapter.ts`, `postgresOrder.repository.ts` |
| **Test** | le nom de ce qu'il teste (`*.test.ts` ou `*.test.tsx`) | `.ts` / `.tsx` | `result.test.ts`, `CheckoutScreen.test.tsx` |
---
## Typage et TypeScript Strict sous Maedow Arch
Pour éviter les erreurs silencieuses à l'exécution, les règles suivantes sont imposées :
```json
// tsconfig.json (Extrait recommandé)
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
```
### Règles de programmation :
* ⛔ **`any` est strictement interdit** : Utiliser `unknown` avec des gardes de types ou de la validation Zod/Valibot à l'entrée des frontières.
* ⛔ **Double assertion interdite (`as unknown as TargetType`)** : Ne forcez pas les types, validez-les au moment du parsing.
* ✅ **Inférence de types Zod (`z.infer`)** : Dérivez systématiquement les types de DTOs depuis les schémas de validation plutôt que de les dupliquer.
* ✅ **Branded Types pour les identifiants critiques** : Pour éviter d'inverser accidentellement deux IDs de même type primitif (ex: `userId` et `organizationId`), utiliser des types opaques/marqués :
```typescript
export type UserId = string & { readonly __brand: unique symbol };
export type OrganizationId = string & { readonly __brand: unique symbol };
```
---
### Vivre avec `exactOptionalPropertyTypes`
C'est l'option de `strict` qui saute en premier dans un vrai projet, et sa désactivation entraîne rarement qu'elle seule : on la retire un vendredi soir, et le reste du strict suit dans les semaines qui viennent.
Elle distingue deux choses que TypeScript confondait : **une propriété absente** et **une propriété présente valant `undefined`**. `{ auteur?: string }` accepte l'objet sans la clé, et refuse `{ auteur: undefined }`.
C'est ce que vous voulez. Les deux ne se comportent pas pareil devant `Object.keys`, devant un `JSON.stringify`, devant un `PATCH` partiel ou devant une base de données, où l'un ne touche pas la colonne et l'autre l'efface.
#### Reconnaître l'erreur
Deux codes, selon l'endroit :
| Code | Situation |
| :--- | :--- |
| `TS2375` | vous **affectez** : retour de fonction, variable typée, props JSX |
| `TS2379` | vous **passez en argument** à une fonction |
Le message est le même dans les deux cas, et sa dernière phrase est un piège :
```
error TS2375: Type '{ recherche: string | undefined; page: number; }'
is not assignable to type 'Filtre' with 'exactOptionalPropertyTypes: true'.
Consider adding 'undefined' to the types of the target's properties.
```
Le conseil de TypeScript, ajouter `undefined` au type cible, n'est le bon que dans un cas sur deux : celui où la cible vous appartient.
#### Le type vous appartient : dites-le explicitement
```typescript
// ❌ error TS2375
type Filtre = { recherche?: string; page: number };
export function construire(recherche: string | undefined): Filtre {
return { recherche, page: 1 };
}
```
```typescript
// ✅ La clé peut être absente, ou présente et indéfinie. Les deux sont voulus.
type Filtre = { recherche?: string | undefined; page: number };
export function construire(recherche: string | undefined): Filtre {
return { recherche, page: 1 };
}
```
`?: string | undefined` n'est pas un pléonasme sous cette option, c'est la façon d'exprimer que les deux formes conviennent. Écrivez-la quand c'est vrai, et laissez `?: string` quand la clé ne doit vraiment pas exister.
#### Le type ne vous appartient pas : n'écrivez pas la clé
C'est le cas des SDK tiers, dont les types ont été écrits sans cette option.
```typescript
// ❌ error TS2379
declare function envoyer(options: { url: string; token?: string }): Promise;
export async function appeler(token: string | undefined) {
await envoyer({ url: "https://exemple.test", token });
}
```
```typescript
// ✅ La clé n'apparaît que si elle a une valeur
export async function appeler(token: string | undefined) {
await envoyer({ url: "https://exemple.test", ...(token !== undefined && { token }) });
}
```
Le `&&` rend `false` quand la condition est fausse, et répandre `false` ne produit aucune clé.
#### Effacer une propriété, c'est la retirer
```typescript
// ❌ error TS2375 : `{ auteur: undefined }` n'est pas `{}`
export function anonymiser(brouillon: Brouillon): Brouillon {
return { ...brouillon, auteur: undefined };
}
```
```typescript
// ✅ La clé disparaît vraiment
export function anonymiser(brouillon: Brouillon): Brouillon {
const { auteur: _retire, ...reste } = brouillon;
return reste;
}
```
Cette erreur est la plus utile des quatre : elle signale un endroit où votre code croyait effacer une donnée alors qu'il écrivait `undefined` dedans. La différence est réelle dès qu'un `PATCH` ou un ORM lit cet objet.
#### En React, les deux cas se présentent
Si le composant est à vous, appliquez le premier remède : `couleur?: string | undefined`.
S'il vient d'une bibliothèque, l'attribut absent remplace l'attribut indéfini :
```tsx
// ✅ Deux appels plutôt qu'un attribut à undefined
export function Etiquette({ couleurChoisie }: { couleurChoisie: string | undefined }) {
return couleurChoisie === undefined ? (
) : (
);
}
```
#### La limite : quand renoncer localement
Les remèdes ci-dessus supposent un ou deux champs facultatifs. Devant un client d'API qui en compte six, le remède du spread conditionnel donne ceci :
```typescript
// Correct, et illisible
return requete({
url: e.url,
...(e.methode !== undefined && { methode: e.methode }),
...(e.entete !== undefined && { entete: e.entete }),
...(e.corps !== undefined && { corps: e.corps }),
...(e.delai !== undefined && { delai: e.delai }),
...(e.reprises !== undefined && { reprises: e.reprises }),
});
```
Le bon geste est alors de renoncer **localement**, une fois, et de le dire :
```typescript
function sansIndefinis(objet: T): T {
return Object.fromEntries(
Object.entries(objet).filter(([, valeur]) => valeur !== undefined)
) as T;
}
export function appelLisible(e: Entree) {
// Renoncement assumé : les six champs facultatifs de ce client devraient
// être conditionnés un par un. L'assertion couvre cette ligne et rien
// d'autre, et la fonction ci-dessus garantit ce qu'elle affirme.
return requete(sansIndefinis(e) as Parameters[0]);
}
```
**Une assertion confinée à trois lignes, commentée, adossée à une fonction qui fait réellement ce qu'elle prétend, vaut mieux qu'une option désactivée dans le `tsconfig.json`.** La première se relit et se retire le jour où le SDK corrige ses types ; la seconde emporte tout le fichier, puis tout le projet.
C'est la seule forme de renoncement que ce standard accepte : locale, visible, et argumentée à l'endroit où elle s'applique.
## Gestion des Erreurs : Données Typées vs Exceptions (Result Pattern)
Dans Maedow Arch, **les erreurs prévisibles et fonctionnelles sont modélisées comme des données**, pas comme des exceptions système.
### Le Pattern Résultat (Discriminated Union) :
```typescript
// core/common/result.ts
export type Result =
| { ok: true; data: TData }
| { ok: false; error: TError };
// Exemple d'usage métier
export type CheckoutFailure =
| { kind: "insufficient-funds"; amountMissing: number }
| { kind: "item-out-of-stock"; itemId: string }
| { kind: "payment-gateway-timeout" };
export async function processCheckout(orderId: string): Promise> {
// Traitement...
if (stockIssue) {
return { ok: false, error: { kind: "item-out-of-stock", itemId: "123" } };
}
return { ok: true, data: { transactionId: "tx_abc" } };
}
```
### Helpers Obligatoires pour le Result Pattern (Anti-Verbosité)
Sans helpers, le Result Pattern devient rapidement plus verbeux que les exceptions qu'il remplace (`if (!result.ok) { ... }` répété partout). Tout projet Maedow Arch doit définir ces helpers dès l'introduction du pattern :
```typescript
// core/common/result.ts (suite)
// Extrait la donnée ou retourne une valeur par défaut
export function unwrapOr(result: Result, fallback: TData): TData {
return result.ok ? result.data : fallback;
}
// Transforme la donnée si succès, propage l'erreur telle quelle
export function mapResult(
result: Result,
fn: (data: TData) => TMapped
): Result {
return result.ok ? { ok: true, data: fn(result.data) } : result;
}
// Pattern-matching explicite, aucune branche n'est oubliée
export function match(
result: Result,
handlers: { ok: (data: TData) => TReturn; err: (error: TError) => TReturn }
): TReturn {
return result.ok ? handlers.ok(result.data) : handlers.err(result.error);
}
// Enchaîne une opération faillible sur le succès de la précédente
export async function andThen(
result: Result | Promise>,
fn: (data: TData) => Result | Promise>
): Promise> {
const resolu = await result;
return resolu.ok ? fn(resolu.data) : resolu;
}
// Agrège une liste de résultats, en s'arrêtant à la première erreur
export function all(results: Result[]): Result {
const donnees: TData[] = [];
for (const result of results) {
if (!result.ok) return result;
donnees.push(result.data);
}
return { ok: true, data: donnees };
}
```
### Enchaîner plusieurs opérations faillibles
C'est le point où le Result Pattern tient ou s'écroule, et c'est celui qu'on documente le moins.
`mapResult` transforme une donnée, mais son résultat n'est pas faillible. Un service qui enchaîne trois appels pouvant chacun échouer retombe donc sur l'imbrication que le pattern devait supprimer :
```typescript
// ❌ Ce que le Result Pattern était censé faire disparaître
const commande = await trouverCommande(id);
if (!commande.ok) return commande;
const autorisee = await autoriserPaiement(commande.data);
if (!autorisee.ok) return autorisee;
const encaissee = await encaisser(autorisee.data);
if (!encaissee.ok) return encaissee;
return { ok: true, data: encaissee.data };
```
`andThen` enchaîne, et court-circuite à la première erreur :
```typescript
// ✅ Trois étapes faillibles, une seule expression
export async function payerCommande(id: string): Promise> {
return andThen(andThen(trouverCommande(id), autoriserPaiement), encaisser);
}
```
Chaque étape reçoit la donnée de la précédente et rend un `Result`. Dès qu'une échoue, les suivantes ne sont pas appelées et l'erreur remonte telle quelle, sans être enveloppée ni traduite.
`all` sert le cas parallèle plutôt que séquentiel, typiquement une validation :
```typescript
// ✅ Valider une liste, et s'arrêter à la première ligne fautive
const lignes = panier.map(validerLigne);
const validees = all(lignes);
if (!validees.ok) return validees; // la première erreur, telle quelle
```
**Pourquoi `andThen` est asynchrone même quand l'étape ne l'est pas.** Deux variantes, l'une synchrone et l'autre non, obligeraient à choisir à chaque appel selon ce que fait l'étape suivante, c'est-à-dire à connaître son implémentation. Rendre une opération asynchrone cesserait alors d'être un détail interne et deviendrait une rupture de contrat pour tous ses appelants.
**Exemple d'usage recommandé** (à documenter systématiquement dans le README d'un projet Maedow Arch) :
```typescript
const checkoutResult = await processCheckout(orderId);
match(checkoutResult, {
ok: ({ transactionId }) => redirectToConfirmation(transactionId),
err: (error) => showCheckoutError(error),
});
```
### Par où les exceptions entrent, et où elles s'arrêtent
Les cinq helpers ci-dessus supposent tous que ce qu'ils manipulent est un `Result`. Reste une question que le pattern ne résout pas de lui-même : **le monde extérieur, lui, lève.** Un client de base de données jette sur une contrainte violée, un `fetch` jette sur un délai dépassé, et aucune de ces exceptions n'est un `Result`.
Sans convention, chaque dépôt improvise son `try/catch`, et il suffit d'un oubli pour qu'une exception traverse un `andThen` qui croyait n'enchaîner que des résultats. La chaîne est court-circuitée, et le service qui l'appelait n'a jamais vu passer l'erreur qu'il pensait gérer.
`fromThrowable` est la porte d'entrée, et elle a **un seul endroit légitime** :
```typescript
// core/orders/repository.ts
import { fromThrowable, type Result } from "@/core/common/result";
type ErreurDepot = { kind: "indisponible" } | { kind: "conflit"; champ: string };
export function enregistrerCommande(
commande: Commande
): Promise> {
return fromThrowable(
() => db.order.create({ data: commande }),
(cause) => (estConflitUnicite(cause) ? { kind: "conflit", champ: "reference" } : { kind: "indisponible" })
);
}
```
**Dans `repository.ts`, jamais plus haut.** C'est la frontière entre ce qui parle au monde extérieur et le domaine, et c'est le seul endroit où une exception a encore un sens. Au-dessus, `core//service.ts` n'a plus à se demander si un appel peut lever : il reçoit un `Result` et compose.
La fonction de conversion est obligatoire, et c'est délibéré. Elle force à décider ce que l'erreur signifie **pour le métier** : un `catch` qui rendrait l'exception telle quelle ferait remonter un objet de la bibliothèque jusqu'aux écrans, et le jour où l'on change de client de base de données, c'est la couche de rendu qui casse.
---
## Sécurité & Gestion des Données Sensibles
1. **Isolation Serveur / Client** :
* Tout module manipulant des clés API secrètes, des tokens d'administration ou des accès directs à la base de données doit être restreint au serveur (`import "server-only"` ou convention `.server.ts`).
2. **Zéro Secret dans les Logs** :
* Ne jamais logger d'objets bruts pouvant contenir des en-têtes d'autorisation, des mots de passe ou des tokens d'authentification.
* Utiliser une allowlist d'en-têtes et de propriétés autorisées pour les rapports d'incidents.
3. **Validation Systématique aux Frontières** :
* Toute entrée externe (requêtes HTTP, formulaires, webhooks, paramètres d'URL) doit être validée via un parseur de schéma (ex: Zod) avant d'atteindre le cœur de domaine.
4. **Sérialisation Décrite en Sortie** :
* Ce qui part vers le client est décrit par un schéma, jamais laissé au hasard de la forme de l'entité. Voir [Le DTO de Sortie](./models.md) dans `models.md`.
* La règle 2 protège les journaux, celle-ci protège les réponses. C'est la seconde qui fuit le plus souvent : un `return user` renvoie l'entité entière, rien n'échoue, et le champ interne ajouté ce matin part vers le client le jour même.
* Une liste d'inclusions, jamais une liste d'exclusions. Un `Omit` laisse passer par défaut et n'exclut que ce dont on s'est souvenu : il est en retard d'un champ en permanence.
---
## Générateurs de Code & Productivité (Scaffolding Maedow Arch)
Pour réduire le coût de création de fichiers liés au découplage de Maedow Arch, configurez un script de génération dans votre `package.json` :
```json
// package.json
{
"scripts": {
"generate:feature": "node scripts/scaffold-feature.mjs",
"generate:domain": "node scripts/scaffold-domain.mjs"
}
}
```
Ce script initialise automatiquement :
* `features//Screen.tsx`
* `features//types.ts`
* `features//hooks/use.ts`
* `features//.test.tsx`
---
## Pyramide de Tests et Testabilité
### Où vivent les tests
**À côté de ce qu'ils testent, toujours.** `core/billing/billing.test.ts` accompagne `core/billing/service.ts`, et `features/checkout/Checkout.test.tsx` accompagne son écran.
Ce n'est pas un goût de rangement. Un test séparé de son sujet se met à mentir dès le premier déplacement de fichier : il continue de passer alors qu'il teste une version antérieure du comportement, ou il est oublié lors d'une suppression et devient un test orphelin qui vérifie du code mort. Colocalisé, il se déplace, se relit et se supprime avec ce qu'il couvre.
C'est aussi ce que produisent les deux générateurs : `generate:domain` et `generate:feature` amorcent chacun leur test au bon endroit.
Un projet Maedow Arch ne livre donc **aucun dossier `tests/`**. Une arborescence par nature de test, `unit/`, `integration/`, `e2e/`, oblige à ranger avant d'écrire, et la question « ceci est-il unitaire ou d'intégration ? » n'a aucune réponse utile pour un service de domaine qui appelle deux fonctions pures.
### Ce que chaque couche appelle un test
La pyramide de Maedow Arch ne se lit pas en pourcentages, elle se lit en **coût d'exécution**. Chaque couche se teste par le moyen le moins cher qui prouve quelque chose.
| Couche | Ce qu'on teste | Ce dont on n'a pas besoin |
| :--- | :--- | :--- |
| `core/` | les règles métier, les transitions d'état, les contrats | ni DOM, ni mock, ni rendu |
| `lib/` | les fonctions d'aide, sur leurs cas limites | rien non plus |
| `components/` | le rendu d'une primitive à partir de ses props | un moteur de rendu, sans données métier |
| `features/` | la logique de vue dans son hook, puis le parcours de l'écran | pour le hook, aucun écran ; pour le parcours, un moteur de rendu et rien de simulé côté domaine |
| `app/` | l'assemblage, de bout en bout, sur les chemins critiques seulement | un navigateur, donc le test le plus cher |
La base large de la pyramide n'est pas une consigne de quantité, c'est une conséquence : **si le domaine est pur, ses tests sont si rapides et si simples à écrire qu'ils deviennent naturellement les plus nombreux.** Une pyramide qui ne se remplit pas par le bas signale que la logique a fui vers le haut, dans les écrans ou dans les routes, ce que la règle « zéro modèle dans le JSX » cherche précisément à empêcher.
Reste à savoir où la remettre quand elle a déjà fui. Un écran qui porte plus de trois états, ou qui déclenche un appel réseau, ne se teste pas sans monter un arbre React : sa logique appartient à un hook de la feature, où elle redevient du TypeScript ordinaire. [`architecture.md`](./architecture.md) dit ce que `hooks/` reçoit, et donne la forme. C'est ce qui fait monter la pyramide au-dessus de `core/`, là où elle s'arrêtait.
### Ce que la pyramide ne dit pas
**Elle ne fixe aucun taux de couverture.** Une exigence de couverture chiffrée sur `core/` produit des tests écrits pour la métrique : on teste les accesseurs et on saute la règle de gestion, parce que la première ligne compte autant que la seconde dans le calcul.
L'exigence utile est vérifiable autrement, et elle l'est déjà : **`core/` doit se tester sans DOM, sans mock et sans rendu.** Un test du domaine qui a besoin de simuler quelque chose signale que le domaine dépend de quelque chose dont il ne devrait pas dépendre, et c'est une violation de frontière avant d'être un problème de test.
### En Mode Light
La couche domaine n'existe pas, la base de la pyramide non plus. Les tests portent alors sur `lib/` et sur les écrans. C'est cohérent, et c'est même l'argument du profil : un site vitrine n'a pas de règles de gestion à protéger.
Le jour où il en acquiert, `generate:domain` fait naître `core/` avec son premier test, et la pyramide se remplit par le bas.
======================================================================
# Registre des règles, MA-001 à MA-009
======================================================================
Ce document recense les règles normatives du standard, leur donne un code stable, et dit pour chacune si elle est **vérifiée par la machine** ou **tenue par l'équipe**.
Il existe pour une raison simple : un standard qui laisse croire que tout est appliqué est moins crédible qu'un standard qui distingue clairement ce qu'il outille de ce qu'il demande. Les deux catégories ont leur place. Ce qui n'en a pas, c'est le flou entre les deux.
## Comment lire ce registre
**Vérifiée** signifie qu'une violation fait échouer le lint, donc la CI, avant d'atteindre la revue. La règle possède une fixture invalide qui échoue et une fixture valide qui passe : sans cette paire, une règle peut être chargée, valide, et ne rien regarder du tout, ce que ce dépôt a rencontré quatre fois et documente de [F-001](./FRICTIONS.md) à F-015.
**Tenue par l'équipe** signifie que la règle relève de la revue et de la discipline. Ce n'est pas un aveu d'échec, et ce n'est pas une étape en attendant mieux : certaines exigences ne se réduisent pas à un motif syntaxique sans produire plus de faux positifs que de vraies détections. La règle reste normative, et sa violation reste une violation.
Les deux règles qui restent dans cette catégorie, MA-008 et MA-009, **y sont par décision et non par manque de travail**. Chacune dit en quoi l'outiller coûterait plus qu'elle ne rapporte. Le rapport de sept règles vérifiées sur neuf n'est donc pas une dette à combler : c'est la part du standard qui se prête à la vérification automatique, et l'autre part est reconnue comme telle plutôt que passée sous silence.
## À quoi sert un code stable
Les messages de lint renvoyaient jusqu'ici vers un titre de section. Un titre se réécrit, et le lien casse en silence : c'est arrivé au commit `71c0a3f`, quand la numérotation des titres a été retirée du corpus.
Un code ne bouge pas. `MA-002` relie le message d'erreur, la section du corpus, la fixture qui la teste et l'entrée du journal, quelle que soit la façon dont chacun de ces textes évolue ensuite.
Les codes ne sont jamais réattribués. Une règle retirée laisse son code vacant plutôt que de le céder à une autre.
## Le registre
| Code | Règle | Statut | Où elle est appliquée |
| :--- | :--- | :--- | :--- |
| **MA-001** | Le flux `app → features → core → lib` est unidirectionnel | vérifiée | `boundaries/dependencies` |
| **MA-002** | Une feature n'importe jamais une autre feature | vérifiée | `boundaries/dependencies` |
| **MA-003** | `features/_shared/` ne connaît aucune feature | vérifiée | `boundaries/dependencies` |
| **MA-004** | Zéro fichier `.tsx` et zéro JSX dans `core/` | vérifiée | `no-restricted-syntax`, `no-restricted-imports` |
| **MA-005** | `any` interdit, `unknown` et gardes de type à la place | vérifiée | `@typescript-eslint/no-explicit-any` · entrée `strict` |
| **MA-006** | Double assertion `as unknown as` interdite | vérifiée | `no-restricted-syntax` · entrée `strict` |
| **MA-007** | Aucun cycle d'import entre modules | vérifiée | `import-x/no-cycle` · entrée `strict` |
| **MA-008** | Les modules à secrets sont marqués `server-only` | tenue par l'équipe | revue |
| **MA-009** | Les adaptateurs, dépôts et contrats suivent leur nommage | tenue par l'équipe | revue |
> **Ce tableau n'est pas la liste complète de ce que le standard demande.** Six autres exigences, les [Règles de conception](#les-règles-de-conception-distinctes-des-neuf-codes), n'ont pas de code parce qu'elles s'apprécient au lieu de se constater. Elles ne font échouer aucun lint, et c'est précisément ce qui les rend faciles à ignorer : un projet réel a passé dix-neuf lots sans appliquer la Règle de Logique Extraite, avec des écrans montés à 443 lignes, pendant que ces neuf codes restaient tous au vert.
---
## MA-001 · Le flux de dépendance est unidirectionnel
**Vérifiée.** `app` peut tout importer, une feature descend la pile, `core` ne connaît que `core` et `lib`, `components` reste présentationnel, et `lib` ne dépend que de lui-même. Le sens inverse est refusé.
Énoncé dans [architecture.md](./architecture.md), section « Règle de dépendance et frontières ».
Fixtures : `core/billing/render.ts` (core vers components), `core/billing/service.ts` (core vers feature) et `lib/bad.ts` (lib vers core) dans `packages/eslint-config-maedow-arch/test/fixtures/invalid/`.
## MA-002 · Une feature n'importe jamais une autre feature
**Vérifiée.** Les imports internes à une feature restent libres. Ce qu'une feature partage avec une autre passe par `features/_shared/`, par `core/` ou par `components/`, jamais en ligne directe.
Énoncé dans [architecture.md](./architecture.md), section « Règle de dépendance et frontières ».
Fixture : `features/checkout/Screen.tsx`.
## MA-003 · `features/_shared/` ne connaît aucune feature
**Vérifiée.** Le partagé est transverse par définition. S'il devait importer une feature, il n'aurait plus rien de transverse et deviendrait un fourre-tout, ce que la Règle de Dégradation décrit précisément.
Énoncé dans [architecture.md](./architecture.md), section « Composition de features et éléments partagés ».
Fixture : `features/_shared/Card.tsx`.
## MA-004 · Zéro `.tsx` et zéro JSX dans `core/`
**Vérifiée.** Deux règles la portent, et il en faut bien deux. Le runtime JSX automatique ne demande aucun import de React : un composant peut donc vivre dans `core/` sans qu'aucune règle d'import ne se déclenche, ce qui a été constaté avant d'écrire la règle. `no-restricted-imports` seul aurait donné l'illusion d'une protection, en n'attrapant que le cas devenu rare où quelqu'un écrit encore `import React from "react"`.
La règle vise le contenu, pas l'extension. Un fichier `.tsx` sans JSX dans `core/` ne casse rien, et un `.ts` ne peut pas en contenir : le parser le refuserait avant nous.
Énoncé dans [architecture.md](./architecture.md), tableau des couches : « **ZÉRO fichier `.tsx`**, aucun import React/DOM ».
Fixtures : `core/audit/Widget.tsx` pour le JSX sans import, `core/audit/useTheme.ts` pour la dépendance d'interface.
## MA-005 · `any` interdit
**Vérifiée**, dans l'entrée `strict`. `unknown` avec des gardes de type, ou une validation à l'entrée des frontières.
Énoncé dans [conventions.md](./conventions.md), section « TypeScript strict ».
Fixtures : `strict-invalid/src/core/billing/types.ts` et sa contrepartie valide, qui montre la garde de type attendue à la place.
## MA-006 · Double assertion interdite
**Vérifiée**, dans l'entrée `strict`. `as unknown as TargetType` force un type au lieu de le valider. La validation appartient au moment du parsing.
Aucune règle publiée ne vise cette forme, d'où un sélecteur qui cible l'assertion vers `unknown` dont le parent est une autre assertion. Un `as unknown` isolé reste permis : c'est l'enchaînement qui pose problème, pas le passage par `unknown`.
Énoncé dans [conventions.md](./conventions.md), section « TypeScript strict ».
Fixtures : `strict-invalid/src/core/billing/cast.ts` et sa contrepartie valide.
## MA-007 · Aucun cycle d'import entre modules
**Vérifiée**, dans l'entrée `strict`. Deux modules qui s'importent mutuellement ne peuvent plus être lus, testés ni déplacés séparément. Le cycle ne viole aucune frontière de couche, ce qui le rend invisible aux règles MA-001 à MA-003 : il se forme à l'intérieur d'une même couche.
Cette règle a été introduite par le registre. Le corpus ne l'énonçait pas, alors qu'elle conditionne la testabilité que le standard revendique.
Fixtures : `strict-invalid/src/core/billing/aller.ts` et `retour.ts`, qui forment un cycle sans franchir la moindre frontière.
## MA-008 · Les modules à secrets sont marqués `server-only`
**Tenue par l'équipe.** Tout module manipulant des clés d'API, des jetons d'administration ou un accès direct à la base doit être restreint au serveur, par `import "server-only"` ou par la convention `.server.ts`.
Énoncé dans [conventions.md](./conventions.md), section « Sécurité et données sensibles ».
**Elle reste tenue par l'équipe par décision.** Reconnaître mécaniquement ce qui constitue un secret demanderait une liste de motifs de noms, qui signalerait des modules inoffensifs et manquerait ceux qui comptent. Une règle qui se trompe souvent finit désactivée, et l'exigence disparaît alors complètement. La revue, elle, sait lire ce que fait un module.
## MA-009 · Le nommage des adaptateurs, dépôts et contrats
**Tenue par l'équipe.** `payment.contract.ts` pour un port, `stripePayment.adapter.ts` et `postgresOrder.repository.ts` pour ses implémentations.
Énoncé dans [conventions.md](./conventions.md), table de nommage.
**Elle reste tenue par l'équipe par décision.** Une règle de nom ne sait pas distinguer un adaptateur mal nommé d'un fichier qui n'en est pas un. Elle imposerait donc de nommer selon ce que la règle sait reconnaître, plutôt que selon ce que le fichier fait, ce qui est exactement l'inverse du but.
---
## Les Règles de conception, distinctes des neuf codes
Le corpus porte une seconde famille d'exigences, qui n'a pas de code et n'en aura pas. Elle n'était recensée nulle part : les cinq règles ci-dessous vivaient dispersées dans trois documents, sans que rien ne dise ce qui les sépare du registre.
| Règle | Ce qu'elle demande | Où elle est énoncée |
| :--- | :--- | :--- |
| **Règle d'Or Maedow Arch** | Une erreur attendue est une donnée typée, pas une exception | [`conventions.md`](./conventions.md) |
| **Zéro Modèle dans le JSX** | Les types métier ne traversent pas la couche de rendu | [`models.md`](./models.md) |
| **Règle du Pragmatisme Typé** | Ne pas dupliquer un modèle qui n'a pas divergé | [`models.md`](./models.md) |
| **Règle de Lazy Abstraction** | Pas de contrat ni d'adaptateur avant la deuxième implémentation réelle | [`architecture.md`](./architecture.md) |
| **Règle de Dégradation de `features/_shared/`** | Un composant partagé par une seule feature en redescend | [`architecture.md`](./architecture.md) |
| **Règle de Logique Extraite** | Plus de trois états ou un appel réseau : la logique de vue passe dans un hook | [`architecture.md`](./architecture.md) |
### Ce qui les sépare d'un code `MA`
**Un code se constate, une Règle de conception s'apprécie.** Un fichier `.tsx` est dans `core/` ou il n'y est pas : MA-004 se tranche sans discussion, et c'est ce qui permet à une machine ou à une revue de rendre un verdict. « La logique de vue est-elle extraite ? » n'a pas de réponse binaire, et le seuil de trois états est **indicatif** : un écran à quatre états ne viole rien, il mérite un regard.
C'est aussi pourquoi elles ne figurent pas dans la clause de conformité. Se dire conforme, c'est affirmer que MA-008 et MA-009 sont tenues, deux questions auxquelles une revue répond par oui ou par non. Une clause qui reposerait sur une appréciation cesserait d'être une clause.
**Elles ne sont pas pour autant facultatives.** Une Règle de conception ignorée ne fait échouer aucun contrôle, et c'est précisément son danger : un projet réel a passé dix-neuf lots sans appliquer la Règle de Logique Extraite, avec des écrans montés à 443 lignes, pendant que le lint, l'audit et les neuf règles restaient au vert. Ce que le standard vérifie mécaniquement ne dit rien de ce qu'il demande par ailleurs.
### Pourquoi ne pas leur donner un code quand même
Un registre dont les entrées mêlent le constatable et l'appréciable perd ce qui fait sa valeur : le rapport de sept règles vérifiées sur neuf, et la raison donnée pour chacune des deux autres. Diluer ce compte avec des exigences qu'aucun outil ne pourra jamais trancher rendrait le registre moins crédible, pas plus complet.
Le flou entre les deux catégories est ce que ce document existe pour supprimer. Les nommer et les recenser ici, sans les numéroter, est la façon de les rendre citables en revue sans effacer cette frontière.
## Se dire conforme à Maedow Arch
Un projet est **conforme à Maedow Arch 1.0** lorsque les quatre conditions suivantes sont réunies.
1. **Les sept règles vérifiables passent.** `eslint-config-maedow-arch` est installé et son lint est vert, entrée `strict` comprise. C'est la seule condition qu'une machine constate.
2. **Les deux règles humaines sont tenues.** MA-008 et MA-009 relèvent de la revue, et se dire conforme, c'est affirmer qu'elle a lieu.
3. **Le typage est strict.** `strict`, `noUncheckedIndexedAccess` et `exactOptionalPropertyTypes` sont activés, et aucun n'est désactivé fichier par fichier.
4. **Les renoncements sont locaux et écrits.** Une assertion commentée à l'endroit où elle s'applique reste conforme ; une option retirée du `tsconfig.json` ne l'est pas.
`npx maedow-arch check` mesure la première et la troisième. Il ne décerne pas la conformité : il dit ce qui vous en sépare.
### Ce qui constitue une rupture du standard
Une rupture est un changement qui **rend non conforme un projet qui l'était**. C'est une notion distincte du versionnage des paquets : `eslint-config-maedow-arch` peut publier une version majeure sans que le standard bouge, et l'inverse est vrai aussi.
Trois changements constituent une rupture :
- **Une règle passe de tenue par l'équipe à vérifiée**, et fait donc échouer un lint qui passait.
- **Une règle change de sens**, et interdit ce qu'elle autorisait.
- **Une règle nouvelle rejoint l'entrée par défaut.**
Ne constituent pas une rupture : l'ajout d'une règle à l'entrée `strict`, qui se charge explicitement ; la reformulation d'un message ; l'ajout d'une section au corpus ; le retrait d'une règle, qui ne peut que faire passer un lint qui échouait.
Toute rupture est annoncée dans le `CHANGELOG.md` sous cette forme, avec le chemin de migration. Un standard qui change sans le dire ne vaut pas mieux qu'une convention d'équipe.