mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +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:
@@ -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