mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
1379 lines
61 KiB
Markdown
1379 lines
61 KiB
Markdown
# Capitalisation en attente — Lead_tech
|
||
|
||
Ce fichier sert de **zone tampon de capitalisation**.
|
||
|
||
Les agents et les projets peuvent y déposer des propositions
|
||
d’amélioration de la base de connaissance globale (`Lead_tech`).
|
||
|
||
Le contenu de ce fichier **n'est pas encore validé**.
|
||
|
||
Une fois relues et confirmées, les propositions doivent être **déplacées**
|
||
vers les fichiers appropriés :
|
||
|
||
- `10_backend_patterns_valides.md`
|
||
- `10_frontend_patterns_valides.md`
|
||
- `10_ux_patterns_valides.md`
|
||
- `10_product_patterns_valides.md`
|
||
- `10_n8n_patterns_valides.md`
|
||
- `10_backend_risques_et_vigilance.md`
|
||
- `10_frontend_risques_et_vigilance.md`
|
||
- `10_ux_risques_et_vigilance.md`
|
||
- `10_n8n_risques_et_vigilance.md`
|
||
- `10_conventions_redaction.md`
|
||
- `40_decisions_et_archi.md`
|
||
- `90_debug_et_postmortem.md`
|
||
|
||
Ce fichier ne doit donc **jamais devenir une documentation permanente**.
|
||
|
||
---
|
||
|
||
2026-03-23 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Pattern récurrent : les endpoints GET de lecture ne vérifient pas les droits d'accès au forum, alors que les endpoints d'écriture le font. Trouvé sur `getCategories` (4.5) — l'endpoint était exposé à tout utilisateur authentifié sans contrôle d'entitlements.
|
||
|
||
Proposition :
|
||
**VIGILANCE — Endpoints lecture sans contrôle d'accès (forum/ressource restreinte)**
|
||
Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les opérations GET. La règle "seuls les writes vérifient les droits" est un anti-pattern qui expose des données à des utilisateurs non autorisés. Checklist de review : pour chaque nouveau GET, vérifier qu'il passe bien par le guard/helper d'accès si la ressource appartient à un scope protégé.
|
||
|
||
---
|
||
|
||
2026-03-23 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Vérification d'unicité Prisma + race condition : une vérification `findUnique` avant `create` ne suffit pas en cas de requêtes concurrentes. Le catch sur `P2002` est le seul garde-fou fiable.
|
||
|
||
Proposition :
|
||
**PATTERN — Gestion de contrainte unique Prisma : toujours catch P2002**
|
||
Ne pas se fier uniquement à un `findUnique` pré-insertion pour garantir l'unicité. Toujours encapsuler le `create` dans un `try/catch` ciblant `err.code === 'P2002'` et relancer l'erreur métier appropriée. Cela couvre les race conditions entre requêtes concurrentes. Exemple validé : `createCategory` (4.5), `createThread` (4.2 aurait dû l'avoir aussi).
|
||
|
||
---
|
||
|
||
2026-03-23 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Store Zustand partagé entre plusieurs écrans d'un même type (ex: plusieurs forums) : les collections sans clé de contexte (ex: `categories: Category[]` sans `categoriesForumSlug`) causent des affichages de données périmées lors d'une navigation inter-forum.
|
||
|
||
Proposition :
|
||
**VIGILANCE — Store Zustand : collections sans clé de contexte**
|
||
Quand un store stocke des données qui dépendent d'un paramètre de navigation (forumSlug, threadId...), ne pas se contenter d'un guard `if (items.length > 0) return` — cela empêche le rechargement lors d'un changement de contexte. Soit stocker la clé de contexte avec les données (`categoriesForumSlug: string | null`), soit supprimer le guard et dépendre uniquement du changement de paramètre dans le useEffect.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review story 1.9 a révélé un anti-pattern : les tâches de story peuvent déclarer [x] consumedAt sans que le champ existe réellement dans le schéma Prisma.
|
||
|
||
Proposition :
|
||
**Anti-pattern : Divergence schéma / spec story**
|
||
Lors d'une implémentation, valider que chaque champ mentionné dans les tâches (consumedAt, tokenHash, etc.) existe réellement dans le schéma Prisma avant de marquer la tâche [x]. Une story peut décrire consumedAt comme stratégie de conception sans que le champ soit présent — toujours croiser avec schema.prisma.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Le module sendPasswordResetEmail utilise `server-only` ce qui le rend non-importable dans le runner de tests Node. Résolution : tester la logique pure (safeHttpUrl) dans un fichier séparé sans dépendances Next.js.
|
||
|
||
Proposition :
|
||
**Pattern : Isolation des guards purs des modules server-only**
|
||
Extraire la logique pure (validation URL, sanitisation) dans des fonctions utilitaires sans import `server-only` ou `nodemailer`. Le module email orchestre uniquement. Cela permet de tester les guards en isolation sans les contraintes du runtime Next.js.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 1.10 — la règle `server-only` vs testabilité est implicite dans le projet mais mérite d'être explicitée pour les agents futurs.
|
||
|
||
Proposition :
|
||
**Pattern : `server-only` réservé aux modules avec APIs Next.js exclusivement serveur**
|
||
Ne pas mettre `import "server-only"` sur les modules de logique pure injectés via dépendances (ex: `deleteSession({ prisma, sessionToken })`). Réserver `server-only` aux modules qui appellent des APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`). Les modules purs sans ces imports peuvent être importés par le test runner Node et testés unitairement sans friction.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 1.10 — suppression de session avec gestion idempotente, réutilisable pour toute opération de révocation.
|
||
|
||
Proposition :
|
||
**Pattern : Suppression de session idempotente (P2025)**
|
||
Lors d'une déconnexion ou révocation de session, entourer le `prisma.session.delete()` d'un try/catch qui absorbe silencieusement le code Prisma `P2025` (record not found). Une session peut déjà avoir été supprimée (expiration, logout concurrent) — ce n'est pas une erreur, ne pas la propager.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 1.10 — Server Action Next.js qui orchestre des dépendances Next.js runtime non-testables : isoler la logique pure dans un module injectable.
|
||
|
||
Proposition :
|
||
**Pattern : Server Action Next.js — isoler la logique pure dans un module injectable**
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Problème rencontré en build réel sur Next 16/Turbopack. Le pattern `useSearchParams()` côté client casse le prerender si le composant n'est pas protégé par `Suspense`.
|
||
|
||
Proposition :
|
||
**Risque : `useSearchParams()` sans `Suspense` casse le build Next.js App Router**
|
||
Avec Next.js App Router récent, tout composant client utilisant `useSearchParams()` peut provoquer un échec de prerender/build s'il est rendu sans boundary `Suspense` depuis la page/layout serveur. Quand un écran dépend de `useSearchParams()`, isoler ce composant client et le rendre sous `<Suspense fallback={...}>` au niveau de la page.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Le build Next a échoué sur une initialisation Prisma trop tôt dans le cycle, avant disponibilité effective de `DATABASE_URL`. Le correctif est réutilisable sur d'autres apps App Router.
|
||
|
||
Proposition :
|
||
**Risque : initialiser Prisma au chargement de module peut casser le build**
|
||
Dans une app Next.js App Router, un import global qui initialise immédiatement Prisma peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build. Préférer une initialisation lazy-safe : soit retarder l'accès DB au moment de l'appel métier, soit retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
La migration de config ESLint a permis de remettre `lint` au vert avec Next 16 sans dépendre d'un pont de compatibilité cassant.
|
||
|
||
Proposition :
|
||
**Pattern : utiliser directement les presets flat de `eslint-config-next`**
|
||
Sur un projet Next.js récent, préférer une config `eslint.config.mjs` qui importe directement `eslint-config-next/core-web-vitals` et `eslint-config-next/typescript`, puis ajoute des overrides ciblés. Cela évite les bugs de compatibilité de l'ancien `.eslintrc` et limite la dette de config quand le repo utilise déjà le flat config.
|
||
Une Server Action qui appelle `cookies()`, `headers()` ou `redirect()` ne peut pas être testée unitairement (imports runtime-only). Pattern : extraire la logique pure (suppression DB, validation) dans une fonction avec injection de dépendances (`performSignOut({ prismaClient, sessionToken, redirectFn })`). La Server Action reste fine et orchestre uniquement les dépendances Next.js. Le module extrait est testable sans friction avec le runner Node natif.
|
||
|
||
---
|
||
2026-03-16 — app-template-resto / code-review story 1.11
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
`import "server-only"` dans les repositories casse les tests Node.js hors Next.js — rencontré lors de cette review.
|
||
|
||
Proposition :
|
||
## Risque : `server-only` dans les repositories bloque les tests unitaires
|
||
|
||
`import "server-only"` empêche l'exécution des fichiers hors runtime Next.js.
|
||
Solution : créer un stub `node_modules/server-only/index.js` (no-op) pour les tests.
|
||
Alternativement, ne mettre `server-only` que dans les fichiers qui utilisent des APIs
|
||
Next.js (`cookies()`, `headers()`), pas dans les repositories purs.
|
||
|
||
---
|
||
|
||
2026-03-16 — app-template-resto / code-review story 2.1
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Deux anti-patterns Next.js App Router détectés en code review : duplication de type entre couche server et couche UI, et usage de React.createElement dans un fichier .ts pour contourner l'exigence JSX.
|
||
|
||
Proposition :
|
||
## Anti-pattern : type ViewData dupliqué entre server et composant UI (Next.js App Router)
|
||
|
||
Contexte : quand un service server-only (`src/server/...`) produit un type de données et qu'un composant UI (`src/app/...`) en a besoin, il est tentant de redéfinir le type localement dans le composant.
|
||
|
||
Risque : divergence silencieuse — TypeScript accepte deux structures identiques par structural typing, mais si le type source évolue (champ ajouté, renommé), la couche UI reste désynchronisée sans erreur de compilation tant que les formes correspondent.
|
||
|
||
Règle : le type appartient à la couche qui le produit. La couche UI importe et re-exporte uniquement.
|
||
|
||
```ts
|
||
// ✅ Dans PublicHomeContent.tsx
|
||
export type { PublicHomeViewData } from "@/server/public/getPublicHomeData";
|
||
|
||
// ❌ À éviter : redéfinir le même type dans le composant
|
||
export type PublicHomeViewData = { tenantName: string; ... };
|
||
```
|
||
|
||
---
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Anti-pattern détecté : composant React écrit en .ts avec React.createElement pour éviter d'avoir à déclarer l'extension .tsx, rendant le code illisible et non extensible.
|
||
|
||
Proposition :
|
||
## Anti-pattern : composant React dans un fichier .ts (React.createElement workaround)
|
||
|
||
Tout fichier contenant du JSX ou un composant React doit avoir l'extension `.tsx`. Utiliser `React.createElement` dans un `.ts` fonctionne techniquement mais est un anti-pattern :
|
||
- Rend le code illisible comparé à JSX natif
|
||
- Donne une fausse impression que le fichier est "sans JSX"
|
||
- Empêche l'utilisation de la syntaxe JSX si besoin d'ajouter des enfants complexes
|
||
- Peut tromper les outils de linting et les reviewers
|
||
|
||
Règle : si un fichier exporte une fonction retournant un ReactElement ou utilise React, l'extension est `.tsx`.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-2
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Review story 2-2 — anti-pattern "double validation de garde" dans Next.js App Router : un layout enfant rejette déjà les segments invalides via notFound(), mais la page enfant répétait la même condition. Risque de désynchronisation silencieuse si l'une des deux est modifiée sans l'autre.
|
||
|
||
Proposition :
|
||
**Anti-pattern : double validation de segment dynamique App Router**
|
||
Dans une structure layout → page, si le layout fait `notFound()` sur un segment invalide, la page ne doit PAS répéter la même condition. La page doit faire confiance à son layout parent. Répéter la validation :
|
||
- crée une fausse impression que les deux chemins sont indépendants
|
||
- rend le code difficile à maintenir (si on change la condition dans le layout, il faut penser à changer la page)
|
||
- peut induire en erreur sur quel composant est réellement responsable de la garde
|
||
Règle : une seule responsabilité par couche — le layout garde, la page consomme.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-2
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Review story 2-2 — faux test d'exclusion : un test nommé "test négatif : une page X n'utilise pas helper Y" appelait Y et vérifiait un résultat null — ce n'est pas un test d'exclusion, c'est juste un test normal mal documenté.
|
||
|
||
Proposition :
|
||
**Anti-pattern : faux test négatif — tester le helper au lieu de tester l'exclusion**
|
||
Un test intitulé "X n'utilise pas Y" doit vérifier que X n'importe pas Y, ou que le comportement par défaut de Y empêche l'effet indésirable. Si on appelle Y dans le test, on teste Y, pas l'exclusion de X. Pour les helpers à fallback optionnel, le vrai test négatif est : "avec fallbackToFr=false (défaut), une valeur EN vide n'est PAS remplacée silencieusement" — ce qui force l'appelant à choisir explicitement le fallback.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-3
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 2.3 — séparation nette entre le repository (requête Prisma brute) et le service (mapping DTO + règles métier). La règle isVisible est appliquée dans le service, pas dans la requête DB, ce qui la rend testable sans Prisma.
|
||
|
||
Proposition :
|
||
**Pattern : filtrage des règles métier dans le service, pas dans le repository**
|
||
Pour les données publiques avec des règles de visibilité (ex: `isVisible`, `isActive`), mettre le filtre dans le service et non dans la clause `where` du repository. Le repository charge tout ce qui est candidat (ex: catégories visibles, plats de toutes visibilités) ; le service applique les règles métier et mappe vers des DTOs. Avantages :
|
||
- la règle est testable unitairement sans Prisma (mock de données brutes)
|
||
- la requête DB reste simple et stable entre les contextes (dashboard edit ≠ rendu public)
|
||
- les futurs cas (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
|
||
|
||
Exception acceptable : filtres de performance (pagination, tenant scoping) restent dans le `where`.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-3
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 2.3 — les champs Decimal Prisma doivent être sérialisés explicitement avant de traverser des boundaries (service, réseau, tests). Sans toString(), le type Decimal Prisma n'est pas JSON-safe et peut provoquer des erreurs silencieuses.
|
||
|
||
Proposition :
|
||
**Pattern : sérialiser les Decimal Prisma en string au niveau du repository**
|
||
Tout champ `Decimal` Prisma (ex: `price`) doit être converti en `string` (`decimal.toString()`) dans le repository avant d'être retourné au service. Ne pas laisser les objets Decimal traverser les couches — ils ne sont pas JSON-sérialisables nativement et leur comportement varie selon le contexte (Node vs browser vs test runner). Le DTO public utilise `string | null` pour le prix, pas `Decimal`.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / code-review story 2-3
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Anti-pattern détecté en code review : les modèles menu (Allergen, Tag, MenuCategory, Dish) avaient chacun un champ `tenantId` sans relation Prisma `@relation` vers `Tenant`. La migration SQL ne créait pas non plus les FK correspondantes. Résultat : isolation multi-tenant non enforced au niveau DB.
|
||
|
||
Proposition :
|
||
**Anti-pattern : champ tenantId sans FK ni relation Prisma vers Tenant**
|
||
Tout modèle tenant-scoped doit avoir :
|
||
1. `tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)` dans le modèle Prisma
|
||
2. La relation inverse dans `Tenant` (ex: `menuCategories MenuCategory[]`)
|
||
3. La FK correspondante dans la migration SQL (`ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"`)
|
||
|
||
Un `tenantId TEXT NOT NULL` sans ces trois éléments ne garantit aucune isolation au niveau DB — Prisma ne génère pas de FK automatiquement sans la relation déclarée. Vérifier systématiquement que les nouveaux modèles respectent ce guardrail lors du code review.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / code-review story 2-3
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé en code review 2.3 — une fonction utilitaire de résolution de tenant (`resolvePublicTenantSelection`) était définie dans `getPublicHomeData.ts` et importée par le module menu. Ce couplage crée une dépendance sémantique incorrecte entre deux domaines distincts.
|
||
|
||
Proposition :
|
||
**Pattern : extraire les helpers de résolution tenant dans un module partagé dédié**
|
||
Toute fonction utilitaire transverse aux domaines (ex: résolution du tenant public depuis les variables d'environnement) doit vivre dans `src/server/tenant/` plutôt que dans un module métier spécifique. Les modules métier importent depuis ce module partagé. L'ancien emplacement peut ré-exporter pour rétrocompatibilité le temps de la migration.
|
||
```ts
|
||
// ✅ src/server/tenant/resolvePublicTenant.ts
|
||
export function resolvePublicTenantSelection(env) { ... }
|
||
|
||
// ✅ src/server/public/getPublicHomeData.ts (rétrocompatibilité)
|
||
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
|
||
```
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-4
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 90_debug_et_postmortem.md
|
||
|
||
Pourquoi :
|
||
Bug discret détecté au build : un `export { fn }` ne rend pas `fn` disponible localement dans le même fichier. TypeScript et ESLint ne le signalent pas, mais le build strict (TypeScript `--noEmit`) le rejette avec "Cannot find name". Piège fréquent lors de refactors de module.
|
||
|
||
Proposition :
|
||
**Bug : `export { fn }` ne constitue pas un import local — détecté uniquement au build**
|
||
|
||
Dans `getPublicHomeData.ts`, la fonction `resolvePublicTenantSelection` était re-exportée via :
|
||
```ts
|
||
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
|
||
```
|
||
…puis utilisée localement dans le même fichier sans `import`. ESLint et `tsc` (hors build) ne l'ont pas signalé, mais `next build` avec TypeScript strict a levé `Cannot find name 'resolvePublicTenantSelection'`.
|
||
|
||
Règle : un re-export ne crée pas de binding local. Si la fonction est utilisée dans le même fichier, ajouter un `import` séparé en plus du `export`.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / code-review story 2-4
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review 2.4 — bug de boucle infinie potentielle : `resolvePublicPageAccess("home", ...)` redirige vers `buildLocalizedPath("home")` = `/`. Si `home` est désactivé, la page `/` redirige vers `/` indéfiniment. Next.js absorbe la boucle silencieusement mais le comportement utilisateur est cassé.
|
||
|
||
Proposition :
|
||
**Anti-pattern : redirect vers la destination désactivée elle-même**
|
||
|
||
Dans un mécanisme de redirection sur page désactivée (feature flags, pages publiques), toujours vérifier que la destination de fallback n'est pas la page en cours. Cas typique : `home` désactivé → redirect vers `/` (qui est `home`) → boucle.
|
||
|
||
Règle : si `pageKey === fallbackKey`, ne pas rediriger. Retourner `null` (accès non bloqué ou comportement non défini en V1) plutôt que de boucler.
|
||
|
||
```ts
|
||
if (pageKey === "home") return null; // évite redirect home → home
|
||
return buildLocalizedPath(locale, "home");
|
||
```
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / code-review story 2-4
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 2.4 — mécanisme centralisé d'activation de pages/features par tenant, reutilisable sans duplication dans chaque page ou composant.
|
||
|
||
Proposition :
|
||
**Pattern : helper centralisé d'activation de features tenant-scoped**
|
||
|
||
Pour les features activables par tenant (ex: pages publiques, modules optionnels), centraliser la logique dans un helper pur distinct :
|
||
|
||
```ts
|
||
// src/server/public/publicPagesConfig.ts
|
||
export function isPublicPageEnabled(
|
||
config: PublicPagesConfigRecord | null | undefined,
|
||
pageKey: PublicPageKey
|
||
): boolean {
|
||
if (!config) return true; // config absente = tout activé par défaut
|
||
return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
|
||
}
|
||
```
|
||
|
||
Principes :
|
||
- Le helper est pur (pas d'I/O, testable sans Prisma).
|
||
- La config est chargée une seule fois par le module d'entrée (`getPublicPagesConfigFromEnv`).
|
||
- Les composants de navigation et les pages importent `isPublicPageEnabled` depuis ce module — jamais depuis Prisma directement.
|
||
- Comportement par défaut sain : `null`/`undefined` → tout activé (évite les régressions si la config n'a pas été provisionnée).
|
||
|
||
Ce pattern s'applique à tous les feature flags tenant-scoped : pages, modules, intégrations tierces.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-5
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Story 2.5 — l'URL de réservation était déjà dans `PublicHomeProfile.reservationUrl`. Créer un modèle ou un champ dédié aurait introduit une duplication de donnée sans bénéfice en V1.
|
||
|
||
Proposition :
|
||
**Pattern : réutiliser un champ existant plutôt que créer un modèle dédié pour un besoin V1**
|
||
|
||
Avant d'ajouter un nouveau modèle ou une nouvelle table pour stocker une configuration simple, vérifier si le schéma existant ne contient pas déjà le champ. Une URL de réservation externe est une donnée de profil tenant — elle appartient naturellement à `PublicHomeProfile`, pas à un modèle `ReservationConfig` séparé.
|
||
|
||
Règle : ne créer un modèle dédié que si la configuration a un cycle de vie, des relations, ou des cardinalités qui ne correspondent pas à un champ simple dans un modèle existant. En V1, un champ optionnel dans le modèle le plus proche est suffisant.
|
||
|
||
---
|
||
|
||
2026-03-17 — app-template-resto / story 2-5
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Story 2.5 — l'URL de réservation vient d'une config tenant et est rendue dans un lien public. Sans validation côté serveur, une URL mal formée ou avec un protocole non-https pourrait être injectée dans le HTML.
|
||
|
||
Proposition :
|
||
**Pattern : valider le protocole d'une URL externe avant de la passer à un lien public**
|
||
|
||
Toute URL provenant d'une config tenant et rendue dans un `<a href>` public doit être validée côté serveur avant d'être transmise au composant :
|
||
|
||
```ts
|
||
function isSafeUrl(url: string): boolean {
|
||
try {
|
||
const { protocol } = new URL(url);
|
||
return protocol === "https:" || protocol === "http:";
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
// Retourner null si invalide — le composant gère l'absence d'URL
|
||
if (!url || !isSafeUrl(url)) return null;
|
||
```
|
||
|
||
Règle : ne jamais passer directement une URL de base de données dans un `<a href>` sans validation. Le composant UI reçoit soit une URL valide soit `null` — jamais une chaîne non vérifiée. Cela prévient les injections `javascript:` ou les URLs malformées.
|
||
|
||
---
|
||
|
||
2026-03-20 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Un agent dev a livré un NestJS controller syntaxiquement cassé (méthodes imbriquées, route dupliquée 3x) sans que TypeScript ne l'ait bloqué avant commit. Le code ne pouvait pas compiler mais le statut story était "completed".
|
||
|
||
Proposition :
|
||
## Risque : Controller NestJS corrompu par insertions multiples
|
||
|
||
Un agent qui insère des endpoints dans un controller existant peut briser la syntaxe TypeScript (méthodes imbriquées, décorateurs orphelins, routes dupliquées) si les modifications sont faites par concaténation plutôt que par réécriture structurée.
|
||
|
||
Symptômes typiques :
|
||
- `@Get('/route')` apparaît dans le corps d'une autre méthode
|
||
- La même route est déclarée 2-3 fois dans le même controller
|
||
- Le compilateur TypeScript ne catch pas toujours cela (dépend de la position dans l'AST)
|
||
|
||
Règle : Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original — ne jamais insérer par blocs séparés.
|
||
|
||
---
|
||
|
||
2026-03-20 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Quota TTL calculé avec heure locale serveur au lieu d'UTC — crée une dérive du reset de quota pouvant aller jusqu'à +/-12h selon le timezone serveur. Découvert en review adversariale story 4.3.
|
||
|
||
Proposition :
|
||
## Risque : TTL Redis quota calculé en heure locale
|
||
|
||
Toujours calculer le TTL des quotas journaliers en UTC :
|
||
|
||
```typescript
|
||
// ✅ CORRECT — UTC midnight garanti
|
||
const midnight = new Date(
|
||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
|
||
);
|
||
const ttlMs = midnight.getTime();
|
||
|
||
// ❌ RISQUÉ — heure locale du serveur
|
||
const endOfDay = new Date();
|
||
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
|
||
```
|
||
|
||
Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` pour le calcul. Vérifier systématiquement en review.
|
||
|
||
---
|
||
|
||
2026-03-20 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Un agent a marqué une story "completed" alors que le store mobile, le service mobile et les tests e2e étaient déclarés ❌ non implémentés dans son propre Dev Agent Record. La story Status ne doit jamais être "completed" si des ACs ou tâches sont marquées ❌.
|
||
|
||
Proposition :
|
||
## Anti-pattern : Story "completed" avec tâches ❌ auto-déclarées
|
||
|
||
Si le Dev Agent Record liste explicitement des items ❌ (non implémentés), le Status de la story doit être `in-progress` ou `review` — jamais `completed`.
|
||
|
||
Règle : avant de setter `Status: completed`, vérifier que le Dev Agent Record ne contient aucun ❌. En cas de doute, setter `Status: review` pour déclencher la code review.
|
||
|
||
---
|
||
|
||
2026-03-20 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review Story 4.4 — l'état "isBookmarked" était hardcodé à `false` dans l'écran thread detail, ce qui rendait le bouton bookmark toujours en mode "ajouter" sans jamais réfléchir l'état réel. Anti-pattern récurrent quand l'état vient du store mais n'est pas dérivé correctement.
|
||
|
||
Proposition :
|
||
## Anti-pattern : état booléen UI dérivé hardcodé au lieu d'être calculé depuis le store
|
||
|
||
Dans un écran qui affiche un état toggle (bookmarké, liké, suivi), ne jamais initialiser l'état via `const isX = false` avec un commentaire "géré ci-dessous". L'état doit toujours être dérivé du store au moment du rendu :
|
||
|
||
```typescript
|
||
// ❌ Anti-pattern — state hardcodé, jamais mis à jour
|
||
const isBookmarked = false; // état local géré ci-dessous via state
|
||
|
||
// ✅ Pattern correct — dérivé du store au rendu
|
||
const { bookmarks } = useCommunityStore();
|
||
const isBookmarked = bookmarks.some((b) => b.thread.id === threadId);
|
||
```
|
||
|
||
Règle : si le store contient la liste (bookmarks, likes, follows), l'état booléen se dérive avec `.some()` ou `.has()` — pas de state local redondant. Cela garantit la cohérence entre les écrans sans synchronisation manuelle.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / code-review story 2.8
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Un agent (GPT-5 Codex) a marqué une story done avec une File List réduite à 2 fichiers meta, une completion note générique ("Ultimate context engine analysis completed"), et aucun code écrit. Le contexte d'exécution de cet agent était probablement dégradé (timeout, quota).
|
||
|
||
Proposition :
|
||
## Anti-pattern : Story "done" avec File List vide de fichiers source
|
||
|
||
Un agent peut halluciner la completion d'une story en produisant une note générique sans écrire de code. Signal d'alerte : la File List ne contient que des fichiers `_bmad-output/` (story file, sprint-status) mais aucun fichier `src/`, `prisma/`, `tests/`.
|
||
|
||
Règle : lors d'une code review, si la File List ne contient aucun fichier source, traiter la story comme non implémentée. Vérifier avec `git log --follow src/` pour confirmer l'absence de commits. Ne pas faire confiance au status `done` sans preuve dans le code.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 2.8
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur story 2.8 — le mode embed click-to-load est une décision de performance et de consentement implicite : aucun tiers ne se charge sans action explicite utilisateur.
|
||
|
||
Proposition :
|
||
**Pattern : click-to-load strict pour les embeds tiers (iframe/widget)**
|
||
|
||
Pour tout embed tiers chargé à la demande (module de réservation, map, chat) :
|
||
- La page se rend initialement sans l'iframe (état `loaded=false`)
|
||
- Un bouton explicite déclenche le chargement (`onClick={() => setLoaded(true)}`)
|
||
- L'iframe est conditionnellement rendu : `{loaded && <iframe src={url} />}`
|
||
- Un fallback actionnable (lien externe + `tel:`) est toujours disponible en cas d'erreur iframe (`onError={() => setErrored(true)}`)
|
||
|
||
Ce pattern respecte les principes de performance-first (LCP non pollué par des tiers) et de consentement implicite (aucun tiers ne reçoit de données utilisateur sans action volontaire).
|
||
|
||
---
|
||
|
||
2026-03-20 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review Story 4.4 — `isBookmarking` servait à la fois de flag pour les opérations add/remove ET comme indicateur de chargement de la liste. Dans l'écran bookmarks, cela provoquait un spinner manquant au premier chargement si un add/remove était en cours en parallèle depuis un autre écran.
|
||
|
||
Proposition :
|
||
## Anti-pattern : réutiliser un seul flag `isLoading` pour des opérations de natures différentes
|
||
|
||
Quand un store gère plusieurs types d'opérations asynchrones sur la même ressource (chargement de liste ET mutations), utiliser des flags distincts :
|
||
|
||
```typescript
|
||
// ❌ Anti-pattern — un seul flag pour tout
|
||
isBookmarking: boolean; // add/remove ET chargement liste
|
||
|
||
// ✅ Pattern correct — séparation claire
|
||
isBookmarking: boolean; // opérations add/remove (mutation)
|
||
isLoadingBookmarks: boolean; // chargement de la liste (requête GET)
|
||
```
|
||
|
||
L'écran de liste utilise `isLoadingBookmarks` pour le spinner initial. L'écran de détail utilise `isBookmarking` pour désactiver le bouton pendant une mutation. Les deux états sont indépendants et peuvent être vrais simultanément sans conflit visuel.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 2.7
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Anti-pattern détecté : un flag `decided` manquant dans un état de consentement cookies cause un bug UX silencieux — le banner réapparaît à chaque visite après un refus explicite, car `!analytics` et "pas de cookie" sont indistinguables.
|
||
|
||
Proposition :
|
||
## Consent state : toujours distinguer "pas de décision" de "refus explicite"
|
||
|
||
Un état de consentement booléen `analytics: boolean` est insuffisant. `false` peut signifier deux choses distinctes : pas encore de cookie (première visite) ou refus explicite (cookie présent). Sans champ `decided`, le banner de consentement réapparaît à chaque visite après un refus, violant l'AC de persistance du choix.
|
||
|
||
Pattern validé :
|
||
```typescript
|
||
type ConsentState = {
|
||
analytics: boolean;
|
||
decided: boolean; // true = cookie présent, l'utilisateur a fait un choix
|
||
};
|
||
|
||
const DEFAULT: ConsentState = { analytics: false, decided: false };
|
||
|
||
// À la lecture du cookie :
|
||
if (!cookieValue) return DEFAULT; // decided=false
|
||
return { analytics: parsed.analytics, decided: true }; // decided=true
|
||
```
|
||
|
||
L'état initial du banner doit être `!decided`, pas `!analytics`.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 2.7
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Anti-pattern sécurité : injection d'une valeur de configuration dans un `<Script>` inline via interpolation de template string au lieu de JSON.stringify. Même avec une regex de validation en amont, l'interpolation directe est fragile si le validateur évolue.
|
||
|
||
Proposition :
|
||
## Script inline : toujours passer les valeurs via JSON.stringify
|
||
|
||
Lors de l'injection de valeurs dans des scripts Next.js inline (`<Script id="...">`) :
|
||
|
||
```tsx
|
||
// ❌ Anti-pattern — interpolation directe
|
||
{`gtag('config', '${measurementId}');`}
|
||
|
||
// ✅ Pattern correct — JSON.stringify garantit l'échappement
|
||
{`gtag('config', ${JSON.stringify(measurementId)});`}
|
||
```
|
||
|
||
S'applique aussi aux `dangerouslySetInnerHTML` et aux attributs `data-*` injectés en JS.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.1
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
En Prisma, une vérification préalable d'appartenance tenant + un `$transaction` avec `update({ where: { id } })` crée une fenêtre TOCTOU : entre le check et l'écriture, un autre tenant pourrait théoriquement s'intercaler si un bug upstream laisse passer un id cross-tenant. Détecté en code review sur la story 3.1.
|
||
|
||
Proposition :
|
||
## Prisma $transaction multi-tenant : toujours inclure tenantId dans chaque WHERE
|
||
|
||
Même après un check préalable d'isolation, les `update`/`delete` individuels dans une `$transaction` doivent inclure `tenantId` dans le WHERE. Utiliser `updateMany`/`deleteMany` (pas `update`/`delete`) pour pouvoir inclure tenantId sans lever d'exception si 0 lignes trouvées.
|
||
|
||
```typescript
|
||
// ❌ Anti-pattern — check OK mais écriture sans tenantId (fenêtre TOCTOU)
|
||
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
|
||
if (existing.length !== ids.length) throw new HttpError("...", { status: 404 });
|
||
await prisma.$transaction(
|
||
ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
|
||
);
|
||
|
||
// ✅ Pattern correct — tenantId dans chaque écriture (défense en profondeur)
|
||
await prisma.$transaction(
|
||
ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
|
||
);
|
||
```
|
||
|
||
Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure `tenantId` dans le WHERE, même dans une transaction précédée d'un check.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.1
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
`window.location.reload()` utilisé après un server action dans un client component Next.js App Router. C'est un full reload (perd l'état React, navigation complète, plus lent). `router.refresh()` est le bon outil : il retrigger le fetch des server components sans détruire l'état client.
|
||
|
||
Proposition :
|
||
## Next.js App Router : router.refresh() et non window.location.reload() après un Server Action
|
||
|
||
```tsx
|
||
// ❌ Anti-pattern — full reload, perd l'état client, navigation complète
|
||
await createCategoryAction(formData);
|
||
window.location.reload();
|
||
|
||
// ✅ Pattern correct — RSC diff, préserve l'état client
|
||
const router = useRouter();
|
||
await createCategoryAction(formData);
|
||
router.refresh();
|
||
```
|
||
|
||
`router.refresh()` : Next.js refetch uniquement les server components affectés (ceux dont le segment est invalidé par `revalidatePath`), et applique un diff. L'état des client components (`useState`, scroll, focus) est préservé.
|
||
|
||
S'applique systématiquement après tout Server Action qui mute des données et doit mettre à jour l'UI.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.1
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Stale closure classique dans un rollback optimiste avec `useTransition`. Le snapshot est capturé après `setCategories(newList)`, donc `categories` peut déjà référencer la nouvelle liste au moment du rollback.
|
||
|
||
Proposition :
|
||
## useTransition + optimistic update : capturer le snapshot AVANT setState
|
||
|
||
```tsx
|
||
// ❌ Anti-pattern — snapshot capturé après setCategories (closure sur la nouvelle valeur)
|
||
const newList = [...categories];
|
||
setCategories(newList); // batching async, categories peut déjà pointer vers newList
|
||
startTransition(async () => {
|
||
try { await action(); }
|
||
catch { setCategories(categories); } // ← peut être newList, pas l'ancienne liste
|
||
});
|
||
|
||
// ✅ Pattern correct — snapshot explicite avant toute mutation d'état
|
||
const snapshot = categories; // capturer avant setCategories
|
||
setCategories(newList);
|
||
startTransition(async () => {
|
||
try { await action(); }
|
||
catch { setCategories(snapshot); } // rollback garanti vers l'ancienne liste
|
||
});
|
||
```
|
||
|
||
Règle : dans tout optimistic update avec rollback, toujours assigner le snapshot dans une variable `const` **avant** le premier `setState`.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.1
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
`window.confirm()` utilisé dans un client component Next.js pour confirmer une suppression. Bloque le thread JS principal, incompatible SSR, pauvre sur mobile, et non stylable. À remplacer systématiquement par un état de confirmation inline.
|
||
|
||
Proposition :
|
||
## window.confirm() : ne jamais utiliser dans une app React/Next.js
|
||
|
||
`window.confirm()` bloque le thread principal, ne fonctionne pas en SSR, est non stylable et l'UX mobile est mauvaise.
|
||
|
||
Pattern de remplacement : état de confirmation inline.
|
||
|
||
```tsx
|
||
// ❌ Anti-pattern
|
||
if (!confirm("Supprimer ?")) return;
|
||
await deleteAction(id);
|
||
|
||
// ✅ Pattern correct — confirmation inline via état React
|
||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||
|
||
// Bouton "Supprimer" → setDeletingId(id)
|
||
// Inline dans la liste :
|
||
{deletingId === item.id && (
|
||
<div>
|
||
<span>Supprimer « {item.label} » ?</span>
|
||
<button onClick={() => { setDeletingId(null); doDelete(item.id); }}>Confirmer</button>
|
||
<button onClick={() => setDeletingId(null)}>Annuler</button>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
S'applique aussi à `window.alert()` et `window.prompt()`.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.2 code review
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Filtre Prisma `{ isSystem: true }` sans `tenantId: null` sur un modèle à tenantId nullable : si un bug crée un tag `isSystem: true` avec un tenantId non-null, il sera exposé à tous les tenants. Pattern détecté en review — défense en profondeur sur les filtres OR multi-tenant.
|
||
|
||
Proposition :
|
||
## Prisma OR multi-tenant : toujours être explicite sur tenantId: null pour les ressources système
|
||
|
||
Quand un modèle a un `tenantId` nullable pour distinguer ressources "système" (globales) et ressources "tenant" (privées), le filtre OR doit inclure `tenantId: null` explicitement sur la branche système.
|
||
|
||
```typescript
|
||
// ❌ Trop permissif — un tag isSystem:true avec un tenantId non-null serait exposé à tous
|
||
OR: [{ isSystem: true }, { tenantId, isSystem: false }]
|
||
|
||
// ✅ Défense en profondeur — double condition sur la branche système
|
||
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
|
||
```
|
||
|
||
Règle : sur tout modèle `tenantId?` (nullable) + flag de type `isSystem`/`isGlobal`/`isPublic`, la branche "ressource publique" du filtre OR doit toujours inclure `tenantId: null`.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.3 code review
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern détecté : extraire les fonctions utilitaires pures (slugify, formatters) dans un module dédié sans "server-only" permet de les partager entre le repository serveur et les tests unitaires purs. Sans ça, on duplique la logique et les tests peuvent diverger silencieusement.
|
||
|
||
Proposition :
|
||
## Utilitaires purs : extraire dans un module partagé sans "server-only"
|
||
|
||
Les fonctions pures (slugify, formatters, validators) utilisées par les repositories doivent être extraites dans un module sans `import "server-only"`, afin d'être importables directement par les tests unitaires.
|
||
|
||
```
|
||
src/server/menuAdmin/
|
||
allergensRepository.ts ← import { slugify } from "./slugify"
|
||
slugify.ts ← export function slugify() {} (pas de "server-only")
|
||
tests/
|
||
allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
|
||
```
|
||
|
||
Avantage : si la logique slugify change, les tests le détectent immédiatement (même implémentation). Sans ça, la copie dans les tests diverge silencieusement = faux positifs.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.3 code review
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Pattern TOCTOU : même avec un pre-check applicatif (findFirst/findUnique), une race condition peut déclencher une violation de contrainte unique Prisma (P2002). Sans catch, l'erreur Prisma brute remonte au client. Toujours catcher P2002 et la convertir en 409 propre.
|
||
|
||
Proposition :
|
||
## Prisma unique constraint violation (P2002) : toujours catcher après pre-check
|
||
|
||
Un pre-check applicatif ne suffit pas contre les race conditions. Toujours ajouter un try/catch sur les writes qui ont une contrainte unique :
|
||
|
||
```typescript
|
||
import { Prisma } from "@prisma/client";
|
||
|
||
try {
|
||
await prisma.item.updateMany({ where: { id, tenantId }, data: { ... } });
|
||
} catch (err) {
|
||
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
||
throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
|
||
}
|
||
throw err;
|
||
}
|
||
```
|
||
|
||
S'applique à `create`, `update`, `updateMany`, `upsert` sur des modèles avec contraintes uniques.
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.4
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Bug réel découvert en review : `DishForm` utilisait `defaultValue`/`defaultChecked` (props de montage uniquement) sans `key` prop. Changer la cible d'édition (dish A → dish B) re-rendait le composant sans le démonter, laissant les anciens champs affichés.
|
||
|
||
Proposition :
|
||
## Anti-pattern : formulaire React avec defaultValue sans key
|
||
|
||
**Contexte :** Formulaire d'édition réutilisé pour plusieurs entités (ex: liste de plats avec bouton "Modifier").
|
||
|
||
**Risque :** `defaultValue`, `defaultChecked`, `defaultSelected` ne s'appliquent qu'au montage. Si le composant est réutilisé (même nœud DOM, nouvelle prop) sans être démonté, les valeurs ne se mettent pas à jour.
|
||
|
||
**Symptôme :** l'utilisateur édite l'entité A, clique sur "Modifier" pour l'entité B → le formulaire affiche toujours les données de A.
|
||
|
||
**Fix obligatoire :** ajouter une `key` unique sur le composant formulaire, basée sur l'ID de l'entité éditée (ou sur un discriminant de mode pour create/edit) :
|
||
```tsx
|
||
<EntityForm
|
||
key={formState.mode === "edit" ? formState.entity.id : `create-${formState.contextId}`}
|
||
...
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto / story 3.4
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Race condition détectée : `maxOrder` calculé hors transaction, puis utilisé dans une transaction séparée pour le `create`. Deux requêtes concurrentes obtiennent le même `maxOrder` et créent deux entités avec le même `sortOrder`.
|
||
|
||
Proposition :
|
||
## Anti-pattern : calcul de nextOrder hors transaction
|
||
|
||
**Contexte :** Entités avec un champ `sortOrder` auto-incrémenté dans un scope (ex: plats d'une catégorie).
|
||
|
||
**Risque :** Si l'aggregate `MAX(sortOrder)` est calculé en dehors de la transaction qui crée l'entité, deux requêtes concurrentes peuvent obtenir le même max et créer deux entités avec le même `sortOrder`.
|
||
|
||
**Fix :** déplacer le calcul du `nextOrder` à l'intérieur de la transaction interactive :
|
||
```typescript
|
||
return prisma.$transaction(async (tx) => {
|
||
const maxOrder = await tx.entity.aggregate({
|
||
where: { tenantId, scopeId },
|
||
_max: { sortOrder: true },
|
||
});
|
||
const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
|
||
return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
2026-03-21 — app-template-resto
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern EN enforcement multi-entités validé sur projet réel (story 3.5) — réutilisable pour toute app avec internationalisation optionnelle par tenant.
|
||
|
||
Proposition :
|
||
|
||
## EN enforcement optionnel par tenant (toggle + publish gate)
|
||
|
||
**Contexte :** tenant peut activer/désactiver l'obligation de remplir les champs traduits EN.
|
||
|
||
**Pattern :**
|
||
1. `enableEn Boolean @default(false)` sur le modèle `Tenant`.
|
||
2. `getEnConfig(tenantId)` — `findUnique` sur PK, appelé dans chaque action mutante (create/update).
|
||
3. Dans chaque action : `if (enableEn && !labelEn) throw new HttpError("...", { status: 400 })`.
|
||
4. `checkEnCompleteness(tenantId)` — 4 requêtes parallèles (`Promise.all`) pour catégories, plats, tags custom, allergènes — exclut les entités système (`isSystem: true`, `tenantId: null`) et les entités masquées (`isVisible: false`).
|
||
5. Gate publish : `if (!result.complete) throw new HttpError("...", { status: 422 })`.
|
||
|
||
**Règles :**
|
||
- `isVisible: false` n'est pas inclus dans le check de complétude (une entité masquée ne bloque pas la publication).
|
||
- Les system tags (tenantId null) sont exclus du check.
|
||
- `revalidatePath` sur **toutes** les pages menu après toggle (pas seulement `/settings`) pour rafraîchir l'état required/optional des champs dans chaque liste.
|
||
|
||
---
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern toggle optimiste avec rollback validé (story 3.5 — settings EN toggle).
|
||
|
||
Proposition :
|
||
|
||
## Toggle optimiste avec rollback (React Server Action)
|
||
|
||
```tsx
|
||
const [optimistic, setOptimistic] = useState(initialValue);
|
||
|
||
async function handleToggle() {
|
||
const prev = optimistic;
|
||
setOptimistic(!prev); // update immédiat
|
||
try {
|
||
await toggleAction(!prev);
|
||
router.refresh();
|
||
} catch {
|
||
setOptimistic(prev); // rollback si erreur
|
||
}
|
||
}
|
||
```
|
||
|
||
Convient aux toggles boolean où la latence serveur doit être masquée. Le `router.refresh()` après succès synchronise le Server Component parent.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.5
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
`import type` depuis `src/server/**` dans un composant `"use client"` est passé à travers la review initiale car c'est un type-only import (effacé à la compilation). Mais ça viole la guardrail architecture et ouvre la porte à des imports runtime si refactoré rapidement. Pattern récurrent à surveiller.
|
||
|
||
Proposition :
|
||
**Anti-pattern : `import type` depuis `src/server/**` dans des composants client**
|
||
Même un `import type` depuis `src/server/**` dans un fichier `"use client"` est une violation de boundary. La règle ESLint `no-restricted-imports` doit couvrir les imports de type aussi (option `allowTypeImports: false`). Les types partagés entre server et client doivent vivre dans `src/types/` ou `src/lib/`.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.5
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Les composants dashboard livrés avec des inline `style={{...}}` passent en review sans alerte. C'est une violation silencieuse du pattern UI (Tailwind + tokens CSS) qui s'accumule si non bloquée dès la code review.
|
||
|
||
Proposition :
|
||
**Anti-pattern : inline styles dans les composants dashboard**
|
||
Les styles inline (`style={{...}}`) contournent le système Tailwind + tokens CSS et créent des incohérences visuelles non détectées par le linter. À bloquer en code review systématiquement pour tout composant dashboard. Exception acceptable : animations CSS dynamiques (valeurs calculées au runtime) uniquement.
|
||
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto (story 3.6)
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Classes Tailwind invalides trouvées en code review et produisant des bugs silencieux (item masqué affiché à pleine opacité).
|
||
|
||
Proposition :
|
||
## Tailwind — classes invalides courantes (bugs silencieux)
|
||
|
||
- `opacity-55` → invalide. Scale par défaut : 0/5/10/20/25/30/40/50/60/70/75/80/90/95/100. Utiliser `opacity-50` ou `opacity-60`.
|
||
- `w-35` → invalide. Scale saute de `w-32` à `w-36`. Utiliser `w-36`.
|
||
- `box-border` → redondant. Tailwind Preflight applique déjà `box-sizing: border-box` globalement. Supprimer.
|
||
- Toujours vérifier les classes custom/non-standard avec `npx tailwindcss --content "..." --dry-run` ou l'extension IntelliSense.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto (story 3.6)
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern mobile-first pour les grilles FR/EN côte à côte.
|
||
|
||
Proposition :
|
||
## Grilles 2 colonnes FR/EN — mobile-first
|
||
|
||
Pour les formulaires avec champs FR + EN côte à côte, toujours utiliser un breakpoint responsive :
|
||
```
|
||
grid grid-cols-1 sm:grid-cols-2
|
||
```
|
||
`grid-cols-2` sans breakpoint produit des colonnes trop étroites sur mobile (< 640px). Le projet est mobile-first — les formulaires dashboard doivent être utilisables sur téléphone.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.7
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review a trouvé `<img>` HTML natif dans un composant public Next.js, causant un warning ESLint `@next/next/no-img-element` et un risque CLS. Pattern récurrent à surveiller dans les nouveaux composants.
|
||
|
||
Proposition :
|
||
## Vigilance Next.js — `<img>` natif interdit dans les composants
|
||
|
||
Toujours utiliser `<Image>` de `next/image` à la place de `<img>` natif dans les composants Next.js.
|
||
- `<img>` natif déclenche le warning ESLint `@next/next/no-img-element`
|
||
- Le projet a `--max-warnings=0` : tout warning ESLint = erreur de CI
|
||
- `<Image>` apporte lazy loading, optimisation WebP, et évite les layout shifts (CLS)
|
||
|
||
Exception acceptable : composants de test ou storybook.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.7
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern de validation URL (mediaUrl) identifié : toujours valider le format ET le protocole côté serveur quand un champ URL est stocké et rendu dans le HTML. Validé en review story 3.7.
|
||
|
||
Proposition :
|
||
## Validation champs URL côté serveur
|
||
|
||
Quand un champ URL libre est saisi par l'utilisateur et rendu dans le HTML (img src, a href, iframe src), toujours appliquer côté serveur :
|
||
1. Validation de format : `new URL(value)` dans un try/catch
|
||
2. Validation de protocole : `startsWith("https://")` minimum (ou `http://` si nécessaire)
|
||
3. Longueur max (500 chars recommandé)
|
||
|
||
Pattern validé :
|
||
```ts
|
||
if (mediaUrl) {
|
||
try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
|
||
if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
|
||
throw new HttpError("URL doit commencer par https://.", { status: 400 });
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.8
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review story 3.8 a identifié un `useTransition` global pour gérer le pending d'une liste de plats. Quand une opération est en cours sur un plat, `isPending` désactive tous les boutons de tous les plats — friction mobile importante sur des listes longues.
|
||
|
||
Proposition :
|
||
## Anti-pattern : useTransition global pour des listes d'items interactifs
|
||
|
||
**Contexte :** liste d'items avec des actions par item (toggle visibilité, déplacer, supprimer).
|
||
|
||
**Risque :** `useTransition` expose un `isPending` global. Si une opération est en cours sur l'item A, tous les boutons de tous les items B, C, D... sont désactivés. Sur mobile, l'UX est bloquée.
|
||
|
||
**Symptôme :** l'utilisateur clique sur "Masquer" pour le plat A, et constate que les boutons des plats B et C sont grisés jusqu'à la fin de l'opération.
|
||
|
||
**Fix :** remplacer `useTransition` par un `pendingId: string | null` par item :
|
||
|
||
```tsx
|
||
// ❌ Avant — bloque tout
|
||
const [isPending, startTransition] = useTransition();
|
||
startTransition(async () => { await toggleAction(id); });
|
||
// render : disabled={isPending} ← désactive TOUS les items
|
||
|
||
// ✅ Après — per-item
|
||
const [pendingId, setPendingId] = useState<string | null>(null);
|
||
|
||
function handleToggle(id: string) {
|
||
setPendingId(id);
|
||
(async () => {
|
||
try { await toggleAction(id); }
|
||
catch (err) { handleError(err); }
|
||
finally { setPendingId(null); }
|
||
})();
|
||
}
|
||
// render : disabled={pendingId === item.id} ← désactive uniquement l'item en cours
|
||
```
|
||
|
||
**Règles :**
|
||
- Utiliser `pendingId === item.id` pour les boutons d'item.
|
||
- Utiliser `pendingId !== null` pour les boutons globaux (ex: "Ajouter" en haut de liste) qui ne doivent pas coexister avec une mutation en cours.
|
||
- Pour le create form, utiliser une sentinelle : `setPendingId("creating")`.
|
||
- `finally` garantit la réinitialisation même en cas d'erreur.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / code-review story 3.8
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Code review story 3.8 a trouvé des `useCallback` sur des handlers passés à un composant mémoïsé (`React.memo`), mais ces handlers étaient re-wrappés dans des arrows inline au moment du rendu. Le bénéfice de `useCallback` était donc nul, et `memo` n'était pas protégé.
|
||
|
||
Proposition :
|
||
## Anti-pattern : useCallback inutile quand les callbacks sont wrappés en inline au render
|
||
|
||
**Contexte :** composant enfant mémoïsé avec `React.memo`, handlers stables via `useCallback` dans le parent.
|
||
|
||
**Risque :** si le handler stable est re-wrappé dans une arrow inline lors du passage en prop, une nouvelle référence est créée à chaque render — `memo` ne peut pas éviter le re-render de l'enfant.
|
||
|
||
**Symptôme :**
|
||
```tsx
|
||
const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
|
||
|
||
// Mais au render :
|
||
<ItemCard onToggle={() => handleToggle(item.id)} />
|
||
// ↑ nouvelle closure à chaque render → memo inutile
|
||
```
|
||
|
||
**Règles :**
|
||
- `useCallback` n'a de valeur que si le callback est passé **directement** en prop, sans re-wrapping.
|
||
- Si la signature du callback doit capturer des variables de boucle (ex: `item.id`), deux options valides :
|
||
1. Passer les données nécessaires en props et laisser l'enfant appeler le handler avec ses propres props.
|
||
2. Accepter que `memo` ne soit pas protégé pour ces props-là et supprimer le `useCallback` inutile.
|
||
- Ne pas laisser un `useCallback` "pour faire bien" si son effet réel est nul — c'est du bruit.
|
||
|
||
---
|
||
|
||
2026-03-22 — app-template-resto / story 3.8
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_frontend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Story 3.8 a systématisé le pattern "Server Action retourne l'entité créée/modifiée → mise à jour état local sans router.refresh()". Validé sur 5 entités (categories, allergens, tags, dishes, team members). Plus performant que le toggle optimiste + router.refresh() déjà capitalisé.
|
||
|
||
Proposition :
|
||
## Server Action retournant l'entité → élimination de router.refresh() sur create/edit
|
||
|
||
**Contexte :** liste d'items managée côté client (`useState`), avec création et modification via Server Actions.
|
||
|
||
**Pattern :** le repository retourne l'entité complète après mutation ; l'action la propage au client ; le client met à jour son état local directement.
|
||
|
||
```ts
|
||
// Repository — retourne l'entité complète
|
||
export async function createItem(tenantId: string, data: Input): Promise<ItemRow> {
|
||
return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } });
|
||
}
|
||
|
||
// Action — retourne la donnée au client
|
||
export async function createItemAction(formData: FormData): Promise<ItemRow> {
|
||
const actor = await requireOwner();
|
||
// ... validation ...
|
||
const item = await createItem(actor.tenantId, input);
|
||
revalidatePath("/dashboard/...");
|
||
return item; // ← clé : retourner l'entité
|
||
}
|
||
|
||
// Client — mise à jour locale sans round-trip SSR
|
||
const created = await createItemAction(formData);
|
||
setItems((prev) => [...prev, created]); // ← pas de router.refresh()
|
||
```
|
||
|
||
**Avantages vs toggle optimiste + router.refresh() :**
|
||
- Zéro aller-retour SSR supplémentaire (économie ~500ms–2s sur mobile).
|
||
- L'état local est garanti cohérent avec la DB (données réelles, pas une valeur calculée localement).
|
||
- Pas de flash de rechargement.
|
||
|
||
**Règles :**
|
||
- Pour les entités avec relations (tags, allergens), utiliser un helper `findXxxById(tenantId, id)` appelé après la mutation pour retourner la forme complète avec les relations résolues.
|
||
- `revalidatePath` reste nécessaire pour invalider le cache des pages publiques.
|
||
- Ce pattern convient au create et à l'update. Pour les simples toggles boolean (visibilité, disponibilité), le pattern optimiste avec rollback est suffisant (pas besoin de re-fetcher l'entité entière).
|
||
|
||
---
|
||
|
||
2026-03-23 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_risques_et_vigilance.md
|
||
|
||
Pourquoi :
|
||
Bug silencieux validé en production sur app-alexandrie Story 4.5 : `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` ne protège rien. Le guard laisse passer toutes les requêtes sans lever d'erreur.
|
||
|
||
Proposition :
|
||
### NestJS — AdminRoleGuard : toujours utiliser les deux décorateurs ensemble
|
||
|
||
`AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()`. Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer la requête sans vérification.
|
||
|
||
**Règle :** toujours utiliser `@UseGuards(AdminRoleGuard)` ET `@RequireAdminRole()` ensemble sur un endpoint admin.
|
||
|
||
```typescript
|
||
// ✅ Correct
|
||
@Post('admin/ressource')
|
||
@UseGuards(AdminRoleGuard)
|
||
@RequireAdminRole()
|
||
async createRessource(...) {}
|
||
|
||
// ❌ Silencieusement non protégé
|
||
@Post('admin/ressource')
|
||
@UseGuards(AdminRoleGuard) // ← @RequireAdminRole() manquant → toutes les requêtes passent
|
||
async createRessource(...) {}
|
||
```
|
||
|
||
S'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur. Vérifier le pattern systématiquement lors des code reviews d'endpoints admin.
|
||
|
||
---
|
||
|
||
2026-03-23 — app-alexandrie
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern validé sur app-alexandrie Story 4.5 pour créer une migration Prisma quand la shadow DB est interdite (permission denied to create database — typique des DB managées comme Supabase, PlanetScale, RDS avec permissions limitées).
|
||
|
||
Proposition :
|
||
### Prisma — Migration manuelle sans shadow DB (P3014)
|
||
|
||
Quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database` (DB managée, permissions restreintes) :
|
||
|
||
```bash
|
||
# 1. Écrire le SQL manuellement dans le dossier migration
|
||
mkdir -p prisma/migrations/<timestamp>_<nom>
|
||
# Créer migration.sql à la main
|
||
|
||
# 2. Appliquer le SQL directement en DB
|
||
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql
|
||
|
||
# 3. Marquer la migration comme appliquée dans _prisma_migrations
|
||
npx prisma migrate resolve --applied <timestamp>_<nom>
|
||
|
||
# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts
|
||
```
|
||
|
||
**Cas d'usage :** DB managées (Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser). Ne pas utiliser `prisma db push` en production — il ne versionne pas les migrations.
|
||
|
||
# Format attendu
|
||
|
||
Chaque proposition doit suivre ce format :
|
||
|
||
```
|
||
DATE — PROJET
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : <10_backend_patterns_valides.md | 10_frontend_patterns_valides.md | 10_ux_patterns_valides.md | 10_product_patterns_valides.md | 10_n8n_patterns_valides.md | 10_backend_risques_et_vigilance.md | 10_frontend_risques_et_vigilance.md | 10_ux_risques_et_vigilance.md | 10_n8n_risques_et_vigilance.md | 10_conventions_redaction.md | 40_decisions_et_archi.md | 90_debug_et_postmortem.md>
|
||
|
||
Pourquoi :
|
||
<raison pour laquelle ce savoir mérite d'être capitalisé>
|
||
|
||
Proposition :
|
||
<contenu suggéré à intégrer dans le fichier cible>
|
||
```
|
||
|
||
---
|
||
|
||
# Exemple
|
||
|
||
```
|
||
2026-03-08 — portfolio
|
||
|
||
FILE_UPDATE_PROPOSAL
|
||
Fichier cible : 10_backend_patterns_valides.md
|
||
|
||
Pourquoi :
|
||
Pattern réutilisable validé sur un projet réel.
|
||
|
||
Proposition :
|
||
|
||
## Nom du pattern
|
||
|
||
Description courte, factuelle, orientée réutilisation.
|
||
```
|
||
|
||
---
|
||
|
||
# Règles
|
||
|
||
1. Les agents peuvent **proposer librement** ici.
|
||
2. Les propositions doivent rester **courtes et factuelles**.
|
||
3. La validation et l'intégration finale dans `Lead_tech`
|
||
sont faites **manuellement**.
|
||
4. Une fois intégrée, la proposition doit être **supprimée de ce fichier**.
|
||
5. La structure de ce fichier est **restaurée à son état initial** (voir `70_templates/template_a_capitaliser.md`).
|
||
|
||
---
|
||
|
||
_Aucune entrée pour le moment_
|
||
|
||
---
|
||
|
||
# Rôle dans l'architecture
|
||
|
||
```
|
||
Projet
|
||
↓
|
||
Proposition
|
||
↓
|
||
95_a_capitaliser.md
|
||
↓
|
||
Validation humaine
|
||
↓
|
||
Lead_tech
|
||
```
|
||
|
||
Ce mécanisme permet :
|
||
|
||
- d'éviter la pollution de la base de connaissance
|
||
- de capitaliser progressivement l'expérience des projets
|
||
- de garder `Lead_tech` cohérent et fiable.
|