# Guide de l'API Figen pour ROMEO

Ce guide s'adresse aux développeurs qui branchent ROMEO sur l'API de Figen. Il se lit dans l'ordre :
l'essentiel en une page, un premier appel, la façon de marquer un modèle, puis les parcours un par un, avec
leurs commandes `curl`. Tous les exemples sont fictifs. Version du guide : API `v1`, novembre 2026.

## 1. L'essentiel en une page

### Ce que fait l'API

Deux choses.

**Contrôler les modèles.** Inovéa dépose ses modèles de documents réglementaires : ce sont les *modèles de
base*. Chaque cabinet peut en adapter une copie, le *modèle du cabinet*. L'API compare cette copie au modèle
de base et dit ce qui a été supprimé ou modifié dans les mentions obligatoires.

**Générer les documents.** ROMEO envoie les données d'un dossier. L'API rend le document en Markdown et en
HTML, rédige les commentaires libres quand le modèle en prévoit, puis contrôle le rendu.

Tout se passe de serveur à serveur, avec une clé d'API. Aucun utilisateur d'Inovéa ne se connecte chez Figen.
Les textes circulent en Markdown ; le HTML est accepté en entrée et converti. Jamais de Word.

### Les trois règles

1. **Figen contrôle et alerte, ROMEO décide.** L'API dit l'état d'un modèle et détaille ses alertes. Bloquer
   un cabinet ou le laisser continuer reste votre décision. Figen ne corrige jamais un modèle.
2. **Rien de vos clients n'est conservé.** Le document généré part dans la réponse. Il ne reste chez nous
   qu'une ligne de journal, sans contenu.
3. **`/v1` ne casse jamais sans préavis.** Vous êtes prévenus avant tout changement, chaque évolution est
   consignée dans le journal des changements, et une rupture passe par une nouvelle version.

### Le vocabulaire

| Mot | Sens |
|---|---|
| Modèle de base | Le modèle déposé par Inovéa. Il est versionné et fait référence. |
| Modèle du cabinet | La copie adaptée par un cabinet (un CIF). `cif_id` est l'identifiant du cabinet, choisi par ROMEO. |
| Mention obligatoire | Un passage que le cabinet ne peut ni supprimer ni dénaturer. Dans le modèle, une marque de *clause* la délimite. |
| Zone libre | Tout ce qui n'est pas dans une clause. Le cabinet peut l'adapter. |
| Commentaire | Un texte libre, rédigé par l'IA au moment de la génération ou fourni par ROMEO. Dans le modèle, une marque de *bloc IA* dit où il se place. |
| Contrôle | La comparaison d'un texte avec sa référence. Il rend un statut et des alertes. |
| Alerte | Un écart constaté. Elle est `bloquante` ou vaut simple `information`. |
| Dérive | L'état d'un modèle de cabinet qui était valide quand le modèle de base a été republié. Il attend une revalidation. |

Ce guide dit *mention*, *cabinet*, *modèle* et *commentaire*. Dans les marques, les champs et les codes de
l'API, les mêmes choses s'appellent `clause`, `cif`, `gabarit` et `bloc` ou `ia`.

### Où trouver quoi

| Adresse | Contenu |
|---|---|
| `/guide` | Ce guide, suivi du journal des changements de l'API. |
| `/docs` | La référence complète des routes et de leurs schémas. |
| `/openapi.json` | La même référence, au format OpenAPI. |

## 2. Premier appel en deux minutes

Il vous faut `curl`, `jq` (version 1.6 ou plus récente : le parcours 4.6 utilise son option `--rawfile`) et
deux variables d'environnement. L'adresse vous est remise avec votre clé.

```bash
export ROMEO_URL="…"   # adresse de l'API, sans barre oblique à la fin
export ROMEO_CLE="…"   # votre clé : ne l'écrivez ni dans un dépôt, ni dans une adresse
```

Premier appel, sans clé. `/v1/sante` est la seule route de `/v1` qui n'en demande pas.

```bash
curl -sS "$ROMEO_URL/v1/sante"
```

```json
{"statut": "ok", "service": "inovea-romeo", "version": "0.1.0",
 "revision": "a653dd83277e562295e158df51fd88bae0c38a17", "environnement": "recette",
 "base_de_donnees": "ok", "ia": {"mode": "bedrock", "modele": "claude-opus-5-5"},
 "heure": "2026-11-10T09:00:00Z"}
```

`revision` identifie le code servi : elle change à chaque mise à jour du service, citez-la dans vos
questions. Si la base de données ne répond pas, l'API répond `503` avec `"statut": "degrade"`. `ia.mode` vaut `bedrock`
ou `factice` : regardez-le avant vos essais. En mode factice, les jugements et les rédactions sont simulés et
signés `"modele_ia": "factice"` : de quoi tester l'enchaînement des appels, pas juger de la qualité d'un avis.

Deuxième appel, avec la clé :

```bash
curl -sS "$ROMEO_URL/v1/moi" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"cle": {"nom": "ROMEO recette", "prefixe": "romeo_rec_ab12cd", "creee_le": "…",
         "derniere_utilisation": "…"},
 "environnement": "recette", "ia": {"mode": "bedrock", "modele": "claude-opus-5-5"}}
```

La réponse nomme la clé, sans jamais la répéter, et dit dans quel environnement elle travaille. `recette` est
l'environnement de test. `creee_le` et `derniere_utilisation` sont des dates ; prévoyez aussi `null` : c'est
ce que rend la clé de développement d'un service lancé en local.

Toutes les autres routes de `/v1` attendent le même en-tête. Sans clé reconnue, l'API répond `401`, avec une
erreur de la forme décrite en partie 9. Pour lire une réponse à l'écran, ajoutez `| jq` à la commande. Dans
les corps JSON que vous envoyez, un champ inconnu est refusé (`400 requete_invalide`) : une faute de frappe
ne passe pas inaperçue. Seuls `donnees` et `blocs` portent des clés libres.

## 3. Le modèle de document

Un modèle est un texte Markdown qui suit quelques conventions. Elles disent à l'API ce qui se remplace, ce qui
est obligatoire et ce qui se rédige.

### Les conventions de marquage

| Convention | Syntaxe | Rôle |
|---|---|---|
| Variable | `{{ org_orias }}`, `{{ product.name }}` | Remplacée par une donnée à la génération. |
| Condition, boucle | `{% if org_status_cif %}…{% endif %}`, `{% for product in products %}…{% endfor %}` | Syntaxe Jinja2 : un passage présent sous condition, ou répété. |
| Clause | `<!-- clause: mediation -->…<!-- /clause -->` | Délimite une mention obligatoire. |
| Bloc IA | `<!-- ia: justification_recommandation -->` | Emplacement d'un commentaire rédigé à la génération. Toujours hors d'une clause. |
| Note | `<!-- note: … -->` | Consigne au conseiller. Ignorée au contrôle, retirée à la génération. |

Tout ce qui n'est pas dans une clause est une zone libre, que le cabinet peut adapter.

Une clause porte un identifiant (minuscules, chiffres et `_`, en commençant par une lettre) et trois attributs
facultatifs, séparés par une barre verticale :

```markdown
<!-- clause: immatriculation | exigence: identique | titre: Immatriculation -->
```

- `exigence` dit jusqu'où le cabinet peut toucher au texte. Avec `sens`, la valeur par défaut, il peut
  reformuler tant que le sens reste le même. Avec `identique`, le texte se reprend mot pour mot.
- `titre` donne à la mention un libellé lisible dans les rapports.
- `presence` dit si le cabinet peut se passer de la mention. Sans cet attribut, elle est obligatoire. Avec
  `presence: facultative`, le cabinet peut la retirer en entier, pas la dénaturer.

Exemple, inventé lui aussi : une section qui ne concerne que les cabinets exerçant une seconde activité.

```markdown
<!-- clause: activite_credit | presence: facultative | titre: Activité de crédit -->
Le cabinet exerce aussi une activité d'intermédiaire en crédit, pour laquelle il est immatriculé à part.
<!-- /clause -->
```

Un cabinet qui n'exerce pas cette activité supprime la section : le contrôle le note pour information
(`clause_facultative_retiree`) et le modèle reste valide. Un cabinet qui la garde mais en change le sens
reçoit la même alerte bloquante que pour toute autre mention.

Un bloc IA peut porter sa propre consigne de rédaction : `<!-- ia: cle | consigne: … -->`.

Les variables acceptent des filtres de mise en forme :

| Filtre | Valeur reçue | Texte rendu |
|---|---|---|
| `date` | `2026-12-15` | 15 décembre 2026 |
| `date("court")` | `2026-12-15` | 15/12/2026 |
| `euros` | `1234.5` | 1 234,50 € |
| `pct` | `3.5` | 3,5 % |
| `ouinon` | `true` | Oui |

S'y ajoutent `nombre` et les filtres courants de Jinja2, par exemple `{{ org_orias | default("néant") }}` ou
`{{ vigilance | join(", ") }}`. Même sans filtre, un booléen rendu par une variable s'écrit « Oui » ou
« Non », jamais « True ».

### Un mini-modèle, fictif

Le modèle ci-dessous est inventé pour ce guide. Il sert dans tous les parcours ; enregistrez-le dans un
fichier `modele.md` pour les rejouer.

```markdown
# Rapport de conseil

{{ org_name }} a préparé ce rapport pour {{ client_first_name }}.

<!-- note: Adaptez cette présentation à votre cabinet. -->
Notre cabinet accompagne des particuliers dans la gestion de leur patrimoine depuis 2009.

<!-- clause: immatriculation | exigence: identique | titre: Immatriculation -->
{{ org_name }} est immatriculé au registre des intermédiaires sous le numéro {{ org_orias }}.
<!-- /clause -->

{% if org_status_cif %}
<!-- clause: mediation | titre: Médiation -->
En cas de désaccord, écrivez d'abord au cabinet. Sans réponse satisfaisante sous deux mois, vous pouvez
saisir gratuitement le médiateur : {{ org_mediator_name }}.
<!-- /clause -->
{% endif %}

## Notre recommandation

<!-- ia: justification_recommandation | consigne: Expliquer en quelques phrases le choix proposé. -->
```

Ce qu'il faut y voir :

- Le paragraphe de présentation est une zone libre. Le cabinet l'adapte comme il veut, et la note qui le
  précède ne sortira jamais dans un document.
- `immatriculation` est une mention à reprendre mot pour mot. Le moindre changement de texte lève une alerte
  bloquante, sans appel à l'IA.
- `mediation` est une mention dont le sens doit être conservé. Le cabinet peut la reformuler ; si le texte
  change, un juge dit si le sens a tenu.
- La condition `{% if org_status_cif %}` entoure la mention de médiation. Quand la donnée vaut faux, la
  mention est écartée et dite non applicable : le document reste conforme. Placée à l'intérieur de la clause,
  la même condition laisserait une mention vide, et une alerte bloquante (`clause_vide_au_rendu`).
- Cette condition fait partie de ce qui est contrôlé. Un cabinet qui la retire, la change, ou en ajoute une
  autour d'une mention reçoit une alerte bloquante (`condition_modifiee`), même si le texte de la mention
  n'a pas bougé : entourée d'une condition fausse, la mention disparaîtrait du document.
- Le bloc IA sera remplacé par un commentaire, rédigé par l'IA ou fourni par ROMEO.

### Pourquoi des commentaires HTML

Les marques de clause, de bloc IA et de note sont des commentaires HTML, pour trois raisons.

- **Ils sont invisibles au rendu.** Le cabinet voit son modèle comme le client verra le document.
- **Ils survivent au Markdown comme au HTML.** La même marque vaut dans les deux formats que l'API accepte en
  entrée.
- **Ils permettent de recontrôler un document après coup.** Un document généré garde ses marques de clause
  (option `conserver_marques`), toujours invisibles. S'il est retouché avant signature, l'API retrouve chaque
  mention.

Si un éditeur de texte retire les marques, le contrôle ne tombe pas pour autant. L'API cherche le texte de
chaque mention dans tout le document. Retrouvé à l'identique, il donne une simple information
(`marque_absente`). Retrouvé avec des changements, il est traité comme une mention modifiée. Introuvable, la
mention est tenue pour supprimée.

Une limite connue de l'entrée en HTML : dans un tableau, une ligne qui porte une balise Jinja ou une cellule
qui porte un filtre (`{{ montant | euros }}`) ne survit pas au passage par un éditeur HTML, parce que la
barre verticale y sépare les cellules. Le Markdown reste donc le format de référence des modèles. Au dépôt
d'un modèle de base, `contenu_markdown` vous rend ce que l'API a compris. Pour le modèle d'un cabinet, il se
lit dans son détail (`GET /v1/cifs/{cif_id}/modeles/{code}`).

### Ce que le contrôle tient pour identique

La comparaison ignore ce qui ne change pas le fond : la typographie (apostrophes, guillemets, tirets), les
espaces, le gras et l'italique, les notes, l'espacement à l'intérieur des expressions Jinja. Deux textes qui
ne diffèrent que par cela sont identiques pour le contrôle.

### Les données attendues à la génération

ROMEO envoie un objet JSON à plat pour les valeurs simples (`org_name`, `client_first_name`,
`profile_risk_label`…). S'y ajoutent `products`, une liste d'objets dont les clés sont celles des variables
`product.*` sans le préfixe, puis `totals`, `remuneration`, `vigilance` (une liste de phrases) et les
drapeaux de condition (`org_status_cif`, `org_status_miobsp`, `realestateActive`, `personType`…). Le modèle
lit ce JSON tel quel.

Une valeur mal formée est refusée : `400 requete_invalide`, avec le chemin de la clé dans `details`. C'est
le cas de `products` qui n'est pas une liste d'objets, ou d'un booléen là où le catalogue attend un texte.

`GET /v1/catalogue-variables` liste les variables, leurs groupes, les drapeaux de condition et les blocs IA
connus : près de 400 variables, une trentaine de drapeaux, une quinzaine de blocs IA. L'exemple n'en montre
qu'un extrait.

```bash
curl -sS "$ROMEO_URL/v1/catalogue-variables" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"variables": [{"cle": "org_orias", "libelle": "N° ORIAS de l'organisation", "type": "text",
                "groupe": "cabinet", "source": "figen"}],
 "groupes": [{"id": "cabinet", "libelle": "Cabinet"}], "drapeaux": ["org_status_cif", "personType"],
 "blocs_ia": [{"cle": "justification_recommandation", "libelle": "…", "description": "…"}]}
```

Avec `?code=`, la réponse porte en plus `modele` : ce que ce modèle utilise, mention par mention. Pour le
mini-modèle, une fois déposé au parcours 4.1 (`?code=rapport-exemple`) :

```json
{"modele": {"code": "rapport-exemple", "version": 1, "variables": ["org_name", "…"],
            "par_clause": {"immatriculation": ["org_name", "org_orias"], "mediation": ["org_mediator_name"]},
            "variables_inconnues": [], "blocs_ia": ["justification_recommandation"]}}
```

Une variable hors catalogue dans un modèle de base n'est pas refusée : elle est signalée dans
`variables_inconnues`.

## 4. Les parcours

Les sept parcours se suivent. Rejoués dans l'ordre avec le mini-modèle de la partie 3, ils donnent les
résultats décrits. Ils utilisent le code `rapport-exemple`, pour ne pas toucher aux modèles d'Inovéa, qui
portent des codes comme `der-conseiller` ou `rapport-conseil`. Le cabinet fictif s'appelle `CIF-0042`, le
conseiller `u-1234`. Jouez-les sur l'environnement de test : `/v1` n'a pas de route pour retirer un modèle de
base, le modèle d'exemple y restera.

### 4.1 Déposer un modèle de base

```text
ROMEO → PUT /v1/modeles-de-base/{code}    le texte du modèle, son type, son profil, son libellé
ROMEO ← 201                               version créée, mentions, variables, anomalies, impact
```

Le code du modèle est le vôtre : c'est vous qui le choisissez, dans l'adresse.

```bash
jq -Rs '{type: "rapport_conseil", profil: "conseiller", libelle: "Rapport de conseil (exemple)",
         contenu: ., format: "markdown", notes_version: "Première version",
         personnalisable: true, publier: true}' modele.md \
  | curl -sS -X PUT "$ROMEO_URL/v1/modeles-de-base/rapport-exemple" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

```json
{"code": "rapport-exemple", "type": "rapport_conseil", "profil": "conseiller", "libelle": "…",
 "personnalisable": true, "version": 1, "statut": "publiee", "inchange": false, "simulation": false,
 "contenu_markdown": "…",
 "clauses": [{"id": "immatriculation", "titre": "Immatriculation", "exigence": "identique",
              "facultative": false, "lignes": [8, 10], "variables": ["org_name", "org_orias"]},
             {"id": "mediation", "titre": "Médiation", "exigence": "sens",
              "facultative": false, "lignes": [13, 16], "variables": ["org_mediator_name"]}],
 "variables": ["org_name", "…"], "variables_inconnues": [],
 "blocs_ia": [{"cle": "justification_recommandation", "consigne": "…", "ligne": 21}],
 "notes": [{"texte": "…", "ligne": 5}], "anomalies": [],
 "impact": {"modeles_cif": 0, "restent_valides": 0, "a_corriger": []}}
```

À lire dans cette réponse :

- `201` annonce une nouvelle version. Si le contenu est celui de la dernière version, l'API répond `200` avec
  `"inchange": true` : aucune nouvelle version n'est créée. Redéposer le contenu d'une version plus ancienne
  en crée une nouvelle. Une version déposée ne change plus.
- `inchange` ne parle que du contenu. `type`, `profil` et `libelle` sont remplacés à chaque dépôt, même avec
  `"inchange": true`. `personnalisable`, s'il est omis, garde sa valeur enregistrée (`true` à la création).
- `clauses` est ce que l'API a compris de vos marques. Vérifiez que toutes vos mentions obligatoires y sont.
- `anomalies` relève les défauts de forme. Un simple `avertissement` n'empêche pas le dépôt. Une anomalie de
  gravité `erreur` le refuse : `400 gabarit_invalide`, rien n'est stocké.
- `personnalisable` dit si les cabinets ont le droit d'adapter ce modèle. `publier` décide si la version est
  publiée ou reste en brouillon (`"statut": "brouillon"`).
- Un brouillon n'a aucun effet sur les cabinets, et son `impact` n'est qu'une projection. Pour le publier,
  redéposez le même contenu avec `publier: true` : `"inchange": true`, mais `"statut": "publiee"`.
- Tant qu'un modèle de base n'a aucune version publiée, le dépôt d'un cabinet, les trois routes de
  `/v1/documents` et le contrôle qui ne précise pas `reference.version` répondent `404 introuvable`.

Les valeurs admises pour `type`, `profil` et `format` sont dans la partie 9. Pour relire ce qui est déposé :

```bash
curl -sS "$ROMEO_URL/v1/modeles-de-base" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"modeles": [{"code": "rapport-exemple", "type": "rapport_conseil", "profil": "conseiller", "libelle": "…",
  "personnalisable": true, "version": 1, "statut": "publiee", "publiee_le": "…", "nb_clauses": 2,
  "modeles_cif": {"valide": 0, "alertes": 0, "derivant": 0}}]}
```

`modeles_cif` compte les modèles de cabinet par état. `GET /v1/modeles-de-base/{code}` rend le détail de la
version publiée courante (à défaut, du dernier brouillon), et `?version=1` celui d'une version précise. Le
paramètre ne s'écrit pas sans valeur : `?version=` rend `400`. La forme est celle du dépôt, sans `impact`,
avec en plus `"versions": [{"version": 1, "statut": "publiee", "cree_le": "…", "notes_version": "…"}]`.

### 4.2 Déposer le modèle d'un cabinet et lire le contrôle

```text
ROMEO → POST /v1/controles                      facultatif : contrôle à blanc pendant la saisie
ROMEO → PUT /v1/cifs/{cif_id}/modeles/{code}    dépôt du modèle du cabinet
ROMEO ← statut du modèle et contrôle complet    dans la même réponse
ROMEO   montre le rapport au cabinet et décide de bloquer ou non
```

Le cabinet part du modèle de base et l'adapte. Ici, il touche à la mention de médiation :

```bash
sed 's/saisir gratuitement le médiateur/saisir le médiateur, à vos frais/' modele.md > modele-cabinet.md
```

Pendant la saisie, ROMEO peut demander un contrôle à blanc. Le modèle du cabinet ne change pas d'état :

```bash
jq -Rs '{reference: {code: "rapport-exemple"}, contenu: ., format: "markdown", mode: "modele"}' \
    modele-cabinet.md \
  | curl -sS -X POST "$ROMEO_URL/v1/controles" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

La réponse est un contrôle, avec `"id": null` et `"enregistre": false`. Le dépôt, lui, enregistre le modèle
et son contrôle. `cif_id` est votre identifiant du cabinet. Aucun appel préalable ne crée le cabinet : le
premier dépôt suffit.

```bash
jq -Rs '{contenu: ., format: "markdown"}' modele-cabinet.md \
  | curl -sS -X PUT "$ROMEO_URL/v1/cifs/CIF-0042/modeles/rapport-exemple" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

```json
{"cif_id": "CIF-0042", "code": "rapport-exemple", "version": 1, "statut": "alertes", "inchange": false,
 "base": {"version_controlee": 1, "version_validee": null, "version_courante": 1},
 "controle": {
   "id": "ctl_…", "enregistre": true, "cree_le": "…", "statut": "alertes",
   "resume": {"bloquantes": 1, "informations": 0, "clauses_attendues": 2, "clauses_intactes": 1,
              "clauses_reformulees": 0},
   "alertes": [{"code": "clause_alteree", "gravite": "bloquante", "niveau": "paragraphe",
     "message": "La mention « Médiation » a été modifiée et son sens n'est plus celui du modèle de base.",
     "clause_id": "mediation", "titre": "Médiation",
     "attendu": {"texte": "… vous pouvez\nsaisir gratuitement le médiateur : …", "lignes": [14, 15]},
     "constate": {"texte": "… vous pouvez\nsaisir le médiateur, à vos frais : …", "lignes": [14, 15]},
     "diff": [{"op": "egal", "texte": "… vous pouvez\nsaisir"}, {"op": "supprime", "texte": " gratuitement"},
              {"op": "egal", "texte": " le médiateur"}, {"op": "ajoute", "texte": ", à vos frais"},
              {"op": "egal", "texte": " : {{ org_mediator_name }}."}],
     "variable": null,
     "jugement": {"verdict": "sens_altere", "justification": "La saisine du médiateur n'est plus gratuite.",
                  "modele_ia": "claude-opus-5-5", "depuis_cache": true}}],
   "rapport_markdown": "…", "rapport_html": "…", "modele_annote": "…"}}
```

Cet exemple montre une IA branchée. Sur un service lancé en local, l'IA est simulée (`ia.mode` vaut
`factice`, partie 2) : vous lirez `"modele_ia": "factice"` et une justification convenue.

À lire dans cette réponse :

- `statut` est l'état du modèle du cabinet : `valide` sans alerte bloquante, `alertes` sinon. C'est sur lui
  que ROMEO décide.
- `base` porte partout trois numéros de version du modèle de base : `version_controlee`, celle du dernier
  contrôle ; `version_validee`, la dernière sur laquelle le modèle du cabinet a été valide (`null` s'il ne
  l'a jamais été) ; `version_courante`, la version publiée.
- `controle` dit pourquoi : le rapport prêt à afficher au cabinet (`rapport_html`) et, pour chaque alerte, ce
  qui était attendu, ce qui est constaté, la différence mot à mot et l'avis du juge. La partie 5 détaille ces
  champs. `depuis_cache` vaut ici `true` : le contrôle à blanc qui précède a déjà fait juger ce passage.

Si le modèle de base n'est pas personnalisable, le dépôt est refusé : `409 modele_non_personnalisable`. Si
un modèle de base passe à `personnalisable: false` après coup, la génération utilise le modèle de base pour
tous les cabinets, même ceux dont le modèle était valide, et le dit (`"repli_sur_base": true`). Leur modèle
est gardé : il resservira si le modèle de base redevient personnalisable.

Le cabinet corrige et redépose. Cette fois, il n'adapte que la zone libre :

```bash
sed 's/depuis 2009/depuis plus de quinze ans/' modele.md > modele-cabinet.md
```

Rejouez le `PUT` ci-dessus. Le modèle passe à `"statut": "valide"`. Le contrôle garde une alerte
`zone_libre_modifiee`, de gravité `information` : elle renseigne, elle ne bloque pas.

Pour relire l'état de tous les modèles d'un cabinet :

```bash
curl -sS "$ROMEO_URL/v1/cifs/CIF-0042/modeles" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"cif_id": "CIF-0042", "modeles": [{"code": "rapport-exemple", "type": "rapport_conseil", "libelle": "…",
  "utilise": "modele_cif", "suspendu": false, "statut": "valide", "version": 2,
  "base": {"version_controlee": 1, "version_validee": 1, "version_courante": 1},
  "dernier_controle": {"id": "ctl_…", "statut": "valide",
                       "resume": {"bloquantes": 0, "informations": 1, "…": "…"}, "cree_le": "…"},
  "maj_le": "…"}]}
```

Tous les modèles de base publiés figurent dans cette liste ; un modèle resté en brouillon n'y est pas.
`utilise` dit lequel servirait à générer : `modele_cif` quand le modèle du cabinet est valide,
`modele_de_base` quand le cabinet n'a rien déposé (`statut` vaut alors `null`), que son modèle n'est pas
valide, ou que le modèle de base n'est plus personnalisable : son modèle est suspendu (`"suspendu": true`).

Trois autres routes complètent le parcours.

- `GET /v1/cifs/{cif_id}/modeles/{code}` rend le détail d'un modèle et son dernier contrôle : la forme de la
  réponse du dépôt, plus `contenu_markdown`, `suspendu` et `maj_le`.
- `GET /v1/controles/{id}` relit un contrôle enregistré.
- `DELETE /v1/cifs/{cif_id}/modeles/{code}` fait revenir le cabinet au modèle de base et répond
  `{"cif_id": "CIF-0042", "code": "rapport-exemple", "supprime": true}`. Le modèle du cabinet, ses versions
  et ses contrôles sont supprimés : `GET /v1/controles/{id}` répond ensuite `404`. Ne le jouez pas
  maintenant : la suite se sert du modèle déposé.

### 4.3 Mettre à jour un modèle de base

```text
1. PUT /v1/modeles-de-base/{code}, simulation: true    anomalies et impact, rien n'est stocké
2. PUT /v1/modeles-de-base/{code}, publier: true       nouvelle version ; les modèles de cabinet valides
                                                       passent à « derivant » et sont recontrôlés
3. GET /v1/cifs/{cif_id}/modeles/{code}                le contrôle joint dit ce qu'il reste à corriger
4. POST …/revalider                                    200 : retour à « valide » ; 409 : il reste à corriger
```

Inovéa raccourcit le délai de la mention de médiation :

```bash
sed 's/sous deux mois/sous un mois/' modele.md > modele-v2.md
```

**Simuler d'abord.** Le même `PUT`, avec `simulation: true`, analyse le texte et mesure l'effet sur les
modèles des cabinets. Rien n'est stocké.

```bash
jq -Rs '{type: "rapport_conseil", profil: "conseiller", libelle: "Rapport de conseil (exemple)",
         contenu: ., format: "markdown", notes_version: "Délai de médiation ramené à un mois",
         personnalisable: true, publier: true, simulation: true}' modele-v2.md \
  | curl -sS -X PUT "$ROMEO_URL/v1/modeles-de-base/rapport-exemple" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

La réponse a la même forme qu'au dépôt, avec `"simulation": true`. Le champ à regarder est `impact` :

```json
{"impact": {"modeles_cif": 1, "restent_valides": 0,
            "a_corriger": [{"cif_id": "CIF-0042", "bloquantes": 1, "controle_id": null}]}}
```

Un modèle de cabinet est concerné, et il ne resterait pas valide : sa mention de médiation annonce encore
deux mois. Vous savez qui prévenir avant de publier. En simulation, `controle_id` vaut `null` : aucun
contrôle n'est enregistré. Après la publication, il désigne le contrôle stocké.

**Publier ensuite.** Rejouez la commande en passant `simulation` à `false`. L'API crée la version 2. Les
modèles de cabinet qui étaient valides passent à `derivant` et sont recontrôlés aussitôt contre la nouvelle
version. Avec `"revalidation_automatique": true` dans la requête, ceux qui n'ont aucune alerte bloquante
redeviennent `valide` sans autre appel.

**Lire la dérive.** Le détail du modèle du cabinet porte ce nouveau contrôle, qui dit ce qu'il reste à
corriger :

```bash
curl -sS "$ROMEO_URL/v1/cifs/CIF-0042/modeles/rapport-exemple" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"cif_id": "CIF-0042", "code": "rapport-exemple", "version": 2, "statut": "derivant", "…": "…",
 "controle": {"id": "ctl_…", "statut": "alertes", "resume": {"bloquantes": 1, "…": "…"}, "…": "…"},
 "contenu_markdown": "…", "maj_le": "…"}
```

Dans la liste des modèles du cabinet (`GET /v1/cifs/CIF-0042/modeles`), `base` montre l'écart,
`{"version_controlee": 2, "version_validee": 1, "version_courante": 2}`, et `utilise` vaut `modele_de_base`.

**Lever la dérive.** ROMEO demande la revalidation :

```bash
curl -sS -X POST "$ROMEO_URL/v1/cifs/CIF-0042/modeles/rapport-exemple/revalider" \
  -H "Authorization: Bearer $ROMEO_CLE"
```

Si le contrôle joint est sans alerte bloquante, l'API répond `200` avec la forme du dépôt, et le modèle
redevient `valide`. Ici, la mention de médiation du cabinet annonce encore deux mois : l'API répond `409`.

```json
{"erreur": {"code": "revalidation_impossible", "message": "…",
            "details": [{"controle_id": "ctl_…", "bloquantes": 1}]}}
```

Le cabinet doit reprendre son modèle et le redéposer. Le parcours 4.4 montre ce qui se passe en attendant,
puis lève la dérive.

### 4.4 Générer un document simple

```text
ROMEO → POST /v1/documents/verifier-donnees    facultatif : ce qui manque, sans générer
ROMEO → POST /v1/documents                     code du modèle, cabinet, conseiller, données
ROMEO ← markdown, html, modèle utilisé, conformité, durée
```

Les données du dossier, fictives, dans un fichier `donnees.json` :

```json
{"org_name": "Cabinet Dupont Patrimoine", "org_orias": "00000000", "org_status_cif": true,
 "org_mediator_name": "Médiateur fictif du patrimoine", "client_first_name": "Camille"}
```

**Vérifier les données.** Cet appel fait une génération à blanc, sans IA, et dit ce qui manque. Son corps
ne porte que `code`, `cif_id` et `donnees` : il n'accepte pas `options`. Essayez-le en retirant une donnée :

```bash
jq 'del(.org_orias) | {code: "rapport-exemple", cif_id: "CIF-0042", donnees: .}' donnees.json \
  | curl -sS -X POST "$ROMEO_URL/v1/documents/verifier-donnees" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

```json
{"complet": false,
 "manquantes": [{"cle": "org_orias", "libelle": "…", "dans_clauses": ["immatriculation"], "bloquante": true}],
 "alertes": [{"code": "variable_non_resolue", "gravite": "bloquante", "niveau": "variable", "message": "…",
              "clause_id": "immatriculation", "variable": "org_orias", "…": "…"}],
 "conditions_non_renseignees": [], "inconnues": []}
```

`bloquante` vaut vrai parce que la donnée manque dans une mention obligatoire. `libelle` vaut `null` pour une
variable hors catalogue. `conditions_non_renseignees` liste les drapeaux de condition absents, `inconnues`
les clés envoyées que ni le catalogue ni le modèle ne connaissent. `alertes` a la forme des alertes de la
génération : c'est ce qu'elle relèverait avec ces données, sans dépendre de l'IA (`clause_vide_au_rendu`,
par exemple, pour une mention que les données laissent vide).

`complet: true` veut dire : aucune variable ne manque, et aucune de ces alertes n'est bloquante. Cela ne
garantit pas un document `conforme` : les commentaires, rédigés ou fournis, ne sont jugés qu'à la
génération.

**Générer.** Le modèle porte un bloc IA. Pour un rendu immédiat, on demande ici de ne pas le rédiger
(`blocs_ia: false`) ; le parcours 4.5 s'en occupe.

```bash
jq '{code: "rapport-exemple", cif_id: "CIF-0042", conseiller_id: "u-1234", donnees: .,
     options: {formats: ["markdown", "html"], blocs_ia: false, reference_romeo: "DOC-1"}}' donnees.json \
  | curl -sS -X POST "$ROMEO_URL/v1/documents" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @- > reponse.json

jq -r .markdown reponse.json > document.md
jq -r .html reponse.json > document.html
jq 'del(.markdown, .html)' reponse.json
```

```json
{"id": "doc_…", "code": "rapport-exemple", "markdown": "…", "html": "…",
 "modele": {"origine": "modele_de_base", "version_base": 2, "version_cif": null,
            "etat_modele_cif": "derivant", "repli_sur_base": true},
 "conformite": {"statut": "conforme",
                "alertes": [{"code": "bloc_ia_indisponible", "gravite": "information", "…": "…"}],
                "variables_non_resolues": [], "conditions_non_renseignees": [],
                "clauses_presentes": ["immatriculation", "mediation"], "clauses_non_applicables": []},
 "blocs_ia": [{"cle": "justification_recommandation", "statut": "indisponible", "…": "…"}],
 "produits_enrichis": [], "duree_ms": 2140}
```

À lire dans cette réponse :

- `modele` dit quel modèle a servi, et pourquoi. Ici le modèle du cabinet dérive depuis le parcours 4.3. Seul
  un modèle `valide` sert à générer : l'API s'est repliée sur le modèle de base (`repli_sur_base`) et le dit.
- `conformite.statut` vaut `conforme` s'il n'y a aucune alerte bloquante. Chaque mention obligatoire est soit
  présente, soit non applicable parce qu'une condition l'écarte.
- Une donnée absente laisse dans le document le marqueur `[À compléter : nom]` et figure dans
  `variables_non_resolues`. Dans une mention obligatoire, c'est une alerte bloquante.
- Le commentaire non rédigé laisse `[Bloc à rédiger : justification_recommandation]` et une alerte
  d'information.
- `html` est le format d'affichage. Markdown et HTML gardent les marques de clause, invisibles, pour un
  contrôle ultérieur. En HTML, les marqueurs `[À compléter : …]` et `[Bloc à rédiger : …]` sont entourés de
  `<mark class="a-completer">`.

Les options de la requête, toutes facultatives :

| Option | Effet |
|---|---|
| `formats` | Formats rendus : `markdown`, `html`. Par défaut, `["markdown"]` seul : demandez `html` pour l'affichage. Un format non demandé revient à `null` dans la réponse, il n'en est pas absent. |
| `si_modele_non_valide` | Que faire si le modèle du cabinet n'est pas valide. `modele_de_base`, la valeur par défaut : générer avec le modèle de base et le dire. `generer` : générer avec le modèle du cabinet malgré ses alertes. `refuser` : répondre `422 modele_non_valide`. |
| `conserver_marques` | Garder les marques de clause dans le document, pour pouvoir le recontrôler. Vrai par défaut. |
| `blocs_ia` | Rédiger par l'IA les commentaires que ROMEO ne fournit pas. Vrai par défaut ; à faux, ils restent à rédiger. |
| `verifier_blocs` | Soumettre au juge les commentaires fournis par ROMEO (parcours 4.5). Faux par défaut. |
| `enrichir_produits` | Compléter, pour chaque produit qui porte un `isin`, les données absentes à partir du référentiel. Une valeur envoyée par ROMEO n'est jamais écrasée. Les ISIN complétés sont listés dans `produits_enrichis`. Vrai par défaut. |
| `reference_romeo` | Votre référence du document. Elle est reprise dans le journal. |

`cif_id` est facultatif : un consultant n'a pas de modèle propre, le modèle de base sert alors directement.

**Lever la dérive, pour finir.** Le cabinet repart de la nouvelle version du modèle de base et y reporte son
adaptation :

```bash
sed 's/depuis 2009/depuis plus de quinze ans/' modele-v2.md > modele-cabinet.md
```

Rejouez le `PUT` du parcours 4.2. Le modèle du cabinet redevient `valide`, contrôlé contre la version 2, et
c'est lui qui sert aux générations suivantes.

### 4.5 L'aller-retour du rapport de conseil

Un rapport de conseil contient un commentaire libre. Le conseiller doit pouvoir le relire avant qu'il
n'entre dans le document. D'où un aller-retour en deux appels :

```text
ROMEO                                      API Figen
  1. POST /v1/documents/blocs         →    rédige les commentaires (l'appel qui attend l'IA)
                                      ←    blocs : cle, libelle, statut, texte, modele_ia
  2. le conseiller relit et corrige les textes dans ROMEO
  3. POST /v1/documents, avec blocs   →    insère les textes validés, rend le document
                                      ←    markdown, html, conformité (sans appel à l'IA, donc vite)
```

**Rédiger les commentaires.** Le corps est celui de `POST /v1/documents`, et vous pouvez l'envoyer tel quel :
`blocs`, `formats` et les autres options du rendu y sont acceptés, puis ignorés. Toute autre clé est refusée
(`400 requete_invalide`). Rien n'est stocké.

```bash
jq '{code: "rapport-exemple", cif_id: "CIF-0042", conseiller_id: "u-1234", donnees: .}' donnees.json \
  | curl -sS -X POST "$ROMEO_URL/v1/documents/blocs" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

```json
{"code": "rapport-exemple",
 "blocs": [{"cle": "justification_recommandation", "libelle": "…", "statut": "redige",
            "texte": "…", "modele_ia": "claude-opus-5-5"}],
 "duree_ms": 8200}
```

Le rédacteur ne s'appuie que sur les données envoyées et n'invente aucun chiffre. ROMEO affiche chaque
`texte` au conseiller, qui le corrige.

**Générer le document final.** ROMEO renvoie les textes validés dans `blocs`, clé par clé :

```bash
jq --arg texte "Texte relu et validé par le conseiller." \
   '{code: "rapport-exemple", cif_id: "CIF-0042", conseiller_id: "u-1234", donnees: .,
     blocs: {justification_recommandation: $texte},
     options: {formats: ["markdown", "html"], reference_romeo: "DOC-2"}}' donnees.json \
  | curl -sS -X POST "$ROMEO_URL/v1/documents" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

Dans la réponse, chaque commentaire de `blocs_ia` porte son `texte` et un statut. Vérifiez que chaque texte
envoyé revient avec `"statut": "fourni"` : c'est la preuve qu'il est dans le document.

```json
{"blocs_ia": [{"cle": "justification_recommandation", "statut": "fourni",
               "texte": "Texte relu et validé par le conseiller.", "…": "…"}], "…": "…"}
```

| Statut | Sens |
|---|---|
| `redige` | Rédigé par l'IA. |
| `fourni` | Texte envoyé par ROMEO, inséré dans le document. |
| `indisponible` | Non rédigé : le document porte `[Bloc à rédiger : cle]` et l'alerte `bloc_ia_indisponible`. |

Un commentaire que ROMEO ne fournit pas est rédigé par l'IA si `blocs_ia` est vrai, sinon laissé à rédiger.
Un texte fourni vide n'est jamais rédigé à sa place par l'IA : le commentaire sort `indisponible`, avec son
marqueur « à rédiger ». Une clé de `blocs` qui ne correspond à aucun commentaire du document lève l'alerte
d'information `bloc_fourni_inconnu` : ce texte n'est pas dans le document.

Le texte fourni est lu comme du Markdown : paragraphes, listes, gras et liens sont rendus. Un titre devient
un paragraphe en gras, une image est remplacée par son texte, le HTML s'affiche comme du texte, et aucune
marque ne peut y être posée. Dans le document, chaque commentaire est encadré par `<!-- ia-debut: cle -->`
et `<!-- ia-fin: cle -->` ; en HTML, il est en plus dans un `<div class="bloc-ia" data-cle="cle">`. Avec
`conserver_marques: false`, ces repères disparaissent.

Avec `"verifier_blocs": true`, l'API soumet les textes fournis au juge, qui cherche une contradiction avec
une mention obligatoire ou une promesse interdite : alerte `ajout_contradictoire`, ou `ajout_a_juger` si le
juge n'a pas pu se prononcer. Les deux sont bloquantes.

**Régler le délai d'attente.** Un relais (Cloudflare) placé devant l'environnement de test coupe tout appel
à 100 secondes. Réglez votre client sur 95 secondes au plus. Une coupure, délai dépassé ou code `524`, n'est
pas un refus de l'API : `POST /v1/documents/blocs` ne stocke rien, vous pouvez le relancer. Les commentaires
d'un même document sont rédigés de front : la durée est celle du plus long, pas leur somme.

### 4.6 Contrôler un document retouché avant signature

Un conseiller retouche parfois le document final. Avant la signature, ROMEO peut vérifier que les mentions
obligatoires ont tenu.

```text
ROMEO → POST /v1/controles, mode « document »    le modèle et les données d'origine, le texte retouché
ROMEO ← un contrôle                              mêmes alertes que pour un modèle
```

Reprenons le document du parcours 4.4 et retirons un mot de la mention de médiation :

```bash
sed 's/saisir gratuitement/saisir/' document.md > document-retouche.md
```

Figen n'a pas gardé le document d'origine. La référence se reconstruit donc à partir du modèle et des
données : avec `reference.donnees`, l'API rend d'abord le modèle avec ces données, puis compare.

```bash
jq -n --slurpfile donnees donnees.json --rawfile retouche document-retouche.md \
   '{reference: {code: "rapport-exemple", donnees: $donnees[0]},
     contenu: $retouche, format: "markdown", mode: "document"}' \
  | curl -sS -X POST "$ROMEO_URL/v1/controles" \
      -H "Authorization: Bearer $ROMEO_CLE" -H "Content-Type: application/json" \
      --data-binary @-
```

La réponse est un contrôle, comme au parcours 4.2 : une alerte sur `mediation`, son diff, l'avis du juge. Si
le document a été retouché en HTML, envoyez-le avec `"format": "html"`.

`reference` n'accepte pas `cif_id` : un document généré avec le modèle d'un cabinet est comparé au modèle de
base. Les zones libres adaptées par le cabinet et le commentaire inséré ressortent alors comme des passages
modifiés, en simple information (`zone_libre_modifiee`) quand le juge les tient pour neutres.

ROMEO peut aussi fournir lui-même le document d'origine, s'il l'a gardé. La référence devient alors
`{"contenu": "…", "format": "markdown"}`. `type_document` est facultatif ; avec `reference.code`, il vaut
par défaut le type du modèle de base.

### 4.7 Suivre ce qui change

L'API ne rappelle pas ROMEO. C'est ROMEO qui vient lire le fil des changements d'état, à son rythme.

```text
ROMEO → GET /v1/evenements                 premier appel
ROMEO ← evenements, suivant                ROMEO garde la valeur de « suivant »
ROMEO → GET /v1/evenements?depuis=…        appels suivants : seulement ce qui est nouveau
```

```bash
curl -sS "$ROMEO_URL/v1/evenements?limite=1" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"evenements": [{"id": 1, "cree_le": "…", "type": "base_publiee", "code": "rapport-exemple", "cif_id": null,
                 "details": {"version": 1, "revalidation_automatique": false}}],
 "suivant": "1"}
```

Les événements viennent du plus ancien au plus récent, `limite` par appel : 100 par défaut, 1000 au plus.
Vos numéros différeront si d'autres modèles ont déjà été déposés. Gardez `suivant` et passez-le dans `depuis`
à l'appel suivant. Quand il n'y a rien de nouveau, la liste est vide et `suivant` ne change pas.

```bash
SUIVANT=1   # la valeur de « suivant » que vous venez de recevoir
curl -sS "$ROMEO_URL/v1/evenements?depuis=$SUIVANT" -H "Authorization: Bearer $ROMEO_CLE"
```

| Type d'événement | Ce qui s'est passé |
|---|---|
| `base_publiee` | Une version d'un modèle de base a été publiée. |
| `modele_cif_valide` | Le modèle d'un cabinet est valide. |
| `modele_cif_alertes` | Le modèle d'un cabinet porte au moins une alerte bloquante. |
| `modele_cif_derivant` | Le modèle d'un cabinet dérive : son modèle de base a changé. |
| `modele_cif_revalide` | La dérive d'un modèle de cabinet a été levée. |
| `modele_cif_supprime` | Un cabinet est revenu au modèle de base. |

`details` précise l'événement : `version` et `revalidation_automatique` pour `base_publiee` ; `controle_id`,
`bloquantes` et `version_base` pour un modèle de cabinet, plus sa `version` après un dépôt ou un appel à
`revalider` ; `version` seule pour `modele_cif_supprime`. Après une publication avec
`revalidation_automatique`, un modèle resté sans alerte bloquante donne directement `modele_cif_revalide`,
avec `"automatique": true`, sans `modele_cif_derivant` avant.

Le journal des générations a ses filtres, tous facultatifs : `cif_id`, `conseiller_id`, `depuis` et `limite`
(100 par défaut, 1000 au plus). Ici, `depuis` est une date ou une date et heure ISO 8601 (`2026-11-10`,
`2026-11-10T09:00:00Z`), pas un curseur, et les lignes viennent de la plus récente à la plus ancienne. Le
journal ne contient aucun document, aucune donnée de client :

```bash
curl -sS "$ROMEO_URL/v1/journal?cif_id=CIF-0042&limite=20" -H "Authorization: Bearer $ROMEO_CLE"
```

```json
{"generations": [{"id": "doc_…", "cree_le": "…", "code": "rapport-exemple", "cif_id": "CIF-0042",
                  "conseiller_id": "u-1234", "modele": {}, "statut": "conforme", "nb_alertes": 0,
                  "reference_romeo": "DOC-2", "duree_ms": 2140}]}
```

## 5. Lire un contrôle

### Les états

Un contrôle est `valide` s'il n'a aucune alerte bloquante, `alertes` sinon. Un document généré suit la même
règle : `conforme` ou `alertes`. Le modèle d'un cabinet connaît trois états :

| État | Sens |
|---|---|
| `valide` | Contrôlé sans alerte bloquante contre la version courante du modèle de base. |
| `alertes` | Au moins une alerte bloquante. |
| `derivant` | Il était valide, le modèle de base a été republié. Il a été recontrôlé contre la nouvelle version (le contrôle joint dit ce qu'il reste à corriger) et attend une revalidation. |

Seul un modèle `valide` sert à générer ; sinon l'option `si_modele_non_valide` décide (parcours 4.4). Un
modèle valide peut porter des alertes d'information : elles disent ce que le cabinet a changé sans que cela
pose de problème.

### Les codes d'alerte

Une alerte est `bloquante` ou vaut `information`. Seules les bloquantes font passer un modèle à `alertes`.
Son `niveau` dit sur quoi elle porte : `clause`, `paragraphe`, `variable`, `zone_libre` ou `document`.
Voici les codes à ce jour ; d'autres pourront s'ajouter (partie 8). Au contrôle d'un modèle, ou d'un document
retouché :

| Code | Gravité | Niveau | Libellé | Quoi faire |
|---|---|---|---|---|
| `clause_supprimee` | bloquante | clause | Mention obligatoire supprimée | Remettre la mention telle qu'elle figure dans le modèle de base. |
| `clause_alteree` | bloquante | paragraphe | Mention obligatoire dont le sens a changé | Reprendre la rédaction du modèle de base, ou reformuler sans en changer la portée. |
| `clause_non_identique` | bloquante | paragraphe | Mention à reprendre mot pour mot, modifiée | Rétablir le texte exact du modèle de base : cette mention ne se reformule pas. |
| `clause_a_juger` | bloquante | paragraphe | Mention modifiée, jugement indisponible | Relancer le contrôle plus tard : le juge n'a pas pu se prononcer et, dans le doute, le modèle reste bloqué. |
| `variable_retiree` | bloquante | variable | Variable retirée d'une mention obligatoire | Remettre la variable dans la mention : sans elle, l'information ne figurerait plus dans les documents. |
| `condition_modifiee` | bloquante | clause | Condition modifiée dans ou autour d'une mention obligatoire | Rétablir les conditions et les boucles du modèle de base, dans la mention comme autour d'elle : entourée d'une condition fausse, une mention disparaîtrait du document. |
| `jinja_invalide` | bloquante | document | Syntaxe du modèle invalide : aucun document ne peut être généré | Corriger la syntaxe à la ligne indiquée : tant qu'elle est fausse, aucun document ne peut être généré avec ce modèle. Un commentaire jamais fermé lève aussi cette alerte : une marque à laquelle il manque `-->`, par exemple. |
| `ajout_contradictoire` | bloquante | zone_libre | Ajout qui contredit une mention obligatoire | Retirer ou corriger l'ajout. |
| `ajout_a_juger` | bloquante | zone_libre | Passage ajouté ou modifié, jugement indisponible | Relancer le contrôle plus tard : tant que le passage n'a pas pu être jugé, le modèle reste bloqué. |
| `jinja_ajoute` | bloquante | clause ou document | Variable, condition ou instruction ajoutée par le cabinet | Retirer ce qui a été ajouté : une mention n'admet que les variables du modèle de base, et seules des conditions ou des boucles peuvent être ajoutées ailleurs. |
| `mise_en_forme_masquante` | bloquante | clause | Mise en forme qui masque ou modifie une mention | Retirer de la mention le texte barré, le HTML, l'image, le code ou les caractères invisibles : elle doit s'afficher telle qu'elle est écrite. |
| `rendu_divergent` | bloquante | clause ou document | Mention qui ne s'afficherait pas comme elle a été contrôlée | Repartir du modèle de base autour de cette mention : un élément placé hors d'elle en modifie ou en empêche l'affichage. |
| `bloc_ia_modifie` | bloquante | document | Commentaire à rédiger ajouté, retiré ou modifié | Rétablir les commentaires à rédiger et leurs consignes tels qu'ils sont dans le modèle de base. |
| `clause_reformulee` | information | paragraphe | Mention reformulée, sens conservé | Rien à faire : la reformulation est acceptée. |
| `clause_facultative_retiree` | information | clause | Mention facultative retirée | Rien à faire : le cabinet peut retirer cette mention quand elle ne le concerne pas. |
| `marque_absente` | information | clause | Marque de clause absente, texte retrouvé | Rien d'obligatoire ; remettre les marques facilite les contrôles suivants. |
| `clause_deplacee` | information | clause | Mention déplacée | Rien à faire ; vérifier que l'ordre reste logique à la lecture. |
| `clause_inconnue` | information | clause | Clause inconnue du modèle de base | Vérifier l'identifiant : il n'existe pas dans le modèle de base. |
| `zone_libre_modifiee` | information | zone_libre | Zone libre modifiée | Rien à faire : le cabinet est libre d'adapter ces passages. |
| `marques_abimees` | information | document | Marques abîmées | Corriger les marques signalées. Le contrôle a été mené malgré tout. Si une marque n'est plus fermée par `-->` (`<!-- /clause` seul, par exemple), s'y ajoute `jinja_invalide`, bloquante. |

À la génération d'un document :

| Code | Gravité | Niveau | Libellé | Quoi faire |
|---|---|---|---|---|
| `variable_non_resolue` | bloquante dans une clause, information ailleurs | variable | Donnée manquante à la génération | Compléter la donnée et régénérer ; bloquante si elle se trouve dans une mention obligatoire. |
| `clause_vide_au_rendu` | bloquante | clause | Mention obligatoire vide après rendu | Envoyer les données qui alimentent cette mention : une condition l'a vidée, elle sortirait vide dans le document. |
| `condition_non_renseignee` | information | variable | Condition non renseignée, tenue pour fausse | Envoyer la donnée de condition, vraie ou fausse, pour lever le doute. |
| `bloc_ia_indisponible` | information | document | Commentaire non rédigé | Rédiger le commentaire à la main, ou relancer la rédaction. |
| `bloc_fourni_inconnu` | information | document | Texte fourni pour un commentaire que le document ne porte pas | Vérifier la clé du commentaire : le texte n'a pas été inséré dans le document. |

### Ce que contient un contrôle

| Champ | Contenu |
|---|---|
| `statut` | `valide` ou `alertes`. |
| `resume` | Le nombre d'alertes `bloquantes` et d'`informations`, et le compte des mentions : `clauses_attendues`, `clauses_intactes`, `clauses_reformulees`. |
| `alertes` | La liste des alertes, détaillée ci-dessous. |
| `rapport_markdown`, `rapport_html` | Le rapport en français, lisible tel quel par un conseiller : une ligne de synthèse, une section par alerte bloquante, puis les informations. Dans le Markdown, le diff s'écrit `~~supprimé~~` et `**ajouté**`. |
| `modele_annote` | Le texte contrôlé, où chaque passage en cause est encadré par une marque d'alerte. |

La marque d'alerte du modèle annoté s'écrit `<!-- alerte: code | clause: id -->…<!-- /alerte -->`.

Chaque alerte porte :

| Champ | Contenu |
|---|---|
| `code`, `gravite`, `niveau` | Voir les tableaux ci-dessus. |
| `message` | La phrase à montrer telle quelle. |
| `clause_id`, `titre` | La mention concernée et son libellé, s'il y en a un. |
| `attendu`, `constate` | Le texte du modèle de base et celui du texte contrôlé, avec leurs lignes. |
| `diff` | La différence mot à mot : une suite d'opérations `egal`, `supprime`, `ajoute`. |
| `variable` | La variable en cause, pour les alertes de niveau `variable`. |
| `jugement` | L'avis du juge, quand il a été consulté : `verdict`, `justification`, `modele_ia`, `depuis_cache`. |

### Le juge : son rôle, ses limites

Le contrôle commence toujours par une comparaison mot à mot, sans IA. Le juge n'intervient qu'ensuite, et
seulement sur les passages modifiés. Une mention intacte ne lui est jamais soumise.

| Passage modifié | Verdict | Alerte |
|---|---|---|
| Mention d'exigence `sens` | `sens_conserve` | `clause_reformulee`, information |
| Mention d'exigence `sens` | `sens_altere` | `clause_alteree`, bloquante |
| Mention d'exigence `sens` | `indisponible` | `clause_a_juger`, bloquante |
| Zone libre ajoutée ou modifiée | `neutre` | `zone_libre_modifiee`, information |
| Zone libre ajoutée ou modifiée | `contradictoire` | `ajout_contradictoire`, bloquante |
| Zone libre ajoutée ou modifiée | `indisponible` | `ajout_a_juger`, bloquante |

Certains écarts ne se discutent pas, et le juge n'est pas consulté : une mention supprimée, une mention
d'exigence `identique` modifiée, une variable retirée d'une mention, une condition modifiée. L'alerte est
bloquante d'office.

### Ce que le contrôle garantit sans l'IA

Un modèle n'est jamais `valide` parce que le juge a été convaincu, absent ou contourné. Ces règles sont
appliquées par le code, avant tout jugement, et bloquent d'office :

- **Ce qui est en commentaire est absent.** Un texte de mention mis en commentaire est une mention
  supprimée, et ce que le contrôle tient pour absent n'est jamais dans le document généré.
- **Une mention doit s'afficher comme elle a été contrôlée** (`rendu_divergent`). Le contrôle rend le
  modèle du cabinet avec des données d'épreuve et compare, mention par mention, le texte qui
  s'afficherait à celui qu'il a examiné. Un élément placé hors de la mention qui la cache, la remplace
  ou en change une valeur est ainsi repéré, quelle que soit l'astuce.
- **Le cabinet n'ajoute pas d'instructions au modèle** (`jinja_ajoute`). Dans une mention, seules les
  variables et conditions du modèle de base sont admises. Ailleurs, le cabinet peut ajouter des
  conditions et des boucles, rien d'autre : une instruction qui redéfinirait une variable changerait le
  contenu d'une mention sans y toucher.
- **Pas de mise en forme qui masque** (`mise_en_forme_masquante`) : texte barré, HTML brut, image, code
  ou caractères invisibles dans une mention ; et, partout dans le modèle, HTML brut, bloc de code ou
  caractères invisibles que le modèle de base ne porte pas. Dans un modèle ou un document envoyé en HTML,
  ce que l'éditeur a rendu invisible (attribut `hidden`, `display:none`…) est traité de même.
- **Les commentaires à rédiger sont ceux du modèle de base** (`bloc_ia_modifie`) : un cabinet ne peut ni
  en ajouter, ni en retirer, ni en changer la consigne.
- **Un passage non jugé bloque** (`clause_a_juger`, `ajout_a_juger`) : si le juge ne peut pas se prononcer
  sur une mention modifiée ou sur un passage ajouté, le modèle n'est pas validé.

Ces garanties portent sur ce que l'API rend. Pour qu'elles vaillent jusqu'à la signature, affichez et
faites signer le HTML que l'API fournit (`html`) : c'est lui qui fait référence. Un document retouché se
recontrôle avant signature (parcours 4.6). L'API ne lit pas vos feuilles de style : une mention cachée par
une classe CSS de votre côté lui échappe.

Les champs Markdown rendus par l'API (`markdown`, `modele_annote`, `contenu_markdown`) reprennent des
textes écrits par des cabinets ou venus de vos données. Affichez-les avec un rendu qui n'interprète pas le
HTML brut, ou utilisez le HTML que l'API fournit (`html`, `rapport_html`), déjà neutralisé.

Ce que vous pouvez attendre d'un jugement :

- **Il est justifié.** Chaque verdict vient avec une justification d'une ou deux phrases et le nom du modèle
  d'IA qui l'a rendu (`modele_ia`).
- **Il est mémorisé.** Une même paire de textes n'est jugée qu'une fois. Si elle revient, l'avis sort de la
  mémoire et la réponse le dit : `"depuis_cache": true`.
- **Il est rejouable.** Le même modèle redéposé reçoit le même avis. Deux cabinets qui font la même
  modification reçoivent la même réponse.
- **Dans le doute, il bloque.** Si le juge ne peut pas se prononcer sur une mention modifiée ou sur un
  passage ajouté, le modèle n'est pas validé. Un avis indisponible n'est pas mémorisé : relancer le contrôle
  suffit.

Ses limites, pour être clairs. Le juge donne un avis sur le sens d'un texte. Il peut se tromper : sa
justification est là pour qu'un humain puisse le vérifier. Et il ne décide de rien. Il alerte, ROMEO décide.

## 6. Ce que nous proposons de plus simple, et pourquoi

Sur huit points, l'API fait un choix plus simple que ce qu'on attend souvent d'un service de ce genre. Pour
chacun : l'idée de départ, notre proposition, et ce que vous y gagnez.

| Idée | Ce que vous imaginiez peut-être | Ce que nous proposons | Pourquoi |
|---|---|---|---|
| **Vos identifiants plutôt que les nôtres** | Créer chaque modèle et chaque cabinet chez Figen, recevoir un identifiant Figen, le stocker dans ROMEO et tenir la correspondance. | Les adresses portent vos clés : le code du modèle, votre `cif_id`, votre `conseiller_id`, votre `reference_romeo`. | Aucune table de correspondance à tenir de votre côté. Un `PUT` sur votre clé crée ou met à jour, et redéposer le même contenu ne crée pas de nouvelle version (`inchange`). |
| **Des réponses immédiates plutôt que des rappels** | Déposer un modèle, puis attendre que Figen rappelle ROMEO quand le contrôle est prêt. | Le contrôle est dans la réponse du dépôt. Les changements d'état survenus entre deux appels se lisent dans `/v1/evenements`, quand vous le voulez. | ROMEO n'a pas de point d'entrée à exposer ni de rappel perdu à rattraper. Un seul sens de circulation, facile à tester. |
| **Simuler avant de publier** | Publier la nouvelle version d'un modèle de base, puis découvrir combien de cabinets se retrouvent en dérive. | `simulation: true` sur le dépôt : la réponse donne les anomalies et l'effet sur les modèles des cabinets, sans rien stocker. | Inovéa sait qui sera touché avant de publier, et peut prévenir les cabinets concernés. |
| **Se replier sur le modèle de base** | Refuser de générer tant que le modèle du cabinet porte une alerte. | Par défaut, la génération se replie sur le modèle de base et le dit (`repli_sur_base`). Avec `si_modele_non_valide`, ROMEO peut préférer refuser, ou générer malgré les alertes. | Un conseiller n'est jamais bloqué devant son client parce que le modèle de son cabinet attend une correction. Le document sort quand même, avec les mentions dans la rédaction d'Inovéa. |
| **Contrôler à blanc pendant la saisie** | Déposer le modèle pour connaître ses alertes, corriger, redéposer. | `POST /v1/controles` contrôle n'importe quel texte sans le déposer. | ROMEO peut montrer les alertes dans son éditeur, avant le dépôt. Le cabinet corrige au fil de l'eau et l'historique de ses dépôts reste propre. |
| **Rédiger les commentaires à part** | Un seul appel qui rédige le commentaire et rend le document. | Deux appels : `POST /v1/documents/blocs` rédige, `POST /v1/documents` rend. | Les temps de réponse sont maîtrisés : le rendu ne dépend pas de l'IA et répond vite (parcours 4.5). ROMEO sait quand afficher une attente, et le conseiller relit le commentaire avant qu'il n'entre dans le document. |
| **Vérifier les données sans générer** | Générer le document pour découvrir qu'il manque une donnée. | `POST /v1/documents/verifier-donnees` dit ce qui manque, dans quelles mentions, et si c'est bloquant. | ROMEO peut signaler au conseiller les champs à compléter avant de lancer la génération. L'appel se fait sans IA. |
| **Des mentions qu'un cabinet peut retirer** | Un modèle par combinaison de statuts, ou des sections laissées en zone libre pour que chaque cabinet enlève celles qui ne le concernent pas. | `presence: facultative` sur la clause : le cabinet peut la retirer en entier, pas la dénaturer. | Les modèles d'un groupement contiennent des sections qu'un cabinet doit retirer quand elles ne le concernent pas, par exemple un statut qu'il n'a pas. Il fallait le permettre sans ouvrir la porte à leur réécriture : retirée, la section est signalée pour information ; gardée, elle est contrôlée comme les autres. |

## 7. Données et confidentialité

### Ce qui est stocké

| Donnée | Ce que nous gardons |
|---|---|
| Clés d'API | Le nom, le préfixe et un hachage SHA-256 de la clé, avec ses dates. Jamais la clé elle-même. |
| Modèles de base | Chaque version, avec son contenu, ses mentions et ses variables. Une version ne change plus. |
| Modèles des cabinets | Chaque version déposée, son statut et la version de base sur laquelle elle a été validée. Un `DELETE` supprime le modèle du cabinet, ses versions et ses contrôles. |
| Contrôles | Le statut, le résumé, les alertes, le rapport et le modèle annoté. |
| Jugements | Le verdict, sa justification et le modèle d'IA, pour ne pas juger deux fois la même modification. |
| Journal des générations | Une ligne sans contenu : identifiant, qui, quand, modèle et versions, statut, empreinte. |
| Événements | Le fil des changements d'état. |
| Référentiel produits | Un extrait du référentiel des fonds et contrats, par ISIN. |

### Ce qui ne l'est pas

- **Les documents générés.** Ils partent dans la réponse et ne sont pas gardés.
- **Les données de vos clients.** Les `donnees` envoyées servent au rendu, puis disparaissent.
- **Les essais.** Un contrôle à blanc (`POST /v1/controles`), un dépôt avec `simulation: true` et une
  rédaction de commentaires (`POST /v1/documents/blocs`) n'enregistrent ni le texte envoyé ni leur résultat.
  Seuls les avis du juge sur un passage de modèle sont mémorisés : empreintes des deux textes, verdict et
  justification, laquelle peut citer quelques mots du modèle.
- **Les avis du juge sur un document.** Quand le texte jugé est un document généré ou un commentaire
  (contrôle d'un document retouché, option `verifier_blocs`), il peut contenir des données de client :
  rien n'est mémorisé, et l'avis n'existe que dans la réponse.

### Où

La machine et la base de données sont en France ; les modèles d'IA traitent dans l'Union européenne, pas
seulement en France. Le modèle utilisé est Claude Opus, appelé par Amazon Bedrock (région `eu-west-3`).

### Ce que voit l'IA

- **Au contrôle :** les passages modifiés et, pour y confronter les zones libres, les mentions obligatoires
  du modèle de base. Un texte déposé sans aucun changement ne donne lieu à aucun appel à l'IA.
- **À la rédaction d'un commentaire :** les données envoyées pour ce document, et le document déjà rendu, qui
  sert de contexte. Si vous ne voulez pas d'IA sur un document, passez `blocs_ia` à faux, ou fournissez
  vous-mêmes les textes dans `blocs`.

### Vos modèles

Les modèles d'Inovéa sont confidentiels. Aucune page publique n'en montre une ligne : un modèle ne se lit
que par des appels à l'API, avec une clé, et ce guide n'utilise que des exemples inventés.
Les essais et les démonstrations se font sur des données fictives.

### À confirmer

- La durée de conservation des modèles, des contrôles et du journal, et les modalités de suppression.

## 8. Nos engagements

### Versions, préavis, journal des changements

- **Une API versionnée.** Toutes les routes portent le préfixe `/v1`. Aucune rupture n'y sera introduite : un
  changement incompatible passe par une nouvelle version de l'API. Ajouter un champ à une réponse ou un
  code d'alerte n'est pas une rupture : décidez sur `gravite` (`bloquante` ou `information`), et traitez un
  code que vous ne connaissez pas encore selon sa gravité.
- **Un préavis.** Vous êtes prévenus avant tout changement de l'API. Le délai de préavis et le canal
  d'annonce sont à confirmer.
- **Un journal des changements.** Chaque évolution de `/v1` est consignée dans `CHANGELOG-API.md`, remis avec
  ce guide. Sur le service, il se lit à la fin de la page `/guide`.

### Environnement de test et temps de réponse

- **Une plateforme de test.** Vous disposez d'un environnement de test et de cette documentation.
  L'adresse vous est remise avec votre clé ; `/v1/moi` rappelle dans quel environnement
  une clé travaille. Les essais s'y font sur des données fictives. L'adresse de production est à confirmer.
- **Des temps de réponse maîtrisés.** Le rendu d'un document est immédiat, la rédaction par l'IA est plus
  lente : c'est pour cela que les deux sont séparés (parcours 4.5). Les réponses de génération portent leur
  durée (`duree_ms`). Le relais placé devant l'environnement de test coupe tout appel à 100 secondes
  (parcours 4.5). Les temps de réponse garantis, la disponibilité du service et le maintien de cette
  limite en production sont à confirmer.

## 9. Référence rapide

### Les routes

Authentification : `Authorization: Bearer` suivi de la clé, sur tout `/v1` sauf `/v1/sante`.

| Méthode | Route | Rôle |
|---|---|---|
| `GET` | `/v1/sante` | État du service, mode de l'IA, base de données. Sans clé. Répond aussi à `HEAD`, sans corps. |
| `GET` | `/v1/moi` | À qui appartient la clé, environnement. |
| `GET` | `/v1/catalogue-variables?code=` | Variables et blocs IA ; avec `code`, ce que ce modèle utilise. |
| `GET` | `/v1/modeles-de-base` | Liste des modèles de base et compte des modèles de cabinet par état. |
| `PUT` | `/v1/modeles-de-base/{code}` | Déposer une version (analyse, anomalies, impact). `simulation: true` ne stocke rien. |
| `GET` | `/v1/modeles-de-base/{code}?version=` | Détail d'une version ; sans `version`, la version publiée courante. |
| `PUT` | `/v1/cifs/{cif_id}/modeles/{code}` | Déposer le modèle d'un cabinet. La réponse contient le contrôle. |
| `GET` | `/v1/cifs/{cif_id}/modeles` | État de tous les modèles d'un cabinet, et lequel servirait à générer. |
| `GET` | `/v1/cifs/{cif_id}/modeles/{code}` | Détail et dernier contrôle. |
| `POST` | `/v1/cifs/{cif_id}/modeles/{code}/revalider` | Lever la dérive si le dernier contrôle est sans alerte bloquante. |
| `DELETE` | `/v1/cifs/{cif_id}/modeles/{code}` | Revenir au modèle de base : le modèle du cabinet, ses versions et ses contrôles sont supprimés. |
| `POST` | `/v1/controles` | Contrôle à blanc de n'importe quel texte. Rien n'est stocké. |
| `GET` | `/v1/controles/{id}` | Relire un contrôle stocké. |
| `POST` | `/v1/documents` | Générer un document et contrôler le rendu. |
| `POST` | `/v1/documents/blocs` | Rédiger seulement les commentaires libres, pour relecture dans ROMEO. |
| `POST` | `/v1/documents/verifier-donnees` | Dire ce qui manque dans les données, sans générer. |
| `GET` | `/v1/journal` | Journal des générations, sans contenu. Filtres : `cif_id`, `conseiller_id`, `depuis` (une date), `limite`. |
| `GET` | `/v1/evenements?depuis=` | Fil des changements d'état. `depuis` est le curseur `suivant` ; `limite` borne la réponse. |

Les schémas complets sont dans `/docs` et `/openapi.json`. Un paramètre d'adresse (`?code=`, `?version=`,
`?depuis=`) se donne avec une valeur ou s'omet. Les adresses s'écrivent sans barre finale : `/v1/moi/` rend
`404 introuvable`.

### Les valeurs admises

| Champ | Valeurs |
|---|---|
| `type` | `der`, `lettre_mission`, `mandat_courtage`, `rapport_conseil`, `declaration_adequation`, `mandat_rto`, `autre` |
| `profil` | `conseiller`, `consultant`, `tous` |
| `format` | `markdown` (par défaut), `html` |
| `mode` d'un contrôle | `modele` (par défaut), `document` |
| `si_modele_non_valide` | `modele_de_base` (par défaut), `generer`, `refuser` |
| Statut d'un commentaire | `redige`, `fourni`, `indisponible` |
| `code` d'un modèle | 2 à 63 caractères : minuscules sans accent, chiffres et tirets, sans tiret en tête |
| `cif_id`, `conseiller_id` | 1 à 64 caractères : lettres sans accent, chiffres, `.`, `_` et `-` |
| Clé d'un commentaire, dans `blocs` | 1 à 64 caractères : minuscules sans accent, chiffres et `_`, une lettre en tête |

Champs obligatoires : `type`, `profil`, `libelle` et `contenu` pour un modèle de base ; `contenu` pour le
modèle d'un cabinet ; `contenu` et `reference` (avec `code` ou `contenu`, jamais les deux) pour un contrôle ;
`code` pour les trois routes de `/v1/documents`. Tous les autres champs sont facultatifs.

### Les limites

| Quoi | Limite | Au-delà |
|---|---|---|
| Corps d'une requête | 2 Mo ; 32 niveaux d'imbrication | `413 corps_trop_grand` ; `400 requete_invalide` |
| Texte d'un modèle ou d'un document (`contenu`) | 500 000 caractères | `413 corps_trop_grand` |
| `donnees.products` | 200 produits | `400 requete_invalide` |
| Document rendu | 300 000 caractères | `422 generation_impossible` |
| `libelle`, `notes_version`, `reference_romeo` | 300, 2 000 et 128 caractères | `400 requete_invalide` |

### Les erreurs

Toutes les erreurs ont la même forme :

```json
{"erreur": {"code": "modele_non_personnalisable", "message": "…", "details": []}}
```

`code` est fait pour votre programme, `message` pour un humain, `details` précise quand il y a lieu.

| HTTP | Code d'erreur | Quand |
|---|---|---|
| `400` | `requete_invalide` | Corps ou paramètres mal formés : JSON illisible, champ obligatoire absent, champ inconnu, valeur non admise, donnée mal formée. `details` nomme chaque champ refusé (`champ`, `emplacement`, `message`). |
| `400` | `gabarit_invalide` | Modèle refusé pour sa forme : au dépôt d'un modèle de base, une anomalie de gravité `erreur`. `details` liste les anomalies. Rien n'est stocké. |
| `400` | `format_incoherent` | Le `format` annoncé contredit le contenu : du Markdown annoncé `html`, par exemple. |
| `401` | `cle_absente`, `cle_invalide` | En-tête `Authorization` absent ou mal formé ; clé inconnue ou révoquée. |
| `404` | `introuvable` | Adresse, modèle de base, version, modèle de cabinet ou contrôle inconnu. `details` dit lequel (`ressource`). |
| `405` | `methode_non_permise` | Méthode HTTP non prévue sur cette adresse. |
| `409` | `modele_non_personnalisable` | Un cabinet dépose un modèle alors que le modèle de base ne l'autorise pas. |
| `409` | `revalidation_impossible` | Revalidation demandée alors que le dernier contrôle porte une alerte bloquante, ou un passage que le juge n'a pas pu juger. `details` donne le contrôle et le nombre d'alertes bloquantes. |
| `409` | `depots_simultanes` | Le même modèle est modifié par plusieurs appels à la fois : rejouez l'appel. |
| `413` | `corps_trop_grand` | Corps de plus de 2 Mo, ou texte de plus de 500 000 caractères. |
| `422` | `modele_non_valide` | Génération demandée avec `si_modele_non_valide: "refuser"` alors que le modèle du cabinet n'est pas valide. |
| `422` | `generation_impossible` | Le modèle ne peut pas être rendu avec ces données : un document de plus de 300 000 caractères, par exemple. `details` dit pourquoi. |
| `500` | `erreur_interne` | Défaut du service. `details` porte l'identifiant de la requête, à nous transmettre. |
| `503` | `service_degrade` | `/v1/sante` seulement, quand la base de données ne répond pas. |

Dans tous les cas, `message` dit ce qui ne va pas. Le `503` de `/v1/sante` garde le corps habituel de la
route, avec `"statut": "degrade"`, et y ajoute l'objet `erreur`. Une réponse qui vient du relais, et non de
l'API, ne suit pas cette forme : une coupure (délai dépassé ou code `524`, parcours 4.5), par exemple.

Chaque réponse porte un en-tête `X-Request-Id` : le vôtre si vous en envoyez un (1 à 64 lettres sans accent,
chiffres, `.`, `_` ou `-`), sinon celui que l'API crée. Gardez-le, et joignez-le à toute question.
