mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-27 17:43:41 +02:00
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:
+156
-1
@@ -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>
|
||||
|
||||
@@ -383,3 +383,262 @@ services:
|
||||
Conteneur `Up (healthy)` mais `docker port <container>` vide, ou `docker inspect ... NetworkSettings.Ports` à `{}` → le conteneur a été recréé sans sa config de ports à jour. Fix : `compose up -d --force-recreate` (après avoir vérifié que le réseau externe existe).
|
||||
|
||||
> Risque connexe côté DNS Docker : voir `knowledge/infra/risques/docker.md`.
|
||||
|
||||
---
|
||||
|
||||
## Type métier dupliqué dans un package `shared` avec bridge de conversion
|
||||
|
||||
### Contexte
|
||||
|
||||
Projet RL799_V2, chantier `refactor/document-types-fusion` (commit `1a0398a`), le 06-05-2026.
|
||||
Audit cartographique du package `@rl799/shared` : le concept `DocumentType` existait en **deux définitions concurrentes avec des valeurs différentes**, reliées par un bridge de conversion `v1ToDbType`.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Deux types nommés `DocumentType` à la racine de fichiers distincts du package partagé :
|
||||
- `dto/DocumentType.ts` exporte une union V1 (`'planches' | 'planches-tracees' | …`, pluriel/kebab-case, valeurs naturelles pour les URLs publiques)
|
||||
- `utils/documentPermissions.ts` exporte `DOCUMENT_TYPES = { … } as const` + `DocumentType = (typeof DOCUMENT_TYPES)[keyof …]` (V2 : `'planche' | 'planche_tracee' | …`, singulier/snake_case, valeurs DB)
|
||||
- Un bridge `v1ToDbType` (côté API) + mapping symétrique (frontend, `DocumentEditModal`) faisaient l'aller-retour.
|
||||
- ~15 min perdues à comprendre lequel est canonique lors de l'audit.
|
||||
- Coût caché révélé : `GET /api/documents` retournait V1 alors que `POST /upload` retournait V2 — **incohérence d'API publique silencieuse**, deux endpoints renvoyant deux dialectes du même concept.
|
||||
|
||||
### Cause
|
||||
|
||||
Un package `shared` existe précisément pour empêcher la duplication de contrats. Y greffer deux dialectes du même type viole sa raison d'être. L'installation est progressive et invisible :
|
||||
|
||||
1. Itération 1 : type défini dans `dto/`, valeurs naturelles pour les URLs publiques.
|
||||
2. Itération 2 : un autre dev a besoin du type pour la DB et le redéfinit localement avec les valeurs DB.
|
||||
3. Itération 3 : un bridge `v1ToDbType` est créé pour les concilier.
|
||||
4. Le mismatch est *vu* mais accepté comme « rétrocompat ».
|
||||
5. Le bridge se propage à 5+ call-sites ; aucun renommage n'est tenté car « trop de choses cassent ».
|
||||
|
||||
C'est une dette historique déguisée en nécessité technique.
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- **Détection** : `grep -rn "export.*DocumentType\|export const DOCUMENT_TYPES" packages/shared/src` → plus d'une définition d'un même type avec des valeurs différentes = flag rouge.
|
||||
- **Remédiation audit-driven (15-30 min de cartographie AVANT de coder)** :
|
||||
1. lister toutes les valeurs des deux dialectes,
|
||||
2. identifier qui utilise V1, qui utilise V2, où vit le bridge,
|
||||
3. choisir le dialecte canonique (en général celui qui matche l'UI vivant ; la DB se migre),
|
||||
4. migration SQL atomique pour aligner la DB,
|
||||
5. source unique (`utils/X.ts`), ré-export depuis `dto/X.ts`,
|
||||
6. suppression du bridge et de tout type fossile,
|
||||
7. relancer toutes les suites avant push.
|
||||
- Effort typique : ~2h pour ~25 fichiers + ~10 tests adaptés.
|
||||
- **Prévention** : refuser en review tout PR qui ajoute une 2ᵉ définition d'un type métier dans le package partagé ; exiger une RFC si un nouveau dialecte est vraiment nécessaire.
|
||||
|
||||
---
|
||||
|
||||
## Flakiness « socket hang up » en e2e Jest/supertest : recyclage de port éphémère
|
||||
|
||||
### Contexte
|
||||
|
||||
app-alexandrie, story infra-8 (code review), le 21-05-2026. Une suite e2e Jest API échouait de façon non déterministe.
|
||||
Complète le diagnostic partiel d'infra-6 (cycle de vie des apps) qui n'avait pas résolu le problème : infra-8 a trouvé la vraie cause racine.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Échecs non déterministes : `socket hang up`, mais AUSSI `400` (body Zod rejeté sur un body pourtant valide), `404`/`401`/`501` sur des routes existantes, parfois une suite entière qui tombe.
|
||||
- Re-run isolé toujours vert.
|
||||
|
||||
### Fausses pistes (toutes réfutées par mesure)
|
||||
|
||||
- `--runInBand` : le retirer AGGRAVE (plus de serveurs HTTP simultanés) → ce n'est pas un problème de concurrence à supprimer.
|
||||
- `--detectOpenHandles` : ne montre RIEN → ce n'est pas une fuite de handle.
|
||||
- Apps NestJS non fermées : instrumentation `{port, closed}` → `open=0`, tous les serveurs sont bien fermés.
|
||||
|
||||
### Cause racine
|
||||
|
||||
Pattern `request(app.getHttpServer())` **sans** `app.listen()`. supertest fait alors `app.listen(0)` (port éphémère) à **chaque** requête. Une suite lourde qui crée une app NestJS par test (20+ apps) recycle 20+ ports éphémères. À la transition « app N libère le port P → app N+1 réobtient P », une connexion TCP initiée pile à cet instant est servie par le **mauvais** serveur ou réinitialisée → d'où les symptômes variés.
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- Helper `startE2EApp(moduleFixture, options?)` qui fait `app.listen(<port>)` **une** fois, sur un **port monotone** (compteur incrémental, jamais recyclé dans un run) + `keepAliveTimeout=0` + repli `listen(0)` si `EADDRINUSE`. Toutes les suites bootent via ce helper. Résultat : 37 runs verts consécutifs.
|
||||
- **Règle** : tout projet e2e Jest/supertest qui boote des apps HTTP en boucle doit utiliser un port stable/monotone, jamais `listen(0)` implicite par requête.
|
||||
- **Leçon de méthode** : ne pas s'arrêter au 1er symptôme (`socket hang up`). Les symptômes secondaires (`400` sur body valide, `501` introuvable dans le code) sont la clé : ils prouvent que la requête atteint le MAUVAIS serveur → collision de port.
|
||||
|
||||
---
|
||||
|
||||
## `rtk` (proxy de tokens) masque ou tronque la sortie des CLI de build/test
|
||||
|
||||
### Contexte
|
||||
|
||||
app-alexandrie, le 26-05-2026. Plusieurs heures perdues en faux diagnostics : `rtk` (Rust Token Killer, proxy de tokens des commandes) intercepte les CLI `jest`, `prisma`, `playwright`, `tsc` et **tronque ou vide leur sortie**.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `prisma migrate status` affiche « 0 applied / 0 pending » alors que les migrations sont bien appliquées → faux finding de code review « migrations non appliquées ».
|
||||
- Sorties `jest` vides ou avec « All parsing tiers failed ».
|
||||
- Runs relancés inutilement sur la base de sorties trompeuses.
|
||||
|
||||
### Cause
|
||||
|
||||
Le filtrage `rtk` n'est pas transparent pour les outils à sortie structurée volumineuse : il peut vider ou tronquer le flux, donnant l'illusion d'un échec ou d'un état vide.
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- Pour tout outil à sortie structurée critique (`jest`, `prisma`, `playwright`, `tsc` en mode diagnostic) : invoquer via `rtk proxy <cmd>` pour bypasser le filtrage, OU rediriger vers un fichier et le lire directement.
|
||||
- **Ne JAMAIS conclure un diagnostic** (migration non appliquée, tests échoués, etc.) sur une sortie passée par `rtk` sans l'avoir confirmé via `rtk proxy` ou la source réelle (ex. requête `pg` directe pour l'état des migrations/index).
|
||||
|
||||
---
|
||||
|
||||
## `vi.spyOn` sur un module ESM : intercepte-t-il vraiment l'appel ?
|
||||
|
||||
### Contexte
|
||||
|
||||
Test de résilience d'une notification, RL799_V2, story v2-1-4 (review), le 13-06-2026.
|
||||
Doute récurrent et coûteux : « mon `vi.spyOn` sur un module ESM intercepte-t-il vraiment l'appel fait par le code testé, qui importe la fonction en top-level ? »
|
||||
|
||||
### Symptôme du doute
|
||||
|
||||
Le code testé fait `import { batchCreateNotifications } from '...'` au top-level. Le binding semble figé, et un spy posé sur l'objet module pourrait ne PAS intercepter cet appel indirect.
|
||||
|
||||
### Réponse vérifiée empiriquement
|
||||
|
||||
**`vi.spyOn(moduleNamespace, 'fn')` intercepte bien un import top-level `import { fn }`** — sous Vitest avec interop CJS (projet sans `"type": "module"`). esbuild transforme les imports ESM en accès CJS via des **getters live** (bindings vivants), donc `vi.spyOn` sur le namespace réimporté (`await import(...)`) intercepte l'appel indirect.
|
||||
|
||||
### Méthode de levée de doute
|
||||
|
||||
Test-sonde temporaire comptant (a) les appels du spy, (b) les effets réels en DB/mock. Si le spy est appelé N× ET zéro effet réel observé → l'interception marche. Ne pas présumer un faux positif sans cette sonde.
|
||||
Cas vécu : spy `mockRejectedValue` sur `batchCreateNotifications` → vérifié : `201` rendu, 0 notif écrite, catch loggé.
|
||||
|
||||
---
|
||||
|
||||
## `as const` sur un objet d'options passé à une lib tierce : TS2769 (arrays readonly)
|
||||
|
||||
### Contexte
|
||||
|
||||
Spike Keycloak RL799_V2 (`session.ts`), le 13-06-2026. Réflexe « je fige mes constantes en `as const` » appliqué à une whitelist d'algorithmes jose.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Typecheck cassé en `TS2769 — No overload matches this call` sur un appel qui compilait avant.
|
||||
- Message peu parlant qui ne pointe PAS vers la readonly-ness.
|
||||
|
||||
### Cause
|
||||
|
||||
`as const` transforme les `string[]` en `readonly string[]`. Si la signature de la fonction tierce attend un array **mutable** (`string[]`), TS rejette :
|
||||
|
||||
```typescript
|
||||
const OPTS = { keyManagementAlgorithms: ['ECDH-ES'] } as const;
|
||||
jose.jwtDecrypt(jwe, key, OPTS); // TS2769 : keyManagementAlgorithms est readonly
|
||||
```
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- Soit typer l'objet explicitement : `const OPTS: { keyManagementAlgorithms: string[] } = { … };`
|
||||
- Soit spread à l'appel : `{ keyManagementAlgorithms: [...MY_CONST] }`.
|
||||
- À garder en tête : un `as const` ajouté « pour bien faire » peut faire échouer soudainement un appel qui compilait.
|
||||
|
||||
---
|
||||
|
||||
## Échec massif soudain de la suite de tests (DB-per-worker) = template/worker DB corrompu, pas une régression de code
|
||||
|
||||
### Contexte
|
||||
|
||||
RL799_V2, session v2-1-5, le 13-06-2026. Du jour au lendemain, 107 fichiers de tests échouent alors que rien de structurant n'a changé, et le compte total de tests est anormal (beaucoup de `skipped`).
|
||||
|
||||
### Symptômes
|
||||
|
||||
- 100+ fichiers KO soudainement, sans changement applicatif correspondant.
|
||||
- Compte de tests anormal, nombreux `skipped`.
|
||||
|
||||
### Cause
|
||||
|
||||
Architecture « template database clonée par worker ». Deux causes typiques :
|
||||
|
||||
- **(a) DB worker orphelines** (`*_test_w1/w2/…`) laissées par un run tué → collision au clonage du run suivant.
|
||||
- **(b) Template Prisma en P3009** (« migrate found failed migrations ») quand un `migrate deploy` de reconstruction du template a été interrompu en plein milieu — la table `_prisma_migrations` garde une migration marquée `failed`, et toute reconstruction ultérieure refuse d'avancer.
|
||||
|
||||
Cas vécu : P3009 sur `cotisation_payment_amount_bounds` (migration sans rapport avec le code touché) après interruption.
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- **Fix** : drop COMPLET du template + des DB worker orphelines, puis reconstruction from scratch (`ensureTemplateReady`) — un template neuf n'a pas de migration `failed`.
|
||||
- **Réflexe** : avant de débugger un « échec massif », vérifier
|
||||
`SELECT datname FROM pg_database WHERE datname LIKE '<prefix>_test%';`
|
||||
et tenter un `migrate deploy` isolé sur le template pour lire l'erreur réelle (souvent masquée par le runner en parallèle).
|
||||
|
||||
---
|
||||
|
||||
## Séparer deux chantiers mélangés dans un working tree non commité (barrel partagé), sans `git add -p`
|
||||
|
||||
### Contexte
|
||||
|
||||
RL799_V2, code-review de la story v2-2-1 (module MC), le 18-06-2026. La réconciliation `git status` vs File List de la story a révélé qu'un SECOND chantier disjoint (ODJ Lot 1) contaminait le working tree non commité, partageant un barrel (`packages/shared/src/index.ts`) avec le chantier en revue. `git add -p` interactif est indisponible en agent.
|
||||
|
||||
### Symptômes / signal de détection
|
||||
|
||||
À faire en début de toute code-review :
|
||||
|
||||
```bash
|
||||
git status --porcelain | grep -v <dossier-artefacts>
|
||||
```
|
||||
|
||||
Comparer à la File List déclarée de la story. Tout fichier modifié hors File List = soit doc incomplète, soit contamination par un autre chantier. Discriminer en `git diff <fichier>` (commentaires/symboles trahissent le chantier d'origine, ex. `// Lot 1 allowedGrades`). Vérifier la disjonction : `grep -rn <symboles-chantier-B> <dossiers-chantier-A>` → si vide, les deux sont indépendants et séparables.
|
||||
|
||||
### Procédure de séparation (agent, `add -p` indisponible)
|
||||
|
||||
1. `git reset` pour repartir d'un index vide.
|
||||
2. `git add -- <fichiers purs du chantier A>` (entiers, modifiés + untracked).
|
||||
3. Pour un FICHIER PARTAGÉ (barrel touché par A ET B) :
|
||||
- `git diff <barrel> > /tmp/full.patch`,
|
||||
- repérer les bornes de hunk (`grep -nE '^@@'`),
|
||||
- reconstruire un patch ne contenant QUE les hunks de A via `sed -n` (entête lignes 1-4 obligatoire + hunks voulus),
|
||||
- valider `git apply --cached --check /tmp/a.patch`, puis `git apply --cached /tmp/a.patch`.
|
||||
4. Vérifier la coupe : `git diff --cached <barrel>` (= A seul) et `git diff <barrel>` (= B seul).
|
||||
5. Committer A (thématique) ; B reste non commité pour SON passage en review.
|
||||
|
||||
### Règle à retenir
|
||||
|
||||
- Garde-fou : retirer un hunk intermédiaire ne casse pas les hunks suivants tant que les zones ne se chevauchent pas (contexte ≥ quelques lignes d'écart) — `git apply` réapplique chaque hunk via son contexte sur le fichier d'origine.
|
||||
- Sanity post-séparation : rebuild du package partagé (`tsc -p`) avec le reste du tree présent.
|
||||
- Cas vécu : commit MC `587d8b5b` isolé du chantier ODJ (hunks `mcChecklist`/`getMaterialChecklist` vs `deriveTemplateItemGrades` dans le barrel).
|
||||
|
||||
---
|
||||
|
||||
## Modèle Prisma fantôme : migration SQL sans `model` dans schema.prisma
|
||||
|
||||
### Contexte
|
||||
|
||||
RL799_V2, Epic v2-4 Hospitalier, le 20-06-2026. Un commit de feature a livré un module complet (repository, service, routes, DTOs, schémas Zod, tests) AVEC les migrations SQL créant la table (`CREATE TABLE care_contacts` + enum), MAIS le `model CareContact { }` n'a jamais été ajouté à `prisma/schema.prisma`.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- CI rouge au build API (`next build`) :
|
||||
`Type error: Property 'careContact' does not exist on type 'PrismaClient<...>'`.
|
||||
- Le typecheck local **pouvait passer** si le dev n'avait pas régénéré le client après le pull (client en cache encore aligné sur un état antérieur).
|
||||
|
||||
### Cause racine
|
||||
|
||||
La table existe en base, le code l'appelle via `prisma.careContact`, mais le client généré (dérivé du **schéma déclaratif**, pas de la base) ignore le modèle. SQL et schéma déclaratif ont divergé.
|
||||
|
||||
### Correctif / règle à retenir
|
||||
|
||||
- **Fix** : ajouter le `model` + l'`enum` au schéma, calés EXACTEMENT sur le SQL des migrations (nom de table via `@@map`, colonnes via `@map`, index, FK, `onDelete`), puis `prisma generate`. AUCUNE nouvelle migration — la table existe déjà, on réaligne seulement le schéma déclaratif sur l'état réel. Penser aux relations inverses sur les modèles cibles des FK (ici 2 FK vers `users` ⇒ 2 relations nommées distinctes sur `User`, sinon erreur de relation ambiguë).
|
||||
- **Détection proactive (audit anti-fantôme)** : diff entre les `CREATE TABLE` des migrations et les `@@map`/noms de modèles du schéma —
|
||||
`comm -23 <(grep 'CREATE TABLE' migrations | extract) <(grep '@@map' schema | extract)`.
|
||||
Tout écart doit s'expliquer par un DROP/rename/fusion ultérieur ; sinon c'est un fantôme. Attention aux faux positifs (commentaires SQL contenant « CREATE TABLE »).
|
||||
- **Prévention** : toute évolution de schéma passe par `schema.prisma` D'ABORD (`prisma migrate dev` génère le SQL depuis le schéma). Si on édite une migration à la main (`ADD VALUE` sur enum, data-fix), vérifier que le schéma déclaratif reflète bien l'état final. Test de garde possible : assertion CI que toute table en base a un modèle dans le schéma.
|
||||
|
||||
---
|
||||
|
||||
## Effet iceberg CI (variante) : extension d'enum/catalogue sans mise à jour des consommateurs exhaustifs
|
||||
|
||||
### Contexte
|
||||
|
||||
RL799_V2, Epic v2-4 Hospitalier + relance, le 21-06-2026. Suite directe du postmortem « modèle Prisma fantôme » : le déblocage du build CI a révélé 2 strates supplémentaires, toutes de la même nature (extension d'un ensemble « fermé » sans mise à jour des consommateurs exhaustifs). Variante ciblée de l'effet iceberg générique (voir « Effet iceberg en CI — patcher en cascade » plus haut).
|
||||
|
||||
### Strates (ordre du pipeline : build → test:api → test:frontend → test:shared)
|
||||
|
||||
1. **Build API** : `model CareContact` absent de `schema.prisma` (cf. postmortem fantôme).
|
||||
2. **test:api** : actions d'audit `care_contact:created` + `soiree:reminder_sent` loggées dans le code mais absentes du catalogue `AUDIT_ACTION_CATALOG` (un test de couverture scanne le code et exige la déclaration).
|
||||
3. **typecheck frontend** : valeur d'enum `CONVOCATION_REMINDER` ajoutée à `NotificationType` mais absente du `Record<NotificationType, IconConfig>` du composant (`vue-tsc` exige l'exhaustivité ; un fallback runtime `?? {...}` masque le crash mais PAS l'erreur de type).
|
||||
|
||||
Le runner s'arrête à la 1ʳᵉ étape échouée → les strates aval sont invisibles tant que l'amont n'est pas réparé.
|
||||
|
||||
### Leçons à retenir
|
||||
|
||||
- **Leçon 1 — consommateurs exhaustifs d'un enum.** Ajouter une valeur à un enum/catalogue « fermé » casse en aval, sans erreur au site d'ajout, trois familles de consommateurs : (a) `Record<Enum, X>` TS, (b) catalogues testés en couverture, (c) `switch` sans `default`. Checklist : à chaque nouvelle valeur d'enum, `grep -rn "Record<MonEnum\|MON_CATALOG\|switch.*monType"` sur front + shared + back.
|
||||
- **Leçon 2 — méthode de rejeu CI.** Après un fix CI structurant (build, ordre d'étapes, import cassé), rejouer la suite COMPLÈTE dans l'ordre exact du pipeline avant de pusher, jamais une suite isolée — sinon on découvre les strates une par une au rythme du CI (yo-yo ~6-9 min/run). Le coût des suites locales cumulées (~3 min) est très inférieur. Lire `.github/workflows` pour connaître l'ordre exact des étapes plutôt que de présumer.
|
||||
|
||||
Reference in New Issue
Block a user