# TREE.md — Système de Pensée IA LintellO

> Instructions techniques pour l'implémentation du système de pensée (Thinking System).
> Ce fichier est la référence pour Claude Code.

---

## 1. Contexte

Sur-couche d'orchestration backend sur Mistral (pas de thinking natif). Décompose les requêtes complexes en plusieurs appels IA structurés.

**Équation fondamentale :** `Mode = Pré-prompt (existant) + Schéma de pensée (nouveau) + Template d'affichage`

**Principe économique :** `Small × 5 appels cadrés < coût 1 appel Large` ET `> qualité 1 appel Large` sur tâches complexes.

---

## 2. Les 3 schémas de pensée

Seulement 3 schémas à implémenter. Les autres patterns (Debate, Multi-Agent, Backward, Decompose, Analogical) sont des **configurations** de ces 3 schémas via le champ `thinking_config`.

### 2.1 Chain (linéaire)

Décomposition séquentielle. Chaque étape nourrit la suivante.

```
Prompt utilisateur
    │
    ▼
Étape 1 : Décomposer la tâche
    │ résultat 1
    ▼
Étape 2 : Exécuter avec contexte étape 1
    │ résultat 1 + résultat 2
    ▼
Étape 3 : Exécuter avec contexte étapes 1+2
    │ résultat 1 + résultat 2 + résultat 3
    ▼
Étape N : Vérification finale (si enable_critique)
    │
    ▼
Réponse finale
```

**Variantes via configuration :**
- `direction: "forward"` → classique, du début vers la fin
- `direction: "backward"` → partir de l'objectif, remonter les prérequis
- `recursive: true` → si une sous-tâche est trop complexe, elle est elle-même décomposée
- `expert_prompts: {...}` → chaque étape utilise un system prompt d'expert différent (simule le multi-agent)

### 2.2 Tree (arborescent)

Exploration de branches en parallèle, évaluation, sélection de la meilleure.

```
           Prompt utilisateur
                  │
           Décomposition
           ┌──────┼──────┐
           ▼      ▼      ▼
        Branche  Branche  Branche
           A       B       C
           │      │       │
           ▼      ▼       ▼
        Évaluation (score 0-1)
           │
           ▼
        Meilleure branche → développer
           │
           ▼
        Synthèse finale
```

**Variantes via configuration :**
- `max_branches: 3` → exploration classique multi-approches
- `max_branches: 2` + `branch_prompts: {pour, contre}` → simule le **Debate** (arguments pour/contre)
- `max_branches: 2` + `display_template: "debate_columns"` → affichage en colonnes

### 2.3 Refine (itératif)

Boucle générer → critiquer → corriger.

```
Prompt utilisateur
    │
    ▼
Génération V1 (Large)
    │
    ▼
Critique V1 (Small) → "Liste les faiblesses"
    │
    ▼
Correction → V2 (Large)
    │
    ▼
Critique V2 (Small) → améliorations mineures ?
    │
    ├── Oui → Correction → V3
    └── Non → V2 est la version finale
```

**Configuration :**
- `max_iterations: 2-3` → nombre max de boucles
- `critique_prompt: "..."` → prompt custom pour la critique (sinon générique)

---

## 3. Évolution de la table Mode (BDD)

### 3.1 Champs à ajouter

La table `Mode` existante reçoit **2 nouveaux champs** :

```sql
ALTER TABLE mode ADD COLUMN thinking_schema VARCHAR(20) DEFAULT NULL;
ALTER TABLE mode ADD COLUMN thinking_config JSON DEFAULT NULL;
```

| Champ | Type | Description |
|-------|------|-------------|
| `thinking_schema` | `enum nullable` | `chain`, `tree`, `refine` ou `NULL` (pas de pensée) |
| `thinking_config` | `JSON nullable` | Options spécifiques au schéma. NULL si thinking_schema est NULL |

**Principe clé : tous les modes commencent avec `thinking_schema = NULL`. Activation progressive. Zéro risque de régression.**

### 3.2 Structure thinking_config par schéma

#### Chain config

```json
{
  "complexity_threshold": 3,
  "direction": "forward",
  "recursive": false,
  "max_depth": 4,
  "enable_critique": true,
  "expert_prompts": null,
  "display_template": "drawer",
  "model_override": null
}
```

| Option | Type | Défaut | Description |
|--------|------|--------|-------------|
| `complexity_threshold` | int (1-5) | 3 | Seuil d'activation. 1 = toujours, 5 = très complexe seulement |
| `direction` | string | `"forward"` | `"forward"` = classique, `"backward"` = partir de l'objectif |
| `recursive` | boolean | `false` | Redécouper les sous-tâches trop complexes |
| `max_depth` | int | 4 | Profondeur max de la chaîne |
| `enable_critique` | boolean | `true` | Vérification finale de cohérence |
| `expert_prompts` | object\|null | `null` | Prompts par expert. Clés = rôles, valeurs = system prompts |
| `display_template` | string | `"drawer"` | `"drawer"`, `"checklist"`, `"expert_tabs"` |
| `model_override` | string\|null | `null` | Forcer un modèle spécifique (sinon utilise celui du plan user) |

#### Tree config

```json
{
  "complexity_threshold": 2,
  "max_branches": 3,
  "max_depth": 3,
  "enable_critique": true,
  "branch_prompts": null,
  "display_template": "proposals",
  "model_override": null
}
```

| Option | Type | Défaut | Description |
|--------|------|--------|-------------|
| `complexity_threshold` | int (1-5) | 2 | Seuil d'activation |
| `max_branches` | int | 3 | Nombre de branches à explorer. 2 = debate |
| `max_depth` | int | 3 | Profondeur max de l'arbre |
| `enable_critique` | boolean | `true` | Critique après synthèse |
| `branch_prompts` | object\|null | `null` | Prompts forcés par branche (ex: `{pour: "...", contre: "..."}`) |
| `display_template` | string | `"proposals"` | `"proposals"`, `"debate_columns"` |
| `model_override` | string\|null | `null` | Forcer un modèle spécifique |

#### Refine config

```json
{
  "complexity_threshold": 3,
  "max_iterations": 3,
  "critique_prompt": null,
  "display_template": "drawer_revisions",
  "model_override": null
}
```

| Option | Type | Défaut | Description |
|--------|------|--------|-------------|
| `complexity_threshold` | int (1-5) | 3 | Seuil d'activation |
| `max_iterations` | int | 3 | Nombre max de boucles générer/critiquer/corriger |
| `critique_prompt` | text\|null | `null` | Prompt custom pour la critique. Si null, utilise un prompt générique |
| `display_template` | string | `"drawer_revisions"` | Template d'affichage |
| `model_override` | string\|null | `null` | Forcer un modèle spécifique |

### 3.3 Mapping recommandé des modes

Voici la configuration cible pour chaque mode. **Ne pas tout activer d'un coup** — suivre la roadmap des sprints (section 8).

| Mode | thinking_schema | Options clés | display_template |
|------|----------------|--------------|-----------------|
| 📝 Rédaction | `refine` | max_iterations: 3 | drawer_revisions |
| ✉️ Communication | `refine` | max_iterations: 2 | drawer_revisions |
| 💻 Code | `chain` | forward, critique: true | drawer |
| 📋 Organisation | `chain` | direction: backward | checklist |
| 🔍 Recherche | `chain` | forward, critique: true | drawer |
| 🏢 Business | `chain` | expert_prompts: {marketing, finance, juridique, tech} | expert_tabs |
| 📐 Maths | `chain` | recursive: true | drawer |
| 🧠 Stratégie | `tree` | max_branches: 3 | proposals |
| 💡 Créatif | `tree` | max_branches: 3 | proposals |
| 📊 Analyse | `tree` | max_branches: 2, branch_prompts: {pour, contre} | debate_columns |
| ⚖️ Juridique | `tree` | max_branches: 2 + critique | debate_columns |
| 🎓 Apprentissage | `null` | — (analogie gérée par le pré-prompt) | default |
| 💬 Conversation | `null` | — | default |

### 3.4 EasyAdmin CRUD

Le formulaire EasyAdmin du Mode doit afficher les options de `thinking_config` **dynamiquement** selon le `thinking_schema` sélectionné :

- Si `null` → aucune option affichée
- Si `chain` → direction, recursive, max_depth, enable_critique, expert_prompts, display_template
- Si `tree` → max_branches, max_depth, enable_critique, branch_prompts, display_template
- Si `refine` → max_iterations, critique_prompt, display_template

Le `complexity_threshold` est commun aux 3 schémas.

---

## 4. Nouvelle table ThoughtTree (BDD)

### 4.1 Structure

```sql
CREATE TABLE thought_tree (
    id CHAR(36) NOT NULL PRIMARY KEY,
    conversation_id CHAR(36) NOT NULL UNIQUE COMMENT '1 conversation = 1 ThoughtTree (voir Q8)',
    tree_data JSON NOT NULL COMMENT 'Arbre complet, chiffré AES-256-GCM',
    schema_used VARCHAR(20) NOT NULL COMMENT 'chain, tree ou refine',
    total_tokens INT NOT NULL DEFAULT 0,
    duration_ms INT NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL COMMENT 'Mis à jour à chaque nouveau message Thinks',
    FOREIGN KEY (conversation_id) REFERENCES conversation(id) ON DELETE CASCADE
);
```

**Important :** Le champ `tree_data` est chiffré AES-256-GCM comme tous les contenus sensibles de LintellO. L'arbre entier est protégé d'un coup.

**Relation 1:1 avec Conversation** : La contrainte `UNIQUE` sur `conversation_id` garantit qu'une conversation n'a qu'un seul ThoughtTree, mis à jour au fil des messages (voir Q8). Le champ `updated_at` trace chaque évolution de l'arbre.

La table `Message` existante n'est **PAS modifiée**. Les `message_id` sont référencés dans le corps du JSON (`tree_data`), pas en clé étrangère relationnelle.

**Modifications sur la table `conversation`** (décidées en Q8) :

```sql
ALTER TABLE conversation ADD COLUMN active_tree_id CHAR(36) NULL;
ALTER TABLE conversation ADD COLUMN active_node_id VARCHAR(50) NULL
  COMMENT 'ID du nœud actif dans tree_data (backtrack/merge)';
ALTER TABLE conversation ADD FOREIGN KEY (active_tree_id)
  REFERENCES thought_tree(id) ON DELETE SET NULL;
```

`active_node_id` pointe vers l'`id` d'un nœud dans le JSON `tree_data`. C'est ce champ qui permet le backtrack (changer de branche active) et le merge. Voir Q8 pour le comportement complet.

### 4.2 Structure du JSON tree_data

```json
{
  "schema": "tree",
  "mode": "strategie",
  "complexity": 4,
  "nodes": [
    {
      "id": "root",
      "role": "decompose",
      "summary": "Analyse de la demande",
      "content": "Contenu détaillé du raisonnement...",
      "status": "explored",
      "tokens": 450,
      "model": "mistral-small",
      "message_id": null,
      "children": [
        {
          "id": "branch-a",
          "role": "execute",
          "branch": "Approche SEO",
          "summary": "Stratégie acquisition organique...",
          "content": "Développement complet...",
          "score": 0.9,
          "status": "selected",
          "tokens": 2800,
          "model": "mistral-small",
          "message_id": "uuid-msg-42",
          "children": [
            {
              "id": "branch-a-eval",
              "role": "evaluate",
              "summary": "Vérification cohérence",
              "content": "L'approche SEO est cohérente avec...",
              "status": "selected",
              "tokens": 600,
              "model": "mistral-small",
              "message_id": null,
              "children": []
            }
          ]
        },
        {
          "id": "branch-b",
          "role": "execute",
          "branch": "Approche Ads",
          "summary": "Budget pub LinkedIn...",
          "content": "Développement...",
          "score": 0.5,
          "status": "pruned",
          "tokens": 1200,
          "model": "mistral-small",
          "message_id": "uuid-msg-43",
          "children": []
        }
      ]
    }
  ]
}
```

**Champs par nœud :**

| Champ | Type | Description |
|-------|------|-------------|
| `id` | string | Identifiant unique du nœud (ex: "root", "branch-a", "step-3") |
| `role` | string | `"decompose"`, `"execute"`, `"evaluate"`, `"critique"`, `"refine"`, `"synthesize"`, `"merge"`, `"conversation"` |
| `branch` | string\|null | Nom de la branche (uniquement pour Tree) |
| `summary` | string | Résumé court pour l'affichage (tiroir, mind map) |
| `content` | string | Contenu complet du raisonnement à cette étape |
| `score` | float\|null | Score d'évaluation 0-1 (uniquement pour Tree) |
| `status` | string | `"exploring"`, `"explored"`, `"selected"`, `"pruned"`, `"merged"` |
| `tokens` | int | Tokens consommés pour ce nœud |
| `model` | string | Modèle utilisé ("mistral-small", "mistral-large") |
| `message_id` | string\|null | UUID du Message associé (si la réponse est stockée comme Message) |
| `parents` | array | IDs des nœuds parents (vide pour la racine, 2 éléments pour un nœud `merge`) |
| `children` | array | Nœuds enfants (structure récursive) |

**Couleurs de status pour le frontend :**
- `selected` → vert
- `pruned` → rouge
- `exploring` → orange
- `explored` → gris
- `merged` → violet

---

## 5. Architecture backend Symfony

### 5.1 Fichiers à créer

```
src/Service/Thinking/
├── ComplexityDetector.php           ← Évalue la complexité (1-5)
├── ThinkingEngine.php               ← Orchestrateur principal
├── TreeBuilder.php                  ← Construit le JSON de l'arbre
└── Strategy/
    ├── ThinkingStrategyInterface.php ← Interface commune
    ├── ChainStrategy.php            ← Logique chain-of-thought
    ├── TreeStrategy.php             ← Logique tree-of-thought
    └── RefineStrategy.php           ← Logique self-refine

src/Entity/
└── ThoughtTree.php                  ← Entité Doctrine
```

### 5.2 ComplexityDetector

Évalue si le système de pensée doit s'activer pour une requête donnée.

**Deux niveaux de détection :**

1. **Heuristiques (gratuit, pas d'appel API) :**
   - Longueur du prompt > 100 mots → +1
   - Mots-clés détectés ("planning", "compare", "analyse", "organise", "stratégie", "étapes", "budget", "avantages et inconvénients") → +1 par mot-clé (max +2)
   - Nombre de contraintes (virgules, listes, conditions "si...alors") → +1 si > 3
   - Questions multiples détectées → +1

2. **Classificateur Small (si score heuristique incertain 2-3) :**
   - Appel rapide à Mistral Small (~50 tokens)
   - Prompt : "Sur une échelle de 1 à 5, évalue la complexité de cette demande. 1 = question simple, 5 = tâche multi-étapes avec contraintes croisées. Réponds uniquement par le chiffre."
   - Coût quasi nul

**Signature :**

```php
class ComplexityDetector
{
    /**
     * Évalue la complexité d'un prompt utilisateur.
     * Retourne un score de 1 à 5.
     */
    public function evaluate(string $userPrompt): int;
}
```

### 5.3 ThinkingEngine

Orchestrateur principal. Point d'entrée unique. Il reçoit le message et le mode, coordonne tout.

> **Note d'implémentation** : La signature complète et définitive de `ThinkingEngine::process()` est documentée en **section 11.6 (Q4)**. Elle retourne un `?\Generator` pour le streaming SSE, pas un tableau. La section 11.6 fait référence.

**Constructeur :**

```php
class ThinkingEngine
{
    public function __construct(
        private ComplexityDetector $complexityDetector,
        private TreeBuilder $treeBuilder,
        private ThoughtTreeRepository $thoughtTreeRepository,
        private ChainStrategy $chainStrategy,
        private TreeStrategy $treeStrategy,
        private RefineStrategy $refineStrategy,
        private MistralClient $mistralClient,
    ) {}
}
```

**Point d'intégration crucial :** Le ThinkingEngine est injecté dans `ChatController`. L'intégration complète (avec streaming SSE) est documentée en **section 11.6 (Q4)**.

### 5.4 TreeBuilder

Construit le JSON `tree_data` au fur et à mesure des appels. Chaque fois qu'une Strategy fait un appel à Mistral, elle ajoute un nœud via le TreeBuilder.

```php
class TreeBuilder
{
    private array $tree;

    public function create(string $schema, string $mode): self;

    // Création de nœuds
    public function createRoot(string $summary, string $role = 'decompose'): string; // retourne l'id
    public function addChild(string $parentId, string $summary, array $data = []): string; // retourne l'id

    // Mise à jour de nœuds
    public function setContent(string $nodeId, string $content): void;
    public function setScore(string $nodeId, float $score): void;
    public function setStatus(string $nodeId, string $status): void;
    public function setMessageId(string $nodeId, string $messageId): void;
    public function setTokens(string $nodeId, int $tokens): void;
    public function setModel(string $nodeId, string $model): void;

    // Lecture
    public function getNode(string $nodeId): array;
    public function getTotalTokens(): int;
    public function toArray(): array;  // Retourne le JSON complet
    public function toJson(): string;  // Sérialisé
}
```

### 5.5 ThinkingStrategyInterface

```php
interface ThinkingStrategyInterface
{
    /**
     * Exécute le schéma de pensée et retourne la réponse finale.
     *
     * @param string $userPrompt    Le message de l'utilisateur
     * @param string $systemPrompt  Le system prompt du mode (pré-prompt existant)
     * @param array $config         Le thinking_config du mode
     * @param TreeBuilder $tree     Le constructeur d'arbre pour stocker les nœuds
     * @param MistralClient $ai     Le client Mistral pour les appels API
     * @return string               La réponse finale à afficher à l'utilisateur
     */
    public function process(
        string $userPrompt,
        string $systemPrompt,
        array $config,
        TreeBuilder $tree,
        MistralClient $ai
    ): string;
}
```

### 5.6 – 5.8 Logique des Strategies

> La logique détaillée (`processWithStreaming`) de chaque Strategy est documentée en **section 11.6 (Q4) et 11.7 (Q5)**, qui font référence. Les squelettes de base ci-dessous rappellent uniquement la structure attendue.

**ChainStrategy** : décomposition (forward/backward) → exécution séquentielle des étapes avec contexte cumulé → critique finale si `enable_critique`. Voir section 11.7 pour le code `processWithStreaming` complet.

**TreeStrategy** : génération de N branches (ou branches forcées si `branch_prompts`) → évaluation scorée → développement de la meilleure → synthèse. Voir section 11.7.

**RefineStrategy** : génération V1 → boucle critique (Small) / correction (auto) jusqu'à `max_iterations` ou satisfaction. Voir section 11.7.

---

## 6. Intégration frontend React

### 6.1 Données renvoyées par l'API

L'API existante de réponse au message doit inclure les données de pensée quand elles existent :

```json
{
  "message": {
    "id": "uuid-msg",
    "content": "Voici votre planning de révisions...",
    "role": "assistant"
  },
  "thinking": {
    "schema": "chain",
    "tree_data": { ... },
    "tokens_used": 8340,
    "duration_ms": 4100,
    "display_template": "drawer"
  }
}
```

Si `thinking` est `null`, pas de système de pensée activé → affichage normal.

### 6.2 Templates d'affichage

6 templates selon le schéma et la config du mode :

| Template | Schéma | Description |
|----------|--------|-------------|
| `drawer` | Chain | Tiroir dépliable : liste les étapes (`summary`), clic → détail (`content`). **Sprint 1.** |
| `drawer_revisions` | Refine | Variante tiroir : V1 → Critique → V2 → ... → V finale. **Sprint 2.** |
| `proposals` | Tree | Cards côte à côte avec score, bouton "Combiner". **Sprint 3.** |
| `debate_columns` | Tree (2 branches) | Colonnes POUR / CONTRE + synthèse. **Sprint 3.** |
| `checklist` | Chain backward | Arbre de prérequis (objectif → actions). **Sprint 4.** |
| `expert_tabs` | Chain multi-expert | Onglets par expert + onglet Synthèse. **Sprint 4.** |

**Implémentation `drawer` (Sprint 1)** : parcourir récursivement `tree_data.nodes`, afficher chaque `summary` comme étape. Clic → déplier `content`.

### 6.3 Visualisation progressive (roadmap frontend)

| Version | Affichage | Composant | Quand |
|---------|-----------|-----------|-------|
| V1 | Tiroir dépliable | Composant React custom | Sprint 1 |
| V2 | Arbre interactif | react-flow ou markmap | Sprint 7 |
| V3 | Mind map + backtracking | react-flow + actions | Futur |

Le même JSON `tree_data` alimente les 3 versions. Seul le composant de rendu change.

**Pour la V2/V3 :** Le format JSON arborescent (`id` + `children`) est nativement compatible avec les librairies de mind map. Mapping minimal : renommer `summary` en `label`.

Librairie recommandée : **react-flow** (MIT, nœuds customisables, zoom, pan, clic interactif, très maintenue).

---

## 7. Sécurité et chiffrement

- Le champ `tree_data` dans ThoughtTree est chiffré AES-256-GCM, comme tous les contenus sensibles
- Chaque appel intermédiaire à Mistral utilise le même system prompt du mode → pas de fuite de contexte
- Le ThinkingEngine n'expose jamais les nœuds intermédiaires directement. Le frontend reçoit le JSON complet et l'affiche localement
- Les nœuds `pruned` (branches abandonnées) sont conservés pour la transparence mais ne contiennent pas de données sensibles supplémentaires
- La suppression d'une conversation (droit à l'effacement RGPD) cascade vers ThoughtTree via `ON DELETE CASCADE`

---

## 8. Roadmap d'implémentation

### Sprint 1 (semaines 1-2) : Chain + Tiroir

**Objectif :** Le schéma le plus simple, testable rapidement, couvre 5 modes.

| Tâche | Détail |
|-------|--------|
| ComplexityDetector | Heuristiques uniquement (pas de classificateur Small) |
| ThinkingEngine | Orchestrateur de base |
| TreeBuilder | Construction JSON |
| ChainStrategy | Direction forward uniquement |
| Entité ThoughtTree | + migration Doctrine |
| Modifier l'API | Ajouter le if/else pour déclencher la pensée |
| Template tiroir React | Composant dépliable sous la réponse |
| Champs Mode BDD | thinking_schema + thinking_config |
| CRUD EasyAdmin | Formulaire dynamique pour configurer les modes |
| Activer sur Code et Organisation | Premiers modes avec Chain |

### Sprint 2 (semaines 3-4) : Refine + Tiroir révisions

| Tâche | Détail |
|-------|--------|
| RefineStrategy | Boucle générer/critiquer/corriger |
| Template drawer_revisions | Variante tiroir avec versions |
| Activer sur Rédaction et Communication | Modes avec Refine |

### Sprint 3 (semaines 5-6) : Tree + Proposals/Debate

| Tâche | Détail |
|-------|--------|
| TreeStrategy | Exploration multi-branches + évaluation + scoring |
| Template proposals | Cards côte à côte |
| Template debate_columns | Colonnes pour/contre |
| Activer sur Stratégie, Créatif, Analyse | Modes avec Tree |

### Sprint 4 (semaines 7-8) : Variantes Chain

| Tâche | Détail |
|-------|--------|
| Chain backward | Option direction pour Organisation |
| Chain recursive | Option pour Maths |
| Chain expert_prompts | Multi-expert pour Business |
| Template checklist | Pour backward |
| Template expert_tabs | Pour multi-expert |
| Activer sur Business, Maths, Juridique | Modes restants |

### Sprint 5 (semaines 9-10) : Polish et optimisation

| Tâche | Détail |
|-------|--------|
| Classificateur Small | Détection complexité via appel API (remplace heuristiques pures) |
| Streaming | Afficher les étapes en temps réel pendant que l'IA réfléchit |
| Métriques | Dashboard EasyAdmin pour suivre l'usage des schémas |
| Optimisation tokens | Ajuster les prompts pour minimiser la consommation |

### Sprint 6 (futur) : Git de Pensée V2

| Tâche | Détail |
|-------|--------|
| Arbre interactif | react-flow pour visualiser le tree_data en mind map |
| Nœuds cliquables | Clic sur un nœud → voir le détail du raisonnement |
| Backtracking interactif | L'utilisateur peut demander d'explorer une branche abandonnée |

---

## 8 bis. Dette technique Sprint 1

Sprint 1 livré fonctionnellement (toggle 🧠, ChainStrategy, persistance par message,
hybride Small+Large synthèse, anti-verbosité Large classique). Reste à solder :

### Tests à écrire (specs §9)

- [ ] **`TreeBuilderTest`** : construction JSON, ajout de nœuds, calcul tokens, accès O(1) via index
- [ ] **`ChainStrategyTest`** : décomposition + exécution séquentielle + critique + synthèse, avec mock du callable Mistral. Couvrir le fallback parsing (lignes non numérotées)
- [ ] **`ThinkingEngineTest`** : try/catch arbre partiel, hybride steps_model vs synthesis_model, fallback quota Large→Small, retry chatRaw
- [ ] **`MistralServiceTest::buildLengthInstruction`** : matrice complète des patterns (court / factuel / détail / rapport / code / défaut) × `activeModel` (small / large / null)
- [ ] **Tests d'intégration** : POST `/chat/stream` avec `enableThinking=true` → vérifier events SSE + persistance Message + persistance ThoughtTree liée au Message + cascade delete (supprimer Message → ThoughtTree supprimé)

### Code à nettoyer

- [ ] Supprimer `src/Service/Thinking/ComplexityDetector.php` (plus utilisé depuis le passage au toggle manuel) et nettoyer son binding `services.yaml` si présent
- [ ] Supprimer la clé legacy `model_override` dans la rétro-compat `ThinkingEngine::process()` une fois qu'on a vérifié qu'aucun mode en BDD ne s'en sert (`SELECT name, thinking_config FROM mode WHERE thinking_config LIKE '%model_override%'`)

### Fonctionnel à finir

- [ ] **Bloquer le toggle Flow pour le plan Gratuit** (backend + frontend, toggle disabled + tooltip "Disponible avec [plan]")
- [ ] **Sommaire cliquable** pour les réponses Flow > 2000 tokens (pattern Gemini Deep Research)
- [ ] **Badge "Interrompu"** dans le drawer quand `tree.partial === true` (l'arbre partiel sauvé en cas d'échec Mistral)
- [ ] **Resserrement Small classique** si on observe que la différenciation Small/Large/Flow est encore floue après mise en prod

### À mesurer en prod

- [ ] Latence moyenne Flow hybride (objectif : < 90s p95)
- [ ] Taux de fallback quota Large → Small sur la synthèse
- [ ] Taux de retry chatRaw (alerter si > 5%)
- [ ] Distribution longueur réponses Small / Large / Flow → vérifier la hiérarchie effective

---

## 9. Tests

### Tests unitaires

- `ComplexityDetectorTest` : vérifier le scoring sur des prompts simples vs complexes
- `TreeBuilderTest` : vérifier la construction du JSON, l'ajout de nœuds, le calcul des tokens
- `ChainStrategyTest` : vérifier la décomposition et l'exécution séquentielle (avec mock Mistral)
- `TreeStrategyTest` : vérifier l'exploration de branches et le scoring
- `RefineStrategyTest` : vérifier la boucle critique et l'arrêt anticipé

### Tests d'intégration

- Envoyer un prompt simple → vérifier que la pensée ne s'active PAS
- Envoyer un prompt complexe avec mode Chain → vérifier que tree_data est généré
- Vérifier que le chiffrement/déchiffrement de tree_data fonctionne
- Vérifier le cascade delete (supprimer conversation → ThoughtTree supprimé)

---

## 10. Résumé

```
3 schémas : Chain, Tree, Refine
2 champs ajoutés sur Mode : thinking_schema, thinking_config
1 nouvelle table : ThoughtTree (JSON chiffré)
5 classes Symfony : ComplexityDetector, ThinkingEngine, TreeBuilder, + 3 Strategies
6 templates frontend : drawer, drawer_revisions, proposals, debate_columns, checklist, expert_tabs

Toute l'intelligence est dans la data (thinking_config JSON), pas dans le code.
On configure dans EasyAdmin, on sauvegarde, le backend s'adapte.
Zéro risque de régression : les modes sans thinking_schema fonctionnent comme avant.
```

---

## 11. Décisions d'architecture validées

> **Note** : Cette section documente les décisions prises lors de la phase de conception avant implémentation. Elle clarifie les points bloquants potentiels et les choix architecturaux.

---

### 11.1 Intégration ModeDetector ↔ ThinkingEngine (Q1)

**Problème identifié** : Le `ModeDetectorService` détecte automatiquement le mode optimal via keywords. Risque de conflit si le mode change **pendant** l'exécution du Thinks (ex: un nœud génère du contenu avec keywords d'un autre mode → détection → switch → incohérence).

**Décision validée** : **Option C — ModeDetector AVANT ThinkingEngine uniquement**

#### Architecture du flux

```
1. User envoie message
   ↓
2. ModeDetectorService.detectOptimalMode(message, conversation, user)
   → Analyse keywords du MESSAGE USER uniquement (pas des réponses IA)
   ↓
3. Si shouldSwitch = true → conversation.setMode(newMode)
   ↓
4. mode = conversation.getEffectiveMode()
   ↓
5. ThinkingEngine.process(message, mode, ...)
   → ModeDetector INACTIF pendant toute l'exécution du Thinks
   → Tous les nœuds s'ajoutent séquentiellement dans tree_data
   ↓
6. Réponse générée
```

#### Règles clés

- ✅ **1 mode = 1 Thinks** (pas de switch pendant l'exécution)
- ✅ **ModeDetector s'exécute TOUJOURS AVANT ThinkingEngine**
- ✅ **ModeDetector désactivé pendant l'exécution du Thinks** (les nœuds générés ne déclenchent pas de détection)
- ✅ **Tous les nœuds s'ajoutent séquentiellement** dans `tree_data.nodes[]` (structure JSON récursive commune à chain/tree/refine)

#### Vocabulaire standardisé

- **Modes** = les 17 modes de LintellO (Recherche, Stratégie, Rédaction, etc.)
- **Thinks** = les modes de pensée (chain, tree, refine)

---

### 11.2 Modèle économique et quotas (Q2)

**Décision** : Thinks conditionné par l'accès aux modes (pas de quota dédié). Référence quotas : `PLANS_REFERENCE.md`.

| Plan | Small | Thinks |
|------|-------|--------|
| **Gratuit** | 20 msg/jour | Mode Découverte uniquement, limité naturellement |
| **Étudiant+** | Illimité | ✅ Sans limite de nombre d'exécutions |
| **Pro / LintellO** | Illimité | ✅ + Large/Codestral selon quotas mensuels |

**Tokens** : Pas de quota Thinks dédié. Tokens Small = inclus dans Small illimité. Tokens Large (nœud `synthesize` ou `model_override`) = décomptés de `large_monthly_quota`. Plan Gratuit : 1 Think complet = 1 message quota (pas 1 par nœud).

---

### 11.3 Usage de Large dans Thinks

**Problème identifié** : Certains cas nécessitent Large malgré la stratégie Small-first (contexte ultra-long, synthèse complexe, etc.).

**Décision validée** : **Détection automatique + configuration manuelle (Option C)**

#### Hiérarchie de sélection du modèle (par nœud)

| Niveau | Règle | Exemple |
|--------|-------|---------|
| **1. Conversation** | `model = 'small'` par défaut | Toutes les conversations commencent Small |
| **2. Mode** | `thinking_config.model_override` peut forcer | Mode Code force `codestral` pour tous les nœuds |
| **3. Nœud individuel** | Détection auto si contexte > seuil | Nœud 5 bascule Large car 10k tokens de contexte |
| **4. Expert spécifique** | `expert_prompts[].model` peut forcer | Expert juridique force `large` dans mode Business |

#### Cas d'usage Large dans Thinks

**A. Contexte ultra-long (> 7k tokens)**

```php
// Dans ThinkingEngine, avant chaque nœud :
$contextSize = $this->calculateContextSize($tree->getAllPreviousNodes());

if ($contextSize > 7000) { // Seuil de sécurité Small (max 32k mais conservateur)
    $nodeModel = 'large';
} else {
    $nodeModel = $config['model_override'] ?? 'small';
}
```

**B. Nœud de synthèse finale complexe**

```php
// Rôle 'synthesize' avec contexte riche → Large recommandé
if ($node['role'] === 'synthesize' && $contextSize > 3000) {
    $nodeModel = 'large';
}
```

**C. Expert spécifique nécessite Large**

```json
{
  "thinking_schema": "chain",
  "thinking_config": {
    "expert_prompts": {
      "marketing": { "prompt": "...", "model": "small" },
      "juridique": { "prompt": "...", "model": "large" }
    }
  }
}
```

**D. Mode force Large globalement**

```json
{
  "thinking_schema": "tree",
  "thinking_config": {
    "model_override": "large"  // TOUS les nœuds en Large
  }
}
```

#### Ajustement MAX_HISTORY_TOKENS (dynamique par plan)

**Actuellement** : `MAX_HISTORY_TOKENS = 4000` (fixe pour tous)

**Nouveau** : Dynamique selon le plan utilisateur

```php
private function getMaxHistoryTokens(User $user): int {
    return match($user->getSubscriptionPlan()) {
        'free' => 2000,      // Protège les coûts
        'student', 'perso' => 4000,  // Standard
        'pro' => 8000,       // Conversations riches
        'lintello' => 16000, // Contexte maximal
    };
}
```

**Impact** : Les plans premium peuvent avoir des conversations plus longues et des Thinks avec plus de contexte préservé entre nœuds.

---

---

### 11.6 Intégration backend — Point d'injection (Q4)

**Fichiers identifiés** :
- **Point d'entrée** : `ChatController::streamChat()` (ligne 86-352)
- **Service appelé** : `MistralService::streamChat()` (ligne 71-237)

#### Flux actuel (sans Thinks)

```
ChatController::streamChat()
    ↓
1. Vérifications user/conversation/quotas (ligne 89-120)
    ↓
2. ModeDetectorService.detectOptimalMode() (ligne 132)
   → Switch de mode si nécessaire (ligne 137-167)
    ↓
3. Préparation fichiers + recherche web (ligne 176-200)
    ↓
4. ModelDetectorService.detectOptimalModel() (ligne 219)
   → Détection Small/Large/Codestral (ligne 227-246)
    ↓
5. StreamedResponse avec callback (ligne 256)
    ↓
6. MistralService::streamChat() (ligne 280)
   → Generator yield chunk par chunk (ligne 185)
    ↓
7. Envoi SSE "data: {content: chunk}" (ligne 281)
    ↓
8. Event final "data: {done: true, ...}" (ligne 339)
```

#### Point d'injection identifié

**Ligne 280 dans ChatController::streamChat()** :

```php
// AVANT (ligne 280)
foreach ($mistralService->streamChat($conversation, $message, $user, $fileContents, $webSearchContext, $detectedModel) as $chunk) {
    echo "data: " . json_encode(['content' => $chunk]) . "\n\n";
    if (ob_get_level() > 0) {
        ob_flush();
    }
    flush();
}
```

**APRÈS (avec ThinkingEngine)** :

```php
// Ligne 240 : Récupérer le mode APRÈS ModeDetector
$activeMode = $conversation->getEffectiveMode();

// Ligne 280 : Injection du ThinkingEngine
$thinkingResult = null;

// Vérifier si le mode a un thinking_schema configuré
if ($activeMode && $activeMode->getThinkingSchema() !== null) {
    // ThinkingEngine s'active
    $thinkingResult = $this->thinkingEngine->process(
        userPrompt: $message,
        mode: $activeMode,
        user: $user,
        conversation: $conversation,
        fileContents: $fileContents,
        webSearchContext: $webSearchContext,
        detectedModel: $detectedModel
    );
}

// Si Thinks activé, utiliser son generator
if ($thinkingResult !== null) {
    // ThinkingEngine yield des events SSE spécifiques (thinking_step)
    foreach ($thinkingResult['generator'] as $event) {
        echo "data: " . json_encode($event) . "\n\n";
        if (ob_get_level() > 0) {
            ob_flush();
        }
        flush();
    }

    // Sauvegarder le ThoughtTree
    $this->thoughtTreeRepository->save($thinkingResult['tree_data'], $conversation);

} else {
    // Appel classique MistralService (flux existant)
    foreach ($mistralService->streamChat($conversation, $message, $user, $fileContents, $webSearchContext, $detectedModel) as $chunk) {
        echo "data: " . json_encode(['content' => $chunk]) . "\n\n";
        if (ob_get_level() > 0) {
            ob_flush();
        }
        flush();
    }
}
```

#### Modification de ThinkingEngine::process()

**Signature modifiée pour retourner un generator** :

```php
class ThinkingEngine
{
    /**
     * Traite un message avec le système de pensée si nécessaire.
     *
     * @return \Generator|null Generator d'events SSE si Thinks activé, null sinon
     */
    public function process(
        string $userPrompt,
        Mode $mode,
        User $user,
        Conversation $conversation,
        array $fileContents = [],
        ?string $webSearchContext = null,
        ?string $detectedModel = null
    ): ?\Generator {
        // 1. Vérifier si le mode a un schéma de pensée configuré
        if ($mode->getThinkingSchema() === null) {
            return null; // Pas de pensée → flux existant
        }

        $config = $mode->getThinkingConfig();

        // 2. Évaluer la complexité
        $complexity = $this->complexityDetector->evaluate($userPrompt);

        // 3. Si complexité < seuil → pas besoin du schéma
        if ($complexity < ($config['complexity_threshold'] ?? 3)) {
            return null; // Flux existant
        }

        // 4. Activer le schéma de pensée
        $tree = $this->treeBuilder->create($mode->getThinkingSchema(), $mode->getSlug());

        $strategy = match($mode->getThinkingSchema()) {
            'chain'  => $this->chainStrategy,
            'tree'   => $this->treeStrategy,
            'refine' => $this->refineStrategy,
        };

        // 5. Generator qui yield des events SSE + le contenu final
        return $this->executeWithStreaming(
            strategy: $strategy,
            userPrompt: $userPrompt,
            mode: $mode,
            config: $config,
            tree: $tree,
            user: $user,
            conversation: $conversation,
            fileContents: $fileContents,
            webSearchContext: $webSearchContext,
            detectedModel: $detectedModel
        );
    }

    /**
     * Exécute le Thinks et yield des events SSE progressifs
     */
    private function executeWithStreaming(...): \Generator
    {
        $startTime = microtime(true);
        $currentStep = 0;
        $totalSteps = 5; // Estimé, mis à jour dynamiquement

        // Yield event de démarrage
        yield [
            'type' => 'thinking_started',
            'schema' => $tree->getSchema(),
            'mode' => $mode->getName(),
        ];

        // Exécuter la stratégie avec yields intermédiaires
        foreach ($strategy->processWithStreaming(...) as $event) {
            // Les Strategies yielden des events 'thinking_step'
            yield $event;
        }

        // Yield la réponse finale
        yield [
            'type' => 'content',
            'content' => $finalResponse,
        ];

        // Yield event de fin avec tree_data
        yield [
            'type' => 'thinking_completed',
            'tree_data' => $tree->toArray(),
            'tokens_used' => $tree->getTotalTokens(),
            'duration_ms' => (int) ((microtime(true) - $startTime) * 1000),
        ];

        // Sauvegarder messages + usage log
        $this->saveMessagesAndUsage($conversation, $user, $tree, ...);
    }
}
```

#### Modification des Strategies

**Chaque Strategy doit yielder des events SSE** :

```php
class ChainStrategy implements ThinkingStrategyInterface
{
    public function processWithStreaming(...): \Generator
    {
        // ÉTAPE 1 : Décomposition
        yield [
            'type' => 'thinking_step',
            'step' => 1,
            'total' => 5,
            'status' => 'started',
            'summary' => 'Décomposition de la tâche...',
            'icon' => '🔍',
            'role' => 'decompose'
        ];

        $decomposition = $ai->chat($decomposePrompt, ...);
        $tree->setContent($rootId, $decomposition);

        yield [
            'type' => 'thinking_step',
            'step' => 1,
            'total' => 5,
            'status' => 'completed',
            'summary' => '3 stratégies identifiées',
            'icon' => '✅',
            'role' => 'decompose'
        ];

        // ÉTAPE 2-N : Exécution des steps
        foreach ($steps as $i => $step) {
            yield [
                'type' => 'thinking_step',
                'step' => $i + 2,
                'total' => count($steps) + 2,
                'status' => 'started',
                'summary' => $step['summary'],
                'icon' => '📊',
                'role' => 'execute'
            ];

            $result = $ai->chat($stepPrompt, ...);
            $tree->addChild($parentId, $step['summary'], ['content' => $result]);

            yield [
                'type' => 'thinking_step',
                'step' => $i + 2,
                'total' => count($steps) + 2,
                'status' => 'completed',
                'summary' => 'Étape terminée',
                'icon' => '✅',
                'role' => 'execute'
            ];
        }

        // ÉTAPE FINALE : Synthèse
        $finalResponse = $this->compileFinalResponse($tree, ...);

        return $finalResponse;
    }
}
```

#### Services à créer/modifier

| Service/Classe | Action | Fichier |
|----------------|--------|---------|
| **ThinkingEngine** | Créer | `src/Service/Thinking/ThinkingEngine.php` |
| **ComplexityDetector** | Créer | `src/Service/Thinking/ComplexityDetector.php` |
| **TreeBuilder** | Créer | `src/Service/Thinking/TreeBuilder.php` |
| **ChainStrategy** | Créer | `src/Service/Thinking/Strategy/ChainStrategy.php` |
| **TreeStrategy** | Créer | `src/Service/Thinking/Strategy/TreeStrategy.php` |
| **RefineStrategy** | Créer | `src/Service/Thinking/Strategy/RefineStrategy.php` |
| **ThoughtTreeRepository** | Créer | `src/Repository/ThoughtTreeRepository.php` |
| **ThoughtTree** (entité) | Créer | `src/Entity/ThoughtTree.php` |
| **Mode** (entité) | Modifier | `src/Entity/Mode.php` (ajouter `thinking_schema`, `thinking_config`) |
| **MistralService** | Modifier | Ajouter `getMaxHistoryTokens(User $user)` dynamique |
| **ChatController** | Modifier | Injecter `ThinkingEngine`, ligne 280 (switch Thinks/classique) |

#### Logique upsert de ThoughtTreeRepository

Une conversation n'a qu'**un seul** ThoughtTree (contrainte UNIQUE sur `conversation_id`, voir section 4). Le repository applique donc un **upsert** : création au premier Thinks, mise à jour ensuite.

```php
class ThoughtTreeRepository extends ServiceEntityRepository
{
    /**
     * Crée ou met à jour le ThoughtTree d'une conversation.
     * 1 conversation = 1 ThoughtTree (upsert, pas insert aveugle).
     */
    public function save(array $treeData, Conversation $conversation): ThoughtTree
    {
        // Chercher un ThoughtTree existant pour cette conversation
        $thoughtTree = $this->findOneBy(['conversation' => $conversation]);

        if ($thoughtTree === null) {
            // Première fois → création
            $thoughtTree = new ThoughtTree();
            $thoughtTree->setConversation($conversation);
            $thoughtTree->setCreatedAt(new \DateTimeImmutable());
        }

        // Dans les deux cas → mise à jour du tree_data (nœuds accumulés)
        $thoughtTree->setTreeData($treeData);                  // JSON chiffré en entité
        $thoughtTree->setSchemaUsed($treeData['schema']);
        $thoughtTree->setTotalTokens($this->sumTokens($treeData));
        $thoughtTree->setUpdatedAt(new \DateTimeImmutable());

        $this->getEntityManager()->persist($thoughtTree);
        $this->getEntityManager()->flush();

        return $thoughtTree;
    }

    private function sumTokens(array $treeData): int
    {
        // Parcours récursif des nœuds pour sommer les tokens
        return $this->sumNodeTokens($treeData['nodes'] ?? []);
    }

    private function sumNodeTokens(array $nodes): int
    {
        $total = 0;
        foreach ($nodes as $node) {
            $total += $node['tokens'] ?? 0;
            $total += $this->sumNodeTokens($node['children'] ?? []);
        }
        return $total;
    }
}
```

**Comportement** :
- **Message 1 (premier Thinks)** → `findOneBy` retourne `null` → création de l'enregistrement
- **Messages 2-N** → `findOneBy` retourne le ThoughtTree existant → mise à jour du `tree_data` avec les nouveaux nœuds ajoutés par la Strategy

Le `tree_data` passé à `save()` est **l'arbre complet courant** (pas un delta). C'est le `TreeBuilder` qui accumule les nœuds au fil de l'exécution et retourne la structure complète via `toArray()`.

#### Gestion du streaming SSE

**Point clé** : Le `ThinkingEngine::process()` retourne un `Generator` qui yield des events structurés.

**Types d'events SSE** :

| Type | Quand | Contenu |
|------|-------|---------|
| `thinking_started` | Début du Thinks | `{schema, mode}` |
| `thinking_step` | Avant/après chaque nœud | `{step, total, status, summary, icon, role}` |
| `content` | Réponse finale générée | `{content: "..."}` (comme actuellement) |
| `thinking_completed` | Fin du Thinks | `{tree_data, tokens_used, duration_ms}` |

**Frontend compatibilité** : Le frontend existant ignore les types inconnus → pas de régression si Thinks désactivé.

#### Ordre d'exécution validé (rappel Q1)

```
1. User envoie message
2. ModeDetectorService (ligne 132) → switch mode si nécessaire
3. Mode final récupéré (ligne 128)
4. ThinkingEngine.process() (ligne 280 NEW)
   → Si thinking_schema != null → Generator Thinks
   → Sinon → null → flux classique MistralService
5. Streaming SSE (progressif ou classique selon résultat)
```

#### Notes d'implémentation

**Sprint 1** :
- Créer les 6 classes Thinking
- Modifier `ChatController` (ligne 280)
- Ajouter champs `thinking_schema` et `thinking_config` sur `Mode`
- Créer table `ThoughtTree` + migration
- Implémenter `ChainStrategy` uniquement

**Sprint 2** (voir roadmap section 8) : `RefineStrategy`

**Sprint 3** (voir roadmap section 8) : `TreeStrategy`

**Sprint 4-5** :
- Améliorer ComplexityDetector (classificateur Small API)
- Optimiser streaming (animations frontend, react-flow)

---

---

### 11.5 UX Frontend — Affichage progressif (Q3)

**Décision validée** : **UX progressive avec feedback par étape (inspiré Claude Code)**

#### Comportement utilisateur

**Pendant l'exécution du Thinks** (5-10 secondes), l'utilisateur voit les étapes s'afficher progressivement :

```
User envoie : "Compare 3 stratégies marketing pour mon SaaS"
    ↓
🧠 LintellO réfléchit...
    ↓
🔍 Étape 1/5 : Décomposition de la tâche...
    ↓ (1.5s)
✅ 3 stratégies identifiées : SEO, LinkedIn Ads, Partenariats
    ↓
📊 Étape 2/5 : Analyse stratégie SEO...
    ↓ (2s)
✅ Budget et planning SEO établis
    ↓
📊 Étape 3/5 : Analyse stratégie LinkedIn Ads...
    ↓ (2s)
✅ Budget et planning Ads établis
    ↓
📊 Étape 4/5 : Analyse stratégie Partenariats...
    ↓ (1.5s)
✅ Budget et planning Partenariats établis
    ↓
🎯 Étape 5/5 : Comparaison et recommandation...
    ↓ (1s)
✅ Analyse terminée

[Réponse finale s'affiche avec tiroir "🧠 Voir le raisonnement détaillé"]
```

#### Architecture technique

**Backend — SSE streaming par nœud**

Chaque nœud envoie 2 événements SSE (avant et après exécution) :

```php
// Dans ThinkingEngine ou Strategy, AVANT d'exécuter le nœud
yield json_encode([
    'type' => 'thinking_step',
    'step' => $currentStep,
    'total' => $totalSteps,
    'status' => 'started',
    'summary' => 'Décomposition de la tâche...',
    'icon' => '🔍',
    'role' => 'decompose'
]) . "\n\n";

// Exécution du nœud (appel Mistral)
$result = $ai->chat($nodePrompt, ...);

// APRÈS l'exécution du nœud
yield json_encode([
    'type' => 'thinking_step',
    'step' => $currentStep,
    'total' => $totalSteps,
    'status' => 'completed',
    'summary' => '3 stratégies identifiées : SEO, LinkedIn Ads, Partenariats',
    'icon' => '✅',
    'role' => 'decompose'
]) . "\n\n";
```

**Frontend — Composant ThinkingProgress.tsx**

```tsx
interface ThinkingStep {
  step: number;
  total: number;
  status: 'started' | 'completed';
  summary: string;
  icon: string;
  role: string;
}

const ThinkingProgress: React.FC = () => {
  const [steps, setSteps] = useState<ThinkingStep[]>([]);

  // Écoute des events SSE
  useEffect(() => {
    const eventSource = new EventSource('/api/chat/stream');

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);

      if (data.type === 'thinking_step') {
        setSteps(prev => {
          // Si status = 'started', ajouter l'étape
          if (data.status === 'started') {
            return [...prev, data];
          }
          // Si status = 'completed', mettre à jour l'étape existante
          return prev.map(s =>
            s.step === data.step ? { ...s, ...data } : s
          );
        });
      }
    };

    return () => eventSource.close();
  }, []);

  return (
    <div className="thinking-progress">
      {steps.map((step, i) => (
        <div key={i} className={`thinking-step ${step.status}`}>
          <span className="icon">{step.icon}</span>
          <span className="summary">{step.summary}</span>
          {step.status === 'started' && <Spinner />}
        </div>
      ))}
    </div>
  );
};
```

#### Mapping icônes par rôle

| Rôle | Icône Started | Icône Completed | Exemple summary |
|------|---------------|-----------------|-----------------|
| `decompose` | 🔍 | ✅ | "Décomposition de la tâche..." / "3 stratégies identifiées" |
| `execute` | 📊 | ✅ | "Analyse stratégie SEO..." / "Budget et planning SEO établis" |
| `evaluate` | ⚖️ | ✅ | "Évaluation des approches..." / "Approche SEO : score 0.9" |
| `critique` | 🔎 | ✅ | "Vérification cohérence..." / "Cohérence validée" |
| `refine` | ✏️ | ✅ | "Amélioration version 2..." / "Version 2 améliorée" |
| `synthesize` | 🎯 | ✅ | "Synthèse finale..." / "Recommandation établie" |

#### Switch d'affichage Thinks activé/désactivé

**Problème** : Comment gérer l'affichage quand le Thinks n'est PAS activé (message simple, complexity < seuil) ?

**Solution** : Détection côté frontend via le premier event SSE

```tsx
// Dans ChatWindow.tsx
const [thinkingMode, setThinkingMode] = useState<'normal' | 'thinking'>('normal');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);

  // Premier event détermine le mode
  if (data.type === 'thinking_step') {
    setThinkingMode('thinking'); // Afficher ThinkingProgress
  } else if (data.type === 'content') {
    setThinkingMode('normal'); // Afficher le loader classique
  }
};

return (
  <div className="chat-window">
    {thinkingMode === 'thinking' ? (
      <ThinkingProgress />
    ) : (
      <div className="thinking-loader">
        🧠 LintellO réfléchit...
        <Spinner />
      </div>
    )}
  </div>
);
```

#### Affichage final (après Thinks terminé)

**Une fois tous les nœuds exécutés** :

1. La réponse finale s'affiche (event SSE `type: 'response'`)
2. Le composant `ThinkingProgress` disparaît
3. Un bouton/lien apparaît sous la réponse : **"🧠 Voir le raisonnement (5 étapes)"**
4. Au clic → ouverture du tiroir avec l'arbre complet (template `drawer`, `proposals`, etc.)

```tsx
<div className="message assistant">
  <div className="content">
    {messageContent}
  </div>

  {hasThinkingData && (
    <button
      className="view-thinking-btn"
      onClick={() => setShowThinkingDrawer(true)}
    >
      🧠 Voir le raisonnement ({thinkingData.nodes.length} étapes)
    </button>
  )}

  {showThinkingDrawer && (
    <ThinkingDrawer
      data={thinkingData}
      template={thinkingData.display_template}
      onClose={() => setShowThinkingDrawer(false)}
    />
  )}
</div>
```

#### Note d'implémentation

**Sprint 1** : Focus sur l'affichage progressif simple (icônes + texte).

**Sprint 5-6** : Amélioration avec animations, progress bar, et switch vers visualisation interactive (react-flow pour mind map).

---

### 11.7 Streaming SSE par nœud — Option C (Q5)

**Problème identifié** : Le Thinks fait 5-7 appels Mistral successifs (un par nœud). Chaque appel prend 1-3 secondes. Total = 5-10 secondes. Risque d'attente frustrante pour l'utilisateur si aucun feedback visuel pendant les appels.

**Décision validée** : **Option C — Nœuds intermédiaires synchrones, réponse finale streamée**

#### Architecture

**Nœuds intermédiaires (décomposition, analyse, évaluation)** : Appels synchrones

```php
// Dans ChainStrategy::processWithStreaming()

// Nœud 1 : Décomposition
yield ['type' => 'thinking_step', 'status' => 'started', 'summary' => 'Décomposition...'];

// Appel synchrone (bloquant 1-2s, pas de stream)
$decomposition = $this->mistralClient->chat($decomposePrompt, $systemPrompt, 'small');

yield ['type' => 'thinking_step', 'status' => 'completed', 'summary' => '3 stratégies identifiées'];

// Nœud 2 : Analyse stratégie A
yield ['type' => 'thinking_step', 'status' => 'started', 'summary' => 'Analyse SEO...'];

$resultA = $this->mistralClient->chat($promptA, $systemPrompt, 'small');

yield ['type' => 'thinking_step', 'status' => 'completed', 'summary' => 'Budget SEO établi'];

// Nœuds 3-4 : idem (synchrones)
```

**Nœud final (synthèse/réponse)** : Stream chunk par chunk

```php
// Nœud 5 : Synthèse finale
yield ['type' => 'thinking_step', 'status' => 'started', 'summary' => 'Synthèse finale...'];

// Stream la réponse finale comme actuellement
foreach ($this->mistralClient->streamChat($synthesisPrompt, $systemPrompt, 'auto') as $chunk) {
    yield ['type' => 'content', 'content' => $chunk];
}

yield ['type' => 'thinking_step', 'status' => 'completed', 'summary' => 'Analyse terminée'];
```

#### UX Résultante

```
User envoie : "Compare 3 stratégies marketing"
    ↓
🔍 Étape 1/5 : Décomposition de la tâche...
   [Spinner 1.5s]
✅ 3 stratégies identifiées
    ↓
📊 Étape 2/5 : Analyse stratégie SEO...
   [Spinner 2s]
✅ Budget et planning SEO établis
    ↓
📊 Étape 3/5 : Analyse stratégie LinkedIn Ads...
   [Spinner 2s]
✅ Budget et planning Ads établis
    ↓
📊 Étape 4/5 : Analyse stratégie Partenariats...
   [Spinner 1.5s]
✅ Budget et planning Partenariats établis
    ↓
🎯 Étape 5/5 : Synthèse finale...
   → "Voici ma recommandation..."  [texte qui s'écrit mot par mot]
   → "Stratégie SEO présente..."   [stream continu]
   → "le meilleur ROI..."          [stream continu]
✅ Analyse terminée
```

**Timing total** : ~8 secondes
- Nœuds 1-4 : 7 secondes avec spinners
- Nœud 5 (stream) : 1 seconde avec texte visible qui s'écrit

**Avantages** :
- ✅ Temps morts réduits (seulement sur les nœuds intermédiaires)
- ✅ Réponse finale fluide (comme actuellement)
- ✅ Balance simplicité/UX
- ✅ Compatible avec l'architecture existante

**Inconvénient acceptable** :
- ⚠️ Nœuds 1-4 : spinner visible pendant 1-2s chacun (mais progression visible : 1/5, 2/5, etc.)

#### Implémentation technique

**MistralClient doit exposer 2 méthodes** :

```php
class MistralClient
{
    /**
     * Appel synchrone (pas de stream)
     * Utilisé pour les nœuds intermédiaires Thinks
     */
    public function chat(string $prompt, string $systemPrompt, string $model): string
    {
        $response = $this->httpClient->request('POST', self::API_URL, [
            'json' => [
                'model' => $this->getModelName($model),
                'messages' => [...],
                'stream' => false, // ← Synchrone
            ],
        ]);

        return $response->toArray()['choices'][0]['message']['content'];
    }

    /**
     * Appel avec stream (Generator)
     * Utilisé pour la réponse finale
     */
    public function streamChat(...): \Generator
    {
        // Méthode existante, inchangée
        $response = $this->httpClient->request('POST', self::API_URL, [
            'json' => [..., 'stream' => true],
        ]);

        foreach ($this->httpClient->stream($response) as $chunk) {
            yield $chunk->getContent();
        }
    }
}
```

#### Code dans ChainStrategy

```php
class ChainStrategy
{
    public function processWithStreaming(...): \Generator
    {
        // Nœuds 1-N : Synchrones
        for ($i = 0; $i < count($steps) - 1; $i++) {
            yield ['status' => 'started', ...];
            $result = $this->mistralClient->chat($prompt, $systemPrompt, 'small');
            yield ['status' => 'completed', ...];
        }

        // Nœud final : Stream
        yield ['status' => 'started', 'summary' => 'Synthèse finale...'];

        $finalResponse = '';
        foreach ($this->mistralClient->streamChat($finalPrompt, $systemPrompt, 'auto') as $chunk) {
            $finalResponse .= $chunk;
            yield ['type' => 'content', 'content' => $chunk];
        }

        yield ['status' => 'completed', 'summary' => 'Analyse terminée'];

        return $finalResponse;
    }
}
```

#### Notes Sprint 1 vs Sprint 5

**Sprint 1** : Implémentation Option C
- Nœuds 1-N synchrones (simple)
- Nœud final streamé (réutilise code existant)
- UX acceptable (progression visible)

**Sprint 5 (amélioration optionnelle)** :
- Stream TOUS les nœuds (Option B complète)
- Animations plus fluides
- Affichage contenu intermédiaire dans le tiroir en temps réel

---

### 11.8 Comptabilisation quotas — Comptage APRÈS exécution complète (Q6)

**Problème identifié** : Le Thinks peut consommer plusieurs nœuds (~5-7 appels). Si on vérifie le quota AVANT ou PENDANT l'exécution, risque de stopper en plein milieu → frustration user avec réponse incomplète.

**Décision validée** : **Comptabilisation en fin de Thinks uniquement + Protection UX**

#### Principes clés

**1. AUCUNE vérification bloquante avant/pendant le Thinks**
- L'utilisateur reçoit **TOUJOURS** une réponse complète
- Pas de stop en plein milieu d'une exécution de nœuds
- Priorité absolue : ne pas frustrer avec une réponse incomplète

**2. Comptabilisation groupée APRÈS exécution**
- Tous les tokens sont loggés **après** que le dernier nœud soit terminé
- 1 seule entrée `UsageLog` pour tout le tree (pas une par nœud)
- Agrégation : `total_input_tokens`, `total_output_tokens`, `node_count`

**3. Notification d'upgrade APRÈS la réponse complète**
- Si quota dépassé → message affiché **après** la réponse finale
- Moment de conversion positif : "Vous avez vu la qualité ? Passez payant pour continuer"

#### Argument économique

**Coût d'un Thinks complet (5 nœuds en Small)** :
```
Input :  5 × 1500 tokens = 7500 tokens × 0.10€/M = 0.00075€
Output : 5 × 1000 tokens = 5000 tokens × 0.30€/M = 0.0015€
TOTAL : ~0.0023€ par Thinks
```

**Même si abus (user Gratuit 20 req Thinks/jour)** :
```
20 × 0.0023€ = 0.046€/jour = 1.38€/mois
→ Coût négligeable << frustration d'une réponse incomplète
```

**Conclusion** : Le coût de laisser finir un Thinks même si quota dépassé est **négligeable** comparé au coût d'acquisition/conversion perdu.

#### Comptabilisation par plan

**Plans Gratuit — Modes Découverte uniquement**
```
1 Think complet (ex: 5 nœuds) = 5 messages consommés sur les 20/jour
Mais : On termine TOUJOURS le Think même si quota dépassé en cours de route
```

**Exemple concret** :
```
User Gratuit : 18 messages déjà consommés sur 20
User pose question → Mode Recherche → Think de 5 nœuds
Pendant exécution : 18 → 19 → 20 → 21 → 22 (dépasse)
Résultat : User reçoit réponse complète + notification upgrade
```

**Plans Payants (Étudiant+, Perso, Pro, LintellO)**
```
Small illimité → Thinks ne comptent PAS dans un quota messages
On log juste les tokens pour stats, mais pas de limite bloquante
Seule limite : quotas Large/Codestral/Pixtral (si utilisés dans un nœud)
```

#### Implémentation technique

**Dans ThinkingEngine.php — Fin d'exécution**

```php
public function executeWithStreaming(...): \Generator
{
    // ... exécution complète du tree avec tous les nœuds ...

    // Yield réponse finale
    yield [
        'type' => 'content',
        'content' => $finalResponse,
    ];

    // Yield tree_data
    yield [
        'type' => 'thinking_completed',
        'tree_data' => $tree->toArray(),
        'tokens_used' => $tree->getTotalTokens(),
        'duration_ms' => $durationMs,
    ];

    // ===== COMPTABILISATION APRÈS TOUT =====

    $nodeCount = $this->treeBuilder->getNodeCount();
    $totalTokens = $this->treeBuilder->getTotalTokens();

    // Log usage APRÈS exécution complète
    $this->usageLogService->logThinkingUsage(
        user: $user,
        conversation: $conversation,
        totalInputTokens: $totalTokens['input'],
        totalOutputTokens: $totalTokens['output'],
        model: 'mistral-small-latest',
        thinkingSchema: $strategy->getName(),
        nodeCount: $nodeCount
    );

    // Pour Plan Gratuit uniquement : compter les messages
    if ($user->getPlan()->getSlug() === 'gratuit') {
        $this->quotaService->incrementMessageCount($user, $nodeCount); // +5 messages

        // Vérifier APRÈS si quota dépassé (pour notification seulement)
        $quotaStatus = $this->quotaService->checkQuotaStatus($user);

        if ($quotaStatus['exceeded']) {
            yield [
                'type' => 'quota_exceeded',
                'message' => '🎉 Vous avez épuisé vos 20 messages quotidiens. Passez à Perso (9.90€/mois) pour profiter de conversations illimitées !',
                'cta' => 'Voir les offres',
                'link' => '/abonnement'
            ];
        }
    }
}
```

**Dans UsageLogService.php — Nouvelle méthode**

```php
public function logThinkingUsage(
    User $user,
    Conversation $conversation,
    int $totalInputTokens,
    int $totalOutputTokens,
    string $model,
    string $thinkingSchema,
    int $nodeCount
): void
{
    $usageLog = new UsageLog();
    $usageLog->setUser($user);
    $usageLog->setConversation($conversation);
    $usageLog->setModel($model);
    $usageLog->setInputTokens($totalInputTokens);
    $usageLog->setOutputTokens($totalOutputTokens);
    $usageLog->setThinkingSchema($thinkingSchema); // Nouveau champ
    $usageLog->setNodeCount($nodeCount);           // Nouveau champ
    $usageLog->setCreatedAt(new \DateTimeImmutable());

    $this->entityManager->persist($usageLog);
    $this->entityManager->flush();
}
```

**Dans QuotaService.php — Modification**

```php
public function incrementMessageCount(User $user, int $count = 1): void
{
    // Incrémente le compteur de messages daily
    // Utilisé uniquement pour Plan Gratuit
    // $count = nombre de nœuds pour les Thinks, 1 pour message classique

    $dailyKey = 'user_messages_' . $user->getId() . '_' . date('Y-m-d');
    $current = $this->cache->get($dailyKey, 0);
    $this->cache->set($dailyKey, $current + $count, 86400); // TTL 24h
}
```

#### Comptage messages : Mode Général vs Modes Découverte

**Mode Général (pas de Thinks)** :
- Comptabilisation normale (avant/pendant comme actuellement)
- 1 question = 1 message
- Pas de changement du flux existant

**Modes Découverte (Plans Gratuit avec Thinks)** :
- 1 Think de 5 nœuds = **5 messages** consommés
- Justification : Chaque nœud est un appel API = 1 message
- **MAIS** : On termine toujours le Think même si quota dépassé

**Modes payants (tous modes avec Thinks)** :
- Thinks ne comptent pas dans quota messages (Small illimité)
- Log uniquement pour stats

#### Migration BDD — Nouveaux champs UsageLog

```sql
ALTER TABLE usage_log ADD COLUMN thinking_schema VARCHAR(20) DEFAULT NULL COMMENT 'chain, tree ou refine (si Thinks activé)';
ALTER TABLE usage_log ADD COLUMN node_count INT DEFAULT 0 COMMENT 'Nombre de nœuds générés (0 si pas de Thinks)';
```

#### Affichage notification frontend

**Si quota dépassé** :

```tsx
{quotaExceeded && (
  <div className="quota-exceeded-notification">
    <div className="icon">🎉</div>
    <div className="message">
      {quotaExceeded.message}
    </div>
    <a href={quotaExceeded.link} className="cta-button">
      {quotaExceeded.cta}
    </a>
  </div>
)}
```

**Timing** : La notification apparaît **APRÈS** que la réponse complète soit affichée.

#### Résumé décision

| Aspect | Décision |
|--------|----------|
| **Quand compter ?** | **APRÈS** exécution complète du Thinks |
| **Protection UX** | Toujours finir le Think, même si quota dépassé |
| **Plan Gratuit** | 1 nœud = 1 message (Think de 5 nœuds = 5 messages) |
| **Plans Payants** | Thinks ne compte pas dans quota messages (Small illimité) |
| **Notification** | Après la réponse complète si quota dépassé |
| **Justification** | Coût négligeable (< 0.05€/jour max) vs frustration user |

---

### 11.9 Stockage tree_data (Q7)

**Décision** : JSON brut chiffré AES-256-GCM, 1 tree = 1 enregistrement SQL. Pas de compression gzip (complexité inutile, économie < 0.01€/mois). Pas de fragmentation parent/enfant (volume max ~10 KB << limite MySQL 1 GB). Les `max_depth`/`max_iterations`/`max_branches` dans `thinking_config` garantissent un volume borné.

---

### 11.10 Arbre conversationnel Git-like — Branches, Backtrack et Merge (Q8)

**Décision** : 1 tree par conversation, arbre vivant mis à jour à chaque message. Pas un snapshot figé.

```
1 Conversation = 1 ThoughtTree (upsert à chaque message Thinks)
1 Message      = nœud enfant ajouté dans la branche active
1 Backtrack    = switch de conversation.active_node_id
1 Merge        = nœud role:"merge" avec parents: [node-A, node-B]
```

#### Structure JSON tree_data (évolutive)

```json
{
  "schema": "tree",
  "mode": "strategie",
  "nodes": [
    {
      "id": "branch-seo",
      "role": "execute",
      "branch": "SEO",
      "score": 0.9,
      "status": "selected",
      "summary": "Acquisition organique",
      "content": "Stratégie SEO détaillée...",
      "message_id": "msg-001",
      "parents": [],
      "children": [
        {
          "id": "node-seo-tech",
          "role": "conversation",
          "summary": "SEO technique",
          "message_id": "msg-002",
          "parents": ["branch-seo"],
          "children": [
            {
              "id": "node-seo-content",
              "role": "conversation",
              "summary": "Stratégie contenu",
              "message_id": "msg-003",
              "parents": ["node-seo-tech"],
              "children": []
            }
          ]
        }
      ]
    },
    {
      "id": "branch-ads",
      "role": "execute",
      "branch": "LinkedIn Ads",
      "score": 0.7,
      "status": "explored",
      "summary": "Acquisition payante",
      "content": "Stratégie Ads détaillée...",
      "message_id": "msg-004",
      "parents": [],
      "children": [
        {
          "id": "node-ads-targeting",
          "role": "conversation",
          "summary": "Ciblage LinkedIn",
          "message_id": "msg-005",
          "parents": ["branch-ads"],
          "children": [
            {
              "id": "node-ads-budget",
              "role": "conversation",
              "summary": "Budget 100 leads",
              "message_id": "msg-006",
              "parents": ["node-ads-targeting"],
              "children": []
            }
          ]
        }
      ]
    },
    {
      "id": "branch-partner",
      "role": "execute",
      "branch": "Partenariats",
      "score": 0.5,
      "status": "pruned",
      "message_id": null,
      "parents": [],
      "children": []
    },
    {
      "id": "merge-001",
      "role": "merge",
      "summary": "Fusion SEO + Ads",
      "message_id": "msg-007",
      "parents": ["node-seo-content", "node-ads-budget"],
      "children": [
        {
          "id": "node-hybrid",
          "role": "conversation",
          "summary": "Stratégie hybride",
          "message_id": "msg-008",
          "parents": ["merge-001"],
          "children": []
        }
      ]
    }
  ]
}
```

#### BDD — Tracking branche active

**Table `conversation`** (mis à jour à chaque message/backtrack/merge) :
```sql
ALTER TABLE conversation ADD COLUMN active_tree_id CHAR(36) NULL;
ALTER TABLE conversation ADD COLUMN active_node_id VARCHAR(50) NULL
  COMMENT 'ID du nœud actif dans tree_data';
ALTER TABLE conversation ADD FOREIGN KEY (active_tree_id)
  REFERENCES thought_tree(id) ON DELETE SET NULL;
```

**Exemple état conversation** :
```sql
-- User sur branche SEO
active_node_id: "branch-seo"

-- Après backtrack vers Ads
active_node_id: "branch-ads"    ← Switch !

-- Après merge
active_node_id: "merge-001"     ← Nouveau point de départ
```

#### UX Tiroir — 2 niveaux

**Niveau 1 : Vue branches** (défaut)

Montre les branches + leur arborescence de messages :

```
🌳 Arbre de conversation

├─ ✅ Branche SEO (retenue)
│  ├─ msg-002: 5 optimisations SEO
│  └─ msg-003: 30 articles/mois ───┐
│                                   ▼
├─ 🔶 Branche Ads (explorée)      🔀 MERGE → msg-007
│  ├─ msg-005: Ciblage             │
│  └─ msg-006: Budget ─────────────┘
│                                    └─ msg-008: Stratégie hybride
│
└─ ❌ Partenariats (non explorée)
   └─ [🔀 Explorer] [🔀 Merger avec...]
```

**Boutons cliquables par type de nœud** :

| Nœud | Boutons disponibles |
|------|---------------------|
| Branche `selected` | `[💬 Cette réponse]` `[🔍 Voir raisonnement]` |
| Branche `pruned` | `[🔀 Explorer]` `[🔀 Merger avec...]` `[🔍 Voir raisonnement]` |
| Branche `explored` | `[🔍 Voir raisonnement]` |
| Nœud `conversation` | `[💬 Voir message]` |
| Nœud `merge` | `[🔀 Voir la fusion]` |

**Niveau 2 : Détail raisonnement** (clic sur `[🔍 Voir raisonnement]`)

Montre les étapes internes de l'IA (décomposition, évaluation, critique...) pour cette branche.

#### Résumé décision Q8

| Aspect | Décision |
|--------|----------|
| **Branches pruned** | Stockées dans tree_data JSON, pas de Message initial |
| **Backtrack** | Change `conversation.active_node_id`, repart du nœud d'origine (PAS enfant de la branche précédente) |
| **Branches parallèles** | Coexistent dans le même tree, jamais fusionnées sauf Merge explicite |
| **Merge** | Nœud `role: "merge"`, champ `parents: [node-A, node-B]` |
| **1 conversation** | 1 seul ThoughtTree, mis à jour à chaque message |
| **Tiroir niveau 1** | Branches + messages (vue humaine) |
| **Tiroir niveau 2** | Étapes de raisonnement IA (vue détaillée) |
| **Innovation** | Premier chatbot avec arbre conversationnel navigable et mergeable |

---

### 11.11 ComplexityDetector & Protection serveur (Q9)

**Problème identifié** : Le ComplexityDetector détermine si le Thinks s'active. Deux risques :
- **Faux positifs** (Thinks inutile) → gaspillage serveur + tokens
- **Faux négatifs** (Thinks manqué) → réponse moins bonne

**Facteur non anticipé** : Un Thinks consomme ~5× plus de ressources serveur qu'une réponse classique (5 appels HTTP séquentiels = 1 thread PHP bloqué 8-15 secondes).

**Décision validée** : **Threshold 3 + Levier 1 (concurrent) + Levier 2 (quota horaire)**

#### Threshold : 3 par défaut

```
Score 1-2 : Question simple → Réponse classique directe
Score 3+  : Question complexe → Thinks activé
```

**Exemple** :
```
"C'est quoi le SEO ?"              → Score 2 → Réponse classique
"Compare 5 stratégies SEO B2B"     → Score 4 → Thinks activé
"Organise mes révisions 6 matières → Score 3 → Thinks activé
 avec contraintes de planning"
```

**Ajustement par mode** :

| Mode | Threshold | Raison |
|------|-----------|--------|
| Maths, Juridique | 2 | Presque toujours complexe par nature |
| Recherche, Stratégie, Code | 3 | Standard (défaut) |
| Rédaction, Communication | 3 | Standard (défaut) |
| Créatif | 2 | Exploration toujours utile |
| Conversation, Général | `null` | Jamais de Thinks |

#### Levier 1 : MAX_THINKS_CONCURRENT (protection serveur global)

```php
// QuotaService.php
const MAX_THINKS_CONCURRENT = 20; // À ajuster selon config serveur PHP-FPM

// Dans ThinkingEngine::process()
$activeThinks = $redis->get('thinks_active_count') ?? 0;

if ($activeThinks >= self::MAX_THINKS_CONCURRENT) {
    // Dégradation SILENCIEUSE (pas de message à l'user)
    // Il reçoit une bonne réponse classique, pas de frustration
    return null; // Flux classique MistralService
}

$redis->incr('thinks_active_count');
try {
    // ... exécution Thinks ...
} finally {
    $redis->decr('thinks_active_count'); // Toujours décrémenté, même si erreur
}
```

**Comportement** : Dégradation **silencieuse** — l'user reçoit une réponse classique de qualité sans savoir que le Thinks a été désactivé temporairement.

#### Levier 2 : Quota Thinks/heure par plan

```php
// QuotaService.php
const THINKS_PER_HOUR = [
    'gratuit'   => 3,
    'etudiant'  => 10,
    'perso'     => 20,
    'pro'       => 50,
    'lintello'  => PHP_INT_MAX,
];

public function canUseThinks(User $user): bool
{
    $plan = $user->getSubscriptionPlan();
    $limit = self::THINKS_PER_HOUR[$plan] ?? 3;

    $hourKey = 'thinks_' . $user->getId() . '_' . date('Y-m-d-H');
    $used = $this->redis->get($hourKey) ?? 0;

    return $used < $limit;
}

public function incrementThinksUsage(User $user): void
{
    $hourKey = 'thinks_' . $user->getId() . '_' . date('Y-m-d-H');
    $this->redis->incr($hourKey);
    $this->redis->expire($hourKey, 3600); // TTL 1 heure
}
```

**Si quota horaire dépassé** : Notification visible (contrairement au Levier 1) :
```json
{
  "type": "thinks_quota_exceeded",
  "message": "Vous avez utilisé vos 10 analyses avancées cette heure. Recharge dans 23 minutes ou passez à Pro.",
  "reset_in_minutes": 23
}
```

#### Pipeline de décision complet

```
Question reçue
    ↓
1. thinking_schema = null ?
   → OUI : Réponse classique (mode sans Thinks)
    ↓
2. ComplexityDetector score < threshold (3) ?
   → OUI : Réponse classique (question trop simple)
    ↓
3. canUseThinks(user) = false ? (quota horaire)
   → OUI : Réponse classique + notification upgrade
    ↓
4. thinks_active_count >= MAX_THINKS_CONCURRENT ?
   → OUI : Réponse classique silencieuse (serveur chargé)
    ↓
✅ Thinks activé !
```

#### Coût réel d'un Thinks vs réponse classique

| Ressource | Réponse classique | Thinks (5 nœuds) | Ratio |
|-----------|-------------------|-------------------|-------|
| Thread PHP | ~2-3 secondes | ~8-15 secondes | ×5 |
| Tokens Mistral | ~2000 tokens | ~10 000 tokens | ×5 |
| Coût API | ~0.0004€ | ~0.002€ | ×5 |
| SSE connection | ~2-3 secondes | ~8-15 secondes | ×5 |

**Conclusion** : Threshold 3 + protection serveur = **Thinks visible sur vraies questions complexes** sans risque de saturation.

#### Résumé décision Q9

| Aspect | Décision |
|--------|----------|
| **Threshold par défaut** | 3 (questions vraiment complexes uniquement) |
| **Threshold par mode** | Configurable dans `thinking_config` (2 pour Maths/Juridique/Créatif) |
| **Faux positifs** | Acceptés mais limités par threshold 3 |
| **Faux négatifs** | Acceptés (user reçoit bonne réponse classique) |
| **Protection serveur** | Levier 1 : `MAX_THINKS_CONCURRENT = 20` (silencieux) |
| **Protection abus** | Levier 2 : quota horaire par plan (avec notification) |
| **Leviers futurs** | File d'attente (Q Sprint 5) + réduction dynamique complexité |

---

### 11.12 Kill-switch global — Réutilisation du Levier 1 (Q10)

**Problème identifié** : Comment désactiver le Thinks en urgence (bug critique, instabilité) sans déploiement ni manipulation serveur ?

**Insight clé** : Si le serveur est totalement planté → tout est planté de toute façon. Le vrai besoin c'est : **Thinks bugué MAIS serveur encore vivant → basculer vers réponse classique instantanément.**

**Décision validée** : **Réutiliser le Levier 1 de Q9 — Forcer `thinks_active_count` à `PHP_INT_MAX` via EasyAdmin**

#### Principe : Zéro code supplémentaire

Le pipeline de décision Q9 gère déjà le cas :

```
4. thinks_active_count >= MAX_THINKS_CONCURRENT ?
   → OUI : Réponse classique silencieuse ✅
```

**Kill-switch = forcer ce compteur à la valeur maximale.**

```php
// Désactiver tous les Thinks (kill-switch ON)
$redis->set('thinks_active_count', PHP_INT_MAX);
$redis->set('thinks_kill_switch', true); // Flag pour le dashboard EasyAdmin

// Réactiver (kill-switch OFF)
$redis->del('thinks_active_count');
$redis->del('thinks_kill_switch');
```

**Résultat** : Instantané, sans déploiement, sans restart serveur, sans manipulation technique.

#### Interface EasyAdmin

```
EasyAdmin → Paramètres système → Thinks Engine

┌─────────────────────────────────────────┐
│ ⚙️  Thinks Engine                        │
│                                          │
│  Status actuel : ● ACTIF                │
│  Thinks en cours : 3 / 20              │
│                                          │
│  [🔴 Désactiver Thinks]  ← 1 clic      │
└─────────────────────────────────────────┘

── Si kill-switch actif ──

┌─────────────────────────────────────────┐
│ ⚙️  Thinks Engine                        │
│                                          │
│  Status actuel : ● DÉSACTIVÉ (manuel)  │
│  Thinks en cours : 0 / 20              │
│                                          │
│  [🟢 Réactiver Thinks]  ← 1 clic       │
└─────────────────────────────────────────┘
```

#### Pipeline de décision final et complet

```
Question reçue
    ↓
1. thinking_schema = null ?
   → OUI : Réponse classique (mode sans Thinks)
    ↓
2. ComplexityDetector score < threshold (3) ?
   → OUI : Réponse classique (question trop simple)
    ↓
3. Quota horaire user dépassé ?
   → OUI : Réponse classique + notification upgrade
    ↓
4. thinks_active_count >= MAX_THINKS_CONCURRENT ?
   → OUI : Réponse classique silencieuse
   ↑
   └── KILL-SWITCH : redis->set('thinks_active_count', PHP_INT_MAX)
       = même effet, 1 clic EasyAdmin, instantané
    ↓
✅ Thinks activé !
```

#### Avantages de cette approche

| Critère | Résultat |
|---------|----------|
| **Vitesse** | Instantané (Redis, pas de BDD) |
| **Simplicité** | 0 ligne de code supplémentaire |
| **Réversibilité** | 1 clic pour réactiver |
| **Sans déploiement** | ✅ |
| **Sans restart serveur** | ✅ |
| **Sans manipulation technique** | ✅ (EasyAdmin suffit) |

#### Résumé décision Q10

| Aspect | Décision |
|--------|----------|
| **Mécanisme** | Réutilisation Levier 1 Q9 (`thinks_active_count = PHP_INT_MAX`) |
| **Interface** | Bouton EasyAdmin "Désactiver/Réactiver Thinks" |
| **Stockage** | Redis (instantané, pas de restart) |
| **Comportement** | Dégradation silencieuse → users reçoivent réponse classique |
| **Réactivation** | 1 clic → `redis->del('thinks_active_count')` |
| **Code ajouté** | Aucun (réutilise pipeline Q9 existant) |

---

## ✅ Q1 à Q10 — Toutes les décisions architecturales documentées

| Q | Sujet | Décision clé |
|---|-------|-------------|
| Q1 | ModeDetector ↔ ThinkingEngine | ModeDetector AVANT, inactif pendant Thinks |
| Q2 | Modèle économique | Thinks conditionné par accès modes, Small illimité |
| Q3 | UX Frontend | Affichage progressif inspiré Claude Code |
| Q4 | Point d'injection backend | Ligne 280 ChatController, Generator SSE |
| Q5 | Streaming | Option C : nœuds sync, réponse finale streamée |
| Q6 | Quotas | Comptabilisation APRÈS exécution, jamais couper en plein milieu |
| Q7 | Stockage tree_data | JSON brut chiffré, 1 tree = 1 enregistrement |
| Q8 | Branches & Backtrack | Arbre Git-like évolutif, branches parallèles, merge |
| Q9 | ComplexityDetector | Threshold 3 + Levier 1 concurrent + Levier 2 horaire |
| Q10 | Kill-switch | Réutilisation Levier 1 via EasyAdmin, instantané |
