# GAB-230 · SmartSelectEmptyChoice — « État vide honnête anti-fake »

**Archétype / renderer_key :** `text_cta` (cartographie) · **module :** EdTechSmartSelect
**Critère validé :** changer le JSON change le rendu sans modifier le HTML. ✅ check.py 12/12.

## Pack (structure officielle par-GAB)
```
GAB-230/
  renderer.html            ← moteur état vide (ne pas modifier par instance)
  instance.example.json    ← SOURCE DE VÉRITÉ (contenu réel, à plat)
  schema.contract.json     ← contrat de validation
  README-contract.md       ← ce fichier
```

## Archétype pédagogique
GAB-230 est l'**état vide honnête** de SmartSelect : quand aucun candidat fiable n'existe,
le moteur refuse de générer une sélection inventée. Il affiche :
1. Un message clair expliquant pourquoi il n'y a pas de sélection.
2. Un bloc doctrine **anti-fake content** (parallèle à GAB-209 SessionQualityGuard côté PlayEngine).
3. Des **actions de secours dignes** (fiche générale, mascotte Ketty, catalogue libre).
4. Un enum `empty_reason` sur 5 valeurs pour distinguer les cas de vacuité.

## Distinction clé (garde-fou)
**GAB-230 ≠ ErrorFallbackBlock** :
- `EmptyChoice` (GAB-230) = absence **légitime** de candidat fiable → log QA "empty_choice"
- `ErrorFallbackBlock` = erreur **technique** (bug, 500, indexation cassée) → log QA "error"

Ces deux cas doivent produire des logs séparés en production pour distinguer un trou de catalogue
d'un bug d'indexation.

## Champs requis (instance, à plat)
`gab_id` · `empty_choice_id` · `title` · `body`

Optionnels : `empty_reason` · `anti_fake_label` · `anti_fake_text` · `fallback_options[]{icon,title,description,action}` · `distinction_note` · `primary_cta{label,action}` · `secondary_cta{label,action}`

## Ce qui vient du JSON vs HTML
- **JSON** : titre, corps, label et texte doctrine anti-fake, chaque option de secours (icon/titre/description), reason active, distinction note, libellés des CTAs.
- **HTML** : layout carte dashed-gold, bloc anti-fake statique, liste options, enum chips (lecture seule), CTAs structurels, moteur init.

## Garde-fous (child_safety / anti-invention)
- **Anti-fake absolu** : le renderer n'insère aucun contenu pédagogique en dur. Si `fallback_options` est vide, la zone est masquée proprement.
- **BLOCKED** si `title` absent ou `body` absent.
- **empty_reason** = enum strict 5 valeurs ; valeur inconnue → aucun chip activé, pas de crash.
- Log QA production doit distinguer `empty_choice` de `error_fallback` (deux pipelines séparés).

## QA à vérifier
1. Modifier `title`/`body`/`fallback_options` → rendu change sans toucher au HTML (critère d'or).
2. `title` absent → BLOCKED propre.
3. `fallback_options: []` → zone options masquée, pas d'erreur JS.
4. `empty_reason: "no_level_fit"` → chip `no_level_fit` activé.
5. `ENGINE.init(autreInstance)` → rendu change sans rechargement.
6. Responsive 375/768/1024.

## external_refs / dependencies
- **GAB-209 SessionQualityGuard** (PlayEngine) : parallèle doctrinal — même principe anti-fake côté exécution.
- **GAB-224** (SmartSelect) : prend le relais quand une ressource prof est indisponible (notice claire), différent de l'EmptyChoice.
- **Ketty (EdTechChat)** : l'action `open_ketty_chat` dans `fallback_options` fait référence à la mascotte IA. Le renderer ne l'implémente pas — c'est la couche applicative qui interprète l'`action`.

## Source
`INDEX-300-smartselect-GAB-226-230-PLAYABLE.html` (stage `data-tpl="230"`, bloc `.ec-card`, CSS `.ec-*`).
