capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)

Triage du 95_a_capitaliser.md (~75 propositions) :
- 60 entrées intégrées dans knowledge/ (backend, frontend, workflow)
- 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md,
  frontend/patterns/general.md, workflow/patterns/general.md
- 6 doublons rejetés
- Mise à jour des READMEs index pour refléter les nouvelles entrées
- 95_a_capitaliser.md restauré à sa structure initiale
- 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant
- 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI,
  prisma migrate diffs cosmétiques

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-05-02 22:12:44 +02:00
parent 02ad0de258
commit b3417ad77b
31 changed files with 5370 additions and 12 deletions

View File

@@ -164,3 +164,124 @@ if (mediaUrl) {
- [ ] Validation format (`new URL()`) + protocole + longueur max
- [ ] Retourner `null` si invalide, jamais passer la string brute
- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée
---
<a id="pattern-nextjs-after-fanout-post-reponse"></a>
## Pattern : Next.js `after()` pour fanout post-réponse
- Objectif : exécuter du travail accessoire (notif push, audit, webhook) APRÈS avoir renvoyé la réponse HTTP, sans bloquer la latence métier ni risquer la fermeture lambda du fire-and-forget naïf.
- Contexte : route Next.js 15+ (App Router) qui termine une mutation métier (ex : `prisma.$transaction`) et veut déclencher un effet de bord non critique.
- Quand l'utiliser : effet de bord best-effort qui ne doit JAMAIS bloquer la réponse principale (push, log audit asynchrone, webhook sortant).
- Quand l'éviter : effet de bord qui DOIT réussir avant de répondre 200 (validation transactionnelle, écriture critique).
- Avantage :
- réponse HTTP immédiate (latence métier préservée)
- le runtime Node attend la fin du callback avant de fermer le process — pas de fire-and-forget orphelin
- les exceptions du callback sont attrapables localement, ne propagent pas au caller
- Limites / vigilance :
- **JAMAIS placer `after()` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...) })`** : si la transaction rollback, le callback `after()` reste planifié et s'exécute sur des données qui n'existent pas en DB
- construire le payload SYNCHRONE avant `after()` et attraper les erreurs là, sinon une exception dans le callback async devient silencieuse
- comportement en serverless (Vercel, Lambda) vs Node standalone peut différer — tester en cible
- Validé le : 28-04-2026
- Contexte technique : Next.js 15+ App Router — RL799_V2
### Implémentation
```typescript
import { after } from 'next/server';
export const notifyConvocationPublished = async (
tenueId: string,
recipientIds: string[],
): Promise<void> => {
// 1. Mutation métier (transactionnelle)
await prisma.$transaction(async (tx) => {
await tx.notification.createMany({ data: ... });
});
// 2. Construction payload SYNCHRONE — toute exception attrapée ici
let payload: PushPayload;
try {
payload = buildPushPayload(tenueId);
} catch (err) {
log.warn('payload build failed', { err });
return;
}
// 3. Hook après la transaction — JAMAIS dedans
after(async () => {
try {
await pushService.sendPushToUsers(recipientIds, payload);
} catch (err) {
log.warn('fanout failed', { err }); // jamais throw vers le caller
}
});
};
```
### Anti-patterns
-`await pushService.sendPushToUsers(...)` dans le service métier (bloque la latence + propage les erreurs)
-`after(() => { ... })` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...); })`
- ❌ Construire le payload DANS le callback `after()` async — une exception y devient silencieuse
### Checklist
- [ ] `after()` appelé APRÈS le `await prisma.$transaction(...)`, jamais à l'intérieur
- [ ] Payload construit synchrone avant `after()`, exceptions attrapées localement
- [ ] Le callback `after()` n'a pas le droit de throw (best-effort wrapping)
- [ ] La route métier renvoie 2xx même si le fanout échoue
---
<a id="pattern-gate-agir-au-nom-de"></a>
## Pattern : Gate "agir au nom de X" (3 étages : rôle → type/scope → cible actif)
- Objectif : valider correctement une autorisation d'override d'attribution (`uploadedByOverride`, `createdByOverride`) pour qu'un rôle élevé puisse agir "au nom de" un autre user, sans laisser de faille.
- Contexte : endpoint API qui accepte un override d'attribution (archiviste upload pour un Frère, admin crée une entité pour X).
- Quand l'utiliser : tout endpoint avec un override d'attribution sensible.
- Quand l'éviter : si la délégation est implicite et déjà couverte par un guard centralisé.
- Avantage :
- validation strictement séquentielle et défensive — chaque étage a son code HTTP propre
- le check "actif" combine toutes les dimensions disponibles (pas un seul flag)
- Limites / vigilance :
- ordre impératif : rôle EN PREMIER (plus sensible). Un membre lambda envoyant un payload avec override doit recevoir 403 même si la cible est valide
- Validé le : 20-04-2026
- Contexte technique : Next.js / API HTTP — RL799_V2
### Les 3 étages
1. **Rôle** : le user courant a-t-il la capacité ? Sinon **403 FORBIDDEN**.
2. **Type/scope** : l'override est-il pertinent pour ce type d'entité ? Sinon **400 VALIDATION_ERROR**.
3. **Cible** : la cible existe-t-elle ET est-elle active ? Sinon **400 VALIDATION_ERROR** si introuvable OU inactive OU démissionnée OU décédée.
### Implémentation
```typescript
let effectiveUploadedBy = currentUser.id;
if (metadata.uploadedByOverride) {
// Étage 1 : rôle
if (userRole !== 'archiviste' && userRole !== 'admin') {
return errorResponse(403, 'FORBIDDEN', '...');
}
// Étage 2 : type/scope
if (metadata.type !== 'planche') {
return errorResponse(400, 'VALIDATION_ERROR', '...');
}
// Étage 3 : cible actif (toutes les dimensions disponibles)
const targetUser = await getUserById(metadata.uploadedByOverride);
const isInactive =
!targetUser
|| !targetUser.isActive
|| !!targetUser.profile?.resignedAt
|| !!targetUser.profile?.deceasedAt;
if (isInactive) {
return errorResponse(400, 'VALIDATION_ERROR', '...');
}
effectiveUploadedBy = metadata.uploadedByOverride;
}
```
### Pourquoi vérifier toutes les dimensions
Un user peut être `isActive: true` dans le système mais avoir une `resignedAt` antérieure (désactivation non-synchrone). Le check "actif" doit combiner **toutes** les dimensions disponibles du modèle, pas un seul flag.