From 5650f26b08431b2d3701ca44ccfc2c9251d61f5f Mon Sep 17 00:00:00 2001 From: MaksTinyWorkshop Date: Mon, 9 Mar 2026 14:13:34 +0100 Subject: [PATCH] feat: capitalise Epic 2 app-alexandrie + enrichit post-bmad-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Intègre 9 propositions de 95_a_capitaliser.md (Stripe, webhooks, Redis, entitlements, guards, catch silencieux, conventions File List) - Ajoute core-bmad-master dans les agents patchés (orchestrateur) - Différencie les fichiers cibles par rôle d'agent (dev/architect/qa…) - Patch dev-story et code-review XML pour déclencher la capitalisation à chaque fin de story et après chaque code review Co-Authored-By: Claude Sonnet 4.6 --- 10_backend_patterns_valides.md | 64 ++++++++++++++ 10_backend_risques_et_vigilance.md | 124 ++++++++++++++++++++++++++++ 10_conventions_redaction.md | 14 ++++ 10_frontend_risques_et_vigilance.md | 33 ++++++++ 95_a_capitaliser.md | 2 - scripts/post-bmad-install.sh | 81 +++++++++++++++++- 6 files changed, 314 insertions(+), 4 deletions(-) diff --git a/10_backend_patterns_valides.md b/10_backend_patterns_valides.md index f77e7bb..2304897 100644 --- a/10_backend_patterns_valides.md +++ b/10_backend_patterns_valides.md @@ -24,6 +24,9 @@ Dernière mise à jour : 09-03-2026 - [Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)](#pattern-contracts-first-zod-infer-no-dto) - [Guard global NestJS — ordre d’enregistrement et décorateurs de bypass](#pattern-guard-global-nestjs) - [Provider-Strategy pour intégrations tierces — périmètre complet](#pattern-provider-strategy-integrations-tierces) +- [Stripe — metadata sur `subscription_data`, pas sur la Session](#pattern-stripe-subscription-metadata) +- [Webhooks entrants — parsing unique (single constructWebhookEvent)](#pattern-webhook-parsing-unique) +- [Contracts-First — error codes comme contrat obligatoire](#pattern-contracts-error-codes) --- @@ -494,6 +497,67 @@ async handleWebhook(rawBody: Buffer, signature: string): Promise { --- + +## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session + +- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`. +- Contexte : intégration Stripe Checkout avec webhooks abonnement. +- Quand l’utiliser : systématiquement dès qu’on crée une Checkout Session liée à une Subscription. +- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod. +- Validé le : 09-03-2026 +- Contexte technique : Stripe API v17+ / NestJS + +### Implémentation + +```typescript +stripe.checkout.sessions.create({ + metadata: { userId }, // pour checkout.session.completed + subscription_data: { metadata: { userId } }, // pour customer.subscription.* +}); +``` + +--- + + +## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`) + +- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs. +- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…). +- Quand l’utiliser : dès qu’on a 2+ handlers webhook sur le même endpoint. +- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO). +- Validé le : 09-03-2026 +- Contexte technique : Stripe / NestJS + +### Implémentation + +```typescript +// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque +// 2. Extracteurs purs, sans effet de bord : +handleSubscriptionWebhookEvent(event): WebhookResult | null +handlePackWebhookEvent(event): PackWebhookResult | null +// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats +``` + +--- + + +## Pattern : Contracts-First — error codes comme contrat obligatoire + +- Objectif : maintenir les codes d’erreur API dans `packages/contracts` pour éviter les clients stringly-typed. +- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`. +- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après. +- Risque si ignoré : clients qui testent des strings hardcodées au lieu d’importer l’enum → drift silencieux. +- Validé le : 09-03-2026 +- Contexte technique : TypeScript / NestJS + Expo (React Native) + +### Checklist + +- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit +- [ ] Clients importent l’enum, pas une string littérale +- [ ] PR review : vérifier `error-code.ts` à chaque ajout d’endpoint d’erreur + +--- + ### Notes importantes - On préfère 5 patterns solides à 50 “bons conseils”. diff --git a/10_backend_risques_et_vigilance.md b/10_backend_risques_et_vigilance.md index a0d57f0..be5fbe9 100644 --- a/10_backend_risques_et_vigilance.md +++ b/10_backend_risques_et_vigilance.md @@ -34,6 +34,11 @@ Dernière mise à jour : 09-03-2026 - [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end) - [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable) - [Observabilité insuffisante](#risque-observabilite-insuffisante) +- [Webhooks entrants — répondre 200 pendant `processing` (event perdu)](#risque-webhook-200-processing) +- [Redis — thrash de connexion sous charge](#risque-redis-thrash-connexion) +- [Entitlements — TTL cache supérieur au SLA de propagation](#risque-entitlements-ttl-sla) +- [Guard NestJS route-level — null-check manquant sur `request.user`](#risque-guard-request-user-null) +- [Compteurs in-memory ≠ métriques persistées](#risque-compteurs-inmemory) --- @@ -254,3 +259,122 @@ Dernière mise à jour : 09-03-2026 - Logs structurés + requestId/traceId - Métriques de base (latence, erreurs, throughput) - Alertes simples sur 5xx/latence + +--- + + +## Webhooks entrants — répondre 200 pendant `processing` (event perdu) + +### Risques + +- Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué +- Event non appliqué mais marqué "traité" → état incohérent silencieux + +### Symptômes + +- Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour +- Aucun retry du provider → impossible à détecter sans monitoring actif + +### Bonnes pratiques / mitigations + +- Lock DB (`WebhookEvent`) avec machine d'état : `pending` → `processing` → `processed` / `failed` +- Si `processing` détecté (concurrent) : attendre brièvement la transition `processed`, sinon répondre **non-2xx** (force retry provider) +- Ne jamais passer à `processed` sans preuve d'un traitement effectif +- Contexte technique : Stripe / NestJS — 09-03-2026 + +--- + + +## Redis — thrash de connexion sous charge + +### Risques + +- Connexions concurrentes multiples si `connect()` est appelé "à la demande" sans lock +- Spam logs + saturation connexions quand Redis est down ou lent + +### Symptômes + +- N appels simultanés → N tentatives de connexion en parallèle +- Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis + +### Bonnes pratiques / mitigations + +```typescript +// Pattern single-flight + cooldown + fallback DB best-effort +if (!this.connectPromise) { + this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; }); +} +await this.connectPromise; +// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB +``` + +- Contexte technique : Redis / NestJS — 09-03-2026 + +--- + + +## Entitlements — TTL cache supérieur au SLA de propagation + +### Risques + +- TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti) +- Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel + +### Symptômes + +- Accès premium encore actif après annulation (ou inversement) +- NFR "propagation ≤ 60s" non respecté en cas de webhook manqué + +### Bonnes pratiques / mitigations + +- TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max) +- Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre) +- Contexte technique : Redis / entitlements / NestJS — 09-03-2026 + +--- + + +## Guard NestJS route-level — null-check manquant sur `request.user` + +### Risques + +- Un guard route-level qui lit `request.user.userId` sans null-check lève une `TypeError` (500) si `request.user` est absent +- Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état + +### Symptômes + +- `TypeError: Cannot read properties of undefined (reading 'userId')` en prod +- Tests "verts" car `request.user` mocké globalement, mais pas le guard isolé + +### Bonnes pratiques / mitigations + +```typescript +const user = (request as any).user as { userId: string } | undefined; +if (!user?.userId) { + throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } }); +} +``` + +- **Règle** : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes. +- Contexte technique : NestJS v10+ — 09-03-2026 + +--- + + +## Compteurs in-memory ≠ métriques persistées + +### Risques + +- Compteurs in-memory remis à zéro au restart (perte de données) +- Non agrégables sur plusieurs instances (données partielles par pod) + +### Symptômes + +- Métriques qui "repartent de 0" à chaque déploiement +- Dashboards incorrects en environnement multi-instance + +### Bonnes pratiques / mitigations + +- V1 low-cost : `Redis INCRBY` best-effort par `eventType` → persisté et agrégé multi-instances +- Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ) +- Contexte technique : Redis / NestJS — 09-03-2026 diff --git a/10_conventions_redaction.md b/10_conventions_redaction.md index 596b342..c887713 100644 --- a/10_conventions_redaction.md +++ b/10_conventions_redaction.md @@ -13,6 +13,7 @@ Dernière mise à jour : 2026-03-09 ## Index - [Langue par type de document](#convention-langue-par-type-de-document) +- [File List story — exhaustivité obligatoire](#convention-file-list-story) --- @@ -57,3 +58,16 @@ Pas de "bonne pratique" théorique. - Contexte projet : Lead_tech (convention globale) --- + + +### Convention : File List story — exhaustivité obligatoire + +- Scope : section "File List" des story files BMAD (Dev Agent Record) +- Règle : inclure **tous** les fichiers créés ou modifiés pendant la story — migrations, modules infra, fichiers contracts, fichiers de config. Un reviewer ne doit pas avoir à faire `git status` pour reconstituer le périmètre. +- Règle complémentaire : les fichiers créés en avance de phase (scope d'une story future) doivent être annotés : `— créé en avance (scope story X.Y)` +- Vérification recommandée : cross-checker via `git status --porcelain` avant de passer la story en review +- Contre-exemple : story 2.3 app-alexandrie — 13 fichiers manquants (migrations, modules Redis, services entitlements, error codes contracts) +- Validé le : 09-03-2026 +- Contexte projet : app-alexandrie + +--- diff --git a/10_frontend_risques_et_vigilance.md b/10_frontend_risques_et_vigilance.md index 85997f1..0af9dd4 100644 --- a/10_frontend_risques_et_vigilance.md +++ b/10_frontend_risques_et_vigilance.md @@ -29,6 +29,7 @@ Dernière mise à jour : 09-03-2026 - [Appels API en state local d’écran](#risque-api-state-local-ecran) - [Performances : sur-renders + bundle](#risque-performances-sur-renders) - [Accessibilité oubliée (a11y)](#risque-accessibilite-oubliee) +- [Catch silencieux — erreur inconnue sans feedback utilisateur](#risque-catch-silencieux) --- @@ -172,3 +173,35 @@ Dernière mise à jour : 09-03-2026 - Checklist a11y minimale sur chaque écran clé - Gestion de focus (modales, erreurs formulaire) - Labels/aria cohérents + tests simples + +--- + + +## Catch silencieux — erreur inconnue sans feedback utilisateur + +### Risques + +- Un `catch` qui ne traite que les cas connus laisse l'utilisateur face à un spinner qui disparaît sans message +- L'état d'erreur reste implicite → impossible de diagnostiquer ou de reproduire + +### Symptômes + +- Bouton spinner qui s'arrête, rien ne se passe +- Pas de toast / message d'erreur affiché +- Erreur "avalée" silencieusement dans les logs + +### Bonnes pratiques / mitigations + +```typescript +} catch (err: unknown) { + const code = (err as { code?: string }).code; + if (code === 'SUBSCRIPTION_REQUIRED') { + setSubscriptionRequired(true); + } else { + setError('Une erreur est survenue. Veuillez réessayer.'); // toujours un fallback + } +} +``` + +- **Règle** : tout `catch` doit avoir une branche `else` (ou `default`) qui affiche un feedback utilisateur explicite. +- Contexte technique : React Native / Expo — 09-03-2026 diff --git a/95_a_capitaliser.md b/95_a_capitaliser.md index 5d8fc87..0830e5f 100644 --- a/95_a_capitaliser.md +++ b/95_a_capitaliser.md @@ -81,8 +81,6 @@ Sinon `request.user` peut être undefined dans les guards suivants. --- -_Aucune proposition en attente pour le moment._ - # Rôle dans l'architecture ``` diff --git a/scripts/post-bmad-install.sh b/scripts/post-bmad-install.sh index 2307a09..637abc5 100755 --- a/scripts/post-bmad-install.sh +++ b/scripts/post-bmad-install.sh @@ -41,6 +41,7 @@ PRODUCER_AGENTS=( "bmm-tech-writer" "bmm-ux-designer" "tea-tea" + "core-bmad-master" ) CAPITALIZE_MARKER="95_a_capitaliser.md" @@ -85,10 +86,10 @@ build_memory() { case "$agent" in bmm-dev|bmm-quick-flow-solo-dev) - echo "${base} during implementation, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." + echo "${base} during implementation, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: <10_backend_patterns_valides.md | 10_frontend_patterns_valides.md | 10_backend_risques_et_vigilance.md | 10_frontend_risques_et_vigilance.md | 90_debug_et_postmortem.md> / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." ;; bmm-architect) - echo "${base}, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." + echo "${base} during architecture or technical design, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: <40_decisions_et_archi.md | 10_backend_patterns_valides.md | 10_backend_risques_et_vigilance.md> / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." ;; bmm-sm) echo "When a process improvement, recurring friction, or architecture decision emerges during sprint work, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." @@ -108,9 +109,80 @@ build_memory() { bmm-tech-writer) echo "When a reusable documentation pattern, writing convention, or recurring documentation friction emerges, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: <10_conventions_redaction.md | 40_decisions_et_archi.md> / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." ;; + core-bmad-master) + echo "As the orchestrating agent, when any cross-cutting pattern, process improvement, recurring friction, or architectural decision emerges across the project, write a proposal to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md (NUC: /srv/projects/_Assistant_Lead_Tech/95_a_capitaliser.md). Format: DATE — ${PROJECT_NAME} / FILE_UPDATE_PROPOSAL / Fichier cible: <10_backend_patterns_valides.md | 10_frontend_patterns_valides.md | 10_product_patterns_valides.md | 10_ux_patterns_valides.md | 10_backend_risques_et_vigilance.md | 10_frontend_risques_et_vigilance.md | 40_decisions_et_archi.md | 90_debug_et_postmortem.md> / Pourquoi: / Proposition: . Never write directly to Lead_tech validated files." + ;; esac } +CAPITALIZE_MARKER_XML="Capitalisation Lead_tech" +DEV_STORY_XML="$PROJECT_ROOT/_bmad/bmm/workflows/4-implementation/dev-story/instructions.xml" +CODE_REVIEW_XML="$PROJECT_ROOT/_bmad/bmm/workflows/4-implementation/code-review/instructions.xml" + +patch_dev_story() { + local file="$DEV_STORY_XML" + + if [ ! -f "$file" ]; then + echo " [skip] dev-story/instructions.xml — fichier absent" + return 0 + fi + + if grep -q "$CAPITALIZE_MARKER_XML" "$file"; then + echo " [skip] dev-story/instructions.xml — capitalisation déjà présente" + return 0 + fi + + # Insérer le bloc capitalisation juste avant les Final validation gates + awk ' + // { + print " " + print " Review implementation for reusable patterns, difficult bug fixes, anti-patterns, or architecture decisions that emerged during this story" + print " " + print " Write proposals to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md ONLY \xe2\x80\x94 NEVER inside the project repo" + print " For each proposal: FORMAT = \"DATE \xe2\x80\x94 '"$PROJECT_NAME"' / FILE_UPDATE_PROPOSAL / Fichier cible: <target> / Pourquoi: <reason> / Proposition: <content>\"" + print " " + print "" + } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + + echo " [ok] dev-story/instructions.xml — capitalisation injectée" +} + +patch_code_review() { + local file="$CODE_REVIEW_XML" + + if [ ! -f "$file" ]; then + echo " [skip] code-review/instructions.xml — fichier absent" + return 0 + fi + + if grep -q "$CAPITALIZE_MARKER_XML" "$file"; then + echo " [skip] code-review/instructions.xml — capitalisation déjà présente" + return 0 + fi + + # Insérer le bloc capitalisation après le output "✅ Review Complete!" + awk ' + /✅ Review Complete!/ { in_review_complete = 1 } + in_review_complete && /<\/output>/ { + print + print "" + print " " + print " Review findings for patterns worth capitalizing: anti-patterns found, recurring issues, architecture decisions confirmed or invalidated" + print " " + print " Write proposals to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md ONLY \xe2\x80\x94 NEVER inside the project repo" + print " For each proposal: FORMAT = \"DATE \xe2\x80\x94 '"$PROJECT_NAME"' / FILE_UPDATE_PROPOSAL / Fichier cible: <target> / Pourquoi: <reason> / Proposition: <content>\"" + print " " + in_review_complete = 0 + next + } + { print } + ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" + + echo " [ok] code-review/instructions.xml — capitalisation injectée" +} + patch_claude_md() { if [ ! -f "$CLAUDE_MD" ]; then echo " [skip] CLAUDE.md — fichier absent" @@ -170,6 +242,11 @@ for agent in "${PRODUCER_AGENTS[@]}"; do patch_agent "$agent" done +echo "" +echo "Patch workflows :" +patch_dev_story +patch_code_review + echo "" echo "Patch CLAUDE.md :" patch_claude_md