docs(knowledge): capitalisation racine — post-mortems (90_) et ADR (40_) du triage local

Intégration des propositions ciblant les fichiers racine validés.

90_debug_et_postmortem.md (9 post-mortems) :
- type métier dupliqué dans package shared + bridge ; flakiness "socket hang up" e2e
  (recyclage de port éphémère) ; rtk masque la sortie des CLI build/test ; vi.spyOn sur
  module ESM ; `as const` → TS2769 (jose) ; échec massif suite = template DB corrompu ;
  séparer 2 chantiers mélangés (barrel partagé) ; modèle Prisma fantôme (migration sans
  model) + variante effet iceberg CI

40_decisions_et_archi.md (4 ADR) :
- CI e2e mobile pas un prérequis prod ; vérifier le modèle de données réel avant spec ;
  segmenter l'auth par sensibilité d'action ; IdP Keycloak auth-only + RBAC local (Proposed)

Dédupliqué vs knowledge/ déjà écrit : les ADR/post-mortems apportent l'angle narratif/décisionnel
(le "pourquoi"/"récit de debug"), complémentaire des règles réutilisables en knowledge/, avec
cross-références. Blocs déjà couverts ailleurs (113 liste/détail) non réintégrés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 15:58:46 +02:00
parent 81fde91259
commit 2a06429898
2 changed files with 415 additions and 1 deletions
+156 -1
View File
@@ -8,7 +8,7 @@ Objectifs :
- éviter de reposer les mêmes questions
- assumer les compromis
Dernière mise à jour : 2026-03-12
Dernière mise à jour : 2026-06-25
---
@@ -32,6 +32,9 @@ Dernière mise à jour : 2026-03-12
- [User views — User public par défaut + MeUser explicite](#decision-user-views)
- [Mono-tenant déployable vs SaaS multi-tenant](#decision-mono-tenant-deployable)
- [Rollout MCP Lead_tech dans BMAD — Phase 2 partielle (strict ciblé)](#decision-mcp-rollout-phase-2)
- [CI e2e mobile : pas un prérequis de mise en prod](#decision-ci-e2e-mobile)
- [Vérifier le modèle de données réel avant d'accepter une spec](#decision-modele-donnees-avant-spec)
- [Segmenter l'auth par sensibilité d'action (forte vs sans-friction)](#decision-segmentation-auth-sensibilite)
### 2. Infra
@@ -51,6 +54,7 @@ Dernière mise à jour : 2026-03-12
- [Observabilité minimale obligatoire](#decision-observabilite)
- [Authentification et autorisation centrales](#decision-auth-central)
- [Idempotence et gestion des retries](#decision-idempotence-retries)
- [IdP centralisé (Keycloak) auth-only + RBAC métier local](#decision-idp-keycloak-auth-only)
### 4. n8n
@@ -378,6 +382,110 @@ Aligner la doc sur l'implémentation : **déclarer Phase 2 partielle** avec 3 bl
---
<a id="decision-ci-e2e-mobile"></a>
## CI e2e mobile : pas un prérequis de mise en prod
- Date : 2026-05-21
- Statut : Accepted
- Périmètre : global / mobile
### Contexte
Un job CI qui build l'app mobile + lance un smoke test (Maestro, Detox…) sur émulateur/simulateur est tentant, mais coûteux — surtout iOS, qui exige un runner macOS (~10× le prix d'un runner Linux, ~30-45 min/run). Tranché sur app-alexandrie : les e2e mobile (smoke Maestro iOS/Android) ont été retirés de la CI GitHub après constat que le coût ne se justifiait pas.
### Options envisagées
- Garder un job CI mobile bloquant « par principe »
- Garder une CI mobile non bloquante / limitée en coût (paths-filter, iOS sur `push main`)
- Retirer les e2e mobile de la CI et diagnostiquer en local au cas par cas
### Décision
La CI e2e mobile **n'est pas une porte vers la prod**. Pour un dev solo / petite équipe sur plan CI gratuit, retirer les e2e mobile de la CI est défendable.
### Justification
- Les stores (Apple App Review, Google Play) testent le *binaire soumis*, pas la CI GitHub. GitHub ne « refuse » jamais une publication.
- Le vrai garde-fou est le **build de release** (EAS / Xcode / Gradle), qui doit passer avant soumission.
- Un smoke test CI ne confirme que « l'app build et boote » — ce qu'un build local valide déjà. Le seul delta réel (« marche sur ma machine mais pas sur un env propre ») est déjà couvert par le build cloud EAS.
### Conséquences
- Grille de décision à réévaluer selon le contexte :
- **Dev solo / petite équipe, plan CI gratuit** → retrait défendable, smoke en local pour diagnostic ponctuel.
- **Si on garde une CI mobile** → ne pas la rendre bloquante sans données de flake calibrées (émulateurs sujets au flake) ; limiter le coût macOS via paths-filter ou réserver iOS au `push main`.
- **Équipe qui grandit / multiples contributeurs** → la CI mobile reprend de la valeur (env partagé, PR de tiers) — réévaluer.
- Anti-pattern à éviter : un job CI mobile coûteux « par principe » sans avoir identifié quelle classe de bug il attrape que le build local n'attrape pas.
---
<a id="decision-modele-donnees-avant-spec"></a>
## Vérifier le modèle de données réel avant d'accepter une spec
- Date : 2026-06-09
- Statut : Accepted
- Périmètre : global / méthode
### Contexte
Une spec ou une architecture peut supposer une capacité du modèle de données (cumul, multi-valué, relation) qui n'existe pas dans le code réel. Piège récurrent qui fait perdre du temps ou produit une implémentation fausse si on ne vérifie pas. Cas vécu RL799_V2 : l'architecture v2 (décision D1) décrivait une synchro `OfficerMandate → UserRole` supposant un **cumul de rôles**, alors que `User.role` est un enum Postgres **mono-valué** (un seul rôle par user). Dériver un rôle aurait écrasé le rôle existant → violation directe d'une NFR (cumul préservé).
### Options envisagées
- Accepter la spec telle quelle et coder sur le modèle supposé
- Ouvrir le schéma réel (Prisma/SQL) et confirmer la cardinalité du champ avant de cadrer l'implémentation
### Décision
Quand une spec suppose « ajouter un rôle / cumuler / multi-valué / relation », **ouvrir le schéma (Prisma/SQL) et confirmer la cardinalité réelle du champ AVANT de cadrer l'implémentation.**
### Justification
- La hiérarchie de règles s'applique : **contraintes réelles du code > règles archi**. Une spec ne peut pas créer une capacité que le modèle n'a pas.
- Une implémentation sur un modèle supposé produit une régression silencieuse (ici, écrasement de rôle).
### Conséquences
- Vérification systématique du schéma déclaratif avant tout cadrage qui suppose une capacité du modèle.
- Si contradiction archi ↔ code, **ne pas trancher seul** — remonter au décideur (c'est une décision produit/archi, pas technique).
---
<a id="decision-segmentation-auth-sensibilite"></a>
## Segmenter l'auth par sensibilité d'action (forte vs sans-friction)
- Date : 2026-06-13
- Statut : Accepted
- Périmètre : global / auth
### Contexte
Deux populations/usages d'une même app peuvent légitimement exiger des régimes d'auth **opposés** : une action sensible (secrétaire, trésorier, admin VM) appelle une auth forte (IdP, MFA) ; un geste trivial à fort besoin d'adoption (répondre présent, RSVP) appelle l'inverse — aucune friction. Cas vécu RL799_V2 : développement de `presence-sans-friction` (quick-link, token opaque) en parallèle d'un cap Keycloak.
### Options envisagées
- Uniformiser l'auth (même régime fort partout) → tue l'adoption du geste trivial
- Segmenter par sensibilité d'action : friction proportionnelle à la sensibilité
### Décision
**Segmenter l'auth par sensibilité d'action plutôt que d'uniformiser.** La friction acceptable est proportionnelle à la sensibilité de l'action : action sensible → auth forte ; geste trivial → token opaque sans login.
### Justification
- Ce n'est pas une contradiction mais une segmentation : exiger une auth forte sur un RSVP tue l'adoption sans gain de sécurité réel.
- Condition de coexistence : les deux régimes vivent sur des **plans orthogonaux** (routes protégées vs `/api/public/*`), et le token sans-friction est **souverain** — JAMAIS couplé au système d'auth (ni JWT-maison, ni IdP).
### Conséquences
- Test de l'invariant : « le geste sans-friction fonctionne-t-il si l'utilisateur n'a aucun compte / si l'IdP est down ? » → doit être OUI.
- Implémentation côté API : voir `pattern-surface-publique-token-opaque` dans `knowledge/backend/patterns/auth.md` (durcissement du token opaque). La présente décision en est le cadrage stratégique.
---
## 2. Infra
<a id="decision-structure-docker"></a>
@@ -903,6 +1011,53 @@ Principes :
---
<a id="decision-idp-keycloak-auth-only"></a>
## IdP centralisé (Keycloak) en auth-only + RBAC métier local, pour une architecture multi-instances fédérée
- Date : 2026-06-12
- Statut : Proposed (epic futur, horizon < 1 an)
- Périmètre : backend / auth / multi-tenant
### Contexte
Application interne (loge maçonnique, RL799_V2) avec auth JWT-maison (cookie httpOnly) et un RBAC mandate-based récent (union `{role} {offices}` dérivée des `OfficerMandate`). Cap réel à < 1 an : plusieurs instances (1 loge = 1 VPS, données isolées — pas de multi-tenant DB) mais **identité d'authentification centralisée et fédérée** (un Frère membre de 2 loges = 1 identité). Besoin : auth éprouvée (MFA, standards OIDC) + fédération cross-instance, SANS sacrifier l'isolation des données ni le RBAC métier.
### Options envisagées
- **(a) Durcir l'auth maison** — ne répond pas à la fédération.
- **(b) Lib d'auth in-app (type Auth.js)** — ne fédère pas N apps.
- **(c) IdP centralisé (Keycloak / Authentik / Zitadel)** — répond au multi-instances fédéré. **Keycloak retenu** (parti pris assumé).
### Décision
Keycloak en **auth-only**. Frontière en **3 couches** :
1. credential + identité = Keycloak (login, MFA, reset, `email_verified`, first-password) ;
2. **pont = une seule colonne `User.keycloakSub`** ;
3. autorisation métier = app locale (RBAC mandate-based **inchangé**).
Le token OIDC porte UNIQUEMENT l'identité (`sub`, `email`, état civil, `iss`/`aud`/`exp`) ; **JAMAIS** de grade/office/rôle. **Invariant sacré : aucune donnée d'autorisation ne franchit vers Keycloak.**
### Justification
- Le grade/office est local par nature (un Frère peut être Vénérable en loge A et Apprenti en loge B) → ne peut pas vivre dans une identité globale.
- Point de jonction code minimal : un `keycloakTokenValidator` (JWKS/issuer/audience) remplace `verifyJwt` EN AMONT du RBAC ; le middleware RBAC et les guards ne changent pas (le `sub` remplace le `userId`).
- Simplification nette de l'app : disparition du code credential (first-password, reset, hashing) → migre côté Keycloak. Compense partiellement le coût opérationnel d'un serveur de plus.
- Provisioning **app-first** (flux d'enregistrement VM existant + Admin API Keycloak → écrit `keycloakSub`). Rattachement multi-loges par **invitation + login Keycloak prouvé** (jamais auto-rattachement par email). Dé-provision en **ref-count** : identité Keycloak supprimée seulement au départ de la DERNIÈRE loge → croise le chantier RGPD/anonymisation.
- Cohabitation avec l'auth-less : les routes guest (`/api/public/*`) restent HORS Keycloak (cf. décision « Segmenter l'auth par sensibilité d'action »).
### Conséquences
- Epic à risque → découpage en lots Go/No-Go (PoC OIDC → validation token API → pont identité → login frontend → préservation routes guest → bascule + retrait JWT-maison → déploiement Keycloak prod).
- Migration initiale : import des users existants dans Keycloak (par email) + back-fill `keycloakSub` (one-shot à blinder).
- UX : l'écran de login devient celui de Keycloak (thème Freemarker/CSS custom ou page distincte) — continuité visuelle à soigner.
- Volume multi-appartenance INCONNU → architecture prête (un même `sub` partageable par N `User`) mais flux de rattachement fluide implémenté SEULEMENT si le volume le justifie (ne pas sur-construire).
- Points de couture avec l'existant : `invitation-magic-link-v2` (entrée) + anonymisation RGPD (sortie).
- Patterns d'implémentation de la membrane fédérée déjà capitalisés : voir `pattern-membrane-auth-federee` et `pattern-pont-identite-federee` dans `knowledge/backend/patterns/auth.md`. La présente entrée en est le cadrage ADR (le « pourquoi ce choix »).
---
## 4. n8n
<a id="decision-n8n-mini-systemes"></a>