feat: capitalise Epic 2 app-alexandrie + enrichit post-bmad-install

- 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 <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-03-09 14:13:34 +01:00
parent a5ce37a3eb
commit 5650f26b08
6 changed files with 314 additions and 4 deletions

View File

@@ -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) - [Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)](#pattern-contracts-first-zod-infer-no-dto)
- [Guard global NestJS — ordre denregistrement et décorateurs de bypass](#pattern-guard-global-nestjs) - [Guard global NestJS — ordre denregistrement et décorateurs de bypass](#pattern-guard-global-nestjs)
- [Provider-Strategy pour intégrations tierces — périmètre complet](#pattern-provider-strategy-integrations-tierces) - [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<void> {
--- ---
<a id=”pattern-stripe-subscription-metadata”></a>
## 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 lutiliser : systématiquement dès quon 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.*
});
```
---
<a id=”pattern-webhook-parsing-unique”></a>
## 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 lutiliser : dès quon 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
```
---
<a id=”pattern-contracts-error-codes”></a>
## Pattern : Contracts-First — error codes comme contrat obligatoire
- Objectif : maintenir les codes derreur 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 dimporter lenum → 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 lenum, pas une string littérale
- [ ] PR review : vérifier `error-code.ts` à chaque ajout dendpoint derreur
---
### Notes importantes ### Notes importantes
- On préfère 5 patterns solides à 50 “bons conseils”. - On préfère 5 patterns solides à 50 “bons conseils”.

View File

@@ -34,6 +34,11 @@ Dernière mise à jour : 09-03-2026
- [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end) - [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end)
- [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable) - [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable)
- [Observabilité insuffisante](#risque-observabilite-insuffisante) - [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 - Logs structurés + requestId/traceId
- Métriques de base (latence, erreurs, throughput) - Métriques de base (latence, erreurs, throughput)
- Alertes simples sur 5xx/latence - Alertes simples sur 5xx/latence
---
<a id="risque-webhook-200-processing"></a>
## 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
---
<a id="risque-redis-thrash-connexion"></a>
## 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
---
<a id="risque-entitlements-ttl-sla"></a>
## 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
---
<a id="risque-guard-request-user-null"></a>
## 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
---
<a id="risque-compteurs-inmemory"></a>
## 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

View File

@@ -13,6 +13,7 @@ Dernière mise à jour : 2026-03-09
## Index ## Index
- [Langue par type de document](#convention-langue-par-type-de-document) - [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) - Contexte projet : Lead_tech (convention globale)
--- ---
<a id="convention-file-list-story"></a>
### 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
---

View File

@@ -29,6 +29,7 @@ Dernière mise à jour : 09-03-2026
- [Appels API en state local décran](#risque-api-state-local-ecran) - [Appels API en state local décran](#risque-api-state-local-ecran)
- [Performances : sur-renders + bundle](#risque-performances-sur-renders) - [Performances : sur-renders + bundle](#risque-performances-sur-renders)
- [Accessibilité oubliée (a11y)](#risque-accessibilite-oubliee) - [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é - Checklist a11y minimale sur chaque écran clé
- Gestion de focus (modales, erreurs formulaire) - Gestion de focus (modales, erreurs formulaire)
- Labels/aria cohérents + tests simples - Labels/aria cohérents + tests simples
---
<a id="risque-catch-silencieux"></a>
## 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

View File

@@ -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 # Rôle dans l'architecture
``` ```

View File

@@ -41,6 +41,7 @@ PRODUCER_AGENTS=(
"bmm-tech-writer" "bmm-tech-writer"
"bmm-ux-designer" "bmm-ux-designer"
"tea-tea" "tea-tea"
"core-bmad-master"
) )
CAPITALIZE_MARKER="95_a_capitaliser.md" CAPITALIZE_MARKER="95_a_capitaliser.md"
@@ -85,10 +86,10 @@ build_memory() {
case "$agent" in case "$agent" in
bmm-dev|bmm-quick-flow-solo-dev) 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: <target file> / Pourquoi: <reason> / Proposition: <content>. 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: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files."
;; ;;
bmm-architect) 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: <target file> / Pourquoi: <reason> / Proposition: <content>. 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: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files."
;; ;;
bmm-sm) 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: <target file> / Pourquoi: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files." 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: <target file> / Pourquoi: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files."
@@ -108,9 +109,80 @@ build_memory() {
bmm-tech-writer) 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: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files." 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: <reason> / Proposition: <content>. 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: <reason> / Proposition: <content>. Never write directly to Lead_tech validated files."
;;
esac 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 '
/<!-- Final validation gates -->/ {
print " <!-- Capitalisation Lead_tech -->"
print " <action>Review implementation for reusable patterns, difficult bug fixes, anti-patterns, or architecture decisions that emerged during this story</action>"
print " <check if=\"capitalisation-worthy content identified\">"
print " <critical>Write proposals to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md ONLY \xe2\x80\x94 NEVER inside the project repo</critical>"
print " <action>For each proposal: FORMAT = \"DATE \xe2\x80\x94 '"$PROJECT_NAME"' / FILE_UPDATE_PROPOSAL / Fichier cible: &lt;target&gt; / Pourquoi: &lt;reason&gt; / Proposition: &lt;content&gt;\"</action>"
print " </check>"
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 " <!-- Capitalisation Lead_tech -->"
print " <action>Review findings for patterns worth capitalizing: anti-patterns found, recurring issues, architecture decisions confirmed or invalidated</action>"
print " <check if=\"capitalisation-worthy findings identified\">"
print " <critical>Write proposals to ~/AI_RULES/_Assistant_Lead_Tech/95_a_capitaliser.md ONLY \xe2\x80\x94 NEVER inside the project repo</critical>"
print " <action>For each proposal: FORMAT = \"DATE \xe2\x80\x94 '"$PROJECT_NAME"' / FILE_UPDATE_PROPOSAL / Fichier cible: &lt;target&gt; / Pourquoi: &lt;reason&gt; / Proposition: &lt;content&gt;\"</action>"
print " </check>"
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() { patch_claude_md() {
if [ ! -f "$CLAUDE_MD" ]; then if [ ! -f "$CLAUDE_MD" ]; then
echo " [skip] CLAUDE.md — fichier absent" echo " [skip] CLAUDE.md — fichier absent"
@@ -170,6 +242,11 @@ for agent in "${PRODUCER_AGENTS[@]}"; do
patch_agent "$agent" patch_agent "$agent"
done done
echo ""
echo "Patch workflows :"
patch_dev_story
patch_code_review
echo "" echo ""
echo "Patch CLAUDE.md :" echo "Patch CLAUDE.md :"
patch_claude_md patch_claude_md