# 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 <cli>` 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/<package>
rm -f ~/.npm-global/bin/<cli>
npm install -g <package>@latest
```

### Commandes de diagnostic utiles

- `npm config get prefix`
- `which <cli>`
- `npm root -g`
- `npm ls -g --depth=0 <package>` | npm list -g @openai/codex --depth=0
- <package> --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 | `<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 `.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 <container>` 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-<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 :

```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 <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`.
