### **3. SDK Client**

Le SDK s'exécute chez les utilisateurs finaux de nos clients, dans des pages que nous ne contrôlons pas. Sa règle cardinale en découle : **une défaillance du SDK ne doit jamais être visible autrement que par l'absence de guidage.** Aucune exception ne remonte à la page hôte, aucune erreur n'est affichée, rien n'est jamais bloqué.

#### **3.1. Intégration et amorçage**

Le client insère une seule ligne :

```html
<script src="https://cdn.circuy.net/v1/sdk.js" data-key="pk_live_a1b2c3" async></script>
```

Le SDK expose une fonction globale acceptant les appels émis avant son chargement, via une file d'attente installée par le snippet :

```javascript
// Attributs de ciblage fournis par le site hôte, jamais transmis au serveur
circuy('identify', { role: 'admin', locale: 'fr' });

// Démarrage manuel d'un parcours, en complément du déclenchement automatique
circuy('start', 'tr_7f3a9c');
```

**Séquence d'amorçage**

1. Lecture de la clé publique depuis l'attribut `data-key` de sa propre balise.
2. Attente de l'inactivité du fil principal, par `requestIdleCallback` avec repli sur un `setTimeout` court. Rien n'est exécuté pendant le rendu initial de la page.
3. Récupération du manifeste des parcours actifs du projet (section 7.2).
4. Évaluation des déclencheurs contre l'URL courante et les attributs déclarés.
5. Si un parcours correspond, récupération de sa version publiée, puis exécution.

**Budget de performance.** Le seuil de 15 Ko du cahier fonctionnel est remplacé par un budget mesuré (ADR-02) :

| Indicateur | Seuil | Contrôle |
| --- | --- | --- |
| Poids du bundle compressé | 40 Ko, alerte à 35 Ko | Mesuré à chaque compilation, dépassement bloquant en intégration continue |
| Décalage cumulé de mise en page | strictement nul | Tout est en position fixe dans la racine fantôme, aucun élément n'entre dans le flux du document |
| Travail avant interactivité | nul | Aucune exécution avant inactivité du fil principal |

Le poids est un indicateur surveillé, pas une contrainte contractuelle : sa dérive doit être visible et justifiée, non interdite par principe.

#### **3.2. Moteur de rendu**

**Point d'ancrage unique.** Le SDK insère un seul élément en fin de `body`, porteur d'une racine fantôme ouverte (ADR-04). Tout ce que le SDK affiche vit à l'intérieur. Rien n'est jamais inséré ailleurs dans le document du client.

**Styles.** La feuille de style est fournie au moment de la compilation sous forme de chaîne et adoptée par la racine fantôme via `adoptedStyleSheets`. Aucune balise de style n'est ajoutée au document hôte. Le thème est exposé en propriétés CSS personnalisées, ce qui permettra une personnalisation par client sans recompiler le SDK.

**Positionnement.** `@floating-ui/dom` fournit `computePosition` avec les intergiciels `offset`, `flip`, `shift` et `arrow`, ainsi que `autoUpdate`, qui réaligne la bulle au défilement, au redimensionnement et aux changements de taille de la cible. C'est la partie réellement difficile du problème, et elle n'est pas réécrite.

**Mise en évidence.** Lorsque `highlight` est vrai, l'élément cible est mis en avant par un cadre positionné sur ses coordonnées et portant une ombre portée de très grande étendue, qui assombrit tout le reste de la page :

```css
box-shadow: 0 0 0 9999px rgba(15, 23, 42, 0.55);
```

Cette technique tient en une déclaration, suit la cible sans calcul supplémentaire et laisse l'élément cliquable puisque le voile n'est pas au-dessus de lui. Sa limite est connue et acceptée : la découpe est un rectangle à coins arrondis, pas une forme quelconque. Le recours à un tracé SVG n'interviendra que si un besoin de forme complexe apparaît, ce qui n'est pas le cas aujourd'hui.

#### **3.3. Résolution de cible et auto-healing**

C'est le mécanisme qui porte le différenciateur produit. Il consomme la liste ordonnée de candidats définie en section 2.2.

**Algorithme.** Les candidats sont essayés dans l'ordre du tableau. Un candidat est retenu si et seulement s'il désigne **exactement un** élément à la fois présent dans le document et visible, la visibilité étant établie par un rectangle de dimensions non nulles et l'absence de `display: none` ou de `visibility: hidden`. Zéro ou plusieurs correspondances font passer au candidat suivant.

Cette exigence d'unicité est le garde-fou central : dans ce type de produit, désigner le mauvais élément avec assurance est bien pire que ne rien désigner du tout.

**Attente des éléments asynchrones.** Si aucun candidat n'aboutit, le SDK attend l'apparition de la cible au moyen d'un `MutationObserver`, pendant cinq secondes au maximum. Deux précautions encadrent cette surveillance :

- L'observateur n'est actif **que pendant une attente**. Il n'observe jamais le document en continu, ce qui serait un coût permanent imposé au site du client sur toutes ses pages.
- Les notifications sont regroupées sur une trame d'animation avant réévaluation, afin qu'une application qui remanie massivement son arbre ne déclenche pas des milliers de résolutions.

**Échec de résolution.** Au terme du délai, l'étape est **sautée** et un événement `target_lost` est émis, portant l'identifiant de l'étape concernée. Le parcours continue à l'étape suivante ; si plus aucune étape n'est résoluble, il se termine.

Ce choix est délibéré : un parcours partiel vaut mieux qu'un parcours mort, et surtout l'événement `target_lost` remonte au dashboard le fait qu'un sélecteur a cédé. C'est ce qui referme la boucle produit, en transformant une rupture silencieuse en une réparation identifiée (section 6.3).

#### **3.4. Machine d'exécution**

Un parcours est à tout instant dans l'un de quatre états : inactif, en cours, terminé ou abandonné. Une seule instance est active à la fois ; si plusieurs parcours correspondent à l'URL, celui dont l'identifiant est le plus ancien l'emporte, et les autres sont ignorés pour cette page.

**Passage à l'étape suivante.** Il est commandé par le champ `advance` : appui sur le bouton, clic sur l'élément cible, ou première saisie dans cet élément. Dans les deux derniers cas, l'écouteur est posé sur l'élément résolu et retiré dès le passage à l'étape suivante.

**Navigation multi-pages et applications monopages.** Le SDK réévalue les déclencheurs à chaque changement d'URL. La détection utilise l'API de navigation du navigateur lorsqu'elle est disponible, et se replie sinon sur l'écoute de `popstate` complétée par une interception de `pushState` et `replaceState`.

Cette interception est une intrusion dans le contexte du site hôte, et elle est traitée comme telle : les fonctions d'origine sont conservées et systématiquement appelées, le SDK se contentant d'être notifié. C'est le seul point du SDK qui modifie un objet du site client, et il est isolé dans un module unique.

Lors d'un rechargement complet de page au milieu d'un parcours, la reprise s'appuie sur l'état persisté (section 3.5), puis sur le mécanisme d'attente de la section 3.3 pour laisser à la nouvelle page le temps de produire l'élément attendu.

#### **3.5. Persistance locale**

L'état est conservé dans `localStorage`, sous un espace de noms préfixé afin de ne jamais entrer en collision avec les clés du site hôte.

| Clé | Contenu | Durée |
| --- | --- | --- |
| `circuy:{cle}:{tourId}` | `{ stepId, version, updatedAt }` | 24 heures |
| `circuy:{cle}:seen` | Identifiants des parcours terminés ou refusés | Sans expiration |

**Règles de reprise.** À l'amorçage, un état de plus de 24 heures est effacé et le parcours est traité comme neuf. Un état dont le champ `version` diffère de la version publiée courante est également effacé : reprendre à une étape d'une version antérieure conduirait à un guidage incohérent.

**Plafonnement de fréquence.** Le mode `once` s'appuie sur la clé `seen`, purement locale. Il n'existe **aucun identifiant d'utilisateur, ni côté client ni côté serveur** : la promesse est tenue par navigateur, ce qui est le comportement attendu et ce qui évite d'introduire un traceur persistant dans un produit vendu sur la conformité (section 8.6).

**Indisponibilité du stockage.** En navigation privée ou en cas de quota atteint, toute écriture est encapsulée et son échec ignoré. Le SDK bascule alors sur un état en mémoire : le parcours fonctionne pour la page courante et ne survit pas au rechargement, ce qui est une dégradation acceptable.

#### **3.6. Télémétrie**

**Événements émis :** `start`, `step_view`, `choice`, `complete`, `dismiss`, `target_lost`. Cette liste est fermée ; elle correspond exactement aux mesures du chapitre 6 et ne contient rien qui ne soit restitué quelque part.

**Transport.** Les événements sont accumulés en mémoire et expédiés par lots au moyen de `navigator.sendBeacon`, déclenché au passage de la page en arrière-plan (`visibilitychange`) et à la fin du parcours. Le regroupement évite une requête par étape ; le déclenchement sur `visibilitychange` est ce qui permet de ne pas perdre les données d'un utilisateur qui ferme son onglet. En l'absence de `sendBeacon`, le repli est un `fetch` avec l'option `keepalive`.

**Contenu.** Un événement ne transporte que des identifiants techniques et un identifiant de session aléatoire, régénéré à chaque session et conservé en `sessionStorage`. Aucun attribut fourni par `identify` n'est transmis : ces attributs servent uniquement au ciblage local.

**Échec.** Toute erreur d'envoi est absorbée sans nouvelle tentative. Perdre une mesure est sans conséquence ; ralentir la navigation d'un utilisateur pour la sauver en aurait une.

#### **3.7. Accessibilité**

L'accessibilité n'est pas négociable : un outil d'onboarding qui s'impose au premier plan et piège un utilisateur au clavier est un défaut bloquant.

- **Rôles.** Les modales et les questions sont annoncées comme boîtes de dialogue, avec un titre accessible rattaché. Les infobulles sont annoncées comme telles.
- **Focus.** À l'affichage d'une étape, le focus est déplacé dans la bulle ; à sa fermeture, il est restitué à l'élément qui le détenait auparavant.
- **Piège de focus, avec une exception déterminante.** Le focus est confiné dans la bulle pour les types `modal` et `question`, qui attendent une réponse. Il ne l'est **jamais** pour une infobulle dont `advance` vaut `element-click` ou `element-input` : confiner le focus empêcherait l'utilisateur d'atteindre l'élément que l'étape lui demande précisément d'utiliser.
- **Clavier.** La touche d'échappement abandonne le parcours et émet `dismiss`. Les boutons de la bulle sont atteignables par tabulation, dans l'ordre visuel.
- **Mouvement.** Lorsque `prefers-reduced-motion` est déclaré, le défilement vers la cible est immédiat et les transitions sont supprimées.
- **Contraste.** Le thème par défaut respecte le niveau AA des règles pour l'accessibilité des contenus web sur le texte comme sur les bordures de mise en évidence.
