61 KiB
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.md10_frontend_patterns_valides.md10_ux_patterns_valides.md10_product_patterns_valides.md10_n8n_patterns_valides.md10_backend_risques_et_vigilance.md10_frontend_risques_et_vigilance.md10_ux_risques_et_vigilance.md10_n8n_risques_et_vigilance.md10_conventions_redaction.md40_decisions_et_archi.md90_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.
// ✅ 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 :
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)dans le modèle Prisma- La relation inverse dans
Tenant(ex:menuCategories MenuCategory[]) - 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.
// ✅ 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 :
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.
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 :
// 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
isPublicPageEnableddepuis 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 :
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 :
// ✅ 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 :
// ❌ 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 :
// ❌ 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é :
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="...">) :
// ❌ 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.
// ❌ 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
// ❌ 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
// ❌ 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.
// ❌ 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.
// ❌ 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 :
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) :
<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 :
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 :
enableEn Boolean @default(false)sur le modèleTenant.getEnConfig(tenantId)—findUniquesur PK, appelé dans chaque action mutante (create/update).- Dans chaque action :
if (enableEn && !labelEn) throw new HttpError("...", { status: 400 }). 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).- Gate publish :
if (!result.complete) throw new HttpError("...", { status: 422 }).
Règles :
isVisible: falsen'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.
revalidatePathsur 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)
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. Utiliseropacity-50ouopacity-60.w-35→ invalide. Scale saute dew-32àw-36. Utiliserw-36.box-border→ redondant. Tailwind Preflight applique déjàbox-sizing: border-boxglobalement. Supprimer.- Toujours vérifier les classes custom/non-standard avec
npx tailwindcss --content "..." --dry-runou 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 :
- Validation de format :
new URL(value)dans un try/catch - Validation de protocole :
startsWith("https://")minimum (ouhttp://si nécessaire) - Longueur max (500 chars recommandé)
Pattern validé :
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 :
// ❌ 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.idpour les boutons d'item. - Utiliser
pendingId !== nullpour 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"). finallygarantit 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 :
const handleToggle = useCallback((id: string) => { ... }, []); // stable ✓
// Mais au render :
<ItemCard onToggle={() => handleToggle(item.id)} />
// ↑ nouvelle closure à chaque render → memo inutile
Règles :
useCallbackn'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 :- Passer les données nécessaires en props et laisser l'enfant appeler le handler avec ses propres props.
- Accepter que
memone soit pas protégé pour ces props-là et supprimer leuseCallbackinutile.
- 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.
// 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. revalidatePathreste 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.
// ✅ 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) :
# 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
- Les agents peuvent proposer librement ici.
- Les propositions doivent rester courtes et factuelles.
- La validation et l'intégration finale dans
Lead_techsont faites manuellement. - Une fois intégrée, la proposition doit être supprimée de ce fichier.
- 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_techcohérent et fiable.