Purge _ A capitaliser

This commit is contained in:
MaksTinyWorkshop
2026-03-20 13:56:14 +01:00
parent 7a35624e1b
commit 2ce7b2955e
5 changed files with 700 additions and 6 deletions

View File

@@ -8,7 +8,7 @@ Ce fichier contient **uniquement** des patterns back-end :
Objectif : éviter de réinventer la roue et réduire le temps de debug. Objectif : éviter de réinventer la roue et réduire le temps de debug.
Dernière mise à jour : 19-03-2026 Dernière mise à jour : 20-03-2026
--- ---
@@ -38,6 +38,10 @@ Dernière mise à jour : 19-03-2026
- [Guardrails multi-tenant — 403 vs 404 selon la sémantique](#pattern-guardrails-multi-tenant-403-404) - [Guardrails multi-tenant — 403 vs 404 selon la sémantique](#pattern-guardrails-multi-tenant-403-404)
- [Repository tenant-aware — `tenantId` obligatoire dans la signature](#pattern-repository-tenant-aware) - [Repository tenant-aware — `tenantId` obligatoire dans la signature](#pattern-repository-tenant-aware)
- [Défense en profondeur — inclure `tenantId` dans les updates](#pattern-tenantid-dans-updates) - [Défense en profondeur — inclure `tenantId` dans les updates](#pattern-tenantid-dans-updates)
- [Next.js server-only & Server Actions — règles d'isolation](#pattern-nextjs-server-only-isolation)
- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques)
- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier)
- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique)
--- ---
@@ -965,3 +969,183 @@ handlePackWebhookEvent(event): PackWebhookResult | null
- `tenantId` présent dans les clauses `where` des updates sensibles - `tenantId` présent dans les clauses `where` des updates sensibles
- Pas de mutation tenant-scoped basée sur `id` seul - Pas de mutation tenant-scoped basée sur `id` seul
- Revue explicite des exceptions documentées - Revue explicite des exceptions documentées
---
<a id="pattern-nextjs-server-only-isolation"></a>
## Pattern : Next.js server-only & Server Actions — règles d'isolation
- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires.
- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif.
- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js.
- Quand l'éviter : modules purement UI côté client.
- Avantage :
- logique pure testable sans friction (runner Node natif)
- Server Action fine et lisible — orchestration uniquement
- `server-only` explicite et intentionnel, pas par habitude
- Limites / vigilance :
- ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js
- Validé le : 16-03-2026
- Contexte technique : Next.js App Router / Node.js test runner
### Règles
```txt
- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime
(cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure
- Logique pure extraite dans un module injectable sans `server-only` :
deleteSession({ prismaClient, sessionToken })
→ testable avec le runner Node sans friction
- Server Action = orchestration mince, elle appelle les modules purs injectés
et gère les dépendances Next.js runtime uniquement
- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé,
sans import nodemailer / server-only
```
### Checklist
- [ ] `server-only` absent des repositories et modules de logique pure
- [ ] Server Action ≤ 10 lignes, délègue au module pur injectable
- [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale
- [ ] La logique pure ne dépend pas du runtime pour être exécutée
---
<a id="pattern-auth-operations-atomiques"></a>
## Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes
- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent.
- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session.
- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence.
- Quand l'éviter : opérations de lecture pure.
- Avantage :
- pas de token valide après reset de mot de passe si l'opération est interrompue
- suppression de session idempotente (P2025 absorbé silencieusement)
- comportement prévisible même en cas de retry ou de concurrence
- Limites / vigilance :
- `$transaction` Prisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction
- Validé le : 16-03-2026
- Contexte technique : Node.js / Prisma / auth par session ou token
### Implémentation (exemple minimal)
```typescript
// consumePasswordReset — atomique dans une transaction
await prisma.$transaction([
prisma.passwordResetToken.update({
where: { tokenHash },
data: { consumedAt: new Date() },
}),
prisma.user.update({
where: { id: userId },
data: { passwordHash: newHash },
}),
prisma.session.deleteMany({ where: { userId } }),
]);
// Suppression de session — idempotente (P2025 absorbé)
try {
await prisma.session.delete({ where: { sessionToken } });
} catch (err) {
if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK
}
```
### Checklist
- [ ] Toute opération hash + update + delete dans une `$transaction`
- [ ] `P2025` absorbé silencieusement sur les suppressions de session
- [ ] Effets de bord hors transaction documentés (cookie, email)
- [ ] Tests couvrant le cas d'une session déjà expirée
---
<a id="pattern-http-200-payload-metier"></a>
## Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès
- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
- Avantage :
- pas de gestion d'exception côté client mobile pour des états courants
- rendu conditionnel (paywall, teaser, empty) piloté par le payload
- log serveur propre — 4xx réservés aux erreurs techniques/sécurité
- Limites / vigilance :
- ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
- Validé le : 20-03-2026
- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1
### Implémentation (exemple minimal)
```typescript
// GET /community/forums
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }
// ❌ Anti-pattern
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });
// ✅ Pattern correct
return res.status(200).json({
data: { forums: [], paywallRequired: true },
meta: { total: 0 },
});
```
### Règle
- **4xx** = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
- **200 + flag métier** = état métier normal que le client doit interpréter pour le rendu
---
<a id="pattern-quota-redis-atomique"></a>
## Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline)
- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline.
- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles).
- Quand l'utiliser : toute limite d'action journalière avec Redis disponible.
- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation).
- Avantage :
- atomicité garantie : `INCR + EXPIREAT` dans un pipeline `MULTI/EXEC`
- pas de clé sans TTL même en cas de deux requêtes simultanées (`count === 1` concurrent)
- mode dégradé explicite si Redis down (`count === null` → permissif)
- Limites / vigilance :
- compensation `incrBy(-1)` en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification
- la fenêtre expire à minuit UTC, pas à minuit local
- Validé le : 20-03-2026
- Contexte technique : Redis / NestJS / app-alexandrie story 4.2
### Implémentation (exemple minimal)
```typescript
// RedisService — méthode dédiée
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
const pipeline = this.client.multi();
pipeline.incr(key);
pipeline.expireAt(key, Math.floor(expireAtMs / 1000));
const results = await pipeline.exec();
return results[0] as number; // valeur post-INCR
}
// Service métier
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
const quotaKey = `app:quota:post:${userId}:${today}`;
const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime());
if (count !== null && count > QUOTA_MAX) {
await redis.incrBy(quotaKey, -1); // compensation
throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS);
}
// count === null → Redis down → mode dégradé permissif
```
### Checklist
- [ ] Vérifier le quota AVANT la création en DB
- [ ] `INCR + EXPIREAT` dans un pipeline atomique
- [ ] Mode dégradé permissif si `count === null` (Redis down)
- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC)
- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent)

View File

@@ -8,7 +8,7 @@ Ce fichier recense des risques back-end susceptibles de provoquer :
- régressions coûteuses, - régressions coûteuses,
- incohérences de données. - incohérences de données.
Dernière mise à jour : 16-03-2026 Dernière mise à jour : 20-03-2026
--- ---
@@ -46,6 +46,9 @@ Dernière mise à jour : 16-03-2026
- [`jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma](#risque-jest-clearallmocks-imbrique) - [`jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma](#risque-jest-clearallmocks-imbrique)
- [Suppression du cookie après révocation DB sur logout](#risque-cookie-apres-revocation-db) - [Suppression du cookie après révocation DB sur logout](#risque-cookie-apres-revocation-db)
- [Repository layer non branché (dead layer)](#risque-repository-dead-layer) - [Repository layer non branché (dead layer)](#risque-repository-dead-layer)
- [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest)
- [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation)
- [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant)
--- ---
@@ -553,3 +556,90 @@ if (!user?.userId) {
- Rechercher explicitement les appels directs restants lors de la review - Rechercher explicitement les appels directs restants lors de la review
- Refuser lintroduction dune couche repository tant que la migration effective nest pas faite - Refuser lintroduction dune couche repository tant que la migration effective nest pas faite
- Contexte technique : TypeScript / Prisma / refactor daccès aux données — 16-03-2026 - Contexte technique : TypeScript / Prisma / refactor daccès aux données — 16-03-2026
---
<a id="risque-nestjs-toomanyrequest"></a>
## NestJS 11 — `TooManyRequestsException` inexistante
### Risques
- `TooManyRequestsException` nest pas exportée par `@nestjs/common` en NestJS ≥ 11
- Erreur de compilation ou 500 si utilisée directement
### Symptômes
- `Cannot find name TooManyRequestsException` à la compilation
- Test qui passe sur NestJS 10 mais échoue sur 11+
### Bonnes pratiques / mitigations
```typescript
// Pattern sûr pour HTTP 429
throw new HttpException(
{ error: { code: QUOTA_EXCEEDED, message: ... } },
HttpStatus.TOO_MANY_REQUESTS,
);
```
- Contexte technique : NestJS v11+ — 20-03-2026
---
<a id="risque-forbidden-pour-validation"></a>
## `ForbiddenException` (403) utilisé pour des erreurs de validation
### Risques
- Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403
- Sémantique API incorrecte → comportements clients imprévisibles
### Symptômes
- `ForbiddenException` lancée pour des tags invalides, des formats incorrects, des liens HTTP
- Clients API qui ignorent ces erreurs ou les traitent comme des refus daccès
### Bonnes pratiques / mitigations
Tableau de correspondance :
| Cas | Exception correcte | Code HTTP |
|---|---|---|
| Tags invalides, contenu trop long, format incorrect | `BadRequestException` | 400 |
| Accès refusé explicitement (accès forum, trial read-only) | `ForbiddenException` | 403 |
| Quota dépassé | `HttpException(429)` via `HttpStatus.TOO_MANY_REQUESTS` | 429 |
- **Règle** : HTTP 403 = "tu nas pas le droit deffectuer cette action". HTTP 400 = "ta requête est mal formée".
- Contexte technique : NestJS / HTTP — 20-03-2026
---
<a id="risque-prismaservice-getter-manquant"></a>
## PrismaService — getter explicite manquant sur nouveau modèle
### Risques
- Lajout dun modèle dans `schema.prisma` sans son getter dans `PrismaService` casse le typecheck
- Erreur silencieuse si les modules sont peu typés
### Symptômes
- `Property forum does not exist on type PrismaService` à la compilation
- Module fonctionnel sur le `PrismaClient` direct mais cassé via `PrismaService`
### Bonnes pratiques / mitigations
Tout ajout de modèle Prisma = **deux actions** :
1. Ajouter le modèle dans `schema.prisma`
2. Ajouter le getter dans `prisma.service.ts`
```typescript
// apps/api/src/infra/prisma/prisma.service.ts
get forum() {
return this.client.forum;
}
```
- **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour.
- Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026

View File

@@ -12,7 +12,7 @@ Il sert de **mémoire durable** pour éviter :
- de redélibérer éternellement sur des sujets déjà tranchés, - de redélibérer éternellement sur des sujets déjà tranchés,
- de propager des “bonnes pratiques” théoriques non éprouvées. - de propager des “bonnes pratiques” théoriques non éprouvées.
Dernière mise à jour : 19-03-2026 Dernière mise à jour : 20-03-2026
--- ---
@@ -25,6 +25,10 @@ Dernière mise à jour : 19-03-2026
- [Refresh idempotent sur store de liste paginée](#pattern-refresh-idempotent-liste-paginee) - [Refresh idempotent sur store de liste paginée](#pattern-refresh-idempotent-liste-paginee)
- [UI admin légère sur domaine existant](#pattern-ui-admin-legere-domaine-existant) - [UI admin légère sur domaine existant](#pattern-ui-admin-legere-domaine-existant)
- [Intégration tierce en mode link-out — préférer une page locale canonique](#pattern-link-out-page-locale-canonique) - [Intégration tierce en mode link-out — préférer une page locale canonique](#pattern-link-out-page-locale-canonique)
- [Design Tokens natifs TypeScript (Expo / React Native)](#pattern-design-tokens-expo-rn)
- [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer)
- [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant)
- [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique)
--- ---
@@ -427,6 +431,193 @@ const handleOAuth = async () => {
- [ ] Les garde-fous et fallbacks sont centralisés - [ ] Les garde-fous et fallbacks sont centralisés
- [ ] Les sorties directes concurrentes vers le tiers sont évitées ou justifiées - [ ] Les sorties directes concurrentes vers le tiers sont évitées ou justifiées
---
<a id="pattern-design-tokens-expo-rn"></a>
## Pattern : Design Tokens natifs TypeScript (Expo / React Native)
### Synthèse
- **Objectif** : centraliser les tokens de design sans librairie externe (NativeBase, Tamagui), typés et barrel-exportés.
- **Contexte** : app Expo / React Native avec un système de design à maintenir.
- **Quand lutiliser** : dès le début dun projet mobile, avant les premiers composants.
- **Quand léviter** : si une librairie UI opinionée est déjà choisie et gère ses propres tokens.
### Analyse
- **Avantages** :
- aucune dépendance externe, zéro configuration magique
- autocomplétion TypeScript exacte via `as const` + types dérivés
- facile à migrer vers un design system plus élaboré ultérieurement
- **Limites / vigilance** :
- les fichiers TTF doivent être présents dans `assets/fonts/` — Google Fonts ne peut pas être téléchargé automatiquement, documenter comme pré-requis dans la story
- ne pas réutiliser les tokens `spacing` pour les dimensions de composants (voir risques)
### Validation
- Validé le : 19-03-2026
- Contexte technique : Expo SDK 52+ / React Native / TypeScript — app-alexandrie story 0.1
### Implémentation (exemple minimal)
```typescript
// apps/mobile/src/theme/colors.ts
export const colors = {
primary: #2563EB,
error: #DC2626,
// ...
} as const;
export type ColorToken = keyof typeof colors;
// apps/mobile/src/theme/spacing.ts
export const spacing = { xs: 4, sm: 8, md: 12, base: 16, lg: 24 } as const;
export type SpacingToken = keyof typeof spacing;
// apps/mobile/src/theme/index.ts (barrel export)
export * from ./colors;
export * from ./spacing;
export * from ./typography;
export * from ./shadows;
```
### Checklist
- [ ] Tous les tokens `as const` pour inférence exacte
- [ ] Pas de Context React — constantes TypeScript pures
- [ ] Types dérivés (`ColorToken = keyof typeof colors`) pour lautocomplétion
- [ ] `useFonts` dans `_layout.tsx` avec guard `!fontsLoaded`
- [ ] Fichiers TTF présents dans `assets/fonts/` et documentés dans la story
---
<a id="pattern-tests-styles-sans-renderer"></a>
## Pattern : Tests de styles React Native sans renderer JSX
### Synthèse
- **Objectif** : tester les tokens et styles de composants React Native dans un environnement Jest `testEnvironment: node` sans renderer JSX.
- **Contexte** : config Jest avec `transform: { ^.+\\.ts$: ts-jest }` — les `.tsx` ne sont pas transformés.
- **Quand lutiliser** : tokens de thème, logique pure, valeurs de style exportées.
- **Quand léviter** : rendu conditionnel (styles dynamiques inline) — nécessite `@testing-library/react-native`.
### Analyse
- **Avantages** :
- teste que le composant utilise les bons tokens, pas seulement que les tokens ont des valeurs
- détecte les régressions de style sans renderer
- rapide, aucune config Jest supplémentaire
- **Limites / vigilance** :
- ne teste pas le style calculé au runtime (style conditionnel dynamique)
### Validation
- Validé le : 19-03-2026
- Contexte technique : React Native / Jest / ts-jest — app-alexandrie story 0.2
### Implémentation
```typescript
// Button.tsx — exporter le StyleSheet avec un nom préfixé
export const buttonStyles = StyleSheet.create({
base: { borderRadius: 20, height: 57 },
primary: { backgroundColor: colors.primary },
});
export function Button(...) { ... }
// ui-components.spec.ts — importer et vérifier les tokens
import { buttonStyles } from ./Button;
import { colors } from @/theme;
it(variante primary utilise colors.primary, () => {
expect(buttonStyles.primary.backgroundColor).toBe(colors.primary);
});
```
### Deux niveaux de tests UI recommandés
1. `.spec.ts` (node) : tokens, valeurs, logique pure
2. `.spec.tsx` (config séparée avec renderer) : rendu visuel, interactions
---
<a id="pattern-export-styles-composant"></a>
## Pattern : Export des styles de composant pour réutilisation partielle (React Native)
### Synthèse
- **Objectif** : partager les dimensions et formes dun composant UI vers des éléments custom qui en dérivent, sans dupliquer les valeurs.
- **Contexte** : app React Native où des screens construisent des éléments qui doivent être "au gabarit" dun composant existant.
- **Quand lutiliser** : bouton custom OAuth, container calqué sur un composant de base, etc.
- **Quand léviter** : si lécart visuel est intentionnel — dans ce cas, une constante locale est plus claire.
### Analyse
- **Avantages** :
- zéro drift silencieux : si les dimensions du composant changent, tous les éléments dérivés suivent
- tests de styles possibles en dehors du composant
- **Limites / vigilance** :
- à nutiliser que pour des éléments vraiment dérivés, pas comme contournement de design system
### Validation
- Validé le : 19-03-2026
- Contexte technique : React Native / StyleSheet — app-alexandrie story 0.3
### Implémentation
```typescript
// Button.tsx
export const buttonStyles = StyleSheet.create({
base: { borderRadius: 20, height: 57 },
primary: { backgroundColor: colors.primary },
});
export function Button(...) { ... }
// login.tsx — bouton OAuth au gabarit du Button
import { buttonStyles } from @/components/ui/Button;
<TouchableOpacity style={[buttonStyles.base, styles.facebookButton]} />
```
---
<a id="pattern-token-typography-semantique"></a>
## Pattern : Token typography par usage sémantique (React Native)
### Synthèse
- **Objectif** : éviter les mauvais usages de tokens typography visuellement proches mais sémantiquement distincts.
- **Contexte** : fichier `typography.ts` dans un design system React Native.
- **Quand lutiliser** : dès que deux tokens partagent la même taille mais un poids différent.
- **Quand léviter** : jamais — les tokens typography doivent toujours refléter lusage, pas lapparence.
### Analyse
- **Avantages** :
- prévient les "approximations" de tokens en code review
- changement de style dusage spécifique sans régression globale
- **Limites / vigilance** :
- en review : chercher les usages sans `fontWeight` explicite — cest souvent le signe que le mauvais token a été choisi
### Validation
- Validé le : 19-03-2026
- Contexte technique : React Native / TypeScript — app-alexandrie story 0.4
### Implémentation
```typescript
// Bon : nommé par usage sémantique
listItemTitle: { fontSize: 12, fontWeight: 600 }, // titre dun item de liste
caption: { fontSize: 12, fontWeight: 500 }, // info secondaire, hints
// Mauvais : nommé par apparence
mediumText12: { fontSize: 12, fontWeight: 500 }, // ambigu, réutilisé à tort
```
**Règle** : `caption` (Medium) ≠ `listItemTitle` (SemiBold) même si la taille est identique. Ne jamais piocher un token "par approximation".
---
### Principes transverses ### Principes transverses
- Un pattern = une responsabilité claire - Un pattern = une responsabilité claire

View File

@@ -7,7 +7,7 @@ Ce fichier recense des risques front-end susceptibles de provoquer :
- dette technique rapide, - dette technique rapide,
- régressions UX/perf/a11y. - régressions UX/perf/a11y.
Dernière mise à jour : 12-03-2026 Dernière mise à jour : 20-03-2026
--- ---
@@ -33,6 +33,12 @@ Dernière mise à jour : 12-03-2026
- [Auto-reset dun état dégradé sur toute réponse 2xx](#risque-auto-reset-etat-degrade) - [Auto-reset dun état dégradé sur toute réponse 2xx](#risque-auto-reset-etat-degrade)
- [Refresh store en fire-and-forget après mutation](#risque-refresh-store-fire-and-forget) - [Refresh store en fire-and-forget après mutation](#risque-refresh-store-fire-and-forget)
- [Loading infini sur écran gated par droits distants](#risque-loading-infini-ecran-gated) - [Loading infini sur écran gated par droits distants](#risque-loading-infini-ecran-gated)
- [Jest React Native — config node bloque les composants `.tsx`](#risque-jest-rn-config-node)
- [Bouton OAuth présent mais handler vide après refacto UI](#risque-oauth-handler-vide)
- [Double système d'espacement dans un monorepo Expo](#risque-double-systeme-espacement)
- [Dimensions d'image via tokens `spacing` (React Native)](#risque-dimensions-image-via-spacing)
- [Écran détail Expo Router — store vide en deep link / reload](#risque-store-vide-deep-link)
- [`useEffect` fetch — guard incomplet sur les états terminaux](#risque-useeffect-guard-incomplet)
--- ---
@@ -281,3 +287,156 @@ Dernière mise à jour : 12-03-2026
- Bloquer les retries automatiques en boucle après erreur - Bloquer les retries automatiques en boucle après erreur
- Réautoriser un retry seulement via action utilisateur explicite ou nouvelle condition dentrée - Réautoriser un retry seulement via action utilisateur explicite ou nouvelle condition dentrée
- Contexte technique : React Native / Expo / store dentitlements — 10-03-2026 - Contexte technique : React Native / Expo / store dentitlements — 10-03-2026
---
<a id="risque-jest-rn-config-node"></a>
## Jest React Native — config node bloque les composants `.tsx`
### Risques
- `SyntaxError: Cannot use import statement outside a module` lors de limport dun barrel `.ts` qui réexporte des `.tsx`
- Impossible dimporter des composants React Native dans les tests — JSX non transformé
### Symptômes
- Erreur de syntaxe inattendue au run des tests sur un fichier `.ts` qui importe un `.tsx`
- Les tests de tokens passent mais tout test touchant un composant échoue
### Bonnes pratiques / mitigations
- `transform: { ^.+\\.ts$: ts-jest }` ne transforme que `.ts` — pas `.tsx`
- **Pattern recommandé** : tester la logique pure (tokens, valeurs de style) dans `.spec.ts`, le rendu visuel dans `.spec.tsx` avec une config séparée (`@testing-library/react-native` + `babel-jest`)
- Exporter le `StyleSheet` de chaque composant pour le tester sans JSX (voir pattern dédié dans `10_frontend_patterns_valides.md`)
- Contexte technique : React Native / Jest / ts-jest — app-alexandrie 19-03-2026
---
<a id="risque-oauth-handler-vide"></a>
## Bouton OAuth présent mais handler vide après refacto UI
### Risques
- LOAuth est silencieusement cassé sur le nouvel écran — zéro erreur au démarrage, zéro crash
- LAC "toutes les fonctionnalités préservées" peut être coché alors que le bouton est mort
### Symptômes
- `<Button title="Google" onPress={() => {}} />` — handler vide après copie depuis un ancien écran
- OAuth fonctionnel sur lécran précédent (`welcome.tsx`) mais absent sur le nouvel écran refactorisé
### Bonnes pratiques / mitigations
- Toute refacto UI qui introduit un bouton OAuth doit brancher le hook existant (`useGoogleAuth(onSuccess)`)
- Si la story exclut explicitement la fonctionnalité : soit le bouton napparaît pas, soit `disabled` avec un label explicite ("bientôt disponible")
- **Checklist review** : chercher `onPress={() => {}}` sur tous les boutons OAuth dans les écrans refactorisés
- Contexte technique : Expo Router / React Native — app-alexandrie story 0.3, 19-03-2026
---
<a id="risque-double-systeme-espacement"></a>
## Double système despacement dans un monorepo Expo
### Risques
- Deux échelles despacement coexistent avec des noms différents pour des valeurs identiques (`Spacing.three = 16` vs `spacing.base = 16`)
- Laudit "zéro hardcode" ne détecte pas linconsistance car les deux sont des constantes nommées
- Les deux échelles peuvent diverger silencieusement
### Symptômes
- `import { Spacing } from @/constants/theme` coexiste avec `import { spacing } from @/theme`
- Certains screens refactorisés utilisent lancien système sans que personne ne le détecte
### Bonnes pratiques / mitigations
- Dès la création de `src/theme/spacing.ts`, supprimer ou vider `constants/theme.ts` (sauf constantes vraiment spécifiques : `MaxContentWidth`, `BottomTabInset`)
- Faire un `grep from @/constants/theme` à chaque story pour détecter les usages résiduels
- **Cause racine** : le template Expo génère `constants/theme.ts` avec `Spacing = { one, two, three... }` — à purger explicitement lors de la story design tokens
- Contexte technique : Expo / React Native — app-alexandrie story 0.5, 19-03-2026
---
<a id="risque-dimensions-image-via-spacing"></a>
## Dimensions dimage via tokens `spacing` (React Native)
### Risques
- Si `spacing.huge` change pour une raison despacement, la taille de limage change silencieusement
- Régression visuelle sans que personne ne réalise limpact — les deux changements semblent indépendants
### Symptômes
- `width: spacing.huge, height: spacing.huge` pour une image dont la taille est fixée par la spec Figma
### Bonnes pratiques / mitigations
```typescript
// Correct : constante locale ou token dédié
const THUMBNAIL_SIZE = 48; // Figma spec node 1-16147
// OU token dans un fichier sizes.ts dédié si la valeur est partagée
export const sizes = { thumbnail: 48, avatar: 40 } as const;
```
**Règle** : `spacing` = espacement entre éléments. `sizes` ou constantes locales = dimensions de composants.
- Contexte technique : React Native / design tokens — app-alexandrie story 0.4, 19-03-2026
---
<a id="risque-store-vide-deep-link"></a>
## Écran détail Expo Router — store vide en deep link / reload
### Risques
- Lécran détail (`[slug].tsx`) lit ses données depuis un store Zustand peuplé par lécran liste
- En deep link, kill + reopen ou navigation OS back, le store est vide → "introuvable" affiché à tort
### Symptômes
- Écran détail vide ou erreur "non trouvé" sur accès direct (pas via la liste)
- Fonctionne normalement en navigation standard mais échoue sur reload
### Bonnes pratiques / mitigations
```typescript
// useEffect de secours dans lécran détail
useEffect(() => {
if (!accessToken) return;
if (items.length > 0 || isLoading || errorState) return;
void fetchItems(accessToken);
}, [accessToken, items.length, isLoading, errorState, fetchItems]);
```
- Ne pas afficher "introuvable" avant davoir vérifié que le store a bien été peuplé
- Contexte technique : Expo Router / Zustand — app-alexandrie story 4.1, 20-03-2026
---
<a id="risque-useeffect-guard-incomplet"></a>
## `useEffect` fetch — guard incomplet sur les états terminaux
### Risques
- Si létat "zéro résultat intentionnel" (ex : `paywallRequired`) nest pas dans les conditions de court-circuit, le fetch est re-déclenché à chaque re-render ou focus
- Boucle de fetch infini sur un état métier normal
### Symptômes
- `forums.length === 0` et `isLoading === false` → le guard ne court-circuite pas → fetch re-déclenché en boucle
- Visible en focus sur lécran depuis un autre onglet
### Bonnes pratiques / mitigations
```typescript
// ❌ Pattern à risque — re-fetch si paywallRequired (forums vide + isLoading false)
if (forums.length > 0 || isLoading) return;
// ✅ Pattern correct — court-circuit sur létat terminal
if (forums.length > 0 || isLoading || paywallRequired) return;
```
**Règle** : les états "zéro résultat intentionnel" (liste vide + flag métier) doivent être traités comme "données présentes" dans le guard de fetch.
- Contexte technique : React Native / Zustand / Expo Router — app-alexandrie story 4.1, 20-03-2026

View File

@@ -9,13 +9,14 @@ Ce fichier contient des patterns de **cadrage produit, priorisation et analyse f
Objectif : éviter de redélibérer sur des sujets déjà tranchés, capitaliser ce qui fonctionne Objectif : éviter de redélibérer sur des sujets déjà tranchés, capitaliser ce qui fonctionne
du point de vue product management et analyse métier. du point de vue product management et analyse métier.
Dernière mise à jour : 2026-03-09 Dernière mise à jour : 2026-03-20
--- ---
## Index ## Index
_(à remplir au fil des validations)_ - [Epic UI Fondation — prérequis bloquant avant Epic 1](#pattern-epic-ui-fondation)
- [Progression V1 sans module dédié — calcul depuis la source de vérité](#pattern-progression-v1-sans-module)
--- ---
@@ -59,3 +60,72 @@ Si ce n'est pas **validé par l'expérience projet**, ça n'a pas sa place ici.
### Checklist (si pertinente) ### Checklist (si pertinente)
--- ---
<a id="pattern-epic-ui-fondation"></a>
## Pattern : Epic UI Fondation — prérequis bloquant avant Epic 1
- Objectif : éviter les régressions et la dette de migration générées par l'insertion tardive d'un epic de fondation UI.
- Contexte : app mobile ou SPA avec un design system à créer (design tokens + composants primitifs).
- Quand l'utiliser : dès qu'un epic design tokens / composants de base existe dans la roadmap.
- Quand l'éviter : si le design ad-hoc est un choix délibéré assumé (prototype jetable, MVP ultra-contraint).
- Avantage :
- les screens livrés ultérieurement utilisent d'emblée les bons tokens et composants
- pas de double système d'espacement ni de migration de composants partagés en cours de route
- les composants primitifs (Button, FilterChip...) sont stables avant d'être utilisés partout
- Limites / vigilance :
- si l'insertion tardive est inévitable, prévoir une story de smoke test explicite sur tous les parcours critiques refactorisés
- Validé le : 19-03-2026
- Contexte produit : App mobile / early stage — app-alexandrie rétrospective Epic 0
### Description
Un Epic "UI Fondation" inséré après Epic 1 (ou plus tard) génère trois risques spécifiques :
1. **Régression sur screens livrés** : les fichiers refactorisés avaient déjà une logique métier validée — toute modification structurelle rouvre un risque de régression.
2. **Composants partagés modifiés** : migrer `Button`, `FilterChip` de `TouchableOpacity` vers `Pressable` après utilisation partout est un breaking change API silencieux.
3. **Double système de tokens** : les screens livrés avaient adopté les anciennes constantes — la migration est une dette qui s'accumule à chaque epic.
**Règle** : L'Epic UI Fondation (tokens + composants primitifs) est un **prérequis bloquant de Epic 1**. Il doit être fait en tout premier, avant toute logique métier — ou ne pas être fait du tout (accepter le design ad-hoc).
### Checklist (insertion tardive inévitable)
- [ ] Story de smoke test explicite sur tous les parcours critiques touchés
- [ ] Grep systématique des usages des composants modifiés avant merge
- [ ] Vérification que l'ancien système de tokens est entièrement purgé (`grep from '@/constants/theme'`)
---
<a id="pattern-progression-v1-sans-module"></a>
## Pattern : Progression V1 sans module dédié — calcul depuis la source de vérité
- Objectif : exposer une feature de progression/gamification sans créer prématurément un domaine `achievements` ou `analytics`.
- Contexte : produit early-stage avec une source de vérité existante (`content`, `billing`, `posts`...) et un besoin de progression MVP.
- Quand l'utiliser : première itération d'une feature de progression, objectifs, stats utilisateur.
- Quand l'éviter : si les compteurs sont déjà complexes, multi-sources ou nécessitent une persistance d'état (ex : streaks, badges conditionnels).
- Avantage :
- zéro scope creep — pas de tables `achievements` ni de service dédié en avance de phase
- logique explicite et testable en code, pas en DB
- couture propre vers un module `achievements` réel si le besoin se confirme
- Limites / vigilance :
- ne convient pas si les calculs de période sont coûteux à chaque appel — à surveiller sur le volume
- la couture vers les sources futures doit être prévue dès le départ (pas de calcul hardcodé sur une seule table)
- Validé le : 10-03-2026
- Contexte produit : App mobile / early stage — app-alexandrie
### Description
Plutôt que d'ouvrir un domaine `achievements` ou `analytics` dès la V1 :
1. **Garder l'agrégat dans le domaine métier source de vérité** (`content`, `billing`, etc.)
2. **Calculer les compteurs de période** (`thisWeek`, `thisMonth`) directement en base via `count` / agrégations filtrées
3. **Centraliser le catalogue d'objectifs en code** — explicite, versionné, testable
4. **Prévoir des coutures zéro-safe** pour les sources futures (`contributions`, `posts`...) sans inventer les tables en avance
### Checklist
- [ ] Aucune table `achievements` / `progress` créée sans besoin confirmé
- [ ] Compteurs calculés depuis la source de vérité existante
- [ ] Catalogue d'objectifs en code (pas en DB)
- [ ] Interface de sortie stable même si les sources de données évoluent
---