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>
33 KiB
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
sqlcmdimpossible → timeout- service
mssql-serveren 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 serviceConfigServiceinjecté par type maisundefinedau runtime@Injectable()etConfigModulecorrectement 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
emitDecoratorMetadatafonctionne avectsx - dans ce contexte, éviter l’injection par type de
ConfigServicepour les services d’infra - lire explicitement les variables via
process.env, après chargement amont deConfigModule.forRoot()
Exemple :
// 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'aunext builduniquement- ESLint et
tsc(hors build) ne signalaient rien - La fonction était utilisée localement dans le même fichier qui la re-exportait
Cause
// 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 :
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 --versionreste sur une ancienne version- Installation via
sudone change rien which codexetnpm root -gpointent 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 <cli>pointe vers ce même prefix
- Faire les installs globales hors d’un repo (éviter
.npmrcprojet) - En cas de doute, nettoyer :
rm -rf ~/.npm-global/lib/node_modules/<package>
rm -f ~/.npm-global/bin/<cli>
npm install -g <package>@latest
Commandes de diagnostic utiles
npm config get prefixwhich <cli>npm root -gnpm ls -g --depth=0 <package>| 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
- Ne pas demander aux sub-agents
Explored'utiliserWrite— briefer explicitement "retourne le JSON en bloc dans ta réponse finale" - L'orchestrateur matérialise les fichiers de sortie pour le compte des sub-agents
- Alternative : utiliser
subagent_type=general-purposequi 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 | <env> 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.envest 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
- 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) - 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
- Tableau iceberg : noter au fil de la session le tableau (étage / symptôme / cause / fix). Ne pas se laisser submerger par "ça casse encore"
- Push après chaque étage : ne pas attendre d'avoir tout fixé. Chaque fix structurant mérite son commit thématique
- 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 INDEXest 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 :
-- 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 diffré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-onlyavant 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-postgresenUp (healthy), maisdocker port <container>retourne vide etdocker inspect ... NetworkSettings.Portsretourne{}. - Les autres stacks connectées au réseau ont perdu toute connectivité.
Cause
Double piège :
external: truene protège pas toujours contre la destruction. Undocker compose downexécuté plus tôt a supprimé le réseaushared-postgres-netmalgré sonexternal: true, parce qu'aucun autre projet ne le "possédait" au moment de la commande. Le flagexternalprotège contre la création, pas toujours contre la destruction — surtout quand le réseau a initialement été créé par uncompose up(il porte alors les labels Compose et appartient au projet).- Config figée à la création. Le conteneur relancé par
restart: unless-stoppedgarde sa config figée au moment de sa création : les modifications ultérieures dudocker-compose.yml(ajout du blocports:, changement de réseau) ne sont pas prises en compte tant qu'on ne fait pasup --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.
docker network create shared-<nom>-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 :
name: shared-postgres
services:
postgres:
container_name: shared-postgres
...
Règles opérationnelles pour les stacks partagées critiques :
- Ne jamais faire
docker compose downsur la stack qui "porte" le réseau partagé. - Utiliser
stop/start/up -d --force-recreatepour 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 <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.tsexporte une union V1 ('planches' | 'planches-tracees' | …, pluriel/kebab-case, valeurs naturelles pour les URLs publiques)utils/documentPermissions.tsexporteDOCUMENT_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/documentsretournait V1 alors quePOST /uploadretournait 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 :
- Itération 1 : type défini dans
dto/, valeurs naturelles pour les URLs publiques. - Itération 2 : un autre dev a besoin du type pour la DB et le redéfinit localement avec les valeurs DB.
- Itération 3 : un bridge
v1ToDbTypeest créé pour les concilier. - Le mismatch est vu mais accepté comme « rétrocompat ».
- 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) :
- lister toutes les valeurs des deux dialectes,
- identifier qui utilise V1, qui utilise V2, où vit le bridge,
- choisir le dialecte canonique (en général celui qui matche l'UI vivant ; la DB se migre),
- migration SQL atomique pour aligner la DB,
- source unique (
utils/X.ts), ré-export depuisdto/X.ts, - suppression du bridge et de tout type fossile,
- 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 AUSSI400(body Zod rejeté sur un body pourtant valide),404/401/501sur 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 faitapp.listen(<port>)une fois, sur un port monotone (compteur incrémental, jamais recyclé dans un run) +keepAliveTimeout=0+ replilisten(0)siEADDRINUSE. 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 (400sur body valide,501introuvable 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 statusaffiche « 0 applied / 0 pending » alors que les migrations sont bien appliquées → faux finding de code review « migrations non appliquées ».- Sorties
jestvides 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,tscen mode diagnostic) : invoquer viartk 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
rtksans l'avoir confirmé viartk proxyou la source réelle (ex. requêtepgdirecte 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 callsur 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 :
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 constajouté « 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 deployde reconstruction du template a été interrompu en plein milieu — la table_prisma_migrationsgarde une migration marquéefailed, 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 migrationfailed. - Réflexe : avant de débugger un « échec massif », vérifier
SELECT datname FROM pg_database WHERE datname LIKE '<prefix>_test%';et tenter unmigrate deployisolé 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 :
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)
git resetpour repartir d'un index vide.git add -- <fichiers purs du chantier A>(entiers, modifiés + untracked).- 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, puisgit apply --cached /tmp/a.patch.
- Vérifier la coupe :
git diff --cached <barrel>(= A seul) etgit diff <barrel>(= B seul). - 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 applyré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
587d8b5bisolé du chantier ODJ (hunksmcChecklist/getMaterialChecklistvsderiveTemplateItemGradesdans 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'enumau schéma, calés EXACTEMENT sur le SQL des migrations (nom de table via@@map, colonnes via@map, index, FK,onDelete), puisprisma 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 versusers⇒ 2 relations nommées distinctes surUser, sinon erreur de relation ambiguë). - Détection proactive (audit anti-fantôme) : diff entre les
CREATE TABLEdes 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.prismaD'ABORD (prisma migrate devgénère le SQL depuis le schéma). Si on édite une migration à la main (ADD VALUEsur 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)
- Build API :
model CareContactabsent deschema.prisma(cf. postmortem fantôme). - test:api : actions d'audit
care_contact:created+soiree:reminder_sentloggées dans le code mais absentes du catalogueAUDIT_ACTION_CATALOG(un test de couverture scanne le code et exige la déclaration). - typecheck frontend : valeur d'enum
CONVOCATION_REMINDERajoutée àNotificationTypemais absente duRecord<NotificationType, IconConfig>du composant (vue-tscexige 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)switchsansdefault. 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/workflowspour connaître l'ordre exact des étapes plutôt que de présumer.