{
  "contract_version": "gab_083_contract_v1",
  "gab_id": "GAB-083",
  "canonical_name": "VisualAnnotatedImage",
  "module_owner": "EdTechVisualLearning",
  "renderer_key": "text_cta",
  "required_fields": [
    "gab_id",
    "visual_annotated_id",
    "image",
    "items",
    "default_caption"
  ],
  "optional_fields": [
    "title",
    "_note_dev"
  ],
  "field_types": {
    "visual_annotated_id": "string — identifiant unique de l'instance",
    "image": "object{src:string, alt:string, kind:enum['svg_css','svg','png','jpg','webp']}",
    "default_caption": "string — texte affiché avant toute sélection",
    "items": "array<{id:string, label:string, caption:string, position:object}> — min 1 item",
    "items[].id": "string — identifiant unique du label",
    "items[].label": "string — texte de l'étiquette affichée sur l'image",
    "items[].caption": "string — description révélée au clic sur l'étiquette",
    "items[].position": "object{top?,left?,right?,bottom?,transform?} — positionnement CSS absolu"
  },
  "constraints": [
    "items doit contenir au moins 1 élément.",
    "Chaque item.id doit être unique dans l'instance.",
    "image.alt est obligatoire pour l'accessibilité (WCAG AA).",
    "Les positions sont des valeurs CSS (ex: '20%', '8px') ; le moteur les applique via style inline.",
    "Le moteur ne doit jamais hardcoder labels ou captions — tout vient de items[]."
  ],
  "blocked_conditions": [
    "image absent ou image.src absent",
    "items absent ou items vide (length === 0)",
    "default_caption absent"
  ],
  "accessibility": [
    "keyboard_navigable — chaque label est un bouton focusable",
    "focus_visible — outline visible sur focus clavier",
    "prefers_reduced_motion — transitions réduites si préférence système",
    "image_alt — alt obligatoire sur l'image de fond",
    "aria_live — zone caption annoncée en aria-live pour les lecteurs d'écran"
  ],
  "qa_cases": [
    { "case": "instance conforme", "expected": "rendu complet, 3 labels cliquables, caption change au clic" },
    { "case": "champ requis manquant (items vide)", "expected": "BLOCKED listant 'items absent ou vide'" },
    { "case": "image absent", "expected": "BLOCKED listant 'image absent'" },
    { "case": "clic label → caption change", "expected": "caption du label cliqué s'affiche, label devient actif" },
    { "case": "instance externe injectée via init(ext)", "expected": "rendu change sans modifier le HTML" },
    { "case": "responsive 375/768/1024", "expected": "aucun débordement horizontal, labels positionnés lisiblement" }
  ],
  "traceability": {
    "derived_from_core_gab": "GAB-083",
    "note": "Ce schema VALIDE l'instance. Le contrat pédagogique complet (input_contract/validation_logic/feedback_scoring_logic) vit dans le CORE-GAB officiel, pas ici (évite la duplication)."
  }
}
