mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 13:31:43 +02:00
Purge _ A capitaliser
This commit is contained in:
@@ -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.
|
||||
|
||||
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)
|
||||
- [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)
|
||||
- [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
|
||||
- Pas de mutation tenant-scoped basée sur `id` seul
|
||||
- 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)
|
||||
|
||||
@@ -8,7 +8,7 @@ Ce fichier recense des risques back-end susceptibles de provoquer :
|
||||
- régressions coûteuses,
|
||||
- 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)
|
||||
- [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)
|
||||
- [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
|
||||
- Refuser l’introduction d’une couche repository tant que la migration effective n’est pas faite
|
||||
- Contexte technique : TypeScript / Prisma / refactor d’accès aux données — 16-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-nestjs-toomanyrequest"></a>
|
||||
## NestJS 11 — `TooManyRequestsException` inexistante
|
||||
|
||||
### Risques
|
||||
|
||||
- `TooManyRequestsException` n’est 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 d’accè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 n’as pas le droit d’effectuer 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
|
||||
|
||||
- L’ajout d’un 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
|
||||
|
||||
@@ -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 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)
|
||||
- [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)
|
||||
- [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 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 l’utiliser** : dès le début d’un 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 l’autocomplé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 l’utiliser** : 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 d’un 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" d’un composant existant.
|
||||
- **Quand l’utiliser** : 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** :
|
||||
- à n’utiliser 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 l’utiliser** : 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 l’usage, pas l’apparence.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- prévient les "approximations" de tokens en code review
|
||||
- changement de style d’usage spécifique sans régression globale
|
||||
- **Limites / vigilance** :
|
||||
- en review : chercher les usages sans `fontWeight` explicite — c’est 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 d’un 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
|
||||
|
||||
- Un pattern = une responsabilité claire
|
||||
|
||||
@@ -7,7 +7,7 @@ Ce fichier recense des risques front-end susceptibles de provoquer :
|
||||
- dette technique rapide,
|
||||
- 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 d’un é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)
|
||||
- [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
|
||||
- Réautoriser un retry seulement via action utilisateur explicite ou nouvelle condition d’entrée
|
||||
- Contexte technique : React Native / Expo / store d’entitlements — 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 l’import d’un barrel `.ts` qui réexporte des `.tsx`
|
||||
- Impossible d’importer 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
|
||||
|
||||
- L’OAuth est silencieusement cassé sur le nouvel écran — zéro erreur au démarrage, zéro crash
|
||||
- L’AC "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 n’apparaî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 d’espacement dans un monorepo Expo
|
||||
|
||||
### Risques
|
||||
|
||||
- Deux échelles d’espacement coexistent avec des noms différents pour des valeurs identiques (`Spacing.three = 16` vs `spacing.base = 16`)
|
||||
- L’audit "zéro hardcode" ne détecte pas l’inconsistance 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 l’ancien 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 d’image via tokens `spacing` (React Native)
|
||||
|
||||
### Risques
|
||||
|
||||
- Si `spacing.huge` change pour une raison d’espacement, la taille de l’image change silencieusement
|
||||
- Régression visuelle sans que personne ne réalise l’impact — 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 d’avoir 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`) n’est 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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
_(à 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)
|
||||
|
||||
---
|
||||
|
||||
<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
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user