# CLAUDE.md — LintellO

> Contexte durable du projet + aiguillage. Lu à chaque session.
> L'état vivant (ce qui est en cours) vit dans `STATE.md`, pas ici.

## Stack (détectée depuis le code)

- **Backend** : PHP 8.4 · Symfony 7.4 (framework-bundle, messenger, scheduler, security, lock, rate-limiter).
- **ORM** : Doctrine ORM 3 / DBAL 3, migrations Doctrine. Entités en attributs PHP 8 (`#[ORM\...]`), IDs `Symfony\Uid\Uuid`.
- **Bases** : MySQL (EntityManager principal) **+** PostgreSQL distant sur VPS pour les modules `Prospect` et `Search`/L'dicO (`DATABASE_URL`, `PROSPECT_DATABASE_URL`, `SEARCH_DATABASE_URL`).
- **Admin** : EasyAdmin 4 (`src/Controller/Admin/*CrudController`).
- **Auth** : lexik/jwt-authentication + Google OAuth (`league/oauth2-google`) + reset-password bundle.
- **IA** : Mistral. Le transport est isolé dans `Service\Mistral\MistralClient` — tout appel à l'API passe par lui. Le tour se construit dans `Service\Turn` (classification, payload, écriture), s'assemble dans `Service\Prompt`, se borne dans `Service\Context`. OCR Tesseract (`thiagoalessio/tesseract_ocr` + service OCR distant VPS).
- **Paiement** : Stripe.
- **Mail** : Symfony Mailer / Brevo.
- **Front** : SPA React 19 + TypeScript + Vite + Zustand + TipTap (`frontend/`).
- **Infra** : app sur mutualisé OVH (crons shell, sans Docker en prod) ; VPS pour Docker + PostgreSQL + SearXNG + OCR. Docker local seulement (`docker-compose.yml`, `DOCKER.md`).

## Règles dures

- **Boucle de vérification** : après toute modif, lancer les tests (`make test` / `bin/phpunit`), relire le `git diff`, corriger jusqu'au vert avant de rendre la main. En local, la suite tourne dans le conteneur (`docker exec lintello_php php bin/phpunit`) : lancée depuis Windows sur le chemin `\\wsl.localhost`, `is_readable()` renvoie faux et PHPUnit refuse son bootstrap.
- **Prompt du classifieur** : toute modification de `IntentClassifier::prompt()` se mesure **avant et après** avec `bin/phpunit --group live` (28 cas de référence, vrai Mistral Small, hors suite par défaut, nécessite `MISTRAL_API_KEY` dans `.env.test.local`). Le risque n'y est pas le bug mais la **dilution** : ajouter une demande dégrade les autres, et aucun test unitaire ne le voit.
- **Payload d'un tour** : `TurnPayloadBuilder::construire()` **écrit** — elle compacte ce qui sort du budget. Une seule construction par tour ; la rejouer par étape ferait payer la compaction cinq à sept fois. Et le budget suit le **modèle effectif** : en Flow, celui des étapes (`small`), jamais celui de la synthèse — un contexte taillé pour la fenêtre de Large envoyé à Small fait échouer la requête.
- **Ajouter un outil, un bloc de prompt ou une consigne** : lire d'abord le **contrat en tête de `STATE.md`** (« Ajouter un outil — ce qui est imposé depuis le 2026-08-19 »), normé par les décisions 19 et 20 de `core/dossier-projet.md`. Quatorze règles tirées de défauts mesurés. Les trois qui coûtent le plus cher quand on les oublie : **un bloc déclare toujours de quoi il parle** (sinon il sort hors du mécanisme d'arbitrage, comme quatorze l'ont fait) ; **un sujet se découpe par décision, jamais par émetteur** ; et **toute décision confiée à un modèle entre dans `--group live` avec son contrôle NÉGATIF** — un cas positif seul ne prouve rien.
- **Étape de Flow** : une étape ne construit jamais ses messages elle-même. Elle appelle `$contexte->etape($consigne)`, et sa consigne ne contient pas le socle. C'est la règle qui empêche une étape d'être oubliée — deux l'avaient été (`doCritique`, et les étapes expert qui remplaçaient la constitution).
- **Front** : `public/build` est gitignoré → **rebuild au déploiement**, sinon l'ancien bundle tourne. En local le build passe par un conteneur (`docker run --rm --volumes-from lintello_php -w /var/www/lintello/frontend node:20-alpine npm run build`) : lancé depuis Windows, `cmd.exe` refuse un cwd UNC et Vite résout mal sa racine. Les noms d'assets **doivent garder leur `[hash]`** (`vite.config.ts`) : nginx et le `.htaccess` les servent en `immutable, 1 an`, donc un nom fixe fige le site chez tous les utilisateurs. Seul `build/index.html` est en `no-store`.
- **Sécurité** : aucun secret dans les fichiers de suivi. Les vrais secrets vont dans `.env.local` (gitignoré), **jamais** dans `.env` / `.env.docker` suivis par git. Voir la dette sécu dans `STATE.md`.
- **Git** : commit atomique par unité logique, branche par feature. Messages en français, préfixe `Feat :` / `Fix :` / `Docs :` (+ module si pertinent, ex. `Fix Prospect :`).
- **CRLF** : le repo a une dette CRLF/LF (~150 fichiers). **Ne jamais `git add -A`** — ajouter les chemins explicitement pour éviter de committer du bruit de fins de ligne.
- **Doctrine** : ne **jamais** lancer `doctrine:schema:update --force` (créerait les tables PostgreSQL dans MySQL + `DROP TABLE`). Passer par des migrations, séparées par SGBD.
- **Deux SGBD** : bien viser le bon EntityManager (MySQL principal vs PostgreSQL Prospect/Search).

## Aiguillage — quand une idée arrive

- **Nouvelle fonctionnalité** → créer `docs/specs/<module>/<nom>.md` depuis `docs/specs/_template.md` + ajouter une ligne à `docs/specs/INDEX.md`.
- **Choix d'archi tranché** → `docs/decisions/NNNN-<titre>.md` depuis `docs/decisions/_template.md` + ligne dans `docs/decisions/INDEX.md`.
- **Travail en cours / état** → mettre à jour `STATE.md`.
- **Bug à corriger maintenant** → noter dans `STATE.md` puis traiter.

Modules : `core` (chat/IA), `prospect` (leads B2B art. 28), `search` (L'dicO + crawl), `thinking` (Tree),
`billing` (plans/quotas/Stripe), `files` (upload/OCR/export), `admin`, `auth`, `frontend`.

## Avant d'implémenter

Consulter `docs/specs/INDEX.md` (specs liées) et `docs/decisions/INDEX.md` (contraintes existantes) **AVANT** de coder.

## Docs profondes (chargées au besoin)

- Flux de réponse du chat (le cœur du produit, lots 0 à 6b) : `docs/specs/core/flux-reponse.md`
- Architecture / frontières des modules : `docs/architecture.md`
- Conventions de facto : `docs/conventions.md`
- Roadmap / jalons : `docs/ROADMAP.md`
- Ancienne doc (historique, non normatif) : `/old/`
