# Debug & post-mortems Ce fichier sert à capitaliser sur les problèmes rencontrés. ## À documenter - bug pénible - mauvaise compréhension - fausse hypothèse - solution finale ## Objectif Ne plus jamais perdre du temps sur le même problème. --- # Post‑mortems ## SQL Server qui crash dans un conteneur LXC Proxmox ### Contexte NUC personnel sous Proxmox avec plusieurs services en conteneurs LXC. Un conteneur SQL Server (Microsoft SQL Server Linux) ne démarrait plus. ### Symptômes - `sqlcmd` impossible → timeout - service `mssql-server` en boucle de restart - logs contenant : ``` Operation not permitted chmod: changing permissions of '/var/opt/mssql/log/...' ``` - crash + génération de core dump ### Cause probable SQL Server utilise certaines opérations système qui sont mal supportées dans les conteneurs LXC (permissions, filesystem, capabilities). Dans un environnement Proxmox LXC, cela peut casser après : - une mise à jour - un changement de permissions - un changement de configuration du conteneur ### Conclusion SQL Server **n'est pas un bon candidat pour un conteneur LXC Proxmox**. ### Décision architecturale Pour un homelab ou un petit serveur : - éviter SQL Server en LXC - préférer : - PostgreSQL - MariaDB / MySQL Si SQL Server est nécessaire : - utiliser une **VM complète** plutôt qu'un conteneur. ### Règle à retenir > Éviter les bases lourdes nécessitant des capabilities système avancées dans des conteneurs LXC. --- ## Suppression silencieuse due à deux éditions concurrentes sur le même fichier ### Contexte Un même fichier a été modifié par deux mécanismes proches dans le temps : édition en cours d’agent et passe outillée/linter/formatteur. ### Symptômes - bloc de code disparu sans erreur explicite - diff final incohérent avec l’intention de modification - impression de “régression fantôme” après une édition pourtant correcte ### Cause probable Deux processus ont réécrit le même fichier sans coordination, le second écrasant silencieusement une partie du travail du premier. ### Correctif / règle à retenir - éviter deux passes d’écriture concurrentes sur le même fichier - relire le diff immédiatement après toute passe automatique - privilégier une séquence stricte : édition, puis lint/format, puis vérification --- ## tsx + NestJS : injection par type cassée silencieusement ### Contexte Projet `app-alexandrie`, Epic 3, le 10-03-2026. Le backend NestJS tournait avec `tsx watch` dans un contexte ESM (`module: nodenext`), notamment pour rester compatible avec Prisma v7. ### Symptômes - `TypeError: Cannot read properties of undefined (reading 'get')` dans le constructeur d’un service - `ConfigService` injecté par type mais `undefined` au runtime - `@Injectable()` et `ConfigModule` correctement configurés, sans erreur de compilation ### Cause probable `tsx` repose sur esbuild pour transpiler TypeScript. Dans ce contexte, `emitDecoratorMetadata` est ignoré même s’il est activé dans `tsconfig.json`. NestJS ne peut donc plus résoudre correctement certaines injections par type, notamment `constructor(private readonly config: ConfigService)`. ### Correctif / règle à retenir - ne pas supposer que `emitDecoratorMetadata` fonctionne avec `tsx` - dans ce contexte, éviter l’injection par type de `ConfigService` pour les services d’infra - lire explicitement les variables via `process.env`, après chargement amont de `ConfigModule.forRoot()` Exemple : ```typescript // AVANT constructor(private readonly config: ConfigService) { const host = this.config.get('REDIS_HOST'); } // APRES constructor() { const host = process.env['REDIS_HOST'] ?? 'localhost'; } ``` ### Alternative écartée `nest start --watch` a été testé mais a introduit des conflits ESM/CJS dans ce contexte (`exports is not defined`). --- ## `export { fn }` ne constitue pas un import local — détecté uniquement au build ### Contexte Projet `app-template-resto`, story 2-4, le 17-03-2026. Dans `getPublicHomeData.ts`, la fonction `resolvePublicTenantSelection` avait été déplacée dans `src/server/tenant/resolvePublicTenant.ts` et re-exportée depuis l'ancien emplacement. ### Symptômes - `Cannot find name 'resolvePublicTenantSelection'` au `next build` uniquement - ESLint et `tsc` (hors build) ne signalaient rien - La fonction était utilisée localement dans le même fichier qui la re-exportait ### Cause ```typescript // getPublicHomeData.ts export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant"; // puis, plus bas dans le même fichier : const result = resolvePublicTenantSelection(env); // ← NameError au build ``` Un re-export (`export { fn } from "..."`) ne crée pas de binding local dans le fichier. La fonction est exportée vers l'extérieur mais n'est pas disponible comme variable locale. ### Correctif / règle à retenir Si une fonction est utilisée dans le même fichier qui la re-exporte, ajouter un `import` séparé en plus du `export` : ```typescript import { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant"; export { resolvePublicTenantSelection }; // pour les appelants externes ``` --- ## CLI npm globale qui ne se met pas à jour (prefix / permissions / contexte projet) ### Contexte Mise à jour de `@openai/codex` via la CLI (`codex update`), sur une machine avec installation npm globale utilisateur (`~/.npm-global`) et exécution depuis un repo contenant un `.npmrc` non standard. ### Symptômes - Message d’update CLI affiché mais version inchangée après `npm install -g` - `codex --version` reste sur une ancienne version - Installation via `sudo` ne change rien - `which codex` et `npm root -g` pointent vers des chemins différents ### Cause - Décalage entre : - le **prefix npm** utilisé pour installer - le **binaire exécuté** - Ancienne installation toujours active dans le bon prefix utilisateur - Contexte projet (`.npmrc`) pouvant influencer le comportement de npm ### Correctif / règle à retenir - Ne jamais utiliser `sudo npm install -g` - S’assurer que : - `npm config get prefix` = dossier utilisateur (ex : `~/.npm-global`) - `which ` pointe vers ce même prefix - Faire les installs globales hors d’un repo (éviter `.npmrc` projet) - En cas de doute, nettoyer : ```bash rm -rf ~/.npm-global/lib/node_modules/ rm -f ~/.npm-global/bin/ npm install -g @latest ``` ### Commandes de diagnostic utiles - `npm config get prefix` - `which ` - `npm root -g` - `npm ls -g --depth=0 ` | npm list -g @openai/codex --depth=0 - --version --- ## Sub-agents Claude Code — `Write` indisponible dans la sandbox `Explore` ### Contexte Workflow BMAD `testarch-test-review` sur RL799_V2 (24-04-2026) utilisant 4 sub-agents `subagent_type=Explore` pour évaluer 4 dimensions qualité en parallèle. Chaque sub-agent devait écrire un fichier JSON dans `/tmp/`. ### Symptômes - Les 4 sub-agents ont terminé leur analyse avec succès mais **aucun n'a réussi à écrire son fichier JSON** - Messages de retour : *"Je rencontre une limitation d'outillage… je suis en mode READ-ONLY… je génère le rapport directement en texte."* ### Cause Le sub-agent type `Explore` n'a pas accès à l'outil `Write` dans sa sandbox (spec : "Tools: All tools except Agent, ExitPlanMode, Edit, Write, NotebookEdit"). Non documenté clairement dans les workflows TEA qui demandent pourtant d'écrire en JSON. ### Correctif / règle à retenir 1. **Ne pas demander aux sub-agents `Explore` d'utiliser `Write`** — briefer explicitement "retourne le JSON en bloc dans ta réponse finale" 2. **L'orchestrateur matérialise** les fichiers de sortie pour le compte des sub-agents 3. **Alternative** : utiliser `subagent_type=general-purpose` qui a accès à tous les tools (mais plus cher en tokens et moins spécialisé pour l'exploration) Extrait de brief corrigé pour futur usage : ``` Ta mission : analyse X dans les fichiers Y. Format de sortie : JSON structuré selon le schéma ci-dessous. IMPORTANT : retourne le JSON directement dans ta réponse finale, entre blocs ```json```. Ne tente pas d'écrire de fichier (Write indisponible dans ta sandbox). L'orchestrateur matérialisera le fichier à partir de ton retour. ``` --- ## Effet iceberg en CI — patcher en cascade jusqu'au fond du puits ### Contexte Quand un fix CI structurant rétablit un pipeline qui foirait depuis longtemps, **plusieurs bugs latents en aval peuvent apparaître en cascade** : ils étaient tous présents avant, juste invisibles parce que le runner s'arrêtait à l'échec amont. Vécu sur RL799_V2 le 30-04 / 01-05-2026, 8 étages d'iceberg fixés en cascade. ### Symptômes | # | Phase | Symptôme | Cause | Fix | |---|---|---|---|---| | 1 | CI tests | `Cannot find module '@org/shared'` | `dist/lib` non bâti avant `test:api` | Build workspace en amont | | 2 | CI tests | `Module '@prisma/client' has no exported member 'X'` | Client Prisma non généré | Inverser `prisma generate` → `pnpm build` | | 3 | CI tests | `Seed incomplet : 0 users / N attendus` | Étape seed manquante | Ajouter `prisma db seed` après `prisma migrate deploy` | | 4 | CI tests | ` non configuré (requis hors dev)` | Variable d'env applicative manquante en CI | Définir au bloc `env:` du job | | 5 | CI tests | 14×500 sur endpoints qui chiffrent | `ENCRYPTION_KEY` manquante | Idem | | 6 | CI tests (PDF) | `Could not find Chrome` | Puppeteer cherche son cache local absent du runner | `PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable` | | 7 | CD prod (migrate) | `Cannot find module '/app/scripts/check-node-version.mjs'` | `pnpm run prisma:migrate` appelle un script absent de l'image API | Appel direct du binaire Prisma | | 8 | CI tests | Test attend `50,00 €` reçoit `1,19 €` | `waitForNotification` mal scopé (filtre par `type` mais pas par `recipientId`) — masquée par les étages 1-7 | Re-run OU patch chirurgical du `where:` | Chaque étage masquait le suivant. Aucun n'était nouveau — tous présents avant la session, mais invisibles à cause des étages amont. ### Cause - **Local ≠ CI** : en local, `dist/` traîne, le client Prisma est généré, la DB est seedée d'une session précédente, le `.env` est complet. Le bug est invisible - **Pipeline early-exit** : un échec à l'étape N ne laisse rien tourner aux étapes N+1, N+2, … - **Effet additif des sessions** : plus le pipeline est cassé depuis longtemps, plus le code applicatif a évolué sans validation CI ### Correctif / règle à retenir 1. **Validation locale stricte avant push CI structurant** : simuler les conditions CI vierges (`rm -rf node_modules/.prisma packages/*/dist apps/*/.next` + relancer la chaîne complète) 2. **Lecture honnête des nouveaux failures** : après un fix CI structurant, ne pas présumer que les nouveaux failures sont des régressions du fix. Probablement des bugs latents 3. **Tableau iceberg** : noter au fil de la session le tableau (étage / symptôme / cause / fix). Ne pas se laisser submerger par "ça casse encore" 4. **Push après chaque étage** : ne pas attendre d'avoir tout fixé. Chaque fix structurant mérite son commit thématique 5. **Ne pas stopper trop tôt** : un seul push ne révèle qu'un étage. Tant qu'il y a des bugs latents, le pipeline cassera ### Signal pour repérer un effet iceberg - Le pipeline était cassé depuis ≥ 1 semaine - Le fix d'aujourd'hui touche une étape **précoce** du workflow (install, build, generate, migrate) - Les commits récents ont ajouté des features sans valider en CI - Sentiment vague de "ça pourrait casser plein d'autres trucs" — c'est probablement vrai --- ## Prisma migrate inclut les diffs cosmétiques (RenameIndex) ### Contexte `prisma migrate dev --create-only --name add_lodge_settings` peut générer une migration qui contient (1) le changement attendu mais aussi (2) un side-effect cosmétique pré-existant entre le schema Prisma et la DB qui n'avait jamais été nettoyé. RL799_V2 — migration `20260427120920_add_lodge_settings` qui ramassait un `ALTER INDEX … RENAME TO …` orphelin. ### Symptômes - Migration thématique qui contient un rename d'index sans rapport avec le scope de la story - Un dev qui regarde la migration ne comprend pas pourquoi cet `ALTER INDEX` est là ### Options et décision | Option | Pro | Con | |---|---|---| | Garder le rename dans la migration thématique avec commentaire | la prochaine `prisma migrate dev` ne re-générera pas ce rename | le commit "thématique" contient un side-effect cosmétique | | Retirer le rename | commit propre | la prochaine migration thématique l'inclura à nouveau → piège pour le prochain dev | | Migration de cleanup séparée | plus propre | nécessite 2 migrations + 2 PRs | **Décision recommandée** : option 1 avec commentaire explicite dans le `.sql` : ```sql -- RenameIndex (réalignement DB ↔ schema, dérive cosmétique pré-existante) ALTER INDEX "tronc_entries_tenue_idx" RENAME TO "tronc_entries_tenue_id_idx"; ``` ### Correctif / règle à retenir - **Préventif** : `prisma migrate diff` régulièrement (CI/CD ou pré-commit) pour détecter la dérive AVANT qu'elle ne pollue une migration thématique - **Curatif** : inspecter manuellement le SQL généré par `--create-only` avant de l'appliquer en migration thématique --- ## Réseau Docker partagé entre stacks supprimé par `compose down` malgré `external: true` ### Contexte NUC sous Proxmox, le 22-04-2026. Plusieurs stacks Docker partagent un réseau commun : un Postgres mutualisé (`shared-postgres`) auquel se connectent plusieurs apps via leur IP Tailscale. Chaque stack déclare le réseau en `external: true` dans son `docker-compose.yml`. Après une coupure de courant et redémarrage de la stack, le conteneur Postgres tournait en `Up (healthy)` mais plus aucune app ne pouvait s'y connecter. ### Symptômes - Conteneur `shared-postgres` en `Up (healthy)`, mais `docker port ` retourne vide et `docker inspect ... NetworkSettings.Ports` retourne `{}`. - Les autres stacks connectées au réseau ont perdu toute connectivité. ### Cause Double piège : 1. **`external: true` ne protège pas toujours contre la destruction.** Un `docker compose down` exécuté plus tôt a supprimé le réseau `shared-postgres-net` malgré son `external: true`, parce qu'aucun autre projet ne le "possédait" au moment de la commande. Le flag `external` protège contre la *création*, pas toujours contre la *destruction* — surtout quand le réseau a initialement été créé par un `compose up` (il porte alors les labels Compose et appartient au projet). 2. **Config figée à la création.** Le conteneur relancé par `restart: unless-stopped` garde sa config figée au moment de sa création : les modifications ultérieures du `docker-compose.yml` (ajout du bloc `ports:`, changement de réseau) ne sont pas prises en compte tant qu'on ne fait pas `up --force-recreate`. Le conteneur s'est donc recréé sans réseau ni mapping de ports. ### Correctif / règle à retenir **Un réseau Docker partagé entre plusieurs stacks doit être créé explicitement hors compose, pas par une stack.** ```bash docker network create shared--net ``` Le réseau n'a alors aucun label Compose, donc aucun `compose down` ne peut le supprimer. **Figer le nom de projet Compose.** Par défaut, le nom de projet Compose = nom du dossier. Si le `container_name` diffère du nom de dossier (ex. dossier `postgres/` mais `container_name: shared-postgres`), un `compose down` peut "ne rien voir à supprimer" car il cherche dans le mauvais projet. Solution : déclarer `name:` au top-level : ```yaml name: shared-postgres services: postgres: container_name: shared-postgres ... ``` **Règles opérationnelles pour les stacks partagées critiques :** - Ne jamais faire `docker compose down` sur la stack qui "porte" le réseau partagé. - Utiliser `stop` / `start` / `up -d --force-recreate` pour les maintenances. - Documenter la procédure de recréation du réseau dans le README de la stack. ### Signal de détection Conteneur `Up (healthy)` mais `docker port ` 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()` **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 ` 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 '_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 ``` 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 ` (commentaires/symboles trahissent le chantier d'origine, ex. `// Lot 1 allowedGrades`). Vérifier la disjonction : `grep -rn ` → 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 -- ` (entiers, modifiés + untracked). 3. Pour un FICHIER PARTAGÉ (barrel touché par A ET B) : - `git diff > /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 ` (= A seul) et `git diff ` (= 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` 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` TS, (b) catalogues testés en couverture, (c) `switch` sans `default`. Checklist : à chaque nouvelle valeur d'enum, `grep -rn "Record